@karmaniverous/get-dotenv 5.2.5 → 6.0.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.
- package/README.md +63 -67
- package/dist/cliHost.cjs +765 -549
- package/dist/cliHost.d.cts +128 -84
- package/dist/cliHost.d.mts +128 -84
- package/dist/cliHost.d.ts +128 -84
- package/dist/cliHost.mjs +765 -549
- package/dist/getdotenv.cli.mjs +915 -685
- package/dist/index.cjs +959 -1006
- package/dist/index.d.cts +18 -178
- package/dist/index.d.mts +18 -178
- package/dist/index.d.ts +18 -178
- package/dist/index.mjs +960 -1006
- package/dist/plugins-aws.cjs +0 -1
- package/dist/plugins-aws.d.cts +8 -78
- package/dist/plugins-aws.d.mts +8 -78
- package/dist/plugins-aws.d.ts +8 -78
- package/dist/plugins-aws.mjs +0 -1
- package/dist/plugins-batch.cjs +53 -11
- package/dist/plugins-batch.d.cts +10 -79
- package/dist/plugins-batch.d.mts +10 -79
- package/dist/plugins-batch.d.ts +10 -79
- package/dist/plugins-batch.mjs +53 -11
- package/dist/plugins-cmd.cjs +162 -1555
- package/dist/plugins-cmd.d.cts +8 -78
- package/dist/plugins-cmd.d.mts +8 -78
- package/dist/plugins-cmd.d.ts +8 -78
- package/dist/plugins-cmd.mjs +162 -1554
- package/dist/plugins-demo.cjs +52 -7
- package/dist/plugins-demo.d.cts +8 -78
- package/dist/plugins-demo.d.mts +8 -78
- package/dist/plugins-demo.d.ts +8 -78
- package/dist/plugins-demo.mjs +52 -7
- package/dist/plugins-init.d.cts +8 -78
- package/dist/plugins-init.d.mts +8 -78
- package/dist/plugins-init.d.ts +8 -78
- package/dist/plugins.cjs +283 -1630
- package/dist/plugins.d.cts +10 -79
- package/dist/plugins.d.mts +10 -79
- package/dist/plugins.d.ts +10 -79
- package/dist/plugins.mjs +285 -1631
- package/package.json +4 -2
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Command, Option } from 'commander';
|
|
2
1
|
import fs from 'fs-extra';
|
|
3
2
|
import { packageDirectory } from 'package-directory';
|
|
4
|
-
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
5
3
|
import path, { join, extname } from 'path';
|
|
6
|
-
import {
|
|
4
|
+
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
7
5
|
import YAML from 'yaml';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { Option, Command } from 'commander';
|
|
8
8
|
import { nanoid } from 'nanoid';
|
|
9
9
|
import { parse } from 'dotenv';
|
|
10
10
|
import { createHash } from 'crypto';
|
|
@@ -40,8 +40,6 @@ const baseRootOptionDefaults = {
|
|
|
40
40
|
// (debug/log/exclude* resolved via flag utils)
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
44
|
-
|
|
45
43
|
/** @internal */
|
|
46
44
|
const isPlainObject$1 = (value) => value !== null &&
|
|
47
45
|
typeof value === 'object' &&
|
|
@@ -84,141 +82,130 @@ const defaultsDeep = (...layers) => {
|
|
|
84
82
|
return result;
|
|
85
83
|
};
|
|
86
84
|
|
|
87
|
-
// src/GetDotenvOptions.ts
|
|
88
|
-
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
89
85
|
/**
|
|
90
|
-
*
|
|
86
|
+
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
87
|
+
* - If the user explicitly enabled the flag, return true.
|
|
88
|
+
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
89
|
+
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
90
|
+
*
|
|
91
|
+
* @param exclude - The "on" flag value as parsed by Commander.
|
|
92
|
+
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
93
|
+
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
94
|
+
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
91
95
|
*
|
|
92
96
|
* @example
|
|
93
|
-
*
|
|
97
|
+
* ```ts
|
|
98
|
+
* resolveExclusion(undefined, undefined, true); // => true
|
|
99
|
+
* ```
|
|
94
100
|
*/
|
|
95
|
-
const
|
|
101
|
+
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
96
102
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
103
|
+
* Resolve an optional flag with "--exclude-all" overrides.
|
|
104
|
+
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
105
|
+
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
106
|
+
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
99
107
|
*
|
|
100
|
-
* @
|
|
108
|
+
* @param exclude - Individual include/exclude flag.
|
|
109
|
+
* @param excludeOff - Individual "...-off" flag.
|
|
110
|
+
* @param defaultValue - Default for the individual flag.
|
|
111
|
+
* @param excludeAll - Global "exclude-all" flag.
|
|
112
|
+
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
|
|
101
116
|
*/
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
...restObj,
|
|
147
|
-
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
148
|
-
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
149
|
-
};
|
|
150
|
-
};
|
|
151
|
-
const resolveGetDotenvOptions = async (customOptions) => {
|
|
152
|
-
/**
|
|
153
|
-
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
154
|
-
*
|
|
155
|
-
* 1. Base defaults derived from the CLI generator defaults
|
|
156
|
-
* ({@link baseGetDotenvCliOptions}).
|
|
157
|
-
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
158
|
-
* package root (if present).
|
|
159
|
-
* 3. The provided {@link customOptions}.
|
|
160
|
-
*
|
|
161
|
-
* The result preserves explicit empty values and drops only `undefined`.
|
|
162
|
-
*
|
|
163
|
-
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* ```ts
|
|
167
|
-
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
168
|
-
* ```
|
|
169
|
-
*/
|
|
170
|
-
const localPkgDir = await packageDirectory();
|
|
171
|
-
const localOptionsPath = localPkgDir
|
|
172
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
173
|
-
: undefined;
|
|
174
|
-
const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
|
|
175
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
176
|
-
: {});
|
|
177
|
-
// Merge order: base < local < custom (custom has highest precedence)
|
|
178
|
-
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
179
|
-
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
180
|
-
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
181
|
-
return {
|
|
182
|
-
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
183
|
-
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
184
|
-
};
|
|
117
|
+
const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
|
|
118
|
+
// Order of precedence:
|
|
119
|
+
// 1) Individual explicit "on" wins outright.
|
|
120
|
+
// 2) Individual explicit "off" wins over any global.
|
|
121
|
+
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
122
|
+
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
123
|
+
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
124
|
+
(() => {
|
|
125
|
+
// Individual "on"
|
|
126
|
+
if (exclude === true)
|
|
127
|
+
return true;
|
|
128
|
+
// Individual "off"
|
|
129
|
+
if (excludeOff === true)
|
|
130
|
+
return undefined;
|
|
131
|
+
// Global "exclude-all" ON (unless explicitly turned off)
|
|
132
|
+
if (excludeAll === true)
|
|
133
|
+
return true;
|
|
134
|
+
// Global "exclude-all-off" (unless explicitly enabled)
|
|
135
|
+
if (excludeAllOff === true)
|
|
136
|
+
return undefined;
|
|
137
|
+
// Default
|
|
138
|
+
return defaultValue ? true : undefined;
|
|
139
|
+
})();
|
|
140
|
+
/**
|
|
141
|
+
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
142
|
+
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
143
|
+
*
|
|
144
|
+
* @typeParam T - Target object type.
|
|
145
|
+
* @param obj - The object to write to.
|
|
146
|
+
* @param key - The optional boolean property key of {@link T}.
|
|
147
|
+
* @param value - The value to set or `undefined` to unset.
|
|
148
|
+
*
|
|
149
|
+
* @remarks
|
|
150
|
+
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
151
|
+
*/
|
|
152
|
+
const setOptionalFlag = (obj, key, value) => {
|
|
153
|
+
const target = obj;
|
|
154
|
+
const k = key;
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
156
|
+
if (value === undefined)
|
|
157
|
+
delete target[k];
|
|
158
|
+
else
|
|
159
|
+
target[k] = value;
|
|
185
160
|
};
|
|
186
161
|
|
|
187
162
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
192
|
-
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
193
|
-
* later per the staged plan.
|
|
163
|
+
* Merge and normalize raw Commander options (current + parent + defaults)
|
|
164
|
+
* into a GetDotenvCliOptions-like object. Types are intentionally wide to
|
|
165
|
+
* avoid cross-layer coupling; callers may cast as needed.
|
|
194
166
|
*/
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
167
|
+
const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
168
|
+
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
169
|
+
? JSON.parse(parentJson)
|
|
170
|
+
: undefined;
|
|
171
|
+
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
172
|
+
const current = { ...rest };
|
|
173
|
+
if (typeof scripts === 'string') {
|
|
174
|
+
try {
|
|
175
|
+
current.scripts = JSON.parse(scripts);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// ignore parse errors; leave scripts undefined
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
182
|
+
const d = defaults;
|
|
183
|
+
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
184
|
+
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
185
|
+
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
186
|
+
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
187
|
+
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
188
|
+
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
189
|
+
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
190
|
+
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
191
|
+
// warnEntropy (tri-state)
|
|
192
|
+
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
193
|
+
// Normalize shell for predictability: explicit default shell per OS.
|
|
194
|
+
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
195
|
+
let resolvedShell = merged.shell;
|
|
196
|
+
if (shellOff)
|
|
197
|
+
resolvedShell = false;
|
|
198
|
+
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
199
|
+
resolvedShell = defaultShell;
|
|
200
|
+
}
|
|
201
|
+
else if (typeof resolvedShell !== 'string' &&
|
|
202
|
+
typeof defaults.shell === 'string') {
|
|
203
|
+
resolvedShell = defaults.shell;
|
|
204
|
+
}
|
|
205
|
+
merged.shell = resolvedShell;
|
|
206
|
+
const cmd = typeof command === 'string' ? command : undefined;
|
|
207
|
+
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
208
|
+
};
|
|
222
209
|
|
|
223
210
|
/**
|
|
224
211
|
* Zod schemas for configuration files discovered by the new loader.
|
|
@@ -458,20 +445,190 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
|
458
445
|
};
|
|
459
446
|
|
|
460
447
|
/**
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
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]}`
|
|
448
|
+
* Validate a composed env against config-provided validation surfaces.
|
|
449
|
+
* Precedence for validation definitions:
|
|
450
|
+
* project.local -\> project.public -\> packaged
|
|
469
451
|
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
452
|
+
* Behavior:
|
|
453
|
+
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
454
|
+
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
455
|
+
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
456
|
+
*/
|
|
457
|
+
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
458
|
+
const pick = (getter) => {
|
|
459
|
+
const pl = sources.project?.local;
|
|
460
|
+
const pp = sources.project?.public;
|
|
461
|
+
const pk = sources.packaged;
|
|
462
|
+
return ((pl && getter(pl)) ||
|
|
463
|
+
(pp && getter(pp)) ||
|
|
464
|
+
(pk && getter(pk)) ||
|
|
465
|
+
undefined);
|
|
466
|
+
};
|
|
467
|
+
const schema = pick((cfg) => cfg['schema']);
|
|
468
|
+
if (schema &&
|
|
469
|
+
typeof schema.safeParse === 'function') {
|
|
470
|
+
try {
|
|
471
|
+
const parsed = schema.safeParse(finalEnv);
|
|
472
|
+
if (!parsed.success) {
|
|
473
|
+
// Try to render zod-style issues when available.
|
|
474
|
+
const err = parsed.error;
|
|
475
|
+
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
476
|
+
? err.issues.map((i) => {
|
|
477
|
+
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
478
|
+
const msg = i.message ?? 'Invalid value';
|
|
479
|
+
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
480
|
+
})
|
|
481
|
+
: ['[schema] validation failed'];
|
|
482
|
+
return issues;
|
|
483
|
+
}
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// If schema invocation fails, surface a single diagnostic.
|
|
488
|
+
return [
|
|
489
|
+
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
490
|
+
];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
494
|
+
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
495
|
+
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
496
|
+
if (missing.length > 0) {
|
|
497
|
+
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return [];
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
504
|
+
|
|
505
|
+
// src/GetDotenvOptions.ts
|
|
506
|
+
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
507
|
+
/**
|
|
508
|
+
* Helper to define a dynamic map with strong inference.
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* const dynamic = defineDynamic(\{ KEY: (\{ FOO = '' \}) =\> FOO + '-x' \});
|
|
512
|
+
*/
|
|
513
|
+
const defineDynamic = (d) => d;
|
|
514
|
+
/**
|
|
515
|
+
* Converts programmatic CLI options to `getDotenv` options. *
|
|
516
|
+
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
517
|
+
*
|
|
518
|
+
* @returns `getDotenv` options.
|
|
519
|
+
*/
|
|
520
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
521
|
+
/**
|
|
522
|
+
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
523
|
+
*
|
|
524
|
+
* - 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`
|
|
525
|
+
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
526
|
+
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
527
|
+
*
|
|
528
|
+
* @remarks
|
|
529
|
+
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
530
|
+
*/
|
|
531
|
+
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
532
|
+
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
533
|
+
const restObj = { ...rest };
|
|
534
|
+
delete restObj.debug;
|
|
535
|
+
delete restObj.scripts;
|
|
536
|
+
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
537
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
538
|
+
let parsedVars;
|
|
539
|
+
if (typeof vars === 'string') {
|
|
540
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
541
|
+
? RegExp(varsAssignorPattern)
|
|
542
|
+
: (varsAssignor ?? '=')));
|
|
543
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
544
|
+
}
|
|
545
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
546
|
+
// Keep only string or undefined values to match ProcessEnv.
|
|
547
|
+
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
548
|
+
parsedVars = Object.fromEntries(entries);
|
|
549
|
+
}
|
|
550
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
551
|
+
// expectations and the compat test assertions.
|
|
552
|
+
if (parsedVars) {
|
|
553
|
+
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
554
|
+
}
|
|
555
|
+
// Tolerate paths as either a delimited string or string[]
|
|
556
|
+
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
557
|
+
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
558
|
+
const pathsAny = paths;
|
|
559
|
+
const pathsOut = Array.isArray(pathsAny)
|
|
560
|
+
? pathsAny.filter((p) => typeof p === 'string')
|
|
561
|
+
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
562
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
563
|
+
return {
|
|
564
|
+
...restObj,
|
|
565
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
566
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
const resolveGetDotenvOptions = async (customOptions) => {
|
|
570
|
+
/**
|
|
571
|
+
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
572
|
+
*
|
|
573
|
+
* 1. Base defaults derived from the CLI generator defaults
|
|
574
|
+
* ({@link baseGetDotenvCliOptions}).
|
|
575
|
+
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
576
|
+
* package root (if present).
|
|
577
|
+
* 3. The provided {@link customOptions}.
|
|
578
|
+
*
|
|
579
|
+
* The result preserves explicit empty values and drops only `undefined`.
|
|
580
|
+
*
|
|
581
|
+
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```ts
|
|
585
|
+
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
588
|
+
const localPkgDir = await packageDirectory();
|
|
589
|
+
const localOptionsPath = localPkgDir
|
|
590
|
+
? join(localPkgDir, getDotenvOptionsFilename)
|
|
591
|
+
: undefined;
|
|
592
|
+
// Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
|
|
593
|
+
let localOptions = {};
|
|
594
|
+
if (localOptionsPath && (await fs.exists(localOptionsPath))) {
|
|
595
|
+
try {
|
|
596
|
+
const txt = await fs.readFile(localOptionsPath, 'utf-8');
|
|
597
|
+
const parsed = JSON.parse(txt);
|
|
598
|
+
if (parsed && typeof parsed === 'object') {
|
|
599
|
+
localOptions = parsed;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// Malformed or unreadable local options are treated as absent.
|
|
604
|
+
localOptions = {};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Merge order: base < local < custom (custom has highest precedence)
|
|
608
|
+
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
609
|
+
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
610
|
+
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
611
|
+
return {
|
|
612
|
+
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
613
|
+
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
614
|
+
};
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Dotenv expansion utilities.
|
|
619
|
+
*
|
|
620
|
+
* This module implements recursive expansion of environment-variable
|
|
621
|
+
* references in strings and records. It supports both whitespace and
|
|
622
|
+
* bracket syntaxes with optional defaults:
|
|
623
|
+
*
|
|
624
|
+
* - Whitespace: `$VAR[:default]`
|
|
625
|
+
* - Bracketed: `${VAR[:default]}`
|
|
626
|
+
*
|
|
627
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
628
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
629
|
+
*/
|
|
630
|
+
/**
|
|
631
|
+
* Like String.prototype.search but returns the last index.
|
|
475
632
|
* @internal
|
|
476
633
|
*/
|
|
477
634
|
const searchLast = (str, rgx) => {
|
|
@@ -593,61 +750,271 @@ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduc
|
|
|
593
750
|
*/
|
|
594
751
|
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
595
752
|
|
|
596
|
-
|
|
597
|
-
if (!kv || Object.keys(kv).length === 0)
|
|
598
|
-
return current;
|
|
599
|
-
const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
|
|
600
|
-
return { ...current, ...expanded };
|
|
601
|
-
};
|
|
602
|
-
const applyConfigSlice = (current, cfg, env) => {
|
|
603
|
-
if (!cfg)
|
|
604
|
-
return current;
|
|
605
|
-
// kind axis: global then env (env overrides global)
|
|
606
|
-
const afterGlobal = applyKv(current, cfg.vars);
|
|
607
|
-
const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
|
|
608
|
-
return applyKv(afterGlobal, envKv);
|
|
609
|
-
};
|
|
753
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
610
754
|
/**
|
|
611
|
-
*
|
|
612
|
-
* -
|
|
613
|
-
* -
|
|
614
|
-
* - source: project \> packaged \> base
|
|
615
|
-
*
|
|
616
|
-
* Programmatic explicit vars (if provided) override all config slices.
|
|
617
|
-
* Progressive expansion is applied within each slice.
|
|
755
|
+
* Attach root flags to a GetDotenvCli instance.
|
|
756
|
+
* - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
|
|
757
|
+
* - Any flag that displays an effective default in help uses dynamic descriptions.
|
|
618
758
|
*/
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
759
|
+
const attachRootOptions = (program, defaults, opts) => {
|
|
760
|
+
// Install temporary wrappers to tag all options added here as "base" for grouped help.
|
|
761
|
+
const GROUP = 'base';
|
|
762
|
+
const tagLatest = (cmd, group) => {
|
|
763
|
+
const optsArr = cmd.options;
|
|
764
|
+
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
765
|
+
const last = optsArr[optsArr.length - 1];
|
|
766
|
+
last.__group = group;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
const originalAddOption = program.addOption.bind(program);
|
|
770
|
+
const originalOption = program.option.bind(program);
|
|
771
|
+
program.addOption = function patchedAdd(opt) {
|
|
772
|
+
opt.__group = GROUP;
|
|
773
|
+
return originalAddOption(opt);
|
|
774
|
+
};
|
|
775
|
+
program.option = function patchedOption(...args) {
|
|
776
|
+
const ret = originalOption(...args);
|
|
777
|
+
tagLatest(this, GROUP);
|
|
778
|
+
return ret;
|
|
779
|
+
};
|
|
780
|
+
const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
781
|
+
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
782
|
+
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
783
|
+
// Helper: append (default) tags for ON/OFF toggles
|
|
784
|
+
const onOff = (on, isDefault) => on
|
|
785
|
+
? `ON${isDefault ? ' (default)' : ''}`
|
|
786
|
+
: `OFF${isDefault ? ' (default)' : ''}`;
|
|
787
|
+
let p = program
|
|
788
|
+
.enablePositionalOptions()
|
|
789
|
+
.passThroughOptions()
|
|
790
|
+
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
791
|
+
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
792
|
+
['KEY1', 'VAL1'],
|
|
793
|
+
['KEY2', 'VAL2'],
|
|
794
|
+
]
|
|
795
|
+
.map((v) => v.join(va))
|
|
796
|
+
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
797
|
+
if (opts?.includeCommandOption === true) {
|
|
798
|
+
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
632
799
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
800
|
+
// Output path (interpolated later; help can remain static)
|
|
801
|
+
p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
|
|
802
|
+
// Shell ON (string or boolean true => default shell)
|
|
803
|
+
p = p
|
|
804
|
+
.addOption(program
|
|
805
|
+
.createDynamicOption('-s, --shell [string]', (cfg) => {
|
|
806
|
+
const s = cfg.shell;
|
|
807
|
+
let tag = '';
|
|
808
|
+
if (typeof s === 'boolean' && s)
|
|
809
|
+
tag = ' (default OS shell)';
|
|
810
|
+
else if (typeof s === 'string' && s.length > 0)
|
|
811
|
+
tag = ` (default ${s})`;
|
|
812
|
+
return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
|
|
813
|
+
})
|
|
814
|
+
.conflicts('shellOff'))
|
|
815
|
+
// Shell OFF
|
|
816
|
+
.addOption(program
|
|
817
|
+
.createDynamicOption('-S, --shell-off', (cfg) => {
|
|
818
|
+
const s = cfg.shell;
|
|
819
|
+
return `command execution shell OFF${s === false ? ' (default)' : ''}`;
|
|
820
|
+
})
|
|
821
|
+
.conflicts('shell'));
|
|
822
|
+
// Load process ON/OFF (dynamic defaults)
|
|
823
|
+
p = p
|
|
824
|
+
.addOption(program
|
|
825
|
+
.createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
|
|
826
|
+
.conflicts('loadProcessOff'))
|
|
827
|
+
.addOption(program
|
|
828
|
+
.createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
|
|
829
|
+
.conflicts('loadProcess'));
|
|
830
|
+
// Exclusion master toggle (dynamic)
|
|
831
|
+
p = p
|
|
832
|
+
.addOption(program
|
|
833
|
+
.createDynamicOption('-a, --exclude-all', (cfg) => {
|
|
834
|
+
const c = cfg;
|
|
835
|
+
const allOn = !!c.excludeDynamic &&
|
|
836
|
+
((!!c.excludeEnv && !!c.excludeGlobal) ||
|
|
837
|
+
(!!c.excludePrivate && !!c.excludePublic));
|
|
838
|
+
const suffix = allOn ? ' (default)' : '';
|
|
839
|
+
return `exclude all dotenv variables from loading ON${suffix}`;
|
|
840
|
+
})
|
|
841
|
+
.conflicts('excludeAllOff'))
|
|
842
|
+
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
|
|
843
|
+
// Per-family exclusions (dynamic defaults)
|
|
844
|
+
p = p
|
|
845
|
+
.addOption(program
|
|
846
|
+
.createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
|
|
847
|
+
.conflicts('excludeDynamicOff'))
|
|
848
|
+
.addOption(program
|
|
849
|
+
.createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
|
|
850
|
+
.conflicts('excludeDynamic'))
|
|
851
|
+
.addOption(program
|
|
852
|
+
.createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
|
|
853
|
+
.conflicts('excludeEnvOff'))
|
|
854
|
+
.addOption(program
|
|
855
|
+
.createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
|
|
856
|
+
.conflicts('excludeEnv'))
|
|
857
|
+
.addOption(program
|
|
858
|
+
.createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
|
|
859
|
+
.conflicts('excludeGlobalOff'))
|
|
860
|
+
.addOption(program
|
|
861
|
+
.createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
|
|
862
|
+
.conflicts('excludeGlobal'))
|
|
863
|
+
.addOption(program
|
|
864
|
+
.createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
|
|
865
|
+
.conflicts('excludePrivateOff'))
|
|
866
|
+
.addOption(program
|
|
867
|
+
.createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
|
|
868
|
+
.conflicts('excludePrivate'))
|
|
869
|
+
.addOption(program
|
|
870
|
+
.createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
|
|
871
|
+
.conflicts('excludePublicOff'))
|
|
872
|
+
.addOption(program
|
|
873
|
+
.createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
|
|
874
|
+
.conflicts('excludePublic'));
|
|
875
|
+
// Log ON/OFF (dynamic)
|
|
876
|
+
p = p
|
|
877
|
+
.addOption(program
|
|
878
|
+
.createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
|
|
879
|
+
.conflicts('logOff'))
|
|
880
|
+
.addOption(program
|
|
881
|
+
.createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
|
|
882
|
+
.conflicts('log'));
|
|
883
|
+
// Capture flag (no default display; static)
|
|
884
|
+
p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
|
|
885
|
+
// Core bootstrap/static flags (kept static in help)
|
|
886
|
+
p = p
|
|
887
|
+
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
888
|
+
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
889
|
+
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
890
|
+
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
891
|
+
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
892
|
+
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
893
|
+
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
894
|
+
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
895
|
+
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
896
|
+
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
897
|
+
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
898
|
+
// Hidden scripts pipe-through (stringified)
|
|
899
|
+
.addOption(new Option('--scripts <string>')
|
|
900
|
+
.default(JSON.stringify(scripts))
|
|
901
|
+
.hideHelp());
|
|
902
|
+
// Diagnostics / validation / entropy
|
|
903
|
+
p = p
|
|
904
|
+
.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
|
|
905
|
+
.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
906
|
+
p = p
|
|
907
|
+
.addOption(program
|
|
908
|
+
.createDynamicOption('--entropy-warn', (cfg) => {
|
|
909
|
+
const warn = cfg.warnEntropy;
|
|
910
|
+
// Default is effectively ON when warnEntropy is true or undefined.
|
|
911
|
+
return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
|
|
912
|
+
})
|
|
913
|
+
.conflicts('entropyWarnOff'))
|
|
914
|
+
.addOption(program
|
|
915
|
+
.createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
|
|
916
|
+
.conflicts('entropyWarn'))
|
|
917
|
+
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
918
|
+
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
919
|
+
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
920
|
+
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
921
|
+
// Restore original methods
|
|
922
|
+
program.addOption = originalAddOption;
|
|
923
|
+
program.option = originalOption;
|
|
924
|
+
return p;
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Zod schemas for programmatic GetDotenv options.
|
|
929
|
+
*
|
|
930
|
+
* NOTE: These schemas are introduced without wiring to avoid behavior changes.
|
|
931
|
+
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
932
|
+
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
933
|
+
* later per the staged plan.
|
|
934
|
+
*/
|
|
935
|
+
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
936
|
+
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
937
|
+
// RAW: all fields optional — undefined means "inherit" from lower layers.
|
|
938
|
+
const getDotenvOptionsSchemaRaw = z.object({
|
|
939
|
+
defaultEnv: z.string().optional(),
|
|
940
|
+
dotenvToken: z.string().optional(),
|
|
941
|
+
dynamicPath: z.string().optional(),
|
|
942
|
+
// Dynamic map is intentionally wide for now; refine once sources are normalized.
|
|
943
|
+
dynamic: z.record(z.string(), z.unknown()).optional(),
|
|
944
|
+
env: z.string().optional(),
|
|
945
|
+
excludeDynamic: z.boolean().optional(),
|
|
946
|
+
excludeEnv: z.boolean().optional(),
|
|
947
|
+
excludeGlobal: z.boolean().optional(),
|
|
948
|
+
excludePrivate: z.boolean().optional(),
|
|
949
|
+
excludePublic: z.boolean().optional(),
|
|
950
|
+
loadProcess: z.boolean().optional(),
|
|
951
|
+
log: z.boolean().optional(),
|
|
952
|
+
outputPath: z.string().optional(),
|
|
953
|
+
paths: z.array(z.string()).optional(),
|
|
954
|
+
privateToken: z.string().optional(),
|
|
955
|
+
vars: processEnvSchema.optional(),
|
|
956
|
+
// Host-only feature flag: guarded integration of config loader/overlay
|
|
957
|
+
useConfigLoader: z.boolean().optional(),
|
|
958
|
+
});
|
|
959
|
+
// RESOLVED: service-boundary contract (post-inheritance).
|
|
960
|
+
// For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
|
|
961
|
+
const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
|
|
962
|
+
|
|
963
|
+
const applyKv = (current, kv) => {
|
|
964
|
+
if (!kv || Object.keys(kv).length === 0)
|
|
965
|
+
return current;
|
|
966
|
+
const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
|
|
967
|
+
return { ...current, ...expanded };
|
|
968
|
+
};
|
|
969
|
+
const applyConfigSlice = (current, cfg, env) => {
|
|
970
|
+
if (!cfg)
|
|
971
|
+
return current;
|
|
972
|
+
// kind axis: global then env (env overrides global)
|
|
973
|
+
const afterGlobal = applyKv(current, cfg.vars);
|
|
974
|
+
const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
|
|
975
|
+
return applyKv(afterGlobal, envKv);
|
|
976
|
+
};
|
|
977
|
+
/**
|
|
978
|
+
* Overlay config-provided values onto a base ProcessEnv using precedence axes:
|
|
979
|
+
* - kind: env \> global
|
|
980
|
+
* - privacy: local \> public
|
|
981
|
+
* - source: project \> packaged \> base
|
|
982
|
+
*
|
|
983
|
+
* Programmatic explicit vars (if provided) override all config slices.
|
|
984
|
+
* Progressive expansion is applied within each slice.
|
|
985
|
+
*/
|
|
986
|
+
const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
987
|
+
let current = { ...base };
|
|
988
|
+
// Source: packaged (public -> local)
|
|
989
|
+
current = applyConfigSlice(current, configs.packaged, env);
|
|
990
|
+
// Packaged "local" is not expected by policy; if present, honor it.
|
|
991
|
+
// We do not have a separate object for packaged.local in sources, keep as-is.
|
|
992
|
+
// Source: project (public -> local)
|
|
993
|
+
current = applyConfigSlice(current, configs.project?.public, env);
|
|
994
|
+
current = applyConfigSlice(current, configs.project?.local, env);
|
|
995
|
+
// Programmatic explicit vars (top of static tier)
|
|
996
|
+
if (programmaticVars) {
|
|
997
|
+
const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
|
|
998
|
+
current = applyKv(current, toApply);
|
|
999
|
+
}
|
|
1000
|
+
return current;
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
/** src/diagnostics/entropy.ts
|
|
1004
|
+
* Entropy diagnostics (presentation-only).
|
|
1005
|
+
* - Gated by min length and printable ASCII.
|
|
1006
|
+
* - Warn once per key per run when bits/char \>= threshold.
|
|
1007
|
+
* - Supports whitelist patterns to suppress known-noise keys.
|
|
1008
|
+
*/
|
|
1009
|
+
const warned = new Set();
|
|
1010
|
+
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
1011
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
1012
|
+
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
1013
|
+
const shannonBitsPerChar = (s) => {
|
|
1014
|
+
const freq = new Map();
|
|
1015
|
+
for (const ch of s)
|
|
1016
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
1017
|
+
const n = s.length;
|
|
651
1018
|
let h = 0;
|
|
652
1019
|
for (const c of freq.values()) {
|
|
653
1020
|
const p = c / n;
|
|
@@ -1190,6 +1557,8 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1190
1557
|
};
|
|
1191
1558
|
};
|
|
1192
1559
|
|
|
1560
|
+
// Dynamic help support: attach a private symbol to Option for description fns.
|
|
1561
|
+
const DYN_DESC_SYM = Symbol('getdotenv.dynamic.description');
|
|
1193
1562
|
const HOST_META_URL = import.meta.url;
|
|
1194
1563
|
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
1195
1564
|
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
@@ -1205,13 +1574,20 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
|
1205
1574
|
*
|
|
1206
1575
|
* NOTE: This host is additive and does not alter the legacy CLI.
|
|
1207
1576
|
*/
|
|
1208
|
-
class GetDotenvCli extends Command {
|
|
1577
|
+
let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
1209
1578
|
/** Registered top-level plugins (composition happens via .use()) */
|
|
1210
1579
|
_plugins = [];
|
|
1211
1580
|
/** One-time installation guard */
|
|
1212
1581
|
_installed = false;
|
|
1213
1582
|
/** Optional header line to prepend in help output */
|
|
1214
1583
|
[HELP_HEADER_SYMBOL];
|
|
1584
|
+
/**
|
|
1585
|
+
* Create a subcommand using the same subclass, preserving helpers like
|
|
1586
|
+
* dynamicOption on children.
|
|
1587
|
+
*/
|
|
1588
|
+
createCommand(name) {
|
|
1589
|
+
return new this.constructor(name);
|
|
1590
|
+
}
|
|
1215
1591
|
constructor(alias = 'getdotenv') {
|
|
1216
1592
|
super(alias);
|
|
1217
1593
|
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
@@ -1219,15 +1595,18 @@ class GetDotenvCli extends Command {
|
|
|
1219
1595
|
// child uses passThroughOptions.
|
|
1220
1596
|
this.enablePositionalOptions();
|
|
1221
1597
|
// Configure grouped help: show only base options in default "Options";
|
|
1222
|
-
//
|
|
1598
|
+
// we will insert App/Plugin sections before Commands in helpInformation().
|
|
1223
1599
|
this.configureHelp({
|
|
1224
1600
|
visibleOptions: (cmd) => {
|
|
1225
|
-
const all = cmd.options ??
|
|
1226
|
-
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1601
|
+
const all = cmd.options ?? [];
|
|
1602
|
+
const parent = cmd.parent ?? null;
|
|
1603
|
+
const isRoot = parent === null;
|
|
1604
|
+
const list = isRoot
|
|
1605
|
+
? all.filter((opt) => {
|
|
1606
|
+
const group = opt.__group;
|
|
1607
|
+
return group === 'base';
|
|
1608
|
+
})
|
|
1609
|
+
: all.slice(); // subcommands: show all options (their own "Options:" block)
|
|
1231
1610
|
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1232
1611
|
const hasShort = (opt) => {
|
|
1233
1612
|
const flags = opt.flags ?? '';
|
|
@@ -1235,19 +1614,18 @@ class GetDotenvCli extends Command {
|
|
|
1235
1614
|
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1236
1615
|
};
|
|
1237
1616
|
const byFlags = (opt) => opt.flags ?? '';
|
|
1238
|
-
|
|
1617
|
+
list.sort((a, b) => {
|
|
1239
1618
|
const aS = hasShort(a) ? 1 : 0;
|
|
1240
1619
|
const bS = hasShort(b) ? 1 : 0;
|
|
1241
1620
|
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1242
1621
|
});
|
|
1243
|
-
return
|
|
1622
|
+
return list;
|
|
1244
1623
|
},
|
|
1245
1624
|
});
|
|
1246
1625
|
this.addHelpText('beforeAll', () => {
|
|
1247
1626
|
const header = this[HELP_HEADER_SYMBOL];
|
|
1248
1627
|
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1249
1628
|
});
|
|
1250
|
-
this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
|
|
1251
1629
|
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
1252
1630
|
// mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
|
|
1253
1631
|
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
@@ -1259,9 +1637,15 @@ class GetDotenvCli extends Command {
|
|
|
1259
1637
|
});
|
|
1260
1638
|
}
|
|
1261
1639
|
/**
|
|
1262
|
-
* Resolve options (strict) and compute dotenv context.
|
|
1640
|
+
* Resolve options (strict) and compute dotenv context.
|
|
1641
|
+
* Stores the context on the instance under a symbol.
|
|
1642
|
+
*
|
|
1643
|
+
* Options:
|
|
1644
|
+
* - opts.runAfterResolve (default true): when false, skips running plugin
|
|
1645
|
+
* afterResolve hooks. Useful for top-level help rendering to avoid
|
|
1646
|
+
* long-running side-effects while still evaluating dynamic help text.
|
|
1263
1647
|
*/
|
|
1264
|
-
async resolveAndLoad(customOptions = {}) {
|
|
1648
|
+
async resolveAndLoad(customOptions = {}, opts) {
|
|
1265
1649
|
// Resolve defaults, then validate strictly under the new host.
|
|
1266
1650
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1267
1651
|
getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
@@ -1272,9 +1656,64 @@ class GetDotenvCli extends Command {
|
|
|
1272
1656
|
ctx;
|
|
1273
1657
|
// Ensure plugins are installed exactly once, then run afterResolve.
|
|
1274
1658
|
await this.install();
|
|
1275
|
-
|
|
1659
|
+
if (opts?.runAfterResolve ?? true) {
|
|
1660
|
+
await this._runAfterResolve(ctx);
|
|
1661
|
+
}
|
|
1276
1662
|
return ctx;
|
|
1277
1663
|
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Create a Commander Option that computes its description at help time.
|
|
1666
|
+
* The returned Option may be configured (conflicts, default, parser) and
|
|
1667
|
+
* added via addOption().
|
|
1668
|
+
*/
|
|
1669
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
1670
|
+
createDynamicOption(flags, desc, parser, defaultValue) {
|
|
1671
|
+
const opt = new Option(flags, '');
|
|
1672
|
+
// Keep the function on a private symbol so it survives through Commander.
|
|
1673
|
+
opt[DYN_DESC_SYM] = desc;
|
|
1674
|
+
if (parser)
|
|
1675
|
+
opt.argParser(parser);
|
|
1676
|
+
if (defaultValue !== undefined)
|
|
1677
|
+
opt.default(defaultValue);
|
|
1678
|
+
return opt;
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Chainable helper mirroring .option(), but with a dynamic description.
|
|
1682
|
+
* Equivalent to addOption(createDynamicOption(...)).
|
|
1683
|
+
*/
|
|
1684
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
1685
|
+
dynamicOption(flags, desc, parser, defaultValue) {
|
|
1686
|
+
const opt = this.createDynamicOption(flags, desc, parser, defaultValue);
|
|
1687
|
+
this.addOption(opt);
|
|
1688
|
+
return this;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Evaluate dynamic descriptions for this command and all descendants using
|
|
1692
|
+
* the provided resolved configuration. Mutates the Option.description in
|
|
1693
|
+
* place so Commander help renders updated text.
|
|
1694
|
+
*/
|
|
1695
|
+
evaluateDynamicOptions(resolved) {
|
|
1696
|
+
const visit = (cmd) => {
|
|
1697
|
+
const arr = cmd.options ?? [];
|
|
1698
|
+
for (const o of arr) {
|
|
1699
|
+
const dyn = o[DYN_DESC_SYM];
|
|
1700
|
+
if (typeof dyn === 'function') {
|
|
1701
|
+
try {
|
|
1702
|
+
const txt = dyn(resolved);
|
|
1703
|
+
// Commander Option has a public "description" field used by help.
|
|
1704
|
+
o.description = txt;
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
// Best-effort: leave description as-is on evaluation failure.
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const children = cmd.commands ?? [];
|
|
1712
|
+
for (const c of children)
|
|
1713
|
+
visit(c);
|
|
1714
|
+
};
|
|
1715
|
+
visit(this);
|
|
1716
|
+
}
|
|
1278
1717
|
/**
|
|
1279
1718
|
* Retrieve the current invocation context (if any).
|
|
1280
1719
|
*/
|
|
@@ -1304,6 +1743,7 @@ class GetDotenvCli extends Command {
|
|
|
1304
1743
|
tagAppOptions(fn) {
|
|
1305
1744
|
const root = this;
|
|
1306
1745
|
const originalAddOption = root.addOption.bind(root);
|
|
1746
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1307
1747
|
const originalOption = root.option.bind(root);
|
|
1308
1748
|
const tagLatest = (cmd, group) => {
|
|
1309
1749
|
const optsArr = cmd.options;
|
|
@@ -1316,6 +1756,7 @@ class GetDotenvCli extends Command {
|
|
|
1316
1756
|
opt.__group = 'app';
|
|
1317
1757
|
return originalAddOption(opt);
|
|
1318
1758
|
};
|
|
1759
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1319
1760
|
root.option = function patchedOption(...args) {
|
|
1320
1761
|
const ret = originalOption(...args);
|
|
1321
1762
|
tagLatest(this, 'app');
|
|
@@ -1326,6 +1767,7 @@ class GetDotenvCli extends Command {
|
|
|
1326
1767
|
}
|
|
1327
1768
|
finally {
|
|
1328
1769
|
root.addOption = originalAddOption;
|
|
1770
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1329
1771
|
root.option = originalOption;
|
|
1330
1772
|
}
|
|
1331
1773
|
}
|
|
@@ -1371,6 +1813,40 @@ class GetDotenvCli extends Command {
|
|
|
1371
1813
|
}
|
|
1372
1814
|
return this;
|
|
1373
1815
|
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Insert grouped plugin/app options between "Options" and "Commands" for
|
|
1818
|
+
* hybrid ordering. Applies to root and any parent command.
|
|
1819
|
+
*/
|
|
1820
|
+
helpInformation() {
|
|
1821
|
+
// Base help text first (includes beforeAll/after hooks).
|
|
1822
|
+
const base = super.helpInformation();
|
|
1823
|
+
const groups = this.#renderOptionGroups(this);
|
|
1824
|
+
const block = typeof groups === 'string' ? groups.trim() : '';
|
|
1825
|
+
let out = base;
|
|
1826
|
+
if (!block) {
|
|
1827
|
+
// Ensure a trailing blank line even when no extra groups render.
|
|
1828
|
+
if (!out.endsWith('\n\n'))
|
|
1829
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1830
|
+
return out;
|
|
1831
|
+
}
|
|
1832
|
+
// Insert just before "Commands:" when present.
|
|
1833
|
+
const marker = '\nCommands:';
|
|
1834
|
+
const idx = base.indexOf(marker);
|
|
1835
|
+
if (idx >= 0) {
|
|
1836
|
+
const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
|
|
1837
|
+
out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
// Otherwise append.
|
|
1841
|
+
const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
|
|
1842
|
+
out = `${base}${sep}${groups}`;
|
|
1843
|
+
}
|
|
1844
|
+
// Ensure a trailing blank line for prompt separation.
|
|
1845
|
+
if (!out.endsWith('\n\n')) {
|
|
1846
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1847
|
+
}
|
|
1848
|
+
return out;
|
|
1849
|
+
}
|
|
1374
1850
|
/**
|
|
1375
1851
|
* Register a plugin for installation (parent level).
|
|
1376
1852
|
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
@@ -1409,7 +1885,7 @@ class GetDotenvCli extends Command {
|
|
|
1409
1885
|
for (const p of this._plugins)
|
|
1410
1886
|
await run(p);
|
|
1411
1887
|
}
|
|
1412
|
-
// Render App/Plugin grouped options
|
|
1888
|
+
// Render App/Plugin grouped options (used by helpInformation override).
|
|
1413
1889
|
#renderOptionGroups(cmd) {
|
|
1414
1890
|
const all = cmd.options ?? [];
|
|
1415
1891
|
const byGroup = new Map();
|
|
@@ -1451,369 +1927,96 @@ class GetDotenvCli extends Command {
|
|
|
1451
1927
|
}
|
|
1452
1928
|
// Plugin groups sorted by id
|
|
1453
1929
|
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1930
|
+
const currentName = cmd.name?.() ?? '';
|
|
1454
1931
|
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1455
1932
|
for (const k of pluginKeys) {
|
|
1456
1933
|
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1457
1934
|
const rows = byGroup.get(k) ?? [];
|
|
1458
|
-
|
|
1935
|
+
// Do not show a "Plugin options — <self>" section on the command that owns those options.
|
|
1936
|
+
// Only child-injected plugin groups should render at this level.
|
|
1937
|
+
if (rows.length > 0 && id !== currentName) {
|
|
1459
1938
|
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1460
1939
|
}
|
|
1461
1940
|
}
|
|
1462
1941
|
return out;
|
|
1463
1942
|
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
/**
|
|
1467
|
-
* Validate a composed env against config-provided validation surfaces.
|
|
1468
|
-
* Precedence for validation definitions:
|
|
1469
|
-
* project.local -\> project.public -\> packaged
|
|
1470
|
-
*
|
|
1471
|
-
* Behavior:
|
|
1472
|
-
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
1473
|
-
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
1474
|
-
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
1475
|
-
*/
|
|
1476
|
-
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
1477
|
-
const pick = (getter) => {
|
|
1478
|
-
const pl = sources.project?.local;
|
|
1479
|
-
const pp = sources.project?.public;
|
|
1480
|
-
const pk = sources.packaged;
|
|
1481
|
-
return ((pl && getter(pl)) ||
|
|
1482
|
-
(pp && getter(pp)) ||
|
|
1483
|
-
(pk && getter(pk)) ||
|
|
1484
|
-
undefined);
|
|
1485
|
-
};
|
|
1486
|
-
const schema = pick((cfg) => cfg['schema']);
|
|
1487
|
-
if (schema &&
|
|
1488
|
-
typeof schema.safeParse === 'function') {
|
|
1489
|
-
try {
|
|
1490
|
-
const parsed = schema.safeParse(finalEnv);
|
|
1491
|
-
if (!parsed.success) {
|
|
1492
|
-
// Try to render zod-style issues when available.
|
|
1493
|
-
const err = parsed.error;
|
|
1494
|
-
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
1495
|
-
? err.issues.map((i) => {
|
|
1496
|
-
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1497
|
-
const msg = i.message ?? 'Invalid value';
|
|
1498
|
-
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
1499
|
-
})
|
|
1500
|
-
: ['[schema] validation failed'];
|
|
1501
|
-
return issues;
|
|
1502
|
-
}
|
|
1503
|
-
return [];
|
|
1504
|
-
}
|
|
1505
|
-
catch {
|
|
1506
|
-
// If schema invocation fails, surface a single diagnostic.
|
|
1507
|
-
return [
|
|
1508
|
-
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
1509
|
-
];
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
1513
|
-
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
1514
|
-
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
1515
|
-
if (missing.length > 0) {
|
|
1516
|
-
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
return [];
|
|
1520
|
-
};
|
|
1521
|
-
|
|
1522
|
-
/**
|
|
1523
|
-
* Attach legacy root flags to a Commander program.
|
|
1524
|
-
* Uses provided defaults to render help labels without coupling to generators.
|
|
1525
|
-
*/
|
|
1526
|
-
const attachRootOptions = (program, defaults, opts) => {
|
|
1527
|
-
// Install temporary wrappers to tag all options added here as "base".
|
|
1528
|
-
const GROUP = 'base';
|
|
1529
|
-
const tagLatest = (cmd, group) => {
|
|
1530
|
-
const optsArr = cmd.options;
|
|
1531
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
1532
|
-
const last = optsArr[optsArr.length - 1];
|
|
1533
|
-
last.__group = group;
|
|
1534
|
-
}
|
|
1535
|
-
};
|
|
1536
|
-
const originalAddOption = program.addOption.bind(program);
|
|
1537
|
-
const originalOption = program.option.bind(program);
|
|
1538
|
-
program.addOption = function patchedAdd(opt) {
|
|
1539
|
-
// Tag before adding, in case consumers inspect the Option directly.
|
|
1540
|
-
opt.__group = GROUP;
|
|
1541
|
-
const ret = originalAddOption(opt);
|
|
1542
|
-
return ret;
|
|
1543
|
-
};
|
|
1544
|
-
program.option = function patchedOption(...args) {
|
|
1545
|
-
const ret = originalOption(...args);
|
|
1546
|
-
tagLatest(this, GROUP);
|
|
1547
|
-
return ret;
|
|
1548
|
-
};
|
|
1549
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
1550
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
1551
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
1552
|
-
// Build initial chain.
|
|
1553
|
-
let p = program
|
|
1554
|
-
.enablePositionalOptions()
|
|
1555
|
-
.passThroughOptions()
|
|
1556
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
1557
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
1558
|
-
['KEY1', 'VAL1'],
|
|
1559
|
-
['KEY2', 'VAL2'],
|
|
1560
|
-
]
|
|
1561
|
-
.map((v) => v.join(va))
|
|
1562
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
1563
|
-
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
1564
|
-
// Default is OFF; the generator opts in explicitly.
|
|
1565
|
-
if (opts?.includeCommandOption === true) {
|
|
1566
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
1567
|
-
}
|
|
1568
|
-
p = p
|
|
1569
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
1570
|
-
.addOption(new Option('-s, --shell [string]', (() => {
|
|
1571
|
-
let defaultLabel = '';
|
|
1572
|
-
if (shell !== undefined) {
|
|
1573
|
-
if (typeof shell === 'boolean') {
|
|
1574
|
-
defaultLabel = ' (default OS shell)';
|
|
1575
|
-
}
|
|
1576
|
-
else if (typeof shell === 'string') {
|
|
1577
|
-
// Safe string interpolation
|
|
1578
|
-
defaultLabel = ` (default ${shell})`;
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
1582
|
-
})()).conflicts('shellOff'))
|
|
1583
|
-
.addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
1584
|
-
.addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
1585
|
-
.addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
1586
|
-
.addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
1587
|
-
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
1588
|
-
? ' (default)'
|
|
1589
|
-
: ''}`).conflicts('excludeAllOff'))
|
|
1590
|
-
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
1591
|
-
.addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
1592
|
-
.addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
1593
|
-
.addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
1594
|
-
.addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
1595
|
-
.addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
1596
|
-
.addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
1597
|
-
.addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
1598
|
-
.addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
1599
|
-
.addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
1600
|
-
.addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
1601
|
-
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
1602
|
-
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
1603
|
-
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
1604
|
-
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
1605
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
1606
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
1607
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
1608
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
1609
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
1610
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
1611
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
1612
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
1613
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
1614
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
1615
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
1616
|
-
// Hidden scripts pipe-through (stringified)
|
|
1617
|
-
.addOption(new Option('--scripts <string>')
|
|
1618
|
-
.default(JSON.stringify(scripts))
|
|
1619
|
-
.hideHelp());
|
|
1620
|
-
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
1621
|
-
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
1622
|
-
// Validation: strict mode fails on env validation issues (warn by default).
|
|
1623
|
-
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
1624
|
-
// Entropy diagnostics (presentation-only)
|
|
1625
|
-
p = p
|
|
1626
|
-
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
1627
|
-
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
1628
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
1629
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
1630
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
1631
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
1632
|
-
// Restore original methods to avoid tagging future additions outside base.
|
|
1633
|
-
program.addOption = originalAddOption;
|
|
1634
|
-
program.option = originalOption;
|
|
1635
|
-
return p;
|
|
1636
1943
|
};
|
|
1637
1944
|
|
|
1638
|
-
/**
|
|
1639
|
-
*
|
|
1640
|
-
* - If the user explicitly enabled the flag, return true.
|
|
1641
|
-
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
1642
|
-
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
1643
|
-
*
|
|
1644
|
-
* @param exclude - The "on" flag value as parsed by Commander.
|
|
1645
|
-
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
1646
|
-
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
1647
|
-
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
1945
|
+
/** src/cliHost/definePlugin.ts
|
|
1946
|
+
* Plugin contracts for the GetDotenv CLI host.
|
|
1648
1947
|
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
* ```
|
|
1948
|
+
* This module exposes a structural public interface for the host that plugins
|
|
1949
|
+
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
1950
|
+
* nominal class identity issues (private fields) in downstream consumers.
|
|
1653
1951
|
*/
|
|
1654
|
-
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
1655
1952
|
/**
|
|
1656
|
-
*
|
|
1657
|
-
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
1658
|
-
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
1659
|
-
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
1660
|
-
*
|
|
1661
|
-
* @param exclude - Individual include/exclude flag.
|
|
1662
|
-
* @param excludeOff - Individual "...-off" flag.
|
|
1663
|
-
* @param defaultValue - Default for the individual flag.
|
|
1664
|
-
* @param excludeAll - Global "exclude-all" flag.
|
|
1665
|
-
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
1953
|
+
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
1666
1954
|
*
|
|
1667
1955
|
* @example
|
|
1668
|
-
*
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
// Order of precedence:
|
|
1672
|
-
// 1) Individual explicit "on" wins outright.
|
|
1673
|
-
// 2) Individual explicit "off" wins over any global.
|
|
1674
|
-
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
1675
|
-
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
1676
|
-
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
1677
|
-
(() => {
|
|
1678
|
-
// Individual "on"
|
|
1679
|
-
if (exclude === true)
|
|
1680
|
-
return true;
|
|
1681
|
-
// Individual "off"
|
|
1682
|
-
if (excludeOff === true)
|
|
1683
|
-
return undefined;
|
|
1684
|
-
// Global "exclude-all" ON (unless explicitly turned off)
|
|
1685
|
-
if (excludeAll === true)
|
|
1686
|
-
return true;
|
|
1687
|
-
// Global "exclude-all-off" (unless explicitly enabled)
|
|
1688
|
-
if (excludeAllOff === true)
|
|
1689
|
-
return undefined;
|
|
1690
|
-
// Default
|
|
1691
|
-
return defaultValue ? true : undefined;
|
|
1692
|
-
})();
|
|
1693
|
-
/**
|
|
1694
|
-
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
1695
|
-
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
1696
|
-
*
|
|
1697
|
-
* @typeParam T - Target object type.
|
|
1698
|
-
* @param obj - The object to write to.
|
|
1699
|
-
* @param key - The optional boolean property key of {@link T}.
|
|
1700
|
-
* @param value - The value to set or `undefined` to unset.
|
|
1701
|
-
*
|
|
1702
|
-
* @remarks
|
|
1703
|
-
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
1704
|
-
*/
|
|
1705
|
-
const setOptionalFlag = (obj, key, value) => {
|
|
1706
|
-
const target = obj;
|
|
1707
|
-
const k = key;
|
|
1708
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1709
|
-
if (value === undefined)
|
|
1710
|
-
delete target[k];
|
|
1711
|
-
else
|
|
1712
|
-
target[k] = value;
|
|
1713
|
-
};
|
|
1714
|
-
|
|
1715
|
-
/**
|
|
1716
|
-
* Merge and normalize raw Commander options (current + parent + defaults)
|
|
1717
|
-
* into a GetDotenvCliOptions-like object. Types are intentionally wide to
|
|
1718
|
-
* avoid cross-layer coupling; callers may cast as needed.
|
|
1956
|
+
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
1957
|
+
* .use(childA)
|
|
1958
|
+
* .use(childB);
|
|
1719
1959
|
*/
|
|
1720
|
-
const
|
|
1721
|
-
const
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
// ignore parse errors; leave scripts undefined
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
1735
|
-
const d = defaults;
|
|
1736
|
-
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
1737
|
-
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
1738
|
-
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
1739
|
-
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
1740
|
-
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
1741
|
-
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
1742
|
-
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
1743
|
-
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
1744
|
-
// warnEntropy (tri-state)
|
|
1745
|
-
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
1746
|
-
// Normalize shell for predictability: explicit default shell per OS.
|
|
1747
|
-
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
1748
|
-
let resolvedShell = merged.shell;
|
|
1749
|
-
if (shellOff)
|
|
1750
|
-
resolvedShell = false;
|
|
1751
|
-
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
1752
|
-
resolvedShell = defaultShell;
|
|
1753
|
-
}
|
|
1754
|
-
else if (typeof resolvedShell !== 'string' &&
|
|
1755
|
-
typeof defaults.shell === 'string') {
|
|
1756
|
-
resolvedShell = defaults.shell;
|
|
1757
|
-
}
|
|
1758
|
-
merged.shell = resolvedShell;
|
|
1759
|
-
const cmd = typeof command === 'string' ? command : undefined;
|
|
1760
|
-
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
1960
|
+
const definePlugin = (spec) => {
|
|
1961
|
+
const { children = [], ...rest } = spec;
|
|
1962
|
+
const plugin = {
|
|
1963
|
+
...rest,
|
|
1964
|
+
children: [...children],
|
|
1965
|
+
use(child) {
|
|
1966
|
+
this.children.push(child);
|
|
1967
|
+
return this;
|
|
1968
|
+
},
|
|
1969
|
+
};
|
|
1970
|
+
return plugin;
|
|
1761
1971
|
};
|
|
1762
1972
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
this
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
process.exit(1);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
catch {
|
|
1801
|
-
// Be tolerant: validation errors reported above; unexpected failures here
|
|
1802
|
-
// should not crash non-strict flows.
|
|
1803
|
-
}
|
|
1804
|
-
});
|
|
1805
|
-
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
1806
|
-
// with the same merged options and context without duplicating logic.
|
|
1807
|
-
this.hook('preAction', async (thisCommand) => {
|
|
1808
|
-
const raw = thisCommand.opts();
|
|
1809
|
-
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
1810
|
-
thisCommand.getDotenvCliOptions =
|
|
1811
|
-
merged;
|
|
1812
|
-
this._setOptionsBag(merged);
|
|
1813
|
-
// Avoid duplicate heavy work if a context is already present.
|
|
1814
|
-
if (!this.getCtx()) {
|
|
1973
|
+
/**
|
|
1974
|
+
* GetDotenvCli with root helpers as real class methods.
|
|
1975
|
+
* - attachRootOptions: installs legacy/base root flags on the command.
|
|
1976
|
+
* - passOptions: merges flags (parent \< current), computes dotenv context once,
|
|
1977
|
+
* runs validation, and persists merged options for nested flows.
|
|
1978
|
+
*/
|
|
1979
|
+
class GetDotenvCli extends GetDotenvCli$1 {
|
|
1980
|
+
/**
|
|
1981
|
+
* Attach legacy root flags to this CLI instance. Defaults come from
|
|
1982
|
+
* baseRootOptionDefaults when none are provided.
|
|
1983
|
+
*/
|
|
1984
|
+
attachRootOptions(defaults, opts) {
|
|
1985
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
1986
|
+
attachRootOptions(this, d, opts);
|
|
1987
|
+
return this;
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Install preSubcommand/preAction hooks that:
|
|
1991
|
+
* - Merge options (parent round-trip + current invocation) using resolveCliOptions.
|
|
1992
|
+
* - Persist the merged bag on the current command and on the host (for ergonomics).
|
|
1993
|
+
* - Compute the dotenv context once via resolveAndLoad(serviceOptions).
|
|
1994
|
+
* - Validate the composed env against discovered config (warn or --strict fail).
|
|
1995
|
+
*/
|
|
1996
|
+
passOptions(defaults) {
|
|
1997
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
1998
|
+
this.hook('preSubcommand', async (thisCommand) => {
|
|
1999
|
+
const raw = thisCommand.opts();
|
|
2000
|
+
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
2001
|
+
// Persist merged options (for nested behavior and ergonomic access).
|
|
2002
|
+
thisCommand.getDotenvCliOptions =
|
|
2003
|
+
merged;
|
|
2004
|
+
this._setOptionsBag(merged);
|
|
2005
|
+
// Build service options and compute context (always-on loader path).
|
|
1815
2006
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1816
2007
|
await this.resolveAndLoad(serviceOptions);
|
|
2008
|
+
// Refresh dynamic option descriptions using resolved config + plugin slices
|
|
2009
|
+
try {
|
|
2010
|
+
const ctx = this.getCtx();
|
|
2011
|
+
this.evaluateDynamicOptions({
|
|
2012
|
+
...ctx?.optionsResolved,
|
|
2013
|
+
plugins: ctx?.pluginConfigs ?? {},
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
catch {
|
|
2017
|
+
/* best-effort */
|
|
2018
|
+
}
|
|
2019
|
+
// Global validation: once after Phase C using config sources.
|
|
1817
2020
|
try {
|
|
1818
2021
|
const ctx = this.getCtx();
|
|
1819
2022
|
const dotenv = (ctx?.dotenv ?? {});
|
|
@@ -1832,12 +2035,56 @@ GetDotenvCli.prototype.passOptions = function (defaults) {
|
|
|
1832
2035
|
}
|
|
1833
2036
|
}
|
|
1834
2037
|
catch {
|
|
1835
|
-
//
|
|
2038
|
+
// Be tolerant: do not crash non-strict flows on unexpected validator failures.
|
|
1836
2039
|
}
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2040
|
+
});
|
|
2041
|
+
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
2042
|
+
// with the same merged options and context without duplicating logic.
|
|
2043
|
+
this.hook('preAction', async (thisCommand) => {
|
|
2044
|
+
const raw = thisCommand.opts();
|
|
2045
|
+
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
2046
|
+
thisCommand.getDotenvCliOptions =
|
|
2047
|
+
merged;
|
|
2048
|
+
this._setOptionsBag(merged);
|
|
2049
|
+
// Avoid duplicate heavy work if a context is already present.
|
|
2050
|
+
if (!this.getCtx()) {
|
|
2051
|
+
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
2052
|
+
await this.resolveAndLoad(serviceOptions);
|
|
2053
|
+
try {
|
|
2054
|
+
const ctx = this.getCtx();
|
|
2055
|
+
this.evaluateDynamicOptions({
|
|
2056
|
+
...ctx?.optionsResolved,
|
|
2057
|
+
plugins: ctx?.pluginConfigs ?? {},
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
catch {
|
|
2061
|
+
/* tolerate */
|
|
2062
|
+
}
|
|
2063
|
+
try {
|
|
2064
|
+
const ctx = this.getCtx();
|
|
2065
|
+
const dotenv = (ctx?.dotenv ?? {});
|
|
2066
|
+
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
2067
|
+
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
2068
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
2069
|
+
const logger = (merged
|
|
2070
|
+
.logger ?? console);
|
|
2071
|
+
const emit = logger.error ?? logger.log;
|
|
2072
|
+
issues.forEach((m) => {
|
|
2073
|
+
emit(m);
|
|
2074
|
+
});
|
|
2075
|
+
if (merged.strict) {
|
|
2076
|
+
process.exit(1);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
catch {
|
|
2081
|
+
// Tolerate validation side-effects in non-strict mode.
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
return this;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
1841
2088
|
|
|
1842
2089
|
// Minimal tokenizer for shell-off execution:
|
|
1843
2090
|
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
@@ -2072,34 +2319,6 @@ const buildSpawnEnv = (base, overlay) => {
|
|
|
2072
2319
|
return out;
|
|
2073
2320
|
};
|
|
2074
2321
|
|
|
2075
|
-
/** src/cliHost/definePlugin.ts
|
|
2076
|
-
* Plugin contracts for the GetDotenv CLI host.
|
|
2077
|
-
*
|
|
2078
|
-
* This module exposes a structural public interface for the host that plugins
|
|
2079
|
-
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
2080
|
-
* nominal class identity issues (private fields) in downstream consumers.
|
|
2081
|
-
*/
|
|
2082
|
-
/**
|
|
2083
|
-
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
2084
|
-
*
|
|
2085
|
-
* @example
|
|
2086
|
-
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
2087
|
-
* .use(childA)
|
|
2088
|
-
* .use(childB);
|
|
2089
|
-
*/
|
|
2090
|
-
const definePlugin = (spec) => {
|
|
2091
|
-
const { children = [], ...rest } = spec;
|
|
2092
|
-
const plugin = {
|
|
2093
|
-
...rest,
|
|
2094
|
-
children: [...children],
|
|
2095
|
-
use(child) {
|
|
2096
|
-
this.children.push(child);
|
|
2097
|
-
return this;
|
|
2098
|
-
},
|
|
2099
|
-
};
|
|
2100
|
-
return plugin;
|
|
2101
|
-
};
|
|
2102
|
-
|
|
2103
2322
|
/**
|
|
2104
2323
|
* Batch services (neutral): resolve command and shell settings.
|
|
2105
2324
|
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
@@ -2335,7 +2554,6 @@ const awsPlugin = () => definePlugin({
|
|
|
2335
2554
|
cli
|
|
2336
2555
|
.ns('aws')
|
|
2337
2556
|
.description('Establish an AWS session and optionally forward to the AWS CLI')
|
|
2338
|
-
.configureHelp({ showGlobalOptions: true })
|
|
2339
2557
|
.enablePositionalOptions()
|
|
2340
2558
|
.passThroughOptions()
|
|
2341
2559
|
.allowUnknownOption(true)
|
|
@@ -2526,9 +2744,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
|
2526
2744
|
}
|
|
2527
2745
|
return { absRootPath, paths };
|
|
2528
2746
|
};
|
|
2529
|
-
const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
2747
|
+
const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
2530
2748
|
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
2531
|
-
Boolean(getDotenvCliOptions?.capture);
|
|
2749
|
+
Boolean(getDotenvCliOptions?.capture);
|
|
2750
|
+
// Require a command only when not listing. In list mode, a command is optional.
|
|
2532
2751
|
if (!command && !list) {
|
|
2533
2752
|
logger.error(`No command provided. Use --command or --list.`);
|
|
2534
2753
|
process.exit(0);
|
|
@@ -2575,12 +2794,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
2575
2794
|
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
2576
2795
|
(Array.isArray(command) && command.length > 0);
|
|
2577
2796
|
if (hasCmd) {
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2797
|
+
// Compose child env overlay from dotenv (drop undefined) and merged options
|
|
2798
|
+
const overlay = {};
|
|
2799
|
+
if (dotenvEnv) {
|
|
2800
|
+
for (const [k, v] of Object.entries(dotenvEnv)) {
|
|
2801
|
+
if (typeof v === 'string')
|
|
2802
|
+
overlay[k] = v;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
if (getDotenvCliOptions !== undefined) {
|
|
2806
|
+
try {
|
|
2807
|
+
overlay.getDotenvCliOptions = JSON.stringify(getDotenvCliOptions);
|
|
2808
|
+
}
|
|
2809
|
+
catch {
|
|
2810
|
+
// best-effort: omit if serialization fails
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2581
2813
|
await runCommand(command, shell, {
|
|
2582
2814
|
cwd: path,
|
|
2583
|
-
env: buildSpawnEnv(process.env,
|
|
2815
|
+
env: buildSpawnEnv(process.env, overlay),
|
|
2584
2816
|
stdio: capture ? 'pipe' : 'inherit',
|
|
2585
2817
|
});
|
|
2586
2818
|
}
|
|
@@ -2618,6 +2850,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2618
2850
|
const ctx = cli.getCtx();
|
|
2619
2851
|
const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
|
|
2620
2852
|
const cfg = (cfgRaw || {});
|
|
2853
|
+
const dotenvEnv = (ctx?.dotenv ?? {});
|
|
2621
2854
|
// Resolve batch flags from the captured parent (batch) command.
|
|
2622
2855
|
const raw = batchCmd.opts();
|
|
2623
2856
|
const listFromParent = !!raw.list;
|
|
@@ -2636,6 +2869,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2636
2869
|
if (typeof commandOpt === 'string') {
|
|
2637
2870
|
await execShellCommandBatch({
|
|
2638
2871
|
command: resolveCommand(scripts, commandOpt),
|
|
2872
|
+
dotenvEnv,
|
|
2639
2873
|
globs,
|
|
2640
2874
|
ignoreErrors,
|
|
2641
2875
|
list: false,
|
|
@@ -2647,6 +2881,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2647
2881
|
return;
|
|
2648
2882
|
}
|
|
2649
2883
|
if (raw.list || localList) {
|
|
2884
|
+
const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
|
|
2650
2885
|
await execShellCommandBatch({
|
|
2651
2886
|
globs,
|
|
2652
2887
|
ignoreErrors,
|
|
@@ -2654,7 +2889,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2654
2889
|
logger: loggerLocal,
|
|
2655
2890
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2656
2891
|
rootPath,
|
|
2657
|
-
shell: (shell ?? false),
|
|
2892
|
+
shell: (shell ?? shellBag.shell ?? false),
|
|
2658
2893
|
});
|
|
2659
2894
|
return;
|
|
2660
2895
|
}
|
|
@@ -2721,6 +2956,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2721
2956
|
}
|
|
2722
2957
|
await execShellCommandBatch({
|
|
2723
2958
|
command: commandArg,
|
|
2959
|
+
dotenvEnv,
|
|
2724
2960
|
...(envBag ? { getDotenvCliOptions: envBag } : {}),
|
|
2725
2961
|
globs,
|
|
2726
2962
|
ignoreErrors,
|
|
@@ -2739,6 +2975,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2739
2975
|
const logger = opts.logger ?? console;
|
|
2740
2976
|
// Ensure context exists (host preSubcommand on root creates if missing).
|
|
2741
2977
|
const ctx = cli.getCtx();
|
|
2978
|
+
const dotenvEnv = (ctx?.dotenv ?? {});
|
|
2742
2979
|
const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
|
|
2743
2980
|
const cfg = (cfgRaw || {});
|
|
2744
2981
|
const raw = thisCommand.opts();
|
|
@@ -2761,6 +2998,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2761
2998
|
const commandArg = resolved;
|
|
2762
2999
|
await execShellCommandBatch({
|
|
2763
3000
|
command: commandArg,
|
|
3001
|
+
dotenvEnv,
|
|
2764
3002
|
globs,
|
|
2765
3003
|
ignoreErrors,
|
|
2766
3004
|
list: false,
|
|
@@ -2798,6 +3036,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2798
3036
|
const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
|
|
2799
3037
|
await execShellCommandBatch({
|
|
2800
3038
|
command: resolveCommand(scriptsOpt, commandOpt),
|
|
3039
|
+
dotenvEnv,
|
|
2801
3040
|
globs,
|
|
2802
3041
|
ignoreErrors,
|
|
2803
3042
|
list,
|
|
@@ -2841,7 +3080,8 @@ const BatchConfigSchema = z.object({
|
|
|
2841
3080
|
/**
|
|
2842
3081
|
* Batch plugin for the GetDotenv CLI host.
|
|
2843
3082
|
*
|
|
2844
|
-
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
3083
|
+
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
3084
|
+
* Options:
|
|
2845
3085
|
* - scripts/shell: used to resolve command and shell behavior per script or global default.
|
|
2846
3086
|
* - logger: defaults to console.
|
|
2847
3087
|
*/
|
|
@@ -2853,12 +3093,32 @@ const batchPlugin = (opts = {}) => definePlugin({
|
|
|
2853
3093
|
setup(cli) {
|
|
2854
3094
|
const ns = cli.ns('batch');
|
|
2855
3095
|
const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
|
|
3096
|
+
const host = cli;
|
|
3097
|
+
const pluginId = 'batch';
|
|
3098
|
+
const GROUP = `plugin:${pluginId}`;
|
|
2856
3099
|
ns.description('Batch command execution across multiple working directories.')
|
|
2857
3100
|
.enablePositionalOptions()
|
|
2858
3101
|
.passThroughOptions()
|
|
2859
|
-
|
|
2860
|
-
.
|
|
2861
|
-
.
|
|
3102
|
+
// Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
|
|
3103
|
+
.addOption((() => {
|
|
3104
|
+
const opt = host.createDynamicOption('-p, --pkg-cwd', (cfg) => {
|
|
3105
|
+
const slice = cfg.plugins.batch ?? {};
|
|
3106
|
+
const on = !!slice.pkgCwd;
|
|
3107
|
+
return `use nearest package directory as current working directory${on ? ' (default)' : ''}`;
|
|
3108
|
+
});
|
|
3109
|
+
opt.__group = GROUP;
|
|
3110
|
+
return opt;
|
|
3111
|
+
})())
|
|
3112
|
+
.addOption((() => {
|
|
3113
|
+
const opt = host.createDynamicOption('-r, --root-path <string>', (cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.plugins.batch?.rootPath || './')})`);
|
|
3114
|
+
opt.__group = GROUP;
|
|
3115
|
+
return opt;
|
|
3116
|
+
})())
|
|
3117
|
+
.addOption((() => {
|
|
3118
|
+
const opt = host.createDynamicOption('-g, --globs <string>', (cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.plugins.batch?.globs || '*')})`);
|
|
3119
|
+
opt.__group = GROUP;
|
|
3120
|
+
return opt;
|
|
3121
|
+
})())
|
|
2862
3122
|
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
2863
3123
|
.option('-l, --list', 'list working directories without executing command')
|
|
2864
3124
|
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
@@ -3168,10 +3428,10 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
3168
3428
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
3169
3429
|
};
|
|
3170
3430
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
.
|
|
3174
|
-
.
|
|
3431
|
+
// Create as a GetDotenvCli child so helpInformation includes a trailing blank line.
|
|
3432
|
+
const cmd = cli
|
|
3433
|
+
.createCommand('cmd')
|
|
3434
|
+
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3175
3435
|
.enablePositionalOptions()
|
|
3176
3436
|
.passThroughOptions()
|
|
3177
3437
|
.argument('[command...]')
|
|
@@ -3363,7 +3623,7 @@ const demoPlugin = () => definePlugin({
|
|
|
3363
3623
|
const dotenv = (ctx?.dotenv ?? {});
|
|
3364
3624
|
// Inherit stdio for an interactive demo. Use --capture for CI.
|
|
3365
3625
|
await runCommand(['node', '-e', code], false, {
|
|
3366
|
-
env:
|
|
3626
|
+
env: buildSpawnEnv(process.env, dotenv),
|
|
3367
3627
|
stdio: 'inherit',
|
|
3368
3628
|
});
|
|
3369
3629
|
});
|
|
@@ -3400,20 +3660,23 @@ const demoPlugin = () => definePlugin({
|
|
|
3400
3660
|
const ctx = cli.getCtx();
|
|
3401
3661
|
const dotenv = (ctx?.dotenv ?? {});
|
|
3402
3662
|
await runCommand(resolved, shell, {
|
|
3403
|
-
env:
|
|
3663
|
+
env: buildSpawnEnv(process.env, dotenv),
|
|
3404
3664
|
stdio: 'inherit',
|
|
3405
3665
|
});
|
|
3406
3666
|
});
|
|
3407
3667
|
},
|
|
3408
3668
|
/**
|
|
3409
3669
|
* Optional: afterResolve can initialize per-plugin state using ctx.dotenv.
|
|
3410
|
-
* For the demo we
|
|
3670
|
+
* For the demo we emit a single breadcrumb only when GETDOTENV_DEBUG is set,
|
|
3671
|
+
* keeping default runs (tests/CI/smoke) quiet.
|
|
3411
3672
|
*/
|
|
3412
3673
|
afterResolve(_cli, ctx) {
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3674
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3675
|
+
const keys = Object.keys(ctx.dotenv);
|
|
3676
|
+
if (keys.length > 0) {
|
|
3677
|
+
// Keep noise low; a single-line breadcrumb is sufficient for the demo.
|
|
3678
|
+
console.error('[demo] afterResolve: dotenv keys loaded:', keys.length);
|
|
3679
|
+
}
|
|
3417
3680
|
}
|
|
3418
3681
|
},
|
|
3419
3682
|
});
|
|
@@ -3671,402 +3934,40 @@ const initPlugin = (opts = {}) => definePlugin({
|
|
|
3671
3934
|
},
|
|
3672
3935
|
});
|
|
3673
3936
|
|
|
3674
|
-
const cmdCommand$1 = new Command()
|
|
3675
|
-
.name('cmd')
|
|
3676
|
-
.description('execute command, conflicts with --command option (default subcommand)')
|
|
3677
|
-
.enablePositionalOptions()
|
|
3678
|
-
.passThroughOptions()
|
|
3679
|
-
.argument('[command...]')
|
|
3680
|
-
.action(async (commandParts, _options, thisCommand) => {
|
|
3681
|
-
if (!thisCommand.parent)
|
|
3682
|
-
throw new Error(`unable to resolve parent command`);
|
|
3683
|
-
if (!thisCommand.parent.parent)
|
|
3684
|
-
throw new Error(`unable to resolve root command`);
|
|
3685
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent.parent;
|
|
3686
|
-
const raw = thisCommand.parent.opts();
|
|
3687
|
-
const ignoreErrors = !!raw.ignoreErrors;
|
|
3688
|
-
const globs = typeof raw.globs === 'string' ? raw.globs : '*';
|
|
3689
|
-
const list = !!raw.list;
|
|
3690
|
-
const pkgCwd = !!raw.pkgCwd;
|
|
3691
|
-
const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : './';
|
|
3692
|
-
// Execute command.
|
|
3693
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
3694
|
-
// When no positional tokens are provided (e.g., option form `-c/--command`),
|
|
3695
|
-
// the preSubcommand hook handles execution. Avoid a duplicate call here.
|
|
3696
|
-
if (args.length === 0)
|
|
3697
|
-
return;
|
|
3698
|
-
const command = args.map(String).join(' ');
|
|
3699
|
-
await execShellCommandBatch({
|
|
3700
|
-
command: resolveCommand(getDotenvCliOptions.scripts, command),
|
|
3701
|
-
getDotenvCliOptions,
|
|
3702
|
-
globs,
|
|
3703
|
-
ignoreErrors,
|
|
3704
|
-
list,
|
|
3705
|
-
logger,
|
|
3706
|
-
pkgCwd,
|
|
3707
|
-
rootPath,
|
|
3708
|
-
// execa expects string | boolean | URL for `shell`. We normalize earlier;
|
|
3709
|
-
// scripts[name].shell overrides take precedence and may be boolean or string.
|
|
3710
|
-
shell: resolveShell(getDotenvCliOptions.scripts, command, getDotenvCliOptions.shell),
|
|
3711
|
-
});
|
|
3712
|
-
});
|
|
3713
|
-
|
|
3714
|
-
const batchCommand = new Command()
|
|
3715
|
-
.name('batch')
|
|
3716
|
-
.description('Batch command execution across multiple working directories.')
|
|
3717
|
-
.enablePositionalOptions()
|
|
3718
|
-
.passThroughOptions()
|
|
3719
|
-
.option('-p, --pkg-cwd', 'use nearest package directory as current working directory')
|
|
3720
|
-
.option('-r, --root-path <string>', 'path to batch root directory from current working directory', './')
|
|
3721
|
-
.option('-g, --globs <string>', 'space-delimited globs from root path', '*')
|
|
3722
|
-
.option('-c, --command <string>', 'command executed according to the base --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv)
|
|
3723
|
-
.option('-l, --list', 'list working directories without executing command')
|
|
3724
|
-
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
3725
|
-
.hook('preSubcommand', async (thisCommand) => {
|
|
3726
|
-
if (!thisCommand.parent)
|
|
3727
|
-
throw new Error(`unable to resolve root command`);
|
|
3728
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent;
|
|
3729
|
-
const raw = thisCommand.opts();
|
|
3730
|
-
const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
|
|
3731
|
-
const ignoreErrors = !!raw.ignoreErrors;
|
|
3732
|
-
const globs = typeof raw.globs === 'string' ? raw.globs : '*';
|
|
3733
|
-
const list = !!raw.list;
|
|
3734
|
-
const pkgCwd = !!raw.pkgCwd;
|
|
3735
|
-
const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : './';
|
|
3736
|
-
const argCount = thisCommand.args.length;
|
|
3737
|
-
if (typeof commandOpt === 'string' && argCount > 0) {
|
|
3738
|
-
logger.error(`--command option conflicts with cmd subcommand.`);
|
|
3739
|
-
process.exit(0);
|
|
3740
|
-
}
|
|
3741
|
-
// Execute command.
|
|
3742
|
-
if (typeof commandOpt === 'string')
|
|
3743
|
-
await execShellCommandBatch({
|
|
3744
|
-
command: resolveCommand(getDotenvCliOptions.scripts, commandOpt),
|
|
3745
|
-
getDotenvCliOptions,
|
|
3746
|
-
globs,
|
|
3747
|
-
ignoreErrors,
|
|
3748
|
-
list,
|
|
3749
|
-
logger,
|
|
3750
|
-
pkgCwd,
|
|
3751
|
-
rootPath,
|
|
3752
|
-
// execa expects string | boolean | URL for `shell`. We normalize earlier;
|
|
3753
|
-
// scripts[name].shell overrides take precedence and may be boolean or string.
|
|
3754
|
-
shell: resolveShell(getDotenvCliOptions.scripts, commandOpt, getDotenvCliOptions.shell),
|
|
3755
|
-
});
|
|
3756
|
-
})
|
|
3757
|
-
.addCommand(cmdCommand$1, { isDefault: true });
|
|
3758
|
-
|
|
3759
|
-
const cmdCommand = new Command()
|
|
3760
|
-
.name('cmd')
|
|
3761
|
-
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3762
|
-
.configureHelp({ showGlobalOptions: true })
|
|
3763
|
-
.enablePositionalOptions()
|
|
3764
|
-
.passThroughOptions()
|
|
3765
|
-
.argument('[command...]')
|
|
3766
|
-
.action(async (commandParts, _options, thisCommand) => {
|
|
3767
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
3768
|
-
if (args.length === 0)
|
|
3769
|
-
return;
|
|
3770
|
-
if (!thisCommand.parent)
|
|
3771
|
-
throw new Error('parent command not found');
|
|
3772
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent;
|
|
3773
|
-
const command = args.map(String).join(' ');
|
|
3774
|
-
const cmd = resolveCommand(getDotenvCliOptions.scripts, command);
|
|
3775
|
-
if (getDotenvCliOptions.debug)
|
|
3776
|
-
logger.log('\n*** command ***\n', `'${cmd}'`);
|
|
3777
|
-
await execaCommand(cmd, {
|
|
3778
|
-
env: {
|
|
3779
|
-
...process.env,
|
|
3780
|
-
getDotenvCliOptions: JSON.stringify(getDotenvCliOptions),
|
|
3781
|
-
},
|
|
3782
|
-
// execa expects string | boolean | URL; we normalize in generator
|
|
3783
|
-
// and allow script-level overrides.
|
|
3784
|
-
shell: resolveShell(getDotenvCliOptions.scripts, command, getDotenvCliOptions.shell),
|
|
3785
|
-
stdio: 'inherit',
|
|
3786
|
-
});
|
|
3787
|
-
});
|
|
3788
|
-
|
|
3789
|
-
/**
|
|
3790
|
-
* Create the root Commander command with legacy root options (via cliCore)
|
|
3791
|
-
* and built-in subcommands. Pure builder: no side-effects; the caller attaches
|
|
3792
|
-
* lifecycle hooks separately.
|
|
3793
|
-
*/
|
|
3794
|
-
const createRootCommand = (opts) => {
|
|
3795
|
-
const program = new Command().name(opts.alias).description(opts.description);
|
|
3796
|
-
// Attach legacy root flags using shared cliCore builder to keep parity.
|
|
3797
|
-
attachRootOptions(program, opts, {
|
|
3798
|
-
includeCommandOption: true,
|
|
3799
|
-
});
|
|
3800
|
-
// Subcommands
|
|
3801
|
-
program.addCommand(batchCommand).addCommand(cmdCommand, { isDefault: true });
|
|
3802
|
-
return program;
|
|
3803
|
-
};
|
|
3804
|
-
|
|
3805
|
-
/**
|
|
3806
|
-
* Resolve `GetDotenvCliGenerateOptions` from `import.meta.url` and custom options.
|
|
3807
|
-
*/
|
|
3808
|
-
const resolveGetDotenvCliGenerateOptions = async ({ importMetaUrl, ...customOptions }) => {
|
|
3809
|
-
const baseOptions = {
|
|
3810
|
-
...baseGetDotenvCliOptions,
|
|
3811
|
-
alias: 'getdotenv',
|
|
3812
|
-
description: 'Base CLI.',
|
|
3813
|
-
};
|
|
3814
|
-
const globalPkgDir = importMetaUrl
|
|
3815
|
-
? await packageDirectory({
|
|
3816
|
-
cwd: fileURLToPath(importMetaUrl),
|
|
3817
|
-
})
|
|
3818
|
-
: undefined;
|
|
3819
|
-
const globalOptionsPath = globalPkgDir
|
|
3820
|
-
? join(globalPkgDir, getDotenvOptionsFilename)
|
|
3821
|
-
: undefined;
|
|
3822
|
-
const globalOptions = (globalOptionsPath && (await fs.exists(globalOptionsPath))
|
|
3823
|
-
? JSON.parse((await fs.readFile(globalOptionsPath)).toString())
|
|
3824
|
-
: {});
|
|
3825
|
-
const localPkgDir = await packageDirectory();
|
|
3826
|
-
const localOptionsPath = localPkgDir
|
|
3827
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
3828
|
-
: undefined;
|
|
3829
|
-
const localOptions = (localOptionsPath &&
|
|
3830
|
-
localOptionsPath !== globalOptionsPath &&
|
|
3831
|
-
(await fs.exists(localOptionsPath))
|
|
3832
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
3833
|
-
: {});
|
|
3834
|
-
// Merge order: base < global < local < custom
|
|
3835
|
-
const merged = defaultsDeep(baseOptions, globalOptions, localOptions, customOptions);
|
|
3836
|
-
return merged;
|
|
3837
|
-
};
|
|
3838
|
-
|
|
3839
|
-
/**
|
|
3840
|
-
* Resolve dotenv values using the config-loader/overlay path (always-on in
|
|
3841
|
-
* host/generator flows; no-op when no config files are present).
|
|
3842
|
-
*
|
|
3843
|
-
* Order:
|
|
3844
|
-
* 1) Compute base from files only (exclude dynamic; ignore programmatic vars).
|
|
3845
|
-
* 2) Discover packaged + project config sources and overlay onto base.
|
|
3846
|
-
* 3) Apply dynamics in order:
|
|
3847
|
-
* programmatic dynamic \> config dynamic (packaged → project public → project local)
|
|
3848
|
-
* \> file dynamicPath.
|
|
3849
|
-
* 4) Phase C interpolation of remaining string options (e.g., outputPath).
|
|
3850
|
-
* 5) Optionally write outputPath, log, and merge into process.env.
|
|
3851
|
-
*/
|
|
3852
|
-
const resolveDotenvWithConfigLoader = async (validated) => {
|
|
3853
|
-
// 1) Base from files, no dynamic, no programmatic vars
|
|
3854
|
-
const base = await getDotenv({
|
|
3855
|
-
...validated,
|
|
3856
|
-
// Build a pure base without side effects or logging.
|
|
3857
|
-
excludeDynamic: true,
|
|
3858
|
-
vars: {},
|
|
3859
|
-
log: false,
|
|
3860
|
-
loadProcess: false,
|
|
3861
|
-
outputPath: undefined,
|
|
3862
|
-
});
|
|
3863
|
-
// 2) Discover config sources (packaged via this module's import.meta.url)
|
|
3864
|
-
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
3865
|
-
const dotenv = overlayEnv({
|
|
3866
|
-
base,
|
|
3867
|
-
env: validated.env ?? validated.defaultEnv,
|
|
3868
|
-
configs: sources,
|
|
3869
|
-
...(validated.vars ? { programmaticVars: validated.vars } : {}),
|
|
3870
|
-
});
|
|
3871
|
-
// Helper to apply a dynamic map progressively.
|
|
3872
|
-
const applyDynamic = (target, dynamic, env) => {
|
|
3873
|
-
if (!dynamic)
|
|
3874
|
-
return;
|
|
3875
|
-
for (const key of Object.keys(dynamic)) {
|
|
3876
|
-
const value = typeof dynamic[key] === 'function'
|
|
3877
|
-
? dynamic[key](target, env)
|
|
3878
|
-
: dynamic[key];
|
|
3879
|
-
Object.assign(target, { [key]: value });
|
|
3880
|
-
}
|
|
3881
|
-
};
|
|
3882
|
-
// 3) Apply dynamics in order
|
|
3883
|
-
applyDynamic(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
|
|
3884
|
-
applyDynamic(dotenv, (sources.packaged?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
3885
|
-
applyDynamic(dotenv, (sources.project?.public?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
3886
|
-
applyDynamic(dotenv, (sources.project?.local?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
3887
|
-
// file dynamicPath (lowest)
|
|
3888
|
-
if (validated.dynamicPath) {
|
|
3889
|
-
const absDynamicPath = path.resolve(validated.dynamicPath);
|
|
3890
|
-
try {
|
|
3891
|
-
const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
|
|
3892
|
-
applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
|
|
3893
|
-
}
|
|
3894
|
-
catch {
|
|
3895
|
-
throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
|
|
3896
|
-
}
|
|
3897
|
-
}
|
|
3898
|
-
// 4) Phase C: interpolate remaining string options (exclude bootstrap set).
|
|
3899
|
-
// For now, interpolate outputPath only; bootstrap keys are excluded by design.
|
|
3900
|
-
const envRef = { ...process.env, ...dotenv };
|
|
3901
|
-
const outputPathInterpolated = typeof validated.outputPath === 'string'
|
|
3902
|
-
? interpolateDeep(validated.outputPath, envRef)
|
|
3903
|
-
: undefined;
|
|
3904
|
-
// 5) Output/log/process merge (use interpolated outputPath if present)
|
|
3905
|
-
if (outputPathInterpolated) {
|
|
3906
|
-
await fs.writeFile(outputPathInterpolated, Object.keys(dotenv).reduce((contents, key) => {
|
|
3907
|
-
const value = dotenv[key] ?? '';
|
|
3908
|
-
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
3909
|
-
}, ''), { encoding: 'utf-8' });
|
|
3910
|
-
}
|
|
3911
|
-
const logger = validated.logger ?? console;
|
|
3912
|
-
if (validated.log) {
|
|
3913
|
-
const redactFlag = validated.redact ?? false;
|
|
3914
|
-
const redactPatterns = validated.redactPatterns;
|
|
3915
|
-
const redOpts = {};
|
|
3916
|
-
if (redactFlag)
|
|
3917
|
-
redOpts.redact = true;
|
|
3918
|
-
if (redactFlag && Array.isArray(redactPatterns))
|
|
3919
|
-
redOpts.redactPatterns = redactPatterns;
|
|
3920
|
-
const bag = redactFlag ? redactObject(dotenv, redOpts) : { ...dotenv };
|
|
3921
|
-
logger.log(bag);
|
|
3922
|
-
// Entropy warnings: once per key per run (presentation only)
|
|
3923
|
-
const warnEntropyVal = validated.warnEntropy ?? true;
|
|
3924
|
-
const entropyThresholdVal = validated.entropyThreshold;
|
|
3925
|
-
const entropyMinLengthVal = validated.entropyMinLength;
|
|
3926
|
-
const entropyWhitelistVal = validated.entropyWhitelist;
|
|
3927
|
-
const entOpts = {};
|
|
3928
|
-
// include keys only when defined to satisfy exactOptionalPropertyTypes
|
|
3929
|
-
if (typeof warnEntropyVal === 'boolean')
|
|
3930
|
-
entOpts.warnEntropy = warnEntropyVal;
|
|
3931
|
-
if (typeof entropyThresholdVal === 'number')
|
|
3932
|
-
entOpts.entropyThreshold = entropyThresholdVal;
|
|
3933
|
-
if (typeof entropyMinLengthVal === 'number')
|
|
3934
|
-
entOpts.entropyMinLength = entropyMinLengthVal;
|
|
3935
|
-
if (Array.isArray(entropyWhitelistVal))
|
|
3936
|
-
entOpts.entropyWhitelist = entropyWhitelistVal;
|
|
3937
|
-
for (const [k, v] of Object.entries(dotenv)) {
|
|
3938
|
-
maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
|
|
3939
|
-
logger.log(line);
|
|
3940
|
-
});
|
|
3941
|
-
}
|
|
3942
|
-
}
|
|
3943
|
-
if (validated.loadProcess)
|
|
3944
|
-
Object.assign(process.env, dotenv);
|
|
3945
|
-
return dotenv;
|
|
3946
|
-
};
|
|
3947
|
-
|
|
3948
|
-
/**
|
|
3949
|
-
* Omit a "logger" key from an options object in a typed manner.
|
|
3950
|
-
*/
|
|
3951
|
-
const omitLogger = (obj) => {
|
|
3952
|
-
const { logger: _omitted, ...rest } = obj;
|
|
3953
|
-
return rest;
|
|
3954
|
-
};
|
|
3955
|
-
/**
|
|
3956
|
-
* Build the Commander preSubcommand hook using the provided context.
|
|
3957
|
-
* * Responsibilities:
|
|
3958
|
-
* - Merge parent CLI options with current invocation (parent \< current). * - Resolve tri-state flags, including `--exclude-all` overrides.
|
|
3959
|
-
* - Normalize the shell setting to a concrete value (string | boolean).
|
|
3960
|
-
* - Persist merged options on the command instance and pass to subcommands.
|
|
3961
|
-
* - Execute {@link getDotenv} and optional post-hook.
|
|
3962
|
-
* - Either forward to the default `cmd` subcommand or execute `--command`.
|
|
3963
|
-
*
|
|
3964
|
-
* @param context - See {@link PreSubHookContext}.
|
|
3965
|
-
* @returns An async hook suitable for Commander’s `preSubcommand`.
|
|
3966
|
-
*
|
|
3967
|
-
* @example `program.hook('preSubcommand', makePreSubcommandHook(ctx));`
|
|
3968
|
-
*/
|
|
3969
|
-
const makePreSubcommandHook = ({ logger, preHook, postHook, defaults, }) => {
|
|
3970
|
-
return async (thisCommand) => {
|
|
3971
|
-
// Get raw CLI options from commander.
|
|
3972
|
-
const rawCliOptions = thisCommand.opts();
|
|
3973
|
-
const { merged: mergedGetDotenvCliOptions, command: commandOpt } = resolveCliOptions(rawCliOptions, defaults, process.env.getDotenvCliOptions);
|
|
3974
|
-
// Optional debug logging retained via mergedGetDotenvCliOptions.debug if desired. // Execute pre-hook.
|
|
3975
|
-
if (preHook) {
|
|
3976
|
-
await preHook(mergedGetDotenvCliOptions);
|
|
3977
|
-
if (mergedGetDotenvCliOptions.debug)
|
|
3978
|
-
logger.debug('\n*** GetDotenvCliOptions after pre-hook ***\n', mergedGetDotenvCliOptions);
|
|
3979
|
-
}
|
|
3980
|
-
// Persist GetDotenvCliOptions in command for subcommand access.
|
|
3981
|
-
thisCommand.getDotenvCliOptions =
|
|
3982
|
-
mergedGetDotenvCliOptions;
|
|
3983
|
-
// Execute getdotenv via always-on config loader/overlay path.
|
|
3984
|
-
const serviceOptions = getDotenvCliOptions2Options(mergedGetDotenvCliOptions);
|
|
3985
|
-
const dotenv = await resolveDotenvWithConfigLoader(serviceOptions);
|
|
3986
|
-
// Global validation against config (warn by default; --strict fails).
|
|
3987
|
-
try {
|
|
3988
|
-
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
3989
|
-
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
3990
|
-
if (Array.isArray(issues) && issues.length > 0) {
|
|
3991
|
-
issues.forEach((m) => {
|
|
3992
|
-
logger.error(m);
|
|
3993
|
-
});
|
|
3994
|
-
if (mergedGetDotenvCliOptions.strict) {
|
|
3995
|
-
process.exit(1);
|
|
3996
|
-
}
|
|
3997
|
-
}
|
|
3998
|
-
}
|
|
3999
|
-
catch {
|
|
4000
|
-
// Tolerate validator failures in non-strict mode
|
|
4001
|
-
}
|
|
4002
|
-
// Execute post-hook.
|
|
4003
|
-
if (postHook)
|
|
4004
|
-
await postHook(dotenv); // Execute command.
|
|
4005
|
-
const args = thisCommand.args ?? [];
|
|
4006
|
-
const isCommand = typeof commandOpt === 'string' && commandOpt.length > 0;
|
|
4007
|
-
if (isCommand && args.length > 0) {
|
|
4008
|
-
const lr = logger;
|
|
4009
|
-
(lr.error ?? lr.log)(`--command option conflicts with cmd subcommand.`);
|
|
4010
|
-
process.exit(0);
|
|
4011
|
-
}
|
|
4012
|
-
if (typeof commandOpt === 'string' && commandOpt.length > 0) {
|
|
4013
|
-
const cmd = resolveCommand(mergedGetDotenvCliOptions.scripts, commandOpt);
|
|
4014
|
-
if (mergedGetDotenvCliOptions.debug)
|
|
4015
|
-
logger.debug('\n*** command ***\n', cmd);
|
|
4016
|
-
// Build a logger-free bag for env round-trip.
|
|
4017
|
-
const envSafe = omitLogger(mergedGetDotenvCliOptions);
|
|
4018
|
-
await execaCommand(cmd, {
|
|
4019
|
-
env: { ...process.env, getDotenvCliOptions: JSON.stringify(envSafe) },
|
|
4020
|
-
shell: resolveShell(mergedGetDotenvCliOptions.scripts, commandOpt, mergedGetDotenvCliOptions.shell),
|
|
4021
|
-
stdio: 'inherit',
|
|
4022
|
-
});
|
|
4023
|
-
}
|
|
4024
|
-
};
|
|
4025
|
-
};
|
|
4026
|
-
|
|
4027
|
-
/**
|
|
4028
|
-
* Generate a Commander CLI Command for get-dotenv.
|
|
4029
|
-
* Orchestration only: delegates building and lifecycle hooks.
|
|
4030
|
-
*/
|
|
4031
|
-
const generateGetDotenvCli = async (customOptions) => {
|
|
4032
|
-
const options = await resolveGetDotenvCliGenerateOptions(customOptions);
|
|
4033
|
-
const program = createRootCommand(options);
|
|
4034
|
-
const defaults = {};
|
|
4035
|
-
if (options.debug !== undefined)
|
|
4036
|
-
defaults.debug = options.debug;
|
|
4037
|
-
if (options.excludeDynamic !== undefined)
|
|
4038
|
-
defaults.excludeDynamic = options.excludeDynamic;
|
|
4039
|
-
if (options.excludeEnv !== undefined)
|
|
4040
|
-
defaults.excludeEnv = options.excludeEnv;
|
|
4041
|
-
if (options.excludeGlobal !== undefined)
|
|
4042
|
-
defaults.excludeGlobal = options.excludeGlobal;
|
|
4043
|
-
if (options.excludePrivate !== undefined)
|
|
4044
|
-
defaults.excludePrivate = options.excludePrivate;
|
|
4045
|
-
if (options.excludePublic !== undefined)
|
|
4046
|
-
defaults.excludePublic = options.excludePublic;
|
|
4047
|
-
if (options.loadProcess !== undefined)
|
|
4048
|
-
defaults.loadProcess = options.loadProcess;
|
|
4049
|
-
if (options.log !== undefined)
|
|
4050
|
-
defaults.log = options.log;
|
|
4051
|
-
if (options.scripts !== undefined)
|
|
4052
|
-
defaults.scripts = options.scripts;
|
|
4053
|
-
if (options.shell !== undefined)
|
|
4054
|
-
defaults.shell = options.shell;
|
|
4055
|
-
const ctx = {
|
|
4056
|
-
logger: options.logger,
|
|
4057
|
-
defaults,
|
|
4058
|
-
...(options.preHook ? { preHook: options.preHook } : {}),
|
|
4059
|
-
...(options.postHook ? { postHook: options.postHook } : {}),
|
|
4060
|
-
};
|
|
4061
|
-
program.hook('preSubcommand', makePreSubcommandHook(ctx));
|
|
4062
|
-
return program;
|
|
4063
|
-
};
|
|
4064
|
-
|
|
4065
3937
|
function createCli(opts = {}) {
|
|
4066
3938
|
const alias = typeof opts.alias === 'string' && opts.alias.length > 0
|
|
4067
3939
|
? opts.alias
|
|
4068
3940
|
: 'getdotenv';
|
|
4069
3941
|
const program = new GetDotenvCli(alias);
|
|
3942
|
+
// Normalize Commander output so help prints always end with a blank line.
|
|
3943
|
+
// This keeps E2E assertions (CRLF and >=2 trailing newlines) portable across
|
|
3944
|
+
// runtimes and capture modes without altering Commander internals.
|
|
3945
|
+
const outputCfg = {
|
|
3946
|
+
writeOut(str) {
|
|
3947
|
+
const txt = typeof str === 'string' ? str : '';
|
|
3948
|
+
const hasTwo = /(?:\r?\n){2,}$/.test(txt);
|
|
3949
|
+
const hasOne = /\r?\n$/.test(txt);
|
|
3950
|
+
const out = hasTwo ? txt : hasOne ? txt + '\n' : txt + '\n\n';
|
|
3951
|
+
try {
|
|
3952
|
+
process.stdout.write(out);
|
|
3953
|
+
}
|
|
3954
|
+
catch {
|
|
3955
|
+
/* ignore */
|
|
3956
|
+
}
|
|
3957
|
+
},
|
|
3958
|
+
writeErr(str) {
|
|
3959
|
+
process.stderr.write(str);
|
|
3960
|
+
},
|
|
3961
|
+
};
|
|
3962
|
+
// Apply to root and recursively to subcommands so all help paths are normalized.
|
|
3963
|
+
program.configureOutput(outputCfg);
|
|
3964
|
+
const applyOutputRecursively = (cmd) => {
|
|
3965
|
+
cmd.configureOutput(outputCfg);
|
|
3966
|
+
const kids = cmd.commands ?? [];
|
|
3967
|
+
for (const child of kids)
|
|
3968
|
+
applyOutputRecursively(child);
|
|
3969
|
+
};
|
|
3970
|
+
applyOutputRecursively(program);
|
|
4070
3971
|
// Install base root flags and included plugins; resolve context once per run.
|
|
4071
3972
|
program
|
|
4072
3973
|
.attachRootOptions({ loadProcess: false })
|
|
@@ -4082,19 +3983,72 @@ function createCli(opts = {}) {
|
|
|
4082
3983
|
if (underTests) {
|
|
4083
3984
|
program.exitOverride((err) => {
|
|
4084
3985
|
const code = err?.code;
|
|
4085
|
-
|
|
3986
|
+
// Commander printed help already; ensure a trailing blank line for tests/CI capture.
|
|
3987
|
+
if (code === 'commander.helpDisplayed') {
|
|
3988
|
+
try {
|
|
3989
|
+
process.stdout.write('\n');
|
|
3990
|
+
}
|
|
3991
|
+
catch {
|
|
3992
|
+
/* ignore */
|
|
3993
|
+
}
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
if (code === 'commander.version') {
|
|
4086
3997
|
return;
|
|
3998
|
+
}
|
|
4087
3999
|
throw err;
|
|
4088
4000
|
});
|
|
4089
4001
|
}
|
|
4090
4002
|
return {
|
|
4091
4003
|
async run(argv) {
|
|
4092
|
-
//
|
|
4093
|
-
//
|
|
4094
|
-
//
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4004
|
+
// Help handling:
|
|
4005
|
+
// - Short-circuit ONLY for true top-level -h/--help (no subcommand before flag).
|
|
4006
|
+
// - If a subcommand token appears before -h/--help, defer to Commander
|
|
4007
|
+
// to render that subcommand's help.
|
|
4008
|
+
const helpIdx = argv.findIndex((a) => a === '-h' || a === '--help');
|
|
4009
|
+
if (helpIdx >= 0) {
|
|
4010
|
+
// Build a set of known subcommand names/aliases on the root.
|
|
4011
|
+
const subs = new Set();
|
|
4012
|
+
const cmds = program.commands ?? [];
|
|
4013
|
+
for (const c of cmds) {
|
|
4014
|
+
subs.add(c.name());
|
|
4015
|
+
for (const a of c.aliases())
|
|
4016
|
+
subs.add(a);
|
|
4017
|
+
}
|
|
4018
|
+
const hasSubBeforeHelp = argv
|
|
4019
|
+
.slice(0, helpIdx)
|
|
4020
|
+
.some((tok) => subs.has(tok));
|
|
4021
|
+
if (!hasSubBeforeHelp) {
|
|
4022
|
+
await program.brand({
|
|
4023
|
+
name: alias,
|
|
4024
|
+
importMetaUrl: import.meta.url,
|
|
4025
|
+
description: 'Base CLI.',
|
|
4026
|
+
...(typeof opts.branding === 'string' && opts.branding.length > 0
|
|
4027
|
+
? { helpHeader: opts.branding }
|
|
4028
|
+
: {}),
|
|
4029
|
+
});
|
|
4030
|
+
// Resolve context once without side effects for help rendering.
|
|
4031
|
+
const ctx = await program.resolveAndLoad({
|
|
4032
|
+
loadProcess: false,
|
|
4033
|
+
log: false,
|
|
4034
|
+
}, { runAfterResolve: false });
|
|
4035
|
+
program.evaluateDynamicOptions({
|
|
4036
|
+
...ctx.optionsResolved,
|
|
4037
|
+
plugins: ctx.pluginConfigs ?? {},
|
|
4038
|
+
});
|
|
4039
|
+
// Suppress output only during unit tests; allow E2E to capture.
|
|
4040
|
+
const piping = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
4041
|
+
process.env.GETDOTENV_STDOUT === 'pipe';
|
|
4042
|
+
if (underTests && !piping) {
|
|
4043
|
+
void program.helpInformation();
|
|
4044
|
+
}
|
|
4045
|
+
else {
|
|
4046
|
+
program.outputHelp();
|
|
4047
|
+
}
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
// Subcommand token exists before -h: fall through to normal parsing,
|
|
4051
|
+
// letting Commander print that subcommand's help.
|
|
4098
4052
|
}
|
|
4099
4053
|
await program.brand({
|
|
4100
4054
|
name: alias,
|
|
@@ -4109,4 +4063,4 @@ function createCli(opts = {}) {
|
|
|
4109
4063
|
};
|
|
4110
4064
|
}
|
|
4111
4065
|
|
|
4112
|
-
export { buildSpawnEnv, createCli, defineDynamic, dotenvExpand, dotenvExpandAll, dotenvExpandFromProcessEnv,
|
|
4066
|
+
export { buildSpawnEnv, createCli, defineDynamic, dotenvExpand, dotenvExpandAll, dotenvExpandFromProcessEnv, getDotenv, getDotenvCliOptions2Options, interpolateDeep };
|