diff --git a/messages/retrieve.start.md b/messages/retrieve.start.md index 34431ec9..f03725cb 100644 --- a/messages/retrieve.start.md +++ b/messages/retrieve.start.md @@ -204,3 +204,7 @@ This command expects the org to support source tracking. If it doesn't, you must Starting in December 2025, this command will require that the target org use source tracking. Specifically, to use this command with a production org, scratch org created with the `--no-track-source` flag, or other non-source-tracking org, you must specify the metadata you want to retrieve with either the `--metadata`, `--source-dir`, or `--manifest` flag. + +# outputDirOutsideProject + +The output directory must be inside the current project. The path relative you provided %s is outside the project root. diff --git a/src/commands/project/retrieve/start.ts b/src/commands/project/retrieve/start.ts index 9435511e..22fa8191 100644 --- a/src/commands/project/retrieve/start.ts +++ b/src/commands/project/retrieve/start.ts @@ -15,7 +15,7 @@ */ import { rm } from 'node:fs/promises'; -import { dirname, join, resolve, sep } from 'node:path'; +import { dirname, join, relative, resolve, sep } from 'node:path'; import * as fs from 'node:fs'; import { MultiStageOutput } from '@oclif/multi-stage-output'; @@ -204,6 +204,16 @@ export default class RetrieveMetadata extends SfCommand { if (SfProject.getInstance()?.getPackageNameFromPath(resolvedTargetDir)) { throw messages.createError('retrieveTargetDirOverlapsPackage', [flags['output-dir']]); } + + // Ensure --output-dir is inside the current project directory + const project = await getOptionalProject(); + if (project) { + const rel = relative(project.getPath(), resolvedTargetDir); + if (rel.startsWith('..')) { + // resolvedTargetDir is outside the project path + throw messages.createError('outputDirOutsideProject', [flags['output-dir']]); + } + } } const format = flags['target-metadata-dir'] ? 'metadata' : 'source'; const zipFileName = flags['zip-file-name'] ?? DEFAULT_ZIP_FILE_NAME; diff --git a/test/commands/retrieve/start.test.ts b/test/commands/retrieve/start.test.ts index a2935af9..7406cf26 100644 --- a/test/commands/retrieve/start.test.ts +++ b/test/commands/retrieve/start.test.ts @@ -157,9 +157,9 @@ describe('project retrieve start', () => { // @ts-ignore const renameStub = $$.SANDBOX.stub(RetrieveMetadata.prototype, 'moveResultsForRetrieveTargetDir').resolves(); - const sourcepath = ['somepath']; + const outputDir = resolve(expectedDirectoryPath, '..', 'retrieveOutput'); const metadata = ['ApexClass:MyClass']; - const result = await RetrieveMetadata.run(['--output-dir', sourcepath[0], '--metadata', metadata[0], '--json']); + const result = await RetrieveMetadata.run(['--output-dir', outputDir, '--metadata', metadata[0], '--json']); expect(result).to.deep.equal(expectedResults); ensureCreateComponentSetArgs({ sourcepath: undefined, @@ -168,7 +168,7 @@ describe('project retrieve start', () => { metadataEntries: ['ApexClass:MyClass'], }, }); - ensureRetrieveArgs({ output: resolve(sourcepath[0]), format: 'source' }); + ensureRetrieveArgs({ output: resolve(outputDir), format: 'source' }); expect(renameStub.calledOnce).to.be.true; });