@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/errors.d.ts CHANGED
@@ -111,8 +111,8 @@ export declare class InternalError extends LoomError {
111
111
  readonly cause: unknown;
112
112
  constructor(message: string, cause: unknown);
113
113
  }
114
- /** The four ways the results lane is broken, each named where core meets it. */
115
- export type ResultFault = 'missing' | 'repeated' | 'undeclared' | 'middleware';
114
+ /** The five ways the results lane is broken, each named where core meets it. */
115
+ export type ResultFault = 'missing' | 'repeated' | 'undeclared' | 'middleware' | 'source';
116
116
  /**
117
117
  * Exit 1: the promise a declared result makes was not kept. It wraps no thrown value, so its
118
118
  * `cause` is `undefined`, and it extends `InternalError`, so an override of that class brands it
@@ -125,6 +125,11 @@ export declare class ResultError extends InternalError {
125
125
  readonly cause: undefined;
126
126
  constructor(kind: ResultFault, path: readonly string[]);
127
127
  }
128
+ /**
129
+ * One message someone else wrote, as a sentence of its own. A schema or a plugin writes its message
130
+ * with or without a full stop, so a diagnostic supplies one only where the message carries none.
131
+ */
132
+ export declare function asSentence(text: string): string;
128
133
  /** What a diagnostic says about an unexpected value, whether or not it was an Error. */
129
134
  export declare function reasonOf(thrown: unknown): string;
130
135
  /**
package/dist/errors.js CHANGED
@@ -14,7 +14,8 @@ function resultMessage(kind, command) {
14
14
  if (kind === 'undeclared') {
15
15
  return `${routedSentence(command)} declares no result. Declare one with result() or rows() before action().`;
16
16
  }
17
- return `A middleware called out.results() on ${routedSubject(command)}. Only the action emits a result.`;
17
+ const caller = kind === 'source' ? 'A configuration source' : 'A middleware';
18
+ return `${caller} called out.results() on ${routedSubject(command)}. Only the action emits a result.`;
18
19
  }
19
20
  /**
20
21
  * The clause that offers the candidates, which is absent when there are none: every child of the
@@ -210,6 +211,13 @@ export class ResultError extends InternalError {
210
211
  this.path = path;
211
212
  }
212
213
  }
214
+ /**
215
+ * One message someone else wrote, as a sentence of its own. A schema or a plugin writes its message
216
+ * with or without a full stop, so a diagnostic supplies one only where the message carries none.
217
+ */
218
+ export function asSentence(text) {
219
+ return text.endsWith('.') ? text : `${text}.`;
220
+ }
213
221
  /** What a diagnostic says about an unexpected value, whether or not it was an Error. */
214
222
  export function reasonOf(thrown) {
215
223
  return thrown instanceof Error ? thrown.message : 'An unknown error occurred.';
@@ -83,6 +83,8 @@ type ExtensionRead<Schema extends StandardSchemaV1, Collect extends boolean> = C
83
83
  * collection order, and an empty list where the node carries none.
84
84
  */
85
85
  declare function readExtension<Target extends ExtensionTarget, Schema extends StandardSchemaV1, Collect extends boolean = false>(node: NodeFor<Target>, descriptor: Extension<Target, Schema, Collect>): ExtensionRead<Schema, Collect>;
86
+ /** How a diagnostic names the declarations one target covers, such as "Commands". */
87
+ declare function appliesTo(target: ExtensionTarget): string;
86
88
  /** The declaration one `extensions` slot belongs to, as its own diagnostics name it. */
87
89
  interface ExtensionSubject {
88
90
  /** The subject after a preposition, such as `on Command "get"`. */
@@ -152,4 +154,4 @@ declare function storeCommandLayers(slot: Omit<ExtensionSlot, 'declared' | 'targ
152
154
  layers: readonly unknown[];
153
155
  }): ExtensionStore;
154
156
  export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionStore, ExtensionSubject, ExtensionTarget, ExtensionValue, };
