Compare commits

..

6 Commits

Author SHA1 Message Date
3e4558abc5 v2.1.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-15 20:15:12 +00:00
3e0eb5e198 feat(cli): add global remote builder configuration and native SSH buildx nodes for multi-platform builds 2026-03-15 20:15:12 +00:00
732e9e5cac v2.0.2
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 4m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-12 20:11:47 +00:00
5bf1779243 fix(repo): no changes to commit 2026-03-12 20:11:47 +00:00
4908c21b84 v2.0.1
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Failing after 4m8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-12 20:02:50 +00:00
b1e2f0d8ea fix(repository): no changes to commit 2026-03-12 20:02:50 +00:00
11 changed files with 2913 additions and 3100 deletions

View File

@@ -1,5 +1,21 @@
# Changelog # Changelog
## 2026-03-15 - 2.1.0 - feat(cli)
add global remote builder configuration and native SSH buildx nodes for multi-platform builds
- adds a new `tsdocker config` command with subcommands to add, remove, list, and show remote builder definitions
- introduces global config support for remote builders stored under `~/.git.zone/tsdocker/config.json`
- builds can now create multi-node buildx setups with remote SSH builders and open reverse SSH tunnels so remote nodes can push to the local staging registry
- updates the README and CLI help to document remote builder configuration and native cross-platform build workflows
## 2026-03-12 - 2.0.2 - fix(repo)
no changes to commit
## 2026-03-12 - 2.0.1 - fix(repository)
no changes to commit
## 2026-03-12 - 2.0.0 - BREAKING CHANGE(cli) ## 2026-03-12 - 2.0.0 - BREAKING CHANGE(cli)
remove legacy container test runner and make the default command show the man page remove legacy container test runner and make the default command show the man page

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsdocker", "name": "@git.zone/tsdocker",
"version": "2.0.0", "version": "2.1.0",
"private": false, "private": false,
"description": "develop npm modules cross platform with docker", "description": "develop npm modules cross platform with docker",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -27,22 +27,22 @@
}, },
"homepage": "https://gitlab.com/gitzone/tsdocker#readme", "homepage": "https://gitlab.com/gitzone/tsdocker#readme",
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.6", "@git.zone/tstest": "^3.3.2",
"@types/node": "^25.0.9" "@types/node": "^25.5.0"
}, },
"dependencies": { "dependencies": {
"@push.rocks/lik": "^6.2.2", "@push.rocks/lik": "^6.3.1",
"@push.rocks/npmextra": "^5.3.3", "@push.rocks/npmextra": "^5.3.3",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartcli": "^4.0.20", "@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartfs": "^1.3.1", "@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartinteract": "^2.0.16", "@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartlog-destination-local": "^9.0.2", "@push.rocks/smartlog-destination-local": "^9.0.2",
"@push.rocks/smartlog-source-ora": "^1.0.9", "@push.rocks/smartlog-source-ora": "^1.0.9",
"@push.rocks/smartshell": "^3.3.0" "@push.rocks/smartshell": "^3.3.7"
}, },
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34", "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"type": "module", "type": "module",

