clap-ts 0.2.0 → 0.3.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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Response files and stdin, the `@file` convention git, gcc and java use for
3
+ * command lines too long to type or to pass through the shell's ARG_MAX.
4
+ *
5
+ * ```ts
6
+ * import { expandArgFiles, readStdin } from 'clap-ts/argfile';
7
+ *
8
+ * await runMain(main, { argv: expandArgFiles() });
9
+ * ```
10
+ *
11
+ * clap has no equivalent, so the shape here follows gcc: one argument per line,
12
+ * `#` comments and blank lines skipped, quoted runs kept together, and a
13
+ * literal `@` escaped as `@@`.
14
+ */
15
+ export interface ArgFileOptions {
16
+ /** Prefix marking a response file (default '@'). */
17
+ readonly prefix?: string;
18
+ /** How deep a response file may reference another (default 5). */
19
+ readonly maxDepth?: number;
20
+ /** Read a file's text. Defaults to reading UTF-8 from disk. */
21
+ readonly read?: (path: string) => string;
22
+ }
23
+ /**
24
+ * Split response-file text into arguments.
25
+ *
26
+ * Whitespace separates, `#` at the start of a line comments the rest of it out,
27
+ * and single or double quotes group a run containing spaces. A backslash
28
+ * escapes the next character inside or outside quotes.
29
+ */
30
+ export declare function parseArgFile(text: string): string[];
31
+ /**
32
+ * Replace every `@file` in argv with that file's arguments.
33
+ *
34
+ * Reads `process.argv` when given nothing. A response file may reference
35
+ * another up to `maxDepth`; `@@` is a literal argument starting with `@`, and
36
+ * everything after a bare `--` is left alone.
37
+ */
38
+ export declare function expandArgFiles(argv?: readonly string[], opts?: ArgFileOptions): string[];
39
+ /**
40
+ * Read all of stdin as text, for the `-` convention meaning "read from stdin".
41
+ *
42
+ * Resolves to undefined when stdin is a terminal, so an interactive run does
43
+ * not hang waiting for input that is never coming.
44
+ */
45
+ export declare function readStdin(): Promise<string | undefined>;
46
+ /**
47
+ * Resolve a path argument, reading stdin when it is `-`.
48
+ *
49
+ * ```ts
50
+ * const source = await readPathOrStdin(args.input);
51
+ * ```
52
+ */
53
+ export declare function readPathOrStdin(path: string, opts?: {
54
+ readonly read?: (path: string) => string;
55
+ }): Promise<string>;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Response files and stdin, the `@file` convention git, gcc and java use for
3
+ * command lines too long to type or to pass through the shell's ARG_MAX.
4
+ *
5
+ * ```ts
6
+ * import { expandArgFiles, readStdin } from 'clap-ts/argfile';
7
+ *
8
+ * await runMain(main, { argv: expandArgFiles() });
9
+ * ```
10
+ *
11
+ * clap has no equivalent, so the shape here follows gcc: one argument per line,
12
+ * `#` comments and blank lines skipped, quoted runs kept together, and a
13
+ * literal `@` escaped as `@@`.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { getRawArgs } from './parser.js';
17
+ /**
18
+ * Split response-file text into arguments.
19
+ *
20
+ * Whitespace separates, `#` at the start of a line comments the rest of it out,
21
+ * and single or double quotes group a run containing spaces. A backslash
22
+ * escapes the next character inside or outside quotes.
23
+ */
24
+ export function parseArgFile(text) {
25
+ const args = [];
26
+ let current = '';
27
+ let quote;
28
+ let hasToken = false;
29
+ for (let i = 0; i < text.length; i++) {
30
+ const ch = text[i];
31
+ if (ch === '\\' && i + 1 < text.length) {
32
+ current += text[i + 1];
33
+ hasToken = true;
34
+ i++;
35
+ continue;
36
+ }
37
+ if (quote !== undefined) {
38
+ if (ch === quote) {
39
+ quote = undefined;
40
+ }
41
+ else {
42
+ current += ch;
43
+ }
44
+ continue;
45
+ }
46
+ if (ch === '"' || ch === "'") {
47
+ quote = ch;
48
+ hasToken = true;
49
+ continue;
50
+ }
51
+ // A comment runs to the end of its line, and only starts a token boundary.
52
+ if (ch === '#' && !hasToken) {
53
+ while (i < text.length && text[i] !== '\n') {
54
+ i++;
55
+ }
56
+ continue;
57
+ }
58
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
59
+ if (hasToken) {
60
+ args.push(current);
61
+ current = '';
62
+ hasToken = false;
63
+ }
64
+ continue;
65
+ }
66
+ current += ch;
67
+ hasToken = true;
68
+ }
69
+ if (quote !== undefined) {
70
+ throw new Error(`unterminated ${quote === '"' ? 'double' : 'single'} quote in response file`);
71
+ }
72
+ if (hasToken) {
73
+ args.push(current);
74
+ }
75
+ return args;
76
+ }
77
+ /**
78
+ * Replace every `@file` in argv with that file's arguments.
79
+ *
80
+ * Reads `process.argv` when given nothing. A response file may reference
81
+ * another up to `maxDepth`; `@@` is a literal argument starting with `@`, and
82
+ * everything after a bare `--` is left alone.
83
+ */
84
+ export function expandArgFiles(argv = getRawArgs(), opts) {
85
+ const prefix = opts?.prefix ?? '@';
86
+ const maxDepth = opts?.maxDepth ?? 5;
87
+ const read = opts?.read ?? ((path) => readFileSync(path, 'utf8'));
88
+ const expand = (tokens, depth) => {
89
+ const out = [];
90
+ let escaped = false;
91
+ for (const token of tokens) {
92
+ if (escaped || !token.startsWith(prefix) || token.length === prefix.length) {
93
+ out.push(token);
94
+ if (token === '--') {
95
+ escaped = true;
96
+ }
97
+ continue;
98
+ }
99
+ // `@@file` means a literal argument that happens to start with `@`.
100
+ if (token.startsWith(prefix + prefix)) {
101
+ out.push(token.slice(prefix.length));
102
+ continue;
103
+ }
104
+ if (depth >= maxDepth) {
105
+ throw new Error(`response files nested more than ${String(maxDepth)} deep at '${token}'`);
106
+ }
107
+ const path = token.slice(prefix.length);
108
+ let text;
109
+ try {
110
+ text = read(path);
111
+ }
112
+ catch (error) {
113
+ const message = error instanceof Error ? error.message : String(error);
114
+ throw new Error(`cannot read response file '${path}': ${message}`);
115
+ }
116
+ out.push(...expand(parseArgFile(text), depth + 1));
117
+ }
118
+ return out;
119
+ };
120
+ return expand(argv, 0);
121
+ }
122
+ /**
123
+ * Read all of stdin as text, for the `-` convention meaning "read from stdin".
124
+ *
125
+ * Resolves to undefined when stdin is a terminal, so an interactive run does
126
+ * not hang waiting for input that is never coming.
127
+ */
128
+ export async function readStdin() {
129
+ if (process.stdin.isTTY === true) {
130
+ return undefined;
131
+ }
132
+ const chunks = [];
133
+ for await (const chunk of process.stdin) {
134
+ chunks.push(chunk);
135
+ }
136
+ return Buffer.concat(chunks).toString('utf8');
137
+ }
138
+ /**
139
+ * Resolve a path argument, reading stdin when it is `-`.
140
+ *
141
+ * ```ts
142
+ * const source = await readPathOrStdin(args.input);
143
+ * ```
144
+ */
145
+ export async function readPathOrStdin(path, opts) {
146
+ if (path === '-') {
147
+ const text = await readStdin();
148
+ if (text === undefined) {
149
+ throw new Error('reading from stdin was requested but stdin is a terminal');
150
+ }
151
+ return text;
152
+ }
153
+ const read = opts?.read ?? ((p) => readFileSync(p, 'utf8'));
154
+ return read(path);
155
+ }
@@ -7,6 +7,7 @@
7
7
  * 2. Dynamic: completeEnv() checks env vars, outputs completions, returns true if handled
