@karmaniverous/get-dotenv 4.5.2 → 5.0.0-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.
- package/LICENSE +28 -0
- package/README.md +369 -215
- package/dist/cliHost.cjs +1078 -0
- package/dist/cliHost.d.cts +193 -0
- package/dist/cliHost.d.mts +193 -0
- package/dist/cliHost.d.ts +193 -0
- package/dist/cliHost.mjs +1074 -0
- package/dist/config.cjs +247 -0
- package/dist/config.d.cts +53 -0
- package/dist/config.d.mts +53 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.mjs +242 -0
- package/dist/env-overlay.cjs +163 -0
- package/dist/env-overlay.d.cts +50 -0
- package/dist/env-overlay.d.mts +50 -0
- package/dist/env-overlay.d.ts +50 -0
- package/dist/env-overlay.mjs +161 -0
- package/dist/getdotenv.cli.mjs +2817 -40874
- package/dist/index.cjs +1482 -40965
- package/dist/index.d.cts +206 -67
- package/dist/index.d.mts +206 -67
- package/dist/index.d.ts +206 -67
- package/dist/index.mjs +1454 -40939
- package/dist/plugins-aws.cjs +618 -0
- package/dist/plugins-aws.d.cts +178 -0
- package/dist/plugins-aws.d.mts +178 -0
- package/dist/plugins-aws.d.ts +178 -0
- package/dist/plugins-aws.mjs +616 -0
- package/dist/plugins-batch.cjs +569 -0
- package/dist/plugins-batch.d.cts +200 -0
- package/dist/plugins-batch.d.mts +200 -0
- package/dist/plugins-batch.d.ts +200 -0
- package/dist/plugins-batch.mjs +567 -0
- package/dist/plugins-init.cjs +282 -0
- package/dist/plugins-init.d.cts +182 -0
- package/dist/plugins-init.d.mts +182 -0
- package/dist/plugins-init.d.ts +182 -0
- package/dist/plugins-init.mjs +280 -0
- package/getdotenv.config.json +19 -0
- package/package.json +228 -139
- package/templates/cli/ts/index.ts +9 -0
- package/templates/cli/ts/plugins/hello.ts +17 -0
- package/templates/config/js/getdotenv.config.js +15 -0
- package/templates/config/json/local/getdotenv.config.local.json +7 -0
- package/templates/config/json/public/getdotenv.config.json +12 -0
- package/templates/config/public/getdotenv.config.json +13 -0
- package/templates/config/ts/getdotenv.config.ts +16 -0
- package/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
- package/templates/config/yaml/public/getdotenv.config.yaml +10 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { execa, execaCommand } from 'execa';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
// Minimal tokenizer for shell-off execution:
|
|
5
|
+
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
6
|
+
const tokenize = (command) => {
|
|
7
|
+
const out = [];
|
|
8
|
+
let cur = '';
|
|
9
|
+
let quote = null;
|
|
10
|
+
for (let i = 0; i < command.length; i++) {
|
|
11
|
+
const c = command.charAt(i);
|
|
12
|
+
if (quote) {
|
|
13
|
+
if (c === quote) {
|
|
14
|
+
// Support doubled quotes inside a quoted segment (Windows/PowerShell style):
|
|
15
|
+
// "" -> " and '' -> '
|
|
16
|
+
const next = command.charAt(i + 1);
|
|
17
|
+
if (next === quote) {
|
|
18
|
+
cur += quote;
|
|
19
|
+
i += 1; // skip the second quote
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// end of quoted segment
|
|
23
|
+
quote = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
cur += c;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
if (c === '"' || c === "'") {
|
|
32
|
+
quote = c;
|
|
33
|
+
}
|
|
34
|
+
else if (/\s/.test(c)) {
|
|
35
|
+
if (cur) {
|
|
36
|
+
out.push(cur);
|
|
37
|
+
cur = '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
cur += c;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (cur)
|
|
46
|
+
out.push(cur);
|
|
47
|
+
return out;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const dbg = (...args) => {
|
|
51
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
52
|
+
// Use stderr to avoid interfering with stdout assertions
|
|
53
|
+
console.error('[getdotenv:run]', ...args);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
// Strip repeated symmetric outer quotes (single or double) until stable.
|
|
57
|
+
// This is safe for argv arrays passed to execa (no quoting needed) and avoids
|
|
58
|
+
// passing quote characters through to Node (e.g., for `node -e "<code>"`).
|
|
59
|
+
// Handles stacked quotes from shells like PowerShell: """code""" -> code.
|
|
60
|
+
const stripOuterQuotes = (s) => {
|
|
61
|
+
let out = s;
|
|
62
|
+
// Repeatedly trim only when the entire string is wrapped in matching quotes.
|
|
63
|
+
// Stop as soon as the ends are asymmetric or no quotes remain.
|
|
64
|
+
while (out.length >= 2) {
|
|
65
|
+
const a = out.charAt(0);
|
|
66
|
+
const b = out.charAt(out.length - 1);
|
|
67
|
+
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
68
|
+
if (!symmetric)
|
|
69
|
+
break;
|
|
70
|
+
out = out.slice(1, -1);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
};
|
|
74
|
+
// Extract exitCode/stdout/stderr from execa result or error in a tolerant way.
|
|
75
|
+
const pickResult = (r) => {
|
|
76
|
+
const exit = r.exitCode;
|
|
77
|
+
const stdoutVal = r.stdout;
|
|
78
|
+
const stderrVal = r.stderr;
|
|
79
|
+
return {
|
|
80
|
+
exitCode: typeof exit === 'number' ? exit : Number.NaN,
|
|
81
|
+
stdout: typeof stdoutVal === 'string' ? stdoutVal : '',
|
|
82
|
+
stderr: typeof stderrVal === 'string' ? stderrVal : '',
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
// Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
|
|
86
|
+
// expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
|
|
87
|
+
const sanitizeEnv = (env) => {
|
|
88
|
+
if (!env)
|
|
89
|
+
return undefined;
|
|
90
|
+
const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
|
|
91
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
92
|
+
};
|
|
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 = {}) => {
|
|
100
|
+
const envSan = sanitizeEnv(opts.env);
|
|
101
|
+
{
|
|
102
|
+
let file;
|
|
103
|
+
let args = [];
|
|
104
|
+
if (Array.isArray(command)) {
|
|
105
|
+
file = command[0];
|
|
106
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const tokens = tokenize(command);
|
|
110
|
+
file = tokens[0];
|
|
111
|
+
args = tokens.slice(1);
|
|
112
|
+
}
|
|
113
|
+
if (!file)
|
|
114
|
+
return { exitCode: 0, stdout: '', stderr: '' };
|
|
115
|
+
dbg('exec:capture (plain)', { file, args });
|
|
116
|
+
try {
|
|
117
|
+
const result = await execa(file, args, {
|
|
118
|
+
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
119
|
+
...(envSan !== undefined ? { env: envSan } : {}),
|
|
120
|
+
stdio: 'pipe',
|
|
121
|
+
...(opts.timeoutMs !== undefined
|
|
122
|
+
? { timeout: opts.timeoutMs, killSignal: 'SIGKILL' }
|
|
123
|
+
: {}),
|
|
124
|
+
});
|
|
125
|
+
const ok = pickResult(result);
|
|
126
|
+
dbg('exit:capture (plain)', { exitCode: ok.exitCode });
|
|
127
|
+
return ok;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const out = pickResult(err);
|
|
131
|
+
dbg('exit:capture:error (plain)', { exitCode: out.exitCode });
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const runCommand = async (command, shell, opts) => {
|
|
137
|
+
if (shell === false) {
|
|
138
|
+
let file;
|
|
139
|
+
let args = [];
|
|
140
|
+
if (Array.isArray(command)) {
|
|
141
|
+
file = command[0];
|
|
142
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const tokens = tokenize(command);
|
|
146
|
+
file = tokens[0];
|
|
147
|
+
args = tokens.slice(1);
|
|
148
|
+
}
|
|
149
|
+
if (!file)
|
|
150
|
+
return 0;
|
|
151
|
+
dbg('exec (plain)', { file, args, stdio: opts.stdio });
|
|
152
|
+
// Build options without injecting undefined properties (exactOptionalPropertyTypes).
|
|
153
|
+
const envSan = sanitizeEnv(opts.env);
|
|
154
|
+
const plainOpts = {};
|
|
155
|
+
if (opts.cwd !== undefined)
|
|
156
|
+
plainOpts.cwd = opts.cwd;
|
|
157
|
+
if (envSan !== undefined)
|
|
158
|
+
plainOpts.env = envSan;
|
|
159
|
+
if (opts.stdio !== undefined)
|
|
160
|
+
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'));
|
|
164
|
+
}
|
|
165
|
+
const exit = result?.exitCode;
|
|
166
|
+
dbg('exit (plain)', { exitCode: exit });
|
|
167
|
+
return typeof exit === 'number' ? exit : Number.NaN;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const commandStr = Array.isArray(command) ? command.join(' ') : command;
|
|
171
|
+
dbg('exec (shell)', {
|
|
172
|
+
shell: typeof shell === 'string' ? shell : 'custom',
|
|
173
|
+
stdio: opts.stdio,
|
|
174
|
+
command: commandStr,
|
|
175
|
+
});
|
|
176
|
+
const envSan = sanitizeEnv(opts.env);
|
|
177
|
+
const shellOpts = { shell };
|
|
178
|
+
if (opts.cwd !== undefined)
|
|
179
|
+
shellOpts.cwd = opts.cwd;
|
|
180
|
+
if (envSan !== undefined)
|
|
181
|
+
shellOpts.env = envSan;
|
|
182
|
+
if (opts.stdio !== undefined)
|
|
183
|
+
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'));
|
|
188
|
+
}
|
|
189
|
+
const exit = result?.exitCode;
|
|
190
|
+
dbg('exit (shell)', { exitCode: exit });
|
|
191
|
+
return typeof exit === 'number' ? exit : Number.NaN;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
200
|
+
* .use(childA)
|
|
201
|
+
* .use(childB);
|
|
202
|
+
*/
|
|
203
|
+
const definePlugin = (spec) => {
|
|
204
|
+
const { children = [], ...rest } = spec;
|
|
205
|
+
const plugin = {
|
|
206
|
+
...rest,
|
|
207
|
+
children: [...children],
|
|
208
|
+
use(child) {
|
|
209
|
+
this.children.push(child);
|
|
210
|
+
return this;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
return plugin;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Batch services (neutral): resolve command and shell settings.
|
|
218
|
+
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* Resolve a command string from the {@link Scripts} table.
|
|
222
|
+
* A script may be expressed as a string or an object with a `cmd` property.
|
|
223
|
+
*
|
|
224
|
+
* @param scripts - Optional scripts table.
|
|
225
|
+
* @param command - User-provided command name or string.
|
|
226
|
+
* @returns Resolved command string (falls back to the provided command).
|
|
227
|
+
*/
|
|
228
|
+
/**
|
|
229
|
+
* Resolve the shell setting for a given command:
|
|
230
|
+
* - If the script entry is an object, prefer its `shell` override.
|
|
231
|
+
* - Otherwise use the provided `shell` (string | boolean).
|
|
232
|
+
*
|
|
233
|
+
* @param scripts - Optional scripts table.
|
|
234
|
+
* @param command - User-provided command name or string.
|
|
235
|
+
* @param shell - Global shell preference (string | boolean).
|
|
236
|
+
*/
|
|
237
|
+
const resolveShell = (scripts, command, shell) => scripts && typeof scripts[command] === 'object'
|
|
238
|
+
? (scripts[command].shell ?? false)
|
|
239
|
+
: (shell ?? false);
|
|
240
|
+
|
|
241
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
242
|
+
const trim = (s) => (typeof s === 'string' ? s.trim() : '');
|
|
243
|
+
const unquote = (s) => s.length >= 2 &&
|
|
244
|
+
((s.startsWith('"') && s.endsWith('"')) ||
|
|
245
|
+
(s.startsWith("'") && s.endsWith("'")))
|
|
246
|
+
? s.slice(1, -1)
|
|
247
|
+
: s;
|
|
248
|
+
const parseExportCredentialsJson = (txt) => {
|
|
249
|
+
try {
|
|
250
|
+
const obj = JSON.parse(txt);
|
|
251
|
+
const src = obj.Credentials ?? obj;
|
|
252
|
+
const ak = src.AccessKeyId;
|
|
253
|
+
const sk = src.SecretAccessKey;
|
|
254
|
+
const tk = src.SessionToken;
|
|
255
|
+
if (ak && sk)
|
|
256
|
+
return {
|
|
257
|
+
accessKeyId: ak,
|
|
258
|
+
secretAccessKey: sk,
|
|
259
|
+
...(tk ? { sessionToken: tk } : {}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
/* ignore */
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
};
|
|
267
|
+
const parseExportCredentialsEnv = (txt) => {
|
|
268
|
+
const lines = txt.split(/\r?\n/);
|
|
269
|
+
let id;
|
|
270
|
+
let secret;
|
|
271
|
+
let token;
|
|
272
|
+
for (const raw of lines) {
|
|
273
|
+
const line = raw.trim();
|
|
274
|
+
if (!line)
|
|
275
|
+
continue;
|
|
276
|
+
// POSIX: export AWS_ACCESS_KEY_ID=..., export AWS_SECRET_ACCESS_KEY=..., export AWS_SESSION_TOKEN=...
|
|
277
|
+
let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
|
|
278
|
+
if (!m) {
|
|
279
|
+
// PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
|
|
280
|
+
m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
|
|
281
|
+
}
|
|
282
|
+
if (!m)
|
|
283
|
+
continue;
|
|
284
|
+
const k = m[1];
|
|
285
|
+
const valRaw = m[2];
|
|
286
|
+
if (typeof valRaw !== 'string')
|
|
287
|
+
continue;
|
|
288
|
+
let v = unquote(valRaw.trim());
|
|
289
|
+
// Drop trailing semicolons if present (some shells)
|
|
290
|
+
v = v.replace(/;$/, '');
|
|
291
|
+
if (k === 'AWS_ACCESS_KEY_ID')
|
|
292
|
+
id = v;
|
|
293
|
+
else if (k === 'AWS_SECRET_ACCESS_KEY')
|
|
294
|
+
secret = v;
|
|
295
|
+
else if (k === 'AWS_SESSION_TOKEN')
|
|
296
|
+
token = v;
|
|
297
|
+
}
|
|
298
|
+
if (id && secret)
|
|
299
|
+
return {
|
|
300
|
+
accessKeyId: id,
|
|
301
|
+
secretAccessKey: secret,
|
|
302
|
+
...(token ? { sessionToken: token } : {}),
|
|
303
|
+
};
|
|
304
|
+
return undefined;
|
|
305
|
+
};
|
|
306
|
+
const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
|
|
307
|
+
const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
|
|
308
|
+
env: process.env,
|
|
309
|
+
timeoutMs,
|
|
310
|
+
});
|
|
311
|
+
// Guard for mocked undefined in tests; keep narrow lint scope.
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
313
|
+
if (!r || typeof r.exitCode !== 'number')
|
|
314
|
+
return undefined;
|
|
315
|
+
if (r.exitCode === 0) {
|
|
316
|
+
const v = trim(r.stdout);
|
|
317
|
+
return v.length > 0 ? v : undefined;
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
};
|
|
321
|
+
const exportCredentials = async (profile, timeoutMs = DEFAULT_TIMEOUT_MS) => {
|
|
322
|
+
// Try JSON format first (AWS CLI v2)
|
|
323
|
+
const rJson = await runCommandResult([
|
|
324
|
+
'aws',
|
|
325
|
+
'configure',
|
|
326
|
+
'export-credentials',
|
|
327
|
+
'--profile',
|
|
328
|
+
profile,
|
|
329
|
+
'--format',
|
|
330
|
+
'json',
|
|
331
|
+
], false, { env: process.env, timeoutMs });
|
|
332
|
+
if (rJson.exitCode === 0) {
|
|
333
|
+
const creds = parseExportCredentialsJson(rJson.stdout);
|
|
334
|
+
if (creds)
|
|
335
|
+
return creds;
|
|
336
|
+
}
|
|
337
|
+
// Fallback: env lines
|
|
338
|
+
const rEnv = await runCommandResult(['aws', 'configure', 'export-credentials', '--profile', profile], false, { env: process.env, timeoutMs });
|
|
339
|
+
if (rEnv.exitCode === 0) {
|
|
340
|
+
const creds = parseExportCredentialsEnv(rEnv.stdout);
|
|
341
|
+
if (creds)
|
|
342
|
+
return creds;
|
|
343
|
+
}
|
|
344
|
+
return undefined;
|
|
345
|
+
};
|
|
346
|
+
const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
347
|
+
const profileKey = cfg.profileKey ?? 'AWS_LOCAL_PROFILE';
|
|
348
|
+
const profileFallbackKey = cfg.profileFallbackKey ?? 'AWS_PROFILE';
|
|
349
|
+
const regionKey = cfg.regionKey ?? 'AWS_REGION';
|
|
350
|
+
const profile = cfg.profile ??
|
|
351
|
+
dotenv[profileKey] ??
|
|
352
|
+
dotenv[profileFallbackKey] ??
|
|
353
|
+
undefined;
|
|
354
|
+
let region = cfg.region ?? dotenv[regionKey] ?? undefined;
|
|
355
|
+
// Short-circuit when strategy is disabled.
|
|
356
|
+
if (cfg.strategy === 'none') {
|
|
357
|
+
// If region is still missing and we have a profile, try best-effort region resolve.
|
|
358
|
+
if (!region && profile)
|
|
359
|
+
region = await getAwsConfigure('region', profile);
|
|
360
|
+
if (!region && cfg.defaultRegion)
|
|
361
|
+
region = cfg.defaultRegion;
|
|
362
|
+
const out = {};
|
|
363
|
+
if (profile !== undefined)
|
|
364
|
+
out.profile = profile;
|
|
365
|
+
if (region !== undefined)
|
|
366
|
+
out.region = region;
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
// Env-first credentials.
|
|
370
|
+
let credentials;
|
|
371
|
+
const envId = trim(process.env.AWS_ACCESS_KEY_ID);
|
|
372
|
+
const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
|
|
373
|
+
const envToken = trim(process.env.AWS_SESSION_TOKEN);
|
|
374
|
+
if (envId && envSecret) {
|
|
375
|
+
credentials = {
|
|
376
|
+
accessKeyId: envId,
|
|
377
|
+
secretAccessKey: envSecret,
|
|
378
|
+
...(envToken ? { sessionToken: envToken } : {}),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
else if (profile) {
|
|
382
|
+
// Try export-credentials
|
|
383
|
+
credentials = await exportCredentials(profile);
|
|
384
|
+
// On failure, detect SSO and optionally login then retry
|
|
385
|
+
if (!credentials) {
|
|
386
|
+
const ssoSession = await getAwsConfigure('sso_session', profile);
|
|
387
|
+
const looksSSO = typeof ssoSession === 'string' && ssoSession.length > 0;
|
|
388
|
+
if (looksSSO && cfg.loginOnDemand) {
|
|
389
|
+
// Best-effort login, then retry export once.
|
|
390
|
+
await runCommandResult(['aws', 'sso', 'login', '--profile', profile], false, {
|
|
391
|
+
env: process.env,
|
|
392
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
393
|
+
});
|
|
394
|
+
credentials = await exportCredentials(profile);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Static fallback if still missing.
|
|
398
|
+
if (!credentials) {
|
|
399
|
+
const id = await getAwsConfigure('aws_access_key_id', profile);
|
|
400
|
+
const secret = await getAwsConfigure('aws_secret_access_key', profile);
|
|
401
|
+
const token = await getAwsConfigure('aws_session_token', profile);
|
|
402
|
+
if (id && secret) {
|
|
403
|
+
credentials = {
|
|
404
|
+
accessKeyId: id,
|
|
405
|
+
secretAccessKey: secret,
|
|
406
|
+
...(token ? { sessionToken: token } : {}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Final region resolution
|
|
412
|
+
if (!region && profile)
|
|
413
|
+
region = await getAwsConfigure('region', profile);
|
|
414
|
+
if (!region && cfg.defaultRegion)
|
|
415
|
+
region = cfg.defaultRegion;
|
|
416
|
+
const out = {};
|
|
417
|
+
if (profile !== undefined)
|
|
418
|
+
out.profile = profile;
|
|
419
|
+
if (region !== undefined)
|
|
420
|
+
out.region = region;
|
|
421
|
+
if (credentials)
|
|
422
|
+
out.credentials = credentials;
|
|
423
|
+
return out;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const AwsPluginConfigSchema = z.object({
|
|
427
|
+
profile: z.string().optional(),
|
|
428
|
+
region: z.string().optional(),
|
|
429
|
+
defaultRegion: z.string().optional(),
|
|
430
|
+
profileKey: z.string().default('AWS_LOCAL_PROFILE').optional(),
|
|
431
|
+
profileFallbackKey: z.string().default('AWS_PROFILE').optional(),
|
|
432
|
+
regionKey: z.string().default('AWS_REGION').optional(),
|
|
433
|
+
strategy: z.enum(['cli-export', 'none']).default('cli-export').optional(),
|
|
434
|
+
loginOnDemand: z.boolean().default(false).optional(),
|
|
435
|
+
setEnv: z.boolean().default(true).optional(),
|
|
436
|
+
addCtx: z.boolean().default(true).optional(),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const awsPlugin = () => definePlugin({
|
|
440
|
+
id: 'aws',
|
|
441
|
+
// Host validates this slice when the loader path is active.
|
|
442
|
+
configSchema: AwsPluginConfigSchema,
|
|
443
|
+
setup(cli) {
|
|
444
|
+
// Subcommand: aws
|
|
445
|
+
cli
|
|
446
|
+
.ns('aws')
|
|
447
|
+
.description('Establish an AWS session and optionally forward to the AWS CLI')
|
|
448
|
+
.configureHelp({ showGlobalOptions: true })
|
|
449
|
+
.enablePositionalOptions()
|
|
450
|
+
.passThroughOptions()
|
|
451
|
+
.allowUnknownOption(true)
|
|
452
|
+
// Boolean toggles
|
|
453
|
+
.option('--login-on-demand', 'attempt aws sso login on-demand')
|
|
454
|
+
.option('--no-login-on-demand', 'disable sso login on-demand')
|
|
455
|
+
.option('--set-env', 'write resolved values into process.env')
|
|
456
|
+
.option('--no-set-env', 'do not write resolved values into process.env')
|
|
457
|
+
.option('--add-ctx', 'mirror results under ctx.plugins.aws')
|
|
458
|
+
.option('--no-add-ctx', 'do not mirror results under ctx.plugins.aws')
|
|
459
|
+
// Strings / enums
|
|
460
|
+
.option('--profile <string>', 'AWS profile name')
|
|
461
|
+
.option('--region <string>', 'AWS region')
|
|
462
|
+
.option('--default-region <string>', 'fallback region')
|
|
463
|
+
.option('--strategy <string>', 'credential acquisition strategy: cli-export|none')
|
|
464
|
+
// Advanced key overrides
|
|
465
|
+
.option('--profile-key <string>', 'dotenv/config key for local profile')
|
|
466
|
+
.option('--profile-fallback-key <string>', 'fallback dotenv/config key for profile')
|
|
467
|
+
.option('--region-key <string>', 'dotenv/config key for region')
|
|
468
|
+
// Accept any extra operands so Commander does not error when tokens appear after "--".
|
|
469
|
+
.argument('[args...]')
|
|
470
|
+
.action(async (args, opts, thisCommand) => {
|
|
471
|
+
const self = thisCommand;
|
|
472
|
+
const parent = (self.parent ?? null);
|
|
473
|
+
// Access merged root CLI options (installed by passOptions())
|
|
474
|
+
const rootOpts = (parent?.getDotenvCliOptions ?? {});
|
|
475
|
+
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
476
|
+
Boolean(rootOpts?.capture);
|
|
477
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
478
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
479
|
+
// Build overlay cfg from subcommand flags layered over discovered config.
|
|
480
|
+
const ctx = cli.getCtx();
|
|
481
|
+
const cfgBase = (ctx?.pluginConfigs?.['aws'] ??
|
|
482
|
+
{});
|
|
483
|
+
const overlay = {};
|
|
484
|
+
// Map boolean toggles (respect explicit --no-*)
|
|
485
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'loginOnDemand'))
|
|
486
|
+
overlay.loginOnDemand = Boolean(opts.loginOnDemand);
|
|
487
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'setEnv'))
|
|
488
|
+
overlay.setEnv = Boolean(opts.setEnv);
|
|
489
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'addCtx'))
|
|
490
|
+
overlay.addCtx = Boolean(opts.addCtx);
|
|
491
|
+
// Strings/enums
|
|
492
|
+
if (typeof opts.profile === 'string')
|
|
493
|
+
overlay.profile = opts.profile;
|
|
494
|
+
if (typeof opts.region === 'string')
|
|
495
|
+
overlay.region = opts.region;
|
|
496
|
+
if (typeof opts.defaultRegion === 'string')
|
|
497
|
+
overlay.defaultRegion = opts.defaultRegion;
|
|
498
|
+
if (typeof opts.strategy === 'string')
|
|
499
|
+
overlay.strategy =
|
|
500
|
+
opts.strategy;
|
|
501
|
+
// Advanced key overrides
|
|
502
|
+
if (typeof opts.profileKey === 'string')
|
|
503
|
+
overlay.profileKey = opts.profileKey;
|
|
504
|
+
if (typeof opts.profileFallbackKey === 'string')
|
|
505
|
+
overlay.profileFallbackKey = opts.profileFallbackKey;
|
|
506
|
+
if (typeof opts.regionKey === 'string')
|
|
507
|
+
overlay.regionKey = opts.regionKey;
|
|
508
|
+
const cfg = {
|
|
509
|
+
...cfgBase,
|
|
510
|
+
...overlay,
|
|
511
|
+
};
|
|
512
|
+
// Resolve current context with overrides
|
|
513
|
+
const out = await resolveAwsContext({
|
|
514
|
+
dotenv: ctx?.dotenv ?? {},
|
|
515
|
+
cfg,
|
|
516
|
+
});
|
|
517
|
+
// Apply env/ctx mirrors per toggles
|
|
518
|
+
if (cfg.setEnv !== false) {
|
|
519
|
+
if (out.region) {
|
|
520
|
+
process.env.AWS_REGION = out.region;
|
|
521
|
+
if (!process.env.AWS_DEFAULT_REGION)
|
|
522
|
+
process.env.AWS_DEFAULT_REGION = out.region;
|
|
523
|
+
}
|
|
524
|
+
if (out.credentials) {
|
|
525
|
+
process.env.AWS_ACCESS_KEY_ID = out.credentials.accessKeyId;
|
|
526
|
+
process.env.AWS_SECRET_ACCESS_KEY =
|
|
527
|
+
out.credentials.secretAccessKey;
|
|
528
|
+
if (out.credentials.sessionToken !== undefined) {
|
|
529
|
+
process.env.AWS_SESSION_TOKEN = out.credentials.sessionToken;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (cfg.addCtx !== false) {
|
|
534
|
+
if (ctx) {
|
|
535
|
+
ctx.plugins ??= {};
|
|
536
|
+
ctx.plugins['aws'] = {
|
|
537
|
+
...(out.profile ? { profile: out.profile } : {}),
|
|
538
|
+
...(out.region ? { region: out.region } : {}),
|
|
539
|
+
...(out.credentials ? { credentials: out.credentials } : {}),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Forward when positional args are present; otherwise session-only.
|
|
544
|
+
if (Array.isArray(args) && args.length > 0) {
|
|
545
|
+
const argv = ['aws', ...args];
|
|
546
|
+
const shellSetting = resolveShell(rootOpts?.scripts, 'aws', rootOpts?.shell);
|
|
547
|
+
const ctxDotenv = (ctx?.dotenv ?? {});
|
|
548
|
+
const exit = await runCommand(argv, shellSetting, {
|
|
549
|
+
env: { ...process.env, ...ctxDotenv },
|
|
550
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
551
|
+
});
|
|
552
|
+
// Deterministic termination (suppressed under tests)
|
|
553
|
+
if (!underTests) {
|
|
554
|
+
process.exit(typeof exit === 'number' ? exit : 0);
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
// Session only: low-noise breadcrumb under debug
|
|
560
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
561
|
+
const log = console;
|
|
562
|
+
log.log('[aws] session established', {
|
|
563
|
+
profile: out.profile,
|
|
564
|
+
region: out.region,
|
|
565
|
+
hasCreds: Boolean(out.credentials),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (!underTests)
|
|
569
|
+
process.exit(0);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
},
|
|
574
|
+
async afterResolve(_cli, ctx) {
|
|
575
|
+
const log = console;
|
|
576
|
+
const cfgRaw = (ctx.pluginConfigs?.['aws'] ?? {});
|
|
577
|
+
const cfg = (cfgRaw || {});
|
|
578
|
+
const out = await resolveAwsContext({
|
|
579
|
+
dotenv: ctx.dotenv,
|
|
580
|
+
cfg,
|
|
581
|
+
});
|
|
582
|
+
const { profile, region, credentials } = out;
|
|
583
|
+
if (cfg.setEnv !== false) {
|
|
584
|
+
if (region) {
|
|
585
|
+
process.env.AWS_REGION = region;
|
|
586
|
+
if (!process.env.AWS_DEFAULT_REGION)
|
|
587
|
+
process.env.AWS_DEFAULT_REGION = region;
|
|
588
|
+
}
|
|
589
|
+
if (credentials) {
|
|
590
|
+
process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
|
|
591
|
+
process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
|
|
592
|
+
if (credentials.sessionToken !== undefined) {
|
|
593
|
+
process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (cfg.addCtx !== false) {
|
|
598
|
+
ctx.plugins ??= {};
|
|
599
|
+
ctx.plugins['aws'] = {
|
|
600
|
+
...(profile ? { profile } : {}),
|
|
601
|
+
...(region ? { region } : {}),
|
|
602
|
+
...(credentials ? { credentials } : {}),
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
// Optional: low-noise breadcrumb for diagnostics
|
|
606
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
607
|
+
log.log('[aws] afterResolve', {
|
|
608
|
+
profile,
|
|
609
|
+
region,
|
|
610
|
+
hasCreds: Boolean(credentials),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
export { awsPlugin };
|