@karmaniverous/get-dotenv 6.0.0 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +18 -14
  2. package/dist/cli.mjs +744 -360
  3. package/dist/cliHost.d.ts +14 -1
  4. package/dist/cliHost.mjs +54 -8
  5. package/dist/config.d.ts +4 -0
  6. package/dist/config.mjs +17 -7
  7. package/dist/env-overlay.d.ts +11 -0
  8. package/dist/env-overlay.mjs +24 -7
  9. package/dist/getdotenv.cli.mjs +744 -360
  10. package/dist/index.d.ts +88 -2
  11. package/dist/index.mjs +757 -361
  12. package/dist/plugins-aws.d.ts +4 -3
  13. package/dist/plugins-aws.mjs +369 -177
  14. package/dist/plugins-batch.d.ts +2 -1
  15. package/dist/plugins-batch.mjs +124 -48
  16. package/dist/plugins-cmd.d.ts +2 -1
  17. package/dist/plugins-cmd.mjs +77 -22
  18. package/dist/plugins-init.d.ts +2 -1
  19. package/dist/plugins-init.mjs +235 -128
  20. package/dist/plugins.d.ts +7 -2
  21. package/dist/plugins.mjs +740 -360
  22. package/dist/templates/cli/index.ts +1 -2
  23. package/dist/templates/cli/plugins/hello/defaultAction.ts +27 -0
  24. package/dist/templates/cli/plugins/hello/index.ts +26 -0
  25. package/dist/templates/cli/plugins/hello/options.ts +31 -0
  26. package/dist/templates/cli/plugins/hello/strangerAction.ts +20 -0
  27. package/dist/templates/cli/plugins/hello/types.ts +13 -0
  28. package/dist/templates/config/ts/getdotenv.config.ts +1 -1
  29. package/dist/templates/defaultAction.ts +27 -0
  30. package/dist/templates/getdotenv.config.ts +1 -1
  31. package/dist/templates/hello/defaultAction.ts +27 -0
  32. package/dist/templates/hello/index.ts +26 -0
  33. package/dist/templates/hello/options.ts +31 -0
  34. package/dist/templates/hello/strangerAction.ts +20 -0
  35. package/dist/templates/hello/types.ts +13 -0
  36. package/dist/templates/index.ts +22 -22
  37. package/dist/templates/options.ts +31 -0
  38. package/dist/templates/plugins/hello/defaultAction.ts +27 -0
  39. package/dist/templates/plugins/hello/index.ts +26 -0
  40. package/dist/templates/plugins/hello/options.ts +31 -0
  41. package/dist/templates/plugins/hello/strangerAction.ts +20 -0
  42. package/dist/templates/plugins/hello/types.ts +13 -0
  43. package/dist/templates/strangerAction.ts +20 -0
  44. package/dist/templates/ts/getdotenv.config.ts +1 -1
  45. package/dist/templates/types.ts +13 -0
  46. package/package.json +2 -2
  47. package/templates/cli/index.ts +1 -2
  48. package/templates/cli/plugins/hello/defaultAction.ts +27 -0
  49. package/templates/cli/plugins/hello/index.ts +26 -0
  50. package/templates/cli/plugins/hello/options.ts +31 -0
  51. package/templates/cli/plugins/hello/strangerAction.ts +20 -0
  52. package/templates/cli/plugins/hello/types.ts +13 -0
  53. package/templates/config/ts/getdotenv.config.ts +1 -1
  54. package/dist/templates/cli/plugins/hello.ts +0 -43
  55. package/dist/templates/hello.ts +0 -43
  56. package/dist/templates/plugins/hello.ts +0 -43
  57. package/templates/cli/plugins/hello.ts +0 -43
@@ -151,6 +151,10 @@ function defaultsDeep(...layers) {
151
151
  /**
152
152
  * Serialize a dotenv record to a file with minimal quoting (multiline values are quoted).
153
153
  * Future-proofs for ordering/sorting changes (currently insertion order).
154
+ *
155
+ * @param filename - Destination dotenv file path.
156
+ * @param data - Env-like map of values to write (values may be `undefined`).
157
+ * @returns A `Promise\<void\>` which resolves when the file has been written.
154
158
  */
155
159
  async function writeDotenvFile(filename, data) {
156
160
  // Serialize: key=value with quotes only for multiline values.
@@ -386,14 +390,20 @@ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Numbe
386
390
  }
387
391
  };
