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