@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
@@ -20,26 +20,58 @@ import { Option, Command } from '@commander-js/extra-typings';
20
20
  * Minimal process env representation used by options and helpers.
21
21
  * Values may be `undefined` to indicate "unset".
22
22
  */
23
+ /**
24
+ * Schema for an env-like record.
25
+ *
26
+ * Keys are environment variable names and values are either strings or `undefined`
27
+ * (to represent “unset”).
28
+ *
29
+ * @public
30
+ */
23
31
  const processEnvSchema = z.record(z.string(), z.string().optional());
24
32
  // RAW: all fields optional — undefined means "inherit" from lower layers.
33
+ /**
34
+ * Programmatic options schema (raw).
35
+ *
36
+ * This schema is the canonical runtime source of truth for the `getDotenv()` programmatic API.
37
+ * All fields are optional; `undefined` generally means “inherit default/lower layer”.
38
+ *
39
+ * @public
40
+ */
25
41
  const getDotenvOptionsSchemaRaw = z.object({
42
+ /** Default environment name when `env` is not provided. */
26
43
  defaultEnv: z.string().optional(),
44
+ /** Base dotenv filename token (default `.env`). */
27
45
  dotenvToken: z.string().optional(),
46
+ /** Path to a dynamic variables module (JS/TS) to load and apply. */
28
47
  dynamicPath: z.string().optional(),
29
- // Dynamic map is intentionally wide for now; refine once sources are normalized.
48
+ /** Dynamic map is intentionally wide for now; refine once sources are normalized. */
30
49
  dynamic: z.record(z.string(), z.unknown()).optional(),
50
+ /** Selected environment name for this invocation (for env-scoped files and overlays). */
31
51
  env: z.string().optional(),
52
+ /** When true, skip applying dynamic variables. */
32
53
  excludeDynamic: z.boolean().optional(),
54
+ /** When true, skip environment-scoped dotenv files. */
33
55
  excludeEnv: z.boolean().optional(),
56
+ /** When true, skip global dotenv files. */
34
57
  excludeGlobal: z.boolean().optional(),
58
+ /** When true, skip private dotenv files. */
35
59
  excludePrivate: z.boolean().optional(),
60
+ /** When true, skip public dotenv files. */
36
61
  excludePublic: z.boolean().optional(),
62
+ /** When true, merge the final composed environment into `process.env`. */
37
63
  loadProcess: z.boolean().optional(),
64
+ /** When true, log the final environment map via `logger`. */
38
65
  log: z.boolean().optional(),
66
+ /** Logger used when `log` is enabled (console-compatible). */
39
67
  logger: z.unknown().default(console),
68
+ /** Optional output dotenv file path to write after composition. */
40
69
  outputPath: z.string().optional(),
70
+ /** Dotenv search paths (ordered). */
41
71
  paths: z.array(z.string()).optional(),
72
+ /** Private token suffix for private dotenv files (default `local`). */
42
73
  privateToken: z.string().optional(),
74
+ /** Explicit variables to overlay onto the composed dotenv map. */
43
75
  vars: processEnvSchema.optional(),
44
76
  });
45
77
  /**
@@ -47,6 +79,14 @@ const getDotenvOptionsSchemaRaw = z.object({
47
79
  * For now, this mirrors the RAW schema; future stages may materialize defaults
48
80
  * and narrow shapes as resolution is wired into the host.
49
81
  */
82
+ /**
83
+ * Programmatic options schema (resolved).
84
+ *
85
+ * Today this mirrors {@link getDotenvOptionsSchemaRaw}, but is kept as a distinct export
86
+ * so future resolution steps can narrow or materialize defaults without breaking the API.
87
+ *
88
+ * @public
89
+ */
50
90
  const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
51
91
 
52
92
  /**
@@ -56,27 +96,55 @@ const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
56
96
  * reflect normalized types (paths: string[], vars: ProcessEnv), applied in the
57
97
  * CLI resolution pipeline.
58
98
  */
