@karmaniverous/get-dotenv 5.2.6 → 6.0.0-1

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.
Files changed (55) hide show
  1. package/README.md +106 -70
  2. package/dist/cliHost.d.ts +232 -226
  3. package/dist/cliHost.mjs +777 -545
  4. package/dist/config.d.ts +7 -2
  5. package/dist/env-overlay.d.ts +21 -9
  6. package/dist/env-overlay.mjs +14 -19
  7. package/dist/getdotenv.cli.mjs +1366 -1163
  8. package/dist/index.d.ts +415 -242
  9. package/dist/index.mjs +1364 -1414
  10. package/dist/plugins-aws.d.ts +149 -94
  11. package/dist/plugins-aws.mjs +307 -195
  12. package/dist/plugins-batch.d.ts +153 -99
  13. package/dist/plugins-batch.mjs +277 -95
  14. package/dist/plugins-cmd.d.ts +140 -94
  15. package/dist/plugins-cmd.mjs +636 -502
  16. package/dist/plugins-demo.d.ts +140 -94
  17. package/dist/plugins-demo.mjs +237 -46
  18. package/dist/plugins-init.d.ts +140 -94
  19. package/dist/plugins-init.mjs +129 -12
  20. package/dist/plugins.d.ts +166 -103
  21. package/dist/plugins.mjs +977 -840
  22. package/package.json +15 -53
  23. package/templates/cli/ts/plugins/hello.ts +27 -6
  24. package/templates/config/js/getdotenv.config.js +1 -1
  25. package/templates/config/ts/getdotenv.config.ts +9 -2
  26. package/dist/cliHost.cjs +0 -1875
  27. package/dist/cliHost.d.cts +0 -409
  28. package/dist/cliHost.d.mts +0 -409
  29. package/dist/config.cjs +0 -252
  30. package/dist/config.d.cts +0 -55
  31. package/dist/config.d.mts +0 -55
  32. package/dist/env-overlay.cjs +0 -163
  33. package/dist/env-overlay.d.cts +0 -50
  34. package/dist/env-overlay.d.mts +0 -50
  35. package/dist/index.cjs +0 -4140
  36. package/dist/index.d.cts +0 -457
  37. package/dist/index.d.mts +0 -457
  38. package/dist/plugins-aws.cjs +0 -667
  39. package/dist/plugins-aws.d.cts +0 -158
  40. package/dist/plugins-aws.d.mts +0 -158
  41. package/dist/plugins-batch.cjs +0 -616
  42. package/dist/plugins-batch.d.cts +0 -180
  43. package/dist/plugins-batch.d.mts +0 -180
  44. package/dist/plugins-cmd.cjs +0 -1113
  45. package/dist/plugins-cmd.d.cts +0 -178
  46. package/dist/plugins-cmd.d.mts +0 -178
  47. package/dist/plugins-demo.cjs +0 -307
  48. package/dist/plugins-demo.d.cts +0 -158
  49. package/dist/plugins-demo.d.mts +0 -158
  50. package/dist/plugins-init.cjs +0 -289
  51. package/dist/plugins-init.d.cts +0 -162
  52. package/dist/plugins-init.d.mts +0 -162
  53. package/dist/plugins.cjs +0 -2283
  54. package/dist/plugins.d.cts +0 -210
  55. package/dist/plugins.d.mts +0 -210
@@ -1,9 +1,92 @@
1
1
  import { Command } from 'commander';
2
- import { globby } from 'globby';
3
- import { packageDirectory } from 'package-directory';
2
+ import { z } from 'zod';
3
+ import 'fs-extra';
4
4
  import path from 'path';
5
+ import { packageDirectory } from 'package-directory';
6
+ import 'url';
7
+ import 'yaml';
8
+ import 'nanoid';
9
+ import 'dotenv';
10
+ import 'crypto';
11
+ import { globby } from 'globby';
5
12
  import { execa, execaCommand } from 'execa';
