@karmaniverous/get-dotenv 5.2.6 → 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/dist/cliHost.cjs CHANGED
@@ -1,269 +1,17 @@
1
1
  'use strict';
2
2
 
3
- var commander = require('commander');
4
3
  var fs = require('fs-extra');
5
4
  var packageDirectory = require('package-directory');
6
5
  var path = require('path');
7
6
  var url = require('url');
8
7
  var YAML = require('yaml');
9
8
  var zod = require('zod');
9
+ var commander = require('commander');
10
10
  var nanoid = require('nanoid');
11
11
  var dotenv = require('dotenv');
12
12
  var crypto = require('crypto');
13
13
 
14
14
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
15
- /**
16
- * Dotenv expansion utilities.
17
- *
18
- * This module implements recursive expansion of environment-variable
19
- * references in strings and records. It supports both whitespace and
20
- * bracket syntaxes with optional defaults:
21
- *
22
- * - Whitespace: `$VAR[:default]`
23
- * - Bracketed: `${VAR[:default]}`
24
- *
25
- * Escaped dollar signs (`\$`) are preserved.
26
- * Unknown variables resolve to empty string unless a default is provided.
27
- */
28
- /**
29
- * Like String.prototype.search but returns the last index.
30
- * @internal
31
- */
32
- const searchLast = (str, rgx) => {
33
- const matches = Array.from(str.matchAll(rgx));
34
- return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
35
- };
36
- const replaceMatch = (value, match, ref) => {
37
- /**
38
- * @internal
39
- */
40
- const group = match[0];
41
- const key = match[1];
42
- const defaultValue = match[2];
43
- if (!key)
44
- return value;
45
- const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
46
- return interpolate(replacement, ref);
47
- };
48
- const interpolate = (value = '', ref = {}) => {
49
- /**
50
- * @internal
51
- */
52
- // if value is falsy, return it as is
53
- if (!value)
54
- return value;
55
- // get position of last unescaped dollar sign
56
- const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
57
- // return value if none found
58
- if (lastUnescapedDollarSignIndex === -1)
59
- return value;
60
- // evaluate the value tail
61
- const tail = value.slice(lastUnescapedDollarSignIndex);
62
- // find whitespace pattern: $KEY:DEFAULT
63
- const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
64
- const whitespaceMatch = whitespacePattern.exec(tail);
65
- if (whitespaceMatch != null)
66
- return replaceMatch(value, whitespaceMatch, ref);
67
- else {
68
- // find bracket pattern: ${KEY:DEFAULT}
69
- const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
70
- const bracketMatch = bracketPattern.exec(tail);
71
- if (bracketMatch != null)
72
- return replaceMatch(value, bracketMatch, ref);
73
- }
74
- return value;
75
- };
76
- /**
77
- * Recursively expands environment variables in a string. Variables may be
78
- * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
79
- * Unknown variables will expand to an empty string.
80
- *
81
- * @param value - The string to expand.
82
- * @param ref - The reference object to use for variable expansion.
83
- * @returns The expanded string.
84
- *
85
- * @example
86
- * ```ts
87
- * process.env.FOO = 'bar';
88
- * dotenvExpand('Hello $FOO'); // "Hello bar"
89
- * dotenvExpand('Hello $BAZ:world'); // "Hello world"
90
- * ```
91
- *
92
- * @remarks
93
- * The expansion is recursive. If a referenced variable itself contains
94
- * references, those will also be expanded until a stable value is reached.
95
- * Escaped references (e.g. `\$FOO`) are preserved as literals.
96
- */
97
- const dotenvExpand = (value, ref = process.env) => {
98
- const result = interpolate(value, ref);
99
- return result ? result.replace(/\\\$/g, '$') : undefined;
100
- };
101
- /**
102
- * Recursively expands environment variables in the values of a JSON object.
103
- * Variables may be presented with optional default as `$VAR[:default]` or
104
- * `${VAR[:default]}`. Unknown variables will expand to an empty string.
105
- *
106
- * @param values - The values object to expand.
107
- * @param options - Expansion options.
108
- * @returns The value object with expanded string values.
109
- *
110
- * @example
111
- * ```ts
112
- * process.env.FOO = 'bar';
113
- * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
114
- * // => { A: "bar", B: "xbary" }
115
- * ```
116
- *
117
- * @remarks
118
- * Options:
119
- * - ref: The reference object to use for expansion (defaults to process.env).
120
- * - progressive: Whether to progressively add expanded values to the set of
121
- * reference keys.
122
- *
123
- * When `progressive` is true, each expanded key becomes available for
124
- * subsequent expansions in the same object (left-to-right by object key order).
125
- */
126
- const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
127
- const { ref = process.env, progressive = false } = options;
128
- acc[key] = dotenvExpand(values[key], {
129
- ...ref,
130
- ...(progressive ? acc : {}),
131
- });
132
- return acc;
133
- }, {});
134
- /**
135
- * Recursively expands environment variables in a string using `process.env` as
136
- * the expansion reference. Variables may be presented with optional default as
137
- * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
138
- * empty string.
139
- *
140
- * @param value - The string to expand.
141
- * @returns The expanded string.
142
- *
143
- * @example
144
- * ```ts
145
- * process.env.FOO = 'bar';
146
- * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
147
- * ```
148
- */
149
- const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
150
-
151
- /**
152
- * Attach legacy root flags to a Commander program.
153
- * Uses provided defaults to render help labels without coupling to generators.
154
- */
155
- const attachRootOptions = (program, defaults, opts) => {
156
- // Install temporary wrappers to tag all options added here as "base".
157
- const GROUP = 'base';
158
- const tagLatest = (cmd, group) => {
159
- const optsArr = cmd.options;
160
- if (Array.isArray(optsArr) && optsArr.length > 0) {
161
- const last = optsArr[optsArr.length - 1];
162
- last.__group = group;
163
- }
164
- };
165
- const originalAddOption = program.addOption.bind(program);
166
- const originalOption = program.option.bind(program);
167
- program.addOption = function patchedAdd(opt) {
168
- // Tag before adding, in case consumers inspect the Option directly.
169
- opt.__group = GROUP;
170
- const ret = originalAddOption(opt);
171
- return ret;
172
- };
173
- program.option = function patchedOption(...args) {
174
- const ret = originalOption(...args);
175
- tagLatest(this, GROUP);
176
- return ret;
177
- };
178
- const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
179
- const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
180
- const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
181
- // Build initial chain.
182
- let p = program
183
- .enablePositionalOptions()
184
- .passThroughOptions()
185
- .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
186
- p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
187
- ['KEY1', 'VAL1'],
188
- ['KEY2', 'VAL2'],
189
- ]
190
- .map((v) => v.join(va))
191
- .join(vd)}`, dotenvExpandFromProcessEnv);
192
- // Optional legacy root command flag (kept for generated CLI compatibility).
193
- // Default is OFF; the generator opts in explicitly.
194
- if (opts?.includeCommandOption === true) {
195
- p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
196
- }
197
- p = p
198
- .option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
199
- .addOption(new commander.Option('-s, --shell [string]', (() => {
200
- let defaultLabel = '';
201
- if (shell !== undefined) {
202
- if (typeof shell === 'boolean') {
203
- defaultLabel = ' (default OS shell)';
204
- }
205
- else if (typeof shell === 'string') {
206
- // Safe string interpolation
207
- defaultLabel = ` (default ${shell})`;
208
- }
209
- }
210
- return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
211
- })()).conflicts('shellOff'))
212
- .addOption(new commander.Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
213
- .addOption(new commander.Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
214
- .addOption(new commander.Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
215
- .addOption(new commander.Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
216
- ((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
217
- ? ' (default)'
218
- : ''}`).conflicts('excludeAllOff'))
219
- .addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
220
- .addOption(new commander.Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
221
- .addOption(new commander.Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
222
- .addOption(new commander.Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
223
- .addOption(new commander.Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
224
- .addOption(new commander.Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
225
- .addOption(new commander.Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
226
- .addOption(new commander.Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
227
- .addOption(new commander.Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
228
- .addOption(new commander.Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
229
- .addOption(new commander.Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
230
- .addOption(new commander.Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
231
- .addOption(new commander.Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
232
- .option('--capture', 'capture child process stdio for commands (tests/CI)')
233
- .option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
234
- .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
235
- .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
236
- .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
237
- .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
238
- .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
239
- .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
240
- .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
241
- .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
242
- .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
243
- .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
244
- .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
245
- // Hidden scripts pipe-through (stringified)
246
- .addOption(new commander.Option('--scripts <string>')
247
- .default(JSON.stringify(scripts))
248
- .hideHelp());
249
- // Diagnostics: opt-in tracing; optional variadic keys after the flag.
250
- p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
251
- // Validation: strict mode fails on env validation issues (warn by default).
252
- p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
253
- // Entropy diagnostics (presentation-only)
254
- p = p
255
- .addOption(new commander.Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
256
- .addOption(new commander.Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
257
- .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
258
- .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
259
- .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
260
- .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
261
- // Restore original methods to avoid tagging future additions outside base.
262
- program.addOption = originalAddOption;
263
- program.option = originalOption;
264
- return p;
265
- };
266
-
267
15
  // Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
268
16
  const baseRootOptionDefaults = {
269
17
  dotenvToken: '.env',
@@ -833,9 +581,21 @@ const resolveGetDotenvOptions = async (customOptions) => {
833
581
  const localOptionsPath = localPkgDir
834
582
  ? path.join(localPkgDir, getDotenvOptionsFilename)
835
583
  : undefined;
836
- const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
837
- ? JSON.parse((await fs.readFile(localOptionsPath)).toString())
838
- : {});
584
+ // Safely read local CLI-facing defaults (defensive typing to satisfy strict linting).
585
+ let localOptions = {};
586
+ if (localOptionsPath && (await fs.exists(localOptionsPath))) {
587
+ try {
588
+ const txt = await fs.readFile(localOptionsPath, 'utf-8');
589
+ const parsed = JSON.parse(txt);
590
+ if (parsed && typeof parsed === 'object') {
591
+ localOptions = parsed;
592
+ }
593
+ }
594
+ catch {
595
+ // Malformed or unreadable local options are treated as absent.
596
+ localOptions = {};
597
+ }
598
+ }
839
599
  // Merge order: base < local < custom (custom has highest precedence)
840
600
  const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
841
601
  const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
@@ -846,6 +606,316 @@ const resolveGetDotenvOptions = async (customOptions) => {
846
606
  };
847
607
  };
848
608
 
609
+ /**
610
+ * Dotenv expansion utilities.
611
+ *
612
+ * This module implements recursive expansion of environment-variable
613
+ * references in strings and records. It supports both whitespace and
614
+ * bracket syntaxes with optional defaults:
615
+ *
616
+ * - Whitespace: `$VAR[:default]`
617
+ * - Bracketed: `${VAR[:default]}`
618
+ *
619
+ * Escaped dollar signs (`\$`) are preserved.
620
+ * Unknown variables resolve to empty string unless a default is provided.
621
+ */
622
+ /**
623
+ * Like String.prototype.search but returns the last index.
624
+ * @internal
625
+ */
626
+ const searchLast = (str, rgx) => {
627
+ const matches = Array.from(str.matchAll(rgx));
628
+ return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
629
+ };
630
+ const replaceMatch = (value, match, ref) => {
631
+ /**
632
+ * @internal
633
+ */
634
+ const group = match[0];
635
+ const key = match[1];
636
+ const defaultValue = match[2];
637
+ if (!key)
638
+ return value;
639
+ const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
640
+ return interpolate(replacement, ref);
641
+ };
642
+ const interpolate = (value = '', ref = {}) => {
643
+ /**
644
+ * @internal
645
+ */
646
+ // if value is falsy, return it as is
647
+ if (!value)
648
+ return value;
649
+ // get position of last unescaped dollar sign
650
+ const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
651
+ // return value if none found
652
+ if (lastUnescapedDollarSignIndex === -1)
653
+ return value;
654
+ // evaluate the value tail
655
+ const tail = value.slice(lastUnescapedDollarSignIndex);
656
+ // find whitespace pattern: $KEY:DEFAULT
657
+ const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
658
+ const whitespaceMatch = whitespacePattern.exec(tail);
659
+ if (whitespaceMatch != null)
660
+ return replaceMatch(value, whitespaceMatch, ref);
661
+ else {
662
+ // find bracket pattern: ${KEY:DEFAULT}
663
+ const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
664
+ const bracketMatch = bracketPattern.exec(tail);
665
+ if (bracketMatch != null)
666
+ return replaceMatch(value, bracketMatch, ref);
667
+ }
668
+ return value;
669
+ };
670
+ /**
671
+ * Recursively expands environment variables in a string. Variables may be
672
+ * presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
673
+ * Unknown variables will expand to an empty string.
674
+ *
675
+ * @param value - The string to expand.
676
+ * @param ref - The reference object to use for variable expansion.
677
+ * @returns The expanded string.
678
+ *
679
+ * @example
680
+ * ```ts
681
+ * process.env.FOO = 'bar';
682
+ * dotenvExpand('Hello $FOO'); // "Hello bar"
683
+ * dotenvExpand('Hello $BAZ:world'); // "Hello world"
684
+ * ```
685
+ *
686
+ * @remarks
687
+ * The expansion is recursive. If a referenced variable itself contains
688
+ * references, those will also be expanded until a stable value is reached.
689
+ * Escaped references (e.g. `\$FOO`) are preserved as literals.
690
+ */
691
+ const dotenvExpand = (value, ref = process.env) => {
692
+ const result = interpolate(value, ref);
693
+ return result ? result.replace(/\\\$/g, '$') : undefined;
694
+ };
695
+ /**
696
+ * Recursively expands environment variables in the values of a JSON object.
697
+ * Variables may be presented with optional default as `$VAR[:default]` or
698
+ * `${VAR[:default]}`. Unknown variables will expand to an empty string.
699
+ *
700
+ * @param values - The values object to expand.
701
+ * @param options - Expansion options.
702
+ * @returns The value object with expanded string values.
703
+ *
704
+ * @example
705
+ * ```ts
706
+ * process.env.FOO = 'bar';
707
+ * dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
708
+ * // => { A: "bar", B: "xbary" }
709
+ * ```
710
+ *
711
+ * @remarks
712
+ * Options:
713
+ * - ref: The reference object to use for expansion (defaults to process.env).
714
+ * - progressive: Whether to progressively add expanded values to the set of
715
+ * reference keys.
716
+ *
717
+ * When `progressive` is true, each expanded key becomes available for
718
+ * subsequent expansions in the same object (left-to-right by object key order).
719
+ */
720
+ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
721
+ const { ref = process.env, progressive = false } = options;
722
+ acc[key] = dotenvExpand(values[key], {
723
+ ...ref,
724
+ ...(progressive ? acc : {}),
725
+ });
726
+ return acc;
727
+ }, {});
728
+ /**
729
+ * Recursively expands environment variables in a string using `process.env` as
730
+ * the expansion reference. Variables may be presented with optional default as
731
+ * `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
732
+ * empty string.
733
+ *
734
+ * @param value - The string to expand.
735
+ * @returns The expanded string.
736
+ *
737
+ * @example
738
+ * ```ts
739
+ * process.env.FOO = 'bar';
740
+ * dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
741
+ * ```
742
+ */
743
+ const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
744
+
745
+ /* eslint-disable @typescript-eslint/no-deprecated */
746
+ /**
747
+ * Attach root flags to a GetDotenvCli instance.
748
+ * - Host-only: program is typed as GetDotenvCli and supports dynamicOption/createDynamicOption.
749
+ * - Any flag that displays an effective default in help uses dynamic descriptions.
750
+ */
751
+ const attachRootOptions = (program, defaults, opts) => {
752
+ // Install temporary wrappers to tag all options added here as "base" for grouped help.
753
+ const GROUP = 'base';
754
+ const tagLatest = (cmd, group) => {
755
+ const optsArr = cmd.options;
756
+ if (Array.isArray(optsArr) && optsArr.length > 0) {
757
+ const last = optsArr[optsArr.length - 1];
758
+ last.__group = group;
759
+ }
760
+ };
761
+ const originalAddOption = program.addOption.bind(program);
762
+ const originalOption = program.option.bind(program);
763
+ program.addOption = function patchedAdd(opt) {
764
+ opt.__group = GROUP;
765
+ return originalAddOption(opt);
766
+ };
767
+ program.option = function patchedOption(...args) {
768
+ const ret = originalOption(...args);
769
+ tagLatest(this, GROUP);
770
+ return ret;
771
+ };
772
+ const { defaultEnv, dotenvToken, dynamicPath, env, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
773
+ const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
774
+ const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
775
+ // Helper: append (default) tags for ON/OFF toggles
776
+ const onOff = (on, isDefault) => on
777
+ ? `ON${isDefault ? ' (default)' : ''}`
778
+ : `OFF${isDefault ? ' (default)' : ''}`;
779
+ let p = program
780
+ .enablePositionalOptions()
781
+ .passThroughOptions()
782
+ .option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
783
+ p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
784
+ ['KEY1', 'VAL1'],
785
+ ['KEY2', 'VAL2'],
786
+ ]
787
+ .map((v) => v.join(va))
788
+ .join(vd)}`, dotenvExpandFromProcessEnv);
789
+ if (opts?.includeCommandOption === true) {
790
+ p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
791
+ }
792
+ // Output path (interpolated later; help can remain static)
793
+ p = p.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath);
794
+ // Shell ON (string or boolean true => default shell)
795
+ p = p
796
+ .addOption(program
797
+ .createDynamicOption('-s, --shell [string]', (cfg) => {
798
+ const s = cfg.shell;
799
+ let tag = '';
800
+ if (typeof s === 'boolean' && s)
801
+ tag = ' (default OS shell)';
802
+ else if (typeof s === 'string' && s.length > 0)
803
+ tag = ` (default ${s})`;
804
+ return `command execution shell, no argument for default OS shell or provide shell string${tag}`;
805
+ })
806
+ .conflicts('shellOff'))
807
+ // Shell OFF
808
+ .addOption(program
809
+ .createDynamicOption('-S, --shell-off', (cfg) => {
810
+ const s = cfg.shell;
811
+ return `command execution shell OFF${s === false ? ' (default)' : ''}`;
812
+ })
813
+ .conflicts('shell'));
814
+ // Load process ON/OFF (dynamic defaults)
815
+ p = p
816
+ .addOption(program
817
+ .createDynamicOption('-p, --load-process', (cfg) => `load variables to process.env ${onOff(true, Boolean(cfg.loadProcess))}`)
818
+ .conflicts('loadProcessOff'))
819
+ .addOption(program
820
+ .createDynamicOption('-P, --load-process-off', (cfg) => `load variables to process.env ${onOff(false, !cfg.loadProcess)}`)
821
+ .conflicts('loadProcess'));
822
+ // Exclusion master toggle (dynamic)
823
+ p = p
824
+ .addOption(program
825
+ .createDynamicOption('-a, --exclude-all', (cfg) => {
826
+ const c = cfg;
827
+ const allOn = !!c.excludeDynamic &&
828
+ ((!!c.excludeEnv && !!c.excludeGlobal) ||
829
+ (!!c.excludePrivate && !!c.excludePublic));
830
+ const suffix = allOn ? ' (default)' : '';
831
+ return `exclude all dotenv variables from loading ON${suffix}`;
832
+ })
833
+ .conflicts('excludeAllOff'))
834
+ .addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'));
835
+ // Per-family exclusions (dynamic defaults)
836
+ p = p
837
+ .addOption(program
838
+ .createDynamicOption('-z, --exclude-dynamic', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(true, Boolean(cfg.excludeDynamic))}`)
839
+ .conflicts('excludeDynamicOff'))
840
+ .addOption(program
841
+ .createDynamicOption('-Z, --exclude-dynamic-off', (cfg) => `exclude dynamic dotenv variables from loading ${onOff(false, !cfg.excludeDynamic)}`)
842
+ .conflicts('excludeDynamic'))
843
+ .addOption(program
844
+ .createDynamicOption('-n, --exclude-env', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(true, Boolean(cfg.excludeEnv))}`)
845
+ .conflicts('excludeEnvOff'))
846
+ .addOption(program
847
+ .createDynamicOption('-N, --exclude-env-off', (cfg) => `exclude environment-specific dotenv variables from loading ${onOff(false, !cfg.excludeEnv)}`)
848
+ .conflicts('excludeEnv'))
849
+ .addOption(program
850
+ .createDynamicOption('-g, --exclude-global', (cfg) => `exclude global dotenv variables from loading ${onOff(true, Boolean(cfg.excludeGlobal))}`)
851
+ .conflicts('excludeGlobalOff'))
852
+ .addOption(program
853
+ .createDynamicOption('-G, --exclude-global-off', (cfg) => `exclude global dotenv variables from loading ${onOff(false, !cfg.excludeGlobal)}`)
854
+ .conflicts('excludeGlobal'))
855
+ .addOption(program
856
+ .createDynamicOption('-r, --exclude-private', (cfg) => `exclude private dotenv variables from loading ${onOff(true, Boolean(cfg.excludePrivate))}`)
857
+ .conflicts('excludePrivateOff'))
858
+ .addOption(program
859
+ .createDynamicOption('-R, --exclude-private-off', (cfg) => `exclude private dotenv variables from loading ${onOff(false, !cfg.excludePrivate)}`)
860
+ .conflicts('excludePrivate'))
861
+ .addOption(program
862
+ .createDynamicOption('-u, --exclude-public', (cfg) => `exclude public dotenv variables from loading ${onOff(true, Boolean(cfg.excludePublic))}`)
863
+ .conflicts('excludePublicOff'))
864
+ .addOption(program
865
+ .createDynamicOption('-U, --exclude-public-off', (cfg) => `exclude public dotenv variables from loading ${onOff(false, !cfg.excludePublic)}`)
866
+ .conflicts('excludePublic'));
867
+ // Log ON/OFF (dynamic)
868
+ p = p
869
+ .addOption(program
870
+ .createDynamicOption('-l, --log', (cfg) => `console log loaded variables ${onOff(true, Boolean(cfg.log))}`)
871
+ .conflicts('logOff'))
872
+ .addOption(program
873
+ .createDynamicOption('-L, --log-off', (cfg) => `console log loaded variables ${onOff(false, !cfg.log)}`)
874
+ .conflicts('log'));
875
+ // Capture flag (no default display; static)
876
+ p = p.option('--capture', 'capture child process stdio for commands (tests/CI)');
877
+ // Core bootstrap/static flags (kept static in help)
878
+ p = p
879
+ .option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
880
+ .option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
881
+ .option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
882
+ .option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
883
+ .option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
884
+ .option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
885
+ .option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
886
+ .option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
887
+ .option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
888
+ .option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
889
+ .option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
890
+ // Hidden scripts pipe-through (stringified)
891
+ .addOption(new commander.Option('--scripts <string>')
892
+ .default(JSON.stringify(scripts))
893
+ .hideHelp());
894
+ // Diagnostics / validation / entropy
895
+ p = p
896
+ .option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)')
897
+ .option('--strict', 'fail on env validation errors (schema/requiredKeys)');
898
+ p = p
899
+ .addOption(program
900
+ .createDynamicOption('--entropy-warn', (cfg) => {
901
+ const warn = cfg.warnEntropy;
902
+ // Default is effectively ON when warnEntropy is true or undefined.
903
+ return `enable entropy warnings${warn === false ? '' : ' (default on)'}`;
904
+ })
905
+ .conflicts('entropyWarnOff'))
906
+ .addOption(program
907
+ .createDynamicOption('--entropy-warn-off', (cfg) => `disable entropy warnings${cfg.warnEntropy === false ? ' (default)' : ''}`)
908
+ .conflicts('entropyWarn'))
909
+ .option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
910
+ .option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
911
+ .option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
912
+ .option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
913
+ // Restore original methods
914
+ program.addOption = originalAddOption;
915
+ program.option = originalOption;
916
+ return p;
917
+ };
918
+
849
919
  /**
850
920
  * Zod schemas for programmatic GetDotenv options.
851
921
  *
@@ -1458,6 +1528,8 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
1458
1528
  };
1459
1529
  };
1460
1530
 
1531
+ // Dynamic help support: attach a private symbol to Option for description fns.
1532
+ const DYN_DESC_SYM = Symbol('getdotenv.dynamic.description');
1461
1533
  const HOST_META_URL = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cliHost.cjs', document.baseURI).href));
1462
1534
  const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
1463
1535
  const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
@@ -1480,6 +1552,13 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1480
1552
  _installed = false;
1481
1553
  /** Optional header line to prepend in help output */
1482
1554
  [HELP_HEADER_SYMBOL];
1555
+ /**
1556
+ * Create a subcommand using the same subclass, preserving helpers like
1557
+ * dynamicOption on children.
1558
+ */
1559
+ createCommand(name) {
1560
+ return new this.constructor(name);
1561
+ }
1483
1562
  constructor(alias = 'getdotenv') {
1484
1563
  super(alias);
1485
1564
  // Ensure subcommands that use passThroughOptions can be attached safely.
@@ -1487,15 +1566,18 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1487
1566
  // child uses passThroughOptions.
1488
1567
  this.enablePositionalOptions();
1489
1568
  // Configure grouped help: show only base options in default "Options";
1490
- // append App/Plugin sections after default help.
1569
+ // we will insert App/Plugin sections before Commands in helpInformation().
1491
1570
  this.configureHelp({
1492
1571
  visibleOptions: (cmd) => {
1493
- const all = cmd.options ??
1494
- [];
1495
- const base = all.filter((opt) => {
1496
- const group = opt.__group;
1497
- return group === 'base';
1498
- });
1572
+ const all = cmd.options ?? [];
1573
+ const parent = cmd.parent ?? null;
1574
+ const isRoot = parent === null;
1575
+ const list = isRoot
1576
+ ? all.filter((opt) => {
1577
+ const group = opt.__group;
1578
+ return group === 'base';
1579
+ })
1580
+ : all.slice(); // subcommands: show all options (their own "Options:" block)
1499
1581
  // Sort: short-aliased options first, then long-only; stable by flags.
1500
1582
  const hasShort = (opt) => {
1501
1583
  const flags = opt.flags ?? '';
@@ -1503,19 +1585,18 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1503
1585
  return /(^|\s|,)-[A-Za-z]/.test(flags);
1504
1586
  };
1505
1587
  const byFlags = (opt) => opt.flags ?? '';
1506
- base.sort((a, b) => {
1588
+ list.sort((a, b) => {
1507
1589
  const aS = hasShort(a) ? 1 : 0;
1508
1590
  const bS = hasShort(b) ? 1 : 0;
1509
1591
  return bS - aS || byFlags(a).localeCompare(byFlags(b));
1510
1592
  });
1511
- return base;
1593
+ return list;
1512
1594
  },
1513
1595
  });
1514
1596
  this.addHelpText('beforeAll', () => {
1515
1597
  const header = this[HELP_HEADER_SYMBOL];
1516
1598
  return header && header.length > 0 ? `${header}\n\n` : '';
1517
1599
  });
1518
- this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
1519
1600
  // Skeleton preSubcommand hook: produce a context if absent, without
1520
1601
  // mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
1521
1602
  // loadProcess=false here avoids leaking dotenv values into the parent
@@ -1527,9 +1608,15 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1527
1608
  });
1528
1609
  }
1529
1610
  /**
1530
- * Resolve options (strict) and compute dotenv context. * Stores the context on the instance under a symbol.
1611
+ * Resolve options (strict) and compute dotenv context.
1612
+ * Stores the context on the instance under a symbol.
1613
+ *
1614
+ * Options:
1615
+ * - opts.runAfterResolve (default true): when false, skips running plugin
1616
+ * afterResolve hooks. Useful for top-level help rendering to avoid
1617
+ * long-running side-effects while still evaluating dynamic help text.
1531
1618
  */
1532
- async resolveAndLoad(customOptions = {}) {
1619
+ async resolveAndLoad(customOptions = {}, opts) {
1533
1620
  // Resolve defaults, then validate strictly under the new host.
1534
1621
  const optionsResolved = await resolveGetDotenvOptions(customOptions);
1535
1622
  getDotenvOptionsSchemaResolved.parse(optionsResolved);
@@ -1540,9 +1627,64 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1540
1627
  ctx;
1541
1628
  // Ensure plugins are installed exactly once, then run afterResolve.
1542
1629
  await this.install();
1543
- await this._runAfterResolve(ctx);
1630
+ if (opts?.runAfterResolve ?? true) {
1631
+ await this._runAfterResolve(ctx);
1632
+ }
1544
1633
  return ctx;
1545
1634
  }
1635
+ /**
1636
+ * Create a Commander Option that computes its description at help time.
1637
+ * The returned Option may be configured (conflicts, default, parser) and
1638
+ * added via addOption().
1639
+ */
1640
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
1641
+ createDynamicOption(flags, desc, parser, defaultValue) {
1642
+ const opt = new commander.Option(flags, '');
1643
+ // Keep the function on a private symbol so it survives through Commander.
1644
+ opt[DYN_DESC_SYM] = desc;
1645
+ if (parser)
1646
+ opt.argParser(parser);
1647
+ if (defaultValue !== undefined)
1648
+ opt.default(defaultValue);
1649
+ return opt;
1650
+ }
1651
+ /**
1652
+ * Chainable helper mirroring .option(), but with a dynamic description.
1653
+ * Equivalent to addOption(createDynamicOption(...)).
1654
+ */
1655
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
1656
+ dynamicOption(flags, desc, parser, defaultValue) {
1657
+ const opt = this.createDynamicOption(flags, desc, parser, defaultValue);
1658
+ this.addOption(opt);
1659
+ return this;
1660
+ }
1661
+ /**
1662
+ * Evaluate dynamic descriptions for this command and all descendants using
1663
+ * the provided resolved configuration. Mutates the Option.description in
1664
+ * place so Commander help renders updated text.
1665
+ */
1666
+ evaluateDynamicOptions(resolved) {
1667
+ const visit = (cmd) => {
1668
+ const arr = cmd.options ?? [];
1669
+ for (const o of arr) {
1670
+ const dyn = o[DYN_DESC_SYM];
1671
+ if (typeof dyn === 'function') {
1672
+ try {
1673
+ const txt = dyn(resolved);
1674
+ // Commander Option has a public "description" field used by help.
1675
+ o.description = txt;
1676
+ }
1677
+ catch {
1678
+ // Best-effort: leave description as-is on evaluation failure.
1679
+ }
1680
+ }
1681
+ }
1682
+ const children = cmd.commands ?? [];
1683
+ for (const c of children)
1684
+ visit(c);
1685
+ };
1686
+ visit(this);
1687
+ }
1546
1688
  /**
1547
1689
  * Retrieve the current invocation context (if any).
1548
1690
  */
@@ -1572,6 +1714,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1572
1714
  tagAppOptions(fn) {
1573
1715
  const root = this;
1574
1716
  const originalAddOption = root.addOption.bind(root);
1717
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1575
1718
  const originalOption = root.option.bind(root);
1576
1719
  const tagLatest = (cmd, group) => {
1577
1720
  const optsArr = cmd.options;
@@ -1584,6 +1727,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1584
1727
  opt.__group = 'app';
1585
1728
  return originalAddOption(opt);
1586
1729
  };
1730
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1587
1731
  root.option = function patchedOption(...args) {
1588
1732
  const ret = originalOption(...args);
1589
1733
  tagLatest(this, 'app');
@@ -1594,6 +1738,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1594
1738
  }
1595
1739
  finally {
1596
1740
  root.addOption = originalAddOption;
1741
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
1597
1742
  root.option = originalOption;
1598
1743
  }
1599
1744
  }
@@ -1639,6 +1784,40 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1639
1784
  }
1640
1785
  return this;
1641
1786
  }
1787
+ /**
1788
+ * Insert grouped plugin/app options between "Options" and "Commands" for
1789
+ * hybrid ordering. Applies to root and any parent command.
1790
+ */
1791
+ helpInformation() {
1792
+ // Base help text first (includes beforeAll/after hooks).
1793
+ const base = super.helpInformation();
1794
+ const groups = this.#renderOptionGroups(this);
1795
+ const block = typeof groups === 'string' ? groups.trim() : '';
1796
+ let out = base;
1797
+ if (!block) {
1798
+ // Ensure a trailing blank line even when no extra groups render.
1799
+ if (!out.endsWith('\n\n'))
1800
+ out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1801
+ return out;
1802
+ }
1803
+ // Insert just before "Commands:" when present.
1804
+ const marker = '\nCommands:';
1805
+ const idx = base.indexOf(marker);
1806
+ if (idx >= 0) {
1807
+ const toInsert = groups.startsWith('\n') ? groups : `\n${groups}`;
1808
+ out = `${base.slice(0, idx)}${toInsert}${base.slice(idx)}`;
1809
+ }
1810
+ else {
1811
+ // Otherwise append.
1812
+ const sep = base.endsWith('\n') || groups.startsWith('\n') ? '' : '\n';
1813
+ out = `${base}${sep}${groups}`;
1814
+ }
1815
+ // Ensure a trailing blank line for prompt separation.
1816
+ if (!out.endsWith('\n\n')) {
1817
+ out = out.endsWith('\n') ? `${out}\n` : `${out}\n\n`;
1818
+ }
1819
+ return out;
1820
+ }
1642
1821
  /**
1643
1822
  * Register a plugin for installation (parent level).
1644
1823
  * Installation occurs on first resolveAndLoad() (or explicit install()).
@@ -1677,7 +1856,7 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1677
1856
  for (const p of this._plugins)
1678
1857
  await run(p);
1679
1858
  }
1680
- // Render App/Plugin grouped options appended after default help.
1859
+ // Render App/Plugin grouped options (used by helpInformation override).
1681
1860
  #renderOptionGroups(cmd) {
1682
1861
  const all = cmd.options ?? [];
1683
1862
  const byGroup = new Map();
@@ -1719,11 +1898,14 @@ let GetDotenvCli$1 = class GetDotenvCli extends commander.Command {
1719
1898
  }
1720
1899
  // Plugin groups sorted by id
1721
1900
  const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
1901
+ const currentName = cmd.name?.() ?? '';
1722
1902
  pluginKeys.sort((a, b) => a.localeCompare(b));
1723
1903
  for (const k of pluginKeys) {
1724
1904
  const id = k.slice('plugin:'.length) || '(unknown)';
1725
1905
  const rows = byGroup.get(k) ?? [];
1726
- if (rows.length > 0) {
1906
+ // Do not show a "Plugin options — <self>" section on the command that owns those options.
1907
+ // Only child-injected plugin groups should render at this level.
1908
+ if (rows.length > 0 && id !== currentName) {
1727
1909
  out += renderRows(`Plugin options — ${id}`, rows);
1728
1910
  }
1729
1911
  }
@@ -1794,6 +1976,17 @@ class GetDotenvCli extends GetDotenvCli$1 {
1794
1976
  // Build service options and compute context (always-on loader path).
1795
1977
  const serviceOptions = getDotenvCliOptions2Options(merged);
1796
1978
  await this.resolveAndLoad(serviceOptions);
1979
+ // Refresh dynamic option descriptions using resolved config + plugin slices
1980
+ try {
1981
+ const ctx = this.getCtx();
1982
+ this.evaluateDynamicOptions({
1983
+ ...ctx?.optionsResolved,
1984
+ plugins: ctx?.pluginConfigs ?? {},
1985
+ });
1986
+ }
1987
+ catch {
1988
+ /* best-effort */
1989
+ }
1797
1990
  // Global validation: once after Phase C using config sources.
1798
1991
  try {
1799
1992
  const ctx = this.getCtx();
@@ -1828,6 +2021,16 @@ class GetDotenvCli extends GetDotenvCli$1 {
1828
2021
  if (!this.getCtx()) {
1829
2022
  const serviceOptions = getDotenvCliOptions2Options(merged);
1830
2023
  await this.resolveAndLoad(serviceOptions);
2024
+ try {
2025
+ const ctx = this.getCtx();
2026
+ this.evaluateDynamicOptions({
2027
+ ...ctx?.optionsResolved,
2028
+ plugins: ctx?.pluginConfigs ?? {},
2029
+ });
2030
+ }
2031
+ catch {
2032
+ /* tolerate */
2033
+ }
1831
2034
  try {
1832
2035
  const ctx = this.getCtx();
1833
2036
  const dotenv = (ctx?.dotenv ?? {});