@loomcli/core 0.2.0 → 0.4.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 (51) hide show
  1. package/dist/application.d.ts +59 -34
  2. package/dist/application.js +162 -59
  3. package/dist/chain.d.ts +25 -6
  4. package/dist/chain.js +99 -15
  5. package/dist/command.d.ts +146 -42
  6. package/dist/command.js +642 -78
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -59
  10. package/dist/errors.js +49 -106
  11. package/dist/extension.d.ts +80 -20
  12. package/dist/extension.js +115 -30
  13. package/dist/globals.d.ts +14 -25
  14. package/dist/globals.js +10 -61
  15. package/dist/glyphs.generated.d.ts +464 -0
  16. package/dist/glyphs.generated.js +491 -0
  17. package/dist/host.js +2 -1
  18. package/dist/index.d.ts +15 -6
  19. package/dist/index.js +5 -2
  20. package/dist/inspect.d.ts +38 -4
  21. package/dist/inspect.js +69 -6
  22. package/dist/lanes.d.ts +26 -0
  23. package/dist/lanes.js +45 -0
  24. package/dist/output.d.ts +93 -15
  25. package/dist/output.js +307 -34
  26. package/dist/plugin.d.ts +32 -16
  27. package/dist/plugin.js +46 -18
  28. package/dist/rendering.d.ts +21 -0
  29. package/dist/rendering.js +72 -0
  30. package/dist/sequence.d.ts +41 -0
  31. package/dist/sequence.js +225 -0
  32. package/dist/style-ansi.d.ts +13 -0
  33. package/dist/style-ansi.js +306 -0
  34. package/dist/style-layout.d.ts +29 -0
  35. package/dist/style-layout.js +228 -0
  36. package/dist/style-resolve.d.ts +6 -0
  37. package/dist/style-resolve.js +26 -0
  38. package/dist/style-state.d.ts +14 -0
  39. package/dist/style-state.js +179 -0
  40. package/dist/style-wire.d.ts +31 -0
  41. package/dist/style-wire.js +201 -0
  42. package/dist/style.d.ts +86 -0
  43. package/dist/style.js +201 -0
  44. package/dist/theme.d.ts +3 -0
  45. package/dist/theme.js +22 -0
  46. package/dist/types.d.ts +174 -21
  47. package/dist/validation.d.ts +8 -1
  48. package/dist/validation.js +17 -2
  49. package/dist/view.d.ts +180 -0
  50. package/dist/view.js +307 -0
  51. package/package.json +2 -1
package/dist/command.js CHANGED
@@ -1,7 +1,9 @@
1
- import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
2
- import { buildExtensions } from './extension.js';
1
+ var _a;
2
+ import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
3
+ import { buildExtensions, extendStore, publishStore, storeCommandLayers, validateLayer, } from './extension.js';
3
4
  import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
4
- import { bindGlobals, buildGlobals, isGlobalOptions, keyCollision, spellingCollision, } from './globals.js';
5
+ import { buildGlobals, keyCollision, spellingCollision } from './globals.js';
6
+ import { resultNode, snapshot } from './inspect.js';
5
7
  import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
6
8
  import { captureConfig, validateValues } from './validation.js';
7
9
  /** Authored values register here, so the public type publishes no state to reach or replace. */
@@ -14,20 +16,13 @@ function nodeOf(parent, child) {
14
16
  }
15
17
  return node;
16
18
  }
17
- /**
18
- * The shape of a Command's second argument, read where it is supplied. The retired positional form
19
- * declares its globals on a value that holds no `globals` key, so without this rule the globals
20
- * vanish silently and the operator, not the author, meets the consequence as an unknown-option
21
- * error. It answers before every other rule, because a Command that lost its globals this way would
22
- * otherwise report the mismatch with its Application instead of the slot that caused it.
23
- * The facts the slot carried were captured at construction, so this reads the slot's shape alone.
24
- */
19
+ /** Reject retired globals wiring at graph build, before any invocation reads the options. */
25
20
  function checkCommandOptions(name, options) {
26
- if (isGlobalOptions(options)) {
27
- throw new DeclarationError(`${commandSentence(name)} takes an options object. Supply { globals } instead of a positional GlobalOptions value.`);
28
- }
29
21
  if (options !== undefined && !isPlainObject(options)) {
30
- throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply { globals }.`);
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.`);
31
26
  }
