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