@loomcli/core 0.1.1 → 0.3.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.
Files changed (57) hide show
  1. package/dist/application.d.ts +73 -28
  2. package/dist/application.js +334 -99
  3. package/dist/chain.d.ts +68 -0
  4. package/dist/chain.js +372 -0
  5. package/dist/command.d.ts +201 -46
  6. package/dist/command.js +713 -57
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -51
  10. package/dist/errors.js +58 -90
  11. package/dist/extension.d.ts +99 -0
  12. package/dist/extension.js +330 -0
  13. package/dist/facts.d.ts +39 -0
  14. package/dist/facts.js +95 -0
  15. package/dist/globals.d.ts +49 -28
  16. package/dist/globals.js +104 -58
  17. package/dist/glyphs.generated.d.ts +464 -0
  18. package/dist/glyphs.generated.js +491 -0
  19. package/dist/host.js +2 -1
  20. package/dist/index.d.ts +21 -6
  21. package/dist/index.js +7 -2
  22. package/dist/inspect.d.ts +69 -12
  23. package/dist/inspect.js +83 -26
  24. package/dist/lanes.d.ts +26 -0
  25. package/dist/lanes.js +45 -0
  26. package/dist/options.d.ts +7 -0
  27. package/dist/options.js +9 -0
  28. package/dist/output.d.ts +93 -15
  29. package/dist/output.js +307 -34
  30. package/dist/plugin.d.ts +132 -0
  31. package/dist/plugin.js +278 -0
  32. package/dist/rendering.d.ts +21 -0
  33. package/dist/rendering.js +72 -0
  34. package/dist/sequence.d.ts +41 -0
  35. package/dist/sequence.js +225 -0
  36. package/dist/signals.d.ts +52 -0
  37. package/dist/signals.js +85 -0
  38. package/dist/style-ansi.d.ts +13 -0
  39. package/dist/style-ansi.js +306 -0
  40. package/dist/style-layout.d.ts +29 -0
  41. package/dist/style-layout.js +228 -0
  42. package/dist/style-resolve.d.ts +6 -0
  43. package/dist/style-resolve.js +26 -0
  44. package/dist/style-state.d.ts +14 -0
  45. package/dist/style-state.js +179 -0
  46. package/dist/style-wire.d.ts +31 -0
  47. package/dist/style-wire.js +201 -0
  48. package/dist/style.d.ts +86 -0
  49. package/dist/style.js +201 -0
  50. package/dist/theme.d.ts +3 -0
  51. package/dist/theme.js +22 -0
  52. package/dist/types.d.ts +222 -26
  53. package/dist/validation.d.ts +12 -3
  54. package/dist/validation.js +34 -17
  55. package/dist/view.d.ts +180 -0
  56. package/dist/view.js +307 -0
  57. package/package.json +2 -1
package/dist/command.js CHANGED
@@ -1,5 +1,9 @@
1
- import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
2
- import { bindGlobals, buildGlobals } from './globals.js';
1
+ var _a;
2
+ import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
3
+ import { buildCommandExtensions, buildExtensions } from './extension.js';
4
+ import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
5
+ import { buildGlobals, keyCollision, spellingCollision } from './globals.js';
6
+ import { resultNode, snapshot } from './inspect.js';
3
7
  import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
4
8
  import { captureConfig, validateValues } from './validation.js';
5
9
  /** Authored values register here, so the public type publishes no state to reach or replace. */
@@ -12,6 +16,15 @@ function nodeOf(parent, child) {
12
16
  }
13
17
  return node;
14
18
  }
