@loomcli/core 0.6.0 → 0.8.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.
Files changed (52) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +11 -6
  3. package/dist/application.js +57 -19
  4. package/dist/capture.d.ts +65 -0
  5. package/dist/capture.js +99 -0
  6. package/dist/chain.d.ts +28 -17
  7. package/dist/chain.js +11 -10
  8. package/dist/command-rules.d.ts +1 -1
  9. package/dist/command-rules.js +2 -2
  10. package/dist/command.d.ts +31 -47
  11. package/dist/command.js +204 -209
  12. package/dist/errors.d.ts +22 -21
  13. package/dist/errors.js +38 -18
  14. package/dist/facts.d.ts +5 -0
  15. package/dist/facts.js +8 -1
  16. package/dist/globals.d.ts +48 -22
  17. package/dist/globals.js +72 -29
  18. package/dist/glyphs.generated.js +1 -1
  19. package/dist/index.d.ts +5 -3
  20. package/dist/index.js +3 -1
  21. package/dist/input-rules.d.ts +20 -2
  22. package/dist/input-rules.js +47 -5
  23. package/dist/inspect.d.ts +33 -17
  24. package/dist/inspect.js +74 -87
  25. package/dist/locate.js +52 -60
  26. package/dist/options.d.ts +101 -83
  27. package/dist/options.js +311 -267
  28. package/dist/parse.d.ts +162 -0
  29. package/dist/parse.js +601 -0
  30. package/dist/plain.d.ts +52 -2
  31. package/dist/plain.js +228 -2
  32. package/dist/plugin-rules.d.ts +8 -4
  33. package/dist/plugin-rules.js +13 -9
  34. package/dist/plugin-settings.d.ts +18 -0
  35. package/dist/plugin-settings.js +38 -0
  36. package/dist/plugin.d.ts +32 -27
  37. package/dist/plugin.js +107 -89
  38. package/dist/sources.d.ts +18 -9
  39. package/dist/sources.js +50 -20
  40. package/dist/style-layout.js +2 -2
  41. package/dist/style-width.d.ts +13 -0
  42. package/dist/style-width.js +170 -0
  43. package/dist/types.d.ts +70 -30
  44. package/dist/unicode.generated.d.ts +27 -0
  45. package/dist/unicode.generated.js +1036 -0
  46. package/dist/validation.d.ts +108 -34
  47. package/dist/validation.js +282 -125
  48. package/dist/view.d.ts +16 -11
  49. package/dist/view.js +13 -4
  50. package/licenses/unicode-LICENSE.txt +41 -0
  51. package/licenses/uucode-LICENSE.md +35 -0
  52. package/package.json +9 -5
@@ -4,9 +4,9 @@ import { registerRule } from './diagnostic-text.js';
4
4
  * options, and environment bindings. Each is declared once here and shared by every site that
5
5
  * raises it, as a plugin's rules are.
6
6
  */
7
- /** An option declared with a type other than string or Boolean. */
7
+ /** An option declared with a type other than string, Boolean, or count. */
8
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.',
9
+ explanation: 'The type decides how the parser reads an option: a string option consumes a value, a Boolean option consumes none, and a counted option consumes none and counts its occurrences. Core reads no other kind.',
10
10
  headline: 'Invalid option type',
11
11
  });
12
12
  /** A short alias that is not one ASCII letter. */
@@ -27,11 +27,36 @@ const shortOnlyWithoutShort = registerRule('@loomcli/core/short-only-without-sho
27
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
28
  headline: 'Short only with no short alias',
29
29
  });
30
+ /** `aliases` on an option that declares `shortOnly`. */
31
+ const shortOnlyWithAliases = registerRule('@loomcli/core/short-only-with-aliases', {
32
+ explanation: 'shortOnly removes every long spelling of an option, and each alias adds a long spelling, so an option cannot declare both.',
33
+ headline: 'Short only with aliases',
34
+ });
30
35
  /** `multiple` on a Boolean option. */
