@karmaniverous/get-dotenv 6.1.0 → 6.2.0

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 (51) hide show
  1. package/README.md +20 -14
  2. package/dist/cli.d.ts +58 -2
  3. package/dist/cli.mjs +800 -364
  4. package/dist/cliHost.d.ts +216 -17
  5. package/dist/cliHost.mjs +178 -14
  6. package/dist/config.d.ts +12 -0
  7. package/dist/config.mjs +79 -2
  8. package/dist/env-overlay.d.ts +8 -0
  9. package/dist/getdotenv.cli.mjs +800 -364
  10. package/dist/index.d.ts +220 -35
  11. package/dist/index.mjs +851 -365
  12. package/dist/plugins-aws.d.ts +94 -6
  13. package/dist/plugins-aws.mjs +462 -184
  14. package/dist/plugins-batch.d.ts +85 -3
  15. package/dist/plugins-batch.mjs +203 -54
  16. package/dist/plugins-cmd.d.ts +85 -3
  17. package/dist/plugins-cmd.mjs +150 -28
  18. package/dist/plugins-init.d.ts +85 -3
  19. package/dist/plugins-init.mjs +270 -131
  20. package/dist/plugins.d.ts +85 -4
  21. package/dist/plugins.mjs +800 -364
  22. package/dist/templates/cli/plugins/hello/defaultAction.ts +27 -0
  23. package/dist/templates/cli/plugins/hello/index.ts +26 -0
  24. package/dist/templates/cli/plugins/hello/options.ts +31 -0
  25. package/dist/templates/cli/plugins/hello/strangerAction.ts +20 -0
  26. package/dist/templates/cli/plugins/hello/types.ts +13 -0
  27. package/dist/templates/defaultAction.ts +27 -0
  28. package/dist/templates/hello/defaultAction.ts +27 -0
  29. package/dist/templates/hello/index.ts +26 -0
  30. package/dist/templates/hello/options.ts +31 -0
  31. package/dist/templates/hello/strangerAction.ts +20 -0
  32. package/dist/templates/hello/types.ts +13 -0
  33. package/dist/templates/index.ts +23 -22
  34. package/dist/templates/options.ts +31 -0
  35. package/dist/templates/plugins/hello/defaultAction.ts +27 -0
  36. package/dist/templates/plugins/hello/index.ts +26 -0
  37. package/dist/templates/plugins/hello/options.ts +31 -0
  38. package/dist/templates/plugins/hello/strangerAction.ts +20 -0
  39. package/dist/templates/plugins/hello/types.ts +13 -0
  40. package/dist/templates/strangerAction.ts +20 -0
  41. package/dist/templates/types.ts +13 -0
  42. package/package.json +3 -4
  43. package/templates/cli/plugins/hello/defaultAction.ts +27 -0
  44. package/templates/cli/plugins/hello/index.ts +26 -0
  45. package/templates/cli/plugins/hello/options.ts +31 -0
  46. package/templates/cli/plugins/hello/strangerAction.ts +20 -0
  47. package/templates/cli/plugins/hello/types.ts +13 -0
  48. package/dist/templates/cli/plugins/hello.ts +0 -42
  49. package/dist/templates/hello.ts +0 -42
  50. package/dist/templates/plugins/hello.ts +0 -42
  51. package/templates/cli/plugins/hello.ts +0 -42
package/dist/cli.mjs CHANGED
@@ -38,26 +38,58 @@ import { versions, env } from 'process';
38
38
  * Minimal process env representation used by options and helpers.
39
39
  * Values may be `undefined` to indicate "unset".
40
40
  */
41
+ /**
42
+ * Schema for an env-like record.
43
+ *
44
+ * Keys are environment variable names and values are either strings or `undefined`
45
+ * (to represent “unset”).
46
+ *
47
+ * @public
48
+ */
41
49
  const processEnvSchema = z$2.record(z$2.string(), z$2.string().optional());
42
50
  // RAW: all fields optional — undefined means "inherit" from lower layers.
51
+ /**
52
+ * Programmatic options schema (raw).
53
+ *
54
+ * This schema is the canonical runtime source of truth for the `getDotenv()` programmatic API.
55
+ * All fields are optional; `undefined` generally means “inherit default/lower layer”.
56
+ *
57
+ * @public
58
+ */
43
59
  const getDotenvOptionsSchemaRaw = z$2.object({
60
+ /** Default environment name when `env` is not provided. */
44
61
  defaultEnv: z$2.string().optional(),
62
+ /** Base dotenv filename token (default `.env`). */
45
63
  dotenvToken: z$2.string().optional(),
64
+ /** Path to a dynamic variables module (JS/TS) to load and apply. */
46
65
  dynamicPath: z$2.string().optional(),
47
- // Dynamic map is intentionally wide for now; refine once sources are normalized.
66
+ /** Dynamic map is intentionally wide for now; refine once sources are normalized. */
48
67
  dynamic: z$2.record(z$2.string(), z$2.unknown()).optional(),
68
+ /** Selected environment name for this invocation (for env-scoped files and overlays). */
49
69
  env: z$2.string().optional(),
70
+ /** When true, skip applying dynamic variables. */
50
71
  excludeDynamic: z$2.boolean().optional(),
72
+ /** When true, skip environment-scoped dotenv files. */
51
73
  excludeEnv: z$2.boolean().optional(),
74
+ /** When true, skip global dotenv files. */
52
75
  excludeGlobal: z$2.boolean().optional(),
76
+ /** When true, skip private dotenv files. */
53
77
  excludePrivate: z$2.boolean().optional(),
78
+ /** When true, skip public dotenv files. */
54
79
  excludePublic: z$2.boolean().optional(),
80
+ /** When true, merge the final composed environment into `process.env`. */
55
81
  loadProcess: z$2.boolean().optional(),
82
+ /** When true, log the final environment map via `logger`. */
56
83
  log: z$2.boolean().optional(),
84
+ /** Logger used when `log` is enabled (console-compatible). */
57
85
  logger: z$2.unknown().default(console),
86
+ /** Optional output dotenv file path to write after composition. */
58
87
  outputPath: z$2.string().optional(),
88
+ /** Dotenv search paths (ordered). */
59
89
  paths: z$2.array(z$2.string()).optional(),
90
+ /** Private token suffix for private dotenv files (default `local`). */
60
91
  privateToken: z$2.string().optional(),
92
+ /** Explicit variables to overlay onto the composed dotenv map. */
61
93
  vars: processEnvSchema.optional(),
62
94
  });
63
95
  /**
@@ -65,6 +97,14 @@ const getDotenvOptionsSchemaRaw = z$2.object({
65
97
  * For now, this mirrors the RAW schema; future stages may materialize defaults
66
98
  * and narrow shapes as resolution is wired into the host.
67
99
  */
100
+ /**
101
+ * Programmatic options schema (resolved).
102
+ *
103
+ * Today this mirrors {@link getDotenvOptionsSchemaRaw}, but is kept as a distinct export
104
+ * so future resolution steps can narrow or materialize defaults without breaking the API.
105
+ *
106
+ * @public
107
+ */
68
108
  const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
69
109
 
70
110
  /**
@@ -74,27 +114,55 @@ const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
74
114
  * reflect normalized types (paths: string[], vars: ProcessEnv), applied in the
75
115
  * CLI resolution pipeline.
76
116
  */
117
+ /**
118
+ * CLI options schema (raw).
119
+ *
120
+ * Extends the programmatic options schema with CLI-only flags and stringly inputs
121
+ * which are normalized later by the host resolution pipeline.
122
+ *
123
+ * @public
124
+ */
77
125
  const getDotenvCliOptionsSchemaRaw = getDotenvOptionsSchemaRaw.extend({
78
126
  // CLI-specific fields (stringly inputs before preprocessing)
127
+ /** Enable verbose debug output (host-specific). */
79
128
  debug: z$2.boolean().optional(),
129
+ /** Fail on validation errors (schema/requiredKeys). */
80
130
  strict: z$2.boolean().optional(),
131
+ /** Capture child process stdio (useful for CI/tests). */
81
132
  capture: z$2.boolean().optional(),
133
+ /** Emit child env diagnostics (boolean or selected keys). */
82
134
  trace: z$2.union([z$2.boolean(), z$2.array(z$2.string())]).optional(),
135
+ /** Enable presentation-time redaction in trace/log output. */
83
136
  redact: z$2.boolean().optional(),
137
+ /** Enable entropy warnings in trace/log output. */
84
138
  warnEntropy: z$2.boolean().optional(),
139
+ /** Entropy threshold (bits/char) for warnings. */
85
140
  entropyThreshold: z$2.number().optional(),
141
+ /** Minimum value length to consider for entropy warnings. */
86
142
  entropyMinLength: z$2.number().optional(),
143
+ /** Regex patterns (strings) to suppress entropy warnings by key. */
87
144
  entropyWhitelist: z$2.array(z$2.string()).optional(),
145
+ /** Additional key-match patterns (strings) for redaction. */
88
146
  redactPatterns: z$2.array(z$2.string()).optional(),
147
+ /** Dotenv search paths provided as a single delimited string. */
89
148
  paths: z$2.string().optional(),
149
+ /** Delimiter string used to split `paths`. */
90
150
  pathsDelimiter: z$2.string().optional(),
151
+ /** Regex pattern used to split `paths` (takes precedence over delimiter). */
91
152
  pathsDelimiterPattern: z$2.string().optional(),
153
+ /** Scripts table in a permissive shape at parse time (validated elsewhere). */
92
154
  scripts: z$2.record(z$2.string(), z$2.unknown()).optional(),
155
+ /** Shell selection (`false` for shell-off, string for explicit shell). */
93
156
  shell: z$2.union([z$2.boolean(), z$2.string()]).optional(),
157
+ /** Extra variables expressed as a single delimited string of assignments. */
94
158
  vars: z$2.string().optional(),
159
+ /** Assignment operator used when parsing `vars`. */
95
160
  varsAssignor: z$2.string().optional(),
161
+ /** Regex pattern used as the assignment operator for `vars` parsing. */
96
162
  varsAssignorPattern: z$2.string().optional(),
163
+ /** Delimiter string used to split `vars`. */
97
164
  varsDelimiter: z$2.string().optional(),
165
+ /** Regex pattern used to split `vars` (takes precedence over delimiter). */
98
166
  varsDelimiterPattern: z$2.string().optional(),
99
167
  });
