@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/plugins.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 = {};
|
|
@@ -714,6 +732,11 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
|
714
732
|
* Apply a dynamic map to the target progressively.
|
|
715
733
|
* - Functions receive (target, env) and may return string | undefined.
|
|
716
734
|
* - Literals are assigned directly (including undefined).
|
|
735
|
+
*
|
|
736
|
+
* @param target - Mutable target environment to assign into.
|
|
737
|
+
* @param map - Dynamic map to apply (functions and/or literal values).
|
|
738
|
+
* @param env - Selected environment name (if any) passed through to dynamic functions.
|
|
739
|
+
* @returns Nothing.
|
|
717
740
|
*/
|
|
718
741
|
function applyDynamicMap(target, map, env) {
|
|
719
742
|
if (!map)
|
|
@@ -733,6 +756,12 @@ function applyDynamicMap(target, map, env) {
|
|
|
733
756
|
* Error behavior:
|
|
734
757
|
* - On failure to load/compile/evaluate the module, throws a unified message:
|
|
735
758
|
* "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
|
|
759
|
+
*
|
|
760
|
+
* @param target - Mutable target environment to assign into.
|
|
761
|
+
* @param absPath - Absolute path to the dynamic module file.
|
|
762
|
+
* @param env - Selected environment name (if any).
|
|
763
|
+
* @param cacheDirName - Cache subdirectory under `.tsbuild/` for compiled artifacts.
|
|
764
|
+
* @returns A `Promise\<void\>` which resolves after the module (if present) has been applied.
|
|
736
765
|
*/
|
|
737
766
|
async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
|
|
738
767
|
if (!(await fs.exists(absPath)))
|
|
@@ -1895,6 +1924,10 @@ function renderOptionGroups(cmd) {
|
|
|
1895
1924
|
/**
|
|
1896
1925
|
* Compose root/parent help output by inserting grouped sections between
|
|
1897
1926
|
* Options and Commands, ensuring a trailing blank line.
|
|
1927
|
+
*
|
|
1928
|
+
* @param base - Base help text produced by Commander.
|
|
1929
|
+
* @param cmd - Command instance whose grouped options should be rendered.
|
|
1930
|
+
* @returns The modified help text with grouped blocks inserted.
|
|
1898
1931
|
*/
|
|
1899
1932
|
function buildHelpInformation(base, cmd) {
|
|
1900
1933
|
const groups = renderOptionGroups(cmd);
|
|
@@ -2396,6 +2429,10 @@ const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
|
2396
2429
|
/**
|
|
2397
2430
|
* Compose a child-process env overlay from dotenv and the merged CLI options bag.
|
|
2398
2431
|
* Returns a shallow object including getDotenvCliOptions when serializable.
|
|
2432
|
+
*
|
|
2433
|
+
* @param merged - Resolved CLI options bag (or a JSON-serializable subset).
|
|
2434
|
+
* @param dotenv - Composed dotenv variables for the current invocation.
|
|
2435
|
+
* @returns A string-only env overlay suitable for child process spawning.
|
|
2399
2436
|
*/
|
|
2400
2437
|
function composeNestedEnv(merged, dotenv) {
|
|
2401
2438
|
const out = {};
|
|
@@ -2418,6 +2455,7 @@ function composeNestedEnv(merged, dotenv) {
|
|
|
2418
2455
|
* Strip one layer of symmetric outer quotes (single or double) from a string.
|
|
2419
2456
|
*
|
|
2420
2457
|
* @param s - Input string.
|
|
2458
|
+
* @returns `s` without one symmetric outer quote pair (when present).
|
|
2421
2459
|
*/
|
|
2422
2460
|
const stripOne = (s) => {
|
|
2423
2461
|
if (s.length < 2)
|
|
@@ -2430,6 +2468,9 @@ const stripOne = (s) => {
|
|
|
2430
2468
|
/**
|
|
2431
2469
|
* Preserve argv array for Node -e/--eval payloads under shell-off and
|
|
2432
2470
|
* peel one symmetric outer quote layer from the code argument.
|
|
2471
|
+
*
|
|
2472
|
+
* @param args - Argument vector intended for direct execution (shell-off).
|
|
2473
|
+
* @returns Either the original `args` or a modified copy with a normalized eval payload.
|
|
2433
2474
|
*/
|
|
2434
2475
|
function maybePreserveNodeEvalArgv(args) {
|
|
2435
2476
|
if (args.length >= 3) {
|
|
@@ -2676,23 +2717,57 @@ const buildSpawnEnv = (base, overlay) => {
|
|
|
2676
2717
|
* Apply resolved AWS context to `process.env` and `ctx.plugins`.
|
|
2677
2718
|
* Centralizes logic shared between the plugin action and `afterResolve` hook.
|
|
2678
2719
|
*
|
|
2720
|
+
* @param out - Resolved AWS context to apply.
|
|
2721
|
+
* @param ctx - Host context to publish non-sensitive metadata into.
|
|
2679
2722
|
* @param setProcessEnv - Whether to write credentials/region to `process.env` (default true).
|
|
2723
|
+
* @returns Nothing.
|
|
2680
2724
|
*/
|
|
2681
2725
|
function applyAwsContext(out, ctx, setProcessEnv = true) {
|
|
2682
2726
|
const { profile, region, credentials } = out;
|
|
2683
2727
|
if (setProcessEnv) {
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2728
|
+
// Ensure AWS credential sources are mutually exclusive.
|
|
2729
|
+
// The AWS SDK warns (and may change precedence in future) when both
|
|
2730
|
+
// AWS_PROFILE and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are set.
|
|
2731
|
+
const clear = (keys) => {
|
|
2732
|
+
for (const k of keys) {
|
|
2733
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
2734
|
+
delete process.env[k];
|
|
2688
2735
|
}
|
|
2689
|
-
}
|
|
2736
|
+
};
|
|
2737
|
+
const clearProfileVars = () => {
|
|
2738
|
+
clear(['AWS_PROFILE', 'AWS_DEFAULT_PROFILE', 'AWS_SDK_LOAD_CONFIG']);
|
|
2739
|
+
};
|
|
2740
|
+
const clearStaticCreds = () => {
|
|
2741
|
+
clear([
|
|
2742
|
+
'AWS_ACCESS_KEY_ID',
|
|
2743
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
2744
|
+
'AWS_SESSION_TOKEN',
|
|
2745
|
+
]);
|
|
2746
|
+
};
|
|
2747
|
+
// Mode A: exported/static credentials (clear profile vars)
|
|
2690
2748
|
if (credentials) {
|
|
2749
|
+
clearProfileVars();
|
|
2691
2750
|
process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
|
|
2692
2751
|
process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
|
|
2693
2752
|
if (credentials.sessionToken !== undefined) {
|
|
2694
2753
|
process.env.AWS_SESSION_TOKEN = credentials.sessionToken;
|
|
2695
2754
|
}
|
|
2755
|
+
else {
|
|
2756
|
+
delete process.env.AWS_SESSION_TOKEN;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
else if (profile) {
|
|
2760
|
+
// Mode B: profile-based (SSO) credentials (clear static creds)
|
|
2761
|
+
clearStaticCreds();
|
|
2762
|
+
process.env.AWS_PROFILE = profile;
|
|
2763
|
+
process.env.AWS_DEFAULT_PROFILE = profile;
|
|
2764
|
+
process.env.AWS_SDK_LOAD_CONFIG = '1';
|
|
2765
|
+
}
|
|
2766
|
+
if (region) {
|
|
2767
|
+
process.env.AWS_REGION = region;
|
|
2768
|
+
if (!process.env.AWS_DEFAULT_REGION) {
|
|
2769
|
+
process.env.AWS_DEFAULT_REGION = region;
|
|
2770
|
+
}
|
|
2696
2771
|
}
|
|
2697
2772
|
}
|
|
2698
2773
|
// Always publish minimal, non-sensitive metadata
|
|
@@ -2703,7 +2778,7 @@ function applyAwsContext(out, ctx, setProcessEnv = true) {
|
|
|
2703
2778
|
};
|
|
2704
2779
|
}
|
|
2705
2780
|
|
|
2706
|
-
const
|
|
2781
|
+
const AWS_CLI_TIMEOUT_MS = 15_000;
|
|
2707
2782
|
const trim = (s) => (typeof s === 'string' ? s.trim() : '');
|
|
2708
2783
|
const unquote = (s) => s.length >= 2 &&
|
|
2709
2784
|
((s.startsWith('"') && s.endsWith('"')) ||
|
|
@@ -2712,6 +2787,9 @@ const unquote = (s) => s.length >= 2 &&
|
|
|
2712
2787
|
: s;
|
|
2713
2788
|
/**
|
|
2714
2789
|
* Parse AWS credentials from JSON output (AWS CLI v2 export-credentials).
|
|
2790
|
+
*
|
|
2791
|
+
* @param txt - Raw stdout text from the AWS CLI.
|
|
2792
|
+
* @returns Parsed credentials, or `undefined` when the input is not recognized.
|
|
2715
2793
|
*/
|
|
2716
2794
|
const parseExportCredentialsJson = (txt) => {
|
|
2717
2795
|
try {
|
|
@@ -2735,6 +2813,10 @@ const parseExportCredentialsJson = (txt) => {
|
|
|
2735
2813
|
/**
|
|
2736
2814
|
* Parse AWS credentials from environment-export output (shell-agnostic).
|
|
2737
2815
|
* Supports POSIX `export KEY=VAL` and PowerShell `$Env:KEY=VAL`.
|
|
2816
|
+
* Also supports AWS CLI `windows-cmd` (`set KEY=VAL`) and `env-no-export` (`KEY=VAL`).
|
|
2817
|
+
*
|
|
2818
|
+
* @param txt - Raw stdout text from the AWS CLI.
|
|
2819
|
+
* @returns Parsed credentials, or `undefined` when the input is not recognized.
|
|
2738
2820
|
*/
|
|
2739
2821
|
const parseExportCredentialsEnv = (txt) => {
|
|
2740
2822
|
const lines = txt.split(/\r?\n/);
|
|
@@ -2745,12 +2827,17 @@ const parseExportCredentialsEnv = (txt) => {
|
|
|
2745
2827
|
const line = raw.trim();
|
|
2746
2828
|
if (!line)
|
|
2747
2829
|
continue;
|
|
2748
|
-
// POSIX: export AWS_ACCESS_KEY_ID=...,
|
|
2830
|
+
// POSIX: export AWS_ACCESS_KEY_ID=..., ...
|
|
2749
2831
|
let m = /^export\s+([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)
|
|
2753
|
-
|
|
2832
|
+
// PowerShell: $Env:AWS_ACCESS_KEY_ID="...", etc.
|
|
2833
|
+
if (!m)
|
|
2834
|
+
m = /^\$Env:([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
|
|
2835
|
+
// Windows cmd: set AWS_ACCESS_KEY_ID=..., etc.
|
|
2836
|
+
if (!m)
|
|
2837
|
+
m = /^(?:set)\s+([A-Z0-9_]+)\s*=\s*(.+)$/i.exec(line);
|
|
2838
|
+
// env-no-export: AWS_ACCESS_KEY_ID=..., etc.
|
|
2839
|
+
if (!m)
|
|
2840
|
+
m = /^([A-Z0-9_]+)\s*=\s*(.+)$/.exec(line);
|
|
2754
2841
|
if (!m)
|
|
2755
2842
|
continue;
|
|
2756
2843
|
const k = m[1];
|
|
@@ -2775,7 +2862,7 @@ const parseExportCredentialsEnv = (txt) => {
|
|
|
2775
2862
|
};
|
|
2776
2863
|
return undefined;
|
|
2777
2864
|
};
|
|
2778
|
-
const getAwsConfigure = async (key, profile, timeoutMs =
|
|
2865
|
+
const getAwsConfigure = async (key, profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
|
|
2779
2866
|
const r = await runCommandResult(['aws', 'configure', 'get', key, '--profile', profile], false, {
|
|
2780
2867
|
env: process.env,
|
|
2781
2868
|
timeoutMs,
|
|
@@ -2790,36 +2877,50 @@ const getAwsConfigure = async (key, profile, timeoutMs = DEFAULT_TIMEOUT_MS) =>
|
|
|
2790
2877
|
}
|
|
2791
2878
|
return undefined;
|
|
2792
2879
|
};
|
|
2793
|
-
const exportCredentials = async (profile, timeoutMs =
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2880
|
+
const exportCredentials = async (profile, timeoutMs = AWS_CLI_TIMEOUT_MS) => {
|
|
2881
|
+
const tryExport = async (format) => {
|
|
2882
|
+
const argv = [
|
|
2883
|
+
'aws',
|
|
2884
|
+
'configure',
|
|
2885
|
+
'export-credentials',
|
|
2886
|
+
'--profile',
|
|
2887
|
+
profile,
|
|
2888
|
+
...(format ? ['--format', format] : []),
|
|
2889
|
+
];
|
|
2890
|
+
const r = await runCommandResult(argv, false, {
|
|
2891
|
+
env: process.env,
|
|
2892
|
+
timeoutMs,
|
|
2893
|
+
});
|
|
2894
|
+
if (r.exitCode !== 0)
|
|
2895
|
+
return undefined;
|
|
2896
|
+
const out = trim(r.stdout);
|
|
2897
|
+
if (!out)
|
|
2898
|
+
return undefined;
|
|
2899
|
+
// Some formats produce JSON ("process"), some produce shell-ish env lines.
|
|
2900
|
+
return parseExportCredentialsJson(out) ?? parseExportCredentialsEnv(out);
|
|
2901
|
+
};
|
|
2902
|
+
// Prefer the default/JSON "process" format first; then fall back to shell env outputs.
|
|
2903
|
+
// Note: AWS CLI v2 supports: process | env | env-no-export | powershell | windows-cmd
|
|
2904
|
+
const formats = [
|
|
2905
|
+
'process',
|
|
2906
|
+
...(process.platform === 'win32'
|
|
2907
|
+
? ['powershell', 'windows-cmd', 'env', 'env-no-export']
|
|
2908
|
+
: ['env', 'env-no-export']),
|
|
2909
|
+
];
|
|
2910
|
+
for (const f of formats) {
|
|
2911
|
+
const creds = await tryExport(f);
|
|
2813
2912
|
if (creds)
|
|
2814
2913
|
return creds;
|
|
2815
2914
|
}
|
|
2816
|
-
|
|
2915
|
+
// Final fallback: no --format (AWS CLI default output)
|
|
2916
|
+
return tryExport(undefined);
|
|
2817
2917
|
};
|
|
2818
2918
|
/**
|
|
2819
2919
|
* Resolve AWS context (profile, region, credentials) using configuration and environment.
|
|
2820
2920
|
* Applies strategy (cli-export vs none) and handling for SSO login-on-demand.
|
|
2821
2921
|
*
|
|
2822
2922
|
* @param options - Context options including current dotenv and plugin config.
|
|
2923
|
+
* @returns A `Promise\<AwsContext\>` containing any resolved profile, region, and credentials.
|
|
2823
2924
|
*/
|
|
2824
2925
|
const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
2825
2926
|
const profileKey = cfg.profileKey ?? 'AWS_LOCAL_PROFILE';
|
|
@@ -2844,31 +2945,27 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
|
2844
2945
|
out.region = region;
|
|
2845
2946
|
return out;
|
|
2846
2947
|
}
|
|
2847
|
-
// Env-first credentials.
|
|
2848
2948
|
let credentials;
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
const envToken = trim(process.env.AWS_SESSION_TOKEN);
|
|
2852
|
-
if (envId && envSecret) {
|
|
2853
|
-
credentials = {
|
|
2854
|
-
accessKeyId: envId,
|
|
2855
|
-
secretAccessKey: envSecret,
|
|
2856
|
-
...(envToken ? { sessionToken: envToken } : {}),
|
|
2857
|
-
};
|
|
2858
|
-
}
|
|
2859
|
-
else if (profile) {
|
|
2949
|
+
// Profile wins over ambient env creds when present (from flags/config/dotenv).
|
|
2950
|
+
if (profile) {
|
|
2860
2951
|
// Try export-credentials
|
|
2861
2952
|
credentials = await exportCredentials(profile);
|
|
2862
2953
|
// On failure, detect SSO and optionally login then retry
|
|
2863
2954
|
if (!credentials) {
|
|
2864
2955
|
const ssoSession = await getAwsConfigure('sso_session', profile);
|
|
2865
|
-
|
|
2956
|
+
// Legacy SSO profiles use sso_start_url/sso_region rather than sso_session.
|
|
2957
|
+
const ssoStartUrl = await getAwsConfigure('sso_start_url', profile);
|
|
2958
|
+
const looksSSO = (typeof ssoSession === 'string' && ssoSession.length > 0) ||
|
|
2959
|
+
(typeof ssoStartUrl === 'string' && ssoStartUrl.length > 0);
|
|
2866
2960
|
if (looksSSO && cfg.loginOnDemand) {
|
|
2867
|
-
//
|
|
2868
|
-
await
|
|
2961
|
+
// Interactive login (no timeout by default), then retry export once.
|
|
2962
|
+
const exit = await runCommand(['aws', 'sso', 'login', '--profile', profile], false, {
|
|
2869
2963
|
env: process.env,
|
|
2870
|
-
|
|
2964
|
+
stdio: 'inherit',
|
|
2871
2965
|
});
|
|
2966
|
+
if (exit !== 0) {
|
|
2967
|
+
throw new Error(`aws sso login failed for profile '${profile}' (exit ${String(exit)})`);
|
|
2968
|
+
}
|
|
2872
2969
|
credentials = await exportCredentials(profile);
|
|
2873
2970
|
}
|
|
2874
2971
|
}
|
|
@@ -2886,6 +2983,19 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
|
2886
2983
|
}
|
|
2887
2984
|
}
|
|
2888
2985
|
}
|
|
2986
|
+
else {
|
|
2987
|
+
// Env-first credentials when no profile is present.
|
|
2988
|
+
const envId = trim(process.env.AWS_ACCESS_KEY_ID);
|
|
2989
|
+
const envSecret = trim(process.env.AWS_SECRET_ACCESS_KEY);
|
|
2990
|
+
const envToken = trim(process.env.AWS_SESSION_TOKEN);
|
|
2991
|
+
if (envId && envSecret) {
|
|
2992
|
+
credentials = {
|
|
2993
|
+
accessKeyId: envId,
|
|
2994
|
+
secretAccessKey: envSecret,
|
|
2995
|
+
...(envToken ? { sessionToken: envToken } : {}),
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2889
2999
|
// Final region resolution
|
|
2890
3000
|
if (!region && profile)
|
|
2891
3001
|
region = await getAwsConfigure('region', profile);
|
|
@@ -2901,10 +3011,213 @@ const resolveAwsContext = async ({ dotenv, cfg, }) => {
|
|
|
2901
3011
|
return out;
|
|
2902
3012
|
};
|
|
2903
3013
|
|
|
3014
|
+
/**
|
|
3015
|
+
* Create the AWS plugin `afterResolve` hook.
|
|
3016
|
+
*
|
|
3017
|
+
* This runs once per invocation after the host resolves dotenv context.
|
|
3018
|
+
*
|
|
3019
|
+
* @param plugin - The AWS plugin instance.
|
|
3020
|
+
* @returns An `afterResolve` hook function suitable for assigning to `plugin.afterResolve`.
|
|
3021
|
+
*
|
|
3022
|
+
* @internal
|
|
3023
|
+
*/
|
|
3024
|
+
function attachAwsAfterResolveHook(plugin) {
|
|
3025
|
+
return async (cli, ctx) => {
|
|
3026
|
+
const cfg = plugin.readConfig(cli);
|
|
3027
|
+
const out = await resolveAwsContext({
|
|
3028
|
+
dotenv: ctx.dotenv,
|
|
3029
|
+
cfg,
|
|
3030
|
+
});
|
|
3031
|
+
applyAwsContext(out, ctx, true);
|
|
3032
|
+
// Optional: low-noise breadcrumb for diagnostics
|
|
3033
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3034
|
+
try {
|
|
3035
|
+
const msg = JSON.stringify({
|
|
3036
|
+
profile: out.profile,
|
|
3037
|
+
region: out.region,
|
|
3038
|
+
hasCreds: Boolean(out.credentials),
|
|
3039
|
+
});
|
|
3040
|
+
process.stderr.write(`[aws] afterResolve ${msg}\n`);
|
|
3041
|
+
}
|
|
3042
|
+
catch {
|
|
3043
|
+
/* ignore */
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/** @internal */
|
|
3050
|
+
const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
3051
|
+
/**
|
|
3052
|
+
* Create an AWS plugin config overlay from Commander-parsed option values.
|
|
3053
|
+
*
|
|
3054
|
+
* This preserves tri-state intent:
|
|
3055
|
+
* - If a flag was not provided, it should not overwrite config-derived defaults.
|
|
3056
|
+
* - If `--no-…` was provided, it must explicitly force the boolean false.
|
|
3057
|
+
*
|
|
3058
|
+
* @param opts - Commander option values for the current invocation.
|
|
3059
|
+
* @returns A partial AWS plugin config object containing only explicit overrides.
|
|
3060
|
+
*
|
|
3061
|
+
* @internal
|
|
3062
|
+
*/
|
|
3063
|
+
function awsConfigOverridesFromCommandOpts(opts) {
|
|
3064
|
+
const o = isRecord(opts) ? opts : {};
|
|
3065
|
+
const overlay = {};
|
|
3066
|
+
// Map boolean toggles (respect explicit --no-*)
|
|
3067
|
+
if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand')) {
|
|
3068
|
+
overlay.loginOnDemand = Boolean(o.loginOnDemand);
|
|
3069
|
+
}
|
|
3070
|
+
// Strings/enums
|
|
3071
|
+
if (typeof o.profile === 'string')
|
|
3072
|
+
overlay.profile = o.profile;
|
|
3073
|
+
if (typeof o.region === 'string')
|
|
3074
|
+
overlay.region = o.region;
|
|
3075
|
+
if (typeof o.defaultRegion === 'string')
|
|
3076
|
+
overlay.defaultRegion = o.defaultRegion;
|
|
3077
|
+
if (o.strategy === 'cli-export' || o.strategy === 'none') {
|
|
3078
|
+
overlay.strategy = o.strategy;
|
|
3079
|
+
}
|
|
3080
|
+
// Advanced key overrides
|
|
3081
|
+
if (typeof o.profileKey === 'string')
|
|
3082
|
+
overlay.profileKey = o.profileKey;
|
|
3083
|
+
if (typeof o.profileFallbackKey === 'string') {
|
|
3084
|
+
overlay.profileFallbackKey = o.profileFallbackKey;
|
|
3085
|
+
}
|
|
3086
|
+
if (typeof o.regionKey === 'string')
|
|
3087
|
+
overlay.regionKey = o.regionKey;
|
|
3088
|
+
return overlay;
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
/**
|
|
3092
|
+
* Attach the default action for the AWS plugin mount.
|
|
3093
|
+
*
|
|
3094
|
+
* Behavior:
|
|
3095
|
+
* - With args: forwards to AWS CLI (`aws <args...>`) under the established session.
|
|
3096
|
+
* - Without args: session-only establishment (no forward).
|
|
3097
|
+
*
|
|
3098
|
+
* @param cli - The `aws` command mount.
|
|
3099
|
+
* @param plugin - The AWS plugin instance.
|
|
3100
|
+
*
|
|
3101
|
+
* @internal
|
|
3102
|
+
*/
|
|
3103
|
+
function attachAwsDefaultAction(cli, plugin, awsCmd) {
|
|
3104
|
+
awsCmd.action(async (args, opts, thisCommand) => {
|
|
3105
|
+
// Access merged root CLI options (installed by root hooks).
|
|
3106
|
+
const bag = readMergedOptions(thisCommand);
|
|
3107
|
+
const capture = shouldCapture(bag.capture);
|
|
3108
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
3109
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
3110
|
+
// Build overlay cfg from subcommand flags layered over discovered config.
|
|
3111
|
+
const ctx = cli.getCtx();
|
|
3112
|
+
const cfgBase = plugin.readConfig(cli);
|
|
3113
|
+
const cfg = {
|
|
3114
|
+
...cfgBase,
|
|
3115
|
+
...awsConfigOverridesFromCommandOpts(opts),
|
|
3116
|
+
};
|
|
3117
|
+
// Resolve current context with overrides
|
|
3118
|
+
const out = await resolveAwsContext({
|
|
3119
|
+
dotenv: ctx.dotenv,
|
|
3120
|
+
cfg,
|
|
3121
|
+
});
|
|
3122
|
+
// Publish env/context
|
|
3123
|
+
applyAwsContext(out, ctx, true);
|
|
3124
|
+
// Forward when positional args are present; otherwise session-only.
|
|
3125
|
+
if (args.length > 0) {
|
|
3126
|
+
const argv = ['aws', ...args];
|
|
3127
|
+
const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
|
|
3128
|
+
const exit = await runCommand(argv, shellSetting, {
|
|
3129
|
+
env: buildSpawnEnv(process.env, ctx.dotenv),
|
|
3130
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
3131
|
+
});
|
|
3132
|
+
// Deterministic termination (suppressed under tests)
|
|
3133
|
+
if (!underTests) {
|
|
3134
|
+
process.exit(typeof exit === 'number' ? exit : 0);
|
|
3135
|
+
}
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
// Session only: low-noise breadcrumb under debug
|
|
3139
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3140
|
+
try {
|
|
3141
|
+
const msg = JSON.stringify({
|
|
3142
|
+
profile: out.profile,
|
|
3143
|
+
region: out.region,
|
|
3144
|
+
hasCreds: Boolean(out.credentials),
|
|
3145
|
+
});
|
|
3146
|
+
process.stderr.write(`[aws] session established ${msg}\n`);
|
|
3147
|
+
}
|
|
3148
|
+
catch {
|
|
3149
|
+
/* ignore */
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
if (!underTests)
|
|
3153
|
+
process.exit(0);
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
/**
|
|
3158
|
+
* Attach options/arguments for the AWS plugin mount.
|
|
3159
|
+
*
|
|
3160
|
+
* @param cli - The `aws` command mount.
|
|
3161
|
+
* @param plugin - The AWS plugin instance (for dynamic option descriptions).
|
|
3162
|
+
*
|
|
3163
|
+
* @internal
|
|
3164
|
+
*/
|
|
3165
|
+
function attachAwsOptions(cli, plugin) {
|
|
3166
|
+
return (cli
|
|
3167
|
+
// Description is owned by the plugin index (src/plugins/aws/index.ts).
|
|
3168
|
+
.enablePositionalOptions()
|
|
3169
|
+
.passThroughOptions()
|
|
3170
|
+
.allowUnknownOption(true)
|
|
3171
|
+
// Boolean toggles with dynamic help labels (effective defaults)
|
|
3172
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
|
|
3173
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
|
|
3174
|
+
// Strings / enums
|
|
3175
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
|
|
3176
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
|
|
3177
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
|
|
3178
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
|
|
3179
|
+
// Advanced key overrides
|
|
3180
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
|
|
3181
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
|
|
3182
|
+
.addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
|
|
3183
|
+
// Accept any extra operands so Commander does not error when tokens appear after "--".
|
|
3184
|
+
.argument('[args...]'));
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
/**
|
|
3188
|
+
* Attach the AWS plugin `preSubcommand` hook.
|
|
3189
|
+
*
|
|
3190
|
+
* Ensures `aws --profile/--region <child>` applies the AWS session setup before
|
|
3191
|
+
* child subcommand execution.
|
|
3192
|
+
*
|
|
3193
|
+
* @param cli - The `aws` command mount.
|
|
3194
|
+
* @param plugin - The AWS plugin instance.
|
|
3195
|
+
*
|
|
3196
|
+
* @internal
|
|
3197
|
+
*/
|
|
3198
|
+
function attachAwsPreSubcommandHook(cli, plugin) {
|
|
3199
|
+
cli.hook('preSubcommand', async (thisCommand) => {
|
|
3200
|
+
// Avoid side effects for help rendering.
|
|
3201
|
+
if (process.argv.includes('-h') || process.argv.includes('--help'))
|
|
3202
|
+
return;
|
|
3203
|
+
const ctx = cli.getCtx();
|
|
3204
|
+
const cfgBase = plugin.readConfig(cli);
|
|
3205
|
+
const cfg = {
|
|
3206
|
+
...cfgBase,
|
|
3207
|
+
...awsConfigOverridesFromCommandOpts(thisCommand.opts()),
|
|
3208
|
+
};
|
|
3209
|
+
const out = await resolveAwsContext({
|
|
3210
|
+
dotenv: ctx.dotenv,
|
|
3211
|
+
cfg,
|
|
3212
|
+
});
|
|
3213
|
+
applyAwsContext(out, ctx, true);
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
|
|
2904
3217
|
/**
|
|
2905
3218
|
* Zod schema for AWS plugin configuration.
|
|
2906
3219
|
*/
|
|
2907
|
-
const
|
|
3220
|
+
const awsPluginConfigSchema = z$2.object({
|
|
2908
3221
|
profile: z$2.string().optional(),
|
|
2909
3222
|
region: z$2.string().optional(),
|
|
2910
3223
|
defaultRegion: z$2.string().optional(),
|
|
@@ -2928,129 +3241,16 @@ const AwsPluginConfigSchema = z$2.object({
|
|
|
2928
3241
|
const awsPlugin = () => {
|
|
2929
3242
|
const plugin = definePlugin({
|
|
2930
3243
|
ns: 'aws',
|
|
2931
|
-
configSchema:
|
|
2932
|
-
setup
|
|
2933
|
-
|
|
2934
|
-
cli
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
.passThroughOptions()
|
|
2938
|
-
.allowUnknownOption(true)
|
|
2939
|
-
// Boolean toggles with dynamic help labels (effective defaults)
|
|
2940
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--login-on-demand', (_bag, cfg) => `attempt aws sso login on-demand${cfg.loginOnDemand ? ' (default)' : ''}`))
|
|
2941
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--no-login-on-demand', (_bag, cfg) => `disable sso login on-demand${cfg.loginOnDemand === false ? ' (default)' : ''}`))
|
|
2942
|
-
// Strings / enums
|
|
2943
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--profile <string>', (_bag, cfg) => `AWS profile name${cfg.profile ? ` (default: ${JSON.stringify(cfg.profile)})` : ''}`))
|
|
2944
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--region <string>', (_bag, cfg) => `AWS region${cfg.region ? ` (default: ${JSON.stringify(cfg.region)})` : ''}`))
|
|
2945
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--default-region <string>', (_bag, cfg) => `fallback region${cfg.defaultRegion ? ` (default: ${JSON.stringify(cfg.defaultRegion)})` : ''}`))
|
|
2946
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--strategy <string>', (_bag, cfg) => `credential acquisition strategy: cli-export|none${cfg.strategy ? ` (default: ${JSON.stringify(cfg.strategy)})` : ''}`))
|
|
2947
|
-
// Advanced key overrides
|
|
2948
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--profile-key <string>', (_bag, cfg) => `dotenv/config key for local profile${cfg.profileKey ? ` (default: ${JSON.stringify(cfg.profileKey)})` : ''}`))
|
|
2949
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--profile-fallback-key <string>', (_bag, cfg) => `fallback dotenv/config key for profile${cfg.profileFallbackKey ? ` (default: ${JSON.stringify(cfg.profileFallbackKey)})` : ''}`))
|
|
2950
|
-
.addOption(plugin.createPluginDynamicOption(cli, '--region-key <string>', (_bag, cfg) => `dotenv/config key for region${cfg.regionKey ? ` (default: ${JSON.stringify(cfg.regionKey)})` : ''}`))
|
|
2951
|
-
// Accept any extra operands so Commander does not error when tokens appear after "--".
|
|
2952
|
-
.argument('[args...]')
|
|
2953
|
-
.action(async (args, opts, thisCommand) => {
|
|
2954
|
-
const pluginInst = plugin;
|
|
2955
|
-
// Access merged root CLI options (installed by passOptions())
|
|
2956
|
-
const bag = readMergedOptions(thisCommand);
|
|
2957
|
-
const capture = shouldCapture(bag.capture);
|
|
2958
|
-
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
2959
|
-
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
2960
|
-
// Build overlay cfg from subcommand flags layered over discovered config.
|
|
2961
|
-
const ctx = cli.getCtx();
|
|
2962
|
-
const cfgBase = pluginInst.readConfig(cli);
|
|
2963
|
-
const o = opts;
|
|
2964
|
-
const overlay = {};
|
|
2965
|
-
// Map boolean toggles (respect explicit --no-*)
|
|
2966
|
-
if (Object.prototype.hasOwnProperty.call(o, 'loginOnDemand'))
|
|
2967
|
-
overlay.loginOnDemand = Boolean(o.loginOnDemand);
|
|
2968
|
-
// Strings/enums
|
|
2969
|
-
if (typeof o.profile === 'string')
|
|
2970
|
-
overlay.profile = o.profile;
|
|
2971
|
-
if (typeof o.region === 'string')
|
|
2972
|
-
overlay.region = o.region;
|
|
2973
|
-
if (typeof o.defaultRegion === 'string')
|
|
2974
|
-
overlay.defaultRegion = o.defaultRegion;
|
|
2975
|
-
if (typeof o.strategy === 'string')
|
|
2976
|
-
overlay.strategy = o.strategy;
|
|
2977
|
-
// Advanced key overrides
|
|
2978
|
-
if (typeof o.profileKey === 'string')
|
|
2979
|
-
overlay.profileKey = o.profileKey;
|
|
2980
|
-
if (typeof o.profileFallbackKey === 'string')
|
|
2981
|
-
overlay.profileFallbackKey = o.profileFallbackKey;
|
|
2982
|
-
if (typeof o.regionKey === 'string')
|
|
2983
|
-
overlay.regionKey = o.regionKey;
|
|
2984
|
-
const cfg = {
|
|
2985
|
-
...cfgBase,
|
|
2986
|
-
...overlay,
|
|
2987
|
-
};
|
|
2988
|
-
// Resolve current context with overrides
|
|
2989
|
-
const out = await resolveAwsContext({
|
|
2990
|
-
dotenv: ctx.dotenv,
|
|
2991
|
-
cfg,
|
|
2992
|
-
});
|
|
2993
|
-
// Publish env/context
|
|
2994
|
-
applyAwsContext(out, ctx, true);
|
|
2995
|
-
// Forward when positional args are present; otherwise session-only.
|
|
2996
|
-
if (Array.isArray(args) && args.length > 0) {
|
|
2997
|
-
const argv = ['aws', ...args];
|
|
2998
|
-
const shellSetting = resolveShell(bag.scripts, 'aws', bag.shell);
|
|
2999
|
-
const exit = await runCommand(argv, shellSetting, {
|
|
3000
|
-
env: buildSpawnEnv(process.env, ctx.dotenv),
|
|
3001
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
3002
|
-
});
|
|
3003
|
-
// Deterministic termination (suppressed under tests)
|
|
3004
|
-
if (!underTests) {
|
|
3005
|
-
process.exit(typeof exit === 'number' ? exit : 0);
|
|
3006
|
-
}
|
|
3007
|
-
return;
|
|
3008
|
-
}
|
|
3009
|
-
else {
|
|
3010
|
-
// Session only: low-noise breadcrumb under debug
|
|
3011
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
3012
|
-
try {
|
|
3013
|
-
const msg = JSON.stringify({
|
|
3014
|
-
profile: out.profile,
|
|
3015
|
-
region: out.region,
|
|
3016
|
-
hasCreds: Boolean(out.credentials),
|
|
3017
|
-
});
|
|
3018
|
-
process.stderr.write(`[aws] session established ${msg}\n`);
|
|
3019
|
-
}
|
|
3020
|
-
catch {
|
|
3021
|
-
/* ignore */
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
if (!underTests)
|
|
3025
|
-
process.exit(0);
|
|
3026
|
-
return;
|
|
3027
|
-
}
|
|
3028
|
-
});
|
|
3244
|
+
configSchema: awsPluginConfigSchema,
|
|
3245
|
+
setup(cli) {
|
|
3246
|
+
cli.description('Establish an AWS session and optionally forward to the AWS CLI');
|
|
3247
|
+
const awsCmd = attachAwsOptions(cli, plugin);
|
|
3248
|
+
attachAwsPreSubcommandHook(cli, plugin);
|
|
3249
|
+
attachAwsDefaultAction(cli, plugin, awsCmd);
|
|
3029
3250
|
return undefined;
|
|
3030
3251
|
},
|
|
3031
|
-
afterResolve: async (_cli, ctx) => {
|
|
3032
|
-
const cfg = plugin.readConfig(_cli);
|
|
3033
|
-
const out = await resolveAwsContext({
|
|
3034
|
-
dotenv: ctx.dotenv,
|
|
3035
|
-
cfg,
|
|
3036
|
-
});
|
|
3037
|
-
applyAwsContext(out, ctx, true);
|
|
3038
|
-
// Optional: low-noise breadcrumb for diagnostics
|
|
3039
|
-
if (process.env.GETDOTENV_DEBUG) {
|
|
3040
|
-
try {
|
|
3041
|
-
const msg = JSON.stringify({
|
|
3042
|
-
profile: out.profile,
|
|
3043
|
-
region: out.region,
|
|
3044
|
-
hasCreds: Boolean(out.credentials),
|
|
3045
|
-
});
|
|
3046
|
-
process.stderr.write(`[aws] afterResolve ${msg}\n`);
|
|
3047
|
-
}
|
|
3048
|
-
catch {
|
|
3049
|
-
/* ignore */
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
},
|
|
3053
3252
|
});
|
|
3253
|
+
plugin.afterResolve = attachAwsAfterResolveHook(plugin);
|
|
3054
3254
|
return plugin;
|
|
3055
3255
|
};
|
|
3056
3256
|
|
|
@@ -13512,21 +13712,78 @@ class GetCallerIdentityCommand extends Command
|
|
|
13512
13712
|
}
|
|
13513
13713
|
|
|
13514
13714
|
/**
|
|
13515
|
-
*
|
|
13516
|
-
*
|
|
13715
|
+
* Attach the default action for the `aws whoami` command.
|
|
13716
|
+
*
|
|
13717
|
+
* This behavior executes only when `aws whoami` is invoked without a subcommand.
|
|
13718
|
+
*
|
|
13719
|
+
* @param cli - The `whoami` command mount.
|
|
13720
|
+
* @returns Nothing.
|
|
13721
|
+
*/
|
|
13722
|
+
function attachWhoamiDefaultAction(cli) {
|
|
13723
|
+
cli.action(async () => {
|
|
13724
|
+
// The AWS SDK default providers will read credentials from process.env,
|
|
13725
|
+
// which the aws parent has already populated.
|
|
13726
|
+
const client = new STSClient$1();
|
|
13727
|
+
const result = await client.send(new GetCallerIdentityCommand());
|
|
13728
|
+
console.log(JSON.stringify(result, null, 2));
|
|
13729
|
+
});
|
|
13730
|
+
}
|
|
13731
|
+
|
|
13732
|
+
/**
|
|
13733
|
+
* Attach options/arguments for the `aws whoami` plugin mount.
|
|
13734
|
+
*
|
|
13735
|
+
* This subcommand currently takes no flags/args; this module exists to keep the
|
|
13736
|
+
* wiring layout consistent across shipped plugins (options vs actions).
|
|
13737
|
+
*
|
|
13738
|
+
* Note: the plugin description is owned by `src/plugins/aws/whoami/index.ts` and
|
|
13739
|
+
* must not be set here.
|
|
13740
|
+
*
|
|
13741
|
+
* @param cli - The `whoami` command mount under `aws`.
|
|
13742
|
+
* @returns The same `cli` instance for chaining.
|
|
13743
|
+
*
|
|
13744
|
+
* @internal
|
|
13745
|
+
*/
|
|
13746
|
+
function attachWhoamiOptions(cli) {
|
|
13747
|
+
return cli;
|
|
13748
|
+
}
|
|
13749
|
+
|
|
13750
|
+
/**
|
|
13751
|
+
* Attach the `really` subcommand under `aws whoami`.
|
|
13752
|
+
*
|
|
13753
|
+
* Reads `SECRET_IDENTITY` from the resolved get-dotenv context (`cli.getCtx().dotenv`).
|
|
13754
|
+
*
|
|
13755
|
+
* @param cli - The `whoami` command mount.
|
|
13756
|
+
* @returns Nothing.
|
|
13757
|
+
*/
|
|
13758
|
+
function attachWhoamiReallyAction(cli) {
|
|
13759
|
+
const really = cli
|
|
13760
|
+
.ns('really')
|
|
13761
|
+
.description('Print SECRET_IDENTITY from the resolved dotenv context');
|
|
13762
|
+
really.action(() => {
|
|
13763
|
+
const secretIdentity = really.getCtx().dotenv.SECRET_IDENTITY;
|
|
13764
|
+
console.log(`Your secret identity is ${secretIdentity ?? 'still a secret'}.`);
|
|
13765
|
+
});
|
|
13766
|
+
}
|
|
13767
|
+
|
|
13768
|
+
/**
|
|
13769
|
+
* AWS Whoami plugin factory.
|
|
13770
|
+
*
|
|
13771
|
+
* This plugin demonstrates a “bucket of subcommands” pattern:
|
|
13772
|
+
* - Subcommand behavior is articulated in separate modules as `attach*` helpers.
|
|
13773
|
+
* - Those helpers are not individually composable plugins; they are internal wiring for one plugin instance.
|
|
13774
|
+
*
|
|
13775
|
+
* @returns A plugin instance mounted at `aws whoami`.
|
|
13517
13776
|
*/
|
|
13518
13777
|
const awsWhoamiPlugin = () => definePlugin({
|
|
13519
13778
|
ns: 'whoami',
|
|
13520
13779
|
setup(cli) {
|
|
13521
|
-
cli
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
console.log(JSON.stringify(result, null, 2));
|
|
13529
|
-
});
|
|
13780
|
+
cli.description('Print AWS caller identity (uses parent aws session)');
|
|
13781
|
+
// Options/args (none today, but keep layout consistent with other plugins).
|
|
13782
|
+
const whoami = attachWhoamiOptions(cli);
|
|
13783
|
+
// Default behavior: `getdotenv aws whoami`
|
|
13784
|
+
attachWhoamiDefaultAction(whoami);
|
|
13785
|
+
// Subcommand behavior: `getdotenv aws whoami really`
|
|
13786
|
+
attachWhoamiReallyAction(whoami);
|
|
13530
13787
|
return undefined;
|
|
13531
13788
|
},
|
|
13532
13789
|
});
|
|
@@ -13634,7 +13891,7 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv,
|
|
|
13634
13891
|
/**
|
|
13635
13892
|
* Attach the default "cmd" subcommand action with contextual typing.
|
|
13636
13893
|
*/
|
|
13637
|
-
const
|
|
13894
|
+
const attachBatchCmdAction = (plugin, cli, batchCmd, pluginOpts, cmd) => {
|
|
13638
13895
|
cmd.action(async (commandParts, _subOpts, thisCommand) => {
|
|
13639
13896
|
const mergedBag = readMergedOptions(batchCmd);
|
|
13640
13897
|
const logger = mergedBag.logger;
|
|
@@ -13743,11 +14000,37 @@ const attachDefaultCmdAction$1 = (plugin, cli, batchCmd, pluginOpts, cmd) => {
|
|
|
13743
14000
|
});
|
|
13744
14001
|
};
|
|
13745
14002
|
|
|
14003
|
+
/**
|
|
14004
|
+
* Attach the default `cmd` subcommand under the `batch` command.
|
|
14005
|
+
*
|
|
14006
|
+
* This encapsulates:
|
|
14007
|
+
* - Subcommand construction (`new Command().name('cmd')…`)
|
|
14008
|
+
* - Action wiring
|
|
14009
|
+
* - Mounting as the default subcommand for `batch`
|
|
14010
|
+
*
|
|
14011
|
+
* @param plugin - The batch plugin instance.
|
|
14012
|
+
* @param cli - The batch command mount.
|
|
14013
|
+
* @param batchCmd - The `batch` command (same as `cli` mount).
|
|
14014
|
+
* @param pluginOpts - Batch plugin factory options.
|
|
14015
|
+
*
|
|
14016
|
+
* @internal
|
|
14017
|
+
*/
|
|
14018
|
+
const attachBatchCmdSubcommand = (plugin, cli, batchCmd, pluginOpts) => {
|
|
14019
|
+
const cmdSub = new Command$1()
|
|
14020
|
+
.name('cmd')
|
|
14021
|
+
.description('execute command, conflicts with --command option (default subcommand)')
|
|
14022
|
+
.enablePositionalOptions()
|
|
14023
|
+
.passThroughOptions()
|
|
14024
|
+
.argument('[command...]');
|
|
14025
|
+
attachBatchCmdAction(plugin, cli, batchCmd, pluginOpts, cmdSub);
|
|
14026
|
+
batchCmd.addCommand(cmdSub, { isDefault: true });
|
|
14027
|
+
};
|
|
14028
|
+
|
|
13746
14029
|
/**
|
|
13747
14030
|
* Attach the parent-level action for the batch plugin.
|
|
13748
14031
|
* Handles parent flags (e.g. `getdotenv batch -l`) and delegates to the batch executor.
|
|
13749
14032
|
*/
|
|
13750
|
-
const
|
|
14033
|
+
const attachBatchDefaultAction = (plugin, cli, pluginOpts, parent) => {
|
|
13751
14034
|
parent.action(async function (...args) {
|
|
13752
14035
|
// Commander Unknown generics: [...unknown[], OptionValues, thisCommand]
|
|
13753
14036
|
const thisCommand = args[args.length - 1];
|
|
@@ -13848,6 +14131,45 @@ const attachParentInvoker$1 = (plugin, cli, pluginOpts, parent) => {
|
|
|
13848
14131
|
});
|
|
13849
14132
|
};
|
|
13850
14133
|
|
|
14134
|
+
/**
|
|
14135
|
+
* Attach options/arguments for the batch plugin mount.
|
|
14136
|
+
*
|
|
14137
|
+
* Note: the plugin description is owned by `src/plugins/batch/index.ts` and
|
|
14138
|
+
* must not be set here.
|
|
14139
|
+
*
|
|
14140
|
+
* @param plugin - Batch plugin instance (for dynamic option descriptions).
|
|
14141
|
+
* @param cli - The `batch` command mount.
|
|
14142
|
+
* @returns The same `cli` instance for chaining.
|
|
14143
|
+
*
|
|
14144
|
+
* @internal
|
|
14145
|
+
*/
|
|
14146
|
+
function attachBatchOptions(plugin, cli) {
|
|
14147
|
+
const GROUP = `plugin:${cli.name()}`;
|
|
14148
|
+
return (cli
|
|
14149
|
+
.enablePositionalOptions()
|
|
14150
|
+
.passThroughOptions()
|
|
14151
|
+
// Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
|
|
14152
|
+
.addOption((() => {
|
|
14153
|
+
const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
|
|
14154
|
+
cli.setOptionGroup(opt, GROUP);
|
|
14155
|
+
return opt;
|
|
14156
|
+
})())
|
|
14157
|
+
.addOption((() => {
|
|
14158
|
+
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 || './')})`);
|
|
14159
|
+
cli.setOptionGroup(opt, GROUP);
|
|
14160
|
+
return opt;
|
|
14161
|
+
})())
|
|
14162
|
+
.addOption((() => {
|
|
14163
|
+
const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
|
|
14164
|
+
cli.setOptionGroup(opt, GROUP);
|
|
14165
|
+
return opt;
|
|
14166
|
+
})())
|
|
14167
|
+
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
14168
|
+
.option('-l, --list', 'list working directories without executing command')
|
|
14169
|
+
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
14170
|
+
.argument('[command...]'));
|
|
14171
|
+
}
|
|
14172
|
+
|
|
13851
14173
|
/**
|
|
13852
14174
|
* Zod schema for a single script entry (string or object).
|
|
13853
14175
|
*/
|
|
@@ -13861,7 +14183,7 @@ const ScriptSchema = z$2.union([
|
|
|
13861
14183
|
/**
|
|
13862
14184
|
* Zod schema for batch plugin configuration.
|
|
13863
14185
|
*/
|
|
13864
|
-
const
|
|
14186
|
+
const batchPluginConfigSchema = z$2.object({
|
|
13865
14187
|
scripts: z$2.record(z$2.string(), ScriptSchema).optional(),
|
|
13866
14188
|
shell: z$2.union([z$2.string(), z$2.boolean()]).optional(),
|
|
13867
14189
|
rootPath: z$2.string().optional(),
|
|
@@ -13887,45 +14209,15 @@ const batchPlugin = (opts = {}) => {
|
|
|
13887
14209
|
ns: 'batch',
|
|
13888
14210
|
// Host validates this when config-loader is enabled; plugins may also
|
|
13889
14211
|
// re-validate at action time as a safety belt.
|
|
13890
|
-
configSchema:
|
|
14212
|
+
configSchema: batchPluginConfigSchema,
|
|
13891
14213
|
setup(cli) {
|
|
13892
14214
|
const batchCmd = cli; // mount provided by host
|
|
13893
|
-
|
|
13894
|
-
batchCmd
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
.addOption((() => {
|
|
13900
|
-
const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
|
|
13901
|
-
cli.setOptionGroup(opt, GROUP);
|
|
13902
|
-
return opt;
|
|
13903
|
-
})())
|
|
13904
|
-
.addOption((() => {
|
|
13905
|
-
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 || './')})`);
|
|
13906
|
-
cli.setOptionGroup(opt, GROUP);
|
|
13907
|
-
return opt;
|
|
13908
|
-
})())
|
|
13909
|
-
.addOption((() => {
|
|
13910
|
-
const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
|
|
13911
|
-
cli.setOptionGroup(opt, GROUP);
|
|
13912
|
-
return opt;
|
|
13913
|
-
})())
|
|
13914
|
-
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
13915
|
-
.option('-l, --list', 'list working directories without executing command')
|
|
13916
|
-
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
13917
|
-
.argument('[command...]');
|
|
13918
|
-
// Default subcommand "cmd" with contextual typing for args/opts
|
|
13919
|
-
const cmdSub = new Command$1()
|
|
13920
|
-
.name('cmd')
|
|
13921
|
-
.description('execute command, conflicts with --command option (default subcommand)')
|
|
13922
|
-
.enablePositionalOptions()
|
|
13923
|
-
.passThroughOptions()
|
|
13924
|
-
.argument('[command...]');
|
|
13925
|
-
attachDefaultCmdAction$1(plugin, cli, batchCmd, opts, cmdSub);
|
|
13926
|
-
batchCmd.addCommand(cmdSub, { isDefault: true });
|
|
13927
|
-
// Parent invoker (unified naming)
|
|
13928
|
-
attachParentInvoker$1(plugin, cli, opts, batchCmd);
|
|
14215
|
+
batchCmd.description('Batch command execution across multiple working directories.');
|
|
14216
|
+
attachBatchOptions(plugin, batchCmd);
|
|
14217
|
+
// Default subcommand `cmd` (mounted as batch default subcommand)
|
|
14218
|
+
attachBatchCmdSubcommand(plugin, cli, batchCmd, opts);
|
|
14219
|
+
// Default action for the batch command mount (parent flags and positional form)
|
|
14220
|
+
attachBatchDefaultAction(plugin, cli, opts, batchCmd);
|
|
13929
14221
|
return undefined;
|
|
13930
14222
|
},
|
|
13931
14223
|
});
|
|
@@ -14019,14 +14311,11 @@ async function runCmdWithContext(cli, merged, command, _opts) {
|
|
|
14019
14311
|
}
|
|
14020
14312
|
|
|
14021
14313
|
/**
|
|
14022
|
-
* Attach the default "cmd" subcommand action
|
|
14314
|
+
* Attach the default "cmd" subcommand action.
|
|
14023
14315
|
* Mirrors the prior inline implementation in cmd/index.ts.
|
|
14024
14316
|
*/
|
|
14025
|
-
const
|
|
14026
|
-
cmd
|
|
14027
|
-
.enablePositionalOptions()
|
|
14028
|
-
.passThroughOptions()
|
|
14029
|
-
.action(async function (...allArgs) {
|
|
14317
|
+
const attachCmdDefaultAction = (cli, cmd, aliasKey) => {
|
|
14318
|
+
cmd.action(async function (...allArgs) {
|
|
14030
14319
|
// Commander passes: [...positionals, options, thisCommand]
|
|
14031
14320
|
const thisCommand = allArgs[allArgs.length - 1];
|
|
14032
14321
|
const commandParts = allArgs[0];
|
|
@@ -14059,11 +14348,29 @@ const attachDefaultCmdAction = (cli, cmd, aliasKey) => {
|
|
|
14059
14348
|
});
|
|
14060
14349
|
};
|
|
14061
14350
|
|
|
14351
|
+
/**
|
|
14352
|
+
* Attach options/arguments for the cmd plugin mount.
|
|
14353
|
+
*
|
|
14354
|
+
* Note: the plugin description is owned by `src/plugins/cmd/index.ts` and must
|
|
14355
|
+
* not be set here.
|
|
14356
|
+
*
|
|
14357
|
+
* @param cli - The `cmd` command mount.
|
|
14358
|
+
* @returns The same `cli` instance for chaining.
|
|
14359
|
+
*
|
|
14360
|
+
* @internal
|
|
14361
|
+
*/
|
|
14362
|
+
function attachCmdOptions(cli) {
|
|
14363
|
+
return cli
|
|
14364
|
+
.enablePositionalOptions()
|
|
14365
|
+
.passThroughOptions()
|
|
14366
|
+
.argument('[command...]');
|
|
14367
|
+
}
|
|
14368
|
+
|
|
14062
14369
|
/**
|
|
14063
14370
|
* Install the parent-level invoker (alias) for the cmd plugin.
|
|
14064
14371
|
* Unifies naming with batch attachParentInvoker; behavior unchanged.
|
|
14065
14372
|
*/
|
|
14066
|
-
const
|
|
14373
|
+
const attachCmdParentInvoker = (cli, options, plugin) => {
|
|
14067
14374
|
const dbg = (...args) => {
|
|
14068
14375
|
if (process.env.GETDOTENV_DEBUG) {
|
|
14069
14376
|
try {
|
|
@@ -14174,7 +14481,7 @@ const attachParentInvoker = (cli, options, _cmd, plugin) => {
|
|
|
14174
14481
|
/**
|
|
14175
14482
|
* Zod schema for cmd plugin configuration.
|
|
14176
14483
|
*/
|
|
14177
|
-
const
|
|
14484
|
+
const cmdPluginConfigSchema = z$2
|
|
14178
14485
|
.object({
|
|
14179
14486
|
expand: z$2.boolean().optional(),
|
|
14180
14487
|
})
|
|
@@ -14194,7 +14501,7 @@ const CmdConfigSchema = z$2
|
|
|
14194
14501
|
const cmdPlugin = (options = {}) => {
|
|
14195
14502
|
const plugin = definePlugin({
|
|
14196
14503
|
ns: 'cmd',
|
|
14197
|
-
configSchema:
|
|
14504
|
+
configSchema: cmdPluginConfigSchema,
|
|
14198
14505
|
setup(cli) {
|
|
14199
14506
|
const aliasSpec = typeof options.optionAlias === 'string'
|
|
14200
14507
|
? { flags: options.optionAlias}
|
|
@@ -14206,14 +14513,13 @@ const cmdPlugin = (options = {}) => {
|
|
|
14206
14513
|
};
|
|
14207
14514
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
14208
14515
|
// Mount is the command ('cmd'); attach default action.
|
|
14209
|
-
cli
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
attachDefaultCmdAction(cli, cli, aliasKey);
|
|
14516
|
+
cli.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)');
|
|
14517
|
+
// Options/arguments (positional payload, argv routing) are attached separately.
|
|
14518
|
+
attachCmdOptions(cli);
|
|
14519
|
+
attachCmdDefaultAction(cli, cli, aliasKey);
|
|
14214
14520
|
// Parent-attached option alias (optional, unified naming).
|
|
14215
14521
|
if (aliasSpec !== undefined) {
|
|
14216
|
-
|
|
14522
|
+
attachCmdParentInvoker(cli, options, plugin);
|
|
14217
14523
|
}
|
|
14218
14524
|
return undefined;
|
|
14219
14525
|
},
|
|
@@ -14222,14 +14528,21 @@ const cmdPlugin = (options = {}) => {
|
|
|
14222
14528
|
};
|
|
14223
14529
|
|
|
14224
14530
|
/**
|
|
14225
|
-
* Ensure a directory exists.
|
|
14531
|
+
* Ensure a directory exists (parents included).
|
|
14532
|
+
*
|
|
14533
|
+
* @param p - Directory path to create.
|
|
14534
|
+
* @returns A `Promise\<string\>` resolving to the provided `p` value.
|
|
14226
14535
|
*/
|
|
14227
14536
|
const ensureDir = async (p) => {
|
|
14228
14537
|
await fs.ensureDir(p);
|
|
14229
14538
|
return p;
|
|
14230
14539
|
};
|
|
14231
14540
|
/**
|
|
14232
|
-
* Write text content to a file, ensuring the parent directory exists.
|
|
14541
|
+
* Write UTF-8 text content to a file, ensuring the parent directory exists.
|
|
14542
|
+
*
|
|
14543
|
+
* @param dest - Destination file path.
|
|
14544
|
+
* @param data - File contents to write.
|
|
14545
|
+
* @returns A `Promise\<void\>` which resolves when the file is written.
|
|
14233
14546
|
*/
|
|
14234
14547
|
const writeFile$1 = async (dest, data) => {
|
|
14235
14548
|
await ensureDir(path.dirname(dest));
|
|
@@ -14241,6 +14554,7 @@ const writeFile$1 = async (dest, data) => {
|
|
|
14241
14554
|
* @param src - Source file path.
|
|
14242
14555
|
* @param dest - Destination file path.
|
|
14243
14556
|
* @param substitutions - Map of token literals to replacement strings.
|
|
14557
|
+
* @returns A `Promise\<void\>` which resolves when the file has been copied.
|
|
14244
14558
|
*/
|
|
14245
14559
|
const copyTextFile = async (src, dest, substitutions) => {
|
|
14246
14560
|
const contents = await fs.readFile(src, 'utf-8');
|
|
@@ -14252,6 +14566,10 @@ const copyTextFile = async (src, dest, substitutions) => {
|
|
|
14252
14566
|
/**
|
|
14253
14567
|
* Ensure a set of lines exist (exact match) in a file. Creates the file
|
|
14254
14568
|
* when missing. Returns whether it was created or changed.
|
|
14569
|
+
*
|
|
14570
|
+
* @param filePath - Target file path to create/update.
|
|
14571
|
+
* @param lines - Lines which must be present (exact string match).
|
|
14572
|
+
* @returns A `Promise\<object\>` describing whether the file was created and/or changed.
|
|
14255
14573
|
*/
|
|
14256
14574
|
const ensureLines = async (filePath, lines) => {
|
|
14257
14575
|
const exists = await fs.pathExists(filePath);
|
|
@@ -14279,11 +14597,23 @@ const ensureLines = async (filePath, lines) => {
|
|
|
14279
14597
|
return { created: false, changed: false };
|
|
14280
14598
|
};
|
|
14281
14599
|
|
|
14282
|
-
|
|
14600
|
+
/**
|
|
14601
|
+
* Absolute path to the shipped templates directory.
|
|
14602
|
+
*
|
|
14603
|
+
* Used by the init scaffolder to locate files under `templates/` at runtime.
|
|
14604
|
+
*
|
|
14605
|
+
* @remarks
|
|
14606
|
+
* This path is resolved relative to the current working directory. It assumes
|
|
14607
|
+
* the `templates/` folder is present alongside the installed package (or in the
|
|
14608
|
+
* repository when running from source).
|
|
14609
|
+
*/
|
|
14283
14610
|
const TEMPLATES_ROOT = path.resolve('templates');
|
|
14284
14611
|
|
|
14285
14612
|
/**
|
|
14286
14613
|
* Plan the copy operations for configuration files.
|
|
14614
|
+
*
|
|
14615
|
+
* @param options - Planning options for config scaffolding.
|
|
14616
|
+
* @returns An array of copy operations to perform.
|
|
14287
14617
|
*/
|
|
14288
14618
|
const planConfigCopies = ({ format, withLocal, destRoot, }) => {
|
|
14289
14619
|
const copies = [];
|
|
@@ -14327,10 +14657,14 @@ const planConfigCopies = ({ format, withLocal, destRoot, }) => {
|
|
|
14327
14657
|
};
|
|
14328
14658
|
/**
|
|
14329
14659
|
* Plan the copy operations for the CLI skeleton.
|
|
14660
|
+
*
|
|
14661
|
+
* @param options - Planning options for CLI scaffolding.
|
|
14662
|
+
* @returns An array of copy operations to perform.
|
|
14330
14663
|
*/
|
|
14331
14664
|
const planCliCopies = ({ cliName, destRoot, }) => {
|
|
14332
14665
|
const subs = { __CLI_NAME__: cliName };
|
|
14333
14666
|
const base = path.join(destRoot, 'src', 'cli', cliName);
|
|
14667
|
+
const helloBase = path.join(base, 'plugins', 'hello');
|
|
14334
14668
|
return [
|
|
14335
14669
|
{
|
|
14336
14670
|
src: path.join(TEMPLATES_ROOT, 'cli', 'index.ts'),
|
|
@@ -14338,8 +14672,28 @@ const planCliCopies = ({ cliName, destRoot, }) => {
|
|
|
14338
14672
|
subs,
|
|
14339
14673
|
},
|
|
14340
14674
|
{
|
|
14341
|
-
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello.ts'),
|
|
14342
|
-
dest: path.join(
|
|
14675
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'index.ts'),
|
|
14676
|
+
dest: path.join(helloBase, 'index.ts'),
|
|
14677
|
+
subs,
|
|
14678
|
+
},
|
|
14679
|
+
{
|
|
14680
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'options.ts'),
|
|
14681
|
+
dest: path.join(helloBase, 'options.ts'),
|
|
14682
|
+
subs,
|
|
14683
|
+
},
|
|
14684
|
+
{
|
|
14685
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'defaultAction.ts'),
|
|
14686
|
+
dest: path.join(helloBase, 'defaultAction.ts'),
|
|
14687
|
+
subs,
|
|
14688
|
+
},
|
|
14689
|
+
{
|
|
14690
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'strangerAction.ts'),
|
|
14691
|
+
dest: path.join(helloBase, 'strangerAction.ts'),
|
|
14692
|
+
subs,
|
|
14693
|
+
},
|
|
14694
|
+
{
|
|
14695
|
+
src: path.join(TEMPLATES_ROOT, 'cli', 'plugins', 'hello', 'types.ts'),
|
|
14696
|
+
dest: path.join(helloBase, 'types.ts'),
|
|
14343
14697
|
subs,
|
|
14344
14698
|
},
|
|
14345
14699
|
];
|
|
@@ -14348,6 +14702,8 @@ const planCliCopies = ({ cliName, destRoot, }) => {
|
|
|
14348
14702
|
/**
|
|
14349
14703
|
* Determine whether the current environment should be treated as non-interactive.
|
|
14350
14704
|
* CI heuristics include: CI, GITHUB_ACTIONS, BUILDKITE, TEAMCITY_VERSION, TF_BUILD.
|
|
14705
|
+
*
|
|
14706
|
+
* @returns `true` when running in a CI-like environment or when stdin/stdout are not TTYs.
|
|
14351
14707
|
*/
|
|
14352
14708
|
const isNonInteractive = () => {
|
|
14353
14709
|
const ciLike = process.env.CI ||
|
|
@@ -14360,6 +14716,11 @@ const isNonInteractive = () => {
|
|
|
14360
14716
|
/**
|
|
14361
14717
|
* Prompt the user for a file collision decision.
|
|
14362
14718
|
* Returns a single-character code representing overwrite/example/skip (or 'all' variants).
|
|
14719
|
+
*
|
|
14720
|
+
* @param filePath - Path of the colliding file (for display).
|
|
14721
|
+
* @param logger - Logger used for user-facing messages.
|
|
14722
|
+
* @param rl - Readline interface used to capture user input.
|
|
14723
|
+
* @returns A single-character decision code.
|
|
14363
14724
|
*/
|
|
14364
14725
|
const promptDecision = async (filePath, logger, rl) => {
|
|
14365
14726
|
logger.log(`File exists: ${filePath}\nChoose: [o]verwrite, [e]xample, [s]kip, [O]verwrite All, [E]xample All, [S]kip All`);
|
|
@@ -14374,129 +14735,148 @@ const promptDecision = async (filePath, logger, rl) => {
|
|
|
14374
14735
|
};
|
|
14375
14736
|
|
|
14376
14737
|
/**
|
|
14377
|
-
*
|
|
14378
|
-
*
|
|
14379
|
-
*
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
* Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
|
|
14383
|
-
* Supports collision detection, interactive prompts, and CI bypass.
|
|
14738
|
+
* Attach the init plugin default action.
|
|
14739
|
+
*
|
|
14740
|
+
* @param cli - The `init` command mount (with args/options attached).
|
|
14741
|
+
*
|
|
14742
|
+
* @internal
|
|
14384
14743
|
*/
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
14392
|
-
|
|
14393
|
-
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14397
|
-
|
|
14398
|
-
|
|
14399
|
-
|
|
14400
|
-
|
|
14401
|
-
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
|
|
14440
|
-
await copyTextFile(item.src, item.dest, subs);
|
|
14441
|
-
logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
|
|
14442
|
-
continue;
|
|
14443
|
-
}
|
|
14444
|
-
if (yes) {
|
|
14445
|
-
logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
|
|
14446
|
-
continue;
|
|
14447
|
-
}
|
|
14448
|
-
let decision = globalDecision;
|
|
14449
|
-
if (!decision) {
|
|
14450
|
-
const a = await promptDecision(item.dest, logger, rl);
|
|
14451
|
-
if (a === 'O') {
|
|
14452
|
-
globalDecision = 'overwrite';
|
|
14453
|
-
decision = 'overwrite';
|
|
14454
|
-
}
|
|
14455
|
-
else if (a === 'E') {
|
|
14456
|
-
globalDecision = 'example';
|
|
14457
|
-
decision = 'example';
|
|
14458
|
-
}
|
|
14459
|
-
else if (a === 'S') {
|
|
14460
|
-
globalDecision = 'skip';
|
|
14461
|
-
decision = 'skip';
|
|
14462
|
-
}
|
|
14463
|
-
else {
|
|
14464
|
-
decision =
|
|
14465
|
-
a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
|
|
14466
|
-
}
|
|
14744
|
+
function attachInitDefaultAction(cli) {
|
|
14745
|
+
cli.action(async (destArg, opts, thisCommand) => {
|
|
14746
|
+
// Inherit logger from merged root options (base).
|
|
14747
|
+
const bag = readMergedOptions(thisCommand);
|
|
14748
|
+
const logger = bag.logger;
|
|
14749
|
+
const destRel = typeof destArg === 'string' && destArg.length > 0 ? destArg : '.';
|
|
14750
|
+
const cwd = process.cwd();
|
|
14751
|
+
const destRoot = path.resolve(cwd, destRel);
|
|
14752
|
+
const formatInput = opts['configFormat'];
|
|
14753
|
+
const formatRaw = typeof formatInput === 'string' ? formatInput.toLowerCase() : 'json';
|
|
14754
|
+
const format = (['json', 'yaml', 'js', 'ts'].includes(formatRaw) ? formatRaw : 'json');
|
|
14755
|
+
const withLocal = Boolean(opts['withLocal']);
|
|
14756
|
+
// dynamic flag reserved for future template variants; present for UX compatibility
|
|
14757
|
+
void opts['dynamic'];
|
|
14758
|
+
// CLI name default: --cli-name | basename(dest) | 'mycli'
|
|
14759
|
+
const cliNameInput = opts['cliName'];
|
|
14760
|
+
const cliName = (typeof cliNameInput === 'string' && cliNameInput.length > 0
|
|
14761
|
+
? cliNameInput
|
|
14762
|
+
: path.basename(destRoot) || 'mycli') || 'mycli';
|
|
14763
|
+
// Precedence: --force > --yes > auto-detect(non-interactive => yes)
|
|
14764
|
+
const force = Boolean(opts['force']);
|
|
14765
|
+
const yes = Boolean(opts['yes']) || (!force && isNonInteractive());
|
|
14766
|
+
// Build copy plan
|
|
14767
|
+
const cfgCopies = planConfigCopies({ format, withLocal, destRoot });
|
|
14768
|
+
const cliCopies = planCliCopies({ cliName, destRoot });
|
|
14769
|
+
const copies = [...cfgCopies, ...cliCopies];
|
|
14770
|
+
// Interactive state
|
|
14771
|
+
let globalDecision;
|
|
14772
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
14773
|
+
try {
|
|
14774
|
+
for (const item of copies) {
|
|
14775
|
+
const exists = await fs.pathExists(item.dest);
|
|
14776
|
+
if (!exists) {
|
|
14777
|
+
const subs = item.subs ?? {};
|
|
14778
|
+
await copyTextFile(item.src, item.dest, subs);
|
|
14779
|
+
logger.log(`Created ${path.relative(cwd, item.dest)}`);
|
|
14780
|
+
continue;
|
|
14781
|
+
}
|
|
14782
|
+
// Collision
|
|
14783
|
+
if (force) {
|
|
14784
|
+
const subs = item.subs ?? {};
|
|
14785
|
+
await copyTextFile(item.src, item.dest, subs);
|
|
14786
|
+
logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
|
|
14787
|
+
continue;
|
|
14788
|
+
}
|
|
14789
|
+
if (yes) {
|
|
14790
|
+
logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
|
|
14791
|
+
continue;
|
|
14792
|
+
}
|
|
14793
|
+
let decision = globalDecision;
|
|
14794
|
+
if (!decision) {
|
|
14795
|
+
const a = await promptDecision(item.dest, logger, rl);
|
|
14796
|
+
if (a === 'O') {
|
|
14797
|
+
globalDecision = 'overwrite';
|
|
14798
|
+
decision = 'overwrite';
|
|
14467
14799
|
}
|
|
14468
|
-
if (
|
|
14469
|
-
|
|
14470
|
-
|
|
14471
|
-
logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
|
|
14800
|
+
else if (a === 'E') {
|
|
14801
|
+
globalDecision = 'example';
|
|
14802
|
+
decision = 'example';
|
|
14472
14803
|
}
|
|
14473
|
-
else if (
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
await copyTextFile(item.src, destEx, subs);
|
|
14477
|
-
logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
|
|
14804
|
+
else if (a === 'S') {
|
|
14805
|
+
globalDecision = 'skip';
|
|
14806
|
+
decision = 'skip';
|
|
14478
14807
|
}
|
|
14479
14808
|
else {
|
|
14480
|
-
|
|
14809
|
+
decision = a === 'o' ? 'overwrite' : a === 'e' ? 'example' : 'skip';
|
|
14481
14810
|
}
|
|
14482
14811
|
}
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
'*.local',
|
|
14488
|
-
]);
|
|
14489
|
-
if (created) {
|
|
14490
|
-
logger.log(`Created ${path.relative(cwd, giPath)}`);
|
|
14812
|
+
if (decision === 'overwrite') {
|
|
14813
|
+
const subs = item.subs ?? {};
|
|
14814
|
+
await copyTextFile(item.src, item.dest, subs);
|
|
14815
|
+
logger.log(`Overwrote ${path.relative(cwd, item.dest)}`);
|
|
14491
14816
|
}
|
|
14492
|
-
else if (
|
|
14493
|
-
|
|
14817
|
+
else if (decision === 'example') {
|
|
14818
|
+
const destEx = `${item.dest}.example`;
|
|
14819
|
+
const subs = item.subs ?? {};
|
|
14820
|
+
await copyTextFile(item.src, destEx, subs);
|
|
14821
|
+
logger.log(`Wrote example ${path.relative(cwd, destEx)}`);
|
|
14822
|
+
}
|
|
14823
|
+
else {
|
|
14824
|
+
logger.log(`Skipped ${path.relative(cwd, item.dest)}`);
|
|
14494
14825
|
}
|
|
14495
14826
|
}
|
|
14496
|
-
|
|
14497
|
-
|
|
14827
|
+
// Ensure .gitignore includes local config patterns.
|
|
14828
|
+
const giPath = path.join(destRoot, '.gitignore');
|
|
14829
|
+
const { created, changed } = await ensureLines(giPath, [
|
|
14830
|
+
'getdotenv.config.local.*',
|
|
14831
|
+
'*.local',
|
|
14832
|
+
]);
|
|
14833
|
+
if (created) {
|
|
14834
|
+
logger.log(`Created ${path.relative(cwd, giPath)}`);
|
|
14498
14835
|
}
|
|
14499
|
-
|
|
14836
|
+
else if (changed) {
|
|
14837
|
+
logger.log(`Updated ${path.relative(cwd, giPath)}`);
|
|
14838
|
+
}
|
|
14839
|
+
}
|
|
14840
|
+
finally {
|
|
14841
|
+
rl.close();
|
|
14842
|
+
}
|
|
14843
|
+
});
|
|
14844
|
+
}
|
|
14845
|
+
|
|
14846
|
+
/**
|
|
14847
|
+
* Attach options/arguments for the init plugin mount.
|
|
14848
|
+
*
|
|
14849
|
+
* @param cli - The `init` command mount.
|
|
14850
|
+
*
|
|
14851
|
+
* @internal
|
|
14852
|
+
*/
|
|
14853
|
+
function attachInitOptions(cli) {
|
|
14854
|
+
return (cli
|
|
14855
|
+
// Description is owned by the plugin index (src/plugins/init/index.ts).
|
|
14856
|
+
.argument('[dest]', 'destination path (default: ./)', '.')
|
|
14857
|
+
.option('--config-format <format>', 'config format: json|yaml|js|ts', 'json')
|
|
14858
|
+
.option('--with-local', 'include .local config variant')
|
|
14859
|
+
.option('--dynamic', 'include dynamic examples (JS/TS configs)')
|
|
14860
|
+
.option('--cli-name <string>', 'CLI name for skeleton and tokens')
|
|
14861
|
+
.option('--force', 'overwrite all existing files')
|
|
14862
|
+
.option('--yes', 'skip all collisions (no overwrite)'));
|
|
14863
|
+
}
|
|
14864
|
+
|
|
14865
|
+
/**
|
|
14866
|
+
* @packageDocumentation
|
|
14867
|
+
* Init plugin subpath. Scaffolds get‑dotenv configuration files and a simple
|
|
14868
|
+
* host‑based CLI skeleton with collision handling and CI‑safe defaults.
|
|
14869
|
+
*/
|
|
14870
|
+
/**
|
|
14871
|
+
* Init plugin: scaffolds configuration files and a CLI skeleton for get-dotenv.
|
|
14872
|
+
* Supports collision detection, interactive prompts, and CI bypass.
|
|
14873
|
+
*/
|
|
14874
|
+
const initPlugin = () => definePlugin({
|
|
14875
|
+
ns: 'init',
|
|
14876
|
+
setup(cli) {
|
|
14877
|
+
cli.description('Scaffold getdotenv config files and a host-based CLI skeleton.');
|
|
14878
|
+
const initCmd = attachInitOptions(cli);
|
|
14879
|
+
attachInitDefaultAction(initCmd);
|
|
14500
14880
|
return undefined;
|
|
14501
14881
|
},
|
|
14502
14882
|
});
|