import * as plugins from './tsdocker.plugins.js'; import * as paths from './tsdocker.paths.js'; // modules import * as ConfigModule from './tsdocker.config.js'; import { logger, ora } from './tsdocker.logging.js'; import { TsDockerManager } from './classes.tsdockermanager.js'; import { DockerContext } from './classes.dockercontext.js'; import { GlobalConfig } from './classes.globalconfig.js'; import type { IBuildCommandOptions } from './interfaces/index.js'; import { commitinfo } from './00_commitinfo_data.js'; const tsdockerCli = new plugins.smartcli.Smartcli(); tsdockerCli.addVersion(commitinfo.version); const printManPage = () => { const manPage = ` TSDOCKER(1) User Commands TSDOCKER(1) NAME tsdocker - build, test, and push Docker images VERSION ${commitinfo.version} SYNOPSIS tsdocker [options] COMMANDS build [patterns...] [flags] Build Dockerfiles in dependency order push [patterns...] [flags] Build and push images to registries pull Pull images from a registry test [flags] Build and run container test scripts login Authenticate with configured registries list List discovered Dockerfiles config [flags] Manage global tsdocker configuration clean [-y] [--all] Interactive Docker resource cleanup BUILD / PUSH OPTIONS --platform=

Target platform (e.g. linux/arm64) --timeout= Build timeout in seconds --no-cache Rebuild without Docker layer cache --cached Skip builds when Dockerfile is unchanged --verbose Stream raw docker build output --parallel[=] Parallel builds (optional concurrency limit) --context= Docker context to use PUSH-ONLY OPTIONS --registry= Push to a specific registry --no-build Push already-built images (skip build step) CLEAN OPTIONS -y Auto-confirm all prompts --all Include all images and volumes (not just dangling) CONFIG SUBCOMMANDS add-builder Add a remote builder node --name= Builder name (e.g. arm64-builder) --host= SSH host (e.g. user@192.168.1.100) --platform=

