@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/globals.js CHANGED
@@ -1,8 +1,15 @@
1
+ import { checkEnvBinding, claimVariables } from './bindings.js';
1
2
  import { DeclarationError } from './errors.js';
2
3
  import { buildExtensions } from './extension.js';
3
4
  import { checkDeprecated, checkDescription, checkHidden } from './facts.js';
4
5
  import { compileOptions } from './options.js';
5
6
  const globalSubject = 'the global options';
7
+ /**
8
+ * The presence keys a global option may not declare, in the order its diagnostic names them. A
9
+ * global's validation runs on every Command, plugin Commands included, so a rule that a value must
10
+ * exist belongs to the Commands that read it.
11
+ */
12
+ const omissionRules = ['required', 'validateOmitted'];
6
13
  /** A collision sentence names a plugin first, then the application's globals, then a local. */
7
14
  const ranks = {
8
15
  application: 1,
@@ -57,12 +64,34 @@ function spellingCollision(spelling, first, second) {
57
64
  return new DeclarationError(`Option spelling "${spelling}" is used by ${usedBy(leading)} and ${usedBy(trailing)}. Change one declaration.`);
58
65
  }
59
66
  function emptyGlobals() {
60
- return { bind: () => ({}), inputs: [] };
67
+ return { bind: () => ({}), inputs: [], records: new Map() };
61
68
  }
62
- function declareGlobalOption(state, input) {
69
+ /**
70
+ * One global option's own facts, binding, and extension values, checked at its `globalOption()`
71
+ * call against the Application's descriptors. The rules that pair it with another option belong to
72
+ * the table, which the caller rebuilds with it.
73
+ */
74
+ function declareGlobalOption(state, input, descriptors) {
75
+ // A global option belongs to the application, not to one Command, so its facts read that way.
76
+ const sentence = `Global option "${input.name}"`;
77
+ const rejected = omissionRules.find((key) => key in input.config);
78
+ if (rejected !== undefined) {
79
+ throw new DeclarationError(`${sentence} declares ${rejected}. Remove it; an omitted global option is absent, and a Command that needs its value checks for it.`);
80
+ }
81
+ checkDescription(sentence, input.config.description);
82
+ checkHidden(sentence, input.config.hidden);
83
+ checkDeprecated(sentence, input.config.deprecated);
84
+ checkEnvBinding(sentence, input.config);
85
+ const record = buildExtensions({
86
+ declared: input.config.extensions,
87
+ descriptors,
88
+ subject: { phrase: `on the global option "${input.name}"`, sentence },
89
+ target: 'option',
90
+ });
63
91
  return {
64
92
  bind: (values) => ({ ...state.bind(values), ...values.option(input) }),
65
93
  inputs: [...state.inputs, input],
94
+ records: new Map([...state.records, [input, record]]),
66
95
  };
67
96
  }
68
97
  /**
@@ -70,31 +99,58 @@ function declareGlobalOption(state, input) {
70
99
  * installed plugin's options in installation order. Every collision between the two scopes, by key
71
100
  * or by spelling, is reported here, so the pre-scan meets a table with one owner per name.
72
101
  */
73
- function buildGlobals(node, plugins, build) {
102
+ function globalTable(inputs, plugins) {
74
103
  const names = new Map();
75
104
  const application = { kind: 'application' };
76
- // A global option belongs to the application, not to one Command, so its facts read that way.
77
- for (const input of node.inputs) {
78
- const sentence = `Global option "${input.name}"`;
79
- checkDescription(sentence, input.config.description);
80
- checkHidden(sentence, input.config.hidden);
81
- checkDeprecated(sentence, input.config.deprecated);
82
- build.extensions.set(input, buildExtensions({
83
- declared: input.config.extensions,
84
- descriptors: build.descriptors,
85
- subject: { phrase: `on the global option "${input.name}"`, sentence },
86
- target: 'option',
87
- }));
105
+ for (const input of inputs) {
88
106
  names.set(input.name, application);
89
107
  }
90
- const options = compileOptions(node.inputs, globalSubject);
108
+ const options = compileOptions(inputs, globalSubject);
91
109
  plugins.forEach((installed, order) => {
92
110
  join({ identity: installed.identity, kind: 'plugin', order }, installed.inputs, {
93
111
  names,
94
112
  options,
95
113
  });
96
114
  });
97
- return { bind: node.bind, inputs: node.inputs, names, options, plugins };
115
+ const variables = claimVariables([
116
+ ...boundOptions(inputs, (name) => `global option "${name}"`),
117
+ ...plugins.flatMap((installed) => boundOptions(installed.inputs, (name) => `plugin "${installed.identity}" option "${name}"`)),
118
+ ]);
119
+ return { names, options, variables };
120
+ }
121
+ /** The table one graph build shares, which every earlier call already proved free of collisions. */
122
+ function buildGlobals(node, plugins) {
123
+ return { ...globalTable(node.inputs, plugins), bind: node.bind, inputs: node.inputs, plugins };
124
+ }
125
+ /**
126
+ * One Command's own options against the globals table, compiled for dispatch. The table holds the
127
+ * application's globals and every plugin option, so a local collision reads the same sentence
128
+ * whichever scope on the other side claimed the name, the spelling, or the variable. One
129
+ * invocation's scope is this Command's own options and the table, so a variable binds one option
130
+ * there, while a sibling Command may bind it again.
131
+ */
132
+ function checkLocalOptions(declarations, table, subject) {
133
+ const local = { kind: 'local', subject };
134
+ const application = { kind: 'application' };
135
+ for (const declaration of declarations) {
136
+ const claimed = table.names.get(declaration.name);
137
+ if (claimed) {
138
+ throw keyCollision(declaration.name, claimed, local);
139
+ }
140
+ }
141
+ const options = compileOptions(declarations, subject);
142
+ for (const [spelling, option] of options) {
143
+ const global = table.options.get(spelling);
144
+ if (global) {
145
+ throw spellingCollision(spelling, { name: global.name, owner: table.names.get(global.name) ?? application }, { name: option.name, owner: local });
146
+ }
147
+ }
148
+ claimVariables(boundOptions(declarations, (option) => `${subject} option "${option}"`), table.variables);
149
+ return options;
150
+ }
151
+ /** The options in one list that bind a variable, each named by the phrase its scope gives it. */
152
+ function boundOptions(inputs, site) {
153
+ return inputs.flatMap(({ config, name }) => config.env === undefined ? [] : [{ site: site(name), variable: config.env }]);
98
154
  }
99
155
  /** One plugin's options joining the table the application's globals already hold. */
100
156
  function join(owner, inputs, table) {
@@ -114,4 +170,4 @@ function join(owner, inputs, table) {
114
170
  options.set(spelling, option);
115
171
  }
116
172
  }
117
- export { buildGlobals, declareGlobalOption, emptyGlobals, keyCollision, spellingCollision };
173
+ export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalTable, keyCollision, spellingCollision, };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { Command } from './command.js';
3
3
  export { validationContext, validationContextKey } from './context.js';
4
4
  export { DeclarationError, FatalError, InputError, InternalError, LoomError, MissingValueError, NonCallableCommandError, RepeatedOptionError, ResultError, ShortGroupError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, UsageError, } from './errors.js';
5
5
  export { extension, readExtension } from './extension.js';
6
+ export { locate } from './locate.js';
6
7
  export { incompleteResult, lanes } from './lanes.js';
7
8
  export { override, view } from './view.js';
8
9
  export { plugin } from './plugin.js';
@@ -11,7 +12,7 @@ export type { RenderingPolicy } from './rendering.js';
11
12
  export type { ViewContext } from './types.js';
12
13
  export { pad, style } from './style.js';
13
14
  export { issuePath } from './validation.js';
14
- export type { StandardSchemaV1 } from '@standard-schema/spec';
15
+ export type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
15
16
  export type { ApplicationMethod, ApplicationOptions } from './application.js';
16
17
  export type { ChainOutcome, MiddlewareContext } from './chain.js';
17
18
  export type { InputProblem, ResultFault } from './errors.js';
@@ -19,9 +20,10 @@ export type { AnyExtension, Extension, ExtensionValue } from './extension.js';
19
20
  export type { CommandMethod, CommandOptions } from './command.js';
20
21
  export type { CancellationReason } from './signals.js';
21
22
  export type { IncompleteResult } from './lanes.js';
23
+ export type { WordPosition } from './locate.js';
22
24
  export type { AnyDeclaredView, DeclaredRowView, DeclaredView, DeclaredViewBrand, FailureClass, ViewContribution, ViewOverride, } from './view.js';
23
25
  export type { ArgumentNode, CommandGraph, CommandNode, OptionNode, ResultNode } from './inspect.js';
24
- export type { Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionValues, } from './plugin.js';
26
+ export type { Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionSpellings, PluginOptionValues, SourceAnswer, SourceContext, SourceResolver, } from './plugin.js';
25
27
  export type { Action, ActionArgs, ActionContext, ActionHandler, ActionOptions, ArgumentConfig, AttachedCommand, BooleanOption, CommandAttachHook, ExitCode, Host, InputIdentity, InputTerminal, Out, OptionConfig, OutputTerminal, Request, ResultInput, ResultView, ResultViews, RunOptions, ScalarArgument, StringOption, SuppliedInputs, RowView, RowViews, ValidationContext, VariadicArgument, View, } from './types.js';
26
28
  export type { ApplicationEnvironment, EnvironmentOf, Register, RegisteredEnvironment, } from './environment.js';
27
- export type { Ansi16Color, Ansi256Fallbacks, ColorFallbacks, ConcreteStyle, Style, ThemeMapping, ThemeConstraint, } from './style.js';
29
+ export type { Ansi16Color, Ansi256Fallbacks, ColorFallbacks, ConcreteStyle, ContextualStyle, Style, ThemeMapping, ThemeConstraint, } from './style.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { Command } from './command.js';
3
3
  export { validationContext, validationContextKey } from './context.js';
4
4
  export { DeclarationError, FatalError, InputError, InternalError, LoomError, MissingValueError, NonCallableCommandError, RepeatedOptionError, ResultError, ShortGroupError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, UsageError, } from './errors.js';
5
5
  export { extension, readExtension } from './extension.js';
6
+ export { locate } from './locate.js';
6
7
  export { incompleteResult, lanes } from './lanes.js';
7
8
  export { override, view } from './view.js';
8
9
  export { plugin } from './plugin.js';
package/dist/inspect.d.ts CHANGED
@@ -1,6 +1,11 @@
1
- import type { BuiltGraph } from './command.js';
1
+ import type { BuiltCommand, BuiltGraph } from './command.js';
2
2
  import type { DeclaredResult } from './types.js';
3
- /** One declared argument. `default` wraps the declared value, so an explicit `undefined` shows. */
3
+ /** The plain JSON Schema a validated input publishes, or `null` where the graph holds no shape. */
4
+ type InputSchema = Readonly<Record<string, unknown>> | null;
5
+ /**
6
+ * One declared argument. `default` wraps the declared value, so an explicit `undefined` shows, and
7
+ * `schema` is the input schema its validator publishes through the Standard JSON Schema converter.
8
+ */
4
9
  interface ArgumentNode {
5
10
  readonly name: string;
6
11
  readonly description: string | undefined;
@@ -8,6 +13,7 @@ interface ArgumentNode {
8
13
  readonly variadic: boolean;
9
14
  readonly validated: boolean;
10
15
  readonly validateOmitted: boolean;
16
+ readonly schema: InputSchema;
11
17
  readonly default: {
12
18
  readonly value: unknown;
13
19
  } | undefined;
@@ -21,6 +27,10 @@ interface ArgumentNode {
21
27
  * `hidden` is `false` unless the declaration says `true`, and `deprecated` is the declared
22
28
  * migration message or `undefined`. A listing projection omits a hidden node and marks a
23
29
  * deprecated one; parsing binds without reading either.
30
+ * `schema` is the input schema the validator publishes. A Boolean option validates nothing, so its
31
+ * variant carries the field at `null`, and every projection built on the node holds if a later
32
+ * contract lets it validate.
33
+ * `env` is the variable the option's environment binding names, or `null` when it binds none.
24
34
  */
25
35
  type OptionNode = {
26
36
  readonly type: 'string';
@@ -35,6 +45,8 @@ type OptionNode = {
35
45
  readonly multiple: boolean;
36
46
  readonly validated: boolean;
37
47
  readonly validateOmitted: boolean;
48
+ readonly schema: InputSchema;
49
+ readonly env: string | null;
38
50
  readonly default: {
39
51
  readonly value: unknown;
40
52
  } | undefined;
@@ -50,6 +62,8 @@ type OptionNode = {
50
62
  readonly short: string | null;
51
63
  readonly negative: string | null;
52
64
  readonly polarity: 'positive' | 'negative' | 'both';
65
+ readonly schema: InputSchema;
66
+ readonly env: string | null;
53
67
  readonly extensions: Readonly<Record<string, unknown>>;
54
68
  };
55
69
  /**
@@ -114,5 +128,25 @@ declare function inspectGraph(name: string, graph: BuiltGraph, facts: {
114
128
  description: string | undefined;
115
129
  version: string;
116
130
  }): CommandGraph;
131
+ /**
132
+ * The built graph behind one rendered graph, and each built Command's own node in it. The parser
133
+ * reads the built tables, so a reader of the rendered graph that must parse as the parser does
134
+ * reaches them here.
135
+ */
136
+ interface GraphLink {
137
+ graph: BuiltGraph;
138
+ nodes: WeakMap<BuiltCommand, CommandNode>;
139
+ }
140
+ /**
141
+ * The link of one rendered graph. A graph core did not render has none, which is a caller's
142
+ * programming error rather than an invocation fault.
143
+ */
144
+ declare function linkOf(graph: CommandGraph): GraphLink;
145
+ /**
146
+ * The routed node inside the inspected graph, which routing already proved reachable. A missing
147
+ * segment means the two readings of one graph disagree, so the run stops rather than hand a
148
+ * middleware or a configuration source the wrong Command.
149
+ */
150
+ declare function nodeAt(graph: CommandGraph, path: readonly string[]): CommandNode;
117
151
  export type { ArgumentNode, CommandGraph, CommandNode, OptionNode, ResultNode };
118
- export { inspectGraph, resultNode };
152
+ export { inspectGraph, linkOf, nodeAt, resultNode };
package/dist/inspect.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { InternalError } from './errors.js';
1
2
  import { isPlainObject } from './facts.js';
2
3
  import { validatesOmission } from './validation.js';
3
4
  /** A declaration that carries no extension value publishes one shared, empty frozen record. */
@@ -28,14 +29,60 @@ export function snapshot(value) {
28
29
  return Object.freeze(value.map((entry) => snapshot(entry)));
29
30
  }
30
31
  if (isPlainObject(value)) {
31
- return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshot(entry)])));
32
+ return snapshotRecord(value);
32
33
  }
33
34
  return value;
34
35
  }
36
+ /** The snapshot of one plain object, under the record type the caller already established. */
37
+ function snapshotRecord(value) {
38
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshot(entry)])));
39
+ }
35
40
  /** A declared default is wrapped, so `default: undefined` reads apart from no default at all. */
