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
package/dist/spec.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A machine-readable description of a command tree.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { toSpec } from 'clap-ts/spec';
|
|
6
|
+
*
|
|
7
|
+
* writeFileSync('cli.json', JSON.stringify(toSpec(main), null, 2));
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Useful for generating a docs site, driving editor integration, or turning a
|
|
11
|
+
* CLI into tool definitions for something that calls it. The shape is plain
|
|
12
|
+
* JSON: no functions survive, so a `valueParser` function is reported only as
|
|
13
|
+
* the fact that one exists.
|
|
14
|
+
*/
|
|
15
|
+
import type { ArgDef, CommandDef, PossibleValue, ValueHint } from './types.js';
|
|
16
|
+
/** One argument, flattened to JSON. */
|
|
17
|
+
export interface ArgSpecJson {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly type: ArgDef['type'];
|
|
20
|
+
readonly description?: string;
|
|
21
|
+
readonly longDescription?: string;
|
|
22
|
+
readonly short?: string;
|
|
23
|
+
readonly long?: string;
|
|
24
|
+
readonly aliases?: readonly string[];
|
|
25
|
+
readonly visibleAliases?: readonly string[];
|
|
26
|
+
readonly valueName?: string;
|
|
27
|
+
readonly valueNames?: readonly string[];
|
|
28
|
+
readonly valueHint?: ValueHint;
|
|
29
|
+
readonly required: boolean;
|
|
30
|
+
readonly hidden: boolean;
|
|
31
|
+
readonly deprecated?: string | true;
|
|
32
|
+
readonly replacedBy?: string;
|
|
33
|
+
readonly global?: boolean;
|
|
34
|
+
readonly env?: string;
|
|
35
|
+
readonly default?: string | number | boolean | readonly string[];
|
|
36
|
+
readonly action?: ArgDef['action'];
|
|
37
|
+
readonly numArgs?: {
|
|
38
|
+
readonly min: number;
|
|
39
|
+
readonly max: number;
|
|
40
|
+
};
|
|
41
|
+
readonly possibleValues?: readonly PossibleValue[];
|
|
42
|
+
/** True when a function parser is attached; the function itself cannot serialise. */
|
|
43
|
+
readonly hasCustomParser?: boolean;
|
|
44
|
+
readonly conflictsWith?: readonly string[];
|
|
45
|
+
readonly requires?: readonly string[];
|
|
46
|
+
readonly groups?: readonly string[];
|
|
47
|
+
readonly helpHeading?: string;
|
|
48
|
+
}
|
|
49
|
+
/** One command, flattened to JSON, with its subcommands nested. */
|
|
50
|
+
export interface CommandSpecJson {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly path: readonly string[];
|
|
53
|
+
readonly description?: string;
|
|
54
|
+
readonly about?: string;
|
|
55
|
+
readonly longAbout?: string;
|
|
56
|
+
readonly version?: string;
|
|
57
|
+
readonly author?: string;
|
|
58
|
+
readonly aliases?: readonly string[];
|
|
59
|
+
readonly hidden: boolean;
|
|
60
|
+
readonly deprecated?: string | true;
|
|
61
|
+
readonly replacedBy?: string;
|
|
62
|
+
readonly usage: string;
|
|
63
|
+
readonly args: readonly ArgSpecJson[];
|
|
64
|
+
readonly subcommands: readonly CommandSpecJson[];
|
|
65
|
+
}
|
|
66
|
+
export interface SpecOptions {
|
|
67
|
+
/** Root name; defaults to the command's binName or name. */
|
|
68
|
+
readonly name?: string;
|
|
69
|
+
/** Include args and commands marked hidden (default false). */
|
|
70
|
+
readonly includeHidden?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Describe a command tree as plain JSON-safe data.
|
|
74
|
+
*
|
|
75
|
+
* Forces any `lazySubCommands` thunk, since the whole tree has to be walked.
|
|
76
|
+
*/
|
|
77
|
+
export declare function toSpec(command: CommandDef, opts?: SpecOptions): CommandSpecJson;
|
|
78
|
+
/** Describe a command tree as a JSON string. */
|
|
79
|
+
export declare function toSpecJson(command: CommandDef, opts?: SpecOptions & {
|
|
80
|
+
space?: number;
|
|
81
|
+
}): string;
|
|
82
|
+
/** Walk every command in a spec, root first. */
|
|
83
|
+
export declare function walkSpec(spec: CommandSpecJson): Generator<CommandSpecJson>;
|
package/dist/spec.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A machine-readable description of a command tree.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { toSpec } from 'clap-ts/spec';
|
|
6
|
+
*
|
|
7
|
+
* writeFileSync('cli.json', JSON.stringify(toSpec(main), null, 2));
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Useful for generating a docs site, driving editor integration, or turning a
|
|
11
|
+
* CLI into tool definitions for something that calls it. The shape is plain
|
|
12
|
+
* JSON: no functions survive, so a `valueParser` function is reported only as
|
|
13
|
+
* the fact that one exists.
|
|
14
|
+
*/
|
|
15
|
+
import { possibleValues, subCommandsOf } from './parser.js';
|
|
16
|
+
function defined(obj) {
|
|
17
|
+
for (const key of Object.keys(obj)) {
|
|
18
|
+
if (obj[key] === undefined) {
|
|
19
|
+
delete obj[key];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return obj;
|
|
23
|
+
}
|
|
24
|
+
function normalizeDeprecated(value) {
|
|
25
|
+
if (value === undefined || value === false) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return value === true ? true : value;
|
|
29
|
+
}
|
|
30
|
+
function argToJson(key, def) {
|
|
31
|
+
const values = possibleValues(def);
|
|
32
|
+
const groups = def.group === undefined ? def.groups : [def.group, ...(def.groups ?? [])];
|
|
33
|
+
return defined({
|
|
34
|
+
name: key,
|
|
35
|
+
type: def.type,
|
|
36
|
+
description: def.description,
|
|
37
|
+
longDescription: def.longDescription,
|
|
38
|
+
short: def.short,
|
|
39
|
+
long: def.long ?? (def.type === 'positional' ? undefined : key),
|
|
40
|
+
aliases: def.alias,
|
|
41
|
+
visibleAliases: def.visibleAlias,
|
|
42
|
+
valueName: def.valueName,
|
|
43
|
+
valueNames: def.valueNames,
|
|
44
|
+
valueHint: def.valueHint,
|
|
45
|
+
required: def.required === true,
|
|
46
|
+
hidden: def.hidden === true,
|
|
47
|
+
deprecated: normalizeDeprecated(def.deprecated),
|
|
48
|
+
replacedBy: def.replacedBy,
|
|
49
|
+
global: def.global,
|
|
50
|
+
env: def.env,
|
|
51
|
+
default: def.default,
|
|
52
|
+
action: def.action,
|
|
53
|
+
numArgs: def.numArgs,
|
|
54
|
+
possibleValues: values.length > 0 ? values : undefined,
|
|
55
|
+
hasCustomParser: typeof def.valueParser === 'function' ? true : undefined,
|
|
56
|
+
conflictsWith: def.conflictsWith,
|
|
57
|
+
requires: def.requires,
|
|
58
|
+
groups,
|
|
59
|
+
helpHeading: def.helpHeading,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function usageOf(path, command, args) {
|
|
63
|
+
const parts = [...path];
|
|
64
|
+
if (args.some((a) => a.type !== 'positional')) {
|
|
65
|
+
parts.push('[OPTIONS]');
|
|
66
|
+
}
|
|
67
|
+
for (const arg of args) {
|
|
68
|
+
if (arg.type !== 'positional') {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const name = `<${arg.valueName ?? arg.name.toUpperCase()}>`;
|
|
72
|
+
parts.push(arg.required ? name : `[${name}]`);
|
|
73
|
+
}
|
|
74
|
+
if (Object.keys(subCommandsOf(command)).length > 0) {
|
|
75
|
+
parts.push(`[${command.meta.subcommandValueName ?? 'COMMAND'}]`);
|
|
76
|
+
}
|
|
77
|
+
return parts.join(' ');
|
|
78
|
+
}
|
|
79
|
+
function commandToJson(command, path, includeHidden) {
|
|
80
|
+
const argsDef = command.args ?? {};
|
|
81
|
+
const args = Object.entries(argsDef)
|
|
82
|
+
.filter(([, def]) => includeHidden || def.hidden !== true)
|
|
83
|
+
.map(([key, def]) => argToJson(key, def));
|
|
84
|
+
const subcommands = Object.entries(subCommandsOf(command))
|
|
85
|
+
.filter(([, sub]) => includeHidden || sub.meta.hidden !== true)
|
|
86
|
+
.map(([name, sub]) => commandToJson(sub, [...path, name], includeHidden));
|
|
87
|
+
const { meta } = command;
|
|
88
|
+
return defined({
|
|
89
|
+
name: meta.name,
|
|
90
|
+
path,
|
|
91
|
+
description: meta.description,
|
|
92
|
+
about: meta.about,
|
|
93
|
+
longAbout: meta.longAbout,
|
|
94
|
+
version: meta.version,
|
|
95
|
+
author: meta.author,
|
|
96
|
+
aliases: meta.aliases,
|
|
97
|
+
hidden: meta.hidden === true,
|
|
98
|
+
deprecated: normalizeDeprecated(meta.deprecated),
|
|
99
|
+
replacedBy: meta.replacedBy,
|
|
100
|
+
usage: usageOf(path, command, args),
|
|
101
|
+
args,
|
|
102
|
+
subcommands,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Describe a command tree as plain JSON-safe data.
|
|
107
|
+
*
|
|
108
|
+
* Forces any `lazySubCommands` thunk, since the whole tree has to be walked.
|
|
109
|
+
*/
|
|
110
|
+
export function toSpec(command, opts) {
|
|
111
|
+
const name = opts?.name ?? command.meta.binName ?? command.meta.name;
|
|
112
|
+
return commandToJson(command, [name], opts?.includeHidden === true);
|
|
113
|
+
}
|
|
114
|
+
/** Describe a command tree as a JSON string. */
|
|
115
|
+
export function toSpecJson(command, opts) {
|
|
116
|
+
return JSON.stringify(toSpec(command, opts), null, opts?.space ?? 2);
|
|
117
|
+
}
|
|
118
|
+
/** Walk every command in a spec, root first. */
|
|
119
|
+
export function* walkSpec(spec) {
|
|
120
|
+
yield spec;
|
|
121
|
+
for (const sub of spec.subcommands) {
|
|
122
|
+
yield* walkSpec(sub);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers: run a command definition and get back what it wrote and the
|
|
3
|
+
* code it settled on, without spawning a process or stubbing globals.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { runCli } from 'clap-ts/testing';
|
|
7
|
+
*
|
|
8
|
+
* const result = await runCli(main, ['serve', '--port', '8080']);
|
|
9
|
+
* expect(result.exitCode).toBe(0);
|
|
10
|
+
* expect(result.stdout).toContain('listening');
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import type { CommandDef, CommandContext, RunOptions } from './types.js';
|
|
14
|
+
/** Everything a run produced. */
|
|
15
|
+
export interface CliResult {
|
|
16
|
+
/** Text written to stdout, with ANSI escapes intact. */
|
|
17
|
+
readonly stdout: string;
|
|
18
|
+
/** Text written to stderr, with ANSI escapes intact. */
|
|
19
|
+
readonly stderr: string;
|
|
20
|
+
/** stdout with ANSI escapes removed, for readable assertions. */
|
|
21
|
+
readonly plainStdout: string;
|
|
22
|
+
/** stderr with ANSI escapes removed. */
|
|
23
|
+
readonly plainStderr: string;
|
|
24
|
+
/** The code the run settled on. 0 unless something failed. */
|
|
25
|
+
readonly exitCode: number;
|
|
26
|
+
/** An error that escaped the command handler, if any. */
|
|
27
|
+
readonly error?: unknown;
|
|
28
|
+
}
|
|
29
|
+
/** Strip ANSI colour codes from text. */
|
|
30
|
+
export declare function stripAnsi(text: string): string;
|
|
31
|
+
/** Options for runCli, minus the ones it controls itself. */
|
|
32
|
+
export type RunCliOptions = Omit<RunOptions, 'argv' | 'exit' | 'stdout' | 'stderr' | 'onExit'>;
|
|
33
|
+
/**
|
|
34
|
+
* Run a command against an argv and capture the result.
|
|
35
|
+
*
|
|
36
|
+
* The process is never exited and no global is patched: output goes to
|
|
37
|
+
* collectors and the exit code is observed through `onExit`. An error thrown by
|
|
38
|
+
* a handler is returned on `error` rather than rethrown, so one assertion style
|
|
39
|
+
* covers success and failure alike.
|
|
40
|
+
*/
|
|
41
|
+
export declare function runCli(command: CommandDef<any>, argv?: readonly string[], opts?: RunCliOptions): Promise<CliResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Capture the args the handler that actually runs receives, without running its
|
|
44
|
+
* body. Every `run` in the tree is replaced, so this works for a subcommand as
|
|
45
|
+
* well as the root.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const { args } = await captureArgs(main, ['serve', '--port', '9']);
|
|
49
|
+
* expect(args.port).toBe(9);
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Note this forces any `lazySubCommands` thunk, since the tree has to be walked
|
|
53
|
+
* to be probed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function captureArgs(command: CommandDef<any>, argv?: readonly string[], opts?: RunCliOptions): Promise<{
|
|
56
|
+
args: Record<string, unknown>;
|
|
57
|
+
context?: CommandContext<any>;
|
|
58
|
+
result: CliResult;
|
|
59
|
+
}>;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers: run a command definition and get back what it wrote and the
|
|
3
|
+
* code it settled on, without spawning a process or stubbing globals.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { runCli } from 'clap-ts/testing';
|
|
7
|
+
*
|
|
8
|
+
* const result = await runCli(main, ['serve', '--port', '8080']);
|
|
9
|
+
* expect(result.exitCode).toBe(0);
|
|
10
|
+
* expect(result.stdout).toContain('listening');
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import { runMain } from './runner.js';
|
|
14
|
+
import { subCommandsOf, hasSubCommands } from './parser.js';
|
|
15
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
16
|
+
/** Strip ANSI colour codes from text. */
|
|
17
|
+
export function stripAnsi(text) {
|
|
18
|
+
return text.replace(ANSI, '');
|
|
19
|
+
}
|
|
20
|
+
/** A sink that keeps everything written to it. */
|
|
21
|
+
function collector() {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
return {
|
|
24
|
+
write(chunk) {
|
|
25
|
+
chunks.push(chunk);
|
|
26
|
+
},
|
|
27
|
+
text() {
|
|
28
|
+
return chunks.join('');
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run a command against an argv and capture the result.
|
|
34
|
+
*
|
|
35
|
+
* The process is never exited and no global is patched: output goes to
|
|
36
|
+
* collectors and the exit code is observed through `onExit`. An error thrown by
|
|
37
|
+
* a handler is returned on `error` rather than rethrown, so one assertion style
|
|
38
|
+
* covers success and failure alike.
|
|
39
|
+
*/
|
|
40
|
+
export async function runCli(command, argv = [], opts) {
|
|
41
|
+
const out = collector();
|
|
42
|
+
const err = collector();
|
|
43
|
+
let exitCode = 0;
|
|
44
|
+
let error;
|
|
45
|
+
try {
|
|
46
|
+
await runMain(command, {
|
|
47
|
+
...opts,
|
|
48
|
+
argv,
|
|
49
|
+
exit: false,
|
|
50
|
+
stdout: out,
|
|
51
|
+
stderr: err,
|
|
52
|
+
onExit: (code) => {
|
|
53
|
+
exitCode = code;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (caught) {
|
|
58
|
+
error = caught;
|
|
59
|
+
if (exitCode === 0) {
|
|
60
|
+
exitCode = 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const stdout = out.text();
|
|
64
|
+
const stderr = err.text();
|
|
65
|
+
return {
|
|
66
|
+
stdout,
|
|
67
|
+
stderr,
|
|
68
|
+
plainStdout: stripAnsi(stdout),
|
|
69
|
+
plainStderr: stripAnsi(stderr),
|
|
70
|
+
exitCode,
|
|
71
|
+
...(error === undefined ? {} : { error }),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Replace every `run` in the tree with the same probe, so whichever command the
|
|
76
|
+
* argv resolves to is the one observed.
|
|
77
|
+
*/
|
|
78
|
+
function probeTree(command, onRun) {
|
|
79
|
+
const probed = {};
|
|
80
|
+
if (hasSubCommands(command)) {
|
|
81
|
+
for (const [name, sub] of Object.entries(subCommandsOf(command))) {
|
|
82
|
+
probed[name] = probeTree(sub, onRun);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...command,
|
|
87
|
+
run: onRun,
|
|
88
|
+
...(hasSubCommands(command) ? { subCommands: probed, lazySubCommands: undefined } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Capture the args the handler that actually runs receives, without running its
|
|
93
|
+
* body. Every `run` in the tree is replaced, so this works for a subcommand as
|
|
94
|
+
* well as the root.
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* const { args } = await captureArgs(main, ['serve', '--port', '9']);
|
|
98
|
+
* expect(args.port).toBe(9);
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* Note this forces any `lazySubCommands` thunk, since the tree has to be walked
|
|
102
|
+
* to be probed.
|
|
103
|
+
*/
|
|
104
|
+
export async function captureArgs(command, argv = [], opts) {
|
|
105
|
+
let args = {};
|
|
106
|
+
let context;
|
|
107
|
+
const probe = probeTree(command, (ctx) => {
|
|
108
|
+
args = ctx.args;
|
|
109
|
+
context = ctx;
|
|
110
|
+
});
|
|
111
|
+
const result = await runCli(probe, argv, opts);
|
|
112
|
+
return { args, ...(context === undefined ? {} : { context }), result };
|
|
113
|
+
}
|