@karmaniverous/get-dotenv 5.2.6 → 6.0.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -70
- package/dist/cliHost.d.ts +232 -226
- package/dist/cliHost.mjs +777 -545
- package/dist/config.d.ts +7 -2
- package/dist/env-overlay.d.ts +21 -9
- package/dist/env-overlay.mjs +14 -19
- package/dist/getdotenv.cli.mjs +1366 -1163
- package/dist/index.d.ts +415 -242
- package/dist/index.mjs +1364 -1414
- package/dist/plugins-aws.d.ts +149 -94
- package/dist/plugins-aws.mjs +307 -195
- package/dist/plugins-batch.d.ts +153 -99
- package/dist/plugins-batch.mjs +277 -95
- package/dist/plugins-cmd.d.ts +140 -94
- package/dist/plugins-cmd.mjs +636 -502
- package/dist/plugins-demo.d.ts +140 -94
- package/dist/plugins-demo.mjs +237 -46
- package/dist/plugins-init.d.ts +140 -94
- package/dist/plugins-init.mjs +129 -12
- package/dist/plugins.d.ts +166 -103
- package/dist/plugins.mjs +977 -840
- package/package.json +15 -53
- package/templates/cli/ts/plugins/hello.ts +27 -6
- package/templates/config/js/getdotenv.config.js +1 -1
- package/templates/config/ts/getdotenv.config.ts +9 -2
- package/dist/cliHost.cjs +0 -1875
- package/dist/cliHost.d.cts +0 -409
- package/dist/cliHost.d.mts +0 -409
- package/dist/config.cjs +0 -252
- package/dist/config.d.cts +0 -55
- package/dist/config.d.mts +0 -55
- package/dist/env-overlay.cjs +0 -163
- package/dist/env-overlay.d.cts +0 -50
- package/dist/env-overlay.d.mts +0 -50
- package/dist/index.cjs +0 -4140
- package/dist/index.d.cts +0 -457
- package/dist/index.d.mts +0 -457
- package/dist/plugins-aws.cjs +0 -667
- package/dist/plugins-aws.d.cts +0 -158
- package/dist/plugins-aws.d.mts +0 -158
- package/dist/plugins-batch.cjs +0 -616
- package/dist/plugins-batch.d.cts +0 -180
- package/dist/plugins-batch.d.mts +0 -180
- package/dist/plugins-cmd.cjs +0 -1113
- package/dist/plugins-cmd.d.cts +0 -178
- package/dist/plugins-cmd.d.mts +0 -178
- package/dist/plugins-demo.cjs +0 -307
- package/dist/plugins-demo.d.cts +0 -158
- package/dist/plugins-demo.d.mts +0 -158
- package/dist/plugins-init.cjs +0 -289
- package/dist/plugins-init.d.cts +0 -162
- package/dist/plugins-init.d.mts +0 -162
- package/dist/plugins.cjs +0 -2283
- package/dist/plugins.d.cts +0 -210
- package/dist/plugins.d.mts +0 -210
package/dist/getdotenv.cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { packageDirectory } from 'package-directory';
|
|
5
5
|
import path, { join, extname } from 'path';
|
|
6
6
|
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
7
7
|
import YAML from 'yaml';
|
|
8
|
-
import {
|
|
8
|
+
import { Option, Command } from 'commander';
|
|
9
9
|
import { nanoid } from 'nanoid';
|
|
10
10
|
import { parse } from 'dotenv';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
@@ -14,258 +14,6 @@ import { globby } from 'globby';
|
|
|
14
14
|
import { stdin, stdout } from 'node:process';
|
|
15
15
|
import { createInterface } from 'readline/promises';
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Dotenv expansion utilities.
|
|
19
|
-
*
|
|
20
|
-
* This module implements recursive expansion of environment-variable
|
|
21
|
-
* references in strings and records. It supports both whitespace and
|
|
22
|
-
* bracket syntaxes with optional defaults:
|
|
23
|
-
*
|
|
24
|
-
* - Whitespace: `$VAR[:default]`
|
|
25
|
-
* - Bracketed: `${VAR[:default]}`
|
|
26
|
-
*
|
|
27
|
-
* Escaped dollar signs (`\$`) are preserved.
|
|
28
|
-
* Unknown variables resolve to empty string unless a default is provided.
|
|
29
|
-
*/
|
|
30
|
-
/**
|
|
31
|
-
* Like String.prototype.search but returns the last index.
|
|
32
|
-
* @internal
|
|
33
|
-
*/
|
|
34
|
-
const searchLast = (str, rgx) => {
|
|
35
|
-
const matches = Array.from(str.matchAll(rgx));
|
|
36
|
-
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
37
|
-
};
|
|
38
|
-
const replaceMatch = (value, match, ref) => {
|
|
39
|
-
/**
|
|
40
|
-
* @internal
|
|
41
|
-
*/
|
|
42
|
-
const group = match[0];
|
|
43
|
-
const key = match[1];
|
|
44
|
-
const defaultValue = match[2];
|
|
45
|
-
if (!key)
|
|
46
|
-
return value;
|
|
47
|
-
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
48
|
-
return interpolate(replacement, ref);
|
|
49
|
-
};
|
|
50
|
-
const interpolate = (value = '', ref = {}) => {
|
|
51
|
-
/**
|
|
52
|
-
* @internal
|
|
53
|
-
*/
|
|
54
|
-
// if value is falsy, return it as is
|
|
55
|
-
if (!value)
|
|
56
|
-
return value;
|
|
57
|
-
// get position of last unescaped dollar sign
|
|
58
|
-
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
59
|
-
// return value if none found
|
|
60
|
-
if (lastUnescapedDollarSignIndex === -1)
|
|
61
|
-
return value;
|
|
62
|
-
// evaluate the value tail
|
|
63
|
-
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
64
|
-
// find whitespace pattern: $KEY:DEFAULT
|
|
65
|
-
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
66
|
-
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
67
|
-
if (whitespaceMatch != null)
|
|
68
|
-
return replaceMatch(value, whitespaceMatch, ref);
|
|
69
|
-
else {
|
|
70
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
71
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
72
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
73
|
-
if (bracketMatch != null)
|
|
74
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
75
|
-
}
|
|
76
|
-
return value;
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* Recursively expands environment variables in a string. Variables may be
|
|
80
|
-
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
81
|
-
* Unknown variables will expand to an empty string.
|
|
82
|
-
*
|
|
83
|
-
* @param value - The string to expand.
|
|
84
|
-
* @param ref - The reference object to use for variable expansion.
|
|
85
|
-
* @returns The expanded string.
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* ```ts
|
|
89
|
-
* process.env.FOO = 'bar';
|
|
90
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
91
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
92
|
-
* ```
|
|
93
|
-
*
|
|
94
|
-
* @remarks
|
|
95
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
96
|
-
* references, those will also be expanded until a stable value is reached.
|
|
97
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
98
|
-
*/
|
|
99
|
-
const dotenvExpand = (value, ref = process.env) => {
|
|
100
|
-
const result = interpolate(value, ref);
|
|
101
|
-
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
102
|
-
};
|
|
103
|
-
/**
|
|
104
|
-
* Recursively expands environment variables in the values of a JSON object.
|
|
105
|
-
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
106
|
-
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
107
|
-
*
|
|
108
|
-
* @param values - The values object to expand.
|
|
109
|
-
* @param options - Expansion options.
|
|
110
|
-
* @returns The value object with expanded string values.
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* ```ts
|
|
114
|
-
* process.env.FOO = 'bar';
|
|
115
|
-
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
116
|
-
* // => { A: "bar", B: "xbary" }
|
|
117
|
-
* ```
|
|
118
|
-
*
|
|
119
|
-
* @remarks
|
|
120
|
-
* Options:
|
|
121
|
-
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
122
|
-
* - progressive: Whether to progressively add expanded values to the set of
|
|
123
|
-
* reference keys.
|
|
124
|
-
*
|
|
125
|
-
* When `progressive` is true, each expanded key becomes available for
|
|
126
|
-
* subsequent expansions in the same object (left-to-right by object key order).
|
|
127
|
-
*/
|
|
128
|
-
const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
|
|
129
|
-
const { ref = process.env, progressive = false } = options;
|
|
130
|
-
acc[key] = dotenvExpand(values[key], {
|
|
131
|
-
...ref,
|
|
132
|
-
...(progressive ? acc : {}),
|
|
133
|
-
});
|
|
134
|
-
return acc;
|
|
135
|
-
}, {});
|
|
136
|
-
/**
|
|
137
|
-
* Recursively expands environment variables in a string using `process.env` as
|
|
138
|
-
* the expansion reference. Variables may be presented with optional default as
|
|
139
|
-
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
140
|
-
* empty string.
|
|
141
|
-
*
|
|
142
|
-
* @param value - The string to expand.
|
|
143
|
-
* @returns The expanded string.
|
|
144
|
-
*
|
|
145
|
-
* @example
|
|
146
|
-
* ```ts
|
|
147
|
-
* process.env.FOO = 'bar';
|
|
148
|
-
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
149
|
-
* ```
|
|
150
|
-
*/
|
|
151
|
-
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Attach legacy root flags to a Commander program.
|
|
155
|
-
* Uses provided defaults to render help labels without coupling to generators.
|
|
156
|
-
*/
|
|
157
|
-
const attachRootOptions = (program, defaults, opts) => {
|
|
158
|
-
// Install temporary wrappers to tag all options added here as "base".
|
|
159
|
-
const GROUP = 'base';
|
|
160
|
-
const tagLatest = (cmd, group) => {
|
|
161
|
-
const optsArr = cmd.options;
|
|
162
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
163
|
-
const last = optsArr[optsArr.length - 1];
|
|
164
|
-
last.__group = group;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
const originalAddOption = program.addOption.bind(program);
|
|
168
|
-
const originalOption = program.option.bind(program);
|
|
169
|
-
program.addOption = function patchedAdd(opt) {
|
|
170
|
-
// Tag before adding, in case consumers inspect the Option directly.
|
|
171
|
-
opt.__group = GROUP;
|
|
172
|
-
const ret = originalAddOption(opt);
|
|
173
|
-
return ret;
|
|
174
|
-
};
|
|
175
|
-
program.option = function patchedOption(...args) {
|
|
176
|
-
const ret = originalOption(...args);
|
|
177
|
-
tagLatest(this, GROUP);
|
|
178
|
-
return ret;
|
|
179
|
-
};
|
|
180
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
181
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
182
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
183
|
-
// Build initial chain.
|
|
184
|
-
let p = program
|
|
185
|
-
.enablePositionalOptions()
|
|
186
|
-
.passThroughOptions()
|
|
187
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
188
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
189
|
-
['KEY1', 'VAL1'],
|
|
190
|
-
['KEY2', 'VAL2'],
|
|
191
|
-
]
|
|
192
|
-
.map((v) => v.join(va))
|
|
193
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
194
|
-
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
195
|
-
// Default is OFF; the generator opts in explicitly.
|
|
196
|
-
if (opts?.includeCommandOption === true) {
|
|
197
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
198
|
-
}
|
|
199
|
-
p = p
|
|
200
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
201
|
-
.addOption(new Option('-s, --shell [string]', (() => {
|
|
202
|
-
let defaultLabel = '';
|
|
203
|
-
if (shell !== undefined) {
|
|
204
|
-
if (typeof shell === 'boolean') {
|
|
205
|
-
defaultLabel = ' (default OS shell)';
|
|
206
|
-
}
|
|
207
|
-
else if (typeof shell === 'string') {
|
|
208
|
-
// Safe string interpolation
|
|
209
|
-
defaultLabel = ` (default ${shell})`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
213
|
-
})()).conflicts('shellOff'))
|
|
214
|
-
.addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
215
|
-
.addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
216
|
-
.addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
217
|
-
.addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
218
|
-
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
219
|
-
? ' (default)'
|
|
220
|
-
: ''}`).conflicts('excludeAllOff'))
|
|
221
|
-
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
222
|
-
.addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
223
|
-
.addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
224
|
-
.addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
225
|
-
.addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
226
|
-
.addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
227
|
-
.addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
228
|
-
.addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
229
|
-
.addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
230
|
-
.addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
231
|
-
.addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
232
|
-
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
233
|
-
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
234
|
-
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
235
|
-
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
236
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
237
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
238
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
239
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
240
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
241
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
242
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
243
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
244
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
245
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
246
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
247
|
-
// Hidden scripts pipe-through (stringified)
|
|
248
|
-
.addOption(new Option('--scripts <string>')
|
|
249
|
-
.default(JSON.stringify(scripts))
|
|
250
|
-
.hideHelp());
|
|
251
|
-
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
252
|
-
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
253
|
-
// Validation: strict mode fails on env validation issues (warn by default).
|
|
254
|
-
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
255
|
-
// Entropy diagnostics (presentation-only)
|
|
256
|
-
p = p
|
|
257
|
-
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
258
|
-
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
259
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
260
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
261
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
262
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
263
|
-
// Restore original methods to avoid tagging future additions outside base.
|
|
264
|
-
program.addOption = originalAddOption;
|
|
265
|
-
program.option = originalOption;
|
|
266
|
-
return p;
|
|
267
|
-
};
|
|
268
|
-
|
|
269
17
|
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
270
18
|
const baseRootOptionDefaults = {
|
|
271
19
|
dotenvToken: '.env',
|
|
@@ -314,26 +62,12 @@ const mergeInto = (target, source) => {
|
|
|
314
62
|
}
|
|
315
63
|
return target;
|
|
316
64
|
};
|
|
317
|
-
|
|
318
|
-
* Perform a deep defaults-style merge across plain objects. *
|
|
319
|
-
* - Only merges plain objects (prototype === Object.prototype).
|
|
320
|
-
* - Arrays and non-objects are replaced, not merged.
|
|
321
|
-
* - `undefined` values are ignored and do not overwrite prior values.
|
|
322
|
-
*
|
|
323
|
-
* @typeParam T - The resulting shape after merging all layers.
|
|
324
|
-
* @param layers - Zero or more partial layers in ascending precedence order.
|
|
325
|
-
* @returns The merged object typed as {@link T}.
|
|
326
|
-
*
|
|
327
|
-
* @example
|
|
328
|
-
* defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
|
|
329
|
-
* =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
|
|
330
|
-
*/
|
|
331
|
-
const defaultsDeep = (...layers) => {
|
|
65
|
+
function defaultsDeep(...layers) {
|
|
332
66
|
const result = layers
|
|
333
67
|
.filter(Boolean)
|
|
334
68
|
.reduce((acc, layer) => mergeInto(acc, layer), {});
|
|
335
69
|
return result;
|
|
336
|
-
}
|
|
70
|
+
}
|
|
337
71
|
|
|
338
72
|
/**
|
|
339
73
|
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
@@ -755,106 +489,451 @@ const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
|
755
489
|
|
|
756
490
|
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
757
491
|
|
|
492
|
+
/** src/util/omitUndefined.ts
|
|
493
|
+
* Helpers to drop undefined-valued properties in a typed-friendly way.
|
|
494
|
+
*/
|
|
495
|
+
/**
|
|
496
|
+
* Omit keys whose runtime value is undefined from a shallow object.
|
|
497
|
+
* Returns a Partial with non-undefined value types preserved.
|
|
498
|
+
*/
|
|
499
|
+
function omitUndefined(obj) {
|
|
500
|
+
const out = {};
|
|
501
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
502
|
+
if (v !== undefined)
|
|
503
|
+
out[k] = v;
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Specialized helper for env-like maps: drop undefined and return string-only.
|
|
509
|
+
*/
|
|
510
|
+
function omitUndefinedRecord(obj) {
|
|
511
|
+
const out = {};
|
|
512
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
513
|
+
if (v !== undefined)
|
|
514
|
+
out[k] = v;
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
758
519
|
// src/GetDotenvOptions.ts
|
|
520
|
+
/**
|
|
521
|
+
* Canonical programmatic options and helpers for get-dotenv.
|
|
522
|
+
*
|
|
523
|
+
* Requirements addressed:
|
|
524
|
+
* - GetDotenvOptions derives from the Zod schema output (single source of truth).
|
|
525
|
+
* - Removed deprecated/compat flags from the public shape (e.g., useConfigLoader).
|
|
526
|
+
* - Provide Vars-aware defineDynamic and a typed config builder defineGetDotenvConfig\<Vars, Env\>().
|
|
527
|
+
* - Preserve existing behavior for defaults resolution and compat converters.
|
|
528
|
+
*/
|
|
759
529
|
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
760
530
|
/**
|
|
761
|
-
* Converts programmatic CLI options to `getDotenv` options.
|
|
762
|
-
*
|
|
531
|
+
* Converts programmatic CLI options to `getDotenv` options.
|
|
532
|
+
*
|
|
533
|
+
* Accepts "stringly" CLI inputs for vars/paths and normalizes them into
|
|
534
|
+
* the programmatic shape. Preserves exactOptionalPropertyTypes semantics by
|
|
535
|
+
* omitting keys when undefined.
|
|
536
|
+
*/
|
|
537
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern,
|
|
538
|
+
// drop CLI-only keys from the pass-through bag
|
|
539
|
+
debug: _debug, scripts: _scripts, ...rest }) => {
|
|
540
|
+
// Split helper for delimited strings or regex patterns
|
|
541
|
+
const splitBy = (value, delim, pattern) => {
|
|
542
|
+
if (!value)
|
|
543
|
+
return [];
|
|
544
|
+
if (pattern)
|
|
545
|
+
return value.split(RegExp(pattern));
|
|
546
|
+
if (typeof delim === 'string')
|
|
547
|
+
return value.split(delim);
|
|
548
|
+
return value.split(' ');
|
|
549
|
+
};
|
|
550
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
551
|
+
let parsedVars;
|
|
552
|
+
if (typeof vars === 'string') {
|
|
553
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern)
|
|
554
|
+
.map((v) => v.split(varsAssignorPattern
|
|
555
|
+
? RegExp(varsAssignorPattern)
|
|
556
|
+
: (varsAssignor ?? '=')))
|
|
557
|
+
.filter(([k]) => typeof k === 'string' && k.length > 0);
|
|
558
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
559
|
+
}
|
|
560
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
561
|
+
// Accept provided object map of string | undefined; drop undefined values
|
|
562
|
+
// in the normalization step below to produce a ProcessEnv-compatible bag.
|
|
563
|
+
parsedVars = Object.fromEntries(Object.entries(vars));
|
|
564
|
+
}
|
|
565
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
566
|
+
// expectations and the compat test assertions.
|
|
567
|
+
if (parsedVars) {
|
|
568
|
+
parsedVars = omitUndefinedRecord(parsedVars);
|
|
569
|
+
}
|
|
570
|
+
// Tolerate paths as either a delimited string or string[]
|
|
571
|
+
const pathsOut = Array.isArray(paths)
|
|
572
|
+
? paths.filter((p) => typeof p === 'string')
|
|
573
|
+
: splitBy(paths, pathsDelimiter, pathsDelimiterPattern);
|
|
574
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
575
|
+
return {
|
|
576
|
+
...rest,
|
|
577
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
578
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
579
|
+
};
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
583
|
+
*
|
|
584
|
+
* 1. Base defaults derived from the CLI generator defaults
|
|
585
|
+
* ({@link baseGetDotenvCliOptions}).
|
|
586
|
+
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
587
|
+
* package root (if present).
|
|
588
|
+
* 3. The provided customOptions.
|
|
589
|
+
*
|
|
590
|
+
* The result preserves explicit empty values and drops only `undefined`.
|
|
591
|
+
*/
|
|
592
|
+
const resolveGetDotenvOptions = async (customOptions) => {
|
|
593
|
+
const localPkgDir = await packageDirectory();
|
|
594
|
+
const localOptionsPath = localPkgDir
|
|
595
|
+
? join(localPkgDir, getDotenvOptionsFilename)
|
|
596
|
+
: undefined;
|
|
597
|
+
// Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
|
|
598
|
+
let localOptions = {};
|
|
599
|
+
if (localOptionsPath && (await fs.exists(localOptionsPath))) {
|
|
600
|
+
try {
|
|
601
|
+
const txt = await fs.readFile(localOptionsPath, 'utf-8');
|
|
602
|
+
const parsed = JSON.parse(txt);
|
|
603
|
+
if (parsed && typeof parsed === 'object') {
|
|
604
|
+
localOptions = parsed;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
// Malformed or unreadable local options are treated as absent.
|
|
609
|
+
localOptions = {};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Merge order: base < local < custom (custom has highest precedence)
|
|
613
|
+
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
614
|
+
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
615
|
+
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
616
|
+
return {
|
|
617
|
+
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
618
|
+
vars: omitUndefinedRecord(result.vars ?? {}),
|
|
619
|
+
};
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Dotenv expansion utilities.
|
|
624
|
+
*
|
|
625
|
+
* This module implements recursive expansion of environment-variable
|
|
626
|
+
* references in strings and records. It supports both whitespace and
|
|
627
|
+
* bracket syntaxes with optional defaults:
|
|
628
|
+
*
|
|
629
|
+
* - Whitespace: `$VAR[:default]`
|
|
630
|
+
* - Bracketed: `${VAR[:default]}`
|
|
631
|
+
*
|
|
632
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
633
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
634
|
+
*/
|
|
635
|
+
/**
|
|
636
|
+
* Like String.prototype.search but returns the last index.
|
|
637
|
+
* @internal
|
|
638
|
+
*/
|
|
639
|
+
const searchLast = (str, rgx) => {
|
|
640
|
+
const matches = Array.from(str.matchAll(rgx));
|
|
641
|
+
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
642
|
+
};
|
|
643
|
+
const replaceMatch = (value, match, ref) => {
|
|
644
|
+
/**
|
|
645
|
+
* @internal
|
|
646
|
+
*/
|
|
647
|
+
const group = match[0];
|
|
648
|
+
const key = match[1];
|
|
649
|
+
const defaultValue = match[2];
|
|
650
|
+
if (!key)
|
|
651
|
+
return value;
|
|
652
|
+
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
653
|
+
return interpolate(replacement, ref);
|
|
654
|
+
};
|
|
655
|
+
const interpolate = (value = '', ref = {}) => {
|
|
656
|
+
/**
|
|
657
|
+
* @internal
|
|
658
|
+
*/
|
|
659
|
+
// if value is falsy, return it as is
|
|
660
|
+
if (!value)
|
|
661
|
+
return value;
|
|
662
|
+
// get position of last unescaped dollar sign
|
|
663
|
+
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
664
|
+
// return value if none found
|
|
665
|
+
if (lastUnescapedDollarSignIndex === -1)
|
|
666
|
+
return value;
|
|
667
|
+
// evaluate the value tail
|
|
668
|
+
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
669
|
+
// find whitespace pattern: $KEY:DEFAULT
|
|
670
|
+
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
671
|
+
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
672
|
+
if (whitespaceMatch != null)
|
|
673
|
+
return replaceMatch(value, whitespaceMatch, ref);
|
|
674
|
+
else {
|
|
675
|
+
// find bracket pattern: ${KEY:DEFAULT}
|
|
676
|
+
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
677
|
+
const bracketMatch = bracketPattern.exec(tail);
|
|
678
|
+
if (bracketMatch != null)
|
|
679
|
+
return replaceMatch(value, bracketMatch, ref);
|
|
680
|
+
}
|
|
681
|
+
return value;
|
|
682
|
+
};
|
|
683
|
+
/**
|
|
684
|
+
* Recursively expands environment variables in a string. Variables may be
|
|
685
|
+
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
686
|
+
* Unknown variables will expand to an empty string.
|
|
687
|
+
*
|
|
688
|
+
* @param value - The string to expand.
|
|
689
|
+
* @param ref - The reference object to use for variable expansion.
|
|
690
|
+
* @returns The expanded string.
|
|
691
|
+
*
|
|
692
|
+
* @example
|
|
693
|
+
* ```ts
|
|
694
|
+
* process.env.FOO = 'bar';
|
|
695
|
+
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
696
|
+
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
697
|
+
* ```
|
|
698
|
+
*
|
|
699
|
+
* @remarks
|
|
700
|
+
* The expansion is recursive. If a referenced variable itself contains
|
|
701
|
+
* references, those will also be expanded until a stable value is reached.
|
|
702
|
+
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
703
|
+
*/
|
|
704
|
+
const dotenvExpand = (value, ref = process.env) => {
|
|
705
|
+
const result = interpolate(value, ref);
|
|
706
|
+
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
707
|
+
};
|
|
708
|
+
/**
|
|
709
|
+
* Recursively expands environment variables in the values of a JSON object.
|
|
710
|
+
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
711
|
+
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
712
|
+
*
|
|
713
|
+
* @param values - The values object to expand.
|
|
714
|
+
* @param options - Expansion options.
|
|
715
|
+
* @returns The value object with expanded string values.
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```ts
|
|
719
|
+
* process.env.FOO = 'bar';
|
|
720
|
+
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
721
|
+
* // => { A: "bar", B: "xbary" }
|
|
722
|
+
* ```
|
|
723
|
+
*
|
|
724
|
+
* @remarks
|
|
725
|
+
* Options:
|
|
726
|
+
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
727
|
+
* - progressive: Whether to progressively add expanded values to the set of
|
|
728
|
+
* reference keys.
|
|
729
|
+
*
|
|
730
|
+
* When `progressive` is true, each expanded key becomes available for
|
|
731
|
+
* subsequent expansions in the same object (left-to-right by object key order).
|
|
732
|
+
*/
|
|
733
|
+
function dotenvExpandAll(values, options = {}) {
|
|
734
|
+
const { ref = process.env, progressive = false, } = options;
|
|
735
|
+
const out = Object.keys(values).reduce((acc, key) => {
|
|
736
|
+
acc[key] = dotenvExpand(values[key], {
|
|
737
|
+
...ref,
|
|
738
|
+
...(progressive ? acc : {}),
|
|
739
|
+
});
|
|
740
|
+
return acc;
|
|
741
|
+
}, {});
|
|
742
|
+
// Key-preserving return with a permissive index signature to allow later additions.
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
747
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
748
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
749
|
+
* empty string.
|
|
750
|
+
*
|
|
751
|
+
* @param value - The string to expand.
|
|
752
|
+
* @returns The expanded string.
|
|
763
753
|
*
|
|
764
|
-
* @
|
|
754
|
+
* @example
|
|
755
|
+
* ```ts
|
|
756
|
+
* process.env.FOO = 'bar';
|
|
757
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
758
|
+
* ```
|
|
765
759
|
*/
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
784
|
-
let parsedVars;
|
|
785
|
-
if (typeof vars === 'string') {
|
|
786
|
-
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
787
|
-
? RegExp(varsAssignorPattern)
|
|
788
|
-
: (varsAssignor ?? '=')));
|
|
789
|
-
parsedVars = Object.fromEntries(kvPairs);
|
|
790
|
-
}
|
|
791
|
-
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
792
|
-
// Keep only string or undefined values to match ProcessEnv.
|
|
793
|
-
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
794
|
-
parsedVars = Object.fromEntries(entries);
|
|
795
|
-
}
|
|
796
|
-
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
797
|
-
// expectations and the compat test assertions.
|
|
798
|
-
if (parsedVars) {
|
|
799
|
-
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
800
|
-
}
|
|
801
|
-
// Tolerate paths as either a delimited string or string[]
|
|
802
|
-
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
803
|
-
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
804
|
-
const pathsAny = paths;
|
|
805
|
-
const pathsOut = Array.isArray(pathsAny)
|
|
806
|
-
? pathsAny.filter((p) => typeof p === 'string')
|
|
807
|
-
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
808
|
-
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
809
|
-
return {
|
|
810
|
-
...restObj,
|
|
811
|
-
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
812
|
-
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
760
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
761
|
+
|
|
762
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
763
|
+
/**
|
|
764
|
+
* Attach root flags to a GetDotenvCli instance.
|
|
765
|
+
* - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
|
|
766
|
+
* - Any flag that displays an effective default in help uses dynamic descriptions.
|
|
767
|
+
*/
|
|
768
|
+
const attachRootOptions = (program, defaults) => {
|
|
769
|
+
// Install temporary wrappers to tag all options added here as "base" for grouped help.
|
|
770
|
+
const GROUP = 'base';
|
|
771
|
+
const tagLatest = (cmd, group) => {
|
|
772
|
+
const optsArr = cmd.options ?? [];
|
|
773
|
+
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
774
|
+
const last = optsArr[optsArr.length - 1];
|
|
775
|
+
program.setOptionGroup(last, group);
|
|
776
|
+
}
|
|
813
777
|
};
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
*
|
|
825
|
-
* The result preserves explicit empty values and drops only `undefined`.
|
|
826
|
-
*
|
|
827
|
-
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
828
|
-
*
|
|
829
|
-
* @example
|
|
830
|
-
* ```ts
|
|
831
|
-
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
832
|
-
* ```
|
|
833
|
-
*/
|
|
834
|
-
const localPkgDir = await packageDirectory();
|
|
835
|
-
const localOptionsPath = localPkgDir
|
|
836
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
837
|
-
: undefined;
|
|
838
|
-
const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
|
|
839
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
840
|
-
: {});
|
|
841
|
-
// Merge order: base < local < custom (custom has highest precedence)
|
|
842
|
-
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
843
|
-
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
844
|
-
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
845
|
-
return {
|
|
846
|
-
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
847
|
-
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
778
|
+
const originalAddOption = program.addOption.bind(program);
|
|
779
|
+
const originalOption = program.option.bind(program);
|
|
780
|
+
program.addOption = function patchedAdd(opt) {
|
|
781
|
+
program.setOptionGroup(opt, GROUP);
|
|
782
|
+
return originalAddOption(opt);
|
|
783
|
+
};
|
|
784
|
+
program.option = function patchedOption(...args) {
|
|
785
|
+
const ret = originalOption(...args);
|
|
786
|
+
tagLatest(this, GROUP);
|
|
787
|
+
return ret;
|
|
848
788
|
};
|
|
789
|
+
const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
790
|
+
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
791
|
+
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
792
|
+
// Helper: append (default) tags for ON/OFF toggles
|
|
793
|
+
const onOff = (on, isDefault) => on
|
|
794
|
+
? `ON${isDefault ? ' (default)' : ''}`
|
|
795
|
+
: `OFF${isDefault ? ' (default)' : ''}`;
|
|
796
|
+
let p = program
|
|
797
|
+
.enablePositionalOptions()
|
|
798
|
+
.passThroughOptions()
|
|
799
|
+
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
800
|
+
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
801
|
+
['KEY1', 'VAL1'],
|
|
802
|
+
['KEY2', 'VAL2'],
|
|
803
|
+
]
|
|
804
|
+
.map((v) => v.join(va))
|
|
805
|
+
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
806
|
+
// Output path (interpolated later; help can remain static)
|
|
807
|
+
p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
|
|
808
|
+
// Shell ON (string or boolean true => default shell)
|
|
809
|
+
p = p
|
|
810
|
+
.addOption(program
|
|
811
|
+
.createDynamicOption('-s, --shell [string]', (cfg) => {
|
|
812
|
+
const s = cfg.shell;
|
|
813
|
+
let tag = '';
|
|
814
|
+
if (typeof s === 'boolean' && s)
|
|
815
|
+
tag = ' (default OS shell)';
|
|
816
|
+
else if (typeof s === 'string' && s.length > 0)
|
|
817
|
+
tag = ` (default ${s})`;
|
|
818
|
+
return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
|
|
819
|
+
})
|
|
820
|
+
.conflicts('shellOff'))
|
|
821
|
+
// Shell OFF
|
|
822
|
+
.addOption(program
|
|
823
|
+
.createDynamicOption('-S, --shell-off', (cfg) => {
|
|
824
|
+
const s = cfg.shell;
|
|
825
|
+
return `command execution shell OFF${s === false ? ' (default)' : ''}`;
|
|
826
|
+
})
|
|
827
|
+
.conflicts('shell'));
|
|
828
|
+
// Load process ON/OFF (dynamic defaults)
|
|
829
|
+
p = p
|
|
830
|
+
.addOption(program
|
|
831
|
+
.createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
|
|
832
|
+
.conflicts('loadProcessOff'))
|
|
833
|
+
.addOption(program
|
|
834
|
+
.createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
|
|
835
|
+
.conflicts('loadProcess'));
|
|
836
|
+
// Exclusion master toggle (dynamic)
|
|
837
|
+
p = p
|
|
838
|
+
.addOption(program
|
|
839
|
+
.createDynamicOption('-a, --exclude-all', (cfg) => {
|
|
840
|
+
const allOn = !!cfg.excludeDynamic &&
|
|
841
|
+
((!!cfg.excludeEnv && !!cfg.excludeGlobal) ||
|
|
842
|
+
(!!cfg.excludePrivate && !!cfg.excludePublic));
|
|
843
|
+
const suffix = allOn ? ' (default)' : '';
|
|
844
|
+
return `exclude all dotenv variables from loading ON${suffix}`;
|
|
845
|
+
})
|
|
846
|
+
.conflicts('excludeAllOff'))
|
|
847
|
+
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
|
|
848
|
+
// Per-family exclusions (dynamic defaults)
|
|
849
|
+
p = p
|
|
850
|
+
.addOption(program
|
|
851
|
+
.createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
|
|
852
|
+
.conflicts('excludeDynamicOff'))
|
|
853
|
+
.addOption(program
|
|
854
|
+
.createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
|
|
855
|
+
.conflicts('excludeDynamic'))
|
|
856
|
+
.addOption(program
|
|
857
|
+
.createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
|
|
858
|
+
.conflicts('excludeEnvOff'))
|
|
859
|
+
.addOption(program
|
|
860
|
+
.createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
|
|
861
|
+
.conflicts('excludeEnv'))
|
|
862
|
+
.addOption(program
|
|
863
|
+
.createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
|
|
864
|
+
.conflicts('excludeGlobalOff'))
|
|
865
|
+
.addOption(program
|
|
866
|
+
.createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
|
|
867
|
+
.conflicts('excludeGlobal'))
|
|
868
|
+
.addOption(program
|
|
869
|
+
.createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
|
|
870
|
+
.conflicts('excludePrivateOff'))
|
|
871
|
+
.addOption(program
|
|
872
|
+
.createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
|
|
873
|
+
.conflicts('excludePrivate'))
|
|
874
|
+
.addOption(program
|
|
875
|
+
.createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
|
|
876
|
+
.conflicts('excludePublicOff'))
|
|
877
|
+
.addOption(program
|
|
878
|
+
.createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
|
|
879
|
+
.conflicts('excludePublic'));
|
|
880
|
+
// Log ON/OFF (dynamic)
|
|
881
|
+
p = p
|
|
882
|
+
.addOption(program
|
|
883
|
+
.createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
|
|
884
|
+
.conflicts('logOff'))
|
|
885
|
+
.addOption(program
|
|
886
|
+
.createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
|
|
887
|
+
.conflicts('log'));
|
|
888
|
+
// Capture flag (no default display; static)
|
|
889
|
+
p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
|
|
890
|
+
// Core bootstrap/static flags (kept static in help)
|
|
891
|
+
p = p
|
|
892
|
+
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
893
|
+
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
894
|
+
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
895
|
+
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
896
|
+
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
897
|
+
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
898
|
+
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
899
|
+
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
900
|
+
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
901
|
+
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
902
|
+
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
903
|
+
// Hidden scripts pipe-through (stringified)
|
|
904
|
+
.addOption(new Option('--scripts <string>')
|
|
905
|
+
.default(JSON.stringify(scripts))
|
|
906
|
+
.hideHelp());
|
|
907
|
+
// Diagnostics / validation / entropy
|
|
908
|
+
p = p
|
|
909
|
+
.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
|
|
910
|
+
.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
911
|
+
p = p
|
|
912
|
+
.addOption(program
|
|
913
|
+
.createDynamicOption('--entropy-warn', (cfg) => {
|
|
914
|
+
const warn = cfg.warnEntropy;
|
|
915
|
+
// Default is effectively ON when warnEntropy is true or undefined.
|
|
916
|
+
return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
|
|
917
|
+
})
|
|
918
|
+
.conflicts('entropyWarnOff'))
|
|
919
|
+
.addOption(program
|
|
920
|
+
.createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
|
|
921
|
+
.conflicts('entropyWarn'))
|
|
922
|
+
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
923
|
+
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
924
|
+
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
925
|
+
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
926
|
+
// Restore original methods
|
|
927
|
+
program.addOption = originalAddOption;
|
|
928
|
+
program.option = originalOption;
|
|
929
|
+
return p;
|
|
849
930
|
};
|
|
850
931
|
|
|
851
932
|
/**
|
|
852
933
|
* Zod schemas for programmatic GetDotenv options.
|
|
853
934
|
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
857
|
-
* later per the staged plan.
|
|
935
|
+
* Canonical source of truth for options shape. Public types are derived
|
|
936
|
+
* from these schemas (see consumers via z.output\<\>).
|
|
858
937
|
*/
|
|
859
938
|
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
860
939
|
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
@@ -873,12 +952,11 @@ const getDotenvOptionsSchemaRaw = z.object({
|
|
|
873
952
|
excludePublic: z.boolean().optional(),
|
|
874
953
|
loadProcess: z.boolean().optional(),
|
|
875
954
|
log: z.boolean().optional(),
|
|
955
|
+
logger: z.unknown().optional(),
|
|
876
956
|
outputPath: z.string().optional(),
|
|
877
957
|
paths: z.array(z.string()).optional(),
|
|
878
958
|
privateToken: z.string().optional(),
|
|
879
959
|
vars: processEnvSchema.optional(),
|
|
880
|
-
// Host-only feature flag: guarded integration of config loader/overlay
|
|
881
|
-
useConfigLoader: z.boolean().optional(),
|
|
882
960
|
});
|
|
883
961
|
// RESOLVED: service-boundary contract (post-inheritance).
|
|
884
962
|
// For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
|
|
@@ -898,16 +976,7 @@ const applyConfigSlice = (current, cfg, env) => {
|
|
|
898
976
|
const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
|
|
899
977
|
return applyKv(afterGlobal, envKv);
|
|
900
978
|
};
|
|
901
|
-
|
|
902
|
-
* Overlay config-provided values onto a base ProcessEnv using precedence axes:
|
|
903
|
-
* - kind: env \> global
|
|
904
|
-
* - privacy: local \> public
|
|
905
|
-
* - source: project \> packaged \> base
|
|
906
|
-
*
|
|
907
|
-
* Programmatic explicit vars (if provided) override all config slices.
|
|
908
|
-
* Progressive expansion is applied within each slice.
|
|
909
|
-
*/
|
|
910
|
-
const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
979
|
+
function overlayEnv({ base, env, configs, programmaticVars, }) {
|
|
911
980
|
let current = { ...base };
|
|
912
981
|
// Source: packaged (public -> local)
|
|
913
982
|
current = applyConfigSlice(current, configs.packaged, env);
|
|
@@ -922,7 +991,7 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
|
922
991
|
current = applyKv(current, toApply);
|
|
923
992
|
}
|
|
924
993
|
return current;
|
|
925
|
-
}
|
|
994
|
+
}
|
|
926
995
|
|
|
927
996
|
/** src/diagnostics/entropy.ts
|
|
928
997
|
* Entropy diagnostics (presentation-only).
|
|
@@ -932,7 +1001,7 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
|
932
1001
|
*/
|
|
933
1002
|
const warned = new Set();
|
|
934
1003
|
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
935
|
-
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
1004
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => (typeof p === 'string' ? new RegExp(p, 'i') : p));
|
|
936
1005
|
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
937
1006
|
const shannonBitsPerChar = (s) => {
|
|
938
1007
|
const freq = new Map();
|
|
@@ -979,7 +1048,7 @@ const DEFAULT_PATTERNS = [
|
|
|
979
1048
|
'\\bapi[_-]?key\\b',
|
|
980
1049
|
'\\bkey\\b',
|
|
981
1050
|
];
|
|
982
|
-
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
1051
|
+
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
|
|
983
1052
|
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
984
1053
|
const MASK = '[redacted]';
|
|
985
1054
|
/**
|
|
@@ -1143,43 +1212,7 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
|
1143
1212
|
}
|
|
1144
1213
|
};
|
|
1145
1214
|
|
|
1146
|
-
|
|
1147
|
-
* Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
|
|
1148
|
-
*
|
|
1149
|
-
* @param options - `GetDotenvOptions` object
|
|
1150
|
-
* @returns The combined parsed dotenv object.
|
|
1151
|
-
* * @example Load from the project root with default tokens
|
|
1152
|
-
* ```ts
|
|
1153
|
-
* const vars = await getDotenv();
|
|
1154
|
-
* console.log(vars.MY_SETTING);
|
|
1155
|
-
* ```
|
|
1156
|
-
*
|
|
1157
|
-
* @example Load from multiple paths and a specific environment
|
|
1158
|
-
* ```ts
|
|
1159
|
-
* const vars = await getDotenv({
|
|
1160
|
-
* env: 'dev',
|
|
1161
|
-
* dotenvToken: '.testenv',
|
|
1162
|
-
* privateToken: 'secret',
|
|
1163
|
-
* paths: ['./', './packages/app'],
|
|
1164
|
-
* });
|
|
1165
|
-
* ```
|
|
1166
|
-
*
|
|
1167
|
-
* @example Use dynamic variables
|
|
1168
|
-
* ```ts
|
|
1169
|
-
* // .env.js default-exports: { DYNAMIC: ({ PREV }) => `${PREV}-suffix` }
|
|
1170
|
-
* const vars = await getDotenv({ dynamicPath: '.env.js' });
|
|
1171
|
-
* ```
|
|
1172
|
-
*
|
|
1173
|
-
* @remarks
|
|
1174
|
-
* - When {@link GetDotenvOptions.loadProcess} is true, the resulting variables are merged
|
|
1175
|
-
* into `process.env` as a side effect.
|
|
1176
|
-
* - When {@link GetDotenvOptions.outputPath} is provided, a consolidated dotenv file is written.
|
|
1177
|
-
* The path is resolved after expansion, so it may reference previously loaded vars.
|
|
1178
|
-
*
|
|
1179
|
-
* @throws Error when a dynamic module is present but cannot be imported.
|
|
1180
|
-
* @throws Error when an output path was requested but could not be resolved.
|
|
1181
|
-
*/
|
|
1182
|
-
const getDotenv = async (options = {}) => {
|
|
1215
|
+
async function getDotenv(options = {}) {
|
|
1183
1216
|
// Apply defaults.
|
|
1184
1217
|
const { defaultEnv, dotenvToken = '.env', dynamicPath, env, excludeDynamic = false, excludeEnv = false, excludeGlobal = false, excludePrivate = false, excludePublic = false, loadProcess = false, log = false, logger = console, outputPath, paths = [], privateToken = 'local', vars = {}, } = await resolveGetDotenvOptions(options);
|
|
1185
1218
|
// Read .env files.
|
|
@@ -1284,8 +1317,7 @@ const getDotenv = async (options = {}) => {
|
|
|
1284
1317
|
.entropyThreshold;
|
|
1285
1318
|
const entropyMinLengthVal = options
|
|
1286
1319
|
.entropyMinLength;
|
|
1287
|
-
const entropyWhitelistVal = options
|
|
1288
|
-
.entropyWhitelist;
|
|
1320
|
+
const entropyWhitelistVal = options.entropyWhitelist;
|
|
1289
1321
|
const entOpts = {};
|
|
1290
1322
|
if (typeof warnEntropyVal === 'boolean')
|
|
1291
1323
|
entOpts.warnEntropy = warnEntropyVal;
|
|
@@ -1305,7 +1337,7 @@ const getDotenv = async (options = {}) => {
|
|
|
1305
1337
|
if (loadProcess)
|
|
1306
1338
|
Object.assign(process.env, resultDotenv);
|
|
1307
1339
|
return resultDotenv;
|
|
1308
|
-
}
|
|
1340
|
+
}
|
|
1309
1341
|
|
|
1310
1342
|
/**
|
|
1311
1343
|
* Deep interpolation utility for string leaves.
|
|
@@ -1363,6 +1395,18 @@ const interpolateDeep = (value, envRef) => {
|
|
|
1363
1395
|
return value;
|
|
1364
1396
|
};
|
|
1365
1397
|
|
|
1398
|
+
/**
|
|
1399
|
+
* Instance-bound plugin config store.
|
|
1400
|
+
* Host stores the validated/interpolated slice per plugin instance.
|
|
1401
|
+
* The store is intentionally private to this module; definePlugin()
|
|
1402
|
+
* provides a typed accessor that reads from this store for the calling
|
|
1403
|
+
* plugin instance.
|
|
1404
|
+
*/
|
|
1405
|
+
const PLUGIN_CONFIG_STORE = new WeakMap();
|
|
1406
|
+
const _setPluginConfigForInstance = (plugin, cfg) => {
|
|
1407
|
+
PLUGIN_CONFIG_STORE.set(plugin, cfg);
|
|
1408
|
+
};
|
|
1409
|
+
const _getPluginConfigForInstance = (plugin) => PLUGIN_CONFIG_STORE.get(plugin);
|
|
1366
1410
|
/**
|
|
1367
1411
|
* Compute the dotenv context for the host (uses the config loader/overlay path).
|
|
1368
1412
|
* - Resolves and validates options strictly (host-only).
|
|
@@ -1371,20 +1415,26 @@ const interpolateDeep = (value, envRef) => {
|
|
|
1371
1415
|
*
|
|
1372
1416
|
* @param customOptions - Partial options from the current invocation.
|
|
1373
1417
|
* @param plugins - Installed plugins (for config validation).
|
|
1374
|
-
* @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery).
|
|
1418
|
+
* @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery).
|
|
1419
|
+
*/
|
|
1375
1420
|
const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
1376
1421
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1422
|
+
// Zod boundary: parse returns the schema-derived shape; we adopt our public
|
|
1423
|
+
// GetDotenvOptions overlay (logger/dynamic typing) for internal processing.
|
|
1377
1424
|
const validated = getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
1378
1425
|
// Always-on loader path
|
|
1379
1426
|
// 1) Base from files only (no dynamic, no programmatic vars)
|
|
1427
|
+
// Sanitize to avoid passing properties explicitly set to undefined.
|
|
1428
|
+
const cleanedValidated = omitUndefined(validated);
|
|
1380
1429
|
const base = await getDotenv({
|
|
1381
|
-
...
|
|
1430
|
+
...cleanedValidated,
|
|
1382
1431
|
// Build a pure base without side effects or logging.
|
|
1383
1432
|
excludeDynamic: true,
|
|
1384
1433
|
vars: {},
|
|
1385
1434
|
log: false,
|
|
1386
1435
|
loadProcess: false,
|
|
1387
|
-
outputPath
|
|
1436
|
+
// Intentionally omit outputPath for the base pass; including a key with
|
|
1437
|
+
// undefined would violate exactOptionalPropertyTypes on the Partial target.
|
|
1388
1438
|
});
|
|
1389
1439
|
// 2) Discover config sources and overlay
|
|
1390
1440
|
const sources = await resolveGetDotenvConfigSources(hostMetaUrl);
|
|
@@ -1429,7 +1479,7 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1429
1479
|
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
1430
1480
|
}, ''), { encoding: 'utf-8' });
|
|
1431
1481
|
}
|
|
1432
|
-
const logger =
|
|
1482
|
+
const logger = customOptions.logger ?? console;
|
|
1433
1483
|
if (validated.log)
|
|
1434
1484
|
logger.log(dotenv);
|
|
1435
1485
|
if (validated.loadProcess)
|
|
@@ -1444,43 +1494,148 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1444
1494
|
const localPlugins = (sources.project?.local &&
|
|
1445
1495
|
sources.project.local.plugins) ??
|
|
1446
1496
|
{};
|
|
1447
|
-
|
|
1497
|
+
// The by-id map is retained only for backwards-compat rendering paths
|
|
1498
|
+
// (root help dynamic evaluation). Instance-bound access is the source
|
|
1499
|
+
// of truth going forward and is populated below.
|
|
1500
|
+
const mergedPluginConfigsById = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
|
|
1448
1501
|
for (const p of plugins) {
|
|
1449
1502
|
if (!p.id)
|
|
1450
1503
|
continue;
|
|
1451
|
-
const slice =
|
|
1452
|
-
|
|
1453
|
-
continue;
|
|
1454
|
-
// Per-plugin interpolation just before validation/afterResolve:
|
|
1455
|
-
// precedence: process.env wins over ctx.dotenv for slice defaults.
|
|
1504
|
+
const slice = mergedPluginConfigsById[p.id];
|
|
1505
|
+
// Build interpolation reference once per plugin:
|
|
1456
1506
|
const envRef = {
|
|
1457
1507
|
...dotenv,
|
|
1458
1508
|
...process.env,
|
|
1459
1509
|
};
|
|
1460
|
-
const interpolated =
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1510
|
+
const interpolated = slice && typeof slice === 'object'
|
|
1511
|
+
? interpolateDeep(slice, envRef)
|
|
1512
|
+
: {};
|
|
1513
|
+
// Enforced: plugins always carry a schema (strict empty by default).
|
|
1514
|
+
// Zod v4: avoid legacy multi-generic usage; treat as generic ZodObject.
|
|
1515
|
+
const schema = p.configSchema;
|
|
1516
|
+
const toParse = interpolated;
|
|
1517
|
+
const parsed = schema.safeParse(toParse);
|
|
1518
|
+
if (!parsed.success) {
|
|
1519
|
+
const err = parsed.error;
|
|
1520
|
+
const msgs = err.issues
|
|
1521
|
+
.map((i) => {
|
|
1522
|
+
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1523
|
+
const msg = typeof i.message === 'string' ? i.message : 'Invalid value';
|
|
1524
|
+
return path ? `${path}: ${msg}` : msg;
|
|
1525
|
+
})
|
|
1526
|
+
.join('\n');
|
|
1527
|
+
throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
|
|
1474
1528
|
}
|
|
1529
|
+
// Store a readonly (shallow-frozen) value for runtime safety.
|
|
1530
|
+
const frozen = Object.freeze(parsed.data);
|
|
1531
|
+
_setPluginConfigForInstance(p, frozen);
|
|
1532
|
+
mergedPluginConfigsById[p.id] = frozen;
|
|
1475
1533
|
}
|
|
1476
1534
|
return {
|
|
1477
1535
|
optionsResolved: validated,
|
|
1478
1536
|
dotenv: dotenv,
|
|
1479
1537
|
plugins: {},
|
|
1480
|
-
|
|
1538
|
+
// Retained for legacy root help dynamic evaluation only. Instance-bound
|
|
1539
|
+
// access is used by plugins themselves and tests/docs moving forward.
|
|
1540
|
+
pluginConfigs: mergedPluginConfigsById,
|
|
1481
1541
|
};
|
|
1482
1542
|
};
|
|
1483
1543
|
|
|
1544
|
+
// Registry for dynamic descriptions keyed by Option (WeakMap so GC-friendly)
|
|
1545
|
+
const DYN_DESC = new WeakMap();
|
|
1546
|
+
/**
|
|
1547
|
+
* Create an Option with a dynamic description callback stored in DYN_DESC.
|
|
1548
|
+
*/
|
|
1549
|
+
function makeDynamicOption(flags, desc, parser, defaultValue) {
|
|
1550
|
+
const opt = new Option(flags, '');
|
|
1551
|
+
DYN_DESC.set(opt, desc);
|
|
1552
|
+
if (parser) {
|
|
1553
|
+
opt.argParser((value, previous) => parser(value, previous));
|
|
1554
|
+
}
|
|
1555
|
+
if (defaultValue !== undefined)
|
|
1556
|
+
opt.default(defaultValue);
|
|
1557
|
+
return opt;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Evaluate dynamic descriptions across a command tree using the resolved config.
|
|
1561
|
+
*/
|
|
1562
|
+
function evaluateDynamicOptions(root, resolved) {
|
|
1563
|
+
const visit = (cmd) => {
|
|
1564
|
+
const arr = cmd.options;
|
|
1565
|
+
for (const o of arr) {
|
|
1566
|
+
const dyn = DYN_DESC.get(o);
|
|
1567
|
+
if (typeof dyn === 'function') {
|
|
1568
|
+
try {
|
|
1569
|
+
const txt = dyn(resolved);
|
|
1570
|
+
// Commander uses Option.description during help rendering.
|
|
1571
|
+
o.description = txt;
|
|
1572
|
+
}
|
|
1573
|
+
catch {
|
|
1574
|
+
/* best-effort; leave as-is */
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
for (const c of cmd.commands)
|
|
1579
|
+
visit(c);
|
|
1580
|
+
};
|
|
1581
|
+
visit(root);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Registry for grouping; root help renders groups between Options and Commands.
|
|
1585
|
+
const GROUP_TAG = new WeakMap();
|
|
1586
|
+
function renderOptionGroups(cmd) {
|
|
1587
|
+
const all = cmd.options;
|
|
1588
|
+
const byGroup = new Map();
|
|
1589
|
+
for (const o of all) {
|
|
1590
|
+
const opt = o;
|
|
1591
|
+
const g = GROUP_TAG.get(opt);
|
|
1592
|
+
if (!g || g === 'base')
|
|
1593
|
+
continue; // base handled by default help
|
|
1594
|
+
const rows = byGroup.get(g) ?? [];
|
|
1595
|
+
rows.push({
|
|
1596
|
+
flags: opt.flags,
|
|
1597
|
+
description: opt.description ?? '',
|
|
1598
|
+
});
|
|
1599
|
+
byGroup.set(g, rows);
|
|
1600
|
+
}
|
|
1601
|
+
if (byGroup.size === 0)
|
|
1602
|
+
return '';
|
|
1603
|
+
const renderRows = (title, rows) => {
|
|
1604
|
+
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
1605
|
+
// Sort within group: short-aliased flags first
|
|
1606
|
+
rows.sort((a, b) => {
|
|
1607
|
+
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
1608
|
+
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
1609
|
+
return bS - aS || a.flags.localeCompare(b.flags);
|
|
1610
|
+
});
|
|
1611
|
+
const lines = rows
|
|
1612
|
+
.map((r) => {
|
|
1613
|
+
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
1614
|
+
return ` ${r.flags}${pad}${r.description}`.trimEnd();
|
|
1615
|
+
})
|
|
1616
|
+
.join('\n');
|
|
1617
|
+
return `\n${title}:\n${lines}\n`;
|
|
1618
|
+
};
|
|
1619
|
+
let out = '';
|
|
1620
|
+
// App options (if any)
|
|
1621
|
+
const app = byGroup.get('app');
|
|
1622
|
+
if (app && app.length > 0) {
|
|
1623
|
+
out += renderRows('App options', app);
|
|
1624
|
+
}
|
|
1625
|
+
// Plugin groups sorted by id; suppress self group on the owning command name.
|
|
1626
|
+
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1627
|
+
const currentName = cmd.name();
|
|
1628
|
+
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1629
|
+
for (const k of pluginKeys) {
|
|
1630
|
+
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1631
|
+
const rows = byGroup.get(k) ?? [];
|
|
1632
|
+
if (rows.length > 0 && id !== currentName) {
|
|
1633
|
+
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return out;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1484
1639
|
const HOST_META_URL = import.meta.url;
|
|
1485
1640
|
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
1486
1641
|
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
@@ -1493,8 +1648,6 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
|
1493
1648
|
* - Expose a stable accessor for the current context (getCtx).
|
|
1494
1649
|
* - Provide a namespacing helper (ns).
|
|
1495
1650
|
* - Support composable plugins with parent → children install and afterResolve.
|
|
1496
|
-
*
|
|
1497
|
-
* NOTE: This host is additive and does not alter the legacy CLI.
|
|
1498
1651
|
*/
|
|
1499
1652
|
let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
1500
1653
|
/** Registered top-level plugins (composition happens via .use()) */
|
|
@@ -1503,6 +1656,17 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1503
1656
|
_installed = false;
|
|
1504
1657
|
/** Optional header line to prepend in help output */
|
|
1505
1658
|
[HELP_HEADER_SYMBOL];
|
|
1659
|
+
/** Context/options stored under symbols (typed) */
|
|
1660
|
+
[CTX_SYMBOL];
|
|
1661
|
+
[OPTS_SYMBOL];
|
|
1662
|
+
/**
|
|
1663
|
+
* Create a subcommand using the same subclass, preserving helpers like
|
|
1664
|
+
* dynamicOption on children.
|
|
1665
|
+
*/
|
|
1666
|
+
createCommand(name) {
|
|
1667
|
+
// Explicitly construct a GetDotenvCli (drop subclass constructor semantics).
|
|
1668
|
+
return new GetDotenvCli(name);
|
|
1669
|
+
}
|
|
1506
1670
|
constructor(alias = 'getdotenv') {
|
|
1507
1671
|
super(alias);
|
|
1508
1672
|
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
@@ -1510,37 +1674,39 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1510
1674
|
// child uses passThroughOptions.
|
|
1511
1675
|
this.enablePositionalOptions();
|
|
1512
1676
|
// Configure grouped help: show only base options in default "Options";
|
|
1513
|
-
//
|
|
1677
|
+
// we will insert App/Plugin sections before Commands in helpInformation().
|
|
1514
1678
|
this.configureHelp({
|
|
1515
1679
|
visibleOptions: (cmd) => {
|
|
1516
|
-
const all = cmd.options
|
|
1517
|
-
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1680
|
+
const all = cmd.options;
|
|
1681
|
+
const isRoot = cmd.parent === null;
|
|
1682
|
+
const list = isRoot
|
|
1683
|
+
? all.filter((opt) => {
|
|
1684
|
+
const group = GROUP_TAG.get(opt);
|
|
1685
|
+
return group === 'base';
|
|
1686
|
+
})
|
|
1687
|
+
: all.slice(); // subcommands: show all options (their own "Options:" block)
|
|
1522
1688
|
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1523
1689
|
const hasShort = (opt) => {
|
|
1524
|
-
const flags = opt.flags
|
|
1690
|
+
const flags = opt.flags;
|
|
1525
1691
|
// Matches "-x," or starting "-x " before any long
|
|
1526
1692
|
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1527
1693
|
};
|
|
1528
|
-
const byFlags = (opt) => opt.flags
|
|
1529
|
-
|
|
1694
|
+
const byFlags = (opt) => opt.flags;
|
|
1695
|
+
list.sort((a, b) => {
|
|
1530
1696
|
const aS = hasShort(a) ? 1 : 0;
|
|
1531
1697
|
const bS = hasShort(b) ? 1 : 0;
|
|
1532
1698
|
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1533
1699
|
});
|
|
1534
|
-
return
|
|
1700
|
+
return list;
|
|
1535
1701
|
},
|
|
1536
1702
|
});
|
|
1537
1703
|
this.addHelpText('beforeAll', () => {
|
|
1538
1704
|
const header = this[HELP_HEADER_SYMBOL];
|
|
1539
1705
|
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1540
1706
|
});
|
|
1541
|
-
this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
|
|
1542
1707
|
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
1543
|
-
// mutating process.env. The passOptions hook (when installed) will
|
|
1708
|
+
// mutating process.env. The passOptions hook (when installed) will
|
|
1709
|
+
// compute the final context using merged CLI options; keeping
|
|
1544
1710
|
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
1545
1711
|
// process env before subcommands execute.
|
|
1546
1712
|
this.hook('preSubcommand', async () => {
|
|
@@ -1550,22 +1716,49 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1550
1716
|
});
|
|
1551
1717
|
}
|
|
1552
1718
|
/**
|
|
1553
|
-
* Resolve options (strict) and compute dotenv context.
|
|
1719
|
+
* Resolve options (strict) and compute dotenv context.
|
|
1720
|
+
* Stores the context on the instance under a symbol.
|
|
1721
|
+
*
|
|
1722
|
+
* Options:
|
|
1723
|
+
* - opts.runAfterResolve (default true): when false, skips running plugin
|
|
1724
|
+
* afterResolve hooks. Useful for top-level help rendering to avoid
|
|
1725
|
+
* long-running side-effects while still evaluating dynamic help text.
|
|
1554
1726
|
*/
|
|
1555
|
-
async resolveAndLoad(customOptions = {}) {
|
|
1727
|
+
async resolveAndLoad(customOptions = {}, opts) {
|
|
1556
1728
|
// Resolve defaults, then validate strictly under the new host.
|
|
1557
1729
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1558
1730
|
getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
1559
1731
|
// Delegate the heavy lifting to the shared helper (guarded path supported).
|
|
1560
1732
|
const ctx = await computeContext(optionsResolved, this._plugins, HOST_META_URL);
|
|
1561
1733
|
// Persist context on the instance for later access.
|
|
1562
|
-
this[CTX_SYMBOL] =
|
|
1563
|
-
ctx;
|
|
1734
|
+
this[CTX_SYMBOL] = ctx;
|
|
1564
1735
|
// Ensure plugins are installed exactly once, then run afterResolve.
|
|
1565
1736
|
await this.install();
|
|
1566
|
-
|
|
1737
|
+
if (opts?.runAfterResolve ?? true) {
|
|
1738
|
+
await this._runAfterResolve(ctx);
|
|
1739
|
+
}
|
|
1567
1740
|
return ctx;
|
|
1568
1741
|
}
|
|
1742
|
+
// Implementation
|
|
1743
|
+
createDynamicOption(flags, desc, parser, defaultValue) {
|
|
1744
|
+
return makeDynamicOption(flags, (c) => desc(c), parser, defaultValue);
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Chainable helper mirroring .option(), but with a dynamic description.
|
|
1748
|
+
* Equivalent to addOption(createDynamicOption(...)).
|
|
1749
|
+
*/
|
|
1750
|
+
dynamicOption(flags, desc, parser, defaultValue) {
|
|
1751
|
+
this.addOption(this.createDynamicOption(flags, desc, parser, defaultValue));
|
|
1752
|
+
return this;
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Evaluate dynamic descriptions for this command and all descendants using
|
|
1756
|
+
* the provided resolved configuration. Mutates the Option.description in
|
|
1757
|
+
* place so Commander help renders updated text.
|
|
1758
|
+
*/
|
|
1759
|
+
evaluateDynamicOptions(resolved) {
|
|
1760
|
+
evaluateDynamicOptions(this, resolved);
|
|
1761
|
+
}
|
|
1569
1762
|
/**
|
|
1570
1763
|
* Retrieve the current invocation context (if any).
|
|
1571
1764
|
*/
|
|
@@ -1583,9 +1776,13 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1583
1776
|
_setOptionsBag(bag) {
|
|
1584
1777
|
this[OPTS_SYMBOL] = bag;
|
|
1585
1778
|
}
|
|
1586
|
-
/**
|
|
1587
|
-
*/
|
|
1779
|
+
/** Convenience helper to create a namespaced subcommand. */
|
|
1588
1780
|
ns(name) {
|
|
1781
|
+
// Guard against same-level duplicate command names for clearer diagnostics.
|
|
1782
|
+
const exists = this.commands.some((c) => c.name() === name);
|
|
1783
|
+
if (exists) {
|
|
1784
|
+
throw new Error(`Duplicate command name: ${name}`);
|
|
1785
|
+
}
|
|
1589
1786
|
return this.command(name);
|
|
1590
1787
|
}
|
|
1591
1788
|
/**
|
|
@@ -1595,29 +1792,15 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1595
1792
|
tagAppOptions(fn) {
|
|
1596
1793
|
const root = this;
|
|
1597
1794
|
const originalAddOption = root.addOption.bind(root);
|
|
1598
|
-
const originalOption = root.option.bind(root);
|
|
1599
|
-
const tagLatest = (cmd, group) => {
|
|
1600
|
-
const optsArr = cmd.options;
|
|
1601
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
1602
|
-
const last = optsArr[optsArr.length - 1];
|
|
1603
|
-
last.__group = group;
|
|
1604
|
-
}
|
|
1605
|
-
};
|
|
1606
1795
|
root.addOption = function patchedAdd(opt) {
|
|
1607
|
-
opt
|
|
1796
|
+
root.setOptionGroup(opt, 'app');
|
|
1608
1797
|
return originalAddOption(opt);
|
|
1609
1798
|
};
|
|
1610
|
-
root.option = function patchedOption(...args) {
|
|
1611
|
-
const ret = originalOption(...args);
|
|
1612
|
-
tagLatest(this, 'app');
|
|
1613
|
-
return ret;
|
|
1614
|
-
};
|
|
1615
1799
|
try {
|
|
1616
1800
|
return fn(root);
|
|
1617
1801
|
}
|
|
1618
1802
|
finally {
|
|
1619
1803
|
root.addOption = originalAddOption;
|
|
1620
|
-
root.option = originalOption;
|
|
1621
1804
|
}
|
|
1622
1805
|
}
|
|
1623
1806
|
/**
|
|
@@ -1656,12 +1839,51 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1656
1839
|
this[HELP_HEADER_SYMBOL] = helpHeader;
|
|
1657
1840
|
}
|
|
1658
1841
|
else if (v) {
|
|
1659
|
-
// Use the current command name (possibly overridden by 'name' above).
|
|
1660
1842
|
const header = `${this.name()} v${v}`;
|
|
1661
1843
|
this[HELP_HEADER_SYMBOL] = header;
|
|
1662
1844
|
}
|
|
1663
1845
|
return this;
|
|
1664
1846
|
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Insert grouped plugin/app options between "Options" and "Commands" for
|
|
1849
|
+
* hybrid ordering. Applies to root and any parent command.
|
|
1850
|
+
*/
|
|
1851
|
+
helpInformation() {
|
|
1852
|
+
// Base help text first (includes beforeAll/after hooks).
|
|
1853
|
+
const base = super.helpInformation();
|
|
1854
|
+
const groups = renderOptionGroups(this);
|
|
1855
|
+
const block = typeof groups === 'string' ? groups.trim() : '';
|
|
1856
|
+
let out = base;
|
|
1857
|
+
if (!block) {
|
|
1858
|
+
// Ensure a trailing blank line even when no extra groups render.
|
|
1859
|
+
if (!out.endsWith('\n\n'))
|
|
1860
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1861
|
+
return out;
|
|
1862
|
+
}
|
|
1863
|
+
// Insert just before "Commands:" when present.
|
|
1864
|
+
const marker = '\nCommands:';
|
|
1865
|
+
const idx = base.indexOf(marker);
|
|
1866
|
+
if (idx >= 0) {
|
|
1867
|
+
const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
|
|
1868
|
+
out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
|
|
1869
|
+
}
|
|
1870
|
+
else {
|
|
1871
|
+
// Otherwise append.
|
|
1872
|
+
const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
|
|
1873
|
+
out = `${base}${sep}${groups}`;
|
|
1874
|
+
}
|
|
1875
|
+
// Ensure a trailing blank line for prompt separation.
|
|
1876
|
+
if (!out.endsWith('\n\n')) {
|
|
1877
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1878
|
+
}
|
|
1879
|
+
return out;
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Public: tag an Option with a display group for help (root/app/plugin:<id>).
|
|
1883
|
+
*/
|
|
1884
|
+
setOptionGroup(opt, group) {
|
|
1885
|
+
GROUP_TAG.set(opt, group);
|
|
1886
|
+
}
|
|
1665
1887
|
/**
|
|
1666
1888
|
* Register a plugin for installation (parent level).
|
|
1667
1889
|
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
@@ -1700,58 +1922,18 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1700
1922
|
for (const p of this._plugins)
|
|
1701
1923
|
await run(p);
|
|
1702
1924
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
description: opt.description ?? '',
|
|
1716
|
-
});
|
|
1717
|
-
byGroup.set(g, rows);
|
|
1718
|
-
}
|
|
1719
|
-
if (byGroup.size === 0)
|
|
1720
|
-
return '';
|
|
1721
|
-
const renderRows = (title, rows) => {
|
|
1722
|
-
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
1723
|
-
// Sort within group: short-aliased flags first
|
|
1724
|
-
rows.sort((a, b) => {
|
|
1725
|
-
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
1726
|
-
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
1727
|
-
return bS - aS || a.flags.localeCompare(b.flags);
|
|
1728
|
-
});
|
|
1729
|
-
const lines = rows
|
|
1730
|
-
.map((r) => {
|
|
1731
|
-
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
1732
|
-
return ` ${r.flags}${pad}${r.description}`.trimEnd();
|
|
1733
|
-
})
|
|
1734
|
-
.join('\n');
|
|
1735
|
-
return `\n${title}:\n${lines}\n`;
|
|
1736
|
-
};
|
|
1737
|
-
let out = '';
|
|
1738
|
-
// App options (if any)
|
|
1739
|
-
const app = byGroup.get('app');
|
|
1740
|
-
if (app && app.length > 0) {
|
|
1741
|
-
out += renderRows('App options', app);
|
|
1742
|
-
}
|
|
1743
|
-
// Plugin groups sorted by id
|
|
1744
|
-
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1745
|
-
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1746
|
-
for (const k of pluginKeys) {
|
|
1747
|
-
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1748
|
-
const rows = byGroup.get(k) ?? [];
|
|
1749
|
-
if (rows.length > 0) {
|
|
1750
|
-
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
return out;
|
|
1754
|
-
}
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Build a help-time configuration bag for dynamic option descriptions.
|
|
1929
|
+
* Centralizes construction and reduces inline casts at call sites.
|
|
1930
|
+
*/
|
|
1931
|
+
const toHelpConfig = (merged, plugins) => {
|
|
1932
|
+
const bag = {
|
|
1933
|
+
...merged,
|
|
1934
|
+
plugins: plugins ?? {},
|
|
1935
|
+
};
|
|
1936
|
+
return bag;
|
|
1755
1937
|
};
|
|
1756
1938
|
|
|
1757
1939
|
/** src/cliHost/definePlugin.ts
|
|
@@ -1761,26 +1943,59 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1761
1943
|
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
1762
1944
|
* nominal class identity issues (private fields) in downstream consumers.
|
|
1763
1945
|
*/
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
*
|
|
1767
|
-
* @example
|
|
1768
|
-
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
1769
|
-
* .use(childA)
|
|
1770
|
-
* .use(childB);
|
|
1771
|
-
*/
|
|
1772
|
-
const definePlugin = (spec) => {
|
|
1946
|
+
/* eslint-disable tsdoc/syntax */
|
|
1947
|
+
function definePlugin(spec) {
|
|
1773
1948
|
const { children = [], ...rest } = spec;
|
|
1774
|
-
|
|
1949
|
+
// Default to a strict empty-object schema so “no-config” plugins fail fast
|
|
1950
|
+
// on unknown keys and provide a concrete {} at runtime.
|
|
1951
|
+
const effectiveSchema = spec.configSchema ?? z.object({}).strict();
|
|
1952
|
+
// Build base plugin first, then extend with instance-bound helpers.
|
|
1953
|
+
const base = {
|
|
1775
1954
|
...rest,
|
|
1955
|
+
// Always carry a schema (strict empty by default) to simplify host logic
|
|
1956
|
+
// and improve inference/ergonomics for plugin authors.
|
|
1957
|
+
configSchema: effectiveSchema,
|
|
1776
1958
|
children: [...children],
|
|
1777
1959
|
use(child) {
|
|
1778
1960
|
this.children.push(child);
|
|
1779
1961
|
return this;
|
|
1780
1962
|
},
|
|
1781
1963
|
};
|
|
1782
|
-
|
|
1783
|
-
|
|
1964
|
+
// Attach instance-bound helpers on the returned plugin object.
|
|
1965
|
+
const extended = base;
|
|
1966
|
+
extended.readConfig = function (_cli) {
|
|
1967
|
+
// Config is stored per-plugin-instance by the host (WeakMap in computeContext).
|
|
1968
|
+
const value = _getPluginConfigForInstance(extended);
|
|
1969
|
+
if (value === undefined) {
|
|
1970
|
+
// Guard: host has not resolved config yet (incorrect lifecycle usage).
|
|
1971
|
+
throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
|
|
1972
|
+
}
|
|
1973
|
+
return value;
|
|
1974
|
+
};
|
|
1975
|
+
// Plugin-bound dynamic option factory
|
|
1976
|
+
extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
|
|
1977
|
+
return cli.createDynamicOption(flags, (cfg) => {
|
|
1978
|
+
// Prefer the validated slice stored per instance; fallback to help-bag
|
|
1979
|
+
// (by-id) so top-level `-h` can render effective defaults before resolve.
|
|
1980
|
+
const fromStore = _getPluginConfigForInstance(extended);
|
|
1981
|
+
const id = extended.id;
|
|
1982
|
+
let fromBag;
|
|
1983
|
+
if (!fromStore && id) {
|
|
1984
|
+
const maybe = cfg.plugins[id];
|
|
1985
|
+
if (maybe && typeof maybe === 'object') {
|
|
1986
|
+
fromBag = maybe;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
// Always provide a concrete object to dynamic callbacks:
|
|
1990
|
+
// - With a schema: computeContext stores the parsed object.
|
|
1991
|
+
// - Without a schema: computeContext stores {}.
|
|
1992
|
+
// - Help-time fallback: coalesce to {} when only a by-id bag exists.
|
|
1993
|
+
const cfgVal = (fromStore ?? fromBag ?? {});
|
|
1994
|
+
return desc(cfg, cfgVal);
|
|
1995
|
+
}, parser, defaultValue);
|
|
1996
|
+
};
|
|
1997
|
+
return extended;
|
|
1998
|
+
}
|
|
1784
1999
|
|
|
1785
2000
|
/**
|
|
1786
2001
|
* GetDotenvCli with root helpers as real class methods.
|
|
@@ -1793,9 +2008,9 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1793
2008
|
* Attach legacy root flags to this CLI instance. Defaults come from
|
|
1794
2009
|
* baseRootOptionDefaults when none are provided.
|
|
1795
2010
|
*/
|
|
1796
|
-
attachRootOptions(defaults
|
|
2011
|
+
attachRootOptions(defaults) {
|
|
1797
2012
|
const d = (defaults ?? baseRootOptionDefaults);
|
|
1798
|
-
attachRootOptions(this, d
|
|
2013
|
+
attachRootOptions(this, d);
|
|
1799
2014
|
return this;
|
|
1800
2015
|
}
|
|
1801
2016
|
/**
|
|
@@ -1817,10 +2032,19 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1817
2032
|
// Build service options and compute context (always-on loader path).
|
|
1818
2033
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1819
2034
|
await this.resolveAndLoad(serviceOptions);
|
|
2035
|
+
// Refresh dynamic option descriptions using resolved config + plugin slices
|
|
2036
|
+
try {
|
|
2037
|
+
const ctx = this.getCtx();
|
|
2038
|
+
const helpCfg = toHelpConfig(merged, ctx?.pluginConfigs ?? {});
|
|
2039
|
+
this.evaluateDynamicOptions(helpCfg);
|
|
2040
|
+
}
|
|
2041
|
+
catch {
|
|
2042
|
+
/* best-effort */
|
|
2043
|
+
}
|
|
1820
2044
|
// Global validation: once after Phase C using config sources.
|
|
1821
2045
|
try {
|
|
1822
2046
|
const ctx = this.getCtx();
|
|
1823
|
-
const dotenv =
|
|
2047
|
+
const dotenv = ctx?.dotenv ?? {};
|
|
1824
2048
|
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
1825
2049
|
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1826
2050
|
if (Array.isArray(issues) && issues.length > 0) {
|
|
@@ -1830,9 +2054,8 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1830
2054
|
issues.forEach((m) => {
|
|
1831
2055
|
emit(m);
|
|
1832
2056
|
});
|
|
1833
|
-
if (merged.strict)
|
|
2057
|
+
if (merged.strict)
|
|
1834
2058
|
process.exit(1);
|
|
1835
|
-
}
|
|
1836
2059
|
}
|
|
1837
2060
|
}
|
|
1838
2061
|
catch {
|
|
@@ -1851,6 +2074,14 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1851
2074
|
if (!this.getCtx()) {
|
|
1852
2075
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1853
2076
|
await this.resolveAndLoad(serviceOptions);
|
|
2077
|
+
try {
|
|
2078
|
+
const ctx = this.getCtx();
|
|
2079
|
+
const helpCfg = toHelpConfig(merged, ctx?.pluginConfigs ?? {});
|
|
2080
|
+
this.evaluateDynamicOptions(helpCfg);
|
|
2081
|
+
}
|
|
2082
|
+
catch {
|
|
2083
|
+
/* tolerate */
|
|
2084
|
+
}
|
|
1854
2085
|
try {
|
|
1855
2086
|
const ctx = this.getCtx();
|
|
1856
2087
|
const dotenv = (ctx?.dotenv ?? {});
|
|
@@ -1879,20 +2110,33 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1879
2110
|
|
|
1880
2111
|
// Minimal tokenizer for shell-off execution:
|
|
1881
2112
|
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
1882
|
-
|
|
2113
|
+
// Optionally preserve doubled quotes inside quoted segments:
|
|
2114
|
+
// - default: "" => " (Windows/PowerShell style literal-quote escape)
|
|
2115
|
+
// - preserveDoubledQuotes: true => "" stays "" (needed for Node -e payloads)
|
|
2116
|
+
const tokenize = (command, opts) => {
|
|
1883
2117
|
const out = [];
|
|
1884
2118
|
let cur = '';
|
|
1885
2119
|
let quote = null;
|
|
2120
|
+
const preserve = opts && opts.preserveDoubledQuotes === true ? true : false;
|
|
1886
2121
|
for (let i = 0; i < command.length; i++) {
|
|
1887
2122
|
const c = command.charAt(i);
|
|
1888
2123
|
if (quote) {
|
|
1889
2124
|
if (c === quote) {
|
|
1890
|
-
// Support doubled quotes inside a quoted segment
|
|
1891
|
-
// "" -> " and '' -> '
|
|
2125
|
+
// Support doubled quotes inside a quoted segment:
|
|
2126
|
+
// default: "" -> " and '' -> ' (Windows/PowerShell style)
|
|
2127
|
+
// preserve: keep as "" to allow empty string literals in Node -e payloads
|
|
1892
2128
|
const next = command.charAt(i + 1);
|
|
1893
2129
|
if (next === quote) {
|
|
1894
|
-
|
|
1895
|
-
|
|
2130
|
+
if (preserve) {
|
|
2131
|
+
// Keep "" as-is; append both and continue within the quoted segment.
|
|
2132
|
+
cur += quote + quote;
|
|
2133
|
+
i += 1; // skip the second quote char (we already appended both)
|
|
2134
|
+
}
|
|
2135
|
+
else {
|
|
2136
|
+
// Collapse to a single literal quote
|
|
2137
|
+
cur += quote;
|
|
2138
|
+
i += 1; // skip the second quote
|
|
2139
|
+
}
|
|
1896
2140
|
}
|
|
1897
2141
|
else {
|
|
1898
2142
|
// end of quoted segment
|
|
@@ -1966,62 +2210,55 @@ const sanitizeEnv = (env) => {
|
|
|
1966
2210
|
const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
|
|
1967
2211
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
1968
2212
|
};
|
|
1969
|
-
|
|
1970
|
-
* Execute a command and capture stdout/stderr (buffered).
|
|
1971
|
-
* - Preserves plain vs shell behavior and argv/string normalization.
|
|
1972
|
-
* - Never re-emits stdout/stderr to parent; returns captured buffers.
|
|
1973
|
-
* - Supports optional timeout (ms).
|
|
1974
|
-
*/
|
|
1975
|
-
const runCommandResult = async (command, shell, opts = {}) => {
|
|
2213
|
+
async function runCommandResult(command, shell, opts = {}) {
|
|
1976
2214
|
const envSan = sanitizeEnv(opts.env);
|
|
1977
2215
|
{
|
|
1978
2216
|
let file;
|
|
1979
2217
|
let args = [];
|
|
1980
|
-
if (
|
|
1981
|
-
file = command[0];
|
|
1982
|
-
args = command.slice(1).map(stripOuterQuotes);
|
|
1983
|
-
}
|
|
1984
|
-
else {
|
|
2218
|
+
if (typeof command === 'string') {
|
|
1985
2219
|
const tokens = tokenize(command);
|
|
1986
2220
|
file = tokens[0];
|
|
1987
2221
|
args = tokens.slice(1);
|
|
1988
2222
|
}
|
|
2223
|
+
else {
|
|
2224
|
+
file = command[0];
|
|
2225
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
2226
|
+
}
|
|
1989
2227
|
if (!file)
|
|
1990
2228
|
return { exitCode: 0, stdout: '', stderr: '' };
|
|
1991
2229
|
dbg$1('exec:capture (plain)', { file, args });
|
|
1992
2230
|
try {
|
|
1993
|
-
const
|
|
2231
|
+
const ok = pickResult((await execa(file, args, {
|
|
1994
2232
|
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
1995
2233
|
...(envSan !== undefined ? { env: envSan } : {}),
|
|
1996
2234
|
stdio: 'pipe',
|
|
1997
2235
|
...(opts.timeoutMs !== undefined
|
|
1998
2236
|
? { timeout: opts.timeoutMs, killSignal: 'SIGKILL' }
|
|
1999
2237
|
: {}),
|
|
2000
|
-
});
|
|
2001
|
-
const ok = pickResult(result);
|
|
2238
|
+
})));
|
|
2002
2239
|
dbg$1('exit:capture (plain)', { exitCode: ok.exitCode });
|
|
2003
2240
|
return ok;
|
|
2004
2241
|
}
|
|
2005
|
-
catch (
|
|
2006
|
-
const out = pickResult(
|
|
2242
|
+
catch (e) {
|
|
2243
|
+
const out = pickResult(e);
|
|
2007
2244
|
dbg$1('exit:capture:error (plain)', { exitCode: out.exitCode });
|
|
2008
2245
|
return out;
|
|
2009
2246
|
}
|
|
2010
2247
|
}
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2248
|
+
}
|
|
2249
|
+
async function runCommand(command, shell, opts) {
|
|
2013
2250
|
if (shell === false) {
|
|
2014
2251
|
let file;
|
|
2015
2252
|
let args = [];
|
|
2016
|
-
if (
|
|
2017
|
-
file = command[0];
|
|
2018
|
-
args = command.slice(1).map(stripOuterQuotes);
|
|
2019
|
-
}
|
|
2020
|
-
else {
|
|
2253
|
+
if (typeof command === 'string') {
|
|
2021
2254
|
const tokens = tokenize(command);
|
|
2022
2255
|
file = tokens[0];
|
|
2023
2256
|
args = tokens.slice(1);
|
|
2024
2257
|
}
|
|
2258
|
+
else {
|
|
2259
|
+
file = command[0];
|
|
2260
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
2261
|
+
}
|
|
2025
2262
|
if (!file)
|
|
2026
2263
|
return 0;
|
|
2027
2264
|
dbg$1('exec (plain)', { file, args, stdio: opts.stdio });
|
|
@@ -2034,16 +2271,15 @@ const runCommand = async (command, shell, opts) => {
|
|
|
2034
2271
|
plainOpts.env = envSan;
|
|
2035
2272
|
if (opts.stdio !== undefined)
|
|
2036
2273
|
plainOpts.stdio = opts.stdio;
|
|
2037
|
-
const
|
|
2038
|
-
if (opts.stdio === 'pipe' &&
|
|
2039
|
-
process.stdout.write(
|
|
2274
|
+
const ok = pickResult((await execa(file, args, plainOpts)));
|
|
2275
|
+
if (opts.stdio === 'pipe' && ok.stdout) {
|
|
2276
|
+
process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
|
|
2040
2277
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
return typeof exit === 'number' ? exit : Number.NaN;
|
|
2278
|
+
dbg$1('exit (plain)', { exitCode: ok.exitCode });
|
|
2279
|
+
return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
|
|
2044
2280
|
}
|
|
2045
2281
|
else {
|
|
2046
|
-
const commandStr =
|
|
2282
|
+
const commandStr = typeof command === 'string' ? command : command.join(' ');
|
|
2047
2283
|
dbg$1('exec (shell)', {
|
|
2048
2284
|
shell: typeof shell === 'string' ? shell : 'custom',
|
|
2049
2285
|
stdio: opts.stdio,
|
|
@@ -2057,17 +2293,29 @@ const runCommand = async (command, shell, opts) => {
|
|
|
2057
2293
|
shellOpts.env = envSan;
|
|
2058
2294
|
if (opts.stdio !== undefined)
|
|
2059
2295
|
shellOpts.stdio = opts.stdio;
|
|
2060
|
-
const
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
|
|
2296
|
+
const ok = pickResult((await execaCommand(commandStr, shellOpts)));
|
|
2297
|
+
if (opts.stdio === 'pipe' && ok.stdout) {
|
|
2298
|
+
process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
|
|
2064
2299
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
return typeof exit === 'number' ? exit : Number.NaN;
|
|
2300
|
+
dbg$1('exit (shell)', { exitCode: ok.exitCode });
|
|
2301
|
+
return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
|
|
2068
2302
|
}
|
|
2069
|
-
}
|
|
2303
|
+
}
|
|
2070
2304
|
|
|
2305
|
+
/** src/cliCore/spawnEnv.ts
|
|
2306
|
+
* Build a sanitized environment bag for child processes.
|
|
2307
|
+
*
|
|
2308
|
+
* Requirements addressed:
|
|
2309
|
+
* - Provide a single helper (buildSpawnEnv) to normalize/dedupe child env.
|
|
2310
|
+
* - Drop undefined values (exactOptional semantics).
|
|
2311
|
+
* - On Windows, dedupe keys case-insensitively and prefer the last value,
|
|
2312
|
+
* preserving the latest key's casing. Ensure HOME fallback from USERPROFILE.
|
|
2313
|
+
* Normalize TMP/TEMP consistency when either is present.
|
|
2314
|
+
* - On POSIX, keep keys as-is; when a temp dir key is present (TMPDIR/TMP/TEMP),
|
|
2315
|
+
* ensure TMPDIR exists for downstream consumers that expect it.
|
|
2316
|
+
*
|
|
2317
|
+
* Adapter responsibility: pure mapping; no business logic.
|
|
2318
|
+
*/
|
|
2071
2319
|
const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
|
|
2072
2320
|
/** Build a sanitized env for child processes from base + overlay. */
|
|
2073
2321
|
const buildSpawnEnv = (base, overlay) => {
|
|
@@ -2332,90 +2580,79 @@ const AwsPluginConfigSchema = z.object({
|
|
|
2332
2580
|
regionKey: z.string().default('AWS_REGION').optional(),
|
|
2333
2581
|
strategy: z.enum(['cli-export', 'none']).default('cli-export').optional(),
|
|
2334
2582
|
loginOnDemand: z.boolean().default(false).optional(),
|
|
2335
|
-
setEnv: z.boolean().default(true).optional(),
|
|
2336
|
-
addCtx: z.boolean().default(true).optional(),
|
|
2337
2583
|
});
|
|
2338
2584
|
|
|
2339
|
-
const awsPlugin = () =>
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
...overlay,
|
|
2411
|
-
};
|
|
2412
|
-
// Resolve current context with overrides
|
|
2413
|
-
const out = await resolveAwsContext({
|
|
2414
|
-
dotenv: ctx?.dotenv ?? {},
|
|
2415
|
-
cfg,
|
|
2416
|
-
});
|
|
2417
|
-
// Apply env/ctx mirrors per toggles
|
|
2418
|
-
if (cfg.setEnv !== false) {
|
|
2585
|
+
const awsPlugin = () => {
|
|
2586
|
+
const plugin = definePlugin({
|
|
2587
|
+
id: 'aws',
|
|
2588
|
+
// Host validates this slice when the loader path is active.
|
|
2589
|
+
configSchema: AwsPluginConfigSchema,
|
|
2590
|
+
setup(cli) {
|
|
2591
|
+
// Subcommand: aws
|
|
2592
|
+
cli
|
|
2593
|
+
.ns('aws')
|
|
2594
|
+
.description('Establish an AWS session and optionally forward to the AWS CLI')
|
|
2595
|
+
.enablePositionalOptions()
|
|
2596
|
+
.passThroughOptions()
|
|
2597
|
+
.allowUnknownOption(true)
|
|
2598
|
+
// Boolean toggles with dynamic help labels (effective defaults)
|
|
2599
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
|
|
2600
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
|
|
2601
|
+
// Strings / enums
|
|
2602
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
|
|
2603
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
|
|
2604
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
|
|
2605
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
|
|
2606
|
+
// Advanced key overrides
|
|
2607
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
|
|
2608
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
|
|
2609
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
|
|
2610
|
+
// Accept any extra operands so Commander does not error when tokens appear after "--".
|
|
2611
|
+
.argument('[args...]')
|
|
2612
|
+
.action(async (args, opts, thisCommand) => {
|
|
2613
|
+
const pluginInst = plugin;
|
|
2614
|
+
const cmdSelf = thisCommand;
|
|
2615
|
+
const parent = (cmdSelf.parent ?? null);
|
|
2616
|
+
// Access merged root CLI options (installed by passOptions())
|
|
2617
|
+
const rootOpts = (parent?.getDotenvCliOptions ?? {});
|
|
2618
|
+
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
2619
|
+
Boolean(rootOpts?.capture);
|
|
2620
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
2621
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
2622
|
+
// Build overlay cfg from subcommand flags layered over discovered config.
|
|
2623
|
+
const ctx = cli.getCtx();
|
|
2624
|
+
const cfgBase = pluginInst.readConfig(cli);
|
|
2625
|
+
const o = opts;
|
|
2626
|
+
const overlay = {};
|
|
2627
|
+
// Map boolean toggles (respect explicit --no-*)
|
|
2628
|
+
if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
|
|
2629
|
+
overlay.loginOnDemand = Boolean(o.loginOnDemand);
|
|
2630
|
+
// Strings/enums
|
|
2631
|
+
if (typeof o.profile === 'string')
|
|
2632
|
+
overlay.profile = o.profile;
|
|
2633
|
+
if (typeof o.region === 'string')
|
|
2634
|
+
overlay.region = o.region;
|
|
2635
|
+
if (typeof o.defaultRegion === 'string')
|
|
2636
|
+
overlay.defaultRegion = o.defaultRegion;
|
|
2637
|
+
if (typeof o.strategy === 'string')
|
|
2638
|
+
overlay.strategy = o.strategy;
|
|
2639
|
+
// Advanced key overrides
|
|
2640
|
+
if (typeof o.profileKey === 'string')
|
|
2641
|
+
overlay.profileKey = o.profileKey;
|
|
2642
|
+
if (typeof o.profileFallbackKey === 'string')
|
|
2643
|
+
overlay.profileFallbackKey = o.profileFallbackKey;
|
|
2644
|
+
if (typeof o.regionKey === 'string')
|
|
2645
|
+
overlay.regionKey = o.regionKey;
|
|
2646
|
+
const cfg = {
|
|
2647
|
+
...cfgBase,
|
|
2648
|
+
...overlay,
|
|
2649
|
+
};
|
|
2650
|
+
// Resolve current context with overrides
|
|
2651
|
+
const out = await resolveAwsContext({
|
|
2652
|
+
dotenv: ctx?.dotenv ?? {},
|
|
2653
|
+
cfg,
|
|
2654
|
+
});
|
|
2655
|
+
// Unconditional env writes (no per-plugin toggle)
|
|
2419
2656
|
if (out.region) {
|
|
2420
2657
|
process.env.AWS_REGION = out.region;
|
|
2421
2658
|
if (!process.env.AWS_DEFAULT_REGION)
|
|
@@ -2429,58 +2666,53 @@ const awsPlugin = () => definePlugin({
|
|
|
2429
2666
|
process.env.AWS_SESSION_TOKEN = out.credentials.sessionToken;
|
|
2430
2667
|
}
|
|
2431
2668
|
}
|
|
2432
|
-
|
|
2433
|
-
if (cfg.addCtx !== false) {
|
|
2669
|
+
// Always publish minimal non-sensitive metadata
|
|
2434
2670
|
if (ctx) {
|
|
2435
2671
|
ctx.plugins ??= {};
|
|
2436
2672
|
ctx.plugins['aws'] = {
|
|
2437
2673
|
...(out.profile ? { profile: out.profile } : {}),
|
|
2438
2674
|
...(out.region ? { region: out.region } : {}),
|
|
2439
|
-
...(out.credentials ? { credentials: out.credentials } : {}),
|
|
2440
2675
|
};
|
|
2441
2676
|
}
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
env: buildSpawnEnv(process.env, ctxDotenv),
|
|
2450
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
2451
|
-
});
|
|
2452
|
-
// Deterministic termination (suppressed under tests)
|
|
2453
|
-
if (!underTests) {
|
|
2454
|
-
process.exit(typeof exit === 'number' ? exit : 0);
|
|
2455
|
-
}
|
|
2456
|
-
return;
|
|
2457
|
-
}
|
|
2458
|
-
else {
|
|
2459
|
-
// Session only: low-noise breadcrumb under debug
|
|
2460
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
2461
|
-
const log = console;
|
|
2462
|
-
log.log('[aws] session established', {
|
|
2463
|
-
profile: out.profile,
|
|
2464
|
-
region: out.region,
|
|
2465
|
-
hasCreds: Boolean(out.credentials),
|
|
2677
|
+
// Forward when positional args are present; otherwise session-only.
|
|
2678
|
+
if (Array.isArray(args) && args.length > 0) {
|
|
2679
|
+
const argv = ['aws', ...args];
|
|
2680
|
+
const shellSetting = resolveShell(rootOpts?.scripts, 'aws', rootOpts?.shell);
|
|
2681
|
+
const exit = await runCommand(argv, shellSetting, {
|
|
2682
|
+
env: buildSpawnEnv(process.env, ctx?.dotenv),
|
|
2683
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
2466
2684
|
});
|
|
2685
|
+
// Deterministic termination (suppressed under tests)
|
|
2686
|
+
if (!underTests) {
|
|
2687
|
+
process.exit(typeof exit === 'number' ? exit : 0);
|
|
2688
|
+
}
|
|
2689
|
+
return;
|
|
2467
2690
|
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2691
|
+
else {
|
|
2692
|
+
// Session only: low-noise breadcrumb under debug
|
|
2693
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
2694
|
+
const log = console;
|
|
2695
|
+
log.log('[aws] session established', {
|
|
2696
|
+
profile: out.profile,
|
|
2697
|
+
region: out.region,
|
|
2698
|
+
hasCreds: Boolean(out.credentials),
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
if (!underTests)
|
|
2702
|
+
process.exit(0);
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
},
|
|
2707
|
+
async afterResolve(_cli, ctx) {
|
|
2708
|
+
const log = console;
|
|
2709
|
+
const cfg = plugin.readConfig(_cli);
|
|
2710
|
+
const out = await resolveAwsContext({
|
|
2711
|
+
dotenv: ctx.dotenv,
|
|
2712
|
+
cfg,
|
|
2713
|
+
});
|
|
2714
|
+
const { profile, region, credentials } = out;
|
|
2715
|
+
// Unconditional env writes in host path
|
|
2484
2716
|
if (region) {
|
|
2485
2717
|
process.env.AWS_REGION = region;
|
|
2486
2718
|
if (!process.env.AWS_DEFAULT_REGION)
|
|
@@ -2493,25 +2725,24 @@ const awsPlugin = () => definePlugin({
|
|
|
2493
2725
|
process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
|
|
2494
2726
|
}
|
|
2495
2727
|
}
|
|
2496
|
-
|
|
2497
|
-
if (cfg.addCtx !== false) {
|
|
2728
|
+
// Always publish minimal non-sensitive metadata
|
|
2498
2729
|
ctx.plugins ??= {};
|
|
2499
2730
|
ctx.plugins['aws'] = {
|
|
2500
2731
|
...(profile ? { profile } : {}),
|
|
2501
2732
|
...(region ? { region } : {}),
|
|
2502
|
-
...(credentials ? { credentials } : {}),
|
|
2503
2733
|
};
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2734
|
+
// Optional: low-noise breadcrumb for diagnostics
|
|
2735
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
2736
|
+
log.log('[aws] afterResolve', {
|
|
2737
|
+
profile,
|
|
2738
|
+
region,
|
|
2739
|
+
hasCreds: Boolean(credentials),
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
},
|
|
2743
|
+
});
|
|
2744
|
+
return plugin;
|
|
2745
|
+
};
|
|
2515
2746
|
|
|
2516
2747
|
const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
2517
2748
|
let cwd = process.cwd();
|
|
@@ -2536,9 +2767,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
|
2536
2767
|
}
|
|
2537
2768
|
return { absRootPath, paths };
|
|
2538
2769
|
};
|
|
2539
|
-
const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
2770
|
+
const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
2540
2771
|
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
2541
|
-
Boolean(getDotenvCliOptions?.capture);
|
|
2772
|
+
Boolean(getDotenvCliOptions?.capture);
|
|
2773
|
+
// Require a command only when not listing. In list mode, a command is optional.
|
|
2542
2774
|
if (!command && !list) {
|
|
2543
2775
|
logger.error(`No command provided. Use --command or --list.`);
|
|
2544
2776
|
process.exit(0);
|
|
@@ -2585,12 +2817,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
2585
2817
|
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
2586
2818
|
(Array.isArray(command) && command.length > 0);
|
|
2587
2819
|
if (hasCmd) {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2820
|
+
// Compose child env overlay from dotenv (drop undefined) and merged options
|
|
2821
|
+
const overlay = {};
|
|
2822
|
+
if (dotenvEnv) {
|
|
2823
|
+
for (const [k, v] of Object.entries(dotenvEnv)) {
|
|
2824
|
+
if (typeof v === 'string')
|
|
2825
|
+
overlay[k] = v;
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
if (getDotenvCliOptions !== undefined) {
|
|
2829
|
+
try {
|
|
2830
|
+
overlay.getDotenvCliOptions = JSON.stringify(getDotenvCliOptions);
|
|
2831
|
+
}
|
|
2832
|
+
catch {
|
|
2833
|
+
// best-effort: omit if serialization fails
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2591
2836
|
await runCommand(command, shell, {
|
|
2592
2837
|
cwd: path,
|
|
2593
|
-
env: buildSpawnEnv(process.env,
|
|
2838
|
+
env: buildSpawnEnv(process.env, overlay),
|
|
2594
2839
|
stdio: capture ? 'pipe' : 'inherit',
|
|
2595
2840
|
});
|
|
2596
2841
|
}
|
|
@@ -2613,7 +2858,7 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
2613
2858
|
* Build the default "cmd" subcommand action for the batch plugin.
|
|
2614
2859
|
* Mirrors the original inline implementation with identical behavior.
|
|
2615
2860
|
*/
|
|
2616
|
-
const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _subOpts, _thisCommand) => {
|
|
2861
|
+
const buildDefaultCmdAction = (plugin, cli, batchCmd, opts) => async (commandParts, _subOpts, _thisCommand) => {
|
|
2617
2862
|
const loggerLocal = opts.logger ?? console;
|
|
2618
2863
|
// Guard: when invoked without positional args (e.g., `batch --list`),
|
|
2619
2864
|
// defer entirely to the parent action handler.
|
|
@@ -2625,9 +2870,8 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2625
2870
|
? argsRaw.filter((t) => t !== '-l' && t !== '--list')
|
|
2626
2871
|
: argsRaw;
|
|
2627
2872
|
// Access merged per-plugin config from host context (if any).
|
|
2628
|
-
const
|
|
2629
|
-
const
|
|
2630
|
-
const cfg = (cfgRaw || {});
|
|
2873
|
+
const cfg = plugin.readConfig(cli);
|
|
2874
|
+
const dotenvEnv = (cli.getCtx()?.dotenv ?? {});
|
|
2631
2875
|
// Resolve batch flags from the captured parent (batch) command.
|
|
2632
2876
|
const raw = batchCmd.opts();
|
|
2633
2877
|
const listFromParent = !!raw.list;
|
|
@@ -2646,6 +2890,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2646
2890
|
if (typeof commandOpt === 'string') {
|
|
2647
2891
|
await execShellCommandBatch({
|
|
2648
2892
|
command: resolveCommand(scripts, commandOpt),
|
|
2893
|
+
dotenvEnv,
|
|
2649
2894
|
globs,
|
|
2650
2895
|
ignoreErrors,
|
|
2651
2896
|
list: false,
|
|
@@ -2657,6 +2902,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2657
2902
|
return;
|
|
2658
2903
|
}
|
|
2659
2904
|
if (raw.list || localList) {
|
|
2905
|
+
const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
|
|
2660
2906
|
await execShellCommandBatch({
|
|
2661
2907
|
globs,
|
|
2662
2908
|
ignoreErrors,
|
|
@@ -2664,7 +2910,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2664
2910
|
logger: loggerLocal,
|
|
2665
2911
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2666
2912
|
rootPath,
|
|
2667
|
-
shell:
|
|
2913
|
+
shell: shell ?? shellBag.shell ?? false,
|
|
2668
2914
|
});
|
|
2669
2915
|
return;
|
|
2670
2916
|
}
|
|
@@ -2688,7 +2934,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2688
2934
|
logger: loggerLocal,
|
|
2689
2935
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2690
2936
|
rootPath,
|
|
2691
|
-
shell:
|
|
2937
|
+
shell: shell ?? shellBag.shell ?? false,
|
|
2692
2938
|
});
|
|
2693
2939
|
return;
|
|
2694
2940
|
}
|
|
@@ -2731,6 +2977,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2731
2977
|
}
|
|
2732
2978
|
await execShellCommandBatch({
|
|
2733
2979
|
command: commandArg,
|
|
2980
|
+
dotenvEnv,
|
|
2734
2981
|
...(envBag ? { getDotenvCliOptions: envBag } : {}),
|
|
2735
2982
|
globs,
|
|
2736
2983
|
ignoreErrors,
|
|
@@ -2745,12 +2992,11 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2745
2992
|
/**
|
|
2746
2993
|
* Build the parent "batch" action handler (no explicit subcommand).
|
|
2747
2994
|
*/
|
|
2748
|
-
const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
2749
|
-
const
|
|
2995
|
+
const buildParentAction = (plugin, cli, opts) => async (commandParts, thisCommand) => {
|
|
2996
|
+
const loggerLocal = opts.logger ?? console;
|
|
2750
2997
|
// Ensure context exists (host preSubcommand on root creates if missing).
|
|
2751
|
-
const
|
|
2752
|
-
const
|
|
2753
|
-
const cfg = (cfgRaw || {});
|
|
2998
|
+
const dotenvEnv = (cli.getCtx()?.dotenv ?? {});
|
|
2999
|
+
const cfg = plugin.readConfig(cli);
|
|
2754
3000
|
const raw = thisCommand.opts();
|
|
2755
3001
|
const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
|
|
2756
3002
|
const ignoreErrors = !!raw.ignoreErrors;
|
|
@@ -2771,10 +3017,11 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2771
3017
|
const commandArg = resolved;
|
|
2772
3018
|
await execShellCommandBatch({
|
|
2773
3019
|
command: commandArg,
|
|
3020
|
+
dotenvEnv,
|
|
2774
3021
|
globs,
|
|
2775
3022
|
ignoreErrors,
|
|
2776
3023
|
list: false,
|
|
2777
|
-
logger,
|
|
3024
|
+
logger: loggerLocal,
|
|
2778
3025
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2779
3026
|
rootPath,
|
|
2780
3027
|
shell: shellSetting,
|
|
@@ -2787,19 +3034,20 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2787
3034
|
if (extra.length > 0)
|
|
2788
3035
|
globs = [globs, extra].filter(Boolean).join(' ');
|
|
2789
3036
|
const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
|
|
3037
|
+
const shellMerged = opts.shell ?? cfg.shell ?? mergedBag.shell ?? false;
|
|
2790
3038
|
await execShellCommandBatch({
|
|
2791
3039
|
globs,
|
|
2792
3040
|
ignoreErrors,
|
|
2793
3041
|
list: true,
|
|
2794
|
-
logger,
|
|
3042
|
+
logger: loggerLocal,
|
|
2795
3043
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2796
3044
|
rootPath,
|
|
2797
|
-
shell:
|
|
3045
|
+
shell: shellMerged,
|
|
2798
3046
|
});
|
|
2799
3047
|
return;
|
|
2800
3048
|
}
|
|
2801
3049
|
if (!commandOpt && !list) {
|
|
2802
|
-
|
|
3050
|
+
loggerLocal.error(`No command provided. Use --command or --list.`);
|
|
2803
3051
|
process.exit(0);
|
|
2804
3052
|
}
|
|
2805
3053
|
if (typeof commandOpt === 'string') {
|
|
@@ -2808,10 +3056,11 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2808
3056
|
const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
|
|
2809
3057
|
await execShellCommandBatch({
|
|
2810
3058
|
command: resolveCommand(scriptsOpt, commandOpt),
|
|
3059
|
+
dotenvEnv,
|
|
2811
3060
|
globs,
|
|
2812
3061
|
ignoreErrors,
|
|
2813
3062
|
list,
|
|
2814
|
-
logger,
|
|
3063
|
+
logger: loggerLocal,
|
|
2815
3064
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2816
3065
|
rootPath,
|
|
2817
3066
|
shell: resolveShell(scriptsOpt, commandOpt, shellOpt),
|
|
@@ -2820,15 +3069,15 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2820
3069
|
}
|
|
2821
3070
|
// list only (explicit --list without --command)
|
|
2822
3071
|
const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
|
|
2823
|
-
const shellOnly =
|
|
3072
|
+
const shellOnly = opts.shell ?? cfg.shell ?? mergedBag.shell ?? false;
|
|
2824
3073
|
await execShellCommandBatch({
|
|
2825
3074
|
globs,
|
|
2826
3075
|
ignoreErrors,
|
|
2827
3076
|
list: true,
|
|
2828
|
-
logger,
|
|
3077
|
+
logger: loggerLocal,
|
|
2829
3078
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2830
3079
|
rootPath,
|
|
2831
|
-
shell:
|
|
3080
|
+
shell: shellOnly,
|
|
2832
3081
|
});
|
|
2833
3082
|
};
|
|
2834
3083
|
|
|
@@ -2851,38 +3100,57 @@ const BatchConfigSchema = z.object({
|
|
|
2851
3100
|
/**
|
|
2852
3101
|
* Batch plugin for the GetDotenv CLI host.
|
|
2853
3102
|
*
|
|
2854
|
-
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
3103
|
+
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
3104
|
+
* Options:
|
|
2855
3105
|
* - scripts/shell: used to resolve command and shell behavior per script or global default.
|
|
2856
3106
|
* - logger: defaults to console.
|
|
2857
3107
|
*/
|
|
2858
|
-
const batchPlugin = (opts = {}) =>
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
.
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
3108
|
+
const batchPlugin = (opts = {}) => {
|
|
3109
|
+
const plugin = definePlugin({
|
|
3110
|
+
id: 'batch',
|
|
3111
|
+
// Host validates this when config-loader is enabled; plugins may also
|
|
3112
|
+
// re-validate at action time as a safety belt.
|
|
3113
|
+
configSchema: BatchConfigSchema,
|
|
3114
|
+
setup(cli) {
|
|
3115
|
+
const ns = cli.ns('batch');
|
|
3116
|
+
const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
|
|
3117
|
+
const pluginId = 'batch';
|
|
3118
|
+
const GROUP = `plugin:${pluginId}`;
|
|
3119
|
+
ns.description('Batch command execution across multiple working directories.')
|
|
3120
|
+
.enablePositionalOptions()
|
|
3121
|
+
.passThroughOptions()
|
|
3122
|
+
// Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
|
|
3123
|
+
.addOption((() => {
|
|
3124
|
+
const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
|
|
3125
|
+
cli.setOptionGroup(opt, GROUP);
|
|
3126
|
+
return opt;
|
|
3127
|
+
})())
|
|
3128
|
+
.addOption((() => {
|
|
3129
|
+
const opt = plugin.createPluginDynamicOption(cli, '-r, --root-path <string>', (_bag, cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.rootPath || './')})`);
|
|
3130
|
+
cli.setOptionGroup(opt, GROUP);
|
|
3131
|
+
return opt;
|
|
3132
|
+
})())
|
|
3133
|
+
.addOption((() => {
|
|
3134
|
+
const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
|
|
3135
|
+
cli.setOptionGroup(opt, GROUP);
|
|
3136
|
+
return opt;
|
|
3137
|
+
})())
|
|
3138
|
+
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
3139
|
+
.option('-l, --list', 'list working directories without executing command')
|
|
3140
|
+
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
3141
|
+
.argument('[command...]')
|
|
3142
|
+
.addCommand(new Command()
|
|
3143
|
+
.name('cmd')
|
|
3144
|
+
.description('execute command, conflicts with --command option (default subcommand)')
|
|
3145
|
+
.enablePositionalOptions()
|
|
3146
|
+
.passThroughOptions()
|
|
3147
|
+
.argument('[command...]')
|
|
3148
|
+
.action(buildDefaultCmdAction(plugin, cli, batchCmd, opts)), { isDefault: true })
|
|
3149
|
+
.action(buildParentAction(plugin, cli, opts));
|
|
3150
|
+
},
|
|
3151
|
+
});
|
|
3152
|
+
return plugin;
|
|
3153
|
+
};
|
|
2886
3154
|
|
|
2887
3155
|
const dbg = (...args) => {
|
|
2888
3156
|
if (process.env.GETDOTENV_DEBUG) {
|
|
@@ -2890,14 +3158,219 @@ const dbg = (...args) => {
|
|
|
2890
3158
|
console.error('[getdotenv:alias]', ...args);
|
|
2891
3159
|
}
|
|
2892
3160
|
};
|
|
3161
|
+
// Strip one symmetric outer quote layer
|
|
3162
|
+
const stripOne = (s) => {
|
|
3163
|
+
if (s.length < 2)
|
|
3164
|
+
return s;
|
|
3165
|
+
const a = s.charAt(0);
|
|
3166
|
+
const b = s.charAt(s.length - 1);
|
|
3167
|
+
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
3168
|
+
return symmetric ? s.slice(1, -1) : s;
|
|
3169
|
+
};
|
|
3170
|
+
async function maybeRunAlias(cli, thisCommand, aliasKey, state) {
|
|
3171
|
+
dbg('alias:maybe:start');
|
|
3172
|
+
const raw = thisCommand.rawArgs ?? [];
|
|
3173
|
+
const childNames = thisCommand.commands.flatMap((c) => [
|
|
3174
|
+
c.name(),
|
|
3175
|
+
...c.aliases(),
|
|
3176
|
+
]);
|
|
3177
|
+
const hasSub = childNames.some((n) => raw.includes(n));
|
|
3178
|
+
const o = thisCommand.opts();
|
|
3179
|
+
const val = o[aliasKey];
|
|
3180
|
+
const provided = typeof val === 'string'
|
|
3181
|
+
? val.length > 0
|
|
3182
|
+
: Array.isArray(val)
|
|
3183
|
+
? val.length > 0
|
|
3184
|
+
: false;
|
|
3185
|
+
if (!provided || hasSub) {
|
|
3186
|
+
dbg('alias:maybe:skip', { provided, hasSub });
|
|
3187
|
+
return; // not an alias-only invocation
|
|
3188
|
+
}
|
|
3189
|
+
if (state.handled) {
|
|
3190
|
+
dbg('alias:maybe:already-handled');
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
state.handled = true;
|
|
3194
|
+
dbg('alias-only invocation detected');
|
|
3195
|
+
// Merge CLI options and resolve dotenv context.
|
|
3196
|
+
const { merged } = resolveCliOptions(o, baseGetDotenvCliOptions, process.env.getDotenvCliOptions);
|
|
3197
|
+
const mergedBag = merged;
|
|
3198
|
+
const logger = (mergedBag.logger ?? console);
|
|
3199
|
+
const serviceOptions = getDotenvCliOptions2Options(mergedBag);
|
|
3200
|
+
await cli.resolveAndLoad(serviceOptions);
|
|
3201
|
+
// Normalize alias value
|
|
3202
|
+
const joined = typeof val === 'string'
|
|
3203
|
+
? val
|
|
3204
|
+
: Array.isArray(val)
|
|
3205
|
+
? val.map(String).join(' ')
|
|
3206
|
+
: '';
|
|
3207
|
+
const expanded = dotenvExpandFromProcessEnv(joined);
|
|
3208
|
+
const input = mergedBag.expand === false
|
|
3209
|
+
? joined
|
|
3210
|
+
: expanded !== undefined
|
|
3211
|
+
? expanded
|
|
3212
|
+
: joined;
|
|
3213
|
+
// Scripts: prefer well-formed records; tolerate absent/bad shapes
|
|
3214
|
+
const maybeScripts = mergedBag.scripts;
|
|
3215
|
+
const scripts = maybeScripts && typeof maybeScripts === 'object'
|
|
3216
|
+
? maybeScripts
|
|
3217
|
+
: undefined;
|
|
3218
|
+
const resolved = resolveCommand(scripts, input);
|
|
3219
|
+
if (mergedBag.debug) {
|
|
3220
|
+
logger.log('\n*** command ***\n', `'${resolved}'`);
|
|
3221
|
+
}
|
|
3222
|
+
// Round-trip CLI options for nested getdotenv invocations. Omit logger
|
|
3223
|
+
// (functions/circulars) and guard JSON serialization to avoid hard failures.
|
|
3224
|
+
const { logger: _omitLogger, ...envBag } = mergedBag;
|
|
3225
|
+
let nestedBag;
|
|
3226
|
+
try {
|
|
3227
|
+
nestedBag = JSON.stringify(envBag);
|
|
3228
|
+
}
|
|
3229
|
+
catch {
|
|
3230
|
+
nestedBag = undefined;
|
|
3231
|
+
}
|
|
3232
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
3233
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
3234
|
+
const forceExit = process.env.GETDOTENV_FORCE_EXIT === '1';
|
|
3235
|
+
const capture = !underTests &&
|
|
3236
|
+
(process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
3237
|
+
Boolean(mergedBag.capture));
|
|
3238
|
+
dbg('run:start', {
|
|
3239
|
+
capture,
|
|
3240
|
+
shell: mergedBag.shell,
|
|
3241
|
+
});
|
|
3242
|
+
const ctx = cli.getCtx();
|
|
3243
|
+
const dotenv = (ctx?.dotenv ?? {});
|
|
3244
|
+
// Diagnostics: --trace [keys...]
|
|
3245
|
+
const traceOpt = mergedBag.trace;
|
|
3246
|
+
if (traceOpt) {
|
|
3247
|
+
const parentKeys = Object.keys(process.env);
|
|
3248
|
+
const dotenvKeys = Object.keys(dotenv);
|
|
3249
|
+
const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
|
|
3250
|
+
const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
|
|
3251
|
+
const childEnvPreview = {
|
|
3252
|
+
...process.env,
|
|
3253
|
+
...dotenv,
|
|
3254
|
+
};
|
|
3255
|
+
for (const k of keys) {
|
|
3256
|
+
const parent = process.env[k];
|
|
3257
|
+
const dot = dotenv[k];
|
|
3258
|
+
const final = childEnvPreview[k];
|
|
3259
|
+
const origin = dot !== undefined
|
|
3260
|
+
? 'dotenv'
|
|
3261
|
+
: parent !== undefined
|
|
3262
|
+
? 'parent'
|
|
3263
|
+
: 'unset';
|
|
3264
|
+
const redFlag = mergedBag.redact;
|
|
3265
|
+
const redPatterns = mergedBag.redactPatterns;
|
|
3266
|
+
const redOpts = {};
|
|
3267
|
+
if (redFlag)
|
|
3268
|
+
redOpts.redact = true;
|
|
3269
|
+
if (redFlag && Array.isArray(redPatterns))
|
|
3270
|
+
redOpts.redactPatterns = redPatterns;
|
|
3271
|
+
const tripleBag = {};
|
|
3272
|
+
if (parent !== undefined)
|
|
3273
|
+
tripleBag.parent = parent;
|
|
3274
|
+
if (dot !== undefined)
|
|
3275
|
+
tripleBag.dotenv = dot;
|
|
3276
|
+
if (final !== undefined)
|
|
3277
|
+
tripleBag.final = final;
|
|
3278
|
+
const triple = redactTriple(k, tripleBag, redOpts);
|
|
3279
|
+
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
3280
|
+
const entOpts = {};
|
|
3281
|
+
const warnEntropy = mergedBag.warnEntropy;
|
|
3282
|
+
const entropyThreshold = mergedBag
|
|
3283
|
+
.entropyThreshold;
|
|
3284
|
+
const entropyMinLength = mergedBag
|
|
3285
|
+
.entropyMinLength;
|
|
3286
|
+
const entropyWhitelist = mergedBag.entropyWhitelist;
|
|
3287
|
+
if (typeof warnEntropy === 'boolean')
|
|
3288
|
+
entOpts.warnEntropy = warnEntropy;
|
|
3289
|
+
if (typeof entropyThreshold === 'number')
|
|
3290
|
+
entOpts.entropyThreshold = entropyThreshold;
|
|
3291
|
+
if (typeof entropyMinLength === 'number')
|
|
3292
|
+
entOpts.entropyMinLength = entropyMinLength;
|
|
3293
|
+
if (Array.isArray(entropyWhitelist))
|
|
3294
|
+
entOpts.entropyWhitelist = entropyWhitelist;
|
|
3295
|
+
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
const shellSetting = resolveShell(scripts, input, mergedBag.shell);
|
|
3299
|
+
// Preserve argv array for Node -e snippets under shell-off
|
|
3300
|
+
let commandArg = resolved;
|
|
3301
|
+
if (shellSetting === false && resolved === input) {
|
|
3302
|
+
// Important: preserve doubled quotes within the Node -e payload so
|
|
3303
|
+
// empty string literals ("") survive; Windows-style doubling must not
|
|
3304
|
+
// collapse "" -> " in this path.
|
|
3305
|
+
const parts = tokenize(input, { preserveDoubledQuotes: true });
|
|
3306
|
+
if (parts.length >= 3 &&
|
|
3307
|
+
parts[0]?.toLowerCase() === 'node' &&
|
|
3308
|
+
(parts[1] === '-e' || parts[1] === '--eval')) {
|
|
3309
|
+
// Peel exactly one symmetric outer quote on the code arg
|
|
3310
|
+
parts[2] = stripOne(parts[2] ?? '');
|
|
3311
|
+
// Historical behavior: pass the argv array through unchanged for shell-off.
|
|
3312
|
+
commandArg = parts;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
let exitCode = Number.NaN;
|
|
3316
|
+
try {
|
|
3317
|
+
exitCode = await runCommand(commandArg, shellSetting, {
|
|
3318
|
+
env: buildSpawnEnv(process.env, nestedBag
|
|
3319
|
+
? {
|
|
3320
|
+
...dotenv,
|
|
3321
|
+
getDotenvCliOptions: nestedBag,
|
|
3322
|
+
}
|
|
3323
|
+
: {
|
|
3324
|
+
...dotenv,
|
|
3325
|
+
}),
|
|
3326
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
3327
|
+
});
|
|
3328
|
+
dbg('run:done', { exitCode });
|
|
3329
|
+
}
|
|
3330
|
+
catch (err) {
|
|
3331
|
+
const code = typeof err.exitCode === 'number'
|
|
3332
|
+
? err.exitCode
|
|
3333
|
+
: 1;
|
|
3334
|
+
dbg('run:error', { exitCode: code, error: String(err) });
|
|
3335
|
+
if (!underTests) {
|
|
3336
|
+
dbg('process.exit (error path)', { exitCode: code });
|
|
3337
|
+
process.exit(code);
|
|
3338
|
+
}
|
|
3339
|
+
else {
|
|
3340
|
+
dbg('process.exit suppressed for tests (error path)', {
|
|
3341
|
+
exitCode: code,
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
if (!Number.isNaN(exitCode)) {
|
|
3347
|
+
dbg('process.exit', { exitCode });
|
|
3348
|
+
process.exit(exitCode);
|
|
3349
|
+
}
|
|
3350
|
+
if (!underTests) {
|
|
3351
|
+
dbg('process.exit (fallback: non-numeric exitCode)', { exitCode: 0 });
|
|
3352
|
+
process.exit(0);
|
|
3353
|
+
}
|
|
3354
|
+
else {
|
|
3355
|
+
dbg('process.exit (fallback suppressed for tests: non-numeric exitCode)', {
|
|
3356
|
+
exitCode: 0,
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
if (forceExit) {
|
|
3360
|
+
setImmediate(() => process.exit(Number.isNaN(exitCode) ? 0 : exitCode));
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
|
|
2893
3364
|
const attachParentAlias = (cli, options, _cmd) => {
|
|
2894
3365
|
const aliasSpec = typeof options.optionAlias === 'string'
|
|
2895
|
-
? { flags: options.optionAlias, description: undefined
|
|
3366
|
+
? { flags: options.optionAlias, description: undefined}
|
|
2896
3367
|
: options.optionAlias;
|
|
2897
3368
|
if (!aliasSpec)
|
|
2898
3369
|
return;
|
|
2899
3370
|
const deriveKey = (flags) => {
|
|
2900
|
-
|
|
3371
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3372
|
+
console.error('[getdotenv:alias] install alias option', flags);
|
|
3373
|
+
}
|
|
2901
3374
|
const long = flags.split(/[ ,|]+/).find((f) => f.startsWith('--')) ?? '--cmd';
|
|
2902
3375
|
const name = long.replace(/^--/, '');
|
|
2903
3376
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
@@ -2907,256 +3380,24 @@ const attachParentAlias = (cli, options, _cmd) => {
|
|
|
2907
3380
|
const desc = aliasSpec.description ??
|
|
2908
3381
|
'alias of cmd subcommand; provide command tokens (variadic)';
|
|
2909
3382
|
cli.option(aliasSpec.flags, desc);
|
|
2910
|
-
// Tag the just-added parent option for grouped help rendering.
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
last
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
catch {
|
|
2919
|
-
/* noop */
|
|
3383
|
+
// Tag the just-added parent option for grouped help rendering at the root.
|
|
3384
|
+
const optsArr = cli.options;
|
|
3385
|
+
if (optsArr.length > 0) {
|
|
3386
|
+
const last = optsArr[optsArr.length - 1];
|
|
3387
|
+
if (last)
|
|
3388
|
+
cli.setOptionGroup(last, 'plugin:cmd');
|
|
2920
3389
|
}
|
|
2921
3390
|
// Shared alias executor for either preAction or preSubcommand hooks.
|
|
2922
3391
|
// Ensure we only execute once even if both hooks fire in a single parse.
|
|
2923
|
-
|
|
2924
|
-
const
|
|
2925
|
-
|
|
2926
|
-
const raw = thisCommand.rawArgs ?? [];
|
|
2927
|
-
const childNames = thisCommand.commands.flatMap((c) => [
|
|
2928
|
-
c.name(),
|
|
2929
|
-
...c.aliases(),
|
|
2930
|
-
]);
|
|
2931
|
-
const hasSub = childNames.some((n) => raw.includes(n));
|
|
2932
|
-
// Read alias value from parent opts.
|
|
2933
|
-
const o = thisCommand.opts();
|
|
2934
|
-
const val = o[aliasKey];
|
|
2935
|
-
const provided = typeof val === 'string'
|
|
2936
|
-
? val.length > 0
|
|
2937
|
-
: Array.isArray(val)
|
|
2938
|
-
? val.length > 0
|
|
2939
|
-
: false;
|
|
2940
|
-
if (!provided || hasSub) {
|
|
2941
|
-
dbg('alias:maybe:skip', { provided, hasSub });
|
|
2942
|
-
return; // not an alias-only invocation
|
|
2943
|
-
}
|
|
2944
|
-
if (aliasHandled) {
|
|
2945
|
-
dbg('alias:maybe:already-handled');
|
|
2946
|
-
return;
|
|
2947
|
-
}
|
|
2948
|
-
aliasHandled = true;
|
|
2949
|
-
dbg('alias-only invocation detected');
|
|
2950
|
-
// Merge CLI options and resolve dotenv context.
|
|
2951
|
-
const { merged } = resolveCliOptions(o,
|
|
2952
|
-
// cast through unknown to avoid readonly -> mutable incompatibilities
|
|
2953
|
-
baseRootOptionDefaults, process.env.getDotenvCliOptions);
|
|
2954
|
-
const logger = merged.logger ?? console;
|
|
2955
|
-
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
2956
|
-
await cli.resolveAndLoad(serviceOptions);
|
|
2957
|
-
// Normalize alias value.
|
|
2958
|
-
const joined = typeof val === 'string'
|
|
2959
|
-
? val
|
|
2960
|
-
: Array.isArray(val)
|
|
2961
|
-
? val.map(String).join(' ')
|
|
2962
|
-
: '';
|
|
2963
|
-
const input = aliasSpec.expand === false
|
|
2964
|
-
? joined
|
|
2965
|
-
: (dotenvExpandFromProcessEnv(joined) ?? joined);
|
|
2966
|
-
dbg('resolved input', { input });
|
|
2967
|
-
const resolved = resolveCommand(merged.scripts, input);
|
|
2968
|
-
const lg = logger;
|
|
2969
|
-
if (merged.debug) {
|
|
2970
|
-
(lg.debug ?? lg.log)('\n*** command ***\n', `'${resolved}'`);
|
|
2971
|
-
}
|
|
2972
|
-
const { logger: _omit, ...envBag } = merged;
|
|
2973
|
-
// Test guard: when running under tests, prefer stdio: 'inherit' to avoid
|
|
2974
|
-
// assertions depending on captured stdio; ignore GETDOTENV_STDIO/capture.
|
|
2975
|
-
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
2976
|
-
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
2977
|
-
const forceExit = process.env.GETDOTENV_FORCE_EXIT === '1';
|
|
2978
|
-
const capture = !underTests &&
|
|
2979
|
-
(process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
2980
|
-
Boolean(merged.capture));
|
|
2981
|
-
dbg('run:start', { capture, shell: merged.shell });
|
|
2982
|
-
// Prefer explicit env injection: include resolved dotenv map to avoid leaking
|
|
2983
|
-
// parent process.env secrets when exclusions are set.
|
|
2984
|
-
const ctx = cli.getCtx();
|
|
2985
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
2986
|
-
// Diagnostics: --trace [keys...]
|
|
2987
|
-
const traceOpt = merged.trace;
|
|
2988
|
-
if (traceOpt) {
|
|
2989
|
-
const parentKeys = Object.keys(process.env);
|
|
2990
|
-
const dotenvKeys = Object.keys(dotenv);
|
|
2991
|
-
const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
|
|
2992
|
-
const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
|
|
2993
|
-
const childEnvPreview = {
|
|
2994
|
-
...process.env,
|
|
2995
|
-
...dotenv,
|
|
2996
|
-
};
|
|
2997
|
-
for (const k of keys) {
|
|
2998
|
-
const parent = process.env[k];
|
|
2999
|
-
const dot = dotenv[k];
|
|
3000
|
-
const final = childEnvPreview[k];
|
|
3001
|
-
const origin = dot !== undefined
|
|
3002
|
-
? 'dotenv'
|
|
3003
|
-
: parent !== undefined
|
|
3004
|
-
? 'parent'
|
|
3005
|
-
: 'unset';
|
|
3006
|
-
// Build redact options and triple bag without undefined-valued fields
|
|
3007
|
-
const redOpts = {};
|
|
3008
|
-
const redFlag = merged.redact;
|
|
3009
|
-
const redPatterns = merged
|
|
3010
|
-
.redactPatterns;
|
|
3011
|
-
if (redFlag)
|
|
3012
|
-
redOpts.redact = true;
|
|
3013
|
-
if (redFlag && Array.isArray(redPatterns))
|
|
3014
|
-
redOpts.redactPatterns = redPatterns;
|
|
3015
|
-
const tripleBag = {};
|
|
3016
|
-
if (parent !== undefined)
|
|
3017
|
-
tripleBag.parent = parent;
|
|
3018
|
-
if (dot !== undefined)
|
|
3019
|
-
tripleBag.dotenv = dot;
|
|
3020
|
-
if (final !== undefined)
|
|
3021
|
-
tripleBag.final = final;
|
|
3022
|
-
const triple = redactTriple(k, tripleBag, redOpts);
|
|
3023
|
-
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
3024
|
-
const entOpts = {};
|
|
3025
|
-
const warnEntropy = merged.warnEntropy;
|
|
3026
|
-
const entropyThreshold = merged
|
|
3027
|
-
.entropyThreshold;
|
|
3028
|
-
const entropyMinLength = merged
|
|
3029
|
-
.entropyMinLength;
|
|
3030
|
-
const entropyWhitelist = merged
|
|
3031
|
-
.entropyWhitelist;
|
|
3032
|
-
if (typeof warnEntropy === 'boolean')
|
|
3033
|
-
entOpts.warnEntropy = warnEntropy;
|
|
3034
|
-
if (typeof entropyThreshold === 'number')
|
|
3035
|
-
entOpts.entropyThreshold = entropyThreshold;
|
|
3036
|
-
if (typeof entropyMinLength === 'number')
|
|
3037
|
-
entOpts.entropyMinLength = entropyMinLength;
|
|
3038
|
-
if (Array.isArray(entropyWhitelist))
|
|
3039
|
-
entOpts.entropyWhitelist = entropyWhitelist;
|
|
3040
|
-
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
let exitCode = Number.NaN;
|
|
3044
|
-
try {
|
|
3045
|
-
// Resolve shell and preserve argv for Node -e snippets under shell-off.
|
|
3046
|
-
const shellSetting = resolveShell(merged.scripts, input, merged.shell);
|
|
3047
|
-
let commandArg = resolved;
|
|
3048
|
-
/** * Special-case: when shell is OFF and no script alias remap occurred
|
|
3049
|
-
* (resolved === input), treat a Node eval payload as an argv array to
|
|
3050
|
-
* avoid lossy re-tokenization of the code string.
|
|
3051
|
-
*
|
|
3052
|
-
* Examples handled:
|
|
3053
|
-
* "node -e \"console.log(JSON.stringify(...))\""
|
|
3054
|
-
* "node --eval 'console.log(...)'"
|
|
3055
|
-
*
|
|
3056
|
-
* We peel exactly one pair of symmetric outer quotes from the code
|
|
3057
|
-
* argument when present; inner quotes remain untouched.
|
|
3058
|
-
*/
|
|
3059
|
-
if (shellSetting === false && resolved === input) {
|
|
3060
|
-
// Helper: strip one symmetric outer quote layer
|
|
3061
|
-
const stripOne = (s) => {
|
|
3062
|
-
if (s.length < 2)
|
|
3063
|
-
return s;
|
|
3064
|
-
const a = s.charAt(0);
|
|
3065
|
-
const b = s.charAt(s.length - 1);
|
|
3066
|
-
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
3067
|
-
return symmetric ? s.slice(1, -1) : s;
|
|
3068
|
-
};
|
|
3069
|
-
// Normalize whole input once for robust matching
|
|
3070
|
-
const normalized = stripOne(input.trim());
|
|
3071
|
-
// First try a lightweight regex on the normalized string
|
|
3072
|
-
const m = /^\s*node\s+(--eval|-e)\s+([\s\S]+)$/i.exec(normalized);
|
|
3073
|
-
if (m && typeof m[1] === 'string' && typeof m[2] === 'string') {
|
|
3074
|
-
const evalFlag = m[1];
|
|
3075
|
-
let codeArg = m[2].trim();
|
|
3076
|
-
codeArg = stripOne(codeArg);
|
|
3077
|
-
const flag = evalFlag.startsWith('--') ? '--eval' : '-e';
|
|
3078
|
-
commandArg = ['node', flag, codeArg];
|
|
3079
|
-
}
|
|
3080
|
-
else {
|
|
3081
|
-
// Fallback: tokenize and detect node -e/--eval form
|
|
3082
|
-
const parts = tokenize(input);
|
|
3083
|
-
if (parts.length >= 3) {
|
|
3084
|
-
// Narrow under noUncheckedIndexedAccess
|
|
3085
|
-
const p0 = parts[0];
|
|
3086
|
-
const p1 = parts[1];
|
|
3087
|
-
if (p0?.toLowerCase() === 'node' &&
|
|
3088
|
-
(p1 === '-e' || p1 === '--eval')) {
|
|
3089
|
-
commandArg = parts;
|
|
3090
|
-
}
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
exitCode = await runCommand(commandArg, shellSetting, {
|
|
3095
|
-
env: buildSpawnEnv(process.env, {
|
|
3096
|
-
...dotenv,
|
|
3097
|
-
getDotenvCliOptions: JSON.stringify(envBag),
|
|
3098
|
-
}),
|
|
3099
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
3100
|
-
});
|
|
3101
|
-
dbg('run:done', { exitCode });
|
|
3102
|
-
}
|
|
3103
|
-
catch (err) {
|
|
3104
|
-
const code = typeof err.exitCode === 'number'
|
|
3105
|
-
? err.exitCode
|
|
3106
|
-
: 1;
|
|
3107
|
-
dbg('run:error', { exitCode: code, error: String(err) });
|
|
3108
|
-
if (!underTests) {
|
|
3109
|
-
dbg('process.exit (error path)', { exitCode: code });
|
|
3110
|
-
process.exit(code);
|
|
3111
|
-
}
|
|
3112
|
-
else {
|
|
3113
|
-
dbg('process.exit suppressed for tests (error path)', {
|
|
3114
|
-
exitCode: code,
|
|
3115
|
-
});
|
|
3116
|
-
}
|
|
3117
|
-
return;
|
|
3118
|
-
}
|
|
3119
|
-
if (!Number.isNaN(exitCode)) {
|
|
3120
|
-
dbg('process.exit', { exitCode });
|
|
3121
|
-
process.exit(exitCode);
|
|
3122
|
-
}
|
|
3123
|
-
// Fallback: Some environments may not surface a numeric exitCode even on success.
|
|
3124
|
-
// Always terminate alias-only invocations outside tests to avoid hanging the process,
|
|
3125
|
-
// regardless of capture/GETDOTENV_STDIO. Under tests, suppress to keep the runner alive.
|
|
3126
|
-
if (!underTests) {
|
|
3127
|
-
dbg('process.exit (fallback: non-numeric exitCode)', { exitCode: 0 });
|
|
3128
|
-
process.exit(0);
|
|
3129
|
-
}
|
|
3130
|
-
else {
|
|
3131
|
-
dbg('process.exit (fallback suppressed for tests: non-numeric exitCode)', { exitCode: 0 });
|
|
3132
|
-
}
|
|
3133
|
-
// Optional last-resort guard: force an exit on the next tick when enabled.
|
|
3134
|
-
// Intended for diagnosing environments where the process appears to linger
|
|
3135
|
-
// despite reaching the success/error handlers above. Disabled under tests.
|
|
3136
|
-
if (forceExit) {
|
|
3137
|
-
try {
|
|
3138
|
-
if (process.env.GETDOTENV_DEBUG_VERBOSE) {
|
|
3139
|
-
const getHandles = process._getActiveHandles;
|
|
3140
|
-
const handles = typeof getHandles === 'function' ? getHandles() : [];
|
|
3141
|
-
dbg('active handles before forced exit', {
|
|
3142
|
-
count: Array.isArray(handles) ? handles.length : undefined,
|
|
3143
|
-
});
|
|
3144
|
-
}
|
|
3145
|
-
}
|
|
3146
|
-
catch {
|
|
3147
|
-
// best-effort only
|
|
3148
|
-
}
|
|
3149
|
-
const code = Number.isNaN(exitCode) ? 0 : exitCode;
|
|
3150
|
-
dbg('process.exit (forced)', { exitCode: code });
|
|
3151
|
-
setImmediate(() => process.exit(code));
|
|
3152
|
-
}
|
|
3392
|
+
const aliasState = { handled: false };
|
|
3393
|
+
const maybeRun = async (thisCommand) => {
|
|
3394
|
+
await maybeRunAlias(cli, thisCommand, aliasKey, aliasState);
|
|
3153
3395
|
};
|
|
3154
|
-
// Execute alias-only invocations whether the root handles the action // itself (preAction) or Commander routes to a default subcommand (preSubcommand).
|
|
3155
3396
|
cli.hook('preAction', async (thisCommand, _actionCommand) => {
|
|
3156
|
-
await
|
|
3397
|
+
await maybeRun(thisCommand);
|
|
3157
3398
|
});
|
|
3158
3399
|
cli.hook('preSubcommand', async (thisCommand) => {
|
|
3159
|
-
await
|
|
3400
|
+
await maybeRun(thisCommand);
|
|
3160
3401
|
});
|
|
3161
3402
|
};
|
|
3162
3403
|
|
|
@@ -3178,10 +3419,10 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
3178
3419
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
3179
3420
|
};
|
|
3180
3421
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
3181
|
-
|
|
3182
|
-
|
|
3422
|
+
// Create as a GetDotenvCli child so helpInformation includes a trailing blank line.
|
|
3423
|
+
const cmd = cli
|
|
3424
|
+
.createCommand('cmd')
|
|
3183
3425
|
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3184
|
-
.configureHelp({ showGlobalOptions: true })
|
|
3185
3426
|
.enablePositionalOptions()
|
|
3186
3427
|
.passThroughOptions()
|
|
3187
3428
|
.argument('[command...]')
|
|
@@ -3252,8 +3493,7 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
3252
3493
|
: 'unset';
|
|
3253
3494
|
// Apply presentation-time redaction (if enabled)
|
|
3254
3495
|
const redFlag = merged.redact;
|
|
3255
|
-
const redPatterns = merged
|
|
3256
|
-
.redactPatterns;
|
|
3496
|
+
const redPatterns = merged.redactPatterns;
|
|
3257
3497
|
const redOpts = {};
|
|
3258
3498
|
if (redFlag)
|
|
3259
3499
|
redOpts.redact = true;
|
|
@@ -3370,10 +3610,9 @@ const demoPlugin = () => definePlugin({
|
|
|
3370
3610
|
// Build a minimal node -e payload via argv array (avoid quoting issues).
|
|
3371
3611
|
const code = `console.log(process.env.${key} ?? "")`;
|
|
3372
3612
|
const ctx = cli.getCtx();
|
|
3373
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
3374
3613
|
// Inherit stdio for an interactive demo. Use --capture for CI.
|
|
3375
3614
|
await runCommand(['node', '-e', code], false, {
|
|
3376
|
-
env:
|
|
3615
|
+
env: buildSpawnEnv(process.env, ctx?.dotenv),
|
|
3377
3616
|
stdio: 'inherit',
|
|
3378
3617
|
});
|
|
3379
3618
|
});
|
|
@@ -3408,22 +3647,24 @@ const demoPlugin = () => definePlugin({
|
|
|
3408
3647
|
const shell = resolveShell(bag?.scripts, input, bag?.shell);
|
|
3409
3648
|
// Compose child env (parent + ctx.dotenv). This mirrors cmd/batch behavior.
|
|
3410
3649
|
const ctx = cli.getCtx();
|
|
3411
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
3412
3650
|
await runCommand(resolved, shell, {
|
|
3413
|
-
env:
|
|
3651
|
+
env: buildSpawnEnv(process.env, ctx?.dotenv),
|
|
3414
3652
|
stdio: 'inherit',
|
|
3415
3653
|
});
|
|
3416
3654
|
});
|
|
3417
3655
|
},
|
|
3418
3656
|
/**
|
|
3419
3657
|
* Optional: afterResolve can initialize per-plugin state using ctx.dotenv.
|
|
3420
|
-
* For the demo we
|
|
3658
|
+
* For the demo we emit a single breadcrumb only when GETDOTENV_DEBUG is set,
|
|
3659
|
+
* keeping default runs (tests/CI/smoke) quiet.
|
|
3421
3660
|
*/
|
|
3422
3661
|
afterResolve(_cli, ctx) {
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3662
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3663
|
+
const keys = Object.keys(ctx.dotenv);
|
|
3664
|
+
if (keys.length > 0) {
|
|
3665
|
+
// Keep noise low; a single-line breadcrumb is sufficient for the demo.
|
|
3666
|
+
console.error('[demo] afterResolve: dotenv keys loaded:', keys.length);
|
|
3667
|
+
}
|
|
3427
3668
|
}
|
|
3428
3669
|
},
|
|
3429
3670
|
});
|
|
@@ -3681,126 +3922,39 @@ const initPlugin = (opts = {}) => definePlugin({
|
|
|
3681
3922
|
},
|
|
3682
3923
|
});
|
|
3683
3924
|
|
|
3684
|
-
const cmdCommand = new Command()
|
|
3685
|
-
.name('cmd')
|
|
3686
|
-
.description('execute command, conflicts with --command option (default subcommand)')
|
|
3687
|
-
.enablePositionalOptions()
|
|
3688
|
-
.passThroughOptions()
|
|
3689
|
-
.argument('[command...]')
|
|
3690
|
-
.action(async (commandParts, _options, thisCommand) => {
|
|
3691
|
-
if (!thisCommand.parent)
|
|
3692
|
-
throw new Error(`unable to resolve parent command`);
|
|
3693
|
-
if (!thisCommand.parent.parent)
|
|
3694
|
-
throw new Error(`unable to resolve root command`);
|
|
3695
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent.parent;
|
|
3696
|
-
const raw = thisCommand.parent.opts();
|
|
3697
|
-
const ignoreErrors = !!raw.ignoreErrors;
|
|
3698
|
-
const globs = typeof raw.globs === 'string' ? raw.globs : '*';
|
|
3699
|
-
const list = !!raw.list;
|
|
3700
|
-
const pkgCwd = !!raw.pkgCwd;
|
|
3701
|
-
const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : './';
|
|
3702
|
-
// Execute command.
|
|
3703
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
3704
|
-
// When no positional tokens are provided (e.g., option form `-c/--command`),
|
|
3705
|
-
// the preSubcommand hook handles execution. Avoid a duplicate call here.
|
|
3706
|
-
if (args.length === 0)
|
|
3707
|
-
return;
|
|
3708
|
-
const command = args.map(String).join(' ');
|
|
3709
|
-
await execShellCommandBatch({
|
|
3710
|
-
command: resolveCommand(getDotenvCliOptions.scripts, command),
|
|
3711
|
-
getDotenvCliOptions,
|
|
3712
|
-
globs,
|
|
3713
|
-
ignoreErrors,
|
|
3714
|
-
list,
|
|
3715
|
-
logger,
|
|
3716
|
-
pkgCwd,
|
|
3717
|
-
rootPath,
|
|
3718
|
-
// execa expects string | boolean | URL for `shell`. We normalize earlier;
|
|
3719
|
-
// scripts[name].shell overrides take precedence and may be boolean or string.
|
|
3720
|
-
shell: resolveShell(getDotenvCliOptions.scripts, command, getDotenvCliOptions.shell),
|
|
3721
|
-
});
|
|
3722
|
-
});
|
|
3723
|
-
|
|
3724
|
-
new Command()
|
|
3725
|
-
.name('batch')
|
|
3726
|
-
.description('Batch command execution across multiple working directories.')
|
|
3727
|
-
.enablePositionalOptions()
|
|
3728
|
-
.passThroughOptions()
|
|
3729
|
-
.option('-p, --pkg-cwd', 'use nearest package directory as current working directory')
|
|
3730
|
-
.option('-r, --root-path <string>', 'path to batch root directory from current working directory', './')
|
|
3731
|
-
.option('-g, --globs <string>', 'space-delimited globs from root path', '*')
|
|
3732
|
-
.option('-c, --command <string>', 'command executed according to the base --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv)
|
|
3733
|
-
.option('-l, --list', 'list working directories without executing command')
|
|
3734
|
-
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
3735
|
-
.hook('preSubcommand', async (thisCommand) => {
|
|
3736
|
-
if (!thisCommand.parent)
|
|
3737
|
-
throw new Error(`unable to resolve root command`);
|
|
3738
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent;
|
|
3739
|
-
const raw = thisCommand.opts();
|
|
3740
|
-
const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
|
|
3741
|
-
const ignoreErrors = !!raw.ignoreErrors;
|
|
3742
|
-
const globs = typeof raw.globs === 'string' ? raw.globs : '*';
|
|
3743
|
-
const list = !!raw.list;
|
|
3744
|
-
const pkgCwd = !!raw.pkgCwd;
|
|
3745
|
-
const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : './';
|
|
3746
|
-
const argCount = thisCommand.args.length;
|
|
3747
|
-
if (typeof commandOpt === 'string' && argCount > 0) {
|
|
3748
|
-
logger.error(`--command option conflicts with cmd subcommand.`);
|
|
3749
|
-
process.exit(0);
|
|
3750
|
-
}
|
|
3751
|
-
// Execute command.
|
|
3752
|
-
if (typeof commandOpt === 'string')
|
|
3753
|
-
await execShellCommandBatch({
|
|
3754
|
-
command: resolveCommand(getDotenvCliOptions.scripts, commandOpt),
|
|
3755
|
-
getDotenvCliOptions,
|
|
3756
|
-
globs,
|
|
3757
|
-
ignoreErrors,
|
|
3758
|
-
list,
|
|
3759
|
-
logger,
|
|
3760
|
-
pkgCwd,
|
|
3761
|
-
rootPath,
|
|
3762
|
-
// execa expects string | boolean | URL for `shell`. We normalize earlier;
|
|
3763
|
-
// scripts[name].shell overrides take precedence and may be boolean or string.
|
|
3764
|
-
shell: resolveShell(getDotenvCliOptions.scripts, commandOpt, getDotenvCliOptions.shell),
|
|
3765
|
-
});
|
|
3766
|
-
})
|
|
3767
|
-
.addCommand(cmdCommand, { isDefault: true });
|
|
3768
|
-
|
|
3769
|
-
new Command()
|
|
3770
|
-
.name('cmd')
|
|
3771
|
-
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3772
|
-
.configureHelp({ showGlobalOptions: true })
|
|
3773
|
-
.enablePositionalOptions()
|
|
3774
|
-
.passThroughOptions()
|
|
3775
|
-
.argument('[command...]')
|
|
3776
|
-
.action(async (commandParts, _options, thisCommand) => {
|
|
3777
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
3778
|
-
if (args.length === 0)
|
|
3779
|
-
return;
|
|
3780
|
-
if (!thisCommand.parent)
|
|
3781
|
-
throw new Error('parent command not found');
|
|
3782
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent;
|
|
3783
|
-
const command = args.map(String).join(' ');
|
|
3784
|
-
const cmd = resolveCommand(getDotenvCliOptions.scripts, command);
|
|
3785
|
-
if (getDotenvCliOptions.debug)
|
|
3786
|
-
logger.log('\n*** command ***\n', `'${cmd}'`);
|
|
3787
|
-
await execaCommand(cmd, {
|
|
3788
|
-
env: {
|
|
3789
|
-
...process.env,
|
|
3790
|
-
getDotenvCliOptions: JSON.stringify(getDotenvCliOptions),
|
|
3791
|
-
},
|
|
3792
|
-
// execa expects string | boolean | URL; we normalize in generator
|
|
3793
|
-
// and allow script-level overrides.
|
|
3794
|
-
shell: resolveShell(getDotenvCliOptions.scripts, command, getDotenvCliOptions.shell),
|
|
3795
|
-
stdio: 'inherit',
|
|
3796
|
-
});
|
|
3797
|
-
});
|
|
3798
|
-
|
|
3799
3925
|
function createCli(opts = {}) {
|
|
3800
3926
|
const alias = typeof opts.alias === 'string' && opts.alias.length > 0
|
|
3801
3927
|
? opts.alias
|
|
3802
3928
|
: 'getdotenv';
|
|
3803
3929
|
const program = new GetDotenvCli(alias);
|
|
3930
|
+
// Normalize Commander output so help prints always end with a blank line.
|
|
3931
|
+
// This keeps E2E assertions (CRLF and >=2 trailing newlines) portable across
|
|
3932
|
+
// runtimes and capture modes without altering Commander internals.
|
|
3933
|
+
const outputCfg = {
|
|
3934
|
+
writeOut(str) {
|
|
3935
|
+
const txt = typeof str === 'string' ? str : '';
|
|
3936
|
+
const hasTwo = /(?:\r?\n){2,}$/.test(txt);
|
|
3937
|
+
const hasOne = /\r?\n$/.test(txt);
|
|
3938
|
+
const out = hasTwo ? txt : hasOne ? txt + '\n' : txt + '\n\n';
|
|
3939
|
+
try {
|
|
3940
|
+
process.stdout.write(out);
|
|
3941
|
+
}
|
|
3942
|
+
catch {
|
|
3943
|
+
/* ignore */
|
|
3944
|
+
}
|
|
3945
|
+
},
|
|
3946
|
+
writeErr(str) {
|
|
3947
|
+
process.stderr.write(str);
|
|
3948
|
+
},
|
|
3949
|
+
};
|
|
3950
|
+
// Apply to root and recursively to subcommands so all help paths are normalized.
|
|
3951
|
+
program.configureOutput(outputCfg);
|
|
3952
|
+
const applyOutputRecursively = (cmd) => {
|
|
3953
|
+
cmd.configureOutput(outputCfg);
|
|
3954
|
+
for (const child of cmd.commands)
|
|
3955
|
+
applyOutputRecursively(child);
|
|
3956
|
+
};
|
|
3957
|
+
applyOutputRecursively(program);
|
|
3804
3958
|
// Install base root flags and included plugins; resolve context once per run.
|
|
3805
3959
|
program
|
|
3806
3960
|
.attachRootOptions({ loadProcess: false })
|
|
@@ -3816,19 +3970,68 @@ function createCli(opts = {}) {
|
|
|
3816
3970
|
if (underTests) {
|
|
3817
3971
|
program.exitOverride((err) => {
|
|
3818
3972
|
const code = err?.code;
|
|
3819
|
-
|
|
3973
|
+
// Commander printed help already; ensure a trailing blank line for tests/CI capture.
|
|
3974
|
+
if (code === 'commander.helpDisplayed') {
|
|
3975
|
+
try {
|
|
3976
|
+
process.stdout.write('\n');
|
|
3977
|
+
}
|
|
3978
|
+
catch {
|
|
3979
|
+
/* ignore */
|
|
3980
|
+
}
|
|
3820
3981
|
return;
|
|
3982
|
+
}
|
|
3983
|
+
if (code === 'commander.version') {
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3821
3986
|
throw err;
|
|
3822
3987
|
});
|
|
3823
3988
|
}
|
|
3824
3989
|
return {
|
|
3825
3990
|
async run(argv) {
|
|
3826
|
-
//
|
|
3827
|
-
//
|
|
3828
|
-
//
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3991
|
+
// Help handling:
|
|
3992
|
+
// - Short-circuit ONLY for true top-level -h/--help (no subcommand before flag).
|
|
3993
|
+
// - If a subcommand token appears before -h/--help, defer to Commander
|
|
3994
|
+
// to render that subcommand's help.
|
|
3995
|
+
const helpIdx = argv.findIndex((a) => a === '-h' || a === '--help');
|
|
3996
|
+
if (helpIdx >= 0) {
|
|
3997
|
+
// Build a set of known subcommand names/aliases on the root.
|
|
3998
|
+
const subs = new Set();
|
|
3999
|
+
for (const c of program.commands) {
|
|
4000
|
+
subs.add(c.name());
|
|
4001
|
+
for (const a of c.aliases())
|
|
4002
|
+
subs.add(a);
|
|
4003
|
+
}
|
|
4004
|
+
const hasSubBeforeHelp = argv
|
|
4005
|
+
.slice(0, helpIdx)
|
|
4006
|
+
.some((tok) => subs.has(tok));
|
|
4007
|
+
if (!hasSubBeforeHelp) {
|
|
4008
|
+
await program.brand({
|
|
4009
|
+
name: alias,
|
|
4010
|
+
importMetaUrl: import.meta.url,
|
|
4011
|
+
description: 'Base CLI.',
|
|
4012
|
+
...(typeof opts.branding === 'string' && opts.branding.length > 0
|
|
4013
|
+
? { helpHeader: opts.branding }
|
|
4014
|
+
: {}),
|
|
4015
|
+
});
|
|
4016
|
+
// Resolve context once without side effects for help rendering.
|
|
4017
|
+
const ctx = await program.resolveAndLoad({
|
|
4018
|
+
loadProcess: false,
|
|
4019
|
+
log: false,
|
|
4020
|
+
}, { runAfterResolve: false });
|
|
4021
|
+
// Build a defaults-only merged CLI bag for help-time parity (no side effects).
|
|
4022
|
+
const { merged: defaultsMerged } = resolveCliOptions({}, baseRootOptionDefaults, undefined);
|
|
4023
|
+
const helpCfg = toHelpConfig(defaultsMerged, ctx.pluginConfigs ?? {});
|
|
4024
|
+
program.evaluateDynamicOptions(helpCfg);
|
|
4025
|
+
// Suppress output only during unit tests; allow E2E to capture.
|
|
4026
|
+
const piping = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
4027
|
+
process.env.GETDOTENV_STDOUT === 'pipe';
|
|
4028
|
+
if (!(underTests && !piping)) {
|
|
4029
|
+
program.outputHelp();
|
|
4030
|
+
}
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
// Subcommand token exists before -h: fall through to normal parsing,
|
|
4034
|
+
// letting Commander print that subcommand's help.
|
|
3832
4035
|
}
|
|
3833
4036
|
await program.brand({
|
|
3834
4037
|
name: alias,
|