8
8
  */
9
9
  import type { CommandDef, Shell } from './types.js';
10
+ export type { Shell, ValueHint } from './types.js';
10
11
  /**
11
12
  * Generate a shell completion script for the given command and shell.
12
13
  *
@@ -21,6 +22,8 @@ import type { CommandDef, Shell } from './types.js';
21
22
  * - zsh: copy to a directory in `$fpath` (e.g., `~/.zsh/completions/`)
22
23
  * - fish: copy to `~/.config/fish/completions/`
23
24
  * - powershell: add to `$PROFILE`
25
+ * - elvish: source from `~/.config/elvish/rc.elv`
26
+ * - nushell: save and `source` it from `$nu.config-path`
24
27
  */
25
28
  export declare function generateCompletions(command: CommandDef, shell: Shell, binaryName?: string): string;
26
29
  /**
@@ -37,4 +40,4 @@ export declare function generateCompletions(command: CommandDef, shell: Shell, b
37
40
  * eval "$(my-cli completions bash)"
38
41
  * ```
39
42
  */
40
- export declare function withCompletions<T extends CommandDef>(rootCommand: T): T;
43
+ export declare function withCompletions(rootCommand: CommandDef<any>): CommandDef<any>;
@@ -6,12 +6,25 @@
6
6
  * 1. Static: generateCompletions() returns a shell script string to source
