@karmaniverous/get-dotenv 5.2.6 → 6.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 (55) hide show
  1. package/README.md +106 -70
  2. package/dist/cliHost.d.ts +232 -226
  3. package/dist/cliHost.mjs +777 -545
  4. package/dist/config.d.ts +7 -2
  5. package/dist/env-overlay.d.ts +21 -9
  6. package/dist/env-overlay.mjs +14 -19
  7. package/dist/getdotenv.cli.mjs +1366 -1163
  8. package/dist/index.d.ts +415 -242
  9. package/dist/index.mjs +1364 -1414
  10. package/dist/plugins-aws.d.ts +149 -94
  11. package/dist/plugins-aws.mjs +307 -195
  12. package/dist/plugins-batch.d.ts +153 -99
  13. package/dist/plugins-batch.mjs +277 -95
  14. package/dist/plugins-cmd.d.ts +140 -94
  15. package/dist/plugins-cmd.mjs +636 -502
  16. package/dist/plugins-demo.d.ts +140 -94
  17. package/dist/plugins-demo.mjs +237 -46
  18. package/dist/plugins-init.d.ts +140 -94
  19. package/dist/plugins-init.mjs +129 -12
  20. package/dist/plugins.d.ts +166 -103
  21. package/dist/plugins.mjs +977 -840
  22. package/package.json +15 -53
  23. package/templates/cli/ts/plugins/hello.ts +27 -6
  24. package/templates/config/js/getdotenv.config.js +1 -1
  25. package/templates/config/ts/getdotenv.config.ts +9 -2
  26. package/dist/cliHost.cjs +0 -1875
  27. package/dist/cliHost.d.cts +0 -409
  28. package/dist/cliHost.d.mts +0 -409
  29. package/dist/config.cjs +0 -252
  30. package/dist/config.d.cts +0 -55
  31. package/dist/config.d.mts +0 -55
  32. package/dist/env-overlay.cjs +0 -163
  33. package/dist/env-overlay.d.cts +0 -50
  34. package/dist/env-overlay.d.mts +0 -50
  35. package/dist/index.cjs +0 -4140
  36. package/dist/index.d.cts +0 -457
  37. package/dist/index.d.mts +0 -457
  38. package/dist/plugins-aws.cjs +0 -667
  39. package/dist/plugins-aws.d.cts +0 -158
  40. package/dist/plugins-aws.d.mts +0 -158
  41. package/dist/plugins-batch.cjs +0 -616
  42. package/dist/plugins-batch.d.cts +0 -180
  43. package/dist/plugins-batch.d.mts +0 -180
  44. package/dist/plugins-cmd.cjs +0 -1113
  45. package/dist/plugins-cmd.d.cts +0 -178
  46. package/dist/plugins-cmd.d.mts +0 -178
  47. package/dist/plugins-demo.cjs +0 -307
  48. package/dist/plugins-demo.d.cts +0 -158
  49. package/dist/plugins-demo.d.mts +0 -158
  50. package/dist/plugins-init.cjs +0 -289
  51. package/dist/plugins-init.d.cts +0 -162
  52. package/dist/plugins-init.d.mts +0 -162
  53. package/dist/plugins.cjs +0 -2283
  54. package/dist/plugins.d.cts +0 -210
  55. package/dist/plugins.d.mts +0 -210
package/dist/cliHost.mjs CHANGED
@@ -1,266 +1,15 @@
1
- import { Option, Command } from 'commander';
1
+ import { z } from 'zod';
2
+ export { z } from 'zod';
2
3
  import fs from 'fs-extra';
3
4
  import { packageDirectory } from 'package-directory';
4
5
  import path, { join, extname } from 'path';
5
6
  import url, { fileURLToPath, pathToFileURL } from 'url';
6
7
  import YAML from 'yaml';
7
- import { z } from 'zod';
8
+ import { Option, Command } from 'commander';
8
9
  import { nanoid } from 'nanoid';
9
10
  import { parse } from 'dotenv';
10
11
  import { createHash } from 'crypto';
11
12
 
12
- /**
13
- * Dotenv expansion utilities.
14
- *
15
- * This module implements recursive expansion of environment-variable
16
- * references in strings and records. It supports both whitespace and
17
- * bracket syntaxes with optional defaults:
18
- *
19
- * - Whitespace: `$VAR[:default]`
20
- * - Bracketed: `${VAR[:default]}`
21
- *
22
- * Escaped dollar signs (`\$`) are preserved.
23
- * Unknown variables resolve to empty string unless a default is provided.
24
- */
25
- /**
26
- * Like String.prototype.search but returns the last index.
27
- * @internal
28
- */
29
- const searchLast = (str, rgx) => {
30
- const matches = Array.from(str.matchAll(rgx));
31
- return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
32
- };
33
- const replaceMatch = (value, match, ref) => {
34
- /**
35
- * @internal
36
- */
37
- const group = match[0];
38
- const key = match[1];
39
- const defaultValue = match[2];
40
- if (!key)
41
- return value;
42
- const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
43
- return interpolate(replacement, ref);
44
- };
45
- const interpolate = (value = '', ref = {}) => {
46
- /**
47
- * @internal
48
- */
49
- // if value is falsy, return it as is
50
- if (!value)
51
- return value;
52
- // get position of last unescaped dollar sign
53
- const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
54
- // return value if none found
55
- if (lastUnescapedDollarSignIndex === -1)
56
- return value;
57
- // evaluate the value tail
58
- const tail = value.slice(lastUnescapedDollarSignIndex);
59
- // find whitespace pattern: $KEY:DEFAULT
60
- const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
61
- const whitespaceMatch = whitespacePattern.exec(tail);
62
- if (whitespaceMatch != null)
63
- return replaceMatch(value, whitespaceMatch, ref);
64
- else {
65
- // find bracket pattern: ${KEY:DEFAULT}
66
- const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
67
- const bracketMatch = bracketPattern.exec(tail);
68
- if (bracketMatch != null)
69
- return replaceMatch(value, bracketMatch, ref);
70
- }
71
- return value;
72
- };
73
- /**
74
- * Recursively expands environment variables in a string. Variables may be
75
- * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
76
- * Unknown variables will expand to an empty string.
77
- *
78
- * @param value - The string to expand.
79
- * @param ref - The reference object to use for variable expansion.
80
- * @returns The expanded string.
81
- *
82
- * @example
83
- * ```ts
84
- * process.env.FOO = 'bar';
85
- * dotenvExpand('Hello $FOO'); // "Hello bar"
86
- * dotenvExpand('Hello $BAZ:world'); // "Hello world"
87
- * ```
88
- *
89
- * @remarks
90
- * The expansion is recursive. If a referenced variable itself contains
91
- * references, those will also be expanded until a stable value is reached.
92
- * Escaped references (e.g. `\$FOO`) are preserved as literals.
93
- */
94
- const dotenvExpand = (value, ref = process.env) => {
95
- const result = interpolate(value, ref);
96
- return result ? result.replace(/\\\$/g, '$') : undefined;
97
- };
98
- /**
99
- * Recursively expands environment variables in the values of a JSON object.
100
- * Variables may be presented with optional default as `$VAR[:default]` or
101
- * `${VAR[:default]}`. Unknown variables will expand to an empty string.
102
- *
103
- * @param values - The values object to expand.
104
- * @param options - Expansion options.
105
- * @returns The value object with expanded string values.
106
- *
107
- * @example
108
- * ```ts
109
- * process.env.FOO = 'bar';
110
- * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
111
- * // => { A: "bar", B: "xbary" }
112
- * ```
113
- *
114
- * @remarks
115
- * Options:
116
- * - ref: The reference object to use for expansion (defaults to process.env).
117
- * - progressive: Whether to progressively add expanded values to the set of
118
- * reference keys.
119
- *
120
- * When `progressive` is true, each expanded key becomes available for
121
- * subsequent expansions in the same object (left-to-right by object key order).
122
- */
123
- const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
124
- const { ref = process.env, progressive = false } = options;
125
- acc[key] = dotenvExpand(values[key], {
126
- ...ref,
127
- ...(progressive ? acc : {}),
128
- });
129
- return acc;
130
- }, {});
131
- /**
132
- * Recursively expands environment variables in a string using `process.env` as
133
- * the expansion reference. Variables may be presented with optional default as
134
- * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
135
- * empty string.
136
- *
137
- * @param value - The string to expand.
138
- * @returns The expanded string.
139
- *
140
- * @example
141
- * ```ts
142
- * process.env.FOO = 'bar';
143
- * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
144
- * ```
145
- */
146
- const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
147
-
148
- /**
149
- * Attach legacy root flags to a Commander program.
150
- * Uses provided defaults to render help labels without coupling to generators.
151
- */
152
- const attachRootOptions = (program, defaults, opts) => {
153
- // Install temporary wrappers to tag all options added here as "base".
154
- const GROUP = 'base';
155
- const tagLatest = (cmd, group) => {
156
- const optsArr = cmd.options;
157
- if (Array.isArray(optsArr) && optsArr.length > 0) {
158
- const last = optsArr[optsArr.length - 1];
159
- last.__group = group;
160
- }
161
- };
162
- const originalAddOption = program.addOption.bind(program);
163
- const originalOption = program.option.bind(program);
164
- program.addOption = function patchedAdd(opt) {
165
- // Tag before adding, in case consumers inspect the Option directly.
166
- opt.__group = GROUP;
167
- const ret = originalAddOption(opt);
168
- return ret;
169
- };
170
- program.option = function patchedOption(...args) {
171
- const ret = originalOption(...args);
172
- tagLatest(this, GROUP);
173
- return ret;
174
- };
175
- const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
176
- const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
177
- const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
178
- // Build initial chain.
179
- let p = program
180
- .enablePositionalOptions()
181
- .passThroughOptions()
182
- .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
183
- p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
184
- ['KEY1', 'VAL1'],
185
- ['KEY2', 'VAL2'],
186
- ]
187
- .map((v) => v.join(va))
188
- .join(vd)}`, dotenvExpandFromProcessEnv);
189
- // Optional legacy root command flag (kept for generated CLI compatibility).
190
- // Default is OFF; the generator opts in explicitly.
191
- if (opts?.includeCommandOption === true) {
192
- p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
193
- }
194
- p = p
195
- .option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
196
- .addOption(new Option('-s, --shell [string]', (() => {
197
- let defaultLabel = '';
198
- if (shell !== undefined) {
199
- if (typeof shell === 'boolean') {
200
- defaultLabel = ' (default OS shell)';
201
- }
202
- else if (typeof shell === 'string') {
203
- // Safe string interpolation
204
- defaultLabel = ` (default ${shell})`;
205
- }
206
- }
207
- return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
208
- })()).conflicts('shellOff'))
209
- .addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
210
- .addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
211
- .addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
212
- .addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
213
- ((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
214
- ? ' (default)'
215
- : ''}`).conflicts('excludeAllOff'))
216
- .addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
217
- .addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
218
- .addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
219
- .addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
220
- .addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
221
- .addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
222
- .addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
223
- .addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
224
- .addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
225
- .addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
226
- .addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
227
- .addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
228
- .addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
229
- .option('--capture', 'capture child process stdio for commands (tests/CI)')
230
- .option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
231
- .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
232
- .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
233
- .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
234
- .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
235
- .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
236
- .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
237
- .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
238
- .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
239
- .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
240
- .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
241
- .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
242
- // Hidden scripts pipe-through (stringified)
243
- .addOption(new Option('--scripts <string>')
244
- .default(JSON.stringify(scripts))
245
- .hideHelp());
246
- // Diagnostics: opt-in tracing; optional variadic keys after the flag.
247
- p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
248
- // Validation: strict mode fails on env validation issues (warn by default).
249
- p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
250
- // Entropy diagnostics (presentation-only)
251
- p = p
252
- .addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
253
- .addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
254
- .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
255
- .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
256
- .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
257
- .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
258
- // Restore original methods to avoid tagging future additions outside base.
259
- program.addOption = originalAddOption;
260
- program.option = originalOption;
261
- return p;
262
- };
263
-
264
13
  // Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
