From 089bb18272b7b2fe4b6026cafaae3fe8d388aaaa Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Mon, 30 Jun 2025 20:53:42 +0700 Subject: [PATCH 1/3] feat: implement devcontainer stop and down commands - Add 'devcontainer stop' command to stop running containers - Add 'devcontainer down' command to stop and remove containers - Support --all flag to target all devcontainers - Support --workspace-folder to target specific workspace - Support --id-label for custom label filtering - Support --remove-volumes for down command - Update README.md to mark both commands as completed - Handle both single container and Docker Compose configurations --- README.md | 6 +- src/spec-node/devContainersSpecCLI.ts | 380 +++++++++++++++++++++++++- 2 files changed, 383 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d59523f83..3fdcecb8a 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ This CLI is in active development. Current status: - [x] `devcontainer exec` - Executes a command in a container with `userEnvProbe`, `remoteUser`, `remoteEnv`, and other properties applied - [x] `devcontainer features <...>` - Tools to assist in authoring and testing [Dev Container Features](https://containers.dev/implementors/features/) - [x] `devcontainer templates <...>` - Tools to assist in authoring and testing [Dev Container Templates](https://containers.dev/implementors/templates/) -- [ ] `devcontainer stop` - Stops containers -- [ ] `devcontainer down` - Stops and deletes containers +- [x] `devcontainer stop` - Stops containers +- [x] `devcontainer down` - Stops and deletes containers ## Try it out @@ -44,6 +44,8 @@ Commands: devcontainer read-configuration Read configuration devcontainer features Features commands devcontainer templates Templates commands + devcontainer stop Stop dev containers + devcontainer down Stop and remove dev containers devcontainer exec [args..] Execute a command on a running dev container Options: diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 59136695d..eb1a64d59 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -16,7 +16,7 @@ import { ContainerError } from '../spec-common/errors'; import { Log, LogDimensions, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log'; import { probeRemoteEnv, runLifecycleHooks, runRemoteCommand, UserEnvProbe, setupInContainer } from '../spec-common/injectHeadless'; import { extendImage } from './containerFeatures'; -import { dockerCLI, DockerCLIParameters, dockerPtyCLI, inspectContainer } from '../spec-shutdown/dockerUtils'; +import { dockerCLI, DockerCLIParameters, dockerPtyCLI, inspectContainer, listContainers, removeContainer, dockerComposeCLI as dockerComposeCLICommand } from '../spec-shutdown/dockerUtils'; import { buildAndExtendDockerCompose, dockerComposeCLIConfig, getDefaultImageName, getProjectName, readDockerComposeConfig, readVersionPrefix } from './dockerCompose'; import { DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration'; import { workspaceFromPath } from '../spec-utils/workspaces'; @@ -50,6 +50,16 @@ const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell'; const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,external=(true|false))?$/; +function getDockerComposeCLI(cliHost: CLIHost, options: { dockerComposePath?: string }) { + return dockerComposeCLIConfig({ + exec: cliHost.exec, + env: cliHost.env, + output: makeLog({ + event: () => undefined, + }, LogLevel.Info), + }, 'docker', options.dockerComposePath || 'docker-compose'); +} + (async () => { const packageFolder = path.join(__dirname, '..', '..'); @@ -89,6 +99,8 @@ const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,externa y.command('metadata ', 'Fetch a published Template\'s metadata', templateMetadataOptions, templateMetadataHandler); y.command('generate-docs', 'Generate documentation', templatesGenerateDocsOptions, templatesGenerateDocsHandler); }); + y.command('stop', 'Stop dev containers', stopOptions, stopHandler); + y.command('down', 'Stop and remove dev containers', downOptions, downHandler); y.command(restArgs ? ['exec', '*'] : ['exec [args..]'], 'Execute a command on a running dev container', execOptions, execHandler); y.epilog(`devcontainer@${version} ${packageFolder}`); y.parse(restArgs ? argv.slice(1) : argv); @@ -1202,6 +1214,170 @@ async function outdated({ process.exit(0); } +function stopOptions(y: Argv) { + return y.options({ + 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, + 'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }, + 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, + 'id-label': { type: 'string', description: 'Label(s) of the format name=value to use for filtering dev containers. If no labels are provided, all dev containers will be stopped.' }, + 'all': { type: 'boolean', default: false, description: 'Stop all running dev containers.' }, + }) + .check(argv => { + if (!argv['all'] && !argv['workspace-folder'] && !argv['id-label']) { + throw new Error('Either --all, --workspace-folder, or --id-label must be specified'); + } + return true; + }); +} + +type StopArgs = UnpackArgv>; + +function stopHandler(args: StopArgs) { + runAsyncHandler(stop.bind(null, args)); +} + +async function stop({ + 'docker-path': dockerPath, + 'docker-compose-path': dockerComposePath, + 'workspace-folder': workspaceFolderArg, + 'config': config, + 'log-level': logLevel, + 'log-format': logFormat, + 'id-label': idLabel, + 'all': all, +}: StopArgs) { + const disposables: (() => Promise | undefined)[] = []; + const dispose = async () => { + await Promise.all(disposables.map(d => d())); + }; + let output: Log | undefined; + try { + const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd(); + const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, logFormat === 'text'); + const sessionStart = new Date(); + const pkg = getPackageConfig(); + output = createLog({ + logLevel: mapLogLevel(logLevel), + logFormat, + log: text => process.stderr.write(text), + terminalDimensions: undefined, + }, pkg, sessionStart, disposables); + + const params: DockerCLIParameters = { + cliHost, + dockerCLI: dockerPath || 'docker', + dockerComposeCLI: getDockerComposeCLI(cliHost, { dockerComposePath }), + env: cliHost.env, + output, + platformInfo: { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch || process.arch), + } + }; + + const result = await stopContainers(params, { all, workspaceFolder: workspaceFolderArg, configFile: config, idLabel }); + await new Promise((resolve, reject) => { + process.stdout.write(JSON.stringify(result) + '\n', err => err ? reject(err) : resolve()); + }); + } catch (err) { + if (output) { + output.write(err && (err.stack || err.message) || String(err)); + } else { + console.error(err); + } + await dispose(); + process.exit(1); + } + await dispose(); + process.exit(0); +} + +function downOptions(y: Argv) { + return y.options({ + 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, + 'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }, + 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, + 'id-label': { type: 'string', description: 'Label(s) of the format name=value to use for filtering dev containers. If no labels are provided, all dev containers will be removed.' }, + 'all': { type: 'boolean', default: false, description: 'Remove all dev containers.' }, + 'remove-volumes': { type: 'boolean', default: false, description: 'Also remove associated volumes.' }, + }) + .check(argv => { + if (!argv['all'] && !argv['workspace-folder'] && !argv['id-label']) { + throw new Error('Either --all, --workspace-folder, or --id-label must be specified'); + } + return true; + }); +} + +type DownArgs = UnpackArgv>; + +function downHandler(args: DownArgs) { + runAsyncHandler(down.bind(null, args)); +} + +async function down({ + 'docker-path': dockerPath, + 'docker-compose-path': dockerComposePath, + 'workspace-folder': workspaceFolderArg, + 'config': config, + 'log-level': logLevel, + 'log-format': logFormat, + 'id-label': idLabel, + 'all': all, + 'remove-volumes': removeVolumes, +}: DownArgs) { + const disposables: (() => Promise | undefined)[] = []; + const dispose = async () => { + await Promise.all(disposables.map(d => d())); + }; + let output: Log | undefined; + try { + const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd(); + const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, logFormat === 'text'); + const sessionStart = new Date(); + const pkg = getPackageConfig(); + output = createLog({ + logLevel: mapLogLevel(logLevel), + logFormat, + log: text => process.stderr.write(text), + terminalDimensions: undefined, + }, pkg, sessionStart, disposables); + + const params: DockerCLIParameters = { + cliHost, + dockerCLI: dockerPath || 'docker', + dockerComposeCLI: getDockerComposeCLI(cliHost, { dockerComposePath }), + env: cliHost.env, + output, + platformInfo: { + os: mapNodeOSToGOOS(cliHost.platform), + arch: mapNodeArchitectureToGOARCH(cliHost.arch || process.arch), + } + }; + + const result = await downContainers(params, { all, workspaceFolder: workspaceFolderArg, configFile: config, idLabel, removeVolumes }); + await new Promise((resolve, reject) => { + process.stdout.write(JSON.stringify(result) + '\n', err => err ? reject(err) : resolve()); + }); + } catch (err) { + if (output) { + output.write(err && (err.stack || err.message) || String(err)); + } else { + console.error(err); + } + await dispose(); + process.exit(1); + } + await dispose(); + process.exit(0); +} + function execOptions(y: Argv) { return y.options({ 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, @@ -1429,3 +1605,205 @@ async function readSecretsFromFile(params: { output?: Log; secretsFile?: string; }); } } + +async function stopContainers(params: DockerCLIParameters, options: { all?: boolean; workspaceFolder?: string; configFile?: string; idLabel?: string }) { + const { all, workspaceFolder, configFile, idLabel } = options; + const { cliHost, output } = params; + + try { + let containerIds: string[] = []; + + if (all) { + // Stop all dev containers - look for containers with devcontainer labels + const labels = ['devcontainer.local_folder']; + containerIds = await listContainers(params, false, labels); + } else if (workspaceFolder || configFile) { + // Stop containers related to a specific workspace + const resolvedWorkspaceFolder = workspaceFolder ? path.resolve(process.cwd(), workspaceFolder) : process.cwd(); + const workspace = workspaceFromPath(cliHost.path, resolvedWorkspaceFolder); + + // Look for containers with matching local folder + const labels = idLabel ? [idLabel] : []; + labels.push(`devcontainer.local_folder=${resolvedWorkspaceFolder}`); + containerIds = await listContainers(params, false, labels); + + // If no containers found by local folder, try by config file + if (containerIds.length === 0) { + const configPath = configFile ? URI.file(path.resolve(process.cwd(), configFile)) : await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath); + if (configPath) { + const configFilePath = uriToFsPath(configPath, cliHost.platform); + labels.length = 0; // Clear labels + if (idLabel) labels.push(idLabel); + labels.push(`devcontainer.config_file=${configFilePath}`); + containerIds = await listContainers(params, false, labels); + } + } + } else if (idLabel) { + // Stop containers with specific label + containerIds = await listContainers(params, false, [idLabel]); + } + + const stoppedContainers: string[] = []; + const errors: string[] = []; + + for (const containerId of containerIds) { + try { + const text = `Stopping container ${containerId.substring(0, 12)}...`; + const start = output.start(text); + await dockerCLI(params, 'stop', containerId); + output.stop(text, start); + stoppedContainers.push(containerId); + } catch (err) { + errors.push(`Failed to stop container ${containerId}: ${err.message || err}`); + } + } + + return { + outcome: errors.length === 0 ? 'success' : 'error', + message: errors.length > 0 ? errors.join('\n') : undefined, + stoppedContainers, + containersFound: containerIds.length, + }; + } catch (err) { + return { + outcome: 'error', + message: err.message || String(err), + stoppedContainers: [], + containersFound: 0, + }; + } +} + +async function downContainers(params: DockerCLIParameters, options: { all?: boolean; workspaceFolder?: string; configFile?: string; idLabel?: string; removeVolumes?: boolean }) { + const { all, workspaceFolder, configFile, idLabel, removeVolumes } = options; + const { cliHost, output } = params; + + try { + let containerIds: string[] = []; + let volumesToRemove: string[] = []; + + if (all) { + // Remove all dev containers - look for containers with devcontainer labels + const labels = ['devcontainer.local_folder']; + containerIds = await listContainers(params, true, labels); + } else if (workspaceFolder || configFile) { + // Remove containers related to a specific workspace + const resolvedWorkspaceFolder = workspaceFolder ? path.resolve(process.cwd(), workspaceFolder) : process.cwd(); + const workspace = workspaceFromPath(cliHost.path, resolvedWorkspaceFolder); + + // First, try to find containers with matching local folder + const labels = idLabel ? [idLabel] : []; + labels.push(`devcontainer.local_folder=${resolvedWorkspaceFolder}`); + containerIds = await listContainers(params, true, labels); + + // Check if it's a Docker Compose configuration + const configPath = configFile ? URI.file(path.resolve(process.cwd(), configFile)) : await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath); + if (configPath) { + const configs = await readDevContainerConfigFile(cliHost, workspace, configPath, true, output); + if (configs) { + const { config } = configs; + if ('dockerComposeFile' in config.config) { + // Handle Docker Compose down + const composeFiles = await getDockerComposeFilePaths(cliHost, config.config, cliHost.env, cliHost.cwd); + const cwdEnvFile = cliHost.path.join(cliHost.cwd, '.env'); + const envFile = Array.isArray(config.config.dockerComposeFile) && config.config.dockerComposeFile.length === 0 && await cliHost.isFile(cwdEnvFile) ? cwdEnvFile : undefined; + const composeConfig = await readDockerComposeConfig(params, composeFiles, envFile); + const projectName = await getProjectName(params, workspace, composeFiles, composeConfig); + + const text = `Running docker-compose down for project ${projectName}...`; + const start = output.start(text); + const args = ['down']; + if (removeVolumes) { + args.push('-v'); + } + await dockerComposeCLICommand(params, ...composeFiles.map(f => ['-f', f]).flat(), '-p', projectName, ...args); + output.stop(text, start); + + return { + outcome: 'success', + message: undefined, + removedContainers: [], + containersFound: 0, + dockerComposeProject: projectName, + }; + } else if (containerIds.length === 0) { + // If no containers found by local folder and it's not compose, try by config file + const configFilePath = uriToFsPath(configPath, cliHost.platform); + labels.length = 0; // Clear labels + if (idLabel) labels.push(idLabel); + labels.push(`devcontainer.config_file=${configFilePath}`); + containerIds = await listContainers(params, true, labels); + } + } + } + } else if (idLabel) { + // Remove containers with specific label + containerIds = await listContainers(params, true, [idLabel]); + } + + // Collect volumes if needed + if (removeVolumes && containerIds.length > 0) { + for (const containerId of containerIds) { + try { + const containerDetails = await inspectContainer(params, containerId); + for (const mount of containerDetails.Mounts) { + if (mount.Type === 'volume' && mount.Name) { + volumesToRemove.push(mount.Name); + } + } + } catch (err) { + // Continue even if inspection fails + } + } + } + + const removedContainers: string[] = []; + const removedVolumes: string[] = []; + const errors: string[] = []; + + // Remove containers + for (const containerId of containerIds) { + try { + const text = `Removing container ${containerId.substring(0, 12)}...`; + const start = output.start(text); + await removeContainer(params, containerId); + output.stop(text, start); + removedContainers.push(containerId); + } catch (err) { + errors.push(`Failed to remove container ${containerId}: ${err.message || err}`); + } + } + + // Remove volumes if requested + if (removeVolumes && volumesToRemove.length > 0) { + const uniqueVolumes = [...new Set(volumesToRemove)]; + for (const volume of uniqueVolumes) { + try { + const text = `Removing volume ${volume}...`; + const start = output.start(text); + await dockerCLI(params, 'volume', 'rm', volume); + output.stop(text, start); + removedVolumes.push(volume); + } catch (err) { + // Volume might be in use or already removed + } + } + } + + return { + outcome: errors.length === 0 ? 'success' : 'error', + message: errors.length > 0 ? errors.join('\n') : undefined, + removedContainers, + removedVolumes, + containersFound: containerIds.length, + }; + } catch (err) { + return { + outcome: 'error', + message: err.message || String(err), + removedContainers: [], + removedVolumes: [], + containersFound: 0, + }; + } +} From 69dc74a3abeff3af534b39420047857d85457e20 Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Sat, 5 Jul 2025 11:25:55 +0700 Subject: [PATCH 2/3] fix: add container observability for docker-compose in stop/down commands - List containers before stopping/removing in docker-compose projects - Return actual container IDs in removedContainers/stoppedContainers arrays - Report correct containersFound count for docker-compose configurations - Ensures proper visibility of which containers are affected by operations --- src/spec-node/devContainersSpecCLI.ts | 55 +++++++++++++++++++++------ 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index eb1a64d59..16665aecd 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -1627,15 +1627,44 @@ async function stopContainers(params: DockerCLIParameters, options: { all?: bool labels.push(`devcontainer.local_folder=${resolvedWorkspaceFolder}`); containerIds = await listContainers(params, false, labels); - // If no containers found by local folder, try by config file - if (containerIds.length === 0) { - const configPath = configFile ? URI.file(path.resolve(process.cwd(), configFile)) : await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath); - if (configPath) { - const configFilePath = uriToFsPath(configPath, cliHost.platform); - labels.length = 0; // Clear labels - if (idLabel) labels.push(idLabel); - labels.push(`devcontainer.config_file=${configFilePath}`); - containerIds = await listContainers(params, false, labels); + // Check if it's a Docker Compose configuration + const configPath = configFile ? URI.file(path.resolve(process.cwd(), configFile)) : await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath); + if (configPath) { + const configs = await readDevContainerConfigFile(cliHost, workspace, configPath, true, output); + if (configs) { + const { config } = configs; + if ('dockerComposeFile' in config.config) { + // Handle Docker Compose stop + const composeFiles = await getDockerComposeFilePaths(cliHost, config.config, cliHost.env, cliHost.cwd); + const cwdEnvFile = cliHost.path.join(cliHost.cwd, '.env'); + const envFile = Array.isArray(config.config.dockerComposeFile) && config.config.dockerComposeFile.length === 0 && await cliHost.isFile(cwdEnvFile) ? cwdEnvFile : undefined; + const composeConfig = await readDockerComposeConfig(params, composeFiles, envFile); + const projectName = await getProjectName(params, workspace, composeFiles, composeConfig); + + // List containers before stopping them for observability + const projectLabel = `com.docker.compose.project=${projectName}`; + const projectContainers = await listContainers(params, false, [projectLabel]); + + const text = `Running docker-compose stop for project ${projectName}...`; + const start = output.start(text); + await dockerComposeCLICommand(params, ...composeFiles.map(f => ['-f', f]).flat(), '-p', projectName, 'stop'); + output.stop(text, start); + + return { + outcome: 'success', + message: undefined, + stoppedContainers: projectContainers, + containersFound: projectContainers.length, + dockerComposeProject: projectName, + }; + } else if (containerIds.length === 0) { + // If no containers found by local folder and it's not compose, try by config file + const configFilePath = uriToFsPath(configPath, cliHost.platform); + labels.length = 0; // Clear labels + if (idLabel) labels.push(idLabel); + labels.push(`devcontainer.config_file=${configFilePath}`); + containerIds = await listContainers(params, false, labels); + } } } } else if (idLabel) { @@ -1710,6 +1739,10 @@ async function downContainers(params: DockerCLIParameters, options: { all?: bool const composeConfig = await readDockerComposeConfig(params, composeFiles, envFile); const projectName = await getProjectName(params, workspace, composeFiles, composeConfig); + // List containers before removing them for observability + const projectLabel = `com.docker.compose.project=${projectName}`; + const projectContainers = await listContainers(params, true, [projectLabel]); + const text = `Running docker-compose down for project ${projectName}...`; const start = output.start(text); const args = ['down']; @@ -1722,8 +1755,8 @@ async function downContainers(params: DockerCLIParameters, options: { all?: bool return { outcome: 'success', message: undefined, - removedContainers: [], - containersFound: 0, + removedContainers: projectContainers, + containersFound: projectContainers.length, dockerComposeProject: projectName, }; } else if (containerIds.length === 0) { From 39e61776f73cc83ee01e44f52e3a9d16fcca5b01 Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Mon, 7 Jul 2025 15:53:22 +0700 Subject: [PATCH 3/3] fix: address PR review comments for stop/down commands - Use proper logging output instead of dropping log messages - Add dockerPath parameter support for Docker/Podman compatibility - Change --all flag to use devcontainer.metadata label for safer filtering - Add error logging for container inspection and volume removal failures - Follow existing codebase patterns for dockerComposeCLI configuration --- src/spec-node/devContainersSpecCLI.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 16665aecd..0fece7276 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -50,14 +50,12 @@ const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell'; const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,external=(true|false))?$/; -function getDockerComposeCLI(cliHost: CLIHost, options: { dockerComposePath?: string }) { +function getDockerComposeCLI(cliHost: CLIHost, output: Log, options: { dockerPath?: string; dockerComposePath?: string }) { return dockerComposeCLIConfig({ exec: cliHost.exec, env: cliHost.env, - output: makeLog({ - event: () => undefined, - }, LogLevel.Info), - }, 'docker', options.dockerComposePath || 'docker-compose'); + output, + }, options.dockerPath || 'docker', options.dockerComposePath || 'docker-compose'); } (async () => { @@ -1269,7 +1267,7 @@ async function stop({ const params: DockerCLIParameters = { cliHost, dockerCLI: dockerPath || 'docker', - dockerComposeCLI: getDockerComposeCLI(cliHost, { dockerComposePath }), + dockerComposeCLI: getDockerComposeCLI(cliHost, output, { dockerPath, dockerComposePath }), env: cliHost.env, output, platformInfo: { @@ -1352,7 +1350,7 @@ async function down({ const params: DockerCLIParameters = { cliHost, dockerCLI: dockerPath || 'docker', - dockerComposeCLI: getDockerComposeCLI(cliHost, { dockerComposePath }), + dockerComposeCLI: getDockerComposeCLI(cliHost, output, { dockerPath, dockerComposePath }), env: cliHost.env, output, platformInfo: { @@ -1615,7 +1613,7 @@ async function stopContainers(params: DockerCLIParameters, options: { all?: bool if (all) { // Stop all dev containers - look for containers with devcontainer labels - const labels = ['devcontainer.local_folder']; + const labels = ['devcontainer.metadata']; containerIds = await listContainers(params, false, labels); } else if (workspaceFolder || configFile) { // Stop containers related to a specific workspace @@ -1713,7 +1711,7 @@ async function downContainers(params: DockerCLIParameters, options: { all?: bool if (all) { // Remove all dev containers - look for containers with devcontainer labels - const labels = ['devcontainer.local_folder']; + const labels = ['devcontainer.metadata']; containerIds = await listContainers(params, true, labels); } else if (workspaceFolder || configFile) { // Remove containers related to a specific workspace @@ -1786,6 +1784,7 @@ async function downContainers(params: DockerCLIParameters, options: { all?: bool } } catch (err) { // Continue even if inspection fails + output.write(`Failed to inspect container ${containerId}: ${err.message || err}`); } } } @@ -1819,6 +1818,7 @@ async function downContainers(params: DockerCLIParameters, options: { all?: bool removedVolumes.push(volume); } catch (err) { // Volume might be in use or already removed + output.write(`Failed to remove volume ${volume}: ${err.message || err}`); } } }