Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
77de275
For Exec Defaults workspace folder to current directory if not specified
Mathiyarasy Nov 12, 2025
070aa87
update error message
Mathiyarasy Nov 12, 2025
260a884
devcontainer outdated command CWD as default folder
Mathiyarasy Nov 17, 2025
afc2a34
upgradecommand CWD as default folder
Mathiyarasy Nov 17, 2025
bbb3c3a
cwd default for build, runusercommands, readconfig
Mathiyarasy Nov 17, 2025
f542ed9
Merge branch 'main' into dev/Mathi/Issue1069
Mathiyarasy Nov 17, 2025
7a3282a
Type 'undefined' is not assignable to type 'string'.
Mathiyarasy Nov 18, 2025
633f21a
Remove check
Mathiyarasy Nov 18, 2025
b3821cb
template apply
Mathiyarasy Nov 18, 2025
ac701cc
feature resolve-dependencies
Mathiyarasy Nov 18, 2025
7533b0b
update cli version
Mathiyarasy Nov 19, 2025
3fbe2bc
exec test cases
Mathiyarasy Nov 21, 2025
621c930
remove only
Mathiyarasy Nov 21, 2025
fc5e570
devcontainer outdated test
Mathiyarasy Nov 21, 2025
3e34e78
upgrade command test
Mathiyarasy Nov 21, 2025
202aa2d
run-user-commands test
Mathiyarasy Nov 21, 2025
d2d47d6
Read-user commands test
Mathiyarasy Nov 24, 2025
98c3316
build test
Mathiyarasy Nov 24, 2025
bcb3652
devcontainer up cwd and test
Mathiyarasy Nov 24, 2025
a348b3f
features resolve-dependencies and template apply test
Mathiyarasy Nov 24, 2025
3712c3d
Merge branch 'main' into dev/Mathi/Issue1069
Mathiyarasy Nov 24, 2025
4cd749d
update comments
Mathiyarasy Dec 2, 2025
2b01a90
Apply suggestion from @mr-brobot
chrmarti Jan 21, 2026
e7e269c
Merge branch 'main' into dev/Mathi/Issue1069
chrmarti Jan 21, 2026
010aab5
remove comments
Mathiyarasy Jan 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Notable changes.

## January 2026

### [0.82.0]
- devcontainer commands now use current directory as default workspace folder when not specified (https://github.com/devcontainers/cli/pull/1104)

### [0.81.1]
- Update js-yaml and glob dependencies. (https://github.com/devcontainers/cli/pull/1128)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@devcontainers/cli",
"description": "Dev Containers CLI",
"version": "0.81.1",
"version": "0.82.0",
"bin": {
"devcontainer": "devcontainer.js"
},
Expand Down
30 changes: 14 additions & 16 deletions src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 --id-label, --override-config, and --workspace-folder are not provided, this 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.' },
Expand Down Expand Up @@ -149,11 +149,9 @@ function provisionOptions(y: Argv) {
if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) {
throw new Error('Unmatched argument format: id-label must match <name>=<value>');
}
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))) {
Expand Down Expand Up @@ -511,7 +509,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.' },
Expand Down Expand Up @@ -578,7 +576,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]) : [];
Expand Down Expand Up @@ -757,7 +755,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.' },
'mount-git-worktree-common-dir': { type: 'boolean', default: false, description: 'Mount the Git worktree common dir for Git operations to work in the container. This requires the worktree to be created with relative paths (`git worktree add --relative-paths`).' },
'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' },
Expand Down Expand Up @@ -791,7 +789,7 @@ function runUserCommandsOptions(y: Argv) {
throw new Error('Unmatched argument format: remote-env must match <name>=<value>');
}
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;
});
Expand Down Expand Up @@ -962,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.' },
'mount-git-worktree-common-dir': { type: 'boolean', default: false, description: 'Mount the Git worktree common dir for Git operations to work in the container. This requires the worktree to be created with relative paths (`git worktree add --relative-paths`).' },
'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' },
Expand All @@ -984,7 +982,7 @@ function readConfigurationOptions(y: Argv) {
throw new Error('Unmatched argument format: id-label must match <name>=<value>');
}
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;
});
Expand Down Expand Up @@ -1117,7 +1115,7 @@ 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.' },
Expand Down Expand Up @@ -1149,7 +1147,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, '..', '..');
Expand Down Expand Up @@ -1219,7 +1217,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. 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.' },
'mount-git-worktree-common-dir': { type: 'boolean', default: false, description: 'Mount the Git worktree common dir for Git operations to work in the container. This requires the worktree to be created with relative paths (`git worktree add --relative-paths`).' },
'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' },
Expand Down Expand Up @@ -1254,7 +1252,7 @@ function execOptions(y: Argv) {
throw new Error('Unmatched argument format: remote-env must match <name>=<value>');
}
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;
});
Expand Down
6 changes: 4 additions & 2 deletions src/spec-node/featuresCLI/resolveDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. If --workspace-folder is not provided, this defaults to the current directory' },
});
}

Expand All @@ -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<unknown> | undefined)[] = [];
Expand All @@ -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))) {
Expand Down
6 changes: 4 additions & 2 deletions src/spec-node/templatesCLI/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', description: 'Target workspace folder to apply Template. If --workspace-folder is not provided, this defaults to the current directory' },
'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.' },
Expand All @@ -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,
Expand All @@ -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();

Expand Down
5 changes: 2 additions & 3 deletions src/spec-node/upgradeCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.' },
Expand All @@ -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+)?)?$/)) {
Expand Down Expand Up @@ -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, '..', '..');
Expand Down
44 changes: 44 additions & 0 deletions src/test/cli.build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
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}`);
}
});

});
});
Loading