@karmaniverous/get-dotenv 4.6.0-0 → 5.0.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +130 -23
  2. package/dist/cliHost.cjs +1089 -0
  3. package/dist/cliHost.d.cts +191 -0
  4. package/dist/cliHost.d.mts +191 -0
  5. package/dist/cliHost.d.ts +191 -0
  6. package/dist/cliHost.mjs +1085 -0
  7. package/dist/config.cjs +247 -0
  8. package/dist/config.d.cts +53 -0
  9. package/dist/config.d.mts +53 -0
  10. package/dist/config.d.ts +53 -0
  11. package/dist/config.mjs +242 -0
  12. package/dist/env-overlay.cjs +163 -0
  13. package/dist/env-overlay.d.cts +48 -0
  14. package/dist/env-overlay.d.mts +48 -0
  15. package/dist/env-overlay.d.ts +48 -0
  16. package/dist/env-overlay.mjs +161 -0
  17. package/dist/getdotenv.cli.mjs +2788 -734
  18. package/dist/index.cjs +902 -280
  19. package/dist/index.d.cts +122 -64
  20. package/dist/index.d.mts +122 -64
  21. package/dist/index.d.ts +122 -64
  22. package/dist/index.mjs +904 -283
  23. package/dist/plugins-aws.cjs +618 -0
  24. package/dist/plugins-aws.d.cts +176 -0
  25. package/dist/plugins-aws.d.mts +176 -0
  26. package/dist/plugins-aws.d.ts +176 -0
  27. package/dist/plugins-aws.mjs +616 -0
  28. package/dist/plugins-batch.cjs +569 -0
  29. package/dist/plugins-batch.d.cts +198 -0
  30. package/dist/plugins-batch.d.mts +198 -0
  31. package/dist/plugins-batch.d.ts +198 -0
  32. package/dist/plugins-batch.mjs +567 -0
  33. package/dist/plugins-init.cjs +282 -0
  34. package/dist/plugins-init.d.cts +180 -0
  35. package/dist/plugins-init.d.mts +180 -0
  36. package/dist/plugins-init.d.ts +180 -0
  37. package/dist/plugins-init.mjs +280 -0
  38. package/getdotenv.config.json +19 -0
  39. package/package.json +88 -17
  40. package/templates/cli/ts/index.ts +9 -0
  41. package/templates/cli/ts/plugins/hello.ts +17 -0
  42. package/templates/config/js/getdotenv.config.js +15 -0
  43. package/templates/config/json/local/getdotenv.config.local.json +7 -0
  44. package/templates/config/json/public/getdotenv.config.json +12 -0
  45. package/templates/config/public/getdotenv.config.json +13 -0
  46. package/templates/config/ts/getdotenv.config.ts +16 -0
  47. package/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
  48. package/templates/config/yaml/public/getdotenv.config.yaml +10 -0
package/dist/index.mjs CHANGED
@@ -1,13 +1,15 @@
1
- import { Command, Option } from 'commander';
2
- import { execaCommand } from 'execa';
1
+ import { Option, Command } from 'commander';
3
2
  import { globby } from 'globby';
4
3
  import { packageDirectory } from 'package-directory';
5
- import path, { join } from 'path';
4
+ import path, { join, extname } from 'path';
5
+ import { execa, execaCommand } from 'execa';
6
6
  import fs from 'fs-extra';
7
- import url, { fileURLToPath } from 'url';
8
- import { createHash } from 'crypto';
7
+ import url, { fileURLToPath, pathToFileURL } from 'url';
9
8
  import { nanoid } from 'nanoid';
10
9
  import { parse } from 'dotenv';
10
+ import { createHash } from 'crypto';
11
+ import YAML from 'yaml';
12
+ import { z } from 'zod';
11
13
 
12
14
  /**
13
15
  * Dotenv expansion utilities.
@@ -145,25 +147,90 @@ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduc
145
147
  */
146
148
  const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
147
149
 
148
- const baseGetDotenvCliOptions = {
149
- dotenvToken: '.env',
150
- loadProcess: true,
151
- logger: console,
152
- paths: './',
153
- pathsDelimiter: ' ',
154
- privateToken: 'local',
155
- scripts: {
156
- 'git-status': {
157
- cmd: 'git branch --show-current && git status -s -u',
158
- shell: true,
159
- },
160
- },
161
- shell: true,
162
- vars: '',
163
- varsAssignor: '=',
164
- varsDelimiter: ' ',
150
+ /**
151
+ * Attach legacy root flags to a Commander program.
152
+ * Uses provided defaults to render help labels without coupling to generators.
153
+ */
154
+ const attachRootOptions = (program, defaults, opts) => {
155
+ const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
156
+ const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
157
+ const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
158
+ // Build initial chain.
159
+ let p = program
160
+ .enablePositionalOptions()
161
+ .passThroughOptions()
162
+ .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
163
+ p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
164
+ ['KEY1', 'VAL1'],
165
+ ['KEY2', 'VAL2'],
166
+ ]
167
+ .map((v) => v.join(va))
168
+ .join(vd)}`, dotenvExpandFromProcessEnv);
169
+ // Optional legacy root command flag (kept for generated CLI compatibility).
170
+ // Default is OFF; the generator opts in explicitly.
171
+ {
172
+ p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
173
+ }
174
+ p = p
175
+ .option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
176
+ .addOption(new Option('-s, --shell [string]', (() => {
177
+ let defaultLabel = '';
178
+ if (shell !== undefined) {
179
+ if (typeof shell === 'boolean') {
180
+ defaultLabel = ' (default OS shell)';
181
+ }
182
+ else if (typeof shell === 'string') {
183
+ // Safe string interpolation
184
+ defaultLabel = ` (default ${shell})`;
185
+ }
186
+ }
187
+ return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
188
+ })()).conflicts('shellOff'))
189
+ .addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
190
+ .addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
191
+ .addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
192
+ .addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
193
+ ((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
194
+ ? ' (default)'
195
+ : ''}`).conflicts('excludeAllOff'))
196
+ .addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
197
+ .addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
198
+ .addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
199
+ .addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
200
+ .addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
201
+ .addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
202
+ .addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
203
+ .addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
204
+ .addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
205
+ .addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
206
+ .addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
207
+ .addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
208
+ .addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
209
+ .option('--capture', 'capture child process stdio for commands (tests/CI)')
210
+ .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
211
+ .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
212
+ .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
213
+ .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
214
+ .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
215
+ .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
216
+ .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
217
+ .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
218
+ .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
219
+ .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
220
+ .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
221
+ // Hidden scripts pipe-through (stringified)
222
+ .addOption(new Option('--scripts <string>')
223
+ .default(JSON.stringify(scripts))
224
+ .hideHelp());
225
+ // Diagnostics: opt-in tracing; optional variadic keys after the flag.
226
+ p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
227
+ return p;
165
228
  };
166
229
 
