@karmaniverous/get-dotenv 5.2.5 → 6.0.0-0

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 (41) hide show
  1. package/README.md +63 -67
  2. package/dist/cliHost.cjs +765 -549
  3. package/dist/cliHost.d.cts +128 -84
  4. package/dist/cliHost.d.mts +128 -84
  5. package/dist/cliHost.d.ts +128 -84
  6. package/dist/cliHost.mjs +765 -549
  7. package/dist/getdotenv.cli.mjs +915 -685
  8. package/dist/index.cjs +959 -1006
  9. package/dist/index.d.cts +18 -178
  10. package/dist/index.d.mts +18 -178
  11. package/dist/index.d.ts +18 -178
  12. package/dist/index.mjs +960 -1006
  13. package/dist/plugins-aws.cjs +0 -1
  14. package/dist/plugins-aws.d.cts +8 -78
  15. package/dist/plugins-aws.d.mts +8 -78
  16. package/dist/plugins-aws.d.ts +8 -78
  17. package/dist/plugins-aws.mjs +0 -1
  18. package/dist/plugins-batch.cjs +53 -11
  19. package/dist/plugins-batch.d.cts +10 -79
  20. package/dist/plugins-batch.d.mts +10 -79
  21. package/dist/plugins-batch.d.ts +10 -79
  22. package/dist/plugins-batch.mjs +53 -11
  23. package/dist/plugins-cmd.cjs +162 -1555
  24. package/dist/plugins-cmd.d.cts +8 -78
  25. package/dist/plugins-cmd.d.mts +8 -78
  26. package/dist/plugins-cmd.d.ts +8 -78
  27. package/dist/plugins-cmd.mjs +162 -1554
  28. package/dist/plugins-demo.cjs +52 -7
  29. package/dist/plugins-demo.d.cts +8 -78
  30. package/dist/plugins-demo.d.mts +8 -78
  31. package/dist/plugins-demo.d.ts +8 -78
  32. package/dist/plugins-demo.mjs +52 -7
  33. package/dist/plugins-init.d.cts +8 -78
  34. package/dist/plugins-init.d.mts +8 -78
  35. package/dist/plugins-init.d.ts +8 -78
  36. package/dist/plugins.cjs +283 -1630
  37. package/dist/plugins.d.cts +10 -79
  38. package/dist/plugins.d.mts +10 -79
  39. package/dist/plugins.d.ts +10 -79
  40. package/dist/plugins.mjs +285 -1631
  41. package/package.json +4 -2
package/dist/cliHost.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- var commander = require('commander');
4
3
  var fs = require('fs-extra');
5
4
  var packageDirectory = require('package-directory');
6
- var url = require('url');
7
5
  var path = require('path');
8
- var zod = require('zod');
6
+ var url = require('url');
9
7
  var YAML = require('yaml');
8
+ var zod = require('zod');
9
+ var commander = require('commander');
10
10
  var nanoid = require('nanoid');
11
11
  var dotenv = require('dotenv');
12
12
  var crypto = require('crypto');
@@ -39,8 +39,6 @@ const baseRootOptionDefaults = {
39
39
  // (debug/log/exclude* resolved via flag utils)
40
40
  };
41
41
 
42
- const baseGetDotenvCliOptions = baseRootOptionDefaults;
43
-
44
42
  /** @internal */
45
43
  const isPlainObject$1 = (value) => value !== null &&
46
44
  typeof value === 'object' &&
@@ -83,134 +81,130 @@ const defaultsDeep = (...layers) => {
83
81
  return result;
84
82
  };
85
83
 
86
- // src/GetDotenvOptions.ts
87
- const getDotenvOptionsFilename = 'getdotenv.config.json';
88
84
  /**
89
- * Converts programmatic CLI options to `getDotenv` options. *
90
- * @param cliOptions - CLI options. Defaults to `{}`.
85
+ * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
86
+ * - If the user explicitly enabled the flag, return true.
87
+ * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
88
+ * - Otherwise, adopt the default (true → set; false/undefined → unset).
91
89
  *
92
- * @returns `getDotenv` options.
90
+ * @param exclude - The "on" flag value as parsed by Commander.
91
+ * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
92
+ * @param defaultValue - The generator default to adopt when no explicit toggle is present.
93
+ * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * resolveExclusion(undefined, undefined, true); // => true
98
+ * ```
93
99
  */
94
- const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
95
- /**
96
- * Convert CLI-facing string options into {@link GetDotenvOptions}.
97
- *
98
- * - 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`
99
- * pairs (configurable delimiters) into a {@link ProcessEnv}.
100
- * - Drops CLI-only keys that have no programmatic equivalent.
101
- *
102
- * @remarks
103
- * Follows exact-optional semantics by not emitting undefined-valued entries.
104
- */
105
- // Drop CLI-only keys (debug/scripts) without relying on Record casts.
106
- // Create a shallow copy then delete optional CLI-only keys if present.
107
- const restObj = { ...rest };
108
- delete restObj.debug;
109
- delete restObj.scripts;
110
- const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
111
- // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
112
- let parsedVars;
113
- if (typeof vars === 'string') {
114
- const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
115
- ? RegExp(varsAssignorPattern)
116
- : (varsAssignor ?? '=')));
117
- parsedVars = Object.fromEntries(kvPairs);
118
- }
119
- else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
120
- // Keep only string or undefined values to match ProcessEnv.
121
- const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
122
- parsedVars = Object.fromEntries(entries);
123
- }
124
- // Drop undefined-valued entries at the converter stage to match ProcessEnv
125
- // expectations and the compat test assertions.
126
- if (parsedVars) {
127
- parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
128
- }
129
- // Tolerate paths as either a delimited string or string[]
130
- // Use a locally cast union type to avoid lint warnings about always-falsy conditions
131
- // under the RootOptionsShape (which declares paths as string | undefined).
132
- const pathsAny = paths;
133
- const pathsOut = Array.isArray(pathsAny)
134
- ? pathsAny.filter((p) => typeof p === 'string')
135
- : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
136
- // Preserve exactOptionalPropertyTypes: only include keys when defined.
137
- return {
138
- ...restObj,
139
- ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
140
- ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
141
- };
142
- };
143
- const resolveGetDotenvOptions = async (customOptions) => {
144
- /**
145
- * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
146
- *
147
- * 1. Base defaults derived from the CLI generator defaults
148
- * ({@link baseGetDotenvCliOptions}).
149
- * 2. Local project overrides from a `getdotenv.config.json` in the nearest
150
- * package root (if present).
151
- * 3. The provided {@link customOptions}.
152
- *
153
- * The result preserves explicit empty values and drops only `undefined`.
154
- *
155
- * @returns Fully-resolved {@link GetDotenvOptions}.
156
- *
157
- * @example
158
- * ```ts
159
- * const options = await resolveGetDotenvOptions({ env: 'dev' });
160
- * ```
161
- */
162
- const localPkgDir = await packageDirectory.packageDirectory();
163
- const localOptionsPath = localPkgDir
164
- ? path.join(localPkgDir, getDotenvOptionsFilename)
165
- : undefined;
166
- const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
167
- ? JSON.parse((await fs.readFile(localOptionsPath)).toString())
168
- : {});
169
- // Merge order: base < local < custom (custom has highest precedence)
170
- const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
171
- const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
172
- const result = defaultsDeep(defaultsFromCli, customOptions);
173
- return {
174
- ...result, // Keep explicit empty strings/zeros; drop only undefined
175
- vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
176
- };
100
+ const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
101
+ /**
102
+ * Resolve an optional flag with "--exclude-all" overrides.
103
+ * If excludeAll is set and the individual "...-off" is not, force true.
104
+ * If excludeAllOff is set and the individual flag is not explicitly set, unset.
105
+ * Otherwise, adopt the default (true set; false/undefined unset).
106
+ *
107
+ * @param exclude - Individual include/exclude flag.
108
+ * @param excludeOff - Individual "...-off" flag.
109
+ * @param defaultValue - Default for the individual flag.
110
+ * @param excludeAll - Global "exclude-all" flag.
111
+ * @param excludeAllOff - Global "exclude-all-off" flag.
112
+ *
113
+ * @example
114
+ * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
115
+ */
116
+ const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
117
+ // Order of precedence:
118
+ // 1) Individual explicit "on" wins outright.
119
+ // 2) Individual explicit "off" wins over any global.
120
+ // 3) Global exclude-all forces true when not explicitly turned off.
121
+ // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
122
+ // 5) Fall back to the default (true => set; false/undefined => unset).
123
+ (() => {
124
+ // Individual "on"
125
+ if (exclude === true)
126
+ return true;
127
+ // Individual "off"
128
+ if (excludeOff === true)
129
+ return undefined;
130
+ // Global "exclude-all" ON (unless explicitly turned off)
131
+ if (excludeAll === true)
132
+ return true;
133
+ // Global "exclude-all-off" (unless explicitly enabled)
134
+ if (excludeAllOff === true)
135
+ return undefined;
136
+ // Default
137
+ return defaultValue ? true : undefined;
138
+ })();
139
+ /**
140
+ * exactOptionalPropertyTypes-safe setter for optional boolean flags:
141
+ * delete when undefined; assign when defined — without requiring an index signature on T.
142
+ *
143
+ * @typeParam T - Target object type.
144
+ * @param obj - The object to write to.
145
+ * @param key - The optional boolean property key of {@link T}.
146
+ * @param value - The value to set or `undefined` to unset.
147
+ *
148
+ * @remarks
149
+ * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
150
+ */
151
+ const setOptionalFlag = (obj, key, value) => {
152
+ const target = obj;
153
+ const k = key;
154
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
155
+ if (value === undefined)
156
+ delete target[k];
157
+ else
158
+ target[k] = value;
177
159
  };