5549
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -93,6 +93,7 @@ tsdocker push --no-build Dockerfile_api Dockerfile_web
| `tsdocker test` | Build + run container test scripts (`test_*.sh`) | | `tsdocker test` | Build + run container test scripts (`test_*.sh`) |
| `tsdocker login` | Authenticate with configured registries | | `tsdocker login` | Authenticate with configured registries |
| `tsdocker list` | Display discovered Dockerfiles and their dependencies | | `tsdocker list` | Display discovered Dockerfiles and their dependencies |
| `tsdocker config` | Manage global tsdocker configuration (remote builders, etc.) |
| `tsdocker clean` | Interactively clean Docker environment | | `tsdocker clean` | Interactively clean Docker environment |
### Build Flags ### Build Flags
@@ -117,6 +118,24 @@ tsdocker push --no-build Dockerfile_api Dockerfile_web
| `--registry=<url>` | Push to a single specific registry instead of all configured | | `--registry=<url>` | Push to a single specific registry instead of all configured |
| `--no-build` | Skip the build phase; only push existing images from local registry | | `--no-build` | Skip the build phase; only push existing images from local registry |
### Config Subcommands
| Subcommand | Description |
|------------|-------------|
| `add-builder` | Add or update a remote builder node |
| `remove-builder` | Remove a remote builder by name |
| `list-builders` | List all configured remote builders |
| `show` | Show the full global configuration |
**`add-builder` flags:**
| Flag | Description |
|------|-------------|
| `--name=<name>` | Builder name (e.g. `arm64-builder`) |
| `--host=<user@ip>` | SSH host (e.g. `armbuilder@192.168.1.100`) |
| `--platform=<p>` | Target platform (e.g. `linux/arm64`) |
| `--ssh-key=<path>` | SSH key path (optional, uses SSH agent/config by default) |
### Clean Flags ### Clean Flags
| Flag | Description | | Flag | Description |
@@ -294,6 +313,51 @@ tsdocker automatically:
- Pushes multi-platform images to the local registry via `buildx --push` - Pushes multi-platform images to the local registry via `buildx --push`
- Copies the full manifest list (including all platform variants) to remote registries on `tsdocker push` - Copies the full manifest list (including all platform variants) to remote registries on `tsdocker push`
### 🖥️ Native Remote Builders
Instead of relying on slow QEMU emulation for cross-platform builds, tsdocker can use **native remote machines** via SSH as build nodes. For example, use a real arm64 machine for `linux/arm64` builds:
```bash
# Add a remote arm64 builder
tsdocker config add-builder \
--name=arm64-builder \
--host=armbuilder@192.168.1.100 \
--platform=linux/arm64 \
--ssh-key=~/.ssh/id_ed25519
# List configured builders
tsdocker config list-builders
# Remove a builder
tsdocker config remove-builder --name=arm64-builder
# Show full global config
tsdocker config show
```
Global configuration is stored at `~/.git.zone/tsdocker/config.json`.
**How it works:**
When remote builders are configured and the project's `platforms` includes a matching platform, tsdocker automatically:
1. Creates a **multi-node buildx builder** — local node for `linux/amd64`, remote SSH node for `linux/arm64`
2. Opens **SSH reverse tunnels** so the remote builder can push to the local staging registry
3. Builds natively on each platform's hardware — no QEMU overhead
4. Tears down tunnels after the build completes
```
[Local machine] [Remote arm64 machine]
registry:2 on localhost:PORT <──── SSH reverse tunnel ──── localhost:PORT
BuildKit (amd64) ──push──> BuildKit (arm64) ──push──>
localhost:PORT localhost:PORT (tunneled)
```
**Prerequisites for the remote machine:**
- Docker installed and running
- A user with Docker group access (no sudo needed)
- SSH key access configured
### ⚡ Parallel Builds ### ⚡ Parallel Builds
Speed up builds by building independent images concurrently: Speed up builds by building independent images concurrently:

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsdocker', name: '@git.zone/tsdocker',
version: '2.0.0', version: '2.1.0',
description: 'develop npm modules cross platform with docker' description: 'develop npm modules cross platform with docker'
} }

View File

@@ -266,12 +266,15 @@ export class Dockerfile {
public static async buildDockerfiles( public static async buildDockerfiles(
sortedArrayArg: Dockerfile[], sortedArrayArg: Dockerfile[],
session: TsDockerSession, session: TsDockerSession,
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number }, options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number; onRegistryStarted?: () => Promise<void>; onBeforeRegistryStop?: () => Promise<void> },
): Promise<Dockerfile[]> { ): Promise<Dockerfile[]> {
const total = sortedArrayArg.length; const total = sortedArrayArg.length;
const overallStart = Date.now(); const overallStart = Date.now();
await Dockerfile.startLocalRegistry(session, options?.isRootless); await Dockerfile.startLocalRegistry(session, options?.isRootless);
if (options?.onRegistryStarted) {
await options.onRegistryStarted();
}
try { try {
if (options?.parallel) { if (options?.parallel) {
@@ -351,6 +354,9 @@ export class Dockerfile {
} }
} }
} finally { } finally {
if (options?.onBeforeRegistryStop) {
await options.onBeforeRegistryStop();
}
await Dockerfile.stopLocalRegistry(session); await Dockerfile.stopLocalRegistry(session);
} }

View File