36
41
  function declaredDefault(config) {
37
42
  return 'default' in config ? Object.freeze({ value: snapshot(config.default) }) : undefined;
38
43
  }
44
+ /**
45
+ * The JSON Schema draft build asks every converter for, with no library options. Every converter
46
+ * receives this one object, so it is frozen against a library that writes to its argument.
47
+ */
48
+ const schemaTarget = Object.freeze({ target: 'draft-2020-12' });
49
+ /** Whether a validator declares the Standard JSON Schema converter, both sides, beside `validate`. */
50
+ function publishesSchema(schema) {
51
+ const props = schema['~standard'];
52
+ return ('jsonSchema' in props &&
53
+ typeof props.jsonSchema === 'object' &&
54
+ props.jsonSchema !== null &&
55
+ 'input' in props.jsonSchema &&
56
+ typeof props.jsonSchema.input === 'function' &&
57
+ 'output' in props.jsonSchema &&
58
+ typeof props.jsonSchema.output === 'function');
59
+ }
60
+ /**
61
+ * The input-side schema a declaration's validator publishes, snapshotted the way a declared
62
+ * default is, or `null` where the graph holds no published shape: no validator, a validator with
63
+ * no converter, or a converter that throws or returns anything but a plain object. The contract of
64
+ * 2026-09-19 made that last case a declaration error `inspect()` alone reports; that diagnostic is
65
+ * held while the question of how a run tells development from a distributed application is
66
+ * decided, so it reads `null` on both paths.
67
+ */
68
+ function inputSchema(config) {
69
+ const schema = 'validate' in config ? config.validate : undefined;
70
+ if (schema === undefined) {
71
+ return null;
72
+ }
73
+ // The converter is the library's code from the first property read.
74
+ // A throw on reaching it and a throw on calling it are one failure.
75
+ try {
76
+ if (!publishesSchema(schema)) {
77
+ return null;
78
+ }
79
+ const published = schema['~standard'].jsonSchema.input(schemaTarget);
80
+ return isPlainObject(published) ? snapshotRecord(published) : null;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
39
86
  /** One declaration's extension record, which is the shared empty one when it carries no value. */
40
87
  function extensionsOf(records, declaration) {
41
88
  return records.get(declaration) ?? noExtensions;
@@ -48,12 +95,14 @@ function optionNode(input, { records, scope, table }) {
48
95
  ? {
49
96
  deprecated: config.deprecated,
50
97
  description: config.description,
98
+ env: config.env ?? null,
51
99
  extensions,
52
100
  hidden: config.hidden === true,
53
101
  long,
54
102
  name,
55
103
  negative,
56
104
  polarity: config.polarity ?? 'positive',
105
+ schema: null,
57
106
  scope,
58
107
  short,
59
108
  type: 'boolean',
@@ -62,6 +111,7 @@ function optionNode(input, { records, scope, table }) {
62
111
  default: declaredDefault(config),
63
112
  deprecated: config.deprecated,
64
113
  description: config.description,
114
+ env: config.env ?? null,
65
115
  extensions,
66
116
  hidden: config.hidden === true,
67
117
  long,
@@ -69,6 +119,7 @@ function optionNode(input, { records, scope, table }) {
69
119
  multiple: config.multiple === true,
70
120
  name,
71
121
  required: config.required === true,
122
+ schema: inputSchema(config),
72
123
  scope,
73
124
  short,
74
125
  type: 'string',
@@ -86,6 +137,7 @@ function argumentNode(slot, records) {
86
137
  extensions: extensionsOf(records, slot.input),
87
138
  name,
88
139
  required: slot.required,
140
+ schema: inputSchema(config),
89
141
  validateOmitted: validatesOmission(slot.input),
90
142
  validated: config.validate !== undefined,
91
143
  variadic: slot.variadic,
@@ -100,11 +152,11 @@ function optionNodes(inputs, read) {
100
152
  * and the root reports the Application's, which is why the caller supplies that one.
101
153
  */
102
154
  function commandNode(command, place) {
103
- const { path, records } = place;
155
+ const { nodes, path, records } = place;
104
156
  const node = {
105
157
  aliases: Object.freeze([...command.aliases]),
106
158
  arguments: Object.freeze(command.arguments.map((slot) => argumentNode(slot, records))),
107
- children: Object.freeze([...command.children].map(([name, child]) => commandNode(child, { path: Object.freeze([...path, name]), records }))),
159
+ children: Object.freeze([...command.children].map(([name, child]) => commandNode(child, { nodes, path: Object.freeze([...path, name]), records }))),
108
160
  deprecated: command.deprecated,
109
161
  description: 'description' in place ? place.description : command.description,
110
162
  extensions: command.extensions,
@@ -115,7 +167,9 @@ function commandNode(command, place) {
115
167
  path,
116
168
  result: resultNode(command.result),
117
169
  };
118
- return Object.freeze(node);
170
+ const frozen = Object.freeze(node);
171
+ nodes.set(command, frozen);
172
+ return frozen;
119
173
  }
120
174
  /** The declared result as plain data, or `null` on a Command that declares none. */
121
175
  function resultNode(result) {
@@ -136,6 +190,7 @@ function resultNode(result) {
136
190
  function inspectGraph(name, graph, facts) {
137
191
  const records = graph.extensions;
138
192
  const table = graph.globals.options;
193
+ const nodes = new WeakMap();
139
194
  const inspected = {
140
195
  description: facts.description,
141
196
  globals: Object.freeze([
@@ -145,11 +200,43 @@ function inspectGraph(name, graph, facts) {
145
200
  name,
146
201
  root: commandNode(graph.root, {
147
202
  description: facts.description,
203
+ nodes,
148
204
  path: Object.freeze([]),
149
205
  records,
150
206
  }),
151
207
  version: facts.version,
152
208
  };
153
- return Object.freeze(inspected);
209
+ const frozen = Object.freeze(inspected);
210
+ links.set(frozen, { graph, nodes });
211
+ return frozen;
212
+ }
213
+ /** Every graph core rendered, keyed by the frozen object a caller holds. */
214
+ const links = new WeakMap();
215
+ /**
216
+ * The link of one rendered graph. A graph core did not render has none, which is a caller's
217
+ * programming error rather than an invocation fault.
218
+ */
219
+ function linkOf(graph) {
220
+ const link = links.get(graph);
221
+ if (!link) {
222
+ throw new InternalError('The graph was not produced by inspect(). Pass the graph inspect() returned.', undefined);
223
+ }
224
+ return link;
225
+ }
226
+ /**
227
+ * The routed node inside the inspected graph, which routing already proved reachable. A missing
228
+ * segment means the two readings of one graph disagree, so the run stops rather than hand a
229
+ * middleware or a configuration source the wrong Command.
230
+ */
231
+ function nodeAt(graph, path) {
232
+ let node = graph.root;
233
+ for (const name of path) {
234
+ const child = node.children.find((entry) => entry.name === name);
235
+ if (!child) {
236
+ throw new InternalError(`The routed command "${path.join(' ')}" is not in the inspected graph.`, undefined);
237
+ }
238
+ node = child;
239
+ }
240
+ return node;
154
241
  }
155
- export { inspectGraph, resultNode };
242
+ export { inspectGraph, linkOf, nodeAt, resultNode };
@@ -0,0 +1,41 @@
1
+ import type { ArgumentNode, CommandGraph, CommandNode, OptionNode } from './inspect.js';
2
+ /**
3
+ * Where the last word of an unfinished invocation sits. Every node is the given graph's own, so a
4
+ * reader compares by identity. `prefix` is the part of the word being completed.
5
+ */
6
+ type WordPosition = {
7
+ readonly kind: 'command';
8
+ readonly command: CommandNode;
9
+ readonly prefix: string;
10
+ } | {
11
+ readonly kind: 'option';
12
+ readonly command: CommandNode;
13
+ readonly prefix: string;
14
+ readonly supplied: readonly string[];
15
+ } | {
16
+ readonly kind: 'value';
17
+ readonly command: CommandNode;
18
+ readonly option: OptionNode;
19
+ readonly lead: string;
20
+ readonly prefix: string;
21
+ } | {
22
+ readonly kind: 'argument';
23
+ readonly command: CommandNode;
24
+ readonly argument: ArgumentNode;
25
+ readonly prefix: string;
26
+ } | {
27
+ readonly kind: 'passthrough';
28
+ readonly command: CommandNode;
29
+ readonly prefix: string;
30
+ } | {
31
+ readonly kind: 'none';
32
+ };
33
+ /**
34
+ * Reads an unfinished invocation against a graph `inspect()` returned and reports where its last
35
+ * word sits. `words` holds the tokens after the application name; an empty list reads as one empty
36
+ * word. It runs no validator, input source, or middleware, and a structural fault among the earlier
37
+ * words reads as `none`.
38
+ */
39
+ declare function locate(graph: CommandGraph, words: readonly string[]): WordPosition;
40
+ export type { WordPosition };
41
+ export { locate };
package/dist/locate.js ADDED
@@ -0,0 +1,121 @@
1
+ import { argumentSlot, readsAsChild, route } from './command.js';
2
+ import { InternalError, UsageError } from './errors.js';
3
+ import { linkOf } from './inspect.js';
4
+ import { isOptionToken, longStringOption, longToken, scanGlobals, scanInputs } from './options.js';
5
+ const none = Object.freeze({ kind: 'none' });
6
+ /**
7
+ * The option names the earlier words supplied, globals and locals merged into token order.
8
+ * Routing consumed the first `offset` rest tokens, so local token `i` is rest token `i + offset`.
9
+ */
10
+ function suppliedOrder(count, scans) {
11
+ const { globals, local, offset } = scans;
12
+ const byToken = Array.from({ length: count }, () => []);
13
+ for (const { name, token } of globals.supplied) {
14
+ byToken[token]?.push(name);
15
+ }
16
+ for (const { name, token } of local.supplied) {
17
+ byToken[globals.positions[token + offset] ?? count]?.push(name);
18
+ }
19
+ return byToken.flat();
20
+ }
21
+ /**
22
+ * The parser's own pre-scan, routing, and local scan over the complete words. An earlier
23
+ * positional that no slot accepts is the unexpected-argument fault, so it reads as no position.
24
+ */
25
+ function readStructure(graph, earlier) {
26
+ const globals = scanGlobals(graph.globals.options, earlier);
27
+ const routed = route(graph.root, globals.rest);
28
+ const local = scanInputs(routed.command.options, routed.tokens);
29
+ const positionals = local.positionals.length;
30
+ if (positionals > 0 && !argumentSlot(routed.command.arguments, positionals - 1)) {
31
+ return undefined;
32
+ }
33
+ return {
34
+ awaiting: globals.awaiting ?? local.awaiting,
35
+ command: routed.command,
36
+ committed: routed.tokens.length > 0,
37
+ delimited: local.delimited,
38
+ positionals,
39
+ supplied: suppliedOrder(earlier.length, { globals, local, offset: routed.path.length }),
40
+ };
41
+ }
42
+ /** Every structural fault the grammar raises is a usage error, and it reads as no position. */
43
+ function readEarlier(graph, earlier) {
44
+ try {
45
+ return readStructure(graph, earlier);
46
+ }
47
+ catch (error) {
48
+ if (error instanceof UsageError) {
49
+ return undefined;
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+ /** The value position of one option in scope: a global, or the routed Command's own. */
55
+ function valueOf(scope, name, word) {
56
+ const { command, graph } = scope;
57
+ const option = graph.globals.find((entry) => entry.name === name) ??
58
+ command.options.find((entry) => entry.name === name);
59
+ if (!option) {
60
+ throw new InternalError(`Option "${name}" is not in the inspected graph.`, undefined);
61
+ }
62
+ return { command, kind: 'value', lead: word.lead, option, prefix: word.prefix };
63
+ }
64
+ /** A long token with an inline value is that option's value when it names a string option. */
65
+ function inlineValue(scope, spelling, inline) {
66
+ const name = longStringOption(scope.built.globals.options, spelling) ??
67
+ longStringOption(scope.earlier.command.options, spelling);
68
+ return name === undefined ? none : valueOf(scope, name, { lead: `${spelling}=`, prefix: inline });
69
+ }
70
+ /** The word after a string option that ended the earlier words is that option's value. */
71
+ function awaitedValue(scope, awaiting, last) {
72
+ return isOptionToken(last) ? none : valueOf(scope, awaiting.name, { lead: '', prefix: last });
73
+ }
74
+ /** A bare word names a child until routing commits, and fills the next positional after. */
75
+ function bareWord(scope, last) {
76
+ const { command, earlier } = scope;
77
+ if (!earlier.committed && readsAsChild(earlier.command, last)) {
78
+ return { command, kind: 'command', prefix: last };
79
+ }
80
+ const slot = argumentSlot(earlier.command.arguments, earlier.positionals);
81
+ const argument = slot && command.arguments[earlier.command.arguments.indexOf(slot)];
82
+ return argument ? { argument, command, kind: 'argument', prefix: last } : none;
83
+ }
84
+ /** An option token: a long token's inline value, or else an option spelling being completed. */
85
+ function optionWord(scope, last) {
86
+ const long = longToken(last);
87
+ if (long?.inline !== undefined) {
88
+ return inlineValue(scope, long.spelling, long.inline);
89
+ }
90
+ return { command: scope.command, kind: 'option', prefix: last, supplied: scope.earlier.supplied };
91
+ }
92
+ /** The last word, read as the parser would read the next token. */
93
+ function lastWord(scope, last) {
94
+ const { command, earlier } = scope;
95
+ if (earlier.delimited) {
96
+ return { command, kind: 'passthrough', prefix: last };
97
+ }
98
+ if (earlier.awaiting) {
99
+ return awaitedValue(scope, earlier.awaiting, last);
100
+ }
101
+ return isOptionToken(last) ? optionWord(scope, last) : bareWord(scope, last);
102
+ }
103
+ /**
104
+ * Reads an unfinished invocation against a graph `inspect()` returned and reports where its last
105
+ * word sits. `words` holds the tokens after the application name; an empty list reads as one empty
106
+ * word. It runs no validator, input source, or middleware, and a structural fault among the earlier
107
+ * words reads as `none`.
108
+ */
109
+ function locate(graph, words) {
110
+ const link = linkOf(graph);
111
+ const earlier = readEarlier(link.graph, words.slice(0, -1));
112
+ if (!earlier) {
113
+ return none;
114
+ }
115
+ const command = link.nodes.get(earlier.command);
116
+ if (!command) {
117
+ throw new InternalError('The routed command is not in the inspected graph.', undefined);
118
+ }
119
+ return lastWord({ built: link.graph, command, earlier, graph }, words.at(-1) ?? '');
120
+ }
121
+ export { locate };