@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
package/dist/plugins.mjs CHANGED
@@ -169,6 +169,10 @@ function defaultsDeep(...layers) {
169
169
  /**
170
170
  * Serialize a dotenv record to a file with minimal quoting (multiline values are quoted).
171
171
  * Future-proofs for ordering/sorting changes (currently insertion order).
172
+ *
173
+ * @param filename - Destination dotenv file path.
174
+ * @param data - Env-like map of values to write (values may be `undefined`).
175
+ * @returns A `Promise\<void\>` which resolves when the file has been written.
172
176
  */
173
177
  async function writeDotenvFile(filename, data) {
174
178
  // Serialize: key=value with quotes only for multiline values.
@@ -404,14 +408,20 @@ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Numbe
404
408
  }
405
409
  };
406
410
  /**
407
- * Load a module default export from a JS/TS file with robust fallbacks:
408
- * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
409
- * 1) try direct import (if a TS loader is active),
410
- * 2) esbuild bundle to a temp ESM file,
411
- * 3) typescript.transpileModule fallback for simple modules.
411
+ * Load a module default export from a JS/TS file with robust fallbacks.
412
+ *
413
+ * Behavior by extension:
414
+ *
415
+ * - `.js`/`.mjs`/`.cjs`: direct dynamic import.
416
+ * - `.ts`/`.mts`/`.cts`/`.tsx`:
417
+ * - try direct dynamic import (when a TS loader is active),
418
+ * - else compile via `esbuild` to a cached `.mjs` file and import,
419
+ * - else fallback to `typescript.transpileModule` for simple modules.
412
420
  *
413
- * @param absPath - absolute path to source file
414
- * @param cacheDirName - cache subfolder under .tsbuild
421
+ * @typeParam T - Type of the expected default export.
422
+ * @param absPath - Absolute path to the source file.
423
+ * @param cacheDirName - Cache subfolder under `.tsbuild/`.
424
+ * @returns A `Promise\<T | undefined\>` resolving to the default export (if any).
415
425
  */