178
160
 
179
161
  /**
180
- * Zod schemas for programmatic GetDotenv options.
181
- *
182
- * NOTE: These schemas are introduced without wiring to avoid behavior changes.
183
- * Legacy paths continue to use existing types/logic. The new plugin host will
184
- * use these schemas in strict mode; legacy paths will adopt them in warn mode
185
- * later per the staged plan.
162
+ * Merge and normalize raw Commander options (current + parent + defaults)
163
+ * into a GetDotenvCliOptions-like object. Types are intentionally wide to
164
+ * avoid cross-layer coupling; callers may cast as needed.
186
165
  */
187
- // Minimal process env representation: string values or undefined to indicate "unset".
188
- const processEnvSchema = zod.z.record(zod.z.string(), zod.z.string().optional());
189
- // RAW: all fields optional — undefined means "inherit" from lower layers.
190
- const getDotenvOptionsSchemaRaw = zod.z.object({
191
- defaultEnv: zod.z.string().optional(),
192
- dotenvToken: zod.z.string().optional(),
193
- dynamicPath: zod.z.string().optional(),
194
- // Dynamic map is intentionally wide for now; refine once sources are normalized.
195
- dynamic: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
196
- env: zod.z.string().optional(),
197
- excludeDynamic: zod.z.boolean().optional(),
198
- excludeEnv: zod.z.boolean().optional(),
199
- excludeGlobal: zod.z.boolean().optional(),
200
- excludePrivate: zod.z.boolean().optional(),
201
- excludePublic: zod.z.boolean().optional(),
202
- loadProcess: zod.z.boolean().optional(),
203
- log: zod.z.boolean().optional(),
204
- outputPath: zod.z.string().optional(),
205
- paths: zod.z.array(zod.z.string()).optional(),
206
- privateToken: zod.z.string().optional(),
207
- vars: processEnvSchema.optional(),
208
- // Host-only feature flag: guarded integration of config loader/overlay
209
- useConfigLoader: zod.z.boolean().optional(),
210
- });
211
- // RESOLVED: service-boundary contract (post-inheritance).
212
- // For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
213
- const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
166
+ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
167
+ const parent = typeof parentJson === 'string' && parentJson.length > 0
168
+ ? JSON.parse(parentJson)
169
+ : undefined;
170
+ const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
171
+ const current = { ...rest };
172
+ if (typeof scripts === 'string') {
173
+ try {
174
+ current.scripts = JSON.parse(scripts);
175
+ }
176
+ catch {
177
+ // ignore parse errors; leave scripts undefined
178
+ }
179
+ }
180
+ const merged = defaultsDeep({}, defaults, parent ?? {}, current);
181
+ const d = defaults;
182
+ setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
183
+ setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
184
+ setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
185
+ setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
186
+ setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
187
+ setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
188
+ setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
189
+ setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
190
+ // warnEntropy (tri-state)
191
+ setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
192
+ // Normalize shell for predictability: explicit default shell per OS.
193
+ const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
194
+ let resolvedShell = merged.shell;
195
+ if (shellOff)
196
+ resolvedShell = false;
197
+ else if (resolvedShell === true || resolvedShell === undefined) {
198
+ resolvedShell = defaultShell;
199
+ }
200
+ else if (typeof resolvedShell !== 'string' &&
201
+ typeof defaults.shell === 'string') {
202
+ resolvedShell = defaults.shell;
203
+ }
204
+ merged.shell = resolvedShell;
205
+ const cmd = typeof command === 'string' ? command : undefined;
206
+ return cmd !== undefined ? { merged, command: cmd } : { merged };
207
+ };
214
208
 
