@karmaniverous/get-dotenv 6.0.0-0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -334
- package/dist/cli.d.ts +569 -0
- package/dist/cli.mjs +18788 -0
- package/dist/cliHost.d.ts +548 -253
- package/dist/cliHost.mjs +1990 -1458
- package/dist/config.d.ts +192 -14
- package/dist/config.mjs +256 -81
- package/dist/env-overlay.d.ts +226 -18
- package/dist/env-overlay.mjs +181 -22
- package/dist/getdotenv.cli.mjs +18166 -3437
- package/dist/index.d.ts +729 -136
- package/dist/index.mjs +18207 -3457
- package/dist/plugins-aws.d.ts +289 -104
- package/dist/plugins-aws.mjs +2462 -350
- package/dist/plugins-batch.d.ts +355 -105
- package/dist/plugins-batch.mjs +2595 -420
- package/dist/plugins-cmd.d.ts +287 -118
- package/dist/plugins-cmd.mjs +2661 -839
- package/dist/plugins-init.d.ts +272 -100
- package/dist/plugins-init.mjs +2152 -37
- package/dist/plugins.d.ts +323 -140
- package/dist/plugins.mjs +18006 -2025
- package/dist/templates/cli/index.ts +26 -0
- package/dist/templates/cli/plugins/hello.ts +43 -0
- package/dist/templates/config/js/getdotenv.config.js +20 -0
- package/dist/templates/config/json/local/getdotenv.config.local.json +7 -0
- package/dist/templates/config/json/public/getdotenv.config.json +9 -0
- package/dist/templates/config/public/getdotenv.config.json +8 -0
- package/dist/templates/config/ts/getdotenv.config.ts +28 -0
- package/dist/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
- package/dist/templates/config/yaml/public/getdotenv.config.yaml +7 -0
- package/dist/templates/getdotenv.config.js +20 -0
- package/dist/templates/getdotenv.config.json +9 -0
- package/dist/templates/getdotenv.config.local.json +7 -0
- package/dist/templates/getdotenv.config.local.yaml +7 -0
- package/dist/templates/getdotenv.config.ts +28 -0
- package/dist/templates/getdotenv.config.yaml +7 -0
- package/dist/templates/hello.ts +43 -0
- package/dist/templates/index.ts +26 -0
- package/dist/templates/js/getdotenv.config.js +20 -0
- package/dist/templates/json/local/getdotenv.config.local.json +7 -0
- package/dist/templates/json/public/getdotenv.config.json +9 -0
- package/dist/templates/local/getdotenv.config.local.json +7 -0
- package/dist/templates/local/getdotenv.config.local.yaml +7 -0
- package/dist/templates/plugins/hello.ts +43 -0
- package/dist/templates/public/getdotenv.config.json +9 -0
- package/dist/templates/public/getdotenv.config.yaml +7 -0
- package/dist/templates/ts/getdotenv.config.ts +28 -0
- package/dist/templates/yaml/local/getdotenv.config.local.yaml +7 -0
- package/dist/templates/yaml/public/getdotenv.config.yaml +7 -0
- package/getdotenv.config.json +1 -19
- package/package.json +52 -89
- package/templates/cli/index.ts +26 -0
- package/templates/cli/plugins/hello.ts +43 -0
- package/templates/config/js/getdotenv.config.js +9 -4
- package/templates/config/json/public/getdotenv.config.json +0 -3
- package/templates/config/public/getdotenv.config.json +0 -5
- package/templates/config/ts/getdotenv.config.ts +17 -5
- package/templates/config/yaml/public/getdotenv.config.yaml +0 -3
- package/dist/cliHost.cjs +0 -2078
- package/dist/cliHost.d.cts +0 -451
- package/dist/cliHost.d.mts +0 -451
- 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 -4077
- package/dist/index.d.cts +0 -318
- package/dist/index.d.mts +0 -318
- package/dist/plugins-aws.cjs +0 -666
- package/dist/plugins-aws.d.cts +0 -158
- package/dist/plugins-aws.d.mts +0 -158
- package/dist/plugins-batch.cjs +0 -658
- package/dist/plugins-batch.d.cts +0 -181
- package/dist/plugins-batch.d.mts +0 -181
- package/dist/plugins-cmd.cjs +0 -1112
- package/dist/plugins-cmd.d.cts +0 -178
- package/dist/plugins-cmd.d.mts +0 -178
- package/dist/plugins-demo.cjs +0 -352
- package/dist/plugins-demo.d.cts +0 -158
- package/dist/plugins-demo.d.mts +0 -158
- package/dist/plugins-demo.d.ts +0 -158
- package/dist/plugins-demo.mjs +0 -350
- 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 -2327
- package/dist/plugins.d.cts +0 -211
- package/dist/plugins.d.mts +0 -211
- package/templates/cli/ts/index.ts +0 -9
- package/templates/cli/ts/plugins/hello.ts +0 -17
package/dist/cliHost.mjs
CHANGED
|
@@ -1,40 +1,125 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export { z } from 'zod';
|
|
3
|
+
import path, { join, extname } from 'path';
|
|
1
4
|
import fs from 'fs-extra';
|
|
2
5
|
import { packageDirectory } from 'package-directory';
|
|
3
|
-
import
|
|
4
|
-
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
6
|
+
import url, { fileURLToPath } from 'url';
|
|
5
7
|
import YAML from 'yaml';
|
|
6
|
-
import {
|
|
7
|
-
import { Option, Command } from 'commander';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
8
9
|
import { nanoid } from 'nanoid';
|
|
9
10
|
import { parse } from 'dotenv';
|
|
10
|
-
import {
|
|
11
|
+
import { execa, execaCommand } from 'execa';
|
|
12
|
+
import { Option, Command } from '@commander-js/extra-typings';
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Zod schemas for programmatic GetDotenv options.
|
|
16
|
+
*
|
|
17
|
+
* Canonical source of truth for options shape. Public types are derived
|
|
18
|
+
* from these schemas (see consumers via z.output\<\>).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Minimal process env representation used by options and helpers.
|
|
22
|
+
* Values may be `undefined` to indicate "unset".
|
|
23
|
+
*/
|
|
24
|
+
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
25
|
+
// RAW: all fields optional — undefined means "inherit" from lower layers.
|
|
26
|
+
const getDotenvOptionsSchemaRaw = z.object({
|
|
27
|
+
defaultEnv: z.string().optional(),
|
|
28
|
+
dotenvToken: z.string().optional(),
|
|
29
|
+
dynamicPath: z.string().optional(),
|
|
30
|
+
// Dynamic map is intentionally wide for now; refine once sources are normalized.
|
|
31
|
+
dynamic: z.record(z.string(), z.unknown()).optional(),
|
|
32
|
+
env: z.string().optional(),
|
|
33
|
+
excludeDynamic: z.boolean().optional(),
|
|
34
|
+
excludeEnv: z.boolean().optional(),
|
|
35
|
+
excludeGlobal: z.boolean().optional(),
|
|
36
|
+
excludePrivate: z.boolean().optional(),
|
|
37
|
+
excludePublic: z.boolean().optional(),
|
|
38
|
+
loadProcess: z.boolean().optional(),
|
|
39
|
+
log: z.boolean().optional(),
|
|
40
|
+
logger: z.unknown().default(console),
|
|
41
|
+
outputPath: z.string().optional(),
|
|
42
|
+
paths: z.array(z.string()).optional(),
|
|
43
|
+
privateToken: z.string().optional(),
|
|
44
|
+
vars: processEnvSchema.optional(),
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Resolved programmatic options schema (post-inheritance).
|
|
48
|
+
* For now, this mirrors the RAW schema; future stages may materialize defaults
|
|
49
|
+
* and narrow shapes as resolution is wired into the host.
|
|
50
|
+
*/
|
|
51
|
+
const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Zod schemas for CLI-facing GetDotenv options (raw/resolved stubs).
|
|
55
|
+
*
|
|
56
|
+
* RAW allows stringly inputs (paths/vars + splitters). RESOLVED will later
|
|
57
|
+
* reflect normalized types (paths: string[], vars: ProcessEnv), applied in the
|
|
58
|
+
* CLI resolution pipeline.
|
|
59
|
+
*/
|
|
60
|
+
const getDotenvCliOptionsSchemaRaw = getDotenvOptionsSchemaRaw.extend({
|
|
61
|
+
// CLI-specific fields (stringly inputs before preprocessing)
|
|
62
|
+
debug: z.boolean().optional(),
|
|
63
|
+
strict: z.boolean().optional(),
|
|
64
|
+
capture: z.boolean().optional(),
|
|
65
|
+
trace: z.union([z.boolean(), z.array(z.string())]).optional(),
|
|
66
|
+
redact: z.boolean().optional(),
|
|
67
|
+
warnEntropy: z.boolean().optional(),
|
|
68
|
+
entropyThreshold: z.number().optional(),
|
|
69
|
+
entropyMinLength: z.number().optional(),
|
|
70
|
+
entropyWhitelist: z.array(z.string()).optional(),
|
|
71
|
+
redactPatterns: z.array(z.string()).optional(),
|
|
72
|
+
paths: z.string().optional(),
|
|
73
|
+
pathsDelimiter: z.string().optional(),
|
|
74
|
+
pathsDelimiterPattern: z.string().optional(),
|
|
75
|
+
scripts: z.record(z.string(), z.unknown()).optional(),
|
|
76
|
+
shell: z.union([z.boolean(), z.string()]).optional(),
|
|
77
|
+
vars: z.string().optional(),
|
|
78
|
+
varsAssignor: z.string().optional(),
|
|
79
|
+
varsAssignorPattern: z.string().optional(),
|
|
80
|
+
varsDelimiter: z.string().optional(),
|
|
81
|
+
varsDelimiterPattern: z.string().optional(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const visibilityMap = z.record(z.string(), z.boolean());
|
|
85
|
+
/**
|
|
86
|
+
* Zod schemas for configuration files discovered by the new loader.
|
|
87
|
+
*
|
|
88
|
+
* Notes:
|
|
89
|
+
* - RAW: all fields optional; only allowed top-level keys are:
|
|
90
|
+
* - rootOptionDefaults, rootOptionVisibility
|
|
91
|
+
* - scripts, vars, envVars
|
|
92
|
+
* - dynamic (JS/TS only), schema (JS/TS only)
|
|
93
|
+
* - plugins, requiredKeys
|
|
94
|
+
* - RESOLVED: mirrors RAW (no path normalization).
|
|
95
|
+
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS only).
|
|
96
|
+
*/
|
|
97
|
+
// String-only env value map
|
|
98
|
+
const stringMap = z.record(z.string(), z.string());
|
|
99
|
+
const envStringMap = z.record(z.string(), stringMap);
|
|
100
|
+
/**
|
|
101
|
+
* Raw configuration schema for get‑dotenv config files (JSON/YAML/JS/TS).
|
|
102
|
+
* Validates allowed top‑level keys without performing path normalization.
|
|
103
|
+
*/
|
|
104
|
+
const getDotenvConfigSchemaRaw = z.object({
|
|
105
|
+
rootOptionDefaults: getDotenvCliOptionsSchemaRaw.optional(),
|
|
106
|
+
rootOptionVisibility: visibilityMap.optional(),
|
|
107
|
+
scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
108
|
+
requiredKeys: z.array(z.string()).optional(),
|
|
109
|
+
schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
110
|
+
vars: stringMap.optional(), // public, global
|
|
111
|
+
envVars: envStringMap.optional(), // public, per-env
|
|
112
|
+
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
113
|
+
dynamic: z.unknown().optional(),
|
|
114
|
+
// Per-plugin config bag; validated by plugins/host when used.
|
|
115
|
+
plugins: z.record(z.string(), z.unknown()).optional(),
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* Resolved configuration schema which preserves the raw shape while narrowing
|
|
119
|
+
* the output to {@link GetDotenvConfigResolved}. Consumers get a strongly typed
|
|
120
|
+
* object, while the underlying validation remains Zod‑driven.
|
|
121
|
+
*/
|
|
122
|
+
const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => raw);
|
|
38
123
|
|
|
39
124
|
/** @internal */
|
|
40
125
|
const isPlainObject$1 = (value) => value !== null &&
|
|
@@ -57,189 +142,412 @@ const mergeInto = (target, source) => {
|
|
|
57
142
|
}
|
|
58
143
|
return target;
|
|
59
144
|
};
|
|
60
|
-
|
|
61
|
-
* Perform a deep defaults-style merge across plain objects. *
|
|
62
|
-
* - Only merges plain objects (prototype === Object.prototype).
|
|
63
|
-
* - Arrays and non-objects are replaced, not merged.
|
|
64
|
-
* - `undefined` values are ignored and do not overwrite prior values.
|
|
65
|
-
*
|
|
66
|
-
* @typeParam T - The resulting shape after merging all layers.
|
|
67
|
-
* @param layers - Zero or more partial layers in ascending precedence order.
|
|
68
|
-
* @returns The merged object typed as {@link T}.
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
|
|
72
|
-
* =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
|
|
73
|
-
*/
|
|
74
|
-
const defaultsDeep = (...layers) => {
|
|
145
|
+
function defaultsDeep(...layers) {
|
|
75
146
|
const result = layers
|
|
76
147
|
.filter(Boolean)
|
|
77
148
|
.reduce((acc, layer) => mergeInto(acc, layer), {});
|
|
78
149
|
return result;
|
|
79
|
-
}
|
|
150
|
+
}
|
|
80
151
|
|
|
81
152
|
/**
|
|
82
|
-
*
|
|
83
|
-
* -
|
|
84
|
-
|
|
85
|
-
|
|
153
|
+
* Serialize a dotenv record to a file with minimal quoting (multiline values are quoted).
|
|
154
|
+
* Future-proofs for ordering/sorting changes (currently insertion order).
|
|
155
|
+
*/
|
|
156
|
+
async function writeDotenvFile(filename, data) {
|
|
157
|
+
// Serialize: key=value with quotes only for multiline values.
|
|
158
|
+
const body = Object.keys(data).reduce((acc, key) => {
|
|
159
|
+
const v = data[key] ?? '';
|
|
160
|
+
const val = v.includes('\n') ? `"${v}"` : v;
|
|
161
|
+
return `${acc}${key}=${val}\n`;
|
|
162
|
+
}, '');
|
|
163
|
+
await fs.writeFile(filename, body, { encoding: 'utf-8' });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Dotenv expansion utilities.
|
|
86
168
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
169
|
+
* This module implements recursive expansion of environment-variable
|
|
170
|
+
* references in strings and records. It supports both whitespace and
|
|
171
|
+
* bracket syntaxes with optional defaults:
|
|
91
172
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
173
|
+
* - Whitespace: `$VAR[:default]`
|
|
174
|
+
* - Bracketed: `${VAR[:default]}`
|
|
175
|
+
*
|
|
176
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
177
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
96
178
|
*/
|
|
97
|
-
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
98
179
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
180
|
+
* Like String.prototype.search but returns the last index.
|
|
181
|
+
* @internal
|
|
182
|
+
*/
|
|
183
|
+
const searchLast = (str, rgx) => {
|
|
184
|
+
const matches = Array.from(str.matchAll(rgx));
|
|
185
|
+
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
186
|
+
};
|
|
187
|
+
const replaceMatch = (value, match, ref) => {
|
|
188
|
+
/**
|
|
189
|
+
* @internal
|
|
190
|
+
*/
|
|
191
|
+
const group = match[0];
|
|
192
|
+
const key = match[1];
|
|
193
|
+
const defaultValue = match[2];
|
|
194
|
+
if (!key)
|
|
195
|
+
return value;
|
|
196
|
+
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
197
|
+
return interpolate(replacement, ref);
|
|
198
|
+
};
|
|
199
|
+
const interpolate = (value = '', ref = {}) => {
|
|
200
|
+
/**
|
|
201
|
+
* @internal
|
|
202
|
+
*/
|
|
203
|
+
// if value is falsy, return it as is
|
|
204
|
+
if (!value)
|
|
205
|
+
return value;
|
|
206
|
+
// get position of last unescaped dollar sign
|
|
207
|
+
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
208
|
+
// return value if none found
|
|
209
|
+
if (lastUnescapedDollarSignIndex === -1)
|
|
210
|
+
return value;
|
|
211
|
+
// evaluate the value tail
|
|
212
|
+
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
213
|
+
// find whitespace pattern: $KEY:DEFAULT
|
|
214
|
+
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
215
|
+
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
216
|
+
if (whitespaceMatch != null)
|
|
217
|
+
return replaceMatch(value, whitespaceMatch, ref);
|
|
218
|
+
else {
|
|
219
|
+
// find bracket pattern: ${KEY:DEFAULT}
|
|
220
|
+
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
221
|
+
const bracketMatch = bracketPattern.exec(tail);
|
|
222
|
+
if (bracketMatch != null)
|
|
223
|
+
return replaceMatch(value, bracketMatch, ref);
|
|
224
|
+
}
|
|
225
|
+
return value;
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Recursively expands environment variables in a string. Variables may be
|
|
229
|
+
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
230
|
+
* Unknown variables will expand to an empty string.
|
|
103
231
|
*
|
|
104
|
-
* @param
|
|
105
|
-
* @param
|
|
106
|
-
* @
|
|
107
|
-
* @param excludeAll - Global "exclude-all" flag.
|
|
108
|
-
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
232
|
+
* @param value - The string to expand.
|
|
233
|
+
* @param ref - The reference object to use for variable expansion.
|
|
234
|
+
* @returns The expanded string.
|
|
109
235
|
*
|
|
110
236
|
* @example
|
|
111
|
-
*
|
|
237
|
+
* ```ts
|
|
238
|
+
* process.env.FOO = 'bar';
|
|
239
|
+
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
240
|
+
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* @remarks
|
|
244
|
+
* The expansion is recursive. If a referenced variable itself contains
|
|
245
|
+
* references, those will also be expanded until a stable value is reached.
|
|
246
|
+
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
112
247
|
*/
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
118
|
-
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
119
|
-
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
120
|
-
(() => {
|
|
121
|
-
// Individual "on"
|
|
122
|
-
if (exclude === true)
|
|
123
|
-
return true;
|
|
124
|
-
// Individual "off"
|
|
125
|
-
if (excludeOff === true)
|
|
126
|
-
return undefined;
|
|
127
|
-
// Global "exclude-all" ON (unless explicitly turned off)
|
|
128
|
-
if (excludeAll === true)
|
|
129
|
-
return true;
|
|
130
|
-
// Global "exclude-all-off" (unless explicitly enabled)
|
|
131
|
-
if (excludeAllOff === true)
|
|
132
|
-
return undefined;
|
|
133
|
-
// Default
|
|
134
|
-
return defaultValue ? true : undefined;
|
|
135
|
-
})();
|
|
248
|
+
const dotenvExpand = (value, ref = process.env) => {
|
|
249
|
+
const result = interpolate(value, ref);
|
|
250
|
+
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
251
|
+
};
|
|
136
252
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
253
|
+
* Recursively expands environment variables in the values of a JSON object.
|
|
254
|
+
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
255
|
+
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
139
256
|
*
|
|
140
|
-
* @
|
|
141
|
-
* @param
|
|
142
|
-
* @
|
|
143
|
-
*
|
|
257
|
+
* @param values - The values object to expand.
|
|
258
|
+
* @param options - Expansion options.
|
|
259
|
+
* @returns The value object with expanded string values.
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```ts
|
|
263
|
+
* process.env.FOO = 'bar';
|
|
264
|
+
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
265
|
+
* // => { A: "bar", B: "xbary" }
|
|
266
|
+
* ```
|
|
144
267
|
*
|
|
145
268
|
* @remarks
|
|
146
|
-
*
|
|
269
|
+
* Options:
|
|
270
|
+
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
271
|
+
* - progressive: Whether to progressively add expanded values to the set of
|
|
272
|
+
* reference keys.
|
|
273
|
+
*
|
|
274
|
+
* When `progressive` is true, each expanded key becomes available for
|
|
275
|
+
* subsequent expansions in the same object (left-to-right by object key order).
|
|
147
276
|
*/
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
};
|
|
277
|
+
function dotenvExpandAll(values, options = {}) {
|
|
278
|
+
const { ref = process.env, progressive = false, } = options;
|
|
279
|
+
const out = Object.keys(values).reduce((acc, key) => {
|
|
280
|
+
acc[key] = dotenvExpand(values[key], {
|
|
281
|
+
...ref,
|
|
282
|
+
...(progressive ? acc : {}),
|
|
283
|
+
});
|
|
284
|
+
return acc;
|
|
285
|
+
}, {});
|
|
286
|
+
// Key-preserving return with a permissive index signature to allow later additions.
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
291
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
292
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
293
|
+
* empty string.
|
|
294
|
+
*
|
|
295
|
+
* @param value - The string to expand.
|
|
296
|
+
* @returns The expanded string.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* process.env.FOO = 'bar';
|
|
301
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
157
305
|
|
|
306
|
+
/** @internal */
|
|
307
|
+
const isPlainObject = (v) => v !== null &&
|
|
308
|
+
typeof v === 'object' &&
|
|
309
|
+
!Array.isArray(v) &&
|
|
310
|
+
Object.getPrototypeOf(v) === Object.prototype;
|
|
158
311
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
312
|
+
* Deeply interpolate string leaves against envRef.
|
|
313
|
+
* Arrays are not recursed into; they are returned unchanged.
|
|
314
|
+
*
|
|
315
|
+
* @typeParam T - Shape of the input value.
|
|
316
|
+
* @param value - Input value (object/array/primitive).
|
|
317
|
+
* @param envRef - Reference environment for interpolation.
|
|
318
|
+
* @returns A new value with string leaves interpolated.
|
|
162
319
|
*/
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
320
|
+
const interpolateDeep = (value, envRef) => {
|
|
321
|
+
// Strings: expand and return
|
|
322
|
+
if (typeof value === 'string') {
|
|
323
|
+
const out = dotenvExpand(value, envRef);
|
|
324
|
+
// dotenvExpand returns string | undefined; preserve original on undefined
|
|
325
|
+
return (out ?? value);
|
|
326
|
+
}
|
|
327
|
+
// Arrays: return as-is (no recursion)
|
|
328
|
+
if (Array.isArray(value)) {
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
// Plain objects: shallow clone and recurse into values
|
|
332
|
+
if (isPlainObject(value)) {
|
|
333
|
+
const src = value;
|
|
334
|
+
const out = {};
|
|
335
|
+
for (const [k, v] of Object.entries(src)) {
|
|
336
|
+
// Recurse for strings/objects; keep arrays as-is; preserve other scalars
|
|
337
|
+
if (typeof v === 'string')
|
|
338
|
+
out[k] = dotenvExpand(v, envRef) ?? v;
|
|
339
|
+
else if (Array.isArray(v))
|
|
340
|
+
out[k] = v;
|
|
341
|
+
else if (isPlainObject(v))
|
|
342
|
+
out[k] = interpolateDeep(v, envRef);
|
|
343
|
+
else
|
|
344
|
+
out[k] = v;
|
|
175
345
|
}
|
|
346
|
+
return out;
|
|
176
347
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
348
|
+
// Other primitives/types: return as-is
|
|
349
|
+
return value;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const importDefault = async (fileUrl) => {
|
|
353
|
+
const mod = (await import(fileUrl));
|
|
354
|
+
return mod.default;
|
|
355
|
+
};
|
|
356
|
+
const cacheHash = (absPath, mtimeMs) => createHash('sha1')
|
|
357
|
+
.update(absPath)
|
|
358
|
+
.update(String(mtimeMs))
|
|
359
|
+
.digest('hex')
|
|
360
|
+
.slice(0, 12);
|
|
361
|
+
/**
|
|
362
|
+
* Remove older compiled cache files for a given source base name, keeping
|
|
363
|
+
* at most `keep` most-recent files. Errors are ignored by design.
|
|
364
|
+
*/
|
|
365
|
+
const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
|
|
366
|
+
try {
|
|
367
|
+
const entries = await fs.readdir(cacheDir);
|
|
368
|
+
const mine = entries
|
|
369
|
+
.filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
|
|
370
|
+
.map((f) => path.join(cacheDir, f));
|
|
371
|
+
if (mine.length <= keep)
|
|
372
|
+
return;
|
|
373
|
+
const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
|
|
374
|
+
stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
375
|
+
const toDelete = stats.slice(keep).map((s) => s.p);
|
|
376
|
+
await Promise.all(toDelete.map(async (p) => {
|
|
377
|
+
try {
|
|
378
|
+
await fs.remove(p);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// best-effort cleanup
|
|
382
|
+
}
|
|
383
|
+
}));
|
|
196
384
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
385
|
+
catch {
|
|
386
|
+
// best-effort cleanup
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Load a module default export from a JS/TS file with robust fallbacks:
|
|
391
|
+
* - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
|
|
392
|
+
* 1) try direct import (if a TS loader is active),
|
|
393
|
+
* 2) esbuild bundle to a temp ESM file,
|
|
394
|
+
* 3) typescript.transpileModule fallback for simple modules.
|
|
395
|
+
*
|
|
396
|
+
* @param absPath - absolute path to source file
|
|
397
|
+
* @param cacheDirName - cache subfolder under .tsbuild
|
|
398
|
+
*/
|
|
399
|
+
const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
400
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
401
|
+
const fileUrl = url.pathToFileURL(absPath).toString();
|
|
402
|
+
if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
|
|
403
|
+
return importDefault(fileUrl);
|
|
404
|
+
}
|
|
405
|
+
// Try direct import first (TS loader active)
|
|
406
|
+
try {
|
|
407
|
+
const dyn = await importDefault(fileUrl);
|
|
408
|
+
if (dyn)
|
|
409
|
+
return dyn;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
/* fall through */
|
|
413
|
+
}
|
|
414
|
+
const stat = await fs.stat(absPath);
|
|
415
|
+
const hash = cacheHash(absPath, stat.mtimeMs);
|
|
416
|
+
const cacheDir = path.resolve('.tsbuild', cacheDirName);
|
|
417
|
+
await fs.ensureDir(cacheDir);
|
|
418
|
+
const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
|
|
419
|
+
// Try esbuild
|
|
420
|
+
try {
|
|
421
|
+
const esbuild = (await import('esbuild'));
|
|
422
|
+
await esbuild.build({
|
|
423
|
+
entryPoints: [absPath],
|
|
424
|
+
bundle: true,
|
|
425
|
+
platform: 'node',
|
|
426
|
+
format: 'esm',
|
|
427
|
+
target: 'node20',
|
|
428
|
+
outfile: cacheFile,
|
|
429
|
+
sourcemap: false,
|
|
430
|
+
logLevel: 'silent',
|
|
431
|
+
});
|
|
432
|
+
const result = await importDefault(url.pathToFileURL(cacheFile).toString());
|
|
433
|
+
// Best-effort: trim older cache files for this source.
|
|
434
|
+
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
/* fall through to TS transpile */
|
|
439
|
+
}
|
|
440
|
+
// TypeScript transpile fallback
|
|
441
|
+
try {
|
|
442
|
+
const ts = (await import('typescript'));
|
|
443
|
+
const code = await fs.readFile(absPath, 'utf-8');
|
|
444
|
+
const out = ts.transpileModule(code, {
|
|
445
|
+
compilerOptions: {
|
|
446
|
+
module: 'ESNext',
|
|
447
|
+
target: 'ES2022',
|
|
448
|
+
moduleResolution: 'NodeNext',
|
|
449
|
+
},
|
|
450
|
+
}).outputText;
|
|
451
|
+
await fs.writeFile(cacheFile, out, 'utf-8');
|
|
452
|
+
const result = await importDefault(url.pathToFileURL(cacheFile).toString());
|
|
453
|
+
// Best-effort: trim older cache files for this source.
|
|
454
|
+
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Caller decides final error wording; rethrow for upstream mapping.
|
|
459
|
+
throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
|
|
200
460
|
}
|
|
201
|
-
merged.shell = resolvedShell;
|
|
202
|
-
const cmd = typeof command === 'string' ? command : undefined;
|
|
203
|
-
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
204
461
|
};
|
|
205
462
|
|
|
463
|
+
/** src/util/omitUndefined.ts
|
|
464
|
+
* Helpers to drop undefined-valued properties in a typed-friendly way.
|
|
465
|
+
*/
|
|
206
466
|
/**
|
|
207
|
-
*
|
|
467
|
+
* Omit keys whose runtime value is undefined from a shallow object.
|
|
468
|
+
* Returns a Partial with non-undefined value types preserved.
|
|
469
|
+
*/
|
|
470
|
+
function omitUndefined(obj) {
|
|
471
|
+
const out = {};
|
|
472
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
473
|
+
if (v !== undefined)
|
|
474
|
+
out[k] = v;
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Specialized helper for env-like maps: drop undefined and return string-only.
|
|
480
|
+
*/
|
|
481
|
+
function omitUndefinedRecord(obj) {
|
|
482
|
+
const out = {};
|
|
483
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
484
|
+
if (v !== undefined)
|
|
485
|
+
out[k] = v;
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Minimal tokenizer for shell-off execution.
|
|
492
|
+
* Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
208
493
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* - RESOLVED: normalized shapes (paths always string[]).
|
|
212
|
-
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
494
|
+
* @param command - The command string to tokenize.
|
|
495
|
+
* @param opts - Tokenization options (e.g. quote handling).
|
|
213
496
|
*/
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
497
|
+
const tokenize = (command, opts) => {
|
|
498
|
+
const out = [];
|
|
499
|
+
let cur = '';
|
|
500
|
+
let quote = null;
|
|
501
|
+
for (let i = 0; i < command.length; i++) {
|
|
502
|
+
const c = command.charAt(i);
|
|
503
|
+
if (quote) {
|
|
504
|
+
if (c === quote) {
|
|
505
|
+
// Support doubled quotes inside a quoted segment:
|
|
506
|
+
// default: "" -> " and '' -> ' (Windows/PowerShell style)
|
|
507
|
+
// preserve: keep as "" to allow empty string literals in Node -e payloads
|
|
508
|
+
const next = command.charAt(i + 1);
|
|
509
|
+
if (next === quote) {
|
|
510
|
+
{
|
|
511
|
+
// Collapse to a single literal quote
|
|
512
|
+
cur += quote;
|
|
513
|
+
i += 1; // skip the second quote
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// end of quoted segment
|
|
518
|
+
quote = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
cur += c;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
if (c === '"' || c === "'") {
|
|
527
|
+
quote = c;
|
|
528
|
+
}
|
|
529
|
+
else if (/\s/.test(c)) {
|
|
530
|
+
if (cur) {
|
|
531
|
+
out.push(cur);
|
|
532
|
+
cur = '';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
cur += c;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (cur)
|
|
541
|
+
out.push(cur);
|
|
542
|
+
return out;
|
|
543
|
+
};
|
|
242
544
|
|
|
545
|
+
/**
|
|
546
|
+
* @packageDocumentation
|
|
547
|
+
* Configuration discovery and loading for get‑dotenv. Discovers config files
|
|
548
|
+
* in the packaged root and project root, loads JSON/YAML/JS/TS documents, and
|
|
549
|
+
* validates them against Zod schemas.
|
|
550
|
+
*/
|
|
243
551
|
// Discovery candidates (first match wins per scope/privacy).
|
|
244
552
|
// Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
|
|
245
553
|
const PUBLIC_FILENAMES = [
|
|
@@ -267,75 +575,6 @@ const LOCAL_FILENAMES = [
|
|
|
267
575
|
const isYaml = (p) => ['.yaml', '.yml'].includes(extname(p).toLowerCase());
|
|
268
576
|
const isJson = (p) => extname(p).toLowerCase() === '.json';
|
|
269
577
|
const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(extname(p).toLowerCase());
|
|
270
|
-
// --- Internal JS/TS module loader helpers (default export) ---
|
|
271
|
-
const importDefault$1 = async (fileUrl) => {
|
|
272
|
-
const mod = (await import(fileUrl));
|
|
273
|
-
return mod.default;
|
|
274
|
-
};
|
|
275
|
-
const cacheName = (absPath, suffix) => {
|
|
276
|
-
// sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
|
|
277
|
-
const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
278
|
-
return `${base}.${suffix}.mjs`;
|
|
279
|
-
};
|
|
280
|
-
const ensureDir = async (dir) => {
|
|
281
|
-
await fs.ensureDir(dir);
|
|
282
|
-
return dir;
|
|
283
|
-
};
|
|
284
|
-
const loadJsTsDefault = async (absPath) => {
|
|
285
|
-
const fileUrl = pathToFileURL(absPath).toString();
|
|
286
|
-
const ext = extname(absPath).toLowerCase();
|
|
287
|
-
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
288
|
-
return importDefault$1(fileUrl);
|
|
289
|
-
}
|
|
290
|
-
// Try direct import first in case a TS loader is active.
|
|
291
|
-
try {
|
|
292
|
-
const val = await importDefault$1(fileUrl);
|
|
293
|
-
if (val)
|
|
294
|
-
return val;
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
/* fallthrough */
|
|
298
|
-
}
|
|
299
|
-
// esbuild bundle to a temp ESM file
|
|
300
|
-
try {
|
|
301
|
-
const esbuild = (await import('esbuild'));
|
|
302
|
-
const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
303
|
-
const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
|
|
304
|
-
await esbuild.build({
|
|
305
|
-
entryPoints: [absPath],
|
|
306
|
-
bundle: true,
|
|
307
|
-
platform: 'node',
|
|
308
|
-
format: 'esm',
|
|
309
|
-
target: 'node20',
|
|
310
|
-
outfile,
|
|
311
|
-
sourcemap: false,
|
|
312
|
-
logLevel: 'silent',
|
|
313
|
-
});
|
|
314
|
-
return await importDefault$1(pathToFileURL(outfile).toString());
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
/* fallthrough to TS transpile */
|
|
318
|
-
}
|
|
319
|
-
// typescript.transpileModule simple transpile (single-file)
|
|
320
|
-
try {
|
|
321
|
-
const ts = (await import('typescript'));
|
|
322
|
-
const src = await fs.readFile(absPath, 'utf-8');
|
|
323
|
-
const out = ts.transpileModule(src, {
|
|
324
|
-
compilerOptions: {
|
|
325
|
-
module: 'ESNext',
|
|
326
|
-
target: 'ES2022',
|
|
327
|
-
moduleResolution: 'NodeNext',
|
|
328
|
-
},
|
|
329
|
-
}).outputText;
|
|
330
|
-
const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
331
|
-
const outfile = path.join(outDir, cacheName(absPath, 'ts'));
|
|
332
|
-
await fs.writeFile(outfile, out, 'utf-8');
|
|
333
|
-
return await importDefault$1(pathToFileURL(outfile).toString());
|
|
334
|
-
}
|
|
335
|
-
catch {
|
|
336
|
-
throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
578
|
/**
|
|
340
579
|
* Discover JSON/YAML config files in the packaged root and project root.
|
|
341
580
|
* Order: packaged public → project public → project local. */
|
|
@@ -388,8 +627,8 @@ const loadConfigFile = async (filePath) => {
|
|
|
388
627
|
try {
|
|
389
628
|
const abs = path.resolve(filePath);
|
|
390
629
|
if (isJsOrTs(abs)) {
|
|
391
|
-
// JS/TS support: load default export via robust pipeline.
|
|
392
|
-
const mod = await
|
|
630
|
+
// JS/TS support: load default export via shared robust pipeline.
|
|
631
|
+
const mod = await loadModuleDefault(abs, 'getdotenv-config');
|
|
393
632
|
raw = mod ?? {};
|
|
394
633
|
}
|
|
395
634
|
else {
|
|
@@ -428,526 +667,64 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
|
428
667
|
if (f.scope === 'packaged') {
|
|
429
668
|
// packaged public only
|
|
430
669
|
result.packaged = cfg;
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
result.project ??= {};
|
|
434
|
-
if (f.privacy === 'public')
|
|
435
|
-
result.project.public = cfg;
|
|
436
|
-
else
|
|
437
|
-
result.project.local = cfg;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return result;
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Validate a composed env against config-provided validation surfaces.
|
|
445
|
-
* Precedence for validation definitions:
|
|
446
|
-
* project.local -\> project.public -\> packaged
|
|
447
|
-
*
|
|
448
|
-
* Behavior:
|
|
449
|
-
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
450
|
-
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
451
|
-
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
452
|
-
*/
|
|
453
|
-
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
454
|
-
const pick = (getter) => {
|
|
455
|
-
const pl = sources.project?.local;
|
|
456
|
-
const pp = sources.project?.public;
|
|
457
|
-
const pk = sources.packaged;
|
|
458
|
-
return ((pl && getter(pl)) ||
|
|
459
|
-
(pp && getter(pp)) ||
|
|
460
|
-
(pk && getter(pk)) ||
|
|
461
|
-
undefined);
|
|
462
|
-
};
|
|
463
|
-
const schema = pick((cfg) => cfg['schema']);
|
|
464
|
-
if (schema &&
|
|
465
|
-
typeof schema.safeParse === 'function') {
|
|
466
|
-
try {
|
|
467
|
-
const parsed = schema.safeParse(finalEnv);
|
|
468
|
-
if (!parsed.success) {
|
|
469
|
-
// Try to render zod-style issues when available.
|
|
470
|
-
const err = parsed.error;
|
|
471
|
-
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
472
|
-
? err.issues.map((i) => {
|
|
473
|
-
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
474
|
-
const msg = i.message ?? 'Invalid value';
|
|
475
|
-
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
476
|
-
})
|
|
477
|
-
: ['[schema] validation failed'];
|
|
478
|
-
return issues;
|
|
479
|
-
}
|
|
480
|
-
return [];
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
// If schema invocation fails, surface a single diagnostic.
|
|
484
|
-
return [
|
|
485
|
-
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
486
|
-
];
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
490
|
-
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
491
|
-
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
492
|
-
if (missing.length > 0) {
|
|
493
|
-
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
return [];
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
500
|
-
|
|
501
|
-
// src/GetDotenvOptions.ts
|
|
502
|
-
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
503
|
-
/**
|
|
504
|
-
* Converts programmatic CLI options to `getDotenv` options. *
|
|
505
|
-
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
506
|
-
*
|
|
507
|
-
* @returns `getDotenv` options.
|
|
508
|
-
*/
|
|
509
|
-
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
510
|
-
/**
|
|
511
|
-
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
512
|
-
*
|
|
513
|
-
* - 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`
|
|
514
|
-
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
515
|
-
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
516
|
-
*
|
|
517
|
-
* @remarks
|
|
518
|
-
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
519
|
-
*/
|
|
520
|
-
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
521
|
-
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
522
|
-
const restObj = { ...rest };
|
|
523
|
-
delete restObj.debug;
|
|
524
|
-
delete restObj.scripts;
|
|
525
|
-
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
526
|
-
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
527
|
-
let parsedVars;
|
|
528
|
-
if (typeof vars === 'string') {
|
|
529
|
-
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
530
|
-
? RegExp(varsAssignorPattern)
|
|
531
|
-
: (varsAssignor ?? '=')));
|
|
532
|
-
parsedVars = Object.fromEntries(kvPairs);
|
|
533
|
-
}
|
|
534
|
-
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
535
|
-
// Keep only string or undefined values to match ProcessEnv.
|
|
536
|
-
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
537
|
-
parsedVars = Object.fromEntries(entries);
|
|
538
|
-
}
|
|
539
|
-
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
540
|
-
// expectations and the compat test assertions.
|
|
541
|
-
if (parsedVars) {
|
|
542
|
-
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
543
|
-
}
|
|
544
|
-
// Tolerate paths as either a delimited string or string[]
|
|
545
|
-
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
546
|
-
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
547
|
-
const pathsAny = paths;
|
|
548
|
-
const pathsOut = Array.isArray(pathsAny)
|
|
549
|
-
? pathsAny.filter((p) => typeof p === 'string')
|
|
550
|
-
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
551
|
-
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
552
|
-
return {
|
|
553
|
-
...restObj,
|
|
554
|
-
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
555
|
-
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
556
|
-
};
|
|
557
|
-
};
|
|
558
|
-
const resolveGetDotenvOptions = async (customOptions) => {
|
|
559
|
-
/**
|
|
560
|
-
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
561
|
-
*
|
|
562
|
-
* 1. Base defaults derived from the CLI generator defaults
|
|
563
|
-
* ({@link baseGetDotenvCliOptions}).
|
|
564
|
-
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
565
|
-
* package root (if present).
|
|
566
|
-
* 3. The provided {@link customOptions}.
|
|
567
|
-
*
|
|
568
|
-
* The result preserves explicit empty values and drops only `undefined`.
|
|
569
|
-
*
|
|
570
|
-
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
571
|
-
*
|
|
572
|
-
* @example
|
|
573
|
-
* ```ts
|
|
574
|
-
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
575
|
-
* ```
|
|
576
|
-
*/
|
|
577
|
-
const localPkgDir = await packageDirectory();
|
|
578
|
-
const localOptionsPath = localPkgDir
|
|
579
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
580
|
-
: undefined;
|
|
581
|
-
// Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
|
|
582
|
-
let localOptions = {};
|
|
583
|
-
if (localOptionsPath && (await fs.exists(localOptionsPath))) {
|
|
584
|
-
try {
|
|
585
|
-
const txt = await fs.readFile(localOptionsPath, 'utf-8');
|
|
586
|
-
const parsed = JSON.parse(txt);
|
|
587
|
-
if (parsed && typeof parsed === 'object') {
|
|
588
|
-
localOptions = parsed;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
catch {
|
|
592
|
-
// Malformed or unreadable local options are treated as absent.
|
|
593
|
-
localOptions = {};
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
// Merge order: base < local < custom (custom has highest precedence)
|
|
597
|
-
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
598
|
-
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
599
|
-
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
600
|
-
return {
|
|
601
|
-
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
602
|
-
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
603
|
-
};
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* Dotenv expansion utilities.
|
|
608
|
-
*
|
|
609
|
-
* This module implements recursive expansion of environment-variable
|
|
610
|
-
* references in strings and records. It supports both whitespace and
|
|
611
|
-
* bracket syntaxes with optional defaults:
|
|
612
|
-
*
|
|
613
|
-
* - Whitespace: `$VAR[:default]`
|
|
614
|
-
* - Bracketed: `${VAR[:default]}`
|
|
615
|
-
*
|
|
616
|
-
* Escaped dollar signs (`\$`) are preserved.
|
|
617
|
-
* Unknown variables resolve to empty string unless a default is provided.
|
|
618
|
-
*/
|
|
619
|
-
/**
|
|
620
|
-
* Like String.prototype.search but returns the last index.
|
|
621
|
-
* @internal
|
|
622
|
-
*/
|
|
623
|
-
const searchLast = (str, rgx) => {
|
|
624
|
-
const matches = Array.from(str.matchAll(rgx));
|
|
625
|
-
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
626
|
-
};
|
|
627
|
-
const replaceMatch = (value, match, ref) => {
|
|
628
|
-
/**
|
|
629
|
-
* @internal
|
|
630
|
-
*/
|
|
631
|
-
const group = match[0];
|
|
632
|
-
const key = match[1];
|
|
633
|
-
const defaultValue = match[2];
|
|
634
|
-
if (!key)
|
|
635
|
-
return value;
|
|
636
|
-
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
637
|
-
return interpolate(replacement, ref);
|
|
638
|
-
};
|
|
639
|
-
const interpolate = (value = '', ref = {}) => {
|
|
640
|
-
/**
|
|
641
|
-
* @internal
|
|
642
|
-
*/
|
|
643
|
-
// if value is falsy, return it as is
|
|
644
|
-
if (!value)
|
|
645
|
-
return value;
|
|
646
|
-
// get position of last unescaped dollar sign
|
|
647
|
-
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
648
|
-
// return value if none found
|
|
649
|
-
if (lastUnescapedDollarSignIndex === -1)
|
|
650
|
-
return value;
|
|
651
|
-
// evaluate the value tail
|
|
652
|
-
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
653
|
-
// find whitespace pattern: $KEY:DEFAULT
|
|
654
|
-
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
655
|
-
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
656
|
-
if (whitespaceMatch != null)
|
|
657
|
-
return replaceMatch(value, whitespaceMatch, ref);
|
|
658
|
-
else {
|
|
659
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
660
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
661
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
662
|
-
if (bracketMatch != null)
|
|
663
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
664
|
-
}
|
|
665
|
-
return value;
|
|
666
|
-
};
|
|
667
|
-
/**
|
|
668
|
-
* Recursively expands environment variables in a string. Variables may be
|
|
669
|
-
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
670
|
-
* Unknown variables will expand to an empty string.
|
|
671
|
-
*
|
|
672
|
-
* @param value - The string to expand.
|
|
673
|
-
* @param ref - The reference object to use for variable expansion.
|
|
674
|
-
* @returns The expanded string.
|
|
675
|
-
*
|
|
676
|
-
* @example
|
|
677
|
-
* ```ts
|
|
678
|
-
* process.env.FOO = 'bar';
|
|
679
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
680
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
681
|
-
* ```
|
|
682
|
-
*
|
|
683
|
-
* @remarks
|
|
684
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
685
|
-
* references, those will also be expanded until a stable value is reached.
|
|
686
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
687
|
-
*/
|
|
688
|
-
const dotenvExpand = (value, ref = process.env) => {
|
|
689
|
-
const result = interpolate(value, ref);
|
|
690
|
-
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
691
|
-
};
|
|
692
|
-
/**
|
|
693
|
-
* Recursively expands environment variables in the values of a JSON object.
|
|
694
|
-
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
695
|
-
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
696
|
-
*
|
|
697
|
-
* @param values - The values object to expand.
|
|
698
|
-
* @param options - Expansion options.
|
|
699
|
-
* @returns The value object with expanded string values.
|
|
700
|
-
*
|
|
701
|
-
* @example
|
|
702
|
-
* ```ts
|
|
703
|
-
* process.env.FOO = 'bar';
|
|
704
|
-
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
705
|
-
* // => { A: "bar", B: "xbary" }
|
|
706
|
-
* ```
|
|
707
|
-
*
|
|
708
|
-
* @remarks
|
|
709
|
-
* Options:
|
|
710
|
-
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
711
|
-
* - progressive: Whether to progressively add expanded values to the set of
|
|
712
|
-
* reference keys.
|
|
713
|
-
*
|
|
714
|
-
* When `progressive` is true, each expanded key becomes available for
|
|
715
|
-
* subsequent expansions in the same object (left-to-right by object key order).
|
|
716
|
-
*/
|
|
717
|
-
const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
|
|
718
|
-
const { ref = process.env, progressive = false } = options;
|
|
719
|
-
acc[key] = dotenvExpand(values[key], {
|
|
720
|
-
...ref,
|
|
721
|
-
...(progressive ? acc : {}),
|
|
722
|
-
});
|
|
723
|
-
return acc;
|
|
724
|
-
}, {});
|
|
725
|
-
/**
|
|
726
|
-
* Recursively expands environment variables in a string using `process.env` as
|
|
727
|
-
* the expansion reference. Variables may be presented with optional default as
|
|
728
|
-
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
729
|
-
* empty string.
|
|
730
|
-
*
|
|
731
|
-
* @param value - The string to expand.
|
|
732
|
-
* @returns The expanded string.
|
|
733
|
-
*
|
|
734
|
-
* @example
|
|
735
|
-
* ```ts
|
|
736
|
-
* process.env.FOO = 'bar';
|
|
737
|
-
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
738
|
-
* ```
|
|
739
|
-
*/
|
|
740
|
-
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
741
|
-
|
|
742
|
-
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
743
|
-
/**
|
|
744
|
-
* Attach root flags to a GetDotenvCli instance.
|
|
745
|
-
* - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
|
|
746
|
-
* - Any flag that displays an effective default in help uses dynamic descriptions.
|
|
747
|
-
*/
|
|
748
|
-
const attachRootOptions = (program, defaults, opts) => {
|
|
749
|
-
// Install temporary wrappers to tag all options added here as "base" for grouped help.
|
|
750
|
-
const GROUP = 'base';
|
|
751
|
-
const tagLatest = (cmd, group) => {
|
|
752
|
-
const optsArr = cmd.options;
|
|
753
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
754
|
-
const last = optsArr[optsArr.length - 1];
|
|
755
|
-
last.__group = group;
|
|
756
|
-
}
|
|
757
|
-
};
|
|
758
|
-
const originalAddOption = program.addOption.bind(program);
|
|
759
|
-
const originalOption = program.option.bind(program);
|
|
760
|
-
program.addOption = function patchedAdd(opt) {
|
|
761
|
-
opt.__group = GROUP;
|
|
762
|
-
return originalAddOption(opt);
|
|
763
|
-
};
|
|
764
|
-
program.option = function patchedOption(...args) {
|
|
765
|
-
const ret = originalOption(...args);
|
|
766
|
-
tagLatest(this, GROUP);
|
|
767
|
-
return ret;
|
|
768
|
-
};
|
|
769
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
770
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
771
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
772
|
-
// Helper: append (default) tags for ON/OFF toggles
|
|
773
|
-
const onOff = (on, isDefault) => on
|
|
774
|
-
? `ON${isDefault ? ' (default)' : ''}`
|
|
775
|
-
: `OFF${isDefault ? ' (default)' : ''}`;
|
|
776
|
-
let p = program
|
|
777
|
-
.enablePositionalOptions()
|
|
778
|
-
.passThroughOptions()
|
|
779
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
780
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
781
|
-
['KEY1', 'VAL1'],
|
|
782
|
-
['KEY2', 'VAL2'],
|
|
783
|
-
]
|
|
784
|
-
.map((v) => v.join(va))
|
|
785
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
786
|
-
if (opts?.includeCommandOption === true) {
|
|
787
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
result.project ??= {};
|
|
673
|
+
if (f.privacy === 'public')
|
|
674
|
+
result.project.public = cfg;
|
|
675
|
+
else
|
|
676
|
+
result.project.local = cfg;
|
|
677
|
+
}
|
|
788
678
|
}
|
|
789
|
-
|
|
790
|
-
p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
|
|
791
|
-
// Shell ON (string or boolean true => default shell)
|
|
792
|
-
p = p
|
|
793
|
-
.addOption(program
|
|
794
|
-
.createDynamicOption('-s, --shell [string]', (cfg) => {
|
|
795
|
-
const s = cfg.shell;
|
|
796
|
-
let tag = '';
|
|
797
|
-
if (typeof s === 'boolean' && s)
|
|
798
|
-
tag = ' (default OS shell)';
|
|
799
|
-
else if (typeof s === 'string' && s.length > 0)
|
|
800
|
-
tag = ` (default ${s})`;
|
|
801
|
-
return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
|
|
802
|
-
})
|
|
803
|
-
.conflicts('shellOff'))
|
|
804
|
-
// Shell OFF
|
|
805
|
-
.addOption(program
|
|
806
|
-
.createDynamicOption('-S, --shell-off', (cfg) => {
|
|
807
|
-
const s = cfg.shell;
|
|
808
|
-
return `command execution shell OFF${s === false ? ' (default)' : ''}`;
|
|
809
|
-
})
|
|
810
|
-
.conflicts('shell'));
|
|
811
|
-
// Load process ON/OFF (dynamic defaults)
|
|
812
|
-
p = p
|
|
813
|
-
.addOption(program
|
|
814
|
-
.createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
|
|
815
|
-
.conflicts('loadProcessOff'))
|
|
816
|
-
.addOption(program
|
|
817
|
-
.createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
|
|
818
|
-
.conflicts('loadProcess'));
|
|
819
|
-
// Exclusion master toggle (dynamic)
|
|
820
|
-
p = p
|
|
821
|
-
.addOption(program
|
|
822
|
-
.createDynamicOption('-a, --exclude-all', (cfg) => {
|
|
823
|
-
const c = cfg;
|
|
824
|
-
const allOn = !!c.excludeDynamic &&
|
|
825
|
-
((!!c.excludeEnv && !!c.excludeGlobal) ||
|
|
826
|
-
(!!c.excludePrivate && !!c.excludePublic));
|
|
827
|
-
const suffix = allOn ? ' (default)' : '';
|
|
828
|
-
return `exclude all dotenv variables from loading ON${suffix}`;
|
|
829
|
-
})
|
|
830
|
-
.conflicts('excludeAllOff'))
|
|
831
|
-
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
|
|
832
|
-
// Per-family exclusions (dynamic defaults)
|
|
833
|
-
p = p
|
|
834
|
-
.addOption(program
|
|
835
|
-
.createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
|
|
836
|
-
.conflicts('excludeDynamicOff'))
|
|
837
|
-
.addOption(program
|
|
838
|
-
.createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
|
|
839
|
-
.conflicts('excludeDynamic'))
|
|
840
|
-
.addOption(program
|
|
841
|
-
.createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
|
|
842
|
-
.conflicts('excludeEnvOff'))
|
|
843
|
-
.addOption(program
|
|
844
|
-
.createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
|
|
845
|
-
.conflicts('excludeEnv'))
|
|
846
|
-
.addOption(program
|
|
847
|
-
.createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
|
|
848
|
-
.conflicts('excludeGlobalOff'))
|
|
849
|
-
.addOption(program
|
|
850
|
-
.createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
|
|
851
|
-
.conflicts('excludeGlobal'))
|
|
852
|
-
.addOption(program
|
|
853
|
-
.createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
|
|
854
|
-
.conflicts('excludePrivateOff'))
|
|
855
|
-
.addOption(program
|
|
856
|
-
.createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
|
|
857
|
-
.conflicts('excludePrivate'))
|
|
858
|
-
.addOption(program
|
|
859
|
-
.createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
|
|
860
|
-
.conflicts('excludePublicOff'))
|
|
861
|
-
.addOption(program
|
|
862
|
-
.createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
|
|
863
|
-
.conflicts('excludePublic'));
|
|
864
|
-
// Log ON/OFF (dynamic)
|
|
865
|
-
p = p
|
|
866
|
-
.addOption(program
|
|
867
|
-
.createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
|
|
868
|
-
.conflicts('logOff'))
|
|
869
|
-
.addOption(program
|
|
870
|
-
.createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
|
|
871
|
-
.conflicts('log'));
|
|
872
|
-
// Capture flag (no default display; static)
|
|
873
|
-
p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
|
|
874
|
-
// Core bootstrap/static flags (kept static in help)
|
|
875
|
-
p = p
|
|
876
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
877
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
878
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
879
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
880
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
881
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
882
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
883
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
884
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
885
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
886
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
887
|
-
// Hidden scripts pipe-through (stringified)
|
|
888
|
-
.addOption(new Option('--scripts <string>')
|
|
889
|
-
.default(JSON.stringify(scripts))
|
|
890
|
-
.hideHelp());
|
|
891
|
-
// Diagnostics / validation / entropy
|
|
892
|
-
p = p
|
|
893
|
-
.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
|
|
894
|
-
.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
895
|
-
p = p
|
|
896
|
-
.addOption(program
|
|
897
|
-
.createDynamicOption('--entropy-warn', (cfg) => {
|
|
898
|
-
const warn = cfg.warnEntropy;
|
|
899
|
-
// Default is effectively ON when warnEntropy is true or undefined.
|
|
900
|
-
return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
|
|
901
|
-
})
|
|
902
|
-
.conflicts('entropyWarnOff'))
|
|
903
|
-
.addOption(program
|
|
904
|
-
.createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
|
|
905
|
-
.conflicts('entropyWarn'))
|
|
906
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
907
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
908
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
909
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
910
|
-
// Restore original methods
|
|
911
|
-
program.addOption = originalAddOption;
|
|
912
|
-
program.option = originalOption;
|
|
913
|
-
return p;
|
|
679
|
+
return result;
|
|
914
680
|
};
|
|
915
681
|
|
|
682
|
+
/** src/env/dynamic.ts
|
|
683
|
+
* Helpers for applying and loading dynamic variables (JS/TS).
|
|
684
|
+
*
|
|
685
|
+
* Requirements addressed:
|
|
686
|
+
* - Single service to apply a dynamic map progressively.
|
|
687
|
+
* - Single service to load a JS/TS dynamic module with robust fallbacks (util/loadModuleDefault).
|
|
688
|
+
* - Unify error messaging so callers show consistent guidance.
|
|
689
|
+
*/
|
|
916
690
|
/**
|
|
917
|
-
*
|
|
691
|
+
* Apply a dynamic map to the target progressively.
|
|
692
|
+
* - Functions receive (target, env) and may return string | undefined.
|
|
693
|
+
* - Literals are assigned directly (including undefined).
|
|
694
|
+
*/
|
|
695
|
+
function applyDynamicMap(target, map, env) {
|
|
696
|
+
if (!map)
|
|
697
|
+
return;
|
|
698
|
+
for (const key of Object.keys(map)) {
|
|
699
|
+
const val = typeof map[key] === 'function'
|
|
700
|
+
? map[key](target, env)
|
|
701
|
+
: map[key];
|
|
702
|
+
Object.assign(target, { [key]: val });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Load a default-export dynamic map from a JS/TS file and apply it.
|
|
707
|
+
* Uses util/loadModuleDefault for robust TS handling (direct import, esbuild,
|
|
708
|
+
* typescript.transpile fallback).
|
|
918
709
|
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
* later per the staged plan.
|
|
710
|
+
* Error behavior:
|
|
711
|
+
* - On failure to load/compile/evaluate the module, throws a unified message:
|
|
712
|
+
* "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
|
|
923
713
|
*/
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
excludePublic: z.boolean().optional(),
|
|
939
|
-
loadProcess: z.boolean().optional(),
|
|
940
|
-
log: z.boolean().optional(),
|
|
941
|
-
outputPath: z.string().optional(),
|
|
942
|
-
paths: z.array(z.string()).optional(),
|
|
943
|
-
privateToken: z.string().optional(),
|
|
944
|
-
vars: processEnvSchema.optional(),
|
|
945
|
-
// Host-only feature flag: guarded integration of config loader/overlay
|
|
946
|
-
useConfigLoader: z.boolean().optional(),
|
|
947
|
-
});
|
|
948
|
-
// RESOLVED: service-boundary contract (post-inheritance).
|
|
949
|
-
// For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
|
|
950
|
-
const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
|
|
714
|
+
async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
|
|
715
|
+
if (!(await fs.exists(absPath)))
|
|
716
|
+
return;
|
|
717
|
+
let dyn;
|
|
718
|
+
try {
|
|
719
|
+
dyn = await loadModuleDefault(absPath, cacheDirName);
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// Preserve legacy/clear guidance used by tests and docs.
|
|
723
|
+
throw new Error(`Unable to load dynamic TypeScript file: ${absPath}. ` +
|
|
724
|
+
`Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
|
|
725
|
+
}
|
|
726
|
+
applyDynamicMap(target, dyn, env);
|
|
727
|
+
}
|
|
951
728
|
|
|
952
729
|
const applyKv = (current, kv) => {
|
|
953
730
|
if (!kv || Object.keys(kv).length === 0)
|
|
@@ -963,16 +740,8 @@ const applyConfigSlice = (current, cfg, env) => {
|
|
|
963
740
|
const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
|
|
964
741
|
return applyKv(afterGlobal, envKv);
|
|
965
742
|
};
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
* - kind: env \> global
|
|
969
|
-
* - privacy: local \> public
|
|
970
|
-
* - source: project \> packaged \> base
|
|
971
|
-
*
|
|
972
|
-
* Programmatic explicit vars (if provided) override all config slices.
|
|
973
|
-
* Progressive expansion is applied within each slice.
|
|
974
|
-
*/
|
|
975
|
-
const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
743
|
+
function overlayEnv(args) {
|
|
744
|
+
const { base, env, configs } = args;
|
|
976
745
|
let current = { ...base };
|
|
977
746
|
// Source: packaged (public -> local)
|
|
978
747
|
current = applyConfigSlice(current, configs.packaged, env);
|
|
@@ -982,12 +751,12 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
|
982
751
|
current = applyConfigSlice(current, configs.project?.public, env);
|
|
983
752
|
current = applyConfigSlice(current, configs.project?.local, env);
|
|
984
753
|
// Programmatic explicit vars (top of static tier)
|
|
985
|
-
if (programmaticVars) {
|
|
986
|
-
const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
|
|
754
|
+
if ('programmaticVars' in args) {
|
|
755
|
+
const toApply = Object.fromEntries(Object.entries(args.programmaticVars).filter(([_k, v]) => typeof v === 'string'));
|
|
987
756
|
current = applyKv(current, toApply);
|
|
988
757
|
}
|
|
989
758
|
return current;
|
|
990
|
-
}
|
|
759
|
+
}
|
|
991
760
|
|
|
992
761
|
/** src/diagnostics/entropy.ts
|
|
993
762
|
* Entropy diagnostics (presentation-only).
|
|
@@ -997,7 +766,7 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
|
997
766
|
*/
|
|
998
767
|
const warned = new Set();
|
|
999
768
|
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
1000
|
-
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
769
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => (typeof p === 'string' ? new RegExp(p, 'i') : p));
|
|
1001
770
|
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
1002
771
|
const shannonBitsPerChar = (s) => {
|
|
1003
772
|
const freq = new Map();
|
|
@@ -1044,7 +813,7 @@ const DEFAULT_PATTERNS = [
|
|
|
1044
813
|
'\\bapi[_-]?key\\b',
|
|
1045
814
|
'\\bkey\\b',
|
|
1046
815
|
];
|
|
1047
|
-
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
816
|
+
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => typeof p === 'string' ? new RegExp(p, 'i') : p);
|
|
1048
817
|
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
1049
818
|
const MASK = '[redacted]';
|
|
1050
819
|
/**
|
|
@@ -1062,168 +831,137 @@ const redactObject = (obj, opts) => {
|
|
|
1062
831
|
};
|
|
1063
832
|
|
|
1064
833
|
/**
|
|
1065
|
-
*
|
|
834
|
+
* Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
835
|
+
* Used as the bottom layer for CLI option resolution.
|
|
836
|
+
*/
|
|
837
|
+
/**
|
|
838
|
+
* Default values for root CLI options used by the host and helpers as the
|
|
839
|
+
* baseline layer during option resolution.
|
|
1066
840
|
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
841
|
+
* These defaults correspond to the "stringly" root surface (see `RootOptionsShape`)
|
|
842
|
+
* and are merged by precedence with create-time overrides and any discovered
|
|
843
|
+
* configuration `rootOptionDefaults` before CLI flags are applied.
|
|
1069
844
|
*/
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
845
|
+
const baseRootOptionDefaults = {
|
|
846
|
+
dotenvToken: '.env',
|
|
847
|
+
loadProcess: true,
|
|
848
|
+
logger: console,
|
|
849
|
+
// Diagnostics defaults
|
|
850
|
+
warnEntropy: true,
|
|
851
|
+
entropyThreshold: 3.8,
|
|
852
|
+
entropyMinLength: 16,
|
|
853
|
+
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
854
|
+
paths: './',
|
|
855
|
+
pathsDelimiter: ' ',
|
|
856
|
+
privateToken: 'local',
|
|
857
|
+
scripts: {
|
|
858
|
+
'git-status': {
|
|
859
|
+
cmd: 'git branch --show-current && git status -s -u',
|
|
860
|
+
shell: true,
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
shell: true,
|
|
864
|
+
vars: '',
|
|
865
|
+
varsAssignor: '=',
|
|
866
|
+
varsDelimiter: ' ',
|
|
867
|
+
// tri-state flags default to unset unless explicitly provided
|
|
868
|
+
// (debug/log/exclude* resolved via flag utils)
|
|
1077
869
|
};
|
|
1078
870
|
|
|
1079
|
-
const importDefault = async (fileUrl) => {
|
|
1080
|
-
const mod = (await import(fileUrl));
|
|
1081
|
-
return mod.default;
|
|
1082
|
-
};
|
|
1083
|
-
const cacheHash = (absPath, mtimeMs) => createHash('sha1')
|
|
1084
|
-
.update(absPath)
|
|
1085
|
-
.update(String(mtimeMs))
|
|
1086
|
-
.digest('hex')
|
|
1087
|
-
.slice(0, 12);
|
|
1088
871
|
/**
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
872
|
+
* Converts programmatic CLI options to `getDotenv` options.
|
|
873
|
+
*
|
|
874
|
+
* Accepts "stringly" CLI inputs for vars/paths and normalizes them into
|
|
875
|
+
* the programmatic shape. Preserves exactOptionalPropertyTypes semantics by
|
|
876
|
+
* omitting keys when undefined.
|
|
1091
877
|
*/
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
878
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern,
|
|
879
|
+
// drop CLI-only keys from the pass-through bag
|
|
880
|
+
debug: _debug, scripts: _scripts, ...rest }) => {
|
|
881
|
+
// Split helper for delimited strings or regex patterns
|
|
882
|
+
const splitBy = (value, delim, pattern) => {
|
|
883
|
+
if (!value)
|
|
884
|
+
return [];
|
|
885
|
+
if (pattern)
|
|
886
|
+
return value.split(RegExp(pattern));
|
|
887
|
+
if (typeof delim === 'string')
|
|
888
|
+
return value.split(delim);
|
|
889
|
+
return value.split(' ');
|
|
890
|
+
};
|
|
891
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
892
|
+
let parsedVars;
|
|
893
|
+
if (typeof vars === 'string') {
|
|
894
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern)
|
|
895
|
+
.map((v) => v.split(varsAssignorPattern
|
|
896
|
+
? RegExp(varsAssignorPattern)
|
|
897
|
+
: (varsAssignor ?? '=')))
|
|
898
|
+
.filter(([k]) => typeof k === 'string' && k.length > 0);
|
|
899
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
1111
900
|
}
|
|
1112
|
-
|
|
1113
|
-
//
|
|
901
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
902
|
+
// Accept provided object map of string | undefined; drop undefined values
|
|
903
|
+
// in the normalization step below to produce a ProcessEnv-compatible bag.
|
|
904
|
+
parsedVars = Object.fromEntries(Object.entries(vars));
|
|
1114
905
|
}
|
|
906
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
907
|
+
// expectations and the compat test assertions.
|
|
908
|
+
if (parsedVars) {
|
|
909
|
+
parsedVars = omitUndefinedRecord(parsedVars);
|
|
910
|
+
}
|
|
911
|
+
// Tolerate paths as either a delimited string or string[]
|
|
912
|
+
const pathsOut = Array.isArray(paths)
|
|
913
|
+
? paths.filter((p) => typeof p === 'string')
|
|
914
|
+
: splitBy(paths, pathsDelimiter, pathsDelimiterPattern);
|
|
915
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
916
|
+
return {
|
|
917
|
+
// Ensure the required logger property is present. The base CLI defaults
|
|
918
|
+
// specify console as the logger; callers can override upstream if desired.
|
|
919
|
+
logger: console,
|
|
920
|
+
...rest,
|
|
921
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
922
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
923
|
+
};
|
|
1115
924
|
};
|
|
1116
925
|
/**
|
|
1117
|
-
*
|
|
1118
|
-
* - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
|
|
1119
|
-
* 1) try direct import (if a TS loader is active),
|
|
1120
|
-
* 2) esbuild bundle to a temp ESM file,
|
|
1121
|
-
* 3) typescript.transpileModule fallback for simple modules.
|
|
926
|
+
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
1122
927
|
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
928
|
+
* 1. Base defaults derived from the CLI generator defaults
|
|
929
|
+
* ({@link baseGetDotenvCliOptions}).
|
|
930
|
+
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
931
|
+
* package root (if present).
|
|
932
|
+
* 3. The provided customOptions.
|
|
933
|
+
*
|
|
934
|
+
* The result preserves explicit empty values and drops only `undefined`.
|
|
1125
935
|
*/
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
// Try esbuild
|
|
1147
|
-
try {
|
|
1148
|
-
const esbuild = (await import('esbuild'));
|
|
1149
|
-
await esbuild.build({
|
|
1150
|
-
entryPoints: [absPath],
|
|
1151
|
-
bundle: true,
|
|
1152
|
-
platform: 'node',
|
|
1153
|
-
format: 'esm',
|
|
1154
|
-
target: 'node20',
|
|
1155
|
-
outfile: cacheFile,
|
|
1156
|
-
sourcemap: false,
|
|
1157
|
-
logLevel: 'silent',
|
|
1158
|
-
});
|
|
1159
|
-
const result = await importDefault(url.pathToFileURL(cacheFile).toString());
|
|
1160
|
-
// Best-effort: trim older cache files for this source.
|
|
1161
|
-
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
1162
|
-
return result;
|
|
1163
|
-
}
|
|
1164
|
-
catch {
|
|
1165
|
-
/* fall through to TS transpile */
|
|
1166
|
-
}
|
|
1167
|
-
// TypeScript transpile fallback
|
|
936
|
+
const resolveGetDotenvOptions = (customOptions) => {
|
|
937
|
+
// Programmatic callers use neutral defaults only. Do not read local packaged
|
|
938
|
+
// getdotenv.config.json here; the host path applies packaged/project configs
|
|
939
|
+
// via the dedicated loader/overlay pipeline.
|
|
940
|
+
const mergedDefaults = baseRootOptionDefaults;
|
|
941
|
+
const defaultsFromCli = getDotenvCliOptions2Options(mergedDefaults);
|
|
942
|
+
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
943
|
+
return Promise.resolve({
|
|
944
|
+
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
945
|
+
vars: omitUndefinedRecord(result.vars ?? {}),
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Asynchronously read a dotenv file & parse it into an object.
|
|
951
|
+
*
|
|
952
|
+
* @param path - Path to dotenv file.
|
|
953
|
+
* @returns The parsed dotenv object.
|
|
954
|
+
*/
|
|
955
|
+
const readDotenv = async (path) => {
|
|
1168
956
|
try {
|
|
1169
|
-
|
|
1170
|
-
const code = await fs.readFile(absPath, 'utf-8');
|
|
1171
|
-
const out = ts.transpileModule(code, {
|
|
1172
|
-
compilerOptions: {
|
|
1173
|
-
module: 'ESNext',
|
|
1174
|
-
target: 'ES2022',
|
|
1175
|
-
moduleResolution: 'NodeNext',
|
|
1176
|
-
},
|
|
1177
|
-
}).outputText;
|
|
1178
|
-
await fs.writeFile(cacheFile, out, 'utf-8');
|
|
1179
|
-
const result = await importDefault(url.pathToFileURL(cacheFile).toString());
|
|
1180
|
-
// Best-effort: trim older cache files for this source.
|
|
1181
|
-
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
1182
|
-
return result;
|
|
957
|
+
return (await fs.exists(path)) ? parse(await fs.readFile(path)) : {};
|
|
1183
958
|
}
|
|
1184
959
|
catch {
|
|
1185
|
-
|
|
1186
|
-
throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
|
|
960
|
+
return {};
|
|
1187
961
|
}
|
|
1188
962
|
};
|
|
1189
963
|
|
|
1190
|
-
|
|
1191
|
-
* Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
|
|
1192
|
-
*
|
|
1193
|
-
* @param options - `GetDotenvOptions` object
|
|
1194
|
-
* @returns The combined parsed dotenv object.
|
|
1195
|
-
* * @example Load from the project root with default tokens
|
|
1196
|
-
* ```ts
|
|
1197
|
-
* const vars = await getDotenv();
|
|
1198
|
-
* console.log(vars.MY_SETTING);
|
|
1199
|
-
* ```
|
|
1200
|
-
*
|
|
1201
|
-
* @example Load from multiple paths and a specific environment
|
|
1202
|
-
* ```ts
|
|
1203
|
-
* const vars = await getDotenv({
|
|
1204
|
-
* env: 'dev',
|
|
1205
|
-
* dotenvToken: '.testenv',
|
|
1206
|
-
* privateToken: 'secret',
|
|
1207
|
-
* paths: ['./', './packages/app'],
|
|
1208
|
-
* });
|
|
1209
|
-
* ```
|
|
1210
|
-
*
|
|
1211
|
-
* @example Use dynamic variables
|
|
1212
|
-
* ```ts
|
|
1213
|
-
* // .env.js default-exports: { DYNAMIC: ({ PREV }) => `${PREV}-suffix` }
|
|
1214
|
-
* const vars = await getDotenv({ dynamicPath: '.env.js' });
|
|
1215
|
-
* ```
|
|
1216
|
-
*
|
|
1217
|
-
* @remarks
|
|
1218
|
-
* - When {@link GetDotenvOptions.loadProcess} is true, the resulting variables are merged
|
|
1219
|
-
* into `process.env` as a side effect.
|
|
1220
|
-
* - When {@link GetDotenvOptions.outputPath} is provided, a consolidated dotenv file is written.
|
|
1221
|
-
* The path is resolved after expansion, so it may reference previously loaded vars.
|
|
1222
|
-
*
|
|
1223
|
-
* @throws Error when a dynamic module is present but cannot be imported.
|
|
1224
|
-
* @throws Error when an output path was requested but could not be resolved.
|
|
1225
|
-
*/
|
|
1226
|
-
const getDotenv = async (options = {}) => {
|
|
964
|
+
async function getDotenv(options = {}) {
|
|
1227
965
|
// Apply defaults.
|
|
1228
966
|
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);
|
|
1229
967
|
// Read .env files.
|
|
@@ -1271,25 +1009,11 @@ const getDotenv = async (options = {}) => {
|
|
|
1271
1009
|
}
|
|
1272
1010
|
else if (dynamicPath) {
|
|
1273
1011
|
const absDynamicPath = path.resolve(dynamicPath);
|
|
1274
|
-
|
|
1275
|
-
try {
|
|
1276
|
-
dynamic = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic');
|
|
1277
|
-
}
|
|
1278
|
-
catch {
|
|
1279
|
-
// Preserve legacy error text for compatibility with tests/docs.
|
|
1280
|
-
throw new Error(`Unable to load dynamic TypeScript file: ${absDynamicPath}. ` +
|
|
1281
|
-
`Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1012
|
+
await loadAndApplyDynamic(dotenv, absDynamicPath, env ?? defaultEnv, 'getdotenv-dynamic');
|
|
1284
1013
|
}
|
|
1285
1014
|
if (dynamic) {
|
|
1286
1015
|
try {
|
|
1287
|
-
|
|
1288
|
-
Object.assign(dotenv, {
|
|
1289
|
-
[key]: typeof dynamic[key] === 'function'
|
|
1290
|
-
? dynamic[key](dotenv, env ?? defaultEnv)
|
|
1291
|
-
: dynamic[key],
|
|
1292
|
-
});
|
|
1016
|
+
applyDynamicMap(dotenv, dynamic, env ?? defaultEnv);
|
|
1293
1017
|
}
|
|
1294
1018
|
catch {
|
|
1295
1019
|
throw new Error(`Unable to evaluate dynamic variables.`);
|
|
@@ -1303,10 +1027,7 @@ const getDotenv = async (options = {}) => {
|
|
|
1303
1027
|
if (!outputPathResolved)
|
|
1304
1028
|
throw new Error('Output path not found.');
|
|
1305
1029
|
const { [outputKey]: _omitted, ...dotenvForOutput } = dotenv;
|
|
1306
|
-
await
|
|
1307
|
-
const value = dotenvForOutput[key] ?? '';
|
|
1308
|
-
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
1309
|
-
}, ''), { encoding: 'utf-8' });
|
|
1030
|
+
await writeDotenvFile(outputPathResolved, dotenvForOutput);
|
|
1310
1031
|
resultDotenv = dotenvForOutput;
|
|
1311
1032
|
}
|
|
1312
1033
|
// Log result.
|
|
@@ -1328,8 +1049,7 @@ const getDotenv = async (options = {}) => {
|
|
|
1328
1049
|
.entropyThreshold;
|
|
1329
1050
|
const entropyMinLengthVal = options
|
|
1330
1051
|
.entropyMinLength;
|
|
1331
|
-
const entropyWhitelistVal = options
|
|
1332
|
-
.entropyWhitelist;
|
|
1052
|
+
const entropyWhitelistVal = options.entropyWhitelist;
|
|
1333
1053
|
const entOpts = {};
|
|
1334
1054
|
if (typeof warnEntropyVal === 'boolean')
|
|
1335
1055
|
entOpts.warnEntropy = warnEntropyVal;
|
|
@@ -1349,88 +1069,78 @@ const getDotenv = async (options = {}) => {
|
|
|
1349
1069
|
if (loadProcess)
|
|
1350
1070
|
Object.assign(process.env, resultDotenv);
|
|
1351
1071
|
return resultDotenv;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
/**
|
|
1355
|
-
* Deep interpolation utility for string leaves.
|
|
1356
|
-
* - Expands string values using dotenv-style expansion against the provided envRef.
|
|
1357
|
-
* - Preserves non-strings as-is.
|
|
1358
|
-
* - Does not recurse into arrays (arrays are returned unchanged).
|
|
1359
|
-
*
|
|
1360
|
-
* Intended for:
|
|
1361
|
-
* - Phase C option/config interpolation after composing ctx.dotenv.
|
|
1362
|
-
* - Per-plugin config slice interpolation before afterResolve.
|
|
1363
|
-
*/
|
|
1364
|
-
/** @internal */
|
|
1365
|
-
const isPlainObject = (v) => v !== null &&
|
|
1366
|
-
typeof v === 'object' &&
|
|
1367
|
-
!Array.isArray(v) &&
|
|
1368
|
-
Object.getPrototypeOf(v) === Object.prototype;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1369
1074
|
/**
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1075
|
+
* Compute the realized path for a command mount (leaf-up to root).
|
|
1076
|
+
* Excludes the root application alias.
|
|
1372
1077
|
*
|
|
1373
|
-
* @
|
|
1374
|
-
* @param value - Input value (object/array/primitive).
|
|
1375
|
-
* @param envRef - Reference environment for interpolation.
|
|
1376
|
-
* @returns A new value with string leaves interpolated.
|
|
1078
|
+
* @param cli - The mounted command instance.
|
|
1377
1079
|
*/
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
// Plain objects: shallow clone and recurse into values
|
|
1390
|
-
if (isPlainObject(value)) {
|
|
1391
|
-
const src = value;
|
|
1392
|
-
const out = {};
|
|
1393
|
-
for (const [k, v] of Object.entries(src)) {
|
|
1394
|
-
// Recurse for strings/objects; keep arrays as-is; preserve other scalars
|
|
1395
|
-
if (typeof v === 'string')
|
|
1396
|
-
out[k] = dotenvExpand(v, envRef) ?? v;
|
|
1397
|
-
else if (Array.isArray(v))
|
|
1398
|
-
out[k] = v;
|
|
1399
|
-
else if (isPlainObject(v))
|
|
1400
|
-
out[k] = interpolateDeep(v, envRef);
|
|
1401
|
-
else
|
|
1402
|
-
out[k] = v;
|
|
1080
|
+
/**
|
|
1081
|
+
* Flatten a plugin tree into a list of `{ plugin, path }` entries.
|
|
1082
|
+
* Traverses the namespace chain in pre-order.
|
|
1083
|
+
*/
|
|
1084
|
+
function flattenPluginTreeByPath(plugins, prefix) {
|
|
1085
|
+
const out = [];
|
|
1086
|
+
for (const p of plugins) {
|
|
1087
|
+
const here = prefix && prefix.length > 0 ? `${prefix}/${p.ns}` : p.ns;
|
|
1088
|
+
out.push({ plugin: p, path: here });
|
|
1089
|
+
if (Array.isArray(p.children) && p.children.length > 0) {
|
|
1090
|
+
out.push(...flattenPluginTreeByPath(p.children.map((c) => c.plugin), here));
|
|
1403
1091
|
}
|
|
1404
|
-
return out;
|
|
1405
1092
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
};
|
|
1093
|
+
return out;
|
|
1094
|
+
}
|
|
1409
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* Instance-bound plugin config store.
|
|
1098
|
+
* Host stores the validated/interpolated slice per plugin instance.
|
|
1099
|
+
* The store is intentionally private to this module; definePlugin()
|
|
1100
|
+
* provides a typed accessor that reads from this store for the calling
|
|
1101
|
+
* plugin instance.
|
|
1102
|
+
*/
|
|
1103
|
+
const PLUGIN_CONFIG_STORE = new WeakMap();
|
|
1104
|
+
/**
|
|
1105
|
+
* Store a validated, interpolated config slice for a specific plugin instance.
|
|
1106
|
+
* Generic on both the host options type and the plugin config type to avoid
|
|
1107
|
+
* defaulting to GetDotenvOptions under exactOptionalPropertyTypes.
|
|
1108
|
+
*/
|
|
1109
|
+
const setPluginConfig = (plugin, cfg) => {
|
|
1110
|
+
PLUGIN_CONFIG_STORE.set(plugin, cfg);
|
|
1111
|
+
};
|
|
1112
|
+
/**
|
|
1113
|
+
* Retrieve the validated/interpolated config slice for a plugin instance.
|
|
1114
|
+
*/
|
|
1115
|
+
const getPluginConfig = (plugin) => {
|
|
1116
|
+
return PLUGIN_CONFIG_STORE.get(plugin);
|
|
1117
|
+
};
|
|
1410
1118
|
/**
|
|
1411
1119
|
* Compute the dotenv context for the host (uses the config loader/overlay path).
|
|
1412
1120
|
* - Resolves and validates options strictly (host-only).
|
|
1413
1121
|
* - Applies file cascade, overlays, dynamics, and optional effects.
|
|
1414
|
-
* - Merges and validates per-plugin config slices (when provided)
|
|
1122
|
+
* - Merges and validates per-plugin config slices (when provided), keyed by
|
|
1123
|
+
* realized mount path (ns chain).
|
|
1415
1124
|
*
|
|
1416
1125
|
* @param customOptions - Partial options from the current invocation.
|
|
1417
1126
|
* @param plugins - Installed plugins (for config validation).
|
|
1418
|
-
* @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery).
|
|
1127
|
+
* @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery).
|
|
1128
|
+
*/
|
|
1419
1129
|
const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
1420
1130
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1131
|
+
// Zod boundary: parse returns the schema-derived shape; we adopt our public
|
|
1132
|
+
// GetDotenvOptions overlay (logger/dynamic typing) for internal processing.
|
|
1421
1133
|
const validated = getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
1422
|
-
//
|
|
1423
|
-
|
|
1134
|
+
// Build a pure base without side effects or logging (no dynamics, no programmatic vars).
|
|
1135
|
+
const cleanedValidated = omitUndefined(validated);
|
|
1424
1136
|
const base = await getDotenv({
|
|
1425
|
-
...
|
|
1426
|
-
// Build a pure base without side effects or logging.
|
|
1137
|
+
...cleanedValidated,
|
|
1427
1138
|
excludeDynamic: true,
|
|
1428
1139
|
vars: {},
|
|
1429
1140
|
log: false,
|
|
1430
1141
|
loadProcess: false,
|
|
1431
|
-
outputPath: undefined,
|
|
1432
1142
|
});
|
|
1433
|
-
//
|
|
1143
|
+
// Discover config sources and overlay with progressive expansion per slice.
|
|
1434
1144
|
const sources = await resolveGetDotenvConfigSources(hostMetaUrl);
|
|
1435
1145
|
const dotenvOverlaid = overlayEnv({
|
|
1436
1146
|
base,
|
|
@@ -1438,47 +1148,31 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1438
1148
|
configs: sources,
|
|
1439
1149
|
...(validated.vars ? { programmaticVars: validated.vars } : {}),
|
|
1440
1150
|
});
|
|
1441
|
-
// Helper to apply a dynamic map progressively.
|
|
1442
|
-
const applyDynamic = (target, dynamic, env) => {
|
|
1443
|
-
if (!dynamic)
|
|
1444
|
-
return;
|
|
1445
|
-
for (const key of Object.keys(dynamic)) {
|
|
1446
|
-
const value = typeof dynamic[key] === 'function'
|
|
1447
|
-
? dynamic[key](target, env)
|
|
1448
|
-
: dynamic[key];
|
|
1449
|
-
Object.assign(target, { [key]: value });
|
|
1450
|
-
}
|
|
1451
|
-
};
|
|
1452
|
-
// 3) Apply dynamics in order
|
|
1453
1151
|
const dotenv = { ...dotenvOverlaid };
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1152
|
+
// Programmatic dynamic variables (when provided)
|
|
1153
|
+
applyDynamicMap(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
|
|
1154
|
+
// Packaged/project dynamics
|
|
1155
|
+
const packagedDyn = (sources.packaged?.dynamic ?? undefined);
|
|
1156
|
+
const publicDyn = (sources.project?.public?.dynamic ?? undefined);
|
|
1157
|
+
const localDyn = (sources.project?.local?.dynamic ?? undefined);
|
|
1158
|
+
applyDynamicMap(dotenv, packagedDyn, validated.env ?? validated.defaultEnv);
|
|
1159
|
+
applyDynamicMap(dotenv, publicDyn, validated.env ?? validated.defaultEnv);
|
|
1160
|
+
applyDynamicMap(dotenv, localDyn, validated.env ?? validated.defaultEnv);
|
|
1458
1161
|
// file dynamicPath (lowest)
|
|
1459
1162
|
if (validated.dynamicPath) {
|
|
1460
1163
|
const absDynamicPath = path.resolve(validated.dynamicPath);
|
|
1461
|
-
|
|
1462
|
-
const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
|
|
1463
|
-
applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
|
|
1464
|
-
}
|
|
1465
|
-
catch {
|
|
1466
|
-
throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
|
|
1467
|
-
}
|
|
1164
|
+
await loadAndApplyDynamic(dotenv, absDynamicPath, validated.env ?? validated.defaultEnv, 'getdotenv-dynamic-host');
|
|
1468
1165
|
}
|
|
1469
|
-
//
|
|
1166
|
+
// Effects:
|
|
1470
1167
|
if (validated.outputPath) {
|
|
1471
|
-
await
|
|
1472
|
-
const value = dotenv[key] ?? '';
|
|
1473
|
-
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
1474
|
-
}, ''), { encoding: 'utf-8' });
|
|
1168
|
+
await writeDotenvFile(validated.outputPath, dotenv);
|
|
1475
1169
|
}
|
|
1476
|
-
const logger = validated.logger
|
|
1170
|
+
const logger = validated.logger;
|
|
1477
1171
|
if (validated.log)
|
|
1478
1172
|
logger.log(dotenv);
|
|
1479
1173
|
if (validated.loadProcess)
|
|
1480
1174
|
Object.assign(process.env, dotenv);
|
|
1481
|
-
//
|
|
1175
|
+
// Merge and validate per-plugin config keyed by realized path (ns chain).
|
|
1482
1176
|
const packagedPlugins = (sources.packaged &&
|
|
1483
1177
|
sources.packaged.plugins) ??
|
|
1484
1178
|
{};
|
|
@@ -1488,45 +1182,881 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1488
1182
|
const localPlugins = (sources.project?.local &&
|
|
1489
1183
|
sources.project.local.plugins) ??
|
|
1490
1184
|
{};
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
if (p.configSchema) {
|
|
1507
|
-
const parsed = p.configSchema.safeParse(interpolated);
|
|
1185
|
+
const entries = flattenPluginTreeByPath(plugins);
|
|
1186
|
+
const mergedPluginConfigsByPath = {};
|
|
1187
|
+
const envRef = {
|
|
1188
|
+
...dotenv,
|
|
1189
|
+
...process.env,
|
|
1190
|
+
};
|
|
1191
|
+
for (const e of entries) {
|
|
1192
|
+
const pathKey = e.path;
|
|
1193
|
+
const mergedRaw = defaultsDeep({}, packagedPlugins[pathKey] ?? {}, publicPlugins[pathKey] ?? {}, localPlugins[pathKey] ?? {});
|
|
1194
|
+
const interpolated = mergedRaw && typeof mergedRaw === 'object'
|
|
1195
|
+
? interpolateDeep(mergedRaw, envRef)
|
|
1196
|
+
: {};
|
|
1197
|
+
const schema = e.plugin.configSchema;
|
|
1198
|
+
if (schema) {
|
|
1199
|
+
const parsed = schema.safeParse(interpolated);
|
|
1508
1200
|
if (!parsed.success) {
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1201
|
+
const err = parsed.error;
|
|
1202
|
+
const msgs = err.issues
|
|
1203
|
+
.map((i) => {
|
|
1204
|
+
const pth = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1205
|
+
const msg = typeof i.message === 'string' ? i.message : 'Invalid value';
|
|
1206
|
+
return pth ? `${pth}: ${msg}` : msg;
|
|
1207
|
+
})
|
|
1511
1208
|
.join('\n');
|
|
1512
|
-
throw new Error(`Invalid config for plugin '${
|
|
1209
|
+
throw new Error(`Invalid config for plugin at '${pathKey}':\n${msgs}`);
|
|
1513
1210
|
}
|
|
1514
|
-
|
|
1211
|
+
const frozen = Object.freeze(parsed.data);
|
|
1212
|
+
setPluginConfig(e.plugin, frozen);
|
|
1213
|
+
mergedPluginConfigsByPath[pathKey] = frozen;
|
|
1515
1214
|
}
|
|
1516
1215
|
else {
|
|
1517
|
-
|
|
1216
|
+
const frozen = Object.freeze(interpolated);
|
|
1217
|
+
setPluginConfig(e.plugin, frozen);
|
|
1218
|
+
mergedPluginConfigsByPath[pathKey] = frozen;
|
|
1518
1219
|
}
|
|
1519
1220
|
}
|
|
1520
1221
|
return {
|
|
1521
1222
|
optionsResolved: validated,
|
|
1522
|
-
dotenv
|
|
1223
|
+
dotenv,
|
|
1523
1224
|
plugins: {},
|
|
1524
|
-
pluginConfigs:
|
|
1225
|
+
pluginConfigs: mergedPluginConfigsByPath,
|
|
1226
|
+
};
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
// Implementation
|
|
1230
|
+
function definePlugin(spec) {
|
|
1231
|
+
const { ...rest } = spec;
|
|
1232
|
+
const effectiveSchema = spec.configSchema ?? z.object({}).strict();
|
|
1233
|
+
const base = {
|
|
1234
|
+
...rest,
|
|
1235
|
+
configSchema: effectiveSchema,
|
|
1236
|
+
children: [],
|
|
1237
|
+
use(child, override) {
|
|
1238
|
+
// Enforce sibling uniqueness at composition time.
|
|
1239
|
+
const desired = (override && typeof override.ns === 'string' && override.ns.length > 0
|
|
1240
|
+
? override.ns
|
|
1241
|
+
: child.ns).trim();
|
|
1242
|
+
const collision = this.children.some((c) => {
|
|
1243
|
+
const ns = (c.override &&
|
|
1244
|
+
typeof c.override.ns === 'string' &&
|
|
1245
|
+
c.override.ns.length > 0
|
|
1246
|
+
? c.override.ns
|
|
1247
|
+
: c.plugin.ns).trim();
|
|
1248
|
+
return ns === desired;
|
|
1249
|
+
});
|
|
1250
|
+
if (collision) {
|
|
1251
|
+
const under = this.ns && this.ns.length > 0 ? this.ns : 'root';
|
|
1252
|
+
throw new Error(`Duplicate namespace '${desired}' under '${under}'. ` +
|
|
1253
|
+
`Override via .use(plugin, { ns: '...' }).`);
|
|
1254
|
+
}
|
|
1255
|
+
this.children.push({ plugin: child, override });
|
|
1256
|
+
return this;
|
|
1257
|
+
},
|
|
1258
|
+
};
|
|
1259
|
+
const extended = base;
|
|
1260
|
+
extended.readConfig = function (_cli) {
|
|
1261
|
+
const value = getPluginConfig(extended);
|
|
1262
|
+
if (value === undefined) {
|
|
1263
|
+
throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
|
|
1264
|
+
}
|
|
1265
|
+
return value;
|
|
1266
|
+
};
|
|
1267
|
+
extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
|
|
1268
|
+
// Derive realized path strictly from the provided mount (leaf-up).
|
|
1269
|
+
const realizedPath = (() => {
|
|
1270
|
+
const parts = [];
|
|
1271
|
+
let node = cli;
|
|
1272
|
+
while (node.parent) {
|
|
1273
|
+
parts.push(node.name());
|
|
1274
|
+
node = node.parent;
|
|
1275
|
+
}
|
|
1276
|
+
return parts.reverse().join('/');
|
|
1277
|
+
})();
|
|
1278
|
+
return cli.createDynamicOption(flags, (c) => {
|
|
1279
|
+
const fromStore = getPluginConfig(extended);
|
|
1280
|
+
let cfgVal = fromStore ?? {};
|
|
1281
|
+
// Strict fallback only by realized path for help-time synthetic usage.
|
|
1282
|
+
if (!fromStore && realizedPath.length > 0) {
|
|
1283
|
+
const bag = c.plugins;
|
|
1284
|
+
const maybe = bag[realizedPath];
|
|
1285
|
+
if (maybe && typeof maybe === 'object') {
|
|
1286
|
+
cfgVal = maybe;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
// c is strictly typed as ResolvedHelpConfig from cli.createDynamicOption
|
|
1290
|
+
return desc(c, cfgVal);
|
|
1291
|
+
}, parser, defaultValue);
|
|
1292
|
+
};
|
|
1293
|
+
return extended;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const dbg = (...args) => {
|
|
1297
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
1298
|
+
// Use stderr to avoid interfering with stdout assertions
|
|
1299
|
+
console.error('[getdotenv:run]', ...args);
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
// Strip repeated symmetric outer quotes (single or double) until stable.
|
|
1303
|
+
// This is safe for argv arrays passed to execa (no quoting needed) and avoids
|
|
1304
|
+
// passing quote characters through to Node (e.g., for `node -e "<code>"`).
|
|
1305
|
+
// Handles stacked quotes from shells like PowerShell: """code""" -> code.
|
|
1306
|
+
const stripOuterQuotes = (s) => {
|
|
1307
|
+
let out = s;
|
|
1308
|
+
// Repeatedly trim only when the entire string is wrapped in matching quotes.
|
|
1309
|
+
// Stop as soon as the ends are asymmetric or no quotes remain.
|
|
1310
|
+
while (out.length >= 2) {
|
|
1311
|
+
const a = out.charAt(0);
|
|
1312
|
+
const b = out.charAt(out.length - 1);
|
|
1313
|
+
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
1314
|
+
if (!symmetric)
|
|
1315
|
+
break;
|
|
1316
|
+
out = out.slice(1, -1);
|
|
1317
|
+
}
|
|
1318
|
+
return out;
|
|
1319
|
+
};
|
|
1320
|
+
// Extract exitCode/stdout/stderr from execa result or error in a tolerant way.
|
|
1321
|
+
const pickResult = (r) => {
|
|
1322
|
+
const exit = r.exitCode;
|
|
1323
|
+
const stdoutVal = r.stdout;
|
|
1324
|
+
const stderrVal = r.stderr;
|
|
1325
|
+
return {
|
|
1326
|
+
exitCode: typeof exit === 'number' ? exit : Number.NaN,
|
|
1327
|
+
stdout: typeof stdoutVal === 'string' ? stdoutVal : '',
|
|
1328
|
+
stderr: typeof stderrVal === 'string' ? stderrVal : '',
|
|
1329
|
+
};
|
|
1330
|
+
};
|
|
1331
|
+
// Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
|
|
1332
|
+
// expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
|
|
1333
|
+
const sanitizeEnv = (env) => {
|
|
1334
|
+
if (!env)
|
|
1335
|
+
return undefined;
|
|
1336
|
+
const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
|
|
1337
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
1338
|
+
};
|
|
1339
|
+
/**
|
|
1340
|
+
* Core executor that normalizes shell/plain forms and capture/inherit modes.
|
|
1341
|
+
* Returns captured buffers; callers may stream stdout when desired.
|
|
1342
|
+
*/
|
|
1343
|
+
async function _execNormalized(command, shell, opts = {}) {
|
|
1344
|
+
const envSan = sanitizeEnv(opts.env);
|
|
1345
|
+
const timeoutBits = typeof opts.timeoutMs === 'number'
|
|
1346
|
+
? { timeout: opts.timeoutMs, killSignal: 'SIGKILL' }
|
|
1347
|
+
: {};
|
|
1348
|
+
const stdio = opts.stdio ?? 'pipe';
|
|
1349
|
+
if (shell === false) {
|
|
1350
|
+
let file;
|
|
1351
|
+
let args = [];
|
|
1352
|
+
if (typeof command === 'string') {
|
|
1353
|
+
const tokens = tokenize(command);
|
|
1354
|
+
file = tokens[0];
|
|
1355
|
+
args = tokens.slice(1);
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
file = command[0];
|
|
1359
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
1360
|
+
}
|
|
1361
|
+
if (!file)
|
|
1362
|
+
return { exitCode: 0, stdout: '', stderr: '' };
|
|
1363
|
+
dbg('exec (plain)', { file, args, stdio });
|
|
1364
|
+
try {
|
|
1365
|
+
const ok = pickResult((await execa(file, args, {
|
|
1366
|
+
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
1367
|
+
...(envSan !== undefined ? { env: envSan } : {}),
|
|
1368
|
+
stdio,
|
|
1369
|
+
...timeoutBits,
|
|
1370
|
+
})));
|
|
1371
|
+
dbg('exit (plain)', { exitCode: ok.exitCode });
|
|
1372
|
+
return ok;
|
|
1373
|
+
}
|
|
1374
|
+
catch (e) {
|
|
1375
|
+
const out = pickResult(e);
|
|
1376
|
+
dbg('exit:error (plain)', { exitCode: out.exitCode });
|
|
1377
|
+
return out;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
// Shell path (string|true|URL): execaCommand handles shell resolution.
|
|
1381
|
+
const commandStr = typeof command === 'string' ? command : command.join(' ');
|
|
1382
|
+
dbg('exec (shell)', {
|
|
1383
|
+
command: commandStr,
|
|
1384
|
+
shell: typeof shell === 'string' ? shell : 'custom',
|
|
1385
|
+
stdio,
|
|
1386
|
+
});
|
|
1387
|
+
try {
|
|
1388
|
+
const ok = pickResult((await execaCommand(commandStr, {
|
|
1389
|
+
shell,
|
|
1390
|
+
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
1391
|
+
...(envSan !== undefined ? { env: envSan } : {}),
|
|
1392
|
+
stdio,
|
|
1393
|
+
...timeoutBits,
|
|
1394
|
+
})));
|
|
1395
|
+
dbg('exit (shell)', { exitCode: ok.exitCode });
|
|
1396
|
+
return ok;
|
|
1397
|
+
}
|
|
1398
|
+
catch (e) {
|
|
1399
|
+
const out = pickResult(e);
|
|
1400
|
+
dbg('exit:error (shell)', { exitCode: out.exitCode });
|
|
1401
|
+
return out;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function runCommandResult(command, shell, opts = {}) {
|
|
1405
|
+
// Build opts without injecting undefined (exactOptionalPropertyTypes-safe)
|
|
1406
|
+
const coreOpts = { stdio: 'pipe' };
|
|
1407
|
+
if (opts.cwd !== undefined) {
|
|
1408
|
+
coreOpts.cwd = opts.cwd;
|
|
1409
|
+
}
|
|
1410
|
+
if (opts.env !== undefined) {
|
|
1411
|
+
coreOpts.env = opts.env;
|
|
1412
|
+
}
|
|
1413
|
+
if (opts.timeoutMs !== undefined) {
|
|
1414
|
+
coreOpts.timeoutMs = opts.timeoutMs;
|
|
1415
|
+
}
|
|
1416
|
+
return _execNormalized(command, shell, coreOpts);
|
|
1417
|
+
}
|
|
1418
|
+
async function runCommand(command, shell, opts) {
|
|
1419
|
+
// Build opts without injecting undefined (exactOptionalPropertyTypes-safe)
|
|
1420
|
+
const callOpts = {};
|
|
1421
|
+
if (opts.cwd !== undefined) {
|
|
1422
|
+
callOpts.cwd = opts.cwd;
|
|
1423
|
+
}
|
|
1424
|
+
if (opts.env !== undefined) {
|
|
1425
|
+
callOpts.env = opts.env;
|
|
1426
|
+
}
|
|
1427
|
+
if (opts.stdio !== undefined)
|
|
1428
|
+
callOpts.stdio = opts.stdio;
|
|
1429
|
+
const ok = await _execNormalized(command, shell, callOpts);
|
|
1430
|
+
if (opts.stdio === 'pipe' && ok.stdout) {
|
|
1431
|
+
process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
|
|
1432
|
+
}
|
|
1433
|
+
return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Attach root flags to a {@link GetDotenvCli} instance.
|
|
1438
|
+
*
|
|
1439
|
+
* Program is typed as {@link GetDotenvCli} and supports {@link GetDotenvCli.createDynamicOption | createDynamicOption}.
|
|
1440
|
+
*/
|
|
1441
|
+
const attachRootOptions = (program, defaults) => {
|
|
1442
|
+
const GROUP = 'base';
|
|
1443
|
+
const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
1444
|
+
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
1445
|
+
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
1446
|
+
// Helper: append (default) tags for ON/OFF toggles
|
|
1447
|
+
const onOff = (on, isDefault) => on
|
|
1448
|
+
? `ON${isDefault ? ' (default)' : ''}`
|
|
1449
|
+
: `OFF${isDefault ? ' (default)' : ''}`;
|
|
1450
|
+
program.enablePositionalOptions().passThroughOptions();
|
|
1451
|
+
// -e, --env <string>
|
|
1452
|
+
{
|
|
1453
|
+
const opt = new Option('-e, --env <string>', 'target environment (dotenv-expanded)');
|
|
1454
|
+
opt.argParser(dotenvExpandFromProcessEnv);
|
|
1455
|
+
if (env !== undefined)
|
|
1456
|
+
opt.default(env);
|
|
1457
|
+
program.addOption(opt);
|
|
1458
|
+
program.setOptionGroup(opt, GROUP);
|
|
1459
|
+
}
|
|
1460
|
+
// -v, --vars <string>
|
|
1461
|
+
{
|
|
1462
|
+
const examples = [
|
|
1463
|
+
['KEY1', 'VAL1'],
|
|
1464
|
+
['KEY2', 'VAL2'],
|
|
1465
|
+
]
|
|
1466
|
+
.map((v) => v.join(va))
|
|
1467
|
+
.join(vd);
|
|
1468
|
+
const opt = new Option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${examples}`);
|
|
1469
|
+
opt.argParser(dotenvExpandFromProcessEnv);
|
|
1470
|
+
program.addOption(opt);
|
|
1471
|
+
program.setOptionGroup(opt, GROUP);
|
|
1472
|
+
}
|
|
1473
|
+
// Output path (interpolated later; help can remain static)
|
|
1474
|
+
{
|
|
1475
|
+
const opt = new Option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)');
|
|
1476
|
+
opt.argParser(dotenvExpandFromProcessEnv);
|
|
1477
|
+
if (outputPath !== undefined)
|
|
1478
|
+
opt.default(outputPath);
|
|
1479
|
+
program.addOption(opt);
|
|
1480
|
+
program.setOptionGroup(opt, GROUP);
|
|
1481
|
+
}
|
|
1482
|
+
// Shell ON (string or boolean true => default shell)
|
|
1483
|
+
{
|
|
1484
|
+
const opt = program
|
|
1485
|
+
.createDynamicOption('-s, --shell [string]', (cfg) => {
|
|
1486
|
+
const s = cfg.shell;
|
|
1487
|
+
let tag = '';
|
|
1488
|
+
if (typeof s === 'boolean' && s)
|
|
1489
|
+
tag = ' (default OS shell)';
|
|
1490
|
+
else if (typeof s === 'string' && s.length > 0)
|
|
1491
|
+
tag = ` (default ${s})`;
|
|
1492
|
+
return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
|
|
1493
|
+
})
|
|
1494
|
+
.conflicts('shellOff');
|
|
1495
|
+
program.addOption(opt);
|
|
1496
|
+
program.setOptionGroup(opt, GROUP);
|
|
1497
|
+
}
|
|
1498
|
+
// Shell OFF
|
|
1499
|
+
{
|
|
1500
|
+
const opt = program
|
|
1501
|
+
.createDynamicOption('-S, --shell-off', (cfg) => {
|
|
1502
|
+
const s = cfg.shell;
|
|
1503
|
+
return `command execution shell OFF${s === false ? ' (default)' : ''}`;
|
|
1504
|
+
})
|
|
1505
|
+
.conflicts('shell');
|
|
1506
|
+
program.addOption(opt);
|
|
1507
|
+
program.setOptionGroup(opt, GROUP);
|
|
1508
|
+
}
|
|
1509
|
+
// Load process ON/OFF (dynamic defaults)
|
|
1510
|
+
{
|
|
1511
|
+
const optOn = program
|
|
1512
|
+
.createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
|
|
1513
|
+
.conflicts('loadProcessOff');
|
|
1514
|
+
program.addOption(optOn);
|
|
1515
|
+
program.setOptionGroup(optOn, GROUP);
|
|
1516
|
+
const optOff = program
|
|
1517
|
+
.createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
|
|
1518
|
+
.conflicts('loadProcess');
|
|
1519
|
+
program.addOption(optOff);
|
|
1520
|
+
program.setOptionGroup(optOff, GROUP);
|
|
1521
|
+
}
|
|
1522
|
+
// Exclusion master toggle (dynamic)
|
|
1523
|
+
{
|
|
1524
|
+
const optAll = program
|
|
1525
|
+
.createDynamicOption('-a, --exclude-all', (cfg) => {
|
|
1526
|
+
const allOn = !!cfg.excludeDynamic &&
|
|
1527
|
+
((!!cfg.excludeEnv && !!cfg.excludeGlobal) ||
|
|
1528
|
+
(!!cfg.excludePrivate && !!cfg.excludePublic));
|
|
1529
|
+
const suffix = allOn ? ' (default)' : '';
|
|
1530
|
+
return `exclude all dotenv variables from loading ON${suffix}`;
|
|
1531
|
+
})
|
|
1532
|
+
.conflicts('excludeAllOff');
|
|
1533
|
+
program.addOption(optAll);
|
|
1534
|
+
program.setOptionGroup(optAll, GROUP);
|
|
1535
|
+
const optAllOff = new Option('-A, --exclude-all-off', 'exclude all dotenv variables from loading OFF (default)').conflicts('excludeAll');
|
|
1536
|
+
program.addOption(optAllOff);
|
|
1537
|
+
program.setOptionGroup(optAllOff, GROUP);
|
|
1538
|
+
}
|
|
1539
|
+
// Per-family exclusions (dynamic defaults)
|
|
1540
|
+
{
|
|
1541
|
+
const o1 = program
|
|
1542
|
+
.createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
|
|
1543
|
+
.conflicts('excludeDynamicOff');
|
|
1544
|
+
program.addOption(o1);
|
|
1545
|
+
program.setOptionGroup(o1, GROUP);
|
|
1546
|
+
const o2 = program
|
|
1547
|
+
.createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
|
|
1548
|
+
.conflicts('excludeDynamic');
|
|
1549
|
+
program.addOption(o2);
|
|
1550
|
+
program.setOptionGroup(o2, GROUP);
|
|
1551
|
+
}
|
|
1552
|
+
{
|
|
1553
|
+
const o1 = program
|
|
1554
|
+
.createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
|
|
1555
|
+
.conflicts('excludeEnvOff');
|
|
1556
|
+
program.addOption(o1);
|
|
1557
|
+
program.setOptionGroup(o1, GROUP);
|
|
1558
|
+
const o2 = program
|
|
1559
|
+
.createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
|
|
1560
|
+
.conflicts('excludeEnv');
|
|
1561
|
+
program.addOption(o2);
|
|
1562
|
+
program.setOptionGroup(o2, GROUP);
|
|
1563
|
+
}
|
|
1564
|
+
{
|
|
1565
|
+
const o1 = program
|
|
1566
|
+
.createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
|
|
1567
|
+
.conflicts('excludeGlobalOff');
|
|
1568
|
+
program.addOption(o1);
|
|
1569
|
+
program.setOptionGroup(o1, GROUP);
|
|
1570
|
+
const o2 = program
|
|
1571
|
+
.createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
|
|
1572
|
+
.conflicts('excludeGlobal');
|
|
1573
|
+
program.addOption(o2);
|
|
1574
|
+
program.setOptionGroup(o2, GROUP);
|
|
1575
|
+
}
|
|
1576
|
+
{
|
|
1577
|
+
const p1 = program
|
|
1578
|
+
.createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
|
|
1579
|
+
.conflicts('excludePrivateOff');
|
|
1580
|
+
program.addOption(p1);
|
|
1581
|
+
program.setOptionGroup(p1, GROUP);
|
|
1582
|
+
const p2 = program
|
|
1583
|
+
.createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
|
|
1584
|
+
.conflicts('excludePrivate');
|
|
1585
|
+
program.addOption(p2);
|
|
1586
|
+
program.setOptionGroup(p2, GROUP);
|
|
1587
|
+
const pu1 = program
|
|
1588
|
+
.createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
|
|
1589
|
+
.conflicts('excludePublicOff');
|
|
1590
|
+
program.addOption(pu1);
|
|
1591
|
+
program.setOptionGroup(pu1, GROUP);
|
|
1592
|
+
const pu2 = program
|
|
1593
|
+
.createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
|
|
1594
|
+
.conflicts('excludePublic');
|
|
1595
|
+
program.addOption(pu2);
|
|
1596
|
+
program.setOptionGroup(pu2, GROUP);
|
|
1597
|
+
}
|
|
1598
|
+
// Log ON/OFF (dynamic)
|
|
1599
|
+
{
|
|
1600
|
+
const lo = program
|
|
1601
|
+
.createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
|
|
1602
|
+
.conflicts('logOff');
|
|
1603
|
+
program.addOption(lo);
|
|
1604
|
+
program.setOptionGroup(lo, GROUP);
|
|
1605
|
+
const lf = program
|
|
1606
|
+
.createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
|
|
1607
|
+
.conflicts('log');
|
|
1608
|
+
program.addOption(lf);
|
|
1609
|
+
program.setOptionGroup(lf, GROUP);
|
|
1610
|
+
}
|
|
1611
|
+
// Capture flag (no default display; static)
|
|
1612
|
+
{
|
|
1613
|
+
const opt = new Option('--capture', 'capture child process stdio for commands (tests/CI)');
|
|
1614
|
+
program.addOption(opt);
|
|
1615
|
+
program.setOptionGroup(opt, GROUP);
|
|
1616
|
+
}
|
|
1617
|
+
// Core bootstrap/static flags (kept static in help)
|
|
1618
|
+
{
|
|
1619
|
+
const o1 = new Option('--default-env <string>', 'default target environment');
|
|
1620
|
+
o1.argParser(dotenvExpandFromProcessEnv);
|
|
1621
|
+
if (defaultEnv !== undefined)
|
|
1622
|
+
o1.default(defaultEnv);
|
|
1623
|
+
program.addOption(o1);
|
|
1624
|
+
program.setOptionGroup(o1, GROUP);
|
|
1625
|
+
const o2 = new Option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file');
|
|
1626
|
+
o2.argParser(dotenvExpandFromProcessEnv);
|
|
1627
|
+
if (dotenvToken !== undefined)
|
|
1628
|
+
o2.default(dotenvToken);
|
|
1629
|
+
program.addOption(o2);
|
|
1630
|
+
program.setOptionGroup(o2, GROUP);
|
|
1631
|
+
const o3 = new Option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)');
|
|
1632
|
+
o3.argParser(dotenvExpandFromProcessEnv);
|
|
1633
|
+
if (dynamicPath !== undefined)
|
|
1634
|
+
o3.default(dynamicPath);
|
|
1635
|
+
program.addOption(o3);
|
|
1636
|
+
program.setOptionGroup(o3, GROUP);
|
|
1637
|
+
const o4 = new Option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory');
|
|
1638
|
+
o4.argParser(dotenvExpandFromProcessEnv);
|
|
1639
|
+
if (paths !== undefined)
|
|
1640
|
+
o4.default(paths);
|
|
1641
|
+
program.addOption(o4);
|
|
1642
|
+
program.setOptionGroup(o4, GROUP);
|
|
1643
|
+
const o5 = new Option('--paths-delimiter <string>', 'paths delimiter string');
|
|
1644
|
+
if (pathsDelimiter !== undefined)
|
|
1645
|
+
o5.default(pathsDelimiter);
|
|
1646
|
+
program.addOption(o5);
|
|
1647
|
+
program.setOptionGroup(o5, GROUP);
|
|
1648
|
+
const o6 = new Option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern');
|
|
1649
|
+
if (pathsDelimiterPattern !== undefined)
|
|
1650
|
+
o6.default(pathsDelimiterPattern);
|
|
1651
|
+
program.addOption(o6);
|
|
1652
|
+
program.setOptionGroup(o6, GROUP);
|
|
1653
|
+
const o7 = new Option('--private-token <string>', 'dotenv-expanded token indicating private variables');
|
|
1654
|
+
o7.argParser(dotenvExpandFromProcessEnv);
|
|
1655
|
+
if (privateToken !== undefined)
|
|
1656
|
+
o7.default(privateToken);
|
|
1657
|
+
program.addOption(o7);
|
|
1658
|
+
program.setOptionGroup(o7, GROUP);
|
|
1659
|
+
const o8 = new Option('--vars-delimiter <string>', 'vars delimiter string');
|
|
1660
|
+
if (varsDelimiter !== undefined)
|
|
1661
|
+
o8.default(varsDelimiter);
|
|
1662
|
+
program.addOption(o8);
|
|
1663
|
+
program.setOptionGroup(o8, GROUP);
|
|
1664
|
+
const o9 = new Option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern');
|
|
1665
|
+
if (varsDelimiterPattern !== undefined)
|
|
1666
|
+
o9.default(varsDelimiterPattern);
|
|
1667
|
+
program.addOption(o9);
|
|
1668
|
+
program.setOptionGroup(o9, GROUP);
|
|
1669
|
+
const o10 = new Option('--vars-assignor <string>', 'vars assignment operator string');
|
|
1670
|
+
if (varsAssignor !== undefined)
|
|
1671
|
+
o10.default(varsAssignor);
|
|
1672
|
+
program.addOption(o10);
|
|
1673
|
+
program.setOptionGroup(o10, GROUP);
|
|
1674
|
+
const o11 = new Option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern');
|
|
1675
|
+
if (varsAssignorPattern !== undefined)
|
|
1676
|
+
o11.default(varsAssignorPattern);
|
|
1677
|
+
program.addOption(o11);
|
|
1678
|
+
program.setOptionGroup(o11, GROUP);
|
|
1679
|
+
}
|
|
1680
|
+
// Diagnostics / validation / entropy
|
|
1681
|
+
{
|
|
1682
|
+
const tr = new Option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
1683
|
+
program.addOption(tr);
|
|
1684
|
+
program.setOptionGroup(tr, GROUP);
|
|
1685
|
+
const st = new Option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
1686
|
+
program.addOption(st);
|
|
1687
|
+
program.setOptionGroup(st, GROUP);
|
|
1688
|
+
}
|
|
1689
|
+
{
|
|
1690
|
+
const w = program
|
|
1691
|
+
.createDynamicOption('--entropy-warn', (cfg) => {
|
|
1692
|
+
const warn = cfg.warnEntropy;
|
|
1693
|
+
// Default is effectively ON when warnEntropy is true or undefined.
|
|
1694
|
+
return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
|
|
1695
|
+
})
|
|
1696
|
+
.conflicts('entropyWarnOff');
|
|
1697
|
+
program.addOption(w);
|
|
1698
|
+
program.setOptionGroup(w, GROUP);
|
|
1699
|
+
const woff = program
|
|
1700
|
+
.createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
|
|
1701
|
+
.conflicts('entropyWarn');
|
|
1702
|
+
program.addOption(woff);
|
|
1703
|
+
program.setOptionGroup(woff, GROUP);
|
|
1704
|
+
const th = new Option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)');
|
|
1705
|
+
program.addOption(th);
|
|
1706
|
+
program.setOptionGroup(th, GROUP);
|
|
1707
|
+
const ml = new Option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)');
|
|
1708
|
+
program.addOption(ml);
|
|
1709
|
+
program.setOptionGroup(ml, GROUP);
|
|
1710
|
+
const wl = new Option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern');
|
|
1711
|
+
program.addOption(wl);
|
|
1712
|
+
program.setOptionGroup(wl, GROUP);
|
|
1713
|
+
const rp = new Option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
1714
|
+
program.addOption(rp);
|
|
1715
|
+
program.setOptionGroup(rp, GROUP);
|
|
1716
|
+
// Redact ON/OFF (dynamic)
|
|
1717
|
+
{
|
|
1718
|
+
const rOn = program
|
|
1719
|
+
.createDynamicOption('--redact', (cfg) => `presentation-time redaction for secret-like keys ON${cfg.redact ? ' (default)' : ''}`)
|
|
1720
|
+
.conflicts('redactOff');
|
|
1721
|
+
program.addOption(rOn);
|
|
1722
|
+
program.setOptionGroup(rOn, GROUP);
|
|
1723
|
+
const rOff = program
|
|
1724
|
+
.createDynamicOption('--redact-off', (cfg) => `presentation-time redaction for secret-like keys OFF${cfg.redact === false ? ' (default)' : ''}`)
|
|
1725
|
+
.conflicts('redact');
|
|
1726
|
+
program.addOption(rOff);
|
|
1727
|
+
program.setOptionGroup(rOff, GROUP);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
return program;
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Registry for option grouping.
|
|
1735
|
+
* Root help renders these groups between "Options" and "Commands".
|
|
1736
|
+
*/
|
|
1737
|
+
const GROUP_TAG = new WeakMap();
|
|
1738
|
+
/**
|
|
1739
|
+
* Render help option groups (App/Plugins) for a given command.
|
|
1740
|
+
* Groups are injected between Options and Commands in the help output.
|
|
1741
|
+
*/
|
|
1742
|
+
function renderOptionGroups(cmd) {
|
|
1743
|
+
const all = cmd.options;
|
|
1744
|
+
const byGroup = new Map();
|
|
1745
|
+
for (const o of all) {
|
|
1746
|
+
const opt = o;
|
|
1747
|
+
const g = GROUP_TAG.get(opt);
|
|
1748
|
+
if (!g || g === 'base')
|
|
1749
|
+
continue; // base handled by default help
|
|
1750
|
+
const rows = byGroup.get(g) ?? [];
|
|
1751
|
+
rows.push({
|
|
1752
|
+
flags: opt.flags,
|
|
1753
|
+
description: opt.description ?? '',
|
|
1754
|
+
});
|
|
1755
|
+
byGroup.set(g, rows);
|
|
1756
|
+
}
|
|
1757
|
+
if (byGroup.size === 0)
|
|
1758
|
+
return '';
|
|
1759
|
+
const renderRows = (title, rows) => {
|
|
1760
|
+
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
1761
|
+
// Sort within group: short-aliased flags first
|
|
1762
|
+
rows.sort((a, b) => {
|
|
1763
|
+
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
1764
|
+
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
1765
|
+
return bS - aS || a.flags.localeCompare(b.flags);
|
|
1766
|
+
});
|
|
1767
|
+
const lines = rows
|
|
1768
|
+
.map((r) => {
|
|
1769
|
+
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
1770
|
+
return ` ${r.flags}${pad}${r.description}`.trimEnd();
|
|
1771
|
+
})
|
|
1772
|
+
.join('\n');
|
|
1773
|
+
return `\n${title}:\n${lines}\n`;
|
|
1774
|
+
};
|
|
1775
|
+
let out = '';
|
|
1776
|
+
// App options (if any)
|
|
1777
|
+
const app = byGroup.get('app');
|
|
1778
|
+
if (app && app.length > 0) {
|
|
1779
|
+
out += renderRows('App options', app);
|
|
1780
|
+
}
|
|
1781
|
+
// Plugin groups sorted by id; suppress self group on the owning command name.
|
|
1782
|
+
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1783
|
+
const currentName = cmd.name();
|
|
1784
|
+
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1785
|
+
for (const k of pluginKeys) {
|
|
1786
|
+
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1787
|
+
const rows = byGroup.get(k) ?? [];
|
|
1788
|
+
if (rows.length > 0 && id !== currentName) {
|
|
1789
|
+
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return out;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
/**
|
|
1796
|
+
* Compose root/parent help output by inserting grouped sections between
|
|
1797
|
+
* Options and Commands, ensuring a trailing blank line.
|
|
1798
|
+
*/
|
|
1799
|
+
function buildHelpInformation(base, cmd) {
|
|
1800
|
+
const groups = renderOptionGroups(cmd);
|
|
1801
|
+
const block = typeof groups === 'string' ? groups.trim() : '';
|
|
1802
|
+
if (!block) {
|
|
1803
|
+
return base.endsWith('\n\n')
|
|
1804
|
+
? base
|
|
1805
|
+
: base.endsWith('\n')
|
|
1806
|
+
? `${base}\n`
|
|
1807
|
+
: `${base}\n\n`;
|
|
1808
|
+
}
|
|
1809
|
+
const marker = '\nCommands:';
|
|
1810
|
+
const idx = base.indexOf(marker);
|
|
1811
|
+
let out = base;
|
|
1812
|
+
if (idx >= 0) {
|
|
1813
|
+
const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
|
|
1814
|
+
out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
|
|
1815
|
+
}
|
|
1816
|
+
else {
|
|
1817
|
+
const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
|
|
1818
|
+
out = `${base}${sep}${groups}`;
|
|
1819
|
+
}
|
|
1820
|
+
return out.endsWith('\n\n')
|
|
1821
|
+
? out
|
|
1822
|
+
: out.endsWith('\n')
|
|
1823
|
+
? `${out}\n`
|
|
1824
|
+
: `${out}\n\n`;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/** src/cliHost/GetDotenvCli/dynamicOptions.ts
|
|
1828
|
+
* Helpers for dynamic option descriptions and evaluation.
|
|
1829
|
+
*/
|
|
1830
|
+
/**
|
|
1831
|
+
* Registry for dynamic descriptions keyed by Option (WeakMap for GC safety).
|
|
1832
|
+
*/
|
|
1833
|
+
const DYN_DESC = new WeakMap();
|
|
1834
|
+
/**
|
|
1835
|
+
* Create an Option with a dynamic description callback stored in DYN_DESC.
|
|
1836
|
+
*/
|
|
1837
|
+
function makeDynamicOption(flags, desc, parser, defaultValue) {
|
|
1838
|
+
const opt = new Option(flags, '');
|
|
1839
|
+
DYN_DESC.set(opt, desc);
|
|
1840
|
+
if (parser) {
|
|
1841
|
+
opt.argParser((value, previous) => parser(value, previous));
|
|
1842
|
+
}
|
|
1843
|
+
if (defaultValue !== undefined)
|
|
1844
|
+
opt.default(defaultValue);
|
|
1845
|
+
// Commander.Option is structurally compatible; help-time wiring is stored in DYN_DESC.
|
|
1846
|
+
return opt;
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Evaluate dynamic descriptions across a command tree using the resolved config.
|
|
1850
|
+
*/
|
|
1851
|
+
function evaluateDynamicOptions(root, resolved) {
|
|
1852
|
+
const visit = (cmd) => {
|
|
1853
|
+
const arr = cmd.options;
|
|
1854
|
+
for (const o of arr) {
|
|
1855
|
+
const dyn = DYN_DESC.get(o);
|
|
1856
|
+
if (typeof dyn === 'function') {
|
|
1857
|
+
try {
|
|
1858
|
+
const txt = dyn(resolved);
|
|
1859
|
+
// Commander uses Option.description during help rendering.
|
|
1860
|
+
o.description = txt;
|
|
1861
|
+
}
|
|
1862
|
+
catch {
|
|
1863
|
+
/* best-effort; leave as-is */
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
for (const c of cmd.commands)
|
|
1868
|
+
visit(c);
|
|
1525
1869
|
};
|
|
1870
|
+
visit(root);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function initializeInstance(cli, headerGetter) {
|
|
1874
|
+
// Configure grouped help: show only base options in default "Options";
|
|
1875
|
+
// subcommands show all of their own options.
|
|
1876
|
+
cli.configureHelp({
|
|
1877
|
+
visibleOptions: (cmd) => {
|
|
1878
|
+
const all = cmd.options;
|
|
1879
|
+
const isRoot = cmd.parent === null;
|
|
1880
|
+
const list = isRoot
|
|
1881
|
+
? all.filter((opt) => {
|
|
1882
|
+
const group = GROUP_TAG.get(opt);
|
|
1883
|
+
return group === 'base';
|
|
1884
|
+
})
|
|
1885
|
+
: all.slice();
|
|
1886
|
+
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1887
|
+
const hasShort = (opt) => {
|
|
1888
|
+
const flags = opt.flags;
|
|
1889
|
+
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1890
|
+
};
|
|
1891
|
+
const byFlags = (opt) => opt.flags;
|
|
1892
|
+
list.sort((a, b) => {
|
|
1893
|
+
const aS = hasShort(a) ? 1 : 0;
|
|
1894
|
+
const bS = hasShort(b) ? 1 : 0;
|
|
1895
|
+
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1896
|
+
});
|
|
1897
|
+
return list;
|
|
1898
|
+
},
|
|
1899
|
+
});
|
|
1900
|
+
// Optional branded header before help text (kept minimal and deterministic).
|
|
1901
|
+
cli.addHelpText('beforeAll', () => {
|
|
1902
|
+
const header = headerGetter();
|
|
1903
|
+
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1904
|
+
});
|
|
1905
|
+
// Tests-only: suppress process.exit during help/version flows under Vitest.
|
|
1906
|
+
// Unit tests often construct GetDotenvCli directly (bypassing createCli),
|
|
1907
|
+
// so install a local exitOverride when a test environment is detected.
|
|
1908
|
+
const underTests = process.env.GETDOTENV_TEST === '1' ||
|
|
1909
|
+
typeof process.env.VITEST_WORKER_ID === 'string';
|
|
1910
|
+
if (underTests) {
|
|
1911
|
+
cli.exitOverride((err) => {
|
|
1912
|
+
const code = err?.code;
|
|
1913
|
+
if (code === 'commander.helpDisplayed' ||
|
|
1914
|
+
code === 'commander.version' ||
|
|
1915
|
+
code === 'commander.help')
|
|
1916
|
+
return;
|
|
1917
|
+
throw err;
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
// Ensure the root has a no-op action so preAction hooks installed by
|
|
1921
|
+
// passOptions() fire for root-only invocations (no subcommand).
|
|
1922
|
+
// Subcommands still take precedence and will not hit this action.
|
|
1923
|
+
// This keeps root-side effects (e.g., --log) working in direct hosts/tests.
|
|
1924
|
+
cli.action(() => {
|
|
1925
|
+
/* no-op */
|
|
1926
|
+
});
|
|
1927
|
+
// PreSubcommand hook: compute a context if absent, without mutating process.env.
|
|
1928
|
+
// The passOptions() helper, when installed, resolves the final context.
|
|
1929
|
+
cli.hook('preSubcommand', async () => {
|
|
1930
|
+
if (cli.hasCtx())
|
|
1931
|
+
return;
|
|
1932
|
+
await cli.resolveAndLoad({ loadProcess: false });
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* Determine the effective namespace for a child plugin (override \> default).
|
|
1938
|
+
*/
|
|
1939
|
+
const effectiveNs = (child) => {
|
|
1940
|
+
const o = child.override;
|
|
1941
|
+
return (o && typeof o.ns === 'string' && o.ns.length > 0 ? o.ns : child.plugin.ns).trim();
|
|
1526
1942
|
};
|
|
1943
|
+
const isPromise = (v) => !!v && typeof v.then === 'function';
|
|
1944
|
+
function runInstall(parentCli, plugin) {
|
|
1945
|
+
// Create mount and run setup
|
|
1946
|
+
const mount = parentCli.ns(plugin.ns);
|
|
1947
|
+
const setupRet = plugin.setup(mount);
|
|
1948
|
+
const pending = [];
|
|
1949
|
+
if (isPromise(setupRet))
|
|
1950
|
+
pending.push(setupRet.then(() => undefined));
|
|
1951
|
+
// Enforce sibling uniqueness before creating children
|
|
1952
|
+
const names = new Set();
|
|
1953
|
+
for (const entry of plugin.children) {
|
|
1954
|
+
const ns = effectiveNs(entry);
|
|
1955
|
+
if (names.has(ns)) {
|
|
1956
|
+
const under = mount.name();
|
|
1957
|
+
throw new Error(`Duplicate namespace '${ns}' under '${under || 'root'}'. Override via .use(plugin, { ns: '...' }).`);
|
|
1958
|
+
}
|
|
1959
|
+
names.add(ns);
|
|
1960
|
+
}
|
|
1961
|
+
// Install children (pre-order), synchronously when possible
|
|
1962
|
+
for (const entry of plugin.children) {
|
|
1963
|
+
const childRet = runInstall(mount, entry.plugin);
|
|
1964
|
+
if (isPromise(childRet))
|
|
1965
|
+
pending.push(childRet);
|
|
1966
|
+
}
|
|
1967
|
+
if (pending.length > 0)
|
|
1968
|
+
return Promise.all(pending).then(() => undefined);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Install a plugin and its children (pre-order setup phase).
|
|
1973
|
+
* Enforces sibling namespace uniqueness.
|
|
1974
|
+
*/
|
|
1975
|
+
function setupPluginTree(cli, plugin) {
|
|
1976
|
+
const ret = runInstall(cli, plugin);
|
|
1977
|
+
return isPromise(ret) ? ret : Promise.resolve();
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
/**
|
|
1981
|
+
* Resolve options strictly and compute the dotenv context via the loader/overlay path.
|
|
1982
|
+
*
|
|
1983
|
+
* @param customOptions - Partial options overlay.
|
|
1984
|
+
* @param plugins - Plugins list for config validation.
|
|
1985
|
+
* @param hostMetaUrl - Import URL for resolving the packaged root.
|
|
1986
|
+
*/
|
|
1987
|
+
async function resolveAndComputeContext(customOptions, plugins, hostMetaUrl) {
|
|
1988
|
+
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1989
|
+
// Strict schema validation
|
|
1990
|
+
getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
1991
|
+
const ctx = await computeContext(optionsResolved, plugins, hostMetaUrl);
|
|
1992
|
+
return ctx;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Run afterResolve hooks for a plugin tree (parent → children).
|
|
1997
|
+
*/
|
|
1998
|
+
async function runAfterResolveTree(cli, plugins, ctx) {
|
|
1999
|
+
const run = async (p) => {
|
|
2000
|
+
if (p.afterResolve)
|
|
2001
|
+
await p.afterResolve(cli, ctx);
|
|
2002
|
+
for (const child of p.children)
|
|
2003
|
+
await run(child.plugin);
|
|
2004
|
+
};
|
|
2005
|
+
for (const p of plugins)
|
|
2006
|
+
await run(p);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* Temporarily tag options added during a callback as 'app' for grouped help.
|
|
2011
|
+
* Wraps `addOption` on the command instance.
|
|
2012
|
+
*/
|
|
2013
|
+
function tagAppOptionsAround(root, setOptionGroup, fn) {
|
|
2014
|
+
const originalAddOption = root.addOption.bind(root);
|
|
2015
|
+
root.addOption = ((opt) => {
|
|
2016
|
+
setOptionGroup(opt, 'app');
|
|
2017
|
+
return originalAddOption(opt);
|
|
2018
|
+
});
|
|
2019
|
+
try {
|
|
2020
|
+
return fn(root);
|
|
2021
|
+
}
|
|
2022
|
+
finally {
|
|
2023
|
+
root.addOption = originalAddOption;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* Read the version from the nearest `package.json` relative to the provided import URL.
|
|
2029
|
+
*
|
|
2030
|
+
* @param importMetaUrl - The `import.meta.url` of the calling module.
|
|
2031
|
+
* @returns The version string or undefined if not found.
|
|
2032
|
+
*/
|
|
2033
|
+
async function readPkgVersion(importMetaUrl) {
|
|
2034
|
+
if (!importMetaUrl)
|
|
2035
|
+
return undefined;
|
|
2036
|
+
try {
|
|
2037
|
+
const fromUrl = fileURLToPath(importMetaUrl);
|
|
2038
|
+
const pkgDir = await packageDirectory({ cwd: fromUrl });
|
|
2039
|
+
if (!pkgDir)
|
|
2040
|
+
return undefined;
|
|
2041
|
+
const txt = await fs.readFile(`${pkgDir}/package.json`, 'utf-8');
|
|
2042
|
+
const pkg = JSON.parse(txt);
|
|
2043
|
+
return pkg.version ?? undefined;
|
|
2044
|
+
}
|
|
2045
|
+
catch {
|
|
2046
|
+
// best-effort only
|
|
2047
|
+
return undefined;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1527
2050
|
|
|
1528
|
-
|
|
1529
|
-
|
|
2051
|
+
/** src/cliHost/GetDotenvCli.ts
|
|
2052
|
+
* Plugin-first CLI host for get-dotenv with Commander generics preserved.
|
|
2053
|
+
* Public surface implements GetDotenvCliPublic and provides:
|
|
2054
|
+
* - attachRootOptions (builder-only; no public override wiring)
|
|
2055
|
+
* - resolveAndLoad (strict resolve + context compute)
|
|
2056
|
+
* - getCtx/hasCtx accessors
|
|
2057
|
+
* - ns() for typed subcommand creation with duplicate-name guard
|
|
2058
|
+
* - grouped help rendering with dynamic option descriptions
|
|
2059
|
+
*/
|
|
1530
2060
|
const HOST_META_URL = import.meta.url;
|
|
1531
2061
|
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
1532
2062
|
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
@@ -1539,70 +2069,41 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
|
1539
2069
|
* - Expose a stable accessor for the current context (getCtx).
|
|
1540
2070
|
* - Provide a namespacing helper (ns).
|
|
1541
2071
|
* - Support composable plugins with parent → children install and afterResolve.
|
|
1542
|
-
*
|
|
1543
|
-
* NOTE: This host is additive and does not alter the legacy CLI.
|
|
1544
2072
|
*/
|
|
1545
|
-
|
|
2073
|
+
class GetDotenvCli extends Command {
|
|
1546
2074
|
/** Registered top-level plugins (composition happens via .use()) */
|
|
1547
2075
|
_plugins = [];
|
|
1548
2076
|
/** One-time installation guard */
|
|
1549
2077
|
_installed = false;
|
|
2078
|
+
/** In-flight installation promise to guard against concurrent installs */
|
|
2079
|
+
_installing;
|
|
1550
2080
|
/** Optional header line to prepend in help output */
|
|
1551
2081
|
[HELP_HEADER_SYMBOL];
|
|
2082
|
+
/** Context/options stored under symbols (typed) */
|
|
2083
|
+
[CTX_SYMBOL];
|
|
2084
|
+
[OPTS_SYMBOL];
|
|
1552
2085
|
/**
|
|
1553
2086
|
* Create a subcommand using the same subclass, preserving helpers like
|
|
1554
2087
|
* dynamicOption on children.
|
|
1555
2088
|
*/
|
|
1556
2089
|
createCommand(name) {
|
|
1557
|
-
|
|
2090
|
+
// Explicitly construct a GetDotenvCli for children to preserve helpers.
|
|
2091
|
+
return new GetDotenvCli(name);
|
|
1558
2092
|
}
|
|
1559
2093
|
constructor(alias = 'getdotenv') {
|
|
1560
2094
|
super(alias);
|
|
1561
|
-
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
1562
|
-
// Commander requires parent commands to enable positional options when a
|
|
1563
|
-
// child uses passThroughOptions.
|
|
1564
2095
|
this.enablePositionalOptions();
|
|
1565
|
-
//
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
})
|
|
1577
|
-
: all.slice(); // subcommands: show all options (their own "Options:" block)
|
|
1578
|
-
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1579
|
-
const hasShort = (opt) => {
|
|
1580
|
-
const flags = opt.flags ?? '';
|
|
1581
|
-
// Matches "-x," or starting "-x " before any long
|
|
1582
|
-
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1583
|
-
};
|
|
1584
|
-
const byFlags = (opt) => opt.flags ?? '';
|
|
1585
|
-
list.sort((a, b) => {
|
|
1586
|
-
const aS = hasShort(a) ? 1 : 0;
|
|
1587
|
-
const bS = hasShort(b) ? 1 : 0;
|
|
1588
|
-
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1589
|
-
});
|
|
1590
|
-
return list;
|
|
1591
|
-
},
|
|
1592
|
-
});
|
|
1593
|
-
this.addHelpText('beforeAll', () => {
|
|
1594
|
-
const header = this[HELP_HEADER_SYMBOL];
|
|
1595
|
-
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1596
|
-
});
|
|
1597
|
-
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
1598
|
-
// mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
|
|
1599
|
-
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
1600
|
-
// process env before subcommands execute.
|
|
1601
|
-
this.hook('preSubcommand', async () => {
|
|
1602
|
-
if (this.getCtx())
|
|
1603
|
-
return;
|
|
1604
|
-
await this.resolveAndLoad({ loadProcess: false });
|
|
1605
|
-
});
|
|
2096
|
+
// Delegate the heavy setup to a helper to keep the constructor lean.
|
|
2097
|
+
initializeInstance(this, () => this[HELP_HEADER_SYMBOL]);
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Attach legacy/base root flags to this CLI instance.
|
|
2101
|
+
* Delegates to the pure builder in attachRootOptions.ts.
|
|
2102
|
+
*/
|
|
2103
|
+
attachRootOptions(defaults) {
|
|
2104
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
2105
|
+
attachRootOptions(this, d);
|
|
2106
|
+
return this;
|
|
1606
2107
|
}
|
|
1607
2108
|
/**
|
|
1608
2109
|
* Resolve options (strict) and compute dotenv context.
|
|
@@ -1614,14 +2115,11 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1614
2115
|
* long-running side-effects while still evaluating dynamic help text.
|
|
1615
2116
|
*/
|
|
1616
2117
|
async resolveAndLoad(customOptions = {}, opts) {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
// Delegate the heavy lifting to the shared helper (guarded path supported).
|
|
1621
|
-
const ctx = await computeContext(optionsResolved, this._plugins, HOST_META_URL);
|
|
2118
|
+
const ctx = await resolveAndComputeContext(customOptions,
|
|
2119
|
+
// Pass only plugin instances to the resolver (not entries with overrides)
|
|
2120
|
+
this._plugins.map((e) => e.plugin), HOST_META_URL);
|
|
1622
2121
|
// Persist context on the instance for later access.
|
|
1623
|
-
this[CTX_SYMBOL] =
|
|
1624
|
-
ctx;
|
|
2122
|
+
this[CTX_SYMBOL] = ctx;
|
|
1625
2123
|
// Ensure plugins are installed exactly once, then run afterResolve.
|
|
1626
2124
|
await this.install();
|
|
1627
2125
|
if (opts?.runAfterResolve ?? true) {
|
|
@@ -1629,31 +2127,9 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1629
2127
|
}
|
|
1630
2128
|
return ctx;
|
|
1631
2129
|
}
|
|
1632
|
-
|
|
1633
|
-
* Create a Commander Option that computes its description at help time.
|
|
1634
|
-
* The returned Option may be configured (conflicts, default, parser) and
|
|
1635
|
-
* added via addOption().
|
|
1636
|
-
*/
|
|
1637
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
2130
|
+
// Implementation
|
|
1638
2131
|
createDynamicOption(flags, desc, parser, defaultValue) {
|
|
1639
|
-
|
|
1640
|
-
// Keep the function on a private symbol so it survives through Commander.
|
|
1641
|
-
opt[DYN_DESC_SYM] = desc;
|
|
1642
|
-
if (parser)
|
|
1643
|
-
opt.argParser(parser);
|
|
1644
|
-
if (defaultValue !== undefined)
|
|
1645
|
-
opt.default(defaultValue);
|
|
1646
|
-
return opt;
|
|
1647
|
-
}
|
|
1648
|
-
/**
|
|
1649
|
-
* Chainable helper mirroring .option(), but with a dynamic description.
|
|
1650
|
-
* Equivalent to addOption(createDynamicOption(...)).
|
|
1651
|
-
*/
|
|
1652
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
1653
|
-
dynamicOption(flags, desc, parser, defaultValue) {
|
|
1654
|
-
const opt = this.createDynamicOption(flags, desc, parser, defaultValue);
|
|
1655
|
-
this.addOption(opt);
|
|
1656
|
-
return this;
|
|
2132
|
+
return makeDynamicOption(flags, (c) => desc(c), parser, defaultValue);
|
|
1657
2133
|
}
|
|
1658
2134
|
/**
|
|
1659
2135
|
* Evaluate dynamic descriptions for this command and all descendants using
|
|
@@ -1661,47 +2137,66 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1661
2137
|
* place so Commander help renders updated text.
|
|
1662
2138
|
*/
|
|
1663
2139
|
evaluateDynamicOptions(resolved) {
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
}
|
|
1674
|
-
catch {
|
|
1675
|
-
// Best-effort: leave description as-is on evaluation failure.
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
const children = cmd.commands ?? [];
|
|
1680
|
-
for (const c of children)
|
|
1681
|
-
visit(c);
|
|
1682
|
-
};
|
|
1683
|
-
visit(this);
|
|
2140
|
+
evaluateDynamicOptions(this, resolved);
|
|
2141
|
+
}
|
|
2142
|
+
/** Internal: climb to the true root (host) command. */
|
|
2143
|
+
_root() {
|
|
2144
|
+
let node = this;
|
|
2145
|
+
while (node.parent) {
|
|
2146
|
+
node = node.parent;
|
|
2147
|
+
}
|
|
2148
|
+
return node;
|
|
1684
2149
|
}
|
|
1685
2150
|
/**
|
|
1686
2151
|
* Retrieve the current invocation context (if any).
|
|
1687
2152
|
*/
|
|
1688
2153
|
getCtx() {
|
|
1689
|
-
|
|
2154
|
+
let ctx = this[CTX_SYMBOL];
|
|
2155
|
+
if (!ctx) {
|
|
2156
|
+
const root = this._root();
|
|
2157
|
+
ctx = root[CTX_SYMBOL];
|
|
2158
|
+
}
|
|
2159
|
+
if (!ctx) {
|
|
2160
|
+
throw new Error('Dotenv context unavailable. Ensure resolveAndLoad() has been called or the host is wired with passOptions() before invoking commands.');
|
|
2161
|
+
}
|
|
2162
|
+
return ctx;
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Check whether a context has been resolved (non-throwing guard).
|
|
2166
|
+
*/
|
|
2167
|
+
hasCtx() {
|
|
2168
|
+
if (this[CTX_SYMBOL] !== undefined)
|
|
2169
|
+
return true;
|
|
2170
|
+
const root = this._root();
|
|
2171
|
+
return root[CTX_SYMBOL] !== undefined;
|
|
1690
2172
|
}
|
|
1691
2173
|
/**
|
|
1692
2174
|
* Retrieve the merged root CLI options bag (if set by passOptions()).
|
|
1693
2175
|
* Downstream-safe: no generics required.
|
|
1694
2176
|
*/
|
|
1695
2177
|
getOptions() {
|
|
1696
|
-
|
|
2178
|
+
if (this[OPTS_SYMBOL])
|
|
2179
|
+
return this[OPTS_SYMBOL];
|
|
2180
|
+
const root = this._root();
|
|
2181
|
+
const bag = root[OPTS_SYMBOL];
|
|
2182
|
+
if (bag)
|
|
2183
|
+
return bag;
|
|
2184
|
+
return undefined;
|
|
1697
2185
|
}
|
|
1698
2186
|
/** Internal: set the merged root options bag for this run. */
|
|
1699
2187
|
_setOptionsBag(bag) {
|
|
1700
2188
|
this[OPTS_SYMBOL] = bag;
|
|
1701
2189
|
}
|
|
1702
|
-
/**
|
|
2190
|
+
/**
|
|
2191
|
+
* Convenience helper to create a namespaced subcommand with argument inference.
|
|
2192
|
+
* This mirrors Commander generics so downstream chaining stays fully typed.
|
|
1703
2193
|
*/
|
|
1704
2194
|
ns(name) {
|
|
2195
|
+
// Guard against same-level duplicate command names for clearer diagnostics.
|
|
2196
|
+
const exists = this.commands.some((c) => c.name() === name);
|
|
2197
|
+
if (exists) {
|
|
2198
|
+
throw new Error(`Duplicate command name: ${name}`);
|
|
2199
|
+
}
|
|
1705
2200
|
return this.command(name);
|
|
1706
2201
|
}
|
|
1707
2202
|
/**
|
|
@@ -1709,35 +2204,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1709
2204
|
* Allows downstream apps to demarcate their root-level options.
|
|
1710
2205
|
*/
|
|
1711
2206
|
tagAppOptions(fn) {
|
|
1712
|
-
|
|
1713
|
-
const originalAddOption = root.addOption.bind(root);
|
|
1714
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1715
|
-
const originalOption = root.option.bind(root);
|
|
1716
|
-
const tagLatest = (cmd, group) => {
|
|
1717
|
-
const optsArr = cmd.options;
|
|
1718
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
1719
|
-
const last = optsArr[optsArr.length - 1];
|
|
1720
|
-
last.__group = group;
|
|
1721
|
-
}
|
|
1722
|
-
};
|
|
1723
|
-
root.addOption = function patchedAdd(opt) {
|
|
1724
|
-
opt.__group = 'app';
|
|
1725
|
-
return originalAddOption(opt);
|
|
1726
|
-
};
|
|
1727
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1728
|
-
root.option = function patchedOption(...args) {
|
|
1729
|
-
const ret = originalOption(...args);
|
|
1730
|
-
tagLatest(this, 'app');
|
|
1731
|
-
return ret;
|
|
1732
|
-
};
|
|
1733
|
-
try {
|
|
1734
|
-
return fn(root);
|
|
1735
|
-
}
|
|
1736
|
-
finally {
|
|
1737
|
-
root.addOption = originalAddOption;
|
|
1738
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1739
|
-
root.option = originalOption;
|
|
1740
|
-
}
|
|
2207
|
+
return tagAppOptionsAround(this, this.setOptionGroup.bind(this), fn);
|
|
1741
2208
|
}
|
|
1742
2209
|
/**
|
|
1743
2210
|
* Branding helper: set CLI name/description/version and optional help header.
|
|
@@ -1750,22 +2217,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1750
2217
|
this.name(name);
|
|
1751
2218
|
if (typeof description === 'string')
|
|
1752
2219
|
this.description(description);
|
|
1753
|
-
|
|
1754
|
-
if (!v && importMetaUrl) {
|
|
1755
|
-
try {
|
|
1756
|
-
const fromUrl = fileURLToPath(importMetaUrl);
|
|
1757
|
-
const pkgDir = await packageDirectory({ cwd: fromUrl });
|
|
1758
|
-
if (pkgDir) {
|
|
1759
|
-
const txt = await fs.readFile(`${pkgDir}/package.json`, 'utf-8');
|
|
1760
|
-
const pkg = JSON.parse(txt);
|
|
1761
|
-
if (pkg.version)
|
|
1762
|
-
v = pkg.version;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
catch {
|
|
1766
|
-
// best-effort only
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
2220
|
+
const v = version ?? (await readPkgVersion(importMetaUrl));
|
|
1769
2221
|
if (v)
|
|
1770
2222
|
this.version(v);
|
|
1771
2223
|
// Help header:
|
|
@@ -1775,7 +2227,6 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1775
2227
|
this[HELP_HEADER_SYMBOL] = helpHeader;
|
|
1776
2228
|
}
|
|
1777
2229
|
else if (v) {
|
|
1778
|
-
// Use the current command name (possibly overridden by 'name' above).
|
|
1779
2230
|
const header = `${this.name()} v${v}`;
|
|
1780
2231
|
this[HELP_HEADER_SYMBOL] = header;
|
|
1781
2232
|
}
|
|
@@ -1786,48 +2237,20 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1786
2237
|
* hybrid ordering. Applies to root and any parent command.
|
|
1787
2238
|
*/
|
|
1788
2239
|
helpInformation() {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if (!out.endsWith('\n\n'))
|
|
1797
|
-
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1798
|
-
return out;
|
|
1799
|
-
}
|
|
1800
|
-
// Insert just before "Commands:" when present.
|
|
1801
|
-
const marker = '\nCommands:';
|
|
1802
|
-
const idx = base.indexOf(marker);
|
|
1803
|
-
if (idx >= 0) {
|
|
1804
|
-
const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
|
|
1805
|
-
out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
|
|
1806
|
-
}
|
|
1807
|
-
else {
|
|
1808
|
-
// Otherwise append.
|
|
1809
|
-
const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
|
|
1810
|
-
out = `${base}${sep}${groups}`;
|
|
1811
|
-
}
|
|
1812
|
-
// Ensure a trailing blank line for prompt separation.
|
|
1813
|
-
if (!out.endsWith('\n\n')) {
|
|
1814
|
-
out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
|
|
1815
|
-
}
|
|
1816
|
-
return out;
|
|
2240
|
+
return buildHelpInformation(super.helpInformation(), this);
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Public: tag an Option with a display group for help (root/app/plugin:<id>).
|
|
2244
|
+
*/
|
|
2245
|
+
setOptionGroup(opt, group) {
|
|
2246
|
+
GROUP_TAG.set(opt, group);
|
|
1817
2247
|
}
|
|
1818
2248
|
/**
|
|
1819
2249
|
* Register a plugin for installation (parent level).
|
|
1820
2250
|
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
1821
2251
|
*/
|
|
1822
|
-
use(plugin) {
|
|
1823
|
-
this._plugins.push(plugin);
|
|
1824
|
-
// Immediately run setup so subcommands exist before parsing.
|
|
1825
|
-
const setupOne = (p) => {
|
|
1826
|
-
p.setup(this);
|
|
1827
|
-
for (const child of p.children)
|
|
1828
|
-
setupOne(child);
|
|
1829
|
-
};
|
|
1830
|
-
setupOne(plugin);
|
|
2252
|
+
use(plugin, override) {
|
|
2253
|
+
this._plugins.push({ plugin, override });
|
|
1831
2254
|
return this;
|
|
1832
2255
|
}
|
|
1833
2256
|
/**
|
|
@@ -1835,239 +2258,348 @@ let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
|
1835
2258
|
* Runs only once per CLI instance.
|
|
1836
2259
|
*/
|
|
1837
2260
|
async install() {
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2261
|
+
if (this._installed)
|
|
2262
|
+
return;
|
|
2263
|
+
if (this._installing) {
|
|
2264
|
+
await this._installing;
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
this._installing = (async () => {
|
|
2268
|
+
// Install parent → children with host-created mounts (async-aware).
|
|
2269
|
+
for (const entry of this._plugins) {
|
|
2270
|
+
const p = entry.plugin;
|
|
2271
|
+
await setupPluginTree(this, p);
|
|
2272
|
+
}
|
|
2273
|
+
this._installed = true;
|
|
2274
|
+
})();
|
|
2275
|
+
try {
|
|
2276
|
+
await this._installing;
|
|
2277
|
+
}
|
|
2278
|
+
finally {
|
|
2279
|
+
// leave _installing as resolved; subsequent calls return early via _installed
|
|
2280
|
+
}
|
|
1842
2281
|
}
|
|
1843
2282
|
/**
|
|
1844
2283
|
* Run afterResolve hooks for all plugins (parent → children).
|
|
1845
2284
|
*/
|
|
1846
2285
|
async _runAfterResolve(ctx) {
|
|
1847
|
-
|
|
1848
|
-
if (p.afterResolve)
|
|
1849
|
-
await p.afterResolve(this, ctx);
|
|
1850
|
-
for (const child of p.children)
|
|
1851
|
-
await run(child);
|
|
1852
|
-
};
|
|
1853
|
-
for (const p of this._plugins)
|
|
1854
|
-
await run(p);
|
|
1855
|
-
}
|
|
1856
|
-
// Render App/Plugin grouped options (used by helpInformation override).
|
|
1857
|
-
#renderOptionGroups(cmd) {
|
|
1858
|
-
const all = cmd.options ?? [];
|
|
1859
|
-
const byGroup = new Map();
|
|
1860
|
-
for (const o of all) {
|
|
1861
|
-
const opt = o;
|
|
1862
|
-
const g = opt.__group;
|
|
1863
|
-
if (!g || g === 'base')
|
|
1864
|
-
continue; // base handled by default help
|
|
1865
|
-
const rows = byGroup.get(g) ?? [];
|
|
1866
|
-
rows.push({
|
|
1867
|
-
flags: opt.flags ?? '',
|
|
1868
|
-
description: opt.description ?? '',
|
|
1869
|
-
});
|
|
1870
|
-
byGroup.set(g, rows);
|
|
1871
|
-
}
|
|
1872
|
-
if (byGroup.size === 0)
|
|
1873
|
-
return '';
|
|
1874
|
-
const renderRows = (title, rows) => {
|
|
1875
|
-
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
1876
|
-
// Sort within group: short-aliased flags first
|
|
1877
|
-
rows.sort((a, b) => {
|
|
1878
|
-
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
1879
|
-
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
1880
|
-
return bS - aS || a.flags.localeCompare(b.flags);
|
|
1881
|
-
});
|
|
1882
|
-
const lines = rows
|
|
1883
|
-
.map((r) => {
|
|
1884
|
-
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
1885
|
-
return ` ${r.flags}${pad}${r.description}`.trimEnd();
|
|
1886
|
-
})
|
|
1887
|
-
.join('\n');
|
|
1888
|
-
return `\n${title}:\n${lines}\n`;
|
|
1889
|
-
};
|
|
1890
|
-
let out = '';
|
|
1891
|
-
// App options (if any)
|
|
1892
|
-
const app = byGroup.get('app');
|
|
1893
|
-
if (app && app.length > 0) {
|
|
1894
|
-
out += renderRows('App options', app);
|
|
1895
|
-
}
|
|
1896
|
-
// Plugin groups sorted by id
|
|
1897
|
-
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1898
|
-
const currentName = cmd.name?.() ?? '';
|
|
1899
|
-
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1900
|
-
for (const k of pluginKeys) {
|
|
1901
|
-
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1902
|
-
const rows = byGroup.get(k) ?? [];
|
|
1903
|
-
// Do not show a "Plugin options — <self>" section on the command that owns those options.
|
|
1904
|
-
// Only child-injected plugin groups should render at this level.
|
|
1905
|
-
if (rows.length > 0 && id !== currentName) {
|
|
1906
|
-
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
return out;
|
|
2286
|
+
await runAfterResolveTree(this, this._plugins.map((e) => e.plugin), ctx);
|
|
1910
2287
|
}
|
|
1911
|
-
}
|
|
2288
|
+
}
|
|
1912
2289
|
|
|
1913
|
-
/**
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
* This module exposes a structural public interface for the host that plugins
|
|
1917
|
-
* should use (GetDotenvCliPublic). Using a structural type at the seam avoids
|
|
1918
|
-
* nominal class identity issues (private fields) in downstream consumers.
|
|
2290
|
+
/**
|
|
2291
|
+
* Base CLI options derived from the shared root option defaults.
|
|
2292
|
+
* Used for type-safe initialization of CLI options bags.
|
|
1919
2293
|
*/
|
|
2294
|
+
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
2295
|
+
|
|
1920
2296
|
/**
|
|
1921
|
-
*
|
|
2297
|
+
* Return the top-level root command for a given mount or action's thisCommand.
|
|
1922
2298
|
*
|
|
1923
|
-
* @
|
|
1924
|
-
*
|
|
1925
|
-
* .use(childA)
|
|
1926
|
-
* .use(childB);
|
|
2299
|
+
* @param cmd - any command (mount or thisCommand inside an action)
|
|
2300
|
+
* @returns the root command instance
|
|
1927
2301
|
*/
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2302
|
+
const getRootCommand = (cmd) => {
|
|
2303
|
+
let node = cmd;
|
|
2304
|
+
while (node.parent)
|
|
2305
|
+
node = node.parent;
|
|
2306
|
+
return node;
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
/**
|
|
2310
|
+
* Build a help-time configuration bag for dynamic option descriptions.
|
|
2311
|
+
* Centralizes construction and reduces inline casts at call sites.
|
|
2312
|
+
*/
|
|
2313
|
+
const toHelpConfig = (merged, plugins) => {
|
|
2314
|
+
return {
|
|
2315
|
+
...merged,
|
|
2316
|
+
plugins: plugins ?? {},
|
|
1937
2317
|
};
|
|
1938
|
-
return plugin;
|
|
1939
2318
|
};
|
|
1940
2319
|
|
|
1941
2320
|
/**
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1944
|
-
* - passOptions: merges flags (parent \< current), computes dotenv context once,
|
|
1945
|
-
* runs validation, and persists merged options for nested flows.
|
|
2321
|
+
* Compose a child-process env overlay from dotenv and the merged CLI options bag.
|
|
2322
|
+
* Returns a shallow object including getDotenvCliOptions when serializable.
|
|
1946
2323
|
*/
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
attachRootOptions(defaults, opts) {
|
|
1953
|
-
const d = (defaults ?? baseRootOptionDefaults);
|
|
1954
|
-
attachRootOptions(this, d, opts);
|
|
1955
|
-
return this;
|
|
2324
|
+
function composeNestedEnv(merged, dotenv) {
|
|
2325
|
+
const out = {};
|
|
2326
|
+
for (const [k, v] of Object.entries(dotenv)) {
|
|
2327
|
+
if (typeof v === 'string')
|
|
2328
|
+
out[k] = v;
|
|
1956
2329
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
const logger = (merged
|
|
1995
|
-
.logger ?? console);
|
|
1996
|
-
const emit = logger.error ?? logger.log;
|
|
1997
|
-
issues.forEach((m) => {
|
|
1998
|
-
emit(m);
|
|
1999
|
-
});
|
|
2000
|
-
if (merged.strict) {
|
|
2001
|
-
process.exit(1);
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
catch {
|
|
2006
|
-
// Be tolerant: do not crash non-strict flows on unexpected validator failures.
|
|
2007
|
-
}
|
|
2008
|
-
});
|
|
2009
|
-
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
2010
|
-
// with the same merged options and context without duplicating logic.
|
|
2011
|
-
this.hook('preAction', async (thisCommand) => {
|
|
2012
|
-
const raw = thisCommand.opts();
|
|
2013
|
-
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
2014
|
-
thisCommand.getDotenvCliOptions =
|
|
2015
|
-
merged;
|
|
2016
|
-
this._setOptionsBag(merged);
|
|
2017
|
-
// Avoid duplicate heavy work if a context is already present.
|
|
2018
|
-
if (!this.getCtx()) {
|
|
2019
|
-
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
2020
|
-
await this.resolveAndLoad(serviceOptions);
|
|
2021
|
-
try {
|
|
2022
|
-
const ctx = this.getCtx();
|
|
2023
|
-
this.evaluateDynamicOptions({
|
|
2024
|
-
...ctx?.optionsResolved,
|
|
2025
|
-
plugins: ctx?.pluginConfigs ?? {},
|
|
2026
|
-
});
|
|
2027
|
-
}
|
|
2028
|
-
catch {
|
|
2029
|
-
/* tolerate */
|
|
2030
|
-
}
|
|
2031
|
-
try {
|
|
2032
|
-
const ctx = this.getCtx();
|
|
2033
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
2034
|
-
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
2035
|
-
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
2036
|
-
if (Array.isArray(issues) && issues.length > 0) {
|
|
2037
|
-
const logger = (merged
|
|
2038
|
-
.logger ?? console);
|
|
2039
|
-
const emit = logger.error ?? logger.log;
|
|
2040
|
-
issues.forEach((m) => {
|
|
2041
|
-
emit(m);
|
|
2042
|
-
});
|
|
2043
|
-
if (merged.strict) {
|
|
2044
|
-
process.exit(1);
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
catch {
|
|
2049
|
-
// Tolerate validation side-effects in non-strict mode.
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
});
|
|
2053
|
-
return this;
|
|
2330
|
+
try {
|
|
2331
|
+
const { logger: _omit, ...bag } = merged;
|
|
2332
|
+
const txt = JSON.stringify(bag);
|
|
2333
|
+
if (typeof txt === 'string')
|
|
2334
|
+
out.getDotenvCliOptions = txt;
|
|
2335
|
+
}
|
|
2336
|
+
catch {
|
|
2337
|
+
/* best-effort only */
|
|
2338
|
+
}
|
|
2339
|
+
return out;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Strip one layer of symmetric outer quotes (single or double) from a string.
|
|
2343
|
+
*
|
|
2344
|
+
* @param s - Input string.
|
|
2345
|
+
*/
|
|
2346
|
+
const stripOne = (s) => {
|
|
2347
|
+
if (s.length < 2)
|
|
2348
|
+
return s;
|
|
2349
|
+
const a = s.charAt(0);
|
|
2350
|
+
const b = s.charAt(s.length - 1);
|
|
2351
|
+
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
2352
|
+
return symmetric ? s.slice(1, -1) : s;
|
|
2353
|
+
};
|
|
2354
|
+
/**
|
|
2355
|
+
* Preserve argv array for Node -e/--eval payloads under shell-off and
|
|
2356
|
+
* peel one symmetric outer quote layer from the code argument.
|
|
2357
|
+
*/
|
|
2358
|
+
function maybePreserveNodeEvalArgv(args) {
|
|
2359
|
+
if (args.length >= 3) {
|
|
2360
|
+
const first = (args[0] ?? '').toLowerCase();
|
|
2361
|
+
const hasEval = args[1] === '-e' || args[1] === '--eval';
|
|
2362
|
+
if (first === 'node' && hasEval) {
|
|
2363
|
+
const copy = args.slice();
|
|
2364
|
+
copy[2] = stripOne(copy[2] ?? '');
|
|
2365
|
+
return copy;
|
|
2366
|
+
}
|
|
2054
2367
|
}
|
|
2368
|
+
return args;
|
|
2055
2369
|
}
|
|
2370
|
+
|
|
2056
2371
|
/**
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2372
|
+
* Retrieve the merged root options bag from the current command context.
|
|
2373
|
+
* Climbs to the root `GetDotenvCli` instance to access the persisted options.
|
|
2374
|
+
*
|
|
2375
|
+
* @param cmd - The current command instance (thisCommand).
|
|
2376
|
+
* @throws Error if the root is not a GetDotenvCli or options are missing.
|
|
2059
2377
|
*/
|
|
2060
2378
|
const readMergedOptions = (cmd) => {
|
|
2061
|
-
//
|
|
2379
|
+
// Climb to the true root
|
|
2062
2380
|
let root = cmd;
|
|
2063
|
-
while (root.parent)
|
|
2381
|
+
while (root.parent)
|
|
2064
2382
|
root = root.parent;
|
|
2383
|
+
// Assert we ended at our host
|
|
2384
|
+
if (!(root instanceof GetDotenvCli)) {
|
|
2385
|
+
throw new Error('readMergedOptions: root command is not a GetDotenvCli.' +
|
|
2386
|
+
'Ensure your CLI is constructed with GetDotenvCli.');
|
|
2387
|
+
}
|
|
2388
|
+
// Require passOptions() to have persisted the bag
|
|
2389
|
+
const bag = root.getOptions();
|
|
2390
|
+
if (!bag || typeof bag !== 'object') {
|
|
2391
|
+
throw new Error('readMergedOptions: merged options are unavailable. ' +
|
|
2392
|
+
'Call .passOptions() on the host before parsing.');
|
|
2393
|
+
}
|
|
2394
|
+
return bag;
|
|
2395
|
+
};
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* Batch services (neutral): resolve command and shell settings.
|
|
2399
|
+
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
2400
|
+
*/
|
|
2401
|
+
/**
|
|
2402
|
+
* Resolve a command string from the {@link ScriptsTable} table.
|
|
2403
|
+
* A script may be expressed as a string or an object with a `cmd` property.
|
|
2404
|
+
*
|
|
2405
|
+
* @param scripts - Optional scripts table.
|
|
2406
|
+
* @param command - User-provided command name or string.
|
|
2407
|
+
* @returns Resolved command string (falls back to the provided command).
|
|
2408
|
+
*/
|
|
2409
|
+
const resolveCommand = (scripts, command) => scripts && typeof scripts[command] === 'object'
|
|
2410
|
+
? scripts[command].cmd
|
|
2411
|
+
: (scripts?.[command] ?? command);
|
|
2412
|
+
/**
|
|
2413
|
+
* Resolve the shell setting for a given command:
|
|
2414
|
+
* - If the script entry is an object, prefer its `shell` override.
|
|
2415
|
+
* - Otherwise use the provided `shell` (string | boolean).
|
|
2416
|
+
*
|
|
2417
|
+
* @param scripts - Optional scripts table.
|
|
2418
|
+
* @param command - User-provided command name or string.
|
|
2419
|
+
* @param shell - Global shell preference (string | boolean).
|
|
2420
|
+
*/
|
|
2421
|
+
const resolveShell = (scripts, command, shell) => scripts && typeof scripts[command] === 'object'
|
|
2422
|
+
? (scripts[command].shell ?? false)
|
|
2423
|
+
: (shell ?? false);
|
|
2424
|
+
|
|
2425
|
+
/**
|
|
2426
|
+
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
2427
|
+
* - If the user explicitly enabled the flag, return true.
|
|
2428
|
+
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
2429
|
+
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
2430
|
+
*
|
|
2431
|
+
* @param exclude - The "on" flag value as parsed by Commander.
|
|
2432
|
+
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
2433
|
+
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
2434
|
+
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
2435
|
+
*
|
|
2436
|
+
* @example
|
|
2437
|
+
* ```ts
|
|
2438
|
+
* resolveExclusion(undefined, undefined, true); // => true
|
|
2439
|
+
* ```
|
|
2440
|
+
*/
|
|
2441
|
+
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
2442
|
+
/**
|
|
2443
|
+
* Resolve an optional flag with "--exclude-all" overrides.
|
|
2444
|
+
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
2445
|
+
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
2446
|
+
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
2447
|
+
*
|
|
2448
|
+
* @param exclude - Individual include/exclude flag.
|
|
2449
|
+
* @param excludeOff - Individual "...-off" flag.
|
|
2450
|
+
* @param defaultValue - Default for the individual flag.
|
|
2451
|
+
* @param excludeAll - Global "exclude-all" flag.
|
|
2452
|
+
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
2453
|
+
*
|
|
2454
|
+
* @example
|
|
2455
|
+
* resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
|
|
2456
|
+
*/
|
|
2457
|
+
const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
|
|
2458
|
+
// Order of precedence:
|
|
2459
|
+
// 1) Individual explicit "on" wins outright.
|
|
2460
|
+
// 2) Individual explicit "off" wins over any global.
|
|
2461
|
+
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
2462
|
+
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
2463
|
+
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
2464
|
+
(() => {
|
|
2465
|
+
// Individual "on"
|
|
2466
|
+
if (exclude === true)
|
|
2467
|
+
return true;
|
|
2468
|
+
// Individual "off"
|
|
2469
|
+
if (excludeOff === true)
|
|
2470
|
+
return undefined;
|
|
2471
|
+
// Global "exclude-all" ON (unless explicitly turned off)
|
|
2472
|
+
if (excludeAll === true)
|
|
2473
|
+
return true;
|
|
2474
|
+
// Global "exclude-all-off" (unless explicitly enabled)
|
|
2475
|
+
if (excludeAllOff === true)
|
|
2476
|
+
return undefined;
|
|
2477
|
+
// Default
|
|
2478
|
+
return defaultValue ? true : undefined;
|
|
2479
|
+
})();
|
|
2480
|
+
/**
|
|
2481
|
+
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
2482
|
+
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
2483
|
+
*
|
|
2484
|
+
* @typeParam T - Target object type.
|
|
2485
|
+
* @param obj - The object to write to.
|
|
2486
|
+
* @param key - The optional boolean property key of {@link T}.
|
|
2487
|
+
* @param value - The value to set or `undefined` to unset.
|
|
2488
|
+
*
|
|
2489
|
+
* @remarks
|
|
2490
|
+
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
2491
|
+
*/
|
|
2492
|
+
const setOptionalFlag = (obj, key, value) => {
|
|
2493
|
+
const target = obj;
|
|
2494
|
+
const k = key;
|
|
2495
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
2496
|
+
if (value === undefined)
|
|
2497
|
+
delete target[k];
|
|
2498
|
+
else
|
|
2499
|
+
target[k] = value;
|
|
2500
|
+
};
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* Merge and normalize raw Commander options (current + parent + defaults)
|
|
2504
|
+
* into a GetDotenvCliOptions-like object. Types are intentionally wide to
|
|
2505
|
+
* avoid cross-layer coupling; callers may cast as needed.
|
|
2506
|
+
*/
|
|
2507
|
+
const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
2508
|
+
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
2509
|
+
? JSON.parse(parentJson)
|
|
2510
|
+
: undefined;
|
|
2511
|
+
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
2512
|
+
const current = { ...rest };
|
|
2513
|
+
if (typeof scripts === 'string') {
|
|
2514
|
+
try {
|
|
2515
|
+
current.scripts = JSON.parse(scripts);
|
|
2516
|
+
}
|
|
2517
|
+
catch {
|
|
2518
|
+
// ignore parse errors; leave scripts undefined
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
2522
|
+
const d = defaults;
|
|
2523
|
+
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
2524
|
+
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
2525
|
+
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
2526
|
+
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
2527
|
+
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
2528
|
+
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
2529
|
+
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
2530
|
+
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
2531
|
+
// warnEntropy (tri-state)
|
|
2532
|
+
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
2533
|
+
// Normalize shell for predictability: explicit default shell per OS.
|
|
2534
|
+
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
2535
|
+
let resolvedShell = merged.shell;
|
|
2536
|
+
if (shellOff)
|
|
2537
|
+
resolvedShell = false;
|
|
2538
|
+
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
2539
|
+
resolvedShell = defaultShell;
|
|
2540
|
+
}
|
|
2541
|
+
else if (typeof resolvedShell !== 'string' &&
|
|
2542
|
+
typeof defaults.shell === 'string') {
|
|
2543
|
+
resolvedShell = defaults.shell;
|
|
2544
|
+
}
|
|
2545
|
+
merged.shell = resolvedShell;
|
|
2546
|
+
const cmd = typeof command === 'string' ? command : undefined;
|
|
2547
|
+
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
2548
|
+
};
|
|
2549
|
+
|
|
2550
|
+
const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
|
|
2551
|
+
/**
|
|
2552
|
+
* Build a sanitized environment object for spawning child processes.
|
|
2553
|
+
* Merges `base` and `overlay`, drops undefined values, and handles platform-specific
|
|
2554
|
+
* normalization (e.g. case-insensitivity on Windows).
|
|
2555
|
+
*
|
|
2556
|
+
* @param base - Base environment (usually `process.env`).
|
|
2557
|
+
* @param overlay - Environment variables to overlay.
|
|
2558
|
+
*/
|
|
2559
|
+
const buildSpawnEnv = (base, overlay) => {
|
|
2560
|
+
const raw = {
|
|
2561
|
+
...(base ?? {}),
|
|
2562
|
+
...(overlay ?? {}),
|
|
2563
|
+
};
|
|
2564
|
+
// Drop undefined first
|
|
2565
|
+
const entries = Object.entries(dropUndefined(raw));
|
|
2566
|
+
if (process.platform === 'win32') {
|
|
2567
|
+
// Windows: keys are case-insensitive; collapse duplicates
|
|
2568
|
+
const byLower = new Map();
|
|
2569
|
+
for (const [k, v] of entries) {
|
|
2570
|
+
byLower.set(k.toLowerCase(), [k, v]); // last wins; preserve latest casing
|
|
2571
|
+
}
|
|
2572
|
+
const out = {};
|
|
2573
|
+
for (const [, [k, v]] of byLower)
|
|
2574
|
+
out[k] = v;
|
|
2575
|
+
// HOME fallback from USERPROFILE (common expectation)
|
|
2576
|
+
if (!Object.prototype.hasOwnProperty.call(out, 'HOME')) {
|
|
2577
|
+
const up = out['USERPROFILE'];
|
|
2578
|
+
if (typeof up === 'string' && up.length > 0)
|
|
2579
|
+
out['HOME'] = up;
|
|
2580
|
+
}
|
|
2581
|
+
// Normalize TMP/TEMP coherence (pick any present; reflect to both)
|
|
2582
|
+
const tmp = out['TMP'] ?? out['TEMP'];
|
|
2583
|
+
if (typeof tmp === 'string' && tmp.length > 0) {
|
|
2584
|
+
out['TMP'] = tmp;
|
|
2585
|
+
out['TEMP'] = tmp;
|
|
2586
|
+
}
|
|
2587
|
+
return out;
|
|
2588
|
+
}
|
|
2589
|
+
// POSIX: keep keys as-is
|
|
2590
|
+
const out = Object.fromEntries(entries);
|
|
2591
|
+
// Ensure TMPDIR exists when any temp key is present (best-effort)
|
|
2592
|
+
const tmpdir = out['TMPDIR'] ?? out['TMP'] ?? out['TEMP'];
|
|
2593
|
+
if (typeof tmpdir === 'string' && tmpdir.length > 0) {
|
|
2594
|
+
out['TMPDIR'] = tmpdir;
|
|
2065
2595
|
}
|
|
2066
|
-
|
|
2067
|
-
return typeof hostAny.getOptions === 'function'
|
|
2068
|
-
? hostAny.getOptions()
|
|
2069
|
-
: root
|
|
2070
|
-
.getDotenvCliOptions;
|
|
2596
|
+
return out;
|
|
2071
2597
|
};
|
|
2072
2598
|
|
|
2073
|
-
|
|
2599
|
+
/**
|
|
2600
|
+
* Identity helper to define a scripts table while preserving a concrete TShell
|
|
2601
|
+
* type parameter in downstream inference.
|
|
2602
|
+
*/
|
|
2603
|
+
const defineScripts = () => (t) => t;
|
|
2604
|
+
|
|
2605
|
+
export { GetDotenvCli, baseGetDotenvCliOptions, buildSpawnEnv, composeNestedEnv, definePlugin, defineScripts, getRootCommand, maybePreserveNodeEvalArgv, readMergedOptions, resolveCliOptions, resolveCommand, resolveShell, runCommand, runCommandResult, stripOne, toHelpConfig };
|