@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/cli.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 = {};
@@ -711,6 +729,10 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
711
729
  * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
712
730
  * - Else if `requiredKeys` is present, check presence (value !== undefined).
713
731
  * - Returns a flat list of issue strings; caller decides warn vs fail.
732
+ *
733
+ * @param finalEnv - Final composed environment to validate.
734
+ * @param sources - Resolved config sources providing `schema` and/or `requiredKeys`.
735
+ * @returns A list of human-readable issue strings (empty when valid).
714
736
  */
715
737
  const validateEnvAgainstSources = (finalEnv, sources) => {
716
738
  const pick = (getter) => {
@@ -770,6 +792,11 @@ const validateEnvAgainstSources = (finalEnv, sources) => {
770
792
  * Apply a dynamic map to the target progressively.
771
793
  * - Functions receive (target, env) and may return string | undefined.
772
794
  * - Literals are assigned directly (including undefined).
795
+ *
796
+ * @param target - Mutable target environment to assign into.
797
+ * @param map - Dynamic map to apply (functions and/or literal values).
798
+ * @param env - Selected environment name (if any) passed through to dynamic functions.
799
+ * @returns Nothing.
773
800
  */
774
801
  function applyDynamicMap(target, map, env) {
775
802
  if (!map)
@@ -789,6 +816,12 @@ function applyDynamicMap(target, map, env) {
789
816
  * Error behavior:
790
817
  * - On failure to load/compile/evaluate the module, throws a unified message:
791
818
  * "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
819
+ *
820
+ * @param target - Mutable target environment to assign into.
821
+ * @param absPath - Absolute path to the dynamic module file.
822
+ * @param env - Selected environment name (if any).
823
+ * @param cacheDirName - Cache subdirectory under `.tsbuild/` for compiled artifacts.
824
+ * @returns A `Promise\<void\>` which resolves after the module (if present) has been applied.
792
825
  */
793
826
  async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
794
827
  if (!(await fs.exists(absPath)))
@@ -1951,6 +1984,10 @@ function renderOptionGroups(cmd) {
1951
1984
  /**
1952
1985
  * Compose root/parent help output by inserting grouped sections between
1953
1986
  * Options and Commands, ensuring a trailing blank line.
1987
+ *
1988
+ * @param base - Base help text produced by Commander.
1989
+ * @param cmd - Command instance whose grouped options should be rendered.
1990
+ * @returns The modified help text with grouped blocks inserted.
1954
1991
  */
1955
1992
  function buildHelpInformation(base, cmd) {
1956
1993
  const groups = renderOptionGroups(cmd);
@@ -2463,6 +2500,10 @@ const toHelpConfig = (merged, plugins) => {
2463
2500
  /**
2464
2501
  * Compose a child-process env overlay from dotenv and the merged CLI options bag.
2465
2502
  * Returns a shallow object including getDotenvCliOptions when serializable.
2503
+ *
2504
+ * @param merged - Resolved CLI options bag (or a JSON-serializable subset).
2505
+ * @param dotenv - Composed dotenv variables for the current invocation.
2506
+ * @returns A string-only env overlay suitable for child process spawning.
2466
2507
  */
2467
2508
  function composeNestedEnv(merged, dotenv) {
2468
2509
  const out = {};
@@ -2485,6 +2526,7 @@ function composeNestedEnv(merged, dotenv) {
2485
2526
  * Strip one layer of symmetric outer quotes (single or double) from a string.
2486
2527
  *
2487
2528
  * @param s - Input string.
2529
+ * @returns `s` without one symmetric outer quote pair (when present).
2488
2530
  */
2489
2531
  const stripOne = (s) => {
2490
2532
  if (s.length < 2)
@@ -2497,6 +2539,9 @@ const stripOne = (s) => {
2497
2539
  /**
2498
2540
  * Preserve argv array for Node -e/--eval payloads under shell-off and
2499
2541
  * peel one symmetric outer quote layer from the code argument.
2542
+ *
2543
+ * @param args - Argument vector intended for direct execution (shell-off).
2544
+ * @returns Either the original `args` or a modified copy with a normalized eval payload.
2500
2545
  */
2501
2546
  function maybePreserveNodeEvalArgv(args) {
2502
2547
  if (args.length >= 3) {
@@ -2963,23 +3008,57 @@ function applyRootVisibility(program, visibility) {
2963
3008
  * Apply resolved AWS context to `process.env` and `ctx.plugins`.
2964
3009
  * Centralizes logic shared between the plugin action and `afterResolve` hook.
2965
3010
  *
3011
+ * @param out - Resolved AWS context to apply.
3012
+ * @param ctx - Host context to publish non-sensitive metadata into.
2966
3013
  * @param setProcessEnv - Whether to write credentials/region to `process.env` (default true).
3014
+ * @returns Nothing.
2967
3015
  */
2968
3016
  function applyAwsContext(out, ctx, setProcessEnv = true) {
2969
3017
  const { profile, region, credentials } = out;
2970
3018
  if (setProcessEnv) {
2971
- if (region) {
2972
- process.env.AWS_REGION = region;
2973
- if (!process.env.AWS_DEFAULT_REGION) {
2974
- process.env.AWS_DEFAULT_REGION = region;
3019
+ // Ensure AWS credential sources are mutually exclusive.
3020
+ // The AWS SDK warns (and may change precedence in future) when both
3021
+ // AWS_PROFILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are set.
3022
+ const clear = (keys) => {
3023
+ for (const k of keys) {
3024
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
3025
+ delete process.env[k];
2975
3026
  }
2976
- }
3027
+ };
3028
+ const clearProfileVars = () => {
3029
+ clear(['AWS_PROFILE', 'AWS_DEFAULT_PROFILE', 'AWS_SDK_LOAD_CONFIG']);
3030
+ };
3031
+ const clearStaticCreds = () => {
3032
+ clear([
3033
+ 'AWS_ACCESS_KEY_ID',
3034
+ 'AWS_SECRET_ACCESS_KEY',
3035
+ 'AWS_SESSION_TOKEN',
3036
+ ]);
3037
+ };
3038
+ // Mode A: exported/static credentials (clear profile vars)
2977
3039
  if (credentials) {
3040
+ clearProfileVars();
2978
3041
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
2979
3042
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
2980
3043
  if (credentials.sessionToken !== undefined) {
2981
3044
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
2982
3045
  }
3046
+ else {
3047
+ delete process.env.AWS_SESSION_TOKEN;
3048
+ }
3049
+ }
3050
+ else if (profile) {
3051
+ // Mode B: profile-based (SSO) credentials (clear static creds)
3052
+ clearStaticCreds();
3053
+ process.env.AWS_PROFILE = profile;
3054
+ process.env.AWS_DEFAULT_PROFILE = profile;
3055
+ process.env.AWS_SDK_LOAD_CONFIG = '1';
3056
+ }
3057
+ if (region) {
3058
+ process.env.AWS_REGION = region;
3059
+ if (!process.env.AWS_DEFAULT_REGION) {
3060
+ process.env.AWS_DEFAULT_REGION = region;
3061
+ }
2983
3062
  }
2984
3063
  }
2985
3064
  // Always publish minimal, non-sensitive metadata
@@ -2990,7 +3069,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
2990
3069
  };
2991
3070
  }
2992
3071
 
2993
- const DEFAULT_TIMEOUT_MS = 15_000;
3072
+ const AWS_CLI_TIMEOUT_MS = 15_000;
2994
3073
  const trim = (s) => (typeof s === 'string' ? s.trim() : '');
2995
3074
  const unquote = (s) => s.length >= 2 &&
2996
3075
  ((s.startsWith('"') && s.endsWith('"')) ||
@@ -2999,6 +3078,9 @@ const unquote = (s) => s.length >= 2 &&
2999
3078
  : s;
3000
3079
  /**
3001
3080
  * Parse AWS credentials from JSON output (AWS CLI v2 export-credentials).
3081
+ *
3082
+ * @param txt - Raw stdout text from the AWS CLI.
3083
+ * @returns Parsed credentials, or `undefined` when the input is not recognized.
3002
3084
  */
3003
3085
  const parseExportCredentialsJson = (txt) => {
3004
3086
  try {
@@ -3022,6 +3104,10 @@ const parseExportCredentialsJson = (txt) => {
3022
3104
  /**
3023
3105
  * Parse AWS credentials from environment-export output (shell-agnostic).
3024
3106
  * Supports POSIX `export KEY=VAL` and PowerShell `$Env:KEY=VAL`.
3107
+ * Also supports AWS CLI `windows-cmd` (`set KEY=VAL`) and `env-no-export` (`KEY=VAL`).
3108
+ *
3109
+ * @param txt - Raw stdout text from the AWS CLI.
3110
+ * @returns Parsed credentials, or `undefined` when the input is not recognized.
3025
3111
  */
3026
3112
  const parseExportCredentialsEnv = (txt) => {
3027
3113
  const lines = txt.split(/\r?\n/);
@@ -3032,12 +3118,17 @@ const parseExportCredentialsEnv = (txt) => {
3032
3118
  const line = raw.trim();
3033
3119
  if (!line)
3034
3120
  continue;
3035
- // POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
3121
+ // POSIX: export AWS_ACCESS_KEY_ID=..., ...
3036
3122
  let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3037
- if (!m) {
3038
- // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
3039
- m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3040
- }
3123
+ // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
3124
+ if (!m)
3125
+ m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
3126
+ // Windows cmd: set AWS_ACCESS_KEY_ID=..., etc.
3127
+ if (!m)
3128
+ m = /^(?:set)\s+([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
3129
+ // env-no-export: AWS_ACCESS_KEY_ID=..., etc.
3130
+ if (!m)
3131
+ m = /^([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3041
3132
  if (!m)
3042
3133
  continue;
3043
3134
  const k = m[1];
@@ -3062,7 +3153,7 @@ const parseExportCredentialsEnv = (txt) => {
3062
3153
  };
3063
3154
  return undefined;
3064
3155
  };
3065
- const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
3156
+ const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
3066
3157
  const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
3067
3158
  env: process.env,
3068
3159
  timeoutMs,
@@ -3077,36 +3168,50 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
3077
3168
  }
3078
3169
  return undefined;
3079
3170
  };
3080
- const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
3081
- // Try JSON format first (AWS CLI v2)
3082
- const rJson = await runCommandResult([
3083
- 'aws',
3084
- 'configure',
3085
- 'export-credentials',
3086
- '--profile',
3087
- profile,
3088
- '--format',
3089
- 'json',
3090
- ], false, { env: process.env, timeoutMs });
3091
- if (rJson.exitCode === 0) {
3092
- const creds = parseExportCredentialsJson(rJson.stdout);
3093
- if (creds)
3094
- return creds;
3095
- }
3096
- // Fallback: env lines
3097
- const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
3098
- if (rEnv.exitCode === 0) {
3099
- const creds = parseExportCredentialsEnv(rEnv.stdout);
3171
+ const exportCredentials = async (profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
3172
+ const tryExport = async (format) => {
3173
+ const argv = [
3174
+ 'aws',
3175
+ 'configure',
3176
+ 'export-credentials',
3177
+ '--profile',
3178
+ profile,
3179
+ ...(format ? ['--format', format] : []),
3180
+ ];
3181
+ const r = await runCommandResult(argv, false, {
3182
+ env: process.env,
3183
+ timeoutMs,
3184
+ });
3185
+ if (r.exitCode !== 0)
3186
+ return undefined;
3187
+ const out = trim(r.stdout);
3188
+ if (!out)
3189
+ return undefined;
3190
+ // Some formats produce JSON ("process"), some produce shell-ish env lines.
3191
+ return parseExportCredentialsJson(out) ?? parseExportCredentialsEnv(out);
3192
+ };
3193
+ // Prefer the default/JSON "process" format first; then fall back to shell env outputs.
3194
+ // Note: AWS CLI v2 supports: process | env | env-no-export | powershell | windows-cmd
3195
+ const formats = [
3196
+ 'process',
3197
+ ...(process.platform === 'win32'
3198
+ ? ['powershell', 'windows-cmd', 'env', 'env-no-export']
3199
+ : ['env', 'env-no-export']),
3200
+ ];
3201
+ for (const f of formats) {
3202
+ const creds = await tryExport(f);
3100
3203
  if (creds)
3101
3204
  return creds;
3102
3205
  }
3103
- return undefined;
3206
+ // Final fallback: no --format (AWS CLI default output)
3207
+ return tryExport(undefined);
3104
3208
  };
3105
3209
  /**
3106
3210
  * Resolve AWS context (profile, region, credentials) using configuration and environment.
3107
3211
  * Applies strategy (cli-export vs none) and handling for SSO login-on-demand.
3108
3212
  *
3109
3213
  * @param options - Context options including current dotenv and plugin config.
3214
+ * @returns A `Promise\<AwsContext\>` containing any resolved profile, region, and credentials.
3110
3215
  */
3111
3216
  const resolveAwsContext = async ({ dotenv, cfg, }) => {
3112
3217
  const profileKey = cfg.profileKey ?? 'AWS_LOCAL_PROFILE';
@@ -3131,31 +3236,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3131
3236
  out.region = region;
3132
3237
  return out;
3133
3238
  }
3134
- // Env-first credentials.
3135
3239
  let credentials;
3136
- const envId = trim(process.env.AWS_ACCESS_KEY_ID);
3137
- const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
3138
- const envToken = trim(process.env.AWS_SESSION_TOKEN);
3139
- if (envId && envSecret) {
3140
- credentials = {
3141
- accessKeyId: envId,
3142
- secretAccessKey: envSecret,
3143
- ...(envToken ? { sessionToken: envToken } : {}),
3144
- };
3145
- }
3146
- else if (profile) {
3240
+ // Profile wins over ambient env creds when present (from flags/config/dotenv).
3241
+ if (profile) {
3147
3242
  // Try export-credentials
3148
3243
  credentials = await exportCredentials(profile);
3149
3244
  // On failure, detect SSO and optionally login then retry
3150
3245
  if (!credentials) {
3151
3246
  const ssoSession = await getAwsConfigure('sso_session', profile);
3152
- const looksSSO = typeof ssoSession === 'string' && ssoSession.length > 0;
3247
+ // Legacy SSO profiles use sso_start_url/sso_region rather than sso_session.
3248
+ const ssoStartUrl = await getAwsConfigure('sso_start_url', profile);
3249
+ const looksSSO = (typeof ssoSession === 'string' && ssoSession.length > 0) ||
3250
+ (typeof ssoStartUrl === 'string' && ssoStartUrl.length > 0);
3153
3251
  if (looksSSO && cfg.loginOnDemand) {
3154
- // Best-effort login, then retry export once.
3155
- await runCommandResult(['aws', 'sso', 'login', '--profile', profile], false, {
3252
+ // Interactive login (no timeout by default), then retry export once.
3253
+ const exit = await runCommand(['aws', 'sso', 'login', '--profile', profile], false, {
3156
3254
  env: process.env,
3157
- timeoutMs: DEFAULT_TIMEOUT_MS,
3255
+ stdio: 'inherit',
3158
3256
  });
3257
+ if (exit !== 0) {
3258
+ throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
3259
+ }
3159
3260
  credentials = await exportCredentials(profile);
3160
3261
  }
3161
3262
  }
@@ -3173,6 +3274,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3173
3274
  }
3174
3275
  }
3175
3276
  }
3277
+ else {
3278
+ // Env-first credentials when no profile is present.
3279
+ const envId = trim(process.env.AWS_ACCESS_KEY_ID);
3280
+ const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
3281
+ const envToken = trim(process.env.AWS_SESSION_TOKEN);
3282
+ if (envId && envSecret) {
3283
+ credentials = {
3284
+ accessKeyId: envId,
3285
+ secretAccessKey: envSecret,
3286
+ ...(envToken ? { sessionToken: envToken } : {}),
3287
+ };
3288
+ }
3289
+ }
3176
3290
  // Final region resolution
3177
3291
  if (!region && profile)
3178
3292
  region = await getAwsConfigure('region', profile);
@@ -3188,10 +3302,213 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3188
3302
  return out;
3189
3303
  };
3190
3304
 
3305
+ /**
3306
+ * Create the AWS plugin `afterResolve` hook.
3307
+ *
3308
+ * This runs once per invocation after the host resolves dotenv context.
3309
+ *
3310
+ * @param plugin - The AWS plugin instance.
3311
+ * @returns An `afterResolve` hook function suitable for assigning to `plugin.afterResolve`.
3312
+ *
3313
+ * @internal
3314
+ */
3315
+ function attachAwsAfterResolveHook(plugin) {
3316
+ return async (cli, ctx) => {
3317
+ const cfg = plugin.readConfig(cli);
3318
+ const out = await resolveAwsContext({
3319
+ dotenv: ctx.dotenv,
3320
+ cfg,
3321
+ });
3322
+ applyAwsContext(out, ctx, true);
3323
+ // Optional: low-noise breadcrumb for diagnostics
3324
+ if (process.env.GETDOTENV_DEBUG) {
3325
+ try {
3326
+ const msg = JSON.stringify({
3327
+ profile: out.profile,
3328
+ region: out.region,
3329
+ hasCreds: Boolean(out.credentials),
3330
+ });
3331
+ process.stderr.write(`[aws] afterResolve ${msg}\n`);
3332
+ }
3333
+ catch {
3334
+ /* ignore */
3335
+ }
3336
+ }
3337
+ };
3338
+ }
3339
+
3340
+ /** @internal */
3341
+ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
3342
+ /**
3343
+ * Create an AWS plugin config overlay from Commander-parsed option values.
3344
+ *
3345
+ * This preserves tri-state intent:
3346
+ * - If a flag was not provided, it should not overwrite config-derived defaults.
3347
+ * - If `--no-…` was provided, it must explicitly force the boolean false.
3348
+ *
3349
+ * @param opts - Commander option values for the current invocation.
3350
+ * @returns A partial AWS plugin config object containing only explicit overrides.
3351
+ *
3352
+ * @internal
3353
+ */
3354
+ function awsConfigOverridesFromCommandOpts(opts) {
3355
+ const o = isRecord(opts) ? opts : {};
3356
+ const overlay = {};
3357
+ // Map boolean toggles (respect explicit --no-*)
3358
+ if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand')) {
3359
+ overlay.loginOnDemand = Boolean(o.loginOnDemand);
3360
+ }
3361
+ // Strings/enums
3362
+ if (typeof o.profile === 'string')
3363
+ overlay.profile = o.profile;
3364
+ if (typeof o.region === 'string')
3365
+ overlay.region = o.region;
3366
+ if (typeof o.defaultRegion === 'string')
3367
+ overlay.defaultRegion = o.defaultRegion;
3368
+ if (o.strategy === 'cli-export' || o.strategy === 'none') {
3369
+ overlay.strategy = o.strategy;
3370
+ }
3371
+ // Advanced key overrides
3372
+ if (typeof o.profileKey === 'string')
3373
+ overlay.profileKey = o.profileKey;
3374
+ if (typeof o.profileFallbackKey === 'string') {
3375
+ overlay.profileFallbackKey = o.profileFallbackKey;
3376
+ }
3377
+ if (typeof o.regionKey === 'string')
3378
+ overlay.regionKey = o.regionKey;
3379
+ return overlay;
3380
+ }
3381
+
3382
+ /**
3383
+ * Attach the default action for the AWS plugin mount.
3384
+ *
3385
+ * Behavior:
3386
+ * - With args: forwards to AWS CLI (`aws <args...>`) under the established session.
3387
+ * - Without args: session-only establishment (no forward).
3388
+ *
3389
+ * @param cli - The `aws` command mount.
3390
+ * @param plugin - The AWS plugin instance.
3391
+ *
3392
+ * @internal
3393
+ */
3394
+ function attachAwsDefaultAction(cli, plugin, awsCmd) {
3395
+ awsCmd.action(async (args, opts, thisCommand) => {
3396
+ // Access merged root CLI options (installed by root hooks).
3397
+ const bag = readMergedOptions(thisCommand);
3398
+ const capture = shouldCapture(bag.capture);
3399
+ const underTests = process.env.GETDOTENV_TEST === '1' ||
3400
+ typeof process.env.VITEST_WORKER_ID === 'string';
3401
+ // Build overlay cfg from subcommand flags layered over discovered config.
3402
+ const ctx = cli.getCtx();
3403
+ const cfgBase = plugin.readConfig(cli);
3404
+ const cfg = {
3405
+ ...cfgBase,
3406
+ ...awsConfigOverridesFromCommandOpts(opts),
3407
+ };
3408
+ // Resolve current context with overrides
3409
+ const out = await resolveAwsContext({
3410
+ dotenv: ctx.dotenv,
3411
+ cfg,
3412
+ });
3413
+ // Publish env/context
3414
+ applyAwsContext(out, ctx, true);
3415
+ // Forward when positional args are present; otherwise session-only.
3416
+ if (args.length > 0) {
3417
+ const argv = ['aws', ...args];
3418
+ const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
3419
+ const exit = await runCommand(argv, shellSetting, {
3420
+ env: buildSpawnEnv(process.env, ctx.dotenv),
3421
+ stdio: capture ? 'pipe' : 'inherit',
3422
+ });
3423
+ // Deterministic termination (suppressed under tests)
3424
+ if (!underTests) {
3425
+ process.exit(typeof exit === 'number' ? exit : 0);
3426
+ }
3427
+ return;
3428
+ }
3429
+ // Session only: low-noise breadcrumb under debug
3430
+ if (process.env.GETDOTENV_DEBUG) {
3431
+ try {
3432
+ const msg = JSON.stringify({
3433
+ profile: out.profile,
3434
+ region: out.region,
3435
+ hasCreds: Boolean(out.credentials),
3436
+ });
3437
+ process.stderr.write(`[aws] session established ${msg}\n`);
3438
+ }
3439
+ catch {
3440
+ /* ignore */
3441
+ }
3442
+ }
3443
+ if (!underTests)
3444
+ process.exit(0);
3445
+ });
3446
+ }
3447
+
3448
+ /**
3449
+ * Attach options/arguments for the AWS plugin mount.
3450
+ *
3451
+ * @param cli - The `aws` command mount.
3452
+ * @param plugin - The AWS plugin instance (for dynamic option descriptions).
3453
+ *
3454
+ * @internal
3455
+ */
3456
+ function attachAwsOptions(cli, plugin) {
3457
+ return (cli
3458
+ // Description is owned by the plugin index (src/plugins/aws/index.ts).
3459
+ .enablePositionalOptions()
3460
+ .passThroughOptions()
3461
+ .allowUnknownOption(true)
3462
+ // Boolean toggles with dynamic help labels (effective defaults)
3463
+ .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
3464
+ .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
3465
+ // Strings / enums
3466
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
3467
+ .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
3468
+ .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
3469
+ .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
3470
+ // Advanced key overrides
3471
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
3472
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
3473
+ .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
3474
+ // Accept any extra operands so Commander does not error when tokens appear after "--".
3475
+ .argument('[args...]'));
3476
+ }
3477
+
3478
+ /**
3479
+ * Attach the AWS plugin `preSubcommand` hook.
3480
+ *
3481
+ * Ensures `aws --profile/--region <child>` applies the AWS session setup before
3482
+ * child subcommand execution.
3483
+ *
3484
+ * @param cli - The `aws` command mount.
3485
+ * @param plugin - The AWS plugin instance.
3486
+ *
3487
+ * @internal
3488
+ */
3489
+ function attachAwsPreSubcommandHook(cli, plugin) {
3490
+ cli.hook('preSubcommand', async (thisCommand) => {
3491
+ // Avoid side effects for help rendering.
3492
+ if (process.argv.includes('-h') || process.argv.includes('--help'))
3493
+ return;
3494
+ const ctx = cli.getCtx();
3495
+ const cfgBase = plugin.readConfig(cli);
3496
+ const cfg = {
3497
+ ...cfgBase,
3498
+ ...awsConfigOverridesFromCommandOpts(thisCommand.opts()),
3499
+ };
3500
+ const out = await resolveAwsContext({
3501
+ dotenv: ctx.dotenv,
3502
+ cfg,
3503
+ });
3504
+ applyAwsContext(out, ctx, true);
3505
+ });
3506
+ }
3507
+
3191
3508
  /**
3192
3509
  * Zod schema for AWS plugin configuration.
3193
3510
  */
3194
- const AwsPluginConfigSchema = z$2.object({
3511
+ const awsPluginConfigSchema = z$2.object({
3195
3512
  profile: z$2.string().optional(),
3196
3513
  region: z$2.string().optional(),
3197
3514
  defaultRegion: z$2.string().optional(),
@@ -3215,129 +3532,16 @@ const AwsPluginConfigSchema = z$2.object({
3215
3532
  const awsPlugin = () => {
3216
3533
  const plugin = definePlugin({
3217
3534
  ns: 'aws',
3218
- configSchema: AwsPluginConfigSchema,
3219
- setup: (cli) => {
3220
- // Mount: aws (provided)
3221
- cli
3222
- .description('Establish an AWS session and optionally forward to the AWS CLI')
3223
- .enablePositionalOptions()
3224
- .passThroughOptions()
3225
- .allowUnknownOption(true)
3226
- // Boolean toggles with dynamic help labels (effective defaults)
3227
- .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
3228
- .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
3229
- // Strings / enums
3230
- .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
3231
- .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
3232
- .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
3233
- .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
3234
- // Advanced key overrides
3235
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
3236
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
3237
- .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
3238
- // Accept any extra operands so Commander does not error when tokens appear after "--".
3239
- .argument('[args...]')
3240
- .action(async (args, opts, thisCommand) => {
3241
- const pluginInst = plugin;
3242
- // Access merged root CLI options (installed by passOptions())
3243
- const bag = readMergedOptions(thisCommand);
3244
- const capture = shouldCapture(bag.capture);
3245
- const underTests = process.env.GETDOTENV_TEST === '1' ||
3246
- typeof process.env.VITEST_WORKER_ID === 'string';
3247
- // Build overlay cfg from subcommand flags layered over discovered config.
3248
- const ctx = cli.getCtx();
3249
- const cfgBase = pluginInst.readConfig(cli);
3250
- const o = opts;
3251
- const overlay = {};
3252
- // Map boolean toggles (respect explicit --no-*)
3253
- if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
3254
- overlay.loginOnDemand = Boolean(o.loginOnDemand);
3255
- // Strings/enums
3256
- if (typeof o.profile === 'string')
3257
- overlay.profile = o.profile;
3258
- if (typeof o.region === 'string')
3259
- overlay.region = o.region;
3260
- if (typeof o.defaultRegion === 'string')
3261
- overlay.defaultRegion = o.defaultRegion;
3262
- if (typeof o.strategy === 'string')
3263
- overlay.strategy = o.strategy;
3264
- // Advanced key overrides
3265
- if (typeof o.profileKey === 'string')
3266
- overlay.profileKey = o.profileKey;
3267
- if (typeof o.profileFallbackKey === 'string')
3268
- overlay.profileFallbackKey = o.profileFallbackKey;
3269
- if (typeof o.regionKey === 'string')
3270
- overlay.regionKey = o.regionKey;
3271
- const cfg = {
3272
- ...cfgBase,
3273
- ...overlay,
3274
- };
3275
- // Resolve current context with overrides
3276
- const out = await resolveAwsContext({
3277
- dotenv: ctx.dotenv,
3278
- cfg,
3279
- });
3280
- // Publish env/context
3281
- applyAwsContext(out, ctx, true);
3282
- // Forward when positional args are present; otherwise session-only.
3283
- if (Array.isArray(args) && args.length > 0) {
3284
- const argv = ['aws', ...args];
3285
- const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
3286
- const exit = await runCommand(argv, shellSetting, {
3287
- env: buildSpawnEnv(process.env, ctx.dotenv),
3288
- stdio: capture ? 'pipe' : 'inherit',
3289
- });
3290
- // Deterministic termination (suppressed under tests)
3291
- if (!underTests) {
3292
- process.exit(typeof exit === 'number' ? exit : 0);
3293
- }
3294
- return;
3295
- }
3296
- else {
3297
- // Session only: low-noise breadcrumb under debug
3298
- if (process.env.GETDOTENV_DEBUG) {
3299
- try {
3300
- const msg = JSON.stringify({
3301
- profile: out.profile,
3302
- region: out.region,
3303
- hasCreds: Boolean(out.credentials),
3304
- });
3305
- process.stderr.write(`[aws] session established ${msg}\n`);
3306
- }
3307
- catch {
3308
- /* ignore */
3309
- }
3310
- }
3311
- if (!underTests)
3312
- process.exit(0);
3313
- return;
3314
- }
3315
- });
3535
+ configSchema: awsPluginConfigSchema,
3536
+ setup(cli) {
3537
+ cli.description('Establish an AWS session and optionally forward to the AWS CLI');
3538
+ const awsCmd = attachAwsOptions(cli, plugin);
3539
+ attachAwsPreSubcommandHook(cli, plugin);
3540
+ attachAwsDefaultAction(cli, plugin, awsCmd);
3316
3541
  return undefined;
3317
3542
  },
3318
- afterResolve: async (_cli, ctx) => {
3319
- const cfg = plugin.readConfig(_cli);
3320
- const out = await resolveAwsContext({
3321
- dotenv: ctx.dotenv,
3322
- cfg,
3323
- });
3324
- applyAwsContext(out, ctx, true);
3325
- // Optional: low-noise breadcrumb for diagnostics
3326
- if (process.env.GETDOTENV_DEBUG) {
3327
- try {
3328
- const msg = JSON.stringify({
3329
- profile: out.profile,
3330
- region: out.region,
3331
- hasCreds: Boolean(out.credentials),
3332
- });
3333
- process.stderr.write(`[aws] afterResolve ${msg}\n`);
3334
- }
3335
- catch {
3336
- /* ignore */
3337
- }
3338
- }
3339
- },
3340
3543
  });
3544
+ plugin.afterResolve = attachAwsAfterResolveHook(plugin);
3341
3545
  return plugin;
3342
3546
  };
3343
3547
 
@@ -13799,21 +14003,78 @@ class GetCallerIdentityCommand extends Command
13799
14003
  }
13800
14004
 
13801
14005
  /**
13802
- * AWS Whoami plugin: prints the current AWS caller identity (account, arn, userid).
13803
- * Intended to be mounted under the `aws` plugin.
14006
+ * Attach the default action for the `aws whoami` command.
14007
+ *
14008
+ * This behavior executes only when `aws whoami` is invoked without a subcommand.
14009
+ *
14010
+ * @param cli - The `whoami` command mount.
14011
+ * @returns Nothing.
14012
+ */
14013
+ function attachWhoamiDefaultAction(cli) {
14014
+ cli.action(async () => {
14015
+ // The AWS SDK default providers will read credentials from process.env,
14016
+ // which the aws parent has already populated.
14017
+ const client = new STSClient$1();
14018
+ const result = await client.send(new GetCallerIdentityCommand());
14019
+ console.log(JSON.stringify(result, null, 2));
14020
+ });
14021
+ }
14022
+
14023
+ /**
14024
+ * Attach options/arguments for the `aws whoami` plugin mount.
14025
+ *
14026
+ * This subcommand currently takes no flags/args; this module exists to keep the
14027
+ * wiring layout consistent across shipped plugins (options vs actions).
14028
+ *
14029
+ * Note: the plugin description is owned by `src/plugins/aws/whoami/index.ts` and
14030
+ * must not be set here.
14031
+ *
14032
+ * @param cli - The `whoami` command mount under `aws`.
14033
+ * @returns The same `cli` instance for chaining.
14034
+ *
14035
+ * @internal
14036
+ */
14037
+ function attachWhoamiOptions(cli) {
14038
+ return cli;
14039
+ }
14040
+
14041
+ /**
14042
+ * Attach the `really` subcommand under `aws whoami`.
14043
+ *
14044
+ * Reads `SECRET_IDENTITY` from the resolved get-dotenv context (`cli.getCtx().dotenv`).
14045
+ *
14046
+ * @param cli - The `whoami` command mount.
14047
+ * @returns Nothing.
14048
+ */
14049
+ function attachWhoamiReallyAction(cli) {
14050
+ const really = cli
14051
+ .ns('really')
14052
+ .description('Print SECRET_IDENTITY from the resolved dotenv context');
14053
+ really.action(() => {
14054
+ const secretIdentity = really.getCtx().dotenv.SECRET_IDENTITY;
14055
+ console.log(`Your secret identity is ${secretIdentity ?? 'still a secret'}.`);
14056
+ });
14057
+ }
14058
+
14059
+ /**
14060
+ * AWS Whoami plugin factory.
14061
+ *
14062
+ * This plugin demonstrates a “bucket of subcommands” pattern:
14063
+ * - Subcommand behavior is articulated in separate modules as `attach*` helpers.
14064
+ * - Those helpers are not individually composable plugins; they are internal wiring for one plugin instance.
14065
+ *
14066
+ * @returns A plugin instance mounted at `aws whoami`.
13804
14067
  */
13805
14068
  const awsWhoamiPlugin = () => definePlugin({
13806
14069
  ns: 'whoami',
13807
14070
  setup(cli) {
13808
- cli
13809
- .description('Print AWS caller identity (uses parent aws session)')
13810
- .action(async () => {
13811
- // The AWS SDK default providers will read credentials from process.env,
13812
- // which the aws parent has already populated.
13813
- const client = new STSClient$1();
13814
- const result = await client.send(new GetCallerIdentityCommand());
13815
- console.log(JSON.stringify(result, null, 2));
13816
- });
14071
+ cli.description('Print AWS caller identity (uses parent aws session)');
14072
+ // Options/args (none today, but keep layout consistent with other plugins).
14073
+ const whoami = attachWhoamiOptions(cli);
14074
+ // Default behavior: `getdotenv aws whoami`
14075
+ attachWhoamiDefaultAction(whoami);
14076
+ // Subcommand behavior: `getdotenv aws whoami really`
14077
+ attachWhoamiReallyAction(whoami);
13817
14078
  return undefined;
13818
14079
  },
13819
14080
  });
@@ -13921,7 +14182,7 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv,
13921
14182
  /**
13922
14183
  * Attach the default "cmd" subcommand action with contextual typing.
13923
14184
  */
13924
- const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
14185
+ const attachBatchCmdAction = (plugin, cli, batchCmd, pluginOpts, cmd) => {
13925
14186
  cmd.action(async (commandParts, _subOpts, thisCommand) => {
13926
14187
  const mergedBag = readMergedOptions(batchCmd);
13927
14188
  const logger = mergedBag.logger;
@@ -14030,11 +14291,37 @@ const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
14030
14291
  });
14031
14292
  };
14032
14293
 
14294
+ /**
14295
+ * Attach the default `cmd` subcommand under the `batch` command.
14296
+ *
14297
+ * This encapsulates:
14298
+ * - Subcommand construction (`new Command().name('cmd')…`)
14299
+ * - Action wiring
14300
+ * - Mounting as the default subcommand for `batch`
14301
+ *
14302
+ * @param plugin - The batch plugin instance.
14303
+ * @param cli - The batch command mount.
14304
+ * @param batchCmd - The `batch` command (same as `cli` mount).
14305
+ * @param pluginOpts - Batch plugin factory options.
14306
+ *
14307
+ * @internal
14308
+ */
14309
+ const attachBatchCmdSubcommand = (plugin, cli, batchCmd, pluginOpts) => {
14310
+ const cmdSub = new Command$1()
14311
+ .name('cmd')
14312
+ .description('execute command, conflicts with --command option (default subcommand)')
14313
+ .enablePositionalOptions()
14314
+ .passThroughOptions()
14315
+ .argument('[command...]');
14316
+ attachBatchCmdAction(plugin, cli, batchCmd, pluginOpts, cmdSub);
14317
+ batchCmd.addCommand(cmdSub, { isDefault: true });
14318
+ };
14319
+
14033
14320
  /**
14034
14321
  * Attach the parent-level action for the batch plugin.
14035
14322
  * Handles parent flags (e.g. `getdotenv batch -l`) and delegates to the batch executor.
14036
14323
  */
14037
- const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
14324
+ const attachBatchDefaultAction = (plugin, cli, pluginOpts, parent) => {
14038
14325
  parent.action(async function (...args) {
14039
14326
  // Commander Unknown generics: [...unknown[], OptionValues, thisCommand]
14040
14327
  const thisCommand = args[args.length - 1];
@@ -14135,6 +14422,45 @@ const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
14135
14422
  });
14136
14423
  };
14137
14424
 
14425
+ /**
14426
+ * Attach options/arguments for the batch plugin mount.
14427
+ *
14428
+ * Note: the plugin description is owned by `src/plugins/batch/index.ts` and
14429
+ * must not be set here.
14430
+ *
14431
+ * @param plugin - Batch plugin instance (for dynamic option descriptions).
14432
+ * @param cli - The `batch` command mount.
14433
+ * @returns The same `cli` instance for chaining.
14434
+ *
14435
+ * @internal
14436
+ */
14437
+ function attachBatchOptions(plugin, cli) {
14438
+ const GROUP = `plugin:${cli.name()}`;
14439
+ return (cli
14440
+ .enablePositionalOptions()
14441
+ .passThroughOptions()
14442
+ // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
14443
+ .addOption((() => {
14444
+ const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
14445
+ cli.setOptionGroup(opt, GROUP);
14446
+ return opt;
14447
+ })())
14448
+ .addOption((() => {
14449
+ 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 || './')})`);
14450
+ cli.setOptionGroup(opt, GROUP);
14451
+ return opt;
14452
+ })())
14453
+ .addOption((() => {
14454
+ const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
14455
+ cli.setOptionGroup(opt, GROUP);
14456
+ return opt;
14457
+ })())
14458
+ .option('-c, --command <string>', 'command executed according to the base shell resolution')
14459
+ .option('-l, --list', 'list working directories without executing command')
14460
+ .option('-e, --ignore-errors', 'ignore errors and continue with next path')
14461
+ .argument('[command...]'));
14462
+ }
14463
+
14138
14464
  /**
14139
14465
  * Zod schema for a single script entry (string or object).
14140
14466
  */
@@ -14148,7 +14474,7 @@ const ScriptSchema = z$2.union([
14148
14474
  /**
14149
14475
  * Zod schema for batch plugin configuration.
14150
14476
  */
14151
- const BatchConfigSchema = z$2.object({
14477
+ const batchPluginConfigSchema = z$2.object({
14152
14478
  scripts: z$2.record(z$2.string(), ScriptSchema).optional(),
14153
14479
  shell: z$2.union([z$2.string(), z$2.boolean()]).optional(),
14154
14480
  rootPath: z$2.string().optional(),
@@ -14174,45 +14500,15 @@ const batchPlugin = (opts = {}) => {
14174
14500
  ns: 'batch',
14175
14501
  // Host validates this when config-loader is enabled; plugins may also
14176
14502
  // re-validate at action time as a safety belt.
14177
- configSchema: BatchConfigSchema,
14503
+ configSchema: batchPluginConfigSchema,
14178
14504
  setup(cli) {
14179
14505
  const batchCmd = cli; // mount provided by host
14180
- const GROUP = `plugin:${cli.name()}`;
14181
- batchCmd
14182
- .description('Batch command execution across multiple working directories.')
14183
- .enablePositionalOptions()
14184
- .passThroughOptions()
14185
- // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
14186
- .addOption((() => {
14187
- const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
14188
- cli.setOptionGroup(opt, GROUP);
14189
- return opt;
14190
- })())
14191
- .addOption((() => {
14192
- 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 || './')})`);
14193
- cli.setOptionGroup(opt, GROUP);
14194
- return opt;
14195
- })())
14196
- .addOption((() => {
14197
- const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
14198
- cli.setOptionGroup(opt, GROUP);
14199
- return opt;
14200
- })())
14201
- .option('-c, --command <string>', 'command executed according to the base shell resolution')
14202
- .option('-l, --list', 'list working directories without executing command')
14203
- .option('-e, --ignore-errors', 'ignore errors and continue with next path')
14204
- .argument('[command...]');
14205
- // Default subcommand "cmd" with contextual typing for args/opts
14206
- const cmdSub = new Command$1()
14207
- .name('cmd')
14208
- .description('execute command, conflicts with --command option (default subcommand)')
14209
- .enablePositionalOptions()
14210
- .passThroughOptions()
14211
- .argument('[command...]');
14212
- attachDefaultCmdAction$1(plugin, cli, batchCmd, opts, cmdSub);
14213
- batchCmd.addCommand(cmdSub, { isDefault: true });
14214
- // Parent invoker (unified naming)
14215
- attachParentInvoker$1(plugin, cli, opts, batchCmd);
14506
+ batchCmd.description('Batch command execution across multiple working directories.');
14507
+ attachBatchOptions(plugin, batchCmd);
14508
+ // Default subcommand `cmd` (mounted as batch default subcommand)
14509
+ attachBatchCmdSubcommand(plugin, cli, batchCmd, opts);
14510
+ // Default action for the batch command mount (parent flags and positional form)
14511
+ attachBatchDefaultAction(plugin, cli, opts, batchCmd);
14216
14512
  return undefined;
14217
14513
  },
14218
14514
  });
@@ -14306,14 +14602,11 @@ async function runCmdWithContext(cli, merged, command, _opts) {
14306
14602
  }
14307
14603
 
14308
14604
  /**
14309
- * Attach the default "cmd" subcommand action (unified name).
14605
+ * Attach the default "cmd" subcommand action.
14310
14606
  * Mirrors the prior inline implementation in cmd/index.ts.
14311
14607
  */
14312
- const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
14313
- cmd
14314
- .enablePositionalOptions()
14315
- .passThroughOptions()
14316
- .action(async function (...allArgs) {
14608
+ const attachCmdDefaultAction = (cli, cmd, aliasKey) => {
14609
+ cmd.action(async function (...allArgs) {
14317
14610
  // Commander passes: [...positionals, options, thisCommand]
14318
14611
  const thisCommand = allArgs[allArgs.length - 1];
14319
14612
  const commandParts = allArgs[0];
@@ -14346,11 +14639,29 @@ const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
14346
14639
  });
14347
14640
  };
14348
14641
 
14642
+ /**
14643
+ * Attach options/arguments for the cmd plugin mount.
14644
+ *
14645
+ * Note: the plugin description is owned by `src/plugins/cmd/index.ts` and must
14646
+ * not be set here.
14647
+ *
14648
+ * @param cli - The `cmd` command mount.
14649
+ * @returns The same `cli` instance for chaining.
14650
+ *
14651
+ * @internal
14652
+ */
14653
+ function attachCmdOptions(cli) {
14654
+ return cli
14655
+ .enablePositionalOptions()
14656
+ .passThroughOptions()
14657
+ .argument('[command...]');
14658
+ }
14659
+
14349
14660
  /**
14350
14661
  * Install the parent-level invoker (alias) for the cmd plugin.
14351
14662
  * Unifies naming with batch attachParentInvoker; behavior unchanged.
14352
14663
  */
14353
- const attachParentInvoker = (cli, options, _cmd, plugin) => {
14664
+ const attachCmdParentInvoker = (cli, options, plugin) => {
14354
14665
  const dbg = (...args) => {
14355
14666
  if (process.env.GETDOTENV_DEBUG) {
14356
14667
  try {
@@ -14461,7 +14772,7 @@ const attachParentInvoker = (cli, options, _cmd, plugin) => {
14461
14772
  /**
14462
14773
  * Zod schema for cmd plugin configuration.
14463
14774
  */
14464
- const CmdConfigSchema = z$2
14775
+ const cmdPluginConfigSchema = z$2
14465
14776
  .object({
14466
14777
  expand: z$2.boolean().optional(),
14467
14778
  })
@@ -14481,7 +14792,7 @@ const CmdConfigSchema = z$2
14481
14792
  const cmdPlugin = (options = {}) => {
14482
14793
  const plugin = definePlugin({
14483
14794
  ns: 'cmd',
14484
- configSchema: CmdConfigSchema,
14795
+ configSchema: cmdPluginConfigSchema,
14485
14796
  setup(cli) {
14486
14797
  const aliasSpec = typeof options.optionAlias === 'string'
14487
14798
  ? { flags: options.optionAlias}
@@ -14493,14 +14804,13 @@ const cmdPlugin = (options = {}) => {
14493
14804
  };
14494
14805
  const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
14495
14806
  // Mount is the command ('cmd'); attach default action.
14496
- cli
14497
- .description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
14498
- // Accept payload tokens as positional arguments for the default subcommand.
14499
- .argument('[command...]');
14500
- attachDefaultCmdAction(cli, cli, aliasKey);
14807
+ cli.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)');
14808
+ // Options/arguments (positional payload, argv routing) are attached separately.
14809
+ attachCmdOptions(cli);
14810
+ attachCmdDefaultAction(cli, cli, aliasKey);
14501
14811
  // Parent-attached option alias (optional, unified naming).
14502
14812
  if (aliasSpec !== undefined) {
14503
- attachParentInvoker(cli, options, cli, plugin);
14813
+ attachCmdParentInvoker(cli, options, plugin);
14504
14814
  }
14505
14815
  return undefined;
14506
14816
  },
@@ -14509,14 +14819,21 @@ const cmdPlugin = (options = {}) => {
14509
14819
  };
14510
14820
 
14511
14821
  /**
14512
- * Ensure a directory exists.
14822
+ * Ensure a directory exists (parents included).
14823
+ *
14824
+ * @param p - Directory path to create.
14825
+ * @returns A `Promise\<string\>` resolving to the provided `p` value.
14513
14826
  */
14514
14827
  const ensureDir = async (p) => {
14515
14828
  await fs.ensureDir(p);
14516
14829
  return p;
14517
14830
  };
14518
14831
  /**
14519
- * Write text content to a file, ensuring the parent directory exists.
14832
+ * Write UTF-8 text content to a file, ensuring the parent directory exists.
14833
+ *
14834
+ * @param dest - Destination file path.
14835
+ * @param data - File contents to write.
14836
+ * @returns A `Promise\<void\>` which resolves when the file is written.
14520
14837
  */
14521
14838
  const writeFile$1 = async (dest, data) => {
14522
14839
  await ensureDir(path.dirname(dest));
@@ -14528,6 +14845,7 @@ const writeFile$1 = async (dest, data) => {
14528
14845
  * @param src - Source file path.
14529
14846
  * @param dest - Destination file path.
14530
14847
  * @param substitutions - Map of token literals to replacement strings.
14848
+ * @returns A `Promise\<void\>` which resolves when the file has been copied.
14531
14849
  */
14532
14850
  const copyTextFile = async (src, dest, substitutions) => {
14533
14851
  const contents = await fs.readFile(src, 'utf-8');
@@ -14539,6 +14857,10 @@ const copyTextFile = async (src, dest, substitutions) => {
14539
14857
  /**
14540
14858
  * Ensure a set of lines exist (exact match) in a file. Creates the file
14541
14859
  * when missing. Returns whether it was created or changed.
14860
+ *
14861
+ * @param filePath - Target file path to create/update.
14862
+ * @param lines - Lines which must be present (exact string match).
14863
+ * @returns A `Promise\<object\>` describing whether the file was created and/or changed.
14542
14864
  */
14543
14865
  const ensureLines = async (filePath, lines) => {
14544
14866
  const exists = await fs.pathExists(filePath);
@@ -14566,11 +14888,23 @@ const ensureLines = async (filePath, lines) => {
14566
14888
  return { created: false, changed: false };
14567
14889
  };
14568
14890
 
14569
- // Templates root used by the scaffolder
14891
+ /**
14892
+ * Absolute path to the shipped templates directory.
14893
+ *
14894
+ * Used by the init scaffolder to locate files under `templates/` at runtime.
14895
+ *
14896
+ * @remarks
14897
+ * This path is resolved relative to the current working directory. It assumes
14898
+ * the `templates/` folder is present alongside the installed package (or in the
14899
+ * repository when running from source).
14900
+ */
14570
14901
  const TEMPLATES_ROOT = path.resolve('templates');
14571
14902
 
14572
14903
  /**
14573
14904
  * Plan the copy operations for configuration files.
14905
+ *
14906
+ * @param options - Planning options for config scaffolding.
14907
+ * @returns An array of copy operations to perform.
14574
14908
  */
14575
14909
  const planConfigCopies = ({ format, withLocal, destRoot, }) => {
14576
14910
  const copies = [];
@@ -14614,10 +14948,14 @@ const planConfigCopies = ({ format, withLocal, destRoot, }) => {
14614
14948
  };
14615
14949
  /**
14616
14950
  * Plan the copy operations for the CLI skeleton.
14951
+ *
14952
+ * @param options - Planning options for CLI scaffolding.
14953
+ * @returns An array of copy operations to perform.
14617
14954
  */
14618
14955
  const planCliCopies = ({ cliName, destRoot, }) => {
14619
14956
  const subs = { __CLI_NAME__: cliName };
14620
14957
  const base = path.join(destRoot, 'src', 'cli', cliName);
14958
+ const helloBase = path.join(base, 'plugins', 'hello');
14621
14959
  return [
14622
14960
  {
14623
14961
  src: path.join(TEMPLATES_ROOT, 'cli', 'index.ts'),
@@ -14625,8 +14963,28 @@ const planCliCopies = ({ cliName, destRoot, }) => {
14625
14963
  subs,
14626
14964
  },
14627
14965
  {
14628
- src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello.ts'),
14629
- dest: path.join(base, 'plugins', 'hello.ts'),
14966
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'index.ts'),
14967
+ dest: path.join(helloBase, 'index.ts'),
14968
+ subs,
14969
+ },
14970
+ {
14971
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'options.ts'),
14972
+ dest: path.join(helloBase, 'options.ts'),
14973
+ subs,
14974
+ },
14975
+ {
14976
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'defaultAction.ts'),
14977
+ dest: path.join(helloBase, 'defaultAction.ts'),
14978
+ subs,
14979
+ },
14980
+ {
14981
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'strangerAction.ts'),
14982
+ dest: path.join(helloBase, 'strangerAction.ts'),
14983
+ subs,
14984
+ },
14985
+ {
14986
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'types.ts'),
14987
+ dest: path.join(helloBase, 'types.ts'),
14630
14988
  subs,
14631
14989
  },
14632
14990
  ];
@@ -14635,6 +14993,8 @@ const planCliCopies = ({ cliName, destRoot, }) => {
14635
14993
  /**
14636
14994
  * Determine whether the current environment should be treated as non-interactive.
14637
14995
  * CI heuristics include: CI, GITHUB_ACTIONS, BUILDKITE, TEAMCITY_VERSION, TF_BUILD.
14996
+ *
14997
+ * @returns `true` when running in a CI-like environment or when stdin/stdout are not TTYs.
14638
14998
  */
14639
14999
  const isNonInteractive = () => {
14640
15000
  const ciLike = process.env.CI ||
@@ -14647,6 +15007,11 @@ const isNonInteractive = () => {
14647
15007
  /**
14648
15008
  * Prompt the user for a file collision decision.
14649
15009
  * Returns a single-character code representing overwrite/example/skip (or 'all' variants).
15010
+ *
15011
+ * @param filePath - Path of the colliding file (for display).
15012
+ * @param logger - Logger used for user-facing messages.
15013
+ * @param rl - Readline interface used to capture user input.
15014
+ * @returns A single-character decision code.
14650
15015
  */
14651
15016
  const promptDecision = async (filePath, logger, rl) => {
14652
15017
  logger.log(`File exists: ${filePath}\nChoose: [o]verwrite, [e]xample, [s]kip, [O]verwrite All, [E]xample All, [S]kip All`);
@@ -14661,129 +15026,148 @@ const promptDecision = async (filePath, logger, rl) => {
14661
15026
  };
14662
15027
 
14663
15028
  /**
14664
- * @packageDocumentation
14665
- * Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
14666
- * host‑based CLI skeleton with collision handling and CI‑safe defaults.
14667
- */
14668
- /**
14669
- * Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
14670
- * Supports collision detection, interactive prompts, and CI bypass.
15029
+ * Attach the init plugin default action.
15030
+ *
15031
+ * @param cli - The `init` command mount (with args/options attached).
15032
+ *
15033
+ * @internal
14671
15034
  */
14672
- const initPlugin = () => definePlugin({
14673
- ns: 'init',
14674
- setup(cli) {
14675
- cli
14676
- .description('Scaffold getdotenv config files and a host-based CLI skeleton.')
14677
- .argument('[dest]', 'destination path (default: ./)', '.')
14678
- .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
14679
- .option('--with-local', 'include .local config variant')
14680
- .option('--dynamic', 'include dynamic examples (JS/TS configs)')
14681
- .option('--cli-name <string>', 'CLI name for skeleton and tokens')
14682
- .option('--force', 'overwrite all existing files')
14683
- .option('--yes', 'skip all collisions (no overwrite)')
14684
- .action(async (destArg, opts, thisCommand) => {
14685
- // Inherit logger from merged root options (base).
14686
- const bag = readMergedOptions(thisCommand);
14687
- const logger = bag.logger;
14688
- const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
14689
- const cwd = process.cwd();
14690
- const destRoot = path.resolve(cwd, destRel);
14691
- const formatInput = opts.configFormat;
14692
- const formatRaw = typeof formatInput === 'string'
14693
- ? formatInput.toLowerCase()
14694
- : 'json';
14695
- const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw)
14696
- ? formatRaw
14697
- : 'json');
14698
- const withLocal = !!opts.withLocal;
14699
- // dynamic flag reserved for future template variants; present for UX compatibility
14700
- void opts.dynamic;
14701
- // CLI name default: --cli-name | basename(dest) | 'mycli'
14702
- const cliName = (typeof opts.cliName === 'string' && opts.cliName.length > 0
14703
- ? opts.cliName
14704
- : path.basename(destRoot) || 'mycli') || 'mycli';
14705
- // Precedence: --force > --yes > auto-detect(non-interactive => yes)
14706
- const force = !!opts.force;
14707
- const yes = !!opts.yes || (!force && isNonInteractive());
14708
- // Build copy plan
14709
- const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
14710
- const cliCopies = planCliCopies({ cliName, destRoot });
14711
- const copies = [...cfgCopies, ...cliCopies];
14712
- // Interactive state
14713
- let globalDecision;
14714
- const rl = createInterface({ input: stdin, output: stdout });
14715
- try {
14716
- for (const item of copies) {
14717
- const exists = await fs.pathExists(item.dest);
14718
- if (!exists) {
14719
- const subs = item.subs ?? {};
14720
- await copyTextFile(item.src, item.dest, subs);
14721
- logger.log(`Created ${path.relative(cwd, item.dest)}`);
14722
- continue;
14723
- }
14724
- // Collision
14725
- if (force) {
14726
- const subs = item.subs ?? {};
14727
- await copyTextFile(item.src, item.dest, subs);
14728
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14729
- continue;
14730
- }
14731
- if (yes) {
14732
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14733
- continue;
14734
- }
14735
- let decision = globalDecision;
14736
- if (!decision) {
14737
- const a = await promptDecision(item.dest, logger, rl);
14738
- if (a === 'O') {
14739
- globalDecision = 'overwrite';
14740
- decision = 'overwrite';
14741
- }
14742
- else if (a === 'E') {
14743
- globalDecision = 'example';
14744
- decision = 'example';
14745
- }
14746
- else if (a === 'S') {
14747
- globalDecision = 'skip';
14748
- decision = 'skip';
14749
- }
14750
- else {
14751
- decision =
14752
- a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
14753
- }
15035
+ function attachInitDefaultAction(cli) {
15036
+ cli.action(async (destArg, opts, thisCommand) => {
15037
+ // Inherit logger from merged root options (base).
15038
+ const bag = readMergedOptions(thisCommand);
15039
+ const logger = bag.logger;
15040
+ const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
15041
+ const cwd = process.cwd();
15042
+ const destRoot = path.resolve(cwd, destRel);
15043
+ const formatInput = opts['configFormat'];
15044
+ const formatRaw = typeof formatInput === 'string' ? formatInput.toLowerCase() : 'json';
15045
+ const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw) ? formatRaw : 'json');
15046
+ const withLocal = Boolean(opts['withLocal']);
15047
+ // dynamic flag reserved for future template variants; present for UX compatibility
15048
+ void opts['dynamic'];
15049
+ // CLI name default: --cli-name | basename(dest) | 'mycli'
15050
+ const cliNameInput = opts['cliName'];
15051
+ const cliName = (typeof cliNameInput === 'string' && cliNameInput.length > 0
15052
+ ? cliNameInput
15053
+ : path.basename(destRoot) || 'mycli') || 'mycli';
15054
+ // Precedence: --force > --yes > auto-detect(non-interactive => yes)
15055
+ const force = Boolean(opts['force']);
15056
+ const yes = Boolean(opts['yes']) || (!force && isNonInteractive());
15057
+ // Build copy plan
15058
+ const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
15059
+ const cliCopies = planCliCopies({ cliName, destRoot });
15060
+ const copies = [...cfgCopies, ...cliCopies];
15061
+ // Interactive state
15062
+ let globalDecision;
15063
+ const rl = createInterface({ input: stdin, output: stdout });
15064
+ try {
15065
+ for (const item of copies) {
15066
+ const exists = await fs.pathExists(item.dest);
15067
+ if (!exists) {
15068
+ const subs = item.subs ?? {};
15069
+ await copyTextFile(item.src, item.dest, subs);
15070
+ logger.log(`Created ${path.relative(cwd, item.dest)}`);
15071
+ continue;
15072
+ }
15073
+ // Collision
15074
+ if (force) {
15075
+ const subs = item.subs ?? {};
15076
+ await copyTextFile(item.src, item.dest, subs);
15077
+ logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
15078
+ continue;
15079
+ }
15080
+ if (yes) {
15081
+ logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
15082
+ continue;
15083
+ }
15084
+ let decision = globalDecision;
15085
+ if (!decision) {
15086
+ const a = await promptDecision(item.dest, logger, rl);
15087
+ if (a === 'O') {
15088
+ globalDecision = 'overwrite';
15089
+ decision = 'overwrite';
14754
15090
  }
14755
- if (decision === 'overwrite') {
14756
- const subs = item.subs ?? {};
14757
- await copyTextFile(item.src, item.dest, subs);
14758
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
15091
+ else if (a === 'E') {
15092
+ globalDecision = 'example';
15093
+ decision = 'example';
14759
15094
  }
14760
- else if (decision === 'example') {
14761
- const destEx = `${item.dest}.example`;
14762
- const subs = item.subs ?? {};
14763
- await copyTextFile(item.src, destEx, subs);
14764
- logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
15095
+ else if (a === 'S') {
15096
+ globalDecision = 'skip';
15097
+ decision = 'skip';
14765
15098
  }
14766
15099
  else {
14767
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
15100
+ decision = a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
14768
15101
  }
14769
15102
  }
14770
- // Ensure .gitignore includes local config patterns.
14771
- const giPath = path.join(destRoot, '.gitignore');
14772
- const { created, changed } = await ensureLines(giPath, [
14773
- 'getdotenv.config.local.*',
14774
- '*.local',
14775
- ]);
14776
- if (created) {
14777
- logger.log(`Created ${path.relative(cwd, giPath)}`);
15103
+ if (decision === 'overwrite') {
15104
+ const subs = item.subs ?? {};
15105
+ await copyTextFile(item.src, item.dest, subs);
15106
+ logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14778
15107
  }
14779
- else if (changed) {
14780
- logger.log(`Updated ${path.relative(cwd, giPath)}`);
15108
+ else if (decision === 'example') {
15109
+ const destEx = `${item.dest}.example`;
15110
+ const subs = item.subs ?? {};
15111
+ await copyTextFile(item.src, destEx, subs);
15112
+ logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
15113
+ }
15114
+ else {
15115
+ logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14781
15116
  }
14782
15117
  }
14783
- finally {
14784
- rl.close();
15118
+ // Ensure .gitignore includes local config patterns.
15119
+ const giPath = path.join(destRoot, '.gitignore');
15120
+ const { created, changed } = await ensureLines(giPath, [
15121
+ 'getdotenv.config.local.*',
15122
+ '*.local',
15123
+ ]);
15124
+ if (created) {
15125
+ logger.log(`Created ${path.relative(cwd, giPath)}`);
14785
15126
  }
14786
- });
15127
+ else if (changed) {
15128
+ logger.log(`Updated ${path.relative(cwd, giPath)}`);
15129
+ }
15130
+ }
15131
+ finally {
15132
+ rl.close();
15133
+ }
15134
+ });
15135
+ }
15136
+
15137
+ /**
15138
+ * Attach options/arguments for the init plugin mount.
15139
+ *
15140
+ * @param cli - The `init` command mount.
15141
+ *
15142
+ * @internal
15143
+ */
15144
+ function attachInitOptions(cli) {
15145
+ return (cli
15146
+ // Description is owned by the plugin index (src/plugins/init/index.ts).
15147
+ .argument('[dest]', 'destination path (default: ./)', '.')
15148
+ .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
15149
+ .option('--with-local', 'include .local config variant')
15150
+ .option('--dynamic', 'include dynamic examples (JS/TS configs)')
15151
+ .option('--cli-name <string>', 'CLI name for skeleton and tokens')
15152
+ .option('--force', 'overwrite all existing files')
15153
+ .option('--yes', 'skip all collisions (no overwrite)'));
15154
+ }
15155
+
15156
+ /**
15157
+ * @packageDocumentation
15158
+ * Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
15159
+ * host‑based CLI skeleton with collision handling and CI‑safe defaults.
15160
+ */
15161
+ /**
15162
+ * Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
15163
+ * Supports collision detection, interactive prompts, and CI bypass.
15164
+ */
15165
+ const initPlugin = () => definePlugin({
15166
+ ns: 'init',
15167
+ setup(cli) {
15168
+ cli.description('Scaffold getdotenv config files and a host-based CLI skeleton.');
15169
+ const initCmd = attachInitOptions(cli);
15170
+ attachInitDefaultAction(initCmd);
14787
15171
  return undefined;
14788
15172
  },
14789
15173
  });