@karmaniverous/get-dotenv 6.0.0-1 → 6.1.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 (62) hide show
  1. package/README.md +91 -379
  2. package/dist/cli.d.ts +569 -0
  3. package/dist/cli.mjs +18877 -0
  4. package/dist/cliHost.d.ts +528 -184
  5. package/dist/cliHost.mjs +1977 -1428
  6. package/dist/config.d.ts +191 -14
  7. package/dist/config.mjs +266 -81
  8. package/dist/env-overlay.d.ts +223 -16
  9. package/dist/env-overlay.mjs +185 -4
  10. package/dist/getdotenv.cli.mjs +18025 -3196
  11. package/dist/index.d.ts +623 -256
  12. package/dist/index.mjs +18045 -3206
  13. package/dist/plugins-aws.d.ts +221 -91
  14. package/dist/plugins-aws.mjs +2411 -369
  15. package/dist/plugins-batch.d.ts +300 -103
  16. package/dist/plugins-batch.mjs +2560 -484
  17. package/dist/plugins-cmd.d.ts +229 -106
  18. package/dist/plugins-cmd.mjs +2518 -790
  19. package/dist/plugins-init.d.ts +221 -95
  20. package/dist/plugins-init.mjs +2170 -105
  21. package/dist/plugins.d.ts +246 -125
  22. package/dist/plugins.mjs +17941 -1968
  23. package/dist/templates/cli/index.ts +25 -0
  24. package/{templates/cli/ts → dist/templates/cli}/plugins/hello.ts +13 -9
  25. package/dist/templates/config/js/getdotenv.config.js +20 -0
  26. package/dist/templates/config/json/local/getdotenv.config.local.json +7 -0
  27. package/dist/templates/config/json/public/getdotenv.config.json +9 -0
  28. package/dist/templates/config/public/getdotenv.config.json +8 -0
  29. package/dist/templates/config/ts/getdotenv.config.ts +28 -0
  30. package/dist/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
  31. package/dist/templates/config/yaml/public/getdotenv.config.yaml +7 -0
  32. package/dist/templates/getdotenv.config.js +20 -0
  33. package/dist/templates/getdotenv.config.json +9 -0
  34. package/dist/templates/getdotenv.config.local.json +7 -0
  35. package/dist/templates/getdotenv.config.local.yaml +7 -0
  36. package/dist/templates/getdotenv.config.ts +28 -0
  37. package/dist/templates/getdotenv.config.yaml +7 -0
  38. package/dist/templates/hello.ts +42 -0
  39. package/dist/templates/index.ts +25 -0
  40. package/dist/templates/js/getdotenv.config.js +20 -0
  41. package/dist/templates/json/local/getdotenv.config.local.json +7 -0
  42. package/dist/templates/json/public/getdotenv.config.json +9 -0
  43. package/dist/templates/local/getdotenv.config.local.json +7 -0
  44. package/dist/templates/local/getdotenv.config.local.yaml +7 -0
  45. package/dist/templates/plugins/hello.ts +42 -0
  46. package/dist/templates/public/getdotenv.config.json +9 -0
  47. package/dist/templates/public/getdotenv.config.yaml +7 -0
  48. package/dist/templates/ts/getdotenv.config.ts +28 -0
  49. package/dist/templates/yaml/local/getdotenv.config.local.yaml +7 -0
  50. package/dist/templates/yaml/public/getdotenv.config.yaml +7 -0
  51. package/getdotenv.config.json +1 -19
  52. package/package.json +42 -39
  53. package/templates/cli/index.ts +25 -0
  54. package/templates/cli/plugins/hello.ts +42 -0
  55. package/templates/config/js/getdotenv.config.js +8 -3
  56. package/templates/config/json/public/getdotenv.config.json +0 -3
  57. package/templates/config/public/getdotenv.config.json +0 -5
  58. package/templates/config/ts/getdotenv.config.ts +8 -3
  59. package/templates/config/yaml/public/getdotenv.config.yaml +0 -3
  60. package/dist/plugins-demo.d.ts +0 -204
  61. package/dist/plugins-demo.mjs +0 -496
  62. package/templates/cli/ts/index.ts +0 -9
package/dist/cliHost.mjs CHANGED
@@ -1,41 +1,125 @@
1
1
  import { z } from 'zod';
2
2
  export { z } from 'zod';
3
+ import path, { join, extname } from 'path';
3
4
  import fs from 'fs-extra';
4
5
  import { packageDirectory } from 'package-directory';
5
- import path, { join, extname } from 'path';
6
- import url, { fileURLToPath, pathToFileURL } from 'url';
6
+ import url, { fileURLToPath } from 'url';
7
7
  import YAML from 'yaml';
8
- import { Option, Command } from 'commander';
8
+ import { createHash } from 'crypto';
9
9
  import { nanoid } from 'nanoid';
10
10
  import { parse } from 'dotenv';
11
- import { createHash } from 'crypto';
11
+ import { execa, execaCommand } from 'execa';
12
+ import { Option, Command } from '@commander-js/extra-typings';
12
13
 
13
- // Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
14
- const baseRootOptionDefaults = {
15
- dotenvToken: '.env',
16
- loadProcess: true,
17
- logger: console,
18
- // Diagnostics defaults
19
- warnEntropy: true,
20
- entropyThreshold: 3.8,
21
- entropyMinLength: 16,
22
- entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
23
- paths: './',
24
- pathsDelimiter: ' ',
25
- privateToken: 'local',
26
- scripts: {
27
- 'git-status': {
28
- cmd: 'git branch --show-current && git status -s -u',
29
- shell: true,
30
- },
31
- },
32
- shell: true,
33
- vars: '',
34
- varsAssignor: '=',
35
- varsDelimiter: ' ',
36
- // tri-state flags default to unset unless explicitly provided
37
- // (debug/log/exclude* resolved via flag utils)
38
- };
14
+ /**
15
+ * Zod schemas for programmatic GetDotenv options.
16
+ *
17
+ * Canonical source of truth for options shape. Public types are derived
18
+ * from these schemas (see consumers via z.output\<\>).
19
+ */
20
+ /**
21
+ * Minimal process env representation used by options and helpers.
22
+ * Values may be `undefined` to indicate "unset".
23
+ */
24
+ const processEnvSchema = z.record(z.string(), z.string().optional());
25
+ // RAW: all fields optional — undefined means "inherit" from lower layers.
26
+ const getDotenvOptionsSchemaRaw = z.object({
27
+ defaultEnv: z.string().optional(),
28
+ dotenvToken: z.string().optional(),
29
+ dynamicPath: z.string().optional(),
30
+ // Dynamic map is intentionally wide for now; refine once sources are normalized.
31
+ dynamic: z.record(z.string(), z.unknown()).optional(),
32
+ env: z.string().optional(),
33
+ excludeDynamic: z.boolean().optional(),
34
+ excludeEnv: z.boolean().optional(),
35
+ excludeGlobal: z.boolean().optional(),
36
+ excludePrivate: z.boolean().optional(),
37
+ excludePublic: z.boolean().optional(),
38
+ loadProcess: z.boolean().optional(),
39
+ log: z.boolean().optional(),
40
+ logger: z.unknown().default(console),
41
+ outputPath: z.string().optional(),
42
+ paths: z.array(z.string()).optional(),
43
+ privateToken: z.string().optional(),
44
+ vars: processEnvSchema.optional(),
45
+ });
46
+ /**
47
+ * Resolved programmatic options schema (post-inheritance).
48
+ * For now, this mirrors the RAW schema; future stages may materialize defaults
49
+ * and narrow shapes as resolution is wired into the host.
50
+ */
51
+ const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
52
+
53
+ /**
54
+ * Zod schemas for CLI-facing GetDotenv options (raw/resolved stubs).
55
+ *
56
+ * RAW allows stringly inputs (paths/vars + splitters). RESOLVED will later
57
+ * reflect normalized types (paths: string[], vars: ProcessEnv), applied in the
58
+ * CLI resolution pipeline.
59
+ */
60
+ const getDotenvCliOptionsSchemaRaw = getDotenvOptionsSchemaRaw.extend({
61
+ // CLI-specific fields (stringly inputs before preprocessing)
62
+ debug: z.boolean().optional(),
63
+ strict: z.boolean().optional(),
64
+ capture: z.boolean().optional(),
65
+ trace: z.union([z.boolean(), z.array(z.string())]).optional(),
66
+ redact: z.boolean().optional(),
67
+ warnEntropy: z.boolean().optional(),
68
+ entropyThreshold: z.number().optional(),
69
+ entropyMinLength: z.number().optional(),
70
+ entropyWhitelist: z.array(z.string()).optional(),
71
+ redactPatterns: z.array(z.string()).optional(),
72
+ paths: z.string().optional(),
73
+ pathsDelimiter: z.string().optional(),
74
+ pathsDelimiterPattern: z.string().optional(),
75
+ scripts: z.record(z.string(), z.unknown()).optional(),
76
+ shell: z.union([z.boolean(), z.string()]).optional(),
77
+ vars: z.string().optional(),
78
+ varsAssignor: z.string().optional(),
79
+ varsAssignorPattern: z.string().optional(),
80
+ varsDelimiter: z.string().optional(),
81
+ varsDelimiterPattern: z.string().optional(),
82
+ });
83
+
84
+ const visibilityMap = z.record(z.string(), z.boolean());
85
+ /**
86
+ * Zod schemas for configuration files discovered by the new loader.
87
+ *
88
+ * Notes:
89
+ * - RAW: all fields optional; only allowed top-level keys are:
90
+ * - rootOptionDefaults, rootOptionVisibility
91
+ * - scripts, vars, envVars
92
+ * - dynamic (JS/TS only), schema (JS/TS only)
93
+ * - plugins, requiredKeys
94
+ * - RESOLVED: mirrors RAW (no path normalization).
95
+ * - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS only).
96
+ */
97
+ // String-only env value map
98
+ const stringMap = z.record(z.string(), z.string());
99
+ const envStringMap = z.record(z.string(), stringMap);
100
+ /**
101
+ * Raw configuration schema for get‑dotenv config files (JSON/YAML/JS/TS).
102
+ * Validates allowed top‑level keys without performing path normalization.
103
+ */
104
+ const getDotenvConfigSchemaRaw = z.object({
105
+ rootOptionDefaults: getDotenvCliOptionsSchemaRaw.optional(),
106
+ rootOptionVisibility: visibilityMap.optional(),
107
+ scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
108
+ requiredKeys: z.array(z.string()).optional(),
109
+ schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
110
+ vars: stringMap.optional(), // public, global
111
+ envVars: envStringMap.optional(), // public, per-env
112
+ // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
113
+ dynamic: z.unknown().optional(),
114
+ // Per-plugin config bag; validated by plugins/host when used.
115
+ plugins: z.record(z.string(), z.unknown()).optional(),
116
+ });
117
+ /**
118
+ * Resolved configuration schema which preserves the raw shape while narrowing
119
+ * the output to {@link GetDotenvConfigResolved}. Consumers get a strongly typed
120
+ * object, while the underlying validation remains Zod‑driven.
121
+ */
122
+ const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => raw);
39
123
 
40
124
  /** @internal */
41
125
  const isPlainObject$1 = (value) => value !== null &&
@@ -66,263 +150,449 @@ function defaultsDeep(...layers) {
66
150
  }
67
151
 
68
152
  /**
69
- * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
70
- * - If the user explicitly enabled the flag, return true.
71
- * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
72
- * - Otherwise, adopt the default (true → set; false/undefined → unset).
73
- *
74
- * @param exclude - The "on" flag value as parsed by Commander.
75
- * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
76
- * @param defaultValue - The generator default to adopt when no explicit toggle is present.
77
- * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
153
+ * Serialize a dotenv record to a file with minimal quoting (multiline values are quoted).
154
+ * Future-proofs for ordering/sorting changes (currently insertion order).
78
155
  *
79
- * @example
80
- * ```ts
81
- * resolveExclusion(undefined, undefined, true); // => true
82
- * ```
156
+ * @param filename - Destination dotenv file path.
157
+ * @param data - Env-like map of values to write (values may be `undefined`).
158
+ * @returns A `Promise\<void\>` which resolves when the file has been written.
83
159
  */
84
- const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
160
+ async function writeDotenvFile(filename, data) {
161
+ // Serialize: key=value with quotes only for multiline values.
162
+ const body = Object.keys(data).reduce((acc, key) => {
163
+ const v = data[key] ?? '';
164
+ const val = v.includes('\n') ? `"${v}"` : v;
165
+ return `${acc}${key}=${val}\n`;
166
+ }, '');
167
+ await fs.writeFile(filename, body, { encoding: 'utf-8' });
168
+ }
169
+
85
170
  /**
86
- * Resolve an optional flag with "--exclude-all" overrides.
87
- * If excludeAll is set and the individual "...-off" is not, force true.
88
- * If excludeAllOff is set and the individual flag is not explicitly set, unset.
89
- * Otherwise, adopt the default (true → set; false/undefined → unset).
171
+ * Dotenv expansion utilities.
90
172
  *
91
- * @param exclude - Individual include/exclude flag.
92
- * @param excludeOff - Individual "...-off" flag.
93
- * @param defaultValue - Default for the individual flag.
94
- * @param excludeAll - Global "exclude-all" flag.
95
- * @param excludeAllOff - Global "exclude-all-off" flag.
173
+ * This module implements recursive expansion of environment-variable
174
+ * references in strings and records. It supports both whitespace and
175
+ * bracket syntaxes with optional defaults:
96
176
  *
97
- * @example
98
- * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
177
+ * - Whitespace: `$VAR[:default]`
178
+ * - Bracketed: `${VAR[:default]}`
179
+ *
180
+ * Escaped dollar signs (`\$`) are preserved.
181
+ * Unknown variables resolve to empty string unless a default is provided.
99
182
  */
100
- const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
101
- // Order of precedence:
102
- // 1) Individual explicit "on" wins outright.
103
- // 2) Individual explicit "off" wins over any global.
104
- // 3) Global exclude-all forces true when not explicitly turned off.
105
- // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
106
- // 5) Fall back to the default (true => set; false/undefined => unset).
107
- (() => {
108
- // Individual "on"
109
- if (exclude === true)
110
- return true;
111
- // Individual "off"
112
- if (excludeOff === true)
113
- return undefined;
114
- // Global "exclude-all" ON (unless explicitly turned off)
115
- if (excludeAll === true)
116
- return true;
117
- // Global "exclude-all-off" (unless explicitly enabled)
118
- if (excludeAllOff === true)
119
- return undefined;
120
- // Default
121
- return defaultValue ? true : undefined;
122
- })();
123
183
  /**
124
- * exactOptionalPropertyTypes-safe setter for optional boolean flags:
125
- * delete when undefined; assign when defined — without requiring an index signature on T.
184
+ * Like String.prototype.search but returns the last index.
185
+ * @internal
186
+ */
187
+ const searchLast = (str, rgx) => {
188
+ const matches = Array.from(str.matchAll(rgx));
189
+ return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
190
+ };
191
+ const replaceMatch = (value, match, ref) => {
192
+ /**
193
+ * @internal
194
+ */
195
+ const group = match[0];
196
+ const key = match[1];
197
+ const defaultValue = match[2];
198
+ if (!key)
199
+ return value;
200
+ const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
201
+ return interpolate(replacement, ref);
202
+ };
203
+ const interpolate = (value = '', ref = {}) => {
204
+ /**
205
+ * @internal
206
+ */
207
+ // if value is falsy, return it as is
208
+ if (!value)
209
+ return value;
210
+ // get position of last unescaped dollar sign
211
+ const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
212
+ // return value if none found
213
+ if (lastUnescapedDollarSignIndex === -1)
214
+ return value;
215
+ // evaluate the value tail
216
+ const tail = value.slice(lastUnescapedDollarSignIndex);
217
+ // find whitespace pattern: $KEY:DEFAULT
218
+ const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
219
+ const whitespaceMatch = whitespacePattern.exec(tail);
220
+ if (whitespaceMatch != null)
221
+ return replaceMatch(value, whitespaceMatch, ref);
222
+ else {
223
+ // find bracket pattern: ${KEY:DEFAULT}
224
+ const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
225
+ const bracketMatch = bracketPattern.exec(tail);
226
+ if (bracketMatch != null)
227
+ return replaceMatch(value, bracketMatch, ref);
228
+ }
229
+ return value;
230
+ };
231
+ /**
232
+ * Recursively expands environment variables in a string. Variables may be
233
+ * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
234
+ * Unknown variables will expand to an empty string.
126
235
  *
127
- * @typeParam T - Target object type.
128
- * @param obj - The object to write to.
129
- * @param key - The optional boolean property key of {@link T}.
130
- * @param value - The value to set or `undefined` to unset.
236
+ * @param value - The string to expand.
237
+ * @param ref - The reference object to use for variable expansion.
238
+ * @returns The expanded string.
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * process.env.FOO = 'bar';
243
+ * dotenvExpand('Hello $FOO'); // "Hello bar"
244
+ * dotenvExpand('Hello $BAZ:world'); // "Hello world"
245
+ * ```
131
246
  *
132
247
  * @remarks
133
- * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
248
+ * The expansion is recursive. If a referenced variable itself contains
249
+ * references, those will also be expanded until a stable value is reached.
250
+ * Escaped references (e.g. `\$FOO`) are preserved as literals.
134
251
  */
135
- const setOptionalFlag = (obj, key, value) => {
136
- const target = obj;
137
- const k = key;
138
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
139
- if (value === undefined)
140
- delete target[k];
141
- else
142
- target[k] = value;
252
+ const dotenvExpand = (value, ref = process.env) => {
253
+ const result = interpolate(value, ref);
254
+ return result ? result.replace(/\\\$/g, '$') : undefined;
143
255
  };
144
-
145
256
  /**
146
- * Merge and normalize raw Commander options (current + parent + defaults)
147
- * into a GetDotenvCliOptions-like object. Types are intentionally wide to
148
- * avoid cross-layer coupling; callers may cast as needed.
149
- */
150
- const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
151
- const parent = typeof parentJson === 'string' && parentJson.length > 0
152
- ? JSON.parse(parentJson)
153
- : undefined;
154
- const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
155
- const current = { ...rest };
156
- if (typeof scripts === 'string') {
157
- try {
158
- current.scripts = JSON.parse(scripts);
159
- }
160
- catch {
161
- // ignore parse errors; leave scripts undefined
162
- }
163
- }
164
- const merged = defaultsDeep({}, defaults, parent ?? {}, current);
165
- const d = defaults;
166
- setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
167
- setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
168
- setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
169
- setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
170
- setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
171
- setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
172
- setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
173
- setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
174
- // warnEntropy (tri-state)
175
- setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
176
- // Normalize shell for predictability: explicit default shell per OS.
177
- const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
178
- let resolvedShell = merged.shell;
179
- if (shellOff)
180
- resolvedShell = false;
181
- else if (resolvedShell === true || resolvedShell === undefined) {
182
- resolvedShell = defaultShell;
183
- }
184
- else if (typeof resolvedShell !== 'string' &&
185
- typeof defaults.shell === 'string') {
186
- resolvedShell = defaults.shell;
187
- }
188
- merged.shell = resolvedShell;
189
- const cmd = typeof command === 'string' ? command : undefined;
190
- return cmd !== undefined ? { merged, command: cmd } : { merged };
191
- };
257
+ * Recursively expands environment variables in the values of a JSON object.
258
+ * Variables may be presented with optional default as `$VAR[:default]` or
259
+ * `${VAR[:default]}`. Unknown variables will expand to an empty string.
260
+ *
261
+ * @param values - The values object to expand.
262
+ * @param options - Expansion options.
263
+ * @returns The value object with expanded string values.
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * process.env.FOO = 'bar';
268
+ * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
269
+ * // => { A: "bar", B: "xbary" }
270
+ * ```
271
+ *
272
+ * @remarks
273
+ * Options:
274
+ * - ref: The reference object to use for expansion (defaults to process.env).
275
+ * - progressive: Whether to progressively add expanded values to the set of
276
+ * reference keys.
277
+ *
278
+ * When `progressive` is true, each expanded key becomes available for
279
+ * subsequent expansions in the same object (left-to-right by object key order).
280
+ */
281
+ function dotenvExpandAll(values, options = {}) {
282
+ const { ref = process.env, progressive = false, } = options;
283
+ const out = Object.keys(values).reduce((acc, key) => {
284
+ acc[key] = dotenvExpand(values[key], {
285
+ ...ref,
286
+ ...(progressive ? acc : {}),
287
+ });
288
+ return acc;
289
+ }, {});
290
+ // Key-preserving return with a permissive index signature to allow later additions.
291
+ return out;
292
+ }
293
+ /**
294
+ * Recursively expands environment variables in a string using `process.env` as
295
+ * the expansion reference. Variables may be presented with optional default as
296
+ * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
297
+ * empty string.
298
+ *
299
+ * @param value - The string to expand.
300
+ * @returns The expanded string.
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * process.env.FOO = 'bar';
305
+ * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
306
+ * ```
307
+ */
308
+ const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
192
309
 
