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
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.80.3",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it 0.81.0 and list the improvement in the changelog.

"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 not provided, defaults to the current directory.' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention --container-id and --id-label like in the descriptions of the other commands.

'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.' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention using the current folder when not provided like in the other descriptions.

});
}

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', default: '.', description: 'Target workspace folder to apply Template' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention the default like in the other descriptions. Maybe omit default: '.', though I'm not sure that shows anywhere (like in the help text).

'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