416
426
  const loadModuleDefault = async (absPath, cacheDirName) => {
417
427
  const ext = path.extname(absPath).toLowerCase();
@@ -483,6 +493,10 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
483
493
  /**
484
494
  * Omit keys whose runtime value is undefined from a shallow object.
485
495
  * Returns a Partial with non-undefined value types preserved.
496
+ *
497
+ * @typeParam T - Input object shape.
498
+ * @param obj - Object to filter.
499
+ * @returns A shallow copy of `obj` without keys whose value is `undefined`.
486
500
  */
487
501
  function omitUndefined(obj) {
488
502
  const out = {};
@@ -494,6 +508,10 @@ function omitUndefined(obj) {
494
508
  }
495
509
  /**
496
510
  * Specialized helper for env-like maps: drop undefined and return string-only.
511
+ *
512
+ * @typeParam V - Value type for present entries (must extend `string`).
513
+ * @param obj - Env-like record containing `string | undefined` values.
514
+ * @returns A new record containing only the keys with defined values.
497
515
  */
498
516
  function omitUndefinedRecord(obj) {
499
517
  const out = {};
@@ -714,6 +732,11 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
714
732
  * Apply a dynamic map to the target progressively.
715
733
  * - Functions receive (target, env) and may return string | undefined.
716
734
  * - Literals are assigned directly (including undefined).
735
+ *
736
+ * @param target - Mutable target environment to assign into.
737
+ * @param map - Dynamic map to apply (functions and/or literal values).
738
+ * @param env - Selected environment name (if any) passed through to dynamic functions.
739
+ * @returns Nothing.
717
740
  */
718
741
  function applyDynamicMap(target, map, env) {
719
742
  if (!map)
@@ -733,6 +756,12 @@ function applyDynamicMap(target, map, env) {
733
756
  * Error behavior:
734
757
  * - On failure to load/compile/evaluate the module, throws a unified message:
735
758
  * "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
759
+ *
760
+ * @param target - Mutable target environment to assign into.
761
+ * @param absPath - Absolute path to the dynamic module file.
762
+ * @param env - Selected environment name (if any).
763
+ * @param cacheDirName - Cache subdirectory under `.tsbuild/` for compiled artifacts.
764
+ * @returns A `Promise\<void\>` which resolves after the module (if present) has been applied.
736
765
  */
737
766
  async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
738
767
  if (!(await fs.exists(absPath)))
@@ -1895,6 +1924,10 @@ function renderOptionGroups(cmd) {
1895
1924
  /**
1896
1925
  * Compose root/parent help output by inserting grouped sections between
1897
1926
  * Options and Commands, ensuring a trailing blank line.
1927
+ *
1928
+ * @param base - Base help text produced by Commander.
1929
+ * @param cmd - Command instance whose grouped options should be rendered.
1930
+ * @returns The modified help text with grouped blocks inserted.
1898
1931
  */
1899
1932
  function buildHelpInformation(base, cmd) {
1900
1933
  const groups = renderOptionGroups(cmd);
@@ -2396,6 +2429,10 @@ const baseGetDotenvCliOptions = baseRootOptionDefaults;
2396
2429
  /**
2397
2430
  * Compose a child-process env overlay from dotenv and the merged CLI options bag.
2398
2431
  * Returns a shallow object including getDotenvCliOptions when serializable.
2432
+ *
2433
+ * @param merged - Resolved CLI options bag (or a JSON-serializable subset).
2434
+ * @param dotenv - Composed dotenv variables for the current invocation.
2435
+ * @returns A string-only env overlay suitable for child process spawning.
2399
2436
  */
2400
2437
  function composeNestedEnv(merged, dotenv) {
2401
2438
  const out = {};
@@ -2418,6 +2455,7 @@ function composeNestedEnv(merged, dotenv) {
2418
2455
  * Strip one layer of symmetric outer quotes (single or double) from a string.
2419
2456
  *
2420
2457
  * @param s - Input string.
2458
+ * @returns `s` without one symmetric outer quote pair (when present).
2421
2459
  */
2422
2460
  const stripOne = (s) => {
2423
2461
  if (s.length < 2)
@@ -2430,6 +2468,9 @@ const stripOne = (s) => {
2430
2468
  /**
2431
2469
  * Preserve argv array for Node -e/--eval payloads under shell-off and
2432
2470
  * peel one symmetric outer quote layer from the code argument.
2471
+ *
2472
+ * @param args - Argument vector intended for direct execution (shell-off).
2473
+ * @returns Either the original `args` or a modified copy with a normalized eval payload.
2433
2474
  */
2434
2475
  function maybePreserveNodeEvalArgv(args) {
2435
2476
  if (args.length >= 3) {
@@ -2676,23 +2717,57 @@ const buildSpawnEnv = (base, overlay) => {
2676
2717
  * Apply resolved AWS context to `process.env` and `ctx.plugins`.
2677
2718
  * Centralizes logic shared between the plugin action and `afterResolve` hook.
2678
2719
  *
2720
+ * @param out - Resolved AWS context to apply.
2721
+ * @param ctx - Host context to publish non-sensitive metadata into.
2679
2722
  * @param setProcessEnv - Whether to write credentials/region to `process.env` (default true).
2723
+ * @returns Nothing.
2680
2724
  */
2681
2725
  function applyAwsContext(out, ctx, setProcessEnv = true) {
2682
2726
  const { profile, region, credentials } = out;
2683
2727
  if (setProcessEnv) {
2684
- if (region) {
2685
- process.env.AWS_REGION = region;
2686
- if (!process.env.AWS_DEFAULT_REGION) {
2687
- process.env.AWS_DEFAULT_REGION = region;
2728
+ // Ensure AWS credential sources are mutually exclusive.
2729
+ // The AWS SDK warns (and may change precedence in future) when both
2730
+ // AWS_PROFILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are set.
2731
+ const clear = (keys) => {
2732
+ for (const k of keys) {
2733
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
2734
+ delete process.env[k];
2688
2735
  }
2689
- }
2736
+ };
2737
+ const clearProfileVars = () => {
2738
+ clear(['AWS_PROFILE', 'AWS_DEFAULT_PROFILE', 'AWS_SDK_LOAD_CONFIG']);
2739
+ };
2740
+ const clearStaticCreds = () => {
2741
+ clear([
2742
+ 'AWS_ACCESS_KEY_ID',
2743
+ 'AWS_SECRET_ACCESS_KEY',
2744
+ 'AWS_SESSION_TOKEN',
2745
+ ]);
2746
+ };
2747
+ // Mode A: exported/static credentials (clear profile vars)
2690
2748
  if (credentials) {
2749
+ clearProfileVars();
2691
2750
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
2692
2751
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
2693
2752
  if (credentials.sessionToken !== undefined) {
2694
2753
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
2695
2754
  }
2755
+ else {
2756
+ delete process.env.AWS_SESSION_TOKEN;
2757
+ }
2758
+ }
2759
+ else if (profile) {
2760
+ // Mode B: profile-based (SSO) credentials (clear static creds)
2761
+ clearStaticCreds();
2762
+ process.env.AWS_PROFILE = profile;
2763
+ process.env.AWS_DEFAULT_PROFILE = profile;
2764
+ process.env.AWS_SDK_LOAD_CONFIG = '1';
2765
+ }
2766
+ if (region) {
2767
+ process.env.AWS_REGION = region;
2768
+ if (!process.env.AWS_DEFAULT_REGION) {
2769
+ process.env.AWS_DEFAULT_REGION = region;
2770
+ }
2696
2771
  }
2697
2772
  }
2698
2773
  // Always publish minimal, non-sensitive metadata
@@ -2703,7 +2778,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
2703
2778
  };
2704
2779
  }
2705
2780
 
2706
- const DEFAULT_TIMEOUT_MS = 15_000;
2781
+ const AWS_CLI_TIMEOUT_MS = 15_000;
2707
2782
  const trim = (s) => (typeof s === 'string' ? s.trim() : '');
2708
2783
  const unquote = (s) => s.length >= 2 &&
2709
2784
  ((s.startsWith('"') && s.endsWith('"')) ||
@@ -2712,6 +2787,9 @@ const unquote = (s) => s.length >= 2 &&
2712
2787
  : s;
2713
2788
  /**
2714
2789
  * Parse AWS credentials from JSON output (AWS CLI v2 export-credentials).
2790
+ *
2791
+ * @param txt - Raw stdout text from the AWS CLI.
2792
+ * @returns Parsed credentials, or `undefined` when the input is not recognized.
2715
2793
  */
2716
2794
  const parseExportCredentialsJson = (txt) => {
2717
2795
  try {
@@ -2735,6 +2813,10 @@ const parseExportCredentialsJson = (txt) => {
2735
2813
  /**
2736
2814
  * Parse AWS credentials from environment-export output (shell-agnostic).
2737
2815
  * Supports POSIX `export KEY=VAL` and PowerShell `$Env:KEY=VAL`.
2816
+ * Also supports AWS CLI `windows-cmd` (`set KEY=VAL`) and `env-no-export` (`KEY=VAL`).
2817
+ *
2818
+ * @param txt - Raw stdout text from the AWS CLI.
2819
+ * @returns Parsed credentials, or `undefined` when the input is not recognized.
2738
2820
  */
2739
2821
  const parseExportCredentialsEnv = (txt) => {
2740
2822
  const lines = txt.split(/\r?\n/);
@@ -2745,12 +2827,17 @@ const parseExportCredentialsEnv = (txt) => {
2745
2827
  const line = raw.trim();
2746
2828
  if (!line)
2747
2829
  continue;
2748
- // POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
2830
+ // POSIX: export AWS_ACCESS_KEY_ID=..., ...
2749
2831
  let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2750
- if (!m) {
2751
- // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
2752
- m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2753
- }
2832
+ // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
2833
+ if (!m)
2834
+ m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
2835
+ // Windows cmd: set AWS_ACCESS_KEY_ID=..., etc.
2836
+ if (!m)
2837
+ m = /^(?:set)\s+([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
2838
+ // env-no-export: AWS_ACCESS_KEY_ID=..., etc.
2839
+ if (!m)
2840
+ m = /^([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2754
2841
  if (!m)
2755
2842
  continue;
2756
2843
  const k = m[1];
@@ -2775,7 +2862,7 @@ const parseExportCredentialsEnv = (txt) => {
2775
2862
  };
2776
2863
  return undefined;
2777
2864
  };
2778
- const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
2865
+ const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
2779
2866
  const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
2780
2867
  env: process.env,
2781
2868
  timeoutMs,
@@ -2790,36 +2877,50 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
2790
2877
  }
2791
2878
  return undefined;
2792
2879
  };
2793
- const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
2794
- // Try JSON format first (AWS CLI v2)
2795
- const rJson = await runCommandResult([
2796
- 'aws',
2797
- 'configure',
2798
- 'export-credentials',
2799
- '--profile',
2800
- profile,
2801
- '--format',
2802
- 'json',
2803
- ], false, { env: process.env, timeoutMs });
2804
- if (rJson.exitCode === 0) {
2805
- const creds = parseExportCredentialsJson(rJson.stdout);
2806
- if (creds)
2807
- return creds;
2808
- }
2809
- // Fallback: env lines
2810
- const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
2811
- if (rEnv.exitCode === 0) {
2812
- const creds = parseExportCredentialsEnv(rEnv.stdout);
2880
+ const exportCredentials = async (profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
2881
+ const tryExport = async (format) => {
2882
+ const argv = [
2883
+ 'aws',
2884
+ 'configure',
2885
+ 'export-credentials',
2886
+ '--profile',
2887
+ profile,
2888
+ ...(format ? ['--format', format] : []),
2889
+ ];
2890
+ const r = await runCommandResult(argv, false, {
2891
+ env: process.env,
2892
+ timeoutMs,
2893
+ });
2894
+ if (r.exitCode !== 0)
2895
+ return undefined;
2896
+ const out = trim(r.stdout);
2897
+ if (!out)
2898
+ return undefined;
2899
+ // Some formats produce JSON ("process"), some produce shell-ish env lines.
2900
+ return parseExportCredentialsJson(out) ?? parseExportCredentialsEnv(out);
2901
+ };
2902
+ // Prefer the default/JSON "process" format first; then fall back to shell env outputs.
2903
+ // Note: AWS CLI v2 supports: process | env | env-no-export | powershell | windows-cmd
2904
+ const formats = [
2905
+ 'process',
2906
+ ...(process.platform === 'win32'
2907
+ ? ['powershell', 'windows-cmd', 'env', 'env-no-export']
2908
+ : ['env', 'env-no-export']),
2909
+ ];
2910
+ for (const f of formats) {
2911
+ const creds = await tryExport(f);
2813
2912
  if (creds)
2814
2913
  return creds;
2815
2914
  }
2816
- return undefined;
2915
+ // Final fallback: no --format (AWS CLI default output)
2916
+ return tryExport(undefined);
2817
2917
  };
2818
2918
  /**
2819
2919
  * Resolve AWS context (profile, region, credentials) using configuration and environment.
2820
2920
  * Applies strategy (cli-export vs none) and handling for SSO login-on-demand.
2821
2921
  *
2822
2922
  * @param options - Context options including current dotenv and plugin config.
2923
+ * @returns A `Promise\<AwsContext\>` containing any resolved profile, region, and credentials.
2823
2924
  */
2824
2925
  const resolveAwsContext = async ({ dotenv, cfg, }) => {
2825
2926
  const profileKey = cfg.profileKey ?? 'AWS_LOCAL_PROFILE';
@@ -2844,31 +2945,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2844
2945
  out.region = region;
2845
2946
  return out;
2846
2947
  }
2847
- // Env-first credentials.
2848
2948
  let credentials;
2849
- const envId = trim(process.env.AWS_ACCESS_KEY_ID);
2850
- const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
2851
- const envToken = trim(process.env.AWS_SESSION_TOKEN);
2852
- if (envId && envSecret) {
2853
- credentials = {
2854
- accessKeyId: envId,
2855
- secretAccessKey: envSecret,
2856
- ...(envToken ? { sessionToken: envToken } : {}),
2857
- };
2858
- }
2859
- else if (profile) {
2949
+ // Profile wins over ambient env creds when present (from flags/config/dotenv).
2950
+ if (profile) {
2860
2951
  // Try export-credentials
2861
2952
  credentials = await exportCredentials(profile);
2862
2953
  // On failure, detect SSO and optionally login then retry
2863
2954
  if (!credentials) {
2864
2955
  const ssoSession = await getAwsConfigure('sso_session', profile);
2865
- const looksSSO = typeof ssoSession === 'string' && ssoSession.length > 0;
2956
+ // Legacy SSO profiles use sso_start_url/sso_region rather than sso_session.
2957
+ const ssoStartUrl = await getAwsConfigure('sso_start_url', profile);
2958
+ const looksSSO = (typeof ssoSession === 'string' && ssoSession.length > 0) ||
2959
+ (typeof ssoStartUrl === 'string' && ssoStartUrl.length > 0);
2866
2960
  if (looksSSO && cfg.loginOnDemand) {
2867
- // Best-effort login, then retry export once.
2868
- await runCommandResult(['aws', 'sso', 'login', '--profile', profile], false, {
2961
+ // Interactive login (no timeout by default), then retry export once.
2962
+ const exit = await runCommand(['aws', 'sso', 'login', '--profile', profile], false, {
2869
2963
  env: process.env,
2870
- timeoutMs: DEFAULT_TIMEOUT_MS,
2964
+ stdio: 'inherit',
2871
2965
  });
2966
+ if (exit !== 0) {
2967
+ throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
2968
+ }
2872
2969
  credentials = await exportCredentials(profile);
2873
2970
  }
2874
2971
  }
@@ -2886,6 +2983,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2886
2983
  }
2887
2984
  }
2888
2985
  }
2986
+ else {
2987
+ // Env-first credentials when no profile is present.
2988
+ const envId = trim(process.env.AWS_ACCESS_KEY_ID);
2989
+ const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
2990
+ const envToken = trim(process.env.AWS_SESSION_TOKEN);
2991
+ if (envId && envSecret) {
2992
+ credentials = {
2993
+ accessKeyId: envId,
2994
+ secretAccessKey: envSecret,
2995
+ ...(envToken ? { sessionToken: envToken } : {}),
2996
+ };
2997
+ }
2998
+ }
2889
2999
  // Final region resolution
2890
3000
  if (!region && profile)
2891
3001
  region = await getAwsConfigure('region', profile);
@@ -2901,10 +3011,213 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2901
3011
  return out;
2902
3012
  };
2903
3013
 
3014
+ /**
3015
+ * Create the AWS plugin `afterResolve` hook.
3016
+ *
3017
+ * This runs once per invocation after the host resolves dotenv context.
3018
+ *
3019
+ * @param plugin - The AWS plugin instance.
3020
+ * @returns An `afterResolve` hook function suitable for assigning to `plugin.afterResolve`.
3021
+ *
3022
+ * @internal
3023
+ */
3024
+ function attachAwsAfterResolveHook(plugin) {
3025
+ return async (cli, ctx) => {
3026
+ const cfg = plugin.readConfig(cli);
3027
+ const out = await resolveAwsContext({
3028
+ dotenv: ctx.dotenv,
3029
+ cfg,
3030
+ });
3031
+ applyAwsContext(out, ctx, true);
3032
+ // Optional: low-noise breadcrumb for diagnostics
3033
+ if (process.env.GETDOTENV_DEBUG) {
3034
+ try {
3035
+ const msg = JSON.stringify({
3036
+ profile: out.profile,
3037
+ region: out.region,
3038
+ hasCreds: Boolean(out.credentials),
3039
+ });
3040
+ process.stderr.write(`[aws] afterResolve ${msg}\n`);
3041
+ }
3042
+ catch {
3043
+ /* ignore */
3044
+ }
3045
+ }
3046
+ };
3047
+ }
3048
+
3049
+ /** @internal */
3050
+ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
3051
+ /**
3052
+ * Create an AWS plugin config overlay from Commander-parsed option values.
3053
+ *
3054
+ * This preserves tri-state intent:
3055
+ * - If a flag was not provided, it should not overwrite config-derived defaults.
3056
+ * - If `--no-…` was provided, it must explicitly force the boolean false.
3057
+ *
3058
+ * @param opts - Commander option values for the current invocation.
3059
+ * @returns A partial AWS plugin config object containing only explicit overrides.
3060
+ *
3061
+ * @internal
3062
+ */
3063
+ function awsConfigOverridesFromCommandOpts(opts) {
3064
+ const o = isRecord(opts) ? opts : {};
3065
+ const overlay = {};
3066
+ // Map boolean toggles (respect explicit --no-*)
3067
+ if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand')) {
3068
+ overlay.loginOnDemand = Boolean(o.loginOnDemand);
3069
+ }
3070
+ // Strings/enums
3071
+ if (typeof o.profile === 'string')
3072
+ overlay.profile = o.profile;
3073
+ if (typeof o.region === 'string')
3074
+ overlay.region = o.region;
3075
+ if (typeof o.defaultRegion === 'string')
3076
+ overlay.defaultRegion = o.defaultRegion;
3077
+ if (o.strategy === 'cli-export' || o.strategy === 'none') {
3078
+ overlay.strategy = o.strategy;
3079
+ }
3080
+ // Advanced key overrides
3081
+ if (typeof o.profileKey === 'string')
3082
+ overlay.profileKey = o.profileKey;
3083
+ if (typeof o.profileFallbackKey === 'string') {
3084
+ overlay.profileFallbackKey = o.profileFallbackKey;
3085
+ }
3086
+ if (typeof o.regionKey === 'string')
3087
+ overlay.regionKey = o.regionKey;
3088
+ return overlay;
3089
+ }
3090
+
3091
+ /**
3092
+ * Attach the default action for the AWS plugin mount.
3093
+ *
3094
+ * Behavior:
3095
+ * - With args: forwards to AWS CLI (`aws <args...>`) under the established session.
3096
+ * - Without args: session-only establishment (no forward).
3097
+ *
3098
+ * @param cli - The `aws` command mount.
3099
+ * @param plugin - The AWS plugin instance.
3100
+ *
3101
+ * @internal
3102
+ */
3103
+ function attachAwsDefaultAction(cli, plugin, awsCmd) {
3104
+ awsCmd.action(async (args, opts, thisCommand) => {
3105
+ // Access merged root CLI options (installed by root hooks).
3106
+ const bag = readMergedOptions(thisCommand);
3107
+ const capture = shouldCapture(bag.capture);
3108
+ const underTests = process.env.GETDOTENV_TEST === '1' ||
3109
+ typeof process.env.VITEST_WORKER_ID === 'string';
3110
+ // Build overlay cfg from subcommand flags layered over discovered config.
3111
+ const ctx = cli.getCtx();
3112
+ const cfgBase = plugin.readConfig(cli);
3113
+ const cfg = {
3114
+ ...cfgBase,
3115
+ ...awsConfigOverridesFromCommandOpts(opts),
3116
+ };
3117
+ // Resolve current context with overrides
3118
+ const out = await resolveAwsContext({
3119
+ dotenv: ctx.dotenv,
3120
+ cfg,
3121
+ });
3122
+ // Publish env/context
3123
+ applyAwsContext(out, ctx, true);
3124
+ // Forward when positional args are present; otherwise session-only.
3125
+ if (args.length > 0) {
3126
+ const argv = ['aws', ...args];
3127
+ const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
3128
+ const exit = await runCommand(argv, shellSetting, {
3129
+ env: buildSpawnEnv(process.env, ctx.dotenv),
3130
+ stdio: capture ? 'pipe' : 'inherit',
3131
+ });
3132
+ // Deterministic termination (suppressed under tests)
3133
+ if (!underTests) {
3134
+ process.exit(typeof exit === 'number' ? exit : 0);
3135
+ }
3136
+ return;
3137
+ }
3138
+ // Session only: low-noise breadcrumb under debug
3139
+ if (process.env.GETDOTENV_DEBUG) {
3140
+ try {
3141
+ const msg = JSON.stringify({
3142
+ profile: out.profile,
3143
+ region: out.region,
3144
+ hasCreds: Boolean(out.credentials),
3145
+ });
3146
+ process.stderr.write(`[aws] session established ${msg}\n`);
3147
+ }
3148
+ catch {
3149
+ /* ignore */
3150
+ }
3151
+ }
3152
+ if (!underTests)
3153
+ process.exit(0);
3154
+ });
3155
+ }
3156
+
3157
+ /**
3158
+ * Attach options/arguments for the AWS plugin mount.
3159
+ *
3160
+ * @param cli - The `aws` command mount.
3161
+ * @param plugin - The AWS plugin instance (for dynamic option descriptions).
3162
+ *
3163
+ * @internal
3164
+ */
3165
+ function attachAwsOptions(cli, plugin) {
3166
+ return (cli
3167
+ // Description is owned by the plugin index (src/plugins/aws/index.ts).
3168
+ .enablePositionalOptions()
3169
+ .passThroughOptions()
3170
+ .allowUnknownOption(true)
3171
+ // Boolean toggles with dynamic help labels (effective defaults)
3172
+ .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
3173
+ .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
3174
+ // Strings / enums
3175
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
3176
+ .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
3177
+ .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
3178
+ .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
3179
+ // Advanced key overrides
3180
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
3181
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
3182
+ .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
3183
+ // Accept any extra operands so Commander does not error when tokens appear after "--".
3184
+ .argument('[args...]'));
3185
+ }
3186
+
3187
+ /**
3188
+ * Attach the AWS plugin `preSubcommand` hook.
3189
+ *
3190
+ * Ensures `aws --profile/--region <child>` applies the AWS session setup before
3191
+ * child subcommand execution.
3192
+ *
3193
+ * @param cli - The `aws` command mount.
3194
+ * @param plugin - The AWS plugin instance.
3195
+ *
3196
+ * @internal
3197
+ */
3198
+ function attachAwsPreSubcommandHook(cli, plugin) {
3199
+ cli.hook('preSubcommand', async (thisCommand) => {
3200
+ // Avoid side effects for help rendering.
3201
+ if (process.argv.includes('-h') || process.argv.includes('--help'))
3202
+ return;
3203
+ const ctx = cli.getCtx();
3204
+ const cfgBase = plugin.readConfig(cli);
3205
+ const cfg = {
3206
+ ...cfgBase,
3207
+ ...awsConfigOverridesFromCommandOpts(thisCommand.opts()),
3208
+ };
3209
+ const out = await resolveAwsContext({
3210
+ dotenv: ctx.dotenv,
3211
+ cfg,
3212
+ });
3213
+ applyAwsContext(out, ctx, true);
3214
+ });
3215
+ }
3216
+
2904
3217
  /**
2905
3218
  * Zod schema for AWS plugin configuration.
2906
3219
  */
2907
- const AwsPluginConfigSchema = z$2.object({
3220
+ const awsPluginConfigSchema = z$2.object({
2908
3221
  profile: z$2.string().optional(),
2909
3222
  region: z$2.string().optional(),
2910
3223
  defaultRegion: z$2.string().optional(),
@@ -2928,129 +3241,16 @@ const AwsPluginConfigSchema = z$2.object({
2928
3241
  const awsPlugin = () => {
2929
3242
  const plugin = definePlugin({
2930
3243
  ns: 'aws',
2931
- configSchema: AwsPluginConfigSchema,
2932
- setup: (cli) => {
2933
- // Mount: aws (provided)
2934
- cli
2935
- .description('Establish an AWS session and optionally forward to the AWS CLI')
2936
- .enablePositionalOptions()
2937
- .passThroughOptions()
2938
- .allowUnknownOption(true)
2939
- // Boolean toggles with dynamic help labels (effective defaults)
2940
- .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
2941
- .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
2942
- // Strings / enums
2943
- .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
2944
- .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
2945
- .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
2946
- .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
2947
- // Advanced key overrides
2948
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
2949
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
2950
- .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
2951
- // Accept any extra operands so Commander does not error when tokens appear after "--".
2952
- .argument('[args...]')
2953
- .action(async (args, opts, thisCommand) => {
2954
- const pluginInst = plugin;
2955
- // Access merged root CLI options (installed by passOptions())
2956
- const bag = readMergedOptions(thisCommand);
2957
- const capture = shouldCapture(bag.capture);
2958
- const underTests = process.env.GETDOTENV_TEST === '1' ||
2959
- typeof process.env.VITEST_WORKER_ID === 'string';
2960
- // Build overlay cfg from subcommand flags layered over discovered config.
2961
- const ctx = cli.getCtx();
2962
- const cfgBase = pluginInst.readConfig(cli);
2963
- const o = opts;
2964
- const overlay = {};
2965
- // Map boolean toggles (respect explicit --no-*)
2966
- if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
2967
- overlay.loginOnDemand = Boolean(o.loginOnDemand);
2968
- // Strings/enums
2969
- if (typeof o.profile === 'string')
2970
- overlay.profile = o.profile;
2971
- if (typeof o.region === 'string')
2972
- overlay.region = o.region;
2973
- if (typeof o.defaultRegion === 'string')
2974
- overlay.defaultRegion = o.defaultRegion;
2975
- if (typeof o.strategy === 'string')
2976
- overlay.strategy = o.strategy;
2977
- // Advanced key overrides
2978
- if (typeof o.profileKey === 'string')
2979
- overlay.profileKey = o.profileKey;
2980
- if (typeof o.profileFallbackKey === 'string')
2981
- overlay.profileFallbackKey = o.profileFallbackKey;
2982
- if (typeof o.regionKey === 'string')
2983
- overlay.regionKey = o.regionKey;
2984
- const cfg = {
2985
- ...cfgBase,
2986
- ...overlay,
2987
- };
2988
- // Resolve current context with overrides
2989
- const out = await resolveAwsContext({
2990
- dotenv: ctx.dotenv,
2991
- cfg,
2992
- });
2993
- // Publish env/context
2994
- applyAwsContext(out, ctx, true);
2995
- // Forward when positional args are present; otherwise session-only.
2996
- if (Array.isArray(args) && args.length > 0) {
2997
- const argv = ['aws', ...args];
2998
- const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
2999
- const exit = await runCommand(argv, shellSetting, {
3000
- env: buildSpawnEnv(process.env, ctx.dotenv),
3001
- stdio: capture ? 'pipe' : 'inherit',
3002
- });
3003
- // Deterministic termination (suppressed under tests)
3004
- if (!underTests) {
3005
- process.exit(typeof exit === 'number' ? exit : 0);
3006
- }
3007
- return;
3008
- }
3009
- else {
3010
- // Session only: low-noise breadcrumb under debug
3011
- if (process.env.GETDOTENV_DEBUG) {
3012
- try {
3013
- const msg = JSON.stringify({
3014
- profile: out.profile,
3015
- region: out.region,
3016
- hasCreds: Boolean(out.credentials),
3017
- });
3018
- process.stderr.write(`[aws] session established ${msg}\n`);
3019
- }
3020
- catch {
3021
- /* ignore */
3022
- }
3023
- }
3024
- if (!underTests)
3025
- process.exit(0);
3026
- return;
3027
- }
3028
- });
3244
+ configSchema: awsPluginConfigSchema,
3245
+ setup(cli) {
3246
+ cli.description('Establish an AWS session and optionally forward to the AWS CLI');
3247
+ const awsCmd = attachAwsOptions(cli, plugin);
3248
+ attachAwsPreSubcommandHook(cli, plugin);
3249
+ attachAwsDefaultAction(cli, plugin, awsCmd);
3029
3250
  return undefined;
3030
3251
  },
3031
- afterResolve: async (_cli, ctx) => {
3032
- const cfg = plugin.readConfig(_cli);
3033
- const out = await resolveAwsContext({
3034
- dotenv: ctx.dotenv,
3035
- cfg,
3036
- });
3037
- applyAwsContext(out, ctx, true);
3038
- // Optional: low-noise breadcrumb for diagnostics
3039
- if (process.env.GETDOTENV_DEBUG) {
3040
- try {
3041
- const msg = JSON.stringify({
3042
- profile: out.profile,
3043
- region: out.region,
3044
- hasCreds: Boolean(out.credentials),
3045
- });
3046
- process.stderr.write(`[aws] afterResolve ${msg}\n`);
3047
- }
3048
- catch {
3049
- /* ignore */
3050
- }
3051
- }
3052
- },
3053
3252
  });
3253
+ plugin.afterResolve = attachAwsAfterResolveHook(plugin);
3054
3254
  return plugin;
3055
3255
  };
3056
3256
 
@@ -13512,21 +13712,78 @@ class GetCallerIdentityCommand extends Command
13512
13712
  }
13513
13713
 
13514
13714
  /**
13515
- * AWS Whoami plugin: prints the current AWS caller identity (account, arn, userid).
13516
- * Intended to be mounted under the `aws` plugin.
13715
+ * Attach the default action for the `aws whoami` command.
13716
+ *
13717
+ * This behavior executes only when `aws whoami` is invoked without a subcommand.
13718
+ *
13719
+ * @param cli - The `whoami` command mount.
13720
+ * @returns Nothing.
13721
+ */
13722
+ function attachWhoamiDefaultAction(cli) {
13723
+ cli.action(async () => {
13724
+ // The AWS SDK default providers will read credentials from process.env,
13725
+ // which the aws parent has already populated.
13726
+ const client = new STSClient$1();
13727
+ const result = await client.send(new GetCallerIdentityCommand());
13728
+ console.log(JSON.stringify(result, null, 2));
13729
+ });
13730
+ }
13731
+
13732
+ /**
13733
+ * Attach options/arguments for the `aws whoami` plugin mount.
13734
+ *
13735
+ * This subcommand currently takes no flags/args; this module exists to keep the
13736
+ * wiring layout consistent across shipped plugins (options vs actions).
13737
+ *
13738
+ * Note: the plugin description is owned by `src/plugins/aws/whoami/index.ts` and
13739
+ * must not be set here.
13740
+ *
13741
+ * @param cli - The `whoami` command mount under `aws`.
13742
+ * @returns The same `cli` instance for chaining.
13743
+ *
13744
+ * @internal
13745
+ */
13746
+ function attachWhoamiOptions(cli) {
13747
+ return cli;
13748
+ }
13749
+
13750
+ /**
13751
+ * Attach the `really` subcommand under `aws whoami`.
13752
+ *
13753
+ * Reads `SECRET_IDENTITY` from the resolved get-dotenv context (`cli.getCtx().dotenv`).
13754
+ *
13755
+ * @param cli - The `whoami` command mount.
13756
+ * @returns Nothing.
13757
+ */
13758
+ function attachWhoamiReallyAction(cli) {
13759
+ const really = cli
13760
+ .ns('really')
13761
+ .description('Print SECRET_IDENTITY from the resolved dotenv context');
13762
+ really.action(() => {
13763
+ const secretIdentity = really.getCtx().dotenv.SECRET_IDENTITY;
13764
+ console.log(`Your secret identity is ${secretIdentity ?? 'still a secret'}.`);
13765
+ });
13766
+ }
13767
+
13768
+ /**
13769
+ * AWS Whoami plugin factory.
13770
+ *
13771
+ * This plugin demonstrates a “bucket of subcommands” pattern:
13772
+ * - Subcommand behavior is articulated in separate modules as `attach*` helpers.
13773
+ * - Those helpers are not individually composable plugins; they are internal wiring for one plugin instance.
13774
+ *
13775
+ * @returns A plugin instance mounted at `aws whoami`.
13517
13776
  */
13518
13777
  const awsWhoamiPlugin = () => definePlugin({
13519
13778
  ns: 'whoami',
13520
13779
  setup(cli) {
13521
- cli
13522
- .description('Print AWS caller identity (uses parent aws session)')
13523
- .action(async () => {
13524
- // The AWS SDK default providers will read credentials from process.env,
13525
- // which the aws parent has already populated.
13526
- const client = new STSClient$1();
13527
- const result = await client.send(new GetCallerIdentityCommand());
13528
- console.log(JSON.stringify(result, null, 2));
13529
- });
13780
+ cli.description('Print AWS caller identity (uses parent aws session)');
13781
+ // Options/args (none today, but keep layout consistent with other plugins).
13782
+ const whoami = attachWhoamiOptions(cli);
13783
+ // Default behavior: `getdotenv aws whoami`
13784
+ attachWhoamiDefaultAction(whoami);
13785
+ // Subcommand behavior: `getdotenv aws whoami really`
13786
+ attachWhoamiReallyAction(whoami);
13530
13787
  return undefined;
13531
13788
  },
13532
13789
  });
@@ -13634,7 +13891,7 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv,
13634
13891
  /**
13635
13892
  * Attach the default "cmd" subcommand action with contextual typing.
13636
13893
  */
13637
- const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
13894
+ const attachBatchCmdAction = (plugin, cli, batchCmd, pluginOpts, cmd) => {
13638
13895
  cmd.action(async (commandParts, _subOpts, thisCommand) => {
13639
13896
  const mergedBag = readMergedOptions(batchCmd);
13640
13897
  const logger = mergedBag.logger;
@@ -13743,11 +14000,37 @@ const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
13743
14000
  });
13744
14001
  };
13745
14002
 
14003
+ /**
14004
+ * Attach the default `cmd` subcommand under the `batch` command.
14005
+ *
14006
+ * This encapsulates:
14007
+ * - Subcommand construction (`new Command().name('cmd')…`)
14008
+ * - Action wiring
14009
+ * - Mounting as the default subcommand for `batch`
14010
+ *
14011
+ * @param plugin - The batch plugin instance.
14012
+ * @param cli - The batch command mount.
14013
+ * @param batchCmd - The `batch` command (same as `cli` mount).
14014
+ * @param pluginOpts - Batch plugin factory options.
14015
+ *
14016
+ * @internal
14017
+ */
14018
+ const attachBatchCmdSubcommand = (plugin, cli, batchCmd, pluginOpts) => {
14019
+ const cmdSub = new Command$1()
14020
+ .name('cmd')
14021
+ .description('execute command, conflicts with --command option (default subcommand)')
14022
+ .enablePositionalOptions()
14023
+ .passThroughOptions()
14024
+ .argument('[command...]');
14025
+ attachBatchCmdAction(plugin, cli, batchCmd, pluginOpts, cmdSub);
14026
+ batchCmd.addCommand(cmdSub, { isDefault: true });
14027
+ };
14028
+
13746
14029
  /**
13747
14030
  * Attach the parent-level action for the batch plugin.
13748
14031
  * Handles parent flags (e.g. `getdotenv batch -l`) and delegates to the batch executor.
13749
14032
  */
13750
- const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
14033
+ const attachBatchDefaultAction = (plugin, cli, pluginOpts, parent) => {
13751
14034
  parent.action(async function (...args) {
13752
14035
  // Commander Unknown generics: [...unknown[], OptionValues, thisCommand]
13753
14036
  const thisCommand = args[args.length - 1];
@@ -13848,6 +14131,45 @@ const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
13848
14131
  });
13849
14132
  };
13850
14133
 
14134
+ /**
14135
+ * Attach options/arguments for the batch plugin mount.
14136
+ *
14137
+ * Note: the plugin description is owned by `src/plugins/batch/index.ts` and
14138
+ * must not be set here.
14139
+ *
14140
+ * @param plugin - Batch plugin instance (for dynamic option descriptions).
14141
+ * @param cli - The `batch` command mount.
14142
+ * @returns The same `cli` instance for chaining.
14143
+ *
14144
+ * @internal
14145
+ */
14146
+ function attachBatchOptions(plugin, cli) {
14147
+ const GROUP = `plugin:${cli.name()}`;
14148
+ return (cli
14149
+ .enablePositionalOptions()
14150
+ .passThroughOptions()
14151
+ // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
14152
+ .addOption((() => {
14153
+ const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
14154
+ cli.setOptionGroup(opt, GROUP);
14155
+ return opt;
14156
+ })())
14157
+ .addOption((() => {
14158
+ const opt = plugin.createPluginDynamicOption(cli, '-r, --root-path <string>', (_bag, cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.rootPath || './')})`);
14159
+ cli.setOptionGroup(opt, GROUP);
14160
+ return opt;
14161
+ })())
14162
+ .addOption((() => {
14163
+ const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
14164
+ cli.setOptionGroup(opt, GROUP);
14165
+ return opt;
14166
+ })())
14167
+ .option('-c, --command <string>', 'command executed according to the base shell resolution')
14168
+ .option('-l, --list', 'list working directories without executing command')
14169
+ .option('-e, --ignore-errors', 'ignore errors and continue with next path')
14170
+ .argument('[command...]'));
14171
+ }
14172
+
13851
14173
  /**
13852
14174
  * Zod schema for a single script entry (string or object).
13853
14175
  */
@@ -13861,7 +14183,7 @@ const ScriptSchema = z$2.union([
13861
14183
  /**
13862
14184
  * Zod schema for batch plugin configuration.
13863
14185
  */
13864
- const BatchConfigSchema = z$2.object({
14186
+ const batchPluginConfigSchema = z$2.object({
13865
14187
  scripts: z$2.record(z$2.string(), ScriptSchema).optional(),
13866
14188
  shell: z$2.union([z$2.string(), z$2.boolean()]).optional(),
13867
14189
  rootPath: z$2.string().optional(),
@@ -13887,45 +14209,15 @@ const batchPlugin = (opts = {}) => {
13887
14209
  ns: 'batch',
13888
14210
  // Host validates this when config-loader is enabled; plugins may also
13889
14211
  // re-validate at action time as a safety belt.
13890
- configSchema: BatchConfigSchema,
14212
+ configSchema: batchPluginConfigSchema,
13891
14213
  setup(cli) {
13892
14214
  const batchCmd = cli; // mount provided by host
13893
- const GROUP = `plugin:${cli.name()}`;
13894
- batchCmd
13895
- .description('Batch command execution across multiple working directories.')
13896
- .enablePositionalOptions()
13897
- .passThroughOptions()
13898
- // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
13899
- .addOption((() => {
13900
- const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
13901
- cli.setOptionGroup(opt, GROUP);
13902
- return opt;
13903
- })())
13904
- .addOption((() => {
13905
- const opt = plugin.createPluginDynamicOption(cli, '-r, --root-path <string>', (_bag, cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.rootPath || './')})`);
13906
- cli.setOptionGroup(opt, GROUP);
13907
- return opt;
13908
- })())
13909
- .addOption((() => {
13910
- const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
13911
- cli.setOptionGroup(opt, GROUP);
13912
- return opt;
13913
- })())
13914
- .option('-c, --command <string>', 'command executed according to the base shell resolution')
13915
- .option('-l, --list', 'list working directories without executing command')
13916
- .option('-e, --ignore-errors', 'ignore errors and continue with next path')
13917
- .argument('[command...]');
13918
- // Default subcommand "cmd" with contextual typing for args/opts
13919
- const cmdSub = new Command$1()
13920
- .name('cmd')
13921
- .description('execute command, conflicts with --command option (default subcommand)')
13922
- .enablePositionalOptions()
13923
- .passThroughOptions()
13924
- .argument('[command...]');
13925
- attachDefaultCmdAction$1(plugin, cli, batchCmd, opts, cmdSub);
13926
- batchCmd.addCommand(cmdSub, { isDefault: true });
13927
- // Parent invoker (unified naming)
13928
- attachParentInvoker$1(plugin, cli, opts, batchCmd);
14215
+ batchCmd.description('Batch command execution across multiple working directories.');
14216
+ attachBatchOptions(plugin, batchCmd);
14217
+ // Default subcommand `cmd` (mounted as batch default subcommand)
14218
+ attachBatchCmdSubcommand(plugin, cli, batchCmd, opts);
14219
+ // Default action for the batch command mount (parent flags and positional form)
14220
+ attachBatchDefaultAction(plugin, cli, opts, batchCmd);
13929
14221
  return undefined;
13930
14222
  },
13931
14223
  });
@@ -14019,14 +14311,11 @@ async function runCmdWithContext(cli, merged, command, _opts) {
14019
14311
  }
14020
14312
 
14021
14313
  /**
14022
- * Attach the default "cmd" subcommand action (unified name).
14314
+ * Attach the default "cmd" subcommand action.
14023
14315
  * Mirrors the prior inline implementation in cmd/index.ts.
14024
14316
  */
14025
- const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
14026
- cmd
14027
- .enablePositionalOptions()
14028
- .passThroughOptions()
14029
- .action(async function (...allArgs) {
14317
+ const attachCmdDefaultAction = (cli, cmd, aliasKey) => {
14318
+ cmd.action(async function (...allArgs) {
14030
14319
  // Commander passes: [...positionals, options, thisCommand]
14031
14320
  const thisCommand = allArgs[allArgs.length - 1];
14032
14321
  const commandParts = allArgs[0];
@@ -14059,11 +14348,29 @@ const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
14059
14348
  });
14060
14349
  };
14061
14350
 
14351
+ /**
14352
+ * Attach options/arguments for the cmd plugin mount.
14353
+ *
14354
+ * Note: the plugin description is owned by `src/plugins/cmd/index.ts` and must
14355
+ * not be set here.
14356
+ *
14357
+ * @param cli - The `cmd` command mount.
14358
+ * @returns The same `cli` instance for chaining.
14359
+ *
14360
+ * @internal
14361
+ */
14362
+ function attachCmdOptions(cli) {
14363
+ return cli
14364
+ .enablePositionalOptions()
14365
+ .passThroughOptions()
14366
+ .argument('[command...]');
14367
+ }
14368
+
14062
14369
  /**
14063
14370
  * Install the parent-level invoker (alias) for the cmd plugin.
14064
14371
  * Unifies naming with batch attachParentInvoker; behavior unchanged.
14065
14372
  */
14066
- const attachParentInvoker = (cli, options, _cmd, plugin) => {
14373
+ const attachCmdParentInvoker = (cli, options, plugin) => {
14067
14374
  const dbg = (...args) => {
14068
14375
  if (process.env.GETDOTENV_DEBUG) {
14069
14376
  try {
@@ -14174,7 +14481,7 @@ const attachParentInvoker = (cli, options, _cmd, plugin) => {
14174
14481
  /**
14175
14482
  * Zod schema for cmd plugin configuration.
14176
14483
  */
14177
- const CmdConfigSchema = z$2
14484
+ const cmdPluginConfigSchema = z$2
14178
14485
  .object({
14179
14486
  expand: z$2.boolean().optional(),
14180
14487
  })
@@ -14194,7 +14501,7 @@ const CmdConfigSchema = z$2
14194
14501
  const cmdPlugin = (options = {}) => {
14195
14502
  const plugin = definePlugin({
14196
14503
  ns: 'cmd',
14197
- configSchema: CmdConfigSchema,
14504
+ configSchema: cmdPluginConfigSchema,
14198
14505
  setup(cli) {
14199
14506
  const aliasSpec = typeof options.optionAlias === 'string'
14200
14507
  ? { flags: options.optionAlias}
@@ -14206,14 +14513,13 @@ const cmdPlugin = (options = {}) => {
14206
14513
  };
14207
14514
  const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
14208
14515
  // Mount is the command ('cmd'); attach default action.
14209
- cli
14210
- .description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
14211
- // Accept payload tokens as positional arguments for the default subcommand.
14212
- .argument('[command...]');
14213
- attachDefaultCmdAction(cli, cli, aliasKey);
14516
+ cli.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)');
14517
+ // Options/arguments (positional payload, argv routing) are attached separately.
14518
+ attachCmdOptions(cli);
14519
+ attachCmdDefaultAction(cli, cli, aliasKey);
14214
14520
  // Parent-attached option alias (optional, unified naming).
14215
14521
  if (aliasSpec !== undefined) {
14216
- attachParentInvoker(cli, options, cli, plugin);
14522
+ attachCmdParentInvoker(cli, options, plugin);
14217
14523
  }
14218
14524
  return undefined;
14219
14525
  },
@@ -14222,14 +14528,21 @@ const cmdPlugin = (options = {}) => {
14222
14528
  };
14223
14529
 
14224
14530
  /**
14225
- * Ensure a directory exists.
14531
+ * Ensure a directory exists (parents included).
14532
+ *
14533
+ * @param p - Directory path to create.
14534
+ * @returns A `Promise\<string\>` resolving to the provided `p` value.
14226
14535
  */
14227
14536
  const ensureDir = async (p) => {
14228
14537
  await fs.ensureDir(p);
14229
14538
  return p;
14230
14539
  };
14231
14540
  /**
14232
- * Write text content to a file, ensuring the parent directory exists.
14541
+ * Write UTF-8 text content to a file, ensuring the parent directory exists.
14542
+ *
14543
+ * @param dest - Destination file path.
14544
+ * @param data - File contents to write.
14545
+ * @returns A `Promise\<void\>` which resolves when the file is written.
14233
14546
  */
14234
14547
  const writeFile$1 = async (dest, data) => {
14235
14548
  await ensureDir(path.dirname(dest));
@@ -14241,6 +14554,7 @@ const writeFile$1 = async (dest, data) => {
14241
14554
  * @param src - Source file path.
14242
14555
  * @param dest - Destination file path.
14243
14556
  * @param substitutions - Map of token literals to replacement strings.
14557
+ * @returns A `Promise\<void\>` which resolves when the file has been copied.
14244
14558
  */
14245
14559
  const copyTextFile = async (src, dest, substitutions) => {
14246
14560
  const contents = await fs.readFile(src, 'utf-8');
@@ -14252,6 +14566,10 @@ const copyTextFile = async (src, dest, substitutions) => {
14252
14566
  /**
14253
14567
  * Ensure a set of lines exist (exact match) in a file. Creates the file
14254
14568
  * when missing. Returns whether it was created or changed.
14569
+ *
14570
+ * @param filePath - Target file path to create/update.
14571
+ * @param lines - Lines which must be present (exact string match).
14572
+ * @returns A `Promise\<object\>` describing whether the file was created and/or changed.
14255
14573
  */
14256
14574
  const ensureLines = async (filePath, lines) => {
14257
14575
  const exists = await fs.pathExists(filePath);
@@ -14279,11 +14597,23 @@ const ensureLines = async (filePath, lines) => {
14279
14597
  return { created: false, changed: false };
14280
14598
  };
14281
14599
 
14282
- // Templates root used by the scaffolder
14600
+ /**
14601
+ * Absolute path to the shipped templates directory.
14602
+ *
14603
+ * Used by the init scaffolder to locate files under `templates/` at runtime.
14604
+ *
14605
+ * @remarks
14606
+ * This path is resolved relative to the current working directory. It assumes
14607
+ * the `templates/` folder is present alongside the installed package (or in the
14608
+ * repository when running from source).
14609
+ */
14283
14610
  const TEMPLATES_ROOT = path.resolve('templates');
14284
14611
 
14285
14612
  /**
14286
14613
  * Plan the copy operations for configuration files.
14614
+ *
14615
+ * @param options - Planning options for config scaffolding.
14616
+ * @returns An array of copy operations to perform.
14287
14617
  */
14288
14618
  const planConfigCopies = ({ format, withLocal, destRoot, }) => {
14289
14619
  const copies = [];
@@ -14327,10 +14657,14 @@ const planConfigCopies = ({ format, withLocal, destRoot, }) => {
14327
14657
  };
14328
14658
  /**
14329
14659
  * Plan the copy operations for the CLI skeleton.
14660
+ *
14661
+ * @param options - Planning options for CLI scaffolding.
14662
+ * @returns An array of copy operations to perform.
14330
14663
  */
14331
14664
  const planCliCopies = ({ cliName, destRoot, }) => {
14332
14665
  const subs = { __CLI_NAME__: cliName };
14333
14666
  const base = path.join(destRoot, 'src', 'cli', cliName);
14667
+ const helloBase = path.join(base, 'plugins', 'hello');
14334
14668
  return [
14335
14669
  {
14336
14670
  src: path.join(TEMPLATES_ROOT, 'cli', 'index.ts'),
@@ -14338,8 +14672,28 @@ const planCliCopies = ({ cliName, destRoot, }) => {
14338
14672
  subs,
14339
14673
  },
14340
14674
  {
14341
- src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello.ts'),
14342
- dest: path.join(base, 'plugins', 'hello.ts'),
14675
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'index.ts'),
14676
+ dest: path.join(helloBase, 'index.ts'),
14677
+ subs,
14678
+ },
14679
+ {
14680
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'options.ts'),
14681
+ dest: path.join(helloBase, 'options.ts'),
14682
+ subs,
14683
+ },
14684
+ {
14685
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'defaultAction.ts'),
14686
+ dest: path.join(helloBase, 'defaultAction.ts'),
14687
+ subs,
14688
+ },
14689
+ {
14690
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'strangerAction.ts'),
14691
+ dest: path.join(helloBase, 'strangerAction.ts'),
14692
+ subs,
14693
+ },
14694
+ {
14695
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'types.ts'),
14696
+ dest: path.join(helloBase, 'types.ts'),
14343
14697
  subs,
14344
14698
  },
14345
14699
  ];
@@ -14348,6 +14702,8 @@ const planCliCopies = ({ cliName, destRoot, }) => {
14348
14702
  /**
14349
14703
  * Determine whether the current environment should be treated as non-interactive.
14350
14704
  * CI heuristics include: CI, GITHUB_ACTIONS, BUILDKITE, TEAMCITY_VERSION, TF_BUILD.
14705
+ *
14706
+ * @returns `true` when running in a CI-like environment or when stdin/stdout are not TTYs.
14351
14707
  */
14352
14708
  const isNonInteractive = () => {
14353
14709
  const ciLike = process.env.CI ||
@@ -14360,6 +14716,11 @@ const isNonInteractive = () => {
14360
14716
  /**
14361
14717
  * Prompt the user for a file collision decision.
14362
14718
  * Returns a single-character code representing overwrite/example/skip (or 'all' variants).
14719
+ *
14720
+ * @param filePath - Path of the colliding file (for display).
14721
+ * @param logger - Logger used for user-facing messages.
14722
+ * @param rl - Readline interface used to capture user input.
14723
+ * @returns A single-character decision code.
14363
14724
  */
14364
14725
  const promptDecision = async (filePath, logger, rl) => {
14365
14726
  logger.log(`File exists: ${filePath}\nChoose: [o]verwrite, [e]xample, [s]kip, [O]verwrite All, [E]xample All, [S]kip All`);
@@ -14374,129 +14735,148 @@ const promptDecision = async (filePath, logger, rl) => {
14374
14735
  };
14375
14736
 
14376
14737
  /**
14377
- * @packageDocumentation
14378
- * Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
14379
- * host‑based CLI skeleton with collision handling and CI‑safe defaults.
14380
- */
14381
- /**
14382
- * Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
14383
- * Supports collision detection, interactive prompts, and CI bypass.
14738
+ * Attach the init plugin default action.
14739
+ *
14740
+ * @param cli - The `init` command mount (with args/options attached).
14741
+ *
14742
+ * @internal
14384
14743
  */
14385
- const initPlugin = () => definePlugin({
14386
- ns: 'init',
14387
- setup(cli) {
14388
- cli
14389
- .description('Scaffold getdotenv config files and a host-based CLI skeleton.')
14390
- .argument('[dest]', 'destination path (default: ./)', '.')
14391
- .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
14392
- .option('--with-local', 'include .local config variant')
14393
- .option('--dynamic', 'include dynamic examples (JS/TS configs)')
14394
- .option('--cli-name <string>', 'CLI name for skeleton and tokens')
14395
- .option('--force', 'overwrite all existing files')
14396
- .option('--yes', 'skip all collisions (no overwrite)')
14397
- .action(async (destArg, opts, thisCommand) => {
14398
- // Inherit logger from merged root options (base).
14399
- const bag = readMergedOptions(thisCommand);
14400
- const logger = bag.logger;
14401
- const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
14402
- const cwd = process.cwd();
14403
- const destRoot = path.resolve(cwd, destRel);
14404
- const formatInput = opts.configFormat;
14405
- const formatRaw = typeof formatInput === 'string'
14406
- ? formatInput.toLowerCase()
14407
- : 'json';
14408
- const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw)
14409
- ? formatRaw
14410
- : 'json');
14411
- const withLocal = !!opts.withLocal;
14412
- // dynamic flag reserved for future template variants; present for UX compatibility
14413
- void opts.dynamic;
14414
- // CLI name default: --cli-name | basename(dest) | 'mycli'
14415
- const cliName = (typeof opts.cliName === 'string' && opts.cliName.length > 0
14416
- ? opts.cliName
14417
- : path.basename(destRoot) || 'mycli') || 'mycli';
14418
- // Precedence: --force > --yes > auto-detect(non-interactive => yes)
14419
- const force = !!opts.force;
14420
- const yes = !!opts.yes || (!force && isNonInteractive());
14421
- // Build copy plan
14422
- const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
14423
- const cliCopies = planCliCopies({ cliName, destRoot });
14424
- const copies = [...cfgCopies, ...cliCopies];
14425
- // Interactive state
14426
- let globalDecision;
14427
- const rl = createInterface({ input: stdin, output: stdout });
14428
- try {
14429
- for (const item of copies) {
14430
- const exists = await fs.pathExists(item.dest);
14431
- if (!exists) {
14432
- const subs = item.subs ?? {};
14433
- await copyTextFile(item.src, item.dest, subs);
14434
- logger.log(`Created ${path.relative(cwd, item.dest)}`);
14435
- continue;
14436
- }
14437
- // Collision
14438
- if (force) {
14439
- const subs = item.subs ?? {};
14440
- await copyTextFile(item.src, item.dest, subs);
14441
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14442
- continue;
14443
- }
14444
- if (yes) {
14445
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14446
- continue;
14447
- }
14448
- let decision = globalDecision;
14449
- if (!decision) {
14450
- const a = await promptDecision(item.dest, logger, rl);
14451
- if (a === 'O') {
14452
- globalDecision = 'overwrite';
14453
- decision = 'overwrite';
14454
- }
14455
- else if (a === 'E') {
14456
- globalDecision = 'example';
14457
- decision = 'example';
14458
- }
14459
- else if (a === 'S') {
14460
- globalDecision = 'skip';
14461
- decision = 'skip';
14462
- }
14463
- else {
14464
- decision =
14465
- a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
14466
- }
14744
+ function attachInitDefaultAction(cli) {
14745
+ cli.action(async (destArg, opts, thisCommand) => {
14746
+ // Inherit logger from merged root options (base).
14747
+ const bag = readMergedOptions(thisCommand);
14748
+ const logger = bag.logger;
14749
+ const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
14750
+ const cwd = process.cwd();
14751
+ const destRoot = path.resolve(cwd, destRel);
14752
+ const formatInput = opts['configFormat'];
14753
+ const formatRaw = typeof formatInput === 'string' ? formatInput.toLowerCase() : 'json';
14754
+ const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw) ? formatRaw : 'json');
14755
+ const withLocal = Boolean(opts['withLocal']);
14756
+ // dynamic flag reserved for future template variants; present for UX compatibility
14757
+ void opts['dynamic'];
14758
+ // CLI name default: --cli-name | basename(dest) | 'mycli'
14759
+ const cliNameInput = opts['cliName'];
14760
+ const cliName = (typeof cliNameInput === 'string' && cliNameInput.length > 0
14761
+ ? cliNameInput
14762
+ : path.basename(destRoot) || 'mycli') || 'mycli';
14763
+ // Precedence: --force > --yes > auto-detect(non-interactive => yes)
14764
+ const force = Boolean(opts['force']);
14765
+ const yes = Boolean(opts['yes']) || (!force && isNonInteractive());
14766
+ // Build copy plan
14767
+ const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
14768
+ const cliCopies = planCliCopies({ cliName, destRoot });
14769
+ const copies = [...cfgCopies, ...cliCopies];
14770
+ // Interactive state
14771
+ let globalDecision;
14772
+ const rl = createInterface({ input: stdin, output: stdout });
14773
+ try {
14774
+ for (const item of copies) {
14775
+ const exists = await fs.pathExists(item.dest);
14776
+ if (!exists) {
14777
+ const subs = item.subs ?? {};
14778
+ await copyTextFile(item.src, item.dest, subs);
14779
+ logger.log(`Created ${path.relative(cwd, item.dest)}`);
14780
+ continue;
14781
+ }
14782
+ // Collision
14783
+ if (force) {
14784
+ const subs = item.subs ?? {};
14785
+ await copyTextFile(item.src, item.dest, subs);
14786
+ logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14787
+ continue;
14788
+ }
14789
+ if (yes) {
14790
+ logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14791
+ continue;
14792
+ }
14793
+ let decision = globalDecision;
14794
+ if (!decision) {
14795
+ const a = await promptDecision(item.dest, logger, rl);
14796
+ if (a === 'O') {
14797
+ globalDecision = 'overwrite';
14798
+ decision = 'overwrite';
14467
14799
  }
14468
- if (decision === 'overwrite') {
14469
- const subs = item.subs ?? {};
14470
- await copyTextFile(item.src, item.dest, subs);
14471
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14800
+ else if (a === 'E') {
14801
+ globalDecision = 'example';
14802
+ decision = 'example';
14472
14803
  }
14473
- else if (decision === 'example') {
14474
- const destEx = `${item.dest}.example`;
14475
- const subs = item.subs ?? {};
14476
- await copyTextFile(item.src, destEx, subs);
14477
- logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
14804
+ else if (a === 'S') {
14805
+ globalDecision = 'skip';
14806
+ decision = 'skip';
14478
14807
  }
14479
14808
  else {
14480
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14809
+ decision = a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
14481
14810
  }
14482
14811
  }
14483
- // Ensure .gitignore includes local config patterns.
14484
- const giPath = path.join(destRoot, '.gitignore');
14485
- const { created, changed } = await ensureLines(giPath, [
14486
- 'getdotenv.config.local.*',
14487
- '*.local',
14488
- ]);
14489
- if (created) {
14490
- logger.log(`Created ${path.relative(cwd, giPath)}`);
14812
+ if (decision === 'overwrite') {
14813
+ const subs = item.subs ?? {};
14814
+ await copyTextFile(item.src, item.dest, subs);
14815
+ logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14491
14816
  }
14492
- else if (changed) {
14493
- logger.log(`Updated ${path.relative(cwd, giPath)}`);
14817
+ else if (decision === 'example') {
14818
+ const destEx = `${item.dest}.example`;
14819
+ const subs = item.subs ?? {};
14820
+ await copyTextFile(item.src, destEx, subs);
14821
+ logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
14822
+ }
14823
+ else {
14824
+ logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14494
14825
  }
14495
14826
  }
14496
- finally {
14497
- rl.close();
14827
+ // Ensure .gitignore includes local config patterns.
14828
+ const giPath = path.join(destRoot, '.gitignore');
14829
+ const { created, changed } = await ensureLines(giPath, [
14830
+ 'getdotenv.config.local.*',
14831
+ '*.local',
14832
+ ]);
14833
+ if (created) {
14834
+ logger.log(`Created ${path.relative(cwd, giPath)}`);
14498
14835
  }
14499
- });
14836
+ else if (changed) {
14837
+ logger.log(`Updated ${path.relative(cwd, giPath)}`);
14838
+ }
14839
+ }
14840
+ finally {
14841
+ rl.close();
14842
+ }
14843
+ });
14844
+ }
14845
+
14846
+ /**
14847
+ * Attach options/arguments for the init plugin mount.
14848
+ *
14849
+ * @param cli - The `init` command mount.
14850
+ *
14851
+ * @internal
14852
+ */
14853
+ function attachInitOptions(cli) {
14854
+ return (cli
14855
+ // Description is owned by the plugin index (src/plugins/init/index.ts).
14856
+ .argument('[dest]', 'destination path (default: ./)', '.')
14857
+ .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
14858
+ .option('--with-local', 'include .local config variant')
14859
+ .option('--dynamic', 'include dynamic examples (JS/TS configs)')
14860
+ .option('--cli-name <string>', 'CLI name for skeleton and tokens')
14861
+ .option('--force', 'overwrite all existing files')
14862
+ .option('--yes', 'skip all collisions (no overwrite)'));
14863
+ }
14864
+
14865
+ /**
14866
+ * @packageDocumentation
14867
+ * Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
14868
+ * host‑based CLI skeleton with collision handling and CI‑safe defaults.
14869
+ */
14870
+ /**
14871
+ * Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
14872
+ * Supports collision detection, interactive prompts, and CI bypass.
14873
+ */
14874
+ const initPlugin = () => definePlugin({
14875
+ ns: 'init',
14876
+ setup(cli) {
14877
+ cli.description('Scaffold getdotenv config files and a host-based CLI skeleton.');
14878
+ const initCmd = attachInitOptions(cli);
14879
+ attachInitDefaultAction(initCmd);
14500
14880
  return undefined;
14501
14881
  },
14502
14882
  });