@karmaniverous/get-dotenv 5.2.4 → 5.2.6
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 +5 -5
- package/dist/cliHost.cjs +678 -271
- package/dist/cliHost.d.cts +124 -60
- package/dist/cliHost.d.mts +124 -60
- package/dist/cliHost.d.ts +124 -60
- package/dist/cliHost.mjs +679 -272
- package/dist/getdotenv.cli.mjs +647 -631
- package/dist/index.cjs +694 -678
- package/dist/index.d.cts +18 -39
- package/dist/index.d.mts +18 -39
- package/dist/index.d.ts +18 -39
- package/dist/index.mjs +695 -679
- package/dist/plugins-aws.d.cts +8 -8
- package/dist/plugins-aws.d.mts +8 -8
- package/dist/plugins-aws.d.ts +8 -8
- package/dist/plugins-batch.d.cts +8 -8
- package/dist/plugins-batch.d.mts +8 -8
- package/dist/plugins-batch.d.ts +8 -8
- package/dist/plugins-cmd.cjs +159 -1551
- package/dist/plugins-cmd.d.cts +8 -8
- package/dist/plugins-cmd.d.mts +8 -8
- package/dist/plugins-cmd.d.ts +8 -8
- package/dist/plugins-cmd.mjs +160 -1551
- package/dist/plugins-demo.d.cts +8 -8
- package/dist/plugins-demo.d.mts +8 -8
- package/dist/plugins-demo.d.ts +8 -8
- package/dist/plugins-init.d.cts +8 -8
- package/dist/plugins-init.d.mts +8 -8
- package/dist/plugins-init.d.ts +8 -8
- package/dist/plugins.cjs +156 -1547
- package/dist/plugins.d.cts +8 -8
- package/dist/plugins.d.mts +8 -8
- package/dist/plugins.d.ts +8 -8
- package/dist/plugins.mjs +158 -1548
- package/package.json +1 -1
package/dist/getdotenv.cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { Option, Command } from 'commander';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { packageDirectory } from 'package-directory';
|
|
5
|
-
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
6
5
|
import path, { join, extname } from 'path';
|
|
7
|
-
import {
|
|
6
|
+
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
8
7
|
import YAML from 'yaml';
|
|
8
|
+
import { z } from 'zod';
|
|
9
9
|
import { nanoid } from 'nanoid';
|
|
10
10
|
import { parse } from 'dotenv';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
@@ -14,6 +14,258 @@ import { globby } from 'globby';
|
|
|
14
14
|
import { stdin, stdout } from 'node:process';
|
|
15
15
|
import { createInterface } from 'readline/promises';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Dotenv expansion utilities.
|
|
19
|
+
*
|
|
20
|
+
* This module implements recursive expansion of environment-variable
|
|
21
|
+
* references in strings and records. It supports both whitespace and
|
|
22
|
+
* bracket syntaxes with optional defaults:
|
|
23
|
+
*
|
|
24
|
+
* - Whitespace: `$VAR[:default]`
|
|
25
|
+
* - Bracketed: `${VAR[:default]}`
|
|
26
|
+
*
|
|
27
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
28
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Like String.prototype.search but returns the last index.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
const searchLast = (str, rgx) => {
|
|
35
|
+
const matches = Array.from(str.matchAll(rgx));
|
|
36
|
+
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
37
|
+
};
|
|
38
|
+
const replaceMatch = (value, match, ref) => {
|
|
39
|
+
/**
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
const group = match[0];
|
|
43
|
+
const key = match[1];
|
|
44
|
+
const defaultValue = match[2];
|
|
45
|
+
if (!key)
|
|
46
|
+
return value;
|
|
47
|
+
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
48
|
+
return interpolate(replacement, ref);
|
|
49
|
+
};
|
|
50
|
+
const interpolate = (value = '', ref = {}) => {
|
|
51
|
+
/**
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
// if value is falsy, return it as is
|
|
55
|
+
if (!value)
|
|
56
|
+
return value;
|
|
57
|
+
// get position of last unescaped dollar sign
|
|
58
|
+
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
59
|
+
// return value if none found
|
|
60
|
+
if (lastUnescapedDollarSignIndex === -1)
|
|
61
|
+
return value;
|
|
62
|
+
// evaluate the value tail
|
|
63
|
+
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
64
|
+
// find whitespace pattern: $KEY:DEFAULT
|
|
65
|
+
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
66
|
+
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
67
|
+
if (whitespaceMatch != null)
|
|
68
|
+
return replaceMatch(value, whitespaceMatch, ref);
|
|
69
|
+
else {
|
|
70
|
+
// find bracket pattern: ${KEY:DEFAULT}
|
|
71
|
+
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
72
|
+
const bracketMatch = bracketPattern.exec(tail);
|
|
73
|
+
if (bracketMatch != null)
|
|
74
|
+
return replaceMatch(value, bracketMatch, ref);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Recursively expands environment variables in a string. Variables may be
|
|
80
|
+
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
81
|
+
* Unknown variables will expand to an empty string.
|
|
82
|
+
*
|
|
83
|
+
* @param value - The string to expand.
|
|
84
|
+
* @param ref - The reference object to use for variable expansion.
|
|
85
|
+
* @returns The expanded string.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* process.env.FOO = 'bar';
|
|
90
|
+
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
91
|
+
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @remarks
|
|
95
|
+
* The expansion is recursive. If a referenced variable itself contains
|
|
96
|
+
* references, those will also be expanded until a stable value is reached.
|
|
97
|
+
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
98
|
+
*/
|
|
99
|
+
const dotenvExpand = (value, ref = process.env) => {
|
|
100
|
+
const result = interpolate(value, ref);
|
|
101
|
+
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Recursively expands environment variables in the values of a JSON object.
|
|
105
|
+
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
106
|
+
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
107
|
+
*
|
|
108
|
+
* @param values - The values object to expand.
|
|
109
|
+
* @param options - Expansion options.
|
|
110
|
+
* @returns The value object with expanded string values.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* process.env.FOO = 'bar';
|
|
115
|
+
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
116
|
+
* // => { A: "bar", B: "xbary" }
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @remarks
|
|
120
|
+
* Options:
|
|
121
|
+
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
122
|
+
* - progressive: Whether to progressively add expanded values to the set of
|
|
123
|
+
* reference keys.
|
|
124
|
+
*
|
|
125
|
+
* When `progressive` is true, each expanded key becomes available for
|
|
126
|
+
* subsequent expansions in the same object (left-to-right by object key order).
|
|
127
|
+
*/
|
|
128
|
+
const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
|
|
129
|
+
const { ref = process.env, progressive = false } = options;
|
|
130
|
+
acc[key] = dotenvExpand(values[key], {
|
|
131
|
+
...ref,
|
|
132
|
+
...(progressive ? acc : {}),
|
|
133
|
+
});
|
|
134
|
+
return acc;
|
|
135
|
+
}, {});
|
|
136
|
+
/**
|
|
137
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
138
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
139
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
140
|
+
* empty string.
|
|
141
|
+
*
|
|
142
|
+
* @param value - The string to expand.
|
|
143
|
+
* @returns The expanded string.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* process.env.FOO = 'bar';
|
|
148
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Attach legacy root flags to a Commander program.
|
|
155
|
+
* Uses provided defaults to render help labels without coupling to generators.
|
|
156
|
+
*/
|
|
157
|
+
const attachRootOptions = (program, defaults, opts) => {
|
|
158
|
+
// Install temporary wrappers to tag all options added here as "base".
|
|
159
|
+
const GROUP = 'base';
|
|
160
|
+
const tagLatest = (cmd, group) => {
|
|
161
|
+
const optsArr = cmd.options;
|
|
162
|
+
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
163
|
+
const last = optsArr[optsArr.length - 1];
|
|
164
|
+
last.__group = group;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const originalAddOption = program.addOption.bind(program);
|
|
168
|
+
const originalOption = program.option.bind(program);
|
|
169
|
+
program.addOption = function patchedAdd(opt) {
|
|
170
|
+
// Tag before adding, in case consumers inspect the Option directly.
|
|
171
|
+
opt.__group = GROUP;
|
|
172
|
+
const ret = originalAddOption(opt);
|
|
173
|
+
return ret;
|
|
174
|
+
};
|
|
175
|
+
program.option = function patchedOption(...args) {
|
|
176
|
+
const ret = originalOption(...args);
|
|
177
|
+
tagLatest(this, GROUP);
|
|
178
|
+
return ret;
|
|
179
|
+
};
|
|
180
|
+
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
181
|
+
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
182
|
+
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
183
|
+
// Build initial chain.
|
|
184
|
+
let p = program
|
|
185
|
+
.enablePositionalOptions()
|
|
186
|
+
.passThroughOptions()
|
|
187
|
+
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
188
|
+
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
189
|
+
['KEY1', 'VAL1'],
|
|
190
|
+
['KEY2', 'VAL2'],
|
|
191
|
+
]
|
|
192
|
+
.map((v) => v.join(va))
|
|
193
|
+
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
194
|
+
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
195
|
+
// Default is OFF; the generator opts in explicitly.
|
|
196
|
+
if (opts?.includeCommandOption === true) {
|
|
197
|
+
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
198
|
+
}
|
|
199
|
+
p = p
|
|
200
|
+
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
201
|
+
.addOption(new Option('-s, --shell [string]', (() => {
|
|
202
|
+
let defaultLabel = '';
|
|
203
|
+
if (shell !== undefined) {
|
|
204
|
+
if (typeof shell === 'boolean') {
|
|
205
|
+
defaultLabel = ' (default OS shell)';
|
|
206
|
+
}
|
|
207
|
+
else if (typeof shell === 'string') {
|
|
208
|
+
// Safe string interpolation
|
|
209
|
+
defaultLabel = ` (default ${shell})`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
213
|
+
})()).conflicts('shellOff'))
|
|
214
|
+
.addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
215
|
+
.addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
216
|
+
.addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
217
|
+
.addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
218
|
+
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
219
|
+
? ' (default)'
|
|
220
|
+
: ''}`).conflicts('excludeAllOff'))
|
|
221
|
+
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
222
|
+
.addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
223
|
+
.addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
224
|
+
.addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
225
|
+
.addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
226
|
+
.addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
227
|
+
.addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
228
|
+
.addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
229
|
+
.addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
230
|
+
.addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
231
|
+
.addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
232
|
+
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
233
|
+
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
234
|
+
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
235
|
+
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
236
|
+
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
237
|
+
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
238
|
+
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
239
|
+
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
240
|
+
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
241
|
+
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
242
|
+
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
243
|
+
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
244
|
+
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
245
|
+
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
246
|
+
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
247
|
+
// Hidden scripts pipe-through (stringified)
|
|
248
|
+
.addOption(new Option('--scripts <string>')
|
|
249
|
+
.default(JSON.stringify(scripts))
|
|
250
|
+
.hideHelp());
|
|
251
|
+
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
252
|
+
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
253
|
+
// Validation: strict mode fails on env validation issues (warn by default).
|
|
254
|
+
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
255
|
+
// Entropy diagnostics (presentation-only)
|
|
256
|
+
p = p
|
|
257
|
+
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
258
|
+
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
259
|
+
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
260
|
+
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
261
|
+
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
262
|
+
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
263
|
+
// Restore original methods to avoid tagging future additions outside base.
|
|
264
|
+
program.addOption = originalAddOption;
|
|
265
|
+
program.option = originalOption;
|
|
266
|
+
return p;
|
|
267
|
+
};
|
|
268
|
+
|
|
17
269
|
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
18
270
|
const baseRootOptionDefaults = {
|
|
19
271
|
dotenvToken: '.env',
|
|
@@ -41,8 +293,6 @@ const baseRootOptionDefaults = {
|
|
|
41
293
|
// (debug/log/exclude* resolved via flag utils)
|
|
42
294
|
};
|
|
43
295
|
|
|
44
|
-
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
45
|
-
|
|
46
296
|
/** @internal */
|
|
47
297
|
const isPlainObject$1 = (value) => value !== null &&
|
|
48
298
|
typeof value === 'object' &&
|
|
@@ -85,134 +335,130 @@ const defaultsDeep = (...layers) => {
|
|
|
85
335
|
return result;
|
|
86
336
|
};
|
|
87
337
|
|
|
88
|
-
// src/GetDotenvOptions.ts
|
|
89
|
-
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
90
338
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
339
|
+
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
340
|
+
* - If the user explicitly enabled the flag, return true.
|
|
341
|
+
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
342
|
+
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
93
343
|
*
|
|
94
|
-
* @
|
|
344
|
+
* @param exclude - The "on" flag value as parsed by Commander.
|
|
345
|
+
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
346
|
+
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
347
|
+
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```ts
|
|
351
|
+
* resolveExclusion(undefined, undefined, true); // => true
|
|
352
|
+
* ```
|
|
95
353
|
*/
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
* The result preserves explicit empty values and drops only `undefined`.
|
|
156
|
-
*
|
|
157
|
-
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* ```ts
|
|
161
|
-
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
162
|
-
* ```
|
|
163
|
-
*/
|
|
164
|
-
const localPkgDir = await packageDirectory();
|
|
165
|
-
const localOptionsPath = localPkgDir
|
|
166
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
167
|
-
: undefined;
|
|
168
|
-
const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
|
|
169
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
170
|
-
: {});
|
|
171
|
-
// Merge order: base < local < custom (custom has highest precedence)
|
|
172
|
-
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
173
|
-
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
174
|
-
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
175
|
-
return {
|
|
176
|
-
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
177
|
-
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
178
|
-
};
|
|
354
|
+
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
355
|
+
/**
|
|
356
|
+
* Resolve an optional flag with "--exclude-all" overrides.
|
|
357
|
+
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
358
|
+
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
359
|
+
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
360
|
+
*
|
|
361
|
+
* @param exclude - Individual include/exclude flag.
|
|
362
|
+
* @param excludeOff - Individual "...-off" flag.
|
|
363
|
+
* @param defaultValue - Default for the individual flag.
|
|
364
|
+
* @param excludeAll - Global "exclude-all" flag.
|
|
365
|
+
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
|
|
369
|
+
*/
|
|
370
|
+
const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
|
|
371
|
+
// Order of precedence:
|
|
372
|
+
// 1) Individual explicit "on" wins outright.
|
|
373
|
+
// 2) Individual explicit "off" wins over any global.
|
|
374
|
+
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
375
|
+
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
376
|
+
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
377
|
+
(() => {
|
|
378
|
+
// Individual "on"
|
|
379
|
+
if (exclude === true)
|
|
380
|
+
return true;
|
|
381
|
+
// Individual "off"
|
|
382
|
+
if (excludeOff === true)
|
|
383
|
+
return undefined;
|
|
384
|
+
// Global "exclude-all" ON (unless explicitly turned off)
|
|
385
|
+
if (excludeAll === true)
|
|
386
|
+
return true;
|
|
387
|
+
// Global "exclude-all-off" (unless explicitly enabled)
|
|
388
|
+
if (excludeAllOff === true)
|
|
389
|
+
return undefined;
|
|
390
|
+
// Default
|
|
391
|
+
return defaultValue ? true : undefined;
|
|
392
|
+
})();
|
|
393
|
+
/**
|
|
394
|
+
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
395
|
+
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
396
|
+
*
|
|
397
|
+
* @typeParam T - Target object type.
|
|
398
|
+
* @param obj - The object to write to.
|
|
399
|
+
* @param key - The optional boolean property key of {@link T}.
|
|
400
|
+
* @param value - The value to set or `undefined` to unset.
|
|
401
|
+
*
|
|
402
|
+
* @remarks
|
|
403
|
+
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
404
|
+
*/
|
|
405
|
+
const setOptionalFlag = (obj, key, value) => {
|
|
406
|
+
const target = obj;
|
|
407
|
+
const k = key;
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
409
|
+
if (value === undefined)
|
|
410
|
+
delete target[k];
|
|
411
|
+
else
|
|
412
|
+
target[k] = value;
|
|
179
413
|
};
|
|
180
414
|
|
|
181
415
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
186
|
-
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
187
|
-
* later per the staged plan.
|
|
416
|
+
* Merge and normalize raw Commander options (current + parent + defaults)
|
|
417
|
+
* into a GetDotenvCliOptions-like object. Types are intentionally wide to
|
|
418
|
+
* avoid cross-layer coupling; callers may cast as needed.
|
|
188
419
|
*/
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
420
|
+
const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
421
|
+
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
422
|
+
? JSON.parse(parentJson)
|
|
423
|
+
: undefined;
|
|
424
|
+
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
425
|
+
const current = { ...rest };
|
|
426
|
+
if (typeof scripts === 'string') {
|
|
427
|
+
try {
|
|
428
|
+
current.scripts = JSON.parse(scripts);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// ignore parse errors; leave scripts undefined
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
435
|
+
const d = defaults;
|
|
436
|
+
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
437
|
+
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
438
|
+
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
439
|
+
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
440
|
+
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
441
|
+
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
442
|
+
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
443
|
+
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
444
|
+
// warnEntropy (tri-state)
|
|
445
|
+
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
446
|
+
// Normalize shell for predictability: explicit default shell per OS.
|
|
447
|
+
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
448
|
+
let resolvedShell = merged.shell;
|
|
449
|
+
if (shellOff)
|
|
450
|
+
resolvedShell = false;
|
|
451
|
+
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
452
|
+
resolvedShell = defaultShell;
|
|
453
|
+
}
|
|
454
|
+
else if (typeof resolvedShell !== 'string' &&
|
|
455
|
+
typeof defaults.shell === 'string') {
|
|
456
|
+
resolvedShell = defaults.shell;
|
|
457
|
+
}
|
|
458
|
+
merged.shell = resolvedShell;
|
|
459
|
+
const cmd = typeof command === 'string' ? command : undefined;
|
|
460
|
+
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
461
|
+
};
|
|
216
462
|
|
|
217
463
|
/**
|
|
218
464
|
* Zod schemas for configuration files discovered by the new loader.
|
|
@@ -452,140 +698,191 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
|
452
698
|
};
|
|
453
699
|
|
|
454
700
|
/**
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
* references in strings and records. It supports both whitespace and
|
|
459
|
-
* bracket syntaxes with optional defaults:
|
|
460
|
-
*
|
|
461
|
-
* - Whitespace: `$VAR[:default]`
|
|
462
|
-
* - Bracketed: `${VAR[:default]}`
|
|
701
|
+
* Validate a composed env against config-provided validation surfaces.
|
|
702
|
+
* Precedence for validation definitions:
|
|
703
|
+
* project.local -\> project.public -\> packaged
|
|
463
704
|
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
* Like String.prototype.search but returns the last index.
|
|
469
|
-
* @internal
|
|
705
|
+
* Behavior:
|
|
706
|
+
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
707
|
+
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
708
|
+
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
470
709
|
*/
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
else {
|
|
507
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
508
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
509
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
510
|
-
if (bracketMatch != null)
|
|
511
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
710
|
+
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
711
|
+
const pick = (getter) => {
|
|
712
|
+
const pl = sources.project?.local;
|
|
713
|
+
const pp = sources.project?.public;
|
|
714
|
+
const pk = sources.packaged;
|
|
715
|
+
return ((pl && getter(pl)) ||
|
|
716
|
+
(pp && getter(pp)) ||
|
|
717
|
+
(pk && getter(pk)) ||
|
|
718
|
+
undefined);
|
|
719
|
+
};
|
|
720
|
+
const schema = pick((cfg) => cfg['schema']);
|
|
721
|
+
if (schema &&
|
|
722
|
+
typeof schema.safeParse === 'function') {
|
|
723
|
+
try {
|
|
724
|
+
const parsed = schema.safeParse(finalEnv);
|
|
725
|
+
if (!parsed.success) {
|
|
726
|
+
// Try to render zod-style issues when available.
|
|
727
|
+
const err = parsed.error;
|
|
728
|
+
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
729
|
+
? err.issues.map((i) => {
|
|
730
|
+
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
731
|
+
const msg = i.message ?? 'Invalid value';
|
|
732
|
+
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
733
|
+
})
|
|
734
|
+
: ['[schema] validation failed'];
|
|
735
|
+
return issues;
|
|
736
|
+
}
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// If schema invocation fails, surface a single diagnostic.
|
|
741
|
+
return [
|
|
742
|
+
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
743
|
+
];
|
|
744
|
+
}
|
|
512
745
|
}
|
|
513
|
-
|
|
746
|
+
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
747
|
+
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
748
|
+
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
749
|
+
if (missing.length > 0) {
|
|
750
|
+
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return [];
|
|
514
754
|
};
|
|
755
|
+
|
|
756
|
+
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
757
|
+
|
|
758
|
+
// src/GetDotenvOptions.ts
|
|
759
|
+
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
515
760
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* Unknown variables will expand to an empty string.
|
|
519
|
-
*
|
|
520
|
-
* @param value - The string to expand.
|
|
521
|
-
* @param ref - The reference object to use for variable expansion.
|
|
522
|
-
* @returns The expanded string.
|
|
523
|
-
*
|
|
524
|
-
* @example
|
|
525
|
-
* ```ts
|
|
526
|
-
* process.env.FOO = 'bar';
|
|
527
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
528
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
529
|
-
* ```
|
|
761
|
+
* Converts programmatic CLI options to `getDotenv` options. *
|
|
762
|
+
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
530
763
|
*
|
|
531
|
-
* @
|
|
532
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
533
|
-
* references, those will also be expanded until a stable value is reached.
|
|
534
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
764
|
+
* @returns `getDotenv` options.
|
|
535
765
|
*/
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
766
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
767
|
+
/**
|
|
768
|
+
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
769
|
+
*
|
|
770
|
+
* - Splits {@link GetDotenvCliOptions.paths} using either a delimiter * or a regular expression pattern into a string array. * - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
|
|
771
|
+
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
772
|
+
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
773
|
+
*
|
|
774
|
+
* @remarks
|
|
775
|
+
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
776
|
+
*/
|
|
777
|
+
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
778
|
+
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
779
|
+
const restObj = { ...rest };
|
|
780
|
+
delete restObj.debug;
|
|
781
|
+
delete restObj.scripts;
|
|
782
|
+
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
783
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
784
|
+
let parsedVars;
|
|
785
|
+
if (typeof vars === 'string') {
|
|
786
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
787
|
+
? RegExp(varsAssignorPattern)
|
|
788
|
+
: (varsAssignor ?? '=')));
|
|
789
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
790
|
+
}
|
|
791
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
792
|
+
// Keep only string or undefined values to match ProcessEnv.
|
|
793
|
+
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
794
|
+
parsedVars = Object.fromEntries(entries);
|
|
795
|
+
}
|
|
796
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
797
|
+
// expectations and the compat test assertions.
|
|
798
|
+
if (parsedVars) {
|
|
799
|
+
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
800
|
+
}
|
|
801
|
+
// Tolerate paths as either a delimited string or string[]
|
|
802
|
+
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
803
|
+
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
804
|
+
const pathsAny = paths;
|
|
805
|
+
const pathsOut = Array.isArray(pathsAny)
|
|
806
|
+
? pathsAny.filter((p) => typeof p === 'string')
|
|
807
|
+
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
808
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
809
|
+
return {
|
|
810
|
+
...restObj,
|
|
811
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
812
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
813
|
+
};
|
|
539
814
|
};
|
|
815
|
+
const resolveGetDotenvOptions = async (customOptions) => {
|
|
816
|
+
/**
|
|
817
|
+
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
818
|
+
*
|
|
819
|
+
* 1. Base defaults derived from the CLI generator defaults
|
|
820
|
+
* ({@link baseGetDotenvCliOptions}).
|
|
821
|
+
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
822
|
+
* package root (if present).
|
|
823
|
+
* 3. The provided {@link customOptions}.
|
|
824
|
+
*
|
|
825
|
+
* The result preserves explicit empty values and drops only `undefined`.
|
|
826
|
+
*
|
|
827
|
+
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* ```ts
|
|
831
|
+
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
832
|
+
* ```
|
|
833
|
+
*/
|
|
834
|
+
const localPkgDir = await packageDirectory();
|
|
835
|
+
const localOptionsPath = localPkgDir
|
|
836
|
+
? join(localPkgDir, getDotenvOptionsFilename)
|
|
837
|
+
: undefined;
|
|
838
|
+
const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
|
|
839
|
+
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
840
|
+
: {});
|
|
841
|
+
// Merge order: base < local < custom (custom has highest precedence)
|
|
842
|
+
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
843
|
+
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
844
|
+
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
845
|
+
return {
|
|
846
|
+
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
847
|
+
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
848
|
+
};
|
|
849
|
+
};
|
|
850
|
+
|
|
540
851
|
/**
|
|
541
|
-
*
|
|
542
|
-
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
543
|
-
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
544
|
-
*
|
|
545
|
-
* @param values - The values object to expand.
|
|
546
|
-
* @param options - Expansion options.
|
|
547
|
-
* @returns The value object with expanded string values.
|
|
548
|
-
*
|
|
549
|
-
* @example
|
|
550
|
-
* ```ts
|
|
551
|
-
* process.env.FOO = 'bar';
|
|
552
|
-
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
553
|
-
* // => { A: "bar", B: "xbary" }
|
|
554
|
-
* ```
|
|
555
|
-
*
|
|
556
|
-
* @remarks
|
|
557
|
-
* Options:
|
|
558
|
-
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
559
|
-
* - progressive: Whether to progressively add expanded values to the set of
|
|
560
|
-
* reference keys.
|
|
561
|
-
*
|
|
562
|
-
* When `progressive` is true, each expanded key becomes available for
|
|
563
|
-
* subsequent expansions in the same object (left-to-right by object key order).
|
|
564
|
-
*/
|
|
565
|
-
const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
|
|
566
|
-
const { ref = process.env, progressive = false } = options;
|
|
567
|
-
acc[key] = dotenvExpand(values[key], {
|
|
568
|
-
...ref,
|
|
569
|
-
...(progressive ? acc : {}),
|
|
570
|
-
});
|
|
571
|
-
return acc;
|
|
572
|
-
}, {});
|
|
573
|
-
/**
|
|
574
|
-
* Recursively expands environment variables in a string using `process.env` as
|
|
575
|
-
* the expansion reference. Variables may be presented with optional default as
|
|
576
|
-
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
577
|
-
* empty string.
|
|
578
|
-
*
|
|
579
|
-
* @param value - The string to expand.
|
|
580
|
-
* @returns The expanded string.
|
|
852
|
+
* Zod schemas for programmatic GetDotenv options.
|
|
581
853
|
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* ```
|
|
854
|
+
* NOTE: These schemas are introduced without wiring to avoid behavior changes.
|
|
855
|
+
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
856
|
+
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
857
|
+
* later per the staged plan.
|
|
587
858
|
*/
|
|
588
|
-
|
|
859
|
+
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
860
|
+
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
861
|
+
// RAW: all fields optional — undefined means "inherit" from lower layers.
|
|
862
|
+
const getDotenvOptionsSchemaRaw = z.object({
|
|
863
|
+
defaultEnv: z.string().optional(),
|
|
864
|
+
dotenvToken: z.string().optional(),
|
|
865
|
+
dynamicPath: z.string().optional(),
|
|
866
|
+
// Dynamic map is intentionally wide for now; refine once sources are normalized.
|
|
867
|
+
dynamic: z.record(z.string(), z.unknown()).optional(),
|
|
868
|
+
env: z.string().optional(),
|
|
869
|
+
excludeDynamic: z.boolean().optional(),
|
|
870
|
+
excludeEnv: z.boolean().optional(),
|
|
871
|
+
excludeGlobal: z.boolean().optional(),
|
|
872
|
+
excludePrivate: z.boolean().optional(),
|
|
873
|
+
excludePublic: z.boolean().optional(),
|
|
874
|
+
loadProcess: z.boolean().optional(),
|
|
875
|
+
log: z.boolean().optional(),
|
|
876
|
+
outputPath: z.string().optional(),
|
|
877
|
+
paths: z.array(z.string()).optional(),
|
|
878
|
+
privateToken: z.string().optional(),
|
|
879
|
+
vars: processEnvSchema.optional(),
|
|
880
|
+
// Host-only feature flag: guarded integration of config loader/overlay
|
|
881
|
+
useConfigLoader: z.boolean().optional(),
|
|
882
|
+
});
|
|
883
|
+
// RESOLVED: service-boundary contract (post-inheritance).
|
|
884
|
+
// For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
|
|
885
|
+
const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
|
|
589
886
|
|
|
590
887
|
const applyKv = (current, kv) => {
|
|
591
888
|
if (!kv || Object.keys(kv).length === 0)
|
|
@@ -1199,7 +1496,7 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
|
1199
1496
|
*
|
|
1200
1497
|
* NOTE: This host is additive and does not alter the legacy CLI.
|
|
1201
1498
|
*/
|
|
1202
|
-
class GetDotenvCli extends Command {
|
|
1499
|
+
let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
1203
1500
|
/** Registered top-level plugins (composition happens via .use()) */
|
|
1204
1501
|
_plugins = [];
|
|
1205
1502
|
/** One-time installation guard */
|
|
@@ -1445,369 +1742,82 @@ class GetDotenvCli extends Command {
|
|
|
1445
1742
|
}
|
|
1446
1743
|
// Plugin groups sorted by id
|
|
1447
1744
|
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1448
|
-
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1449
|
-
for (const k of pluginKeys) {
|
|
1450
|
-
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1451
|
-
const rows = byGroup.get(k) ?? [];
|
|
1452
|
-
if (rows.length > 0) {
|
|
1453
|
-
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
return out;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
/**
|
|
1461
|
-
* Validate a composed env against config-provided validation surfaces.
|
|
1462
|
-
* Precedence for validation definitions:
|
|
1463
|
-
* project.local -\> project.public -\> packaged
|
|
1464
|
-
*
|
|
1465
|
-
* Behavior:
|
|
1466
|
-
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
1467
|
-
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
1468
|
-
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
1469
|
-
*/
|
|
1470
|
-
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
1471
|
-
const pick = (getter) => {
|
|
1472
|
-
const pl = sources.project?.local;
|
|
1473
|
-
const pp = sources.project?.public;
|
|
1474
|
-
const pk = sources.packaged;
|
|
1475
|
-
return ((pl && getter(pl)) ||
|
|
1476
|
-
(pp && getter(pp)) ||
|
|
1477
|
-
(pk && getter(pk)) ||
|
|
1478
|
-
undefined);
|
|
1479
|
-
};
|
|
1480
|
-
const schema = pick((cfg) => cfg['schema']);
|
|
1481
|
-
if (schema &&
|
|
1482
|
-
typeof schema.safeParse === 'function') {
|
|
1483
|
-
try {
|
|
1484
|
-
const parsed = schema.safeParse(finalEnv);
|
|
1485
|
-
if (!parsed.success) {
|
|
1486
|
-
// Try to render zod-style issues when available.
|
|
1487
|
-
const err = parsed.error;
|
|
1488
|
-
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
1489
|
-
? err.issues.map((i) => {
|
|
1490
|
-
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1491
|
-
const msg = i.message ?? 'Invalid value';
|
|
1492
|
-
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
1493
|
-
})
|
|
1494
|
-
: ['[schema] validation failed'];
|
|
1495
|
-
return issues;
|
|
1496
|
-
}
|
|
1497
|
-
return [];
|
|
1498
|
-
}
|
|
1499
|
-
catch {
|
|
1500
|
-
// If schema invocation fails, surface a single diagnostic.
|
|
1501
|
-
return [
|
|
1502
|
-
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
1503
|
-
];
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
1507
|
-
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
1508
|
-
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
1509
|
-
if (missing.length > 0) {
|
|
1510
|
-
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
return [];
|
|
1514
|
-
};
|
|
1515
|
-
|
|
1516
|
-
/**
|
|
1517
|
-
* Attach legacy root flags to a Commander program.
|
|
1518
|
-
* Uses provided defaults to render help labels without coupling to generators.
|
|
1519
|
-
*/
|
|
1520
|
-
const attachRootOptions = (program, defaults, opts) => {
|
|
1521
|
-
// Install temporary wrappers to tag all options added here as "base".
|
|
1522
|
-
const GROUP = 'base';
|
|
1523
|
-
const tagLatest = (cmd, group) => {
|
|
1524
|
-
const optsArr = cmd.options;
|
|
1525
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
1526
|
-
const last = optsArr[optsArr.length - 1];
|
|
1527
|
-
last.__group = group;
|
|
1528
|
-
}
|
|
1529
|
-
};
|
|
1530
|
-
const originalAddOption = program.addOption.bind(program);
|
|
1531
|
-
const originalOption = program.option.bind(program);
|
|
1532
|
-
program.addOption = function patchedAdd(opt) {
|
|
1533
|
-
// Tag before adding, in case consumers inspect the Option directly.
|
|
1534
|
-
opt.__group = GROUP;
|
|
1535
|
-
const ret = originalAddOption(opt);
|
|
1536
|
-
return ret;
|
|
1537
|
-
};
|
|
1538
|
-
program.option = function patchedOption(...args) {
|
|
1539
|
-
const ret = originalOption(...args);
|
|
1540
|
-
tagLatest(this, GROUP);
|
|
1541
|
-
return ret;
|
|
1542
|
-
};
|
|
1543
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
1544
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
1545
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
1546
|
-
// Build initial chain.
|
|
1547
|
-
let p = program
|
|
1548
|
-
.enablePositionalOptions()
|
|
1549
|
-
.passThroughOptions()
|
|
1550
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
1551
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
1552
|
-
['KEY1', 'VAL1'],
|
|
1553
|
-
['KEY2', 'VAL2'],
|
|
1554
|
-
]
|
|
1555
|
-
.map((v) => v.join(va))
|
|
1556
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
1557
|
-
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
1558
|
-
// Default is OFF; the generator opts in explicitly.
|
|
1559
|
-
if (opts?.includeCommandOption === true) {
|
|
1560
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
1561
|
-
}
|
|
1562
|
-
p = p
|
|
1563
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
1564
|
-
.addOption(new Option('-s, --shell [string]', (() => {
|
|
1565
|
-
let defaultLabel = '';
|
|
1566
|
-
if (shell !== undefined) {
|
|
1567
|
-
if (typeof shell === 'boolean') {
|
|
1568
|
-
defaultLabel = ' (default OS shell)';
|
|
1569
|
-
}
|
|
1570
|
-
else if (typeof shell === 'string') {
|
|
1571
|
-
// Safe string interpolation
|
|
1572
|
-
defaultLabel = ` (default ${shell})`;
|
|
1745
|
+
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1746
|
+
for (const k of pluginKeys) {
|
|
1747
|
+
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1748
|
+
const rows = byGroup.get(k) ?? [];
|
|
1749
|
+
if (rows.length > 0) {
|
|
1750
|
+
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1573
1751
|
}
|
|
1574
1752
|
}
|
|
1575
|
-
return
|
|
1576
|
-
}
|
|
1577
|
-
.addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
1578
|
-
.addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
1579
|
-
.addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
1580
|
-
.addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
1581
|
-
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
1582
|
-
? ' (default)'
|
|
1583
|
-
: ''}`).conflicts('excludeAllOff'))
|
|
1584
|
-
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
1585
|
-
.addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
1586
|
-
.addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
1587
|
-
.addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
1588
|
-
.addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
1589
|
-
.addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
1590
|
-
.addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
1591
|
-
.addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
1592
|
-
.addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
1593
|
-
.addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
1594
|
-
.addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
1595
|
-
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
1596
|
-
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
1597
|
-
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
1598
|
-
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
1599
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
1600
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
1601
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
1602
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
1603
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
1604
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
1605
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
1606
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
1607
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
1608
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
1609
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
1610
|
-
// Hidden scripts pipe-through (stringified)
|
|
1611
|
-
.addOption(new Option('--scripts <string>')
|
|
1612
|
-
.default(JSON.stringify(scripts))
|
|
1613
|
-
.hideHelp());
|
|
1614
|
-
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
1615
|
-
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
1616
|
-
// Validation: strict mode fails on env validation issues (warn by default).
|
|
1617
|
-
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
1618
|
-
// Entropy diagnostics (presentation-only)
|
|
1619
|
-
p = p
|
|
1620
|
-
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
1621
|
-
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
1622
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
1623
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
1624
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
1625
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
1626
|
-
// Restore original methods to avoid tagging future additions outside base.
|
|
1627
|
-
program.addOption = originalAddOption;
|
|
1628
|
-
program.option = originalOption;
|
|
1629
|
-
return p;
|
|
1753
|
+
return out;
|
|
1754
|
+
}
|
|
1630
1755
|
};
|
|
1631
1756
|
|
|
1632
|
-
/**
|
|
1633
|
-
*
|
|
1634
|
-
* - If the user explicitly enabled the flag, return true.
|
|
1635
|
-
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
1636
|
-
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
1637
|
-
*
|
|
1638
|
-
* @param exclude - The "on" flag value as parsed by Commander.
|
|
1639
|
-
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
1640
|
-
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
1641
|
-
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
1757
|
+
/** src/cliHost/definePlugin.ts
|
|
1758
|
+
* Plugin contracts for the GetDotenv CLI host.
|
|
1642
1759
|
*
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1646
|
-
* ```
|
|
1760
|
+
* This module exposes a structural public interface for the host that plugins
|
|
1761
|
+
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
1762
|
+
* nominal class identity issues (private fields) in downstream consumers.
|
|
1647
1763
|
*/
|
|
1648
|
-
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
1649
1764
|
/**
|
|
1650
|
-
*
|
|
1651
|
-
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
1652
|
-
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
1653
|
-
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
1654
|
-
*
|
|
1655
|
-
* @param exclude - Individual include/exclude flag.
|
|
1656
|
-
* @param excludeOff - Individual "...-off" flag.
|
|
1657
|
-
* @param defaultValue - Default for the individual flag.
|
|
1658
|
-
* @param excludeAll - Global "exclude-all" flag.
|
|
1659
|
-
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
1765
|
+
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
1660
1766
|
*
|
|
1661
1767
|
* @example
|
|
1662
|
-
*
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
// Order of precedence:
|
|
1666
|
-
// 1) Individual explicit "on" wins outright.
|
|
1667
|
-
// 2) Individual explicit "off" wins over any global.
|
|
1668
|
-
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
1669
|
-
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
1670
|
-
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
1671
|
-
(() => {
|
|
1672
|
-
// Individual "on"
|
|
1673
|
-
if (exclude === true)
|
|
1674
|
-
return true;
|
|
1675
|
-
// Individual "off"
|
|
1676
|
-
if (excludeOff === true)
|
|
1677
|
-
return undefined;
|
|
1678
|
-
// Global "exclude-all" ON (unless explicitly turned off)
|
|
1679
|
-
if (excludeAll === true)
|
|
1680
|
-
return true;
|
|
1681
|
-
// Global "exclude-all-off" (unless explicitly enabled)
|
|
1682
|
-
if (excludeAllOff === true)
|
|
1683
|
-
return undefined;
|
|
1684
|
-
// Default
|
|
1685
|
-
return defaultValue ? true : undefined;
|
|
1686
|
-
})();
|
|
1687
|
-
/**
|
|
1688
|
-
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
1689
|
-
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
1690
|
-
*
|
|
1691
|
-
* @typeParam T - Target object type.
|
|
1692
|
-
* @param obj - The object to write to.
|
|
1693
|
-
* @param key - The optional boolean property key of {@link T}.
|
|
1694
|
-
* @param value - The value to set or `undefined` to unset.
|
|
1695
|
-
*
|
|
1696
|
-
* @remarks
|
|
1697
|
-
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
1768
|
+
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
1769
|
+
* .use(childA)
|
|
1770
|
+
* .use(childB);
|
|
1698
1771
|
*/
|
|
1699
|
-
const
|
|
1700
|
-
const
|
|
1701
|
-
const
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1772
|
+
const definePlugin = (spec) => {
|
|
1773
|
+
const { children = [], ...rest } = spec;
|
|
1774
|
+
const plugin = {
|
|
1775
|
+
...rest,
|
|
1776
|
+
children: [...children],
|
|
1777
|
+
use(child) {
|
|
1778
|
+
this.children.push(child);
|
|
1779
|
+
return this;
|
|
1780
|
+
},
|
|
1781
|
+
};
|
|
1782
|
+
return plugin;
|
|
1707
1783
|
};
|
|
1708
1784
|
|
|
1709
1785
|
/**
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1786
|
+
* GetDotenvCli with root helpers as real class methods.
|
|
1787
|
+
* - attachRootOptions: installs legacy/base root flags on the command.
|
|
1788
|
+
* - passOptions: merges flags (parent \< current), computes dotenv context once,
|
|
1789
|
+
* runs validation, and persists merged options for nested flows.
|
|
1713
1790
|
*/
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
}
|
|
1724
|
-
catch {
|
|
1725
|
-
// ignore parse errors; leave scripts undefined
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
1729
|
-
const d = defaults;
|
|
1730
|
-
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
1731
|
-
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
1732
|
-
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
1733
|
-
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
1734
|
-
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
1735
|
-
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
1736
|
-
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
1737
|
-
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
1738
|
-
// warnEntropy (tri-state)
|
|
1739
|
-
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
1740
|
-
// Normalize shell for predictability: explicit default shell per OS.
|
|
1741
|
-
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
1742
|
-
let resolvedShell = merged.shell;
|
|
1743
|
-
if (shellOff)
|
|
1744
|
-
resolvedShell = false;
|
|
1745
|
-
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
1746
|
-
resolvedShell = defaultShell;
|
|
1747
|
-
}
|
|
1748
|
-
else if (typeof resolvedShell !== 'string' &&
|
|
1749
|
-
typeof defaults.shell === 'string') {
|
|
1750
|
-
resolvedShell = defaults.shell;
|
|
1791
|
+
class GetDotenvCli extends GetDotenvCli$1 {
|
|
1792
|
+
/**
|
|
1793
|
+
* Attach legacy root flags to this CLI instance. Defaults come from
|
|
1794
|
+
* baseRootOptionDefaults when none are provided.
|
|
1795
|
+
*/
|
|
1796
|
+
attachRootOptions(defaults, opts) {
|
|
1797
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
1798
|
+
attachRootOptions(this, d, opts);
|
|
1799
|
+
return this;
|
|
1751
1800
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
merged;
|
|
1770
|
-
// Also store on the host for downstream ergonomic accessors.
|
|
1771
|
-
this._setOptionsBag(merged);
|
|
1772
|
-
// Build service options and compute context (always-on config loader path).
|
|
1773
|
-
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1774
|
-
await this.resolveAndLoad(serviceOptions);
|
|
1775
|
-
// Global validation: once after Phase C using config sources.
|
|
1776
|
-
try {
|
|
1777
|
-
const ctx = this.getCtx();
|
|
1778
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
1779
|
-
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
1780
|
-
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1781
|
-
if (Array.isArray(issues) && issues.length > 0) {
|
|
1782
|
-
const logger = (merged.logger ??
|
|
1783
|
-
console);
|
|
1784
|
-
const emit = logger.error ?? logger.log;
|
|
1785
|
-
issues.forEach((m) => {
|
|
1786
|
-
emit(m);
|
|
1787
|
-
});
|
|
1788
|
-
if (merged.strict) {
|
|
1789
|
-
// Deterministic failure under strict mode
|
|
1790
|
-
process.exit(1);
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
catch {
|
|
1795
|
-
// Be tolerant: validation errors reported above; unexpected failures here
|
|
1796
|
-
// should not crash non-strict flows.
|
|
1797
|
-
}
|
|
1798
|
-
});
|
|
1799
|
-
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
1800
|
-
// with the same merged options and context without duplicating logic.
|
|
1801
|
-
this.hook('preAction', async (thisCommand) => {
|
|
1802
|
-
const raw = thisCommand.opts();
|
|
1803
|
-
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
1804
|
-
thisCommand.getDotenvCliOptions =
|
|
1805
|
-
merged;
|
|
1806
|
-
this._setOptionsBag(merged);
|
|
1807
|
-
// Avoid duplicate heavy work if a context is already present.
|
|
1808
|
-
if (!this.getCtx()) {
|
|
1801
|
+
/**
|
|
1802
|
+
* Install preSubcommand/preAction hooks that:
|
|
1803
|
+
* - Merge options (parent round-trip + current invocation) using resolveCliOptions.
|
|
1804
|
+
* - Persist the merged bag on the current command and on the host (for ergonomics).
|
|
1805
|
+
* - Compute the dotenv context once via resolveAndLoad(serviceOptions).
|
|
1806
|
+
* - Validate the composed env against discovered config (warn or --strict fail).
|
|
1807
|
+
*/
|
|
1808
|
+
passOptions(defaults) {
|
|
1809
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
1810
|
+
this.hook('preSubcommand', async (thisCommand) => {
|
|
1811
|
+
const raw = thisCommand.opts();
|
|
1812
|
+
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
1813
|
+
// Persist merged options (for nested behavior and ergonomic access).
|
|
1814
|
+
thisCommand.getDotenvCliOptions =
|
|
1815
|
+
merged;
|
|
1816
|
+
this._setOptionsBag(merged);
|
|
1817
|
+
// Build service options and compute context (always-on loader path).
|
|
1809
1818
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1810
1819
|
await this.resolveAndLoad(serviceOptions);
|
|
1820
|
+
// Global validation: once after Phase C using config sources.
|
|
1811
1821
|
try {
|
|
1812
1822
|
const ctx = this.getCtx();
|
|
1813
1823
|
const dotenv = (ctx?.dotenv ?? {});
|
|
@@ -1826,12 +1836,46 @@ GetDotenvCli.prototype.passOptions = function (defaults) {
|
|
|
1826
1836
|
}
|
|
1827
1837
|
}
|
|
1828
1838
|
catch {
|
|
1829
|
-
//
|
|
1839
|
+
// Be tolerant: do not crash non-strict flows on unexpected validator failures.
|
|
1830
1840
|
}
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1841
|
+
});
|
|
1842
|
+
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
1843
|
+
// with the same merged options and context without duplicating logic.
|
|
1844
|
+
this.hook('preAction', async (thisCommand) => {
|
|
1845
|
+
const raw = thisCommand.opts();
|
|
1846
|
+
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
1847
|
+
thisCommand.getDotenvCliOptions =
|
|
1848
|
+
merged;
|
|
1849
|
+
this._setOptionsBag(merged);
|
|
1850
|
+
// Avoid duplicate heavy work if a context is already present.
|
|
1851
|
+
if (!this.getCtx()) {
|
|
1852
|
+
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1853
|
+
await this.resolveAndLoad(serviceOptions);
|
|
1854
|
+
try {
|
|
1855
|
+
const ctx = this.getCtx();
|
|
1856
|
+
const dotenv = (ctx?.dotenv ?? {});
|
|
1857
|
+
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
1858
|
+
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1859
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
1860
|
+
const logger = (merged
|
|
1861
|
+
.logger ?? console);
|
|
1862
|
+
const emit = logger.error ?? logger.log;
|
|
1863
|
+
issues.forEach((m) => {
|
|
1864
|
+
emit(m);
|
|
1865
|
+
});
|
|
1866
|
+
if (merged.strict) {
|
|
1867
|
+
process.exit(1);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
catch {
|
|
1872
|
+
// Tolerate validation side-effects in non-strict mode.
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
return this;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1835
1879
|
|
|
1836
1880
|
// Minimal tokenizer for shell-off execution:
|
|
1837
1881
|
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
@@ -2066,34 +2110,6 @@ const buildSpawnEnv = (base, overlay) => {
|
|
|
2066
2110
|
return out;
|
|
2067
2111
|
};
|
|
2068
2112
|
|
|
2069
|
-
/** src/cliHost/definePlugin.ts
|
|
2070
|
-
* Plugin contracts for the GetDotenv CLI host.
|
|
2071
|
-
*
|
|
2072
|
-
* This module exposes a structural public interface for the host that plugins
|
|
2073
|
-
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
2074
|
-
* nominal class identity issues (private fields) in downstream consumers.
|
|
2075
|
-
*/
|
|
2076
|
-
/**
|
|
2077
|
-
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
2078
|
-
*
|
|
2079
|
-
* @example
|
|
2080
|
-
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
2081
|
-
* .use(childA)
|
|
2082
|
-
* .use(childB);
|
|
2083
|
-
*/
|
|
2084
|
-
const definePlugin = (spec) => {
|
|
2085
|
-
const { children = [], ...rest } = spec;
|
|
2086
|
-
const plugin = {
|
|
2087
|
-
...rest,
|
|
2088
|
-
children: [...children],
|
|
2089
|
-
use(child) {
|
|
2090
|
-
this.children.push(child);
|
|
2091
|
-
return this;
|
|
2092
|
-
},
|
|
2093
|
-
};
|
|
2094
|
-
return plugin;
|
|
2095
|
-
};
|
|
2096
|
-
|
|
2097
2113
|
/**
|
|
2098
2114
|
* Batch services (neutral): resolve command and shell settings.
|
|
2099
2115
|
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
@@ -3164,7 +3180,7 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
3164
3180
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
3165
3181
|
const cmd = new Command()
|
|
3166
3182
|
.name('cmd')
|
|
3167
|
-
.description('
|
|
3183
|
+
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3168
3184
|
.configureHelp({ showGlobalOptions: true })
|
|
3169
3185
|
.enablePositionalOptions()
|
|
3170
3186
|
.passThroughOptions()
|
|
@@ -3752,7 +3768,7 @@ new Command()
|
|
|
3752
3768
|
|
|
3753
3769
|
new Command()
|
|
3754
3770
|
.name('cmd')
|
|
3755
|
-
.description('
|
|
3771
|
+
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3756
3772
|
.configureHelp({ showGlobalOptions: true })
|
|
3757
3773
|
.enablePositionalOptions()
|
|
3758
3774
|
.passThroughOptions()
|