@karmaniverous/get-dotenv 6.0.0-0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +86 -334
  2. package/dist/cli.d.ts +569 -0
  3. package/dist/cli.mjs +18788 -0
  4. package/dist/cliHost.d.ts +548 -253
  5. package/dist/cliHost.mjs +1990 -1458
  6. package/dist/config.d.ts +192 -14
  7. package/dist/config.mjs +256 -81
  8. package/dist/env-overlay.d.ts +226 -18
  9. package/dist/env-overlay.mjs +181 -22
  10. package/dist/getdotenv.cli.mjs +18166 -3437
  11. package/dist/index.d.ts +729 -136
  12. package/dist/index.mjs +18207 -3457
  13. package/dist/plugins-aws.d.ts +289 -104
  14. package/dist/plugins-aws.mjs +2462 -350
  15. package/dist/plugins-batch.d.ts +355 -105
  16. package/dist/plugins-batch.mjs +2595 -420
  17. package/dist/plugins-cmd.d.ts +287 -118
  18. package/dist/plugins-cmd.mjs +2661 -839
  19. package/dist/plugins-init.d.ts +272 -100
  20. package/dist/plugins-init.mjs +2152 -37
  21. package/dist/plugins.d.ts +323 -140
  22. package/dist/plugins.mjs +18006 -2025
  23. package/dist/templates/cli/index.ts +26 -0
  24. package/dist/templates/cli/plugins/hello.ts +43 -0
  25. package/dist/templates/config/js/getdotenv.config.js +20 -0
  26. package/dist/templates/config/json/local/getdotenv.config.local.json +7 -0
  27. package/dist/templates/config/json/public/getdotenv.config.json +9 -0
  28. package/dist/templates/config/public/getdotenv.config.json +8 -0
  29. package/dist/templates/config/ts/getdotenv.config.ts +28 -0
  30. package/dist/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
  31. package/dist/templates/config/yaml/public/getdotenv.config.yaml +7 -0
  32. package/dist/templates/getdotenv.config.js +20 -0
  33. package/dist/templates/getdotenv.config.json +9 -0
  34. package/dist/templates/getdotenv.config.local.json +7 -0
  35. package/dist/templates/getdotenv.config.local.yaml +7 -0
  36. package/dist/templates/getdotenv.config.ts +28 -0
  37. package/dist/templates/getdotenv.config.yaml +7 -0
  38. package/dist/templates/hello.ts +43 -0
  39. package/dist/templates/index.ts +26 -0
  40. package/dist/templates/js/getdotenv.config.js +20 -0
  41. package/dist/templates/json/local/getdotenv.config.local.json +7 -0
  42. package/dist/templates/json/public/getdotenv.config.json +9 -0
  43. package/dist/templates/local/getdotenv.config.local.json +7 -0
  44. package/dist/templates/local/getdotenv.config.local.yaml +7 -0
  45. package/dist/templates/plugins/hello.ts +43 -0
  46. package/dist/templates/public/getdotenv.config.json +9 -0
  47. package/dist/templates/public/getdotenv.config.yaml +7 -0
  48. package/dist/templates/ts/getdotenv.config.ts +28 -0
  49. package/dist/templates/yaml/local/getdotenv.config.local.yaml +7 -0
  50. package/dist/templates/yaml/public/getdotenv.config.yaml +7 -0
  51. package/getdotenv.config.json +1 -19
  52. package/package.json +52 -89
  53. package/templates/cli/index.ts +26 -0
  54. package/templates/cli/plugins/hello.ts +43 -0
  55. package/templates/config/js/getdotenv.config.js +9 -4
  56. package/templates/config/json/public/getdotenv.config.json +0 -3
  57. package/templates/config/public/getdotenv.config.json +0 -5
  58. package/templates/config/ts/getdotenv.config.ts +17 -5
  59. package/templates/config/yaml/public/getdotenv.config.yaml +0 -3
  60. package/dist/cliHost.cjs +0 -2078
  61. package/dist/cliHost.d.cts +0 -451
  62. package/dist/cliHost.d.mts +0 -451
  63. package/dist/config.cjs +0 -252
  64. package/dist/config.d.cts +0 -55
  65. package/dist/config.d.mts +0 -55
  66. package/dist/env-overlay.cjs +0 -163
  67. package/dist/env-overlay.d.cts +0 -50
  68. package/dist/env-overlay.d.mts +0 -50
  69. package/dist/index.cjs +0 -4077
  70. package/dist/index.d.cts +0 -318
  71. package/dist/index.d.mts +0 -318
  72. package/dist/plugins-aws.cjs +0 -666
  73. package/dist/plugins-aws.d.cts +0 -158
  74. package/dist/plugins-aws.d.mts +0 -158
  75. package/dist/plugins-batch.cjs +0 -658
  76. package/dist/plugins-batch.d.cts +0 -181
  77. package/dist/plugins-batch.d.mts +0 -181
  78. package/dist/plugins-cmd.cjs +0 -1112
  79. package/dist/plugins-cmd.d.cts +0 -178
  80. package/dist/plugins-cmd.d.mts +0 -178
  81. package/dist/plugins-demo.cjs +0 -352
  82. package/dist/plugins-demo.d.cts +0 -158
  83. package/dist/plugins-demo.d.mts +0 -158
  84. package/dist/plugins-demo.d.ts +0 -158
  85. package/dist/plugins-demo.mjs +0 -350
  86. package/dist/plugins-init.cjs +0 -289
  87. package/dist/plugins-init.d.cts +0 -162
  88. package/dist/plugins-init.d.mts +0 -162
  89. package/dist/plugins.cjs +0 -2327
  90. package/dist/plugins.d.cts +0 -211
  91. package/dist/plugins.d.mts +0 -211
  92. package/templates/cli/ts/index.ts +0 -9
  93. package/templates/cli/ts/plugins/hello.ts +0 -17