230
+ /**
231
+ * Batch services (neutral): resolve command and shell settings.
232
+ * Shared by the generator path and the batch plugin to avoid circular deps.
233
+ */
167
234
  /**
168
235
  * Resolve a command string from the {@link Scripts} table.
169
236
  * A script may be expressed as a string or an object with a `cmd` property.
@@ -188,6 +255,143 @@ const resolveShell = (scripts, command, shell) => scripts && typeof scripts[comm
188
255
  ? (scripts[command].shell ?? false)
189
256
  : (shell ?? false);
190
257
 
258
+ // Minimal tokenizer for shell-off execution:
259
+ // Splits by whitespace while preserving quoted segments (single or double quotes).
260
+ const tokenize = (command) => {
261
+ const out = [];
262
+ let cur = '';
263
+ let quote = null;
264
+ for (let i = 0; i < command.length; i++) {
265
+ const c = command.charAt(i);
266
+ if (quote) {
267
+ if (c === quote) {
268
+ // Support doubled quotes inside a quoted segment (Windows/PowerShell style):
269
+ // "" -> " and '' -> '
270
+ const next = command.charAt(i + 1);
271
+ if (next === quote) {
272
+ cur += quote;
273
+ i += 1; // skip the second quote
274
+ }
275
+ else {
276
+ // end of quoted segment
277
+ quote = null;
278
+ }
279
+ }
280
+ else {
281
+ cur += c;
282
+ }
283
+ }
284
+ else {
285
+ if (c === '"' || c === "'") {
286
+ quote = c;
287
+ }
288
+ else if (/\s/.test(c)) {
289
+ if (cur) {
290
+ out.push(cur);
291
+ cur = '';
292
+ }
293
+ }
294
+ else {
295
+ cur += c;
296
+ }
297
+ }
298
+ }
299
+ if (cur)
300
+ out.push(cur);
301
+ return out;
302
+ };
303
+
304
+ const dbg = (...args) => {
305
+ if (process.env.GETDOTENV_DEBUG) {
306
+ // Use stderr to avoid interfering with stdout assertions
307
+ console.error('[getdotenv:run]', ...args);
308
+ }
309
+ };
310
+ // Strip repeated symmetric outer quotes (single or double) until stable.
311
+ // This is safe for argv arrays passed to execa (no quoting needed) and avoids
312
+ // passing quote characters through to Node (e.g., for `node -e "<code>"`).
313
+ // Handles stacked quotes from shells like PowerShell: """code""" -> code.
314
+ const stripOuterQuotes = (s) => {
315
+ let out = s;
316
+ // Repeatedly trim only when the entire string is wrapped in matching quotes.
317
+ // Stop as soon as the ends are asymmetric or no quotes remain.
318
+ while (out.length >= 2) {
319
+ const a = out.charAt(0);
320
+ const b = out.charAt(out.length - 1);
321
+ const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
322
+ if (!symmetric)
323
+ break;
324
+ out = out.slice(1, -1);
325
+ }
326
+ return out;
327
+ };
328
+ // Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
329
+ // expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
330
+ const sanitizeEnv = (env) => {
331
+ if (!env)
332
+ return undefined;
333
+ const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
334
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
335
+ };
336
+ const runCommand = async (command, shell, opts) => {
337
+ if (shell === false) {
338
+ let file;
339
+ let args = [];
340
+ if (Array.isArray(command)) {
341
+ file = command[0];
342
+ args = command.slice(1).map(stripOuterQuotes);
343
+ }
344
+ else {
345
+ const tokens = tokenize(command);
346
+ file = tokens[0];
347
+ args = tokens.slice(1);
348
+ }
349
+ if (!file)
350
+ return 0;
351
+ dbg('exec (plain)', { file, args, stdio: opts.stdio });
352
+ // Build options without injecting undefined properties (exactOptionalPropertyTypes).
353
+ const envSan = sanitizeEnv(opts.env);
354
+ const plainOpts = {};
355
+ if (opts.cwd !== undefined)
356
+ plainOpts.cwd = opts.cwd;
357
+ if (envSan !== undefined)
358
+ plainOpts.env = envSan;
359
+ if (opts.stdio !== undefined)
360
+ plainOpts.stdio = opts.stdio;
361
+ const result = await execa(file, args, plainOpts);
362
+ if (opts.stdio === 'pipe' && result.stdout) {
363
+ process.stdout.write(result.stdout + (result.stdout.endsWith('\n') ? '' : '\n'));
364
+ }
365
+ const exit = result?.exitCode;
366
+ dbg('exit (plain)', { exitCode: exit });
367
+ return typeof exit === 'number' ? exit : Number.NaN;
368
+ }
369
+ else {
370
+ const commandStr = Array.isArray(command) ? command.join(' ') : command;
371
+ dbg('exec (shell)', {
372
+ shell: typeof shell === 'string' ? shell : 'custom',
373
+ stdio: opts.stdio,
374
+ command: commandStr,
375
+ });
376
+ const envSan = sanitizeEnv(opts.env);
377
+ const shellOpts = { shell };
378
+ if (opts.cwd !== undefined)
379
+ shellOpts.cwd = opts.cwd;
380
+ if (envSan !== undefined)
381
+ shellOpts.env = envSan;
382
+ if (opts.stdio !== undefined)
383
+ shellOpts.stdio = opts.stdio;
384
+ const result = await execaCommand(commandStr, shellOpts);
385
+ const out = result?.stdout;
386
+ if (opts.stdio === 'pipe' && out) {
387
+ process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
388
+ }
389
+ const exit = result?.exitCode;
390
+ dbg('exit (shell)', { exitCode: exit });
391
+ return typeof exit === 'number' ? exit : Number.NaN;
392
+ }
393
+ };
394
+
191
395
  const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
192
396
  let cwd = process.cwd();
193
397
  if (pkgCwd) {
@@ -212,8 +416,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
212
416
  return { absRootPath, paths };
213
417
  };
214
418
  const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
215
- if (!command) {
216
- logger.error(`No command provided.`);
419
+ const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
420
+ Boolean(getDotenvCliOptions?.capture); // Require a command only when not listing. In list mode, a command is optional.
421
+ if (!command && !list) {
422
+ logger.error(`No command provided. Use --command or --list.`);
217
423
  process.exit(0);
218
424
  }
219
425
  const { absRootPath, paths } = await globPaths({
@@ -229,7 +435,13 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
229
435
  logger.info('');
230
436
  const headerRootPath = `ROOT: ${absRootPath}`;
231
437
  const headerGlobs = `GLOBS: ${globs}`;
232
- const headerCommand = `CMD: ${command}`;
438
+ // Prepare a safe label for the header (avoid undefined in template)
439
+ const commandLabel = Array.isArray(command)
440
+ ? command.join(' ')
441
+ : typeof command === 'string' && command.length > 0
442
+ ? command
443
+ : '';
444
+ const headerCommand = list ? `CMD: (list only)` : `CMD: ${commandLabel}`;
233
445
  logger.info('*'.repeat(Math.max(headerTitle.length, headerRootPath.length, headerGlobs.length, headerCommand.length)));
234
446
  logger.info(headerTitle);
235
447
  logger.info('');
@@ -249,17 +461,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
249
461
  logger.info(headerCommand);
250
462
  // Execute command.
251
463
  try {
252
- await execaCommand(command, {
253
- cwd: path,
254
- env: {
255
- ...process.env,
256
- getDotenvCliOptions: getDotenvCliOptions
257
- ? JSON.stringify(getDotenvCliOptions)
258
- : undefined,
259
- },
260
- stdio: 'inherit',
261
- shell, // already normalized to string | boolean | URL
262
- });
464
+ const hasCmd = (typeof command === 'string' && command.length > 0) ||
465
+ (Array.isArray(command) && command.length > 0);
466
+ if (hasCmd) {
467
+ await runCommand(command, shell, {
468
+ cwd: path,
469
+ env: {
470
+ ...process.env,
471
+ getDotenvCliOptions: getDotenvCliOptions
472
+ ? JSON.stringify(getDotenvCliOptions)
473
+ : undefined,
474
+ },
475
+ stdio: capture ? 'pipe' : 'inherit',
476
+ });
477
+ }
478
+ else {
479
+ // Should not occur due to the early guard; retain for type safety.
480
+ logger.error(`No command provided. Use --command or --list.`);
481
+ process.exit(0);
482
+ }
263
483
  }
264
484
  catch (error) {
265
485
  if (!ignoreErrors) {
@@ -379,72 +599,43 @@ const cmdCommand = new Command()
379
599
  });
380
600
 
381
601
  /**
382
- * Create the root Commander command with all options and subcommands.
383
- * Pure builder: no side-effects; the caller attaches lifecycle hooks.
602
+ * Create the root Commander command with legacy root options (via cliCore)
603
+ * and built-in subcommands. Pure builder: no side-effects; the caller attaches
604
+ * lifecycle hooks separately.
384
605
  */
