@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
@@ -1,25 +1,23 @@
1
- import type { AfterAction, AfterArgument, AfterCommand, Command, CommandMethod, CommandState } from './command.js';
2
- import type { FailureRenderer } from './errors.js';
1
+ import type { AfterAction, AttachmentConstraint, AfterArgument, AfterCommand, AfterResult, Command, CommandMethod, CommandState, ResultMethod } from './command.js';
2
+ import type { ApplicationEnvironment, applicationEnvironment } from './environment.js';
3
3
  import type { ExtensionValue } from './extension.js';
4
- import type { GlobalOptions } from './globals.js';
4
+ import type { GlobalsState } from './globals.js';
5
5
  import type { CommandGraph } from './inspect.js';
6
6
  import type { Plugin } from './plugin.js';
7
- import type { Action, ArgumentConfig, ArgumentValue, declaredTypes, DeclaredTypes, DefaultConstraint, GlobalNameConstraint, MultipleConstraint, NameConstraint, ExitCode, OptionConfig, OptionValue, RunOptions, ValidateOmittedConstraint } from './types.js';
7
+ import type { RenderingPolicy } from './rendering.js';
8
+ import type { Action, ArgumentConfig, ArgumentValue, declaredTypes, DeclaredTypes, DefaultConstraint, GlobalNameConstraint, MultipleConstraint, NameConstraint, ExitCode, OptionConfig, OptionValue, ResultViews, ResultViewsOf, RowViews, RunOptions, ValidateOmittedConstraint } from './types.js';
9
+ import type { ViewOverride } from './view.js';
8
10
  /**
9
11
  * Every authoring call an Application can publish, beside `run()` and `name`, which always remain.
10
12
  * An Application's type state is a subset of these, and each call removes the names it invalidates.
11
13
  * The unnamed root declares what a named Command declares, except for `alias()`: the root answers
12
14
  * to no bare token, so it has no name to alias.
13
15
  */