388
392
  /**
389
- * Load a module default export from a JS/TS file with robust fallbacks:
390
- * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
391
- * 1) try direct import (if a TS loader is active),
392
- * 2) esbuild bundle to a temp ESM file,
393
- * 3) typescript.transpileModule fallback for simple modules.
393
+ * Load a module default export from a JS/TS file with robust fallbacks.
394
+ *
395
+ * Behavior by extension:
394
396
  *
395
- * @param absPath - absolute path to source file
396
- * @param cacheDirName - cache subfolder under .tsbuild
397
+ * - `.js`/`.mjs`/`.cjs`: direct dynamic import.
398
+ * - `.ts`/`.mts`/`.cts`/`.tsx`:
399
+ * - try direct dynamic import (when a TS loader is active),
400
+ * - else compile via `esbuild` to a cached `.mjs` file and import,
401
+ * - else fallback to `typescript.transpileModule` for simple modules.
402
+ *
403
+ * @typeParam T - Type of the expected default export.
404
+ * @param absPath - Absolute path to the source file.
405
+ * @param cacheDirName - Cache subfolder under `.tsbuild/`.
406
+ * @returns A `Promise\<T | undefined\>` resolving to the default export (if any).
397
407
  */
398
408
  const loadModuleDefault = async (absPath, cacheDirName) => {
399
409
  const ext = path.extname(absPath).toLowerCase();
@@ -465,6 +475,10 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
465
475
  /**
466
476
  * Omit keys whose runtime value is undefined from a shallow object.
467
477
  * Returns a Partial with non-undefined value types preserved.
478
+ *
479
+ * @typeParam T - Input object shape.
480
+ * @param obj - Object to filter.
481
+ * @returns A shallow copy of `obj` without keys whose value is `undefined`.
468
482
  */
469
483
  function omitUndefined(obj) {
470
484
  const out = {};
@@ -476,6 +490,10 @@ function omitUndefined(obj) {
476
490
  }
477
491
  /**
478
492
  * Specialized helper for env-like maps: drop undefined and return string-only.
493
+ *
494
+ * @typeParam V - Value type for present entries (must extend `string`).
495
+ * @param obj - Env-like record containing `string | undefined` values.
496
+ * @returns A new record containing only the keys with defined values.
479
497
  */
480
498
  function omitUndefinedRecord(obj) {
481
499
  const out = {};
@@ -690,6 +708,11 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
690
708
  * Apply a dynamic map to the target progressively.
691
709
  * - Functions receive (target, env) and may return string | undefined.
692
710
  * - Literals are assigned directly (including undefined).
711
+ *
712
+ * @param target - Mutable target environment to assign into.
713
+ * @param map - Dynamic map to apply (functions and/or literal values).
714
+ * @param env - Selected environment name (if any) passed through to dynamic functions.
715
+ * @returns Nothing.
693
716
  */
694
717
  function applyDynamicMap(target, map, env) {
695
718
  if (!map)
@@ -709,6 +732,12 @@ function applyDynamicMap(target, map, env) {
709
732
  * Error behavior:
710
733
  * - On failure to load/compile/evaluate the module, throws a unified message:
711
734
  * "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
735
+ *
736
+ * @param target - Mutable target environment to assign into.
737
+ * @param absPath - Absolute path to the dynamic module file.
738
+ * @param env - Selected environment name (if any).
739
+ * @param cacheDirName - Cache subdirectory under `.tsbuild/` for compiled artifacts.
740
+ * @returns A `Promise\<void\>` which resolves after the module (if present) has been applied.
712
741
  */
713
742
  async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
714
743
  if (!(await fs.exists(absPath)))
@@ -1799,6 +1828,10 @@ function renderOptionGroups(cmd) {
1799
1828
  /**
1800
1829
  * Compose root/parent help output by inserting grouped sections between
1801
1830
  * Options and Commands, ensuring a trailing blank line.
1831
+ *
1832
+ * @param base - Base help text produced by Commander.
1833
+ * @param cmd - Command instance whose grouped options should be rendered.
1834
+ * @returns The modified help text with grouped blocks inserted.
1802
1835
  */
1803
1836
  function buildHelpInformation(base, cmd) {
1804
1837
  const groups = renderOptionGroups(cmd);
@@ -2395,23 +2428,57 @@ const buildSpawnEnv = (base, overlay) => {
2395
2428
  * Apply resolved AWS context to `process.env` and `ctx.plugins`.
2396
2429
  * Centralizes logic shared between the plugin action and `afterResolve` hook.
2397
2430
  *
2431
+ * @param out - Resolved AWS context to apply.
2432
+ * @param ctx - Host context to publish non-sensitive metadata into.
2398
2433
  * @param setProcessEnv - Whether to write credentials/region to `process.env` (default true).
2434
+ * @returns Nothing.
2399
2435
  */
2400
2436
  function applyAwsContext(out, ctx, setProcessEnv = true) {
2401
2437
  const { profile, region, credentials } = out;
2402
2438
  if (setProcessEnv) {
2403
- if (region) {
2404
- process.env.AWS_REGION = region;
2405
- if (!process.env.AWS_DEFAULT_REGION) {
2406
- process.env.AWS_DEFAULT_REGION = region;
2439
+ // Ensure AWS credential sources are mutually exclusive.
2440
+ // The AWS SDK warns (and may change precedence in future) when both
2441
+ // AWS_PROFILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are set.
2442
+ const clear = (keys) => {
2443
+ for (const k of keys) {
2444
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
2445
+ delete process.env[k];
2407
2446
  }
2408
- }
2447
+ };
2448
+ const clearProfileVars = () => {
2449
+ clear(['AWS_PROFILE', 'AWS_DEFAULT_PROFILE', 'AWS_SDK_LOAD_CONFIG']);
2450
+ };
2451
+ const clearStaticCreds = () => {
2452
+ clear([
2453
+ 'AWS_ACCESS_KEY_ID',
2454
+ 'AWS_SECRET_ACCESS_KEY',
2455
+ 'AWS_SESSION_TOKEN',
2456
+ ]);
2457
+ };
2458
+ // Mode A: exported/static credentials (clear profile vars)
2409
2459
  if (credentials) {
2460
+ clearProfileVars();
2410
2461
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
2411
2462
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
2412
2463
  if (credentials.sessionToken !== undefined) {
2413
2464
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
2414
2465
  }
2466
+ else {
2467
+ delete process.env.AWS_SESSION_TOKEN;
2468
+ }
2469
+ }
2470
+ else if (profile) {
2471
+ // Mode B: profile-based (SSO) credentials (clear static creds)
2472
+ clearStaticCreds();
2473
+ process.env.AWS_PROFILE = profile;
2474
+ process.env.AWS_DEFAULT_PROFILE = profile;
2475
+ process.env.AWS_SDK_LOAD_CONFIG = '1';
2476
+ }
2477
+ if (region) {
2478
+ process.env.AWS_REGION = region;
2479
+ if (!process.env.AWS_DEFAULT_REGION) {
2480
+ process.env.AWS_DEFAULT_REGION = region;
2481
+ }
2415
2482
  }
2416
2483
  }
2417
2484
  // Always publish minimal, non-sensitive metadata
@@ -2422,7 +2489,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
2422
2489
  };
2423
2490
  }
2424
2491
 
2425
- const DEFAULT_TIMEOUT_MS = 15_000;
2492
+ const AWS_CLI_TIMEOUT_MS = 15_000;
2426
2493
  const trim = (s) => (typeof s === 'string' ? s.trim() : '');
2427
2494
  const unquote = (s) => s.length >= 2 &&
2428
2495
  ((s.startsWith('"') && s.endsWith('"')) ||
@@ -2431,6 +2498,9 @@ const unquote = (s) => s.length >= 2 &&
2431
2498
  : s;
2432
2499
  /**
2433
2500
  * Parse AWS credentials from JSON output (AWS CLI v2 export-credentials).
2501
+ *
2502
+ * @param txt - Raw stdout text from the AWS CLI.
2503
+ * @returns Parsed credentials, or `undefined` when the input is not recognized.
2434
2504
  */
2435
2505
  const parseExportCredentialsJson = (txt) => {
2436
2506
  try {
@@ -2454,6 +2524,10 @@ const parseExportCredentialsJson = (txt) => {
2454
2524
  /**
2455
2525
  * Parse AWS credentials from environment-export output (shell-agnostic).
2456
2526
  * Supports POSIX `export KEY=VAL` and PowerShell `$Env:KEY=VAL`.
2527
+ * Also supports AWS CLI `windows-cmd` (`set KEY=VAL`) and `env-no-export` (`KEY=VAL`).
2528
+ *
2529
+ * @param txt - Raw stdout text from the AWS CLI.
2530
+ * @returns Parsed credentials, or `undefined` when the input is not recognized.
2457
2531
  */
2458
2532
  const parseExportCredentialsEnv = (txt) => {
2459
2533
  const lines = txt.split(/\r?\n/);
@@ -2464,12 +2538,17 @@ const parseExportCredentialsEnv = (txt) => {
2464
2538
  const line = raw.trim();
2465
2539
  if (!line)
2466
2540
  continue;
2467
- // POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
2541
+ // POSIX: export AWS_ACCESS_KEY_ID=..., ...
2468
2542
  let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2469
- if (!m) {
2470
- // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
2471
- m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2472
- }
2543
+ // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
2544
+ if (!m)
2545
+ m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
2546
+ // Windows cmd: set AWS_ACCESS_KEY_ID=..., etc.
2547
+ if (!m)
2548
+ m = /^(?:set)\s+([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
2549
+ // env-no-export: AWS_ACCESS_KEY_ID=..., etc.
2550
+ if (!m)
2551
+ m = /^([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2473
2552
  if (!m)
2474
2553
  continue;
2475
2554
  const k = m[1];
@@ -2494,7 +2573,7 @@ const parseExportCredentialsEnv = (txt) => {
2494
2573
  };
2495
2574
  return undefined;
2496
2575
  };
2497
- const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
2576
+ const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
2498
2577
  const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
2499
2578
  env: process.env,
2500
2579
  timeoutMs,
@@ -2509,36 +2588,50 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
2509
2588
  }
2510
2589
  return undefined;
2511
2590
  };
2512
- const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
2513
- // Try JSON format first (AWS CLI v2)
2514
- const rJson = await runCommandResult([
2515
- 'aws',
2516
- 'configure',
2517
- 'export-credentials',
2518
- '--profile',
2519
- profile,
2520
- '--format',
2521
- 'json',
2522
- ], false, { env: process.env, timeoutMs });
2523
- if (rJson.exitCode === 0) {
2524
- const creds = parseExportCredentialsJson(rJson.stdout);
2525
- if (creds)
2526
- return creds;
2527
- }
2528
- // Fallback: env lines
2529
- const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
2530
- if (rEnv.exitCode === 0) {
2531
- const creds = parseExportCredentialsEnv(rEnv.stdout);
2591
+ const exportCredentials = async (profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
2592
+ const tryExport = async (format) => {
2593
+ const argv = [
2594
+ 'aws',
2595
+ 'configure',
2596
+ 'export-credentials',
2597
+ '--profile',
2598
+ profile,
2599
+ ...(format ? ['--format', format] : []),
2600
+ ];
2601
+ const r = await runCommandResult(argv, false, {
2602
+ env: process.env,
2603
+ timeoutMs,
2604
+ });
2605
+ if (r.exitCode !== 0)
2606
+ return undefined;
2607
+ const out = trim(r.stdout);
2608
+ if (!out)
2609
+ return undefined;
2610
+ // Some formats produce JSON ("process"), some produce shell-ish env lines.
2611
+ return parseExportCredentialsJson(out) ?? parseExportCredentialsEnv(out);
2612
+ };
2613
+ // Prefer the default/JSON "process" format first; then fall back to shell env outputs.
2614
+ // Note: AWS CLI v2 supports: process | env | env-no-export | powershell | windows-cmd
2615
+ const formats = [
2616
+ 'process',
2617
+ ...(process.platform === 'win32'
2618
+ ? ['powershell', 'windows-cmd', 'env', 'env-no-export']
2619
+ : ['env', 'env-no-export']),
2620
+ ];
2621
+ for (const f of formats) {
2622
+ const creds = await tryExport(f);
2532
2623
  if (creds)
2533
2624
  return creds;
2534
2625
  }
2535
- return undefined;
2626
+ // Final fallback: no --format (AWS CLI default output)
2627
+ return tryExport(undefined);
2536
2628
  };
2537
2629
  /**
2538
2630
  * Resolve AWS context (profile, region, credentials) using configuration and environment.
2539
2631
  * Applies strategy (cli-export vs none) and handling for SSO login-on-demand.
2540
2632
  *
2541
2633
  * @param options - Context options including current dotenv and plugin config.
2634
+ * @returns A `Promise\<AwsContext\>` containing any resolved profile, region, and credentials.
2542
2635
  */
2543
2636
  const resolveAwsContext = async ({ dotenv, cfg, }) => {
2544
2637
  const profileKey = cfg.profileKey ?? 'AWS_LOCAL_PROFILE';
@@ -2563,31 +2656,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2563
2656
  out.region = region;
2564
2657
  return out;
2565
2658
  }
2566
- // Env-first credentials.
2567
2659
  let credentials;
2568
- const envId = trim(process.env.AWS_ACCESS_KEY_ID);
2569
- const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
2570
- const envToken = trim(process.env.AWS_SESSION_TOKEN);
2571
- if (envId && envSecret) {
2572
- credentials = {
2573
- accessKeyId: envId,
2574
- secretAccessKey: envSecret,
2575
- ...(envToken ? { sessionToken: envToken } : {}),
2576
- };
2577
- }
2578
- else if (profile) {
2660
+ // Profile wins over ambient env creds when present (from flags/config/dotenv).
2661
+ if (profile) {
2579
2662
  // Try export-credentials
2580
2663
  credentials = await exportCredentials(profile);
2581
2664
  // On failure, detect SSO and optionally login then retry
2582
2665
  if (!credentials) {
2583
2666
  const ssoSession = await getAwsConfigure('sso_session', profile);
2584
- const looksSSO = typeof ssoSession === 'string' && ssoSession.length > 0;
2667
+ // Legacy SSO profiles use sso_start_url/sso_region rather than sso_session.
2668
+ const ssoStartUrl = await getAwsConfigure('sso_start_url', profile);
2669
+ const looksSSO = (typeof ssoSession === 'string' && ssoSession.length > 0) ||
2670
+ (typeof ssoStartUrl === 'string' && ssoStartUrl.length > 0);
2585
2671
  if (looksSSO && cfg.loginOnDemand) {
2586
- // Best-effort login, then retry export once.
2587
- await runCommandResult(['aws', 'sso', 'login', '--profile', profile], false, {
2672
+ // Interactive login (no timeout by default), then retry export once.
2673
+ const exit = await runCommand(['aws', 'sso', 'login', '--profile', profile], false, {
2588
2674
  env: process.env,
2589
- timeoutMs: DEFAULT_TIMEOUT_MS,
2675
+ stdio: 'inherit',
2590
2676
  });
2677
+ if (exit !== 0) {
2678
+ throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
2679
+ }
2591
2680
  credentials = await exportCredentials(profile);
2592
2681
  }
2593
2682
  }
@@ -2605,6 +2694,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2605
2694
  }
2606
2695
  }
2607
2696
  }
2697
+ else {
2698
+ // Env-first credentials when no profile is present.
2699
+ const envId = trim(process.env.AWS_ACCESS_KEY_ID);
2700
+ const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
2701
+ const envToken = trim(process.env.AWS_SESSION_TOKEN);
2702
+ if (envId && envSecret) {
2703
+ credentials = {
2704
+ accessKeyId: envId,
2705
+ secretAccessKey: envSecret,
2706
+ ...(envToken ? { sessionToken: envToken } : {}),
2707
+ };
2708
+ }
2709
+ }
2608
2710
  // Final region resolution
2609
2711
  if (!region && profile)
2610
2712
  region = await getAwsConfigure('region', profile);
@@ -2620,10 +2722,213 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2620
2722
  return out;
2621
2723
  };
2622
2724
 
2725
+ /**
2726
+ * Create the AWS plugin `afterResolve` hook.
2727
+ *
2728
+ * This runs once per invocation after the host resolves dotenv context.
2729
+ *
2730
+ * @param plugin - The AWS plugin instance.
2731
+ * @returns An `afterResolve` hook function suitable for assigning to `plugin.afterResolve`.
2732
+ *
2733
+ * @internal
2734
+ */
2735
+ function attachAwsAfterResolveHook(plugin) {
2736
+ return async (cli, ctx) => {
2737
+ const cfg = plugin.readConfig(cli);
2738
+ const out = await resolveAwsContext({
2739
+ dotenv: ctx.dotenv,
2740
+ cfg,
2741
+ });
2742
+ applyAwsContext(out, ctx, true);
2743
+ // Optional: low-noise breadcrumb for diagnostics
2744
+ if (process.env.GETDOTENV_DEBUG) {
2745
+ try {
2746
+ const msg = JSON.stringify({
2747
+ profile: out.profile,
2748
+ region: out.region,
2749
+ hasCreds: Boolean(out.credentials),
2750
+ });
2751
+ process.stderr.write(`[aws] afterResolve ${msg}\n`);
2752
+ }
2753
+ catch {
2754
+ /* ignore */
2755
+ }
2756
+ }
2757
+ };
2758
+ }
2759
+
2760
+ /** @internal */
2761
+ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
2762
+ /**
2763
+ * Create an AWS plugin config overlay from Commander-parsed option values.
2764
+ *
2765
+ * This preserves tri-state intent:
2766
+ * - If a flag was not provided, it should not overwrite config-derived defaults.
2767
+ * - If `--no-…` was provided, it must explicitly force the boolean false.
2768
+ *
2769
+ * @param opts - Commander option values for the current invocation.
2770
+ * @returns A partial AWS plugin config object containing only explicit overrides.
2771
+ *
2772
+ * @internal
2773
+ */
2774
+ function awsConfigOverridesFromCommandOpts(opts) {
2775
+ const o = isRecord(opts) ? opts : {};
2776
+ const overlay = {};
2777
+ // Map boolean toggles (respect explicit --no-*)
2778
+ if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand')) {
2779
+ overlay.loginOnDemand = Boolean(o.loginOnDemand);
2780
+ }
2781
+ // Strings/enums
2782
+ if (typeof o.profile === 'string')
2783
+ overlay.profile = o.profile;
2784
+ if (typeof o.region === 'string')
2785
+ overlay.region = o.region;
2786
+ if (typeof o.defaultRegion === 'string')
2787
+ overlay.defaultRegion = o.defaultRegion;
2788
+ if (o.strategy === 'cli-export' || o.strategy === 'none') {
2789
+ overlay.strategy = o.strategy;
2790
+ }
2791
+ // Advanced key overrides
2792
+ if (typeof o.profileKey === 'string')
2793
+ overlay.profileKey = o.profileKey;
2794
+ if (typeof o.profileFallbackKey === 'string') {
2795
+ overlay.profileFallbackKey = o.profileFallbackKey;
2796
+ }
2797
+ if (typeof o.regionKey === 'string')
2798
+ overlay.regionKey = o.regionKey;
2799
+ return overlay;
2800
+ }
2801
+
2802
+ /**
2803
+ * Attach the default action for the AWS plugin mount.
2804
+ *
2805
+ * Behavior:
2806
+ * - With args: forwards to AWS CLI (`aws <args...>`) under the established session.
2807
+ * - Without args: session-only establishment (no forward).
2808
+ *
2809
+ * @param cli - The `aws` command mount.
2810
+ * @param plugin - The AWS plugin instance.
2811
+ *
2812
+ * @internal
2813
+ */
2814
+ function attachAwsDefaultAction(cli, plugin, awsCmd) {
2815
+ awsCmd.action(async (args, opts, thisCommand) => {
2816
+ // Access merged root CLI options (installed by root hooks).
2817
+ const bag = readMergedOptions(thisCommand);
2818
+ const capture = shouldCapture(bag.capture);
2819
+ const underTests = process.env.GETDOTENV_TEST === '1' ||
2820
+ typeof process.env.VITEST_WORKER_ID === 'string';
2821
+ // Build overlay cfg from subcommand flags layered over discovered config.
2822
+ const ctx = cli.getCtx();
2823
+ const cfgBase = plugin.readConfig(cli);
2824
+ const cfg = {
2825
+ ...cfgBase,
2826
+ ...awsConfigOverridesFromCommandOpts(opts),
2827
+ };
2828
+ // Resolve current context with overrides
2829
+ const out = await resolveAwsContext({
2830
+ dotenv: ctx.dotenv,
2831
+ cfg,
2832
+ });
2833
+ // Publish env/context
2834
+ applyAwsContext(out, ctx, true);
2835
+ // Forward when positional args are present; otherwise session-only.
2836
+ if (args.length > 0) {
2837
+ const argv = ['aws', ...args];
2838
+ const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
2839
+ const exit = await runCommand(argv, shellSetting, {
2840
+ env: buildSpawnEnv(process.env, ctx.dotenv),
2841
+ stdio: capture ? 'pipe' : 'inherit',
2842
+ });
2843
+ // Deterministic termination (suppressed under tests)
2844
+ if (!underTests) {
2845
+ process.exit(typeof exit === 'number' ? exit : 0);
2846
+ }
2847
+ return;
2848
+ }
2849
+ // Session only: low-noise breadcrumb under debug
2850
+ if (process.env.GETDOTENV_DEBUG) {
2851
+ try {
2852
+ const msg = JSON.stringify({
2853
+ profile: out.profile,
2854
+ region: out.region,
2855
+ hasCreds: Boolean(out.credentials),
2856
+ });
2857
+ process.stderr.write(`[aws] session established ${msg}\n`);
2858
+ }
2859
+ catch {
2860
+ /* ignore */
2861
+ }
2862
+ }
2863
+ if (!underTests)
2864
+ process.exit(0);
2865
+ });
2866
+ }
2867
+
2868
+ /**
2869
+ * Attach options/arguments for the AWS plugin mount.
2870
+ *
2871
+ * @param cli - The `aws` command mount.
2872
+ * @param plugin - The AWS plugin instance (for dynamic option descriptions).
2873
+ *
2874
+ * @internal
2875
+ */
2876
+ function attachAwsOptions(cli, plugin) {
2877
+ return (cli
2878
+ // Description is owned by the plugin index (src/plugins/aws/index.ts).
2879
+ .enablePositionalOptions()
2880
+ .passThroughOptions()
2881
+ .allowUnknownOption(true)
2882
+ // Boolean toggles with dynamic help labels (effective defaults)
2883
+ .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
2884
+ .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
2885
+ // Strings / enums
2886
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
2887
+ .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
2888
+ .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
2889
+ .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
2890
+ // Advanced key overrides
2891
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
2892
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
2893
+ .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
2894
+ // Accept any extra operands so Commander does not error when tokens appear after "--".
2895
+ .argument('[args...]'));
2896
+ }
2897
+
2898
+ /**
2899
+ * Attach the AWS plugin `preSubcommand` hook.
2900
+ *
2901
+ * Ensures `aws --profile/--region <child>` applies the AWS session setup before
2902
+ * child subcommand execution.
2903
+ *
2904
+ * @param cli - The `aws` command mount.
2905
+ * @param plugin - The AWS plugin instance.
2906
+ *
2907
+ * @internal
2908
+ */
2909
+ function attachAwsPreSubcommandHook(cli, plugin) {
2910
+ cli.hook('preSubcommand', async (thisCommand) => {
2911
+ // Avoid side effects for help rendering.
2912
+ if (process.argv.includes('-h') || process.argv.includes('--help'))
2913
+ return;
2914
+ const ctx = cli.getCtx();
2915
+ const cfgBase = plugin.readConfig(cli);
2916
+ const cfg = {
2917
+ ...cfgBase,
2918
+ ...awsConfigOverridesFromCommandOpts(thisCommand.opts()),
2919
+ };
2920
+ const out = await resolveAwsContext({
2921
+ dotenv: ctx.dotenv,
2922
+ cfg,
2923
+ });
2924
+ applyAwsContext(out, ctx, true);
2925
+ });
2926
+ }
2927
+
2623
2928
  /**
2624
2929
  * Zod schema for AWS plugin configuration.
2625
2930
  */
2626
- const AwsPluginConfigSchema = z.object({
2931
+ const awsPluginConfigSchema = z.object({
2627
2932
  profile: z.string().optional(),
2628
2933
  region: z.string().optional(),
2629
2934
  defaultRegion: z.string().optional(),
@@ -2647,129 +2952,16 @@ const AwsPluginConfigSchema = z.object({
2647
2952
  const awsPlugin = () => {
2648
2953
  const plugin = definePlugin({
2649
2954
  ns: 'aws',
2650
- configSchema: AwsPluginConfigSchema,
2651
- setup: (cli) => {
2652
- // Mount: aws (provided)
2653
- cli
2654
- .description('Establish an AWS session and optionally forward to the AWS CLI')
2655
- .enablePositionalOptions()
2656
- .passThroughOptions()
2657
- .allowUnknownOption(true)
2658
- // Boolean toggles with dynamic help labels (effective defaults)
2659
- .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
2660
- .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
2661
- // Strings / enums
2662
- .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
2663
- .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
2664
- .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
2665
- .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
2666
- // Advanced key overrides
2667
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
2668
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
2669
- .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
2670
- // Accept any extra operands so Commander does not error when tokens appear after "--".
2671
- .argument('[args...]')
2672
- .action(async (args, opts, thisCommand) => {
2673
- const pluginInst = plugin;
2674
- // Access merged root CLI options (installed by passOptions())
2675
- const bag = readMergedOptions(thisCommand);
2676
- const capture = shouldCapture(bag.capture);
2677
- const underTests = process.env.GETDOTENV_TEST === '1' ||
2678
- typeof process.env.VITEST_WORKER_ID === 'string';
2679
- // Build overlay cfg from subcommand flags layered over discovered config.
2680
- const ctx = cli.getCtx();
2681
- const cfgBase = pluginInst.readConfig(cli);
2682
- const o = opts;
2683
- const overlay = {};
2684
- // Map boolean toggles (respect explicit --no-*)
2685
- if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
2686
- overlay.loginOnDemand = Boolean(o.loginOnDemand);
2687
- // Strings/enums
2688
- if (typeof o.profile === 'string')
2689
- overlay.profile = o.profile;
2690
- if (typeof o.region === 'string')
2691
- overlay.region = o.region;
2692
- if (typeof o.defaultRegion === 'string')
2693
- overlay.defaultRegion = o.defaultRegion;
2694
- if (typeof o.strategy === 'string')
2695
- overlay.strategy = o.strategy;
2696
- // Advanced key overrides
2697
- if (typeof o.profileKey === 'string')
2698
- overlay.profileKey = o.profileKey;
2699
- if (typeof o.profileFallbackKey === 'string')
2700
- overlay.profileFallbackKey = o.profileFallbackKey;
2701
- if (typeof o.regionKey === 'string')
2702
- overlay.regionKey = o.regionKey;
2703
- const cfg = {
2704
- ...cfgBase,
2705
- ...overlay,
2706
- };
2707
- // Resolve current context with overrides
2708
- const out = await resolveAwsContext({
2709
- dotenv: ctx.dotenv,
2710
- cfg,
2711
- });
2712
- // Publish env/context
2713
- applyAwsContext(out, ctx, true);
2714
- // Forward when positional args are present; otherwise session-only.
2715
- if (Array.isArray(args) && args.length > 0) {
2716
- const argv = ['aws', ...args];
2717
- const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
2718
- const exit = await runCommand(argv, shellSetting, {
2719
- env: buildSpawnEnv(process.env, ctx.dotenv),
2720
- stdio: capture ? 'pipe' : 'inherit',
2721
- });
2722
- // Deterministic termination (suppressed under tests)
2723
- if (!underTests) {
2724
- process.exit(typeof exit === 'number' ? exit : 0);
2725
- }
2726
- return;
2727
- }
2728
- else {
2729
- // Session only: low-noise breadcrumb under debug
2730
- if (process.env.GETDOTENV_DEBUG) {
2731
- try {
2732
- const msg = JSON.stringify({
2733
- profile: out.profile,
2734
- region: out.region,
2735
- hasCreds: Boolean(out.credentials),
2736
- });
2737
- process.stderr.write(`[aws] session established ${msg}\n`);
2738
- }
2739
- catch {
2740
- /* ignore */
2741
- }
2742
- }
2743
- if (!underTests)
2744
- process.exit(0);
2745
- return;
2746
- }
2747
- });
2955
+ configSchema: awsPluginConfigSchema,
2956
+ setup(cli) {
2957
+ cli.description('Establish an AWS session and optionally forward to the AWS CLI');
2958
+ const awsCmd = attachAwsOptions(cli, plugin);
2959
+ attachAwsPreSubcommandHook(cli, plugin);
2960
+ attachAwsDefaultAction(cli, plugin, awsCmd);
2748
2961
  return undefined;
2749
2962
  },
2750
- afterResolve: async (_cli, ctx) => {
2751
- const cfg = plugin.readConfig(_cli);
2752
- const out = await resolveAwsContext({
2753
- dotenv: ctx.dotenv,
2754
- cfg,
2755
- });
2756
- applyAwsContext(out, ctx, true);
2757
- // Optional: low-noise breadcrumb for diagnostics
2758
- if (process.env.GETDOTENV_DEBUG) {
2759
- try {
2760
- const msg = JSON.stringify({
2761
- profile: out.profile,
2762
- region: out.region,
2763
- hasCreds: Boolean(out.credentials),
2764
- });
2765
- process.stderr.write(`[aws] afterResolve ${msg}\n`);
2766
- }
2767
- catch {
2768
- /* ignore */
2769
- }
2770
- }
2771
- },
2772
2963
  });
2964
+ plugin.afterResolve = attachAwsAfterResolveHook(plugin);
2773
2965
  return plugin;
2774
2966
  };
2775
2967