@karmaniverous/get-dotenv 5.2.5 → 6.0.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -67
- package/dist/cliHost.cjs +765 -549
- package/dist/cliHost.d.cts +128 -84
- package/dist/cliHost.d.mts +128 -84
- package/dist/cliHost.d.ts +128 -84
- package/dist/cliHost.mjs +765 -549
- package/dist/getdotenv.cli.mjs +915 -685
- package/dist/index.cjs +959 -1006
- package/dist/index.d.cts +18 -178
- package/dist/index.d.mts +18 -178
- package/dist/index.d.ts +18 -178
- package/dist/index.mjs +960 -1006
- package/dist/plugins-aws.cjs +0 -1
- package/dist/plugins-aws.d.cts +8 -78
- package/dist/plugins-aws.d.mts +8 -78
- package/dist/plugins-aws.d.ts +8 -78
- package/dist/plugins-aws.mjs +0 -1
- package/dist/plugins-batch.cjs +53 -11
- package/dist/plugins-batch.d.cts +10 -79
- package/dist/plugins-batch.d.mts +10 -79
- package/dist/plugins-batch.d.ts +10 -79
- package/dist/plugins-batch.mjs +53 -11
- package/dist/plugins-cmd.cjs +162 -1555
- package/dist/plugins-cmd.d.cts +8 -78
- package/dist/plugins-cmd.d.mts +8 -78
- package/dist/plugins-cmd.d.ts +8 -78
- package/dist/plugins-cmd.mjs +162 -1554
- package/dist/plugins-demo.cjs +52 -7
- package/dist/plugins-demo.d.cts +8 -78
- package/dist/plugins-demo.d.mts +8 -78
- package/dist/plugins-demo.d.ts +8 -78
- package/dist/plugins-demo.mjs +52 -7
- package/dist/plugins-init.d.cts +8 -78
- package/dist/plugins-init.d.mts +8 -78
- package/dist/plugins-init.d.ts +8 -78
- package/dist/plugins.cjs +283 -1630
- package/dist/plugins.d.cts +10 -79
- package/dist/plugins.d.mts +10 -79
- package/dist/plugins.d.ts +10 -79
- package/dist/plugins.mjs +285 -1631
- package/package.json +4 -2
package/dist/cliHost.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Command, Option } from 'commander';
|
|
2
1
|
import fs from 'fs-extra';
|
|
3
2
|
import { packageDirectory } from 'package-directory';
|
|
4
|
-
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
5
3
|
import path, { join, extname } from 'path';
|
|
6
|
-
import {
|
|
4
|
+
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
7
5
|
import YAML from 'yaml';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { Option, Command } from 'commander';
|
|
8
8
|
import { nanoid } from 'nanoid';
|
|
9
9
|
import { parse } from 'dotenv';
|
|
10
10
|
import { createHash } from 'crypto';
|
|
@@ -36,8 +36,6 @@ const baseRootOptionDefaults = {
|
|
|
36
36
|
// (debug/log/exclude* resolved via flag utils)
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
40
|
-
|
|
41
39
|
/** @internal */
|
|
42
40
|
const isPlainObject$1 = (value) => value !== null &&
|
|
43
41
|
typeof value === 'object' &&
|
|
@@ -80,134 +78,130 @@ const defaultsDeep = (...layers) => {
|
|
|
80
78
|
return result;
|
|
81
79
|
};
|
|
82
80
|
|
|
83
|
-
// src/GetDotenvOptions.ts
|
|
84
|
-
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
85
81
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
82
|
+
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
83
|
+
* - If the user explicitly enabled the flag, return true.
|
|
84
|
+
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
85
|
+
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
88
86
|
*
|
|
89
|
-
* @
|
|
87
|
+
* @param exclude - The "on" flag value as parsed by Commander.
|
|
88
|
+
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
89
|
+
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
90
|
+
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* resolveExclusion(undefined, undefined, true); // => true
|
|
95
|
+
* ```
|
|
90
96
|
*/
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* The result preserves explicit empty values and drops only `undefined`.
|
|
151
|
-
*
|
|
152
|
-
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
153
|
-
*
|
|
154
|
-
* @example
|
|
155
|
-
* ```ts
|
|
156
|
-
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
157
|
-
* ```
|
|
158
|
-
*/
|
|
159
|
-
const localPkgDir = await packageDirectory();
|
|
160
|
-
const localOptionsPath = localPkgDir
|
|
161
|
-
? join(localPkgDir, getDotenvOptionsFilename)
|
|
162
|
-
: undefined;
|
|
163
|
-
const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
|
|
164
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
165
|
-
: {});
|
|
166
|
-
// Merge order: base < local < custom (custom has highest precedence)
|
|
167
|
-
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
168
|
-
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
169
|
-
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
170
|
-
return {
|
|
171
|
-
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
172
|
-
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
173
|
-
};
|
|
97
|
+
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Resolve an optional flag with "--exclude-all" overrides.
|
|
100
|
+
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
101
|
+
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
102
|
+
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
103
|
+
*
|
|
104
|
+
* @param exclude - Individual include/exclude flag.
|
|
105
|
+
* @param excludeOff - Individual "...-off" flag.
|
|
106
|
+
* @param defaultValue - Default for the individual flag.
|
|
107
|
+
* @param excludeAll - Global "exclude-all" flag.
|
|
108
|
+
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
|
|
112
|
+
*/
|
|
113
|
+
const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
|
|
114
|
+
// Order of precedence:
|
|
115
|
+
// 1) Individual explicit "on" wins outright.
|
|
116
|
+
// 2) Individual explicit "off" wins over any global.
|
|
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
|
+
})();
|
|
136
|
+
/**
|
|
137
|
+
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
138
|
+
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
139
|
+
*
|
|
140
|
+
* @typeParam T - Target object type.
|
|
141
|
+
* @param obj - The object to write to.
|
|
142
|
+
* @param key - The optional boolean property key of {@link T}.
|
|
143
|
+
* @param value - The value to set or `undefined` to unset.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
147
|
+
*/
|
|
148
|
+
const setOptionalFlag = (obj, key, value) => {
|
|
149
|
+
const target = obj;
|
|
150
|
+
const k = key;
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
152
|
+
if (value === undefined)
|
|
153
|
+
delete target[k];
|
|
154
|
+
else
|
|
155
|
+
target[k] = value;
|
|
174
156
|
};
|
|
175
157
|
|
|
176
158
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
181
|
-
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
182
|
-
* later per the staged plan.
|
|
159
|
+
* Merge and normalize raw Commander options (current + parent + defaults)
|
|
160
|
+
* into a GetDotenvCliOptions-like object. Types are intentionally wide to
|
|
161
|
+
* avoid cross-layer coupling; callers may cast as needed.
|
|
183
162
|
*/
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
163
|
+
const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
164
|
+
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
165
|
+
? JSON.parse(parentJson)
|
|
166
|
+
: undefined;
|
|
167
|
+
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
168
|
+
const current = { ...rest };
|
|
169
|
+
if (typeof scripts === 'string') {
|
|
170
|
+
try {
|
|
171
|
+
current.scripts = JSON.parse(scripts);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// ignore parse errors; leave scripts undefined
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
178
|
+
const d = defaults;
|
|
179
|
+
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
180
|
+
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
181
|
+
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
182
|
+
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
183
|
+
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
184
|
+
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
185
|
+
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
186
|
+
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
187
|
+
// warnEntropy (tri-state)
|
|
188
|
+
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
189
|
+
// Normalize shell for predictability: explicit default shell per OS.
|
|
190
|
+
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
191
|
+
let resolvedShell = merged.shell;
|
|
192
|
+
if (shellOff)
|
|
193
|
+
resolvedShell = false;
|
|
194
|
+
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
195
|
+
resolvedShell = defaultShell;
|
|
196
|
+
}
|
|
197
|
+
else if (typeof resolvedShell !== 'string' &&
|
|
198
|
+
typeof defaults.shell === 'string') {
|
|
199
|
+
resolvedShell = defaults.shell;
|
|
200
|
+
}
|
|
201
|
+
merged.shell = resolvedShell;
|
|
202
|
+
const cmd = typeof command === 'string' ? command : undefined;
|
|
203
|
+
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
204
|
+
};
|
|
211
205
|
|
|
212
206
|
/**
|
|
213
207
|
* Zod schemas for configuration files discovered by the new loader.
|
|
@@ -447,19 +441,182 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
|
447
441
|
};
|
|
448
442
|
|
|
449
443
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* references in strings and records. It supports both whitespace and
|
|
454
|
-
* bracket syntaxes with optional defaults:
|
|
455
|
-
*
|
|
456
|
-
* - Whitespace: `$VAR[:default]`
|
|
457
|
-
* - Bracketed: `${VAR[:default]}`
|
|
444
|
+
* Validate a composed env against config-provided validation surfaces.
|
|
445
|
+
* Precedence for validation definitions:
|
|
446
|
+
* project.local -\> project.public -\> packaged
|
|
458
447
|
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
/**
|
|
463
620
|
* Like String.prototype.search but returns the last index.
|
|
464
621
|
* @internal
|
|
465
622
|
*/
|
|
@@ -582,31 +739,241 @@ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduc
|
|
|
582
739
|
*/
|
|
583
740
|
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
584
741
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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);
|
|
788
|
+
}
|
|
789
|
+
// Output path (interpolated later; help can remain static)
|
|
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;
|
|
598
914
|
};
|
|
915
|
+
|
|
599
916
|
/**
|
|
600
|
-
*
|
|
601
|
-
* - kind: env \> global
|
|
602
|
-
* - privacy: local \> public
|
|
603
|
-
* - source: project \> packaged \> base
|
|
917
|
+
* Zod schemas for programmatic GetDotenv options.
|
|
604
918
|
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
919
|
+
* NOTE: These schemas are introduced without wiring to avoid behavior changes.
|
|
920
|
+
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
921
|
+
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
922
|
+
* later per the staged plan.
|
|
607
923
|
*/
|
|
608
|
-
|
|
609
|
-
|
|
924
|
+
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
925
|
+
const processEnvSchema = z.record(z.string(), z.string().optional());
|
|
926
|
+
// RAW: all fields optional — undefined means "inherit" from lower layers.
|
|
927
|
+
const getDotenvOptionsSchemaRaw = z.object({
|
|
928
|
+
defaultEnv: z.string().optional(),
|
|
929
|
+
dotenvToken: z.string().optional(),
|
|
930
|
+
dynamicPath: z.string().optional(),
|
|
931
|
+
// Dynamic map is intentionally wide for now; refine once sources are normalized.
|
|
932
|
+
dynamic: z.record(z.string(), z.unknown()).optional(),
|
|
933
|
+
env: z.string().optional(),
|
|
934
|
+
excludeDynamic: z.boolean().optional(),
|
|
935
|
+
excludeEnv: z.boolean().optional(),
|
|
936
|
+
excludeGlobal: z.boolean().optional(),
|
|
937
|
+
excludePrivate: z.boolean().optional(),
|
|
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;
|
|
951
|
+
|
|
952
|
+
const applyKv = (current, kv) => {
|
|
953
|
+
if (!kv || Object.keys(kv).length === 0)
|
|
954
|
+
return current;
|
|
955
|
+
const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
|
|
956
|
+
return { ...current, ...expanded };
|
|
957
|
+
};
|
|
958
|
+
const applyConfigSlice = (current, cfg, env) => {
|
|
959
|
+
if (!cfg)
|
|
960
|
+
return current;
|
|
961
|
+
// kind axis: global then env (env overrides global)
|
|
962
|
+
const afterGlobal = applyKv(current, cfg.vars);
|
|
963
|
+
const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
|
|
964
|
+
return applyKv(afterGlobal, envKv);
|
|
965
|
+
};
|
|
966
|
+
/**
|
|
967
|
+
* Overlay config-provided values onto a base ProcessEnv using precedence axes:
|
|
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, }) => {
|
|
976
|
+
let current = { ...base };
|
|
610
977
|
// Source: packaged (public -> local)
|
|
611
978
|
current = applyConfigSlice(current, configs.packaged, env);
|
|
612
979
|
// Packaged "local" is not expected by policy; if present, honor it.
|
|
@@ -1158,6 +1525,8 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
1158
1525
|
};
|
|
1159
1526
|
};
|
|
1160
1527
|
|
|
1528
|
+
// Dynamic help support: attach a private symbol to Option for description fns.
|
|
1529
|
+
const DYN_DESC_SYM = Symbol('getdotenv.dynamic.description');
|
|
1161
1530
|
const HOST_META_URL = import.meta.url;
|
|
1162
1531
|
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
1163
1532
|
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
@@ -1173,13 +1542,20 @@ const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
|
1173
1542
|
*
|
|
1174
1543
|
* NOTE: This host is additive and does not alter the legacy CLI.
|
|
1175
1544
|
*/
|
|
1176
|
-
class GetDotenvCli extends Command {
|
|
1545
|
+
let GetDotenvCli$1 = class GetDotenvCli extends Command {
|
|
1177
1546
|
/** Registered top-level plugins (composition happens via .use()) */
|
|
1178
1547
|
_plugins = [];
|
|
1179
1548
|
/** One-time installation guard */
|
|
1180
1549
|
_installed = false;
|
|
1181
1550
|
/** Optional header line to prepend in help output */
|
|
1182
1551
|
[HELP_HEADER_SYMBOL];
|
|
1552
|
+
/**
|
|
1553
|
+
* Create a subcommand using the same subclass, preserving helpers like
|
|
1554
|
+
* dynamicOption on children.
|
|
1555
|
+
*/
|
|
1556
|
+
createCommand(name) {
|
|
1557
|
+
return new this.constructor(name);
|
|
1558
|
+
}
|
|
1183
1559
|
constructor(alias = 'getdotenv') {
|
|
1184
1560
|
super(alias);
|
|
1185
1561
|
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
@@ -1187,15 +1563,18 @@ class GetDotenvCli extends Command {
|
|
|
1187
1563
|
// child uses passThroughOptions.
|
|
1188
1564
|
this.enablePositionalOptions();
|
|
1189
1565
|
// Configure grouped help: show only base options in default "Options";
|
|
1190
|
-
//
|
|
1566
|
+
// we will insert App/Plugin sections before Commands in helpInformation().
|
|
1191
1567
|
this.configureHelp({
|
|
1192
1568
|
visibleOptions: (cmd) => {
|
|
1193
|
-
const all = cmd.options ??
|
|
1194
|
-
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1569
|
+
const all = cmd.options ?? [];
|
|
1570
|
+
const parent = cmd.parent ?? null;
|
|
1571
|
+
const isRoot = parent === null;
|
|
1572
|
+
const list = isRoot
|
|
1573
|
+
? all.filter((opt) => {
|
|
1574
|
+
const group = opt.__group;
|
|
1575
|
+
return group === 'base';
|
|
1576
|
+
})
|
|
1577
|
+
: all.slice(); // subcommands: show all options (their own "Options:" block)
|
|
1199
1578
|
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1200
1579
|
const hasShort = (opt) => {
|
|
1201
1580
|
const flags = opt.flags ?? '';
|
|
@@ -1203,19 +1582,18 @@ class GetDotenvCli extends Command {
|
|
|
1203
1582
|
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1204
1583
|
};
|
|
1205
1584
|
const byFlags = (opt) => opt.flags ?? '';
|
|
1206
|
-
|
|
1585
|
+
list.sort((a, b) => {
|
|
1207
1586
|
const aS = hasShort(a) ? 1 : 0;
|
|
1208
1587
|
const bS = hasShort(b) ? 1 : 0;
|
|
1209
1588
|
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1210
1589
|
});
|
|
1211
|
-
return
|
|
1590
|
+
return list;
|
|
1212
1591
|
},
|
|
1213
1592
|
});
|
|
1214
1593
|
this.addHelpText('beforeAll', () => {
|
|
1215
1594
|
const header = this[HELP_HEADER_SYMBOL];
|
|
1216
1595
|
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1217
1596
|
});
|
|
1218
|
-
this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
|
|
1219
1597
|
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
1220
1598
|
// mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
|
|
1221
1599
|
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
@@ -1227,9 +1605,15 @@ class GetDotenvCli extends Command {
|
|
|
1227
1605
|
});
|
|
1228
1606
|
}
|
|
1229
1607
|
/**
|
|
1230
|
-
* Resolve options (strict) and compute dotenv context.
|
|
1608
|
+
* Resolve options (strict) and compute dotenv context.
|
|
1609
|
+
* Stores the context on the instance under a symbol.
|
|
1610
|
+
*
|
|
1611
|
+
* Options:
|
|
1612
|
+
* - opts.runAfterResolve (default true): when false, skips running plugin
|
|
1613
|
+
* afterResolve hooks. Useful for top-level help rendering to avoid
|
|
1614
|
+
* long-running side-effects while still evaluating dynamic help text.
|
|
1231
1615
|
*/
|
|
1232
|
-
async resolveAndLoad(customOptions = {}) {
|
|
1616
|
+
async resolveAndLoad(customOptions = {}, opts) {
|
|
1233
1617
|
// Resolve defaults, then validate strictly under the new host.
|
|
1234
1618
|
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
1235
1619
|
getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
@@ -1240,9 +1624,64 @@ class GetDotenvCli extends Command {
|
|
|
1240
1624
|
ctx;
|
|
1241
1625
|
// Ensure plugins are installed exactly once, then run afterResolve.
|
|
1242
1626
|
await this.install();
|
|
1243
|
-
|
|
1627
|
+
if (opts?.runAfterResolve ?? true) {
|
|
1628
|
+
await this._runAfterResolve(ctx);
|
|
1629
|
+
}
|
|
1244
1630
|
return ctx;
|
|
1245
1631
|
}
|
|
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
|
|
1638
|
+
createDynamicOption(flags, desc, parser, defaultValue) {
|
|
1639
|
+
const opt = new Option(flags, '');
|
|
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;
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Evaluate dynamic descriptions for this command and all descendants using
|
|
1660
|
+
* the provided resolved configuration. Mutates the Option.description in
|
|
1661
|
+
* place so Commander help renders updated text.
|
|
1662
|
+
*/
|
|
1663
|
+
evaluateDynamicOptions(resolved) {
|
|
1664
|
+
const visit = (cmd) => {
|
|
1665
|
+
const arr = cmd.options ?? [];
|
|
1666
|
+
for (const o of arr) {
|
|
1667
|
+
const dyn = o[DYN_DESC_SYM];
|
|
1668
|
+
if (typeof dyn === 'function') {
|
|
1669
|
+
try {
|
|
1670
|
+
const txt = dyn(resolved);
|
|
1671
|
+
// Commander Option has a public "description" field used by help.
|
|
1672
|
+
o.description = txt;
|
|
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);
|
|
1684
|
+
}
|
|
1246
1685
|
/**
|
|
1247
1686
|
* Retrieve the current invocation context (if any).
|
|
1248
1687
|
*/
|
|
@@ -1272,6 +1711,7 @@ class GetDotenvCli extends Command {
|
|
|
1272
1711
|
tagAppOptions(fn) {
|
|
1273
1712
|
const root = this;
|
|
1274
1713
|
const originalAddOption = root.addOption.bind(root);
|
|
1714
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1275
1715
|
const originalOption = root.option.bind(root);
|
|
1276
1716
|
const tagLatest = (cmd, group) => {
|
|
1277
1717
|
const optsArr = cmd.options;
|
|
@@ -1284,6 +1724,7 @@ class GetDotenvCli extends Command {
|
|
|
1284
1724
|
opt.__group = 'app';
|
|
1285
1725
|
return originalAddOption(opt);
|
|
1286
1726
|
};
|
|
1727
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1287
1728
|
root.option = function patchedOption(...args) {
|
|
1288
1729
|
const ret = originalOption(...args);
|
|
1289
1730
|
tagLatest(this, 'app');
|
|
@@ -1294,6 +1735,7 @@ class GetDotenvCli extends Command {
|
|
|
1294
1735
|
}
|
|
1295
1736
|
finally {
|
|
1296
1737
|
root.addOption = originalAddOption;
|
|
1738
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
1297
1739
|
root.option = originalOption;
|
|
1298
1740
|
}
|
|
1299
1741
|
}
|
|
@@ -1339,6 +1781,40 @@ class GetDotenvCli extends Command {
|
|
|
1339
1781
|
}
|
|
1340
1782
|
return this;
|
|
1341
1783
|
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Insert grouped plugin/app options between "Options" and "Commands" for
|
|
1786
|
+
* hybrid ordering. Applies to root and any parent command.
|
|
1787
|
+
*/
|
|
1788
|
+
helpInformation() {
|
|
1789
|
+
// Base help text first (includes beforeAll/after hooks).
|
|
1790
|
+
const base = super.helpInformation();
|
|
1791
|
+
const groups = this.#renderOptionGroups(this);
|
|
1792
|
+
const block = typeof groups === 'string' ? groups.trim() : '';
|
|
1793
|
+
let out = base;
|
|
1794
|
+
if (!block) {
|
|
1795
|
+
// Ensure a trailing blank line even when no extra groups render.
|
|
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;
|
|
1817
|
+
}
|
|
1342
1818
|
/**
|
|
1343
1819
|
* Register a plugin for installation (parent level).
|
|
1344
1820
|
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
@@ -1377,7 +1853,7 @@ class GetDotenvCli extends Command {
|
|
|
1377
1853
|
for (const p of this._plugins)
|
|
1378
1854
|
await run(p);
|
|
1379
1855
|
}
|
|
1380
|
-
// Render App/Plugin grouped options
|
|
1856
|
+
// Render App/Plugin grouped options (used by helpInformation override).
|
|
1381
1857
|
#renderOptionGroups(cmd) {
|
|
1382
1858
|
const all = cmd.options ?? [];
|
|
1383
1859
|
const byGroup = new Map();
|
|
@@ -1419,369 +1895,96 @@ class GetDotenvCli extends Command {
|
|
|
1419
1895
|
}
|
|
1420
1896
|
// Plugin groups sorted by id
|
|
1421
1897
|
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1898
|
+
const currentName = cmd.name?.() ?? '';
|
|
1422
1899
|
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1423
1900
|
for (const k of pluginKeys) {
|
|
1424
1901
|
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1425
1902
|
const rows = byGroup.get(k) ?? [];
|
|
1426
|
-
|
|
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) {
|
|
1427
1906
|
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1428
1907
|
}
|
|
1429
1908
|
}
|
|
1430
1909
|
return out;
|
|
1431
1910
|
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
/**
|
|
1435
|
-
* Validate a composed env against config-provided validation surfaces.
|
|
1436
|
-
* Precedence for validation definitions:
|
|
1437
|
-
* project.local -\> project.public -\> packaged
|
|
1438
|
-
*
|
|
1439
|
-
* Behavior:
|
|
1440
|
-
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
1441
|
-
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
1442
|
-
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
1443
|
-
*/
|
|
1444
|
-
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
1445
|
-
const pick = (getter) => {
|
|
1446
|
-
const pl = sources.project?.local;
|
|
1447
|
-
const pp = sources.project?.public;
|
|
1448
|
-
const pk = sources.packaged;
|
|
1449
|
-
return ((pl && getter(pl)) ||
|
|
1450
|
-
(pp && getter(pp)) ||
|
|
1451
|
-
(pk && getter(pk)) ||
|
|
1452
|
-
undefined);
|
|
1453
|
-
};
|
|
1454
|
-
const schema = pick((cfg) => cfg['schema']);
|
|
1455
|
-
if (schema &&
|
|
1456
|
-
typeof schema.safeParse === 'function') {
|
|
1457
|
-
try {
|
|
1458
|
-
const parsed = schema.safeParse(finalEnv);
|
|
1459
|
-
if (!parsed.success) {
|
|
1460
|
-
// Try to render zod-style issues when available.
|
|
1461
|
-
const err = parsed.error;
|
|
1462
|
-
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
1463
|
-
? err.issues.map((i) => {
|
|
1464
|
-
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1465
|
-
const msg = i.message ?? 'Invalid value';
|
|
1466
|
-
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
1467
|
-
})
|
|
1468
|
-
: ['[schema] validation failed'];
|
|
1469
|
-
return issues;
|
|
1470
|
-
}
|
|
1471
|
-
return [];
|
|
1472
|
-
}
|
|
1473
|
-
catch {
|
|
1474
|
-
// If schema invocation fails, surface a single diagnostic.
|
|
1475
|
-
return [
|
|
1476
|
-
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
1477
|
-
];
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
1481
|
-
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
1482
|
-
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
1483
|
-
if (missing.length > 0) {
|
|
1484
|
-
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
return [];
|
|
1488
|
-
};
|
|
1489
|
-
|
|
1490
|
-
/**
|
|
1491
|
-
* Attach legacy root flags to a Commander program.
|
|
1492
|
-
* Uses provided defaults to render help labels without coupling to generators.
|
|
1493
|
-
*/
|
|
1494
|
-
const attachRootOptions = (program, defaults, opts) => {
|
|
1495
|
-
// Install temporary wrappers to tag all options added here as "base".
|
|
1496
|
-
const GROUP = 'base';
|
|
1497
|
-
const tagLatest = (cmd, group) => {
|
|
1498
|
-
const optsArr = cmd.options;
|
|
1499
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
1500
|
-
const last = optsArr[optsArr.length - 1];
|
|
1501
|
-
last.__group = group;
|
|
1502
|
-
}
|
|
1503
|
-
};
|
|
1504
|
-
const originalAddOption = program.addOption.bind(program);
|
|
1505
|
-
const originalOption = program.option.bind(program);
|
|
1506
|
-
program.addOption = function patchedAdd(opt) {
|
|
1507
|
-
// Tag before adding, in case consumers inspect the Option directly.
|
|
1508
|
-
opt.__group = GROUP;
|
|
1509
|
-
const ret = originalAddOption(opt);
|
|
1510
|
-
return ret;
|
|
1511
|
-
};
|
|
1512
|
-
program.option = function patchedOption(...args) {
|
|
1513
|
-
const ret = originalOption(...args);
|
|
1514
|
-
tagLatest(this, GROUP);
|
|
1515
|
-
return ret;
|
|
1516
|
-
};
|
|
1517
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
1518
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
1519
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
1520
|
-
// Build initial chain.
|
|
1521
|
-
let p = program
|
|
1522
|
-
.enablePositionalOptions()
|
|
1523
|
-
.passThroughOptions()
|
|
1524
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
1525
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
1526
|
-
['KEY1', 'VAL1'],
|
|
1527
|
-
['KEY2', 'VAL2'],
|
|
1528
|
-
]
|
|
1529
|
-
.map((v) => v.join(va))
|
|
1530
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
1531
|
-
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
1532
|
-
// Default is OFF; the generator opts in explicitly.
|
|
1533
|
-
if (opts?.includeCommandOption === true) {
|
|
1534
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
1535
|
-
}
|
|
1536
|
-
p = p
|
|
1537
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
1538
|
-
.addOption(new Option('-s, --shell [string]', (() => {
|
|
1539
|
-
let defaultLabel = '';
|
|
1540
|
-
if (shell !== undefined) {
|
|
1541
|
-
if (typeof shell === 'boolean') {
|
|
1542
|
-
defaultLabel = ' (default OS shell)';
|
|
1543
|
-
}
|
|
1544
|
-
else if (typeof shell === 'string') {
|
|
1545
|
-
// Safe string interpolation
|
|
1546
|
-
defaultLabel = ` (default ${shell})`;
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
1550
|
-
})()).conflicts('shellOff'))
|
|
1551
|
-
.addOption(new Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
1552
|
-
.addOption(new Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
1553
|
-
.addOption(new Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
1554
|
-
.addOption(new Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
1555
|
-
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
1556
|
-
? ' (default)'
|
|
1557
|
-
: ''}`).conflicts('excludeAllOff'))
|
|
1558
|
-
.addOption(new Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
1559
|
-
.addOption(new Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
1560
|
-
.addOption(new Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
1561
|
-
.addOption(new Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
1562
|
-
.addOption(new Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
1563
|
-
.addOption(new Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
1564
|
-
.addOption(new Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
1565
|
-
.addOption(new Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
1566
|
-
.addOption(new Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
1567
|
-
.addOption(new Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
1568
|
-
.addOption(new Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
1569
|
-
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
1570
|
-
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
1571
|
-
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
1572
|
-
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
1573
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
1574
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
1575
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
1576
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
1577
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
1578
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
1579
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
1580
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
1581
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
1582
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
1583
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
1584
|
-
// Hidden scripts pipe-through (stringified)
|
|
1585
|
-
.addOption(new Option('--scripts <string>')
|
|
1586
|
-
.default(JSON.stringify(scripts))
|
|
1587
|
-
.hideHelp());
|
|
1588
|
-
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
1589
|
-
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
1590
|
-
// Validation: strict mode fails on env validation issues (warn by default).
|
|
1591
|
-
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
1592
|
-
// Entropy diagnostics (presentation-only)
|
|
1593
|
-
p = p
|
|
1594
|
-
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
1595
|
-
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
1596
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
1597
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
1598
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
1599
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
1600
|
-
// Restore original methods to avoid tagging future additions outside base.
|
|
1601
|
-
program.addOption = originalAddOption;
|
|
1602
|
-
program.option = originalOption;
|
|
1603
|
-
return p;
|
|
1604
1911
|
};
|
|
1605
1912
|
|
|
1606
|
-
/**
|
|
1607
|
-
*
|
|
1608
|
-
* - If the user explicitly enabled the flag, return true.
|
|
1609
|
-
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
1610
|
-
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
1611
|
-
*
|
|
1612
|
-
* @param exclude - The "on" flag value as parsed by Commander.
|
|
1613
|
-
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
1614
|
-
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
1615
|
-
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
1913
|
+
/** src/cliHost/definePlugin.ts
|
|
1914
|
+
* Plugin contracts for the GetDotenv CLI host.
|
|
1616
1915
|
*
|
|
1617
|
-
*
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
* ```
|
|
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.
|
|
1621
1919
|
*/
|
|
1622
|
-
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
1623
1920
|
/**
|
|
1624
|
-
*
|
|
1625
|
-
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
1626
|
-
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
1627
|
-
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
1628
|
-
*
|
|
1629
|
-
* @param exclude - Individual include/exclude flag.
|
|
1630
|
-
* @param excludeOff - Individual "...-off" flag.
|
|
1631
|
-
* @param defaultValue - Default for the individual flag.
|
|
1632
|
-
* @param excludeAll - Global "exclude-all" flag.
|
|
1633
|
-
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
1921
|
+
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
1634
1922
|
*
|
|
1635
1923
|
* @example
|
|
1636
|
-
*
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
// Order of precedence:
|
|
1640
|
-
// 1) Individual explicit "on" wins outright.
|
|
1641
|
-
// 2) Individual explicit "off" wins over any global.
|
|
1642
|
-
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
1643
|
-
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
1644
|
-
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
1645
|
-
(() => {
|
|
1646
|
-
// Individual "on"
|
|
1647
|
-
if (exclude === true)
|
|
1648
|
-
return true;
|
|
1649
|
-
// Individual "off"
|
|
1650
|
-
if (excludeOff === true)
|
|
1651
|
-
return undefined;
|
|
1652
|
-
// Global "exclude-all" ON (unless explicitly turned off)
|
|
1653
|
-
if (excludeAll === true)
|
|
1654
|
-
return true;
|
|
1655
|
-
// Global "exclude-all-off" (unless explicitly enabled)
|
|
1656
|
-
if (excludeAllOff === true)
|
|
1657
|
-
return undefined;
|
|
1658
|
-
// Default
|
|
1659
|
-
return defaultValue ? true : undefined;
|
|
1660
|
-
})();
|
|
1661
|
-
/**
|
|
1662
|
-
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
1663
|
-
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
1664
|
-
*
|
|
1665
|
-
* @typeParam T - Target object type.
|
|
1666
|
-
* @param obj - The object to write to.
|
|
1667
|
-
* @param key - The optional boolean property key of {@link T}.
|
|
1668
|
-
* @param value - The value to set or `undefined` to unset.
|
|
1669
|
-
*
|
|
1670
|
-
* @remarks
|
|
1671
|
-
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
1924
|
+
* const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
|
|
1925
|
+
* .use(childA)
|
|
1926
|
+
* .use(childB);
|
|
1672
1927
|
*/
|
|
1673
|
-
const
|
|
1674
|
-
const
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1928
|
+
const definePlugin = (spec) => {
|
|
1929
|
+
const { children = [], ...rest } = spec;
|
|
1930
|
+
const plugin = {
|
|
1931
|
+
...rest,
|
|
1932
|
+
children: [...children],
|
|
1933
|
+
use(child) {
|
|
1934
|
+
this.children.push(child);
|
|
1935
|
+
return this;
|
|
1936
|
+
},
|
|
1937
|
+
};
|
|
1938
|
+
return plugin;
|
|
1681
1939
|
};
|
|
1682
1940
|
|
|
1683
1941
|
/**
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1942
|
+
* GetDotenvCli with root helpers as real class methods.
|
|
1943
|
+
* - attachRootOptions: installs legacy/base root flags on the command.
|
|
1944
|
+
* - passOptions: merges flags (parent \< current), computes dotenv context once,
|
|
1945
|
+
* runs validation, and persists merged options for nested flows.
|
|
1687
1946
|
*/
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
catch {
|
|
1699
|
-
// ignore parse errors; leave scripts undefined
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
1703
|
-
const d = defaults;
|
|
1704
|
-
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
1705
|
-
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
1706
|
-
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
1707
|
-
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
1708
|
-
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
1709
|
-
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
1710
|
-
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
1711
|
-
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
1712
|
-
// warnEntropy (tri-state)
|
|
1713
|
-
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
1714
|
-
// Normalize shell for predictability: explicit default shell per OS.
|
|
1715
|
-
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
1716
|
-
let resolvedShell = merged.shell;
|
|
1717
|
-
if (shellOff)
|
|
1718
|
-
resolvedShell = false;
|
|
1719
|
-
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
1720
|
-
resolvedShell = defaultShell;
|
|
1721
|
-
}
|
|
1722
|
-
else if (typeof resolvedShell !== 'string' &&
|
|
1723
|
-
typeof defaults.shell === 'string') {
|
|
1724
|
-
resolvedShell = defaults.shell;
|
|
1947
|
+
class GetDotenvCli extends GetDotenvCli$1 {
|
|
1948
|
+
/**
|
|
1949
|
+
* Attach legacy root flags to this CLI instance. Defaults come from
|
|
1950
|
+
* baseRootOptionDefaults when none are provided.
|
|
1951
|
+
*/
|
|
1952
|
+
attachRootOptions(defaults, opts) {
|
|
1953
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
1954
|
+
attachRootOptions(this, d, opts);
|
|
1955
|
+
return this;
|
|
1725
1956
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
merged;
|
|
1744
|
-
// Also store on the host for downstream ergonomic accessors.
|
|
1745
|
-
this._setOptionsBag(merged);
|
|
1746
|
-
// Build service options and compute context (always-on config loader path).
|
|
1747
|
-
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1748
|
-
await this.resolveAndLoad(serviceOptions);
|
|
1749
|
-
// Global validation: once after Phase C using config sources.
|
|
1750
|
-
try {
|
|
1751
|
-
const ctx = this.getCtx();
|
|
1752
|
-
const dotenv = (ctx?.dotenv ?? {});
|
|
1753
|
-
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
1754
|
-
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1755
|
-
if (Array.isArray(issues) && issues.length > 0) {
|
|
1756
|
-
const logger = (merged.logger ??
|
|
1757
|
-
console);
|
|
1758
|
-
const emit = logger.error ?? logger.log;
|
|
1759
|
-
issues.forEach((m) => {
|
|
1760
|
-
emit(m);
|
|
1761
|
-
});
|
|
1762
|
-
if (merged.strict) {
|
|
1763
|
-
// Deterministic failure under strict mode
|
|
1764
|
-
process.exit(1);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
catch {
|
|
1769
|
-
// Be tolerant: validation errors reported above; unexpected failures here
|
|
1770
|
-
// should not crash non-strict flows.
|
|
1771
|
-
}
|
|
1772
|
-
});
|
|
1773
|
-
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
1774
|
-
// with the same merged options and context without duplicating logic.
|
|
1775
|
-
this.hook('preAction', async (thisCommand) => {
|
|
1776
|
-
const raw = thisCommand.opts();
|
|
1777
|
-
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
1778
|
-
thisCommand.getDotenvCliOptions =
|
|
1779
|
-
merged;
|
|
1780
|
-
this._setOptionsBag(merged);
|
|
1781
|
-
// Avoid duplicate heavy work if a context is already present.
|
|
1782
|
-
if (!this.getCtx()) {
|
|
1957
|
+
/**
|
|
1958
|
+
* Install preSubcommand/preAction hooks that:
|
|
1959
|
+
* - Merge options (parent round-trip + current invocation) using resolveCliOptions.
|
|
1960
|
+
* - Persist the merged bag on the current command and on the host (for ergonomics).
|
|
1961
|
+
* - Compute the dotenv context once via resolveAndLoad(serviceOptions).
|
|
1962
|
+
* - Validate the composed env against discovered config (warn or --strict fail).
|
|
1963
|
+
*/
|
|
1964
|
+
passOptions(defaults) {
|
|
1965
|
+
const d = (defaults ?? baseRootOptionDefaults);
|
|
1966
|
+
this.hook('preSubcommand', async (thisCommand) => {
|
|
1967
|
+
const raw = thisCommand.opts();
|
|
1968
|
+
const { merged } = resolveCliOptions(raw, d, process.env.getDotenvCliOptions);
|
|
1969
|
+
// Persist merged options (for nested behavior and ergonomic access).
|
|
1970
|
+
thisCommand.getDotenvCliOptions =
|
|
1971
|
+
merged;
|
|
1972
|
+
this._setOptionsBag(merged);
|
|
1973
|
+
// Build service options and compute context (always-on loader path).
|
|
1783
1974
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1784
1975
|
await this.resolveAndLoad(serviceOptions);
|
|
1976
|
+
// Refresh dynamic option descriptions using resolved config + plugin slices
|
|
1977
|
+
try {
|
|
1978
|
+
const ctx = this.getCtx();
|
|
1979
|
+
this.evaluateDynamicOptions({
|
|
1980
|
+
...ctx?.optionsResolved,
|
|
1981
|
+
plugins: ctx?.pluginConfigs ?? {},
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
catch {
|
|
1985
|
+
/* best-effort */
|
|
1986
|
+
}
|
|
1987
|
+
// Global validation: once after Phase C using config sources.
|
|
1785
1988
|
try {
|
|
1786
1989
|
const ctx = this.getCtx();
|
|
1787
1990
|
const dotenv = (ctx?.dotenv ?? {});
|
|
@@ -1800,43 +2003,56 @@ GetDotenvCli.prototype.passOptions = function (defaults) {
|
|
|
1800
2003
|
}
|
|
1801
2004
|
}
|
|
1802
2005
|
catch {
|
|
1803
|
-
//
|
|
2006
|
+
// Be tolerant: do not crash non-strict flows on unexpected validator failures.
|
|
1804
2007
|
}
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1840
2056
|
/**
|
|
1841
2057
|
* Helper to retrieve the merged root options bag from any action handler
|
|
1842
2058
|
* that only has access to thisCommand. Avoids structural casts.
|