Skip to content

Commit 8f16144

Browse files
authored
DevContainer commands now default the workspace folder to the current directory if not specified (#1104)
2 parents f27ecff + 010aab5 commit 8f16144

File tree

13 files changed

+491
-26
lines changed

13 files changed

+491
-26
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Notable changes.
44

55
## January 2026
66

7+
### [0.82.0]
8+
- devcontainer commands now use current directory as default workspace folder when not specified (https://github.com/devcontainers/cli/pull/1104)
9+
710
### [0.81.1]
811
- Update js-yaml and glob dependencies. (https://github.com/devcontainers/cli/pull/1128)
912

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@devcontainers/cli",
33
"description": "Dev Containers CLI",
4-
"version": "0.81.1",
4+
"version": "0.82.0",
55
"bin": {
66
"devcontainer": "devcontainer.js"
77
},

src/spec-node/devContainersSpecCLI.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ function provisionOptions(y: Argv) {
103103
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
104104
'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' },
105105
'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' },
106-
'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' },
106+
'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.' },
107107
'workspace-mount-consistency': { choices: ['consistent' as 'consistent', 'cached' as 'cached', 'delegated' as 'delegated'], default: 'cached' as 'cached', description: 'Workspace mount consistency.' },
108108
'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.' },
109109
'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' },
@@ -149,11 +149,9 @@ function provisionOptions(y: Argv) {
149149
if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) {
150150
throw new Error('Unmatched argument format: id-label must match <name>=<value>');
151151
}
152-
if (!(argv['workspace-folder'] || argv['id-label'])) {
153-
throw new Error('Missing required argument: workspace-folder or id-label');
154-
}
155-
if (!(argv['workspace-folder'] || argv['override-config'])) {
156-
throw new Error('Missing required argument: workspace-folder or override-config');
152+
// Default workspace-folder to current directory if not provided and no id-label or override-config
153+
if (!argv['workspace-folder'] && !argv['id-label'] && !argv['override-config']) {
154+
argv['workspace-folder'] = process.cwd();
157155
}
158156
const mounts = (argv.mount && (Array.isArray(argv.mount) ? argv.mount : [argv.mount])) as string[] | undefined;
159157
if (mounts?.some(mount => !mountRegex.test(mount))) {
@@ -511,7 +509,7 @@ function buildOptions(y: Argv) {
511509
'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' },
512510
'docker-path': { type: 'string', description: 'Docker CLI path.' },
513511
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
514-
'workspace-folder': { type: 'string', required: true, description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' },
512+
'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.' },
515513
'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.' },
516514
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' },
517515
'log-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text' as 'text', description: 'Log format.' },
@@ -578,7 +576,7 @@ async function doBuild({
578576
await Promise.all(disposables.map(d => d()));
579577
};
580578
try {
581-
const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg);
579+
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd();
582580
const configFile: URI | undefined = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined;
583581
const overrideConfigFile: URI | undefined = /* overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : */ undefined;
584582
const addCacheFroms = addCacheFrom ? (Array.isArray(addCacheFrom) ? addCacheFrom as string[] : [addCacheFrom]) : [];
@@ -757,7 +755,7 @@ function runUserCommandsOptions(y: Argv) {
757755
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
758756
'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' },
759757
'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' },
760-
'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' },
758+
'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.' },
761759
'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' },
762760
'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`).' },
763761
'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' },
@@ -791,7 +789,7 @@ function runUserCommandsOptions(y: Argv) {
791789
throw new Error('Unmatched argument format: remote-env must match <name>=<value>');
792790
}
793791
if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) {
794-
throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.');
792+
argv['workspace-folder'] = process.cwd();
795793
}
796794
return true;
797795
});
@@ -962,7 +960,7 @@ function readConfigurationOptions(y: Argv) {
962960
'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' },
963961
'docker-path': { type: 'string', description: 'Docker CLI path.' },
964962
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
965-
'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' },
963+
'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.' },
966964
'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' },
967965
'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`).' },
968966
'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' },
@@ -984,7 +982,7 @@ function readConfigurationOptions(y: Argv) {
984982
throw new Error('Unmatched argument format: id-label must match <name>=<value>');
985983
}
986984
if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) {
987-
throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.');
985+
argv['workspace-folder'] = process.cwd();
988986
}
989987
return true;
990988
});
@@ -1117,7 +1115,7 @@ async function readConfiguration({
11171115
function outdatedOptions(y: Argv) {
11181116
return y.options({
11191117
'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' },
1120-
'workspace-folder': { type: 'string', required: true, description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' },
1118+
'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.' },
11211119
'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.' },
11221120
'output-format': { choices: ['text' as 'text', 'json' as 'json'], default: 'text', description: 'Output format.' },
11231121
'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.' },
@@ -1149,7 +1147,7 @@ async function outdated({
11491147
};
11501148
let output: Log | undefined;
11511149
try {
1152-
const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg);
1150+
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd();
11531151
const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined;
11541152
const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, logFormat === 'text');
11551153
const extensionPath = path.join(__dirname, '..', '..');
@@ -1219,7 +1217,7 @@ function execOptions(y: Argv) {
12191217
'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' },
12201218
'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' },
12211219
'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' },
1222-
'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' },
1220+
'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.' },
12231221
'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' },
12241222
'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`).' },
12251223
'container-id': { type: 'string', description: 'Id of the container to run the user commands for.' },
@@ -1254,7 +1252,7 @@ function execOptions(y: Argv) {
12541252
throw new Error('Unmatched argument format: remote-env must match <name>=<value>');
12551253
}
12561254
if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) {
1257-
throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.');
1255+
argv['workspace-folder'] = process.cwd();
12581256
}
12591257
return true;
12601258
});

