@karmaniverous/get-dotenv 6.0.0 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -14
- package/dist/cli.mjs +744 -360
- package/dist/cliHost.d.ts +14 -1
- package/dist/cliHost.mjs +54 -8
- package/dist/config.d.ts +4 -0
- package/dist/config.mjs +17 -7
- package/dist/env-overlay.d.ts +11 -0
- package/dist/env-overlay.mjs +24 -7
- package/dist/getdotenv.cli.mjs +744 -360
- package/dist/index.d.ts +88 -2
- package/dist/index.mjs +757 -361
- package/dist/plugins-aws.d.ts +4 -3
- package/dist/plugins-aws.mjs +369 -177
- package/dist/plugins-batch.d.ts +2 -1
- package/dist/plugins-batch.mjs +124 -48
- package/dist/plugins-cmd.d.ts +2 -1
- package/dist/plugins-cmd.mjs +77 -22
- package/dist/plugins-init.d.ts +2 -1
- package/dist/plugins-init.mjs +235 -128
- package/dist/plugins.d.ts +7 -2
- package/dist/plugins.mjs +740 -360
- package/dist/templates/cli/index.ts +1 -2
- package/dist/templates/cli/plugins/hello/defaultAction.ts +27 -0
- package/dist/templates/cli/plugins/hello/index.ts +26 -0
- package/dist/templates/cli/plugins/hello/options.ts +31 -0
- package/dist/templates/cli/plugins/hello/strangerAction.ts +20 -0
- package/dist/templates/cli/plugins/hello/types.ts +13 -0
- package/dist/templates/config/ts/getdotenv.config.ts +1 -1
- package/dist/templates/defaultAction.ts +27 -0
- package/dist/templates/getdotenv.config.ts +1 -1
- package/dist/templates/hello/defaultAction.ts +27 -0
- package/dist/templates/hello/index.ts +26 -0
- package/dist/templates/hello/options.ts +31 -0
- package/dist/templates/hello/strangerAction.ts +20 -0
- package/dist/templates/hello/types.ts +13 -0
- package/dist/templates/index.ts +22 -22
- package/dist/templates/options.ts +31 -0
- package/dist/templates/plugins/hello/defaultAction.ts +27 -0
- package/dist/templates/plugins/hello/index.ts +26 -0
- package/dist/templates/plugins/hello/options.ts +31 -0
- package/dist/templates/plugins/hello/strangerAction.ts +20 -0
- package/dist/templates/plugins/hello/types.ts +13 -0
- package/dist/templates/strangerAction.ts +20 -0
- package/dist/templates/ts/getdotenv.config.ts +1 -1
- package/dist/templates/types.ts +13 -0
- package/package.json +2 -2
- package/templates/cli/index.ts +1 -2
- package/templates/cli/plugins/hello/defaultAction.ts +27 -0
- package/templates/cli/plugins/hello/index.ts +26 -0
- package/templates/cli/plugins/hello/options.ts +31 -0
- package/templates/cli/plugins/hello/strangerAction.ts +20 -0
- package/templates/cli/plugins/hello/types.ts +13 -0
- package/templates/config/ts/getdotenv.config.ts +1 -1
- package/dist/templates/cli/plugins/hello.ts +0 -43
- package/dist/templates/hello.ts +0 -43
- package/dist/templates/plugins/hello.ts +0 -43
- package/templates/cli/plugins/hello.ts +0 -43
package/dist/cli.mjs
CHANGED
|
@@ -169,6 +169,10 @@ function defaultsDeep(...layers) {
|
|
|
169
169
|
/**
|
|
170
170
|
* Serialize a dotenv record to a file with minimal quoting (multiline values are quoted).
|
|
171
171
|
* Future-proofs for ordering/sorting changes (currently insertion order).
|
|
172
|
+
*
|
|
173
|
+
* @param filename - Destination dotenv file path.
|
|
174
|
+
* @param data - Env-like map of values to write (values may be `undefined`).
|
|
175
|
+
* @returns A `Promise\<void\>` which resolves when the file has been written.
|
|
172
176
|
*/
|
|
173
177
|
async function writeDotenvFile(filename, data) {
|
|
174
178
|
// Serialize: key=value with quotes only for multiline values.
|
|
@@ -404,14 +408,20 @@ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Numbe
|
|
|
404
408
|
}
|
|
405
409
|
};
|
|
406
410
|
/**
|
|
407
|
-
* Load a module default export from a JS/TS file with robust fallbacks
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
411
|
+
* Load a module default export from a JS/TS file with robust fallbacks.
|
|
412
|
+
*
|
|
413
|
+
* Behavior by extension:
|
|
414
|
+
*
|
|
415
|
+
* - `.js`/`.mjs`/`.cjs`: direct dynamic import.
|
|
416
|
+
* - `.ts`/`.mts`/`.cts`/`.tsx`:
|
|
417
|
+
* - try direct dynamic import (when a TS loader is active),
|
|
418
|
+
* - else compile via `esbuild` to a cached `.mjs` file and import,
|
|
419
|
+
* - else fallback to `typescript.transpileModule` for simple modules.
|
|
412
420
|
*
|
|
413
|
-
* @
|
|
414
|
-
* @param
|
|
421
|
+
* @typeParam T - Type of the expected default export.
|
|
422
|
+
* @param absPath - Absolute path to the source file.
|
|
423
|
+
* @param cacheDirName - Cache subfolder under `.tsbuild/`.
|
|
424
|
+
* @returns A `Promise\<T | undefined\>` resolving to the default export (if any).
|
|
415
425
|
*/
|
|
416
426
|
const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
417
427
|
const ext = path.extname(absPath).toLowerCase();
|
|
@@ -483,6 +493,10 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
|
483
493
|
/**
|
|
484
494
|
* Omit keys whose runtime value is undefined from a shallow object.
|
|
485
495
|
* Returns a Partial with non-undefined value types preserved.
|
|
496
|
+
*
|
|
497
|
+
* @typeParam T - Input object shape.
|
|
498
|
+
* @param obj - Object to filter.
|
|
499
|
+
* @returns A shallow copy of `obj` without keys whose value is `undefined`.
|
|
486
500
|
*/
|
|
487
501
|
function omitUndefined(obj) {
|
|
488
502
|
const out = {};
|
|
@@ -494,6 +508,10 @@ function omitUndefined(obj) {
|
|
|
494
508
|
}
|
|
495
509
|
/**
|
|
496
510
|
* Specialized helper for env-like maps: drop undefined and return string-only.
|
|
511
|
+
*
|
|
512
|
+
* @typeParam V - Value type for present entries (must extend `string`).
|
|
513
|
+
* @param obj - Env-like record containing `string | undefined` values.
|
|
514
|
+
* @returns A new record containing only the keys with defined values.
|
|
497
515
|
*/
|
|
498
516
|
function omitUndefinedRecord(obj) {
|
|
499
517
|
const out = {};
|
|
@@ -711,6 +729,10 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
|
711
729
|
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
712
730
|
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
713
731
|
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
732
|
+
*
|
|
733
|
+
* @param finalEnv - Final composed environment to validate.
|
|
734
|
+
* @param sources - Resolved config sources providing `schema` and/or `requiredKeys`.
|
|
735
|
+
* @returns A list of human-readable issue strings (empty when valid).
|
|
714
736
|
*/
|
|
715
737
|
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
716
738
|
const pick = (getter) => {
|
|
@@ -770,6 +792,11 @@ const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
|
770
792
|
* Apply a dynamic map to the target progressively.
|
|
771
793
|
* - Functions receive (target, env) and may return string | undefined.
|
|
772
794
|
* - Literals are assigned directly (including undefined).
|
|
795
|
+
*
|
|
796
|
+
* @param target - Mutable target environment to assign into.
|
|
797
|
+
* @param map - Dynamic map to apply (functions and/or literal values).
|
|
798
|
+
* @param env - Selected environment name (if any) passed through to dynamic functions.
|
|
799
|
+
* @returns Nothing.
|
|
773
800
|
*/
|
|
774
801
|
function applyDynamicMap(target, map, env) {
|
|
775
802
|
if (!map)
|
|
@@ -789,6 +816,12 @@ function applyDynamicMap(target, map, env) {
|
|
|
789
816
|
* Error behavior:
|
|
790
817
|
* - On failure to load/compile/evaluate the module, throws a unified message:
|
|
791
818
|
* "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
|
|
819
|
+
*
|
|
820
|
+
* @param target - Mutable target environment to assign into.
|
|
821
|
+
* @param absPath - Absolute path to the dynamic module file.
|
|
822
|
+
* @param env - Selected environment name (if any).
|
|
823
|
+
* @param cacheDirName - Cache subdirectory under `.tsbuild/` for compiled artifacts.
|
|
824
|
+
* @returns A `Promise\<void\>` which resolves after the module (if present) has been applied.
|
|
792
825
|
*/
|
|
793
826
|
async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
|
|
794
827
|
if (!(await fs.exists(absPath)))
|
|
@@ -1951,6 +1984,10 @@ function renderOptionGroups(cmd) {
|
|
|
1951
1984
|
/**
|
|
1952
1985
|
* Compose root/parent help output by inserting grouped sections between
|
|
1953
1986
|
* Options and Commands, ensuring a trailing blank line.
|
|
1987
|
+
*
|
|
1988
|
+
* @param base - Base help text produced by Commander.
|
|
1989
|
+
* @param cmd - Command instance whose grouped options should be rendered.
|
|
1990
|
+
* @returns The modified help text with grouped blocks inserted.
|
|
1954
1991
|
*/
|
|
1955
1992
|
function buildHelpInformation(base, cmd) {
|
|
1956
1993
|
const groups = renderOptionGroups(cmd);
|
|
@@ -2463,6 +2500,10 @@ const toHelpConfig = (merged, plugins) => {
|
|
|
2463
2500
|
/**
|
|
2464
2501
|
* Compose a child-process env overlay from dotenv and the merged CLI options bag.
|
|
2465
2502
|
* Returns a shallow object including getDotenvCliOptions when serializable.
|
|
2503
|
+
*
|
|
2504
|
+
* @param merged - Resolved CLI options bag (or a JSON-serializable subset).
|
|
2505
|
+
* @param dotenv - Composed dotenv variables for the current invocation.
|
|
2506
|
+
* @returns A string-only env overlay suitable for child process spawning.
|
|
2466
2507
|
*/
|
|
2467
2508
|
function composeNestedEnv(merged, dotenv) {
|
|
2468
2509
|
const out = {};
|
|
@@ -2485,6 +2526,7 @@ function composeNestedEnv(merged, dotenv) {
|
|
|
2485
2526
|
* Strip one layer of symmetric outer quotes (single or double) from a string.
|
|
2486
2527
|
*
|
|
2487
2528
|
* @param s - Input string.
|
|
2529
|
+
* @returns `s` without one symmetric outer quote pair (when present).
|
|
2488
2530
|
*/
|
|
2489
2531
|
const stripOne = (s) => {
|
|
2490
2532
|
if (s.length < 2)
|
|
@@ -2497,6 +2539,9 @@ const stripOne = (s) => {
|
|
|
2497
2539
|
/**
|
|
2498
2540
|
* Preserve argv array for Node -e/--eval payloads under shell-off and
|
|
2499
2541
|
* peel one symmetric outer quote layer from the code argument.
|
|
2542
|
+
*
|
|
2543
|
+
* @param args - Argument vector intended for direct execution (shell-off).
|
|
2544
|
+
* @returns Either the original `args` or a modified copy with a normalized eval payload.
|
|
2500
2545
|
*/
|
|
2501
2546
|
function maybePreserveNodeEvalArgv(args) {
|
|
2502
2547
|
if (args.length >= 3) {
|
|
@@ -2963,23 +3008,57 @@ function applyRootVisibility(program, visibility) {
|
|
|
2963
3008
|
* Apply resolved AWS context to `process.env` and `ctx.plugins`.
|
|
2964
3009
|
* Centralizes logic shared between the plugin action and `afterResolve` hook.
|
|
2965
3010
|
*
|
|
3011
|
+
* @param out - Resolved AWS context to apply.
|
|
3012
|
+
* @param ctx - Host context to publish non-sensitive metadata into.
|
|
2966
3013
|
* @param setProcessEnv - Whether to write credentials/region to `process.env` (default true).
|
|
3014
|
+
* @returns Nothing.
|
|
2967
3015
|
*/
|
|
2968
3016
|
function applyAwsContext(out, ctx, setProcessEnv = true) {
|
|
2969
3017
|
const { profile, region, credentials } = out;
|
|
2970
3018
|
if (setProcessEnv) {
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3019
|
+
// Ensure AWS credential sources are mutually exclusive.
|
|
3020
|
+
// The AWS SDK warns (and may change precedence in future) when both
|
|
3021
|
+
// AWS_PROFILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are set.
|
|
3022
|
+
const clear = (keys) => {
|
|
3023
|
+
for (const k of keys) {
|
|
3024
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
3025
|
+
delete process.env[k];
|
|
2975
3026
|
}
|
|
2976
|
-
}
|
|
3027
|
+
};
|
|
3028
|
+
const clearProfileVars = () => {
|
|
3029
|
+
clear(['AWS_PROFILE', 'AWS_DEFAULT_PROFILE', 'AWS_SDK_LOAD_CONFIG']);
|
|
3030
|
+
};
|
|
3031
|
+
const clearStaticCreds = () => {
|
|
3032
|
+
clear([
|
|
3033
|
+
'AWS_ACCESS_KEY_ID',
|
|
3034
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
3035
|
+
'AWS_SESSION_TOKEN',
|
|
3036
|
+
]);
|
|
3037
|
+
};
|
|
3038
|
+
// Mode A: exported/static credentials (clear profile vars)
|
|
2977
3039
|
if (credentials) {
|
|
3040
|
+
clearProfileVars();
|
|
2978
3041
|
process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
|
|
2979
3042
|
process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
|
|
2980
3043
|
if (credentials.sessionToken !== undefined) {
|
|
2981
3044
|
process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
|
|
2982
3045
|
}
|
|
3046
|
+
else {
|
|
3047
|
+
delete process.env.AWS_SESSION_TOKEN;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
else if (profile) {
|
|
3051
|
+
// Mode B: profile-based (SSO) credentials (clear static creds)
|
|
3052
|
+
clearStaticCreds();
|
|
3053
|
+
process.env.AWS_PROFILE = profile;
|
|
3054
|
+
process.env.AWS_DEFAULT_PROFILE = profile;
|
|
3055
|
+
process.env.AWS_SDK_LOAD_CONFIG = '1';
|
|
3056
|
+
}
|
|
3057
|
+
if (region) {
|
|
3058
|
+
process.env.AWS_REGION = region;
|
|
3059
|
+
if (!process.env.AWS_DEFAULT_REGION) {
|
|
3060
|
+
process.env.AWS_DEFAULT_REGION = region;
|
|
3061
|
+
}
|
|
2983
3062
|
}
|
|
2984
3063
|
}
|
|
2985
3064
|
// Always publish minimal, non-sensitive metadata
|
|
@@ -2990,7 +3069,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
|
|
|
2990
3069
|
};
|
|
2991
3070
|
}
|
|
2992
3071
|
|
|
2993
|
-
const
|
|
3072
|
+
const AWS_CLI_TIMEOUT_MS = 15_000;
|
|
2994
3073
|
const trim = (s) => (typeof s === 'string' ? s.trim() : '');
|
|
2995
3074
|
const unquote = (s) => s.length >= 2 &&
|
|
2996
3075
|
((s.startsWith('"') && s.endsWith('"')) ||
|
|
@@ -2999,6 +3078,9 @@ const unquote = (s) => s.length >= 2 &&
|
|
|
2999
3078
|
: s;
|
|
3000
3079
|
/**
|
|
3001
3080
|
* Parse AWS credentials from JSON output (AWS CLI v2 export-credentials).
|
|
3081
|
+
*
|
|
3082
|
+
* @param txt - Raw stdout text from the AWS CLI.
|
|
3083
|
+
* @returns Parsed credentials, or `undefined` when the input is not recognized.
|
|
3002
3084
|
*/
|
|
3003
3085
|
const parseExportCredentialsJson = (txt) => {
|
|
3004
3086
|
try {
|
|
@@ -3022,6 +3104,10 @@ const parseExportCredentialsJson = (txt) => {
|
|
|
3022
3104
|
/**
|
|
3023
3105
|
* Parse AWS credentials from environment-export output (shell-agnostic).
|
|
3024
3106
|
* Supports POSIX `export KEY=VAL` and PowerShell `$Env:KEY=VAL`.
|
|
3107
|
+
* Also supports AWS CLI `windows-cmd` (`set KEY=VAL`) and `env-no-export` (`KEY=VAL`).
|
|
3108
|
+
*
|
|
3109
|
+
* @param txt - Raw stdout text from the AWS CLI.
|
|
3110
|
+
* @returns Parsed credentials, or `undefined` when the input is not recognized.
|
|
3025
3111
|
*/
|
|
3026
3112
|
const parseExportCredentialsEnv = (txt) => {
|
|
3027
3113
|
const lines = txt.split(/\r?\n/);
|
|
@@ -3032,12 +3118,17 @@ const parseExportCredentialsEnv = (txt) => {
|
|
|
3032
3118
|
const line = raw.trim();
|
|
3033
3119
|
if (!line)
|
|
3034
3120
|
continue;
|
|
3035
|
-
// POSIX: export AWS_ACCESS_KEY_ID=...,
|
|
3121
|
+
// POSIX: export AWS_ACCESS_KEY_ID=..., ...
|
|
3036
3122
|
let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)
|
|
3040
|
-
|
|
3123
|
+
// PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
|
|
3124
|
+
if (!m)
|
|
3125
|
+
m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
|
|
3126
|
+
// Windows cmd: set AWS_ACCESS_KEY_ID=..., etc.
|
|
3127
|
+
if (!m)
|
|
3128
|
+
m = /^(?:set)\s+([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
|
|
3129
|
+
// env-no-export: AWS_ACCESS_KEY_ID=..., etc.
|
|
3130
|
+
if (!m)
|
|
3131
|
+
m = /^([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
|
|
3041
3132
|
if (!m)
|
|
3042
3133
|
continue;
|
|
3043
3134
|
const k = m[1];
|
|
@@ -3062,7 +3153,7 @@ const parseExportCredentialsEnv = (txt) => {
|
|
|
3062
3153
|
};
|
|
3063
3154
|
return undefined;
|
|
3064
3155
|
};
|
|
3065
|
-
const getAwsConfigure = async (key, profile, timeoutMs =
|
|
3156
|
+
const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
|
|
3066
3157
|
const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
|
|
3067
3158
|
env: process.env,
|
|
3068
3159
|
timeoutMs,
|
|
@@ -3077,36 +3168,50 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
|
|
|
3077
3168
|
}
|
|
3078
3169
|
return undefined;
|
|
3079
3170
|
};
|
|
3080
|
-
const exportCredentials = async (profile, timeoutMs =
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3171
|
+
const exportCredentials = async (profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
|
|
3172
|
+
const tryExport = async (format) => {
|
|
3173
|
+
const argv = [
|
|
3174
|
+
'aws',
|
|
3175
|
+
'configure',
|
|
3176
|
+
'export-credentials',
|
|
3177
|
+
'--profile',
|
|
3178
|
+
profile,
|
|
3179
|
+
...(format ? ['--format', format] : []),
|
|
3180
|
+
];
|
|
3181
|
+
const r = await runCommandResult(argv, false, {
|
|
3182
|
+
env: process.env,
|
|
3183
|
+
timeoutMs,
|
|
3184
|
+
});
|
|
3185
|
+
if (r.exitCode !== 0)
|
|
3186
|
+
return undefined;
|
|
3187
|
+
const out = trim(r.stdout);
|
|
3188
|
+
if (!out)
|
|
3189
|
+
return undefined;
|
|
3190
|
+
// Some formats produce JSON ("process"), some produce shell-ish env lines.
|
|
3191
|
+
return parseExportCredentialsJson(out) ?? parseExportCredentialsEnv(out);
|
|
3192
|
+
};
|
|
3193
|
+
// Prefer the default/JSON "process" format first; then fall back to shell env outputs.
|
|
3194
|
+
// Note: AWS CLI v2 supports: process | env | env-no-export | powershell | windows-cmd
|
|
3195
|
+
const formats = [
|
|
3196
|
+
'process',
|
|
3197
|
+
...(process.platform === 'win32'
|
|
3198
|
+
? ['powershell', 'windows-cmd', 'env', 'env-no-export']
|
|
3199
|
+
: ['env', 'env-no-export']),
|
|
3200
|
+
];
|
|
3201
|
+
for (const f of formats) {
|
|
3202
|
+
const creds = await tryExport(f);
|
|
3100
3203
|
if (creds)
|
|
3101
3204
|
return creds;
|
|
3102
3205
|
}
|
|
3103
|
-
|
|
3206
|
+
// Final fallback: no --format (AWS CLI default output)
|
|
3207
|
+
return tryExport(undefined);
|
|
3104
3208
|
};
|
|
3105
3209
|
/**
|
|
3106
3210
|
* Resolve AWS context (profile, region, credentials) using configuration and environment.
|
|
3107
3211
|
* Applies strategy (cli-export vs none) and handling for SSO login-on-demand.
|
|
3108
3212
|
*
|
|
3109
3213
|
* @param options - Context options including current dotenv and plugin config.
|
|
3214
|
+
* @returns A `Promise\<AwsContext\>` containing any resolved profile, region, and credentials.
|
|
3110
3215
|
*/
|
|
3111
3216
|
const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
3112
3217
|
const profileKey = cfg.profileKey ?? 'AWS_LOCAL_PROFILE';
|
|
@@ -3131,31 +3236,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
|
3131
3236
|
out.region = region;
|
|
3132
3237
|
return out;
|
|
3133
3238
|
}
|
|
3134
|
-
// Env-first credentials.
|
|
3135
3239
|
let credentials;
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
const envToken = trim(process.env.AWS_SESSION_TOKEN);
|
|
3139
|
-
if (envId && envSecret) {
|
|
3140
|
-
credentials = {
|
|
3141
|
-
accessKeyId: envId,
|
|
3142
|
-
secretAccessKey: envSecret,
|
|
3143
|
-
...(envToken ? { sessionToken: envToken } : {}),
|
|
3144
|
-
};
|
|
3145
|
-
}
|
|
3146
|
-
else if (profile) {
|
|
3240
|
+
// Profile wins over ambient env creds when present (from flags/config/dotenv).
|
|
3241
|
+
if (profile) {
|
|
3147
3242
|
// Try export-credentials
|
|
3148
3243
|
credentials = await exportCredentials(profile);
|
|
3149
3244
|
// On failure, detect SSO and optionally login then retry
|
|
3150
3245
|
if (!credentials) {
|
|
3151
3246
|
const ssoSession = await getAwsConfigure('sso_session', profile);
|
|
3152
|
-
|
|
3247
|
+
// Legacy SSO profiles use sso_start_url/sso_region rather than sso_session.
|
|
3248
|
+
const ssoStartUrl = await getAwsConfigure('sso_start_url', profile);
|
|
3249
|
+
const looksSSO = (typeof ssoSession === 'string' && ssoSession.length > 0) ||
|
|
3250
|
+
(typeof ssoStartUrl === 'string' && ssoStartUrl.length > 0);
|
|
3153
3251
|
if (looksSSO && cfg.loginOnDemand) {
|
|
3154
|
-
//
|
|
3155
|
-
await
|
|
3252
|
+
// Interactive login (no timeout by default), then retry export once.
|
|
3253
|
+
const exit = await runCommand(['aws', 'sso', 'login', '--profile', profile], false, {
|
|
3156
3254
|
env: process.env,
|
|
3157
|
-
|
|
3255
|
+
stdio: 'inherit',
|
|
3158
3256
|
});
|
|
3257
|
+
if (exit !== 0) {
|
|
3258
|
+
throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
|
|
3259
|
+
}
|
|
3159
3260
|
credentials = await exportCredentials(profile);
|
|
3160
3261
|
}
|
|
3161
3262
|
}
|
|
@@ -3173,6 +3274,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
|
3173
3274
|
}
|
|
3174
3275
|
}
|
|
3175
3276
|
}
|
|
3277
|
+
else {
|
|
3278
|
+
// Env-first credentials when no profile is present.
|
|
3279
|
+
const envId = trim(process.env.AWS_ACCESS_KEY_ID);
|
|
3280
|
+
const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
|
|
3281
|
+
const envToken = trim(process.env.AWS_SESSION_TOKEN);
|
|
3282
|
+
if (envId && envSecret) {
|
|
3283
|
+
credentials = {
|
|
3284
|
+
accessKeyId: envId,
|
|
3285
|
+
secretAccessKey: envSecret,
|
|
3286
|
+
...(envToken ? { sessionToken: envToken } : {}),
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3176
3290
|
// Final region resolution
|
|
3177
3291
|
if (!region && profile)
|
|
3178
3292
|
region = await getAwsConfigure('region', profile);
|
|
@@ -3188,10 +3302,213 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
|
3188
3302
|
return out;
|
|
3189
3303
|
};
|
|
3190
3304
|
|
|
3305
|
+
/**
|
|
3306
|
+
* Create the AWS plugin `afterResolve` hook.
|
|
3307
|
+
*
|
|
3308
|
+
* This runs once per invocation after the host resolves dotenv context.
|
|
3309
|
+
*
|
|
3310
|
+
* @param plugin - The AWS plugin instance.
|
|
3311
|
+
* @returns An `afterResolve` hook function suitable for assigning to `plugin.afterResolve`.
|
|
3312
|
+
*
|
|
3313
|
+
* @internal
|
|
3314
|
+
*/
|
|
3315
|
+
function attachAwsAfterResolveHook(plugin) {
|
|
3316
|
+
return async (cli, ctx) => {
|
|
3317
|
+
const cfg = plugin.readConfig(cli);
|
|
3318
|
+
const out = await resolveAwsContext({
|
|
3319
|
+
dotenv: ctx.dotenv,
|
|
3320
|
+
cfg,
|
|
3321
|
+
});
|
|
3322
|
+
applyAwsContext(out, ctx, true);
|
|
3323
|
+
// Optional: low-noise breadcrumb for diagnostics
|
|
3324
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3325
|
+
try {
|
|
3326
|
+
const msg = JSON.stringify({
|
|
3327
|
+
profile: out.profile,
|
|
3328
|
+
region: out.region,
|
|
3329
|
+
hasCreds: Boolean(out.credentials),
|
|
3330
|
+
});
|
|
3331
|
+
process.stderr.write(`[aws] afterResolve ${msg}\n`);
|
|
3332
|
+
}
|
|
3333
|
+
catch {
|
|
3334
|
+
/* ignore */
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
/** @internal */
|
|
3341
|
+
const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
3342
|
+
/**
|
|
3343
|
+
* Create an AWS plugin config overlay from Commander-parsed option values.
|
|
3344
|
+
*
|
|
3345
|
+
* This preserves tri-state intent:
|
|
3346
|
+
* - If a flag was not provided, it should not overwrite config-derived defaults.
|
|
3347
|
+
* - If `--no-…` was provided, it must explicitly force the boolean false.
|
|
3348
|
+
*
|
|
3349
|
+
* @param opts - Commander option values for the current invocation.
|
|
3350
|
+
* @returns A partial AWS plugin config object containing only explicit overrides.
|
|
3351
|
+
*
|
|
3352
|
+
* @internal
|
|
3353
|
+
*/
|
|
3354
|
+
function awsConfigOverridesFromCommandOpts(opts) {
|
|
3355
|
+
const o = isRecord(opts) ? opts : {};
|
|
3356
|
+
const overlay = {};
|
|
3357
|
+
// Map boolean toggles (respect explicit --no-*)
|
|
3358
|
+
if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand')) {
|
|
3359
|
+
overlay.loginOnDemand = Boolean(o.loginOnDemand);
|
|
3360
|
+
}
|
|
3361
|
+
// Strings/enums
|
|
3362
|
+
if (typeof o.profile === 'string')
|
|
3363
|
+
overlay.profile = o.profile;
|
|
3364
|
+
if (typeof o.region === 'string')
|
|
3365
|
+
overlay.region = o.region;
|
|
3366
|
+
if (typeof o.defaultRegion === 'string')
|
|
3367
|
+
overlay.defaultRegion = o.defaultRegion;
|
|
3368
|
+
if (o.strategy === 'cli-export' || o.strategy === 'none') {
|
|
3369
|
+
overlay.strategy = o.strategy;
|
|
3370
|
+
}
|
|
3371
|
+
// Advanced key overrides
|
|
3372
|
+
if (typeof o.profileKey === 'string')
|
|
3373
|
+
overlay.profileKey = o.profileKey;
|
|
3374
|
+
if (typeof o.profileFallbackKey === 'string') {
|
|
3375
|
+
overlay.profileFallbackKey = o.profileFallbackKey;
|
|
3376
|
+
}
|
|
3377
|
+
if (typeof o.regionKey === 'string')
|
|
3378
|
+
overlay.regionKey = o.regionKey;
|
|
3379
|
+
return overlay;
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
/**
|
|
3383
|
+
* Attach the default action for the AWS plugin mount.
|
|
3384
|
+
*
|
|
3385
|
+
* Behavior:
|
|
3386
|
+
* - With args: forwards to AWS CLI (`aws <args...>`) under the established session.
|
|
3387
|
+
* - Without args: session-only establishment (no forward).
|
|
3388
|
+
*
|
|
3389
|
+
* @param cli - The `aws` command mount.
|
|
3390
|
+
* @param plugin - The AWS plugin instance.
|
|
3391
|
+
*
|
|
3392
|
+
* @internal
|
|
3393
|
+
*/
|
|
3394
|
+
function attachAwsDefaultAction(cli, plugin, awsCmd) {
|
|
3395
|
+
awsCmd.action(async (args, opts, thisCommand) => {
|
|
3396
|
+
// Access merged root CLI options (installed by root hooks).
|
|
3397
|
+
const bag = readMergedOptions(thisCommand);
|
|
3398
|
+
const capture = shouldCapture(bag.capture);
|
|
3399
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
3400
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
3401
|
+
// Build overlay cfg from subcommand flags layered over discovered config.
|
|
3402
|
+
const ctx = cli.getCtx();
|
|
3403
|
+
const cfgBase = plugin.readConfig(cli);
|
|
3404
|
+
const cfg = {
|
|
3405
|
+
...cfgBase,
|
|
3406
|
+
...awsConfigOverridesFromCommandOpts(opts),
|
|
3407
|
+
};
|
|
3408
|
+
// Resolve current context with overrides
|
|
3409
|
+
const out = await resolveAwsContext({
|
|
3410
|
+
dotenv: ctx.dotenv,
|
|
3411
|
+
cfg,
|
|
3412
|
+
});
|
|
3413
|
+
// Publish env/context
|
|
3414
|
+
applyAwsContext(out, ctx, true);
|
|
3415
|
+
// Forward when positional args are present; otherwise session-only.
|
|
3416
|
+
if (args.length > 0) {
|
|
3417
|
+
const argv = ['aws', ...args];
|
|
3418
|
+
const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
|
|
3419
|
+
const exit = await runCommand(argv, shellSetting, {
|
|
3420
|
+
env: buildSpawnEnv(process.env, ctx.dotenv),
|
|
3421
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
3422
|
+
});
|
|
3423
|
+
// Deterministic termination (suppressed under tests)
|
|
3424
|
+
if (!underTests) {
|
|
3425
|
+
process.exit(typeof exit === 'number' ? exit : 0);
|
|
3426
|
+
}
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
// Session only: low-noise breadcrumb under debug
|
|
3430
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3431
|
+
try {
|
|
3432
|
+
const msg = JSON.stringify({
|
|
3433
|
+
profile: out.profile,
|
|
3434
|
+
region: out.region,
|
|
3435
|
+
hasCreds: Boolean(out.credentials),
|
|
3436
|
+
});
|
|
3437
|
+
process.stderr.write(`[aws] session established ${msg}\n`);
|
|
3438
|
+
}
|
|
3439
|
+
catch {
|
|
3440
|
+
/* ignore */
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
if (!underTests)
|
|
3444
|
+
process.exit(0);
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
/**
|
|
3449
|
+
* Attach options/arguments for the AWS plugin mount.
|
|
3450
|
+
*
|
|
3451
|
+
* @param cli - The `aws` command mount.
|
|
3452
|
+
* @param plugin - The AWS plugin instance (for dynamic option descriptions).
|
|
3453
|
+
*
|
|
3454
|
+
* @internal
|
|
3455
|
+
*/
|
|
3456
|
+
function attachAwsOptions(cli, plugin) {
|
|
3457
|
+
return (cli
|
|
3458
|
+
// Description is owned by the plugin index (src/plugins/aws/index.ts).
|
|
3459
|
+
.enablePositionalOptions()
|
|
3460
|
+
.passThroughOptions()
|
|
3461
|
+
.allowUnknownOption(true)
|
|
3462
|
+
// Boolean toggles with dynamic help labels (effective defaults)
|
|
3463
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
|
|
3464
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
|
|
3465
|
+
// Strings / enums
|
|
3466
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
|
|
3467
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
|
|
3468
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
|
|
3469
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
|
|
3470
|
+
// Advanced key overrides
|
|
3471
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
|
|
3472
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
|
|
3473
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
|
|
3474
|
+
// Accept any extra operands so Commander does not error when tokens appear after "--".
|
|
3475
|
+
.argument('[args...]'));
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
/**
|
|
3479
|
+
* Attach the AWS plugin `preSubcommand` hook.
|
|
3480
|
+
*
|
|
3481
|
+
* Ensures `aws --profile/--region <child>` applies the AWS session setup before
|
|
3482
|
+
* child subcommand execution.
|
|
3483
|
+
*
|
|
3484
|
+
* @param cli - The `aws` command mount.
|
|
3485
|
+
* @param plugin - The AWS plugin instance.
|
|
3486
|
+
*
|
|
3487
|
+
* @internal
|
|
3488
|
+
*/
|
|
3489
|
+
function attachAwsPreSubcommandHook(cli, plugin) {
|
|
3490
|
+
cli.hook('preSubcommand', async (thisCommand) => {
|
|
3491
|
+
// Avoid side effects for help rendering.
|
|
3492
|
+
if (process.argv.includes('-h') || process.argv.includes('--help'))
|
|
3493
|
+
return;
|
|
3494
|
+
const ctx = cli.getCtx();
|
|
3495
|
+
const cfgBase = plugin.readConfig(cli);
|
|
3496
|
+
const cfg = {
|
|
3497
|
+
...cfgBase,
|
|
3498
|
+
...awsConfigOverridesFromCommandOpts(thisCommand.opts()),
|
|
3499
|
+
};
|
|
3500
|
+
const out = await resolveAwsContext({
|
|
3501
|
+
dotenv: ctx.dotenv,
|
|
3502
|
+
cfg,
|
|
3503
|
+
});
|
|
3504
|
+
applyAwsContext(out, ctx, true);
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3191
3508
|
/**
|
|
3192
3509
|
* Zod schema for AWS plugin configuration.
|
|
3193
3510
|
*/
|
|
3194
|
-
const
|
|
3511
|
+
const awsPluginConfigSchema = z$2.object({
|
|
3195
3512
|
profile: z$2.string().optional(),
|
|
3196
3513
|
region: z$2.string().optional(),
|
|
3197
3514
|
defaultRegion: z$2.string().optional(),
|
|
@@ -3215,129 +3532,16 @@ const AwsPluginConfigSchema = z$2.object({
|
|
|
3215
3532
|
const awsPlugin = () => {
|
|
3216
3533
|
const plugin = definePlugin({
|
|
3217
3534
|
ns: 'aws',
|
|
3218
|
-
configSchema:
|
|
3219
|
-
setup
|
|
3220
|
-
|
|
3221
|
-
cli
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
.passThroughOptions()
|
|
3225
|
-
.allowUnknownOption(true)
|
|
3226
|
-
// Boolean toggles with dynamic help labels (effective defaults)
|
|
3227
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
|
|
3228
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
|
|
3229
|
-
// Strings / enums
|
|
3230
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
|
|
3231
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
|
|
3232
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
|
|
3233
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
|
|
3234
|
-
// Advanced key overrides
|
|
3235
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
|
|
3236
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
|
|
3237
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
|
|
3238
|
-
// Accept any extra operands so Commander does not error when tokens appear after "--".
|
|
3239
|
-
.argument('[args...]')
|
|
3240
|
-
.action(async (args, opts, thisCommand) => {
|
|
3241
|
-
const pluginInst = plugin;
|
|
3242
|
-
// Access merged root CLI options (installed by passOptions())
|
|
3243
|
-
const bag = readMergedOptions(thisCommand);
|
|
3244
|
-
const capture = shouldCapture(bag.capture);
|
|
3245
|
-
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
3246
|
-
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
3247
|
-
// Build overlay cfg from subcommand flags layered over discovered config.
|
|
3248
|
-
const ctx = cli.getCtx();
|
|
3249
|
-
const cfgBase = pluginInst.readConfig(cli);
|
|
3250
|
-
const o = opts;
|
|
3251
|
-
const overlay = {};
|
|
3252
|
-
// Map boolean toggles (respect explicit --no-*)
|
|
3253
|
-
if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
|
|
3254
|
-
overlay.loginOnDemand = Boolean(o.loginOnDemand);
|
|
3255
|
-
// Strings/enums
|
|
3256
|
-
if (typeof o.profile === 'string')
|
|
3257
|
-
overlay.profile = o.profile;
|
|
3258
|
-
if (typeof o.region === 'string')
|
|
3259
|
-
overlay.region = o.region;
|
|
3260
|
-
if (typeof o.defaultRegion === 'string')
|
|
3261
|
-
overlay.defaultRegion = o.defaultRegion;
|
|
3262
|
-
if (typeof o.strategy === 'string')
|
|
3263
|
-
overlay.strategy = o.strategy;
|
|
3264
|
-
// Advanced key overrides
|
|
3265
|
-
if (typeof o.profileKey === 'string')
|
|
3266
|
-
overlay.profileKey = o.profileKey;
|
|
3267
|
-
if (typeof o.profileFallbackKey === 'string')
|
|
3268
|
-
overlay.profileFallbackKey = o.profileFallbackKey;
|
|
3269
|
-
if (typeof o.regionKey === 'string')
|
|
3270
|
-
overlay.regionKey = o.regionKey;
|
|
3271
|
-
const cfg = {
|
|
3272
|
-
...cfgBase,
|
|
3273
|
-
...overlay,
|
|
3274
|
-
};
|
|
3275
|
-
// Resolve current context with overrides
|
|
3276
|
-
const out = await resolveAwsContext({
|
|
3277
|
-
dotenv: ctx.dotenv,
|
|
3278
|
-
cfg,
|
|
3279
|
-
});
|
|
3280
|
-
// Publish env/context
|
|
3281
|
-
applyAwsContext(out, ctx, true);
|
|
3282
|
-
// Forward when positional args are present; otherwise session-only.
|
|
3283
|
-
if (Array.isArray(args) && args.length > 0) {
|
|
3284
|
-
const argv = ['aws', ...args];
|
|
3285
|
-
const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
|
|
3286
|
-
const exit = await runCommand(argv, shellSetting, {
|
|
3287
|
-
env: buildSpawnEnv(process.env, ctx.dotenv),
|
|
3288
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
3289
|
-
});
|
|
3290
|
-
// Deterministic termination (suppressed under tests)
|
|
3291
|
-
if (!underTests) {
|
|
3292
|
-
process.exit(typeof exit === 'number' ? exit : 0);
|
|
3293
|
-
}
|
|
3294
|
-
return;
|
|
3295
|
-
}
|
|
3296
|
-
else {
|
|
3297
|
-
// Session only: low-noise breadcrumb under debug
|
|
3298
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
3299
|
-
try {
|
|
3300
|
-
const msg = JSON.stringify({
|
|
3301
|
-
profile: out.profile,
|
|
3302
|
-
region: out.region,
|
|
3303
|
-
hasCreds: Boolean(out.credentials),
|
|
3304
|
-
});
|
|
3305
|
-
process.stderr.write(`[aws] session established ${msg}\n`);
|
|
3306
|
-
}
|
|
3307
|
-
catch {
|
|
3308
|
-
/* ignore */
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
if (!underTests)
|
|
3312
|
-
process.exit(0);
|
|
3313
|
-
return;
|
|
3314
|
-
}
|
|
3315
|
-
});
|
|
3535
|
+
configSchema: awsPluginConfigSchema,
|
|
3536
|
+
setup(cli) {
|
|
3537
|
+
cli.description('Establish an AWS session and optionally forward to the AWS CLI');
|
|
3538
|
+
const awsCmd = attachAwsOptions(cli, plugin);
|
|
3539
|
+
attachAwsPreSubcommandHook(cli, plugin);
|
|
3540
|
+
attachAwsDefaultAction(cli, plugin, awsCmd);
|
|
3316
3541
|
return undefined;
|
|
3317
3542
|
},
|
|
3318
|
-
afterResolve: async (_cli, ctx) => {
|
|
3319
|
-
const cfg = plugin.readConfig(_cli);
|
|
3320
|
-
const out = await resolveAwsContext({
|
|
3321
|
-
dotenv: ctx.dotenv,
|
|
3322
|
-
cfg,
|
|
3323
|
-
});
|
|
3324
|
-
applyAwsContext(out, ctx, true);
|
|
3325
|
-
// Optional: low-noise breadcrumb for diagnostics
|
|
3326
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
3327
|
-
try {
|
|
3328
|
-
const msg = JSON.stringify({
|
|
3329
|
-
profile: out.profile,
|
|
3330
|
-
region: out.region,
|
|
3331
|
-
hasCreds: Boolean(out.credentials),
|
|
3332
|
-
});
|
|
3333
|
-
process.stderr.write(`[aws] afterResolve ${msg}\n`);
|
|
3334
|
-
}
|
|
3335
|
-
catch {
|
|
3336
|
-
/* ignore */
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
},
|
|
3340
3543
|
});
|
|
3544
|
+
plugin.afterResolve = attachAwsAfterResolveHook(plugin);
|
|
3341
3545
|
return plugin;
|
|
3342
3546
|
};
|
|
3343
3547
|
|
|
@@ -13799,21 +14003,78 @@ class GetCallerIdentityCommand extends Command
|
|
|
13799
14003
|
}
|
|
13800
14004
|
|
|
13801
14005
|
/**
|
|
13802
|
-
*
|
|
13803
|
-
*
|
|
14006
|
+
* Attach the default action for the `aws whoami` command.
|
|
14007
|
+
*
|
|
14008
|
+
* This behavior executes only when `aws whoami` is invoked without a subcommand.
|
|
14009
|
+
*
|
|
14010
|
+
* @param cli - The `whoami` command mount.
|
|
14011
|
+
* @returns Nothing.
|
|
14012
|
+
*/
|
|
14013
|
+
function attachWhoamiDefaultAction(cli) {
|
|
14014
|
+
cli.action(async () => {
|
|
14015
|
+
// The AWS SDK default providers will read credentials from process.env,
|
|
14016
|
+
// which the aws parent has already populated.
|
|
14017
|
+
const client = new STSClient$1();
|
|
14018
|
+
const result = await client.send(new GetCallerIdentityCommand());
|
|
14019
|
+
console.log(JSON.stringify(result, null, 2));
|
|
14020
|
+
});
|
|
14021
|
+
}
|
|
14022
|
+
|
|
14023
|
+
/**
|
|
14024
|
+
* Attach options/arguments for the `aws whoami` plugin mount.
|
|
14025
|
+
*
|
|
14026
|
+
* This subcommand currently takes no flags/args; this module exists to keep the
|
|
14027
|
+
* wiring layout consistent across shipped plugins (options vs actions).
|
|
14028
|
+
*
|
|
14029
|
+
* Note: the plugin description is owned by `src/plugins/aws/whoami/index.ts` and
|
|
14030
|
+
* must not be set here.
|
|
14031
|
+
*
|
|
14032
|
+
* @param cli - The `whoami` command mount under `aws`.
|
|
14033
|
+
* @returns The same `cli` instance for chaining.
|
|
14034
|
+
*
|
|
14035
|
+
* @internal
|
|
14036
|
+
*/
|
|
14037
|
+
function attachWhoamiOptions(cli) {
|
|
14038
|
+
return cli;
|
|
14039
|
+
}
|
|
14040
|
+
|
|
14041
|
+
/**
|
|
14042
|
+
* Attach the `really` subcommand under `aws whoami`.
|
|
14043
|
+
*
|
|
14044
|
+
* Reads `SECRET_IDENTITY` from the resolved get-dotenv context (`cli.getCtx().dotenv`).
|
|
14045
|
+
*
|
|
14046
|
+
* @param cli - The `whoami` command mount.
|
|
14047
|
+
* @returns Nothing.
|
|
14048
|
+
*/
|
|
14049
|
+
function attachWhoamiReallyAction(cli) {
|
|
14050
|
+
const really = cli
|
|
14051
|
+
.ns('really')
|
|
14052
|
+
.description('Print SECRET_IDENTITY from the resolved dotenv context');
|
|
14053
|
+
really.action(() => {
|
|
14054
|
+
const secretIdentity = really.getCtx().dotenv.SECRET_IDENTITY;
|
|
14055
|
+
console.log(`Your secret identity is ${secretIdentity ?? 'still a secret'}.`);
|
|
14056
|
+
});
|
|
14057
|
+
}
|
|
14058
|
+
|
|
14059
|
+
/**
|
|
14060
|
+
* AWS Whoami plugin factory.
|
|
14061
|
+
*
|
|
14062
|
+
* This plugin demonstrates a “bucket of subcommands” pattern:
|
|
14063
|
+
* - Subcommand behavior is articulated in separate modules as `attach*` helpers.
|
|
14064
|
+
* - Those helpers are not individually composable plugins; they are internal wiring for one plugin instance.
|
|
14065
|
+
*
|
|
14066
|
+
* @returns A plugin instance mounted at `aws whoami`.
|
|
13804
14067
|
*/
|
|
13805
14068
|
const awsWhoamiPlugin = () => definePlugin({
|
|
13806
14069
|
ns: 'whoami',
|
|
13807
14070
|
setup(cli) {
|
|
13808
|
-
cli
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
console.log(JSON.stringify(result, null, 2));
|
|
13816
|
-
});
|
|
14071
|
+
cli.description('Print AWS caller identity (uses parent aws session)');
|
|
14072
|
+
// Options/args (none today, but keep layout consistent with other plugins).
|
|
14073
|
+
const whoami = attachWhoamiOptions(cli);
|
|
14074
|
+
// Default behavior: `getdotenv aws whoami`
|
|
14075
|
+
attachWhoamiDefaultAction(whoami);
|
|
14076
|
+
// Subcommand behavior: `getdotenv aws whoami really`
|
|
14077
|
+
attachWhoamiReallyAction(whoami);
|
|
13817
14078
|
return undefined;
|
|
13818
14079
|
},
|
|
13819
14080
|
});
|
|
@@ -13921,7 +14182,7 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv,
|
|
|
13921
14182
|
/**
|
|
13922
14183
|
* Attach the default "cmd" subcommand action with contextual typing.
|
|
13923
14184
|
*/
|
|
13924
|
-
const
|
|
14185
|
+
const attachBatchCmdAction = (plugin, cli, batchCmd, pluginOpts, cmd) => {
|
|
13925
14186
|
cmd.action(async (commandParts, _subOpts, thisCommand) => {
|
|
13926
14187
|
const mergedBag = readMergedOptions(batchCmd);
|
|
13927
14188
|
const logger = mergedBag.logger;
|
|
@@ -14030,11 +14291,37 @@ const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
|
|
|
14030
14291
|
});
|
|
14031
14292
|
};
|
|
14032
14293
|
|
|
14294
|
+
/**
|
|
14295
|
+
* Attach the default `cmd` subcommand under the `batch` command.
|
|
14296
|
+
*
|
|
14297
|
+
* This encapsulates:
|
|
14298
|
+
* - Subcommand construction (`new Command().name('cmd')…`)
|
|
14299
|
+
* - Action wiring
|
|
14300
|
+
* - Mounting as the default subcommand for `batch`
|
|
14301
|
+
*
|
|
14302
|
+
* @param plugin - The batch plugin instance.
|
|
14303
|
+
* @param cli - The batch command mount.
|
|
14304
|
+
* @param batchCmd - The `batch` command (same as `cli` mount).
|
|
14305
|
+
* @param pluginOpts - Batch plugin factory options.
|
|
14306
|
+
*
|
|
14307
|
+
* @internal
|
|
14308
|
+
*/
|
|
14309
|
+
const attachBatchCmdSubcommand = (plugin, cli, batchCmd, pluginOpts) => {
|
|
14310
|
+
const cmdSub = new Command$1()
|
|
14311
|
+
.name('cmd')
|
|
14312
|
+
.description('execute command, conflicts with --command option (default subcommand)')
|
|
14313
|
+
.enablePositionalOptions()
|
|
14314
|
+
.passThroughOptions()
|
|
14315
|
+
.argument('[command...]');
|
|
14316
|
+
attachBatchCmdAction(plugin, cli, batchCmd, pluginOpts, cmdSub);
|
|
14317
|
+
batchCmd.addCommand(cmdSub, { isDefault: true });
|
|
14318
|
+
};
|
|
14319
|
+
|
|
14033
14320
|
/**
|
|
14034
14321
|
* Attach the parent-level action for the batch plugin.
|
|
14035
14322
|
* Handles parent flags (e.g. `getdotenv batch -l`) and delegates to the batch executor.
|
|
14036
14323
|
*/
|
|
14037
|
-
const
|
|
14324
|
+
const attachBatchDefaultAction = (plugin, cli, pluginOpts, parent) => {
|
|
14038
14325
|
parent.action(async function (...args) {
|
|
14039
14326
|
// Commander Unknown generics: [...unknown[], OptionValues, thisCommand]
|
|
14040
14327
|
const thisCommand = args[args.length - 1];
|
|
@@ -14135,6 +14422,45 @@ const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
|
|
|
14135
14422
|
});
|
|
14136
14423
|
};
|
|
14137
14424
|
|
|
14425
|
+
/**
|
|
14426
|
+
* Attach options/arguments for the batch plugin mount.
|
|
14427
|
+
*
|
|
14428
|
+
* Note: the plugin description is owned by `src/plugins/batch/index.ts` and
|
|
14429
|
+
* must not be set here.
|
|
14430
|
+
*
|
|
14431
|
+
* @param plugin - Batch plugin instance (for dynamic option descriptions).
|
|
14432
|
+
* @param cli - The `batch` command mount.
|
|
14433
|
+
* @returns The same `cli` instance for chaining.
|
|
14434
|
+
*
|
|
14435
|
+
* @internal
|
|
14436
|
+
*/
|
|
14437
|
+
function attachBatchOptions(plugin, cli) {
|
|
14438
|
+
const GROUP = `plugin:${cli.name()}`;
|
|
14439
|
+
return (cli
|
|
14440
|
+
.enablePositionalOptions()
|
|
14441
|
+
.passThroughOptions()
|
|
14442
|
+
// Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
|
|
14443
|
+
.addOption((() => {
|
|
14444
|
+
const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
|
|
14445
|
+
cli.setOptionGroup(opt, GROUP);
|
|
14446
|
+
return opt;
|
|
14447
|
+
})())
|
|
14448
|
+
.addOption((() => {
|
|
14449
|
+
const opt = plugin.createPluginDynamicOption(cli, '-r, --root-path <string>', (_bag, cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.rootPath || './')})`);
|
|
14450
|
+
cli.setOptionGroup(opt, GROUP);
|
|
14451
|
+
return opt;
|
|
14452
|
+
})())
|
|
14453
|
+
.addOption((() => {
|
|
14454
|
+
const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
|
|
14455
|
+
cli.setOptionGroup(opt, GROUP);
|
|
14456
|
+
return opt;
|
|
14457
|
+
})())
|
|
14458
|
+
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
14459
|
+
.option('-l, --list', 'list working directories without executing command')
|
|
14460
|
+
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
14461
|
+
.argument('[command...]'));
|
|
14462
|
+
}
|
|
14463
|
+
|
|
14138
14464
|
/**
|
|
14139
14465
|
* Zod schema for a single script entry (string or object).
|
|
14140
14466
|
*/
|
|
@@ -14148,7 +14474,7 @@ const ScriptSchema = z$2.union([
|
|
|
14148
14474
|
/**
|
|
14149
14475
|
* Zod schema for batch plugin configuration.
|
|
14150
14476
|
*/
|
|
14151
|
-
const
|
|
14477
|
+
const batchPluginConfigSchema = z$2.object({
|
|
14152
14478
|
scripts: z$2.record(z$2.string(), ScriptSchema).optional(),
|
|
14153
14479
|
shell: z$2.union([z$2.string(), z$2.boolean()]).optional(),
|
|
14154
14480
|
rootPath: z$2.string().optional(),
|
|
@@ -14174,45 +14500,15 @@ const batchPlugin = (opts = {}) => {
|
|
|
14174
14500
|
ns: 'batch',
|
|
14175
14501
|
// Host validates this when config-loader is enabled; plugins may also
|
|
14176
14502
|
// re-validate at action time as a safety belt.
|
|
14177
|
-
configSchema:
|
|
14503
|
+
configSchema: batchPluginConfigSchema,
|
|
14178
14504
|
setup(cli) {
|
|
14179
14505
|
const batchCmd = cli; // mount provided by host
|
|
14180
|
-
|
|
14181
|
-
batchCmd
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
.addOption((() => {
|
|
14187
|
-
const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
|
|
14188
|
-
cli.setOptionGroup(opt, GROUP);
|
|
14189
|
-
return opt;
|
|
14190
|
-
})())
|
|
14191
|
-
.addOption((() => {
|
|
14192
|
-
const opt = plugin.createPluginDynamicOption(cli, '-r, --root-path <string>', (_bag, cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.rootPath || './')})`);
|
|
14193
|
-
cli.setOptionGroup(opt, GROUP);
|
|
14194
|
-
return opt;
|
|
14195
|
-
})())
|
|
14196
|
-
.addOption((() => {
|
|
14197
|
-
const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
|
|
14198
|
-
cli.setOptionGroup(opt, GROUP);
|
|
14199
|
-
return opt;
|
|
14200
|
-
})())
|
|
14201
|
-
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
14202
|
-
.option('-l, --list', 'list working directories without executing command')
|
|
14203
|
-
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
14204
|
-
.argument('[command...]');
|
|
14205
|
-
// Default subcommand "cmd" with contextual typing for args/opts
|
|
14206
|
-
const cmdSub = new Command$1()
|
|
14207
|
-
.name('cmd')
|
|
14208
|
-
.description('execute command, conflicts with --command option (default subcommand)')
|
|
14209
|
-
.enablePositionalOptions()
|
|
14210
|
-
.passThroughOptions()
|
|
14211
|
-
.argument('[command...]');
|
|
14212
|
-
attachDefaultCmdAction$1(plugin, cli, batchCmd, opts, cmdSub);
|
|
14213
|
-
batchCmd.addCommand(cmdSub, { isDefault: true });
|
|
14214
|
-
// Parent invoker (unified naming)
|
|
14215
|
-
attachParentInvoker$1(plugin, cli, opts, batchCmd);
|
|
14506
|
+
batchCmd.description('Batch command execution across multiple working directories.');
|
|
14507
|
+
attachBatchOptions(plugin, batchCmd);
|
|
14508
|
+
// Default subcommand `cmd` (mounted as batch default subcommand)
|
|
14509
|
+
attachBatchCmdSubcommand(plugin, cli, batchCmd, opts);
|
|
14510
|
+
// Default action for the batch command mount (parent flags and positional form)
|
|
14511
|
+
attachBatchDefaultAction(plugin, cli, opts, batchCmd);
|
|
14216
14512
|
return undefined;
|
|
14217
14513
|
},
|
|
14218
14514
|
});
|
|
@@ -14306,14 +14602,11 @@ async function runCmdWithContext(cli, merged, command, _opts) {
|
|
|
14306
14602
|
}
|
|
14307
14603
|
|
|
14308
14604
|
/**
|
|
14309
|
-
* Attach the default "cmd" subcommand action
|
|
14605
|
+
* Attach the default "cmd" subcommand action.
|
|
14310
14606
|
* Mirrors the prior inline implementation in cmd/index.ts.
|
|
14311
14607
|
*/
|
|
14312
|
-
const
|
|
14313
|
-
cmd
|
|
14314
|
-
.enablePositionalOptions()
|
|
14315
|
-
.passThroughOptions()
|
|
14316
|
-
.action(async function (...allArgs) {
|
|
14608
|
+
const attachCmdDefaultAction = (cli, cmd, aliasKey) => {
|
|
14609
|
+
cmd.action(async function (...allArgs) {
|
|
14317
14610
|
// Commander passes: [...positionals, options, thisCommand]
|
|
14318
14611
|
const thisCommand = allArgs[allArgs.length - 1];
|
|
14319
14612
|
const commandParts = allArgs[0];
|
|
@@ -14346,11 +14639,29 @@ const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
|
|
|
14346
14639
|
});
|
|
14347
14640
|
};
|
|
14348
14641
|
|
|
14642
|
+
/**
|
|
14643
|
+
* Attach options/arguments for the cmd plugin mount.
|
|
14644
|
+
*
|
|
14645
|
+
* Note: the plugin description is owned by `src/plugins/cmd/index.ts` and must
|
|
14646
|
+
* not be set here.
|
|
14647
|
+
*
|
|
14648
|
+
* @param cli - The `cmd` command mount.
|
|
14649
|
+
* @returns The same `cli` instance for chaining.
|
|
14650
|
+
*
|
|
14651
|
+
* @internal
|
|
14652
|
+
*/
|
|
14653
|
+
function attachCmdOptions(cli) {
|
|
14654
|
+
return cli
|
|
14655
|
+
.enablePositionalOptions()
|
|
14656
|
+
.passThroughOptions()
|
|
14657
|
+
.argument('[command...]');
|
|
14658
|
+
}
|
|
14659
|
+
|
|
14349
14660
|
/**
|
|
14350
14661
|
* Install the parent-level invoker (alias) for the cmd plugin.
|
|
14351
14662
|
* Unifies naming with batch attachParentInvoker; behavior unchanged.
|
|
14352
14663
|
*/
|
|
14353
|
-
const
|
|
14664
|
+
const attachCmdParentInvoker = (cli, options, plugin) => {
|
|
14354
14665
|
const dbg = (...args) => {
|
|
14355
14666
|
if (process.env.GETDOTENV_DEBUG) {
|
|
14356
14667
|
try {
|
|
@@ -14461,7 +14772,7 @@ const attachParentInvoker = (cli, options, _cmd, plugin) => {
|
|
|
14461
14772
|
/**
|
|
14462
14773
|
* Zod schema for cmd plugin configuration.
|
|
14463
14774
|
*/
|
|
14464
|
-
const
|
|
14775
|
+
const cmdPluginConfigSchema = z$2
|
|
14465
14776
|
.object({
|
|
14466
14777
|
expand: z$2.boolean().optional(),
|
|
14467
14778
|
})
|
|
@@ -14481,7 +14792,7 @@ const CmdConfigSchema = z$2
|
|
|
14481
14792
|
const cmdPlugin = (options = {}) => {
|
|
14482
14793
|
const plugin = definePlugin({
|
|
14483
14794
|
ns: 'cmd',
|
|
14484
|
-
configSchema:
|
|
14795
|
+
configSchema: cmdPluginConfigSchema,
|
|
14485
14796
|
setup(cli) {
|
|
14486
14797
|
const aliasSpec = typeof options.optionAlias === 'string'
|
|
14487
14798
|
? { flags: options.optionAlias}
|
|
@@ -14493,14 +14804,13 @@ const cmdPlugin = (options = {}) => {
|
|
|
14493
14804
|
};
|
|
14494
14805
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
14495
14806
|
// Mount is the command ('cmd'); attach default action.
|
|
14496
|
-
cli
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
attachDefaultCmdAction(cli, cli, aliasKey);
|
|
14807
|
+
cli.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)');
|
|
14808
|
+
// Options/arguments (positional payload, argv routing) are attached separately.
|
|
14809
|
+
attachCmdOptions(cli);
|
|
14810
|
+
attachCmdDefaultAction(cli, cli, aliasKey);
|
|
14501
14811
|
// Parent-attached option alias (optional, unified naming).
|
|
14502
14812
|
if (aliasSpec !== undefined) {
|
|
14503
|
-
|
|
14813
|
+
attachCmdParentInvoker(cli, options, plugin);
|
|
14504
14814
|
}
|
|
14505
14815
|
return undefined;
|
|
14506
14816
|
},
|
|
@@ -14509,14 +14819,21 @@ const cmdPlugin = (options = {}) => {
|
|
|
14509
14819
|
};
|
|
14510
14820
|
|
|
14511
14821
|
/**
|
|
14512
|
-
* Ensure a directory exists.
|
|
14822
|
+
* Ensure a directory exists (parents included).
|
|
14823
|
+
*
|
|
14824
|
+
* @param p - Directory path to create.
|
|
14825
|
+
* @returns A `Promise\<string\>` resolving to the provided `p` value.
|
|
14513
14826
|
*/
|
|
14514
14827
|
const ensureDir = async (p) => {
|
|
14515
14828
|
await fs.ensureDir(p);
|
|
14516
14829
|
return p;
|
|
14517
14830
|
};
|
|
14518
14831
|
/**
|
|
14519
|
-
* Write text content to a file, ensuring the parent directory exists.
|
|
14832
|
+
* Write UTF-8 text content to a file, ensuring the parent directory exists.
|
|
14833
|
+
*
|
|
14834
|
+
* @param dest - Destination file path.
|
|
14835
|
+
* @param data - File contents to write.
|
|
14836
|
+
* @returns A `Promise\<void\>` which resolves when the file is written.
|
|
14520
14837
|
*/
|
|
14521
14838
|
const writeFile$1 = async (dest, data) => {
|
|
14522
14839
|
await ensureDir(path.dirname(dest));
|
|
@@ -14528,6 +14845,7 @@ const writeFile$1 = async (dest, data) => {
|
|
|
14528
14845
|
* @param src - Source file path.
|
|
14529
14846
|
* @param dest - Destination file path.
|
|
14530
14847
|
* @param substitutions - Map of token literals to replacement strings.
|
|
14848
|
+
* @returns A `Promise\<void\>` which resolves when the file has been copied.
|
|
14531
14849
|
*/
|
|
14532
14850
|
const copyTextFile = async (src, dest, substitutions) => {
|
|
14533
14851
|
const contents = await fs.readFile(src, 'utf-8');
|
|
@@ -14539,6 +14857,10 @@ const copyTextFile = async (src, dest, substitutions) => {
|
|
|
14539
14857
|
/**
|
|
14540
14858
|
* Ensure a set of lines exist (exact match) in a file. Creates the file
|
|
14541
14859
|
* when missing. Returns whether it was created or changed.
|
|
14860
|
+
*
|
|
14861
|
+
* @param filePath - Target file path to create/update.
|
|
14862
|
+
* @param lines - Lines which must be present (exact string match).
|
|
14863
|
+
* @returns A `Promise\<object\>` describing whether the file was created and/or changed.
|
|
14542
14864
|
*/
|
|
14543
14865
|
const ensureLines = async (filePath, lines) => {
|
|
14544
14866
|
const exists = await fs.pathExists(filePath);
|
|
@@ -14566,11 +14888,23 @@ const ensureLines = async (filePath, lines) => {
|
|
|
14566
14888
|
return { created: false, changed: false };
|
|
14567
14889
|
};
|
|
14568
14890
|
|
|
14569
|
-
|
|
14891
|
+
/**
|
|
14892
|
+
* Absolute path to the shipped templates directory.
|
|
14893
|
+
*
|
|
14894
|
+
* Used by the init scaffolder to locate files under `templates/` at runtime.
|
|
14895
|
+
*
|
|
14896
|
+
* @remarks
|
|
14897
|
+
* This path is resolved relative to the current working directory. It assumes
|
|
14898
|
+
* the `templates/` folder is present alongside the installed package (or in the
|
|
14899
|
+
* repository when running from source).
|
|
14900
|
+
*/
|
|
14570
14901
|
const TEMPLATES_ROOT = path.resolve('templates');
|
|
14571
14902
|
|
|
14572
14903
|
/**
|
|
14573
14904
|
* Plan the copy operations for configuration files.
|
|
14905
|
+
*
|
|
14906
|
+
* @param options - Planning options for config scaffolding.
|
|
14907
|
+
* @returns An array of copy operations to perform.
|
|
14574
14908
|
*/
|
|
14575
14909
|
const planConfigCopies = ({ format, withLocal, destRoot, }) => {
|
|
14576
14910
|
const copies = [];
|
|
@@ -14614,10 +14948,14 @@ const planConfigCopies = ({ format, withLocal, destRoot, }) => {
|
|
|
14614
14948
|
};
|
|
14615
14949
|
/**
|
|
14616
14950
|
* Plan the copy operations for the CLI skeleton.
|
|
14951
|
+
*
|
|
14952
|
+
* @param options - Planning options for CLI scaffolding.
|
|
14953
|
+
* @returns An array of copy operations to perform.
|
|
14617
14954
|
*/
|
|
14618
14955
|
const planCliCopies = ({ cliName, destRoot, }) => {
|
|
14619
14956
|
const subs = { __CLI_NAME__: cliName };
|
|
14620
14957
|
const base = path.join(destRoot, 'src', 'cli', cliName);
|
|
14958
|
+
const helloBase = path.join(base, 'plugins', 'hello');
|
|
14621
14959
|
return [
|
|
14622
14960
|
{
|
|
14623
14961
|
src: path.join(TEMPLATES_ROOT, 'cli', 'index.ts'),
|
|
@@ -14625,8 +14963,28 @@ const planCliCopies = ({ cliName, destRoot, }) => {
|
|
|
14625
14963
|
subs,
|
|
14626
14964
|
},
|
|
14627
14965
|
{
|
|
14628
|
-
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello.ts'),
|
|
14629
|
-
dest: path.join(
|
|
14966
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'index.ts'),
|
|
14967
|
+
dest: path.join(helloBase, 'index.ts'),
|
|
14968
|
+
subs,
|
|
14969
|
+
},
|
|
14970
|
+
{
|
|
14971
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'options.ts'),
|
|
14972
|
+
dest: path.join(helloBase, 'options.ts'),
|
|
14973
|
+
subs,
|
|
14974
|
+
},
|
|
14975
|
+
{
|
|
14976
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'defaultAction.ts'),
|
|
14977
|
+
dest: path.join(helloBase, 'defaultAction.ts'),
|
|
14978
|
+
subs,
|
|
14979
|
+
},
|
|
14980
|
+
{
|
|
14981
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'strangerAction.ts'),
|
|
14982
|
+
dest: path.join(helloBase, 'strangerAction.ts'),
|
|
14983
|
+
subs,
|
|
14984
|
+
},
|
|
14985
|
+
{
|
|
14986
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'types.ts'),
|
|
14987
|
+
dest: path.join(helloBase, 'types.ts'),
|
|
14630
14988
|
subs,
|
|
14631
14989
|
},
|
|
14632
14990
|
];
|
|
@@ -14635,6 +14993,8 @@ const planCliCopies = ({ cliName, destRoot, }) => {
|
|
|
14635
14993
|
/**
|
|
14636
14994
|
* Determine whether the current environment should be treated as non-interactive.
|
|
14637
14995
|
* CI heuristics include: CI, GITHUB_ACTIONS, BUILDKITE, TEAMCITY_VERSION, TF_BUILD.
|
|
14996
|
+
*
|
|
14997
|
+
* @returns `true` when running in a CI-like environment or when stdin/stdout are not TTYs.
|
|
14638
14998
|
*/
|
|
14639
14999
|
const isNonInteractive = () => {
|
|
14640
15000
|
const ciLike = process.env.CI ||
|
|
@@ -14647,6 +15007,11 @@ const isNonInteractive = () => {
|
|
|
14647
15007
|
/**
|
|
14648
15008
|
* Prompt the user for a file collision decision.
|
|
14649
15009
|
* Returns a single-character code representing overwrite/example/skip (or 'all' variants).
|
|
15010
|
+
*
|
|
15011
|
+
* @param filePath - Path of the colliding file (for display).
|
|
15012
|
+
* @param logger - Logger used for user-facing messages.
|
|
15013
|
+
* @param rl - Readline interface used to capture user input.
|
|
15014
|
+
* @returns A single-character decision code.
|
|
14650
15015
|
*/
|
|
14651
15016
|
const promptDecision = async (filePath, logger, rl) => {
|
|
14652
15017
|
logger.log(`File exists: ${filePath}\nChoose: [o]verwrite, [e]xample, [s]kip, [O]verwrite All, [E]xample All, [S]kip All`);
|
|
@@ -14661,129 +15026,148 @@ const promptDecision = async (filePath, logger, rl) => {
|
|
|
14661
15026
|
};
|
|
14662
15027
|
|
|
14663
15028
|
/**
|
|
14664
|
-
*
|
|
14665
|
-
*
|
|
14666
|
-
*
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
* Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
|
|
14670
|
-
* Supports collision detection, interactive prompts, and CI bypass.
|
|
15029
|
+
* Attach the init plugin default action.
|
|
15030
|
+
*
|
|
15031
|
+
* @param cli - The `init` command mount (with args/options attached).
|
|
15032
|
+
*
|
|
15033
|
+
* @internal
|
|
14671
15034
|
*/
|
|
14672
|
-
|
|
14673
|
-
|
|
14674
|
-
|
|
14675
|
-
|
|
14676
|
-
|
|
14677
|
-
|
|
14678
|
-
|
|
14679
|
-
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
await copyTextFile(item.src, item.dest, subs);
|
|
14728
|
-
logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
|
|
14729
|
-
continue;
|
|
14730
|
-
}
|
|
14731
|
-
if (yes) {
|
|
14732
|
-
logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
|
|
14733
|
-
continue;
|
|
14734
|
-
}
|
|
14735
|
-
let decision = globalDecision;
|
|
14736
|
-
if (!decision) {
|
|
14737
|
-
const a = await promptDecision(item.dest, logger, rl);
|
|
14738
|
-
if (a === 'O') {
|
|
14739
|
-
globalDecision = 'overwrite';
|
|
14740
|
-
decision = 'overwrite';
|
|
14741
|
-
}
|
|
14742
|
-
else if (a === 'E') {
|
|
14743
|
-
globalDecision = 'example';
|
|
14744
|
-
decision = 'example';
|
|
14745
|
-
}
|
|
14746
|
-
else if (a === 'S') {
|
|
14747
|
-
globalDecision = 'skip';
|
|
14748
|
-
decision = 'skip';
|
|
14749
|
-
}
|
|
14750
|
-
else {
|
|
14751
|
-
decision =
|
|
14752
|
-
a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
|
|
14753
|
-
}
|
|
15035
|
+
function attachInitDefaultAction(cli) {
|
|
15036
|
+
cli.action(async (destArg, opts, thisCommand) => {
|
|
15037
|
+
// Inherit logger from merged root options (base).
|
|
15038
|
+
const bag = readMergedOptions(thisCommand);
|
|
15039
|
+
const logger = bag.logger;
|
|
15040
|
+
const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
|
|
15041
|
+
const cwd = process.cwd();
|
|
15042
|
+
const destRoot = path.resolve(cwd, destRel);
|
|
15043
|
+
const formatInput = opts['configFormat'];
|
|
15044
|
+
const formatRaw = typeof formatInput === 'string' ? formatInput.toLowerCase() : 'json';
|
|
15045
|
+
const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw) ? formatRaw : 'json');
|
|
15046
|
+
const withLocal = Boolean(opts['withLocal']);
|
|
15047
|
+
// dynamic flag reserved for future template variants; present for UX compatibility
|
|
15048
|
+
void opts['dynamic'];
|
|
15049
|
+
// CLI name default: --cli-name | basename(dest) | 'mycli'
|
|
15050
|
+
const cliNameInput = opts['cliName'];
|
|
15051
|
+
const cliName = (typeof cliNameInput === 'string' && cliNameInput.length > 0
|
|
15052
|
+
? cliNameInput
|
|
15053
|
+
: path.basename(destRoot) || 'mycli') || 'mycli';
|
|
15054
|
+
// Precedence: --force > --yes > auto-detect(non-interactive => yes)
|
|
15055
|
+
const force = Boolean(opts['force']);
|
|
15056
|
+
const yes = Boolean(opts['yes']) || (!force && isNonInteractive());
|
|
15057
|
+
// Build copy plan
|
|
15058
|
+
const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
|
|
15059
|
+
const cliCopies = planCliCopies({ cliName, destRoot });
|
|
15060
|
+
const copies = [...cfgCopies, ...cliCopies];
|
|
15061
|
+
// Interactive state
|
|
15062
|
+
let globalDecision;
|
|
15063
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
15064
|
+
try {
|
|
15065
|
+
for (const item of copies) {
|
|
15066
|
+
const exists = await fs.pathExists(item.dest);
|
|
15067
|
+
if (!exists) {
|
|
15068
|
+
const subs = item.subs ?? {};
|
|
15069
|
+
await copyTextFile(item.src, item.dest, subs);
|
|
15070
|
+
logger.log(`Created ${path.relative(cwd, item.dest)}`);
|
|
15071
|
+
continue;
|
|
15072
|
+
}
|
|
15073
|
+
// Collision
|
|
15074
|
+
if (force) {
|
|
15075
|
+
const subs = item.subs ?? {};
|
|
15076
|
+
await copyTextFile(item.src, item.dest, subs);
|
|
15077
|
+
logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
|
|
15078
|
+
continue;
|
|
15079
|
+
}
|
|
15080
|
+
if (yes) {
|
|
15081
|
+
logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
|
|
15082
|
+
continue;
|
|
15083
|
+
}
|
|
15084
|
+
let decision = globalDecision;
|
|
15085
|
+
if (!decision) {
|
|
15086
|
+
const a = await promptDecision(item.dest, logger, rl);
|
|
15087
|
+
if (a === 'O') {
|
|
15088
|
+
globalDecision = 'overwrite';
|
|
15089
|
+
decision = 'overwrite';
|
|
14754
15090
|
}
|
|
14755
|
-
if (
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
|
|
15091
|
+
else if (a === 'E') {
|
|
15092
|
+
globalDecision = 'example';
|
|
15093
|
+
decision = 'example';
|
|
14759
15094
|
}
|
|
14760
|
-
else if (
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
await copyTextFile(item.src, destEx, subs);
|
|
14764
|
-
logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
|
|
15095
|
+
else if (a === 'S') {
|
|
15096
|
+
globalDecision = 'skip';
|
|
15097
|
+
decision = 'skip';
|
|
14765
15098
|
}
|
|
14766
15099
|
else {
|
|
14767
|
-
|
|
15100
|
+
decision = a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
|
|
14768
15101
|
}
|
|
14769
15102
|
}
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
'*.local',
|
|
14775
|
-
]);
|
|
14776
|
-
if (created) {
|
|
14777
|
-
logger.log(`Created ${path.relative(cwd, giPath)}`);
|
|
15103
|
+
if (decision === 'overwrite') {
|
|
15104
|
+
const subs = item.subs ?? {};
|
|
15105
|
+
await copyTextFile(item.src, item.dest, subs);
|
|
15106
|
+
logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
|
|
14778
15107
|
}
|
|
14779
|
-
else if (
|
|
14780
|
-
|
|
15108
|
+
else if (decision === 'example') {
|
|
15109
|
+
const destEx = `${item.dest}.example`;
|
|
15110
|
+
const subs = item.subs ?? {};
|
|
15111
|
+
await copyTextFile(item.src, destEx, subs);
|
|
15112
|
+
logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
|
|
15113
|
+
}
|
|
15114
|
+
else {
|
|
15115
|
+
logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
|
|
14781
15116
|
}
|
|
14782
15117
|
}
|
|
14783
|
-
|
|
14784
|
-
|
|
15118
|
+
// Ensure .gitignore includes local config patterns.
|
|
15119
|
+
const giPath = path.join(destRoot, '.gitignore');
|
|
15120
|
+
const { created, changed } = await ensureLines(giPath, [
|
|
15121
|
+
'getdotenv.config.local.*',
|
|
15122
|
+
'*.local',
|
|
15123
|
+
]);
|
|
15124
|
+
if (created) {
|
|
15125
|
+
logger.log(`Created ${path.relative(cwd, giPath)}`);
|
|
14785
15126
|
}
|
|
14786
|
-
|
|
15127
|
+
else if (changed) {
|
|
15128
|
+
logger.log(`Updated ${path.relative(cwd, giPath)}`);
|
|
15129
|
+
}
|
|
15130
|
+
}
|
|
15131
|
+
finally {
|
|
15132
|
+
rl.close();
|
|
15133
|
+
}
|
|
15134
|
+
});
|
|
15135
|
+
}
|
|
15136
|
+
|
|
15137
|
+
/**
|
|
15138
|
+
* Attach options/arguments for the init plugin mount.
|
|
15139
|
+
*
|
|
15140
|
+
* @param cli - The `init` command mount.
|
|
15141
|
+
*
|
|
15142
|
+
* @internal
|
|
15143
|
+
*/
|
|
15144
|
+
function attachInitOptions(cli) {
|
|
15145
|
+
return (cli
|
|
15146
|
+
// Description is owned by the plugin index (src/plugins/init/index.ts).
|
|
15147
|
+
.argument('[dest]', 'destination path (default: ./)', '.')
|
|
15148
|
+
.option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
|
|
15149
|
+
.option('--with-local', 'include .local config variant')
|
|
15150
|
+
.option('--dynamic', 'include dynamic examples (JS/TS configs)')
|
|
15151
|
+
.option('--cli-name <string>', 'CLI name for skeleton and tokens')
|
|
15152
|
+
.option('--force', 'overwrite all existing files')
|
|
15153
|
+
.option('--yes', 'skip all collisions (no overwrite)'));
|
|
15154
|
+
}
|
|
15155
|
+
|
|
15156
|
+
/**
|
|
15157
|
+
* @packageDocumentation
|
|
15158
|
+
* Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
|
|
15159
|
+
* host‑based CLI skeleton with collision handling and CI‑safe defaults.
|
|
15160
|
+
*/
|
|
15161
|
+
/**
|
|
15162
|
+
* Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
|
|
15163
|
+
* Supports collision detection, interactive prompts, and CI bypass.
|
|
15164
|
+
*/
|
|
15165
|
+
const initPlugin = () => definePlugin({
|
|
15166
|
+
ns: 'init',
|
|
15167
|
+
setup(cli) {
|
|
15168
|
+
cli.description('Scaffold getdotenv config files and a host-based CLI skeleton.');
|
|
15169
|
+
const initCmd = attachInitOptions(cli);
|
|
15170
|
+
attachInitDefaultAction(initCmd);
|
|
14787
15171
|
return undefined;
|
|
14788
15172
|
},
|
|
14789
15173
|
});
|