@@ -0,0 +1,76 @@
import * as fs from 'fs';
import * as plugins from './tsdocker.plugins.js';
import { logger } from './tsdocker.logging.js';
import type { IGlobalConfig, IRemoteBuilder } from './interfaces/index.js';
const CONFIG_DIR = plugins.path.join(
process.env.HOME || process.env.USERPROFILE || '~',
'.git.zone',
'tsdocker',
);
const CONFIG_PATH = plugins.path.join(CONFIG_DIR, 'config.json');
const DEFAULT_CONFIG: IGlobalConfig = {
remoteBuilders: [],
};
export class GlobalConfig {
static getConfigPath(): string {
return CONFIG_PATH;
}
static load(): IGlobalConfig {
try {
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
const parsed = JSON.parse(raw);
return {
...DEFAULT_CONFIG,
...parsed,
};
} catch {
return { ...DEFAULT_CONFIG };
}
}
static save(config: IGlobalConfig): void {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8');
}
static addBuilder(builder: IRemoteBuilder): void {
const config = GlobalConfig.load();
const existing = config.remoteBuilders.findIndex((b) => b.name === builder.name);
if (existing >= 0) {
config.remoteBuilders[existing] = builder;
logger.log('info', `Updated remote builder: ${builder.name}`);
} else {
config.remoteBuilders.push(builder);
logger.log('info', `Added remote builder: ${builder.name}`);
}
GlobalConfig.save(config);
}
static removeBuilder(name: string): void {
const config = GlobalConfig.load();
const before = config.remoteBuilders.length;
config.remoteBuilders = config.remoteBuilders.filter((b) => b.name !== name);
if (config.remoteBuilders.length < before) {
logger.log('info', `Removed remote builder: ${name}`);
} else {
logger.log('warn', `Remote builder not found: ${name}`);
}
GlobalConfig.save(config);
}
static getBuilders(): IRemoteBuilder[] {
return GlobalConfig.load().remoteBuilders;
}
/**
* Returns remote builders that match any of the requested platforms
*/
static getBuildersForPlatforms(platforms: string[]): IRemoteBuilder[] {
const builders = GlobalConfig.getBuilders();
return builders.filter((b) => platforms.includes(b.platform));
}
}

77
ts/classes.sshtunnel.ts Normal file
View File

@@ -0,0 +1,77 @@
import * as plugins from './tsdocker.plugins.js';
import { logger } from './tsdocker.logging.js';
import type { IRemoteBuilder } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
/**
* Manages SSH reverse tunnels for remote builder nodes.
* Opens tunnels so that the local staging registry (localhost:<port>)
* is accessible as localhost:<port> on each remote machine.
*/
export class SshTunnelManager {
private tunnelPids: number[] = [];
/**
* Opens a reverse SSH tunnel to make localPort accessible on the remote machine.
* ssh -f -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes
* -R <localPort>:localhost:<localPort> [-i keyPath] user@host
*/
async openTunnel(builder: IRemoteBuilder, localPort: number): Promise<void> {
const keyOpt = builder.sshKeyPath ? `-i ${builder.sshKeyPath} ` : '';
const cmd = [
'ssh -f -N',
'-o StrictHostKeyChecking=no',
'-o ExitOnForwardFailure=yes',
`-R ${localPort}:localhost:${localPort}`,
`${keyOpt}${builder.host}`,
].join(' ');
logger.log('info', `Opening SSH tunnel to ${builder.host} for port ${localPort}...`);
const result = await smartshellInstance.exec(cmd);
if (result.exitCode !== 0) {
throw new Error(
`Failed to open SSH tunnel to ${builder.host}: ${result.stderr || 'unknown error'}`
);
}
// Find the PID of the tunnel process we just started
const pidResult = await smartshellInstance.exec(
`pgrep -f "ssh.*-R ${localPort}:localhost:${localPort}.*${builder.host}" | tail -1`
);
if (pidResult.exitCode === 0 && pidResult.stdout.trim()) {
const pid = parseInt(pidResult.stdout.trim(), 10);
if (!isNaN(pid)) {
this.tunnelPids.push(pid);
logger.log('ok', `SSH tunnel to ${builder.host} established (PID ${pid})`);
}
}
}
/**
* Opens tunnels for all provided remote builders
*/
async openTunnels(builders: IRemoteBuilder[], localPort: number): Promise<void> {
for (const builder of builders) {
await this.openTunnel(builder, localPort);
}
}
/**
* Closes all tunnel processes
*/
async closeAll(): Promise<void> {
for (const pid of this.tunnelPids) {
try {
process.kill(pid, 'SIGTERM');
logger.log('info', `Closed SSH tunnel (PID ${pid})`);
} catch {
// Process may have already exited
}
}
this.tunnelPids = [];
}
}