19
+ /** Reject retired globals wiring at graph build, before any invocation reads the options. */
20
+ function checkCommandOptions(name, options) {
21
+ if (options !== undefined && !isPlainObject(options)) {
22
+ throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply a Command options object.`);
23
+ }
24
+ if (isPlainObject(options) && 'globals' in options) {
25
+ throw new DeclarationError(`${commandSentence(name)} declares globals. Declare globals on the Application and register its environment.`);
26
+ }
27
+ }
15
28
  /** One name rule for every declared name in the graph, so a child and an argument read alike. */
16
29
  function isDeclaredName(name) {
17
30
  return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
@@ -27,17 +40,26 @@ function checkAliasName(command, alias) {
27
40
  throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
28
41
  }
29
42
  }
30
- /** The state every declaration starts from. The unnamed root and each named Command share it. */
31
- export function freshState(name, globals) {
43
+ /**
44
+ * The state every declaration starts from. The unnamed root and each named Command share it.
45
+ * The declaration values arrive captured, because a later change to the options object the author
46
+ * passed changes nothing the declaration holds.
47
+ */
48
+ export function freshState(declaration) {
32
49
  return {
33
50
  actions: [],
34
51
  aliases: [],
35
52
  bind: () => ({ args: {}, options: {} }),
36
53
  children: [],
37
- globals,
54
+ deprecated: declaration.deprecated,
55
+ description: declaration.description,
56
+ extensions: [declaration.extensions],
57
+ hidden: declaration.hidden,
38
58
  inputs: [],
39
59
  late: [],
40
- name,
60
+ name: declaration.name,
61
+ options: declaration.options,
62
+ results: [],
41
63
  };
42
64
  }
43
65
  /** A declaration after the action is an order fault; build reports the first one recorded. */
@@ -70,6 +92,15 @@ export function declareOption(state, input) {
70
92
  late: recordLate(state, [{ input, kind: 'input' }]),
71
93
  };
72
94
  }
95
+ /** Globals close when composition starts; retain late calls for the shared build-order check. */
96
+ export function recordGlobalOption(state, name) {
97
+ return {
98
+ ...state,
99
+ late: state.actions.length > 0 || state.children.length > 0
100
+ ? [...state.late, { kind: 'global', name }]
101
+ : state.late,
102
+ };
103
+ }
73
104
  /** One call's names stay one group, so the empty call the types reject still reports as one. */
74
105
  export function declareAlias(state, names) {
75
106
  return {
@@ -78,8 +109,47 @@ export function declareAlias(state, names) {
78
109
  late: recordLate(state, names.map((alias) => ({ alias, kind: 'alias' }))),
79
110
  };
80
111
  }
112
+ /** Extension layers remain open after inputs and the action have been fixed. */
113
+ export function declareExtensions(state, values) {
114
+ return { ...state, extensions: [...state.extensions, values] };
115
+ }
116
+ /**
117
+ * The same channel, read as the result the handler's own declaration names. The value one call
118
+ * emits is checked where the action was authored, and the channel core hands an action accepts
119
+ * whatever the routed declaration named, so this reading adds no promise the run does not keep.
120
+ */
121
+ function declaredChannel(out) {
122
+ return { ...out, results: (value) => out.results(value) };
123
+ }
124
+ /**
125
+ * The handler is typed against the result its own declaration carries, and the state holds one
126
+ * list for every declaration, so the context each handler receives is read back at the call.
127
+ */
81
128
  export function declareAction(state, handler) {
82
- return { ...state, actions: [...state.actions, handler] };
129
+ const stored = (context) => handler({ ...context, out: declaredChannel(context.out) });
130
+ return { ...state, actions: [...state.actions, stored] };
131
+ }
132
+ /** The result declaration, which closes both result calls and reports lateness like the rest. */
133
+ export function declareResult(state, kind, declaration) {
134
+ return {
135
+ ...state,
136
+ late: recordLate(state, [{ kind: 'result' }]),
137
+ results: [...state.results, { kind, views: recordOf(declaration, 'views') }],
138
+ };
139
+ }
140
+ /** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
141
+ export function declareResultViews(state, replacements, options) {
142
+ return {
143
+ ...state,
144
+ results: [
145
+ ...state.results,
146
+ { default: recordOf(options, 'default'), kind: 'views', views: replacements },
147
+ ],
148
+ };
149
+ }
150
+ /** One property of an authoring argument, read defensively: build reports whatever it holds. */
151
+ function recordOf(declaration, key) {
152
+ return isPlainObject(declaration) ? declaration[key] : undefined;
83
153
  }
84
154
  /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
85
155
  export function attachChild(state, child) {
@@ -96,12 +166,18 @@ function checkDeclarationOrder(state) {
96
166
  if (!late) {
97
167
  return;
98
168
  }
169
+ if (late.kind === 'global') {
170
+ throw new DeclarationError(`The Application declares global option "${late.name}" after command() or action(). Declare global options before attaching Commands or registering an action.`);
171
+ }
99
172
  if (late.kind === 'input') {
100
173
  throw new DeclarationError(`${commandSentence(name)} declares ${late.input.kind} "${late.input.name}" after its action. Declare arguments and options before action().`);
101
174
  }
102
175
  if (late.kind === 'alias') {
103
176
  throw new DeclarationError(`${commandSentence(name)} declares alias "${late.alias}" after its action. Declare aliases before action().`);
104
177
  }
178
+ if (late.kind === 'result') {
179
+ throw new DeclarationError(`${commandSentence(name)} declares its result after its action. Declare result() or rows() before action().`);
180
+ }
105
181
  // Child identity and names are settled before this call, so the node and its name are valid.
106
182
  throw new DeclarationError(`${commandSentence(name)} attaches child "${String(nodeOf(name, late.child).name)}" after its action. Attach children before action().`);
107
183
  }
@@ -177,7 +253,7 @@ function buildChild(parent, [name, node], context) {
177
253
  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
254
  }
179
255
  context.owners.set(node, parent);
180
- return node.build(context);
256
+ return node.build({ ...context, path: [...context.path, name] });
181
257
  }
182
258
  /** A variadic or optional slot ends the positional list, so nothing may follow either one. */
183
259
  function checkSlotOrder(slot, next, subject) {
@@ -216,18 +292,26 @@ function collectArguments(state, subject) {
216
292
  }
217
293
  return slots;
218
294
  }
295
+ /**
296
+ * One Command's own options against the shared globals table. The table holds the application's
297
+ * globals and every plugin option, so a local collision reads the same sentence whichever scope on
298
+ * the other side claimed the name or the spelling.
299
+ */
219
300
  function compileLocalOptions(state, globals, subject) {
220
301
  const declarations = state.inputs.filter((input) => input.kind === 'option');
302
+ const local = { kind: 'local', subject };
303
+ const application = { kind: 'application' };
221
304
  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.`);
305
+ const claimed = globals.names.get(declaration.name);
306
+ if (claimed) {
307
+ throw keyCollision(declaration.name, claimed, local);
224
308
  }
225
309
  }
226
310
  const options = compileOptions(declarations, subject);
