diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 16e316cb4..05a664b8a 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import fs from 'fs'; import * as path from 'path'; import yargs, { Argv } from 'yargs'; @@ -82,6 +83,16 @@ const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,externa })().catch(console.error); +function findWorkspaceFolder(): string | undefined { + const workspacePath = path.join(process.cwd(), '.devcontainer'); + if (fs.existsSync(workspacePath)) { + if (fs.lstatSync(workspacePath).isDirectory()) { + return process.cwd(); + } + } + return undefined; +} + export type UnpackArgv = T extends Argv ? U : T; function provisionOptions(y: Argv) { @@ -124,14 +135,12 @@ function provisionOptions(y: Argv) { }) .check(argv => { const idLabels = (argv['id-label'] && (Array.isArray(argv['id-label']) ? argv['id-label'] : [argv['id-label']])) as string[] | undefined; + const isWorkspaceFound = argv['workspace-folder'] || findWorkspaceFolder(); 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'); + if (!(argv['id-label'] || argv['override-config'] || isWorkspaceFound)) { + throw new Error('Missing required argument: workspace-folder or id-label or override-config'); } const mounts = (argv.mount && (Array.isArray(argv.mount) ? argv.mount : [argv.mount])) as string[] | undefined; if (mounts?.some(mount => !mountRegex.test(mount))) { @@ -142,6 +151,10 @@ function provisionOptions(y: Argv) { throw new Error('Unmatched argument format: remote-env must match ='); } return true; + }).middleware((argv) => { + if (!(argv['id-label'] || argv['override-config'] || argv['workspace-folder'])) { + argv['workspace-folder'] = findWorkspaceFolder(); + } }); } @@ -188,7 +201,6 @@ async function provision({ 'dotfiles-target-path': dotfilesTargetPath, 'container-session-data-folder': containerSessionDataFolder, }: ProvisionArgs) { - const workspaceFolder = workspaceFolderArg ? path.resolve(process.cwd(), workspaceFolderArg) : undefined; const addRemoteEnvs = addRemoteEnv ? (Array.isArray(addRemoteEnv) ? addRemoteEnv as string[] : [addRemoteEnv]) : []; const addCacheFroms = addCacheFrom ? (Array.isArray(addCacheFrom) ? addCacheFrom as string[] : [addCacheFrom]) : []; @@ -451,7 +463,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', default: '.', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path.' }, '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.' }, 'no-cache': { type: 'boolean', default: false, description: 'Builds the image with `--no-cache`.' }, @@ -465,6 +477,14 @@ function buildOptions(y: Argv) { 'skip-feature-auto-mapping': { type: 'boolean', default: false, hidden: true, description: 'Temporary option for testing.' }, 'experimental-image-metadata': { type: 'boolean', default: experimentalImageMetadataDefault, hidden: true, description: 'Temporary option for testing.' }, 'skip-persisting-customizations-from-features': { type: 'boolean', default: false, hidden: true, description: 'Do not save customizations from referenced Features as image metadata' }, + }).check(argv => { + if (argv['workspace-folder'] === '.') { + const workspaceFolder = findWorkspaceFolder(); + if (!workspaceFolder) { + throw new Error('Unable to find Configuration File, provide workspace-folder argument'); + } + } + return true; }); } @@ -696,6 +716,8 @@ function runUserCommandsOptions(y: Argv) { }) .check(argv => { const idLabels = (argv['id-label'] && (Array.isArray(argv['id-label']) ? argv['id-label'] : [argv['id-label']])) as string[] | undefined; + const isWorkspaceFound = argv['workspace-folder'] || findWorkspaceFolder(); + console.debug(isWorkspaceFound); if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) { throw new Error('Unmatched argument format: id-label must match ='); } @@ -703,10 +725,14 @@ function runUserCommandsOptions(y: Argv) { if (remoteEnvs?.some(remoteEnv => !/.+=.+/.test(remoteEnv))) { throw new Error('Unmatched argument format: remote-env must match ='); } - if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) { + if (!(argv['container-id'] || idLabels?.length || isWorkspaceFound)) { throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.'); } return true; + }).middleware((argv) => { + if (!(argv['id-label'] || argv['override-config'] || argv['workspace-folder'])) { + argv['workspace-folder'] = findWorkspaceFolder(); + } }); } @@ -882,13 +908,18 @@ function readConfigurationOptions(y: Argv) { }) .check(argv => { const idLabels = (argv['id-label'] && (Array.isArray(argv['id-label']) ? argv['id-label'] : [argv['id-label']])) as string[] | undefined; + const isWorkspaceFound = argv['workspace-folder'] || findWorkspaceFolder(); if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) { throw new Error('Unmatched argument format: id-label must match ='); } - if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) { + if (!(argv['container-id'] || idLabels?.length || isWorkspaceFound)) { throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.'); } return true; + }).middleware((argv) => { + if (!(argv['id-label'] || argv['override-config'] || argv['workspace-folder'])) { + argv['workspace-folder'] = findWorkspaceFolder(); + } }); } @@ -1053,6 +1084,7 @@ function execOptions(y: Argv) { }) .check(argv => { const idLabels = (argv['id-label'] && (Array.isArray(argv['id-label']) ? argv['id-label'] : [argv['id-label']])) as string[] | undefined; + const isWorkspaceFound = argv['workspace-folder'] || findWorkspaceFolder(); if (idLabels?.some(idLabel => !/.+=.+/.test(idLabel))) { throw new Error('Unmatched argument format: id-label must match ='); } @@ -1060,10 +1092,14 @@ function execOptions(y: Argv) { if (remoteEnvs?.some(remoteEnv => !/.+=.+/.test(remoteEnv))) { throw new Error('Unmatched argument format: remote-env must match ='); } - if (!argv['container-id'] && !idLabels?.length && !argv['workspace-folder']) { + if (!(argv['container-id'] || idLabels?.length || isWorkspaceFound)) { throw new Error('Missing required argument: One of --container-id, --id-label or --workspace-folder is required.'); } return true; + }).middleware((argv) => { + if (!(argv['id-label'] || argv['override-config'] || argv['workspace-folder'])) { + argv['workspace-folder'] = findWorkspaceFolder(); + } }); }