@karmaniverous/get-dotenv 6.0.0-1 → 6.1.0

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