feat(clean): Make the command interactive: add smartinteract prompts, docker context detection, and selective resource removal with support for --all and -y auto-confirm

This commit is contained in:
2026-02-07 05:17:32 +00:00
parent 3085eb590f
commit 2d4d7c671a
6 changed files with 480 additions and 17 deletions

View File

@@ -7,6 +7,7 @@ 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';
@@ -236,27 +237,200 @@ export let run = () => {
});
tsdockerCli.addCommand('clean').subscribe(async argvArg => {
ora.text('cleaning up docker env...');
if (argvArg.all) {
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash'
});
ora.text('killing any running docker containers...');
await smartshellInstance.exec(`docker kill $(docker ps -q)`);
try {
const autoYes = !!argvArg.y;
const includeAll = !!argvArg.all;
ora.text('removing stopped containers...');
await smartshellInstance.exec(`docker rm $(docker ps -a -q)`);
const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash' });
const interact = new plugins.smartinteract.SmartInteract();
ora.text('removing images...');
await smartshellInstance.exec(`docker rmi -f $(docker images -q -f dangling=true)`);
// --- 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();
ora.text('removing all other images...');
await smartshellInstance.exec(`docker rmi $(docker images -a -q)`);
// --- Helper: parse docker output into resource list ---
interface IDockerResource {
id: string;
display: string;
}
ora.text('removing all volumes...');
await smartshellInstance.exec(`docker volume rm $(docker volume ls -f dangling=true -q)`);
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);
}
ora.finishSuccess('docker environment now is clean!');
});
tsdockerCli.addCommand('vscode').subscribe(async argvArg => {