@karmaniverous/get-dotenv 6.0.0-1 → 6.0.0

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