@loomcli/core 0.4.0 → 0.6.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 +60 -31
- package/dist/application.js +356 -149
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +64 -0
- package/dist/chain.d.ts +26 -12
- package/dist/chain.js +59 -92
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +212 -93
- package/dist/command.js +1224 -454
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +115 -26
- package/dist/errors.js +333 -54
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +44 -9
- package/dist/extension.js +147 -65
- package/dist/facts.d.ts +71 -11
- package/dist/facts.js +108 -25
- package/dist/globals.d.ts +63 -22
- package/dist/globals.js +164 -39
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +36 -5
- package/dist/inspect.js +125 -27
- package/dist/lanes.js +1 -1
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +96 -2
- package/dist/options.js +259 -71
- package/dist/output.d.ts +11 -2
- package/dist/output.js +23 -3
- package/dist/plain.d.ts +6 -0
- package/dist/plain.js +12 -0
- package/dist/plugin-rules.d.ts +62 -0
- package/dist/plugin-rules.js +155 -0
- package/dist/plugin.d.ts +121 -55
- package/dist/plugin.js +496 -126
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +58 -0
- package/dist/sources.js +258 -0
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +76 -21
- package/dist/validation.d.ts +65 -10
- package/dist/validation.js +314 -108
- package/dist/view.d.ts +49 -15
- package/dist/view.js +157 -79
- package/package.json +3 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { registerRule } from './diagnostic-text.js';
|
|
2
|
+
/*
|
|
3
|
+
* Core's rules for the declaration faults of arguments, options, validators, defaults, global
|
|
4
|
+
* options, and environment bindings. Each is declared once here and shared by every site that
|
|
5
|
+
* raises it, as a plugin's rules are.
|
|
6
|
+
*/
|
|
7
|
+
/** An option declared with a type other than string or Boolean. */
|
|
8
|
+
const optionType = registerRule('@loomcli/core/option-type', {
|
|
9
|
+
explanation: 'The type decides how the parser reads an option: a string option consumes a value, and a Boolean option consumes none. Core reads no other kind.',
|
|
10
|
+
headline: 'Invalid option type',
|
|
11
|
+
});
|
|
12
|
+
/** A short alias that is not one ASCII letter. */
|
|
13
|
+
const shortAlias = registerRule('@loomcli/core/short-alias', {
|
|
14
|
+
explanation: 'An operator types a short alias as a hyphen and one letter, and several combine into one short group such as -tm, so each is one ASCII letter the parser can split apart.',
|
|
15
|
+
headline: 'Invalid short alias',
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* A yes-or-no declaration key, such as `required`, `hidden`, or an extension descriptor's
|
|
19
|
+
* `collect`, that holds a value other than a Boolean.
|
|
20
|
+
*/
|
|
21
|
+
const flagNotBoolean = registerRule('@loomcli/core/flag-not-boolean', {
|
|
22
|
+
explanation: 'hidden, shortOnly, multiple, required, variadic, validateOmitted, and an extension\'s collect each answer one yes-or-no question about a declaration, so each holds true or false. A value such as the string "false" would read as true.',
|
|
23
|
+
headline: 'Flag not a Boolean',
|
|
24
|
+
});
|
|
25
|
+
/** `shortOnly` on an option that declares no short alias. */
|
|
26
|
+
const shortOnlyWithoutShort = registerRule('@loomcli/core/short-only-without-short', {
|
|
27
|
+
explanation: 'shortOnly removes every long spelling of an option, so an option with no short alias would leave an operator no spelling to type.',
|
|
28
|
+
headline: 'Short only with no short alias',
|
|
29
|
+
});
|
|
30
|
+
/** `multiple` on a Boolean option. */
|
|
31
|
+
const booleanOptionMultiple = registerRule('@loomcli/core/boolean-option-multiple', {
|
|
32
|
+
explanation: 'A Boolean option reports whether its spelling was supplied, so a repeat has no second value to collect. multiple collects each occurrence of a string option into an array.',
|
|
33
|
+
headline: 'Boolean option takes one value',
|
|
34
|
+
});
|
|
35
|
+
/** `polarity` on a string option. */
|
|
36
|
+
const polarityOnString = registerRule('@loomcli/core/polarity-on-string', {
|
|
37
|
+
explanation: 'Polarity chooses which long forms a Boolean option accepts and what its absence means. A string option takes its value from the operator, so it has no polarity.',
|
|
38
|
+
headline: 'Polarity on a string option',
|
|
39
|
+
});
|
|
40
|
+
/** A polarity outside the three settings. */
|
|
41
|
+
const optionPolarity = registerRule('@loomcli/core/option-polarity', {
|
|
42
|
+
explanation: "Polarity chooses a Boolean option's long forms and its absent value from three settings: positive, both, and negative.",
|
|
43
|
+
headline: 'Invalid polarity',
|
|
44
|
+
});
|
|
45
|
+
/** `polarity: 'both'` beside `shortOnly`. */
|
|
46
|
+
const shortOnlyBothPolarities = registerRule('@loomcli/core/short-only-both-polarities', {
|
|
47
|
+
explanation: 'Polarity both gives an option one spelling that turns it on and one that turns it off. shortOnly leaves the option its short alias alone, and one spelling sets one value.',
|
|
48
|
+
headline: 'Both polarities with short only',
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* One spelling that two options in one scope claim, the application's, a plugin's, or one a
|
|
52
|
+
* plugin's hook declared.
|
|
53
|
+
*/
|
|
54
|
+
const spellingTaken = registerRule('@loomcli/core/spelling-taken', {
|
|
55
|
+
explanation: "The parser reads each spelling as one option, and a Command's own options share one invocation with the global options and every installed plugin's options. A spelling two options claim, a short alias or a generated negative form included, would reach only one of them.",
|
|
56
|
+
headline: 'Spelling used twice',
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Two options with one declared name in one scope, the application's, a plugin's, or one a
|
|
60
|
+
* plugin's hook declared.
|
|
61
|
+
*/
|
|
62
|
+
const optionDeclaredTwice = registerRule('@loomcli/core/option-declared-twice', {
|
|
63
|
+
explanation: "An action reads the global options and its Command's own options from one options object, each under its declared name, and the pre-scan reads the global options and every installed plugin's options from one table. Two options with one name in either leave one of them unreadable.",
|
|
64
|
+
headline: 'Option declared twice',
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* An input a plugin's `onCommandAttach` hook declares under a name that an input of the other kind
|
|
68
|
+
* already holds in the Command's scope: an argument under an option's name, or an option under an
|
|
69
|
+
* argument's name.
|
|
70
|
+
*/
|
|
71
|
+
const nameSharedAcrossKinds = registerRule('@loomcli/core/name-shared-across-kinds', {
|
|
72
|
+
explanation: "An onCommandAttach hook adds inputs to a Command whose other inputs the plugin did not declare, so each name a hook declares stays apart from every argument and option in the Command's scope, whichever kind holds it. An author who gives an argument and an option one name does so knowingly, but a hook cannot see the Command's inputs, so a shared name there is an accident the author did not choose.",
|
|
73
|
+
headline: 'Argument and option share a name',
|
|
74
|
+
});
|
|
75
|
+
/** `required` or `validateOmitted` on a global option. */
|
|
76
|
+
const globalPresenceRule = registerRule('@loomcli/core/global-presence-rule', {
|
|
77
|
+
explanation: 'A global option is validated on every Command, the Commands of plugins included, so a rule that its value must exist would fail a Command that never reads it. An omitted global option is absent.',
|
|
78
|
+
headline: 'Presence rule on a global option',
|
|
79
|
+
});
|
|
80
|
+
/** `globalOption()` after the application's own `command()` or `action()`. */
|
|
81
|
+
const globalOptionAfterCommand = registerRule('@loomcli/core/global-option-after-command', {
|
|
82
|
+
explanation: 'A Command attached with command() and the root action read their types from the global options declared before them, so a global option declared later would reach an action whose types never name it.',
|
|
83
|
+
headline: 'Global option after a Command',
|
|
84
|
+
});
|
|
85
|
+
/** An environment binding on a multiple option. */
|
|
86
|
+
const envOnMultiple = registerRule('@loomcli/core/env-on-multiple', {
|
|
87
|
+
explanation: 'A variable holds one string, and core never splits it, so it cannot supply the several values a multiple option collects. The configuration source supplies a list.',
|
|
88
|
+
headline: 'Environment binding on a list',
|
|
89
|
+
});
|
|
90
|
+
/** An environment binding whose name is outside the variable name grammar. */
|
|
91
|
+
const envName = registerRule('@loomcli/core/env-name', {
|
|
92
|
+
explanation: 'The input-source stage reads the variable an option binds by its name, and a shell sets a variable only under a name of letters, digits, and underscores that does not start with a digit.',
|
|
93
|
+
headline: 'Invalid variable name',
|
|
94
|
+
});
|
|
95
|
+
/** An environment binding on an argument. */
|
|
96
|
+
const envOnArgument = registerRule('@loomcli/core/env-on-argument', {
|
|
97
|
+
explanation: 'An argument is identified by its place among the bare tokens, so no variable can stand in for it. An option names itself on the command line, which lets a variable fill it.',
|
|
98
|
+
headline: 'Environment binding on an argument',
|
|
99
|
+
});
|
|
100
|
+
/** One variable that two options in one invocation's scope bind. */
|
|
101
|
+
const variableBoundTwice = registerRule('@loomcli/core/variable-bound-twice', {
|
|
102
|
+
explanation: "Within one invocation's scope a variable fills one option, so two options that bind it would both take the value an operator set for one of them.",
|
|
103
|
+
headline: 'Variable bound twice',
|
|
104
|
+
});
|
|
105
|
+
/** `validateOmitted` on a declaration whose absence another rule already decides. */
|
|
106
|
+
const omissionAlreadyDecided = registerRule('@loomcli/core/omission-already-decided', {
|
|
107
|
+
explanation: 'validateOmitted sends an omitted value to its validator, which only an input with no other absence rule needs. An omitted required input fails, a default fills an omitted value, and an omitted multiple option or variadic argument receives an empty array.',
|
|
108
|
+
headline: 'Absence already decided',
|
|
109
|
+
});
|
|
110
|
+
/** `validateOmitted` on a declaration with no validator. */
|
|
111
|
+
const omissionWithoutValidator = registerRule('@loomcli/core/omission-without-validator', {
|
|
112
|
+
explanation: "validateOmitted sends an omitted value to the input's validator, so an input with no validator has nothing to receive it.",
|
|
113
|
+
headline: 'Omission with no validator',
|
|
114
|
+
});
|
|
115
|
+
/** `validate`, `default`, `required`, or `validateOmitted` on a Boolean option. */
|
|
116
|
+
const booleanOptionValueRule = registerRule('@loomcli/core/boolean-option-value-rule', {
|
|
117
|
+
explanation: 'A Boolean option consumes no value, so there is nothing to validate, and its polarity decides the value an absent option reads. validate, default, required, and validateOmitted belong to inputs that take a value.',
|
|
118
|
+
headline: 'Value rule on a Boolean option',
|
|
119
|
+
});
|
|
120
|
+
/** A required input that also declares a default. */
|
|
121
|
+
const requiredWithDefault = registerRule('@loomcli/core/required-with-default', {
|
|
122
|
+
explanation: 'A default fills an omitted value, and a required input fails when it is omitted, so a required input never reads its default.',
|
|
123
|
+
headline: 'Default on a required input',
|
|
124
|
+
});
|
|
125
|
+
/** A `validate` value that is not a Standard Schema v1 object. */
|
|
126
|
+
const notAValidator = registerRule('@loomcli/core/not-a-validator', {
|
|
127
|
+
explanation: "Core validates every value through the Standard Schema v1 interface: the object's ~standard property, with version 1, a vendor, and a validate function. It calls nothing else.",
|
|
128
|
+
headline: 'Not a Standard Schema',
|
|
129
|
+
});
|
|
130
|
+
/** A default of the wrong raw shape for its declaration. */
|
|
131
|
+
const defaultShape = registerRule('@loomcli/core/default-shape', {
|
|
132
|
+
explanation: "A default stands in for the value an operator would supply. Without a validator it is that raw value, a string or, for an input that takes several values, an array of strings; with one, it is the validator's input, and an input that takes several values still takes an array of them.",
|
|
133
|
+
headline: 'Default of the wrong shape',
|
|
134
|
+
});
|
|
135
|
+
/** A declared default its validator rejected. */
|
|
136
|
+
const invalidDefault = registerRule('@loomcli/core/invalid-default', {
|
|
137
|
+
explanation: 'Each run passes every declared default through its validator before it reads a token, because a default reaches the action as a validated value. A default the validator rejects would reach no action, whatever the operator supplies.',
|
|
138
|
+
headline: 'Default rejected',
|
|
139
|
+
});
|
|
140
|
+
/** A validator's JSON Schema converter that threw or returned a value that is not a plain object. */
|
|
141
|
+
const schemaConverterFailed = registerRule('@loomcli/core/schema-converter-failed', {
|
|
142
|
+
explanation: "Help, the manifest, and completion read what an input accepts from the JSON Schema its validator publishes. A converter that throws or returns anything but a plain object publishes no shape, so a distributed build reads the input's schema as null.",
|
|
143
|
+
headline: 'Schema converter failed',
|
|
144
|
+
});
|
|
145
|
+
export { booleanOptionMultiple, booleanOptionValueRule, defaultShape, envName, envOnArgument, envOnMultiple, flagNotBoolean, globalOptionAfterCommand, globalPresenceRule, invalidDefault, notAValidator, omissionAlreadyDecided, nameSharedAcrossKinds, omissionWithoutValidator, optionDeclaredTwice, optionPolarity, optionType, polarityOnString, requiredWithDefault, schemaConverterFailed, shortAlias, shortOnlyBothPolarities, shortOnlyWithoutShort, spellingTaken, variableBoundTwice, };
|
package/dist/inspect.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BuiltGraph } from './command.js';
|
|
1
|
+
import type { BuiltCommand, BuiltGraph } from './command.js';
|
|
2
|
+
import { InternalError } from './errors.js';
|
|
2
3
|
import type { DeclaredResult } from './types.js';
|
|
3
4
|
/** The plain JSON Schema a validated input publishes, or `null` where the graph holds no shape. */
|
|
4
5
|
type InputSchema = Readonly<Record<string, unknown>> | null;
|
|
@@ -30,6 +31,7 @@ interface ArgumentNode {
|
|
|
30
31
|
* `schema` is the input schema the validator publishes. A Boolean option validates nothing, so its
|
|
31
32
|
* variant carries the field at `null`, and every projection built on the node holds if a later
|
|
32
33
|
* contract lets it validate.
|
|
34
|
+
* `env` is the variable the option's environment binding names, or `null` when it binds none.
|
|
33
35
|
*/
|
|
34
36
|
type OptionNode = {
|
|
35
37
|
readonly type: 'string';
|
|
@@ -45,6 +47,7 @@ type OptionNode = {
|
|
|
45
47
|
readonly validated: boolean;
|
|
46
48
|
readonly validateOmitted: boolean;
|
|
47
49
|
readonly schema: InputSchema;
|
|
50
|
+
readonly env: string | null;
|
|
48
51
|
readonly default: {
|
|
49
52
|
readonly value: unknown;
|
|
50
53
|
} | undefined;
|
|
@@ -61,6 +64,7 @@ type OptionNode = {
|
|
|
61
64
|
readonly negative: string | null;
|
|
62
65
|
readonly polarity: 'positive' | 'negative' | 'both';
|
|
63
66
|
readonly schema: InputSchema;
|
|
67
|
+
readonly env: string | null;
|
|
64
68
|
readonly extensions: Readonly<Record<string, unknown>>;
|
|
65
69
|
};
|
|
66
70
|
/**
|
|
@@ -117,13 +121,40 @@ export declare function snapshot(value: unknown): unknown;
|
|
|
117
121
|
/** The declared result as plain data, or `null` on a Command that declares none. */
|
|
118
122
|
declare function resultNode(result: DeclaredResult | undefined): ResultNode | null;
|
|
119
123
|
/**
|
|
120
|
-
* Renders one built graph as frozen plain data. Nothing here reads a host fact
|
|
121
|
-
*
|
|
122
|
-
*
|
|
124
|
+
* Renders one built graph as frozen plain data. Nothing here reads a host fact; each validated
|
|
125
|
+
* input's converter is asked for its input schema, and in a development build a converter that
|
|
126
|
+
* fails is a declaration fault. The globals list holds the application's own options, then each
|
|
127
|
+
* installed plugin's in installation order, which is the order the globals table holds them in.
|
|
123
128
|
*/
|
|
124
129
|
declare function inspectGraph(name: string, graph: BuiltGraph, facts: {
|
|
125
130
|
description: string | undefined;
|
|
131
|
+
development: boolean;
|
|
126
132
|
version: string;
|
|
127
133
|
}): CommandGraph;
|
|
134
|
+
/**
|
|
135
|
+
* The built graph behind one rendered graph, and each built Command's own node in it. The parser
|
|
136
|
+
* reads the built tables, so a reader of the rendered graph that must parse as the parser does
|
|
137
|
+
* reaches them here.
|
|
138
|
+
*/
|
|
139
|
+
interface GraphLink {
|
|
140
|
+
graph: BuiltGraph;
|
|
141
|
+
nodes: WeakMap<BuiltCommand, CommandNode>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The link of one rendered graph. A graph core did not render has none, which is a caller's
|
|
145
|
+
* programming error rather than an invocation fault.
|
|
146
|
+
*/
|
|
147
|
+
declare function linkOf(graph: CommandGraph): GraphLink;
|
|
148
|
+
/**
|
|
149
|
+
* The defect a graph reports when its nodes disagree with the build it was rendered from, so a
|
|
150
|
+
* node core looked up is missing.
|
|
151
|
+
*/
|
|
152
|
+
declare function graphMismatch(sentence: string): InternalError;
|
|
153
|
+
/**
|
|
154
|
+
* The routed node inside the inspected graph, which routing already proved reachable. A missing
|
|
155
|
+
* segment means the two readings of one graph disagree, so the run stops rather than hand a
|
|
156
|
+
* middleware or a configuration source the wrong Command.
|
|
157
|
+
*/
|
|
158
|
+
declare function nodeAt(graph: CommandGraph, path: readonly string[]): CommandNode;
|
|
128
159
|
export type { ArgumentNode, CommandGraph, CommandNode, OptionNode, ResultNode };
|
|
129
|
-
export { inspectGraph, resultNode };
|
|
160
|
+
export { graphMismatch, inspectGraph, linkOf, nodeAt, resultNode };
|
package/dist/inspect.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { asSentence, DeclarationError, InternalError, reasonOf } from './errors.js';
|
|
2
|
+
import { partOf, siteFinding } from './facts.js';
|
|
3
|
+
import { schemaConverterFailed } from './input-rules.js';
|
|
4
|
+
import { isPlainObject } from './plain.js';
|
|
5
|
+
import { foreignGraph, foreignGraphCorrection } from './rules.js';
|
|
6
|
+
import { declaringSite, inputPlace, validatesOmission } from './validation.js';
|
|
3
7
|
/** A declaration that carries no extension value publishes one shared, empty frozen record. */
|
|
4
8
|
const noExtensions = Object.freeze({});
|
|
5
9
|
/**
|
|
@@ -56,37 +60,68 @@ function publishesSchema(schema) {
|
|
|
56
60
|
'output' in props.jsonSchema &&
|
|
57
61
|
typeof props.jsonSchema.output === 'function');
|
|
58
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The converter fault a development build reports for one input, with the way it failed and, when
|
|
65
|
+
* the converter threw, the thrown value as its cause.
|
|
66
|
+
*/
|
|
67
|
+
function converterFault(input, check, failed) {
|
|
68
|
+
const site = declaringSite(input, inputPlace(input, check));
|
|
69
|
+
const parts = {
|
|
70
|
+
correction: 'Fix the converter so it returns a JSON Schema object, or declare a validator that publishes none.',
|
|
71
|
+
findings: [siteFinding(site, partOf(site, 'validate'))],
|
|
72
|
+
sentence: `${site.subject} validator's JSON Schema converter ${failed.failure}`,
|
|
73
|
+
};
|
|
74
|
+
return 'cause' in failed
|
|
75
|
+
? new DeclarationError(schemaConverterFailed, parts, { cause: failed.cause })
|
|
76
|
+
: new DeclarationError(schemaConverterFailed, parts);
|
|
77
|
+
}
|
|
59
78
|
/**
|
|
60
79
|
* The input-side schema a declaration's validator publishes, snapshotted the way a declared
|
|
61
80
|
* default is, or `null` where the graph holds no published shape: no validator, a validator with
|
|
62
|
-
* no converter, or a converter that throws or returns anything but a plain
|
|
63
|
-
*
|
|
64
|
-
* held while the question of how a run tells development from a distributed application is
|
|
65
|
-
* decided, so it reads `null` on both paths.
|
|
81
|
+
* no converter, or, in a distributed build, a converter that throws or returns anything but a plain
|
|
82
|
+
* object. A development build reports that last case as a declaration fault instead.
|
|
66
83
|
*/
|
|
67
|
-
function inputSchema(
|
|
84
|
+
function inputSchema(input, check) {
|
|
85
|
+
const { config } = input;
|
|
68
86
|
const schema = 'validate' in config ? config.validate : undefined;
|
|
69
87
|
if (schema === undefined) {
|
|
70
88
|
return null;
|
|
71
89
|
}
|
|
72
|
-
|
|
73
|
-
//
|
|
90
|
+
let copied = undefined;
|
|
91
|
+
// The converter is the library's code from the first property read to the last key of its answer.
|
|
92
|
+
// A throw on reaching it, on calling it, or on reading what it answered is one failure.
|
|
74
93
|
try {
|
|
75
94
|
if (!publishesSchema(schema)) {
|
|
76
95
|
return null;
|
|
77
96
|
}
|
|
78
97
|
const published = schema['~standard'].jsonSchema.input(schemaTarget);
|
|
79
|
-
|
|
98
|
+
copied = isPlainObject(published) ? snapshotRecord(published) : undefined;
|
|
80
99
|
}
|
|
81
|
-
catch {
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (check.development) {
|
|
102
|
+
throw converterFault(input, check, {
|
|
103
|
+
cause: error,
|
|
104
|
+
failure: `failed for target "${schemaTarget.target}": ${asSentence(reasonOf(error))}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
82
107
|
return null;
|
|
83
108
|
}
|
|
109
|
+
if (copied !== undefined) {
|
|
110
|
+
return copied;
|
|
111
|
+
}
|
|
112
|
+
if (check.development) {
|
|
113
|
+
throw converterFault(input, check, {
|
|
114
|
+
failure: `answered target "${schemaTarget.target}" with a value that is not a plain object.`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
84
118
|
}
|
|
85
119
|
/** One declaration's extension record, which is the shared empty one when it carries no value. */
|
|
86
120
|
function extensionsOf(records, declaration) {
|
|
87
121
|
return records.get(declaration) ?? noExtensions;
|
|
88
122
|
}
|
|
89
|
-
function optionNode(input,
|
|
123
|
+
function optionNode(input, read) {
|
|
124
|
+
const { check, records, scope, table } = read;
|
|
90
125
|
const { config, name } = input;
|
|
91
126
|
const { long, negative, short } = spellingsOf(table, name);
|
|
92
127
|
const extensions = extensionsOf(records, input);
|
|
@@ -94,6 +129,7 @@ function optionNode(input, { records, scope, table }) {
|
|
|
94
129
|
? {
|
|
95
130
|
deprecated: config.deprecated,
|
|
96
131
|
description: config.description,
|
|
132
|
+
env: config.env ?? null,
|
|
97
133
|
extensions,
|
|
98
134
|
hidden: config.hidden === true,
|
|
99
135
|
long,
|
|
@@ -109,6 +145,7 @@ function optionNode(input, { records, scope, table }) {
|
|
|
109
145
|
default: declaredDefault(config),
|
|
110
146
|
deprecated: config.deprecated,
|
|
111
147
|
description: config.description,
|
|
148
|
+
env: config.env ?? null,
|
|
112
149
|
extensions,
|
|
113
150
|
hidden: config.hidden === true,
|
|
114
151
|
long,
|
|
@@ -116,7 +153,7 @@ function optionNode(input, { records, scope, table }) {
|
|
|
116
153
|
multiple: config.multiple === true,
|
|
117
154
|
name,
|
|
118
155
|
required: config.required === true,
|
|
119
|
-
schema: inputSchema(
|
|
156
|
+
schema: inputSchema(input, check),
|
|
120
157
|
scope,
|
|
121
158
|
short,
|
|
122
159
|
type: 'string',
|
|
@@ -126,7 +163,8 @@ function optionNode(input, { records, scope, table }) {
|
|
|
126
163
|
return Object.freeze(node);
|
|
127
164
|
}
|
|
128
165
|
/** The built slots already answer presence and arity, so the node repeats no config reading. */
|
|
129
|
-
function argumentNode(slot,
|
|
166
|
+
function argumentNode(slot, read) {
|
|
167
|
+
const { check, records } = read;
|
|
130
168
|
const { config, name } = slot.input;
|
|
131
169
|
const node = {
|
|
132
170
|
default: declaredDefault(config),
|
|
@@ -134,7 +172,7 @@ function argumentNode(slot, records) {
|
|
|
134
172
|
extensions: extensionsOf(records, slot.input),
|
|
135
173
|
name,
|
|
136
174
|
required: slot.required,
|
|
137
|
-
schema: inputSchema(
|
|
175
|
+
schema: inputSchema(slot.input, check),
|
|
138
176
|
validateOmitted: validatesOmission(slot.input),
|
|
139
177
|
validated: config.validate !== undefined,
|
|
140
178
|
variadic: slot.variadic,
|
|
@@ -149,22 +187,30 @@ function optionNodes(inputs, read) {
|
|
|
149
187
|
* and the root reports the Application's, which is why the caller supplies that one.
|
|
150
188
|
*/
|
|
151
189
|
function commandNode(command, place) {
|
|
152
|
-
const { path, records } = place;
|
|
190
|
+
const { development, nodes, path, records } = place;
|
|
191
|
+
const check = { development, global: false, path };
|
|
153
192
|
const node = {
|
|
154
193
|
aliases: Object.freeze([...command.aliases]),
|
|
155
|
-
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 }))),
|
|
194
|
+
arguments: Object.freeze(command.arguments.map((slot) => argumentNode(slot, { check, records }))),
|
|
195
|
+
children: Object.freeze([...command.children].map(([name, child]) => commandNode(child, { development, nodes, path: Object.freeze([...path, name]), records }))),
|
|
157
196
|
deprecated: command.deprecated,
|
|
158
197
|
description: 'description' in place ? place.description : command.description,
|
|
159
198
|
extensions: command.extensions,
|
|
160
199
|
hasAction: command.dispatch !== undefined,
|
|
161
200
|
hidden: command.hidden,
|
|
162
201
|
name: command.name,
|
|
163
|
-
options: Object.freeze(optionNodes(command.inputs, {
|
|
202
|
+
options: Object.freeze(optionNodes(command.inputs, {
|
|
203
|
+
check,
|
|
204
|
+
records,
|
|
205
|
+
scope: 'application',
|
|
206
|
+
table: command.options,
|
|
207
|
+
})),
|
|
164
208
|
path,
|
|
165
209
|
result: resultNode(command.result),
|
|
166
210
|
};
|
|
167
|
-
|
|
211
|
+
const frozen = Object.freeze(node);
|
|
212
|
+
nodes.set(command, frozen);
|
|
213
|
+
return frozen;
|
|
168
214
|
}
|
|
169
215
|
/** The declared result as plain data, or `null` on a Command that declares none. */
|
|
170
216
|
function resultNode(result) {
|
|
@@ -178,27 +224,79 @@ function resultNode(result) {
|
|
|
178
224
|
});
|
|
179
225
|
}
|
|
180
226
|
/**
|
|
181
|
-
* Renders one built graph as frozen plain data. Nothing here reads a host fact
|
|
182
|
-
*
|
|
183
|
-
*
|
|
227
|
+
* Renders one built graph as frozen plain data. Nothing here reads a host fact; each validated
|
|
228
|
+
* input's converter is asked for its input schema, and in a development build a converter that
|
|
229
|
+
* fails is a declaration fault. The globals list holds the application's own options, then each
|
|
230
|
+
* installed plugin's in installation order, which is the order the globals table holds them in.
|
|
184
231
|
*/
|
|
185
232
|
function inspectGraph(name, graph, facts) {
|
|
233
|
+
const { development } = facts;
|
|
186
234
|
const records = graph.extensions;
|
|
187
235
|
const table = graph.globals.options;
|
|
236
|
+
const nodes = new WeakMap();
|
|
237
|
+
const check = { development, global: true, path: [] };
|
|
188
238
|
const inspected = {
|
|
189
239
|
description: facts.description,
|
|
190
240
|
globals: Object.freeze([
|
|
191
|
-
...optionNodes(graph.globals.inputs, { records, scope: 'application', table }),
|
|
192
|
-
...graph.globals.plugins.flatMap((installed) => optionNodes(installed.inputs, { records, scope: 'plugin', table })),
|
|
241
|
+
...optionNodes(graph.globals.inputs, { check, records, scope: 'application', table }),
|
|
242
|
+
...graph.globals.plugins.flatMap((installed) => optionNodes(installed.inputs, { check, records, scope: 'plugin', table })),
|
|
193
243
|
]),
|
|
194
244
|
name,
|
|
195
245
|
root: commandNode(graph.root, {
|
|
196
246
|
description: facts.description,
|
|
247
|
+
development,
|
|
248
|
+
nodes,
|
|
197
249
|
path: Object.freeze([]),
|
|
198
250
|
records,
|
|
199
251
|
}),
|
|
200
252
|
version: facts.version,
|
|
201
253
|
};
|
|
202
|
-
|
|
254
|
+
const frozen = Object.freeze(inspected);
|
|
255
|
+
links.set(frozen, { graph, nodes });
|
|
256
|
+
return frozen;
|
|
257
|
+
}
|
|
258
|
+
/** Every graph core rendered, keyed by the frozen object a caller holds. */
|
|
259
|
+
const links = new WeakMap();
|
|
260
|
+
/**
|
|
261
|
+
* The link of one rendered graph. A graph core did not render has none, which is a caller's
|
|
262
|
+
* programming error rather than an invocation fault.
|
|
263
|
+
*/
|
|
264
|
+
function linkOf(graph) {
|
|
265
|
+
const link = links.get(graph);
|
|
266
|
+
if (!link) {
|
|
267
|
+
throw new InternalError(foreignGraph, {
|
|
268
|
+
cause: undefined,
|
|
269
|
+
correction: 'Pass the graph inspect() returned.',
|
|
270
|
+
sentence: 'The graph was not produced by inspect().',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return link;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The defect a graph reports when its nodes disagree with the build it was rendered from, so a
|
|
277
|
+
* node core looked up is missing.
|
|
278
|
+
*/
|
|
279
|
+
function graphMismatch(sentence) {
|
|
280
|
+
return new InternalError(foreignGraph, {
|
|
281
|
+
cause: undefined,
|
|
282
|
+
correction: foreignGraphCorrection,
|
|
283
|
+
sentence,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The routed node inside the inspected graph, which routing already proved reachable. A missing
|
|
288
|
+
* segment means the two readings of one graph disagree, so the run stops rather than hand a
|
|
289
|
+
* middleware or a configuration source the wrong Command.
|
|
290
|
+
*/
|
|
291
|
+
function nodeAt(graph, path) {
|
|
292
|
+
let node = graph.root;
|
|
293
|
+
for (const name of path) {
|
|
294
|
+
const child = node.children.find((entry) => entry.name === name);
|
|
295
|
+
if (!child) {
|
|
296
|
+
throw graphMismatch(`The routed command "${path.join(' ')}" is not in the inspected graph.`);
|
|
297
|
+
}
|
|
298
|
+
node = child;
|
|
299
|
+
}
|
|
300
|
+
return node;
|
|
203
301
|
}
|
|
204
|
-
export { inspectGraph, resultNode };
|
|
302
|
+
export { graphMismatch, inspectGraph, linkOf, nodeAt, resultNode };
|
package/dist/lanes.js
CHANGED
|
@@ -2,7 +2,7 @@ import { routedSubject } from './errors.js';
|
|
|
2
2
|
import { glyph } from './glyphs.generated.js';
|
|
3
3
|
import { view } from './view.js';
|
|
4
4
|
/**
|
|
5
|
-
* The identity prefix core's own views take, the package name by the plugin
|
|
5
|
+
* The identity prefix core's own views take, the package name by the plugin identity convention.
|
|
6
6
|
* Core compiles from `src` alone, so the name is spelled here rather than read from the manifest.
|
|
7
7
|
*/
|
|
8
8
|
const core = '@loomcli/core';
|
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 { UsageError } from './errors.js';
|
|
3
|
+
import { graphMismatch, 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 graphMismatch(`Option "${name}" is not in the inspected graph.`);
|
|
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 graphMismatch('The routed command is not in the inspected graph.');
|
|
118
|
+
}
|
|
119
|
+
return lastWord({ built: link.graph, command, earlier, graph }, words.at(-1) ?? '');
|
|
120
|
+
}
|
|
121
|
+
export { locate };
|