View File

@@ -8,7 +8,9 @@ import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js'; import { DockerContext } from './classes.dockercontext.js';
import { TsDockerSession } from './classes.tsdockersession.js'; import { TsDockerSession } from './classes.tsdockersession.js';
import { RegistryCopy } from './classes.registrycopy.js'; import { RegistryCopy } from './classes.registrycopy.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js'; import { GlobalConfig } from './classes.globalconfig.js';
import { SshTunnelManager } from './classes.sshtunnel.js';
import type { ITsDockerConfig, IBuildCommandOptions, IRemoteBuilder } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
@@ -24,6 +26,8 @@ export class TsDockerManager {
public dockerContext: DockerContext; public dockerContext: DockerContext;
public session!: TsDockerSession; public session!: TsDockerSession;
private dockerfiles: Dockerfile[] = []; private dockerfiles: Dockerfile[] = [];
private activeRemoteBuilders: IRemoteBuilder[] = [];
private sshTunnelManager?: SshTunnelManager;
constructor(config: ITsDockerConfig) { constructor(config: ITsDockerConfig) {
this.config = config; this.config = config;
@@ -235,6 +239,7 @@ export class TsDockerManager {
const total = toBuild.length; const total = toBuild.length;
const overallStart = Date.now(); const overallStart = Date.now();
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless); await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
await this.openRemoteTunnels();
try { try {
if (options?.parallel) { if (options?.parallel) {
@@ -332,6 +337,7 @@ export class TsDockerManager {
} }
} }
} finally { } finally {
await this.closeRemoteTunnels();
await Dockerfile.stopLocalRegistry(this.session); await Dockerfile.stopLocalRegistry(this.session);
} }
@@ -347,6 +353,8 @@ export class TsDockerManager {
isRootless: this.dockerContext.contextInfo?.isRootless, isRootless: this.dockerContext.contextInfo?.isRootless,
parallel: options?.parallel, parallel: options?.parallel,
parallelConcurrency: options?.parallelConcurrency, parallelConcurrency: options?.parallelConcurrency,
onRegistryStarted: () => this.openRemoteTunnels(),
onBeforeRegistryStop: () => this.closeRemoteTunnels(),
}); });
} }
@@ -373,13 +381,76 @@ export class TsDockerManager {
} }
/** /**
* Ensures Docker buildx is set up for multi-architecture builds * Ensures Docker buildx is set up for multi-architecture builds.
* When remote builders are configured in the global config, creates a multi-node
* builder with native nodes instead of relying on QEMU emulation.
*/ */
private async ensureBuildx(): Promise<void> { private async ensureBuildx(): Promise<void> {
const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || ''); const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || '');
const platforms = this.config.platforms?.join(', ') || 'default'; const platforms = this.config.platforms?.join(', ') || 'default';
logger.log('info', `Setting up Docker buildx [${platforms}]...`); logger.log('info', `Setting up Docker buildx [${platforms}]...`);
logger.log('info', `Builder: ${builderName}`); logger.log('info', `Builder: ${builderName}`);
// Check for remote builders matching our target platforms
const requestedPlatforms = this.config.platforms || ['linux/amd64'];
const remoteBuilders = GlobalConfig.getBuildersForPlatforms(requestedPlatforms);
if (remoteBuilders.length > 0) {
await this.ensureBuildxWithRemoteNodes(builderName, requestedPlatforms, remoteBuilders);
} else {
await this.ensureBuildxLocal(builderName);
}
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
}
/**
* Creates a multi-node buildx builder with local + remote SSH nodes.
*/
private async ensureBuildxWithRemoteNodes(
builderName: string,
requestedPlatforms: string[],
remoteBuilders: IRemoteBuilder[],
): Promise<void> {
const remotePlatforms = new Set(remoteBuilders.map((b) => b.platform));
const localPlatforms = requestedPlatforms.filter((p) => !remotePlatforms.has(p));
logger.log('info', `Remote builders: ${remoteBuilders.map((b) => `${b.name} (${b.platform} @ ${b.host})`).join(', ')}`);
if (localPlatforms.length > 0) {
logger.log('info', `Local platforms: ${localPlatforms.join(', ')}`);
}
// Always recreate the builder to ensure correct node topology
await smartshellInstance.execSilent(`docker buildx rm ${builderName} 2>/dev/null || true`);
// Create the local node
const localPlatformFlag = localPlatforms.length > 0 ? ` --platform ${localPlatforms.join(',')}` : '';
await smartshellInstance.exec(
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host${localPlatformFlag} --use`
);
// Append remote nodes
for (const builder of remoteBuilders) {
logger.log('info', `Appending remote node: ${builder.name} (${builder.platform}) via ssh://${builder.host}`);
const appendResult = await smartshellInstance.exec(
`docker buildx create --append --name ${builderName} --driver docker-container --driver-opt network=host --platform ${builder.platform} --node ${builder.name} ssh://${builder.host}`
);
if (appendResult.exitCode !== 0) {
throw new Error(`Failed to append remote builder ${builder.name}: ${appendResult.stderr}`);
}
}
// Bootstrap all nodes
await smartshellInstance.exec('docker buildx inspect --bootstrap');
// Store active remote builders for SSH tunnel setup during build
this.activeRemoteBuilders = remoteBuilders;
}
/**
* Creates a single-node local buildx builder (original behavior, uses QEMU for cross-platform).
*/
private async ensureBuildxLocal(builderName: string): Promise<void> {
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`); const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
if (inspectResult.exitCode !== 0) { if (inspectResult.exitCode !== 0) {
@@ -401,7 +472,30 @@ export class TsDockerManager {
await smartshellInstance.exec(`docker buildx use ${builderName}`); await smartshellInstance.exec(`docker buildx use ${builderName}`);
} }
} }
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`); this.activeRemoteBuilders = [];
}
/**
* Opens SSH reverse tunnels for remote builders so they can reach the local registry.
*/
private async openRemoteTunnels(): Promise<void> {
if (this.activeRemoteBuilders.length === 0) return;
this.sshTunnelManager = new SshTunnelManager();
await this.sshTunnelManager.openTunnels(
this.activeRemoteBuilders,
this.session.config.registryPort,
);
}
/**
* Closes any active SSH tunnels.
*/
private async closeRemoteTunnels(): Promise<void> {
if (this.sshTunnelManager) {
await this.sshTunnelManager.closeAll();
this.sshTunnelManager = undefined;
}
} }
/** /**

View File

@@ -95,3 +95,20 @@ export interface IDockerContextInfo {
dockerHost?: string; // value of DOCKER_HOST env var, if set dockerHost?: string; // value of DOCKER_HOST env var, if set
topology?: 'socket-mount' | 'dind' | 'local'; topology?: 'socket-mount' | 'dind' | 'local';
} }
/**
* A remote builder node for native cross-platform builds
*/
export interface IRemoteBuilder {
name: string; // e.g., "arm64-builder"
host: string; // e.g., "armbuilder@192.168.190.216"
platform: string; // e.g., "linux/arm64"
sshKeyPath?: string; // e.g., "~/.ssh/id_ed25519"
}
/**
* Global tsdocker configuration stored at ~/.git.zone/tsdocker/config.json
*/
export interface IGlobalConfig {
remoteBuilders: IRemoteBuilder[];
}