99
+ /**
100
+ * CLI options schema (raw).
101
+ *
102
+ * Extends the programmatic options schema with CLI-only flags and stringly inputs
103
+ * which are normalized later by the host resolution pipeline.
104
+ *
105
+ * @public
106
+ */
59
107
  const getDotenvCliOptionsSchemaRaw = getDotenvOptionsSchemaRaw.extend({
60
108
  // CLI-specific fields (stringly inputs before preprocessing)
109
+ /** Enable verbose debug output (host-specific). */
61
110
  debug: z.boolean().optional(),
111
+ /** Fail on validation errors (schema/requiredKeys). */
62
112
  strict: z.boolean().optional(),
113
+ /** Capture child process stdio (useful for CI/tests). */
63
114
  capture: z.boolean().optional(),
115
+ /** Emit child env diagnostics (boolean or selected keys). */
64
116
  trace: z.union([z.boolean(), z.array(z.string())]).optional(),
117
+ /** Enable presentation-time redaction in trace/log output. */
65
118
  redact: z.boolean().optional(),
119
+ /** Enable entropy warnings in trace/log output. */
66
120
  warnEntropy: z.boolean().optional(),
121
+ /** Entropy threshold (bits/char) for warnings. */
67
122
  entropyThreshold: z.number().optional(),
123
+ /** Minimum value length to consider for entropy warnings. */
68
124
  entropyMinLength: z.number().optional(),
125
+ /** Regex patterns (strings) to suppress entropy warnings by key. */
69
126
  entropyWhitelist: z.array(z.string()).optional(),
127
+ /** Additional key-match patterns (strings) for redaction. */
70
128
  redactPatterns: z.array(z.string()).optional(),
129
+ /** Dotenv search paths provided as a single delimited string. */
71
130
  paths: z.string().optional(),
131
+ /** Delimiter string used to split `paths`. */
72
132
  pathsDelimiter: z.string().optional(),
133
+ /** Regex pattern used to split `paths` (takes precedence over delimiter). */
73
134
  pathsDelimiterPattern: z.string().optional(),
135
+ /** Scripts table in a permissive shape at parse time (validated elsewhere). */
74
136
  scripts: z.record(z.string(), z.unknown()).optional(),
137
+ /** Shell selection (`false` for shell-off, string for explicit shell). */
75
138
  shell: z.union([z.boolean(), z.string()]).optional(),
139
+ /** Extra variables expressed as a single delimited string of assignments. */
76
140
  vars: z.string().optional(),
141
+ /** Assignment operator used when parsing `vars`. */
77
142
  varsAssignor: z.string().optional(),
143
+ /** Regex pattern used as the assignment operator for `vars` parsing. */
78
144
  varsAssignorPattern: z.string().optional(),
145
+ /** Delimiter string used to split `vars`. */
79
146
  varsDelimiter: z.string().optional(),
147
+ /** Regex pattern used to split `vars` (takes precedence over delimiter). */
80
148
  varsDelimiterPattern: z.string().optional(),
81
149
  });
82
150
 
@@ -100,17 +168,34 @@ const envStringMap = z.record(z.string(), stringMap);
100
168
  * Raw configuration schema for get‑dotenv config files (JSON/YAML/JS/TS).
101
169
  * Validates allowed top‑level keys without performing path normalization.
102
170
  */
171
+ /**
172
+ * Config schema for discovered get-dotenv configuration documents (raw).
173
+ *
174
+ * This schema validates the allowed top-level keys for configuration files.
175
+ * It does not normalize paths or coerce types beyond Zod’s parsing.
176
+ *
177
+ * @public
178
+ */
103
179
  const getDotenvConfigSchemaRaw = z.object({
180
+ /** Root option defaults applied by the host (CLI-like, collapsed families). */
104
181
  rootOptionDefaults: getDotenvCliOptionsSchemaRaw.optional(),
182
+ /** Help-time visibility map for root flags (false hides). */
105
183
  rootOptionVisibility: visibilityMap.optional(),
106
- scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
184
+ /** Scripts table used by cmd/batch resolution (validation intentionally permissive here). */
185
+ scripts: z.record(z.string(), z.unknown()).optional(),
186
+ /** Keys required to be present in the final composed environment. */
107
187
  requiredKeys: z.array(z.string()).optional(),
188
+ /** Validation schema (JS/TS only; JSON/YAML loader rejects). */
108
189
  schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
190
+ /** Public global variables (string-only). */
109
191
  vars: stringMap.optional(), // public, global
192
+ /** Public per-environment variables (string-only). */
110
193
  envVars: envStringMap.optional(), // public, per-env
111
194
  // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
195
+ /** Dynamic variable definitions (JS/TS only). */
112
196
  dynamic: z.unknown().optional(),
113
197
  // Per-plugin config bag; validated by plugins/host when used.
198
+ /** Per-plugin config slices keyed by realized mount path (for example, `aws/whoami`). */
114
199
  plugins: z.record(z.string(), z.unknown()).optional(),
115
200
  });
116
201
  /**
@@ -862,13 +947,21 @@ const redactObject = (obj, opts) => {
862
947
  * Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
863
948
  * Used as the bottom layer for CLI option resolution.
864
949
  */
950
+ const baseScripts = {
951
+ 'git-status': {
952
+ cmd: 'git branch --show-current && git status -s -u',
953
+ shell: true,
954
+ },
955
+ };
865
956
  /**
866
- * Default values for root CLI options used by the host and helpers as the
867
- * baseline layer during option resolution.
957
+ * Default values for root CLI options used by the host and helpers as the baseline layer during option resolution.
868
958
  *
869
- * These defaults correspond to the "stringly" root surface (see `RootOptionsShape`)
870
- * and are merged by precedence with create-time overrides and any discovered
871
- * configuration `rootOptionDefaults` before CLI flags are applied.
959
+ * These defaults correspond to the stringly root surface (see `RootOptionsShape`) and are merged by precedence with:
960
+ * - create-time overrides
961
+ * - any discovered configuration `rootOptionDefaults`
962
+ * - and finally CLI flags at runtime
963
+ *
964
+ * @public
872
965
  */
