@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/index.mjs CHANGED
@@ -170,6 +170,10 @@ function defaultsDeep(...layers) {
170
170
  /**
171
171
  * Serialize a dotenv record to a file with minimal quoting (multiline values are quoted).
172
172
  * Future-proofs for ordering/sorting changes (currently insertion order).
173
+ *
174
+ * @param filename - Destination dotenv file path.
175
+ * @param data - Env-like map of values to write (values may be `undefined`).
176
+ * @returns A `Promise\<void\>` which resolves when the file has been written.
173
177
  */
174
178
  async function writeDotenvFile(filename, data) {
175
179
  // Serialize: key=value with quotes only for multiline values.
@@ -405,14 +409,20 @@ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Numbe
405
409
  }
406
410
  };
407
411
  /**
408
- * Load a module default export from a JS/TS file with robust fallbacks:
409
- * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
410
- * 1) try direct import (if a TS loader is active),
411
- * 2) esbuild bundle to a temp ESM file,
412
- * 3) typescript.transpileModule fallback for simple modules.
412
+ * Load a module default export from a JS/TS file with robust fallbacks.
413
+ *
414
+ * Behavior by extension:
415
+ *
416
+ * - `.js`/`.mjs`/`.cjs`: direct dynamic import.
417
+ * - `.ts`/`.mts`/`.cts`/`.tsx`:
418
+ * - try direct dynamic import (when a TS loader is active),
419
+ * - else compile via `esbuild` to a cached `.mjs` file and import,
420
+ * - else fallback to `typescript.transpileModule` for simple modules.
413
421
  *
414
- * @param absPath - absolute path to source file
415
- * @param cacheDirName - cache subfolder under .tsbuild
422
+ * @typeParam T - Type of the expected default export.
423
+ * @param absPath - Absolute path to the source file.
424
+ * @param cacheDirName - Cache subfolder under `.tsbuild/`.
425
+ * @returns A `Promise\<T | undefined\>` resolving to the default export (if any).
416
426
  */