310
+ /** @internal */
311
+ const isPlainObject = (v) => v !== null &&
312
+ typeof v === 'object' &&
313
+ !Array.isArray(v) &&
314
+ Object.getPrototypeOf(v) === Object.prototype;
193
315
  /**
194
- * Zod schemas for configuration files discovered by the new loader.
316
+ * Deeply interpolate string leaves against envRef.
317
+ * Arrays are not recursed into; they are returned unchanged.
195
318
  *
196
- * Notes:
197
- * - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
198
- * - RESOLVED: normalized shapes (paths always string[]).
199
- * - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
319
+ * @typeParam T - Shape of the input value.
320
+ * @param value - Input value (object/array/primitive).
321
+ * @param envRef - Reference environment for interpolation.
322
+ * @returns A new value with string leaves interpolated.
200
323
  */
201
- // String-only env value map
202
- const stringMap = z.record(z.string(), z.string());
203
- const envStringMap = z.record(z.string(), stringMap);
204
- // Allow string[] or single string for "paths" in RAW; normalize later.
205
- const rawPathsSchema = z.union([z.array(z.string()), z.string()]).optional();
206
- const getDotenvConfigSchemaRaw = z.object({
207
- dotenvToken: z.string().optional(),
208
- privateToken: z.string().optional(),
209
- paths: rawPathsSchema,
210
- loadProcess: z.boolean().optional(),
211
- log: z.boolean().optional(),
212
- shell: z.union([z.string(), z.boolean()]).optional(),
213
- scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
214
- requiredKeys: z.array(z.string()).optional(),
215
- schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
216
- vars: stringMap.optional(), // public, global
217
- envVars: envStringMap.optional(), // public, per-env
218
- // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
219
- dynamic: z.unknown().optional(),
220
- // Per-plugin config bag; validated by plugins/host when used.
221
- plugins: z.record(z.string(), z.unknown()).optional(),
222
- });
223
- // Normalize paths to string[]
224
- const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
225
- const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
226
- ...raw,
227
- paths: normalizePaths(raw.paths),
228
- }));
324
+ const interpolateDeep = (value, envRef) => {
325
+ // Strings: expand and return
326
+ if (typeof value === 'string') {
327
+ const out = dotenvExpand(value, envRef);
328
+ // dotenvExpand returns string | undefined; preserve original on undefined
329
+ return (out ?? value);
330
+ }
331
+ // Arrays: return as-is (no recursion)
332
+ if (Array.isArray(value)) {
333
+ return value;
334
+ }
335
+ // Plain objects: shallow clone and recurse into values
336
+ if (isPlainObject(value)) {
337
+ const src = value;
338
+ const out = {};
339
+ for (const [k, v] of Object.entries(src)) {
340
+ // Recurse for strings/objects; keep arrays as-is; preserve other scalars
341
+ if (typeof v === 'string')
342
+ out[k] = dotenvExpand(v, envRef) ?? v;
343
+ else if (Array.isArray(v))
344
+ out[k] = v;
345
+ else if (isPlainObject(v))
346
+ out[k] = interpolateDeep(v, envRef);
347
+ else
348
+ out[k] = v;
349
+ }
350
+ return out;
351
+ }
352
+ // Other primitives/types: return as-is
353
+ return value;
354
+ };
229
355
 