src/spec-node/featuresCLI/resolveDependencies.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function featuresResolveDependenciesOptions(y: Argv) {
3030
return y
3131
.options({
3232
'log-level': { choices: ['error' as 'error', 'info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'error' as 'error', description: 'Log level.' },
33-
'workspace-folder': { type: 'string', description: 'Workspace folder to use for the configuration.', demandOption: true },
33+
'workspace-folder': { type: 'string', description: 'Workspace folder to use for the configuration. If --workspace-folder is not provided, this defaults to the current directory' },
3434
});
3535
}
3636

@@ -41,7 +41,7 @@ export function featuresResolveDependenciesHandler(args: featuresResolveDependen
4141
}
4242

4343
async function featuresResolveDependencies({
44-
'workspace-folder': workspaceFolder,
44+
'workspace-folder': workspaceFolderArg,
4545
'log-level': inputLogLevel,
4646
}: featuresResolveDependenciesArgs) {
4747
const disposables: (() => Promise<unknown> | undefined)[] = [];
@@ -62,6 +62,8 @@ async function featuresResolveDependencies({
6262

6363
let jsonOutput: JsonOutput = {};
6464

65+
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd();
66+
6567
// Detect path to dev container config
6668
let configPath = path.join(workspaceFolder, '.devcontainer.json');
6769
if (!(await isLocalFile(configPath))) {

src/spec-node/templatesCLI/apply.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import * as jsonc from 'jsonc-parser';
66
import { UnpackArgv } from '../devContainersSpecCLI';
77
import { fetchTemplate, SelectedTemplate, TemplateFeatureOption, TemplateOptions } from '../../spec-configuration/containerTemplatesOCI';
88
import { runAsyncHandler } from '../utils';
9+
import path from 'path';
910

1011
export function templateApplyOptions(y: Argv) {
1112
return y
1213
.options({
13-
'workspace-folder': { type: 'string', alias: 'w', demandOption: true, default: '.', description: 'Target workspace folder to apply Template' },
14+
'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' },
1415
'template-id': { type: 'string', alias: 't', demandOption: true, description: 'Reference to a Template in a supported OCI registry' },
1516
'template-args': { type: 'string', alias: 'a', default: '{}', description: 'Arguments to replace within the provided Template, provided as JSON' },
1617
'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) {
3031
}
3132

3233
async function templateApply({
33-
'workspace-folder': workspaceFolder,
34+
'workspace-folder': workspaceFolderArg,
3435
'template-id': templateId,
3536
'template-args': templateArgs,
3637
'features': featuresArgs,
@@ -42,6 +43,7 @@ async function templateApply({
4243
const dispose = async () => {
4344
await Promise.all(disposables.map(d => d()));
4445
};
46+
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd();
4547

4648
const pkg = getPackageConfig();
4749

src/spec-node/upgradeCommand.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { mapNodeArchitectureToGOARCH, mapNodeOSToGOOS } from '../spec-configurat
2323
export function featuresUpgradeOptions(y: Argv) {
2424
return y
2525
.options({
26-
'workspace-folder': { type: 'string', description: 'Workspace folder.', demandOption: true },
26+
'workspace-folder': { type: 'string', description: 'Workspace folder. If --workspace-folder is not provided defaults to the current directory.' },
2727
'docker-path': { type: 'string', description: 'Path to docker executable.', default: 'docker' },
2828
'docker-compose-path': { type: 'string', description: 'Path to docker-compose executable.', default: 'docker-compose' },
2929
'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) {
3737
if (argv.feature && !argv['target-version'] || !argv.feature && argv['target-version']) {
3838
throw new Error('The \'--target-version\' and \'--feature\' flag must be used together.');
3939
}
40-
4140
if (argv['target-version']) {
4241
const targetVersion = argv['target-version'];
4342
if (!targetVersion.match(/^\d+(\.\d+(\.\d+)?)?$/)) {
@@ -70,7 +69,7 @@ async function featuresUpgrade({
7069
};
7170
let output: Log | undefined;
7271
try {
73-
const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg);
72+
const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : process.cwd();
7473
const configFile = configArg ? URI.file(path.resolve(process.cwd(), configArg)) : undefined;
7574
const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, true);
7675
const extensionPath = path.join(__dirname, '..', '..');

src/test/cli.build.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,5 +433,49 @@ describe('Dev Containers CLI', function () {
433433
const details = JSON.parse((await shellExec(`docker inspect ${response.imageName}`)).stdout)[0] as ImageDetails;
434434
assert.strictEqual(details.Config.Labels?.test_build_options, 'success');
435435
});
436+
437+
it('should use current directory for build when no workspace-folder provided', async function () {
438+
const testFolder = `${__dirname}/configs/image`;
439+
const absoluteTmpPath = path.resolve(__dirname, 'tmp');
440+
const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`;
441+
const originalCwd = process.cwd();
442+
console.log(`Original cwd: ${originalCwd}`);
443+
console.log(`Changing to test folder: ${testFolder}`);
444+
try {
445+
process.chdir(testFolder);
446+
const res = await shellExec(`${absoluteCli} build`);
447+
const response = JSON.parse(res.stdout);
448+
assert.equal(response.outcome, 'success');
449+
assert.ok(response.imageName);
450+
} finally {
451+
process.chdir(originalCwd);
452+
}
453+
});
454+
455+
it('should fail gracefully when no workspace-folder and no config in current directory', async function () {
456+
const tempDir = path.join(os.tmpdir(), 'devcontainer-build-test-' + Date.now());
457+
await shellExec(`mkdir -p ${tempDir}`);
458+
const absoluteTmpPath = path.resolve(__dirname, 'tmp');
459+
const absoluteCli = `npx --prefix ${absoluteTmpPath} devcontainer`;
460+
const originalCwd = process.cwd();
461+
try {
462+
process.chdir(tempDir);
463+
let success = false;
464+
try {
465+
await shellExec(`${absoluteCli} build`);
466+
success = true;
467+
} catch (error) {
468+
assert.equal(error.error.code, 1, 'Should fail with exit code 1');
469+
const res = JSON.parse(error.stdout);
470+
assert.equal(res.outcome, 'error');
471+
assert.match(res.message, /Dev container config .* not found/);
472+
}
473+
assert.equal(success, false, 'expect non-successful call');
474+
} finally {
475+
process.chdir(originalCwd);
476+
await shellExec(`rm -rf ${tempDir}`);
477+
}
478+
});
479+
436480
});
437481
});

0 commit comments

Comments
 (0)