@karmaniverous/get-dotenv 4.5.2 → 5.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.
Files changed (49) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +369 -215
  3. package/dist/cliHost.cjs +1078 -0
  4. package/dist/cliHost.d.cts +193 -0
  5. package/dist/cliHost.d.mts +193 -0
  6. package/dist/cliHost.d.ts +193 -0
  7. package/dist/cliHost.mjs +1074 -0
  8. package/dist/config.cjs +247 -0
  9. package/dist/config.d.cts +53 -0
  10. package/dist/config.d.mts +53 -0
  11. package/dist/config.d.ts +53 -0
  12. package/dist/config.mjs +242 -0
  13. package/dist/env-overlay.cjs +163 -0
  14. package/dist/env-overlay.d.cts +50 -0
  15. package/dist/env-overlay.d.mts +50 -0
  16. package/dist/env-overlay.d.ts +50 -0
  17. package/dist/env-overlay.mjs +161 -0
  18. package/dist/getdotenv.cli.mjs +2817 -40874
  19. package/dist/index.cjs +1482 -40965
  20. package/dist/index.d.cts +206 -67
  21. package/dist/index.d.mts +206 -67
  22. package/dist/index.d.ts +206 -67
  23. package/dist/index.mjs +1454 -40939
  24. package/dist/plugins-aws.cjs +618 -0
  25. package/dist/plugins-aws.d.cts +178 -0
  26. package/dist/plugins-aws.d.mts +178 -0
  27. package/dist/plugins-aws.d.ts +178 -0
  28. package/dist/plugins-aws.mjs +616 -0
  29. package/dist/plugins-batch.cjs +569 -0
  30. package/dist/plugins-batch.d.cts +200 -0
  31. package/dist/plugins-batch.d.mts +200 -0
  32. package/dist/plugins-batch.d.ts +200 -0
  33. package/dist/plugins-batch.mjs +567 -0
  34. package/dist/plugins-init.cjs +282 -0
  35. package/dist/plugins-init.d.cts +182 -0
  36. package/dist/plugins-init.d.mts +182 -0
  37. package/dist/plugins-init.d.ts +182 -0
  38. package/dist/plugins-init.mjs +280 -0
  39. package/getdotenv.config.json +19 -0
  40. package/package.json +228 -139
  41. package/templates/cli/ts/index.ts +9 -0
  42. package/templates/cli/ts/plugins/hello.ts +17 -0
  43. package/templates/config/js/getdotenv.config.js +15 -0
  44. package/templates/config/json/local/getdotenv.config.local.json +7 -0
  45. package/templates/config/json/public/getdotenv.config.json +12 -0
  46. package/templates/config/public/getdotenv.config.json +13 -0
  47. package/templates/config/ts/getdotenv.config.ts +16 -0
  48. package/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
  49. package/templates/config/yaml/public/getdotenv.config.yaml +10 -0
