@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.mjs
CHANGED
|
@@ -1,29 +1,47 @@
|
|
|
1
1
|
import { execa, execaCommand } from 'execa';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { packageDirectory } from 'package-directory';
|
|
6
|
+
import 'url';
|
|
7
|
+
import 'yaml';
|
|
8
|
+
import 'nanoid';
|
|
9
|
+
import 'dotenv';
|
|
10
|
+
import 'crypto';
|
|
3
11
|
import { Command } from 'commander';
|
|
4
12
|
import { globby } from 'globby';
|
|
5
|
-
import { packageDirectory } from 'package-directory';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import fs from 'fs-extra';
|
|
8
13
|
import { stdin, stdout } from 'node:process';
|
|
9
14
|
import { createInterface } from 'readline/promises';
|
|
10
15
|
|
|
11
16
|
// Minimal tokenizer for shell-off execution:
|
|
12
17
|
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
13
|
-
|
|
18
|
+
// Optionally preserve doubled quotes inside quoted segments:
|
|
19
|
+
// - default: "" => " (Windows/PowerShell style literal-quote escape)
|
|
20
|
+
// - preserveDoubledQuotes: true => "" stays "" (needed for Node -e payloads)
|
|
21
|
+
const tokenize = (command, opts) => {
|
|
14
22
|
const out = [];
|
|
15
23
|
let cur = '';
|
|
16
24
|
let quote = null;
|
|
25
|
+
const preserve = opts && opts.preserveDoubledQuotes === true ? true : false;
|
|
17
26
|
for (let i = 0; i < command.length; i++) {
|
|
18
27
|
const c = command.charAt(i);
|
|
19
28
|
if (quote) {
|
|
20
29
|
if (c === quote) {
|
|
21
|
-
// Support doubled quotes inside a quoted segment
|
|
22
|
-
// "" -> " and '' -> '
|
|
30
|
+
// Support doubled quotes inside a quoted segment:
|
|
31
|
+
// default: "" -> " and '' -> ' (Windows/PowerShell style)
|
|
32
|
+
// preserve: keep as "" to allow empty string literals in Node -e payloads
|
|
23
33
|
const next = command.charAt(i + 1);
|
|
24
34
|
if (next === quote) {
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
if (preserve) {
|
|
36
|
+
// Keep "" as-is; append both and continue within the quoted segment.
|
|
37
|
+
cur += quote + quote;
|
|
38
|
+
i += 1; // skip the second quote char (we already appended both)
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Collapse to a single literal quote
|
|
42
|
+
cur += quote;
|
|
43
|
+
i += 1; // skip the second quote
|
|
44
|
+
}
|
|
27
45
|
}
|
|
28
46
|
else {
|
|
29
47
|
// end of quoted segment
|
|
@@ -97,62 +115,55 @@ const sanitizeEnv = (env) => {
|
|
|
97
115
|
const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
|
|
98
116
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
99
117
|
};
|
|
100
|
-
|
|
101
|
-
* Execute a command and capture stdout/stderr (buffered).
|
|
102
|
-
* - Preserves plain vs shell behavior and argv/string normalization.
|
|
103
|
-
* - Never re-emits stdout/stderr to parent; returns captured buffers.
|
|
104
|
-
* - Supports optional timeout (ms).
|
|
105
|
-
*/
|
|
106
|
-
const runCommandResult = async (command, shell, opts = {}) => {
|
|
118
|
+
async function runCommandResult(command, shell, opts = {}) {
|
|
107
119
|
const envSan = sanitizeEnv(opts.env);
|
|
108
120
|
{
|
|
109
121
|
let file;
|
|
110
122
|
let args = [];
|
|
111
|
-
if (
|
|
112
|
-
file = command[0];
|
|
113
|
-
args = command.slice(1).map(stripOuterQuotes);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
123
|
+
if (typeof command === 'string') {
|
|
116
124
|
const tokens = tokenize(command);
|
|
117
125
|
file = tokens[0];
|
|
118
126
|
args = tokens.slice(1);
|
|
119
127
|
}
|
|
128
|
+
else {
|
|
129
|
+
file = command[0];
|
|
130
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
131
|
+
}
|
|
120
132
|
if (!file)
|
|
121
133
|
return { exitCode: 0, stdout: '', stderr: '' };
|
|
122
134
|
dbg$1('exec:capture (plain)', { file, args });
|
|
123
135
|
try {
|
|
124
|
-
const
|
|
136
|
+
const ok = pickResult((await execa(file, args, {
|
|
125
137
|
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
126
138
|
...(envSan !== undefined ? { env: envSan } : {}),
|
|
127
139
|
stdio: 'pipe',
|
|
128
140
|
...(opts.timeoutMs !== undefined
|
|
129
141
|
? { timeout: opts.timeoutMs, killSignal: 'SIGKILL' }
|
|
130
142
|
: {}),
|
|
131
|
-
});
|
|
132
|
-
const ok = pickResult(result);
|
|
143
|
+
})));
|
|
133
144
|
dbg$1('exit:capture (plain)', { exitCode: ok.exitCode });
|
|
134
145
|
return ok;
|
|
135
146
|
}
|
|
136
|
-
catch (
|
|
137
|
-
const out = pickResult(
|
|
147
|
+
catch (e) {
|
|
148
|
+
const out = pickResult(e);
|
|
138
149
|
dbg$1('exit:capture:error (plain)', { exitCode: out.exitCode });
|
|
139
150
|
return out;
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
|
-
}
|
|
143
|
-
|
|
153
|
+
}
|
|
154
|
+
async function runCommand(command, shell, opts) {
|
|
144
155
|
if (shell === false) {
|
|
145
156
|
let file;
|
|
146
157
|
let args = [];
|
|
147
|
-
if (
|
|
148
|
-
file = command[0];
|
|
149
|
-
args = command.slice(1).map(stripOuterQuotes);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
158
|
+
if (typeof command === 'string') {
|
|
152
159
|
const tokens = tokenize(command);
|
|
153
160
|
file = tokens[0];
|
|
154
161
|
args = tokens.slice(1);
|
|
155
162
|
}
|
|
163
|
+
else {
|
|
164
|
+
file = command[0];
|
|
165
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
166
|
+
}
|
|
156
167
|
if (!file)
|
|
157
168
|
return 0;
|
|
158
169
|
dbg$1('exec (plain)', { file, args, stdio: opts.stdio });
|
|
@@ -165,16 +176,15 @@ const runCommand = async (command, shell, opts) => {
|
|
|
165
176
|
plainOpts.env = envSan;
|
|
166
177
|
if (opts.stdio !== undefined)
|
|
167
178
|
plainOpts.stdio = opts.stdio;
|
|
168
|
-
const
|
|
169
|
-
if (opts.stdio === 'pipe' &&
|
|
170
|
-
process.stdout.write(
|
|
179
|
+
const ok = pickResult((await execa(file, args, plainOpts)));
|
|
180
|
+
if (opts.stdio === 'pipe' && ok.stdout) {
|
|
181
|
+
process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
|
|
171
182
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return typeof exit === 'number' ? exit : Number.NaN;
|
|
183
|
+
dbg$1('exit (plain)', { exitCode: ok.exitCode });
|
|
184
|
+
return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
|
|
175
185
|
}
|
|
176
186
|
else {
|
|
177
|
-
const commandStr =
|
|
187
|
+
const commandStr = typeof command === 'string' ? command : command.join(' ');
|
|
178
188
|
dbg$1('exec (shell)', {
|
|
179
189
|
shell: typeof shell === 'string' ? shell : 'custom',
|
|
180
190
|
stdio: opts.stdio,
|
|
@@ -188,17 +198,29 @@ const runCommand = async (command, shell, opts) => {
|
|
|
188
198
|
shellOpts.env = envSan;
|
|
189
199
|
if (opts.stdio !== undefined)
|
|
190
200
|
shellOpts.stdio = opts.stdio;
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
|
|
201
|
+
const ok = pickResult((await execaCommand(commandStr, shellOpts)));
|
|
202
|
+
if (opts.stdio === 'pipe' && ok.stdout) {
|
|
203
|
+
process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
|
|
195
204
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return typeof exit === 'number' ? exit : Number.NaN;
|
|
205
|
+
dbg$1('exit (shell)', { exitCode: ok.exitCode });
|
|
206
|
+
return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
|
|
199
207
|
}
|
|
200
|
-
}
|
|
208
|
+
}
|
|
201
209
|
|
|
210
|
+
/** src/cliCore/spawnEnv.ts
|
|
211
|
+
* Build a sanitized environment bag for child processes.
|
|
212
|
+
*
|
|
213
|
+
* Requirements addressed:
|
|
214
|
+
* - Provide a single helper (buildSpawnEnv) to normalize/dedupe child env.
|
|
215
|
+
* - Drop undefined values (exactOptional semantics).
|
|
216
|
+
* - On Windows, dedupe keys case-insensitively and prefer the last value,
|
|
217
|
+
* preserving the latest key's casing. Ensure HOME fallback from USERPROFILE.
|
|
218
|
+
* Normalize TMP/TEMP consistency when either is present.
|
|
219
|
+
* - On POSIX, keep keys as-is; when a temp dir key is present (TMPDIR/TMP/TEMP),
|
|
220
|
+
* ensure TMPDIR exists for downstream consumers that expect it.
|
|
221
|
+
*
|
|
222
|
+
* Adapter responsibility: pure mapping; no business logic.
|
|
223
|
+
*/
|
|
202
224
|
const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
|
|
203
225
|
/** Build a sanitized env for child processes from base + overlay. */
|
|
204
226
|
const buildSpawnEnv = (base, overlay) => {
|
|
@@ -238,9 +260,407 @@ const buildSpawnEnv = (base, overlay) => {
|
|
|
238
260
|
if (typeof tmpdir === 'string' && tmpdir.length > 0) {
|
|
239
261
|
out['TMPDIR'] = tmpdir;
|
|
240
262
|
}
|
|
241
|
-
return out;
|
|
263
|
+
return out;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Zod schemas for configuration files discovered by the new loader.
|
|
268
|
+
*
|
|
269
|
+
* Notes:
|
|
270
|
+
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
271
|
+
* - RESOLVED: normalized shapes (paths always string[]).
|
|
272
|
+
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
273
|
+
*/
|
|
274
|
+
// String-only env value map
|
|
275
|
+
const stringMap = z.record(z.string(), z.string());
|
|
276
|
+
const envStringMap = z.record(z.string(), stringMap);
|
|
277
|
+
// Allow string[] or single string for "paths" in RAW; normalize later.
|
|
278
|
+
const rawPathsSchema = z.union([z.array(z.string()), z.string()]).optional();
|
|
279
|
+
const getDotenvConfigSchemaRaw = z.object({
|
|
280
|
+
dotenvToken: z.string().optional(),
|
|
281
|
+
privateToken: z.string().optional(),
|
|
282
|
+
paths: rawPathsSchema,
|
|
283
|
+
loadProcess: z.boolean().optional(),
|
|
284
|
+
log: z.boolean().optional(),
|
|
285
|
+
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
286
|
+
scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
287
|
+
requiredKeys: z.array(z.string()).optional(),
|
|
288
|
+
schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
289
|
+
vars: stringMap.optional(), // public, global
|
|
290
|
+
envVars: envStringMap.optional(), // public, per-env
|
|
291
|
+
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
292
|
+
dynamic: z.unknown().optional(),
|
|
293
|
+
// Per-plugin config bag; validated by plugins/host when used.
|
|
294
|
+
plugins: z.record(z.string(), z.unknown()).optional(),
|
|
295
|
+
});
|
|
296
|
+
// Normalize paths to string[]
|
|
297
|
+
const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
|
|
298
|
+
getDotenvConfigSchemaRaw.transform((raw) => ({
|
|
299
|
+
...raw,
|
|
300
|
+
paths: normalizePaths(raw.paths),
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Dotenv expansion utilities.
|
|
305
|
+
*
|
|
306
|
+
* This module implements recursive expansion of environment-variable
|
|
307
|
+
* references in strings and records. It supports both whitespace and
|
|
308
|
+
* bracket syntaxes with optional defaults:
|
|
309
|
+
*
|
|
310
|
+
* - Whitespace: `$VAR[:default]`
|
|
311
|
+
* - Bracketed: `${VAR[:default]}`
|
|
312
|
+
*
|
|
313
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
314
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
315
|
+
*/
|
|
316
|
+
/**
|
|
317
|
+
* Like String.prototype.search but returns the last index.
|
|
318
|
+
* @internal
|
|
319
|
+
*/
|
|
320
|
+
const searchLast = (str, rgx) => {
|
|
321
|
+
const matches = Array.from(str.matchAll(rgx));
|
|
322
|
+
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
323
|
+
};
|
|
324
|
+
const replaceMatch = (value, match, ref) => {
|
|
325
|
+
/**
|
|
326
|
+
* @internal
|
|
327
|
+
*/
|
|
328
|
+
const group = match[0];
|
|
329
|
+
const key = match[1];
|
|
330
|
+
const defaultValue = match[2];
|
|
331
|
+
if (!key)
|
|
332
|
+
return value;
|
|
333
|
+
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
334
|
+
return interpolate(replacement, ref);
|
|
335
|
+
};
|
|
336
|
+
const interpolate = (value = '', ref = {}) => {
|
|
337
|
+
/**
|
|
338
|
+
* @internal
|
|
339
|
+
*/
|
|
340
|
+
// if value is falsy, return it as is
|
|
341
|
+
if (!value)
|
|
342
|
+
return value;
|
|
343
|
+
// get position of last unescaped dollar sign
|
|
344
|
+
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
345
|
+
// return value if none found
|
|
346
|
+
if (lastUnescapedDollarSignIndex === -1)
|
|
347
|
+
return value;
|
|
348
|
+
// evaluate the value tail
|
|
349
|
+
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
350
|
+
// find whitespace pattern: $KEY:DEFAULT
|
|
351
|
+
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
352
|
+
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
353
|
+
if (whitespaceMatch != null)
|
|
354
|
+
return replaceMatch(value, whitespaceMatch, ref);
|
|
355
|
+
else {
|
|
356
|
+
// find bracket pattern: ${KEY:DEFAULT}
|
|
357
|
+
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
358
|
+
const bracketMatch = bracketPattern.exec(tail);
|
|
359
|
+
if (bracketMatch != null)
|
|
360
|
+
return replaceMatch(value, bracketMatch, ref);
|
|
361
|
+
}
|
|
362
|
+
return value;
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Recursively expands environment variables in a string. Variables may be
|
|
366
|
+
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
367
|
+
* Unknown variables will expand to an empty string.
|
|
368
|
+
*
|
|
369
|
+
* @param value - The string to expand.
|
|
370
|
+
* @param ref - The reference object to use for variable expansion.
|
|
371
|
+
* @returns The expanded string.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* ```ts
|
|
375
|
+
* process.env.FOO = 'bar';
|
|
376
|
+
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
377
|
+
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
378
|
+
* ```
|
|
379
|
+
*
|
|
380
|
+
* @remarks
|
|
381
|
+
* The expansion is recursive. If a referenced variable itself contains
|
|
382
|
+
* references, those will also be expanded until a stable value is reached.
|
|
383
|
+
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
384
|
+
*/
|
|
385
|
+
const dotenvExpand = (value, ref = process.env) => {
|
|
386
|
+
const result = interpolate(value, ref);
|
|
387
|
+
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
391
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
392
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
393
|
+
* empty string.
|
|
394
|
+
*
|
|
395
|
+
* @param value - The string to expand.
|
|
396
|
+
* @returns The expanded string.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```ts
|
|
400
|
+
* process.env.FOO = 'bar';
|
|
401
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
405
|
+
|
|
406
|
+
/** src/diagnostics/entropy.ts
|
|
407
|
+
* Entropy diagnostics (presentation-only).
|
|
408
|
+
* - Gated by min length and printable ASCII.
|
|
409
|
+
* - Warn once per key per run when bits/char \>= threshold.
|
|
410
|
+
* - Supports whitelist patterns to suppress known-noise keys.
|
|
411
|
+
*/
|
|
412
|
+
const warned = new Set();
|
|
413
|
+
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
414
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => (typeof p === 'string' ? new RegExp(p, 'i') : p));
|
|
415
|
+
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
416
|
+
const shannonBitsPerChar = (s) => {
|
|
417
|
+
const freq = new Map();
|
|
418
|
+
for (const ch of s)
|
|
419
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
420
|
+
const n = s.length;
|
|
421
|
+
let h = 0;
|
|
422
|
+
for (const c of freq.values()) {
|
|
423
|
+
const p = c / n;
|
|
424
|
+
h -= p * Math.log2(p);
|
|
425
|
+
}
|
|
426
|
+
return h;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Maybe emit a one-line entropy warning for a key.
|
|
430
|
+
* Caller supplies an `emit(line)` function; the helper ensures once-per-key.
|
|
431
|
+
*/
|
|
432
|
+
const maybeWarnEntropy = (key, value, origin, opts, emit) => {
|
|
433
|
+
if (!opts || opts.warnEntropy === false)
|
|
434
|
+
return;
|
|
435
|
+
if (warned.has(key))
|
|
436
|
+
return;
|
|
437
|
+
const v = value ?? '';
|
|
438
|
+
const minLen = Math.max(0, opts.entropyMinLength ?? 16);
|
|
439
|
+
const threshold = opts.entropyThreshold ?? 3.8;
|
|
440
|
+
if (v.length < minLen)
|
|
441
|
+
return;
|
|
442
|
+
if (!isPrintableAscii(v))
|
|
443
|
+
return;
|
|
444
|
+
const wl = compile$1(opts.entropyWhitelist);
|
|
445
|
+
if (whitelisted(key, wl))
|
|
446
|
+
return;
|
|
447
|
+
const bpc = shannonBitsPerChar(v);
|
|
448
|
+
if (bpc >= threshold) {
|
|
449
|
+
warned.add(key);
|
|
450
|
+
emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const DEFAULT_PATTERNS = [
|
|
455
|
+
'\\bsecret\\b',
|
|
456
|
+
'\\btoken\\b',
|
|
457
|
+
'\\bpass(word)?\\b',
|
|
458
|
+
'\\bapi[_-]?key\\b',
|
|
459
|
+
'\\bkey\\b',
|
|
460
|
+
];
|
|
461
|
+
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
|
|
462
|
+
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
463
|
+
const MASK = '[redacted]';
|
|
464
|
+
/**
|
|
465
|
+
* Utility to redact three related displayed values (parent/dotenv/final)
|
|
466
|
+
* consistently for trace lines.
|
|
467
|
+
*/
|
|
468
|
+
const redactTriple = (key, triple, opts) => {
|
|
469
|
+
if (!opts?.redact)
|
|
470
|
+
return triple;
|
|
471
|
+
const regs = compile(opts.redactPatterns);
|
|
472
|
+
const maskIf = (v) => (v && shouldRedactKey(key, regs) ? MASK : v);
|
|
473
|
+
const out = {};
|
|
474
|
+
const p = maskIf(triple.parent);
|
|
475
|
+
const d = maskIf(triple.dotenv);
|
|
476
|
+
const f = maskIf(triple.final);
|
|
477
|
+
if (p !== undefined)
|
|
478
|
+
out.parent = p;
|
|
479
|
+
if (d !== undefined)
|
|
480
|
+
out.dotenv = d;
|
|
481
|
+
if (f !== undefined)
|
|
482
|
+
out.final = f;
|
|
483
|
+
return out;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
487
|
+
const baseRootOptionDefaults = {
|
|
488
|
+
dotenvToken: '.env',
|
|
489
|
+
loadProcess: true,
|
|
490
|
+
logger: console,
|
|
491
|
+
// Diagnostics defaults
|
|
492
|
+
warnEntropy: true,
|
|
493
|
+
entropyThreshold: 3.8,
|
|
494
|
+
entropyMinLength: 16,
|
|
495
|
+
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
496
|
+
paths: './',
|
|
497
|
+
pathsDelimiter: ' ',
|
|
498
|
+
privateToken: 'local',
|
|
499
|
+
scripts: {
|
|
500
|
+
'git-status': {
|
|
501
|
+
cmd: 'git branch --show-current && git status -s -u',
|
|
502
|
+
shell: true,
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
shell: true,
|
|
506
|
+
vars: '',
|
|
507
|
+
varsAssignor: '=',
|
|
508
|
+
varsDelimiter: ' ',
|
|
509
|
+
// tri-state flags default to unset unless explicitly provided
|
|
510
|
+
// (debug/log/exclude* resolved via flag utils)
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
514
|
+
|
|
515
|
+
/** @internal */
|
|
516
|
+
const isPlainObject = (value) => value !== null &&
|
|
517
|
+
typeof value === 'object' &&
|
|
518
|
+
Object.getPrototypeOf(value) === Object.prototype;
|
|
519
|
+
const mergeInto = (target, source) => {
|
|
520
|
+
for (const [key, sVal] of Object.entries(source)) {
|
|
521
|
+
if (sVal === undefined)
|
|
522
|
+
continue; // do not overwrite with undefined
|
|
523
|
+
const tVal = target[key];
|
|
524
|
+
if (isPlainObject(tVal) && isPlainObject(sVal)) {
|
|
525
|
+
target[key] = mergeInto({ ...tVal }, sVal);
|
|
526
|
+
}
|
|
527
|
+
else if (isPlainObject(sVal)) {
|
|
528
|
+
target[key] = mergeInto({}, sVal);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
target[key] = sVal;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return target;
|
|
535
|
+
};
|
|
536
|
+
function defaultsDeep(...layers) {
|
|
537
|
+
const result = layers
|
|
538
|
+
.filter(Boolean)
|
|
539
|
+
.reduce((acc, layer) => mergeInto(acc, layer), {});
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** src/util/omitUndefined.ts
|
|
544
|
+
* Helpers to drop undefined-valued properties in a typed-friendly way.
|
|
545
|
+
*/
|
|
546
|
+
/**
|
|
547
|
+
* Omit keys whose runtime value is undefined from a shallow object.
|
|
548
|
+
* Returns a Partial with non-undefined value types preserved.
|
|
549
|
+
*/
|
|
550
|
+
/**
|
|
551
|
+
* Specialized helper for env-like maps: drop undefined and return string-only.
|
|
552
|
+
*/
|
|
553
|
+
function omitUndefinedRecord(obj) {
|
|
554
|
+
const out = {};
|
|
555
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
556
|
+
if (v !== undefined)
|
|
557
|
+
out[k] = v;
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/GetDotenvOptions.ts
|
|
563
|
+
/**
|
|
564
|
+
* Canonical programmatic options and helpers for get-dotenv.
|
|
565
|
+
*
|
|
566
|
+
* Requirements addressed:
|
|
567
|
+
* - GetDotenvOptions derives from the Zod schema output (single source of truth).
|
|
568
|
+
* - Removed deprecated/compat flags from the public shape (e.g., useConfigLoader).
|
|
569
|
+
* - Provide Vars-aware defineDynamic and a typed config builder defineGetDotenvConfig\<Vars, Env\>().
|
|
570
|
+
* - Preserve existing behavior for defaults resolution and compat converters.
|
|
571
|
+
*/
|
|
572
|
+
/**
|
|
573
|
+
* Converts programmatic CLI options to `getDotenv` options.
|
|
574
|
+
*
|
|
575
|
+
* Accepts "stringly" CLI inputs for vars/paths and normalizes them into
|
|
576
|
+
* the programmatic shape. Preserves exactOptionalPropertyTypes semantics by
|
|
577
|
+
* omitting keys when undefined.
|
|
578
|
+
*/
|
|
579
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern,
|
|
580
|
+
// drop CLI-only keys from the pass-through bag
|
|
581
|
+
debug: _debug, scripts: _scripts, ...rest }) => {
|
|
582
|
+
// Split helper for delimited strings or regex patterns
|
|
583
|
+
const splitBy = (value, delim, pattern) => {
|
|
584
|
+
if (!value)
|
|
585
|
+
return [];
|
|
586
|
+
if (pattern)
|
|
587
|
+
return value.split(RegExp(pattern));
|
|
588
|
+
if (typeof delim === 'string')
|
|
589
|
+
return value.split(delim);
|
|
590
|
+
return value.split(' ');
|
|
591
|
+
};
|
|
592
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
593
|
+
let parsedVars;
|
|
594
|
+
if (typeof vars === 'string') {
|
|
595
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern)
|
|
596
|
+
.map((v) => v.split(varsAssignorPattern
|
|
597
|
+
? RegExp(varsAssignorPattern)
|
|
598
|
+
: (varsAssignor ?? '=')))
|
|
599
|
+
.filter(([k]) => typeof k === 'string' && k.length > 0);
|
|
600
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
601
|
+
}
|
|
602
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
603
|
+
// Accept provided object map of string | undefined; drop undefined values
|
|
604
|
+
// in the normalization step below to produce a ProcessEnv-compatible bag.
|
|
605
|
+
parsedVars = Object.fromEntries(Object.entries(vars));
|
|
606
|
+
}
|
|
607
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
608
|
+
// expectations and the compat test assertions.
|
|
609
|
+
if (parsedVars) {
|
|
610
|
+
parsedVars = omitUndefinedRecord(parsedVars);
|
|
611
|
+
}
|
|
612
|
+
// Tolerate paths as either a delimited string or string[]
|
|
613
|
+
const pathsOut = Array.isArray(paths)
|
|
614
|
+
? paths.filter((p) => typeof p === 'string')
|
|
615
|
+
: splitBy(paths, pathsDelimiter, pathsDelimiterPattern);
|
|
616
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
617
|
+
return {
|
|
618
|
+
...rest,
|
|
619
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
620
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
621
|
+
};
|
|
242
622
|
};
|
|
243
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Zod schemas for programmatic GetDotenv options.
|
|
626
|
+
*
|
|
627
|
+
* Canonical source of truth for options shape. Public types are derived
|
|
628
|
+
* from these schemas (see consumers via z.output\<\>).
|
|
629
|
+
*/
|
|
630
|
+
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
631
|
+
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
632
|
+
// RAW: all fields optional — undefined means "inherit" from lower layers.
|
|
633
|
+
z.object({
|
|
634
|
+
defaultEnv: z.string().optional(),
|
|
635
|
+
dotenvToken: z.string().optional(),
|
|
636
|
+
dynamicPath: z.string().optional(),
|
|
637
|
+
// Dynamic map is intentionally wide for now; refine once sources are normalized.
|
|
638
|
+
dynamic: z.record(z.string(), z.unknown()).optional(),
|
|
639
|
+
env: z.string().optional(),
|
|
640
|
+
excludeDynamic: z.boolean().optional(),
|
|
641
|
+
excludeEnv: z.boolean().optional(),
|
|
642
|
+
excludeGlobal: z.boolean().optional(),
|
|
643
|
+
excludePrivate: z.boolean().optional(),
|
|
644
|
+
excludePublic: z.boolean().optional(),
|
|
645
|
+
loadProcess: z.boolean().optional(),
|
|
646
|
+
log: z.boolean().optional(),
|
|
647
|
+
logger: z.unknown().optional(),
|
|
648
|
+
outputPath: z.string().optional(),
|
|
649
|
+
paths: z.array(z.string()).optional(),
|
|
650
|
+
privateToken: z.string().optional(),
|
|
651
|
+
vars: processEnvSchema.optional(),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Instance-bound plugin config store.
|
|
656
|
+
* Host stores the validated/interpolated slice per plugin instance.
|
|
657
|
+
* The store is intentionally private to this module; definePlugin()
|
|
658
|
+
* provides a typed accessor that reads from this store for the calling
|
|
659
|
+
* plugin instance.
|
|
660
|
+
*/
|
|
661
|
+
const PLUGIN_CONFIG_STORE = new WeakMap();
|
|
662
|
+
const _getPluginConfigForInstance = (plugin) => PLUGIN_CONFIG_STORE.get(plugin);
|
|
663
|
+
|
|
244
664
|
/** src/cliHost/definePlugin.ts
|
|
245
665
|
* Plugin contracts for the GetDotenv CLI host.
|
|
246
666
|
*
|
|
@@ -248,26 +668,59 @@ const buildSpawnEnv = (base, overlay) => {
|
|
|
248
668
|
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
249
669
|
* nominal class identity issues (private fields) in downstream consumers.
|
|
250
670
|
*/
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
*
|
|
254
|
-
* @example
|
|
255
|
-
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
256
|
-
* .use(childA)
|
|
257
|
-
* .use(childB);
|
|
258
|
-
*/
|
|
259
|
-
const definePlugin = (spec) => {
|
|
671
|
+
/* eslint-disable tsdoc/syntax */
|
|
672
|
+
function definePlugin(spec) {
|
|
260
673
|
const { children = [], ...rest } = spec;
|
|
261
|
-
|
|
674
|
+
// Default to a strict empty-object schema so “no-config” plugins fail fast
|
|
675
|
+
// on unknown keys and provide a concrete {} at runtime.
|
|
676
|
+
const effectiveSchema = spec.configSchema ?? z.object({}).strict();
|
|
677
|
+
// Build base plugin first, then extend with instance-bound helpers.
|
|
678
|
+
const base = {
|
|
262
679
|
...rest,
|
|
680
|
+
// Always carry a schema (strict empty by default) to simplify host logic
|
|
681
|
+
// and improve inference/ergonomics for plugin authors.
|
|
682
|
+
configSchema: effectiveSchema,
|
|
263
683
|
children: [...children],
|
|
264
684
|
use(child) {
|
|
265
685
|
this.children.push(child);
|
|
266
686
|
return this;
|
|
267
687
|
},
|
|
268
688
|
};
|
|
269
|
-
|
|
270
|
-
|
|
689
|
+
// Attach instance-bound helpers on the returned plugin object.
|
|
690
|
+
const extended = base;
|
|
691
|
+
extended.readConfig = function (_cli) {
|
|
692
|
+
// Config is stored per-plugin-instance by the host (WeakMap in computeContext).
|
|
693
|
+
const value = _getPluginConfigForInstance(extended);
|
|
694
|
+
if (value === undefined) {
|
|
695
|
+
// Guard: host has not resolved config yet (incorrect lifecycle usage).
|
|
696
|
+
throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
|
|
697
|
+
}
|
|
698
|
+
return value;
|
|
699
|
+
};
|
|
700
|
+
// Plugin-bound dynamic option factory
|
|
701
|
+
extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
|
|
702
|
+
return cli.createDynamicOption(flags, (cfg) => {
|
|
703
|
+
// Prefer the validated slice stored per instance; fallback to help-bag
|
|
704
|
+
// (by-id) so top-level `-h` can render effective defaults before resolve.
|
|
705
|
+
const fromStore = _getPluginConfigForInstance(extended);
|
|
706
|
+
const id = extended.id;
|
|
707
|
+
let fromBag;
|
|
708
|
+
if (!fromStore && id) {
|
|
709
|
+
const maybe = cfg.plugins[id];
|
|
710
|
+
if (maybe && typeof maybe === 'object') {
|
|
711
|
+
fromBag = maybe;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// Always provide a concrete object to dynamic callbacks:
|
|
715
|
+
// - With a schema: computeContext stores the parsed object.
|
|
716
|
+
// - Without a schema: computeContext stores {}.
|
|
717
|
+
// - Help-time fallback: coalesce to {} when only a by-id bag exists.
|
|
718
|
+
const cfgVal = (fromStore ?? fromBag ?? {});
|
|
719
|
+
return desc(cfg, cfgVal);
|
|
720
|
+
}, parser, defaultValue);
|
|
721
|
+
};
|
|
722
|
+
return extended;
|
|
723
|
+
}
|
|
271
724
|
|
|
272
725
|
/**
|
|
273
726
|
* Batch services (neutral): resolve command and shell settings.
|
|
@@ -491,90 +944,79 @@ const AwsPluginConfigSchema = z.object({
|
|
|
491
944
|
regionKey: z.string().default('AWS_REGION').optional(),
|
|
492
945
|
strategy: z.enum(['cli-export', 'none']).default('cli-export').optional(),
|
|
493
946
|
loginOnDemand: z.boolean().default(false).optional(),
|
|
494
|
-
setEnv: z.boolean().default(true).optional(),
|
|
495
|
-
addCtx: z.boolean().default(true).optional(),
|
|
496
947
|
});
|
|
497
948
|
|
|
498
|
-
const awsPlugin = () =>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
...overlay,
|
|
570
|
-
};
|
|
571
|
-
// Resolve current context with overrides
|
|
572
|
-
const out = await resolveAwsContext({
|
|
573
|
-
dotenv: ctx?.dotenv ?? {},
|
|
574
|
-
cfg,
|
|
575
|
-
});
|
|
576
|
-
// Apply env/ctx mirrors per toggles
|
|
577
|
-
if (cfg.setEnv !== false) {
|
|
949
|
+
const awsPlugin = () => {
|
|
950
|
+
const plugin = definePlugin({
|
|
951
|
+
id: 'aws',
|
|
952
|
+
// Host validates this slice when the loader path is active.
|
|
953
|
+
configSchema: AwsPluginConfigSchema,
|
|
954
|
+
setup(cli) {
|
|
955
|
+
// Subcommand: aws
|
|
956
|
+
cli
|
|
957
|
+
.ns('aws')
|
|
958
|
+
.description('Establish an AWS session and optionally forward to the AWS CLI')
|
|
959
|
+
.enablePositionalOptions()
|
|
960
|
+
.passThroughOptions()
|
|
961
|
+
.allowUnknownOption(true)
|
|
962
|
+
// Boolean toggles with dynamic help labels (effective defaults)
|
|
963
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
|
|
964
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
|
|
965
|
+
// Strings / enums
|
|
966
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
|
|
967
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
|
|
968
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
|
|
969
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
|
|
970
|
+
// Advanced key overrides
|
|
971
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
|
|
972
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
|
|
973
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
|
|
974
|
+
// Accept any extra operands so Commander does not error when tokens appear after "--".
|
|
975
|
+
.argument('[args...]')
|
|
976
|
+
.action(async (args, opts, thisCommand) => {
|
|
977
|
+
const pluginInst = plugin;
|
|
978
|
+
const cmdSelf = thisCommand;
|
|
979
|
+
const parent = (cmdSelf.parent ?? null);
|
|
980
|
+
// Access merged root CLI options (installed by passOptions())
|
|
981
|
+
const rootOpts = (parent?.getDotenvCliOptions ?? {});
|
|
982
|
+
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
983
|
+
Boolean(rootOpts?.capture);
|
|
984
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
985
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
986
|
+
// Build overlay cfg from subcommand flags layered over discovered config.
|
|
987
|
+
const ctx = cli.getCtx();
|
|
988
|
+
const cfgBase = pluginInst.readConfig(cli);
|
|
989
|
+
const o = opts;
|
|
990
|
+
const overlay = {};
|
|
991
|
+
// Map boolean toggles (respect explicit --no-*)
|
|
992
|
+
if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
|
|
993
|
+
overlay.loginOnDemand = Boolean(o.loginOnDemand);
|
|
994
|
+
// Strings/enums
|
|
995
|
+
if (typeof o.profile === 'string')
|
|
996
|
+
overlay.profile = o.profile;
|
|
997
|
+
if (typeof o.region === 'string')
|
|
998
|
+
overlay.region = o.region;
|
|
999
|
+
if (typeof o.defaultRegion === 'string')
|
|
1000
|
+
overlay.defaultRegion = o.defaultRegion;
|
|
1001
|
+
if (typeof o.strategy === 'string')
|
|
1002
|
+
overlay.strategy = o.strategy;
|
|
1003
|
+
// Advanced key overrides
|
|
1004
|
+
if (typeof o.profileKey === 'string')
|
|
1005
|
+
overlay.profileKey = o.profileKey;
|
|
1006
|
+
if (typeof o.profileFallbackKey === 'string')
|
|
1007
|
+
overlay.profileFallbackKey = o.profileFallbackKey;
|
|
1008
|
+
if (typeof o.regionKey === 'string')
|
|
1009
|
+
overlay.regionKey = o.regionKey;
|
|
1010
|
+
const cfg = {
|
|
1011
|
+
...cfgBase,
|
|
1012
|
+
...overlay,
|
|
1013
|
+
};
|
|
1014
|
+
// Resolve current context with overrides
|
|
1015
|
+
const out = await resolveAwsContext({
|
|
1016
|
+
dotenv: ctx?.dotenv ?? {},
|
|
1017
|
+
cfg,
|
|
1018
|
+
});
|
|
1019
|
+
// Unconditional env writes (no per-plugin toggle)
|
|
578
1020
|
if (out.region) {
|
|
579
1021
|
process.env.AWS_REGION = out.region;
|
|
580
1022
|
if (!process.env.AWS_DEFAULT_REGION)
|
|
@@ -588,58 +1030,53 @@ const awsPlugin = () => definePlugin({
|
|
|
588
1030
|
process.env.AWS_SESSION_TOKEN = out.credentials.sessionToken;
|
|
589
1031
|
}
|
|
590
1032
|
}
|
|
591
|
-
|
|
592
|
-
if (cfg.addCtx !== false) {
|
|
1033
|
+
// Always publish minimal non-sensitive metadata
|
|
593
1034
|
if (ctx) {
|
|
594
1035
|
ctx.plugins ??= {};
|
|
595
1036
|
ctx.plugins['aws'] = {
|
|
596
1037
|
...(out.profile ? { profile: out.profile } : {}),
|
|
597
1038
|
...(out.region ? { region: out.region } : {}),
|
|
598
|
-
...(out.credentials ? { credentials: out.credentials } : {}),
|
|
599
1039
|
};
|
|
600
1040
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
env: buildSpawnEnv(process.env, ctxDotenv),
|
|
609
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
610
|
-
});
|
|
611
|
-
// Deterministic termination (suppressed under tests)
|
|
612
|
-
if (!underTests) {
|
|
613
|
-
process.exit(typeof exit === 'number' ? exit : 0);
|
|
614
|
-
}
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
// Session only: low-noise breadcrumb under debug
|
|
619
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
620
|
-
const log = console;
|
|
621
|
-
log.log('[aws] session established', {
|
|
622
|
-
profile: out.profile,
|
|
623
|
-
region: out.region,
|
|
624
|
-
hasCreds: Boolean(out.credentials),
|
|
1041
|
+
// Forward when positional args are present; otherwise session-only.
|
|
1042
|
+
if (Array.isArray(args) && args.length > 0) {
|
|
1043
|
+
const argv = ['aws', ...args];
|
|
1044
|
+
const shellSetting = resolveShell(rootOpts?.scripts, 'aws', rootOpts?.shell);
|
|
1045
|
+
const exit = await runCommand(argv, shellSetting, {
|
|
1046
|
+
env: buildSpawnEnv(process.env, ctx?.dotenv),
|
|
1047
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
625
1048
|
});
|
|
1049
|
+
// Deterministic termination (suppressed under tests)
|
|
1050
|
+
if (!underTests) {
|
|
1051
|
+
process.exit(typeof exit === 'number' ? exit : 0);
|
|
1052
|
+
}
|
|
1053
|
+
return;
|
|
626
1054
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1055
|
+
else {
|
|
1056
|
+
// Session only: low-noise breadcrumb under debug
|
|
1057
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
1058
|
+
const log = console;
|
|
1059
|
+
log.log('[aws] session established', {
|
|
1060
|
+
profile: out.profile,
|
|
1061
|
+
region: out.region,
|
|
1062
|
+
hasCreds: Boolean(out.credentials),
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
if (!underTests)
|
|
1066
|
+
process.exit(0);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
},
|
|
1071
|
+
async afterResolve(_cli, ctx) {
|
|
1072
|
+
const log = console;
|
|
1073
|
+
const cfg = plugin.readConfig(_cli);
|
|
1074
|
+
const out = await resolveAwsContext({
|
|
1075
|
+
dotenv: ctx.dotenv,
|
|
1076
|
+
cfg,
|
|
1077
|
+
});
|
|
1078
|
+
const { profile, region, credentials } = out;
|
|
1079
|
+
// Unconditional env writes in host path
|
|
643
1080
|
if (region) {
|
|
644
1081
|
process.env.AWS_REGION = region;
|
|
645
1082
|
if (!process.env.AWS_DEFAULT_REGION)
|
|
@@ -652,25 +1089,24 @@ const awsPlugin = () => definePlugin({
|
|
|
652
1089
|
process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
|
|
653
1090
|
}
|
|
654
1091
|
}
|
|
655
|
-
|
|
656
|
-
if (cfg.addCtx !== false) {
|
|
1092
|
+
// Always publish minimal non-sensitive metadata
|
|
657
1093
|
ctx.plugins ??= {};
|
|
658
1094
|
ctx.plugins['aws'] = {
|
|
659
1095
|
...(profile ? { profile } : {}),
|
|
660
1096
|
...(region ? { region } : {}),
|
|
661
|
-
...(credentials ? { credentials } : {}),
|
|
662
1097
|
};
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
1098
|
+
// Optional: low-noise breadcrumb for diagnostics
|
|
1099
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
1100
|
+
log.log('[aws] afterResolve', {
|
|
1101
|
+
profile,
|
|
1102
|
+
region,
|
|
1103
|
+
hasCreds: Boolean(credentials),
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
});
|
|
1108
|
+
return plugin;
|
|
1109
|
+
};
|
|
674
1110
|
|
|
675
1111
|
const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
676
1112
|
let cwd = process.cwd();
|
|
@@ -695,9 +1131,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
|
695
1131
|
}
|
|
696
1132
|
return { absRootPath, paths };
|
|
697
1133
|
};
|
|
698
|
-
const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
1134
|
+
const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
699
1135
|
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
700
|
-
Boolean(getDotenvCliOptions?.capture);
|
|
1136
|
+
Boolean(getDotenvCliOptions?.capture);
|
|
1137
|
+
// Require a command only when not listing. In list mode, a command is optional.
|
|
701
1138
|
if (!command && !list) {
|
|
702
1139
|
logger.error(`No command provided. Use --command or --list.`);
|
|
703
1140
|
process.exit(0);
|
|
@@ -744,12 +1181,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
744
1181
|
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
745
1182
|
(Array.isArray(command) && command.length > 0);
|
|
746
1183
|
if (hasCmd) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1184
|
+
// Compose child env overlay from dotenv (drop undefined) and merged options
|
|
1185
|
+
const overlay = {};
|
|
1186
|
+
if (dotenvEnv) {
|
|
1187
|
+
for (const [k, v] of Object.entries(dotenvEnv)) {
|
|
1188
|
+
if (typeof v === 'string')
|
|
1189
|
+
overlay[k] = v;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (getDotenvCliOptions !== undefined) {
|
|
1193
|
+
try {
|
|
1194
|
+
overlay.getDotenvCliOptions = JSON.stringify(getDotenvCliOptions);
|
|
1195
|
+
}
|
|
1196
|
+
catch {
|
|
1197
|
+
// best-effort: omit if serialization fails
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
750
1200
|
await runCommand(command, shell, {
|
|
751
1201
|
cwd: path,
|
|
752
|
-
env: buildSpawnEnv(process.env,
|
|
1202
|
+
env: buildSpawnEnv(process.env, overlay),
|
|
753
1203
|
stdio: capture ? 'pipe' : 'inherit',
|
|
754
1204
|
});
|
|
755
1205
|
}
|
|
@@ -772,7 +1222,7 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
772
1222
|
* Build the default "cmd" subcommand action for the batch plugin.
|
|
773
1223
|
* Mirrors the original inline implementation with identical behavior.
|
|
774
1224
|
*/
|
|
775
|
-
const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _subOpts, _thisCommand) => {
|
|
1225
|
+
const buildDefaultCmdAction = (plugin, cli, batchCmd, opts) => async (commandParts, _subOpts, _thisCommand) => {
|
|
776
1226
|
const loggerLocal = opts.logger ?? console;
|
|
777
1227
|
// Guard: when invoked without positional args (e.g., `batch --list`),
|
|
778
1228
|
// defer entirely to the parent action handler.
|
|
@@ -784,9 +1234,8 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
784
1234
|
? argsRaw.filter((t) => t !== '-l' && t !== '--list')
|
|
785
1235
|
: argsRaw;
|
|
786
1236
|
// Access merged per-plugin config from host context (if any).
|
|
787
|
-
const
|
|
788
|
-
const
|
|
789
|
-
const cfg = (cfgRaw || {});
|
|
1237
|
+
const cfg = plugin.readConfig(cli);
|
|
1238
|
+
const dotenvEnv = (cli.getCtx()?.dotenv ?? {});
|
|
790
1239
|
// Resolve batch flags from the captured parent (batch) command.
|
|
791
1240
|
const raw = batchCmd.opts();
|
|
792
1241
|
const listFromParent = !!raw.list;
|
|
@@ -805,6 +1254,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
805
1254
|
if (typeof commandOpt === 'string') {
|
|
806
1255
|
await execShellCommandBatch({
|
|
807
1256
|
command: resolveCommand(scripts, commandOpt),
|
|
1257
|
+
dotenvEnv,
|
|
808
1258
|
globs,
|
|
809
1259
|
ignoreErrors,
|
|
810
1260
|
list: false,
|
|
@@ -816,6 +1266,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
816
1266
|
return;
|
|
817
1267
|
}
|
|
818
1268
|
if (raw.list || localList) {
|
|
1269
|
+
const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
|
|
819
1270
|
await execShellCommandBatch({
|
|
820
1271
|
globs,
|
|
821
1272
|
ignoreErrors,
|
|
@@ -823,7 +1274,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
823
1274
|
logger: loggerLocal,
|
|
824
1275
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
825
1276
|
rootPath,
|
|
826
|
-
shell:
|
|
1277
|
+
shell: shell ?? shellBag.shell ?? false,
|
|
827
1278
|
});
|
|
828
1279
|
return;
|
|
829
1280
|
}
|
|
@@ -847,7 +1298,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
847
1298
|
logger: loggerLocal,
|
|
848
1299
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
849
1300
|
rootPath,
|
|
850
|
-
shell:
|
|
1301
|
+
shell: shell ?? shellBag.shell ?? false,
|
|
851
1302
|
});
|
|
852
1303
|
return;
|
|
853
1304
|
}
|
|
@@ -890,6 +1341,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
890
1341
|
}
|
|
891
1342
|
await execShellCommandBatch({
|
|
892
1343
|
command: commandArg,
|
|
1344
|
+
dotenvEnv,
|
|
893
1345
|
...(envBag ? { getDotenvCliOptions: envBag } : {}),
|
|
894
1346
|
globs,
|
|
895
1347
|
ignoreErrors,
|
|
@@ -904,12 +1356,11 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
904
1356
|
/**
|
|
905
1357
|
* Build the parent "batch" action handler (no explicit subcommand).
|
|
906
1358
|
*/
|
|
907
|
-
const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
908
|
-
const
|
|
1359
|
+
const buildParentAction = (plugin, cli, opts) => async (commandParts, thisCommand) => {
|
|
1360
|
+
const loggerLocal = opts.logger ?? console;
|
|
909
1361
|
// Ensure context exists (host preSubcommand on root creates if missing).
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
const cfg = (cfgRaw || {});
|
|
1362
|
+
const dotenvEnv = (cli.getCtx()?.dotenv ?? {});
|
|
1363
|
+
const cfg = plugin.readConfig(cli);
|
|
913
1364
|
const raw = thisCommand.opts();
|
|
914
1365
|
const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
|
|
915
1366
|
const ignoreErrors = !!raw.ignoreErrors;
|
|
@@ -930,10 +1381,11 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
930
1381
|
const commandArg = resolved;
|
|
931
1382
|
await execShellCommandBatch({
|
|
932
1383
|
command: commandArg,
|
|
1384
|
+
dotenvEnv,
|
|
933
1385
|
globs,
|
|
934
1386
|
ignoreErrors,
|
|
935
1387
|
list: false,
|
|
936
|
-
logger,
|
|
1388
|
+
logger: loggerLocal,
|
|
937
1389
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
938
1390
|
rootPath,
|
|
939
1391
|
shell: shellSetting,
|
|
@@ -946,19 +1398,20 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
946
1398
|
if (extra.length > 0)
|
|
947
1399
|
globs = [globs, extra].filter(Boolean).join(' ');
|
|
948
1400
|
const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
|
|
1401
|
+
const shellMerged = opts.shell ?? cfg.shell ?? mergedBag.shell ?? false;
|
|
949
1402
|
await execShellCommandBatch({
|
|
950
1403
|
globs,
|
|
951
1404
|
ignoreErrors,
|
|
952
1405
|
list: true,
|
|
953
|
-
logger,
|
|
1406
|
+
logger: loggerLocal,
|
|
954
1407
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
955
1408
|
rootPath,
|
|
956
|
-
shell:
|
|
1409
|
+
shell: shellMerged,
|
|
957
1410
|
});
|
|
958
1411
|
return;
|
|
959
1412
|
}
|
|
960
1413
|
if (!commandOpt && !list) {
|
|
961
|
-
|
|
1414
|
+
loggerLocal.error(`No command provided. Use --command or --list.`);
|
|
962
1415
|
process.exit(0);
|
|
963
1416
|
}
|
|
964
1417
|
if (typeof commandOpt === 'string') {
|
|
@@ -967,10 +1420,11 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
967
1420
|
const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
|
|
968
1421
|
await execShellCommandBatch({
|
|
969
1422
|
command: resolveCommand(scriptsOpt, commandOpt),
|
|
1423
|
+
dotenvEnv,
|
|
970
1424
|
globs,
|
|
971
1425
|
ignoreErrors,
|
|
972
1426
|
list,
|
|
973
|
-
logger,
|
|
1427
|
+
logger: loggerLocal,
|
|
974
1428
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
975
1429
|
rootPath,
|
|
976
1430
|
shell: resolveShell(scriptsOpt, commandOpt, shellOpt),
|
|
@@ -979,217 +1433,87 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
979
1433
|
}
|
|
980
1434
|
// list only (explicit --list without --command)
|
|
981
1435
|
const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
|
|
982
|
-
const shellOnly =
|
|
1436
|
+
const shellOnly = opts.shell ?? cfg.shell ?? mergedBag.shell ?? false;
|
|
983
1437
|
await execShellCommandBatch({
|
|
984
1438
|
globs,
|
|
985
1439
|
ignoreErrors,
|
|
986
1440
|
list: true,
|
|
987
|
-
logger,
|
|
1441
|
+
logger: loggerLocal,
|
|
988
1442
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
989
1443
|
rootPath,
|
|
990
|
-
shell:
|
|
991
|
-
});
|
|
992
|
-
};
|
|
993
|
-
|
|
994
|
-
// Per-plugin config schema (optional fields; used as defaults).
|
|
995
|
-
const ScriptSchema = z.union([
|
|
996
|
-
z.string(),
|
|
997
|
-
z.object({
|
|
998
|
-
cmd: z.string(),
|
|
999
|
-
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
1000
|
-
}),
|
|
1001
|
-
]);
|
|
1002
|
-
const BatchConfigSchema = z.object({
|
|
1003
|
-
scripts: z.record(z.string(), ScriptSchema).optional(),
|
|
1004
|
-
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
1005
|
-
rootPath: z.string().optional(),
|
|
1006
|
-
globs: z.string().optional(),
|
|
1007
|
-
pkgCwd: z.boolean().optional(),
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
|
-
/**
|
|
1011
|
-
* Batch plugin for the GetDotenv CLI host.
|
|
1012
|
-
*
|
|
1013
|
-
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI. * Options:
|
|
1014
|
-
* - scripts/shell: used to resolve command and shell behavior per script or global default.
|
|
1015
|
-
* - logger: defaults to console.
|
|
1016
|
-
*/
|
|
1017
|
-
const batchPlugin = (opts = {}) => definePlugin({
|
|
1018
|
-
id: 'batch',
|
|
1019
|
-
// Host validates this when config-loader is enabled; plugins may also
|
|
1020
|
-
// re-validate at action time as a safety belt.
|
|
1021
|
-
configSchema: BatchConfigSchema,
|
|
1022
|
-
setup(cli) {
|
|
1023
|
-
const ns = cli.ns('batch');
|
|
1024
|
-
const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
|
|
1025
|
-
ns.description('Batch command execution across multiple working directories.')
|
|
1026
|
-
.enablePositionalOptions()
|
|
1027
|
-
.passThroughOptions()
|
|
1028
|
-
.option('-p, --pkg-cwd', 'use nearest package directory as current working directory')
|
|
1029
|
-
.option('-r, --root-path <string>', 'path to batch root directory from current working directory', './')
|
|
1030
|
-
.option('-g, --globs <string>', 'space-delimited globs from root path', '*')
|
|
1031
|
-
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
1032
|
-
.option('-l, --list', 'list working directories without executing command')
|
|
1033
|
-
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
1034
|
-
.argument('[command...]')
|
|
1035
|
-
.addCommand(new Command()
|
|
1036
|
-
.name('cmd')
|
|
1037
|
-
.description('execute command, conflicts with --command option (default subcommand)')
|
|
1038
|
-
.enablePositionalOptions()
|
|
1039
|
-
.passThroughOptions()
|
|
1040
|
-
.argument('[command...]')
|
|
1041
|
-
.action(buildDefaultCmdAction(cli, batchCmd, opts)), { isDefault: true })
|
|
1042
|
-
.action(buildParentAction(cli, opts));
|
|
1043
|
-
},
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
/** src/diagnostics/entropy.ts
|
|
1047
|
-
* Entropy diagnostics (presentation-only).
|
|
1048
|
-
* - Gated by min length and printable ASCII.
|
|
1049
|
-
* - Warn once per key per run when bits/char \>= threshold.
|
|
1050
|
-
* - Supports whitelist patterns to suppress known-noise keys.
|
|
1051
|
-
*/
|
|
1052
|
-
const warned = new Set();
|
|
1053
|
-
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
1054
|
-
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
1055
|
-
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
1056
|
-
const shannonBitsPerChar = (s) => {
|
|
1057
|
-
const freq = new Map();
|
|
1058
|
-
for (const ch of s)
|
|
1059
|
-
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
1060
|
-
const n = s.length;
|
|
1061
|
-
let h = 0;
|
|
1062
|
-
for (const c of freq.values()) {
|
|
1063
|
-
const p = c / n;
|
|
1064
|
-
h -= p * Math.log2(p);
|
|
1065
|
-
}
|
|
1066
|
-
return h;
|
|
1067
|
-
};
|
|
1068
|
-
/**
|
|
1069
|
-
* Maybe emit a one-line entropy warning for a key.
|
|
1070
|
-
* Caller supplies an `emit(line)` function; the helper ensures once-per-key.
|
|
1071
|
-
*/
|
|
1072
|
-
const maybeWarnEntropy = (key, value, origin, opts, emit) => {
|
|
1073
|
-
if (!opts || opts.warnEntropy === false)
|
|
1074
|
-
return;
|
|
1075
|
-
if (warned.has(key))
|
|
1076
|
-
return;
|
|
1077
|
-
const v = value ?? '';
|
|
1078
|
-
const minLen = Math.max(0, opts.entropyMinLength ?? 16);
|
|
1079
|
-
const threshold = opts.entropyThreshold ?? 3.8;
|
|
1080
|
-
if (v.length < minLen)
|
|
1081
|
-
return;
|
|
1082
|
-
if (!isPrintableAscii(v))
|
|
1083
|
-
return;
|
|
1084
|
-
const wl = compile$1(opts.entropyWhitelist);
|
|
1085
|
-
if (whitelisted(key, wl))
|
|
1086
|
-
return;
|
|
1087
|
-
const bpc = shannonBitsPerChar(v);
|
|
1088
|
-
if (bpc >= threshold) {
|
|
1089
|
-
warned.add(key);
|
|
1090
|
-
emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
|
|
1091
|
-
}
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
const DEFAULT_PATTERNS = [
|
|
1095
|
-
'\\bsecret\\b',
|
|
1096
|
-
'\\btoken\\b',
|
|
1097
|
-
'\\bpass(word)?\\b',
|
|
1098
|
-
'\\bapi[_-]?key\\b',
|
|
1099
|
-
'\\bkey\\b',
|
|
1100
|
-
];
|
|
1101
|
-
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
1102
|
-
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
1103
|
-
const MASK = '[redacted]';
|
|
1104
|
-
/**
|
|
1105
|
-
* Utility to redact three related displayed values (parent/dotenv/final)
|
|
1106
|
-
* consistently for trace lines.
|
|
1107
|
-
*/
|
|
1108
|
-
const redactTriple = (key, triple, opts) => {
|
|
1109
|
-
if (!opts?.redact)
|
|
1110
|
-
return triple;
|
|
1111
|
-
const regs = compile(opts.redactPatterns);
|
|
1112
|
-
const maskIf = (v) => (v && shouldRedactKey(key, regs) ? MASK : v);
|
|
1113
|
-
const out = {};
|
|
1114
|
-
const p = maskIf(triple.parent);
|
|
1115
|
-
const d = maskIf(triple.dotenv);
|
|
1116
|
-
const f = maskIf(triple.final);
|
|
1117
|
-
if (p !== undefined)
|
|
1118
|
-
out.parent = p;
|
|
1119
|
-
if (d !== undefined)
|
|
1120
|
-
out.dotenv = d;
|
|
1121
|
-
if (f !== undefined)
|
|
1122
|
-
out.final = f;
|
|
1123
|
-
return out;
|
|
1124
|
-
};
|
|
1125
|
-
|
|
1126
|
-
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
1127
|
-
const baseRootOptionDefaults = {
|
|
1128
|
-
dotenvToken: '.env',
|
|
1129
|
-
loadProcess: true,
|
|
1130
|
-
logger: console,
|
|
1131
|
-
// Diagnostics defaults
|
|
1132
|
-
warnEntropy: true,
|
|
1133
|
-
entropyThreshold: 3.8,
|
|
1134
|
-
entropyMinLength: 16,
|
|
1135
|
-
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
1136
|
-
paths: './',
|
|
1137
|
-
pathsDelimiter: ' ',
|
|
1138
|
-
privateToken: 'local',
|
|
1139
|
-
scripts: {
|
|
1140
|
-
'git-status': {
|
|
1141
|
-
cmd: 'git branch --show-current && git status -s -u',
|
|
1142
|
-
shell: true,
|
|
1143
|
-
},
|
|
1144
|
-
},
|
|
1145
|
-
shell: true,
|
|
1146
|
-
vars: '',
|
|
1147
|
-
varsAssignor: '=',
|
|
1148
|
-
varsDelimiter: ' ',
|
|
1149
|
-
// tri-state flags default to unset unless explicitly provided
|
|
1150
|
-
// (debug/log/exclude* resolved via flag utils)
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
/** @internal */
|
|
1154
|
-
const isPlainObject = (value) => value !== null &&
|
|
1155
|
-
typeof value === 'object' &&
|
|
1156
|
-
Object.getPrototypeOf(value) === Object.prototype;
|
|
1157
|
-
const mergeInto = (target, source) => {
|
|
1158
|
-
for (const [key, sVal] of Object.entries(source)) {
|
|
1159
|
-
if (sVal === undefined)
|
|
1160
|
-
continue; // do not overwrite with undefined
|
|
1161
|
-
const tVal = target[key];
|
|
1162
|
-
if (isPlainObject(tVal) && isPlainObject(sVal)) {
|
|
1163
|
-
target[key] = mergeInto({ ...tVal }, sVal);
|
|
1164
|
-
}
|
|
1165
|
-
else if (isPlainObject(sVal)) {
|
|
1166
|
-
target[key] = mergeInto({}, sVal);
|
|
1167
|
-
}
|
|
1168
|
-
else {
|
|
1169
|
-
target[key] = sVal;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
return target;
|
|
1444
|
+
shell: shellOnly,
|
|
1445
|
+
});
|
|
1173
1446
|
};
|
|
1447
|
+
|
|
1448
|
+
// Per-plugin config schema (optional fields; used as defaults).
|
|
1449
|
+
const ScriptSchema = z.union([
|
|
1450
|
+
z.string(),
|
|
1451
|
+
z.object({
|
|
1452
|
+
cmd: z.string(),
|
|
1453
|
+
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
1454
|
+
}),
|
|
1455
|
+
]);
|
|
1456
|
+
const BatchConfigSchema = z.object({
|
|
1457
|
+
scripts: z.record(z.string(), ScriptSchema).optional(),
|
|
1458
|
+
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
1459
|
+
rootPath: z.string().optional(),
|
|
1460
|
+
globs: z.string().optional(),
|
|
1461
|
+
pkgCwd: z.boolean().optional(),
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1174
1464
|
/**
|
|
1175
|
-
*
|
|
1176
|
-
* - Only merges plain objects (prototype === Object.prototype).
|
|
1177
|
-
* - Arrays and non-objects are replaced, not merged.
|
|
1178
|
-
* - `undefined` values are ignored and do not overwrite prior values.
|
|
1179
|
-
*
|
|
1180
|
-
* @typeParam T - The resulting shape after merging all layers.
|
|
1181
|
-
* @param layers - Zero or more partial layers in ascending precedence order.
|
|
1182
|
-
* @returns The merged object typed as {@link T}.
|
|
1465
|
+
* Batch plugin for the GetDotenv CLI host.
|
|
1183
1466
|
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1467
|
+
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
1468
|
+
* Options:
|
|
1469
|
+
* - scripts/shell: used to resolve command and shell behavior per script or global default.
|
|
1470
|
+
* - logger: defaults to console.
|
|
1187
1471
|
*/
|
|
1188
|
-
const
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1472
|
+
const batchPlugin = (opts = {}) => {
|
|
1473
|
+
const plugin = definePlugin({
|
|
1474
|
+
id: 'batch',
|
|
1475
|
+
// Host validates this when config-loader is enabled; plugins may also
|
|
1476
|
+
// re-validate at action time as a safety belt.
|
|
1477
|
+
configSchema: BatchConfigSchema,
|
|
1478
|
+
setup(cli) {
|
|
1479
|
+
const ns = cli.ns('batch');
|
|
1480
|
+
const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
|
|
1481
|
+
const pluginId = 'batch';
|
|
1482
|
+
const GROUP = `plugin:${pluginId}`;
|
|
1483
|
+
ns.description('Batch command execution across multiple working directories.')
|
|
1484
|
+
.enablePositionalOptions()
|
|
1485
|
+
.passThroughOptions()
|
|
1486
|
+
// Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
|
|
1487
|
+
.addOption((() => {
|
|
1488
|
+
const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
|
|
1489
|
+
cli.setOptionGroup(opt, GROUP);
|
|
1490
|
+
return opt;
|
|
1491
|
+
})())
|
|
1492
|
+
.addOption((() => {
|
|
1493
|
+
const opt = plugin.createPluginDynamicOption(cli, '-r, --root-path <string>', (_bag, cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.rootPath || './')})`);
|
|
1494
|
+
cli.setOptionGroup(opt, GROUP);
|
|
1495
|
+
return opt;
|
|
1496
|
+
})())
|
|
1497
|
+
.addOption((() => {
|
|
1498
|
+
const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
|
|
1499
|
+
cli.setOptionGroup(opt, GROUP);
|
|
1500
|
+
return opt;
|
|
1501
|
+
})())
|
|
1502
|
+
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
1503
|
+
.option('-l, --list', 'list working directories without executing command')
|
|
1504
|
+
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
1505
|
+
.argument('[command...]')
|
|
1506
|
+
.addCommand(new Command()
|
|
1507
|
+
.name('cmd')
|
|
1508
|
+
.description('execute command, conflicts with --command option (default subcommand)')
|
|
1509
|
+
.enablePositionalOptions()
|
|
1510
|
+
.passThroughOptions()
|
|
1511
|
+
.argument('[command...]')
|
|
1512
|
+
.action(buildDefaultCmdAction(plugin, cli, batchCmd, opts)), { isDefault: true })
|
|
1513
|
+
.action(buildParentAction(plugin, cli, opts));
|
|
1514
|
+
},
|
|
1515
|
+
});
|
|
1516
|
+
return plugin;
|
|
1193
1517
|
};
|
|
1194
1518
|
|
|
1195
1519
|
/**
|
|
@@ -1317,180 +1641,225 @@ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
|
1317
1641
|
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
1318
1642
|
};
|
|
1319
1643
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
* references in strings and records. It supports both whitespace and
|
|
1325
|
-
* bracket syntaxes with optional defaults:
|
|
1326
|
-
*
|
|
1327
|
-
* - Whitespace: `$VAR[:default]`
|
|
1328
|
-
* - Bracketed: `${VAR[:default]}`
|
|
1329
|
-
*
|
|
1330
|
-
* Escaped dollar signs (`\$`) are preserved.
|
|
1331
|
-
* Unknown variables resolve to empty string unless a default is provided.
|
|
1332
|
-
*/
|
|
1333
|
-
/**
|
|
1334
|
-
* Like String.prototype.search but returns the last index.
|
|
1335
|
-
* @internal
|
|
1336
|
-
*/
|
|
1337
|
-
const searchLast = (str, rgx) => {
|
|
1338
|
-
const matches = Array.from(str.matchAll(rgx));
|
|
1339
|
-
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
1340
|
-
};
|
|
1341
|
-
const replaceMatch = (value, match, ref) => {
|
|
1342
|
-
/**
|
|
1343
|
-
* @internal
|
|
1344
|
-
*/
|
|
1345
|
-
const group = match[0];
|
|
1346
|
-
const key = match[1];
|
|
1347
|
-
const defaultValue = match[2];
|
|
1348
|
-
if (!key)
|
|
1349
|
-
return value;
|
|
1350
|
-
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
1351
|
-
return interpolate(replacement, ref);
|
|
1352
|
-
};
|
|
1353
|
-
const interpolate = (value = '', ref = {}) => {
|
|
1354
|
-
/**
|
|
1355
|
-
* @internal
|
|
1356
|
-
*/
|
|
1357
|
-
// if value is falsy, return it as is
|
|
1358
|
-
if (!value)
|
|
1359
|
-
return value;
|
|
1360
|
-
// get position of last unescaped dollar sign
|
|
1361
|
-
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
1362
|
-
// return value if none found
|
|
1363
|
-
if (lastUnescapedDollarSignIndex === -1)
|
|
1364
|
-
return value;
|
|
1365
|
-
// evaluate the value tail
|
|
1366
|
-
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
1367
|
-
// find whitespace pattern: $KEY:DEFAULT
|
|
1368
|
-
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
1369
|
-
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
1370
|
-
if (whitespaceMatch != null)
|
|
1371
|
-
return replaceMatch(value, whitespaceMatch, ref);
|
|
1372
|
-
else {
|
|
1373
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
1374
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
1375
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
1376
|
-
if (bracketMatch != null)
|
|
1377
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
1644
|
+
const dbg = (...args) => {
|
|
1645
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
1646
|
+
// Use stderr to avoid interfering with stdout assertions
|
|
1647
|
+
console.error('[getdotenv:alias]', ...args);
|
|
1378
1648
|
}
|
|
1379
|
-
return value;
|
|
1380
1649
|
};
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
*
|
|
1390
|
-
* @example
|
|
1391
|
-
* ```ts
|
|
1392
|
-
* process.env.FOO = 'bar';
|
|
1393
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
1394
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
1395
|
-
* ```
|
|
1396
|
-
*
|
|
1397
|
-
* @remarks
|
|
1398
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
1399
|
-
* references, those will also be expanded until a stable value is reached.
|
|
1400
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
1401
|
-
*/
|
|
1402
|
-
const dotenvExpand = (value, ref = process.env) => {
|
|
1403
|
-
const result = interpolate(value, ref);
|
|
1404
|
-
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
1650
|
+
// Strip one symmetric outer quote layer
|
|
1651
|
+
const stripOne = (s) => {
|
|
1652
|
+
if (s.length < 2)
|
|
1653
|
+
return s;
|
|
1654
|
+
const a = s.charAt(0);
|
|
1655
|
+
const b = s.charAt(s.length - 1);
|
|
1656
|
+
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
1657
|
+
return symmetric ? s.slice(1, -1) : s;
|
|
1405
1658
|
};
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
//
|
|
1424
|
-
/**
|
|
1425
|
-
* Converts programmatic CLI options to `getDotenv` options. *
|
|
1426
|
-
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
1427
|
-
*
|
|
1428
|
-
* @returns `getDotenv` options.
|
|
1429
|
-
*/
|
|
1430
|
-
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
1431
|
-
/**
|
|
1432
|
-
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
1433
|
-
*
|
|
1434
|
-
* - 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`
|
|
1435
|
-
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
1436
|
-
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
1437
|
-
*
|
|
1438
|
-
* @remarks
|
|
1439
|
-
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
1440
|
-
*/
|
|
1441
|
-
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
1442
|
-
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
1443
|
-
const restObj = { ...rest };
|
|
1444
|
-
delete restObj.debug;
|
|
1445
|
-
delete restObj.scripts;
|
|
1446
|
-
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
1447
|
-
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
1448
|
-
let parsedVars;
|
|
1449
|
-
if (typeof vars === 'string') {
|
|
1450
|
-
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
1451
|
-
? RegExp(varsAssignorPattern)
|
|
1452
|
-
: (varsAssignor ?? '=')));
|
|
1453
|
-
parsedVars = Object.fromEntries(kvPairs);
|
|
1659
|
+
async function maybeRunAlias(cli, thisCommand, aliasKey, state) {
|
|
1660
|
+
dbg('alias:maybe:start');
|
|
1661
|
+
const raw = thisCommand.rawArgs ?? [];
|
|
1662
|
+
const childNames = thisCommand.commands.flatMap((c) => [
|
|
1663
|
+
c.name(),
|
|
1664
|
+
...c.aliases(),
|
|
1665
|
+
]);
|
|
1666
|
+
const hasSub = childNames.some((n) => raw.includes(n));
|
|
1667
|
+
const o = thisCommand.opts();
|
|
1668
|
+
const val = o[aliasKey];
|
|
1669
|
+
const provided = typeof val === 'string'
|
|
1670
|
+
? val.length > 0
|
|
1671
|
+
: Array.isArray(val)
|
|
1672
|
+
? val.length > 0
|
|
1673
|
+
: false;
|
|
1674
|
+
if (!provided || hasSub) {
|
|
1675
|
+
dbg('alias:maybe:skip', { provided, hasSub });
|
|
1676
|
+
return; // not an alias-only invocation
|
|
1454
1677
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
parsedVars = Object.fromEntries(entries);
|
|
1678
|
+
if (state.handled) {
|
|
1679
|
+
dbg('alias:maybe:already-handled');
|
|
1680
|
+
return;
|
|
1459
1681
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1682
|
+
state.handled = true;
|
|
1683
|
+
dbg('alias-only invocation detected');
|
|
1684
|
+
// Merge CLI options and resolve dotenv context.
|
|
1685
|
+
const { merged } = resolveCliOptions(o, baseGetDotenvCliOptions, process.env.getDotenvCliOptions);
|
|
1686
|
+
const mergedBag = merged;
|
|
1687
|
+
const logger = (mergedBag.logger ?? console);
|
|
1688
|
+
const serviceOptions = getDotenvCliOptions2Options(mergedBag);
|
|
1689
|
+
await cli.resolveAndLoad(serviceOptions);
|
|
1690
|
+
// Normalize alias value
|
|
1691
|
+
const joined = typeof val === 'string'
|
|
1692
|
+
? val
|
|
1693
|
+
: Array.isArray(val)
|
|
1694
|
+
? val.map(String).join(' ')
|
|
1695
|
+
: '';
|
|
1696
|
+
const expanded = dotenvExpandFromProcessEnv(joined);
|
|
1697
|
+
const input = mergedBag.expand === false
|
|
1698
|
+
? joined
|
|
1699
|
+
: expanded !== undefined
|
|
1700
|
+
? expanded
|
|
1701
|
+
: joined;
|
|
1702
|
+
// Scripts: prefer well-formed records; tolerate absent/bad shapes
|
|
1703
|
+
const maybeScripts = mergedBag.scripts;
|
|
1704
|
+
const scripts = maybeScripts && typeof maybeScripts === 'object'
|
|
1705
|
+
? maybeScripts
|
|
1706
|
+
: undefined;
|
|
1707
|
+
const resolved = resolveCommand(scripts, input);
|
|
1708
|
+
if (mergedBag.debug) {
|
|
1709
|
+
logger.log('\n*** command ***\n', `'${resolved}'`);
|
|
1464
1710
|
}
|
|
1465
|
-
//
|
|
1466
|
-
//
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
1472
|
-
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
1473
|
-
return {
|
|
1474
|
-
...restObj,
|
|
1475
|
-
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
1476
|
-
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
1477
|
-
};
|
|
1478
|
-
};
|
|
1479
|
-
|
|
1480
|
-
const dbg = (...args) => {
|
|
1481
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
1482
|
-
// Use stderr to avoid interfering with stdout assertions
|
|
1483
|
-
console.error('[getdotenv:alias]', ...args);
|
|
1711
|
+
// Round-trip CLI options for nested getdotenv invocations. Omit logger
|
|
1712
|
+
// (functions/circulars) and guard JSON serialization to avoid hard failures.
|
|
1713
|
+
const { logger: _omitLogger, ...envBag } = mergedBag;
|
|
1714
|
+
let nestedBag;
|
|
1715
|
+
try {
|
|
1716
|
+
nestedBag = JSON.stringify(envBag);
|
|
1484
1717
|
}
|
|
1485
|
-
|
|
1718
|
+
catch {
|
|
1719
|
+
nestedBag = undefined;
|
|
1720
|
+
}
|
|
1721
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
1722
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
1723
|
+
const forceExit = process.env.GETDOTENV_FORCE_EXIT === '1';
|
|
1724
|
+
const capture = !underTests &&
|
|
1725
|
+
(process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
1726
|
+
Boolean(mergedBag.capture));
|
|
1727
|
+
dbg('run:start', {
|
|
1728
|
+
capture,
|
|
1729
|
+
shell: mergedBag.shell,
|
|
1730
|
+
});
|
|
1731
|
+
const ctx = cli.getCtx();
|
|
1732
|
+
const dotenv = (ctx?.dotenv ?? {});
|
|
1733
|
+
// Diagnostics: --trace [keys...]
|
|
1734
|
+
const traceOpt = mergedBag.trace;
|
|
1735
|
+
if (traceOpt) {
|
|
1736
|
+
const parentKeys = Object.keys(process.env);
|
|
1737
|
+
const dotenvKeys = Object.keys(dotenv);
|
|
1738
|
+
const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
|
|
1739
|
+
const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
|
|
1740
|
+
const childEnvPreview = {
|
|
1741
|
+
...process.env,
|
|
1742
|
+
...dotenv,
|
|
1743
|
+
};
|
|
1744
|
+
for (const k of keys) {
|
|
1745
|
+
const parent = process.env[k];
|
|
1746
|
+
const dot = dotenv[k];
|
|
1747
|
+
const final = childEnvPreview[k];
|
|
1748
|
+
const origin = dot !== undefined
|
|
1749
|
+
? 'dotenv'
|
|
1750
|
+
: parent !== undefined
|
|
1751
|
+
? 'parent'
|
|
1752
|
+
: 'unset';
|
|
1753
|
+
const redFlag = mergedBag.redact;
|
|
1754
|
+
const redPatterns = mergedBag.redactPatterns;
|
|
1755
|
+
const redOpts = {};
|
|
1756
|
+
if (redFlag)
|
|
1757
|
+
redOpts.redact = true;
|
|
1758
|
+
if (redFlag && Array.isArray(redPatterns))
|
|
1759
|
+
redOpts.redactPatterns = redPatterns;
|
|
1760
|
+
const tripleBag = {};
|
|
1761
|
+
if (parent !== undefined)
|
|
1762
|
+
tripleBag.parent = parent;
|
|
1763
|
+
if (dot !== undefined)
|
|
1764
|
+
tripleBag.dotenv = dot;
|
|
1765
|
+
if (final !== undefined)
|
|
1766
|
+
tripleBag.final = final;
|
|
1767
|
+
const triple = redactTriple(k, tripleBag, redOpts);
|
|
1768
|
+
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
1769
|
+
const entOpts = {};
|
|
1770
|
+
const warnEntropy = mergedBag.warnEntropy;
|
|
1771
|
+
const entropyThreshold = mergedBag
|
|
1772
|
+
.entropyThreshold;
|
|
1773
|
+
const entropyMinLength = mergedBag
|
|
1774
|
+
.entropyMinLength;
|
|
1775
|
+
const entropyWhitelist = mergedBag.entropyWhitelist;
|
|
1776
|
+
if (typeof warnEntropy === 'boolean')
|
|
1777
|
+
entOpts.warnEntropy = warnEntropy;
|
|
1778
|
+
if (typeof entropyThreshold === 'number')
|
|
1779
|
+
entOpts.entropyThreshold = entropyThreshold;
|
|
1780
|
+
if (typeof entropyMinLength === 'number')
|
|
1781
|
+
entOpts.entropyMinLength = entropyMinLength;
|
|
1782
|
+
if (Array.isArray(entropyWhitelist))
|
|
1783
|
+
entOpts.entropyWhitelist = entropyWhitelist;
|
|
1784
|
+
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
const shellSetting = resolveShell(scripts, input, mergedBag.shell);
|
|
1788
|
+
// Preserve argv array for Node -e snippets under shell-off
|
|
1789
|
+
let commandArg = resolved;
|
|
1790
|
+
if (shellSetting === false && resolved === input) {
|
|
1791
|
+
// Important: preserve doubled quotes within the Node -e payload so
|
|
1792
|
+
// empty string literals ("") survive; Windows-style doubling must not
|
|
1793
|
+
// collapse "" -> " in this path.
|
|
1794
|
+
const parts = tokenize(input, { preserveDoubledQuotes: true });
|
|
1795
|
+
if (parts.length >= 3 &&
|
|
1796
|
+
parts[0]?.toLowerCase() === 'node' &&
|
|
1797
|
+
(parts[1] === '-e' || parts[1] === '--eval')) {
|
|
1798
|
+
// Peel exactly one symmetric outer quote on the code arg
|
|
1799
|
+
parts[2] = stripOne(parts[2] ?? '');
|
|
1800
|
+
// Historical behavior: pass the argv array through unchanged for shell-off.
|
|
1801
|
+
commandArg = parts;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
let exitCode = Number.NaN;
|
|
1805
|
+
try {
|
|
1806
|
+
exitCode = await runCommand(commandArg, shellSetting, {
|
|
1807
|
+
env: buildSpawnEnv(process.env, nestedBag
|
|
1808
|
+
? {
|
|
1809
|
+
...dotenv,
|
|
1810
|
+
getDotenvCliOptions: nestedBag,
|
|
1811
|
+
}
|
|
1812
|
+
: {
|
|
1813
|
+
...dotenv,
|
|
1814
|
+
}),
|
|
1815
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
1816
|
+
});
|
|
1817
|
+
dbg('run:done', { exitCode });
|
|
1818
|
+
}
|
|
1819
|
+
catch (err) {
|
|
1820
|
+
const code = typeof err.exitCode === 'number'
|
|
1821
|
+
? err.exitCode
|
|
1822
|
+
: 1;
|
|
1823
|
+
dbg('run:error', { exitCode: code, error: String(err) });
|
|
1824
|
+
if (!underTests) {
|
|
1825
|
+
dbg('process.exit (error path)', { exitCode: code });
|
|
1826
|
+
process.exit(code);
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
dbg('process.exit suppressed for tests (error path)', {
|
|
1830
|
+
exitCode: code,
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (!Number.isNaN(exitCode)) {
|
|
1836
|
+
dbg('process.exit', { exitCode });
|
|
1837
|
+
process.exit(exitCode);
|
|
1838
|
+
}
|
|
1839
|
+
if (!underTests) {
|
|
1840
|
+
dbg('process.exit (fallback: non-numeric exitCode)', { exitCode: 0 });
|
|
1841
|
+
process.exit(0);
|
|
1842
|
+
}
|
|
1843
|
+
else {
|
|
1844
|
+
dbg('process.exit (fallback suppressed for tests: non-numeric exitCode)', {
|
|
1845
|
+
exitCode: 0,
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
if (forceExit) {
|
|
1849
|
+
setImmediate(() => process.exit(Number.isNaN(exitCode) ? 0 : exitCode));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1486
1853
|
const attachParentAlias = (cli, options, _cmd) => {
|
|
1487
1854
|
const aliasSpec = typeof options.optionAlias === 'string'
|
|
1488
|
-
? { flags: options.optionAlias, description: undefined
|
|
1855
|
+
? { flags: options.optionAlias, description: undefined}
|
|
1489
1856
|
: options.optionAlias;
|
|
1490
1857
|
if (!aliasSpec)
|
|
1491
1858
|
return;
|
|
1492
1859
|
const deriveKey = (flags) => {
|
|
1493
|
-
|
|
1860
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
1861
|
+
console.error('[getdotenv:alias] install alias option', flags);
|
|
1862
|
+
}
|
|
1494
1863
|
const long = flags.split(/[ ,|]+/).find((f) => f.startsWith('--')) ?? '--cmd';
|
|
1495
1864
|
const name = long.replace(/^--/, '');
|
|
1496
1865
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
@@ -1500,256 +1869,24 @@ const attachParentAlias = (cli, options, _cmd) => {
|
|
|
1500
1869
|
const desc = aliasSpec.description ??
|
|
1501
1870
|
'alias of cmd subcommand; provide command tokens (variadic)';
|
|
1502
1871
|
cli.option(aliasSpec.flags, desc);
|
|
1503
|
-
// Tag the just-added parent option for grouped help rendering.
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
last
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
catch {
|
|
1512
|
-
/* noop */
|
|
1872
|
+
// Tag the just-added parent option for grouped help rendering at the root.
|
|
1873
|
+
const optsArr = cli.options;
|
|
1874
|
+
if (optsArr.length > 0) {
|
|
1875
|
+
const last = optsArr[optsArr.length - 1];
|
|
1876
|
+
if (last)
|
|
1877
|
+
cli.setOptionGroup(last, 'plugin:cmd');
|
|
1513
1878
|
}
|
|
1514
1879
|
// Shared alias executor for either preAction or preSubcommand hooks.
|
|
1515
1880
|
// Ensure we only execute once even if both hooks fire in a single parse.
|
|
1516
|
-
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
const raw = thisCommand.rawArgs ?? [];
|
|
1520
|
-
const childNames = thisCommand.commands.flatMap((c) => [
|
|
1521
|
-
c.name(),
|
|
1522
|
-
...c.aliases(),
|
|
1523
|
-
]);
|
|
1524
|
-
const hasSub = childNames.some((n) => raw.includes(n));
|
|
1525
|
-
// Read alias value from parent opts.
|
|
1526
|
-
const o = thisCommand.opts();
|
|
1527
|
-
const val = o[aliasKey];
|
|
1528
|
-
const provided = typeof val === 'string'
|
|
1529
|
-
? val.length > 0
|
|
1530
|
-
: Array.isArray(val)
|
|
1531
|
-
? val.length > 0
|
|
1532
|
-
: false;
|
|
1533
|
-
if (!provided || hasSub) {
|
|
1534
|
-
dbg('alias:maybe:skip', { provided, hasSub });
|
|
1535
|
-
return; // not an alias-only invocation
|
|
1536
|
-
}
|
|
1537
|
-
if (aliasHandled) {
|
|
1538
|
-
dbg('alias:maybe:already-handled');
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
aliasHandled = true;
|
|
1542
|
-
dbg('alias-only invocation detected');
|
|
1543
|
-
// Merge CLI options and resolve dotenv context.
|
|
1544
|
-
const { merged } = resolveCliOptions(o,
|
|
1545
|
-
// cast through unknown to avoid readonly -> mutable incompatibilities
|
|
1546
|
-
baseRootOptionDefaults, process.env.getDotenvCliOptions);
|
|
1547
|
-
const logger = merged.logger ?? console;
|
|
1548
|
-
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1549
|
-
await cli.resolveAndLoad(serviceOptions);
|
|
1550
|
-
// Normalize alias value.
|
|
1551
|
-
const joined = typeof val === 'string'
|
|
1552
|
-
? val
|
|
1553
|
-
: Array.isArray(val)
|
|
1554
|
-
? val.map(String).join(' ')
|
|
1555
|
-
: '';
|
|
1556
|
-
const input = aliasSpec.expand === false
|
|
1557
|
-
? joined
|
|
1558
|
-
: (dotenvExpandFromProcessEnv(joined) ?? joined);
|
|
1559
|
-
dbg('resolved input', { input });
|
|
1560
|
-
const resolved = resolveCommand(merged.scripts, input);
|
|
1561
|
-
const lg = logger;
|
|
1562
|
-
if (merged.debug) {
|
|
1563
|
-
(lg.debug ?? lg.log)('\n*** command ***\n', `'${resolved}'`);
|
|
1564
|
-
}
|
|
1565
|
-
const { logger: _omit, ...envBag } = merged;
|
|
1566
|
-
// Test guard: when running under tests, prefer stdio: 'inherit' to avoid
|
|
1567
|
-
// assertions depending on captured stdio; ignore GETDOTENV_STDIO/capture.
|
|
1568
|
-
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
1569
|
-
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
1570
|
-
const forceExit = process.env.GETDOTENV_FORCE_EXIT === '1';
|
|
1571
|
-
const capture = !underTests &&
|
|
1572
|
-
(process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
1573
|
-
Boolean(merged.capture));
|
|
1574
|
-
dbg('run:start', { capture, shell: merged.shell });
|
|
1575
|
-
// Prefer explicit env injection: include resolved dotenv map to avoid leaking
|
|
1576
|
-
// parent process.env secrets when exclusions are set.
|
|
1577
|
-
const ctx = cli.getCtx();
|
|
1578
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
1579
|
-
// Diagnostics: --trace [keys...]
|
|
1580
|
-
const traceOpt = merged.trace;
|
|
1581
|
-
if (traceOpt) {
|
|
1582
|
-
const parentKeys = Object.keys(process.env);
|
|
1583
|
-
const dotenvKeys = Object.keys(dotenv);
|
|
1584
|
-
const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
|
|
1585
|
-
const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
|
|
1586
|
-
const childEnvPreview = {
|
|
1587
|
-
...process.env,
|
|
1588
|
-
...dotenv,
|
|
1589
|
-
};
|
|
1590
|
-
for (const k of keys) {
|
|
1591
|
-
const parent = process.env[k];
|
|
1592
|
-
const dot = dotenv[k];
|
|
1593
|
-
const final = childEnvPreview[k];
|
|
1594
|
-
const origin = dot !== undefined
|
|
1595
|
-
? 'dotenv'
|
|
1596
|
-
: parent !== undefined
|
|
1597
|
-
? 'parent'
|
|
1598
|
-
: 'unset';
|
|
1599
|
-
// Build redact options and triple bag without undefined-valued fields
|
|
1600
|
-
const redOpts = {};
|
|
1601
|
-
const redFlag = merged.redact;
|
|
1602
|
-
const redPatterns = merged
|
|
1603
|
-
.redactPatterns;
|
|
1604
|
-
if (redFlag)
|
|
1605
|
-
redOpts.redact = true;
|
|
1606
|
-
if (redFlag && Array.isArray(redPatterns))
|
|
1607
|
-
redOpts.redactPatterns = redPatterns;
|
|
1608
|
-
const tripleBag = {};
|
|
1609
|
-
if (parent !== undefined)
|
|
1610
|
-
tripleBag.parent = parent;
|
|
1611
|
-
if (dot !== undefined)
|
|
1612
|
-
tripleBag.dotenv = dot;
|
|
1613
|
-
if (final !== undefined)
|
|
1614
|
-
tripleBag.final = final;
|
|
1615
|
-
const triple = redactTriple(k, tripleBag, redOpts);
|
|
1616
|
-
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
1617
|
-
const entOpts = {};
|
|
1618
|
-
const warnEntropy = merged.warnEntropy;
|
|
1619
|
-
const entropyThreshold = merged
|
|
1620
|
-
.entropyThreshold;
|
|
1621
|
-
const entropyMinLength = merged
|
|
1622
|
-
.entropyMinLength;
|
|
1623
|
-
const entropyWhitelist = merged
|
|
1624
|
-
.entropyWhitelist;
|
|
1625
|
-
if (typeof warnEntropy === 'boolean')
|
|
1626
|
-
entOpts.warnEntropy = warnEntropy;
|
|
1627
|
-
if (typeof entropyThreshold === 'number')
|
|
1628
|
-
entOpts.entropyThreshold = entropyThreshold;
|
|
1629
|
-
if (typeof entropyMinLength === 'number')
|
|
1630
|
-
entOpts.entropyMinLength = entropyMinLength;
|
|
1631
|
-
if (Array.isArray(entropyWhitelist))
|
|
1632
|
-
entOpts.entropyWhitelist = entropyWhitelist;
|
|
1633
|
-
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
let exitCode = Number.NaN;
|
|
1637
|
-
try {
|
|
1638
|
-
// Resolve shell and preserve argv for Node -e snippets under shell-off.
|
|
1639
|
-
const shellSetting = resolveShell(merged.scripts, input, merged.shell);
|
|
1640
|
-
let commandArg = resolved;
|
|
1641
|
-
/** * Special-case: when shell is OFF and no script alias remap occurred
|
|
1642
|
-
* (resolved === input), treat a Node eval payload as an argv array to
|
|
1643
|
-
* avoid lossy re-tokenization of the code string.
|
|
1644
|
-
*
|
|
1645
|
-
* Examples handled:
|
|
1646
|
-
* "node -e \"console.log(JSON.stringify(...))\""
|
|
1647
|
-
* "node --eval 'console.log(...)'"
|
|
1648
|
-
*
|
|
1649
|
-
* We peel exactly one pair of symmetric outer quotes from the code
|
|
1650
|
-
* argument when present; inner quotes remain untouched.
|
|
1651
|
-
*/
|
|
1652
|
-
if (shellSetting === false && resolved === input) {
|
|
1653
|
-
// Helper: strip one symmetric outer quote layer
|
|
1654
|
-
const stripOne = (s) => {
|
|
1655
|
-
if (s.length < 2)
|
|
1656
|
-
return s;
|
|
1657
|
-
const a = s.charAt(0);
|
|
1658
|
-
const b = s.charAt(s.length - 1);
|
|
1659
|
-
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
1660
|
-
return symmetric ? s.slice(1, -1) : s;
|
|
1661
|
-
};
|
|
1662
|
-
// Normalize whole input once for robust matching
|
|
1663
|
-
const normalized = stripOne(input.trim());
|
|
1664
|
-
// First try a lightweight regex on the normalized string
|
|
1665
|
-
const m = /^\s*node\s+(--eval|-e)\s+([\s\S]+)$/i.exec(normalized);
|
|
1666
|
-
if (m && typeof m[1] === 'string' && typeof m[2] === 'string') {
|
|
1667
|
-
const evalFlag = m[1];
|
|
1668
|
-
let codeArg = m[2].trim();
|
|
1669
|
-
codeArg = stripOne(codeArg);
|
|
1670
|
-
const flag = evalFlag.startsWith('--') ? '--eval' : '-e';
|
|
1671
|
-
commandArg = ['node', flag, codeArg];
|
|
1672
|
-
}
|
|
1673
|
-
else {
|
|
1674
|
-
// Fallback: tokenize and detect node -e/--eval form
|
|
1675
|
-
const parts = tokenize(input);
|
|
1676
|
-
if (parts.length >= 3) {
|
|
1677
|
-
// Narrow under noUncheckedIndexedAccess
|
|
1678
|
-
const p0 = parts[0];
|
|
1679
|
-
const p1 = parts[1];
|
|
1680
|
-
if (p0?.toLowerCase() === 'node' &&
|
|
1681
|
-
(p1 === '-e' || p1 === '--eval')) {
|
|
1682
|
-
commandArg = parts;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
exitCode = await runCommand(commandArg, shellSetting, {
|
|
1688
|
-
env: buildSpawnEnv(process.env, {
|
|
1689
|
-
...dotenv,
|
|
1690
|
-
getDotenvCliOptions: JSON.stringify(envBag),
|
|
1691
|
-
}),
|
|
1692
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
1693
|
-
});
|
|
1694
|
-
dbg('run:done', { exitCode });
|
|
1695
|
-
}
|
|
1696
|
-
catch (err) {
|
|
1697
|
-
const code = typeof err.exitCode === 'number'
|
|
1698
|
-
? err.exitCode
|
|
1699
|
-
: 1;
|
|
1700
|
-
dbg('run:error', { exitCode: code, error: String(err) });
|
|
1701
|
-
if (!underTests) {
|
|
1702
|
-
dbg('process.exit (error path)', { exitCode: code });
|
|
1703
|
-
process.exit(code);
|
|
1704
|
-
}
|
|
1705
|
-
else {
|
|
1706
|
-
dbg('process.exit suppressed for tests (error path)', {
|
|
1707
|
-
exitCode: code,
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
if (!Number.isNaN(exitCode)) {
|
|
1713
|
-
dbg('process.exit', { exitCode });
|
|
1714
|
-
process.exit(exitCode);
|
|
1715
|
-
}
|
|
1716
|
-
// Fallback: Some environments may not surface a numeric exitCode even on success.
|
|
1717
|
-
// Always terminate alias-only invocations outside tests to avoid hanging the process,
|
|
1718
|
-
// regardless of capture/GETDOTENV_STDIO. Under tests, suppress to keep the runner alive.
|
|
1719
|
-
if (!underTests) {
|
|
1720
|
-
dbg('process.exit (fallback: non-numeric exitCode)', { exitCode: 0 });
|
|
1721
|
-
process.exit(0);
|
|
1722
|
-
}
|
|
1723
|
-
else {
|
|
1724
|
-
dbg('process.exit (fallback suppressed for tests: non-numeric exitCode)', { exitCode: 0 });
|
|
1725
|
-
}
|
|
1726
|
-
// Optional last-resort guard: force an exit on the next tick when enabled.
|
|
1727
|
-
// Intended for diagnosing environments where the process appears to linger
|
|
1728
|
-
// despite reaching the success/error handlers above. Disabled under tests.
|
|
1729
|
-
if (forceExit) {
|
|
1730
|
-
try {
|
|
1731
|
-
if (process.env.GETDOTENV_DEBUG_VERBOSE) {
|
|
1732
|
-
const getHandles = process._getActiveHandles;
|
|
1733
|
-
const handles = typeof getHandles === 'function' ? getHandles() : [];
|
|
1734
|
-
dbg('active handles before forced exit', {
|
|
1735
|
-
count: Array.isArray(handles) ? handles.length : undefined,
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
catch {
|
|
1740
|
-
// best-effort only
|
|
1741
|
-
}
|
|
1742
|
-
const code = Number.isNaN(exitCode) ? 0 : exitCode;
|
|
1743
|
-
dbg('process.exit (forced)', { exitCode: code });
|
|
1744
|
-
setImmediate(() => process.exit(code));
|
|
1745
|
-
}
|
|
1881
|
+
const aliasState = { handled: false };
|
|
1882
|
+
const maybeRun = async (thisCommand) => {
|
|
1883
|
+
await maybeRunAlias(cli, thisCommand, aliasKey, aliasState);
|
|
1746
1884
|
};
|
|
1747
|
-
// Execute alias-only invocations whether the root handles the action // itself (preAction) or Commander routes to a default subcommand (preSubcommand).
|
|
1748
1885
|
cli.hook('preAction', async (thisCommand, _actionCommand) => {
|
|
1749
|
-
await
|
|
1886
|
+
await maybeRun(thisCommand);
|
|
1750
1887
|
});
|
|
1751
1888
|
cli.hook('preSubcommand', async (thisCommand) => {
|
|
1752
|
-
await
|
|
1889
|
+
await maybeRun(thisCommand);
|
|
1753
1890
|
});
|
|
1754
1891
|
};
|
|
1755
1892
|
|
|
@@ -1771,10 +1908,10 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
1771
1908
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
1772
1909
|
};
|
|
1773
1910
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
1774
|
-
|
|
1775
|
-
|
|
1911
|
+
// Create as a GetDotenvCli child so helpInformation includes a trailing blank line.
|
|
1912
|
+
const cmd = cli
|
|
1913
|
+
.createCommand('cmd')
|
|
1776
1914
|
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
1777
|
-
.configureHelp({ showGlobalOptions: true })
|
|
1778
1915
|
.enablePositionalOptions()
|
|
1779
1916
|
.passThroughOptions()
|
|
1780
1917
|
.argument('[command...]')
|
|
@@ -1845,8 +1982,7 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
1845
1982
|
: 'unset';
|
|
1846
1983
|
// Apply presentation-time redaction (if enabled)
|
|
1847
1984
|
const redFlag = merged.redact;
|
|
1848
|
-
const redPatterns = merged
|
|
1849
|
-
.redactPatterns;
|
|
1985
|
+
const redPatterns = merged.redactPatterns;
|
|
1850
1986
|
const redOpts = {};
|
|
1851
1987
|
if (redFlag)
|
|
1852
1988
|
redOpts.redact = true;
|
|
@@ -1963,10 +2099,9 @@ const demoPlugin = () => definePlugin({
|
|
|
1963
2099
|
// Build a minimal node -e payload via argv array (avoid quoting issues).
|
|
1964
2100
|
const code = `console.log(process.env.${key} ?? "")`;
|
|
1965
2101
|
const ctx = cli.getCtx();
|
|
1966
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
1967
2102
|
// Inherit stdio for an interactive demo. Use --capture for CI.
|
|
1968
2103
|
await runCommand(['node', '-e', code], false, {
|
|
1969
|
-
env:
|
|
2104
|
+
env: buildSpawnEnv(process.env, ctx?.dotenv),
|
|
1970
2105
|
stdio: 'inherit',
|
|
1971
2106
|
});
|
|
1972
2107
|
});
|
|
@@ -2001,22 +2136,24 @@ const demoPlugin = () => definePlugin({
|
|
|
2001
2136
|
const shell = resolveShell(bag?.scripts, input, bag?.shell);
|
|
2002
2137
|
// Compose child env (parent + ctx.dotenv). This mirrors cmd/batch behavior.
|
|
2003
2138
|
const ctx = cli.getCtx();
|
|
2004
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
2005
2139
|
await runCommand(resolved, shell, {
|
|
2006
|
-
env:
|
|
2140
|
+
env: buildSpawnEnv(process.env, ctx?.dotenv),
|
|
2007
2141
|
stdio: 'inherit',
|
|
2008
2142
|
});
|
|
2009
2143
|
});
|
|
2010
2144
|
},
|
|
2011
2145
|
/**
|
|
2012
2146
|
* Optional: afterResolve can initialize per-plugin state using ctx.dotenv.
|
|
2013
|
-
* For the demo we
|
|
2147
|
+
* For the demo we emit a single breadcrumb only when GETDOTENV_DEBUG is set,
|
|
2148
|
+
* keeping default runs (tests/CI/smoke) quiet.
|
|
2014
2149
|
*/
|
|
2015
2150
|
afterResolve(_cli, ctx) {
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2151
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
2152
|
+
const keys = Object.keys(ctx.dotenv);
|
|
2153
|
+
if (keys.length > 0) {
|
|
2154
|
+
// Keep noise low; a single-line breadcrumb is sufficient for the demo.
|
|
2155
|
+
console.error('[demo] afterResolve: dotenv keys loaded:', keys.length);
|
|
2156
|
+
}
|
|
2020
2157
|
}
|
|
2021
2158
|
},
|
|
2022
2159
|
});
|