Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Notable changes.

## December 2025

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

## November 2025

### [0.80.2]
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.80.2",
"version": "0.81.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 @@ -148,11 +148,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 @@ -507,7 +505,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 @@ -574,7 +572,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 @@ -752,7 +750,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.' },
Expand Down Expand Up @@ -785,7 +783,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 @@ -954,7 +952,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.' },
Expand All @@ -975,7 +973,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 @@ -1107,7 +1105,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 @@ -1139,7 +1137,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 @@ -1209,7 +1207,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.' },
'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.' },
Expand Down Expand Up @@ -1243,7 +1241,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`; // 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}`);
}
});

});
});
Loading