Platform (e.g. linux/arm64) --ssh-key= SSH key path (optional) remove-builder Remove a remote builder by name --name= Builder name to remove list-builders List all configured remote builders show Show full global config CONFIGURATION Configure via npmextra.json under the "@git.zone/tsdocker" key: registries Array of registry URLs to push to registryRepoMap Map of registry URL to repo path overrides buildArgEnvMap Map of Docker build-arg names to env var names platforms Array of target platforms (default: ["linux/amd64"]) push Boolean, auto-push after build testDir Directory containing test_*.sh scripts Global config is stored at ~/.git.zone/tsdocker/config.json and managed via the "config" command. EXAMPLES tsdocker build tsdocker build Dockerfile_app --platform=linux/arm64 tsdocker push --registry=ghcr.io tsdocker test --verbose tsdocker clean -y --all tsdocker config add-builder --name=arm64 --host=user@host --platform=linux/arm64 tsdocker config list-builders `; console.log(manPage); }; export let run = () => { // Default command: print man page tsdockerCli.standardCommand().subscribe(async () => { printManPage(); }); /** * Build Dockerfiles in dependency order * Usage: tsdocker build [Dockerfile_patterns...] [--platform=linux/arm64] [--timeout=600] */ tsdockerCli.addCommand('build').subscribe(async argvArg => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); await manager.prepare(argvArg.context as string | undefined); const buildOptions: IBuildCommandOptions = {}; const patterns = argvArg._.slice(1) as string[]; if (patterns.length > 0) { buildOptions.patterns = patterns; } if (argvArg.platform) { buildOptions.platform = argvArg.platform as string; } if (argvArg.timeout) { buildOptions.timeout = Number(argvArg.timeout); } if (argvArg.cache === false) { buildOptions.noCache = true; } if (argvArg.cached) { buildOptions.cached = true; } if (argvArg.verbose) { buildOptions.verbose = true; } if (argvArg.parallel) { buildOptions.parallel = true; if (typeof argvArg.parallel === 'number') { buildOptions.parallelConcurrency = argvArg.parallel; } } await manager.build(buildOptions); await manager.cleanup(); logger.log('success', 'Build completed successfully'); } catch (err) { logger.log('error', `Build failed: ${(err as Error).message}`); process.exit(1); } }); /** * Push built images to configured registries * Usage: tsdocker push [Dockerfile_patterns...] [--platform=linux/arm64] [--timeout=600] [--registry=url] */ tsdockerCli.addCommand('push').subscribe(async argvArg => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); await manager.prepare(argvArg.context as string | undefined); // Login first await manager.login(); // Parse build options from positional args and flags const buildOptions: IBuildCommandOptions = {}; const patterns = argvArg._.slice(1) as string[]; if (patterns.length > 0) { buildOptions.patterns = patterns; } if (argvArg.platform) { buildOptions.platform = argvArg.platform as string; } if (argvArg.timeout) { buildOptions.timeout = Number(argvArg.timeout); } if (argvArg.cache === false) { buildOptions.noCache = true; } if (argvArg.verbose) { buildOptions.verbose = true; } if (argvArg.parallel) { buildOptions.parallel = true; if (typeof argvArg.parallel === 'number') { buildOptions.parallelConcurrency = argvArg.parallel; } } // Build images first, unless --no-build is set if (argvArg.build === false) { await manager.discoverDockerfiles(); if (buildOptions.patterns?.length) { manager.filterDockerfiles(buildOptions.patterns); } } else { await manager.build(buildOptions); } // Get registry from --registry flag const registryArg = argvArg.registry as string | undefined; const registries = registryArg ? [registryArg] : undefined; await manager.push(registries); await manager.cleanup(); logger.log('success', 'Push completed successfully'); } catch (err) { logger.log('error', `Push failed: ${(err as Error).message}`); process.exit(1); } }); /** * Pull images from a specified registry */ tsdockerCli.addCommand('pull').subscribe(async argvArg => { try { const registryArg = argvArg._[1]; // e.g., tsdocker pull registry.gitlab.com if (!registryArg) { logger.log('error', 'Registry URL required. Usage: tsdocker pull '); process.exit(1); } const config = await ConfigModule.run(); const manager = new TsDockerManager(config); await manager.prepare(argvArg.context as string | undefined); // Login first await manager.login(); await manager.pull(registryArg); logger.log('success', 'Pull completed successfully'); } catch (err) { logger.log('error', `Pull failed: ${(err as Error).message}`); process.exit(1); } }); /** * Run container tests for all Dockerfiles */ tsdockerCli.addCommand('test').subscribe(async argvArg => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); await manager.prepare(argvArg.context as string | undefined); // Build images first const buildOptions: IBuildCommandOptions = {}; if (argvArg.cache === false) { buildOptions.noCache = true; } if (argvArg.cached) { buildOptions.cached = true; } if (argvArg.verbose) { buildOptions.verbose = true; } if (argvArg.parallel) { buildOptions.parallel = true; if (typeof argvArg.parallel === 'number') { buildOptions.parallelConcurrency = argvArg.parallel; } } await manager.build(buildOptions); // Run tests await manager.test(); await manager.cleanup(); logger.log('success', 'Tests completed successfully'); } catch (err) { logger.log('error', `Tests failed: ${(err as Error).message}`); process.exit(1); } }); /** * Login to configured registries */ tsdockerCli.addCommand('login').subscribe(async argvArg => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); await manager.prepare(argvArg.context as string | undefined); await manager.login(); logger.log('success', 'Login completed successfully'); } catch (err) { logger.log('error', `Login failed: ${(err as Error).message}`); process.exit(1); } }); /** * List discovered Dockerfiles and their dependencies */ tsdockerCli.addCommand('list').subscribe(async argvArg => { try { const config = await ConfigModule.run(); const manager = new TsDockerManager(config); await manager.prepare(argvArg.context as string | undefined); await manager.list(); } catch (err) { logger.log('error', `List failed: ${(err as Error).message}`); process.exit(1); } }); /** * Manage global tsdocker configuration (remote builders, etc.) * Usage: tsdocker config [--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 => { try { const autoYes = !!argvArg.y; const includeAll = !!argvArg.all; const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash' }); const interact = new plugins.smartinteract.SmartInteract(); // --- Docker context detection --- ora.text('detecting docker context...'); const dockerContext = new DockerContext(); if (argvArg.context) { dockerContext.setContext(argvArg.context as string); } await dockerContext.detect(); ora.stop(); dockerContext.logContextInfo(); // --- Helper: parse docker output into resource list --- interface IDockerResource { id: string; display: string; } const listResources = async (command: string): Promise => { const result = await smartshellInstance.execSilent(command); if (result.exitCode !== 0 || !result.stdout.trim()) { return []; } return result.stdout.trim().split('\n').filter(Boolean).map((line) => { const parts = line.split('\t'); return { id: parts[0], display: parts.join(' | '), }; }); }; // --- Helper: checkbox selection --- const selectResources = async ( name: string, message: string, resources: IDockerResource[], ): Promise => { if (autoYes) { return resources.map((r) => r.id); } const answer = await interact.askQuestion({ name, type: 'checkbox', message, default: [], choices: resources.map((r) => ({ name: r.display, value: r.id })), }); return answer.value as string[]; }; // --- Helper: confirm action --- const confirmAction = async ( name: string, message: string, ): Promise => { if (autoYes) { return true; } const answer = await interact.askQuestion({ name, type: 'confirm', message, default: false, }); return answer.value as boolean; }; // === RUNNING CONTAINERS === const runningContainers = await listResources( `docker ps --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}'` ); if (runningContainers.length > 0) { logger.log('info', `Found ${runningContainers.length} running container(s)`); const selectedIds = await selectResources( 'runningContainers', 'Select running containers to kill:', runningContainers, ); if (selectedIds.length > 0) { logger.log('info', `Killing ${selectedIds.length} container(s)...`); await smartshellInstance.exec(`docker kill ${selectedIds.join(' ')}`); } } else { logger.log('info', 'No running containers found'); } // === STOPPED CONTAINERS === const stoppedContainers = await listResources( `docker ps -a --filter status=exited --filter status=created --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}'` ); if (stoppedContainers.length > 0) { logger.log('info', `Found ${stoppedContainers.length} stopped container(s)`); const selectedIds = await selectResources( 'stoppedContainers', 'Select stopped containers to remove:', stoppedContainers, ); if (selectedIds.length > 0) { logger.log('info', `Removing ${selectedIds.length} container(s)...`); await smartshellInstance.exec(`docker rm ${selectedIds.join(' ')}`); } } else { logger.log('info', 'No stopped containers found'); } // === DANGLING IMAGES === const danglingImages = await listResources( `docker images -f dangling=true --format '{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}'` ); if (danglingImages.length > 0) { const confirmed = await confirmAction( 'removeDanglingImages', `Remove ${danglingImages.length} dangling image(s)?`, ); if (confirmed) { logger.log('info', `Removing ${danglingImages.length} dangling image(s)...`); const ids = danglingImages.map((r) => r.id).join(' '); await smartshellInstance.exec(`docker rmi ${ids}`); } } else { logger.log('info', 'No dangling images found'); } // === ALL IMAGES (only with --all) === if (includeAll) { const allImages = await listResources( `docker images --format '{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}'` ); if (allImages.length > 0) { logger.log('info', `Found ${allImages.length} image(s) total`); const selectedIds = await selectResources( 'allImages', 'Select images to remove:', allImages, ); if (selectedIds.length > 0) { logger.log('info', `Removing ${selectedIds.length} image(s)...`); await smartshellInstance.exec(`docker rmi -f ${selectedIds.join(' ')}`); } } else { logger.log('info', 'No images found'); } } // === DANGLING VOLUMES === const danglingVolumes = await listResources( `docker volume ls -f dangling=true --format '{{.Name}}\t{{.Driver}}'` ); if (danglingVolumes.length > 0) { const confirmed = await confirmAction( 'removeDanglingVolumes', `Remove ${danglingVolumes.length} dangling volume(s)?`, ); if (confirmed) { logger.log('info', `Removing ${danglingVolumes.length} dangling volume(s)...`); const names = danglingVolumes.map((r) => r.id).join(' '); await smartshellInstance.exec(`docker volume rm ${names}`); } } else { logger.log('info', 'No dangling volumes found'); } // === ALL VOLUMES (only with --all) === if (includeAll) { const allVolumes = await listResources( `docker volume ls --format '{{.Name}}\t{{.Driver}}'` ); if (allVolumes.length > 0) { logger.log('info', `Found ${allVolumes.length} volume(s) total`); const selectedIds = await selectResources( 'allVolumes', 'Select volumes to remove:', allVolumes, ); if (selectedIds.length > 0) { logger.log('info', `Removing ${selectedIds.length} volume(s)...`); await smartshellInstance.exec(`docker volume rm ${selectedIds.join(' ')}`); } } else { logger.log('info', 'No volumes found'); } } logger.log('success', 'Docker cleanup completed!'); } catch (err) { logger.log('error', `Clean failed: ${(err as Error).message}`); process.exit(1); } }); tsdockerCli.startParse(); };