@karmaniverous/get-dotenv 5.2.6 → 6.0.0-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 (55) hide show
  1. package/README.md +106 -70
  2. package/dist/cliHost.d.ts +232 -226
  3. package/dist/cliHost.mjs +777 -545
  4. package/dist/config.d.ts +7 -2
  5. package/dist/env-overlay.d.ts +21 -9
  6. package/dist/env-overlay.mjs +14 -19
  7. package/dist/getdotenv.cli.mjs +1366 -1163
  8. package/dist/index.d.ts +415 -242
  9. package/dist/index.mjs +1364 -1414
  10. package/dist/plugins-aws.d.ts +149 -94
  11. package/dist/plugins-aws.mjs +307 -195
  12. package/dist/plugins-batch.d.ts +153 -99
  13. package/dist/plugins-batch.mjs +277 -95
  14. package/dist/plugins-cmd.d.ts +140 -94
  15. package/dist/plugins-cmd.mjs +636 -502
  16. package/dist/plugins-demo.d.ts +140 -94
  17. package/dist/plugins-demo.mjs +237 -46
  18. package/dist/plugins-init.d.ts +140 -94
  19. package/dist/plugins-init.mjs +129 -12
  20. package/dist/plugins.d.ts +166 -103
  21. package/dist/plugins.mjs +977 -840
  22. package/package.json +15 -53
  23. package/templates/cli/ts/plugins/hello.ts +27 -6
  24. package/templates/config/js/getdotenv.config.js +1 -1
  25. package/templates/config/ts/getdotenv.config.ts +9 -2
  26. package/dist/cliHost.cjs +0 -1875
  27. package/dist/cliHost.d.cts +0 -409
  28. package/dist/cliHost.d.mts +0 -409
  29. package/dist/config.cjs +0 -252
  30. package/dist/config.d.cts +0 -55
  31. package/dist/config.d.mts +0 -55
  32. package/dist/env-overlay.cjs +0 -163
  33. package/dist/env-overlay.d.cts +0 -50
  34. package/dist/env-overlay.d.mts +0 -50
  35. package/dist/index.cjs +0 -4140
  36. package/dist/index.d.cts +0 -457
  37. package/dist/index.d.mts +0 -457
  38. package/dist/plugins-aws.cjs +0 -667
  39. package/dist/plugins-aws.d.cts +0 -158
  40. package/dist/plugins-aws.d.mts +0 -158
  41. package/dist/plugins-batch.cjs +0 -616
  42. package/dist/plugins-batch.d.cts +0 -180
  43. package/dist/plugins-batch.d.mts +0 -180
  44. package/dist/plugins-cmd.cjs +0 -1113
  45. package/dist/plugins-cmd.d.cts +0 -178
  46. package/dist/plugins-cmd.d.mts +0 -178
  47. package/dist/plugins-demo.cjs +0 -307
  48. package/dist/plugins-demo.d.cts +0 -158
  49. package/dist/plugins-demo.d.mts +0 -158
  50. package/dist/plugins-init.cjs +0 -289
  51. package/dist/plugins-init.d.cts +0 -162
  52. package/dist/plugins-init.d.mts +0 -162
  53. package/dist/plugins.cjs +0 -2283
  54. package/dist/plugins.d.cts +0 -210
  55. package/dist/plugins.d.mts +0 -210
@@ -1,9 +1,20 @@
1
1
  import { execa, execaCommand } from 'execa';
2
2
  import { z } from 'zod';
3
+ import 'fs-extra';
4
+ import 'path';
5
+ import 'package-directory';
6
+ import 'url';
7
+ import 'yaml';
8
+ import 'nanoid';
9
+ import 'dotenv';
10
+ import 'crypto';
3
11
 
4
12
  // Minimal tokenizer for shell-off execution:
5
13
  // Splits by whitespace while preserving quoted segments (single or double quotes).