265
14
  const baseRootOptionDefaults = {
266
15
  dotenvToken: '.env',
@@ -309,26 +58,12 @@ const mergeInto = (target, source) => {
309
58
  }
310
59
  return target;
311
60
  };
312
- /**
313
- * Perform a deep defaults-style merge across plain objects. *
314
- * - Only merges plain objects (prototype === Object.prototype).
315
- * - Arrays and non-objects are replaced, not merged.
316
- * - `undefined` values are ignored and do not overwrite prior values.
317
- *
318
- * @typeParam T - The resulting shape after merging all layers.
319
- * @param layers - Zero or more partial layers in ascending precedence order.
320
- * @returns The merged object typed as {@link T}.
321
- *
322
- * @example
323
- * defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
324
- * =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
325
- */
326
- const defaultsDeep = (...layers) => {
61
+ function defaultsDeep(...layers) {
327
62
  const result = layers
328
63
  .filter(Boolean)
329
64
  .reduce((acc, layer) => mergeInto(acc, layer), {});
330
65
  return result;
331
- };
66
+ }
332
67
 
333
68
  /**
334
69
  * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
@@ -750,106 +485,451 @@ const validateEnvAgainstSources = (finalEnv, sources) => {
750
485
 
751
486
  const baseGetDotenvCliOptions = baseRootOptionDefaults;
752
487
 
488
+ /** src/util/omitUndefined.ts
489
+ * Helpers to drop undefined-valued properties in a typed-friendly way.
490
+ */
491
+ /**
492
+ * Omit keys whose runtime value is undefined from a shallow object.
493
+ * Returns a Partial with non-undefined value types preserved.
494
+ */
495
+ function omitUndefined(obj) {
496
+ const out = {};
497
+ for (const [k, v] of Object.entries(obj)) {
498
+ if (v !== undefined)
499
+ out[k] = v;
500
+ }
501
+ return out;
502
+ }
503
+ /**
504
+ * Specialized helper for env-like maps: drop undefined and return string-only.
505
+ */
506
+ function omitUndefinedRecord(obj) {
507
+ const out = {};
508
+ for (const [k, v] of Object.entries(obj)) {
509
+ if (v !== undefined)
510
+ out[k] = v;
511
+ }
512
+ return out;
513
+ }
514
+
753
515
  // src/GetDotenvOptions.ts
516
+ /**
517
+ * Canonical programmatic options and helpers for get-dotenv.
518
+ *
519
+ * Requirements addressed:
520
+ * - GetDotenvOptions derives from the Zod schema output (single source of truth).
521
+ * - Removed deprecated/compat flags from the public shape (e.g., useConfigLoader).
522
+ * - Provide Vars-aware defineDynamic and a typed config builder defineGetDotenvConfig\<Vars, Env\>().
523
+ * - Preserve existing behavior for defaults resolution and compat converters.
524
+ */
754
525
  const getDotenvOptionsFilename = 'getdotenv.config.json';
