@karmaniverous/get-dotenv 5.2.6 → 6.0.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -67
- package/dist/cliHost.cjs +474 -271
- package/dist/cliHost.d.cts +45 -3
- package/dist/cliHost.d.mts +45 -3
- package/dist/cliHost.d.ts +45 -3
- package/dist/cliHost.mjs +474 -271
- package/dist/getdotenv.cli.mjs +713 -499
- package/dist/index.cjs +629 -692
- package/dist/index.d.cts +1 -140
- package/dist/index.d.mts +1 -140
- package/dist/index.d.ts +1 -140
- package/dist/index.mjs +630 -692
- package/dist/plugins-aws.cjs +0 -1
- package/dist/plugins-aws.mjs +0 -1
- package/dist/plugins-batch.cjs +53 -11
- package/dist/plugins-batch.d.cts +2 -1
- package/dist/plugins-batch.d.mts +2 -1
- package/dist/plugins-batch.d.ts +2 -1
- package/dist/plugins-batch.mjs +53 -11
- package/dist/plugins-cmd.cjs +3 -4
- package/dist/plugins-cmd.mjs +3 -4
- package/dist/plugins-demo.cjs +52 -7
- package/dist/plugins-demo.mjs +52 -7
- package/dist/plugins.cjs +66 -22
- package/dist/plugins.d.cts +2 -1
- package/dist/plugins.d.mts +2 -1
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.mjs +66 -22
- package/package.json +4 -2
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Option, Command } from 'commander';
|
|
2
1
|
import fs from 'fs-extra';
|
|
3
2
|
import { packageDirectory } from 'package-directory';
|
|
4
3
|
import path, { join, extname } from 'path';
|
|
5
4
|
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
6
5
|
import YAML from 'yaml';
|
|
7
6
|
import { z } from 'zod';
|
|
7
|
+
import { Option, Command } from 'commander';
|
|
8
8
|
import { nanoid } from 'nanoid';
|
|
9
9
|
import { parse } from 'dotenv';
|
|
10
10
|
import { createHash } from 'crypto';
|
|
@@ -13,258 +13,6 @@ import { globby } from 'globby';
|
|
|
13
13
|
import { stdin, stdout } from 'node:process';
|
|
14
14
|
import { createInterface } from 'readline/promises';
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Dotenv expansion utilities.
|
|
18
|
-
*
|
|
19
|
-
* This module implements recursive expansion of environment-variable
|
|
20
|
-
* references in strings and records. It supports both whitespace and
|
|
21
|
-
* bracket syntaxes with optional defaults:
|
|
22
|
-
*
|
|
23
|
-
* - Whitespace: `$VAR[:default]`
|
|
24
|
-
* - Bracketed: `${VAR[:default]}`
|
|
25
|
-
*
|
|
26
|
-
* Escaped dollar signs (`\$`) are preserved.
|
|
27
|
-
* Unknown variables resolve to empty string unless a default is provided.
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* Like String.prototype.search but returns the last index.
|
|
31
|
-
* @internal
|
|
32
|
-
*/
|
|
33
|
-
const searchLast = (str, rgx) => {
|
|
34
|
-
const matches = Array.from(str.matchAll(rgx));
|
|
35
|
-
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
36
|
-
};
|
|
37
|
-
const replaceMatch = (value, match, ref) => {
|
|
38
|
-
/**
|
|
39
|
-
* @internal
|
|
40
|
-
*/
|
|
41
|
-
const group = match[0];
|
|
42
|
-
const key = match[1];
|
|
43
|
-
const defaultValue = match[2];
|
|
44
|
-
if (!key)
|
|
45
|
-
return value;
|
|
46
|
-
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
47
|
-
return interpolate(replacement, ref);
|
|
48
|
-
};
|
|
49
|
-
const interpolate = (value = '', ref = {}) => {
|
|
50
|
-
/**
|
|
51
|
-
* @internal
|
|
52
|
-
*/
|
|
53
|
-
// if value is falsy, return it as is
|
|
54
|
-
if (!value)
|
|
55
|
-
return value;
|
|
56
|
-
// get position of last unescaped dollar sign
|
|
57
|
-
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
58
|
-
// return value if none found
|
|
59
|
-
if (lastUnescapedDollarSignIndex === -1)
|
|
60
|
-
return value;
|
|
61
|
-
// evaluate the value tail
|
|
62
|
-
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
63
|
-
// find whitespace pattern: $KEY:DEFAULT
|
|
64
|
-
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
65
|
-
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
66
|
-
if (whitespaceMatch != null)
|
|
67
|
-
return replaceMatch(value, whitespaceMatch, ref);
|
|
68
|
-
else {
|
|
69
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
70
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
71
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
72
|
-
if (bracketMatch != null)
|
|
73
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
74
|
-
}
|
|
75
|
-
return value;
|
|
76
|
-
};
|
|
77
|
-
/**
|
|
78
|
-
* Recursively expands environment variables in a string. Variables may be
|
|
79
|
-
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
80
|
-
* Unknown variables will expand to an empty string.
|
|
81
|
-
*
|
|
82
|
-
* @param value - The string to expand.
|
|
83
|
-
* @param ref - The reference object to use for variable expansion.
|
|
84
|
-
* @returns The expanded string.
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```ts
|
|
88
|
-
* process.env.FOO = 'bar';
|
|
89
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
90
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
91
|
-
* ```
|
|
92
|
-
*
|
|
93
|
-
* @remarks
|
|
94
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
95
|
-
* references, those will also be expanded until a stable value is reached.
|
|
96
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
97
|
-
*/
|
|
98
|
-
const dotenvExpand = (value, ref = process.env) => {
|
|
99
|
-
const result = interpolate(value, ref);
|
|
100
|
-
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
101
|
-
};
|
|
102
|
-
/**
|
|
103
|
-
* Recursively expands environment variables in the values of a JSON object.
|
|
104
|
-
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
105
|
-
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
106
|
-
*
|
|
107
|
-
* @param values - The values object to expand.
|
|
108
|
-
* @param options - Expansion options.
|
|
109
|
-
* @returns The value object with expanded string values.
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```ts
|
|
113
|
-
* process.env.FOO = 'bar';
|
|
114
|
-
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
115
|
-
* // => { A: "bar", B: "xbary" }
|
|
116
|
-
* ```
|
|
117
|
-
*
|
|
118
|
-
* @remarks
|
|
119
|
-
* Options:
|
|
120
|
-
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
121
|
-
* - progressive: Whether to progressively add expanded values to the set of
|
|
122
|
-
* reference keys.
|
|
123
|
-
*
|
|
124
|
-
* When `progressive` is true, each expanded key becomes available for
|
|
125
|
-
* subsequent expansions in the same object (left-to-right by object key order).
|
|
126
|
-
*/
|
|
127
|
-
const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
|
|
128
|
-
const { ref = process.env, progressive = false } = options;
|
|
129
|
-
acc[key] = dotenvExpand(values[key], {
|
|
130
|
-
...ref,
|
|
131
|
-
...(progressive ? acc : {}),
|
|
132
|
-
});
|
|
133
|
-
return acc;
|
|
134
|
-
}, {});
|
|
135
|
-
/**
|
|
136
|
-
* Recursively expands environment variables in a string using `process.env` as
|
|
137
|
-
* the expansion reference. Variables may be presented with optional default as
|
|
138
|
-
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
139
|
-
* empty string.
|
|
140
|
-
*
|
|
141
|
-
* @param value - The string to expand.
|
|
142
|
-
* @returns The expanded string.
|
|
143
|
-
*
|
|
144
|
-
* @example
|
|
145
|
-
* ```ts
|
|
146
|
-
* process.env.FOO = 'bar';
|
|
147
|
-
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
148
|
-
* ```
|
|
149
|
-
*/
|
|
150
|
-
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Attach legacy root flags to a Commander program.
|
|
154
|
-
* Uses provided defaults to render help labels without coupling to generators.
|
|
155
|
-
*/
|
|
156
|
-
const attachRootOptions = (program, defaults, opts) => {
|
|
157
|
-
// Install temporary wrappers to tag all options added here as "base".
|
|
158
|
-
const GROUP = 'base';
|
|
159
|
-
const tagLatest = (cmd, group) => {
|
|
160
|
-
const optsArr = cmd.options;
|
|
161
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
162
|
-
const last = optsArr[optsArr.length - 1];
|
|
163
|
-
last.__group = group;
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
const originalAddOption = program.addOption.bind(program);
|
|
167
|
-
const originalOption = program.option.bind(program);
|
|
168
|
-
program.addOption = function patchedAdd(opt) {
|
|
169
|
-
// Tag before adding, in case consumers inspect the Option directly.
|
|
170
|
-
opt.__group = GROUP;
|
|
171
|
-
const ret = originalAddOption(opt);
|
|
172
|
-
return ret;
|
|
173
|
-
};
|
|
174
|
-
program.option = function patchedOption(...args) {
|
|
175
|
-
const ret = originalOption(...args);
|
|
176
|
-
tagLatest(this, GROUP);
|
|
177
|
-
return ret;
|
|
178
|
-
};
|
|
179
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
180
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
181
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
182
|
-
// Build initial chain.
|
|
183
|
-
let p = program
|
|
184
|
-
.enablePositionalOptions()
|
|
185
|
-
.passThroughOptions()
|
|
186
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
187
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
188
|
-
['KEY1', 'VAL1'],
|
|
189
|
-
['KEY2', 'VAL2'],
|
|
190
|
-
]
|
|
191
|
-
.map((v) => v.join(va))
|
|
192
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
193
|
-
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
194
|
-
// Default is OFF; the generator opts in explicitly.
|
|
195
|
-
if (opts?.includeCommandOption === true) {
|
|
196
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
197
|
-
}
|
|
198
|
-
p = p
|
|
199
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
200
|
-
.addOption(new Option('-s, --shell [string]', (() => {
|
|
201
|
-
let defaultLabel = '';
|
|
202
|
-
if (shell !== undefined) {
|
|
203
|
-
if (typeof shell === 'boolean') {
|
|
204
|
-
defaultLabel = ' (default OS shell)';
|
|
205
|
-
}
|
|
206
|
-
else if (typeof shell === 'string') {
|
|
207
|
-
// Safe string interpolation
|
|
208
|
-
defaultLabel = ` (default ${shell})`;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
212
|
-
})()).conflicts('shellOff'))
|
|
213
|
-
.addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
214
|
-
.addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
215
|
-
.addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
216
|
-
.addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
217
|
-
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
218
|
-
? ' (default)'
|
|
219
|
-
: ''}`).conflicts('excludeAllOff'))
|
|
220
|
-
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
221
|
-
.addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
222
|
-
.addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
223
|
-
.addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
224
|
-
.addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
225
|
-
.addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
226
|
-
.addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
227
|
-
.addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
228
|
-
.addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
229
|
-
.addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
230
|
-
.addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
231
|
-
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
232
|
-
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
233
|
-
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
234
|
-
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
235
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
236
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
237
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
238
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
239
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
240
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
241
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
242
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
243
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
244
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
245
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
246
|
-
// Hidden scripts pipe-through (stringified)
|
|
247
|
-
.addOption(new Option('--scripts <string>')
|
|
248
|
-
.default(JSON.stringify(scripts))
|
|
249
|
-
.hideHelp());
|
|
250
|
-
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
251
|
-
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
252
|
-
// Validation: strict mode fails on env validation issues (warn by default).
|
|
253
|
-
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
254
|
-
// Entropy diagnostics (presentation-only)
|
|
255
|
-
p = p
|
|
256
|
-
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
257
|
-
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
258
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
259
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
260
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
261
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
262
|
-
// Restore original methods to avoid tagging future additions outside base.
|
|
263
|
-
program.addOption = originalAddOption;
|
|
264
|
-
program.option = originalOption;
|
|
265
|
-
return p;
|
|
266
|
-
};
|
|
267
|
-
|
|
268
16
|
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
269
17
|
const baseRootOptionDefaults = {
|
|
270
18
|
dotenvToken: '.env',
|
|
@@ -841,9 +589,21 @@ const resolveGetDotenvOptions = async (customOptions) => {
|
|
|
841
589
|
const localOptionsPath = localPkgDir
|
|
842
590
|
? join(localPkgDir, getDotenvOptionsFilename)
|
|
843
591
|
: undefined;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
592
|
+
// Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
|
|
593
|
+
let localOptions = {};
|
|
594
|
+
if (localOptionsPath && (await fs.exists(localOptionsPath))) {
|
|
595
|
+
try {
|
|
596
|
+
const txt = await fs.readFile(localOptionsPath, 'utf-8');
|
|
597
|
+
const parsed = JSON.parse(txt);
|
|
598
|
+
if (parsed && typeof parsed === 'object') {
|
|
599
|
+
localOptions = parsed;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// Malformed or unreadable local options are treated as absent.
|
|
604
|
+
localOptions = {};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
847
607
|
// Merge order: base < local < custom (custom has highest precedence)
|
|
848
608
|
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
849
609
|
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
@@ -854,6 +614,316 @@ const resolveGetDotenvOptions = async (customOptions) => {
|
|
|
854
614
|
};
|
|
855
615
|
};
|
|
856
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Dotenv expansion utilities.
|
|
619
|
+
*
|
|
620
|
+
* This module implements recursive expansion of environment-variable
|
|
621
|
+
* references in strings and records. It supports both whitespace and
|
|
622
|
+
* bracket syntaxes with optional defaults:
|
|
623
|
+
*
|
|
624
|
+
* - Whitespace: `$VAR[:default]`
|
|
625
|
+
* - Bracketed: `${VAR[:default]}`
|
|
626
|
+
*
|
|
627
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
628
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
629
|
+
*/
|
|
630
|
+
/**
|
|
631
|
+
* Like String.prototype.search but returns the last index.
|
|
632
|
+
* @internal
|
|
633
|
+
*/
|
|
634
|
+
const searchLast = (str, rgx) => {
|
|
635
|
+
const matches = Array.from(str.matchAll(rgx));
|
|
636
|
+
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
637
|
+
};
|
|
638
|
+
const replaceMatch = (value, match, ref) => {
|
|
639
|
+
/**
|
|
640
|
+
* @internal
|
|
641
|
+
*/
|
|
642
|
+
const group = match[0];
|
|
643
|
+
const key = match[1];
|
|
644
|
+
const defaultValue = match[2];
|
|
645
|
+
if (!key)
|
|
646
|
+
return value;
|
|
647
|
+
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
648
|
+
return interpolate(replacement, ref);
|
|
649
|
+
};
|
|
650
|
+
const interpolate = (value = '', ref = {}) => {
|
|
651
|
+
/**
|
|
652
|
+
* @internal
|
|
653
|
+
*/
|
|
654
|
+
// if value is falsy, return it as is
|
|
655
|
+
if (!value)
|
|
656
|
+
return value;
|
|
657
|
+
// get position of last unescaped dollar sign
|
|
658
|
+
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
659
|
+
// return value if none found
|
|
660
|
+
if (lastUnescapedDollarSignIndex === -1)
|
|
661
|
+
return value;
|
|
662
|
+
// evaluate the value tail
|
|
663
|
+
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
664
|
+
// find whitespace pattern: $KEY:DEFAULT
|
|
665
|
+
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
666
|
+
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
667
|
+
if (whitespaceMatch != null)
|
|
668
|
+
return replaceMatch(value, whitespaceMatch, ref);
|
|
669
|
+
else {
|
|
670
|
+
// find bracket pattern: ${KEY:DEFAULT}
|
|
671
|
+
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
672
|
+
const bracketMatch = bracketPattern.exec(tail);
|
|
673
|
+
if (bracketMatch != null)
|
|
674
|
+
return replaceMatch(value, bracketMatch, ref);
|
|
675
|
+
}
|
|
676
|
+
return value;
|
|
677
|
+
};
|
|
678
|
+
/**
|
|
679
|
+
* Recursively expands environment variables in a string. Variables may be
|
|
680
|
+
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
681
|
+
* Unknown variables will expand to an empty string.
|
|
682
|
+
*
|
|
683
|
+
* @param value - The string to expand.
|
|
684
|
+
* @param ref - The reference object to use for variable expansion.
|
|
685
|
+
* @returns The expanded string.
|
|
686
|
+
*
|
|
687
|
+
* @example
|
|
688
|
+
* ```ts
|
|
689
|
+
* process.env.FOO = 'bar';
|
|
690
|
+
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
691
|
+
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
692
|
+
* ```
|
|
693
|
+
*
|
|
694
|
+
* @remarks
|
|
695
|
+
* The expansion is recursive. If a referenced variable itself contains
|
|
696
|
+
* references, those will also be expanded until a stable value is reached.
|
|
697
|
+
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
698
|
+
*/
|
|
699
|
+
const dotenvExpand = (value, ref = process.env) => {
|
|
700
|
+
const result = interpolate(value, ref);
|
|
701
|
+
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
702
|
+
};
|
|
703
|
+
/**
|
|
704
|
+
* Recursively expands environment variables in the values of a JSON object.
|
|
705
|
+
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
706
|
+
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
707
|
+
*
|
|
708
|
+
* @param values - The values object to expand.
|
|
709
|
+
* @param options - Expansion options.
|
|
710
|
+
* @returns The value object with expanded string values.
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* ```ts
|
|
714
|
+
* process.env.FOO = 'bar';
|
|
715
|
+
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
716
|
+
* // => { A: "bar", B: "xbary" }
|
|
717
|
+
* ```
|
|
718
|
+
*
|
|
719
|
+
* @remarks
|
|
720
|
+
* Options:
|
|
721
|
+
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
722
|
+
* - progressive: Whether to progressively add expanded values to the set of
|
|
723
|
+
* reference keys.
|
|
724
|
+
*
|
|
725
|
+
* When `progressive` is true, each expanded key becomes available for
|
|
726
|
+
* subsequent expansions in the same object (left-to-right by object key order).
|
|
727
|
+
*/
|
|
728
|
+
const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
|
|
729
|
+
const { ref = process.env, progressive = false } = options;
|
|
730
|
+
acc[key] = dotenvExpand(values[key], {
|
|
731
|
+
...ref,
|
|
732
|
+
...(progressive ? acc : {}),
|
|
733
|
+
});
|
|
734
|
+
return acc;
|
|
735
|
+
}, {});
|
|
736
|
+
/**
|
|
737
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
738
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
739
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
740
|
+
* empty string.
|
|
741
|
+
*
|
|
742
|
+
* @param value - The string to expand.
|
|
743
|
+
* @returns The expanded string.
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* ```ts
|
|
747
|
+
* process.env.FOO = 'bar';
|
|
748
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
752
|
+
|
|
753
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
754
|
+
/**
|
|
755
|
+
* Attach root flags to a GetDotenvCli instance.
|
|
756
|
+
* - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
|
|
757
|
+
* - Any flag that displays an effective default in help uses dynamic descriptions.
|
|
758
|
+
*/
|
|
759
|
+
const attachRootOptions = (program, defaults, opts) => {
|
|
760
|
+
// Install temporary wrappers to tag all options added here as "base" for grouped help.
|
|
761
|
+
const GROUP = 'base';
|
|
762
|
+
const tagLatest = (cmd, group) => {
|
|
763
|
+
const optsArr = cmd.options;
|
|
764
|
+
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
765
|
+
const last = optsArr[optsArr.length - 1];
|
|
766
|
+
last.__group = group;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
const originalAddOption = program.addOption.bind(program);
|
|
770
|
+
const originalOption = program.option.bind(program);
|
|
771
|
+
program.addOption = function patchedAdd(opt) {
|
|
772
|
+
opt.__group = GROUP;
|
|
773
|
+
return originalAddOption(opt);
|
|
774
|
+
};
|
|
775
|
+
program.option = function patchedOption(...args) {
|
|
776
|
+
const ret = originalOption(...args);
|
|
777
|
+
tagLatest(this, GROUP);
|
|
778
|
+
return ret;
|
|
779
|
+
};
|
|
780
|
+
const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
781
|
+
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
782
|
+
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
783
|
+
// Helper: append (default) tags for ON/OFF toggles
|
|
784
|
+
const onOff = (on, isDefault) => on
|
|
785
|
+
? `ON${isDefault ? ' (default)' : ''}`
|
|
786
|
+
: `OFF${isDefault ? ' (default)' : ''}`;
|
|
787
|
+
let p = program
|
|
788
|
+
.enablePositionalOptions()
|
|
789
|
+
.passThroughOptions()
|
|
790
|
+
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
791
|
+
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
792
|
+
['KEY1', 'VAL1'],
|
|
793
|
+
['KEY2', 'VAL2'],
|
|
794
|
+
]
|
|
795
|
+
.map((v) => v.join(va))
|
|
796
|
+
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
797
|
+
if (opts?.includeCommandOption === true) {
|
|
798
|
+
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
799
|
+
}
|
|
800
|
+
// Output path (interpolated later; help can remain static)
|
|
801
|
+
p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
|
|
802
|
+
// Shell ON (string or boolean true => default shell)
|
|
803
|
+
p = p
|
|
804
|
+
.addOption(program
|
|
805
|
+
.createDynamicOption('-s, --shell [string]', (cfg) => {
|
|
806
|
+
const s = cfg.shell;
|
|
807
|
+
let tag = '';
|
|
808
|
+
if (typeof s === 'boolean' && s)
|
|
809
|
+
tag = ' (default OS shell)';
|
|
810
|
+
else if (typeof s === 'string' && s.length > 0)
|
|
811
|
+
tag = ` (default ${s})`;
|
|
812
|
+
return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
|
|
813
|
+
})
|
|
814
|
+
.conflicts('shellOff'))
|
|
815
|
+
// Shell OFF
|
|
816
|
+
.addOption(program
|
|
817
|
+
.createDynamicOption('-S, --shell-off', (cfg) => {
|
|
818
|
+
const s = cfg.shell;
|
|
819
|
+
return `command execution shell OFF${s === false ? ' (default)' : ''}`;
|
|
820
|
+
})
|
|
821
|
+
.conflicts('shell'));
|
|
822
|
+
// Load process ON/OFF (dynamic defaults)
|
|
823
|
+
p = p
|
|
824
|
+
.addOption(program
|
|
825
|
+
.createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
|
|
826
|
+
.conflicts('loadProcessOff'))
|
|
827
|
+
.addOption(program
|
|
828
|
+
.createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
|
|
829
|
+
.conflicts('loadProcess'));
|
|
830
|
+
// Exclusion master toggle (dynamic)
|
|
831
|
+
p = p
|
|
832
|
+
.addOption(program
|
|
833
|
+
.createDynamicOption('-a, --exclude-all', (cfg) => {
|
|
834
|
+
const c = cfg;
|
|
835
|
+
const allOn = !!c.excludeDynamic &&
|
|
836
|
+
((!!c.excludeEnv && !!c.excludeGlobal) ||
|
|
837
|
+
(!!c.excludePrivate && !!c.excludePublic));
|
|
838
|
+
const suffix = allOn ? ' (default)' : '';
|
|
839
|
+
return `exclude all dotenv variables from loading ON${suffix}`;
|
|
840
|
+
})
|
|
841
|
+
.conflicts('excludeAllOff'))
|
|
842
|
+
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
|
|
843
|
+
// Per-family exclusions (dynamic defaults)
|
|
844
|
+
p = p
|
|
845
|
+
.addOption(program
|
|
846
|
+
.createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
|
|
847
|
+
.conflicts('excludeDynamicOff'))
|
|
848
|
+
.addOption(program
|
|
849
|
+
.createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
|
|
850
|
+
.conflicts('excludeDynamic'))
|
|
851
|
+
.addOption(program
|
|
852
|
+
.createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
|
|
853
|
+
.conflicts('excludeEnvOff'))
|
|
854
|
+
.addOption(program
|
|
855
|
+
.createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
|
|
856
|
+
.conflicts('excludeEnv'))
|
|
857
|
+
.addOption(program
|
|
858
|
+
.createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
|
|
859
|
+
.conflicts('excludeGlobalOff'))
|
|
860
|
+
.addOption(program
|
|
861
|
+
.createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
|
|
862
|
+
.conflicts('excludeGlobal'))
|
|
863
|
+
.addOption(program
|
|
864
|
+
.createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
|
|
865
|
+
.conflicts('excludePrivateOff'))
|
|
866
|
+
.addOption(program
|
|
867
|
+
.createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
|
|
868
|
+
.conflicts('excludePrivate'))
|
|
869
|
+
.addOption(program
|
|
870
|
+
.createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
|
|
871
|
+
.conflicts('excludePublicOff'))
|
|
872
|
+
.addOption(program
|
|
873
|
+
.createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
|
|
874
|
+
.conflicts('excludePublic'));
|
|
875
|
+
// Log ON/OFF (dynamic)
|
|
876
|
+
p = p
|
|
877
|
+
.addOption(program
|
|
878
|
+
.createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
|
|
879
|
+
.conflicts('logOff'))
|
|
880
|
+
.addOption(program
|
|
881
|
+
.createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
|
|
882
|
+
.conflicts('log'));
|
|
883
|
+
// Capture flag (no default display; static)
|
|
884
|
+
p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
|
|
885
|
+
// Core bootstrap/static flags (kept static in help)
|
|
886
|
+
p = p
|
|
887
|
+
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
888
|
+
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
889
|
+
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
890
|
+
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
891
|
+
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
892
|
+
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
893
|
+
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
894
|
+
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
895
|
+
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
896
|
+
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
897
|
+
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
898
|
+
// Hidden scripts pipe-through (stringified)
|
|
899
|
+
.addOption(new Option('--scripts <string>')
|
|
900
|
+
.default(JSON.stringify(scripts))
|
|
901
|
+
.hideHelp());
|
|
902
|
+
// Diagnostics / validation / entropy
|
|
903
|
+
p = p
|
|
904
|
+
.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
|
|
905
|
+
.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
906
|
+
p = p
|
|
907
|
+
.addOption(program
|
|
908
|
+
.createDynamicOption('--entropy-warn', (cfg) => {
|
|
909
|
+
const warn = cfg.warnEntropy;
|
|
910
|
+
// Default is effectively ON when warnEntropy is true or undefined.
|
|
911
|
+
return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
|
|
912
|
+
})
|
|
913
|
+
.conflicts('entropyWarnOff'))
|
|
914
|
+
.addOption(program
|
|
915
|
+
.createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
|
|
916
|
+
.conflicts('entropyWarn'))
|
|
917
|
+
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
918
|
+
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
919
|
+
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
920
|
+
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
921
|
+
// Restore original methods
|
|
922
|
+
program.addOption = originalAddOption;
|
|
923
|
+
program.option = originalOption;
|
|
924
|
+
return p;
|
|
925
|
+
};
|
|
926
|
+
|
|
857
927
|
/**
|
|
858
928
|
* Zod schemas for programmatic GetDotenv options.
|
|
859
929
|
*
|
|
@@ -1487,6 +1557,8 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1487
1557
|
};
|
|
1488
1558
|
};
|
|
1489
1559
|
|
|
1560
|
+
// Dynamic help support: attach a private symbol to Option for description fns.
|
|
1561
|
+
const DYN_DESC_SYM = Symbol('getdotenv.dynamic.description');
|
|
1490
1562
|
const HOST_META_URL = import.meta.url;
|
|
1491
1563
|
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
1492
1564
|
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
@@ -1509,6 +1581,13 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1509
1581
|
_installed = false;
|
|
1510
1582
|
/** Optional header line to prepend in help output */
|
|
1511
1583
|
[HELP_HEADER_SYMBOL];
|
|
1584
|
+
/**
|
|
1585
|
+
* Create a subcommand using the same subclass, preserving helpers like
|
|
1586
|
+
* dynamicOption on children.
|
|
1587
|
+
*/
|
|
1588
|
+
createCommand(name) {
|
|
1589
|
+
return new this.constructor(name);
|
|
1590
|
+
}
|
|
1512
1591
|
constructor(alias = 'getdotenv') {
|
|
1513
1592
|
super(alias);
|
|
1514
1593
|
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
@@ -1516,15 +1595,18 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1516
1595
|
// child uses passThroughOptions.
|
|
1517
1596
|
this.enablePositionalOptions();
|
|
1518
1597
|
// Configure grouped help: show only base options in default "Options";
|
|
1519
|
-
//
|
|
1598
|
+
// we will insert App/Plugin sections before Commands in helpInformation().
|
|
1520
1599
|
this.configureHelp({
|
|
1521
1600
|
visibleOptions: (cmd) => {
|
|
1522
|
-
const all = cmd.options ??
|
|
1523
|
-
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1601
|
+
const all = cmd.options ?? [];
|
|
1602
|
+
const parent = cmd.parent ?? null;
|
|
1603
|
+
const isRoot = parent === null;
|
|
1604
|
+
const list = isRoot
|
|
1605
|
+
? all.filter((opt) => {
|
|
1606
|
+
const group = opt.__group;
|
|
1607
|
+
return group === 'base';
|
|
1608
|
+
})
|
|
1609
|
+
: all.slice(); // subcommands: show all options (their own "Options:" block)
|
|
1528
1610
|
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1529
1611
|
const hasShort = (opt) => {
|
|
1530
1612
|
const flags = opt.flags ?? '';
|
|
@@ -1532,19 +1614,18 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1532
1614
|
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1533
1615
|
};
|
|
1534
1616
|
const byFlags = (opt) => opt.flags ?? '';
|
|
1535
|
-
|
|
1617
|
+
list.sort((a, b) => {
|
|
1536
1618
|
const aS = hasShort(a) ? 1 : 0;
|
|
1537
1619
|
const bS = hasShort(b) ? 1 : 0;
|
|
1538
1620
|
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1539
1621
|
});
|
|
1540
|
-
return
|
|
1622
|
+
return list;
|
|
1541
1623
|
},
|
|
1542
1624
|
});
|
|
1543
1625
|
this.addHelpText('beforeAll', () => {
|
|
1544
1626
|
const header = this[HELP_HEADER_SYMBOL];
|
|
1545
1627
|
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1546
1628
|
});
|
|
1547
|
-
this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
|
|
1548
1629
|
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
1549
1630
|
// mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
|
|
1550
1631
|
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
@@ -1556,9 +1637,15 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1556
1637
|
});
|
|
1557
1638
|
}
|
|
1558
1639
|
/**
|
|
1559
|
-
* Resolve options (strict) and compute dotenv context.
|
|
1640
|
+
* Resolve options (strict) and compute dotenv context.
|
|
1641
|
+
* Stores the context on the instance under a symbol.
|
|
1642
|
+
*
|
|
1643
|
+
* Options:
|
|
1644
|
+
* - opts.runAfterResolve (default true): when false, skips running plugin
|
|
1645
|
+
* afterResolve hooks. Useful for top-level help rendering to avoid
|
|
1646
|
+
* long-running side-effects while still evaluating dynamic help text.
|
|
1560
1647
|
*/
|
|
1561
|
-
async resolveAndLoad(customOptions = {}) {
|
|
1648
|
+
async resolveAndLoad(customOptions = {}, opts) {
|
|
1562
1649
|
// Resolve defaults, then validate strictly under the new host.
|
|
1563
1650
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1564
1651
|
getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
@@ -1569,9 +1656,64 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1569
1656
|
ctx;
|
|
1570
1657
|
// Ensure plugins are installed exactly once, then run afterResolve.
|
|
1571
1658
|
await this.install();
|
|
1572
|
-
|
|
1659
|
+
if (opts?.runAfterResolve ?? true) {
|
|
1660
|
+
await this._runAfterResolve(ctx);
|
|
1661
|
+
}
|
|
1573
1662
|
return ctx;
|
|
1574
1663
|
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Create a Commander Option that computes its description at help time.
|
|
1666
|
+
* The returned Option may be configured (conflicts, default, parser) and
|
|
1667
|
+
* added via addOption().
|
|
1668
|
+
*/
|
|
1669
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
1670
|
+
createDynamicOption(flags, desc, parser, defaultValue) {
|
|
1671
|
+
const opt = new Option(flags, '');
|
|
1672
|
+
// Keep the function on a private symbol so it survives through Commander.
|
|
1673
|
+
opt[DYN_DESC_SYM] = desc;
|
|
1674
|
+
if (parser)
|
|
1675
|
+
opt.argParser(parser);
|
|
1676
|
+
if (defaultValue !== undefined)
|
|
1677
|
+
opt.default(defaultValue);
|
|
1678
|
+
return opt;
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Chainable helper mirroring .option(), but with a dynamic description.
|
|
1682
|
+
* Equivalent to addOption(createDynamicOption(...)).
|
|
1683
|
+
*/
|
|
1684
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
1685
|
+
dynamicOption(flags, desc, parser, defaultValue) {
|
|
1686
|
+
const opt = this.createDynamicOption(flags, desc, parser, defaultValue);
|
|
1687
|
+
this.addOption(opt);
|
|
1688
|
+
return this;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Evaluate dynamic descriptions for this command and all descendants using
|
|
1692
|
+
* the provided resolved configuration. Mutates the Option.description in
|
|
1693
|
+
* place so Commander help renders updated text.
|
|
1694
|
+
*/
|
|
1695
|
+
evaluateDynamicOptions(resolved) {
|
|
1696
|
+
const visit = (cmd) => {
|
|
1697
|
+
const arr = cmd.options ?? [];
|
|
1698
|
+
for (const o of arr) {
|
|
1699
|
+
const dyn = o[DYN_DESC_SYM];
|
|
1700
|
+
if (typeof dyn === 'function') {
|
|
1701
|
+
try {
|
|
1702
|
+
const txt = dyn(resolved);
|
|
1703
|
+
// Commander Option has a public "description" field used by help.
|
|
1704
|
+
o.description = txt;
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
// Best-effort: leave description as-is on evaluation failure.
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const children = cmd.commands ?? [];
|
|
1712
|
+
for (const c of children)
|
|
1713
|
+
visit(c);
|
|
1714
|
+
};
|
|
1715
|
+
visit(this);
|
|
1716
|
+
}
|
|
1575
1717
|
/**
|
|
1576
1718
|
* Retrieve the current invocation context (if any).
|
|
1577
1719
|
*/
|
|
@@ -1601,6 +1743,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1601
1743
|
tagAppOptions(fn) {
|
|
1602
1744
|
const root = this;
|
|
1603
1745
|
const originalAddOption = root.addOption.bind(root);
|
|
1746
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1604
1747
|
const originalOption = root.option.bind(root);
|
|
1605
1748
|
const tagLatest = (cmd, group) => {
|
|
1606
1749
|
const optsArr = cmd.options;
|
|
@@ -1613,6 +1756,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1613
1756
|
opt.__group = 'app';
|
|
1614
1757
|
return originalAddOption(opt);
|
|
1615
1758
|
};
|
|
1759
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1616
1760
|
root.option = function patchedOption(...args) {
|
|
1617
1761
|
const ret = originalOption(...args);
|
|
1618
1762
|
tagLatest(this, 'app');
|
|
@@ -1623,6 +1767,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1623
1767
|
}
|
|
1624
1768
|
finally {
|
|
1625
1769
|
root.addOption = originalAddOption;
|
|
1770
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1626
1771
|
root.option = originalOption;
|
|
1627
1772
|
}
|
|
1628
1773
|
}
|
|
@@ -1668,6 +1813,40 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1668
1813
|
}
|
|
1669
1814
|
return this;
|
|
1670
1815
|
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Insert grouped plugin/app options between "Options" and "Commands" for
|
|
1818
|
+
* hybrid ordering. Applies to root and any parent command.
|
|
1819
|
+
*/
|
|
1820
|
+
helpInformation() {
|
|
1821
|
+
// Base help text first (includes beforeAll/after hooks).
|
|
1822
|
+
const base = super.helpInformation();
|
|
1823
|
+
const groups = this.#renderOptionGroups(this);
|
|
1824
|
+
const block = typeof groups === 'string' ? groups.trim() : '';
|
|
1825
|
+
let out = base;
|
|
1826
|
+
if (!block) {
|
|
1827
|
+
// Ensure a trailing blank line even when no extra groups render.
|
|
1828
|
+
if (!out.endsWith('\n\n'))
|
|
1829
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1830
|
+
return out;
|
|
1831
|
+
}
|
|
1832
|
+
// Insert just before "Commands:" when present.
|
|
1833
|
+
const marker = '\nCommands:';
|
|
1834
|
+
const idx = base.indexOf(marker);
|
|
1835
|
+
if (idx >= 0) {
|
|
1836
|
+
const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
|
|
1837
|
+
out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
// Otherwise append.
|
|
1841
|
+
const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
|
|
1842
|
+
out = `${base}${sep}${groups}`;
|
|
1843
|
+
}
|
|
1844
|
+
// Ensure a trailing blank line for prompt separation.
|
|
1845
|
+
if (!out.endsWith('\n\n')) {
|
|
1846
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1847
|
+
}
|
|
1848
|
+
return out;
|
|
1849
|
+
}
|
|
1671
1850
|
/**
|
|
1672
1851
|
* Register a plugin for installation (parent level).
|
|
1673
1852
|
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
@@ -1706,7 +1885,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1706
1885
|
for (const p of this._plugins)
|
|
1707
1886
|
await run(p);
|
|
1708
1887
|
}
|
|
1709
|
-
// Render App/Plugin grouped options
|
|
1888
|
+
// Render App/Plugin grouped options (used by helpInformation override).
|
|
1710
1889
|
#renderOptionGroups(cmd) {
|
|
1711
1890
|
const all = cmd.options ?? [];
|
|
1712
1891
|
const byGroup = new Map();
|
|
@@ -1748,11 +1927,14 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1748
1927
|
}
|
|
1749
1928
|
// Plugin groups sorted by id
|
|
1750
1929
|
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1930
|
+
const currentName = cmd.name?.() ?? '';
|
|
1751
1931
|
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1752
1932
|
for (const k of pluginKeys) {
|
|
1753
1933
|
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1754
1934
|
const rows = byGroup.get(k) ?? [];
|
|
1755
|
-
|
|
1935
|
+
// Do not show a "Plugin options — <self>" section on the command that owns those options.
|
|
1936
|
+
// Only child-injected plugin groups should render at this level.
|
|
1937
|
+
if (rows.length > 0 && id !== currentName) {
|
|
1756
1938
|
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1757
1939
|
}
|
|
1758
1940
|
}
|
|
@@ -1823,6 +2005,17 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1823
2005
|
// Build service options and compute context (always-on loader path).
|
|
1824
2006
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1825
2007
|
await this.resolveAndLoad(serviceOptions);
|
|
2008
|
+
// Refresh dynamic option descriptions using resolved config + plugin slices
|
|
2009
|
+
try {
|
|
2010
|
+
const ctx = this.getCtx();
|
|
2011
|
+
this.evaluateDynamicOptions({
|
|
2012
|
+
...ctx?.optionsResolved,
|
|
2013
|
+
plugins: ctx?.pluginConfigs ?? {},
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
catch {
|
|
2017
|
+
/* best-effort */
|
|
2018
|
+
}
|
|
1826
2019
|
// Global validation: once after Phase C using config sources.
|
|
1827
2020
|
try {
|
|
1828
2021
|
const ctx = this.getCtx();
|
|
@@ -1857,6 +2050,16 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1857
2050
|
if (!this.getCtx()) {
|
|
1858
2051
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1859
2052
|
await this.resolveAndLoad(serviceOptions);
|
|
2053
|
+
try {
|
|
2054
|
+
const ctx = this.getCtx();
|
|
2055
|
+
this.evaluateDynamicOptions({
|
|
2056
|
+
...ctx?.optionsResolved,
|
|
2057
|
+
plugins: ctx?.pluginConfigs ?? {},
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
catch {
|
|
2061
|
+
/* tolerate */
|
|
2062
|
+
}
|
|
1860
2063
|
try {
|
|
1861
2064
|
const ctx = this.getCtx();
|
|
1862
2065
|
const dotenv = (ctx?.dotenv ?? {});
|
|
@@ -2351,7 +2554,6 @@ const awsPlugin = () => definePlugin({
|
|
|
2351
2554
|
cli
|
|
2352
2555
|
.ns('aws')
|
|
2353
2556
|
.description('Establish an AWS session and optionally forward to the AWS CLI')
|
|
2354
|
-
.configureHelp({ showGlobalOptions: true })
|
|
2355
2557
|
.enablePositionalOptions()
|
|
2356
2558
|
.passThroughOptions()
|
|
2357
2559
|
.allowUnknownOption(true)
|
|
@@ -2542,9 +2744,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
|
2542
2744
|
}
|
|
2543
2745
|
return { absRootPath, paths };
|
|
2544
2746
|
};
|
|
2545
|
-
const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
2747
|
+
const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
2546
2748
|
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
2547
|
-
Boolean(getDotenvCliOptions?.capture);
|
|
2749
|
+
Boolean(getDotenvCliOptions?.capture);
|
|
2750
|
+
// Require a command only when not listing. In list mode, a command is optional.
|
|
2548
2751
|
if (!command && !list) {
|
|
2549
2752
|
logger.error(`No command provided. Use --command or --list.`);
|
|
2550
2753
|
process.exit(0);
|
|
@@ -2591,12 +2794,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
2591
2794
|
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
2592
2795
|
(Array.isArray(command) && command.length > 0);
|
|
2593
2796
|
if (hasCmd) {
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2797
|
+
// Compose child env overlay from dotenv (drop undefined) and merged options
|
|
2798
|
+
const overlay = {};
|
|
2799
|
+
if (dotenvEnv) {
|
|
2800
|
+
for (const [k, v] of Object.entries(dotenvEnv)) {
|
|
2801
|
+
if (typeof v === 'string')
|
|
2802
|
+
overlay[k] = v;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
if (getDotenvCliOptions !== undefined) {
|
|
2806
|
+
try {
|
|
2807
|
+
overlay.getDotenvCliOptions = JSON.stringify(getDotenvCliOptions);
|
|
2808
|
+
}
|
|
2809
|
+
catch {
|
|
2810
|
+
// best-effort: omit if serialization fails
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2597
2813
|
await runCommand(command, shell, {
|
|
2598
2814
|
cwd: path,
|
|
2599
|
-
env: buildSpawnEnv(process.env,
|
|
2815
|
+
env: buildSpawnEnv(process.env, overlay),
|
|
2600
2816
|
stdio: capture ? 'pipe' : 'inherit',
|
|
2601
2817
|
});
|
|
2602
2818
|
}
|
|
@@ -2634,6 +2850,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2634
2850
|
const ctx = cli.getCtx();
|
|
2635
2851
|
const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
|
|
2636
2852
|
const cfg = (cfgRaw || {});
|
|
2853
|
+
const dotenvEnv = (ctx?.dotenv ?? {});
|
|
2637
2854
|
// Resolve batch flags from the captured parent (batch) command.
|
|
2638
2855
|
const raw = batchCmd.opts();
|
|
2639
2856
|
const listFromParent = !!raw.list;
|
|
@@ -2652,6 +2869,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2652
2869
|
if (typeof commandOpt === 'string') {
|
|
2653
2870
|
await execShellCommandBatch({
|
|
2654
2871
|
command: resolveCommand(scripts, commandOpt),
|
|
2872
|
+
dotenvEnv,
|
|
2655
2873
|
globs,
|
|
2656
2874
|
ignoreErrors,
|
|
2657
2875
|
list: false,
|
|
@@ -2663,6 +2881,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2663
2881
|
return;
|
|
2664
2882
|
}
|
|
2665
2883
|
if (raw.list || localList) {
|
|
2884
|
+
const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
|
|
2666
2885
|
await execShellCommandBatch({
|
|
2667
2886
|
globs,
|
|
2668
2887
|
ignoreErrors,
|
|
@@ -2670,7 +2889,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2670
2889
|
logger: loggerLocal,
|
|
2671
2890
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
2672
2891
|
rootPath,
|
|
2673
|
-
shell: (shell ?? false),
|
|
2892
|
+
shell: (shell ?? shellBag.shell ?? false),
|
|
2674
2893
|
});
|
|
2675
2894
|
return;
|
|
2676
2895
|
}
|
|
@@ -2737,6 +2956,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
2737
2956
|
}
|
|
2738
2957
|
await execShellCommandBatch({
|
|
2739
2958
|
command: commandArg,
|
|
2959
|
+
dotenvEnv,
|
|
2740
2960
|
...(envBag ? { getDotenvCliOptions: envBag } : {}),
|
|
2741
2961
|
globs,
|
|
2742
2962
|
ignoreErrors,
|
|
@@ -2755,6 +2975,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2755
2975
|
const logger = opts.logger ?? console;
|
|
2756
2976
|
// Ensure context exists (host preSubcommand on root creates if missing).
|
|
2757
2977
|
const ctx = cli.getCtx();
|
|
2978
|
+
const dotenvEnv = (ctx?.dotenv ?? {});
|
|
2758
2979
|
const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
|
|
2759
2980
|
const cfg = (cfgRaw || {});
|
|
2760
2981
|
const raw = thisCommand.opts();
|
|
@@ -2777,6 +2998,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2777
2998
|
const commandArg = resolved;
|
|
2778
2999
|
await execShellCommandBatch({
|
|
2779
3000
|
command: commandArg,
|
|
3001
|
+
dotenvEnv,
|
|
2780
3002
|
globs,
|
|
2781
3003
|
ignoreErrors,
|
|
2782
3004
|
list: false,
|
|
@@ -2814,6 +3036,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
2814
3036
|
const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
|
|
2815
3037
|
await execShellCommandBatch({
|
|
2816
3038
|
command: resolveCommand(scriptsOpt, commandOpt),
|
|
3039
|
+
dotenvEnv,
|
|
2817
3040
|
globs,
|
|
2818
3041
|
ignoreErrors,
|
|
2819
3042
|
list,
|
|
@@ -2857,7 +3080,8 @@ const BatchConfigSchema = z.object({
|
|
|
2857
3080
|
/**
|
|
2858
3081
|
* Batch plugin for the GetDotenv CLI host.
|
|
2859
3082
|
*
|
|
2860
|
-
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
3083
|
+
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
3084
|
+
* Options:
|
|
2861
3085
|
* - scripts/shell: used to resolve command and shell behavior per script or global default.
|
|
2862
3086
|
* - logger: defaults to console.
|
|
2863
3087
|
*/
|
|
@@ -2869,12 +3093,32 @@ const batchPlugin = (opts = {}) => definePlugin({
|
|
|
2869
3093
|
setup(cli) {
|
|
2870
3094
|
const ns = cli.ns('batch');
|
|
2871
3095
|
const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
|
|
3096
|
+
const host = cli;
|
|
3097
|
+
const pluginId = 'batch';
|
|
3098
|
+
const GROUP = `plugin:${pluginId}`;
|
|
2872
3099
|
ns.description('Batch command execution across multiple working directories.')
|
|
2873
3100
|
.enablePositionalOptions()
|
|
2874
3101
|
.passThroughOptions()
|
|
2875
|
-
|
|
2876
|
-
.
|
|
2877
|
-
.
|
|
3102
|
+
// Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
|
|
3103
|
+
.addOption((() => {
|
|
3104
|
+
const opt = host.createDynamicOption('-p, --pkg-cwd', (cfg) => {
|
|
3105
|
+
const slice = cfg.plugins.batch ?? {};
|
|
3106
|
+
const on = !!slice.pkgCwd;
|
|
3107
|
+
return `use nearest package directory as current working directory${on ? ' (default)' : ''}`;
|
|
3108
|
+
});
|
|
3109
|
+
opt.__group = GROUP;
|
|
3110
|
+
return opt;
|
|
3111
|
+
})())
|
|
3112
|
+
.addOption((() => {
|
|
3113
|
+
const opt = host.createDynamicOption('-r, --root-path <string>', (cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.plugins.batch?.rootPath || './')})`);
|
|
3114
|
+
opt.__group = GROUP;
|
|
3115
|
+
return opt;
|
|
3116
|
+
})())
|
|
3117
|
+
.addOption((() => {
|
|
3118
|
+
const opt = host.createDynamicOption('-g, --globs <string>', (cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.plugins.batch?.globs || '*')})`);
|
|
3119
|
+
opt.__group = GROUP;
|
|
3120
|
+
return opt;
|
|
3121
|
+
})())
|
|
2878
3122
|
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
2879
3123
|
.option('-l, --list', 'list working directories without executing command')
|
|
2880
3124
|
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
@@ -3184,10 +3428,10 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
3184
3428
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
3185
3429
|
};
|
|
3186
3430
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
3187
|
-
|
|
3188
|
-
|
|
3431
|
+
// Create as a GetDotenvCli child so helpInformation includes a trailing blank line.
|
|
3432
|
+
const cmd = cli
|
|
3433
|
+
.createCommand('cmd')
|
|
3189
3434
|
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3190
|
-
.configureHelp({ showGlobalOptions: true })
|
|
3191
3435
|
.enablePositionalOptions()
|
|
3192
3436
|
.passThroughOptions()
|
|
3193
3437
|
.argument('[command...]')
|
|
@@ -3379,7 +3623,7 @@ const demoPlugin = () => definePlugin({
|
|
|
3379
3623
|
const dotenv = (ctx?.dotenv ?? {});
|
|
3380
3624
|
// Inherit stdio for an interactive demo. Use --capture for CI.
|
|
3381
3625
|
await runCommand(['node', '-e', code], false, {
|
|
3382
|
-
env:
|
|
3626
|
+
env: buildSpawnEnv(process.env, dotenv),
|
|
3383
3627
|
stdio: 'inherit',
|
|
3384
3628
|
});
|
|
3385
3629
|
});
|
|
@@ -3416,20 +3660,23 @@ const demoPlugin = () => definePlugin({
|
|
|
3416
3660
|
const ctx = cli.getCtx();
|
|
3417
3661
|
const dotenv = (ctx?.dotenv ?? {});
|
|
3418
3662
|
await runCommand(resolved, shell, {
|
|
3419
|
-
env:
|
|
3663
|
+
env: buildSpawnEnv(process.env, dotenv),
|
|
3420
3664
|
stdio: 'inherit',
|
|
3421
3665
|
});
|
|
3422
3666
|
});
|
|
3423
3667
|
},
|
|
3424
3668
|
/**
|
|
3425
3669
|
* Optional: afterResolve can initialize per-plugin state using ctx.dotenv.
|
|
3426
|
-
* For the demo we
|
|
3670
|
+
* For the demo we emit a single breadcrumb only when GETDOTENV_DEBUG is set,
|
|
3671
|
+
* keeping default runs (tests/CI/smoke) quiet.
|
|
3427
3672
|
*/
|
|
3428
3673
|
afterResolve(_cli, ctx) {
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3674
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
3675
|
+
const keys = Object.keys(ctx.dotenv);
|
|
3676
|
+
if (keys.length > 0) {
|
|
3677
|
+
// Keep noise low; a single-line breadcrumb is sufficient for the demo.
|
|
3678
|
+
console.error('[demo] afterResolve: dotenv keys loaded:', keys.length);
|
|
3679
|
+
}
|
|
3433
3680
|
}
|
|
3434
3681
|
},
|
|
3435
3682
|
});
|
|
@@ -3687,402 +3934,40 @@ const initPlugin = (opts = {}) => definePlugin({
|
|
|
3687
3934
|
},
|
|
3688
3935
|
});
|
|
3689
3936
|
|
|
3690
|
-
const cmdCommand$1 = new Command()
|
|
3691
|
-
.name('cmd')
|
|
3692
|
-
.description('execute command, conflicts with --command option (default subcommand)')
|
|
3693
|
-
.enablePositionalOptions()
|
|
3694
|
-
.passThroughOptions()
|
|
3695
|
-
.argument('[command...]')
|
|
3696
|
-
.action(async (commandParts, _options, thisCommand) => {
|
|
3697
|
-
if (!thisCommand.parent)
|
|
3698
|
-
throw new Error(`unable to resolve parent command`);
|
|
3699
|
-
if (!thisCommand.parent.parent)
|
|
3700
|
-
throw new Error(`unable to resolve root command`);
|
|
3701
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent.parent;
|
|
3702
|
-
const raw = thisCommand.parent.opts();
|
|
3703
|
-
const ignoreErrors = !!raw.ignoreErrors;
|
|
3704
|
-
const globs = typeof raw.globs === 'string' ? raw.globs : '*';
|
|
3705
|
-
const list = !!raw.list;
|
|
3706
|
-
const pkgCwd = !!raw.pkgCwd;
|
|
3707
|
-
const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : './';
|
|
3708
|
-
// Execute command.
|
|
3709
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
3710
|
-
// When no positional tokens are provided (e.g., option form `-c/--command`),
|
|
3711
|
-
// the preSubcommand hook handles execution. Avoid a duplicate call here.
|
|
3712
|
-
if (args.length === 0)
|
|
3713
|
-
return;
|
|
3714
|
-
const command = args.map(String).join(' ');
|
|
3715
|
-
await execShellCommandBatch({
|
|
3716
|
-
command: resolveCommand(getDotenvCliOptions.scripts, command),
|
|
3717
|
-
getDotenvCliOptions,
|
|
3718
|
-
globs,
|
|
3719
|
-
ignoreErrors,
|
|
3720
|
-
list,
|
|
3721
|
-
logger,
|
|
3722
|
-
pkgCwd,
|
|
3723
|
-
rootPath,
|
|
3724
|
-
// execa expects string | boolean | URL for `shell`. We normalize earlier;
|
|
3725
|
-
// scripts[name].shell overrides take precedence and may be boolean or string.
|
|
3726
|
-
shell: resolveShell(getDotenvCliOptions.scripts, command, getDotenvCliOptions.shell),
|
|
3727
|
-
});
|
|
3728
|
-
});
|
|
3729
|
-
|
|
3730
|
-
const batchCommand = new Command()
|
|
3731
|
-
.name('batch')
|
|
3732
|
-
.description('Batch command execution across multiple working directories.')
|
|
3733
|
-
.enablePositionalOptions()
|
|
3734
|
-
.passThroughOptions()
|
|
3735
|
-
.option('-p, --pkg-cwd', 'use nearest package directory as current working directory')
|
|
3736
|
-
.option('-r, --root-path <string>', 'path to batch root directory from current working directory', './')
|
|
3737
|
-
.option('-g, --globs <string>', 'space-delimited globs from root path', '*')
|
|
3738
|
-
.option('-c, --command <string>', 'command executed according to the base --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv)
|
|
3739
|
-
.option('-l, --list', 'list working directories without executing command')
|
|
3740
|
-
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
3741
|
-
.hook('preSubcommand', async (thisCommand) => {
|
|
3742
|
-
if (!thisCommand.parent)
|
|
3743
|
-
throw new Error(`unable to resolve root command`);
|
|
3744
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent;
|
|
3745
|
-
const raw = thisCommand.opts();
|
|
3746
|
-
const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
|
|
3747
|
-
const ignoreErrors = !!raw.ignoreErrors;
|
|
3748
|
-
const globs = typeof raw.globs === 'string' ? raw.globs : '*';
|
|
3749
|
-
const list = !!raw.list;
|
|
3750
|
-
const pkgCwd = !!raw.pkgCwd;
|
|
3751
|
-
const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : './';
|
|
3752
|
-
const argCount = thisCommand.args.length;
|
|
3753
|
-
if (typeof commandOpt === 'string' && argCount > 0) {
|
|
3754
|
-
logger.error(`--command option conflicts with cmd subcommand.`);
|
|
3755
|
-
process.exit(0);
|
|
3756
|
-
}
|
|
3757
|
-
// Execute command.
|
|
3758
|
-
if (typeof commandOpt === 'string')
|
|
3759
|
-
await execShellCommandBatch({
|
|
3760
|
-
command: resolveCommand(getDotenvCliOptions.scripts, commandOpt),
|
|
3761
|
-
getDotenvCliOptions,
|
|
3762
|
-
globs,
|
|
3763
|
-
ignoreErrors,
|
|
3764
|
-
list,
|
|
3765
|
-
logger,
|
|
3766
|
-
pkgCwd,
|
|
3767
|
-
rootPath,
|
|
3768
|
-
// execa expects string | boolean | URL for `shell`. We normalize earlier;
|
|
3769
|
-
// scripts[name].shell overrides take precedence and may be boolean or string.
|
|
3770
|
-
shell: resolveShell(getDotenvCliOptions.scripts, commandOpt, getDotenvCliOptions.shell),
|
|
3771
|
-
});
|
|
3772
|
-
})
|
|
3773
|
-
.addCommand(cmdCommand$1, { isDefault: true });
|
|
3774
|
-
|
|
3775
|
-
const cmdCommand = new Command()
|
|
3776
|
-
.name('cmd')
|
|
3777
|
-
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3778
|
-
.configureHelp({ showGlobalOptions: true })
|
|
3779
|
-
.enablePositionalOptions()
|
|
3780
|
-
.passThroughOptions()
|
|
3781
|
-
.argument('[command...]')
|
|
3782
|
-
.action(async (commandParts, _options, thisCommand) => {
|
|
3783
|
-
const args = Array.isArray(commandParts) ? commandParts : [];
|
|
3784
|
-
if (args.length === 0)
|
|
3785
|
-
return;
|
|
3786
|
-
if (!thisCommand.parent)
|
|
3787
|
-
throw new Error('parent command not found');
|
|
3788
|
-
const { getDotenvCliOptions: { logger = console, ...getDotenvCliOptions }, } = thisCommand.parent;
|
|
3789
|
-
const command = args.map(String).join(' ');
|
|
3790
|
-
const cmd = resolveCommand(getDotenvCliOptions.scripts, command);
|
|
3791
|
-
if (getDotenvCliOptions.debug)
|
|
3792
|
-
logger.log('\n*** command ***\n', `'${cmd}'`);
|
|
3793
|
-
await execaCommand(cmd, {
|
|
3794
|
-
env: {
|
|
3795
|
-
...process.env,
|
|
3796
|
-
getDotenvCliOptions: JSON.stringify(getDotenvCliOptions),
|
|
3797
|
-
},
|
|
3798
|
-
// execa expects string | boolean | URL; we normalize in generator
|
|
3799
|
-
// and allow script-level overrides.
|
|
3800
|
-
shell: resolveShell(getDotenvCliOptions.scripts, command, getDotenvCliOptions.shell),
|
|
3801
|
-
stdio: 'inherit',
|
|
3802
|
-
});
|
|
3803
|
-
});
|
|
3804
|
-
|
|
3805
|
-
/**
|
|
3806
|
-
* Create the root Commander command with legacy root options (via cliCore)
|
|
3807
|
-
* and built-in subcommands. Pure builder: no side-effects; the caller attaches
|
|
3808
|
-
* lifecycle hooks separately.
|
|
3809
|
-
*/
|
|
3810
|
-
const createRootCommand = (opts) => {
|
|
3811
|
-
const program = new Command().name(opts.alias).description(opts.description);
|
|
3812
|
-
// Attach legacy root flags using shared cliCore builder to keep parity.
|
|
3813
|
-
attachRootOptions(program, opts, {
|
|
3814
|
-
includeCommandOption: true,
|
|
3815
|
-
});
|
|
3816
|
-
// Subcommands
|
|
3817
|
-
program.addCommand(batchCommand).addCommand(cmdCommand, { isDefault: true });
|
|
3818
|
-
return program;
|
|
3819
|
-
};
|
|
3820
|
-
|
|
3821
|
-
/**
|
|
3822
|
-
* Resolve `GetDotenvCliGenerateOptions` from `import.meta.url` and custom options.
|
|
3823
|
-
*/
|
|
3824
|
-
const resolveGetDotenvCliGenerateOptions = async ({ importMetaUrl, ...customOptions }) => {
|
|
3825
|
-
const baseOptions = {
|
|
3826
|
-
...baseGetDotenvCliOptions,
|
|
3827
|
-
alias: 'getdotenv',
|
|
3828
|
-
description: 'Base CLI.',
|
|
3829
|
-
};
|
|
3830
|
-
const globalPkgDir = importMetaUrl
|
|
3831
|
-
? await packageDirectory({
|
|
3832
|
-
cwd: fileURLToPath(importMetaUrl),
|
|
3833
|
-
})
|
|
3834
|
-
: undefined;
|
|
3835
|
-
const globalOptionsPath = globalPkgDir
|
|
3836
|
-
? join(globalPkgDir, getDotenvOptionsFilename)
|
|
3837
|
-
: undefined;
|
|
3838
|
-
const globalOptions = (globalOptionsPath && (await fs.exists(globalOptionsPath))
|
|
3839
|
-
? JSON.parse((await fs.readFile(globalOptionsPath)).toString())
|
|
3840
|
-
: {});
|
|
3841
|
-
const localPkgDir = await packageDirectory();
|
|
3842
|
-
const localOptionsPath = localPkgDir
|
|
3843
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
3844
|
-
: undefined;
|
|
3845
|
-
const localOptions = (localOptionsPath &&
|
|
3846
|
-
localOptionsPath !== globalOptionsPath &&
|
|
3847
|
-
(await fs.exists(localOptionsPath))
|
|
3848
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
3849
|
-
: {});
|
|
3850
|
-
// Merge order: base < global < local < custom
|
|
3851
|
-
const merged = defaultsDeep(baseOptions, globalOptions, localOptions, customOptions);
|
|
3852
|
-
return merged;
|
|
3853
|
-
};
|
|
3854
|
-
|
|
3855
|
-
/**
|
|
3856
|
-
* Resolve dotenv values using the config-loader/overlay path (always-on in
|
|
3857
|
-
* host/generator flows; no-op when no config files are present).
|
|
3858
|
-
*
|
|
3859
|
-
* Order:
|
|
3860
|
-
* 1) Compute base from files only (exclude dynamic; ignore programmatic vars).
|
|
3861
|
-
* 2) Discover packaged + project config sources and overlay onto base.
|
|
3862
|
-
* 3) Apply dynamics in order:
|
|
3863
|
-
* programmatic dynamic \> config dynamic (packaged → project public → project local)
|
|
3864
|
-
* \> file dynamicPath.
|
|
3865
|
-
* 4) Phase C interpolation of remaining string options (e.g., outputPath).
|
|
3866
|
-
* 5) Optionally write outputPath, log, and merge into process.env.
|
|
3867
|
-
*/
|
|
3868
|
-
const resolveDotenvWithConfigLoader = async (validated) => {
|
|
3869
|
-
// 1) Base from files, no dynamic, no programmatic vars
|
|
3870
|
-
const base = await getDotenv({
|
|
3871
|
-
...validated,
|
|
3872
|
-
// Build a pure base without side effects or logging.
|
|
3873
|
-
excludeDynamic: true,
|
|
3874
|
-
vars: {},
|
|
3875
|
-
log: false,
|
|
3876
|
-
loadProcess: false,
|
|
3877
|
-
outputPath: undefined,
|
|
3878
|
-
});
|
|
3879
|
-
// 2) Discover config sources (packaged via this module's import.meta.url)
|
|
3880
|
-
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
3881
|
-
const dotenv = overlayEnv({
|
|
3882
|
-
base,
|
|
3883
|
-
env: validated.env ?? validated.defaultEnv,
|
|
3884
|
-
configs: sources,
|
|
3885
|
-
...(validated.vars ? { programmaticVars: validated.vars } : {}),
|
|
3886
|
-
});
|
|
3887
|
-
// Helper to apply a dynamic map progressively.
|
|
3888
|
-
const applyDynamic = (target, dynamic, env) => {
|
|
3889
|
-
if (!dynamic)
|
|
3890
|
-
return;
|
|
3891
|
-
for (const key of Object.keys(dynamic)) {
|
|
3892
|
-
const value = typeof dynamic[key] === 'function'
|
|
3893
|
-
? dynamic[key](target, env)
|
|
3894
|
-
: dynamic[key];
|
|
3895
|
-
Object.assign(target, { [key]: value });
|
|
3896
|
-
}
|
|
3897
|
-
};
|
|
3898
|
-
// 3) Apply dynamics in order
|
|
3899
|
-
applyDynamic(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
|
|
3900
|
-
applyDynamic(dotenv, (sources.packaged?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
3901
|
-
applyDynamic(dotenv, (sources.project?.public?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
3902
|
-
applyDynamic(dotenv, (sources.project?.local?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
3903
|
-
// file dynamicPath (lowest)
|
|
3904
|
-
if (validated.dynamicPath) {
|
|
3905
|
-
const absDynamicPath = path.resolve(validated.dynamicPath);
|
|
3906
|
-
try {
|
|
3907
|
-
const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
|
|
3908
|
-
applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
|
|
3909
|
-
}
|
|
3910
|
-
catch {
|
|
3911
|
-
throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
|
|
3912
|
-
}
|
|
3913
|
-
}
|
|
3914
|
-
// 4) Phase C: interpolate remaining string options (exclude bootstrap set).
|
|
3915
|
-
// For now, interpolate outputPath only; bootstrap keys are excluded by design.
|
|
3916
|
-
const envRef = { ...process.env, ...dotenv };
|
|
3917
|
-
const outputPathInterpolated = typeof validated.outputPath === 'string'
|
|
3918
|
-
? interpolateDeep(validated.outputPath, envRef)
|
|
3919
|
-
: undefined;
|
|
3920
|
-
// 5) Output/log/process merge (use interpolated outputPath if present)
|
|
3921
|
-
if (outputPathInterpolated) {
|
|
3922
|
-
await fs.writeFile(outputPathInterpolated, Object.keys(dotenv).reduce((contents, key) => {
|
|
3923
|
-
const value = dotenv[key] ?? '';
|
|
3924
|
-
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
3925
|
-
}, ''), { encoding: 'utf-8' });
|
|
3926
|
-
}
|
|
3927
|
-
const logger = validated.logger ?? console;
|
|
3928
|
-
if (validated.log) {
|
|
3929
|
-
const redactFlag = validated.redact ?? false;
|
|
3930
|
-
const redactPatterns = validated.redactPatterns;
|
|
3931
|
-
const redOpts = {};
|
|
3932
|
-
if (redactFlag)
|
|
3933
|
-
redOpts.redact = true;
|
|
3934
|
-
if (redactFlag && Array.isArray(redactPatterns))
|
|
3935
|
-
redOpts.redactPatterns = redactPatterns;
|
|
3936
|
-
const bag = redactFlag ? redactObject(dotenv, redOpts) : { ...dotenv };
|
|
3937
|
-
logger.log(bag);
|
|
3938
|
-
// Entropy warnings: once per key per run (presentation only)
|
|
3939
|
-
const warnEntropyVal = validated.warnEntropy ?? true;
|
|
3940
|
-
const entropyThresholdVal = validated.entropyThreshold;
|
|
3941
|
-
const entropyMinLengthVal = validated.entropyMinLength;
|
|
3942
|
-
const entropyWhitelistVal = validated.entropyWhitelist;
|
|
3943
|
-
const entOpts = {};
|
|
3944
|
-
// include keys only when defined to satisfy exactOptionalPropertyTypes
|
|
3945
|
-
if (typeof warnEntropyVal === 'boolean')
|
|
3946
|
-
entOpts.warnEntropy = warnEntropyVal;
|
|
3947
|
-
if (typeof entropyThresholdVal === 'number')
|
|
3948
|
-
entOpts.entropyThreshold = entropyThresholdVal;
|
|
3949
|
-
if (typeof entropyMinLengthVal === 'number')
|
|
3950
|
-
entOpts.entropyMinLength = entropyMinLengthVal;
|
|
3951
|
-
if (Array.isArray(entropyWhitelistVal))
|
|
3952
|
-
entOpts.entropyWhitelist = entropyWhitelistVal;
|
|
3953
|
-
for (const [k, v] of Object.entries(dotenv)) {
|
|
3954
|
-
maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
|
|
3955
|
-
logger.log(line);
|
|
3956
|
-
});
|
|
3957
|
-
}
|
|
3958
|
-
}
|
|
3959
|
-
if (validated.loadProcess)
|
|
3960
|
-
Object.assign(process.env, dotenv);
|
|
3961
|
-
return dotenv;
|
|
3962
|
-
};
|
|
3963
|
-
|
|
3964
|
-
/**
|
|
3965
|
-
* Omit a "logger" key from an options object in a typed manner.
|
|
3966
|
-
*/
|
|
3967
|
-
const omitLogger = (obj) => {
|
|
3968
|
-
const { logger: _omitted, ...rest } = obj;
|
|
3969
|
-
return rest;
|
|
3970
|
-
};
|
|
3971
|
-
/**
|
|
3972
|
-
* Build the Commander preSubcommand hook using the provided context.
|
|
3973
|
-
* * Responsibilities:
|
|
3974
|
-
* - Merge parent CLI options with current invocation (parent \< current). * - Resolve tri-state flags, including `--exclude-all` overrides.
|
|
3975
|
-
* - Normalize the shell setting to a concrete value (string | boolean).
|
|
3976
|
-
* - Persist merged options on the command instance and pass to subcommands.
|
|
3977
|
-
* - Execute {@link getDotenv} and optional post-hook.
|
|
3978
|
-
* - Either forward to the default `cmd` subcommand or execute `--command`.
|
|
3979
|
-
*
|
|
3980
|
-
* @param context - See {@link PreSubHookContext}.
|
|
3981
|
-
* @returns An async hook suitable for Commander’s `preSubcommand`.
|
|
3982
|
-
*
|
|
3983
|
-
* @example `program.hook('preSubcommand', makePreSubcommandHook(ctx));`
|
|
3984
|
-
*/
|
|
3985
|
-
const makePreSubcommandHook = ({ logger, preHook, postHook, defaults, }) => {
|
|
3986
|
-
return async (thisCommand) => {
|
|
3987
|
-
// Get raw CLI options from commander.
|
|
3988
|
-
const rawCliOptions = thisCommand.opts();
|
|
3989
|
-
const { merged: mergedGetDotenvCliOptions, command: commandOpt } = resolveCliOptions(rawCliOptions, defaults, process.env.getDotenvCliOptions);
|
|
3990
|
-
// Optional debug logging retained via mergedGetDotenvCliOptions.debug if desired. // Execute pre-hook.
|
|
3991
|
-
if (preHook) {
|
|
3992
|
-
await preHook(mergedGetDotenvCliOptions);
|
|
3993
|
-
if (mergedGetDotenvCliOptions.debug)
|
|
3994
|
-
logger.debug('\n*** GetDotenvCliOptions after pre-hook ***\n', mergedGetDotenvCliOptions);
|
|
3995
|
-
}
|
|
3996
|
-
// Persist GetDotenvCliOptions in command for subcommand access.
|
|
3997
|
-
thisCommand.getDotenvCliOptions =
|
|
3998
|
-
mergedGetDotenvCliOptions;
|
|
3999
|
-
// Execute getdotenv via always-on config loader/overlay path.
|
|
4000
|
-
const serviceOptions = getDotenvCliOptions2Options(mergedGetDotenvCliOptions);
|
|
4001
|
-
const dotenv = await resolveDotenvWithConfigLoader(serviceOptions);
|
|
4002
|
-
// Global validation against config (warn by default; --strict fails).
|
|
4003
|
-
try {
|
|
4004
|
-
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
4005
|
-
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
4006
|
-
if (Array.isArray(issues) && issues.length > 0) {
|
|
4007
|
-
issues.forEach((m) => {
|
|
4008
|
-
logger.error(m);
|
|
4009
|
-
});
|
|
4010
|
-
if (mergedGetDotenvCliOptions.strict) {
|
|
4011
|
-
process.exit(1);
|
|
4012
|
-
}
|
|
4013
|
-
}
|
|
4014
|
-
}
|
|
4015
|
-
catch {
|
|
4016
|
-
// Tolerate validator failures in non-strict mode
|
|
4017
|
-
}
|
|
4018
|
-
// Execute post-hook.
|
|
4019
|
-
if (postHook)
|
|
4020
|
-
await postHook(dotenv); // Execute command.
|
|
4021
|
-
const args = thisCommand.args ?? [];
|
|
4022
|
-
const isCommand = typeof commandOpt === 'string' && commandOpt.length > 0;
|
|
4023
|
-
if (isCommand && args.length > 0) {
|
|
4024
|
-
const lr = logger;
|
|
4025
|
-
(lr.error ?? lr.log)(`--command option conflicts with cmd subcommand.`);
|
|
4026
|
-
process.exit(0);
|
|
4027
|
-
}
|
|
4028
|
-
if (typeof commandOpt === 'string' && commandOpt.length > 0) {
|
|
4029
|
-
const cmd = resolveCommand(mergedGetDotenvCliOptions.scripts, commandOpt);
|
|
4030
|
-
if (mergedGetDotenvCliOptions.debug)
|
|
4031
|
-
logger.debug('\n*** command ***\n', cmd);
|
|
4032
|
-
// Build a logger-free bag for env round-trip.
|
|
4033
|
-
const envSafe = omitLogger(mergedGetDotenvCliOptions);
|
|
4034
|
-
await execaCommand(cmd, {
|
|
4035
|
-
env: { ...process.env, getDotenvCliOptions: JSON.stringify(envSafe) },
|
|
4036
|
-
shell: resolveShell(mergedGetDotenvCliOptions.scripts, commandOpt, mergedGetDotenvCliOptions.shell),
|
|
4037
|
-
stdio: 'inherit',
|
|
4038
|
-
});
|
|
4039
|
-
}
|
|
4040
|
-
};
|
|
4041
|
-
};
|
|
4042
|
-
|
|
4043
|
-
/**
|
|
4044
|
-
* Generate a Commander CLI Command for get-dotenv.
|
|
4045
|
-
* Orchestration only: delegates building and lifecycle hooks.
|
|
4046
|
-
*/
|
|
4047
|
-
const generateGetDotenvCli = async (customOptions) => {
|
|
4048
|
-
const options = await resolveGetDotenvCliGenerateOptions(customOptions);
|
|
4049
|
-
const program = createRootCommand(options);
|
|
4050
|
-
const defaults = {};
|
|
4051
|
-
if (options.debug !== undefined)
|
|
4052
|
-
defaults.debug = options.debug;
|
|
4053
|
-
if (options.excludeDynamic !== undefined)
|
|
4054
|
-
defaults.excludeDynamic = options.excludeDynamic;
|
|
4055
|
-
if (options.excludeEnv !== undefined)
|
|
4056
|
-
defaults.excludeEnv = options.excludeEnv;
|
|
4057
|
-
if (options.excludeGlobal !== undefined)
|
|
4058
|
-
defaults.excludeGlobal = options.excludeGlobal;
|
|
4059
|
-
if (options.excludePrivate !== undefined)
|
|
4060
|
-
defaults.excludePrivate = options.excludePrivate;
|
|
4061
|
-
if (options.excludePublic !== undefined)
|
|
4062
|
-
defaults.excludePublic = options.excludePublic;
|
|
4063
|
-
if (options.loadProcess !== undefined)
|
|
4064
|
-
defaults.loadProcess = options.loadProcess;
|
|
4065
|
-
if (options.log !== undefined)
|
|
4066
|
-
defaults.log = options.log;
|
|
4067
|
-
if (options.scripts !== undefined)
|
|
4068
|
-
defaults.scripts = options.scripts;
|
|
4069
|
-
if (options.shell !== undefined)
|
|
4070
|
-
defaults.shell = options.shell;
|
|
4071
|
-
const ctx = {
|
|
4072
|
-
logger: options.logger,
|
|
4073
|
-
defaults,
|
|
4074
|
-
...(options.preHook ? { preHook: options.preHook } : {}),
|
|
4075
|
-
...(options.postHook ? { postHook: options.postHook } : {}),
|
|
4076
|
-
};
|
|
4077
|
-
program.hook('preSubcommand', makePreSubcommandHook(ctx));
|
|
4078
|
-
return program;
|
|
4079
|
-
};
|
|
4080
|
-
|
|
4081
3937
|
function createCli(opts = {}) {
|
|
4082
3938
|
const alias = typeof opts.alias === 'string' && opts.alias.length > 0
|
|
4083
3939
|
? opts.alias
|
|
4084
3940
|
: 'getdotenv';
|
|
4085
3941
|
const program = new GetDotenvCli(alias);
|
|
3942
|
+
// Normalize Commander output so help prints always end with a blank line.
|
|
3943
|
+
// This keeps E2E assertions (CRLF and >=2 trailing newlines) portable across
|
|
3944
|
+
// runtimes and capture modes without altering Commander internals.
|
|
3945
|
+
const outputCfg = {
|
|
3946
|
+
writeOut(str) {
|
|
3947
|
+
const txt = typeof str === 'string' ? str : '';
|
|
3948
|
+
const hasTwo = /(?:\r?\n){2,}$/.test(txt);
|
|
3949
|
+
const hasOne = /\r?\n$/.test(txt);
|
|
3950
|
+
const out = hasTwo ? txt : hasOne ? txt + '\n' : txt + '\n\n';
|
|
3951
|
+
try {
|
|
3952
|
+
process.stdout.write(out);
|
|
3953
|
+
}
|
|
3954
|
+
catch {
|
|
3955
|
+
/* ignore */
|
|
3956
|
+
}
|
|
3957
|
+
},
|
|
3958
|
+
writeErr(str) {
|
|
3959
|
+
process.stderr.write(str);
|
|
3960
|
+
},
|
|
3961
|
+
};
|
|
3962
|
+
// Apply to root and recursively to subcommands so all help paths are normalized.
|
|
3963
|
+
program.configureOutput(outputCfg);
|
|
3964
|
+
const applyOutputRecursively = (cmd) => {
|
|
3965
|
+
cmd.configureOutput(outputCfg);
|
|
3966
|
+
const kids = cmd.commands ?? [];
|
|
3967
|
+
for (const child of kids)
|
|
3968
|
+
applyOutputRecursively(child);
|
|
3969
|
+
};
|
|
3970
|
+
applyOutputRecursively(program);
|
|
4086
3971
|
// Install base root flags and included plugins; resolve context once per run.
|
|
4087
3972
|
program
|
|
4088
3973
|
.attachRootOptions({ loadProcess: false })
|
|
@@ -4098,19 +3983,72 @@ function createCli(opts = {}) {
|
|
|
4098
3983
|
if (underTests) {
|
|
4099
3984
|
program.exitOverride((err) => {
|
|
4100
3985
|
const code = err?.code;
|
|
4101
|
-
|
|
3986
|
+
// Commander printed help already; ensure a trailing blank line for tests/CI capture.
|
|
3987
|
+
if (code === 'commander.helpDisplayed') {
|
|
3988
|
+
try {
|
|
3989
|
+
process.stdout.write('\n');
|
|
3990
|
+
}
|
|
3991
|
+
catch {
|
|
3992
|
+
/* ignore */
|
|
3993
|
+
}
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
if (code === 'commander.version') {
|
|
4102
3997
|
return;
|
|
3998
|
+
}
|
|
4103
3999
|
throw err;
|
|
4104
4000
|
});
|
|
4105
4001
|
}
|
|
4106
4002
|
return {
|
|
4107
4003
|
async run(argv) {
|
|
4108
|
-
//
|
|
4109
|
-
//
|
|
4110
|
-
//
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4004
|
+
// Help handling:
|
|
4005
|
+
// - Short-circuit ONLY for true top-level -h/--help (no subcommand before flag).
|
|
4006
|
+
// - If a subcommand token appears before -h/--help, defer to Commander
|
|
4007
|
+
// to render that subcommand's help.
|
|
4008
|
+
const helpIdx = argv.findIndex((a) => a === '-h' || a === '--help');
|
|
4009
|
+
if (helpIdx >= 0) {
|
|
4010
|
+
// Build a set of known subcommand names/aliases on the root.
|
|
4011
|
+
const subs = new Set();
|
|
4012
|
+
const cmds = program.commands ?? [];
|
|
4013
|
+
for (const c of cmds) {
|
|
4014
|
+
subs.add(c.name());
|
|
4015
|
+
for (const a of c.aliases())
|
|
4016
|
+
subs.add(a);
|
|
4017
|
+
}
|
|
4018
|
+
const hasSubBeforeHelp = argv
|
|
4019
|
+
.slice(0, helpIdx)
|
|
4020
|
+
.some((tok) => subs.has(tok));
|
|
4021
|
+
if (!hasSubBeforeHelp) {
|
|
4022
|
+
await program.brand({
|
|
4023
|
+
name: alias,
|
|
4024
|
+
importMetaUrl: import.meta.url,
|
|
4025
|
+
description: 'Base CLI.',
|
|
4026
|
+
...(typeof opts.branding === 'string' && opts.branding.length > 0
|
|
4027
|
+
? { helpHeader: opts.branding }
|
|
4028
|
+
: {}),
|
|
4029
|
+
});
|
|
4030
|
+
// Resolve context once without side effects for help rendering.
|
|
4031
|
+
const ctx = await program.resolveAndLoad({
|
|
4032
|
+
loadProcess: false,
|
|
4033
|
+
log: false,
|
|
4034
|
+
}, { runAfterResolve: false });
|
|
4035
|
+
program.evaluateDynamicOptions({
|
|
4036
|
+
...ctx.optionsResolved,
|
|
4037
|
+
plugins: ctx.pluginConfigs ?? {},
|
|
4038
|
+
});
|
|
4039
|
+
// Suppress output only during unit tests; allow E2E to capture.
|
|
4040
|
+
const piping = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
4041
|
+
process.env.GETDOTENV_STDOUT === 'pipe';
|
|
4042
|
+
if (underTests && !piping) {
|
|
4043
|
+
void program.helpInformation();
|
|
4044
|
+
}
|
|
4045
|
+
else {
|
|
4046
|
+
program.outputHelp();
|
|
4047
|
+
}
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
// Subcommand token exists before -h: fall through to normal parsing,
|
|
4051
|
+
// letting Commander print that subcommand's help.
|
|
4114
4052
|
}
|
|
4115
4053
|
await program.brand({
|
|
4116
4054
|
name: alias,
|
|
@@ -4125,4 +4063,4 @@ function createCli(opts = {}) {
|
|
|
4125
4063
|
};
|
|
4126
4064
|
}
|
|
4127
4065
|
|
|
4128
|
-
export { buildSpawnEnv, createCli, defineDynamic, dotenvExpand, dotenvExpandAll, dotenvExpandFromProcessEnv,
|
|
4066
|
+
export { buildSpawnEnv, createCli, defineDynamic, dotenvExpand, dotenvExpandAll, dotenvExpandFromProcessEnv, getDotenv, getDotenvCliOptions2Options, interpolateDeep };
|