@@ -0,0 +1,567 @@
1
+ import { Command } from 'commander';
2
+ import { globby } from 'globby';
3
+ import { packageDirectory } from 'package-directory';
4
+ import path from 'path';
5
+ import { execa, execaCommand } from 'execa';
6
+ import { z } from 'zod';
7
+
8
+ /**
9
+ * Define a GetDotenv CLI plugin with compositional helpers.
10
+ *
11
+ * @example
12
+ * const parent = definePlugin(\{ id: 'p', setup(cli) \{ /* ... *\/ \} \})
13
+ * .use(childA)
14
+ * .use(childB);
15
+ */
16
+ const definePlugin = (spec) => {
17
+ const { children = [], ...rest } = spec;
18
+ const plugin = {
19
+ ...rest,
20
+ children: [...children],
21
+ use(child) {
22
+ this.children.push(child);
23
+ return this;
24
+ },
25
+ };
26
+ return plugin;
27
+ };
28
+
29
+ // Minimal tokenizer for shell-off execution:
30
+ // Splits by whitespace while preserving quoted segments (single or double quotes).
31
+ const tokenize = (command) => {
32
+ const out = [];
33
+ let cur = '';
34
+ let quote = null;
35
+ for (let i = 0; i < command.length; i++) {
36
+ const c = command.charAt(i);
37
+ if (quote) {
38
+ if (c === quote) {
39
+ // Support doubled quotes inside a quoted segment (Windows/PowerShell style):
40
+ // "" -> " and '' -> '
41
+ const next = command.charAt(i + 1);
42
+ if (next === quote) {
43
+ cur += quote;
44
+ i += 1; // skip the second quote
45
+ }
46
+ else {
47
+ // end of quoted segment
48
+ quote = null;
49
+ }
50
+ }
51
+ else {
52
+ cur += c;
53
+ }
54
+ }
55
+ else {
56
+ if (c === '"' || c === "'") {
57
+ quote = c;
58
+ }
59
+ else if (/\s/.test(c)) {
60
+ if (cur) {
61
+ out.push(cur);
62
+ cur = '';
63
+ }
64
+ }
65
+ else {
66
+ cur += c;
67
+ }
68
+ }
69
+ }
70
+ if (cur)
71
+ out.push(cur);
72
+ return out;
73
+ };
74
+
75
+ const dbg = (...args) => {
76
+ if (process.env.GETDOTENV_DEBUG) {
77
+ // Use stderr to avoid interfering with stdout assertions
78
+ console.error('[getdotenv:run]', ...args);
79
+ }
80
+ };
81
+ // Strip repeated symmetric outer quotes (single or double) until stable.
82
+ // This is safe for argv arrays passed to execa (no quoting needed) and avoids
83
+ // passing quote characters through to Node (e.g., for `node -e "<code>"`).
84
+ // Handles stacked quotes from shells like PowerShell: """code""" -> code.
85
+ const stripOuterQuotes = (s) => {
86
+ let out = s;
87
+ // Repeatedly trim only when the entire string is wrapped in matching quotes.
88
+ // Stop as soon as the ends are asymmetric or no quotes remain.
89
+ while (out.length >= 2) {
90
+ const a = out.charAt(0);
91
+ const b = out.charAt(out.length - 1);
92
+ const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
93
+ if (!symmetric)
94
+ break;
95
+ out = out.slice(1, -1);
96
+ }
97
+ return out;
98
+ };
99
+ // Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
100
+ // expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
101
+ const sanitizeEnv = (env) => {
102
+ if (!env)
103
+ return undefined;
104
+ const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
105
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
106
+ };
107
+ const runCommand = async (command, shell, opts) => {
108
+ if (shell === false) {
109
+ let file;
110
+ let args = [];
111
+ if (Array.isArray(command)) {
112
+ file = command[0];
113
+ args = command.slice(1).map(stripOuterQuotes);
114
+ }
115
+ else {
116
+ const tokens = tokenize(command);
117
+ file = tokens[0];
118
+ args = tokens.slice(1);
119
+ }
120
+ if (!file)
121
+ return 0;
122
+ dbg('exec (plain)', { file, args, stdio: opts.stdio });
123
+ // Build options without injecting undefined properties (exactOptionalPropertyTypes).
124
+ const envSan = sanitizeEnv(opts.env);
125
+ const plainOpts = {};
126
+ if (opts.cwd !== undefined)
127
+ plainOpts.cwd = opts.cwd;
128
+ if (envSan !== undefined)
129
+ plainOpts.env = envSan;
130
+ if (opts.stdio !== undefined)
131
+ plainOpts.stdio = opts.stdio;
132
+ const result = await execa(file, args, plainOpts);
133
+ if (opts.stdio === 'pipe' && result.stdout) {
134
+ process.stdout.write(result.stdout + (result.stdout.endsWith('\n') ? '' : '\n'));
135
+ }
136
+ const exit = result?.exitCode;
137
+ dbg('exit (plain)', { exitCode: exit });
138
+ return typeof exit === 'number' ? exit : Number.NaN;
139
+ }
140
+ else {
141
+ const commandStr = Array.isArray(command) ? command.join(' ') : command;
142
+ dbg('exec (shell)', {
143
+ shell: typeof shell === 'string' ? shell : 'custom',
144
+ stdio: opts.stdio,
145
+ command: commandStr,
146
+ });
147
+ const envSan = sanitizeEnv(opts.env);
148
+ const shellOpts = { shell };
149
+ if (opts.cwd !== undefined)
150
+ shellOpts.cwd = opts.cwd;
151
+ if (envSan !== undefined)
152
+ shellOpts.env = envSan;
153
+ if (opts.stdio !== undefined)
154
+ shellOpts.stdio = opts.stdio;
155
+ const result = await execaCommand(commandStr, shellOpts);
156
+ const out = result?.stdout;
157
+ if (opts.stdio === 'pipe' && out) {
158
+ process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
159
+ }
160
+ const exit = result?.exitCode;
161
+ dbg('exit (shell)', { exitCode: exit });
162
+ return typeof exit === 'number' ? exit : Number.NaN;
163
+ }
164
+ };
165
+
166
+ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
167
+ let cwd = process.cwd();
168
+ if (pkgCwd) {
169
+ const pkgDir = await packageDirectory();
170
+ if (!pkgDir) {
171
+ logger.error('No package directory found.');
172
+ process.exit(0);
173
+ }
174
+ cwd = pkgDir;
175
+ }
176
+ const absRootPath = path.posix.join(cwd.split(path.sep).join(path.posix.sep), rootPath.split(path.sep).join(path.posix.sep));
177
+ const paths = await globby(globs.split(/\s+/), {
178
+ cwd: absRootPath,
179
+ expandDirectories: false,
180
+ onlyDirectories: true,
181
+ absolute: true,
182
+ });
183
+ if (!paths.length) {
184
+ logger.error(`No paths found for globs '${globs}' at '${absRootPath}'.`);
185
+ process.exit(0);
186
+ }
187
+ return { absRootPath, paths };
188
+ };
189
+ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
190
+ const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
191
+ Boolean(getDotenvCliOptions?.capture); // Require a command only when not listing. In list mode, a command is optional.
192
+ if (!command && !list) {
193
+ logger.error(`No command provided. Use --command or --list.`);
194
+ process.exit(0);
195
+ }
196
+ const { absRootPath, paths } = await globPaths({
197
+ globs,
198
+ logger,
199
+ rootPath,
200
+ // exactOptionalPropertyTypes: only include when defined
201
+ ...(pkgCwd !== undefined ? { pkgCwd } : {}),
202
+ });
203
+ const headerTitle = list
204
+ ? 'Listing working directories...'
205
+ : 'Executing command batch...';
206
+ logger.info('');
207
+ const headerRootPath = `ROOT: ${absRootPath}`;
208
+ const headerGlobs = `GLOBS: ${globs}`;
209
+ // Prepare a safe label for the header (avoid undefined in template)
210
+ const commandLabel = Array.isArray(command)
211
+ ? command.join(' ')
212
+ : typeof command === 'string' && command.length > 0
213
+ ? command
214
+ : '';
215
+ const headerCommand = list ? `CMD: (list only)` : `CMD: ${commandLabel}`;
216
+ logger.info('*'.repeat(Math.max(headerTitle.length, headerRootPath.length, headerGlobs.length, headerCommand.length)));
217
+ logger.info(headerTitle);
218
+ logger.info('');
219
+ logger.info(headerRootPath);
220
+ logger.info(headerGlobs);
221
+ logger.info(headerCommand);
222
+ for (const path of paths) {
223
+ // Write path and command to console.
224
+ const pathLabel = `CWD: ${path}`;
225
+ if (list) {
226
+ logger.info(pathLabel);
227
+ continue;
228
+ }
229
+ logger.info('');
230
+ logger.info('*'.repeat(pathLabel.length));
231
+ logger.info(pathLabel);
232
+ logger.info(headerCommand);
233
+ // Execute command.
234
+ try {
235
+ const hasCmd = (typeof command === 'string' && command.length > 0) ||
236
+ (Array.isArray(command) && command.length > 0);
237
+ if (hasCmd) {
238
+ await runCommand(command, shell, {
239
+ cwd: path,
240
+ env: {
241
+ ...process.env,
242
+ getDotenvCliOptions: getDotenvCliOptions
243
+ ? JSON.stringify(getDotenvCliOptions)
244
+ : undefined,
245
+ },
246
+ stdio: capture ? 'pipe' : 'inherit',
247
+ });
248
+ }
249
+ else {
250
+ // Should not occur due to the early guard; retain for type safety.
251
+ logger.error(`No command provided. Use --command or --list.`);
252
+ process.exit(0);
253
+ }
254
+ }
255
+ catch (error) {
256
+ if (!ignoreErrors) {
257
+ throw error;
258
+ }
259
+ }
260
+ }
261
+ logger.info('');
262
+ };
263
+
264
+ /**
265
+ * Batch services (neutral): resolve command and shell settings.
266
+ * Shared by the generator path and the batch plugin to avoid circular deps.
267
+ */
268
+ /**
269
+ * Resolve a command string from the {@link Scripts} table.
270
+ * A script may be expressed as a string or an object with a `cmd` property.
271
+ *
272
+ * @param scripts - Optional scripts table.
273
+ * @param command - User-provided command name or string.
274
+ * @returns Resolved command string (falls back to the provided command).
275
+ */
276
+ const resolveCommand = (scripts, command) => scripts && typeof scripts[command] === 'object'
277
+ ? scripts[command].cmd
278
+ : (scripts?.[command] ?? command);
279
+ /**
280
+ * Resolve the shell setting for a given command:
281
+ * - If the script entry is an object, prefer its `shell` override.
282
+ * - Otherwise use the provided `shell` (string | boolean).
283
+ *
284
+ * @param scripts - Optional scripts table.
285
+ * @param command - User-provided command name or string.
286
+ * @param shell - Global shell preference (string | boolean).
287
+ */
288
+ const resolveShell = (scripts, command, shell) => scripts && typeof scripts[command] === 'object'
289
+ ? (scripts[command].shell ?? false)
290
+ : (shell ?? false);
291
+
292
+ /**
293
+ * Build the default "cmd" subcommand action for the batch plugin.
294
+ * Mirrors the original inline implementation with identical behavior.
295
+ */
296
+ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _subOpts, _thisCommand) => {
297
+ const loggerLocal = opts.logger ?? console;
298
+ // Guard: when invoked without positional args (e.g., `batch --list`),
299
+ // defer entirely to the parent action handler.
300
+ const argsRaw = Array.isArray(commandParts)
301
+ ? commandParts
302
+ : [];
303
+ const localList = argsRaw.includes('-l') || argsRaw.includes('--list');
304
+ const args = localList
305
+ ? argsRaw.filter((t) => t !== '-l' && t !== '--list')
306
+ : argsRaw;
307
+ // Access merged per-plugin config from host context (if any).
308
+ const ctx = cli.getCtx();
309
+ const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
310
+ const cfg = (cfgRaw || {});
311
+ // Resolve batch flags from the captured parent (batch) command.
312
+ const raw = batchCmd.opts();
313
+ const listFromParent = !!raw.list;
314
+ const ignoreErrors = !!raw.ignoreErrors;
315
+ const globs = typeof raw.globs === 'string' ? raw.globs : (cfg.globs ?? '*');
316
+ const pkgCwd = raw.pkgCwd !== undefined ? !!raw.pkgCwd : !!cfg.pkgCwd;
317
+ const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : (cfg.rootPath ?? './');
318
+ // Resolve scripts/shell with precedence:
319
+ // plugin opts → plugin config → merged root CLI options
320
+ const mergedBag = ((batchCmd.parent ?? null)?.getDotenvCliOptions ?? {});
321
+ const scripts = opts.scripts ?? cfg.scripts ?? mergedBag.scripts;
322
+ const shell = opts.shell ?? cfg.shell ?? mergedBag.shell;
323
+ // If no positional args were given, bridge to --command/--list paths here.
324
+ if (args.length === 0) {
325
+ const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
326
+ if (typeof commandOpt === 'string') {
327
+ await execShellCommandBatch({
328
+ command: resolveCommand(scripts, commandOpt),
329
+ globs,
330
+ ignoreErrors,
331
+ list: false,
332
+ logger: loggerLocal,
333
+ ...(pkgCwd ? { pkgCwd } : {}),
334
+ rootPath,
335
+ shell: resolveShell(scripts, commandOpt, shell),
336
+ });
337
+ return;
338
+ }
339
+ if (raw.list || localList) {
340
+ await execShellCommandBatch({
341
+ globs,
342
+ ignoreErrors,
343
+ list: true,
344
+ logger: loggerLocal,
345
+ ...(pkgCwd ? { pkgCwd } : {}),
346
+ rootPath,
347
+ shell: (shell ?? false),
348
+ });
349
+ return;
350
+ }
351
+ {
352
+ const lr = loggerLocal;
353
+ const emit = lr.error ?? lr.log;
354
+ emit(`No command provided. Use --command or --list.`);
355
+ }
356
+ process.exit(0);
357
+ }
358
+ // If a local list flag was supplied with positional tokens (and no --command),
359
+ // treat tokens as additional globs and execute list mode.
360
+ if (localList && typeof raw.command !== 'string') {
361
+ const extraGlobs = args.map(String).join(' ').trim();
362
+ const mergedGlobs = [globs, extraGlobs].filter(Boolean).join(' ');
363
+ const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
364
+ await execShellCommandBatch({
365
+ globs: mergedGlobs,
366
+ ignoreErrors,
367
+ list: true,
368
+ logger: loggerLocal,
369
+ ...(pkgCwd ? { pkgCwd } : {}),
370
+ rootPath,
371
+ shell: (shell ?? shellBag.shell ?? false),
372
+ });
373
+ return;
374
+ }
375
+ // If parent list flag is set and positional tokens are present (and no --command),
376
+ // treat tokens as additional globs for list-only mode.
377
+ if (listFromParent && args.length > 0 && typeof raw.command !== 'string') {
378
+ const extra = args.map(String).join(' ').trim();
379
+ const mergedGlobs = [globs, extra].filter(Boolean).join(' ');
380
+ const mergedBag2 = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
381
+ await execShellCommandBatch({
382
+ globs: mergedGlobs,
383
+ ignoreErrors,
384
+ list: true,
385
+ logger: loggerLocal,
386
+ ...(pkgCwd ? { pkgCwd } : {}),
387
+ rootPath,
388
+ shell: (shell ?? mergedBag2.shell ?? false),
389
+ });
390
+ return;
391
+ }
392
+ // Join positional args as the command to execute.
393
+ const input = args.map(String).join(' ');
394
+ // Optional: round-trip parent merged options if present (shipped CLI).
395
+ const envBag = (batchCmd.parent ?? undefined)?.getDotenvCliOptions;
396
+ const mergedExec = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
397
+ const scriptsExec = scripts ?? mergedExec.scripts;
398
+ const shellExec = shell ?? mergedExec.shell;
399
+ const resolved = resolveCommand(scriptsExec, input);
400
+ const shellSetting = resolveShell(scriptsExec, input, shellExec);
401
+ // Preserve argv array only for shell-off Node -e snippets to avoid
402
+ // lossy re-tokenization (Windows/PowerShell quoting). For simple
403
+ // commands (e.g., "echo OK") keep string form to satisfy unit tests.
404
+ let commandArg = resolved;
405
+ if (shellSetting === false && resolved === input) {
406
+ const first = (args[0] ?? '').toLowerCase();
407
+ const hasEval = args.includes('-e') || args.includes('--eval');
408
+ if (first === 'node' && hasEval) {
409
+ commandArg = args.map(String);
410
+ }
411
+ }
412
+ await execShellCommandBatch({
413
+ command: commandArg,
414
+ ...(envBag ? { getDotenvCliOptions: envBag } : {}),
415
+ globs,
416
+ ignoreErrors,
417
+ list: false,
418
+ logger: loggerLocal,
419
+ ...(pkgCwd ? { pkgCwd } : {}),
420
+ rootPath,
421
+ shell: shellSetting,
422
+ });
423
+ };
424
+
425
+ /**
426
+ * Build the parent "batch" action handler (no explicit subcommand).
427
+ */
428
+ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
429
+ const logger = opts.logger ?? console;
430
+ // Ensure context exists (host preSubcommand on root creates if missing).
431
+ const ctx = cli.getCtx();
432
+ const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
433
+ const cfg = (cfgRaw || {});
434
+ const raw = thisCommand.opts();
435
+ const commandOpt = typeof raw.command === 'string' ? raw.command : undefined;
436
+ const ignoreErrors = !!raw.ignoreErrors;
437
+ let globs = typeof raw.globs === 'string' ? raw.globs : (cfg.globs ?? '*');
438
+ const list = !!raw.list;
439
+ const pkgCwd = raw.pkgCwd !== undefined ? !!raw.pkgCwd : !!cfg.pkgCwd;
440
+ const rootPath = typeof raw.rootPath === 'string' ? raw.rootPath : (cfg.rootPath ?? './');
441
+ // Treat parent positional tokens as the command when no explicit 'cmd' is used.
442
+ const argsParent = Array.isArray(commandParts) ? commandParts : [];
443
+ if (argsParent.length > 0 && !list) {
444
+ const input = argsParent.map(String).join(' ');
445
+ const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
446
+ const scriptsAll = opts.scripts ?? cfg.scripts ?? mergedBag.scripts;
447
+ const shellAll = opts.shell ?? cfg.shell ?? mergedBag.shell;
448
+ const resolved = resolveCommand(scriptsAll, input);
449
+ const shellSetting = resolveShell(scriptsAll, input, shellAll);
450
+ // Parent path: pass a string; executor handles shell-specific details.
451
+ const commandArg = resolved;
452
+ await execShellCommandBatch({
453
+ command: commandArg,
454
+ globs,
455
+ ignoreErrors,
456
+ list: false,
457
+ logger,
458
+ ...(pkgCwd ? { pkgCwd } : {}),
459
+ rootPath,
460
+ shell: shellSetting,
461
+ });
462
+ return;
463
+ }
464
+ // List-only: merge extra positional tokens into globs when no --command is present.
465
+ if (list && argsParent.length > 0 && !commandOpt) {
466
+ const extra = argsParent.map(String).join(' ').trim();
467
+ if (extra.length > 0)
468
+ globs = [globs, extra].filter(Boolean).join(' ');
469
+ const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
470
+ await execShellCommandBatch({
471
+ globs,
472
+ ignoreErrors,
473
+ list: true,
474
+ logger,
475
+ ...(pkgCwd ? { pkgCwd } : {}),
476
+ rootPath,
477
+ shell: (opts.shell ?? cfg.shell ?? mergedBag.shell ?? false),
478
+ });
479
+ return;
480
+ }
481
+ if (!commandOpt && !list) {
482
+ logger.error(`No command provided. Use --command or --list.`);
483
+ process.exit(0);
484
+ }
485
+ if (typeof commandOpt === 'string') {
486
+ const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
487
+ const scriptsOpt = opts.scripts ?? cfg.scripts ?? mergedBag.scripts;
488
+ const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
489
+ await execShellCommandBatch({
490
+ command: resolveCommand(scriptsOpt, commandOpt),
491
+ globs,
492
+ ignoreErrors,
493
+ list,
494
+ logger,
495
+ ...(pkgCwd ? { pkgCwd } : {}),
496
+ rootPath,
497
+ shell: resolveShell(scriptsOpt, commandOpt, shellOpt),
498
+ });
499
+ return;
500
+ }
501
+ // list only (explicit --list without --command)
502
+ const mergedBag = ((thisCommand.parent ?? null)?.getDotenvCliOptions ?? {});
503
+ const shellOnly = (opts.shell ?? cfg.shell ?? mergedBag.shell ?? false);
504
+ await execShellCommandBatch({
505
+ globs,
506
+ ignoreErrors,
507
+ list: true,
508
+ logger,
509
+ ...(pkgCwd ? { pkgCwd } : {}),
510
+ rootPath,
511
+ shell: (shellOnly ?? false),
512
+ });
513
+ };
514
+
515
+ // Per-plugin config schema (optional fields; used as defaults).
516
+ const ScriptSchema = z.union([
517
+ z.string(),
518
+ z.object({
519
+ cmd: z.string(),
520
+ shell: z.union([z.string(), z.boolean()]).optional(),
521
+ }),
522
+ ]);
523
+ const BatchConfigSchema = z.object({
524
+ scripts: z.record(z.string(), ScriptSchema).optional(),
525
+ shell: z.union([z.string(), z.boolean()]).optional(),
526
+ rootPath: z.string().optional(),
527
+ globs: z.string().optional(),
528
+ pkgCwd: z.boolean().optional(),
529
+ });
530
+
531
+ /**
532
+ * Batch plugin for the GetDotenv CLI host.
533
+ *
534
+ * Mirrors the legacy batch subcommand behavior without altering the shipped CLI. * Options:
535
+ * - scripts/shell: used to resolve command and shell behavior per script or global default.
536
+ * - logger: defaults to console.
537
+ */
538
+ const batchPlugin = (opts = {}) => definePlugin({
539
+ id: 'batch',
540
+ // Host validates this when config-loader is enabled; plugins may also
541
+ // re-validate at action time as a safety belt.
542
+ configSchema: BatchConfigSchema,
543
+ setup(cli) {
544
+ const ns = cli.ns('batch');
545
+ const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
546
+ ns.description('Batch command execution across multiple working directories.')
547
+ .enablePositionalOptions()
548
+ .passThroughOptions()
549
+ .option('-p, --pkg-cwd', 'use nearest package directory as current working directory')
550
+ .option('-r, --root-path <string>', 'path to batch root directory from current working directory', './')
551
+ .option('-g, --globs <string>', 'space-delimited globs from root path', '*')
552
+ .option('-c, --command <string>', 'command executed according to the base shell resolution')
553
+ .option('-l, --list', 'list working directories without executing command')
554
+ .option('-e, --ignore-errors', 'ignore errors and continue with next path')
555
+ .argument('[command...]')
556
+ .addCommand(new Command()
557
+ .name('cmd')
558
+ .description('execute command, conflicts with --command option (default subcommand)')
559
+ .enablePositionalOptions()
560
+ .passThroughOptions()
561
+ .argument('[command...]')
562
+ .action(buildDefaultCmdAction(cli, batchCmd, opts)), { isDefault: true })
563
+ .action(buildParentAction(cli, opts));
564
+ },
565
+ });
566
+
567
+ export { batchPlugin };