@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/getdotenv.cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command, Option } from 'commander';
|
|
3
2
|
import fs from 'fs-extra';
|
|
4
3
|
import { packageDirectory } from 'package-directory';
|
|
5
|
-
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
6
4
|
import path, { join, extname } from 'path';
|
|
7
|
-
import {
|
|
5
|
+
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
8
6
|
import YAML from 'yaml';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { Option, Command } from 'commander';
|
|
9
9
|
import { nanoid } from 'nanoid';
|
|
10
10
|
import { parse } from 'dotenv';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
@@ -41,8 +41,6 @@ const baseRootOptionDefaults = {
|
|
|
41
41
|
// (debug/log/exclude* resolved via flag utils)
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
45
|
-
|
|
46
44
|
/** @internal */
|
|
47
45
|
const isPlainObject$1 = (value) => value !== null &&
|
|
48
46
|
typeof value === 'object' &&
|
|
@@ -85,134 +83,130 @@ const defaultsDeep = (...layers) => {
|
|
|
85
83
|
return result;
|
|
86
84
|
};
|
|
87
85
|
|
|
88
|
-
// src/GetDotenvOptions.ts
|
|
89
|
-
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
90
86
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
87
|
+
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
88
|
+
* - If the user explicitly enabled the flag, return true.
|
|
89
|
+
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
90
|
+
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
93
91
|
*
|
|
94
|
-
* @
|
|
92
|
+
* @param exclude - The "on" flag value as parsed by Commander.
|
|
93
|
+
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
94
|
+
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
95
|
+
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* resolveExclusion(undefined, undefined, true); // => true
|
|
100
|
+
* ```
|
|
95
101
|
*/
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
* The result preserves explicit empty values and drops only `undefined`.
|
|
156
|
-
*
|
|
157
|
-
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* ```ts
|
|
161
|
-
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
162
|
-
* ```
|
|
163
|
-
*/
|
|
164
|
-
const localPkgDir = await packageDirectory();
|
|
165
|
-
const localOptionsPath = localPkgDir
|
|
166
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
167
|
-
: undefined;
|
|
168
|
-
const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
|
|
169
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
170
|
-
: {});
|
|
171
|
-
// Merge order: base < local < custom (custom has highest precedence)
|
|
172
|
-
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
173
|
-
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
174
|
-
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
175
|
-
return {
|
|
176
|
-
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
177
|
-
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
178
|
-
};
|
|
102
|
+
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Resolve an optional flag with "--exclude-all" overrides.
|
|
105
|
+
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
106
|
+
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
107
|
+
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
108
|
+
*
|
|
109
|
+
* @param exclude - Individual include/exclude flag.
|
|
110
|
+
* @param excludeOff - Individual "...-off" flag.
|
|
111
|
+
* @param defaultValue - Default for the individual flag.
|
|
112
|
+
* @param excludeAll - Global "exclude-all" flag.
|
|
113
|
+
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
|
|
117
|
+
*/
|
|
118
|
+
const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
|
|
119
|
+
// Order of precedence:
|
|
120
|
+
// 1) Individual explicit "on" wins outright.
|
|
121
|
+
// 2) Individual explicit "off" wins over any global.
|
|
122
|
+
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
123
|
+
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
124
|
+
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
125
|
+
(() => {
|
|
126
|
+
// Individual "on"
|
|
127
|
+
if (exclude === true)
|
|
128
|
+
return true;
|
|
129
|
+
// Individual "off"
|
|
130
|
+
if (excludeOff === true)
|
|
131
|
+
return undefined;
|
|
132
|
+
// Global "exclude-all" ON (unless explicitly turned off)
|
|
133
|
+
if (excludeAll === true)
|
|
134
|
+
return true;
|
|
135
|
+
// Global "exclude-all-off" (unless explicitly enabled)
|
|
136
|
+
if (excludeAllOff === true)
|
|
137
|
+
return undefined;
|
|
138
|
+
// Default
|
|
139
|
+
return defaultValue ? true : undefined;
|
|
140
|
+
})();
|
|
141
|
+
/**
|
|
142
|
+
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
143
|
+
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
144
|
+
*
|
|
145
|
+
* @typeParam T - Target object type.
|
|
146
|
+
* @param obj - The object to write to.
|
|
147
|
+
* @param key - The optional boolean property key of {@link T}.
|
|
148
|
+
* @param value - The value to set or `undefined` to unset.
|
|
149
|
+
*
|
|
150
|
+
* @remarks
|
|
151
|
+
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
152
|
+
*/
|
|
153
|
+
const setOptionalFlag = (obj, key, value) => {
|
|
154
|
+
const target = obj;
|
|
155
|
+
const k = key;
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
157
|
+
if (value === undefined)
|
|
158
|
+
delete target[k];
|
|
159
|
+
else
|
|
160
|
+
target[k] = value;
|
|
179
161
|
};
|
|
180
162
|
|
|
181
163
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
186
|
-
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
187
|
-
* later per the staged plan.
|
|
164
|
+
* Merge and normalize raw Commander options (current + parent + defaults)
|
|
165
|
+
* into a GetDotenvCliOptions-like object. Types are intentionally wide to
|
|
166
|
+
* avoid cross-layer coupling; callers may cast as needed.
|
|
188
167
|
*/
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
168
|
+
const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
169
|
+
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
170
|
+
? JSON.parse(parentJson)
|
|
171
|
+
: undefined;
|
|
172
|
+
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
173
|
+
const current = { ...rest };
|
|
174
|
+
if (typeof scripts === 'string') {
|
|
175
|
+
try {
|
|
176
|
+
current.scripts = JSON.parse(scripts);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// ignore parse errors; leave scripts undefined
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
183
|
+
const d = defaults;
|
|
184
|
+
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
185
|
+
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
186
|
+
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
187
|
+
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
188
|
+
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
189
|
+
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
190
|
+
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
191
|
+
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
192
|
+
// warnEntropy (tri-state)
|
|
193
|
+
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
194
|
+
// Normalize shell for predictability: explicit default shell per OS.
|
|
195
|
+
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
196
|
+
let resolvedShell = merged.shell;
|
|
197
|
+
if (shellOff)
|
|
198
|
+
resolvedShell = false;
|
|
199
|
+
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
200
|
+
resolvedShell = defaultShell;
|
|
201
|
+
}
|
|
202
|
+
else if (typeof resolvedShell !== 'string' &&
|
|
203
|
+
typeof defaults.shell === 'string') {
|
|
204
|
+
resolvedShell = defaults.shell;
|
|
205
|
+
}
|
|
206
|
+
merged.shell = resolvedShell;
|
|
207
|
+
const cmd = typeof command === 'string' ? command : undefined;
|
|
208
|
+
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
209
|
+
};
|
|
216
210
|
|
|
217
211
|
/**
|
|
218
212
|
* Zod schemas for configuration files discovered by the new loader.
|
|
@@ -451,6 +445,169 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
|
451
445
|
return result;
|
|
452
446
|
};
|
|
453
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Validate a composed env against config-provided validation surfaces.
|
|
450
|
+
* Precedence for validation definitions:
|
|
451
|
+
* project.local -\> project.public -\> packaged
|
|
452
|
+
*
|
|
453
|
+
* Behavior:
|
|
454
|
+
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
455
|
+
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
456
|
+
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
457
|
+
*/
|
|
458
|
+
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
459
|
+
const pick = (getter) => {
|
|
460
|
+
const pl = sources.project?.local;
|
|
461
|
+
const pp = sources.project?.public;
|
|
462
|
+
const pk = sources.packaged;
|
|
463
|
+
return ((pl && getter(pl)) ||
|
|
464
|
+
(pp && getter(pp)) ||
|
|
465
|
+
(pk && getter(pk)) ||
|
|
466
|
+
undefined);
|
|
467
|
+
};
|
|
468
|
+
const schema = pick((cfg) => cfg['schema']);
|
|
469
|
+
if (schema &&
|
|
470
|
+
typeof schema.safeParse === 'function') {
|
|
471
|
+
try {
|
|
472
|
+
const parsed = schema.safeParse(finalEnv);
|
|
473
|
+
if (!parsed.success) {
|
|
474
|
+
// Try to render zod-style issues when available.
|
|
475
|
+
const err = parsed.error;
|
|
476
|
+
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
477
|
+
? err.issues.map((i) => {
|
|
478
|
+
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
479
|
+
const msg = i.message ?? 'Invalid value';
|
|
480
|
+
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
481
|
+
})
|
|
482
|
+
: ['[schema] validation failed'];
|
|
483
|
+
return issues;
|
|
484
|
+
}
|
|
485
|
+
return [];
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// If schema invocation fails, surface a single diagnostic.
|
|
489
|
+
return [
|
|
490
|
+
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
495
|
+
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
496
|
+
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
497
|
+
if (missing.length > 0) {
|
|
498
|
+
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return [];
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
505
|
+
|
|
506
|
+
// src/GetDotenvOptions.ts
|
|
507
|
+
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
508
|
+
/**
|
|
509
|
+
* Converts programmatic CLI options to `getDotenv` options. *
|
|
510
|
+
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
511
|
+
*
|
|
512
|
+
* @returns `getDotenv` options.
|
|
513
|
+
*/
|
|
514
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
515
|
+
/**
|
|
516
|
+
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
517
|
+
*
|
|
518
|
+
* - 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`
|
|
519
|
+
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
520
|
+
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
521
|
+
*
|
|
522
|
+
* @remarks
|
|
523
|
+
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
524
|
+
*/
|
|
525
|
+
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
526
|
+
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
527
|
+
const restObj = { ...rest };
|
|
528
|
+
delete restObj.debug;
|
|
529
|
+
delete restObj.scripts;
|
|
530
|
+
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
531
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
532
|
+
let parsedVars;
|
|
533
|
+
if (typeof vars === 'string') {
|
|
534
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
535
|
+
? RegExp(varsAssignorPattern)
|
|
536
|
+
: (varsAssignor ?? '=')));
|
|
537
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
538
|
+
}
|
|
539
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
540
|
+
// Keep only string or undefined values to match ProcessEnv.
|
|
541
|
+
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
542
|
+
parsedVars = Object.fromEntries(entries);
|
|
543
|
+
}
|
|
544
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
545
|
+
// expectations and the compat test assertions.
|
|
546
|
+
if (parsedVars) {
|
|
547
|
+
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
548
|
+
}
|
|
549
|
+
// Tolerate paths as either a delimited string or string[]
|
|
550
|
+
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
551
|
+
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
552
|
+
const pathsAny = paths;
|
|
553
|
+
const pathsOut = Array.isArray(pathsAny)
|
|
554
|
+
? pathsAny.filter((p) => typeof p === 'string')
|
|
555
|
+
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
556
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
557
|
+
return {
|
|
558
|
+
...restObj,
|
|
559
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
560
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
561
|
+
};
|
|
562
|
+
};
|
|
563
|
+
const resolveGetDotenvOptions = async (customOptions) => {
|
|
564
|
+
/**
|
|
565
|
+
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
566
|
+
*
|
|
567
|
+
* 1. Base defaults derived from the CLI generator defaults
|
|
568
|
+
* ({@link baseGetDotenvCliOptions}).
|
|
569
|
+
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
570
|
+
* package root (if present).
|
|
571
|
+
* 3. The provided {@link customOptions}.
|
|
572
|
+
*
|
|
573
|
+
* The result preserves explicit empty values and drops only `undefined`.
|
|
574
|
+
*
|
|
575
|
+
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* ```ts
|
|
579
|
+
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
const localPkgDir = await packageDirectory();
|
|
583
|
+
const localOptionsPath = localPkgDir
|
|
584
|
+
? join(localPkgDir, getDotenvOptionsFilename)
|
|
585
|
+
: undefined;
|
|
586
|
+
// Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
|
|
587
|
+
let localOptions = {};
|
|
588
|
+
if (localOptionsPath && (await fs.exists(localOptionsPath))) {
|
|
589
|
+
try {
|
|
590
|
+
const txt = await fs.readFile(localOptionsPath, 'utf-8');
|
|
591
|
+
const parsed = JSON.parse(txt);
|
|
592
|
+
if (parsed && typeof parsed === 'object') {
|
|
593
|
+
localOptions = parsed;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// Malformed or unreadable local options are treated as absent.
|
|
598
|
+
localOptions = {};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Merge order: base < local < custom (custom has highest precedence)
|
|
602
|
+
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
603
|
+
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
604
|
+
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
605
|
+
return {
|
|
606
|
+
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
607
|
+
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
|
|
454
611
|
/**
|
|
455
612
|
* Dotenv expansion utilities.
|
|
456
613
|
*
|
|
@@ -571,21 +728,231 @@ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduc
|
|
|
571
728
|
return acc;
|
|
572
729
|
}, {});
|
|
573
730
|
/**
|
|
574
|
-
* Recursively expands environment variables in a string using `process.env` as
|
|
575
|
-
* the expansion reference. Variables may be presented with optional default as
|
|
576
|
-
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
577
|
-
* empty string.
|
|
578
|
-
*
|
|
579
|
-
* @param value - The string to expand.
|
|
580
|
-
* @returns The expanded string.
|
|
731
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
732
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
733
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
734
|
+
* empty string.
|
|
735
|
+
*
|
|
736
|
+
* @param value - The string to expand.
|
|
737
|
+
* @returns The expanded string.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```ts
|
|
741
|
+
* process.env.FOO = 'bar';
|
|
742
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
746
|
+
|
|
747
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
748
|
+
/**
|
|
749
|
+
* Attach root flags to a GetDotenvCli instance.
|
|
750
|
+
* - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
|
|
751
|
+
* - Any flag that displays an effective default in help uses dynamic descriptions.
|
|
752
|
+
*/
|
|
753
|
+
const attachRootOptions = (program, defaults, opts) => {
|
|
754
|
+
// Install temporary wrappers to tag all options added here as "base" for grouped help.
|
|
755
|
+
const GROUP = 'base';
|
|
756
|
+
const tagLatest = (cmd, group) => {
|
|
757
|
+
const optsArr = cmd.options;
|
|
758
|
+
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
759
|
+
const last = optsArr[optsArr.length - 1];
|
|
760
|
+
last.__group = group;
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
const originalAddOption = program.addOption.bind(program);
|
|
764
|
+
const originalOption = program.option.bind(program);
|
|
765
|
+
program.addOption = function patchedAdd(opt) {
|
|
766
|
+
opt.__group = GROUP;
|
|
767
|
+
return originalAddOption(opt);
|
|
768
|
+
};
|
|
769
|
+
program.option = function patchedOption(...args) {
|
|
770
|
+
const ret = originalOption(...args);
|
|
771
|
+
tagLatest(this, GROUP);
|
|
772
|
+
return ret;
|
|
773
|
+
};
|
|
774
|
+
const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
775
|
+
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
776
|
+
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
777
|
+
// Helper: append (default) tags for ON/OFF toggles
|
|
778
|
+
const onOff = (on, isDefault) => on
|
|
779
|
+
? `ON${isDefault ? ' (default)' : ''}`
|
|
780
|
+
: `OFF${isDefault ? ' (default)' : ''}`;
|
|
781
|
+
let p = program
|
|
782
|
+
.enablePositionalOptions()
|
|
783
|
+
.passThroughOptions()
|
|
784
|
+
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
785
|
+
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
786
|
+
['KEY1', 'VAL1'],
|
|
787
|
+
['KEY2', 'VAL2'],
|
|
788
|
+
]
|
|
789
|
+
.map((v) => v.join(va))
|
|
790
|
+
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
791
|
+
if (opts?.includeCommandOption === true) {
|
|
792
|
+
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
793
|
+
}
|
|
794
|
+
// Output path (interpolated later; help can remain static)
|
|
795
|
+
p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
|
|
796
|
+
// Shell ON (string or boolean true => default shell)
|
|
797
|
+
p = p
|
|
798
|
+
.addOption(program
|
|
799
|
+
.createDynamicOption('-s, --shell [string]', (cfg) => {
|
|
800
|
+
const s = cfg.shell;
|
|
801
|
+
let tag = '';
|
|
802
|
+
if (typeof s === 'boolean' && s)
|
|
803
|
+
tag = ' (default OS shell)';
|
|
804
|
+
else if (typeof s === 'string' && s.length > 0)
|
|
805
|
+
tag = ` (default ${s})`;
|
|
806
|
+
return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
|
|
807
|
+
})
|
|
808
|
+
.conflicts('shellOff'))
|
|
809
|
+
// Shell OFF
|
|
810
|
+
.addOption(program
|
|
811
|
+
.createDynamicOption('-S, --shell-off', (cfg) => {
|
|
812
|
+
const s = cfg.shell;
|
|
813
|
+
return `command execution shell OFF${s === false ? ' (default)' : ''}`;
|
|
814
|
+
})
|
|
815
|
+
.conflicts('shell'));
|
|
816
|
+
// Load process ON/OFF (dynamic defaults)
|
|
817
|
+
p = p
|
|
818
|
+
.addOption(program
|
|
819
|
+
.createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
|
|
820
|
+
.conflicts('loadProcessOff'))
|
|
821
|
+
.addOption(program
|
|
822
|
+
.createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
|
|
823
|
+
.conflicts('loadProcess'));
|
|
824
|
+
// Exclusion master toggle (dynamic)
|
|
825
|
+
p = p
|
|
826
|
+
.addOption(program
|
|
827
|
+
.createDynamicOption('-a, --exclude-all', (cfg) => {
|
|
828
|
+
const c = cfg;
|
|
829
|
+
const allOn = !!c.excludeDynamic &&
|
|
830
|
+
((!!c.excludeEnv && !!c.excludeGlobal) ||
|
|
831
|
+
(!!c.excludePrivate && !!c.excludePublic));
|
|
832
|
+
const suffix = allOn ? ' (default)' : '';
|
|
833
|
+
return `exclude all dotenv variables from loading ON${suffix}`;
|
|
834
|
+
})
|
|
835
|
+
.conflicts('excludeAllOff'))
|
|
836
|
+
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
|
|
837
|
+
// Per-family exclusions (dynamic defaults)
|
|
838
|
+
p = p
|
|
839
|
+
.addOption(program
|
|
840
|
+
.createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
|
|
841
|
+
.conflicts('excludeDynamicOff'))
|
|
842
|
+
.addOption(program
|
|
843
|
+
.createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
|
|
844
|
+
.conflicts('excludeDynamic'))
|
|
845
|
+
.addOption(program
|
|
846
|
+
.createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
|
|
847
|
+
.conflicts('excludeEnvOff'))
|
|
848
|
+
.addOption(program
|
|
849
|
+
.createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
|
|
850
|
+
.conflicts('excludeEnv'))
|
|
851
|
+
.addOption(program
|
|
852
|
+
.createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
|
|
853
|
+
.conflicts('excludeGlobalOff'))
|
|
854
|
+
.addOption(program
|
|
855
|
+
.createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
|
|
856
|
+
.conflicts('excludeGlobal'))
|
|
857
|
+
.addOption(program
|
|
858
|
+
.createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
|
|
859
|
+
.conflicts('excludePrivateOff'))
|
|
860
|
+
.addOption(program
|
|
861
|
+
.createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
|
|
862
|
+
.conflicts('excludePrivate'))
|
|
863
|
+
.addOption(program
|
|
864
|
+
.createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
|
|
865
|
+
.conflicts('excludePublicOff'))
|
|
866
|
+
.addOption(program
|
|
867
|
+
.createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
|
|
868
|
+
.conflicts('excludePublic'));
|
|
869
|
+
// Log ON/OFF (dynamic)
|
|
870
|
+
p = p
|
|
871
|
+
.addOption(program
|
|
872
|
+
.createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
|
|
873
|
+
.conflicts('logOff'))
|
|
874
|
+
.addOption(program
|
|
875
|
+
.createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
|
|
876
|
+
.conflicts('log'));
|
|
877
|
+
// Capture flag (no default display; static)
|
|
878
|
+
p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
|
|
879
|
+
// Core bootstrap/static flags (kept static in help)
|
|
880
|
+
p = p
|
|
881
|
+
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
882
|
+
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
883
|
+
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
884
|
+
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
885
|
+
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
886
|
+
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
887
|
+
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
888
|
+
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
889
|
+
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
890
|
+
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
891
|
+
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
892
|
+
// Hidden scripts pipe-through (stringified)
|
|
893
|
+
.addOption(new Option('--scripts <string>')
|
|
894
|
+
.default(JSON.stringify(scripts))
|
|
895
|
+
.hideHelp());
|
|
896
|
+
// Diagnostics / validation / entropy
|
|
897
|
+
p = p
|
|
898
|
+
.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
|
|
899
|
+
.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
900
|
+
p = p
|
|
901
|
+
.addOption(program
|
|
902
|
+
.createDynamicOption('--entropy-warn', (cfg) => {
|
|
903
|
+
const warn = cfg.warnEntropy;
|
|
904
|
+
// Default is effectively ON when warnEntropy is true or undefined.
|
|
905
|
+
return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
|
|
906
|
+
})
|
|
907
|
+
.conflicts('entropyWarnOff'))
|
|
908
|
+
.addOption(program
|
|
909
|
+
.createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
|
|
910
|
+
.conflicts('entropyWarn'))
|
|
911
|
+
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
912
|
+
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
913
|
+
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
914
|
+
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
915
|
+
// Restore original methods
|
|
916
|
+
program.addOption = originalAddOption;
|
|
917
|
+
program.option = originalOption;
|
|
918
|
+
return p;
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Zod schemas for programmatic GetDotenv options.
|
|
581
923
|
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* ```
|
|
924
|
+
* NOTE: These schemas are introduced without wiring to avoid behavior changes.
|
|
925
|
+
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
926
|
+
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
927
|
+
* later per the staged plan.
|
|
587
928
|
*/
|
|
588
|
-
|
|
929
|
+
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
930
|
+
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
931
|
+
// RAW: all fields optional — undefined means "inherit" from lower layers.
|
|
932
|
+
const getDotenvOptionsSchemaRaw = z.object({
|
|
933
|
+
defaultEnv: z.string().optional(),
|
|
934
|
+
dotenvToken: z.string().optional(),
|
|
935
|
+
dynamicPath: z.string().optional(),
|
|
936
|
+
// Dynamic map is intentionally wide for now; refine once sources are normalized.
|
|
937
|
+
dynamic: z.record(z.string(), z.unknown()).optional(),
|
|
938
|
+
env: z.string().optional(),
|
|
939
|
+
excludeDynamic: z.boolean().optional(),
|
|
940
|
+
excludeEnv: z.boolean().optional(),
|
|
941
|
+
excludeGlobal: z.boolean().optional(),
|
|
942
|
+
excludePrivate: z.boolean().optional(),
|
|
943
|
+
excludePublic: z.boolean().optional(),
|
|
944
|
+
loadProcess: z.boolean().optional(),
|
|
945
|
+
log: z.boolean().optional(),
|
|
946
|
+
outputPath: z.string().optional(),
|
|
947
|
+
paths: z.array(z.string()).optional(),
|
|
948
|
+
privateToken: z.string().optional(),
|
|
949
|
+
vars: processEnvSchema.optional(),
|
|
950
|
+
// Host-only feature flag: guarded integration of config loader/overlay
|
|
951
|
+
useConfigLoader: z.boolean().optional(),
|
|
952
|
+
});
|
|
953
|
+
// RESOLVED: service-boundary contract (post-inheritance).
|
|
954
|
+
// For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
|
|
955
|
+
const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
|
|
589
956
|
|
|
590
957
|
const applyKv = (current, kv) => {
|
|
591
958
|
if (!kv || Object.keys(kv).length === 0)
|
|
@@ -1184,6 +1551,8 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1184
1551
|
};
|
|
1185
1552
|
};
|
|
1186
1553
|
|
|
1554
|
+
// Dynamic help support: attach a private symbol to Option for description fns.
|
|
1555
|
+
const DYN_DESC_SYM = Symbol('getdotenv.dynamic.description');
|
|
1187
1556
|
const HOST_META_URL = import.meta.url;
|
|
1188
1557
|
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
1189
1558
|
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
@@ -1199,13 +1568,20 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
|
1199
1568
|
*
|
|
1200
1569
|
* NOTE: This host is additive and does not alter the legacy CLI.
|
|
1201
1570
|
*/
|
|
1202
|
-
class GetDotenvCli extends Command {
|
|
1571
|
+
let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
1203
1572
|
/** Registered top-level plugins (composition happens via .use()) */
|
|
1204
1573
|
_plugins = [];
|
|
1205
1574
|
/** One-time installation guard */
|
|
1206
1575
|
_installed = false;
|
|
1207
1576
|
/** Optional header line to prepend in help output */
|
|
1208
1577
|
[HELP_HEADER_SYMBOL];
|
|
1578
|
+
/**
|
|
1579
|
+
* Create a subcommand using the same subclass, preserving helpers like
|
|
1580
|
+
* dynamicOption on children.
|
|
1581
|
+
*/
|
|
1582
|
+
createCommand(name) {
|
|
1583
|
+
return new this.constructor(name);
|
|
1584
|
+
}
|
|
1209
1585
|
constructor(alias = 'getdotenv') {
|
|
1210
1586
|
super(alias);
|
|
1211
1587
|
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
@@ -1213,15 +1589,18 @@ class GetDotenvCli extends Command {
|
|
|
1213
1589
|
// child uses passThroughOptions.
|
|
1214
1590
|
this.enablePositionalOptions();
|
|
1215
1591
|
// Configure grouped help: show only base options in default "Options";
|
|
1216
|
-
//
|
|
1592
|
+
// we will insert App/Plugin sections before Commands in helpInformation().
|
|
1217
1593
|
this.configureHelp({
|
|
1218
1594
|
visibleOptions: (cmd) => {
|
|
1219
|
-
const all = cmd.options ??
|
|
1220
|
-
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1595
|
+
const all = cmd.options ?? [];
|
|
1596
|
+
const parent = cmd.parent ?? null;
|
|
1597
|
+
const isRoot = parent === null;
|
|
1598
|
+
const list = isRoot
|
|
1599
|
+
? all.filter((opt) => {
|
|
1600
|
+
const group = opt.__group;
|
|
1601
|
+
return group === 'base';
|
|
1602
|
+
})
|
|
1603
|
+
: all.slice(); // subcommands: show all options (their own "Options:" block)
|
|
1225
1604
|
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1226
1605
|
const hasShort = (opt) => {
|
|
1227
1606
|
const flags = opt.flags ?? '';
|
|
@@ -1229,19 +1608,18 @@ class GetDotenvCli extends Command {
|
|
|
1229
1608
|
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1230
1609
|
};
|
|
1231
1610
|
const byFlags = (opt) => opt.flags ?? '';
|
|
1232
|
-
|
|
1611
|
+
list.sort((a, b) => {
|
|
1233
1612
|
const aS = hasShort(a) ? 1 : 0;
|
|
1234
1613
|
const bS = hasShort(b) ? 1 : 0;
|
|
1235
1614
|
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1236
1615
|
});
|
|
1237
|
-
return
|
|
1616
|
+
return list;
|
|
1238
1617
|
},
|
|
1239
1618
|
});
|
|
1240
1619
|
this.addHelpText('beforeAll', () => {
|
|
1241
1620
|
const header = this[HELP_HEADER_SYMBOL];
|
|
1242
1621
|
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1243
1622
|
});
|
|
1244
|
-
this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
|
|
1245
1623
|
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
1246
1624
|
// mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
|
|
1247
1625
|
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
@@ -1253,9 +1631,15 @@ class GetDotenvCli extends Command {
|
|
|
1253
1631
|
});
|
|
1254
1632
|
}
|
|
1255
1633
|
/**
|
|
1256
|
-
* Resolve options (strict) and compute dotenv context.
|
|
1634
|
+
* Resolve options (strict) and compute dotenv context.
|
|
1635
|
+
* Stores the context on the instance under a symbol.
|
|
1636
|
+
*
|
|
1637
|
+
* Options:
|
|
1638
|
+
* - opts.runAfterResolve (default true): when false, skips running plugin
|
|
1639
|
+
* afterResolve hooks. Useful for top-level help rendering to avoid
|
|
1640
|
+
* long-running side-effects while still evaluating dynamic help text.
|
|
1257
1641
|
*/
|
|
1258
|
-
async resolveAndLoad(customOptions = {}) {
|
|
1642
|
+
async resolveAndLoad(customOptions = {}, opts) {
|
|
1259
1643
|
// Resolve defaults, then validate strictly under the new host.
|
|
1260
1644
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1261
1645
|
getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
@@ -1266,9 +1650,64 @@ class GetDotenvCli extends Command {
|
|
|
1266
1650
|
ctx;
|
|
1267
1651
|
// Ensure plugins are installed exactly once, then run afterResolve.
|
|
1268
1652
|
await this.install();
|
|
1269
|
-
|
|
1653
|
+
if (opts?.runAfterResolve ?? true) {
|
|
1654
|
+
await this._runAfterResolve(ctx);
|
|
1655
|
+
}
|
|
1270
1656
|
return ctx;
|
|
1271
1657
|
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Create a Commander Option that computes its description at help time.
|
|
1660
|
+
* The returned Option may be configured (conflicts, default, parser) and
|
|
1661
|
+
* added via addOption().
|
|
1662
|
+
*/
|
|
1663
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
1664
|
+
createDynamicOption(flags, desc, parser, defaultValue) {
|
|
1665
|
+
const opt = new Option(flags, '');
|
|
1666
|
+
// Keep the function on a private symbol so it survives through Commander.
|
|
1667
|
+
opt[DYN_DESC_SYM] = desc;
|
|
1668
|
+
if (parser)
|
|
1669
|
+
opt.argParser(parser);
|
|
1670
|
+
if (defaultValue !== undefined)
|
|
1671
|
+
opt.default(defaultValue);
|
|
1672
|
+
return opt;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Chainable helper mirroring .option(), but with a dynamic description.
|
|
1676
|
+
* Equivalent to addOption(createDynamicOption(...)).
|
|
1677
|
+
*/
|
|
1678
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
1679
|
+
dynamicOption(flags, desc, parser, defaultValue) {
|
|
1680
|
+
const opt = this.createDynamicOption(flags, desc, parser, defaultValue);
|
|
1681
|
+
this.addOption(opt);
|
|
1682
|
+
return this;
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Evaluate dynamic descriptions for this command and all descendants using
|
|
1686
|
+
* the provided resolved configuration. Mutates the Option.description in
|
|
1687
|
+
* place so Commander help renders updated text.
|
|
1688
|
+
*/
|
|
1689
|
+
evaluateDynamicOptions(resolved) {
|
|
1690
|
+
const visit = (cmd) => {
|
|
1691
|
+
const arr = cmd.options ?? [];
|
|
1692
|
+
for (const o of arr) {
|
|
1693
|
+
const dyn = o[DYN_DESC_SYM];
|
|
1694
|
+
if (typeof dyn === 'function') {
|
|
1695
|
+
try {
|
|
1696
|
+
const txt = dyn(resolved);
|
|
1697
|
+
// Commander Option has a public "description" field used by help.
|
|
1698
|
+
o.description = txt;
|
|
1699
|
+
}
|
|
1700
|
+
catch {
|
|
1701
|
+
// Best-effort: leave description as-is on evaluation failure.
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const children = cmd.commands ?? [];
|
|
1706
|
+
for (const c of children)
|
|
1707
|
+
visit(c);
|
|
1708
|
+
};
|
|
1709
|
+
visit(this);
|
|
1710
|
+
}
|
|
1272
1711
|
/**
|
|
1273
1712
|
* Retrieve the current invocation context (if any).
|
|
1274
1713
|
*/
|
|
@@ -1298,6 +1737,7 @@ class GetDotenvCli extends Command {
|
|
|
1298
1737
|
tagAppOptions(fn) {
|
|
1299
1738
|
const root = this;
|
|
1300
1739
|
const originalAddOption = root.addOption.bind(root);
|
|
1740
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1301
1741
|
const originalOption = root.option.bind(root);
|
|
1302
1742
|
const tagLatest = (cmd, group) => {
|
|
1303
1743
|
const optsArr = cmd.options;
|
|
@@ -1310,6 +1750,7 @@ class GetDotenvCli extends Command {
|
|
|
1310
1750
|
opt.__group = 'app';
|
|
1311
1751
|
return originalAddOption(opt);
|
|
1312
1752
|
};
|
|
1753
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1313
1754
|
root.option = function patchedOption(...args) {
|
|
1314
1755
|
const ret = originalOption(...args);
|
|
1315
1756
|
tagLatest(this, 'app');
|
|
@@ -1320,6 +1761,7 @@ class GetDotenvCli extends Command {
|
|
|
1320
1761
|
}
|
|
1321
1762
|
finally {
|
|
1322
1763
|
root.addOption = originalAddOption;
|
|
1764
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1323
1765
|
root.option = originalOption;
|
|
1324
1766
|
}
|
|
1325
1767
|
}
|
|
@@ -1365,6 +1807,40 @@ class GetDotenvCli extends Command {
|
|
|
1365
1807
|
}
|
|
1366
1808
|
return this;
|
|
1367
1809
|
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Insert grouped plugin/app options between "Options" and "Commands" for
|
|
1812
|
+
* hybrid ordering. Applies to root and any parent command.
|
|
1813
|
+
*/
|
|
1814
|
+
helpInformation() {
|
|
1815
|
+
// Base help text first (includes beforeAll/after hooks).
|
|
1816
|
+
const base = super.helpInformation();
|
|
1817
|
+
const groups = this.#renderOptionGroups(this);
|
|
1818
|
+
const block = typeof groups === 'string' ? groups.trim() : '';
|
|
1819
|
+
let out = base;
|
|
1820
|
+
if (!block) {
|
|
1821
|
+
// Ensure a trailing blank line even when no extra groups render.
|
|
1822
|
+
if (!out.endsWith('\n\n'))
|
|
1823
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1824
|
+
return out;
|
|
1825
|
+
}
|
|
1826
|
+
// Insert just before "Commands:" when present.
|
|
1827
|
+
const marker = '\nCommands:';
|
|
1828
|
+
const idx = base.indexOf(marker);
|
|
1829
|
+
if (idx >= 0) {
|
|
1830
|
+
const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
|
|
1831
|
+
out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
// Otherwise append.
|
|
1835
|
+
const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
|
|
1836
|
+
out = `${base}${sep}${groups}`;
|
|
1837
|
+
}
|
|
1838
|
+
// Ensure a trailing blank line for prompt separation.
|
|
1839
|
+
if (!out.endsWith('\n\n')) {
|
|
1840
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1841
|
+
}
|
|
1842
|
+
return out;
|
|
1843
|
+
}
|
|
1368
1844
|
/**
|
|
1369
1845
|
* Register a plugin for installation (parent level).
|
|
1370
1846
|
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
@@ -1403,7 +1879,7 @@ class GetDotenvCli extends Command {
|
|
|
1403
1879
|
for (const p of this._plugins)
|
|
1404
1880
|
await run(p);
|
|
1405
1881
|
}
|
|
1406
|
-
// Render App/Plugin grouped options
|
|
1882
|
+
// Render App/Plugin grouped options (used by helpInformation override).
|
|
1407
1883
|
#renderOptionGroups(cmd) {
|
|
1408
1884
|
const all = cmd.options ?? [];
|
|
1409
1885
|
const byGroup = new Map();
|
|
@@ -1443,371 +1919,98 @@ class GetDotenvCli extends Command {
|
|
|
1443
1919
|
if (app && app.length > 0) {
|
|
1444
1920
|
out += renderRows('App options', app);
|
|
1445
1921
|
}
|
|
1446
|
-
// Plugin groups sorted by id
|
|
1447
|
-
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
*
|
|
1465
|
-
* Behavior:
|
|
1466
|
-
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
1467
|
-
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
1468
|
-
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
1469
|
-
*/
|
|
1470
|
-
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
1471
|
-
const pick = (getter) => {
|
|
1472
|
-
const pl = sources.project?.local;
|
|
1473
|
-
const pp = sources.project?.public;
|
|
1474
|
-
const pk = sources.packaged;
|
|
1475
|
-
return ((pl && getter(pl)) ||
|
|
1476
|
-
(pp && getter(pp)) ||
|
|
1477
|
-
(pk && getter(pk)) ||
|
|
1478
|
-
undefined);
|
|
1479
|
-
};
|
|
1480
|
-
const schema = pick((cfg) => cfg['schema']);
|
|
1481
|
-
if (schema &&
|
|
1482
|
-
typeof schema.safeParse === 'function') {
|
|
1483
|
-
try {
|
|
1484
|
-
const parsed = schema.safeParse(finalEnv);
|
|
1485
|
-
if (!parsed.success) {
|
|
1486
|
-
// Try to render zod-style issues when available.
|
|
1487
|
-
const err = parsed.error;
|
|
1488
|
-
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
1489
|
-
? err.issues.map((i) => {
|
|
1490
|
-
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1491
|
-
const msg = i.message ?? 'Invalid value';
|
|
1492
|
-
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
1493
|
-
})
|
|
1494
|
-
: ['[schema] validation failed'];
|
|
1495
|
-
return issues;
|
|
1496
|
-
}
|
|
1497
|
-
return [];
|
|
1498
|
-
}
|
|
1499
|
-
catch {
|
|
1500
|
-
// If schema invocation fails, surface a single diagnostic.
|
|
1501
|
-
return [
|
|
1502
|
-
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
1503
|
-
];
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
1507
|
-
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
1508
|
-
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
1509
|
-
if (missing.length > 0) {
|
|
1510
|
-
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
return [];
|
|
1514
|
-
};
|
|
1515
|
-
|
|
1516
|
-
/**
|
|
1517
|
-
* Attach legacy root flags to a Commander program.
|
|
1518
|
-
* Uses provided defaults to render help labels without coupling to generators.
|
|
1519
|
-
*/
|
|
1520
|
-
const attachRootOptions = (program, defaults, opts) => {
|
|
1521
|
-
// Install temporary wrappers to tag all options added here as "base".
|
|
1522
|
-
const GROUP = 'base';
|
|
1523
|
-
const tagLatest = (cmd, group) => {
|
|
1524
|
-
const optsArr = cmd.options;
|
|
1525
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
1526
|
-
const last = optsArr[optsArr.length - 1];
|
|
1527
|
-
last.__group = group;
|
|
1528
|
-
}
|
|
1529
|
-
};
|
|
1530
|
-
const originalAddOption = program.addOption.bind(program);
|
|
1531
|
-
const originalOption = program.option.bind(program);
|
|
1532
|
-
program.addOption = function patchedAdd(opt) {
|
|
1533
|
-
// Tag before adding, in case consumers inspect the Option directly.
|
|
1534
|
-
opt.__group = GROUP;
|
|
1535
|
-
const ret = originalAddOption(opt);
|
|
1536
|
-
return ret;
|
|
1537
|
-
};
|
|
1538
|
-
program.option = function patchedOption(...args) {
|
|
1539
|
-
const ret = originalOption(...args);
|
|
1540
|
-
tagLatest(this, GROUP);
|
|
1541
|
-
return ret;
|
|
1542
|
-
};
|
|
1543
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
1544
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
1545
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
1546
|
-
// Build initial chain.
|
|
1547
|
-
let p = program
|
|
1548
|
-
.enablePositionalOptions()
|
|
1549
|
-
.passThroughOptions()
|
|
1550
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
1551
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
1552
|
-
['KEY1', 'VAL1'],
|
|
1553
|
-
['KEY2', 'VAL2'],
|
|
1554
|
-
]
|
|
1555
|
-
.map((v) => v.join(va))
|
|
1556
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
1557
|
-
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
1558
|
-
// Default is OFF; the generator opts in explicitly.
|
|
1559
|
-
if (opts?.includeCommandOption === true) {
|
|
1560
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
1561
|
-
}
|
|
1562
|
-
p = p
|
|
1563
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
1564
|
-
.addOption(new Option('-s, --shell [string]', (() => {
|
|
1565
|
-
let defaultLabel = '';
|
|
1566
|
-
if (shell !== undefined) {
|
|
1567
|
-
if (typeof shell === 'boolean') {
|
|
1568
|
-
defaultLabel = ' (default OS shell)';
|
|
1569
|
-
}
|
|
1570
|
-
else if (typeof shell === 'string') {
|
|
1571
|
-
// Safe string interpolation
|
|
1572
|
-
defaultLabel = ` (default ${shell})`;
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
1576
|
-
})()).conflicts('shellOff'))
|
|
1577
|
-
.addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
1578
|
-
.addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
1579
|
-
.addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
1580
|
-
.addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
1581
|
-
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
1582
|
-
? ' (default)'
|
|
1583
|
-
: ''}`).conflicts('excludeAllOff'))
|
|
1584
|
-
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
1585
|
-
.addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
1586
|
-
.addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
1587
|
-
.addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
1588
|
-
.addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
1589
|
-
.addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
1590
|
-
.addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
1591
|
-
.addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
1592
|
-
.addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
1593
|
-
.addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
1594
|
-
.addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
1595
|
-
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
1596
|
-
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
1597
|
-
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
1598
|
-
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
1599
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
1600
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
1601
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
1602
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
1603
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
1604
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
1605
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
1606
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
1607
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
1608
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
1609
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
1610
|
-
// Hidden scripts pipe-through (stringified)
|
|
1611
|
-
.addOption(new Option('--scripts <string>')
|
|
1612
|
-
.default(JSON.stringify(scripts))
|
|
1613
|
-
.hideHelp());
|
|
1614
|
-
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
1615
|
-
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
1616
|
-
// Validation: strict mode fails on env validation issues (warn by default).
|
|
1617
|
-
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
1618
|
-
// Entropy diagnostics (presentation-only)
|
|
1619
|
-
p = p
|
|
1620
|
-
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
1621
|
-
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
1622
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
1623
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
1624
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
1625
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
1626
|
-
// Restore original methods to avoid tagging future additions outside base.
|
|
1627
|
-
program.addOption = originalAddOption;
|
|
1628
|
-
program.option = originalOption;
|
|
1629
|
-
return p;
|
|
1630
|
-
};
|
|
1631
|
-
|
|
1632
|
-
/**
|
|
1633
|
-
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
1634
|
-
* - If the user explicitly enabled the flag, return true.
|
|
1635
|
-
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
1636
|
-
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
1637
|
-
*
|
|
1638
|
-
* @param exclude - The "on" flag value as parsed by Commander.
|
|
1639
|
-
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
1640
|
-
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
1641
|
-
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
1642
|
-
*
|
|
1643
|
-
* @example
|
|
1644
|
-
* ```ts
|
|
1645
|
-
* resolveExclusion(undefined, undefined, true); // => true
|
|
1646
|
-
* ```
|
|
1647
|
-
*/
|
|
1648
|
-
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
1649
|
-
/**
|
|
1650
|
-
* Resolve an optional flag with "--exclude-all" overrides.
|
|
1651
|
-
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
1652
|
-
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
1653
|
-
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
1654
|
-
*
|
|
1655
|
-
* @param exclude - Individual include/exclude flag.
|
|
1656
|
-
* @param excludeOff - Individual "...-off" flag.
|
|
1657
|
-
* @param defaultValue - Default for the individual flag.
|
|
1658
|
-
* @param excludeAll - Global "exclude-all" flag.
|
|
1659
|
-
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
1922
|
+
// Plugin groups sorted by id
|
|
1923
|
+
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1924
|
+
const currentName = cmd.name?.() ?? '';
|
|
1925
|
+
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1926
|
+
for (const k of pluginKeys) {
|
|
1927
|
+
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1928
|
+
const rows = byGroup.get(k) ?? [];
|
|
1929
|
+
// Do not show a "Plugin options — <self>" section on the command that owns those options.
|
|
1930
|
+
// Only child-injected plugin groups should render at this level.
|
|
1931
|
+
if (rows.length > 0 && id !== currentName) {
|
|
1932
|
+
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return out;
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
/** src/cliHost/definePlugin.ts
|
|
1940
|
+
* Plugin contracts for the GetDotenv CLI host.
|
|
1660
1941
|
*
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
1942
|
+
* This module exposes a structural public interface for the host that plugins
|
|
1943
|
+
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
1944
|
+
* nominal class identity issues (private fields) in downstream consumers.
|
|
1663
1945
|
*/
|
|
1664
|
-
const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
|
|
1665
|
-
// Order of precedence:
|
|
1666
|
-
// 1) Individual explicit "on" wins outright.
|
|
1667
|
-
// 2) Individual explicit "off" wins over any global.
|
|
1668
|
-
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
1669
|
-
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
1670
|
-
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
1671
|
-
(() => {
|
|
1672
|
-
// Individual "on"
|
|
1673
|
-
if (exclude === true)
|
|
1674
|
-
return true;
|
|
1675
|
-
// Individual "off"
|
|
1676
|
-
if (excludeOff === true)
|
|
1677
|
-
return undefined;
|
|
1678
|
-
// Global "exclude-all" ON (unless explicitly turned off)
|
|
1679
|
-
if (excludeAll === true)
|
|
1680
|
-
return true;
|
|
1681
|
-
// Global "exclude-all-off" (unless explicitly enabled)
|
|
1682
|
-
if (excludeAllOff === true)
|
|
1683
|
-
return undefined;
|
|
1684
|
-
// Default
|
|
1685
|
-
return defaultValue ? true : undefined;
|
|
1686
|
-
})();
|
|
1687
1946
|
/**
|
|
1688
|
-
*
|
|
1689
|
-
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
1690
|
-
*
|
|
1691
|
-
* @typeParam T - Target object type.
|
|
1692
|
-
* @param obj - The object to write to.
|
|
1693
|
-
* @param key - The optional boolean property key of {@link T}.
|
|
1694
|
-
* @param value - The value to set or `undefined` to unset.
|
|
1947
|
+
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
1695
1948
|
*
|
|
1696
|
-
* @
|
|
1697
|
-
*
|
|
1949
|
+
* @example
|
|
1950
|
+
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
1951
|
+
* .use(childA)
|
|
1952
|
+
* .use(childB);
|
|
1698
1953
|
*/
|
|
1699
|
-
const
|
|
1700
|
-
const
|
|
1701
|
-
const
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1954
|
+
const definePlugin = (spec) => {
|
|
1955
|
+
const { children = [], ...rest } = spec;
|
|
1956
|
+
const plugin = {
|
|
1957
|
+
...rest,
|
|
1958
|
+
children: [...children],
|
|
1959
|
+
use(child) {
|
|
1960
|
+
this.children.push(child);
|
|
1961
|
+
return this;
|
|
1962
|
+
},
|
|
1963
|
+
};
|
|
1964
|
+
return plugin;
|
|
1707
1965
|
};
|
|
1708
1966
|
|
|
1709
1967
|
/**
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1968
|
+
* GetDotenvCli with root helpers as real class methods.
|
|
1969
|
+
* - attachRootOptions: installs legacy/base root flags on the command.
|
|
1970
|
+
* - passOptions: merges flags (parent \< current), computes dotenv context once,
|
|
1971
|
+
* runs validation, and persists merged options for nested flows.
|
|
1713
1972
|
*/
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
}
|
|
1724
|
-
catch {
|
|
1725
|
-
// ignore parse errors; leave scripts undefined
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
1729
|
-
const d = defaults;
|
|
1730
|
-
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
1731
|
-
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
1732
|
-
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
1733
|
-
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
1734
|
-
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
1735
|
-
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
1736
|
-
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
1737
|
-
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
1738
|
-
// warnEntropy (tri-state)
|
|
1739
|
-
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
1740
|
-
// Normalize shell for predictability: explicit default shell per OS.
|
|
1741
|
-
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
1742
|
-
let resolvedShell = merged.shell;
|
|
1743
|
-
if (shellOff)
|
|
1744
|
-
resolvedShell = false;
|
|
1745
|
-
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
1746
|
-
resolvedShell = defaultShell;
|
|
1747
|
-
}
|
|
1748
|
-
else if (typeof resolvedShell !== 'string' &&
|
|
1749
|
-
typeof defaults.shell === 'string') {
|
|
1750
|
-
resolvedShell = defaults.shell;
|
|
1973
|
+
class GetDotenvCli extends GetDotenvCli$1 {
|
|
1974
|
+
/**
|
|
1975
|
+
* Attach legacy root flags to this CLI instance. Defaults come from
|
|
1976
|
+
* baseRootOptionDefaults when none are provided.
|
|
1977
|
+
*/
|
|
1978
|
+
attachRootOptions(defaults, opts) {
|
|
1979
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
1980
|
+
attachRootOptions(this, d, opts);
|
|
1981
|
+
return this;
|
|
1751
1982
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
merged;
|
|
1770
|
-
// Also store on the host for downstream ergonomic accessors.
|
|
1771
|
-
this._setOptionsBag(merged);
|
|
1772
|
-
// Build service options and compute context (always-on config loader path).
|
|
1773
|
-
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1774
|
-
await this.resolveAndLoad(serviceOptions);
|
|
1775
|
-
// Global validation: once after Phase C using config sources.
|
|
1776
|
-
try {
|
|
1777
|
-
const ctx = this.getCtx();
|
|
1778
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
1779
|
-
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
1780
|
-
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1781
|
-
if (Array.isArray(issues) && issues.length > 0) {
|
|
1782
|
-
const logger = (merged.logger ??
|
|
1783
|
-
console);
|
|
1784
|
-
const emit = logger.error ?? logger.log;
|
|
1785
|
-
issues.forEach((m) => {
|
|
1786
|
-
emit(m);
|
|
1787
|
-
});
|
|
1788
|
-
if (merged.strict) {
|
|
1789
|
-
// Deterministic failure under strict mode
|
|
1790
|
-
process.exit(1);
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
catch {
|
|
1795
|
-
// Be tolerant: validation errors reported above; unexpected failures here
|
|
1796
|
-
// should not crash non-strict flows.
|
|
1797
|
-
}
|
|
1798
|
-
});
|
|
1799
|
-
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
1800
|
-
// with the same merged options and context without duplicating logic.
|
|
1801
|
-
this.hook('preAction', async (thisCommand) => {
|
|
1802
|
-
const raw = thisCommand.opts();
|
|
1803
|
-
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
1804
|
-
thisCommand.getDotenvCliOptions =
|
|
1805
|
-
merged;
|
|
1806
|
-
this._setOptionsBag(merged);
|
|
1807
|
-
// Avoid duplicate heavy work if a context is already present.
|
|
1808
|
-
if (!this.getCtx()) {
|
|
1983
|
+
/**
|
|
1984
|
+
* Install preSubcommand/preAction hooks that:
|
|
1985
|
+
* - Merge options (parent round-trip + current invocation) using resolveCliOptions.
|
|
1986
|
+
* - Persist the merged bag on the current command and on the host (for ergonomics).
|
|
1987
|
+
* - Compute the dotenv context once via resolveAndLoad(serviceOptions).
|
|
1988
|
+
* - Validate the composed env against discovered config (warn or --strict fail).
|
|
1989
|
+
*/
|
|
1990
|
+
passOptions(defaults) {
|
|
1991
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
1992
|
+
this.hook('preSubcommand', async (thisCommand) => {
|
|
1993
|
+
const raw = thisCommand.opts();
|
|
1994
|
+
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
1995
|
+
// Persist merged options (for nested behavior and ergonomic access).
|
|
1996
|
+
thisCommand.getDotenvCliOptions =
|
|
1997
|
+
merged;
|
|
1998
|
+
this._setOptionsBag(merged);
|
|
1999
|
+
// Build service options and compute context (always-on loader path).
|
|
1809
2000
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1810
2001
|
await this.resolveAndLoad(serviceOptions);
|
|
2002
|
+
// Refresh dynamic option descriptions using resolved config + plugin slices
|
|
2003
|
+
try {
|
|
2004
|
+
const ctx = this.getCtx();
|
|
2005
|
+
this.evaluateDynamicOptions({
|
|
2006
|
+
...ctx?.optionsResolved,
|
|
2007
|
+
plugins: ctx?.pluginConfigs ?? {},
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
catch {
|
|
2011
|
+
/* best-effort */
|
|
2012
|
+
}
|
|
2013
|
+
// Global validation: once after Phase C using config sources.
|
|
1811
2014
|
try {
|
|
1812
2015
|
const ctx = this.getCtx();
|
|
1813
2016
|
const dotenv = (ctx?.dotenv ?? {});
|
|
@@ -1826,12 +2029,56 @@ GetDotenvCli.prototype.passOptions = function (defaults) {
|
|
|
1826
2029
|
}
|
|
1827
2030
|
}
|
|
1828
2031
|
catch {
|
|
1829
|
-
//
|
|
2032
|
+
// Be tolerant: do not crash non-strict flows on unexpected validator failures.
|
|
1830
2033
|
}
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2034
|
+
});
|
|
2035
|
+
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
2036
|
+
// with the same merged options and context without duplicating logic.
|
|
2037
|
+
this.hook('preAction', async (thisCommand) => {
|
|
2038
|
+
const raw = thisCommand.opts();
|
|
2039
|
+
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
2040
|
+
thisCommand.getDotenvCliOptions =
|
|
2041
|
+
merged;
|
|
2042
|
+
this._setOptionsBag(merged);
|
|
2043
|
+
// Avoid duplicate heavy work if a context is already present.
|
|
2044
|
+
if (!this.getCtx()) {
|
|
2045
|
+
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
2046
|
+
await this.resolveAndLoad(serviceOptions);
|
|
2047
|
+
try {
|
|
2048
|
+
const ctx = this.getCtx();
|
|
2049
|
+
this.evaluateDynamicOptions({
|
|
2050
|
+
...ctx?.optionsResolved,
|
|
2051
|
+
plugins: ctx?.pluginConfigs ?? {},
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
catch {
|
|
2055
|
+
/* tolerate */
|
|
2056
|
+
}
|
|
2057
|
+
try {
|
|
2058
|
+
const ctx = this.getCtx();
|
|
2059
|
+
const dotenv = (ctx?.dotenv ?? {});
|
|
2060
|
+
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
2061
|
+
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
2062
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
2063
|
+
const logger = (merged
|
|
2064
|
+
.logger ?? console);
|
|
2065
|
+
const emit = logger.error ?? logger.log;
|
|
2066
|
+
issues.forEach((m) => {
|
|
2067
|
+
emit(m);
|
|
2068
|
+
});
|
|
2069
|
+
if (merged.strict) {
|
|
2070
|
+
process.exit(1);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
catch {
|
|
2075
|
+
// Tolerate validation side-effects in non-strict mode.
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
return this;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
1835
2082
|
|
|
1836
2083
|
// Minimal tokenizer for shell-off execution:
|
|
1837
2084
|
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
@@ -2066,34 +2313,6 @@ const buildSpawnEnv = (base, overlay) => {
|
|
|
2066
2313
|
return out;
|
|
2067
2314
|
};
|
|
2068
2315
|
|
|
2069
|
-
/** src/cliHost/definePlugin.ts
|
|
2070
|
-
* Plugin contracts for the GetDotenv CLI host.
|
|
2071
|
-
*
|
|
2072
|
-
* This module exposes a structural public interface for the host that plugins
|
|
2073
|
-
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
2074
|
-
* nominal class identity issues (private fields) in downstream consumers.
|
|
2075
|
-
*/
|
|
2076
|
-
/**
|
|
2077
|
-
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
2078
|
-
*
|
|
2079
|
-
* @example
|
|
2080
|
-
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
2081
|
-
* .use(childA)
|
|
2082
|
-
* .use(childB);
|
|
2083
|
-
*/
|
|
2084
|
-
const definePlugin = (spec) => {
|
|
2085
|
-
const { children = [], ...rest } = spec;
|
|
2086
|
-
const plugin = {
|
|
2087
|
-
...rest,
|
|
2088
|
-
children: [...children],
|
|
2089
|
-
use(child) {
|
|
2090
|
-
this.children.push(child);
|
|
2091
|
-
return this;
|
|
2092
|
-
},
|
|
2093
|
-
};
|
|
2094
|
-
return plugin;
|
|
2095
|
-
};
|
|
2096
|
-
|
|
2097
2316
|
/**
|
|
2098
2317
|
* Batch services (neutral): resolve command and shell settings.
|
|
2099
2318
|
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
@@ -2329,7 +2548,6 @@ const awsPlugin = () => definePlugin({
|
|
|
2329
2548
|
cli
|
|
2330
2549
|
.ns('aws')
|
|
2331
2550
|
.description('Establish an AWS session and optionally forward to the AWS CLI')
|
|
2332
|
-
.configureHelp({ showGlobalOptions: true })
|
|
2333
2551
|
.enablePositionalOptions()
|
|
2334
2552
|
.passThroughOptions()
|
|
2335
2553
|
.allowUnknownOption(true)
|
|
@@ -2520,9 +2738,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
|
2520
2738
|
}
|
|
2521
2739
|
return { absRootPath, paths };
|
|
2522
2740
|
};
|
|
2523
|
-
const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
2741
|
+
const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
2524
2742
|
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
2525
|
-
Boolean(getDotenvCliOptions?.capture);
|
|
2743
|
+
Boolean(getDotenvCliOptions?.capture);
|
|
2744
|
+
// Require a command only when not listing. In list mode, a command is optional.
|
|
2526
2745
|
if (!command && !list) {
|
|
2527
2746
|
logger.error(`No command provided. Use --command or --list.`);
|
|
2528
2747
|
process.exit(0);
|
|
@@ -2569,12 +2788,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
2569
2788
|
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
2570
2789
|
(Array.isArray(command) && command.length > 0);
|
|
2571
2790
|
if (hasCmd) {
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2791
|
+
// Compose child env overlay from dotenv (drop undefined) and merged options
|
|
2792
|
+
const overlay = {};
|
|
2793
|
+
if (dotenvEnv) {
|
|
2794
|
+
for (const [k, v] of Object.entries(dotenvEnv)) {
|
|
2795
|
+
if (typeof v === 'string')
|
|
2796
|
+
overlay[k] = v;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
if (getDotenvCliOptions !== undefined) {
|
|
2800
|
+
try {
|
|
2801
|
+
overlay.getDotenvCliOptions = JSON.stringify(getDotenvCliOptions);
|
|
2802
|
+
}
|
|
2803
|
+
catch {
|
|
2804
|
+
// best-effort: omit if serialization fails
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2575
2807
|
await runCommand(command, shell, {
|
|
2576
2808
|
cwd: path,
|
|
2577
|
-
env: buildSpawnEnv(process.env,
|
|
2809
|
+
env: buildSpawnEnv(process.env, overlay),
|
|
2578
2810
|
stdio: capture ? 'pipe' : 'inherit',
|
|
2579
2811
|
});
|
|
2580
2812
|
}
|
|
@@ -2612,6 +2844,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2612
2844
|
const ctx = cli.getCtx();
|
|
2613
2845
|
const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
|
|
2614
2846
|
const cfg = (cfgRaw || {});
|
|
2847
|
+
const dotenvEnv = (ctx?.dotenv ?? {});
|
|
2615
2848
|
// Resolve batch flags from the captured parent (batch) command.
|
|
2616
2849
|
const raw = batchCmd.opts();
|
|
2617
2850
|
const listFromParent = !!raw.list;
|
|
@@ -2630,6 +2863,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2630
2863
|
if (typeof commandOpt === 'string') {
|
|
2631
2864
|
await execShellCommandBatch({
|
|
2632
2865
|
command: resolveCommand(scripts, commandOpt),
|
|
2866
|
+
dotenvEnv,
|
|
2633
2867
|
globs,
|
|
2634
2868
|
ignoreErrors,
|
|
2635
2869
|
list: false,
|
|
@@ -2641,6 +2875,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2641
2875
|
return;
|
|
2642
2876
|
}
|
|
2643
2877
|
if (raw.list || localList) {
|
|
2878
|
+
const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
|
|
2644
2879
|
await execShellCommandBatch({
|
|
2645
2880
|
globs,
|
|
2646
2881
|
ignoreErrors,
|
|
@@ -2648,7 +2883,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2648
2883
|
logger: loggerLocal,
|
|
2649
2884
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2650
2885
|
rootPath,
|
|
2651
|
-
shell: (shell ?? false),
|
|
2886
|
+
shell: (shell ?? shellBag.shell ?? false),
|
|
2652
2887
|
});
|
|
2653
2888
|
return;
|
|
2654
2889
|
}
|
|
@@ -2715,6 +2950,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2715
2950
|
}
|
|
2716
2951
|
await execShellCommandBatch({
|
|
2717
2952
|
command: commandArg,
|
|
2953
|
+
dotenvEnv,
|
|
2718
2954
|
...(envBag ? { getDotenvCliOptions: envBag } : {}),
|
|
2719
2955
|
globs,
|
|
2720
2956
|
ignoreErrors,
|
|
@@ -2733,6 +2969,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2733
2969
|
const logger = opts.logger ?? console;
|
|
2734
2970
|
// Ensure context exists (host preSubcommand on root creates if missing).
|
|
2735
2971
|
const ctx = cli.getCtx();
|
|
2972
|
+
const dotenvEnv = (ctx?.dotenv ?? {});
|
|
2736
2973
|
const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
|
|
2737
2974
|
const cfg = (cfgRaw || {});
|
|
2738
2975
|
const raw = thisCommand.opts();
|
|
@@ -2755,6 +2992,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2755
2992
|
const commandArg = resolved;
|
|
2756
2993
|
await execShellCommandBatch({
|
|
2757
2994
|
command: commandArg,
|
|
2995
|
+
dotenvEnv,
|
|
2758
2996
|
globs,
|
|
2759
2997
|
ignoreErrors,
|
|
2760
2998
|
list: false,
|
|
@@ -2792,6 +3030,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2792
3030
|
const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
|
|
2793
3031
|
await execShellCommandBatch({
|
|
2794
3032
|
command: resolveCommand(scriptsOpt, commandOpt),
|
|
3033
|
+
dotenvEnv,
|
|
2795
3034
|
globs,
|
|
2796
3035
|
ignoreErrors,
|
|
2797
3036
|
list,
|
|
@@ -2835,7 +3074,8 @@ const BatchConfigSchema = z.object({
|
|
|
2835
3074
|
/**
|
|
2836
3075
|
* Batch plugin for the GetDotenv CLI host.
|
|
2837
3076
|
*
|
|
2838
|
-
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
3077
|
+
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
3078
|
+
* Options:
|
|
2839
3079
|
* - scripts/shell: used to resolve command and shell behavior per script or global default.
|
|
2840
3080
|
* - logger: defaults to console.
|
|
2841
3081
|
*/
|
|
@@ -2847,12 +3087,32 @@ const batchPlugin = (opts = {}) => definePlugin({
|
|
|
2847
3087
|
setup(cli) {
|
|
2848
3088
|
const ns = cli.ns('batch');
|
|
2849
3089
|
const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
|
|
3090
|
+
const host = cli;
|
|
3091
|
+
const pluginId = 'batch';
|
|
3092
|
+
const GROUP = `plugin:${pluginId}`;
|
|
2850
3093
|
ns.description('Batch command execution across multiple working directories.')
|
|
2851
3094
|
.enablePositionalOptions()
|
|
2852
3095
|
.passThroughOptions()
|
|
2853
|
-
|
|
2854
|
-
.
|
|
2855
|
-
.
|
|
3096
|
+
// Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
|
|
3097
|
+
.addOption((() => {
|
|
3098
|
+
const opt = host.createDynamicOption('-p, --pkg-cwd', (cfg) => {
|
|
3099
|
+
const slice = cfg.plugins.batch ?? {};
|
|
3100
|
+
const on = !!slice.pkgCwd;
|
|
3101
|
+
return `use nearest package directory as current working directory${on ? ' (default)' : ''}`;
|
|
3102
|
+
});
|
|
3103
|
+
opt.__group = GROUP;
|
|
3104
|
+
return opt;
|
|
3105
|
+
})())
|
|
3106
|
+
.addOption((() => {
|
|
3107
|
+
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 || './')})`);
|
|
3108
|
+
opt.__group = GROUP;
|
|
3109
|
+
return opt;
|
|
3110
|
+
})())
|
|
3111
|
+
.addOption((() => {
|
|
3112
|
+
const opt = host.createDynamicOption('-g, --globs <string>', (cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.plugins.batch?.globs || '*')})`);
|
|
3113
|
+
opt.__group = GROUP;
|
|
3114
|
+
return opt;
|
|
3115
|
+
})())
|
|
2856
3116
|
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
2857
3117
|
.option('-l, --list', 'list working directories without executing command')
|
|
2858
3118
|
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
@@ -3162,10 +3422,10 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
3162
3422
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
3163
3423
|
};
|
|
3164
3424
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
.
|
|
3168
|
-
.
|
|
3425
|
+
// Create as a GetDotenvCli child so helpInformation includes a trailing blank line.
|
|
3426
|
+
const cmd = cli
|
|
3427
|
+
.createCommand('cmd')
|
|
3428
|
+
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3169
3429
|
.enablePositionalOptions()
|
|
3170
3430
|
.passThroughOptions()
|
|
3171
3431
|
.argument('[command...]')
|
|
@@ -3357,7 +3617,7 @@ const demoPlugin = () => definePlugin({
|
|
|
3357
3617
|
const dotenv = (ctx?.dotenv ?? {});
|
|
3358
3618
|
// Inherit stdio for an interactive demo. Use --capture for CI.
|
|
3359
3619
|
await runCommand(['node', '-e', code], false, {
|
|
3360
|
-
env:
|
|
3620
|
+
env: buildSpawnEnv(process.env, dotenv),
|
|
3361
3621
|
stdio: 'inherit',
|
|
3362
3622
|
});
|
|
3363
3623
|
});
|
|
@@ -3394,20 +3654,23 @@ const demoPlugin = () => definePlugin({
|
|
|
3394
3654
|
const ctx = cli.getCtx();
|
|
3395
3655
|
const dotenv = (ctx?.dotenv ?? {});
|
|
3396
3656
|
await runCommand(resolved, shell, {
|
|
3397
|
-
env:
|
|
3657
|
+
env: buildSpawnEnv(process.env, dotenv),
|
|
3398
3658
|
stdio: 'inherit',
|
|
3399
3659
|
});
|
|
3400
3660
|
});
|
|
3401
3661
|
},
|
|
3402
3662
|
/**
|
|
3403
3663
|
* Optional: afterResolve can initialize per-plugin state using ctx.dotenv.
|
|
3404
|
-
* For the demo we
|
|
3664
|
+
* For the demo we emit a single breadcrumb only when GETDOTENV_DEBUG is set,
|
|
3665
|
+
* keeping default runs (tests/CI/smoke) quiet.
|
|
3405
3666
|
*/
|
|
3406
3667
|
afterResolve(_cli, ctx) {
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3668
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3669
|
+
const keys = Object.keys(ctx.dotenv);
|
|
3670
|
+
if (keys.length > 0) {
|
|
3671
|
+
// Keep noise low; a single-line breadcrumb is sufficient for the demo.
|
|
3672
|
+
console.error('[demo] afterResolve: dotenv keys loaded:', keys.length);
|
|
3673
|
+
}
|
|
3411
3674
|
}
|
|
3412
3675
|
},
|
|
3413
3676
|
});
|
|
@@ -3665,126 +3928,40 @@ const initPlugin = (opts = {}) => definePlugin({
|
|
|
3665
3928
|
},
|
|
3666
3929
|
});
|
|
3667
3930
|
|
|
3668
|
-
const cmdCommand = new Command()
|
|
3669
|
-
.name('cmd')
|
|
3670
|
-
.description('execute command, conflicts with --command option (default subcommand)')
|
|
3671
|
-
.enablePositionalOptions()
|
|
3672
|
-
.passThroughOptions()
|
|
3673
|
-
.argument('[command...]')
|
|
3674
|
-
.action(async (commandParts, _options, thisCommand) => {
|
|
3675
|
-
if (!thisCommand.parent)
|
|
3676
|
-
throw new Error(`unable to resolve parent command`);
|
|
3677
|
-
if (!thisCommand.parent.parent)
|
|
3678
|
-
throw new Error(`unable to resolve root command`);
|
|
3679
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent.parent;
|
|
3680
|
-
const raw = thisCommand.parent.opts();
|
|
3681
|
-
const ignoreErrors = !!raw.ignoreErrors;
|
|
3682
|
-
const globs = typeof raw.globs === 'string' ? raw.globs : '*';
|
|
3683
|
-
const list = !!raw.list;
|
|
3684
|
-
const pkgCwd = !!raw.pkgCwd;
|
|
3685
|
-
const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : './';
|
|
3686
|
-
// Execute command.
|
|
3687
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
3688
|
-
// When no positional tokens are provided (e.g., option form `-c/--command`),
|
|
3689
|
-
// the preSubcommand hook handles execution. Avoid a duplicate call here.
|
|
3690
|
-
if (args.length === 0)
|
|
3691
|
-
return;
|
|
3692
|
-
const command = args.map(String).join(' ');
|
|
3693
|
-
await execShellCommandBatch({
|
|
3694
|
-
command: resolveCommand(getDotenvCliOptions.scripts, command),
|
|
3695
|
-
getDotenvCliOptions,
|
|
3696
|
-
globs,
|
|
3697
|
-
ignoreErrors,
|
|
3698
|
-
list,
|
|
3699
|
-
logger,
|
|
3700
|
-
pkgCwd,
|
|
3701
|
-
rootPath,
|
|
3702
|
-
// execa expects string | boolean | URL for `shell`. We normalize earlier;
|
|
3703
|
-
// scripts[name].shell overrides take precedence and may be boolean or string.
|
|
3704
|
-
shell: resolveShell(getDotenvCliOptions.scripts, command, getDotenvCliOptions.shell),
|
|
3705
|
-
});
|
|
3706
|
-
});
|
|
3707
|
-
|
|
3708
|
-
new Command()
|
|
3709
|
-
.name('batch')
|
|
3710
|
-
.description('Batch command execution across multiple working directories.')
|
|
3711
|
-
.enablePositionalOptions()
|
|
3712
|
-
.passThroughOptions()
|
|
3713
|
-
.option('-p, --pkg-cwd', 'use nearest package directory as current working directory')
|
|
3714
|
-
.option('-r, --root-path <string>', 'path to batch root directory from current working directory', './')
|
|
3715
|
-
.option('-g, --globs <string>', 'space-delimited globs from root path', '*')
|
|
3716
|
-
.option('-c, --command <string>', 'command executed according to the base --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv)
|
|
3717
|
-
.option('-l, --list', 'list working directories without executing command')
|
|
3718
|
-
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
3719
|
-
.hook('preSubcommand', async (thisCommand) => {
|
|
3720
|
-
if (!thisCommand.parent)
|
|
3721
|
-
throw new Error(`unable to resolve root command`);
|
|
3722
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent;
|
|
3723
|
-
const raw = thisCommand.opts();
|
|
3724
|
-
const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
|
|
3725
|
-
const ignoreErrors = !!raw.ignoreErrors;
|
|
3726
|
-
const globs = typeof raw.globs === 'string' ? raw.globs : '*';
|
|
3727
|
-
const list = !!raw.list;
|
|
3728
|
-
const pkgCwd = !!raw.pkgCwd;
|
|
3729
|
-
const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : './';
|
|
3730
|
-
const argCount = thisCommand.args.length;
|
|
3731
|
-
if (typeof commandOpt === 'string' && argCount > 0) {
|
|
3732
|
-
logger.error(`--command option conflicts with cmd subcommand.`);
|
|
3733
|
-
process.exit(0);
|
|
3734
|
-
}
|
|
3735
|
-
// Execute command.
|
|
3736
|
-
if (typeof commandOpt === 'string')
|
|
3737
|
-
await execShellCommandBatch({
|
|
3738
|
-
command: resolveCommand(getDotenvCliOptions.scripts, commandOpt),
|
|
3739
|
-
getDotenvCliOptions,
|
|
3740
|
-
globs,
|
|
3741
|
-
ignoreErrors,
|
|
3742
|
-
list,
|
|
3743
|
-
logger,
|
|
3744
|
-
pkgCwd,
|
|
3745
|
-
rootPath,
|
|
3746
|
-
// execa expects string | boolean | URL for `shell`. We normalize earlier;
|
|
3747
|
-
// scripts[name].shell overrides take precedence and may be boolean or string.
|
|
3748
|
-
shell: resolveShell(getDotenvCliOptions.scripts, commandOpt, getDotenvCliOptions.shell),
|
|
3749
|
-
});
|
|
3750
|
-
})
|
|
3751
|
-
.addCommand(cmdCommand, { isDefault: true });
|
|
3752
|
-
|
|
3753
|
-
new Command()
|
|
3754
|
-
.name('cmd')
|
|
3755
|
-
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3756
|
-
.configureHelp({ showGlobalOptions: true })
|
|
3757
|
-
.enablePositionalOptions()
|
|
3758
|
-
.passThroughOptions()
|
|
3759
|
-
.argument('[command...]')
|
|
3760
|
-
.action(async (commandParts, _options, thisCommand) => {
|
|
3761
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
3762
|
-
if (args.length === 0)
|
|
3763
|
-
return;
|
|
3764
|
-
if (!thisCommand.parent)
|
|
3765
|
-
throw new Error('parent command not found');
|
|
3766
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent;
|
|
3767
|
-
const command = args.map(String).join(' ');
|
|
3768
|
-
const cmd = resolveCommand(getDotenvCliOptions.scripts, command);
|
|
3769
|
-
if (getDotenvCliOptions.debug)
|
|
3770
|
-
logger.log('\n*** command ***\n', `'${cmd}'`);
|
|
3771
|
-
await execaCommand(cmd, {
|
|
3772
|
-
env: {
|
|
3773
|
-
...process.env,
|
|
3774
|
-
getDotenvCliOptions: JSON.stringify(getDotenvCliOptions),
|
|
3775
|
-
},
|
|
3776
|
-
// execa expects string | boolean | URL; we normalize in generator
|
|
3777
|
-
// and allow script-level overrides.
|
|
3778
|
-
shell: resolveShell(getDotenvCliOptions.scripts, command, getDotenvCliOptions.shell),
|
|
3779
|
-
stdio: 'inherit',
|
|
3780
|
-
});
|
|
3781
|
-
});
|
|
3782
|
-
|
|
3783
3931
|
function createCli(opts = {}) {
|
|
3784
3932
|
const alias = typeof opts.alias === 'string' && opts.alias.length > 0
|
|
3785
3933
|
? opts.alias
|
|
3786
3934
|
: 'getdotenv';
|
|
3787
3935
|
const program = new GetDotenvCli(alias);
|
|
3936
|
+
// Normalize Commander output so help prints always end with a blank line.
|
|
3937
|
+
// This keeps E2E assertions (CRLF and >=2 trailing newlines) portable across
|
|
3938
|
+
// runtimes and capture modes without altering Commander internals.
|
|
3939
|
+
const outputCfg = {
|
|
3940
|
+
writeOut(str) {
|
|
3941
|
+
const txt = typeof str === 'string' ? str : '';
|
|
3942
|
+
const hasTwo = /(?:\r?\n){2,}$/.test(txt);
|
|
3943
|
+
const hasOne = /\r?\n$/.test(txt);
|
|
3944
|
+
const out = hasTwo ? txt : hasOne ? txt + '\n' : txt + '\n\n';
|
|
3945
|
+
try {
|
|
3946
|
+
process.stdout.write(out);
|
|
3947
|
+
}
|
|
3948
|
+
catch {
|
|
3949
|
+
/* ignore */
|
|
3950
|
+
}
|
|
3951
|
+
},
|
|
3952
|
+
writeErr(str) {
|
|
3953
|
+
process.stderr.write(str);
|
|
3954
|
+
},
|
|
3955
|
+
};
|
|
3956
|
+
// Apply to root and recursively to subcommands so all help paths are normalized.
|
|
3957
|
+
program.configureOutput(outputCfg);
|
|
3958
|
+
const applyOutputRecursively = (cmd) => {
|
|
3959
|
+
cmd.configureOutput(outputCfg);
|
|
3960
|
+
const kids = cmd.commands ?? [];
|
|
3961
|
+
for (const child of kids)
|
|
3962
|
+
applyOutputRecursively(child);
|
|
3963
|
+
};
|
|
3964
|
+
applyOutputRecursively(program);
|
|
3788
3965
|
// Install base root flags and included plugins; resolve context once per run.
|
|
3789
3966
|
program
|
|
3790
3967
|
.attachRootOptions({ loadProcess: false })
|
|
@@ -3800,19 +3977,72 @@ function createCli(opts = {}) {
|
|
|
3800
3977
|
if (underTests) {
|
|
3801
3978
|
program.exitOverride((err) => {
|
|
3802
3979
|
const code = err?.code;
|
|
3803
|
-
|
|
3980
|
+
// Commander printed help already; ensure a trailing blank line for tests/CI capture.
|
|
3981
|
+
if (code === 'commander.helpDisplayed') {
|
|
3982
|
+
try {
|
|
3983
|
+
process.stdout.write('\n');
|
|
3984
|
+
}
|
|
3985
|
+
catch {
|
|
3986
|
+
/* ignore */
|
|
3987
|
+
}
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
if (code === 'commander.version') {
|
|
3804
3991
|
return;
|
|
3992
|
+
}
|
|
3805
3993
|
throw err;
|
|
3806
3994
|
});
|
|
3807
3995
|
}
|
|
3808
3996
|
return {
|
|
3809
3997
|
async run(argv) {
|
|
3810
|
-
//
|
|
3811
|
-
//
|
|
3812
|
-
//
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3998
|
+
// Help handling:
|
|
3999
|
+
// - Short-circuit ONLY for true top-level -h/--help (no subcommand before flag).
|
|
4000
|
+
// - If a subcommand token appears before -h/--help, defer to Commander
|
|
4001
|
+
// to render that subcommand's help.
|
|
4002
|
+
const helpIdx = argv.findIndex((a) => a === '-h' || a === '--help');
|
|
4003
|
+
if (helpIdx >= 0) {
|
|
4004
|
+
// Build a set of known subcommand names/aliases on the root.
|
|
4005
|
+
const subs = new Set();
|
|
4006
|
+
const cmds = program.commands ?? [];
|
|
4007
|
+
for (const c of cmds) {
|
|
4008
|
+
subs.add(c.name());
|
|
4009
|
+
for (const a of c.aliases())
|
|
4010
|
+
subs.add(a);
|
|
4011
|
+
}
|
|
4012
|
+
const hasSubBeforeHelp = argv
|
|
4013
|
+
.slice(0, helpIdx)
|
|
4014
|
+
.some((tok) => subs.has(tok));
|
|
4015
|
+
if (!hasSubBeforeHelp) {
|
|
4016
|
+
await program.brand({
|
|
4017
|
+
name: alias,
|
|
4018
|
+
importMetaUrl: import.meta.url,
|
|
4019
|
+
description: 'Base CLI.',
|
|
4020
|
+
...(typeof opts.branding === 'string' && opts.branding.length > 0
|
|
4021
|
+
? { helpHeader: opts.branding }
|
|
4022
|
+
: {}),
|
|
4023
|
+
});
|
|
4024
|
+
// Resolve context once without side effects for help rendering.
|
|
4025
|
+
const ctx = await program.resolveAndLoad({
|
|
4026
|
+
loadProcess: false,
|
|
4027
|
+
log: false,
|
|
4028
|
+
}, { runAfterResolve: false });
|
|
4029
|
+
program.evaluateDynamicOptions({
|
|
4030
|
+
...ctx.optionsResolved,
|
|
4031
|
+
plugins: ctx.pluginConfigs ?? {},
|
|
4032
|
+
});
|
|
4033
|
+
// Suppress output only during unit tests; allow E2E to capture.
|
|
4034
|
+
const piping = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
4035
|
+
process.env.GETDOTENV_STDOUT === 'pipe';
|
|
4036
|
+
if (underTests && !piping) {
|
|
4037
|
+
void program.helpInformation();
|
|
4038
|
+
}
|
|
4039
|
+
else {
|
|
4040
|
+
program.outputHelp();
|
|
4041
|
+
}
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
// Subcommand token exists before -h: fall through to normal parsing,
|
|
4045
|
+
// letting Commander print that subcommand's help.
|
|
3816
4046
|
}
|
|
3817
4047
|
await program.brand({
|
|
3818
4048
|
name: alias,
|