385
606
  const createRootCommand = (opts) => {
386
- const { alias, debug, defaultEnv, description, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = opts;
387
- const excludeAll = !!excludeDynamic &&
388
- ((!!excludeEnv && !!excludeGlobal) ||
389
- (!!excludePrivate && !!excludePublic));
390
- const program = new Command()
391
- .name(alias)
392
- .description(description)
393
- .enablePositionalOptions()
394
- .passThroughOptions()
395
- .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env)
396
- .option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
397
- ['KEY1', 'VAL1'],
398
- ['KEY2', 'VAL2'],
399
- ]
400
- .map((v) => v.join(varsAssignor ?? '='))
401
- .join(varsDelimiter ?? ' ')}`, dotenvExpandFromProcessEnv)
402
- .option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv)
403
- .option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
404
- .addOption(new Option('-s, --shell [string]', (() => {
405
- const defaultLabel = shell
406
- ? ` (default ${typeof shell === 'boolean' ? 'OS shell' : shell})`
407
- : '';
408
- return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
409
- })()).conflicts('shellOff'))
410
- .addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
411
- .addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
412
- .addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
413
- .addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeAll ? ' (default)' : ''}`).conflicts('excludeAllOff'))
414
- .addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF${!excludeAll ? ' (default)' : ''}`).conflicts('excludeAll'))
415
- .addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
416
- .addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
417
- .addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
418
- .addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
419
- .addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
420
- .addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
421
- .addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
422
- .addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
423
- .addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
424
- .addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
425
- .addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
426
- .addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
427
- .addOption(new Option('-d, --debug', `debug mode ON${debug ? ' (default)' : ''}`).conflicts('debugOff'))
428
- .addOption(new Option('-D, --debug-off', `debug mode OFF${!debug ? ' (default)' : ''}`).conflicts('debug'))
429
- .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
430
- .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
431
- .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
432
- .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
433
- .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
434
- .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
435
- .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
436
- .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
437
- .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
438
- .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
439
- .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
440
- .addOption(new Option('--scripts <string>')
441
- .default(JSON.stringify(scripts))
442
- .hideHelp())
443
- .addCommand(batchCommand)
444
- .addCommand(cmdCommand, { isDefault: true });
607
+ const program = new Command().name(opts.alias).description(opts.description);
608
+ // Attach legacy root flags using shared cliCore builder to keep parity.
609
+ attachRootOptions(program, opts);
610
+ // Subcommands
611
+ program.addCommand(batchCommand).addCommand(cmdCommand, { isDefault: true });
445
612
  return program;
446
613
  };
447
614
 
615
+ // Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
616
+ const baseRootOptionDefaults = {
617
+ dotenvToken: '.env',
618
+ loadProcess: true,
619
+ logger: console,
620
+ paths: './',
621
+ pathsDelimiter: ' ',
622
+ privateToken: 'local',
623
+ scripts: {
624
+ 'git-status': {
625
+ cmd: 'git branch --show-current && git status -s -u',
626
+ shell: true,
627
+ },
628
+ },
629
+ shell: true,
630
+ vars: '',
631
+ varsAssignor: '=',
632
+ varsDelimiter: ' ',
633
+ // tri-state flags default to unset unless explicitly provided
634
+ // (debug/log/exclude* resolved via flag utils)
635
+ };
636
+
637
+ const baseGetDotenvCliOptions = baseRootOptionDefaults;
638
+
448
639
  /** @internal */
449
640
  const isPlainObject = (value) => value !== null &&
450
641
  typeof value === 'object' &&
@@ -497,8 +688,7 @@ const getDotenvOptionsFilename = 'getdotenv.config.json';
497
688
  */
498
689
  const defineDynamic = (d) => d;
499
690
  /**
500
- * Converts programmatic CLI options to `getDotenv` options.
501
- *
691
+ * Converts programmatic CLI options to `getDotenv` options. *
502
692
  * @param cliOptions - CLI options. Defaults to `{}`.
503
693
  *
504
694
  * @returns `getDotenv` options.
@@ -507,28 +697,49 @@ const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPatt
507
697
  /**
508
698
  * Convert CLI-facing string options into {@link GetDotenvOptions}.
509
699
  *
510
- * - Splits {@link GetDotenvCliOptions.paths} using either a delimiter
511
- * or a regular expression pattern into a string array.
512
- * - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
700
+ * - Splits {@link GetDotenvCliOptions.paths} using either a delimiter * or a regular expression pattern into a string array. * - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
513
701
  * pairs (configurable delimiters) into a {@link ProcessEnv}.
514
702
  * - Drops CLI-only keys that have no programmatic equivalent.
515
703
  *
516
704
  * @remarks
517
705
  * Follows exact-optional semantics by not emitting undefined-valued entries.
518
706
  */
519
- // Drop CLI-only keys
520
- const { debug, scripts, ...restFlags } = rest;
707
+ // Drop CLI-only keys (debug/scripts) without relying on Record casts.
708
+ // Create a shallow copy then delete optional CLI-only keys if present.
709
+ const restObj = { ...rest };
710
+ delete restObj.debug;
711
+ delete restObj.scripts;
521
712
  const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
522
- const kvPairs = (vars
523
- ? splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
713
+ // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
714
+ let parsedVars;
715
+ if (typeof vars === 'string') {
716
+ const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
524
717
  ? RegExp(varsAssignorPattern)
525
- : (varsAssignor ?? '=')))
526
- : []);
527
- const parsedVars = Object.fromEntries(kvPairs);
718
+ : (varsAssignor ?? '=')));
719
+ parsedVars = Object.fromEntries(kvPairs);
720
+ }
721
+ else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
722
+ // Keep only string or undefined values to match ProcessEnv.
723
+ const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
724
+ parsedVars = Object.fromEntries(entries);
725
+ }
726
+ // Drop undefined-valued entries at the converter stage to match ProcessEnv
727
+ // expectations and the compat test assertions.
728
+ if (parsedVars) {
729
+ parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
730
+ }
731
+ // Tolerate paths as either a delimited string or string[]
732
+ // Use a locally cast union type to avoid lint warnings about always-falsy conditions
733
+ // under the RootOptionsShape (which declares paths as string | undefined).
734
+ const pathsAny = paths;
735
+ const pathsOut = Array.isArray(pathsAny)
736
+ ? pathsAny.filter((p) => typeof p === 'string')
737
+ : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
738
+ // Preserve exactOptionalPropertyTypes: only include keys when defined.
528
739
  return {
529
- ...restFlags,
530
- paths: splitBy(paths, pathsDelimiter, pathsDelimiterPattern),
531
- vars: parsedVars,
740
+ ...restObj,
741
+ ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
742
+ ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
532
743
  };
533
744
  };
534
745
  const resolveGetDotenvOptions = async (customOptions) => {
@@ -601,6 +812,169 @@ const resolveGetDotenvCliGenerateOptions = async ({ importMetaUrl, ...customOpti
601
812
  return merged;
602
813
  };
603
814
 
815
+ /**
816
+ * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
817
+ * - If the user explicitly enabled the flag, return true.
818
+ * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
819
+ * - Otherwise, adopt the default (true → set; false/undefined → unset).
820
+ *
821
+ * @param exclude - The "on" flag value as parsed by Commander.
822
+ * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
823
+ * @param defaultValue - The generator default to adopt when no explicit toggle is present.
824
+ * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
825
+ *
826
+ * @example
827
+ * ```ts
828
+ * resolveExclusion(undefined, undefined, true); // => true
829
+ * ```
830
+ */
831
+ const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
832
+ /**
833
+ * Resolve an optional flag with "--exclude-all" overrides.
834
+ * If excludeAll is set and the individual "...-off" is not, force true.
835
+ * If excludeAllOff is set and the individual flag is not explicitly set, unset.
836
+ * Otherwise, adopt the default (true → set; false/undefined → unset).
837
+ *
838
+ * @param exclude - Individual include/exclude flag.
839
+ * @param excludeOff - Individual "...-off" flag.
840
+ * @param defaultValue - Default for the individual flag.
841
+ * @param excludeAll - Global "exclude-all" flag.
842
+ * @param excludeAllOff - Global "exclude-all-off" flag.
843
+ *
844
+ * @example
845
+ * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
846
+ */
847
+ const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
848
+ // Order of precedence:
849
+ // 1) Individual explicit "on" wins outright.
850
+ // 2) Individual explicit "off" wins over any global.
851
+ // 3) Global exclude-all forces true when not explicitly turned off.
852
+ // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
853
+ // 5) Fall back to the default (true => set; false/undefined => unset).
854
+ (() => {
855
+ // Individual "on"
856
+ if (exclude === true)
857
+ return true;
858
+ // Individual "off"
859
+ if (excludeOff === true)
860
+ return undefined;
861
+ // Global "exclude-all" ON (unless explicitly turned off)
862
+ if (excludeAll === true)
863
+ return true;
864
+ // Global "exclude-all-off" (unless explicitly enabled)
865
+ if (excludeAllOff === true)
866
+ return undefined;
867
+ // Default
868
+ return defaultValue ? true : undefined;
869
+ })();
870
+ /**
871
+ * exactOptionalPropertyTypes-safe setter for optional boolean flags:
872
+ * delete when undefined; assign when defined — without requiring an index signature on T.
873
+ *
874
+ * @typeParam T - Target object type.
875
+ * @param obj - The object to write to.
876
+ * @param key - The optional boolean property key of {@link T}.
877
+ * @param value - The value to set or `undefined` to unset.
878
+ *
879
+ * @remarks
880
+ * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
881
+ */
882
+ const setOptionalFlag = (obj, key, value) => {
883
+ const target = obj;
884
+ const k = key;
885
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
886
+ if (value === undefined)
887
+ delete target[k];
888
+ else
889
+ target[k] = value;
890
+ };
891
+
892
+ /**
893
+ * Merge and normalize raw Commander options (current + parent + defaults)
894
+ * into a GetDotenvCliOptions-like object. Types are intentionally wide to
895
+ * avoid cross-layer coupling; callers may cast as needed.
896
+ */
897
+ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
898
+ const parent = typeof parentJson === 'string' && parentJson.length > 0
899
+ ? JSON.parse(parentJson)
900
+ : undefined;
901
+ const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, scripts, shellOff, ...rest } = rawCliOptions;
902
+ const current = { ...rest };
903
+ if (typeof scripts === 'string') {
904
+ try {
905
+ current.scripts = JSON.parse(scripts);
906
+ }
907
+ catch {
908
+ // ignore parse errors; leave scripts undefined
909
+ }
910
+ }
911
+ const merged = defaultsDeep({}, defaults, parent ?? {}, current);
912
+ const d = defaults;
913
+ setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
914
+ setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
915
+ setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
916
+ setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
917
+ setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
918
+ setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
919
+ setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
920
+ setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
921
+ // Normalize shell for predictability: explicit default shell per OS.
922
+ const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
923
+ let resolvedShell = merged.shell;
924
+ if (shellOff)
925
+ resolvedShell = false;
926
+ else if (resolvedShell === true || resolvedShell === undefined) {
927
+ resolvedShell = defaultShell;
928
+ }
929
+ else if (typeof resolvedShell !== 'string' &&
930
+ typeof defaults.shell === 'string') {
931
+ resolvedShell = defaults.shell;
932
+ }
933
+ merged.shell = resolvedShell;
934
+ const cmd = typeof command === 'string' ? command : undefined;
935
+ return cmd !== undefined ? { merged, command: cmd } : { merged };
936
+ };
937
+
938
+ const applyKv = (current, kv) => {
939
+ if (!kv || Object.keys(kv).length === 0)
940
+ return current;
941
+ const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
942
+ return { ...current, ...expanded };
943
+ };
944
+ const applyConfigSlice = (current, cfg, env) => {
945
+ if (!cfg)
946
+ return current;
947
+ // kind axis: global then env (env overrides global)
948
+ const afterGlobal = applyKv(current, cfg.vars);
949
+ const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
950
+ return applyKv(afterGlobal, envKv);
951
+ };
952
+ /**
953
+ * Overlay config-provided values onto a base ProcessEnv using precedence axes:
954
+ * - kind: env \> global
955
+ * - privacy: local \> public
956
+ * - source: project \> packaged \> base
957
+ *
958
+ * Programmatic explicit vars (if provided) override all config slices.
959
+ * Progressive expansion is applied within each slice.
960
+ */
961
+ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
962
+ let current = { ...base };
963
+ // Source: packaged (public -> local)
964
+ current = applyConfigSlice(current, configs.packaged, env);
965
+ // Packaged "local" is not expected by policy; if present, honor it.
966
+ // We do not have a separate object for packaged.local in sources, keep as-is.
967
+ // Source: project (public -> local)
968
+ current = applyConfigSlice(current, configs.project?.public, env);
969
+ current = applyConfigSlice(current, configs.project?.local, env);
970
+ // Programmatic explicit vars (top of static tier)
971
+ if (programmaticVars) {
972
+ const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
973
+ current = applyKv(current, toApply);
974
+ }
975
+ return current;
976
+ };
977
+
604
978
  /**
605
979
  * Asynchronously read a dotenv file & parse it into an object.
606
980
  *
@@ -616,65 +990,95 @@ const readDotenv = async (path) => {
616
990
  }
617
991
  };
618
992
 
619
- const importDefault = async (fileUrl) => {
993
+ const importDefault$1 = async (fileUrl) => {
620
994
  const mod = (await import(fileUrl));
621
995
  return mod.default;
622
996
  };
623
- /**
624
- * @internal Compute a short hash from path + mtime for cache filenames.
625
- */
626
997
  const cacheHash = (absPath, mtimeMs) => createHash('sha1')
627
998
  .update(absPath)
628
999
  .update(String(mtimeMs))
629
1000
  .digest('hex')
630
1001
  .slice(0, 12);
631
1002
  /**
632
- * @internal Load a dynamic module from path. Supports .js/.mjs/.ts/.tsx:
633
- * - .js/.mjs: direct import
634
- * - .ts/.tsx: try direct import (in case a TS loader is active), otherwise:
635
- * - esbuild (if present): bundle to a temp ESM file and import it
636
- * - fallback: typescript.transpileModule (single-file), then import temp file
1003
+ * Remove older compiled cache files for a given source base name, keeping
1004
+ * at most `keep` most-recent files. Errors are ignored by design.
637
1005
  */
638
- const loadDynamicFromPath = async (absPath) => {
639
- if (!(await fs.exists(absPath)))
640
- return undefined;
1006
+ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
1007
+ try {
1008
+ const entries = await fs.readdir(cacheDir);
1009
+ const mine = entries
1010
+ .filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
1011
+ .map((f) => path.join(cacheDir, f));
1012
+ if (mine.length <= keep)
1013
+ return;
1014
+ const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
1015
+ stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
1016
+ const toDelete = stats.slice(keep).map((s) => s.p);
1017
+ await Promise.all(toDelete.map(async (p) => {
1018
+ try {
1019
+ await fs.remove(p);
1020
+ }
1021
+ catch {
1022
+ // best-effort cleanup
1023
+ }
1024
+ }));
1025
+ }
1026
+ catch {
1027
+ // best-effort cleanup
1028
+ }
1029
+ };
1030
+ /**
1031
+ * Load a module default export from a JS/TS file with robust fallbacks:
1032
+ * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
1033
+ * 1) try direct import (if a TS loader is active),
1034
+ * 2) esbuild bundle to a temp ESM file,
1035
+ * 3) typescript.transpileModule fallback for simple modules.
1036
+ *
1037
+ * @param absPath - absolute path to source file
1038
+ * @param cacheDirName - cache subfolder under .tsbuild
1039
+ */
1040
+ const loadModuleDefault = async (absPath, cacheDirName) => {
641
1041
  const ext = path.extname(absPath).toLowerCase();
642
1042
  const fileUrl = url.pathToFileURL(absPath).toString();
643
- if (ext !== '.ts' && ext !== '.tsx') {
644
- return importDefault(fileUrl);
1043
+ if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
1044
+ return importDefault$1(fileUrl);
645
1045
  }
646
- // Try direct import (in case user started Node with a TS loader).
1046
+ // Try direct import first (TS loader active)
647
1047
  try {
648
- const dyn = await importDefault(fileUrl);
1048
+ const dyn = await importDefault$1(fileUrl);
649
1049
  if (dyn)
650
1050
  return dyn;
651
1051
  }
652
1052
  catch {
653
- // ignore; fall through to compile
1053
+ /* fall through */
654
1054
  }
655
1055
  const stat = await fs.stat(absPath);
656
1056
  const hash = cacheHash(absPath, stat.mtimeMs);
657
- const cacheDir = path.resolve('.tsbuild', 'getdotenv-dynamic');
1057
+ const cacheDir = path.resolve('.tsbuild', cacheDirName);
1058
+ await fs.ensureDir(cacheDir);
658
1059
  const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
659
- // Try esbuild first
1060
+ // Try esbuild
660
1061
  try {
661
1062
  const esbuild = (await import('esbuild'));
662
- await fs.ensureDir(cacheDir);
663
1063
  await esbuild.build({
664
1064
  entryPoints: [absPath],
665
1065
  bundle: true,
666
1066
  platform: 'node',
667
1067
  format: 'esm',
668
- target: 'node22',
1068
+ target: 'node20',
669
1069
  outfile: cacheFile,
670
1070
  sourcemap: false,
671
1071
  logLevel: 'silent',
672
1072
  });
673
- return await importDefault(url.pathToFileURL(cacheFile).toString());
1073
+ const result = await importDefault$1(url.pathToFileURL(cacheFile).toString());
1074
+ // Best-effort: trim older cache files for this source.
1075
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
1076
+ return result;
674
1077
  }
675
1078
  catch {
676
- // no esbuild; fall back to TS transpile for simple modules
1079
+ /* fall through to TS transpile */
677
1080
  }
1081
+ // TypeScript transpile fallback
678
1082
  try {
679
1083
  const ts = (await import('typescript'));
680
1084
  const code = await fs.readFile(absPath, 'utf-8');
@@ -684,16 +1088,19 @@ const loadDynamicFromPath = async (absPath) => {
684
1088
  target: 'ES2022',
685
1089
  moduleResolution: 'NodeNext',
686
1090
  },
687
- });
688
- await fs.ensureDir(cacheDir);
689
- await fs.writeFile(cacheFile, out.outputText, 'utf-8');
690
- return await importDefault(url.pathToFileURL(cacheFile).toString());
1091
+ }).outputText;
1092
+ await fs.writeFile(cacheFile, out, 'utf-8');
1093
+ const result = await importDefault$1(url.pathToFileURL(cacheFile).toString());
1094
+ // Best-effort: trim older cache files for this source.
1095
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
1096
+ return result;
691
1097
  }
692
1098
  catch {
693
- throw new Error(`Unable to load dynamic TypeScript file: ${absPath}. ` +
694
- `Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
1099
+ // Caller decides final error wording; rethrow for upstream mapping.
1100
+ throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
695
1101
  }
