@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/LICENSE +21 -0
- package/dist/application.d.ts +44 -31
- package/dist/application.js +122 -110
- package/dist/bindings.d.ts +26 -0
- package/dist/bindings.js +45 -0
- package/dist/chain.d.ts +11 -6
- package/dist/chain.js +28 -81
- package/dist/command.d.ts +161 -84
- package/dist/command.js +692 -369
- package/dist/errors.d.ts +7 -2
- package/dist/errors.js +9 -1
- package/dist/extension.d.ts +81 -23
- package/dist/extension.js +119 -54
- package/dist/facts.d.ts +5 -0
- package/dist/facts.js +10 -3
- package/dist/globals.d.ts +45 -12
- package/dist/globals.js +74 -18
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -0
- package/dist/inspect.d.ts +37 -3
- package/dist/inspect.js +93 -6
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +73 -0
- package/dist/options.js +124 -27
- package/dist/output.d.ts +2 -0
- package/dist/output.js +5 -1
- package/dist/plugin.d.ts +107 -53
- package/dist/plugin.js +204 -66
- package/dist/sources.d.ts +56 -0
- package/dist/sources.js +249 -0
- package/dist/types.d.ts +70 -20
- package/dist/validation.d.ts +22 -8
- package/dist/validation.js +184 -77
- package/dist/view.d.ts +1 -1
- package/dist/view.js +2 -2
- package/package.json +3 -2
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
|
-
|
|
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
|
|
102
|
+
function globalTable(inputs, plugins) {
|
|
74
103
|
const names = new Map();
|
|
75
104
|
const application = { kind: 'application' };
|
|
76
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/locate.d.ts
ADDED
|
@@ -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 };
|