227
311
  for (const [spelling, option] of options) {
228
312
  const global = globals.options.get(spelling);
229
313
  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.`);
314
+ throw spellingCollision(spelling, { name: global.name, owner: globals.names.get(global.name) ?? application }, { name: option.name, owner: local });
231
315
  }
232
316
  }
233
317
  return options;
@@ -247,44 +331,483 @@ function checkGroup(state, children) {
247
331
  throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
248
332
  }
249
333
  }
334
+ /**
335
+ * A view name is a bare token the way a child name is, and never an array index, because an
336
+ * integer-like key does not keep the position the author gave it.
337
+ */
338
+ function isViewName(name) {
339
+ return isDeclaredName(name) && !/^(?:0|[1-9]\d*)$/u.test(name);
340
+ }
341
+ /** One entry read back as the whole view its own `render` function names. */
342
+ function isWholeView(entry) {
343
+ return (typeof entry === 'object' &&
344
+ entry !== null &&
345
+ 'render' in entry &&
346
+ typeof entry.render === 'function');
347
+ }
348
+ /** The same reading for the row shape, which core feeds one row at a time. */
349
+ function isRowView(entry) {
350
+ return (typeof entry === 'object' && entry !== null && 'row' in entry && typeof entry.row === 'function');
351
+ }
352
+ /**
353
+ * The key one `views()` call selected, spelled the way its own diagnostic names it. A call that
354
+ * names none keeps whichever key an earlier call named.
355
+ */
356
+ function selectedKey(value) {
357
+ if (value === undefined) {
358
+ return undefined;
359
+ }
360
+ return typeof value === 'string' ? value : (JSON.stringify(value) ?? 'undefined');
361
+ }
362
+ /** The entries one authored `views` record holds, in record order; anything else holds none. */
363
+ function recordEntries(record) {
364
+ return isPlainObject(record) ? Object.entries(record) : [];
365
+ }
366
+ /**
367
+ * One `views` entry under the unit its declaration named, read back as the shape its own functions
368
+ * name. The two shapes are exclusive, and a row view answers a rows declaration alone.
369
+ */
370
+ function resultView(declaration, name, entry) {
371
+ const { kind, sentence } = declaration;
372
+ const whole = isWholeView(entry);
373
+ const row = isRowView(entry);
374
+ if (whole && row) {
375
+ throw new DeclarationError(`${sentence} names view "${name}" with render and row. Supply one of the two.`);
376
+ }
377
+ if (row) {
378
+ if (kind === 'value') {
379
+ throw new DeclarationError(`${sentence} names row view "${name}" on a value result. Supply a view with render, or declare the result with rows().`);
380
+ }
381
+ return entry;
382
+ }
383
+ if (whole) {
384
+ return entry;
385
+ }
386
+ throw new DeclarationError(`${sentence} names view "${name}" with a value that is not a view. Supply a view with render or a row view with row.`);
387
+ }
388
+ /**
389
+ * Every rule the results lane carries, applied to one Command's calls in the order it made them.
390
+ * The merged record is what the rules read: a later `views()` call replaces a key in place and
391
+ * appends a new one, so each name keeps the position the call that first named it gave it. A
392
+ * `default` once named persists through later calls that name none, and the first key answers
393
+ * until one is named.
394
+ */
395
+ function buildResult(state, hasAction) {
396
+ const sentence = commandSentence(state.name);
397
+ const declarations = state.results.filter((call) => call.kind !== 'views');
398
+ if (declarations.length > 1) {
399
+ throw new DeclarationError(`${sentence} declares two results. Declare one result() or rows() call.`);
400
+ }
401
+ const declaration = declarations[0];
402
+ // A `views()` call reshapes a result's views, so one with no result reshapes nothing.
403
+ // The types publish the call where a result is carried, so this reaches a JavaScript author.
404
+ if (!declaration) {
405
+ if (state.results.length > 0) {
406
+ throw new DeclarationError(`${sentence} reshapes its views and declares no result. Declare result() or rows() before action().`);
407
+ }
408
+ return undefined;
409
+ }
410
+ if (!hasAction) {
411
+ throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
412
+ }
413
+ const views = new Map();
414
+ let selected = undefined;
415
+ for (const call of state.results) {
416
+ for (const [name, entry] of recordEntries(call.views)) {
417
+ const view = resultView({ kind: declaration.kind, sentence }, name, entry);
418
+ if (!isViewName(name)) {
419
+ throw new DeclarationError(`${sentence} names view "${name}". Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.`);
420
+ }
421
+ views.set(name, view);
422
+ }
423
+ if (call.kind === 'views') {
424
+ selected = selectedKey(call.default) ?? selected;
425
+ }
426
+ }
427
+ const first = views.keys().next();
428
+ if (first.done === true) {
429
+ throw new DeclarationError(`${sentence} declares a result with no views. Name at least one view.`);
430
+ }
431
+ if (selected !== undefined && !views.has(selected)) {
432
+ throw new DeclarationError(`${sentence} selects default view "${selected}", which it does not name. Name the view or select a named one.`);
433
+ }
434
+ return { default: selected ?? first.value, kind: declaration.kind, views };
435
+ }
436
+ /** The values one build made, so a value a hook returns is one of them and never a forged shape. */
437
+ const attachments = new WeakMap();
438
+ /**
439
+ * The declared names of one kind, in declaration order, as a hook reads them. The list is frozen,
440
+ * because the surface publishes it read-only and a hook reshapes a Command through its calls alone.
441
+ */
442
+ function declaredNames(declared, kind) {
443
+ return Object.freeze(declared.inputs.filter((input) => input.kind === kind).map((input) => input.name));
444
+ }
445
+ /**
446
+ * One Command unlocked for a hook. Every call returns a new value and leaves its receiver
447
+ * unchanged, and records what it added twice over: on the erased declaration the next value reads
448
+ * its facts from, and on the call list the declaration reads back once every hook has returned.
449
+ * The result is resolved for each value, so a `views()` call reports its own fault where it is made
450
+ * and a later call reads the record that call left behind.
451
+ */
452
+ class AttachedCommandValue {
453
+ #state;
454
+ #result;
455
+ constructor(state) {
456
+ this.#state = state;
457
+ this.#result = resultNode(buildResult(state.declared, state.hasAction));
458
+ attachments.set(this, state);
459
+ Object.freeze(this);
460
+ }
461
+ get name() {
462
+ return this.#state.declared.name;
463
+ }
464
+ get path() {
465
+ return this.#state.path;
466
+ }
467
+ get hasAction() {
468
+ return this.#state.hasAction;
469
+ }
470
+ get arguments() {
471
+ return declaredNames(this.#state.declared, 'argument');
472
+ }
473
+ get options() {
474
+ return declaredNames(this.#state.declared, 'option');
475
+ }
476
+ get result() {
477
+ return this.#result;
478
+ }
479
+ argument(name, config) {
480
+ return this.#declare({ config: captureConfig(config), kind: 'argument', name });
481
+ }
482
+ option(name, config) {
483
+ return this.#declare({ config: captureConfig(config), kind: 'option', name });
484
+ }
485
+ views(replacements, options) {
486
+ const call = {
487
+ default: recordOf(options, 'default'),
488
+ kind: 'views',
489
+ views: replacements,
490
+ };
491
+ const { declared } = this.#state;
492
+ return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
493
+ }
494
+ extend(...values) {
495
+ return this.#derive({ kind: 'extend', values }, this.#state.declared);
496
+ }
497
+ /** One input the running hook declared, which the Command's own names now hold. */
498
+ #declare(input) {
499
+ const { declared, identity } = this.#state;
500
+ return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
501
+ }
502
+ #derive(call, declared) {
503
+ const state = this.#state;
504
+ return new _a({ ...state, calls: [...state.calls, call], declared });
505
+ }
506
+ }
507
+ _a = AttachedCommandValue;
508
+ /** The reason one failed hook reports, which is the thrown value's own message. */
509
+ function attachReason(error) {
510
+ return error instanceof Error ? error.message : String(error);
511
+ }
512
+ /** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
513
+ function callHook(value, hook, named) {
514
+ try {
515
+ return hook(value);
516
+ }
517
+ catch (error) {
518
+ // A hook that reports a declaration fault of its own reports as itself.
519
+ if (error instanceof DeclarationError) {
520
+ throw error;
521
+ }
522
+ throw new DeclarationError(`Plugin "${named.identity}" failed in onCommandAttach for ${named.subject}: ${attachReason(error)}.`);
523
+ }
524
+ }
525
+ /**
526
+ * One plugin's hook over one Command, which answers with the declaration the hook returned. The
527
+ * returned value has to carry the lineage token of the value the hook received, so the surface of
528
+ * another Command, or of one an earlier build made, never replays its calls onto this Command.
529
+ */
530
+ function attachOnce(value, hook, named) {
531
+ const state = attachments.get(callHook(value, hook, named));
532
+ if (!state || state.lineage !== named.lineage) {
533
+ throw new DeclarationError(`Plugin "${named.identity}" returned a value that is not the attached Command from onCommandAttach for ${named.subject}. Return the value it received or a value derived from it.`);
534
+ }
535
+ return state;
536
+ }
537
+ /**
538
+ * Every installed plugin's hook over one Command, in installation order, each receiving what the
539
+ * previous returned. The calls they made travel back to the declaration the remaining rules read.
540
+ */
541
+ function runAttachHooks(declared, facts, plugins) {
542
+ const subject = commandSubject(declared.name);
543
+ const { lineage } = facts;
544
+ let progress = { calls: [], declared };
545
+ for (const installed of plugins) {
546
+ const hook = installed.onCommandAttach;
547
+ if (hook) {
548
+ const identity = installed.identity;
549
+ const value = new AttachedCommandValue({ ...progress, ...facts, identity });
550
+ const state = attachOnce(value, hook, { identity, lineage, subject });
551
+ progress = { calls: state.calls, declared: state.declared };
552
+ }
553
+ }
554
+ return progress.calls;
555
+ }
556
+ /**
557
+ * One input a hook declared. It joins the values an action reads at run time and the declaration's
558
+ * types not at all, and it records no late declaration, because a hook's calls are exempt from the
559
+ * closures `action()` applies.
560
+ */
561
+ function declareHookInput(state, input) {
562
+ const previous = state.bind;
563
+ return {
564
+ ...state,
565
+ bind: (values) => {
566
+ const bound = previous(values);
567
+ return input.kind === 'argument'
568
+ ? { ...bound, args: { ...bound.args, ...values.argument(input) } }
569
+ : { ...bound, options: { ...bound.options, ...values.option(input) } };
570
+ },
571
+ inputs: [...state.inputs, input],
572
+ };
573
+ }
574
+ /** The declaration the hooks left behind, which every remaining build rule reads. */
575
+ function applyAttachCalls(state, calls) {
576
+ let next = state;
577
+ for (const call of calls) {
578
+ if (call.kind === 'input') {
579
+ next = declareHookInput(next, call.input);
580
+ }
581
+ else if (call.kind === 'views') {
582
+ next = { ...next, results: [...next.results, call.call] };
583
+ }
584
+ else {
585
+ next = declareExtensions(next, call.values);
586
+ }
587
+ }
588
+ return next;
589
+ }
590
+ /** The clause and the remedy an input another plugin's hook already declared earns. */
591
+ function hookClause(kind, identity) {
592
+ return {
593
+ clause: `an ${kind} plugin "${identity}" declared through onCommandAttach`,
594
+ remedy: 'Install one of them.',
595
+ };
596
+ }
597
+ /**
598
+ * The remedy a collision against the Command's own declarations, the globals table, or another
599
+ * plugin's option earns. The remedy follows the target alone, whichever kind the hook declared:
600
+ * the author renames their own local option or argument, the author renames the colliding global
601
+ * option, or, when the target is another plugin's option, the two plugins install one of them.
602
+ */
603
+ function attachedRemedy(target) {
604
+ if (target === 'local') {
605
+ return "Rename the Command's option or omit the plugin.";
606
+ }
607
+ if (target === 'argument') {
608
+ return "Rename the Command's argument or omit the plugin.";
609
+ }
610
+ if (target === 'global') {
611
+ return 'Rename the global option or omit the plugin.';
612
+ }
613
+ return 'Install one of them.';
614
+ }
615
+ /**
616
+ * What one name a hook-declared input of either kind collides with, in the order the scopes are
617
+ * reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
618
+ * plugin-owned option, or an argument the Command declares. The remedy follows the target alone,
619
+ * not which kind the hook declared.
620
+ */
621
+ function attachedCollision(input, held) {
622
+ const { name } = input;
623
+ const hook = held.hooks.get(name);
624
+ if (hook !== undefined) {
625
+ return hookClause('option', hook);
626
+ }
627
+ const hooked = held.hookArguments.get(name);
628
+ if (hooked !== undefined) {
629
+ return hookClause('argument', hooked);
630
+ }
631
+ if (held.locals.has(name)) {
632
+ return { clause: 'a local option', remedy: attachedRemedy('local') };
633
+ }
634
+ const claimed = held.globals.names.get(name);
635
+ if (claimed) {
636
+ const target = claimed.kind === 'plugin' ? 'plugin' : 'global';
637
+ const clause = claimed.kind === 'plugin' ? `an option of plugin "${claimed.identity}"` : 'a global option';
638
+ return { clause, remedy: attachedRemedy(target) };
639
+ }
640
+ return held.arguments.has(name)
641
+ ? { clause: 'an argument', remedy: attachedRemedy('argument') }
642
+ : undefined;
643
+ }
644
+ /**
645
+ * The spellings one compiled table holds, each under the form its own option is named by, which is
646
+ * its long form where it declares one and the colliding spelling itself where it declares none.
647
+ */
648
+ function readSpellings(table, claimed) {
649
+ const longs = new Map();
650
+ for (const [spelling, option] of table) {
651
+ if (option.role === 'long') {
652
+ longs.set(option.name, spelling);
653
+ }
654
+ }
655
+ for (const [spelling, option] of table) {
656
+ if (!claimed.has(spelling)) {
657
+ claimed.set(spelling, longs.get(option.name) ?? spelling);
658
+ }
659
+ }
660
+ }
661
+ /** One hook-declared option's spellings, against every spelling the table already claims. */
662
+ function checkAttachedSpelling(declared, named) {
663
+ const { claimed, subject } = named;
664
+ const table = compileOptions([declared.input], subject);
665
+ for (const [spelling] of table) {
666
+ const used = claimed.get(spelling);
667
+ if (used !== undefined) {
668
+ throw new DeclarationError(`Plugin "${declared.identity}" declares option "${declared.input.name}" with spelling "${spelling}" on ${subject}, which "${used}" already uses.`);
669
+ }
670
+ }
671
+ readSpellings(table, claimed);
672
+ }
673
+ /**
674
+ * Every input a hook declared, against the names and spellings the Command, the globals table, and
675
+ * an earlier hook already hold. It runs before the Command's own inputs compile, so a hook-declared
676
+ * input reports as the plugin's fault and never as the author's.
677
+ */
678
+ function checkAttachedInputs(declared, attached, globals) {
679
+ const subject = commandSubject(declared.name);
680
+ const hooked = new Set(attached.map((entry) => entry.input));
681
+ const authored = declared.inputs.filter((input) => !hooked.has(input));
682
+ const options = authored.filter((input) => input.kind === 'option');
683
+ const held = {
684
+ arguments: new Set(authored.filter((input) => input.kind === 'argument').map((input) => input.name)),
685
+ globals,
686
+ hookArguments: new Map(),
687
+ hooks: new Map(),
688
+ locals: new Set(options.map((input) => input.name)),
689
+ };
690
+ const claimed = new Map();
691
+ readSpellings(globals.options, claimed);
692
+ readSpellings(compileOptions(options, subject), claimed);
693
+ for (const { identity, input } of attached) {
694
+ const collision = attachedCollision(input, held);
695
+ if (collision) {
696
+ throw new DeclarationError(`Plugin "${identity}" declares ${input.kind} "${input.name}" on ${subject}, which is already declared as ${collision.clause}. ${collision.remedy}`);
697
+ }
698
+ if (input.kind === 'option') {
699
+ checkAttachedSpelling({ identity, input }, { claimed, subject });
700
+ held.hooks.set(input.name, identity);
701
+ }
702
+ else {
703
+ held.hookArguments.set(input.name, identity);
704
+ }
705
+ }
706
+ }
250
707
  /** 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 }) => {
708
+ function bindDispatch(state, action, globals) {
709
+ return ({ host, out, passthrough, signal, style, values }) => {
253
710
  const bound = state.bind(values);
711
+ // Last resort: no typed path exists.
712
+ // The graph erases the binder's generic relationship.
713
+ // It holds because attachment checks the global output requirement.
714
+ // Graph build rejects a collision between a global and a local.
715
+ // This binder returns the Application's validated globals alone.
716
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
717
+ const globalOptions = globals.bind(values);
254
718
  return action({
255
719
  args: bound.args,
256
720
  host,
257
- options: { ...bindGlobals(state.globals, values), ...bound.options },
721
+ options: { ...globalOptions, ...bound.options },
258
722
  out,
259
723
  passthrough,
724
+ signal,
725
+ style,
260
726
  });
261
727
  };
262
728
  }
729
+ /** Each declaration's own facts and extension values, whichever scope declared it. */
730
+ function checkInputFacts(inputs, command, context) {
731
+ const { name, subject } = command;
732
+ for (const input of inputs) {
733
+ const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
734
+ checkDescription(sentence, input.config.description);
735
+ if (input.kind === 'argument') {
736
+ checkNoListingFacts(sentence, input.config);
737
+ }
738
+ else {
739
+ checkHidden(sentence, input.config.hidden);
740
+ checkDeprecated(sentence, input.config.deprecated);
741
+ }
742
+ context.extensions.set(input, buildExtensions({
743
+ declared: input.config.extensions,
744
+ descriptors: context.descriptors,
745
+ subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
746
+ target: input.kind,
747
+ }));
748
+ }
749
+ }
750
+ /** One Command declares arguments or attaches children, whichever declaration made each of them. */
751
+ function checkArgumentPlacement(name, slot, child) {
752
+ if (slot && child) {
753
+ throw new DeclarationError(`${commandSentence(name)} declares argument "${slot.input.name}" and attaches child "${child[0]}". Move the argument into a child Command or remove the children.`);
754
+ }
755
+ }
263
756
  /** Validates one declaration against the shared globals table and compiles it for dispatch. */
264
757
  export function buildCommand(state, context) {
265
758
  const { actions, name } = state;
266
759
  const { globals } = context;
267
760
  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
- }
761
+ const layer = { phrase: `on ${subject}`, sentence: commandSentence(name) };
762
+ checkCommandOptions(name, state.options);
763
+ const description = checkDescription(commandSentence(name), state.description);
764
+ const hidden = checkHidden(commandSentence(name), state.hidden);
765
+ const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
766
+ const declaredExtensions = buildCommandExtensions({
767
+ descriptors: context.descriptors,
768
+ layers: state.extensions,
769
+ subject: layer,
770
+ });
771
+ // Each declaration's own facts, in authoring order, before the rules that pair declarations.
772
+ checkInputFacts(state.inputs, { name, subject }, context);
271
773
  const attached = collectChildren(state);
272
774
  checkDeclarationOrder(state);
273
775
  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
- }
776
+ const declaredSlots = collectArguments(state, subject);
777
+ checkArgumentPlacement(name, declaredSlots[0], attached[0]);
280
778
  if (actions.length > 1) {
281
779
  throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
282
780
  }
283
781
  const action = actions[0];
782
+ const hasAction = action !== undefined;
783
+ const declaredResult = buildResult(state, hasAction);
784
+ /**
785
+ * The hooks run once the author's declaration is complete and its result is resolved, so a hook
786
+ * reads an exact record, and every rule below reads what the hooks returned.
787
+ */
788
+ // One token per Command per build, which binds a hook's return to the value it received.
789
+ const lineage = {};
790
+ const calls = runAttachHooks(state, { hasAction, lineage, path: context.path }, context.plugins);
791
+ const hooked = applyAttachCalls(state, calls);
792
+ const hookInputs = calls.filter((call) => call.kind === 'input');
793
+ checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
794
+ // The hook-declared names are checked first, so a collision reports in the plugin's voice.
795
+ // A rule the author's own declaration voices never speaks for a name a hook declared.
796
+ checkAttachedInputs(hooked, hookInputs, globals);
797
+ const extensions = calls.some((call) => call.kind === 'extend')
798
+ ? buildCommandExtensions({
799
+ descriptors: context.descriptors,
800
+ layers: hooked.extensions,
801
+ subject: layer,
802
+ })
803
+ : declaredExtensions;
804
+ const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
805
+ checkArgumentPlacement(name, slots[0], attached[0]);
806
+ const result = calls.length > 0 ? buildResult(hooked, hasAction) : declaredResult;
284
807
  if (!action) {
285
- checkGroup(state, attached);
808
+ checkGroup(hooked, attached);
286
809
  }
287
- const options = compileLocalOptions(state, globals, subject);
810
+ const options = compileLocalOptions(hooked, globals, subject);
288
811
  const children = new Map();
289
812
  const routes = new Map();
290
813
  const claim = aliasNamespace(name, new Set(attached.map((entry) => entry[0])));
@@ -302,17 +825,33 @@ export function buildCommand(state, context) {
302
825
  aliases,
303
826
  arguments: slots,
304
827
  children,
305
- dispatch: action ? bindDispatch(state, action) : undefined,
306
- inputs: state.inputs,
828
+ deprecated,
829
+ description,
830
+ dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
831
+ extensions,
832
+ hidden,
833
+ inputs: hooked.inputs,
307
834
  name,
308
835
  options,
836
+ result,
309
837
  routes,
310
838
  };
311
839
  }
312
840
  /** 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) };
841
+ export function buildGraph(root, globals, install) {
842
+ const context = {
843
+ descriptors: install.descriptors,
844
+ extensions: install.extensions,
845
+ globals: buildGlobals(globals, install.plugins, install),
846
+ owners: new Map(),
847
+ path: [],
848
+ plugins: install.plugins,
849
+ };
850
+ return {
851
+ extensions: context.extensions,
852
+ globals: context.globals,
853
+ root: buildCommand(root, context),
854
+ };
316
855
  }
317
856
  export class CommandBuilder {
318
857
  #state;
@@ -329,7 +868,7 @@ export class CommandBuilder {
329
868
  kind: 'argument',
330
869
  name,
331
870
  };
332
- return new CommandBuilder(declareArgument(this.#state, input));
871
+ return this.derive(declareArgument(this.#state, input));
333
872
  }
334
873
  option(name, config) {
335
874
  const input = {
@@ -337,38 +876,69 @@ export class CommandBuilder {
337
876
  kind: 'option',
338
877
  name,
339
878
  };
340
- return new CommandBuilder(declareOption(this.#state, input));
879
+ return this.derive(declareOption(this.#state, input));
341
880
  }
342
881
  /**
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.
882
+ * Aliases are other bare tokens that route to this Command. They invalidate no call, and the
883
+ * tuple rest parameter rejects a call that names none.
345
884
  */
346
885
  alias(...names) {
347
- return new CommandBuilder(declareAlias(this.#state, names));
886
+ return this.derive(declareAlias(this.#state, names));
348
887
  }
349
888
  /** A child arrives in any type state, because its own action is the call that finished it. */
350
889
  command(child) {
351
- return new CommandBuilder(attachChild(this.#state, child));
890
+ return this.derive(attachChild(this.#state, child));
891
+ }
892
+ /**
893
+ * The value this Command produces for its consumer. The type argument is stated by the author,
894
+ * so the views record states no type of its own and an omitted argument names none either.
895
+ */
896
+ result(declaration) {
897
+ return this.derive(declareResult(this.#state, 'value', declaration));
352
898
  }
353
- /** The action is the last declaration call, so the value it returns publishes `AfterAction`. */
899
+ /** The same declaration over a sequence, whose type argument is one row. */
900
+ rows(declaration) {
901
+ return this.derive(declareResult(this.#state, 'rows', declaration));
902
+ }
903
+ /**
904
+ * Views after the fact. It merges by key, so an existing name is replaced in place and a new one
905
+ * is appended, and `default` names the key core renders when nothing selects another.
906
+ */
907
+ views(replacements, options) {
908
+ return this.derive(declareResultViews(this.#state, replacements, options));
909
+ }
910
+ /** The action closes input authoring; `extend()` remains outside this state transition. */
354
911
  action(handler) {
355
- return new CommandBuilder(declareAction(this.#state, handler));
912
+ return this.derive(declareAction(this.#state, handler));
913
+ }
914
+ extend(...values) {
915
+ return this.derive(declareExtensions(this.#state, values));
356
916
  }
357
917
  build(context) {
358
918
  return buildCommand(this.#state, context);
359
919
  }
920
+ /**
921
+ * The same runtime value in the state the calling method's return type names. Each call states
922
+ * its own transition, and the declared result travels with it unless the call replaces it.
923
+ */
924
+ derive(state) {
925
+ return new CommandBuilder(state);
926
+ }
360
927
  }
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
- */
928
+ /** The constructor uses the Application registration; public type defaults stay library-neutral. */
366
929
  class CommandDeclaration extends CommandBuilder {
367
- constructor(name, globals) {
368
- super(freshState(name, globals));
930
+ constructor(name, options) {
931
+ super(freshState({
932
+ deprecated: options?.deprecated,
933
+ description: options?.description,
934
+ extensions: options?.extensions,
935
+ hidden: options?.hidden,
936
+ name,
937
+ options,
938
+ }));
369
939
  }
370
940
  }
371
- /** The public constructor requires a name and narrows the globals type to the supplied value. */
941
+ /** The public constructor takes a name and one options object, as the Application does. */
372
942
  export const Command = CommandDeclaration;
373
943
  /** Every declaration in the graph, so defaults are validated before any token is read. */
374
944
  export function collectInputs(command) {
@@ -377,6 +947,14 @@ export function collectInputs(command) {
377
947
  ...[...command.children.values()].flatMap((child) => collectInputs(child)),
378
948
  ];
379
949
  }
950
+ /**
951
+ * The names a routing failure offers: the canonical names of the visible children, in authoring
952
+ * order. A candidate list is a listing, so a hidden child is absent from it, and a parent whose
953
+ * children are all hidden offers none.
954
+ */
955
+ function candidatesOf(command) {
956
+ return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
957
+ }
380
958
  /** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
381
959
  export function route(root, tokens) {
382
960
  let command = root;
@@ -389,7 +967,7 @@ export function route(root, tokens) {
389
967
  }
390
968
  const child = command.routes.get(token);
391
969
  if (!child) {
392
- throw new UnknownCommandError(token, [...command.children.keys()]);
970
+ throw new UnknownCommandError(token, candidatesOf(command));
393
971
  }
394
972
  // An alias routes like the canonical name, and the path it walks reports that name alone.
395
973
  command = child.command;
@@ -428,25 +1006,103 @@ function bindArguments(command, path, positionals) {
428
1006
  }
429
1007
  return values;
430
1008
  }
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);
1009
+ /** Consumes the globals table, then routes the remaining bare tokens to a Command. */
1010
+ export function routeInvocation(graph, argv) {
1011
+ const scan = extractGlobals(graph.globals.options, [...argv]);
1012
+ const routed = route(graph.root, scan.rest);
1013
+ return {
1014
+ command: routed.command,
1015
+ path: routed.path,
1016
+ scan: scan.values,
1017
+ tokens: routed.tokens,
1018
+ };
1019
+ }
1020
+ /**
1021
+ * The frozen records one middleware reads: the routed Command's own inputs, and the tail. Every
1022
+ * value is a plain-data copy frozen to every depth, so a middleware that reaches into a list or a
1023
+ * schema's output object reaches its own copy and contributes nothing to what the action receives.
1024
+ * A value that is neither an array nor a plain object, a class instance or a `Date` a schema
1025
+ * produced, is shared by reference, because core cannot copy it meaningfully.
1026
+ */
1027
+ function requestOf(command, values, passthrough) {
1028
+ const args = {};
1029
+ const options = {};
1030
+ for (const input of command.inputs) {
1031
+ const target = input.kind === 'argument' ? args : options;
1032
+ target[input.name] = snapshot(values.read(input));
1033
+ }
1034
+ return Object.freeze({
1035
+ args: Object.freeze(args),
1036
+ options: Object.freeze(options),
1037
+ passthrough: Object.freeze([...passthrough]),
1038
+ });
1039
+ }
1040
+ /**
1041
+ * The phases that run ahead of the chain: the callable check, local parsing, and validation. It
1042
+ * answers with the call that dispatches, so the caller records that the action was invoked at the
1043
+ * moment it invokes it and no earlier failure reads as a dispatch.
1044
+ */
1045
+ async function readyDispatch(graph, routed, invocation) {
1046
+ const { command, path, scan } = routed;
436
1047
  const { dispatch } = command;
437
1048
  // A group answers no invocation of its own, so it fails with the routing errors above it.
438
1049
  if (!dispatch) {
439
- throw new NonCallableCommandError(path, [...command.children.keys()]);
1050
+ throw new NonCallableCommandError(path, candidatesOf(command));
440
1051
  }
441
- const parsed = parseInputs(command.options, rest);
1052
+ const parsed = parseInputs(command.options, routed.tokens);
442
1053
  const args = bindArguments(command, path, parsed.positionals);
443
1054
  const values = await validateValues({
444
1055
  command: path,
445
1056
  defaults: invocation.defaults,
446
- host,
1057
+ host: invocation.host,
447
1058
  inputs: { globals: graph.globals.inputs, locals: command.inputs },
448
1059
  passthrough: parsed.passthrough,
449
- supplied: { args, options: mergeValues(scan.values, parsed.options) },
1060
+ signal: invocation.signal,
1061
+ supplied: { args, options: mergeValues(scan, parsed.options) },
450
1062
  });
451
- return { dispatch, passthrough: parsed.passthrough, values };
1063
+ return {
1064
+ dispatch: async (view) => {
1065
+ // The channel is built at the boundary, because the view a middleware selected is read there.
1066
+ const channel = invocation.channel({ path, result: command.result, view });
1067
+ try {
1068
+ await dispatch({
1069
+ host: invocation.host,
1070
+ out: channel.out,
1071
+ passthrough: parsed.passthrough,
1072
+ signal: invocation.signal,
1073
+ style: invocation.style,
1074
+ values,
1075
+ });
1076
+ /**
1077
+ * A declared result is a promise the Command makes, so an action that returned normally
1078
+ * without emitting one broke it. A failure raised before the call is that failure, and a
1079
+ * cancelled run raises none, because an action that reads its signal and returns is the
1080
+ * sanctioned path.
1081
+ */
1082
+ if (command.result && !channel.emitted() && !invocation.signal.aborted) {
1083
+ throw new ResultError('missing', path);
1084
+ }
1085
+ }
1086
+ catch (error) {
1087
+ // The action's failure is the invocation's outcome.
1088
+ // A sequence it left pending is stopped where it stands rather than drained to its end.
1089
+ channel.stop();
1090
+ throw error;
1091
+ }
1092
+ },
1093
+ request: requestOf(command, values, parsed.passthrough),
1094
+ };
1095
+ }
1096
+ /**
1097
+ * Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
1098
+ * middleware reads the request before the action runs and a takeover never observes the fault.
1099
+ */
1100
+ export async function prepareDispatch(graph, routed, invocation) {
1101
+ const result = routed.command.result;
1102
+ try {
1103
+ return { ...(await readyDispatch(graph, routed, invocation)), kind: 'ready', result };
1104
+ }
1105
+ catch (error) {
1106
+ return { fault: error, kind: 'held', request: null, result };
1107
+ }
452
1108
  }