417
427
  const loadModuleDefault = async (absPath, cacheDirName) => {
418
428
  const ext = path.extname(absPath).toLowerCase();
@@ -484,6 +494,10 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
484
494
  /**
485
495
  * Omit keys whose runtime value is undefined from a shallow object.
486
496
  * Returns a Partial with non-undefined value types preserved.
497
+ *
498
+ * @typeParam T - Input object shape.
499
+ * @param obj - Object to filter.
500
+ * @returns A shallow copy of `obj` without keys whose value is `undefined`.
487
501
  */
488
502
  function omitUndefined(obj) {
489
503
  const out = {};
@@ -495,6 +509,10 @@ function omitUndefined(obj) {
495
509
  }
496
510
  /**
497
511
  * Specialized helper for env-like maps: drop undefined and return string-only.
512
+ *
513
+ * @typeParam V - Value type for present entries (must extend `string`).
514
+ * @param obj - Env-like record containing `string | undefined` values.
515
+ * @returns A new record containing only the keys with defined values.
498
516
  */
499
517
  function omitUndefinedRecord(obj) {
500
518
  const out = {};
@@ -712,6 +730,10 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
712
730
  * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
713
731
  * - Else if `requiredKeys` is present, check presence (value !== undefined).
714
732
  * - Returns a flat list of issue strings; caller decides warn vs fail.
733
+ *
734
+ * @param finalEnv - Final composed environment to validate.
735
+ * @param sources - Resolved config sources providing `schema` and/or `requiredKeys`.
736
+ * @returns A list of human-readable issue strings (empty when valid).
715
737
  */
716
738
  const validateEnvAgainstSources = (finalEnv, sources) => {
717
739
  const pick = (getter) => {
@@ -771,6 +793,11 @@ const validateEnvAgainstSources = (finalEnv, sources) => {
771
793
  * Apply a dynamic map to the target progressively.
772
794
  * - Functions receive (target, env) and may return string | undefined.
773
795
  * - Literals are assigned directly (including undefined).
796
+ *
797
+ * @param target - Mutable target environment to assign into.
798
+ * @param map - Dynamic map to apply (functions and/or literal values).
799
+ * @param env - Selected environment name (if any) passed through to dynamic functions.
800
+ * @returns Nothing.
774
801
  */
775
802
  function applyDynamicMap(target, map, env) {
776
803
  if (!map)
@@ -790,6 +817,12 @@ function applyDynamicMap(target, map, env) {
790
817
  * Error behavior:
791
818
  * - On failure to load/compile/evaluate the module, throws a unified message:
792
819
  * "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
820
+ *
821
+ * @param target - Mutable target environment to assign into.
822
+ * @param absPath - Absolute path to the dynamic module file.
823
+ * @param env - Selected environment name (if any).
824
+ * @param cacheDirName - Cache subdirectory under `.tsbuild/` for compiled artifacts.
825
+ * @returns A `Promise\<void\>` which resolves after the module (if present) has been applied.
793
826
  */
794
827
  async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
795
828
  if (!(await fs.exists(absPath)))
@@ -896,6 +929,18 @@ const DEFAULT_PATTERNS = [
896
929
  const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
897
930
  const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
898
931
  const MASK = '[redacted]';
932
+ /**
933
+ * Redact a single displayed value according to key/patterns.
934
+ * Returns the original value when redaction is disabled or key is not matched.
935
+ */
936
+ const redactDisplay = (key, value, opts) => {
937
+ if (!value)
938
+ return value;
939
+ if (!opts?.redact)
940
+ return value;
941
+ const regs = compile(opts.redactPatterns);
942
+ return shouldRedactKey(key, regs) ? MASK : value;
943
+ };
899
944
  /**
900
945
  * Produce a shallow redacted copy of an env-like object for display.
901
946
  */
@@ -1973,6 +2018,10 @@ function renderOptionGroups(cmd) {
1973
2018
  /**
1974
2019
  * Compose root/parent help output by inserting grouped sections between
1975
2020
  * Options and Commands, ensuring a trailing blank line.
2021
+ *
2022
+ * @param base - Base help text produced by Commander.
2023
+ * @param cmd - Command instance whose grouped options should be rendered.
2024
+ * @returns The modified help text with grouped blocks inserted.
1976
2025
  */
1977
2026
  function buildHelpInformation(base, cmd) {
1978
2027
  const groups = renderOptionGroups(cmd);
@@ -2485,6 +2534,10 @@ const toHelpConfig = (merged, plugins) => {
2485
2534
  /**
2486
2535
  * Compose a child-process env overlay from dotenv and the merged CLI options bag.
2487
2536
  * Returns a shallow object including getDotenvCliOptions when serializable.
2537
+ *
2538
+ * @param merged - Resolved CLI options bag (or a JSON-serializable subset).
2539
+ * @param dotenv - Composed dotenv variables for the current invocation.
2540
+ * @returns A string-only env overlay suitable for child process spawning.
2488
2541
  */
2489
2542
  function composeNestedEnv(merged, dotenv) {
2490
2543
  const out = {};
@@ -2507,6 +2560,7 @@ function composeNestedEnv(merged, dotenv) {
2507
2560
  * Strip one layer of symmetric outer quotes (single or double) from a string.
2508
2561
  *
2509
2562
  * @param s - Input string.
2563
+ * @returns `s` without one symmetric outer quote pair (when present).
2510
2564
  */
2511
2565
  const stripOne = (s) => {
2512
2566
  if (s.length < 2)
@@ -2519,6 +2573,9 @@ const stripOne = (s) => {
2519
2573
  /**
2520
2574
  * Preserve argv array for Node -e/--eval payloads under shell-off and
2521
2575
  * peel one symmetric outer quote layer from the code argument.
2576
+ *
2577
+ * @param args - Argument vector intended for direct execution (shell-off).
2578
+ * @returns Either the original `args` or a modified copy with a normalized eval payload.
2522
2579
  */
2523
2580
  function maybePreserveNodeEvalArgv(args) {
2524
2581
  if (args.length >= 3) {
@@ -2991,23 +3048,57 @@ function applyRootVisibility(program, visibility) {
2991
3048
  * Apply resolved AWS context to `process.env` and `ctx.plugins`.
2992
3049
  * Centralizes logic shared between the plugin action and `afterResolve` hook.
2993
3050
  *
3051
+ * @param out - Resolved AWS context to apply.
3052
+ * @param ctx - Host context to publish non-sensitive metadata into.
2994
3053
  * @param setProcessEnv - Whether to write credentials/region to `process.env` (default true).
3054
+ * @returns Nothing.
2995
3055
  */
2996
3056
  function applyAwsContext(out, ctx, setProcessEnv = true) {
2997
3057
  const { profile, region, credentials } = out;
2998
3058
  if (setProcessEnv) {
2999
- if (region) {
3000
- process.env.AWS_REGION = region;
3001
- if (!process.env.AWS_DEFAULT_REGION) {
3002
- process.env.AWS_DEFAULT_REGION = region;
3059
+ // Ensure AWS credential sources are mutually exclusive.
3060
+ // The AWS SDK warns (and may change precedence in future) when both
3061
+ // AWS_PROFILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are set.
3062
+ const clear = (keys) => {
3063
+ for (const k of keys) {
3064
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
3065
+ delete process.env[k];
3003
3066
  }
3004
- }
3067
+ };
3068
+ const clearProfileVars = () => {
3069
+ clear(['AWS_PROFILE', 'AWS_DEFAULT_PROFILE', 'AWS_SDK_LOAD_CONFIG']);
3070
+ };
3071
+ const clearStaticCreds = () => {
3072
+ clear([
3073
+ 'AWS_ACCESS_KEY_ID',
3074
+ 'AWS_SECRET_ACCESS_KEY',
3075
+ 'AWS_SESSION_TOKEN',
3076
+ ]);
3077
+ };
3078
+ // Mode A: exported/static credentials (clear profile vars)
3005
3079
  if (credentials) {
3080
+ clearProfileVars();
3006
3081
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
3007
3082
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
3008
3083
  if (credentials.sessionToken !== undefined) {
3009
3084
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
3010
3085
  }
3086
+ else {
3087
+ delete process.env.AWS_SESSION_TOKEN;
3088
+ }
3089
+ }
3090
+ else if (profile) {
3091
+ // Mode B: profile-based (SSO) credentials (clear static creds)
3092
+ clearStaticCreds();
3093
+ process.env.AWS_PROFILE = profile;
3094
+ process.env.AWS_DEFAULT_PROFILE = profile;
3095
+ process.env.AWS_SDK_LOAD_CONFIG = '1';
3096
+ }
3097
+ if (region) {
3098
+ process.env.AWS_REGION = region;
3099
+ if (!process.env.AWS_DEFAULT_REGION) {
3100
+ process.env.AWS_DEFAULT_REGION = region;
3101
+ }
3011
3102
  }
3012
3103
  }
3013
3104
  // Always publish minimal, non-sensitive metadata
@@ -3018,7 +3109,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
3018
3109
  };
3019
3110
  }
3020
3111
 
3021
- const DEFAULT_TIMEOUT_MS = 15_000;
3112
+ const AWS_CLI_TIMEOUT_MS = 15_000;
3022
3113
  const trim = (s) => (typeof s === 'string' ? s.trim() : '');
3023
3114
  const unquote = (s) => s.length >= 2 &&
3024
3115
  ((s.startsWith('"') && s.endsWith('"')) ||
@@ -3027,6 +3118,9 @@ const unquote = (s) => s.length >= 2 &&
3027
3118
  : s;
3028
3119
  /**
3029
3120
  * Parse AWS credentials from JSON output (AWS CLI v2 export-credentials).
3121
+ *
3122
+ * @param txt - Raw stdout text from the AWS CLI.
3123
+ * @returns Parsed credentials, or `undefined` when the input is not recognized.
3030
3124
  */
3031
3125
  const parseExportCredentialsJson = (txt) => {
3032
3126
  try {
@@ -3050,6 +3144,10 @@ const parseExportCredentialsJson = (txt) => {
3050
3144
  /**
3051
3145
  * Parse AWS credentials from environment-export output (shell-agnostic).
3052
3146
  * Supports POSIX `export KEY=VAL` and PowerShell `$Env:KEY=VAL`.
3147
+ * Also supports AWS CLI `windows-cmd` (`set KEY=VAL`) and `env-no-export` (`KEY=VAL`).
3148
+ *
3149
+ * @param txt - Raw stdout text from the AWS CLI.
3150
+ * @returns Parsed credentials, or `undefined` when the input is not recognized.
3053
3151
  */
3054
3152
  const parseExportCredentialsEnv = (txt) => {
3055
3153
  const lines = txt.split(/\r?\n/);
@@ -3060,12 +3158,17 @@ const parseExportCredentialsEnv = (txt) => {
3060
3158
  const line = raw.trim();
3061
3159
  if (!line)
3062
3160
  continue;
3063
- // POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
3161
+ // POSIX: export AWS_ACCESS_KEY_ID=..., ...
3064
3162
  let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3065
- if (!m) {
3066
- // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
3067
- m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3068
- }
3163
+ // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
3164
+ if (!m)
3165
+ m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
3166
+ // Windows cmd: set AWS_ACCESS_KEY_ID=..., etc.
3167
+ if (!m)
3168
+ m = /^(?:set)\s+([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
3169
+ // env-no-export: AWS_ACCESS_KEY_ID=..., etc.
3170
+ if (!m)
3171
+ m = /^([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3069
3172
  if (!m)
3070
3173
  continue;
3071
3174
  const k = m[1];
@@ -3090,7 +3193,7 @@ const parseExportCredentialsEnv = (txt) => {
3090
3193
  };
3091
3194
  return undefined;
3092
3195
  };
3093
- const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
3196
+ const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
3094
3197
  const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
3095
3198
  env: process.env,
3096
3199
  timeoutMs,
@@ -3105,36 +3208,50 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
3105
3208
  }
3106
3209
  return undefined;
3107
3210
  };
3108
- const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
3109
- // Try JSON format first (AWS CLI v2)
3110
- const rJson = await runCommandResult([
3111
- 'aws',
3112
- 'configure',
3113
- 'export-credentials',
3114
- '--profile',
3115
- profile,
3116
- '--format',
3117
- 'json',
3118
- ], false, { env: process.env, timeoutMs });
3119
- if (rJson.exitCode === 0) {
3120
- const creds = parseExportCredentialsJson(rJson.stdout);
3121
- if (creds)
3122
- return creds;
3123
- }
3124
- // Fallback: env lines
3125
- const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
3126
- if (rEnv.exitCode === 0) {
3127
- const creds = parseExportCredentialsEnv(rEnv.stdout);
3211
+ const exportCredentials = async (profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
3212
+ const tryExport = async (format) => {
3213
+ const argv = [
3214
+ 'aws',
3215
+ 'configure',
3216
+ 'export-credentials',
3217
+ '--profile',
3218
+ profile,
3219
+ ...(format ? ['--format', format] : []),
3220
+ ];
3221
+ const r = await runCommandResult(argv, false, {
3222
+ env: process.env,
3223
+ timeoutMs,
3224
+ });
3225
+ if (r.exitCode !== 0)
3226
+ return undefined;
3227
+ const out = trim(r.stdout);
3228
+ if (!out)
3229
+ return undefined;
3230
+ // Some formats produce JSON ("process"), some produce shell-ish env lines.
3231
+ return parseExportCredentialsJson(out) ?? parseExportCredentialsEnv(out);
3232
+ };
3233
+ // Prefer the default/JSON "process" format first; then fall back to shell env outputs.
3234
+ // Note: AWS CLI v2 supports: process | env | env-no-export | powershell | windows-cmd
3235
+ const formats = [
3236
+ 'process',
3237
+ ...(process.platform === 'win32'
3238
+ ? ['powershell', 'windows-cmd', 'env', 'env-no-export']
3239
+ : ['env', 'env-no-export']),
3240
+ ];
3241
+ for (const f of formats) {
3242
+ const creds = await tryExport(f);
3128
3243
  if (creds)
3129
3244
  return creds;
3130
3245
  }
3131
- return undefined;
3246
+ // Final fallback: no --format (AWS CLI default output)
3247
+ return tryExport(undefined);
3132
3248
  };
3133
3249
  /**
3134
3250
  * Resolve AWS context (profile, region, credentials) using configuration and environment.
3135
3251
  * Applies strategy (cli-export vs none) and handling for SSO login-on-demand.
3136
3252
  *
3137
3253
  * @param options - Context options including current dotenv and plugin config.
3254
+ * @returns A `Promise\<AwsContext\>` containing any resolved profile, region, and credentials.
3138
3255
  */
3139
3256
  const resolveAwsContext = async ({ dotenv, cfg, }) => {
3140
3257
  const profileKey = cfg.profileKey ?? 'AWS_LOCAL_PROFILE';
@@ -3159,31 +3276,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3159
3276
  out.region = region;
3160
3277
  return out;
3161
3278
  }
3162
- // Env-first credentials.
3163
3279
  let credentials;
3164
- const envId = trim(process.env.AWS_ACCESS_KEY_ID);
3165
- const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
3166
- const envToken = trim(process.env.AWS_SESSION_TOKEN);
3167
- if (envId && envSecret) {
3168
- credentials = {
3169
- accessKeyId: envId,
3170
- secretAccessKey: envSecret,
3171
- ...(envToken ? { sessionToken: envToken } : {}),
3172
- };
3173
- }
3174
- else if (profile) {
3280
+ // Profile wins over ambient env creds when present (from flags/config/dotenv).
3281
+ if (profile) {
3175
3282
  // Try export-credentials
3176
3283
  credentials = await exportCredentials(profile);
3177
3284
  // On failure, detect SSO and optionally login then retry
3178
3285
  if (!credentials) {
3179
3286
  const ssoSession = await getAwsConfigure('sso_session', profile);
3180
- const looksSSO = typeof ssoSession === 'string' && ssoSession.length > 0;
3287
+ // Legacy SSO profiles use sso_start_url/sso_region rather than sso_session.
3288
+ const ssoStartUrl = await getAwsConfigure('sso_start_url', profile);
3289
+ const looksSSO = (typeof ssoSession === 'string' && ssoSession.length > 0) ||
3290
+ (typeof ssoStartUrl === 'string' && ssoStartUrl.length > 0);
3181
3291
  if (looksSSO && cfg.loginOnDemand) {
3182
- // Best-effort login, then retry export once.
3183
- await runCommandResult(['aws', 'sso', 'login', '--profile', profile], false, {
3292
+ // Interactive login (no timeout by default), then retry export once.
3293
+ const exit = await runCommand(['aws', 'sso', 'login', '--profile', profile], false, {
3184
3294
  env: process.env,
3185
- timeoutMs: DEFAULT_TIMEOUT_MS,
3295
+ stdio: 'inherit',
3186
3296
  });
3297
+ if (exit !== 0) {
3298
+ throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
3299
+ }
3187
3300
  credentials = await exportCredentials(profile);
3188
3301
  }
3189
3302
  }
@@ -3201,6 +3314,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3201
3314
  }
3202
3315
  }
3203
3316
  }
3317
+ else {
3318
+ // Env-first credentials when no profile is present.
3319
+ const envId = trim(process.env.AWS_ACCESS_KEY_ID);
3320
+ const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
3321
+ const envToken = trim(process.env.AWS_SESSION_TOKEN);
3322
+ if (envId && envSecret) {
3323
+ credentials = {
3324
+ accessKeyId: envId,
3325
+ secretAccessKey: envSecret,
3326
+ ...(envToken ? { sessionToken: envToken } : {}),
3327
+ };
3328
+ }
3329
+ }
3204
3330
  // Final region resolution
3205
3331
  if (!region && profile)
3206
3332
  region = await getAwsConfigure('region', profile);
@@ -3216,10 +3342,213 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3216
3342
  return out;
3217
3343
  };
3218
3344
 
3345
+ /**
3346
+ * Create the AWS plugin `afterResolve` hook.
3347
+ *
3348
+ * This runs once per invocation after the host resolves dotenv context.
3349
+ *
3350
+ * @param plugin - The AWS plugin instance.
3351
+ * @returns An `afterResolve` hook function suitable for assigning to `plugin.afterResolve`.
3352
+ *
3353
+ * @internal
3354
+ */
3355
+ function attachAwsAfterResolveHook(plugin) {
3356
+ return async (cli, ctx) => {
3357
+ const cfg = plugin.readConfig(cli);
3358
+ const out = await resolveAwsContext({
3359
+ dotenv: ctx.dotenv,
3360
+ cfg,
3361
+ });
3362
+ applyAwsContext(out, ctx, true);
3363
+ // Optional: low-noise breadcrumb for diagnostics
3364
+ if (process.env.GETDOTENV_DEBUG) {
3365
+ try {
3366
+ const msg = JSON.stringify({
3367
+ profile: out.profile,
3368
+ region: out.region,
3369
+ hasCreds: Boolean(out.credentials),
3370
+ });
3371
+ process.stderr.write(`[aws] afterResolve ${msg}\n`);
3372
+ }
3373
+ catch {
3374
+ /* ignore */
3375
+ }
3376
+ }
3377
+ };
3378
+ }
3379
+
3380
+ /** @internal */
3381
+ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
3382
+ /**
3383
+ * Create an AWS plugin config overlay from Commander-parsed option values.
3384
+ *
3385
+ * This preserves tri-state intent:
3386
+ * - If a flag was not provided, it should not overwrite config-derived defaults.
3387
+ * - If `--no-…` was provided, it must explicitly force the boolean false.
3388
+ *
3389
+ * @param opts - Commander option values for the current invocation.
3390
+ * @returns A partial AWS plugin config object containing only explicit overrides.
3391
+ *
3392
+ * @internal
3393
+ */
3394
+ function awsConfigOverridesFromCommandOpts(opts) {
3395
+ const o = isRecord(opts) ? opts : {};
3396
+ const overlay = {};
3397
+ // Map boolean toggles (respect explicit --no-*)
3398
+ if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand')) {
3399
+ overlay.loginOnDemand = Boolean(o.loginOnDemand);
3400
+ }
3401
+ // Strings/enums
3402
+ if (typeof o.profile === 'string')
3403
+ overlay.profile = o.profile;
3404
+ if (typeof o.region === 'string')
3405
+ overlay.region = o.region;
3406
+ if (typeof o.defaultRegion === 'string')
3407
+ overlay.defaultRegion = o.defaultRegion;
3408
+ if (o.strategy === 'cli-export' || o.strategy === 'none') {
3409
+ overlay.strategy = o.strategy;
3410
+ }
3411
+ // Advanced key overrides
3412
+ if (typeof o.profileKey === 'string')
3413
+ overlay.profileKey = o.profileKey;
3414
+ if (typeof o.profileFallbackKey === 'string') {
3415
+ overlay.profileFallbackKey = o.profileFallbackKey;
3416
+ }
3417
+ if (typeof o.regionKey === 'string')
3418
+ overlay.regionKey = o.regionKey;
3419
+ return overlay;
3420
+ }
3421
+
3422
+ /**
3423
+ * Attach the default action for the AWS plugin mount.
3424
+ *
3425
+ * Behavior:
3426
+ * - With args: forwards to AWS CLI (`aws <args...>`) under the established session.
3427
+ * - Without args: session-only establishment (no forward).
3428
+ *
3429
+ * @param cli - The `aws` command mount.
3430
+ * @param plugin - The AWS plugin instance.
3431
+ *
3432
+ * @internal
3433
+ */
3434
+ function attachAwsDefaultAction(cli, plugin, awsCmd) {
3435
+ awsCmd.action(async (args, opts, thisCommand) => {
3436
+ // Access merged root CLI options (installed by root hooks).
3437
+ const bag = readMergedOptions(thisCommand);
3438
+ const capture = shouldCapture(bag.capture);
3439
+ const underTests = process.env.GETDOTENV_TEST === '1' ||
3440
+ typeof process.env.VITEST_WORKER_ID === 'string';
3441
+ // Build overlay cfg from subcommand flags layered over discovered config.
3442
+ const ctx = cli.getCtx();
3443
+ const cfgBase = plugin.readConfig(cli);
3444
+ const cfg = {
3445
+ ...cfgBase,
3446
+ ...awsConfigOverridesFromCommandOpts(opts),
3447
+ };
3448
+ // Resolve current context with overrides
3449
+ const out = await resolveAwsContext({
3450
+ dotenv: ctx.dotenv,
3451
+ cfg,
3452
+ });
3453
+ // Publish env/context
3454
+ applyAwsContext(out, ctx, true);
3455
+ // Forward when positional args are present; otherwise session-only.
3456
+ if (args.length > 0) {
3457
+ const argv = ['aws', ...args];
3458
+ const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
3459
+ const exit = await runCommand(argv, shellSetting, {
3460
+ env: buildSpawnEnv(process.env, ctx.dotenv),
3461
+ stdio: capture ? 'pipe' : 'inherit',
3462
+ });
3463
+ // Deterministic termination (suppressed under tests)
3464
+ if (!underTests) {
3465
+ process.exit(typeof exit === 'number' ? exit : 0);
3466
+ }
3467
+ return;
3468
+ }
3469
+ // Session only: low-noise breadcrumb under debug
3470
+ if (process.env.GETDOTENV_DEBUG) {
3471
+ try {
3472
+ const msg = JSON.stringify({
3473
+ profile: out.profile,
3474
+ region: out.region,
3475
+ hasCreds: Boolean(out.credentials),
3476
+ });
3477
+ process.stderr.write(`[aws] session established ${msg}\n`);
3478
+ }
3479
+ catch {
3480
+ /* ignore */
3481
+ }
3482
+ }
3483
+ if (!underTests)
3484
+ process.exit(0);
3485
+ });
3486
+ }
3487
+
3488
+ /**
3489
+ * Attach options/arguments for the AWS plugin mount.
3490
+ *
3491
+ * @param cli - The `aws` command mount.
3492
+ * @param plugin - The AWS plugin instance (for dynamic option descriptions).
3493
+ *
3494
+ * @internal
3495
+ */
3496
+ function attachAwsOptions(cli, plugin) {
3497
+ return (cli
3498
+ // Description is owned by the plugin index (src/plugins/aws/index.ts).
3499
+ .enablePositionalOptions()
3500
+ .passThroughOptions()
3501
+ .allowUnknownOption(true)
3502
+ // Boolean toggles with dynamic help labels (effective defaults)
3503
+ .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
3504
+ .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
3505
+ // Strings / enums
3506
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
3507
+ .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
3508
+ .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
3509
+ .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
3510
+ // Advanced key overrides
3511
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
3512
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
3513
+ .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
3514
+ // Accept any extra operands so Commander does not error when tokens appear after "--".
3515
+ .argument('[args...]'));
3516
+ }
3517
+
3518
+ /**
3519
+ * Attach the AWS plugin `preSubcommand` hook.
3520
+ *
3521
+ * Ensures `aws --profile/--region <child>` applies the AWS session setup before
3522
+ * child subcommand execution.
3523
+ *
3524
+ * @param cli - The `aws` command mount.
3525
+ * @param plugin - The AWS plugin instance.
3526
+ *
3527
+ * @internal
3528
+ */
3529
+ function attachAwsPreSubcommandHook(cli, plugin) {
3530
+ cli.hook('preSubcommand', async (thisCommand) => {
3531
+ // Avoid side effects for help rendering.
3532
+ if (process.argv.includes('-h') || process.argv.includes('--help'))
3533
+ return;
3534
+ const ctx = cli.getCtx();
3535
+ const cfgBase = plugin.readConfig(cli);
3536
+ const cfg = {
3537
+ ...cfgBase,
3538
+ ...awsConfigOverridesFromCommandOpts(thisCommand.opts()),
3539
+ };
3540
+ const out = await resolveAwsContext({
3541
+ dotenv: ctx.dotenv,
3542
+ cfg,
3543
+ });
3544
+ applyAwsContext(out, ctx, true);
3545
+ });
3546
+ }
3547
+
3219
3548
  /**
3220
3549
  * Zod schema for AWS plugin configuration.
3221
3550
  */
3222
- const AwsPluginConfigSchema = z$2.object({
3551
+ const awsPluginConfigSchema = z$2.object({
3223
3552
  profile: z$2.string().optional(),
3224
3553
  region: z$2.string().optional(),
3225
3554
  defaultRegion: z$2.string().optional(),
@@ -3243,129 +3572,16 @@ const AwsPluginConfigSchema = z$2.object({
3243
3572
  const awsPlugin = () => {
3244
3573
  const plugin = definePlugin({
3245
3574
  ns: 'aws',
3246
- configSchema: AwsPluginConfigSchema,
3247
- setup: (cli) => {
3248
- // Mount: aws (provided)
3249
- cli
3250
- .description('Establish an AWS session and optionally forward to the AWS CLI')
3251
- .enablePositionalOptions()
3252
- .passThroughOptions()
3253
- .allowUnknownOption(true)
3254
- // Boolean toggles with dynamic help labels (effective defaults)
3255
- .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
3256
- .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
3257
- // Strings / enums
3258
- .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
3259
- .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
3260
- .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
3261
- .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
3262
- // Advanced key overrides
3263
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
3264
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
3265
- .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
3266
- // Accept any extra operands so Commander does not error when tokens appear after "--".
3267
- .argument('[args...]')
3268
- .action(async (args, opts, thisCommand) => {
3269
- const pluginInst = plugin;
3270
- // Access merged root CLI options (installed by passOptions())
3271
- const bag = readMergedOptions(thisCommand);
3272
- const capture = shouldCapture(bag.capture);
3273
- const underTests = process.env.GETDOTENV_TEST === '1' ||
3274
- typeof process.env.VITEST_WORKER_ID === 'string';
3275
- // Build overlay cfg from subcommand flags layered over discovered config.
3276
- const ctx = cli.getCtx();
3277
- const cfgBase = pluginInst.readConfig(cli);
3278
- const o = opts;
3279
- const overlay = {};
3280
- // Map boolean toggles (respect explicit --no-*)
3281
- if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
3282
- overlay.loginOnDemand = Boolean(o.loginOnDemand);
3283
- // Strings/enums
3284
- if (typeof o.profile === 'string')
3285
- overlay.profile = o.profile;
3286
- if (typeof o.region === 'string')
3287
- overlay.region = o.region;
3288
- if (typeof o.defaultRegion === 'string')
3289
- overlay.defaultRegion = o.defaultRegion;
3290
- if (typeof o.strategy === 'string')
3291
- overlay.strategy = o.strategy;
3292
- // Advanced key overrides
3293
- if (typeof o.profileKey === 'string')
3294
- overlay.profileKey = o.profileKey;
3295
- if (typeof o.profileFallbackKey === 'string')
3296
- overlay.profileFallbackKey = o.profileFallbackKey;
3297
- if (typeof o.regionKey === 'string')
3298
- overlay.regionKey = o.regionKey;
3299
- const cfg = {
3300
- ...cfgBase,
3301
- ...overlay,
3302
- };
3303
- // Resolve current context with overrides
3304
- const out = await resolveAwsContext({
3305
- dotenv: ctx.dotenv,
3306
- cfg,
3307
- });
3308
- // Publish env/context
3309
- applyAwsContext(out, ctx, true);
3310
- // Forward when positional args are present; otherwise session-only.
3311
- if (Array.isArray(args) && args.length > 0) {
3312
- const argv = ['aws', ...args];
3313
- const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
3314
- const exit = await runCommand(argv, shellSetting, {
3315
- env: buildSpawnEnv(process.env, ctx.dotenv),
3316
- stdio: capture ? 'pipe' : 'inherit',
3317
- });
3318
- // Deterministic termination (suppressed under tests)
3319
- if (!underTests) {
3320
- process.exit(typeof exit === 'number' ? exit : 0);
3321
- }
3322
- return;
3323
- }
3324
- else {
3325
- // Session only: low-noise breadcrumb under debug
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] session established ${msg}\n`);
3334
- }
3335
- catch {
3336
- /* ignore */
3337
- }
3338
- }
3339
- if (!underTests)
3340
- process.exit(0);
3341
- return;
3342
- }
3343
- });
3575
+ configSchema: awsPluginConfigSchema,
3576
+ setup(cli) {
3577
+ cli.description('Establish an AWS session and optionally forward to the AWS CLI');
3578
+ const awsCmd = attachAwsOptions(cli, plugin);
3579
+ attachAwsPreSubcommandHook(cli, plugin);
3580
+ attachAwsDefaultAction(cli, plugin, awsCmd);
3344
3581
  return undefined;
3345
3582
  },
3346
- afterResolve: async (_cli, ctx) => {
3347
- const cfg = plugin.readConfig(_cli);
3348
- const out = await resolveAwsContext({
3349
- dotenv: ctx.dotenv,
3350
- cfg,
3351
- });
3352
- applyAwsContext(out, ctx, true);
3353
- // Optional: low-noise breadcrumb for diagnostics
3354
- if (process.env.GETDOTENV_DEBUG) {
3355
- try {
3356
- const msg = JSON.stringify({
3357
- profile: out.profile,
3358
- region: out.region,
3359
- hasCreds: Boolean(out.credentials),
3360
- });
3361
- process.stderr.write(`[aws] afterResolve ${msg}\n`);
3362
- }
3363
- catch {
3364
- /* ignore */
3365
- }
3366
- }
3367
- },
3368
3583
  });
3584
+ plugin.afterResolve = attachAwsAfterResolveHook(plugin);
3369
3585
  return plugin;
3370
3586
  };
3371
3587
 
@@ -13827,21 +14043,78 @@ class GetCallerIdentityCommand extends Command
13827
14043
  }
13828
14044
 
13829
14045
  /**
13830
- * AWS Whoami plugin: prints the current AWS caller identity (account, arn, userid).
13831
- * Intended to be mounted under the `aws` plugin.
14046
+ * Attach the default action for the `aws whoami` command.
14047
+ *
14048
+ * This behavior executes only when `aws whoami` is invoked without a subcommand.
14049
+ *
14050
+ * @param cli - The `whoami` command mount.
14051
+ * @returns Nothing.
14052
+ */
14053
+ function attachWhoamiDefaultAction(cli) {
14054
+ cli.action(async () => {
14055
+ // The AWS SDK default providers will read credentials from process.env,
14056
+ // which the aws parent has already populated.
14057
+ const client = new STSClient$1();
14058
+ const result = await client.send(new GetCallerIdentityCommand());
14059
+ console.log(JSON.stringify(result, null, 2));
14060
+ });
14061
+ }
14062
+
14063
+ /**
14064
+ * Attach options/arguments for the `aws whoami` plugin mount.
14065
+ *
14066
+ * This subcommand currently takes no flags/args; this module exists to keep the
14067
+ * wiring layout consistent across shipped plugins (options vs actions).
14068
+ *
14069
+ * Note: the plugin description is owned by `src/plugins/aws/whoami/index.ts` and
14070
+ * must not be set here.
14071
+ *
14072
+ * @param cli - The `whoami` command mount under `aws`.
14073
+ * @returns The same `cli` instance for chaining.
14074
+ *
14075
+ * @internal
14076
+ */
14077
+ function attachWhoamiOptions(cli) {
14078
+ return cli;
14079
+ }
14080
+
14081
+ /**
14082
+ * Attach the `really` subcommand under `aws whoami`.
14083
+ *
14084
+ * Reads `SECRET_IDENTITY` from the resolved get-dotenv context (`cli.getCtx().dotenv`).
14085
+ *
14086
+ * @param cli - The `whoami` command mount.
14087
+ * @returns Nothing.
14088
+ */
14089
+ function attachWhoamiReallyAction(cli) {
14090
+ const really = cli
14091
+ .ns('really')
14092
+ .description('Print SECRET_IDENTITY from the resolved dotenv context');
14093
+ really.action(() => {
14094
+ const secretIdentity = really.getCtx().dotenv.SECRET_IDENTITY;
14095
+ console.log(`Your secret identity is ${secretIdentity ?? 'still a secret'}.`);
14096
+ });
14097
+ }
14098
+
14099
+ /**
14100
+ * AWS Whoami plugin factory.
14101
+ *
14102
+ * This plugin demonstrates a “bucket of subcommands” pattern:
14103
+ * - Subcommand behavior is articulated in separate modules as `attach*` helpers.
14104
+ * - Those helpers are not individually composable plugins; they are internal wiring for one plugin instance.
14105
+ *
14106
+ * @returns A plugin instance mounted at `aws whoami`.
13832
14107
  */
13833
14108
  const awsWhoamiPlugin = () => definePlugin({
13834
14109
  ns: 'whoami',
13835
14110
  setup(cli) {
13836
- cli
13837
- .description('Print AWS caller identity (uses parent aws session)')
13838
- .action(async () => {
13839
- // The AWS SDK default providers will read credentials from process.env,
13840
- // which the aws parent has already populated.
13841
- const client = new STSClient$1();
13842
- const result = await client.send(new GetCallerIdentityCommand());
13843
- console.log(JSON.stringify(result, null, 2));
13844
- });
14111
+ cli.description('Print AWS caller identity (uses parent aws session)');
14112
+ // Options/args (none today, but keep layout consistent with other plugins).
14113
+ const whoami = attachWhoamiOptions(cli);
14114
+ // Default behavior: `getdotenv aws whoami`
14115
+ attachWhoamiDefaultAction(whoami);
14116
+ // Subcommand behavior: `getdotenv aws whoami really`
14117
+ attachWhoamiReallyAction(whoami);
13845
14118
  return undefined;
13846
14119
  },
13847
14120
  });
@@ -13949,7 +14222,7 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv,
13949
14222
  /**
13950
14223
  * Attach the default "cmd" subcommand action with contextual typing.
13951
14224
  */
13952
- const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
14225
+ const attachBatchCmdAction = (plugin, cli, batchCmd, pluginOpts, cmd) => {
13953
14226
  cmd.action(async (commandParts, _subOpts, thisCommand) => {
13954
14227
  const mergedBag = readMergedOptions(batchCmd);
13955
14228
  const logger = mergedBag.logger;
@@ -14058,11 +14331,37 @@ const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
14058
14331
  });
14059
14332
  };
14060
14333
 
14334
+ /**
14335
+ * Attach the default `cmd` subcommand under the `batch` command.
14336
+ *
14337
+ * This encapsulates:
14338
+ * - Subcommand construction (`new Command().name('cmd')…`)
14339
+ * - Action wiring
14340
+ * - Mounting as the default subcommand for `batch`
14341
+ *
14342
+ * @param plugin - The batch plugin instance.
14343
+ * @param cli - The batch command mount.
14344
+ * @param batchCmd - The `batch` command (same as `cli` mount).
14345
+ * @param pluginOpts - Batch plugin factory options.
14346
+ *
14347
+ * @internal
14348
+ */
14349
+ const attachBatchCmdSubcommand = (plugin, cli, batchCmd, pluginOpts) => {
14350
+ const cmdSub = new Command$1()
14351
+ .name('cmd')
14352
+ .description('execute command, conflicts with --command option (default subcommand)')
14353
+ .enablePositionalOptions()
14354
+ .passThroughOptions()
14355
+ .argument('[command...]');
14356
+ attachBatchCmdAction(plugin, cli, batchCmd, pluginOpts, cmdSub);
14357
+ batchCmd.addCommand(cmdSub, { isDefault: true });
14358
+ };
14359
+
14061
14360
  /**
14062
14361
  * Attach the parent-level action for the batch plugin.
14063
14362
  * Handles parent flags (e.g. `getdotenv batch -l`) and delegates to the batch executor.
14064
14363
  */
14065
- const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
14364
+ const attachBatchDefaultAction = (plugin, cli, pluginOpts, parent) => {
14066
14365
  parent.action(async function (...args) {
14067
14366
  // Commander Unknown generics: [...unknown[], OptionValues, thisCommand]
14068
14367
  const thisCommand = args[args.length - 1];
@@ -14163,6 +14462,45 @@ const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
14163
14462
  });
14164
14463
  };
14165
14464
 
14465
+ /**
14466
+ * Attach options/arguments for the batch plugin mount.
14467
+ *
14468
+ * Note: the plugin description is owned by `src/plugins/batch/index.ts` and
14469
+ * must not be set here.
14470
+ *
14471
+ * @param plugin - Batch plugin instance (for dynamic option descriptions).
14472
+ * @param cli - The `batch` command mount.
14473
+ * @returns The same `cli` instance for chaining.
14474
+ *
14475
+ * @internal
14476
+ */
14477
+ function attachBatchOptions(plugin, cli) {
14478
+ const GROUP = `plugin:${cli.name()}`;
14479
+ return (cli
14480
+ .enablePositionalOptions()
14481
+ .passThroughOptions()
14482
+ // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
14483
+ .addOption((() => {
14484
+ const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
14485
+ cli.setOptionGroup(opt, GROUP);
14486
+ return opt;
14487
+ })())
14488
+ .addOption((() => {
14489
+ 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 || './')})`);
14490
+ cli.setOptionGroup(opt, GROUP);
14491
+ return opt;
14492
+ })())
14493
+ .addOption((() => {
14494
+ const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
14495
+ cli.setOptionGroup(opt, GROUP);
14496
+ return opt;
14497
+ })())
14498
+ .option('-c, --command <string>', 'command executed according to the base shell resolution')
14499
+ .option('-l, --list', 'list working directories without executing command')
14500
+ .option('-e, --ignore-errors', 'ignore errors and continue with next path')
14501
+ .argument('[command...]'));
14502
+ }
14503
+
14166
14504
  /**
14167
14505
  * Zod schema for a single script entry (string or object).
14168
14506
  */
@@ -14176,7 +14514,7 @@ const ScriptSchema = z$2.union([
14176
14514
  /**
14177
14515
  * Zod schema for batch plugin configuration.
14178
14516
  */
14179
- const BatchConfigSchema = z$2.object({
14517
+ const batchPluginConfigSchema = z$2.object({
14180
14518
  scripts: z$2.record(z$2.string(), ScriptSchema).optional(),
14181
14519
  shell: z$2.union([z$2.string(), z$2.boolean()]).optional(),
14182
14520
  rootPath: z$2.string().optional(),
@@ -14202,45 +14540,15 @@ const batchPlugin = (opts = {}) => {
14202
14540
  ns: 'batch',
14203
14541
  // Host validates this when config-loader is enabled; plugins may also
14204
14542
  // re-validate at action time as a safety belt.
14205
- configSchema: BatchConfigSchema,
14543
+ configSchema: batchPluginConfigSchema,
14206
14544
  setup(cli) {
14207
14545
  const batchCmd = cli; // mount provided by host
14208
- const GROUP = `plugin:${cli.name()}`;
14209
- batchCmd
14210
- .description('Batch command execution across multiple working directories.')
14211
- .enablePositionalOptions()
14212
- .passThroughOptions()
14213
- // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
14214
- .addOption((() => {
14215
- const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
14216
- cli.setOptionGroup(opt, GROUP);
14217
- return opt;
14218
- })())
14219
- .addOption((() => {
14220
- 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 || './')})`);
14221
- cli.setOptionGroup(opt, GROUP);
14222
- return opt;
14223
- })())
14224
- .addOption((() => {
14225
- const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
14226
- cli.setOptionGroup(opt, GROUP);
14227
- return opt;
14228
- })())
14229
- .option('-c, --command <string>', 'command executed according to the base shell resolution')
14230
- .option('-l, --list', 'list working directories without executing command')
14231
- .option('-e, --ignore-errors', 'ignore errors and continue with next path')
14232
- .argument('[command...]');
14233
- // Default subcommand "cmd" with contextual typing for args/opts
14234
- const cmdSub = new Command$1()
14235
- .name('cmd')
14236
- .description('execute command, conflicts with --command option (default subcommand)')
14237
- .enablePositionalOptions()
14238
- .passThroughOptions()
14239
- .argument('[command...]');
14240
- attachDefaultCmdAction$1(plugin, cli, batchCmd, opts, cmdSub);
14241
- batchCmd.addCommand(cmdSub, { isDefault: true });
14242
- // Parent invoker (unified naming)
14243
- attachParentInvoker$1(plugin, cli, opts, batchCmd);
14546
+ batchCmd.description('Batch command execution across multiple working directories.');
14547
+ attachBatchOptions(plugin, batchCmd);
14548
+ // Default subcommand `cmd` (mounted as batch default subcommand)
14549
+ attachBatchCmdSubcommand(plugin, cli, batchCmd, opts);
14550
+ // Default action for the batch command mount (parent flags and positional form)
14551
+ attachBatchDefaultAction(plugin, cli, opts, batchCmd);
14244
14552
  return undefined;
14245
14553
  },
14246
14554
  });
@@ -14334,14 +14642,11 @@ async function runCmdWithContext(cli, merged, command, _opts) {
14334
14642
  }
14335
14643
 
14336
14644
  /**
14337
- * Attach the default "cmd" subcommand action (unified name).
14645
+ * Attach the default "cmd" subcommand action.
14338
14646
  * Mirrors the prior inline implementation in cmd/index.ts.
14339
14647
  */
14340
- const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
14341
- cmd
14342
- .enablePositionalOptions()
14343
- .passThroughOptions()
14344
- .action(async function (...allArgs) {
14648
+ const attachCmdDefaultAction = (cli, cmd, aliasKey) => {
14649
+ cmd.action(async function (...allArgs) {
14345
14650
  // Commander passes: [...positionals, options, thisCommand]
14346
14651
  const thisCommand = allArgs[allArgs.length - 1];
14347
14652
  const commandParts = allArgs[0];
@@ -14374,11 +14679,29 @@ const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
14374
14679
  });
14375
14680
  };
14376
14681
 
14682
+ /**
14683
+ * Attach options/arguments for the cmd plugin mount.
14684
+ *
14685
+ * Note: the plugin description is owned by `src/plugins/cmd/index.ts` and must
14686
+ * not be set here.
14687
+ *
14688
+ * @param cli - The `cmd` command mount.
14689
+ * @returns The same `cli` instance for chaining.
14690
+ *
14691
+ * @internal
14692
+ */
14693
+ function attachCmdOptions(cli) {
14694
+ return cli
14695
+ .enablePositionalOptions()
14696
+ .passThroughOptions()
14697
+ .argument('[command...]');
14698
+ }
14699
+
14377
14700
  /**
14378
14701
  * Install the parent-level invoker (alias) for the cmd plugin.
14379
14702
  * Unifies naming with batch attachParentInvoker; behavior unchanged.
14380
14703
  */
14381
- const attachParentInvoker = (cli, options, _cmd, plugin) => {
14704
+ const attachCmdParentInvoker = (cli, options, plugin) => {
14382
14705
  const dbg = (...args) => {
14383
14706
  if (process.env.GETDOTENV_DEBUG) {
14384
14707
  try {
@@ -14489,7 +14812,7 @@ const attachParentInvoker = (cli, options, _cmd, plugin) => {
14489
14812
  /**
14490
14813
  * Zod schema for cmd plugin configuration.
14491
14814
  */
14492
- const CmdConfigSchema = z$2
14815
+ const cmdPluginConfigSchema = z$2
14493
14816
  .object({
14494
14817
  expand: z$2.boolean().optional(),
14495
14818
  })
@@ -14509,7 +14832,7 @@ const CmdConfigSchema = z$2
14509
14832
  const cmdPlugin = (options = {}) => {
14510
14833
  const plugin = definePlugin({
14511
14834
  ns: 'cmd',
14512
- configSchema: CmdConfigSchema,
14835
+ configSchema: cmdPluginConfigSchema,
14513
14836
  setup(cli) {
14514
14837
  const aliasSpec = typeof options.optionAlias === 'string'
14515
14838
  ? { flags: options.optionAlias}
@@ -14521,14 +14844,13 @@ const cmdPlugin = (options = {}) => {
14521
14844
  };
14522
14845
  const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
14523
14846
  // Mount is the command ('cmd'); attach default action.
14524
- cli
14525
- .description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
14526
- // Accept payload tokens as positional arguments for the default subcommand.
14527
- .argument('[command...]');
14528
- attachDefaultCmdAction(cli, cli, aliasKey);
14847
+ cli.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)');
14848
+ // Options/arguments (positional payload, argv routing) are attached separately.
14849
+ attachCmdOptions(cli);
14850
+ attachCmdDefaultAction(cli, cli, aliasKey);
14529
14851
  // Parent-attached option alias (optional, unified naming).
14530
14852
  if (aliasSpec !== undefined) {
14531
- attachParentInvoker(cli, options, cli, plugin);
14853
+ attachCmdParentInvoker(cli, options, plugin);
14532
14854
  }
14533
14855
  return undefined;
14534
14856
  },
@@ -14537,14 +14859,21 @@ const cmdPlugin = (options = {}) => {
14537
14859
  };
14538
14860
 
14539
14861
  /**
14540
- * Ensure a directory exists.
14862
+ * Ensure a directory exists (parents included).
14863
+ *
14864
+ * @param p - Directory path to create.
14865
+ * @returns A `Promise\<string\>` resolving to the provided `p` value.
14541
14866
  */
14542
14867
  const ensureDir = async (p) => {
14543
14868
  await fs.ensureDir(p);
14544
14869
  return p;
14545
14870
  };
14546
14871
  /**
14547
- * Write text content to a file, ensuring the parent directory exists.
14872
+ * Write UTF-8 text content to a file, ensuring the parent directory exists.
14873
+ *
14874
+ * @param dest - Destination file path.
14875
+ * @param data - File contents to write.
14876
+ * @returns A `Promise\<void\>` which resolves when the file is written.
14548
14877
  */
14549
14878
  const writeFile$1 = async (dest, data) => {
14550
14879
  await ensureDir(path.dirname(dest));
@@ -14556,6 +14885,7 @@ const writeFile$1 = async (dest, data) => {
14556
14885
  * @param src - Source file path.
14557
14886
  * @param dest - Destination file path.
14558
14887
  * @param substitutions - Map of token literals to replacement strings.
14888
+ * @returns A `Promise\<void\>` which resolves when the file has been copied.
14559
14889
  */
14560
14890
  const copyTextFile = async (src, dest, substitutions) => {
14561
14891
  const contents = await fs.readFile(src, 'utf-8');
@@ -14567,6 +14897,10 @@ const copyTextFile = async (src, dest, substitutions) => {
14567
14897
  /**
14568
14898
  * Ensure a set of lines exist (exact match) in a file. Creates the file
14569
14899
  * when missing. Returns whether it was created or changed.
14900
+ *
14901
+ * @param filePath - Target file path to create/update.
14902
+ * @param lines - Lines which must be present (exact string match).
14903
+ * @returns A `Promise\<object\>` describing whether the file was created and/or changed.
14570
14904
  */
14571
14905
  const ensureLines = async (filePath, lines) => {
14572
14906
  const exists = await fs.pathExists(filePath);
@@ -14594,11 +14928,23 @@ const ensureLines = async (filePath, lines) => {
14594
14928
  return { created: false, changed: false };
14595
14929
  };
14596
14930
 
14597
- // Templates root used by the scaffolder
14931
+ /**
14932
+ * Absolute path to the shipped templates directory.
14933
+ *
14934
+ * Used by the init scaffolder to locate files under `templates/` at runtime.
14935
+ *
14936
+ * @remarks
14937
+ * This path is resolved relative to the current working directory. It assumes
14938
+ * the `templates/` folder is present alongside the installed package (or in the
14939
+ * repository when running from source).
14940
+ */
14598
14941
  const TEMPLATES_ROOT = path.resolve('templates');
14599
14942
 
14600
14943
  /**
14601
14944
  * Plan the copy operations for configuration files.
14945
+ *
14946
+ * @param options - Planning options for config scaffolding.
14947
+ * @returns An array of copy operations to perform.
14602
14948
  */
14603
14949
  const planConfigCopies = ({ format, withLocal, destRoot, }) => {
14604
14950
  const copies = [];
@@ -14642,10 +14988,14 @@ const planConfigCopies = ({ format, withLocal, destRoot, }) => {
14642
14988
  };
14643
14989
  /**
14644
14990
  * Plan the copy operations for the CLI skeleton.
14991
+ *
14992
+ * @param options - Planning options for CLI scaffolding.
14993
+ * @returns An array of copy operations to perform.
14645
14994
  */
14646
14995
  const planCliCopies = ({ cliName, destRoot, }) => {
14647
14996
  const subs = { __CLI_NAME__: cliName };
14648
14997
  const base = path.join(destRoot, 'src', 'cli', cliName);
14998
+ const helloBase = path.join(base, 'plugins', 'hello');
14649
14999
  return [
14650
15000
  {
14651
15001
  src: path.join(TEMPLATES_ROOT, 'cli', 'index.ts'),
@@ -14653,8 +15003,28 @@ const planCliCopies = ({ cliName, destRoot, }) => {
14653
15003
  subs,
14654
15004
  },
14655
15005
  {
14656
- src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello.ts'),
14657
- dest: path.join(base, 'plugins', 'hello.ts'),
15006
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'index.ts'),
15007
+ dest: path.join(helloBase, 'index.ts'),
15008
+ subs,
15009
+ },
15010
+ {
15011
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'options.ts'),
15012
+ dest: path.join(helloBase, 'options.ts'),
15013
+ subs,
15014
+ },
15015
+ {
15016
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'defaultAction.ts'),
15017
+ dest: path.join(helloBase, 'defaultAction.ts'),
15018
+ subs,
15019
+ },
15020
+ {
15021
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'strangerAction.ts'),
15022
+ dest: path.join(helloBase, 'strangerAction.ts'),
15023
+ subs,
15024
+ },
15025
+ {
15026
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'types.ts'),
15027
+ dest: path.join(helloBase, 'types.ts'),
14658
15028
  subs,
14659
15029
  },
14660
15030
  ];
@@ -14663,6 +15033,8 @@ const planCliCopies = ({ cliName, destRoot, }) => {
14663
15033
  /**
14664
15034
  * Determine whether the current environment should be treated as non-interactive.
14665
15035
  * CI heuristics include: CI, GITHUB_ACTIONS, BUILDKITE, TEAMCITY_VERSION, TF_BUILD.
15036
+ *
15037
+ * @returns `true` when running in a CI-like environment or when stdin/stdout are not TTYs.
14666
15038
  */
14667
15039
  const isNonInteractive = () => {
14668
15040
  const ciLike = process.env.CI ||
@@ -14675,6 +15047,11 @@ const isNonInteractive = () => {
14675
15047
  /**
14676
15048
  * Prompt the user for a file collision decision.
14677
15049
  * Returns a single-character code representing overwrite/example/skip (or 'all' variants).
15050
+ *
15051
+ * @param filePath - Path of the colliding file (for display).
15052
+ * @param logger - Logger used for user-facing messages.
15053
+ * @param rl - Readline interface used to capture user input.
15054
+ * @returns A single-character decision code.
14678
15055
  */
14679
15056
  const promptDecision = async (filePath, logger, rl) => {
14680
15057
  logger.log(`File exists: ${filePath}\nChoose: [o]verwrite, [e]xample, [s]kip, [O]verwrite All, [E]xample All, [S]kip All`);
@@ -14689,129 +15066,148 @@ const promptDecision = async (filePath, logger, rl) => {
14689
15066
  };
14690
15067
 
14691
15068
  /**
14692
- * @packageDocumentation
14693
- * Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
14694
- * host‑based CLI skeleton with collision handling and CI‑safe defaults.
14695
- */
14696
- /**
14697
- * Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
14698
- * Supports collision detection, interactive prompts, and CI bypass.
15069
+ * Attach the init plugin default action.
15070
+ *
15071
+ * @param cli - The `init` command mount (with args/options attached).
15072
+ *
15073
+ * @internal
14699
15074
  */
14700
- const initPlugin = () => definePlugin({
14701
- ns: 'init',
14702
- setup(cli) {
14703
- cli
14704
- .description('Scaffold getdotenv config files and a host-based CLI skeleton.')
14705
- .argument('[dest]', 'destination path (default: ./)', '.')
14706
- .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
14707
- .option('--with-local', 'include .local config variant')
14708
- .option('--dynamic', 'include dynamic examples (JS/TS configs)')
14709
- .option('--cli-name <string>', 'CLI name for skeleton and tokens')
14710
- .option('--force', 'overwrite all existing files')
14711
- .option('--yes', 'skip all collisions (no overwrite)')
14712
- .action(async (destArg, opts, thisCommand) => {
14713
- // Inherit logger from merged root options (base).
14714
- const bag = readMergedOptions(thisCommand);
14715
- const logger = bag.logger;
14716
- const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
14717
- const cwd = process.cwd();
14718
- const destRoot = path.resolve(cwd, destRel);
14719
- const formatInput = opts.configFormat;
14720
- const formatRaw = typeof formatInput === 'string'
14721
- ? formatInput.toLowerCase()
14722
- : 'json';
14723
- const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw)
14724
- ? formatRaw
14725
- : 'json');
14726
- const withLocal = !!opts.withLocal;
14727
- // dynamic flag reserved for future template variants; present for UX compatibility
14728
- void opts.dynamic;
14729
- // CLI name default: --cli-name | basename(dest) | 'mycli'
14730
- const cliName = (typeof opts.cliName === 'string' && opts.cliName.length > 0
14731
- ? opts.cliName
14732
- : path.basename(destRoot) || 'mycli') || 'mycli';
14733
- // Precedence: --force > --yes > auto-detect(non-interactive => yes)
14734
- const force = !!opts.force;
14735
- const yes = !!opts.yes || (!force && isNonInteractive());
14736
- // Build copy plan
14737
- const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
14738
- const cliCopies = planCliCopies({ cliName, destRoot });
14739
- const copies = [...cfgCopies, ...cliCopies];
14740
- // Interactive state
14741
- let globalDecision;
14742
- const rl = createInterface({ input: stdin, output: stdout });
14743
- try {
14744
- for (const item of copies) {
14745
- const exists = await fs.pathExists(item.dest);
14746
- if (!exists) {
14747
- const subs = item.subs ?? {};
14748
- await copyTextFile(item.src, item.dest, subs);
14749
- logger.log(`Created ${path.relative(cwd, item.dest)}`);
14750
- continue;
14751
- }
14752
- // Collision
14753
- if (force) {
14754
- const subs = item.subs ?? {};
14755
- await copyTextFile(item.src, item.dest, subs);
14756
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14757
- continue;
14758
- }
14759
- if (yes) {
14760
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14761
- continue;
14762
- }
14763
- let decision = globalDecision;
14764
- if (!decision) {
14765
- const a = await promptDecision(item.dest, logger, rl);
14766
- if (a === 'O') {
14767
- globalDecision = 'overwrite';
14768
- decision = 'overwrite';
14769
- }
14770
- else if (a === 'E') {
14771
- globalDecision = 'example';
14772
- decision = 'example';
14773
- }
14774
- else if (a === 'S') {
14775
- globalDecision = 'skip';
14776
- decision = 'skip';
14777
- }
14778
- else {
14779
- decision =
14780
- a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
14781
- }
15075
+ function attachInitDefaultAction(cli) {
15076
+ cli.action(async (destArg, opts, thisCommand) => {
15077
+ // Inherit logger from merged root options (base).
15078
+ const bag = readMergedOptions(thisCommand);
15079
+ const logger = bag.logger;
15080
+ const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
15081
+ const cwd = process.cwd();
15082
+ const destRoot = path.resolve(cwd, destRel);
15083
+ const formatInput = opts['configFormat'];
15084
+ const formatRaw = typeof formatInput === 'string' ? formatInput.toLowerCase() : 'json';
15085
+ const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw) ? formatRaw : 'json');
15086
+ const withLocal = Boolean(opts['withLocal']);
15087
+ // dynamic flag reserved for future template variants; present for UX compatibility
15088
+ void opts['dynamic'];
15089
+ // CLI name default: --cli-name | basename(dest) | 'mycli'
15090
+ const cliNameInput = opts['cliName'];
15091
+ const cliName = (typeof cliNameInput === 'string' && cliNameInput.length > 0
15092
+ ? cliNameInput
15093
+ : path.basename(destRoot) || 'mycli') || 'mycli';
15094
+ // Precedence: --force > --yes > auto-detect(non-interactive => yes)
15095
+ const force = Boolean(opts['force']);
15096
+ const yes = Boolean(opts['yes']) || (!force && isNonInteractive());
15097
+ // Build copy plan
15098
+ const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
15099
+ const cliCopies = planCliCopies({ cliName, destRoot });
15100
+ const copies = [...cfgCopies, ...cliCopies];
15101
+ // Interactive state
15102
+ let globalDecision;
15103
+ const rl = createInterface({ input: stdin, output: stdout });
15104
+ try {
15105
+ for (const item of copies) {
15106
+ const exists = await fs.pathExists(item.dest);
15107
+ if (!exists) {
15108
+ const subs = item.subs ?? {};
15109
+ await copyTextFile(item.src, item.dest, subs);
15110
+ logger.log(`Created ${path.relative(cwd, item.dest)}`);
15111
+ continue;
15112
+ }
15113
+ // Collision
15114
+ if (force) {
15115
+ const subs = item.subs ?? {};
15116
+ await copyTextFile(item.src, item.dest, subs);
15117
+ logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
15118
+ continue;
15119
+ }
15120
+ if (yes) {
15121
+ logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
15122
+ continue;
15123
+ }
15124
+ let decision = globalDecision;
15125
+ if (!decision) {
15126
+ const a = await promptDecision(item.dest, logger, rl);
15127
+ if (a === 'O') {
15128
+ globalDecision = 'overwrite';
15129
+ decision = 'overwrite';
14782
15130
  }
14783
- if (decision === 'overwrite') {
14784
- const subs = item.subs ?? {};
14785
- await copyTextFile(item.src, item.dest, subs);
14786
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
15131
+ else if (a === 'E') {
15132
+ globalDecision = 'example';
15133
+ decision = 'example';
14787
15134
  }
14788
- else if (decision === 'example') {
14789
- const destEx = `${item.dest}.example`;
14790
- const subs = item.subs ?? {};
14791
- await copyTextFile(item.src, destEx, subs);
14792
- logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
15135
+ else if (a === 'S') {
15136
+ globalDecision = 'skip';
15137
+ decision = 'skip';
14793
15138
  }
14794
15139
  else {
14795
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
15140
+ decision = a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
14796
15141
  }
14797
15142
  }
14798
- // Ensure .gitignore includes local config patterns.
14799
- const giPath = path.join(destRoot, '.gitignore');
14800
- const { created, changed } = await ensureLines(giPath, [
14801
- 'getdotenv.config.local.*',
14802
- '*.local',
14803
- ]);
14804
- if (created) {
14805
- logger.log(`Created ${path.relative(cwd, giPath)}`);
15143
+ if (decision === 'overwrite') {
15144
+ const subs = item.subs ?? {};
15145
+ await copyTextFile(item.src, item.dest, subs);
15146
+ logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14806
15147
  }
14807
- else if (changed) {
14808
- logger.log(`Updated ${path.relative(cwd, giPath)}`);
15148
+ else if (decision === 'example') {
15149
+ const destEx = `${item.dest}.example`;
15150
+ const subs = item.subs ?? {};
15151
+ await copyTextFile(item.src, destEx, subs);
15152
+ logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
15153
+ }
15154
+ else {
15155
+ logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14809
15156
  }
14810
15157
  }
14811
- finally {
14812
- rl.close();
15158
+ // Ensure .gitignore includes local config patterns.
15159
+ const giPath = path.join(destRoot, '.gitignore');
15160
+ const { created, changed } = await ensureLines(giPath, [
15161
+ 'getdotenv.config.local.*',
15162
+ '*.local',
15163
+ ]);
15164
+ if (created) {
15165
+ logger.log(`Created ${path.relative(cwd, giPath)}`);
14813
15166
  }
14814
- });
15167
+ else if (changed) {
15168
+ logger.log(`Updated ${path.relative(cwd, giPath)}`);
15169
+ }
15170
+ }
15171
+ finally {
15172
+ rl.close();
15173
+ }
15174
+ });
15175
+ }
15176
+
15177
+ /**
15178
+ * Attach options/arguments for the init plugin mount.
15179
+ *
15180
+ * @param cli - The `init` command mount.
15181
+ *
15182
+ * @internal
15183
+ */
15184
+ function attachInitOptions(cli) {
15185
+ return (cli
15186
+ // Description is owned by the plugin index (src/plugins/init/index.ts).
15187
+ .argument('[dest]', 'destination path (default: ./)', '.')
15188
+ .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
15189
+ .option('--with-local', 'include .local config variant')
15190
+ .option('--dynamic', 'include dynamic examples (JS/TS configs)')
15191
+ .option('--cli-name <string>', 'CLI name for skeleton and tokens')
15192
+ .option('--force', 'overwrite all existing files')
15193
+ .option('--yes', 'skip all collisions (no overwrite)'));
15194
+ }
15195
+
15196
+ /**
15197
+ * @packageDocumentation
15198
+ * Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
15199
+ * host‑based CLI skeleton with collision handling and CI‑safe defaults.
15200
+ */
15201
+ /**
15202
+ * Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
15203
+ * Supports collision detection, interactive prompts, and CI bypass.
15204
+ */
15205
+ const initPlugin = () => definePlugin({
15206
+ ns: 'init',
15207
+ setup(cli) {
15208
+ cli.description('Scaffold getdotenv config files and a host-based CLI skeleton.');
15209
+ const initCmd = attachInitOptions(cli);
15210
+ attachInitDefaultAction(initCmd);
14815
15211
  return undefined;
14816
15212
  },
14817
15213
  });
@@ -18813,4 +19209,4 @@ var index = /*#__PURE__*/Object.freeze({
18813
19209
  __Client: Client
18814
19210
  });
18815
19211
 
18816
- export { GetDotenvCli, baseRootOptionDefaults, buildSpawnEnv, createCli, defineDynamic, defineGetDotenvConfig, definePlugin, defineScripts, dotenvExpand, dotenvExpandAll, dotenvExpandFromProcessEnv, getDotenv, getDotenvCliOptions2Options, interpolateDeep, readMergedOptions };
19212
+ export { GetDotenvCli, baseRootOptionDefaults, buildSpawnEnv, createCli, defineDynamic, defineGetDotenvConfig, definePlugin, defineScripts, dotenvExpand, dotenvExpandAll, dotenvExpandFromProcessEnv, getDotenv, getDotenvCliOptions2Options, interpolateDeep, maybeWarnEntropy, readMergedOptions, redactDisplay, redactObject, shouldCapture, traceChildEnv };