@karmaniverous/get-dotenv 6.1.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 (44) hide show
  1. package/README.md +18 -14
  2. package/dist/cli.mjs +645 -350
  3. package/dist/getdotenv.cli.mjs +645 -350
  4. package/dist/index.mjs +645 -350
  5. package/dist/plugins-aws.d.ts +4 -3
  6. package/dist/plugins-aws.mjs +319 -170
  7. package/dist/plugins-batch.d.ts +2 -1
  8. package/dist/plugins-batch.mjs +76 -41
  9. package/dist/plugins-cmd.d.ts +2 -1
  10. package/dist/plugins-cmd.mjs +29 -15
  11. package/dist/plugins-init.d.ts +2 -1
  12. package/dist/plugins-init.mjs +158 -118
  13. package/dist/plugins.d.ts +7 -2
  14. package/dist/plugins.mjs +645 -350
  15. package/dist/templates/cli/plugins/hello/defaultAction.ts +27 -0
  16. package/dist/templates/cli/plugins/hello/index.ts +26 -0
  17. package/dist/templates/cli/plugins/hello/options.ts +31 -0
  18. package/dist/templates/cli/plugins/hello/strangerAction.ts +20 -0
  19. package/dist/templates/cli/plugins/hello/types.ts +13 -0
  20. package/dist/templates/defaultAction.ts +27 -0
  21. package/dist/templates/hello/defaultAction.ts +27 -0
  22. package/dist/templates/hello/index.ts +26 -0
  23. package/dist/templates/hello/options.ts +31 -0
  24. package/dist/templates/hello/strangerAction.ts +20 -0
  25. package/dist/templates/hello/types.ts +13 -0
  26. package/dist/templates/index.ts +23 -22
  27. package/dist/templates/options.ts +31 -0
  28. package/dist/templates/plugins/hello/defaultAction.ts +27 -0
  29. package/dist/templates/plugins/hello/index.ts +26 -0
  30. package/dist/templates/plugins/hello/options.ts +31 -0
  31. package/dist/templates/plugins/hello/strangerAction.ts +20 -0
  32. package/dist/templates/plugins/hello/types.ts +13 -0
  33. package/dist/templates/strangerAction.ts +20 -0
  34. package/dist/templates/types.ts +13 -0
  35. package/package.json +2 -2
  36. package/templates/cli/plugins/hello/defaultAction.ts +27 -0
  37. package/templates/cli/plugins/hello/index.ts +26 -0
  38. package/templates/cli/plugins/hello/options.ts +31 -0
  39. package/templates/cli/plugins/hello/strangerAction.ts +20 -0
  40. package/templates/cli/plugins/hello/types.ts +13 -0
  41. package/dist/templates/cli/plugins/hello.ts +0 -42
  42. package/dist/templates/hello.ts +0 -42
  43. package/dist/templates/plugins/hello.ts +0 -42
  44. package/templates/cli/plugins/hello.ts +0 -42
@@ -292,7 +292,7 @@ interface PluginWithInstanceHelpers<TOptions extends GetDotenvOptions = GetDoten
292
292
  /**
293
293
  * Zod schema for AWS plugin configuration.
294
294
  */