230
- // Discovery candidates (first match wins per scope/privacy).
231
- // Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
232
- const PUBLIC_FILENAMES = [
233
- 'getdotenv.config.json',
234
- 'getdotenv.config.yaml',
235
- 'getdotenv.config.yml',
236
- 'getdotenv.config.js',
237
- 'getdotenv.config.mjs',
238
- 'getdotenv.config.cjs',
239
- 'getdotenv.config.ts',
240
- 'getdotenv.config.mts',
241
- 'getdotenv.config.cts',
242
- ];
243
- const LOCAL_FILENAMES = [
244
- 'getdotenv.config.local.json',
245
- 'getdotenv.config.local.yaml',
246
- 'getdotenv.config.local.yml',
247
- 'getdotenv.config.local.js',
248
- 'getdotenv.config.local.mjs',
249
- 'getdotenv.config.local.cjs',
250
- 'getdotenv.config.local.ts',
251
- 'getdotenv.config.local.mts',
252
- 'getdotenv.config.local.cts',
253
- ];
254
- const isYaml = (p) => ['.yaml', '.yml'].includes(extname(p).toLowerCase());
255
- const isJson = (p) => extname(p).toLowerCase() === '.json';
256
- const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(extname(p).toLowerCase());
257
- // --- Internal JS/TS module loader helpers (default export) ---
258
- const importDefault$1 = async (fileUrl) => {
356
+ const importDefault = async (fileUrl) => {
259
357
  const mod = (await import(fileUrl));
260
358
  return mod.default;
261
359
  };
262
- const cacheName = (absPath, suffix) => {
263
- // sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
264
- const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
265
- return `${base}.${suffix}.mjs`;
266
- };
267
- const ensureDir = async (dir) => {
268
- await fs.ensureDir(dir);
269
- return dir;
360
+ const cacheHash = (absPath, mtimeMs) => createHash('sha1')
361
+ .update(absPath)
362
+ .update(String(mtimeMs))
363
+ .digest('hex')
364
+ .slice(0, 12);
365
+ /**
366
+ * Remove older compiled cache files for a given source base name, keeping
367
+ * at most `keep` most-recent files. Errors are ignored by design.
368
+ */
369
+ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
370
+ try {
371
+ const entries = await fs.readdir(cacheDir);
372
+ const mine = entries
373
+ .filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
374
+ .map((f) => path.join(cacheDir, f));
375
+ if (mine.length <= keep)
376
+ return;
377
+ const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
378
+ stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
379
+ const toDelete = stats.slice(keep).map((s) => s.p);
380
+ await Promise.all(toDelete.map(async (p) => {
381
+ try {
382
+ await fs.remove(p);
383
+ }
384
+ catch {
385
+ // best-effort cleanup
386
+ }
387
+ }));
388
+ }
389
+ catch {
390
+ // best-effort cleanup
391
+ }
270
392
  };
271
- const loadJsTsDefault = async (absPath) => {
272
- const fileUrl = pathToFileURL(absPath).toString();
273
- const ext = extname(absPath).toLowerCase();
274
- if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
275
- return importDefault$1(fileUrl);
393
+ /**
394
+ * Load a module default export from a JS/TS file with robust fallbacks.
395
+ *
396
+ * Behavior by extension:
397
+ *
398
+ * - `.js`/`.mjs`/`.cjs`: direct dynamic import.
399
+ * - `.ts`/`.mts`/`.cts`/`.tsx`:
400
+ * - try direct dynamic import (when a TS loader is active),
401
+ * - else compile via `esbuild` to a cached `.mjs` file and import,
402
+ * - else fallback to `typescript.transpileModule` for simple modules.
403
+ *
404
+ * @typeParam T - Type of the expected default export.
405
+ * @param absPath - Absolute path to the source file.
406
+ * @param cacheDirName - Cache subfolder under `.tsbuild/`.
407
+ * @returns A `Promise\<T | undefined\>` resolving to the default export (if any).
408
+ */
409
+ const loadModuleDefault = async (absPath, cacheDirName) => {
410
+ const ext = path.extname(absPath).toLowerCase();
411
+ const fileUrl = url.pathToFileURL(absPath).toString();
412
+ if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
413
+ return importDefault(fileUrl);
276
414
  }
277
- // Try direct import first in case a TS loader is active.
415
+ // Try direct import first (TS loader active)
278
416
  try {
279
- const val = await importDefault$1(fileUrl);
280
- if (val)
281
- return val;
417
+ const dyn = await importDefault(fileUrl);
418
+ if (dyn)
419
+ return dyn;
282
420
  }
283
421
  catch {
284
- /* fallthrough */
422
+ /* fall through */
285
423
  }
286
- // esbuild bundle to a temp ESM file
424
+ const stat = await fs.stat(absPath);
425
+ const hash = cacheHash(absPath, stat.mtimeMs);
426
+ const cacheDir = path.resolve('.tsbuild', cacheDirName);
427
+ await fs.ensureDir(cacheDir);
428
+ const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
429
+ // Try esbuild
287
430
  try {
288
431
  const esbuild = (await import('esbuild'));
289
- const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
290
- const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
291
432
  await esbuild.build({
292
433
  entryPoints: [absPath],
293
434
  bundle: true,
294
435
  platform: 'node',
295
436
  format: 'esm',
296
437
  target: 'node20',
297
- outfile,
438
+ outfile: cacheFile,
298
439
  sourcemap: false,
299
440
  logLevel: 'silent',
300
441
  });
301
- return await importDefault$1(pathToFileURL(outfile).toString());
442
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
443
+ // Best-effort: trim older cache files for this source.
444
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
445
+ return result;
302
446
  }
303
447
  catch {
304
- /* fallthrough to TS transpile */
448
+ /* fall through to TS transpile */
305
449
  }
306
- // typescript.transpileModule simple transpile (single-file)
450
+ // TypeScript transpile fallback
307
451
  try {
308
452
  const ts = (await import('typescript'));
309
- const src = await fs.readFile(absPath, 'utf-8');
310
- const out = ts.transpileModule(src, {
453
+ const code = await fs.readFile(absPath, 'utf-8');
454
+ const out = ts.transpileModule(code, {
311
455
  compilerOptions: {
312
456
  module: 'ESNext',
313
457
  target: 'ES2022',
314
458
  moduleResolution: 'NodeNext',
315
459
  },
316
460
  }).outputText;
317
- const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
318
- const outfile = path.join(outDir, cacheName(absPath, 'ts'));
319
- await fs.writeFile(outfile, out, 'utf-8');
320
- return await importDefault$1(pathToFileURL(outfile).toString());
461
+ await fs.writeFile(cacheFile, out, 'utf-8');
462
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
463
+ // Best-effort: trim older cache files for this source.
464
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
465
+ return result;
321
466
  }
322
467
  catch {
323
- throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
468
+ // Caller decides final error wording; rethrow for upstream mapping.
469
+ throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
470
+ }
471
+ };
472
+
473
+ /** src/util/omitUndefined.ts
474
+ * Helpers to drop undefined-valued properties in a typed-friendly way.
475
+ */
476
+ /**
477
+ * Omit keys whose runtime value is undefined from a shallow object.
478
+ * Returns a Partial with non-undefined value types preserved.
479
+ *
480
+ * @typeParam T - Input object shape.
481
+ * @param obj - Object to filter.
482
+ * @returns A shallow copy of `obj` without keys whose value is `undefined`.
483
+ */
484
+ function omitUndefined(obj) {
485
+ const out = {};
486
+ for (const [k, v] of Object.entries(obj)) {
487
+ if (v !== undefined)
488
+ out[k] = v;
489
+ }
490
+ return out;
491
+ }
492
+ /**
493
+ * Specialized helper for env-like maps: drop undefined and return string-only.
494
+ *
495
+ * @typeParam V - Value type for present entries (must extend `string`).
496
+ * @param obj - Env-like record containing `string | undefined` values.
497
+ * @returns A new record containing only the keys with defined values.
498
+ */
499
+ function omitUndefinedRecord(obj) {
500
+ const out = {};
501
+ for (const [k, v] of Object.entries(obj)) {
502
+ if (v !== undefined)
503
+ out[k] = v;
504
+ }
505
+ return out;
506
+ }
507
+
508
+ /**
509
+ * Minimal tokenizer for shell-off execution.
510
+ * Splits by whitespace while preserving quoted segments (single or double quotes).
511
+ *
512
+ * @param command - The command string to tokenize.
513
+ * @param opts - Tokenization options (e.g. quote handling).
514
+ */
515
+ const tokenize = (command, opts) => {
516
+ const out = [];
517
+ let cur = '';
518
+ let quote = null;
519
+ for (let i = 0; i < command.length; i++) {
520
+ const c = command.charAt(i);
521
+ if (quote) {
522
+ if (c === quote) {
523
+ // Support doubled quotes inside a quoted segment:
524
+ // default: "" -> " and '' -> ' (Windows/PowerShell style)
525
+ // preserve: keep as "" to allow empty string literals in Node -e payloads
526
+ const next = command.charAt(i + 1);
527
+ if (next === quote) {
528
+ {
529
+ // Collapse to a single literal quote
530
+ cur += quote;
531
+ i += 1; // skip the second quote
532
+ }
533
+ }
534
+ else {
535
+ // end of quoted segment
536
+ quote = null;
537
+ }
538
+ }
539
+ else {
540
+ cur += c;
541
+ }
542
+ }
543
+ else {
544
+ if (c === '"' || c === "'") {
545
+ quote = c;
546
+ }
547
+ else if (/\s/.test(c)) {
548
+ if (cur) {
549
+ out.push(cur);
550
+ cur = '';
551
+ }
552
+ }
553
+ else {
554
+ cur += c;
555
+ }
556
+ }
324
557
  }
558
+ if (cur)
559
+ out.push(cur);
560
+ return out;
325
561
  };
562
+
563
+ /**
564
+ * @packageDocumentation
565
+ * Configuration discovery and loading for get‑dotenv. Discovers config files
566
+ * in the packaged root and project root, loads JSON/YAML/JS/TS documents, and
567
+ * validates them against Zod schemas.
568
+ */
569
+ // Discovery candidates (first match wins per scope/privacy).
570
+ // Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
571
+ const PUBLIC_FILENAMES = [
572
+ 'getdotenv.config.json',
573
+ 'getdotenv.config.yaml',
574
+ 'getdotenv.config.yml',
575
+ 'getdotenv.config.js',
576
+ 'getdotenv.config.mjs',
577
+ 'getdotenv.config.cjs',
578
+ 'getdotenv.config.ts',
579
+ 'getdotenv.config.mts',
580
+ 'getdotenv.config.cts',
581
+ ];
582
+ const LOCAL_FILENAMES = [
583
+ 'getdotenv.config.local.json',
584
+ 'getdotenv.config.local.yaml',
585
+ 'getdotenv.config.local.yml',
586
+ 'getdotenv.config.local.js',
587
+ 'getdotenv.config.local.mjs',
588
+ 'getdotenv.config.local.cjs',
589
+ 'getdotenv.config.local.ts',
590
+ 'getdotenv.config.local.mts',
591
+ 'getdotenv.config.local.cts',
592
+ ];
593
+ const isYaml = (p) => ['.yaml', '.yml'].includes(extname(p).toLowerCase());
594
+ const isJson = (p) => extname(p).toLowerCase() === '.json';
595
+ const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(extname(p).toLowerCase());
326
596
  /**
327
597
  * Discover JSON/YAML config files in the packaged root and project root.
328
598
  * Order: packaged public → project public → project local. */
@@ -375,8 +645,8 @@ const loadConfigFile = async (filePath) => {
375
645
  try {
376
646
  const abs = path.resolve(filePath);
377
647
  if (isJsOrTs(abs)) {
378
- // JS/TS support: load default export via robust pipeline.
379
- const mod = await loadJsTsDefault(abs);
648
+ // JS/TS support: load default export via shared robust pipeline.
649
+ const mod = await loadModuleDefault(abs, 'getdotenv-config');
380
650
  raw = mod ?? {};
381
651
  }
382
652
  else {
@@ -388,575 +658,102 @@ const loadConfigFile = async (filePath) => {
388
658
  throw new Error(`Failed to read/parse config: ${filePath}. ${String(err)}`);
389
659
  }
390
660
  // Validate RAW
391
- const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
392
- if (!parsed.success) {
393
- const msgs = parsed.error.issues
394
- .map((i) => `${i.path.join('.')}: ${i.message}`)
395
- .join('\n');
396
- throw new Error(`Invalid config ${filePath}:\n${msgs}`);
397
- }
398
- // Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
399
- if (!isJsOrTs(filePath) &&
400
- (parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
401
- throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
402
- `Use JS/TS config for "dynamic" or "schema".`);
403
- }
404
- return getDotenvConfigSchemaResolved.parse(parsed.data);
405
- };
406
- /**
407
- * Discover and load configs into resolved shapes, ordered by scope/privacy.
408
- * JSON/YAML/JS/TS supported; first match per scope/privacy applies.
409
- */
410
- const resolveGetDotenvConfigSources = async (importMetaUrl) => {
411
- const discovered = await discoverConfigFiles(importMetaUrl);
412
- const result = {};
413
- for (const f of discovered) {
414
- const cfg = await loadConfigFile(f.path);
415
- if (f.scope === 'packaged') {
416
- // packaged public only
417
- result.packaged = cfg;
418
- }
419
- else {
420
- result.project ??= {};
421
- if (f.privacy === 'public')
422
- result.project.public = cfg;
423
- else
424
- result.project.local = cfg;
425
- }
426
- }
427
- return result;
428
- };
429
-
430
- /**
431
- * Validate a composed env against config-provided validation surfaces.
432
- * Precedence for validation definitions:
433
- * project.local -\> project.public -\> packaged
434
- *
435
- * Behavior:
436
- * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
437
- * - Else if `requiredKeys` is present, check presence (value !== undefined).
438
- * - Returns a flat list of issue strings; caller decides warn vs fail.
439
- */
440
- const validateEnvAgainstSources = (finalEnv, sources) => {
441
- const pick = (getter) => {
442
- const pl = sources.project?.local;
443
- const pp = sources.project?.public;
444
- const pk = sources.packaged;
445
- return ((pl && getter(pl)) ||
446
- (pp && getter(pp)) ||
447
- (pk && getter(pk)) ||
448
- undefined);
449
- };
450
- const schema = pick((cfg) => cfg['schema']);
451
- if (schema &&
452
- typeof schema.safeParse === 'function') {
453
- try {
454
- const parsed = schema.safeParse(finalEnv);
455
- if (!parsed.success) {
456
- // Try to render zod-style issues when available.
457
- const err = parsed.error;
458
- const issues = Array.isArray(err.issues) && err.issues.length > 0
459
- ? err.issues.map((i) => {
460
- const path = Array.isArray(i.path) ? i.path.join('.') : '';
461
- const msg = i.message ?? 'Invalid value';
462
- return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
463
- })
464
- : ['[schema] validation failed'];
465
- return issues;
466
- }
467
- return [];
468
- }
469
- catch {
470
- // If schema invocation fails, surface a single diagnostic.
471
- return [
472
- '[schema] validation failed (unable to execute schema.safeParse)',
473
- ];
474
- }
475
- }
476
- const requiredKeys = pick((cfg) => cfg['requiredKeys']);
477
- if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
478
- const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
479
- if (missing.length > 0) {
480
- return missing.map((k) => `[requiredKeys] missing: ${k}`);
481
- }
482
- }
483
- return [];
484
- };
485
-
486
- const baseGetDotenvCliOptions = baseRootOptionDefaults;
487
-
488
- /** src/util/omitUndefined.ts
489
- * Helpers to drop undefined-valued properties in a typed-friendly way.
490
- */
491
- /**
492
- * Omit keys whose runtime value is undefined from a shallow object.
493
- * Returns a Partial with non-undefined value types preserved.
494
- */
495
- function omitUndefined(obj) {
496
- const out = {};
497
- for (const [k, v] of Object.entries(obj)) {
498
- if (v !== undefined)
499
- out[k] = v;
500
- }
501
- return out;
502
- }
503
- /**
504
- * Specialized helper for env-like maps: drop undefined and return string-only.
505
- */
506
- function omitUndefinedRecord(obj) {
507
- const out = {};
508
- for (const [k, v] of Object.entries(obj)) {
509
- if (v !== undefined)
510
- out[k] = v;
511
- }
512
- return out;
513
- }
514
-
515
- // src/GetDotenvOptions.ts
516
- /**
517
- * Canonical programmatic options and helpers for get-dotenv.
518
- *
519
- * Requirements addressed:
520
- * - GetDotenvOptions derives from the Zod schema output (single source of truth).
521
- * - Removed deprecated/compat flags from the public shape (e.g., useConfigLoader).
522
- * - Provide Vars-aware defineDynamic and a typed config builder defineGetDotenvConfig\<Vars, Env\>().
523
- * - Preserve existing behavior for defaults resolution and compat converters.
524
- */
525
- const getDotenvOptionsFilename = 'getdotenv.config.json';
526
- /**
527
- * Converts programmatic CLI options to `getDotenv` options.
528
- *
529
- * Accepts "stringly" CLI inputs for vars/paths and normalizes them into
530
- * the programmatic shape. Preserves exactOptionalPropertyTypes semantics by
531
- * omitting keys when undefined.
532
- */
533
- const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern,
534
- // drop CLI-only keys from the pass-through bag
535
- debug: _debug, scripts: _scripts, ...rest }) => {
536
- // Split helper for delimited strings or regex patterns
537
- const splitBy = (value, delim, pattern) => {
538
- if (!value)
539
- return [];
540
- if (pattern)
541
- return value.split(RegExp(pattern));
542
- if (typeof delim === 'string')
543
- return value.split(delim);
544
- return value.split(' ');
545
- };
546
- // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
547
- let parsedVars;
548
- if (typeof vars === 'string') {
549
- const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern)
550
- .map((v) => v.split(varsAssignorPattern
551
- ? RegExp(varsAssignorPattern)
552
- : (varsAssignor ?? '=')))
553
- .filter(([k]) => typeof k === 'string' && k.length > 0);
554
- parsedVars = Object.fromEntries(kvPairs);
555
- }
556
- else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
557
- // Accept provided object map of string | undefined; drop undefined values
558
- // in the normalization step below to produce a ProcessEnv-compatible bag.
559
- parsedVars = Object.fromEntries(Object.entries(vars));
560
- }
561
- // Drop undefined-valued entries at the converter stage to match ProcessEnv
562
- // expectations and the compat test assertions.
563
- if (parsedVars) {
564
- parsedVars = omitUndefinedRecord(parsedVars);
565
- }
566
- // Tolerate paths as either a delimited string or string[]
567
- const pathsOut = Array.isArray(paths)
568
- ? paths.filter((p) => typeof p === 'string')
569
- : splitBy(paths, pathsDelimiter, pathsDelimiterPattern);
570
- // Preserve exactOptionalPropertyTypes: only include keys when defined.
571
- return {
572
- ...rest,
573
- ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
574
- ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
575
- };
576
- };
577
- /**
578
- * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
579
- *
580
- * 1. Base defaults derived from the CLI generator defaults
581
- * ({@link baseGetDotenvCliOptions}).
582
- * 2. Local project overrides from a `getdotenv.config.json` in the nearest
583
- * package root (if present).
584
- * 3. The provided customOptions.
585
- *
586
- * The result preserves explicit empty values and drops only `undefined`.
587
- */
588
- const resolveGetDotenvOptions = async (customOptions) => {
589
- const localPkgDir = await packageDirectory();
590
- const localOptionsPath = localPkgDir
591
- ? join(localPkgDir, getDotenvOptionsFilename)
592
- : undefined;
593
- // Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
594
- let localOptions = {};
595
- if (localOptionsPath && (await fs.exists(localOptionsPath))) {
596
- try {
597
- const txt = await fs.readFile(localOptionsPath, 'utf-8');
598
- const parsed = JSON.parse(txt);
599
- if (parsed && typeof parsed === 'object') {
600
- localOptions = parsed;
601
- }
602
- }
603
- catch {
604
- // Malformed or unreadable local options are treated as absent.
605
- localOptions = {};
606
- }
607
- }
608
- // Merge order: base < local < custom (custom has highest precedence)
609
- const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
610
- const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
611
- const result = defaultsDeep(defaultsFromCli, customOptions);
612
- return {
613
- ...result, // Keep explicit empty strings/zeros; drop only undefined
614
- vars: omitUndefinedRecord(result.vars ?? {}),
615
- };
616
- };
617
-
618
- /**
619
- * Dotenv expansion utilities.
620
- *
621
- * This module implements recursive expansion of environment-variable
622
- * references in strings and records. It supports both whitespace and
623
- * bracket syntaxes with optional defaults:
624
- *
625
- * - Whitespace: `$VAR[:default]`
626
- * - Bracketed: `${VAR[:default]}`
627
- *
628
- * Escaped dollar signs (`\$`) are preserved.
629
- * Unknown variables resolve to empty string unless a default is provided.
630
- */
631
- /**
632
- * Like String.prototype.search but returns the last index.
633
- * @internal
634
- */
635
- const searchLast = (str, rgx) => {
636
- const matches = Array.from(str.matchAll(rgx));
637
- return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
638
- };
639
- const replaceMatch = (value, match, ref) => {
640
- /**
641
- * @internal
642
- */
643
- const group = match[0];
644
- const key = match[1];
645
- const defaultValue = match[2];
646
- if (!key)
647
- return value;
648
- const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
649
- return interpolate(replacement, ref);
650
- };
651
- const interpolate = (value = '', ref = {}) => {
652
- /**
653
- * @internal
654
- */
655
- // if value is falsy, return it as is
656
- if (!value)
657
- return value;
658
- // get position of last unescaped dollar sign
659
- const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
660
- // return value if none found
661
- if (lastUnescapedDollarSignIndex === -1)
662
- return value;
663
- // evaluate the value tail
664
- const tail = value.slice(lastUnescapedDollarSignIndex);
665
- // find whitespace pattern: $KEY:DEFAULT
666
- const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
667
- const whitespaceMatch = whitespacePattern.exec(tail);
668
- if (whitespaceMatch != null)
669
- return replaceMatch(value, whitespaceMatch, ref);
670
- else {
671
- // find bracket pattern: ${KEY:DEFAULT}
672
- const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
673
- const bracketMatch = bracketPattern.exec(tail);
674
- if (bracketMatch != null)
675
- return replaceMatch(value, bracketMatch, ref);
661
+ const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
662
+ if (!parsed.success) {
663
+ const msgs = parsed.error.issues
664
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
665
+ .join('\n');
666
+ throw new Error(`Invalid config ${filePath}:\n${msgs}`);
676
667
  }
677
- return value;
668
+ // Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
669
+ if (!isJsOrTs(filePath) &&
670
+ (parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
671
+ throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
672
+ `Use JS/TS config for "dynamic" or "schema".`);
673
+ }
674
+ return getDotenvConfigSchemaResolved.parse(parsed.data);
678
675
  };
679
676
  /**
680
- * Recursively expands environment variables in a string. Variables may be
681
- * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
682
- * Unknown variables will expand to an empty string.
683
- *
684
- * @param value - The string to expand.
685
- * @param ref - The reference object to use for variable expansion.
686
- * @returns The expanded string.
687
- *
688
- * @example
689
- * ```ts
690
- * process.env.FOO = 'bar';
691
- * dotenvExpand('Hello $FOO'); // "Hello bar"
692
- * dotenvExpand('Hello $BAZ:world'); // "Hello world"
693
- * ```
694
- *
695
- * @remarks
696
- * The expansion is recursive. If a referenced variable itself contains
697
- * references, those will also be expanded until a stable value is reached.
698
- * Escaped references (e.g. `\$FOO`) are preserved as literals.
677
+ * Discover and load configs into resolved shapes, ordered by scope/privacy.
678
+ * JSON/YAML/JS/TS supported; first match per scope/privacy applies.
699
679
  */
700
- const dotenvExpand = (value, ref = process.env) => {
701
- const result = interpolate(value, ref);
702
- return result ? result.replace(/\\\$/g, '$') : undefined;
680
+ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
681
+ const discovered = await discoverConfigFiles(importMetaUrl);
682
+ const result = {};
683
+ for (const f of discovered) {
684
+ const cfg = await loadConfigFile(f.path);
685
+ if (f.scope === 'packaged') {
686
+ // packaged public only
687
+ result.packaged = cfg;
688
+ }
689
+ else {
690
+ result.project ??= {};
691
+ if (f.privacy === 'public')
692
+ result.project.public = cfg;
693
+ else
694
+ result.project.local = cfg;
695
+ }
696
+ }
697
+ return result;
703
698
  };
704
- /**
705
- * Recursively expands environment variables in the values of a JSON object.
706
- * Variables may be presented with optional default as `$VAR[:default]` or
707
- * `${VAR[:default]}`. Unknown variables will expand to an empty string.
708
- *
709
- * @param values - The values object to expand.
710
- * @param options - Expansion options.
711
- * @returns The value object with expanded string values.
712
- *
713
- * @example
714
- * ```ts
715
- * process.env.FOO = 'bar';
716
- * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
717
- * // => { A: "bar", B: "xbary" }
718
- * ```
719
- *
720
- * @remarks
721
- * Options:
722
- * - ref: The reference object to use for expansion (defaults to process.env).
723
- * - progressive: Whether to progressively add expanded values to the set of
724
- * reference keys.
699
+
700
+ /** src/env/dynamic.ts
701
+ * Helpers for applying and loading dynamic variables (JS/TS).
725
702
  *
726
- * When `progressive` is true, each expanded key becomes available for
727
- * subsequent expansions in the same object (left-to-right by object key order).
703
+ * Requirements addressed:
704
+ * - Single service to apply a dynamic map progressively.
705
+ * - Single service to load a JS/TS dynamic module with robust fallbacks (util/loadModuleDefault).
706
+ * - Unify error messaging so callers show consistent guidance.
728
707
  */
729
- function dotenvExpandAll(values, options = {}) {
730
- const { ref = process.env, progressive = false, } = options;
731
- const out = Object.keys(values).reduce((acc, key) => {
732
- acc[key] = dotenvExpand(values[key], {
733
- ...ref,
734
- ...(progressive ? acc : {}),
735
- });
736
- return acc;
737
- }, {});
738
- // Key-preserving return with a permissive index signature to allow later additions.
739
- return out;
740
- }
741
708
  /**
742
- * Recursively expands environment variables in a string using `process.env` as
743
- * the expansion reference. Variables may be presented with optional default as
744
- * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
745
- * empty string.
746
- *
747
- * @param value - The string to expand.
748
- * @returns The expanded string.
709
+ * Apply a dynamic map to the target progressively.
710
+ * - Functions receive (target, env) and may return string | undefined.
711
+ * - Literals are assigned directly (including undefined).
749
712
  *
750
- * @example
751
- * ```ts
752
- * process.env.FOO = 'bar';
753
- * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
754
- * ```
755
- */
756
- const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
757
-
758
- /* eslint-disable @typescript-eslint/no-deprecated */
759
- /**
760
- * Attach root flags to a GetDotenvCli instance.
761
- * - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
762
- * - Any flag that displays an effective default in help uses dynamic descriptions.
713
+ * @param target - Mutable target environment to assign into.
714
+ * @param map - Dynamic map to apply (functions and/or literal values).
715
+ * @param env - Selected environment name (if any) passed through to dynamic functions.
716
+ * @returns Nothing.
763
717
  */
764
- const attachRootOptions = (program, defaults) => {
765
- // Install temporary wrappers to tag all options added here as "base" for grouped help.
766
- const GROUP = 'base';
767
- const tagLatest = (cmd, group) => {
768
- const optsArr = cmd.options ?? [];
769
- if (Array.isArray(optsArr) && optsArr.length > 0) {
770
- const last = optsArr[optsArr.length - 1];
771
- program.setOptionGroup(last, group);
772
- }
773
- };
774
- const originalAddOption = program.addOption.bind(program);
775
- const originalOption = program.option.bind(program);
776
- program.addOption = function patchedAdd(opt) {
777
- program.setOptionGroup(opt, GROUP);
778
- return originalAddOption(opt);
779
- };
780
- program.option = function patchedOption(...args) {
781
- const ret = originalOption(...args);
782
- tagLatest(this, GROUP);
783
- return ret;
784
- };
785
- const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
786
- const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
787
- const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
788
- // Helper: append (default) tags for ON/OFF toggles
789
- const onOff = (on, isDefault) => on
790
- ? `ON${isDefault ? ' (default)' : ''}`
791
- : `OFF${isDefault ? ' (default)' : ''}`;
792
- let p = program
793
- .enablePositionalOptions()
794
- .passThroughOptions()
795
- .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
796
- p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
797
- ['KEY1', 'VAL1'],
798
- ['KEY2', 'VAL2'],
799
- ]
800
- .map((v) => v.join(va))
801
- .join(vd)}`, dotenvExpandFromProcessEnv);
802
- // Output path (interpolated later; help can remain static)
803
- p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
804
- // Shell ON (string or boolean true => default shell)
805
- p = p
806
- .addOption(program
807
- .createDynamicOption('-s, --shell [string]', (cfg) => {
808
- const s = cfg.shell;
809
- let tag = '';
810
- if (typeof s === 'boolean' && s)
811
- tag = ' (default OS shell)';
812
- else if (typeof s === 'string' && s.length > 0)
813
- tag = ` (default ${s})`;
814
- return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
815
- })
816
- .conflicts('shellOff'))
817
- // Shell OFF
818
- .addOption(program
819
- .createDynamicOption('-S, --shell-off', (cfg) => {
820
- const s = cfg.shell;
821
- return `command execution shell OFF${s === false ? ' (default)' : ''}`;
822
- })
823
- .conflicts('shell'));
824
- // Load process ON/OFF (dynamic defaults)
825
- p = p
826
- .addOption(program
827
- .createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
828
- .conflicts('loadProcessOff'))
829
- .addOption(program
830
- .createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
831
- .conflicts('loadProcess'));
832
- // Exclusion master toggle (dynamic)
833
- p = p
834
- .addOption(program
835
- .createDynamicOption('-a, --exclude-all', (cfg) => {
836
- const allOn = !!cfg.excludeDynamic &&
837
- ((!!cfg.excludeEnv && !!cfg.excludeGlobal) ||
838
- (!!cfg.excludePrivate && !!cfg.excludePublic));
839
- const suffix = allOn ? ' (default)' : '';
840
- return `exclude all dotenv variables from loading ON${suffix}`;
841
- })
842
- .conflicts('excludeAllOff'))
843
- .addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
844
- // Per-family exclusions (dynamic defaults)
845
- p = p
846
- .addOption(program
847
- .createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
848
- .conflicts('excludeDynamicOff'))
849
- .addOption(program
850
- .createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
851
- .conflicts('excludeDynamic'))
852
- .addOption(program
853
- .createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
854
- .conflicts('excludeEnvOff'))
855
- .addOption(program
856
- .createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
857
- .conflicts('excludeEnv'))
858
- .addOption(program
859
- .createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
860
- .conflicts('excludeGlobalOff'))
861
- .addOption(program
862
- .createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
863
- .conflicts('excludeGlobal'))
864
- .addOption(program
865
- .createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
866
- .conflicts('excludePrivateOff'))
867
- .addOption(program
868
- .createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
869
- .conflicts('excludePrivate'))
870
- .addOption(program
871
- .createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
872
- .conflicts('excludePublicOff'))
873
- .addOption(program
874
- .createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
875
- .conflicts('excludePublic'));
876
- // Log ON/OFF (dynamic)
877
- p = p
878
- .addOption(program
879
- .createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
880
- .conflicts('logOff'))
881
- .addOption(program
882
- .createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
883
- .conflicts('log'));
884
- // Capture flag (no default display; static)
885
- p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
886
- // Core bootstrap/static flags (kept static in help)
887
- p = p
888
- .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
889
- .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
890
- .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
891
- .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
892
- .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
893
- .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
894
- .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
895
- .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
896
- .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
897
- .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
898
- .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
899
- // Hidden scripts pipe-through (stringified)
900
- .addOption(new Option('--scripts <string>')
901
- .default(JSON.stringify(scripts))
902
- .hideHelp());
903
- // Diagnostics / validation / entropy
904
- p = p
905
- .option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
906
- .option('--strict', 'fail on env validation errors (schema/requiredKeys)');
907
- p = p
908
- .addOption(program
909
- .createDynamicOption('--entropy-warn', (cfg) => {
910
- const warn = cfg.warnEntropy;
911
- // Default is effectively ON when warnEntropy is true or undefined.
912
- return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
913
- })
914
- .conflicts('entropyWarnOff'))
915
- .addOption(program
916
- .createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
917
- .conflicts('entropyWarn'))
918
- .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
919
- .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
920
- .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
921
- .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
922
- // Restore original methods
923
- program.addOption = originalAddOption;
924
- program.option = originalOption;
925
- return p;
926
- };
927
-
718
+ function applyDynamicMap(target, map, env) {
719
+ if (!map)
720
+ return;
721
+ for (const key of Object.keys(map)) {
722
+ const val = typeof map[key] === 'function'
723
+ ? map[key](target, env)
724
+ : map[key];
725
+ Object.assign(target, { [key]: val });
726
+ }
727
+ }
928
728
  /**
929
- * Zod schemas for programmatic GetDotenv options.
729
+ * Load a default-export dynamic map from a JS/TS file and apply it.
730
+ * Uses util/loadModuleDefault for robust TS handling (direct import, esbuild,
731
+ * typescript.transpile fallback).
930
732
  *
931
- * Canonical source of truth for options shape. Public types are derived
932
- * from these schemas (see consumers via z.output\<\>).
733
+ * Error behavior:
734
+ * - On failure to load/compile/evaluate the module, throws a unified message:
735
+ * "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
736
+ *
737
+ * @param target - Mutable target environment to assign into.
738
+ * @param absPath - Absolute path to the dynamic module file.
739
+ * @param env - Selected environment name (if any).
740
+ * @param cacheDirName - Cache subdirectory under `.tsbuild/` for compiled artifacts.
741
+ * @returns A `Promise\<void\>` which resolves after the module (if present) has been applied.
933
742
  */
934
- // Minimal process env representation: string values or undefined to indicate "unset".
935
- const processEnvSchema = z.record(z.string(), z.string().optional());
936
- // RAW: all fields optional — undefined means "inherit" from lower layers.
937
- const getDotenvOptionsSchemaRaw = z.object({
938
- defaultEnv: z.string().optional(),
939
- dotenvToken: z.string().optional(),
940
- dynamicPath: z.string().optional(),
941
- // Dynamic map is intentionally wide for now; refine once sources are normalized.
942
- dynamic: z.record(z.string(), z.unknown()).optional(),
943
- env: z.string().optional(),
944
- excludeDynamic: z.boolean().optional(),
945
- excludeEnv: z.boolean().optional(),
946
- excludeGlobal: z.boolean().optional(),
947
- excludePrivate: z.boolean().optional(),
948
- excludePublic: z.boolean().optional(),
949
- loadProcess: z.boolean().optional(),
950
- log: z.boolean().optional(),
951
- logger: z.unknown().optional(),
952
- outputPath: z.string().optional(),
953
- paths: z.array(z.string()).optional(),
954
- privateToken: z.string().optional(),
955
- vars: processEnvSchema.optional(),
956
- });
957
- // RESOLVED: service-boundary contract (post-inheritance).
958
- // For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
959
- const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
743
+ async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
744
+ if (!(await fs.exists(absPath)))
745
+ return;
746
+ let dyn;
747
+ try {
748
+ dyn = await loadModuleDefault(absPath, cacheDirName);
749
+ }
750
+ catch {
751
+ // Preserve legacy/clear guidance used by tests and docs.
752
+ throw new Error(`Unable to load dynamic TypeScript file: ${absPath}. ` +
753
+ `Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
754
+ }
755
+ applyDynamicMap(target, dyn, env);
756
+ }
960
757
 