31
36
  const booleanOptionMultiple = registerRule('@loomcli/core/boolean-option-multiple', {
32
37
  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
38
  headline: 'Boolean option takes one value',
34
39
  });
40
+ /** `multiple` on a counted option. */
41
+ const countOptionMultiple = registerRule('@loomcli/core/count-option-multiple', {
42
+ explanation: 'A counted option already counts every occurrence of every spelling, so it has no values to collect. multiple collects each occurrence of a string option into an array.',
43
+ headline: 'Counted option takes no values',
44
+ });
45
+ /** `polarity` on a counted option. */
46
+ const polarityOnCount = registerRule('@loomcli/core/polarity-on-count', {
47
+ explanation: 'Polarity chooses which long forms a Boolean option accepts and what its absence means. A counted option reads how many times it was supplied, so it has no negative form and no polarity.',
48
+ headline: 'Polarity on a counted option',
49
+ });
50
+ /** `implied` on a Boolean or counted option. */
51
+ const impliedOnBooleanOrCount = registerRule('@loomcli/core/implied-on-boolean-or-count', {
52
+ explanation: 'An implied value is the value a bare spelling of a string option supplies. A Boolean option and a counted option take no value, so a bare spelling already says everything they read.',
53
+ headline: 'Implied on a valueless option',
54
+ });
55
+ /** An `implied` value that is not a string. */
56
+ const impliedNotAString = registerRule('@loomcli/core/implied-not-a-string', {
57
+ explanation: "An implied value stands in for the string an operator would otherwise attach to the spelling, so it is a string, in the validator's input type.",
58
+ headline: 'Implied value not a string',
59
+ });
35
60
  /** `polarity` on a string option. */
36
61
  const polarityOnString = registerRule('@loomcli/core/polarity-on-string', {
37
62
  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.',
@@ -52,7 +77,7 @@ const shortOnlyBothPolarities = registerRule('@loomcli/core/short-only-both-pola
52
77
  * plugin's hook declared.
53
78
  */
54
79
  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.",
80
+ 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, an alias, or a generated negative form included, would reach only one of them.",
56
81
  headline: 'Spelling used twice',
57
82
  });
58
83
  /**
@@ -60,7 +85,7 @@ const spellingTaken = registerRule('@loomcli/core/spelling-taken', {
60
85
  * plugin's hook declared.
61
86
  */
62
87
  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.",
88
+ explanation: "An action reads the global options and its Command's own options from one options object, each under its declared name, and the global options, the application's and every installed plugin's, share every Command's one table of spellings. Two options with one name in either leave one of them unreadable.",
64
89
  headline: 'Option declared twice',
65
90
  });
66
91
  /**
@@ -117,6 +142,11 @@ const booleanOptionValueRule = registerRule('@loomcli/core/boolean-option-value-
117
142
  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
143
  headline: 'Value rule on a Boolean option',
119
144
  });
145
+ /** `validate`, `default`, `required`, or `validateOmitted` on a counted option. */
146
+ const countOptionValueRule = registerRule('@loomcli/core/count-option-value-rule', {
147
+ explanation: 'A counted option consumes no value and reads how many times it was supplied, 0 when nothing supplied it, so there is nothing to validate and no absence to decide. validate, default, required, and validateOmitted belong to inputs that take a value.',
148
+ headline: 'Value rule on a counted option',
149
+ });
120
150
  /** A required input that also declares a default. */
