@karmaniverous/get-dotenv 6.0.0-0 → 6.0.0

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.
Files changed (93) hide show
  1. package/README.md +86 -334
  2. package/dist/cli.d.ts +569 -0
  3. package/dist/cli.mjs +18788 -0
  4. package/dist/cliHost.d.ts +548 -253
  5. package/dist/cliHost.mjs +1990 -1458
  6. package/dist/config.d.ts +192 -14
  7. package/dist/config.mjs +256 -81
  8. package/dist/env-overlay.d.ts +226 -18
  9. package/dist/env-overlay.mjs +181 -22
  10. package/dist/getdotenv.cli.mjs +18166 -3437
  11. package/dist/index.d.ts +729 -136
  12. package/dist/index.mjs +18207 -3457
  13. package/dist/plugins-aws.d.ts +289 -104
  14. package/dist/plugins-aws.mjs +2462 -350
  15. package/dist/plugins-batch.d.ts +355 -105
  16. package/dist/plugins-batch.mjs +2595 -420
  17. package/dist/plugins-cmd.d.ts +287 -118
  18. package/dist/plugins-cmd.mjs +2661 -839
  19. package/dist/plugins-init.d.ts +272 -100
  20. package/dist/plugins-init.mjs +2152 -37
  21. package/dist/plugins.d.ts +323 -140
  22. package/dist/plugins.mjs +18006 -2025
  23. package/dist/templates/cli/index.ts +26 -0
  24. package/dist/templates/cli/plugins/hello.ts +43 -0
  25. package/dist/templates/config/js/getdotenv.config.js +20 -0
  26. package/dist/templates/config/json/local/getdotenv.config.local.json +7 -0
  27. package/dist/templates/config/json/public/getdotenv.config.json +9 -0
  28. package/dist/templates/config/public/getdotenv.config.json +8 -0
  29. package/dist/templates/config/ts/getdotenv.config.ts +28 -0
  30. package/dist/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
  31. package/dist/templates/config/yaml/public/getdotenv.config.yaml +7 -0
  32. package/dist/templates/getdotenv.config.js +20 -0
  33. package/dist/templates/getdotenv.config.json +9 -0
  34. package/dist/templates/getdotenv.config.local.json +7 -0
  35. package/dist/templates/getdotenv.config.local.yaml +7 -0
  36. package/dist/templates/getdotenv.config.ts +28 -0
  37. package/dist/templates/getdotenv.config.yaml +7 -0
  38. package/dist/templates/hello.ts +43 -0
  39. package/dist/templates/index.ts +26 -0
  40. package/dist/templates/js/getdotenv.config.js +20 -0
  41. package/dist/templates/json/local/getdotenv.config.local.json +7 -0
  42. package/dist/templates/json/public/getdotenv.config.json +9 -0
  43. package/dist/templates/local/getdotenv.config.local.json +7 -0
  44. package/dist/templates/local/getdotenv.config.local.yaml +7 -0
  45. package/dist/templates/plugins/hello.ts +43 -0
  46. package/dist/templates/public/getdotenv.config.json +9 -0
  47. package/dist/templates/public/getdotenv.config.yaml +7 -0
  48. package/dist/templates/ts/getdotenv.config.ts +28 -0
  49. package/dist/templates/yaml/local/getdotenv.config.local.yaml +7 -0
  50. package/dist/templates/yaml/public/getdotenv.config.yaml +7 -0
  51. package/getdotenv.config.json +1 -19
  52. package/package.json +52 -89
  53. package/templates/cli/index.ts +26 -0
  54. package/templates/cli/plugins/hello.ts +43 -0
  55. package/templates/config/js/getdotenv.config.js +9 -4
  56. package/templates/config/json/public/getdotenv.config.json +0 -3
  57. package/templates/config/public/getdotenv.config.json +0 -5
  58. package/templates/config/ts/getdotenv.config.ts +17 -5
  59. package/templates/config/yaml/public/getdotenv.config.yaml +0 -3
  60. package/dist/cliHost.cjs +0 -2078
  61. package/dist/cliHost.d.cts +0 -451
  62. package/dist/cliHost.d.mts +0 -451
  63. package/dist/config.cjs +0 -252
  64. package/dist/config.d.cts +0 -55
  65. package/dist/config.d.mts +0 -55
  66. package/dist/env-overlay.cjs +0 -163
  67. package/dist/env-overlay.d.cts +0 -50
  68. package/dist/env-overlay.d.mts +0 -50
  69. package/dist/index.cjs +0 -4077
  70. package/dist/index.d.cts +0 -318
  71. package/dist/index.d.mts +0 -318
  72. package/dist/plugins-aws.cjs +0 -666
  73. package/dist/plugins-aws.d.cts +0 -158
  74. package/dist/plugins-aws.d.mts +0 -158
  75. package/dist/plugins-batch.cjs +0 -658
  76. package/dist/plugins-batch.d.cts +0 -181
  77. package/dist/plugins-batch.d.mts +0 -181
  78. package/dist/plugins-cmd.cjs +0 -1112
  79. package/dist/plugins-cmd.d.cts +0 -178
  80. package/dist/plugins-cmd.d.mts +0 -178
  81. package/dist/plugins-demo.cjs +0 -352
  82. package/dist/plugins-demo.d.cts +0 -158
  83. package/dist/plugins-demo.d.mts +0 -158
  84. package/dist/plugins-demo.d.ts +0 -158
  85. package/dist/plugins-demo.mjs +0 -350
  86. package/dist/plugins-init.cjs +0 -289
  87. package/dist/plugins-init.d.cts +0 -162
  88. package/dist/plugins-init.d.mts +0 -162
  89. package/dist/plugins.cjs +0 -2327
  90. package/dist/plugins.d.cts +0 -211
  91. package/dist/plugins.d.mts +0 -211
  92. package/templates/cli/ts/index.ts +0 -9
  93. package/templates/cli/ts/plugins/hello.ts +0 -17