6
- import { z } from 'zod';
13
+
14
+ /**
15
+ * Zod schemas for configuration files discovered by the new loader.
16
+ *
17
+ * Notes:
18
+ * - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
19
+ * - RESOLVED: normalized shapes (paths always string[]).
20
+ * - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
21
+ */
22
+ // String-only env value map
23
+ const stringMap = z.record(z.string(), z.string());
24
+ const envStringMap = z.record(z.string(), stringMap);
25
+ // Allow string[] or single string for "paths" in RAW; normalize later.
26
+ const rawPathsSchema = z.union([z.array(z.string()), z.string()]).optional();
27
+ const getDotenvConfigSchemaRaw = z.object({
28
+ dotenvToken: z.string().optional(),
29
+ privateToken: z.string().optional(),
30
+ paths: rawPathsSchema,
31
+ loadProcess: z.boolean().optional(),
32
+ log: z.boolean().optional(),
33
+ shell: z.union([z.string(), z.boolean()]).optional(),
34
+ scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
35
+ requiredKeys: z.array(z.string()).optional(),
36
+ schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
37
+ vars: stringMap.optional(), // public, global
38
+ envVars: envStringMap.optional(), // public, per-env
39
+ // Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
40
+ dynamic: z.unknown().optional(),
41
+ // Per-plugin config bag; validated by plugins/host when used.
42
+ plugins: z.record(z.string(), z.unknown()).optional(),
43
+ });
44
+ // Normalize paths to string[]
45
+ const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
46
+ getDotenvConfigSchemaRaw.transform((raw) => ({
47
+ ...raw,
48
+ paths: normalizePaths(raw.paths),
49
+ }));
50
+
51
+ /**
52
+ * Zod schemas for programmatic GetDotenv options.
53
+ *
54
+ * Canonical source of truth for options shape. Public types are derived
55
+ * from these schemas (see consumers via z.output\<\>).
56
+ */
57
+ // Minimal process env representation: string values or undefined to indicate "unset".
58
+ const processEnvSchema = z.record(z.string(), z.string().optional());
59
+ // RAW: all fields optional — undefined means "inherit" from lower layers.
60
+ z.object({
61
+ defaultEnv: z.string().optional(),
62
+ dotenvToken: z.string().optional(),
63
+ dynamicPath: z.string().optional(),
64
+ // Dynamic map is intentionally wide for now; refine once sources are normalized.
65
+ dynamic: z.record(z.string(), z.unknown()).optional(),
66
+ env: z.string().optional(),
67
+ excludeDynamic: z.boolean().optional(),
68
+ excludeEnv: z.boolean().optional(),
69
+ excludeGlobal: z.boolean().optional(),
70
+ excludePrivate: z.boolean().optional(),
71
+ excludePublic: z.boolean().optional(),
72
+ loadProcess: z.boolean().optional(),
73
+ log: z.boolean().optional(),
74
+ logger: z.unknown().optional(),
75
+ outputPath: z.string().optional(),
76
+ paths: z.array(z.string()).optional(),
77
+ privateToken: z.string().optional(),
78
+ vars: processEnvSchema.optional(),
79
+ });
80
+
81
+ /**
82
+ * Instance-bound plugin config store.
83
+ * Host stores the validated/interpolated slice per plugin instance.
84
+ * The store is intentionally private to this module; definePlugin()
85
+ * provides a typed accessor that reads from this store for the calling
86
+ * plugin instance.
87
+ */
88
+ const PLUGIN_CONFIG_STORE = new WeakMap();
89
+ const _getPluginConfigForInstance = (plugin) => PLUGIN_CONFIG_STORE.get(plugin);
7
90
 
8
91
  /** src/cliHost/definePlugin.ts
9
92
  * Plugin contracts for the GetDotenv CLI host.
@@ -12,30 +95,66 @@ import { z } from 'zod';
12
95
  * should use (GetDotenvCliPublic). Using a structural type at the seam avoids
13
96
  * nominal class identity issues (private fields) in downstream consumers.
14
97
  */