215
209
  /**
216
210
  * Zod schemas for configuration files discovered by the new loader.
@@ -450,17 +444,180 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
450
444
  };
451
445
 
452
446
  /**
453
- * Dotenv expansion utilities.
454
- *
455
- * This module implements recursive expansion of environment-variable
456
- * references in strings and records. It supports both whitespace and
457
- * bracket syntaxes with optional defaults:
458
- *
459
- * - Whitespace: `$VAR[:default]`
460
- * - Bracketed: `${VAR[:default]}`
447
+ * Validate a composed env against config-provided validation surfaces.
448
+ * Precedence for validation definitions:
449
+ * project.local -\> project.public -\> packaged
461
450
  *
462
- * Escaped dollar signs (`\$`) are preserved.
463
- * Unknown variables resolve to empty string unless a default is provided.
451
+ * Behavior:
452
+ * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
453
+ * - Else if `requiredKeys` is present, check presence (value !== undefined).
454
+ * - Returns a flat list of issue strings; caller decides warn vs fail.
455
+ */
456
+ const validateEnvAgainstSources = (finalEnv, sources) => {
457
+ const pick = (getter) => {
458
+ const pl = sources.project?.local;
459
+ const pp = sources.project?.public;
460
+ const pk = sources.packaged;
461
+ return ((pl && getter(pl)) ||
462
+ (pp && getter(pp)) ||
463
+ (pk && getter(pk)) ||
464
+ undefined);
465
+ };
466
+ const schema = pick((cfg) => cfg['schema']);
467
+ if (schema &&
468
+ typeof schema.safeParse === 'function') {
469
+ try {
470
+ const parsed = schema.safeParse(finalEnv);
471
+ if (!parsed.success) {
472
+ // Try to render zod-style issues when available.
473
+ const err = parsed.error;
474
+ const issues = Array.isArray(err.issues) && err.issues.length > 0
475
+ ? err.issues.map((i) => {
476
+ const path = Array.isArray(i.path) ? i.path.join('.') : '';
477
+ const msg = i.message ?? 'Invalid value';
478
+ return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
479
+ })
480
+ : ['[schema] validation failed'];
481
+ return issues;
482
+ }
483
+ return [];
484
+ }
485
+ catch {
486
+ // If schema invocation fails, surface a single diagnostic.
487
+ return [
488
+ '[schema] validation failed (unable to execute schema.safeParse)',
489
+ ];
490
+ }
491
+ }
492
+ const requiredKeys = pick((cfg) => cfg['requiredKeys']);
493
+ if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
494
+ const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
495
+ if (missing.length > 0) {
496
+ return missing.map((k) => `[requiredKeys] missing: ${k}`);
497
+ }
498
+ }
499
+ return [];
500
+ };
501
+
502
+ const baseGetDotenvCliOptions = baseRootOptionDefaults;
503
+
504
+ // src/GetDotenvOptions.ts
505
+ const getDotenvOptionsFilename = 'getdotenv.config.json';
506
+ /**
507
+ * Converts programmatic CLI options to `getDotenv` options. *
508
+ * @param cliOptions - CLI options. Defaults to `{}`.
509
+ *
510
+ * @returns `getDotenv` options.
511
+ */
512
+ const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
513
+ /**
514
+ * Convert CLI-facing string options into {@link GetDotenvOptions}.
515
+ *
516
+ * - 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`
517
+ * pairs (configurable delimiters) into a {@link ProcessEnv}.
518
+ * - Drops CLI-only keys that have no programmatic equivalent.
519
+ *
520
+ * @remarks
521
+ * Follows exact-optional semantics by not emitting undefined-valued entries.
522
+ */
523
+ // Drop CLI-only keys (debug/scripts) without relying on Record casts.
524
+ // Create a shallow copy then delete optional CLI-only keys if present.
525
+ const restObj = { ...rest };
526
+ delete restObj.debug;
527
+ delete restObj.scripts;
528
+ const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
529
+ // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
530
+ let parsedVars;
531
+ if (typeof vars === 'string') {
532
+ const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
533
+ ? RegExp(varsAssignorPattern)
534
+ : (varsAssignor ?? '=')));
535
+ parsedVars = Object.fromEntries(kvPairs);
536
+ }
537
+ else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
538
+ // Keep only string or undefined values to match ProcessEnv.
539
+ const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
540
+ parsedVars = Object.fromEntries(entries);
541
+ }
542
+ // Drop undefined-valued entries at the converter stage to match ProcessEnv
543
+ // expectations and the compat test assertions.
544
+ if (parsedVars) {
545
+ parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
546
+ }
547
+ // Tolerate paths as either a delimited string or string[]
548
+ // Use a locally cast union type to avoid lint warnings about always-falsy conditions
549
+ // under the RootOptionsShape (which declares paths as string | undefined).
550
+ const pathsAny = paths;
551
+ const pathsOut = Array.isArray(pathsAny)
552
+ ? pathsAny.filter((p) => typeof p === 'string')
553
+ : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
554
+ // Preserve exactOptionalPropertyTypes: only include keys when defined.
555
+ return {
556
+ ...restObj,
557
+ ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
558
+ ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
559
+ };
560
+ };
561
+ const resolveGetDotenvOptions = async (customOptions) => {
562
+ /**
563
+ * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
564
+ *
565
+ * 1. Base defaults derived from the CLI generator defaults
566
+ * ({@link baseGetDotenvCliOptions}).
567
+ * 2. Local project overrides from a `getdotenv.config.json` in the nearest
568
+ * package root (if present).
569
+ * 3. The provided {@link customOptions}.
570
+ *
571
+ * The result preserves explicit empty values and drops only `undefined`.
572
+ *
573
+ * @returns Fully-resolved {@link GetDotenvOptions}.
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * const options = await resolveGetDotenvOptions({ env: 'dev' });
578
+ * ```
579
+ */
580
+ const localPkgDir = await packageDirectory.packageDirectory();
581
+ const localOptionsPath = localPkgDir
582
+ ? path.join(localPkgDir, getDotenvOptionsFilename)
583
+ : undefined;
584
+ // Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
585
+ let localOptions = {};
586
+ if (localOptionsPath && (await fs.exists(localOptionsPath))) {
587
+ try {
588
+ const txt = await fs.readFile(localOptionsPath, 'utf-8');
589
+ const parsed = JSON.parse(txt);
590
+ if (parsed && typeof parsed === 'object') {
591
+ localOptions = parsed;
592
+ }
593
+ }
594
+ catch {
595
+ // Malformed or unreadable local options are treated as absent.
596
+ localOptions = {};
597
+ }
598
+ }
599
+ // Merge order: base < local < custom (custom has highest precedence)
600
+ const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
601
+ const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
602
+ const result = defaultsDeep(defaultsFromCli, customOptions);
603
+ return {
604
+ ...result, // Keep explicit empty strings/zeros; drop only undefined
605
+ vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
606
+ };
607
+ };
608
+
609
+ /**
610
+ * Dotenv expansion utilities.
611
+ *
612
+ * This module implements recursive expansion of environment-variable
613
+ * references in strings and records. It supports both whitespace and
614
+ * bracket syntaxes with optional defaults:
615
+ *
616
+ * - Whitespace: `$VAR[:default]`
617
+ * - Bracketed: `${VAR[:default]}`
618
+ *
619
+ * Escaped dollar signs (`\$`) are preserved.
620
+ * Unknown variables resolve to empty string unless a default is provided.
464
621
  */
465
622
  /**
466
623
  * Like String.prototype.search but returns the last index.
@@ -585,33 +742,243 @@ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduc
585
742
  */
586
743
  const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
587
744
 