7
7
  * 2. Dynamic: completeEnv() checks env vars, outputs completions, returns true if handled
8
8
  */
9
+ import { possibleValues, subCommandsOf } from './parser.js';
9
10
  /** Extract completion-relevant data from a CommandDef tree. */
10
11
  function buildCompletionTree(command, name) {
11
12
  const flags = [];
13
+ const positionals = [];
12
14
  const argsDef = command.args ?? {};
13
15
  for (const [key, def] of Object.entries(argsDef)) {
14
16
  if (def.type === 'positional') {
17
+ positionals.push({
18
+ key,
19
+ name: def.valueName ?? key,
20
+ description: def.description ?? '',
21
+ required: def.required ?? false,
22
+ possibleValues: possibleValues(def)
23
+ .filter((v) => !v.hidden)
24
+ .map((v) => v.name),
25
+ valueHint: def.valueHint,
26
+ hidden: def.hidden ?? false,
27
+ });
15
28
  continue;
16
29
  }
17
30
  const longName = def.long ?? key;
@@ -21,20 +34,24 @@ function buildCompletionTree(command, name) {
21
34
  long: longName,
22
35
  description: def.description ?? '',
23
36
  takesValue: def.type !== 'boolean' && def.action !== 'count',
24
- possibleValues: Array.isArray(def.valueParser) ? def.valueParser : [],
37
+ possibleValues: possibleValues(def)
38
+ .filter((v) => !v.hidden)
39
+ .map((v) => v.name),
25
40
  valueHint: def.valueHint,
26
41
  hidden: def.hidden ?? false,
27
42
  });
28
43
  }
29
44
  // Always include --help and --version