295
- declare const AwsPluginConfigSchema: z.ZodObject<{
295
+ declare const awsPluginConfigSchema: z.ZodObject<{
296
296
  profile: z.ZodOptional<z.ZodString>;
297
297
  region: z.ZodOptional<z.ZodString>;
298
298
  defaultRegion: z.ZodOptional<z.ZodString>;
@@ -308,7 +308,7 @@ declare const AwsPluginConfigSchema: z.ZodObject<{
308
308
  /**
309
309
  * AWS plugin configuration object.
310
310
  */
311
- type AwsPluginConfig = z.infer<typeof AwsPluginConfigSchema>;
311
+ type AwsPluginConfig = z.infer<typeof awsPluginConfigSchema>;
312
312
  /**
313
313
  * Arguments for resolving AWS context (profile/region/credentials).
314
314
  *
@@ -338,6 +338,7 @@ declare const awsPlugin: () => PluginWithInstanceHelpers<GetDotenvOptions, {
338
338
  strategy?: "cli-export" | "none" | undefined;
339
339
  loginOnDemand?: boolean | undefined;
340
340
  }, [], {}, {}>;
341
+ type AwsPlugin = ReturnType<typeof awsPlugin>;
341
342
 
342
343
  export { awsPlugin };
343
- export type { ResolveAwsContextOptions };
344
+ export type { AwsPlugin, ResolveAwsContextOptions };
@@ -2436,18 +2436,49 @@ const buildSpawnEnv = (base, overlay) => {
2436
2436
  function applyAwsContext(out, ctx, setProcessEnv = true) {
2437
2437
  const { profile, region, credentials } = out;
2438
2438
  if (setProcessEnv) {
2439
- if (region) {
2440
- process.env.AWS_REGION = region;
2441
- if (!process.env.AWS_DEFAULT_REGION) {
2442
- 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];
2443
2446
  }
2444
- }
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)
2445
2459
  if (credentials) {
2460
+ clearProfileVars();
2446
2461
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
2447
2462
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
2448
2463
  if (credentials.sessionToken !== undefined) {
2449
2464
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
2450
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
+ }
2451
2482
  }
2452
2483
  }
2453
2484
  // Always publish minimal, non-sensitive metadata
@@ -2458,7 +2489,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
2458
2489
  };
2459
2490
  }
2460
2491
 
2461
- const DEFAULT_TIMEOUT_MS = 15_000;
2492
+ const AWS_CLI_TIMEOUT_MS = 15_000;
2462
2493
  const trim = (s) => (typeof s === 'string' ? s.trim() : '');
2463
2494
  const unquote = (s) => s.length >= 2 &&
2464
2495
  ((s.startsWith('"') && s.endsWith('"')) ||
@@ -2493,6 +2524,7 @@ const parseExportCredentialsJson = (txt) => {
2493
2524
  /**
2494
2525
  * Parse AWS credentials from environment-export output (shell-agnostic).
2495
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`).
2496
2528
  *
2497
2529
  * @param txt - Raw stdout text from the AWS CLI.
2498
2530
  * @returns Parsed credentials, or `undefined` when the input is not recognized.
@@ -2506,12 +2538,17 @@ const parseExportCredentialsEnv = (txt) => {
2506
2538
  const line = raw.trim();
2507
2539
  if (!line)
2508
2540
  continue;
2509
- // POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
2541
+ // POSIX: export AWS_ACCESS_KEY_ID=..., ...
2510
2542
  let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2511
- if (!m) {
2512
- // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
2513
- m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2514
- }
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);
2515
2552
  if (!m)
2516
2553
  continue;
2517
2554
  const k = m[1];
@@ -2536,7 +2573,7 @@ const parseExportCredentialsEnv = (txt) => {
2536
2573
  };
2537
2574
  return undefined;
2538
2575
  };
2539
- const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
2576
+ const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
2540
2577
  const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
2541
2578
  env: process.env,
2542
2579
  timeoutMs,
@@ -2551,30 +2588,43 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
2551
2588
  }
2552
2589
  return undefined;
2553
2590
  };
2554
- const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
2555
- // Try JSON format first (AWS CLI v2)
2556
- const rJson = await runCommandResult([
2557
- 'aws',
2558
- 'configure',
2559
- 'export-credentials',
2560
- '--profile',
2561
- profile,
2562
- '--format',
2563
- 'json',
2564
- ], false, { env: process.env, timeoutMs });
2565
- if (rJson.exitCode === 0) {
2566
- const creds = parseExportCredentialsJson(rJson.stdout);
2567
- if (creds)
2568
- return creds;
2569
- }
2570
- // Fallback: env lines
2571
- const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
2572
- if (rEnv.exitCode === 0) {
2573
- 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);
2574
2623
  if (creds)
2575
2624
  return creds;
2576
2625
  }
2577
- return undefined;
2626
+ // Final fallback: no --format (AWS CLI default output)
2627
+ return tryExport(undefined);
2578
2628
  };
2579
2629
  /**
2580
2630
  * Resolve AWS context (profile, region, credentials) using configuration and environment.
@@ -2606,31 +2656,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2606
2656
  out.region = region;
2607
2657
  return out;
2608
2658
  }
2609
- // Env-first credentials.
2610
2659
  let credentials;
2611
- const envId = trim(process.env.AWS_ACCESS_KEY_ID);
2612
- const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
2613
- const envToken = trim(process.env.AWS_SESSION_TOKEN);
2614
- if (envId && envSecret) {
2615
- credentials = {
2616
- accessKeyId: envId,
2617
- secretAccessKey: envSecret,
2618
- ...(envToken ? { sessionToken: envToken } : {}),
2619
- };
2620
- }
2621
- else if (profile) {
2660
+ // Profile wins over ambient env creds when present (from flags/config/dotenv).
2661
+ if (profile) {
2622
2662
  // Try export-credentials
2623
2663
  credentials = await exportCredentials(profile);
2624
2664
  // On failure, detect SSO and optionally login then retry
2625
2665
  if (!credentials) {
2626
2666
  const ssoSession = await getAwsConfigure('sso_session', profile);
2627
- 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);
2628
2671
  if (looksSSO && cfg.loginOnDemand) {
2629
- // Best-effort login, then retry export once.
2630
- 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, {
2631
2674
  env: process.env,
2632
- timeoutMs: DEFAULT_TIMEOUT_MS,
2675
+ stdio: 'inherit',
2633
2676
  });
2677
+ if (exit !== 0) {
2678
+ throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
2679
+ }
2634
2680
  credentials = await exportCredentials(profile);
2635
2681
  }
2636
2682
  }
@@ -2648,6 +2694,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2648
2694
  }
2649
2695
  }
2650
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
+ }
2651
2710
  // Final region resolution
2652
2711
  if (!region && profile)
2653
2712
  region = await getAwsConfigure('region', profile);
@@ -2663,10 +2722,213 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2663
2722
  return out;
2664
2723
  };
2665
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
+
2666
2928
  /**
2667
2929
  * Zod schema for AWS plugin configuration.
2668
2930
  */
2669
- const AwsPluginConfigSchema = z.object({
2931
+ const awsPluginConfigSchema = z.object({
2670
2932
  profile: z.string().optional(),
2671
2933
  region: z.string().optional(),
2672
2934
  defaultRegion: z.string().optional(),
@@ -2690,129 +2952,16 @@ const AwsPluginConfigSchema = z.object({
2690
2952
  const awsPlugin = () => {
2691
2953
  const plugin = definePlugin({
2692
2954
  ns: 'aws',
2693
- configSchema: AwsPluginConfigSchema,
2694
- setup: (cli) => {
2695
- // Mount: aws (provided)
2696
- cli
2697
- .description('Establish an AWS session and optionally forward to the AWS CLI')
2698
- .enablePositionalOptions()
2699
- .passThroughOptions()
2700
- .allowUnknownOption(true)
2701
- // Boolean toggles with dynamic help labels (effective defaults)
2702
- .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
2703
- .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
2704
- // Strings / enums
2705
- .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
2706
- .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
2707
- .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
2708
- .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
2709
- // Advanced key overrides
2710
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
2711
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
2712
- .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
2713
- // Accept any extra operands so Commander does not error when tokens appear after "--".
2714
- .argument('[args...]')
2715
- .action(async (args, opts, thisCommand) => {
2716
- const pluginInst = plugin;
2717
- // Access merged root CLI options (installed by passOptions())
2718
- const bag = readMergedOptions(thisCommand);
2719
- const capture = shouldCapture(bag.capture);
2720
- const underTests = process.env.GETDOTENV_TEST === '1' ||
2721
- typeof process.env.VITEST_WORKER_ID === 'string';
2722
- // Build overlay cfg from subcommand flags layered over discovered config.
2723
- const ctx = cli.getCtx();
2724
- const cfgBase = pluginInst.readConfig(cli);
2725
- const o = opts;
2726
- const overlay = {};
2727
- // Map boolean toggles (respect explicit --no-*)
2728
- if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
2729
- overlay.loginOnDemand = Boolean(o.loginOnDemand);
2730
- // Strings/enums
2731
- if (typeof o.profile === 'string')
2732
- overlay.profile = o.profile;
2733
- if (typeof o.region === 'string')
2734
- overlay.region = o.region;
2735
- if (typeof o.defaultRegion === 'string')
2736
- overlay.defaultRegion = o.defaultRegion;
2737
- if (typeof o.strategy === 'string')
2738
- overlay.strategy = o.strategy;
2739
- // Advanced key overrides
2740
- if (typeof o.profileKey === 'string')
2741
- overlay.profileKey = o.profileKey;
2742
- if (typeof o.profileFallbackKey === 'string')
2743
- overlay.profileFallbackKey = o.profileFallbackKey;
2744
- if (typeof o.regionKey === 'string')
2745
- overlay.regionKey = o.regionKey;
2746
- const cfg = {
2747
- ...cfgBase,
2748
- ...overlay,
2749
- };
2750
- // Resolve current context with overrides
2751
- const out = await resolveAwsContext({
2752
- dotenv: ctx.dotenv,
2753
- cfg,
2754
- });
2755
- // Publish env/context
2756
- applyAwsContext(out, ctx, true);
2757
- // Forward when positional args are present; otherwise session-only.
2758
- if (Array.isArray(args) && args.length > 0) {
2759
- const argv = ['aws', ...args];
2760
- const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
2761
- const exit = await runCommand(argv, shellSetting, {
2762
- env: buildSpawnEnv(process.env, ctx.dotenv),
2763
- stdio: capture ? 'pipe' : 'inherit',
2764
- });
2765
- // Deterministic termination (suppressed under tests)
2766
- if (!underTests) {
2767
- process.exit(typeof exit === 'number' ? exit : 0);
2768
- }
2769
- return;
2770
- }
2771
- else {
2772
- // Session only: low-noise breadcrumb under debug
2773
- if (process.env.GETDOTENV_DEBUG) {
2774
- try {
2775
- const msg = JSON.stringify({
2776
- profile: out.profile,
2777
- region: out.region,
2778
- hasCreds: Boolean(out.credentials),
2779
- });
2780
- process.stderr.write(`[aws] session established ${msg}\n`);
2781
- }
2782
- catch {
2783
- /* ignore */
2784
- }
2785
- }
2786
- if (!underTests)
2787
- process.exit(0);
2788
- return;
2789
- }
2790
- });
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);
2791
2961
  return undefined;
2792
2962
  },
2793
- afterResolve: async (_cli, ctx) => {
2794
- const cfg = plugin.readConfig(_cli);
2795
- const out = await resolveAwsContext({
2796
- dotenv: ctx.dotenv,
2797
- cfg,
2798
- });
2799
- applyAwsContext(out, ctx, true);
2800
- // Optional: low-noise breadcrumb for diagnostics
2801
- if (process.env.GETDOTENV_DEBUG) {
2802
- try {
2803
- const msg = JSON.stringify({
2804
- profile: out.profile,
2805
- region: out.region,
2806
- hasCreds: Boolean(out.credentials),
2807
- });
2808
- process.stderr.write(`[aws] afterResolve ${msg}\n`);
2809
- }
2810
- catch {
2811
- /* ignore */
2812
- }
2813
- }
2814
- },
2815
2963
  });
2964
+ plugin.afterResolve = attachAwsAfterResolveHook(plugin);
2816
2965
  return plugin;
2817
2966
  };
2818
2967
 
@@ -426,6 +426,7 @@ declare const batchPlugin: (opts?: BatchPluginOptions) => PluginWithInstanceHelp
426
426
  globs?: string | undefined;
427
427
  pkgCwd?: boolean | undefined;
428
428
  }, [], {}, {}>;
429
+ type BatchPlugin = ReturnType<typeof batchPlugin>;
429
430
 
430
431
  export { batchPlugin };
431
- export type { BatchCmdSubcommandOptions, BatchGlobPathsOptions, BatchParentInvokerFlags, ExecShellCommandBatchOptions };
432
+ export type { BatchCmdSubcommandOptions, BatchGlobPathsOptions, BatchParentInvokerFlags, BatchPlugin, ExecShellCommandBatchOptions };