32
27
  }
33
28
  /** One name rule for every declared name in the graph, so a child and an argument read alike. */
@@ -58,13 +53,13 @@ export function freshState(declaration) {
58
53
  children: [],
59
54
  deprecated: declaration.deprecated,
60
55
  description: declaration.description,
61
- extensions: declaration.extensions,
62
- globals: declaration.globals,
56
+ extensions: [declaration.extensions],
63
57
  hidden: declaration.hidden,
64
58
  inputs: [],
65
59
  late: [],
66
60
  name: declaration.name,
67
61
  options: declaration.options,
62
+ results: [],
68
63
  };
69
64
  }
70
65
  /** A declaration after the action is an order fault; build reports the first one recorded. */
@@ -97,6 +92,15 @@ export function declareOption(state, input) {
97
92
  late: recordLate(state, [{ input, kind: 'input' }]),
98
93
  };
99
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
+ }
100
104
  /** One call's names stay one group, so the empty call the types reject still reports as one. */
101
105
  export function declareAlias(state, names) {
102
106
  return {
@@ -105,8 +109,47 @@ export function declareAlias(state, names) {
105
109
  late: recordLate(state, names.map((alias) => ({ alias, kind: 'alias' }))),
106
110
  };
107
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
+ */
108
128
  export function declareAction(state, handler) {
109
- 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;
110
153
  }
111
154
  /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
112
155
  export function attachChild(state, child) {
@@ -123,12 +166,18 @@ function checkDeclarationOrder(state) {
123
166
  if (!late) {
124
167
  return;
125
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
+ }
126
172
  if (late.kind === 'input') {
127
173
  throw new DeclarationError(`${commandSentence(name)} declares ${late.input.kind} "${late.input.name}" after its action. Declare arguments and options before action().`);
128
174
  }
129
175
  if (late.kind === 'alias') {
130
176
  throw new DeclarationError(`${commandSentence(name)} declares alias "${late.alias}" after its action. Declare aliases before action().`);
131
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
+ }
132
181
  // Child identity and names are settled before this call, so the node and its name are valid.
133
182
  throw new DeclarationError(`${commandSentence(name)} attaches child "${String(nodeOf(name, late.child).name)}" after its action. Attach children before action().`);
134
183
  }
@@ -204,7 +253,7 @@ function buildChild(parent, [name, node], context) {
204
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.`);
205
254
  }
206
255
  context.owners.set(node, parent);
207
- return node.build(context);
256
+ return node.build({ ...context, path: [...context.path, name] });
208
257
  }
209
258
  /** A variadic or optional slot ends the positional list, so nothing may follow either one. */
210
259
  function checkSlotOrder(slot, next, subject) {
@@ -282,37 +331,426 @@ function checkGroup(state, children) {
282
331
  throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
283
332
  }
284
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
+ /** Published on first read and shared by every value whose store is unchanged. */
480
+ get extensions() {
481
+ return publishStore(this.#state.extensions);
482
+ }
483
+ argument(name, config) {
484
+ return this.#declare({ config: captureConfig(config), kind: 'argument', name });
485
+ }
486
+ option(name, config) {
487
+ return this.#declare({ config: captureConfig(config), kind: 'option', name });
488
+ }
489
+ views(replacements, options) {
490
+ const call = {
491
+ default: recordOf(options, 'default'),
492
+ kind: 'views',
493
+ views: replacements,
494
+ };
495
+ const { declared } = this.#state;
496
+ return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
497
+ }
498
+ /**
499
+ * The values are validated here, at the call, so the next read of `extensions` holds them and
500
+ * build never validates them again. A rejected value throws from the call and adds nothing.
501
+ */
502
+ extend(...values) {
503
+ const state = this.#state;
504
+ const registry = new Map(state.registry);
505
+ const layer = validateLayer({
506
+ declared: values,
507
+ descriptors: registry,
508
+ subject: state.subject,
509
+ target: 'command',
510
+ });
511
+ return new _a({
512
+ ...state,
513
+ extensions: extendStore(state.extensions, layer),
514
+ registry,
515
+ });
516
+ }
517
+ /** One input the running hook declared, which the Command's own names now hold. */
518
+ #declare(input) {
519
+ const { declared, identity } = this.#state;
520
+ return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
521
+ }
522
+ #derive(call, declared) {
523
+ const state = this.#state;
524
+ return new _a({ ...state, calls: [...state.calls, call], declared });
525
+ }
526
+ }
527
+ _a = AttachedCommandValue;
528
+ /** The reason one failed hook reports, which is the thrown value's own message. */
529
+ function attachReason(error) {
530
+ return error instanceof Error ? error.message : String(error);
531
+ }
532
+ /** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
533
+ function callHook(value, hook, named) {
534
+ try {
535
+ return hook(value);
536
+ }
537
+ catch (error) {
538
+ // A hook that reports a declaration fault of its own reports as itself.
539
+ if (error instanceof DeclarationError) {
540
+ throw error;
541
+ }
542
+ throw new DeclarationError(`Plugin "${named.identity}" failed in onCommandAttach for ${named.subject}: ${attachReason(error)}.`);
543
+ }
544
+ }
545
+ /**
546
+ * One plugin's hook over one Command, which answers with the declaration the hook returned. The
547
+ * returned value has to carry the lineage token of the value the hook received, so the surface of
548
+ * another Command, or of one an earlier build made, never replays its calls onto this Command.
549
+ */
550
+ function attachOnce(value, hook, named) {
551
+ const state = attachments.get(callHook(value, hook, named));
552
+ if (!state || state.lineage !== named.lineage) {
553
+ 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.`);
554
+ }
555
+ return state;
556
+ }
557
+ /**
558
+ * Every installed plugin's hook over one Command, in installation order, each receiving what the
559
+ * previous returned. The calls they made travel back to the declaration the remaining rules read.
560
+ */
561
+ function runAttachHooks(start, facts, plugins) {
562
+ const { declared, extensions, layer, registry } = start;
563
+ const subject = commandSubject(declared.name);
564
+ const { lineage } = facts;
565
+ let progress = { calls: [], declared, extensions, registry };
566
+ for (const installed of plugins) {
567
+ const hook = installed.onCommandAttach;
568
+ if (hook) {
569
+ const identity = installed.identity;
570
+ const value = new AttachedCommandValue({ ...progress, ...facts, identity, subject: layer });
571
+ const state = attachOnce(value, hook, { identity, lineage, subject });
572
+ progress = {
573
+ calls: state.calls,
574
+ declared: state.declared,
575
+ extensions: state.extensions,
576
+ registry: state.registry,
577
+ };
578
+ }
579
+ }
580
+ return progress;
581
+ }
582
+ /**
583
+ * One input a hook declared. It joins the values an action reads at run time and the declaration's
584
+ * types not at all, and it records no late declaration, because a hook's calls are exempt from the
585
+ * closures `action()` applies.
586
+ */
587
+ function declareHookInput(state, input) {
588
+ const previous = state.bind;
589
+ return {
590
+ ...state,
591
+ bind: (values) => {
592
+ const bound = previous(values);
593
+ return input.kind === 'argument'
594
+ ? { ...bound, args: { ...bound.args, ...values.argument(input) } }
595
+ : { ...bound, options: { ...bound.options, ...values.option(input) } };
596
+ },
597
+ inputs: [...state.inputs, input],
598
+ };
599
+ }
600
+ /** The declaration the hooks left behind, which every remaining build rule reads. */
601
+ function applyAttachCalls(state, calls) {
602
+ let next = state;
603
+ for (const call of calls) {
604
+ next =
605
+ call.kind === 'input'
606
+ ? declareHookInput(next, call.input)
607
+ : { ...next, results: [...next.results, call.call] };
608
+ }
609
+ return next;
610
+ }
611
+ /** The clause and the remedy an input another plugin's hook already declared earns. */
612
+ function hookClause(kind, identity) {
613
+ return {
614
+ clause: `an ${kind} plugin "${identity}" declared through onCommandAttach`,
615
+ remedy: 'Install one of them.',
616
+ };
617
+ }
618
+ /**
619
+ * The remedy a collision against the Command's own declarations, the globals table, or another
620
+ * plugin's option earns. The remedy follows the target alone, whichever kind the hook declared:
621
+ * the author renames their own local option or argument, the author renames the colliding global
622
+ * option, or, when the target is another plugin's option, the two plugins install one of them.
623
+ */
624
+ function attachedRemedy(target) {
625
+ if (target === 'local') {
626
+ return "Rename the Command's option or omit the plugin.";
627
+ }
628
+ if (target === 'argument') {
629
+ return "Rename the Command's argument or omit the plugin.";
630
+ }
631
+ if (target === 'global') {
632
+ return 'Rename the global option or omit the plugin.';
633
+ }
634
+ return 'Install one of them.';
635
+ }
636
+ /**
637
+ * What one name a hook-declared input of either kind collides with, in the order the scopes are
638
+ * reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
639
+ * plugin-owned option, or an argument the Command declares. The remedy follows the target alone,
640
+ * not which kind the hook declared.
641
+ */
642
+ function attachedCollision(input, held) {
643
+ const { name } = input;
644
+ const hook = held.hooks.get(name);
645
+ if (hook !== undefined) {
646
+ return hookClause('option', hook);
647
+ }
648
+ const hooked = held.hookArguments.get(name);
649
+ if (hooked !== undefined) {
650
+ return hookClause('argument', hooked);
651
+ }
652
+ if (held.locals.has(name)) {
653
+ return { clause: 'a local option', remedy: attachedRemedy('local') };
654
+ }
655
+ const claimed = held.globals.names.get(name);
656
+ if (claimed) {
657
+ const target = claimed.kind === 'plugin' ? 'plugin' : 'global';
658
+ const clause = claimed.kind === 'plugin' ? `an option of plugin "${claimed.identity}"` : 'a global option';
659
+ return { clause, remedy: attachedRemedy(target) };
660
+ }
661
+ return held.arguments.has(name)
662
+ ? { clause: 'an argument', remedy: attachedRemedy('argument') }
663
+ : undefined;
664
+ }
665
+ /**
666
+ * The spellings one compiled table holds, each under the form its own option is named by, which is
667
+ * its long form where it declares one and the colliding spelling itself where it declares none.
668
+ */
669
+ function readSpellings(table, claimed) {
670
+ const longs = new Map();
671
+ for (const [spelling, option] of table) {
672
+ if (option.role === 'long') {
673
+ longs.set(option.name, spelling);
674
+ }
675
+ }
676
+ for (const [spelling, option] of table) {
677
+ if (!claimed.has(spelling)) {
678
+ claimed.set(spelling, longs.get(option.name) ?? spelling);
679
+ }
680
+ }
681
+ }
682
+ /** One hook-declared option's spellings, against every spelling the table already claims. */
683
+ function checkAttachedSpelling(declared, named) {
684
+ const { claimed, subject } = named;
685
+ const table = compileOptions([declared.input], subject);
686
+ for (const [spelling] of table) {
687
+ const used = claimed.get(spelling);
688
+ if (used !== undefined) {
689
+ throw new DeclarationError(`Plugin "${declared.identity}" declares option "${declared.input.name}" with spelling "${spelling}" on ${subject}, which "${used}" already uses.`);
690
+ }
691
+ }
692
+ readSpellings(table, claimed);
693
+ }
694
+ /**
695
+ * Every input a hook declared, against the names and spellings the Command, the globals table, and
696
+ * an earlier hook already hold. It runs before the Command's own inputs compile, so a hook-declared
697
+ * input reports as the plugin's fault and never as the author's.
698
+ */
699
+ function checkAttachedInputs(declared, attached, globals) {
700
+ const subject = commandSubject(declared.name);
701
+ const hooked = new Set(attached.map((entry) => entry.input));
702
+ const authored = declared.inputs.filter((input) => !hooked.has(input));
703
+ const options = authored.filter((input) => input.kind === 'option');
704
+ const held = {
705
+ arguments: new Set(authored.filter((input) => input.kind === 'argument').map((input) => input.name)),
706
+ globals,
707
+ hookArguments: new Map(),
708
+ hooks: new Map(),
709
+ locals: new Set(options.map((input) => input.name)),
710
+ };
711
+ const claimed = new Map();
712
+ readSpellings(globals.options, claimed);
713
+ readSpellings(compileOptions(options, subject), claimed);
714
+ for (const { identity, input } of attached) {
715
+ const collision = attachedCollision(input, held);
716
+ if (collision) {
717
+ throw new DeclarationError(`Plugin "${identity}" declares ${input.kind} "${input.name}" on ${subject}, which is already declared as ${collision.clause}. ${collision.remedy}`);
718
+ }
719
+ if (input.kind === 'option') {
720
+ checkAttachedSpelling({ identity, input }, { claimed, subject });
721
+ held.hooks.set(input.name, identity);
722
+ }
723
+ else {
724
+ held.hookArguments.set(input.name, identity);
725
+ }
726
+ }
727
+ }
285
728
  /** Binds one Command's declarations to its action, so an action reads only validated values. */
286
- function bindDispatch(state, action) {
287
- return ({ host, out, passthrough, signal, values }) => {
729
+ function bindDispatch(state, action, globals) {
730
+ return ({ host, out, passthrough, signal, style, values }) => {
288
731
  const bound = state.bind(values);
732
+ // Last resort: no typed path exists.
733
+ // The graph erases the binder's generic relationship.
734
+ // It holds because attachment checks the global output requirement.
735
+ // Graph build rejects a collision between a global and a local.
736
+ // This binder returns the Application's validated globals alone.
737
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
738
+ const globalOptions = globals.bind(values);
289
739
  return action({
290
740
  args: bound.args,
291
741
  host,
292
- options: { ...bindGlobals(state.globals, values), ...bound.options },
742
+ options: { ...globalOptions, ...bound.options },
293
743
  out,
294
744
  passthrough,
295
745
  signal,
746
+ style,
296
747
  });
297
748
  };
298
749
  }
299
- /** Validates one declaration against the shared globals table and compiles it for dispatch. */
300
- export function buildCommand(state, context) {
301
- const { actions, name } = state;
302
- const { globals } = context;
303
- const subject = commandSubject(name);
304
- checkCommandOptions(name, state.options);
305
- const description = checkDescription(commandSentence(name), state.description);
306
- const hidden = checkHidden(commandSentence(name), state.hidden);
307
- const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
308
- const extensions = buildExtensions({
309
- declared: state.extensions,
310
- descriptors: context.descriptors,
311
- subject: { phrase: `on ${subject}`, sentence: commandSentence(name) },
312
- target: 'command',
313
- });
314
- // Each declaration's own facts, in authoring order, before the rules that pair declarations.
315
- for (const input of state.inputs) {
750
+ /** Each declaration's own facts and extension values, whichever scope declared it. */
751
+ function checkInputFacts(inputs, command, context) {
752
+ const { name, subject } = command;
753
+ for (const input of inputs) {
316
754
  const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
317
755
  checkDescription(sentence, input.config.description);
318
756
  if (input.kind === 'argument') {
@@ -329,26 +767,69 @@ export function buildCommand(state, context) {
329
767
  target: input.kind,
330
768
  }));
331
769
  }
332
- if (state.globals !== globals.source) {
333
- throw new DeclarationError(`${commandSentence(name)} holds a different GlobalOptions value than its Application. Share one GlobalOptions value across the declarations.`);
770
+ }
771
+ /** One Command declares arguments or attaches children, whichever declaration made each of them. */
772
+ function checkArgumentPlacement(name, slot, child) {
773
+ if (slot && child) {
774
+ 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.`);
334
775
  }
776
+ }
777
+ /** Validates one declaration against the shared globals table and compiles it for dispatch. */
778
+ export function buildCommand(state, context) {
779
+ const { actions, name } = state;
780
+ const { globals } = context;
781
+ const subject = commandSubject(name);
782
+ const layer = { phrase: `on ${subject}`, sentence: commandSentence(name) };
783
+ checkCommandOptions(name, state.options);
784
+ const description = checkDescription(commandSentence(name), state.description);
785
+ const hidden = checkHidden(commandSentence(name), state.hidden);
786
+ const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
787
+ // The author's layers validate before any hook runs on this Command, so a hook reads them.
788
+ const declaredExtensions = storeCommandLayers({
789
+ descriptors: context.descriptors,
790
+ layers: state.extensions,
791
+ subject: layer,
792
+ });
793
+ // Each declaration's own facts, in authoring order, before the rules that pair declarations.
794
+ checkInputFacts(state.inputs, { name, subject }, context);
335
795
  const attached = collectChildren(state);
336
796
  checkDeclarationOrder(state);
337
797
  const aliases = collectAliases(state);
338
- const slots = collectArguments(state, subject);
339
- const first = slots[0];
340
- const child = attached[0];
341
- if (first && child) {
342
- 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.`);
343
- }
798
+ const declaredSlots = collectArguments(state, subject);
799
+ checkArgumentPlacement(name, declaredSlots[0], attached[0]);
344
800
  if (actions.length > 1) {
345
801
  throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
346
802
  }
347
803
  const action = actions[0];
804
+ const hasAction = action !== undefined;
805
+ const declaredResult = buildResult(state, hasAction);
806
+ /**
807
+ * The hooks run once the author's declaration is complete and its result is resolved, so a hook
808
+ * reads an exact record, and every rule below reads what the hooks returned.
809
+ */
810
+ // One token per Command per build, which binds a hook's return to the value it received.
811
+ const lineage = {};
812
+ const progress = runAttachHooks({ declared: state, extensions: declaredExtensions, layer, registry: context.descriptors }, { hasAction, lineage, path: context.path }, context.plugins);
813
+ const { calls } = progress;
814
+ // The descriptors the returned value's extensions registered join the build's registry.
815
+ for (const [identity, descriptor] of progress.registry) {
816
+ context.descriptors.set(identity, descriptor);
817
+ }
818
+ const hooked = applyAttachCalls(state, calls);
819
+ const hookInputs = calls.filter((call) => call.kind === 'input');
820
+ checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
821
+ // The hook-declared names are checked first, so a collision reports in the plugin's voice.
822
+ // A rule the author's own declaration voices never speaks for a name a hook declared.
823
+ checkAttachedInputs(hooked, hookInputs, globals);
824
+ // Every value was validated once, the author's above and each hook's at its `extend()` call.
825
+ const extensions = publishStore(progress.extensions);
826
+ const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
827
+ checkArgumentPlacement(name, slots[0], attached[0]);
828
+ const result = calls.length > 0 ? buildResult(hooked, hasAction) : declaredResult;
348
829
  if (!action) {
349
- checkGroup(state, attached);
830
+ checkGroup(hooked, attached);
350
831
  }
351
- const options = compileLocalOptions(state, globals, subject);
832
+ const options = compileLocalOptions(hooked, globals, subject);
352
833
  const children = new Map();
353
834
  const routes = new Map();
354
835
  const claim = aliasNamespace(name, new Set(attached.map((entry) => entry[0])));
@@ -368,22 +849,25 @@ export function buildCommand(state, context) {
368
849
  children,
369
850
  deprecated,
370
851
  description,
371
- dispatch: action ? bindDispatch(state, action) : undefined,
852
+ dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
372
853
  extensions,
373
854
  hidden,
374
- inputs: state.inputs,
855
+ inputs: hooked.inputs,
375
856
  name,
376
857
  options,
858
+ result,
377
859
  routes,
378
860
  };
379
861
  }
380
862
  /** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
381
- export function buildGraph(root, install) {
863
+ export function buildGraph(root, globals, install) {
382
864
  const context = {
383
865
  descriptors: install.descriptors,
384
866
  extensions: install.extensions,
385
- globals: buildGlobals(root.globals, install.plugins, install),
867
+ globals: buildGlobals(globals, install.plugins, install),
386
868
  owners: new Map(),
869
+ path: [],
870
+ plugins: install.plugins,
387
871
  };
388
872
  return {
389
873
  extensions: context.extensions,
@@ -406,7 +890,7 @@ export class CommandBuilder {
406
890
  kind: 'argument',
407
891
  name,
408
892
  };
409
- return new CommandBuilder(declareArgument(this.#state, input));
893
+ return this.derive(declareArgument(this.#state, input));
410
894
  }
411
895
  option(name, config) {
412
896
  const input = {
@@ -414,41 +898,62 @@ export class CommandBuilder {
414
898
  kind: 'option',
415
899
  name,
416
900
  };
417
- return new CommandBuilder(declareOption(this.#state, input));
901
+ return this.derive(declareOption(this.#state, input));
418
902
  }
419
903
  /**
420
904
  * Aliases are other bare tokens that route to this Command. They invalidate no call, and the
421
905
  * tuple rest parameter rejects a call that names none.
422
906
  */
423
907
  alias(...names) {
424
- return new CommandBuilder(declareAlias(this.#state, names));
908
+ return this.derive(declareAlias(this.#state, names));
425
909
  }
426
910
  /** A child arrives in any type state, because its own action is the call that finished it. */
427
911
  command(child) {
428
- return new CommandBuilder(attachChild(this.#state, child));
912
+ return this.derive(attachChild(this.#state, child));
913
+ }
914
+ /**
915
+ * The value this Command produces for its consumer. The type argument is stated by the author,
916
+ * so the views record states no type of its own and an omitted argument names none either.
917
+ */
918
+ result(declaration) {
919
+ return this.derive(declareResult(this.#state, 'value', declaration));
429
920
  }
430
- /** The action is the last declaration call, so the value it returns publishes `AfterAction`. */
921
+ /** The same declaration over a sequence, whose type argument is one row. */
922
+ rows(declaration) {
923
+ return this.derive(declareResult(this.#state, 'rows', declaration));
924
+ }
925
+ /**
926
+ * Views after the fact. It merges by key, so an existing name is replaced in place and a new one
927
+ * is appended, and `default` names the key core renders when nothing selects another.
928
+ */
929
+ views(replacements, options) {
930
+ return this.derive(declareResultViews(this.#state, replacements, options));
931
+ }
932
+ /** The action closes input authoring; `extend()` remains outside this state transition. */
431
933
  action(handler) {
432
- return new CommandBuilder(declareAction(this.#state, handler));
934
+ return this.derive(declareAction(this.#state, handler));
935
+ }
936
+ extend(...values) {
937
+ return this.derive(declareExtensions(this.#state, values));
433
938
  }
434
939
  build(context) {
435
940
  return buildCommand(this.#state, context);
436
941
  }
942
+ /**
943
+ * The same runtime value in the state the calling method's return type names. Each call states
944
+ * its own transition, and the declared result travels with it unless the call replaces it.
945
+ */
946
+ derive(state) {
947
+ return new CommandBuilder(state);
948
+ }
437
949
  }
438
- /**
439
- * The runtime class behind the public constructor. It is generic so that an instance's `Globals`
440
- * is the type of the value it holds, with `{}` standing in when there is none, which is what each
441
- * signature of the constructor interface publishes.
442
- */
950
+ /** The constructor uses the Application registration; public type defaults stay library-neutral. */
443
951
  class CommandDeclaration extends CommandBuilder {
444
952
  constructor(name, options) {
445
- // The options slot is read defensively, never inspected: an invalid value still yields
446
- // `globals`, the core facts, and `extensions` of some kind, and `buildCommand` reports it.
447
953
  super(freshState({
448
954
  deprecated: options?.deprecated,
449
955
  description: options?.description,
450
956
  extensions: options?.extensions,
451
- globals: options?.globals,
452
957
  hidden: options?.hidden,
453
958
  name,
454
959
  options,
@@ -535,11 +1040,31 @@ export function routeInvocation(graph, argv) {
535
1040
  };
536
1041
  }
537
1042
  /**
538
- * The phases the middleware chain terminates in: the callable check, local parsing, validation, and
539
- * the action. It answers with the call that dispatches, so the caller records that the action was
540
- * invoked at the moment it invokes it and no earlier failure reads as a dispatch.
1043
+ * The frozen records one middleware reads: the routed Command's own inputs, and the tail. Every
1044
+ * value is a plain-data copy frozen to every depth, so a middleware that reaches into a list or a
1045
+ * schema's output object reaches its own copy and contributes nothing to what the action receives.
1046
+ * A value that is neither an array nor a plain object, a class instance or a `Date` a schema
1047
+ * produced, is shared by reference, because core cannot copy it meaningfully.
541
1048
  */
542
- export async function prepareDispatch(graph, routed, invocation) {
1049
+ function requestOf(command, values, passthrough) {
1050
+ const args = {};
1051
+ const options = {};
1052
+ for (const input of command.inputs) {
1053
+ const target = input.kind === 'argument' ? args : options;
1054
+ target[input.name] = snapshot(values.read(input));
1055
+ }
1056
+ return Object.freeze({
1057
+ args: Object.freeze(args),
1058
+ options: Object.freeze(options),
1059
+ passthrough: Object.freeze([...passthrough]),
1060
+ });
1061
+ }
1062
+ /**
1063
+ * The phases that run ahead of the chain: the callable check, local parsing, and validation. It
1064
+ * answers with the call that dispatches, so the caller records that the action was invoked at the
1065
+ * moment it invokes it and no earlier failure reads as a dispatch.
1066
+ */
1067
+ async function readyDispatch(graph, routed, invocation) {
543
1068
  const { command, path, scan } = routed;
544
1069
  const { dispatch } = command;
545
1070
  // A group answers no invocation of its own, so it fails with the routing errors above it.
@@ -554,13 +1079,52 @@ export async function prepareDispatch(graph, routed, invocation) {
554
1079
  host: invocation.host,
555
1080
  inputs: { globals: graph.globals.inputs, locals: command.inputs },
556
1081
  passthrough: parsed.passthrough,
557
- supplied: { args, options: mergeValues(scan, parsed.options) },
558
- });
559
- return () => dispatch({
560
- host: invocation.host,
561
- out: invocation.out,
562
- passthrough: parsed.passthrough,
563
1082
  signal: invocation.signal,
564
- values,
1083
+ supplied: { args, options: mergeValues(scan, parsed.options) },
565
1084
  });
1085
+ return {
1086
+ dispatch: async (view) => {
1087
+ // The channel is built at the boundary, because the view a middleware selected is read there.
1088
+ const channel = invocation.channel({ path, result: command.result, view });
1089
+ try {
1090
+ await dispatch({
1091
+ host: invocation.host,
1092
+ out: channel.out,
1093
+ passthrough: parsed.passthrough,
1094
+ signal: invocation.signal,
1095
+ style: invocation.style,
1096
+ values,
1097
+ });
1098
+ /**
1099
+ * A declared result is a promise the Command makes, so an action that returned normally
1100
+ * without emitting one broke it. A failure raised before the call is that failure, and a
1101
+ * cancelled run raises none, because an action that reads its signal and returns is the
1102
+ * sanctioned path.
1103
+ */
1104
+ if (command.result && !channel.emitted() && !invocation.signal.aborted) {
1105
+ throw new ResultError('missing', path);
1106
+ }
1107
+ }
1108
+ catch (error) {
1109
+ // The action's failure is the invocation's outcome.
1110
+ // A sequence it left pending is stopped where it stands rather than drained to its end.
1111
+ channel.stop();
1112
+ throw error;
1113
+ }
1114
+ },
1115
+ request: requestOf(command, values, parsed.passthrough),
1116
+ };
1117
+ }
1118
+ /**
1119
+ * Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
1120
+ * middleware reads the request before the action runs and a takeover never observes the fault.
1121
+ */
1122
+ export async function prepareDispatch(graph, routed, invocation) {
1123
+ const result = routed.command.result;
1124
+ try {
1125
+ return { ...(await readyDispatch(graph, routed, invocation)), kind: 'ready', result };
1126
+ }
1127
+ catch (error) {
1128
+ return { fault: error, kind: 'held', request: null, result };
1129
+ }
566
1130
  }