30
- flags.push({ key: 'help', short: 'h', long: 'help', description: 'Print help', takesValue: false, possibleValues: [], hidden: false });
31
- if (command.meta.version) {
45
+ if (command.meta.disableHelpFlag !== true) {
46
+ flags.push({ key: 'help', short: 'h', long: 'help', description: 'Print help', takesValue: false, possibleValues: [], hidden: false });
47
+ }
48
+ if (command.meta.version && command.meta.disableVersionFlag !== true) {
32
49
  flags.push({ key: 'version', short: 'V', long: 'version', description: 'Print version', takesValue: false, possibleValues: [], hidden: false });
33
50
  }
34
51
  const subcommands = [];
35
52
  const childNodes = new Map();
36
- if (command.subCommands) {
37
- for (const [subName, subDef] of Object.entries(command.subCommands)) {
53
+ {
54
+ for (const [subName, subDef] of Object.entries(subCommandsOf(command))) {
38
55
  subcommands.push({
39
56
  name: subName,
40
57
  description: subDef.meta.description ?? '',
@@ -46,7 +63,9 @@ function buildCompletionTree(command, name) {
46
63
  }
47
64
  return {
48
65
  name: name ?? command.meta.name,
66
+ description: command.meta.description ?? command.meta.about ?? '',
49
67
  flags,
68
+ positionals,
50
69
  subcommands,
51
70
  childNodes,
52
71
  };
@@ -285,7 +304,12 @@ function zshValueHintSpec(hint, key) {
285
304
  case 'hostname': return `:${key}:_hosts`;
286
305
  case 'username': return `:${key}:_users`;
287
306
  case 'url': return `:${key}:_urls`;
288
- case 'emailAddress': return `:${key}:`;
307
+ case 'commandString': return `:${key}:_cmdstring`;
308
+ case 'commandWithArguments': return `:${key}:_command_names -e`;
309
+ case 'emailAddress':
310
+ case 'other':
311
+ case 'unknown':
312
+ return `:${key}:`;
289
313
  }
290
314
  }
291
315
  // ---- Fish Generator ----
@@ -434,7 +458,6 @@ function generatePowerShellNode(node, indent, tokensVar, depth, lines) {
434
458
  // Complete flags and subcommands at this level
435
459
  const completions = [];
436
460
  for (const f of visibleFlags) {
437
- const desc = escDq(f.description);
438
461
  completions.push(`${indent}[System.Management.Automation.CompletionResult]::new('--${f.long}', '--${f.long}', 'ParameterName', '${escDq(f.description)}')`);
439
462
  if (f.short) {
440
463
  completions.push(`${indent}[System.Management.Automation.CompletionResult]::new('-${f.short}', '-${f.short}', 'ParameterName', '${escDq(f.description)}')`);
@@ -447,6 +470,158 @@ function generatePowerShellNode(node, indent, tokensVar, depth, lines) {
447
470
  lines.push(c);
448
471
  }
449
472
  }
473
+ // ---- Elvish Generator ----
474
+ /**
475
+ * Elvish completions are a map from command path (segments joined by ';') to a
476
+ * lambda emitting candidates, which is how clap_complete shapes them: the
477
+ * completer walks the words it has seen to build the key, then calls it.
478
+ */
479
+ function generateElvish(root, binaryName) {
480
+ const cases = [];
481
+ collectElvishCases(root, [binaryName], cases);
482
+ return [
483
+ `# elvish completion for ${binaryName}`,
484
+ '# Generated by clap-ts',
485
+ '',
486
+ 'use builtin;',
487
+ 'use str;',
488
+ '',
489
+ `set edit:completion:arg-completer[${binaryName}] = {|@words|`,
490
+ ' fn spaces {|n|',
491
+ " builtin:repeat $n ' ' | str:join ''",
492
+ ' }',
493
+ ' fn cand {|text desc|',
494
+ " edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc",
495
+ ' }',
496
+ ` var command = '${esc(binaryName)}'`,
497
+ ' for word $words[1..-1] {',
498
+ " if (str:has-prefix $word '-') {",
499
+ ' break',
500
+ ' }',
501
+ " set command = $command';'$word",
502
+ ' }',
503
+ ' var completions = [',
504
+ ...cases,
505
+ ' ]',
506
+ ' if (has-key $completions $command) {',
507
+ ' $completions[$command]',
508
+ ' }',
509
+ '}',
510
+ '',
511
+ ].join('\n');
512
+ }
513
+ function collectElvishCases(node, path, cases) {
514
+ const key = path.join(';');
515
+ cases.push(` &'${esc(key)}'= {`);
516
+ for (const f of node.flags) {
517
+ if (f.hidden) {
518
+ continue;
519
+ }
520
+ const desc = esc(f.description.replaceAll('\n', ' '));
521
+ if (f.short) {
522
+ cases.push(` cand -${f.short} '${desc}'`);
523
+ }
524
+ cases.push(` cand --${f.long} '${desc}'`);
525
+ }
526
+ for (const sub of node.subcommands) {
527
+ if (sub.hidden) {
528
+ continue;
529
+ }
530
+ const desc = esc(sub.description.replaceAll('\n', ' '));
531
+ for (const name of [sub.name, ...sub.aliases]) {
532
+ cases.push(` cand ${name} '${desc}'`);
533
+ }
534
+ }
535
+ cases.push(' }');
536
+ for (const sub of node.subcommands) {
537
+ const child = node.childNodes.get(sub.name);
538
+ if (child) {
539
+ collectElvishCases(child, [...path, sub.name], cases);
540
+ }
541
+ }
542
+ }
543
+ // ---- Nushell Generator ----
544
+ /**
545
+ * Nushell completes through `extern` declarations, one per command path, with
546
+ * a `nu-complete` helper per argument that restricts its values.
547
+ */
548
+ function generateNushell(root, binaryName) {
549
+ const lines = [];
550
+ lines.push(`# nushell completion for ${binaryName}`);
551
+ lines.push('# Generated by clap-ts');
552
+ lines.push('');
553
+ lines.push('module completions {');
554
+ lines.push('');
555
+ collectNushellCommand(root, [binaryName], lines);
556
+ lines.push('}');
557
+ lines.push('');
558
+ lines.push('export use completions *');
559
+ lines.push('');
560
+ return lines.join('\n');
561
+ }
562
+ /** Nushell type annotation for a value, from its hint. */
563
+ function nushellType(hint) {
564
+ switch (hint) {
565
+ case 'filePath':
566
+ case 'anyPath':
567
+ case 'executablePath':
568
+ return 'path';
569
+ case 'dirPath':
570
+ return 'directory';
571
+ default:
572
+ return 'string';
573
+ }
574
+ }
575
+ function collectNushellCommand(node, path, lines) {
576
+ const commandPath = path.join(' ');
577
+ const visibleFlags = node.flags.filter((f) => !f.hidden);
578
+ const visiblePositionals = node.positionals.filter((a) => !a.hidden);
579
+ // Value completers first: nushell resolves them by name within the module.
580
+ for (const arg of [...visibleFlags, ...visiblePositionals]) {
581
+ if (arg.possibleValues.length === 0) {
582
+ continue;
583
+ }
584
+ lines.push(` def "nu-complete ${commandPath} ${arg.key}" [] {`);
585
+ lines.push(` [ ${arg.possibleValues.map((v) => `"${escDq(v)}"`).join(' ')} ]`);
586
+ lines.push(' }');
587
+ lines.push('');
588
+ }
589
+ if (node.description) {
590
+ lines.push(` # ${node.description.replaceAll('\n', ' ')}`);
591
+ }
592
+ lines.push(` export extern "${commandPath}" [`);
593
+ for (const f of visibleFlags) {
594
+ const short = f.short ? `(-${f.short})` : '';
595
+ let value = '';
596
+ if (f.takesValue) {
597
+ value =
598
+ f.possibleValues.length > 0
599
+ ? `: string@"nu-complete ${commandPath} ${f.key}"`
600
+ : `: ${nushellType(f.valueHint)}`;
601
+ }
602
+ const comment = f.description ? ` # ${f.description.replaceAll('\n', ' ')}` : '';
603
+ lines.push(` --${f.long}${short}${value}${comment}`);
604
+ }
605
+ for (const arg of visiblePositionals) {
606
+ const optional = arg.required ? '' : '?';
607
+ const type = arg.possibleValues.length > 0
608
+ ? `string@"nu-complete ${commandPath} ${arg.key}"`
609
+ : nushellType(arg.valueHint);
610
+ const comment = arg.description ? ` # ${arg.description.replaceAll('\n', ' ')}` : '';
611
+ lines.push(` ${arg.name}${optional}: ${type}${comment}`);
612
+ }
613
+ lines.push(' ]');
614
+ lines.push('');
615
+ for (const sub of node.subcommands) {
616
+ if (sub.hidden) {
617
+ continue;
618
+ }
619
+ const child = node.childNodes.get(sub.name);
620
+ if (child) {
621
+ collectNushellCommand(child, [...path, sub.name], lines);
622
+ }
623
+ }
624
+ }
450
625
  // ---- Public API: Static Generation ----
451
626
  /**
452
627
  * Generate a shell completion script for the given command and shell.
@@ -462,6 +637,8 @@ function generatePowerShellNode(node, indent, tokensVar, depth, lines) {
462
637
  * - zsh: copy to a directory in `$fpath` (e.g., `~/.zsh/completions/`)
463
638
  * - fish: copy to `~/.config/fish/completions/`
464
639
  * - powershell: add to `$PROFILE`
640
+ * - elvish: source from `~/.config/elvish/rc.elv`
641
+ * - nushell: save and `source` it from `$nu.config-path`
465
642
  */
466
643
  export function generateCompletions(command, shell, binaryName) {
467
644
  const name = binaryName ?? command.meta.name;
@@ -471,10 +648,19 @@ export function generateCompletions(command, shell, binaryName) {
471
648
  case 'zsh': return generateZsh(root, name);
472
649
  case 'fish': return generateFish(root, name);
473
650
  case 'powershell': return generatePowerShell(root, name);
651
+ case 'elvish': return generateElvish(root, name);
652
+ case 'nushell': return generateNushell(root, name);
474
653
  }
475
654
  }
476
655
  // ---- Public API: Auto-inject completions subcommand ----
477
- const VALID_SHELLS = ['bash', 'zsh', 'fish', 'powershell'];
656
+ const VALID_SHELLS = [
657
+ 'bash',
658
+ 'zsh',
659
+ 'fish',
660
+ 'powershell',
661
+ 'elvish',
662
+ 'nushell',
663
+ ];
478
664
  /**
479
665
  * Return a new command with a `completions` subcommand auto-injected.
480
666
  * The subcommand generates shell completion scripts when invoked.
@@ -501,16 +687,12 @@ export function withCompletions(rootCommand) {
501
687
  type: 'positional',
502
688
  valueName: 'SHELL',
503
689
  required: true,
504
- description: 'Target shell: bash, zsh, fish, or powershell',
690
+ description: `Target shell: ${VALID_SHELLS.join(', ')}`,
691
+ valueParser: [...VALID_SHELLS],
505
692
  },
506
693
  },
507
- run({ args }) {
508
- const shell = String(args['shell']);
509
- if (!VALID_SHELLS.includes(shell)) {
510
- process.stderr.write(`error: invalid shell '${shell}'. Valid options: ${VALID_SHELLS.join(', ')}\n`);
511
- process.exit(2);
512
- }
513
- process.stdout.write(generateCompletions(rootCommand, shell));
694
+ run({ args, stdout }) {
695
+ stdout.write(generateCompletions(rootCommand, String(args['shell'])));
514
696
  },
515
697
  };
516
698
  return {
@@ -519,5 +701,6 @@ export function withCompletions(rootCommand) {
519
701
  ...rootCommand.subCommands,
520
702
  completions: completionsCmd,
521
703
  },
704
+ lazySubCommands: rootCommand.lazySubCommands,
522
705
  };
523
706
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Configuration file loading, layered under the command line and environment.
3
+ *
4
+ * ```ts
5
+ * import { loadConfig } from 'clap-ts/config';
6
+ *
7
+ * const config = loadConfig('mytool');
8
+ * await runMain(main, { config: config?.values });
9
+ * ```
10
+ *
11
+ * Precedence ends up as command line, then environment, then config file, then
12
+ * the argument's own default. `ctx.valueSources` reports which one won.
13
+ *
14
+ * Only JSON is understood out of the box, which keeps this dependency-free.
15
+ * Point `parse` at a TOML or YAML reader to accept those.
16
+ *
17
+ * The search costs one `existsSync` per candidate per directory, so it is
18
+ * O(directories x candidates) and independent of how many files those
19
+ * directories hold. Listing each directory once instead would be one syscall
20
+ * per level, but `readdirSync` is O(entries): measured against a 2000-entry
21
+ * directory it took 117us where four `existsSync` calls took 2.4us. Walking up
22
+ * through a large directory is exactly the case that has to stay cheap.
23
+ */
24
+ /** A loaded configuration file. */
25
+ export interface LoadedConfig {
26
+ /** The parsed contents, ready for `RunOptions.config`. */
27
+ readonly values: Record<string, unknown>;
28
+ /** Absolute path of the file the values came from. */
29
+ readonly path: string;
30
+ }
31
+ export interface ConfigOptions {
32
+ /**
33
+ * File names to look for, in order of preference. Defaults to
34
+ * `.<name>rc`, `.<name>rc.json`, `<name>.config.json` and `.config/<name>.json`.
35
+ */
36
+ readonly files?: readonly string[];
37
+ /** Directory to start searching from (default: `process.cwd()`). */
38
+ readonly cwd?: string;
39
+ /** Stop searching at this directory, inclusive (default: the home directory). */
40
+ readonly stopAt?: string;
41
+ /**
42
+ * Read this key out of a `package.json` found during the walk. Defaults to
43
+ * the tool name; pass `null` to skip package.json entirely.
44
+ */
45
+ readonly packageJsonKey?: string | null;
46
+ /** Parse a file's text. Defaults to `JSON.parse`. */
47
+ readonly parse?: (text: string, path: string) => unknown;
48
+ /** Load exactly this file and skip the search. */
49
+ readonly path?: string;
50
+ /** Search parent directories as well as `cwd` (default: true). */
51
+ readonly searchParents?: boolean;
52
+ /**
53
+ * Stop once a directory holding `package.json` or `.git` has been examined.
54
+ * Cuts the walk to the project, which is where a project's config lives.
55
+ */
56
+ readonly stopAtProjectRoot?: boolean;
57
+ }
58
+ /**
59
+ * Find and read the nearest configuration file, walking up from `cwd`.
60
+ *
61
+ * Returns `undefined` when nothing is found, and throws only when a file exists
62
+ * but cannot be read or parsed: a broken config should be loud, a missing one
63
+ * should not.
64
+ */
65
+ export declare function loadConfig(name: string, opts?: ConfigOptions): LoadedConfig | undefined;
66
+ /**
67
+ * Load a config and hand back the run options to spread into `runMain`.
68
+ *
69
+ * ```ts
70
+ * await runMain(main, { ...configOptions('mytool') });
71
+ * ```
72
+ */
73
+ export declare function configOptions(name: string, opts?: ConfigOptions): {
74
+ config: () => Record<string, unknown> | undefined;
75
+ };