@loomcli/core 0.4.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 { buildExtensions, extendStore, publishStore, storeCommandLayers, validateLayer, } 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
+ });
18
39
  }
19
- /** Reject retired globals wiring at graph build, before any invocation reads the options. */
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;
43
+ }
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
244
  };
94
245
  }
95
- /** Globals close when composition starts; retain late calls for the shared build-order check. */
96
- export function recordGlobalOption(state, name) {
97
- return {
98
- ...state,
99
- late: state.actions.length > 0 || state.children.length > 0
100
- ? [...state.late, { kind: 'global', name }]
101
- : state.late,
102
- };
103
- }
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();
@@ -581,8 +790,8 @@ function runAttachHooks(start, facts, plugins) {
581
790
  }
582
791
  /**
583
792
  * One input a hook declared. It joins the values an action reads at run time and the declaration's
584
- * types not at all, and it records no late declaration, because a hook's calls are exempt from the
585
- * closures `action()` applies.
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.
586
795
  */
587
796
  function declareHookInput(state, input) {
588
797
  const previous = state.bind;
@@ -727,17 +936,23 @@ function checkAttachedInputs(declared, attached, globals) {
727
936
  }
728
937
  /** Binds one Command's declarations to its action, so an action reads only validated values. */
729
938
  function bindDispatch(state, action, globals) {
730
- return ({ host, out, passthrough, signal, style, values }) => {
939
+ return ({ command, graph, host, out, passthrough, signal, style, values }) => {
731
940
  const bound = state.bind(values);
732
941
  // Last resort: no typed path exists.
733
942
  // The graph erases the binder's generic relationship.
734
943
  // It holds because attachment checks the global output requirement.
735
- // 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.
736
945
  // This binder returns the Application's validated globals alone.
737
946
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
738
947
  const globalOptions = globals.bind(values);
739
948
  return action({
740
949
  args: bound.args,
950
+ get command() {
951
+ return command();
952
+ },
953
+ get graph() {
954
+ return graph();
955
+ },
741
956
  host,
742
957
  options: { ...globalOptions, ...bound.options },
743
958
  out,
@@ -747,7 +962,10 @@ function bindDispatch(state, action, globals) {
747
962
  });
748
963
  };
749
964
  }
750
- /** 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
+ */
751
969
  function checkInputFacts(inputs, command, context) {
752
970
  const { name, subject } = command;
753
971
  for (const input of inputs) {
@@ -755,10 +973,12 @@ function checkInputFacts(inputs, command, context) {
755
973
  checkDescription(sentence, input.config.description);
756
974
  if (input.kind === 'argument') {
757
975
  checkNoListingFacts(sentence, input.config);
976
+ checkNoArgumentBinding(sentence, input.config);
758
977
  }
759
978
  else {
760
979
  checkHidden(sentence, input.config.hidden);
761
980
  checkDeprecated(sentence, input.config.deprecated);
981
+ checkEnvBinding(sentence, input.config);
762
982
  }
763
983
  context.extensions.set(input, buildExtensions({
764
984
  declared: input.config.extensions,
@@ -771,45 +991,32 @@ function checkInputFacts(inputs, command, context) {
771
991
  /** One Command declares arguments or attaches children, whichever declaration made each of them. */
772
992
  function checkArgumentPlacement(name, slot, child) {
773
993
  if (slot && child) {
774
- throw new DeclarationError(`${commandSentence(name)} declares argument "${slot.input.name}" and attaches child "${child[0]}". Move the argument into a child Command or remove the children.`);
994
+ throw placementFault(name, slot.input.name, child.name);
775
995
  }
776
996
  }
777
- /** 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
+ */
778
1003
  export function buildCommand(state, context) {
779
- const { actions, name } = state;
1004
+ const { action, facts, name } = state;
780
1005
  const { globals } = context;
781
1006
  const subject = commandSubject(name);
782
- const layer = { phrase: `on ${subject}`, sentence: commandSentence(name) };
783
- checkCommandOptions(name, state.options);
784
- const description = checkDescription(commandSentence(name), state.description);
785
- const hidden = checkHidden(commandSentence(name), state.hidden);
786
- const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
787
- // The author's layers validate before any hook runs on this Command, so a hook reads them.
788
- const declaredExtensions = storeCommandLayers({
789
- descriptors: context.descriptors,
790
- layers: state.extensions,
791
- subject: layer,
792
- });
793
- // Each declaration's own facts, in authoring order, before the rules that pair declarations.
794
- checkInputFacts(state.inputs, { name, subject }, context);
795
- const attached = collectChildren(state);
796
- checkDeclarationOrder(state);
797
- const aliases = collectAliases(state);
798
- const declaredSlots = collectArguments(state, subject);
799
- checkArgumentPlacement(name, declaredSlots[0], attached[0]);
800
- if (actions.length > 1) {
801
- 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);
802
1010
  }
803
- const action = actions[0];
1011
+ const declaredSlots = collectArguments(state, subject);
804
1012
  const hasAction = action !== undefined;
805
- const declaredResult = buildResult(state, hasAction);
806
1013
  /**
807
- * The hooks run once the author's declaration is complete and its result is resolved, so a hook
808
- * reads an exact record, and every rule below reads what the hooks returned.
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.
809
1016
  */
810
1017
  // One token per Command per build, which binds a hook's return to the value it received.
811
1018
  const lineage = {};
812
- const progress = runAttachHooks({ declared: state, extensions: declaredExtensions, layer, registry: context.descriptors }, { 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);
813
1020
  const { calls } = progress;
814
1021
  // The descriptors the returned value's extensions registered join the build's registry.
815
1022
  for (const [identity, descriptor] of progress.registry) {
@@ -821,37 +1028,36 @@ export function buildCommand(state, context) {
821
1028
  // The hook-declared names are checked first, so a collision reports in the plugin's voice.
822
1029
  // A rule the author's own declaration voices never speaks for a name a hook declared.
823
1030
  checkAttachedInputs(hooked, hookInputs, globals);
824
- // Every value was validated once, the author's above and each hook's at its `extend()` call.
1031
+ // Every value was validated once, the author's at each call and each hook's at its `extend()`.
825
1032
  const extensions = publishStore(progress.extensions);
826
1033
  const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
827
- checkArgumentPlacement(name, slots[0], attached[0]);
828
- const result = calls.length > 0 ? buildResult(hooked, hasAction) : declaredResult;
829
- if (!action) {
830
- checkGroup(hooked, attached);
831
- }
832
- 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));
833
1039
  const children = new Map();
834
1040
  const routes = new Map();
835
- const claim = aliasNamespace(name, new Set(attached.map((entry) => entry[0])));
836
- for (const entry of attached) {
837
- // The subtree builds before its aliases are claimed, so the tree rule keeps its precedence.
838
- 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
+ };
839
1046
  children.set(routed.name, routed.command);
840
1047
  routes.set(routed.name, routed);
841
1048
  for (const alias of routed.command.aliases) {
842
- claim(routed.name, alias);
843
1049
  routes.set(alias, routed);
844
1050
  }
845
1051
  }
846
1052
  return {
847
- aliases,
1053
+ aliases: state.aliases,
848
1054
  arguments: slots,
849
1055
  children,
850
- deprecated,
851
- description,
1056
+ deprecated: facts.deprecated,
1057
+ description: facts.description,
852
1058
  dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
853
1059
  extensions,
854
- hidden,
1060
+ hidden: facts.hidden,
855
1061
  inputs: hooked.inputs,
856
1062
  name,
857
1063
  options,
@@ -859,30 +1065,31 @@ export function buildCommand(state, context) {
859
1065
  routes,
860
1066
  };
861
1067
  }
862
- /** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
863
- 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
+ ]);
864
1077
  const context = {
865
- descriptors: install.descriptors,
866
- extensions: install.extensions,
867
- globals: buildGlobals(globals, install.plugins, install),
868
- owners: new Map(),
1078
+ descriptors: new Map(root.descriptors),
1079
+ extensions,
1080
+ globals: buildGlobals(globals, plugins),
869
1081
  path: [],
870
- plugins: install.plugins,
871
- };
872
- return {
873
- extensions: context.extensions,
874
- globals: context.globals,
875
- root: buildCommand(root, context),
1082
+ plugins,
876
1083
  };
1084
+ return { extensions, globals: context.globals, root: buildCommand(root, context) };
877
1085
  }
878
1086
  export class CommandBuilder {
1087
+ #name;
879
1088
  #state;
880
- constructor(state) {
1089
+ constructor(name, state) {
1090
+ this.#name = name;
881
1091
  this.#state = state;
882
- nodes.set(this, this);
883
- }
884
- get name() {
885
- return this.#state.name;
1092
+ nodes.set(this, nodeHandle(name, state));
886
1093
  }
887
1094
  argument(name, config) {
888
1095
  const input = {
@@ -890,7 +1097,7 @@ export class CommandBuilder {
890
1097
  kind: 'argument',
891
1098
  name,
892
1099
  };
893
- return this.derive(declareArgument(this.#state, input));
1100
+ return this.#derive(declareArgument(this.#state, input));
894
1101
  }
895
1102
  option(name, config) {
896
1103
  const input = {
@@ -898,66 +1105,61 @@ export class CommandBuilder {
898
1105
  kind: 'option',
899
1106
  name,
900
1107
  };
901
- return this.derive(declareOption(this.#state, input));
1108
+ return this.#derive(declareOption(this.#state, input));
902
1109
  }
903
1110
  /**
904
- * 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
905
1112
  * tuple rest parameter rejects a call that names none.
906
1113
  */
907
1114
  alias(...names) {
908
- return this.derive(declareAlias(this.#state, names));
1115
+ return this.#derive(declareAlias(this.#state, names));
909
1116
  }
910
1117
  /** A child arrives in any type state, because its own action is the call that finished it. */
911
1118
  command(child) {
912
- return this.derive(attachChild(this.#state, child));
1119
+ return this.#derive(attachChild(this.#state, child));
913
1120
  }
914
1121
  /**
915
1122
  * The value this Command produces for its consumer. The type argument is stated by the author,
916
1123
  * so the views record states no type of its own and an omitted argument names none either.
917
1124
  */
918
1125
  result(declaration) {
919
- return this.derive(declareResult(this.#state, 'value', declaration));
1126
+ return this.#derive(declareResult(this.#state, 'value', declaration));
920
1127
  }
921
1128
  /** The same declaration over a sequence, whose type argument is one row. */
922
1129
  rows(declaration) {
923
- return this.derive(declareResult(this.#state, 'rows', declaration));
1130
+ return this.#derive(declareResult(this.#state, 'rows', declaration));
924
1131
  }
925
1132
  /**
926
1133
  * Views after the fact. It merges by key, so an existing name is replaced in place and a new one
927
1134
  * is appended, and `default` names the key core renders when nothing selects another.
928
1135
  */
929
1136
  views(replacements, options) {
930
- return this.derive(declareResultViews(this.#state, replacements, options));
1137
+ return this.#derive(declareResultViews(this.#state, replacements, options));
931
1138
  }
932
1139
  /** The action closes input authoring; `extend()` remains outside this state transition. */
933
1140
  action(handler) {
934
- return this.derive(declareAction(this.#state, handler));
1141
+ return this.#derive(declareAction(this.#state, handler));
935
1142
  }
936
1143
  extend(...values) {
937
- return this.derive(declareExtensions(this.#state, values));
938
- }
939
- build(context) {
940
- return buildCommand(this.#state, context);
1144
+ return this.#derive(declareExtensions(this.#state, values));
941
1145
  }
942
1146
  /**
943
1147
  * The same runtime value in the state the calling method's return type names. Each call states
944
1148
  * its own transition, and the declared result travels with it unless the call replaces it.
945
1149
  */
946
- derive(state) {
947
- return new CommandBuilder(state);
1150
+ #derive(state) {
1151
+ return new _b(this.#name, state);
948
1152
  }
949
1153
  }
950
- /** 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
+ */
951
1159
  class CommandDeclaration extends CommandBuilder {
952
1160
  constructor(name, options) {
953
- super(freshState({
954
- deprecated: options?.deprecated,
955
- description: options?.description,
956
- extensions: options?.extensions,
957
- hidden: options?.hidden,
958
- name,
959
- options,
960
- }));
1161
+ const declared = namedState(name, options);
1162
+ super(declared.name, declared.state);
961
1163
  }
962
1164
  }
963
1165
  /** The public constructor takes a name and one options object, as the Application does. */
@@ -977,14 +1179,20 @@ export function collectInputs(command) {
977
1179
  function candidatesOf(command) {
978
1180
  return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
979
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
+ }
980
1189
  /** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
981
1190
  export function route(root, tokens) {
982
1191
  let command = root;
983
1192
  const path = [];
984
1193
  let index = 0;
985
- while (command.children.size > 0) {
986
- const token = tokens[index];
987
- if (token === undefined || token === '--' || token.startsWith('-')) {
1194
+ for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
1195
+ if (!readsAsChild(command, token)) {
988
1196
  break;
989
1197
  }
990
1198
  const child = command.routes.get(token);
@@ -1005,29 +1213,33 @@ export function route(root, tokens) {
1005
1213
  */
1006
1214
  function bindArguments(command, path, positionals) {
1007
1215
  const values = new Map();
1008
- let index = 0;
1009
- for (const slot of command.arguments) {
1010
- if (slot.variadic) {
1011
- const rest = positionals.slice(index);
1012
- // An empty tail binds nothing, so validation reads it as `[]` or reports the omission.
1013
- if (rest.length > 0) {
1014
- values.set(slot.input, rest);
1015
- }
1016
- 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);
1017
1228
  }
1018
1229
  else {
1019
- const value = positionals[index];
1020
- if (value !== undefined) {
1021
- values.set(slot.input, value);
1022
- index += 1;
1023
- }
1230
+ values.set(slot.input, [token]);
1024
1231
  }
1025
1232
  }
1026
- if (index < positionals.length) {
1027
- throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(index));
1028
- }
1029
1233
  return values;
1030
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
+ }
1031
1243
  /** Consumes the globals table, then routes the remaining bare tokens to a Command. */
1032
1244
  export function routeInvocation(graph, argv) {
1033
1245
  const scan = extractGlobals(graph.globals.options, [...argv]);
@@ -1060,37 +1272,103 @@ function requestOf(command, values, passthrough) {
1060
1272
  });
1061
1273
  }
1062
1274
  /**
1063
- * The phases that run ahead of the chain: the callable check, local parsing, and validation. It
1064
- * answers with the call that dispatches, so the caller records that the action was invoked at the
1065
- * moment it invokes it and no earlier failure reads as a dispatch.
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.
1066
1278
  */
1067
- async function readyDispatch(graph, routed, invocation) {
1068
- const { command, path, scan } = routed;
1279
+ function parseLocal(routed) {
1280
+ const { command, path } = routed;
1069
1281
  const { dispatch } = command;
1070
- // A group answers no invocation of its own, so it fails with the routing errors above it.
1071
- if (!dispatch) {
1072
- 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' };
1298
+ }
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);
1073
1310
  }
1074
- const parsed = parseInputs(command.options, routed.tokens);
1075
- const args = bindArguments(command, path, parsed.positionals);
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;
1076
1349
  const values = await validateValues({
1077
1350
  command: path,
1078
1351
  defaults: invocation.defaults,
1079
1352
  host: invocation.host,
1080
1353
  inputs: { globals: graph.globals.inputs, locals: command.inputs },
1081
- passthrough: parsed.passthrough,
1354
+ passthrough: local.passthrough,
1355
+ plugins: graph.globals.plugins.flatMap((entry) => entry.inputs),
1082
1356
  signal: invocation.signal,
1083
- supplied: { args, options: mergeValues(scan, parsed.options) },
1357
+ sources: filled.sources,
1358
+ supplied: { args: local.args, options: filled.values },
1084
1359
  });
1085
1360
  return {
1086
1361
  dispatch: async (view) => {
1087
1362
  // The channel is built at the boundary, because the view a middleware selected is read there.
1088
1363
  const channel = invocation.channel({ path, result: command.result, view });
1089
1364
  try {
1090
- 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,
1091
1369
  host: invocation.host,
1092
1370
  out: channel.out,
1093
- passthrough: parsed.passthrough,
1371
+ passthrough: local.passthrough,
1094
1372
  signal: invocation.signal,
1095
1373
  style: invocation.style,
1096
1374
  values,
@@ -1112,19 +1390,42 @@ async function readyDispatch(graph, routed, invocation) {
1112
1390
  throw error;
1113
1391
  }
1114
1392
  },
1115
- request: requestOf(command, values, parsed.passthrough),
1393
+ request: requestOf(command, values, local.passthrough),
1116
1394
  };
1117
1395
  }
1118
1396
  /**
1119
1397
  * Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
1120
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.
1121
1401
  */
1122
1402
  export async function prepareDispatch(graph, routed, invocation) {
1123
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
+ }
1124
1420
  try {
1125
- 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 };
1126
1427
  }
1127
1428
  catch (error) {
1128
- return { fault: error, kind: 'held', request: null, result };
1429
+ return held(error);
1129
1430
  }
1130
1431
  }