@sdxc/cli 2026.10.16 → 2026.10.23
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 +14 -1
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +51 -1
- package/dist/describe.d.ts +72 -0
- package/dist/describe.js +88 -0
- package/dist/exec.js +27 -20
- package/dist/help.js +8 -5
- package/dist/index.d.ts +2 -1
- package/dist/interactions.js +25 -14
- package/dist/keys.js +8 -1
- package/dist/parse.js +14 -3
- package/dist/resolve.d.ts +1 -1
- package/dist/resolve.js +17 -14
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ What to ask when a flag or argument is missing on a terminal: `prompt.select({ m
|
|
|
157
157
|
|
|
158
158
|
#### `createCLI({ name, version?, commands, middleware? })`
|
|
159
159
|
|
|
160
|
-
The program. `--help` and `--version` come for free.
|
|
160
|
+
The program. `--help` and `--version` come for free, and `--help --json` prints the same facts as data.
|
|
161
161
|
|
|
162
162
|
#### `cli.map(command, handler)`
|
|
163
163
|
|
|
@@ -167,6 +167,19 @@ Binds a handler, a `createCommand(...)` or a `lazy(...)` import to a command.
|
|
|
167
167
|
|
|
168
168
|
Runs the command and answers its exit code; `io` replaces the process's `cwd`, `env`, streams or `exec`.
|
|
169
169
|
|
|
170
|
+
#### `cli.describe(path?)`
|
|
171
|
+
|
|
172
|
+
The contract as data: every visible command under `path` with its arguments, flags and examples, as `--help --json` prints it.
|
|
173
|
+
|
|
174
|
+
#### `cli.check(argv)`
|
|
175
|
+
|
|
176
|
+
Checks a command line without running it: the command, every flag and every value against its schema. A value a prompt would ask for passes. Answers the command's path, or a `UsageError` listing every problem.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
let checked = await cli.check(["greet", "Ada", "--shuot"]);
|
|
180
|
+
if (isFailure(checked)) console.error(checked.error.message); // Unknown flag --shuot …
|
|
181
|
+
```
|
|
182
|
+
|
|
170
183
|
#### `createCommand(command, handler)`
|
|
171
184
|
|
|
172
185
|
A handler typed from its command, for a module of its own; pass `{ middleware, handler }` for middleware this command alone runs.
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
|
+
import type { Result } from "@sdxc/result";
|
|
9
10
|
import type { ArgumentsOf, FlagsOf, Group, Node } from "./command.js";
|
|
11
|
+
import type { CommandDescription } from "./describe.js";
|
|
10
12
|
import type { CommandAction, CommandHandler } from "./handler.js";
|
|
11
13
|
import type { Lazy } from "./lazy.js";
|
|
12
14
|
import type { AnyCommandMiddleware, ChainProperties } from "./middleware.js";
|
|
13
15
|
import type { RunIO, Script } from "./session.js";
|
|
14
16
|
import { CommandContext } from "./context.js";
|
|
17
|
+
import { UsageError } from "./errors.js";
|
|
15
18
|
/** What a CLI is built from. */
|
|
16
19
|
export interface CLIOptions<Commands extends Group, Chain extends readonly AnyCommandMiddleware[]> {
|
|
17
20
|
/** The program name, as the usage line prints it. */
|
|
@@ -48,6 +51,28 @@ export interface CLI<Commands extends Group = Group, Chain extends readonly AnyC
|
|
|
48
51
|
* @example process.exitCode = await cli.run(process.argv.slice(2));
|
|
49
52
|
*/
|
|
50
53
|
run(argv: readonly string[], io?: RunIO): Promise<number>;
|
|
54
|
+
/**
|
|
55
|
+
* The contract as data, the facts `--help --json` prints: the whole program, or the command
|
|
56
|
+
* `path` names, with every visible member below it.
|
|
57
|
+
*
|
|
58
|
+
* @returns The description, or `undefined` when `path` names no command
|
|
59
|
+
* @example let make = cli.describe(["make"]);
|
|
60
|
+
*/
|
|
61
|
+
describe(path?: readonly string[]): CommandDescription | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Checks `argv` the way a run does before it asks or runs anything: the command it names, every
|
|
64
|
+
* flag, and every value against its schema. A missing value that a prompt would ask for passes,
|
|
65
|
+
* since a terminal run asks for it. Nothing is loaded and no handler runs.
|
|
66
|
+
*
|
|
67
|
+
* @returns The path of the command `argv` reaches, or a usage failure listing every problem
|
|
68
|
+
* @example let checked = await cli.check(["make", "controller", "posts", "--method", "form"]);
|
|
69
|
+
*/
|
|
70
|
+
check(argv: readonly string[]): Promise<Result<CheckedCommand, UsageError>>;
|
|
71
|
+
}
|
|
72
|
+
/** A command line `check()` accepted. */
|
|
73
|
+
export interface CheckedCommand {
|
|
74
|
+
/** The words that reached the command; empty for the root. */
|
|
75
|
+
path: string[];
|
|
51
76
|
}
|
|
52
77
|
/**
|
|
53
78
|
* Builds a CLI over a command contract.
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { failure, isFailure, success } from "@sdxc/result";
|
|
10
10
|
import { CommandContext } from "./context.js";
|
|
11
|
+
import { describeEntry } from "./describe.js";
|
|
11
12
|
import { CancelledError, EXIT_CODES, exitCodeOf, UsageError } from "./errors.js";
|
|
13
|
+
import { ExecError } from "./exec.js";
|
|
12
14
|
import { isAction } from "./handler.js";
|
|
13
15
|
import { renderHelp } from "./help.js";
|
|
14
16
|
import { isLazy } from "./lazy.js";
|
|
@@ -45,6 +47,15 @@ export function createCLI(options) {
|
|
|
45
47
|
bindings.set(target, resolver(binding));
|
|
46
48
|
},
|
|
47
49
|
run: (argv, io = {}) => execute(cli, argv, io, undefined),
|
|
50
|
+
describe(path = []) {
|
|
51
|
+
let entry = tree.root;
|
|
52
|
+
for (let word of path)
|
|
53
|
+
entry = entry?.children.get(word);
|
|
54
|
+
if (entry === undefined)
|
|
55
|
+
return undefined;
|
|
56
|
+
return describeEntry(entry, { version: options.version, description: options.description });
|
|
57
|
+
},
|
|
58
|
+
check: (argv) => check(tree, options.version !== undefined, argv),
|
|
48
59
|
};
|
|
49
60
|
INTERNALS.set(cli, {
|
|
50
61
|
tree,
|
|
@@ -82,6 +93,35 @@ function normalize(binding) {
|
|
|
82
93
|
export function runScripted(cli, argv, io, script) {
|
|
83
94
|
return execute(cli, argv, io, script);
|
|
84
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether `--json` rides along with `--help`, before any `--`, so the help prints as data. A
|
|
98
|
+
* command with a `--json` flag of its own reads the pair the same way.
|
|
99
|
+
*/
|
|
100
|
+
function asksForJSON(argv) {
|
|
101
|
+
let end = argv.indexOf("--");
|
|
102
|
+
return (end === -1 ? argv : argv.slice(0, end)).includes("--json");
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The prompts of a check: every one answers its initial value, so a value a terminal run would
|
|
106
|
+
* ask for counts as given.
|
|
107
|
+
*/
|
|
108
|
+
const CHECK_SESSION = {
|
|
109
|
+
interactive: true,
|
|
110
|
+
ask: (request) => Promise.resolve(success(request.initial)),
|
|
111
|
+
};
|
|
112
|
+
/** Parses and checks `argv` against `tree`, asking nothing and running nothing. */
|
|
113
|
+
async function check(tree, withVersion, argv) {
|
|
114
|
+
let parsed = parseArgv(tree, argv, withVersion);
|
|
115
|
+
let path = [...parsed.entry.path];
|
|
116
|
+
if (parsed.help || parsed.version)
|
|
117
|
+
return success({ path });
|
|
118
|
+
let inputs = await resolveInputs(parsed, CHECK_SESSION);
|
|
119
|
+
if (isFailure(inputs)) {
|
|
120
|
+
let error = inputs.error;
|
|
121
|
+
return failure(error instanceof UsageError ? error : new UsageError(error.message));
|
|
122
|
+
}
|
|
123
|
+
return success({ path });
|
|
124
|
+
}
|
|
85
125
|
/** One run, from argument vector to exit code. */
|
|
86
126
|
async function execute(cli, argv, io, script) {
|
|
87
127
|
let internals = INTERNALS.get(cli);
|
|
@@ -91,6 +131,10 @@ async function execute(cli, argv, io, script) {
|
|
|
91
131
|
let session = openSession(io, parsed.noProgress, script);
|
|
92
132
|
try {
|
|
93
133
|
let entry = parsed.entry;
|
|
134
|
+
if (parsed.help && asksForJSON(argv)) {
|
|
135
|
+
session.stdout.write(`${JSON.stringify(describeEntry(entry, program), null, 2)}\n`);
|
|
136
|
+
return EXIT_CODES.success;
|
|
137
|
+
}
|
|
94
138
|
if (parsed.help) {
|
|
95
139
|
session.stdout.write(renderHelp(entry, program, session.style));
|
|
96
140
|
return EXIT_CODES.success;
|
|
@@ -173,7 +217,10 @@ async function dispatch(chain, context, handler) {
|
|
|
173
217
|
function asError(error) {
|
|
174
218
|
return error instanceof Error ? error : new Error(String(error));
|
|
175
219
|
}
|
|
176
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* Prints a failed run's message and answers its exit code. An `ExecError` also prints the
|
|
222
|
+
* child's stderr so CI sees wrangler/cf authentication lines instead of only "exited with 1".
|
|
223
|
+
*/
|
|
177
224
|
function report(error, entry, session, program) {
|
|
178
225
|
if (error instanceof UsageError) {
|
|
179
226
|
let help = renderHelp(entry, program, session.style.stderr);
|
|
@@ -181,6 +228,9 @@ function report(error, entry, session, program) {
|
|
|
181
228
|
}
|
|
182
229
|
else if (!(error instanceof CancelledError) && error.message !== "") {
|
|
183
230
|
session.screen.write(session.stderr, `${error.message}\n`);
|
|
231
|
+
if (error instanceof ExecError && error.stderr !== "") {
|
|
232
|
+
session.screen.write(session.stderr, error.stderr.endsWith("\n") ? error.stderr : `${error.stderr}\n`);
|
|
233
|
+
}
|
|
184
234
|
}
|
|
185
235
|
return exitCodeOf(error);
|
|
186
236
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract as plain data: every command with its arguments, flags and examples, the same
|
|
3
|
+
* facts the help prints. `--help --json` prints it, and a tool reads it to check what a script,
|
|
4
|
+
* a guide or an agent passes against what the program accepts.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { FlagKind } from "./flag.js";
|
|
10
|
+
import type { Entry } from "./tree.js";
|
|
11
|
+
/** One argument of a command, in the order the command line takes them. */
|
|
12
|
+
export interface ArgumentDescription {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string | null;
|
|
15
|
+
/** A value is needed; a missing one is asked for on a terminal when the argument prompts. */
|
|
16
|
+
required: boolean;
|
|
17
|
+
/** Takes every remaining positional. */
|
|
18
|
+
variadic: boolean;
|
|
19
|
+
/** Takes every token after the command's name unparsed, flags included. */
|
|
20
|
+
rest: boolean;
|
|
21
|
+
/** The default, when one is declared. */
|
|
22
|
+
default?: unknown;
|
|
23
|
+
}
|
|
24
|
+
/** One flag of a command, or one every command answers to (`builtin`). */
|
|
25
|
+
export interface FlagDescription {
|
|
26
|
+
/** The long name, written `--<name>`. */
|
|
27
|
+
name: string;
|
|
28
|
+
aliases: string[];
|
|
29
|
+
/** The one-character alias, written `-<short>`. */
|
|
30
|
+
short: string | null;
|
|
31
|
+
kind: FlagKind;
|
|
32
|
+
/** A boolean's `--no-<name>` spelling is accepted. */
|
|
33
|
+
negatable: boolean;
|
|
34
|
+
/** What the help shows in `<…>` after the flag; `null` for a boolean. */
|
|
35
|
+
value: string | null;
|
|
36
|
+
/** The flag may appear bare or with `=value`. */
|
|
37
|
+
optionalValue: boolean;
|
|
38
|
+
/** One value holds several, separated by commas. */
|
|
39
|
+
list: boolean;
|
|
40
|
+
/** Repeating the flag collects every value. */
|
|
41
|
+
repeatable: boolean;
|
|
42
|
+
required: boolean;
|
|
43
|
+
description: string | null;
|
|
44
|
+
/** Answered by every command the program runs: `--help`, `--version`, `--no-progress`. */
|
|
45
|
+
builtin: boolean;
|
|
46
|
+
/** The default, when one is declared. */
|
|
47
|
+
default?: unknown;
|
|
48
|
+
}
|
|
49
|
+
/** One command or group, with every visible member below it. */
|
|
50
|
+
export interface CommandDescription {
|
|
51
|
+
/** The words typed after the program to reach it; empty for the root. */
|
|
52
|
+
path: string[];
|
|
53
|
+
/** The program name and the path, as the help prints it. */
|
|
54
|
+
title: string;
|
|
55
|
+
description: string | null;
|
|
56
|
+
arguments: ArgumentDescription[];
|
|
57
|
+
flags: FlagDescription[];
|
|
58
|
+
/** Example command lines, each starting with the program name. */
|
|
59
|
+
examples: string[];
|
|
60
|
+
/** Members of a group by name; empty for a command. */
|
|
61
|
+
commands: CommandDescription[];
|
|
62
|
+
}
|
|
63
|
+
/** The program's own facts the description includes beside the contract. */
|
|
64
|
+
export interface DescribeProgram {
|
|
65
|
+
version: string | undefined;
|
|
66
|
+
description: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Describes `entry` and every visible member under it. Hidden commands and flags stay out, as
|
|
70
|
+
* they do from the help.
|
|
71
|
+
*/
|
|
72
|
+
export declare function describeEntry(entry: Entry, program: DescribeProgram): CommandDescription;
|
package/dist/describe.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract as plain data: every command with its arguments, flags and examples, the same
|
|
3
|
+
* facts the help prints. `--help --json` prints it, and a tool reads it to check what a script,
|
|
4
|
+
* a guide or an agent passes against what the program accepts.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { groupSpec, isGroup } from "./command.js";
|
|
10
|
+
/**
|
|
11
|
+
* Describes `entry` and every visible member under it. Hidden commands and flags stay out, as
|
|
12
|
+
* they do from the help.
|
|
13
|
+
*/
|
|
14
|
+
export function describeEntry(entry, program) {
|
|
15
|
+
let node = entry.node;
|
|
16
|
+
let spec = isGroup(node) ? groupSpec(node) : node;
|
|
17
|
+
let description = spec.description ?? (entry.parent === undefined ? program.description : undefined);
|
|
18
|
+
let flags = entry.flags.filter(({ spec }) => !spec.hidden).map(({ spec }) => describeFlag(spec));
|
|
19
|
+
if (entry.tasks)
|
|
20
|
+
flags.push(builtin("progress", "Print tasks without animation", true));
|
|
21
|
+
if (program.version !== undefined && entry.parent === undefined) {
|
|
22
|
+
flags.push(builtin("version", "Print the version", false));
|
|
23
|
+
}
|
|
24
|
+
flags.push({ ...builtin("help", "Show this help", false), short: "h" });
|
|
25
|
+
let programName = entry.title.split(" ")[0];
|
|
26
|
+
let commands = [];
|
|
27
|
+
for (let child of entry.children.values()) {
|
|
28
|
+
let childSpec = isGroup(child.node) ? groupSpec(child.node) : child.node;
|
|
29
|
+
if (childSpec.hidden)
|
|
30
|
+
continue;
|
|
31
|
+
commands.push(describeEntry(child, program));
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
path: [...entry.path],
|
|
35
|
+
title: entry.title,
|
|
36
|
+
description: description ?? null,
|
|
37
|
+
arguments: entry.arguments.map(({ key, spec }) => ({
|
|
38
|
+
name: spec.valueName ?? key,
|
|
39
|
+
description: spec.description ?? null,
|
|
40
|
+
required: !spec.optional && !spec.hasDefault && (spec.variadic ? spec.min > 0 : true),
|
|
41
|
+
variadic: spec.variadic,
|
|
42
|
+
rest: spec.rest,
|
|
43
|
+
...(spec.hasDefault ? { default: spec.default } : {}),
|
|
44
|
+
})),
|
|
45
|
+
flags,
|
|
46
|
+
examples: isGroup(node) ? [] : node.examples.map((example) => `${programName} ${example}`),
|
|
47
|
+
commands,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** A declared flag as data. */
|
|
51
|
+
function describeFlag(spec) {
|
|
52
|
+
let boolean = spec.kind === "boolean";
|
|
53
|
+
return {
|
|
54
|
+
name: spec.name,
|
|
55
|
+
aliases: [...spec.aliases],
|
|
56
|
+
short: spec.short ?? null,
|
|
57
|
+
kind: spec.kind,
|
|
58
|
+
negatable: boolean,
|
|
59
|
+
value: boolean ? null : (spec.valueName ?? (spec.kind === "number" ? "number" : "value")),
|
|
60
|
+
optionalValue: spec.optionalValue,
|
|
61
|
+
list: spec.list,
|
|
62
|
+
repeatable: spec.multiple,
|
|
63
|
+
required: spec.required && spec.prompt === undefined,
|
|
64
|
+
description: spec.description ?? null,
|
|
65
|
+
builtin: false,
|
|
66
|
+
...(spec.hasDefault && spec.default !== undefined ? { default: spec.default } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A flag the runtime answers for every command. `--no-progress` is the one spelled negated, so
|
|
71
|
+
* its name is `progress` and it is negatable.
|
|
72
|
+
*/
|
|
73
|
+
function builtin(name, description, negatable) {
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
aliases: [],
|
|
77
|
+
short: null,
|
|
78
|
+
kind: "boolean",
|
|
79
|
+
negatable,
|
|
80
|
+
value: null,
|
|
81
|
+
optionalValue: false,
|
|
82
|
+
list: false,
|
|
83
|
+
repeatable: false,
|
|
84
|
+
required: false,
|
|
85
|
+
description,
|
|
86
|
+
builtin: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
package/dist/exec.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
+
import { iife } from "@sdxc/iife";
|
|
10
11
|
import { failure, success } from "@sdxc/result";
|
|
11
12
|
import { CancelledError } from "./errors.js";
|
|
12
13
|
/** A child that could not start or exited with a code other than 0. */
|
|
@@ -61,27 +62,33 @@ export const spawnExec = (command, request) => new Promise((resolve) => {
|
|
|
61
62
|
}
|
|
62
63
|
let shown = displayCommand(command);
|
|
63
64
|
let direct = (target, stream) => target === stream;
|
|
64
|
-
let outTarget =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
let outTarget = null;
|
|
66
|
+
if (request.stdio === "inherit")
|
|
67
|
+
outTarget = request.stdout;
|
|
68
|
+
else if (request.stdio === "stderr")
|
|
69
|
+
outTarget = request.stderr;
|
|
69
70
|
let errTarget = request.stdio === "inherit" || request.stdio === "stderr" ? request.stderr : null;
|
|
70
|
-
let stdout =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
let stdout = iife(() => {
|
|
72
|
+
if (request.stdio === "ignore")
|
|
73
|
+
return "ignore";
|
|
74
|
+
if (outTarget === null)
|
|
75
|
+
return "pipe";
|
|
76
|
+
if (direct(outTarget, process.stdout))
|
|
77
|
+
return "inherit";
|
|
78
|
+
if (direct(outTarget, process.stderr))
|
|
79
|
+
return 2;
|
|
80
|
+
return "pipe";
|
|
81
|
+
});
|
|
82
|
+
let stderr = "pipe";
|
|
83
|
+
if (request.stdio === "ignore")
|
|
84
|
+
stderr = "ignore";
|
|
85
|
+
else if (errTarget !== null && direct(errTarget, process.stderr))
|
|
86
|
+
stderr = "inherit";
|
|
87
|
+
let stdin = "ignore";
|
|
88
|
+
if (request.input !== undefined)
|
|
89
|
+
stdin = "pipe";
|
|
90
|
+
else if (request.stdio === "inherit")
|
|
91
|
+
stdin = "inherit";
|
|
85
92
|
let child = spawn(program, args, {
|
|
86
93
|
cwd: request.cwd,
|
|
87
94
|
env: { ...request.env },
|
package/dist/help.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
|
+
import { iife } from "@sdxc/iife";
|
|
9
10
|
import { groupSpec, isGroup } from "./command.js";
|
|
10
11
|
import { visibleWidth } from "./style.js";
|
|
11
12
|
/** Renders `entry`'s help, ending with a newline. */
|
|
@@ -99,11 +100,13 @@ function flagLabel(spec) {
|
|
|
99
100
|
if (spec.short !== undefined)
|
|
100
101
|
names.push(`-${spec.short}`);
|
|
101
102
|
if (spec.kind === "boolean") {
|
|
102
|
-
let prefix =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
let prefix = iife(() => {
|
|
104
|
+
if (spec.prompt !== undefined || spec.default === undefined)
|
|
105
|
+
return "[no-]";
|
|
106
|
+
if (spec.default === true)
|
|
107
|
+
return "no-";
|
|
108
|
+
return "";
|
|
109
|
+
});
|
|
107
110
|
names.push(`--${prefix}${spec.name}`);
|
|
108
111
|
for (let alias of spec.aliases)
|
|
109
112
|
names.push(`--${prefix}${alias}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
9
|
export type { Argument, ArgumentOptions, ArgumentValue, RestArgumentOptions, SingleArgumentOptions, VariadicArgumentOptions, } from "./argument.js";
|
|
10
|
-
export type { Binding, CLI, CLIOptions } from "./cli.js";
|
|
10
|
+
export type { Binding, CheckedCommand, CLI, CLIOptions } from "./cli.js";
|
|
11
11
|
export type { ArgumentsOf, ArgumentsShape, Command, CommandDefinition, FlagsOf, FlagsShape, Group, GroupDefinition, GroupInputs, Node, } from "./command.js";
|
|
12
12
|
export type { AnyCommandContext, CommandContextInit, Stdin } from "./context.js";
|
|
13
|
+
export type { ArgumentDescription, CommandDescription, FlagDescription } from "./describe.js";
|
|
13
14
|
export type { Exec, ExecOptions, ExecOutput, ExecRequest, Stdio } from "./exec.js";
|
|
14
15
|
export type { BooleanFlagOptions, Flag, FlagKind, FlagOptions, FlagValue, ValueFlag, ValueFlagOptions, } from "./flag.js";
|
|
15
16
|
export type { ChainContext, CLITypes, CommandAction, CommandHandler, HandlerContext, } from "./handler.js";
|
package/dist/interactions.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
|
+
import { iife } from "@sdxc/iife";
|
|
9
10
|
import { typed } from "./keys.js";
|
|
10
11
|
import { normalizeChoices } from "./prompt.js";
|
|
11
12
|
import { graphemes } from "./style.js";
|
|
@@ -150,9 +151,11 @@ function numberLine(options, initial) {
|
|
|
150
151
|
let current = editor.text === "" ? (fallback ?? options.min ?? 0) : Number(editor.text);
|
|
151
152
|
if (Number.isNaN(current))
|
|
152
153
|
current = fallback ?? 0;
|
|
153
|
-
let next =
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
let next = iife(() => {
|
|
155
|
+
if (editor.text === "" && fallback === undefined)
|
|
156
|
+
return current;
|
|
157
|
+
return current + (key.name === "up" ? step : -step);
|
|
158
|
+
});
|
|
156
159
|
editor = new Editor(String(clamp(next)));
|
|
157
160
|
return undefined;
|
|
158
161
|
}
|
|
@@ -261,11 +264,13 @@ function list(choices, initial, mode) {
|
|
|
261
264
|
render(style, header) {
|
|
262
265
|
let lines = [];
|
|
263
266
|
if (mode.search === true) {
|
|
264
|
-
let shownQuery =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
let shownQuery = iife(() => {
|
|
268
|
+
if (query.chars.length === 0)
|
|
269
|
+
return mode.placeholder === undefined
|
|
270
|
+
? style.inverse(" ")
|
|
271
|
+
: style.dim(mode.placeholder);
|
|
272
|
+
return query.render(style);
|
|
273
|
+
});
|
|
269
274
|
lines.push(`${header} › ${shownQuery}`);
|
|
270
275
|
}
|
|
271
276
|
else
|
|
@@ -283,12 +288,18 @@ function list(choices, initial, mode) {
|
|
|
283
288
|
for (let index of window) {
|
|
284
289
|
let option = options[index];
|
|
285
290
|
let pointer = index === cursor ? style.cyan("❯") : " ";
|
|
286
|
-
let box
|
|
287
|
-
|
|
288
|
-
? style.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
let box;
|
|
292
|
+
if (mode.mode === "many")
|
|
293
|
+
box = `${selected.has(index) ? style.green("◉") : "◯"} `;
|
|
294
|
+
else
|
|
295
|
+
box = "";
|
|
296
|
+
let label;
|
|
297
|
+
if (option.disabled)
|
|
298
|
+
label = style.dim(`${option.label} (disabled)`);
|
|
299
|
+
else if (index === cursor)
|
|
300
|
+
label = style.cyan(option.label);
|
|
301
|
+
else
|
|
302
|
+
label = option.label;
|
|
292
303
|
let hint = option.hint === "" ? "" : ` ${style.dim(option.hint)}`;
|
|
293
304
|
lines.push(`${pointer} ${box}${label}${hint}`);
|
|
294
305
|
}
|
package/dist/keys.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
9
|
import { emitKeypressEvents } from "node:readline";
|
|
10
|
+
import { iife } from "@sdxc/iife";
|
|
10
11
|
import { graphemes } from "./style.js";
|
|
11
12
|
/**
|
|
12
13
|
* Starts reading keys from `input` in raw mode.
|
|
@@ -101,7 +102,13 @@ export function keyFromName(name) {
|
|
|
101
102
|
let bare = ctrl ? name.slice(5) : name;
|
|
102
103
|
let named = NAMED_KEYS[bare];
|
|
103
104
|
if (named !== undefined) {
|
|
104
|
-
let sequence =
|
|
105
|
+
let sequence = iife(() => {
|
|
106
|
+
if (named === "space")
|
|
107
|
+
return " ";
|
|
108
|
+
if (named === "return")
|
|
109
|
+
return "\r";
|
|
110
|
+
return "";
|
|
111
|
+
});
|
|
105
112
|
return { name: named, sequence, ctrl, meta: false, shift: false };
|
|
106
113
|
}
|
|
107
114
|
let shift = bare !== bare.toLowerCase();
|
package/dist/parse.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
|
+
import { iife } from "@sdxc/iife";
|
|
9
10
|
import { isGroup } from "./command.js";
|
|
10
11
|
import { closest } from "./suggest.js";
|
|
11
12
|
/** A token that starts a flag: a dash and something after it, other than a negative number. */
|
|
@@ -185,6 +186,11 @@ function skipValue(argv, index) {
|
|
|
185
186
|
let next = argv[index];
|
|
186
187
|
return next === undefined || isFlagToken(next) || next === "--" ? index : index + 1;
|
|
187
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* How many commands declaring a misplaced flag its usage error names; past it the rest are
|
|
191
|
+
* counted, so a flag every member of a large group shares still fits on one line.
|
|
192
|
+
*/
|
|
193
|
+
const NAMED_OWNERS = 3;
|
|
188
194
|
/**
|
|
189
195
|
* The message for a flag `entry` does not declare: the commands that do declare it, or the
|
|
190
196
|
* closest name it does declare.
|
|
@@ -198,9 +204,14 @@ function unknownFlag(tree, entry, token) {
|
|
|
198
204
|
let owners = long ? (tree.owners.get(name) ?? tree.owners.get(positive) ?? []) : [];
|
|
199
205
|
if (owners.length > 0) {
|
|
200
206
|
let titles = owners.map((owner) => owner.title);
|
|
201
|
-
let listed =
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
let listed = iife(() => {
|
|
208
|
+
if (titles.length === 1)
|
|
209
|
+
return titles[0];
|
|
210
|
+
if (titles.length > NAMED_OWNERS) {
|
|
211
|
+
return `${titles.slice(0, NAMED_OWNERS).join(", ")} and ${titles.length - NAMED_OWNERS} more`;
|
|
212
|
+
}
|
|
213
|
+
return `${titles.slice(0, -1).join(", ")} and ${titles.at(-1)}`;
|
|
214
|
+
});
|
|
204
215
|
return `${spelled} is not a flag of ${entry.title}; ${listed} ${titles.length === 1 ? "takes" : "take"} it`;
|
|
205
216
|
}
|
|
206
217
|
let candidates = [];
|
package/dist/resolve.d.ts
CHANGED
|
@@ -22,6 +22,6 @@ export interface Inputs {
|
|
|
22
22
|
* @returns The inputs, a usage failure listing every problem, a cancelled failure, or the
|
|
23
23
|
* failure of a scripted answer that was missing or rejected
|
|
24
24
|
*/
|
|
25
|
-
export declare function resolveInputs(parsed: Parsed, session: Session): Promise<Result<Inputs, Error>>;
|
|
25
|
+
export declare function resolveInputs(parsed: Parsed, session: Pick<Session, "interactive" | "ask">): Promise<Result<Inputs, Error>>;
|
|
26
26
|
/** The `ctx.prompt` of one run: asks on a terminal or from a script, else answers the default. */
|
|
27
27
|
export declare function createPrompter(session: Session): Prompter;
|
package/dist/resolve.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
7
|
* @copyright Sergio Xalambrí 2026
|
|
8
8
|
*/
|
|
9
|
+
import { iife } from "@sdxc/iife";
|
|
9
10
|
import { failure, isFailure, success } from "@sdxc/result";
|
|
10
11
|
import { isGroup } from "./command.js";
|
|
11
12
|
import { CancelledError, UsageError } from "./errors.js";
|
|
@@ -109,13 +110,14 @@ export async function resolveInputs(parsed, session) {
|
|
|
109
110
|
return success(spec.variadic ? checked.data : checked.data[0]);
|
|
110
111
|
};
|
|
111
112
|
if (given.length === 0) {
|
|
112
|
-
let fallback =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
let fallback = iife(() => {
|
|
114
|
+
if (spec.hasDefault)
|
|
115
|
+
return { value: spec.default };
|
|
116
|
+
if (spec.optional)
|
|
117
|
+
return { value: undefined };
|
|
118
|
+
if (spec.variadic && spec.min === 0)
|
|
119
|
+
return { value: [] };
|
|
120
|
+
});
|
|
119
121
|
missing.push({
|
|
120
122
|
label,
|
|
121
123
|
names: [key],
|
|
@@ -154,13 +156,14 @@ export async function resolveInputs(parsed, session) {
|
|
|
154
156
|
return success(many ? checked.data : checked.data[0]);
|
|
155
157
|
};
|
|
156
158
|
if (occurrences.length === 0) {
|
|
157
|
-
let fallback =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
:
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
let fallback = iife(() => {
|
|
160
|
+
if (spec.hasDefault)
|
|
161
|
+
return { value: spec.default };
|
|
162
|
+
if (spec.kind === "boolean")
|
|
163
|
+
return { value: false };
|
|
164
|
+
if (!spec.required)
|
|
165
|
+
return { value: many ? [] : undefined };
|
|
166
|
+
});
|
|
164
167
|
missing.push({
|
|
165
168
|
label,
|
|
166
169
|
names: [key, spec.name],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/cli",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.23",
|
|
4
4
|
"description": "Build command-line tools with generated help, prompts bound to flags, tasks and a test harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./test": "./dist/test.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@sdxc/iife": "2026.10.23",
|
|
12
13
|
"@sdxc/result": "2026.10.13",
|
|
13
14
|
"@standard-schema/spec": "^1.1.0"
|
|
14
15
|
},
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
"optional": true
|
|
21
22
|
}
|
|
22
23
|
},
|
|
23
|
-
"gitHead": "
|
|
24
|
+
"gitHead": "8e428ddf15290d44500a9618ec4d04bd7629fa99",
|
|
24
25
|
"publishConfig": {
|
|
25
26
|
"access": "public"
|
|
26
27
|
},
|