588
- const applyKv = (current, kv) => {
589
- if (!kv || Object.keys(kv).length === 0)
590
- return current;
591
- const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
592
- return { ...current, ...expanded };
593
- };
594
- const applyConfigSlice = (current, cfg, env) => {
595
- if (!cfg)
596
- return current;
597
- // kind axis: global then env (env overrides global)
598
- const afterGlobal = applyKv(current, cfg.vars);
599
- const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
600
- return applyKv(afterGlobal, envKv);
745
+ /* eslint-disable @typescript-eslint/no-deprecated */
746
+ /**
747
+ * Attach root flags to a GetDotenvCli instance.
748
+ * - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
749
+ * - Any flag that displays an effective default in help uses dynamic descriptions.
750
+ */
751
+ const attachRootOptions = (program, defaults, opts) => {
752
+ // Install temporary wrappers to tag all options added here as "base" for grouped help.
753
+ const GROUP = 'base';
754
+ const tagLatest = (cmd, group) => {
755
+ const optsArr = cmd.options;
756
+ if (Array.isArray(optsArr) && optsArr.length > 0) {
757
+ const last = optsArr[optsArr.length - 1];
758
+ last.__group = group;
759
+ }
760
+ };
761
+ const originalAddOption = program.addOption.bind(program);
762
+ const originalOption = program.option.bind(program);
763
+ program.addOption = function patchedAdd(opt) {
764
+ opt.__group = GROUP;
765
+ return originalAddOption(opt);
766
+ };
767
+ program.option = function patchedOption(...args) {
768
+ const ret = originalOption(...args);
769
+ tagLatest(this, GROUP);
770
+ return ret;
771
+ };
772
+ const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
773
+ const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
774
+ const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
775
+ // Helper: append (default) tags for ON/OFF toggles
776
+ const onOff = (on, isDefault) => on
777
+ ? `ON${isDefault ? ' (default)' : ''}`
778
+ : `OFF${isDefault ? ' (default)' : ''}`;
779
+ let p = program
780
+ .enablePositionalOptions()
781
+ .passThroughOptions()
782
+ .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
783
+ p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
784
+ ['KEY1', 'VAL1'],
785
+ ['KEY2', 'VAL2'],
786
+ ]
787
+ .map((v) => v.join(va))
788
+ .join(vd)}`, dotenvExpandFromProcessEnv);
789
+ if (opts?.includeCommandOption === true) {
790
+ p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
791
+ }
792
+ // Output path (interpolated later; help can remain static)
793
+ p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
794
+ // Shell ON (string or boolean true => default shell)
795
+ p = p
796
+ .addOption(program
797
+ .createDynamicOption('-s, --shell [string]', (cfg) => {
798
+ const s = cfg.shell;
799
+ let tag = '';
800
+ if (typeof s === 'boolean' && s)
801
+ tag = ' (default OS shell)';
802
+ else if (typeof s === 'string' && s.length > 0)
803
+ tag = ` (default ${s})`;
804
+ return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
805
+ })
806
+ .conflicts('shellOff'))
807
+ // Shell OFF
808
+ .addOption(program
809
+ .createDynamicOption('-S, --shell-off', (cfg) => {
810
+ const s = cfg.shell;
811
+ return `command execution shell OFF${s === false ? ' (default)' : ''}`;
812
+ })
813
+ .conflicts('shell'));
814
+ // Load process ON/OFF (dynamic defaults)
815
+ p = p
816
+ .addOption(program
817
+ .createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
818
+ .conflicts('loadProcessOff'))
819
+ .addOption(program
820
+ .createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
821
+ .conflicts('loadProcess'));
822
+ // Exclusion master toggle (dynamic)
823
+ p = p
824
+ .addOption(program
825
+ .createDynamicOption('-a, --exclude-all', (cfg) => {
826
+ const c = cfg;
827
+ const allOn = !!c.excludeDynamic &&
828
+ ((!!c.excludeEnv && !!c.excludeGlobal) ||
829
+ (!!c.excludePrivate && !!c.excludePublic));
830
+ const suffix = allOn ? ' (default)' : '';
831
+ return `exclude all dotenv variables from loading ON${suffix}`;
832
+ })
833
+ .conflicts('excludeAllOff'))
834
+ .addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
835
+ // Per-family exclusions (dynamic defaults)
836
+ p = p
837
+ .addOption(program
838
+ .createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
839
+ .conflicts('excludeDynamicOff'))
840
+ .addOption(program
841
+ .createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
842
+ .conflicts('excludeDynamic'))
843
+ .addOption(program
844
+ .createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
845
+ .conflicts('excludeEnvOff'))
846
+ .addOption(program
847
+ .createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
848
+ .conflicts('excludeEnv'))
849
+ .addOption(program
850
+ .createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
851
+ .conflicts('excludeGlobalOff'))
852
+ .addOption(program
853
+ .createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
854
+ .conflicts('excludeGlobal'))
855
+ .addOption(program
856
+ .createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
857
+ .conflicts('excludePrivateOff'))
858
+ .addOption(program
859
+ .createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
860
+ .conflicts('excludePrivate'))
861
+ .addOption(program
862
+ .createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
863
+ .conflicts('excludePublicOff'))
864
+ .addOption(program
865
+ .createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
866
+ .conflicts('excludePublic'));
867
+ // Log ON/OFF (dynamic)
868
+ p = p
869
+ .addOption(program
870
+ .createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
871
+ .conflicts('logOff'))
872
+ .addOption(program
873
+ .createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
874
+ .conflicts('log'));
875
+ // Capture flag (no default display; static)
876
+ p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
877
+ // Core bootstrap/static flags (kept static in help)
878
+ p = p
879
+ .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
880
+ .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
881
+ .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
882
+ .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
883
+ .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
884
+ .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
885
+ .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
886
+ .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
887
+ .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
888
+ .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
889
+ .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
890
+ // Hidden scripts pipe-through (stringified)
891
+ .addOption(new commander.Option('--scripts <string>')
892
+ .default(JSON.stringify(scripts))
893
+ .hideHelp());
894
+ // Diagnostics / validation / entropy
895
+ p = p
896
+ .option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
897
+ .option('--strict', 'fail on env validation errors (schema/requiredKeys)');
898
+ p = p
899
+ .addOption(program
900
+ .createDynamicOption('--entropy-warn', (cfg) => {
901
+ const warn = cfg.warnEntropy;
902
+ // Default is effectively ON when warnEntropy is true or undefined.
903
+ return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
904
+ })
905
+ .conflicts('entropyWarnOff'))
906
+ .addOption(program
907
+ .createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
908
+ .conflicts('entropyWarn'))
909
+ .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
910
+ .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
911
+ .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
912
+ .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
913
+ // Restore original methods
914
+ program.addOption = originalAddOption;
915
+ program.option = originalOption;
916
+ return p;
601
917
  };
918
+
602
919
  /**
603
- * Overlay config-provided values onto a base ProcessEnv using precedence axes:
604
- * - kind: env \> global
605
- * - privacy: local \> public
606
- * - source: project \> packaged \> base
920
+ * Zod schemas for programmatic GetDotenv options.
607
921
  *
608
- * Programmatic explicit vars (if provided) override all config slices.
609
- * Progressive expansion is applied within each slice.
922
+ * NOTE: These schemas are introduced without wiring to avoid behavior changes.
923
+ * Legacy paths continue to use existing types/logic. The new plugin host will
924
+ * use these schemas in strict mode; legacy paths will adopt them in warn mode
925
+ * later per the staged plan.
610
926
  */
611
- const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
612
- let current = { ...base };
613
- // Source: packaged (public -> local)
614
- current = applyConfigSlice(current, configs.packaged, env);
927
+ // Minimal process env representation: string values or undefined to indicate "unset".
928
+ const processEnvSchema = zod.z.record(zod.z.string(), zod.z.string().optional());
929
+ // RAW: all fields optional — undefined means "inherit" from lower layers.
930
+ const getDotenvOptionsSchemaRaw = zod.z.object({
931
+ defaultEnv: zod.z.string().optional(),
932
+ dotenvToken: zod.z.string().optional(),
933
+ dynamicPath: zod.z.string().optional(),
934
+ // Dynamic map is intentionally wide for now; refine once sources are normalized.
935
+ dynamic: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
936
+ env: zod.z.string().optional(),
937
+ excludeDynamic: zod.z.boolean().optional(),
938
+ excludeEnv: zod.z.boolean().optional(),
939
+ excludeGlobal: zod.z.boolean().optional(),
940
+ excludePrivate: zod.z.boolean().optional(),
941
+ excludePublic: zod.z.boolean().optional(),
942
+ loadProcess: zod.z.boolean().optional(),
943
+ log: zod.z.boolean().optional(),
944
+ outputPath: zod.z.string().optional(),
945
+ paths: zod.z.array(zod.z.string()).optional(),
946
+ privateToken: zod.z.string().optional(),
947
+ vars: processEnvSchema.optional(),
948
+ // Host-only feature flag: guarded integration of config loader/overlay
949
+ useConfigLoader: zod.z.boolean().optional(),
950
+ });
951
+ // RESOLVED: service-boundary contract (post-inheritance).
952
+ // For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
953
+ const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
954
+
955
+ const applyKv = (current, kv) => {
956
+ if (!kv || Object.keys(kv).length === 0)
957
+ return current;
958
+ const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
959
+ return { ...current, ...expanded };
960
+ };
961
+ const applyConfigSlice = (current, cfg, env) => {
962
+ if (!cfg)
963
+ return current;
964
+ // kind axis: global then env (env overrides global)
965
+ const afterGlobal = applyKv(current, cfg.vars);
966
+ const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
967
+ return applyKv(afterGlobal, envKv);
968
+ };
969
+ /**
970
+ * Overlay config-provided values onto a base ProcessEnv using precedence axes:
971
+ * - kind: env \> global
972
+ * - privacy: local \> public
973
+ * - source: project \> packaged \> base
974
+ *
975
+ * Programmatic explicit vars (if provided) override all config slices.
976
+ * Progressive expansion is applied within each slice.
977
+ */
978
+ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
979
+ let current = { ...base };
980
+ // Source: packaged (public -> local)
981
+ current = applyConfigSlice(current, configs.packaged, env);
615
982
  // Packaged "local" is not expected by policy; if present, honor it.
616
983
  // We do not have a separate object for packaged.local in sources, keep as-is.
617
984
  // Source: project (public -> local)
@@ -1161,6 +1528,8 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1161
1528
  };
1162
1529
  };
1163
1530
 
1531
+ // Dynamic help support: attach a private symbol to Option for description fns.
1532
+ const DYN_DESC_SYM = Symbol('getdotenv.dynamic.description');
1164
1533
  const HOST_META_URL = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cliHost.cjs', document.baseURI).href));
1165
1534
  const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
1166
1535
  const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
@@ -1176,13 +1545,20 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
1176
1545
  *
1177
1546
  * NOTE: This host is additive and does not alter the legacy CLI.
1178
1547
  */
1179
- class GetDotenvCli extends commander.Command {
1548
+ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1180
1549
  /** Registered top-level plugins (composition happens via .use()) */
1181
1550
  _plugins = [];
1182
1551
  /** One-time installation guard */
1183
1552
  _installed = false;
1184
1553
  /** Optional header line to prepend in help output */
1185
1554
  [HELP_HEADER_SYMBOL];
1555
+ /**
1556
+ * Create a subcommand using the same subclass, preserving helpers like
1557
+ * dynamicOption on children.
1558
+ */
1559
+ createCommand(name) {
1560
+ return new this.constructor(name);
1561
+ }
1186
1562
  constructor(alias = 'getdotenv') {
1187
1563
  super(alias);
1188
1564
  // Ensure subcommands that use passThroughOptions can be attached safely.
@@ -1190,15 +1566,18 @@ class GetDotenvCli extends commander.Command {
1190
1566
  // child uses passThroughOptions.
1191
1567
  this.enablePositionalOptions();
1192
1568
  // Configure grouped help: show only base options in default "Options";
1193
- // append App/Plugin sections after default help.
1569
+ // we will insert App/Plugin sections before Commands in helpInformation().
1194
1570
  this.configureHelp({
1195
1571
  visibleOptions: (cmd) => {
1196
- const all = cmd.options ??
1197
- [];
1198
- const base = all.filter((opt) => {
1199
- const group = opt.__group;
1200
- return group === 'base';
1201
- });
1572
+ const all = cmd.options ?? [];
1573
+ const parent = cmd.parent ?? null;
1574
+ const isRoot = parent === null;
1575
+ const list = isRoot
1576
+ ? all.filter((opt) => {
1577
+ const group = opt.__group;
1578
+ return group === 'base';
1579
+ })
1580
+ : all.slice(); // subcommands: show all options (their own "Options:" block)
1202
1581
  // Sort: short-aliased options first, then long-only; stable by flags.
1203
1582
  const hasShort = (opt) => {
1204
1583
  const flags = opt.flags ?? '';
@@ -1206,19 +1585,18 @@ class GetDotenvCli extends commander.Command {
1206
1585
  return /(^|\s|,)-[A-Za-z]/.test(flags);
1207
1586
  };
1208
1587
  const byFlags = (opt) => opt.flags ?? '';
1209
- base.sort((a, b) => {
1588
+ list.sort((a, b) => {
1210
1589
  const aS = hasShort(a) ? 1 : 0;
1211
1590
  const bS = hasShort(b) ? 1 : 0;
1212
1591
  return bS - aS || byFlags(a).localeCompare(byFlags(b));
1213
1592
  });
1214
- return base;
1593
+ return list;
1215
1594
  },
1216
1595
  });
1217
1596
  this.addHelpText('beforeAll', () => {
1218
1597
  const header = this[HELP_HEADER_SYMBOL];
1219
1598
  return header && header.length > 0 ? `${header}\n\n` : '';
1220
1599
  });
1221
- this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
1222
1600
  // Skeleton preSubcommand hook: produce a context if absent, without
1223
1601
  // mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
1224
1602
  // loadProcess=false here avoids leaking dotenv values into the parent
@@ -1230,9 +1608,15 @@ class GetDotenvCli extends commander.Command {
1230
1608
  });
1231
1609
  }
1232
1610
  /**
1233
- * Resolve options (strict) and compute dotenv context. * Stores the context on the instance under a symbol.
1611
+ * Resolve options (strict) and compute dotenv context.
1612
+ * Stores the context on the instance under a symbol.
1613
+ *
1614
+ * Options:
1615
+ * - opts.runAfterResolve (default true): when false, skips running plugin
1616
+ * afterResolve hooks. Useful for top-level help rendering to avoid
1617
+ * long-running side-effects while still evaluating dynamic help text.
1234
1618
  */
1235
- async resolveAndLoad(customOptions = {}) {
1619
+ async resolveAndLoad(customOptions = {}, opts) {
1236
1620
  // Resolve defaults, then validate strictly under the new host.
1237
1621
  const optionsResolved = await resolveGetDotenvOptions(customOptions);
1238
1622
  getDotenvOptionsSchemaResolved.parse(optionsResolved);
@@ -1243,9 +1627,64 @@ class GetDotenvCli extends commander.Command {
1243
1627
  ctx;
1244
1628
  // Ensure plugins are installed exactly once, then run afterResolve.
1245
1629
  await this.install();
1246
- await this._runAfterResolve(ctx);
1630
+ if (opts?.runAfterResolve ?? true) {
1631
+ await this._runAfterResolve(ctx);
1632
+ }
1247
1633
  return ctx;
1248
1634
  }
1635
+ /**
1636
+ * Create a Commander Option that computes its description at help time.
1637
+ * The returned Option may be configured (conflicts, default, parser) and
1638
+ * added via addOption().
1639
+ */
1640
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
1641
+ createDynamicOption(flags, desc, parser, defaultValue) {
1642
+ const opt = new commander.Option(flags, '');
1643
+ // Keep the function on a private symbol so it survives through Commander.
1644
+ opt[DYN_DESC_SYM] = desc;
1645
+ if (parser)
1646
+ opt.argParser(parser);
1647
+ if (defaultValue !== undefined)
1648
+ opt.default(defaultValue);
1649
+ return opt;
1650
+ }
1651
+ /**
1652
+ * Chainable helper mirroring .option(), but with a dynamic description.
1653
+ * Equivalent to addOption(createDynamicOption(...)).
1654
+ */
1655
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
1656
+ dynamicOption(flags, desc, parser, defaultValue) {
1657
+ const opt = this.createDynamicOption(flags, desc, parser, defaultValue);
1658
+ this.addOption(opt);
1659
+ return this;
1660
+ }
1661
+ /**
1662
+ * Evaluate dynamic descriptions for this command and all descendants using
1663
+ * the provided resolved configuration. Mutates the Option.description in
1664
+ * place so Commander help renders updated text.
1665
+ */
1666
+ evaluateDynamicOptions(resolved) {
1667
+ const visit = (cmd) => {
1668
+ const arr = cmd.options ?? [];
1669
+ for (const o of arr) {
1670
+ const dyn = o[DYN_DESC_SYM];
1671
+ if (typeof dyn === 'function') {
1672
+ try {
1673
+ const txt = dyn(resolved);
1674
+ // Commander Option has a public "description" field used by help.
1675
+ o.description = txt;
1676
+ }
1677
+ catch {
1678
+ // Best-effort: leave description as-is on evaluation failure.
1679
+ }
1680
+ }
1681
+ }
1682
+ const children = cmd.commands ?? [];
1683
+ for (const c of children)
1684
+ visit(c);
1685
+ };
1686
+ visit(this);
1687
+ }
1249
1688
  /**
1250
1689
  * Retrieve the current invocation context (if any).
1251
1690
  */
@@ -1275,6 +1714,7 @@ class GetDotenvCli extends commander.Command {
1275
1714
  tagAppOptions(fn) {
1276
1715
  const root = this;
1277
1716
  const originalAddOption = root.addOption.bind(root);
1717
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1278
1718
  const originalOption = root.option.bind(root);
1279
1719
  const tagLatest = (cmd, group) => {
1280
1720
  const optsArr = cmd.options;
@@ -1287,6 +1727,7 @@ class GetDotenvCli extends commander.Command {
1287
1727
  opt.__group = 'app';
1288
1728
  return originalAddOption(opt);
1289
1729
  };
1730
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1290
1731
  root.option = function patchedOption(...args) {
1291
1732
  const ret = originalOption(...args);
1292
1733
  tagLatest(this, 'app');
@@ -1297,6 +1738,7 @@ class GetDotenvCli extends commander.Command {
1297
1738
  }
1298
1739
  finally {
1299
1740
  root.addOption = originalAddOption;
1741
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1300
1742
  root.option = originalOption;
1301
1743
  }
1302
1744
  }
@@ -1342,6 +1784,40 @@ class GetDotenvCli extends commander.Command {
1342
1784
  }
1343
1785
  return this;
1344
1786
  }
1787
+ /**
1788
+ * Insert grouped plugin/app options between "Options" and "Commands" for
1789
+ * hybrid ordering. Applies to root and any parent command.
1790
+ */
1791
+ helpInformation() {
1792
+ // Base help text first (includes beforeAll/after hooks).
1793
+ const base = super.helpInformation();
1794
+ const groups = this.#renderOptionGroups(this);
1795
+ const block = typeof groups === 'string' ? groups.trim() : '';
1796
+ let out = base;
1797
+ if (!block) {
1798
+ // Ensure a trailing blank line even when no extra groups render.
1799
+ if (!out.endsWith('\n\n'))
1800
+ out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1801
+ return out;
1802
+ }
1803
+ // Insert just before "Commands:" when present.
1804
+ const marker = '\nCommands:';
1805
+ const idx = base.indexOf(marker);
1806
+ if (idx >= 0) {
1807
+ const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
1808
+ out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
1809
+ }
1810
+ else {
1811
+ // Otherwise append.
1812
+ const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
1813
+ out = `${base}${sep}${groups}`;
1814
+ }
1815
+ // Ensure a trailing blank line for prompt separation.
1816
+ if (!out.endsWith('\n\n')) {
1817
+ out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1818
+ }
1819
+ return out;
1820
+ }
1345
1821
  /**
1346
1822
  * Register a plugin for installation (parent level).
1347
1823
  * Installation occurs on first resolveAndLoad() (or explicit install()).
@@ -1380,7 +1856,7 @@ class GetDotenvCli extends commander.Command {
1380
1856
  for (const p of this._plugins)
1381
1857
  await run(p);
1382
1858
  }
1383
- // Render App/Plugin grouped options appended after default help.
1859
+ // Render App/Plugin grouped options (used by helpInformation override).
1384
1860
  #renderOptionGroups(cmd) {
1385
1861
  const all = cmd.options ?? [];
1386
1862
  const byGroup = new Map();
@@ -1422,369 +1898,96 @@ class GetDotenvCli extends commander.Command {
1422
1898
  }
1423
1899
  // Plugin groups sorted by id
1424
1900
  const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
1901
+ const currentName = cmd.name?.() ?? '';
1425
1902
  pluginKeys.sort((a, b) => a.localeCompare(b));
1426
1903
  for (const k of pluginKeys) {
1427
1904
  const id = k.slice('plugin:'.length) || '(unknown)';
1428
1905
  const rows = byGroup.get(k) ?? [];
1429
- if (rows.length > 0) {
1906
+ // Do not show a "Plugin options — <self>" section on the command that owns those options.
1907
+ // Only child-injected plugin groups should render at this level.
1908
+ if (rows.length > 0 && id !== currentName) {
1430
1909
  out += renderRows(`Plugin options — ${id}`, rows);
1431
1910
  }
1432
1911
  }
1433
1912
  return out;
1434
1913
  }
1435
- }
1436
-
1437
- /**
1438
- * Validate a composed env against config-provided validation surfaces.
1439
- * Precedence for validation definitions:
1440
- * project.local -\> project.public -\> packaged
1441
- *
1442
- * Behavior:
1443
- * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
1444
- * - Else if `requiredKeys` is present, check presence (value !== undefined).
1445
- * - Returns a flat list of issue strings; caller decides warn vs fail.
1446
- */
1447
- const validateEnvAgainstSources = (finalEnv, sources) => {
1448
- const pick = (getter) => {
1449
- const pl = sources.project?.local;
1450
- const pp = sources.project?.public;
1451
- const pk = sources.packaged;
1452
- return ((pl && getter(pl)) ||
1453
- (pp && getter(pp)) ||
1454
- (pk && getter(pk)) ||
1455
- undefined);
1456
- };
1457
- const schema = pick((cfg) => cfg['schema']);
1458
- if (schema &&
1459
- typeof schema.safeParse === 'function') {
1460
- try {
1461
- const parsed = schema.safeParse(finalEnv);
1462
- if (!parsed.success) {
1463
- // Try to render zod-style issues when available.
1464
- const err = parsed.error;
1465
- const issues = Array.isArray(err.issues) && err.issues.length > 0
1466
- ? err.issues.map((i) => {
1467
- const path = Array.isArray(i.path) ? i.path.join('.') : '';
1468
- const msg = i.message ?? 'Invalid value';
1469
- return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
1470
- })
1471
- : ['[schema] validation failed'];
1472
- return issues;
1473
- }
1474
- return [];
1475
- }
1476
- catch {
1477
- // If schema invocation fails, surface a single diagnostic.
1478
- return [
1479
- '[schema] validation failed (unable to execute schema.safeParse)',
1480
- ];
1481
- }
1482
- }
1483
- const requiredKeys = pick((cfg) => cfg['requiredKeys']);
1484
- if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
1485
- const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
1486
- if (missing.length > 0) {
1487
- return missing.map((k) => `[requiredKeys] missing: ${k}`);
1488
- }
1489
- }
1490
- return [];
1491
- };
1492
-
1493
- /**
1494
- * Attach legacy root flags to a Commander program.
1495
- * Uses provided defaults to render help labels without coupling to generators.
1496
- */
1497
- const attachRootOptions = (program, defaults, opts) => {
1498
- // Install temporary wrappers to tag all options added here as "base".
1499
- const GROUP = 'base';
1500
- const tagLatest = (cmd, group) => {
1501
- const optsArr = cmd.options;
1502
- if (Array.isArray(optsArr) && optsArr.length > 0) {
1503
- const last = optsArr[optsArr.length - 1];
1504
- last.__group = group;
1505
- }
1506
- };
1507
- const originalAddOption = program.addOption.bind(program);
1508
- const originalOption = program.option.bind(program);
1509
- program.addOption = function patchedAdd(opt) {
1510
- // Tag before adding, in case consumers inspect the Option directly.
1511
- opt.__group = GROUP;
1512
- const ret = originalAddOption(opt);
1513
- return ret;
1514
- };
1515
- program.option = function patchedOption(...args) {
1516
- const ret = originalOption(...args);
1517
- tagLatest(this, GROUP);
1518
- return ret;
1519
- };
1520
- const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
1521
- const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
1522
- const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
1523
- // Build initial chain.
1524
- let p = program
1525
- .enablePositionalOptions()
1526
- .passThroughOptions()
1527
- .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
1528
- p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
1529
- ['KEY1', 'VAL1'],
1530
- ['KEY2', 'VAL2'],
1531
- ]
1532
- .map((v) => v.join(va))
1533
- .join(vd)}`, dotenvExpandFromProcessEnv);
1534
- // Optional legacy root command flag (kept for generated CLI compatibility).
1535
- // Default is OFF; the generator opts in explicitly.
1536
- if (opts?.includeCommandOption === true) {
1537
- p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
1538
- }
1539
- p = p
1540
- .option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
1541
- .addOption(new commander.Option('-s, --shell [string]', (() => {
1542
- let defaultLabel = '';
1543
- if (shell !== undefined) {
1544
- if (typeof shell === 'boolean') {
1545
- defaultLabel = ' (default OS shell)';
1546
- }
1547
- else if (typeof shell === 'string') {
1548
- // Safe string interpolation
1549
- defaultLabel = ` (default ${shell})`;
1550
- }
1551
- }
1552
- return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
1553
- })()).conflicts('shellOff'))
1554
- .addOption(new commander.Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
1555
- .addOption(new commander.Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
1556
- .addOption(new commander.Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
1557
- .addOption(new commander.Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
1558
- ((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
1559
- ? ' (default)'
1560
- : ''}`).conflicts('excludeAllOff'))
1561
- .addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
1562
- .addOption(new commander.Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
1563
- .addOption(new commander.Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
1564
- .addOption(new commander.Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
1565
- .addOption(new commander.Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
1566
- .addOption(new commander.Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
1567
- .addOption(new commander.Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
1568
- .addOption(new commander.Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
1569
- .addOption(new commander.Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
1570
- .addOption(new commander.Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
1571
- .addOption(new commander.Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
1572
- .addOption(new commander.Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
1573
- .addOption(new commander.Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
1574
- .option('--capture', 'capture child process stdio for commands (tests/CI)')
1575
- .option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
1576
- .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
1577
- .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
1578
- .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
1579
- .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
1580
- .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
1581
- .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
1582
- .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
1583
- .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
1584
- .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
1585
- .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
1586
- .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
1587
- // Hidden scripts pipe-through (stringified)
1588
- .addOption(new commander.Option('--scripts <string>')
1589
- .default(JSON.stringify(scripts))
1590
- .hideHelp());
1591
- // Diagnostics: opt-in tracing; optional variadic keys after the flag.
1592
- p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
1593
- // Validation: strict mode fails on env validation issues (warn by default).
1594
- p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
1595
- // Entropy diagnostics (presentation-only)
1596
- p = p
1597
- .addOption(new commander.Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
1598
- .addOption(new commander.Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
1599
- .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
1600
- .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
1601
- .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
1602
- .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
1603
- // Restore original methods to avoid tagging future additions outside base.
1604
- program.addOption = originalAddOption;
1605
- program.option = originalOption;
1606
- return p;
1607
1914
  };
1608
1915
 
1609
- /**
1610
- * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
1611
- * - If the user explicitly enabled the flag, return true.
1612
- * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
1613
- * - Otherwise, adopt the default (true → set; false/undefined → unset).
1614
- *
1615
- * @param exclude - The "on" flag value as parsed by Commander.
1616
- * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
1617
- * @param defaultValue - The generator default to adopt when no explicit toggle is present.
1618
- * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
1916
+ /** src/cliHost/definePlugin.ts
1917
+ * Plugin contracts for the GetDotenv CLI host.
1619
1918
  *
1620
- * @example
1621
- * ```ts
1622
- * resolveExclusion(undefined, undefined, true); // => true
1623
- * ```
1919
+ * This module exposes a structural public interface for the host that plugins
1920
+ * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
1921
+ * nominal class identity issues (private fields) in downstream consumers.
1624
1922
  */
1625
- const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
1626
1923
  /**
1627
- * Resolve an optional flag with "--exclude-all" overrides.
1628
- * If excludeAll is set and the individual "...-off" is not, force true.
1629
- * If excludeAllOff is set and the individual flag is not explicitly set, unset.
1630
- * Otherwise, adopt the default (true → set; false/undefined → unset).
1631
- *
1632
- * @param exclude - Individual include/exclude flag.
1633
- * @param excludeOff - Individual "...-off" flag.
1634
- * @param defaultValue - Default for the individual flag.
1635
- * @param excludeAll - Global "exclude-all" flag.
1636
- * @param excludeAllOff - Global "exclude-all-off" flag.
1924
+ * Define a GetDotenv CLI plugin with compositional helpers.
1637
1925
  *
1638
1926
  * @example
1639
- * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
1640
- */
1641
- const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
1642
- // Order of precedence:
1643
- // 1) Individual explicit "on" wins outright.
1644
- // 2) Individual explicit "off" wins over any global.
1645
- // 3) Global exclude-all forces true when not explicitly turned off.
1646
- // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
1647
- // 5) Fall back to the default (true => set; false/undefined => unset).
1648
- (() => {
1649
- // Individual "on"
1650
- if (exclude === true)
1651
- return true;
1652
- // Individual "off"
1653
- if (excludeOff === true)
1654
- return undefined;
1655
- // Global "exclude-all" ON (unless explicitly turned off)
1656
- if (excludeAll === true)
1657
- return true;
1658
- // Global "exclude-all-off" (unless explicitly enabled)
1659
- if (excludeAllOff === true)
1660
- return undefined;
1661
- // Default
1662
- return defaultValue ? true : undefined;
1663
- })();
1664
- /**
1665
- * exactOptionalPropertyTypes-safe setter for optional boolean flags:
1666
- * delete when undefined; assign when defined — without requiring an index signature on T.
1667
- *
1668
- * @typeParam T - Target object type.
1669
- * @param obj - The object to write to.
1670
- * @param key - The optional boolean property key of {@link T}.
1671
- * @param value - The value to set or `undefined` to unset.
1672
- *
1673
- * @remarks
1674
- * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
1927
+ * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
1928
+ * .use(childA)
1929
+ * .use(childB);
1675
1930
  */
1676
- const setOptionalFlag = (obj, key, value) => {
1677
- const target = obj;
1678
- const k = key;
1679
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1680
- if (value === undefined)
1681
- delete target[k];
1682
- else
1683
- target[k] = value;
1931
+ const definePlugin = (spec) => {
1932
+ const { children = [], ...rest } = spec;
1933
+ const plugin = {
1934
+ ...rest,
1935
+ children: [...children],
1936
+ use(child) {
1937
+ this.children.push(child);
1938
+ return this;
1939
+ },
1940
+ };
1941
+ return plugin;
1684
1942
  };
1685
1943
 
1686
1944
  /**
1687
- * Merge and normalize raw Commander options (current + parent + defaults)
1688
- * into a GetDotenvCliOptions-like object. Types are intentionally wide to
1689
- * avoid cross-layer coupling; callers may cast as needed.
1945
+ * GetDotenvCli with root helpers as real class methods.
1946
+ * - attachRootOptions: installs legacy/base root flags on the command.
1947
+ * - passOptions: merges flags (parent \< current), computes dotenv context once,
1948
+ * runs validation, and persists merged options for nested flows.
1690
1949
  */
1691
- const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
1692
- const parent = typeof parentJson === 'string' && parentJson.length > 0
1693
- ? JSON.parse(parentJson)
1694
- : undefined;
1695
- const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
1696
- const current = { ...rest };
1697
- if (typeof scripts === 'string') {
1698
- try {
1699
- current.scripts = JSON.parse(scripts);
1700
- }
1701
- catch {
1702
- // ignore parse errors; leave scripts undefined
1703
- }
1704
- }
1705
- const merged = defaultsDeep({}, defaults, parent ?? {}, current);
1706
- const d = defaults;
1707
- setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
1708
- setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
1709
- setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
1710
- setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
1711
- setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
1712
- setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
1713
- setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
1714
- setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
1715
- // warnEntropy (tri-state)
1716
- setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
1717
- // Normalize shell for predictability: explicit default shell per OS.
1718
- const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
1719
- let resolvedShell = merged.shell;
1720
- if (shellOff)
1721
- resolvedShell = false;
1722
- else if (resolvedShell === true || resolvedShell === undefined) {
1723
- resolvedShell = defaultShell;
1724
- }
1725
- else if (typeof resolvedShell !== 'string' &&
1726
- typeof defaults.shell === 'string') {
1727
- resolvedShell = defaults.shell;
1950
+ class GetDotenvCli extends GetDotenvCli$1 {
1951
+ /**
1952
+ * Attach legacy root flags to this CLI instance. Defaults come from
1953
+ * baseRootOptionDefaults when none are provided.
1954
+ */
1955
+ attachRootOptions(defaults, opts) {
1956
+ const d = (defaults ?? baseRootOptionDefaults);
1957
+ attachRootOptions(this, d, opts);
1958
+ return this;
1728
1959
  }
1729
- merged.shell = resolvedShell;
1730
- const cmd = typeof command === 'string' ? command : undefined;
1731
- return cmd !== undefined ? { merged, command: cmd } : { merged };
1732
- };
1733
-
1734
- GetDotenvCli.prototype.attachRootOptions = function (defaults, opts) {
1735
- const d = (defaults ?? baseRootOptionDefaults);
1736
- attachRootOptions(this, d, opts);
1737
- return this;
1738
- };
1739
- GetDotenvCli.prototype.passOptions = function (defaults) {
1740
- const d = (defaults ?? baseRootOptionDefaults);
1741
- this.hook('preSubcommand', async (thisCommand) => {
1742
- const raw = thisCommand.opts();
1743
- const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
1744
- // Persist merged options for nested invocations (batch exec).
1745
- thisCommand.getDotenvCliOptions =
1746
- merged;
1747
- // Also store on the host for downstream ergonomic accessors.
1748
- this._setOptionsBag(merged);
1749
- // Build service options and compute context (always-on config loader path).
1750
- const serviceOptions = getDotenvCliOptions2Options(merged);
1751
- await this.resolveAndLoad(serviceOptions);
1752
- // Global validation: once after Phase C using config sources.
1753
- try {
1754
- const ctx = this.getCtx();
1755
- const dotenv = (ctx?.dotenv ?? {});
1756
- const sources = await resolveGetDotenvConfigSources((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cliHost.cjs', document.baseURI).href)));
1757
- const issues = validateEnvAgainstSources(dotenv, sources);
1758
- if (Array.isArray(issues) && issues.length > 0) {
1759
- const logger = (merged.logger ??
1760
- console);
1761
- const emit = logger.error ?? logger.log;
1762
- issues.forEach((m) => {
1763
- emit(m);
1764
- });
1765
- if (merged.strict) {
1766
- // Deterministic failure under strict mode
1767
- process.exit(1);
1768
- }
1769
- }
1770
- }
1771
- catch {
1772
- // Be tolerant: validation errors reported above; unexpected failures here
1773
- // should not crash non-strict flows.
1774
- }
1775
- });
1776
- // Also handle root-level flows (no subcommand) so option-aliases can run
1777
- // with the same merged options and context without duplicating logic.
1778
- this.hook('preAction', async (thisCommand) => {
1779
- const raw = thisCommand.opts();
1780
- const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
1781
- thisCommand.getDotenvCliOptions =
1782
- merged;
1783
- this._setOptionsBag(merged);
1784
- // Avoid duplicate heavy work if a context is already present.
1785
- if (!this.getCtx()) {
1960
+ /**
1961
+ * Install preSubcommand/preAction hooks that:
1962
+ * - Merge options (parent round-trip + current invocation) using resolveCliOptions.
1963
+ * - Persist the merged bag on the current command and on the host (for ergonomics).
1964
+ * - Compute the dotenv context once via resolveAndLoad(serviceOptions).
1965
+ * - Validate the composed env against discovered config (warn or --strict fail).
1966
+ */
1967
+ passOptions(defaults) {
1968
+ const d = (defaults ?? baseRootOptionDefaults);
1969
+ this.hook('preSubcommand', async (thisCommand) => {
1970
+ const raw = thisCommand.opts();
1971
+ const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
1972
+ // Persist merged options (for nested behavior and ergonomic access).
1973
+ thisCommand.getDotenvCliOptions =
1974
+ merged;
1975
+ this._setOptionsBag(merged);
1976
+ // Build service options and compute context (always-on loader path).
1786
1977
  const serviceOptions = getDotenvCliOptions2Options(merged);
1787
1978
  await this.resolveAndLoad(serviceOptions);
1979
+ // Refresh dynamic option descriptions using resolved config + plugin slices
1980
+ try {
1981
+ const ctx = this.getCtx();
1982
+ this.evaluateDynamicOptions({
1983
+ ...ctx?.optionsResolved,
1984
+ plugins: ctx?.pluginConfigs ?? {},
1985
+ });
1986
+ }
1987
+ catch {
1988
+ /* best-effort */
1989
+ }
1990
+ // Global validation: once after Phase C using config sources.
1788
1991
  try {
1789
1992
  const ctx = this.getCtx();
1790
1993
  const dotenv = (ctx?.dotenv ?? {});
@@ -1803,43 +2006,56 @@ GetDotenvCli.prototype.passOptions = function (defaults) {
1803
2006
  }
1804
2007
  }
1805
2008
  catch {
1806
- // Tolerate validation side-effects in non-strict mode
2009
+ // Be tolerant: do not crash non-strict flows on unexpected validator failures.
1807
2010
  }
1808
- }
1809
- });
1810
- return this;
1811
- };
1812
-
1813
- /** src/cliHost/definePlugin.ts
1814
- * Plugin contracts for the GetDotenv CLI host.
1815
- *
1816
- * This module exposes a structural public interface for the host that plugins
1817
- * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
1818
- * nominal class identity issues (private fields) in downstream consumers.
1819
- */
1820
- /**
1821
- * Define a GetDotenv CLI plugin with compositional helpers.
1822
- *
1823
- * @example
1824
- * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
1825
- * .use(childA)
1826
- * .use(childB);
1827
- */
1828
- const definePlugin = (spec) => {
1829
- const { children = [], ...rest } = spec;
1830
- const plugin = {
1831
- ...rest,
1832
- children: [...children],
1833
- use(child) {
1834
- this.children.push(child);
1835
- return this;
1836
- },
1837
- };
1838
- return plugin;
1839
- };
1840
-
1841
- // Ensure attachRootOptions() and passOptions() are available whenever the
1842
- // /cliHost subpath is imported (unconditional for downstream hosts).
2011
+ });
2012
+ // Also handle root-level flows (no subcommand) so option-aliases can run
2013
+ // with the same merged options and context without duplicating logic.
2014
+ this.hook('preAction', async (thisCommand) => {
2015
+ const raw = thisCommand.opts();
2016
+ const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
2017
+ thisCommand.getDotenvCliOptions =
2018
+ merged;
2019
+ this._setOptionsBag(merged);
2020
+ // Avoid duplicate heavy work if a context is already present.
2021
+ if (!this.getCtx()) {
2022
+ const serviceOptions = getDotenvCliOptions2Options(merged);
2023
+ await this.resolveAndLoad(serviceOptions);
2024
+ try {
2025
+ const ctx = this.getCtx();
2026
+ this.evaluateDynamicOptions({
2027
+ ...ctx?.optionsResolved,
2028
+ plugins: ctx?.pluginConfigs ?? {},
2029
+ });
2030
+ }
2031
+ catch {
2032
+ /* tolerate */
2033
+ }
2034
+ try {
2035
+ const ctx = this.getCtx();
2036
+ const dotenv = (ctx?.dotenv ?? {});
2037
+ const sources = await resolveGetDotenvConfigSources((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cliHost.cjs', document.baseURI).href)));
2038
+ const issues = validateEnvAgainstSources(dotenv, sources);
2039
+ if (Array.isArray(issues) && issues.length > 0) {
2040
+ const logger = (merged
2041
+ .logger ?? console);
2042
+ const emit = logger.error ?? logger.log;
2043
+ issues.forEach((m) => {
2044
+ emit(m);
2045
+ });
2046
+ if (merged.strict) {
2047
+ process.exit(1);
2048
+ }
2049
+ }
2050
+ }
2051
+ catch {
2052
+ // Tolerate validation side-effects in non-strict mode.
2053
+ }
2054
+ }
2055
+ });
2056
+ return this;
2057
+ }
2058
+ }
1843
2059
  /**
1844
2060
  * Helper to retrieve the merged root options bag from any action handler
1845
2061
  * that only has access to thisCommand. Avoids structural casts.