15
- /**
16
- * Define a GetDotenv CLI plugin with compositional helpers.
17
- *
18
- * @example
19
- * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
20
- * .use(childA)
21
- * .use(childB);
22
- */
23
- const definePlugin = (spec) => {
98
+ /* eslint-disable tsdoc/syntax */
99
+ function definePlugin(spec) {
24
100
  const { children = [], ...rest } = spec;
25
- const plugin = {
101
+ // Default to a strict empty-object schema so “no-config” plugins fail fast
102
+ // on unknown keys and provide a concrete {} at runtime.
103
+ const effectiveSchema = spec.configSchema ?? z.object({}).strict();
104
+ // Build base plugin first, then extend with instance-bound helpers.
105
+ const base = {
26
106
  ...rest,
107
+ // Always carry a schema (strict empty by default) to simplify host logic
108
+ // and improve inference/ergonomics for plugin authors.
109
+ configSchema: effectiveSchema,
27
110
  children: [...children],
28
111
  use(child) {
29
112
  this.children.push(child);
30
113
  return this;
31
114
  },
32
115
  };
33
- return plugin;
34
- };
116
+ // Attach instance-bound helpers on the returned plugin object.
117
+ const extended = base;
118
+ extended.readConfig = function (_cli) {
119
+ // Config is stored per-plugin-instance by the host (WeakMap in computeContext).
120
+ const value = _getPluginConfigForInstance(extended);
121
+ if (value === undefined) {
122
+ // Guard: host has not resolved config yet (incorrect lifecycle usage).
123
+ throw new Error('Plugin config not available. Ensure resolveAndLoad() has been called before readConfig().');
124
+ }
125
+ return value;
126
+ };
127
+ // Plugin-bound dynamic option factory
128
+ extended.createPluginDynamicOption = function (cli, flags, desc, parser, defaultValue) {
129
+ return cli.createDynamicOption(flags, (cfg) => {
130
+ // Prefer the validated slice stored per instance; fallback to help-bag
131
+ // (by-id) so top-level `-h` can render effective defaults before resolve.
132
+ const fromStore = _getPluginConfigForInstance(extended);
133
+ const id = extended.id;
134
+ let fromBag;
135
+ if (!fromStore && id) {
136
+ const maybe = cfg.plugins[id];
137
+ if (maybe && typeof maybe === 'object') {
138
+ fromBag = maybe;
139
+ }
140
+ }
141
+ // Always provide a concrete object to dynamic callbacks:
142
+ // - With a schema: computeContext stores the parsed object.
143
+ // - Without a schema: computeContext stores {}.
144
+ // - Help-time fallback: coalesce to {} when only a by-id bag exists.
145
+ const cfgVal = (fromStore ?? fromBag ?? {});
146
+ return desc(cfg, cfgVal);
147
+ }, parser, defaultValue);
148
+ };
149
+ return extended;
150
+ }
35
151
 
36
152
  // Minimal tokenizer for shell-off execution:
37
153
  // Splits by whitespace while preserving quoted segments (single or double quotes).
38
- const tokenize = (command) => {
154
+ // Optionally preserve doubled quotes inside quoted segments:
155
+ // - default: "" => " (Windows/PowerShell style literal-quote escape)
156
+ // - preserveDoubledQuotes: true => "" stays "" (needed for Node -e payloads)
157
+ const tokenize = (command, opts) => {
39
158
  const out = [];
40
159
  let cur = '';
41
160
  let quote = null;
@@ -43,12 +162,16 @@ const tokenize = (command) => {
43
162
  const c = command.charAt(i);
44
163
  if (quote) {
45
164
  if (c === quote) {
46
- // Support doubled quotes inside a quoted segment (Windows/PowerShell style):
47
- // "" -> " and '' -> '
165
+ // Support doubled quotes inside a quoted segment:
166
+ // default: "" -> " and '' -> ' (Windows/PowerShell style)
167
+ // preserve: keep as "" to allow empty string literals in Node -e payloads
48
168
  const next = command.charAt(i + 1);
49
169
  if (next === quote) {
50
- cur += quote;
51
- i += 1; // skip the second quote
170
+ {
171
+ // Collapse to a single literal quote
172
+ cur += quote;
173
+ i += 1; // skip the second quote
174
+ }
52
175
  }
53
176
  else {
54
177
  // end of quoted segment
@@ -103,6 +226,17 @@ const stripOuterQuotes = (s) => {
103
226
  }
104
227
  return out;
105
228
  };
229
+ // Extract exitCode/stdout/stderr from execa result or error in a tolerant way.
230
+ const pickResult = (r) => {
231
+ const exit = r.exitCode;
232
+ const stdoutVal = r.stdout;
233
+ const stderrVal = r.stderr;
234
+ return {
235
+ exitCode: typeof exit === 'number' ? exit : Number.NaN,
236
+ stdout: typeof stdoutVal === 'string' ? stdoutVal : '',
237
+ stderr: typeof stderrVal === 'string' ? stderrVal : '',
238
+ };
239
+ };
106
240
  // Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
107
241
  // expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
108
242
  const sanitizeEnv = (env) => {
@@ -111,19 +245,19 @@ const sanitizeEnv = (env) => {
111
245
  const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
112
246
  return entries.length > 0 ? Object.fromEntries(entries) : undefined;
113
247
  };
114
- const runCommand = async (command, shell, opts) => {
248
+ async function runCommand(command, shell, opts) {
115
249
  if (shell === false) {
116
250
  let file;
117
251
  let args = [];
118
- if (Array.isArray(command)) {
119
- file = command[0];
120
- args = command.slice(1).map(stripOuterQuotes);
121
- }
122
- else {
252
+ if (typeof command === 'string') {
123
253
  const tokens = tokenize(command);
124
254
  file = tokens[0];
125
255
  args = tokens.slice(1);
126
256
  }
257
+ else {
258
+ file = command[0];
259
+ args = command.slice(1).map(stripOuterQuotes);
260
+ }
127
261
  if (!file)
128
262
  return 0;
129
263
  dbg('exec (plain)', { file, args, stdio: opts.stdio });
@@ -136,16 +270,15 @@ const runCommand = async (command, shell, opts) => {
136
270
  plainOpts.env = envSan;
137
271
  if (opts.stdio !== undefined)
138
272
  plainOpts.stdio = opts.stdio;
139
- const result = await execa(file, args, plainOpts);
140
- if (opts.stdio === 'pipe' && result.stdout) {
141
- process.stdout.write(result.stdout + (result.stdout.endsWith('\n') ? '' : '\n'));
273
+ const ok = pickResult((await execa(file, args, plainOpts)));
274
+ if (opts.stdio === 'pipe' && ok.stdout) {
275
+ process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
142
276
  }
143
- const exit = result?.exitCode;
144
- dbg('exit (plain)', { exitCode: exit });
145
- return typeof exit === 'number' ? exit : Number.NaN;
277
+ dbg('exit (plain)', { exitCode: ok.exitCode });
278
+ return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
146
279
  }
147
280
  else {
148
- const commandStr = Array.isArray(command) ? command.join(' ') : command;
281
+ const commandStr = typeof command === 'string' ? command : command.join(' ');
149
282
  dbg('exec (shell)', {
150
283
  shell: typeof shell === 'string' ? shell : 'custom',
151
284
  stdio: opts.stdio,
@@ -159,17 +292,29 @@ const runCommand = async (command, shell, opts) => {
159
292
  shellOpts.env = envSan;
160
293
  if (opts.stdio !== undefined)
161
294
  shellOpts.stdio = opts.stdio;
162
- const result = await execaCommand(commandStr, shellOpts);
163
- const out = result?.stdout;
164
- if (opts.stdio === 'pipe' && out) {
165
- process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
295
+ const ok = pickResult((await execaCommand(commandStr, shellOpts)));
296
+ if (opts.stdio === 'pipe' && ok.stdout) {
297
+ process.stdout.write(ok.stdout + (ok.stdout.endsWith('\n') ? '' : '\n'));
166
298
  }
167
- const exit = result?.exitCode;
168
- dbg('exit (shell)', { exitCode: exit });
169
- return typeof exit === 'number' ? exit : Number.NaN;
299
+ dbg('exit (shell)', { exitCode: ok.exitCode });
300
+ return typeof ok.exitCode === 'number' ? ok.exitCode : Number.NaN;
170
301
  }
171
- };
302
+ }
172
303
 
304
+ /** src/cliCore/spawnEnv.ts
305
+ * Build a sanitized environment bag for child processes.
306
+ *
307
+ * Requirements addressed:
308
+ * - Provide a single helper (buildSpawnEnv) to normalize/dedupe child env.
309
+ * - Drop undefined values (exactOptional semantics).
310
+ * - On Windows, dedupe keys case-insensitively and prefer the last value,
311
+ * preserving the latest key's casing. Ensure HOME fallback from USERPROFILE.
312
+ * Normalize TMP/TEMP consistency when either is present.
313
+ * - On POSIX, keep keys as-is; when a temp dir key is present (TMPDIR/TMP/TEMP),
314
+ * ensure TMPDIR exists for downstream consumers that expect it.
315
+ *
316
+ * Adapter responsibility: pure mapping; no business logic.
317
+ */
173
318
  const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
174
319
  /** Build a sanitized env for child processes from base + overlay. */
175
320
  const buildSpawnEnv = (base, overlay) => {
@@ -235,9 +380,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
235
380
  }
236
381
  return { absRootPath, paths };
237
382
  };
238
- const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
383
+ const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
239
384
  const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
240
- Boolean(getDotenvCliOptions?.capture); // Require a command only when not listing. In list mode, a command is optional.
385
+ Boolean(getDotenvCliOptions?.capture);
386
+ // Require a command only when not listing. In list mode, a command is optional.
241
387
  if (!command && !list) {
242
388
  logger.error(`No command provided. Use --command or --list.`);
243
389
  process.exit(0);
@@ -284,12 +430,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
284
430
  const hasCmd = (typeof command === 'string' && command.length > 0) ||
285
431
  (Array.isArray(command) && command.length > 0);
286
432
  if (hasCmd) {
287
- const envBag = getDotenvCliOptions !== undefined
288
- ? { getDotenvCliOptions: JSON.stringify(getDotenvCliOptions) }
289
- : undefined;
433
+ // Compose child env overlay from dotenv (drop undefined) and merged options
434
+ const overlay = {};
435
+ if (dotenvEnv) {
436
+ for (const [k, v] of Object.entries(dotenvEnv)) {
437
+ if (typeof v === 'string')
438
+ overlay[k] = v;
439
+ }
440
+ }
441
+ if (getDotenvCliOptions !== undefined) {
442
+ try {
443
+ overlay.getDotenvCliOptions = JSON.stringify(getDotenvCliOptions);
444
+ }
445
+ catch {
446
+ // best-effort: omit if serialization fails
447
+ }
448
+ }
290
449
  await runCommand(command, shell, {
291
450
  cwd: path,
292
- env: buildSpawnEnv(process.env, envBag),
451
+ env: buildSpawnEnv(process.env, overlay),
293
452
  stdio: capture ? 'pipe' : 'inherit',
294
453
  });
295
454
  }
@@ -340,7 +499,7 @@ const resolveShell = (scripts, command, shell) => scripts && typeof scripts[comm
340
499
  * Build the default "cmd" subcommand action for the batch plugin.
341
500
  * Mirrors the original inline implementation with identical behavior.
342
501
  */
343
- const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _subOpts, _thisCommand) => {
502
+ const buildDefaultCmdAction = (plugin, cli, batchCmd, opts) => async (commandParts, _subOpts, _thisCommand) => {
344
503
  const loggerLocal = opts.logger ?? console;
345
504
  // Guard: when invoked without positional args (e.g., `batch --list`),
346
505
  // defer entirely to the parent action handler.
@@ -352,9 +511,8 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
352
511
  ? argsRaw.filter((t) => t !== '-l' && t !== '--list')
353
512
  : argsRaw;
354
513
  // Access merged per-plugin config from host context (if any).
355
- const ctx = cli.getCtx();
356
- const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
357
- const cfg = (cfgRaw || {});
514
+ const cfg = plugin.readConfig(cli);
515
+ const dotenvEnv = (cli.getCtx()?.dotenv ?? {});
358
516
  // Resolve batch flags from the captured parent (batch) command.
359
517
  const raw = batchCmd.opts();
360
518
  const listFromParent = !!raw.list;
@@ -373,6 +531,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
373
531
  if (typeof commandOpt === 'string') {
374
532
  await execShellCommandBatch({
375
533
  command: resolveCommand(scripts, commandOpt),
534
+ dotenvEnv,
376
535
  globs,
377
536
  ignoreErrors,
378
537
  list: false,
@@ -384,6 +543,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
384
543
  return;
385
544
  }
386
545
  if (raw.list || localList) {
546
+ const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
387
547
  await execShellCommandBatch({
388
548
  globs,
389
549
  ignoreErrors,
@@ -391,7 +551,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
391
551
  logger: loggerLocal,
392
552
  ...(pkgCwd ? { pkgCwd } : {}),
393
553
  rootPath,
394
- shell: (shell ?? false),
554
+ shell: shell ?? shellBag.shell ?? false,
395
555
  });
396
556
  return;
397
557
  }
@@ -415,7 +575,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
415
575
  logger: loggerLocal,
416
576
  ...(pkgCwd ? { pkgCwd } : {}),
417
577
  rootPath,
418
- shell: (shell ?? shellBag.shell ?? false),
578
+ shell: shell ?? shellBag.shell ?? false,
419
579
  });
420
580
  return;
421
581
  }
@@ -458,6 +618,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
458
618
  }
459
619
  await execShellCommandBatch({
460
620
  command: commandArg,
621
+ dotenvEnv,
461
622
  ...(envBag ? { getDotenvCliOptions: envBag } : {}),
462
623
  globs,
463
624
  ignoreErrors,
@@ -472,12 +633,11 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
472
633
  /**
473
634
  * Build the parent "batch" action handler (no explicit subcommand).
474
635
  */
475
- const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
476
- const logger = opts.logger ?? console;
636
+ const buildParentAction = (plugin, cli, opts) => async (commandParts, thisCommand) => {
637
+ const loggerLocal = opts.logger ?? console;
477
638
  // Ensure context exists (host preSubcommand on root creates if missing).
478
- const ctx = cli.getCtx();
479
- const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
480
- const cfg = (cfgRaw || {});
639
+ const dotenvEnv = (cli.getCtx()?.dotenv ?? {});
640
+ const cfg = plugin.readConfig(cli);
481
641
  const raw = thisCommand.opts();
482
642
  const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
483
643
  const ignoreErrors = !!raw.ignoreErrors;
@@ -498,10 +658,11 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
498
658
  const commandArg = resolved;
499
659
  await execShellCommandBatch({
500
660
  command: commandArg,
661
+ dotenvEnv,
501
662
  globs,
502
663
  ignoreErrors,
503
664
  list: false,
504
- logger,
665
+ logger: loggerLocal,
505
666
  ...(pkgCwd ? { pkgCwd } : {}),
506
667
  rootPath,
507
668
  shell: shellSetting,
@@ -514,19 +675,20 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
514
675
  if (extra.length > 0)
515
676
  globs = [globs, extra].filter(Boolean).join(' ');
516
677
  const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
678
+ const shellMerged = opts.shell ?? cfg.shell ?? mergedBag.shell ?? false;
517
679
  await execShellCommandBatch({
518
680
  globs,
519
681
  ignoreErrors,
520
682
  list: true,
521
- logger,
683
+ logger: loggerLocal,
522
684
  ...(pkgCwd ? { pkgCwd } : {}),
523
685
  rootPath,
524
- shell: (opts.shell ?? cfg.shell ?? mergedBag.shell ?? false),
686
+ shell: shellMerged,
525
687
  });
526
688
  return;
527
689
  }
528
690
  if (!commandOpt && !list) {
529
- logger.error(`No command provided. Use --command or --list.`);
691
+ loggerLocal.error(`No command provided. Use --command or --list.`);
530
692
  process.exit(0);
531
693
  }
532
694
  if (typeof commandOpt === 'string') {
@@ -535,10 +697,11 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
535
697
  const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
536
698
  await execShellCommandBatch({
537
699
  command: resolveCommand(scriptsOpt, commandOpt),
700
+ dotenvEnv,
538
701
  globs,
539
702
  ignoreErrors,
540
703
  list,
541
- logger,
704
+ logger: loggerLocal,
542
705
  ...(pkgCwd ? { pkgCwd } : {}),
543
706
  rootPath,
544
707
  shell: resolveShell(scriptsOpt, commandOpt, shellOpt),
@@ -547,15 +710,15 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
547
710
  }
548
711
  // list only (explicit --list without --command)
549
712
  const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
550
- const shellOnly = (opts.shell ?? cfg.shell ?? mergedBag.shell ?? false);
713
+ const shellOnly = opts.shell ?? cfg.shell ?? mergedBag.shell ?? false;
551
714
  await execShellCommandBatch({
552
715
  globs,
553
716
  ignoreErrors,
554
717
  list: true,
555
- logger,
718
+ logger: loggerLocal,
556
719
  ...(pkgCwd ? { pkgCwd } : {}),
557
720
  rootPath,
558
- shell: (shellOnly ?? false),
721
+ shell: shellOnly,
559
722
  });
560
723
  };
561
724
 
@@ -578,37 +741,56 @@ const BatchConfigSchema = z.object({
578
741
  /**
579
742
  * Batch plugin for the GetDotenv CLI host.
580
743
  *
581
- * Mirrors the legacy batch subcommand behavior without altering the shipped CLI. * Options:
744
+ * Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
745
+ * Options:
582
746
  * - scripts/shell: used to resolve command and shell behavior per script or global default.
583
747
  * - logger: defaults to console.
584
748
  */
585
- const batchPlugin = (opts = {}) => definePlugin({
586
- id: 'batch',
587
- // Host validates this when config-loader is enabled; plugins may also
588
- // re-validate at action time as a safety belt.
589
- configSchema: BatchConfigSchema,
590
- setup(cli) {
591
- const ns = cli.ns('batch');
592
- const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
593
- ns.description('Batch command execution across multiple working directories.')
594
- .enablePositionalOptions()
595
- .passThroughOptions()
596
- .option('-p, --pkg-cwd', 'use nearest package directory as current working directory')
597
- .option('-r, --root-path <string>', 'path to batch root directory from current working directory', './')
598
- .option('-g, --globs <string>', 'space-delimited globs from root path', '*')
599
- .option('-c, --command <string>', 'command executed according to the base shell resolution')
600
- .option('-l, --list', 'list working directories without executing command')
601
- .option('-e, --ignore-errors', 'ignore errors and continue with next path')
602
- .argument('[command...]')
603
- .addCommand(new Command()
604
- .name('cmd')
605
- .description('execute command, conflicts with --command option (default subcommand)')
606
- .enablePositionalOptions()
607
- .passThroughOptions()
608
- .argument('[command...]')
609
- .action(buildDefaultCmdAction(cli, batchCmd, opts)), { isDefault: true })
610
- .action(buildParentAction(cli, opts));
611
- },
612
- });
749
+ const batchPlugin = (opts = {}) => {
750
+ const plugin = definePlugin({
751
+ id: 'batch',
752
+ // Host validates this when config-loader is enabled; plugins may also
753
+ // re-validate at action time as a safety belt.
754
+ configSchema: BatchConfigSchema,
755
+ setup(cli) {
756
+ const ns = cli.ns('batch');
757
+ const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
758
+ const pluginId = 'batch';
759
+ const GROUP = `plugin:${pluginId}`;
760
+ ns.description('Batch command execution across multiple working directories.')
761
+ .enablePositionalOptions()
762
+ .passThroughOptions()
763
+ // Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
764
+ .addOption((() => {
765
+ const opt = plugin.createPluginDynamicOption(cli, '-p, --pkg-cwd', (_bag, cfg) => `use nearest package directory as current working directory${cfg.pkgCwd ? ' (default)' : ''}`);
766
+ cli.setOptionGroup(opt, GROUP);
767
+ return opt;
768
+ })())
769
+ .addOption((() => {
770
+ const opt = plugin.createPluginDynamicOption(cli, '-r, --root-path <string>', (_bag, cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.rootPath || './')})`);
771
+ cli.setOptionGroup(opt, GROUP);
772
+ return opt;
773
+ })())
774
+ .addOption((() => {
775
+ const opt = plugin.createPluginDynamicOption(cli, '-g, --globs <string>', (_bag, cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.globs || '*')})`);
776
+ cli.setOptionGroup(opt, GROUP);
777
+ return opt;
778
+ })())
779
+ .option('-c, --command <string>', 'command executed according to the base shell resolution')
780
+ .option('-l, --list', 'list working directories without executing command')
781
+ .option('-e, --ignore-errors', 'ignore errors and continue with next path')
782
+ .argument('[command...]')
783
+ .addCommand(new Command()
784
+ .name('cmd')
785
+ .description('execute command, conflicts with --command option (default subcommand)')
786
+ .enablePositionalOptions()
787
+ .passThroughOptions()
788
+ .argument('[command...]')
789
+ .action(buildDefaultCmdAction(plugin, cli, batchCmd, opts)), { isDefault: true })
790
+ .action(buildParentAction(plugin, cli, opts));
791
+ },
792
+ });
793
+ return plugin;
794
+ };
613
795
 
614
796
  export { batchPlugin };