961
758
  const applyKv = (current, kv) => {
962
759
  if (!kv || Object.keys(kv).length === 0)
@@ -972,7 +769,8 @@ const applyConfigSlice = (current, cfg, env) => {
972
769
  const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
973
770
  return applyKv(afterGlobal, envKv);
974
771
  };
975
- function overlayEnv({ base, env, configs, programmaticVars, }) {
772
+ function overlayEnv(args) {
773
+ const { base, env, configs } = args;
976
774
  let current = { ...base };
977
775
  // Source: packaged (public -> local)
978
776
  current = applyConfigSlice(current, configs.packaged, env);
@@ -982,8 +780,8 @@ function overlayEnv({ base, env, configs, programmaticVars, }) {
982
780
  current = applyConfigSlice(current, configs.project?.public, env);
983
781
  current = applyConfigSlice(current, configs.project?.local, env);
984
782
  // Programmatic explicit vars (top of static tier)
985
- if (programmaticVars) {
986
- const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
783
+ if ('programmaticVars' in args) {
784
+ const toApply = Object.fromEntries(Object.entries(args.programmaticVars).filter(([_k, v]) => typeof v === 'string'));
987
785
  current = applyKv(current, toApply);
988
786
  }
989
787
  return current;
@@ -1036,29 +834,145 @@ const maybeWarnEntropy = (key, value, origin, opts, emit) => {
1036
834
  emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
1037
835
  }
1038
836
  };
1039
-
1040
- const DEFAULT_PATTERNS = [
1041
- '\\bsecret\\b',
1042
- '\\btoken\\b',
1043
- '\\bpass(word)?\\b',
1044
- '\\bapi[_-]?key\\b',
1045
- '\\bkey\\b',
1046
- ];
1047
- const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
1048
- const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
1049
- const MASK = '[redacted]';
837
+
838
+ const DEFAULT_PATTERNS = [
839
+ '\\bsecret\\b',
840
+ '\\btoken\\b',
841
+ '\\bpass(word)?\\b',
842
+ '\\bapi[_-]?key\\b',
843
+ '\\bkey\\b',
844
+ ];
845
+ const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
846
+ const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
847
+ const MASK = '[redacted]';
848
+ /**
849
+ * Produce a shallow redacted copy of an env-like object for display.
850
+ */
851
+ const redactObject = (obj, opts) => {
852
+ if (!opts?.redact)
853
+ return { ...obj };
854
+ const regs = compile(opts.redactPatterns);
855
+ const out = {};
856
+ for (const [k, v] of Object.entries(obj)) {
857
+ out[k] = v && shouldRedactKey(k, regs) ? MASK : v;
858
+ }
859
+ return out;
860
+ };
861
+
862
+ /**
863
+ * Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
864
+ * Used as the bottom layer for CLI option resolution.
865
+ */
866
+ /**
867
+ * Default values for root CLI options used by the host and helpers as the
868
+ * baseline layer during option resolution.
869
+ *
870
+ * These defaults correspond to the "stringly" root surface (see `RootOptionsShape`)
871
+ * and are merged by precedence with create-time overrides and any discovered
872
+ * configuration `rootOptionDefaults` before CLI flags are applied.
873
+ */
874
+ const baseRootOptionDefaults = {
875
+ dotenvToken: '.env',
876
+ loadProcess: true,
877
+ logger: console,
878
+ // Diagnostics defaults
879
+ warnEntropy: true,
880
+ entropyThreshold: 3.8,
881
+ entropyMinLength: 16,
882
+ entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
883
+ paths: './',
884
+ pathsDelimiter: ' ',
885
+ privateToken: 'local',
886
+ scripts: {
887
+ 'git-status': {
888
+ cmd: 'git branch --show-current && git status -s -u',
889
+ shell: true,
890
+ },
891
+ },
892
+ shell: true,
893
+ vars: '',
894
+ varsAssignor: '=',
895
+ varsDelimiter: ' ',
896
+ // tri-state flags default to unset unless explicitly provided
897
+ // (debug/log/exclude* resolved via flag utils)
898
+ };
899
+
900
+ /**
901
+ * Converts programmatic CLI options to `getDotenv` options.
902
+ *
903
+ * Accepts "stringly" CLI inputs for vars/paths and normalizes them into
904
+ * the programmatic shape. Preserves exactOptionalPropertyTypes semantics by
905
+ * omitting keys when undefined.
906
+ */
907
+ const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern,
908
+ // drop CLI-only keys from the pass-through bag
909
+ debug: _debug, scripts: _scripts, ...rest }) => {
910
+ // Split helper for delimited strings or regex patterns
911
+ const splitBy = (value, delim, pattern) => {
912
+ if (!value)
913
+ return [];
914
+ if (pattern)
915
+ return value.split(RegExp(pattern));
916
+ if (typeof delim === 'string')
917
+ return value.split(delim);
918
+ return value.split(' ');
919
+ };
920
+ // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
921
+ let parsedVars;
922
+ if (typeof vars === 'string') {
923
+ const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern)
924
+ .map((v) => v.split(varsAssignorPattern
925
+ ? RegExp(varsAssignorPattern)
926
+ : (varsAssignor ?? '=')))
927
+ .filter(([k]) => typeof k === 'string' && k.length > 0);
928
+ parsedVars = Object.fromEntries(kvPairs);
929
+ }
930
+ else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
931
+ // Accept provided object map of string | undefined; drop undefined values
932
+ // in the normalization step below to produce a ProcessEnv-compatible bag.
933
+ parsedVars = Object.fromEntries(Object.entries(vars));
934
+ }
935
+ // Drop undefined-valued entries at the converter stage to match ProcessEnv
936
+ // expectations and the compat test assertions.
937
+ if (parsedVars) {
938
+ parsedVars = omitUndefinedRecord(parsedVars);
939
+ }
940
+ // Tolerate paths as either a delimited string or string[]
941
+ const pathsOut = Array.isArray(paths)
942
+ ? paths.filter((p) => typeof p === 'string')
943
+ : splitBy(paths, pathsDelimiter, pathsDelimiterPattern);
944
+ // Preserve exactOptionalPropertyTypes: only include keys when defined.
945
+ return {
946
+ // Ensure the required logger property is present. The base CLI defaults
947
+ // specify console as the logger; callers can override upstream if desired.
948
+ logger: console,
949
+ ...rest,
950
+ ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
951
+ ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
952
+ };
953
+ };
1050
954
  /**
1051
- * Produce a shallow redacted copy of an env-like object for display.
955
+ * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
956
+ *
957
+ * 1. Base defaults derived from the CLI generator defaults
958
+ * ({@link baseGetDotenvCliOptions}).
959
+ * 2. Local project overrides from a `getdotenv.config.json` in the nearest
960
+ * package root (if present).
961
+ * 3. The provided customOptions.
962
+ *
963
+ * The result preserves explicit empty values and drops only `undefined`.
1052
964
  */
