@loomcli/core 0.1.0

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.
@@ -0,0 +1,452 @@
1
+ import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
2
+ import { bindGlobals, buildGlobals } from './globals.js';
3
+ import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
4
+ import { captureConfig, validateValues } from './validation.js';
5
+ /** Authored values register here, so the public type publishes no state to reach or replace. */
6
+ const nodes = new WeakMap();
7
+ /** Reads the declarations behind an attached value; anything else is a declaration error. */
8
+ function nodeOf(parent, child) {
9
+ const node = nodes.get(child);
10
+ if (!node) {
11
+ throw new DeclarationError(`${commandSentence(parent)} attaches a value that is not a Command. Attach the value returned by new Command(name).`);
12
+ }
13
+ return node;
14
+ }
15
+ /** One name rule for every declared name in the graph, so a child and an argument read alike. */
16
+ function isDeclaredName(name) {
17
+ return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
18
+ }
19
+ function checkChildName(parent, name) {
20
+ if (!isDeclaredName(name)) {
21
+ throw new DeclarationError(`${commandSentence(parent)} attaches a child named "${String(name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
22
+ }
23
+ }
24
+ /** An alias is a bare token the way a child name is, so it answers to the same name rule. */
25
+ function checkAliasName(command, alias) {
26
+ if (!isDeclaredName(alias)) {
27
+ throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
28
+ }
29
+ }
30
+ /** The state every declaration starts from. The unnamed root and each named Command share it. */
31
+ export function freshState(name, globals) {
32
+ return {
33
+ actions: [],
34
+ aliases: [],
35
+ bind: () => ({ args: {}, options: {} }),
36
+ children: [],
37
+ globals,
38
+ inputs: [],
39
+ late: [],
40
+ name,
41
+ };
42
+ }
43
+ /** A declaration after the action is an order fault; build reports the first one recorded. */
44
+ function recordLate(state, declarations) {
45
+ return state.actions.length > 0 ? [...state.late, ...declarations] : state.late;
46
+ }
47
+ /** The declared value joins `args` under its literal name, typed by its own config. */
48
+ export function declareArgument(state, input) {
49
+ const previous = state.bind;
50
+ return {
51
+ ...state,
52
+ bind: (values) => {
53
+ const bound = previous(values);
54
+ return { ...bound, args: { ...bound.args, ...values.argument(input) } };
55
+ },
56
+ inputs: [...state.inputs, input],
57
+ late: recordLate(state, [{ input, kind: 'input' }]),
58
+ };
59
+ }
60
+ /** The declared value joins `options` under its literal name, typed by its own config. */
61
+ export function declareOption(state, input) {
62
+ const previous = state.bind;
63
+ return {
64
+ ...state,
65
+ bind: (values) => {
66
+ const bound = previous(values);
67
+ return { ...bound, options: { ...bound.options, ...values.option(input) } };
68
+ },
69
+ inputs: [...state.inputs, input],
70
+ late: recordLate(state, [{ input, kind: 'input' }]),
71
+ };
72
+ }
73
+ /** One call's names stay one group, so the empty call the types reject still reports as one. */
74
+ export function declareAlias(state, names) {
75
+ return {
76
+ ...state,
77
+ aliases: [...state.aliases, names],
78
+ late: recordLate(state, names.map((alias) => ({ alias, kind: 'alias' }))),
79
+ };
80
+ }
81
+ export function declareAction(state, handler) {
82
+ return { ...state, actions: [...state.actions, handler] };
83
+ }
84
+ /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
85
+ export function attachChild(state, child) {
86
+ return {
87
+ ...state,
88
+ children: [...state.children, child],
89
+ late: recordLate(state, [{ child, kind: 'child' }]),
90
+ };
91
+ }
92
+ /** The types remove a late call for TypeScript authors; JavaScript authors read it here. */
93
+ function checkDeclarationOrder(state) {
94
+ const { name } = state;
95
+ const late = state.late[0];
96
+ if (!late) {
97
+ return;
98
+ }
99
+ if (late.kind === 'input') {
100
+ throw new DeclarationError(`${commandSentence(name)} declares ${late.input.kind} "${late.input.name}" after its action. Declare arguments and options before action().`);
101
+ }
102
+ if (late.kind === 'alias') {
103
+ throw new DeclarationError(`${commandSentence(name)} declares alias "${late.alias}" after its action. Declare aliases before action().`);
104
+ }
105
+ // Child identity and names are settled before this call, so the node and its name are valid.
106
+ throw new DeclarationError(`${commandSentence(name)} attaches child "${String(nodeOf(name, late.child).name)}" after its action. Attach children before action().`);
107
+ }
108
+ /** Child names are checked before any child builds, so parent diagnostics come first. */
109
+ function collectChildren(state) {
110
+ const attached = [];
111
+ const seen = new Set();
112
+ for (const child of state.children) {
113
+ const node = nodeOf(state.name, child);
114
+ const name = node.name;
115
+ checkChildName(state.name, name);
116
+ if (seen.has(name)) {
117
+ throw new DeclarationError(`${commandSentence(state.name)} attaches two children named "${name}". Rename or remove one.`);
118
+ }
119
+ seen.add(name);
120
+ attached.push([name, node]);
121
+ }
122
+ return attached;
123
+ }
124
+ /**
125
+ * A Command's own alias rules, and the flat list in declaration order that routing and inspection
126
+ * read. Each call keeps its own group, so a call that names none reports as the call it is.
127
+ */
128
+ function collectAliases(state) {
129
+ const { name } = state;
130
+ const aliases = [];
131
+ const seen = new Set();
132
+ for (const declaration of state.aliases) {
133
+ if (declaration.length === 0) {
134
+ throw new DeclarationError(`${commandSentence(name)} declares an alias with no names. Supply at least one name.`);
135
+ }
136
+ for (const alias of declaration) {
137
+ checkAliasName(name, alias);
138
+ if (alias === name) {
139
+ throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}", which is its own name. Remove the alias.`);
140
+ }
141
+ if (seen.has(alias)) {
142
+ throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}" twice. Remove the repeated alias.`);
143
+ }
144
+ seen.add(alias);
145
+ aliases.push(alias);
146
+ }
147
+ }
148
+ return aliases;
149
+ }
150
+ /**
151
+ * One parent's namespace, which every canonical name and alias under it shares. Each child settled
152
+ * its own alias rules while it built, so what is left is the collision with a sibling. The names are
153
+ * read before the walk, so the rule reads the same whichever sibling the author declared first.
154
+ */
155
+ function aliasNamespace(parent, names) {
156
+ const owners = new Map();
157
+ return function claim(child, alias) {
158
+ if (names.has(alias)) {
159
+ throw new DeclarationError(`${commandSentence(parent)} attaches child "${child}" with alias "${alias}", which is also the name of child "${alias}". Rename or remove one.`);
160
+ }
161
+ const owner = owners.get(alias);
162
+ if (owner !== undefined) {
163
+ throw new DeclarationError(`${commandSentence(parent)} attaches child "${child}" with alias "${alias}", which is also an alias of child "${owner}". Rename or remove one.`);
164
+ }
165
+ owners.set(alias, child);
166
+ };
167
+ }
168
+ /**
169
+ * Claims a child for its parent when the depth-first walk reaches it, then builds its subtree. A
170
+ * claimed node always means a second parent, because the duplicate-name rule rejects one parent
171
+ * attaching a value twice. Parents may share a name, so the claim is by node identity and the name
172
+ * serves the diagnostic alone.
173
+ */
174
+ function buildChild(parent, [name, node], context) {
175
+ const owner = context.owners.get(node);
176
+ if (owner !== undefined) {
177
+ throw new DeclarationError(`${commandSentence(parent)} attaches child "${name}", which ${commandSubject(owner)} also attaches. Attach a Command value at one point; create a new Command for each placement.`);
178
+ }
179
+ context.owners.set(node, parent);
180
+ return node.build(context);
181
+ }
182
+ /** A variadic or optional slot ends the positional list, so nothing may follow either one. */
183
+ function checkSlotOrder(slot, next, subject) {
184
+ if (slot.variadic) {
185
+ throw new DeclarationError(`Argument "${slot.input.name}" is variadic and precedes argument "${next.input.name}" on ${subject}. Declare the variadic argument last.`);
186
+ }
187
+ if (!slot.required) {
188
+ throw new DeclarationError(next.required
189
+ ? `Argument "${slot.input.name}" is optional and precedes required argument "${next.input.name}" on ${subject}. Declare optional arguments after required ones.`
190
+ : `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}. Declare an optional argument last.`);
191
+ }
192
+ }
193
+ function collectArguments(state, subject) {
194
+ const slots = [];
195
+ const seen = new Set();
196
+ for (const input of state.inputs.filter((entry) => entry.kind === 'argument')) {
197
+ if (!isDeclaredName(input.name)) {
198
+ throw new DeclarationError(`${commandSentence(state.name)} declares an argument named "${String(input.name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
199
+ }
200
+ if (seen.has(input.name)) {
201
+ throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
202
+ }
203
+ seen.add(input.name);
204
+ slots.push({
205
+ input,
206
+ required: input.config.required === true,
207
+ variadic: input.config.variadic === true,
208
+ });
209
+ }
210
+ for (let index = 0; index + 1 < slots.length; index += 1) {
211
+ const slot = slots[index];
212
+ const next = slots[index + 1];
213
+ if (slot && next) {
214
+ checkSlotOrder(slot, next, subject);
215
+ }
216
+ }
217
+ return slots;
218
+ }
219
+ function compileLocalOptions(state, globals, subject) {
220
+ const declarations = state.inputs.filter((input) => input.kind === 'option');
221
+ for (const declaration of declarations) {
222
+ if (globals.names.has(declaration.name)) {
223
+ throw new DeclarationError(`Option "${declaration.name}" is declared as a global option and as a local option on ${subject}. Rename the local option.`);
224
+ }
225
+ }
226
+ const options = compileOptions(declarations, subject);
227
+ for (const [spelling, option] of options) {
228
+ const global = globals.options.get(spelling);
229
+ if (global) {
230
+ throw new DeclarationError(`Option spelling "${spelling}" is used by the global option "${global.name}" and the local option "${option.name}" on ${subject}. Change one declaration.`);
231
+ }
232
+ }
233
+ return options;
234
+ }
235
+ /**
236
+ * A Command without an action is a group, and routing sends an invocation on to one of its
237
+ * children. A group with no children receives an invocation no handler can answer, and a local
238
+ * option on a group reaches no handler either, because locals never inherit.
239
+ */
240
+ function checkGroup(state, children) {
241
+ const { name } = state;
242
+ if (children.length === 0) {
243
+ throw new DeclarationError(`${commandSentence(name)} has no action. Register an action.`);
244
+ }
245
+ const option = state.inputs.find((input) => input.kind === 'option');
246
+ if (option) {
247
+ throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
248
+ }
249
+ }
250
+ /** Binds one Command's declarations to its action, so an action reads only validated values. */
251
+ function bindDispatch(state, action) {
252
+ return ({ host, out, passthrough, values }) => {
253
+ const bound = state.bind(values);
254
+ return action({
255
+ args: bound.args,
256
+ host,
257
+ options: { ...bindGlobals(state.globals, values), ...bound.options },
258
+ out,
259
+ passthrough,
260
+ });
261
+ };
262
+ }
263
+ /** Validates one declaration against the shared globals table and compiles it for dispatch. */
264
+ export function buildCommand(state, context) {
265
+ const { actions, name } = state;
266
+ const { globals } = context;
267
+ const subject = commandSubject(name);
268
+ if (state.globals !== globals.source) {
269
+ throw new DeclarationError(`${commandSentence(name)} holds a different GlobalOptions value than its Application. Share one GlobalOptions value across the declarations.`);
270
+ }
271
+ const attached = collectChildren(state);
272
+ checkDeclarationOrder(state);
273
+ const aliases = collectAliases(state);
274
+ const slots = collectArguments(state, subject);
275
+ const first = slots[0];
276
+ const child = attached[0];
277
+ if (first && child) {
278
+ throw new DeclarationError(`${commandSentence(name)} declares argument "${first.input.name}" and attaches child "${child[0]}". Move the argument into a child Command or remove the children.`);
279
+ }
280
+ if (actions.length > 1) {
281
+ throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
282
+ }
283
+ const action = actions[0];
284
+ if (!action) {
285
+ checkGroup(state, attached);
286
+ }
287
+ const options = compileLocalOptions(state, globals, subject);
288
+ const children = new Map();
289
+ const routes = new Map();
290
+ const claim = aliasNamespace(name, new Set(attached.map((entry) => entry[0])));
291
+ for (const entry of attached) {
292
+ // The subtree builds before its aliases are claimed, so the tree rule keeps its precedence.
293
+ const routed = { command: buildChild(name, entry, context), name: entry[0] };
294
+ children.set(routed.name, routed.command);
295
+ routes.set(routed.name, routed);
296
+ for (const alias of routed.command.aliases) {
297
+ claim(routed.name, alias);
298
+ routes.set(alias, routed);
299
+ }
300
+ }
301
+ return {
302
+ aliases,
303
+ arguments: slots,
304
+ children,
305
+ dispatch: action ? bindDispatch(state, action) : undefined,
306
+ inputs: state.inputs,
307
+ name,
308
+ options,
309
+ routes,
310
+ };
311
+ }
312
+ /** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
313
+ export function buildGraph(root) {
314
+ const context = { globals: buildGlobals(root.globals), owners: new Map() };
315
+ return { globals: context.globals, root: buildCommand(root, context) };
316
+ }
317
+ export class CommandBuilder {
318
+ #state;
319
+ constructor(state) {
320
+ this.#state = state;
321
+ nodes.set(this, this);
322
+ }
323
+ get name() {
324
+ return this.#state.name;
325
+ }
326
+ argument(name, config) {
327
+ const input = {
328
+ config: captureConfig(config),
329
+ kind: 'argument',
330
+ name,
331
+ };
332
+ return new CommandBuilder(declareArgument(this.#state, input));
333
+ }
334
+ option(name, config) {
335
+ const input = {
336
+ config: captureConfig(config),
337
+ kind: 'option',
338
+ name,
339
+ };
340
+ return new CommandBuilder(declareOption(this.#state, input));
341
+ }
342
+ /**
343
+ * Hidden aliases are other bare tokens that route to this Command. They invalidate no call, and
344
+ * the tuple rest parameter rejects a call that names none.
345
+ */
346
+ alias(...names) {
347
+ return new CommandBuilder(declareAlias(this.#state, names));
348
+ }
349
+ /** A child arrives in any type state, because its own action is the call that finished it. */
350
+ command(child) {
351
+ return new CommandBuilder(attachChild(this.#state, child));
352
+ }
353
+ /** The action is the last declaration call, so the value it returns publishes `AfterAction`. */
354
+ action(handler) {
355
+ return new CommandBuilder(declareAction(this.#state, handler));
356
+ }
357
+ build(context) {
358
+ return buildCommand(this.#state, context);
359
+ }
360
+ }
361
+ /**
362
+ * The runtime class behind the public constructor. It is generic so that an instance's `Globals`
363
+ * is the type of the value it holds, with `{}` standing in when there is none, which is what each
364
+ * signature of the constructor interface publishes.
365
+ */
366
+ class CommandDeclaration extends CommandBuilder {
367
+ constructor(name, globals) {
368
+ super(freshState(name, globals));
369
+ }
370
+ }
371
+ /** The public constructor requires a name and narrows the globals type to the supplied value. */
372
+ export const Command = CommandDeclaration;
373
+ /** Every declaration in the graph, so defaults are validated before any token is read. */
374
+ export function collectInputs(command) {
375
+ return [
376
+ ...command.inputs,
377
+ ...[...command.children.values()].flatMap((child) => collectInputs(child)),
378
+ ];
379
+ }
380
+ /** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
381
+ export function route(root, tokens) {
382
+ let command = root;
383
+ const path = [];
384
+ let index = 0;
385
+ while (command.children.size > 0) {
386
+ const token = tokens[index];
387
+ if (token === undefined || token === '--' || token.startsWith('-')) {
388
+ break;
389
+ }
390
+ const child = command.routes.get(token);
391
+ if (!child) {
392
+ throw new UnknownCommandError(token, [...command.children.keys()]);
393
+ }
394
+ // An alias routes like the canonical name, and the path it walks reports that name alone.
395
+ command = child.command;
396
+ path.push(child.name);
397
+ index += 1;
398
+ }
399
+ return { command, path, tokens: tokens.slice(index) };
400
+ }
401
+ /**
402
+ * The tokens each positional slot received. An omitted required argument binds nothing here and
403
+ * reports as a missing input in the validation phase, so omission has one class whether the input
404
+ * is an argument or an option. Extra tokens are a token fault, so this phase still reports them.
405
+ */
406
+ function bindArguments(command, path, positionals) {
407
+ const values = new Map();
408
+ let index = 0;
409
+ for (const slot of command.arguments) {
410
+ if (slot.variadic) {
411
+ const rest = positionals.slice(index);
412
+ // An empty tail binds nothing, so validation reads it as `[]` or reports the omission.
413
+ if (rest.length > 0) {
414
+ values.set(slot.input, rest);
415
+ }
416
+ index = positionals.length;
417
+ }
418
+ else {
419
+ const value = positionals[index];
420
+ if (value !== undefined) {
421
+ values.set(slot.input, value);
422
+ index += 1;
423
+ }
424
+ }
425
+ }
426
+ if (index < positionals.length) {
427
+ throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(index));
428
+ }
429
+ return values;
430
+ }
431
+ /** Consumes globals, routes to a Command, then validates globals and locals in one pass. */
432
+ export async function selectCommand(graph, invocation) {
433
+ const { host } = invocation;
434
+ const scan = extractGlobals(graph.globals.options, [...host.argv]);
435
+ const { command, path, tokens: rest } = route(graph.root, scan.rest);
436
+ const { dispatch } = command;
437
+ // A group answers no invocation of its own, so it fails with the routing errors above it.
438
+ if (!dispatch) {
439
+ throw new NonCallableCommandError(path, [...command.children.keys()]);
440
+ }
441
+ const parsed = parseInputs(command.options, rest);
442
+ const args = bindArguments(command, path, parsed.positionals);
443
+ const values = await validateValues({
444
+ command: path,
445
+ defaults: invocation.defaults,
446
+ host,
447
+ inputs: { globals: graph.globals.inputs, locals: command.inputs },
448
+ passthrough: parsed.passthrough,
449
+ supplied: { args, options: mergeValues(scan.values, parsed.options) },
450
+ });
451
+ return { dispatch, passthrough: parsed.passthrough, values };
452
+ }
@@ -0,0 +1,9 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { ValidationContext } from './types.js';
3
+ /** The `libraryOptions` key core writes its context under. It names the package that owns it. */
4
+ declare const validationContextKey = "@loomcli/core";
5
+ /** The options of one schema call, with its context registered for the accessor to recognize. */
6
+ declare function schemaOptions(context: ValidationContext): StandardSchemaV1.Options;
7
+ /** Reads the context core attached to a schema call, or undefined when another caller ran it. */
8
+ declare function validationContext(options: StandardSchemaV1.Options | undefined): ValidationContext | undefined;
9
+ export { schemaOptions, validationContext, validationContextKey };
@@ -0,0 +1,18 @@
1
+ /** The `libraryOptions` key core writes its context under. It names the package that owns it. */
2
+ const validationContextKey = '@loomcli/core';
3
+ /**
4
+ * Every context core has emitted. The accessor answers from this registry alone, so a value some
5
+ * other caller wrote under the same key reads as no context, and the reading needs no assertion.
6
+ */
7
+ const emitted = new WeakMap();
8
+ /** The options of one schema call, with its context registered for the accessor to recognize. */
9
+ function schemaOptions(context) {
10
+ emitted.set(context, context);
11
+ return { libraryOptions: { [validationContextKey]: context } };
12
+ }
13
+ /** Reads the context core attached to a schema call, or undefined when another caller ran it. */
14
+ function validationContext(options) {
15
+ const carried = options?.libraryOptions?.[validationContextKey];
16
+ return carried !== null && typeof carried === 'object' ? emitted.get(carried) : undefined;
17
+ }
18
+ export { schemaOptions, validationContext, validationContextKey };
@@ -0,0 +1,158 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { InputIdentity, Renderer } from './types.js';
3
+ /**
4
+ * The two short-group faults. A value option that is not last in its group names that option's
5
+ * spelling; a group that mixes scopes names the whole group and the two letters that disagree.
6
+ * The extra letters shape the sentence alone, so `token` and `reason` are the reported facts.
7
+ */
8
+ type ShortGroupFault = {
9
+ reason: 'value-position';
10
+ token: string;
11
+ } | {
12
+ reason: 'mixed-scope';
13
+ token: string;
14
+ global: string;
15
+ other: string;
16
+ };
17
+ /** One registration: the class it names, read as the prototype and the name that class holds. */
18
+ interface Registration {
19
+ name: string;
20
+ prototype: unknown;
21
+ render: (failure: LoomError) => unknown;
22
+ }
23
+ /** Phantom key. It marks a failure registration and holds no runtime value. */
24
+ declare const failureRegistration: unique symbol;
25
+ /** The runtime value `renderFailure` returns. Its pair lives in the registry above. */
26
+ declare class RegisteredFailure {
27
+ readonly [failureRegistration]: true;
28
+ constructor(registration: Registration);
29
+ }
30
+ /** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
31
+ export declare function commandSubject(name: string | null): string;
32
+ /** The same subject at the start of a sentence. */
33
+ export declare function commandSentence(name: string | null): string;
34
+ /**
35
+ * Every failure `run()` reports is an instance of a public class. Each class carries the facts its
36
+ * sentence interpolates, so a renderer reads them instead of parsing prose, and the exit status is
37
+ * a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
38
+ * default renderers add it.
39
+ */
40
+ export declare abstract class LoomError extends Error {
41
+ readonly exitCode: 1 | 2;
42
+ constructor(message: string, exitCode: 1 | 2);
43
+ }
44
+ /** Exit 2: the invocation, not the application, is wrong. */
45
+ export declare abstract class UsageError extends LoomError {
46
+ constructor(message: string);
47
+ }
48
+ /** One input the validation phase rejected: an omission, or a value its schema refused. */
49
+ export type InputProblem = {
50
+ input: InputIdentity;
51
+ spelling: string;
52
+ reason: 'missing';
53
+ } | {
54
+ input: InputIdentity;
55
+ spelling: string;
56
+ reason: 'invalid';
57
+ issues: readonly StandardSchemaV1.Issue[];
58
+ };
59
+ /** The whole validation phase in authoring order, so one failure reports every rejected input. */
60
+ export declare class InputError extends UsageError {
61
+ readonly problems: readonly InputProblem[];
62
+ constructor(message: string, problems: readonly InputProblem[]);
63
+ }
64
+ export declare class UnknownCommandError extends UsageError {
65
+ readonly token: string;
66
+ readonly candidates: readonly string[];
67
+ constructor(token: string, candidates: readonly string[]);
68
+ }
69
+ /** A group answers no invocation of its own, so the routed path names no callable Command. */
70
+ export declare class NonCallableCommandError extends UsageError {
71
+ readonly command: readonly string[];
72
+ readonly candidates: readonly string[];
73
+ constructor(command: readonly string[], candidates: readonly string[]);
74
+ }
75
+ export declare class UnexpectedArgumentError extends UsageError {
76
+ readonly command: readonly string[];
77
+ readonly accepted: number;
78
+ readonly extra: readonly string[];
79
+ constructor(command: readonly string[], accepted: number, extra: readonly string[]);
80
+ }
81
+ export declare class UnknownOptionError extends UsageError {
82
+ readonly spelling: string;
83
+ constructor(spelling: string);
84
+ }
85
+ export declare class MissingValueError extends UsageError {
86
+ readonly spelling: string;
87
+ constructor(spelling: string);
88
+ }
89
+ /** A Boolean spelling takes no value, so the token carried one the declaration cannot accept. */
90
+ export declare class UnexpectedValueError extends UsageError {
91
+ readonly spelling: string;
92
+ readonly value: string;
93
+ constructor(spelling: string, value: string);
94
+ }
95
+ export declare class RepeatedOptionError extends UsageError {
96
+ readonly spelling: string;
97
+ constructor(spelling: string);
98
+ }
99
+ export declare class ShortGroupError extends UsageError {
100
+ readonly token: string;
101
+ readonly reason: 'value-position' | 'mixed-scope';
102
+ constructor(fault: ShortGroupFault);
103
+ }
104
+ /** Exit 1: the declaration is wrong, so the author reads the diagnostic. */
105
+ export declare class DeclarationError extends LoomError {
106
+ constructor(message: string);
107
+ }
108
+ /** Exit 1: the application ended the invocation itself. An application may subclass it. */
109
+ export declare class FatalError extends LoomError {
110
+ constructor(message: string);
111
+ }
112
+ /** Exit 1: an unexpected exception, a non-error throw, or a renderer that could not answer. */
113
+ export declare class InternalError extends LoomError {
114
+ readonly cause: unknown;
115
+ constructor(message: string, cause: unknown);
116
+ }
117
+ /** What a diagnostic says about an unexpected value, whether or not it was an Error. */
118
+ export declare function reasonOf(thrown: unknown): string;
119
+ /**
120
+ * Why a returned value is not the text a renderer owes. A renderer is synchronous, so a returned
121
+ * promise is a non-string return like any other, and its rejection is adopted and swallowed here:
122
+ * an unobserved rejection would end the process before the invocation could report anything.
123
+ */
124
+ export declare function notTextReason(value: unknown): string;
125
+ /** Every thrown value reaches reporting as a failure class; anything else is internal. */
126
+ export declare function toFailure(thrown: unknown): LoomError;
127
+ /** An opaque registration pairing one failure class with a renderer for its instances. */
128
+ export type FailureRenderer = Pick<RegisteredFailure, typeof failureRegistration>;
129
+ /**
130
+ * A registration pairing one failure class with a renderer for its instances. The helper is the
131
+ * typed path for a class-keyed list, because an array literal cannot carry a different type
132
+ * parameter per element.
133
+ */
134
+ export declare function renderFailure<Failure extends LoomError>(type: abstract new (...args: never[]) => Failure, renderer: Renderer<Failure>): FailureRenderer;
135
+ /** The renderers one application registered, keyed by the class each one names. */
136
+ export type FailureRegistry = ReadonlyMap<unknown, Registration>;
137
+ /** One class answers to one renderer, so a second registration for it is a declaration fault. */
138
+ export declare function buildFailures(failures: readonly FailureRenderer[]): FailureRegistry;
139
+ /**
140
+ * The report of one failure: the text core writes, and whether a registered renderer produced it.
141
+ * An unrendered report carries core's own text, which the plain fallback path writes beside the
142
+ * diagnostic naming the renderer that could not answer.
143
+ */
144
+ export type FailureReport = {
145
+ kind: 'rendered';
146
+ text: string;
147
+ } | {
148
+ kind: 'unrendered';
149
+ text: string;
150
+ reason: string;
151
+ };
152
+ /**
153
+ * The text core writes for one failure. Resolution walks the failure's prototype chain most
154
+ * derived first through the application's registrations, then falls to core's own text, so a
155
+ * registration for a base class brands every failure below it.
156
+ */
157
+ export declare function describeFailure(registry: FailureRegistry, failure: LoomError): FailureReport;
158
+ export {};