@loomcli/core 0.3.0 → 0.5.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.
package/dist/command.js CHANGED
@@ -1,22 +1,47 @@
1
- var _a;
2
- import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
3
- import { buildCommandExtensions, buildExtensions } from './extension.js';
1
+ var _a, _b;
2
+ import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
3
+ import { commandSentence, commandSubject, DeclarationError, InternalError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
4
+ import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
4
5
  import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
5
- import { buildGlobals, keyCollision, spellingCollision } from './globals.js';
6
- import { resultNode, snapshot } from './inspect.js';
7
- import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
8
- import { captureConfig, validateValues } from './validation.js';
9
- /** Authored values register here, so the public type publishes no state to reach or replace. */
6
+ import { buildGlobals, checkLocalOptions } from './globals.js';
7
+ import { nodeAt, resultNode, snapshot } from './inspect.js';
8
+ import { compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, } from './options.js';
9
+ import { fillInputs } from './sources.js';
10
+ import { captureConfig, checkDeclarations, validateValues } from './validation.js';
11
+ /**
12
+ * The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
13
+ * A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
14
+ */
15
+ const nestingCap = { depth: 2, words: 'two' };
16
+ /**
17
+ * The shallowest level a parent can sit at: the root is level 0, and a named Command is attached
18
+ * somewhere below it, so it sits at level 1 or deeper.
19
+ */
20
+ function parentLevel(name) {
21
+ return name === null ? 0 : 1;
22
+ }
23
+ /**
24
+ * Each authored value registers its handle here, so the public value holds no state to reach or
25
+ * replace.
26
+ */
10
27
  const nodes = new WeakMap();
11
- /** Reads the declarations behind an attached value; anything else is a declaration error. */
12
- function nodeOf(parent, child) {
13
- const node = nodes.get(child);
14
- if (!node) {
15
- throw new DeclarationError(`${commandSentence(parent)} attaches a value that is not a Command. Attach the value returned by new Command(name).`);
16
- }
17
- return node;
28
+ /**
29
+ * The handle one Command value registers, closed over its state. The value itself carries no
30
+ * member that reaches the state, so an attached Command stays final.
31
+ */
32
+ function nodeHandle(name, state) {
33
+ return Object.freeze({
34
+ build: (context) => buildCommand(state, context),
35
+ declared: state,
36
+ hasAction: state.action !== undefined,
37
+ name,
38
+ });
39
+ }
40
+ /** The handle behind a value an author built as a Command, or nothing for any other value. */
41
+ export function commandNode(value) {
42
+ return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
18
43
  }
19
- /** Reject retired globals wiring at graph build, before any invocation reads the options. */
44
+ /** A named Command's options slot holds a plain options object, and never retired globals wiring. */
20
45
  function checkCommandOptions(name, options) {
21
46
  if (options !== undefined && !isPlainObject(options)) {
22
47
  throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply a Command options object.`);
@@ -25,93 +50,240 @@ function checkCommandOptions(name, options) {
25
50
  throw new DeclarationError(`${commandSentence(name)} declares globals. Declare globals on the Application and register its environment.`);
26
51
  }
27
52
  }
28
- /** One name rule for every declared name in the graph, so a child and an argument read alike. */
53
+ /** One name rule for an argument, option, or view name: a bare token the parser can read. */
29
54
  function isDeclaredName(name) {
30
55
  return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
31
56
  }
32
- function checkChildName(parent, name) {
33
- if (!isDeclaredName(name)) {
34
- throw new DeclarationError(`${commandSentence(parent)} attaches a child named "${String(name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
35
- }
57
+ /**
58
+ * The portable name rule, for every name an operator types as a command at a shell prompt: the
59
+ * application name, every Command name, and every alias. The characters are the POSIX portable
60
+ * filename set, and a name starts with neither `-`, which reads as an option, nor `.`, which a
61
+ * shell hides.
62
+ */
63
+ export function isPortableName(name) {
64
+ return typeof name === 'string' && /^[A-Za-z0-9_][A-Za-z0-9._-]*$/u.test(name);
36
65
  }
37
- /** An alias is a bare token the way a child name is, so it answers to the same name rule. */
66
+ /** The one correction every portable name diagnostic ends with. */
67
+ export const portableNameCorrection = 'Use a nonempty name of A-Z, a-z, 0-9, ".", "_", and "-" that does not start with "-" or ".".';
68
+ /** An alias is typed at the prompt the way a Command name is, so it answers to the portable rule. */
38
69
  function checkAliasName(command, alias) {
39
- if (!isDeclaredName(alias)) {
40
- throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
70
+ if (!isPortableName(alias)) {
71
+ throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". ${portableNameCorrection}`);
41
72
  }
42
73
  }
74
+ /** How the extension diagnostics of one Command's own layers name it. */
75
+ export function layerOf(name) {
76
+ return { phrase: `on ${commandSubject(name)}`, sentence: commandSentence(name) };
77
+ }
43
78
  /**
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.
79
+ * The state every declaration starts from. The unnamed root and each named Command share it. The
80
+ * declaration values arrive checked, because the constructor that read them threw for any fault.
47
81
  */
48
82
  export function freshState(declaration) {
49
83
  return {
50
- actions: [],
84
+ action: undefined,
51
85
  aliases: [],
52
86
  bind: () => ({ args: {}, options: {} }),
53
87
  children: [],
54
- deprecated: declaration.deprecated,
55
- description: declaration.description,
56
- extensions: [declaration.extensions],
57
- hidden: declaration.hidden,
88
+ descriptors: declaration.descriptors,
89
+ extensions: declaration.extensions,
90
+ facts: declaration.facts,
58
91
  inputs: [],
59
- late: [],
60
92
  name: declaration.name,
61
- options: declaration.options,
93
+ records: new Map(),
62
94
  results: [],
63
95
  };
64
96
  }
65
- /** A declaration after the action is an order fault; build reports the first one recorded. */
66
- function recordLate(state, declarations) {
67
- return state.actions.length > 0 ? [...state.late, ...declarations] : state.late;
97
+ /**
98
+ * A named Command's own declaration, checked before the value exists: the name, the options slot,
99
+ * the core facts, and the extension values the slot carries. A later change to the options object
100
+ * the author passed changes nothing the declaration holds.
101
+ */
102
+ function namedState(name, options) {
103
+ if (!isPortableName(name)) {
104
+ throw new DeclarationError(`Command name "${String(name)}" is invalid. ${portableNameCorrection}`);
105
+ }
106
+ checkCommandOptions(name, options);
107
+ const slot = isPlainObject(options) ? options : undefined;
108
+ const sentence = commandSentence(name);
109
+ // The facts are read in the order their diagnostics have always ranked.
110
+ const description = checkDescription(sentence, slot?.description);
111
+ const hidden = checkHidden(sentence, slot?.hidden);
112
+ const deprecated = checkDeprecated(sentence, slot?.deprecated);
113
+ const descriptors = new Map();
114
+ const extensions = storeCommandLayers({
115
+ descriptors,
116
+ layers: [slot?.extensions],
117
+ subject: layerOf(name),
118
+ });
119
+ return {
120
+ name,
121
+ state: freshState({
122
+ descriptors,
123
+ extensions,
124
+ facts: { deprecated, description, hidden },
125
+ name,
126
+ }),
127
+ };
128
+ }
129
+ /**
130
+ * A declaration call after `action()` is an order fault the receiver's own earlier call makes
131
+ * certain. The types remove the call for a TypeScript author; a JavaScript author meets it here.
132
+ */
133
+ function checkOpen(state, clause, remedy) {
134
+ if (state.action !== undefined) {
135
+ throw new DeclarationError(`${commandSentence(state.name)} ${clause} after its action. ${remedy}`);
136
+ }
137
+ }
138
+ /** The remedy a late argument or option earns. */
139
+ const inputRemedy = 'Declare arguments and options before action().';
140
+ /** One Command declares arguments or attaches children, whichever call came second. */
141
+ function placementFault(name, argument, child) {
142
+ return new DeclarationError(`${commandSentence(name)} declares argument "${argument}" and attaches child "${child}". Move the argument into a child Command or remove the children.`);
143
+ }
144
+ /** The options one declaration list holds, in declaration order. */
145
+ function optionsOf(inputs) {
146
+ return inputs.filter((input) => input.kind === 'option');
147
+ }
148
+ /** The positional slot one argument fills. */
149
+ function slotOf(input) {
150
+ return {
151
+ input,
152
+ required: input.config.required === true,
153
+ variadic: input.config.variadic === true,
154
+ };
155
+ }
156
+ /**
157
+ * One input's extension values, validated at the call that declares it, and the descriptors they
158
+ * name joined to the declaration's own.
159
+ */
160
+ function recordInput(state, input) {
161
+ const { name } = state;
162
+ const descriptors = new Map(state.descriptors);
163
+ const record = buildExtensions({
164
+ declared: input.config.extensions,
165
+ descriptors,
166
+ subject: {
167
+ phrase: `on ${commandSubject(name)} ${input.kind} "${input.name}"`,
168
+ sentence: `${commandSentence(name)} ${input.kind} "${input.name}"`,
169
+ },
170
+ target: input.kind,
171
+ });
172
+ return { descriptors, records: new Map([...state.records, [input, record]]) };
173
+ }
174
+ /**
175
+ * Every rule one argument answers at its own call: its name, a repeated name, children beside it,
176
+ * its place after the arguments before it, its own facts, and its declaration rules.
177
+ */
178
+ function checkArgument(state, input) {
179
+ const { name } = state;
180
+ const subject = commandSubject(name);
181
+ if (!isDeclaredName(input.name)) {
182
+ throw new DeclarationError(`${commandSentence(name)} declares an argument named "${String(input.name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
183
+ }
184
+ const declared = state.inputs.filter((entry) => entry.kind === 'argument');
185
+ if (declared.some((entry) => entry.name === input.name)) {
186
+ throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
187
+ }
188
+ const child = state.children[0];
189
+ if (child) {
190
+ throw placementFault(name, input.name, child.name);
191
+ }
192
+ const previous = declared.at(-1);
193
+ if (previous) {
194
+ checkSlotOrder(slotOf(previous), slotOf(input), subject);
195
+ }
196
+ const sentence = `${commandSentence(name)} argument "${input.name}"`;
197
+ checkDescription(sentence, input.config.description);
198
+ checkNoListingFacts(sentence, input.config);
199
+ checkNoArgumentBinding(sentence, input.config);
200
+ checkDeclarations([input]);
68
201
  }
69
202
  /** The declared value joins `args` under its literal name, typed by its own config. */
70
203
  export function declareArgument(state, input) {
204
+ checkOpen(state, `declares argument "${input.name}"`, inputRemedy);
205
+ checkArgument(state, input);
206
+ const recorded = recordInput(state, input);
71
207
  const previous = state.bind;
72
208
  return {
73
209
  ...state,
210
+ ...recorded,
74
211
  bind: (values) => {
75
212
  const bound = previous(values);
76
213
  return { ...bound, args: { ...bound.args, ...values.argument(input) } };
77
214
  },
78
215
  inputs: [...state.inputs, input],
79
- late: recordLate(state, [{ input, kind: 'input' }]),
80
216
  };
81
217
  }
82
- /** The declared value joins `options` under its literal name, typed by its own config. */
83
- export function declareOption(state, input) {
218
+ /** The table a named Command's options meet at its own calls, before any Application holds it. */
219
+ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() };
220
+ /**
221
+ * The declared value joins `options` under its literal name, typed by its own config. The root's
222
+ * options also meet the Application's globals table, which a named Command meets when its subtree
223
+ * joins an Application.
224
+ */
225
+ export function declareOption(state, input, table = noGlobals) {
226
+ checkOpen(state, `declares option "${input.name}"`, inputRemedy);
227
+ const sentence = `${commandSentence(state.name)} option "${input.name}"`;
228
+ checkDescription(sentence, input.config.description);
229
+ checkHidden(sentence, input.config.hidden);
230
+ checkDeprecated(sentence, input.config.deprecated);
231
+ checkEnvBinding(sentence, input.config);
232
+ const recorded = recordInput(state, input);
233
+ checkLocalOptions([...optionsOf(state.inputs), input], table, commandSubject(state.name));
234
+ checkDeclarations([input]);
84
235
  const previous = state.bind;
85
236
  return {
86
237
  ...state,
238
+ ...recorded,
87
239
  bind: (values) => {
88
240
  const bound = previous(values);
89
241
  return { ...bound, options: { ...bound.options, ...values.option(input) } };
90
242
  },
91
243
  inputs: [...state.inputs, input],
92
- late: recordLate(state, [{ input, kind: 'input' }]),
93
- };
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
244
  };
103
245
  }
104
- /** One call's names stay one group, so the empty call the types reject still reports as one. */
246
+ /**
247
+ * One call's names join the Command's aliases. A call that names none, which the types reject and
248
+ * a JavaScript author can still write, reports as the call it is.
249
+ */
105
250
  export function declareAlias(state, names) {
106
- return {
107
- ...state,
108
- aliases: [...state.aliases, names],
109
- late: recordLate(state, names.map((alias) => ({ alias, kind: 'alias' }))),
110
- };
251
+ const { name } = state;
252
+ const first = names[0];
253
+ if (first === undefined) {
254
+ throw new DeclarationError(`${commandSentence(name)} declares an alias with no names. Supply at least one name.`);
255
+ }
256
+ // The call's own input is judged before the receiver's state.
257
+ // A non-string name then reports as an alias name instead of failing to print in the order diagnostic.
258
+ for (const alias of names) {
259
+ checkAliasName(name, alias);
260
+ }
261
+ checkOpen(state, `declares alias "${first}"`, 'Declare aliases before action().');
262
+ const aliases = [...state.aliases];
263
+ for (const alias of names) {
264
+ if (alias === name) {
265
+ throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}", which is its own name. Remove the alias.`);
266
+ }
267
+ if (aliases.includes(alias)) {
268
+ throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}" twice. Remove the repeated alias.`);
269
+ }
270
+ aliases.push(alias);
271
+ }
272
+ return { ...state, aliases };
111
273
  }
112
- /** Extension layers remain open after inputs and the action have been fixed. */
274
+ /**
275
+ * Extension layers remain open after inputs and the action have been fixed. Each layer is validated
276
+ * at its own call, against every descriptor the declaration already names.
277
+ */
113
278
  export function declareExtensions(state, values) {
114
- return { ...state, extensions: [...state.extensions, values] };
279
+ const descriptors = new Map(state.descriptors);
280
+ const layer = validateLayer({
281
+ declared: values,
282
+ descriptors,
283
+ subject: layerOf(state.name),
284
+ target: 'command',
285
+ });
286
+ return { ...state, descriptors, extensions: extendStore(state.extensions, layer) };
115
287
  }
116
288
  /**
117
289
  * The same channel, read as the result the handler's own declaration names. The value one call
@@ -123,137 +295,201 @@ function declaredChannel(out) {
123
295
  }
124
296
  /**
125
297
  * 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.
298
+ * action for every declaration, so the context the handler receives is read back at the call.
127
299
  */
128
300
  export function declareAction(state, handler) {
129
- const stored = (context) => handler({ ...context, out: declaredChannel(context.out) });
130
- return { ...state, actions: [...state.actions, stored] };
301
+ if (state.action !== undefined) {
302
+ throw new DeclarationError(`${commandSentence(state.name)} has multiple actions. Register one action.`);
303
+ }
304
+ // The graph and the routed node stay getters, because a spread would build the graph on every run.
305
+ const stored = (context) => handler({
306
+ args: context.args,
307
+ get command() {
308
+ return context.command;
309
+ },
310
+ get graph() {
311
+ return context.graph;
312
+ },
313
+ host: context.host,
314
+ options: context.options,
315
+ out: declaredChannel(context.out),
316
+ passthrough: context.passthrough,
317
+ signal: context.signal,
318
+ style: context.style,
319
+ });
320
+ return { ...state, action: stored };
131
321
  }
132
- /** The result declaration, which closes both result calls and reports lateness like the rest. */
322
+ /**
323
+ * The result declaration, which closes both result calls. Every rule its own call can judge throws
324
+ * here; the empty record and the default wait for attach, because `views()` can still add keys.
325
+ */
133
326
  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
- };
327
+ checkOpen(state, 'declares its result', 'Declare result() or rows() before action().');
328
+ const results = [...state.results, { kind, views: recordOf(declaration, 'views') }];
329
+ mergeResult(state.name, results);
330
+ return { ...state, results };
139
331
  }
140
332
  /** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
141
333
  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
- };
334
+ const results = [
335
+ ...state.results,
336
+ { default: recordOf(options, 'default'), kind: 'views', views: replacements },
337
+ ];
338
+ mergeResult(state.name, results);
339
+ return { ...state, results };
149
340
  }
150
- /** One property of an authoring argument, read defensively: build reports whatever it holds. */
341
+ /** One property of an authoring argument, read defensively: the rules report whatever it holds. */
151
342
  function recordOf(declaration, key) {
152
343
  return isPlainObject(declaration) ? declaration[key] : undefined;
153
344
  }
154
- /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
155
- export function attachChild(state, child) {
345
+ /** The parent a declaration is, as attach reads it. */
346
+ function parentOf(state) {
156
347
  return {
157
- ...state,
158
- children: [...state.children, child],
159
- late: recordLate(state, [{ child, kind: 'child' }]),
348
+ argument: state.inputs.find((input) => input.kind === 'argument')?.name,
349
+ children: state.children,
350
+ hasAction: state.action !== undefined,
351
+ name: state.name,
160
352
  };
161
353
  }
162
- /** The types remove a late call for TypeScript authors; JavaScript authors read it here. */
163
- function checkDeclarationOrder(state) {
164
- const { name } = state;
165
- const late = state.late[0];
166
- if (!late) {
167
- return;
354
+ /**
355
+ * One parent's namespace, which every canonical name and alias under it shares. The rule reads the
356
+ * same whichever sibling the author attached first.
357
+ */
358
+ function checkSiblings(parent, child) {
359
+ const sentence = commandSentence(parent.name);
360
+ const { children } = parent;
361
+ if (children.some((entry) => entry.name === child.name)) {
362
+ throw new DeclarationError(`${sentence} attaches two children named "${child.name}". Rename or remove one.`);
363
+ }
364
+ for (const alias of child.node.declared.aliases) {
365
+ if (children.some((entry) => entry.name === alias)) {
366
+ throw new DeclarationError(`${sentence} attaches child "${child.name}" with alias "${alias}", which is also the name of child "${alias}". Rename or remove one.`);
367
+ }
368
+ const owner = children.find((entry) => entry.node.declared.aliases.includes(alias));
369
+ if (owner) {
370
+ throw new DeclarationError(`${sentence} attaches child "${child.name}" with alias "${alias}", which is also an alias of child "${owner.name}". Rename or remove one.`);
371
+ }
168
372
  }
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.`);
373
+ const aliased = children.find((entry) => entry.node.declared.aliases.includes(child.name));
374
+ if (aliased) {
375
+ throw new DeclarationError(`${sentence} attaches child "${aliased.name}" with alias "${child.name}", which is also the name of child "${child.name}". Rename or remove one.`);
171
376
  }
172
- if (late.kind === 'input') {
173
- throw new DeclarationError(`${commandSentence(name)} declares ${late.input.kind} "${late.input.name}" after its action. Declare arguments and options before action().`);
377
+ }
378
+ /**
379
+ * A child at the cap holds no children. Attach reads the child at the shallowest level its parent
380
+ * can sit at, and the Application's join reads every node at its level below the root.
381
+ */
382
+ function checkNesting(parent, child, level) {
383
+ if (level >= nestingCap.depth && child.node.declared.children.length > 0) {
384
+ throw new DeclarationError(`${commandSentence(parent)} attaches child "${child.name}", which has children of its own. Nest Commands at most ${nestingCap.words} levels below the root.`);
174
385
  }
175
- if (late.kind === 'alias') {
176
- throw new DeclarationError(`${commandSentence(name)} declares alias "${late.alias}" after its action. Declare aliases before action().`);
386
+ }
387
+ /**
388
+ * A Command without an action is a group, and routing sends an invocation on to one of its
389
+ * children. A group with no children receives an invocation no handler can answer, and a local
390
+ * option on a group reaches no handler either, because locals never inherit.
391
+ */
392
+ function checkGroup(state) {
393
+ const { name } = state;
394
+ if (state.children.length === 0) {
395
+ throw new DeclarationError(`${commandSentence(name)} has no action. Register an action.`);
177
396
  }
178
- if (late.kind === 'result') {
179
- throw new DeclarationError(`${commandSentence(name)} declares its result after its action. Declare result() or rows() before action().`);
397
+ const option = state.inputs.find((input) => input.kind === 'option');
398
+ if (option) {
399
+ throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
180
400
  }
181
- // Child identity and names are settled before this call, so the node and its name are valid.
182
- throw new DeclarationError(`${commandSentence(name)} attaches child "${String(nodeOf(name, late.child).name)}" after its action. Attach children before action().`);
183
401
  }
184
- /** Child names are checked before any child builds, so parent diagnostics come first. */
185
- function collectChildren(state) {
186
- const attached = [];
187
- const seen = new Set();
188
- for (const child of state.children) {
189
- const node = nodeOf(state.name, child);
190
- const name = node.name;
191
- checkChildName(state.name, name);
192
- if (seen.has(name)) {
193
- throw new DeclarationError(`${commandSentence(state.name)} attaches two children named "${name}". Rename or remove one.`);
194
- }
195
- seen.add(name);
196
- attached.push([name, node]);
402
+ /**
403
+ * The rules a finished Command answers, which attach applies to a child and build to the root: a
404
+ * result needs an action, its merged views record names at least one view and its default, and a
405
+ * Command without an action is a group. It answers with the resolved result.
406
+ */
407
+ function checkFinished(declared, hasAction) {
408
+ const result = buildResult(declared, hasAction);
409
+ if (!hasAction) {
410
+ checkGroup(declared);
197
411
  }
198
- return attached;
412
+ return result;
199
413
  }
200
414
  /**
201
- * A Command's own alias rules, and the flat list in declaration order that routing and inspection
202
- * read. Each call keeps its own group, so a call that names none reports as the call it is.
415
+ * The one attach operation `Command.command()`, `Application.command()`, and a plugin's
416
+ * `commands` list share. A Command is an immutable value, so the child is final here: it is checked
417
+ * as a finished Command, against the parent's current children, and against the nesting cap from
418
+ * the shallowest level the parent can sit at.
203
419
  */
204
- function collectAliases(state) {
205
- const { name } = state;
206
- const aliases = [];
207
- const seen = new Set();
208
- for (const declaration of state.aliases) {
209
- if (declaration.length === 0) {
210
- throw new DeclarationError(`${commandSentence(name)} declares an alias with no names. Supply at least one name.`);
211
- }
212
- for (const alias of declaration) {
213
- checkAliasName(name, alias);
214
- if (alias === name) {
215
- throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}", which is its own name. Remove the alias.`);
216
- }
217
- if (seen.has(alias)) {
218
- throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}" twice. Remove the repeated alias.`);
219
- }
220
- seen.add(alias);
221
- aliases.push(alias);
222
- }
420
+ export function attach(parent, node) {
421
+ const { name } = node;
422
+ if (parent.hasAction) {
423
+ throw new DeclarationError(`${commandSentence(parent.name)} attaches child "${name}" after its action. Attach children before action().`);
424
+ }
425
+ if (parent.argument !== undefined) {
426
+ throw placementFault(parent.name, parent.argument, name);
427
+ }
428
+ checkFinished(node.declared, node.hasAction);
429
+ const child = { name, node };
430
+ checkSiblings(parent, child);
431
+ checkNesting(parent.name, child, parentLevel(parent.name) + 1);
432
+ return child;
433
+ }
434
+ /** The handle behind the value one `command()` call received; anything else is a declaration error. */
435
+ export function childNode(parent, child) {
436
+ const node = commandNode(child);
437
+ if (!node) {
438
+ throw new DeclarationError(`${commandSentence(parent)} attaches a value that is not a Command. Attach the value returned by new Command(name).`);
223
439
  }
224
- return aliases;
440
+ return node;
441
+ }
442
+ /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
443
+ function attachChild(state, child) {
444
+ const attached = attach(parentOf(state), childNode(state.name, child));
445
+ return { ...state, children: [...state.children, attached] };
225
446
  }
226
447
  /**
227
- * One parent's namespace, which every canonical name and alias under it shares. Each child settled
228
- * its own alias rules while it built, so what is left is the collision with a sibling. The names are
229
- * read before the walk, so the rule reads the same whichever sibling the author declared first.
448
+ * Walks one subtree joining an Application, once, for the rules only the Application can judge: one
449
+ * Command value reached through two paths, two distinct descriptors under one identity, a local
450
+ * option that meets a global or plugin option's key, spelling, or variable, and the nesting cap
451
+ * measured from the root. A claim is by node identity, and the name serves the diagnostic alone.
452
+ * At a cap of two a named parent's `command()` already rejects every deeper tree, so the depth check
453
+ * here fires only once the cap rises: attach reads one level, and only this walk knows each level.
230
454
  */
231
- function aliasNamespace(parent, names) {
232
- const owners = new Map();
233
- return function claim(child, alias) {
234
- if (names.has(alias)) {
235
- throw new DeclarationError(`${commandSentence(parent)} attaches child "${child}" with alias "${alias}", which is also the name of child "${alias}". Rename or remove one.`);
236
- }
237
- const owner = owners.get(alias);
238
- if (owner !== undefined) {
239
- throw new DeclarationError(`${commandSentence(parent)} attaches child "${child}" with alias "${alias}", which is also an alias of child "${owner}". Rename or remove one.`);
240
- }
241
- owners.set(alias, child);
242
- };
455
+ function joinSubtree(scope, child, { level, parent }) {
456
+ const { name, node } = child;
457
+ checkNesting(parent, child, level);
458
+ if (scope.owners.has(node)) {
459
+ throw new DeclarationError(`${commandSentence(parent)} attaches child "${name}", which ${commandSubject(scope.owners.get(node) ?? null)} also attaches. Attach a Command value at one point; create a new Command for each placement.`);
460
+ }
461
+ scope.owners.set(node, parent);
462
+ for (const descriptor of node.declared.descriptors.values()) {
463
+ registerDescriptor(scope.descriptors, descriptor);
464
+ }
465
+ checkLocalOptions(optionsOf(node.declared.inputs), scope.table, commandSubject(name));
466
+ for (const entry of node.declared.children) {
467
+ joinSubtree(scope, entry, { level: level + 1, parent: name });
468
+ }
243
469
  }
244
470
  /**
245
- * Claims a child for its parent when the depth-first walk reaches it, then builds its subtree. A
246
- * claimed node always means a second parent, because the duplicate-name rule rejects one parent
247
- * attaching a value twice. Parents may share a name, so the claim is by node identity and the name
248
- * serves the diagnostic alone.
471
+ * Attaches one child to an Application's root and walks the subtree it brings, once. The scope's
472
+ * registers are copies: the caller commits the owners, and the returned root holds the descriptors.
249
473
  */
250
- function buildChild(parent, [name, node], context) {
251
- const owner = context.owners.get(node);
252
- if (owner !== undefined) {
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.`);
474
+ export function attachToRoot(state, node, scope) {
475
+ const attached = attach(parentOf(state), node);
476
+ joinSubtree(scope, attached, { level: parentLevel(state.name) + 1, parent: state.name });
477
+ return { ...state, children: [...state.children, attached], descriptors: scope.descriptors };
478
+ }
479
+ /** Every attached Command's local options against a globals table that has just grown. */
480
+ function checkAttachedOptions(table, children) {
481
+ for (const { name, node } of children) {
482
+ checkLocalOptions(optionsOf(node.declared.inputs), table, commandSubject(name));
483
+ checkAttachedOptions(table, node.declared.children);
254
484
  }
255
- context.owners.set(node, parent);
256
- return node.build({ ...context, path: [...context.path, name] });
485
+ }
486
+ /**
487
+ * A declaration's own options and every attached Command's, against a globals table that has just
488
+ * grown. The table's new option reads as the other side of any collision.
489
+ */
490
+ export function checkDeclaredOptions(state, table) {
491
+ checkLocalOptions(optionsOf(state.inputs), table, commandSubject(state.name));
492
+ checkAttachedOptions(table, state.children);
257
493
  }
258
494
  /** A variadic or optional slot ends the positional list, so nothing may follow either one. */
259
495
  function checkSlotOrder(slot, next, subject) {
@@ -266,6 +502,10 @@ function checkSlotOrder(slot, next, subject) {
266
502
  : `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}. Declare an optional argument last.`);
267
503
  }
268
504
  }
505
+ /**
506
+ * The positional slots one declaration holds. Every authored argument answered these rules at its
507
+ * own call, so they throw here only for an argument a lifecycle hook declared.
508
+ */
269
509
  function collectArguments(state, subject) {
270
510
  const slots = [];
271
511
  const seen = new Set();
@@ -277,11 +517,7 @@ function collectArguments(state, subject) {
277
517
  throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
278
518
  }
279
519
  seen.add(input.name);
280
- slots.push({
281
- input,
282
- required: input.config.required === true,
283
- variadic: input.config.variadic === true,
284
- });
520
+ slots.push(slotOf(input));
285
521
  }
286
522
  for (let index = 0; index + 1 < slots.length; index += 1) {
287
523
  const slot = slots[index];
@@ -293,47 +529,8 @@ function collectArguments(state, subject) {
293
529
  return slots;
294
530
  }
295
531
  /**
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
- */
300
- function compileLocalOptions(state, globals, subject) {
301
- const declarations = state.inputs.filter((input) => input.kind === 'option');
302
- const local = { kind: 'local', subject };
303
- const application = { kind: 'application' };
304
- for (const declaration of declarations) {
305
- const claimed = globals.names.get(declaration.name);
306
- if (claimed) {
307
- throw keyCollision(declaration.name, claimed, local);
308
- }
309
- }
310
- const options = compileOptions(declarations, subject);
311
- for (const [spelling, option] of options) {
312
- const global = globals.options.get(spelling);
313
- if (global) {
314
- throw spellingCollision(spelling, { name: global.name, owner: globals.names.get(global.name) ?? application }, { name: option.name, owner: local });
315
- }
316
- }
317
- return options;
318
- }
319
- /**
320
- * A Command without an action is a group, and routing sends an invocation on to one of its
321
- * children. A group with no children receives an invocation no handler can answer, and a local
322
- * option on a group reaches no handler either, because locals never inherit.
323
- */
324
- function checkGroup(state, children) {
325
- const { name } = state;
326
- if (children.length === 0) {
327
- throw new DeclarationError(`${commandSentence(name)} has no action. Register an action.`);
328
- }
329
- const option = state.inputs.find((input) => input.kind === 'option');
330
- if (option) {
331
- throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
332
- }
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.
532
+ * A view name is a bare token the way an argument or option name is, and never an array index,
533
+ * because an integer-like key does not keep the position the author gave it.
337
534
  */
338
535
  function isViewName(name) {
339
536
  return isDeclaredName(name) && !/^(?:0|[1-9]\d*)$/u.test(name);
@@ -386,15 +583,14 @@ function resultView(declaration, name, entry) {
386
583
  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
584
  }
388
585
  /**
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.
586
+ * Every rule a results-lane call can judge on its own, applied to one Command's calls in the order
587
+ * it made them. The merged record is what the rules read: a later `views()` call replaces a key in
588
+ * place and appends a new one, so each name keeps the position the call that first named it gave
589
+ * it. A `default` once named persists through later calls that name none.
394
590
  */
395
- function buildResult(state, hasAction) {
396
- const sentence = commandSentence(state.name);
397
- const declarations = state.results.filter((call) => call.kind !== 'views');
591
+ function mergeResult(name, results) {
592
+ const sentence = commandSentence(name);
593
+ const declarations = results.filter((call) => call.kind !== 'views');
398
594
  if (declarations.length > 1) {
399
595
  throw new DeclarationError(`${sentence} declares two results. Declare one result() or rows() call.`);
400
596
  }
@@ -402,28 +598,41 @@ function buildResult(state, hasAction) {
402
598
  // A `views()` call reshapes a result's views, so one with no result reshapes nothing.
403
599
  // The types publish the call where a result is carried, so this reaches a JavaScript author.
404
600
  if (!declaration) {
405
- if (state.results.length > 0) {
601
+ if (results.length > 0) {
406
602
  throw new DeclarationError(`${sentence} reshapes its views and declares no result. Declare result() or rows() before action().`);
407
603
  }
408
604
  return undefined;
409
605
  }
410
- if (!hasAction) {
411
- throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
412
- }
413
606
  const views = new Map();
414
607
  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.`);
608
+ for (const call of results) {
609
+ for (const [key, entry] of recordEntries(call.views)) {
610
+ const view = resultView({ kind: declaration.kind, sentence }, key, entry);
611
+ if (!isViewName(key)) {
612
+ throw new DeclarationError(`${sentence} names view "${key}". Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.`);
420
613
  }
421
- views.set(name, view);
614
+ views.set(key, view);
422
615
  }
423
616
  if (call.kind === 'views') {
424
617
  selected = selectedKey(call.default) ?? selected;
425
618
  }
426
619
  }
620
+ return { kind: declaration.kind, selected, views };
621
+ }
622
+ /**
623
+ * The finished result: the merged record, which needs an action, at least one view, and a default
624
+ * that names one of them. The first key answers until one is named.
625
+ */
626
+ function buildResult(declared, hasAction) {
627
+ const merged = mergeResult(declared.name, declared.results);
628
+ if (!merged) {
629
+ return undefined;
630
+ }
631
+ const sentence = commandSentence(declared.name);
632
+ if (!hasAction) {
633
+ throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
634
+ }
635
+ const { selected, views } = merged;
427
636
  const first = views.keys().next();
428
637
  if (first.done === true) {
429
638
  throw new DeclarationError(`${sentence} declares a result with no views. Name at least one view.`);
@@ -431,7 +640,7 @@ function buildResult(state, hasAction) {
431
640
  if (selected !== undefined && !views.has(selected)) {
432
641
  throw new DeclarationError(`${sentence} selects default view "${selected}", which it does not name. Name the view or select a named one.`);
433
642
  }
434
- return { default: selected ?? first.value, kind: declaration.kind, views };
643
+ return { default: selected ?? first.value, kind: merged.kind, views };
435
644
  }
436
645
  /** The values one build made, so a value a hook returns is one of them and never a forged shape. */
437
646
  const attachments = new WeakMap();
@@ -476,6 +685,10 @@ class AttachedCommandValue {
476
685
  get result() {
477
686
  return this.#result;
478
687
  }
688
+ /** Published on first read and shared by every value whose store is unchanged. */
689
+ get extensions() {
690
+ return publishStore(this.#state.extensions);
691
+ }
479
692
  argument(name, config) {
480
693
  return this.#declare({ config: captureConfig(config), kind: 'argument', name });
481
694
  }
@@ -491,8 +704,24 @@ class AttachedCommandValue {
491
704
  const { declared } = this.#state;
492
705
  return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
493
706
  }
707
+ /**
708
+ * The values are validated here, at the call, so the next read of `extensions` holds them and
709
+ * build never validates them again. A rejected value throws from the call and adds nothing.
710
+ */
494
711
  extend(...values) {
495
- return this.#derive({ kind: 'extend', values }, this.#state.declared);
712
+ const state = this.#state;
713
+ const registry = new Map(state.registry);
714
+ const layer = validateLayer({
715
+ declared: values,
716
+ descriptors: registry,
717
+ subject: state.subject,
718
+ target: 'command',
719
+ });
720
+ return new _a({
721
+ ...state,
722
+ extensions: extendStore(state.extensions, layer),
723
+ registry,
724
+ });
496
725
  }
497
726
  /** One input the running hook declared, which the Command's own names now hold. */
498
727
  #declare(input) {
@@ -538,25 +767,31 @@ function attachOnce(value, hook, named) {
538
767
  * Every installed plugin's hook over one Command, in installation order, each receiving what the
539
768
  * previous returned. The calls they made travel back to the declaration the remaining rules read.
540
769
  */
541
- function runAttachHooks(declared, facts, plugins) {
770
+ function runAttachHooks(start, facts, plugins) {
771
+ const { declared, extensions, layer, registry } = start;
542
772
  const subject = commandSubject(declared.name);
543
773
  const { lineage } = facts;
544
- let progress = { calls: [], declared };
774
+ let progress = { calls: [], declared, extensions, registry };
545
775
  for (const installed of plugins) {
546
776
  const hook = installed.onCommandAttach;
547
777
  if (hook) {
548
778
  const identity = installed.identity;
549
- const value = new AttachedCommandValue({ ...progress, ...facts, identity });
779
+ const value = new AttachedCommandValue({ ...progress, ...facts, identity, subject: layer });
550
780
  const state = attachOnce(value, hook, { identity, lineage, subject });
551
- progress = { calls: state.calls, declared: state.declared };
781
+ progress = {
782
+ calls: state.calls,
783
+ declared: state.declared,
784
+ extensions: state.extensions,
785
+ registry: state.registry,
786
+ };
552
787
  }
553
788
  }
554
- return progress.calls;
789
+ return progress;
555
790
  }
556
791
  /**
557
792
  * 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.
793
+ * types not at all, and it skips the order checks an authoring call makes, because a hook's calls
794
+ * are exempt from the closures `action()` applies.
560
795
  */
561
796
  function declareHookInput(state, input) {
562
797
  const previous = state.bind;
@@ -575,15 +810,10 @@ function declareHookInput(state, input) {
575
810
  function applyAttachCalls(state, calls) {
576
811
  let next = state;
577
812
  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
- }
813
+ next =
814
+ call.kind === 'input'
815
+ ? declareHookInput(next, call.input)
816
+ : { ...next, results: [...next.results, call.call] };
587
817
  }
588
818
  return next;
589
819
  }
@@ -706,17 +936,23 @@ function checkAttachedInputs(declared, attached, globals) {
706
936
  }
707
937
  /** Binds one Command's declarations to its action, so an action reads only validated values. */
708
938
  function bindDispatch(state, action, globals) {
709
- return ({ host, out, passthrough, signal, style, values }) => {
939
+ return ({ command, graph, host, out, passthrough, signal, style, values }) => {
710
940
  const bound = state.bind(values);
711
941
  // Last resort: no typed path exists.
712
942
  // The graph erases the binder's generic relationship.
713
943
  // It holds because attachment checks the global output requirement.
714
- // Graph build rejects a collision between a global and a local.
944
+ // The call or the attach that met both options rejected a collision between a global and a local.
715
945
  // This binder returns the Application's validated globals alone.
716
946
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
717
947
  const globalOptions = globals.bind(values);
718
948
  return action({
719
949
  args: bound.args,
950
+ get command() {
951
+ return command();
952
+ },
953
+ get graph() {
954
+ return graph();
955
+ },
720
956
  host,
721
957
  options: { ...globalOptions, ...bound.options },
722
958
  out,
@@ -726,7 +962,10 @@ function bindDispatch(state, action, globals) {
726
962
  });
727
963
  };
728
964
  }
729
- /** Each declaration's own facts and extension values, whichever scope declared it. */
965
+ /**
966
+ * The own facts and extension values of each input a lifecycle hook declared, which the calls that
967
+ * declared the author's inputs already checked.
968
+ */
730
969
  function checkInputFacts(inputs, command, context) {
731
970
  const { name, subject } = command;
732
971
  for (const input of inputs) {
@@ -734,10 +973,12 @@ function checkInputFacts(inputs, command, context) {
734
973
  checkDescription(sentence, input.config.description);
735
974
  if (input.kind === 'argument') {
736
975
  checkNoListingFacts(sentence, input.config);
976
+ checkNoArgumentBinding(sentence, input.config);
737
977
  }
738
978
  else {
739
979
  checkHidden(sentence, input.config.hidden);
740
980
  checkDeprecated(sentence, input.config.deprecated);
981
+ checkEnvBinding(sentence, input.config);
741
982
  }
742
983
  context.extensions.set(input, buildExtensions({
743
984
  declared: input.config.extensions,
@@ -750,86 +991,73 @@ function checkInputFacts(inputs, command, context) {
750
991
  /** One Command declares arguments or attaches children, whichever declaration made each of them. */
751
992
  function checkArgumentPlacement(name, slot, child) {
752
993
  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.`);
994
+ throw placementFault(name, slot.input.name, child.name);
754
995
  }
755
996
  }
756
- /** Validates one declaration against the shared globals table and compiles it for dispatch. */
997
+ /**
998
+ * Compiles one declaration for dispatch against the shared globals table. Every authored rule threw
999
+ * at the call or the attach that first held its data, so what can still fail here is the root's
1000
+ * finished-Command rules, the root never being attached, and whatever the lifecycle hooks
1001
+ * contributed.
1002
+ */
757
1003
  export function buildCommand(state, context) {
758
- const { actions, name } = state;
1004
+ const { action, facts, name } = state;
759
1005
  const { globals } = context;
760
1006
  const subject = commandSubject(name);
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);
773
- const attached = collectChildren(state);
774
- checkDeclarationOrder(state);
775
- const aliases = collectAliases(state);
776
- const declaredSlots = collectArguments(state, subject);
777
- checkArgumentPlacement(name, declaredSlots[0], attached[0]);
778
- if (actions.length > 1) {
779
- throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
1007
+ const layer = layerOf(name);
1008
+ for (const [input, record] of state.records) {
1009
+ context.extensions.set(input, record);
780
1010
  }
781
- const action = actions[0];
1011
+ const declaredSlots = collectArguments(state, subject);
782
1012
  const hasAction = action !== undefined;
783
- const declaredResult = buildResult(state, hasAction);
784
1013
  /**
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.
1014
+ * The hooks run once the author's declaration is complete, and each value a hook receives resolves
1015
+ * its result, so a hook reads an exact record. Every rule below reads what the hooks returned.
787
1016
  */
788
1017
  // One token per Command per build, which binds a hook's return to the value it received.
789
1018
  const lineage = {};
790
- const calls = runAttachHooks(state, { hasAction, lineage, path: context.path }, context.plugins);
1019
+ const progress = runAttachHooks({ declared: state, extensions: state.extensions, layer, registry: context.descriptors }, { hasAction, lineage, path: context.path }, context.plugins);
1020
+ const { calls } = progress;
1021
+ // The descriptors the returned value's extensions registered join the build's registry.
1022
+ for (const [identity, descriptor] of progress.registry) {
1023
+ context.descriptors.set(identity, descriptor);
1024
+ }
791
1025
  const hooked = applyAttachCalls(state, calls);
792
1026
  const hookInputs = calls.filter((call) => call.kind === 'input');
793
1027
  checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
794
1028
  // The hook-declared names are checked first, so a collision reports in the plugin's voice.
795
1029
  // A rule the author's own declaration voices never speaks for a name a hook declared.
796
1030
  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;
1031
+ // Every value was validated once, the author's at each call and each hook's at its `extend()`.
1032
+ const extensions = publishStore(progress.extensions);
804
1033
  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;
807
- if (!action) {
808
- checkGroup(hooked, attached);
809
- }
810
- const options = compileLocalOptions(hooked, globals, subject);
1034
+ checkArgumentPlacement(name, slots[0], state.children[0]);
1035
+ const result = checkFinished(hooked, hasAction);
1036
+ const options = checkLocalOptions(optionsOf(hooked.inputs), globals, subject);
1037
+ // A hook's erased calls answer the declaration rules an authored call answers at the call.
1038
+ checkDeclarations(hookInputs.map((call) => call.input));
811
1039
  const children = new Map();
812
1040
  const routes = new Map();
813
- const claim = aliasNamespace(name, new Set(attached.map((entry) => entry[0])));
814
- for (const entry of attached) {
815
- // The subtree builds before its aliases are claimed, so the tree rule keeps its precedence.
816
- const routed = { command: buildChild(name, entry, context), name: entry[0] };
1041
+ for (const child of state.children) {
1042
+ const routed = {
1043
+ command: child.node.build({ ...context, path: [...context.path, child.name] }),
1044
+ name: child.name,
1045
+ };
817
1046
  children.set(routed.name, routed.command);
818
1047
  routes.set(routed.name, routed);
819
1048
  for (const alias of routed.command.aliases) {
820
- claim(routed.name, alias);
821
1049
  routes.set(alias, routed);
822
1050
  }
823
1051
  }
824
1052
  return {
825
- aliases,
1053
+ aliases: state.aliases,
826
1054
  arguments: slots,
827
1055
  children,
828
- deprecated,
829
- description,
1056
+ deprecated: facts.deprecated,
1057
+ description: facts.description,
830
1058
  dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
831
1059
  extensions,
832
- hidden,
1060
+ hidden: facts.hidden,
833
1061
  inputs: hooked.inputs,
834
1062
  name,
835
1063
  options,
@@ -837,30 +1065,31 @@ export function buildCommand(state, context) {
837
1065
  routes,
838
1066
  };
839
1067
  }
840
- /** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
841
- export function buildGraph(root, globals, install) {
1068
+ /**
1069
+ * The globals table compiles once per build and the whole graph shares it. The root's registry
1070
+ * holds every descriptor the Application names, so a hook's `extend()` call meets all of them.
1071
+ */
1072
+ export function buildGraph(root, globals, plugins) {
1073
+ const extensions = new Map([
1074
+ ...globals.records,
1075
+ ...plugins.flatMap((installed) => [...installed.records]),
1076
+ ]);
842
1077
  const context = {
843
- descriptors: install.descriptors,
844
- extensions: install.extensions,
845
- globals: buildGlobals(globals, install.plugins, install),
846
- owners: new Map(),
1078
+ descriptors: new Map(root.descriptors),
1079
+ extensions,
1080
+ globals: buildGlobals(globals, plugins),
847
1081
  path: [],
848
- plugins: install.plugins,
849
- };
850
- return {
851
- extensions: context.extensions,
852
- globals: context.globals,
853
- root: buildCommand(root, context),
1082
+ plugins,
854
1083
  };
1084
+ return { extensions, globals: context.globals, root: buildCommand(root, context) };
855
1085
  }
856
1086
  export class CommandBuilder {
1087
+ #name;
857
1088
  #state;
858
- constructor(state) {
1089
+ constructor(name, state) {
1090
+ this.#name = name;
859
1091
  this.#state = state;
860
- nodes.set(this, this);
861
- }
862
- get name() {
863
- return this.#state.name;
1092
+ nodes.set(this, nodeHandle(name, state));
864
1093
  }
865
1094
  argument(name, config) {
866
1095
  const input = {
@@ -868,7 +1097,7 @@ export class CommandBuilder {
868
1097
  kind: 'argument',
869
1098
  name,
870
1099
  };
871
- return this.derive(declareArgument(this.#state, input));
1100
+ return this.#derive(declareArgument(this.#state, input));
872
1101
  }
873
1102
  option(name, config) {
874
1103
  const input = {
@@ -876,66 +1105,61 @@ export class CommandBuilder {
876
1105
  kind: 'option',
877
1106
  name,
878
1107
  };
879
- return this.derive(declareOption(this.#state, input));
1108
+ return this.#derive(declareOption(this.#state, input));
880
1109
  }
881
1110
  /**
882
- * Aliases are other bare tokens that route to this Command. They invalidate no call, and the
1111
+ * Aliases are other portable names that route to this Command. They invalidate no call, and the
883
1112
  * tuple rest parameter rejects a call that names none.
884
1113
  */
885
1114
  alias(...names) {
886
- return this.derive(declareAlias(this.#state, names));
1115
+ return this.#derive(declareAlias(this.#state, names));
887
1116
  }
888
1117
  /** A child arrives in any type state, because its own action is the call that finished it. */
889
1118
  command(child) {
890
- return this.derive(attachChild(this.#state, child));
1119
+ return this.#derive(attachChild(this.#state, child));
891
1120
  }
892
1121
  /**
893
1122
  * The value this Command produces for its consumer. The type argument is stated by the author,
894
1123
  * so the views record states no type of its own and an omitted argument names none either.
895
1124
  */
896
1125
  result(declaration) {
897
- return this.derive(declareResult(this.#state, 'value', declaration));
1126
+ return this.#derive(declareResult(this.#state, 'value', declaration));
898
1127
  }
899
1128
  /** The same declaration over a sequence, whose type argument is one row. */
900
1129
  rows(declaration) {
901
- return this.derive(declareResult(this.#state, 'rows', declaration));
1130
+ return this.#derive(declareResult(this.#state, 'rows', declaration));
902
1131
  }
903
1132
  /**
904
1133
  * Views after the fact. It merges by key, so an existing name is replaced in place and a new one
905
1134
  * is appended, and `default` names the key core renders when nothing selects another.
906
1135
  */
907
1136
  views(replacements, options) {
908
- return this.derive(declareResultViews(this.#state, replacements, options));
1137
+ return this.#derive(declareResultViews(this.#state, replacements, options));
909
1138
  }
910
1139
  /** The action closes input authoring; `extend()` remains outside this state transition. */
911
1140
  action(handler) {
912
- return this.derive(declareAction(this.#state, handler));
1141
+ return this.#derive(declareAction(this.#state, handler));
913
1142
  }
914
1143
  extend(...values) {
915
- return this.derive(declareExtensions(this.#state, values));
916
- }
917
- build(context) {
918
- return buildCommand(this.#state, context);
1144
+ return this.#derive(declareExtensions(this.#state, values));
919
1145
  }
920
1146
  /**
921
1147
  * The same runtime value in the state the calling method's return type names. Each call states
922
1148
  * its own transition, and the declared result travels with it unless the call replaces it.
923
1149
  */
924
- derive(state) {
925
- return new CommandBuilder(state);
1150
+ #derive(state) {
1151
+ return new _b(this.#name, state);
926
1152
  }
927
1153
  }
928
- /** The constructor uses the Application registration; public type defaults stay library-neutral. */
1154
+ _b = CommandBuilder;
1155
+ /**
1156
+ * The constructor uses the Application registration; public type defaults stay library-neutral.
1157
+ * Every rule on the name and the options slot throws before the value exists.
1158
+ */
929
1159
  class CommandDeclaration extends CommandBuilder {
930
1160
  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
- }));
1161
+ const declared = namedState(name, options);
1162
+ super(declared.name, declared.state);
939
1163
  }
940
1164
  }
941
1165
  /** The public constructor takes a name and one options object, as the Application does. */
@@ -955,14 +1179,20 @@ export function collectInputs(command) {
955
1179
  function candidatesOf(command) {
956
1180
  return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
957
1181
  }
1182
+ /**
1183
+ * Whether a token names one of the Command's children: the Command has children and the token is
1184
+ * no option token. Routing and `locate` read a bare word through this rule.
1185
+ */
1186
+ export function readsAsChild(command, token) {
1187
+ return command.children.size > 0 && !isOptionToken(token);
1188
+ }
958
1189
  /** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
959
1190
  export function route(root, tokens) {
960
1191
  let command = root;
961
1192
  const path = [];
962
1193
  let index = 0;
963
- while (command.children.size > 0) {
964
- const token = tokens[index];
965
- if (token === undefined || token === '--' || token.startsWith('-')) {
1194
+ for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
1195
+ if (!readsAsChild(command, token)) {
966
1196
  break;
967
1197
  }
968
1198
  const child = command.routes.get(token);
@@ -983,29 +1213,33 @@ export function route(root, tokens) {
983
1213
  */
984
1214
  function bindArguments(command, path, positionals) {
985
1215
  const values = new Map();
986
- let index = 0;
987
- for (const slot of command.arguments) {
988
- if (slot.variadic) {
989
- const rest = positionals.slice(index);
990
- // An empty tail binds nothing, so validation reads it as `[]` or reports the omission.
991
- if (rest.length > 0) {
992
- values.set(slot.input, rest);
993
- }
994
- index = positionals.length;
1216
+ for (const [position, token] of positionals.entries()) {
1217
+ const slot = argumentSlot(command.arguments, position);
1218
+ if (!slot) {
1219
+ throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(position));
1220
+ }
1221
+ // An empty variadic tail binds nothing, so validation reads it as `[]` or reports the omission.
1222
+ const bound = values.get(slot.input);
1223
+ if (!slot.variadic) {
1224
+ values.set(slot.input, token);
1225
+ }
1226
+ else if (Array.isArray(bound)) {
1227
+ bound.push(token);
995
1228
  }
996
1229
  else {
997
- const value = positionals[index];
998
- if (value !== undefined) {
999
- values.set(slot.input, value);
1000
- index += 1;
1001
- }
1230
+ values.set(slot.input, [token]);
1002
1231
  }
1003
1232
  }
1004
- if (index < positionals.length) {
1005
- throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(index));
1006
- }
1007
1233
  return values;
1008
1234
  }
1235
+ /**
1236
+ * The slot the positional at one index fills: the slot at that index, else a variadic last slot,
1237
+ * which accepts every later positional, else none. Binding and `locate` read positions through it.
1238
+ */
1239
+ export function argumentSlot(slots, position) {
1240
+ const last = slots.at(-1);
1241
+ return slots[position] ?? (last?.variadic ? last : undefined);
1242
+ }
1009
1243
  /** Consumes the globals table, then routes the remaining bare tokens to a Command. */
1010
1244
  export function routeInvocation(graph, argv) {
1011
1245
  const scan = extractGlobals(graph.globals.options, [...argv]);
@@ -1038,37 +1272,103 @@ function requestOf(command, values, passthrough) {
1038
1272
  });
1039
1273
  }
1040
1274
  /**
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.
1275
+ * The callable check and local parsing. A group answers no invocation of its own, so it holds the
1276
+ * missing-subcommand error with the rank the routing errors have and parses no token; a token
1277
+ * fault holds the same way.
1044
1278
  */
1045
- async function readyDispatch(graph, routed, invocation) {
1046
- const { command, path, scan } = routed;
1279
+ function parseLocal(routed) {
1280
+ const { command, path } = routed;
1047
1281
  const { dispatch } = command;
1048
- // A group answers no invocation of its own, so it fails with the routing errors above it.
1049
- if (!dispatch) {
1050
- throw new NonCallableCommandError(path, candidatesOf(command));
1282
+ try {
1283
+ if (!dispatch) {
1284
+ throw new NonCallableCommandError(path, candidatesOf(command));
1285
+ }
1286
+ const parsed = parseInputs(command.options, routed.tokens);
1287
+ const args = bindArguments(command, path, parsed.positionals);
1288
+ return {
1289
+ args,
1290
+ dispatch,
1291
+ kind: 'parsed',
1292
+ options: parsed.options,
1293
+ passthrough: parsed.passthrough,
1294
+ };
1295
+ }
1296
+ catch (error) {
1297
+ return { fault: error, kind: 'held' };
1051
1298
  }
1052
- const parsed = parseInputs(command.options, routed.tokens);
1053
- const args = bindArguments(command, path, parsed.positionals);
1299
+ }
1300
+ /**
1301
+ * The node of one option a configuration source is asked about: in the graph's globals, or among
1302
+ * the routed Command's own options. Build published every declaration, so a missing node means the
1303
+ * two readings of one graph disagree.
1304
+ */
1305
+ function requestNode(graph, path, place) {
1306
+ const options = place.global ? graph.globals : nodeAt(graph, path).options;
1307
+ const node = options.find((option) => option.name === place.name);
1308
+ if (!node) {
1309
+ throw new InternalError(`Option "${place.name}" is not in the inspected graph.`, undefined);
1310
+ }
1311
+ return node;
1312
+ }
1313
+ /**
1314
+ * The input-source stage over this run's own copies of the parsed values. The global and plugin
1315
+ * options fill whatever local parsing held; the routed Command's own options fill only when local
1316
+ * parsing held no fault, because the request is `null` otherwise.
1317
+ */
1318
+ async function fillScope({ graph, invocation, routed }, local) {
1319
+ const globals = copyValues(routed.scan);
1320
+ const locals = copyValues(local.kind === 'parsed' ? local.options : emptyValues());
1321
+ const sources = await fillInputs({
1322
+ extensions: graph.extensions,
1323
+ globals: {
1324
+ global: true,
1325
+ inputs: [...graph.globals.inputs, ...graph.globals.plugins.flatMap((entry) => entry.inputs)],
1326
+ values: globals,
1327
+ },
1328
+ host: invocation.host,
1329
+ inspected: invocation.inspected,
1330
+ locals: local.kind === 'parsed'
1331
+ ? { global: false, inputs: optionsOf(routed.command.inputs), values: locals }
1332
+ : undefined,
1333
+ out: invocation.sourceOut,
1334
+ plugins: graph.globals.plugins,
1335
+ request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
1336
+ signal: invocation.signal,
1337
+ style: invocation.style,
1338
+ });
1339
+ return { globals, locals, sources };
1340
+ }
1341
+ /**
1342
+ * Validation and the dispatch it prepares, over the values the input-source stage left. It answers
1343
+ * with the call that dispatches, so the caller records that the action was invoked at the moment
1344
+ * it invokes it and no earlier failure reads as a dispatch.
1345
+ */
1346
+ async function readyDispatch({ graph, invocation, routed }, filled) {
1347
+ const { command, path } = routed;
1348
+ const { local } = filled;
1054
1349
  const values = await validateValues({
1055
1350
  command: path,
1056
1351
  defaults: invocation.defaults,
1057
1352
  host: invocation.host,
1058
1353
  inputs: { globals: graph.globals.inputs, locals: command.inputs },
1059
- passthrough: parsed.passthrough,
1354
+ passthrough: local.passthrough,
1355
+ plugins: graph.globals.plugins.flatMap((entry) => entry.inputs),
1060
1356
  signal: invocation.signal,
1061
- supplied: { args, options: mergeValues(scan, parsed.options) },
1357
+ sources: filled.sources,
1358
+ supplied: { args: local.args, options: filled.values },
1062
1359
  });
1063
1360
  return {
1064
1361
  dispatch: async (view) => {
1065
1362
  // The channel is built at the boundary, because the view a middleware selected is read there.
1066
1363
  const channel = invocation.channel({ path, result: command.result, view });
1067
1364
  try {
1068
- await dispatch({
1365
+ await local.dispatch({
1366
+ // The run builds its graph once, so every read answers the same node.
1367
+ command: () => nodeAt(invocation.inspected(), path),
1368
+ graph: invocation.inspected,
1069
1369
  host: invocation.host,
1070
1370
  out: channel.out,
1071
- passthrough: parsed.passthrough,
1371
+ passthrough: local.passthrough,
1072
1372
  signal: invocation.signal,
1073
1373
  style: invocation.style,
1074
1374
  values,
@@ -1090,19 +1390,42 @@ async function readyDispatch(graph, routed, invocation) {
1090
1390
  throw error;
1091
1391
  }
1092
1392
  },
1093
- request: requestOf(command, values, parsed.passthrough),
1393
+ request: requestOf(command, values, local.passthrough),
1094
1394
  };
1095
1395
  }
1096
1396
  /**
1097
1397
  * Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
1098
1398
  * middleware reads the request before the action runs and a takeover never observes the fault.
1399
+ * The phases run in order: local parsing, the input-source stage, and validation. A local fault
1400
+ * outranks a configuration source's fault, and after either one core runs no validation.
1099
1401
  */
1100
1402
  export async function prepareDispatch(graph, routed, invocation) {
1101
1403
  const result = routed.command.result;
1404
+ const local = parseLocal(routed);
1405
+ const preparation = { graph, invocation, routed };
1406
+ const { globals, locals, sources } = await fillScope(preparation, local);
1407
+ const held = (fault) => ({
1408
+ fault,
1409
+ globals,
1410
+ kind: 'held',
1411
+ request: null,
1412
+ result,
1413
+ });
1414
+ if (local.kind === 'held') {
1415
+ return held(local.fault);
1416
+ }
1417
+ if (sources.fault) {
1418
+ return held(sources.fault);
1419
+ }
1102
1420
  try {
1103
- return { ...(await readyDispatch(graph, routed, invocation)), kind: 'ready', result };
1421
+ const ready = await readyDispatch(preparation, {
1422
+ local,
1423
+ sources,
1424
+ values: mergeValues(globals, locals),
1425
+ });
1426
+ return { ...ready, globals, kind: 'ready', result };
1104
1427
  }
1105
1428
  catch (error) {
1106
- return { fault: error, kind: 'held', request: null, result };
1429
+ return held(error);
1107
1430
  }
1108
1431
  }