1053
- const redactObject = (obj, opts) => {
1054
- if (!opts?.redact)
1055
- return { ...obj };
1056
- const regs = compile(opts.redactPatterns);
1057
- const out = {};
1058
- for (const [k, v] of Object.entries(obj)) {
1059
- out[k] = v && shouldRedactKey(k, regs) ? MASK : v;
1060
- }
1061
- return out;
965
+ const resolveGetDotenvOptions = (customOptions) => {
966
+ // Programmatic callers use neutral defaults only. Do not read local packaged
967
+ // getdotenv.config.json here; the host path applies packaged/project configs
968
+ // via the dedicated loader/overlay pipeline.
969
+ const mergedDefaults = baseRootOptionDefaults;
970
+ const defaultsFromCli = getDotenvCliOptions2Options(mergedDefaults);
971
+ const result = defaultsDeep(defaultsFromCli, customOptions);
972
+ return Promise.resolve({
973
+ ...result, // Keep explicit empty strings/zeros; drop only undefined
974
+ vars: omitUndefinedRecord(result.vars ?? {}),
975
+ });
1062
976
  };
1063
977
 
1064
978
  /**
@@ -1076,117 +990,6 @@ const readDotenv = async (path) => {
1076
990
  }
1077
991
  };
1078
992
 
1079
- const importDefault = async (fileUrl) => {
1080
- const mod = (await import(fileUrl));
1081
- return mod.default;
1082
- };
1083
- const cacheHash = (absPath, mtimeMs) => createHash('sha1')
1084
- .update(absPath)
1085
- .update(String(mtimeMs))
1086
- .digest('hex')
1087
- .slice(0, 12);
1088
- /**
1089
- * Remove older compiled cache files for a given source base name, keeping
1090
- * at most `keep` most-recent files. Errors are ignored by design.
1091
- */
1092
- const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
1093
- try {
1094
- const entries = await fs.readdir(cacheDir);
1095
- const mine = entries
1096
- .filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
1097
- .map((f) => path.join(cacheDir, f));
1098
- if (mine.length <= keep)
1099
- return;
1100
- const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
1101
- stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
1102
- const toDelete = stats.slice(keep).map((s) => s.p);
1103
- await Promise.all(toDelete.map(async (p) => {
1104
- try {
1105
- await fs.remove(p);
1106
- }
1107
- catch {
1108
- // best-effort cleanup
1109
- }
1110
- }));
1111
- }
1112
- catch {
1113
- // best-effort cleanup
1114
- }
1115
- };
1116
- /**
1117
- * Load a module default export from a JS/TS file with robust fallbacks:
1118
- * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
1119
- * 1) try direct import (if a TS loader is active),
1120
- * 2) esbuild bundle to a temp ESM file,
1121
- * 3) typescript.transpileModule fallback for simple modules.
1122
- *
1123
- * @param absPath - absolute path to source file
1124
- * @param cacheDirName - cache subfolder under .tsbuild
1125
- */
1126
- const loadModuleDefault = async (absPath, cacheDirName) => {
1127
- const ext = path.extname(absPath).toLowerCase();
1128
- const fileUrl = url.pathToFileURL(absPath).toString();
1129
- if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
1130
- return importDefault(fileUrl);
1131
- }
1132
- // Try direct import first (TS loader active)
1133
- try {
1134
- const dyn = await importDefault(fileUrl);
1135
- if (dyn)
1136
- return dyn;
1137
- }
1138
- catch {
1139
- /* fall through */
1140
- }
1141
- const stat = await fs.stat(absPath);
1142
- const hash = cacheHash(absPath, stat.mtimeMs);
1143
- const cacheDir = path.resolve('.tsbuild', cacheDirName);
1144
- await fs.ensureDir(cacheDir);
1145
- const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
1146
- // Try esbuild
1147
- try {
1148
- const esbuild = (await import('esbuild'));
1149
- await esbuild.build({
1150
- entryPoints: [absPath],
1151
- bundle: true,
1152
- platform: 'node',
1153
- format: 'esm',
1154
- target: 'node20',
1155
- outfile: cacheFile,
1156
- sourcemap: false,
1157
- logLevel: 'silent',
1158
- });
1159
- const result = await importDefault(url.pathToFileURL(cacheFile).toString());
1160
- // Best-effort: trim older cache files for this source.
1161
- await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
1162
- return result;
1163
- }
1164
- catch {
1165
- /* fall through to TS transpile */
1166
- }
1167
- // TypeScript transpile fallback
1168
- try {
1169
- const ts = (await import('typescript'));
1170
- const code = await fs.readFile(absPath, 'utf-8');
1171
- const out = ts.transpileModule(code, {
1172
- compilerOptions: {
1173
- module: 'ESNext',
1174
- target: 'ES2022',
1175
- moduleResolution: 'NodeNext',
1176
- },
1177
- }).outputText;
1178
- await fs.writeFile(cacheFile, out, 'utf-8');
1179
- const result = await importDefault(url.pathToFileURL(cacheFile).toString());
1180
- // Best-effort: trim older cache files for this source.
1181
- await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
1182
- return result;
1183
- }
1184
- catch {
1185
- // Caller decides final error wording; rethrow for upstream mapping.
1186
- throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
1187
- }
1188
- };
1189
-
1190
993
  async function getDotenv(options = {}) {
1191
994
  // Apply defaults.
1192
995
  const { defaultEnv, dotenvToken = '.env', dynamicPath, env, excludeDynamic = false, excludeEnv = false, excludeGlobal = false, excludePrivate = false, excludePublic = false, loadProcess = false, log = false, logger = console, outputPath, paths = [], privateToken = 'local', vars = {}, } = await resolveGetDotenvOptions(options);
@@ -1235,25 +1038,11 @@ async function getDotenv(options = {}) {
1235
1038
  }
1236
1039
  else if (dynamicPath) {
1237
1040
  const absDynamicPath = path.resolve(dynamicPath);
1238
- if (await fs.exists(absDynamicPath)) {
1239
- try {
1240
- dynamic = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic');
1241
- }
1242
- catch {
1243
- // Preserve legacy error text for compatibility with tests/docs.
1244
- throw new Error(`Unable to load dynamic TypeScript file: ${absDynamicPath}. ` +
1245
- `Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
1246
- }
1247
- }
1041
+ await loadAndApplyDynamic(dotenv, absDynamicPath, env ?? defaultEnv, 'getdotenv-dynamic');
1248
1042
  }
1249
1043
  if (dynamic) {
1250
1044
  try {
1251
- for (const key in dynamic)
1252
- Object.assign(dotenv, {
1253
- [key]: typeof dynamic[key] === 'function'
1254
- ? dynamic[key](dotenv, env ?? defaultEnv)
1255
- : dynamic[key],
1256
- });
1045
+ applyDynamicMap(dotenv, dynamic, env ?? defaultEnv);
1257
1046
  }
1258
1047
  catch {
1259
1048
  throw new Error(`Unable to evaluate dynamic variables.`);
@@ -1267,10 +1056,7 @@ async function getDotenv(options = {}) {
1267
1056
  if (!outputPathResolved)
1268
1057
  throw new Error('Output path not found.');
1269
1058
  const { [outputKey]: _omitted, ...dotenvForOutput } = dotenv;
1270
- await fs.writeFile(outputPathResolved, Object.keys(dotenvForOutput).reduce((contents, key) => {
1271
- const value = dotenvForOutput[key] ?? '';
1272
- return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
1273
- }, ''), { encoding: 'utf-8' });
1059
+ await writeDotenvFile(outputPathResolved, dotenvForOutput);
1274
1060
  resultDotenv = dotenvForOutput;
1275
1061
  }
1276
1062
  // Log result.
@@ -1315,60 +1101,26 @@ async function getDotenv(options = {}) {
1315
1101
  }
1316
1102
 
1317
1103
  /**
1318
- * Deep interpolation utility for string leaves.
1319
- * - Expands string values using dotenv-style expansion against the provided envRef.
1320
- * - Preserves non-strings as-is.
1321
- * - Does not recurse into arrays (arrays are returned unchanged).
1104
+ * Compute the realized path for a command mount (leaf-up to root).
1105
+ * Excludes the root application alias.
1322
1106
  *
1323
- * Intended for:
1324
- * - Phase C option/config interpolation after composing ctx.dotenv.
1325
- * - Per-plugin config slice interpolation before afterResolve.
1107
+ * @param cli - The mounted command instance.
1326
1108
  */
1327
- /** @internal */
1328
- const isPlainObject = (v) => v !== null &&
1329
- typeof v === 'object' &&
1330
- !Array.isArray(v) &&
1331
- Object.getPrototypeOf(v) === Object.prototype;
1332
1109
  /**
1333
- * Deeply interpolate string leaves against envRef.
1334
- * Arrays are not recursed into; they are returned unchanged.
1335
- *
1336
- * @typeParam T - Shape of the input value.
1337
- * @param value - Input value (object/array/primitive).
1338
- * @param envRef - Reference environment for interpolation.
1339
- * @returns A new value with string leaves interpolated.
1110
+ * Flatten a plugin tree into a list of `{ plugin, path }` entries.
1111
+ * Traverses the namespace chain in pre-order.
1340
1112
  */
1341
- const interpolateDeep = (value, envRef) => {
1342
- // Strings: expand and return
1343
- if (typeof value === 'string') {
1344
- const out = dotenvExpand(value, envRef);
1345
- // dotenvExpand returns string | undefined; preserve original on undefined
1346
- return (out ?? value);
1347
- }
1348
- // Arrays: return as-is (no recursion)
1349
- if (Array.isArray(value)) {
1350
- return value;
1351
- }
1352
- // Plain objects: shallow clone and recurse into values
1353
- if (isPlainObject(value)) {
1354
- const src = value;
1355
- const out = {};
1356
- for (const [k, v] of Object.entries(src)) {
1357
- // Recurse for strings/objects; keep arrays as-is; preserve other scalars
1358
- if (typeof v === 'string')
1359
- out[k] = dotenvExpand(v, envRef) ?? v;
1360
- else if (Array.isArray(v))
1361
- out[k] = v;
1362
- else if (isPlainObject(v))
1363
- out[k] = interpolateDeep(v, envRef);
1364
- else
1365
- out[k] = v;
1113
+ function flattenPluginTreeByPath(plugins, prefix) {
1114
+ const out = [];
1115
+ for (const p of plugins) {
1116
+ const here = prefix && prefix.length > 0 ? `${prefix}/${p.ns}` : p.ns;
1117
+ out.push({ plugin: p, path: here });
1118
+ if (Array.isArray(p.children) && p.children.length > 0) {
1119
+ out.push(...flattenPluginTreeByPath(p.children.map((c) => c.plugin), here));
1366
1120
  }
1367
- return out;
1368
1121
  }
1369
- // Other primitives/types: return as-is
1370
- return value;
1371
- };
1122
+ return out;
1123
+ }
1372
1124
 
1373
1125
  /**
1374
1126
  * Instance-bound plugin config store.
@@ -1378,15 +1130,26 @@ const interpolateDeep = (value, envRef) => {
1378
1130
  * plugin instance.
1379
1131
  */
1380
1132
  const PLUGIN_CONFIG_STORE = new WeakMap();
1381
- const _setPluginConfigForInstance = (plugin, cfg) => {
1133
+ /**
1134
+ * Store a validated, interpolated config slice for a specific plugin instance.
1135
+ * Generic on both the host options type and the plugin config type to avoid
1136
+ * defaulting to GetDotenvOptions under exactOptionalPropertyTypes.
1137
+ */
1138
+ const setPluginConfig = (plugin, cfg) => {
1382
1139
  PLUGIN_CONFIG_STORE.set(plugin, cfg);
1383
1140
  };
1384
- const _getPluginConfigForInstance = (plugin) => PLUGIN_CONFIG_STORE.get(plugin);
1141
+ /**
1142
+ * Retrieve the validated/interpolated config slice for a plugin instance.
1143
+ */
1144
+ const getPluginConfig = (plugin) => {
1145
+ return PLUGIN_CONFIG_STORE.get(plugin);
1146
+ };
1385
1147
  /**
1386
1148
  * Compute the dotenv context for the host (uses the config loader/overlay path).
1387
1149
  * - Resolves and validates options strictly (host-only).
1388
1150
  * - Applies file cascade, overlays, dynamics, and optional effects.
1389
- * - Merges and validates per-plugin config slices (when provided).
1151
+ * - Merges and validates per-plugin config slices (when provided), keyed by
1152
+ * realized mount path (ns chain).
1390
1153
  *
1391
1154
  * @param customOptions - Partial options from the current invocation.
1392
1155
  * @param plugins - Installed plugins (for config validation).
@@ -1397,21 +1160,16 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1397
1160
  // Zod boundary: parse returns the schema-derived shape; we adopt our public
1398
1161
  // GetDotenvOptions overlay (logger/dynamic typing) for internal processing.
1399
1162
  const validated = getDotenvOptionsSchemaResolved.parse(optionsResolved);
1400
- // Always-on loader path
1401
- // 1) Base from files only (no dynamic, no programmatic vars)
1402
- // Sanitize to avoid passing properties explicitly set to undefined.
1163
+ // Build a pure base without side effects or logging (no dynamics, no programmatic vars).
1403
1164
  const cleanedValidated = omitUndefined(validated);
1404
1165
  const base = await getDotenv({
1405
1166
  ...cleanedValidated,
1406
- // Build a pure base without side effects or logging.
1407
1167
  excludeDynamic: true,
1408
1168
  vars: {},
1409
1169
  log: false,
1410
1170
  loadProcess: false,
1411
- // Intentionally omit outputPath for the base pass; including a key with
1412
- // undefined would violate exactOptionalPropertyTypes on the Partial target.
1413
1171
  });
1414
- // 2) Discover config sources and overlay
1172
+ // Discover config sources and overlay with progressive expansion per slice.
1415
1173
  const sources = await resolveGetDotenvConfigSources(hostMetaUrl);
1416
1174
  const dotenvOverlaid = overlayEnv({
1417
1175
  base,
@@ -1419,47 +1177,31 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1419
1177
  configs: sources,
1420
1178
  ...(validated.vars ? { programmaticVars: validated.vars } : {}),
1421
1179
  });
1422
- // Helper to apply a dynamic map progressively.
1423
- const applyDynamic = (target, dynamic, env) => {
1424
- if (!dynamic)
1425
- return;
1426
- for (const key of Object.keys(dynamic)) {
1427
- const value = typeof dynamic[key] === 'function'
1428
- ? dynamic[key](target, env)
1429
- : dynamic[key];
1430
- Object.assign(target, { [key]: value });
1431
- }
1432
- };
1433
- // 3) Apply dynamics in order
1434
1180
  const dotenv = { ...dotenvOverlaid };
1435
- applyDynamic(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
1436
- applyDynamic(dotenv, (sources.packaged?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1437
- applyDynamic(dotenv, (sources.project?.public?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1438
- applyDynamic(dotenv, (sources.project?.local?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1181
+ // Programmatic dynamic variables (when provided)
1182
+ applyDynamicMap(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
1183
+ // Packaged/project dynamics
1184
+ const packagedDyn = (sources.packaged?.dynamic ?? undefined);
1185
+ const publicDyn = (sources.project?.public?.dynamic ?? undefined);
1186
+ const localDyn = (sources.project?.local?.dynamic ?? undefined);
1187
+ applyDynamicMap(dotenv, packagedDyn, validated.env ?? validated.defaultEnv);
1188
+ applyDynamicMap(dotenv, publicDyn, validated.env ?? validated.defaultEnv);
1189
+ applyDynamicMap(dotenv, localDyn, validated.env ?? validated.defaultEnv);
1439
1190
  // file dynamicPath (lowest)
1440
1191
  if (validated.dynamicPath) {
1441
1192
  const absDynamicPath = path.resolve(validated.dynamicPath);
1442
- try {
1443
- const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
1444
- applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
1445
- }
1446
- catch {
1447
- throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
1448
- }
1193
+ await loadAndApplyDynamic(dotenv, absDynamicPath, validated.env ?? validated.defaultEnv, 'getdotenv-dynamic-host');
1449
1194
  }
1450
- // 4) Output/log/process merge
1195
+ // Effects:
1451
1196
  if (validated.outputPath) {
1452
- await fs.writeFile(validated.outputPath, Object.keys(dotenv).reduce((contents, key) => {
1453
- const value = dotenv[key] ?? '';
1454
- return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
1455
- }, ''), { encoding: 'utf-8' });
1197
+ await writeDotenvFile(validated.outputPath, dotenv);
1456
1198
  }
1457
- const logger = customOptions.logger ?? console;
1199
+ const logger = validated.logger;
1458
1200
  if (validated.log)
1459
1201
  logger.log(dotenv);
1460
1202
  if (validated.loadProcess)
1461
1203
  Object.assign(process.env, dotenv);
1462
- // 5) Merge and validate per-plugin config (packaged < project.public < project.local)
1204
+ // Merge and validate per-plugin config keyed by realized path (ns chain).
1463
1205
  const packagedPlugins = (sources.packaged &&
1464
1206
  sources.packaged.plugins) ??
1465
1207
  {};
@@ -1469,95 +1211,568 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1469
1211
  const localPlugins = (sources.project?.local &&
1470
1212
  sources.project.local.plugins) ??
1471
1213
  {};
1472
- // The by-id map is retained only for backwards-compat rendering paths
1473
- // (root help dynamic evaluation). Instance-bound access is the source
1474
- // of truth going forward and is populated below.
1475
- const mergedPluginConfigsById = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
1476
- for (const p of plugins) {
1477
- if (!p.id)
1478
- continue;
1479
- const slice = mergedPluginConfigsById[p.id];
1480
- // Build interpolation reference once per plugin:
1481
- const envRef = {
1482
- ...dotenv,
1483
- ...process.env,
1484
- };
1485
- const interpolated = slice && typeof slice === 'object'
1486
- ? interpolateDeep(slice, envRef)
1214
+ const entries = flattenPluginTreeByPath(plugins);
1215
+ const mergedPluginConfigsByPath = {};
1216
+ const envRef = {
1217
+ ...dotenv,
1218
+ ...process.env,
1219
+ };
1220
+ for (const e of entries) {
1221
+ const pathKey = e.path;
1222
+ const mergedRaw = defaultsDeep({}, packagedPlugins[pathKey] ?? {}, publicPlugins[pathKey] ?? {}, localPlugins[pathKey] ?? {});
1223
+ const interpolated = mergedRaw && typeof mergedRaw === 'object'
1224
+ ? interpolateDeep(mergedRaw, envRef)
1487
1225
  : {};
1488
- // Enforced: plugins always carry a schema (strict empty by default).
1489
- // Zod v4: avoid legacy multi-generic usage; treat as generic ZodObject.
1490
- const schema = p.configSchema;
1491
- const toParse = interpolated;
1492
- const parsed = schema.safeParse(toParse);
1493
- if (!parsed.success) {
1494
- const err = parsed.error;
1495
- const msgs = err.issues
1496
- .map((i) => {
1497
- const path = Array.isArray(i.path) ? i.path.join('.') : '';
1498
- const msg = typeof i.message === 'string' ? i.message : 'Invalid value';
1499
- return path ? `${path}: ${msg}` : msg;
1500
- })
1501
- .join('\n');
1502
- throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
1226
+ const schema = e.plugin.configSchema;
1227
+ if (schema) {
1228
+ const parsed = schema.safeParse(interpolated);
1229
+ if (!parsed.success) {
1230
+ const err = parsed.error;
1231
+ const msgs = err.issues
1232
+ .map((i) => {
1233
+ const pth = Array.isArray(i.path) ? i.path.join('.') : '';
1234
+ const msg = typeof i.message === 'string' ? i.message : 'Invalid value';
1235
+ return pth ? `${pth}: ${msg}` : msg;
1236
+ })
1237
+ .join('\n');
1238
+ throw new Error(`Invalid config for plugin at '${pathKey}':\n${msgs}`);
1239
+ }
1240
+ const frozen = Object.freeze(parsed.data);
1241
+ setPluginConfig(e.plugin, frozen);
1242
+ mergedPluginConfigsByPath[pathKey] = frozen;
1503
1243
  }
1504
- // Store a readonly (shallow-frozen) value for runtime safety.
1505
- const frozen = Object.freeze(parsed.data);
1506
- _setPluginConfigForInstance(p, frozen);
1507
- mergedPluginConfigsById[p.id] = frozen;
1244
+ else {
1245
+ const frozen = Object.freeze(interpolated);
1246
+ setPluginConfig(e.plugin, frozen);
1247
+ mergedPluginConfigsByPath[pathKey] = frozen;
1248
+ }
1249
+ }
1250
+ return {
1251
+ optionsResolved: validated,
1252
+ dotenv,
1253
+ plugins: {},
1254
+ pluginConfigs: mergedPluginConfigsByPath,
1255
+ };
1256
+ };
1257
+
1258
+ // Implementation
1259
+ function definePlugin(spec) {
1260
+ const { ...rest } = spec;
1261
+ const effectiveSchema = spec.configSchema ?? z.object({}).strict();
1262
+ const base = {
1263
+ ...rest,
1264
+ configSchema: effectiveSchema,
1265
+ children: [],
1266
+ use(child, override) {
1267
+ // Enforce sibling uniqueness at composition time.
1268
+ const desired = (override && typeof override.ns === 'string' && override.ns.length > 0
1269
+ ? override.ns
1270
+ : child.ns).trim();
1271
+ const collision = this.children.some((c) => {
1272
+ const ns = (c.override &&
1273
+ typeof c.override.ns === 'string' &&
1274
+ c.override.ns.length > 0
1275
+ ? c.override.ns
1276
+ : c.plugin.ns).trim();
1277
+ return ns === desired;
1278
+ });
1279
+ if (collision) {
1280
+ const under = this.ns && this.ns.length > 0 ? this.ns : 'root';
1281
+ throw new Error(`Duplicate namespace '${desired}' under '${under}'. ` +
1282
+ `Override via .use(plugin, { ns: '...' }).`);
1283
+ }
1284
+ this.children.push({ plugin: child, override });
1285
+ return this;
1286
+ },
1287
+ };
1288
+ const extended = base;
1289
+ extended.readConfig = function (_cli) {
1290
+ const value = getPluginConfig(extended);
1291
+ if (value === undefined) {
1292
+ throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
1293
+ }
1294
+ return value;
1295
+ };
1296
+ extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
1297
+ // Derive realized path strictly from the provided mount (leaf-up).
1298
+ const realizedPath = (() => {
1299
+ const parts = [];
1300
+ let node = cli;
1301
+ while (node.parent) {
1302
+ parts.push(node.name());
1303
+ node = node.parent;
1304
+ }
1305
+ return parts.reverse().join('/');
1306
+ })();
1307
+ return cli.createDynamicOption(flags, (c) => {
1308
+ const fromStore = getPluginConfig(extended);
1309
+ let cfgVal = fromStore ?? {};
1310
+ // Strict fallback only by realized path for help-time synthetic usage.
1311
+ if (!fromStore && realizedPath.length > 0) {
1312
+ const bag = c.plugins;
1313
+ const maybe = bag[realizedPath];
1314
+ if (maybe && typeof maybe === 'object') {
1315
+ cfgVal = maybe;
1316
+ }
1317
+ }
1318
+ // c is strictly typed as ResolvedHelpConfig from cli.createDynamicOption
1319
+ return desc(c, cfgVal);
1320
+ }, parser, defaultValue);
1321
+ };
1322
+ return extended;
1323
+ }
1324
+
1325
+ const dbg = (...args) => {
1326
+ if (process.env.GETDOTENV_DEBUG) {
1327
+ // Use stderr to avoid interfering with stdout assertions
1328
+ console.error('[getdotenv:run]', ...args);
1329
+ }
1330
+ };
1331
+ /**
1332
+ * Helper to decide whether to capture child stdio.
1333
+ * Checks GETDOTENV_STDIO env var or the provided bag capture flag.
1334
+ */
1335
+ const shouldCapture = (bagCapture) => process.env.GETDOTENV_STDIO === 'pipe' || Boolean(bagCapture);
1336
+ // Strip repeated symmetric outer quotes (single or double) until stable.
1337
+ // This is safe for argv arrays passed to execa (no quoting needed) and avoids
1338
+ // passing quote characters through to Node (e.g., for `node -e "<code>"`).
1339
+ // Handles stacked quotes from shells like PowerShell: """code""" -> code.
1340
+ const stripOuterQuotes = (s) => {
1341
+ let out = s;
1342
+ // Repeatedly trim only when the entire string is wrapped in matching quotes.
1343
+ // Stop as soon as the ends are asymmetric or no quotes remain.
1344
+ while (out.length >= 2) {
1345
+ const a = out.charAt(0);
1346
+ const b = out.charAt(out.length - 1);
1347
+ const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
1348
+ if (!symmetric)
1349
+ break;
1350
+ out = out.slice(1, -1);
1508
1351
  }
1352
+ return out;
1353
+ };
1354
+ // Extract exitCode/stdout/stderr from execa result or error in a tolerant way.
1355
+ const pickResult = (r) => {
1356
+ const exit = r.exitCode;
1357
+ const stdoutVal = r.stdout;
1358
+ const stderrVal = r.stderr;
1509
1359
  return {
1510
- optionsResolved: validated,
1511
- dotenv: dotenv,
1512
- plugins: {},
1513
- // Retained for legacy root help dynamic evaluation only. Instance-bound
1514
- // access is used by plugins themselves and tests/docs moving forward.
1515
- pluginConfigs: mergedPluginConfigsById,
1360
+ exitCode: typeof exit === 'number' ? exit : Number.NaN,
1361
+ stdout: typeof stdoutVal === 'string' ? stdoutVal : '',
1362
+ stderr: typeof stderrVal === 'string' ? stderrVal : '',
1516
1363
  };
1517
1364
  };
1518
-
1519
- // Registry for dynamic descriptions keyed by Option (WeakMap so GC-friendly)
1520
- const DYN_DESC = new WeakMap();
1365
+ // Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
1366
+ // expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
1367
+ const sanitizeEnv = (env) => {
1368
+ if (!env)
1369
+ return undefined;
1370
+ const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
1371
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
1372
+ };
1521
1373
  /**
1522
- * Create an Option with a dynamic description callback stored in DYN_DESC.
1374
+ * Core executor that normalizes shell/plain forms and capture/inherit modes.
1375
+ * Returns captured buffers; callers may stream stdout when desired.
1523
1376
  */
1524
- function makeDynamicOption(flags, desc, parser, defaultValue) {
1525
- const opt = new Option(flags, '');
1526
- DYN_DESC.set(opt, desc);
1527
- if (parser) {
1528
- opt.argParser((value, previous) => parser(value, previous));
1377
+ async function _execNormalized(command, shell, opts = {}) {
1378
+ const envSan = sanitizeEnv(opts.env);
1379
+ const timeoutBits = typeof opts.timeoutMs === 'number'
1380
+ ? { timeout: opts.timeoutMs, killSignal: 'SIGKILL' }
1381
+ : {};
1382
+ const stdio = opts.stdio ?? 'pipe';
1383
+ if (shell === false) {
1384
+ let file;
1385
+ let args = [];
1386
+ if (typeof command === 'string') {
1387
+ const tokens = tokenize(command);
1388
+ file = tokens[0];
1389
+ args = tokens.slice(1);
1390
+ }
1391
+ else {
1392
+ file = command[0];
1393
+ args = command.slice(1).map(stripOuterQuotes);
1394
+ }
1395
+ if (!file)
1396
+ return { exitCode: 0, stdout: '', stderr: '' };
1397
+ dbg('exec (plain)', { file, args, stdio });
1398
+ try {
1399
+ const ok = pickResult((await execa(file, args, {
1400
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
1401
+ ...(envSan !== undefined ? { env: envSan } : {}),
1402
+ stdio,
1403
+ ...timeoutBits,
1404
+ })));
1405
+ dbg('exit (plain)', { exitCode: ok.exitCode });
1406
+ return ok;
1407
+ }
1408
+ catch (e) {
1409
+ const out = pickResult(e);
1410
+ dbg('exit:error (plain)', { exitCode: out.exitCode });
1411
+ return out;
1412
+ }
1529
1413
  }
1530
- if (defaultValue !== undefined)
1531
- opt.default(defaultValue);
1532
- return opt;
1414
+ // Shell path (string|true|URL): execaCommand handles shell resolution.
1415
+ const commandStr = typeof command === 'string' ? command : command.join(' ');
1416
+ dbg('exec (shell)', {
1417
+ command: commandStr,
1418
+ shell: typeof shell === 'string' ? shell : 'custom',
1419
+ stdio,
1420
+ });
1421
+ try {
1422
+ const ok = pickResult((await execaCommand(commandStr, {
1423
+ shell,
1424
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
1425
+ ...(envSan !== undefined ? { env: envSan } : {}),
1426
+ stdio,
1427
+ ...timeoutBits,
1428
+ })));
1429
+ dbg('exit (shell)', { exitCode: ok.exitCode });
1430
+ return ok;
1431
+ }
1432
+ catch (e) {
1433
+ const out = pickResult(e);
1434
+ dbg('exit:error (shell)', { exitCode: out.exitCode });
1435
+ return out;
1436
+ }
1437
+ }
1438
+ async function runCommandResult(command, shell, opts = {}) {
1439
+ // Build opts without injecting undefined (exactOptionalPropertyTypes-safe)
1440
+ const coreOpts = { stdio: 'pipe' };
1441
+ if (opts.cwd !== undefined) {
1442
+ coreOpts.cwd = opts.cwd;
1443
+ }
1444
+ if (opts.env !== undefined) {
1445
+ coreOpts.env = opts.env;
1446
+ }
1447
+ if (opts.timeoutMs !== undefined) {
1448
+ coreOpts.timeoutMs = opts.timeoutMs;
1449
+ }
1450
+ return _execNormalized(command, shell, coreOpts);
1451
+ }
1452
+ async function runCommand(command, shell, opts) {
1453
+ // Build opts without injecting undefined (exactOptionalPropertyTypes-safe)
1454
+ const callOpts = {};
1455
+ if (opts.cwd !== undefined) {
1456
+ callOpts.cwd = opts.cwd;
1457
+ }
1458
+ if (opts.env !== undefined) {
1459
+ callOpts.env = opts.env;
1460
+ }
1461
+ if (opts.stdio !== undefined)
1462
+ callOpts.stdio = opts.stdio;
1463
+ const ok = await _execNormalized(command, shell, callOpts);
1464
+ if (opts.stdio === 'pipe' && ok.stdout) {
1465
+ process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
1466
+ }
1467
+ return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
1533
1468
  }
1469
+
1534
1470
  /**
1535
- * Evaluate dynamic descriptions across a command tree using the resolved config.
1471
+ * Attach root flags to a {@link GetDotenvCli} instance.
1472
+ *
1473
+ * Program is typed as {@link GetDotenvCli} and supports {@link GetDotenvCli.createDynamicOption | createDynamicOption}.
1536
1474
  */
1537
- function evaluateDynamicOptions(root, resolved) {
1538
- const visit = (cmd) => {
1539
- const arr = cmd.options;
1540
- for (const o of arr) {
1541
- const dyn = DYN_DESC.get(o);
1542
- if (typeof dyn === 'function') {
1543
- try {
1544
- const txt = dyn(resolved);
1545
- // Commander uses Option.description during help rendering.
1546
- o.description = txt;
1547
- }
1548
- catch {
1549
- /* best-effort; leave as-is */
1550
- }
1551
- }
1475
+ const attachRootOptions = (program, defaults) => {
1476
+ const GROUP = 'base';
1477
+ const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
1478
+ const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
1479
+ const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
1480
+ // Helper: append (default) tags for ON/OFF toggles
1481
+ const onOff = (on, isDefault) => on
1482
+ ? `ON${isDefault ? ' (default)' : ''}`
1483
+ : `OFF${isDefault ? ' (default)' : ''}`;
1484
+ program.enablePositionalOptions().passThroughOptions();
1485
+ // -e, --env <string>
1486
+ {
1487
+ const opt = new Option('-e, --env <string>', 'target environment (dotenv-expanded)');
1488
+ opt.argParser(dotenvExpandFromProcessEnv);
1489
+ if (env !== undefined)
1490
+ opt.default(env);
1491
+ program.addOption(opt);
1492
+ program.setOptionGroup(opt, GROUP);
1493
+ }
1494
+ // -v, --vars <string>
1495
+ {
1496
+ const examples = [
1497
+ ['KEY1', 'VAL1'],
1498
+ ['KEY2', 'VAL2'],
1499
+ ]
1500
+ .map((v) => v.join(va))
1501
+ .join(vd);
1502
+ const opt = new Option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${examples}`);
1503
+ opt.argParser(dotenvExpandFromProcessEnv);
1504
+ program.addOption(opt);
1505
+ program.setOptionGroup(opt, GROUP);
1506
+ }
1507
+ // Output path (interpolated later; help can remain static)
1508
+ {
1509
+ const opt = new Option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)');
1510
+ opt.argParser(dotenvExpandFromProcessEnv);
1511
+ if (outputPath !== undefined)
1512
+ opt.default(outputPath);
1513
+ program.addOption(opt);
1514
+ program.setOptionGroup(opt, GROUP);
1515
+ }
1516
+ // Shell ON (string or boolean true => default shell)
1517
+ {
1518
+ const opt = program
1519
+ .createDynamicOption('-s, --shell [string]', (cfg) => {
1520
+ const s = cfg.shell;
1521
+ let tag = '';
1522
+ if (typeof s === 'boolean' && s)
1523
+ tag = ' (default OS shell)';
1524
+ else if (typeof s === 'string' && s.length > 0)
1525
+ tag = ` (default ${s})`;
1526
+ return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
1527
+ })
1528
+ .conflicts('shellOff');
1529
+ program.addOption(opt);
1530
+ program.setOptionGroup(opt, GROUP);
1531
+ }
1532
+ // Shell OFF
1533
+ {
1534
+ const opt = program
1535
+ .createDynamicOption('-S, --shell-off', (cfg) => {
1536
+ const s = cfg.shell;
1537
+ return `command execution shell OFF${s === false ? ' (default)' : ''}`;
1538
+ })
1539
+ .conflicts('shell');
1540
+ program.addOption(opt);
1541
+ program.setOptionGroup(opt, GROUP);
1542
+ }
1543
+ // Load process ON/OFF (dynamic defaults)
1544
+ {
1545
+ const optOn = program
1546
+ .createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
1547
+ .conflicts('loadProcessOff');
1548
+ program.addOption(optOn);
1549
+ program.setOptionGroup(optOn, GROUP);
1550
+ const optOff = program
1551
+ .createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
1552
+ .conflicts('loadProcess');
1553
+ program.addOption(optOff);
1554
+ program.setOptionGroup(optOff, GROUP);
1555
+ }
1556
+ // Exclusion master toggle (dynamic)
1557
+ {
1558
+ const optAll = program
1559
+ .createDynamicOption('-a, --exclude-all', (cfg) => {
1560
+ const allOn = !!cfg.excludeDynamic &&
1561
+ ((!!cfg.excludeEnv && !!cfg.excludeGlobal) ||
1562
+ (!!cfg.excludePrivate && !!cfg.excludePublic));
1563
+ const suffix = allOn ? ' (default)' : '';
1564
+ return `exclude all dotenv variables from loading ON${suffix}`;
1565
+ })
1566
+ .conflicts('excludeAllOff');
1567
+ program.addOption(optAll);
1568
+ program.setOptionGroup(optAll, GROUP);
1569
+ const optAllOff = new Option('-A, --exclude-all-off', 'exclude all dotenv variables from loading OFF (default)').conflicts('excludeAll');
1570
+ program.addOption(optAllOff);
1571
+ program.setOptionGroup(optAllOff, GROUP);
1572
+ }
1573
+ // Per-family exclusions (dynamic defaults)
1574
+ {
1575
+ const o1 = program
1576
+ .createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
1577
+ .conflicts('excludeDynamicOff');
1578
+ program.addOption(o1);
1579
+ program.setOptionGroup(o1, GROUP);
1580
+ const o2 = program
1581
+ .createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
1582
+ .conflicts('excludeDynamic');
1583
+ program.addOption(o2);
1584
+ program.setOptionGroup(o2, GROUP);
1585
+ }
1586
+ {
1587
+ const o1 = program
1588
+ .createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
1589
+ .conflicts('excludeEnvOff');
1590
+ program.addOption(o1);
1591
+ program.setOptionGroup(o1, GROUP);
1592
+ const o2 = program
1593
+ .createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
1594
+ .conflicts('excludeEnv');
1595
+ program.addOption(o2);
1596
+ program.setOptionGroup(o2, GROUP);
1597
+ }
1598
+ {
1599
+ const o1 = program
1600
+ .createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
1601
+ .conflicts('excludeGlobalOff');
1602
+ program.addOption(o1);
1603
+ program.setOptionGroup(o1, GROUP);
1604
+ const o2 = program
1605
+ .createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
1606
+ .conflicts('excludeGlobal');
1607
+ program.addOption(o2);
1608
+ program.setOptionGroup(o2, GROUP);
1609
+ }
1610
+ {
1611
+ const p1 = program
1612
+ .createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
1613
+ .conflicts('excludePrivateOff');
1614
+ program.addOption(p1);
1615
+ program.setOptionGroup(p1, GROUP);
1616
+ const p2 = program
1617
+ .createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
1618
+ .conflicts('excludePrivate');
1619
+ program.addOption(p2);
1620
+ program.setOptionGroup(p2, GROUP);
1621
+ const pu1 = program
1622
+ .createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
1623
+ .conflicts('excludePublicOff');
1624
+ program.addOption(pu1);
1625
+ program.setOptionGroup(pu1, GROUP);
1626
+ const pu2 = program
1627
+ .createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
1628
+ .conflicts('excludePublic');
1629
+ program.addOption(pu2);
1630
+ program.setOptionGroup(pu2, GROUP);
1631
+ }
1632
+ // Log ON/OFF (dynamic)
1633
+ {
1634
+ const lo = program
1635
+ .createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
1636
+ .conflicts('logOff');
1637
+ program.addOption(lo);
1638
+ program.setOptionGroup(lo, GROUP);
1639
+ const lf = program
1640
+ .createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
1641
+ .conflicts('log');
1642
+ program.addOption(lf);
1643
+ program.setOptionGroup(lf, GROUP);
1644
+ }
1645
+ // Capture flag (no default display; static)
1646
+ {
1647
+ const opt = new Option('--capture', 'capture child process stdio for commands (tests/CI)');
1648
+ program.addOption(opt);
1649
+ program.setOptionGroup(opt, GROUP);
1650
+ }
1651
+ // Core bootstrap/static flags (kept static in help)
1652
+ {
1653
+ const o1 = new Option('--default-env <string>', 'default target environment');
1654
+ o1.argParser(dotenvExpandFromProcessEnv);
1655
+ if (defaultEnv !== undefined)
1656
+ o1.default(defaultEnv);
1657
+ program.addOption(o1);
1658
+ program.setOptionGroup(o1, GROUP);
1659
+ const o2 = new Option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file');
1660
+ o2.argParser(dotenvExpandFromProcessEnv);
1661
+ if (dotenvToken !== undefined)
1662
+ o2.default(dotenvToken);
1663
+ program.addOption(o2);
1664
+ program.setOptionGroup(o2, GROUP);
1665
+ const o3 = new Option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)');
1666
+ o3.argParser(dotenvExpandFromProcessEnv);
1667
+ if (dynamicPath !== undefined)
1668
+ o3.default(dynamicPath);
1669
+ program.addOption(o3);
1670
+ program.setOptionGroup(o3, GROUP);
1671
+ const o4 = new Option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory');
1672
+ o4.argParser(dotenvExpandFromProcessEnv);
1673
+ if (paths !== undefined)
1674
+ o4.default(paths);
1675
+ program.addOption(o4);
1676
+ program.setOptionGroup(o4, GROUP);
1677
+ const o5 = new Option('--paths-delimiter <string>', 'paths delimiter string');
1678
+ if (pathsDelimiter !== undefined)
1679
+ o5.default(pathsDelimiter);
1680
+ program.addOption(o5);
1681
+ program.setOptionGroup(o5, GROUP);
1682
+ const o6 = new Option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern');
1683
+ if (pathsDelimiterPattern !== undefined)
1684
+ o6.default(pathsDelimiterPattern);
1685
+ program.addOption(o6);
1686
+ program.setOptionGroup(o6, GROUP);
1687
+ const o7 = new Option('--private-token <string>', 'dotenv-expanded token indicating private variables');
1688
+ o7.argParser(dotenvExpandFromProcessEnv);
1689
+ if (privateToken !== undefined)
1690
+ o7.default(privateToken);
1691
+ program.addOption(o7);
1692
+ program.setOptionGroup(o7, GROUP);
1693
+ const o8 = new Option('--vars-delimiter <string>', 'vars delimiter string');
1694
+ if (varsDelimiter !== undefined)
1695
+ o8.default(varsDelimiter);
1696
+ program.addOption(o8);
1697
+ program.setOptionGroup(o8, GROUP);
1698
+ const o9 = new Option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern');
1699
+ if (varsDelimiterPattern !== undefined)
1700
+ o9.default(varsDelimiterPattern);
1701
+ program.addOption(o9);
1702
+ program.setOptionGroup(o9, GROUP);
1703
+ const o10 = new Option('--vars-assignor <string>', 'vars assignment operator string');
1704
+ if (varsAssignor !== undefined)
1705
+ o10.default(varsAssignor);
1706
+ program.addOption(o10);
1707
+ program.setOptionGroup(o10, GROUP);
1708
+ const o11 = new Option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern');
1709
+ if (varsAssignorPattern !== undefined)
1710
+ o11.default(varsAssignorPattern);
1711
+ program.addOption(o11);
1712
+ program.setOptionGroup(o11, GROUP);
1713
+ }
1714
+ // Diagnostics / validation / entropy
1715
+ {
1716
+ const tr = new Option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
1717
+ program.addOption(tr);
1718
+ program.setOptionGroup(tr, GROUP);
1719
+ const st = new Option('--strict', 'fail on env validation errors (schema/requiredKeys)');
1720
+ program.addOption(st);
1721
+ program.setOptionGroup(st, GROUP);
1722
+ }
1723
+ {
1724
+ const w = program
1725
+ .createDynamicOption('--entropy-warn', (cfg) => {
1726
+ const warn = cfg.warnEntropy;
1727
+ // Default is effectively ON when warnEntropy is true or undefined.
1728
+ return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
1729
+ })
1730
+ .conflicts('entropyWarnOff');
1731
+ program.addOption(w);
1732
+ program.setOptionGroup(w, GROUP);
1733
+ const woff = program
1734
+ .createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
1735
+ .conflicts('entropyWarn');
1736
+ program.addOption(woff);
1737
+ program.setOptionGroup(woff, GROUP);
1738
+ const th = new Option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)');
1739
+ program.addOption(th);
1740
+ program.setOptionGroup(th, GROUP);
1741
+ const ml = new Option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)');
1742
+ program.addOption(ml);
1743
+ program.setOptionGroup(ml, GROUP);
1744
+ const wl = new Option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern');
1745
+ program.addOption(wl);
1746
+ program.setOptionGroup(wl, GROUP);
1747
+ const rp = new Option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
1748
+ program.addOption(rp);
1749
+ program.setOptionGroup(rp, GROUP);
1750
+ // Redact ON/OFF (dynamic)
1751
+ {
1752
+ const rOn = program
1753
+ .createDynamicOption('--redact', (cfg) => `presentation-time redaction for secret-like keys ON${cfg.redact ? ' (default)' : ''}`)
1754
+ .conflicts('redactOff');
1755
+ program.addOption(rOn);
1756
+ program.setOptionGroup(rOn, GROUP);
1757
+ const rOff = program
1758
+ .createDynamicOption('--redact-off', (cfg) => `presentation-time redaction for secret-like keys OFF${cfg.redact === false ? ' (default)' : ''}`)
1759
+ .conflicts('redact');
1760
+ program.addOption(rOff);
1761
+ program.setOptionGroup(rOff, GROUP);
1552
1762
  }
