@karmaniverous/get-dotenv 4.6.0-0 → 5.0.0-1

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 (48) hide show
  1. package/README.md +130 -23
  2. package/dist/cliHost.cjs +1089 -0
  3. package/dist/cliHost.d.cts +191 -0
  4. package/dist/cliHost.d.mts +191 -0
  5. package/dist/cliHost.d.ts +191 -0
  6. package/dist/cliHost.mjs +1085 -0
  7. package/dist/config.cjs +247 -0
  8. package/dist/config.d.cts +53 -0
  9. package/dist/config.d.mts +53 -0
  10. package/dist/config.d.ts +53 -0
  11. package/dist/config.mjs +242 -0
  12. package/dist/env-overlay.cjs +163 -0
  13. package/dist/env-overlay.d.cts +48 -0
  14. package/dist/env-overlay.d.mts +48 -0
  15. package/dist/env-overlay.d.ts +48 -0
  16. package/dist/env-overlay.mjs +161 -0
  17. package/dist/getdotenv.cli.mjs +2788 -734
  18. package/dist/index.cjs +902 -280
  19. package/dist/index.d.cts +122 -64
  20. package/dist/index.d.mts +122 -64
  21. package/dist/index.d.ts +122 -64
  22. package/dist/index.mjs +904 -283
  23. package/dist/plugins-aws.cjs +618 -0
  24. package/dist/plugins-aws.d.cts +176 -0
  25. package/dist/plugins-aws.d.mts +176 -0
  26. package/dist/plugins-aws.d.ts +176 -0
  27. package/dist/plugins-aws.mjs +616 -0
  28. package/dist/plugins-batch.cjs +569 -0
  29. package/dist/plugins-batch.d.cts +198 -0
  30. package/dist/plugins-batch.d.mts +198 -0
  31. package/dist/plugins-batch.d.ts +198 -0
  32. package/dist/plugins-batch.mjs +567 -0
  33. package/dist/plugins-init.cjs +282 -0
  34. package/dist/plugins-init.d.cts +180 -0
  35. package/dist/plugins-init.d.mts +180 -0
  36. package/dist/plugins-init.d.ts +180 -0
  37. package/dist/plugins-init.mjs +280 -0
  38. package/getdotenv.config.json +19 -0
  39. package/package.json +88 -17
  40. package/templates/cli/ts/index.ts +9 -0
  41. package/templates/cli/ts/plugins/hello.ts +17 -0
  42. package/templates/config/js/getdotenv.config.js +15 -0
  43. package/templates/config/json/local/getdotenv.config.local.json +7 -0
  44. package/templates/config/json/public/getdotenv.config.json +12 -0
  45. package/templates/config/public/getdotenv.config.json +13 -0
  46. package/templates/config/ts/getdotenv.config.ts +16 -0
  47. package/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
  48. package/templates/config/yaml/public/getdotenv.config.yaml +10 -0