696
1102
  };
1103
+
697
1104
  /**
698
1105
  * Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
699
1106
  *
@@ -778,7 +1185,16 @@ const getDotenv = async (options = {}) => {
778
1185
  }
779
1186
  else if (dynamicPath) {
780
1187
  const absDynamicPath = path.resolve(dynamicPath);
781
- dynamic = await loadDynamicFromPath(absDynamicPath);
1188
+ if (await fs.exists(absDynamicPath)) {
1189
+ try {
1190
+ dynamic = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic');
1191
+ }
1192
+ catch {
1193
+ // Preserve legacy error text for compatibility with tests/docs.
1194
+ throw new Error(`Unable to load dynamic TypeScript file: ${absDynamicPath}. ` +
1195
+ `Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
1196
+ }
1197
+ }
782
1198
  }
783
1199
  if (dynamic) {
784
1200
  try {
@@ -817,72 +1233,320 @@ const getDotenv = async (options = {}) => {
817
1233
  };
818
1234
 
819
1235
  /**
820
- * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
821
- * - If the user explicitly enabled the flag, return true.
822
- * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
823
- * - Otherwise, adopt the default (true set; false/undefined → unset).
824
- *
825
- * @param exclude - The "on" flag value as parsed by Commander.
826
- * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
827
- * @param defaultValue - The generator default to adopt when no explicit toggle is present.
828
- * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
829
- *
830
- * @example
831
- * ```ts
832
- * resolveExclusion(undefined, undefined, true); // => true
833
- * ```
1236
+ * Zod schemas for configuration files discovered by the new loader. *
1237
+ * Notes:
1238
+ * - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
1239
+ * - RESOLVED: normalized shapes (paths always string[]).
1240
+ * - For this step (JSON/YAML only), any defined `dynamic` will be rejected by the loader.
834
1241
  */