1553
- for (const c of cmd.commands)
1554
- visit(c);
1555
- };
1556
- visit(root);
1557
- }
1763
+ }
1764
+ return program;
1765
+ };
1558
1766
 
1559
- // Registry for grouping; root help renders groups between Options and Commands.
1767
+ /**
1768
+ * Registry for option grouping.
1769
+ * Root help renders these groups between "Options" and "Commands".
1770
+ */
1560
1771
  const GROUP_TAG = new WeakMap();
1772
+ /**
1773
+ * Render help option groups (App/Plugins) for a given command.
1774
+ * Groups are injected between Options and Commands in the help output.
1775
+ */
1561
1776
  function renderOptionGroups(cmd) {
1562
1777
  const all = cmd.options;
1563
1778
  const byGroup = new Map();
@@ -1611,6 +1826,275 @@ function renderOptionGroups(cmd) {
1611
1826
  return out;
1612
1827
  }
1613
1828
 
1829
+ /**
1830
+ * Compose root/parent help output by inserting grouped sections between
1831
+ * Options and Commands, ensuring a trailing blank line.
1832
+ *
1833
+ * @param base - Base help text produced by Commander.
1834
+ * @param cmd - Command instance whose grouped options should be rendered.
1835
+ * @returns The modified help text with grouped blocks inserted.
1836
+ */
1837
+ function buildHelpInformation(base, cmd) {
1838
+ const groups = renderOptionGroups(cmd);
1839
+ const block = typeof groups === 'string' ? groups.trim() : '';
1840
+ if (!block) {
1841
+ return base.endsWith('\n\n')
1842
+ ? base
1843
+ : base.endsWith('\n')
1844
+ ? `${base}\n`
1845
+ : `${base}\n\n`;
1846
+ }
1847
+ const marker = '\nCommands:';
1848
+ const idx = base.indexOf(marker);
1849
+ let out = base;
1850
+ if (idx >= 0) {
1851
+ const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
1852
+ out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
1853
+ }
1854
+ else {
1855
+ const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
1856
+ out = `${base}${sep}${groups}`;
1857
+ }
1858
+ return out.endsWith('\n\n')
1859
+ ? out
1860
+ : out.endsWith('\n')
1861
+ ? `${out}\n`
1862
+ : `${out}\n\n`;
1863
+ }
1864
+
1865
+ /** src/cliHost/GetDotenvCli/dynamicOptions.ts
1866
+ * Helpers for dynamic option descriptions and evaluation.
1867
+ */
1868
+ /**
1869
+ * Registry for dynamic descriptions keyed by Option (WeakMap for GC safety).
1870
+ */
1871
+ const DYN_DESC = new WeakMap();
1872
+ /**
1873
+ * Create an Option with a dynamic description callback stored in DYN_DESC.
1874
+ */
1875
+ function makeDynamicOption(flags, desc, parser, defaultValue) {
1876
+ const opt = new Option(flags, '');
1877
+ DYN_DESC.set(opt, desc);
1878
+ if (parser) {
1879
+ opt.argParser((value, previous) => parser(value, previous));
1880
+ }
1881
+ if (defaultValue !== undefined)
1882
+ opt.default(defaultValue);
1883
+ // Commander.Option is structurally compatible; help-time wiring is stored in DYN_DESC.
1884
+ return opt;
1885
+ }
1886
+ /**
1887
+ * Evaluate dynamic descriptions across a command tree using the resolved config.
1888
+ */
1889
+ function evaluateDynamicOptions(root, resolved) {
1890
+ const visit = (cmd) => {
1891
+ const arr = cmd.options;
1892
+ for (const o of arr) {
1893
+ const dyn = DYN_DESC.get(o);
1894
+ if (typeof dyn === 'function') {
1895
+ try {
1896
+ const txt = dyn(resolved);
1897
+ // Commander uses Option.description during help rendering.
1898
+ o.description = txt;
1899
+ }
1900
+ catch {
1901
+ /* best-effort; leave as-is */
1902
+ }
1903
+ }
1904
+ }
1905
+ for (const c of cmd.commands)
1906
+ visit(c);
1907
+ };
1908
+ visit(root);
1909
+ }
1910
+
1911
+ function initializeInstance(cli, headerGetter) {
1912
+ // Configure grouped help: show only base options in default "Options";
1913
+ // subcommands show all of their own options.
1914
+ cli.configureHelp({
1915
+ visibleOptions: (cmd) => {
1916
+ const all = cmd.options;
1917
+ const isRoot = cmd.parent === null;
1918
+ const list = isRoot
1919
+ ? all.filter((opt) => {
1920
+ const group = GROUP_TAG.get(opt);
1921
+ return group === 'base';
1922
+ })
1923
+ : all.slice();
1924
+ // Sort: short-aliased options first, then long-only; stable by flags.
1925
+ const hasShort = (opt) => {
1926
+ const flags = opt.flags;
1927
+ return /(^|\s|,)-[A-Za-z]/.test(flags);
1928
+ };
1929
+ const byFlags = (opt) => opt.flags;
1930
+ list.sort((a, b) => {
1931
+ const aS = hasShort(a) ? 1 : 0;
1932
+ const bS = hasShort(b) ? 1 : 0;
1933
+ return bS - aS || byFlags(a).localeCompare(byFlags(b));
1934
+ });
1935
+ return list;
1936
+ },
1937
+ });
1938
+ // Optional branded header before help text (kept minimal and deterministic).
1939
+ cli.addHelpText('beforeAll', () => {
1940
+ const header = headerGetter();
1941
+ return header && header.length > 0 ? `${header}\n\n` : '';
1942
+ });
1943
+ // Tests-only: suppress process.exit during help/version flows under Vitest.
1944
+ // Unit tests often construct GetDotenvCli directly (bypassing createCli),
1945
+ // so install a local exitOverride when a test environment is detected.
1946
+ const underTests = process.env.GETDOTENV_TEST === '1' ||
1947
+ typeof process.env.VITEST_WORKER_ID === 'string';
1948
+ if (underTests) {
1949
+ cli.exitOverride((err) => {
1950
+ const code = err?.code;
1951
+ if (code === 'commander.helpDisplayed' ||
1952
+ code === 'commander.version' ||
1953
+ code === 'commander.help')
1954
+ return;
1955
+ throw err;
1956
+ });
1957
+ }
1958
+ // Ensure the root has a no-op action so preAction hooks installed by
1959
+ // passOptions() fire for root-only invocations (no subcommand).
1960
+ // Subcommands still take precedence and will not hit this action.
1961
+ // This keeps root-side effects (e.g., --log) working in direct hosts/tests.
1962
+ cli.action(() => {
1963
+ /* no-op */
1964
+ });
1965
+ // PreSubcommand hook: compute a context if absent, without mutating process.env.
1966
+ // The passOptions() helper, when installed, resolves the final context.
1967
+ cli.hook('preSubcommand', async () => {
1968
+ if (cli.hasCtx())
1969
+ return;
1970
+ await cli.resolveAndLoad({ loadProcess: false });
1971
+ });
1972
+ }
1973
+
1974
+ /**
1975
+ * Determine the effective namespace for a child plugin (override \> default).
1976
+ */
1977
+ const effectiveNs = (child) => {
1978
+ const o = child.override;
1979
+ return (o && typeof o.ns === 'string' && o.ns.length > 0 ? o.ns : child.plugin.ns).trim();
1980
+ };
1981
+ const isPromise = (v) => !!v && typeof v.then === 'function';
1982
+ function runInstall(parentCli, plugin) {
1983
+ // Create mount and run setup
1984
+ const mount = parentCli.ns(plugin.ns);
1985
+ const setupRet = plugin.setup(mount);
1986
+ const pending = [];
1987
+ if (isPromise(setupRet))
1988
+ pending.push(setupRet.then(() => undefined));
1989
+ // Enforce sibling uniqueness before creating children
1990
+ const names = new Set();
1991
+ for (const entry of plugin.children) {
1992
+ const ns = effectiveNs(entry);
1993
+ if (names.has(ns)) {
1994
+ const under = mount.name();
1995
+ throw new Error(`Duplicate namespace '${ns}' under '${under || 'root'}'. Override via .use(plugin, { ns: '...' }).`);
1996
+ }
1997
+ names.add(ns);
1998
+ }
1999
+ // Install children (pre-order), synchronously when possible
2000
+ for (const entry of plugin.children) {
2001
+ const childRet = runInstall(mount, entry.plugin);
2002
+ if (isPromise(childRet))
2003
+ pending.push(childRet);
2004
+ }
2005
+ if (pending.length > 0)
2006
+ return Promise.all(pending).then(() => undefined);
2007
+ return;
2008
+ }
2009
+ /**
2010
+ * Install a plugin and its children (pre-order setup phase).
2011
+ * Enforces sibling namespace uniqueness.
2012
+ */
2013
+ function setupPluginTree(cli, plugin) {
2014
+ const ret = runInstall(cli, plugin);
2015
+ return isPromise(ret) ? ret : Promise.resolve();
2016
+ }
2017
+
2018
+ /**
2019
+ * Resolve options strictly and compute the dotenv context via the loader/overlay path.
2020
+ *
2021
+ * @param customOptions - Partial options overlay.
2022
+ * @param plugins - Plugins list for config validation.
2023
+ * @param hostMetaUrl - Import URL for resolving the packaged root.
2024
+ */
2025
+ async function resolveAndComputeContext(customOptions, plugins, hostMetaUrl) {
2026
+ const optionsResolved = await resolveGetDotenvOptions(customOptions);
2027
+ // Strict schema validation
2028
+ getDotenvOptionsSchemaResolved.parse(optionsResolved);
2029
+ const ctx = await computeContext(optionsResolved, plugins, hostMetaUrl);
2030
+ return ctx;
2031
+ }
2032
+
2033
+ /**
2034
+ * Run afterResolve hooks for a plugin tree (parent → children).
2035
+ */
2036
+ async function runAfterResolveTree(cli, plugins, ctx) {
2037
+ const run = async (p) => {
2038
+ if (p.afterResolve)
2039
+ await p.afterResolve(cli, ctx);
2040
+ for (const child of p.children)
2041
+ await run(child.plugin);
2042
+ };
2043
+ for (const p of plugins)
2044
+ await run(p);
2045
+ }
2046
+
2047
+ /**
2048
+ * Temporarily tag options added during a callback as 'app' for grouped help.
2049
+ * Wraps `addOption` on the command instance.
2050
+ */
2051
+ function tagAppOptionsAround(root, setOptionGroup, fn) {
2052
+ const originalAddOption = root.addOption.bind(root);
2053
+ root.addOption = ((opt) => {
2054
+ setOptionGroup(opt, 'app');
2055
+ return originalAddOption(opt);
2056
+ });
2057
+ try {
2058
+ return fn(root);
2059
+ }
2060
+ finally {
2061
+ root.addOption = originalAddOption;
2062
+ }
2063
+ }
2064
+
2065
+ /**
2066
+ * Read the version from the nearest `package.json` relative to the provided import URL.
2067
+ *
2068
+ * @param importMetaUrl - The `import.meta.url` of the calling module.
2069
+ * @returns The version string or undefined if not found.
2070
+ */
2071
+ async function readPkgVersion(importMetaUrl) {
2072
+ if (!importMetaUrl)
2073
+ return undefined;
2074
+ try {
2075
+ const fromUrl = fileURLToPath(importMetaUrl);
2076
+ const pkgDir = await packageDirectory({ cwd: fromUrl });
2077
+ if (!pkgDir)
2078
+ return undefined;
2079
+ const txt = await fs.readFile(`${pkgDir}/package.json`, 'utf-8');
2080
+ const pkg = JSON.parse(txt);
2081
+ return pkg.version ?? undefined;
2082
+ }
2083
+ catch {
2084
+ // best-effort only
2085
+ return undefined;
2086
+ }
2087
+ }
2088
+
2089
+ /** src/cliHost/GetDotenvCli.ts
2090
+ * Plugin-first CLI host for get-dotenv with Commander generics preserved.
2091
+ * Public surface implements GetDotenvCliPublic and provides:
2092
+ * - attachRootOptions (builder-only; no public override wiring)
2093
+ * - resolveAndLoad (strict resolve + context compute)
2094
+ * - getCtx/hasCtx accessors
2095
+ * - ns() for typed subcommand creation with duplicate-name guard
2096
+ * - grouped help rendering with dynamic option descriptions
2097
+ */
1614
2098
  const HOST_META_URL = import.meta.url;
1615
2099
  const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
1616
2100
  const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
@@ -1624,11 +2108,13 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
1624
2108
  * - Provide a namespacing helper (ns).
1625
2109
  * - Support composable plugins with parent → children install and afterResolve.
1626
2110
  */
1627
- let GetDotenvCli$1 = class GetDotenvCli extends Command {
2111
+ class GetDotenvCli extends Command {
1628
2112
  /** Registered top-level plugins (composition happens via .use()) */
1629
2113
  _plugins = [];
1630
2114
  /** One-time installation guard */
1631
2115
  _installed = false;
2116
+ /** In-flight installation promise to guard against concurrent installs */
2117
+ _installing;
1632
2118
  /** Optional header line to prepend in help output */
1633
2119
  [HELP_HEADER_SYMBOL];
1634
2120
  /** Context/options stored under symbols (typed) */
@@ -1639,56 +2125,23 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1639
2125
  * dynamicOption on children.
1640
2126
  */
1641
2127
  createCommand(name) {
1642
- // Explicitly construct a GetDotenvCli (drop subclass constructor semantics).
2128
+ // Explicitly construct a GetDotenvCli for children to preserve helpers.
1643
2129
  return new GetDotenvCli(name);
1644
2130
  }
1645
2131
  constructor(alias = 'getdotenv') {
1646
2132
  super(alias);
1647
- // Ensure subcommands that use passThroughOptions can be attached safely.
1648
- // Commander requires parent commands to enable positional options when a
1649
- // child uses passThroughOptions.
1650
2133
  this.enablePositionalOptions();
1651
- // Configure grouped help: show only base options in default "Options";
1652
- // we will insert App/Plugin sections before Commands in helpInformation().
1653
- this.configureHelp({
1654
- visibleOptions: (cmd) => {
1655
- const all = cmd.options;
1656
- const isRoot = cmd.parent === null;
1657
- const list = isRoot
1658
- ? all.filter((opt) => {
1659
- const group = GROUP_TAG.get(opt);
1660
- return group === 'base';
1661
- })
1662
- : all.slice(); // subcommands: show all options (their own "Options:" block)
1663
- // Sort: short-aliased options first, then long-only; stable by flags.
1664
- const hasShort = (opt) => {
1665
- const flags = opt.flags;
1666
- // Matches "-x," or starting "-x " before any long
1667
- return /(^|\s|,)-[A-Za-z]/.test(flags);
1668
- };
1669
- const byFlags = (opt) => opt.flags;
1670
- list.sort((a, b) => {
1671
- const aS = hasShort(a) ? 1 : 0;
1672
- const bS = hasShort(b) ? 1 : 0;
1673
- return bS - aS || byFlags(a).localeCompare(byFlags(b));
1674
- });
1675
- return list;
1676
- },
1677
- });
1678
- this.addHelpText('beforeAll', () => {
1679
- const header = this[HELP_HEADER_SYMBOL];
1680
- return header && header.length > 0 ? `${header}\n\n` : '';
1681
- });
1682
- // Skeleton preSubcommand hook: produce a context if absent, without
1683
- // mutating process.env. The passOptions hook (when installed) will
1684
- // compute the final context using merged CLI options; keeping
1685
- // loadProcess=false here avoids leaking dotenv values into the parent
1686
- // process env before subcommands execute.
1687
- this.hook('preSubcommand', async () => {
1688
- if (this.getCtx())
1689
- return;
1690
- await this.resolveAndLoad({ loadProcess: false });
1691
- });
2134
+ // Delegate the heavy setup to a helper to keep the constructor lean.
2135
+ initializeInstance(this, () => this[HELP_HEADER_SYMBOL]);
2136
+ }
2137
+ /**
2138
+ * Attach legacy/base root flags to this CLI instance.
2139
+ * Delegates to the pure builder in attachRootOptions.ts.
2140
+ */
2141
+ attachRootOptions(defaults) {
2142
+ const d = (defaults ?? baseRootOptionDefaults);
2143
+ attachRootOptions(this, d);
2144
+ return this;
1692
2145
  }
1693
2146
  /**
1694
2147
  * Resolve options (strict) and compute dotenv context.
@@ -1700,11 +2153,9 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1700
2153
  * long-running side-effects while still evaluating dynamic help text.
1701
2154
  */
1702
2155
  async resolveAndLoad(customOptions = {}, opts) {
1703
- // Resolve defaults, then validate strictly under the new host.
1704
- const optionsResolved = await resolveGetDotenvOptions(customOptions);
1705
- getDotenvOptionsSchemaResolved.parse(optionsResolved);
1706
- // Delegate the heavy lifting to the shared helper (guarded path supported).
1707
- const ctx = await computeContext(optionsResolved, this._plugins, HOST_META_URL);
2156
+ const ctx = await resolveAndComputeContext(customOptions,
2157
+ // Pass only plugin instances to the resolver (not entries with overrides)
2158
+ this._plugins.map((e) => e.plugin), HOST_META_URL);
1708
2159
  // Persist context on the instance for later access.
1709
2160
  this[CTX_SYMBOL] = ctx;
1710
2161
  // Ensure plugins are installed exactly once, then run afterResolve.
@@ -1718,14 +2169,6 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1718
2169
  createDynamicOption(flags, desc, parser, defaultValue) {
1719
2170
  return makeDynamicOption(flags, (c) => desc(c), parser, defaultValue);
1720
2171
  }
1721
- /**
1722
- * Chainable helper mirroring .option(), but with a dynamic description.
1723
- * Equivalent to addOption(createDynamicOption(...)).
1724
- */
1725
- dynamicOption(flags, desc, parser, defaultValue) {
1726
- this.addOption(this.createDynamicOption(flags, desc, parser, defaultValue));
1727
- return this;
1728
- }
1729
2172
  /**
1730
2173
  * Evaluate dynamic descriptions for this command and all descendants using
1731
2174
  * the provided resolved configuration. Mutates the Option.description in
@@ -1734,24 +2177,58 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1734
2177
  evaluateDynamicOptions(resolved) {
1735
2178
  evaluateDynamicOptions(this, resolved);
1736
2179
  }
2180
+ /** Internal: climb to the true root (host) command. */
2181
+ _root() {
2182
+ let node = this;
2183
+ while (node.parent) {
2184
+ node = node.parent;
2185
+ }
2186
+ return node;
2187
+ }
1737
2188
  /**
1738
2189
  * Retrieve the current invocation context (if any).
1739
2190
  */
1740
2191
  getCtx() {
1741
- return this[CTX_SYMBOL];
2192
+ let ctx = this[CTX_SYMBOL];
2193
+ if (!ctx) {
2194
+ const root = this._root();
2195
+ ctx = root[CTX_SYMBOL];
2196
+ }
2197
+ if (!ctx) {
2198
+ throw new Error('Dotenv context unavailable. Ensure resolveAndLoad() has been called or the host is wired with passOptions() before invoking commands.');
2199
+ }
2200
+ return ctx;
2201
+ }
2202
+ /**
2203
+ * Check whether a context has been resolved (non-throwing guard).
2204
+ */
2205
+ hasCtx() {
2206
+ if (this[CTX_SYMBOL] !== undefined)
2207
+ return true;
2208
+ const root = this._root();
2209
+ return root[CTX_SYMBOL] !== undefined;
1742
2210
  }
1743
2211
  /**
1744
2212
  * Retrieve the merged root CLI options bag (if set by passOptions()).
1745
2213
  * Downstream-safe: no generics required.
1746
2214
  */
1747
2215
  getOptions() {
1748
- return this[OPTS_SYMBOL];
2216
+ if (this[OPTS_SYMBOL])
2217
+ return this[OPTS_SYMBOL];
2218
+ const root = this._root();
2219
+ const bag = root[OPTS_SYMBOL];
2220
+ if (bag)
2221
+ return bag;
2222
+ return undefined;
1749
2223
  }
1750
2224
  /** Internal: set the merged root options bag for this run. */
1751
2225
  _setOptionsBag(bag) {
1752
2226
  this[OPTS_SYMBOL] = bag;
1753
2227
  }
1754
- /** Convenience helper to create a namespaced subcommand. */
2228
+ /**
2229
+ * Convenience helper to create a namespaced subcommand with argument inference.
2230
+ * This mirrors Commander generics so downstream chaining stays fully typed.
2231
+ */
1755
2232
  ns(name) {
1756
2233
  // Guard against same-level duplicate command names for clearer diagnostics.
1757
2234
  const exists = this.commands.some((c) => c.name() === name);
@@ -1765,18 +2242,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1765
2242
  * Allows downstream apps to demarcate their root-level options.
1766
2243
  */
1767
2244
  tagAppOptions(fn) {
1768
- const root = this;
1769
- const originalAddOption = root.addOption.bind(root);
1770
- root.addOption = function patchedAdd(opt) {
1771
- root.setOptionGroup(opt, 'app');
1772
- return originalAddOption(opt);
1773
- };
1774
- try {
1775
- return fn(root);
1776
- }
1777
- finally {
1778
- root.addOption = originalAddOption;
1779
- }
2245
+ return tagAppOptionsAround(this, this.setOptionGroup.bind(this), fn);
1780
2246
  }
1781
2247
  /**
1782
2248
  * Branding helper: set CLI name/description/version and optional help header.
@@ -1785,26 +2251,11 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1785
2251
  */
1786
2252
  async brand(args) {
1787
2253
  const { name, description, version, importMetaUrl, helpHeader } = args;
1788
- if (typeof name === 'string' && name.length > 0)
1789
- this.name(name);
1790
- if (typeof description === 'string')
1791
- this.description(description);
1792
- let v = version;
1793
- if (!v && importMetaUrl) {
1794
- try {
1795
- const fromUrl = fileURLToPath(importMetaUrl);
1796
- const pkgDir = await packageDirectory({ cwd: fromUrl });
1797
- if (pkgDir) {
1798
- const txt = await fs.readFile(`${pkgDir}/package.json`, 'utf-8');
1799
- const pkg = JSON.parse(txt);
1800
- if (pkg.version)
1801
- v = pkg.version;
1802
- }
1803
- }
1804
- catch {
1805
- // best-effort only
1806
- }
1807
- }
2254
+ if (typeof name === 'string' && name.length > 0)
2255
+ this.name(name);
2256
+ if (typeof description === 'string')
2257
+ this.description(description);
2258
+ const v = version ?? (await readPkgVersion(importMetaUrl));
1808
2259
  if (v)
1809
2260
  this.version(v);
1810
2261
  // Help header:
@@ -1824,34 +2275,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1824
2275
  * hybrid ordering. Applies to root and any parent command.
1825
2276
  */
1826
2277
  helpInformation() {
1827
- // Base help text first (includes beforeAll/after hooks).
1828
- const base = super.helpInformation();
1829
- const groups = renderOptionGroups(this);
1830
- const block = typeof groups === 'string' ? groups.trim() : '';
1831
- let out = base;
1832
- if (!block) {
1833
- // Ensure a trailing blank line even when no extra groups render.
1834
- if (!out.endsWith('\n\n'))
1835
- out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1836
- return out;
1837
- }
1838
- // Insert just before "Commands:" when present.
1839
- const marker = '\nCommands:';
1840
- const idx = base.indexOf(marker);
1841
- if (idx >= 0) {
1842
- const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
1843
- out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
1844
- }
1845
- else {
1846
- // Otherwise append.
1847
- const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
1848
- out = `${base}${sep}${groups}`;
1849
- }
1850
- // Ensure a trailing blank line for prompt separation.
1851
- if (!out.endsWith('\n\n')) {
1852
- out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1853
- }
1854
- return out;
2278
+ return buildHelpInformation(super.helpInformation(), this);
1855
2279
  }
1856
2280
  /**
1857
2281
  * Public: tag an Option with a display group for help (root/app/plugin:<id>).
@@ -1863,15 +2287,8 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1863
2287
  * Register a plugin for installation (parent level).
1864
2288
  * Installation occurs on first resolveAndLoad() (or explicit install()).
1865
2289
  */
1866
- use(plugin) {
1867
- this._plugins.push(plugin);
1868
- // Immediately run setup so subcommands exist before parsing.
1869
- const setupOne = (p) => {
1870
- p.setup(this);
1871
- for (const child of p.children)
1872
- setupOne(child);
1873
- };
1874
- setupOne(plugin);
2290
+ use(plugin, override) {
2291
+ this._plugins.push({ plugin, override });
1875
2292
  return this;
1876
2293
  }
1877
2294
  /**
@@ -1879,24 +2296,52 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1879
2296
  * Runs only once per CLI instance.
1880
2297
  */
1881
2298
  async install() {
1882
- // Setup is performed immediately in use(); here we only guard for afterResolve.
1883
- this._installed = true;
1884
- // Satisfy require-await without altering behavior.
1885
- await Promise.resolve();
2299
+ if (this._installed)
2300
+ return;
2301
+ if (this._installing) {
2302
+ await this._installing;
2303
+ return;
2304
+ }
2305
+ this._installing = (async () => {
2306
+ // Install parent → children with host-created mounts (async-aware).
2307
+ for (const entry of this._plugins) {
2308
+ const p = entry.plugin;
2309
+ await setupPluginTree(this, p);
2310
+ }
2311
+ this._installed = true;
2312
+ })();
2313
+ try {
2314
+ await this._installing;
2315
+ }
2316
+ finally {
2317
+ // leave _installing as resolved; subsequent calls return early via _installed
2318
+ }
1886
2319
  }
1887
2320
  /**
1888
2321
  * Run afterResolve hooks for all plugins (parent → children).
1889
2322
  */
1890
2323
  async _runAfterResolve(ctx) {
1891
- const run = async (p) => {
1892
- if (p.afterResolve)
1893
- await p.afterResolve(this, ctx);
1894
- for (const child of p.children)
1895
- await run(child);
1896
- };
1897
- for (const p of this._plugins)
1898
- await run(p);
2324
+ await runAfterResolveTree(this, this._plugins.map((e) => e.plugin), ctx);
1899
2325
  }
2326
+ }
2327
+
2328
+ /**
2329
+ * Base CLI options derived from the shared root option defaults.
2330
+ * Used for type-safe initialization of CLI options bags.
2331
+ */
2332
+ const baseGetDotenvCliOptions = baseRootOptionDefaults;
2333
+
2334
+ /**
2335
+ * Return the top-level root command for a given mount or action's thisCommand.
2336
+ *
2337
+ * @param cmd - any command (mount or thisCommand inside an action)
2338
+ * @returns the root command instance
2339
+ */
2340
+ const getRootCommand = (cmd) => {
2341
+ let node = cmd;
2342
+ while (node.parent)
2343
+ node = node.parent;
2344
+ return node;
1900
2345
  };
1901
2346
 
1902
2347
  /**
@@ -1904,199 +2349,303 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
1904
2349
  * Centralizes construction and reduces inline casts at call sites.
1905
2350
  */
1906
2351
  const toHelpConfig = (merged, plugins) => {
1907
- const bag = {
2352
+ return {
1908
2353
  ...merged,
1909
2354
  plugins: plugins ?? {},
1910
2355
  };
1911
- return bag;
1912
2356
  };
1913
2357
 
1914
- /** src/cliHost/definePlugin.ts
1915
- * Plugin contracts for the GetDotenv CLI host.
2358
+ /**
2359
+ * Compose a child-process env overlay from dotenv and the merged CLI options bag.
2360
+ * Returns a shallow object including getDotenvCliOptions when serializable.
1916
2361
  *
1917
- * This module exposes a structural public interface for the host that plugins
1918
- * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
1919
- * nominal class identity issues (private fields) in downstream consumers.
2362
+ * @param merged - Resolved CLI options bag (or a JSON-serializable subset).
2363
+ * @param dotenv - Composed dotenv variables for the current invocation.
2364
+ * @returns A string-only env overlay suitable for child process spawning.
1920
2365
  */
1921
- /* eslint-disable tsdoc/syntax */
1922
- function definePlugin(spec) {
1923
- const { children = [], ...rest } = spec;
1924
- // Default to a strict empty-object schema so “no-config” plugins fail fast
1925
- // on unknown keys and provide a concrete {} at runtime.
1926
- const effectiveSchema = spec.configSchema ?? z.object({}).strict();
1927
- // Build base plugin first, then extend with instance-bound helpers.
1928
- const base = {
1929
- ...rest,
1930
- // Always carry a schema (strict empty by default) to simplify host logic
1931
- // and improve inference/ergonomics for plugin authors.
1932
- configSchema: effectiveSchema,
1933
- children: [...children],
1934
- use(child) {
1935
- this.children.push(child);
1936
- return this;
1937
- },
1938
- };
1939
- // Attach instance-bound helpers on the returned plugin object.
1940
- const extended = base;
1941
- extended.readConfig = function (_cli) {
1942
- // Config is stored per-plugin-instance by the host (WeakMap in computeContext).
1943
- const value = _getPluginConfigForInstance(extended);
1944
- if (value === undefined) {
1945
- // Guard: host has not resolved config yet (incorrect lifecycle usage).
1946
- throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
1947
- }
1948
- return value;
1949
- };
1950
- // Plugin-bound dynamic option factory
1951
- extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
1952
- return cli.createDynamicOption(flags, (cfg) => {
1953
- // Prefer the validated slice stored per instance; fallback to help-bag
1954
- // (by-id) so top-level `-h` can render effective defaults before resolve.
1955
- const fromStore = _getPluginConfigForInstance(extended);
1956
- const id = extended.id;
1957
- let fromBag;
1958
- if (!fromStore && id) {
1959
- const maybe = cfg.plugins[id];
1960
- if (maybe && typeof maybe === 'object') {
1961
- fromBag = maybe;
1962
- }
1963
- }
1964
- // Always provide a concrete object to dynamic callbacks:
1965
- // - With a schema: computeContext stores the parsed object.
1966
- // - Without a schema: computeContext stores {}.
1967
- // - Help-time fallback: coalesce to {} when only a by-id bag exists.
1968
- const cfgVal = (fromStore ?? fromBag ?? {});
1969
- return desc(cfg, cfgVal);
1970
- }, parser, defaultValue);
1971
- };
1972
- return extended;
2366
+ function composeNestedEnv(merged, dotenv) {
2367
+ const out = {};
2368
+ for (const [k, v] of Object.entries(dotenv)) {
2369
+ if (typeof v === 'string')
2370
+ out[k] = v;
2371
+ }
2372
+ try {
2373
+ const { logger: _omit, ...bag } = merged;
2374
+ const txt = JSON.stringify(bag);
2375
+ if (typeof txt === 'string')
2376
+ out.getDotenvCliOptions = txt;
2377
+ }
2378
+ catch {
2379
+ /* best-effort only */
2380
+ }
2381
+ return out;
1973
2382
  }
1974
-
1975
2383
  /**
1976
- * GetDotenvCli with root helpers as real class methods.
1977
- * - attachRootOptions: installs legacy/base root flags on the command.
1978
- * - passOptions: merges flags (parent \< current), computes dotenv context once,
1979
- * runs validation, and persists merged options for nested flows.
2384
+ * Strip one layer of symmetric outer quotes (single or double) from a string.
2385
+ *
2386
+ * @param s - Input string.
2387
+ * @returns `s` without one symmetric outer quote pair (when present).
1980
2388
  */
1981
- class GetDotenvCli extends GetDotenvCli$1 {
1982
- /**
1983
- * Attach legacy root flags to this CLI instance. Defaults come from
1984
- * baseRootOptionDefaults when none are provided.
1985
- */
1986
- attachRootOptions(defaults) {
1987
- const d = (defaults ?? baseRootOptionDefaults);
1988
- attachRootOptions(this, d);
1989
- return this;
1990
- }
1991
- /**
1992
- * Install preSubcommand/preAction hooks that:
1993
- * - Merge options (parent round-trip + current invocation) using resolveCliOptions.
1994
- * - Persist the merged bag on the current command and on the host (for ergonomics).
1995
- * - Compute the dotenv context once via resolveAndLoad(serviceOptions).
1996
- * - Validate the composed env against discovered config (warn or --strict fail).
1997
- */
1998
- passOptions(defaults) {
1999
- const d = (defaults ?? baseRootOptionDefaults);
2000
- this.hook('preSubcommand', async (thisCommand) => {
2001
- const raw = thisCommand.opts();
2002
- const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
2003
- // Persist merged options (for nested behavior and ergonomic access).
2004
- thisCommand.getDotenvCliOptions =
2005
- merged;
2006
- this._setOptionsBag(merged);
2007
- // Build service options and compute context (always-on loader path).
2008
- const serviceOptions = getDotenvCliOptions2Options(merged);
2009
- await this.resolveAndLoad(serviceOptions);
2010
- // Refresh dynamic option descriptions using resolved config + plugin slices
2011
- try {
2012
- const ctx = this.getCtx();
2013
- const helpCfg = toHelpConfig(merged, ctx?.pluginConfigs ?? {});
2014
- this.evaluateDynamicOptions(helpCfg);
2015
- }
2016
- catch {
2017
- /* best-effort */
2018
- }
2019
- // Global validation: once after Phase C using config sources.
2020
- try {
2021
- const ctx = this.getCtx();
2022
- const dotenv = ctx?.dotenv ?? {};
2023
- const sources = await resolveGetDotenvConfigSources(import.meta.url);
2024
- const issues = validateEnvAgainstSources(dotenv, sources);
2025
- if (Array.isArray(issues) && issues.length > 0) {
2026
- const logger = (merged
2027
- .logger ?? console);
2028
- const emit = logger.error ?? logger.log;
2029
- issues.forEach((m) => {
2030
- emit(m);
2031
- });
2032
- if (merged.strict)
2033
- process.exit(1);
2034
- }
2035
- }
2036
- catch {
2037
- // Be tolerant: do not crash non-strict flows on unexpected validator failures.
2038
- }
2039
- });
2040
- // Also handle root-level flows (no subcommand) so option-aliases can run
2041
- // with the same merged options and context without duplicating logic.
2042
- this.hook('preAction', async (thisCommand) => {
2043
- const raw = thisCommand.opts();
2044
- const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
2045
- thisCommand.getDotenvCliOptions =
2046
- merged;
2047
- this._setOptionsBag(merged);
2048
- // Avoid duplicate heavy work if a context is already present.
2049
- if (!this.getCtx()) {
2050
- const serviceOptions = getDotenvCliOptions2Options(merged);
2051
- await this.resolveAndLoad(serviceOptions);
2052
- try {
2053
- const ctx = this.getCtx();
2054
- const helpCfg = toHelpConfig(merged, ctx?.pluginConfigs ?? {});
2055
- this.evaluateDynamicOptions(helpCfg);
2056
- }
2057
- catch {
2058
- /* tolerate */
2059
- }
2060
- try {
2061
- const ctx = this.getCtx();
2062
- const dotenv = (ctx?.dotenv ?? {});
2063
- const sources = await resolveGetDotenvConfigSources(import.meta.url);
2064
- const issues = validateEnvAgainstSources(dotenv, sources);
2065
- if (Array.isArray(issues) && issues.length > 0) {
2066
- const logger = (merged
2067
- .logger ?? console);
2068
- const emit = logger.error ?? logger.log;
2069
- issues.forEach((m) => {
2070
- emit(m);
2071
- });
2072
- if (merged.strict) {
2073
- process.exit(1);
2074
- }
2075
- }
2076
- }
2077
- catch {
2078
- // Tolerate validation side-effects in non-strict mode.
2079
- }
2080
- }
2081
- });
2082
- return this;
2389
+ const stripOne = (s) => {
2390
+ if (s.length < 2)
2391
+ return s;
2392
+ const a = s.charAt(0);
2393
+ const b = s.charAt(s.length - 1);
2394
+ const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
2395
+ return symmetric ? s.slice(1, -1) : s;
2396
+ };
2397
+ /**
2398
+ * Preserve argv array for Node -e/--eval payloads under shell-off and
2399
+ * peel one symmetric outer quote layer from the code argument.
2400
+ *
2401
+ * @param args - Argument vector intended for direct execution (shell-off).
2402
+ * @returns Either the original `args` or a modified copy with a normalized eval payload.
2403
+ */
2404
+ function maybePreserveNodeEvalArgv(args) {
2405
+ if (args.length >= 3) {
2406
+ const first = (args[0] ?? '').toLowerCase();
2407
+ const hasEval = args[1] === '-e' || args[1] === '--eval';
2408
+ if (first === 'node' && hasEval) {
2409
+ const copy = args.slice();
2410
+ copy[2] = stripOne(copy[2] ?? '');
2411
+ return copy;
2412
+ }
2083
2413
  }
2414
+ return args;
2084
2415
  }
2416
+
2085
2417
  /**
2086
- * Helper to retrieve the merged root options bag from any action handler
2087
- * that only has access to thisCommand. Avoids structural casts.
2418
+ * Retrieve the merged root options bag from the current command context.
2419
+ * Climbs to the root `GetDotenvCli` instance to access the persisted options.
2420
+ *
2421
+ * @param cmd - The current command instance (thisCommand).
2422
+ * @throws Error if the root is not a GetDotenvCli or options are missing.
2088
2423
  */
2089
2424
  const readMergedOptions = (cmd) => {
2090
- // Ascend to the root command
2425
+ // Climb to the true root
2091
2426
  let root = cmd;
2092
- while (root.parent) {
2427
+ while (root.parent)
2093
2428
  root = root.parent;
2429
+ // Assert we ended at our host
2430
+ if (!(root instanceof GetDotenvCli)) {
2431
+ throw new Error('readMergedOptions: root command is not a GetDotenvCli.' +
2432
+ 'Ensure your CLI is constructed with GetDotenvCli.');
2433
+ }
2434
+ // Require passOptions() to have persisted the bag
2435
+ const bag = root.getOptions();
2436
+ if (!bag || typeof bag !== 'object') {
2437
+ throw new Error('readMergedOptions: merged options are unavailable. ' +
2438
+ 'Call .passOptions() on the host before parsing.');
2439
+ }
2440
+ return bag;
2441
+ };
2442
+
2443
+ /**
2444
+ * Batch services (neutral): resolve command and shell settings.
2445
+ * Shared by the generator path and the batch plugin to avoid circular deps.
2446
+ */
2447
+ /**
2448
+ * Resolve a command string from the {@link ScriptsTable} table.
2449
+ * A script may be expressed as a string or an object with a `cmd` property.
2450
+ *
2451
+ * @param scripts - Optional scripts table.
2452
+ * @param command - User-provided command name or string.
2453
+ * @returns Resolved command string (falls back to the provided command).
2454
+ */
2455
+ const resolveCommand = (scripts, command) => scripts && typeof scripts[command] === 'object'
2456
+ ? scripts[command].cmd
2457
+ : (scripts?.[command] ?? command);
2458
+ /**
2459
+ * Resolve the shell setting for a given command:
2460
+ * - If the script entry is an object, prefer its `shell` override.
2461
+ * - Otherwise use the provided `shell` (string | boolean).
2462
+ *
2463
+ * @param scripts - Optional scripts table.
2464
+ * @param command - User-provided command name or string.
2465
+ * @param shell - Global shell preference (string | boolean).
2466
+ */
2467
+ const resolveShell = (scripts, command, shell) => scripts && typeof scripts[command] === 'object'
2468
+ ? (scripts[command].shell ?? false)
2469
+ : (shell ?? false);
2470
+
2471
+ /**
2472
+ * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
2473
+ * - If the user explicitly enabled the flag, return true.
2474
+ * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
2475
+ * - Otherwise, adopt the default (true → set; false/undefined → unset).
2476
+ *
2477
+ * @param exclude - The "on" flag value as parsed by Commander.
2478
+ * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
2479
+ * @param defaultValue - The generator default to adopt when no explicit toggle is present.
2480
+ * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
2481
+ *
2482
+ * @example
2483
+ * ```ts
2484
+ * resolveExclusion(undefined, undefined, true); // => true
2485
+ * ```
2486
+ */
2487
+ const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
2488
+ /**
2489
+ * Resolve an optional flag with "--exclude-all" overrides.
2490
+ * If excludeAll is set and the individual "...-off" is not, force true.
2491
+ * If excludeAllOff is set and the individual flag is not explicitly set, unset.
2492
+ * Otherwise, adopt the default (true → set; false/undefined → unset).
2493
+ *
2494
+ * @param exclude - Individual include/exclude flag.
2495
+ * @param excludeOff - Individual "...-off" flag.
2496
+ * @param defaultValue - Default for the individual flag.
2497
+ * @param excludeAll - Global "exclude-all" flag.
2498
+ * @param excludeAllOff - Global "exclude-all-off" flag.
2499
+ *
2500
+ * @example
2501
+ * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
2502
+ */
2503
+ const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
2504
+ // Order of precedence:
2505
+ // 1) Individual explicit "on" wins outright.
2506
+ // 2) Individual explicit "off" wins over any global.
2507
+ // 3) Global exclude-all forces true when not explicitly turned off.
2508
+ // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
2509
+ // 5) Fall back to the default (true => set; false/undefined => unset).
2510
+ (() => {
2511
+ // Individual "on"
2512
+ if (exclude === true)
2513
+ return true;
2514
+ // Individual "off"
2515
+ if (excludeOff === true)
2516
+ return undefined;
2517
+ // Global "exclude-all" ON (unless explicitly turned off)
2518
+ if (excludeAll === true)
2519
+ return true;
2520
+ // Global "exclude-all-off" (unless explicitly enabled)
2521
+ if (excludeAllOff === true)
2522
+ return undefined;
2523
+ // Default
2524
+ return defaultValue ? true : undefined;
2525
+ })();
2526
+ /**
2527
+ * exactOptionalPropertyTypes-safe setter for optional boolean flags:
2528
+ * delete when undefined; assign when defined — without requiring an index signature on T.
2529
+ *
2530
+ * @typeParam T - Target object type.
2531
+ * @param obj - The object to write to.
2532
+ * @param key - The optional boolean property key of {@link T}.
2533
+ * @param value - The value to set or `undefined` to unset.
2534
+ *
2535
+ * @remarks
2536
+ * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
2537
+ */
2538
+ const setOptionalFlag = (obj, key, value) => {
2539
+ const target = obj;
2540
+ const k = key;
2541
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
2542
+ if (value === undefined)
2543
+ delete target[k];
2544
+ else
2545
+ target[k] = value;
2546
+ };
2547
+
2548
+ /**
2549
+ * Merge and normalize raw Commander options (current + parent + defaults)
2550
+ * into a GetDotenvCliOptions-like object. Types are intentionally wide to
2551
+ * avoid cross-layer coupling; callers may cast as needed.
2552
+ */
2553
+ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
2554
+ const parent = typeof parentJson === 'string' && parentJson.length > 0
2555
+ ? JSON.parse(parentJson)
2556
+ : undefined;
2557
+ const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
2558
+ const current = { ...rest };
2559
+ if (typeof scripts === 'string') {
2560
+ try {
2561
+ current.scripts = JSON.parse(scripts);
2562
+ }
2563
+ catch {
2564
+ // ignore parse errors; leave scripts undefined
2565
+ }
2566
+ }
2567
+ const merged = defaultsDeep({}, defaults, parent ?? {}, current);
2568
+ const d = defaults;
2569
+ setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
2570
+ setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
2571
+ setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
2572
+ setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
2573
+ setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
2574
+ setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
2575
+ setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
2576
+ setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
2577
+ // warnEntropy (tri-state)
2578
+ setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
2579
+ // Normalize shell for predictability: explicit default shell per OS.
2580
+ const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
2581
+ let resolvedShell = merged.shell;
2582
+ if (shellOff)
2583
+ resolvedShell = false;
2584
+ else if (resolvedShell === true || resolvedShell === undefined) {
2585
+ resolvedShell = defaultShell;
2586
+ }
2587
+ else if (typeof resolvedShell !== 'string' &&
2588
+ typeof defaults.shell === 'string') {
2589
+ resolvedShell = defaults.shell;
2590
+ }
2591
+ merged.shell = resolvedShell;
2592
+ const cmd = typeof command === 'string' ? command : undefined;
2593
+ return cmd !== undefined ? { merged, command: cmd } : { merged };
2594
+ };
2595
+
2596
+ const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
2597
+ /**
2598
+ * Build a sanitized environment object for spawning child processes.
2599
+ * Merges `base` and `overlay`, drops undefined values, and handles platform-specific
2600
+ * normalization (e.g. case-insensitivity on Windows).
2601
+ *
2602
+ * @param base - Base environment (usually `process.env`).
2603
+ * @param overlay - Environment variables to overlay.
2604
+ */
2605
+ const buildSpawnEnv = (base, overlay) => {
2606
+ const raw = {
2607
+ ...(base ?? {}),
2608
+ ...(overlay ?? {}),
2609
+ };
2610
+ // Drop undefined first
2611
+ const entries = Object.entries(dropUndefined(raw));
2612
+ if (process.platform === 'win32') {
2613
+ // Windows: keys are case-insensitive; collapse duplicates
2614
+ const byLower = new Map();
2615
+ for (const [k, v] of entries) {
2616
+ byLower.set(k.toLowerCase(), [k, v]); // last wins; preserve latest casing
2617
+ }
2618
+ const out = {};
2619
+ for (const [, [k, v]] of byLower)
2620
+ out[k] = v;
2621
+ // HOME fallback from USERPROFILE (common expectation)
2622
+ if (!Object.prototype.hasOwnProperty.call(out, 'HOME')) {
2623
+ const up = out['USERPROFILE'];
2624
+ if (typeof up === 'string' && up.length > 0)
2625
+ out['HOME'] = up;
2626
+ }
2627
+ // Normalize TMP/TEMP coherence (pick any present; reflect to both)
2628
+ const tmp = out['TMP'] ?? out['TEMP'];
2629
+ if (typeof tmp === 'string' && tmp.length > 0) {
2630
+ out['TMP'] = tmp;
2631
+ out['TEMP'] = tmp;
2632
+ }
2633
+ return out;
2634
+ }
2635
+ // POSIX: keep keys as-is
2636
+ const out = Object.fromEntries(entries);
2637
+ // Ensure TMPDIR exists when any temp key is present (best-effort)
2638
+ const tmpdir = out['TMPDIR'] ?? out['TMP'] ?? out['TEMP'];
2639
+ if (typeof tmpdir === 'string' && tmpdir.length > 0) {
2640
+ out['TMPDIR'] = tmpdir;
2094
2641
  }
2095
- const hostAny = root;
2096
- return typeof hostAny.getOptions === 'function'
2097
- ? hostAny.getOptions()
2098
- : root
2099
- .getDotenvCliOptions;
2642
+ return out;
2100
2643
  };
2101
2644
 
2102
- export { GetDotenvCli, definePlugin, readMergedOptions };
2645
+ /**
2646
+ * Identity helper to define a scripts table while preserving a concrete TShell
2647
+ * type parameter in downstream inference.
2648
+ */
2649
+ const defineScripts = () => (t) => t;
2650
+
2651
+ export { GetDotenvCli, baseGetDotenvCliOptions, buildSpawnEnv, composeNestedEnv, definePlugin, defineScripts, getRootCommand, maybePreserveNodeEvalArgv, readMergedOptions, resolveCliOptions, resolveCommand, resolveShell, runCommand, runCommandResult, shouldCapture, stripOne, toHelpConfig };