@karmaniverous/get-dotenv 5.2.6 → 6.0.0-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 +106 -70
- package/dist/cliHost.d.ts +232 -226
- package/dist/cliHost.mjs +777 -545
- package/dist/config.d.ts +7 -2
- package/dist/env-overlay.d.ts +21 -9
- package/dist/env-overlay.mjs +14 -19
- package/dist/getdotenv.cli.mjs +1366 -1163
- package/dist/index.d.ts +415 -242
- package/dist/index.mjs +1364 -1414
- package/dist/plugins-aws.d.ts +149 -94
- package/dist/plugins-aws.mjs +307 -195
- package/dist/plugins-batch.d.ts +153 -99
- package/dist/plugins-batch.mjs +277 -95
- package/dist/plugins-cmd.d.ts +140 -94
- package/dist/plugins-cmd.mjs +636 -502
- package/dist/plugins-demo.d.ts +140 -94
- package/dist/plugins-demo.mjs +237 -46
- package/dist/plugins-init.d.ts +140 -94
- package/dist/plugins-init.mjs +129 -12
- package/dist/plugins.d.ts +166 -103
- package/dist/plugins.mjs +977 -840
- package/package.json +15 -53
- package/templates/cli/ts/plugins/hello.ts +27 -6
- package/templates/config/js/getdotenv.config.js +1 -1
- package/templates/config/ts/getdotenv.config.ts +9 -2
- package/dist/cliHost.cjs +0 -1875
- package/dist/cliHost.d.cts +0 -409
- package/dist/cliHost.d.mts +0 -409
- package/dist/config.cjs +0 -252
- package/dist/config.d.cts +0 -55
- package/dist/config.d.mts +0 -55
- package/dist/env-overlay.cjs +0 -163
- package/dist/env-overlay.d.cts +0 -50
- package/dist/env-overlay.d.mts +0 -50
- package/dist/index.cjs +0 -4140
- package/dist/index.d.cts +0 -457
- package/dist/index.d.mts +0 -457
- package/dist/plugins-aws.cjs +0 -667
- package/dist/plugins-aws.d.cts +0 -158
- package/dist/plugins-aws.d.mts +0 -158
- package/dist/plugins-batch.cjs +0 -616
- package/dist/plugins-batch.d.cts +0 -180
- package/dist/plugins-batch.d.mts +0 -180
- package/dist/plugins-cmd.cjs +0 -1113
- package/dist/plugins-cmd.d.cts +0 -178
- package/dist/plugins-cmd.d.mts +0 -178
- package/dist/plugins-demo.cjs +0 -307
- package/dist/plugins-demo.d.cts +0 -158
- package/dist/plugins-demo.d.mts +0 -158
- package/dist/plugins-init.cjs +0 -289
- package/dist/plugins-init.d.cts +0 -162
- package/dist/plugins-init.d.mts +0 -162
- package/dist/plugins.cjs +0 -2283
- package/dist/plugins.d.cts +0 -210
- package/dist/plugins.d.mts +0 -210
package/dist/cliHost.mjs
CHANGED
|
@@ -1,266 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export { z } from 'zod';
|
|
2
3
|
import fs from 'fs-extra';
|
|
3
4
|
import { packageDirectory } from 'package-directory';
|
|
4
5
|
import path, { join, extname } from 'path';
|
|
5
6
|
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
6
7
|
import YAML from 'yaml';
|
|
7
|
-
import {
|
|
8
|
+
import { Option, Command } from 'commander';
|
|
8
9
|
import { nanoid } from 'nanoid';
|
|
9
10
|
import { parse } from 'dotenv';
|
|
10
11
|
import { createHash } from 'crypto';
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* Dotenv expansion utilities.
|
|
14
|
-
*
|
|
15
|
-
* This module implements recursive expansion of environment-variable
|
|
16
|
-
* references in strings and records. It supports both whitespace and
|
|
17
|
-
* bracket syntaxes with optional defaults:
|
|
18
|
-
*
|
|
19
|
-
* - Whitespace: `$VAR[:default]`
|
|
20
|
-
* - Bracketed: `${VAR[:default]}`
|
|
21
|
-
*
|
|
22
|
-
* Escaped dollar signs (`\$`) are preserved.
|
|
23
|
-
* Unknown variables resolve to empty string unless a default is provided.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* Like String.prototype.search but returns the last index.
|
|
27
|
-
* @internal
|
|
28
|
-
*/
|
|
29
|
-
const searchLast = (str, rgx) => {
|
|
30
|
-
const matches = Array.from(str.matchAll(rgx));
|
|
31
|
-
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
32
|
-
};
|
|
33
|
-
const replaceMatch = (value, match, ref) => {
|
|
34
|
-
/**
|
|
35
|
-
* @internal
|
|
36
|
-
*/
|
|
37
|
-
const group = match[0];
|
|
38
|
-
const key = match[1];
|
|
39
|
-
const defaultValue = match[2];
|
|
40
|
-
if (!key)
|
|
41
|
-
return value;
|
|
42
|
-
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
43
|
-
return interpolate(replacement, ref);
|
|
44
|
-
};
|
|
45
|
-
const interpolate = (value = '', ref = {}) => {
|
|
46
|
-
/**
|
|
47
|
-
* @internal
|
|
48
|
-
*/
|
|
49
|
-
// if value is falsy, return it as is
|
|
50
|
-
if (!value)
|
|
51
|
-
return value;
|
|
52
|
-
// get position of last unescaped dollar sign
|
|
53
|
-
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
54
|
-
// return value if none found
|
|
55
|
-
if (lastUnescapedDollarSignIndex === -1)
|
|
56
|
-
return value;
|
|
57
|
-
// evaluate the value tail
|
|
58
|
-
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
59
|
-
// find whitespace pattern: $KEY:DEFAULT
|
|
60
|
-
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
61
|
-
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
62
|
-
if (whitespaceMatch != null)
|
|
63
|
-
return replaceMatch(value, whitespaceMatch, ref);
|
|
64
|
-
else {
|
|
65
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
66
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
67
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
68
|
-
if (bracketMatch != null)
|
|
69
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
70
|
-
}
|
|
71
|
-
return value;
|
|
72
|
-
};
|
|
73
|
-
/**
|
|
74
|
-
* Recursively expands environment variables in a string. Variables may be
|
|
75
|
-
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
76
|
-
* Unknown variables will expand to an empty string.
|
|
77
|
-
*
|
|
78
|
-
* @param value - The string to expand.
|
|
79
|
-
* @param ref - The reference object to use for variable expansion.
|
|
80
|
-
* @returns The expanded string.
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* ```ts
|
|
84
|
-
* process.env.FOO = 'bar';
|
|
85
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
86
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
87
|
-
* ```
|
|
88
|
-
*
|
|
89
|
-
* @remarks
|
|
90
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
91
|
-
* references, those will also be expanded until a stable value is reached.
|
|
92
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
93
|
-
*/
|
|
94
|
-
const dotenvExpand = (value, ref = process.env) => {
|
|
95
|
-
const result = interpolate(value, ref);
|
|
96
|
-
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
97
|
-
};
|
|
98
|
-
/**
|
|
99
|
-
* Recursively expands environment variables in the values of a JSON object.
|
|
100
|
-
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
101
|
-
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
102
|
-
*
|
|
103
|
-
* @param values - The values object to expand.
|
|
104
|
-
* @param options - Expansion options.
|
|
105
|
-
* @returns The value object with expanded string values.
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* ```ts
|
|
109
|
-
* process.env.FOO = 'bar';
|
|
110
|
-
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
111
|
-
* // => { A: "bar", B: "xbary" }
|
|
112
|
-
* ```
|
|
113
|
-
*
|
|
114
|
-
* @remarks
|
|
115
|
-
* Options:
|
|
116
|
-
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
117
|
-
* - progressive: Whether to progressively add expanded values to the set of
|
|
118
|
-
* reference keys.
|
|
119
|
-
*
|
|
120
|
-
* When `progressive` is true, each expanded key becomes available for
|
|
121
|
-
* subsequent expansions in the same object (left-to-right by object key order).
|
|
122
|
-
*/
|
|
123
|
-
const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
|
|
124
|
-
const { ref = process.env, progressive = false } = options;
|
|
125
|
-
acc[key] = dotenvExpand(values[key], {
|
|
126
|
-
...ref,
|
|
127
|
-
...(progressive ? acc : {}),
|
|
128
|
-
});
|
|
129
|
-
return acc;
|
|
130
|
-
}, {});
|
|
131
|
-
/**
|
|
132
|
-
* Recursively expands environment variables in a string using `process.env` as
|
|
133
|
-
* the expansion reference. Variables may be presented with optional default as
|
|
134
|
-
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
135
|
-
* empty string.
|
|
136
|
-
*
|
|
137
|
-
* @param value - The string to expand.
|
|
138
|
-
* @returns The expanded string.
|
|
139
|
-
*
|
|
140
|
-
* @example
|
|
141
|
-
* ```ts
|
|
142
|
-
* process.env.FOO = 'bar';
|
|
143
|
-
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
144
|
-
* ```
|
|
145
|
-
*/
|
|
146
|
-
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Attach legacy root flags to a Commander program.
|
|
150
|
-
* Uses provided defaults to render help labels without coupling to generators.
|
|
151
|
-
*/
|
|
152
|
-
const attachRootOptions = (program, defaults, opts) => {
|
|
153
|
-
// Install temporary wrappers to tag all options added here as "base".
|
|
154
|
-
const GROUP = 'base';
|
|
155
|
-
const tagLatest = (cmd, group) => {
|
|
156
|
-
const optsArr = cmd.options;
|
|
157
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
158
|
-
const last = optsArr[optsArr.length - 1];
|
|
159
|
-
last.__group = group;
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
const originalAddOption = program.addOption.bind(program);
|
|
163
|
-
const originalOption = program.option.bind(program);
|
|
164
|
-
program.addOption = function patchedAdd(opt) {
|
|
165
|
-
// Tag before adding, in case consumers inspect the Option directly.
|
|
166
|
-
opt.__group = GROUP;
|
|
167
|
-
const ret = originalAddOption(opt);
|
|
168
|
-
return ret;
|
|
169
|
-
};
|
|
170
|
-
program.option = function patchedOption(...args) {
|
|
171
|
-
const ret = originalOption(...args);
|
|
172
|
-
tagLatest(this, GROUP);
|
|
173
|
-
return ret;
|
|
174
|
-
};
|
|
175
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
176
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
177
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
178
|
-
// Build initial chain.
|
|
179
|
-
let p = program
|
|
180
|
-
.enablePositionalOptions()
|
|
181
|
-
.passThroughOptions()
|
|
182
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
183
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
184
|
-
['KEY1', 'VAL1'],
|
|
185
|
-
['KEY2', 'VAL2'],
|
|
186
|
-
]
|
|
187
|
-
.map((v) => v.join(va))
|
|
188
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
189
|
-
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
190
|
-
// Default is OFF; the generator opts in explicitly.
|
|
191
|
-
if (opts?.includeCommandOption === true) {
|
|
192
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
193
|
-
}
|
|
194
|
-
p = p
|
|
195
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
196
|
-
.addOption(new Option('-s, --shell [string]', (() => {
|
|
197
|
-
let defaultLabel = '';
|
|
198
|
-
if (shell !== undefined) {
|
|
199
|
-
if (typeof shell === 'boolean') {
|
|
200
|
-
defaultLabel = ' (default OS shell)';
|
|
201
|
-
}
|
|
202
|
-
else if (typeof shell === 'string') {
|
|
203
|
-
// Safe string interpolation
|
|
204
|
-
defaultLabel = ` (default ${shell})`;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
208
|
-
})()).conflicts('shellOff'))
|
|
209
|
-
.addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
210
|
-
.addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
211
|
-
.addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
212
|
-
.addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
213
|
-
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
214
|
-
? ' (default)'
|
|
215
|
-
: ''}`).conflicts('excludeAllOff'))
|
|
216
|
-
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
217
|
-
.addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
218
|
-
.addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
219
|
-
.addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
220
|
-
.addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
221
|
-
.addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
222
|
-
.addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
223
|
-
.addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
224
|
-
.addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
225
|
-
.addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
226
|
-
.addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
227
|
-
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
228
|
-
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
229
|
-
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
230
|
-
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
231
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
232
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
233
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
234
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
235
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
236
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
237
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
238
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
239
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
240
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
241
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
242
|
-
// Hidden scripts pipe-through (stringified)
|
|
243
|
-
.addOption(new Option('--scripts <string>')
|
|
244
|
-
.default(JSON.stringify(scripts))
|
|
245
|
-
.hideHelp());
|
|
246
|
-
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
247
|
-
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
248
|
-
// Validation: strict mode fails on env validation issues (warn by default).
|
|
249
|
-
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
250
|
-
// Entropy diagnostics (presentation-only)
|
|
251
|
-
p = p
|
|
252
|
-
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
253
|
-
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
254
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
255
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
256
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
257
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
258
|
-
// Restore original methods to avoid tagging future additions outside base.
|
|
259
|
-
program.addOption = originalAddOption;
|
|
260
|
-
program.option = originalOption;
|
|
261
|
-
return p;
|
|
262
|
-
};
|
|
263
|
-
|
|
264
13
|
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
265
14
|
const baseRootOptionDefaults = {
|
|
266
15
|
dotenvToken: '.env',
|
|
@@ -309,26 +58,12 @@ const mergeInto = (target, source) => {
|
|
|
309
58
|
}
|
|
310
59
|
return target;
|
|
311
60
|
};
|
|
312
|
-
|
|
313
|
-
* Perform a deep defaults-style merge across plain objects. *
|
|
314
|
-
* - Only merges plain objects (prototype === Object.prototype).
|
|
315
|
-
* - Arrays and non-objects are replaced, not merged.
|
|
316
|
-
* - `undefined` values are ignored and do not overwrite prior values.
|
|
317
|
-
*
|
|
318
|
-
* @typeParam T - The resulting shape after merging all layers.
|
|
319
|
-
* @param layers - Zero or more partial layers in ascending precedence order.
|
|
320
|
-
* @returns The merged object typed as {@link T}.
|
|
321
|
-
*
|
|
322
|
-
* @example
|
|
323
|
-
* defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
|
|
324
|
-
* =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
|
|
325
|
-
*/
|
|
326
|
-
const defaultsDeep = (...layers) => {
|
|
61
|
+
function defaultsDeep(...layers) {
|
|
327
62
|
const result = layers
|
|
328
63
|
.filter(Boolean)
|
|
329
64
|
.reduce((acc, layer) => mergeInto(acc, layer), {});
|
|
330
65
|
return result;
|
|
331
|
-
}
|
|
66
|
+
}
|
|
332
67
|
|
|
333
68
|
/**
|
|
334
69
|
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
@@ -750,106 +485,451 @@ const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
|
750
485
|
|
|
751
486
|
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
752
487
|
|
|
488
|
+
/** src/util/omitUndefined.ts
|
|
489
|
+
* Helpers to drop undefined-valued properties in a typed-friendly way.
|
|
490
|
+
*/
|
|
491
|
+
/**
|
|
492
|
+
* Omit keys whose runtime value is undefined from a shallow object.
|
|
493
|
+
* Returns a Partial with non-undefined value types preserved.
|
|
494
|
+
*/
|
|
495
|
+
function omitUndefined(obj) {
|
|
496
|
+
const out = {};
|
|
497
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
498
|
+
if (v !== undefined)
|
|
499
|
+
out[k] = v;
|
|
500
|
+
}
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Specialized helper for env-like maps: drop undefined and return string-only.
|
|
505
|
+
*/
|
|
506
|
+
function omitUndefinedRecord(obj) {
|
|
507
|
+
const out = {};
|
|
508
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
509
|
+
if (v !== undefined)
|
|
510
|
+
out[k] = v;
|
|
511
|
+
}
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
|
|
753
515
|
// src/GetDotenvOptions.ts
|
|
516
|
+
/**
|
|
517
|
+
* Canonical programmatic options and helpers for get-dotenv.
|
|
518
|
+
*
|
|
519
|
+
* Requirements addressed:
|
|
520
|
+
* - GetDotenvOptions derives from the Zod schema output (single source of truth).
|
|
521
|
+
* - Removed deprecated/compat flags from the public shape (e.g., useConfigLoader).
|
|
522
|
+
* - Provide Vars-aware defineDynamic and a typed config builder defineGetDotenvConfig\<Vars, Env\>().
|
|
523
|
+
* - Preserve existing behavior for defaults resolution and compat converters.
|
|
524
|
+
*/
|
|
754
525
|
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
755
526
|
/**
|
|
756
|
-
* Converts programmatic CLI options to `getDotenv` options.
|
|
757
|
-
*
|
|
527
|
+
* Converts programmatic CLI options to `getDotenv` options.
|
|
528
|
+
*
|
|
529
|
+
* Accepts "stringly" CLI inputs for vars/paths and normalizes them into
|
|
530
|
+
* the programmatic shape. Preserves exactOptionalPropertyTypes semantics by
|
|
531
|
+
* omitting keys when undefined.
|
|
532
|
+
*/
|
|
533
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern,
|
|
534
|
+
// drop CLI-only keys from the pass-through bag
|
|
535
|
+
debug: _debug, scripts: _scripts, ...rest }) => {
|
|
536
|
+
// Split helper for delimited strings or regex patterns
|
|
537
|
+
const splitBy = (value, delim, pattern) => {
|
|
538
|
+
if (!value)
|
|
539
|
+
return [];
|
|
540
|
+
if (pattern)
|
|
541
|
+
return value.split(RegExp(pattern));
|
|
542
|
+
if (typeof delim === 'string')
|
|
543
|
+
return value.split(delim);
|
|
544
|
+
return value.split(' ');
|
|
545
|
+
};
|
|
546
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
547
|
+
let parsedVars;
|
|
548
|
+
if (typeof vars === 'string') {
|
|
549
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern)
|
|
550
|
+
.map((v) => v.split(varsAssignorPattern
|
|
551
|
+
? RegExp(varsAssignorPattern)
|
|
552
|
+
: (varsAssignor ?? '=')))
|
|
553
|
+
.filter(([k]) => typeof k === 'string' && k.length > 0);
|
|
554
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
555
|
+
}
|
|
556
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
557
|
+
// Accept provided object map of string | undefined; drop undefined values
|
|
558
|
+
// in the normalization step below to produce a ProcessEnv-compatible bag.
|
|
559
|
+
parsedVars = Object.fromEntries(Object.entries(vars));
|
|
560
|
+
}
|
|
561
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
562
|
+
// expectations and the compat test assertions.
|
|
563
|
+
if (parsedVars) {
|
|
564
|
+
parsedVars = omitUndefinedRecord(parsedVars);
|
|
565
|
+
}
|
|
566
|
+
// Tolerate paths as either a delimited string or string[]
|
|
567
|
+
const pathsOut = Array.isArray(paths)
|
|
568
|
+
? paths.filter((p) => typeof p === 'string')
|
|
569
|
+
: splitBy(paths, pathsDelimiter, pathsDelimiterPattern);
|
|
570
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
571
|
+
return {
|
|
572
|
+
...rest,
|
|
573
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
574
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
575
|
+
};
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
579
|
+
*
|
|
580
|
+
* 1. Base defaults derived from the CLI generator defaults
|
|
581
|
+
* ({@link baseGetDotenvCliOptions}).
|
|
582
|
+
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
583
|
+
* package root (if present).
|
|
584
|
+
* 3. The provided customOptions.
|
|
585
|
+
*
|
|
586
|
+
* The result preserves explicit empty values and drops only `undefined`.
|
|
587
|
+
*/
|
|
588
|
+
const resolveGetDotenvOptions = async (customOptions) => {
|
|
589
|
+
const localPkgDir = await packageDirectory();
|
|
590
|
+
const localOptionsPath = localPkgDir
|
|
591
|
+
? join(localPkgDir, getDotenvOptionsFilename)
|
|
592
|
+
: undefined;
|
|
593
|
+
// Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
|
|
594
|
+
let localOptions = {};
|
|
595
|
+
if (localOptionsPath && (await fs.exists(localOptionsPath))) {
|
|
596
|
+
try {
|
|
597
|
+
const txt = await fs.readFile(localOptionsPath, 'utf-8');
|
|
598
|
+
const parsed = JSON.parse(txt);
|
|
599
|
+
if (parsed && typeof parsed === 'object') {
|
|
600
|
+
localOptions = parsed;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// Malformed or unreadable local options are treated as absent.
|
|
605
|
+
localOptions = {};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Merge order: base < local < custom (custom has highest precedence)
|
|
609
|
+
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
610
|
+
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
611
|
+
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
612
|
+
return {
|
|
613
|
+
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
614
|
+
vars: omitUndefinedRecord(result.vars ?? {}),
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Dotenv expansion utilities.
|
|
620
|
+
*
|
|
621
|
+
* This module implements recursive expansion of environment-variable
|
|
622
|
+
* references in strings and records. It supports both whitespace and
|
|
623
|
+
* bracket syntaxes with optional defaults:
|
|
624
|
+
*
|
|
625
|
+
* - Whitespace: `$VAR[:default]`
|
|
626
|
+
* - Bracketed: `${VAR[:default]}`
|
|
627
|
+
*
|
|
628
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
629
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
630
|
+
*/
|
|
631
|
+
/**
|
|
632
|
+
* Like String.prototype.search but returns the last index.
|
|
633
|
+
* @internal
|
|
634
|
+
*/
|
|
635
|
+
const searchLast = (str, rgx) => {
|
|
636
|
+
const matches = Array.from(str.matchAll(rgx));
|
|
637
|
+
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
638
|
+
};
|
|
639
|
+
const replaceMatch = (value, match, ref) => {
|
|
640
|
+
/**
|
|
641
|
+
* @internal
|
|
642
|
+
*/
|
|
643
|
+
const group = match[0];
|
|
644
|
+
const key = match[1];
|
|
645
|
+
const defaultValue = match[2];
|
|
646
|
+
if (!key)
|
|
647
|
+
return value;
|
|
648
|
+
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
649
|
+
return interpolate(replacement, ref);
|
|
650
|
+
};
|
|
651
|
+
const interpolate = (value = '', ref = {}) => {
|
|
652
|
+
/**
|
|
653
|
+
* @internal
|
|
654
|
+
*/
|
|
655
|
+
// if value is falsy, return it as is
|
|
656
|
+
if (!value)
|
|
657
|
+
return value;
|
|
658
|
+
// get position of last unescaped dollar sign
|
|
659
|
+
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
660
|
+
// return value if none found
|
|
661
|
+
if (lastUnescapedDollarSignIndex === -1)
|
|
662
|
+
return value;
|
|
663
|
+
// evaluate the value tail
|
|
664
|
+
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
665
|
+
// find whitespace pattern: $KEY:DEFAULT
|
|
666
|
+
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
667
|
+
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
668
|
+
if (whitespaceMatch != null)
|
|
669
|
+
return replaceMatch(value, whitespaceMatch, ref);
|
|
670
|
+
else {
|
|
671
|
+
// find bracket pattern: ${KEY:DEFAULT}
|
|
672
|
+
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
673
|
+
const bracketMatch = bracketPattern.exec(tail);
|
|
674
|
+
if (bracketMatch != null)
|
|
675
|
+
return replaceMatch(value, bracketMatch, ref);
|
|
676
|
+
}
|
|
677
|
+
return value;
|
|
678
|
+
};
|
|
679
|
+
/**
|
|
680
|
+
* Recursively expands environment variables in a string. Variables may be
|
|
681
|
+
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
682
|
+
* Unknown variables will expand to an empty string.
|
|
683
|
+
*
|
|
684
|
+
* @param value - The string to expand.
|
|
685
|
+
* @param ref - The reference object to use for variable expansion.
|
|
686
|
+
* @returns The expanded string.
|
|
687
|
+
*
|
|
688
|
+
* @example
|
|
689
|
+
* ```ts
|
|
690
|
+
* process.env.FOO = 'bar';
|
|
691
|
+
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
692
|
+
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
693
|
+
* ```
|
|
694
|
+
*
|
|
695
|
+
* @remarks
|
|
696
|
+
* The expansion is recursive. If a referenced variable itself contains
|
|
697
|
+
* references, those will also be expanded until a stable value is reached.
|
|
698
|
+
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
699
|
+
*/
|
|
700
|
+
const dotenvExpand = (value, ref = process.env) => {
|
|
701
|
+
const result = interpolate(value, ref);
|
|
702
|
+
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
703
|
+
};
|
|
704
|
+
/**
|
|
705
|
+
* Recursively expands environment variables in the values of a JSON object.
|
|
706
|
+
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
707
|
+
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
708
|
+
*
|
|
709
|
+
* @param values - The values object to expand.
|
|
710
|
+
* @param options - Expansion options.
|
|
711
|
+
* @returns The value object with expanded string values.
|
|
712
|
+
*
|
|
713
|
+
* @example
|
|
714
|
+
* ```ts
|
|
715
|
+
* process.env.FOO = 'bar';
|
|
716
|
+
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
717
|
+
* // => { A: "bar", B: "xbary" }
|
|
718
|
+
* ```
|
|
719
|
+
*
|
|
720
|
+
* @remarks
|
|
721
|
+
* Options:
|
|
722
|
+
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
723
|
+
* - progressive: Whether to progressively add expanded values to the set of
|
|
724
|
+
* reference keys.
|
|
725
|
+
*
|
|
726
|
+
* When `progressive` is true, each expanded key becomes available for
|
|
727
|
+
* subsequent expansions in the same object (left-to-right by object key order).
|
|
728
|
+
*/
|
|
729
|
+
function dotenvExpandAll(values, options = {}) {
|
|
730
|
+
const { ref = process.env, progressive = false, } = options;
|
|
731
|
+
const out = Object.keys(values).reduce((acc, key) => {
|
|
732
|
+
acc[key] = dotenvExpand(values[key], {
|
|
733
|
+
...ref,
|
|
734
|
+
...(progressive ? acc : {}),
|
|
735
|
+
});
|
|
736
|
+
return acc;
|
|
737
|
+
}, {});
|
|
738
|
+
// Key-preserving return with a permissive index signature to allow later additions.
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
743
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
744
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
745
|
+
* empty string.
|
|
746
|
+
*
|
|
747
|
+
* @param value - The string to expand.
|
|
748
|
+
* @returns The expanded string.
|
|
758
749
|
*
|
|
759
|
-
* @
|
|
750
|
+
* @example
|
|
751
|
+
* ```ts
|
|
752
|
+
* process.env.FOO = 'bar';
|
|
753
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
754
|
+
* ```
|
|
760
755
|
*/
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
779
|
-
let parsedVars;
|
|
780
|
-
if (typeof vars === 'string') {
|
|
781
|
-
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
782
|
-
? RegExp(varsAssignorPattern)
|
|
783
|
-
: (varsAssignor ?? '=')));
|
|
784
|
-
parsedVars = Object.fromEntries(kvPairs);
|
|
785
|
-
}
|
|
786
|
-
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
787
|
-
// Keep only string or undefined values to match ProcessEnv.
|
|
788
|
-
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
789
|
-
parsedVars = Object.fromEntries(entries);
|
|
790
|
-
}
|
|
791
|
-
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
792
|
-
// expectations and the compat test assertions.
|
|
793
|
-
if (parsedVars) {
|
|
794
|
-
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
795
|
-
}
|
|
796
|
-
// Tolerate paths as either a delimited string or string[]
|
|
797
|
-
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
798
|
-
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
799
|
-
const pathsAny = paths;
|
|
800
|
-
const pathsOut = Array.isArray(pathsAny)
|
|
801
|
-
? pathsAny.filter((p) => typeof p === 'string')
|
|
802
|
-
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
803
|
-
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
804
|
-
return {
|
|
805
|
-
...restObj,
|
|
806
|
-
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
807
|
-
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
756
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
757
|
+
|
|
758
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
759
|
+
/**
|
|
760
|
+
* Attach root flags to a GetDotenvCli instance.
|
|
761
|
+
* - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
|
|
762
|
+
* - Any flag that displays an effective default in help uses dynamic descriptions.
|
|
763
|
+
*/
|
|
764
|
+
const attachRootOptions = (program, defaults) => {
|
|
765
|
+
// Install temporary wrappers to tag all options added here as "base" for grouped help.
|
|
766
|
+
const GROUP = 'base';
|
|
767
|
+
const tagLatest = (cmd, group) => {
|
|
768
|
+
const optsArr = cmd.options ?? [];
|
|
769
|
+
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
770
|
+
const last = optsArr[optsArr.length - 1];
|
|
771
|
+
program.setOptionGroup(last, group);
|
|
772
|
+
}
|
|
808
773
|
};
|
|
809
|
-
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
*
|
|
820
|
-
* The result preserves explicit empty values and drops only `undefined`.
|
|
821
|
-
*
|
|
822
|
-
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
823
|
-
*
|
|
824
|
-
* @example
|
|
825
|
-
* ```ts
|
|
826
|
-
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
827
|
-
* ```
|
|
828
|
-
*/
|
|
829
|
-
const localPkgDir = await packageDirectory();
|
|
830
|
-
const localOptionsPath = localPkgDir
|
|
831
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
832
|
-
: undefined;
|
|
833
|
-
const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
|
|
834
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
835
|
-
: {});
|
|
836
|
-
// Merge order: base < local < custom (custom has highest precedence)
|
|
837
|
-
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
838
|
-
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
839
|
-
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
840
|
-
return {
|
|
841
|
-
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
842
|
-
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
774
|
+
const originalAddOption = program.addOption.bind(program);
|
|
775
|
+
const originalOption = program.option.bind(program);
|
|
776
|
+
program.addOption = function patchedAdd(opt) {
|
|
777
|
+
program.setOptionGroup(opt, GROUP);
|
|
778
|
+
return originalAddOption(opt);
|
|
779
|
+
};
|
|
780
|
+
program.option = function patchedOption(...args) {
|
|
781
|
+
const ret = originalOption(...args);
|
|
782
|
+
tagLatest(this, GROUP);
|
|
783
|
+
return ret;
|
|
843
784
|
};
|
|
785
|
+
const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
786
|
+
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
787
|
+
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
788
|
+
// Helper: append (default) tags for ON/OFF toggles
|
|
789
|
+
const onOff = (on, isDefault) => on
|
|
790
|
+
? `ON${isDefault ? ' (default)' : ''}`
|
|
791
|
+
: `OFF${isDefault ? ' (default)' : ''}`;
|
|
792
|
+
let p = program
|
|
793
|
+
.enablePositionalOptions()
|
|
794
|
+
.passThroughOptions()
|
|
795
|
+
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
796
|
+
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
797
|
+
['KEY1', 'VAL1'],
|
|
798
|
+
['KEY2', 'VAL2'],
|
|
799
|
+
]
|
|
800
|
+
.map((v) => v.join(va))
|
|
801
|
+
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
802
|
+
// Output path (interpolated later; help can remain static)
|
|
803
|
+
p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
|
|
804
|
+
// Shell ON (string or boolean true => default shell)
|
|
805
|
+
p = p
|
|
806
|
+
.addOption(program
|
|
807
|
+
.createDynamicOption('-s, --shell [string]', (cfg) => {
|
|
808
|
+
const s = cfg.shell;
|
|
809
|
+
let tag = '';
|
|
810
|
+
if (typeof s === 'boolean' && s)
|
|
811
|
+
tag = ' (default OS shell)';
|
|
812
|
+
else if (typeof s === 'string' && s.length > 0)
|
|
813
|
+
tag = ` (default ${s})`;
|
|
814
|
+
return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
|
|
815
|
+
})
|
|
816
|
+
.conflicts('shellOff'))
|
|
817
|
+
// Shell OFF
|
|
818
|
+
.addOption(program
|
|
819
|
+
.createDynamicOption('-S, --shell-off', (cfg) => {
|
|
820
|
+
const s = cfg.shell;
|
|
821
|
+
return `command execution shell OFF${s === false ? ' (default)' : ''}`;
|
|
822
|
+
})
|
|
823
|
+
.conflicts('shell'));
|
|
824
|
+
// Load process ON/OFF (dynamic defaults)
|
|
825
|
+
p = p
|
|
826
|
+
.addOption(program
|
|
827
|
+
.createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
|
|
828
|
+
.conflicts('loadProcessOff'))
|
|
829
|
+
.addOption(program
|
|
830
|
+
.createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
|
|
831
|
+
.conflicts('loadProcess'));
|
|
832
|
+
// Exclusion master toggle (dynamic)
|
|
833
|
+
p = p
|
|
834
|
+
.addOption(program
|
|
835
|
+
.createDynamicOption('-a, --exclude-all', (cfg) => {
|
|
836
|
+
const allOn = !!cfg.excludeDynamic &&
|
|
837
|
+
((!!cfg.excludeEnv && !!cfg.excludeGlobal) ||
|
|
838
|
+
(!!cfg.excludePrivate && !!cfg.excludePublic));
|
|
839
|
+
const suffix = allOn ? ' (default)' : '';
|
|
840
|
+
return `exclude all dotenv variables from loading ON${suffix}`;
|
|
841
|
+
})
|
|
842
|
+
.conflicts('excludeAllOff'))
|
|
843
|
+
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
|
|
844
|
+
// Per-family exclusions (dynamic defaults)
|
|
845
|
+
p = p
|
|
846
|
+
.addOption(program
|
|
847
|
+
.createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
|
|
848
|
+
.conflicts('excludeDynamicOff'))
|
|
849
|
+
.addOption(program
|
|
850
|
+
.createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
|
|
851
|
+
.conflicts('excludeDynamic'))
|
|
852
|
+
.addOption(program
|
|
853
|
+
.createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
|
|
854
|
+
.conflicts('excludeEnvOff'))
|
|
855
|
+
.addOption(program
|
|
856
|
+
.createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
|
|
857
|
+
.conflicts('excludeEnv'))
|
|
858
|
+
.addOption(program
|
|
859
|
+
.createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
|
|
860
|
+
.conflicts('excludeGlobalOff'))
|
|
861
|
+
.addOption(program
|
|
862
|
+
.createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
|
|
863
|
+
.conflicts('excludeGlobal'))
|
|
864
|
+
.addOption(program
|
|
865
|
+
.createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
|
|
866
|
+
.conflicts('excludePrivateOff'))
|
|
867
|
+
.addOption(program
|
|
868
|
+
.createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
|
|
869
|
+
.conflicts('excludePrivate'))
|
|
870
|
+
.addOption(program
|
|
871
|
+
.createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
|
|
872
|
+
.conflicts('excludePublicOff'))
|
|
873
|
+
.addOption(program
|
|
874
|
+
.createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
|
|
875
|
+
.conflicts('excludePublic'));
|
|
876
|
+
// Log ON/OFF (dynamic)
|
|
877
|
+
p = p
|
|
878
|
+
.addOption(program
|
|
879
|
+
.createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
|
|
880
|
+
.conflicts('logOff'))
|
|
881
|
+
.addOption(program
|
|
882
|
+
.createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
|
|
883
|
+
.conflicts('log'));
|
|
884
|
+
// Capture flag (no default display; static)
|
|
885
|
+
p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
|
|
886
|
+
// Core bootstrap/static flags (kept static in help)
|
|
887
|
+
p = p
|
|
888
|
+
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
889
|
+
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
890
|
+
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
891
|
+
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
892
|
+
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
893
|
+
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
894
|
+
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
895
|
+
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
896
|
+
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
897
|
+
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
898
|
+
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
899
|
+
// Hidden scripts pipe-through (stringified)
|
|
900
|
+
.addOption(new Option('--scripts <string>')
|
|
901
|
+
.default(JSON.stringify(scripts))
|
|
902
|
+
.hideHelp());
|
|
903
|
+
// Diagnostics / validation / entropy
|
|
904
|
+
p = p
|
|
905
|
+
.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
|
|
906
|
+
.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
907
|
+
p = p
|
|
908
|
+
.addOption(program
|
|
909
|
+
.createDynamicOption('--entropy-warn', (cfg) => {
|
|
910
|
+
const warn = cfg.warnEntropy;
|
|
911
|
+
// Default is effectively ON when warnEntropy is true or undefined.
|
|
912
|
+
return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
|
|
913
|
+
})
|
|
914
|
+
.conflicts('entropyWarnOff'))
|
|
915
|
+
.addOption(program
|
|
916
|
+
.createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
|
|
917
|
+
.conflicts('entropyWarn'))
|
|
918
|
+
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
919
|
+
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
920
|
+
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
921
|
+
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
922
|
+
// Restore original methods
|
|
923
|
+
program.addOption = originalAddOption;
|
|
924
|
+
program.option = originalOption;
|
|
925
|
+
return p;
|
|
844
926
|
};
|
|
845
927
|
|
|
846
928
|
/**
|
|
847
929
|
* Zod schemas for programmatic GetDotenv options.
|
|
848
930
|
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
852
|
-
* later per the staged plan.
|
|
931
|
+
* Canonical source of truth for options shape. Public types are derived
|
|
932
|
+
* from these schemas (see consumers via z.output\<\>).
|
|
853
933
|
*/
|
|
854
934
|
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
855
935
|
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
@@ -868,12 +948,11 @@ const getDotenvOptionsSchemaRaw = z.object({
|
|
|
868
948
|
excludePublic: z.boolean().optional(),
|
|
869
949
|
loadProcess: z.boolean().optional(),
|
|
870
950
|
log: z.boolean().optional(),
|
|
951
|
+
logger: z.unknown().optional(),
|
|
871
952
|
outputPath: z.string().optional(),
|
|
872
953
|
paths: z.array(z.string()).optional(),
|
|
873
954
|
privateToken: z.string().optional(),
|
|
874
955
|
vars: processEnvSchema.optional(),
|
|
875
|
-
// Host-only feature flag: guarded integration of config loader/overlay
|
|
876
|
-
useConfigLoader: z.boolean().optional(),
|
|
877
956
|
});
|
|
878
957
|
// RESOLVED: service-boundary contract (post-inheritance).
|
|
879
958
|
// For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
|
|
@@ -893,16 +972,7 @@ const applyConfigSlice = (current, cfg, env) => {
|
|
|
893
972
|
const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
|
|
894
973
|
return applyKv(afterGlobal, envKv);
|
|
895
974
|
};
|
|
896
|
-
|
|
897
|
-
* Overlay config-provided values onto a base ProcessEnv using precedence axes:
|
|
898
|
-
* - kind: env \> global
|
|
899
|
-
* - privacy: local \> public
|
|
900
|
-
* - source: project \> packaged \> base
|
|
901
|
-
*
|
|
902
|
-
* Programmatic explicit vars (if provided) override all config slices.
|
|
903
|
-
* Progressive expansion is applied within each slice.
|
|
904
|
-
*/
|
|
905
|
-
const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
975
|
+
function overlayEnv({ base, env, configs, programmaticVars, }) {
|
|
906
976
|
let current = { ...base };
|
|
907
977
|
// Source: packaged (public -> local)
|
|
908
978
|
current = applyConfigSlice(current, configs.packaged, env);
|
|
@@ -917,7 +987,7 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
|
917
987
|
current = applyKv(current, toApply);
|
|
918
988
|
}
|
|
919
989
|
return current;
|
|
920
|
-
}
|
|
990
|
+
}
|
|
921
991
|
|
|
922
992
|
/** src/diagnostics/entropy.ts
|
|
923
993
|
* Entropy diagnostics (presentation-only).
|
|
@@ -927,7 +997,7 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
|
927
997
|
*/
|
|
928
998
|
const warned = new Set();
|
|
929
999
|
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
930
|
-
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
1000
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => (typeof p === 'string' ? new RegExp(p, 'i') : p));
|
|
931
1001
|
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
932
1002
|
const shannonBitsPerChar = (s) => {
|
|
933
1003
|
const freq = new Map();
|
|
@@ -974,7 +1044,7 @@ const DEFAULT_PATTERNS = [
|
|
|
974
1044
|
'\\bapi[_-]?key\\b',
|
|
975
1045
|
'\\bkey\\b',
|
|
976
1046
|
];
|
|
977
|
-
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
1047
|
+
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
|
|
978
1048
|
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
979
1049
|
const MASK = '[redacted]';
|
|
980
1050
|
/**
|
|
@@ -1117,43 +1187,7 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
|
1117
1187
|
}
|
|
1118
1188
|
};
|
|
1119
1189
|
|
|
1120
|
-
|
|
1121
|
-
* Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
|
|
1122
|
-
*
|
|
1123
|
-
* @param options - `GetDotenvOptions` object
|
|
1124
|
-
* @returns The combined parsed dotenv object.
|
|
1125
|
-
* * @example Load from the project root with default tokens
|
|
1126
|
-
* ```ts
|
|
1127
|
-
* const vars = await getDotenv();
|
|
1128
|
-
* console.log(vars.MY_SETTING);
|
|
1129
|
-
* ```
|
|
1130
|
-
*
|
|
1131
|
-
* @example Load from multiple paths and a specific environment
|
|
1132
|
-
* ```ts
|
|
1133
|
-
* const vars = await getDotenv({
|
|
1134
|
-
* env: 'dev',
|
|
1135
|
-
* dotenvToken: '.testenv',
|
|
1136
|
-
* privateToken: 'secret',
|
|
1137
|
-
* paths: ['./', './packages/app'],
|
|
1138
|
-
* });
|
|
1139
|
-
* ```
|
|
1140
|
-
*
|
|
1141
|
-
* @example Use dynamic variables
|
|
1142
|
-
* ```ts
|
|
1143
|
-
* // .env.js default-exports: { DYNAMIC: ({ PREV }) => `${PREV}-suffix` }
|
|
1144
|
-
* const vars = await getDotenv({ dynamicPath: '.env.js' });
|
|
1145
|
-
* ```
|
|
1146
|
-
*
|
|
1147
|
-
* @remarks
|
|
1148
|
-
* - When {@link GetDotenvOptions.loadProcess} is true, the resulting variables are merged
|
|
1149
|
-
* into `process.env` as a side effect.
|
|
1150
|
-
* - When {@link GetDotenvOptions.outputPath} is provided, a consolidated dotenv file is written.
|
|
1151
|
-
* The path is resolved after expansion, so it may reference previously loaded vars.
|
|
1152
|
-
*
|
|
1153
|
-
* @throws Error when a dynamic module is present but cannot be imported.
|
|
1154
|
-
* @throws Error when an output path was requested but could not be resolved.
|
|
1155
|
-
*/
|
|
1156
|
-
const getDotenv = async (options = {}) => {
|
|
1190
|
+
async function getDotenv(options = {}) {
|
|
1157
1191
|
// Apply defaults.
|
|
1158
1192
|
const { defaultEnv, dotenvToken = '.env', dynamicPath, env, excludeDynamic = false, excludeEnv = false, excludeGlobal = false, excludePrivate = false, excludePublic = false, loadProcess = false, log = false, logger = console, outputPath, paths = [], privateToken = 'local', vars = {}, } = await resolveGetDotenvOptions(options);
|
|
1159
1193
|
// Read .env files.
|
|
@@ -1258,8 +1292,7 @@ const getDotenv = async (options = {}) => {
|
|
|
1258
1292
|
.entropyThreshold;
|
|
1259
1293
|
const entropyMinLengthVal = options
|
|
1260
1294
|
.entropyMinLength;
|
|
1261
|
-
const entropyWhitelistVal = options
|
|
1262
|
-
.entropyWhitelist;
|
|
1295
|
+
const entropyWhitelistVal = options.entropyWhitelist;
|
|
1263
1296
|
const entOpts = {};
|
|
1264
1297
|
if (typeof warnEntropyVal === 'boolean')
|
|
1265
1298
|
entOpts.warnEntropy = warnEntropyVal;
|
|
@@ -1279,7 +1312,7 @@ const getDotenv = async (options = {}) => {
|
|
|
1279
1312
|
if (loadProcess)
|
|
1280
1313
|
Object.assign(process.env, resultDotenv);
|
|
1281
1314
|
return resultDotenv;
|
|
1282
|
-
}
|
|
1315
|
+
}
|
|
1283
1316
|
|
|
1284
1317
|
/**
|
|
1285
1318
|
* Deep interpolation utility for string leaves.
|
|
@@ -1337,6 +1370,18 @@ const interpolateDeep = (value, envRef) => {
|
|
|
1337
1370
|
return value;
|
|
1338
1371
|
};
|
|
1339
1372
|
|
|
1373
|
+
/**
|
|
1374
|
+
* Instance-bound plugin config store.
|
|
1375
|
+
* Host stores the validated/interpolated slice per plugin instance.
|
|
1376
|
+
* The store is intentionally private to this module; definePlugin()
|
|
1377
|
+
* provides a typed accessor that reads from this store for the calling
|
|
1378
|
+
* plugin instance.
|
|
1379
|
+
*/
|
|
1380
|
+
const PLUGIN_CONFIG_STORE = new WeakMap();
|
|
1381
|
+
const _setPluginConfigForInstance = (plugin, cfg) => {
|
|
1382
|
+
PLUGIN_CONFIG_STORE.set(plugin, cfg);
|
|
1383
|
+
};
|
|
1384
|
+
const _getPluginConfigForInstance = (plugin) => PLUGIN_CONFIG_STORE.get(plugin);
|
|
1340
1385
|
/**
|
|
1341
1386
|
* Compute the dotenv context for the host (uses the config loader/overlay path).
|
|
1342
1387
|
* - Resolves and validates options strictly (host-only).
|
|
@@ -1345,20 +1390,26 @@ const interpolateDeep = (value, envRef) => {
|
|
|
1345
1390
|
*
|
|
1346
1391
|
* @param customOptions - Partial options from the current invocation.
|
|
1347
1392
|
* @param plugins - Installed plugins (for config validation).
|
|
1348
|
-
* @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery).
|
|
1393
|
+
* @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery).
|
|
1394
|
+
*/
|
|
1349
1395
|
const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
1350
1396
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1397
|
+
// Zod boundary: parse returns the schema-derived shape; we adopt our public
|
|
1398
|
+
// GetDotenvOptions overlay (logger/dynamic typing) for internal processing.
|
|
1351
1399
|
const validated = getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
1352
1400
|
// Always-on loader path
|
|
1353
1401
|
// 1) Base from files only (no dynamic, no programmatic vars)
|
|
1402
|
+
// Sanitize to avoid passing properties explicitly set to undefined.
|
|
1403
|
+
const cleanedValidated = omitUndefined(validated);
|
|
1354
1404
|
const base = await getDotenv({
|
|
1355
|
-
...
|
|
1405
|
+
...cleanedValidated,
|
|
1356
1406
|
// Build a pure base without side effects or logging.
|
|
1357
1407
|
excludeDynamic: true,
|
|
1358
1408
|
vars: {},
|
|
1359
1409
|
log: false,
|
|
1360
1410
|
loadProcess: false,
|
|
1361
|
-
outputPath
|
|
1411
|
+
// Intentionally omit outputPath for the base pass; including a key with
|
|
1412
|
+
// undefined would violate exactOptionalPropertyTypes on the Partial target.
|
|
1362
1413
|
});
|
|
1363
1414
|
// 2) Discover config sources and overlay
|
|
1364
1415
|
const sources = await resolveGetDotenvConfigSources(hostMetaUrl);
|
|
@@ -1403,7 +1454,7 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1403
1454
|
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
1404
1455
|
}, ''), { encoding: 'utf-8' });
|
|
1405
1456
|
}
|
|
1406
|
-
const logger =
|
|
1457
|
+
const logger = customOptions.logger ?? console;
|
|
1407
1458
|
if (validated.log)
|
|
1408
1459
|
logger.log(dotenv);
|
|
1409
1460
|
if (validated.loadProcess)
|
|
@@ -1418,43 +1469,148 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1418
1469
|
const localPlugins = (sources.project?.local &&
|
|
1419
1470
|
sources.project.local.plugins) ??
|
|
1420
1471
|
{};
|
|
1421
|
-
|
|
1472
|
+
// The by-id map is retained only for backwards-compat rendering paths
|
|
1473
|
+
// (root help dynamic evaluation). Instance-bound access is the source
|
|
1474
|
+
// of truth going forward and is populated below.
|
|
1475
|
+
const mergedPluginConfigsById = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
|
|
1422
1476
|
for (const p of plugins) {
|
|
1423
1477
|
if (!p.id)
|
|
1424
1478
|
continue;
|
|
1425
|
-
const slice =
|
|
1426
|
-
|
|
1427
|
-
continue;
|
|
1428
|
-
// Per-plugin interpolation just before validation/afterResolve:
|
|
1429
|
-
// precedence: process.env wins over ctx.dotenv for slice defaults.
|
|
1479
|
+
const slice = mergedPluginConfigsById[p.id];
|
|
1480
|
+
// Build interpolation reference once per plugin:
|
|
1430
1481
|
const envRef = {
|
|
1431
1482
|
...dotenv,
|
|
1432
1483
|
...process.env,
|
|
1433
1484
|
};
|
|
1434
|
-
const interpolated =
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1485
|
+
const interpolated = slice && typeof slice === 'object'
|
|
1486
|
+
? interpolateDeep(slice, envRef)
|
|
1487
|
+
: {};
|
|
1488
|
+
// Enforced: plugins always carry a schema (strict empty by default).
|
|
1489
|
+
// Zod v4: avoid legacy multi-generic usage; treat as generic ZodObject.
|
|
1490
|
+
const schema = p.configSchema;
|
|
1491
|
+
const toParse = interpolated;
|
|
1492
|
+
const parsed = schema.safeParse(toParse);
|
|
1493
|
+
if (!parsed.success) {
|
|
1494
|
+
const err = parsed.error;
|
|
1495
|
+
const msgs = err.issues
|
|
1496
|
+
.map((i) => {
|
|
1497
|
+
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1498
|
+
const msg = typeof i.message === 'string' ? i.message : 'Invalid value';
|
|
1499
|
+
return path ? `${path}: ${msg}` : msg;
|
|
1500
|
+
})
|
|
1501
|
+
.join('\n');
|
|
1502
|
+
throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
|
|
1448
1503
|
}
|
|
1504
|
+
// Store a readonly (shallow-frozen) value for runtime safety.
|
|
1505
|
+
const frozen = Object.freeze(parsed.data);
|
|
1506
|
+
_setPluginConfigForInstance(p, frozen);
|
|
1507
|
+
mergedPluginConfigsById[p.id] = frozen;
|
|
1449
1508
|
}
|
|
1450
1509
|
return {
|
|
1451
1510
|
optionsResolved: validated,
|
|
1452
1511
|
dotenv: dotenv,
|
|
1453
1512
|
plugins: {},
|
|
1454
|
-
|
|
1513
|
+
// Retained for legacy root help dynamic evaluation only. Instance-bound
|
|
1514
|
+
// access is used by plugins themselves and tests/docs moving forward.
|
|
1515
|
+
pluginConfigs: mergedPluginConfigsById,
|
|
1455
1516
|
};
|
|
1456
1517
|
};
|
|
1457
1518
|
|
|
1519
|
+
// Registry for dynamic descriptions keyed by Option (WeakMap so GC-friendly)
|
|
1520
|
+
const DYN_DESC = new WeakMap();
|
|
1521
|
+
/**
|
|
1522
|
+
* Create an Option with a dynamic description callback stored in DYN_DESC.
|
|
1523
|
+
*/
|
|
1524
|
+
function makeDynamicOption(flags, desc, parser, defaultValue) {
|
|
1525
|
+
const opt = new Option(flags, '');
|
|
1526
|
+
DYN_DESC.set(opt, desc);
|
|
1527
|
+
if (parser) {
|
|
1528
|
+
opt.argParser((value, previous) => parser(value, previous));
|
|
1529
|
+
}
|
|
1530
|
+
if (defaultValue !== undefined)
|
|
1531
|
+
opt.default(defaultValue);
|
|
1532
|
+
return opt;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Evaluate dynamic descriptions across a command tree using the resolved config.
|
|
1536
|
+
*/
|
|
1537
|
+
function evaluateDynamicOptions(root, resolved) {
|
|
1538
|
+
const visit = (cmd) => {
|
|
1539
|
+
const arr = cmd.options;
|
|
1540
|
+
for (const o of arr) {
|
|
1541
|
+
const dyn = DYN_DESC.get(o);
|
|
1542
|
+
if (typeof dyn === 'function') {
|
|
1543
|
+
try {
|
|
1544
|
+
const txt = dyn(resolved);
|
|
1545
|
+
// Commander uses Option.description during help rendering.
|
|
1546
|
+
o.description = txt;
|
|
1547
|
+
}
|
|
1548
|
+
catch {
|
|
1549
|
+
/* best-effort; leave as-is */
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
for (const c of cmd.commands)
|
|
1554
|
+
visit(c);
|
|
1555
|
+
};
|
|
1556
|
+
visit(root);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Registry for grouping; root help renders groups between Options and Commands.
|
|
1560
|
+
const GROUP_TAG = new WeakMap();
|
|
1561
|
+
function renderOptionGroups(cmd) {
|
|
1562
|
+
const all = cmd.options;
|
|
1563
|
+
const byGroup = new Map();
|
|
1564
|
+
for (const o of all) {
|
|
1565
|
+
const opt = o;
|
|
1566
|
+
const g = GROUP_TAG.get(opt);
|
|
1567
|
+
if (!g || g === 'base')
|
|
1568
|
+
continue; // base handled by default help
|
|
1569
|
+
const rows = byGroup.get(g) ?? [];
|
|
1570
|
+
rows.push({
|
|
1571
|
+
flags: opt.flags,
|
|
1572
|
+
description: opt.description ?? '',
|
|
1573
|
+
});
|
|
1574
|
+
byGroup.set(g, rows);
|
|
1575
|
+
}
|
|
1576
|
+
if (byGroup.size === 0)
|
|
1577
|
+
return '';
|
|
1578
|
+
const renderRows = (title, rows) => {
|
|
1579
|
+
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
1580
|
+
// Sort within group: short-aliased flags first
|
|
1581
|
+
rows.sort((a, b) => {
|
|
1582
|
+
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
1583
|
+
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
1584
|
+
return bS - aS || a.flags.localeCompare(b.flags);
|
|
1585
|
+
});
|
|
1586
|
+
const lines = rows
|
|
1587
|
+
.map((r) => {
|
|
1588
|
+
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
1589
|
+
return ` ${r.flags}${pad}${r.description}`.trimEnd();
|
|
1590
|
+
})
|
|
1591
|
+
.join('\n');
|
|
1592
|
+
return `\n${title}:\n${lines}\n`;
|
|
1593
|
+
};
|
|
1594
|
+
let out = '';
|
|
1595
|
+
// App options (if any)
|
|
1596
|
+
const app = byGroup.get('app');
|
|
1597
|
+
if (app && app.length > 0) {
|
|
1598
|
+
out += renderRows('App options', app);
|
|
1599
|
+
}
|
|
1600
|
+
// Plugin groups sorted by id; suppress self group on the owning command name.
|
|
1601
|
+
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1602
|
+
const currentName = cmd.name();
|
|
1603
|
+
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1604
|
+
for (const k of pluginKeys) {
|
|
1605
|
+
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1606
|
+
const rows = byGroup.get(k) ?? [];
|
|
1607
|
+
if (rows.length > 0 && id !== currentName) {
|
|
1608
|
+
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return out;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1458
1614
|
const HOST_META_URL = import.meta.url;
|
|
1459
1615
|
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
1460
1616
|
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
@@ -1467,8 +1623,6 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
|
1467
1623
|
* - Expose a stable accessor for the current context (getCtx).
|
|
1468
1624
|
* - Provide a namespacing helper (ns).
|
|
1469
1625
|
* - Support composable plugins with parent → children install and afterResolve.
|
|
1470
|
-
*
|
|
1471
|
-
* NOTE: This host is additive and does not alter the legacy CLI.
|
|
1472
1626
|
*/
|
|
1473
1627
|
let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
1474
1628
|
/** Registered top-level plugins (composition happens via .use()) */
|
|
@@ -1477,6 +1631,17 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1477
1631
|
_installed = false;
|
|
1478
1632
|
/** Optional header line to prepend in help output */
|
|
1479
1633
|
[HELP_HEADER_SYMBOL];
|
|
1634
|
+
/** Context/options stored under symbols (typed) */
|
|
1635
|
+
[CTX_SYMBOL];
|
|
1636
|
+
[OPTS_SYMBOL];
|
|
1637
|
+
/**
|
|
1638
|
+
* Create a subcommand using the same subclass, preserving helpers like
|
|
1639
|
+
* dynamicOption on children.
|
|
1640
|
+
*/
|
|
1641
|
+
createCommand(name) {
|
|
1642
|
+
// Explicitly construct a GetDotenvCli (drop subclass constructor semantics).
|
|
1643
|
+
return new GetDotenvCli(name);
|
|
1644
|
+
}
|
|
1480
1645
|
constructor(alias = 'getdotenv') {
|
|
1481
1646
|
super(alias);
|
|
1482
1647
|
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
@@ -1484,37 +1649,39 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1484
1649
|
// child uses passThroughOptions.
|
|
1485
1650
|
this.enablePositionalOptions();
|
|
1486
1651
|
// Configure grouped help: show only base options in default "Options";
|
|
1487
|
-
//
|
|
1652
|
+
// we will insert App/Plugin sections before Commands in helpInformation().
|
|
1488
1653
|
this.configureHelp({
|
|
1489
1654
|
visibleOptions: (cmd) => {
|
|
1490
|
-
const all = cmd.options
|
|
1491
|
-
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1655
|
+
const all = cmd.options;
|
|
1656
|
+
const isRoot = cmd.parent === null;
|
|
1657
|
+
const list = isRoot
|
|
1658
|
+
? all.filter((opt) => {
|
|
1659
|
+
const group = GROUP_TAG.get(opt);
|
|
1660
|
+
return group === 'base';
|
|
1661
|
+
})
|
|
1662
|
+
: all.slice(); // subcommands: show all options (their own "Options:" block)
|
|
1496
1663
|
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1497
1664
|
const hasShort = (opt) => {
|
|
1498
|
-
const flags = opt.flags
|
|
1665
|
+
const flags = opt.flags;
|
|
1499
1666
|
// Matches "-x," or starting "-x " before any long
|
|
1500
1667
|
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1501
1668
|
};
|
|
1502
|
-
const byFlags = (opt) => opt.flags
|
|
1503
|
-
|
|
1669
|
+
const byFlags = (opt) => opt.flags;
|
|
1670
|
+
list.sort((a, b) => {
|
|
1504
1671
|
const aS = hasShort(a) ? 1 : 0;
|
|
1505
1672
|
const bS = hasShort(b) ? 1 : 0;
|
|
1506
1673
|
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1507
1674
|
});
|
|
1508
|
-
return
|
|
1675
|
+
return list;
|
|
1509
1676
|
},
|
|
1510
1677
|
});
|
|
1511
1678
|
this.addHelpText('beforeAll', () => {
|
|
1512
1679
|
const header = this[HELP_HEADER_SYMBOL];
|
|
1513
1680
|
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1514
1681
|
});
|
|
1515
|
-
this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
|
|
1516
1682
|
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
1517
|
-
// mutating process.env. The passOptions hook (when installed) will
|
|
1683
|
+
// mutating process.env. The passOptions hook (when installed) will
|
|
1684
|
+
// compute the final context using merged CLI options; keeping
|
|
1518
1685
|
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
1519
1686
|
// process env before subcommands execute.
|
|
1520
1687
|
this.hook('preSubcommand', async () => {
|
|
@@ -1524,22 +1691,49 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1524
1691
|
});
|
|
1525
1692
|
}
|
|
1526
1693
|
/**
|
|
1527
|
-
* Resolve options (strict) and compute dotenv context.
|
|
1694
|
+
* Resolve options (strict) and compute dotenv context.
|
|
1695
|
+
* Stores the context on the instance under a symbol.
|
|
1696
|
+
*
|
|
1697
|
+
* Options:
|
|
1698
|
+
* - opts.runAfterResolve (default true): when false, skips running plugin
|
|
1699
|
+
* afterResolve hooks. Useful for top-level help rendering to avoid
|
|
1700
|
+
* long-running side-effects while still evaluating dynamic help text.
|
|
1528
1701
|
*/
|
|
1529
|
-
async resolveAndLoad(customOptions = {}) {
|
|
1702
|
+
async resolveAndLoad(customOptions = {}, opts) {
|
|
1530
1703
|
// Resolve defaults, then validate strictly under the new host.
|
|
1531
1704
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1532
1705
|
getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
1533
1706
|
// Delegate the heavy lifting to the shared helper (guarded path supported).
|
|
1534
1707
|
const ctx = await computeContext(optionsResolved, this._plugins, HOST_META_URL);
|
|
1535
1708
|
// Persist context on the instance for later access.
|
|
1536
|
-
this[CTX_SYMBOL] =
|
|
1537
|
-
ctx;
|
|
1709
|
+
this[CTX_SYMBOL] = ctx;
|
|
1538
1710
|
// Ensure plugins are installed exactly once, then run afterResolve.
|
|
1539
1711
|
await this.install();
|
|
1540
|
-
|
|
1712
|
+
if (opts?.runAfterResolve ?? true) {
|
|
1713
|
+
await this._runAfterResolve(ctx);
|
|
1714
|
+
}
|
|
1541
1715
|
return ctx;
|
|
1542
1716
|
}
|
|
1717
|
+
// Implementation
|
|
1718
|
+
createDynamicOption(flags, desc, parser, defaultValue) {
|
|
1719
|
+
return makeDynamicOption(flags, (c) => desc(c), parser, defaultValue);
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Chainable helper mirroring .option(), but with a dynamic description.
|
|
1723
|
+
* Equivalent to addOption(createDynamicOption(...)).
|
|
1724
|
+
*/
|
|
1725
|
+
dynamicOption(flags, desc, parser, defaultValue) {
|
|
1726
|
+
this.addOption(this.createDynamicOption(flags, desc, parser, defaultValue));
|
|
1727
|
+
return this;
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Evaluate dynamic descriptions for this command and all descendants using
|
|
1731
|
+
* the provided resolved configuration. Mutates the Option.description in
|
|
1732
|
+
* place so Commander help renders updated text.
|
|
1733
|
+
*/
|
|
1734
|
+
evaluateDynamicOptions(resolved) {
|
|
1735
|
+
evaluateDynamicOptions(this, resolved);
|
|
1736
|
+
}
|
|
1543
1737
|
/**
|
|
1544
1738
|
* Retrieve the current invocation context (if any).
|
|
1545
1739
|
*/
|
|
@@ -1557,9 +1751,13 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1557
1751
|
_setOptionsBag(bag) {
|
|
1558
1752
|
this[OPTS_SYMBOL] = bag;
|
|
1559
1753
|
}
|
|
1560
|
-
/**
|
|
1561
|
-
*/
|
|
1754
|
+
/** Convenience helper to create a namespaced subcommand. */
|
|
1562
1755
|
ns(name) {
|
|
1756
|
+
// Guard against same-level duplicate command names for clearer diagnostics.
|
|
1757
|
+
const exists = this.commands.some((c) => c.name() === name);
|
|
1758
|
+
if (exists) {
|
|
1759
|
+
throw new Error(`Duplicate command name: ${name}`);
|
|
1760
|
+
}
|
|
1563
1761
|
return this.command(name);
|
|
1564
1762
|
}
|
|
1565
1763
|
/**
|
|
@@ -1569,29 +1767,15 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1569
1767
|
tagAppOptions(fn) {
|
|
1570
1768
|
const root = this;
|
|
1571
1769
|
const originalAddOption = root.addOption.bind(root);
|
|
1572
|
-
const originalOption = root.option.bind(root);
|
|
1573
|
-
const tagLatest = (cmd, group) => {
|
|
1574
|
-
const optsArr = cmd.options;
|
|
1575
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
1576
|
-
const last = optsArr[optsArr.length - 1];
|
|
1577
|
-
last.__group = group;
|
|
1578
|
-
}
|
|
1579
|
-
};
|
|
1580
1770
|
root.addOption = function patchedAdd(opt) {
|
|
1581
|
-
opt
|
|
1771
|
+
root.setOptionGroup(opt, 'app');
|
|
1582
1772
|
return originalAddOption(opt);
|
|
1583
1773
|
};
|
|
1584
|
-
root.option = function patchedOption(...args) {
|
|
1585
|
-
const ret = originalOption(...args);
|
|
1586
|
-
tagLatest(this, 'app');
|
|
1587
|
-
return ret;
|
|
1588
|
-
};
|
|
1589
1774
|
try {
|
|
1590
1775
|
return fn(root);
|
|
1591
1776
|
}
|
|
1592
1777
|
finally {
|
|
1593
1778
|
root.addOption = originalAddOption;
|
|
1594
|
-
root.option = originalOption;
|
|
1595
1779
|
}
|
|
1596
1780
|
}
|
|
1597
1781
|
/**
|
|
@@ -1630,12 +1814,51 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1630
1814
|
this[HELP_HEADER_SYMBOL] = helpHeader;
|
|
1631
1815
|
}
|
|
1632
1816
|
else if (v) {
|
|
1633
|
-
// Use the current command name (possibly overridden by 'name' above).
|
|
1634
1817
|
const header = `${this.name()} v${v}`;
|
|
1635
1818
|
this[HELP_HEADER_SYMBOL] = header;
|
|
1636
1819
|
}
|
|
1637
1820
|
return this;
|
|
1638
1821
|
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Insert grouped plugin/app options between "Options" and "Commands" for
|
|
1824
|
+
* hybrid ordering. Applies to root and any parent command.
|
|
1825
|
+
*/
|
|
1826
|
+
helpInformation() {
|
|
1827
|
+
// Base help text first (includes beforeAll/after hooks).
|
|
1828
|
+
const base = super.helpInformation();
|
|
1829
|
+
const groups = renderOptionGroups(this);
|
|
1830
|
+
const block = typeof groups === 'string' ? groups.trim() : '';
|
|
1831
|
+
let out = base;
|
|
1832
|
+
if (!block) {
|
|
1833
|
+
// Ensure a trailing blank line even when no extra groups render.
|
|
1834
|
+
if (!out.endsWith('\n\n'))
|
|
1835
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1836
|
+
return out;
|
|
1837
|
+
}
|
|
1838
|
+
// Insert just before "Commands:" when present.
|
|
1839
|
+
const marker = '\nCommands:';
|
|
1840
|
+
const idx = base.indexOf(marker);
|
|
1841
|
+
if (idx >= 0) {
|
|
1842
|
+
const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
|
|
1843
|
+
out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
|
|
1844
|
+
}
|
|
1845
|
+
else {
|
|
1846
|
+
// Otherwise append.
|
|
1847
|
+
const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
|
|
1848
|
+
out = `${base}${sep}${groups}`;
|
|
1849
|
+
}
|
|
1850
|
+
// Ensure a trailing blank line for prompt separation.
|
|
1851
|
+
if (!out.endsWith('\n\n')) {
|
|
1852
|
+
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1853
|
+
}
|
|
1854
|
+
return out;
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Public: tag an Option with a display group for help (root/app/plugin:<id>).
|
|
1858
|
+
*/
|
|
1859
|
+
setOptionGroup(opt, group) {
|
|
1860
|
+
GROUP_TAG.set(opt, group);
|
|
1861
|
+
}
|
|
1639
1862
|
/**
|
|
1640
1863
|
* Register a plugin for installation (parent level).
|
|
1641
1864
|
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
@@ -1674,58 +1897,18 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1674
1897
|
for (const p of this._plugins)
|
|
1675
1898
|
await run(p);
|
|
1676
1899
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
description: opt.description ?? '',
|
|
1690
|
-
});
|
|
1691
|
-
byGroup.set(g, rows);
|
|
1692
|
-
}
|
|
1693
|
-
if (byGroup.size === 0)
|
|
1694
|
-
return '';
|
|
1695
|
-
const renderRows = (title, rows) => {
|
|
1696
|
-
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
1697
|
-
// Sort within group: short-aliased flags first
|
|
1698
|
-
rows.sort((a, b) => {
|
|
1699
|
-
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
1700
|
-
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
1701
|
-
return bS - aS || a.flags.localeCompare(b.flags);
|
|
1702
|
-
});
|
|
1703
|
-
const lines = rows
|
|
1704
|
-
.map((r) => {
|
|
1705
|
-
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
1706
|
-
return ` ${r.flags}${pad}${r.description}`.trimEnd();
|
|
1707
|
-
})
|
|
1708
|
-
.join('\n');
|
|
1709
|
-
return `\n${title}:\n${lines}\n`;
|
|
1710
|
-
};
|
|
1711
|
-
let out = '';
|
|
1712
|
-
// App options (if any)
|
|
1713
|
-
const app = byGroup.get('app');
|
|
1714
|
-
if (app && app.length > 0) {
|
|
1715
|
-
out += renderRows('App options', app);
|
|
1716
|
-
}
|
|
1717
|
-
// Plugin groups sorted by id
|
|
1718
|
-
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1719
|
-
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1720
|
-
for (const k of pluginKeys) {
|
|
1721
|
-
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1722
|
-
const rows = byGroup.get(k) ?? [];
|
|
1723
|
-
if (rows.length > 0) {
|
|
1724
|
-
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
return out;
|
|
1728
|
-
}
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Build a help-time configuration bag for dynamic option descriptions.
|
|
1904
|
+
* Centralizes construction and reduces inline casts at call sites.
|
|
1905
|
+
*/
|
|
1906
|
+
const toHelpConfig = (merged, plugins) => {
|
|
1907
|
+
const bag = {
|
|
1908
|
+
...merged,
|
|
1909
|
+
plugins: plugins ?? {},
|
|
1910
|
+
};
|
|
1911
|
+
return bag;
|
|
1729
1912
|
};
|
|
1730
1913
|
|
|
1731
1914
|
/** src/cliHost/definePlugin.ts
|
|
@@ -1735,26 +1918,59 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1735
1918
|
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
1736
1919
|
* nominal class identity issues (private fields) in downstream consumers.
|
|
1737
1920
|
*/
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
*
|
|
1741
|
-
* @example
|
|
1742
|
-
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
1743
|
-
* .use(childA)
|
|
1744
|
-
* .use(childB);
|
|
1745
|
-
*/
|
|
1746
|
-
const definePlugin = (spec) => {
|
|
1921
|
+
/* eslint-disable tsdoc/syntax */
|
|
1922
|
+
function definePlugin(spec) {
|
|
1747
1923
|
const { children = [], ...rest } = spec;
|
|
1748
|
-
|
|
1924
|
+
// Default to a strict empty-object schema so “no-config” plugins fail fast
|
|
1925
|
+
// on unknown keys and provide a concrete {} at runtime.
|
|
1926
|
+
const effectiveSchema = spec.configSchema ?? z.object({}).strict();
|
|
1927
|
+
// Build base plugin first, then extend with instance-bound helpers.
|
|
1928
|
+
const base = {
|
|
1749
1929
|
...rest,
|
|
1930
|
+
// Always carry a schema (strict empty by default) to simplify host logic
|
|
1931
|
+
// and improve inference/ergonomics for plugin authors.
|
|
1932
|
+
configSchema: effectiveSchema,
|
|
1750
1933
|
children: [...children],
|
|
1751
1934
|
use(child) {
|
|
1752
1935
|
this.children.push(child);
|
|
1753
1936
|
return this;
|
|
1754
1937
|
},
|
|
1755
1938
|
};
|
|
1756
|
-
|
|
1757
|
-
|
|
1939
|
+
// Attach instance-bound helpers on the returned plugin object.
|
|
1940
|
+
const extended = base;
|
|
1941
|
+
extended.readConfig = function (_cli) {
|
|
1942
|
+
// Config is stored per-plugin-instance by the host (WeakMap in computeContext).
|
|
1943
|
+
const value = _getPluginConfigForInstance(extended);
|
|
1944
|
+
if (value === undefined) {
|
|
1945
|
+
// Guard: host has not resolved config yet (incorrect lifecycle usage).
|
|
1946
|
+
throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
|
|
1947
|
+
}
|
|
1948
|
+
return value;
|
|
1949
|
+
};
|
|
1950
|
+
// Plugin-bound dynamic option factory
|
|
1951
|
+
extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
|
|
1952
|
+
return cli.createDynamicOption(flags, (cfg) => {
|
|
1953
|
+
// Prefer the validated slice stored per instance; fallback to help-bag
|
|
1954
|
+
// (by-id) so top-level `-h` can render effective defaults before resolve.
|
|
1955
|
+
const fromStore = _getPluginConfigForInstance(extended);
|
|
1956
|
+
const id = extended.id;
|
|
1957
|
+
let fromBag;
|
|
1958
|
+
if (!fromStore && id) {
|
|
1959
|
+
const maybe = cfg.plugins[id];
|
|
1960
|
+
if (maybe && typeof maybe === 'object') {
|
|
1961
|
+
fromBag = maybe;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
// Always provide a concrete object to dynamic callbacks:
|
|
1965
|
+
// - With a schema: computeContext stores the parsed object.
|
|
1966
|
+
// - Without a schema: computeContext stores {}.
|
|
1967
|
+
// - Help-time fallback: coalesce to {} when only a by-id bag exists.
|
|
1968
|
+
const cfgVal = (fromStore ?? fromBag ?? {});
|
|
1969
|
+
return desc(cfg, cfgVal);
|
|
1970
|
+
}, parser, defaultValue);
|
|
1971
|
+
};
|
|
1972
|
+
return extended;
|
|
1973
|
+
}
|
|
1758
1974
|
|
|
1759
1975
|
/**
|
|
1760
1976
|
* GetDotenvCli with root helpers as real class methods.
|
|
@@ -1767,9 +1983,9 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1767
1983
|
* Attach legacy root flags to this CLI instance. Defaults come from
|
|
1768
1984
|
* baseRootOptionDefaults when none are provided.
|
|
1769
1985
|
*/
|
|
1770
|
-
attachRootOptions(defaults
|
|
1986
|
+
attachRootOptions(defaults) {
|
|
1771
1987
|
const d = (defaults ?? baseRootOptionDefaults);
|
|
1772
|
-
attachRootOptions(this, d
|
|
1988
|
+
attachRootOptions(this, d);
|
|
1773
1989
|
return this;
|
|
1774
1990
|
}
|
|
1775
1991
|
/**
|
|
@@ -1791,10 +2007,19 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1791
2007
|
// Build service options and compute context (always-on loader path).
|
|
1792
2008
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1793
2009
|
await this.resolveAndLoad(serviceOptions);
|
|
2010
|
+
// Refresh dynamic option descriptions using resolved config + plugin slices
|
|
2011
|
+
try {
|
|
2012
|
+
const ctx = this.getCtx();
|
|
2013
|
+
const helpCfg = toHelpConfig(merged, ctx?.pluginConfigs ?? {});
|
|
2014
|
+
this.evaluateDynamicOptions(helpCfg);
|
|
2015
|
+
}
|
|
2016
|
+
catch {
|
|
2017
|
+
/* best-effort */
|
|
2018
|
+
}
|
|
1794
2019
|
// Global validation: once after Phase C using config sources.
|
|
1795
2020
|
try {
|
|
1796
2021
|
const ctx = this.getCtx();
|
|
1797
|
-
const dotenv =
|
|
2022
|
+
const dotenv = ctx?.dotenv ?? {};
|
|
1798
2023
|
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
1799
2024
|
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1800
2025
|
if (Array.isArray(issues) && issues.length > 0) {
|
|
@@ -1804,9 +2029,8 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1804
2029
|
issues.forEach((m) => {
|
|
1805
2030
|
emit(m);
|
|
1806
2031
|
});
|
|
1807
|
-
if (merged.strict)
|
|
2032
|
+
if (merged.strict)
|
|
1808
2033
|
process.exit(1);
|
|
1809
|
-
}
|
|
1810
2034
|
}
|
|
1811
2035
|
}
|
|
1812
2036
|
catch {
|
|
@@ -1825,6 +2049,14 @@ class GetDotenvCli extends GetDotenvCli$1 {
|
|
|
1825
2049
|
if (!this.getCtx()) {
|
|
1826
2050
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1827
2051
|
await this.resolveAndLoad(serviceOptions);
|
|
2052
|
+
try {
|
|
2053
|
+
const ctx = this.getCtx();
|
|
2054
|
+
const helpCfg = toHelpConfig(merged, ctx?.pluginConfigs ?? {});
|
|
2055
|
+
this.evaluateDynamicOptions(helpCfg);
|
|
2056
|
+
}
|
|
2057
|
+
catch {
|
|
2058
|
+
/* tolerate */
|
|
2059
|
+
}
|
|
1828
2060
|
try {
|
|
1829
2061
|
const ctx = this.getCtx();
|
|
1830
2062
|
const dotenv = (ctx?.dotenv ?? {});
|