@karmaniverous/get-dotenv 5.0.0 → 5.2.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 +14 -0
- package/dist/cliHost.cjs +393 -21
- package/dist/cliHost.d.cts +132 -3
- package/dist/cliHost.d.mts +132 -3
- package/dist/cliHost.d.ts +132 -3
- package/dist/cliHost.mjs +393 -22
- package/dist/config.cjs +10 -5
- package/dist/config.d.cts +2 -0
- package/dist/config.d.mts +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.mjs +10 -5
- package/dist/env-overlay.d.cts +2 -0
- package/dist/env-overlay.d.mts +2 -0
- package/dist/env-overlay.d.ts +2 -0
- package/dist/getdotenv.cli.mjs +678 -39
- package/dist/index.cjs +604 -245
- package/dist/index.d.cts +36 -1
- package/dist/index.d.mts +36 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.mjs +604 -246
- package/dist/plugins-aws.cjs +43 -1
- package/dist/plugins-aws.d.cts +115 -0
- package/dist/plugins-aws.d.mts +115 -0
- package/dist/plugins-aws.d.ts +115 -0
- package/dist/plugins-aws.mjs +43 -1
- package/dist/plugins-batch.cjs +46 -6
- package/dist/plugins-batch.d.cts +115 -0
- package/dist/plugins-batch.d.mts +115 -0
- package/dist/plugins-batch.d.ts +115 -0
- package/dist/plugins-batch.mjs +46 -6
- package/dist/plugins-init.d.cts +115 -0
- package/dist/plugins-init.d.mts +115 -0
- package/dist/plugins-init.d.ts +115 -0
- package/package.json +25 -24
package/dist/cliHost.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { packageDirectory } from 'package-directory';
|
|
4
|
+
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
4
5
|
import path, { join, extname } from 'path';
|
|
5
6
|
import { z } from 'zod';
|
|
6
|
-
import url, { fileURLToPath, pathToFileURL } from 'url';
|
|
7
7
|
import YAML from 'yaml';
|
|
8
8
|
import { nanoid } from 'nanoid';
|
|
9
9
|
import { parse } from 'dotenv';
|
|
@@ -35,6 +35,11 @@ const baseRootOptionDefaults = {
|
|
|
35
35
|
dotenvToken: '.env',
|
|
36
36
|
loadProcess: true,
|
|
37
37
|
logger: console,
|
|
38
|
+
// Diagnostics defaults
|
|
39
|
+
warnEntropy: true,
|
|
40
|
+
entropyThreshold: 3.8,
|
|
41
|
+
entropyMinLength: 16,
|
|
42
|
+
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
38
43
|
paths: './',
|
|
39
44
|
pathsDelimiter: ' ',
|
|
40
45
|
privateToken: 'local',
|
|
@@ -55,7 +60,7 @@ const baseRootOptionDefaults = {
|
|
|
55
60
|
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
56
61
|
|
|
57
62
|
/** @internal */
|
|
58
|
-
const isPlainObject = (value) => value !== null &&
|
|
63
|
+
const isPlainObject$1 = (value) => value !== null &&
|
|
59
64
|
typeof value === 'object' &&
|
|
60
65
|
Object.getPrototypeOf(value) === Object.prototype;
|
|
61
66
|
const mergeInto = (target, source) => {
|
|
@@ -63,10 +68,10 @@ const mergeInto = (target, source) => {
|
|
|
63
68
|
if (sVal === undefined)
|
|
64
69
|
continue; // do not overwrite with undefined
|
|
65
70
|
const tVal = target[key];
|
|
66
|
-
if (isPlainObject(tVal) && isPlainObject(sVal)) {
|
|
71
|
+
if (isPlainObject$1(tVal) && isPlainObject$1(sVal)) {
|
|
67
72
|
target[key] = mergeInto({ ...tVal }, sVal);
|
|
68
73
|
}
|
|
69
|
-
else if (isPlainObject(sVal)) {
|
|
74
|
+
else if (isPlainObject$1(sVal)) {
|
|
70
75
|
target[key] = mergeInto({}, sVal);
|
|
71
76
|
}
|
|
72
77
|
else {
|
|
@@ -226,11 +231,12 @@ const getDotenvOptionsSchemaRaw = z.object({
|
|
|
226
231
|
const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
|
|
227
232
|
|
|
228
233
|
/**
|
|
229
|
-
* Zod schemas for configuration files discovered by the new loader.
|
|
234
|
+
* Zod schemas for configuration files discovered by the new loader.
|
|
235
|
+
*
|
|
230
236
|
* Notes:
|
|
231
237
|
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
232
238
|
* - RESOLVED: normalized shapes (paths always string[]).
|
|
233
|
-
* - For
|
|
239
|
+
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
234
240
|
*/
|
|
235
241
|
// String-only env value map
|
|
236
242
|
const stringMap = z.record(z.string(), z.string());
|
|
@@ -245,6 +251,8 @@ const getDotenvConfigSchemaRaw = z.object({
|
|
|
245
251
|
log: z.boolean().optional(),
|
|
246
252
|
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
247
253
|
scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
254
|
+
requiredKeys: z.array(z.string()).optional(),
|
|
255
|
+
schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
248
256
|
vars: stringMap.optional(), // public, global
|
|
249
257
|
envVars: envStringMap.optional(), // public, per-env
|
|
250
258
|
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
@@ -427,9 +435,11 @@ const loadConfigFile = async (filePath) => {
|
|
|
427
435
|
.join('\n');
|
|
428
436
|
throw new Error(`Invalid config ${filePath}:\n${msgs}`);
|
|
429
437
|
}
|
|
430
|
-
// Disallow dynamic in JSON/YAML; allow in JS/TS
|
|
431
|
-
if (!isJsOrTs(filePath) &&
|
|
432
|
-
|
|
438
|
+
// Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
|
|
439
|
+
if (!isJsOrTs(filePath) &&
|
|
440
|
+
(parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
|
|
441
|
+
throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
|
|
442
|
+
`Use JS/TS config for "dynamic" or "schema".`);
|
|
433
443
|
}
|
|
434
444
|
return getDotenvConfigSchemaResolved.parse(parsed.data);
|
|
435
445
|
};
|
|
@@ -617,6 +627,78 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
|
617
627
|
return current;
|
|
618
628
|
};
|
|
619
629
|
|
|
630
|
+
/** src/diagnostics/entropy.ts
|
|
631
|
+
* Entropy diagnostics (presentation-only).
|
|
632
|
+
* - Gated by min length and printable ASCII.
|
|
633
|
+
* - Warn once per key per run when bits/char \>= threshold.
|
|
634
|
+
* - Supports whitelist patterns to suppress known-noise keys.
|
|
635
|
+
*/
|
|
636
|
+
const warned = new Set();
|
|
637
|
+
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
638
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
639
|
+
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
640
|
+
const shannonBitsPerChar = (s) => {
|
|
641
|
+
const freq = new Map();
|
|
642
|
+
for (const ch of s)
|
|
643
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
644
|
+
const n = s.length;
|
|
645
|
+
let h = 0;
|
|
646
|
+
for (const c of freq.values()) {
|
|
647
|
+
const p = c / n;
|
|
648
|
+
h -= p * Math.log2(p);
|
|
649
|
+
}
|
|
650
|
+
return h;
|
|
651
|
+
};
|
|
652
|
+
/**
|
|
653
|
+
* Maybe emit a one-line entropy warning for a key.
|
|
654
|
+
* Caller supplies an `emit(line)` function; the helper ensures once-per-key.
|
|
655
|
+
*/
|
|
656
|
+
const maybeWarnEntropy = (key, value, origin, opts, emit) => {
|
|
657
|
+
if (!opts || opts.warnEntropy === false)
|
|
658
|
+
return;
|
|
659
|
+
if (warned.has(key))
|
|
660
|
+
return;
|
|
661
|
+
const v = value ?? '';
|
|
662
|
+
const minLen = Math.max(0, opts.entropyMinLength ?? 16);
|
|
663
|
+
const threshold = opts.entropyThreshold ?? 3.8;
|
|
664
|
+
if (v.length < minLen)
|
|
665
|
+
return;
|
|
666
|
+
if (!isPrintableAscii(v))
|
|
667
|
+
return;
|
|
668
|
+
const wl = compile$1(opts.entropyWhitelist);
|
|
669
|
+
if (whitelisted(key, wl))
|
|
670
|
+
return;
|
|
671
|
+
const bpc = shannonBitsPerChar(v);
|
|
672
|
+
if (bpc >= threshold) {
|
|
673
|
+
warned.add(key);
|
|
674
|
+
emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const DEFAULT_PATTERNS = [
|
|
679
|
+
'\\bsecret\\b',
|
|
680
|
+
'\\btoken\\b',
|
|
681
|
+
'\\bpass(word)?\\b',
|
|
682
|
+
'\\bapi[_-]?key\\b',
|
|
683
|
+
'\\bkey\\b',
|
|
684
|
+
];
|
|
685
|
+
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
686
|
+
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
687
|
+
const MASK = '[redacted]';
|
|
688
|
+
/**
|
|
689
|
+
* Produce a shallow redacted copy of an env-like object for display.
|
|
690
|
+
*/
|
|
691
|
+
const redactObject = (obj, opts) => {
|
|
692
|
+
if (!opts?.redact)
|
|
693
|
+
return { ...obj };
|
|
694
|
+
const regs = compile(opts.redactPatterns);
|
|
695
|
+
const out = {};
|
|
696
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
697
|
+
out[k] = v && shouldRedactKey(k, regs) ? MASK : v;
|
|
698
|
+
}
|
|
699
|
+
return out;
|
|
700
|
+
};
|
|
701
|
+
|
|
620
702
|
/**
|
|
621
703
|
* Asynchronously read a dotenv file & parse it into an object.
|
|
622
704
|
*
|
|
@@ -866,14 +948,103 @@ const getDotenv = async (options = {}) => {
|
|
|
866
948
|
resultDotenv = dotenvForOutput;
|
|
867
949
|
}
|
|
868
950
|
// Log result.
|
|
869
|
-
if (log)
|
|
870
|
-
|
|
951
|
+
if (log) {
|
|
952
|
+
const redactFlag = options.redact ?? false;
|
|
953
|
+
const redactPatterns = options.redactPatterns ?? undefined;
|
|
954
|
+
const redOpts = {};
|
|
955
|
+
if (redactFlag)
|
|
956
|
+
redOpts.redact = true;
|
|
957
|
+
if (redactFlag && Array.isArray(redactPatterns))
|
|
958
|
+
redOpts.redactPatterns = redactPatterns;
|
|
959
|
+
const bag = redactFlag
|
|
960
|
+
? redactObject(resultDotenv, redOpts)
|
|
961
|
+
: { ...resultDotenv };
|
|
962
|
+
logger.log(bag);
|
|
963
|
+
// Entropy warnings: once-per-key-per-run (presentation only)
|
|
964
|
+
const warnEntropyVal = options.warnEntropy ?? true;
|
|
965
|
+
const entropyThresholdVal = options
|
|
966
|
+
.entropyThreshold;
|
|
967
|
+
const entropyMinLengthVal = options
|
|
968
|
+
.entropyMinLength;
|
|
969
|
+
const entropyWhitelistVal = options
|
|
970
|
+
.entropyWhitelist;
|
|
971
|
+
const entOpts = {};
|
|
972
|
+
if (typeof warnEntropyVal === 'boolean')
|
|
973
|
+
entOpts.warnEntropy = warnEntropyVal;
|
|
974
|
+
if (typeof entropyThresholdVal === 'number')
|
|
975
|
+
entOpts.entropyThreshold = entropyThresholdVal;
|
|
976
|
+
if (typeof entropyMinLengthVal === 'number')
|
|
977
|
+
entOpts.entropyMinLength = entropyMinLengthVal;
|
|
978
|
+
if (Array.isArray(entropyWhitelistVal))
|
|
979
|
+
entOpts.entropyWhitelist = entropyWhitelistVal;
|
|
980
|
+
for (const [k, v] of Object.entries(resultDotenv)) {
|
|
981
|
+
maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
|
|
982
|
+
logger.log(line);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
871
986
|
// Load process.env.
|
|
872
987
|
if (loadProcess)
|
|
873
988
|
Object.assign(process.env, resultDotenv);
|
|
874
989
|
return resultDotenv;
|
|
875
990
|
};
|
|
876
991
|
|
|
992
|
+
/**
|
|
993
|
+
* Deep interpolation utility for string leaves.
|
|
994
|
+
* - Expands string values using dotenv-style expansion against the provided envRef.
|
|
995
|
+
* - Preserves non-strings as-is.
|
|
996
|
+
* - Does not recurse into arrays (arrays are returned unchanged).
|
|
997
|
+
*
|
|
998
|
+
* Intended for:
|
|
999
|
+
* - Phase C option/config interpolation after composing ctx.dotenv.
|
|
1000
|
+
* - Per-plugin config slice interpolation before afterResolve.
|
|
1001
|
+
*/
|
|
1002
|
+
/** @internal */
|
|
1003
|
+
const isPlainObject = (v) => v !== null &&
|
|
1004
|
+
typeof v === 'object' &&
|
|
1005
|
+
!Array.isArray(v) &&
|
|
1006
|
+
Object.getPrototypeOf(v) === Object.prototype;
|
|
1007
|
+
/**
|
|
1008
|
+
* Deeply interpolate string leaves against envRef.
|
|
1009
|
+
* Arrays are not recursed into; they are returned unchanged.
|
|
1010
|
+
*
|
|
1011
|
+
* @typeParam T - Shape of the input value.
|
|
1012
|
+
* @param value - Input value (object/array/primitive).
|
|
1013
|
+
* @param envRef - Reference environment for interpolation.
|
|
1014
|
+
* @returns A new value with string leaves interpolated.
|
|
1015
|
+
*/
|
|
1016
|
+
const interpolateDeep = (value, envRef) => {
|
|
1017
|
+
// Strings: expand and return
|
|
1018
|
+
if (typeof value === 'string') {
|
|
1019
|
+
const out = dotenvExpand(value, envRef);
|
|
1020
|
+
// dotenvExpand returns string | undefined; preserve original on undefined
|
|
1021
|
+
return (out ?? value);
|
|
1022
|
+
}
|
|
1023
|
+
// Arrays: return as-is (no recursion)
|
|
1024
|
+
if (Array.isArray(value)) {
|
|
1025
|
+
return value;
|
|
1026
|
+
}
|
|
1027
|
+
// Plain objects: shallow clone and recurse into values
|
|
1028
|
+
if (isPlainObject(value)) {
|
|
1029
|
+
const src = value;
|
|
1030
|
+
const out = {};
|
|
1031
|
+
for (const [k, v] of Object.entries(src)) {
|
|
1032
|
+
// Recurse for strings/objects; keep arrays as-is; preserve other scalars
|
|
1033
|
+
if (typeof v === 'string')
|
|
1034
|
+
out[k] = dotenvExpand(v, envRef) ?? v;
|
|
1035
|
+
else if (Array.isArray(v))
|
|
1036
|
+
out[k] = v;
|
|
1037
|
+
else if (isPlainObject(v))
|
|
1038
|
+
out[k] = interpolateDeep(v, envRef);
|
|
1039
|
+
else
|
|
1040
|
+
out[k] = v;
|
|
1041
|
+
}
|
|
1042
|
+
return out;
|
|
1043
|
+
}
|
|
1044
|
+
// Other primitives/types: return as-is
|
|
1045
|
+
return value;
|
|
1046
|
+
};
|
|
1047
|
+
|
|
877
1048
|
/**
|
|
878
1049
|
* Compute the dotenv context for the host (uses the config loader/overlay path).
|
|
879
1050
|
* - Resolves and validates options strictly (host-only).
|
|
@@ -957,19 +1128,32 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
957
1128
|
{};
|
|
958
1129
|
const mergedPluginConfigs = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
|
|
959
1130
|
for (const p of plugins) {
|
|
960
|
-
if (!p.id
|
|
1131
|
+
if (!p.id)
|
|
961
1132
|
continue;
|
|
962
1133
|
const slice = mergedPluginConfigs[p.id];
|
|
963
1134
|
if (slice === undefined)
|
|
964
1135
|
continue;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1136
|
+
// Per-plugin interpolation just before validation/afterResolve:
|
|
1137
|
+
// precedence: process.env wins over ctx.dotenv for slice defaults.
|
|
1138
|
+
const envRef = {
|
|
1139
|
+
...dotenv,
|
|
1140
|
+
...process.env,
|
|
1141
|
+
};
|
|
1142
|
+
const interpolated = interpolateDeep(slice, envRef);
|
|
1143
|
+
// Validate if a schema is provided; otherwise accept interpolated slice as-is.
|
|
1144
|
+
if (p.configSchema) {
|
|
1145
|
+
const parsed = p.configSchema.safeParse(interpolated);
|
|
1146
|
+
if (!parsed.success) {
|
|
1147
|
+
const msgs = parsed.error.issues
|
|
1148
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
1149
|
+
.join('\n');
|
|
1150
|
+
throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
|
|
1151
|
+
}
|
|
1152
|
+
mergedPluginConfigs[p.id] = parsed.data;
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
mergedPluginConfigs[p.id] = interpolated;
|
|
971
1156
|
}
|
|
972
|
-
mergedPluginConfigs[p.id] = parsed.data;
|
|
973
1157
|
}
|
|
974
1158
|
return {
|
|
975
1159
|
optionsResolved: validated,
|
|
@@ -981,6 +1165,8 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
981
1165
|
|
|
982
1166
|
const HOST_META_URL = import.meta.url;
|
|
983
1167
|
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
1168
|
+
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
1169
|
+
const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
984
1170
|
/**
|
|
985
1171
|
* Plugin-first CLI host for get-dotenv. Extends Commander.Command.
|
|
986
1172
|
*
|
|
@@ -997,15 +1183,46 @@ class GetDotenvCli extends Command {
|
|
|
997
1183
|
_plugins = [];
|
|
998
1184
|
/** One-time installation guard */
|
|
999
1185
|
_installed = false;
|
|
1186
|
+
/** Optional header line to prepend in help output */
|
|
1187
|
+
[HELP_HEADER_SYMBOL];
|
|
1000
1188
|
constructor(alias = 'getdotenv') {
|
|
1001
1189
|
super(alias);
|
|
1002
1190
|
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
1003
1191
|
// Commander requires parent commands to enable positional options when a
|
|
1004
1192
|
// child uses passThroughOptions.
|
|
1005
1193
|
this.enablePositionalOptions();
|
|
1194
|
+
// Configure grouped help: show only base options in default "Options";
|
|
1195
|
+
// append App/Plugin sections after default help.
|
|
1196
|
+
this.configureHelp({
|
|
1197
|
+
visibleOptions: (cmd) => {
|
|
1198
|
+
const all = cmd.options ??
|
|
1199
|
+
[];
|
|
1200
|
+
const base = all.filter((opt) => {
|
|
1201
|
+
const group = opt.__group;
|
|
1202
|
+
return group === 'base';
|
|
1203
|
+
});
|
|
1204
|
+
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1205
|
+
const hasShort = (opt) => {
|
|
1206
|
+
const flags = opt.flags ?? '';
|
|
1207
|
+
// Matches "-x," or starting "-x " before any long
|
|
1208
|
+
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1209
|
+
};
|
|
1210
|
+
const byFlags = (opt) => opt.flags ?? '';
|
|
1211
|
+
base.sort((a, b) => {
|
|
1212
|
+
const aS = hasShort(a) ? 1 : 0;
|
|
1213
|
+
const bS = hasShort(b) ? 1 : 0;
|
|
1214
|
+
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1215
|
+
});
|
|
1216
|
+
return base;
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
this.addHelpText('beforeAll', () => {
|
|
1220
|
+
const header = this[HELP_HEADER_SYMBOL];
|
|
1221
|
+
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
1222
|
+
});
|
|
1223
|
+
this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
|
|
1006
1224
|
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
1007
|
-
// mutating process.env. The passOptions hook (when installed) will
|
|
1008
|
-
// compute the final context using merged CLI options; keeping
|
|
1225
|
+
// mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
|
|
1009
1226
|
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
1010
1227
|
// process env before subcommands execute.
|
|
1011
1228
|
this.hook('preSubcommand', async () => {
|
|
@@ -1037,11 +1254,96 @@ class GetDotenvCli extends Command {
|
|
|
1037
1254
|
getCtx() {
|
|
1038
1255
|
return this[CTX_SYMBOL];
|
|
1039
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Retrieve the merged root CLI options bag (if set by passOptions()).
|
|
1259
|
+
* Downstream-safe: no generics required.
|
|
1260
|
+
*/
|
|
1261
|
+
getOptions() {
|
|
1262
|
+
return this[OPTS_SYMBOL];
|
|
1263
|
+
}
|
|
1264
|
+
/** Internal: set the merged root options bag for this run. */
|
|
1265
|
+
_setOptionsBag(bag) {
|
|
1266
|
+
this[OPTS_SYMBOL] = bag;
|
|
1267
|
+
}
|
|
1040
1268
|
/** * Convenience helper to create a namespaced subcommand.
|
|
1041
1269
|
*/
|
|
1042
1270
|
ns(name) {
|
|
1043
1271
|
return this.command(name);
|
|
1044
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Tag options added during the provided callback as 'app' for grouped help.
|
|
1275
|
+
* Allows downstream apps to demarcate their root-level options.
|
|
1276
|
+
*/
|
|
1277
|
+
tagAppOptions(fn) {
|
|
1278
|
+
const root = this;
|
|
1279
|
+
const originalAddOption = root.addOption.bind(root);
|
|
1280
|
+
const originalOption = root.option.bind(root);
|
|
1281
|
+
const tagLatest = (cmd, group) => {
|
|
1282
|
+
const optsArr = cmd.options;
|
|
1283
|
+
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
1284
|
+
const last = optsArr[optsArr.length - 1];
|
|
1285
|
+
last.__group = group;
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
root.addOption = function patchedAdd(opt) {
|
|
1289
|
+
opt.__group = 'app';
|
|
1290
|
+
return originalAddOption(opt);
|
|
1291
|
+
};
|
|
1292
|
+
root.option = function patchedOption(...args) {
|
|
1293
|
+
const ret = originalOption(...args);
|
|
1294
|
+
tagLatest(this, 'app');
|
|
1295
|
+
return ret;
|
|
1296
|
+
};
|
|
1297
|
+
try {
|
|
1298
|
+
return fn(root);
|
|
1299
|
+
}
|
|
1300
|
+
finally {
|
|
1301
|
+
root.addOption = originalAddOption;
|
|
1302
|
+
root.option = originalOption;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Branding helper: set CLI name/description/version and optional help header.
|
|
1307
|
+
* If version is omitted and importMetaUrl is provided, attempts to read the
|
|
1308
|
+
* nearest package.json version (best-effort; non-fatal on failure).
|
|
1309
|
+
*/
|
|
1310
|
+
async brand(args) {
|
|
1311
|
+
const { name, description, version, importMetaUrl, helpHeader } = args;
|
|
1312
|
+
if (typeof name === 'string' && name.length > 0)
|
|
1313
|
+
this.name(name);
|
|
1314
|
+
if (typeof description === 'string')
|
|
1315
|
+
this.description(description);
|
|
1316
|
+
let v = version;
|
|
1317
|
+
if (!v && importMetaUrl) {
|
|
1318
|
+
try {
|
|
1319
|
+
const fromUrl = fileURLToPath(importMetaUrl);
|
|
1320
|
+
const pkgDir = await packageDirectory({ cwd: fromUrl });
|
|
1321
|
+
if (pkgDir) {
|
|
1322
|
+
const txt = await fs.readFile(`${pkgDir}/package.json`, 'utf-8');
|
|
1323
|
+
const pkg = JSON.parse(txt);
|
|
1324
|
+
if (pkg.version)
|
|
1325
|
+
v = pkg.version;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
// best-effort only
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (v)
|
|
1333
|
+
this.version(v);
|
|
1334
|
+
// Help header:
|
|
1335
|
+
// - If caller provides helpHeader, use it.
|
|
1336
|
+
// - Otherwise, when a version is known, default to "<name> v<version>".
|
|
1337
|
+
if (typeof helpHeader === 'string') {
|
|
1338
|
+
this[HELP_HEADER_SYMBOL] = helpHeader;
|
|
1339
|
+
}
|
|
1340
|
+
else if (v) {
|
|
1341
|
+
// Use the current command name (possibly overridden by 'name' above).
|
|
1342
|
+
const header = `${this.name()} v${v}`;
|
|
1343
|
+
this[HELP_HEADER_SYMBOL] = header;
|
|
1344
|
+
}
|
|
1345
|
+
return this;
|
|
1346
|
+
}
|
|
1045
1347
|
/**
|
|
1046
1348
|
* Register a plugin for installation (parent level).
|
|
1047
1349
|
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
@@ -1080,6 +1382,75 @@ class GetDotenvCli extends Command {
|
|
|
1080
1382
|
for (const p of this._plugins)
|
|
1081
1383
|
await run(p);
|
|
1082
1384
|
}
|
|
1385
|
+
// Render App/Plugin grouped options appended after default help.
|
|
1386
|
+
#renderOptionGroups(cmd) {
|
|
1387
|
+
const all = cmd.options ?? [];
|
|
1388
|
+
const byGroup = new Map();
|
|
1389
|
+
for (const o of all) {
|
|
1390
|
+
const opt = o;
|
|
1391
|
+
const g = opt.__group;
|
|
1392
|
+
if (!g || g === 'base')
|
|
1393
|
+
continue; // base handled by default help
|
|
1394
|
+
const rows = byGroup.get(g) ?? [];
|
|
1395
|
+
rows.push({
|
|
1396
|
+
flags: opt.flags ?? '',
|
|
1397
|
+
description: opt.description ?? '',
|
|
1398
|
+
});
|
|
1399
|
+
byGroup.set(g, rows);
|
|
1400
|
+
}
|
|
1401
|
+
if (byGroup.size === 0)
|
|
1402
|
+
return '';
|
|
1403
|
+
const renderRows = (title, rows) => {
|
|
1404
|
+
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
1405
|
+
// Sort within group: short-aliased flags first
|
|
1406
|
+
rows.sort((a, b) => {
|
|
1407
|
+
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
1408
|
+
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
1409
|
+
return bS - aS || a.flags.localeCompare(b.flags);
|
|
1410
|
+
});
|
|
1411
|
+
const lines = rows
|
|
1412
|
+
.map((r) => {
|
|
1413
|
+
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
1414
|
+
return ` ${r.flags}${pad}${r.description}`.trimEnd();
|
|
1415
|
+
})
|
|
1416
|
+
.join('\n');
|
|
1417
|
+
return `\n${title}:\n${lines}\n`;
|
|
1418
|
+
};
|
|
1419
|
+
let out = '';
|
|
1420
|
+
// App options (if any)
|
|
1421
|
+
const app = byGroup.get('app');
|
|
1422
|
+
if (app && app.length > 0) {
|
|
1423
|
+
out += renderRows('App options', app);
|
|
1424
|
+
}
|
|
1425
|
+
// Plugin groups sorted by id
|
|
1426
|
+
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
1427
|
+
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
1428
|
+
for (const k of pluginKeys) {
|
|
1429
|
+
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
1430
|
+
const rows = byGroup.get(k) ?? [];
|
|
1431
|
+
if (rows.length > 0) {
|
|
1432
|
+
out += renderRows(`Plugin options — ${id}`, rows);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return out;
|
|
1436
|
+
}
|
|
1083
1437
|
}
|
|
1084
1438
|
|
|
1085
|
-
|
|
1439
|
+
/**
|
|
1440
|
+
* Helper to retrieve the merged root options bag from any action handler
|
|
1441
|
+
* that only has access to thisCommand. Avoids structural casts.
|
|
1442
|
+
*/
|
|
1443
|
+
const readMergedOptions = (cmd) => {
|
|
1444
|
+
// Ascend to the root command
|
|
1445
|
+
let root = cmd;
|
|
1446
|
+
while (root.parent) {
|
|
1447
|
+
root = root.parent;
|
|
1448
|
+
}
|
|
1449
|
+
const hostAny = root;
|
|
1450
|
+
return typeof hostAny.getOptions === 'function'
|
|
1451
|
+
? hostAny.getOptions()
|
|
1452
|
+
: root
|
|
1453
|
+
.getDotenvCliOptions;
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
export { GetDotenvCli, definePlugin, readMergedOptions };
|
package/dist/config.cjs
CHANGED
|
@@ -8,11 +8,12 @@ var YAML = require('yaml');
|
|
|
8
8
|
var zod = require('zod');
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Zod schemas for configuration files discovered by the new loader.
|
|
11
|
+
* Zod schemas for configuration files discovered by the new loader.
|
|
12
|
+
*
|
|
12
13
|
* Notes:
|
|
13
14
|
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
14
15
|
* - RESOLVED: normalized shapes (paths always string[]).
|
|
15
|
-
* - For
|
|
16
|
+
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
16
17
|
*/
|
|
17
18
|
// String-only env value map
|
|
18
19
|
const stringMap = zod.z.record(zod.z.string(), zod.z.string());
|
|
@@ -27,6 +28,8 @@ const getDotenvConfigSchemaRaw = zod.z.object({
|
|
|
27
28
|
log: zod.z.boolean().optional(),
|
|
28
29
|
shell: zod.z.union([zod.z.string(), zod.z.boolean()]).optional(),
|
|
29
30
|
scripts: zod.z.record(zod.z.string(), zod.z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
31
|
+
requiredKeys: zod.z.array(zod.z.string()).optional(),
|
|
32
|
+
schema: zod.z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
30
33
|
vars: stringMap.optional(), // public, global
|
|
31
34
|
envVars: envStringMap.optional(), // public, per-env
|
|
32
35
|
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
@@ -209,9 +212,11 @@ const loadConfigFile = async (filePath) => {
|
|
|
209
212
|
.join('\n');
|
|
210
213
|
throw new Error(`Invalid config ${filePath}:\n${msgs}`);
|
|
211
214
|
}
|
|
212
|
-
// Disallow dynamic in JSON/YAML; allow in JS/TS
|
|
213
|
-
if (!isJsOrTs(filePath) &&
|
|
214
|
-
|
|
215
|
+
// Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
|
|
216
|
+
if (!isJsOrTs(filePath) &&
|
|
217
|
+
(parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
|
|
218
|
+
throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
|
|
219
|
+
`Use JS/TS config for "dynamic" or "schema".`);
|
|
215
220
|
}
|
|
216
221
|
return getDotenvConfigSchemaResolved.parse(parsed.data);
|
|
217
222
|
};
|
package/dist/config.d.cts
CHANGED
package/dist/config.d.mts
CHANGED
package/dist/config.d.ts
CHANGED
package/dist/config.mjs
CHANGED
|
@@ -6,11 +6,12 @@ import YAML from 'yaml';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Zod schemas for configuration files discovered by the new loader.
|
|
9
|
+
* Zod schemas for configuration files discovered by the new loader.
|
|
10
|
+
*
|
|
10
11
|
* Notes:
|
|
11
12
|
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
12
13
|
* - RESOLVED: normalized shapes (paths always string[]).
|
|
13
|
-
* - For
|
|
14
|
+
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
14
15
|
*/
|
|
15
16
|
// String-only env value map
|
|
16
17
|
const stringMap = z.record(z.string(), z.string());
|
|
@@ -25,6 +26,8 @@ const getDotenvConfigSchemaRaw = z.object({
|
|
|
25
26
|
log: z.boolean().optional(),
|
|
26
27
|
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
27
28
|
scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
29
|
+
requiredKeys: z.array(z.string()).optional(),
|
|
30
|
+
schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
28
31
|
vars: stringMap.optional(), // public, global
|
|
29
32
|
envVars: envStringMap.optional(), // public, per-env
|
|
30
33
|
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
@@ -207,9 +210,11 @@ const loadConfigFile = async (filePath) => {
|
|
|
207
210
|
.join('\n');
|
|
208
211
|
throw new Error(`Invalid config ${filePath}:\n${msgs}`);
|
|
209
212
|
}
|
|
210
|
-
// Disallow dynamic in JSON/YAML; allow in JS/TS
|
|
211
|
-
if (!isJsOrTs(filePath) &&
|
|
212
|
-
|
|
213
|
+
// Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
|
|
214
|
+
if (!isJsOrTs(filePath) &&
|
|
215
|
+
(parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
|
|
216
|
+
throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
|
|
217
|
+
`Use JS/TS config for "dynamic" or "schema".`);
|
|
213
218
|
}
|
|
214
219
|
return getDotenvConfigSchemaResolved.parse(parsed.data);
|
|
215
220
|
};
|
package/dist/env-overlay.d.cts
CHANGED
package/dist/env-overlay.d.mts
CHANGED
package/dist/env-overlay.d.ts
CHANGED