package/dist/plugins.cjs DELETED
@@ -1,2327 +0,0 @@
1
- 'use strict';
2
-
3
- var execa = require('execa');
4
- var zod = require('zod');
5
- var commander = require('commander');
6
- var globby = require('globby');
7
- var packageDirectory = require('package-directory');
8
- var path = require('path');
9
- var fs = require('fs-extra');
10
- var node_process = require('node:process');
11
- var promises = require('readline/promises');
12
-
13
- // Minimal tokenizer for shell-off execution:
14
- // Splits by whitespace while preserving quoted segments (single or double quotes).
15
- const tokenize = (command) => {
16
- const out = [];
17
- let cur = '';
18
- let quote = null;
19
- for (let i = 0; i < command.length; i++) {
20
- const c = command.charAt(i);
21
- if (quote) {
22
- if (c === quote) {
23
- // Support doubled quotes inside a quoted segment (Windows/PowerShell style):
24
- // "" -> " and '' -> '
25
- const next = command.charAt(i + 1);
26
- if (next === quote) {
27
- cur += quote;
28
- i += 1; // skip the second quote
29
- }
30
- else {
31
- // end of quoted segment
32
- quote = null;
33
- }
34
- }
35
- else {
36
- cur += c;
37
- }
38
- }
39
- else {
40
- if (c === '"' || c === "'") {
41
- quote = c;
42
- }
43
- else if (/\s/.test(c)) {
44
- if (cur) {
45
- out.push(cur);
46
- cur = '';
47
- }
48
- }
49
- else {
50
- cur += c;
51
- }
52
- }
53
- }
54
- if (cur)
55
- out.push(cur);
56
- return out;
57
- };
58
-
59
- const dbg$1 = (...args) => {
60
- if (process.env.GETDOTENV_DEBUG) {
61
- // Use stderr to avoid interfering with stdout assertions
62
- console.error('[getdotenv:run]', ...args);
63
- }
64
- };
65
- // Strip repeated symmetric outer quotes (single or double) until stable.
66
- // This is safe for argv arrays passed to execa (no quoting needed) and avoids
67
- // passing quote characters through to Node (e.g., for `node -e "<code>"`).
68
- // Handles stacked quotes from shells like PowerShell: """code""" -> code.
69
- const stripOuterQuotes = (s) => {
70
- let out = s;
71
- // Repeatedly trim only when the entire string is wrapped in matching quotes.
72
- // Stop as soon as the ends are asymmetric or no quotes remain.
73
- while (out.length >= 2) {
74
- const a = out.charAt(0);
75
- const b = out.charAt(out.length - 1);
76
- const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
77
- if (!symmetric)
78
- break;
79
- out = out.slice(1, -1);
80
- }
81
- return out;
82
- };
83
- // Extract exitCode/stdout/stderr from execa result or error in a tolerant way.
84
- const pickResult = (r) => {
85
- const exit = r.exitCode;
86
- const stdoutVal = r.stdout;
87
- const stderrVal = r.stderr;
88
- return {
89
- exitCode: typeof exit === 'number' ? exit : Number.NaN,
90
- stdout: typeof stdoutVal === 'string' ? stdoutVal : '',
91
- stderr: typeof stderrVal === 'string' ? stderrVal : '',
92
- };
93
- };
94
- // Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
95
- // expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
96
- const sanitizeEnv = (env) => {
97
- if (!env)
98
- return undefined;
99
- const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
100
- return entries.length > 0 ? Object.fromEntries(entries) : undefined;
101
- };
102
- /**
103
- * Execute a command and capture stdout/stderr (buffered).
104
- * - Preserves plain vs shell behavior and argv/string normalization.
105
- * - Never re-emits stdout/stderr to parent; returns captured buffers.
106
- * - Supports optional timeout (ms).
107
- */
108
- const runCommandResult = async (command, shell, opts = {}) => {
109
- const envSan = sanitizeEnv(opts.env);
110
- {
111
- let file;
112
- let args = [];
113
- if (Array.isArray(command)) {
114
- file = command[0];
115
- args = command.slice(1).map(stripOuterQuotes);
116
- }
117
- else {
118
- const tokens = tokenize(command);
119
- file = tokens[0];
120
- args = tokens.slice(1);
121
- }
122
- if (!file)
123
- return { exitCode: 0, stdout: '', stderr: '' };
124
- dbg$1('exec:capture (plain)', { file, args });
125
- try {
126
- const result = await execa.execa(file, args, {
127
- ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
128
- ...(envSan !== undefined ? { env: envSan } : {}),
129
- stdio: 'pipe',
130
- ...(opts.timeoutMs !== undefined
131
- ? { timeout: opts.timeoutMs, killSignal: 'SIGKILL' }
132
- : {}),
133
- });
134
- const ok = pickResult(result);
135
- dbg$1('exit:capture (plain)', { exitCode: ok.exitCode });
136
- return ok;
137
- }
138
- catch (err) {
139
- const out = pickResult(err);
140
- dbg$1('exit:capture:error (plain)', { exitCode: out.exitCode });
141
- return out;
142
- }
143
- }
144
- };
145
- const runCommand = async (command, shell, opts) => {
146
- if (shell === false) {
147
- let file;
148
- let args = [];
149
- if (Array.isArray(command)) {
150
- file = command[0];
151
- args = command.slice(1).map(stripOuterQuotes);
152
- }
153
- else {
154
- const tokens = tokenize(command);
155
- file = tokens[0];
156
- args = tokens.slice(1);
157
- }
158
- if (!file)
159
- return 0;
160
- dbg$1('exec (plain)', { file, args, stdio: opts.stdio });
161
- // Build options without injecting undefined properties (exactOptionalPropertyTypes).
162
- const envSan = sanitizeEnv(opts.env);
163
- const plainOpts = {};
164
- if (opts.cwd !== undefined)
165
- plainOpts.cwd = opts.cwd;
166
- if (envSan !== undefined)
167
- plainOpts.env = envSan;
168
- if (opts.stdio !== undefined)
169
- plainOpts.stdio = opts.stdio;
170
- const result = await execa.execa(file, args, plainOpts);
171
- if (opts.stdio === 'pipe' && result.stdout) {
172
- process.stdout.write(result.stdout + (result.stdout.endsWith('\n') ? '' : '\n'));
173
- }
174
- const exit = result?.exitCode;
175
- dbg$1('exit (plain)', { exitCode: exit });
176
- return typeof exit === 'number' ? exit : Number.NaN;
177
- }
178
- else {
179
- const commandStr = Array.isArray(command) ? command.join(' ') : command;
180
- dbg$1('exec (shell)', {
181
- shell: typeof shell === 'string' ? shell : 'custom',
182
- stdio: opts.stdio,
183
- command: commandStr,
184
- });
185
- const envSan = sanitizeEnv(opts.env);
186
- const shellOpts = { shell };
187
- if (opts.cwd !== undefined)
188
- shellOpts.cwd = opts.cwd;
189
- if (envSan !== undefined)
190
- shellOpts.env = envSan;
191
- if (opts.stdio !== undefined)
192
- shellOpts.stdio = opts.stdio;
193
- const result = await execa.execaCommand(commandStr, shellOpts);
194
- const out = result?.stdout;
195
- if (opts.stdio === 'pipe' && out) {
196
- process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
197
- }
198
- const exit = result?.exitCode;
199
- dbg$1('exit (shell)', { exitCode: exit });
200
- return typeof exit === 'number' ? exit : Number.NaN;
201
- }
202
- };
203
-
204
- const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
205
- /** Build a sanitized env for child processes from base + overlay. */
206
- const buildSpawnEnv = (base, overlay) => {
207
- const raw = {
208
- ...(base ?? {}),
209
- ...(overlay ?? {}),
210
- };
211
- // Drop undefined first
212
- const entries = Object.entries(dropUndefined(raw));
213
- if (process.platform === 'win32') {
214
- // Windows: keys are case-insensitive; collapse duplicates
215
- const byLower = new Map();
216
- for (const [k, v] of entries) {
217
- byLower.set(k.toLowerCase(), [k, v]); // last wins; preserve latest casing
218
- }
219
- const out = {};
220
- for (const [, [k, v]] of byLower)
221
- out[k] = v;
222
- // HOME fallback from USERPROFILE (common expectation)
223
- if (!Object.prototype.hasOwnProperty.call(out, 'HOME')) {
224
- const up = out['USERPROFILE'];
225
- if (typeof up === 'string' && up.length > 0)
226
- out['HOME'] = up;
227
- }
228
- // Normalize TMP/TEMP coherence (pick any present; reflect to both)
229
- const tmp = out['TMP'] ?? out['TEMP'];
230
- if (typeof tmp === 'string' && tmp.length > 0) {
231
- out['TMP'] = tmp;
232
- out['TEMP'] = tmp;
233
- }
234
- return out;
235
- }
236
- // POSIX: keep keys as-is
237
- const out = Object.fromEntries(entries);
238
- // Ensure TMPDIR exists when any temp key is present (best-effort)
239
- const tmpdir = out['TMPDIR'] ?? out['TMP'] ?? out['TEMP'];
240
- if (typeof tmpdir === 'string' && tmpdir.length > 0) {
241
- out['TMPDIR'] = tmpdir;
242
- }
243
- return out;
244
- };
245
-
246
- /** src/cliHost/definePlugin.ts
247
- * Plugin contracts for the GetDotenv CLI host.
248
- *
249
- * This module exposes a structural public interface for the host that plugins
250
- * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
251
- * nominal class identity issues (private fields) in downstream consumers.
252
- */
253
- /**
254
- * Define a GetDotenv CLI plugin with compositional helpers.
255
- *
256
- * @example
257
- * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
258
- * .use(childA)
259
- * .use(childB);
260
- */
261
- const definePlugin = (spec) => {
262
- const { children = [], ...rest } = spec;
263
- const plugin = {
264
- ...rest,
265
- children: [...children],
266
- use(child) {
267
- this.children.push(child);
268
- return this;
269
- },
270
- };
271
- return plugin;
272
- };
273
-
274
- /**
275
- * Batch services (neutral): resolve command and shell settings.
276
- * Shared by the generator path and the batch plugin to avoid circular deps.
277
- */
278
- /**
279
- * Resolve a command string from the {@link Scripts} table.
280
- * A script may be expressed as a string or an object with a `cmd` property.
281
- *
282
- * @param scripts - Optional scripts table.
283
- * @param command - User-provided command name or string.
284
- * @returns Resolved command string (falls back to the provided command).
285
- */
286
- const resolveCommand = (scripts, command) => scripts && typeof scripts[command] === 'object'
287
- ? scripts[command].cmd
288
- : (scripts?.[command] ?? command);
289
- /**
290
- * Resolve the shell setting for a given command:
291
- * - If the script entry is an object, prefer its `shell` override.
292
- * - Otherwise use the provided `shell` (string | boolean).
293
- *
294
- * @param scripts - Optional scripts table.
295
- * @param command - User-provided command name or string.
296
- * @param shell - Global shell preference (string | boolean).
297
- */
298
- const resolveShell = (scripts, command, shell) => scripts && typeof scripts[command] === 'object'
299
- ? (scripts[command].shell ?? false)
300
- : (shell ?? false);
301
-
302
- const DEFAULT_TIMEOUT_MS = 15_000;
303
- const trim = (s) => (typeof s === 'string' ? s.trim() : '');
304
- const unquote = (s) => s.length >= 2 &&
305
- ((s.startsWith('"') && s.endsWith('"')) ||
306
- (s.startsWith("'") && s.endsWith("'")))
307
- ? s.slice(1, -1)
308
- : s;
309
- const parseExportCredentialsJson = (txt) => {
310
- try {
311
- const obj = JSON.parse(txt);
312
- const src = obj.Credentials ?? obj;
313
- const ak = src.AccessKeyId;
314
- const sk = src.SecretAccessKey;
315
- const tk = src.SessionToken;
316
- if (ak && sk)
317
- return {
318
- accessKeyId: ak,
319
- secretAccessKey: sk,
320
- ...(tk ? { sessionToken: tk } : {}),
321
- };
322
- }
323
- catch {
324
- /* ignore */
325
- }
326
- return undefined;
327
- };
328
- const parseExportCredentialsEnv = (txt) => {
329
- const lines = txt.split(/\r?\n/);
330
- let id;
331
- let secret;
332
- let token;
333
- for (const raw of lines) {
334
- const line = raw.trim();
335
- if (!line)
336
- continue;
337
- // POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
338
- let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
339
- if (!m) {
340
- // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
341
- m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
342
- }
343
- if (!m)
344
- continue;
345
- const k = m[1];
346
- const valRaw = m[2];
347
- if (typeof valRaw !== 'string')
348
- continue;
349
- let v = unquote(valRaw.trim());
350
- // Drop trailing semicolons if present (some shells)
351
- v = v.replace(/;$/, '');
352
- if (k === 'AWS_ACCESS_KEY_ID')
353
- id = v;
354
- else if (k === 'AWS_SECRET_ACCESS_KEY')
355
- secret = v;
356
- else if (k === 'AWS_SESSION_TOKEN')
357
- token = v;
358
- }
359
- if (id && secret)
360
- return {
361
- accessKeyId: id,
362
- secretAccessKey: secret,
363
- ...(token ? { sessionToken: token } : {}),
364
- };
365
- return undefined;
366
- };
367
- const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
368
- const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
369
- env: process.env,
370
- timeoutMs,
371
- });
372
- // Guard for mocked undefined in tests; keep narrow lint scope.
373
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
374
- if (!r || typeof r.exitCode !== 'number')
375
- return undefined;
376
- if (r.exitCode === 0) {
377
- const v = trim(r.stdout);
378
- return v.length > 0 ? v : undefined;
379
- }
380
- return undefined;
381
- };
382
- const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
383
- // Try JSON format first (AWS CLI v2)
384
- const rJson = await runCommandResult([
385
- 'aws',
386
- 'configure',
387
- 'export-credentials',
388
- '--profile',
389
- profile,
390
- '--format',
391
- 'json',
392
- ], false, { env: process.env, timeoutMs });
393
- if (rJson.exitCode === 0) {
394
- const creds = parseExportCredentialsJson(rJson.stdout);
395
- if (creds)
396
- return creds;
397
- }
398
- // Fallback: env lines
399
- const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
400
- if (rEnv.exitCode === 0) {
401
- const creds = parseExportCredentialsEnv(rEnv.stdout);
402
- if (creds)
403
- return creds;
404
- }
405
- return undefined;
406
- };
407
- const resolveAwsContext = async ({ dotenv, cfg, }) => {
408
- const profileKey = cfg.profileKey ?? 'AWS_LOCAL_PROFILE';
409
- const profileFallbackKey = cfg.profileFallbackKey ?? 'AWS_PROFILE';
410
- const regionKey = cfg.regionKey ?? 'AWS_REGION';
411
- const profile = cfg.profile ??
412
- dotenv[profileKey] ??
413
- dotenv[profileFallbackKey] ??
414
- undefined;
415
- let region = cfg.region ?? dotenv[regionKey] ?? undefined;
416
- // Short-circuit when strategy is disabled.
417
- if (cfg.strategy === 'none') {
418
- // If region is still missing and we have a profile, try best-effort region resolve.
419
- if (!region && profile)
420
- region = await getAwsConfigure('region', profile);
421
- if (!region && cfg.defaultRegion)
422
- region = cfg.defaultRegion;
423
- const out = {};
424
- if (profile !== undefined)
425
- out.profile = profile;
426
- if (region !== undefined)
427
- out.region = region;
428
- return out;
429
- }
430
- // Env-first credentials.
431
- let credentials;
432
- const envId = trim(process.env.AWS_ACCESS_KEY_ID);
433
- const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
434
- const envToken = trim(process.env.AWS_SESSION_TOKEN);
435
- if (envId && envSecret) {
436
- credentials = {
437
- accessKeyId: envId,
438
- secretAccessKey: envSecret,
439
- ...(envToken ? { sessionToken: envToken } : {}),
440
- };
441
- }
442
- else if (profile) {
443
- // Try export-credentials
444
- credentials = await exportCredentials(profile);
445
- // On failure, detect SSO and optionally login then retry
446
- if (!credentials) {
447
- const ssoSession = await getAwsConfigure('sso_session', profile);
448
- const looksSSO = typeof ssoSession === 'string' && ssoSession.length > 0;
449
- if (looksSSO && cfg.loginOnDemand) {
450
- // Best-effort login, then retry export once.
451
- await runCommandResult(['aws', 'sso', 'login', '--profile', profile], false, {
452
- env: process.env,
453
- timeoutMs: DEFAULT_TIMEOUT_MS,
454
- });
455
- credentials = await exportCredentials(profile);
456
- }
457
- }
458
- // Static fallback if still missing.
459
- if (!credentials) {
460
- const id = await getAwsConfigure('aws_access_key_id', profile);
461
- const secret = await getAwsConfigure('aws_secret_access_key', profile);
462
- const token = await getAwsConfigure('aws_session_token', profile);
463
- if (id && secret) {
464
- credentials = {
465
- accessKeyId: id,
466
- secretAccessKey: secret,
467
- ...(token ? { sessionToken: token } : {}),
468
- };
469
- }
470
- }
471
- }
472
- // Final region resolution
473
- if (!region && profile)
474
- region = await getAwsConfigure('region', profile);
475
- if (!region && cfg.defaultRegion)
476
- region = cfg.defaultRegion;
477
- const out = {};
478
- if (profile !== undefined)
479
- out.profile = profile;
480
- if (region !== undefined)
481
- out.region = region;
482
- if (credentials)
483
- out.credentials = credentials;
484
- return out;
485
- };
486
-
487
- const AwsPluginConfigSchema = zod.z.object({
488
- profile: zod.z.string().optional(),
489
- region: zod.z.string().optional(),
490
- defaultRegion: zod.z.string().optional(),
491
- profileKey: zod.z.string().default('AWS_LOCAL_PROFILE').optional(),
492
- profileFallbackKey: zod.z.string().default('AWS_PROFILE').optional(),
493
- regionKey: zod.z.string().default('AWS_REGION').optional(),
494
- strategy: zod.z.enum(['cli-export', 'none']).default('cli-export').optional(),
495
- loginOnDemand: zod.z.boolean().default(false).optional(),
496
- setEnv: zod.z.boolean().default(true).optional(),
497
- addCtx: zod.z.boolean().default(true).optional(),
498
- });
499
-
500
- const awsPlugin = () => definePlugin({
501
- id: 'aws',
502
- // Host validates this slice when the loader path is active.
503
- configSchema: AwsPluginConfigSchema,
504
- setup(cli) {
505
- // Subcommand: aws
506
- cli
507
- .ns('aws')
508
- .description('Establish an AWS session and optionally forward to the AWS CLI')
509
- .enablePositionalOptions()
510
- .passThroughOptions()
511
- .allowUnknownOption(true)
512
- // Boolean toggles
513
- .option('--login-on-demand', 'attempt aws sso login on-demand')
514
- .option('--no-login-on-demand', 'disable sso login on-demand')
515
- .option('--set-env', 'write resolved values into process.env')
516
- .option('--no-set-env', 'do not write resolved values into process.env')
517
- .option('--add-ctx', 'mirror results under ctx.plugins.aws')
518
- .option('--no-add-ctx', 'do not mirror results under ctx.plugins.aws')
519
- // Strings / enums
520
- .option('--profile <string>', 'AWS profile name')
521
- .option('--region <string>', 'AWS region')
522
- .option('--default-region <string>', 'fallback region')
523
- .option('--strategy <string>', 'credential acquisition strategy: cli-export|none')
524
- // Advanced key overrides
525
- .option('--profile-key <string>', 'dotenv/config key for local profile')
526
- .option('--profile-fallback-key <string>', 'fallback dotenv/config key for profile')
527
- .option('--region-key <string>', 'dotenv/config key for region')
528
- // Accept any extra operands so Commander does not error when tokens appear after "--".
529
- .argument('[args...]')
530
- .action(async (args, opts, thisCommand) => {
531
- const self = thisCommand;
532
- const parent = (self.parent ?? null);
533
- // Access merged root CLI options (installed by passOptions())
534
- const rootOpts = (parent?.getDotenvCliOptions ?? {});
535
- const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
536
- Boolean(rootOpts?.capture);
537
- const underTests = process.env.GETDOTENV_TEST === '1' ||
538
- typeof process.env.VITEST_WORKER_ID === 'string';
539
- // Build overlay cfg from subcommand flags layered over discovered config.
540
- const ctx = cli.getCtx();
541
- const cfgBase = (ctx?.pluginConfigs?.['aws'] ??
542
- {});
543
- const overlay = {};
544
- // Map boolean toggles (respect explicit --no-*)
545
- if (Object.prototype.hasOwnProperty.call(opts, 'loginOnDemand'))
546
- overlay.loginOnDemand = Boolean(opts.loginOnDemand);
547
- if (Object.prototype.hasOwnProperty.call(opts, 'setEnv'))
548
- overlay.setEnv = Boolean(opts.setEnv);
549
- if (Object.prototype.hasOwnProperty.call(opts, 'addCtx'))
550
- overlay.addCtx = Boolean(opts.addCtx);
551
- // Strings/enums
552
- if (typeof opts.profile === 'string')
553
- overlay.profile = opts.profile;
554
- if (typeof opts.region === 'string')
555
- overlay.region = opts.region;
556
- if (typeof opts.defaultRegion === 'string')
557
- overlay.defaultRegion = opts.defaultRegion;
558
- if (typeof opts.strategy === 'string')
559
- overlay.strategy =
560
- opts.strategy;
561
- // Advanced key overrides
562
- if (typeof opts.profileKey === 'string')
563
- overlay.profileKey = opts.profileKey;
564
- if (typeof opts.profileFallbackKey === 'string')
565
- overlay.profileFallbackKey = opts.profileFallbackKey;
566
- if (typeof opts.regionKey === 'string')
567
- overlay.regionKey = opts.regionKey;
568
- const cfg = {
569
- ...cfgBase,
570
- ...overlay,
571
- };
572
- // Resolve current context with overrides
573
- const out = await resolveAwsContext({
574
- dotenv: ctx?.dotenv ?? {},
575
- cfg,
576
- });
577
- // Apply env/ctx mirrors per toggles
578
- if (cfg.setEnv !== false) {
579
- if (out.region) {
580
- process.env.AWS_REGION = out.region;
581
- if (!process.env.AWS_DEFAULT_REGION)
582
- process.env.AWS_DEFAULT_REGION = out.region;
583
- }
584
- if (out.credentials) {
585
- process.env.AWS_ACCESS_KEY_ID = out.credentials.accessKeyId;
586
- process.env.AWS_SECRET_ACCESS_KEY =
587
- out.credentials.secretAccessKey;
588
- if (out.credentials.sessionToken !== undefined) {
589
- process.env.AWS_SESSION_TOKEN = out.credentials.sessionToken;
590
- }
591
- }
592
- }
593
- if (cfg.addCtx !== false) {
594
- if (ctx) {
595
- ctx.plugins ??= {};
596
- ctx.plugins['aws'] = {
597
- ...(out.profile ? { profile: out.profile } : {}),
598
- ...(out.region ? { region: out.region } : {}),
599
- ...(out.credentials ? { credentials: out.credentials } : {}),
600
- };
601
- }
602
- }
603
- // Forward when positional args are present; otherwise session-only.
604
- if (Array.isArray(args) && args.length > 0) {
605
- const argv = ['aws', ...args];
606
- const shellSetting = resolveShell(rootOpts?.scripts, 'aws', rootOpts?.shell);
607
- const ctxDotenv = (ctx?.dotenv ?? {});
608
- const exit = await runCommand(argv, shellSetting, {
609
- env: buildSpawnEnv(process.env, ctxDotenv),
610
- stdio: capture ? 'pipe' : 'inherit',
611
- });
612
- // Deterministic termination (suppressed under tests)
613
- if (!underTests) {
614
- process.exit(typeof exit === 'number' ? exit : 0);
615
- }
616
- return;
617
- }
618
- else {
619
- // Session only: low-noise breadcrumb under debug
620
- if (process.env.GETDOTENV_DEBUG) {
621
- const log = console;
622
- log.log('[aws] session established', {
623
- profile: out.profile,
624
- region: out.region,
625
- hasCreds: Boolean(out.credentials),
626
- });
627
- }
628
- if (!underTests)
629
- process.exit(0);
630
- return;
631
- }
632
- });
633
- },
634
- async afterResolve(_cli, ctx) {
635
- const log = console;
636
- const cfgRaw = (ctx.pluginConfigs?.['aws'] ?? {});
637
- const cfg = (cfgRaw || {});
638
- const out = await resolveAwsContext({
639
- dotenv: ctx.dotenv,
640
- cfg,
641
- });
642
- const { profile, region, credentials } = out;
643
- if (cfg.setEnv !== false) {
644
- if (region) {
645
- process.env.AWS_REGION = region;
646
- if (!process.env.AWS_DEFAULT_REGION)
647
- process.env.AWS_DEFAULT_REGION = region;
648
- }
649
- if (credentials) {
650
- process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
651
- process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
652
- if (credentials.sessionToken !== undefined) {
653
- process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
654
- }
655
- }
656
- }
657
- if (cfg.addCtx !== false) {
658
- ctx.plugins ??= {};
659
- ctx.plugins['aws'] = {
660
- ...(profile ? { profile } : {}),
661
- ...(region ? { region } : {}),
662
- ...(credentials ? { credentials } : {}),
663
- };
664
- }
665
- // Optional: low-noise breadcrumb for diagnostics
666
- if (process.env.GETDOTENV_DEBUG) {
667
- log.log('[aws] afterResolve', {
668
- profile,
669
- region,
670
- hasCreds: Boolean(credentials),
671
- });
672
- }
673
- },
674
- });
675
-
676
- const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
677
- let cwd = process.cwd();
678
- if (pkgCwd) {
679
- const pkgDir = await packageDirectory.packageDirectory();
680
- if (!pkgDir) {
681
- logger.error('No package directory found.');
682
- process.exit(0);
683
- }
684
- cwd = pkgDir;
685
- }
686
- const absRootPath = path.posix.join(cwd.split(path.sep).join(path.posix.sep), rootPath.split(path.sep).join(path.posix.sep));
687
- const paths = await globby.globby(globs.split(/\s+/), {
688
- cwd: absRootPath,
689
- expandDirectories: false,
690
- onlyDirectories: true,
691
- absolute: true,
692
- });
693
- if (!paths.length) {
694
- logger.error(`No paths found for globs '${globs}' at '${absRootPath}'.`);
695
- process.exit(0);
696
- }
697
- return { absRootPath, paths };
698
- };
699
- const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
700
- const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
701
- Boolean(getDotenvCliOptions?.capture);
702
- // Require a command only when not listing. In list mode, a command is optional.
703
- if (!command && !list) {
704
- logger.error(`No command provided. Use --command or --list.`);
705
- process.exit(0);
706
- }
707
- const { absRootPath, paths } = await globPaths({
708
- globs,
709
- logger,
710
- rootPath,
711
- // exactOptionalPropertyTypes: only include when defined
712
- ...(pkgCwd !== undefined ? { pkgCwd } : {}),
713
- });
714
- const headerTitle = list
715
- ? 'Listing working directories...'
716
- : 'Executing command batch...';
717
- logger.info('');
718
- const headerRootPath = `ROOT: ${absRootPath}`;
719
- const headerGlobs = `GLOBS: ${globs}`;
720
- // Prepare a safe label for the header (avoid undefined in template)
721
- const commandLabel = Array.isArray(command)
722
- ? command.join(' ')
723
- : typeof command === 'string' && command.length > 0
724
- ? command
725
- : '';
726
- const headerCommand = list ? `CMD: (list only)` : `CMD: ${commandLabel}`;
727
- logger.info('*'.repeat(Math.max(headerTitle.length, headerRootPath.length, headerGlobs.length, headerCommand.length)));
728
- logger.info(headerTitle);
729
- logger.info('');
730
- logger.info(headerRootPath);
731
- logger.info(headerGlobs);
732
- logger.info(headerCommand);
733
- for (const path of paths) {
734
- // Write path and command to console.
735
- const pathLabel = `CWD: ${path}`;
736
- if (list) {
737
- logger.info(pathLabel);
738
- continue;
739
- }
740
- logger.info('');
741
- logger.info('*'.repeat(pathLabel.length));
742
- logger.info(pathLabel);
743
- logger.info(headerCommand);
744
- // Execute command.
745
- try {
746
- const hasCmd = (typeof command === 'string' && command.length > 0) ||
747
- (Array.isArray(command) && command.length > 0);
748
- if (hasCmd) {
749
- // Compose child env overlay from dotenv (drop undefined) and merged options
750
- const overlay = {};
751
- if (dotenvEnv) {
752
- for (const [k, v] of Object.entries(dotenvEnv)) {
753
- if (typeof v === 'string')
754
- overlay[k] = v;
755
- }
756
- }
757
- if (getDotenvCliOptions !== undefined) {
758
- try {
759
- overlay.getDotenvCliOptions = JSON.stringify(getDotenvCliOptions);
760
- }
761
- catch {
762
- // best-effort: omit if serialization fails
763
- }
764
- }
765
- await runCommand(command, shell, {
766
- cwd: path,
767
- env: buildSpawnEnv(process.env, overlay),
768
- stdio: capture ? 'pipe' : 'inherit',
769
- });
770
- }
771
- else {
772
- // Should not occur due to the early guard; retain for type safety.
773
- logger.error(`No command provided. Use --command or --list.`);
774
- process.exit(0);
775
- }
776
- }
777
- catch (error) {
778
- if (!ignoreErrors) {
779
- throw error;
780
- }
781
- }
782
- }
783
- logger.info('');
784
- };
785
-
786
- /**
787
- * Build the default "cmd" subcommand action for the batch plugin.
788
- * Mirrors the original inline implementation with identical behavior.
789
- */
790
- const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _subOpts, _thisCommand) => {
791
- const loggerLocal = opts.logger ?? console;
792
- // Guard: when invoked without positional args (e.g., `batch --list`),
793
- // defer entirely to the parent action handler.
794
- const argsRaw = Array.isArray(commandParts)
795
- ? commandParts
796
- : [];
797
- const localList = argsRaw.includes('-l') || argsRaw.includes('--list');
798
- const args = localList
799
- ? argsRaw.filter((t) => t !== '-l' && t !== '--list')
800
- : argsRaw;
801
- // Access merged per-plugin config from host context (if any).
802
- const ctx = cli.getCtx();
803
- const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
804
- const cfg = (cfgRaw || {});
805
- const dotenvEnv = (ctx?.dotenv ?? {});
806
- // Resolve batch flags from the captured parent (batch) command.
807
- const raw = batchCmd.opts();
808
- const listFromParent = !!raw.list;
809
- const ignoreErrors = !!raw.ignoreErrors;
810
- const globs = typeof raw.globs === 'string' ? raw.globs : (cfg.globs ?? '*');
811
- const pkgCwd = raw.pkgCwd !== undefined ? !!raw.pkgCwd : !!cfg.pkgCwd;
812
- const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : (cfg.rootPath ?? './');
813
- // Resolve scripts/shell with precedence:
814
- // plugin opts → plugin config → merged root CLI options
815
- const mergedBag = ((batchCmd.parent ?? null)?.getDotenvCliOptions ?? {});
816
- const scripts = opts.scripts ?? cfg.scripts ?? mergedBag.scripts;
817
- const shell = opts.shell ?? cfg.shell ?? mergedBag.shell;
818
- // If no positional args were given, bridge to --command/--list paths here.
819
- if (args.length === 0) {
820
- const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
821
- if (typeof commandOpt === 'string') {
822
- await execShellCommandBatch({
823
- command: resolveCommand(scripts, commandOpt),
824
- dotenvEnv,
825
- globs,
826
- ignoreErrors,
827
- list: false,
828
- logger: loggerLocal,
829
- ...(pkgCwd ? { pkgCwd } : {}),
830
- rootPath,
831
- shell: resolveShell(scripts, commandOpt, shell),
832
- });
833
- return;
834
- }
835
- if (raw.list || localList) {
836
- const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
837
- await execShellCommandBatch({
838
- globs,
839
- ignoreErrors,
840
- list: true,
841
- logger: loggerLocal,
842
- ...(pkgCwd ? { pkgCwd } : {}),
843
- rootPath,
844
- shell: (shell ?? shellBag.shell ?? false),
845
- });
846
- return;
847
- }
848
- {
849
- const lr = loggerLocal;
850
- const emit = lr.error ?? lr.log;
851
- emit(`No command provided. Use --command or --list.`);
852
- }
853
- process.exit(0);
854
- }
855
- // If a local list flag was supplied with positional tokens (and no --command),
856
- // treat tokens as additional globs and execute list mode.
857
- if (localList && typeof raw.command !== 'string') {
858
- const extraGlobs = args.map(String).join(' ').trim();
859
- const mergedGlobs = [globs, extraGlobs].filter(Boolean).join(' ');
860
- const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
861
- await execShellCommandBatch({
862
- globs: mergedGlobs,
863
- ignoreErrors,
864
- list: true,
865
- logger: loggerLocal,
866
- ...(pkgCwd ? { pkgCwd } : {}),
867
- rootPath,
868
- shell: (shell ?? shellBag.shell ?? false),
869
- });
870
- return;
871
- }
872
- // If parent list flag is set and positional tokens are present (and no --command),
873
- // treat tokens as additional globs for list-only mode.
874
- if (listFromParent && args.length > 0 && typeof raw.command !== 'string') {
875
- const extra = args.map(String).join(' ').trim();
876
- const mergedGlobs = [globs, extra].filter(Boolean).join(' ');
877
- const mergedBag2 = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
878
- await execShellCommandBatch({
879
- globs: mergedGlobs,
880
- ignoreErrors,
881
- list: true,
882
- logger: loggerLocal,
883
- ...(pkgCwd ? { pkgCwd } : {}),
884
- rootPath,
885
- shell: (shell ?? mergedBag2.shell ?? false),
886
- });
887
- return;
888
- }
889
- // Join positional args as the command to execute.
890
- const input = args.map(String).join(' ');
891
- // Optional: round-trip parent merged options if present (shipped CLI).
892
- const envBag = (batchCmd.parent ?? undefined)?.getDotenvCliOptions;
893
- const mergedExec = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
894
- const scriptsExec = scripts ?? mergedExec.scripts;
895
- const shellExec = shell ?? mergedExec.shell;
896
- const resolved = resolveCommand(scriptsExec, input);
897
- const shellSetting = resolveShell(scriptsExec, input, shellExec);
898
- // Preserve argv array only for shell-off Node -e snippets to avoid
899
- // lossy re-tokenization (Windows/PowerShell quoting). For simple
900
- // commands (e.g., "echo OK") keep string form to satisfy unit tests.
901
- let commandArg = resolved;
902
- if (shellSetting === false && resolved === input) {
903
- const first = (args[0] ?? '').toLowerCase();
904
- const hasEval = args.includes('-e') || args.includes('--eval');
905
- if (first === 'node' && hasEval) {
906
- commandArg = args.map(String);
907
- }
908
- }
909
- await execShellCommandBatch({
910
- command: commandArg,
911
- dotenvEnv,
912
- ...(envBag ? { getDotenvCliOptions: envBag } : {}),
913
- globs,
914
- ignoreErrors,
915
- list: false,
916
- logger: loggerLocal,
917
- ...(pkgCwd ? { pkgCwd } : {}),
918
- rootPath,
919
- shell: shellSetting,
920
- });
921
- };
922
-
923
- /**
924
- * Build the parent "batch" action handler (no explicit subcommand).
925
- */
926
- const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
927
- const logger = opts.logger ?? console;
928
- // Ensure context exists (host preSubcommand on root creates if missing).
929
- const ctx = cli.getCtx();
930
- const dotenvEnv = (ctx?.dotenv ?? {});
931
- const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
932
- const cfg = (cfgRaw || {});
933
- const raw = thisCommand.opts();
934
- const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
935
- const ignoreErrors = !!raw.ignoreErrors;
936
- let globs = typeof raw.globs === 'string' ? raw.globs : (cfg.globs ?? '*');
937
- const list = !!raw.list;
938
- const pkgCwd = raw.pkgCwd !== undefined ? !!raw.pkgCwd : !!cfg.pkgCwd;
939
- const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : (cfg.rootPath ?? './');
940
- // Treat parent positional tokens as the command when no explicit 'cmd' is used.
941
- const argsParent = Array.isArray(commandParts) ? commandParts : [];
942
- if (argsParent.length > 0 && !list) {
943
- const input = argsParent.map(String).join(' ');
944
- const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
945
- const scriptsAll = opts.scripts ?? cfg.scripts ?? mergedBag.scripts;
946
- const shellAll = opts.shell ?? cfg.shell ?? mergedBag.shell;
947
- const resolved = resolveCommand(scriptsAll, input);
948
- const shellSetting = resolveShell(scriptsAll, input, shellAll);
949
- // Parent path: pass a string; executor handles shell-specific details.
950
- const commandArg = resolved;
951
- await execShellCommandBatch({
952
- command: commandArg,
953
- dotenvEnv,
954
- globs,
955
- ignoreErrors,
956
- list: false,
957
- logger,
958
- ...(pkgCwd ? { pkgCwd } : {}),
959
- rootPath,
960
- shell: shellSetting,
961
- });
962
- return;
963
- }
964
- // List-only: merge extra positional tokens into globs when no --command is present.
965
- if (list && argsParent.length > 0 && !commandOpt) {
966
- const extra = argsParent.map(String).join(' ').trim();
967
- if (extra.length > 0)
968
- globs = [globs, extra].filter(Boolean).join(' ');
969
- const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
970
- await execShellCommandBatch({
971
- globs,
972
- ignoreErrors,
973
- list: true,
974
- logger,
975
- ...(pkgCwd ? { pkgCwd } : {}),
976
- rootPath,
977
- shell: (opts.shell ?? cfg.shell ?? mergedBag.shell ?? false),
978
- });
979
- return;
980
- }
981
- if (!commandOpt && !list) {
982
- logger.error(`No command provided. Use --command or --list.`);
983
- process.exit(0);
984
- }
985
- if (typeof commandOpt === 'string') {
986
- const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
987
- const scriptsOpt = opts.scripts ?? cfg.scripts ?? mergedBag.scripts;
988
- const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
989
- await execShellCommandBatch({
990
- command: resolveCommand(scriptsOpt, commandOpt),
991
- dotenvEnv,
992
- globs,
993
- ignoreErrors,
994
- list,
995
- logger,
996
- ...(pkgCwd ? { pkgCwd } : {}),
997
- rootPath,
998
- shell: resolveShell(scriptsOpt, commandOpt, shellOpt),
999
- });
1000
- return;
1001
- }
1002
- // list only (explicit --list without --command)
1003
- const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
1004
- const shellOnly = (opts.shell ?? cfg.shell ?? mergedBag.shell ?? false);
1005
- await execShellCommandBatch({
1006
- globs,
1007
- ignoreErrors,
1008
- list: true,
1009
- logger,
1010
- ...(pkgCwd ? { pkgCwd } : {}),
1011
- rootPath,
1012
- shell: (shellOnly ?? false),
1013
- });
1014
- };
1015
-
1016
- // Per-plugin config schema (optional fields; used as defaults).
1017
- const ScriptSchema = zod.z.union([
1018
- zod.z.string(),
1019
- zod.z.object({
1020
- cmd: zod.z.string(),
1021
- shell: zod.z.union([zod.z.string(), zod.z.boolean()]).optional(),
1022
- }),
1023
- ]);
1024
- const BatchConfigSchema = zod.z.object({
1025
- scripts: zod.z.record(zod.z.string(), ScriptSchema).optional(),
1026
- shell: zod.z.union([zod.z.string(), zod.z.boolean()]).optional(),
1027
- rootPath: zod.z.string().optional(),
1028
- globs: zod.z.string().optional(),
1029
- pkgCwd: zod.z.boolean().optional(),
1030
- });
1031
-
1032
- /**
1033
- * Batch plugin for the GetDotenv CLI host.
1034
- *
1035
- * Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
1036
- * Options:
1037
- * - scripts/shell: used to resolve command and shell behavior per script or global default.
1038
- * - logger: defaults to console.
1039
- */
1040
- const batchPlugin = (opts = {}) => definePlugin({
1041
- id: 'batch',
1042
- // Host validates this when config-loader is enabled; plugins may also
1043
- // re-validate at action time as a safety belt.
1044
- configSchema: BatchConfigSchema,
1045
- setup(cli) {
1046
- const ns = cli.ns('batch');
1047
- const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
1048
- const host = cli;
1049
- const pluginId = 'batch';
1050
- const GROUP = `plugin:${pluginId}`;
1051
- ns.description('Batch command execution across multiple working directories.')
1052
- .enablePositionalOptions()
1053
- .passThroughOptions()
1054
- // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
1055
- .addOption((() => {
1056
- const opt = host.createDynamicOption('-p, --pkg-cwd', (cfg) => {
1057
- const slice = cfg.plugins.batch ?? {};
1058
- const on = !!slice.pkgCwd;
1059
- return `use nearest package directory as current working directory${on ? ' (default)' : ''}`;
1060
- });
1061
- opt.__group = GROUP;
1062
- return opt;
1063
- })())
1064
- .addOption((() => {
1065
- const opt = host.createDynamicOption('-r, --root-path <string>', (cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.plugins.batch?.rootPath || './')})`);
1066
- opt.__group = GROUP;
1067
- return opt;
1068
- })())
1069
- .addOption((() => {
1070
- const opt = host.createDynamicOption('-g, --globs <string>', (cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.plugins.batch?.globs || '*')})`);
1071
- opt.__group = GROUP;
1072
- return opt;
1073
- })())
1074
- .option('-c, --command <string>', 'command executed according to the base shell resolution')
1075
- .option('-l, --list', 'list working directories without executing command')
1076
- .option('-e, --ignore-errors', 'ignore errors and continue with next path')
1077
- .argument('[command...]')
1078
- .addCommand(new commander.Command()
1079
- .name('cmd')
1080
- .description('execute command, conflicts with --command option (default subcommand)')
1081
- .enablePositionalOptions()
1082
- .passThroughOptions()
1083
- .argument('[command...]')
1084
- .action(buildDefaultCmdAction(cli, batchCmd, opts)), { isDefault: true })
1085
- .action(buildParentAction(cli, opts));
1086
- },
1087
- });
1088
-
1089
- /** src/diagnostics/entropy.ts
1090
- * Entropy diagnostics (presentation-only).
1091
- * - Gated by min length and printable ASCII.
1092
- * - Warn once per key per run when bits/char \>= threshold.
1093
- * - Supports whitelist patterns to suppress known-noise keys.
1094
- */
1095
- const warned = new Set();
1096
- const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
1097
- const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
1098
- const whitelisted = (key, regs) => regs.some((re) => re.test(key));
1099
- const shannonBitsPerChar = (s) => {
1100
- const freq = new Map();
1101
- for (const ch of s)
1102
- freq.set(ch, (freq.get(ch) ?? 0) + 1);
1103
- const n = s.length;
1104
- let h = 0;
1105
- for (const c of freq.values()) {
1106
- const p = c / n;
1107
- h -= p * Math.log2(p);
1108
- }
1109
- return h;
1110
- };
1111
- /**
1112
- * Maybe emit a one-line entropy warning for a key.
1113
- * Caller supplies an `emit(line)` function; the helper ensures once-per-key.
1114
- */
1115
- const maybeWarnEntropy = (key, value, origin, opts, emit) => {
1116
- if (!opts || opts.warnEntropy === false)
1117
- return;
1118
- if (warned.has(key))
1119
- return;
1120
- const v = value ?? '';
1121
- const minLen = Math.max(0, opts.entropyMinLength ?? 16);
1122
- const threshold = opts.entropyThreshold ?? 3.8;
1123
- if (v.length < minLen)
1124
- return;
1125
- if (!isPrintableAscii(v))
1126
- return;
1127
- const wl = compile$1(opts.entropyWhitelist);
1128
- if (whitelisted(key, wl))
1129
- return;
1130
- const bpc = shannonBitsPerChar(v);
1131
- if (bpc >= threshold) {
1132
- warned.add(key);
1133
- emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
1134
- }
1135
- };
1136
-
1137
- const DEFAULT_PATTERNS = [
1138
- '\\bsecret\\b',
1139
- '\\btoken\\b',
1140
- '\\bpass(word)?\\b',
1141
- '\\bapi[_-]?key\\b',
1142
- '\\bkey\\b',
1143
- ];
1144
- const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
1145
- const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
1146
- const MASK = '[redacted]';
1147
- /**
1148
- * Utility to redact three related displayed values (parent/dotenv/final)
1149
- * consistently for trace lines.
1150
- */
1151
- const redactTriple = (key, triple, opts) => {
1152
- if (!opts?.redact)
1153
- return triple;
1154
- const regs = compile(opts.redactPatterns);
1155
- const maskIf = (v) => (v && shouldRedactKey(key, regs) ? MASK : v);
1156
- const out = {};
1157
- const p = maskIf(triple.parent);
1158
- const d = maskIf(triple.dotenv);
1159
- const f = maskIf(triple.final);
1160
- if (p !== undefined)
1161
- out.parent = p;
1162
- if (d !== undefined)
1163
- out.dotenv = d;
1164
- if (f !== undefined)
1165
- out.final = f;
1166
- return out;
1167
- };
1168
-
1169
- // Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
1170
- const baseRootOptionDefaults = {
1171
- dotenvToken: '.env',
1172
- loadProcess: true,
1173
- logger: console,
1174
- // Diagnostics defaults
1175
- warnEntropy: true,
1176
- entropyThreshold: 3.8,
1177
- entropyMinLength: 16,
1178
- entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
1179
- paths: './',
1180
- pathsDelimiter: ' ',
1181
- privateToken: 'local',
1182
- scripts: {
1183
- 'git-status': {
1184
- cmd: 'git branch --show-current && git status -s -u',
1185
- shell: true,
1186
- },
1187
- },
1188
- shell: true,
1189
- vars: '',
1190
- varsAssignor: '=',
1191
- varsDelimiter: ' ',
1192
- // tri-state flags default to unset unless explicitly provided
1193
- // (debug/log/exclude* resolved via flag utils)
1194
- };
1195
-
1196
- /** @internal */
1197
- const isPlainObject = (value) => value !== null &&
1198
- typeof value === 'object' &&
1199
- Object.getPrototypeOf(value) === Object.prototype;
1200
- const mergeInto = (target, source) => {
1201
- for (const [key, sVal] of Object.entries(source)) {
1202
- if (sVal === undefined)
1203
- continue; // do not overwrite with undefined
1204
- const tVal = target[key];
1205
- if (isPlainObject(tVal) && isPlainObject(sVal)) {
1206
- target[key] = mergeInto({ ...tVal }, sVal);
1207
- }
1208
- else if (isPlainObject(sVal)) {
1209
- target[key] = mergeInto({}, sVal);
1210
- }
1211
- else {
1212
- target[key] = sVal;
1213
- }
1214
- }
1215
- return target;
1216
- };
1217
- /**
1218
- * Perform a deep defaults-style merge across plain objects. *
1219
- * - Only merges plain objects (prototype === Object.prototype).
1220
- * - Arrays and non-objects are replaced, not merged.
1221
- * - `undefined` values are ignored and do not overwrite prior values.
1222
- *
1223
- * @typeParam T - The resulting shape after merging all layers.
1224
- * @param layers - Zero or more partial layers in ascending precedence order.
1225
- * @returns The merged object typed as {@link T}.
1226
- *
1227
- * @example
1228
- * defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
1229
- * =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
1230
- */
1231
- const defaultsDeep = (...layers) => {
1232
- const result = layers
1233
- .filter(Boolean)
1234
- .reduce((acc, layer) => mergeInto(acc, layer), {});
1235
- return result;
1236
- };
1237
-
1238
- /**
1239
- * Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
1240
- * - If the user explicitly enabled the flag, return true.
1241
- * - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
1242
- * - Otherwise, adopt the default (true → set; false/undefined → unset).
1243
- *
1244
- * @param exclude - The "on" flag value as parsed by Commander.
1245
- * @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
1246
- * @param defaultValue - The generator default to adopt when no explicit toggle is present.
1247
- * @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
1248
- *
1249
- * @example
1250
- * ```ts
1251
- * resolveExclusion(undefined, undefined, true); // => true
1252
- * ```
1253
- */
1254
- const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
1255
- /**
1256
- * Resolve an optional flag with "--exclude-all" overrides.
1257
- * If excludeAll is set and the individual "...-off" is not, force true.
1258
- * If excludeAllOff is set and the individual flag is not explicitly set, unset.
1259
- * Otherwise, adopt the default (true → set; false/undefined → unset).
1260
- *
1261
- * @param exclude - Individual include/exclude flag.
1262
- * @param excludeOff - Individual "...-off" flag.
1263
- * @param defaultValue - Default for the individual flag.
1264
- * @param excludeAll - Global "exclude-all" flag.
1265
- * @param excludeAllOff - Global "exclude-all-off" flag.
1266
- *
1267
- * @example
1268
- * resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
1269
- */
1270
- const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
1271
- // Order of precedence:
1272
- // 1) Individual explicit "on" wins outright.
1273
- // 2) Individual explicit "off" wins over any global.
1274
- // 3) Global exclude-all forces true when not explicitly turned off.
1275
- // 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
1276
- // 5) Fall back to the default (true => set; false/undefined => unset).
1277
- (() => {
1278
- // Individual "on"
1279
- if (exclude === true)
1280
- return true;
1281
- // Individual "off"
1282
- if (excludeOff === true)
1283
- return undefined;
1284
- // Global "exclude-all" ON (unless explicitly turned off)
1285
- if (excludeAll === true)
1286
- return true;
1287
- // Global "exclude-all-off" (unless explicitly enabled)
1288
- if (excludeAllOff === true)
1289
- return undefined;
1290
- // Default
1291
- return defaultValue ? true : undefined;
1292
- })();
1293
- /**
1294
- * exactOptionalPropertyTypes-safe setter for optional boolean flags:
1295
- * delete when undefined; assign when defined — without requiring an index signature on T.
1296
- *
1297
- * @typeParam T - Target object type.
1298
- * @param obj - The object to write to.
1299
- * @param key - The optional boolean property key of {@link T}.
1300
- * @param value - The value to set or `undefined` to unset.
1301
- *
1302
- * @remarks
1303
- * Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
1304
- */
1305
- const setOptionalFlag = (obj, key, value) => {
1306
- const target = obj;
1307
- const k = key;
1308
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1309
- if (value === undefined)
1310
- delete target[k];
1311
- else
1312
- target[k] = value;
1313
- };
1314
-
1315
- /**
1316
- * Merge and normalize raw Commander options (current + parent + defaults)
1317
- * into a GetDotenvCliOptions-like object. Types are intentionally wide to
1318
- * avoid cross-layer coupling; callers may cast as needed.
1319
- */
1320
- const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
1321
- const parent = typeof parentJson === 'string' && parentJson.length > 0
1322
- ? JSON.parse(parentJson)
1323
- : undefined;
1324
- const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
1325
- const current = { ...rest };
1326
- if (typeof scripts === 'string') {
1327
- try {
1328
- current.scripts = JSON.parse(scripts);
1329
- }
1330
- catch {
1331
- // ignore parse errors; leave scripts undefined
1332
- }
1333
- }
1334
- const merged = defaultsDeep({}, defaults, parent ?? {}, current);
1335
- const d = defaults;
1336
- setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
1337
- setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
1338
- setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
1339
- setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
1340
- setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
1341
- setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
1342
- setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
1343
- setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
1344
- // warnEntropy (tri-state)
1345
- setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
1346
- // Normalize shell for predictability: explicit default shell per OS.
1347
- const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
1348
- let resolvedShell = merged.shell;
1349
- if (shellOff)
1350
- resolvedShell = false;
1351
- else if (resolvedShell === true || resolvedShell === undefined) {
1352
- resolvedShell = defaultShell;
1353
- }
1354
- else if (typeof resolvedShell !== 'string' &&
1355
- typeof defaults.shell === 'string') {
1356
- resolvedShell = defaults.shell;
1357
- }
1358
- merged.shell = resolvedShell;
1359
- const cmd = typeof command === 'string' ? command : undefined;
1360
- return cmd !== undefined ? { merged, command: cmd } : { merged };
1361
- };
1362
-
1363
- /**
1364
- * Dotenv expansion utilities.
1365
- *
1366
- * This module implements recursive expansion of environment-variable
1367
- * references in strings and records. It supports both whitespace and
1368
- * bracket syntaxes with optional defaults:
1369
- *
1370
- * - Whitespace: `$VAR[:default]`
1371
- * - Bracketed: `${VAR[:default]}`
1372
- *
1373
- * Escaped dollar signs (`\$`) are preserved.
1374
- * Unknown variables resolve to empty string unless a default is provided.
1375
- */
1376
- /**
1377
- * Like String.prototype.search but returns the last index.
1378
- * @internal
1379
- */
1380
- const searchLast = (str, rgx) => {
1381
- const matches = Array.from(str.matchAll(rgx));
1382
- return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
1383
- };
1384
- const replaceMatch = (value, match, ref) => {
1385
- /**
1386
- * @internal
1387
- */
1388
- const group = match[0];
1389
- const key = match[1];
1390
- const defaultValue = match[2];
1391
- if (!key)
1392
- return value;
1393
- const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
1394
- return interpolate(replacement, ref);
1395
- };
1396
- const interpolate = (value = '', ref = {}) => {
1397
- /**
1398
- * @internal
1399
- */
1400
- // if value is falsy, return it as is
1401
- if (!value)
1402
- return value;
1403
- // get position of last unescaped dollar sign
1404
- const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
1405
- // return value if none found
1406
- if (lastUnescapedDollarSignIndex === -1)
1407
- return value;
1408
- // evaluate the value tail
1409
- const tail = value.slice(lastUnescapedDollarSignIndex);
1410
- // find whitespace pattern: $KEY:DEFAULT
1411
- const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
1412
- const whitespaceMatch = whitespacePattern.exec(tail);
1413
- if (whitespaceMatch != null)
1414
- return replaceMatch(value, whitespaceMatch, ref);
1415
- else {
1416
- // find bracket pattern: ${KEY:DEFAULT}
1417
- const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
1418
- const bracketMatch = bracketPattern.exec(tail);
1419
- if (bracketMatch != null)
1420
- return replaceMatch(value, bracketMatch, ref);
1421
- }
1422
- return value;
1423
- };
1424
- /**
1425
- * Recursively expands environment variables in a string. Variables may be
1426
- * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
1427
- * Unknown variables will expand to an empty string.
1428
- *
1429
- * @param value - The string to expand.
1430
- * @param ref - The reference object to use for variable expansion.
1431
- * @returns The expanded string.
1432
- *
1433
- * @example
1434
- * ```ts
1435
- * process.env.FOO = 'bar';
1436
- * dotenvExpand('Hello $FOO'); // "Hello bar"
1437
- * dotenvExpand('Hello $BAZ:world'); // "Hello world"
1438
- * ```
1439
- *
1440
- * @remarks
1441
- * The expansion is recursive. If a referenced variable itself contains
1442
- * references, those will also be expanded until a stable value is reached.
1443
- * Escaped references (e.g. `\$FOO`) are preserved as literals.
1444
- */
1445
- const dotenvExpand = (value, ref = process.env) => {
1446
- const result = interpolate(value, ref);
1447
- return result ? result.replace(/\\\$/g, '$') : undefined;
1448
- };
1449
- /**
1450
- * Recursively expands environment variables in a string using `process.env` as
1451
- * the expansion reference. Variables may be presented with optional default as
1452
- * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
1453
- * empty string.
1454
- *
1455
- * @param value - The string to expand.
1456
- * @returns The expanded string.
1457
- *
1458
- * @example
1459
- * ```ts
1460
- * process.env.FOO = 'bar';
1461
- * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
1462
- * ```
1463
- */
1464
- const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
1465
-
1466
- // src/GetDotenvOptions.ts
1467
- /**
1468
- * Converts programmatic CLI options to `getDotenv` options. *
1469
- * @param cliOptions - CLI options. Defaults to `{}`.
1470
- *
1471
- * @returns `getDotenv` options.
1472
- */
1473
- const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
1474
- /**
1475
- * Convert CLI-facing string options into {@link GetDotenvOptions}.
1476
- *
1477
- * - 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`
1478
- * pairs (configurable delimiters) into a {@link ProcessEnv}.
1479
- * - Drops CLI-only keys that have no programmatic equivalent.
1480
- *
1481
- * @remarks
1482
- * Follows exact-optional semantics by not emitting undefined-valued entries.
1483
- */
1484
- // Drop CLI-only keys (debug/scripts) without relying on Record casts.
1485
- // Create a shallow copy then delete optional CLI-only keys if present.
1486
- const restObj = { ...rest };
1487
- delete restObj.debug;
1488
- delete restObj.scripts;
1489
- const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
1490
- // Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
1491
- let parsedVars;
1492
- if (typeof vars === 'string') {
1493
- const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
1494
- ? RegExp(varsAssignorPattern)
1495
- : (varsAssignor ?? '=')));
1496
- parsedVars = Object.fromEntries(kvPairs);
1497
- }
1498
- else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
1499
- // Keep only string or undefined values to match ProcessEnv.
1500
- const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
1501
- parsedVars = Object.fromEntries(entries);
1502
- }
1503
- // Drop undefined-valued entries at the converter stage to match ProcessEnv
1504
- // expectations and the compat test assertions.
1505
- if (parsedVars) {
1506
- parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
1507
- }
1508
- // Tolerate paths as either a delimited string or string[]
1509
- // Use a locally cast union type to avoid lint warnings about always-falsy conditions
1510
- // under the RootOptionsShape (which declares paths as string | undefined).
1511
- const pathsAny = paths;
1512
- const pathsOut = Array.isArray(pathsAny)
1513
- ? pathsAny.filter((p) => typeof p === 'string')
1514
- : splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
1515
- // Preserve exactOptionalPropertyTypes: only include keys when defined.
1516
- return {
1517
- ...restObj,
1518
- ...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
1519
- ...(parsedVars !== undefined ? { vars: parsedVars } : {}),
1520
- };
1521
- };
1522
-
1523
- const dbg = (...args) => {
1524
- if (process.env.GETDOTENV_DEBUG) {
1525
- // Use stderr to avoid interfering with stdout assertions
1526
- console.error('[getdotenv:alias]', ...args);
1527
- }
1528
- };
1529
- const attachParentAlias = (cli, options, _cmd) => {
1530
- const aliasSpec = typeof options.optionAlias === 'string'
1531
- ? { flags: options.optionAlias, description: undefined, expand: true }
1532
- : options.optionAlias;
1533
- if (!aliasSpec)
1534
- return;
1535
- const deriveKey = (flags) => {
1536
- dbg('install alias option', flags);
1537
- const long = flags.split(/[ ,|]+/).find((f) => f.startsWith('--')) ?? '--cmd';
1538
- const name = long.replace(/^--/, '');
1539
- return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
1540
- };
1541
- const aliasKey = deriveKey(aliasSpec.flags);
1542
- // Expose the option on the parent.
1543
- const desc = aliasSpec.description ??
1544
- 'alias of cmd subcommand; provide command tokens (variadic)';
1545
- cli.option(aliasSpec.flags, desc);
1546
- // Tag the just-added parent option for grouped help rendering.
1547
- try {
1548
- const optsArr = cli.options;
1549
- if (Array.isArray(optsArr) && optsArr.length > 0) {
1550
- const last = optsArr[optsArr.length - 1];
1551
- last.__group = 'plugin:cmd';
1552
- }
1553
- }
1554
- catch {
1555
- /* noop */
1556
- }
1557
- // Shared alias executor for either preAction or preSubcommand hooks.
1558
- // Ensure we only execute once even if both hooks fire in a single parse.
1559
- let aliasHandled = false;
1560
- const maybeRunAlias = async (thisCommand) => {
1561
- dbg('alias:maybe:start');
1562
- const raw = thisCommand.rawArgs ?? [];
1563
- const childNames = thisCommand.commands.flatMap((c) => [
1564
- c.name(),
1565
- ...c.aliases(),
1566
- ]);
1567
- const hasSub = childNames.some((n) => raw.includes(n));
1568
- // Read alias value from parent opts.
1569
- const o = thisCommand.opts();
1570
- const val = o[aliasKey];
1571
- const provided = typeof val === 'string'
1572
- ? val.length > 0
1573
- : Array.isArray(val)
1574
- ? val.length > 0
1575
- : false;
1576
- if (!provided || hasSub) {
1577
- dbg('alias:maybe:skip', { provided, hasSub });
1578
- return; // not an alias-only invocation
1579
- }
1580
- if (aliasHandled) {
1581
- dbg('alias:maybe:already-handled');
1582
- return;
1583
- }
1584
- aliasHandled = true;
1585
- dbg('alias-only invocation detected');
1586
- // Merge CLI options and resolve dotenv context.
1587
- const { merged } = resolveCliOptions(o,
1588
- // cast through unknown to avoid readonly -> mutable incompatibilities
1589
- baseRootOptionDefaults, process.env.getDotenvCliOptions);
1590
- const logger = merged.logger ?? console;
1591
- const serviceOptions = getDotenvCliOptions2Options(merged);
1592
- await cli.resolveAndLoad(serviceOptions);
1593
- // Normalize alias value.
1594
- const joined = typeof val === 'string'
1595
- ? val
1596
- : Array.isArray(val)
1597
- ? val.map(String).join(' ')
1598
- : '';
1599
- const input = aliasSpec.expand === false
1600
- ? joined
1601
- : (dotenvExpandFromProcessEnv(joined) ?? joined);
1602
- dbg('resolved input', { input });
1603
- const resolved = resolveCommand(merged.scripts, input);
1604
- const lg = logger;
1605
- if (merged.debug) {
1606
- (lg.debug ?? lg.log)('\n*** command ***\n', `'${resolved}'`);
1607
- }
1608
- const { logger: _omit, ...envBag } = merged;
1609
- // Test guard: when running under tests, prefer stdio: 'inherit' to avoid
1610
- // assertions depending on captured stdio; ignore GETDOTENV_STDIO/capture.
1611
- const underTests = process.env.GETDOTENV_TEST === '1' ||
1612
- typeof process.env.VITEST_WORKER_ID === 'string';
1613
- const forceExit = process.env.GETDOTENV_FORCE_EXIT === '1';
1614
- const capture = !underTests &&
1615
- (process.env.GETDOTENV_STDIO === 'pipe' ||
1616
- Boolean(merged.capture));
1617
- dbg('run:start', { capture, shell: merged.shell });
1618
- // Prefer explicit env injection: include resolved dotenv map to avoid leaking
1619
- // parent process.env secrets when exclusions are set.
1620
- const ctx = cli.getCtx();
1621
- const dotenv = (ctx?.dotenv ?? {});
1622
- // Diagnostics: --trace [keys...]
1623
- const traceOpt = merged.trace;
1624
- if (traceOpt) {
1625
- const parentKeys = Object.keys(process.env);
1626
- const dotenvKeys = Object.keys(dotenv);
1627
- const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
1628
- const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
1629
- const childEnvPreview = {
1630
- ...process.env,
1631
- ...dotenv,
1632
- };
1633
- for (const k of keys) {
1634
- const parent = process.env[k];
1635
- const dot = dotenv[k];
1636
- const final = childEnvPreview[k];
1637
- const origin = dot !== undefined
1638
- ? 'dotenv'
1639
- : parent !== undefined
1640
- ? 'parent'
1641
- : 'unset';
1642
- // Build redact options and triple bag without undefined-valued fields
1643
- const redOpts = {};
1644
- const redFlag = merged.redact;
1645
- const redPatterns = merged
1646
- .redactPatterns;
1647
- if (redFlag)
1648
- redOpts.redact = true;
1649
- if (redFlag && Array.isArray(redPatterns))
1650
- redOpts.redactPatterns = redPatterns;
1651
- const tripleBag = {};
1652
- if (parent !== undefined)
1653
- tripleBag.parent = parent;
1654
- if (dot !== undefined)
1655
- tripleBag.dotenv = dot;
1656
- if (final !== undefined)
1657
- tripleBag.final = final;
1658
- const triple = redactTriple(k, tripleBag, redOpts);
1659
- process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
1660
- const entOpts = {};
1661
- const warnEntropy = merged.warnEntropy;
1662
- const entropyThreshold = merged
1663
- .entropyThreshold;
1664
- const entropyMinLength = merged
1665
- .entropyMinLength;
1666
- const entropyWhitelist = merged
1667
- .entropyWhitelist;
1668
- if (typeof warnEntropy === 'boolean')
1669
- entOpts.warnEntropy = warnEntropy;
1670
- if (typeof entropyThreshold === 'number')
1671
- entOpts.entropyThreshold = entropyThreshold;
1672
- if (typeof entropyMinLength === 'number')
1673
- entOpts.entropyMinLength = entropyMinLength;
1674
- if (Array.isArray(entropyWhitelist))
1675
- entOpts.entropyWhitelist = entropyWhitelist;
1676
- maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
1677
- }
1678
- }
1679
- let exitCode = Number.NaN;
1680
- try {
1681
- // Resolve shell and preserve argv for Node -e snippets under shell-off.
1682
- const shellSetting = resolveShell(merged.scripts, input, merged.shell);
1683
- let commandArg = resolved;
1684
- /** * Special-case: when shell is OFF and no script alias remap occurred
1685
- * (resolved === input), treat a Node eval payload as an argv array to
1686
- * avoid lossy re-tokenization of the code string.
1687
- *
1688
- * Examples handled:
1689
- * "node -e \"console.log(JSON.stringify(...))\""
1690
- * "node --eval 'console.log(...)'"
1691
- *
1692
- * We peel exactly one pair of symmetric outer quotes from the code
1693
- * argument when present; inner quotes remain untouched.
1694
- */
1695
- if (shellSetting === false && resolved === input) {
1696
- // Helper: strip one symmetric outer quote layer
1697
- const stripOne = (s) => {
1698
- if (s.length < 2)
1699
- return s;
1700
- const a = s.charAt(0);
1701
- const b = s.charAt(s.length - 1);
1702
- const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
1703
- return symmetric ? s.slice(1, -1) : s;
1704
- };
1705
- // Normalize whole input once for robust matching
1706
- const normalized = stripOne(input.trim());
1707
- // First try a lightweight regex on the normalized string
1708
- const m = /^\s*node\s+(--eval|-e)\s+([\s\S]+)$/i.exec(normalized);
1709
- if (m && typeof m[1] === 'string' && typeof m[2] === 'string') {
1710
- const evalFlag = m[1];
1711
- let codeArg = m[2].trim();
1712
- codeArg = stripOne(codeArg);
1713
- const flag = evalFlag.startsWith('--') ? '--eval' : '-e';
1714
- commandArg = ['node', flag, codeArg];
1715
- }
1716
- else {
1717
- // Fallback: tokenize and detect node -e/--eval form
1718
- const parts = tokenize(input);
1719
- if (parts.length >= 3) {
1720
- // Narrow under noUncheckedIndexedAccess
1721
- const p0 = parts[0];
1722
- const p1 = parts[1];
1723
- if (p0?.toLowerCase() === 'node' &&
1724
- (p1 === '-e' || p1 === '--eval')) {
1725
- commandArg = parts;
1726
- }
1727
- }
1728
- }
1729
- }
1730
- exitCode = await runCommand(commandArg, shellSetting, {
1731
- env: buildSpawnEnv(process.env, {
1732
- ...dotenv,
1733
- getDotenvCliOptions: JSON.stringify(envBag),
1734
- }),
1735
- stdio: capture ? 'pipe' : 'inherit',
1736
- });
1737
- dbg('run:done', { exitCode });
1738
- }
1739
- catch (err) {
1740
- const code = typeof err.exitCode === 'number'
1741
- ? err.exitCode
1742
- : 1;
1743
- dbg('run:error', { exitCode: code, error: String(err) });
1744
- if (!underTests) {
1745
- dbg('process.exit (error path)', { exitCode: code });
1746
- process.exit(code);
1747
- }
1748
- else {
1749
- dbg('process.exit suppressed for tests (error path)', {
1750
- exitCode: code,
1751
- });
1752
- }
1753
- return;
1754
- }
1755
- if (!Number.isNaN(exitCode)) {
1756
- dbg('process.exit', { exitCode });
1757
- process.exit(exitCode);
1758
- }
1759
- // Fallback: Some environments may not surface a numeric exitCode even on success.
1760
- // Always terminate alias-only invocations outside tests to avoid hanging the process,
1761
- // regardless of capture/GETDOTENV_STDIO. Under tests, suppress to keep the runner alive.
1762
- if (!underTests) {
1763
- dbg('process.exit (fallback: non-numeric exitCode)', { exitCode: 0 });
1764
- process.exit(0);
1765
- }
1766
- else {
1767
- dbg('process.exit (fallback suppressed for tests: non-numeric exitCode)', { exitCode: 0 });
1768
- }
1769
- // Optional last-resort guard: force an exit on the next tick when enabled.
1770
- // Intended for diagnosing environments where the process appears to linger
1771
- // despite reaching the success/error handlers above. Disabled under tests.
1772
- if (forceExit) {
1773
- try {
1774
- if (process.env.GETDOTENV_DEBUG_VERBOSE) {
1775
- const getHandles = process._getActiveHandles;
1776
- const handles = typeof getHandles === 'function' ? getHandles() : [];
1777
- dbg('active handles before forced exit', {
1778
- count: Array.isArray(handles) ? handles.length : undefined,
1779
- });
1780
- }
1781
- }
1782
- catch {
1783
- // best-effort only
1784
- }
1785
- const code = Number.isNaN(exitCode) ? 0 : exitCode;
1786
- dbg('process.exit (forced)', { exitCode: code });
1787
- setImmediate(() => process.exit(code));
1788
- }
1789
- };
1790
- // Execute alias-only invocations whether the root handles the action // itself (preAction) or Commander routes to a default subcommand (preSubcommand).
1791
- cli.hook('preAction', async (thisCommand, _actionCommand) => {
1792
- await maybeRunAlias(thisCommand);
1793
- });
1794
- cli.hook('preSubcommand', async (thisCommand) => {
1795
- await maybeRunAlias(thisCommand);
1796
- });
1797
- };
1798
-
1799
- /**+ Cmd plugin: executes a command using the current getdotenv CLI context.
1800
- *
1801
- * - Joins positional args into a single command string.
1802
- * - Resolves scripts and shell settings using shared helpers.
1803
- * - Forwards merged CLI options to subprocesses via
1804
- * process.env.getDotenvCliOptions for nested CLI behavior. */
1805
- const cmdPlugin = (options = {}) => definePlugin({
1806
- id: 'cmd',
1807
- setup(cli) {
1808
- const aliasSpec = typeof options.optionAlias === 'string'
1809
- ? { flags: options.optionAlias}
1810
- : options.optionAlias;
1811
- const deriveKey = (flags) => {
1812
- const long = flags.split(/[ ,|]+/).find((f) => f.startsWith('--')) ?? '--cmd';
1813
- const name = long.replace(/^--/, '');
1814
- return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
1815
- };
1816
- const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
1817
- // Create as a GetDotenvCli child so helpInformation includes a trailing blank line.
1818
- const cmd = cli
1819
- .createCommand('cmd')
1820
- .description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
1821
- .enablePositionalOptions()
1822
- .passThroughOptions()
1823
- .argument('[command...]')
1824
- .action(async (commandParts, _opts, thisCommand) => {
1825
- // Commander passes positional tokens as the first action argument
1826
- const args = Array.isArray(commandParts) ? commandParts : [];
1827
- // No-op when invoked as the default command with no args.
1828
- if (args.length === 0)
1829
- return;
1830
- const parent = thisCommand.parent;
1831
- if (!parent)
1832
- throw new Error('parent command not found'); // Conflict detection: if an alias option is present on parent, do not
1833
- // also accept positional cmd args.
1834
- if (aliasKey) {
1835
- const pv = parent.opts();
1836
- const ov = pv[aliasKey];
1837
- if (ov !== undefined) {
1838
- const merged = parent.getDotenvCliOptions ?? {};
1839
- const logger = merged.logger ?? console;
1840
- const lr = logger;
1841
- const emit = lr.error ?? lr.log;
1842
- emit(`--${aliasKey} option conflicts with cmd subcommand.`);
1843
- process.exit(0);
1844
- }
1845
- }
1846
- // Merged CLI options are persisted by the shipped CLI preSubcommand hook.
1847
- const merged = parent.getDotenvCliOptions ?? {};
1848
- const logger = merged.logger ?? console;
1849
- // Join positional args into the command string.
1850
- const input = args.map(String).join(' ');
1851
- // Resolve command and shell using shared helpers.
1852
- const scripts = merged.scripts;
1853
- const shell = merged.shell;
1854
- const resolved = resolveCommand(scripts, input);
1855
- if (merged.debug) {
1856
- const lg = logger;
1857
- (lg.debug ?? lg.log)('\n*** command ***\n', `'${resolved}'`);
1858
- }
1859
- // Round-trip CLI options for nested getdotenv invocations.
1860
- // Omit logger (functions are not serializable).
1861
- const { logger: _omit, ...envBag } = merged;
1862
- const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
1863
- Boolean(merged.capture);
1864
- // Prefer explicit env injection using the resolved dotenv map.
1865
- const ctx = cli.getCtx();
1866
- const dotenv = (ctx?.dotenv ?? {});
1867
- // Diagnostics: --trace [keys...] (space-delimited keys if provided; all keys when true)
1868
- const traceOpt = merged.trace;
1869
- if (traceOpt) {
1870
- // Determine keys to trace: all keys (parent ∪ dotenv) or selected.
1871
- const parentKeys = Object.keys(process.env);
1872
- const dotenvKeys = Object.keys(dotenv);
1873
- const allKeys = Array.from(new Set([...parentKeys, ...dotenvKeys])).sort();
1874
- const keys = Array.isArray(traceOpt) ? traceOpt : allKeys;
1875
- // Child env preview (as composed below; excluding getDotenvCliOptions)
1876
- const childEnvPreview = {
1877
- ...process.env,
1878
- ...dotenv,
1879
- };
1880
- for (const k of keys) {
1881
- const parent = process.env[k];
1882
- const dot = dotenv[k];
1883
- const final = childEnvPreview[k];
1884
- const origin = dot !== undefined
1885
- ? 'dotenv'
1886
- : parent !== undefined
1887
- ? 'parent'
1888
- : 'unset';
1889
- // Apply presentation-time redaction (if enabled)
1890
- const redFlag = merged.redact;
1891
- const redPatterns = merged
1892
- .redactPatterns;
1893
- const redOpts = {};
1894
- if (redFlag)
1895
- redOpts.redact = true;
1896
- if (redFlag && Array.isArray(redPatterns))
1897
- redOpts.redactPatterns = redPatterns;
1898
- const tripleBag = {};
1899
- if (parent !== undefined)
1900
- tripleBag.parent = parent;
1901
- if (dot !== undefined)
1902
- tripleBag.dotenv = dot;
1903
- if (final !== undefined)
1904
- tripleBag.final = final;
1905
- const triple = redactTriple(k, tripleBag, redOpts);
1906
- // Emit concise diagnostic line to stderr.
1907
- process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
1908
- // Optional entropy warning (once-per-key)
1909
- const entOpts = {};
1910
- const warnEntropy = merged
1911
- .warnEntropy;
1912
- const entropyThreshold = merged.entropyThreshold;
1913
- const entropyMinLength = merged.entropyMinLength;
1914
- const entropyWhitelist = merged.entropyWhitelist;
1915
- if (typeof warnEntropy === 'boolean')
1916
- entOpts.warnEntropy = warnEntropy;
1917
- if (typeof entropyThreshold === 'number')
1918
- entOpts.entropyThreshold = entropyThreshold;
1919
- if (typeof entropyMinLength === 'number')
1920
- entOpts.entropyMinLength = entropyMinLength;
1921
- if (Array.isArray(entropyWhitelist))
1922
- entOpts.entropyWhitelist = entropyWhitelist;
1923
- maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
1924
- }
1925
- }
1926
- const shellSetting = resolveShell(scripts, input, shell);
1927
- /**
1928
- * Preserve original argv array when:
1929
- * - shell is OFF (plain execa), and
1930
- * - no script alias remap occurred (resolved === input).
1931
- *
1932
- * This avoids lossy re-tokenization of code snippets such as:
1933
- * node -e "console.log(process.env.APP_SECRET ?? '')"
1934
- * where quotes may have been stripped by the parent shell and
1935
- * spaces inside the code must remain a single argument.
1936
- */
1937
- const commandArg = shellSetting === false && resolved === input
1938
- ? args.map(String)
1939
- : resolved;
1940
- await runCommand(commandArg, shellSetting, {
1941
- env: buildSpawnEnv(process.env, {
1942
- ...dotenv,
1943
- getDotenvCliOptions: JSON.stringify(envBag),
1944
- }),
1945
- stdio: capture ? 'pipe' : 'inherit',
1946
- });
1947
- });
1948
- if (options.asDefault)
1949
- cli.addCommand(cmd, { isDefault: true });
1950
- else
1951
- cli.addCommand(cmd);
1952
- // Parent-attached option alias (optional).
1953
- if (aliasSpec)
1954
- attachParentAlias(cli, options);
1955
- },
1956
- });
1957
-
1958
- const demoPlugin = () => definePlugin({
1959
- id: 'demo',
1960
- setup(cli) {
1961
- const logger = console;
1962
- const ns = cli
1963
- .ns('demo')
1964
- .description('Educational demo of host/plugin features (context, child exec, scripts/shell)');
1965
- /**
1966
- * demo ctx
1967
- * Print a summary of the current dotenv context.
1968
- *
1969
- * Notes:
1970
- * - The host resolves context once per invocation in a preSubcommand hook
1971
- * (added by enhanceGetDotenvCli.passOptions() in the shipped CLI).
1972
- * - ctx.dotenv contains the final merged values after overlays/dynamics.
1973
- */
1974
- ns.command('ctx')
1975
- .description('Print a summary of the current dotenv context')
1976
- .action(() => {
1977
- const ctx = cli.getCtx();
1978
- const dotenv = ctx?.dotenv ?? {};
1979
- const keys = Object.keys(dotenv).sort();
1980
- const sample = keys.slice(0, 5);
1981
- logger.log('[demo] Context summary:');
1982
- logger.log(`- keys: ${keys.length.toString()}`);
1983
- logger.log(`- sample keys: ${sample.join(', ') || '(none)'}`);
1984
- logger.log('- tip: use "--trace [keys...]" for per-key diagnostics');
1985
- });
1986
- /**
1987
- * demo run [--print KEY]
1988
- * Execute a small child process that prints a dotenv value.
1989
- *
1990
- * Design:
1991
- * - Use shell-off + argv array to avoid cross-platform quoting pitfalls.
1992
- * - Inject ctx.dotenv explicitly into the child env.
1993
- * - Inherit stdio so output streams live (works well outside CI).
1994
- *
1995
- * Tip:
1996
- * - For deterministic capture in CI, run with "--capture" (or set
1997
- * GETDOTENV_STDIO=pipe). The shipped CLI honors both.
1998
- */
1999
- ns.command('run')
2000
- .description('Run a small child process under the current dotenv (shell-off)')
2001
- .option('--print <key>', 'dotenv key to print', 'APP_SETTING')
2002
- .action(async (opts) => {
2003
- const key = typeof opts.print === 'string' && opts.print.length > 0
2004
- ? opts.print
2005
- : 'APP_SETTING';
2006
- // Build a minimal node -e payload via argv array (avoid quoting issues).
2007
- const code = `console.log(process.env.${key} ?? "")`;
2008
- const ctx = cli.getCtx();
2009
- const dotenv = (ctx?.dotenv ?? {});
2010
- // Inherit stdio for an interactive demo. Use --capture for CI.
2011
- await runCommand(['node', '-e', code], false, {
2012
- env: buildSpawnEnv(process.env, dotenv),
2013
- stdio: 'inherit',
2014
- });
2015
- });
2016
- /**
2017
- * demo script [command...]
2018
- * Resolve and execute a command using the current scripts table and
2019
- * shell preference (with per-script overrides).
2020
- *
2021
- * How it works:
2022
- * - We read the merged CLI options persisted by the shipped CLI’s
2023
- * passOptions() hook on the current command instance’s parent.
2024
- * - resolveCommand resolves a script name → cmd or passes through a raw
2025
- * command string.
2026
- * - resolveShell chooses the appropriate shell:
2027
- * scripts[name].shell ?? global shell (string|boolean).
2028
- */
2029
- ns.command('script')
2030
- .description('Resolve a command via scripts and execute it with the proper shell')
2031
- .argument('[command...]')
2032
- .action(async (commandParts, _opts, thisCommand) => {
2033
- // Safely access the parent’s merged options (installed by passOptions()).
2034
- const parent = thisCommand.parent;
2035
- const bag = (parent?.getDotenvCliOptions ?? {});
2036
- const input = Array.isArray(commandParts)
2037
- ? commandParts.map(String).join(' ')
2038
- : '';
2039
- if (!input) {
2040
- logger.log('[demo] Please provide a command or script name, e.g. "echo OK" or "git-status".');
2041
- return;
2042
- }
2043
- const resolved = resolveCommand(bag?.scripts, input);
2044
- const shell = resolveShell(bag?.scripts, input, bag?.shell);
2045
- // Compose child env (parent + ctx.dotenv). This mirrors cmd/batch behavior.
2046
- const ctx = cli.getCtx();
2047
- const dotenv = (ctx?.dotenv ?? {});
2048
- await runCommand(resolved, shell, {
2049
- env: buildSpawnEnv(process.env, dotenv),
2050
- stdio: 'inherit',
2051
- });
2052
- });
2053
- },
2054
- /**
2055
- * Optional: afterResolve can initialize per-plugin state using ctx.dotenv.
2056
- * For the demo we emit a single breadcrumb only when GETDOTENV_DEBUG is set,
2057
- * keeping default runs (tests/CI/smoke) quiet.
2058
- */
2059
- afterResolve(_cli, ctx) {
2060
- if (process.env.GETDOTENV_DEBUG) {
2061
- const keys = Object.keys(ctx.dotenv);
2062
- if (keys.length > 0) {
2063
- // Keep noise low; a single-line breadcrumb is sufficient for the demo.
2064
- console.error('[demo] afterResolve: dotenv keys loaded:', keys.length);
2065
- }
2066
- }
2067
- },
2068
- });
2069
-
2070
- const ensureDir = async (p) => {
2071
- await fs.ensureDir(p);
2072
- return p;
2073
- };
2074
- const writeFile = async (dest, data) => {
2075
- await ensureDir(path.dirname(dest));
2076
- await fs.writeFile(dest, data, 'utf-8');
2077
- };
2078
- const copyTextFile = async (src, dest, substitutions) => {
2079
- const contents = await fs.readFile(src, 'utf-8');
2080
- const out = substitutions && Object.keys(substitutions).length > 0
2081
- ? Object.entries(substitutions).reduce((acc, [k, v]) => acc.split(k).join(v), contents)
2082
- : contents;
2083
- await writeFile(dest, out);
2084
- };
2085
- /**
2086
- * Ensure a set of lines exist (exact match) in a file. Creates the file
2087
- * when missing. Returns whether it was created or changed.
2088
- */
2089
- const ensureLines = async (filePath, lines) => {
2090
- const exists = await fs.pathExists(filePath);
2091
- const current = exists ? await fs.readFile(filePath, 'utf-8') : '';
2092
- const curLines = current.split(/\r?\n/);
2093
- const have = new Set(curLines.filter((l) => l.length > 0));
2094
- let mutated = false;
2095
- for (const l of lines) {
2096
- if (!have.has(l)) {
2097
- curLines.push(l);
2098
- have.add(l);
2099
- mutated = true;
2100
- }
2101
- }
2102
- // Normalize to LF and ensure trailing newline
2103
- const next = curLines.filter((l) => l.length > 0).join('\n') + '\n';
2104
- if (!exists) {
2105
- await writeFile(filePath, next);
2106
- return { created: true, changed: true };
2107
- }
2108
- if (mutated) {
2109
- await fs.writeFile(filePath, next, 'utf-8');
2110
- return { created: false, changed: true };
2111
- }
2112
- return { created: false, changed: false };
2113
- };
2114
-
2115
- // Templates root used by the scaffolder
2116
- const TEMPLATES_ROOT = path.resolve('templates');
2117
-
2118
- const planConfigCopies = ({ format, withLocal, destRoot, }) => {
2119
- const copies = [];
2120
- if (format === 'json') {
2121
- copies.push({
2122
- src: path.join(TEMPLATES_ROOT, 'config', 'json', 'public', 'getdotenv.config.json'),
2123
- dest: path.join(destRoot, 'getdotenv.config.json'),
2124
- });
2125
- if (withLocal) {
2126
- copies.push({
2127
- src: path.join(TEMPLATES_ROOT, 'config', 'json', 'local', 'getdotenv.config.local.json'),
2128
- dest: path.join(destRoot, 'getdotenv.config.local.json'),
2129
- });
2130
- }
2131
- }
2132
- else if (format === 'yaml') {
2133
- copies.push({
2134
- src: path.join(TEMPLATES_ROOT, 'config', 'yaml', 'public', 'getdotenv.config.yaml'),
2135
- dest: path.join(destRoot, 'getdotenv.config.yaml'),
2136
- });
2137
- if (withLocal) {
2138
- copies.push({
2139
- src: path.join(TEMPLATES_ROOT, 'config', 'yaml', 'local', 'getdotenv.config.local.yaml'),
2140
- dest: path.join(destRoot, 'getdotenv.config.local.yaml'),
2141
- });
2142
- }
2143
- }
2144
- else if (format === 'js') {
2145
- copies.push({
2146
- src: path.join(TEMPLATES_ROOT, 'config', 'js', 'getdotenv.config.js'),
2147
- dest: path.join(destRoot, 'getdotenv.config.js'),
2148
- });
2149
- }
2150
- else {
2151
- copies.push({
2152
- src: path.join(TEMPLATES_ROOT, 'config', 'ts', 'getdotenv.config.ts'),
2153
- dest: path.join(destRoot, 'getdotenv.config.ts'),
2154
- });
2155
- }
2156
- return copies;
2157
- };
2158
- const planCliCopies = ({ cliName, destRoot, }) => {
2159
- const subs = { __CLI_NAME__: cliName };
2160
- const base = path.join(destRoot, 'src', 'cli', cliName);
2161
- return [
2162
- {
2163
- src: path.join(TEMPLATES_ROOT, 'cli', 'ts', 'index.ts'),
2164
- dest: path.join(base, 'index.ts'),
2165
- subs,
2166
- },
2167
- {
2168
- src: path.join(TEMPLATES_ROOT, 'cli', 'ts', 'plugins', 'hello.ts'),
2169
- dest: path.join(base, 'plugins', 'hello.ts'),
2170
- subs,
2171
- },
2172
- ];
2173
- };
2174
-
2175
- /**
2176
- * Determine whether the current environment should be treated as non-interactive.
2177
- * CI heuristics include: CI, GITHUB_ACTIONS, BUILDKITE, TEAMCITY_VERSION, TF_BUILD.
2178
- */
2179
- const isNonInteractive = () => {
2180
- const ciLike = process.env.CI ||
2181
- process.env.GITHUB_ACTIONS ||
2182
- process.env.BUILDKITE ||
2183
- process.env.TEAMCITY_VERSION ||
2184
- process.env.TF_BUILD;
2185
- return Boolean(ciLike) || !(node_process.stdin.isTTY && node_process.stdout.isTTY);
2186
- };
2187
- const promptDecision = async (filePath, logger, rl) => {
2188
- logger.log(`File exists: ${filePath}\nChoose: [o]verwrite, [e]xample, [s]kip, [O]verwrite All, [E]xample All, [S]kip All`);
2189
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2190
- while (true) {
2191
- const a = (await rl.question('> ')).trim();
2192
- const valid = ['o', 'e', 's', 'O', 'E', 'S'];
2193
- if (valid.includes(a))
2194
- return a;
2195
- logger.log('Please enter one of: o e s O E S');
2196
- }
2197
- };
2198
-
2199
- /**
2200
- * Requirements: Init scaffolding plugin with collision flow and CI detection.
2201
- * Note: Large file scheduled for decomposition; tracked in stan.todo.md.
2202
- */
2203
- const initPlugin = (opts = {}) => definePlugin({
2204
- id: 'init',
2205
- setup(cli) {
2206
- const logger = opts.logger ?? console;
2207
- const cmd = cli
2208
- .ns('init')
2209
- .description('Scaffold getdotenv config files and a host-based CLI skeleton.')
2210
- .argument('[dest]', 'destination path (default: ./)', '.')
2211
- .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
2212
- .option('--with-local', 'include .local config variant')
2213
- .option('--dynamic', 'include dynamic examples (JS/TS configs)')
2214
- .option('--cli-name <string>', 'CLI name for skeleton and tokens')
2215
- .option('--force', 'overwrite all existing files')
2216
- .option('--yes', 'skip all collisions (no overwrite)')
2217
- .action(async (destArg) => {
2218
- // Read options directly from the captured command instance.
2219
- // Cast to a plain record to satisfy exact-optional and lint safety.
2220
- const o = cmd.opts() ?? {};
2221
- const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
2222
- const cwd = process.cwd();
2223
- const destRoot = path.resolve(cwd, destRel);
2224
- const formatInput = o.configFormat;
2225
- const formatRaw = typeof formatInput === 'string'
2226
- ? formatInput.toLowerCase()
2227
- : 'json';
2228
- const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw)
2229
- ? formatRaw
2230
- : 'json');
2231
- const withLocal = !!o.withLocal;
2232
- // dynamic flag reserved for future template variants; present for UX compatibility
2233
- void o.dynamic;
2234
- // CLI name default: --cli-name | basename(dest) | 'mycli'
2235
- const cliName = (typeof o.cliName === 'string' && o.cliName.length > 0
2236
- ? o.cliName
2237
- : path.basename(destRoot) || 'mycli') || 'mycli';
2238
- // Precedence: --force > --yes > auto-detect(non-interactive => yes)
2239
- const force = !!o.force;
2240
- const yes = !!o.yes || (!force && isNonInteractive());
2241
- // Build copy plan
2242
- const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
2243
- const cliCopies = planCliCopies({ cliName, destRoot });
2244
- const copies = [...cfgCopies, ...cliCopies];
2245
- // Interactive state
2246
- let globalDecision;
2247
- const rl = promises.createInterface({ input: node_process.stdin, output: node_process.stdout });
2248
- try {
2249
- for (const item of copies) {
2250
- const exists = await fs.pathExists(item.dest);
2251
- if (!exists) {
2252
- const subs = item.subs ?? {};
2253
- await copyTextFile(item.src, item.dest, subs);
2254
- logger.log(`Created ${path.relative(cwd, item.dest)}`);
2255
- continue;
2256
- }
2257
- // Collision
2258
- if (force) {
2259
- const subs = item.subs ?? {};
2260
- await copyTextFile(item.src, item.dest, subs);
2261
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
2262
- continue;
2263
- }
2264
- if (yes) {
2265
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
2266
- continue;
2267
- }
2268
- let decision = globalDecision;
2269
- if (!decision) {
2270
- const a = await promptDecision(item.dest, logger, rl);
2271
- if (a === 'O') {
2272
- globalDecision = 'overwrite';
2273
- decision = 'overwrite';
2274
- }
2275
- else if (a === 'E') {
2276
- globalDecision = 'example';
2277
- decision = 'example';
2278
- }
2279
- else if (a === 'S') {
2280
- globalDecision = 'skip';
2281
- decision = 'skip';
2282
- }
2283
- else {
2284
- decision =
2285
- a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
2286
- }
2287
- }
2288
- if (decision === 'overwrite') {
2289
- const subs = item.subs ?? {};
2290
- await copyTextFile(item.src, item.dest, subs);
2291
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
2292
- }
2293
- else if (decision === 'example') {
2294
- const destEx = `${item.dest}.example`;
2295
- const subs = item.subs ?? {};
2296
- await copyTextFile(item.src, destEx, subs);
2297
- logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
2298
- }
2299
- else {
2300
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
2301
- }
2302
- }
2303
- // Ensure .gitignore includes local config patterns.
2304
- const giPath = path.join(destRoot, '.gitignore');
2305
- const { created, changed } = await ensureLines(giPath, [
2306
- 'getdotenv.config.local.*',
2307
- '*.local',
2308
- ]);
2309
- if (created) {
2310
- logger.log(`Created ${path.relative(cwd, giPath)}`);
2311
- }
2312
- else if (changed) {
2313
- logger.log(`Updated ${path.relative(cwd, giPath)}`);
2314
- }
2315
- }
2316
- finally {
2317
- rl.close();
2318
- }
2319
- });
2320
- },
2321
- });
2322
-
2323
- exports.awsPlugin = awsPlugin;
2324
- exports.batchPlugin = batchPlugin;
2325
- exports.cmdPlugin = cmdPlugin;
2326
- exports.demoPlugin = demoPlugin;
2327
- exports.initPlugin = initPlugin;