755
526
  /**
756
- * Converts programmatic CLI options to `getDotenv` options. *
757
- * @param cliOptions - CLI options. Defaults to `{}`.
527
+ * Converts programmatic CLI options to `getDotenv` options.
528
+ *
529
+ * Accepts "stringly" CLI inputs for vars/paths and normalizes them into
530
+ * the programmatic shape. Preserves exactOptionalPropertyTypes semantics by
531
+ * omitting keys when undefined.
532
+ */
533
+ const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern,
534
+ // drop CLI-only keys from the pass-through bag
535
+ debug: _debug, scripts: _scripts, ...rest }) => {
536
+ // Split helper for delimited strings or regex patterns
537
+ const splitBy = (value, delim, pattern) => {
538
+ if (!value)
539
+ return [];
540
+ if (pattern)
541
+ return value.split(RegExp(pattern));
542
+ if (typeof delim === 'string')
543
+ return value.split(delim);
544
+ return value.split(' ');
545
+ };
546
+ // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
547
+ let parsedVars;
548
+ if (typeof vars === 'string') {
549
+ const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern)
550
+ .map((v) => v.split(varsAssignorPattern
551
+ ? RegExp(varsAssignorPattern)
552
+ : (varsAssignor ?? '=')))
553
+ .filter(([k]) => typeof k === 'string' && k.length > 0);
554
+ parsedVars = Object.fromEntries(kvPairs);
555
+ }
556
+ else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
557
+ // Accept provided object map of string | undefined; drop undefined values
558
+ // in the normalization step below to produce a ProcessEnv-compatible bag.
559
+ parsedVars = Object.fromEntries(Object.entries(vars));
560
+ }
561
+ // Drop undefined-valued entries at the converter stage to match ProcessEnv
562
+ // expectations and the compat test assertions.
563
+ if (parsedVars) {
564
+ parsedVars = omitUndefinedRecord(parsedVars);
565
+ }
566
+ // Tolerate paths as either a delimited string or string[]
567
+ const pathsOut = Array.isArray(paths)
568
+ ? paths.filter((p) => typeof p === 'string')
569
+ : splitBy(paths, pathsDelimiter, pathsDelimiterPattern);
570
+ // Preserve exactOptionalPropertyTypes: only include keys when defined.
571
+ return {
572
+ ...rest,
573
+ ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
574
+ ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
575
+ };
576
+ };
577
+ /**
578
+ * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
579
+ *
580
+ * 1. Base defaults derived from the CLI generator defaults
581
+ * ({@link baseGetDotenvCliOptions}).
582
+ * 2. Local project overrides from a `getdotenv.config.json` in the nearest
583
+ * package root (if present).
584
+ * 3. The provided customOptions.
585
+ *
586
+ * The result preserves explicit empty values and drops only `undefined`.
587
+ */
588
+ const resolveGetDotenvOptions = async (customOptions) => {
589
+ const localPkgDir = await packageDirectory();
590
+ const localOptionsPath = localPkgDir
591
+ ? join(localPkgDir, getDotenvOptionsFilename)
592
+ : undefined;
593
+ // Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
594
+ let localOptions = {};
595
+ if (localOptionsPath && (await fs.exists(localOptionsPath))) {
596
+ try {
597
+ const txt = await fs.readFile(localOptionsPath, 'utf-8');
598
+ const parsed = JSON.parse(txt);
599
+ if (parsed && typeof parsed === 'object') {
600
+ localOptions = parsed;
601
+ }
602
+ }
603
+ catch {
604
+ // Malformed or unreadable local options are treated as absent.
605
+ localOptions = {};
606
+ }
607
+ }
608
+ // Merge order: base < local < custom (custom has highest precedence)
609
+ const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
610
+ const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
611
+ const result = defaultsDeep(defaultsFromCli, customOptions);
612
+ return {
613
+ ...result, // Keep explicit empty strings/zeros; drop only undefined
614
+ vars: omitUndefinedRecord(result.vars ?? {}),
615
+ };
616
+ };
617
+
618
+ /**
619
+ * Dotenv expansion utilities.
620
+ *
621
+ * This module implements recursive expansion of environment-variable
622
+ * references in strings and records. It supports both whitespace and
623
+ * bracket syntaxes with optional defaults:
624
+ *
625
+ * - Whitespace: `$VAR[:default]`
626
+ * - Bracketed: `${VAR[:default]}`
627
+ *
628
+ * Escaped dollar signs (`\$`) are preserved.
629
+ * Unknown variables resolve to empty string unless a default is provided.
630
+ */
631
+ /**
632
+ * Like String.prototype.search but returns the last index.
633
+ * @internal
634
+ */
635
+ const searchLast = (str, rgx) => {
636
+ const matches = Array.from(str.matchAll(rgx));
637
+ return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
638
+ };
639
+ const replaceMatch = (value, match, ref) => {
640
+ /**
641
+ * @internal
642
+ */
643
+ const group = match[0];
644
+ const key = match[1];
645
+ const defaultValue = match[2];
646
+ if (!key)
647
+ return value;
648
+ const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
649
+ return interpolate(replacement, ref);
650
+ };
651
+ const interpolate = (value = '', ref = {}) => {
652
+ /**
653
+ * @internal
654
+ */
655
+ // if value is falsy, return it as is
656
+ if (!value)
657
+ return value;
658
+ // get position of last unescaped dollar sign
659
+ const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
660
+ // return value if none found
661
+ if (lastUnescapedDollarSignIndex === -1)
662
+ return value;
663
+ // evaluate the value tail
664
+ const tail = value.slice(lastUnescapedDollarSignIndex);
665
+ // find whitespace pattern: $KEY:DEFAULT
666
+ const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
667
+ const whitespaceMatch = whitespacePattern.exec(tail);
668
+ if (whitespaceMatch != null)
669
+ return replaceMatch(value, whitespaceMatch, ref);
670
+ else {
671
+ // find bracket pattern: ${KEY:DEFAULT}
672
+ const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
673
+ const bracketMatch = bracketPattern.exec(tail);
674
+ if (bracketMatch != null)
675
+ return replaceMatch(value, bracketMatch, ref);
676
+ }
677
+ return value;
678
+ };
679
+ /**
680
+ * Recursively expands environment variables in a string. Variables may be
681
+ * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
682
+ * Unknown variables will expand to an empty string.
683
+ *
684
+ * @param value - The string to expand.
685
+ * @param ref - The reference object to use for variable expansion.
686
+ * @returns The expanded string.
687
+ *
688
+ * @example
689
+ * ```ts
690
+ * process.env.FOO = 'bar';
691
+ * dotenvExpand('Hello $FOO'); // "Hello bar"
692
+ * dotenvExpand('Hello $BAZ:world'); // "Hello world"
693
+ * ```
694
+ *
695
+ * @remarks
696
+ * The expansion is recursive. If a referenced variable itself contains
697
+ * references, those will also be expanded until a stable value is reached.
698
+ * Escaped references (e.g. `\$FOO`) are preserved as literals.
699
+ */
700
+ const dotenvExpand = (value, ref = process.env) => {
701
+ const result = interpolate(value, ref);
702
+ return result ? result.replace(/\\\$/g, '$') : undefined;
703
+ };
704
+ /**
705
+ * Recursively expands environment variables in the values of a JSON object.
706
+ * Variables may be presented with optional default as `$VAR[:default]` or
707
+ * `${VAR[:default]}`. Unknown variables will expand to an empty string.
708
+ *
709
+ * @param values - The values object to expand.
710
+ * @param options - Expansion options.
711
+ * @returns The value object with expanded string values.
712
+ *
713
+ * @example
714
+ * ```ts
715
+ * process.env.FOO = 'bar';
716
+ * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
717
+ * // => { A: "bar", B: "xbary" }
718
+ * ```
719
+ *
720
+ * @remarks
721
+ * Options:
722
+ * - ref: The reference object to use for expansion (defaults to process.env).
723
+ * - progressive: Whether to progressively add expanded values to the set of
724
+ * reference keys.
725
+ *
726
+ * When `progressive` is true, each expanded key becomes available for
727
+ * subsequent expansions in the same object (left-to-right by object key order).
728
+ */
729
+ function dotenvExpandAll(values, options = {}) {
730
+ const { ref = process.env, progressive = false, } = options;
731
+ const out = Object.keys(values).reduce((acc, key) => {
732
+ acc[key] = dotenvExpand(values[key], {
733
+ ...ref,
734
+ ...(progressive ? acc : {}),
735
+ });
736
+ return acc;
737
+ }, {});
738
+ // Key-preserving return with a permissive index signature to allow later additions.
739
+ return out;
740
+ }
741
+ /**
742
+ * Recursively expands environment variables in a string using `process.env` as
743
+ * the expansion reference. Variables may be presented with optional default as
744
+ * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
745
+ * empty string.
746
+ *
747
+ * @param value - The string to expand.
748
+ * @returns The expanded string.
758
749
  *
759
- * @returns `getDotenv` options.
750
+ * @example
751
+ * ```ts
752
+ * process.env.FOO = 'bar';
753
+ * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
754
+ * ```
760
755
  */