package/dist/cliHost.cjs DELETED
@@ -1,2078 +0,0 @@
1
- 'use strict';
2
-
3
- var fs = require('fs-extra');
4
- var packageDirectory = require('package-directory');
5
- var path = require('path');
6
- var url = require('url');
7
- var YAML = require('yaml');
8
- var zod = require('zod');
9
- var commander = require('commander');
10
- var nanoid = require('nanoid');
11
- var dotenv = require('dotenv');
12
- var crypto = require('crypto');
13
-
14
- var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
15
- // Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
16
- const baseRootOptionDefaults = {
17
- dotenvToken: '.env',
18
- loadProcess: true,
19
- logger: console,
20
- // Diagnostics defaults
21
- warnEntropy: true,
22
- entropyThreshold: 3.8,
23
- entropyMinLength: 16,
24
- entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
25
- paths: './',
26
- pathsDelimiter: ' ',
27
- privateToken: 'local',
28
- scripts: {
29
- 'git-status': {
30
- cmd: 'git branch --show-current && git status -s -u',
31
- shell: true,
32
- },
33
- },
34
- shell: true,
35
- vars: '',
36
- varsAssignor: '=',
37
- varsDelimiter: ' ',
38
- // tri-state flags default to unset unless explicitly provided
39
- // (debug/log/exclude* resolved via flag utils)
40
- };
41
-
42
- /** @internal */
43
- const isPlainObject$1 = (value) => value !== null &&
44
- typeof value === 'object' &&
45
- Object.getPrototypeOf(value) === Object.prototype;
46
- const mergeInto = (target, source) => {
47
- for (const [key, sVal] of Object.entries(source)) {
48
- if (sVal === undefined)
49
- continue; // do not overwrite with undefined
50
- const tVal = target[key];
51
- if (isPlainObject$1(tVal) && isPlainObject$1(sVal)) {
52
- target[key] = mergeInto({ ...tVal }, sVal);
53
- }
54
- else if (isPlainObject$1(sVal)) {
55
- target[key] = mergeInto({}, sVal);
56
- }
57
- else {
58
- target[key] = sVal;
59
- }
60
- }
61
- return target;
62
- };
63
- /**
64
- * Perform a deep defaults-style merge across plain objects. *
65
- * - Only merges plain objects (prototype === Object.prototype).
66
- * - Arrays and non-objects are replaced, not merged.
67
- * - `undefined` values are ignored and do not overwrite prior values.
68
- *
69
- * @typeParam T - The resulting shape after merging all layers.
70
- * @param layers - Zero or more partial layers in ascending precedence order.
71
- * @returns The merged object typed as {@link T}.
72
- *
73
- * @example
74
- * defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
75
- * =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
76
- */
77
- const defaultsDeep = (...layers) => {
78
- const result = layers
79
- .filter(Boolean)
80
- .reduce((acc, layer) => mergeInto(acc, layer), {});
81
- return result;
82
- };
83
-
84
- /**
85
- * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
86
- * - If the user explicitly enabled the flag, return true.
87
- * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
88
- * - Otherwise, adopt the default (true → set; false/undefined → unset).
89
- *
90
- * @param exclude - The "on" flag value as parsed by Commander.
91
- * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
92
- * @param defaultValue - The generator default to adopt when no explicit toggle is present.
93
- * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
94
- *
95
- * @example
96
- * ```ts
97
- * resolveExclusion(undefined, undefined, true); // => true
98
- * ```
99
- */
100
- const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
101
- /**
102
- * Resolve an optional flag with "--exclude-all" overrides.
103
- * If excludeAll is set and the individual "...-off" is not, force true.
104
- * If excludeAllOff is set and the individual flag is not explicitly set, unset.
105
- * Otherwise, adopt the default (true → set; false/undefined → unset).
106
- *
107
- * @param exclude - Individual include/exclude flag.
108
- * @param excludeOff - Individual "...-off" flag.
109
- * @param defaultValue - Default for the individual flag.
110
- * @param excludeAll - Global "exclude-all" flag.
111
- * @param excludeAllOff - Global "exclude-all-off" flag.
112
- *
113
- * @example
114
- * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
115
- */
116
- const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
117
- // Order of precedence:
118
- // 1) Individual explicit "on" wins outright.
119
- // 2) Individual explicit "off" wins over any global.
120
- // 3) Global exclude-all forces true when not explicitly turned off.
121
- // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
122
- // 5) Fall back to the default (true => set; false/undefined => unset).
123
- (() => {
124
- // Individual "on"
125
- if (exclude === true)
126
- return true;
127
- // Individual "off"
128
- if (excludeOff === true)
129
- return undefined;
130
- // Global "exclude-all" ON (unless explicitly turned off)
131
- if (excludeAll === true)
132
- return true;
133
- // Global "exclude-all-off" (unless explicitly enabled)
134
- if (excludeAllOff === true)
135
- return undefined;
136
- // Default
137
- return defaultValue ? true : undefined;
138
- })();
139
- /**
140
- * exactOptionalPropertyTypes-safe setter for optional boolean flags:
141
- * delete when undefined; assign when defined — without requiring an index signature on T.
142
- *
143
- * @typeParam T - Target object type.
144
- * @param obj - The object to write to.
145
- * @param key - The optional boolean property key of {@link T}.
146
- * @param value - The value to set or `undefined` to unset.
147
- *
148
- * @remarks
149
- * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
150
- */
151
- const setOptionalFlag = (obj, key, value) => {
152
- const target = obj;
153
- const k = key;
154
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
155
- if (value === undefined)
156
- delete target[k];
157
- else
158
- target[k] = value;
159
- };
160
-
161
- /**
162
- * Merge and normalize raw Commander options (current + parent + defaults)
163
- * into a GetDotenvCliOptions-like object. Types are intentionally wide to
164
- * avoid cross-layer coupling; callers may cast as needed.
165
- */
166
- const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
167
- const parent = typeof parentJson === 'string' && parentJson.length > 0
168
- ? JSON.parse(parentJson)
169
- : undefined;
170
- const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
171
- const current = { ...rest };
172
- if (typeof scripts === 'string') {
173
- try {
174
- current.scripts = JSON.parse(scripts);
175
- }
176
- catch {
177
- // ignore parse errors; leave scripts undefined
178
- }
179
- }
180
- const merged = defaultsDeep({}, defaults, parent ?? {}, current);
181
- const d = defaults;
182
- setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
183
- setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
184
- setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
185
- setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
186
- setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
187
- setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
188
- setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
189
- setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
190
- // warnEntropy (tri-state)
191
- setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
192
- // Normalize shell for predictability: explicit default shell per OS.
193
- const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
194
- let resolvedShell = merged.shell;
195
- if (shellOff)
196
- resolvedShell = false;
197
- else if (resolvedShell === true || resolvedShell === undefined) {
198
- resolvedShell = defaultShell;
199
- }
200
- else if (typeof resolvedShell !== 'string' &&
201
- typeof defaults.shell === 'string') {
202
- resolvedShell = defaults.shell;
203
- }
204
- merged.shell = resolvedShell;
205
- const cmd = typeof command === 'string' ? command : undefined;
206
- return cmd !== undefined ? { merged, command: cmd } : { merged };
207
- };
208
-
209
- /**
210
- * Zod schemas for configuration files discovered by the new loader.
211
- *
212
- * Notes:
213
- * - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
214
- * - RESOLVED: normalized shapes (paths always string[]).
215
- * - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
216
- */
217
- // String-only env value map
218
- const stringMap = zod.z.record(zod.z.string(), zod.z.string());
219
- const envStringMap = zod.z.record(zod.z.string(), stringMap);
220
- // Allow string[] or single string for "paths" in RAW; normalize later.
221
- const rawPathsSchema = zod.z.union([zod.z.array(zod.z.string()), zod.z.string()]).optional();
222
- const getDotenvConfigSchemaRaw = zod.z.object({
223
- dotenvToken: zod.z.string().optional(),
224
- privateToken: zod.z.string().optional(),
225
- paths: rawPathsSchema,
226
- loadProcess: zod.z.boolean().optional(),
227
- log: zod.z.boolean().optional(),
228
- shell: zod.z.union([zod.z.string(), zod.z.boolean()]).optional(),
229
- scripts: zod.z.record(zod.z.string(), zod.z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
230
- requiredKeys: zod.z.array(zod.z.string()).optional(),
231
- schema: zod.z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
232
- vars: stringMap.optional(), // public, global
233
- envVars: envStringMap.optional(), // public, per-env
234
- // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
235
- dynamic: zod.z.unknown().optional(),
236
- // Per-plugin config bag; validated by plugins/host when used.
237
- plugins: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
238
- });
239
- // Normalize paths to string[]
240
- const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
241
- const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
242
- ...raw,
243
- paths: normalizePaths(raw.paths),
244
- }));
245
-
246
- // Discovery candidates (first match wins per scope/privacy).
247
- // Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
248
- const PUBLIC_FILENAMES = [
249
- 'getdotenv.config.json',
250
- 'getdotenv.config.yaml',
251
- 'getdotenv.config.yml',
252
- 'getdotenv.config.js',
253
- 'getdotenv.config.mjs',
254
- 'getdotenv.config.cjs',
255
- 'getdotenv.config.ts',
256
- 'getdotenv.config.mts',
257
- 'getdotenv.config.cts',
258
- ];
259
- const LOCAL_FILENAMES = [
260
- 'getdotenv.config.local.json',
261
- 'getdotenv.config.local.yaml',
262
- 'getdotenv.config.local.yml',
263
- 'getdotenv.config.local.js',
264
- 'getdotenv.config.local.mjs',
265
- 'getdotenv.config.local.cjs',
266
- 'getdotenv.config.local.ts',
267
- 'getdotenv.config.local.mts',
268
- 'getdotenv.config.local.cts',
269
- ];
270
- const isYaml = (p) => ['.yaml', '.yml'].includes(path.extname(p).toLowerCase());
271
- const isJson = (p) => path.extname(p).toLowerCase() === '.json';
272
- const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(path.extname(p).toLowerCase());
273
- // --- Internal JS/TS module loader helpers (default export) ---
274
- const importDefault$1 = async (fileUrl) => {
275
- const mod = (await import(fileUrl));
276
- return mod.default;
277
- };
278
- const cacheName = (absPath, suffix) => {
279
- // sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
280
- const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
281
- return `${base}.${suffix}.mjs`;
282
- };
283
- const ensureDir = async (dir) => {
284
- await fs.ensureDir(dir);
285
- return dir;
286
- };
287
- const loadJsTsDefault = async (absPath) => {
288
- const fileUrl = url.pathToFileURL(absPath).toString();
289
- const ext = path.extname(absPath).toLowerCase();
290
- if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
291
- return importDefault$1(fileUrl);
292
- }
293
- // Try direct import first in case a TS loader is active.
294
- try {
295
- const val = await importDefault$1(fileUrl);
296
- if (val)
297
- return val;
298
- }
299
- catch {
300
- /* fallthrough */
301
- }
302
- // esbuild bundle to a temp ESM file
303
- try {
304
- const esbuild = (await import('esbuild'));
305
- const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
306
- const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
307
- await esbuild.build({
308
- entryPoints: [absPath],
309
- bundle: true,
310
- platform: 'node',
311
- format: 'esm',
312
- target: 'node20',
313
- outfile,
314
- sourcemap: false,
315
- logLevel: 'silent',
316
- });
317
- return await importDefault$1(url.pathToFileURL(outfile).toString());
318
- }
319
- catch {
320
- /* fallthrough to TS transpile */
321
- }
322
- // typescript.transpileModule simple transpile (single-file)
323
- try {
324
- const ts = (await import('typescript'));
325
- const src = await fs.readFile(absPath, 'utf-8');
326
- const out = ts.transpileModule(src, {
327
- compilerOptions: {
328
- module: 'ESNext',
329
- target: 'ES2022',
330
- moduleResolution: 'NodeNext',
331
- },
332
- }).outputText;
333
- const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
334
- const outfile = path.join(outDir, cacheName(absPath, 'ts'));
335
- await fs.writeFile(outfile, out, 'utf-8');
336
- return await importDefault$1(url.pathToFileURL(outfile).toString());
337
- }
338
- catch {
339
- throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
340
- }
341
- };
342
- /**
343
- * Discover JSON/YAML config files in the packaged root and project root.
344
- * Order: packaged public → project public → project local. */
345
- const discoverConfigFiles = async (importMetaUrl) => {
346
- const files = [];
347
- // Packaged root via importMetaUrl (optional)
348
- if (importMetaUrl) {
349
- const fromUrl = url.fileURLToPath(importMetaUrl);
350
- const packagedRoot = await packageDirectory.packageDirectory({ cwd: fromUrl });
351
- if (packagedRoot) {
352
- for (const name of PUBLIC_FILENAMES) {
353
- const p = path.join(packagedRoot, name);
354
- if (await fs.pathExists(p)) {
355
- files.push({ path: p, privacy: 'public', scope: 'packaged' });
356
- break; // only one public file expected per scope
357
- }
358
- }
359
- // By policy, packaged .local is not expected; skip even if present.
360
- }
361
- }
362
- // Project root (from current working directory)
363
- const projectRoot = await packageDirectory.packageDirectory();
364
- if (projectRoot) {
365
- for (const name of PUBLIC_FILENAMES) {
366
- const p = path.join(projectRoot, name);
367
- if (await fs.pathExists(p)) {
368
- files.push({ path: p, privacy: 'public', scope: 'project' });
369
- break;
370
- }
371
- }
372
- for (const name of LOCAL_FILENAMES) {
373
- const p = path.join(projectRoot, name);
374
- if (await fs.pathExists(p)) {
375
- files.push({ path: p, privacy: 'local', scope: 'project' });
376
- break;
377
- }
378
- }
379
- }
380
- return files;
381
- };
382
- /**
383
- * Load a single config file (JSON/YAML). JS/TS is not supported in this step.
384
- * Validates with Zod RAW schema, then normalizes to RESOLVED.
385
- *
386
- * For JSON/YAML: if a "dynamic" property is present, throws with guidance.
387
- * For JS/TS: default export is loaded; "dynamic" is allowed.
388
- */
389
- const loadConfigFile = async (filePath) => {
390
- let raw = {};
391
- try {
392
- const abs = path.resolve(filePath);
393
- if (isJsOrTs(abs)) {
394
- // JS/TS support: load default export via robust pipeline.
395
- const mod = await loadJsTsDefault(abs);
396
- raw = mod ?? {};
397
- }
398
- else {
399
- const txt = await fs.readFile(abs, 'utf-8');
400
- raw = isJson(abs) ? JSON.parse(txt) : isYaml(abs) ? YAML.parse(txt) : {};
401
- }
402
- }
403
- catch (err) {
404
- throw new Error(`Failed to read/parse config: ${filePath}. ${String(err)}`);
405
- }
406
- // Validate RAW
407
- const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
408
- if (!parsed.success) {
409
- const msgs = parsed.error.issues
410
- .map((i) => `${i.path.join('.')}: ${i.message}`)
411
- .join('\n');
412
- throw new Error(`Invalid config ${filePath}:\n${msgs}`);
413
- }
414
- // Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
415
- if (!isJsOrTs(filePath) &&
416
- (parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
417
- throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
418
- `Use JS/TS config for "dynamic" or "schema".`);
419
- }
420
- return getDotenvConfigSchemaResolved.parse(parsed.data);
421
- };
422
- /**
423
- * Discover and load configs into resolved shapes, ordered by scope/privacy.
424
- * JSON/YAML/JS/TS supported; first match per scope/privacy applies.
425
- */
426
- const resolveGetDotenvConfigSources = async (importMetaUrl) => {
427
- const discovered = await discoverConfigFiles(importMetaUrl);
428
- const result = {};
429
- for (const f of discovered) {
430
- const cfg = await loadConfigFile(f.path);
431
- if (f.scope === 'packaged') {
432
- // packaged public only
433
- result.packaged = cfg;
434
- }
435
- else {
436
- result.project ??= {};
437
- if (f.privacy === 'public')
438
- result.project.public = cfg;
439
- else
440
- result.project.local = cfg;
441
- }
442
- }
443
- return result;
444
- };
445
-
446
- /**
447
- * Validate a composed env against config-provided validation surfaces.
448
- * Precedence for validation definitions:
449
- * project.local -\> project.public -\> packaged
450
- *
451
- * Behavior:
452
- * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
453
- * - Else if `requiredKeys` is present, check presence (value !== undefined).
454
- * - Returns a flat list of issue strings; caller decides warn vs fail.
455
- */
456
- const validateEnvAgainstSources = (finalEnv, sources) => {
457
- const pick = (getter) => {
458
- const pl = sources.project?.local;
459
- const pp = sources.project?.public;
460
- const pk = sources.packaged;
461
- return ((pl && getter(pl)) ||
462
- (pp && getter(pp)) ||
463
- (pk && getter(pk)) ||
464
- undefined);
465
- };
466
- const schema = pick((cfg) => cfg['schema']);
467
- if (schema &&
468
- typeof schema.safeParse === 'function') {
469
- try {
470
- const parsed = schema.safeParse(finalEnv);
471
- if (!parsed.success) {
472
- // Try to render zod-style issues when available.
473
- const err = parsed.error;
474
- const issues = Array.isArray(err.issues) && err.issues.length > 0
475
- ? err.issues.map((i) => {
476
- const path = Array.isArray(i.path) ? i.path.join('.') : '';
477
- const msg = i.message ?? 'Invalid value';
478
- return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
479
- })
480
- : ['[schema] validation failed'];
481
- return issues;
482
- }
483
- return [];
484
- }
485
- catch {
486
- // If schema invocation fails, surface a single diagnostic.
487
- return [
488
- '[schema] validation failed (unable to execute schema.safeParse)',
489
- ];
490
- }
491
- }
492
- const requiredKeys = pick((cfg) => cfg['requiredKeys']);
493
- if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
494
- const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
495
- if (missing.length > 0) {
496
- return missing.map((k) => `[requiredKeys] missing: ${k}`);
497
- }
498
- }
499
- return [];
500
- };
501
-
502
- const baseGetDotenvCliOptions = baseRootOptionDefaults;
503
-
504
- // src/GetDotenvOptions.ts
505
- const getDotenvOptionsFilename = 'getdotenv.config.json';
506
- /**
507
- * Converts programmatic CLI options to `getDotenv` options. *
508
- * @param cliOptions - CLI options. Defaults to `{}`.
509
- *
510
- * @returns `getDotenv` options.
511
- */
512
- const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
513
- /**
514
- * Convert CLI-facing string options into {@link GetDotenvOptions}.
515
- *
516
- * - Splits {@link GetDotenvCliOptions.paths} using either a delimiter * or a regular expression pattern into a string array. * - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
517
- * pairs (configurable delimiters) into a {@link ProcessEnv}.
518
- * - Drops CLI-only keys that have no programmatic equivalent.
519
- *
520
- * @remarks
521
- * Follows exact-optional semantics by not emitting undefined-valued entries.
522
- */
523
- // Drop CLI-only keys (debug/scripts) without relying on Record casts.
524
- // Create a shallow copy then delete optional CLI-only keys if present.
525
- const restObj = { ...rest };
526
- delete restObj.debug;
527
- delete restObj.scripts;
528
- const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
529
- // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
530
- let parsedVars;
531
- if (typeof vars === 'string') {
532
- const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
533
- ? RegExp(varsAssignorPattern)
534
- : (varsAssignor ?? '=')));
535
- parsedVars = Object.fromEntries(kvPairs);
536
- }
537
- else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
538
- // Keep only string or undefined values to match ProcessEnv.
539
- const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
540
- parsedVars = Object.fromEntries(entries);
541
- }
542
- // Drop undefined-valued entries at the converter stage to match ProcessEnv
543
- // expectations and the compat test assertions.
544
- if (parsedVars) {
545
- parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
546
- }
547
- // Tolerate paths as either a delimited string or string[]
548
- // Use a locally cast union type to avoid lint warnings about always-falsy conditions
549
- // under the RootOptionsShape (which declares paths as string | undefined).
550
- const pathsAny = paths;
551
- const pathsOut = Array.isArray(pathsAny)
552
- ? pathsAny.filter((p) => typeof p === 'string')
553
- : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
554
- // Preserve exactOptionalPropertyTypes: only include keys when defined.
555
- return {
556
- ...restObj,
557
- ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
558
- ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
559
- };
560
- };
561
- const resolveGetDotenvOptions = async (customOptions) => {
562
- /**
563
- * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
564
- *
565
- * 1. Base defaults derived from the CLI generator defaults
566
- * ({@link baseGetDotenvCliOptions}).
567
- * 2. Local project overrides from a `getdotenv.config.json` in the nearest
568
- * package root (if present).
569
- * 3. The provided {@link customOptions}.
570
- *
571
- * The result preserves explicit empty values and drops only `undefined`.
572
- *
573
- * @returns Fully-resolved {@link GetDotenvOptions}.
574
- *
575
- * @example
576
- * ```ts
577
- * const options = await resolveGetDotenvOptions({ env: 'dev' });
578
- * ```
579
- */
580
- const localPkgDir = await packageDirectory.packageDirectory();
581
- const localOptionsPath = localPkgDir
582
- ? path.join(localPkgDir, getDotenvOptionsFilename)
583
- : undefined;
584
- // Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
585
- let localOptions = {};
586
- if (localOptionsPath && (await fs.exists(localOptionsPath))) {
587
- try {
588
- const txt = await fs.readFile(localOptionsPath, 'utf-8');
589
- const parsed = JSON.parse(txt);
590
- if (parsed && typeof parsed === 'object') {
591
- localOptions = parsed;
592
- }
593
- }
594
- catch {
595
- // Malformed or unreadable local options are treated as absent.
596
- localOptions = {};
597
- }
598
- }
599
- // Merge order: base < local < custom (custom has highest precedence)
600
- const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
601
- const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
602
- const result = defaultsDeep(defaultsFromCli, customOptions);
603
- return {
604
- ...result, // Keep explicit empty strings/zeros; drop only undefined
605
- vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
606
- };
607
- };
608
-
609
- /**
610
- * Dotenv expansion utilities.
611
- *
612
- * This module implements recursive expansion of environment-variable
613
- * references in strings and records. It supports both whitespace and
614
- * bracket syntaxes with optional defaults:
615
- *
616
- * - Whitespace: `$VAR[:default]`
617
- * - Bracketed: `${VAR[:default]}`
618
- *
619
- * Escaped dollar signs (`\$`) are preserved.
620
- * Unknown variables resolve to empty string unless a default is provided.
621
- */
622
- /**
623
- * Like String.prototype.search but returns the last index.
624
- * @internal
625
- */
626
- const searchLast = (str, rgx) => {
627
- const matches = Array.from(str.matchAll(rgx));
628
- return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
629
- };
630
- const replaceMatch = (value, match, ref) => {
631
- /**
632
- * @internal
633
- */
634
- const group = match[0];
635
- const key = match[1];
636
- const defaultValue = match[2];
637
- if (!key)
638
- return value;
639
- const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
640
- return interpolate(replacement, ref);
641
- };
642
- const interpolate = (value = '', ref = {}) => {
643
- /**
644
- * @internal
645
- */
646
- // if value is falsy, return it as is
647
- if (!value)
648
- return value;
649
- // get position of last unescaped dollar sign
650
- const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
651
- // return value if none found
652
- if (lastUnescapedDollarSignIndex === -1)
653
- return value;
654
- // evaluate the value tail
655
- const tail = value.slice(lastUnescapedDollarSignIndex);
656
- // find whitespace pattern: $KEY:DEFAULT
657
- const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
658
- const whitespaceMatch = whitespacePattern.exec(tail);
659
- if (whitespaceMatch != null)
660
- return replaceMatch(value, whitespaceMatch, ref);
661
- else {
662
- // find bracket pattern: ${KEY:DEFAULT}
663
- const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
664
- const bracketMatch = bracketPattern.exec(tail);
665
- if (bracketMatch != null)
666
- return replaceMatch(value, bracketMatch, ref);
667
- }
668
- return value;
669
- };
670
- /**
671
- * Recursively expands environment variables in a string. Variables may be
672
- * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
673
- * Unknown variables will expand to an empty string.
674
- *
675
- * @param value - The string to expand.
676
- * @param ref - The reference object to use for variable expansion.
677
- * @returns The expanded string.
678
- *
679
- * @example
680
- * ```ts
681
- * process.env.FOO = 'bar';
682
- * dotenvExpand('Hello $FOO'); // "Hello bar"
683
- * dotenvExpand('Hello $BAZ:world'); // "Hello world"
684
- * ```
685
- *
686
- * @remarks
687
- * The expansion is recursive. If a referenced variable itself contains
688
- * references, those will also be expanded until a stable value is reached.
689
- * Escaped references (e.g. `\$FOO`) are preserved as literals.
690
- */
691
- const dotenvExpand = (value, ref = process.env) => {
692
- const result = interpolate(value, ref);
693
- return result ? result.replace(/\\\$/g, '$') : undefined;
694
- };
695
- /**
696
- * Recursively expands environment variables in the values of a JSON object.
697
- * Variables may be presented with optional default as `$VAR[:default]` or
698
- * `${VAR[:default]}`. Unknown variables will expand to an empty string.
699
- *
700
- * @param values - The values object to expand.
701
- * @param options - Expansion options.
702
- * @returns The value object with expanded string values.
703
- *
704
- * @example
705
- * ```ts
706
- * process.env.FOO = 'bar';
707
- * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
708
- * // => { A: "bar", B: "xbary" }
709
- * ```
710
- *
711
- * @remarks
712
- * Options:
713
- * - ref: The reference object to use for expansion (defaults to process.env).
714
- * - progressive: Whether to progressively add expanded values to the set of
715
- * reference keys.
716
- *
717
- * When `progressive` is true, each expanded key becomes available for
718
- * subsequent expansions in the same object (left-to-right by object key order).
719
- */
720
- const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
721
- const { ref = process.env, progressive = false } = options;
722
- acc[key] = dotenvExpand(values[key], {
723
- ...ref,
724
- ...(progressive ? acc : {}),
725
- });
726
- return acc;
727
- }, {});
728
- /**
729
- * Recursively expands environment variables in a string using `process.env` as
730
- * the expansion reference. Variables may be presented with optional default as
731
- * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
732
- * empty string.
733
- *
734
- * @param value - The string to expand.
735
- * @returns The expanded string.
736
- *
737
- * @example
738
- * ```ts
739
- * process.env.FOO = 'bar';
740
- * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
741
- * ```
742
- */
743
- const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
744
-
745
- /* eslint-disable @typescript-eslint/no-deprecated */
746
- /**
747
- * Attach root flags to a GetDotenvCli instance.
748
- * - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
749
- * - Any flag that displays an effective default in help uses dynamic descriptions.
750
- */
751
- const attachRootOptions = (program, defaults, opts) => {
752
- // Install temporary wrappers to tag all options added here as "base" for grouped help.
753
- const GROUP = 'base';
754
- const tagLatest = (cmd, group) => {
755
- const optsArr = cmd.options;
756
- if (Array.isArray(optsArr) && optsArr.length > 0) {
757
- const last = optsArr[optsArr.length - 1];
758
- last.__group = group;
759
- }
760
- };
761
- const originalAddOption = program.addOption.bind(program);
762
- const originalOption = program.option.bind(program);
763
- program.addOption = function patchedAdd(opt) {
764
- opt.__group = GROUP;
765
- return originalAddOption(opt);
766
- };
767
- program.option = function patchedOption(...args) {
768
- const ret = originalOption(...args);
769
- tagLatest(this, GROUP);
770
- return ret;
771
- };
772
- const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
773
- const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
774
- const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
775
- // Helper: append (default) tags for ON/OFF toggles
776
- const onOff = (on, isDefault) => on
777
- ? `ON${isDefault ? ' (default)' : ''}`
778
- : `OFF${isDefault ? ' (default)' : ''}`;
779
- let p = program
780
- .enablePositionalOptions()
781
- .passThroughOptions()
782
- .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
783
- p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
784
- ['KEY1', 'VAL1'],
785
- ['KEY2', 'VAL2'],
786
- ]
787
- .map((v) => v.join(va))
788
- .join(vd)}`, dotenvExpandFromProcessEnv);
789
- if (opts?.includeCommandOption === true) {
790
- p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
791
- }
792
- // Output path (interpolated later; help can remain static)
793
- p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
794
- // Shell ON (string or boolean true => default shell)
795
- p = p
796
- .addOption(program
797
- .createDynamicOption('-s, --shell [string]', (cfg) => {
798
- const s = cfg.shell;
799
- let tag = '';
800
- if (typeof s === 'boolean' && s)
801
- tag = ' (default OS shell)';
802
- else if (typeof s === 'string' && s.length > 0)
803
- tag = ` (default ${s})`;
804
- return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
805
- })
806
- .conflicts('shellOff'))
807
- // Shell OFF
808
- .addOption(program
809
- .createDynamicOption('-S, --shell-off', (cfg) => {
810
- const s = cfg.shell;
811
- return `command execution shell OFF${s === false ? ' (default)' : ''}`;
812
- })
813
- .conflicts('shell'));
814
- // Load process ON/OFF (dynamic defaults)
815
- p = p
816
- .addOption(program
817
- .createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
818
- .conflicts('loadProcessOff'))
819
- .addOption(program
820
- .createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
821
- .conflicts('loadProcess'));
822
- // Exclusion master toggle (dynamic)
823
- p = p
824
- .addOption(program
825
- .createDynamicOption('-a, --exclude-all', (cfg) => {
826
- const c = cfg;
827
- const allOn = !!c.excludeDynamic &&
828
- ((!!c.excludeEnv && !!c.excludeGlobal) ||
829
- (!!c.excludePrivate && !!c.excludePublic));
830
- const suffix = allOn ? ' (default)' : '';
831
- return `exclude all dotenv variables from loading ON${suffix}`;
832
- })
833
- .conflicts('excludeAllOff'))
834
- .addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
835
- // Per-family exclusions (dynamic defaults)
836
- p = p
837
- .addOption(program
838
- .createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
839
- .conflicts('excludeDynamicOff'))
840
- .addOption(program
841
- .createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
842
- .conflicts('excludeDynamic'))
843
- .addOption(program
844
- .createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
845
- .conflicts('excludeEnvOff'))
846
- .addOption(program
847
- .createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
848
- .conflicts('excludeEnv'))
849
- .addOption(program
850
- .createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
851
- .conflicts('excludeGlobalOff'))
852
- .addOption(program
853
- .createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
854
- .conflicts('excludeGlobal'))
855
- .addOption(program
856
- .createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
857
- .conflicts('excludePrivateOff'))
858
- .addOption(program
859
- .createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
860
- .conflicts('excludePrivate'))
861
- .addOption(program
862
- .createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
863
- .conflicts('excludePublicOff'))
864
- .addOption(program
865
- .createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
866
- .conflicts('excludePublic'));
867
- // Log ON/OFF (dynamic)
868
- p = p
869
- .addOption(program
870
- .createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
871
- .conflicts('logOff'))
872
- .addOption(program
873
- .createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
874
- .conflicts('log'));
875
- // Capture flag (no default display; static)
876
- p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
877
- // Core bootstrap/static flags (kept static in help)
878
- p = p
879
- .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
880
- .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
881
- .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
882
- .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
883
- .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
884
- .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
885
- .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
886
- .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
887
- .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
888
- .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
889
- .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
890
- // Hidden scripts pipe-through (stringified)
891
- .addOption(new commander.Option('--scripts <string>')
892
- .default(JSON.stringify(scripts))
893
- .hideHelp());
894
- // Diagnostics / validation / entropy
895
- p = p
896
- .option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
897
- .option('--strict', 'fail on env validation errors (schema/requiredKeys)');
898
- p = p
899
- .addOption(program
900
- .createDynamicOption('--entropy-warn', (cfg) => {
901
- const warn = cfg.warnEntropy;
902
- // Default is effectively ON when warnEntropy is true or undefined.
903
- return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
904
- })
905
- .conflicts('entropyWarnOff'))
906
- .addOption(program
907
- .createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
908
- .conflicts('entropyWarn'))
909
- .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
910
- .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
911
- .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
912
- .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
913
- // Restore original methods
914
- program.addOption = originalAddOption;
915
- program.option = originalOption;
916
- return p;
917
- };
918
-
919
- /**
920
- * Zod schemas for programmatic GetDotenv options.
921
- *
922
- * NOTE: These schemas are introduced without wiring to avoid behavior changes.
923
- * Legacy paths continue to use existing types/logic. The new plugin host will
924
- * use these schemas in strict mode; legacy paths will adopt them in warn mode
925
- * later per the staged plan.
926
- */
927
- // Minimal process env representation: string values or undefined to indicate "unset".
928
- const processEnvSchema = zod.z.record(zod.z.string(), zod.z.string().optional());
929
- // RAW: all fields optional — undefined means "inherit" from lower layers.
930
- const getDotenvOptionsSchemaRaw = zod.z.object({
931
- defaultEnv: zod.z.string().optional(),
932
- dotenvToken: zod.z.string().optional(),
933
- dynamicPath: zod.z.string().optional(),
934
- // Dynamic map is intentionally wide for now; refine once sources are normalized.
935
- dynamic: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
936
- env: zod.z.string().optional(),
937
- excludeDynamic: zod.z.boolean().optional(),
938
- excludeEnv: zod.z.boolean().optional(),
939
- excludeGlobal: zod.z.boolean().optional(),
940
- excludePrivate: zod.z.boolean().optional(),
941
- excludePublic: zod.z.boolean().optional(),
942
- loadProcess: zod.z.boolean().optional(),
943
- log: zod.z.boolean().optional(),
944
- outputPath: zod.z.string().optional(),
945
- paths: zod.z.array(zod.z.string()).optional(),
946
- privateToken: zod.z.string().optional(),
947
- vars: processEnvSchema.optional(),
948
- // Host-only feature flag: guarded integration of config loader/overlay
949
- useConfigLoader: zod.z.boolean().optional(),
950
- });
951
- // RESOLVED: service-boundary contract (post-inheritance).
952
- // For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
953
- const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
954
-
955
- const applyKv = (current, kv) => {
956
- if (!kv || Object.keys(kv).length === 0)
957
- return current;
958
- const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
959
- return { ...current, ...expanded };
960
- };
961
- const applyConfigSlice = (current, cfg, env) => {
962
- if (!cfg)
963
- return current;
964
- // kind axis: global then env (env overrides global)
965
- const afterGlobal = applyKv(current, cfg.vars);
966
- const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
967
- return applyKv(afterGlobal, envKv);
968
- };
969
- /**
970
- * Overlay config-provided values onto a base ProcessEnv using precedence axes:
971
- * - kind: env \> global
972
- * - privacy: local \> public
973
- * - source: project \> packaged \> base
974
- *
975
- * Programmatic explicit vars (if provided) override all config slices.
976
- * Progressive expansion is applied within each slice.
977
- */
978
- const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
979
- let current = { ...base };
980
- // Source: packaged (public -> local)
981
- current = applyConfigSlice(current, configs.packaged, env);
982
- // Packaged "local" is not expected by policy; if present, honor it.
983
- // We do not have a separate object for packaged.local in sources, keep as-is.
984
- // Source: project (public -> local)
985
- current = applyConfigSlice(current, configs.project?.public, env);
986
- current = applyConfigSlice(current, configs.project?.local, env);
987
- // Programmatic explicit vars (top of static tier)
988
- if (programmaticVars) {
989
- const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
990
- current = applyKv(current, toApply);
991
- }
992
- return current;
993
- };
994
-
995
- /** src/diagnostics/entropy.ts
996
- * Entropy diagnostics (presentation-only).
997
- * - Gated by min length and printable ASCII.
998
- * - Warn once per key per run when bits/char \>= threshold.
999
- * - Supports whitelist patterns to suppress known-noise keys.
1000
- */
1001
- const warned = new Set();
1002
- const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
1003
- const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
1004
- const whitelisted = (key, regs) => regs.some((re) => re.test(key));
1005
- const shannonBitsPerChar = (s) => {
1006
- const freq = new Map();
1007
- for (const ch of s)
1008
- freq.set(ch, (freq.get(ch) ?? 0) + 1);
1009
- const n = s.length;
1010
- let h = 0;
1011
- for (const c of freq.values()) {
1012
- const p = c / n;
1013
- h -= p * Math.log2(p);
1014
- }
1015
- return h;
1016
- };
1017
- /**
1018
- * Maybe emit a one-line entropy warning for a key.
1019
- * Caller supplies an `emit(line)` function; the helper ensures once-per-key.
1020
- */
1021
- const maybeWarnEntropy = (key, value, origin, opts, emit) => {
1022
- if (!opts || opts.warnEntropy === false)
1023
- return;
1024
- if (warned.has(key))
1025
- return;
1026
- const v = value ?? '';
1027
- const minLen = Math.max(0, opts.entropyMinLength ?? 16);
1028
- const threshold = opts.entropyThreshold ?? 3.8;
1029
- if (v.length < minLen)
1030
- return;
1031
- if (!isPrintableAscii(v))
1032
- return;
1033
- const wl = compile$1(opts.entropyWhitelist);
1034
- if (whitelisted(key, wl))
1035
- return;
1036
- const bpc = shannonBitsPerChar(v);
1037
- if (bpc >= threshold) {
1038
- warned.add(key);
1039
- emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
1040
- }
1041
- };
1042
-
1043
- const DEFAULT_PATTERNS = [
1044
- '\\bsecret\\b',
1045
- '\\btoken\\b',
1046
- '\\bpass(word)?\\b',
1047
- '\\bapi[_-]?key\\b',
1048
- '\\bkey\\b',
1049
- ];
1050
- const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
1051
- const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
1052
- const MASK = '[redacted]';
1053
- /**
1054
- * Produce a shallow redacted copy of an env-like object for display.
1055
- */
1056
- const redactObject = (obj, opts) => {
1057
- if (!opts?.redact)
1058
- return { ...obj };
1059
- const regs = compile(opts.redactPatterns);
1060
- const out = {};
1061
- for (const [k, v] of Object.entries(obj)) {
1062
- out[k] = v && shouldRedactKey(k, regs) ? MASK : v;
1063
- }
1064
- return out;
1065
- };
1066
-
1067
- /**
1068
- * Asynchronously read a dotenv file & parse it into an object.
1069
- *
1070
- * @param path - Path to dotenv file.
1071
- * @returns The parsed dotenv object.
1072
- */
1073
- const readDotenv = async (path) => {
1074
- try {
1075
- return (await fs.exists(path)) ? dotenv.parse(await fs.readFile(path)) : {};
1076
- }
1077
- catch {
1078
- return {};
1079
- }
1080
- };
1081
-
1082
- const importDefault = async (fileUrl) => {
1083
- const mod = (await import(fileUrl));
1084
- return mod.default;
1085
- };
1086
- const cacheHash = (absPath, mtimeMs) => crypto.createHash('sha1')
1087
- .update(absPath)
1088
- .update(String(mtimeMs))
1089
- .digest('hex')
1090
- .slice(0, 12);
1091
- /**
1092
- * Remove older compiled cache files for a given source base name, keeping
1093
- * at most `keep` most-recent files. Errors are ignored by design.
1094
- */
1095
- const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
1096
- try {
1097
- const entries = await fs.readdir(cacheDir);
1098
- const mine = entries
1099
- .filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
1100
- .map((f) => path.join(cacheDir, f));
1101
- if (mine.length <= keep)
1102
- return;
1103
- const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
1104
- stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
1105
- const toDelete = stats.slice(keep).map((s) => s.p);
1106
- await Promise.all(toDelete.map(async (p) => {
1107
- try {
1108
- await fs.remove(p);
1109
- }
1110
- catch {
1111
- // best-effort cleanup
1112
- }
1113
- }));
1114
- }
1115
- catch {
1116
- // best-effort cleanup
1117
- }
1118
- };
1119
- /**
1120
- * Load a module default export from a JS/TS file with robust fallbacks:
1121
- * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
1122
- * 1) try direct import (if a TS loader is active),
1123
- * 2) esbuild bundle to a temp ESM file,
1124
- * 3) typescript.transpileModule fallback for simple modules.
1125
- *
1126
- * @param absPath - absolute path to source file
1127
- * @param cacheDirName - cache subfolder under .tsbuild
1128
- */
1129
- const loadModuleDefault = async (absPath, cacheDirName) => {
1130
- const ext = path.extname(absPath).toLowerCase();
1131
- const fileUrl = url.pathToFileURL(absPath).toString();
1132
- if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
1133
- return importDefault(fileUrl);
1134
- }
1135
- // Try direct import first (TS loader active)
1136
- try {
1137
- const dyn = await importDefault(fileUrl);
1138
- if (dyn)
1139
- return dyn;
1140
- }
1141
- catch {
1142
- /* fall through */
1143
- }
1144
- const stat = await fs.stat(absPath);
1145
- const hash = cacheHash(absPath, stat.mtimeMs);
1146
- const cacheDir = path.resolve('.tsbuild', cacheDirName);
1147
- await fs.ensureDir(cacheDir);
1148
- const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
1149
- // Try esbuild
1150
- try {
1151
- const esbuild = (await import('esbuild'));
1152
- await esbuild.build({
1153
- entryPoints: [absPath],
1154
- bundle: true,
1155
- platform: 'node',
1156
- format: 'esm',
1157
- target: 'node20',
1158
- outfile: cacheFile,
1159
- sourcemap: false,
1160
- logLevel: 'silent',
1161
- });
1162
- const result = await importDefault(url.pathToFileURL(cacheFile).toString());
1163
- // Best-effort: trim older cache files for this source.
1164
- await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
1165
- return result;
1166
- }
1167
- catch {
1168
- /* fall through to TS transpile */
1169
- }
1170
- // TypeScript transpile fallback
1171
- try {
1172
- const ts = (await import('typescript'));
1173
- const code = await fs.readFile(absPath, 'utf-8');
1174
- const out = ts.transpileModule(code, {
1175
- compilerOptions: {
1176
- module: 'ESNext',
1177
- target: 'ES2022',
1178
- moduleResolution: 'NodeNext',
1179
- },
1180
- }).outputText;
1181
- await fs.writeFile(cacheFile, out, 'utf-8');
1182
- const result = await importDefault(url.pathToFileURL(cacheFile).toString());
1183
- // Best-effort: trim older cache files for this source.
1184
- await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
1185
- return result;
1186
- }
1187
- catch {
1188
- // Caller decides final error wording; rethrow for upstream mapping.
1189
- throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
1190
- }
1191
- };
1192
-
1193
- /**
1194
- * Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
1195
- *
1196
- * @param options - `GetDotenvOptions` object
1197
- * @returns The combined parsed dotenv object.
1198
- * * @example Load from the project root with default tokens
1199
- * ```ts
1200
- * const vars = await getDotenv();
1201
- * console.log(vars.MY_SETTING);
1202
- * ```
1203
- *
1204
- * @example Load from multiple paths and a specific environment
1205
- * ```ts
1206
- * const vars = await getDotenv({
1207
- * env: 'dev',
1208
- * dotenvToken: '.testenv',
1209
- * privateToken: 'secret',
1210
- * paths: ['./', './packages/app'],
1211
- * });
1212
- * ```
1213
- *
1214
- * @example Use dynamic variables
1215
- * ```ts
1216
- * // .env.js default-exports: { DYNAMIC: ({ PREV }) => `${PREV}-suffix` }
1217
- * const vars = await getDotenv({ dynamicPath: '.env.js' });
1218
- * ```
1219
- *
1220
- * @remarks
1221
- * - When {@link GetDotenvOptions.loadProcess} is true, the resulting variables are merged
1222
- * into `process.env` as a side effect.
1223
- * - When {@link GetDotenvOptions.outputPath} is provided, a consolidated dotenv file is written.
1224
- * The path is resolved after expansion, so it may reference previously loaded vars.
1225
- *
1226
- * @throws Error when a dynamic module is present but cannot be imported.
1227
- * @throws Error when an output path was requested but could not be resolved.
1228
- */
1229
- const getDotenv = async (options = {}) => {
1230
- // Apply defaults.
1231
- 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);
1232
- // Read .env files.
1233
- const loaded = paths.length
1234
- ? await paths.reduce(async (e, p) => {
1235
- const publicGlobal = excludePublic || excludeGlobal
1236
- ? Promise.resolve({})
1237
- : readDotenv(path.resolve(p, dotenvToken));
1238
- const publicEnv = excludePublic || excludeEnv || (!env && !defaultEnv)
1239
- ? Promise.resolve({})
1240
- : readDotenv(path.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}`));
1241
- const privateGlobal = excludePrivate || excludeGlobal
1242
- ? Promise.resolve({})
1243
- : readDotenv(path.resolve(p, `${dotenvToken}.${privateToken}`));
1244
- const privateEnv = excludePrivate || excludeEnv || (!env && !defaultEnv)
1245
- ? Promise.resolve({})
1246
- : readDotenv(path.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}.${privateToken}`));
1247
- const [eResolved, publicGlobalResolved, publicEnvResolved, privateGlobalResolved, privateEnvResolved,] = await Promise.all([
1248
- e,
1249
- publicGlobal,
1250
- publicEnv,
1251
- privateGlobal,
1252
- privateEnv,
1253
- ]);
1254
- return {
1255
- ...eResolved,
1256
- ...publicGlobalResolved,
1257
- ...publicEnvResolved,
1258
- ...privateGlobalResolved,
1259
- ...privateEnvResolved,
1260
- };
1261
- }, Promise.resolve({}))
1262
- : {};
1263
- const outputKey = nanoid.nanoid();
1264
- const dotenv = dotenvExpandAll({
1265
- ...loaded,
1266
- ...vars,
1267
- ...(outputPath ? { [outputKey]: outputPath } : {}),
1268
- }, { progressive: true });
1269
- // Process dynamic variables. Programmatic option takes precedence over path.
1270
- if (!excludeDynamic) {
1271
- let dynamic = undefined;
1272
- if (options.dynamic && Object.keys(options.dynamic).length > 0) {
1273
- dynamic = options.dynamic;
1274
- }
1275
- else if (dynamicPath) {
1276
- const absDynamicPath = path.resolve(dynamicPath);
1277
- if (await fs.exists(absDynamicPath)) {
1278
- try {
1279
- dynamic = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic');
1280
- }
1281
- catch {
1282
- // Preserve legacy error text for compatibility with tests/docs.
1283
- throw new Error(`Unable to load dynamic TypeScript file: ${absDynamicPath}. ` +
1284
- `Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
1285
- }
1286
- }
1287
- }
1288
- if (dynamic) {
1289
- try {
1290
- for (const key in dynamic)
1291
- Object.assign(dotenv, {
1292
- [key]: typeof dynamic[key] === 'function'
1293
- ? dynamic[key](dotenv, env ?? defaultEnv)
1294
- : dynamic[key],
1295
- });
1296
- }
1297
- catch {
1298
- throw new Error(`Unable to evaluate dynamic variables.`);
1299
- }
1300
- }
1301
- }
1302
- // Write output file.
1303
- let resultDotenv = dotenv;
1304
- if (outputPath) {
1305
- const outputPathResolved = dotenv[outputKey];
1306
- if (!outputPathResolved)
1307
- throw new Error('Output path not found.');
1308
- const { [outputKey]: _omitted, ...dotenvForOutput } = dotenv;
1309
- await fs.writeFile(outputPathResolved, Object.keys(dotenvForOutput).reduce((contents, key) => {
1310
- const value = dotenvForOutput[key] ?? '';
1311
- return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
1312
- }, ''), { encoding: 'utf-8' });
1313
- resultDotenv = dotenvForOutput;
1314
- }
1315
- // Log result.
1316
- if (log) {
1317
- const redactFlag = options.redact ?? false;
1318
- const redactPatterns = options.redactPatterns ?? undefined;
1319
- const redOpts = {};
1320
- if (redactFlag)
1321
- redOpts.redact = true;
1322
- if (redactFlag && Array.isArray(redactPatterns))
1323
- redOpts.redactPatterns = redactPatterns;
1324
- const bag = redactFlag
1325
- ? redactObject(resultDotenv, redOpts)
1326
- : { ...resultDotenv };
1327
- logger.log(bag);
1328
- // Entropy warnings: once-per-key-per-run (presentation only)
1329
- const warnEntropyVal = options.warnEntropy ?? true;
1330
- const entropyThresholdVal = options
1331
- .entropyThreshold;
1332
- const entropyMinLengthVal = options
1333
- .entropyMinLength;
1334
- const entropyWhitelistVal = options
1335
- .entropyWhitelist;
1336
- const entOpts = {};
1337
- if (typeof warnEntropyVal === 'boolean')
1338
- entOpts.warnEntropy = warnEntropyVal;
1339
- if (typeof entropyThresholdVal === 'number')
1340
- entOpts.entropyThreshold = entropyThresholdVal;
1341
- if (typeof entropyMinLengthVal === 'number')
1342
- entOpts.entropyMinLength = entropyMinLengthVal;
1343
- if (Array.isArray(entropyWhitelistVal))
1344
- entOpts.entropyWhitelist = entropyWhitelistVal;
1345
- for (const [k, v] of Object.entries(resultDotenv)) {
1346
- maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
1347
- logger.log(line);
1348
- });
1349
- }
1350
- }
1351
- // Load process.env.
1352
- if (loadProcess)
1353
- Object.assign(process.env, resultDotenv);
1354
- return resultDotenv;
1355
- };
1356
-
1357
- /**
1358
- * Deep interpolation utility for string leaves.
1359
- * - Expands string values using dotenv-style expansion against the provided envRef.
1360
- * - Preserves non-strings as-is.
1361
- * - Does not recurse into arrays (arrays are returned unchanged).
1362
- *
1363
- * Intended for:
1364
- * - Phase C option/config interpolation after composing ctx.dotenv.
1365
- * - Per-plugin config slice interpolation before afterResolve.
1366
- */
1367
- /** @internal */
1368
- const isPlainObject = (v) => v !== null &&
1369
- typeof v === 'object' &&
1370
- !Array.isArray(v) &&
1371
- Object.getPrototypeOf(v) === Object.prototype;
1372
- /**
1373
- * Deeply interpolate string leaves against envRef.
1374
- * Arrays are not recursed into; they are returned unchanged.
1375
- *
1376
- * @typeParam T - Shape of the input value.
1377
- * @param value - Input value (object/array/primitive).
1378
- * @param envRef - Reference environment for interpolation.
1379
- * @returns A new value with string leaves interpolated.
1380
- */
1381
- const interpolateDeep = (value, envRef) => {
1382
- // Strings: expand and return
1383
- if (typeof value === 'string') {
1384
- const out = dotenvExpand(value, envRef);
1385
- // dotenvExpand returns string | undefined; preserve original on undefined
1386
- return (out ?? value);
1387
- }
1388
- // Arrays: return as-is (no recursion)
1389
- if (Array.isArray(value)) {
1390
- return value;
1391
- }
1392
- // Plain objects: shallow clone and recurse into values
1393
- if (isPlainObject(value)) {
1394
- const src = value;
1395
- const out = {};
1396
- for (const [k, v] of Object.entries(src)) {
1397
- // Recurse for strings/objects; keep arrays as-is; preserve other scalars
1398
- if (typeof v === 'string')
1399
- out[k] = dotenvExpand(v, envRef) ?? v;
1400
- else if (Array.isArray(v))
1401
- out[k] = v;
1402
- else if (isPlainObject(v))
1403
- out[k] = interpolateDeep(v, envRef);
1404
- else
1405
- out[k] = v;
1406
- }
1407
- return out;
1408
- }
1409
- // Other primitives/types: return as-is
1410
- return value;
1411
- };
1412
-
1413
- /**
1414
- * Compute the dotenv context for the host (uses the config loader/overlay path).
1415
- * - Resolves and validates options strictly (host-only).
1416
- * - Applies file cascade, overlays, dynamics, and optional effects.
1417
- * - Merges and validates per-plugin config slices (when provided).
1418
- *
1419
- * @param customOptions - Partial options from the current invocation.
1420
- * @param plugins - Installed plugins (for config validation).
1421
- * @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery). */
1422
- const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1423
- const optionsResolved = await resolveGetDotenvOptions(customOptions);
1424
- const validated = getDotenvOptionsSchemaResolved.parse(optionsResolved);
1425
- // Always-on loader path
1426
- // 1) Base from files only (no dynamic, no programmatic vars)
1427
- const base = await getDotenv({
1428
- ...validated,
1429
- // Build a pure base without side effects or logging.
1430
- excludeDynamic: true,
1431
- vars: {},
1432
- log: false,
1433
- loadProcess: false,
1434
- outputPath: undefined,
1435
- });
1436
- // 2) Discover config sources and overlay
1437
- const sources = await resolveGetDotenvConfigSources(hostMetaUrl);
1438
- const dotenvOverlaid = overlayEnv({
1439
- base,
1440
- env: validated.env ?? validated.defaultEnv,
1441
- configs: sources,
1442
- ...(validated.vars ? { programmaticVars: validated.vars } : {}),
1443
- });
1444
- // Helper to apply a dynamic map progressively.
1445
- const applyDynamic = (target, dynamic, env) => {
1446
- if (!dynamic)
1447
- return;
1448
- for (const key of Object.keys(dynamic)) {
1449
- const value = typeof dynamic[key] === 'function'
1450
- ? dynamic[key](target, env)
1451
- : dynamic[key];
1452
- Object.assign(target, { [key]: value });
1453
- }
1454
- };
1455
- // 3) Apply dynamics in order
1456
- const dotenv = { ...dotenvOverlaid };
1457
- applyDynamic(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
1458
- applyDynamic(dotenv, (sources.packaged?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1459
- applyDynamic(dotenv, (sources.project?.public?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1460
- applyDynamic(dotenv, (sources.project?.local?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
1461
- // file dynamicPath (lowest)
1462
- if (validated.dynamicPath) {
1463
- const absDynamicPath = path.resolve(validated.dynamicPath);
1464
- try {
1465
- const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
1466
- applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
1467
- }
1468
- catch {
1469
- throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
1470
- }
1471
- }
1472
- // 4) Output/log/process merge
1473
- if (validated.outputPath) {
1474
- await fs.writeFile(validated.outputPath, Object.keys(dotenv).reduce((contents, key) => {
1475
- const value = dotenv[key] ?? '';
1476
- return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
1477
- }, ''), { encoding: 'utf-8' });
1478
- }
1479
- const logger = validated.logger ?? console;
1480
- if (validated.log)
1481
- logger.log(dotenv);
1482
- if (validated.loadProcess)
1483
- Object.assign(process.env, dotenv);
1484
- // 5) Merge and validate per-plugin config (packaged < project.public < project.local)
1485
- const packagedPlugins = (sources.packaged &&
1486
- sources.packaged.plugins) ??
1487
- {};
1488
- const publicPlugins = (sources.project?.public &&
1489
- sources.project.public.plugins) ??
1490
- {};
1491
- const localPlugins = (sources.project?.local &&
1492
- sources.project.local.plugins) ??
1493
- {};
1494
- const mergedPluginConfigs = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
1495
- for (const p of plugins) {
1496
- if (!p.id)
1497
- continue;
1498
- const slice = mergedPluginConfigs[p.id];
1499
- if (slice === undefined)
1500
- continue;
1501
- // Per-plugin interpolation just before validation/afterResolve:
1502
- // precedence: process.env wins over ctx.dotenv for slice defaults.
1503
- const envRef = {
1504
- ...dotenv,
1505
- ...process.env,
1506
- };
1507
- const interpolated = interpolateDeep(slice, envRef);
1508
- // Validate if a schema is provided; otherwise accept interpolated slice as-is.
1509
- if (p.configSchema) {
1510
- const parsed = p.configSchema.safeParse(interpolated);
1511
- if (!parsed.success) {
1512
- const msgs = parsed.error.issues
1513
- .map((i) => `${i.path.join('.')}: ${i.message}`)
1514
- .join('\n');
1515
- throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
1516
- }
1517
- mergedPluginConfigs[p.id] = parsed.data;
1518
- }
1519
- else {
1520
- mergedPluginConfigs[p.id] = interpolated;
1521
- }
1522
- }
1523
- return {
1524
- optionsResolved: validated,
1525
- dotenv: dotenv,
1526
- plugins: {},
1527
- pluginConfigs: mergedPluginConfigs,
1528
- };
1529
- };
1530
-
1531
- // Dynamic help support: attach a private symbol to Option for description fns.
1532
- const DYN_DESC_SYM = Symbol('getdotenv.dynamic.description');
1533
- const HOST_META_URL = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cliHost.cjs', document.baseURI).href));
1534
- const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
1535
- const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
1536
- const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
1537
- /**
1538
- * Plugin-first CLI host for get-dotenv. Extends Commander.Command.
1539
- *
1540
- * Responsibilities:
1541
- * - Resolve options strictly and compute dotenv context (resolveAndLoad).
1542
- * - Expose a stable accessor for the current context (getCtx).
1543
- * - Provide a namespacing helper (ns).
1544
- * - Support composable plugins with parent → children install and afterResolve.
1545
- *
1546
- * NOTE: This host is additive and does not alter the legacy CLI.
1547
- */
1548
- let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1549
- /** Registered top-level plugins (composition happens via .use()) */
1550
- _plugins = [];
1551
- /** One-time installation guard */
1552
- _installed = false;
1553
- /** Optional header line to prepend in help output */
1554
- [HELP_HEADER_SYMBOL];
1555
- /**
1556
- * Create a subcommand using the same subclass, preserving helpers like
1557
- * dynamicOption on children.
1558
- */
1559
- createCommand(name) {
1560
- return new this.constructor(name);
1561
- }
1562
- constructor(alias = 'getdotenv') {
1563
- super(alias);
1564
- // Ensure subcommands that use passThroughOptions can be attached safely.
1565
- // Commander requires parent commands to enable positional options when a
1566
- // child uses passThroughOptions.
1567
- this.enablePositionalOptions();
1568
- // Configure grouped help: show only base options in default "Options";
1569
- // we will insert App/Plugin sections before Commands in helpInformation().
1570
- this.configureHelp({
1571
- visibleOptions: (cmd) => {
1572
- const all = cmd.options ?? [];
1573
- const parent = cmd.parent ?? null;
1574
- const isRoot = parent === null;
1575
- const list = isRoot
1576
- ? all.filter((opt) => {
1577
- const group = opt.__group;
1578
- return group === 'base';
1579
- })
1580
- : all.slice(); // subcommands: show all options (their own "Options:" block)
1581
- // Sort: short-aliased options first, then long-only; stable by flags.
1582
- const hasShort = (opt) => {
1583
- const flags = opt.flags ?? '';
1584
- // Matches "-x," or starting "-x " before any long
1585
- return /(^|\s|,)-[A-Za-z]/.test(flags);
1586
- };
1587
- const byFlags = (opt) => opt.flags ?? '';
1588
- list.sort((a, b) => {
1589
- const aS = hasShort(a) ? 1 : 0;
1590
- const bS = hasShort(b) ? 1 : 0;
1591
- return bS - aS || byFlags(a).localeCompare(byFlags(b));
1592
- });
1593
- return list;
1594
- },
1595
- });
1596
- this.addHelpText('beforeAll', () => {
1597
- const header = this[HELP_HEADER_SYMBOL];
1598
- return header && header.length > 0 ? `${header}\n\n` : '';
1599
- });
1600
- // Skeleton preSubcommand hook: produce a context if absent, without
1601
- // mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
1602
- // loadProcess=false here avoids leaking dotenv values into the parent
1603
- // process env before subcommands execute.
1604
- this.hook('preSubcommand', async () => {
1605
- if (this.getCtx())
1606
- return;
1607
- await this.resolveAndLoad({ loadProcess: false });
1608
- });
1609
- }
1610
- /**
1611
- * Resolve options (strict) and compute dotenv context.
1612
- * Stores the context on the instance under a symbol.
1613
- *
1614
- * Options:
1615
- * - opts.runAfterResolve (default true): when false, skips running plugin
1616
- * afterResolve hooks. Useful for top-level help rendering to avoid
1617
- * long-running side-effects while still evaluating dynamic help text.
1618
- */
1619
- async resolveAndLoad(customOptions = {}, opts) {
1620
- // Resolve defaults, then validate strictly under the new host.
1621
- const optionsResolved = await resolveGetDotenvOptions(customOptions);
1622
- getDotenvOptionsSchemaResolved.parse(optionsResolved);
1623
- // Delegate the heavy lifting to the shared helper (guarded path supported).
1624
- const ctx = await computeContext(optionsResolved, this._plugins, HOST_META_URL);
1625
- // Persist context on the instance for later access.
1626
- this[CTX_SYMBOL] =
1627
- ctx;
1628
- // Ensure plugins are installed exactly once, then run afterResolve.
1629
- await this.install();
1630
- if (opts?.runAfterResolve ?? true) {
1631
- await this._runAfterResolve(ctx);
1632
- }
1633
- return ctx;
1634
- }
1635
- /**
1636
- * Create a Commander Option that computes its description at help time.
1637
- * The returned Option may be configured (conflicts, default, parser) and
1638
- * added via addOption().
1639
- */
1640
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
1641
- createDynamicOption(flags, desc, parser, defaultValue) {
1642
- const opt = new commander.Option(flags, '');
1643
- // Keep the function on a private symbol so it survives through Commander.
1644
- opt[DYN_DESC_SYM] = desc;
1645
- if (parser)
1646
- opt.argParser(parser);
1647
- if (defaultValue !== undefined)
1648
- opt.default(defaultValue);
1649
- return opt;
1650
- }
1651
- /**
1652
- * Chainable helper mirroring .option(), but with a dynamic description.
1653
- * Equivalent to addOption(createDynamicOption(...)).
1654
- */
1655
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
1656
- dynamicOption(flags, desc, parser, defaultValue) {
1657
- const opt = this.createDynamicOption(flags, desc, parser, defaultValue);
1658
- this.addOption(opt);
1659
- return this;
1660
- }
1661
- /**
1662
- * Evaluate dynamic descriptions for this command and all descendants using
1663
- * the provided resolved configuration. Mutates the Option.description in
1664
- * place so Commander help renders updated text.
1665
- */
1666
- evaluateDynamicOptions(resolved) {
1667
- const visit = (cmd) => {
1668
- const arr = cmd.options ?? [];
1669
- for (const o of arr) {
1670
- const dyn = o[DYN_DESC_SYM];
1671
- if (typeof dyn === 'function') {
1672
- try {
1673
- const txt = dyn(resolved);
1674
- // Commander Option has a public "description" field used by help.
1675
- o.description = txt;
1676
- }
1677
- catch {
1678
- // Best-effort: leave description as-is on evaluation failure.
1679
- }
1680
- }
1681
- }
1682
- const children = cmd.commands ?? [];
1683
- for (const c of children)
1684
- visit(c);
1685
- };
1686
- visit(this);
1687
- }
1688
- /**
1689
- * Retrieve the current invocation context (if any).
1690
- */
1691
- getCtx() {
1692
- return this[CTX_SYMBOL];
1693
- }
1694
- /**
1695
- * Retrieve the merged root CLI options bag (if set by passOptions()).
1696
- * Downstream-safe: no generics required.
1697
- */
1698
- getOptions() {
1699
- return this[OPTS_SYMBOL];
1700
- }
1701
- /** Internal: set the merged root options bag for this run. */
1702
- _setOptionsBag(bag) {
1703
- this[OPTS_SYMBOL] = bag;
1704
- }
1705
- /** * Convenience helper to create a namespaced subcommand.
1706
- */
1707
- ns(name) {
1708
- return this.command(name);
1709
- }
1710
- /**
1711
- * Tag options added during the provided callback as 'app' for grouped help.
1712
- * Allows downstream apps to demarcate their root-level options.
1713
- */
1714
- tagAppOptions(fn) {
1715
- const root = this;
1716
- const originalAddOption = root.addOption.bind(root);
1717
- // eslint-disable-next-line @typescript-eslint/no-deprecated
1718
- const originalOption = root.option.bind(root);
1719
- const tagLatest = (cmd, group) => {
1720
- const optsArr = cmd.options;
1721
- if (Array.isArray(optsArr) && optsArr.length > 0) {
1722
- const last = optsArr[optsArr.length - 1];
1723
- last.__group = group;
1724
- }
1725
- };
1726
- root.addOption = function patchedAdd(opt) {
1727
- opt.__group = 'app';
1728
- return originalAddOption(opt);
1729
- };
1730
- // eslint-disable-next-line @typescript-eslint/no-deprecated
1731
- root.option = function patchedOption(...args) {
1732
- const ret = originalOption(...args);
1733
- tagLatest(this, 'app');
1734
- return ret;
1735
- };
1736
- try {
1737
- return fn(root);
1738
- }
1739
- finally {
1740
- root.addOption = originalAddOption;
1741
- // eslint-disable-next-line @typescript-eslint/no-deprecated
1742
- root.option = originalOption;
1743
- }
1744
- }
1745
- /**
1746
- * Branding helper: set CLI name/description/version and optional help header.
1747
- * If version is omitted and importMetaUrl is provided, attempts to read the
1748
- * nearest package.json version (best-effort; non-fatal on failure).
1749
- */
1750
- async brand(args) {
1751
- const { name, description, version, importMetaUrl, helpHeader } = args;
1752
- if (typeof name === 'string' && name.length > 0)
1753
- this.name(name);
1754
- if (typeof description === 'string')
1755
- this.description(description);
1756
- let v = version;
1757
- if (!v && importMetaUrl) {
1758
- try {
1759
- const fromUrl = url.fileURLToPath(importMetaUrl);
1760
- const pkgDir = await packageDirectory.packageDirectory({ cwd: fromUrl });
1761
- if (pkgDir) {
1762
- const txt = await fs.readFile(`${pkgDir}/package.json`, 'utf-8');
1763
- const pkg = JSON.parse(txt);
1764
- if (pkg.version)
1765
- v = pkg.version;
1766
- }
1767
- }
1768
- catch {
1769
- // best-effort only
1770
- }
1771
- }
1772
- if (v)
1773
- this.version(v);
1774
- // Help header:
1775
- // - If caller provides helpHeader, use it.
1776
- // - Otherwise, when a version is known, default to "<name> v<version>".
1777
- if (typeof helpHeader === 'string') {
1778
- this[HELP_HEADER_SYMBOL] = helpHeader;
1779
- }
1780
- else if (v) {
1781
- // Use the current command name (possibly overridden by 'name' above).
1782
- const header = `${this.name()} v${v}`;
1783
- this[HELP_HEADER_SYMBOL] = header;
1784
- }
1785
- return this;
1786
- }
1787
- /**
1788
- * Insert grouped plugin/app options between "Options" and "Commands" for
1789
- * hybrid ordering. Applies to root and any parent command.
1790
- */
1791
- helpInformation() {
1792
- // Base help text first (includes beforeAll/after hooks).
1793
- const base = super.helpInformation();
1794
- const groups = this.#renderOptionGroups(this);
1795
- const block = typeof groups === 'string' ? groups.trim() : '';
1796
- let out = base;
1797
- if (!block) {
1798
- // Ensure a trailing blank line even when no extra groups render.
1799
- if (!out.endsWith('\n\n'))
1800
- out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1801
- return out;
1802
- }
1803
- // Insert just before "Commands:" when present.
1804
- const marker = '\nCommands:';
1805
- const idx = base.indexOf(marker);
1806
- if (idx >= 0) {
1807
- const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
1808
- out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
1809
- }
1810
- else {
1811
- // Otherwise append.
1812
- const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
1813
- out = `${base}${sep}${groups}`;
1814
- }
1815
- // Ensure a trailing blank line for prompt separation.
1816
- if (!out.endsWith('\n\n')) {
1817
- out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1818
- }
1819
- return out;
1820
- }
1821
- /**
1822
- * Register a plugin for installation (parent level).
1823
- * Installation occurs on first resolveAndLoad() (or explicit install()).
1824
- */
1825
- use(plugin) {
1826
- this._plugins.push(plugin);
1827
- // Immediately run setup so subcommands exist before parsing.
1828
- const setupOne = (p) => {
1829
- p.setup(this);
1830
- for (const child of p.children)
1831
- setupOne(child);
1832
- };
1833
- setupOne(plugin);
1834
- return this;
1835
- }
1836
- /**
1837
- * Install all registered plugins in parent → children (pre-order).
1838
- * Runs only once per CLI instance.
1839
- */
1840
- async install() {
1841
- // Setup is performed immediately in use(); here we only guard for afterResolve.
1842
- this._installed = true;
1843
- // Satisfy require-await without altering behavior.
1844
- await Promise.resolve();
1845
- }
1846
- /**
1847
- * Run afterResolve hooks for all plugins (parent → children).
1848
- */
1849
- async _runAfterResolve(ctx) {
1850
- const run = async (p) => {
1851
- if (p.afterResolve)
1852
- await p.afterResolve(this, ctx);
1853
- for (const child of p.children)
1854
- await run(child);
1855
- };
1856
- for (const p of this._plugins)
1857
- await run(p);
1858
- }
1859
- // Render App/Plugin grouped options (used by helpInformation override).
1860
- #renderOptionGroups(cmd) {
1861
- const all = cmd.options ?? [];
1862
- const byGroup = new Map();
1863
- for (const o of all) {
1864
- const opt = o;
1865
- const g = opt.__group;
1866
- if (!g || g === 'base')
1867
- continue; // base handled by default help
1868
- const rows = byGroup.get(g) ?? [];
1869
- rows.push({
1870
- flags: opt.flags ?? '',
1871
- description: opt.description ?? '',
1872
- });
1873
- byGroup.set(g, rows);
1874
- }
1875
- if (byGroup.size === 0)
1876
- return '';
1877
- const renderRows = (title, rows) => {
1878
- const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
1879
- // Sort within group: short-aliased flags first
1880
- rows.sort((a, b) => {
1881
- const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
1882
- const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
1883
- return bS - aS || a.flags.localeCompare(b.flags);
1884
- });
1885
- const lines = rows
1886
- .map((r) => {
1887
- const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
1888
- return ` ${r.flags}${pad}${r.description}`.trimEnd();
1889
- })
1890
- .join('\n');
1891
- return `\n${title}:\n${lines}\n`;
1892
- };
1893
- let out = '';
1894
- // App options (if any)
1895
- const app = byGroup.get('app');
1896
- if (app && app.length > 0) {
1897
- out += renderRows('App options', app);
1898
- }
1899
- // Plugin groups sorted by id
1900
- const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
1901
- const currentName = cmd.name?.() ?? '';
1902
- pluginKeys.sort((a, b) => a.localeCompare(b));
1903
- for (const k of pluginKeys) {
1904
- const id = k.slice('plugin:'.length) || '(unknown)';
1905
- const rows = byGroup.get(k) ?? [];
1906
- // Do not show a "Plugin options — <self>" section on the command that owns those options.
1907
- // Only child-injected plugin groups should render at this level.
1908
- if (rows.length > 0 && id !== currentName) {
1909
- out += renderRows(`Plugin options — ${id}`, rows);
1910
- }
1911
- }
1912
- return out;
1913
- }
1914
- };
1915
-
1916
- /** src/cliHost/definePlugin.ts
1917
- * Plugin contracts for the GetDotenv CLI host.
1918
- *
1919
- * This module exposes a structural public interface for the host that plugins
1920
- * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
1921
- * nominal class identity issues (private fields) in downstream consumers.
1922
- */
1923
- /**
1924
- * Define a GetDotenv CLI plugin with compositional helpers.
1925
- *
1926
- * @example
1927
- * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
1928
- * .use(childA)
1929
- * .use(childB);
1930
- */
1931
- const definePlugin = (spec) => {
1932
- const { children = [], ...rest } = spec;
1933
- const plugin = {
1934
- ...rest,
1935
- children: [...children],
1936
- use(child) {
1937
- this.children.push(child);
1938
- return this;
1939
- },
1940
- };
1941
- return plugin;
1942
- };
1943
-
1944
- /**
1945
- * GetDotenvCli with root helpers as real class methods.
1946
- * - attachRootOptions: installs legacy/base root flags on the command.
1947
- * - passOptions: merges flags (parent \< current), computes dotenv context once,
1948
- * runs validation, and persists merged options for nested flows.
1949
- */
1950
- class GetDotenvCli extends GetDotenvCli$1 {
1951
- /**
1952
- * Attach legacy root flags to this CLI instance. Defaults come from
1953
- * baseRootOptionDefaults when none are provided.
1954
- */
1955
- attachRootOptions(defaults, opts) {
1956
- const d = (defaults ?? baseRootOptionDefaults);
1957
- attachRootOptions(this, d, opts);
1958
- return this;
1959
- }
1960
- /**
1961
- * Install preSubcommand/preAction hooks that:
1962
- * - Merge options (parent round-trip + current invocation) using resolveCliOptions.
1963
- * - Persist the merged bag on the current command and on the host (for ergonomics).
1964
- * - Compute the dotenv context once via resolveAndLoad(serviceOptions).
1965
- * - Validate the composed env against discovered config (warn or --strict fail).
1966
- */
1967
- passOptions(defaults) {
1968
- const d = (defaults ?? baseRootOptionDefaults);
1969
- this.hook('preSubcommand', async (thisCommand) => {
1970
- const raw = thisCommand.opts();
1971
- const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
1972
- // Persist merged options (for nested behavior and ergonomic access).
1973
- thisCommand.getDotenvCliOptions =
1974
- merged;
1975
- this._setOptionsBag(merged);
1976
- // Build service options and compute context (always-on loader path).
1977
- const serviceOptions = getDotenvCliOptions2Options(merged);
1978
- await this.resolveAndLoad(serviceOptions);
1979
- // Refresh dynamic option descriptions using resolved config + plugin slices
1980
- try {
1981
- const ctx = this.getCtx();
1982
- this.evaluateDynamicOptions({
1983
- ...ctx?.optionsResolved,
1984
- plugins: ctx?.pluginConfigs ?? {},
1985
- });
1986
- }
1987
- catch {
1988
- /* best-effort */
1989
- }
1990
- // Global validation: once after Phase C using config sources.
1991
- try {
1992
- const ctx = this.getCtx();
1993
- const dotenv = (ctx?.dotenv ?? {});
1994
- const sources = await resolveGetDotenvConfigSources((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cliHost.cjs', document.baseURI).href)));
1995
- const issues = validateEnvAgainstSources(dotenv, sources);
1996
- if (Array.isArray(issues) && issues.length > 0) {
1997
- const logger = (merged
1998
- .logger ?? console);
1999
- const emit = logger.error ?? logger.log;
2000
- issues.forEach((m) => {
2001
- emit(m);
2002
- });
2003
- if (merged.strict) {
2004
- process.exit(1);
2005
- }
2006
- }
2007
- }
2008
- catch {
2009
- // Be tolerant: do not crash non-strict flows on unexpected validator failures.
2010
- }
2011
- });
2012
- // Also handle root-level flows (no subcommand) so option-aliases can run
2013
- // with the same merged options and context without duplicating logic.
2014
- this.hook('preAction', async (thisCommand) => {
2015
- const raw = thisCommand.opts();
2016
- const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
2017
- thisCommand.getDotenvCliOptions =
2018
- merged;
2019
- this._setOptionsBag(merged);
2020
- // Avoid duplicate heavy work if a context is already present.
2021
- if (!this.getCtx()) {
2022
- const serviceOptions = getDotenvCliOptions2Options(merged);
2023
- await this.resolveAndLoad(serviceOptions);
2024
- try {
2025
- const ctx = this.getCtx();
2026
- this.evaluateDynamicOptions({
2027
- ...ctx?.optionsResolved,
2028
- plugins: ctx?.pluginConfigs ?? {},
2029
- });
2030
- }
2031
- catch {
2032
- /* tolerate */
2033
- }
2034
- try {
2035
- const ctx = this.getCtx();
2036
- const dotenv = (ctx?.dotenv ?? {});
2037
- const sources = await resolveGetDotenvConfigSources((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cliHost.cjs', document.baseURI).href)));
2038
- const issues = validateEnvAgainstSources(dotenv, sources);
2039
- if (Array.isArray(issues) && issues.length > 0) {
2040
- const logger = (merged
2041
- .logger ?? console);
2042
- const emit = logger.error ?? logger.log;
2043
- issues.forEach((m) => {
2044
- emit(m);
2045
- });
2046
- if (merged.strict) {
2047
- process.exit(1);
2048
- }
2049
- }
2050
- }
2051
- catch {
2052
- // Tolerate validation side-effects in non-strict mode.
2053
- }
2054
- }
2055
- });
2056
- return this;
2057
- }
2058
- }
2059
- /**
2060
- * Helper to retrieve the merged root options bag from any action handler
2061
- * that only has access to thisCommand. Avoids structural casts.
2062
- */
2063
- const readMergedOptions = (cmd) => {
2064
- // Ascend to the root command
2065
- let root = cmd;
2066
- while (root.parent) {
2067
- root = root.parent;
2068
- }
2069
- const hostAny = root;
2070
- return typeof hostAny.getOptions === 'function'
2071
- ? hostAny.getOptions()
2072
- : root
2073
- .getDotenvCliOptions;
2074
- };
2075
-
2076
- exports.GetDotenvCli = GetDotenvCli;
2077
- exports.definePlugin = definePlugin;
2078
- exports.readMergedOptions = readMergedOptions;