@git.zone/tsdocker 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dockerfile.d.ts +2 -0
- package/dist_ts/classes.dockerfile.js +7 -1
- package/dist_ts/classes.globalconfig.d.ts +13 -0
- package/dist_ts/classes.globalconfig.js +66 -0
- package/dist_ts/classes.sshtunnel.d.ts +23 -0
- package/dist_ts/classes.sshtunnel.js +66 -0
- package/dist_ts/classes.tsdockermanager.d.ts +21 -1
- package/dist_ts/classes.tsdockermanager.js +74 -3
- package/dist_ts/interfaces/index.d.ts +15 -0
- package/dist_ts/tsdocker.cli.js +83 -1
- package/package.json +8 -8
- package/readme.md +64 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dockerfile.ts +7 -1
- package/ts/classes.globalconfig.ts +76 -0
- package/ts/classes.sshtunnel.ts +77 -0
- package/ts/classes.tsdockermanager.ts +97 -3
- package/ts/interfaces/index.ts +17 -0
- package/ts/tsdocker.cli.ts +88 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as plugins from './tsdocker.plugins.js';
|
|
3
|
+
import { logger } from './tsdocker.logging.js';
|
|
4
|
+
import type { IGlobalConfig, IRemoteBuilder } from './interfaces/index.js';
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = plugins.path.join(
|
|
7
|
+
process.env.HOME || process.env.USERPROFILE || '~',
|
|
8
|
+
'.git.zone',
|
|
9
|
+
'tsdocker',
|
|
10
|
+
);
|
|
11
|
+
const CONFIG_PATH = plugins.path.join(CONFIG_DIR, 'config.json');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CONFIG: IGlobalConfig = {
|
|
14
|
+
remoteBuilders: [],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class GlobalConfig {
|
|
18
|
+
static getConfigPath(): string {
|
|
19
|
+
return CONFIG_PATH;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static load(): IGlobalConfig {
|
|
23
|
+
try {
|
|
24
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
return {
|
|
27
|
+
...DEFAULT_CONFIG,
|
|
28
|
+
...parsed,
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return { ...DEFAULT_CONFIG };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static save(config: IGlobalConfig): void {
|
|
36
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
37
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static addBuilder(builder: IRemoteBuilder): void {
|
|
41
|
+
const config = GlobalConfig.load();
|
|
42
|
+
const existing = config.remoteBuilders.findIndex((b) => b.name === builder.name);
|
|
43
|
+
if (existing >= 0) {
|
|
44
|
+
config.remoteBuilders[existing] = builder;
|
|
45
|
+
logger.log('info', `Updated remote builder: ${builder.name}`);
|
|
46
|
+
} else {
|
|
47
|
+
config.remoteBuilders.push(builder);
|
|
48
|
+
logger.log('info', `Added remote builder: ${builder.name}`);
|
|
49
|
+
}
|
|
50
|
+
GlobalConfig.save(config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static removeBuilder(name: string): void {
|
|
54
|
+
const config = GlobalConfig.load();
|
|
55
|
+
const before = config.remoteBuilders.length;
|
|
56
|
+
config.remoteBuilders = config.remoteBuilders.filter((b) => b.name !== name);
|
|
57
|
+
if (config.remoteBuilders.length < before) {
|
|
58
|
+
logger.log('info', `Removed remote builder: ${name}`);
|
|
59
|
+
} else {
|
|
60
|
+
logger.log('warn', `Remote builder not found: ${name}`);
|
|
61
|
+
}
|
|
62
|
+
GlobalConfig.save(config);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static getBuilders(): IRemoteBuilder[] {
|
|
66
|
+
return GlobalConfig.load().remoteBuilders;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns remote builders that match any of the requested platforms
|
|
71
|
+
*/
|
|
72
|
+
static getBuildersForPlatforms(platforms: string[]): IRemoteBuilder[] {
|
|
73
|
+
const builders = GlobalConfig.getBuilders();
|
|
74
|
+
return builders.filter((b) => platforms.includes(b.platform));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as plugins from './tsdocker.plugins.js';
|
|
2
|
+
import { logger } from './tsdocker.logging.js';
|
|
3
|
+
import type { IRemoteBuilder } from './interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
6
|
+
executor: 'bash',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Manages SSH reverse tunnels for remote builder nodes.
|
|
11
|
+
* Opens tunnels so that the local staging registry (localhost:<port>)
|
|
12
|
+
* is accessible as localhost:<port> on each remote machine.
|
|
13
|
+
*/
|
|
14
|
+
export class SshTunnelManager {
|
|
15
|
+
private tunnelPids: number[] = [];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Opens a reverse SSH tunnel to make localPort accessible on the remote machine.
|
|
19
|
+
* ssh -f -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes
|
|
20
|
+
* -R <localPort>:localhost:<localPort> [-i keyPath] user@host
|
|
21
|
+
*/
|
|
22
|
+
async openTunnel(builder: IRemoteBuilder, localPort: number): Promise<void> {
|
|
23
|
+
const keyOpt = builder.sshKeyPath ? `-i ${builder.sshKeyPath} ` : '';
|
|
24
|
+
const cmd = [
|
|
25
|
+
'ssh -f -N',
|
|
26
|
+
'-o StrictHostKeyChecking=no',
|
|
27
|
+
'-o ExitOnForwardFailure=yes',
|
|
28
|
+
`-R ${localPort}:localhost:${localPort}`,
|
|
29
|
+
`${keyOpt}${builder.host}`,
|
|
30
|
+
].join(' ');
|
|
31
|
+
|
|
32
|
+
logger.log('info', `Opening SSH tunnel to ${builder.host} for port ${localPort}...`);
|
|
33
|
+
const result = await smartshellInstance.exec(cmd);
|
|
34
|
+
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Failed to open SSH tunnel to ${builder.host}: ${result.stderr || 'unknown error'}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Find the PID of the tunnel process we just started
|
|
42
|
+
const pidResult = await smartshellInstance.exec(
|
|
43
|
+
`pgrep -f "ssh.*-R ${localPort}:localhost:${localPort}.*${builder.host}" | tail -1`
|
|
44
|
+
);
|
|
45
|
+
if (pidResult.exitCode === 0 && pidResult.stdout.trim()) {
|
|
46
|
+
const pid = parseInt(pidResult.stdout.trim(), 10);
|
|
47
|
+
if (!isNaN(pid)) {
|
|
48
|
+
this.tunnelPids.push(pid);
|
|
49
|
+
logger.log('ok', `SSH tunnel to ${builder.host} established (PID ${pid})`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Opens tunnels for all provided remote builders
|
|
56
|
+
*/
|
|
57
|
+
async openTunnels(builders: IRemoteBuilder[], localPort: number): Promise<void> {
|
|
58
|
+
for (const builder of builders) {
|
|
59
|
+
await this.openTunnel(builder, localPort);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Closes all tunnel processes
|
|
65
|
+
*/
|
|
66
|
+
async closeAll(): Promise<void> {
|
|
67
|
+
for (const pid of this.tunnelPids) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 'SIGTERM');
|
|
70
|
+
logger.log('info', `Closed SSH tunnel (PID ${pid})`);
|
|
71
|
+
} catch {
|
|
72
|
+
// Process may have already exited
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.tunnelPids = [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -8,7 +8,9 @@ import { TsDockerCache } from './classes.tsdockercache.js';
|
|
|
8
8
|
import { DockerContext } from './classes.dockercontext.js';
|
|
9
9
|
import { TsDockerSession } from './classes.tsdockersession.js';
|
|
10
10
|
import { RegistryCopy } from './classes.registrycopy.js';
|
|
11
|
-
import
|
|
11
|
+
import { GlobalConfig } from './classes.globalconfig.js';
|
|
12
|
+
import { SshTunnelManager } from './classes.sshtunnel.js';
|
|
13
|
+
import type { ITsDockerConfig, IBuildCommandOptions, IRemoteBuilder } from './interfaces/index.js';
|
|
12
14
|
|
|
13
15
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
14
16
|
executor: 'bash',
|
|
@@ -24,6 +26,8 @@ export class TsDockerManager {
|
|
|
24
26
|
public dockerContext: DockerContext;
|
|
25
27
|
public session!: TsDockerSession;
|
|
26
28
|
private dockerfiles: Dockerfile[] = [];
|
|
29
|
+
private activeRemoteBuilders: IRemoteBuilder[] = [];
|
|
30
|
+
private sshTunnelManager?: SshTunnelManager;
|
|
27
31
|
|
|
28
32
|
constructor(config: ITsDockerConfig) {
|
|
29
33
|
this.config = config;
|
|
@@ -235,6 +239,7 @@ export class TsDockerManager {
|
|
|
235
239
|
const total = toBuild.length;
|
|
236
240
|
const overallStart = Date.now();
|
|
237
241
|
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
|
|
242
|
+
await this.openRemoteTunnels();
|
|
238
243
|
|
|
239
244
|
try {
|
|
240
245
|
if (options?.parallel) {
|
|
@@ -332,6 +337,7 @@ export class TsDockerManager {
|
|
|
332
337
|
}
|
|
333
338
|
}
|
|
334
339
|
} finally {
|
|
340
|
+
await this.closeRemoteTunnels();
|
|
335
341
|
await Dockerfile.stopLocalRegistry(this.session);
|
|
336
342
|
}
|
|
337
343
|
|
|
@@ -347,6 +353,8 @@ export class TsDockerManager {
|
|
|
347
353
|
isRootless: this.dockerContext.contextInfo?.isRootless,
|
|
348
354
|
parallel: options?.parallel,
|
|
349
355
|
parallelConcurrency: options?.parallelConcurrency,
|
|
356
|
+
onRegistryStarted: () => this.openRemoteTunnels(),
|
|
357
|
+
onBeforeRegistryStop: () => this.closeRemoteTunnels(),
|
|
350
358
|
});
|
|
351
359
|
}
|
|
352
360
|
|
|
@@ -373,13 +381,76 @@ export class TsDockerManager {
|
|
|
373
381
|
}
|
|
374
382
|
|
|
375
383
|
/**
|
|
376
|
-
* Ensures Docker buildx is set up for multi-architecture builds
|
|
384
|
+
* Ensures Docker buildx is set up for multi-architecture builds.
|
|
385
|
+
* When remote builders are configured in the global config, creates a multi-node
|
|
386
|
+
* builder with native nodes instead of relying on QEMU emulation.
|
|
377
387
|
*/
|
|
378
388
|
private async ensureBuildx(): Promise<void> {
|
|
379
389
|
const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || '');
|
|
380
390
|
const platforms = this.config.platforms?.join(', ') || 'default';
|
|
381
391
|
logger.log('info', `Setting up Docker buildx [${platforms}]...`);
|
|
382
392
|
logger.log('info', `Builder: ${builderName}`);
|
|
393
|
+
|
|
394
|
+
// Check for remote builders matching our target platforms
|
|
395
|
+
const requestedPlatforms = this.config.platforms || ['linux/amd64'];
|
|
396
|
+
const remoteBuilders = GlobalConfig.getBuildersForPlatforms(requestedPlatforms);
|
|
397
|
+
|
|
398
|
+
if (remoteBuilders.length > 0) {
|
|
399
|
+
await this.ensureBuildxWithRemoteNodes(builderName, requestedPlatforms, remoteBuilders);
|
|
400
|
+
} else {
|
|
401
|
+
await this.ensureBuildxLocal(builderName);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Creates a multi-node buildx builder with local + remote SSH nodes.
|
|
409
|
+
*/
|
|
410
|
+
private async ensureBuildxWithRemoteNodes(
|
|
411
|
+
builderName: string,
|
|
412
|
+
requestedPlatforms: string[],
|
|
413
|
+
remoteBuilders: IRemoteBuilder[],
|
|
414
|
+
): Promise<void> {
|
|
415
|
+
const remotePlatforms = new Set(remoteBuilders.map((b) => b.platform));
|
|
416
|
+
const localPlatforms = requestedPlatforms.filter((p) => !remotePlatforms.has(p));
|
|
417
|
+
|
|
418
|
+
logger.log('info', `Remote builders: ${remoteBuilders.map((b) => `${b.name} (${b.platform} @ ${b.host})`).join(', ')}`);
|
|
419
|
+
if (localPlatforms.length > 0) {
|
|
420
|
+
logger.log('info', `Local platforms: ${localPlatforms.join(', ')}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Always recreate the builder to ensure correct node topology
|
|
424
|
+
await smartshellInstance.execSilent(`docker buildx rm ${builderName} 2>/dev/null || true`);
|
|
425
|
+
|
|
426
|
+
// Create the local node
|
|
427
|
+
const localPlatformFlag = localPlatforms.length > 0 ? ` --platform ${localPlatforms.join(',')}` : '';
|
|
428
|
+
await smartshellInstance.exec(
|
|
429
|
+
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host${localPlatformFlag} --use`
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// Append remote nodes
|
|
433
|
+
for (const builder of remoteBuilders) {
|
|
434
|
+
logger.log('info', `Appending remote node: ${builder.name} (${builder.platform}) via ssh://${builder.host}`);
|
|
435
|
+
const appendResult = await smartshellInstance.exec(
|
|
436
|
+
`docker buildx create --append --name ${builderName} --driver docker-container --driver-opt network=host --platform ${builder.platform} --node ${builder.name} ssh://${builder.host}`
|
|
437
|
+
);
|
|
438
|
+
if (appendResult.exitCode !== 0) {
|
|
439
|
+
throw new Error(`Failed to append remote builder ${builder.name}: ${appendResult.stderr}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Bootstrap all nodes
|
|
444
|
+
await smartshellInstance.exec('docker buildx inspect --bootstrap');
|
|
445
|
+
|
|
446
|
+
// Store active remote builders for SSH tunnel setup during build
|
|
447
|
+
this.activeRemoteBuilders = remoteBuilders;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Creates a single-node local buildx builder (original behavior, uses QEMU for cross-platform).
|
|
452
|
+
*/
|
|
453
|
+
private async ensureBuildxLocal(builderName: string): Promise<void> {
|
|
383
454
|
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
|
|
384
455
|
|
|
385
456
|
if (inspectResult.exitCode !== 0) {
|
|
@@ -401,7 +472,30 @@ export class TsDockerManager {
|
|
|
401
472
|
await smartshellInstance.exec(`docker buildx use ${builderName}`);
|
|
402
473
|
}
|
|
403
474
|
}
|
|
404
|
-
|
|
475
|
+
this.activeRemoteBuilders = [];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Opens SSH reverse tunnels for remote builders so they can reach the local registry.
|
|
480
|
+
*/
|
|
481
|
+
private async openRemoteTunnels(): Promise<void> {
|
|
482
|
+
if (this.activeRemoteBuilders.length === 0) return;
|
|
483
|
+
|
|
484
|
+
this.sshTunnelManager = new SshTunnelManager();
|
|
485
|
+
await this.sshTunnelManager.openTunnels(
|
|
486
|
+
this.activeRemoteBuilders,
|
|
487
|
+
this.session.config.registryPort,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Closes any active SSH tunnels.
|
|
493
|
+
*/
|
|
494
|
+
private async closeRemoteTunnels(): Promise<void> {
|
|
495
|
+
if (this.sshTunnelManager) {
|
|
496
|
+
await this.sshTunnelManager.closeAll();
|
|
497
|
+
this.sshTunnelManager = undefined;
|
|
498
|
+
}
|
|
405
499
|
}
|
|
406
500
|
|
|
407
501
|
/**
|
package/ts/interfaces/index.ts
CHANGED
|
@@ -95,3 +95,20 @@ export interface IDockerContextInfo {
|
|
|
95
95
|
dockerHost?: string; // value of DOCKER_HOST env var, if set
|
|
96
96
|
topology?: 'socket-mount' | 'dind' | 'local';
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A remote builder node for native cross-platform builds
|
|
101
|
+
*/
|
|
102
|
+
export interface IRemoteBuilder {
|
|
103
|
+
name: string; // e.g., "arm64-builder"
|
|
104
|
+
host: string; // e.g., "armbuilder@192.168.190.216"
|
|
105
|
+
platform: string; // e.g., "linux/arm64"
|
|
106
|
+
sshKeyPath?: string; // e.g., "~/.ssh/id_ed25519"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Global tsdocker configuration stored at ~/.git.zone/tsdocker/config.json
|
|
111
|
+
*/
|
|
112
|
+
export interface IGlobalConfig {
|
|
113
|
+
remoteBuilders: IRemoteBuilder[];
|
|
114
|
+
}
|
package/ts/tsdocker.cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as ConfigModule from './tsdocker.config.js';
|
|
|
7
7
|
import { logger, ora } from './tsdocker.logging.js';
|
|
8
8
|
import { TsDockerManager } from './classes.tsdockermanager.js';
|
|
9
9
|
import { DockerContext } from './classes.dockercontext.js';
|
|
10
|
+
import { GlobalConfig } from './classes.globalconfig.js';
|
|
10
11
|
import type { IBuildCommandOptions } from './interfaces/index.js';
|
|
11
12
|
import { commitinfo } from './00_commitinfo_data.js';
|
|
12
13
|
|
|
@@ -33,6 +34,7 @@ COMMANDS
|
|
|
33
34
|
test [flags] Build and run container test scripts
|
|
34
35
|
login Authenticate with configured registries
|
|
35
36
|
list List discovered Dockerfiles
|
|
37
|
+
config <subcommand> [flags] Manage global tsdocker configuration
|
|
36
38
|
clean [-y] [--all] Interactive Docker resource cleanup
|
|
37
39
|
|
|
38
40
|
BUILD / PUSH OPTIONS
|
|
@@ -52,6 +54,17 @@ CLEAN OPTIONS
|
|
|
52
54
|
-y Auto-confirm all prompts
|
|
53
55
|
--all Include all images and volumes (not just dangling)
|
|
54
56
|
|
|
57
|
+
CONFIG SUBCOMMANDS
|
|
58
|
+
add-builder Add a remote builder node
|
|
59
|
+
--name=<n> Builder name (e.g. arm64-builder)
|
|
60
|
+
--host=<h> SSH host (e.g. user@192.168.1.100)
|
|
61
|
+
--platform=<p> Platform (e.g. linux/arm64)
|
|
62
|
+
--ssh-key=<path> SSH key path (optional)
|
|
63
|
+
remove-builder Remove a remote builder by name
|
|
64
|
+
--name=<n> Builder name to remove
|
|
65
|
+
list-builders List all configured remote builders
|
|
66
|
+
show Show full global config
|
|
67
|
+
|
|
55
68
|
CONFIGURATION
|
|
56
69
|
Configure via npmextra.json under the "@git.zone/tsdocker" key:
|
|
57
70
|
|
|
@@ -62,12 +75,17 @@ CONFIGURATION
|
|
|
62
75
|
push Boolean, auto-push after build
|
|
63
76
|
testDir Directory containing test_*.sh scripts
|
|
64
77
|
|
|
78
|
+
Global config is stored at ~/.git.zone/tsdocker/config.json
|
|
79
|
+
and managed via the "config" command.
|
|
80
|
+
|
|
65
81
|
EXAMPLES
|
|
66
82
|
tsdocker build
|
|
67
83
|
tsdocker build Dockerfile_app --platform=linux/arm64
|
|
68
84
|
tsdocker push --registry=ghcr.io
|
|
69
85
|
tsdocker test --verbose
|
|
70
86
|
tsdocker clean -y --all
|
|
87
|
+
tsdocker config add-builder --name=arm64 --host=user@host --platform=linux/arm64
|
|
88
|
+
tsdocker config list-builders
|
|
71
89
|
`;
|
|
72
90
|
console.log(manPage);
|
|
73
91
|
};
|
|
@@ -280,6 +298,76 @@ export let run = () => {
|
|
|
280
298
|
}
|
|
281
299
|
});
|
|
282
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Manage global tsdocker configuration (remote builders, etc.)
|
|
303
|
+
* Usage: tsdocker config <subcommand> [--name=...] [--host=...] [--platform=...] [--ssh-key=...]
|
|
304
|
+
*/
|
|
305
|
+
tsdockerCli.addCommand('config').subscribe(async argvArg => {
|
|
306
|
+
try {
|
|
307
|
+
const subcommand = argvArg._[1] as string;
|
|
308
|
+
|
|
309
|
+
switch (subcommand) {
|
|
310
|
+
case 'add-builder': {
|
|
311
|
+
const name = argvArg.name as string;
|
|
312
|
+
const host = argvArg.host as string;
|
|
313
|
+
const platform = argvArg.platform as string;
|
|
314
|
+
const sshKeyPath = argvArg['ssh-key'] as string | undefined;
|
|
315
|
+
|
|
316
|
+
if (!name || !host || !platform) {
|
|
317
|
+
logger.log('error', 'Required: --name, --host, --platform');
|
|
318
|
+
logger.log('info', 'Usage: tsdocker config add-builder --name=arm64-builder --host=user@host --platform=linux/arm64 [--ssh-key=~/.ssh/id_ed25519]');
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
GlobalConfig.addBuilder({ name, host, platform, sshKeyPath });
|
|
323
|
+
logger.log('success', `Remote builder "${name}" configured: ${platform} via ssh://${host}`);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
case 'remove-builder': {
|
|
328
|
+
const name = argvArg.name as string;
|
|
329
|
+
if (!name) {
|
|
330
|
+
logger.log('error', 'Required: --name');
|
|
331
|
+
logger.log('info', 'Usage: tsdocker config remove-builder --name=arm64-builder');
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
GlobalConfig.removeBuilder(name);
|
|
335
|
+
logger.log('success', `Remote builder "${name}" removed`);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case 'list-builders': {
|
|
340
|
+
const builders = GlobalConfig.getBuilders();
|
|
341
|
+
if (builders.length === 0) {
|
|
342
|
+
logger.log('info', 'No remote builders configured');
|
|
343
|
+
} else {
|
|
344
|
+
logger.log('info', `${builders.length} remote builder(s):`);
|
|
345
|
+
for (const b of builders) {
|
|
346
|
+
const keyInfo = b.sshKeyPath ? ` (key: ${b.sshKeyPath})` : '';
|
|
347
|
+
logger.log('info', ` ${b.name}: ${b.platform} via ssh://${b.host}${keyInfo}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case 'show': {
|
|
354
|
+
const config = GlobalConfig.load();
|
|
355
|
+
logger.log('info', `Config file: ${GlobalConfig.getConfigPath()}`);
|
|
356
|
+
console.log(JSON.stringify(config, null, 2));
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
default:
|
|
361
|
+
logger.log('error', `Unknown config subcommand: ${subcommand || '(none)'}`);
|
|
362
|
+
logger.log('info', 'Available: add-builder, remove-builder, list-builders, show');
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
logger.log('error', `Config failed: ${(err as Error).message}`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
283
371
|
tsdockerCli.addCommand('clean').subscribe(async argvArg => {
|
|
284
372
|
try {
|
|
285
373
|
const autoYes = !!argvArg.y;
|