100
168
 
@@ -118,17 +186,34 @@ const envStringMap = z$2.record(z$2.string(), stringMap);
118
186
  * Raw configuration schema for get‑dotenv config files (JSON/YAML/JS/TS).
119
187
  * Validates allowed top‑level keys without performing path normalization.
120
188
  */
189
+ /**
190
+ * Config schema for discovered get-dotenv configuration documents (raw).
191
+ *
192
+ * This schema validates the allowed top-level keys for configuration files.
193
+ * It does not normalize paths or coerce types beyond Zod’s parsing.
194
+ *
195
+ * @public
196
+ */
121
197
  const getDotenvConfigSchemaRaw = z$2.object({
198
+ /** Root option defaults applied by the host (CLI-like, collapsed families). */
122
199
  rootOptionDefaults: getDotenvCliOptionsSchemaRaw.optional(),
200
+ /** Help-time visibility map for root flags (false hides). */
123
201
  rootOptionVisibility: visibilityMap.optional(),
124
- scripts: z$2.record(z$2.string(), z$2.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
202
+ /** Scripts table used by cmd/batch resolution (validation intentionally permissive here). */
203
+ scripts: z$2.record(z$2.string(), z$2.unknown()).optional(),
204
+ /** Keys required to be present in the final composed environment. */
125
205
  requiredKeys: z$2.array(z$2.string()).optional(),
206
+ /** Validation schema (JS/TS only; JSON/YAML loader rejects). */
126
207
  schema: z$2.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
208
+ /** Public global variables (string-only). */
127
209
  vars: stringMap.optional(), // public, global
210
+ /** Public per-environment variables (string-only). */
128
211
  envVars: envStringMap.optional(), // public, per-env
129
212
  // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
213
+ /** Dynamic variable definitions (JS/TS only). */
130
214
  dynamic: z$2.unknown().optional(),
131
215
  // Per-plugin config bag; validated by plugins/host when used.
216
+ /** Per-plugin config slices keyed by realized mount path (for example, `aws/whoami`). */
132
217
  plugins: z$2.record(z$2.string(), z$2.unknown()).optional(),
133
218
  });
134
219
  /**
@@ -1018,13 +1103,21 @@ function traceChildEnv(opts) {
1018
1103
  * Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
1019
1104
  * Used as the bottom layer for CLI option resolution.
1020
1105
  */
1106
+ const baseScripts = {
1107
+ 'git-status': {
1108
+ cmd: 'git branch --show-current && git status -s -u',
1109
+ shell: true,
1110
+ },
1111
+ };
1021
1112
  /**
1022
- * Default values for root CLI options used by the host and helpers as the
1023
- * baseline layer during option resolution.
1113
+ * Default values for root CLI options used by the host and helpers as the baseline layer during option resolution.
1024
1114
  *
1025
- * These defaults correspond to the "stringly" root surface (see `RootOptionsShape`)
1026
- * and are merged by precedence with create-time overrides and any discovered
1027
- * configuration `rootOptionDefaults` before CLI flags are applied.
1115
+ * These defaults correspond to the stringly root surface (see `RootOptionsShape`) and are merged by precedence with:
1116
+ * - create-time overrides
1117
+ * - any discovered configuration `rootOptionDefaults`
1118
+ * - and finally CLI flags at runtime
1119
+ *
1120
+ * @public
1028
1121
  */
1029
1122
  const baseRootOptionDefaults = {
1030
1123
  dotenvToken: '.env',
@@ -1038,12 +1131,7 @@ const baseRootOptionDefaults = {
1038
1131
  paths: './',
1039
1132
  pathsDelimiter: ' ',
1040
1133
  privateToken: 'local',
1041
- scripts: {
1042
- 'git-status': {
1043
- cmd: 'git branch --show-current && git status -s -u',
1044
- shell: true,
1045
- },
1046
- },
1134
+ scripts: baseScripts,
1047
1135
  shell: true,
1048
1136
  vars: '',
1049
1137
  varsAssignor: '=',
@@ -1590,6 +1678,14 @@ async function _execNormalized(command, shell, opts = {}) {
1590
1678
  return out;
1591
1679
  }
1592
1680
  }
1681
+ /**
1682
+ * Execute a command and capture stdout/stderr (buffered).
1683
+ *
1684
+ * @param command - Command string (shell) or argv array (shell-off supported).
1685
+ * @param shell - Shell setting (false for plain execution).
1686
+ * @param opts - Execution options (cwd/env/timeout).
1687
+ * @returns A promise resolving to the captured result.
1688
+ */
1593
1689
  async function runCommandResult(command, shell, opts = {}) {
1594
1690
  // Build opts without injecting undefined (exactOptionalPropertyTypes-safe)
1595
1691
  const coreOpts = { stdio: 'pipe' };
@@ -1604,6 +1700,14 @@ async function runCommandResult(command, shell, opts = {}) {
1604
1700
  }
1605
1701
  return _execNormalized(command, shell, coreOpts);
1606
1702
  }
1703
+ /**
1704
+ * Execute a command and return its exit code.
1705
+ *
1706
+ * @param command - Command string (shell) or argv array (shell-off supported).
1707
+ * @param shell - Shell setting (false for plain execution).
1708
+ * @param opts - Execution options (cwd/env/stdio).
1709
+ * @returns A promise resolving to the process exit code.
1710
+ */
1607
1711
  async function runCommand(command, shell, opts) {
1608
1712
  // Build opts without injecting undefined (exactOptionalPropertyTypes-safe)
1609
1713
  const callOpts = {};
@@ -2063,6 +2167,16 @@ function evaluateDynamicOptions(root, resolved) {
2063
2167
  visit(root);
2064
2168
  }
2065
2169
 
2170
+ /**
2171
+ * Initialize a {@link GetDotenvCli} instance with help configuration and safe defaults.
2172
+ *
2173
+ * @remarks
2174
+ * This is a low-level initializer used by the host constructor to keep `GetDotenvCli.ts`
2175
+ * small and to centralize help/output behavior.
2176
+ *
2177
+ * @param cli - The CLI instance to initialize.
2178
+ * @param headerGetter - Callback returning an optional help header string.
2179
+ */
2066
2180
  function initializeInstance(cli, headerGetter) {
2067
2181
  // Configure grouped help: show only base options in default "Options";
2068
2182
  // subcommands show all of their own options.
@@ -3016,18 +3130,49 @@ function applyRootVisibility(program, visibility) {
3016
3130
  function applyAwsContext(out, ctx, setProcessEnv = true) {
3017
3131
  const { profile, region, credentials } = out;
3018
3132
  if (setProcessEnv) {
3019
- if (region) {
3020
- process.env.AWS_REGION = region;
3021
- if (!process.env.AWS_DEFAULT_REGION) {
3022
- process.env.AWS_DEFAULT_REGION = region;
3133
+ // Ensure AWS credential sources are mutually exclusive.
3134
+ // The AWS SDK warns (and may change precedence in future) when both
3135
+ // AWS_PROFILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are set.
3136
+ const clear = (keys) => {
3137
+ for (const k of keys) {
3138
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
3139
+ delete process.env[k];
3023
3140
  }
3024
- }
3141
+ };
3142
+ const clearProfileVars = () => {
3143
+ clear(['AWS_PROFILE', 'AWS_DEFAULT_PROFILE', 'AWS_SDK_LOAD_CONFIG']);
3144
+ };
3145
+ const clearStaticCreds = () => {
3146
+ clear([
3147
+ 'AWS_ACCESS_KEY_ID',
3148
+ 'AWS_SECRET_ACCESS_KEY',
3149
+ 'AWS_SESSION_TOKEN',
3150
+ ]);
3151
+ };
3152
+ // Mode A: exported/static credentials (clear profile vars)
3025
3153
  if (credentials) {
3154
+ clearProfileVars();
3026
3155
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
3027
3156
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
3028
3157
  if (credentials.sessionToken !== undefined) {
3029
3158
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
3030
3159
  }
3160
+ else {
3161
+ delete process.env.AWS_SESSION_TOKEN;
3162
+ }
3163
+ }
3164
+ else if (profile) {
3165
+ // Mode B: profile-based (SSO) credentials (clear static creds)
3166
+ clearStaticCreds();
3167
+ process.env.AWS_PROFILE = profile;
3168
+ process.env.AWS_DEFAULT_PROFILE = profile;
3169
+ process.env.AWS_SDK_LOAD_CONFIG = '1';
3170
+ }
3171
+ if (region) {
3172
+ process.env.AWS_REGION = region;
3173
+ if (!process.env.AWS_DEFAULT_REGION) {
3174
+ process.env.AWS_DEFAULT_REGION = region;
3175
+ }
3031
3176
  }
3032
3177
  }
3033
3178
  // Always publish minimal, non-sensitive metadata
@@ -3038,7 +3183,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
3038
3183
  };
3039
3184
  }
3040
3185
 
3041
- const DEFAULT_TIMEOUT_MS = 15_000;
3186
+ const AWS_CLI_TIMEOUT_MS = 15_000;
3042
3187
  const trim = (s) => (typeof s === 'string' ? s.trim() : '');
3043
3188
  const unquote = (s) => s.length >= 2 &&
3044
3189
  ((s.startsWith('"') && s.endsWith('"')) ||
@@ -3073,6 +3218,7 @@ const parseExportCredentialsJson = (txt) => {
3073
3218
  /**
3074
3219
  * Parse AWS credentials from environment-export output (shell-agnostic).
3075
3220
  * Supports POSIX `export KEY=VAL` and PowerShell `$Env:KEY=VAL`.
3221
+ * Also supports AWS CLI `windows-cmd` (`set KEY=VAL`) and `env-no-export` (`KEY=VAL`).
3076
3222
  *
3077
3223
  * @param txt - Raw stdout text from the AWS CLI.
3078
3224
  * @returns Parsed credentials, or `undefined` when the input is not recognized.
@@ -3086,12 +3232,17 @@ const parseExportCredentialsEnv = (txt) => {
3086
3232
  const line = raw.trim();
3087
3233
  if (!line)
3088
3234
  continue;
3089
- // POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
3235
+ // POSIX: export AWS_ACCESS_KEY_ID=..., ...
3090
3236
  let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3091
- if (!m) {
3092
- // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
3093
- m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3094
- }
3237
+ // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
3238
+ if (!m)
3239
+ m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
3240
+ // Windows cmd: set AWS_ACCESS_KEY_ID=..., etc.
3241
+ if (!m)
3242
+ m = /^(?:set)\s+([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
3243
+ // env-no-export: AWS_ACCESS_KEY_ID=..., etc.
3244
+ if (!m)
3245
+ m = /^([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
3095
3246
  if (!m)
3096
3247
  continue;
3097
3248
  const k = m[1];
@@ -3116,7 +3267,7 @@ const parseExportCredentialsEnv = (txt) => {
3116
3267
  };
3117
3268
  return undefined;
3118
3269
  };
3119
- const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
3270
+ const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
3120
3271
  const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
3121
3272
  env: process.env,
3122
3273
  timeoutMs,
@@ -3131,30 +3282,43 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
3131
3282
  }
3132
3283
  return undefined;
3133
3284
  };
3134
- const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
3135
- // Try JSON format first (AWS CLI v2)
3136
- const rJson = await runCommandResult([
3137
- 'aws',
3138
- 'configure',
3139
- 'export-credentials',
3140
- '--profile',
3141
- profile,
3142
- '--format',
3143
- 'json',
3144
- ], false, { env: process.env, timeoutMs });
3145
- if (rJson.exitCode === 0) {
3146
- const creds = parseExportCredentialsJson(rJson.stdout);
3147
- if (creds)
3148
- return creds;
3149
- }
3150
- // Fallback: env lines
3151
- const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
3152
- if (rEnv.exitCode === 0) {
3153
- const creds = parseExportCredentialsEnv(rEnv.stdout);
3285
+ const exportCredentials = async (profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
3286
+ const tryExport = async (format) => {
3287
+ const argv = [
3288
+ 'aws',
3289
+ 'configure',
3290
+ 'export-credentials',
3291
+ '--profile',
3292
+ profile,
3293
+ ...(format ? ['--format', format] : []),
3294
+ ];
3295
+ const r = await runCommandResult(argv, false, {
3296
+ env: process.env,
3297
+ timeoutMs,
3298
+ });
3299
+ if (r.exitCode !== 0)
3300
+ return undefined;
3301
+ const out = trim(r.stdout);
3302
+ if (!out)
3303
+ return undefined;
3304
+ // Some formats produce JSON ("process"), some produce shell-ish env lines.
3305
+ return parseExportCredentialsJson(out) ?? parseExportCredentialsEnv(out);
3306
+ };
3307
+ // Prefer the default/JSON "process" format first; then fall back to shell env outputs.
3308
+ // Note: AWS CLI v2 supports: process | env | env-no-export | powershell | windows-cmd
3309
+ const formats = [
3310
+ 'process',
3311
+ ...(process.platform === 'win32'
3312
+ ? ['powershell', 'windows-cmd', 'env', 'env-no-export']
3313
+ : ['env', 'env-no-export']),
3314
+ ];
3315
+ for (const f of formats) {
3316
+ const creds = await tryExport(f);
3154
3317
  if (creds)
3155
3318
  return creds;
3156
3319
  }
3157
- return undefined;
3320
+ // Final fallback: no --format (AWS CLI default output)
3321
+ return tryExport(undefined);
3158
3322
  };
3159
3323
  /**
3160
3324
  * Resolve AWS context (profile, region, credentials) using configuration and environment.
@@ -3186,31 +3350,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3186
3350
  out.region = region;
3187
3351
  return out;
3188
3352
  }
3189
- // Env-first credentials.
3190
3353
  let credentials;
3191
- const envId = trim(process.env.AWS_ACCESS_KEY_ID);
3192
- const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
3193
- const envToken = trim(process.env.AWS_SESSION_TOKEN);
3194
- if (envId && envSecret) {
3195
- credentials = {
3196
- accessKeyId: envId,
3197
- secretAccessKey: envSecret,
3198
- ...(envToken ? { sessionToken: envToken } : {}),
3199
- };
3200
- }
3201
- else if (profile) {
3354
+ // Profile wins over ambient env creds when present (from flags/config/dotenv).
3355
+ if (profile) {
3202
3356
  // Try export-credentials
3203
3357
  credentials = await exportCredentials(profile);
3204
3358
  // On failure, detect SSO and optionally login then retry
3205
3359
  if (!credentials) {
3206
3360
  const ssoSession = await getAwsConfigure('sso_session', profile);
3207
- const looksSSO = typeof ssoSession === 'string' && ssoSession.length > 0;
3361
+ // Legacy SSO profiles use sso_start_url/sso_region rather than sso_session.
3362
+ const ssoStartUrl = await getAwsConfigure('sso_start_url', profile);
3363
+ const looksSSO = (typeof ssoSession === 'string' && ssoSession.length > 0) ||
3364
+ (typeof ssoStartUrl === 'string' && ssoStartUrl.length > 0);
3208
3365
  if (looksSSO && cfg.loginOnDemand) {
3209
- // Best-effort login, then retry export once.
3210
- await runCommandResult(['aws', 'sso', 'login', '--profile', profile], false, {
3366
+ // Interactive login (no timeout by default), then retry export once.
3367
+ const exit = await runCommand(['aws', 'sso', 'login', '--profile', profile], false, {
3211
3368
  env: process.env,
3212
- timeoutMs: DEFAULT_TIMEOUT_MS,
3369
+ stdio: 'inherit',
3213
3370
  });
3371
+ if (exit !== 0) {
3372
+ throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
3373
+ }
3214
3374
  credentials = await exportCredentials(profile);
3215
3375
  }
3216
3376
  }
@@ -3228,6 +3388,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3228
3388
  }
3229
3389
  }
3230
3390
  }
3391
+ else {
3392
+ // Env-first credentials when no profile is present.
3393
+ const envId = trim(process.env.AWS_ACCESS_KEY_ID);
3394
+ const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
3395
+ const envToken = trim(process.env.AWS_SESSION_TOKEN);
3396
+ if (envId && envSecret) {
3397
+ credentials = {
3398
+ accessKeyId: envId,
3399
+ secretAccessKey: envSecret,
3400
+ ...(envToken ? { sessionToken: envToken } : {}),
3401
+ };
3402
+ }
3403
+ }
3231
3404
  // Final region resolution
3232
3405
  if (!region && profile)
3233
3406
  region = await getAwsConfigure('region', profile);
@@ -3244,16 +3417,234 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
3244
3417
  };
3245
3418
 
3246
3419
  /**
3247
- * Zod schema for AWS plugin configuration.
3420
+ * Create the AWS plugin `afterResolve` hook.
3421
+ *
3422
+ * This runs once per invocation after the host resolves dotenv context.
3423
+ *
3424
+ * @param plugin - The AWS plugin instance.
3425
+ * @returns An `afterResolve` hook function suitable for assigning to `plugin.afterResolve`.
3426
+ *
3427
+ * @internal
3428
+ */
3429
+ function attachAwsAfterResolveHook(plugin) {
3430
+ return async (cli, ctx) => {
3431
+ const cfg = plugin.readConfig(cli);
3432
+ const out = await resolveAwsContext({
3433
+ dotenv: ctx.dotenv,
3434
+ cfg,
3435
+ });
3436
+ applyAwsContext(out, ctx, true);
3437
+ // Optional: low-noise breadcrumb for diagnostics
3438
+ if (process.env.GETDOTENV_DEBUG) {
3439
+ try {
3440
+ const msg = JSON.stringify({
3441
+ profile: out.profile,
3442
+ region: out.region,
3443
+ hasCreds: Boolean(out.credentials),
3444
+ });
3445
+ process.stderr.write(`[aws] afterResolve ${msg}\n`);
3446
+ }
3447
+ catch {
3448
+ /* ignore */
3449
+ }
3450
+ }
3451
+ };
3452
+ }
3453
+
3454
+ /** @internal */
3455
+ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
3456
+ /**
3457
+ * Create an AWS plugin config overlay from Commander-parsed option values.
3458
+ *
3459
+ * This preserves tri-state intent:
3460
+ * - If a flag was not provided, it should not overwrite config-derived defaults.
3461
+ * - If `--no-…` was provided, it must explicitly force the boolean false.
3462
+ *
3463
+ * @param opts - Commander option values for the current invocation.
3464
+ * @returns A partial AWS plugin config object containing only explicit overrides.
3465
+ *
3466
+ * @internal
3467
+ */
3468
+ function awsConfigOverridesFromCommandOpts(opts) {
3469
+ const o = isRecord(opts) ? opts : {};
3470
+ const overlay = {};
3471
+ // Map boolean toggles (respect explicit --no-*)
3472
+ if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand')) {
3473
+ overlay.loginOnDemand = Boolean(o.loginOnDemand);
3474
+ }
3475
+ // Strings/enums
3476
+ if (typeof o.profile === 'string')
3477
+ overlay.profile = o.profile;
3478
+ if (typeof o.region === 'string')
3479
+ overlay.region = o.region;
3480
+ if (typeof o.defaultRegion === 'string')
3481
+ overlay.defaultRegion = o.defaultRegion;
3482
+ if (o.strategy === 'cli-export' || o.strategy === 'none') {
3483
+ overlay.strategy = o.strategy;
3484
+ }
3485
+ // Advanced key overrides
3486
+ if (typeof o.profileKey === 'string')
3487
+ overlay.profileKey = o.profileKey;
3488
+ if (typeof o.profileFallbackKey === 'string') {
3489
+ overlay.profileFallbackKey = o.profileFallbackKey;
3490
+ }
3491
+ if (typeof o.regionKey === 'string')
3492
+ overlay.regionKey = o.regionKey;
3493
+ return overlay;
3494
+ }
3495
+
3496
+ /**
3497
+ * Attach the default action for the AWS plugin mount.
3498
+ *
3499
+ * Behavior:
3500
+ * - With args: forwards to AWS CLI (`aws <args...>`) under the established session.
3501
+ * - Without args: session-only establishment (no forward).
3502
+ *
3503
+ * @param cli - The `aws` command mount.
3504
+ * @param plugin - The AWS plugin instance.
3505
+ *
3506
+ * @internal
3507
+ */
3508
+ function attachAwsDefaultAction(cli, plugin, awsCmd) {
3509
+ awsCmd.action(async (args, opts, thisCommand) => {
3510
+ // Access merged root CLI options (installed by root hooks).
3511
+ const bag = readMergedOptions(thisCommand);
3512
+ const capture = shouldCapture(bag.capture);
3513
+ const underTests = process.env.GETDOTENV_TEST === '1' ||
3514
+ typeof process.env.VITEST_WORKER_ID === 'string';
3515
+ // Build overlay cfg from subcommand flags layered over discovered config.
3516
+ const ctx = cli.getCtx();
3517
+ const cfgBase = plugin.readConfig(cli);
3518
+ const cfg = {
3519
+ ...cfgBase,
3520
+ ...awsConfigOverridesFromCommandOpts(opts),
3521
+ };
3522
+ // Resolve current context with overrides
3523
+ const out = await resolveAwsContext({
3524
+ dotenv: ctx.dotenv,
3525
+ cfg,
3526
+ });
3527
+ // Publish env/context
3528
+ applyAwsContext(out, ctx, true);
3529
+ // Forward when positional args are present; otherwise session-only.
3530
+ if (args.length > 0) {
3531
+ const argv = ['aws', ...args];
3532
+ const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
3533
+ const exit = await runCommand(argv, shellSetting, {
3534
+ env: buildSpawnEnv(process.env, ctx.dotenv),
3535
+ stdio: capture ? 'pipe' : 'inherit',
3536
+ });
3537
+ // Deterministic termination (suppressed under tests)
3538
+ if (!underTests) {
3539
+ process.exit(typeof exit === 'number' ? exit : 0);
3540
+ }
3541
+ return;
3542
+ }
3543
+ // Session only: low-noise breadcrumb under debug
3544
+ if (process.env.GETDOTENV_DEBUG) {
3545
+ try {
3546
+ const msg = JSON.stringify({
3547
+ profile: out.profile,
3548
+ region: out.region,
3549
+ hasCreds: Boolean(out.credentials),
3550
+ });
3551
+ process.stderr.write(`[aws] session established ${msg}\n`);
3552
+ }
3553
+ catch {
3554
+ /* ignore */
3555
+ }
3556
+ }
3557
+ if (!underTests)
3558
+ process.exit(0);
3559
+ });
3560
+ }
3561
+
3562
+ /**
3563
+ * Attach options/arguments for the AWS plugin mount.
3564
+ *
3565
+ * @param cli - The `aws` command mount.
3566
+ * @param plugin - The AWS plugin instance (for dynamic option descriptions).
3567
+ *
3568
+ * @internal
3248
3569
  */
3249
- const AwsPluginConfigSchema = z$2.object({
3570
+ /** @hidden */
3571
+ function attachAwsOptions(cli, plugin) {
3572
+ return (cli
3573
+ // Description is owned by the plugin index (src/plugins/aws/index.ts).
3574
+ .enablePositionalOptions()
3575
+ .passThroughOptions()
3576
+ .allowUnknownOption(true)
3577
+ // Boolean toggles with dynamic help labels (effective defaults)
3578
+ .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
3579
+ .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
3580
+ // Strings / enums
3581
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
3582
+ .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
3583
+ .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
3584
+ .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
3585
+ // Advanced key overrides
3586
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
3587
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
3588
+ .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
3589
+ // Accept any extra operands so Commander does not error when tokens appear after "--".
3590
+ .argument('[args...]'));
3591
+ }
3592
+
3593
+ /**
3594
+ * Attach the AWS plugin `preSubcommand` hook.
3595
+ *
3596
+ * Ensures `aws --profile/--region <child>` applies the AWS session setup before
3597
+ * child subcommand execution.
3598
+ *
3599
+ * @param cli - The `aws` command mount.
3600
+ * @param plugin - The AWS plugin instance.
3601
+ *
3602
+ * @internal
3603
+ */
3604
+ function attachAwsPreSubcommandHook(cli, plugin) {
3605
+ cli.hook('preSubcommand', async (thisCommand) => {
3606
+ // Avoid side effects for help rendering.
3607
+ if (process.argv.includes('-h') || process.argv.includes('--help'))
3608
+ return;
3609
+ const ctx = cli.getCtx();
3610
+ const cfgBase = plugin.readConfig(cli);
3611
+ const cfg = {
3612
+ ...cfgBase,
3613
+ ...awsConfigOverridesFromCommandOpts(thisCommand.opts()),
3614
+ };
3615
+ const out = await resolveAwsContext({
3616
+ dotenv: ctx.dotenv,
3617
+ cfg,
3618
+ });
3619
+ applyAwsContext(out, ctx, true);
3620
+ });
3621
+ }
3622
+
3623
+ /**
3624
+ * AWS plugin configuration schema.
3625
+ *
3626
+ * @remarks
3627
+ * This Zod schema is used by the host to validate the `plugins.aws` config slice.
3628
+ *
3629
+ * @public
3630
+ * @hidden
3631
+ */
3632
+ const awsPluginConfigSchema = z$2.object({
3633
+ /** Preferred AWS profile name (overrides dotenv-derived profile keys when set). */
3250
3634
  profile: z$2.string().optional(),
3635
+ /** Preferred AWS region (overrides dotenv-derived region key when set). */
3251
3636
  region: z$2.string().optional(),
3637
+ /** Fallback region when region cannot be resolved from config/dotenv/AWS CLI. */
3252
3638
  defaultRegion: z$2.string().optional(),
3639
+ /** Dotenv/config key for local profile lookup (default `AWS_LOCAL_PROFILE`). */
3253
3640
  profileKey: z$2.string().default('AWS_LOCAL_PROFILE').optional(),
3641
+ /** Dotenv/config fallback key for profile lookup (default `AWS_PROFILE`). */
3254
3642
  profileFallbackKey: z$2.string().default('AWS_PROFILE').optional(),
3643
+ /** Dotenv/config key for region lookup (default `AWS_REGION`). */
3255
3644
  regionKey: z$2.string().default('AWS_REGION').optional(),
3645
+ /** Credential acquisition strategy (`cli-export` to resolve via AWS CLI, or `none` to skip). */
3256
3646
  strategy: z$2.enum(['cli-export', 'none']).default('cli-export').optional(),
3647
+ /** When true, attempt `aws sso login` on-demand when credential export fails for an SSO profile. */
3257
3648
  loginOnDemand: z$2.boolean().default(false).optional(),
3258
3649
  });
3259
3650
 
@@ -3270,129 +3661,16 @@ const AwsPluginConfigSchema = z$2.object({
3270
3661
  const awsPlugin = () => {
3271
3662
  const plugin = definePlugin({
3272
3663
  ns: 'aws',
3273
- configSchema: AwsPluginConfigSchema,
3274
- setup: (cli) => {
3275
- // Mount: aws (provided)
3276
- cli
3277
- .description('Establish an AWS session and optionally forward to the AWS CLI')
3278
- .enablePositionalOptions()
3279
- .passThroughOptions()
3280
- .allowUnknownOption(true)
3281
- // Boolean toggles with dynamic help labels (effective defaults)
3282
- .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
3283
- .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
3284
- // Strings / enums
3285
- .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
3286
- .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
3287
- .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
3288
- .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
3289
- // Advanced key overrides
3290
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
3291
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
3292
- .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
3293
- // Accept any extra operands so Commander does not error when tokens appear after "--".
3294
- .argument('[args...]')
3295
- .action(async (args, opts, thisCommand) => {
3296
- const pluginInst = plugin;
3297
- // Access merged root CLI options (installed by passOptions())
3298
- const bag = readMergedOptions(thisCommand);
3299
- const capture = shouldCapture(bag.capture);
3300
- const underTests = process.env.GETDOTENV_TEST === '1' ||
3301
- typeof process.env.VITEST_WORKER_ID === 'string';
3302
- // Build overlay cfg from subcommand flags layered over discovered config.
3303
- const ctx = cli.getCtx();
3304
- const cfgBase = pluginInst.readConfig(cli);
3305
- const o = opts;
3306
- const overlay = {};
3307
- // Map boolean toggles (respect explicit --no-*)
3308
- if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
3309
- overlay.loginOnDemand = Boolean(o.loginOnDemand);
3310
- // Strings/enums
3311
- if (typeof o.profile === 'string')
3312
- overlay.profile = o.profile;
3313
- if (typeof o.region === 'string')
3314
- overlay.region = o.region;
3315
- if (typeof o.defaultRegion === 'string')
3316
- overlay.defaultRegion = o.defaultRegion;
3317
- if (typeof o.strategy === 'string')
3318
- overlay.strategy = o.strategy;
3319
- // Advanced key overrides
3320
- if (typeof o.profileKey === 'string')
3321
- overlay.profileKey = o.profileKey;
3322
- if (typeof o.profileFallbackKey === 'string')
3323
- overlay.profileFallbackKey = o.profileFallbackKey;
3324
- if (typeof o.regionKey === 'string')
3325
- overlay.regionKey = o.regionKey;
3326
- const cfg = {
3327
- ...cfgBase,
3328
- ...overlay,
3329
- };
3330
- // Resolve current context with overrides
3331
- const out = await resolveAwsContext({
3332
- dotenv: ctx.dotenv,
3333
- cfg,
3334
- });
3335
- // Publish env/context
3336
- applyAwsContext(out, ctx, true);
3337
- // Forward when positional args are present; otherwise session-only.
3338
- if (Array.isArray(args) && args.length > 0) {
3339
- const argv = ['aws', ...args];
3340
- const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
3341
- const exit = await runCommand(argv, shellSetting, {
3342
- env: buildSpawnEnv(process.env, ctx.dotenv),
3343
- stdio: capture ? 'pipe' : 'inherit',
3344
- });
3345
- // Deterministic termination (suppressed under tests)
3346
- if (!underTests) {
3347
- process.exit(typeof exit === 'number' ? exit : 0);
3348
- }
3349
- return;
3350
- }
3351
- else {
3352
- // Session only: low-noise breadcrumb under debug
3353
- if (process.env.GETDOTENV_DEBUG) {
3354
- try {
3355
- const msg = JSON.stringify({
3356
- profile: out.profile,
3357
- region: out.region,
3358
- hasCreds: Boolean(out.credentials),
3359
- });
3360
- process.stderr.write(`[aws] session established ${msg}\n`);
3361
- }
3362
- catch {
3363
- /* ignore */
3364
- }
3365
- }
3366
- if (!underTests)
3367
- process.exit(0);
3368
- return;
3369
- }
3370
- });
3664
+ configSchema: awsPluginConfigSchema,
3665
+ setup(cli) {
3666
+ cli.description('Establish an AWS session and optionally forward to the AWS CLI');
3667
+ const awsCmd = attachAwsOptions(cli, plugin);
3668
+ attachAwsPreSubcommandHook(cli, plugin);
3669
+ attachAwsDefaultAction(cli, plugin, awsCmd);
3371
3670
  return undefined;
3372
3671
  },
3373
- afterResolve: async (_cli, ctx) => {
3374
- const cfg = plugin.readConfig(_cli);
3375
- const out = await resolveAwsContext({
3376
- dotenv: ctx.dotenv,
3377
- cfg,
3378
- });
3379
- applyAwsContext(out, ctx, true);
3380
- // Optional: low-noise breadcrumb for diagnostics
3381
- if (process.env.GETDOTENV_DEBUG) {
3382
- try {
3383
- const msg = JSON.stringify({
3384
- profile: out.profile,
3385
- region: out.region,
3386
- hasCreds: Boolean(out.credentials),
3387
- });
3388
- process.stderr.write(`[aws] afterResolve ${msg}\n`);
3389
- }
3390
- catch {
3391
- /* ignore */
3392
- }
3393
- }
3394
- },
3395
3672
  });
3673
+ plugin.afterResolve = attachAwsAfterResolveHook(plugin);
3396
3674
  return plugin;
3397
3675
  };
3398
3676
 
@@ -13854,21 +14132,79 @@ class GetCallerIdentityCommand extends Command
13854
14132
  }
13855
14133
 
13856
14134
  /**
13857
- * AWS Whoami plugin: prints the current AWS caller identity (account, arn, userid).
13858
- * Intended to be mounted under the `aws` plugin.
14135
+ * Attach the default action for the `aws whoami` command.
14136
+ *
14137
+ * This behavior executes only when `aws whoami` is invoked without a subcommand.
14138
+ *
14139
+ * @param cli - The `whoami` command mount.
14140
+ * @returns Nothing.
14141
+ */
14142
+ function attachWhoamiDefaultAction(cli) {
14143
+ cli.action(async () => {
14144
+ // The AWS SDK default providers will read credentials from process.env,
14145
+ // which the aws parent has already populated.
14146
+ const client = new STSClient$1();
14147
+ const result = await client.send(new GetCallerIdentityCommand());
14148
+ console.log(JSON.stringify(result, null, 2));
14149
+ });
14150
+ }
14151
+
14152
+ /**
14153
+ * Attach options/arguments for the `aws whoami` plugin mount.
14154
+ *
14155
+ * This subcommand currently takes no flags/args; this module exists to keep the
14156
+ * wiring layout consistent across shipped plugins (options vs actions).
14157
+ *
14158
+ * Note: the plugin description is owned by `src/plugins/aws/whoami/index.ts` and
14159
+ * must not be set here.
14160
+ *
14161
+ * @param cli - The `whoami` command mount under `aws`.
14162
+ * @returns The same `cli` instance for chaining.
14163
+ *
14164
+ * @internal
14165
+ */
14166
+ /** @hidden */
14167
+ function attachWhoamiOptions(cli) {
14168
+ return cli;
14169
+ }
14170
+
14171
+ /**
14172
+ * Attach the `really` subcommand under `aws whoami`.
14173
+ *
14174
+ * Reads `SECRET_IDENTITY` from the resolved get-dotenv context (`cli.getCtx().dotenv`).
14175
+ *
14176
+ * @param cli - The `whoami` command mount.
14177
+ * @returns Nothing.
14178
+ */
14179
+ function attachWhoamiReallyAction(cli) {
14180
+ const really = cli
14181
+ .ns('really')
14182
+ .description('Print SECRET_IDENTITY from the resolved dotenv context');
14183
+ really.action(() => {
14184
+ const secretIdentity = really.getCtx().dotenv.SECRET_IDENTITY;
14185
+ console.log(`Your secret identity is ${secretIdentity ?? 'still a secret'}.`);
14186
+ });
14187
+ }
14188
+
14189
+ /**
14190
+ * AWS Whoami plugin factory.
14191
+ *
14192
+ * This plugin demonstrates a “bucket of subcommands” pattern:
14193
+ * - Subcommand behavior is articulated in separate modules as `attach*` helpers.
14194
+ * - Those helpers are not individually composable plugins; they are internal wiring for one plugin instance.
14195
+ *
14196
+ * @returns A plugin instance mounted at `aws whoami`.
13859
14197
  */
13860
14198
  const awsWhoamiPlugin = () => definePlugin({
13861
14199
  ns: 'whoami',
13862
14200
  setup(cli) {
13863
- cli
13864
- .description('Print AWS caller identity (uses parent aws session)')
13865
- .action(async () => {
13866
- // The AWS SDK default providers will read credentials from process.env,
13867
- // which the aws parent has already populated.
13868
- const client = new STSClient$1();
13869
- const result = await client.send(new GetCallerIdentityCommand());
13870
- console.log(JSON.stringify(result, null, 2));
13871
- });
14201
+ cli.description('Print AWS caller identity (uses parent aws session)');
14202
+ // Options/args (none today, but keep layout consistent with other plugins).
14203
+ const whoami = attachWhoamiOptions(cli);
14204
+ // Default behavior: `getdotenv aws whoami`
14205
+ attachWhoamiDefaultAction(whoami);
14206
+ // Subcommand behavior: `getdotenv aws whoami really`
14207
+ attachWhoamiReallyAction(whoami);
13872
14208
  return undefined;
13873
14209
  },
13874
14210
  });
@@ -13976,7 +14312,7 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv,
13976
14312
  /**
13977
14313
  * Attach the default "cmd" subcommand action with contextual typing.
13978
14314
  */
13979
- const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
14315
+ const attachBatchCmdAction = (plugin, cli, batchCmd, pluginOpts, cmd) => {
13980
14316
  cmd.action(async (commandParts, _subOpts, thisCommand) => {
13981
14317
  const mergedBag = readMergedOptions(batchCmd);
13982
14318
  const logger = mergedBag.logger;
@@ -14085,11 +14421,37 @@ const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
14085
14421
  });
14086
14422
  };
14087
14423
 
14424
+ /**
14425
+ * Attach the default `cmd` subcommand under the `batch` command.
14426
+ *
14427
+ * This encapsulates:
14428
+ * - Subcommand construction (`new Command().name('cmd')…`)
14429
+ * - Action wiring
14430
+ * - Mounting as the default subcommand for `batch`
14431
+ *
14432
+ * @param plugin - The batch plugin instance.
14433
+ * @param cli - The batch command mount.
14434
+ * @param batchCmd - The `batch` command (same as `cli` mount).
14435
+ * @param pluginOpts - Batch plugin factory options.
14436
+ *
14437
+ * @internal
14438
+ */
14439
+ const attachBatchCmdSubcommand = (plugin, cli, batchCmd, pluginOpts) => {
14440
+ const cmdSub = new Command$1()
14441
+ .name('cmd')
14442
+ .description('execute command, conflicts with --command option (default subcommand)')
14443
+ .enablePositionalOptions()
14444
+ .passThroughOptions()
14445
+ .argument('[command...]');
14446
+ attachBatchCmdAction(plugin, cli, batchCmd, pluginOpts, cmdSub);
14447
+ batchCmd.addCommand(cmdSub, { isDefault: true });
14448
+ };
14449
+
14088
14450
  /**
14089
14451
  * Attach the parent-level action for the batch plugin.
14090
14452
  * Handles parent flags (e.g. `getdotenv batch -l`) and delegates to the batch executor.
14091
14453
  */
14092
- const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
14454
+ const attachBatchDefaultAction = (plugin, cli, pluginOpts, parent) => {
14093
14455
  parent.action(async function (...args) {
14094
14456
  // Commander Unknown generics: [...unknown[], OptionValues, thisCommand]
14095
14457
  const thisCommand = args[args.length - 1];
@@ -14190,24 +14552,71 @@ const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
14190
14552
  });
14191
14553
  };
14192
14554
 
14555
+ /**
14556
+ * Attach options/arguments for the batch plugin mount.
14557
+ *
14558
+ * Note: the plugin description is owned by `src/plugins/batch/index.ts` and
14559
+ * must not be set here.
14560
+ *
14561
+ * @param plugin - Batch plugin instance (for dynamic option descriptions).
14562
+ * @param cli - The `batch` command mount.
14563
+ * @returns The same `cli` instance for chaining.
14564
+ *
14565
+ * @internal
14566
+ */
14567
+ /** @hidden */
14568
+ function attachBatchOptions(plugin, cli) {
14569
+ const GROUP = `plugin:${cli.name()}`;
14570
+ return (cli
14571
+ .enablePositionalOptions()
14572
+ .passThroughOptions()
14573
+ // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
14574
+ .addOption((() => {
14575
+ const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
14576
+ cli.setOptionGroup(opt, GROUP);
14577
+ return opt;
14578
+ })())
14579
+ .addOption((() => {
14580
+ 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 || './')})`);
14581
+ cli.setOptionGroup(opt, GROUP);
14582
+ return opt;
14583
+ })())
14584
+ .addOption((() => {
14585
+ const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
14586
+ cli.setOptionGroup(opt, GROUP);
14587
+ return opt;
14588
+ })())
14589
+ .option('-c, --command <string>', 'command executed according to the base shell resolution')
14590
+ .option('-l, --list', 'list working directories without executing command')
14591
+ .option('-e, --ignore-errors', 'ignore errors and continue with next path')
14592
+ .argument('[command...]'));
14593
+ }
14594
+
14193
14595
  /**
14194
14596
  * Zod schema for a single script entry (string or object).
14195
14597
  */
14196
14598
  const ScriptSchema = z$2.union([
14197
14599
  z$2.string(),
14198
14600
  z$2.object({
14601
+ /** Command string to execute. */
14199
14602
  cmd: z$2.string(),
14603
+ /** Optional shell override for this script entry. */
14200
14604
  shell: z$2.union([z$2.string(), z$2.boolean()]).optional(),
14201
14605
  }),
14202
14606
  ]);
14203
14607
  /**
14204
14608
  * Zod schema for batch plugin configuration.
14205
14609
  */
14206
- const BatchConfigSchema = z$2.object({
14610
+ const batchPluginConfigSchema = z$2.object({
14611
+ /** Optional scripts table scoped to the batch plugin. */
14207
14612
  scripts: z$2.record(z$2.string(), ScriptSchema).optional(),
14613
+ /** Optional default shell for batch execution (overridden by per-script shell when present). */
14208
14614
  shell: z$2.union([z$2.string(), z$2.boolean()]).optional(),
14615
+ /** Root path for discovery, relative to CWD (or package root when pkgCwd is true). */
14209
14616
  rootPath: z$2.string().optional(),
14617
+ /** Space-delimited glob patterns used to discover directories. */
14210
14618
  globs: z$2.string().optional(),
14619
+ /** When true, resolve the batch root from the nearest package directory. */
14211
14620
  pkgCwd: z$2.boolean().optional(),
14212
14621
  });
14213
14622
 
@@ -14229,45 +14638,15 @@ const batchPlugin = (opts = {}) => {
14229
14638
  ns: 'batch',
14230
14639
  // Host validates this when config-loader is enabled; plugins may also
14231
14640
  // re-validate at action time as a safety belt.
14232
- configSchema: BatchConfigSchema,
14641
+ configSchema: batchPluginConfigSchema,
14233
14642
  setup(cli) {
14234
14643
  const batchCmd = cli; // mount provided by host
14235
- const GROUP = `plugin:${cli.name()}`;
14236
- batchCmd
14237
- .description('Batch command execution across multiple working directories.')
14238
- .enablePositionalOptions()
14239
- .passThroughOptions()
14240
- // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
14241
- .addOption((() => {
14242
- const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
14243
- cli.setOptionGroup(opt, GROUP);
14244
- return opt;
14245
- })())
14246
- .addOption((() => {
14247
- 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 || './')})`);
14248
- cli.setOptionGroup(opt, GROUP);
14249
- return opt;
14250
- })())
14251
- .addOption((() => {
14252
- const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
14253
- cli.setOptionGroup(opt, GROUP);
14254
- return opt;
14255
- })())
14256
- .option('-c, --command <string>', 'command executed according to the base shell resolution')
14257
- .option('-l, --list', 'list working directories without executing command')
14258
- .option('-e, --ignore-errors', 'ignore errors and continue with next path')
14259
- .argument('[command...]');
14260
- // Default subcommand "cmd" with contextual typing for args/opts
14261
- const cmdSub = new Command$1()
14262
- .name('cmd')
14263
- .description('execute command, conflicts with --command option (default subcommand)')
14264
- .enablePositionalOptions()
14265
- .passThroughOptions()
14266
- .argument('[command...]');
14267
- attachDefaultCmdAction$1(plugin, cli, batchCmd, opts, cmdSub);
14268
- batchCmd.addCommand(cmdSub, { isDefault: true });
14269
- // Parent invoker (unified naming)
14270
- attachParentInvoker$1(plugin, cli, opts, batchCmd);
14644
+ batchCmd.description('Batch command execution across multiple working directories.');
14645
+ attachBatchOptions(plugin, batchCmd);
14646
+ // Default subcommand `cmd` (mounted as batch default subcommand)
14647
+ attachBatchCmdSubcommand(plugin, cli, batchCmd, opts);
14648
+ // Default action for the batch command mount (parent flags and positional form)
14649
+ attachBatchDefaultAction(plugin, cli, opts, batchCmd);
14271
14650
  return undefined;
14272
14651
  },
14273
14652
  });
@@ -14361,14 +14740,11 @@ async function runCmdWithContext(cli, merged, command, _opts) {
14361
14740
  }
14362
14741
 
14363
14742
  /**
14364
- * Attach the default "cmd" subcommand action (unified name).
14743
+ * Attach the default "cmd" subcommand action.
14365
14744
  * Mirrors the prior inline implementation in cmd/index.ts.
14366
14745
  */
14367
- const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
14368
- cmd
14369
- .enablePositionalOptions()
14370
- .passThroughOptions()
14371
- .action(async function (...allArgs) {
14746
+ const attachCmdDefaultAction = (cli, cmd, aliasKey) => {
14747
+ cmd.action(async function (...allArgs) {
14372
14748
  // Commander passes: [...positionals, options, thisCommand]
14373
14749
  const thisCommand = allArgs[allArgs.length - 1];
14374
14750
  const commandParts = allArgs[0];
@@ -14401,11 +14777,30 @@ const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
14401
14777
  });
14402
14778
  };
14403
14779
 
14780
+ /**
14781
+ * Attach options/arguments for the cmd plugin mount.
14782
+ *
14783
+ * Note: the plugin description is owned by `src/plugins/cmd/index.ts` and must
14784
+ * not be set here.
14785
+ *
14786
+ * @param cli - The `cmd` command mount.
14787
+ * @returns The same `cli` instance for chaining.
14788
+ *
14789
+ * @internal
14790
+ */
14791
+ /** @hidden */
14792
+ function attachCmdOptions(cli) {
14793
+ return cli
14794
+ .enablePositionalOptions()
14795
+ .passThroughOptions()
14796
+ .argument('[command...]');
14797
+ }
14798
+
14404
14799
  /**
14405
14800
  * Install the parent-level invoker (alias) for the cmd plugin.
14406
14801
  * Unifies naming with batch attachParentInvoker; behavior unchanged.
14407
14802
  */
14408
- const attachParentInvoker = (cli, options, _cmd, plugin) => {
14803
+ const attachCmdParentInvoker = (cli, options, plugin) => {
14409
14804
  const dbg = (...args) => {
14410
14805
  if (process.env.GETDOTENV_DEBUG) {
14411
14806
  try {
@@ -14516,8 +14911,9 @@ const attachParentInvoker = (cli, options, _cmd, plugin) => {
14516
14911
  /**
14517
14912
  * Zod schema for cmd plugin configuration.
14518
14913
  */
14519
- const CmdConfigSchema = z$2
14914
+ const cmdPluginConfigSchema = z$2
14520
14915
  .object({
14916
+ /** When true, expand the alias value before execution (default behavior when omitted). */
14521
14917
  expand: z$2.boolean().optional(),
14522
14918
  })
14523
14919
  .strict();
@@ -14536,7 +14932,7 @@ const CmdConfigSchema = z$2
14536
14932
  const cmdPlugin = (options = {}) => {
14537
14933
  const plugin = definePlugin({
14538
14934
  ns: 'cmd',
14539
- configSchema: CmdConfigSchema,
14935
+ configSchema: cmdPluginConfigSchema,
14540
14936
  setup(cli) {
14541
14937
  const aliasSpec = typeof options.optionAlias === 'string'
14542
14938
  ? { flags: options.optionAlias}
@@ -14548,14 +14944,13 @@ const cmdPlugin = (options = {}) => {
14548
14944
  };
14549
14945
  const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
14550
14946
  // Mount is the command ('cmd'); attach default action.
14551
- cli
14552
- .description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
14553
- // Accept payload tokens as positional arguments for the default subcommand.
14554
- .argument('[command...]');
14555
- attachDefaultCmdAction(cli, cli, aliasKey);
14947
+ cli.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)');
14948
+ // Options/arguments (positional payload, argv routing) are attached separately.
14949
+ attachCmdOptions(cli);
14950
+ attachCmdDefaultAction(cli, cli, aliasKey);
14556
14951
  // Parent-attached option alias (optional, unified naming).
14557
14952
  if (aliasSpec !== undefined) {
14558
- attachParentInvoker(cli, options, cli, plugin);
14953
+ attachCmdParentInvoker(cli, options, plugin);
14559
14954
  }
14560
14955
  return undefined;
14561
14956
  },
@@ -14700,6 +15095,7 @@ const planConfigCopies = ({ format, withLocal, destRoot, }) => {
14700
15095
  const planCliCopies = ({ cliName, destRoot, }) => {
14701
15096
  const subs = { __CLI_NAME__: cliName };
14702
15097
  const base = path.join(destRoot, 'src', 'cli', cliName);
15098
+ const helloBase = path.join(base, 'plugins', 'hello');
14703
15099
  return [
14704
15100
  {
14705
15101
  src: path.join(TEMPLATES_ROOT, 'cli', 'index.ts'),
@@ -14707,8 +15103,28 @@ const planCliCopies = ({ cliName, destRoot, }) => {
14707
15103
  subs,
14708
15104
  },
14709
15105
  {
14710
- src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello.ts'),
14711
- dest: path.join(base, 'plugins', 'hello.ts'),
15106
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'index.ts'),
15107
+ dest: path.join(helloBase, 'index.ts'),
15108
+ subs,
15109
+ },
15110
+ {
15111
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'options.ts'),
15112
+ dest: path.join(helloBase, 'options.ts'),
15113
+ subs,
15114
+ },
15115
+ {
15116
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'defaultAction.ts'),
15117
+ dest: path.join(helloBase, 'defaultAction.ts'),
15118
+ subs,
15119
+ },
15120
+ {
15121
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'strangerAction.ts'),
15122
+ dest: path.join(helloBase, 'strangerAction.ts'),
15123
+ subs,
15124
+ },
15125
+ {
15126
+ src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'types.ts'),
15127
+ dest: path.join(helloBase, 'types.ts'),
14712
15128
  subs,
14713
15129
  },
14714
15130
  ];
@@ -14750,129 +15166,149 @@ const promptDecision = async (filePath, logger, rl) => {
14750
15166
  };
14751
15167
 
14752
15168
  /**
14753
- * @packageDocumentation
14754
- * Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
14755
- * host‑based CLI skeleton with collision handling and CI‑safe defaults.
14756
- */
14757
- /**
14758
- * Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
14759
- * Supports collision detection, interactive prompts, and CI bypass.
15169
+ * Attach the init plugin default action.
15170
+ *
15171
+ * @param cli - The `init` command mount (with args/options attached).
15172
+ *
15173
+ * @internal
14760
15174
  */
14761
- const initPlugin = () => definePlugin({
14762
- ns: 'init',
14763
- setup(cli) {
14764
- cli
14765
- .description('Scaffold getdotenv config files and a host-based CLI skeleton.')
14766
- .argument('[dest]', 'destination path (default: ./)', '.')
14767
- .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
14768
- .option('--with-local', 'include .local config variant')
14769
- .option('--dynamic', 'include dynamic examples (JS/TS configs)')
14770
- .option('--cli-name <string>', 'CLI name for skeleton and tokens')
14771
- .option('--force', 'overwrite all existing files')
14772
- .option('--yes', 'skip all collisions (no overwrite)')
14773
- .action(async (destArg, opts, thisCommand) => {
14774
- // Inherit logger from merged root options (base).
14775
- const bag = readMergedOptions(thisCommand);
14776
- const logger = bag.logger;
14777
- const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
14778
- const cwd = process.cwd();
14779
- const destRoot = path.resolve(cwd, destRel);
14780
- const formatInput = opts.configFormat;
14781
- const formatRaw = typeof formatInput === 'string'
14782
- ? formatInput.toLowerCase()
14783
- : 'json';
14784
- const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw)
14785
- ? formatRaw
14786
- : 'json');
14787
- const withLocal = !!opts.withLocal;
14788
- // dynamic flag reserved for future template variants; present for UX compatibility
14789
- void opts.dynamic;
14790
- // CLI name default: --cli-name | basename(dest) | 'mycli'
14791
- const cliName = (typeof opts.cliName === 'string' && opts.cliName.length > 0
14792
- ? opts.cliName
14793
- : path.basename(destRoot) || 'mycli') || 'mycli';
14794
- // Precedence: --force > --yes > auto-detect(non-interactive => yes)
14795
- const force = !!opts.force;
14796
- const yes = !!opts.yes || (!force && isNonInteractive());
14797
- // Build copy plan
14798
- const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
14799
- const cliCopies = planCliCopies({ cliName, destRoot });
14800
- const copies = [...cfgCopies, ...cliCopies];
14801
- // Interactive state
14802
- let globalDecision;
14803
- const rl = createInterface({ input: stdin, output: stdout });
14804
- try {
14805
- for (const item of copies) {
14806
- const exists = await fs.pathExists(item.dest);
14807
- if (!exists) {
14808
- const subs = item.subs ?? {};
14809
- await copyTextFile(item.src, item.dest, subs);
14810
- logger.log(`Created ${path.relative(cwd, item.dest)}`);
14811
- continue;
14812
- }
14813
- // Collision
14814
- if (force) {
14815
- const subs = item.subs ?? {};
14816
- await copyTextFile(item.src, item.dest, subs);
14817
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
14818
- continue;
14819
- }
14820
- if (yes) {
14821
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14822
- continue;
14823
- }
14824
- let decision = globalDecision;
14825
- if (!decision) {
14826
- const a = await promptDecision(item.dest, logger, rl);
14827
- if (a === 'O') {
14828
- globalDecision = 'overwrite';
14829
- decision = 'overwrite';
14830
- }
14831
- else if (a === 'E') {
14832
- globalDecision = 'example';
14833
- decision = 'example';
14834
- }
14835
- else if (a === 'S') {
14836
- globalDecision = 'skip';
14837
- decision = 'skip';
14838
- }
14839
- else {
14840
- decision =
14841
- a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
14842
- }
15175
+ function attachInitDefaultAction(cli) {
15176
+ cli.action(async (destArg, opts, thisCommand) => {
15177
+ // Inherit logger from merged root options (base).
15178
+ const bag = readMergedOptions(thisCommand);
15179
+ const logger = bag.logger;
15180
+ const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
15181
+ const cwd = process.cwd();
15182
+ const destRoot = path.resolve(cwd, destRel);
15183
+ const formatInput = opts['configFormat'];
15184
+ const formatRaw = typeof formatInput === 'string' ? formatInput.toLowerCase() : 'json';
15185
+ const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw) ? formatRaw : 'json');
15186
+ const withLocal = Boolean(opts['withLocal']);
15187
+ // dynamic flag reserved for future template variants; present for UX compatibility
15188
+ void opts['dynamic'];
15189
+ // CLI name default: --cli-name | basename(dest) | 'mycli'
15190
+ const cliNameInput = opts['cliName'];
15191
+ const cliName = (typeof cliNameInput === 'string' && cliNameInput.length > 0
15192
+ ? cliNameInput
15193
+ : path.basename(destRoot) || 'mycli') || 'mycli';
15194
+ // Precedence: --force > --yes > auto-detect(non-interactive => yes)
15195
+ const force = Boolean(opts['force']);
15196
+ const yes = Boolean(opts['yes']) || (!force && isNonInteractive());
15197
+ // Build copy plan
15198
+ const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
15199
+ const cliCopies = planCliCopies({ cliName, destRoot });
15200
+ const copies = [...cfgCopies, ...cliCopies];
15201
+ // Interactive state
15202
+ let globalDecision;
15203
+ const rl = createInterface({ input: stdin, output: stdout });
15204
+ try {
15205
+ for (const item of copies) {
15206
+ const exists = await fs.pathExists(item.dest);
15207
+ if (!exists) {
15208
+ const subs = item.subs ?? {};
15209
+ await copyTextFile(item.src, item.dest, subs);
15210
+ logger.log(`Created ${path.relative(cwd, item.dest)}`);
15211
+ continue;
15212
+ }
15213
+ // Collision
15214
+ if (force) {
15215
+ const subs = item.subs ?? {};
15216
+ await copyTextFile(item.src, item.dest, subs);
15217
+ logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
15218
+ continue;
15219
+ }
15220
+ if (yes) {
15221
+ logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
15222
+ continue;
15223
+ }
15224
+ let decision = globalDecision;
15225
+ if (!decision) {
15226
+ const a = await promptDecision(item.dest, logger, rl);
15227
+ if (a === 'O') {
15228
+ globalDecision = 'overwrite';
15229
+ decision = 'overwrite';
14843
15230
  }
14844
- if (decision === 'overwrite') {
14845
- const subs = item.subs ?? {};
14846
- await copyTextFile(item.src, item.dest, subs);
14847
- logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
15231
+ else if (a === 'E') {
15232
+ globalDecision = 'example';
15233
+ decision = 'example';
14848
15234
  }
14849
- else if (decision === 'example') {
14850
- const destEx = `${item.dest}.example`;
14851
- const subs = item.subs ?? {};
14852
- await copyTextFile(item.src, destEx, subs);
14853
- logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
15235
+ else if (a === 'S') {
15236
+ globalDecision = 'skip';
15237
+ decision = 'skip';
14854
15238
  }
14855
15239
  else {
14856
- logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
15240
+ decision = a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
14857
15241
  }
14858
15242
  }
14859
- // Ensure .gitignore includes local config patterns.
14860
- const giPath = path.join(destRoot, '.gitignore');
14861
- const { created, changed } = await ensureLines(giPath, [
14862
- 'getdotenv.config.local.*',
14863
- '*.local',
14864
- ]);
14865
- if (created) {
14866
- logger.log(`Created ${path.relative(cwd, giPath)}`);
15243
+ if (decision === 'overwrite') {
15244
+ const subs = item.subs ?? {};
15245
+ await copyTextFile(item.src, item.dest, subs);
15246
+ logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
15247
+ }
15248
+ else if (decision === 'example') {
15249
+ const destEx = `${item.dest}.example`;
15250
+ const subs = item.subs ?? {};
15251
+ await copyTextFile(item.src, destEx, subs);
15252
+ logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
14867
15253
  }
14868
- else if (changed) {
14869
- logger.log(`Updated ${path.relative(cwd, giPath)}`);
15254
+ else {
15255
+ logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
14870
15256
  }
14871
15257
  }
14872
- finally {
14873
- rl.close();
15258
+ // Ensure .gitignore includes local config patterns.
15259
+ const giPath = path.join(destRoot, '.gitignore');
15260
+ const { created, changed } = await ensureLines(giPath, [
15261
+ 'getdotenv.config.local.*',
15262
+ '*.local',
15263
+ ]);
15264
+ if (created) {
15265
+ logger.log(`Created ${path.relative(cwd, giPath)}`);
14874
15266
  }
14875
- });
15267
+ else if (changed) {
15268
+ logger.log(`Updated ${path.relative(cwd, giPath)}`);
15269
+ }
15270
+ }
15271
+ finally {
15272
+ rl.close();
15273
+ }
15274
+ });
15275
+ }
15276
+
15277
+ /**
15278
+ * Attach options/arguments for the init plugin mount.
15279
+ *
15280
+ * @param cli - The `init` command mount.
15281
+ *
15282
+ * @internal
15283
+ */
15284
+ /** @hidden */
15285
+ function attachInitOptions(cli) {
15286
+ return (cli
15287
+ // Description is owned by the plugin index (src/plugins/init/index.ts).
15288
+ .argument('[dest]', 'destination path (default: ./)', '.')
15289
+ .option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
15290
+ .option('--with-local', 'include .local config variant')
15291
+ .option('--dynamic', 'include dynamic examples (JS/TS configs)')
15292
+ .option('--cli-name <string>', 'CLI name for skeleton and tokens')
15293
+ .option('--force', 'overwrite all existing files')
15294
+ .option('--yes', 'skip all collisions (no overwrite)'));
15295
+ }
15296
+
15297
+ /**
15298
+ * @packageDocumentation
15299
+ * Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
15300
+ * host‑based CLI skeleton with collision handling and CI‑safe defaults.
15301
+ */
15302
+ /**
15303
+ * Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
15304
+ * Supports collision detection, interactive prompts, and CI bypass.
15305
+ */
15306
+ const initPlugin = () => definePlugin({
15307
+ ns: 'init',
15308
+ setup(cli) {
15309
+ cli.description('Scaffold getdotenv config files and a host-based CLI skeleton.');
15310
+ const initCmd = attachInitOptions(cli);
15311
+ attachInitDefaultAction(initCmd);
14876
15312
  return undefined;
14877
15313
  },
14878
15314
  });