@sdxc/cli 2026.10.16 → 2026.10.23

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