@sdxc/cli 0.0.0-stage → 2026.10.20
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/LICENSE.md +21 -0
- package/README.md +376 -2
- package/dist/argument.d.ts +120 -0
- package/dist/argument.js +104 -0
- package/dist/cli.d.ts +93 -0
- package/dist/cli.js +229 -0
- package/dist/command.d.ts +139 -0
- package/dist/command.js +79 -0
- package/dist/context.d.ts +138 -0
- package/dist/context.js +175 -0
- package/dist/describe.d.ts +72 -0
- package/dist/describe.js +88 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +82 -0
- package/dist/exec.d.ts +83 -0
- package/dist/exec.js +136 -0
- package/dist/flag.d.ts +175 -0
- package/dist/flag.js +91 -0
- package/dist/handler.d.ts +73 -0
- package/dist/handler.js +30 -0
- package/dist/help.d.ts +18 -0
- package/dist/help.js +141 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +20 -0
- package/dist/interactions.d.ts +27 -0
- package/dist/interactions.js +373 -0
- package/dist/keys.d.ts +49 -0
- package/dist/keys.js +146 -0
- package/dist/lazy.d.ts +29 -0
- package/dist/lazy.js +23 -0
- package/dist/middleware.d.ts +78 -0
- package/dist/middleware.js +9 -0
- package/dist/parse.d.ts +38 -0
- package/dist/parse.js +225 -0
- package/dist/prompt.d.ts +153 -0
- package/dist/prompt.js +53 -0
- package/dist/resolve.d.ts +27 -0
- package/dist/resolve.js +294 -0
- package/dist/schema.d.ts +27 -0
- package/dist/schema.js +34 -0
- package/dist/screen.d.ts +42 -0
- package/dist/screen.js +114 -0
- package/dist/session.d.ts +84 -0
- package/dist/session.js +219 -0
- package/dist/style.d.ts +50 -0
- package/dist/style.js +72 -0
- package/dist/suggest.d.ts +15 -0
- package/dist/suggest.js +50 -0
- package/dist/task.d.ts +47 -0
- package/dist/task.js +213 -0
- package/dist/terminal.d.ts +21 -0
- package/dist/terminal.js +101 -0
- package/dist/test.d.ts +89 -0
- package/dist/test.js +140 -0
- package/dist/tree.d.ts +58 -0
- package/dist/tree.js +97 -0
- package/package.json +40 -5
package/dist/exec.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child processes without a shell: arguments reach the program exactly as given, so nothing in
|
|
3
|
+
* them is expanded or split. The answer is a `Result` carrying the exit code and what the child
|
|
4
|
+
* printed, and an aborted signal stops the child and answers a cancelled failure.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { failure, success } from "@sdxc/result";
|
|
11
|
+
import { CancelledError } from "./errors.js";
|
|
12
|
+
/** A child that could not start or exited with a code other than 0. */
|
|
13
|
+
export class ExecError extends Error {
|
|
14
|
+
name = "ExecError";
|
|
15
|
+
/** The command as given. */
|
|
16
|
+
command;
|
|
17
|
+
/** The exit code, or `null` when the child never started or ended on a signal. */
|
|
18
|
+
code;
|
|
19
|
+
stdout;
|
|
20
|
+
stderr;
|
|
21
|
+
output;
|
|
22
|
+
/**
|
|
23
|
+
* @param message - What went wrong
|
|
24
|
+
* @param details - The command and what it printed
|
|
25
|
+
*/
|
|
26
|
+
constructor(message, details) {
|
|
27
|
+
super(message, { cause: details.cause });
|
|
28
|
+
this.command = details.command;
|
|
29
|
+
this.code = details.code;
|
|
30
|
+
this.stdout = details.stdout ?? "";
|
|
31
|
+
this.stderr = details.stderr ?? "";
|
|
32
|
+
this.output = details.output ?? "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The command as a shell user would read it, for messages. */
|
|
36
|
+
export function displayCommand(command) {
|
|
37
|
+
return command
|
|
38
|
+
.map((part) => (/^[\w./:=@-]+$/.test(part) ? part : JSON.stringify(part)))
|
|
39
|
+
.join(" ");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Spawns `command` with `node:child_process`. Output bound for the process's own streams is
|
|
43
|
+
* inherited directly; output bound for any other stream is piped and forwarded to it.
|
|
44
|
+
*/
|
|
45
|
+
export const spawnExec = (command, request) => new Promise((resolve) => {
|
|
46
|
+
let settled = false;
|
|
47
|
+
let settle = (outcome) => {
|
|
48
|
+
if (settled)
|
|
49
|
+
return;
|
|
50
|
+
settled = true;
|
|
51
|
+
resolve(outcome);
|
|
52
|
+
};
|
|
53
|
+
let [program, ...args] = command;
|
|
54
|
+
if (program === undefined) {
|
|
55
|
+
settle(failure(new ExecError("exec needs a command", { command, code: null })));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (request.signal.aborted) {
|
|
59
|
+
settle(failure(new CancelledError()));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let shown = displayCommand(command);
|
|
63
|
+
let direct = (target, stream) => target === stream;
|
|
64
|
+
let outTarget = request.stdio === "inherit"
|
|
65
|
+
? request.stdout
|
|
66
|
+
: request.stdio === "stderr"
|
|
67
|
+
? request.stderr
|
|
68
|
+
: null;
|
|
69
|
+
let errTarget = request.stdio === "inherit" || request.stdio === "stderr" ? request.stderr : null;
|
|
70
|
+
let stdout = request.stdio === "ignore"
|
|
71
|
+
? "ignore"
|
|
72
|
+
: outTarget === null
|
|
73
|
+
? "pipe"
|
|
74
|
+
: direct(outTarget, process.stdout)
|
|
75
|
+
? "inherit"
|
|
76
|
+
: direct(outTarget, process.stderr)
|
|
77
|
+
? 2
|
|
78
|
+
: "pipe";
|
|
79
|
+
let stderr = request.stdio === "ignore"
|
|
80
|
+
? "ignore"
|
|
81
|
+
: errTarget !== null && direct(errTarget, process.stderr)
|
|
82
|
+
? "inherit"
|
|
83
|
+
: "pipe";
|
|
84
|
+
let stdin = request.input !== undefined ? "pipe" : request.stdio === "inherit" ? "inherit" : "ignore";
|
|
85
|
+
let child = spawn(program, args, {
|
|
86
|
+
cwd: request.cwd,
|
|
87
|
+
env: { ...request.env },
|
|
88
|
+
shell: false,
|
|
89
|
+
stdio: [stdin, stdout, stderr],
|
|
90
|
+
});
|
|
91
|
+
let collected = { stdout: "", stderr: "", output: "" };
|
|
92
|
+
child.stdout?.on("data", (chunk) => {
|
|
93
|
+
let text = chunk.toString();
|
|
94
|
+
if (outTarget === null) {
|
|
95
|
+
collected.stdout += text;
|
|
96
|
+
collected.output += text;
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
outTarget.write(text);
|
|
100
|
+
});
|
|
101
|
+
child.stderr?.on("data", (chunk) => {
|
|
102
|
+
let text = chunk.toString();
|
|
103
|
+
if (errTarget === null) {
|
|
104
|
+
collected.stderr += text;
|
|
105
|
+
collected.output += text;
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
errTarget.write(text);
|
|
109
|
+
});
|
|
110
|
+
if (request.input !== undefined)
|
|
111
|
+
child.stdin?.end(request.input);
|
|
112
|
+
let abort = () => child.kill("SIGTERM");
|
|
113
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
114
|
+
child.once("error", (error) => {
|
|
115
|
+
request.signal.removeEventListener("abort", abort);
|
|
116
|
+
settle(failure(new ExecError(`${shown} could not start: ${error.message}`, {
|
|
117
|
+
command,
|
|
118
|
+
code: null,
|
|
119
|
+
cause: error,
|
|
120
|
+
...collected,
|
|
121
|
+
})));
|
|
122
|
+
});
|
|
123
|
+
child.once("close", (code, signal) => {
|
|
124
|
+
request.signal.removeEventListener("abort", abort);
|
|
125
|
+
if (request.signal.aborted) {
|
|
126
|
+
settle(failure(new CancelledError()));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (code === 0) {
|
|
130
|
+
settle(success({ code, ...collected }));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let reason = code === null ? `was stopped by ${signal ?? "a signal"}` : `exited with ${code}`;
|
|
134
|
+
settle(failure(new ExecError(`${shown} ${reason}`, { command, code, ...collected })));
|
|
135
|
+
});
|
|
136
|
+
});
|
package/dist/flag.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flag declarations: booleans with a generated `--no-<name>`, and string and number flags that
|
|
3
|
+
* become repeated (`.multiple()`), comma lists (`.list()`) or take an optional value
|
|
4
|
+
* (`.optionalValue()`). Each declaration carries the type its handler reads off `ctx.flags`.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
10
|
+
import type { PromptDeclaration } from "./prompt.js";
|
|
11
|
+
/** Brands a flag declaration so a command can tell it from an argument. */
|
|
12
|
+
declare const FLAG: unique symbol;
|
|
13
|
+
/** Type-only slot carrying the value a declaration hands its handler. */
|
|
14
|
+
declare const VALUE: unique symbol;
|
|
15
|
+
/** Wraps a value type so an `undefined` inside it survives inference. */
|
|
16
|
+
export interface Carried<T> {
|
|
17
|
+
value: T;
|
|
18
|
+
}
|
|
19
|
+
/** What a flag reads from the command line. */
|
|
20
|
+
export type FlagKind = "boolean" | "string" | "number";
|
|
21
|
+
/** Everything one flag declares, as the parser and the help read it. */
|
|
22
|
+
export interface FlagSpec {
|
|
23
|
+
kind: FlagKind;
|
|
24
|
+
/** The long name, written `--<name>`. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** A one-character alias, written `-<short>`. */
|
|
27
|
+
short: string | undefined;
|
|
28
|
+
/** Other long names the flag answers to, such as `jobs` for `concurrency`. */
|
|
29
|
+
aliases: readonly string[];
|
|
30
|
+
description: string | undefined;
|
|
31
|
+
/** What the help shows in `<…>` after the flag. */
|
|
32
|
+
valueName: string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Whether `default` was declared; a declared `undefined` still counts. A boolean without one
|
|
35
|
+
* still reads `false` when missing, but a prompt bound to it asks, as one does for a default
|
|
36
|
+
* of `undefined`.
|
|
37
|
+
*/
|
|
38
|
+
hasDefault: boolean;
|
|
39
|
+
default: unknown;
|
|
40
|
+
/** A missing value is a usage error. Implied by a prompt without a default. */
|
|
41
|
+
required: boolean;
|
|
42
|
+
/** Checks each value: every element of a repeated or list flag. */
|
|
43
|
+
schema: StandardSchemaV1 | undefined;
|
|
44
|
+
prompt: PromptDeclaration | undefined;
|
|
45
|
+
/** Repeating the flag collects every value instead of keeping the last. */
|
|
46
|
+
multiple: boolean;
|
|
47
|
+
/** One value holds several, separated by commas. */
|
|
48
|
+
list: boolean;
|
|
49
|
+
/** The flag may appear bare, which reads as `true`, or with `=value`. */
|
|
50
|
+
optionalValue: boolean;
|
|
51
|
+
/** Left out of the help. */
|
|
52
|
+
hidden: boolean;
|
|
53
|
+
/** The help section the flag is listed under; `undefined` lists it under "Options". */
|
|
54
|
+
heading: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
/** A declared flag, typed with the value its handler reads. */
|
|
57
|
+
export interface Flag<Value = unknown> {
|
|
58
|
+
readonly [FLAG]: FlagSpec;
|
|
59
|
+
readonly [VALUE]?: Carried<Value>;
|
|
60
|
+
}
|
|
61
|
+
/** The value a flag declaration hands its handler. */
|
|
62
|
+
export type FlagValue<F> = F extends {
|
|
63
|
+
readonly [VALUE]?: Carried<infer V> | undefined;
|
|
64
|
+
} ? V : never;
|
|
65
|
+
/** Whether `value` is a flag declaration. */
|
|
66
|
+
export declare function isFlag(value: unknown): value is Flag;
|
|
67
|
+
/** The spec behind a flag declaration. */
|
|
68
|
+
export declare function flagSpec(flag: Flag): FlagSpec;
|
|
69
|
+
/** The value of a string or number flag, from the shape its modifiers gave it. */
|
|
70
|
+
type ValueOutput<Item, Required extends boolean, Many extends boolean, Bare extends boolean> = (Many extends true ? Item[] : Required extends true ? Item : Item | undefined) | (Bare extends true ? true : never);
|
|
71
|
+
/** A string or number flag, with the modifiers that reshape its value. */
|
|
72
|
+
export interface ValueFlag<Item, Required extends boolean = false, Many extends boolean = false, Bare extends boolean = false> extends Flag<ValueOutput<Item, Required, Many, Bare>> {
|
|
73
|
+
/**
|
|
74
|
+
* Every occurrence is kept, in order: `--tag a --tag b` reads `["a", "b"]`, and a missing
|
|
75
|
+
* flag reads `[]`.
|
|
76
|
+
*/
|
|
77
|
+
multiple(): ValueFlag<Item, Required, true, Bare>;
|
|
78
|
+
/**
|
|
79
|
+
* Each value is split at commas, with blanks dropped: `--allow-env=HOME,PATH` reads
|
|
80
|
+
* `["HOME", "PATH"]`. Repeating the flag adds to the list, and a missing flag reads `[]`.
|
|
81
|
+
*/
|
|
82
|
+
list(): ValueFlag<Item, Required, true, Bare>;
|
|
83
|
+
/**
|
|
84
|
+
* The flag may appear bare, which reads `true`, or with a value after `=`, which reads that
|
|
85
|
+
* value: `--allow-net` grants everything, `--allow-net=api.example.com` one host. A bare
|
|
86
|
+
* occurrence anywhere wins over values in other occurrences.
|
|
87
|
+
*/
|
|
88
|
+
optionalValue(): ValueFlag<Item, Required, Many, true>;
|
|
89
|
+
}
|
|
90
|
+
/** Options every flag takes. */
|
|
91
|
+
export interface FlagOptions {
|
|
92
|
+
description?: string;
|
|
93
|
+
/** A one-character alias, such as `"f"` for `-f`. */
|
|
94
|
+
short?: string;
|
|
95
|
+
/** Other long names the flag answers to. */
|
|
96
|
+
aliases?: readonly string[];
|
|
97
|
+
/** Asked when the flag is missing and stdin and stderr are terminals. */
|
|
98
|
+
prompt?: PromptDeclaration;
|
|
99
|
+
/** Left out of the help. */
|
|
100
|
+
hidden?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Lists the flag in the help under this section title instead of "Options", so a command
|
|
103
|
+
* with many flags reads in groups. Sections follow in the order their first flag is declared.
|
|
104
|
+
*/
|
|
105
|
+
heading?: string;
|
|
106
|
+
}
|
|
107
|
+
/** Options a boolean flag takes. */
|
|
108
|
+
export interface BooleanFlagOptions extends FlagOptions {
|
|
109
|
+
/**
|
|
110
|
+
* The value when neither `--<name>` nor `--no-<name>` is passed. `undefined` leaves the flag
|
|
111
|
+
* unset, so a configured default or a later question decides, and the help shows `--[no-]<name>`.
|
|
112
|
+
* @default false
|
|
113
|
+
*/
|
|
114
|
+
default?: boolean | undefined;
|
|
115
|
+
}
|
|
116
|
+
/** The handler's value type for a boolean flag declared with `options`. */
|
|
117
|
+
type BooleanValue<Options> = Options extends {
|
|
118
|
+
default: infer Default;
|
|
119
|
+
} ? undefined extends Default ? boolean | undefined : boolean : boolean;
|
|
120
|
+
/** Options a string or number flag takes. */
|
|
121
|
+
export interface ValueFlagOptions extends FlagOptions {
|
|
122
|
+
/** What the help shows in `<…>`. @default "value", or "number" for a number flag */
|
|
123
|
+
valueName?: string;
|
|
124
|
+
/** Checks each value; its output is what the handler reads. */
|
|
125
|
+
schema?: StandardSchemaV1;
|
|
126
|
+
/** The value when the flag is missing, taken as the handler reads it. */
|
|
127
|
+
default?: unknown;
|
|
128
|
+
/** A missing flag is a usage error. */
|
|
129
|
+
required?: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** The handler's value type for a flag declared with `options`, before any modifier. */
|
|
132
|
+
type ItemOf<Options, Fallback> = Options extends {
|
|
133
|
+
schema: StandardSchemaV1<infer _Input, infer Output>;
|
|
134
|
+
} ? Output : Fallback;
|
|
135
|
+
/** Whether a flag declared with `options` always has a value once the command runs. */
|
|
136
|
+
type IsRequired<Options> = Options extends {
|
|
137
|
+
default: unknown;
|
|
138
|
+
} ? true : Options extends {
|
|
139
|
+
required: true;
|
|
140
|
+
} ? true : Options extends {
|
|
141
|
+
prompt: PromptDeclaration;
|
|
142
|
+
} ? true : false;
|
|
143
|
+
/**
|
|
144
|
+
* Flag declarations. A flag belongs to the command that declares it, and its key in that
|
|
145
|
+
* command's `flags` is the property the handler reads.
|
|
146
|
+
*
|
|
147
|
+
* @example flags: { dryRun: flag.boolean("dry-run", { description: "Print it and write nothing" }) }
|
|
148
|
+
* @example flags: { allowNet: flag.string("allow-net").optionalValue().list() }
|
|
149
|
+
* @example flags: { retries: flag.number("retries", { default: 0 }) }
|
|
150
|
+
*/
|
|
151
|
+
export declare const flag: {
|
|
152
|
+
/**
|
|
153
|
+
* `--<name>` reads `true` and `--no-<name>` reads `false`; passing both is a usage error.
|
|
154
|
+
* Neither reads the `default`: `false`, or `undefined` for a tri-state flag declared with
|
|
155
|
+
* `default: undefined`.
|
|
156
|
+
*
|
|
157
|
+
* @param name - The long name, in kebab case
|
|
158
|
+
* @example flags: { git: flag.boolean("git", { default: undefined }) } // boolean | undefined
|
|
159
|
+
*/
|
|
160
|
+
boolean<const Options extends BooleanFlagOptions = Record<never, never>>(name: string, options?: Options): Flag<BooleanValue<Options>>;
|
|
161
|
+
/**
|
|
162
|
+
* `--<name> value` or `--<name>=value`, checked by `schema` when one is given.
|
|
163
|
+
*
|
|
164
|
+
* @param name - The long name, in kebab case
|
|
165
|
+
*/
|
|
166
|
+
string<const Options extends ValueFlagOptions = Record<never, never>>(name: string, options?: Options): ValueFlag<ItemOf<Options, string>, IsRequired<Options>>;
|
|
167
|
+
/**
|
|
168
|
+
* A string flag read as a number before `schema` checks it; text that is not a number is a
|
|
169
|
+
* usage error.
|
|
170
|
+
*
|
|
171
|
+
* @param name - The long name, in kebab case
|
|
172
|
+
*/
|
|
173
|
+
number<const Options extends ValueFlagOptions = Record<never, never>>(name: string, options?: Options): ValueFlag<ItemOf<Options, number>, IsRequired<Options>>;
|
|
174
|
+
};
|
|
175
|
+
export {};
|
package/dist/flag.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flag declarations: booleans with a generated `--no-<name>`, and string and number flags that
|
|
3
|
+
* become repeated (`.multiple()`), comma lists (`.list()`) or take an optional value
|
|
4
|
+
* (`.optionalValue()`). Each declaration carries the type its handler reads off `ctx.flags`.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/** Brands a flag declaration so a command can tell it from an argument. */
|
|
10
|
+
const FLAG = Symbol.for("@sdxc/cli.flag");
|
|
11
|
+
/** Whether `value` is a flag declaration. */
|
|
12
|
+
export function isFlag(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && FLAG in value;
|
|
14
|
+
}
|
|
15
|
+
/** The spec behind a flag declaration. */
|
|
16
|
+
export function flagSpec(flag) {
|
|
17
|
+
return flag[FLAG];
|
|
18
|
+
}
|
|
19
|
+
/** Builds the spec shared by every flag kind. */
|
|
20
|
+
function baseSpec(kind, name, options) {
|
|
21
|
+
return {
|
|
22
|
+
kind,
|
|
23
|
+
name,
|
|
24
|
+
short: options.short,
|
|
25
|
+
aliases: options.aliases ?? [],
|
|
26
|
+
description: options.description,
|
|
27
|
+
valueName: options.valueName,
|
|
28
|
+
hasDefault: "default" in options,
|
|
29
|
+
default: options.default,
|
|
30
|
+
required: options.required === true || (options.prompt !== undefined && !("default" in options)),
|
|
31
|
+
schema: options.schema,
|
|
32
|
+
prompt: options.prompt,
|
|
33
|
+
multiple: false,
|
|
34
|
+
list: false,
|
|
35
|
+
optionalValue: false,
|
|
36
|
+
hidden: options.hidden === true,
|
|
37
|
+
heading: options.heading,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** A value flag around `spec`, whose modifiers each answer a new declaration. */
|
|
41
|
+
function valueFlag(spec) {
|
|
42
|
+
return {
|
|
43
|
+
[FLAG]: spec,
|
|
44
|
+
multiple: () => valueFlag({ ...spec, multiple: true }),
|
|
45
|
+
list: () => valueFlag({ ...spec, list: true }),
|
|
46
|
+
optionalValue: () => valueFlag({ ...spec, optionalValue: true }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Flag declarations. A flag belongs to the command that declares it, and its key in that
|
|
51
|
+
* command's `flags` is the property the handler reads.
|
|
52
|
+
*
|
|
53
|
+
* @example flags: { dryRun: flag.boolean("dry-run", { description: "Print it and write nothing" }) }
|
|
54
|
+
* @example flags: { allowNet: flag.string("allow-net").optionalValue().list() }
|
|
55
|
+
* @example flags: { retries: flag.number("retries", { default: 0 }) }
|
|
56
|
+
*/
|
|
57
|
+
export const flag = {
|
|
58
|
+
/**
|
|
59
|
+
* `--<name>` reads `true` and `--no-<name>` reads `false`; passing both is a usage error.
|
|
60
|
+
* Neither reads the `default`: `false`, or `undefined` for a tri-state flag declared with
|
|
61
|
+
* `default: undefined`.
|
|
62
|
+
*
|
|
63
|
+
* @param name - The long name, in kebab case
|
|
64
|
+
* @example flags: { git: flag.boolean("git", { default: undefined }) } // boolean | undefined
|
|
65
|
+
*/
|
|
66
|
+
boolean(name, options) {
|
|
67
|
+
let given = options ?? {};
|
|
68
|
+
let spec = baseSpec("boolean", name, given);
|
|
69
|
+
if (!spec.hasDefault)
|
|
70
|
+
spec.default = false;
|
|
71
|
+
spec.required = false;
|
|
72
|
+
return { [FLAG]: spec };
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* `--<name> value` or `--<name>=value`, checked by `schema` when one is given.
|
|
76
|
+
*
|
|
77
|
+
* @param name - The long name, in kebab case
|
|
78
|
+
*/
|
|
79
|
+
string(name, options) {
|
|
80
|
+
return valueFlag(baseSpec("string", name, options ?? {}));
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* A string flag read as a number before `schema` checks it; text that is not a number is a
|
|
84
|
+
* usage error.
|
|
85
|
+
*
|
|
86
|
+
* @param name - The long name, in kebab case
|
|
87
|
+
*/
|
|
88
|
+
number(name, options) {
|
|
89
|
+
return valueFlag(baseSpec("number", name, options ?? {}));
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createCommand()`, which pairs a handler with the command it runs. Passing the declaration is
|
|
3
|
+
* what types `ctx.arguments` and `ctx.flags`; the middleware listed beside the handler, and the
|
|
4
|
+
* CLI's own when an app augments {@link CLITypes}, type what they publish.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ArgumentsOf, FlagsOf, Node } from "./command.js";
|
|
10
|
+
import type { AnyCommandContext, CommandContext } from "./context.js";
|
|
11
|
+
import type { Carried } from "./flag.js";
|
|
12
|
+
import type { AnyCommandMiddleware, ChainProperties, CommandResult } from "./middleware.js";
|
|
13
|
+
/**
|
|
14
|
+
* Augmented by an app to name its CLI, so handlers written in their own files read what the
|
|
15
|
+
* CLI's middleware publishes, the way `JobTypes` does for jobs.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* declare module "@sdxc/cli" {
|
|
19
|
+
* interface CLITypes {
|
|
20
|
+
* cli: typeof cli;
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export interface CLITypes {
|
|
25
|
+
}
|
|
26
|
+
/** The middleware chain of the CLI an app named in {@link CLITypes}. */
|
|
27
|
+
type CurrentChain = CLITypes extends {
|
|
28
|
+
cli: {
|
|
29
|
+
readonly middleware: infer Chain;
|
|
30
|
+
};
|
|
31
|
+
} ? Chain extends readonly AnyCommandMiddleware[] ? Chain : [] : [];
|
|
32
|
+
/** The context a command's handler receives, with what its middleware chain installed. */
|
|
33
|
+
export type HandlerContext<Target, Chain extends readonly AnyCommandMiddleware[] = [], Global extends readonly AnyCommandMiddleware[] = CurrentChain> = CommandContext<ArgumentsOf<Target>, FlagsOf<Target>> & ChainProperties<Global> & ChainProperties<Chain>;
|
|
34
|
+
/**
|
|
35
|
+
* The context of any command that runs behind `Chain`, whatever it parsed: the type a helper
|
|
36
|
+
* shared by several commands takes, since each command's own context is assignable to it.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const MAKE = [project(), packageRoot()] as const;
|
|
40
|
+
* function write(ctx: ChainContext<typeof MAKE>, file: string) { ctx.project.root; }
|
|
41
|
+
*/
|
|
42
|
+
export type ChainContext<Chain extends readonly AnyCommandMiddleware[] = [], Global extends readonly AnyCommandMiddleware[] = CurrentChain> = AnyCommandContext & ChainProperties<Global> & ChainProperties<Chain>;
|
|
43
|
+
/** The work itself: a success exits 0 and a failure exits by its kind. */
|
|
44
|
+
export type CommandHandler<Context> = (context: Context) => CommandResult | Promise<CommandResult>;
|
|
45
|
+
/** Brands a command implementation so `map()` can tell it from a bare handler. */
|
|
46
|
+
declare const ACTION: unique symbol;
|
|
47
|
+
/** Type-only slot naming the command an action was written for. */
|
|
48
|
+
declare const TARGET: unique symbol;
|
|
49
|
+
/** A handler paired with the middleware that runs before it alone. */
|
|
50
|
+
export interface CommandAction<Target = Node, Chain extends readonly AnyCommandMiddleware[] = readonly AnyCommandMiddleware[]> {
|
|
51
|
+
readonly [ACTION]: true;
|
|
52
|
+
readonly [TARGET]?: Carried<Target>;
|
|
53
|
+
readonly middleware: Chain;
|
|
54
|
+
/** The work itself; a method, so an action written for one chain maps wherever its command does. */
|
|
55
|
+
handler(this: void, context: HandlerContext<Target, Chain>): CommandResult | Promise<CommandResult>;
|
|
56
|
+
}
|
|
57
|
+
/** Whether `value` is what `createCommand` returns. */
|
|
58
|
+
export declare function isAction(value: unknown): value is CommandAction<Node, readonly AnyCommandMiddleware[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Types one command's implementation against its declaration, so it can live in its own file
|
|
61
|
+
* and be mapped through `lazy()`.
|
|
62
|
+
*
|
|
63
|
+
* @param target - The declared command (or group) this implements
|
|
64
|
+
* @param action - The handler, or `{ middleware, handler }` for middleware that runs before
|
|
65
|
+
* this command alone
|
|
66
|
+
* @example export default createCommand(sdxc.ui.add, async (ctx) => success());
|
|
67
|
+
* @example export default createCommand(sdxc.make.job, { middleware: [project()], handler: (ctx) => make(ctx.project) });
|
|
68
|
+
*/
|
|
69
|
+
export declare function createCommand<Target extends Node, const Chain extends readonly AnyCommandMiddleware[] = []>(target: Target, action: CommandHandler<HandlerContext<Target, Chain>> | {
|
|
70
|
+
middleware: Chain;
|
|
71
|
+
handler: CommandHandler<HandlerContext<Target, Chain>>;
|
|
72
|
+
}): CommandAction<Target, Chain>;
|
|
73
|
+
export {};
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createCommand()`, which pairs a handler with the command it runs. Passing the declaration is
|
|
3
|
+
* what types `ctx.arguments` and `ctx.flags`; the middleware listed beside the handler, and the
|
|
4
|
+
* CLI's own when an app augments {@link CLITypes}, type what they publish.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/** Brands a command implementation so `map()` can tell it from a bare handler. */
|
|
10
|
+
const ACTION = Symbol.for("@sdxc/cli.action");
|
|
11
|
+
/** Whether `value` is what `createCommand` returns. */
|
|
12
|
+
export function isAction(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && ACTION in value;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Types one command's implementation against its declaration, so it can live in its own file
|
|
17
|
+
* and be mapped through `lazy()`.
|
|
18
|
+
*
|
|
19
|
+
* @param target - The declared command (or group) this implements
|
|
20
|
+
* @param action - The handler, or `{ middleware, handler }` for middleware that runs before
|
|
21
|
+
* this command alone
|
|
22
|
+
* @example export default createCommand(sdxc.ui.add, async (ctx) => success());
|
|
23
|
+
* @example export default createCommand(sdxc.make.job, { middleware: [project()], handler: (ctx) => make(ctx.project) });
|
|
24
|
+
*/
|
|
25
|
+
export function createCommand(target, action) {
|
|
26
|
+
if (typeof action === "function") {
|
|
27
|
+
return { [ACTION]: true, middleware: [], handler: action };
|
|
28
|
+
}
|
|
29
|
+
return { [ACTION]: true, middleware: action.middleware, handler: action.handler };
|
|
30
|
+
}
|
package/dist/help.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help rendered from the contract: the usage line, the description, the subcommands, the
|
|
3
|
+
* arguments, then the flags with their value names and defaults, under their headings. Every `--help` and every usage
|
|
4
|
+
* error prints it, so the text and what the parser accepts come from one declaration.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Style } from "./style.js";
|
|
10
|
+
import type { Entry } from "./tree.js";
|
|
11
|
+
/** What the help knows about the program beyond the contract. */
|
|
12
|
+
export interface HelpProgram {
|
|
13
|
+
version: string | undefined;
|
|
14
|
+
/** Shown at the root when the root group has no description of its own. */
|
|
15
|
+
description: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Renders `entry`'s help, ending with a newline. */
|
|
18
|
+
export declare function renderHelp(entry: Entry, program: HelpProgram, style: Style): string;
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help rendered from the contract: the usage line, the description, the subcommands, the
|
|
3
|
+
* arguments, then the flags with their value names and defaults, under their headings. Every `--help` and every usage
|
|
4
|
+
* error prints it, so the text and what the parser accepts come from one declaration.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import { groupSpec, isGroup } from "./command.js";
|
|
10
|
+
import { visibleWidth } from "./style.js";
|
|
11
|
+
/** Renders `entry`'s help, ending with a newline. */
|
|
12
|
+
export function renderHelp(entry, program, style) {
|
|
13
|
+
let node = entry.node;
|
|
14
|
+
let sections = [];
|
|
15
|
+
let lines = [];
|
|
16
|
+
if (isGroup(node))
|
|
17
|
+
lines.push(`${entry.title} <command> [options]`);
|
|
18
|
+
if (!isGroup(node) || entry.arguments.length > 0 || entry.flags.length > 0) {
|
|
19
|
+
let usage = [entry.title];
|
|
20
|
+
for (let argument of entry.arguments)
|
|
21
|
+
usage.push(argumentLabel(argument.key, argument.spec));
|
|
22
|
+
usage.push("[options]");
|
|
23
|
+
lines.push(usage.join(" "));
|
|
24
|
+
}
|
|
25
|
+
let indent = " ".repeat("Usage: ".length);
|
|
26
|
+
sections.push(`${style.bold("Usage:")} ${lines.join(`\n${indent}`)}`);
|
|
27
|
+
let description = isGroup(node) ? groupSpec(node).description : node.description;
|
|
28
|
+
if (entry.parent === undefined)
|
|
29
|
+
description ??= program.description;
|
|
30
|
+
if (description !== undefined)
|
|
31
|
+
sections.push(description);
|
|
32
|
+
if (isGroup(node)) {
|
|
33
|
+
let rows = [];
|
|
34
|
+
for (let [name, child] of entry.children) {
|
|
35
|
+
let spec = isGroup(child.node) ? groupSpec(child.node) : child.node;
|
|
36
|
+
if (spec.hidden)
|
|
37
|
+
continue;
|
|
38
|
+
rows.push([name, spec.description ?? ""]);
|
|
39
|
+
}
|
|
40
|
+
sections.push(section("Commands:", rows, style));
|
|
41
|
+
}
|
|
42
|
+
if (entry.arguments.length > 0) {
|
|
43
|
+
let rows = entry.arguments.map(({ key, spec }) => {
|
|
44
|
+
let notes = spec.description ?? "";
|
|
45
|
+
if (spec.hasDefault)
|
|
46
|
+
notes = withDefault(notes, spec.default);
|
|
47
|
+
return [argumentLabel(key, spec), notes];
|
|
48
|
+
});
|
|
49
|
+
sections.push(section("Arguments:", rows, style));
|
|
50
|
+
}
|
|
51
|
+
let headings = new Map([["Options", []]]);
|
|
52
|
+
for (let { spec } of entry.flags) {
|
|
53
|
+
if (spec.hidden)
|
|
54
|
+
continue;
|
|
55
|
+
let heading = spec.heading ?? "Options";
|
|
56
|
+
let rows = headings.get(heading) ?? [];
|
|
57
|
+
headings.set(heading, rows);
|
|
58
|
+
rows.push([flagLabel(spec), flagNotes(spec)]);
|
|
59
|
+
}
|
|
60
|
+
let builtins = headings.get("Options");
|
|
61
|
+
if (entry.tasks)
|
|
62
|
+
builtins.push(["--no-progress", "Print tasks without animation"]);
|
|
63
|
+
if (program.version !== undefined && entry.parent === undefined) {
|
|
64
|
+
builtins.push(["--version", "Print the version"]);
|
|
65
|
+
}
|
|
66
|
+
builtins.push(["-h, --help", "Show this help"]);
|
|
67
|
+
for (let [heading, rows] of headings)
|
|
68
|
+
sections.push(section(`${heading}:`, rows, style));
|
|
69
|
+
if (!isGroup(node) && node.examples.length > 0) {
|
|
70
|
+
let program = entry.title.split(" ")[0];
|
|
71
|
+
let lines = node.examples.map((example) => ` ${program} ${example}`);
|
|
72
|
+
sections.push(`${style.bold("Examples:")}\n${lines.join("\n")}`);
|
|
73
|
+
}
|
|
74
|
+
return `${sections.join("\n\n")}\n`;
|
|
75
|
+
}
|
|
76
|
+
/** A titled section of aligned rows, flag and command names styled. */
|
|
77
|
+
function section(title, rows, style) {
|
|
78
|
+
let width = Math.max(0, ...rows.map(([left]) => visibleWidth(left)));
|
|
79
|
+
let lines = rows.map(([left, right]) => {
|
|
80
|
+
let padded = left + " ".repeat(width - visibleWidth(left));
|
|
81
|
+
return right === "" ? ` ${style.cyan(left)}` : ` ${style.cyan(padded)} ${right}`;
|
|
82
|
+
});
|
|
83
|
+
return `${style.bold(title)}\n${lines.join("\n")}`;
|
|
84
|
+
}
|
|
85
|
+
/** `<name>`, `[name]` or `<name...>`, as the usage line writes an argument. */
|
|
86
|
+
function argumentLabel(key, spec) {
|
|
87
|
+
let name = spec.valueName ?? key;
|
|
88
|
+
if (spec.variadic)
|
|
89
|
+
return spec.min > 0 ? `<${name}...>` : `[${name}...]`;
|
|
90
|
+
return spec.min > 0 ? `<${name}>` : `[${name}]`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The left column for one flag: its short alias, its names, and its value. A boolean shows the
|
|
94
|
+
* spelling that changes something: `--name` when it defaults to false, `--no-name` when it
|
|
95
|
+
* defaults to true, and `--[no-]name` when it asks or is left unset.
|
|
96
|
+
*/
|
|
97
|
+
function flagLabel(spec) {
|
|
98
|
+
let names = [];
|
|
99
|
+
if (spec.short !== undefined)
|
|
100
|
+
names.push(`-${spec.short}`);
|
|
101
|
+
if (spec.kind === "boolean") {
|
|
102
|
+
let prefix = spec.prompt !== undefined || spec.default === undefined
|
|
103
|
+
? "[no-]"
|
|
104
|
+
: spec.default === true
|
|
105
|
+
? "no-"
|
|
106
|
+
: "";
|
|
107
|
+
names.push(`--${prefix}${spec.name}`);
|
|
108
|
+
for (let alias of spec.aliases)
|
|
109
|
+
names.push(`--${prefix}${alias}`);
|
|
110
|
+
return names.join(", ");
|
|
111
|
+
}
|
|
112
|
+
for (let name of [spec.name, ...spec.aliases])
|
|
113
|
+
names.push(`--${name}`);
|
|
114
|
+
let value = spec.valueName ?? (spec.kind === "number" ? "number" : "value");
|
|
115
|
+
if (spec.list)
|
|
116
|
+
value = `${value},...`;
|
|
117
|
+
let label = names.join(", ");
|
|
118
|
+
return spec.optionalValue ? `${label}[=<${value}>]` : `${label} <${value}>`;
|
|
119
|
+
}
|
|
120
|
+
/** The right column for one flag: its description, whether it repeats, and its default. */
|
|
121
|
+
function flagNotes(spec) {
|
|
122
|
+
let notes = spec.description ?? "";
|
|
123
|
+
if (spec.multiple)
|
|
124
|
+
notes = `${notes} (repeatable)`.trim();
|
|
125
|
+
if (spec.required && spec.prompt === undefined)
|
|
126
|
+
notes = `${notes} (required)`.trim();
|
|
127
|
+
if (spec.kind !== "boolean" && spec.hasDefault)
|
|
128
|
+
notes = withDefault(notes, spec.default);
|
|
129
|
+
return notes;
|
|
130
|
+
}
|
|
131
|
+
/** A default value as the help prints it. */
|
|
132
|
+
function display(value) {
|
|
133
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
134
|
+
}
|
|
135
|
+
/** `notes` with the default appended, when there is one worth showing. */
|
|
136
|
+
function withDefault(notes, value) {
|
|
137
|
+
if (value === undefined || (Array.isArray(value) && value.length === 0))
|
|
138
|
+
return notes;
|
|
139
|
+
let shown = Array.isArray(value) ? value.map(display).join(",") : display(value);
|
|
140
|
+
return `${notes} (default: ${shown})`.trim();
|
|
141
|
+
}
|