From 75414243a3125e45ece79f8e52f8f32428c4d786 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Wed, 26 Aug 2026 18:03:25 +0200 Subject: [PATCH 1/6] fix: check the example file for duplicate keys on scan --- .changeset/example-duplicates.md | 5 ++ docs/capabilities.md | 4 +- packages/cli/src/commands/scanUsage.ts | 3 + packages/cli/src/config/types.ts | 1 + packages/cli/src/services/printScanResult.ts | 3 +- .../cli/src/services/processComparisonFile.ts | 52 ++++++++-------- packages/cli/src/ui/shared/printDuplicates.ts | 4 +- .../cli/test/unit/commands/scanUsage.test.ts | 61 +++++++++++++++++++ .../unit/services/printScanResult.test.ts | 22 +++++++ .../services/processComparisonFile.test.ts | 49 ++++++++++++--- 10 files changed, 167 insertions(+), 37 deletions(-) create mode 100644 .changeset/example-duplicates.md diff --git a/.changeset/example-duplicates.md b/.changeset/example-duplicates.md new file mode 100644 index 00000000..99366cea --- /dev/null +++ b/.changeset/example-duplicates.md @@ -0,0 +1,5 @@ +--- +'dotenv-diff': patch +--- + +check the example file for duplicate keys on scan diff --git a/docs/capabilities.md b/docs/capabilities.md index 472e2d1f..7dffa0bf 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -84,7 +84,9 @@ Variables that are **defined in env files** but **never used** in the scanned co ### 3 Duplicate Keys -Duplicate variable definitions inside env files (both main env and example env, when available). +Duplicate variable definitions inside env files. Both the file being scanned and the example file documenting it are checked — the latter on every scan, against whichever example name applies (`.env.example`, `.env.sample`, `.env.template`, …), without needing `--example`. + +`--fix` only rewrites the env file, so duplicates reported in the example file stay reported after a fix. ### 4 Secret Detection diff --git a/packages/cli/src/commands/scanUsage.ts b/packages/cli/src/commands/scanUsage.ts index a8c9920b..459c06fc 100644 --- a/packages/cli/src/commands/scanUsage.ts +++ b/packages/cli/src/commands/scanUsage.ts @@ -110,6 +110,9 @@ export async function scanUsage(opts: ScanUsageOptions): Promise { } else { scanResult = result.scanResult; comparedAgainst = result.comparedAgainst; + if (result.exampleFile) { + scanResult.exampleFile = result.exampleFile; + } fixApplied = result.fix.fixApplied; removedDuplicates = result.fix.removedDuplicates; fixedKeys = result.fix.addedEnv; diff --git a/packages/cli/src/config/types.ts b/packages/cli/src/config/types.ts index c6897ac9..2f20509b 100644 --- a/packages/cli/src/config/types.ts +++ b/packages/cli/src/config/types.ts @@ -289,6 +289,7 @@ export interface ScanResult { }; frameworkWarnings?: FrameworkWarning[]; exampleWarnings?: ExampleSecretWarning[]; + exampleFile?: string; logged: EnvUsage[]; uppercaseWarnings?: UppercaseWarning[]; expireWarnings?: ExpireWarning[]; diff --git a/packages/cli/src/services/printScanResult.ts b/packages/cli/src/services/printScanResult.ts index 5603a265..cd3bf47e 100644 --- a/packages/cli/src/services/printScanResult.ts +++ b/packages/cli/src/services/printScanResult.ts @@ -33,6 +33,7 @@ import { printListAll } from '../ui/scan/printListAll.js'; * @param scanResult - The result of the scan. * @param opts - The scan options. * @param comparedAgainst - The file being compared against. + * @param fixContext - What `--fix` changed, when it ran. * @returns An object indicating whether to exit with an error. */ export function printScanResult( @@ -90,12 +91,12 @@ export function printScanResult( // Duplicates printDuplicates( comparedAgainst || DEFAULT_ENV_FILE, - 'example file', scanResult.duplicates?.env ?? [], scanResult.duplicates?.example ?? [], isJson, opts.fix ?? false, opts.strict, + scanResult.exampleFile, ); // Print potential secrets found diff --git a/packages/cli/src/services/processComparisonFile.ts b/packages/cli/src/services/processComparisonFile.ts index 048bebf4..b2c90439 100644 --- a/packages/cli/src/services/processComparisonFile.ts +++ b/packages/cli/src/services/processComparisonFile.ts @@ -163,7 +163,11 @@ export function processComparisonFile( // Find duplicates if (!opts.allowDuplicates) { - const duplicateResults = checkDuplicates(compareFile, opts); + const duplicateResults = checkDuplicates( + compareFile, + exampleFilePath, + opts, + ); dupsEnv = duplicateResults.dupsEnv; dupsEx = duplicateResults.dupsEx; } @@ -223,18 +227,16 @@ export function processComparisonFile( fix.addedEnv = result.addedEnv; fix.gitignoreUpdated = result.gitignoreUpdated; - // clear the issues that were fixed + // clear the issues that were fixed. `dupsEx` is not among them: the fix + // only rewrites the env file, so duplicates in the example file survive + // it and must still be reported. scanResult.missing = []; dupsEnv = []; - dupsEx = []; } } - // Keep duplicates for output if not fixed - if ( - (dupsEnv.length > 0 || dupsEx.length > 0) && - (!opts.fix || !fix.fixApplied) - ) { + // Keep the duplicates that were not fixed for output + if (dupsEnv.length > 0 || dupsEx.length > 0) { if (!scanResult.duplicates) scanResult.duplicates = {}; if (dupsEnv.length > 0) scanResult.duplicates.env = dupsEnv; if (dupsEx.length > 0) scanResult.duplicates.example = dupsEx; @@ -280,13 +282,22 @@ export function processComparisonFile( } /** - * Check for duplicate keys in env and example files + * Check for duplicate keys in env and example files. + * + * The example file is taken from the caller rather than re-derived from + * `--example`, so it is checked whenever one exists beside the comparison file + * — the same reasoning as the drift check. Gating it on the flag meant the + * example was silently never checked in the common case of a plain + * `dotenv-diff scan`, even though a duplicated key there is exactly the kind of + * thing that misleads whoever copies the file. * @param compareFile - The file to compare against + * @param examplePath - Absolute path of the example file, or null when none was found * @param opts - Scan options * @returns Object containing duplicate keys in env and example files */ function checkDuplicates( compareFile: ComparisonFile, + examplePath: string | null, opts: ScanUsageOptions, ): DuplicateResult { const isIgnored = (key: string) => @@ -297,21 +308,14 @@ function checkDuplicates( isIgnored(key), ); - // Duplicates in example file - let dupsEx: Duplicate[] = []; - - if (opts.examplePath) { - const examplePath = resolveFromCwd(opts.cwd, opts.examplePath); - - const exampleIsDifferentFile = - fs.existsSync(examplePath) && examplePath !== compareFile.path; - - if (exampleIsDifferentFile) { - dupsEx = findDuplicateKeys(examplePath).filter(({ key }) => - isIgnored(key), - ); - } - } + // Duplicates in example file. Skipped when it *is* the comparison file — a + // scan with no `.env` falls through to `.env.example`, and its duplicates are + // already reported as `dupsEnv`. `findDuplicateKeys` handles a path that does + // not exist, which an explicit `--example` may still point at. + const dupsEx: Duplicate[] = + examplePath && examplePath !== compareFile.path + ? findDuplicateKeys(examplePath).filter(({ key }) => isIgnored(key)) + : []; return { dupsEnv, dupsEx } satisfies DuplicateResult; } diff --git a/packages/cli/src/ui/shared/printDuplicates.ts b/packages/cli/src/ui/shared/printDuplicates.ts index 2071e75a..9507f960 100644 --- a/packages/cli/src/ui/shared/printDuplicates.ts +++ b/packages/cli/src/ui/shared/printDuplicates.ts @@ -22,12 +22,12 @@ import type { Duplicate } from '../../config/types.js'; */ export function printDuplicates( envName: string, - exampleName: string, dEnv: Duplicate[], dEx: Duplicate[], json: boolean, fix: boolean = false, strict: boolean = false, + exampleName?: string, ): void { if (json) return; @@ -45,7 +45,7 @@ export function printDuplicates( console.log(`${divider}`); } - if (dEx.length) { + if (dEx.length && exampleName) { console.log(); console.log(`${indicator} ${header(`Duplicate keys in ${exampleName}`)}`); console.log(`${divider}`); diff --git a/packages/cli/test/unit/commands/scanUsage.test.ts b/packages/cli/test/unit/commands/scanUsage.test.ts index 5a8484b5..da80afcc 100644 --- a/packages/cli/test/unit/commands/scanUsage.test.ts +++ b/packages/cli/test/unit/commands/scanUsage.test.ts @@ -411,6 +411,67 @@ describe('scanUsage', () => { ); }); + it('attaches the resolved example file name onto the scan result', async () => { + // Carried through so findings about the example file can name it — a + // project may document itself with .env.sample rather than .env.example. + vi.mocked(determineComparisonFile).mockResolvedValue({ + type: 'found', + file: { path: '/env/.env', name: '.env' }, + }); + vi.mocked(processComparisonFile).mockReturnValue({ + scanResult: { ...baseScanResult }, + comparedAgainst: '.env', + envVariables: {}, + duplicatesFound: false, + dupsEnv: [], + dupsEx: [], + fix: { + fixApplied: false, + removedDuplicates: [], + addedEnv: [], + gitignoreUpdated: false, + }, + exampleFile: '.env.sample', + } as ProcessComparisonResult); + + await scanUsage({ ...baseOpts, json: false }); + + expect(printScanResult).toHaveBeenCalledWith( + expect.objectContaining({ exampleFile: '.env.sample' }), + expect.anything(), + expect.anything(), + expect.anything(), + ); + }); + + it('leaves the example file name unset when no example file was resolved', async () => { + vi.mocked(determineComparisonFile).mockResolvedValue({ + type: 'found', + file: { path: '/env/.env', name: '.env' }, + }); + vi.mocked(processComparisonFile).mockReturnValue({ + scanResult: { ...baseScanResult }, + comparedAgainst: '.env', + envVariables: {}, + duplicatesFound: false, + dupsEnv: [], + dupsEx: [], + fix: { + fixApplied: false, + removedDuplicates: [], + addedEnv: [], + gitignoreUpdated: false, + }, + exampleFile: undefined, + } as ProcessComparisonResult); + + await scanUsage({ ...baseOpts, json: false }); + + expect(vi.mocked(printScanResult).mock.calls[0]?.[0]).not.toHaveProperty( + 'exampleFile', + ); + }); + it('sets frameworkWarnings on scanResult when frameworkValidator returns results', async () => { const { frameworkValidator } = await import('../../../src/core/frameworks/frameworkValidator.js'); diff --git a/packages/cli/test/unit/services/printScanResult.test.ts b/packages/cli/test/unit/services/printScanResult.test.ts index 0c8b5a53..85d6dc9f 100644 --- a/packages/cli/test/unit/services/printScanResult.test.ts +++ b/packages/cli/test/unit/services/printScanResult.test.ts @@ -458,6 +458,28 @@ describe('printScanResult', () => { ); }); + it('names the example file in duplicates when the scan resolved one', () => { + printScanResult( + { + ...baseScanResult, + duplicates: { example: [{ key: 'EX_KEY', count: 2 }] }, + exampleFile: '.env.sample', + }, + baseOpts, + '.env', + ); + + expect(printDuplicates).toHaveBeenCalledWith( + '.env', + '.env.sample', + [], + [{ key: 'EX_KEY', count: 2 }], + false, + false, + undefined, + ); + }); + it('does not print console log warning when logged is undefined', () => { printScanResult( { diff --git a/packages/cli/test/unit/services/processComparisonFile.test.ts b/packages/cli/test/unit/services/processComparisonFile.test.ts index 85be982a..6afaad88 100644 --- a/packages/cli/test/unit/services/processComparisonFile.test.ts +++ b/packages/cli/test/unit/services/processComparisonFile.test.ts @@ -254,15 +254,8 @@ describe('processComparisonFile', () => { }); it('keeps duplicates when fix does not change anything', () => { - vi.mocked(applyFixes).mockReturnValueOnce({ - changed: false, - result: { - removedDuplicates: [], - addedEnv: [], - gitignoreUpdated: false, - }, - }); - + // `fix` is off, so applyFixes never runs — queueing a return value here + // would only leak into the next test that does enable it. const result = processComparisonFile(baseScanResult, compareFile, { ...baseOpts, allowDuplicates: false, @@ -320,6 +313,44 @@ describe('processComparisonFile', () => { expect(result.exampleFull).toEqual({ A: '1', bKey: '2' }); }); + it('checks the example file for duplicates without the examplePath option', () => { + // Same reasoning as the drift check: the example beside the comparison file + // is checked whether or not --example was passed, so a duplicate there is + // not silently missed on a plain scan. + vi.mocked(findDuplicateKeys) + .mockReturnValueOnce([]) + .mockReturnValueOnce([{ key: 'EX_KEY', count: 2 }]); + + const result = processComparisonFile( + { ...baseScanResult, duplicates: {} }, + compareFile, + { ...baseOpts, examplePath: undefined, allowDuplicates: false }, + ); + + expect(findDuplicateKeys).toHaveBeenNthCalledWith(2, '/env/.env.example'); + expect(result.dupsEx).toEqual([{ key: 'EX_KEY', count: 2 }]); + }); + + it('keeps example duplicates after a fix, which only rewrites the env file', () => { + vi.mocked(findDuplicateKeys) + .mockReturnValueOnce([{ key: 'A', count: 2 }]) + .mockReturnValueOnce([{ key: 'EX_KEY', count: 2 }]); + + const result = processComparisonFile( + { ...baseScanResult, duplicates: {} }, + compareFile, + { ...baseOpts, fix: true, allowDuplicates: false }, + ); + + expect(result.fix.fixApplied).toBe(true); + // The env file was deduplicated, the example file was never touched. + expect(result.dupsEnv).toHaveLength(0); + expect(result.scanResult.duplicates?.env).toBeUndefined(); + expect(result.scanResult.duplicates?.example).toEqual([ + { key: 'EX_KEY', count: 2 }, + ]); + }); + it('skips example duplicate check when examplePath equals compareFile path', async () => { // resolveFromCwd returns the compareFile path → same file → skip const { resolveFromCwd } = From 0fbfe20cc867f34faf61f6e98c6e35a73bd1cf43 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Wed, 26 Aug 2026 18:32:04 +0200 Subject: [PATCH 2/6] chore: fix build --- packages/cli/src/commands/compare.ts | 3 +- packages/cli/src/ui/shared/printDuplicates.ts | 2 +- .../cli/test/unit/commands/compare.test.ts | 3 +- .../unit/ui/shared/printDuplicates.test.ts | 35 ++++++++++++++++--- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/compare.ts b/packages/cli/src/commands/compare.ts index 4af94500..188c2857 100644 --- a/packages/cli/src/commands/compare.ts +++ b/packages/cli/src/commands/compare.ts @@ -119,11 +119,12 @@ function processPair( printDuplicates( envName, - exampleName, dupsEnv, dupsEx, opts.json ?? false, opts.fix ?? false, + opts.strict ?? false, + exampleName, ); const entry = compareJsonOutput({ diff --git a/packages/cli/src/ui/shared/printDuplicates.ts b/packages/cli/src/ui/shared/printDuplicates.ts index 9507f960..5425159d 100644 --- a/packages/cli/src/ui/shared/printDuplicates.ts +++ b/packages/cli/src/ui/shared/printDuplicates.ts @@ -12,12 +12,12 @@ import type { Duplicate } from '../../config/types.js'; /** * Prints duplicate keys found in the environment and example files. * @param envName The name of the environment file. - * @param exampleName The name of the example file. * @param dEnv Array of duplicate keys in the environment file with their counts. * @param dEx Array of duplicate keys in the example file with their counts. * @param json Whether to output in JSON format. * @param fix Whether fix mode is enabled (skips printing duplicates as they will be fixed). * @param strict Whether strict mode is enabled. + * @param exampleName The name of the example file, when one was resolved. * @returns void */ export function printDuplicates( diff --git a/packages/cli/test/unit/commands/compare.test.ts b/packages/cli/test/unit/commands/compare.test.ts index a41b8386..4cd6db06 100644 --- a/packages/cli/test/unit/commands/compare.test.ts +++ b/packages/cli/test/unit/commands/compare.test.ts @@ -885,11 +885,12 @@ describe('compareMany', () => { // printDuplicates receives false for both ?? args (lines 123-124) expect(mockPrintDuplicates).toHaveBeenCalledWith( '.env', - '.env.example', [], [], false, false, + false, + '.env.example', ); // printIssues receives false for both ?? args (line 266) expect(mockPrintIssues).toHaveBeenCalledWith( diff --git a/packages/cli/test/unit/ui/shared/printDuplicates.test.ts b/packages/cli/test/unit/ui/shared/printDuplicates.test.ts index 71de544a..31409844 100644 --- a/packages/cli/test/unit/ui/shared/printDuplicates.test.ts +++ b/packages/cli/test/unit/ui/shared/printDuplicates.test.ts @@ -24,18 +24,19 @@ describe('printDuplicates', () => { }); it('does nothing when json is true', () => { - printDuplicates('.env', '.env.example', [], [], true); + printDuplicates('.env', [], [], true, false, false, '.env.example'); expect(logSpy).not.toHaveBeenCalled(); }); it('does not print env duplicates when fix=true', () => { printDuplicates( '.env', - '.env.example', [{ key: 'A', count: 2 }], [], false, true, + false, + '.env.example', ); expect( @@ -46,7 +47,7 @@ describe('printDuplicates', () => { }); it('does not print env duplicates when none exist', () => { - printDuplicates('.env', '.env.example', [], [], false, false); + printDuplicates('.env', [], [], false, false, false, '.env.example'); expect(logSpy).not.toHaveBeenCalled(); }); @@ -54,24 +55,48 @@ describe('printDuplicates', () => { it('prints both env and example duplicates together', () => { printDuplicates( '.env', - '.env.example', [{ key: 'A', count: 2 }], [{ key: 'B', count: 3 }], false, + false, + false, + '.env.example', ); expect(logSpy).toHaveBeenCalled(); }); + it('names the example file that duplicates were found in', () => { + printDuplicates( + '.env', + [], + [{ key: 'B', count: 3 }], + false, + false, + false, + '.env.sample', + ); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('H(Duplicate keys in .env.sample)'), + ); + }); + + it('skips example duplicates when no example file was resolved', () => { + printDuplicates('.env', [], [{ key: 'B', count: 3 }], false); + + expect(logSpy).not.toHaveBeenCalled(); + }); + it('uses strict formatting when strict mode is enabled', () => { printDuplicates( '.env', - '.env.example', [{ key: 'A', count: 2 }], [{ key: 'B', count: 3 }], false, false, true, + '.env.example', ); expect(logSpy).toHaveBeenCalledWith( From 897ff7f50fdc5e061e3a1cb6852d2ea145a29db9 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Thu, 27 Aug 2026 18:22:50 +0200 Subject: [PATCH 3/6] chore: refac --- packages/cli/src/commands/compare.ts | 3 +-- packages/cli/src/services/printScanResult.ts | 2 +- packages/cli/src/ui/shared/printDuplicates.ts | 4 ++-- .../cli/test/unit/commands/compare.test.ts | 3 +-- .../unit/ui/shared/printDuplicates.test.ts | 22 +++++++++++++------ 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/commands/compare.ts b/packages/cli/src/commands/compare.ts index 188c2857..4af94500 100644 --- a/packages/cli/src/commands/compare.ts +++ b/packages/cli/src/commands/compare.ts @@ -119,12 +119,11 @@ function processPair( printDuplicates( envName, + exampleName, dupsEnv, dupsEx, opts.json ?? false, opts.fix ?? false, - opts.strict ?? false, - exampleName, ); const entry = compareJsonOutput({ diff --git a/packages/cli/src/services/printScanResult.ts b/packages/cli/src/services/printScanResult.ts index cd3bf47e..5c64d679 100644 --- a/packages/cli/src/services/printScanResult.ts +++ b/packages/cli/src/services/printScanResult.ts @@ -91,12 +91,12 @@ export function printScanResult( // Duplicates printDuplicates( comparedAgainst || DEFAULT_ENV_FILE, + scanResult.exampleFile || 'example file', scanResult.duplicates?.env ?? [], scanResult.duplicates?.example ?? [], isJson, opts.fix ?? false, opts.strict, - scanResult.exampleFile, ); // Print potential secrets found diff --git a/packages/cli/src/ui/shared/printDuplicates.ts b/packages/cli/src/ui/shared/printDuplicates.ts index 5425159d..758732f4 100644 --- a/packages/cli/src/ui/shared/printDuplicates.ts +++ b/packages/cli/src/ui/shared/printDuplicates.ts @@ -22,12 +22,12 @@ import type { Duplicate } from '../../config/types.js'; */ export function printDuplicates( envName: string, + exampleName: string, dEnv: Duplicate[], dEx: Duplicate[], json: boolean, fix: boolean = false, strict: boolean = false, - exampleName?: string, ): void { if (json) return; @@ -45,7 +45,7 @@ export function printDuplicates( console.log(`${divider}`); } - if (dEx.length && exampleName) { + if (dEx.length) { console.log(); console.log(`${indicator} ${header(`Duplicate keys in ${exampleName}`)}`); console.log(`${divider}`); diff --git a/packages/cli/test/unit/commands/compare.test.ts b/packages/cli/test/unit/commands/compare.test.ts index 4cd6db06..a41b8386 100644 --- a/packages/cli/test/unit/commands/compare.test.ts +++ b/packages/cli/test/unit/commands/compare.test.ts @@ -885,12 +885,11 @@ describe('compareMany', () => { // printDuplicates receives false for both ?? args (lines 123-124) expect(mockPrintDuplicates).toHaveBeenCalledWith( '.env', + '.env.example', [], [], false, false, - false, - '.env.example', ); // printIssues receives false for both ?? args (line 266) expect(mockPrintIssues).toHaveBeenCalledWith( diff --git a/packages/cli/test/unit/ui/shared/printDuplicates.test.ts b/packages/cli/test/unit/ui/shared/printDuplicates.test.ts index 31409844..7d9559e9 100644 --- a/packages/cli/test/unit/ui/shared/printDuplicates.test.ts +++ b/packages/cli/test/unit/ui/shared/printDuplicates.test.ts @@ -24,19 +24,19 @@ describe('printDuplicates', () => { }); it('does nothing when json is true', () => { - printDuplicates('.env', [], [], true, false, false, '.env.example'); + printDuplicates('.env', '.env.example', [], [], true, false, false); expect(logSpy).not.toHaveBeenCalled(); }); it('does not print env duplicates when fix=true', () => { printDuplicates( '.env', + '.env.example', [{ key: 'A', count: 2 }], [], false, true, false, - '.env.example', ); expect( @@ -47,7 +47,7 @@ describe('printDuplicates', () => { }); it('does not print env duplicates when none exist', () => { - printDuplicates('.env', [], [], false, false, false, '.env.example'); + printDuplicates('.env', '.env.example', [], [], false, false, false); expect(logSpy).not.toHaveBeenCalled(); }); @@ -55,12 +55,12 @@ describe('printDuplicates', () => { it('prints both env and example duplicates together', () => { printDuplicates( '.env', + '.env.example', [{ key: 'A', count: 2 }], [{ key: 'B', count: 3 }], false, false, false, - '.env.example', ); expect(logSpy).toHaveBeenCalled(); @@ -69,12 +69,12 @@ describe('printDuplicates', () => { it('names the example file that duplicates were found in', () => { printDuplicates( '.env', + '.env.sample', [], [{ key: 'B', count: 3 }], false, false, false, - '.env.sample', ); expect(logSpy).toHaveBeenCalledWith( @@ -83,7 +83,15 @@ describe('printDuplicates', () => { }); it('skips example duplicates when no example file was resolved', () => { - printDuplicates('.env', [], [{ key: 'B', count: 3 }], false); + printDuplicates( + '.env', + '', + [], + [{ key: 'B', count: 3 }], + false, + false, + false, + ); expect(logSpy).not.toHaveBeenCalled(); }); @@ -91,12 +99,12 @@ describe('printDuplicates', () => { it('uses strict formatting when strict mode is enabled', () => { printDuplicates( '.env', + '.env.example', [{ key: 'A', count: 2 }], [{ key: 'B', count: 3 }], false, false, true, - '.env.example', ); expect(logSpy).toHaveBeenCalledWith( From f5ff108d784444223ce5cb7b2ecc9429d9fecf84 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Thu, 27 Aug 2026 18:26:01 +0200 Subject: [PATCH 4/6] chore: fix comment --- packages/cli/src/ui/shared/printDuplicates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/ui/shared/printDuplicates.ts b/packages/cli/src/ui/shared/printDuplicates.ts index 758732f4..2071e75a 100644 --- a/packages/cli/src/ui/shared/printDuplicates.ts +++ b/packages/cli/src/ui/shared/printDuplicates.ts @@ -12,12 +12,12 @@ import type { Duplicate } from '../../config/types.js'; /** * Prints duplicate keys found in the environment and example files. * @param envName The name of the environment file. + * @param exampleName The name of the example file. * @param dEnv Array of duplicate keys in the environment file with their counts. * @param dEx Array of duplicate keys in the example file with their counts. * @param json Whether to output in JSON format. * @param fix Whether fix mode is enabled (skips printing duplicates as they will be fixed). * @param strict Whether strict mode is enabled. - * @param exampleName The name of the example file, when one was resolved. * @returns void */ export function printDuplicates( From f4d564dad346450252377b25d49b58b017550609 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Thu, 27 Aug 2026 18:31:36 +0200 Subject: [PATCH 5/6] chore: fix test --- packages/cli/src/ui/shared/printDuplicates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/ui/shared/printDuplicates.ts b/packages/cli/src/ui/shared/printDuplicates.ts index 2071e75a..25c5581c 100644 --- a/packages/cli/src/ui/shared/printDuplicates.ts +++ b/packages/cli/src/ui/shared/printDuplicates.ts @@ -45,7 +45,7 @@ export function printDuplicates( console.log(`${divider}`); } - if (dEx.length) { + if (dEx.length && exampleName) { console.log(); console.log(`${indicator} ${header(`Duplicate keys in ${exampleName}`)}`); console.log(`${divider}`); From 13c8c554f2e2a534b35ee92b966f7ac4bde2e16e Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Thu, 27 Aug 2026 18:37:42 +0200 Subject: [PATCH 6/6] chore: fix winodws path --- .../cli/test/unit/services/processComparisonFile.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/unit/services/processComparisonFile.test.ts b/packages/cli/test/unit/services/processComparisonFile.test.ts index 6afaad88..35765109 100644 --- a/packages/cli/test/unit/services/processComparisonFile.test.ts +++ b/packages/cli/test/unit/services/processComparisonFile.test.ts @@ -71,6 +71,7 @@ vi.mock('../../../src/services/detectExampleDrift.js', () => ({ })); import fs from 'fs'; +import path from 'path'; import { processComparisonFile } from '../../../src/services/processComparisonFile.js'; import { applyFixes } from '../../../src/services/fixEnv.js'; import { parseEnvFile } from '../../../src/services/parseEnvFile.js'; @@ -327,7 +328,11 @@ describe('processComparisonFile', () => { { ...baseOpts, examplePath: undefined, allowDuplicates: false }, ); - expect(findDuplicateKeys).toHaveBeenNthCalledWith(2, '/env/.env.example'); + // `path.join` in resolveExampleFile, so the separator is platform-specific. + expect(findDuplicateKeys).toHaveBeenNthCalledWith( + 2, + path.join('/env', '.env.example'), + ); expect(result.dupsEx).toEqual([{ key: 'EX_KEY', count: 2 }]); });