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.
- package/README.md +718 -102
- package/dist/argfile.d.ts +55 -0
- package/dist/argfile.js +155 -0
- package/dist/completions.d.ts +4 -1
- package/dist/completions.js +199 -16
- package/dist/config.d.ts +75 -0
- package/dist/config.js +134 -0
- package/dist/help.d.ts +4 -4
- package/dist/help.js +276 -81
- package/dist/index.d.ts +2 -3
- package/dist/index.js +3 -3
- package/dist/install.d.ts +55 -0
- package/dist/install.js +185 -0
- package/dist/log.d.ts +78 -0
- package/dist/log.js +164 -0
- package/dist/man.d.ts +28 -0
- package/dist/man.js +234 -0
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.js +165 -0
- package/dist/output.d.ts +111 -0
- package/dist/output.js +356 -0
- package/dist/parser.d.ts +40 -19
- package/dist/parser.js +789 -461
- package/dist/plugins.d.ts +58 -0
- package/dist/plugins.js +145 -0
- package/dist/progress.d.ts +89 -0
- package/dist/progress.js +205 -0
- package/dist/prompt.d.ts +99 -0
- package/dist/prompt.js +299 -0
- package/dist/runner.d.ts +5 -2
- package/dist/runner.js +341 -135
- package/dist/spec.d.ts +83 -0
- package/dist/spec.js +124 -0
- package/dist/testing.d.ts +59 -0
- package/dist/testing.js +113 -0
- package/dist/types.d.ts +279 -11
- package/dist/validation.js +191 -69
- package/package.json +63 -6
|
@@ -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>;
|
package/dist/argfile.js
ADDED
|
@@ -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
|
+
}
|
package/dist/completions.d.ts
CHANGED
|
@@ -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
|
|
43
|
+
export declare function withCompletions(rootCommand: CommandDef<any>): CommandDef<any>;
|
package/dist/completions.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
for (const [subName, subDef] of Object.entries(command
|
|
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 '
|
|
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 = [
|
|
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:
|
|
690
|
+
description: `Target shell: ${VALID_SHELLS.join(', ')}`,
|
|
691
|
+
valueParser: [...VALID_SHELLS],
|
|
505
692
|
},
|
|
506
693
|
},
|
|
507
|
-
run({ args }) {
|
|
508
|
-
|
|
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
|
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
};
|