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