From 77de275e6e28fb658fe24d92ff41a2c03a346d0b Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:48:39 +0000 Subject: [PATCH 01/19] For Exec Defaults workspace folder to current directory if not specified --- src/spec-node/devContainersSpecCLI.ts | 4 ++-- src/test/cli.exec.base.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 59136695d..8acaef6e8 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -1209,7 +1209,7 @@ function execOptions(y: Argv) { 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, - 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. Defaults to current directory if not specified. The devcontainer.json will be looked up relative to this path.' }, 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, 'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' }, 'id-label': { type: 'string', description: 'Id label(s) of the format name=value. If no --container-id is given the id labels will be used to look up the container. If no --id-label is given, one will be inferred from the --workspace-folder path.' }, @@ -1243,7 +1243,7 @@ function execOptions(y: Argv) { throw new Error('Unmatched argument format: remote-env must match ='); } if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) { - throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.'); + argv['workspace-folder'] = process.cwd(); } return true; }); diff --git a/src/test/cli.exec.base.ts b/src/test/cli.exec.base.ts index 10e876595..9ba470af8 100644 --- a/src/test/cli.exec.base.ts +++ b/src/test/cli.exec.base.ts @@ -82,6 +82,22 @@ export function describeTests1({ text, options }: BuildKitOption) { assert.strictEqual(env.FOO, 'BAR'); assert.strictEqual(env.BAZ, ''); }); + it('should exec with default workspace folder (current directory)', async () => { + const originalCwd = process.cwd(); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + process.chdir(testFolder); + + try { + // Exec without --workspace-folder should use current directory as default + const execRes = await shellExec(`${absoluteCli} exec echo "default workspace test"`); + assert.strictEqual(execRes.error, null); + assert.match(execRes.stdout, /default workspace test/); + } finally { + // Restore original directory + process.chdir(originalCwd); + } + }); }); describe(`with valid (image) config containing features [${text}]`, () => { let containerId: string | null = null; From 070aa87b9829aa05daa6cd0e69835f2915b6632b Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:18:51 +0000 Subject: [PATCH 02/19] update error message --- src/spec-node/devContainersSpecCLI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 8acaef6e8..9ada4519f 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -1209,7 +1209,7 @@ function execOptions(y: Argv) { 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, - 'workspace-folder': { type: 'string', description: 'Workspace folder path. Defaults to current directory if not specified. The devcontainer.json will be looked up relative to this path.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If --container-id, --id-label, and --workspace-folder are not provided, this defaults to the current directory.' }, 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, 'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' }, 'id-label': { type: 'string', description: 'Id label(s) of the format name=value. If no --container-id is given the id labels will be used to look up the container. If no --id-label is given, one will be inferred from the --workspace-folder path.' }, From 260a884f7db745714b38735c3ae979f40dbeadb8 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:29:31 +0000 Subject: [PATCH 03/19] devcontainer outdated command CWD as default folder --- src/spec-node/devContainersSpecCLI.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 9ada4519f..5e2bdf64e 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -1107,14 +1107,20 @@ async function readConfiguration({ function outdatedOptions(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.' }, - 'workspace-folder': { type: 'string', required: true, description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If --workspace-folder is not provided, defaults to the current directory.' }, '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.' }, 'output-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text', description: 'Output format.' }, 'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level for the --terminal-log-file. When set to trace, the log level for --log-file will also be set to trace.' }, 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, 'terminal-columns': { type: 'number', implies: ['terminal-rows'], description: 'Number of columns to render the output for. This is required for some of the subprocesses to correctly render their output.' }, 'terminal-rows': { type: 'number', implies: ['terminal-columns'], description: 'Number of rows to render the output for. This is required for some of the subprocesses to correctly render their output.' }, - }); + }) + .check(argv => { + if (!argv['workspace-folder']) { + argv['workspace-folder'] = process.cwd(); + } + return true; + }); } type OutdatedArgs = UnpackArgv>; From afc2a34dceff23fb25b6d887bbfb99529f8ec582 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:46:24 +0000 Subject: [PATCH 04/19] upgradecommand CWD as default folder --- src/spec-node/upgradeCommand.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts index 3336087c7..b3a9b1cc9 100644 --- a/src/spec-node/upgradeCommand.ts +++ b/src/spec-node/upgradeCommand.ts @@ -23,7 +23,7 @@ import { mapNodeArchitectureToGOARCH, mapNodeOSToGOOS } from '../spec-configurat export function featuresUpgradeOptions(y: Argv) { return y .options({ - 'workspace-folder': { type: 'string', description: 'Workspace folder.', demandOption: true }, + 'workspace-folder': { type: 'string', description: 'Workspace folder. If --workspace-folder is not provided defaults to the current directory.' }, 'docker-path': { type: 'string', description: 'Path to docker executable.', default: 'docker' }, 'docker-compose-path': { type: 'string', description: 'Path to docker-compose executable.', default: 'docker-compose' }, '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.' }, @@ -37,7 +37,6 @@ export function featuresUpgradeOptions(y: Argv) { if (argv.feature && !argv['target-version'] || !argv.feature && argv['target-version']) { throw new Error('The \'--target-version\' and \'--feature\' flag must be used together.'); } - if (argv['target-version']) { const targetVersion = argv['target-version']; if (!targetVersion.match(/^\d+(\.\d+(\.\d+)?)?$/)) { @@ -70,7 +69,7 @@ async function featuresUpgrade({ }; let output: Log | undefined; try { - const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg); + const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd(); const configFile = configArg ? URI.file(path.resolve(process.cwd(), configArg)) : undefined; const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, true); const extensionPath = path.join(__dirname, '..', '..'); From bbb3c3a1e619a3d08edd1b947c05dcd01418247e Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Mon, 17 Nov 2025 09:20:27 +0000 Subject: [PATCH 05/19] cwd default for build, runusercommands, readconfig --- src/spec-node/devContainersSpecCLI.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 5e2bdf64e..fd8070b59 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -507,7 +507,7 @@ function buildOptions(y: Argv) { 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, 'docker-path': { type: 'string', description: 'Docker CLI path.' }, 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, - 'workspace-folder': { type: 'string', required: true, description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If not provided, defaults to the current directory.' }, '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.' }, @@ -526,6 +526,12 @@ function buildOptions(y: Argv) { 'experimental-lockfile': { type: 'boolean', default: false, hidden: true, description: 'Write lockfile' }, 'experimental-frozen-lockfile': { type: 'boolean', default: false, hidden: true, description: 'Ensure lockfile remains unchanged' }, 'omit-syntax-directive': { type: 'boolean', default: false, hidden: true, description: 'Omit Dockerfile syntax directives' }, + }) + .check(argv => { + if (!argv['workspace-folder']) { + argv['workspace-folder'] = process.cwd(); + } + return true; }); } @@ -752,7 +758,7 @@ function runUserCommandsOptions(y: Argv) { 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, - 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path.The devcontainer.json will be looked up relative to this path. If --container-id, --id-label, and --workspace-folder are not provided, this defaults to the current directory.' }, 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, 'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' }, 'id-label': { type: 'string', description: 'Id label(s) of the format name=value. If no --container-id is given the id labels will be used to look up the container. If no --id-label is given, one will be inferred from the --workspace-folder path.' }, @@ -785,7 +791,7 @@ function runUserCommandsOptions(y: Argv) { throw new Error('Unmatched argument format: remote-env must match ='); } if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) { - throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.'); + argv['workspace-folder'] = process.cwd(); } return true; }); @@ -954,7 +960,7 @@ function readConfigurationOptions(y: Argv) { 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, '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.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If --container-id, --id-label, and --workspace-folder are not provided, this defaults to the current directory.' }, 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, 'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' }, 'id-label': { type: 'string', description: 'Id label(s) of the format name=value. If no --container-id is given the id labels will be used to look up the container. If no --id-label is given, one will be inferred from the --workspace-folder path.' }, @@ -975,7 +981,7 @@ function readConfigurationOptions(y: Argv) { throw new Error('Unmatched argument format: id-label must match ='); } if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) { - throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.'); + argv['workspace-folder'] = process.cwd(); } return true; }); From 7a3282a3ca85945080741f58bcf83a063f9e1233 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:19:28 +0000 Subject: [PATCH 06/19] Type 'undefined' is not assignable to type 'string'. --- src/spec-node/devContainersSpecCLI.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index fd8070b59..4bd7af245 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -580,7 +580,7 @@ async function doBuild({ await Promise.all(disposables.map(d => d())); }; try { - const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg); + const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd(); const configFile: URI | undefined = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined; const overrideConfigFile: URI | undefined = /* overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : */ undefined; const addCacheFroms = addCacheFrom ? (Array.isArray(addCacheFrom) ? addCacheFrom as string[] : [addCacheFrom]) : []; @@ -1151,7 +1151,7 @@ async function outdated({ }; let output: Log | undefined; try { - const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg); + const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd(); const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined; const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, logFormat === 'text'); const extensionPath = path.join(__dirname, '..', '..'); From 633f21a3fdd1825753f69a4c4a8f5530a48b3170 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:27:04 +0000 Subject: [PATCH 07/19] Remove check --- src/spec-node/devContainersSpecCLI.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 4bd7af245..fcc4f53d5 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -526,12 +526,6 @@ function buildOptions(y: Argv) { 'experimental-lockfile': { type: 'boolean', default: false, hidden: true, description: 'Write lockfile' }, 'experimental-frozen-lockfile': { type: 'boolean', default: false, hidden: true, description: 'Ensure lockfile remains unchanged' }, 'omit-syntax-directive': { type: 'boolean', default: false, hidden: true, description: 'Omit Dockerfile syntax directives' }, - }) - .check(argv => { - if (!argv['workspace-folder']) { - argv['workspace-folder'] = process.cwd(); - } - return true; }); } @@ -1120,13 +1114,7 @@ function outdatedOptions(y: Argv) { 'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' }, 'terminal-columns': { type: 'number', implies: ['terminal-rows'], description: 'Number of columns to render the output for. This is required for some of the subprocesses to correctly render their output.' }, 'terminal-rows': { type: 'number', implies: ['terminal-columns'], description: 'Number of rows to render the output for. This is required for some of the subprocesses to correctly render their output.' }, - }) - .check(argv => { - if (!argv['workspace-folder']) { - argv['workspace-folder'] = process.cwd(); - } - return true; - }); + }); } type OutdatedArgs = UnpackArgv>; From b3821cbc2f4bfea155693de1b8c49bd3e6c53cf2 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:27:44 +0000 Subject: [PATCH 08/19] template apply --- src/spec-node/templatesCLI/apply.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/spec-node/templatesCLI/apply.ts b/src/spec-node/templatesCLI/apply.ts index ed410a8c7..24fc33d88 100644 --- a/src/spec-node/templatesCLI/apply.ts +++ b/src/spec-node/templatesCLI/apply.ts @@ -6,11 +6,12 @@ import * as jsonc from 'jsonc-parser'; import { UnpackArgv } from '../devContainersSpecCLI'; import { fetchTemplate, SelectedTemplate, TemplateFeatureOption, TemplateOptions } from '../../spec-configuration/containerTemplatesOCI'; import { runAsyncHandler } from '../utils'; +import path from 'path'; export function templateApplyOptions(y: Argv) { return y .options({ - 'workspace-folder': { type: 'string', alias: 'w', demandOption: true, default: '.', description: 'Target workspace folder to apply Template' }, + 'workspace-folder': { type: 'string', alias: 'w', default: '.', description: 'Target workspace folder to apply Template' }, 'template-id': { type: 'string', alias: 't', demandOption: true, description: 'Reference to a Template in a supported OCI registry' }, 'template-args': { type: 'string', alias: 'a', default: '{}', description: 'Arguments to replace within the provided Template, provided as JSON' }, 'features': { type: 'string', alias: 'f', default: '[]', description: 'Features to add to the provided Template, provided as JSON.' }, @@ -30,7 +31,7 @@ export function templateApplyHandler(args: TemplateApplyArgs) { } async function templateApply({ - 'workspace-folder': workspaceFolder, + 'workspace-folder': workspaceFolderArg, 'template-id': templateId, 'template-args': templateArgs, 'features': featuresArgs, @@ -42,6 +43,7 @@ async function templateApply({ const dispose = async () => { await Promise.all(disposables.map(d => d())); }; + const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd(); const pkg = getPackageConfig(); From ac701cc86cb19c207440b7080b91b2345477d689 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:38:06 +0000 Subject: [PATCH 09/19] feature resolve-dependencies --- src/spec-node/featuresCLI/resolveDependencies.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/spec-node/featuresCLI/resolveDependencies.ts b/src/spec-node/featuresCLI/resolveDependencies.ts index 93e569ff6..7996a5281 100644 --- a/src/spec-node/featuresCLI/resolveDependencies.ts +++ b/src/spec-node/featuresCLI/resolveDependencies.ts @@ -30,7 +30,7 @@ export function featuresResolveDependenciesOptions(y: Argv) { return y .options({ 'log-level': { choices: ['error' as 'error', 'info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'error' as 'error', description: 'Log level.' }, - 'workspace-folder': { type: 'string', description: 'Workspace folder to use for the configuration.', demandOption: true }, + 'workspace-folder': { type: 'string', description: 'Workspace folder to use for the configuration.' }, }); } @@ -41,7 +41,7 @@ export function featuresResolveDependenciesHandler(args: featuresResolveDependen } async function featuresResolveDependencies({ - 'workspace-folder': workspaceFolder, + 'workspace-folder': workspaceFolderArg, 'log-level': inputLogLevel, }: featuresResolveDependenciesArgs) { const disposables: (() => Promise | undefined)[] = []; @@ -62,6 +62,8 @@ async function featuresResolveDependencies({ let jsonOutput: JsonOutput = {}; + const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd(); + // Detect path to dev container config let configPath = path.join(workspaceFolder, '.devcontainer.json'); if (!(await isLocalFile(configPath))) { From 7533b0be72edc199923a9679aac162658101a67a Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Wed, 19 Nov 2025 08:13:15 +0000 Subject: [PATCH 10/19] update cli version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index abbde6d76..183b7c685 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.80.2", + "version": "0.80.3", "bin": { "devcontainer": "devcontainer.js" }, From 3fbe2bcc83275fb504cca71aac3a7e50bb783473 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:16:08 +0000 Subject: [PATCH 11/19] exec test cases --- src/test/cli.exec.base.ts | 57 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/test/cli.exec.base.ts b/src/test/cli.exec.base.ts index 9ba470af8..57ba48c0c 100644 --- a/src/test/cli.exec.base.ts +++ b/src/test/cli.exec.base.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as path from 'path'; +import * as os from 'os'; import { BuildKitOption, commandMarkerTests, devContainerDown, devContainerStop, devContainerUp, pathExists, shellBufferExec, shellExec, shellPtyExec } from './testUtils'; const pkg = require('../../package.json'); @@ -209,7 +210,7 @@ export function describeTests1({ text, options }: BuildKitOption) { } export function describeTests2({ text, options }: BuildKitOption) { - + describe('Dev Containers CLI', function () { this.timeout('300s'); @@ -422,6 +423,60 @@ export function describeTests2({ text, options }: BuildKitOption) { await shellExec(`docker rm -f ${response.containerId}`); }); + + describe.only('Command exec with default workspace', () => { + it('should fail gracefully when no config in current directory and no container-id', async () => { + const tempDir = path.join(os.tmpdir(), 'devcontainer-exec-test-' + Date.now()); + await shellExec(`mkdir -p ${tempDir}`); + const originalCwd = process.cwd(); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + try { + process.chdir(tempDir); + let success = false; + try { + // Test exec without --workspace-folder (should default to current directory with no config) + await shellExec(`${absoluteCli} exec echo "test"`); + success = true; + } catch (error) { + console.log('Caught error as expected: ', error.stderr); + // Should fail because there's no container or config + assert.equal(error.error.code, 1, 'Should fail with exit code 1'); + } + assert.equal(success, false, 'expect non-successful call'); + } finally { + process.chdir(originalCwd); + await shellExec(`rm -rf ${tempDir}`); + } + }); + + describe('with valid config in current directory', () => { + let containerId: string | null = null; + const testFolder = `${__dirname}/configs/image`; + + beforeEach(async () => { + containerId = (await devContainerUp(cli, testFolder, options)).containerId; + }); + + afterEach(async () => await devContainerDown({ containerId })); + + it('should execute command successfully when using current directory', async () => { + const originalCwd = process.cwd(); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + try { + process.chdir(testFolder); + // Test exec without --workspace-folder (should default to current directory) + const res = await shellExec(`${absoluteCli} exec echo "hello world"`); + assert.strictEqual(res.error, null); + assert.match(res.stdout, /hello world/); + } finally { + process.chdir(originalCwd); + } + }); + }); + + }); }); }); } From 621c930ed7a3de6317a99088aa05ef526d0d2d72 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:19:10 +0000 Subject: [PATCH 12/19] remove only --- src/test/cli.exec.base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/cli.exec.base.ts b/src/test/cli.exec.base.ts index 57ba48c0c..681aac775 100644 --- a/src/test/cli.exec.base.ts +++ b/src/test/cli.exec.base.ts @@ -210,7 +210,7 @@ export function describeTests1({ text, options }: BuildKitOption) { } export function describeTests2({ text, options }: BuildKitOption) { - + describe('Dev Containers CLI', function () { this.timeout('300s'); @@ -424,7 +424,7 @@ export function describeTests2({ text, options }: BuildKitOption) { await shellExec(`docker rm -f ${response.containerId}`); }); - describe.only('Command exec with default workspace', () => { + describe('Command exec with default workspace', () => { it('should fail gracefully when no config in current directory and no container-id', async () => { const tempDir = path.join(os.tmpdir(), 'devcontainer-exec-test-' + Date.now()); await shellExec(`mkdir -p ${tempDir}`); From fc5e5703ae6e60fa2ce0de858596156f20bf8758 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:57:59 +0000 Subject: [PATCH 13/19] devcontainer outdated test --- src/test/container-features/lockfile.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/container-features/lockfile.test.ts b/src/test/container-features/lockfile.test.ts index 57034e6f2..fb3894218 100644 --- a/src/test/container-features/lockfile.test.ts +++ b/src/test/container-features/lockfile.test.ts @@ -258,4 +258,24 @@ describe('Lockfile', function () { await cleanup(); } }); + + it('outdated command should work with default workspace folder', async () => { + const workspaceFolder = path.join(__dirname, 'configs/lockfile-outdated-command'); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + + const originalCwd = process.cwd(); + try { + process.chdir(workspaceFolder); + const res = await shellExec(`${absoluteCli} outdated --output-format json`); + const response = JSON.parse(res.stdout); + + // Should have same structure as the test with explicit workspace-folder + assert.ok(response.features); + assert.ok(response.features['ghcr.io/devcontainers/features/git:1.0']); + assert.strictEqual(response.features['ghcr.io/devcontainers/features/git:1.0'].current, '1.0.4'); + } finally { + process.chdir(originalCwd); + } + }); }); \ No newline at end of file From 3e34e78d0223f3bd9035e13e57cce959258ea691 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:10:15 +0000 Subject: [PATCH 14/19] upgrade command test --- src/test/container-features/lockfile.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/container-features/lockfile.test.ts b/src/test/container-features/lockfile.test.ts index fb3894218..96fcd4045 100644 --- a/src/test/container-features/lockfile.test.ts +++ b/src/test/container-features/lockfile.test.ts @@ -278,4 +278,24 @@ describe('Lockfile', function () { process.chdir(originalCwd); } }); + + it('upgrade command should work with default workspace folder', async () => { + const workspaceFolder = path.join(__dirname, 'configs/lockfile-upgrade-command'); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + + const lockfilePath = path.join(workspaceFolder, '.devcontainer-lock.json'); + await cpLocal(path.join(workspaceFolder, 'outdated.devcontainer-lock.json'), lockfilePath); + + const originalCwd = process.cwd(); + try { + process.chdir(workspaceFolder); + await shellExec(`${absoluteCli} upgrade`); + const actual = await readLocalFile(lockfilePath); + const expected = await readLocalFile(path.join(workspaceFolder, 'upgraded.devcontainer-lock.json')); + assert.equal(actual.toString(), expected.toString()); + } finally { + process.chdir(originalCwd); + } + }); }); \ No newline at end of file From 202aa2d90af3ae55f341630a41ebe8cb039c8a25 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:19:26 +0000 Subject: [PATCH 15/19] run-user-commands test --- src/test/cli.test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/cli.test.ts b/src/test/cli.test.ts index 522c9073d..d9f16dde3 100644 --- a/src/test/cli.test.ts +++ b/src/test/cli.test.ts @@ -68,6 +68,37 @@ describe('Dev Containers CLI', function () { await shellExec(`docker rm -f ${upResponse.containerId}`); }); + + it('run-user-commands should run with default workspace folder (current directory)', async () => { + const testFolder = `${__dirname}/configs/image`; + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + + // First, ensure container is up + const upRes = await shellExec(`${cli} up --workspace-folder ${testFolder} --skip-post-create`); + const upResponse = JSON.parse(upRes.stdout); + assert.strictEqual(upResponse.outcome, 'success'); + const containerId = upResponse.containerId; + + const originalCwd = process.cwd(); + try { + // Change to workspace folder + process.chdir(testFolder); + + // Run user commands without --workspace-folder should use current directory as default + const runRes = await shellExec(`${absoluteCli} run-user-commands`); + const runResponse = JSON.parse(runRes.stdout); + assert.strictEqual(runResponse.outcome, 'success'); + + // Verify that the postCreateCommand was executed + await shellExec(`docker exec ${containerId} test -f /postCreateCommand.txt`); + } finally { + // Restore original directory + process.chdir(originalCwd); + // Clean up container + await shellExec(`docker rm -f ${containerId}`); + } + }); }); describe('Command read-configuration', () => { From d2d47d6fdcfbc13fee9f6d1cb98b9882d3876992 Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:13:14 +0000 Subject: [PATCH 16/19] Read-user commands test --- src/test/cli.test.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/test/cli.test.ts b/src/test/cli.test.ts index d9f16dde3..f409cb0fd 100644 --- a/src/test/cli.test.ts +++ b/src/test/cli.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as path from 'path'; +import * as os from 'os'; import { devContainerDown, devContainerUp, shellExec } from './testUtils'; const pkg = require('../../package.json'); @@ -99,6 +100,28 @@ describe('Dev Containers CLI', function () { await shellExec(`docker rm -f ${containerId}`); } }); + + it('run-user-commands should fail gracefully when no config in current directory and no container-id', async () => { + const tempDir = path.join(os.tmpdir(), 'devcontainer-run-test-' + Date.now()); + await shellExec(`mkdir -p ${tempDir}`); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + const originalCwd = process.cwd(); + try { + process.chdir(tempDir); + let success = false; + try { + await shellExec(`${absoluteCli} run-user-commands`); + success = true; + } catch (error) { + assert.equal(error.error.code, 1, 'Should fail with exit code 1'); + } + assert.equal(success, false, 'expect non-successful call'); + } finally { + process.chdir(originalCwd); + await shellExec(`rm -rf ${tempDir}`); + } + }); }); describe('Command read-configuration', () => { @@ -155,5 +178,42 @@ describe('Dev Containers CLI', function () { const response = JSON.parse(res.stdout); assert.strictEqual(response.configuration.remoteEnv.SUBFOLDER_CONFIG_REMOTE_ENV, 'true'); }); + + it('should use current directory for read-configuration when no workspace-folder provided', async () => { + const testFolder = `${__dirname}/configs/image`; + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + const originalCwd = process.cwd(); + try { + process.chdir(testFolder); + const res = await shellExec(`${absoluteCli} read-configuration`); + const response = JSON.parse(res.stdout); + assert.equal(response.configuration.image, 'ubuntu:latest'); + } finally { + process.chdir(originalCwd); + } + }); + + it('should fail gracefully when no workspace-folder and no config in current directory', async () => { + const tempDir = path.join(os.tmpdir(), 'devcontainer-test-' + Date.now()); + await shellExec(`mkdir -p ${tempDir}`); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + const originalCwd = process.cwd(); + try { + process.chdir(tempDir); + let success = false; + try { + await shellExec(`${absoluteCli} read-configuration`); + success = true; + } catch (error) { + assert.equal(error.error.code, 1, 'Should fail with exit code 1'); + } + assert.equal(success, false, 'expect non-successful call'); + } finally { + process.chdir(originalCwd); + await shellExec(`rm -rf ${tempDir}`); + } + }); }); }); \ No newline at end of file From 98c33163095d7884b4af54d4b747561e2affd2fe Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:28:57 +0000 Subject: [PATCH 17/19] build test --- src/test/cli.build.test.ts | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/test/cli.build.test.ts b/src/test/cli.build.test.ts index 0dfae0427..c57e2f4d5 100644 --- a/src/test/cli.build.test.ts +++ b/src/test/cli.build.test.ts @@ -433,5 +433,49 @@ describe('Dev Containers CLI', function () { const details = JSON.parse((await shellExec(`docker inspect ${response.imageName}`)).stdout)[0] as ImageDetails; assert.strictEqual(details.Config.Labels?.test_build_options, 'success'); }); + + it('should use current directory for build when no workspace-folder provided', async function () { + const testFolder = `${__dirname}/configs/image`; // Use simpler config without features + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + const originalCwd = process.cwd(); + console.log(`Original cwd: ${originalCwd}`); + console.log(`Changing to test folder: ${testFolder}`); + try { + process.chdir(testFolder); + const res = await shellExec(`${absoluteCli} build`); + const response = JSON.parse(res.stdout); + assert.equal(response.outcome, 'success'); + assert.ok(response.imageName); + } finally { + process.chdir(originalCwd); + } + }); + + it('should fail gracefully when no workspace-folder and no config in current directory', async function () { + const tempDir = path.join(os.tmpdir(), 'devcontainer-build-test-' + Date.now()); + await shellExec(`mkdir -p ${tempDir}`); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + const originalCwd = process.cwd(); + try { + process.chdir(tempDir); + let success = false; + try { + await shellExec(`${absoluteCli} build`); + success = true; + } catch (error) { + assert.equal(error.error.code, 1, 'Should fail with exit code 1'); + const res = JSON.parse(error.stdout); + assert.equal(res.outcome, 'error'); + assert.match(res.message, /Dev container config .* not found/); + } + assert.equal(success, false, 'expect non-successful call'); + } finally { + process.chdir(originalCwd); + await shellExec(`rm -rf ${tempDir}`); + } + }); + }); }); From bcb365297e266f4bc1c14477ecbe2d944555c10f Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:51:54 +0000 Subject: [PATCH 18/19] devcontainer up cwd and test --- src/spec-node/devContainersSpecCLI.ts | 10 +++--- src/test/cli.up.test.ts | 48 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index fcc4f53d5..2183318e6 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -103,7 +103,7 @@ function provisionOptions(y: Argv) { 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, - 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, + 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If not provided, defaults to the current directory.' }, 'workspace-mount-consistency': { choices: ['consistent' as 'consistent', 'cached' as 'cached', 'delegated' as 'delegated'], default: 'cached' as 'cached', description: 'Workspace mount consistency.' }, 'gpu-availability': { choices: ['all' as 'all', 'detect' as 'detect', 'none' as 'none'], default: 'detect' as 'detect', description: 'Availability of GPUs in case the dev container requires any. `all` expects a GPU to be available.' }, 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, @@ -148,11 +148,9 @@ function provisionOptions(y: Argv) { if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) { throw new Error('Unmatched argument format: id-label must match ='); } - if (!(argv['workspace-folder'] || argv['id-label'])) { - throw new Error('Missing required argument: workspace-folder or id-label'); - } - if (!(argv['workspace-folder'] || argv['override-config'])) { - throw new Error('Missing required argument: workspace-folder or override-config'); + // Default workspace-folder to current directory if not provided and no id-label or override-config + if (!argv['workspace-folder'] && !argv['id-label'] && !argv['override-config']) { + argv['workspace-folder'] = process.cwd(); } const mounts = (argv.mount && (Array.isArray(argv.mount) ? argv.mount : [argv.mount])) as string[] | undefined; if (mounts?.some(mount => !mountRegex.test(mount))) { diff --git a/src/test/cli.up.test.ts b/src/test/cli.up.test.ts index 94515e89a..688a4c84d 100644 --- a/src/test/cli.up.test.ts +++ b/src/test/cli.up.test.ts @@ -310,4 +310,52 @@ describe('Dev Containers CLI', function () { await shellExec(`docker rm -f ${response.containerId}`); }); }); + + describe('Command up with default workspace', () => { + it('should create and start container using current directory config', async () => { + const testFolder = `${__dirname}/configs/image`; + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + const originalCwd = process.cwd(); + let containerId: string | null = null; + try { + process.chdir(testFolder); + const res = await shellExec(`${absoluteCli} up`); + const response = JSON.parse(res.stdout); + containerId = response.containerId; + assert.equal(response.outcome, 'success'); + assert.ok(containerId); + } finally { + process.chdir(originalCwd); + if (containerId) { + await shellExec(`docker rm -f ${containerId}`); + } + } + }); + + it('should fail gracefully when no config in current directory', async () => { + const tempDir = path.join(os.tmpdir(), 'devcontainer-up-test-' + Date.now()); + await shellExec(`mkdir -p ${tempDir}`); + const absoluteTmpPath = path.resolve(__dirname, 'tmp'); + const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`; + const originalCwd = process.cwd(); + try { + process.chdir(tempDir); + let success = false; + try { + await shellExec(`${absoluteCli} up`); + success = true; + } catch (error) { + assert.equal(error.error.code, 1, 'Should fail with exit code 1'); + const res = JSON.parse(error.stdout); + assert.equal(res.outcome, 'error'); + assert.match(res.message, /Dev container config .* not found/); + } + assert.equal(success, false, 'expect non-successful call'); + } finally { + process.chdir(originalCwd); + await shellExec(`rm -rf ${tempDir}`); + } + }); + }); }); \ No newline at end of file From a348b3ffb5b1a0954be41ccd4e43e2245e7d025e Mon Sep 17 00:00:00 2001 From: Mathiyarasy <157102811+Mathiyarasy@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:22:27 +0000 Subject: [PATCH 19/19] features resolve-dependencies and template apply test --- .../featuresCLICommands.test.ts | 113 ++++++++++++++++++ .../templatesCLICommands.test.ts | 54 +++++++++ 2 files changed, 167 insertions(+) diff --git a/src/test/container-features/featuresCLICommands.test.ts b/src/test/container-features/featuresCLICommands.test.ts index 1d20d5b1a..340abdb21 100644 --- a/src/test/container-features/featuresCLICommands.test.ts +++ b/src/test/container-features/featuresCLICommands.test.ts @@ -425,6 +425,119 @@ describe('CLI features subcommands', async function () { }); }); + describe('features resolve-dependencies', function () { + + it('should resolve dependencies when workspace-folder defaults to current directory', async function () { + // Create a test config with features that have dependencies + const testConfigPath = path.resolve(__dirname, 'configs/feature-dependencies/dependsOn/oci-ab'); + const originalCwd = process.cwd(); + + try { + // Change to test config directory to test default workspace folder behavior + process.chdir(testConfigPath); + + // Use absolute path to CLI to prevent npm ENOENT errors + const absoluteTmpPath = path.resolve(originalCwd, tmp); + const absoluteCliPath = `npx --prefix ${absoluteTmpPath} devcontainer`; + + // First check if the config file exists + const configExists = require('fs').existsSync('.devcontainer/devcontainer.json') || + require('fs').existsSync('.devcontainer.json'); + assert.isTrue(configExists, 'Test config file should exist'); + + let result; + try { + result = await shellExec(`${absoluteCliPath} features resolve-dependencies --log-level trace`); + } catch (error: any) { + // If command fails, log details for debugging + console.error('Command failed:', error); + if (error.stderr) { + console.error('STDERR:', error.stderr); + } + if (error.stdout) { + console.error('STDOUT:', error.stdout); + } + throw error; + } + + // Verify the command succeeded + assert.isDefined(result); + assert.isString(result.stdout); + assert.isNotEmpty(result.stdout.trim(), 'Command should produce output'); + + // Parse the JSON output to verify it contains expected structure + let jsonOutput; + try { + // Try parsing stdout directly first + jsonOutput = JSON.parse(result.stdout.trim()); + } catch (parseError) { + // If direct parsing fails, try extracting JSON from mixed output + const lines = result.stdout.split('\n'); + + // Find the last occurrence of '{' that starts a complete JSON object + let jsonStartIndex = -1; + let jsonEndIndex = -1; + let braceCount = 0; + + // Work backwards from the end to find the complete JSON + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i].trim(); + if (line === '}' && jsonEndIndex === -1) { + jsonEndIndex = i; + braceCount = 1; + } else if (jsonEndIndex !== -1) { + // Count braces to find matching opening + for (const char of line) { + if (char === '}') { + braceCount++; + } else if (char === '{') { + braceCount--; + } + } + if (braceCount === 0 && line === '{') { + jsonStartIndex = i; + break; + } + } + } + + if (jsonStartIndex >= 0 && jsonEndIndex >= 0) { + // Extract just the JSON lines + const jsonLines = lines.slice(jsonStartIndex, jsonEndIndex + 1); + const jsonString = jsonLines.join('\n'); + try { + jsonOutput = JSON.parse(jsonString); + } catch (innerError) { + console.error('Failed to parse extracted JSON:', jsonString.substring(0, 500) + '...'); + throw new Error(`Failed to parse extracted JSON: ${innerError}`); + } + } else { + console.error('Could not find complete JSON in output'); + console.error('Last 10 lines:', lines.slice(-10)); + throw new Error(`Failed to find complete JSON in output: ${parseError}`); + } + } + + assert.isDefined(jsonOutput, 'Should have valid JSON output'); + assert.property(jsonOutput, 'installOrder'); + assert.isArray(jsonOutput.installOrder); + + // Verify the install order contains the expected features + const installOrder = jsonOutput.installOrder; + assert.isAbove(installOrder.length, 0, 'Install order should contain at least one feature'); + + // Each item should have id and options + installOrder.forEach((item: any) => { + assert.property(item, 'id'); + assert.property(item, 'options'); + }); + + } finally { + process.chdir(originalCwd); + } + }); + }); + describe('features package', function () { it('features package subcommand by collection', async function () { diff --git a/src/test/container-templates/templatesCLICommands.test.ts b/src/test/container-templates/templatesCLICommands.test.ts index babb4f8f2..1a91e2a2d 100644 --- a/src/test/container-templates/templatesCLICommands.test.ts +++ b/src/test/container-templates/templatesCLICommands.test.ts @@ -62,6 +62,60 @@ describe('tests apply command', async function () { // Assert that the Feature included in the command was added. assert.match(file, /"ghcr.io\/devcontainers\/features\/azure-cli:1": {\n/); }); + + it('templates apply subcommand with default workspace folder', async function () { + const testOutputPath = path.resolve(__dirname, 'tmp-default-workspace'); + const originalCwd = process.cwd(); + + try { + // Create and change to test output directory to test default workspace folder behavior + await shellExec(`rm -rf ${testOutputPath}`); + await shellExec(`mkdir -p ${testOutputPath}`); + process.chdir(testOutputPath); + + // Use absolute path to CLI to prevent npm ENOENT errors + const absoluteTmpPath = path.resolve(originalCwd, tmp); + const absoluteCliPath = `npx --prefix ${absoluteTmpPath} devcontainer`; + + let success = false; + let result: ExecResult | undefined = undefined; + + try { + // Run without --workspace-folder to test default behavior + result = await shellExec(`${absoluteCliPath} templates apply \ + --template-id ghcr.io/devcontainers/templates/docker-from-docker:latest \ + --template-args '{ "installZsh": "false", "upgradePackages": "true", "dockerVersion": "20.10", "moby": "true", "enableNonRootDocker": "true" }' \ + --log-level trace`); + success = true; + + } catch (error) { + assert.fail('templates apply sub-command should not throw when using default workspace folder'); + } + + assert.isTrue(success); + assert.isDefined(result); + assert.strictEqual(result.stdout.trim(), '{"files":["./.devcontainer/devcontainer.json"]}'); + + // Verify the file was created in the current working directory (default workspace folder) + const file = (await readLocalFile(path.join(testOutputPath, '.devcontainer', 'devcontainer.json'))).toString(); + + assert.match(file, /"name": "Docker from Docker"/); + assert.match(file, /"installZsh": "false"/); + assert.match(file, /"upgradePackages": "true"/); + assert.match(file, /"version": "20.10"/); + assert.match(file, /"moby": "true"/); + assert.match(file, /"enableNonRootDocker": "true"/); + + // Assert that the Features included in the template were not removed. + assert.match(file, /"ghcr.io\/devcontainers\/features\/common-utils:1": {\n/); + assert.match(file, /"ghcr.io\/devcontainers\/features\/docker-from-docker:1": {\n/); + + } finally { + process.chdir(originalCwd); + // Clean up test directory + await shellExec(`rm -rf ${testOutputPath}`); + } + }); }); describe('tests packageTemplates()', async function () {