462 lines
15 KiB
TypeScript
462 lines
15 KiB
TypeScript
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, 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 <registry-url>');
|
|
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<IDockerResource[]> => {
|
|
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<string[]> => {
|
|
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<boolean> => {
|
|
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();
|
|
};
|