import * as plugins from './tsdocker.plugins.js'; import * as paths from './tsdocker.paths.js'; // modules import * as ConfigModule from './tsdocker.config.js'; import * as DockerModule from './tsdocker.docker.js'; import { logger, ora } from './tsdocker.logging.js'; import { TsDockerManager } from './classes.tsdockermanager.js'; import { DockerContext } from './classes.dockercontext.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); export let run = () => { // Default command: run tests in container (legacy behavior) tsdockerCli.standardCommand().subscribe(async argvArg => { const configArg = await ConfigModule.run().then(DockerModule.run); if (configArg.exitCode === 0) { logger.log('success', 'container ended all right!'); } else { logger.log('error', `container ended with error! Exit Code is ${configArg.exitCode}`); process.exit(1); } }); /** * 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 (if not already built) 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); } }); /** * this command is executed inside docker and meant for use from outside docker */ tsdockerCli.addCommand('runinside').subscribe(async argvArg => { logger.log('ok', 'Allright. We are now in Docker!'); ora.text('now trying to run your specified command'); const configArg = await ConfigModule.run(); const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash' }); ora.stop(); await smartshellInstance.exec(configArg.command).then(response => { if (response.exitCode !== 0) { 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.addCommand('vscode').subscribe(async argvArg => { const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash' }); logger.log('ok', `Starting vscode in cwd ${paths.cwd}`); await smartshellInstance.execAndWaitForLine( `docker run -p 127.0.0.1:8443:8443 -v "${ paths.cwd }:/home/coder/project" registry.gitlab.com/hosttoday/ht-docker-vscode --allow-http --no-auth`, /Connected to shared process/ ); await plugins.smartopen.openUrl('testing-vscode.git.zone:8443'); }); tsdockerCli.startParse(); };