@sdxc/cli 0.0.0-pre.1
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 +364 -0
- package/dist/argument.d.ts +120 -0
- package/dist/argument.js +104 -0
- package/dist/cli.d.ts +68 -0
- package/dist/cli.js +186 -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/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 +35 -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 +218 -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 +41 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergio Xalambrí
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# @sdxc/cli
|
|
2
|
+
|
|
3
|
+
Build command-line tools with generated help, prompts bound to flags, tasks and a test harness.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm add @sdxc/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Handlers answer a `Result` from [`@sdxc/result`](https://www.npmjs.com/package/@sdxc/result), which installs alongside. Arguments and flags are checked by any [Standard Schema](https://standardschema.dev); the examples use `remix/data-schema` from [`remix`](https://www.npmjs.com/package/remix), whose context keys also type what middleware publishes. It runs on Node and Bun.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### A command with an argument and a flag
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { argument, command, commands, createCLI, flag, success } from "@sdxc/cli";
|
|
19
|
+
import * as s from "remix/data-schema";
|
|
20
|
+
|
|
21
|
+
let contract = commands({
|
|
22
|
+
greet: command({
|
|
23
|
+
description: "Say hello",
|
|
24
|
+
arguments: { name: argument(s.string()) },
|
|
25
|
+
flags: { shout: flag.boolean("shout", { description: "Use capitals" }) },
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let cli = createCLI({ name: "hello", version: "1.0.0", commands: contract });
|
|
30
|
+
|
|
31
|
+
cli.map(contract.greet, (ctx) => {
|
|
32
|
+
let greeting = `Hello, ${ctx.arguments.name}!`;
|
|
33
|
+
ctx.stdout(`${ctx.flags.shout ? greeting.toUpperCase() : greeting}\n`);
|
|
34
|
+
return success();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
process.exitCode = await cli.run(process.argv.slice(2));
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
hello greet Ada --shout # HELLO, ADA!
|
|
42
|
+
hello greet --help # usage, arguments and flags, generated from contract
|
|
43
|
+
hello gret Ada # Unknown command "gret" for hello; did you mean "greet"?
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A success exits 0, a failure prints its message and exits 1, `usage("…")` prints the command's help and exits 2, and a cancelled prompt or task exits 130.
|
|
47
|
+
|
|
48
|
+
### Ask for what the command line left out
|
|
49
|
+
|
|
50
|
+
A prompt is declared on the flag or argument it fills, so every question can also be answered on the command line.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { argument, command, flag, prompt } from "@sdxc/cli";
|
|
54
|
+
import * as s from "remix/data-schema";
|
|
55
|
+
|
|
56
|
+
let create = command({
|
|
57
|
+
arguments: {
|
|
58
|
+
directory: argument(s.string(), {
|
|
59
|
+
prompt: prompt.text({ message: "Where?", placeholder: "my-app" }),
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
flags: {
|
|
63
|
+
template: flag.string("template", {
|
|
64
|
+
default: "basic",
|
|
65
|
+
prompt: prompt.select({
|
|
66
|
+
message: "Which template?",
|
|
67
|
+
options: ["basic", "full"],
|
|
68
|
+
ask: "always",
|
|
69
|
+
}),
|
|
70
|
+
}),
|
|
71
|
+
git: flag.boolean("git", { prompt: prompt.confirm({ message: "Initialize git?" }) }),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A value on the command line is never asked for. A missing one is asked when stdin and stderr are terminals; otherwise its `default` applies, and an input with neither is a usage error naming the flag, so a run in CI never waits on a question.
|
|
77
|
+
|
|
78
|
+
### Show progress while work runs
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { isFailure } from "@sdxc/result";
|
|
82
|
+
|
|
83
|
+
cli.map(contract.sync, async (ctx) => {
|
|
84
|
+
let posts = await ctx.task("Fetching posts", async (task) => {
|
|
85
|
+
let response = await fetch(FEED_URL, { signal: task.signal });
|
|
86
|
+
let list: Post[] = await response.json();
|
|
87
|
+
task.update(`Fetched ${list.length} posts`);
|
|
88
|
+
return list;
|
|
89
|
+
});
|
|
90
|
+
if (isFailure(posts)) return posts; // the task already printed ✗
|
|
91
|
+
|
|
92
|
+
return ctx.task("Saving posts", async (task) => {
|
|
93
|
+
task.total(posts.data.length);
|
|
94
|
+
await Promise.all(
|
|
95
|
+
posts.data.map(async (post) => {
|
|
96
|
+
await save(post);
|
|
97
|
+
task.advance();
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A task answers a `Result`: what the callback returned, or the error it threw. Its final line is ✓ or ✗ with the last label, and the count when it had a total. Ctrl+C aborts `task.signal`.
|
|
105
|
+
|
|
106
|
+
### Test a command
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { invoke } from "@sdxc/cli/test";
|
|
110
|
+
|
|
111
|
+
let run = await invoke(cli, ["greet", "Ada"]);
|
|
112
|
+
run.code; // 0
|
|
113
|
+
run.stdout; // "Hello, Ada!\n"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
### Declaring commands
|
|
119
|
+
|
|
120
|
+
#### `commands(members)`
|
|
121
|
+
|
|
122
|
+
A group of commands; groups nest, so `app users find` is `contract.users.find`.
|
|
123
|
+
|
|
124
|
+
#### `command({ description, arguments, flags, examples })`
|
|
125
|
+
|
|
126
|
+
One command. Its `arguments` and `flags` type the handler's `ctx.arguments` and `ctx.flags`.
|
|
127
|
+
|
|
128
|
+
#### `argument(schema, options?)`
|
|
129
|
+
|
|
130
|
+
A positional value checked by `schema`, with `optional`, `default` or a `prompt`.
|
|
131
|
+
|
|
132
|
+
#### `argument.variadic(schema, { min?, max? })`
|
|
133
|
+
|
|
134
|
+
Every remaining value, as an array.
|
|
135
|
+
|
|
136
|
+
#### `argument.rest()`
|
|
137
|
+
|
|
138
|
+
Every token after the command's name, unparsed, to hand to another program.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
command({ arguments: { args: argument.rest() } }); // prog run users --help → ["users", "--help"]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### `flag.boolean(name, options?)`
|
|
145
|
+
|
|
146
|
+
`--name`, and `--no-name` to turn it off.
|
|
147
|
+
|
|
148
|
+
#### `flag.string(name, options?)` and `flag.number(name, options?)`
|
|
149
|
+
|
|
150
|
+
`--name value` or `--name=value`, with `default`, `required`, `short`, `schema` or a `prompt`. Chain `.multiple()` to repeat it, `.list()` for `--env=HOME,PATH`, or `.optionalValue()` to allow a bare `--name`.
|
|
151
|
+
|
|
152
|
+
#### `prompt.text`, `password`, `number`, `confirm`, `select`, `multiselect`, `search`
|
|
153
|
+
|
|
154
|
+
What to ask when a flag or argument is missing on a terminal: `prompt.select({ message: "Which?", options: ["a", "b"] })`.
|
|
155
|
+
|
|
156
|
+
### Running commands
|
|
157
|
+
|
|
158
|
+
#### `createCLI({ name, version?, commands, middleware? })`
|
|
159
|
+
|
|
160
|
+
The program. `--help` and `--version` come for free.
|
|
161
|
+
|
|
162
|
+
#### `cli.map(command, handler)`
|
|
163
|
+
|
|
164
|
+
Binds a handler, a `createCommand(...)` or a `lazy(...)` import to a command.
|
|
165
|
+
|
|
166
|
+
#### `cli.run(argv, io?)`
|
|
167
|
+
|
|
168
|
+
Runs the command and answers its exit code; `io` replaces the process's `cwd`, `env`, streams or `exec`.
|
|
169
|
+
|
|
170
|
+
#### `createCommand(command, handler)`
|
|
171
|
+
|
|
172
|
+
A handler typed from its command, for a module of its own; pass `{ middleware, handler }` for middleware this command alone runs.
|
|
173
|
+
|
|
174
|
+
#### `lazy(() => import("./command.js"))`
|
|
175
|
+
|
|
176
|
+
A handler loaded only when its command runs.
|
|
177
|
+
|
|
178
|
+
### The handler's context
|
|
179
|
+
|
|
180
|
+
| Member | What it is |
|
|
181
|
+
| ----------------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
182
|
+
| `ctx.arguments`, `ctx.flags` | The parsed input |
|
|
183
|
+
| `ctx.argv`, `ctx.path` | The raw arguments, and the command's name (`["users", "find"]`) |
|
|
184
|
+
| `ctx.cwd`, `ctx.env`, `ctx.signal` | The run's directory, environment, and the signal Ctrl+C aborts |
|
|
185
|
+
| `ctx.stdout(text)`, `ctx.stderr(text)` | Write output |
|
|
186
|
+
| `ctx.stdin` | `isTTY`, and `text()` for piped input |
|
|
187
|
+
| `ctx.style` | Colours, off without a terminal or with `NO_COLOR` |
|
|
188
|
+
| `ctx.prompt.text(…)` and the other kinds | Ask from a handler; `default` answers without a terminal |
|
|
189
|
+
| `ctx.task(label?, fn)` | Run `fn` under a spinner, answering a `Result` |
|
|
190
|
+
| `ctx.exec(command, options?)` | Run a program without a shell |
|
|
191
|
+
| `ctx.get(key)`, `ctx.set(key, value, { property })`, `ctx.require(key)` | Values middleware publishes |
|
|
192
|
+
| `ctx.setExecEnv(variables)` | Variables every `ctx.exec` child starts with |
|
|
193
|
+
|
|
194
|
+
### Results
|
|
195
|
+
|
|
196
|
+
| A handler answers | The run exits |
|
|
197
|
+
| ---------------------- | ------------------------------- |
|
|
198
|
+
| `success()` | 0 |
|
|
199
|
+
| `failure(error)` | 1, printing the error's message |
|
|
200
|
+
| `usage(message)` | 2, printing the command's help |
|
|
201
|
+
| `cancelled()` | 130 |
|
|
202
|
+
| `exit(code, message?)` | `code` |
|
|
203
|
+
|
|
204
|
+
`UsageError`, `CancelledError`, `ExitError` and `ExecError` are the errors behind them, `EXIT_CODES` holds the codes, and `exitCodeOf(error)` maps an error to its code.
|
|
205
|
+
|
|
206
|
+
### Testing
|
|
207
|
+
|
|
208
|
+
#### `invoke(cli, argv, options?)` from `@sdxc/cli/test`
|
|
209
|
+
|
|
210
|
+
Runs a command without a terminal and answers `{ code, stdout, stderr, exec }`. `options.answers` answers prompts by flag or argument name, `options.exec` replies to commands by their text (`{ "git init": { code: 0 } }`), and `options.stdin` is piped input.
|
|
211
|
+
|
|
212
|
+
### Other exports
|
|
213
|
+
|
|
214
|
+
- `spawnExec`: the default `exec`, over `node:child_process`.
|
|
215
|
+
- `createStyle(enabled)`: the colours `ctx.style` uses, for output outside a handler.
|
|
216
|
+
- Types: `CommandMiddleware<Effect>` (a middleware publishing `Effect`'s `property`), `Task`, `RunIO`, `Exec`, `ExecOptions`, `ExecOutput`, `HandlerContext`, `ChainContext` (the context a helper shared by several commands takes), `ArgumentsOf`, `FlagsOf`.
|
|
217
|
+
|
|
218
|
+
## Patterns
|
|
219
|
+
|
|
220
|
+
### Pattern: Publish a database to every command
|
|
221
|
+
|
|
222
|
+
Middleware runs before each handler and publishes what commands share, typed through a context key.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { argument, command, commands, createCLI, failure, success } from "@sdxc/cli";
|
|
226
|
+
import type { CommandMiddleware } from "@sdxc/cli";
|
|
227
|
+
import { DatabaseSync } from "node:sqlite";
|
|
228
|
+
import * as s from "remix/data-schema";
|
|
229
|
+
import { createContextKey } from "remix/router";
|
|
230
|
+
|
|
231
|
+
let Database = createContextKey<DatabaseSync>();
|
|
232
|
+
|
|
233
|
+
function database(
|
|
234
|
+
path: string,
|
|
235
|
+
): CommandMiddleware<{ key: typeof Database; value: DatabaseSync; property: "db" }> {
|
|
236
|
+
return async (ctx, next) => {
|
|
237
|
+
let db = new DatabaseSync(path);
|
|
238
|
+
ctx.set(Database, db, { property: "db" });
|
|
239
|
+
try {
|
|
240
|
+
await next();
|
|
241
|
+
} finally {
|
|
242
|
+
db.close();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let contract = commands({
|
|
248
|
+
users: commands({ find: command({ arguments: { email: argument(s.string()) } }) }),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
let cli = createCLI({ name: "app", commands: contract, middleware: [database("app.db")] });
|
|
252
|
+
|
|
253
|
+
cli.map(contract.users.find, (ctx) => {
|
|
254
|
+
let user = ctx.db.prepare("SELECT id, email FROM users WHERE email = ?").get(ctx.arguments.email);
|
|
255
|
+
if (user === undefined) return failure(new Error(`No user with email ${ctx.arguments.email}`));
|
|
256
|
+
ctx.stdout(`${user.id}\t${user.email}\n`);
|
|
257
|
+
return success();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
process.exitCode = await cli.run(process.argv.slice(2));
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Pattern: One module per command
|
|
264
|
+
|
|
265
|
+
A larger program declares its commands in one module and keeps each handler in its own, loaded only when it runs.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// commands.ts
|
|
269
|
+
import { command, commands, flag } from "@sdxc/cli";
|
|
270
|
+
|
|
271
|
+
export let contract = commands({
|
|
272
|
+
deploy: command({ description: "Build and upload", flags: { dryRun: flag.boolean("dry-run") } }),
|
|
273
|
+
logs: command({ description: "Stream the latest logs" }),
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// commands/deploy.ts
|
|
279
|
+
import { createCommand, success } from "@sdxc/cli";
|
|
280
|
+
import { isFailure } from "@sdxc/result";
|
|
281
|
+
|
|
282
|
+
import { contract } from "../commands.js";
|
|
283
|
+
|
|
284
|
+
export default createCommand(contract.deploy, async (ctx) => {
|
|
285
|
+
let built = await ctx.task("Building", () =>
|
|
286
|
+
ctx.exec(["npm", "run", "build"], { stdio: "stderr" }),
|
|
287
|
+
);
|
|
288
|
+
if (isFailure(built)) return built;
|
|
289
|
+
if (ctx.flags.dryRun) return success();
|
|
290
|
+
return ctx.task("Uploading", () => upload({ signal: ctx.signal }));
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// main.ts
|
|
296
|
+
import { createCLI, lazy } from "@sdxc/cli";
|
|
297
|
+
|
|
298
|
+
import { contract } from "./commands.js";
|
|
299
|
+
|
|
300
|
+
let cli = createCLI({ name: "ship", version: "1.0.0", commands: contract });
|
|
301
|
+
cli.map(
|
|
302
|
+
contract.deploy,
|
|
303
|
+
lazy(() => import("./commands/deploy.js")),
|
|
304
|
+
);
|
|
305
|
+
cli.map(
|
|
306
|
+
contract.logs,
|
|
307
|
+
lazy(() => import("./commands/logs.js")),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
process.exitCode = await cli.run(process.argv.slice(2));
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Pattern: Test prompts and child processes together
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { invoke } from "@sdxc/cli/test";
|
|
317
|
+
import { expect, test } from "vitest";
|
|
318
|
+
|
|
319
|
+
test("creates a project with git", async () => {
|
|
320
|
+
let run = await invoke(cli, ["create"], {
|
|
321
|
+
answers: { directory: "my-app", template: "basic", git: true },
|
|
322
|
+
exec: { "git init": { code: 0 } },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(run.code).toBe(0);
|
|
326
|
+
expect(run.exec.map((call) => call.command.join(" "))).toEqual(["git init"]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("needs a directory without a terminal", async () => {
|
|
330
|
+
let run = await invoke(cli, ["create"], { interactive: false });
|
|
331
|
+
|
|
332
|
+
expect(run.code).toBe(2);
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Versioning
|
|
337
|
+
|
|
338
|
+
Releases are dated rather than semantic. A version is the UTC date it was published,
|
|
339
|
+
written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one
|
|
340
|
+
release goes out per day.
|
|
341
|
+
|
|
342
|
+
Those numbers say when, not what: a later date means a later release and carries no
|
|
343
|
+
compatibility promise. Any release may change or remove an export.
|
|
344
|
+
|
|
345
|
+
Depend on one exact date, and move it when you are ready to take the change:
|
|
346
|
+
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"dependencies": {
|
|
350
|
+
"@sdxc/cli": "2026.9.4"
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
A caret or tilde range reads the date as major, minor and patch, so it accepts every
|
|
356
|
+
later release in the same year. An exact version keeps the upgrade yours to schedule.
|
|
357
|
+
|
|
358
|
+
## License
|
|
359
|
+
|
|
360
|
+
MIT
|
|
361
|
+
|
|
362
|
+
## Author
|
|
363
|
+
|
|
364
|
+
[Sergio Xalambrí](https://sergiodxa.com)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positional argument declarations: one value, every remaining value with `argument.variadic`,
|
|
3
|
+
* each checked by a Standard Schema, or every token after the command's name as typed with
|
|
4
|
+
* `argument.rest`. A command's `arguments` fill in key order, so the declaration is the usage line.
|
|
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 { Carried } from "./flag.js";
|
|
11
|
+
import type { PromptDeclaration } from "./prompt.js";
|
|
12
|
+
import type { SchemaOutput } from "./schema.js";
|
|
13
|
+
/** Brands an argument declaration so a command can tell it from a flag. */
|
|
14
|
+
declare const ARGUMENT: unique symbol;
|
|
15
|
+
/** Type-only slot carrying the value a declaration hands its handler. */
|
|
16
|
+
declare const VALUE: unique symbol;
|
|
17
|
+
/** Everything one argument declares, as the parser and the help read it. */
|
|
18
|
+
export interface ArgumentSpec {
|
|
19
|
+
/** Checks each value, every one of a variadic argument; it receives the text as typed. */
|
|
20
|
+
schema: StandardSchemaV1;
|
|
21
|
+
variadic: boolean;
|
|
22
|
+
/** Takes every token after the command's name unparsed, flags and `--help` included. */
|
|
23
|
+
rest: boolean;
|
|
24
|
+
/** Fewest values a variadic argument takes. */
|
|
25
|
+
min: number;
|
|
26
|
+
/** Most values a variadic argument takes. */
|
|
27
|
+
max: number;
|
|
28
|
+
/** A missing value reads `undefined` instead of failing. */
|
|
29
|
+
optional: boolean;
|
|
30
|
+
hasDefault: boolean;
|
|
31
|
+
default: unknown;
|
|
32
|
+
description: string | undefined;
|
|
33
|
+
/** What the help shows in `<…>`. @default the argument's key */
|
|
34
|
+
valueName: string | undefined;
|
|
35
|
+
prompt: PromptDeclaration | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** A declared argument, typed with the value its handler reads. */
|
|
38
|
+
export interface Argument<Value = unknown> {
|
|
39
|
+
readonly [ARGUMENT]: ArgumentSpec;
|
|
40
|
+
readonly [VALUE]?: Carried<Value>;
|
|
41
|
+
}
|
|
42
|
+
/** The value an argument declaration hands its handler. */
|
|
43
|
+
export type ArgumentValue<A> = A extends Argument<infer _> ? A extends {
|
|
44
|
+
readonly [VALUE]?: Carried<infer V> | undefined;
|
|
45
|
+
} ? V : never : never;
|
|
46
|
+
/** Options every argument takes. */
|
|
47
|
+
export interface ArgumentOptions {
|
|
48
|
+
description?: string;
|
|
49
|
+
/** What the help shows in `<…>`. @default the argument's key */
|
|
50
|
+
valueName?: string;
|
|
51
|
+
/** Asked when the argument is missing and stdin and stderr are terminals. */
|
|
52
|
+
prompt?: PromptDeclaration;
|
|
53
|
+
}
|
|
54
|
+
/** Options a single argument takes. */
|
|
55
|
+
export interface SingleArgumentOptions extends ArgumentOptions {
|
|
56
|
+
/** A missing argument reads `undefined` instead of being a usage error. */
|
|
57
|
+
optional?: boolean;
|
|
58
|
+
/** The value when the argument is missing, taken as the handler reads it. */
|
|
59
|
+
default?: unknown;
|
|
60
|
+
}
|
|
61
|
+
/** Options a variadic argument takes. */
|
|
62
|
+
export interface VariadicArgumentOptions extends ArgumentOptions {
|
|
63
|
+
/** @default 0 */
|
|
64
|
+
min?: number;
|
|
65
|
+
max?: number;
|
|
66
|
+
/** The values when none are passed, taken as the handler reads them. */
|
|
67
|
+
default?: readonly unknown[];
|
|
68
|
+
}
|
|
69
|
+
/** Options a rest argument takes; its values arrive as typed, so it has no schema or prompt. */
|
|
70
|
+
export interface RestArgumentOptions {
|
|
71
|
+
description?: string;
|
|
72
|
+
/** What the help shows in `[…]`. @default the argument's key */
|
|
73
|
+
valueName?: string;
|
|
74
|
+
}
|
|
75
|
+
/** The handler's type for a single argument declared with `options`. */
|
|
76
|
+
type SingleValue<Output, Options> = Options extends {
|
|
77
|
+
default: unknown;
|
|
78
|
+
} ? Output : Options extends {
|
|
79
|
+
optional: true;
|
|
80
|
+
} ? Output | undefined : Output;
|
|
81
|
+
/** The handler's type for a variadic argument: non-empty when `min` is a positive literal. */
|
|
82
|
+
type VariadicValue<Output, Options> = Options extends {
|
|
83
|
+
min: infer Min extends number;
|
|
84
|
+
} ? Min extends 0 ? Output[] : number extends Min ? Output[] : [Output, ...Output[]] : Output[];
|
|
85
|
+
/** Whether `value` is an argument declaration. */
|
|
86
|
+
export declare function isArgument(value: unknown): value is Argument;
|
|
87
|
+
/** The spec behind an argument declaration. */
|
|
88
|
+
export declare function argumentSpec(argument: Argument): ArgumentSpec;
|
|
89
|
+
/**
|
|
90
|
+
* One positional value, required unless it is `optional` or has a `default`.
|
|
91
|
+
*
|
|
92
|
+
* @param schema - Checks the text as typed; its output is what the handler reads
|
|
93
|
+
* @example arguments: { directory: argument(s.string(), { prompt: prompt.text({ message: "Where?" }) }) }
|
|
94
|
+
*/
|
|
95
|
+
declare function single<Schema extends StandardSchemaV1, const Options extends SingleArgumentOptions = Record<never, never>>(schema: Schema, options?: Options): Argument<SingleValue<SchemaOutput<Schema>, Options>>;
|
|
96
|
+
/**
|
|
97
|
+
* Every remaining positional value, each checked by `schema`; it must be the last argument.
|
|
98
|
+
*
|
|
99
|
+
* @param schema - Checks each value as typed
|
|
100
|
+
* @example arguments: { components: argument.variadic(s.string(), { min: 1 }) }
|
|
101
|
+
*/
|
|
102
|
+
declare function variadic<Schema extends StandardSchemaV1, const Options extends VariadicArgumentOptions = Record<never, never>>(schema: Schema, options?: Options): Argument<VariadicValue<SchemaOutput<Schema>, Options>>;
|
|
103
|
+
/**
|
|
104
|
+
* Every token after the command's name exactly as typed, flags and `--help` included, for a
|
|
105
|
+
* command that runs another program with them. It is the command's only argument, and the
|
|
106
|
+
* command declares no flags.
|
|
107
|
+
*
|
|
108
|
+
* @example arguments: { args: argument.rest() } // prog run users find --limit 5 → ["users", "find", "--limit", "5"]
|
|
109
|
+
*/
|
|
110
|
+
declare function rest(options?: RestArgumentOptions): Argument<string[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Argument declarations: `argument(schema, options)` for one value,
|
|
113
|
+
* `argument.variadic(schema, options)` for every remaining one, and `argument.rest()` for the
|
|
114
|
+
* tokens another program receives.
|
|
115
|
+
*/
|
|
116
|
+
export declare const argument: typeof single & {
|
|
117
|
+
variadic: typeof variadic;
|
|
118
|
+
rest: typeof rest;
|
|
119
|
+
};
|
|
120
|
+
export {};
|
package/dist/argument.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positional argument declarations: one value, every remaining value with `argument.variadic`,
|
|
3
|
+
* each checked by a Standard Schema, or every token after the command's name as typed with
|
|
4
|
+
* `argument.rest`. A command's `arguments` fill in key order, so the declaration is the usage line.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/** Brands an argument declaration so a command can tell it from a flag. */
|
|
10
|
+
const ARGUMENT = Symbol.for("@sdxc/cli.argument");
|
|
11
|
+
/** Accepts every token as it is, since a rest argument's values belong to another program. */
|
|
12
|
+
const AS_TYPED = {
|
|
13
|
+
"~standard": {
|
|
14
|
+
version: 1,
|
|
15
|
+
vendor: "@sdxc/cli",
|
|
16
|
+
validate: (value) => ({ value: value }),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
/** Whether `value` is an argument declaration. */
|
|
20
|
+
export function isArgument(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && ARGUMENT in value;
|
|
22
|
+
}
|
|
23
|
+
/** The spec behind an argument declaration. */
|
|
24
|
+
export function argumentSpec(argument) {
|
|
25
|
+
return argument[ARGUMENT];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One positional value, required unless it is `optional` or has a `default`.
|
|
29
|
+
*
|
|
30
|
+
* @param schema - Checks the text as typed; its output is what the handler reads
|
|
31
|
+
* @example arguments: { directory: argument(s.string(), { prompt: prompt.text({ message: "Where?" }) }) }
|
|
32
|
+
*/
|
|
33
|
+
function single(schema, options) {
|
|
34
|
+
let given = options ?? {};
|
|
35
|
+
return {
|
|
36
|
+
[ARGUMENT]: {
|
|
37
|
+
schema,
|
|
38
|
+
variadic: false,
|
|
39
|
+
rest: false,
|
|
40
|
+
min: given.optional === true || "default" in given ? 0 : 1,
|
|
41
|
+
max: 1,
|
|
42
|
+
optional: given.optional === true,
|
|
43
|
+
hasDefault: "default" in given,
|
|
44
|
+
default: given.default,
|
|
45
|
+
description: given.description,
|
|
46
|
+
valueName: given.valueName,
|
|
47
|
+
prompt: given.prompt,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Every remaining positional value, each checked by `schema`; it must be the last argument.
|
|
53
|
+
*
|
|
54
|
+
* @param schema - Checks each value as typed
|
|
55
|
+
* @example arguments: { components: argument.variadic(s.string(), { min: 1 }) }
|
|
56
|
+
*/
|
|
57
|
+
function variadic(schema, options) {
|
|
58
|
+
let given = options ?? {};
|
|
59
|
+
return {
|
|
60
|
+
[ARGUMENT]: {
|
|
61
|
+
schema,
|
|
62
|
+
variadic: true,
|
|
63
|
+
rest: false,
|
|
64
|
+
min: given.min ?? 0,
|
|
65
|
+
max: given.max ?? Infinity,
|
|
66
|
+
optional: false,
|
|
67
|
+
hasDefault: "default" in given,
|
|
68
|
+
default: given.default,
|
|
69
|
+
description: given.description,
|
|
70
|
+
valueName: given.valueName,
|
|
71
|
+
prompt: given.prompt,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Every token after the command's name exactly as typed, flags and `--help` included, for a
|
|
77
|
+
* command that runs another program with them. It is the command's only argument, and the
|
|
78
|
+
* command declares no flags.
|
|
79
|
+
*
|
|
80
|
+
* @example arguments: { args: argument.rest() } // prog run users find --limit 5 → ["users", "find", "--limit", "5"]
|
|
81
|
+
*/
|
|
82
|
+
function rest(options = {}) {
|
|
83
|
+
return {
|
|
84
|
+
[ARGUMENT]: {
|
|
85
|
+
schema: AS_TYPED,
|
|
86
|
+
variadic: true,
|
|
87
|
+
rest: true,
|
|
88
|
+
min: 0,
|
|
89
|
+
max: Infinity,
|
|
90
|
+
optional: false,
|
|
91
|
+
hasDefault: false,
|
|
92
|
+
default: undefined,
|
|
93
|
+
description: options.description,
|
|
94
|
+
valueName: options.valueName,
|
|
95
|
+
prompt: undefined,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Argument declarations: `argument(schema, options)` for one value,
|
|
101
|
+
* `argument.variadic(schema, options)` for every remaining one, and `argument.rest()` for the
|
|
102
|
+
* tokens another program receives.
|
|
103
|
+
*/
|
|
104
|
+
export const argument = Object.assign(single, { variadic, rest });
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createCLI()`: a command contract with handlers mapped onto it, run against an argument
|
|
3
|
+
* vector. A run parses, prints help or a usage error, asks for what is missing, runs the
|
|
4
|
+
* middleware chain and the handler, and answers the exit code its outcome maps to.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ArgumentsOf, FlagsOf, Group, Node } from "./command.js";
|
|
10
|
+
import type { CommandAction, CommandHandler } from "./handler.js";
|
|
11
|
+
import type { Lazy } from "./lazy.js";
|
|
12
|
+
import type { AnyCommandMiddleware, ChainProperties } from "./middleware.js";
|
|
13
|
+
import type { RunIO, Script } from "./session.js";
|
|
14
|
+
import { CommandContext } from "./context.js";
|
|
15
|
+
/** What a CLI is built from. */
|
|
16
|
+
export interface CLIOptions<Commands extends Group, Chain extends readonly AnyCommandMiddleware[]> {
|
|
17
|
+
/** The program name, as the usage line prints it. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Printed by `--version`; without one the flag does not exist. */
|
|
20
|
+
version?: string;
|
|
21
|
+
/** Shown under the root usage line when the root group has no description. */
|
|
22
|
+
description?: string;
|
|
23
|
+
commands: Commands;
|
|
24
|
+
/** Runs before every command's own middleware, in order. */
|
|
25
|
+
middleware?: Chain;
|
|
26
|
+
}
|
|
27
|
+
/** What `map()` accepts for one command or group. */
|
|
28
|
+
export type Binding<Target, Chain extends readonly AnyCommandMiddleware[]> = CommandHandler<CommandContext<ArgumentsOf<Target>, FlagsOf<Target>> & ChainProperties<Chain>> | CommandAction<Target, any> | Lazy<CommandAction<Target, any>>;
|
|
29
|
+
/** A command line: its contract, the handlers mapped onto it, and a way to run it. */
|
|
30
|
+
export interface CLI<Commands extends Group = Group, Chain extends readonly AnyCommandMiddleware[] = readonly AnyCommandMiddleware[]> {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly version: string | undefined;
|
|
33
|
+
readonly commands: Commands;
|
|
34
|
+
readonly middleware: Chain;
|
|
35
|
+
/**
|
|
36
|
+
* Binds a handler to a command. A group can be mapped too; it runs when the command line
|
|
37
|
+
* names the group and no member, which otherwise prints the group's help.
|
|
38
|
+
*
|
|
39
|
+
* @throws Error When the target is not in this CLI's contract, or is mapped twice.
|
|
40
|
+
*/
|
|
41
|
+
map<Target extends Node>(target: Target, binding: Binding<Target, Chain>): void;
|
|
42
|
+
/**
|
|
43
|
+
* Runs the command `argv` names and answers its exit code: 0 success, 1 failure, 2 usage
|
|
44
|
+
* error, 130 cancelled, or the code an `exit()` failure carries.
|
|
45
|
+
*
|
|
46
|
+
* @param argv - The arguments after the program, as `process.argv.slice(2)` holds them
|
|
47
|
+
* @param io - Replaces any part of the process the run uses
|
|
48
|
+
* @example process.exitCode = await cli.run(process.argv.slice(2));
|
|
49
|
+
*/
|
|
50
|
+
run(argv: readonly string[], io?: RunIO): Promise<number>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Builds a CLI over a command contract.
|
|
54
|
+
*
|
|
55
|
+
* @throws Error When the contract repeats a command, gives a flag a reserved or duplicate name,
|
|
56
|
+
* or puts a variadic argument before another one.
|
|
57
|
+
* @example
|
|
58
|
+
* let cli = createCLI({ name: "sdxc", version, commands: sdxc, middleware: [project()] });
|
|
59
|
+
* cli.map(sdxc.init, lazy(() => import("./commands/init.js")));
|
|
60
|
+
* process.exitCode = await cli.run(process.argv.slice(2));
|
|
61
|
+
*/
|
|
62
|
+
export declare function createCLI<Commands extends Group, const Chain extends readonly AnyCommandMiddleware[] = []>(options: CLIOptions<Commands, Chain>): CLI<Commands, Chain>;
|
|
63
|
+
/**
|
|
64
|
+
* Runs `argv` on `cli` with a test's script. Used by `@sdxc/cli/test`.
|
|
65
|
+
*
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
export declare function runScripted<Commands extends Group, Chain extends readonly AnyCommandMiddleware[]>(cli: CLI<Commands, Chain>, argv: readonly string[], io: RunIO, script: Script): Promise<number>;
|