From 771602c1037ba37c383072a7f43c7707a398954d Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Mon, 4 May 2026 07:43:49 +0200 Subject: [PATCH 1/5] feat: list all flag --- docs/configuration_and_flags.md | 53 ++++++++ packages/cli/src/cli/program.ts | 4 + packages/cli/src/cli/run.ts | 1 + packages/cli/src/config/options.ts | 2 + packages/cli/src/config/types.ts | 3 + packages/cli/src/services/printScanResult.ts | 6 + packages/cli/src/ui/scan/printListAll.ts | 25 ++++ packages/cli/test/unit/cli/run.test.ts | 1 + .../unit/services/printScanResult.test.ts | 122 +++++++++++++++--- .../test/unit/ui/scan/printListAll.test.ts | 95 ++++++++++++++ 10 files changed, 296 insertions(+), 16 deletions(-) create mode 100644 packages/cli/src/ui/scan/printListAll.ts create mode 100644 packages/cli/test/unit/ui/scan/printListAll.test.ts diff --git a/docs/configuration_and_flags.md b/docs/configuration_and_flags.md index bc40281a..e53f6d1f 100644 --- a/docs/configuration_and_flags.md +++ b/docs/configuration_and_flags.md @@ -28,6 +28,7 @@ CLI flags always take precedence over configuration file values. ### Display Options +- [--list-all](#--list-all) - [--show-unused](#--show-unused) - [--no-show-unused](#--no-show-unused) - [--show-stats](#--show-stats) @@ -437,6 +438,58 @@ If you later want to scan files from one of the default excluded paths, use `--i ## Display Options +### `--list-all` + +Scans the codebase and prints all unique environment variable names found — without comparing them against any `.env` file. + +This is useful when you want a quick overview of every environment variable your project references, for example when bootstrapping a new environment or auditing env usage. + +The list is sorted alphabetically and deduplicated across all usages. + +Example usage: + +```bash +dotenv-diff --list-all +``` + +Example output: + +``` +Environment variables found in codebase +────────────────────────────────────────────────────────────────────── + API_BASE_URL + DATABASE_URL + NEXT_PUBLIC_ANALYTICS_ID + STRIPE_SECRET_KEY +────────────────────────────────────────────────────────────────────── + 4 unique variable(s) +``` + +Combine with `--json` for machine-readable output: + +```bash +dotenv-diff --list-all --json +``` + +```json +[ + "API_BASE_URL", + "DATABASE_URL", + "NEXT_PUBLIC_ANALYTICS_ID", + "STRIPE_SECRET_KEY" +] +``` + +You can also scope the scan using the standard file scanning flags: + +```bash +dotenv-diff --list-all --include-files "src/**" +``` + +> **Note:** `--list-all` exits immediately after printing the list and does not perform any comparison or validation. + +--- + ### `--show-unused` List variables that are defined in `.env` but not used in the codebase (enabled by default). diff --git a/packages/cli/src/cli/program.ts b/packages/cli/src/cli/program.ts index 1a3a9101..615887a0 100644 --- a/packages/cli/src/cli/program.ts +++ b/packages/cli/src/cli/program.ts @@ -92,5 +92,9 @@ export function createProgram() { 'Disable inconsistent naming pattern warnings', ) .option('--init', 'Create a sample dotenv-diff.config.json file') + .option( + '--list-all', + 'List all unique environment variable keys found in codebase', + ) ); } diff --git a/packages/cli/src/cli/run.ts b/packages/cli/src/cli/run.ts index e1315474..93d5b35c 100644 --- a/packages/cli/src/cli/run.ts +++ b/packages/cli/src/cli/run.ts @@ -87,6 +87,7 @@ async function runScanMode(opts: Options): Promise { uppercaseKeys: opts.uppercaseKeys, expireWarnings: opts.expireWarnings, inconsistentNamingWarnings: opts.inconsistentNamingWarnings, + listAll: opts.listAll, }); return exitWithError; diff --git a/packages/cli/src/config/options.ts b/packages/cli/src/config/options.ts index d729f320..473cb660 100644 --- a/packages/cli/src/config/options.ts +++ b/packages/cli/src/config/options.ts @@ -56,6 +56,7 @@ export function normalizeOptions(raw: RawOptions): Options { const uppercaseKeys = raw.uppercaseKeys !== false; const expireWarnings = raw.expireWarnings !== false; const inconsistentNamingWarnings = raw.inconsistentNamingWarnings !== false; + const listAll = toBool(raw.listAll); const cwd = process.cwd(); const envFlag = @@ -96,6 +97,7 @@ export function normalizeOptions(raw: RawOptions): Options { uppercaseKeys, expireWarnings, inconsistentNamingWarnings, + listAll, }; } diff --git a/packages/cli/src/config/types.ts b/packages/cli/src/config/types.ts index 17241cfd..9251294c 100644 --- a/packages/cli/src/config/types.ts +++ b/packages/cli/src/config/types.ts @@ -93,6 +93,7 @@ export interface RawOptions { uppercaseKeys?: boolean; expireWarnings?: boolean; inconsistentNamingWarnings?: boolean; + listAll?: boolean; } /** @@ -135,6 +136,7 @@ export interface Options { uppercaseKeys: boolean; expireWarnings: boolean; inconsistentNamingWarnings: boolean; + listAll: boolean; } export type EnvPatternName = 'process.env' | 'import.meta.env' | 'sveltekit'; @@ -228,6 +230,7 @@ export interface ScanUsageOptions extends ScanOptions { uppercaseKeys?: boolean; expireWarnings?: boolean; inconsistentNamingWarnings?: boolean; + listAll?: boolean; } /** diff --git a/packages/cli/src/services/printScanResult.ts b/packages/cli/src/services/printScanResult.ts index fc645df5..f5bce3b0 100644 --- a/packages/cli/src/services/printScanResult.ts +++ b/packages/cli/src/services/printScanResult.ts @@ -27,6 +27,7 @@ import { computeHealthScore } from '../core/scan/computeHealthScore.js'; import { printHealthScore } from '../ui/scan/printHealthScore.js'; import { printExpireWarnings } from '../ui/scan/printExpireWarnings.js'; import { printInconsistentNamingWarning } from '../ui/scan/printInconsistentNamingWarning.js'; +import { printListAll } from '../ui/scan/printListAll.js'; /** * Prints the scan result to the console. @@ -46,6 +47,11 @@ export function printScanResult( // Determine if output should be in JSON format const isJson = opts.json; + if (opts.listAll) { + printListAll(scanResult.used); + return { exitWithError: false }; + } + printHeader(comparedAgainst); // Show stats if requested diff --git a/packages/cli/src/ui/scan/printListAll.ts b/packages/cli/src/ui/scan/printListAll.ts new file mode 100644 index 00000000..3160b2fc --- /dev/null +++ b/packages/cli/src/ui/scan/printListAll.ts @@ -0,0 +1,25 @@ +import type { EnvUsage } from '../../config/types.js'; +import { accent, dim, header, divider } from '../theme.js'; + +/** + * Prints all unique environment variable names found in the codebase scan. + * @param usages - All environment variable usages found during the scan. + */ +export function printListAll(usages: EnvUsage[]): void { + const uniqueVars = [...new Set(usages.map((u) => u.variable))].sort(); + + if (uniqueVars.length === 0) { + console.log(dim('\nNo environment variables found in codebase.\n')); + return; + } + + console.log(`\n${header('Environment variables found in codebase')}`); + console.log(divider); + + for (const key of uniqueVars) { + console.log(` ${accent(key)}`); + } + + console.log(divider); + console.log(dim(` ${uniqueVars.length} unique variable(s)\n`)); +} diff --git a/packages/cli/test/unit/cli/run.test.ts b/packages/cli/test/unit/cli/run.test.ts index af19023c..a24134f6 100644 --- a/packages/cli/test/unit/cli/run.test.ts +++ b/packages/cli/test/unit/cli/run.test.ts @@ -86,6 +86,7 @@ function createBaseOptions(overrides: Partial = {}): Options { uppercaseKeys: true, expireWarnings: true, inconsistentNamingWarnings: true, + listAll: false, ...overrides, }; } diff --git a/packages/cli/test/unit/services/printScanResult.test.ts b/packages/cli/test/unit/services/printScanResult.test.ts index 44459748..e19675c1 100644 --- a/packages/cli/test/unit/services/printScanResult.test.ts +++ b/packages/cli/test/unit/services/printScanResult.test.ts @@ -68,6 +68,10 @@ vi.mock('../../../src/ui/scan/printInconsistentNamingWarning.js', () => ({ printInconsistentNamingWarning: vi.fn(), })); +vi.mock('../../../src/ui/scan/printListAll.js', () => ({ + printListAll: vi.fn(), +})); + vi.mock('../../../src/core/scan/computeHealthScore.js', () => ({ computeHealthScore: vi.fn(() => 100), })); @@ -95,10 +99,20 @@ import { printUnused } from '../../../src/ui/scan/printUnused.js'; import { printFrameworkWarnings } from '../../../src/ui/scan/printFrameworkWarnings.js'; import { printUppercaseWarning } from '../../../src/ui/scan/printUppercaseWarning.js'; import { printInconsistentNamingWarning } from '../../../src/ui/scan/printInconsistentNamingWarning.js'; +import { printListAll } from '../../../src/ui/scan/printListAll.js'; import { printExampleWarnings } from '../../../src/ui/scan/printExampleWarnings.js'; import { printSecrets } from '../../../src/ui/scan/printSecrets.js'; import { printExpireWarnings } from '../../../src/ui/scan/printExpireWarnings.js'; import { printConsolelogWarning } from '../../../src/ui/scan/printConsolelogWarning.js'; +import type { SecretFinding } from '../../../src/core/security/secretDetectors.js'; +import type { + FrameworkWarning, + UppercaseWarning, + InconsistentNamingWarning, + ExampleSecretWarning, + ExpireWarning, +} from '../../../src/config/types.js'; +import { GITIGNORE_ISSUES } from '../../../src/config/constants.js'; describe('printScanResult', () => { const baseScanResult: ScanResult = { @@ -142,10 +156,18 @@ describe('printScanResult', () => { }); it('returns exitWithError true when high severity secret exists', () => { + const secret: SecretFinding = { + file: 'test.ts', + line: 1, + kind: 'pattern', + message: 'test', + snippet: 'test', + severity: 'high', + }; const result = printScanResult( { ...baseScanResult, - secrets: [{ severity: 'high' } as any], + secrets: [secret], }, baseOpts, '.env', @@ -186,8 +208,8 @@ describe('printScanResult', () => { it('prints gitignore warning when env file is not ignored', () => { vi.mocked(checkGitignoreStatus).mockReturnValue({ - reason: 'not-ignored', - } as any); + reason: GITIGNORE_ISSUES.NOT_IGNORED, + }); printScanResult(baseScanResult, baseOpts, '.env'); @@ -198,8 +220,8 @@ describe('printScanResult', () => { it('returns exitWithError true in strict mode when gitignore issue exists', () => { vi.mocked(checkGitignoreStatus).mockReturnValue({ - reason: 'not-ignored', - } as any); + reason: GITIGNORE_ISSUES.NOT_IGNORED, + }); const result = printScanResult( baseScanResult, @@ -211,10 +233,17 @@ describe('printScanResult', () => { }); it('prints framework warnings when present', () => { + const warning: FrameworkWarning = { + variable: 'API_KEY', + reason: 'exposed', + file: 'app.ts', + line: 1, + framework: 'nextjs', + }; printScanResult( { ...baseScanResult, - frameworkWarnings: [{ type: 'nextjs', message: 'warn' } as any], + frameworkWarnings: [warning], }, baseOpts, '.env', @@ -224,10 +253,14 @@ describe('printScanResult', () => { }); it('prints uppercase warnings when present', () => { + const warning: UppercaseWarning = { + key: 'Api_Key', + suggestion: 'API_KEY', + }; printScanResult( { ...baseScanResult, - uppercaseWarnings: [{ key: 'Api_Key', expected: 'API_KEY' } as any], + uppercaseWarnings: [warning], }, baseOpts, '.env', @@ -237,10 +270,15 @@ describe('printScanResult', () => { }); it('prints inconsistent naming warnings when present', () => { + const warning: InconsistentNamingWarning = { + key1: 'API_KEY', + key2: 'APIKEY', + suggestion: 'API_KEY', + }; printScanResult( { ...baseScanResult, - inconsistentNamingWarnings: [{ key: 'MY-KEY' } as any], + inconsistentNamingWarnings: [warning], }, baseOpts, '.env', @@ -250,10 +288,16 @@ describe('printScanResult', () => { }); it('prints example warnings when present', () => { + const warning: ExampleSecretWarning = { + key: 'SECRET_VAR', + value: 'suspicious_value', + reason: 'Entropy', + severity: 'low', + }; printScanResult( { ...baseScanResult, - exampleWarnings: [{ severity: 'low' } as any], + exampleWarnings: [warning], }, baseOpts, '.env', @@ -263,10 +307,18 @@ describe('printScanResult', () => { }); it('prints secrets when secrets flag is enabled', () => { + const secret: SecretFinding = { + file: 'test.ts', + line: 1, + kind: 'pattern', + message: 'test', + snippet: 'test', + severity: 'low', + }; printScanResult( { ...baseScanResult, - secrets: [{ severity: 'low' } as any], + secrets: [secret], }, { ...baseOpts, secrets: true }, '.env', @@ -276,10 +328,15 @@ describe('printScanResult', () => { }); it('prints expiration warnings when present', () => { + const warning: ExpireWarning = { + key: 'TOKEN', + date: '2026-12-31', + daysLeft: 5, + }; printScanResult( { ...baseScanResult, - expireWarnings: [{ key: 'TOKEN', daysLeft: 5 } as any], + expireWarnings: [warning], }, baseOpts, '.env', @@ -289,10 +346,16 @@ describe('printScanResult', () => { }); it('returns exitWithError true when high severity example warning exists', () => { + const warning: ExampleSecretWarning = { + key: 'DB_PASSWORD', + value: 'password123', + reason: 'Pattern', + severity: 'high', + }; const result = printScanResult( { ...baseScanResult, - exampleWarnings: [{ severity: 'high' } as any], + exampleWarnings: [warning], }, baseOpts, '.env', @@ -302,10 +365,15 @@ describe('printScanResult', () => { }); it('returns exitWithError true when expiration warning is urgent (<=7 days)', () => { + const warning: ExpireWarning = { + key: 'TOKEN', + date: '2026-03-10', + daysLeft: 7, + }; const result = printScanResult( { ...baseScanResult, - expireWarnings: [{ key: 'TOKEN', daysLeft: 7 } as any], + expireWarnings: [warning], }, baseOpts, '.env', @@ -333,7 +401,7 @@ describe('printScanResult', () => { printScanResult( { ...baseScanResult, - duplicates: undefined as any, + duplicates: {}, }, baseOpts, '', @@ -354,7 +422,7 @@ describe('printScanResult', () => { printScanResult( { ...baseScanResult, - logged: undefined as any, + logged: [], }, baseOpts, '.env', @@ -367,7 +435,7 @@ describe('printScanResult', () => { const result = printScanResult( { ...baseScanResult, - secrets: undefined as any, + secrets: [], }, baseOpts, '.env', @@ -430,4 +498,26 @@ describe('printScanResult', () => { false, ); }); + + it('calls printListAll and returns early when listAll is true', () => { + const usages = [ + { + variable: 'API_KEY', + file: 'src/index.ts', + line: 1, + column: 1, + pattern: 'process.env' as const, + context: 'process.env.API_KEY', + }, + ]; + + const result = printScanResult( + { ...baseScanResult, used: usages }, + { ...baseOpts, listAll: true }, + '.env', + ); + + expect(printListAll).toHaveBeenCalledWith(usages); + expect(result.exitWithError).toBe(false); + }); }); diff --git a/packages/cli/test/unit/ui/scan/printListAll.test.ts b/packages/cli/test/unit/ui/scan/printListAll.test.ts new file mode 100644 index 00000000..4ac3b397 --- /dev/null +++ b/packages/cli/test/unit/ui/scan/printListAll.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { printListAll } from '../../../../src/ui/scan/printListAll.js'; +import type { EnvUsage } from '../../../../src/config/types.js'; + +const makeUsage = (variable: string): EnvUsage => ({ + variable, + file: 'src/index.ts', + line: 1, + column: 1, + pattern: 'process.env', + context: `process.env.${variable}`, +}); + +describe('printListAll', () => { + let logSpy: ReturnType; + + beforeEach(() => { + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('prints empty message when usages is an empty array', () => { + printListAll([]); + + expect(logSpy).toHaveBeenCalledTimes(1); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('No environment variables found in codebase.'), + ); + }); + + it('prints header, each unique variable, and summary for a single usage', () => { + printListAll([makeUsage('API_KEY')]); + + const calls = logSpy.mock.calls.map((call: [string]) => call[0]); + + expect( + calls.some((c: string) => + c?.includes('Environment variables found in codebase'), + ), + ).toBe(true); + expect(calls.some((c: string) => c?.includes('API_KEY'))).toBe(true); + expect(calls.some((c: string) => c?.includes('1 unique variable(s)'))).toBe( + true, + ); + }); + + it('deduplicates variables with multiple usages', () => { + printListAll([ + makeUsage('DB_URL'), + makeUsage('DB_URL'), + makeUsage('DB_URL'), + ]); + + const calls = logSpy.mock.calls.map((call: [string]) => call[0]); + const dbUrlCalls = calls.filter((c: string) => c?.includes('DB_URL')); + + expect(dbUrlCalls).toHaveLength(1); + expect(calls.some((c: string) => c?.includes('1 unique variable(s)'))).toBe( + true, + ); + }); + + it('sorts variables alphabetically', () => { + printListAll([makeUsage('ZEBRA'), makeUsage('ALPHA'), makeUsage('MIDDLE')]); + + const calls = logSpy.mock.calls.map((call: [string]) => call[0]); + const varLines = calls.filter( + (c: string) => + c?.includes('ALPHA') || c?.includes('MIDDLE') || c?.includes('ZEBRA'), + ); + + expect(varLines[0]).toContain('ALPHA'); + expect(varLines[1]).toContain('MIDDLE'); + expect(varLines[2]).toContain('ZEBRA'); + }); + + it('shows correct count for multiple unique variables', () => { + printListAll([makeUsage('FOO'), makeUsage('BAR'), makeUsage('BAZ')]); + + const calls = logSpy.mock.calls.map((call: [string]) => call[0]); + expect(calls.some((c: string) => c?.includes('3 unique variable(s)'))).toBe( + true, + ); + }); + + it('prints divider before and after the variable list', () => { + printListAll([makeUsage('TOKEN')]); + + // header + divider + variable + divider + summary = 5 calls + expect(logSpy).toHaveBeenCalledTimes(5); + }); +}); From d1576399ef7de2a955c71c2f0625e23954ec9324 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Mon, 4 May 2026 14:54:35 +0200 Subject: [PATCH 2/5] chore: json list all --- .github/workflows/release.yml | 3 +- docs/configuration_and_flags.md | 40 +++----------- packages/cli/src/commands/scanUsage.ts | 6 ++- packages/cli/src/services/printScanResult.ts | 3 +- packages/cli/src/ui/scan/printListAll.ts | 12 +++-- packages/cli/src/ui/scan/scanJsonOutput.ts | 8 +++ .../unit/services/printScanResult.test.ts | 27 ++++++++++ .../test/unit/ui/scan/scanJsonOutput.test.ts | 54 +++++++++++++++++++ 8 files changed, 110 insertions(+), 43 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9254a8c3..4767420e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,4 +56,5 @@ jobs: commit: 'chore(release): publish to npm' title: 'chore(release): publish to npm' env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} \ No newline at end of file + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + NPM_CONFIG_PROVENANCE: true \ No newline at end of file diff --git a/docs/configuration_and_flags.md b/docs/configuration_and_flags.md index e53f6d1f..8ff47306 100644 --- a/docs/configuration_and_flags.md +++ b/docs/configuration_and_flags.md @@ -440,9 +440,9 @@ If you later want to scan files from one of the default excluded paths, use `--i ### `--list-all` -Scans the codebase and prints all unique environment variable names found — without comparing them against any `.env` file. +Scans the codebase and prints all unique environment variable names found. -This is useful when you want a quick overview of every environment variable your project references, for example when bootstrapping a new environment or auditing env usage. +This is useful when you want a quick overview of every environment variable your project references. The list is sorted alphabetically and deduplicated across all usages. @@ -452,42 +452,14 @@ Example usage: dotenv-diff --list-all ``` -Example output: - -``` -Environment variables found in codebase -────────────────────────────────────────────────────────────────────── - API_BASE_URL - DATABASE_URL - NEXT_PUBLIC_ANALYTICS_ID - STRIPE_SECRET_KEY -────────────────────────────────────────────────────────────────────── - 4 unique variable(s) -``` - -Combine with `--json` for machine-readable output: - -```bash -dotenv-diff --list-all --json -``` +Usage in the configuration file: ```json -[ - "API_BASE_URL", - "DATABASE_URL", - "NEXT_PUBLIC_ANALYTICS_ID", - "STRIPE_SECRET_KEY" -] -``` - -You can also scope the scan using the standard file scanning flags: - -```bash -dotenv-diff --list-all --include-files "src/**" +{ + "listAll": true +} ``` -> **Note:** `--list-all` exits immediately after printing the list and does not perform any comparison or validation. - --- ### `--show-unused` diff --git a/packages/cli/src/commands/scanUsage.ts b/packages/cli/src/commands/scanUsage.ts index 71171746..914387a2 100644 --- a/packages/cli/src/commands/scanUsage.ts +++ b/packages/cli/src/commands/scanUsage.ts @@ -136,7 +136,11 @@ export async function scanUsage(opts: ScanUsageOptions): Promise { // JSON output if (opts.json) { - const jsonOutput = scanJsonOutput(scanResult, comparedAgainst); + const jsonOutput = scanJsonOutput( + scanResult, + comparedAgainst, + opts.listAll ?? false, + ); console.log(JSON.stringify(jsonOutput, null, 2)); // Check for high severity secrets diff --git a/packages/cli/src/services/printScanResult.ts b/packages/cli/src/services/printScanResult.ts index f5bce3b0..70609531 100644 --- a/packages/cli/src/services/printScanResult.ts +++ b/packages/cli/src/services/printScanResult.ts @@ -49,7 +49,6 @@ export function printScanResult( if (opts.listAll) { printListAll(scanResult.used); - return { exitWithError: false }; } printHeader(comparedAgainst); @@ -108,7 +107,7 @@ export function printScanResult( printSecrets(scanResult.secrets, opts.strict); } // Console log usage warning - if (scanResult.logged) { + if (scanResult.logged?.length) { printConsolelogWarning(scanResult.logged, opts.strict); } diff --git a/packages/cli/src/ui/scan/printListAll.ts b/packages/cli/src/ui/scan/printListAll.ts index 3160b2fc..48a55cff 100644 --- a/packages/cli/src/ui/scan/printListAll.ts +++ b/packages/cli/src/ui/scan/printListAll.ts @@ -13,13 +13,15 @@ export function printListAll(usages: EnvUsage[]): void { return; } - console.log(`\n${header('Environment variables found in codebase')}`); - console.log(divider); + console.log( + `\n${accent('▸')} ${header('Environment variables found in codebase')}`, + ); + console.log(`${divider}`); for (const key of uniqueVars) { - console.log(` ${accent(key)}`); + console.log(`${accent(key)}`); } - console.log(divider); - console.log(dim(` ${uniqueVars.length} unique variable(s)\n`)); + console.log(`${divider}`); + console.log(dim(`${uniqueVars.length} unique variable(s)\n`)); } diff --git a/packages/cli/src/ui/scan/scanJsonOutput.ts b/packages/cli/src/ui/scan/scanJsonOutput.ts index 79640bba..a8b8a054 100644 --- a/packages/cli/src/ui/scan/scanJsonOutput.ts +++ b/packages/cli/src/ui/scan/scanJsonOutput.ts @@ -17,6 +17,7 @@ import { normalizePath } from '../../core/helpers/normalizePath.js'; */ interface ScanJsonOutput { stats?: ScanStats; + listAll?: string[]; missing?: Array<{ variable: string; usages: Array<{ @@ -64,9 +65,16 @@ interface ScanJsonOutput { export function scanJsonOutput( scanResult: ScanResult, comparedAgainst: string, + listAll: boolean = false, ): ScanJsonOutput { const output: ScanJsonOutput = {}; + if (listAll) { + output.listAll = [ + ...new Set(scanResult.used.map((usage) => usage.variable)), + ].sort(); + } + // Add comparison info if we compared against a file if (comparedAgainst) { output.comparedAgainst = comparedAgainst; diff --git a/packages/cli/test/unit/services/printScanResult.test.ts b/packages/cli/test/unit/services/printScanResult.test.ts index e19675c1..2d5b8a48 100644 --- a/packages/cli/test/unit/services/printScanResult.test.ts +++ b/packages/cli/test/unit/services/printScanResult.test.ts @@ -431,6 +431,33 @@ describe('printScanResult', () => { expect(printConsolelogWarning).not.toHaveBeenCalled(); }); + it('calls printConsolelogWarning when logged has items', () => { + const loggedUsages = [ + { + variable: 'SECRET_KEY', + file: 'src/logger.ts', + line: 42, + column: 5, + pattern: 'process.env' as const, + context: 'console.log(process.env.SECRET_KEY)', + }, + ]; + + printScanResult( + { + ...baseScanResult, + logged: loggedUsages, + }, + baseOpts, + '.env', + ); + + expect(printConsolelogWarning).toHaveBeenCalledWith( + loggedUsages, + undefined, + ); + }); + it('keeps exitWithError false when secrets is undefined and no strict violations', () => { const result = printScanResult( { diff --git a/packages/cli/test/unit/ui/scan/scanJsonOutput.test.ts b/packages/cli/test/unit/ui/scan/scanJsonOutput.test.ts index 44417b8e..ae617898 100644 --- a/packages/cli/test/unit/ui/scan/scanJsonOutput.test.ts +++ b/packages/cli/test/unit/ui/scan/scanJsonOutput.test.ts @@ -313,4 +313,58 @@ describe('scanJsonOutput', () => { expect(result.healthScore).toBeDefined(); expect(typeof result.healthScore).toBe('number'); }); + + it('includes listAllVariables when listAll is true', () => { + const scanResult = makeScanResult({ + used: [ + { + variable: 'Z_VAR', + file: 'src\\a.ts', + line: 1, + column: 0, + pattern: 'process.env', + context: 'process.env.Z_VAR', + }, + { + variable: 'A_VAR', + file: 'src\\b.ts', + line: 2, + column: 0, + pattern: 'process.env', + context: 'process.env.A_VAR', + }, + { + variable: 'A_VAR', + file: 'src\\c.ts', + line: 3, + column: 0, + pattern: 'process.env', + context: 'process.env.A_VAR', + }, + ], + }); + + const result = scanJsonOutput(scanResult, '', true); + + expect(result.listAll).toEqual(['A_VAR', 'Z_VAR']); + }); + + it('omits listAllVariables when listAll is false', () => { + const scanResult = makeScanResult({ + used: [ + { + variable: 'ONLY_VAR', + file: 'src\\a.ts', + line: 1, + column: 0, + pattern: 'process.env', + context: 'process.env.ONLY_VAR', + }, + ], + }); + + const result = scanJsonOutput(scanResult, '', false); + + expect(result.listAll).toBeUndefined(); + }); }); From f4e2823050e3761b5acfa7f8770a32abc91826e6 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Mon, 4 May 2026 15:00:09 +0200 Subject: [PATCH 3/5] chore: changeset --- .changeset/gentle-sheep-feel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gentle-sheep-feel.md diff --git a/.changeset/gentle-sheep-feel.md b/.changeset/gentle-sheep-feel.md new file mode 100644 index 00000000..0b515c7e --- /dev/null +++ b/.changeset/gentle-sheep-feel.md @@ -0,0 +1,5 @@ +--- +'dotenv-diff': minor +--- + +added --list-all flag to view all environment variables found in codebase From e3ba5d034a70083566bc019599700dc52e629ddc Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Mon, 4 May 2026 15:07:53 +0200 Subject: [PATCH 4/5] chore: updated v8 --- package.json | 2 +- pnpm-lock.yaml | 40 ++++++++++++---------------------------- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 95cc303b..37fcc15d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@types/prompts": "^2.4.9", "@typescript-eslint/eslint-plugin": "^8.59.1", "@typescript-eslint/parser": "^8.59.1", - "@vitest/coverage-v8": "4.1.4", + "@vitest/coverage-v8": "4.1.5", "eslint": "^10.2.1", "husky": "^9.1.7", "lint-staged": "^16.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d12a305..8e62ab6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^8.59.1 version: 8.59.1(eslint@10.2.1(jiti@2.6.1))(typescript@6.0.3) '@vitest/coverage-v8': - specifier: 4.1.4 - version: 4.1.4(vitest@4.1.5) + specifier: 4.1.5 + version: 4.1.5(vitest@4.1.5) eslint: specifier: ^10.2.1 version: 10.2.1(jiti@2.6.1) @@ -55,7 +55,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(yaml@2.8.3)) + version: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(yaml@2.8.3)) packages/@repo/eslint-config: devDependencies: @@ -664,11 +664,11 @@ packages: resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/coverage-v8@4.1.4': - resolution: {integrity: sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==} + '@vitest/coverage-v8@4.1.5': + resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==} peerDependencies: - '@vitest/browser': 4.1.4 - vitest: 4.1.4 + '@vitest/browser': 4.1.5 + vitest: 4.1.5 peerDependenciesMeta: '@vitest/browser': optional: true @@ -687,9 +687,6 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.4': - resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} - '@vitest/pretty-format@4.1.5': resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} @@ -702,9 +699,6 @@ packages: '@vitest/spy@4.1.5': resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} - '@vitest/utils@4.1.4': - resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} - '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} @@ -2080,10 +2074,10 @@ snapshots: '@typescript-eslint/types': 8.59.1 eslint-visitor-keys: 5.0.1 - '@vitest/coverage-v8@4.1.4(vitest@4.1.5)': + '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.4 + '@vitest/utils': 4.1.5 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -2092,7 +2086,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(yaml@2.8.3)) + vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(yaml@2.8.3)) '@vitest/expect@4.1.5': dependencies: @@ -2111,10 +2105,6 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.6.0)(jiti@2.6.1)(yaml@2.8.3) - '@vitest/pretty-format@4.1.4': - dependencies: - tinyrainbow: 3.1.0 - '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 @@ -2133,12 +2123,6 @@ snapshots: '@vitest/spy@4.1.5': {} - '@vitest/utils@4.1.4': - dependencies: - '@vitest/pretty-format': 4.1.4 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - '@vitest/utils@4.1.5': dependencies: '@vitest/pretty-format': 4.1.5 @@ -2862,7 +2846,7 @@ snapshots: jiti: 2.6.1 yaml: 2.8.3 - vitest@4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(yaml@2.8.3)): + vitest@4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.5 '@vitest/mocker': 4.1.5(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(yaml@2.8.3)) @@ -2886,7 +2870,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 - '@vitest/coverage-v8': 4.1.4(vitest@4.1.5) + '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) transitivePeerDependencies: - msw From 933d9d90822d25670fe8cb37624aa3774e9810a4 Mon Sep 17 00:00:00 2001 From: Chrilleweb Date: Mon, 4 May 2026 15:17:33 +0200 Subject: [PATCH 5/5] chore: fixed test coverage workflow --- .github/workflows/test-coverage.yml | 39 +++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 835a1988..db225dc2 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -33,14 +33,39 @@ jobs: run: pip install diff-cover - name: Run coverage - run: pnpm run coverage + run: pnpm --filter dotenv-diff run coverage - - name: Fix lcov paths + - name: Locate and normalize coverage files + id: coverage_files run: | - if [ -f "coverage/lcov.info" ]; then - sed -i "s|^SF:|SF:|g" coverage/lcov.info + LCOV_FILE="coverage/lcov.info" + SUMMARY_FILE="coverage/coverage-summary.json" + + if [ -f "packages/cli/coverage/lcov.info" ]; then + LCOV_FILE="packages/cli/coverage/lcov.info" + fi + + if [ -f "packages/cli/coverage/coverage-summary.json" ]; then + SUMMARY_FILE="packages/cli/coverage/coverage-summary.json" + fi + + if [ ! -f "$LCOV_FILE" ]; then + echo "Missing lcov file at $LCOV_FILE" + exit 1 fi + if [ ! -f "$SUMMARY_FILE" ]; then + echo "Missing coverage summary at $SUMMARY_FILE" + exit 1 + fi + + # Normalize source-file paths for monolith layout so diff-cover can + # map coverage entries to changed files under packages/cli. + sed -i -E 's|^SF:src/|SF:packages/cli/src/|; s|^SF:src\\|SF:packages/cli/src/|; s|^SF:packages\\cli\\src\\|SF:packages/cli/src/|' "$LCOV_FILE" + + echo "lcov_file=$LCOV_FILE" >> $GITHUB_OUTPUT + echo "summary_file=$SUMMARY_FILE" >> $GITHUB_OUTPUT + - name: Run diff-cover analysis id: diff_cover run: | @@ -51,11 +76,11 @@ jobs: all_passed=true overall_coverage="N/A" - if [ -f "coverage/coverage-summary.json" ]; then - overall_coverage=$(jq -r '.total.lines.pct' "coverage/coverage-summary.json") + if [ -f "${{ steps.coverage_files.outputs.summary_file }}" ]; then + overall_coverage=$(jq -r '.total.lines.pct' "${{ steps.coverage_files.outputs.summary_file }}") fi - diff-cover coverage/lcov.info \ + diff-cover "${{ steps.coverage_files.outputs.lcov_file }}" \ --compare-branch=origin/${{ github.event.pull_request.base.ref }} \ --diff-range-notation=... \ --json-report diff-coverage.json \