873
966
  const baseRootOptionDefaults = {
874
967
  dotenvToken: '.env',
@@ -882,12 +975,7 @@ const baseRootOptionDefaults = {
882
975
  paths: './',
883
976
  pathsDelimiter: ' ',
884
977
  privateToken: 'local',
885
- scripts: {
886
- 'git-status': {
887
- cmd: 'git branch --show-current && git status -s -u',
888
- shell: true,
889
- },
890
- },
978
+ scripts: baseScripts,
891
979
  shell: true,
892
980
  vars: '',
893
981
  varsAssignor: '=',
@@ -1434,6 +1522,14 @@ async function _execNormalized(command, shell, opts = {}) {
1434
1522
  return out;
1435
1523
  }
1436
1524
  }
1525
+ /**
1526
+ * Execute a command and capture stdout/stderr (buffered).
1527
+ *
1528
+ * @param command - Command string (shell) or argv array (shell-off supported).
1529
+ * @param shell - Shell setting (false for plain execution).
1530
+ * @param opts - Execution options (cwd/env/timeout).
1531
+ * @returns A promise resolving to the captured result.
1532
+ */
1437
1533
  async function runCommandResult(command, shell, opts = {}) {
1438
1534
  // Build opts without injecting undefined (exactOptionalPropertyTypes-safe)
1439
1535
  const coreOpts = { stdio: 'pipe' };
@@ -1448,6 +1544,14 @@ async function runCommandResult(command, shell, opts = {}) {
1448
1544
  }
1449
1545
  return _execNormalized(command, shell, coreOpts);
1450
1546
  }
1547
+ /**
1548
+ * Execute a command and return its exit code.
1549
+ *
1550
+ * @param command - Command string (shell) or argv array (shell-off supported).
1551
+ * @param shell - Shell setting (false for plain execution).
1552
+ * @param opts - Execution options (cwd/env/stdio).
1553
+ * @returns A promise resolving to the process exit code.
1554
+ */
1451
1555
  async function runCommand(command, shell, opts) {
1452
1556
  // Build opts without injecting undefined (exactOptionalPropertyTypes-safe)
1453
1557
  const callOpts = {};
@@ -1907,6 +2011,16 @@ function evaluateDynamicOptions(root, resolved) {
1907
2011
  visit(root);
1908
2012
  }
1909
2013
 
2014
+ /**
2015
+ * Initialize a {@link GetDotenvCli} instance with help configuration and safe defaults.
2016
+ *
2017
+ * @remarks
2018
+ * This is a low-level initializer used by the host constructor to keep `GetDotenvCli.ts`
2019
+ * small and to centralize help/output behavior.
2020
+ *
2021
+ * @param cli - The CLI instance to initialize.
2022
+ * @param headerGetter - Callback returning an optional help header string.
2023
+ */
1910
2024
  function initializeInstance(cli, headerGetter) {
1911
2025
  // Configure grouped help: show only base options in default "Options";
1912
2026
  // subcommands show all of their own options.
@@ -2436,18 +2550,49 @@ const buildSpawnEnv = (base, overlay) => {
2436
2550
  function applyAwsContext(out, ctx, setProcessEnv = true) {
2437
2551
  const { profile, region, credentials } = out;
2438
2552
  if (setProcessEnv) {
2439
- if (region) {
2440
- process.env.AWS_REGION = region;
2441
- if (!process.env.AWS_DEFAULT_REGION) {
2442
- process.env.AWS_DEFAULT_REGION = region;
2553
+ // Ensure AWS credential sources are mutually exclusive.
2554
+ // The AWS SDK warns (and may change precedence in future) when both
2555
+ // AWS_PROFILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are set.
2556
+ const clear = (keys) => {
2557
+ for (const k of keys) {
2558
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
2559
+ delete process.env[k];
2443
2560
  }
2444
- }
2561
+ };
2562
+ const clearProfileVars = () => {
2563
+ clear(['AWS_PROFILE', 'AWS_DEFAULT_PROFILE', 'AWS_SDK_LOAD_CONFIG']);
2564
+ };
2565
+ const clearStaticCreds = () => {
2566
+ clear([
2567
+ 'AWS_ACCESS_KEY_ID',
2568
+ 'AWS_SECRET_ACCESS_KEY',
2569
+ 'AWS_SESSION_TOKEN',
2570
+ ]);
2571
+ };
2572
+ // Mode A: exported/static credentials (clear profile vars)
2445
2573
  if (credentials) {
2574
+ clearProfileVars();
2446
2575
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
2447
2576
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
2448
2577
  if (credentials.sessionToken !== undefined) {
2449
2578
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
2450
2579
  }
2580
+ else {
2581
+ delete process.env.AWS_SESSION_TOKEN;
2582
+ }
2583
+ }
2584
+ else if (profile) {
2585
+ // Mode B: profile-based (SSO) credentials (clear static creds)
2586
+ clearStaticCreds();
2587
+ process.env.AWS_PROFILE = profile;
2588
+ process.env.AWS_DEFAULT_PROFILE = profile;
2589
+ process.env.AWS_SDK_LOAD_CONFIG = '1';
2590
+ }
2591
+ if (region) {
2592
+ process.env.AWS_REGION = region;
2593
+ if (!process.env.AWS_DEFAULT_REGION) {
2594
+ process.env.AWS_DEFAULT_REGION = region;
2595
+ }
2451
2596
  }
2452
2597
  }
2453
2598
  // Always publish minimal, non-sensitive metadata
@@ -2458,7 +2603,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
2458
2603
  };
2459
2604
  }
2460
2605
 
2461
- const DEFAULT_TIMEOUT_MS = 15_000;
2606
+ const AWS_CLI_TIMEOUT_MS = 15_000;
2462
2607
  const trim = (s) => (typeof s === 'string' ? s.trim() : '');
2463
2608
  const unquote = (s) => s.length >= 2 &&
2464
2609
  ((s.startsWith('"') && s.endsWith('"')) ||
@@ -2493,6 +2638,7 @@ const parseExportCredentialsJson = (txt) => {
2493
2638
  /**
2494
2639
  * Parse AWS credentials from environment-export output (shell-agnostic).
2495
2640
  * Supports POSIX `export KEY=VAL` and PowerShell `$Env:KEY=VAL`.
2641
+ * Also supports AWS CLI `windows-cmd` (`set KEY=VAL`) and `env-no-export` (`KEY=VAL`).
2496
2642
  *
2497
2643
  * @param txt - Raw stdout text from the AWS CLI.
2498
2644
  * @returns Parsed credentials, or `undefined` when the input is not recognized.
@@ -2506,12 +2652,17 @@ const parseExportCredentialsEnv = (txt) => {
2506
2652
  const line = raw.trim();
2507
2653
  if (!line)
2508
2654
  continue;
2509
- // POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
2655
+ // POSIX: export AWS_ACCESS_KEY_ID=..., ...
2510
2656
  let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2511
- if (!m) {
2512
- // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
2513
- m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2514
- }
2657
+ // PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
2658
+ if (!m)
2659
+ m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
2660
+ // Windows cmd: set AWS_ACCESS_KEY_ID=..., etc.
2661
+ if (!m)
2662
+ m = /^(?:set)\s+([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
2663
+ // env-no-export: AWS_ACCESS_KEY_ID=..., etc.
2664
+ if (!m)
2665
+ m = /^([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
2515
2666
  if (!m)
2516
2667
  continue;
2517
2668
  const k = m[1];
@@ -2536,7 +2687,7 @@ const parseExportCredentialsEnv = (txt) => {
2536
2687
  };
2537
2688
  return undefined;
2538
2689
  };
2539
- const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
2690
+ const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
2540
2691
  const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
2541
2692
  env: process.env,
2542
2693
  timeoutMs,
@@ -2551,30 +2702,43 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
2551
2702
  }
2552
2703
  return undefined;
2553
2704
  };
2554
- const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
2555
- // Try JSON format first (AWS CLI v2)
2556
- const rJson = await runCommandResult([
2557
- 'aws',
2558
- 'configure',
2559
- 'export-credentials',
2560
- '--profile',
2561
- profile,
2562
- '--format',
2563
- 'json',
2564
- ], false, { env: process.env, timeoutMs });
2565
- if (rJson.exitCode === 0) {
2566
- const creds = parseExportCredentialsJson(rJson.stdout);
2567
- if (creds)
2568
- return creds;
2569
- }
2570
- // Fallback: env lines
2571
- const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
2572
- if (rEnv.exitCode === 0) {
2573
- const creds = parseExportCredentialsEnv(rEnv.stdout);
2705
+ const exportCredentials = async (profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
2706
+ const tryExport = async (format) => {
2707
+ const argv = [
2708
+ 'aws',
2709
+ 'configure',
2710
+ 'export-credentials',
2711
+ '--profile',
2712
+ profile,
2713
+ ...(format ? ['--format', format] : []),
2714
+ ];
2715
+ const r = await runCommandResult(argv, false, {
2716
+ env: process.env,
2717
+ timeoutMs,
2718
+ });
2719
+ if (r.exitCode !== 0)
2720
+ return undefined;
2721
+ const out = trim(r.stdout);
2722
+ if (!out)
2723
+ return undefined;
2724
+ // Some formats produce JSON ("process"), some produce shell-ish env lines.
2725
+ return parseExportCredentialsJson(out) ?? parseExportCredentialsEnv(out);
2726
+ };
2727
+ // Prefer the default/JSON "process" format first; then fall back to shell env outputs.
2728
+ // Note: AWS CLI v2 supports: process | env | env-no-export | powershell | windows-cmd
2729
+ const formats = [
2730
+ 'process',
2731
+ ...(process.platform === 'win32'
2732
+ ? ['powershell', 'windows-cmd', 'env', 'env-no-export']
2733
+ : ['env', 'env-no-export']),
2734
+ ];
2735
+ for (const f of formats) {
2736
+ const creds = await tryExport(f);
2574
2737
  if (creds)
2575
2738
  return creds;
2576
2739
  }
2577
- return undefined;
2740
+ // Final fallback: no --format (AWS CLI default output)
2741
+ return tryExport(undefined);
2578
2742
  };
2579
2743
  /**
2580
2744
  * Resolve AWS context (profile, region, credentials) using configuration and environment.
@@ -2606,31 +2770,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2606
2770
  out.region = region;
2607
2771
  return out;
2608
2772
  }
2609
- // Env-first credentials.
2610
2773
  let credentials;
2611
- const envId = trim(process.env.AWS_ACCESS_KEY_ID);
2612
- const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
2613
- const envToken = trim(process.env.AWS_SESSION_TOKEN);
2614
- if (envId && envSecret) {
2615
- credentials = {
2616
- accessKeyId: envId,
2617
- secretAccessKey: envSecret,
2618
- ...(envToken ? { sessionToken: envToken } : {}),
2619
- };
2620
- }
2621
- else if (profile) {
2774
+ // Profile wins over ambient env creds when present (from flags/config/dotenv).
2775
+ if (profile) {
2622
2776
  // Try export-credentials
2623
2777
  credentials = await exportCredentials(profile);
2624
2778
  // On failure, detect SSO and optionally login then retry
2625
2779
  if (!credentials) {
2626
2780
  const ssoSession = await getAwsConfigure('sso_session', profile);
2627
- const looksSSO = typeof ssoSession === 'string' && ssoSession.length > 0;
2781
+ // Legacy SSO profiles use sso_start_url/sso_region rather than sso_session.
2782
+ const ssoStartUrl = await getAwsConfigure('sso_start_url', profile);
2783
+ const looksSSO = (typeof ssoSession === 'string' && ssoSession.length > 0) ||
2784
+ (typeof ssoStartUrl === 'string' && ssoStartUrl.length > 0);
2628
2785
  if (looksSSO && cfg.loginOnDemand) {
2629
- // Best-effort login, then retry export once.
2630
- await runCommandResult(['aws', 'sso', 'login', '--profile', profile], false, {
2786
+ // Interactive login (no timeout by default), then retry export once.
2787
+ const exit = await runCommand(['aws', 'sso', 'login', '--profile', profile], false, {
2631
2788
  env: process.env,
2632
- timeoutMs: DEFAULT_TIMEOUT_MS,
2789
+ stdio: 'inherit',
2633
2790
  });
2791
+ if (exit !== 0) {
2792
+ throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
2793
+ }
2634
2794
  credentials = await exportCredentials(profile);
2635
2795
  }
2636
2796
  }
@@ -2648,6 +2808,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2648
2808
  }
2649
2809
  }
2650
2810
  }
2811
+ else {
2812
+ // Env-first credentials when no profile is present.
2813
+ const envId = trim(process.env.AWS_ACCESS_KEY_ID);
2814
+ const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
2815
+ const envToken = trim(process.env.AWS_SESSION_TOKEN);
2816
+ if (envId && envSecret) {
2817
+ credentials = {
2818
+ accessKeyId: envId,
2819
+ secretAccessKey: envSecret,
2820
+ ...(envToken ? { sessionToken: envToken } : {}),
2821
+ };
2822
+ }
2823
+ }
2651
2824
  // Final region resolution
2652
2825
  if (!region && profile)
2653
2826
  region = await getAwsConfigure('region', profile);
@@ -2664,16 +2837,234 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
2664
2837
  };
2665
2838
 
2666
2839
  /**
2667
- * Zod schema for AWS plugin configuration.
2840
+ * Create the AWS plugin `afterResolve` hook.
2841
+ *
2842
+ * This runs once per invocation after the host resolves dotenv context.
2843
+ *
2844
+ * @param plugin - The AWS plugin instance.
2845
+ * @returns An `afterResolve` hook function suitable for assigning to `plugin.afterResolve`.
2846
+ *
2847
+ * @internal
2668
2848
  */
2669
- const AwsPluginConfigSchema = z.object({
2849
+ function attachAwsAfterResolveHook(plugin) {
2850
+ return async (cli, ctx) => {
2851
+ const cfg = plugin.readConfig(cli);
2852
+ const out = await resolveAwsContext({
2853
+ dotenv: ctx.dotenv,
2854
+ cfg,
2855
+ });
2856
+ applyAwsContext(out, ctx, true);
2857
+ // Optional: low-noise breadcrumb for diagnostics
2858
+ if (process.env.GETDOTENV_DEBUG) {
2859
+ try {
2860
+ const msg = JSON.stringify({
2861
+ profile: out.profile,
2862
+ region: out.region,
2863
+ hasCreds: Boolean(out.credentials),
2864
+ });
2865
+ process.stderr.write(`[aws] afterResolve ${msg}\n`);
2866
+ }
2867
+ catch {
2868
+ /* ignore */
2869
+ }
2870
+ }
2871
+ };
2872
+ }
2873
+
2874
+ /** @internal */
2875
+ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
2876
+ /**
2877
+ * Create an AWS plugin config overlay from Commander-parsed option values.
2878
+ *
2879
+ * This preserves tri-state intent:
2880
+ * - If a flag was not provided, it should not overwrite config-derived defaults.
2881
+ * - If `--no-…` was provided, it must explicitly force the boolean false.
2882
+ *
2883
+ * @param opts - Commander option values for the current invocation.
2884
+ * @returns A partial AWS plugin config object containing only explicit overrides.
2885
+ *
2886
+ * @internal
2887
+ */
2888
+ function awsConfigOverridesFromCommandOpts(opts) {
2889
+ const o = isRecord(opts) ? opts : {};
2890
+ const overlay = {};
2891
+ // Map boolean toggles (respect explicit --no-*)
2892
+ if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand')) {
2893
+ overlay.loginOnDemand = Boolean(o.loginOnDemand);
2894
+ }
2895
+ // Strings/enums
2896
+ if (typeof o.profile === 'string')
2897
+ overlay.profile = o.profile;
2898
+ if (typeof o.region === 'string')
2899
+ overlay.region = o.region;
2900
+ if (typeof o.defaultRegion === 'string')
2901
+ overlay.defaultRegion = o.defaultRegion;
2902
+ if (o.strategy === 'cli-export' || o.strategy === 'none') {
2903
+ overlay.strategy = o.strategy;
2904
+ }
2905
+ // Advanced key overrides
2906
+ if (typeof o.profileKey === 'string')
2907
+ overlay.profileKey = o.profileKey;
2908
+ if (typeof o.profileFallbackKey === 'string') {
2909
+ overlay.profileFallbackKey = o.profileFallbackKey;
2910
+ }
2911
+ if (typeof o.regionKey === 'string')
2912
+ overlay.regionKey = o.regionKey;
2913
+ return overlay;
2914
+ }
2915
+
2916
+ /**
2917
+ * Attach the default action for the AWS plugin mount.
2918
+ *
2919
+ * Behavior:
2920
+ * - With args: forwards to AWS CLI (`aws <args...>`) under the established session.
2921
+ * - Without args: session-only establishment (no forward).
2922
+ *
2923
+ * @param cli - The `aws` command mount.
2924
+ * @param plugin - The AWS plugin instance.
2925
+ *
2926
+ * @internal
2927
+ */
2928
+ function attachAwsDefaultAction(cli, plugin, awsCmd) {
2929
+ awsCmd.action(async (args, opts, thisCommand) => {
2930
+ // Access merged root CLI options (installed by root hooks).
2931
+ const bag = readMergedOptions(thisCommand);
2932
+ const capture = shouldCapture(bag.capture);
2933
+ const underTests = process.env.GETDOTENV_TEST === '1' ||
2934
+ typeof process.env.VITEST_WORKER_ID === 'string';
2935
+ // Build overlay cfg from subcommand flags layered over discovered config.
2936
+ const ctx = cli.getCtx();
2937
+ const cfgBase = plugin.readConfig(cli);
2938
+ const cfg = {
2939
+ ...cfgBase,
2940
+ ...awsConfigOverridesFromCommandOpts(opts),
2941
+ };
2942
+ // Resolve current context with overrides
2943
+ const out = await resolveAwsContext({
2944
+ dotenv: ctx.dotenv,
2945
+ cfg,
2946
+ });
2947
+ // Publish env/context
2948
+ applyAwsContext(out, ctx, true);
2949
+ // Forward when positional args are present; otherwise session-only.
2950
+ if (args.length > 0) {
2951
+ const argv = ['aws', ...args];
2952
+ const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
2953
+ const exit = await runCommand(argv, shellSetting, {
2954
+ env: buildSpawnEnv(process.env, ctx.dotenv),
2955
+ stdio: capture ? 'pipe' : 'inherit',
2956
+ });
2957
+ // Deterministic termination (suppressed under tests)
2958
+ if (!underTests) {
2959
+ process.exit(typeof exit === 'number' ? exit : 0);
2960
+ }
2961
+ return;
2962
+ }
2963
+ // Session only: low-noise breadcrumb under debug
2964
+ if (process.env.GETDOTENV_DEBUG) {
2965
+ try {
2966
+ const msg = JSON.stringify({
2967
+ profile: out.profile,
2968
+ region: out.region,
2969
+ hasCreds: Boolean(out.credentials),
2970
+ });
2971
+ process.stderr.write(`[aws] session established ${msg}\n`);
2972
+ }
2973
+ catch {
2974
+ /* ignore */
2975
+ }
2976
+ }
2977
+ if (!underTests)
2978
+ process.exit(0);
2979
+ });
2980
+ }
2981
+
2982
+ /**
2983
+ * Attach options/arguments for the AWS plugin mount.
2984
+ *
2985
+ * @param cli - The `aws` command mount.
2986
+ * @param plugin - The AWS plugin instance (for dynamic option descriptions).
2987
+ *
2988
+ * @internal
2989
+ */
2990
+ /** @hidden */
2991
+ function attachAwsOptions(cli, plugin) {
2992
+ return (cli
2993
+ // Description is owned by the plugin index (src/plugins/aws/index.ts).
2994
+ .enablePositionalOptions()
2995
+ .passThroughOptions()
2996
+ .allowUnknownOption(true)
2997
+ // Boolean toggles with dynamic help labels (effective defaults)
2998
+ .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
2999
+ .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
3000
+ // Strings / enums
3001
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
3002
+ .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
3003
+ .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
3004
+ .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
3005
+ // Advanced key overrides
3006
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
3007
+ .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
3008
+ .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
3009
+ // Accept any extra operands so Commander does not error when tokens appear after "--".
3010
+ .argument('[args...]'));
3011
+ }
3012
+
3013
+ /**
3014
+ * Attach the AWS plugin `preSubcommand` hook.
3015
+ *
3016
+ * Ensures `aws --profile/--region <child>` applies the AWS session setup before
3017
+ * child subcommand execution.
3018
+ *
3019
+ * @param cli - The `aws` command mount.
3020
+ * @param plugin - The AWS plugin instance.
3021
+ *
3022
+ * @internal
3023
+ */
3024
+ function attachAwsPreSubcommandHook(cli, plugin) {
3025
+ cli.hook('preSubcommand', async (thisCommand) => {
3026
+ // Avoid side effects for help rendering.
3027
+ if (process.argv.includes('-h') || process.argv.includes('--help'))
3028
+ return;
3029
+ const ctx = cli.getCtx();
3030
+ const cfgBase = plugin.readConfig(cli);
3031
+ const cfg = {
3032
+ ...cfgBase,
3033
+ ...awsConfigOverridesFromCommandOpts(thisCommand.opts()),
3034
+ };
3035
+ const out = await resolveAwsContext({
3036
+ dotenv: ctx.dotenv,
3037
+ cfg,
3038
+ });
3039
+ applyAwsContext(out, ctx, true);
3040
+ });
3041
+ }
3042
+
3043
+ /**
3044
+ * AWS plugin configuration schema.
3045
+ *
3046
+ * @remarks
3047
+ * This Zod schema is used by the host to validate the `plugins.aws` config slice.
3048
+ *
3049
+ * @public
3050
+ * @hidden
3051
+ */
3052
+ const awsPluginConfigSchema = z.object({
3053
+ /** Preferred AWS profile name (overrides dotenv-derived profile keys when set). */
2670
3054
  profile: z.string().optional(),
3055
+ /** Preferred AWS region (overrides dotenv-derived region key when set). */
2671
3056
  region: z.string().optional(),
3057
+ /** Fallback region when region cannot be resolved from config/dotenv/AWS CLI. */
2672
3058
  defaultRegion: z.string().optional(),
3059
+ /** Dotenv/config key for local profile lookup (default `AWS_LOCAL_PROFILE`). */
2673
3060
  profileKey: z.string().default('AWS_LOCAL_PROFILE').optional(),
3061
+ /** Dotenv/config fallback key for profile lookup (default `AWS_PROFILE`). */
2674
3062
  profileFallbackKey: z.string().default('AWS_PROFILE').optional(),
3063
+ /** Dotenv/config key for region lookup (default `AWS_REGION`). */
2675
3064
  regionKey: z.string().default('AWS_REGION').optional(),
3065
+ /** Credential acquisition strategy (`cli-export` to resolve via AWS CLI, or `none` to skip). */
2676
3066
  strategy: z.enum(['cli-export', 'none']).default('cli-export').optional(),
3067
+ /** When true, attempt `aws sso login` on-demand when credential export fails for an SSO profile. */
2677
3068
  loginOnDemand: z.boolean().default(false).optional(),
2678
3069
  });
2679
3070
 
@@ -2690,129 +3081,16 @@ const AwsPluginConfigSchema = z.object({
2690
3081
  const awsPlugin = () => {
2691
3082
  const plugin = definePlugin({
2692
3083
  ns: 'aws',
2693
- configSchema: AwsPluginConfigSchema,
2694
- setup: (cli) => {
2695
- // Mount: aws (provided)
2696
- cli
2697
- .description('Establish an AWS session and optionally forward to the AWS CLI')
2698
- .enablePositionalOptions()
2699
- .passThroughOptions()
2700
- .allowUnknownOption(true)
2701
- // Boolean toggles with dynamic help labels (effective defaults)
2702
- .addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
2703
- .addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
2704
- // Strings / enums
2705
- .addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
2706
- .addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
2707
- .addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
2708
- .addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
2709
- // Advanced key overrides
2710
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
2711
- .addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
2712
- .addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
2713
- // Accept any extra operands so Commander does not error when tokens appear after "--".
2714
- .argument('[args...]')
2715
- .action(async (args, opts, thisCommand) => {
2716
- const pluginInst = plugin;
2717
- // Access merged root CLI options (installed by passOptions())
2718
- const bag = readMergedOptions(thisCommand);
2719
- const capture = shouldCapture(bag.capture);
2720
- const underTests = process.env.GETDOTENV_TEST === '1' ||
2721
- typeof process.env.VITEST_WORKER_ID === 'string';
2722
- // Build overlay cfg from subcommand flags layered over discovered config.
2723
- const ctx = cli.getCtx();
2724
- const cfgBase = pluginInst.readConfig(cli);
2725
- const o = opts;
2726
- const overlay = {};
2727
- // Map boolean toggles (respect explicit --no-*)
2728
- if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
2729
- overlay.loginOnDemand = Boolean(o.loginOnDemand);
2730
- // Strings/enums
2731
- if (typeof o.profile === 'string')
2732
- overlay.profile = o.profile;
2733
- if (typeof o.region === 'string')
2734
- overlay.region = o.region;
2735
- if (typeof o.defaultRegion === 'string')
2736
- overlay.defaultRegion = o.defaultRegion;
2737
- if (typeof o.strategy === 'string')
2738
- overlay.strategy = o.strategy;
2739
- // Advanced key overrides
2740
- if (typeof o.profileKey === 'string')
2741
- overlay.profileKey = o.profileKey;
2742
- if (typeof o.profileFallbackKey === 'string')
2743
- overlay.profileFallbackKey = o.profileFallbackKey;
2744
- if (typeof o.regionKey === 'string')
2745
- overlay.regionKey = o.regionKey;
2746
- const cfg = {
2747
- ...cfgBase,
2748
- ...overlay,
2749
- };
2750
- // Resolve current context with overrides
2751
- const out = await resolveAwsContext({
2752
- dotenv: ctx.dotenv,
2753
- cfg,
2754
- });
2755
- // Publish env/context
2756
- applyAwsContext(out, ctx, true);
2757
- // Forward when positional args are present; otherwise session-only.
2758
- if (Array.isArray(args) && args.length > 0) {
2759
- const argv = ['aws', ...args];
2760
- const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
2761
- const exit = await runCommand(argv, shellSetting, {
2762
- env: buildSpawnEnv(process.env, ctx.dotenv),
2763
- stdio: capture ? 'pipe' : 'inherit',
2764
- });
2765
- // Deterministic termination (suppressed under tests)
2766
- if (!underTests) {
2767
- process.exit(typeof exit === 'number' ? exit : 0);
2768
- }
2769
- return;
2770
- }
2771
- else {
2772
- // Session only: low-noise breadcrumb under debug
2773
- if (process.env.GETDOTENV_DEBUG) {
2774
- try {
2775
- const msg = JSON.stringify({
2776
- profile: out.profile,
2777
- region: out.region,
2778
- hasCreds: Boolean(out.credentials),
2779
- });
2780
- process.stderr.write(`[aws] session established ${msg}\n`);
2781
- }
2782
- catch {
2783
- /* ignore */
2784
- }
2785
- }
2786
- if (!underTests)
2787
- process.exit(0);
2788
- return;
2789
- }
2790
- });
3084
+ configSchema: awsPluginConfigSchema,
3085
+ setup(cli) {
3086
+ cli.description('Establish an AWS session and optionally forward to the AWS CLI');
3087
+ const awsCmd = attachAwsOptions(cli, plugin);
3088
+ attachAwsPreSubcommandHook(cli, plugin);
3089
+ attachAwsDefaultAction(cli, plugin, awsCmd);
2791
3090
  return undefined;
2792
3091
  },
2793
- afterResolve: async (_cli, ctx) => {
2794
- const cfg = plugin.readConfig(_cli);
2795
- const out = await resolveAwsContext({
2796
- dotenv: ctx.dotenv,
2797
- cfg,
2798
- });
2799
- applyAwsContext(out, ctx, true);
2800
- // Optional: low-noise breadcrumb for diagnostics
2801
- if (process.env.GETDOTENV_DEBUG) {
2802
- try {
2803
- const msg = JSON.stringify({
2804
- profile: out.profile,
2805
- region: out.region,
2806
- hasCreds: Boolean(out.credentials),
2807
- });
2808
- process.stderr.write(`[aws] afterResolve ${msg}\n`);
2809
- }
2810
- catch {
2811
- /* ignore */
2812
- }
2813
- }
2814
- },
2815
3092
  });
3093
+ plugin.afterResolve = attachAwsAfterResolveHook(plugin);
2816
3094
  return plugin;
2817
3095
  };
2818
3096