@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.cjs
DELETED
|
@@ -1,1113 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var commander = require('commander');
|
|
4
|
-
var execa = require('execa');
|
|
5
|
-
require('fs-extra');
|
|
6
|
-
require('package-directory');
|
|
7
|
-
require('path');
|
|
8
|
-
|
|
9
|
-
// Minimal tokenizer for shell-off execution:
|
|
10
|
-
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
11
|
-
const tokenize = (command) => {
|
|
12
|
-
const out = [];
|
|
13
|
-
let cur = '';
|
|
14
|
-
let quote = null;
|
|
15
|
-
for (let i = 0; i < command.length; i++) {
|
|
16
|
-
const c = command.charAt(i);
|
|
17
|
-
if (quote) {
|
|
18
|
-
if (c === quote) {
|
|
19
|
-
// Support doubled quotes inside a quoted segment (Windows/PowerShell style):
|
|
20
|
-
// "" -> " and '' -> '
|
|
21
|
-
const next = command.charAt(i + 1);
|
|
22
|
-
if (next === quote) {
|
|
23
|
-
cur += quote;
|
|
24
|
-
i += 1; // skip the second quote
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
// end of quoted segment
|
|
28
|
-
quote = null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
cur += c;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
if (c === '"' || c === "'") {
|
|
37
|
-
quote = c;
|
|
38
|
-
}
|
|
39
|
-
else if (/\s/.test(c)) {
|
|
40
|
-
if (cur) {
|
|
41
|
-
out.push(cur);
|
|
42
|
-
cur = '';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
cur += c;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (cur)
|
|
51
|
-
out.push(cur);
|
|
52
|
-
return out;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const dbg$1 = (...args) => {
|
|
56
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
57
|
-
// Use stderr to avoid interfering with stdout assertions
|
|
58
|
-
console.error('[getdotenv:run]', ...args);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
// Strip repeated symmetric outer quotes (single or double) until stable.
|
|
62
|
-
// This is safe for argv arrays passed to execa (no quoting needed) and avoids
|
|
63
|
-
// passing quote characters through to Node (e.g., for `node -e "<code>"`).
|
|
64
|
-
// Handles stacked quotes from shells like PowerShell: """code""" -> code.
|
|
65
|
-
const stripOuterQuotes = (s) => {
|
|
66
|
-
let out = s;
|
|
67
|
-
// Repeatedly trim only when the entire string is wrapped in matching quotes.
|
|
68
|
-
// Stop as soon as the ends are asymmetric or no quotes remain.
|
|
69
|
-
while (out.length >= 2) {
|
|
70
|
-
const a = out.charAt(0);
|
|
71
|
-
const b = out.charAt(out.length - 1);
|
|
72
|
-
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
73
|
-
if (!symmetric)
|
|
74
|
-
break;
|
|
75
|
-
out = out.slice(1, -1);
|
|
76
|
-
}
|
|
77
|
-
return out;
|
|
78
|
-
};
|
|
79
|
-
// Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
|
|
80
|
-
// expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
|
|
81
|
-
const sanitizeEnv = (env) => {
|
|
82
|
-
if (!env)
|
|
83
|
-
return undefined;
|
|
84
|
-
const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
|
|
85
|
-
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
86
|
-
};
|
|
87
|
-
const runCommand = async (command, shell, opts) => {
|
|
88
|
-
if (shell === false) {
|
|
89
|
-
let file;
|
|
90
|
-
let args = [];
|
|
91
|
-
if (Array.isArray(command)) {
|
|
92
|
-
file = command[0];
|
|
93
|
-
args = command.slice(1).map(stripOuterQuotes);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
const tokens = tokenize(command);
|
|
97
|
-
file = tokens[0];
|
|
98
|
-
args = tokens.slice(1);
|
|
99
|
-
}
|
|
100
|
-
if (!file)
|
|
101
|
-
return 0;
|
|
102
|
-
dbg$1('exec (plain)', { file, args, stdio: opts.stdio });
|
|
103
|
-
// Build options without injecting undefined properties (exactOptionalPropertyTypes).
|
|
104
|
-
const envSan = sanitizeEnv(opts.env);
|
|
105
|
-
const plainOpts = {};
|
|
106
|
-
if (opts.cwd !== undefined)
|
|
107
|
-
plainOpts.cwd = opts.cwd;
|
|
108
|
-
if (envSan !== undefined)
|
|
109
|
-
plainOpts.env = envSan;
|
|
110
|
-
if (opts.stdio !== undefined)
|
|
111
|
-
plainOpts.stdio = opts.stdio;
|
|
112
|
-
const result = await execa.execa(file, args, plainOpts);
|
|
113
|
-
if (opts.stdio === 'pipe' && result.stdout) {
|
|
114
|
-
process.stdout.write(result.stdout + (result.stdout.endsWith('\n') ? '' : '\n'));
|
|
115
|
-
}
|
|
116
|
-
const exit = result?.exitCode;
|
|
117
|
-
dbg$1('exit (plain)', { exitCode: exit });
|
|
118
|
-
return typeof exit === 'number' ? exit : Number.NaN;
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
const commandStr = Array.isArray(command) ? command.join(' ') : command;
|
|
122
|
-
dbg$1('exec (shell)', {
|
|
123
|
-
shell: typeof shell === 'string' ? shell : 'custom',
|
|
124
|
-
stdio: opts.stdio,
|
|
125
|
-
command: commandStr,
|
|
126
|
-
});
|
|
127
|
-
const envSan = sanitizeEnv(opts.env);
|
|
128
|
-
const shellOpts = { shell };
|
|
129
|
-
if (opts.cwd !== undefined)
|
|
130
|
-
shellOpts.cwd = opts.cwd;
|
|
131
|
-
if (envSan !== undefined)
|
|
132
|
-
shellOpts.env = envSan;
|
|
133
|
-
if (opts.stdio !== undefined)
|
|
134
|
-
shellOpts.stdio = opts.stdio;
|
|
135
|
-
const result = await execa.execaCommand(commandStr, shellOpts);
|
|
136
|
-
const out = result?.stdout;
|
|
137
|
-
if (opts.stdio === 'pipe' && out) {
|
|
138
|
-
process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
|
|
139
|
-
}
|
|
140
|
-
const exit = result?.exitCode;
|
|
141
|
-
dbg$1('exit (shell)', { exitCode: exit });
|
|
142
|
-
return typeof exit === 'number' ? exit : Number.NaN;
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
|
|
147
|
-
/** Build a sanitized env for child processes from base + overlay. */
|
|
148
|
-
const buildSpawnEnv = (base, overlay) => {
|
|
149
|
-
const raw = {
|
|
150
|
-
...(base ?? {}),
|
|
151
|
-
...(overlay ?? {}),
|
|
152
|
-
};
|
|
153
|
-
// Drop undefined first
|
|
154
|
-
const entries = Object.entries(dropUndefined(raw));
|
|
155
|
-
if (process.platform === 'win32') {
|
|
156
|
-
// Windows: keys are case-insensitive; collapse duplicates
|
|
157
|
-
const byLower = new Map();
|
|
158
|
-
for (const [k, v] of entries) {
|
|
159
|
-
byLower.set(k.toLowerCase(), [k, v]); // last wins; preserve latest casing
|
|
160
|
-
}
|
|
161
|
-
const out = {};
|
|
162
|
-
for (const [, [k, v]] of byLower)
|
|
163
|
-
out[k] = v;
|
|
164
|
-
// HOME fallback from USERPROFILE (common expectation)
|
|
165
|
-
if (!Object.prototype.hasOwnProperty.call(out, 'HOME')) {
|
|
166
|
-
const up = out['USERPROFILE'];
|
|
167
|
-
if (typeof up === 'string' && up.length > 0)
|
|
168
|
-
out['HOME'] = up;
|
|
169
|
-
}
|
|
170
|
-
// Normalize TMP/TEMP coherence (pick any present; reflect to both)
|
|
171
|
-
const tmp = out['TMP'] ?? out['TEMP'];
|
|
172
|
-
if (typeof tmp === 'string' && tmp.length > 0) {
|
|
173
|
-
out['TMP'] = tmp;
|
|
174
|
-
out['TEMP'] = tmp;
|
|
175
|
-
}
|
|
176
|
-
return out;
|
|
177
|
-
}
|
|
178
|
-
// POSIX: keep keys as-is
|
|
179
|
-
const out = Object.fromEntries(entries);
|
|
180
|
-
// Ensure TMPDIR exists when any temp key is present (best-effort)
|
|
181
|
-
const tmpdir = out['TMPDIR'] ?? out['TMP'] ?? out['TEMP'];
|
|
182
|
-
if (typeof tmpdir === 'string' && tmpdir.length > 0) {
|
|
183
|
-
out['TMPDIR'] = tmpdir;
|
|
184
|
-
}
|
|
185
|
-
return out;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
/** src/cliHost/definePlugin.ts
|
|
189
|
-
* Plugin contracts for the GetDotenv CLI host.
|
|
190
|
-
*
|
|
191
|
-
* This module exposes a structural public interface for the host that plugins
|
|
192
|
-
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
193
|
-
* nominal class identity issues (private fields) in downstream consumers.
|
|
194
|
-
*/
|
|
195
|
-
/**
|
|
196
|
-
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
197
|
-
*
|
|
198
|
-
* @example
|
|
199
|
-
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
200
|
-
* .use(childA)
|
|
201
|
-
* .use(childB);
|
|
202
|
-
*/
|
|
203
|
-
const definePlugin = (spec) => {
|
|
204
|
-
const { children = [], ...rest } = spec;
|
|
205
|
-
const plugin = {
|
|
206
|
-
...rest,
|
|
207
|
-
children: [...children],
|
|
208
|
-
use(child) {
|
|
209
|
-
this.children.push(child);
|
|
210
|
-
return this;
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
return plugin;
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
/** src/diagnostics/entropy.ts
|
|
217
|
-
* Entropy diagnostics (presentation-only).
|
|
218
|
-
* - Gated by min length and printable ASCII.
|
|
219
|
-
* - Warn once per key per run when bits/char \>= threshold.
|
|
220
|
-
* - Supports whitelist patterns to suppress known-noise keys.
|
|
221
|
-
*/
|
|
222
|
-
const warned = new Set();
|
|
223
|
-
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
224
|
-
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
225
|
-
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
226
|
-
const shannonBitsPerChar = (s) => {
|
|
227
|
-
const freq = new Map();
|
|
228
|
-
for (const ch of s)
|
|
229
|
-
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
230
|
-
const n = s.length;
|
|
231
|
-
let h = 0;
|
|
232
|
-
for (const c of freq.values()) {
|
|
233
|
-
const p = c / n;
|
|
234
|
-
h -= p * Math.log2(p);
|
|
235
|
-
}
|
|
236
|
-
return h;
|
|
237
|
-
};
|
|
238
|
-
/**
|
|
239
|
-
* Maybe emit a one-line entropy warning for a key.
|
|
240
|
-
* Caller supplies an `emit(line)` function; the helper ensures once-per-key.
|
|
241
|
-
*/
|
|
242
|
-
const maybeWarnEntropy = (key, value, origin, opts, emit) => {
|
|
243
|
-
if (!opts || opts.warnEntropy === false)
|
|
244
|
-
return;
|
|
245
|
-
if (warned.has(key))
|
|
246
|
-
return;
|
|
247
|
-
const v = value ?? '';
|
|
248
|
-
const minLen = Math.max(0, opts.entropyMinLength ?? 16);
|
|
249
|
-
const threshold = opts.entropyThreshold ?? 3.8;
|
|
250
|
-
if (v.length < minLen)
|
|
251
|
-
return;
|
|
252
|
-
if (!isPrintableAscii(v))
|
|
253
|
-
return;
|
|
254
|
-
const wl = compile$1(opts.entropyWhitelist);
|
|
255
|
-
if (whitelisted(key, wl))
|
|
256
|
-
return;
|
|
257
|
-
const bpc = shannonBitsPerChar(v);
|
|
258
|
-
if (bpc >= threshold) {
|
|
259
|
-
warned.add(key);
|
|
260
|
-
emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const DEFAULT_PATTERNS = [
|
|
265
|
-
'\\bsecret\\b',
|
|
266
|
-
'\\btoken\\b',
|
|
267
|
-
'\\bpass(word)?\\b',
|
|
268
|
-
'\\bapi[_-]?key\\b',
|
|
269
|
-
'\\bkey\\b',
|
|
270
|
-
];
|
|
271
|
-
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
272
|
-
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
273
|
-
const MASK = '[redacted]';
|
|
274
|
-
/**
|
|
275
|
-
* Utility to redact three related displayed values (parent/dotenv/final)
|
|
276
|
-
* consistently for trace lines.
|
|
277
|
-
*/
|
|
278
|
-
const redactTriple = (key, triple, opts) => {
|
|
279
|
-
if (!opts?.redact)
|
|
280
|
-
return triple;
|
|
281
|
-
const regs = compile(opts.redactPatterns);
|
|
282
|
-
const maskIf = (v) => (v && shouldRedactKey(key, regs) ? MASK : v);
|
|
283
|
-
const out = {};
|
|
284
|
-
const p = maskIf(triple.parent);
|
|
285
|
-
const d = maskIf(triple.dotenv);
|
|
286
|
-
const f = maskIf(triple.final);
|
|
287
|
-
if (p !== undefined)
|
|
288
|
-
out.parent = p;
|
|
289
|
-
if (d !== undefined)
|
|
290
|
-
out.dotenv = d;
|
|
291
|
-
if (f !== undefined)
|
|
292
|
-
out.final = f;
|
|
293
|
-
return out;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Batch services (neutral): resolve command and shell settings.
|
|
298
|
-
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
299
|
-
*/
|
|
300
|
-
/**
|
|
301
|
-
* Resolve a command string from the {@link Scripts} table.
|
|
302
|
-
* A script may be expressed as a string or an object with a `cmd` property.
|
|
303
|
-
*
|
|
304
|
-
* @param scripts - Optional scripts table.
|
|
305
|
-
* @param command - User-provided command name or string.
|
|
306
|
-
* @returns Resolved command string (falls back to the provided command).
|
|
307
|
-
*/
|
|
308
|
-
const resolveCommand = (scripts, command) => scripts && typeof scripts[command] === 'object'
|
|
309
|
-
? scripts[command].cmd
|
|
310
|
-
: (scripts?.[command] ?? command);
|
|
311
|
-
/**
|
|
312
|
-
* Resolve the shell setting for a given command:
|
|
313
|
-
* - If the script entry is an object, prefer its `shell` override.
|
|
314
|
-
* - Otherwise use the provided `shell` (string | boolean).
|
|
315
|
-
*
|
|
316
|
-
* @param scripts - Optional scripts table.
|
|
317
|
-
* @param command - User-provided command name or string.
|
|
318
|
-
* @param shell - Global shell preference (string | boolean).
|
|
319
|
-
*/
|
|
320
|
-
const resolveShell = (scripts, command, shell) => scripts && typeof scripts[command] === 'object'
|
|
321
|
-
? (scripts[command].shell ?? false)
|
|
322
|
-
: (shell ?? false);
|
|
323
|
-
|
|
324
|
-
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
325
|
-
const baseRootOptionDefaults = {
|
|
326
|
-
dotenvToken: '.env',
|
|
327
|
-
loadProcess: true,
|
|
328
|
-
logger: console,
|
|
329
|
-
// Diagnostics defaults
|
|
330
|
-
warnEntropy: true,
|
|
331
|
-
entropyThreshold: 3.8,
|
|
332
|
-
entropyMinLength: 16,
|
|
333
|
-
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
334
|
-
paths: './',
|
|
335
|
-
pathsDelimiter: ' ',
|
|
336
|
-
privateToken: 'local',
|
|
337
|
-
scripts: {
|
|
338
|
-
'git-status': {
|
|
339
|
-
cmd: 'git branch --show-current && git status -s -u',
|
|
340
|
-
shell: true,
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
shell: true,
|
|
344
|
-
vars: '',
|
|
345
|
-
varsAssignor: '=',
|
|
346
|
-
varsDelimiter: ' ',
|
|
347
|
-
// tri-state flags default to unset unless explicitly provided
|
|
348
|
-
// (debug/log/exclude* resolved via flag utils)
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
/** @internal */
|
|
352
|
-
const isPlainObject = (value) => value !== null &&
|
|
353
|
-
typeof value === 'object' &&
|
|
354
|
-
Object.getPrototypeOf(value) === Object.prototype;
|
|
355
|
-
const mergeInto = (target, source) => {
|
|
356
|
-
for (const [key, sVal] of Object.entries(source)) {
|
|
357
|
-
if (sVal === undefined)
|
|
358
|
-
continue; // do not overwrite with undefined
|
|
359
|
-
const tVal = target[key];
|
|
360
|
-
if (isPlainObject(tVal) && isPlainObject(sVal)) {
|
|
361
|
-
target[key] = mergeInto({ ...tVal }, sVal);
|
|
362
|
-
}
|
|
363
|
-
else if (isPlainObject(sVal)) {
|
|
364
|
-
target[key] = mergeInto({}, sVal);
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
target[key] = sVal;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return target;
|
|
371
|
-
};
|
|
372
|
-
/**
|
|
373
|
-
* Perform a deep defaults-style merge across plain objects. *
|
|
374
|
-
* - Only merges plain objects (prototype === Object.prototype).
|
|
375
|
-
* - Arrays and non-objects are replaced, not merged.
|
|
376
|
-
* - `undefined` values are ignored and do not overwrite prior values.
|
|
377
|
-
*
|
|
378
|
-
* @typeParam T - The resulting shape after merging all layers.
|
|
379
|
-
* @param layers - Zero or more partial layers in ascending precedence order.
|
|
380
|
-
* @returns The merged object typed as {@link T}.
|
|
381
|
-
*
|
|
382
|
-
* @example
|
|
383
|
-
* defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
|
|
384
|
-
* =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
|
|
385
|
-
*/
|
|
386
|
-
const defaultsDeep = (...layers) => {
|
|
387
|
-
const result = layers
|
|
388
|
-
.filter(Boolean)
|
|
389
|
-
.reduce((acc, layer) => mergeInto(acc, layer), {});
|
|
390
|
-
return result;
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
395
|
-
* - If the user explicitly enabled the flag, return true.
|
|
396
|
-
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
397
|
-
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
398
|
-
*
|
|
399
|
-
* @param exclude - The "on" flag value as parsed by Commander.
|
|
400
|
-
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
401
|
-
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
402
|
-
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
403
|
-
*
|
|
404
|
-
* @example
|
|
405
|
-
* ```ts
|
|
406
|
-
* resolveExclusion(undefined, undefined, true); // => true
|
|
407
|
-
* ```
|
|
408
|
-
*/
|
|
409
|
-
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
410
|
-
/**
|
|
411
|
-
* Resolve an optional flag with "--exclude-all" overrides.
|
|
412
|
-
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
413
|
-
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
414
|
-
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
415
|
-
*
|
|
416
|
-
* @param exclude - Individual include/exclude flag.
|
|
417
|
-
* @param excludeOff - Individual "...-off" flag.
|
|
418
|
-
* @param defaultValue - Default for the individual flag.
|
|
419
|
-
* @param excludeAll - Global "exclude-all" flag.
|
|
420
|
-
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
421
|
-
*
|
|
422
|
-
* @example
|
|
423
|
-
* resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
|
|
424
|
-
*/
|
|
425
|
-
const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
|
|
426
|
-
// Order of precedence:
|
|
427
|
-
// 1) Individual explicit "on" wins outright.
|
|
428
|
-
// 2) Individual explicit "off" wins over any global.
|
|
429
|
-
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
430
|
-
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
431
|
-
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
432
|
-
(() => {
|
|
433
|
-
// Individual "on"
|
|
434
|
-
if (exclude === true)
|
|
435
|
-
return true;
|
|
436
|
-
// Individual "off"
|
|
437
|
-
if (excludeOff === true)
|
|
438
|
-
return undefined;
|
|
439
|
-
// Global "exclude-all" ON (unless explicitly turned off)
|
|
440
|
-
if (excludeAll === true)
|
|
441
|
-
return true;
|
|
442
|
-
// Global "exclude-all-off" (unless explicitly enabled)
|
|
443
|
-
if (excludeAllOff === true)
|
|
444
|
-
return undefined;
|
|
445
|
-
// Default
|
|
446
|
-
return defaultValue ? true : undefined;
|
|
447
|
-
})();
|
|
448
|
-
/**
|
|
449
|
-
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
450
|
-
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
451
|
-
*
|
|
452
|
-
* @typeParam T - Target object type.
|
|
453
|
-
* @param obj - The object to write to.
|
|
454
|
-
* @param key - The optional boolean property key of {@link T}.
|
|
455
|
-
* @param value - The value to set or `undefined` to unset.
|
|
456
|
-
*
|
|
457
|
-
* @remarks
|
|
458
|
-
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
459
|
-
*/
|
|
460
|
-
const setOptionalFlag = (obj, key, value) => {
|
|
461
|
-
const target = obj;
|
|
462
|
-
const k = key;
|
|
463
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
464
|
-
if (value === undefined)
|
|
465
|
-
delete target[k];
|
|
466
|
-
else
|
|
467
|
-
target[k] = value;
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Merge and normalize raw Commander options (current + parent + defaults)
|
|
472
|
-
* into a GetDotenvCliOptions-like object. Types are intentionally wide to
|
|
473
|
-
* avoid cross-layer coupling; callers may cast as needed.
|
|
474
|
-
*/
|
|
475
|
-
const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
476
|
-
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
477
|
-
? JSON.parse(parentJson)
|
|
478
|
-
: undefined;
|
|
479
|
-
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
480
|
-
const current = { ...rest };
|
|
481
|
-
if (typeof scripts === 'string') {
|
|
482
|
-
try {
|
|
483
|
-
current.scripts = JSON.parse(scripts);
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
// ignore parse errors; leave scripts undefined
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
490
|
-
const d = defaults;
|
|
491
|
-
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
492
|
-
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
493
|
-
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
494
|
-
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
495
|
-
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
496
|
-
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
497
|
-
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
498
|
-
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
499
|
-
// warnEntropy (tri-state)
|
|
500
|
-
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
501
|
-
// Normalize shell for predictability: explicit default shell per OS.
|
|
502
|
-
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
503
|
-
let resolvedShell = merged.shell;
|
|
504
|
-
if (shellOff)
|
|
505
|
-
resolvedShell = false;
|
|
506
|
-
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
507
|
-
resolvedShell = defaultShell;
|
|
508
|
-
}
|
|
509
|
-
else if (typeof resolvedShell !== 'string' &&
|
|
510
|
-
typeof defaults.shell === 'string') {
|
|
511
|
-
resolvedShell = defaults.shell;
|
|
512
|
-
}
|
|
513
|
-
merged.shell = resolvedShell;
|
|
514
|
-
const cmd = typeof command === 'string' ? command : undefined;
|
|
515
|
-
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Dotenv expansion utilities.
|
|
520
|
-
*
|
|
521
|
-
* This module implements recursive expansion of environment-variable
|
|
522
|
-
* references in strings and records. It supports both whitespace and
|
|
523
|
-
* bracket syntaxes with optional defaults:
|
|
524
|
-
*
|
|
525
|
-
* - Whitespace: `$VAR[:default]`
|
|
526
|
-
* - Bracketed: `${VAR[:default]}`
|
|
527
|
-
*
|
|
528
|
-
* Escaped dollar signs (`\$`) are preserved.
|
|
529
|
-
* Unknown variables resolve to empty string unless a default is provided.
|
|
530
|
-
*/
|
|
531
|
-
/**
|
|
532
|
-
* Like String.prototype.search but returns the last index.
|
|
533
|
-
* @internal
|
|
534
|
-
*/
|
|
535
|
-
const searchLast = (str, rgx) => {
|
|
536
|
-
const matches = Array.from(str.matchAll(rgx));
|
|
537
|
-
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
538
|
-
};
|
|
539
|
-
const replaceMatch = (value, match, ref) => {
|
|
540
|
-
/**
|
|
541
|
-
* @internal
|
|
542
|
-
*/
|
|
543
|
-
const group = match[0];
|
|
544
|
-
const key = match[1];
|
|
545
|
-
const defaultValue = match[2];
|
|
546
|
-
if (!key)
|
|
547
|
-
return value;
|
|
548
|
-
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
549
|
-
return interpolate(replacement, ref);
|
|
550
|
-
};
|
|
551
|
-
const interpolate = (value = '', ref = {}) => {
|
|
552
|
-
/**
|
|
553
|
-
* @internal
|
|
554
|
-
*/
|
|
555
|
-
// if value is falsy, return it as is
|
|
556
|
-
if (!value)
|
|
557
|
-
return value;
|
|
558
|
-
// get position of last unescaped dollar sign
|
|
559
|
-
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
560
|
-
// return value if none found
|
|
561
|
-
if (lastUnescapedDollarSignIndex === -1)
|
|
562
|
-
return value;
|
|
563
|
-
// evaluate the value tail
|
|
564
|
-
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
565
|
-
// find whitespace pattern: $KEY:DEFAULT
|
|
566
|
-
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
567
|
-
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
568
|
-
if (whitespaceMatch != null)
|
|
569
|
-
return replaceMatch(value, whitespaceMatch, ref);
|
|
570
|
-
else {
|
|
571
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
572
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
573
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
574
|
-
if (bracketMatch != null)
|
|
575
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
576
|
-
}
|
|
577
|
-
return value;
|
|
578
|
-
};
|
|
579
|
-
/**
|
|
580
|
-
* Recursively expands environment variables in a string. Variables may be
|
|
581
|
-
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
582
|
-
* Unknown variables will expand to an empty string.
|
|
583
|
-
*
|
|
584
|
-
* @param value - The string to expand.
|
|
585
|
-
* @param ref - The reference object to use for variable expansion.
|
|
586
|
-
* @returns The expanded string.
|
|
587
|
-
*
|
|
588
|
-
* @example
|
|
589
|
-
* ```ts
|
|
590
|
-
* process.env.FOO = 'bar';
|
|
591
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
592
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
593
|
-
* ```
|
|
594
|
-
*
|
|
595
|
-
* @remarks
|
|
596
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
597
|
-
* references, those will also be expanded until a stable value is reached.
|
|
598
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
599
|
-
*/
|
|
600
|
-
const dotenvExpand = (value, ref = process.env) => {
|
|
601
|
-
const result = interpolate(value, ref);
|
|
602
|
-
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
603
|
-
};
|
|
604
|
-
/**
|
|
605
|
-
* Recursively expands environment variables in a string using `process.env` as
|
|
606
|
-
* the expansion reference. Variables may be presented with optional default as
|
|
607
|
-
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
608
|
-
* empty string.
|
|
609
|
-
*
|
|
610
|
-
* @param value - The string to expand.
|
|
611
|
-
* @returns The expanded string.
|
|
612
|
-
*
|
|
613
|
-
* @example
|
|
614
|
-
* ```ts
|
|
615
|
-
* process.env.FOO = 'bar';
|
|
616
|
-
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
617
|
-
* ```
|
|
618
|
-
*/
|
|
619
|
-
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
620
|
-
|
|
621
|
-
// src/GetDotenvOptions.ts
|
|
622
|
-
/**
|
|
623
|
-
* Converts programmatic CLI options to `getDotenv` options. *
|
|
624
|
-
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
625
|
-
*
|
|
626
|
-
* @returns `getDotenv` options.
|
|
627
|
-
*/
|
|
628
|
-
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
629
|
-
/**
|
|
630
|
-
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
631
|
-
*
|
|
632
|
-
* - 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`
|
|
633
|
-
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
634
|
-
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
635
|
-
*
|
|
636
|
-
* @remarks
|
|
637
|
-
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
638
|
-
*/
|
|
639
|
-
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
640
|
-
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
641
|
-
const restObj = { ...rest };
|
|
642
|
-
delete restObj.debug;
|
|
643
|
-
delete restObj.scripts;
|
|
644
|
-
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
645
|
-
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
646
|
-
let parsedVars;
|
|
647
|
-
if (typeof vars === 'string') {
|
|
648
|
-
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
649
|
-
? RegExp(varsAssignorPattern)
|
|
650
|
-
: (varsAssignor ?? '=')));
|
|
651
|
-
parsedVars = Object.fromEntries(kvPairs);
|
|
652
|
-
}
|
|
653
|
-
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
654
|
-
// Keep only string or undefined values to match ProcessEnv.
|
|
655
|
-
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
656
|
-
parsedVars = Object.fromEntries(entries);
|
|
657
|
-
}
|
|
658
|
-
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
659
|
-
// expectations and the compat test assertions.
|
|
660
|
-
if (parsedVars) {
|
|
661
|
-
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
662
|
-
}
|
|
663
|
-
// Tolerate paths as either a delimited string or string[]
|
|
664
|
-
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
665
|
-
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
666
|
-
const pathsAny = paths;
|
|
667
|
-
const pathsOut = Array.isArray(pathsAny)
|
|
668
|
-
? pathsAny.filter((p) => typeof p === 'string')
|
|
669
|
-
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
670
|
-
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
671
|
-
return {
|
|
672
|
-
...restObj,
|
|
673
|
-
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
674
|
-
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
675
|
-
};
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
const dbg = (...args) => {
|
|
679
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
680
|
-
// Use stderr to avoid interfering with stdout assertions
|
|
681
|
-
console.error('[getdotenv:alias]', ...args);
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
const attachParentAlias = (cli, options, _cmd) => {
|
|
685
|
-
const aliasSpec = typeof options.optionAlias === 'string'
|
|
686
|
-
? { flags: options.optionAlias, description: undefined, expand: true }
|
|
687
|
-
: options.optionAlias;
|
|
688
|
-
if (!aliasSpec)
|
|
689
|
-
return;
|
|
690
|
-
const deriveKey = (flags) => {
|
|
691
|
-
dbg('install alias option', flags);
|
|
692
|
-
const long = flags.split(/[ ,|]+/).find((f) => f.startsWith('--')) ?? '--cmd';
|
|
693
|
-
const name = long.replace(/^--/, '');
|
|
694
|
-
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
695
|
-
};
|
|
696
|
-
const aliasKey = deriveKey(aliasSpec.flags);
|
|
697
|
-
// Expose the option on the parent.
|
|
698
|
-
const desc = aliasSpec.description ??
|
|
699
|
-
'alias of cmd subcommand; provide command tokens (variadic)';
|
|
700
|
-
cli.option(aliasSpec.flags, desc);
|
|
701
|
-
// Tag the just-added parent option for grouped help rendering.
|
|
702
|
-
try {
|
|
703
|
-
const optsArr = cli.options;
|
|
704
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
705
|
-
const last = optsArr[optsArr.length - 1];
|
|
706
|
-
last.__group = 'plugin:cmd';
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
710
|
-
/* noop */
|
|
711
|
-
}
|
|
712
|
-
// Shared alias executor for either preAction or preSubcommand hooks.
|
|
713
|
-
// Ensure we only execute once even if both hooks fire in a single parse.
|
|
714
|
-
let aliasHandled = false;
|
|
715
|
-
const maybeRunAlias = async (thisCommand) => {
|
|
716
|
-
dbg('alias:maybe:start');
|
|
717
|
-
const raw = thisCommand.rawArgs ?? [];
|
|
718
|
-
const childNames = thisCommand.commands.flatMap((c) => [
|
|
719
|
-
c.name(),
|
|
720
|
-
...c.aliases(),
|
|
721
|
-
]);
|
|
722
|
-
const hasSub = childNames.some((n) => raw.includes(n));
|
|
723
|
-
// Read alias value from parent opts.
|
|
724
|
-
const o = thisCommand.opts();
|
|
725
|
-
const val = o[aliasKey];
|
|
726
|
-
const provided = typeof val === 'string'
|
|
727
|
-
? val.length > 0
|
|
728
|
-
: Array.isArray(val)
|
|
729
|
-
? val.length > 0
|
|
730
|
-
: false;
|
|
731
|
-
if (!provided || hasSub) {
|
|
732
|
-
dbg('alias:maybe:skip', { provided, hasSub });
|
|
733
|
-
return; // not an alias-only invocation
|
|
734
|
-
}
|
|
735
|
-
if (aliasHandled) {
|
|
736
|
-
dbg('alias:maybe:already-handled');
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
aliasHandled = true;
|
|
740
|
-
dbg('alias-only invocation detected');
|
|
741
|
-
// Merge CLI options and resolve dotenv context.
|
|
742
|
-
const { merged } = resolveCliOptions(o,
|
|
743
|
-
// cast through unknown to avoid readonly -> mutable incompatibilities
|
|
744
|
-
baseRootOptionDefaults, process.env.getDotenvCliOptions);
|
|
745
|
-
const logger = merged.logger ?? console;
|
|
746
|
-
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
747
|
-
await cli.resolveAndLoad(serviceOptions);
|
|
748
|
-
// Normalize alias value.
|
|
749
|
-
const joined = typeof val === 'string'
|
|
750
|
-
? val
|
|
751
|
-
: Array.isArray(val)
|
|
752
|
-
? val.map(String).join(' ')
|
|
753
|
-
: '';
|
|
754
|
-
const input = aliasSpec.expand === false
|
|
755
|
-
? joined
|
|
756
|
-
: (dotenvExpandFromProcessEnv(joined) ?? joined);
|
|
757
|
-
dbg('resolved input', { input });
|
|
758
|
-
const resolved = resolveCommand(merged.scripts, input);
|
|
759
|
-
const lg = logger;
|
|
760
|
-
if (merged.debug) {
|
|
761
|
-
(lg.debug ?? lg.log)('\n*** command ***\n', `'${resolved}'`);
|
|
762
|
-
}
|
|
763
|
-
const { logger: _omit, ...envBag } = merged;
|
|
764
|
-
// Test guard: when running under tests, prefer stdio: 'inherit' to avoid
|
|
765
|
-
// assertions depending on captured stdio; ignore GETDOTENV_STDIO/capture.
|
|
766
|
-
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
767
|
-
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
768
|
-
const forceExit = process.env.GETDOTENV_FORCE_EXIT === '1';
|
|
769
|
-
const capture = !underTests &&
|
|
770
|
-
(process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
771
|
-
Boolean(merged.capture));
|
|
772
|
-
dbg('run:start', { capture, shell: merged.shell });
|
|
773
|
-
// Prefer explicit env injection: include resolved dotenv map to avoid leaking
|
|
774
|
-
// parent process.env secrets when exclusions are set.
|
|
775
|
-
const ctx = cli.getCtx();
|
|
776
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
777
|
-
// Diagnostics: --trace [keys...]
|
|
778
|
-
const traceOpt = merged.trace;
|
|
779
|
-
if (traceOpt) {
|
|
780
|
-
const parentKeys = Object.keys(process.env);
|
|
781
|
-
const dotenvKeys = Object.keys(dotenv);
|
|
782
|
-
const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
|
|
783
|
-
const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
|
|
784
|
-
const childEnvPreview = {
|
|
785
|
-
...process.env,
|
|
786
|
-
...dotenv,
|
|
787
|
-
};
|
|
788
|
-
for (const k of keys) {
|
|
789
|
-
const parent = process.env[k];
|
|
790
|
-
const dot = dotenv[k];
|
|
791
|
-
const final = childEnvPreview[k];
|
|
792
|
-
const origin = dot !== undefined
|
|
793
|
-
? 'dotenv'
|
|
794
|
-
: parent !== undefined
|
|
795
|
-
? 'parent'
|
|
796
|
-
: 'unset';
|
|
797
|
-
// Build redact options and triple bag without undefined-valued fields
|
|
798
|
-
const redOpts = {};
|
|
799
|
-
const redFlag = merged.redact;
|
|
800
|
-
const redPatterns = merged
|
|
801
|
-
.redactPatterns;
|
|
802
|
-
if (redFlag)
|
|
803
|
-
redOpts.redact = true;
|
|
804
|
-
if (redFlag && Array.isArray(redPatterns))
|
|
805
|
-
redOpts.redactPatterns = redPatterns;
|
|
806
|
-
const tripleBag = {};
|
|
807
|
-
if (parent !== undefined)
|
|
808
|
-
tripleBag.parent = parent;
|
|
809
|
-
if (dot !== undefined)
|
|
810
|
-
tripleBag.dotenv = dot;
|
|
811
|
-
if (final !== undefined)
|
|
812
|
-
tripleBag.final = final;
|
|
813
|
-
const triple = redactTriple(k, tripleBag, redOpts);
|
|
814
|
-
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
815
|
-
const entOpts = {};
|
|
816
|
-
const warnEntropy = merged.warnEntropy;
|
|
817
|
-
const entropyThreshold = merged
|
|
818
|
-
.entropyThreshold;
|
|
819
|
-
const entropyMinLength = merged
|
|
820
|
-
.entropyMinLength;
|
|
821
|
-
const entropyWhitelist = merged
|
|
822
|
-
.entropyWhitelist;
|
|
823
|
-
if (typeof warnEntropy === 'boolean')
|
|
824
|
-
entOpts.warnEntropy = warnEntropy;
|
|
825
|
-
if (typeof entropyThreshold === 'number')
|
|
826
|
-
entOpts.entropyThreshold = entropyThreshold;
|
|
827
|
-
if (typeof entropyMinLength === 'number')
|
|
828
|
-
entOpts.entropyMinLength = entropyMinLength;
|
|
829
|
-
if (Array.isArray(entropyWhitelist))
|
|
830
|
-
entOpts.entropyWhitelist = entropyWhitelist;
|
|
831
|
-
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
let exitCode = Number.NaN;
|
|
835
|
-
try {
|
|
836
|
-
// Resolve shell and preserve argv for Node -e snippets under shell-off.
|
|
837
|
-
const shellSetting = resolveShell(merged.scripts, input, merged.shell);
|
|
838
|
-
let commandArg = resolved;
|
|
839
|
-
/** * Special-case: when shell is OFF and no script alias remap occurred
|
|
840
|
-
* (resolved === input), treat a Node eval payload as an argv array to
|
|
841
|
-
* avoid lossy re-tokenization of the code string.
|
|
842
|
-
*
|
|
843
|
-
* Examples handled:
|
|
844
|
-
* "node -e \"console.log(JSON.stringify(...))\""
|
|
845
|
-
* "node --eval 'console.log(...)'"
|
|
846
|
-
*
|
|
847
|
-
* We peel exactly one pair of symmetric outer quotes from the code
|
|
848
|
-
* argument when present; inner quotes remain untouched.
|
|
849
|
-
*/
|
|
850
|
-
if (shellSetting === false && resolved === input) {
|
|
851
|
-
// Helper: strip one symmetric outer quote layer
|
|
852
|
-
const stripOne = (s) => {
|
|
853
|
-
if (s.length < 2)
|
|
854
|
-
return s;
|
|
855
|
-
const a = s.charAt(0);
|
|
856
|
-
const b = s.charAt(s.length - 1);
|
|
857
|
-
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
858
|
-
return symmetric ? s.slice(1, -1) : s;
|
|
859
|
-
};
|
|
860
|
-
// Normalize whole input once for robust matching
|
|
861
|
-
const normalized = stripOne(input.trim());
|
|
862
|
-
// First try a lightweight regex on the normalized string
|
|
863
|
-
const m = /^\s*node\s+(--eval|-e)\s+([\s\S]+)$/i.exec(normalized);
|
|
864
|
-
if (m && typeof m[1] === 'string' && typeof m[2] === 'string') {
|
|
865
|
-
const evalFlag = m[1];
|
|
866
|
-
let codeArg = m[2].trim();
|
|
867
|
-
codeArg = stripOne(codeArg);
|
|
868
|
-
const flag = evalFlag.startsWith('--') ? '--eval' : '-e';
|
|
869
|
-
commandArg = ['node', flag, codeArg];
|
|
870
|
-
}
|
|
871
|
-
else {
|
|
872
|
-
// Fallback: tokenize and detect node -e/--eval form
|
|
873
|
-
const parts = tokenize(input);
|
|
874
|
-
if (parts.length >= 3) {
|
|
875
|
-
// Narrow under noUncheckedIndexedAccess
|
|
876
|
-
const p0 = parts[0];
|
|
877
|
-
const p1 = parts[1];
|
|
878
|
-
if (p0?.toLowerCase() === 'node' &&
|
|
879
|
-
(p1 === '-e' || p1 === '--eval')) {
|
|
880
|
-
commandArg = parts;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
exitCode = await runCommand(commandArg, shellSetting, {
|
|
886
|
-
env: buildSpawnEnv(process.env, {
|
|
887
|
-
...dotenv,
|
|
888
|
-
getDotenvCliOptions: JSON.stringify(envBag),
|
|
889
|
-
}),
|
|
890
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
891
|
-
});
|
|
892
|
-
dbg('run:done', { exitCode });
|
|
893
|
-
}
|
|
894
|
-
catch (err) {
|
|
895
|
-
const code = typeof err.exitCode === 'number'
|
|
896
|
-
? err.exitCode
|
|
897
|
-
: 1;
|
|
898
|
-
dbg('run:error', { exitCode: code, error: String(err) });
|
|
899
|
-
if (!underTests) {
|
|
900
|
-
dbg('process.exit (error path)', { exitCode: code });
|
|
901
|
-
process.exit(code);
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
dbg('process.exit suppressed for tests (error path)', {
|
|
905
|
-
exitCode: code,
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
if (!Number.isNaN(exitCode)) {
|
|
911
|
-
dbg('process.exit', { exitCode });
|
|
912
|
-
process.exit(exitCode);
|
|
913
|
-
}
|
|
914
|
-
// Fallback: Some environments may not surface a numeric exitCode even on success.
|
|
915
|
-
// Always terminate alias-only invocations outside tests to avoid hanging the process,
|
|
916
|
-
// regardless of capture/GETDOTENV_STDIO. Under tests, suppress to keep the runner alive.
|
|
917
|
-
if (!underTests) {
|
|
918
|
-
dbg('process.exit (fallback: non-numeric exitCode)', { exitCode: 0 });
|
|
919
|
-
process.exit(0);
|
|
920
|
-
}
|
|
921
|
-
else {
|
|
922
|
-
dbg('process.exit (fallback suppressed for tests: non-numeric exitCode)', { exitCode: 0 });
|
|
923
|
-
}
|
|
924
|
-
// Optional last-resort guard: force an exit on the next tick when enabled.
|
|
925
|
-
// Intended for diagnosing environments where the process appears to linger
|
|
926
|
-
// despite reaching the success/error handlers above. Disabled under tests.
|
|
927
|
-
if (forceExit) {
|
|
928
|
-
try {
|
|
929
|
-
if (process.env.GETDOTENV_DEBUG_VERBOSE) {
|
|
930
|
-
const getHandles = process._getActiveHandles;
|
|
931
|
-
const handles = typeof getHandles === 'function' ? getHandles() : [];
|
|
932
|
-
dbg('active handles before forced exit', {
|
|
933
|
-
count: Array.isArray(handles) ? handles.length : undefined,
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
catch {
|
|
938
|
-
// best-effort only
|
|
939
|
-
}
|
|
940
|
-
const code = Number.isNaN(exitCode) ? 0 : exitCode;
|
|
941
|
-
dbg('process.exit (forced)', { exitCode: code });
|
|
942
|
-
setImmediate(() => process.exit(code));
|
|
943
|
-
}
|
|
944
|
-
};
|
|
945
|
-
// Execute alias-only invocations whether the root handles the action // itself (preAction) or Commander routes to a default subcommand (preSubcommand).
|
|
946
|
-
cli.hook('preAction', async (thisCommand, _actionCommand) => {
|
|
947
|
-
await maybeRunAlias(thisCommand);
|
|
948
|
-
});
|
|
949
|
-
cli.hook('preSubcommand', async (thisCommand) => {
|
|
950
|
-
await maybeRunAlias(thisCommand);
|
|
951
|
-
});
|
|
952
|
-
};
|
|
953
|
-
|
|
954
|
-
/**+ Cmd plugin: executes a command using the current getdotenv CLI context.
|
|
955
|
-
*
|
|
956
|
-
* - Joins positional args into a single command string.
|
|
957
|
-
* - Resolves scripts and shell settings using shared helpers.
|
|
958
|
-
* - Forwards merged CLI options to subprocesses via
|
|
959
|
-
* process.env.getDotenvCliOptions for nested CLI behavior. */
|
|
960
|
-
const cmdPlugin = (options = {}) => definePlugin({
|
|
961
|
-
id: 'cmd',
|
|
962
|
-
setup(cli) {
|
|
963
|
-
const aliasSpec = typeof options.optionAlias === 'string'
|
|
964
|
-
? { flags: options.optionAlias}
|
|
965
|
-
: options.optionAlias;
|
|
966
|
-
const deriveKey = (flags) => {
|
|
967
|
-
const long = flags.split(/[ ,|]+/).find((f) => f.startsWith('--')) ?? '--cmd';
|
|
968
|
-
const name = long.replace(/^--/, '');
|
|
969
|
-
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
970
|
-
};
|
|
971
|
-
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
972
|
-
const cmd = new commander.Command()
|
|
973
|
-
.name('cmd')
|
|
974
|
-
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
975
|
-
.configureHelp({ showGlobalOptions: true })
|
|
976
|
-
.enablePositionalOptions()
|
|
977
|
-
.passThroughOptions()
|
|
978
|
-
.argument('[command...]')
|
|
979
|
-
.action(async (commandParts, _opts, thisCommand) => {
|
|
980
|
-
// Commander passes positional tokens as the first action argument
|
|
981
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
982
|
-
// No-op when invoked as the default command with no args.
|
|
983
|
-
if (args.length === 0)
|
|
984
|
-
return;
|
|
985
|
-
const parent = thisCommand.parent;
|
|
986
|
-
if (!parent)
|
|
987
|
-
throw new Error('parent command not found'); // Conflict detection: if an alias option is present on parent, do not
|
|
988
|
-
// also accept positional cmd args.
|
|
989
|
-
if (aliasKey) {
|
|
990
|
-
const pv = parent.opts();
|
|
991
|
-
const ov = pv[aliasKey];
|
|
992
|
-
if (ov !== undefined) {
|
|
993
|
-
const merged = parent.getDotenvCliOptions ?? {};
|
|
994
|
-
const logger = merged.logger ?? console;
|
|
995
|
-
const lr = logger;
|
|
996
|
-
const emit = lr.error ?? lr.log;
|
|
997
|
-
emit(`--${aliasKey} option conflicts with cmd subcommand.`);
|
|
998
|
-
process.exit(0);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
// Merged CLI options are persisted by the shipped CLI preSubcommand hook.
|
|
1002
|
-
const merged = parent.getDotenvCliOptions ?? {};
|
|
1003
|
-
const logger = merged.logger ?? console;
|
|
1004
|
-
// Join positional args into the command string.
|
|
1005
|
-
const input = args.map(String).join(' ');
|
|
1006
|
-
// Resolve command and shell using shared helpers.
|
|
1007
|
-
const scripts = merged.scripts;
|
|
1008
|
-
const shell = merged.shell;
|
|
1009
|
-
const resolved = resolveCommand(scripts, input);
|
|
1010
|
-
if (merged.debug) {
|
|
1011
|
-
const lg = logger;
|
|
1012
|
-
(lg.debug ?? lg.log)('\n*** command ***\n', `'${resolved}'`);
|
|
1013
|
-
}
|
|
1014
|
-
// Round-trip CLI options for nested getdotenv invocations.
|
|
1015
|
-
// Omit logger (functions are not serializable).
|
|
1016
|
-
const { logger: _omit, ...envBag } = merged;
|
|
1017
|
-
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
1018
|
-
Boolean(merged.capture);
|
|
1019
|
-
// Prefer explicit env injection using the resolved dotenv map.
|
|
1020
|
-
const ctx = cli.getCtx();
|
|
1021
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
1022
|
-
// Diagnostics: --trace [keys...] (space-delimited keys if provided; all keys when true)
|
|
1023
|
-
const traceOpt = merged.trace;
|
|
1024
|
-
if (traceOpt) {
|
|
1025
|
-
// Determine keys to trace: all keys (parent ∪ dotenv) or selected.
|
|
1026
|
-
const parentKeys = Object.keys(process.env);
|
|
1027
|
-
const dotenvKeys = Object.keys(dotenv);
|
|
1028
|
-
const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
|
|
1029
|
-
const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
|
|
1030
|
-
// Child env preview (as composed below; excluding getDotenvCliOptions)
|
|
1031
|
-
const childEnvPreview = {
|
|
1032
|
-
...process.env,
|
|
1033
|
-
...dotenv,
|
|
1034
|
-
};
|
|
1035
|
-
for (const k of keys) {
|
|
1036
|
-
const parent = process.env[k];
|
|
1037
|
-
const dot = dotenv[k];
|
|
1038
|
-
const final = childEnvPreview[k];
|
|
1039
|
-
const origin = dot !== undefined
|
|
1040
|
-
? 'dotenv'
|
|
1041
|
-
: parent !== undefined
|
|
1042
|
-
? 'parent'
|
|
1043
|
-
: 'unset';
|
|
1044
|
-
// Apply presentation-time redaction (if enabled)
|
|
1045
|
-
const redFlag = merged.redact;
|
|
1046
|
-
const redPatterns = merged
|
|
1047
|
-
.redactPatterns;
|
|
1048
|
-
const redOpts = {};
|
|
1049
|
-
if (redFlag)
|
|
1050
|
-
redOpts.redact = true;
|
|
1051
|
-
if (redFlag && Array.isArray(redPatterns))
|
|
1052
|
-
redOpts.redactPatterns = redPatterns;
|
|
1053
|
-
const tripleBag = {};
|
|
1054
|
-
if (parent !== undefined)
|
|
1055
|
-
tripleBag.parent = parent;
|
|
1056
|
-
if (dot !== undefined)
|
|
1057
|
-
tripleBag.dotenv = dot;
|
|
1058
|
-
if (final !== undefined)
|
|
1059
|
-
tripleBag.final = final;
|
|
1060
|
-
const triple = redactTriple(k, tripleBag, redOpts);
|
|
1061
|
-
// Emit concise diagnostic line to stderr.
|
|
1062
|
-
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
1063
|
-
// Optional entropy warning (once-per-key)
|
|
1064
|
-
const entOpts = {};
|
|
1065
|
-
const warnEntropy = merged
|
|
1066
|
-
.warnEntropy;
|
|
1067
|
-
const entropyThreshold = merged.entropyThreshold;
|
|
1068
|
-
const entropyMinLength = merged.entropyMinLength;
|
|
1069
|
-
const entropyWhitelist = merged.entropyWhitelist;
|
|
1070
|
-
if (typeof warnEntropy === 'boolean')
|
|
1071
|
-
entOpts.warnEntropy = warnEntropy;
|
|
1072
|
-
if (typeof entropyThreshold === 'number')
|
|
1073
|
-
entOpts.entropyThreshold = entropyThreshold;
|
|
1074
|
-
if (typeof entropyMinLength === 'number')
|
|
1075
|
-
entOpts.entropyMinLength = entropyMinLength;
|
|
1076
|
-
if (Array.isArray(entropyWhitelist))
|
|
1077
|
-
entOpts.entropyWhitelist = entropyWhitelist;
|
|
1078
|
-
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
const shellSetting = resolveShell(scripts, input, shell);
|
|
1082
|
-
/**
|
|
1083
|
-
* Preserve original argv array when:
|
|
1084
|
-
* - shell is OFF (plain execa), and
|
|
1085
|
-
* - no script alias remap occurred (resolved === input).
|
|
1086
|
-
*
|
|
1087
|
-
* This avoids lossy re-tokenization of code snippets such as:
|
|
1088
|
-
* node -e "console.log(process.env.APP_SECRET ?? '')"
|
|
1089
|
-
* where quotes may have been stripped by the parent shell and
|
|
1090
|
-
* spaces inside the code must remain a single argument.
|
|
1091
|
-
*/
|
|
1092
|
-
const commandArg = shellSetting === false && resolved === input
|
|
1093
|
-
? args.map(String)
|
|
1094
|
-
: resolved;
|
|
1095
|
-
await runCommand(commandArg, shellSetting, {
|
|
1096
|
-
env: buildSpawnEnv(process.env, {
|
|
1097
|
-
...dotenv,
|
|
1098
|
-
getDotenvCliOptions: JSON.stringify(envBag),
|
|
1099
|
-
}),
|
|
1100
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
1101
|
-
});
|
|
1102
|
-
});
|
|
1103
|
-
if (options.asDefault)
|
|
1104
|
-
cli.addCommand(cmd, { isDefault: true });
|
|
1105
|
-
else
|
|
1106
|
-
cli.addCommand(cmd);
|
|
1107
|
-
// Parent-attached option alias (optional).
|
|
1108
|
-
if (aliasSpec)
|
|
1109
|
-
attachParentAlias(cli, options);
|
|
1110
|
-
},
|
|
1111
|
-
});
|
|
1112
|
-
|
|
1113
|
-
exports.cmdPlugin = cmdPlugin;
|