835
- const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
1242
+ // String-only env value map
1243
+ const stringMap = z.record(z.string(), z.string());
1244
+ const envStringMap = z.record(z.string(), stringMap);
1245
+ // Allow string[] or single string for "paths" in RAW; normalize later.
1246
+ const rawPathsSchema = z.union([z.array(z.string()), z.string()]).optional();
1247
+ const getDotenvConfigSchemaRaw = z.object({
1248
+ dotenvToken: z.string().optional(),
1249
+ privateToken: z.string().optional(),
1250
+ paths: rawPathsSchema,
1251
+ loadProcess: z.boolean().optional(),
1252
+ log: z.boolean().optional(),
1253
+ shell: z.union([z.string(), z.boolean()]).optional(),
1254
+ scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
1255
+ vars: stringMap.optional(), // public, global
1256
+ envVars: envStringMap.optional(), // public, per-env
1257
+ // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
1258
+ dynamic: z.unknown().optional(),
1259
+ // Per-plugin config bag; validated by plugins/host when used.
1260
+ plugins: z.record(z.string(), z.unknown()).optional(),
1261
+ });
1262
+ // Normalize paths to string[]
1263
+ const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
1264
+ const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
1265
+ ...raw,
1266
+ paths: normalizePaths(raw.paths),
1267
+ }));
1268
+
1269
+ // Discovery candidates (first match wins per scope/privacy).
1270
+ // Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
1271
+ const PUBLIC_FILENAMES = [
1272
+ 'getdotenv.config.json',
1273
+ 'getdotenv.config.yaml',
1274
+ 'getdotenv.config.yml',
1275
+ 'getdotenv.config.js',
1276
+ 'getdotenv.config.mjs',
1277
+ 'getdotenv.config.cjs',
1278
+ 'getdotenv.config.ts',
1279
+ 'getdotenv.config.mts',
1280
+ 'getdotenv.config.cts',
1281
+ ];
1282
+ const LOCAL_FILENAMES = [
1283
+ 'getdotenv.config.local.json',
1284
+ 'getdotenv.config.local.yaml',
1285
+ 'getdotenv.config.local.yml',
1286
+ 'getdotenv.config.local.js',
1287
+ 'getdotenv.config.local.mjs',
1288
+ 'getdotenv.config.local.cjs',
1289
+ 'getdotenv.config.local.ts',
1290
+ 'getdotenv.config.local.mts',
1291
+ 'getdotenv.config.local.cts',
1292
+ ];
1293
+ const isYaml = (p) => ['.yaml', '.yml'].includes(extname(p).toLowerCase());
1294
+ const isJson = (p) => extname(p).toLowerCase() === '.json';
1295
+ const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(extname(p).toLowerCase());
1296
+ // --- Internal JS/TS module loader helpers (default export) ---
1297
+ const importDefault = async (fileUrl) => {
1298
+ const mod = (await import(fileUrl));
1299
+ return mod.default;
1300
+ };
1301
+ const cacheName = (absPath, suffix) => {
1302
+ // sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
1303
+ const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
1304
+ return `${base}.${suffix}.mjs`;
1305
+ };
1306
+ const ensureDir = async (dir) => {
1307
+ await fs.ensureDir(dir);
1308
+ return dir;
1309
+ };
1310
+ const loadJsTsDefault = async (absPath) => {
1311
+ const fileUrl = pathToFileURL(absPath).toString();
1312
+ const ext = extname(absPath).toLowerCase();
1313
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
1314
+ return importDefault(fileUrl);
1315
+ }
1316
+ // Try direct import first in case a TS loader is active.
1317
+ try {
1318
+ const val = await importDefault(fileUrl);
1319
+ if (val)
1320
+ return val;
1321
+ }
1322
+ catch {
1323
+ /* fallthrough */
1324
+ }
1325
+ // esbuild bundle to a temp ESM file
1326
+ try {
1327
+ const esbuild = (await import('esbuild'));
1328
+ const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
1329
+ const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
1330
+ await esbuild.build({
1331
+ entryPoints: [absPath],
1332
+ bundle: true,
1333
+ platform: 'node',
1334
+ format: 'esm',
1335
+ target: 'node20',
1336
+ outfile,
1337
+ sourcemap: false,
1338
+ logLevel: 'silent',
1339
+ });
1340
+ return await importDefault(pathToFileURL(outfile).toString());
1341
+ }
1342
+ catch {
1343
+ /* fallthrough to TS transpile */
1344
+ }
1345
+ // typescript.transpileModule simple transpile (single-file)
1346
+ try {
1347
+ const ts = (await import('typescript'));
1348
+ const src = await fs.readFile(absPath, 'utf-8');
1349
+ const out = ts.transpileModule(src, {
1350
+ compilerOptions: {
1351
+ module: 'ESNext',
1352
+ target: 'ES2022',
1353
+ moduleResolution: 'NodeNext',
1354
+ },
1355
+ }).outputText;
1356
+ const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
1357
+ const outfile = path.join(outDir, cacheName(absPath, 'ts'));
1358
+ await fs.writeFile(outfile, out, 'utf-8');
1359
+ return await importDefault(pathToFileURL(outfile).toString());
1360
+ }
1361
+ catch {
1362
+ throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
1363
+ }
1364
+ };
836
1365
  /**
837
- * Resolve an optional flag with "--exclude-all" overrides.
838
- * If excludeAll is set and the individual "...-off" is not, force true.
839
- * If excludeAllOff is set and the individual flag is not explicitly set, unset.
840
- * Otherwise, adopt the default (true → set; false/undefined → unset).
841
- *
842
- * @param exclude - Individual include/exclude flag.
843
- * @param excludeOff - Individual "...-off" flag.
844
- * @param defaultValue - Default for the individual flag.
845
- * @param excludeAll - Global "exclude-all" flag.
846
- * @param excludeAllOff - Global "exclude-all-off" flag.
1366
+ * Discover JSON/YAML config files in the packaged root and project root.
1367
+ * Order: packaged public project public project local. */
1368
+ const discoverConfigFiles = async (importMetaUrl) => {
1369
+ const files = [];
1370
+ // Packaged root via importMetaUrl (optional)
1371
+ if (importMetaUrl) {
1372
+ const fromUrl = fileURLToPath(importMetaUrl);
1373
+ const packagedRoot = await packageDirectory({ cwd: fromUrl });
1374
+ if (packagedRoot) {
1375
+ for (const name of PUBLIC_FILENAMES) {
1376
+ const p = join(packagedRoot, name);
1377
+ if (await fs.pathExists(p)) {
1378
+ files.push({ path: p, privacy: 'public', scope: 'packaged' });
1379
+ break; // only one public file expected per scope
1380
+ }
1381
+ }
1382
+ // By policy, packaged .local is not expected; skip even if present.
1383
+ }
1384
+ }
1385
+ // Project root (from current working directory)
1386
+ const projectRoot = await packageDirectory();
1387
+ if (projectRoot) {
1388
+ for (const name of PUBLIC_FILENAMES) {
1389
+ const p = join(projectRoot, name);
1390
+ if (await fs.pathExists(p)) {
1391
+ files.push({ path: p, privacy: 'public', scope: 'project' });
1392
+ break;
1393
+ }
1394
+ }
1395
+ for (const name of LOCAL_FILENAMES) {
1396
+ const p = join(projectRoot, name);
1397
+ if (await fs.pathExists(p)) {
1398
+ files.push({ path: p, privacy: 'local', scope: 'project' });
1399
+ break;
1400
+ }
1401
+ }
1402
+ }
1403
+ return files;
1404
+ };
1405
+ /**
1406
+ * Load a single config file (JSON/YAML). JS/TS is not supported in this step.
1407
+ * Validates with Zod RAW schema, then normalizes to RESOLVED.
847
1408
  *
848
- * @example
849
- * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
1409
+ * For JSON/YAML: if a "dynamic" property is present, throws with guidance.
1410
+ * For JS/TS: default export is loaded; "dynamic" is allowed.
1411
+ */
1412
+ const loadConfigFile = async (filePath) => {
1413
+ let raw = {};
1414
+ try {
1415
+ const abs = path.resolve(filePath);
1416
+ if (isJsOrTs(abs)) {
1417
+ // JS/TS support: load default export via robust pipeline.
1418
+ const mod = await loadJsTsDefault(abs);
1419
+ raw = mod ?? {};
1420
+ }
1421
+ else {
1422
+ const txt = await fs.readFile(abs, 'utf-8');
1423
+ raw = isJson(abs) ? JSON.parse(txt) : isYaml(abs) ? YAML.parse(txt) : {};
1424
+ }
1425
+ }
1426
+ catch (err) {
1427
+ throw new Error(`Failed to read/parse config: ${filePath}. ${String(err)}`);
1428
+ }
1429
+ // Validate RAW
1430
+ const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
1431
+ if (!parsed.success) {
1432
+ const msgs = parsed.error.issues
1433
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
1434
+ .join('\n');
1435
+ throw new Error(`Invalid config ${filePath}:\n${msgs}`);
1436
+ }
1437
+ // Disallow dynamic in JSON/YAML; allow in JS/TS
1438
+ if (!isJsOrTs(filePath) && parsed.data.dynamic !== undefined) {
1439
+ throw new Error(`Config ${filePath} specifies "dynamic"; JSON/YAML configs cannot include dynamic in this step. Use JS/TS config.`);
1440
+ }
1441
+ return getDotenvConfigSchemaResolved.parse(parsed.data);
1442
+ };
1443
+ /**
1444
+ * Discover and load configs into resolved shapes, ordered by scope/privacy.
1445
+ * JSON/YAML/JS/TS supported; first match per scope/privacy applies.
850
1446
  */