155
- export { buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
157
+ export { appliesTo, buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
package/dist/extension.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DeclarationError, reasonOf } from './errors.js';
1
+ import { asSentence, DeclarationError, reasonOf } from './errors.js';
2
2
  import { isPlainObject } from './facts.js';
3
3
  /** Authored values register here, so the public brand publishes no state to reach or replace. */
4
4
  const values = new WeakMap();
@@ -67,6 +67,10 @@ const applies = {
67
67
  command: 'Commands',
68
68
  option: 'options',
69
69
  };
70
+ /** How a diagnostic names the declarations one target covers, such as "Commands". */
71
+ function appliesTo(target) {
72
+ return applies[target];
73
+ }
70
74
  /**
71
75
  * The children one container contributes, or nothing when its own shape is not plain data. An
72
76
  * accessor, a symbol key, and a non-enumerable own property are not plain data, and an `undefined`
@@ -231,13 +235,6 @@ function isSchema(value) {
231
235
  'validate' in standard &&
232
236
  typeof standard.validate === 'function');
233
237
  }
234
- /**
235
- * One schema message as a sentence of its own. A schema author writes the message with or without a
236
- * full stop, so the diagnostic supplies one only where the message carries none.
237
- */
238
- function sentence(text) {
239
- return text.endsWith('.') ? text : `${text}.`;
240
- }
241
238
  /** The message one rejected value reports, with the placeholder a silent schema earns. */
242
239
  function issueText(issues) {
243
240
  const first = Array.isArray(issues) ? issues[0] : undefined;
@@ -272,7 +269,7 @@ function validated(subject, carried, schema) {
272
269
  }
273
270
  catch (error) {
274
271
  // A schema that throws rejected the value the only way it could, so it reads as a rejection.
275
- throw new DeclarationError(`${subject.sentence} holds an invalid "${carried.descriptor.identity}" value: ${sentence(reasonOf(error))} Correct the value.`);
272
+ throw new DeclarationError(`${subject.sentence} holds an invalid "${carried.descriptor.identity}" value: ${asSentence(reasonOf(error))} Correct the value.`);
276
273
  }
277
274
  }
278
275
  /** Validates one carried value and answers the plain-data output the node stores under it. */
@@ -284,7 +281,7 @@ function validateValue(subject, carried) {
284
281
  }
285
282
  const issues = 'issues' in result ? result.issues : undefined;
286
283
  if (issues !== undefined) {
287
- throw new DeclarationError(`${subject.sentence} holds an invalid "${identity}" value: ${sentence(issueText(issues))} Correct the value.`);
284
+ throw new DeclarationError(`${subject.sentence} holds an invalid "${identity}" value: ${asSentence(issueText(issues))} Correct the value.`);
288
285
  }
289
286
  const output = plainData('value' in result ? result.value : undefined);
290
287
  if (!output) {
@@ -395,4 +392,4 @@ function storeCommandLayers(slot) {
395
392
  }
396
393
  return store;
397
394
  }
398
- export { buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
395
+ export { appliesTo, buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
package/dist/facts.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * One line of prose: a string that holds a character other than whitespace and no line terminator.
3
+ * Every one-line fact reads this rule, and so does the label a configuration source answers with.
4
+ */
5
+ export declare function isProseLine(value: unknown): value is string;
1
6
  /**
2
7
  * The `description` core fact: one line of prose every projection reads. A value that is not a
3
8
  * string fails the same way a blank one does, because the author reads one rule for one fact.
package/dist/facts.js CHANGED
@@ -10,6 +10,13 @@ const prose = /\P{White_Space}/u;
10
10
  * The seven are LF, VT, FF, CR, NEL, LS, and PS, each of them `White_Space` too.
11
11
  */
12
12
  const lineTerminator = /[\n\v\f\r\u0085\u2028\u2029]/u;
13
+ /**
14
+ * One line of prose: a string that holds a character other than whitespace and no line terminator.
15
+ * Every one-line fact reads this rule, and so does the label a configuration source answers with.
16
+ */
17
+ export function isProseLine(value) {
18
+ return typeof value === 'string' && prose.test(value) && !lineTerminator.test(value);
19
+ }
13
20
  /**
14
21
  * The `description` core fact: one line of prose every projection reads. A value that is not a
15
22
  * string fails the same way a blank one does, because the author reads one rule for one fact.
@@ -19,7 +26,7 @@ export function checkDescription(subject, value) {
19
26
  if (value === undefined) {
20
27
  return undefined;
21
28
  }
22
- if (typeof value !== 'string' || !prose.test(value) || lineTerminator.test(value)) {
29
+ if (!isProseLine(value)) {
23
30
  throw new DeclarationError(`${subject} description must hold a character other than whitespace and no line terminator. Supply a one-line summary.`);
24
31
  }
25
32
  return value;
@@ -48,7 +55,7 @@ export function checkDeprecated(subject, value) {
48
55
  if (value === undefined) {
49
56
  return undefined;
50
57
  }
51
- if (typeof value !== 'string' || !prose.test(value) || lineTerminator.test(value)) {
58
+ if (!isProseLine(value)) {
52
59
  throw new DeclarationError(`${subject} deprecated message must hold a character other than whitespace and no line terminator. Supply a one-line migration path, such as "Use get instead.".`);
53
60
  }
54
61
  return value;
@@ -76,7 +83,7 @@ export function checkVersion(value) {
76
83
  if (value === undefined) {
77
84
  return '0.0.0';
78
85
  }
79
- if (typeof value !== 'string' || !prose.test(value) || lineTerminator.test(value)) {
86
+ if (!isProseLine(value)) {
80
87
  throw new DeclarationError('The Application version must be a string that holds a character other than whitespace and no line terminator. Supply a string such as "1.2.0".');
81
88
  }
82
89
  return value;
package/dist/globals.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import type { BoundOption } from './bindings.js';
1
2
  import { DeclarationError } from './errors.js';
3
+ import type { DescriptorRegistry } from './extension.js';
2
4
  import { compileOptions } from './options.js';
3
- import type { BuiltPlugin, PluginBuild } from './plugin.js';
5
+ import type { BuiltPlugin } from './plugin.js';
4
6
  import type { OptionConfig, OptionValue } from './types.js';
5
7
  import type { InputDeclaration, OptionInput, ValidatedInputs } from './validation.js';
6
8
  /**
@@ -28,30 +30,61 @@ declare function keyCollision(name: string, first: OptionOwner, second: OptionOw
28
30
  /** One spelling claimed twice, whichever two scopes claimed it. */
29
31
  declare function spellingCollision(spelling: string, first: OptionSite, second: OptionSite): DeclarationError;
30
32
  /**
31
- * The compiled global table: one spelling map shared by every Command in the graph. It holds the
33
+ * The globals table the pre-scan reads, with every rule that pairs two of its options settled: one
34
+ * spelling map, the scope that owns each key, and the variable each option binds. It holds the
32
35
  * application's global options and every installed plugin's options, because the pre-scan reads one
33
- * table. `inputs` holds the application's declarations alone, because a plugin option is never
34
- * validated and never reaches an action.
36
+ * table.
35
37
  */
36
- interface BuiltGlobals {
37
- bind: (values: ValidatedInputs) => unknown;
38
- inputs: readonly InputDeclaration[];
38
+ interface GlobalTable {
39
39
  names: ReadonlyMap<string, OptionOwner>;
40
40
  options: ReturnType<typeof compileOptions>;
41
+ /** The variable each option in the table binds, under the phrase a duplicate names it by. */
42
+ variables: ReadonlyMap<string, string>;
43
+ }
44
+ /**
45
+ * The compiled table one graph build shares. `inputs` holds the application's declarations alone,
46
+ * because a plugin option is never validated and never reaches an action.
47
+ */
48
+ interface BuiltGlobals extends GlobalTable {
49
+ bind: (values: ValidatedInputs) => unknown;
50
+ inputs: readonly OptionInput[];
41
51
  plugins: readonly BuiltPlugin[];
42
52
  }
43
- /** The Application's private global declarations and their schema-derived value binder. */
53
+ /** The validated extension record of each declaration, keyed by the declaration itself. */
54
+ type InputRecords = ReadonlyMap<InputDeclaration, Readonly<Record<string, unknown>>>;
55
+ /**
56
+ * The Application's private global declarations, their schema-derived value binder, and the
57
+ * extension record each declaration's own call validated.
58
+ */
44
59
  interface GlobalsState<Globals = unknown> {
45
60
  bind: (values: ValidatedInputs) => Globals;
46
61
  inputs: readonly OptionInput[];
62
+ records: InputRecords;
47
63
  }
48
64
  declare function emptyGlobals(): GlobalsState<{}>;
49
- declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, input: OptionInput<Name, Config>): GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
65
+ /**
66
+ * One global option's own facts, binding, and extension values, checked at its `globalOption()`
67
+ * call against the Application's descriptors. The rules that pair it with another option belong to
68
+ * the table, which the caller rebuilds with it.
69
+ */
70
+ declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, input: OptionInput<Name, Config>, descriptors: DescriptorRegistry): GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
50
71
  /**
51
72
  * The one table the pre-scan reads: the application's global options in authoring order, then each
52
73
  * installed plugin's options in installation order. Every collision between the two scopes, by key
53
74
  * or by spelling, is reported here, so the pre-scan meets a table with one owner per name.
54
75
  */
55
- declare function buildGlobals(node: GlobalsState, plugins: readonly BuiltPlugin[], build: PluginBuild): BuiltGlobals;
56
- export type { BuiltGlobals, GlobalsState, OptionOwner, OptionSite };
57
- export { buildGlobals, declareGlobalOption, emptyGlobals, keyCollision, spellingCollision };
76
+ declare function globalTable(inputs: readonly OptionInput[], plugins: readonly BuiltPlugin[]): GlobalTable;
77
+ /** The table one graph build shares, which every earlier call already proved free of collisions. */
78
+ declare function buildGlobals(node: GlobalsState, plugins: readonly BuiltPlugin[]): BuiltGlobals;
79
+ /**
80
+ * One Command's own options against the globals table, compiled for dispatch. The table holds the
81
+ * application's globals and every plugin option, so a local collision reads the same sentence
82
+ * whichever scope on the other side claimed the name, the spelling, or the variable. One
83
+ * invocation's scope is this Command's own options and the table, so a variable binds one option
84
+ * there, while a sibling Command may bind it again.
85
+ */
86
+ declare function checkLocalOptions(declarations: readonly OptionInput[], table: GlobalTable, subject: string): ReturnType<typeof compileOptions>;
87
+ /** The options in one list that bind a variable, each named by the phrase its scope gives it. */
88
+ declare function boundOptions(inputs: readonly OptionInput[], site: (name: string) => string): BoundOption[];
89
+ export type { BuiltGlobals, GlobalsState, GlobalTable, InputRecords, OptionOwner, OptionSite };
90
+ export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalTable, keyCollision, spellingCollision, };
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';
@@ -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,4 +1,4 @@
1
- import type { BuiltGraph } from './command.js';
1
+ import type { BuiltCommand, BuiltGraph } from './command.js';
2
2
  import type { DeclaredResult } from './types.js';
3
3
  /** The plain JSON Schema a validated input publishes, or `null` where the graph holds no shape. */
4
4
  type InputSchema = Readonly<Record<string, unknown>> | null;
@@ -30,6 +30,7 @@ interface ArgumentNode {
30
30
  * `schema` is the input schema the validator publishes. A Boolean option validates nothing, so its
31
31
  * variant carries the field at `null`, and every projection built on the node holds if a later
32
32
  * contract lets it validate.
33
+ * `env` is the variable the option's environment binding names, or `null` when it binds none.
33
34
  */
34
35
  type OptionNode = {
35
36
  readonly type: 'string';
@@ -45,6 +46,7 @@ type OptionNode = {
45
46
  readonly validated: boolean;
46
47
  readonly validateOmitted: boolean;
47
48
  readonly schema: InputSchema;
49
+ readonly env: string | null;
48
50
  readonly default: {
49
51
  readonly value: unknown;
50
52
  } | undefined;
@@ -61,6 +63,7 @@ type OptionNode = {
61
63
  readonly negative: string | null;
62
64
  readonly polarity: 'positive' | 'negative' | 'both';
63
65
  readonly schema: InputSchema;
66
+ readonly env: string | null;
64
67
  readonly extensions: Readonly<Record<string, unknown>>;
65
68
  };
66
69
  /**
@@ -125,5 +128,25 @@ declare function inspectGraph(name: string, graph: BuiltGraph, facts: {
125
128
  description: string | undefined;
126
129
  version: string;
127
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;
128
151
  export type { ArgumentNode, CommandGraph, CommandNode, OptionNode, ResultNode };
129
- 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. */
@@ -94,6 +95,7 @@ function optionNode(input, { records, scope, table }) {
94
95
  ? {
95
96
  deprecated: config.deprecated,
96
97
  description: config.description,
98
+ env: config.env ?? null,
97
99
  extensions,
98
100
  hidden: config.hidden === true,
99
101
  long,
@@ -109,6 +111,7 @@ function optionNode(input, { records, scope, table }) {
109
111
  default: declaredDefault(config),
110
112
  deprecated: config.deprecated,
111
113
  description: config.description,
114
+ env: config.env ?? null,
112
115
  extensions,
113
116
  hidden: config.hidden === true,
114
117
  long,
@@ -149,11 +152,11 @@ function optionNodes(inputs, read) {
149
152
  * and the root reports the Application's, which is why the caller supplies that one.
150
153
  */
151
154
  function commandNode(command, place) {
152
- const { path, records } = place;
155
+ const { nodes, path, records } = place;
153
156
  const node = {
154
157
  aliases: Object.freeze([...command.aliases]),
155
158
  arguments: Object.freeze(command.arguments.map((slot) => argumentNode(slot, records))),
156
- 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 }))),
157
160
  deprecated: command.deprecated,
158
161
  description: 'description' in place ? place.description : command.description,
159
162
  extensions: command.extensions,
@@ -164,7 +167,9 @@ function commandNode(command, place) {
164
167
  path,
165
168
  result: resultNode(command.result),
166
169
  };
167
- return Object.freeze(node);
170
+ const frozen = Object.freeze(node);
171
+ nodes.set(command, frozen);
172
+ return frozen;
168
173
  }
169
174
  /** The declared result as plain data, or `null` on a Command that declares none. */
170
175
  function resultNode(result) {
@@ -185,6 +190,7 @@ function resultNode(result) {
185
190
  function inspectGraph(name, graph, facts) {
186
191
  const records = graph.extensions;
187
192
  const table = graph.globals.options;
193
+ const nodes = new WeakMap();
188
194
  const inspected = {
189
195
  description: facts.description,
190
196
  globals: Object.freeze([
@@ -194,11 +200,43 @@ function inspectGraph(name, graph, facts) {
194
200
  name,
195
201
  root: commandNode(graph.root, {
196
202
  description: facts.description,
203
+ nodes,
197
204
  path: Object.freeze([]),
198
205
  records,
199
206
  }),
200
207
  version: facts.version,
201
208
  };
202
- 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;
203
241
  }
204
- 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 };