121
151
  const requiredWithDefault = registerRule('@loomcli/core/required-with-default', {
122
152
  explanation: 'A default fills an omitted value, and a required input fails when it is omitted, so a required input never reads its default.',
@@ -132,14 +162,26 @@ const defaultShape = registerRule('@loomcli/core/default-shape', {
132
162
  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
163
  headline: 'Default of the wrong shape',
134
164
  });
165
+ /** The most arrays and plain objects any path through a declared default may hold. */
166
+ const defaultLevels = 10;
167
+ /** A declared default nested deeper than `defaultLevels`. */
168
+ const defaultDepth = registerRule('@loomcli/core/default-depth', {
169
+ explanation: `A default stands in for the value an operator would supply, and every reader of the graph, help and the manifest included, walks it. Core keeps every path through a default within ${String(defaultLevels)} levels of arrays and plain objects, and a default that holds itself nests without end, so every reader stays far inside the call stack on every runtime.`,
170
+ headline: 'Default nested too deep',
171
+ });
135
172
  /** A declared default its validator rejected. */
136
173
  const invalidDefault = registerRule('@loomcli/core/invalid-default', {
137
174
  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
175
  headline: 'Default rejected',
139
176
  });
177
+ /** A declared implied value its validator rejected. */
178
+ const invalidImplied = registerRule('@loomcli/core/invalid-implied', {
179
+ explanation: 'Each run passes every implied value through its validator before it reads a token, because a bare spelling supplies it to the action as a validated value. An implied value the validator rejects is the declaration at fault, whatever the operator supplies, so the run reports it whether or not a bare spelling was typed.',
180
+ headline: 'Implied value rejected',
181
+ });
140
182
  /** A validator's JSON Schema converter that threw or returned a value that is not a plain object. */
141
183
  const schemaConverterFailed = registerRule('@loomcli/core/schema-converter-failed', {
142
184
  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
185
  headline: 'Schema converter failed',
144
186
  });
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, };
187
+ export { booleanOptionMultiple, booleanOptionValueRule, countOptionMultiple, countOptionValueRule, defaultDepth, defaultLevels, defaultShape, envName, envOnArgument, envOnMultiple, flagNotBoolean, globalOptionAfterCommand, globalPresenceRule, impliedNotAString, impliedOnBooleanOrCount, invalidDefault, invalidImplied, notAValidator, omissionAlreadyDecided, nameSharedAcrossKinds, omissionWithoutValidator, optionDeclaredTwice, optionPolarity, optionType, polarityOnCount, polarityOnString, requiredWithDefault, schemaConverterFailed, shortAlias, shortOnlyBothPolarities, shortOnlyWithAliases, shortOnlyWithoutShort, spellingTaken, variableBoundTwice, };
package/dist/inspect.d.ts CHANGED
@@ -21,17 +21,22 @@ interface ArgumentNode {
21
21
  readonly extensions: Readonly<Record<string, unknown>>;
22
22
  }
23
23
  /**
24
- * One declared option, in the shape its type gives it. Spellings are the accepted CLI forms, and
25
- * `scope` tells an application's own option from a plugin option, which reaches no action. An
26
- * option a plugin's lifecycle hook declared on a Command is that Command's own in every respect, so
27
- * it reads `application` and names no plugin.
24
+ * One declared option, in the shape its type gives it. Spellings are the accepted CLI forms. A
25
+ * global option reads the same whether the application or a plugin declared it, and an option a
26
+ * plugin's lifecycle hook declared on a Command is that Command's own in every respect, so neither
27
+ * names a plugin.
28
28
  * `hidden` is `false` unless the declaration says `true`, and `deprecated` is the declared
29
29
  * migration message or `undefined`. A listing projection omits a hidden node and marks a
30
30
  * deprecated one; parsing binds without reading either.
31
- * `schema` is the input schema the validator publishes. A Boolean option validates nothing, so its
32
- * variant carries the field at `null`, and every projection built on the node holds if a later
33
- * contract lets it validate.
31
+ * `schema` is the input schema the validator publishes. A Boolean option and a counted option
32
+ * validate nothing, so their variants carry the field at `null`, and every projection built on the
33
+ * node holds if a later contract lets a Boolean option validate. A counted option has no negative
34
+ * spelling, polarity, default, or validator, so its variant carries none of them. A string option's
35
+ * `implied` is the value a bare spelling supplies, or `null` when it declares none.
34
36
  * `env` is the variable the option's environment binding names, or `null` when it binds none.
37
+ * `aliases` holds the declared aliases as bare names in declaration order. An alias is
38
+ * unadvertised, so the spellings above are the ones the declared name derives and no listing reads
39
+ * `aliases`; parsing and `locate` read the table, which holds every alias's spellings.
35
40
  */
36
41
  type OptionNode = {
37
42
  readonly type: 'string';
@@ -39,9 +44,9 @@ type OptionNode = {
39
44
  readonly description: string | undefined;
40
45
  readonly hidden: boolean;
41
46
  readonly deprecated: string | undefined;
42
- readonly scope: 'application' | 'plugin';
43
47
  readonly long: string | null;
44
48
  readonly short: string | null;
49
+ readonly aliases: readonly string[];
45
50
  readonly required: boolean;
46
51
  readonly multiple: boolean;
47
52
  readonly validated: boolean;
@@ -51,6 +56,7 @@ type OptionNode = {
51
56
  readonly default: {
52
57
  readonly value: unknown;
53
58
  } | undefined;
59
+ readonly implied: string | null;
54
60
  readonly extensions: Readonly<Record<string, unknown>>;
55
61
  } | {
56
62
  readonly type: 'boolean';
@@ -58,14 +64,26 @@ type OptionNode = {
58
64
  readonly description: string | undefined;
59
65
  readonly hidden: boolean;
60
66
  readonly deprecated: string | undefined;
61
- readonly scope: 'application' | 'plugin';
62
67
  readonly long: string | null;
63
68
  readonly short: string | null;
64
69
  readonly negative: string | null;
70
+ readonly aliases: readonly string[];
65
71
  readonly polarity: 'positive' | 'negative' | 'both';
66
72
  readonly schema: InputSchema;
67
73
  readonly env: string | null;
68
74
  readonly extensions: Readonly<Record<string, unknown>>;
75
+ } | {
76
+ readonly type: 'count';
77
+ readonly name: string;
78
+ readonly description: string | undefined;
79
+ readonly hidden: boolean;
80
+ readonly deprecated: string | undefined;
81
+ readonly long: string | null;
82
+ readonly short: string | null;
83
+ readonly aliases: readonly string[];
84
+ readonly schema: null;
85
+ readonly env: string | null;
86
+ readonly extensions: Readonly<Record<string, unknown>>;
69
87
  };
70
88
  /**
71
89
  * One Command in the graph. `name` is `null` for the root, and `path` is its route from it.
@@ -111,20 +129,18 @@ interface CommandGraph {
111
129
  readonly root: CommandNode;
112
130
  }
113
131
  /**
114
- * A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
115
- * consumer cannot reach the source through the copy, and a later call reports the value again.
116
- * Primitives and library objects, such as a class instance or a `Date` a schema produced, are
117
- * reported as they are, because core cannot copy them meaningfully. The graph reads it for a
118
- * declared value and the chain reads it for the request one middleware holds.
132
+ * The spelling every reported problem names one option by, the one core's own validation reports:
133
+ * its long form, a negative-only Boolean option's negative form, and otherwise its short form. A
134
+ * plugin that reports a problem for an option, such as an input source, names it this way too.
119
135
  */
120
- export declare function snapshot(value: unknown): unknown;
136
+ export declare function reportedSpelling(option: OptionNode): string;
121
137
  /** The declared result as plain data, or `null` on a Command that declares none. */
122
138
  declare function resultNode(result: DeclaredResult | undefined): ResultNode | null;
123
139
  /**
124
140
  * Renders one built graph as frozen plain data. Nothing here reads a host fact; each validated
125
141
  * 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.
142
+ * fails is a declaration fault. The globals list holds every global option, the application's own
143
+ * and then each installed plugin's in installation order, which is the order the table holds them.
128
144
  */
129
145
  declare function inspectGraph(name: string, graph: BuiltGraph, facts: {
130
146
  description: string | undefined;
package/dist/inspect.js CHANGED
@@ -1,48 +1,28 @@
1
1
  import { asSentence, DeclarationError, InternalError, reasonOf } from './errors.js';
2
2
  import { partOf, siteFinding } from './facts.js';
3
3
  import { schemaConverterFailed } from './input-rules.js';
4
- import { isPlainObject } from './plain.js';
4
+ import { reportedOf, spellingsOf } from './options.js';
5
+ import { isPlainObject, snapshotRecord } from './plain.js';
5
6
  import { foreignGraph, foreignGraphCorrection } from './rules.js';
6
- import { declaringSite, inputPlace, validatesOmission } from './validation.js';
7
+ import { declarationSubject, declaringSite, inputPlace, validatesOmission } from './validation.js';
7
8
  /** A declaration that carries no extension value publishes one shared, empty frozen record. */
8
9
  const noExtensions = Object.freeze({});
9
10
  /**
10
- * Reads the spellings out of the compiled table the parser uses, so inspection cannot report a
11
- * form the parser does not accept. Each entry carries its own role, so the naming convention has
12
- * one owner: the table that writes it.
11
+ * The spelling every reported problem names one option by, the one core's own validation reports:
12
+ * its long form, a negative-only Boolean option's negative form, and otherwise its short form. A
13
+ * plugin that reports a problem for an option, such as an input source, names it this way too.
13
14
  */
14
- function spellingsOf(table, name) {
15
- const spellings = { long: null, negative: null, short: null };
16
- for (const [spelling, option] of table) {
17
- if (option.name === name) {
18
- spellings[option.role] = spelling;
19
- }
20
- }
21
- return spellings;
15
+ export function reportedSpelling(option) {
16
+ const negative = option.type === 'boolean' ? option.negative : null;
17
+ return reportedOf({ long: option.long, negative, short: option.short }, option.name);
22
18
  }
23
19
  /**
24
- * A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
25
- * consumer cannot reach the source through the copy, and a later call reports the value again.
26
- * Primitives and library objects, such as a class instance or a `Date` a schema produced, are
27
- * reported as they are, because core cannot copy them meaningfully. The graph reads it for a
28
- * declared value and the chain reads it for the request one middleware holds.
20
+ * A declared default is wrapped, so `default: undefined` reads apart from no default at all. The
21
+ * value is the frozen snapshot the declaring call took, the one a run validates, so inspection
22
+ * copies nothing and every reader sees one value.
29
23
  */
30
- export function snapshot(value) {
31
- if (Array.isArray(value)) {
32
- return Object.freeze(value.map((entry) => snapshot(entry)));
33
- }
34
- if (isPlainObject(value)) {
35
- return snapshotRecord(value);
36
- }
37
- return value;
38
- }
39
- /** The snapshot of one plain object, under the record type the caller already established. */
40
- function snapshotRecord(value) {
41
- return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshot(entry)])));
42
- }
43
- /** A declared default is wrapped, so `default: undefined` reads apart from no default at all. */
44
24
  function declaredDefault(config) {
45
- return 'default' in config ? Object.freeze({ value: snapshot(config.default) }) : undefined;
25
+ return 'default' in config ? Object.freeze({ value: config.default }) : undefined;
46
26
  }
47
27
  /**
48
28
  * The JSON Schema draft build asks every converter for, with no library options. Every converter
@@ -65,11 +45,11 @@ function publishesSchema(schema) {
65
45
  * the converter threw, the thrown value as its cause.
66
46
  */
67
47
  function converterFault(input, check, failed) {
68
- const site = declaringSite(input, inputPlace(input, check));
48
+ const site = check.siteOf(input);
69
49
  const parts = {
70
50
  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}`,
51
+ findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'validate'))],
52
+ sentence: `${declarationSubject(input)} validator's JSON Schema converter ${failed.failure}`,
73
53
  };
74
54
  return 'cause' in failed
75
55
  ? new DeclarationError(schemaConverterFailed, parts, { cause: failed.cause })
@@ -120,47 +100,58 @@ function inputSchema(input, check) {
120
100
  function extensionsOf(records, declaration) {
121
101
  return records.get(declaration) ?? noExtensions;
122
102
  }
103
+ /**
104
+ * One option's node, in the shape its kind gives it. Every kind publishes the listing facts and the
105
+ * spellings the declared name derives; a string option adds its value facts, a Boolean option its
106
+ * negative spelling and polarity, and a counted option nothing more.
107
+ */
123
108
  function optionNode(input, read) {
124
- const { check, records, scope, table } = read;
109
+ const { check, records, table } = read;
125
110
  const { config, name } = input;
126
111
  const { long, negative, short } = spellingsOf(table, name);
127
- const extensions = extensionsOf(records, input);
128
- const node = config.type === 'boolean'
129
- ? {
130
- deprecated: config.deprecated,
131
- description: config.description,
132
- env: config.env ?? null,
133
- extensions,
134
- hidden: config.hidden === true,
135
- long,
136
- name,
137
- negative,
138
- polarity: config.polarity ?? 'positive',
139
- schema: null,
140
- scope,
141
- short,
142
- type: 'boolean',
112
+ const shared = {
113
+ aliases: Object.freeze([...(config.aliases ?? [])]),
114
+ deprecated: config.deprecated,
115
+ description: config.description,
116
+ env: config.env ?? null,
117
+ extensions: extensionsOf(records, input),
118
+ hidden: config.hidden === true,
119
+ long,
120
+ name,
121
+ short,
122
+ };
123
+ switch (config.type) {
124
+ case 'string': {
125
+ return Object.freeze({
126
+ ...shared,
127
+ default: declaredDefault(config),
128
+ implied: config.implied ?? null,
129
+ // The parser reads the same test, so a collection reports as one here and there.
130
+ multiple: config.multiple === true,
131
+ required: config.required === true,
132
+ schema: inputSchema(input, check),
133
+ type: 'string',
134
+ validateOmitted: validatesOmission(input),
135
+ validated: config.validate !== undefined,
136
+ });
143
137
  }
144
- : {
145
- default: declaredDefault(config),
146
- deprecated: config.deprecated,
147
- description: config.description,
148
- env: config.env ?? null,
149
- extensions,
150
- hidden: config.hidden === true,
151
- long,
152
- // The parser reads the same test, so a collection reports as one here and there.
153
- multiple: config.multiple === true,
154
- name,
155
- required: config.required === true,
156
- schema: inputSchema(input, check),
157
- scope,
158
- short,
159
- type: 'string',
160
- validateOmitted: validatesOmission(input),
161
- validated: config.validate !== undefined,
162
- };
163
- return Object.freeze(node);
138
+ case 'boolean': {
139
+ return Object.freeze({
140
+ ...shared,
141
+ negative,
142
+ polarity: config.polarity ?? 'positive',
143
+ schema: null,
144
+ type: 'boolean',
145
+ });
146
+ }
147
+ case 'count': {
148
+ return Object.freeze({ ...shared, schema: null, type: 'count' });
149
+ }
150
+ default: {
151
+ const exhaustive = config;
152
+ return exhaustive;
153
+ }
154
+ }
164
155
  }
165
156
  /** The built slots already answer presence and arity, so the node repeats no config reading. */
166
157
  function argumentNode(slot, read) {
@@ -188,7 +179,10 @@ function optionNodes(inputs, read) {
188
179
  */
189
180
  function commandNode(command, place) {
190
181
  const { development, nodes, path, records } = place;
191
- const check = { development, global: false, path };
182
+ const check = {
183
+ development,
184
+ siteOf: (input) => declaringSite(input, inputPlace(input, path)),
185
+ };
192
186
  const node = {
193
187
  aliases: Object.freeze([...command.aliases]),
194
188
  arguments: Object.freeze(command.arguments.map((slot) => argumentNode(slot, { check, records }))),
@@ -199,12 +193,7 @@ function commandNode(command, place) {
199
193
  hasAction: command.dispatch !== undefined,
200
194
  hidden: command.hidden,
201
195
  name: command.name,
202
- options: Object.freeze(optionNodes(command.inputs, {
203
- check,
204
- records,
205
- scope: 'application',
206
- table: command.options,
207
- })),
196
+ options: Object.freeze(optionNodes(command.inputs, { check, records, table: command.table })),
208
197
  path,
209
198
  result: resultNode(command.result),
210
199
  };
@@ -226,21 +215,19 @@ function resultNode(result) {
226
215
  /**
227
216
  * Renders one built graph as frozen plain data. Nothing here reads a host fact; each validated
228
217
  * 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.
218
+ * fails is a declaration fault. The globals list holds every global option, the application's own
219
+ * and then each installed plugin's in installation order, which is the order the table holds them.
231
220
  */
232
221
  function inspectGraph(name, graph, facts) {
233
222
  const { development } = facts;
234
223
  const records = graph.extensions;
235
224
  const table = graph.globals.options;
236
225
  const nodes = new WeakMap();
237
- const check = { development, global: true, path: [] };
226
+ const { sites } = graph.globals;
227
+ const check = { development, siteOf: (input) => sites.get(input) };
238
228
  const inspected = {
239
229
  description: facts.description,
240
- globals: Object.freeze([
241
- ...optionNodes(graph.globals.inputs, { check, records, scope: 'application', table }),
242
- ...graph.globals.plugins.flatMap((installed) => optionNodes(installed.inputs, { check, records, scope: 'plugin', table })),
243
- ]),
230
+ globals: Object.freeze(optionNodes(graph.globals.inputs, { check, records, table })),
244
231
  name,
245
232
  root: commandNode(graph.root, {
246
233
  description: facts.description,
package/dist/locate.js CHANGED
@@ -1,48 +1,17 @@
1
- import { argumentSlot, readsAsChild, route } from './command.js';
2
1
  import { UsageError } from './errors.js';
3
2
  import { graphMismatch, linkOf } from './inspect.js';
4
- import { isOptionToken, longStringOption, longToken, scanGlobals, scanInputs } from './options.js';
3
+ import { argumentSlot, isOptionWord, readOptionWord, readWords, refusesValue } from './parse.js';
5
4
  const none = Object.freeze({ kind: 'none' });
6
5
  /**
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`.
6
+ * The words before the last, as the parser reads them, stopped before any validation. Every
7
+ * structural fault among them, an unknown Command included, reads as no position. When they run
8
+ * out at a Command with children, the last word may still continue routing, whatever it holds, so
9
+ * routing has not ended and a parent's own option among them stays unbound.
9
10
  */
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) {
11
+ function readEarlier(graph, words) {
44
12
  try {
45
- return readStructure(graph, earlier);
13
+ const read = readWords(graph, words.slice(0, -1), { partial: true });
14
+ return read.fault === undefined ? read : undefined;
46
15
  }
47
16
  catch (error) {
48
17
  if (error instanceof UsageError) {
@@ -61,35 +30,55 @@ function valueOf(scope, name, word) {
61
30
  }
62
31
  return { command, kind: 'value', lead: word.lead, option, prefix: word.prefix };
63
32
  }
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
33
  /** The word after a string option that ended the earlier words is that option's value. */
71
34
  function awaitedValue(scope, awaiting, last) {
72
- return isOptionToken(last) ? none : valueOf(scope, awaiting.name, { lead: '', prefix: last });
35
+ return refusesValue(last) ? none : valueOf(scope, awaiting.name, { lead: '', prefix: last });
73
36
  }
74
- /** A bare word names a child until routing commits, and fills the next positional after. */
75
- function bareWord(scope, last) {
37
+ /** A plain word names a child until routing ends, and fills the next positional after. */
38
+ function plainWord(scope, last) {
76
39
  const { command, earlier } = scope;
77
- if (!earlier.committed && readsAsChild(earlier.command, last)) {
40
+ if (!earlier.committed && earlier.command.children.size > 0) {
78
41
  return { command, kind: 'command', prefix: last };
79
42
  }
80
- const slot = argumentSlot(earlier.command.arguments, earlier.positionals);
43
+ const slot = argumentSlot(earlier.command.arguments, earlier.positionals.length);
81
44
  const argument = slot && command.arguments[earlier.command.arguments.indexOf(slot)];
82
45
  return argument ? { argument, command, kind: 'argument', prefix: last } : none;
83
46
  }
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);
47
+ /**
48
+ * A word the walk reads against the routed Command's table and the earlier words' values, as the
49
+ * parser reads it: no position where the walk faults, a repeat of an earlier option included, the
50
+ * value a long spelling carries after `=` or a short value letter carries after it, and otherwise
51
+ * `undefined`, an option spelling.
52
+ */
53
+ function carriedValue(scope, last) {
54
+ const { earlier } = scope;
55
+ const context = { table: earlier.command.table, values: earlier.values };
56
+ const { occurrences } = readOptionWord(context, last, undefined);
57
+ for (const occurrence of occurrences) {
58
+ if (occurrence.kind !== 'value' && occurrence.kind !== 'awaiting') {
59
+ return none;
60
+ }
61
+ if (occurrence.kind === 'value' && occurrence.lead !== undefined) {
62
+ const { lead, option } = occurrence;
63
+ return valueOf(scope, option.name, { lead, prefix: last.slice(lead.length) });
64
+ }
89
65
  }
90
- return { command: scope.command, kind: 'option', prefix: last, supplied: scope.earlier.supplied };
66
+ return undefined;
91
67
  }
92
- /** The last word, read as the parser would read the next token. */
68
+ /**
69
+ * An option word. A long spelling without `=` is still being typed, so it is an option spelling
70
+ * whatever it names; any other word is read by the walk.
71
+ */
72
+ function optionWord(scope, last) {
73
+ const typing = last.startsWith('--') && !last.includes('=');
74
+ return ((typing ? undefined : carriedValue(scope, last)) ?? {
75
+ command: scope.command,
76
+ kind: 'option',
77
+ prefix: last,
78
+ supplied: scope.earlier.supplied,
79
+ });
80
+ }
81
+ /** The last word, read as the parser would read the next word. */
93
82
  function lastWord(scope, last) {
94
83
  const { command, earlier } = scope;
95
84
  if (earlier.delimited) {
@@ -98,7 +87,10 @@ function lastWord(scope, last) {
98
87
  if (earlier.awaiting) {
99
88
  return awaitedValue(scope, earlier.awaiting, last);
100
89
  }
101
- return isOptionToken(last) ? optionWord(scope, last) : bareWord(scope, last);
90
+ if (last === '-' || last === '--') {
91
+ return { command, kind: 'option', prefix: last, supplied: earlier.supplied };
92
+ }
93
+ return isOptionWord(last) ? optionWord(scope, last) : plainWord(scope, last);
102
94
  }
103
95
  /**
104
96
  * Reads an unfinished invocation against a graph `inspect()` returned and reports where its last
@@ -108,7 +100,7 @@ function lastWord(scope, last) {
108
100
  */
109
101
  function locate(graph, words) {
110
102
  const link = linkOf(graph);
111
- const earlier = readEarlier(link.graph, words.slice(0, -1));
103
+ const earlier = readEarlier(link.graph, words);
112
104
  if (!earlier) {
113
105
  return none;
114
106
  }
@@ -116,6 +108,6 @@ function locate(graph, words) {
116
108
  if (!command) {
117
109
  throw graphMismatch('The routed command is not in the inspected graph.');
118
110
  }
119
- return lastWord({ built: link.graph, command, earlier, graph }, words.at(-1) ?? '');
111
+ return lastWord({ command, earlier, graph }, words.at(-1) ?? '');
120
112
  }
121
113
  export { locate };