851
- const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) => excludeAll && !excludeOff
852
- ? true
853
- : excludeAllOff && !exclude
854
- ? undefined
855
- : defaultValue
856
- ? true
857
- : undefined;
1447
+ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
1448
+ const discovered = await discoverConfigFiles(importMetaUrl);
1449
+ const result = {};
1450
+ for (const f of discovered) {
1451
+ const cfg = await loadConfigFile(f.path);
1452
+ if (f.scope === 'packaged') {
1453
+ // packaged public only
1454
+ result.packaged = cfg;
1455
+ }
1456
+ else {
1457
+ result.project ??= {};
1458
+ if (f.privacy === 'public')
1459
+ result.project.public = cfg;
1460
+ else
1461
+ result.project.local = cfg;
1462
+ }
1463
+ }
1464
+ return result;
1465
+ };
1466
+
858
1467
  /**
859
- * exactOptionalPropertyTypes-safe setter for optional boolean flags:
860
- * delete when undefined; assign when defined without requiring an index signature on T.
1468
+ * Resolve dotenv values using the config-loader/overlay path (always-on in
1469
+ * host/generator flows; no-op when no config files are present).
861
1470
  *
862
- * @typeParam T - Target object type.
863
- * @param obj - The object to write to.
864
- * @param key - The optional boolean property key of {@link T}.
865
- * @param value - The value to set or `undefined` to unset.
866
- *
867
- * @remarks
868
- * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
1471
+ * Order:
1472
+ * 1) Compute base from files only (exclude dynamic; ignore programmatic vars).
1473
+ * 2) Discover packaged + project config sources and overlay onto base.
1474
+ * 3) Apply dynamics in order:
1475
+ * programmatic dynamic \> config dynamic (packaged → project public → project local)
1476
+ * \> file dynamicPath. * 4) Optionally write outputPath, log, and merge into process.env.
869
1477
  */