@@ -0,0 +1,1085 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'fs-extra';
3
+ import { packageDirectory } from 'package-directory';
4
+ import path, { join, extname } from 'path';
5
+ import { z } from 'zod';
6
+ import url, { fileURLToPath, pathToFileURL } from 'url';
7
+ import YAML from 'yaml';
8
+ import { nanoid } from 'nanoid';
9
+ import { parse } from 'dotenv';
10
+ import { createHash } from 'crypto';
11
+
12
+ /**
13
+ * Define a GetDotenv CLI plugin with compositional helpers.
14
+ *
15
+ * @example
16
+ * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
17
+ * .use(childA)
18
+ * .use(childB);
19
+ */
20
+ const definePlugin = (spec) => {
21
+ const { children = [], ...rest } = spec;
22
+ const plugin = {
23
+ ...rest,
24
+ children: [...children],
25
+ use(child) {
26
+ this.children.push(child);
27
+ return this;
28
+ },
29
+ };
30
+ return plugin;
31
+ };
32
+
33
+ // Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
34
+ const baseRootOptionDefaults = {
35
+ dotenvToken: '.env',
36
+ loadProcess: true,
37
+ logger: console,
38
+ paths: './',
39
+ pathsDelimiter: ' ',
40
+ privateToken: 'local',
41
+ scripts: {
42
+ 'git-status': {
43
+ cmd: 'git branch --show-current && git status -s -u',
44
+ shell: true,
45
+ },
46
+ },
47
+ shell: true,
48
+ vars: '',
49
+ varsAssignor: '=',
50
+ varsDelimiter: ' ',
51
+ // tri-state flags default to unset unless explicitly provided
52
+ // (debug/log/exclude* resolved via flag utils)
53
+ };
54
+
55
+ const baseGetDotenvCliOptions = baseRootOptionDefaults;
56
+
57
+ /** @internal */
58
+ const isPlainObject = (value) => value !== null &&
59
+ typeof value === 'object' &&
60
+ Object.getPrototypeOf(value) === Object.prototype;
61
+ const mergeInto = (target, source) => {
62
+ for (const [key, sVal] of Object.entries(source)) {
63
+ if (sVal === undefined)
64
+ continue; // do not overwrite with undefined
65
+ const tVal = target[key];
66
+ if (isPlainObject(tVal) && isPlainObject(sVal)) {
67
+ target[key] = mergeInto({ ...tVal }, sVal);
68
+ }
69
+ else if (isPlainObject(sVal)) {
70
+ target[key] = mergeInto({}, sVal);
71
+ }
72
+ else {
73
+ target[key] = sVal;
74
+ }
75
+ }
76
+ return target;
77
+ };
78
+ /**
79
+ * Perform a deep defaults-style merge across plain objects. *
80
+ * - Only merges plain objects (prototype === Object.prototype).
81
+ * - Arrays and non-objects are replaced, not merged.
82
+ * - `undefined` values are ignored and do not overwrite prior values.
83
+ *
84
+ * @typeParam T - The resulting shape after merging all layers.
85
+ * @param layers - Zero or more partial layers in ascending precedence order.
86
+ * @returns The merged object typed as {@link T}.
87
+ *
88
+ * @example
89
+ * defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
90
+ * =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
91
+ */
92
+ const defaultsDeep = (...layers) => {
93
+ const result = layers
94
+ .filter(Boolean)
95
+ .reduce((acc, layer) => mergeInto(acc, layer), {});
96
+ return result;
97
+ };
98
+
99
+ // src/GetDotenvOptions.ts
100
+ const getDotenvOptionsFilename = 'getdotenv.config.json';
101
+ /**
102
+ * Converts programmatic CLI options to `getDotenv` options. *
103
+ * @param cliOptions - CLI options. Defaults to `{}`.
104
+ *
105
+ * @returns `getDotenv` options.
106
+ */
107
+ const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
108
+ /**
109
+ * Convert CLI-facing string options into {@link GetDotenvOptions}.
110
+ *
111
+ * - 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`
112
+ * pairs (configurable delimiters) into a {@link ProcessEnv}.
113
+ * - Drops CLI-only keys that have no programmatic equivalent.
114
+ *
115
+ * @remarks
116
+ * Follows exact-optional semantics by not emitting undefined-valued entries.
117
+ */
118
+ // Drop CLI-only keys (debug/scripts) without relying on Record casts.
119
+ // Create a shallow copy then delete optional CLI-only keys if present.
120
+ const restObj = { ...rest };
121
+ delete restObj.debug;
122
+ delete restObj.scripts;
123
+ const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
124
+ // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
125
+ let parsedVars;
126
+ if (typeof vars === 'string') {
127
+ const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
128
+ ? RegExp(varsAssignorPattern)
129
+ : (varsAssignor ?? '=')));
130
+ parsedVars = Object.fromEntries(kvPairs);
131
+ }
132
+ else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
133
+ // Keep only string or undefined values to match ProcessEnv.
134
+ const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
135
+ parsedVars = Object.fromEntries(entries);
136
+ }
137
+ // Drop undefined-valued entries at the converter stage to match ProcessEnv
138
+ // expectations and the compat test assertions.
139
+ if (parsedVars) {
140
+ parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
141
+ }
142
+ // Tolerate paths as either a delimited string or string[]
143
+ // Use a locally cast union type to avoid lint warnings about always-falsy conditions
144
+ // under the RootOptionsShape (which declares paths as string | undefined).
145
+ const pathsAny = paths;
146
+ const pathsOut = Array.isArray(pathsAny)
147
+ ? pathsAny.filter((p) => typeof p === 'string')
148
+ : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
149
+ // Preserve exactOptionalPropertyTypes: only include keys when defined.
150
+ return {
151
+ ...restObj,
152
+ ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
153
+ ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
154
+ };
155
+ };
156
+ const resolveGetDotenvOptions = async (customOptions) => {
157
+ /**
158
+ * Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
159
+ *
160
+ * 1. Base defaults derived from the CLI generator defaults
161
+ * ({@link baseGetDotenvCliOptions}).
162
+ * 2. Local project overrides from a `getdotenv.config.json` in the nearest
163
+ * package root (if present).
164
+ * 3. The provided {@link customOptions}.
165
+ *
166
+ * The result preserves explicit empty values and drops only `undefined`.
167
+ *
168
+ * @returns Fully-resolved {@link GetDotenvOptions}.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * const options = await resolveGetDotenvOptions({ env: 'dev' });
173
+ * ```
174
+ */
175
+ const localPkgDir = await packageDirectory();
176
+ const localOptionsPath = localPkgDir
177
+ ? join(localPkgDir, getDotenvOptionsFilename)
178
+ : undefined;
179
+ const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
180
+ ? JSON.parse((await fs.readFile(localOptionsPath)).toString())
181
+ : {});
182
+ // Merge order: base < local < custom (custom has highest precedence)
183
+ const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
184
+ const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
185
+ const result = defaultsDeep(defaultsFromCli, customOptions);
186
+ return {
187
+ ...result, // Keep explicit empty strings/zeros; drop only undefined
188
+ vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
189
+ };
190
+ };
191
+
192
+ /**
193
+ * Zod schemas for programmatic GetDotenv options.
194
+ *
195
+ * NOTE: These schemas are introduced without wiring to avoid behavior changes.
196
+ * Legacy paths continue to use existing types/logic. The new plugin host will
197
+ * use these schemas in strict mode; legacy paths will adopt them in warn mode
198
+ * later per the staged plan.
199
+ */
200
+ // Minimal process env representation: string values or undefined to indicate "unset".
201
+ const processEnvSchema = z.record(z.string(), z.string().optional());
202
+ // RAW: all fields optional — undefined means "inherit" from lower layers.
203
+ const getDotenvOptionsSchemaRaw = z.object({
204
+ defaultEnv: z.string().optional(),
205
+ dotenvToken: z.string().optional(),
206
+ dynamicPath: z.string().optional(),
207
+ // Dynamic map is intentionally wide for now; refine once sources are normalized.
208
+ dynamic: z.record(z.string(), z.unknown()).optional(),
209
+ env: z.string().optional(),
210
+ excludeDynamic: z.boolean().optional(),
211
+ excludeEnv: z.boolean().optional(),
212
+ excludeGlobal: z.boolean().optional(),
213
+ excludePrivate: z.boolean().optional(),
214
+ excludePublic: z.boolean().optional(),
215
+ loadProcess: z.boolean().optional(),
216
+ log: z.boolean().optional(),
217
+ outputPath: z.string().optional(),
218
+ paths: z.array(z.string()).optional(),
219
+ privateToken: z.string().optional(),
220
+ vars: processEnvSchema.optional(),
221
+ // Host-only feature flag: guarded integration of config loader/overlay
222
+ useConfigLoader: z.boolean().optional(),
223
+ });
224
+ // RESOLVED: service-boundary contract (post-inheritance).
225
+ // For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
226
+ const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
227
+
228
+ /**
229
+ * Zod schemas for configuration files discovered by the new loader. *
230
+ * Notes:
231
+ * - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
232
+ * - RESOLVED: normalized shapes (paths always string[]).
233
+ * - For this step (JSON/YAML only), any defined `dynamic` will be rejected by the loader.
234
+ */
235
+ // String-only env value map
236
+ const stringMap = z.record(z.string(), z.string());
237
+ const envStringMap = z.record(z.string(), stringMap);
238
+ // Allow string[] or single string for "paths" in RAW; normalize later.
239
+ const rawPathsSchema = z.union([z.array(z.string()), z.string()]).optional();
240
+ const getDotenvConfigSchemaRaw = z.object({
241
+ dotenvToken: z.string().optional(),
242
+ privateToken: z.string().optional(),
243
+ paths: rawPathsSchema,
244
+ loadProcess: z.boolean().optional(),
245
+ log: z.boolean().optional(),
246
+ shell: z.union([z.string(), z.boolean()]).optional(),
247
+ scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
248
+ vars: stringMap.optional(), // public, global
249
+ envVars: envStringMap.optional(), // public, per-env
250
+ // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
251
+ dynamic: z.unknown().optional(),
252
+ // Per-plugin config bag; validated by plugins/host when used.
253
+ plugins: z.record(z.string(), z.unknown()).optional(),
254
+ });
255
+ // Normalize paths to string[]
256
+ const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
257
+ const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
258
+ ...raw,
259
+ paths: normalizePaths(raw.paths),
260
+ }));
261
+
262
+ // Discovery candidates (first match wins per scope/privacy).
263
+ // Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
264
+ const PUBLIC_FILENAMES = [
265
+ 'getdotenv.config.json',
266
+ 'getdotenv.config.yaml',
267
+ 'getdotenv.config.yml',
268
+ 'getdotenv.config.js',
269
+ 'getdotenv.config.mjs',
270
+ 'getdotenv.config.cjs',
271
+ 'getdotenv.config.ts',
272
+ 'getdotenv.config.mts',
273
+ 'getdotenv.config.cts',
274
+ ];
275
+ const LOCAL_FILENAMES = [
276
+ 'getdotenv.config.local.json',
277
+ 'getdotenv.config.local.yaml',
278
+ 'getdotenv.config.local.yml',
279
+ 'getdotenv.config.local.js',
280
+ 'getdotenv.config.local.mjs',
281
+ 'getdotenv.config.local.cjs',
282
+ 'getdotenv.config.local.ts',
283
+ 'getdotenv.config.local.mts',
284
+ 'getdotenv.config.local.cts',
285
+ ];
286
+ const isYaml = (p) => ['.yaml', '.yml'].includes(extname(p).toLowerCase());
287
+ const isJson = (p) => extname(p).toLowerCase() === '.json';
288
+ const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(extname(p).toLowerCase());
289
+ // --- Internal JS/TS module loader helpers (default export) ---
290
+ const importDefault$1 = async (fileUrl) => {
291
+ const mod = (await import(fileUrl));
292
+ return mod.default;
293
+ };
294
+ const cacheName = (absPath, suffix) => {
295
+ // sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
296
+ const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
297
+ return `${base}.${suffix}.mjs`;
298
+ };
299
+ const ensureDir = async (dir) => {
300
+ await fs.ensureDir(dir);
301
+ return dir;
302
+ };
303
+ const loadJsTsDefault = async (absPath) => {
304
+ const fileUrl = pathToFileURL(absPath).toString();
305
+ const ext = extname(absPath).toLowerCase();
306
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
307
+ return importDefault$1(fileUrl);
308
+ }
309
+ // Try direct import first in case a TS loader is active.
310
+ try {
311
+ const val = await importDefault$1(fileUrl);
312
+ if (val)
313
+ return val;
314
+ }
315
+ catch {
316
+ /* fallthrough */
317
+ }
318
+ // esbuild bundle to a temp ESM file
319
+ try {
320
+ const esbuild = (await import('esbuild'));
321
+ const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
322
+ const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
323
+ await esbuild.build({
324
+ entryPoints: [absPath],
325
+ bundle: true,
326
+ platform: 'node',
327
+ format: 'esm',
328
+ target: 'node20',
329
+ outfile,
330
+ sourcemap: false,
331
+ logLevel: 'silent',
332
+ });
333
+ return await importDefault$1(pathToFileURL(outfile).toString());
334
+ }
335
+ catch {
336
+ /* fallthrough to TS transpile */
337
+ }
338
+ // typescript.transpileModule simple transpile (single-file)
339
+ try {
340
+ const ts = (await import('typescript'));
341
+ const src = await fs.readFile(absPath, 'utf-8');
342
+ const out = ts.transpileModule(src, {
343
+ compilerOptions: {
344
+ module: 'ESNext',
345
+ target: 'ES2022',
346
+ moduleResolution: 'NodeNext',
347
+ },
348
+ }).outputText;
349
+ const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
350
+ const outfile = path.join(outDir, cacheName(absPath, 'ts'));
351
+ await fs.writeFile(outfile, out, 'utf-8');
352
+ return await importDefault$1(pathToFileURL(outfile).toString());
353
+ }
354
+ catch {
355
+ throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
356
+ }
357
+ };
358
+ /**
359
+ * Discover JSON/YAML config files in the packaged root and project root.
360
+ * Order: packaged public → project public → project local. */
361
+ const discoverConfigFiles = async (importMetaUrl) => {
362
+ const files = [];
363
+ // Packaged root via importMetaUrl (optional)
364
+ if (importMetaUrl) {
365
+ const fromUrl = fileURLToPath(importMetaUrl);
366
+ const packagedRoot = await packageDirectory({ cwd: fromUrl });
367
+ if (packagedRoot) {
368
+ for (const name of PUBLIC_FILENAMES) {
369
+ const p = join(packagedRoot, name);
370
+ if (await fs.pathExists(p)) {
371
+ files.push({ path: p, privacy: 'public', scope: 'packaged' });
372
+ break; // only one public file expected per scope
373
+ }
374
+ }
375
+ // By policy, packaged .local is not expected; skip even if present.
376
+ }
377
+ }
378
+ // Project root (from current working directory)
379
+ const projectRoot = await packageDirectory();
380
+ if (projectRoot) {
381
+ for (const name of PUBLIC_FILENAMES) {
382
+ const p = join(projectRoot, name);
383
+ if (await fs.pathExists(p)) {
384
+ files.push({ path: p, privacy: 'public', scope: 'project' });
385
+ break;
386
+ }
387
+ }
388
+ for (const name of LOCAL_FILENAMES) {
389
+ const p = join(projectRoot, name);
390
+ if (await fs.pathExists(p)) {
391
+ files.push({ path: p, privacy: 'local', scope: 'project' });
392
+ break;
393
+ }
394
+ }
395
+ }
396
+ return files;
397
+ };
398
+ /**
399
+ * Load a single config file (JSON/YAML). JS/TS is not supported in this step.
400
+ * Validates with Zod RAW schema, then normalizes to RESOLVED.
401
+ *
402
+ * For JSON/YAML: if a "dynamic" property is present, throws with guidance.
403
+ * For JS/TS: default export is loaded; "dynamic" is allowed.
404
+ */
405
+ const loadConfigFile = async (filePath) => {
406
+ let raw = {};
407
+ try {
408
+ const abs = path.resolve(filePath);
409
+ if (isJsOrTs(abs)) {
410
+ // JS/TS support: load default export via robust pipeline.
411
+ const mod = await loadJsTsDefault(abs);
412
+ raw = mod ?? {};
413
+ }
414
+ else {
415
+ const txt = await fs.readFile(abs, 'utf-8');
416
+ raw = isJson(abs) ? JSON.parse(txt) : isYaml(abs) ? YAML.parse(txt) : {};
417
+ }
418
+ }
419
+ catch (err) {
420
+ throw new Error(`Failed to read/parse config: ${filePath}. ${String(err)}`);
421
+ }
422
+ // Validate RAW
423
+ const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
424
+ if (!parsed.success) {
425
+ const msgs = parsed.error.issues
426
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
427
+ .join('\n');
428
+ throw new Error(`Invalid config ${filePath}:\n${msgs}`);
429
+ }
430
+ // Disallow dynamic in JSON/YAML; allow in JS/TS
431
+ if (!isJsOrTs(filePath) && parsed.data.dynamic !== undefined) {
432
+ throw new Error(`Config ${filePath} specifies "dynamic"; JSON/YAML configs cannot include dynamic in this step. Use JS/TS config.`);
433
+ }
434
+ return getDotenvConfigSchemaResolved.parse(parsed.data);
435
+ };
436
+ /**
437
+ * Discover and load configs into resolved shapes, ordered by scope/privacy.
438
+ * JSON/YAML/JS/TS supported; first match per scope/privacy applies.
439
+ */
440
+ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
441
+ const discovered = await discoverConfigFiles(importMetaUrl);
442
+ const result = {};
443
+ for (const f of discovered) {
444
+ const cfg = await loadConfigFile(f.path);
445
+ if (f.scope === 'packaged') {
446
+ // packaged public only
447
+ result.packaged = cfg;
448
+ }
449
+ else {
450
+ result.project ??= {};
451
+ if (f.privacy === 'public')
452
+ result.project.public = cfg;
453
+ else
454
+ result.project.local = cfg;
455
+ }
456
+ }
457
+ return result;
458
+ };
459
+
460
+ /**
461
+ * Dotenv expansion utilities.
462
+ *
463
+ * This module implements recursive expansion of environment-variable
464
+ * references in strings and records. It supports both whitespace and
465
+ * bracket syntaxes with optional defaults:
466
+ *
467
+ * - Whitespace: `$VAR[:default]`
468
+ * - Bracketed: `${VAR[:default]}`
469
+ *
470
+ * Escaped dollar signs (`\$`) are preserved.
471
+ * Unknown variables resolve to empty string unless a default is provided.
472
+ */
473
+ /**
474
+ * Like String.prototype.search but returns the last index.
475
+ * @internal
476
+ */
477
+ const searchLast = (str, rgx) => {
478
+ const matches = Array.from(str.matchAll(rgx));
479
+ return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
480
+ };
481
+ const replaceMatch = (value, match, ref) => {
482
+ /**
483
+ * @internal
484
+ */
485
+ const group = match[0];
486
+ const key = match[1];
487
+ const defaultValue = match[2];
488
+ if (!key)
489
+ return value;
490
+ const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
491
+ return interpolate(replacement, ref);
492
+ };
493
+ const interpolate = (value = '', ref = {}) => {
494
+ /**
495
+ * @internal
496
+ */
497
+ // if value is falsy, return it as is
498
+ if (!value)
499
+ return value;
500
+ // get position of last unescaped dollar sign
501
+ const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
502
+ // return value if none found
503
+ if (lastUnescapedDollarSignIndex === -1)
504
+ return value;
505
+ // evaluate the value tail
506
+ const tail = value.slice(lastUnescapedDollarSignIndex);
507
+ // find whitespace pattern: $KEY:DEFAULT
508
+ const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
509
+ const whitespaceMatch = whitespacePattern.exec(tail);
510
+ if (whitespaceMatch != null)
511
+ return replaceMatch(value, whitespaceMatch, ref);
512
+ else {
513
+ // find bracket pattern: ${KEY:DEFAULT}
514
+ const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
515
+ const bracketMatch = bracketPattern.exec(tail);
516
+ if (bracketMatch != null)
517
+ return replaceMatch(value, bracketMatch, ref);
518
+ }
519
+ return value;
520
+ };
521
+ /**
522
+ * Recursively expands environment variables in a string. Variables may be
523
+ * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
524
+ * Unknown variables will expand to an empty string.
525
+ *
526
+ * @param value - The string to expand.
527
+ * @param ref - The reference object to use for variable expansion.
528
+ * @returns The expanded string.
529
+ *
530
+ * @example
531
+ * ```ts
532
+ * process.env.FOO = 'bar';
533
+ * dotenvExpand('Hello $FOO'); // "Hello bar"
534
+ * dotenvExpand('Hello $BAZ:world'); // "Hello world"
535
+ * ```
536
+ *
537
+ * @remarks
538
+ * The expansion is recursive. If a referenced variable itself contains
539
+ * references, those will also be expanded until a stable value is reached.
540
+ * Escaped references (e.g. `\$FOO`) are preserved as literals.
541
+ */
542
+ const dotenvExpand = (value, ref = process.env) => {
543
+ const result = interpolate(value, ref);
544
+ return result ? result.replace(/\\\$/g, '$') : undefined;
545
+ };
546
+ /**
547
+ * Recursively expands environment variables in the values of a JSON object.
548
+ * Variables may be presented with optional default as `$VAR[:default]` or
549
+ * `${VAR[:default]}`. Unknown variables will expand to an empty string.
550
+ *
551
+ * @param values - The values object to expand.
552
+ * @param options - Expansion options.
553
+ * @returns The value object with expanded string values.
554
+ *
555
+ * @example
556
+ * ```ts
557
+ * process.env.FOO = 'bar';
558
+ * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
559
+ * // => { A: "bar", B: "xbary" }
560
+ * ```
561
+ *
562
+ * @remarks
563
+ * Options:
564
+ * - ref: The reference object to use for expansion (defaults to process.env).
565
+ * - progressive: Whether to progressively add expanded values to the set of
566
+ * reference keys.
567
+ *
568
+ * When `progressive` is true, each expanded key becomes available for
569
+ * subsequent expansions in the same object (left-to-right by object key order).
570
+ */
571
+ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
572
+ const { ref = process.env, progressive = false } = options;
573
+ acc[key] = dotenvExpand(values[key], {
574
+ ...ref,
575
+ ...(progressive ? acc : {}),
576
+ });
577
+ return acc;
578
+ }, {});
579
+
580
+ const applyKv = (current, kv) => {
581
+ if (!kv || Object.keys(kv).length === 0)
582
+ return current;
583
+ const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
584
+ return { ...current, ...expanded };
585
+ };
586
+ const applyConfigSlice = (current, cfg, env) => {
587
+ if (!cfg)
588
+ return current;
589
+ // kind axis: global then env (env overrides global)
590
+ const afterGlobal = applyKv(current, cfg.vars);
591
+ const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
592
+ return applyKv(afterGlobal, envKv);
593
+ };
594
+ /**
595
+ * Overlay config-provided values onto a base ProcessEnv using precedence axes:
596
+ * - kind: env \> global
597
+ * - privacy: local \> public
598
+ * - source: project \> packaged \> base
599
+ *
600
+ * Programmatic explicit vars (if provided) override all config slices.
601
+ * Progressive expansion is applied within each slice.
602
+ */
603
+ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
604
+ let current = { ...base };
605
+ // Source: packaged (public -> local)
606
+ current = applyConfigSlice(current, configs.packaged, env);
607
+ // Packaged "local" is not expected by policy; if present, honor it.
608
+ // We do not have a separate object for packaged.local in sources, keep as-is.
609
+ // Source: project (public -> local)
610
+ current = applyConfigSlice(current, configs.project?.public, env);
611
+ current = applyConfigSlice(current, configs.project?.local, env);
612
+ // Programmatic explicit vars (top of static tier)
613
+ if (programmaticVars) {
614
+ const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
615
+ current = applyKv(current, toApply);
616
+ }
617
+ return current;
618
+ };
619
+
620
+ /**
621
+ * Asynchronously read a dotenv file & parse it into an object.
622
+ *
623
+ * @param path - Path to dotenv file.
624
+ * @returns The parsed dotenv object.
625
+ */
626
+ const readDotenv = async (path) => {
627
+ try {
628
+ return (await fs.exists(path)) ? parse(await fs.readFile(path)) : {};
629
+ }
630
+ catch {
631
+ return {};
632
+ }
633
+ };
634
+
635
+ const importDefault = async (fileUrl) => {
636
+ const mod = (await import(fileUrl));
637
+ return mod.default;
638
+ };
639
+ const cacheHash = (absPath, mtimeMs) => createHash('sha1')
640
+ .update(absPath)
641
+ .update(String(mtimeMs))
642
+ .digest('hex')
643
+ .slice(0, 12);
644
+ /**
645
+ * Remove older compiled cache files for a given source base name, keeping
646
+ * at most `keep` most-recent files. Errors are ignored by design.
647
+ */
648
+ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
649
+ try {
650
+ const entries = await fs.readdir(cacheDir);
651
+ const mine = entries
652
+ .filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
653
+ .map((f) => path.join(cacheDir, f));
654
+ if (mine.length <= keep)
655
+ return;
656
+ const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
657
+ stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
658
+ const toDelete = stats.slice(keep).map((s) => s.p);
659
+ await Promise.all(toDelete.map(async (p) => {
660
+ try {
661
+ await fs.remove(p);
662
+ }
663
+ catch {
664
+ // best-effort cleanup
665
+ }
666
+ }));
667
+ }
668
+ catch {
669
+ // best-effort cleanup
670
+ }
671
+ };
672
+ /**
673
+ * Load a module default export from a JS/TS file with robust fallbacks:
674
+ * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
675
+ * 1) try direct import (if a TS loader is active),
676
+ * 2) esbuild bundle to a temp ESM file,
677
+ * 3) typescript.transpileModule fallback for simple modules.
678
+ *
679
+ * @param absPath - absolute path to source file
680
+ * @param cacheDirName - cache subfolder under .tsbuild
681
+ */
682
+ const loadModuleDefault = async (absPath, cacheDirName) => {
683
+ const ext = path.extname(absPath).toLowerCase();
684
+ const fileUrl = url.pathToFileURL(absPath).toString();
685
+ if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
686
+ return importDefault(fileUrl);
687
+ }
688
+ // Try direct import first (TS loader active)
689
+ try {
690
+ const dyn = await importDefault(fileUrl);
691
+ if (dyn)
692
+ return dyn;
693
+ }
694
+ catch {
695
+ /* fall through */
696
+ }
697
+ const stat = await fs.stat(absPath);
698
+ const hash = cacheHash(absPath, stat.mtimeMs);
699
+ const cacheDir = path.resolve('.tsbuild', cacheDirName);
700
+ await fs.ensureDir(cacheDir);
701
+ const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
702
+ // Try esbuild
703
+ try {
704
+ const esbuild = (await import('esbuild'));
705
+ await esbuild.build({
706
+ entryPoints: [absPath],
707
+ bundle: true,
708
+ platform: 'node',
709
+ format: 'esm',
710
+ target: 'node20',
711
+ outfile: cacheFile,
712
+ sourcemap: false,
713
+ logLevel: 'silent',
714
+ });
715
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
716
+ // Best-effort: trim older cache files for this source.
717
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
718
+ return result;
719
+ }
720
+ catch {
721
+ /* fall through to TS transpile */
722
+ }
723
+ // TypeScript transpile fallback
724
+ try {
725
+ const ts = (await import('typescript'));
726
+ const code = await fs.readFile(absPath, 'utf-8');
727
+ const out = ts.transpileModule(code, {
728
+ compilerOptions: {
729
+ module: 'ESNext',
730
+ target: 'ES2022',
731
+ moduleResolution: 'NodeNext',
732
+ },
733
+ }).outputText;
734
+ await fs.writeFile(cacheFile, out, 'utf-8');
735
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
736
+ // Best-effort: trim older cache files for this source.
737
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
738
+ return result;
739
+ }
740
+ catch {
741
+ // Caller decides final error wording; rethrow for upstream mapping.
742
+ throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
743
+ }
744
+ };
745
+
746
+ /**
747
+ * Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
748
+ *
749
+ * @param options - `GetDotenvOptions` object
750
+ * @returns The combined parsed dotenv object.
751
+ * * @example Load from the project root with default tokens
752
+ * ```ts
753
+ * const vars = await getDotenv();
754
+ * console.log(vars.MY_SETTING);
755
+ * ```
756
+ *
757
+ * @example Load from multiple paths and a specific environment
758
+ * ```ts
759
+ * const vars = await getDotenv({
760
+ * env: 'dev',
761
+ * dotenvToken: '.testenv',
762
+ * privateToken: 'secret',
763
+ * paths: ['./', './packages/app'],
764
+ * });
765
+ * ```
766
+ *
767
+ * @example Use dynamic variables
768
+ * ```ts
769
+ * // .env.js default-exports: { DYNAMIC: ({ PREV }) => `${PREV}-suffix` }
770
+ * const vars = await getDotenv({ dynamicPath: '.env.js' });
771
+ * ```
772
+ *
773
+ * @remarks
774
+ * - When {@link GetDotenvOptions.loadProcess} is true, the resulting variables are merged
775
+ * into `process.env` as a side effect.
776
+ * - When {@link GetDotenvOptions.outputPath} is provided, a consolidated dotenv file is written.
777
+ * The path is resolved after expansion, so it may reference previously loaded vars.
778
+ *
779
+ * @throws Error when a dynamic module is present but cannot be imported.
780
+ * @throws Error when an output path was requested but could not be resolved.
781
+ */
782
+ const getDotenv = async (options = {}) => {
783
+ // Apply defaults.
784
+ 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);
785
+ // Read .env files.
786
+ const loaded = paths.length
787
+ ? await paths.reduce(async (e, p) => {
788
+ const publicGlobal = excludePublic || excludeGlobal
789
+ ? Promise.resolve({})
790
+ : readDotenv(path.resolve(p, dotenvToken));
791
+ const publicEnv = excludePublic || excludeEnv || (!env && !defaultEnv)
792
+ ? Promise.resolve({})
793
+ : readDotenv(path.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}`));
794
+ const privateGlobal = excludePrivate || excludeGlobal
795
+ ? Promise.resolve({})
796
+ : readDotenv(path.resolve(p, `${dotenvToken}.${privateToken}`));
797
+ const privateEnv = excludePrivate || excludeEnv || (!env && !defaultEnv)
798
+ ? Promise.resolve({})
799
+ : readDotenv(path.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}.${privateToken}`));
800
+ const [eResolved, publicGlobalResolved, publicEnvResolved, privateGlobalResolved, privateEnvResolved,] = await Promise.all([
801
+ e,
802
+ publicGlobal,
803
+ publicEnv,
804
+ privateGlobal,
805
+ privateEnv,
806
+ ]);
807
+ return {
808
+ ...eResolved,
809
+ ...publicGlobalResolved,
810
+ ...publicEnvResolved,
811
+ ...privateGlobalResolved,
812
+ ...privateEnvResolved,
813
+ };
814
+ }, Promise.resolve({}))
815
+ : {};
816
+ const outputKey = nanoid();
817
+ const dotenv = dotenvExpandAll({
818
+ ...loaded,
819
+ ...vars,
820
+ ...(outputPath ? { [outputKey]: outputPath } : {}),
821
+ }, { progressive: true });
822
+ // Process dynamic variables. Programmatic option takes precedence over path.
823
+ if (!excludeDynamic) {
824
+ let dynamic = undefined;
825
+ if (options.dynamic && Object.keys(options.dynamic).length > 0) {
826
+ dynamic = options.dynamic;
827
+ }
828
+ else if (dynamicPath) {
829
+ const absDynamicPath = path.resolve(dynamicPath);
830
+ if (await fs.exists(absDynamicPath)) {
831
+ try {
832
+ dynamic = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic');
833
+ }
834
+ catch {
835
+ // Preserve legacy error text for compatibility with tests/docs.
836
+ throw new Error(`Unable to load dynamic TypeScript file: ${absDynamicPath}. ` +
837
+ `Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
838
+ }
839
+ }
840
+ }
841
+ if (dynamic) {
842
+ try {
843
+ for (const key in dynamic)
844
+ Object.assign(dotenv, {
845
+ [key]: typeof dynamic[key] === 'function'
846
+ ? dynamic[key](dotenv, env ?? defaultEnv)
847
+ : dynamic[key],
848
+ });
849
+ }
850
+ catch {
851
+ throw new Error(`Unable to evaluate dynamic variables.`);
852
+ }
853
+ }
854
+ }
855
+ // Write output file.
856
+ let resultDotenv = dotenv;
857
+ if (outputPath) {
858
+ const outputPathResolved = dotenv[outputKey];
859
+ if (!outputPathResolved)
860
+ throw new Error('Output path not found.');
861
+ const { [outputKey]: _omitted, ...dotenvForOutput } = dotenv;
862
+ await fs.writeFile(outputPathResolved, Object.keys(dotenvForOutput).reduce((contents, key) => {
863
+ const value = dotenvForOutput[key] ?? '';
864
+ return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
865
+ }, ''), { encoding: 'utf-8' });
866
+ resultDotenv = dotenvForOutput;
867
+ }
868
+ // Log result.
869
+ if (log)
870
+ logger.log(resultDotenv);
871
+ // Load process.env.
872
+ if (loadProcess)
873
+ Object.assign(process.env, resultDotenv);
874
+ return resultDotenv;
875
+ };
876
+
877
+ /**
878
+ * Compute the dotenv context for the host (uses the config loader/overlay path).
879
+ * - Resolves and validates options strictly (host-only).
880
+ * - Applies file cascade, overlays, dynamics, and optional effects.
881
+ * - Merges and validates per-plugin config slices (when provided).
882
+ *
883
+ * @param customOptions - Partial options from the current invocation.
884
+ * @param plugins - Installed plugins (for config validation).
885
+ * @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery). */
886
+ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
887
+ const optionsResolved = await resolveGetDotenvOptions(customOptions);
888
+ const validated = getDotenvOptionsSchemaResolved.parse(optionsResolved);
889
+ // Always-on loader path
890
+ // 1) Base from files only (no dynamic, no programmatic vars)
891
+ const base = await getDotenv({
892
+ ...validated,
893
+ // Build a pure base without side effects or logging.
894
+ excludeDynamic: true,
895
+ vars: {},
896
+ log: false,
897
+ loadProcess: false,
898
+ outputPath: undefined,
899
+ });
900
+ // 2) Discover config sources and overlay
901
+ const sources = await resolveGetDotenvConfigSources(hostMetaUrl);
902
+ const dotenvOverlaid = overlayEnv({
903
+ base,
904
+ env: validated.env ?? validated.defaultEnv,
905
+ configs: sources,
906
+ ...(validated.vars ? { programmaticVars: validated.vars } : {}),
907
+ });
908
+ // Helper to apply a dynamic map progressively.
909
+ const applyDynamic = (target, dynamic, env) => {
910
+ if (!dynamic)
911
+ return;
912
+ for (const key of Object.keys(dynamic)) {
913
+ const value = typeof dynamic[key] === 'function'
914
+ ? dynamic[key](target, env)
915
+ : dynamic[key];
916
+ Object.assign(target, { [key]: value });
917
+ }
918
+ };
919
+ // 3) Apply dynamics in order
920
+ const dotenv = { ...dotenvOverlaid };
921
+ applyDynamic(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
922
+ applyDynamic(dotenv, (sources.packaged?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
923
+ applyDynamic(dotenv, (sources.project?.public?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
924
+ applyDynamic(dotenv, (sources.project?.local?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
925
+ // file dynamicPath (lowest)
926
+ if (validated.dynamicPath) {
927
+ const absDynamicPath = path.resolve(validated.dynamicPath);
928
+ try {
929
+ const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
930
+ applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
931
+ }
932
+ catch {
933
+ throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
934
+ }
935
+ }
936
+ // 4) Output/log/process merge
937
+ if (validated.outputPath) {
938
+ await fs.writeFile(validated.outputPath, Object.keys(dotenv).reduce((contents, key) => {
939
+ const value = dotenv[key] ?? '';
940
+ return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
941
+ }, ''), { encoding: 'utf-8' });
942
+ }
943
+ const logger = validated.logger ?? console;
944
+ if (validated.log)
945
+ logger.log(dotenv);
946
+ if (validated.loadProcess)
947
+ Object.assign(process.env, dotenv);
948
+ // 5) Merge and validate per-plugin config (packaged < project.public < project.local)
949
+ const packagedPlugins = (sources.packaged &&
950
+ sources.packaged.plugins) ??
951
+ {};
952
+ const publicPlugins = (sources.project?.public &&
953
+ sources.project.public.plugins) ??
954
+ {};
955
+ const localPlugins = (sources.project?.local &&
956
+ sources.project.local.plugins) ??
957
+ {};
958
+ const mergedPluginConfigs = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
959
+ for (const p of plugins) {
960
+ if (!p.id || !p.configSchema)
961
+ continue;
962
+ const slice = mergedPluginConfigs[p.id];
963
+ if (slice === undefined)
964
+ continue;
965
+ const parsed = p.configSchema.safeParse(slice);
966
+ if (!parsed.success) {
967
+ const msgs = parsed.error.issues
968
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
969
+ .join('\n');
970
+ throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
971
+ }
972
+ mergedPluginConfigs[p.id] = parsed.data;
973
+ }
974
+ return {
975
+ optionsResolved: validated,
976
+ dotenv: dotenv,
977
+ plugins: {},
978
+ pluginConfigs: mergedPluginConfigs,
979
+ };
980
+ };
981
+
982
+ const HOST_META_URL = import.meta.url;
983
+ const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
984
+ /**
985
+ * Plugin-first CLI host for get-dotenv. Extends Commander.Command.
986
+ *
987
+ * Responsibilities:
988
+ * - Resolve options strictly and compute dotenv context (resolveAndLoad).
989
+ * - Expose a stable accessor for the current context (getCtx).
990
+ * - Provide a namespacing helper (ns).
991
+ * - Support composable plugins with parent → children install and afterResolve.
992
+ *
993
+ * NOTE: This host is additive and does not alter the legacy CLI.
994
+ */
995
+ class GetDotenvCli extends Command {
996
+ /** Registered top-level plugins (composition happens via .use()) */
997
+ _plugins = [];
998
+ /** One-time installation guard */
999
+ _installed = false;
1000
+ constructor(alias = 'getdotenv') {
1001
+ super(alias);
1002
+ // Ensure subcommands that use passThroughOptions can be attached safely.
1003
+ // Commander requires parent commands to enable positional options when a
1004
+ // child uses passThroughOptions.
1005
+ this.enablePositionalOptions();
1006
+ // Skeleton preSubcommand hook: produce a context if absent, without
1007
+ // mutating process.env. The passOptions hook (when installed) will
1008
+ // compute the final context using merged CLI options; keeping
1009
+ // loadProcess=false here avoids leaking dotenv values into the parent
1010
+ // process env before subcommands execute.
1011
+ this.hook('preSubcommand', async () => {
1012
+ if (this.getCtx())
1013
+ return;
1014
+ await this.resolveAndLoad({ loadProcess: false });
1015
+ });
1016
+ }
1017
+ /**
1018
+ * Resolve options (strict) and compute dotenv context. * Stores the context on the instance under a symbol.
1019
+ */
1020
+ async resolveAndLoad(customOptions = {}) {
1021
+ // Resolve defaults, then validate strictly under the new host.
1022
+ const optionsResolved = await resolveGetDotenvOptions(customOptions);
1023
+ getDotenvOptionsSchemaResolved.parse(optionsResolved);
1024
+ // Delegate the heavy lifting to the shared helper (guarded path supported).
1025
+ const ctx = await computeContext(optionsResolved, this._plugins, HOST_META_URL);
1026
+ // Persist context on the instance for later access.
1027
+ this[CTX_SYMBOL] =
1028
+ ctx;
1029
+ // Ensure plugins are installed exactly once, then run afterResolve.
1030
+ await this.install();
1031
+ await this._runAfterResolve(ctx);
1032
+ return ctx;
1033
+ }
1034
+ /**
1035
+ * Retrieve the current invocation context (if any).
1036
+ */
1037
+ getCtx() {
1038
+ return this[CTX_SYMBOL];
1039
+ }
1040
+ /** * Convenience helper to create a namespaced subcommand.
1041
+ */
1042
+ ns(name) {
1043
+ return this.command(name);
1044
+ }
1045
+ /**
1046
+ * Register a plugin for installation (parent level).
1047
+ * Installation occurs on first resolveAndLoad() (or explicit install()).
1048
+ */
1049
+ use(plugin) {
1050
+ this._plugins.push(plugin);
1051
+ // Immediately run setup so subcommands exist before parsing.
1052
+ const setupOne = (p) => {
1053
+ p.setup(this);
1054
+ for (const child of p.children)
1055
+ setupOne(child);
1056
+ };
1057
+ setupOne(plugin);
1058
+ return this;
1059
+ }
1060
+ /**
1061
+ * Install all registered plugins in parent → children (pre-order).
1062
+ * Runs only once per CLI instance.
1063
+ */
1064
+ async install() {
1065
+ // Setup is performed immediately in use(); here we only guard for afterResolve.
1066
+ this._installed = true;
1067
+ // Satisfy require-await without altering behavior.
1068
+ await Promise.resolve();
1069
+ }
1070
+ /**
1071
+ * Run afterResolve hooks for all plugins (parent → children).
1072
+ */
1073
+ async _runAfterResolve(ctx) {
1074
+ const run = async (p) => {
1075
+ if (p.afterResolve)
1076
+ await p.afterResolve(this, ctx);
1077
+ for (const child of p.children)
1078
+ await run(child);
1079
+ };
1080
+ for (const p of this._plugins)
1081
+ await run(p);
1082
+ }
1083
+ }
1084
+
1085
+ export { GetDotenvCli, definePlugin };