761
- const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
762
- /**
763
- * Convert CLI-facing string options into {@link GetDotenvOptions}.
764
- *
765
- * - 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`
766
- * pairs (configurable delimiters) into a {@link ProcessEnv}.
767
- * - Drops CLI-only keys that have no programmatic equivalent.
768
- *
769
- * @remarks
770
- * Follows exact-optional semantics by not emitting undefined-valued entries.
771
- */
772
- // Drop CLI-only keys (debug/scripts) without relying on Record casts.
773
- // Create a shallow copy then delete optional CLI-only keys if present.
774
- const restObj = { ...rest };
775
- delete restObj.debug;
776
- delete restObj.scripts;
777
- const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
778
- // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
779
- let parsedVars;
780
- if (typeof vars === 'string') {
781
- const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
782
- ? RegExp(varsAssignorPattern)
783
- : (varsAssignor ?? '=')));
784
- parsedVars = Object.fromEntries(kvPairs);
785
- }
786
- else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
787
- // Keep only string or undefined values to match ProcessEnv.
788
- const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
789
- parsedVars = Object.fromEntries(entries);
790
- }
791
- // Drop undefined-valued entries at the converter stage to match ProcessEnv
792
- // expectations and the compat test assertions.
793
- if (parsedVars) {
794
- parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
795
- }
796
- // Tolerate paths as either a delimited string or string[]
797
- // Use a locally cast union type to avoid lint warnings about always-falsy conditions
798
- // under the RootOptionsShape (which declares paths as string | undefined).
799
- const pathsAny = paths;
800
- const pathsOut = Array.isArray(pathsAny)
801
- ? pathsAny.filter((p) => typeof p === 'string')
802
- : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
803
- // Preserve exactOptionalPropertyTypes: only include keys when defined.
804
- return {
805
- ...restObj,
806
- ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
807
- ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
756
+ const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
757
+
758
+ /* eslint-disable @typescript-eslint/no-deprecated */
759
+ /**
760
+ * Attach root flags to a GetDotenvCli instance.
761
+ * - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
762
+ * - Any flag that displays an effective default in help uses dynamic descriptions.
763
+ */
764
+ const attachRootOptions = (program, defaults) => {
765
+ // Install temporary wrappers to tag all options added here as "base" for grouped help.
766
+ const GROUP = 'base';
767
+ const tagLatest = (cmd, group) => {
768
+ const optsArr = cmd.options ?? [];
769
+ if (Array.isArray(optsArr) && optsArr.length > 0) {
770
+ const last = optsArr[optsArr.length - 1];
771
+ program.setOptionGroup(last, group);
772
+ }
808
773
  };
809
- };
810
- const resolveGetDotenvOptions = async (customOptions) => {
811
- /**
812
- * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
813
- *
814
- * 1. Base defaults derived from the CLI generator defaults
815
- * ({@link baseGetDotenvCliOptions}).
816
- * 2. Local project overrides from a `getdotenv.config.json` in the nearest
817
- * package root (if present).
818
- * 3. The provided {@link customOptions}.
819
- *
820
- * The result preserves explicit empty values and drops only `undefined`.
821
- *
822
- * @returns Fully-resolved {@link GetDotenvOptions}.
823
- *
824
- * @example
825
- * ```ts
826
- * const options = await resolveGetDotenvOptions({ env: 'dev' });
827
- * ```
828
- */
829
- const localPkgDir = await packageDirectory();
830
- const localOptionsPath = localPkgDir
831
- ? join(localPkgDir, getDotenvOptionsFilename)
832
- : undefined;
833
- const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
834
- ? JSON.parse((await fs.readFile(localOptionsPath)).toString())
835
- : {});
836
- // Merge order: base < local < custom (custom has highest precedence)
837
- const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
838
- const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
839
- const result = defaultsDeep(defaultsFromCli, customOptions);
840
- return {
841
- ...result, // Keep explicit empty strings/zeros; drop only undefined
842
- vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
774
+ const originalAddOption = program.addOption.bind(program);
775
+ const originalOption = program.option.bind(program);
776
+ program.addOption = function patchedAdd(opt) {
777
+ program.setOptionGroup(opt, GROUP);
778
+ return originalAddOption(opt);
779
+ };
780
+ program.option = function patchedOption(...args) {
781
+ const ret = originalOption(...args);
782
+ tagLatest(this, GROUP);
783
+ return ret;
843
784
  };
785
+ const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
786
+ const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
787
+ const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
788
+ // Helper: append (default) tags for ON/OFF toggles
789
+ const onOff = (on, isDefault) => on
790
+ ? `ON${isDefault ? ' (default)' : ''}`
791
+ : `OFF${isDefault ? ' (default)' : ''}`;
792
+ let p = program
793
+ .enablePositionalOptions()
794
+ .passThroughOptions()
795
+ .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
796
+ p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
797
+ ['KEY1', 'VAL1'],
798
+ ['KEY2', 'VAL2'],
799
+ ]
800
+ .map((v) => v.join(va))
801
+ .join(vd)}`, dotenvExpandFromProcessEnv);
802
+ // Output path (interpolated later; help can remain static)
803
+ p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
804
+ // Shell ON (string or boolean true => default shell)
805
+ p = p
806
+ .addOption(program
807
+ .createDynamicOption('-s, --shell [string]', (cfg) => {
808
+ const s = cfg.shell;
809
+ let tag = '';
810
+ if (typeof s === 'boolean' && s)
811
+ tag = ' (default OS shell)';
812
+ else if (typeof s === 'string' && s.length > 0)
813
+ tag = ` (default ${s})`;
814
+ return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
815
+ })
816
+ .conflicts('shellOff'))
817
+ // Shell OFF
818
+ .addOption(program
819
+ .createDynamicOption('-S, --shell-off', (cfg) => {
820
+ const s = cfg.shell;
821
+ return `command execution shell OFF${s === false ? ' (default)' : ''}`;
822
+ })
823
+ .conflicts('shell'));
824
+ // Load process ON/OFF (dynamic defaults)
825
+ p = p
826
+ .addOption(program
827
+ .createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
828
+ .conflicts('loadProcessOff'))
829
+ .addOption(program
830
+ .createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
831
+ .conflicts('loadProcess'));
832
+ // Exclusion master toggle (dynamic)
833
+ p = p
834
+ .addOption(program
835
+ .createDynamicOption('-a, --exclude-all', (cfg) => {
836
+ const allOn = !!cfg.excludeDynamic &&
837
+ ((!!cfg.excludeEnv && !!cfg.excludeGlobal) ||
838
+ (!!cfg.excludePrivate && !!cfg.excludePublic));
839
+ const suffix = allOn ? ' (default)' : '';
840
+ return `exclude all dotenv variables from loading ON${suffix}`;
841
+ })
842
+ .conflicts('excludeAllOff'))
843
+ .addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
844
+ // Per-family exclusions (dynamic defaults)
845
+ p = p
846
+ .addOption(program
847
+ .createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
848
+ .conflicts('excludeDynamicOff'))
849
+ .addOption(program
850
+ .createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
851
+ .conflicts('excludeDynamic'))
852
+ .addOption(program
853
+ .createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
854
+ .conflicts('excludeEnvOff'))
855
+ .addOption(program
856
+ .createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
857
+ .conflicts('excludeEnv'))
858
+ .addOption(program
859
+ .createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
860
+ .conflicts('excludeGlobalOff'))
861
+ .addOption(program
862
+ .createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
863
+ .conflicts('excludeGlobal'))
864
+ .addOption(program
865
+ .createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
866
+ .conflicts('excludePrivateOff'))
867
+ .addOption(program
868
+ .createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
869
+ .conflicts('excludePrivate'))
870
+ .addOption(program
871
+ .createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
872
+ .conflicts('excludePublicOff'))
873
+ .addOption(program
874
+ .createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
875
+ .conflicts('excludePublic'));
876
+ // Log ON/OFF (dynamic)
877
+ p = p
878
+ .addOption(program
879
+ .createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
880
+ .conflicts('logOff'))
881
+ .addOption(program
882
+ .createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
883
+ .conflicts('log'));
884
+ // Capture flag (no default display; static)
885
+ p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
886
+ // Core bootstrap/static flags (kept static in help)
887
+ p = p
888
+ .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
889
+ .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
890
+ .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
891
+ .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
892
+ .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
893
+ .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
894
+ .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
895
+ .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
896
+ .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
897
+ .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
898
+ .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
899
+ // Hidden scripts pipe-through (stringified)
900
+ .addOption(new Option('--scripts <string>')
901
+ .default(JSON.stringify(scripts))
902
+ .hideHelp());
903
+ // Diagnostics / validation / entropy
904
+ p = p
905
+ .option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
906
+ .option('--strict', 'fail on env validation errors (schema/requiredKeys)');
907
+ p = p
908
+ .addOption(program
909
+ .createDynamicOption('--entropy-warn', (cfg) => {
910
+ const warn = cfg.warnEntropy;
911
+ // Default is effectively ON when warnEntropy is true or undefined.
912
+ return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
913
+ })
914
+ .conflicts('entropyWarnOff'))
915
+ .addOption(program
916
+ .createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
917
+ .conflicts('entropyWarn'))
918
+ .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
919
+ .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
920
+ .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
921
+ .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
922
+ // Restore original methods
923
+ program.addOption = originalAddOption;
924
+ program.option = originalOption;
925
+ return p;
844
926
  };
845
927
 
846
928
  /**
847
929
  * Zod schemas for programmatic GetDotenv options.
848
930
  *
849
- * NOTE: These schemas are introduced without wiring to avoid behavior changes.
850
- * Legacy paths continue to use existing types/logic. The new plugin host will
851
- * use these schemas in strict mode; legacy paths will adopt them in warn mode
852
- * later per the staged plan.
931
+ * Canonical source of truth for options shape. Public types are derived
932
+ * from these schemas (see consumers via z.output\<\>).
853
933
  */
854
934
  // Minimal process env representation: string values or undefined to indicate "unset".
855
935
  const processEnvSchema = z.record(z.string(), z.string().optional());
@@ -868,12 +948,11 @@ const getDotenvOptionsSchemaRaw = z.object({
868
948
  excludePublic: z.boolean().optional(),
869
949
  loadProcess: z.boolean().optional(),
870
950
  log: z.boolean().optional(),
951
+ logger: z.unknown().optional(),
871
952
  outputPath: z.string().optional(),
872
953
  paths: z.array(z.string()).optional(),
873
954
  privateToken: z.string().optional(),
874
955
  vars: processEnvSchema.optional(),
875
- // Host-only feature flag: guarded integration of config loader/overlay
876
- useConfigLoader: z.boolean().optional(),
877
956
  });
878
957
  // RESOLVED: service-boundary contract (post-inheritance).
879
958
  // For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
@@ -893,16 +972,7 @@ const applyConfigSlice = (current, cfg, env) => {
893
972
  const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
894
973
  return applyKv(afterGlobal, envKv);
895
974
  };
896
- /**
897
- * Overlay config-provided values onto a base ProcessEnv using precedence axes:
898
- * - kind: env \> global
899
- * - privacy: local \> public
900
- * - source: project \> packaged \> base
901
- *
902
- * Programmatic explicit vars (if provided) override all config slices.
903
- * Progressive expansion is applied within each slice.
904
- */
905
- const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
975
+ function overlayEnv({ base, env, configs, programmaticVars, }) {
906
976
  let current = { ...base };
907
977
  // Source: packaged (public -> local)
908
978
  current = applyConfigSlice(current, configs.packaged, env);
@@ -917,7 +987,7 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
917
987
  current = applyKv(current, toApply);
918
988
  }
919
989
  return current;
920
- };
990
+ }
921
991
 
922
992
  /** src/diagnostics/entropy.ts
923
993
  * Entropy diagnostics (presentation-only).
@@ -927,7 +997,7 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
927
997
  */
928
998
  const warned = new Set();
929
999
  const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
930
- const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
1000
+ const compile$1 = (patterns) => (patterns ?? []).map((p) => (typeof p === 'string' ? new RegExp(p, 'i') : p));
931
1001
  const whitelisted = (key, regs) => regs.some((re) => re.test(key));
932
1002
  const shannonBitsPerChar = (s) => {
933
1003
  const freq = new Map();
@@ -974,7 +1044,7 @@ const DEFAULT_PATTERNS = [
974
1044
  '\\bapi[_-]?key\\b',
975
1045
  '\\bkey\\b',
976
1046
  ];
977
- const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
1047
+ const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
978
1048
  const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
979
1049
  const MASK = '[redacted]';
980
1050
  /**
@@ -1117,43 +1187,7 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
1117
1187
  }
1118
1188
  };
1119
1189
 
1120
- /**
1121
- * Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
1122
- *
1123
- * @param options - `GetDotenvOptions` object
1124
- * @returns The combined parsed dotenv object.
1125
- * * @example Load from the project root with default tokens
1126
- * ```ts
1127
- * const vars = await getDotenv();
1128
- * console.log(vars.MY_SETTING);
1129
- * ```
1130
- *
1131
- * @example Load from multiple paths and a specific environment
1132
- * ```ts
1133
- * const vars = await getDotenv({
1134
- * env: 'dev',
1135
- * dotenvToken: '.testenv',
1136
- * privateToken: 'secret',
1137
- * paths: ['./', './packages/app'],
1138
- * });
1139
- * ```
1140
- *
1141
- * @example Use dynamic variables
1142
- * ```ts
1143
- * // .env.js default-exports: { DYNAMIC: ({ PREV }) => `${PREV}-suffix` }
1144
- * const vars = await getDotenv({ dynamicPath: '.env.js' });
1145
- * ```
1146
- *
1147
- * @remarks
1148
- * - When {@link GetDotenvOptions.loadProcess} is true, the resulting variables are merged
1149
- * into `process.env` as a side effect.
1150
- * - When {@link GetDotenvOptions.outputPath} is provided, a consolidated dotenv file is written.
1151
- * The path is resolved after expansion, so it may reference previously loaded vars.
1152
- *
1153
- * @throws Error when a dynamic module is present but cannot be imported.
1154
- * @throws Error when an output path was requested but could not be resolved.
1155
- */
1156
- const getDotenv = async (options = {}) => {
1190
+ async function getDotenv(options = {}) {
1157
1191
  // Apply defaults.
1158
1192
  const { defaultEnv, dotenvToken = '.env', dynamicPath, env, excludeDynamic = false, excludeEnv = false, excludeGlobal = false, excludePrivate = false, excludePublic = false, loadProcess = false, log = false, logger = console, outputPath, paths = [], privateToken = 'local', vars = {}, } = await resolveGetDotenvOptions(options);
1159
1193
  // Read .env files.
@@ -1258,8 +1292,7 @@ const getDotenv = async (options = {}) => {
1258
1292
  .entropyThreshold;
1259
1293
  const entropyMinLengthVal = options
1260
1294
  .entropyMinLength;
1261
- const entropyWhitelistVal = options
1262
- .entropyWhitelist;
1295
+ const entropyWhitelistVal = options.entropyWhitelist;
1263
1296
  const entOpts = {};
1264
1297
  if (typeof warnEntropyVal === 'boolean')
1265
1298
  entOpts.warnEntropy = warnEntropyVal;
@@ -1279,7 +1312,7 @@ const getDotenv = async (options = {}) => {
1279
1312
  if (loadProcess)
1280
1313
  Object.assign(process.env, resultDotenv);
1281
1314
  return resultDotenv;
1282
- };
1315
+ }
1283
1316
 
1284
1317
  /**
1285
1318
  * Deep interpolation utility for string leaves.
@@ -1337,6 +1370,18 @@ const interpolateDeep = (value, envRef) => {
1337
1370
  return value;
1338
1371
  };
1339
1372
 
1373
+ /**
1374
+ * Instance-bound plugin config store.
1375
+ * Host stores the validated/interpolated slice per plugin instance.
1376
+ * The store is intentionally private to this module; definePlugin()
1377
+ * provides a typed accessor that reads from this store for the calling
1378
+ * plugin instance.
1379
+ */
1380
+ const PLUGIN_CONFIG_STORE = new WeakMap();
1381
+ const _setPluginConfigForInstance = (plugin, cfg) => {
1382
+ PLUGIN_CONFIG_STORE.set(plugin, cfg);
1383
+ };
1384
+ const _getPluginConfigForInstance = (plugin) => PLUGIN_CONFIG_STORE.get(plugin);
1340
1385
  /**
1341
1386
  * Compute the dotenv context for the host (uses the config loader/overlay path).
1342
1387
  * - Resolves and validates options strictly (host-only).
@@ -1345,20 +1390,26 @@ const interpolateDeep = (value, envRef) => {
1345
1390
  *
1346
1391
  * @param customOptions - Partial options from the current invocation.
1347
1392
  * @param plugins - Installed plugins (for config validation).
1348
- * @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery). */
1393
+ * @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery).
1394
+ */
1349
1395
  const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1350
1396
  const optionsResolved = await resolveGetDotenvOptions(customOptions);
1397
+ // Zod boundary: parse returns the schema-derived shape; we adopt our public
1398
+ // GetDotenvOptions overlay (logger/dynamic typing) for internal processing.
1351
1399
  const validated = getDotenvOptionsSchemaResolved.parse(optionsResolved);
1352
1400
  // Always-on loader path
1353
1401
  // 1) Base from files only (no dynamic, no programmatic vars)
1402
+ // Sanitize to avoid passing properties explicitly set to undefined.
1403
+ const cleanedValidated = omitUndefined(validated);
1354
1404
  const base = await getDotenv({
1355
- ...validated,
1405
+ ...cleanedValidated,
1356
1406
  // Build a pure base without side effects or logging.
1357
1407
  excludeDynamic: true,
1358
1408
  vars: {},
1359
1409
  log: false,
1360
1410
  loadProcess: false,
1361
- outputPath: undefined,
1411
+ // Intentionally omit outputPath for the base pass; including a key with
1412
+ // undefined would violate exactOptionalPropertyTypes on the Partial target.
1362
1413
  });
1363
1414
  // 2) Discover config sources and overlay
1364
1415
  const sources = await resolveGetDotenvConfigSources(hostMetaUrl);
@@ -1403,7 +1454,7 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1403
1454
  return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
1404
1455
  }, ''), { encoding: 'utf-8' });
1405
1456
  }
1406
- const logger = validated.logger ?? console;
1457
+ const logger = customOptions.logger ?? console;
1407
1458
  if (validated.log)
1408
1459
  logger.log(dotenv);
1409
1460
  if (validated.loadProcess)
@@ -1418,43 +1469,148 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1418
1469
  const localPlugins = (sources.project?.local &&
1419
1470
  sources.project.local.plugins) ??
1420
1471
  {};
1421
- const mergedPluginConfigs = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
1472
+ // The by-id map is retained only for backwards-compat rendering paths
1473
+ // (root help dynamic evaluation). Instance-bound access is the source
1474
+ // of truth going forward and is populated below.
1475
+ const mergedPluginConfigsById = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
1422
1476
  for (const p of plugins) {
1423
1477
  if (!p.id)
1424
1478
  continue;
1425
- const slice = mergedPluginConfigs[p.id];
1426
- if (slice === undefined)
1427
- continue;
1428
- // Per-plugin interpolation just before validation/afterResolve:
1429
- // precedence: process.env wins over ctx.dotenv for slice defaults.
1479
+ const slice = mergedPluginConfigsById[p.id];
1480
+ // Build interpolation reference once per plugin:
1430
1481
  const envRef = {
1431
1482
  ...dotenv,
1432
1483
  ...process.env,
1433
1484
  };
1434
- const interpolated = interpolateDeep(slice, envRef);
1435
- // Validate if a schema is provided; otherwise accept interpolated slice as-is.
1436
- if (p.configSchema) {
1437
- const parsed = p.configSchema.safeParse(interpolated);
1438
- if (!parsed.success) {
1439
- const msgs = parsed.error.issues
1440
- .map((i) => `${i.path.join('.')}: ${i.message}`)
1441
- .join('\n');
1442
- throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
1443
- }
1444
- mergedPluginConfigs[p.id] = parsed.data;
1445
- }
1446
- else {
1447
- mergedPluginConfigs[p.id] = interpolated;
1485
+ const interpolated = slice && typeof slice === 'object'
1486
+ ? interpolateDeep(slice, envRef)
1487
+ : {};
1488
+ // Enforced: plugins always carry a schema (strict empty by default).
1489
+ // Zod v4: avoid legacy multi-generic usage; treat as generic ZodObject.
1490
+ const schema = p.configSchema;
1491
+ const toParse = interpolated;
1492
+ const parsed = schema.safeParse(toParse);
1493
+ if (!parsed.success) {
1494
+ const err = parsed.error;
1495
+ const msgs = err.issues
1496
+ .map((i) => {
1497
+ const path = Array.isArray(i.path) ? i.path.join('.') : '';
1498
+ const msg = typeof i.message === 'string' ? i.message : 'Invalid value';
1499
+ return path ? `${path}: ${msg}` : msg;
1500
+ })
1501
+ .join('\n');
1502
+ throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
1448
1503
  }
1504
+ // Store a readonly (shallow-frozen) value for runtime safety.
1505
+ const frozen = Object.freeze(parsed.data);
1506
+ _setPluginConfigForInstance(p, frozen);
1507
+ mergedPluginConfigsById[p.id] = frozen;
1449
1508
  }
1450
1509
  return {
1451
1510
  optionsResolved: validated,
1452
1511
  dotenv: dotenv,
1453
1512
  plugins: {},
1454
- pluginConfigs: mergedPluginConfigs,
1513
+ // Retained for legacy root help dynamic evaluation only. Instance-bound
1514
+ // access is used by plugins themselves and tests/docs moving forward.
1515
+ pluginConfigs: mergedPluginConfigsById,
1455
1516
  };
1456
1517
  };
1457
1518
 
1519
+ // Registry for dynamic descriptions keyed by Option (WeakMap so GC-friendly)
1520
+ const DYN_DESC = new WeakMap();
1521
+ /**
1522
+ * Create an Option with a dynamic description callback stored in DYN_DESC.
1523
+ */
1524
+ function makeDynamicOption(flags, desc, parser, defaultValue) {
1525
+ const opt = new Option(flags, '');
1526
+ DYN_DESC.set(opt, desc);
1527
+ if (parser) {
1528
+ opt.argParser((value, previous) => parser(value, previous));
1529
+ }
1530
+ if (defaultValue !== undefined)
1531
+ opt.default(defaultValue);
1532
+ return opt;
1533
+ }
1534
+ /**
1535
+ * Evaluate dynamic descriptions across a command tree using the resolved config.
1536
+ */
1537
+ function evaluateDynamicOptions(root, resolved) {
1538
+ const visit = (cmd) => {
1539
+ const arr = cmd.options;
1540
+ for (const o of arr) {
1541
+ const dyn = DYN_DESC.get(o);
1542
+ if (typeof dyn === 'function') {
1543
+ try {
1544
+ const txt = dyn(resolved);
1545
+ // Commander uses Option.description during help rendering.
1546
+ o.description = txt;
1547
+ }
1548
+ catch {
1549
+ /* best-effort; leave as-is */
1550
+ }
1551
+ }
1552
+ }
1553
+ for (const c of cmd.commands)
1554
+ visit(c);
1555
+ };
1556
+ visit(root);
1557
+ }
1558
+
1559
+ // Registry for grouping; root help renders groups between Options and Commands.
1560
+ const GROUP_TAG = new WeakMap();
1561
+ function renderOptionGroups(cmd) {
1562
+ const all = cmd.options;
1563
+ const byGroup = new Map();
1564
+ for (const o of all) {
1565
+ const opt = o;
1566
+ const g = GROUP_TAG.get(opt);
1567
+ if (!g || g === 'base')
1568
+ continue; // base handled by default help
1569
+ const rows = byGroup.get(g) ?? [];
1570
+ rows.push({
1571
+ flags: opt.flags,
1572
+ description: opt.description ?? '',
1573
+ });
1574
+ byGroup.set(g, rows);
1575
+ }
1576
+ if (byGroup.size === 0)
1577
+ return '';
1578
+ const renderRows = (title, rows) => {
1579
+ const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
1580
+ // Sort within group: short-aliased flags first
1581
+ rows.sort((a, b) => {
1582
+ const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
1583
+ const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
1584
+ return bS - aS || a.flags.localeCompare(b.flags);
1585
+ });
1586
+ const lines = rows
1587
+ .map((r) => {
1588
+ const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
1589
+ return ` ${r.flags}${pad}${r.description}`.trimEnd();
1590
+ })
1591
+ .join('\n');
1592
+ return `\n${title}:\n${lines}\n`;
1593
+ };
1594
+ let out = '';
1595
+ // App options (if any)
1596
+ const app = byGroup.get('app');
1597
+ if (app && app.length > 0) {
1598
+ out += renderRows('App options', app);
1599
+ }
1600
+ // Plugin groups sorted by id; suppress self group on the owning command name.
1601
+ const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
1602
+ const currentName = cmd.name();
1603
+ pluginKeys.sort((a, b) => a.localeCompare(b));
1604
+ for (const k of pluginKeys) {
1605
+ const id = k.slice('plugin:'.length) || '(unknown)';
1606
+ const rows = byGroup.get(k) ?? [];
1607
+ if (rows.length > 0 && id !== currentName) {
1608
+ out += renderRows(`Plugin options — ${id}`, rows);
1609
+ }
1610
+ }
1611
+ return out;
1612
+ }
1613
+
1458
1614
  const HOST_META_URL = import.meta.url;
1459
1615
  const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
1460
1616
  const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
@@ -1467,8 +1623,6 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
1467
1623
  * - Expose a stable accessor for the current context (getCtx).
1468
1624
  * - Provide a namespacing helper (ns).
1469
1625
  * - Support composable plugins with parent → children install and afterResolve.
1470
- *
1471
- * NOTE: This host is additive and does not alter the legacy CLI.
1472
1626
  */
1473
1627
  let GetDotenvCli$1 = class GetDotenvCli extends Command {
1474
1628
  /** Registered top-level plugins (composition happens via .use()) */
@@ -1477,6 +1631,17 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1477
1631
  _installed = false;
1478
1632
  /** Optional header line to prepend in help output */
1479
1633
  [HELP_HEADER_SYMBOL];
1634
+ /** Context/options stored under symbols (typed) */
1635
+ [CTX_SYMBOL];
1636
+ [OPTS_SYMBOL];
1637
+ /**
1638
+ * Create a subcommand using the same subclass, preserving helpers like
1639
+ * dynamicOption on children.
1640
+ */
1641
+ createCommand(name) {
1642
+ // Explicitly construct a GetDotenvCli (drop subclass constructor semantics).
1643
+ return new GetDotenvCli(name);
1644
+ }
1480
1645
  constructor(alias = 'getdotenv') {
1481
1646
  super(alias);
1482
1647
  // Ensure subcommands that use passThroughOptions can be attached safely.
@@ -1484,37 +1649,39 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1484
1649
  // child uses passThroughOptions.
1485
1650
  this.enablePositionalOptions();
1486
1651
  // Configure grouped help: show only base options in default "Options";
1487
- // append App/Plugin sections after default help.
1652
+ // we will insert App/Plugin sections before Commands in helpInformation().
1488
1653
  this.configureHelp({
1489
1654
  visibleOptions: (cmd) => {
1490
- const all = cmd.options ??
1491
- [];
1492
- const base = all.filter((opt) => {
1493
- const group = opt.__group;
1494
- return group === 'base';
1495
- });
1655
+ const all = cmd.options;
1656
+ const isRoot = cmd.parent === null;
1657
+ const list = isRoot
1658
+ ? all.filter((opt) => {
1659
+ const group = GROUP_TAG.get(opt);
1660
+ return group === 'base';
1661
+ })
1662
+ : all.slice(); // subcommands: show all options (their own "Options:" block)
1496
1663
  // Sort: short-aliased options first, then long-only; stable by flags.
1497
1664
  const hasShort = (opt) => {
1498
- const flags = opt.flags ?? '';
1665
+ const flags = opt.flags;
1499
1666
  // Matches "-x," or starting "-x " before any long
1500
1667
  return /(^|\s|,)-[A-Za-z]/.test(flags);
1501
1668
  };
1502
- const byFlags = (opt) => opt.flags ?? '';
1503
- base.sort((a, b) => {
1669
+ const byFlags = (opt) => opt.flags;
1670
+ list.sort((a, b) => {
1504
1671
  const aS = hasShort(a) ? 1 : 0;
1505
1672
  const bS = hasShort(b) ? 1 : 0;
1506
1673
  return bS - aS || byFlags(a).localeCompare(byFlags(b));
1507
1674
  });
1508
- return base;
1675
+ return list;
1509
1676
  },
1510
1677
  });
1511
1678
  this.addHelpText('beforeAll', () => {
1512
1679
  const header = this[HELP_HEADER_SYMBOL];
1513
1680
  return header && header.length > 0 ? `${header}\n\n` : '';
1514
1681
  });
1515
- this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
1516
1682
  // Skeleton preSubcommand hook: produce a context if absent, without
1517
- // mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
1683
+ // mutating process.env. The passOptions hook (when installed) will
1684
+ // compute the final context using merged CLI options; keeping
1518
1685
  // loadProcess=false here avoids leaking dotenv values into the parent
1519
1686
  // process env before subcommands execute.
1520
1687
  this.hook('preSubcommand', async () => {
@@ -1524,22 +1691,49 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1524
1691
  });
1525
1692
  }
1526
1693
  /**
1527
- * Resolve options (strict) and compute dotenv context. * Stores the context on the instance under a symbol.
1694
+ * Resolve options (strict) and compute dotenv context.
1695
+ * Stores the context on the instance under a symbol.
1696
+ *
1697
+ * Options:
1698
+ * - opts.runAfterResolve (default true): when false, skips running plugin
1699
+ * afterResolve hooks. Useful for top-level help rendering to avoid
1700
+ * long-running side-effects while still evaluating dynamic help text.
1528
1701
  */
1529
- async resolveAndLoad(customOptions = {}) {
1702
+ async resolveAndLoad(customOptions = {}, opts) {
1530
1703
  // Resolve defaults, then validate strictly under the new host.
1531
1704
  const optionsResolved = await resolveGetDotenvOptions(customOptions);
1532
1705
  getDotenvOptionsSchemaResolved.parse(optionsResolved);
1533
1706
  // Delegate the heavy lifting to the shared helper (guarded path supported).
1534
1707
  const ctx = await computeContext(optionsResolved, this._plugins, HOST_META_URL);
1535
1708
  // Persist context on the instance for later access.
1536
- this[CTX_SYMBOL] =
1537
- ctx;
1709
+ this[CTX_SYMBOL] = ctx;
1538
1710
  // Ensure plugins are installed exactly once, then run afterResolve.
1539
1711
  await this.install();
1540
- await this._runAfterResolve(ctx);
1712
+ if (opts?.runAfterResolve ?? true) {
1713
+ await this._runAfterResolve(ctx);
1714
+ }
1541
1715
  return ctx;
1542
1716
  }
1717
+ // Implementation
1718
+ createDynamicOption(flags, desc, parser, defaultValue) {
1719
+ return makeDynamicOption(flags, (c) => desc(c), parser, defaultValue);
1720
+ }
1721
+ /**
1722
+ * Chainable helper mirroring .option(), but with a dynamic description.
1723
+ * Equivalent to addOption(createDynamicOption(...)).
1724
+ */
1725
+ dynamicOption(flags, desc, parser, defaultValue) {
1726
+ this.addOption(this.createDynamicOption(flags, desc, parser, defaultValue));
1727
+ return this;
1728
+ }
1729
+ /**
1730
+ * Evaluate dynamic descriptions for this command and all descendants using
1731
+ * the provided resolved configuration. Mutates the Option.description in
1732
+ * place so Commander help renders updated text.
1733
+ */
1734
+ evaluateDynamicOptions(resolved) {
1735
+ evaluateDynamicOptions(this, resolved);
1736
+ }
1543
1737
  /**
1544
1738
  * Retrieve the current invocation context (if any).
1545
1739
  */
@@ -1557,9 +1751,13 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1557
1751
  _setOptionsBag(bag) {
1558
1752
  this[OPTS_SYMBOL] = bag;
1559
1753
  }
1560
- /** * Convenience helper to create a namespaced subcommand.
1561
- */
1754
+ /** Convenience helper to create a namespaced subcommand. */
1562
1755
  ns(name) {
1756
+ // Guard against same-level duplicate command names for clearer diagnostics.
1757
+ const exists = this.commands.some((c) => c.name() === name);
1758
+ if (exists) {
1759
+ throw new Error(`Duplicate command name: ${name}`);
1760
+ }
1563
1761
  return this.command(name);
1564
1762
  }
1565
1763
  /**
@@ -1569,29 +1767,15 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1569
1767
  tagAppOptions(fn) {
1570
1768
  const root = this;
1571
1769
  const originalAddOption = root.addOption.bind(root);
1572
- const originalOption = root.option.bind(root);
1573
- const tagLatest = (cmd, group) => {
1574
- const optsArr = cmd.options;
1575
- if (Array.isArray(optsArr) && optsArr.length > 0) {
1576
- const last = optsArr[optsArr.length - 1];
1577
- last.__group = group;
1578
- }
1579
- };
1580
1770
  root.addOption = function patchedAdd(opt) {
1581
- opt.__group = 'app';
1771
+ root.setOptionGroup(opt, 'app');
1582
1772
  return originalAddOption(opt);
1583
1773
  };
1584
- root.option = function patchedOption(...args) {
1585
- const ret = originalOption(...args);
1586
- tagLatest(this, 'app');
1587
- return ret;
1588
- };
1589
1774
  try {
1590
1775
  return fn(root);
1591
1776
  }
1592
1777
  finally {
1593
1778
  root.addOption = originalAddOption;
1594
- root.option = originalOption;
1595
1779
  }
1596
1780
  }
1597
1781
  /**
@@ -1630,12 +1814,51 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1630
1814
  this[HELP_HEADER_SYMBOL] = helpHeader;
1631
1815
  }
1632
1816
  else if (v) {
1633
- // Use the current command name (possibly overridden by 'name' above).
1634
1817
  const header = `${this.name()} v${v}`;
1635
1818
  this[HELP_HEADER_SYMBOL] = header;
1636
1819
  }
1637
1820
  return this;
1638
1821
  }
1822
+ /**
1823
+ * Insert grouped plugin/app options between "Options" and "Commands" for
1824
+ * hybrid ordering. Applies to root and any parent command.
1825
+ */
1826
+ helpInformation() {
1827
+ // Base help text first (includes beforeAll/after hooks).
1828
+ const base = super.helpInformation();
1829
+ const groups = renderOptionGroups(this);
1830
+ const block = typeof groups === 'string' ? groups.trim() : '';
1831
+ let out = base;
1832
+ if (!block) {
1833
+ // Ensure a trailing blank line even when no extra groups render.
1834
+ if (!out.endsWith('\n\n'))
1835
+ out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1836
+ return out;
1837
+ }
1838
+ // Insert just before "Commands:" when present.
1839
+ const marker = '\nCommands:';
1840
+ const idx = base.indexOf(marker);
1841
+ if (idx >= 0) {
1842
+ const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
1843
+ out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
1844
+ }
1845
+ else {
1846
+ // Otherwise append.
1847
+ const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
1848
+ out = `${base}${sep}${groups}`;
1849
+ }
1850
+ // Ensure a trailing blank line for prompt separation.
1851
+ if (!out.endsWith('\n\n')) {
1852
+ out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1853
+ }
1854
+ return out;
1855
+ }
1856
+ /**
1857
+ * Public: tag an Option with a display group for help (root/app/plugin:<id>).
1858
+ */
1859
+ setOptionGroup(opt, group) {
1860
+ GROUP_TAG.set(opt, group);
1861
+ }
1639
1862
  /**
1640
1863
  * Register a plugin for installation (parent level).
1641
1864
  * Installation occurs on first resolveAndLoad() (or explicit install()).
@@ -1674,58 +1897,18 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1674
1897
  for (const p of this._plugins)
1675
1898
  await run(p);
1676
1899
  }
1677
- // Render App/Plugin grouped options appended after default help.
1678
- #renderOptionGroups(cmd) {
1679
- const all = cmd.options ?? [];
1680
- const byGroup = new Map();
1681
- for (const o of all) {
1682
- const opt = o;
1683
- const g = opt.__group;
1684
- if (!g || g === 'base')
1685
- continue; // base handled by default help
1686
- const rows = byGroup.get(g) ?? [];
1687
- rows.push({
1688
- flags: opt.flags ?? '',
1689
- description: opt.description ?? '',
1690
- });
1691
- byGroup.set(g, rows);
1692
- }
1693
- if (byGroup.size === 0)
1694
- return '';
1695
- const renderRows = (title, rows) => {
1696
- const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
1697
- // Sort within group: short-aliased flags first
1698
- rows.sort((a, b) => {
1699
- const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
1700
- const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
1701
- return bS - aS || a.flags.localeCompare(b.flags);
1702
- });
1703
- const lines = rows
1704
- .map((r) => {
1705
- const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
1706
- return ` ${r.flags}${pad}${r.description}`.trimEnd();
1707
- })
1708
- .join('\n');
1709
- return `\n${title}:\n${lines}\n`;
1710
- };
1711
- let out = '';
1712
- // App options (if any)
1713
- const app = byGroup.get('app');
1714
- if (app && app.length > 0) {
1715
- out += renderRows('App options', app);
1716
- }
1717
- // Plugin groups sorted by id
1718
- const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
1719
- pluginKeys.sort((a, b) => a.localeCompare(b));
1720
- for (const k of pluginKeys) {
1721
- const id = k.slice('plugin:'.length) || '(unknown)';
1722
- const rows = byGroup.get(k) ?? [];
1723
- if (rows.length > 0) {
1724
- out += renderRows(`Plugin options — ${id}`, rows);
1725
- }
1726
- }
1727
- return out;
1728
- }
1900
+ };
1901
+
1902
+ /**
1903
+ * Build a help-time configuration bag for dynamic option descriptions.
1904
+ * Centralizes construction and reduces inline casts at call sites.
1905
+ */
1906
+ const toHelpConfig = (merged, plugins) => {
1907
+ const bag = {
1908
+ ...merged,
1909
+ plugins: plugins ?? {},
1910
+ };
1911
+ return bag;
1729
1912
  };
1730
1913
 
1731
1914
  /** src/cliHost/definePlugin.ts
@@ -1735,26 +1918,59 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1735
1918
  * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
1736
1919
  * nominal class identity issues (private fields) in downstream consumers.
1737
1920
  */
1738
- /**
1739
- * Define a GetDotenv CLI plugin with compositional helpers.
1740
- *
1741
- * @example
1742
- * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
1743
- * .use(childA)
1744
- * .use(childB);
1745
- */
1746
- const definePlugin = (spec) => {
1921
+ /* eslint-disable tsdoc/syntax */
1922
+ function definePlugin(spec) {
1747
1923
  const { children = [], ...rest } = spec;
1748
- const plugin = {
1924
+ // Default to a strict empty-object schema so “no-config” plugins fail fast
1925
+ // on unknown keys and provide a concrete {} at runtime.
1926
+ const effectiveSchema = spec.configSchema ?? z.object({}).strict();
1927
+ // Build base plugin first, then extend with instance-bound helpers.
1928
+ const base = {
1749
1929
  ...rest,
1930
+ // Always carry a schema (strict empty by default) to simplify host logic
1931
+ // and improve inference/ergonomics for plugin authors.
1932
+ configSchema: effectiveSchema,
1750
1933
  children: [...children],
1751
1934
  use(child) {
1752
1935
  this.children.push(child);
1753
1936
  return this;
1754
1937
  },
1755
1938
  };
1756
- return plugin;
1757
- };
1939
+ // Attach instance-bound helpers on the returned plugin object.
1940
+ const extended = base;
1941
+ extended.readConfig = function (_cli) {
1942
+ // Config is stored per-plugin-instance by the host (WeakMap in computeContext).
1943
+ const value = _getPluginConfigForInstance(extended);
1944
+ if (value === undefined) {
1945
+ // Guard: host has not resolved config yet (incorrect lifecycle usage).
1946
+ throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
1947
+ }
1948
+ return value;
1949
+ };
1950
+ // Plugin-bound dynamic option factory
1951
+ extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
1952
+ return cli.createDynamicOption(flags, (cfg) => {
1953
+ // Prefer the validated slice stored per instance; fallback to help-bag
1954
+ // (by-id) so top-level `-h` can render effective defaults before resolve.
1955
+ const fromStore = _getPluginConfigForInstance(extended);
1956
+ const id = extended.id;
1957
+ let fromBag;
1958
+ if (!fromStore && id) {
1959
+ const maybe = cfg.plugins[id];
1960
+ if (maybe && typeof maybe === 'object') {
1961
+ fromBag = maybe;
1962
+ }
1963
+ }
1964
+ // Always provide a concrete object to dynamic callbacks:
1965
+ // - With a schema: computeContext stores the parsed object.
1966
+ // - Without a schema: computeContext stores {}.
1967
+ // - Help-time fallback: coalesce to {} when only a by-id bag exists.
1968
+ const cfgVal = (fromStore ?? fromBag ?? {});
1969
+ return desc(cfg, cfgVal);
1970
+ }, parser, defaultValue);
1971
+ };
1972
+ return extended;
1973
+ }
1758
1974
 
1759
1975
  /**
1760
1976
  * GetDotenvCli with root helpers as real class methods.
@@ -1767,9 +1983,9 @@ class GetDotenvCli extends GetDotenvCli$1 {
1767
1983
  * Attach legacy root flags to this CLI instance. Defaults come from
1768
1984
  * baseRootOptionDefaults when none are provided.
1769
1985
  */
1770
- attachRootOptions(defaults, opts) {
1986
+ attachRootOptions(defaults) {
1771
1987
  const d = (defaults ?? baseRootOptionDefaults);
1772
- attachRootOptions(this, d, opts);
1988
+ attachRootOptions(this, d);
1773
1989
  return this;
1774
1990
  }
1775
1991
  /**
@@ -1791,10 +2007,19 @@ class GetDotenvCli extends GetDotenvCli$1 {
1791
2007
  // Build service options and compute context (always-on loader path).
1792
2008
  const serviceOptions = getDotenvCliOptions2Options(merged);
1793
2009
  await this.resolveAndLoad(serviceOptions);
2010
+ // Refresh dynamic option descriptions using resolved config + plugin slices
2011
+ try {
2012
+ const ctx = this.getCtx();
2013
+ const helpCfg = toHelpConfig(merged, ctx?.pluginConfigs ?? {});
2014
+ this.evaluateDynamicOptions(helpCfg);
2015
+ }
2016
+ catch {
2017
+ /* best-effort */
2018
+ }
1794
2019
  // Global validation: once after Phase C using config sources.
1795
2020
  try {
1796
2021
  const ctx = this.getCtx();
1797
- const dotenv = (ctx?.dotenv ?? {});
2022
+ const dotenv = ctx?.dotenv ?? {};
1798
2023
  const sources = await resolveGetDotenvConfigSources(import.meta.url);
1799
2024
  const issues = validateEnvAgainstSources(dotenv, sources);
1800
2025
  if (Array.isArray(issues) && issues.length > 0) {
@@ -1804,9 +2029,8 @@ class GetDotenvCli extends GetDotenvCli$1 {
1804
2029
  issues.forEach((m) => {
1805
2030
  emit(m);
1806
2031
  });
1807
- if (merged.strict) {
2032
+ if (merged.strict)
1808
2033
  process.exit(1);
1809
- }
1810
2034
  }
1811
2035
  }
1812
2036
  catch {
@@ -1825,6 +2049,14 @@ class GetDotenvCli extends GetDotenvCli$1 {
1825
2049
  if (!this.getCtx()) {
1826
2050
  const serviceOptions = getDotenvCliOptions2Options(merged);
1827
2051
  await this.resolveAndLoad(serviceOptions);
2052
+ try {
2053
+ const ctx = this.getCtx();
2054
+ const helpCfg = toHelpConfig(merged, ctx?.pluginConfigs ?? {});
2055
+ this.evaluateDynamicOptions(helpCfg);
2056
+ }
2057
+ catch {
2058
+ /* tolerate */
2059
+ }
1828
2060
  try {
1829
2061
  const ctx = this.getCtx();
1830
2062
  const dotenv = (ctx?.dotenv ?? {});