From 2f8e35c189090d8357854e1d9380ff3c243ec026 Mon Sep 17 00:00:00 2001 From: soridalac Date: Fri, 11 Jul 2025 09:09:54 -0700 Subject: [PATCH] fix: output-dir for hidden directory --- src/commands/project/retrieve/start.ts | 74 +++++++++++++++++++++++--- test/commands/retrieve/start.test.ts | 28 +++++++--- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/commands/project/retrieve/start.ts b/src/commands/project/retrieve/start.ts index f79134776..d14d2523a 100644 --- a/src/commands/project/retrieve/start.ts +++ b/src/commands/project/retrieve/start.ts @@ -176,10 +176,16 @@ export default class RetrieveMetadata extends SfCommand { // eslint-disable-next-line complexity public async run(): Promise { const { flags } = await this.parse(RetrieveMetadata); + let resolvedTargetDir: string | undefined; if (flags['output-dir']) { resolvedTargetDir = resolve(flags['output-dir']); - if (SfProject.getInstance()?.getPackageNameFromPath(resolvedTargetDir)) { + + await fs.promises.mkdir(resolvedTargetDir, { recursive: true }); + + const packageName = SfProject.getInstance()?.getPackageNameFromPath(resolvedTargetDir); + + if (packageName && !flags['output-dir'].startsWith('.')) { throw messages.createError('retrieveTargetDirOverlapsPackage', [flags['output-dir']]); } } @@ -271,9 +277,42 @@ export default class RetrieveMetadata extends SfCommand { this.ms.stop(); - // flags['output-dir'] will set resolvedTargetDir var, so this check is redundant, but allows for nice typings in the moveResultsForRetrieveTargetDir method if (flags['output-dir'] && resolvedTargetDir) { - await this.moveResultsForRetrieveTargetDir(flags['output-dir'], resolvedTargetDir); + // Determine the actual source directory where files were retrieved + const actualSourceDir = flags['output-dir'].startsWith('.') + ? join((await SfProject.resolve()).getPath(), 'tempRetrieve') + : resolvedTargetDir; + + const mainDefaultPath = join(actualSourceDir, 'main', 'default'); + const hasMainDefaultStructure = await fs.promises + .access(mainDefaultPath) + .then(() => true) + .catch(() => false); + + if (hasMainDefaultStructure) { + await this.moveResultsForRetrieveTargetDir(flags['output-dir'], actualSourceDir); + + // Update file response paths to reflect the actual destination for hidden directories + if (flags['output-dir'].startsWith('.')) { + const outputDir = flags['output-dir']; + this.retrieveResult.getFileResponses().forEach((fileResponse) => { + if (fileResponse.filePath?.includes('tempRetrieve/')) { + fileResponse.filePath = fileResponse.filePath.replace('tempRetrieve/', `${outputDir}/`); + } + }); + } + + if (flags['output-dir'].startsWith('.')) { + await fs.promises.rm(actualSourceDir, { recursive: true }); + } else { + this.retrieveResult.getFileResponses().forEach((fileResponse) => { + // Update the path to reflect the actual location + if (fileResponse.filePath?.includes('main/default/')) { + fileResponse.filePath = fileResponse.filePath.replace(/.*main\/default\//, ''); + } + }); + } + } } // reference the flag instead of `format` so we get correct type @@ -362,7 +401,8 @@ export default class RetrieveMetadata extends SfCommand { await promisesQueue( files, async (file: string): Promise => { - const dest = join(src.replace(join('main', 'default'), ''), file); + const relativePath = src.replace(join(resolvedTargetDir, 'main', 'default'), ''); + const dest = join(targetDir, relativePath, file); const destDir = dirname(dest); await fs.promises.mkdir(destDir, { recursive: true }); await fs.promises.rename(join(src, file), dest); @@ -387,8 +427,16 @@ export default class RetrieveMetadata extends SfCommand { }); // move contents of 'main/default' to 'retrievetargetdir' await promisesQueue([join(resolvedTargetDir, 'main', 'default')], mv, 5, true); - // remove 'main/default' - await fs.promises.rm(join(targetDir, 'main'), { recursive: true }); + + try { + await fs.promises.access(join(targetDir, 'main')); + // remove 'main/default' + await fs.promises.rm(join(targetDir, 'main'), { recursive: true }); + } catch (e) { + const err = SfError.wrap(e); + getLogger().debug(`Error directory does not exist: ${err.message} + Due to: ${err.stack ?? 'unknown (no error stack)'}`); + } } } @@ -518,6 +566,18 @@ const buildRetrieveOptions = async ( output: string | undefined ): Promise => { const apiVersion = await resolveApiVersion(flags); + + let retrieveOutput: string; + + if (flags['output-dir'] && format === 'source' && flags['output-dir'].startsWith('.')) { + // Use a temporary regular directory for hidden directories + const projectRoot = await SfProject.resolve(); + retrieveOutput = join(projectRoot.getPath(), 'tempRetrieve'); + await fs.promises.mkdir(retrieveOutput, { recursive: true }); + } else { + retrieveOutput = output ?? (await SfProject.resolve()).getDefaultPackage().fullPath; + } + return { usernameOrConnection: flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']), merge: true, @@ -533,7 +593,7 @@ const buildRetrieveOptions = async ( output: flags['target-metadata-dir'] as string, } : { - output: output ?? (await SfProject.resolve()).getDefaultPackage().fullPath, + output: retrieveOutput, }), }; }; diff --git a/test/commands/retrieve/start.test.ts b/test/commands/retrieve/start.test.ts index feb77c328..a929efd17 100644 --- a/test/commands/retrieve/start.test.ts +++ b/test/commands/retrieve/start.test.ts @@ -5,8 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { resolve } from 'node:path'; - +import { resolve, join } from 'node:path'; import fs from 'node:fs/promises'; import { Stats } from 'node:fs'; import sinon from 'sinon'; @@ -144,10 +143,6 @@ describe('project retrieve start', () => { ensureRetrieveArgs({ format: 'source' }); }); it('should pass along retrievetargetdir', async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const renameStub = $$.SANDBOX.stub(RetrieveMetadata.prototype, 'moveResultsForRetrieveTargetDir').resolves(); - const sourcepath = ['somepath']; const metadata = ['ApexClass:MyClass']; const result = await RetrieveMetadata.run(['--output-dir', sourcepath[0], '--metadata', metadata[0], '--json']); @@ -160,7 +155,26 @@ describe('project retrieve start', () => { }, }); ensureRetrieveArgs({ output: resolve(sourcepath[0]), format: 'source' }); - expect(renameStub.calledOnce).to.be.true; + }); + + it.only('should pass along retrievetargetdir with hidden directory', async () => { + const sourcepath = ['.hidden']; + const metadata = ['ApexClass:MyClass']; + + // For hidden directories, uses a temp directory + const projectRoot = SfProject.getInstance(); + const tempRetrievePath = join(projectRoot.getPath(), 'tempRetrieve'); + + const result = await RetrieveMetadata.run(['--output-dir', sourcepath[0], '--metadata', metadata[0], '--json']); + expect(result).to.deep.equal(expectedResults); + ensureCreateComponentSetArgs({ + sourcepath: undefined, + metadata: { + directoryPaths: [], + metadataEntries: ['ApexClass:MyClass'], + }, + }); + ensureRetrieveArgs({ output: tempRetrievePath, format: 'source' }); }); it('should pass along metadata', async () => {