6
- const tokenize = (command) => {
14
+ // Optionally preserve doubled quotes inside quoted segments:
15
+ // - default: "" => " (Windows/PowerShell style literal-quote escape)
16
+ // - preserveDoubledQuotes: true => "" stays "" (needed for Node -e payloads)
17
+ const tokenize = (command, opts) => {
7
18
  const out = [];
8
19
  let cur = '';
9
20
  let quote = null;
@@ -11,12 +22,16 @@ const tokenize = (command) => {
11
22
  const c = command.charAt(i);
12
23
  if (quote) {
13
24
  if (c === quote) {
14
- // Support doubled quotes inside a quoted segment (Windows/PowerShell style):
15
- // "" -> " and '' -> '
25
+ // Support doubled quotes inside a quoted segment:
26
+ // default: "" -> " and '' -> ' (Windows/PowerShell style)
27
+ // preserve: keep as "" to allow empty string literals in Node -e payloads
16
28
  const next = command.charAt(i + 1);
17
29
  if (next === quote) {
18
- cur += quote;
19
- i += 1; // skip the second quote
30
+ {
31
+ // Collapse to a single literal quote
32
+ cur += quote;
33
+ i += 1; // skip the second quote
34
+ }
20
35
  }
21
36
  else {
22
37
  // end of quoted segment
@@ -90,62 +105,55 @@ const sanitizeEnv = (env) => {
90
105
  const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
91
106
  return entries.length > 0 ? Object.fromEntries(entries) : undefined;
92
107
  };
93
- /**
94
- * Execute a command and capture stdout/stderr (buffered).
95
- * - Preserves plain vs shell behavior and argv/string normalization.
96
- * - Never re-emits stdout/stderr to parent; returns captured buffers.
97
- * - Supports optional timeout (ms).
98
- */
99
- const runCommandResult = async (command, shell, opts = {}) => {
108
+ async function runCommandResult(command, shell, opts = {}) {
100
109
  const envSan = sanitizeEnv(opts.env);
101
110
  {
102
111
  let file;
103
112
  let args = [];
104
- if (Array.isArray(command)) {
105
- file = command[0];
106
- args = command.slice(1).map(stripOuterQuotes);
107
- }
108
- else {
113
+ if (typeof command === 'string') {
109
114
  const tokens = tokenize(command);
110
115
  file = tokens[0];
111
116
  args = tokens.slice(1);
112
117
  }
118
+ else {
119
+ file = command[0];
120
+ args = command.slice(1).map(stripOuterQuotes);
121
+ }
113
122
  if (!file)
114
123
  return { exitCode: 0, stdout: '', stderr: '' };
115
124
  dbg('exec:capture (plain)', { file, args });
116
125
  try {
117
- const result = await execa(file, args, {
126
+ const ok = pickResult((await execa(file, args, {
118
127
  ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
119
128
  ...(envSan !== undefined ? { env: envSan } : {}),
120
129
  stdio: 'pipe',
121
130
  ...(opts.timeoutMs !== undefined
122
131
  ? { timeout: opts.timeoutMs, killSignal: 'SIGKILL' }
123
132
  : {}),
124
- });
125
- const ok = pickResult(result);
133
+ })));
126
134
  dbg('exit:capture (plain)', { exitCode: ok.exitCode });
127
135
  return ok;
128
136
  }
129
- catch (err) {
130
- const out = pickResult(err);
137
+ catch (e) {
138
+ const out = pickResult(e);
131
139
  dbg('exit:capture:error (plain)', { exitCode: out.exitCode });
132
140
  return out;
133
141
  }
134
142
  }
135
- };
136
- const runCommand = async (command, shell, opts) => {
143
+ }
144
+ async function runCommand(command, shell, opts) {
137
145
  if (shell === false) {
138
146
  let file;
139
147
  let args = [];
140
- if (Array.isArray(command)) {
141
- file = command[0];
142
- args = command.slice(1).map(stripOuterQuotes);
143
- }
144
- else {
148
+ if (typeof command === 'string') {
145
149
  const tokens = tokenize(command);
146
150
  file = tokens[0];
147
151
  args = tokens.slice(1);
148
152
  }
153
+ else {
154
+ file = command[0];
155
+ args = command.slice(1).map(stripOuterQuotes);
156
+ }
149
157
  if (!file)
150
158
  return 0;
151
159
  dbg('exec (plain)', { file, args, stdio: opts.stdio });
@@ -158,16 +166,15 @@ const runCommand = async (command, shell, opts) => {
158
166
  plainOpts.env = envSan;
159
167
  if (opts.stdio !== undefined)
160
168
  plainOpts.stdio = opts.stdio;
161
- const result = await execa(file, args, plainOpts);
162
- if (opts.stdio === 'pipe' && result.stdout) {
163
- process.stdout.write(result.stdout + (result.stdout.endsWith('\n') ? '' : '\n'));
169
+ const ok = pickResult((await execa(file, args, plainOpts)));
170
+ if (opts.stdio === 'pipe' && ok.stdout) {
171
+ process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
164
172
  }
165
- const exit = result?.exitCode;
166
- dbg('exit (plain)', { exitCode: exit });
167
- return typeof exit === 'number' ? exit : Number.NaN;
173
+ dbg('exit (plain)', { exitCode: ok.exitCode });
174
+ return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
168
175
  }
169
176
  else {
170
- const commandStr = Array.isArray(command) ? command.join(' ') : command;
177
+ const commandStr = typeof command === 'string' ? command : command.join(' ');
171
178
  dbg('exec (shell)', {
172
179
  shell: typeof shell === 'string' ? shell : 'custom',
173
180
  stdio: opts.stdio,
@@ -181,17 +188,29 @@ const runCommand = async (command, shell, opts) => {
181
188
  shellOpts.env = envSan;
182
189
  if (opts.stdio !== undefined)
183
190
  shellOpts.stdio = opts.stdio;
184
- const result = await execaCommand(commandStr, shellOpts);
185
- const out = result?.stdout;
186
- if (opts.stdio === 'pipe' && out) {
187
- process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
191
+ const ok = pickResult((await execaCommand(commandStr, shellOpts)));
192
+ if (opts.stdio === 'pipe' && ok.stdout) {
193
+ process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
188
194
  }
189
- const exit = result?.exitCode;
190
- dbg('exit (shell)', { exitCode: exit });
191
- return typeof exit === 'number' ? exit : Number.NaN;
195
+ dbg('exit (shell)', { exitCode: ok.exitCode });
196
+ return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
192
197
  }
193
- };
198
+ }
194
199
 
200
+ /** src/cliCore/spawnEnv.ts
201
+ * Build a sanitized environment bag for child processes.
202
+ *
203
+ * Requirements addressed:
204
+ * - Provide a single helper (buildSpawnEnv) to normalize/dedupe child env.
205
+ * - Drop undefined values (exactOptional semantics).
206
+ * - On Windows, dedupe keys case-insensitively and prefer the last value,
207
+ * preserving the latest key's casing. Ensure HOME fallback from USERPROFILE.
208
+ * Normalize TMP/TEMP consistency when either is present.
209
+ * - On POSIX, keep keys as-is; when a temp dir key is present (TMPDIR/TMP/TEMP),
210
+ * ensure TMPDIR exists for downstream consumers that expect it.
211
+ *
212
+ * Adapter responsibility: pure mapping; no business logic.
213
+ */
195
214
  const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
196
215
  /** Build a sanitized env for child processes from base + overlay. */
197
216
  const buildSpawnEnv = (base, overlay) => {
@@ -234,6 +253,83 @@ const buildSpawnEnv = (base, overlay) => {
234
253
  return out;
235
254
  };
236
255
 
256
+ /**
257
+ * Zod schemas for configuration files discovered by the new loader.
258
+ *
259
+ * Notes:
260
+ * - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
261
+ * - RESOLVED: normalized shapes (paths always string[]).
262
+ * - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
263
+ */
264
+ // String-only env value map
265
+ const stringMap = z.record(z.string(), z.string());
266
+ const envStringMap = z.record(z.string(), stringMap);
267
+ // Allow string[] or single string for "paths" in RAW; normalize later.
268
+ const rawPathsSchema = z.union([z.array(z.string()), z.string()]).optional();
269
+ const getDotenvConfigSchemaRaw = z.object({
270
+ dotenvToken: z.string().optional(),
271
+ privateToken: z.string().optional(),
272
+ paths: rawPathsSchema,
273
+ loadProcess: z.boolean().optional(),
274
+ log: z.boolean().optional(),
275
+ shell: z.union([z.string(), z.boolean()]).optional(),
276
+ scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
277
+ requiredKeys: z.array(z.string()).optional(),
278
+ schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
279
+ vars: stringMap.optional(), // public, global
280
+ envVars: envStringMap.optional(), // public, per-env
281
+ // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
282
+ dynamic: z.unknown().optional(),
283
+ // Per-plugin config bag; validated by plugins/host when used.
284
+ plugins: z.record(z.string(), z.unknown()).optional(),
285
+ });
286
+ // Normalize paths to string[]
287
+ const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
288
+ getDotenvConfigSchemaRaw.transform((raw) => ({
289
+ ...raw,
290
+ paths: normalizePaths(raw.paths),
291
+ }));
292
+
293
+ /**
294
+ * Zod schemas for programmatic GetDotenv options.
295
+ *
296
+ * Canonical source of truth for options shape. Public types are derived
297
+ * from these schemas (see consumers via z.output\<\>).
298
+ */
299
+ // Minimal process env representation: string values or undefined to indicate "unset".
300
+ const processEnvSchema = z.record(z.string(), z.string().optional());
301
+ // RAW: all fields optional — undefined means "inherit" from lower layers.
302
+ z.object({
303
+ defaultEnv: z.string().optional(),
304
+ dotenvToken: z.string().optional(),
305
+ dynamicPath: z.string().optional(),
306
+ // Dynamic map is intentionally wide for now; refine once sources are normalized.
307
+ dynamic: z.record(z.string(), z.unknown()).optional(),
308
+ env: z.string().optional(),
309
+ excludeDynamic: z.boolean().optional(),
310
+ excludeEnv: z.boolean().optional(),
311
+ excludeGlobal: z.boolean().optional(),
312
+ excludePrivate: z.boolean().optional(),
313
+ excludePublic: z.boolean().optional(),
314
+ loadProcess: z.boolean().optional(),
315
+ log: z.boolean().optional(),
316
+ logger: z.unknown().optional(),
317
+ outputPath: z.string().optional(),
318
+ paths: z.array(z.string()).optional(),
319
+ privateToken: z.string().optional(),
320
+ vars: processEnvSchema.optional(),
321
+ });
322
+
323
+ /**
324
+ * Instance-bound plugin config store.
325
+ * Host stores the validated/interpolated slice per plugin instance.
326
+ * The store is intentionally private to this module; definePlugin()
327
+ * provides a typed accessor that reads from this store for the calling
328
+ * plugin instance.
329
+ */
330
+ const PLUGIN_CONFIG_STORE = new WeakMap();
331
+ const _getPluginConfigForInstance = (plugin) => PLUGIN_CONFIG_STORE.get(plugin);
332
+
237
333
  /** src/cliHost/definePlugin.ts
238
334
  * Plugin contracts for the GetDotenv CLI host.
239
335
  *
@@ -241,26 +337,59 @@ const buildSpawnEnv = (base, overlay) => {
241
337
  * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
242
338
  * nominal class identity issues (private fields) in downstream consumers.
243
339
  */
244
- /**
245
- * Define a GetDotenv CLI plugin with compositional helpers.
246
- *
247
- * @example
248
- * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
249
- * .use(childA)
250
- * .use(childB);
251
- */
252
- const definePlugin = (spec) => {
340
+ /* eslint-disable tsdoc/syntax */
341
+ function definePlugin(spec) {
253
342
  const { children = [], ...rest } = spec;
254
- const plugin = {
343
+ // Default to a strict empty-object schema so “no-config” plugins fail fast
344
+ // on unknown keys and provide a concrete {} at runtime.
345
+ const effectiveSchema = spec.configSchema ?? z.object({}).strict();
346
+ // Build base plugin first, then extend with instance-bound helpers.
347
+ const base = {
255
348
  ...rest,
349
+ // Always carry a schema (strict empty by default) to simplify host logic
350
+ // and improve inference/ergonomics for plugin authors.
351
+ configSchema: effectiveSchema,
256
352
  children: [...children],
257
353
  use(child) {
258
354
  this.children.push(child);
259
355
  return this;
260
356
  },
261
357
  };
262
- return plugin;
263
- };
358
+ // Attach instance-bound helpers on the returned plugin object.
359
+ const extended = base;
360
+ extended.readConfig = function (_cli) {
361
+ // Config is stored per-plugin-instance by the host (WeakMap in computeContext).
362
+ const value = _getPluginConfigForInstance(extended);
363
+ if (value === undefined) {
364
+ // Guard: host has not resolved config yet (incorrect lifecycle usage).
365
+ throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
366
+ }
367
+ return value;
368
+ };
369
+ // Plugin-bound dynamic option factory
370
+ extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
371
+ return cli.createDynamicOption(flags, (cfg) => {
372
+ // Prefer the validated slice stored per instance; fallback to help-bag
373
+ // (by-id) so top-level `-h` can render effective defaults before resolve.
374
+ const fromStore = _getPluginConfigForInstance(extended);
375
+ const id = extended.id;
376
+ let fromBag;
377
+ if (!fromStore && id) {
378
+ const maybe = cfg.plugins[id];
379
+ if (maybe && typeof maybe === 'object') {
380
+ fromBag = maybe;
381
+ }
382
+ }
383
+ // Always provide a concrete object to dynamic callbacks:
384
+ // - With a schema: computeContext stores the parsed object.
385
+ // - Without a schema: computeContext stores {}.
386
+ // - Help-time fallback: coalesce to {} when only a by-id bag exists.
387
+ const cfgVal = (fromStore ?? fromBag ?? {});
388
+ return desc(cfg, cfgVal);
389
+ }, parser, defaultValue);
390
+ };
391
+ return extended;
392
+ }
264
393
 
265
394
  /**
266
395
  * Batch services (neutral): resolve command and shell settings.
@@ -481,90 +610,79 @@ const AwsPluginConfigSchema = z.object({
481
610
  regionKey: z.string().default('AWS_REGION').optional(),
482
611
  strategy: z.enum(['cli-export', 'none']).default('cli-export').optional(),
483
612
  loginOnDemand: z.boolean().default(false).optional(),
484
- setEnv: z.boolean().default(true).optional(),
485
- addCtx: z.boolean().default(true).optional(),
486
613
  });
487
614
 
488
- const awsPlugin = () => definePlugin({
489
- id: 'aws',
490
- // Host validates this slice when the loader path is active.
491
- configSchema: AwsPluginConfigSchema,
492
- setup(cli) {
493
- // Subcommand: aws
494
- cli
495
- .ns('aws')
496
- .description('Establish an AWS session and optionally forward to the AWS CLI')
497
- .configureHelp({ showGlobalOptions: true })
498
- .enablePositionalOptions()
499
- .passThroughOptions()
500
- .allowUnknownOption(true)
501
- // Boolean toggles
502
- .option('--login-on-demand', 'attempt aws sso login on-demand')
503
- .option('--no-login-on-demand', 'disable sso login on-demand')
504
- .option('--set-env', 'write resolved values into process.env')
505
- .option('--no-set-env', 'do not write resolved values into process.env')
506
- .option('--add-ctx', 'mirror results under ctx.plugins.aws')
507
- .option('--no-add-ctx', 'do not mirror results under ctx.plugins.aws')
508
- // Strings / enums
509
- .option('--profile <string>', 'AWS profile name')
510
- .option('--region <string>', 'AWS region')
511
- .option('--default-region <string>', 'fallback region')
512
- .option('--strategy <string>', 'credential acquisition strategy: cli-export|none')
513
- // Advanced key overrides
514
- .option('--profile-key <string>', 'dotenv/config key for local profile')
515
- .option('--profile-fallback-key <string>', 'fallback dotenv/config key for profile')
516
- .option('--region-key <string>', 'dotenv/config key for region')
517
- // Accept any extra operands so Commander does not error when tokens appear after "--".
518
- .argument('[args...]')
519
- .action(async (args, opts, thisCommand) => {
520
- const self = thisCommand;
521
- const parent = (self.parent ?? null);
522
- // Access merged root CLI options (installed by passOptions())
523
- const rootOpts = (parent?.getDotenvCliOptions ?? {});
524
- const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
525
- Boolean(rootOpts?.capture);
526
- const underTests = process.env.GETDOTENV_TEST === '1' ||
527
- typeof process.env.VITEST_WORKER_ID === 'string';
528
- // Build overlay cfg from subcommand flags layered over discovered config.
529
- const ctx = cli.getCtx();
530
- const cfgBase = (ctx?.pluginConfigs?.['aws'] ??
531
- {});
532
- const overlay = {};
533
- // Map boolean toggles (respect explicit --no-*)
534
- if (Object.prototype.hasOwnProperty.call(opts, 'loginOnDemand'))
535
- overlay.loginOnDemand = Boolean(opts.loginOnDemand);
536
- if (Object.prototype.hasOwnProperty.call(opts, 'setEnv'))
537
- overlay.setEnv = Boolean(opts.setEnv);
538
- if (Object.prototype.hasOwnProperty.call(opts, 'addCtx'))
539
- overlay.addCtx = Boolean(opts.addCtx);
540
- // Strings/enums
541
- if (typeof opts.profile === 'string')
542
- overlay.profile = opts.profile;
543
- if (typeof opts.region === 'string')
544
- overlay.region = opts.region;
545
- if (typeof opts.defaultRegion === 'string')
546
- overlay.defaultRegion = opts.defaultRegion;
547
- if (typeof opts.strategy === 'string')
548
- overlay.strategy =
549
- opts.strategy;
550
- // Advanced key overrides
551
- if (typeof opts.profileKey === 'string')
552
- overlay.profileKey = opts.profileKey;
553
- if (typeof opts.profileFallbackKey === 'string')
554
- overlay.profileFallbackKey = opts.profileFallbackKey;
555
- if (typeof opts.regionKey === 'string')
556
- overlay.regionKey = opts.regionKey;
557
- const cfg = {
558
- ...cfgBase,
559
- ...overlay,
560
- };
561
- // Resolve current context with overrides
562
- const out = await resolveAwsContext({
563
- dotenv: ctx?.dotenv ?? {},
564
- cfg,
565
- });
566
- // Apply env/ctx mirrors per toggles
567
- if (cfg.setEnv !== false) {
615
+ const awsPlugin = () => {
616
+ const plugin = definePlugin({
617
+ id: 'aws',
618
+ // Host validates this slice when the loader path is active.
619
+ configSchema: AwsPluginConfigSchema,
620
+ setup(cli) {
621
+ // Subcommand: aws
622
+ cli
623
+ .ns('aws')
624
+ .description('Establish an AWS session and optionally forward to the AWS CLI')
625
+ .enablePositionalOptions()
626
+ .passThroughOptions()
627
+ .allowUnknownOption(true)
628
+ // Boolean toggles with dynamic help labels (effective defaults)
629
+ .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
630
+ .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
631
+ // Strings / enums
632
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
633
+ .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
634
+ .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
635
+ .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
636
+ // Advanced key overrides
637
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
638
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
639
+ .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
640
+ // Accept any extra operands so Commander does not error when tokens appear after "--".
641
+ .argument('[args...]')
642
+ .action(async (args, opts, thisCommand) => {
643
+ const pluginInst = plugin;
644
+ const cmdSelf = thisCommand;
645
+ const parent = (cmdSelf.parent ?? null);
646
+ // Access merged root CLI options (installed by passOptions())
647
+ const rootOpts = (parent?.getDotenvCliOptions ?? {});
648
+ const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
649
+ Boolean(rootOpts?.capture);
650
+ const underTests = process.env.GETDOTENV_TEST === '1' ||
651
+ typeof process.env.VITEST_WORKER_ID === 'string';
652
+ // Build overlay cfg from subcommand flags layered over discovered config.
653
+ const ctx = cli.getCtx();
654
+ const cfgBase = pluginInst.readConfig(cli);
655
+ const o = opts;
656
+ const overlay = {};
657
+ // Map boolean toggles (respect explicit --no-*)
658
+ if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
659
+ overlay.loginOnDemand = Boolean(o.loginOnDemand);
660
+ // Strings/enums
661
+ if (typeof o.profile === 'string')
662
+ overlay.profile = o.profile;
663
+ if (typeof o.region === 'string')
664
+ overlay.region = o.region;
665
+ if (typeof o.defaultRegion === 'string')
666
+ overlay.defaultRegion = o.defaultRegion;
667
+ if (typeof o.strategy === 'string')
668
+ overlay.strategy = o.strategy;
669
+ // Advanced key overrides
670
+ if (typeof o.profileKey === 'string')
671
+ overlay.profileKey = o.profileKey;
672
+ if (typeof o.profileFallbackKey === 'string')
673
+ overlay.profileFallbackKey = o.profileFallbackKey;
674
+ if (typeof o.regionKey === 'string')
675
+ overlay.regionKey = o.regionKey;
676
+ const cfg = {
677
+ ...cfgBase,
678
+ ...overlay,
679
+ };
680
+ // Resolve current context with overrides
681
+ const out = await resolveAwsContext({
682
+ dotenv: ctx?.dotenv ?? {},
683
+ cfg,
684
+ });
685
+ // Unconditional env writes (no per-plugin toggle)
568
686
  if (out.region) {
569
687
  process.env.AWS_REGION = out.region;
570
688
  if (!process.env.AWS_DEFAULT_REGION)
@@ -578,58 +696,53 @@ const awsPlugin = () => definePlugin({
578
696
  process.env.AWS_SESSION_TOKEN = out.credentials.sessionToken;
579
697
  }
580
698
  }
581
- }
582
- if (cfg.addCtx !== false) {
699
+ // Always publish minimal non-sensitive metadata
583
700
  if (ctx) {
584
701
  ctx.plugins ??= {};
585
702
  ctx.plugins['aws'] = {
586
703
  ...(out.profile ? { profile: out.profile } : {}),
587
704
  ...(out.region ? { region: out.region } : {}),
588
- ...(out.credentials ? { credentials: out.credentials } : {}),
589
705
  };
590
706
  }
591
- }
592
- // Forward when positional args are present; otherwise session-only.
593
- if (Array.isArray(args) && args.length > 0) {
594
- const argv = ['aws', ...args];
595
- const shellSetting = resolveShell(rootOpts?.scripts, 'aws', rootOpts?.shell);
596
- const ctxDotenv = (ctx?.dotenv ?? {});
597
- const exit = await runCommand(argv, shellSetting, {
598
- env: buildSpawnEnv(process.env, ctxDotenv),
599
- stdio: capture ? 'pipe' : 'inherit',
600
- });
601
- // Deterministic termination (suppressed under tests)
602
- if (!underTests) {
603
- process.exit(typeof exit === 'number' ? exit : 0);
604
- }
605
- return;
606
- }
607
- else {
608
- // Session only: low-noise breadcrumb under debug
609
- if (process.env.GETDOTENV_DEBUG) {
610
- const log = console;
611
- log.log('[aws] session established', {
612
- profile: out.profile,
613
- region: out.region,
614
- hasCreds: Boolean(out.credentials),
707
+ // Forward when positional args are present; otherwise session-only.
708
+ if (Array.isArray(args) && args.length > 0) {
709
+ const argv = ['aws', ...args];
710
+ const shellSetting = resolveShell(rootOpts?.scripts, 'aws', rootOpts?.shell);
711
+ const exit = await runCommand(argv, shellSetting, {
712
+ env: buildSpawnEnv(process.env, ctx?.dotenv),
713
+ stdio: capture ? 'pipe' : 'inherit',
615
714
  });
715
+ // Deterministic termination (suppressed under tests)
716
+ if (!underTests) {
717
+ process.exit(typeof exit === 'number' ? exit : 0);
718
+ }
719
+ return;
616
720
  }
617
- if (!underTests)
618
- process.exit(0);
619
- return;
620
- }
621
- });
622
- },
623
- async afterResolve(_cli, ctx) {
624
- const log = console;
625
- const cfgRaw = (ctx.pluginConfigs?.['aws'] ?? {});
626
- const cfg = (cfgRaw || {});
627
- const out = await resolveAwsContext({
628
- dotenv: ctx.dotenv,
629
- cfg,
630
- });
631
- const { profile, region, credentials } = out;
632
- if (cfg.setEnv !== false) {
721
+ else {
722
+ // Session only: low-noise breadcrumb under debug
723
+ if (process.env.GETDOTENV_DEBUG) {
724
+ const log = console;
725
+ log.log('[aws] session established', {
726
+ profile: out.profile,
727
+ region: out.region,
728
+ hasCreds: Boolean(out.credentials),
729
+ });
730
+ }
731
+ if (!underTests)
732
+ process.exit(0);
733
+ return;
734
+ }
735
+ });
736
+ },
737
+ async afterResolve(_cli, ctx) {
738
+ const log = console;
739
+ const cfg = plugin.readConfig(_cli);
740
+ const out = await resolveAwsContext({
741
+ dotenv: ctx.dotenv,
742
+ cfg,
743
+ });
744
+ const { profile, region, credentials } = out;
745
+ // Unconditional env writes in host path
633
746
  if (region) {
634
747
  process.env.AWS_REGION = region;
635
748
  if (!process.env.AWS_DEFAULT_REGION)
@@ -642,24 +755,23 @@ const awsPlugin = () => definePlugin({
642
755
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
643
756
  }
644
757
  }
645
- }
646
- if (cfg.addCtx !== false) {
758
+ // Always publish minimal non-sensitive metadata
647
759
  ctx.plugins ??= {};
648
760
  ctx.plugins['aws'] = {
649
761
  ...(profile ? { profile } : {}),
650
762
  ...(region ? { region } : {}),
651
- ...(credentials ? { credentials } : {}),
652
763
  };
653
- }
654
- // Optional: low-noise breadcrumb for diagnostics
655
- if (process.env.GETDOTENV_DEBUG) {
656
- log.log('[aws] afterResolve', {
657
- profile,
658
- region,
659
- hasCreds: Boolean(credentials),
660
- });
661
- }
662
- },
663
- });
764
+ // Optional: low-noise breadcrumb for diagnostics
765
+ if (process.env.GETDOTENV_DEBUG) {
766
+ log.log('[aws] afterResolve', {
767
+ profile,
768
+ region,
769
+ hasCreds: Boolean(credentials),
770
+ });
771
+ }
772
+ },
773
+ });
774
+ return plugin;
775
+ };
664
776
 
665
777
  export { awsPlugin };