@karmaniverous/get-dotenv 5.2.5 → 5.2.6

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.
package/dist/index.cjs CHANGED
@@ -3,10 +3,10 @@
3
3
  var commander = require('commander');
4
4
  var fs = require('fs-extra');
5
5
  var packageDirectory = require('package-directory');
6
- var url = require('url');
7
6
  var path = require('path');
8
- var zod = require('zod');
7
+ var url = require('url');
9
8
  var YAML = require('yaml');
9
+ var zod = require('zod');
10
10
  var nanoid = require('nanoid');
11
11
  var dotenv = require('dotenv');
12
12
  var crypto = require('crypto');
@@ -16,6 +16,258 @@ var node_process = require('node:process');
16
16
  var promises = require('readline/promises');
17
17
 
18
18
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
19
+ /**
20
+ * Dotenv expansion utilities.
21
+ *
22
+ * This module implements recursive expansion of environment-variable
23
+ * references in strings and records. It supports both whitespace and
24
+ * bracket syntaxes with optional defaults:
25
+ *
26
+ * - Whitespace: `$VAR[:default]`
27
+ * - Bracketed: `${VAR[:default]}`
28
+ *
29
+ * Escaped dollar signs (`\$`) are preserved.
30
+ * Unknown variables resolve to empty string unless a default is provided.
31
+ */
32
+ /**
33
+ * Like String.prototype.search but returns the last index.
34
+ * @internal
35
+ */
36
+ const searchLast = (str, rgx) => {
37
+ const matches = Array.from(str.matchAll(rgx));
38
+ return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
39
+ };
40
+ const replaceMatch = (value, match, ref) => {
41
+ /**
42
+ * @internal
43
+ */
44
+ const group = match[0];
45
+ const key = match[1];
46
+ const defaultValue = match[2];
47
+ if (!key)
48
+ return value;
49
+ const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
50
+ return interpolate(replacement, ref);
51
+ };
52
+ const interpolate = (value = '', ref = {}) => {
53
+ /**
54
+ * @internal
55
+ */
56
+ // if value is falsy, return it as is
57
+ if (!value)
58
+ return value;
59
+ // get position of last unescaped dollar sign
60
+ const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
61
+ // return value if none found
62
+ if (lastUnescapedDollarSignIndex === -1)
63
+ return value;
64
+ // evaluate the value tail
65
+ const tail = value.slice(lastUnescapedDollarSignIndex);
66
+ // find whitespace pattern: $KEY:DEFAULT
67
+ const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
68
+ const whitespaceMatch = whitespacePattern.exec(tail);
69
+ if (whitespaceMatch != null)
70
+ return replaceMatch(value, whitespaceMatch, ref);
71
+ else {
72
+ // find bracket pattern: ${KEY:DEFAULT}
73
+ const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
74
+ const bracketMatch = bracketPattern.exec(tail);
75
+ if (bracketMatch != null)
76
+ return replaceMatch(value, bracketMatch, ref);
77
+ }
78
+ return value;
79
+ };
80
+ /**
81
+ * Recursively expands environment variables in a string. Variables may be
82
+ * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
83
+ * Unknown variables will expand to an empty string.
84
+ *
85
+ * @param value - The string to expand.
86
+ * @param ref - The reference object to use for variable expansion.
87
+ * @returns The expanded string.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * process.env.FOO = 'bar';
92
+ * dotenvExpand('Hello $FOO'); // "Hello bar"
93
+ * dotenvExpand('Hello $BAZ:world'); // "Hello world"
94
+ * ```
95
+ *
96
+ * @remarks
97
+ * The expansion is recursive. If a referenced variable itself contains
98
+ * references, those will also be expanded until a stable value is reached.
99
+ * Escaped references (e.g. `\$FOO`) are preserved as literals.
100
+ */
101
+ const dotenvExpand = (value, ref = process.env) => {
102
+ const result = interpolate(value, ref);
103
+ return result ? result.replace(/\\\$/g, '$') : undefined;
104
+ };
105
+ /**
106
+ * Recursively expands environment variables in the values of a JSON object.
107
+ * Variables may be presented with optional default as `$VAR[:default]` or
108
+ * `${VAR[:default]}`. Unknown variables will expand to an empty string.
109
+ *
110
+ * @param values - The values object to expand.
111
+ * @param options - Expansion options.
112
+ * @returns The value object with expanded string values.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * process.env.FOO = 'bar';
117
+ * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
118
+ * // => { A: "bar", B: "xbary" }
119
+ * ```
120
+ *
121
+ * @remarks
122
+ * Options:
123
+ * - ref: The reference object to use for expansion (defaults to process.env).
124
+ * - progressive: Whether to progressively add expanded values to the set of
125
+ * reference keys.
126
+ *
127
+ * When `progressive` is true, each expanded key becomes available for
128
+ * subsequent expansions in the same object (left-to-right by object key order).
129
+ */
130
+ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
131
+ const { ref = process.env, progressive = false } = options;
132
+ acc[key] = dotenvExpand(values[key], {
133
+ ...ref,
134
+ ...(progressive ? acc : {}),
135
+ });
136
+ return acc;
137
+ }, {});
138
+ /**
139
+ * Recursively expands environment variables in a string using `process.env` as
140
+ * the expansion reference. Variables may be presented with optional default as
141
+ * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
142
+ * empty string.
143
+ *
144
+ * @param value - The string to expand.
145
+ * @returns The expanded string.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * process.env.FOO = 'bar';
150
+ * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
151
+ * ```
152
+ */
153
+ const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
154
+
155
+ /**
156
+ * Attach legacy root flags to a Commander program.
157
+ * Uses provided defaults to render help labels without coupling to generators.
158
+ */
159
+ const attachRootOptions = (program, defaults, opts) => {
160
+ // Install temporary wrappers to tag all options added here as "base".
161
+ const GROUP = 'base';
162
+ const tagLatest = (cmd, group) => {
163
+ const optsArr = cmd.options;
164
+ if (Array.isArray(optsArr) && optsArr.length > 0) {
165
+ const last = optsArr[optsArr.length - 1];
166
+ last.__group = group;
167
+ }
168
+ };
169
+ const originalAddOption = program.addOption.bind(program);
170
+ const originalOption = program.option.bind(program);
171
+ program.addOption = function patchedAdd(opt) {
172
+ // Tag before adding, in case consumers inspect the Option directly.
173
+ opt.__group = GROUP;
174
+ const ret = originalAddOption(opt);
175
+ return ret;
176
+ };
177
+ program.option = function patchedOption(...args) {
178
+ const ret = originalOption(...args);
179
+ tagLatest(this, GROUP);
180
+ return ret;
181
+ };
182
+ const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
183
+ const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
184
+ const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
185
+ // Build initial chain.
186
+ let p = program
187
+ .enablePositionalOptions()
188
+ .passThroughOptions()
189
+ .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
190
+ p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
191
+ ['KEY1', 'VAL1'],
192
+ ['KEY2', 'VAL2'],
193
+ ]
194
+ .map((v) => v.join(va))
195
+ .join(vd)}`, dotenvExpandFromProcessEnv);
196
+ // Optional legacy root command flag (kept for generated CLI compatibility).
197
+ // Default is OFF; the generator opts in explicitly.
198
+ if (opts?.includeCommandOption === true) {
199
+ p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
200
+ }
201
+ p = p
202
+ .option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
203
+ .addOption(new commander.Option('-s, --shell [string]', (() => {
204
+ let defaultLabel = '';
205
+ if (shell !== undefined) {
206
+ if (typeof shell === 'boolean') {
207
+ defaultLabel = ' (default OS shell)';
208
+ }
209
+ else if (typeof shell === 'string') {
210
+ // Safe string interpolation
211
+ defaultLabel = ` (default ${shell})`;
212
+ }
213
+ }
214
+ return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
215
+ })()).conflicts('shellOff'))
216
+ .addOption(new commander.Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
217
+ .addOption(new commander.Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
218
+ .addOption(new commander.Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
219
+ .addOption(new commander.Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
220
+ ((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
221
+ ? ' (default)'
222
+ : ''}`).conflicts('excludeAllOff'))
223
+ .addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
224
+ .addOption(new commander.Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
225
+ .addOption(new commander.Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
226
+ .addOption(new commander.Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
227
+ .addOption(new commander.Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
228
+ .addOption(new commander.Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
229
+ .addOption(new commander.Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
230
+ .addOption(new commander.Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
231
+ .addOption(new commander.Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
232
+ .addOption(new commander.Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
233
+ .addOption(new commander.Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
234
+ .addOption(new commander.Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
235
+ .addOption(new commander.Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
236
+ .option('--capture', 'capture child process stdio for commands (tests/CI)')
237
+ .option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
238
+ .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
239
+ .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
240
+ .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
241
+ .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
242
+ .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
243
+ .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
244
+ .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
245
+ .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
246
+ .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
247
+ .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
248
+ .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
249
+ // Hidden scripts pipe-through (stringified)
250
+ .addOption(new commander.Option('--scripts <string>')
251
+ .default(JSON.stringify(scripts))
252
+ .hideHelp());
253
+ // Diagnostics: opt-in tracing; optional variadic keys after the flag.
254
+ p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
255
+ // Validation: strict mode fails on env validation issues (warn by default).
256
+ p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
257
+ // Entropy diagnostics (presentation-only)
258
+ p = p
259
+ .addOption(new commander.Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
260
+ .addOption(new commander.Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
261
+ .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
262
+ .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
263
+ .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
264
+ .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
265
+ // Restore original methods to avoid tagging future additions outside base.
266
+ program.addOption = originalAddOption;
267
+ program.option = originalOption;
268
+ return p;
269
+ };
270
+
19
271
  // Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
20
272
  const baseRootOptionDefaults = {
21
273
  dotenvToken: '.env',
@@ -43,8 +295,6 @@ const baseRootOptionDefaults = {
43
295
  // (debug/log/exclude* resolved via flag utils)
44
296
  };
45
297
 
46
- const baseGetDotenvCliOptions = baseRootOptionDefaults;
47
-
48
298
  /** @internal */
49
299
  const isPlainObject$1 = (value) => value !== null &&
50
300
  typeof value === 'object' &&
@@ -87,141 +337,130 @@ const defaultsDeep = (...layers) => {
87
337
  return result;
88
338
  };
89
339
 
90
- // src/GetDotenvOptions.ts
91
- const getDotenvOptionsFilename = 'getdotenv.config.json';
92
340
  /**
93
- * Helper to define a dynamic map with strong inference.
341
+ * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
342
+ * - If the user explicitly enabled the flag, return true.
343
+ * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
344
+ * - Otherwise, adopt the default (true → set; false/undefined → unset).
345
+ *
346
+ * @param exclude - The "on" flag value as parsed by Commander.
347
+ * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
348
+ * @param defaultValue - The generator default to adopt when no explicit toggle is present.
349
+ * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
94
350
  *
95
351
  * @example
96
- * const dynamic = defineDynamic(\{ KEY: (\{ FOO = '' \}) =\> FOO + '-x' \});
352
+ * ```ts
353
+ * resolveExclusion(undefined, undefined, true); // => true
354
+ * ```
97
355
  */
98
- const defineDynamic = (d) => d;
356
+ const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
99
357
  /**
100
- * Converts programmatic CLI options to `getDotenv` options. *
101
- * @param cliOptions - CLI options. Defaults to `{}`.
358
+ * Resolve an optional flag with "--exclude-all" overrides.
359
+ * If excludeAll is set and the individual "...-off" is not, force true.
360
+ * If excludeAllOff is set and the individual flag is not explicitly set, unset.
361
+ * Otherwise, adopt the default (true → set; false/undefined → unset).
102
362
  *
103
- * @returns `getDotenv` options.
363
+ * @param exclude - Individual include/exclude flag.
364
+ * @param excludeOff - Individual "...-off" flag.
365
+ * @param defaultValue - Default for the individual flag.
366
+ * @param excludeAll - Global "exclude-all" flag.
367
+ * @param excludeAllOff - Global "exclude-all-off" flag.
368
+ *
369
+ * @example
370
+ * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
104
371
  */
105
- const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
106
- /**
107
- * Convert CLI-facing string options into {@link GetDotenvOptions}.
108
- *
109
- * - 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`
110
- * pairs (configurable delimiters) into a {@link ProcessEnv}.
111
- * - Drops CLI-only keys that have no programmatic equivalent.
112
- *
113
- * @remarks
114
- * Follows exact-optional semantics by not emitting undefined-valued entries.
115
- */
116
- // Drop CLI-only keys (debug/scripts) without relying on Record casts.
117
- // Create a shallow copy then delete optional CLI-only keys if present.
118
- const restObj = { ...rest };
119
- delete restObj.debug;
120
- delete restObj.scripts;
121
- const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
122
- // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
123
- let parsedVars;
124
- if (typeof vars === 'string') {
125
- const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
126
- ? RegExp(varsAssignorPattern)
127
- : (varsAssignor ?? '=')));
128
- parsedVars = Object.fromEntries(kvPairs);
129
- }
130
- else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
131
- // Keep only string or undefined values to match ProcessEnv.
132
- const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
133
- parsedVars = Object.fromEntries(entries);
134
- }
135
- // Drop undefined-valued entries at the converter stage to match ProcessEnv
136
- // expectations and the compat test assertions.
137
- if (parsedVars) {
138
- parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
139
- }
140
- // Tolerate paths as either a delimited string or string[]
141
- // Use a locally cast union type to avoid lint warnings about always-falsy conditions
142
- // under the RootOptionsShape (which declares paths as string | undefined).
143
- const pathsAny = paths;
144
- const pathsOut = Array.isArray(pathsAny)
145
- ? pathsAny.filter((p) => typeof p === 'string')
146
- : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
147
- // Preserve exactOptionalPropertyTypes: only include keys when defined.
148
- return {
149
- ...restObj,
150
- ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
151
- ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
152
- };
153
- };
154
- const resolveGetDotenvOptions = async (customOptions) => {
155
- /**
156
- * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
157
- *
158
- * 1. Base defaults derived from the CLI generator defaults
159
- * ({@link baseGetDotenvCliOptions}).
160
- * 2. Local project overrides from a `getdotenv.config.json` in the nearest
161
- * package root (if present).
162
- * 3. The provided {@link customOptions}.
163
- *
164
- * The result preserves explicit empty values and drops only `undefined`.
165
- *
166
- * @returns Fully-resolved {@link GetDotenvOptions}.
167
- *
168
- * @example
169
- * ```ts
170
- * const options = await resolveGetDotenvOptions({ env: 'dev' });
171
- * ```
172
- */
173
- const localPkgDir = await packageDirectory.packageDirectory();
174
- const localOptionsPath = localPkgDir
175
- ? path.join(localPkgDir, getDotenvOptionsFilename)
176
- : undefined;
177
- const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
178
- ? JSON.parse((await fs.readFile(localOptionsPath)).toString())
179
- : {});
180
- // Merge order: base < local < custom (custom has highest precedence)
181
- const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
182
- const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
183
- const result = defaultsDeep(defaultsFromCli, customOptions);
184
- return {
185
- ...result, // Keep explicit empty strings/zeros; drop only undefined
186
- vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
187
- };
372
+ const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
373
+ // Order of precedence:
374
+ // 1) Individual explicit "on" wins outright.
375
+ // 2) Individual explicit "off" wins over any global.
376
+ // 3) Global exclude-all forces true when not explicitly turned off.
377
+ // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
378
+ // 5) Fall back to the default (true => set; false/undefined => unset).
379
+ (() => {
380
+ // Individual "on"
381
+ if (exclude === true)
382
+ return true;
383
+ // Individual "off"
384
+ if (excludeOff === true)
385
+ return undefined;
386
+ // Global "exclude-all" ON (unless explicitly turned off)
387
+ if (excludeAll === true)
388
+ return true;
389
+ // Global "exclude-all-off" (unless explicitly enabled)
390
+ if (excludeAllOff === true)
391
+ return undefined;
392
+ // Default
393
+ return defaultValue ? true : undefined;
394
+ })();
395
+ /**
396
+ * exactOptionalPropertyTypes-safe setter for optional boolean flags:
397
+ * delete when undefined; assign when defined without requiring an index signature on T.
398
+ *
399
+ * @typeParam T - Target object type.
400
+ * @param obj - The object to write to.
401
+ * @param key - The optional boolean property key of {@link T}.
402
+ * @param value - The value to set or `undefined` to unset.
403
+ *
404
+ * @remarks
405
+ * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
406
+ */
407
+ const setOptionalFlag = (obj, key, value) => {
408
+ const target = obj;
409
+ const k = key;
410
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
411
+ if (value === undefined)
412
+ delete target[k];
413
+ else
414
+ target[k] = value;
188
415
  };
189
416
 
190
417
  /**
191
- * Zod schemas for programmatic GetDotenv options.
192
- *
193
- * NOTE: These schemas are introduced without wiring to avoid behavior changes.
194
- * Legacy paths continue to use existing types/logic. The new plugin host will
195
- * use these schemas in strict mode; legacy paths will adopt them in warn mode
196
- * later per the staged plan.
418
+ * Merge and normalize raw Commander options (current + parent + defaults)
419
+ * into a GetDotenvCliOptions-like object. Types are intentionally wide to
420
+ * avoid cross-layer coupling; callers may cast as needed.
197
421
  */
198
- // Minimal process env representation: string values or undefined to indicate "unset".
199
- const processEnvSchema = zod.z.record(zod.z.string(), zod.z.string().optional());
200
- // RAW: all fields optional — undefined means "inherit" from lower layers.
201
- const getDotenvOptionsSchemaRaw = zod.z.object({
202
- defaultEnv: zod.z.string().optional(),
203
- dotenvToken: zod.z.string().optional(),
204
- dynamicPath: zod.z.string().optional(),
205
- // Dynamic map is intentionally wide for now; refine once sources are normalized.
206
- dynamic: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
207
- env: zod.z.string().optional(),
208
- excludeDynamic: zod.z.boolean().optional(),
209
- excludeEnv: zod.z.boolean().optional(),
210
- excludeGlobal: zod.z.boolean().optional(),
211
- excludePrivate: zod.z.boolean().optional(),
212
- excludePublic: zod.z.boolean().optional(),
213
- loadProcess: zod.z.boolean().optional(),
214
- log: zod.z.boolean().optional(),
215
- outputPath: zod.z.string().optional(),
216
- paths: zod.z.array(zod.z.string()).optional(),
217
- privateToken: zod.z.string().optional(),
218
- vars: processEnvSchema.optional(),
219
- // Host-only feature flag: guarded integration of config loader/overlay
220
- useConfigLoader: zod.z.boolean().optional(),
221
- });
222
- // RESOLVED: service-boundary contract (post-inheritance).
223
- // For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
224
- const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
422
+ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
423
+ const parent = typeof parentJson === 'string' && parentJson.length > 0
424
+ ? JSON.parse(parentJson)
425
+ : undefined;
426
+ const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
427
+ const current = { ...rest };
428
+ if (typeof scripts === 'string') {
429
+ try {
430
+ current.scripts = JSON.parse(scripts);
431
+ }
432
+ catch {
433
+ // ignore parse errors; leave scripts undefined
434
+ }
435
+ }
436
+ const merged = defaultsDeep({}, defaults, parent ?? {}, current);
437
+ const d = defaults;
438
+ setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
439
+ setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
440
+ setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
441
+ setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
442
+ setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
443
+ setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
444
+ setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
445
+ setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
446
+ // warnEntropy (tri-state)
447
+ setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
448
+ // Normalize shell for predictability: explicit default shell per OS.
449
+ const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
450
+ let resolvedShell = merged.shell;
451
+ if (shellOff)
452
+ resolvedShell = false;
453
+ else if (resolvedShell === true || resolvedShell === undefined) {
454
+ resolvedShell = defaultShell;
455
+ }
456
+ else if (typeof resolvedShell !== 'string' &&
457
+ typeof defaults.shell === 'string') {
458
+ resolvedShell = defaults.shell;
459
+ }
460
+ merged.shell = resolvedShell;
461
+ const cmd = typeof command === 'string' ? command : undefined;
462
+ return cmd !== undefined ? { merged, command: cmd } : { merged };
463
+ };
225
464
 
226
465
  /**
227
466
  * Zod schemas for configuration files discovered by the new loader.
@@ -461,140 +700,198 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
461
700
  };
462
701
 
463
702
  /**
464
- * Dotenv expansion utilities.
465
- *
466
- * This module implements recursive expansion of environment-variable
467
- * references in strings and records. It supports both whitespace and
468
- * bracket syntaxes with optional defaults:
469
- *
470
- * - Whitespace: `$VAR[:default]`
471
- * - Bracketed: `${VAR[:default]}`
703
+ * Validate a composed env against config-provided validation surfaces.
704
+ * Precedence for validation definitions:
705
+ * project.local -\> project.public -\> packaged
472
706
  *
473
- * Escaped dollar signs (`\$`) are preserved.
474
- * Unknown variables resolve to empty string unless a default is provided.
475
- */
476
- /**
477
- * Like String.prototype.search but returns the last index.
478
- * @internal
707
+ * Behavior:
708
+ * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
709
+ * - Else if `requiredKeys` is present, check presence (value !== undefined).
710
+ * - Returns a flat list of issue strings; caller decides warn vs fail.
479
711
  */
480
- const searchLast = (str, rgx) => {
481
- const matches = Array.from(str.matchAll(rgx));
482
- return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
483
- };
484
- const replaceMatch = (value, match, ref) => {
485
- /**
486
- * @internal
487
- */
488
- const group = match[0];
489
- const key = match[1];
490
- const defaultValue = match[2];
491
- if (!key)
492
- return value;
493
- const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
494
- return interpolate(replacement, ref);
495
- };
496
- const interpolate = (value = '', ref = {}) => {
497
- /**
498
- * @internal
499
- */
500
- // if value is falsy, return it as is
501
- if (!value)
502
- return value;
503
- // get position of last unescaped dollar sign
504
- const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
505
- // return value if none found
506
- if (lastUnescapedDollarSignIndex === -1)
507
- return value;
508
- // evaluate the value tail
509
- const tail = value.slice(lastUnescapedDollarSignIndex);
510
- // find whitespace pattern: $KEY:DEFAULT
511
- const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
512
- const whitespaceMatch = whitespacePattern.exec(tail);
513
- if (whitespaceMatch != null)
514
- return replaceMatch(value, whitespaceMatch, ref);
515
- else {
516
- // find bracket pattern: ${KEY:DEFAULT}
517
- const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
518
- const bracketMatch = bracketPattern.exec(tail);
519
- if (bracketMatch != null)
520
- return replaceMatch(value, bracketMatch, ref);
712
+ const validateEnvAgainstSources = (finalEnv, sources) => {
713
+ const pick = (getter) => {
714
+ const pl = sources.project?.local;
715
+ const pp = sources.project?.public;
716
+ const pk = sources.packaged;
717
+ return ((pl && getter(pl)) ||
718
+ (pp && getter(pp)) ||
719
+ (pk && getter(pk)) ||
720
+ undefined);
721
+ };
722
+ const schema = pick((cfg) => cfg['schema']);
723
+ if (schema &&
724
+ typeof schema.safeParse === 'function') {
725
+ try {
726
+ const parsed = schema.safeParse(finalEnv);
727
+ if (!parsed.success) {
728
+ // Try to render zod-style issues when available.
729
+ const err = parsed.error;
730
+ const issues = Array.isArray(err.issues) && err.issues.length > 0
731
+ ? err.issues.map((i) => {
732
+ const path = Array.isArray(i.path) ? i.path.join('.') : '';
733
+ const msg = i.message ?? 'Invalid value';
734
+ return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
735
+ })
736
+ : ['[schema] validation failed'];
737
+ return issues;
738
+ }
739
+ return [];
740
+ }
741
+ catch {
742
+ // If schema invocation fails, surface a single diagnostic.
743
+ return [
744
+ '[schema] validation failed (unable to execute schema.safeParse)',
745
+ ];
746
+ }
521
747
  }
522
- return value;
748
+ const requiredKeys = pick((cfg) => cfg['requiredKeys']);
749
+ if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
750
+ const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
751
+ if (missing.length > 0) {
752
+ return missing.map((k) => `[requiredKeys] missing: ${k}`);
753
+ }
754
+ }
755
+ return [];
523
756
  };
757
+
758
+ const baseGetDotenvCliOptions = baseRootOptionDefaults;
759
+
760
+ // src/GetDotenvOptions.ts
761
+ const getDotenvOptionsFilename = 'getdotenv.config.json';
524
762
  /**
525
- * Recursively expands environment variables in a string. Variables may be
526
- * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
527
- * Unknown variables will expand to an empty string.
528
- *
529
- * @param value - The string to expand.
530
- * @param ref - The reference object to use for variable expansion.
531
- * @returns The expanded string.
763
+ * Helper to define a dynamic map with strong inference.
532
764
  *
533
765
  * @example
534
- * ```ts
535
- * process.env.FOO = 'bar';
536
- * dotenvExpand('Hello $FOO'); // "Hello bar"
537
- * dotenvExpand('Hello $BAZ:world'); // "Hello world"
538
- * ```
539
- *
540
- * @remarks
541
- * The expansion is recursive. If a referenced variable itself contains
542
- * references, those will also be expanded until a stable value is reached.
543
- * Escaped references (e.g. `\$FOO`) are preserved as literals.
766
+ * const dynamic = defineDynamic(\{ KEY: (\{ FOO = '' \}) =\> FOO + '-x' \});
544
767
  */
545
- const dotenvExpand = (value, ref = process.env) => {
546
- const result = interpolate(value, ref);
547
- return result ? result.replace(/\\\$/g, '$') : undefined;
548
- };
768
+ const defineDynamic = (d) => d;
549
769
  /**
550
- * Recursively expands environment variables in the values of a JSON object.
551
- * Variables may be presented with optional default as `$VAR[:default]` or
552
- * `${VAR[:default]}`. Unknown variables will expand to an empty string.
553
- *
554
- * @param values - The values object to expand.
555
- * @param options - Expansion options.
556
- * @returns The value object with expanded string values.
557
- *
558
- * @example
559
- * ```ts
560
- * process.env.FOO = 'bar';
561
- * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
562
- * // => { A: "bar", B: "xbary" }
563
- * ```
564
- *
565
- * @remarks
566
- * Options:
567
- * - ref: The reference object to use for expansion (defaults to process.env).
568
- * - progressive: Whether to progressively add expanded values to the set of
569
- * reference keys.
770
+ * Converts programmatic CLI options to `getDotenv` options. *
771
+ * @param cliOptions - CLI options. Defaults to `{}`.
570
772
  *
571
- * When `progressive` is true, each expanded key becomes available for
572
- * subsequent expansions in the same object (left-to-right by object key order).
773
+ * @returns `getDotenv` options.
573
774
  */
574
- const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
575
- const { ref = process.env, progressive = false } = options;
576
- acc[key] = dotenvExpand(values[key], {
577
- ...ref,
578
- ...(progressive ? acc : {}),
579
- });
580
- return acc;
581
- }, {});
775
+ const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
776
+ /**
777
+ * Convert CLI-facing string options into {@link GetDotenvOptions}.
778
+ *
779
+ * - 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`
780
+ * pairs (configurable delimiters) into a {@link ProcessEnv}.
781
+ * - Drops CLI-only keys that have no programmatic equivalent.
782
+ *
783
+ * @remarks
784
+ * Follows exact-optional semantics by not emitting undefined-valued entries.
785
+ */
786
+ // Drop CLI-only keys (debug/scripts) without relying on Record casts.
787
+ // Create a shallow copy then delete optional CLI-only keys if present.
788
+ const restObj = { ...rest };
789
+ delete restObj.debug;
790
+ delete restObj.scripts;
791
+ const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
792
+ // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
793
+ let parsedVars;
794
+ if (typeof vars === 'string') {
795
+ const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
796
+ ? RegExp(varsAssignorPattern)
797
+ : (varsAssignor ?? '=')));
798
+ parsedVars = Object.fromEntries(kvPairs);
799
+ }
800
+ else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
801
+ // Keep only string or undefined values to match ProcessEnv.
802
+ const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
803
+ parsedVars = Object.fromEntries(entries);
804
+ }
805
+ // Drop undefined-valued entries at the converter stage to match ProcessEnv
806
+ // expectations and the compat test assertions.
807
+ if (parsedVars) {
808
+ parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
809
+ }
810
+ // Tolerate paths as either a delimited string or string[]
811
+ // Use a locally cast union type to avoid lint warnings about always-falsy conditions
812
+ // under the RootOptionsShape (which declares paths as string | undefined).
813
+ const pathsAny = paths;
814
+ const pathsOut = Array.isArray(pathsAny)
815
+ ? pathsAny.filter((p) => typeof p === 'string')
816
+ : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
817
+ // Preserve exactOptionalPropertyTypes: only include keys when defined.
818
+ return {
819
+ ...restObj,
820
+ ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
821
+ ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
822
+ };
823
+ };
824
+ const resolveGetDotenvOptions = async (customOptions) => {
825
+ /**
826
+ * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
827
+ *
828
+ * 1. Base defaults derived from the CLI generator defaults
829
+ * ({@link baseGetDotenvCliOptions}).
830
+ * 2. Local project overrides from a `getdotenv.config.json` in the nearest
831
+ * package root (if present).
832
+ * 3. The provided {@link customOptions}.
833
+ *
834
+ * The result preserves explicit empty values and drops only `undefined`.
835
+ *
836
+ * @returns Fully-resolved {@link GetDotenvOptions}.
837
+ *
838
+ * @example
839
+ * ```ts
840
+ * const options = await resolveGetDotenvOptions({ env: 'dev' });
841
+ * ```
842
+ */
843
+ const localPkgDir = await packageDirectory.packageDirectory();
844
+ const localOptionsPath = localPkgDir
845
+ ? path.join(localPkgDir, getDotenvOptionsFilename)
846
+ : undefined;
847
+ const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
848
+ ? JSON.parse((await fs.readFile(localOptionsPath)).toString())
849
+ : {});
850
+ // Merge order: base < local < custom (custom has highest precedence)
851
+ const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
852
+ const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
853
+ const result = defaultsDeep(defaultsFromCli, customOptions);
854
+ return {
855
+ ...result, // Keep explicit empty strings/zeros; drop only undefined
856
+ vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
857
+ };
858
+ };
859
+
582
860
  /**
583
- * Recursively expands environment variables in a string using `process.env` as
584
- * the expansion reference. Variables may be presented with optional default as
585
- * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
586
- * empty string.
587
- *
588
- * @param value - The string to expand.
589
- * @returns The expanded string.
861
+ * Zod schemas for programmatic GetDotenv options.
590
862
  *
591
- * @example
592
- * ```ts
593
- * process.env.FOO = 'bar';
594
- * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
595
- * ```
863
+ * NOTE: These schemas are introduced without wiring to avoid behavior changes.
864
+ * Legacy paths continue to use existing types/logic. The new plugin host will
865
+ * use these schemas in strict mode; legacy paths will adopt them in warn mode
866
+ * later per the staged plan.
596
867
  */
597
- const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
868
+ // Minimal process env representation: string values or undefined to indicate "unset".
869
+ const processEnvSchema = zod.z.record(zod.z.string(), zod.z.string().optional());
870
+ // RAW: all fields optional — undefined means "inherit" from lower layers.
871
+ const getDotenvOptionsSchemaRaw = zod.z.object({
872
+ defaultEnv: zod.z.string().optional(),
873
+ dotenvToken: zod.z.string().optional(),
874
+ dynamicPath: zod.z.string().optional(),
875
+ // Dynamic map is intentionally wide for now; refine once sources are normalized.
876
+ dynamic: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
877
+ env: zod.z.string().optional(),
878
+ excludeDynamic: zod.z.boolean().optional(),
879
+ excludeEnv: zod.z.boolean().optional(),
880
+ excludeGlobal: zod.z.boolean().optional(),
881
+ excludePrivate: zod.z.boolean().optional(),
882
+ excludePublic: zod.z.boolean().optional(),
883
+ loadProcess: zod.z.boolean().optional(),
884
+ log: zod.z.boolean().optional(),
885
+ outputPath: zod.z.string().optional(),
886
+ paths: zod.z.array(zod.z.string()).optional(),
887
+ privateToken: zod.z.string().optional(),
888
+ vars: processEnvSchema.optional(),
889
+ // Host-only feature flag: guarded integration of config loader/overlay
890
+ useConfigLoader: zod.z.boolean().optional(),
891
+ });
892
+ // RESOLVED: service-boundary contract (post-inheritance).
893
+ // For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
894
+ const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
598
895
 
599
896
  const applyKv = (current, kv) => {
600
897
  if (!kv || Object.keys(kv).length === 0)
@@ -1208,7 +1505,7 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
1208
1505
  *
1209
1506
  * NOTE: This host is additive and does not alter the legacy CLI.
1210
1507
  */
1211
- class GetDotenvCli extends commander.Command {
1508
+ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1212
1509
  /** Registered top-level plugins (composition happens via .use()) */
1213
1510
  _plugins = [];
1214
1511
  /** One-time installation guard */
@@ -1408,415 +1705,128 @@ class GetDotenvCli extends commander.Command {
1408
1705
  await p.afterResolve(this, ctx);
1409
1706
  for (const child of p.children)
1410
1707
  await run(child);
1411
- };
1412
- for (const p of this._plugins)
1413
- await run(p);
1414
- }
1415
- // Render App/Plugin grouped options appended after default help.
1416
- #renderOptionGroups(cmd) {
1417
- const all = cmd.options ?? [];
1418
- const byGroup = new Map();
1419
- for (const o of all) {
1420
- const opt = o;
1421
- const g = opt.__group;
1422
- if (!g || g === 'base')
1423
- continue; // base handled by default help
1424
- const rows = byGroup.get(g) ?? [];
1425
- rows.push({
1426
- flags: opt.flags ?? '',
1427
- description: opt.description ?? '',
1428
- });
1429
- byGroup.set(g, rows);
1430
- }
1431
- if (byGroup.size === 0)
1432
- return '';
1433
- const renderRows = (title, rows) => {
1434
- const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
1435
- // Sort within group: short-aliased flags first
1436
- rows.sort((a, b) => {
1437
- const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
1438
- const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
1439
- return bS - aS || a.flags.localeCompare(b.flags);
1440
- });
1441
- const lines = rows
1442
- .map((r) => {
1443
- const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
1444
- return ` ${r.flags}${pad}${r.description}`.trimEnd();
1445
- })
1446
- .join('\n');
1447
- return `\n${title}:\n${lines}\n`;
1448
- };
1449
- let out = '';
1450
- // App options (if any)
1451
- const app = byGroup.get('app');
1452
- if (app && app.length > 0) {
1453
- out += renderRows('App options', app);
1454
- }
1455
- // Plugin groups sorted by id
1456
- const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
1457
- pluginKeys.sort((a, b) => a.localeCompare(b));
1458
- for (const k of pluginKeys) {
1459
- const id = k.slice('plugin:'.length) || '(unknown)';
1460
- const rows = byGroup.get(k) ?? [];
1461
- if (rows.length > 0) {
1462
- out += renderRows(`Plugin options — ${id}`, rows);
1463
- }
1464
- }
1465
- return out;
1466
- }
1467
- }
1468
-
1469
- /**
1470
- * Validate a composed env against config-provided validation surfaces.
1471
- * Precedence for validation definitions:
1472
- * project.local -\> project.public -\> packaged
1473
- *
1474
- * Behavior:
1475
- * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
1476
- * - Else if `requiredKeys` is present, check presence (value !== undefined).
1477
- * - Returns a flat list of issue strings; caller decides warn vs fail.
1478
- */
1479
- const validateEnvAgainstSources = (finalEnv, sources) => {
1480
- const pick = (getter) => {
1481
- const pl = sources.project?.local;
1482
- const pp = sources.project?.public;
1483
- const pk = sources.packaged;
1484
- return ((pl && getter(pl)) ||
1485
- (pp && getter(pp)) ||
1486
- (pk && getter(pk)) ||
1487
- undefined);
1488
- };
1489
- const schema = pick((cfg) => cfg['schema']);
1490
- if (schema &&
1491
- typeof schema.safeParse === 'function') {
1492
- try {
1493
- const parsed = schema.safeParse(finalEnv);
1494
- if (!parsed.success) {
1495
- // Try to render zod-style issues when available.
1496
- const err = parsed.error;
1497
- const issues = Array.isArray(err.issues) && err.issues.length > 0
1498
- ? err.issues.map((i) => {
1499
- const path = Array.isArray(i.path) ? i.path.join('.') : '';
1500
- const msg = i.message ?? 'Invalid value';
1501
- return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
1502
- })
1503
- : ['[schema] validation failed'];
1504
- return issues;
1505
- }
1506
- return [];
1507
- }
1508
- catch {
1509
- // If schema invocation fails, surface a single diagnostic.
1510
- return [
1511
- '[schema] validation failed (unable to execute schema.safeParse)',
1512
- ];
1513
- }
1514
- }
1515
- const requiredKeys = pick((cfg) => cfg['requiredKeys']);
1516
- if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
1517
- const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
1518
- if (missing.length > 0) {
1519
- return missing.map((k) => `[requiredKeys] missing: ${k}`);
1520
- }
1521
- }
1522
- return [];
1523
- };
1524
-
1525
- /**
1526
- * Attach legacy root flags to a Commander program.
1527
- * Uses provided defaults to render help labels without coupling to generators.
1528
- */
1529
- const attachRootOptions = (program, defaults, opts) => {
1530
- // Install temporary wrappers to tag all options added here as "base".
1531
- const GROUP = 'base';
1532
- const tagLatest = (cmd, group) => {
1533
- const optsArr = cmd.options;
1534
- if (Array.isArray(optsArr) && optsArr.length > 0) {
1535
- const last = optsArr[optsArr.length - 1];
1536
- last.__group = group;
1537
- }
1538
- };
1539
- const originalAddOption = program.addOption.bind(program);
1540
- const originalOption = program.option.bind(program);
1541
- program.addOption = function patchedAdd(opt) {
1542
- // Tag before adding, in case consumers inspect the Option directly.
1543
- opt.__group = GROUP;
1544
- const ret = originalAddOption(opt);
1545
- return ret;
1546
- };
1547
- program.option = function patchedOption(...args) {
1548
- const ret = originalOption(...args);
1549
- tagLatest(this, GROUP);
1550
- return ret;
1551
- };
1552
- const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
1553
- const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
1554
- const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
1555
- // Build initial chain.
1556
- let p = program
1557
- .enablePositionalOptions()
1558
- .passThroughOptions()
1559
- .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
1560
- p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
1561
- ['KEY1', 'VAL1'],
1562
- ['KEY2', 'VAL2'],
1563
- ]
1564
- .map((v) => v.join(va))
1565
- .join(vd)}`, dotenvExpandFromProcessEnv);
1566
- // Optional legacy root command flag (kept for generated CLI compatibility).
1567
- // Default is OFF; the generator opts in explicitly.
1568
- if (opts?.includeCommandOption === true) {
1569
- p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
1570
- }
1571
- p = p
1572
- .option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
1573
- .addOption(new commander.Option('-s, --shell [string]', (() => {
1574
- let defaultLabel = '';
1575
- if (shell !== undefined) {
1576
- if (typeof shell === 'boolean') {
1577
- defaultLabel = ' (default OS shell)';
1578
- }
1579
- else if (typeof shell === 'string') {
1580
- // Safe string interpolation
1581
- defaultLabel = ` (default ${shell})`;
1582
- }
1583
- }
1584
- return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
1585
- })()).conflicts('shellOff'))
1586
- .addOption(new commander.Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
1587
- .addOption(new commander.Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
1588
- .addOption(new commander.Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
1589
- .addOption(new commander.Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
1590
- ((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
1591
- ? ' (default)'
1592
- : ''}`).conflicts('excludeAllOff'))
1593
- .addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
1594
- .addOption(new commander.Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
1595
- .addOption(new commander.Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
1596
- .addOption(new commander.Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
1597
- .addOption(new commander.Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
1598
- .addOption(new commander.Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
1599
- .addOption(new commander.Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
1600
- .addOption(new commander.Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
1601
- .addOption(new commander.Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
1602
- .addOption(new commander.Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
1603
- .addOption(new commander.Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
1604
- .addOption(new commander.Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
1605
- .addOption(new commander.Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
1606
- .option('--capture', 'capture child process stdio for commands (tests/CI)')
1607
- .option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
1608
- .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
1609
- .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
1610
- .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
1611
- .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
1612
- .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
1613
- .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
1614
- .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
1615
- .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
1616
- .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
1617
- .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
1618
- .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
1619
- // Hidden scripts pipe-through (stringified)
1620
- .addOption(new commander.Option('--scripts <string>')
1621
- .default(JSON.stringify(scripts))
1622
- .hideHelp());
1623
- // Diagnostics: opt-in tracing; optional variadic keys after the flag.
1624
- p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
1625
- // Validation: strict mode fails on env validation issues (warn by default).
1626
- p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
1627
- // Entropy diagnostics (presentation-only)
1628
- p = p
1629
- .addOption(new commander.Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
1630
- .addOption(new commander.Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
1631
- .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
1632
- .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
1633
- .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
1634
- .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
1635
- // Restore original methods to avoid tagging future additions outside base.
1636
- program.addOption = originalAddOption;
1637
- program.option = originalOption;
1638
- return p;
1708
+ };
1709
+ for (const p of this._plugins)
1710
+ await run(p);
1711
+ }
1712
+ // Render App/Plugin grouped options appended after default help.
1713
+ #renderOptionGroups(cmd) {
1714
+ const all = cmd.options ?? [];
1715
+ const byGroup = new Map();
1716
+ for (const o of all) {
1717
+ const opt = o;
1718
+ const g = opt.__group;
1719
+ if (!g || g === 'base')
1720
+ continue; // base handled by default help
1721
+ const rows = byGroup.get(g) ?? [];
1722
+ rows.push({
1723
+ flags: opt.flags ?? '',
1724
+ description: opt.description ?? '',
1725
+ });
1726
+ byGroup.set(g, rows);
1727
+ }
1728
+ if (byGroup.size === 0)
1729
+ return '';
1730
+ const renderRows = (title, rows) => {
1731
+ const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
1732
+ // Sort within group: short-aliased flags first
1733
+ rows.sort((a, b) => {
1734
+ const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
1735
+ const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
1736
+ return bS - aS || a.flags.localeCompare(b.flags);
1737
+ });
1738
+ const lines = rows
1739
+ .map((r) => {
1740
+ const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
1741
+ return ` ${r.flags}${pad}${r.description}`.trimEnd();
1742
+ })
1743
+ .join('\n');
1744
+ return `\n${title}:\n${lines}\n`;
1745
+ };
1746
+ let out = '';
1747
+ // App options (if any)
1748
+ const app = byGroup.get('app');
1749
+ if (app && app.length > 0) {
1750
+ out += renderRows('App options', app);
1751
+ }
1752
+ // Plugin groups sorted by id
1753
+ const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
1754
+ pluginKeys.sort((a, b) => a.localeCompare(b));
1755
+ for (const k of pluginKeys) {
1756
+ const id = k.slice('plugin:'.length) || '(unknown)';
1757
+ const rows = byGroup.get(k) ?? [];
1758
+ if (rows.length > 0) {
1759
+ out += renderRows(`Plugin options — ${id}`, rows);
1760
+ }
1761
+ }
1762
+ return out;
1763
+ }
1639
1764
  };
1640
1765
 
1641
- /**
1642
- * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
1643
- * - If the user explicitly enabled the flag, return true.
1644
- * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
1645
- * - Otherwise, adopt the default (true → set; false/undefined → unset).
1646
- *
1647
- * @param exclude - The "on" flag value as parsed by Commander.
1648
- * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
1649
- * @param defaultValue - The generator default to adopt when no explicit toggle is present.
1650
- * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
1766
+ /** src/cliHost/definePlugin.ts
1767
+ * Plugin contracts for the GetDotenv CLI host.
1651
1768
  *
1652
- * @example
1653
- * ```ts
1654
- * resolveExclusion(undefined, undefined, true); // => true
1655
- * ```
1769
+ * This module exposes a structural public interface for the host that plugins
1770
+ * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
1771
+ * nominal class identity issues (private fields) in downstream consumers.
1656
1772
  */
1657
- const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
1658
1773
  /**
1659
- * Resolve an optional flag with "--exclude-all" overrides.
1660
- * If excludeAll is set and the individual "...-off" is not, force true.
1661
- * If excludeAllOff is set and the individual flag is not explicitly set, unset.
1662
- * Otherwise, adopt the default (true → set; false/undefined → unset).
1663
- *
1664
- * @param exclude - Individual include/exclude flag.
1665
- * @param excludeOff - Individual "...-off" flag.
1666
- * @param defaultValue - Default for the individual flag.
1667
- * @param excludeAll - Global "exclude-all" flag.
1668
- * @param excludeAllOff - Global "exclude-all-off" flag.
1774
+ * Define a GetDotenv CLI plugin with compositional helpers.
1669
1775
  *
1670
1776
  * @example
1671
- * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
1672
- */
1673
- const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
1674
- // Order of precedence:
1675
- // 1) Individual explicit "on" wins outright.
1676
- // 2) Individual explicit "off" wins over any global.
1677
- // 3) Global exclude-all forces true when not explicitly turned off.
1678
- // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
1679
- // 5) Fall back to the default (true => set; false/undefined => unset).
1680
- (() => {
1681
- // Individual "on"
1682
- if (exclude === true)
1683
- return true;
1684
- // Individual "off"
1685
- if (excludeOff === true)
1686
- return undefined;
1687
- // Global "exclude-all" ON (unless explicitly turned off)
1688
- if (excludeAll === true)
1689
- return true;
1690
- // Global "exclude-all-off" (unless explicitly enabled)
1691
- if (excludeAllOff === true)
1692
- return undefined;
1693
- // Default
1694
- return defaultValue ? true : undefined;
1695
- })();
1696
- /**
1697
- * exactOptionalPropertyTypes-safe setter for optional boolean flags:
1698
- * delete when undefined; assign when defined — without requiring an index signature on T.
1699
- *
1700
- * @typeParam T - Target object type.
1701
- * @param obj - The object to write to.
1702
- * @param key - The optional boolean property key of {@link T}.
1703
- * @param value - The value to set or `undefined` to unset.
1704
- *
1705
- * @remarks
1706
- * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
1777
+ * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
1778
+ * .use(childA)
1779
+ * .use(childB);
1707
1780
  */
1708
- const setOptionalFlag = (obj, key, value) => {
1709
- const target = obj;
1710
- const k = key;
1711
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1712
- if (value === undefined)
1713
- delete target[k];
1714
- else
1715
- target[k] = value;
1781
+ const definePlugin = (spec) => {
1782
+ const { children = [], ...rest } = spec;
1783
+ const plugin = {
1784
+ ...rest,
1785
+ children: [...children],
1786
+ use(child) {
1787
+ this.children.push(child);
1788
+ return this;
1789
+ },
1790
+ };
1791
+ return plugin;
1716
1792
  };
1717
1793
 
1718
1794
  /**
1719
- * Merge and normalize raw Commander options (current + parent + defaults)
1720
- * into a GetDotenvCliOptions-like object. Types are intentionally wide to
1721
- * avoid cross-layer coupling; callers may cast as needed.
1795
+ * GetDotenvCli with root helpers as real class methods.
1796
+ * - attachRootOptions: installs legacy/base root flags on the command.
1797
+ * - passOptions: merges flags (parent \< current), computes dotenv context once,
1798
+ * runs validation, and persists merged options for nested flows.
1722
1799
  */
1723
- const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
1724
- const parent = typeof parentJson === 'string' && parentJson.length > 0
1725
- ? JSON.parse(parentJson)
1726
- : undefined;
1727
- const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
1728
- const current = { ...rest };
1729
- if (typeof scripts === 'string') {
1730
- try {
1731
- current.scripts = JSON.parse(scripts);
1732
- }
1733
- catch {
1734
- // ignore parse errors; leave scripts undefined
1735
- }
1736
- }
1737
- const merged = defaultsDeep({}, defaults, parent ?? {}, current);
1738
- const d = defaults;
1739
- setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
1740
- setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
1741
- setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
1742
- setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
1743
- setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
1744
- setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
1745
- setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
1746
- setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
1747
- // warnEntropy (tri-state)
1748
- setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
1749
- // Normalize shell for predictability: explicit default shell per OS.
1750
- const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
1751
- let resolvedShell = merged.shell;
1752
- if (shellOff)
1753
- resolvedShell = false;
1754
- else if (resolvedShell === true || resolvedShell === undefined) {
1755
- resolvedShell = defaultShell;
1756
- }
1757
- else if (typeof resolvedShell !== 'string' &&
1758
- typeof defaults.shell === 'string') {
1759
- resolvedShell = defaults.shell;
1800
+ class GetDotenvCli extends GetDotenvCli$1 {
1801
+ /**
1802
+ * Attach legacy root flags to this CLI instance. Defaults come from
1803
+ * baseRootOptionDefaults when none are provided.
1804
+ */
1805
+ attachRootOptions(defaults, opts) {
1806
+ const d = (defaults ?? baseRootOptionDefaults);
1807
+ attachRootOptions(this, d, opts);
1808
+ return this;
1760
1809
  }
1761
- merged.shell = resolvedShell;
1762
- const cmd = typeof command === 'string' ? command : undefined;
1763
- return cmd !== undefined ? { merged, command: cmd } : { merged };
1764
- };
1765
-
1766
- GetDotenvCli.prototype.attachRootOptions = function (defaults, opts) {
1767
- const d = (defaults ?? baseRootOptionDefaults);
1768
- attachRootOptions(this, d, opts);
1769
- return this;
1770
- };
1771
- GetDotenvCli.prototype.passOptions = function (defaults) {
1772
- const d = (defaults ?? baseRootOptionDefaults);
1773
- this.hook('preSubcommand', async (thisCommand) => {
1774
- const raw = thisCommand.opts();
1775
- const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
1776
- // Persist merged options for nested invocations (batch exec).
1777
- thisCommand.getDotenvCliOptions =
1778
- merged;
1779
- // Also store on the host for downstream ergonomic accessors.
1780
- this._setOptionsBag(merged);
1781
- // Build service options and compute context (always-on config loader path).
1782
- const serviceOptions = getDotenvCliOptions2Options(merged);
1783
- await this.resolveAndLoad(serviceOptions);
1784
- // Global validation: once after Phase C using config sources.
1785
- try {
1786
- const ctx = this.getCtx();
1787
- const dotenv = (ctx?.dotenv ?? {});
1788
- const sources = await resolveGetDotenvConfigSources((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
1789
- const issues = validateEnvAgainstSources(dotenv, sources);
1790
- if (Array.isArray(issues) && issues.length > 0) {
1791
- const logger = (merged.logger ??
1792
- console);
1793
- const emit = logger.error ?? logger.log;
1794
- issues.forEach((m) => {
1795
- emit(m);
1796
- });
1797
- if (merged.strict) {
1798
- // Deterministic failure under strict mode
1799
- process.exit(1);
1800
- }
1801
- }
1802
- }
1803
- catch {
1804
- // Be tolerant: validation errors reported above; unexpected failures here
1805
- // should not crash non-strict flows.
1806
- }
1807
- });
1808
- // Also handle root-level flows (no subcommand) so option-aliases can run
1809
- // with the same merged options and context without duplicating logic.
1810
- this.hook('preAction', async (thisCommand) => {
1811
- const raw = thisCommand.opts();
1812
- const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
1813
- thisCommand.getDotenvCliOptions =
1814
- merged;
1815
- this._setOptionsBag(merged);
1816
- // Avoid duplicate heavy work if a context is already present.
1817
- if (!this.getCtx()) {
1810
+ /**
1811
+ * Install preSubcommand/preAction hooks that:
1812
+ * - Merge options (parent round-trip + current invocation) using resolveCliOptions.
1813
+ * - Persist the merged bag on the current command and on the host (for ergonomics).
1814
+ * - Compute the dotenv context once via resolveAndLoad(serviceOptions).
1815
+ * - Validate the composed env against discovered config (warn or --strict fail).
1816
+ */
1817
+ passOptions(defaults) {
1818
+ const d = (defaults ?? baseRootOptionDefaults);
1819
+ this.hook('preSubcommand', async (thisCommand) => {
1820
+ const raw = thisCommand.opts();
1821
+ const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
1822
+ // Persist merged options (for nested behavior and ergonomic access).
1823
+ thisCommand.getDotenvCliOptions =
1824
+ merged;
1825
+ this._setOptionsBag(merged);
1826
+ // Build service options and compute context (always-on loader path).
1818
1827
  const serviceOptions = getDotenvCliOptions2Options(merged);
1819
1828
  await this.resolveAndLoad(serviceOptions);
1829
+ // Global validation: once after Phase C using config sources.
1820
1830
  try {
1821
1831
  const ctx = this.getCtx();
1822
1832
  const dotenv = (ctx?.dotenv ?? {});
@@ -1835,12 +1845,46 @@ GetDotenvCli.prototype.passOptions = function (defaults) {
1835
1845
  }
1836
1846
  }
1837
1847
  catch {
1838
- // Tolerate validation side-effects in non-strict mode
1848
+ // Be tolerant: do not crash non-strict flows on unexpected validator failures.
1839
1849
  }
1840
- }
1841
- });
1842
- return this;
1843
- };
1850
+ });
1851
+ // Also handle root-level flows (no subcommand) so option-aliases can run
1852
+ // with the same merged options and context without duplicating logic.
1853
+ this.hook('preAction', async (thisCommand) => {
1854
+ const raw = thisCommand.opts();
1855
+ const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
1856
+ thisCommand.getDotenvCliOptions =
1857
+ merged;
1858
+ this._setOptionsBag(merged);
1859
+ // Avoid duplicate heavy work if a context is already present.
1860
+ if (!this.getCtx()) {
1861
+ const serviceOptions = getDotenvCliOptions2Options(merged);
1862
+ await this.resolveAndLoad(serviceOptions);
1863
+ try {
1864
+ const ctx = this.getCtx();
1865
+ const dotenv = (ctx?.dotenv ?? {});
1866
+ const sources = await resolveGetDotenvConfigSources((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
1867
+ const issues = validateEnvAgainstSources(dotenv, sources);
1868
+ if (Array.isArray(issues) && issues.length > 0) {
1869
+ const logger = (merged
1870
+ .logger ?? console);
1871
+ const emit = logger.error ?? logger.log;
1872
+ issues.forEach((m) => {
1873
+ emit(m);
1874
+ });
1875
+ if (merged.strict) {
1876
+ process.exit(1);
1877
+ }
1878
+ }
1879
+ }
1880
+ catch {
1881
+ // Tolerate validation side-effects in non-strict mode.
1882
+ }
1883
+ }
1884
+ });
1885
+ return this;
1886
+ }
1887
+ }
1844
1888
 
1845
1889
  // Minimal tokenizer for shell-off execution:
1846
1890
  // Splits by whitespace while preserving quoted segments (single or double quotes).
@@ -2075,34 +2119,6 @@ const buildSpawnEnv = (base, overlay) => {
2075
2119
  return out;
2076
2120
  };
2077
2121
 
2078
- /** src/cliHost/definePlugin.ts
2079
- * Plugin contracts for the GetDotenv CLI host.
2080
- *
2081
- * This module exposes a structural public interface for the host that plugins
2082
- * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
2083
- * nominal class identity issues (private fields) in downstream consumers.
2084
- */
2085
- /**
2086
- * Define a GetDotenv CLI plugin with compositional helpers.
2087
- *
2088
- * @example
2089
- * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
2090
- * .use(childA)
2091
- * .use(childB);
2092
- */
2093
- const definePlugin = (spec) => {
2094
- const { children = [], ...rest } = spec;
2095
- const plugin = {
2096
- ...rest,
2097
- children: [...children],
2098
- use(child) {
2099
- this.children.push(child);
2100
- return this;
2101
- },
2102
- };
2103
- return plugin;
2104
- };
2105
-
2106
2122
  /**
2107
2123
  * Batch services (neutral): resolve command and shell settings.
2108
2124
  * Shared by the generator path and the batch plugin to avoid circular deps.
@@ -3173,7 +3189,7 @@ const cmdPlugin = (options = {}) => definePlugin({
3173
3189
  const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
3174
3190
  const cmd = new commander.Command()
3175
3191
  .name('cmd')
3176
- .description('Batch execute command according to the --shell option, conflicts with --command option (default subcommand)')
3192
+ .description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
3177
3193
  .configureHelp({ showGlobalOptions: true })
3178
3194
  .enablePositionalOptions()
3179
3195
  .passThroughOptions()