@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.
Files changed (55) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +364 -0
  3. package/dist/argument.d.ts +120 -0
  4. package/dist/argument.js +104 -0
  5. package/dist/cli.d.ts +68 -0
  6. package/dist/cli.js +186 -0
  7. package/dist/command.d.ts +139 -0
  8. package/dist/command.js +79 -0
  9. package/dist/context.d.ts +138 -0
  10. package/dist/context.js +175 -0
  11. package/dist/errors.d.ts +67 -0
  12. package/dist/errors.js +82 -0
  13. package/dist/exec.d.ts +83 -0
  14. package/dist/exec.js +136 -0
  15. package/dist/flag.d.ts +175 -0
  16. package/dist/flag.js +91 -0
  17. package/dist/handler.d.ts +73 -0
  18. package/dist/handler.js +30 -0
  19. package/dist/help.d.ts +18 -0
  20. package/dist/help.js +141 -0
  21. package/dist/index.d.ts +35 -0
  22. package/dist/index.js +20 -0
  23. package/dist/interactions.d.ts +27 -0
  24. package/dist/interactions.js +373 -0
  25. package/dist/keys.d.ts +49 -0
  26. package/dist/keys.js +146 -0
  27. package/dist/lazy.d.ts +29 -0
  28. package/dist/lazy.js +23 -0
  29. package/dist/middleware.d.ts +78 -0
  30. package/dist/middleware.js +9 -0
  31. package/dist/parse.d.ts +38 -0
  32. package/dist/parse.js +218 -0
  33. package/dist/prompt.d.ts +153 -0
  34. package/dist/prompt.js +53 -0
  35. package/dist/resolve.d.ts +27 -0
  36. package/dist/resolve.js +294 -0
  37. package/dist/schema.d.ts +27 -0
  38. package/dist/schema.js +34 -0
  39. package/dist/screen.d.ts +42 -0
  40. package/dist/screen.js +114 -0
  41. package/dist/session.d.ts +84 -0
  42. package/dist/session.js +219 -0
  43. package/dist/style.d.ts +50 -0
  44. package/dist/style.js +72 -0
  45. package/dist/suggest.d.ts +15 -0
  46. package/dist/suggest.js +50 -0
  47. package/dist/task.d.ts +47 -0
  48. package/dist/task.js +213 -0
  49. package/dist/terminal.d.ts +21 -0
  50. package/dist/terminal.js +101 -0
  51. package/dist/test.d.ts +89 -0
  52. package/dist/test.js +140 -0
  53. package/dist/tree.d.ts +58 -0
  54. package/dist/tree.js +97 -0
  55. 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 {};
@@ -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>;