View File

@@ -7,6 +7,7 @@ import * as ConfigModule from './tsdocker.config.js';
import { logger, ora } from './tsdocker.logging.js'; import { logger, ora } from './tsdocker.logging.js';
import { TsDockerManager } from './classes.tsdockermanager.js'; import { TsDockerManager } from './classes.tsdockermanager.js';
import { DockerContext } from './classes.dockercontext.js'; import { DockerContext } from './classes.dockercontext.js';
import { GlobalConfig } from './classes.globalconfig.js';
import type { IBuildCommandOptions } from './interfaces/index.js'; import type { IBuildCommandOptions } from './interfaces/index.js';
import { commitinfo } from './00_commitinfo_data.js'; import { commitinfo } from './00_commitinfo_data.js';
@@ -33,6 +34,7 @@ COMMANDS
test [flags] Build and run container test scripts test [flags] Build and run container test scripts
login Authenticate with configured registries login Authenticate with configured registries
list List discovered Dockerfiles list List discovered Dockerfiles
config <subcommand> [flags] Manage global tsdocker configuration
clean [-y] [--all] Interactive Docker resource cleanup clean [-y] [--all] Interactive Docker resource cleanup
BUILD / PUSH OPTIONS BUILD / PUSH OPTIONS
@@ -52,6 +54,17 @@ CLEAN OPTIONS
-y Auto-confirm all prompts -y Auto-confirm all prompts
--all Include all images and volumes (not just dangling) --all Include all images and volumes (not just dangling)
CONFIG SUBCOMMANDS
add-builder Add a remote builder node
--name=<n> Builder name (e.g. arm64-builder)
--host=<h> SSH host (e.g. user@192.168.1.100)
--platform=<p> Platform (e.g. linux/arm64)
--ssh-key=<path> SSH key path (optional)
remove-builder Remove a remote builder by name
--name=<n> Builder name to remove
list-builders List all configured remote builders
show Show full global config
CONFIGURATION CONFIGURATION
Configure via npmextra.json under the "@git.zone/tsdocker" key: Configure via npmextra.json under the "@git.zone/tsdocker" key:
@@ -62,12 +75,17 @@ CONFIGURATION
push Boolean, auto-push after build push Boolean, auto-push after build
testDir Directory containing test_*.sh scripts testDir Directory containing test_*.sh scripts
Global config is stored at ~/.git.zone/tsdocker/config.json
and managed via the "config" command.
EXAMPLES EXAMPLES
tsdocker build tsdocker build
tsdocker build Dockerfile_app --platform=linux/arm64 tsdocker build Dockerfile_app --platform=linux/arm64
tsdocker push --registry=ghcr.io tsdocker push --registry=ghcr.io
tsdocker test --verbose tsdocker test --verbose
tsdocker clean -y --all tsdocker clean -y --all
tsdocker config add-builder --name=arm64 --host=user@host --platform=linux/arm64
tsdocker config list-builders
`; `;
console.log(manPage); console.log(manPage);
}; };
@@ -280,6 +298,76 @@ export let run = () => {
} }
}); });
/**
* Manage global tsdocker configuration (remote builders, etc.)
* Usage: tsdocker config <subcommand> [--name=...] [--host=...] [--platform=...] [--ssh-key=...]
*/
tsdockerCli.addCommand('config').subscribe(async argvArg => {
try {
const subcommand = argvArg._[1] as string;
switch (subcommand) {
case 'add-builder': {
const name = argvArg.name as string;
const host = argvArg.host as string;
const platform = argvArg.platform as string;
const sshKeyPath = argvArg['ssh-key'] as string | undefined;
if (!name || !host || !platform) {
logger.log('error', 'Required: --name, --host, --platform');
logger.log('info', 'Usage: tsdocker config add-builder --name=arm64-builder --host=user@host --platform=linux/arm64 [--ssh-key=~/.ssh/id_ed25519]');
process.exit(1);
}
GlobalConfig.addBuilder({ name, host, platform, sshKeyPath });
logger.log('success', `Remote builder "${name}" configured: ${platform} via ssh://${host}`);
break;
}
case 'remove-builder': {
const name = argvArg.name as string;
if (!name) {
logger.log('error', 'Required: --name');
logger.log('info', 'Usage: tsdocker config remove-builder --name=arm64-builder');
process.exit(1);
}
GlobalConfig.removeBuilder(name);
logger.log('success', `Remote builder "${name}" removed`);
break;
}
case 'list-builders': {
const builders = GlobalConfig.getBuilders();
if (builders.length === 0) {
logger.log('info', 'No remote builders configured');
} else {
logger.log('info', `${builders.length} remote builder(s):`);
for (const b of builders) {
const keyInfo = b.sshKeyPath ? ` (key: ${b.sshKeyPath})` : '';
logger.log('info', ` ${b.name}: ${b.platform} via ssh://${b.host}${keyInfo}`);
}
}
break;
}
case 'show': {
const config = GlobalConfig.load();
logger.log('info', `Config file: ${GlobalConfig.getConfigPath()}`);
console.log(JSON.stringify(config, null, 2));
break;
}
default:
logger.log('error', `Unknown config subcommand: ${subcommand || '(none)'}`);
logger.log('info', 'Available: add-builder, remove-builder, list-builders, show');
process.exit(1);
}
} catch (err) {
logger.log('error', `Config failed: ${(err as Error).message}`);
process.exit(1);
}
});
tsdockerCli.addCommand('clean').subscribe(async argvArg => { tsdockerCli.addCommand('clean').subscribe(async argvArg => {
try { try {
const autoYes = !!argvArg.y; const autoYes = !!argvArg.y;