14
- export type ApplicationMethod = Exclude<CommandMethod, 'alias'>;
15
- /**
16
- * Everything an application configures beside its declarations: the options every Command shares,
17
- * and the renderers that answer the failure classes core throws.
18
- */
19
- export interface ApplicationOptions<Globals = {}> {
20
- globals?: GlobalOptions<Globals>;
21
- failures?: readonly FailureRenderer[];
22
- plugins?: readonly Plugin[];
16
+ export type ApplicationMethod = Exclude<CommandMethod, 'alias'> | 'globalOption';
17
+ export interface ApplicationOptions<Plugins extends readonly Plugin[] = readonly Plugin[]> {
18
+ rendering?: RenderingPolicy;
19
+ views?: readonly ViewOverride[];
20
+ plugins?: Plugins;
23
21
  extensions?: readonly ExtensionValue<'command'>[];
24
22
  description?: string;
25
23
  version?: string;
@@ -28,33 +26,63 @@ export interface ApplicationOptions<Globals = {}> {
28
26
  * The Application holds the unnamed root's declaration state and applies the same transitions a
29
27
  * Command does, so each declaration call has one typed implementation and no builder to recover.
30
28
  */
31
- declare class ApplicationBuilder<Args, Options, Globals, State extends ApplicationMethod = ApplicationMethod> {
29
+ declare class ApplicationBuilder<Args, Options, Globals, State extends ApplicationMethod = ApplicationMethod, Plugins extends readonly Plugin[] = readonly [], Result = unknown> {
32
30
  #private;
33
- readonly [declaredTypes]: DeclaredTypes<Args, Options, Globals>;
31
+ readonly [applicationEnvironment]: ApplicationEnvironment<Globals, Plugins>;
32
+ readonly [declaredTypes]: DeclaredTypes<Args, Options, Globals, Result>;
34
33
  constructor(name: string, root: CommandState<Args, Options, Globals>, config: {
35
34
  declared: DeclaredFacts;
36
- failures: readonly FailureRenderer[];
35
+ views: unknown;
37
36
  options?: unknown;
38
- plugins: readonly Plugin[];
37
+ plugins: Plugins | undefined;
38
+ globals: GlobalsState<Globals>;
39
39
  });
40
40
  get name(): string;
41
- argument<const Name extends string, const Config extends ArgumentConfig>(name: Name, config: Config & NameConstraint<Name> & NoInfer<DefaultConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args & Record<Name, ArgumentValue<Config>>, Options, Globals, AfterArgument<State>>;
42
- option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & GlobalNameConstraint<Name, Globals> & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args, Options & Record<Name, OptionValue<Config>>, Globals, State>;
43
- /** The action is the last call, so it returns `AfterAction`: only `run()` and `name` remain. */
44
- action(handler: Action<Args, Globals & Options>): Application<Args, Options, Globals>;
41
+ argument<const Name extends string, const Config extends ArgumentConfig>(name: Name, config: Config & NameConstraint<Name> & NoInfer<DefaultConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args & Record<Name, ArgumentValue<Config>>, Options, Globals, AfterArgument<State>, Plugins, Result>;
42
+ option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & GlobalNameConstraint<Name, Globals> & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args, Options & Record<Name, OptionValue<Config>>, Globals, State, Plugins, Result>;
43
+ globalOption<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & (Name extends keyof Options ? {
44
+ 'This option name is already declared as a local option': Name;
45
+ } : unknown) & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args, Options, Globals & Record<Name, OptionValue<Config>>, State, Plugins, Result>;
46
+ /** Registering the action closes input authoring; extension configuration remains available. */
47
+ action(handler: Action<Args, Globals & Options, Result>): Application<Args, Options, Globals, AfterAction, Plugins, Result>;
45
48
  /** A child arrives in any type state, because its own action is the call that finished it. */
46
- command(child: Command<unknown, unknown, Globals>): Application<Args, Options, Globals, AfterCommand<State>>;
49
+ command<const Child extends Command<unknown, unknown, Globals>>(child: Child & NoInfer<AttachmentConstraint<Globals, Child>>): Application<Args, Options, Globals, AfterCommand<Exclude<State, 'globalOption'>>, Plugins, Result>;
50
+ /**
51
+ * The value the root action produces for its consumer. The type argument is stated by the
52
+ * author, as it is on a Command.
53
+ */
54
+ result<Value>(declaration: {
55
+ views: ResultViews<NoInfer<Value>>;
56
+ }): Application<Args, Options, Globals, AfterResult<State>, Plugins, {
57
+ kind: 'value';
58
+ value: Value;
59
+ }>;
60
+ /** The same declaration over a sequence, whose type argument is one row. */
61
+ rows<Row>(declaration: {
62
+ views: RowViews<NoInfer<Row>>;
63
+ }): Application<Args, Options, Globals, AfterResult<State>, Plugins, {
64
+ kind: 'rows';
65
+ row: Row;
66
+ }>;
67
+ /** Views after the fact, merged by key, as it is on a Command. */
68
+ views(replacements: ResultViewsOf<Result>, options?: {
69
+ default?: string;
70
+ }): Application<Args, Options, Globals, State, Plugins, Result>;
71
+ extend(...values: readonly ExtensionValue<'command'>[]): Application<Args, Options, Globals, State, Plugins, Result>;
47
72
  /**
48
- * One wrapper for every declaration call, so the Application keeps its name. The next state
73
+ * Root declaration calls preserve the Application configuration. The next state
49
74
  * travels through this call: each method names its transition in its return type, and the
50
75
  * wrapper publishes the same runtime value in exactly that state.
51
76
  */
52
77
  private derive;
53
78
  /**
54
- * Every rule that reads the declarations alone, in the order `run()` reads them: the options
55
- * slot, the installed list, the application's failure registrations, each plugin's declarations,
56
- * then the whole Command graph. The registry is published as soon as it is known, so a later
57
- * declaration error still reaches the renderers the application registered for it.
79
+ * Every rule that reads the declarations alone, in the order `run()` reads them: the
80
+ * application's own view overrides, the options slot, the installed list, each plugin's
81
+ * declarations, then the whole Command graph. The application's overrides are read and published
82
+ * first, because they need core's identities and nothing else, so every later declaration error
83
+ * reaches them, while a fault in that list itself reports through core's own text. The merged
84
+ * registry is published once the whole build has succeeded, so a build-time fault never resolves
85
+ * through a plugin's overrides, which build has not yet validated.
58
86
  */
59
87
  private prepare;
60
88
  /**
@@ -74,19 +102,16 @@ declare class ApplicationBuilder<Args, Options, Globals, State extends Applicati
74
102
  * It defaults to the state after `action()`, which publishes the fewest calls, so
75
103
  * `Application<A, O, G>` accepts an application in any state, a finished one included.
76
104
  */
77
- export type Application<Args = {}, Options = {}, Globals = {}, State extends ApplicationMethod = AfterAction> = Pick<ApplicationBuilder<Args, Options, Globals, State>, typeof declaredTypes | 'inspect' | 'name' | 'run' | State>;
105
+ export type Application<Args = {}, Options = {}, Globals = {}, State extends ApplicationMethod = AfterAction, Plugins extends readonly Plugin[] = readonly Plugin[], Result = unknown> = Pick<ApplicationBuilder<Args, Options, Globals, State, Plugins, Result>, typeof applicationEnvironment | typeof declaredTypes | 'extend' | 'inspect' | 'name' | 'run' | State | ResultMethod<Result>>;
78
106
  interface ApplicationConstructor {
79
- new (name: string): Application<{}, {}, {}, ApplicationMethod>;
80
- new <Globals = {}>(name: string, options: ApplicationOptions<Globals>): Application<{}, {}, Globals, ApplicationMethod>;
107
+ new (name: string): Application<{}, {}, {}, ApplicationMethod, readonly []>;
108
+ new <const Plugins extends readonly Plugin[] = readonly []>(name: string, options: ApplicationOptions<Plugins>): Application<{}, {}, {}, ApplicationMethod, Plugins>;
81
109
  }
82
110
  /** The same facts as the constructor captured them, before any rule has read them. */
83
111
  interface DeclaredFacts {
112
+ rendering: unknown;
84
113
  description: unknown;
85
114
  version: unknown;
86
115
  }
87
- /**
88
- * The public constructor takes a name and one options object. The globals type narrows to the
89
- * supplied value, and the failure renderers configure the application the way its commands do.
90
- */
91
116
  export declare const Application: ApplicationConstructor;
92
117
  export {};
@@ -1,16 +1,19 @@
1
1
  import { runInvocation } from './chain.js';
2
- import { attachChild, buildGraph, collectInputs, declareAction, declareArgument, declareOption, freshState, } from './command.js';
3
- import { buildFailures, DeclarationError, describeFailure, InternalError, mergeFailures, reasonOf, toFailure, } from './errors.js';
2
+ import { attachChild, buildGraph, collectInputs, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, recordGlobalOption, } from './command.js';
3
+ import { DeclarationError, InternalError, reasonOf, toFailure } from './errors.js';
4
4
  import { checkDescription, checkNoListingFacts, checkVersion, isPlainObject } from './facts.js';
5
- import { isGlobalOptions } from './globals.js';
5
+ import { declareGlobalOption, emptyGlobals } from './globals.js';
6
6
  import { captureHost } from './host.js';
7
7
  import { inspectGraph } from './inspect.js';
8
+ import { coreViews } from './lanes.js';
8
9
  import { Output, reportPlainly } from './output.js';
9
10
  import { buildPlugins, installPlugins, ownedSignals, pluginSentence } from './plugin.js';
11
+ import { renderingPolicy } from './rendering.js';
10
12
  import { bracketRun, cancellationCode, isCancellationEcho } from './signals.js';
11
13
  import { captureConfig, checkDeclarations, prepareInputs } from './validation.js';
14
+ import { buildViews, describeFailure, viewIdentities } from './view.js';
12
15
  /** The registry a failure is reported through when the application's own could not be built. */
13
- const noRegistrations = new Map();
16
+ const noViews = [];
14
17
  /**
15
18
  * Whether one failure is the cancellation the run already reports, which core does not report a
16
19
  * second time. Any other failure after cancellation is rendered as usual.
@@ -18,6 +21,29 @@ const noRegistrations = new Map();
18
21
  function silenced(thrown, signal, cancelled) {
19
22
  return cancelled !== undefined && isCancellationEcho(thrown, signal.reason);
20
23
  }
24
+ /**
25
+ * The stand-in for "this run has no primary failure", which is a value no thrown value can be.
26
+ * `undefined` is itself throwable, so the absence is spelled here rather than borrowed from it.
27
+ */
28
+ const noPrimary = Symbol('no primary');
29
+ /**
30
+ * Whether the primary outcome carries one recorded cause already: the value itself, or a failure
31
+ * that wraps it at any depth, which an action that caught a source failure and rethrew its own
32
+ * produces. Such a cause is reported once, through the primary outcome that carries it.
33
+ */
34
+ function carried(primary, cause) {
35
+ const seen = new Set();
36
+ let value = primary;
37
+ while (value !== noPrimary && !seen.has(value)) {
38
+ if (value === cause) {
39
+ return true;
40
+ }
41
+ seen.add(value);
42
+ // A failure wraps its own cause under `cause`, and one that declares none ends the walk.
43
+ value = value instanceof Error && 'cause' in value ? value.cause : noPrimary;
44
+ }
45
+ return false;
46
+ }
21
47
  /**
22
48
  * The caller's own signal, read where it enters. A JavaScript caller reaches the slot with any
23
49
  * value, and a value that is not an `AbortSignal` would otherwise escape as a raw TypeError.
@@ -38,8 +64,10 @@ function checkSignal(signal) {
38
64
  class ApplicationBuilder {
39
65
  #name;
40
66
  #root;
41
- #failures;
67
+ // The override list the constructor read out of the options slot, unexamined until build.
68
+ #views;
42
69
  #plugins;
70
+ #globals;
43
71
  // The constructor's raw options argument, kept for the slot's own shape rules.
44
72
  // The options-slot rules answer at the same point every other authoring fault does:
45
73
  // `inspect()` and `run()`.
@@ -48,10 +76,11 @@ class ApplicationBuilder {
48
76
  #declared;
49
77
  constructor(name, root, config) {
50
78
  this.#declared = config.declared;
51
- this.#failures = config.failures;
79
+ this.#views = config.views;
52
80
  this.#name = name;
53
81
  this.#options = config.options;
54
82
  this.#plugins = config.plugins;
83
+ this.#globals = config.globals;
55
84
  this.#root = root;
56
85
  }
57
86
  get name() {
@@ -73,7 +102,21 @@ class ApplicationBuilder {
73
102
  };
74
103
  return this.derive(declareOption(this.#root, input));
75
104
  }
76
- /** The action is the last call, so it returns `AfterAction`: only `run()` and `name` remain. */
105
+ globalOption(name, config) {
106
+ const input = {
107
+ config: captureConfig(config),
108
+ kind: 'option',
109
+ name,
110
+ };
111
+ return new ApplicationBuilder(this.#name, recordGlobalOption(this.#root, name), {
112
+ declared: this.#declared,
113
+ globals: declareGlobalOption(this.#globals, input),
114
+ options: this.#options,
115
+ plugins: this.#plugins,
116
+ views: this.#views,
117
+ });
118
+ }
119
+ /** Registering the action closes input authoring; extension configuration remains available. */
77
120
  action(handler) {
78
121
  return this.derive(declareAction(this.#root, handler));
79
122
  }
@@ -82,37 +125,60 @@ class ApplicationBuilder {
82
125
  return this.derive(attachChild(this.#root, child));
83
126
  }
84
127
  /**
85
- * One wrapper for every declaration call, so the Application keeps its name. The next state
128
+ * The value the root action produces for its consumer. The type argument is stated by the
129
+ * author, as it is on a Command.
130
+ */
131
+ result(declaration) {
132
+ return this.derive(declareResult(this.#root, 'value', declaration));
133
+ }
134
+ /** The same declaration over a sequence, whose type argument is one row. */
135
+ rows(declaration) {
136
+ return this.derive(declareResult(this.#root, 'rows', declaration));
137
+ }
138
+ /** Views after the fact, merged by key, as it is on a Command. */
139
+ views(replacements, options) {
140
+ return this.derive(declareResultViews(this.#root, replacements, options));
141
+ }
142
+ extend(...values) {
143
+ return this.derive(declareExtensions(this.#root, values));
144
+ }
145
+ /**
146
+ * Root declaration calls preserve the Application configuration. The next state
86
147
  * travels through this call: each method names its transition in its return type, and the
87
148
  * wrapper publishes the same runtime value in exactly that state.
88
149
  */
89
150
  derive(root) {
90
151
  return new ApplicationBuilder(this.#name, root, {
91
152
  declared: this.#declared,
92
- failures: this.#failures,
153
+ globals: this.#globals,
93
154
  options: this.#options,
94
155
  plugins: this.#plugins,
156
+ views: this.#views,
95
157
  });
96
158
  }
97
159
  /**
98
- * Every rule that reads the declarations alone, in the order `run()` reads them: the options
99
- * slot, the installed list, the application's failure registrations, each plugin's declarations,
100
- * then the whole Command graph. The registry is published as soon as it is known, so a later
101
- * declaration error still reaches the renderers the application registered for it.
160
+ * Every rule that reads the declarations alone, in the order `run()` reads them: the
161
+ * application's own view overrides, the options slot, the installed list, each plugin's
162
+ * declarations, then the whole Command graph. The application's overrides are read and published
163
+ * first, because they need core's identities and nothing else, so every later declaration error
164
+ * reaches them, while a fault in that list itself reports through core's own text. The merged
165
+ * registry is published once the whole build has succeeded, so a build-time fault never resolves
166
+ * through a plugin's overrides, which build has not yet validated.
102
167
  */
103
- prepare(register) {
168
+ prepare(stage) {
169
+ const identities = viewIdentities(coreViews);
170
+ const application = buildViews({ declares: false, sentence: 'The Application' }, this.#views, identities);
171
+ stage.views([application]);
172
+ stage.rendering(renderingPolicy(this.#declared.rendering));
104
173
  const facts = checkOptions(this.#options, this.#declared);
105
- const installed = installPlugins(this.#plugins);
106
- const application = buildFailures(this.#failures);
107
- register(application);
174
+ const installed = installPlugins(this.#plugins ?? []);
108
175
  const install = { descriptors: new Map(), extensions: new Map() };
109
176
  const plugins = buildPlugins(installed, install);
110
- register(mergeFailures([
111
- application,
112
- ...plugins.map((entry) => buildFailures(entry.failures, pluginSentence(entry.identity))),
113
- ]));
114
- const graph = buildGraph(this.#root, { ...install, plugins });
177
+ stage.plugins(plugins);
178
+ const contributors = plugins.map((entry) => buildViews({ declares: true, sentence: pluginSentence(entry.identity) }, entry.views, identities));
179
+ const graph = buildGraph(this.#root, this.#globals, { ...install, plugins });
115
180
  checkDeclarations([...graph.globals.inputs, ...collectInputs(graph.root)]);
181
+ stage.views([application, ...contributors]);
116
182
  return { facts, graph, plugins };
117
183
  }
118
184
  /**
@@ -122,7 +188,11 @@ class ApplicationBuilder {
122
188
  * Nothing is cached: each call builds the graph anew.
123
189
  */
124
190
  inspect() {
125
- const built = this.prepare(() => undefined);
191
+ const built = this.prepare({
192
+ plugins: () => undefined,
193
+ rendering: () => undefined,
194
+ views: () => undefined,
195
+ });
126
196
  return inspectGraph(this.#name, built.graph, built.facts);
127
197
  }
128
198
  async run(options) {
@@ -134,6 +204,8 @@ class ApplicationBuilder {
134
204
  let registry = undefined;
135
205
  // Faults a plugin raised beside the primary outcome, reported after it and never before it.
136
206
  const faults = [];
207
+ // The failure this run reports as its primary outcome, so nothing reports it a second time.
208
+ let primary = noPrimary;
137
209
  // One private controller per run, subscribed to the caller's signal at run entry.
138
210
  const controller = new AbortController();
139
211
  /**
@@ -157,10 +229,24 @@ class ApplicationBuilder {
157
229
  const overrides = options?.host;
158
230
  stderr = overrides?.stderr ?? stderr;
159
231
  const host = captureHost(overrides, stderr);
160
- output = new Output(host);
232
+ const invocationOutput = new Output(host, controller.signal);
233
+ output = invocationOutput;
234
+ // The policy is read inside the build, after the application's own overrides are published.
235
+ // A faulty rendering declaration then reports through the view the application listed.
236
+ let policy = {};
161
237
  signals = bracketRun(controller, checkSignal(options?.signal));
162
- const built = this.prepare((value) => {
163
- registry = value;
238
+ const built = this.prepare({
239
+ plugins: (plugins) => {
240
+ invocationOutput.configure(policy, plugins.find((entry) => entry.theme !== undefined)?.theme ?? new Map());
241
+ },
242
+ rendering: (declared) => {
243
+ policy = { ...declared, ...renderingPolicy(options?.rendering) };
244
+ invocationOutput.configure(policy, new Map());
245
+ },
246
+ views: (value) => {
247
+ registry = value;
248
+ invocationOutput.useViews(value);
249
+ },
164
250
  });
165
251
  const { graph } = built;
166
252
  const inputs = { globals: graph.globals.inputs, locals: collectInputs(graph.root) };
@@ -173,6 +259,7 @@ class ApplicationBuilder {
173
259
  */
174
260
  signals.install(ownedSignals(built.plugins));
175
261
  await runInvocation({
262
+ channel: (binding) => invocationOutput.channel(binding),
176
263
  defaults,
177
264
  facts: built.facts,
178
265
  graph,
@@ -181,7 +268,11 @@ class ApplicationBuilder {
181
268
  out: output.out,
182
269
  plugins: built.plugins,
183
270
  report: (fault) => faults.push(fault),
271
+ route: (path) => {
272
+ invocationOutput.useRoute(path);
273
+ },
184
274
  signal: controller.signal,
275
+ style: output.style,
185
276
  });
186
277
  }
187
278
  // The fault check covers the same window the write accounting covers.
@@ -189,20 +280,21 @@ class ApplicationBuilder {
189
280
  await output.settle();
190
281
  const fault = output.fault;
191
282
  if (fault) {
192
- // The action returned, so the renderer failure is this invocation's own failure.
283
+ // The action returned, so the view failure is this invocation's own failure.
193
284
  throw new InternalError(`Rendering output failed: ${reasonOf(fault.cause)}`, fault.cause);
194
285
  }
195
286
  }
196
287
  catch (error) {
288
+ primary = error;
197
289
  try {
198
290
  const failure = toFailure(error);
199
291
  code = failure.exitCode;
200
- output ??= new Output({ stderr, stdout: process.stdout });
292
+ output ??= new Output(captureHost(undefined, stderr), controller.signal);
201
293
  const writes = await output.settle();
202
294
  if (writes.kind === 'ok' && !silenced(error, controller.signal, cancellation())) {
203
- const report = describeFailure(registry ?? noRegistrations, failure);
295
+ const report = describeFailure(registry ?? noViews, failure, output.context('stderr'));
204
296
  if (report.kind === 'rendered') {
205
- // The renderer already owns every byte, trailing newline included: pass it through.
297
+ // The view owns the trailing newline; output resolves its marked text.
206
298
  await output.report(report.text);
207
299
  }
208
300
  else {
@@ -217,14 +309,24 @@ class ApplicationBuilder {
217
309
  reportingFailed = true;
218
310
  }
219
311
  }
312
+ /**
313
+ * A sequence that stopped on its own source reports the same way: the call the action never
314
+ * awaited observed nothing, and a failure the action let propagate is the primary outcome
315
+ * already, so the one it raised is not reported twice.
316
+ */
317
+ for (const cause of output?.stopped ?? []) {
318
+ if (!carried(primary, cause)) {
319
+ faults.push(toFailure(cause));
320
+ }
321
+ }
220
322
  // A plugin's own fault is reported after the primary outcome and turns a would-be 0 into 1.
221
- // The primary outcome keeps its code, the way a renderer failure leaves it alone.
222
- // It is reported the way the primary failure is, so a registered renderer answers its class.
323
+ // The primary outcome keeps its code, the way a view failure leaves it alone.
324
+ // It is reported the way the primary failure is, so an override answers its class.
223
325
  for (const fault of faults) {
224
326
  if (!silenced(fault, controller.signal, cancellation())) {
225
327
  code = code === 0 ? 1 : code;
226
328
  try {
227
- const report = describeFailure(registry ?? noRegistrations, fault);
329
+ const report = describeFailure(registry ?? noViews, fault, output?.context('stderr'));
228
330
  if (report.kind === 'rendered') {
229
331
  await output?.report(report.text);
230
332
  }
@@ -251,7 +353,7 @@ class ApplicationBuilder {
251
353
  }
252
354
  /**
253
355
  * One rule orders every code: a cancelled run resolves its signal's code, and a broken
254
- * failure renderer or destination in that run is reported as text without changing it. The
356
+ * failure view or destination in that run is reported as text without changing it. The
255
357
  * signal decides the code whatever the action did afterward, so this reading comes last.
256
358
  */
257
359
  code = cancellation() ?? code;
@@ -263,19 +365,17 @@ class ApplicationBuilder {
263
365
  }
264
366
  }
265
367
  }
266
- /**
267
- * The second argument, read where it is supplied. The retired positional form declares its globals
268
- * on a value that holds no `globals` key, so without this rule the globals vanish silently and the
269
- * operator, not the author, meets the consequence as an unknown-option error. The slot's own shape
270
- * settles first, because a slot that is not an options object carries no facts to report.
271
- */
368
+ /** Reject obsolete wiring before silently losing options that invocations depend on. */
272
369
  function checkOptions(options, declared) {
273
- if (isGlobalOptions(options)) {
274
- throw new DeclarationError('The Application takes an options object. Supply { globals } instead of a positional GlobalOptions value.');
275
- }
276
370
  if (options !== undefined) {
277
371
  if (!isPlainObject(options)) {
278
- throw new DeclarationError('The Application options must be an object. Supply { globals, failures }.');
372
+ throw new DeclarationError('The Application options must be an object. Supply an Application options object.');
373
+ }
374
+ if ('globals' in options) {
375
+ throw new DeclarationError('The Application options contain globals. Declare them with globalOption(name, config).');
376
+ }
377
+ if ('failures' in options) {
378
+ throw new DeclarationError('The Application options contain failures. Declare view overrides under views with override(key, view).');
279
379
  }
280
380
  // The root is every page's entry point, so it carries neither listing fact.
281
381
  // A key that may not be there is a fault of the slot, so it answers with the slot's shape.
@@ -286,35 +386,38 @@ function checkOptions(options, declared) {
286
386
  version: checkVersion(declared.version),
287
387
  };
288
388
  }
289
- /**
290
- * The runtime class behind the public constructor. It is generic so that an instance's `Globals`
291
- * is the type of the value it holds, with `{}` standing in when there is none, which is what each
292
- * signature of the constructor interface publishes.
293
- */
389
+ /** Constructor inference preserves the installed plugin tuple; globals start empty. */
294
390
  class ApplicationDeclaration extends ApplicationBuilder {
295
391
  constructor(name, options) {
296
- // The options slot is read defensively, never inspected: an invalid value still yields
297
- // `globals`, `failures`, and the facts of some kind, and `checkOptions` reports it at build.
392
+ // The options slot is read defensively, never inspected.
393
+ // An invalid value still yields `views` and the facts of some kind.
394
+ // `checkOptions` reports such a value at build.
298
395
  // The root's own slot and core facts stay empty, because the Application checks its own slot.
299
396
  // Its diagnostics name the Application rather than the root Command.
300
397
  super(name, freshState({
301
398
  deprecated: undefined,
302
399
  description: undefined,
303
400
  extensions: options?.extensions,
304
- globals: options?.globals,
305
401
  hidden: undefined,
306
402
  name: null,
307
403
  options: undefined,
308
404
  }), {
309
- declared: { description: options?.description, version: options?.version },
310
- failures: options?.failures ?? [],
405
+ declared: {
406
+ description: options?.description,
407
+ rendering: isPlainObject(options?.rendering)
408
+ ? { ...options.rendering }
409
+ : options?.rendering,
410
+ version: options?.version,
411
+ },
412
+ globals: emptyGlobals(),
311
413
  options,
312
- plugins: options?.plugins ?? [],
414
+ plugins: options?.plugins,
415
+ // The read is loose because the slot is reachable from JavaScript with any value at all.
416
+ // An Application value passed here answers `views` with its own authoring method.
417
+ // The public `ApplicationOptions.views` stays exactly `readonly ViewOverride[]`.
418
+ // An options slot that is no plain object carries no override list, and its own rule reports it.
419
+ views: isPlainObject(options) ? options.views : undefined,
313
420
  });
314
421
  }
315
422
  }
316
- /**
317
- * The public constructor takes a name and one options object. The globals type narrows to the
318
- * supplied value, and the failure renderers configure the application the way its commands do.
319
- */
320
423
  export const Application = ApplicationDeclaration;
package/dist/chain.d.ts CHANGED
@@ -2,7 +2,8 @@ import type { BuiltGraph } from './command.js';
2
2
  import type { LoomError } from './errors.js';
3
3
  import type { CommandGraph, CommandNode } from './inspect.js';
4
4
  import type { BuiltPlugin, PluginOptions, PluginOptionValues } from './plugin.js';
5
- import type { Host, Out } from './types.js';
5
+ import type { ContextualStyle } from './style.js';
6
+ import type { ActionChannel, Host, OpenResult, Out, Request, ResultBinding } from './types.js';
6
7
  import type { DefaultValues } from './validation.js';
7
8
  /**
8
9
  * What the rest of one chain did: the action ran, a later middleware took over by returning without
@@ -12,12 +13,19 @@ type ChainOutcome = 'cancelled' | 'dispatched' | 'taken-over';
12
13
  /**
13
14
  * What one middleware receives. `graph` is the frozen graph `inspect()` returns, built once for the
14
15
  * run, and `command` is the routed node inside it. `options` holds this plugin's own option values
15
- * and never another plugin's or the application's globals.
16
+ * and never another plugin's or the application's globals. `request` is the routed Command's
17
+ * invocation, parsed and validated ahead of the chain, and `null` while core holds a fault and on a
18
+ * group. `view` names the view the result renders through: it reads as the declaration's default
19
+ * until a middleware assigns one, and as `null` on a Command that declares none. The last
20
+ * assignment before the dispatch boundary wins, and one made after it changes nothing.
16
21
  */
17
22
  interface MiddlewareContext<Options extends PluginOptions = PluginOptions> {
18
23
  readonly options: PluginOptionValues<Options>;
19
24
  readonly graph: CommandGraph;
20
25
  readonly command: CommandNode;
26
+ readonly request: Request | null;
27
+ get view(): string | null;
28
+ set view(name: string);
21
29
  readonly host: Host;
22
30
  readonly out: Out;
23
31
  readonly signal: AbortSignal;
@@ -25,6 +33,9 @@ interface MiddlewareContext<Options extends PluginOptions = PluginOptions> {
25
33
  }
26
34
  /** Everything one invocation needs after its graph is built and its defaults are validated. */
27
35
  interface Invocation {
36
+ style: ContextualStyle;
37
+ /** The action's own channel, built from the routed Command's declaration when it dispatches. */
38
+ channel: (binding: ResultBinding) => ActionChannel;
28
39
  defaults: DefaultValues;
29
40
  facts: {
30
41
  description: string | undefined;
@@ -33,16 +44,24 @@ interface Invocation {
33
44
  graph: BuiltGraph;
34
45
  host: Host;
35
46
  name: string;
36
- out: Out;
47
+ /**
48
+ * The invocation's own channel. A middleware reads it as the neutral `Out`, and the action
49
+ * receives the channel the results lane builds for the Command that was routed.
50
+ */
51
+ out: Out<OpenResult>;
37
52
  plugins: readonly BuiltPlugin[];
38
53
  /** A fault reported after the primary outcome, which turns a would-be 0 into 1. */
39
54
  report: (fault: LoomError) => void;
55
+ /** The routed path, published where routing resolved it, which output names in its own line. */
56
+ route: (path: readonly string[]) => void;
40
57
  signal: AbortSignal;
41
58
  }
42
59
  /**
43
- * Runs one invocation: the global pre-scan, routing, the middleware chain, and the phases the chain
44
- * terminates in. A middleware that returns without calling `next()` has taken over, so the
45
- * remaining tokens are never parsed and nothing later in the chain runs.
60
+ * Runs one invocation: the global pre-scan, routing, the dispatch this invocation prepares, and the
61
+ * middleware chain it then runs. Local parsing and validation run ahead of the chain so that a
62
+ * middleware reads the request, and the fault they find is held until the dispatch boundary. A
63
+ * middleware that returns without calling `next()` has taken over, so the held fault is never
64
+ * raised and nothing later in the chain runs.
46
65
  */
47
66
  declare function runInvocation(invocation: Invocation): Promise<void>;
48
67
  export type { ChainOutcome, Invocation, MiddlewareContext };