870
- const setOptionalFlag = (obj, key, value) => {
871
- const target = obj;
872
- const k = key;
873
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
874
- if (value === undefined)
875
- delete target[k];
876
- else
877
- target[k] = value;
1478
+ const resolveDotenvWithConfigLoader = async (validated) => {
1479
+ // 1) Base from files, no dynamic, no programmatic vars
1480
+ const base = await getDotenv({
1481
+ ...validated,
1482
+ // Build a pure base without side effects or logging.
1483
+ excludeDynamic: true,
1484
+ vars: {},
1485
+ log: false,
1486
+ loadProcess: false,
1487
+ outputPath: undefined,
1488
+ });
1489
+ // 2) Discover config sources (packaged via this module's import.meta.url)
1490
+ const sources = await resolveGetDotenvConfigSources(import.meta.url);
1491
+ const dotenv = overlayEnv({
1492
+ base,
1493
+ env: validated.env ?? validated.defaultEnv,
1494
+ configs: sources,
1495
+ ...(validated.vars ? { programmaticVars: validated.vars } : {}),
1496
+ });
1497
+ // Helper to apply a dynamic map progressively.
1498
+ const applyDynamic = (target, dynamic, env) => {
1499
+ if (!dynamic)
1500
+ return;
1501
+ for (const key of Object.keys(dynamic)) {
1502
+ const value = typeof dynamic[key] === 'function'
1503
+ ? dynamic[key](target, env)
1504
+ : dynamic[key];
1505
+ Object.assign(target, { [key]: value });
1506
+ }
1507
+ };
1508
+ // 3) Apply dynamics in order
1509
+ applyDynamic(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
1510
+ applyDynamic(dotenv, (sources.packaged?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1511
+ applyDynamic(dotenv, (sources.project?.public?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1512
+ applyDynamic(dotenv, (sources.project?.local?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1513
+ // file dynamicPath (lowest)
1514
+ if (validated.dynamicPath) {
1515
+ const absDynamicPath = path.resolve(validated.dynamicPath);
1516
+ try {
1517
+ const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
1518
+ applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
1519
+ }
1520
+ catch {
1521
+ throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
1522
+ }
1523
+ }
1524
+ // 4) Output/log/process merge
1525
+ if (validated.outputPath) {
1526
+ await fs.writeFile(validated.outputPath, Object.keys(dotenv).reduce((contents, key) => {
1527
+ const value = dotenv[key] ?? '';
1528
+ return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
1529
+ }, ''), { encoding: 'utf-8' });
1530
+ }
1531
+ const logger = validated.logger ?? console;
1532
+ if (validated.log)
1533
+ logger.log(dotenv);
1534
+ if (validated.loadProcess)
1535
+ Object.assign(process.env, dotenv);
1536
+ return dotenv;
878
1537
  };
879
1538
 
1539
+ /**
1540
+ * Omit a "logger" key from an options object in a typed manner.
1541
+ */
1542
+ const omitLogger = (obj) => {
1543
+ const { logger: _omitted, ...rest } = obj;
1544
+ return rest;
1545
+ };
880
1546
  /**
881
1547
  * Build the Commander preSubcommand hook using the provided context.
882
- *
883
- * Responsibilities:
884
- * - Merge parent CLI options with current invocation (parent \< current).
885
- * - Resolve tri-state flags, including `--exclude-all` overrides.
1548
+ * * Responsibilities:
1549
+ * - Merge parent CLI options with current invocation (parent \< current). * - Resolve tri-state flags, including `--exclude-all` overrides.
886
1550
  * - Normalize the shell setting to a concrete value (string | boolean).
887
1551
  * - Persist merged options on the command instance and pass to subcommands.
888
1552
  * - Execute {@link getDotenv} and optional post-hook.
@@ -892,95 +1556,52 @@ const setOptionalFlag = (obj, key, value) => {
892
1556
  * @returns An async hook suitable for Commander’s `preSubcommand`.
893
1557
  *
894
1558
  * @example `program.hook('preSubcommand', makePreSubcommandHook(ctx));`
895
- */ const makePreSubcommandHook = ({ logger, preHook, postHook, defaults }) => async (thisCommand) => {
896
- // Get parent command GetDotenvCliOptions.
897
- const parentGetDotenvCliOptions = process.env.getDotenvCliOptions
898
- ? JSON.parse(process.env.getDotenvCliOptions)
899
- : undefined;
900
- // Get raw CLI options from commander.
901
- const rawCliOptions = thisCommand.opts();
902
- // Extract current GetDotenvCliOptions from raw CLI options.
903
- const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, scripts, shellOff, ...rawCliOptionsRest } = rawCliOptions;
904
- const currentGetDotenvCliOptions = rawCliOptionsRest;
905
- if (scripts)
906
- currentGetDotenvCliOptions.scripts = JSON.parse(scripts);
907
- // Merge current & parent GetDotenvCliOptions (parent < current).
908
- const mergedGetDotenvCliOptions = defaultsDeep((parentGetDotenvCliOptions ?? {}), currentGetDotenvCliOptions);
909
- // Resolve flags using defaults + current + exclude-all toggles.
910
- setOptionalFlag(mergedGetDotenvCliOptions, 'debug', resolveExclusion(mergedGetDotenvCliOptions.debug, debugOff, defaults.debug));
911
- setOptionalFlag(mergedGetDotenvCliOptions, 'excludeDynamic', resolveExclusionAll(mergedGetDotenvCliOptions.excludeDynamic, excludeDynamicOff, defaults.excludeDynamic, excludeAll, excludeAllOff));
912
- setOptionalFlag(mergedGetDotenvCliOptions, 'excludeEnv', resolveExclusionAll(mergedGetDotenvCliOptions.excludeEnv, excludeEnvOff, defaults.excludeEnv, excludeAll, excludeAllOff));
913
- setOptionalFlag(mergedGetDotenvCliOptions, 'excludeGlobal', resolveExclusionAll(mergedGetDotenvCliOptions.excludeGlobal, excludeGlobalOff, defaults.excludeGlobal, excludeAll, excludeAllOff));
914
- setOptionalFlag(mergedGetDotenvCliOptions, 'excludePrivate', resolveExclusionAll(mergedGetDotenvCliOptions.excludePrivate, excludePrivateOff, defaults.excludePrivate, excludeAll, excludeAllOff));
915
- setOptionalFlag(mergedGetDotenvCliOptions, 'excludePublic', resolveExclusionAll(mergedGetDotenvCliOptions.excludePublic, excludePublicOff, defaults.excludePublic, excludeAll, excludeAllOff));
916
- setOptionalFlag(mergedGetDotenvCliOptions, 'log', resolveExclusion(mergedGetDotenvCliOptions.log, logOff, defaults.log));
917
- setOptionalFlag(mergedGetDotenvCliOptions, 'loadProcess', resolveExclusion(mergedGetDotenvCliOptions.loadProcess, loadProcessOff, defaults.loadProcess));
918
- // Normalize shell for predictability: explicit default shell per OS.
919
- const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
920
- let resolvedShell = mergedGetDotenvCliOptions.shell;
921
- if (shellOff)
922
- resolvedShell = false;
923
- else if (resolvedShell === true || resolvedShell === undefined) {
924
- resolvedShell = defaultShell;
925
- }
926
- else if (typeof resolvedShell !== 'string' &&
927
- typeof defaults.shell === 'string') {
928
- resolvedShell = defaults.shell;
929
- }
930
- mergedGetDotenvCliOptions.shell = resolvedShell;
931
- if (mergedGetDotenvCliOptions.debug && parentGetDotenvCliOptions) {
932
- logger.debug('\n*** parent command GetDotenvCliOptions ***\n', parentGetDotenvCliOptions);
933
- }
934
- if (mergedGetDotenvCliOptions.debug)
935
- logger.debug('\n*** current command raw options ***\n', rawCliOptions);
936
- if (mergedGetDotenvCliOptions.debug)
937
- logger.debug('\n*** merged GetDotenvCliOptions ***\n', {
938
- mergedGetDotenvCliOptions,
939
- });
940
- // Execute pre-hook.
941
- if (preHook) {
942
- await preHook(mergedGetDotenvCliOptions);
943
- if (mergedGetDotenvCliOptions.debug)
944
- logger.debug('\n*** GetDotenvCliOptions after pre-hook ***\n', mergedGetDotenvCliOptions);
945
- }
946
- // Persist GetDotenvCliOptions in command for subcommand access.
947
- thisCommand.getDotenvCliOptions = mergedGetDotenvCliOptions;
948
- // Execute getdotenv.
949
- const dotenv = await getDotenv(getDotenvCliOptions2Options(mergedGetDotenvCliOptions));
950
- if (mergedGetDotenvCliOptions.debug)
951
- logger.debug('\n*** getDotenv output ***\n', dotenv);
952
- // Execute post-hook.
953
- if (postHook)
954
- await postHook(dotenv);
955
- // Execute command.
956
- const args = thisCommand.args ?? [];
957
- const isCommand = typeof command === 'string' && command.length > 0;
958
- if (isCommand && args.length > 0) {
959
- const lr = logger;
960
- (lr.error ?? lr.log)(`--command option conflicts with cmd subcommand.`);
961
- process.exit(0);
962
- }
963
- if (typeof command === 'string' && command.length > 0) {
964
- const cmd = resolveCommand(mergedGetDotenvCliOptions.scripts, command);
965
- if (mergedGetDotenvCliOptions.debug)
966
- logger.debug('\n*** command ***\n', cmd);
967
- const envSafe = {
968
- ...mergedGetDotenvCliOptions,
969
- };
970
- delete envSafe.logger;
971
- await execaCommand(cmd, {
972
- env: {
973
- ...process.env,
974
- getDotenvCliOptions: JSON.stringify(envSafe),
975
- },
976
- shell: resolveShell(mergedGetDotenvCliOptions.scripts, command, mergedGetDotenvCliOptions.shell),
977
- stdio: 'inherit',
978
- });
979
- }
1559
+ */
1560
+ const makePreSubcommandHook = ({ logger, preHook, postHook, defaults, }) => {
1561
+ return async (thisCommand) => {
1562
+ // Get raw CLI options from commander.
1563
+ const rawCliOptions = thisCommand.opts();
1564
+ const { merged: mergedGetDotenvCliOptions, command: commandOpt } = resolveCliOptions(rawCliOptions, defaults, process.env.getDotenvCliOptions);
1565
+ // Optional debug logging retained via mergedGetDotenvCliOptions.debug if desired. // Execute pre-hook.
1566
+ if (preHook) {
1567
+ await preHook(mergedGetDotenvCliOptions);
1568
+ if (mergedGetDotenvCliOptions.debug)
1569
+ logger.debug('\n*** GetDotenvCliOptions after pre-hook ***\n', mergedGetDotenvCliOptions);
1570
+ }
1571
+ // Persist GetDotenvCliOptions in command for subcommand access.
1572
+ thisCommand.getDotenvCliOptions =
1573
+ mergedGetDotenvCliOptions;
1574
+ // Execute getdotenv via always-on config loader/overlay path.
1575
+ const serviceOptions = getDotenvCliOptions2Options(mergedGetDotenvCliOptions);
1576
+ const dotenv = await resolveDotenvWithConfigLoader(serviceOptions);
1577
+ // Execute post-hook.
1578
+ if (postHook)
1579
+ await postHook(dotenv); // Execute command.
1580
+ const args = thisCommand.args ?? [];
1581
+ const isCommand = typeof commandOpt === 'string' && commandOpt.length > 0;
1582
+ if (isCommand && args.length > 0) {
1583
+ const lr = logger;
1584
+ (lr.error ?? lr.log)(`--command option conflicts with cmd subcommand.`);
1585
+ process.exit(0);
1586
+ }
1587
+ if (typeof commandOpt === 'string' && commandOpt.length > 0) {
1588
+ const cmd = resolveCommand(mergedGetDotenvCliOptions.scripts, commandOpt);
1589
+ if (mergedGetDotenvCliOptions.debug)
1590
+ logger.debug('\n*** command ***\n', cmd);
1591
+ // Build a logger-free bag for env round-trip.
1592
+ const envSafe = omitLogger(mergedGetDotenvCliOptions);
1593
+ await execaCommand(cmd, {
1594
+ env: { ...process.env, getDotenvCliOptions: JSON.stringify(envSafe) },
1595
+ shell: resolveShell(mergedGetDotenvCliOptions.scripts, commandOpt, mergedGetDotenvCliOptions.shell),
1596
+ stdio: 'inherit',
1597
+ });
1598
+ }
1599
+ };
980
1600
  };
981
1601
 
982
1602
  /**
983
- * Generate a Commander CLI Command for get-dotenv. * Orchestration only: delegates building and lifecycle hooks.
1603
+ * Generate a Commander CLI Command for get-dotenv.
1604
+ * Orchestration only: delegates building and lifecycle hooks.
984
1605
  */
985
1606
  const generateGetDotenvCli = async (customOptions) => {
986
1607
  const options = await resolveGetDotenvCliGenerateOptions(customOptions);