@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/plugins-cmd.mjs
CHANGED
|
@@ -1,25 +1,43 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
1
|
import { execa, execaCommand } from 'execa';
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import 'fs-extra';
|
|
4
|
-
import 'package-directory';
|
|
5
4
|
import 'path';
|
|
5
|
+
import 'package-directory';
|
|
6
|
+
import 'url';
|
|
7
|
+
import 'yaml';
|
|
8
|
+
import 'nanoid';
|
|
9
|
+
import 'dotenv';
|
|
10
|
+
import 'crypto';
|
|
6
11
|
|
|
7
12
|
// Minimal tokenizer for shell-off execution:
|
|
8
13
|
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
9
|
-
|
|
14
|
+
// Optionally preserve doubled quotes inside quoted segments:
|
|
15
|
+
// - default: "" => " (Windows/PowerShell style literal-quote escape)
|
|
16
|
+
// - preserveDoubledQuotes: true => "" stays "" (needed for Node -e payloads)
|
|
17
|
+
const tokenize = (command, opts) => {
|
|
10
18
|
const out = [];
|
|
11
19
|
let cur = '';
|
|
12
20
|
let quote = null;
|
|
21
|
+
const preserve = opts && opts.preserveDoubledQuotes === true ? true : false;
|
|
13
22
|
for (let i = 0; i < command.length; i++) {
|
|
14
23
|
const c = command.charAt(i);
|
|
15
24
|
if (quote) {
|
|
16
25
|
if (c === quote) {
|
|
17
|
-
// Support doubled quotes inside a quoted segment
|
|
18
|
-
// "" -> " and '' -> '
|
|
26
|
+
// Support doubled quotes inside a quoted segment:
|
|
27
|
+
// default: "" -> " and '' -> ' (Windows/PowerShell style)
|
|
28
|
+
// preserve: keep as "" to allow empty string literals in Node -e payloads
|
|
19
29
|
const next = command.charAt(i + 1);
|
|
20
30
|
if (next === quote) {
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
if (preserve) {
|
|
32
|
+
// Keep "" as-is; append both and continue within the quoted segment.
|
|
33
|
+
cur += quote + quote;
|
|
34
|
+
i += 1; // skip the second quote char (we already appended both)
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Collapse to a single literal quote
|
|
38
|
+
cur += quote;
|
|
39
|
+
i += 1; // skip the second quote
|
|
40
|
+
}
|
|
23
41
|
}
|
|
24
42
|
else {
|
|
25
43
|
// end of quoted segment
|
|
@@ -74,6 +92,17 @@ const stripOuterQuotes = (s) => {
|
|
|
74
92
|
}
|
|
75
93
|
return out;
|
|
76
94
|
};
|
|
95
|
+
// Extract exitCode/stdout/stderr from execa result or error in a tolerant way.
|
|
96
|
+
const pickResult = (r) => {
|
|
97
|
+
const exit = r.exitCode;
|
|
98
|
+
const stdoutVal = r.stdout;
|
|
99
|
+
const stderrVal = r.stderr;
|
|
100
|
+
return {
|
|
101
|
+
exitCode: typeof exit === 'number' ? exit : Number.NaN,
|
|
102
|
+
stdout: typeof stdoutVal === 'string' ? stdoutVal : '',
|
|
103
|
+
stderr: typeof stderrVal === 'string' ? stderrVal : '',
|
|
104
|
+
};
|
|
105
|
+
};
|
|
77
106
|
// Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
|
|
78
107
|
// expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
|
|
79
108
|
const sanitizeEnv = (env) => {
|
|
@@ -82,19 +111,19 @@ const sanitizeEnv = (env) => {
|
|
|
82
111
|
const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
|
|
83
112
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
84
113
|
};
|
|
85
|
-
|
|
114
|
+
async function runCommand(command, shell, opts) {
|
|
86
115
|
if (shell === false) {
|
|
87
116
|
let file;
|
|
88
117
|
let args = [];
|
|
89
|
-
if (
|
|
90
|
-
file = command[0];
|
|
91
|
-
args = command.slice(1).map(stripOuterQuotes);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
118
|
+
if (typeof command === 'string') {
|
|
94
119
|
const tokens = tokenize(command);
|
|
95
120
|
file = tokens[0];
|
|
96
121
|
args = tokens.slice(1);
|
|
97
122
|
}
|
|
123
|
+
else {
|
|
124
|
+
file = command[0];
|
|
125
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
126
|
+
}
|
|
98
127
|
if (!file)
|
|
99
128
|
return 0;
|
|
100
129
|
dbg$1('exec (plain)', { file, args, stdio: opts.stdio });
|
|
@@ -107,16 +136,15 @@ const runCommand = async (command, shell, opts) => {
|
|
|
107
136
|
plainOpts.env = envSan;
|
|
108
137
|
if (opts.stdio !== undefined)
|
|
109
138
|
plainOpts.stdio = opts.stdio;
|
|
110
|
-
const
|
|
111
|
-
if (opts.stdio === 'pipe' &&
|
|
112
|
-
process.stdout.write(
|
|
139
|
+
const ok = pickResult((await execa(file, args, plainOpts)));
|
|
140
|
+
if (opts.stdio === 'pipe' && ok.stdout) {
|
|
141
|
+
process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
|
|
113
142
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return typeof exit === 'number' ? exit : Number.NaN;
|
|
143
|
+
dbg$1('exit (plain)', { exitCode: ok.exitCode });
|
|
144
|
+
return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
|
|
117
145
|
}
|
|
118
146
|
else {
|
|
119
|
-
const commandStr =
|
|
147
|
+
const commandStr = typeof command === 'string' ? command : command.join(' ');
|
|
120
148
|
dbg$1('exec (shell)', {
|
|
121
149
|
shell: typeof shell === 'string' ? shell : 'custom',
|
|
122
150
|
stdio: opts.stdio,
|
|
@@ -130,17 +158,29 @@ const runCommand = async (command, shell, opts) => {
|
|
|
130
158
|
shellOpts.env = envSan;
|
|
131
159
|
if (opts.stdio !== undefined)
|
|
132
160
|
shellOpts.stdio = opts.stdio;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
|
|
161
|
+
const ok = pickResult((await execaCommand(commandStr, shellOpts)));
|
|
162
|
+
if (opts.stdio === 'pipe' && ok.stdout) {
|
|
163
|
+
process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
|
|
137
164
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return typeof exit === 'number' ? exit : Number.NaN;
|
|
165
|
+
dbg$1('exit (shell)', { exitCode: ok.exitCode });
|
|
166
|
+
return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
|
|
141
167
|
}
|
|
142
|
-
}
|
|
168
|
+
}
|
|
143
169
|
|
|
170
|
+
/** src/cliCore/spawnEnv.ts
|
|
171
|
+
* Build a sanitized environment bag for child processes.
|
|
172
|
+
*
|
|
173
|
+
* Requirements addressed:
|
|
174
|
+
* - Provide a single helper (buildSpawnEnv) to normalize/dedupe child env.
|
|
175
|
+
* - Drop undefined values (exactOptional semantics).
|
|
176
|
+
* - On Windows, dedupe keys case-insensitively and prefer the last value,
|
|
177
|
+
* preserving the latest key's casing. Ensure HOME fallback from USERPROFILE.
|
|
178
|
+
* Normalize TMP/TEMP consistency when either is present.
|
|
179
|
+
* - On POSIX, keep keys as-is; when a temp dir key is present (TMPDIR/TMP/TEMP),
|
|
180
|
+
* ensure TMPDIR exists for downstream consumers that expect it.
|
|
181
|
+
*
|
|
182
|
+
* Adapter responsibility: pure mapping; no business logic.
|
|
183
|
+
*/
|
|
144
184
|
const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
|
|
145
185
|
/** Build a sanitized env for child processes from base + overlay. */
|
|
146
186
|
const buildSpawnEnv = (base, overlay) => {
|
|
@@ -183,33 +223,145 @@ const buildSpawnEnv = (base, overlay) => {
|
|
|
183
223
|
return out;
|
|
184
224
|
};
|
|
185
225
|
|
|
186
|
-
/**
|
|
187
|
-
*
|
|
226
|
+
/**
|
|
227
|
+
* Zod schemas for configuration files discovered by the new loader.
|
|
188
228
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
229
|
+
* Notes:
|
|
230
|
+
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
231
|
+
* - RESOLVED: normalized shapes (paths always string[]).
|
|
232
|
+
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
192
233
|
*/
|
|
234
|
+
// String-only env value map
|
|
235
|
+
const stringMap = z.record(z.string(), z.string());
|
|
236
|
+
const envStringMap = z.record(z.string(), stringMap);
|
|
237
|
+
// Allow string[] or single string for "paths" in RAW; normalize later.
|
|
238
|
+
const rawPathsSchema = z.union([z.array(z.string()), z.string()]).optional();
|
|
239
|
+
const getDotenvConfigSchemaRaw = z.object({
|
|
240
|
+
dotenvToken: z.string().optional(),
|
|
241
|
+
privateToken: z.string().optional(),
|
|
242
|
+
paths: rawPathsSchema,
|
|
243
|
+
loadProcess: z.boolean().optional(),
|
|
244
|
+
log: z.boolean().optional(),
|
|
245
|
+
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
246
|
+
scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
247
|
+
requiredKeys: z.array(z.string()).optional(),
|
|
248
|
+
schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
249
|
+
vars: stringMap.optional(), // public, global
|
|
250
|
+
envVars: envStringMap.optional(), // public, per-env
|
|
251
|
+
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
252
|
+
dynamic: z.unknown().optional(),
|
|
253
|
+
// Per-plugin config bag; validated by plugins/host when used.
|
|
254
|
+
plugins: z.record(z.string(), z.unknown()).optional(),
|
|
255
|
+
});
|
|
256
|
+
// Normalize paths to string[]
|
|
257
|
+
const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
|
|
258
|
+
getDotenvConfigSchemaRaw.transform((raw) => ({
|
|
259
|
+
...raw,
|
|
260
|
+
paths: normalizePaths(raw.paths),
|
|
261
|
+
}));
|
|
262
|
+
|
|
193
263
|
/**
|
|
194
|
-
*
|
|
264
|
+
* Dotenv expansion utilities.
|
|
265
|
+
*
|
|
266
|
+
* This module implements recursive expansion of environment-variable
|
|
267
|
+
* references in strings and records. It supports both whitespace and
|
|
268
|
+
* bracket syntaxes with optional defaults:
|
|
269
|
+
*
|
|
270
|
+
* - Whitespace: `$VAR[:default]`
|
|
271
|
+
* - Bracketed: `${VAR[:default]}`
|
|
272
|
+
*
|
|
273
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
274
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
275
|
+
*/
|
|
276
|
+
/**
|
|
277
|
+
* Like String.prototype.search but returns the last index.
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
const searchLast = (str, rgx) => {
|
|
281
|
+
const matches = Array.from(str.matchAll(rgx));
|
|
282
|
+
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
283
|
+
};
|
|
284
|
+
const replaceMatch = (value, match, ref) => {
|
|
285
|
+
/**
|
|
286
|
+
* @internal
|
|
287
|
+
*/
|
|
288
|
+
const group = match[0];
|
|
289
|
+
const key = match[1];
|
|
290
|
+
const defaultValue = match[2];
|
|
291
|
+
if (!key)
|
|
292
|
+
return value;
|
|
293
|
+
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
294
|
+
return interpolate(replacement, ref);
|
|
295
|
+
};
|
|
296
|
+
const interpolate = (value = '', ref = {}) => {
|
|
297
|
+
/**
|
|
298
|
+
* @internal
|
|
299
|
+
*/
|
|
300
|
+
// if value is falsy, return it as is
|
|
301
|
+
if (!value)
|
|
302
|
+
return value;
|
|
303
|
+
// get position of last unescaped dollar sign
|
|
304
|
+
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
305
|
+
// return value if none found
|
|
306
|
+
if (lastUnescapedDollarSignIndex === -1)
|
|
307
|
+
return value;
|
|
308
|
+
// evaluate the value tail
|
|
309
|
+
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
310
|
+
// find whitespace pattern: $KEY:DEFAULT
|
|
311
|
+
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
312
|
+
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
313
|
+
if (whitespaceMatch != null)
|
|
314
|
+
return replaceMatch(value, whitespaceMatch, ref);
|
|
315
|
+
else {
|
|
316
|
+
// find bracket pattern: ${KEY:DEFAULT}
|
|
317
|
+
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
318
|
+
const bracketMatch = bracketPattern.exec(tail);
|
|
319
|
+
if (bracketMatch != null)
|
|
320
|
+
return replaceMatch(value, bracketMatch, ref);
|
|
321
|
+
}
|
|
322
|
+
return value;
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Recursively expands environment variables in a string. Variables may be
|
|
326
|
+
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
327
|
+
* Unknown variables will expand to an empty string.
|
|
328
|
+
*
|
|
329
|
+
* @param value - The string to expand.
|
|
330
|
+
* @param ref - The reference object to use for variable expansion.
|
|
331
|
+
* @returns The expanded string.
|
|
195
332
|
*
|
|
196
333
|
* @example
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
334
|
+
* ```ts
|
|
335
|
+
* process.env.FOO = 'bar';
|
|
336
|
+
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
337
|
+
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
338
|
+
* ```
|
|
339
|
+
*
|
|
340
|
+
* @remarks
|
|
341
|
+
* The expansion is recursive. If a referenced variable itself contains
|
|
342
|
+
* references, those will also be expanded until a stable value is reached.
|
|
343
|
+
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
200
344
|
*/
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
...rest,
|
|
205
|
-
children: [...children],
|
|
206
|
-
use(child) {
|
|
207
|
-
this.children.push(child);
|
|
208
|
-
return this;
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
return plugin;
|
|
345
|
+
const dotenvExpand = (value, ref = process.env) => {
|
|
346
|
+
const result = interpolate(value, ref);
|
|
347
|
+
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
212
348
|
};
|
|
349
|
+
/**
|
|
350
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
351
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
352
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
353
|
+
* empty string.
|
|
354
|
+
*
|
|
355
|
+
* @param value - The string to expand.
|
|
356
|
+
* @returns The expanded string.
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* ```ts
|
|
360
|
+
* process.env.FOO = 'bar';
|
|
361
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
364
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
213
365
|
|
|
214
366
|
/** src/diagnostics/entropy.ts
|
|
215
367
|
* Entropy diagnostics (presentation-only).
|
|
@@ -219,7 +371,7 @@ const definePlugin = (spec) => {
|
|
|
219
371
|
*/
|
|
220
372
|
const warned = new Set();
|
|
221
373
|
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
222
|
-
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
374
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => (typeof p === 'string' ? new RegExp(p, 'i') : p));
|
|
223
375
|
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
224
376
|
const shannonBitsPerChar = (s) => {
|
|
225
377
|
const freq = new Map();
|
|
@@ -266,7 +418,7 @@ const DEFAULT_PATTERNS = [
|
|
|
266
418
|
'\\bapi[_-]?key\\b',
|
|
267
419
|
'\\bkey\\b',
|
|
268
420
|
];
|
|
269
|
-
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
421
|
+
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
|
|
270
422
|
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
271
423
|
const MASK = '[redacted]';
|
|
272
424
|
/**
|
|
@@ -291,34 +443,6 @@ const redactTriple = (key, triple, opts) => {
|
|
|
291
443
|
return out;
|
|
292
444
|
};
|
|
293
445
|
|
|
294
|
-
/**
|
|
295
|
-
* Batch services (neutral): resolve command and shell settings.
|
|
296
|
-
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
297
|
-
*/
|
|
298
|
-
/**
|
|
299
|
-
* Resolve a command string from the {@link Scripts} table.
|
|
300
|
-
* A script may be expressed as a string or an object with a `cmd` property.
|
|
301
|
-
*
|
|
302
|
-
* @param scripts - Optional scripts table.
|
|
303
|
-
* @param command - User-provided command name or string.
|
|
304
|
-
* @returns Resolved command string (falls back to the provided command).
|
|
305
|
-
*/
|
|
306
|
-
const resolveCommand = (scripts, command) => scripts && typeof scripts[command] === 'object'
|
|
307
|
-
? scripts[command].cmd
|
|
308
|
-
: (scripts?.[command] ?? command);
|
|
309
|
-
/**
|
|
310
|
-
* Resolve the shell setting for a given command:
|
|
311
|
-
* - If the script entry is an object, prefer its `shell` override.
|
|
312
|
-
* - Otherwise use the provided `shell` (string | boolean).
|
|
313
|
-
*
|
|
314
|
-
* @param scripts - Optional scripts table.
|
|
315
|
-
* @param command - User-provided command name or string.
|
|
316
|
-
* @param shell - Global shell preference (string | boolean).
|
|
317
|
-
*/
|
|
318
|
-
const resolveShell = (scripts, command, shell) => scripts && typeof scripts[command] === 'object'
|
|
319
|
-
? (scripts[command].shell ?? false)
|
|
320
|
-
: (shell ?? false);
|
|
321
|
-
|
|
322
446
|
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
323
447
|
const baseRootOptionDefaults = {
|
|
324
448
|
dotenvToken: '.env',
|
|
@@ -346,6 +470,8 @@ const baseRootOptionDefaults = {
|
|
|
346
470
|
// (debug/log/exclude* resolved via flag utils)
|
|
347
471
|
};
|
|
348
472
|
|
|
473
|
+
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
474
|
+
|
|
349
475
|
/** @internal */
|
|
350
476
|
const isPlainObject = (value) => value !== null &&
|
|
351
477
|
typeof value === 'object' &&
|
|
@@ -367,27 +493,223 @@ const mergeInto = (target, source) => {
|
|
|
367
493
|
}
|
|
368
494
|
return target;
|
|
369
495
|
};
|
|
370
|
-
|
|
371
|
-
* Perform a deep defaults-style merge across plain objects. *
|
|
372
|
-
* - Only merges plain objects (prototype === Object.prototype).
|
|
373
|
-
* - Arrays and non-objects are replaced, not merged.
|
|
374
|
-
* - `undefined` values are ignored and do not overwrite prior values.
|
|
375
|
-
*
|
|
376
|
-
* @typeParam T - The resulting shape after merging all layers.
|
|
377
|
-
* @param layers - Zero or more partial layers in ascending precedence order.
|
|
378
|
-
* @returns The merged object typed as {@link T}.
|
|
379
|
-
*
|
|
380
|
-
* @example
|
|
381
|
-
* defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
|
|
382
|
-
* =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
|
|
383
|
-
*/
|
|
384
|
-
const defaultsDeep = (...layers) => {
|
|
496
|
+
function defaultsDeep(...layers) {
|
|
385
497
|
const result = layers
|
|
386
498
|
.filter(Boolean)
|
|
387
499
|
.reduce((acc, layer) => mergeInto(acc, layer), {});
|
|
388
500
|
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** src/util/omitUndefined.ts
|
|
504
|
+
* Helpers to drop undefined-valued properties in a typed-friendly way.
|
|
505
|
+
*/
|
|
506
|
+
/**
|
|
507
|
+
* Omit keys whose runtime value is undefined from a shallow object.
|
|
508
|
+
* Returns a Partial with non-undefined value types preserved.
|
|
509
|
+
*/
|
|
510
|
+
/**
|
|
511
|
+
* Specialized helper for env-like maps: drop undefined and return string-only.
|
|
512
|
+
*/
|
|
513
|
+
function omitUndefinedRecord(obj) {
|
|
514
|
+
const out = {};
|
|
515
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
516
|
+
if (v !== undefined)
|
|
517
|
+
out[k] = v;
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/GetDotenvOptions.ts
|
|
523
|
+
/**
|
|
524
|
+
* Canonical programmatic options and helpers for get-dotenv.
|
|
525
|
+
*
|
|
526
|
+
* Requirements addressed:
|
|
527
|
+
* - GetDotenvOptions derives from the Zod schema output (single source of truth).
|
|
528
|
+
* - Removed deprecated/compat flags from the public shape (e.g., useConfigLoader).
|
|
529
|
+
* - Provide Vars-aware defineDynamic and a typed config builder defineGetDotenvConfig\<Vars, Env\>().
|
|
530
|
+
* - Preserve existing behavior for defaults resolution and compat converters.
|
|
531
|
+
*/
|
|
532
|
+
/**
|
|
533
|
+
* Converts programmatic CLI options to `getDotenv` options.
|
|
534
|
+
*
|
|
535
|
+
* Accepts "stringly" CLI inputs for vars/paths and normalizes them into
|
|
536
|
+
* the programmatic shape. Preserves exactOptionalPropertyTypes semantics by
|
|
537
|
+
* omitting keys when undefined.
|
|
538
|
+
*/
|
|
539
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern,
|
|
540
|
+
// drop CLI-only keys from the pass-through bag
|
|
541
|
+
debug: _debug, scripts: _scripts, ...rest }) => {
|
|
542
|
+
// Split helper for delimited strings or regex patterns
|
|
543
|
+
const splitBy = (value, delim, pattern) => {
|
|
544
|
+
if (!value)
|
|
545
|
+
return [];
|
|
546
|
+
if (pattern)
|
|
547
|
+
return value.split(RegExp(pattern));
|
|
548
|
+
if (typeof delim === 'string')
|
|
549
|
+
return value.split(delim);
|
|
550
|
+
return value.split(' ');
|
|
551
|
+
};
|
|
552
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
553
|
+
let parsedVars;
|
|
554
|
+
if (typeof vars === 'string') {
|
|
555
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern)
|
|
556
|
+
.map((v) => v.split(varsAssignorPattern
|
|
557
|
+
? RegExp(varsAssignorPattern)
|
|
558
|
+
: (varsAssignor ?? '=')))
|
|
559
|
+
.filter(([k]) => typeof k === 'string' && k.length > 0);
|
|
560
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
561
|
+
}
|
|
562
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
563
|
+
// Accept provided object map of string | undefined; drop undefined values
|
|
564
|
+
// in the normalization step below to produce a ProcessEnv-compatible bag.
|
|
565
|
+
parsedVars = Object.fromEntries(Object.entries(vars));
|
|
566
|
+
}
|
|
567
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
568
|
+
// expectations and the compat test assertions.
|
|
569
|
+
if (parsedVars) {
|
|
570
|
+
parsedVars = omitUndefinedRecord(parsedVars);
|
|
571
|
+
}
|
|
572
|
+
// Tolerate paths as either a delimited string or string[]
|
|
573
|
+
const pathsOut = Array.isArray(paths)
|
|
574
|
+
? paths.filter((p) => typeof p === 'string')
|
|
575
|
+
: splitBy(paths, pathsDelimiter, pathsDelimiterPattern);
|
|
576
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
577
|
+
return {
|
|
578
|
+
...rest,
|
|
579
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
580
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
581
|
+
};
|
|
389
582
|
};
|
|
390
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Zod schemas for programmatic GetDotenv options.
|
|
586
|
+
*
|
|
587
|
+
* Canonical source of truth for options shape. Public types are derived
|
|
588
|
+
* from these schemas (see consumers via z.output\<\>).
|
|
589
|
+
*/
|
|
590
|
+
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
591
|
+
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
592
|
+
// RAW: all fields optional — undefined means "inherit" from lower layers.
|
|
593
|
+
z.object({
|
|
594
|
+
defaultEnv: z.string().optional(),
|
|
595
|
+
dotenvToken: z.string().optional(),
|
|
596
|
+
dynamicPath: z.string().optional(),
|
|
597
|
+
// Dynamic map is intentionally wide for now; refine once sources are normalized.
|
|
598
|
+
dynamic: z.record(z.string(), z.unknown()).optional(),
|
|
599
|
+
env: z.string().optional(),
|
|
600
|
+
excludeDynamic: z.boolean().optional(),
|
|
601
|
+
excludeEnv: z.boolean().optional(),
|
|
602
|
+
excludeGlobal: z.boolean().optional(),
|
|
603
|
+
excludePrivate: z.boolean().optional(),
|
|
604
|
+
excludePublic: z.boolean().optional(),
|
|
605
|
+
loadProcess: z.boolean().optional(),
|
|
606
|
+
log: z.boolean().optional(),
|
|
607
|
+
logger: z.unknown().optional(),
|
|
608
|
+
outputPath: z.string().optional(),
|
|
609
|
+
paths: z.array(z.string()).optional(),
|
|
610
|
+
privateToken: z.string().optional(),
|
|
611
|
+
vars: processEnvSchema.optional(),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Instance-bound plugin config store.
|
|
616
|
+
* Host stores the validated/interpolated slice per plugin instance.
|
|
617
|
+
* The store is intentionally private to this module; definePlugin()
|
|
618
|
+
* provides a typed accessor that reads from this store for the calling
|
|
619
|
+
* plugin instance.
|
|
620
|
+
*/
|
|
621
|
+
const PLUGIN_CONFIG_STORE = new WeakMap();
|
|
622
|
+
const _getPluginConfigForInstance = (plugin) => PLUGIN_CONFIG_STORE.get(plugin);
|
|
623
|
+
|
|
624
|
+
/** src/cliHost/definePlugin.ts
|
|
625
|
+
* Plugin contracts for the GetDotenv CLI host.
|
|
626
|
+
*
|
|
627
|
+
* This module exposes a structural public interface for the host that plugins
|
|
628
|
+
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
629
|
+
* nominal class identity issues (private fields) in downstream consumers.
|
|
630
|
+
*/
|
|
631
|
+
/* eslint-disable tsdoc/syntax */
|
|
632
|
+
function definePlugin(spec) {
|
|
633
|
+
const { children = [], ...rest } = spec;
|
|
634
|
+
// Default to a strict empty-object schema so “no-config” plugins fail fast
|
|
635
|
+
// on unknown keys and provide a concrete {} at runtime.
|
|
636
|
+
const effectiveSchema = spec.configSchema ?? z.object({}).strict();
|
|
637
|
+
// Build base plugin first, then extend with instance-bound helpers.
|
|
638
|
+
const base = {
|
|
639
|
+
...rest,
|
|
640
|
+
// Always carry a schema (strict empty by default) to simplify host logic
|
|
641
|
+
// and improve inference/ergonomics for plugin authors.
|
|
642
|
+
configSchema: effectiveSchema,
|
|
643
|
+
children: [...children],
|
|
644
|
+
use(child) {
|
|
645
|
+
this.children.push(child);
|
|
646
|
+
return this;
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
// Attach instance-bound helpers on the returned plugin object.
|
|
650
|
+
const extended = base;
|
|
651
|
+
extended.readConfig = function (_cli) {
|
|
652
|
+
// Config is stored per-plugin-instance by the host (WeakMap in computeContext).
|
|
653
|
+
const value = _getPluginConfigForInstance(extended);
|
|
654
|
+
if (value === undefined) {
|
|
655
|
+
// Guard: host has not resolved config yet (incorrect lifecycle usage).
|
|
656
|
+
throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
|
|
657
|
+
}
|
|
658
|
+
return value;
|
|
659
|
+
};
|
|
660
|
+
// Plugin-bound dynamic option factory
|
|
661
|
+
extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
|
|
662
|
+
return cli.createDynamicOption(flags, (cfg) => {
|
|
663
|
+
// Prefer the validated slice stored per instance; fallback to help-bag
|
|
664
|
+
// (by-id) so top-level `-h` can render effective defaults before resolve.
|
|
665
|
+
const fromStore = _getPluginConfigForInstance(extended);
|
|
666
|
+
const id = extended.id;
|
|
667
|
+
let fromBag;
|
|
668
|
+
if (!fromStore && id) {
|
|
669
|
+
const maybe = cfg.plugins[id];
|
|
670
|
+
if (maybe && typeof maybe === 'object') {
|
|
671
|
+
fromBag = maybe;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Always provide a concrete object to dynamic callbacks:
|
|
675
|
+
// - With a schema: computeContext stores the parsed object.
|
|
676
|
+
// - Without a schema: computeContext stores {}.
|
|
677
|
+
// - Help-time fallback: coalesce to {} when only a by-id bag exists.
|
|
678
|
+
const cfgVal = (fromStore ?? fromBag ?? {});
|
|
679
|
+
return desc(cfg, cfgVal);
|
|
680
|
+
}, parser, defaultValue);
|
|
681
|
+
};
|
|
682
|
+
return extended;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Batch services (neutral): resolve command and shell settings.
|
|
687
|
+
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
688
|
+
*/
|
|
689
|
+
/**
|
|
690
|
+
* Resolve a command string from the {@link Scripts} table.
|
|
691
|
+
* A script may be expressed as a string or an object with a `cmd` property.
|
|
692
|
+
*
|
|
693
|
+
* @param scripts - Optional scripts table.
|
|
694
|
+
* @param command - User-provided command name or string.
|
|
695
|
+
* @returns Resolved command string (falls back to the provided command).
|
|
696
|
+
*/
|
|
697
|
+
const resolveCommand = (scripts, command) => scripts && typeof scripts[command] === 'object'
|
|
698
|
+
? scripts[command].cmd
|
|
699
|
+
: (scripts?.[command] ?? command);
|
|
700
|
+
/**
|
|
701
|
+
* Resolve the shell setting for a given command:
|
|
702
|
+
* - If the script entry is an object, prefer its `shell` override.
|
|
703
|
+
* - Otherwise use the provided `shell` (string | boolean).
|
|
704
|
+
*
|
|
705
|
+
* @param scripts - Optional scripts table.
|
|
706
|
+
* @param command - User-provided command name or string.
|
|
707
|
+
* @param shell - Global shell preference (string | boolean).
|
|
708
|
+
*/
|
|
709
|
+
const resolveShell = (scripts, command, shell) => scripts && typeof scripts[command] === 'object'
|
|
710
|
+
? (scripts[command].shell ?? false)
|
|
711
|
+
: (shell ?? false);
|
|
712
|
+
|
|
391
713
|
/**
|
|
392
714
|
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
393
715
|
* - If the user explicitly enabled the flag, return true.
|
|
@@ -513,180 +835,225 @@ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
|
513
835
|
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
514
836
|
};
|
|
515
837
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
* references in strings and records. It supports both whitespace and
|
|
521
|
-
* bracket syntaxes with optional defaults:
|
|
522
|
-
*
|
|
523
|
-
* - Whitespace: `$VAR[:default]`
|
|
524
|
-
* - Bracketed: `${VAR[:default]}`
|
|
525
|
-
*
|
|
526
|
-
* Escaped dollar signs (`\$`) are preserved.
|
|
527
|
-
* Unknown variables resolve to empty string unless a default is provided.
|
|
528
|
-
*/
|
|
529
|
-
/**
|
|
530
|
-
* Like String.prototype.search but returns the last index.
|
|
531
|
-
* @internal
|
|
532
|
-
*/
|
|
533
|
-
const searchLast = (str, rgx) => {
|
|
534
|
-
const matches = Array.from(str.matchAll(rgx));
|
|
535
|
-
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
536
|
-
};
|
|
537
|
-
const replaceMatch = (value, match, ref) => {
|
|
538
|
-
/**
|
|
539
|
-
* @internal
|
|
540
|
-
*/
|
|
541
|
-
const group = match[0];
|
|
542
|
-
const key = match[1];
|
|
543
|
-
const defaultValue = match[2];
|
|
544
|
-
if (!key)
|
|
545
|
-
return value;
|
|
546
|
-
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
547
|
-
return interpolate(replacement, ref);
|
|
548
|
-
};
|
|
549
|
-
const interpolate = (value = '', ref = {}) => {
|
|
550
|
-
/**
|
|
551
|
-
* @internal
|
|
552
|
-
*/
|
|
553
|
-
// if value is falsy, return it as is
|
|
554
|
-
if (!value)
|
|
555
|
-
return value;
|
|
556
|
-
// get position of last unescaped dollar sign
|
|
557
|
-
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
558
|
-
// return value if none found
|
|
559
|
-
if (lastUnescapedDollarSignIndex === -1)
|
|
560
|
-
return value;
|
|
561
|
-
// evaluate the value tail
|
|
562
|
-
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
563
|
-
// find whitespace pattern: $KEY:DEFAULT
|
|
564
|
-
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
565
|
-
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
566
|
-
if (whitespaceMatch != null)
|
|
567
|
-
return replaceMatch(value, whitespaceMatch, ref);
|
|
568
|
-
else {
|
|
569
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
570
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
571
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
572
|
-
if (bracketMatch != null)
|
|
573
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
838
|
+
const dbg = (...args) => {
|
|
839
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
840
|
+
// Use stderr to avoid interfering with stdout assertions
|
|
841
|
+
console.error('[getdotenv:alias]', ...args);
|
|
574
842
|
}
|
|
575
|
-
return value;
|
|
576
843
|
};
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
*
|
|
586
|
-
* @example
|
|
587
|
-
* ```ts
|
|
588
|
-
* process.env.FOO = 'bar';
|
|
589
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
590
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
591
|
-
* ```
|
|
592
|
-
*
|
|
593
|
-
* @remarks
|
|
594
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
595
|
-
* references, those will also be expanded until a stable value is reached.
|
|
596
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
597
|
-
*/
|
|
598
|
-
const dotenvExpand = (value, ref = process.env) => {
|
|
599
|
-
const result = interpolate(value, ref);
|
|
600
|
-
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
844
|
+
// Strip one symmetric outer quote layer
|
|
845
|
+
const stripOne = (s) => {
|
|
846
|
+
if (s.length < 2)
|
|
847
|
+
return s;
|
|
848
|
+
const a = s.charAt(0);
|
|
849
|
+
const b = s.charAt(s.length - 1);
|
|
850
|
+
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
851
|
+
return symmetric ? s.slice(1, -1) : s;
|
|
601
852
|
};
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
//
|
|
620
|
-
/**
|
|
621
|
-
* Converts programmatic CLI options to `getDotenv` options. *
|
|
622
|
-
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
623
|
-
*
|
|
624
|
-
* @returns `getDotenv` options.
|
|
625
|
-
*/
|
|
626
|
-
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
627
|
-
/**
|
|
628
|
-
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
629
|
-
*
|
|
630
|
-
* - Splits {@link GetDotenvCliOptions.paths} using either a delimiter * or a regular expression pattern into a string array. * - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
|
|
631
|
-
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
632
|
-
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
633
|
-
*
|
|
634
|
-
* @remarks
|
|
635
|
-
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
636
|
-
*/
|
|
637
|
-
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
638
|
-
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
639
|
-
const restObj = { ...rest };
|
|
640
|
-
delete restObj.debug;
|
|
641
|
-
delete restObj.scripts;
|
|
642
|
-
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
643
|
-
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
644
|
-
let parsedVars;
|
|
645
|
-
if (typeof vars === 'string') {
|
|
646
|
-
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
647
|
-
? RegExp(varsAssignorPattern)
|
|
648
|
-
: (varsAssignor ?? '=')));
|
|
649
|
-
parsedVars = Object.fromEntries(kvPairs);
|
|
853
|
+
async function maybeRunAlias(cli, thisCommand, aliasKey, state) {
|
|
854
|
+
dbg('alias:maybe:start');
|
|
855
|
+
const raw = thisCommand.rawArgs ?? [];
|
|
856
|
+
const childNames = thisCommand.commands.flatMap((c) => [
|
|
857
|
+
c.name(),
|
|
858
|
+
...c.aliases(),
|
|
859
|
+
]);
|
|
860
|
+
const hasSub = childNames.some((n) => raw.includes(n));
|
|
861
|
+
const o = thisCommand.opts();
|
|
862
|
+
const val = o[aliasKey];
|
|
863
|
+
const provided = typeof val === 'string'
|
|
864
|
+
? val.length > 0
|
|
865
|
+
: Array.isArray(val)
|
|
866
|
+
? val.length > 0
|
|
867
|
+
: false;
|
|
868
|
+
if (!provided || hasSub) {
|
|
869
|
+
dbg('alias:maybe:skip', { provided, hasSub });
|
|
870
|
+
return; // not an alias-only invocation
|
|
650
871
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
parsedVars = Object.fromEntries(entries);
|
|
872
|
+
if (state.handled) {
|
|
873
|
+
dbg('alias:maybe:already-handled');
|
|
874
|
+
return;
|
|
655
875
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
876
|
+
state.handled = true;
|
|
877
|
+
dbg('alias-only invocation detected');
|
|
878
|
+
// Merge CLI options and resolve dotenv context.
|
|
879
|
+
const { merged } = resolveCliOptions(o, baseGetDotenvCliOptions, process.env.getDotenvCliOptions);
|
|
880
|
+
const mergedBag = merged;
|
|
881
|
+
const logger = (mergedBag.logger ?? console);
|
|
882
|
+
const serviceOptions = getDotenvCliOptions2Options(mergedBag);
|
|
883
|
+
await cli.resolveAndLoad(serviceOptions);
|
|
884
|
+
// Normalize alias value
|
|
885
|
+
const joined = typeof val === 'string'
|
|
886
|
+
? val
|
|
887
|
+
: Array.isArray(val)
|
|
888
|
+
? val.map(String).join(' ')
|
|
889
|
+
: '';
|
|
890
|
+
const expanded = dotenvExpandFromProcessEnv(joined);
|
|
891
|
+
const input = mergedBag.expand === false
|
|
892
|
+
? joined
|
|
893
|
+
: expanded !== undefined
|
|
894
|
+
? expanded
|
|
895
|
+
: joined;
|
|
896
|
+
// Scripts: prefer well-formed records; tolerate absent/bad shapes
|
|
897
|
+
const maybeScripts = mergedBag.scripts;
|
|
898
|
+
const scripts = maybeScripts && typeof maybeScripts === 'object'
|
|
899
|
+
? maybeScripts
|
|
900
|
+
: undefined;
|
|
901
|
+
const resolved = resolveCommand(scripts, input);
|
|
902
|
+
if (mergedBag.debug) {
|
|
903
|
+
logger.log('\n*** command ***\n', `'${resolved}'`);
|
|
660
904
|
}
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
668
|
-
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
669
|
-
return {
|
|
670
|
-
...restObj,
|
|
671
|
-
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
672
|
-
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
673
|
-
};
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
const dbg = (...args) => {
|
|
677
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
678
|
-
// Use stderr to avoid interfering with stdout assertions
|
|
679
|
-
console.error('[getdotenv:alias]', ...args);
|
|
905
|
+
// Round-trip CLI options for nested getdotenv invocations. Omit logger
|
|
906
|
+
// (functions/circulars) and guard JSON serialization to avoid hard failures.
|
|
907
|
+
const { logger: _omitLogger, ...envBag } = mergedBag;
|
|
908
|
+
let nestedBag;
|
|
909
|
+
try {
|
|
910
|
+
nestedBag = JSON.stringify(envBag);
|
|
680
911
|
}
|
|
681
|
-
|
|
912
|
+
catch {
|
|
913
|
+
nestedBag = undefined;
|
|
914
|
+
}
|
|
915
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
916
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
917
|
+
const forceExit = process.env.GETDOTENV_FORCE_EXIT === '1';
|
|
918
|
+
const capture = !underTests &&
|
|
919
|
+
(process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
920
|
+
Boolean(mergedBag.capture));
|
|
921
|
+
dbg('run:start', {
|
|
922
|
+
capture,
|
|
923
|
+
shell: mergedBag.shell,
|
|
924
|
+
});
|
|
925
|
+
const ctx = cli.getCtx();
|
|
926
|
+
const dotenv = (ctx?.dotenv ?? {});
|
|
927
|
+
// Diagnostics: --trace [keys...]
|
|
928
|
+
const traceOpt = mergedBag.trace;
|
|
929
|
+
if (traceOpt) {
|
|
930
|
+
const parentKeys = Object.keys(process.env);
|
|
931
|
+
const dotenvKeys = Object.keys(dotenv);
|
|
932
|
+
const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
|
|
933
|
+
const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
|
|
934
|
+
const childEnvPreview = {
|
|
935
|
+
...process.env,
|
|
936
|
+
...dotenv,
|
|
937
|
+
};
|
|
938
|
+
for (const k of keys) {
|
|
939
|
+
const parent = process.env[k];
|
|
940
|
+
const dot = dotenv[k];
|
|
941
|
+
const final = childEnvPreview[k];
|
|
942
|
+
const origin = dot !== undefined
|
|
943
|
+
? 'dotenv'
|
|
944
|
+
: parent !== undefined
|
|
945
|
+
? 'parent'
|
|
946
|
+
: 'unset';
|
|
947
|
+
const redFlag = mergedBag.redact;
|
|
948
|
+
const redPatterns = mergedBag.redactPatterns;
|
|
949
|
+
const redOpts = {};
|
|
950
|
+
if (redFlag)
|
|
951
|
+
redOpts.redact = true;
|
|
952
|
+
if (redFlag && Array.isArray(redPatterns))
|
|
953
|
+
redOpts.redactPatterns = redPatterns;
|
|
954
|
+
const tripleBag = {};
|
|
955
|
+
if (parent !== undefined)
|
|
956
|
+
tripleBag.parent = parent;
|
|
957
|
+
if (dot !== undefined)
|
|
958
|
+
tripleBag.dotenv = dot;
|
|
959
|
+
if (final !== undefined)
|
|
960
|
+
tripleBag.final = final;
|
|
961
|
+
const triple = redactTriple(k, tripleBag, redOpts);
|
|
962
|
+
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
963
|
+
const entOpts = {};
|
|
964
|
+
const warnEntropy = mergedBag.warnEntropy;
|
|
965
|
+
const entropyThreshold = mergedBag
|
|
966
|
+
.entropyThreshold;
|
|
967
|
+
const entropyMinLength = mergedBag
|
|
968
|
+
.entropyMinLength;
|
|
969
|
+
const entropyWhitelist = mergedBag.entropyWhitelist;
|
|
970
|
+
if (typeof warnEntropy === 'boolean')
|
|
971
|
+
entOpts.warnEntropy = warnEntropy;
|
|
972
|
+
if (typeof entropyThreshold === 'number')
|
|
973
|
+
entOpts.entropyThreshold = entropyThreshold;
|
|
974
|
+
if (typeof entropyMinLength === 'number')
|
|
975
|
+
entOpts.entropyMinLength = entropyMinLength;
|
|
976
|
+
if (Array.isArray(entropyWhitelist))
|
|
977
|
+
entOpts.entropyWhitelist = entropyWhitelist;
|
|
978
|
+
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const shellSetting = resolveShell(scripts, input, mergedBag.shell);
|
|
982
|
+
// Preserve argv array for Node -e snippets under shell-off
|
|
983
|
+
let commandArg = resolved;
|
|
984
|
+
if (shellSetting === false && resolved === input) {
|
|
985
|
+
// Important: preserve doubled quotes within the Node -e payload so
|
|
986
|
+
// empty string literals ("") survive; Windows-style doubling must not
|
|
987
|
+
// collapse "" -> " in this path.
|
|
988
|
+
const parts = tokenize(input, { preserveDoubledQuotes: true });
|
|
989
|
+
if (parts.length >= 3 &&
|
|
990
|
+
parts[0]?.toLowerCase() === 'node' &&
|
|
991
|
+
(parts[1] === '-e' || parts[1] === '--eval')) {
|
|
992
|
+
// Peel exactly one symmetric outer quote on the code arg
|
|
993
|
+
parts[2] = stripOne(parts[2] ?? '');
|
|
994
|
+
// Historical behavior: pass the argv array through unchanged for shell-off.
|
|
995
|
+
commandArg = parts;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
let exitCode = Number.NaN;
|
|
999
|
+
try {
|
|
1000
|
+
exitCode = await runCommand(commandArg, shellSetting, {
|
|
1001
|
+
env: buildSpawnEnv(process.env, nestedBag
|
|
1002
|
+
? {
|
|
1003
|
+
...dotenv,
|
|
1004
|
+
getDotenvCliOptions: nestedBag,
|
|
1005
|
+
}
|
|
1006
|
+
: {
|
|
1007
|
+
...dotenv,
|
|
1008
|
+
}),
|
|
1009
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
1010
|
+
});
|
|
1011
|
+
dbg('run:done', { exitCode });
|
|
1012
|
+
}
|
|
1013
|
+
catch (err) {
|
|
1014
|
+
const code = typeof err.exitCode === 'number'
|
|
1015
|
+
? err.exitCode
|
|
1016
|
+
: 1;
|
|
1017
|
+
dbg('run:error', { exitCode: code, error: String(err) });
|
|
1018
|
+
if (!underTests) {
|
|
1019
|
+
dbg('process.exit (error path)', { exitCode: code });
|
|
1020
|
+
process.exit(code);
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
dbg('process.exit suppressed for tests (error path)', {
|
|
1024
|
+
exitCode: code,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (!Number.isNaN(exitCode)) {
|
|
1030
|
+
dbg('process.exit', { exitCode });
|
|
1031
|
+
process.exit(exitCode);
|
|
1032
|
+
}
|
|
1033
|
+
if (!underTests) {
|
|
1034
|
+
dbg('process.exit (fallback: non-numeric exitCode)', { exitCode: 0 });
|
|
1035
|
+
process.exit(0);
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
dbg('process.exit (fallback suppressed for tests: non-numeric exitCode)', {
|
|
1039
|
+
exitCode: 0,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (forceExit) {
|
|
1043
|
+
setImmediate(() => process.exit(Number.isNaN(exitCode) ? 0 : exitCode));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
682
1047
|
const attachParentAlias = (cli, options, _cmd) => {
|
|
683
1048
|
const aliasSpec = typeof options.optionAlias === 'string'
|
|
684
|
-
? { flags: options.optionAlias, description: undefined
|
|
1049
|
+
? { flags: options.optionAlias, description: undefined}
|
|
685
1050
|
: options.optionAlias;
|
|
686
1051
|
if (!aliasSpec)
|
|
687
1052
|
return;
|
|
688
1053
|
const deriveKey = (flags) => {
|
|
689
|
-
|
|
1054
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
1055
|
+
console.error('[getdotenv:alias] install alias option', flags);
|
|
1056
|
+
}
|
|
690
1057
|
const long = flags.split(/[ ,|]+/).find((f) => f.startsWith('--')) ?? '--cmd';
|
|
691
1058
|
const name = long.replace(/^--/, '');
|
|
692
1059
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
@@ -696,256 +1063,24 @@ const attachParentAlias = (cli, options, _cmd) => {
|
|
|
696
1063
|
const desc = aliasSpec.description ??
|
|
697
1064
|
'alias of cmd subcommand; provide command tokens (variadic)';
|
|
698
1065
|
cli.option(aliasSpec.flags, desc);
|
|
699
|
-
// Tag the just-added parent option for grouped help rendering.
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
last
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
catch {
|
|
708
|
-
/* noop */
|
|
1066
|
+
// Tag the just-added parent option for grouped help rendering at the root.
|
|
1067
|
+
const optsArr = cli.options;
|
|
1068
|
+
if (optsArr.length > 0) {
|
|
1069
|
+
const last = optsArr[optsArr.length - 1];
|
|
1070
|
+
if (last)
|
|
1071
|
+
cli.setOptionGroup(last, 'plugin:cmd');
|
|
709
1072
|
}
|
|
710
1073
|
// Shared alias executor for either preAction or preSubcommand hooks.
|
|
711
1074
|
// Ensure we only execute once even if both hooks fire in a single parse.
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
const raw = thisCommand.rawArgs ?? [];
|
|
716
|
-
const childNames = thisCommand.commands.flatMap((c) => [
|
|
717
|
-
c.name(),
|
|
718
|
-
...c.aliases(),
|
|
719
|
-
]);
|
|
720
|
-
const hasSub = childNames.some((n) => raw.includes(n));
|
|
721
|
-
// Read alias value from parent opts.
|
|
722
|
-
const o = thisCommand.opts();
|
|
723
|
-
const val = o[aliasKey];
|
|
724
|
-
const provided = typeof val === 'string'
|
|
725
|
-
? val.length > 0
|
|
726
|
-
: Array.isArray(val)
|
|
727
|
-
? val.length > 0
|
|
728
|
-
: false;
|
|
729
|
-
if (!provided || hasSub) {
|
|
730
|
-
dbg('alias:maybe:skip', { provided, hasSub });
|
|
731
|
-
return; // not an alias-only invocation
|
|
732
|
-
}
|
|
733
|
-
if (aliasHandled) {
|
|
734
|
-
dbg('alias:maybe:already-handled');
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
aliasHandled = true;
|
|
738
|
-
dbg('alias-only invocation detected');
|
|
739
|
-
// Merge CLI options and resolve dotenv context.
|
|
740
|
-
const { merged } = resolveCliOptions(o,
|
|
741
|
-
// cast through unknown to avoid readonly -> mutable incompatibilities
|
|
742
|
-
baseRootOptionDefaults, process.env.getDotenvCliOptions);
|
|
743
|
-
const logger = merged.logger ?? console;
|
|
744
|
-
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
745
|
-
await cli.resolveAndLoad(serviceOptions);
|
|
746
|
-
// Normalize alias value.
|
|
747
|
-
const joined = typeof val === 'string'
|
|
748
|
-
? val
|
|
749
|
-
: Array.isArray(val)
|
|
750
|
-
? val.map(String).join(' ')
|
|
751
|
-
: '';
|
|
752
|
-
const input = aliasSpec.expand === false
|
|
753
|
-
? joined
|
|
754
|
-
: (dotenvExpandFromProcessEnv(joined) ?? joined);
|
|
755
|
-
dbg('resolved input', { input });
|
|
756
|
-
const resolved = resolveCommand(merged.scripts, input);
|
|
757
|
-
const lg = logger;
|
|
758
|
-
if (merged.debug) {
|
|
759
|
-
(lg.debug ?? lg.log)('\n*** command ***\n', `'${resolved}'`);
|
|
760
|
-
}
|
|
761
|
-
const { logger: _omit, ...envBag } = merged;
|
|
762
|
-
// Test guard: when running under tests, prefer stdio: 'inherit' to avoid
|
|
763
|
-
// assertions depending on captured stdio; ignore GETDOTENV_STDIO/capture.
|
|
764
|
-
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
765
|
-
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
766
|
-
const forceExit = process.env.GETDOTENV_FORCE_EXIT === '1';
|
|
767
|
-
const capture = !underTests &&
|
|
768
|
-
(process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
769
|
-
Boolean(merged.capture));
|
|
770
|
-
dbg('run:start', { capture, shell: merged.shell });
|
|
771
|
-
// Prefer explicit env injection: include resolved dotenv map to avoid leaking
|
|
772
|
-
// parent process.env secrets when exclusions are set.
|
|
773
|
-
const ctx = cli.getCtx();
|
|
774
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
775
|
-
// Diagnostics: --trace [keys...]
|
|
776
|
-
const traceOpt = merged.trace;
|
|
777
|
-
if (traceOpt) {
|
|
778
|
-
const parentKeys = Object.keys(process.env);
|
|
779
|
-
const dotenvKeys = Object.keys(dotenv);
|
|
780
|
-
const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
|
|
781
|
-
const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
|
|
782
|
-
const childEnvPreview = {
|
|
783
|
-
...process.env,
|
|
784
|
-
...dotenv,
|
|
785
|
-
};
|
|
786
|
-
for (const k of keys) {
|
|
787
|
-
const parent = process.env[k];
|
|
788
|
-
const dot = dotenv[k];
|
|
789
|
-
const final = childEnvPreview[k];
|
|
790
|
-
const origin = dot !== undefined
|
|
791
|
-
? 'dotenv'
|
|
792
|
-
: parent !== undefined
|
|
793
|
-
? 'parent'
|
|
794
|
-
: 'unset';
|
|
795
|
-
// Build redact options and triple bag without undefined-valued fields
|
|
796
|
-
const redOpts = {};
|
|
797
|
-
const redFlag = merged.redact;
|
|
798
|
-
const redPatterns = merged
|
|
799
|
-
.redactPatterns;
|
|
800
|
-
if (redFlag)
|
|
801
|
-
redOpts.redact = true;
|
|
802
|
-
if (redFlag && Array.isArray(redPatterns))
|
|
803
|
-
redOpts.redactPatterns = redPatterns;
|
|
804
|
-
const tripleBag = {};
|
|
805
|
-
if (parent !== undefined)
|
|
806
|
-
tripleBag.parent = parent;
|
|
807
|
-
if (dot !== undefined)
|
|
808
|
-
tripleBag.dotenv = dot;
|
|
809
|
-
if (final !== undefined)
|
|
810
|
-
tripleBag.final = final;
|
|
811
|
-
const triple = redactTriple(k, tripleBag, redOpts);
|
|
812
|
-
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
813
|
-
const entOpts = {};
|
|
814
|
-
const warnEntropy = merged.warnEntropy;
|
|
815
|
-
const entropyThreshold = merged
|
|
816
|
-
.entropyThreshold;
|
|
817
|
-
const entropyMinLength = merged
|
|
818
|
-
.entropyMinLength;
|
|
819
|
-
const entropyWhitelist = merged
|
|
820
|
-
.entropyWhitelist;
|
|
821
|
-
if (typeof warnEntropy === 'boolean')
|
|
822
|
-
entOpts.warnEntropy = warnEntropy;
|
|
823
|
-
if (typeof entropyThreshold === 'number')
|
|
824
|
-
entOpts.entropyThreshold = entropyThreshold;
|
|
825
|
-
if (typeof entropyMinLength === 'number')
|
|
826
|
-
entOpts.entropyMinLength = entropyMinLength;
|
|
827
|
-
if (Array.isArray(entropyWhitelist))
|
|
828
|
-
entOpts.entropyWhitelist = entropyWhitelist;
|
|
829
|
-
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
let exitCode = Number.NaN;
|
|
833
|
-
try {
|
|
834
|
-
// Resolve shell and preserve argv for Node -e snippets under shell-off.
|
|
835
|
-
const shellSetting = resolveShell(merged.scripts, input, merged.shell);
|
|
836
|
-
let commandArg = resolved;
|
|
837
|
-
/** * Special-case: when shell is OFF and no script alias remap occurred
|
|
838
|
-
* (resolved === input), treat a Node eval payload as an argv array to
|
|
839
|
-
* avoid lossy re-tokenization of the code string.
|
|
840
|
-
*
|
|
841
|
-
* Examples handled:
|
|
842
|
-
* "node -e \"console.log(JSON.stringify(...))\""
|
|
843
|
-
* "node --eval 'console.log(...)'"
|
|
844
|
-
*
|
|
845
|
-
* We peel exactly one pair of symmetric outer quotes from the code
|
|
846
|
-
* argument when present; inner quotes remain untouched.
|
|
847
|
-
*/
|
|
848
|
-
if (shellSetting === false && resolved === input) {
|
|
849
|
-
// Helper: strip one symmetric outer quote layer
|
|
850
|
-
const stripOne = (s) => {
|
|
851
|
-
if (s.length < 2)
|
|
852
|
-
return s;
|
|
853
|
-
const a = s.charAt(0);
|
|
854
|
-
const b = s.charAt(s.length - 1);
|
|
855
|
-
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
856
|
-
return symmetric ? s.slice(1, -1) : s;
|
|
857
|
-
};
|
|
858
|
-
// Normalize whole input once for robust matching
|
|
859
|
-
const normalized = stripOne(input.trim());
|
|
860
|
-
// First try a lightweight regex on the normalized string
|
|
861
|
-
const m = /^\s*node\s+(--eval|-e)\s+([\s\S]+)$/i.exec(normalized);
|
|
862
|
-
if (m && typeof m[1] === 'string' && typeof m[2] === 'string') {
|
|
863
|
-
const evalFlag = m[1];
|
|
864
|
-
let codeArg = m[2].trim();
|
|
865
|
-
codeArg = stripOne(codeArg);
|
|
866
|
-
const flag = evalFlag.startsWith('--') ? '--eval' : '-e';
|
|
867
|
-
commandArg = ['node', flag, codeArg];
|
|
868
|
-
}
|
|
869
|
-
else {
|
|
870
|
-
// Fallback: tokenize and detect node -e/--eval form
|
|
871
|
-
const parts = tokenize(input);
|
|
872
|
-
if (parts.length >= 3) {
|
|
873
|
-
// Narrow under noUncheckedIndexedAccess
|
|
874
|
-
const p0 = parts[0];
|
|
875
|
-
const p1 = parts[1];
|
|
876
|
-
if (p0?.toLowerCase() === 'node' &&
|
|
877
|
-
(p1 === '-e' || p1 === '--eval')) {
|
|
878
|
-
commandArg = parts;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
exitCode = await runCommand(commandArg, shellSetting, {
|
|
884
|
-
env: buildSpawnEnv(process.env, {
|
|
885
|
-
...dotenv,
|
|
886
|
-
getDotenvCliOptions: JSON.stringify(envBag),
|
|
887
|
-
}),
|
|
888
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
889
|
-
});
|
|
890
|
-
dbg('run:done', { exitCode });
|
|
891
|
-
}
|
|
892
|
-
catch (err) {
|
|
893
|
-
const code = typeof err.exitCode === 'number'
|
|
894
|
-
? err.exitCode
|
|
895
|
-
: 1;
|
|
896
|
-
dbg('run:error', { exitCode: code, error: String(err) });
|
|
897
|
-
if (!underTests) {
|
|
898
|
-
dbg('process.exit (error path)', { exitCode: code });
|
|
899
|
-
process.exit(code);
|
|
900
|
-
}
|
|
901
|
-
else {
|
|
902
|
-
dbg('process.exit suppressed for tests (error path)', {
|
|
903
|
-
exitCode: code,
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
if (!Number.isNaN(exitCode)) {
|
|
909
|
-
dbg('process.exit', { exitCode });
|
|
910
|
-
process.exit(exitCode);
|
|
911
|
-
}
|
|
912
|
-
// Fallback: Some environments may not surface a numeric exitCode even on success.
|
|
913
|
-
// Always terminate alias-only invocations outside tests to avoid hanging the process,
|
|
914
|
-
// regardless of capture/GETDOTENV_STDIO. Under tests, suppress to keep the runner alive.
|
|
915
|
-
if (!underTests) {
|
|
916
|
-
dbg('process.exit (fallback: non-numeric exitCode)', { exitCode: 0 });
|
|
917
|
-
process.exit(0);
|
|
918
|
-
}
|
|
919
|
-
else {
|
|
920
|
-
dbg('process.exit (fallback suppressed for tests: non-numeric exitCode)', { exitCode: 0 });
|
|
921
|
-
}
|
|
922
|
-
// Optional last-resort guard: force an exit on the next tick when enabled.
|
|
923
|
-
// Intended for diagnosing environments where the process appears to linger
|
|
924
|
-
// despite reaching the success/error handlers above. Disabled under tests.
|
|
925
|
-
if (forceExit) {
|
|
926
|
-
try {
|
|
927
|
-
if (process.env.GETDOTENV_DEBUG_VERBOSE) {
|
|
928
|
-
const getHandles = process._getActiveHandles;
|
|
929
|
-
const handles = typeof getHandles === 'function' ? getHandles() : [];
|
|
930
|
-
dbg('active handles before forced exit', {
|
|
931
|
-
count: Array.isArray(handles) ? handles.length : undefined,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
catch {
|
|
936
|
-
// best-effort only
|
|
937
|
-
}
|
|
938
|
-
const code = Number.isNaN(exitCode) ? 0 : exitCode;
|
|
939
|
-
dbg('process.exit (forced)', { exitCode: code });
|
|
940
|
-
setImmediate(() => process.exit(code));
|
|
941
|
-
}
|
|
1075
|
+
const aliasState = { handled: false };
|
|
1076
|
+
const maybeRun = async (thisCommand) => {
|
|
1077
|
+
await maybeRunAlias(cli, thisCommand, aliasKey, aliasState);
|
|
942
1078
|
};
|
|
943
|
-
// Execute alias-only invocations whether the root handles the action // itself (preAction) or Commander routes to a default subcommand (preSubcommand).
|
|
944
1079
|
cli.hook('preAction', async (thisCommand, _actionCommand) => {
|
|
945
|
-
await
|
|
1080
|
+
await maybeRun(thisCommand);
|
|
946
1081
|
});
|
|
947
1082
|
cli.hook('preSubcommand', async (thisCommand) => {
|
|
948
|
-
await
|
|
1083
|
+
await maybeRun(thisCommand);
|
|
949
1084
|
});
|
|
950
1085
|
};
|
|
951
1086
|
|
|
@@ -967,10 +1102,10 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
967
1102
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
968
1103
|
};
|
|
969
1104
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
970
|
-
|
|
971
|
-
|
|
1105
|
+
// Create as a GetDotenvCli child so helpInformation includes a trailing blank line.
|
|
1106
|
+
const cmd = cli
|
|
1107
|
+
.createCommand('cmd')
|
|
972
1108
|
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
973
|
-
.configureHelp({ showGlobalOptions: true })
|
|
974
1109
|
.enablePositionalOptions()
|
|
975
1110
|
.passThroughOptions()
|
|
976
1111
|
.argument('[command...]')
|
|
@@ -1041,8 +1176,7 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
1041
1176
|
: 'unset';
|
|
1042
1177
|
// Apply presentation-time redaction (if enabled)
|
|
1043
1178
|
const redFlag = merged.redact;
|
|
1044
|
-
const redPatterns = merged
|
|
1045
|
-
.redactPatterns;
|
|
1179
|
+
const redPatterns = merged.redactPatterns;
|
|
1046
1180
|
const redOpts = {};
|
|
1047
1181
|
if (redFlag)
|
|
1048
1182
|
redOpts.redact = true;
|