@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/application.d.ts +60 -31
  3. package/dist/application.js +356 -149
  4. package/dist/bindings.d.ts +31 -0
  5. package/dist/bindings.js +64 -0
  6. package/dist/chain.d.ts +26 -12
  7. package/dist/chain.js +59 -92
  8. package/dist/command-rules.d.ts +55 -0
  9. package/dist/command-rules.js +142 -0
  10. package/dist/command.d.ts +212 -93
  11. package/dist/command.js +1224 -454
  12. package/dist/controls.d.ts +8 -0
  13. package/dist/controls.js +23 -0
  14. package/dist/defect.d.ts +18 -0
  15. package/dist/defect.js +272 -0
  16. package/dist/developer.d.ts +24 -0
  17. package/dist/developer.js +52 -0
  18. package/dist/diagnostic-text.d.ts +81 -0
  19. package/dist/diagnostic-text.js +283 -0
  20. package/dist/diagnostic.d.ts +11 -0
  21. package/dist/diagnostic.js +70 -0
  22. package/dist/errors.d.ts +115 -26
  23. package/dist/errors.js +333 -54
  24. package/dist/exit-codes.d.ts +45 -0
  25. package/dist/exit-codes.js +46 -0
  26. package/dist/extension.d.ts +44 -9
  27. package/dist/extension.js +147 -65
  28. package/dist/facts.d.ts +71 -11
  29. package/dist/facts.js +108 -25
  30. package/dist/globals.d.ts +63 -22
  31. package/dist/globals.js +164 -39
  32. package/dist/hints.d.ts +79 -0
  33. package/dist/hints.js +247 -0
  34. package/dist/host.d.ts +13 -0
  35. package/dist/host.js +43 -1
  36. package/dist/identity.d.ts +19 -0
  37. package/dist/identity.js +72 -0
  38. package/dist/index.d.ts +15 -4
  39. package/dist/index.js +6 -0
  40. package/dist/input-rules.d.ts +64 -0
  41. package/dist/input-rules.js +145 -0
  42. package/dist/inspect.d.ts +36 -5
  43. package/dist/inspect.js +125 -27
  44. package/dist/lanes.js +1 -1
  45. package/dist/locate.d.ts +41 -0
  46. package/dist/locate.js +121 -0
  47. package/dist/options.d.ts +96 -2
  48. package/dist/options.js +259 -71
  49. package/dist/output.d.ts +11 -2
  50. package/dist/output.js +23 -3
  51. package/dist/plain.d.ts +6 -0
  52. package/dist/plain.js +12 -0
  53. package/dist/plugin-rules.d.ts +62 -0
  54. package/dist/plugin-rules.js +155 -0
  55. package/dist/plugin.d.ts +121 -55
  56. package/dist/plugin.js +496 -126
  57. package/dist/prototypes.d.ts +7 -0
  58. package/dist/prototypes.js +29 -0
  59. package/dist/rendering.d.ts +6 -1
  60. package/dist/rendering.js +23 -5
  61. package/dist/rules.d.ts +51 -0
  62. package/dist/rules.js +115 -0
  63. package/dist/sequence.js +6 -1
  64. package/dist/sources.d.ts +58 -0
  65. package/dist/sources.js +258 -0
  66. package/dist/style-wire.js +1 -1
  67. package/dist/style.js +1 -1
  68. package/dist/theme.d.ts +4 -0
  69. package/dist/theme.js +25 -5
  70. package/dist/thenable.d.ts +15 -0
  71. package/dist/thenable.js +29 -0
  72. package/dist/translators.d.ts +69 -0
  73. package/dist/translators.js +253 -0
  74. package/dist/types.d.ts +76 -21
  75. package/dist/validation.d.ts +65 -10
  76. package/dist/validation.js +314 -108
  77. package/dist/view.d.ts +49 -15
  78. package/dist/view.js +157 -79
  79. package/package.json +3 -2
package/dist/options.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { InputSite } from './facts.js';
1
2
  import type { OptionConfig } from './types.js';
2
3
  export interface OptionDeclaration {
3
4
  name: string;
@@ -7,9 +8,13 @@ export interface OptionValues {
7
8
  strings: Map<string, string>;
8
9
  lists: Map<string, string[]>;
9
10
  booleans: Map<string, boolean>;
11
+ /** The spelling of the token that supplied each parsed option, which no input source writes. */
12
+ spellings: Map<string, string>;
10
13
  }
14
+ /** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
15
+ export declare function emptyValues(): OptionValues;
11
16
  /** Which accepted form a table entry is. The table owns the convention, so readers never re-derive it. */
12
- type SpellingRole = 'long' | 'negative' | 'short';
17
+ export type SpellingRole = 'long' | 'negative' | 'short';
13
18
  type OptionForm = {
14
19
  type: 'string';
15
20
  name: string;
@@ -22,6 +27,22 @@ type OptionForm = {
22
27
  type OptionSpelling = OptionForm & {
23
28
  role: SpellingRole;
24
29
  };
30
+ /**
31
+ * The part of one declaration that yields a spelling of the given role, which a spelling fault
32
+ * marks: the declared name for the long form, `short` for the short alias, and the `polarity` that
33
+ * generates a negative form.
34
+ */
35
+ export declare function spellingMark(site: InputSite, role: SpellingRole): string;
36
+ /** The declared name answers the declared-name rule an argument's name answers. */
37
+ export declare function checkOptionName(name: unknown, site: InputSite): void;
38
+ /**
39
+ * The scope one table compiles: the phrase a repeated name names it by, and where each of its
40
+ * options was declared, which every fault's findings rebuild.
41
+ */
42
+ export interface CompileScope<Declaration extends OptionDeclaration> {
43
+ readonly subject: string;
44
+ readonly siteOf: (declaration: Declaration) => InputSite;
45
+ }
25
46
  /**
26
47
  * One Boolean option's value for one invocation: the value the parser consumed, or the value its
27
48
  * declared polarity gives an absent option. A negative-only option is absent as `true`, because its
@@ -29,14 +50,87 @@ type OptionSpelling = OptionForm & {
29
50
  * declaration answer the same rule.
30
51
  */
31
52
  export declare function booleanValue(values: OptionValues, name: string, config: OptionConfig): boolean;
32
- export declare function compileOptions(declarations: readonly OptionDeclaration[], subject: string): Map<string, OptionSpelling>;
53
+ /**
54
+ * One scope's options compiled into the spelling table the parser reads, with every rule one
55
+ * declaration answers alone and every rule two of them answer together.
56
+ */
57
+ export declare function compileOptions<Declaration extends OptionDeclaration>(declarations: readonly Declaration[], scope: CompileScope<Declaration>): Map<string, OptionSpelling>;
58
+ /**
59
+ * Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
60
+ * separate value is never one. The parser and `locate` read each token through this rule.
61
+ */
62
+ export declare function isOptionToken(token: string): boolean;
63
+ /** A long option token and the inline value it carries after its first `=`, if any. */
64
+ export interface LongToken {
65
+ spelling: string;
66
+ inline: string | undefined;
67
+ }
68
+ /**
69
+ * A token that starts with `--` split at its first `=` into the spelling and the inline value, or
70
+ * `undefined` for any other token. The parser and `locate` split long tokens through this rule.
71
+ */
72
+ export declare function longToken(token: string): LongToken | undefined;
73
+ /**
74
+ * The name of the string option one long spelling names in a table, which takes its value after
75
+ * `=`, or `undefined` for a Boolean, negative, short, or unknown spelling.
76
+ */
77
+ export declare function longStringOption(spellings: ReadonlyMap<string, OptionSpelling>, spelling: string): string | undefined;
78
+ /**
79
+ * A string option whose value the next token supplies, where the tokens ended first. A complete
80
+ * invocation reports it as a missing value; a partial one reads the next word as that value.
81
+ */
82
+ export interface AwaitingValue {
83
+ name: string;
84
+ spelling: string;
85
+ }
86
+ /** One option name a token newly supplied, with the index of that token in the list read. */
87
+ export interface SuppliedOption {
88
+ name: string;
89
+ token: number;
90
+ }
91
+ /**
92
+ * The pre-scan's reading of a token list that may stop short. `positions` holds the index in
93
+ * `tokens` of each `rest` token, and `awaiting` is the global option the last token left without
94
+ * its value.
95
+ */
96
+ export interface GlobalScan {
97
+ awaiting: AwaitingValue | undefined;
98
+ positions: number[];
99
+ rest: string[];
100
+ supplied: SuppliedOption[];
101
+ values: OptionValues;
102
+ }
33
103
  /** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
104
+ export declare function scanGlobals(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): GlobalScan;
105
+ /** The pre-scan of a complete invocation, where a global still waiting for its value is a fault. */
34
106
  export declare function extractGlobals(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
35
107
  rest: string[];
36
108
  values: OptionValues;
37
109
  };
110
+ /**
111
+ * Whether one option holds a value a tier supplied: a token in any spelling it accepts, or a fill
112
+ * from an input source. A declared default is never in these maps, so it never counts.
113
+ */
114
+ export declare function isSupplied(values: OptionValues, name: string): boolean;
115
+ /** One run's own copy of parsed values, which the input-source stage fills without touching argv's. */
116
+ export declare function copyValues(values: OptionValues): OptionValues;
38
117
  /** Global and local keys never overlap, so one merged view feeds a single validation pass. */
39
118
  export declare function mergeValues(globals: OptionValues, locals: OptionValues): OptionValues;
119
+ /**
120
+ * One Command's reading of its own tokens, which may stop short. `delimited` says a bare `--` was
121
+ * read, and `awaiting` is the option the last token left without its value.
122
+ */
123
+ export interface InputScan {
124
+ awaiting: AwaitingValue | undefined;
125
+ delimited: boolean;
126
+ options: OptionValues;
127
+ passthrough: string[];
128
+ positionals: string[];
129
+ supplied: SuppliedOption[];
130
+ }
131
+ /** Reads one Command's tokens into options, positionals, and the passthrough tail. */
132
+ export declare function scanInputs(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): InputScan;
133
+ /** Parses a complete invocation's local tokens, where a waiting option is a missing value. */
40
134
  export declare function parseInputs(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
41
135
  options: OptionValues;
42
136
  passthrough: string[];
package/dist/options.js CHANGED
@@ -1,52 +1,123 @@
1
- import { DeclarationError, MissingValueError, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
1
+ import { declaredName } from './command-rules.js';
2
+ import { valueCode } from './diagnostic-text.js';
3
+ import { DeclarationError, MissingValueError, quoted, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
4
+ import { factFault, flagFault, siteFinding } from './facts.js';
5
+ import { booleanOptionMultiple, optionDeclaredTwice, optionPolarity, optionType, polarityOnString, shortAlias, shortOnlyBothPolarities, shortOnlyWithoutShort, spellingTaken, } from './input-rules.js';
2
6
  /** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
3
- function emptyValues() {
4
- return { booleans: new Map(), lists: new Map(), strings: new Map() };
7
+ export function emptyValues() {
8
+ return { booleans: new Map(), lists: new Map(), spellings: new Map(), strings: new Map() };
5
9
  }
6
- function validateDeclaration({ name, config }) {
10
+ /**
11
+ * The part of one declaration that yields a spelling of the given role, which a spelling fault
12
+ * marks: the declared name for the long form, `short` for the short alias, and the `polarity` that
13
+ * generates a negative form.
14
+ */
15
+ export function spellingMark(site, role) {
16
+ if (role === 'long') {
17
+ return site.named;
18
+ }
19
+ return `${site.at}.${role === 'short' ? 'short' : 'polarity'}`;
20
+ }
21
+ /** The declared name answers the declared-name rule an argument's name answers. */
22
+ export function checkOptionName(name, site) {
23
+ const findings = [siteFinding(site, site.named)];
7
24
  if (typeof name !== 'string') {
8
- throw new DeclarationError('Option names must be strings. Supply a string name.');
25
+ throw new DeclarationError(declaredName, {
26
+ correction: 'Supply a string name.',
27
+ findings,
28
+ sentence: `Option name ${valueCode(name)} is not a string.`,
29
+ });
9
30
  }
10
31
  if (!name || name.startsWith('-') || /[\s=]/u.test(name)) {
11
- throw new DeclarationError(`Option name "${name}" is invalid. Use a nonempty name without a leading hyphen, whitespace, or "=".`);
12
- }
13
- if (!['string', 'boolean'].includes(config.type)) {
14
- throw new DeclarationError(`Option "${name}" has an invalid type. Use "string" or "boolean".`);
32
+ throw new DeclarationError(declaredName, {
33
+ correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
34
+ findings,
35
+ sentence: `Option name ${quoted(name)} is invalid.`,
36
+ });
15
37
  }
38
+ }
39
+ /** The short alias, and `shortOnly`, which leaves the option that alias alone. */
40
+ function checkShortForms(config, site, subject) {
16
41
  if (config.short !== undefined &&
17
42
  (typeof config.short !== 'string' || !/^[A-Za-z]$/u.test(config.short))) {
18
- throw new DeclarationError(`Option "${name}" requires a short alias of one ASCII letter.`);
43
+ throw factFault(shortAlias, site, {
44
+ correction: 'Supply one ASCII letter.',
45
+ fact: 'short',
46
+ sentence: `${subject} declares a short alias that is not one ASCII letter.`,
47
+ });
19
48
  }
20
49
  if (config.shortOnly !== undefined && typeof config.shortOnly !== 'boolean') {
21
- throw new DeclarationError(`Option "${name}" shortOnly must be Boolean. Use true or false.`);
50
+ throw flagFault(site, 'shortOnly');
22
51
  }
23
52
  if (config.shortOnly && config.short === undefined) {
24
- throw new DeclarationError(`Option "${name}" with shortOnly requires a short alias.`);
53
+ throw factFault(shortOnlyWithoutShort, site, {
54
+ correction: 'Add short or remove shortOnly.',
55
+ fact: 'shortOnly',
56
+ sentence: `${subject} declares shortOnly and no short alias.`,
57
+ });
58
+ }
59
+ }
60
+ /** A Boolean option's polarity, which a string option does not declare. */
61
+ function checkPolarity(config, site, subject) {
62
+ if (config.polarity === undefined) {
63
+ return;
64
+ }
65
+ if (config.type !== 'boolean') {
66
+ throw factFault(polarityOnString, site, {
67
+ correction: 'Remove polarity or use type "boolean".',
68
+ fact: 'polarity',
69
+ sentence: `${subject} declares polarity but is not Boolean.`,
70
+ });
71
+ }
72
+ if (!['positive', 'both', 'negative'].includes(config.polarity)) {
73
+ throw factFault(optionPolarity, site, {
74
+ correction: 'Use "positive", "both", or "negative".',
75
+ fact: 'polarity',
76
+ sentence: `${subject} has an invalid polarity.`,
77
+ });
78
+ }
79
+ if (config.polarity === 'both' && config.shortOnly) {
80
+ throw factFault(shortOnlyBothPolarities, site, {
81
+ correction: 'Enable long forms or select one polarity.',
82
+ fact: 'shortOnly',
83
+ sentence: `${subject} cannot express both polarities with shortOnly.`,
84
+ });
85
+ }
86
+ }
87
+ /** Every rule one option declaration answers alone, before the table meets it. */
88
+ function validateDeclaration({ name, config }, site) {
89
+ checkOptionName(name, site);
90
+ const subject = `Option ${quoted(name)}`;
91
+ if (!['string', 'boolean'].includes(config.type)) {
92
+ throw factFault(optionType, site, {
93
+ correction: 'Use "string" or "boolean".',
94
+ fact: 'type',
95
+ sentence: `${subject} has an invalid type.`,
96
+ });
25
97
  }
98
+ checkShortForms(config, site, subject);
26
99
  if (config.type === 'boolean' && config.multiple !== undefined) {
27
- throw new DeclarationError(`Option "${name}" is a boolean option and declares multiple. Remove multiple or declare a string option.`);
100
+ throw factFault(booleanOptionMultiple, site, {
101
+ correction: 'Remove multiple or declare a string option.',
102
+ fact: 'multiple',
103
+ sentence: `${subject} is a boolean option and declares multiple.`,
104
+ });
28
105
  }
29
106
  if (config.multiple !== undefined && typeof config.multiple !== 'boolean') {
30
- throw new DeclarationError(`Option "${name}" multiple must be Boolean. Use true or false.`);
31
- }
32
- if (config.polarity !== undefined) {
33
- if (config.type !== 'boolean') {
34
- throw new DeclarationError(`Option "${name}" declares polarity but is not Boolean. Remove polarity or use type "boolean".`);
35
- }
36
- if (!['positive', 'both', 'negative'].includes(config.polarity)) {
37
- throw new DeclarationError(`Option "${name}" has an invalid polarity. Use "positive", "both", or "negative".`);
38
- }
39
- if (config.polarity === 'both' && config.shortOnly) {
40
- throw new DeclarationError(`Option "${name}" cannot express both polarities with shortOnly. Enable long forms or select one polarity.`);
41
- }
107
+ throw flagFault(site, 'multiple');
42
108
  }
109
+ checkPolarity(config, site, subject);
43
110
  }
44
- function addSpelling(spellings, spelling, option) {
45
- const existing = spellings.get(spelling);
111
+ function addSpelling(claims, spelling, claim) {
112
+ const existing = claims.get(spelling);
46
113
  if (existing) {
47
- throw new DeclarationError(`Option spelling "${spelling}" is used by both "${existing.name}" and "${option.name}". Change one declaration.`);
114
+ throw new DeclarationError(spellingTaken, {
115
+ correction: 'Change one declaration.',
116
+ findings: [existing, claim].map(({ option, site }) => siteFinding(site, spellingMark(site, option.role))),
117
+ sentence: `Option spelling ${quoted(spelling)} is used by both ${quoted(existing.option.name)} and ${quoted(claim.option.name)}.`,
118
+ });
48
119
  }
49
- spellings.set(spelling, option);
120
+ claims.set(spelling, claim);
50
121
  }
51
122
  /**
52
123
  * One Boolean option's value for one invocation: the value the parser consumed, or the value its
@@ -57,37 +128,70 @@ function addSpelling(spellings, spelling, option) {
57
128
  export function booleanValue(values, name, config) {
58
129
  return values.booleans.get(name) ?? config.polarity === 'negative';
59
130
  }
60
- export function compileOptions(declarations, subject) {
61
- const spellings = new Map();
62
- const names = new Set();
63
- for (const { name, config } of declarations) {
64
- validateDeclaration({ config, name });
65
- if (names.has(name)) {
66
- throw new DeclarationError(`Option "${name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
131
+ /**
132
+ * One scope's options compiled into the spelling table the parser reads, with every rule one
133
+ * declaration answers alone and every rule two of them answer together.
134
+ */
135
+ export function compileOptions(declarations, scope) {
136
+ const claims = new Map();
137
+ const names = new Map();
138
+ for (const declaration of declarations) {
139
+ const { name, config } = declaration;
140
+ const site = scope.siteOf(declaration);
141
+ validateDeclaration({ config, name }, site);
142
+ const first = names.get(name);
143
+ if (first) {
144
+ const earlier = scope.siteOf(first);
145
+ throw new DeclarationError(optionDeclaredTwice, {
146
+ correction: 'Remove or rename the duplicate.',
147
+ findings: [
148
+ siteFinding(earlier, earlier.named, 'the first declaration'),
149
+ siteFinding(site, site.named, 'the second declaration'),
150
+ ],
151
+ sentence: `Option ${quoted(name)} is declared more than once on ${scope.subject}.`,
152
+ });
67
153
  }
68
- names.add(name);
154
+ names.set(name, declaration);
69
155
  const positive = config.type === 'string'
70
156
  ? { multiple: config.multiple === true, name, type: 'string' }
71
157
  : { name, type: 'boolean', value: config.polarity !== 'negative' };
72
158
  if (!config.shortOnly) {
73
159
  if (config.type === 'string' || config.polarity !== 'negative') {
74
- addSpelling(spellings, `--${name}`, { ...positive, role: 'long' });
160
+ addSpelling(claims, `--${name}`, { option: { ...positive, role: 'long' }, site });
75
161
  }
76
162
  if (config.type === 'boolean' &&
77
163
  (config.polarity === 'both' || config.polarity === 'negative')) {
78
- addSpelling(spellings, `--no-${name}`, {
79
- name,
80
- role: 'negative',
81
- type: 'boolean',
82
- value: false,
164
+ addSpelling(claims, `--no-${name}`, {
165
+ option: { name, role: 'negative', type: 'boolean', value: false },
166
+ site,
83
167
  });
84
168
  }
85
169
  }
86
170
  if (config.short !== undefined) {
87
- addSpelling(spellings, `-${config.short}`, { ...positive, role: 'short' });
171
+ addSpelling(claims, `-${config.short}`, { option: { ...positive, role: 'short' }, site });
88
172
  }
89
173
  }
90
- return spellings;
174
+ return new Map([...claims].map(([spelling, { option }]) => [spelling, option]));
175
+ }
176
+ /**
177
+ * Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
178
+ * separate value is never one. The parser and `locate` read each token through this rule.
179
+ */
180
+ export function isOptionToken(token) {
181
+ return token.startsWith('-');
182
+ }
183
+ /**
184
+ * A token that starts with `--` split at its first `=` into the spelling and the inline value, or
185
+ * `undefined` for any other token. The parser and `locate` split long tokens through this rule.
186
+ */
187
+ export function longToken(token) {
188
+ if (!token.startsWith('--')) {
189
+ return undefined;
190
+ }
191
+ const equals = token.indexOf('=');
192
+ return equals === -1
193
+ ? { inline: undefined, spelling: token }
194
+ : { inline: token.slice(equals + 1), spelling: token.slice(0, equals) };
91
195
  }
92
196
  function lookup(spellings, spelling) {
93
197
  const option = spellings.get(spelling);
@@ -96,20 +200,33 @@ function lookup(spellings, spelling) {
96
200
  }
97
201
  return option;
98
202
  }
203
+ /**
204
+ * The name of the string option one long spelling names in a table, which takes its value after
205
+ * `=`, or `undefined` for a Boolean, negative, short, or unknown spelling.
206
+ */
207
+ export function longStringOption(spellings, spelling) {
208
+ const option = spellings.get(spelling);
209
+ return option?.type === 'string' && option.role === 'long' ? option.name : undefined;
210
+ }
99
211
  function acceptValue({ option, spelling, values, next, inline, }) {
100
212
  const repeatable = option.type === 'string' && option.multiple;
101
213
  if (!repeatable && (values.strings.has(option.name) || values.booleans.has(option.name))) {
102
214
  throw new RepeatedOptionError(spelling);
103
215
  }
216
+ // A repeatable option records its last occurrence, because each one overwrites the entry.
217
+ values.spellings.set(option.name, spelling);
104
218
  if (option.type === 'boolean') {
105
219
  if (inline !== undefined) {
106
220
  throw new UnexpectedValueError(spelling, inline);
107
221
  }
108
222
  values.booleans.set(option.name, option.value);
109
- return false;
223
+ return 'alone';
110
224
  }
111
225
  const value = inline ?? next;
112
- if (value === undefined || (inline === undefined && value.startsWith('-'))) {
226
+ if (value === undefined) {
227
+ return { name: option.name, spelling };
228
+ }
229
+ if (inline === undefined && isOptionToken(value)) {
113
230
  throw new MissingValueError(spelling);
114
231
  }
115
232
  if (repeatable) {
@@ -120,14 +237,13 @@ function acceptValue({ option, spelling, values, next, inline, }) {
120
237
  else {
121
238
  values.strings.set(option.name, value);
122
239
  }
123
- return inline === undefined;
240
+ return inline === undefined ? 'next' : 'alone';
124
241
  }
125
242
  function parseOption(spellings, input, values) {
126
243
  const { token, next } = input;
127
- if (token.startsWith('--')) {
128
- const equals = token.indexOf('=');
129
- const spelling = equals === -1 ? token : token.slice(0, equals);
130
- const inline = equals === -1 ? undefined : token.slice(equals + 1);
244
+ const long = longToken(token);
245
+ if (long) {
246
+ const { inline, spelling } = long;
131
247
  return acceptValue({ inline, next, option: lookup(spellings, spelling), spelling, values });
132
248
  }
133
249
  if (token === '-') {
@@ -141,28 +257,52 @@ function parseOption(spellings, input, values) {
141
257
  throw new ShortGroupError({ reason: 'value-position', token: spelling });
142
258
  }
143
259
  const inline = suffix.startsWith('=') ? suffix.slice(1) : undefined;
144
- if (acceptValue({ inline, next, option, spelling, values })) {
145
- return true;
260
+ const reading = acceptValue({ inline, next, option, spelling, values });
261
+ if (reading !== 'alone') {
262
+ return reading;
146
263
  }
147
264
  }
148
- return false;
265
+ return 'alone';
266
+ }
267
+ /**
268
+ * Reads one option token into the scan and answers whether it took the next token too. The names
269
+ * it newly supplied join `supplied` in the order the values map first recorded them, so a repeated
270
+ * option keeps its first position.
271
+ */
272
+ function readOption(spellings, input, state) {
273
+ const known = state.values.spellings.size;
274
+ const reading = parseOption(spellings, input, state.values);
275
+ for (const name of [...state.values.spellings.keys()].slice(known)) {
276
+ state.supplied.push({ name, token: input.index });
277
+ }
278
+ if (typeof reading === 'object') {
279
+ state.awaiting = reading;
280
+ }
281
+ return reading === 'next';
149
282
  }
150
283
  /**
151
284
  * A hyphen token belongs to the globals when its long spelling or every short letter does. The
152
285
  * pre-scan reads the globals alone, so a letter it does not own is only "not a global option".
286
+ * The scan stops at a global value option, because the letters after it may be the value the
287
+ * operator meant to pass: the token then belongs to the globals, and parsing reports the
288
+ * value-position fault that names that option alone.
153
289
  */
154
290
  function isGlobalToken(spellings, token) {
155
- if (token.startsWith('--')) {
156
- const equals = token.indexOf('=');
157
- return spellings.has(equals === -1 ? token : token.slice(0, equals));
291
+ const long = longToken(token);
292
+ if (long) {
293
+ return spellings.has(long.spelling);
158
294
  }
159
295
  const group = token.slice(1).split('=')[0] ?? '';
160
296
  let global = '';
161
297
  let other = '';
162
298
  for (let index = 0; index < group.length; index += 1) {
163
299
  const letter = group.charAt(index);
164
- if (spellings.has(`-${letter}`)) {
300
+ const option = spellings.get(`-${letter}`);
301
+ if (option) {
165
302
  global = global === '' ? letter : global;
303
+ if (option.type === 'string') {
304
+ break;
305
+ }
166
306
  }
167
307
  else {
168
308
  other = other === '' ? letter : other;
@@ -177,52 +317,100 @@ function isGlobalToken(spellings, token) {
177
317
  return true;
178
318
  }
179
319
  /** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
180
- export function extractGlobals(spellings, tokens) {
181
- const values = emptyValues();
320
+ export function scanGlobals(spellings, tokens) {
321
+ const state = { awaiting: undefined, supplied: [], values: emptyValues() };
182
322
  const rest = [];
323
+ const positions = [];
183
324
  for (let index = 0; index < tokens.length; index += 1) {
184
325
  const token = tokens[index];
185
326
  if (token === undefined) {
186
327
  break;
187
328
  }
188
329
  if (token === '--') {
189
- rest.push(...tokens.slice(index));
190
- return { rest, values };
330
+ // One push per token, because a spread call would overflow the stack on a long list.
331
+ for (const [offset, tail] of tokens.slice(index).entries()) {
332
+ rest.push(tail);
333
+ positions.push(index + offset);
334
+ }
335
+ break;
191
336
  }
192
- if (!token.startsWith('-') || !isGlobalToken(spellings, token)) {
337
+ if (!isOptionToken(token) || !isGlobalToken(spellings, token)) {
193
338
  rest.push(token);
339
+ positions.push(index);
194
340
  }
195
- else if (parseOption(spellings, { next: tokens[index + 1], token }, values)) {
341
+ else if (readOption(spellings, { index, next: tokens[index + 1], token }, state)) {
196
342
  index += 1;
197
343
  }
198
344
  }
345
+ return { ...state, positions, rest };
346
+ }
347
+ /** The pre-scan of a complete invocation, where a global still waiting for its value is a fault. */
348
+ export function extractGlobals(spellings, tokens) {
349
+ const { awaiting, rest, values } = scanGlobals(spellings, tokens);
350
+ if (awaiting) {
351
+ throw new MissingValueError(awaiting.spelling);
352
+ }
199
353
  return { rest, values };
200
354
  }
355
+ /**
356
+ * Whether one option holds a value a tier supplied: a token in any spelling it accepts, or a fill
357
+ * from an input source. A declared default is never in these maps, so it never counts.
358
+ */
359
+ export function isSupplied(values, name) {
360
+ return values.strings.has(name) || values.lists.has(name) || values.booleans.has(name);
361
+ }
362
+ /** One run's own copy of parsed values, which the input-source stage fills without touching argv's. */
363
+ export function copyValues(values) {
364
+ return {
365
+ booleans: new Map(values.booleans),
366
+ lists: new Map([...values.lists].map(([name, list]) => [name, [...list]])),
367
+ spellings: new Map(values.spellings),
368
+ strings: new Map(values.strings),
369
+ };
370
+ }
201
371
  /** Global and local keys never overlap, so one merged view feeds a single validation pass. */
202
372
  export function mergeValues(globals, locals) {
203
373
  return {
204
374
  booleans: new Map([...globals.booleans, ...locals.booleans]),
205
375
  lists: new Map([...globals.lists, ...locals.lists]),
376
+ spellings: new Map([...globals.spellings, ...locals.spellings]),
206
377
  strings: new Map([...globals.strings, ...locals.strings]),
207
378
  };
208
379
  }
209
- export function parseInputs(spellings, tokens) {
210
- const options = emptyValues();
380
+ /** Reads one Command's tokens into options, positionals, and the passthrough tail. */
381
+ export function scanInputs(spellings, tokens) {
382
+ const state = { awaiting: undefined, supplied: [], values: emptyValues() };
211
383
  const positionals = [];
384
+ const read = (passthrough, delimited) => ({
385
+ awaiting: state.awaiting,
386
+ delimited,
387
+ options: state.values,
388
+ passthrough,
389
+ positionals,
390
+ supplied: state.supplied,
391
+ });
212
392
  for (let index = 0; index < tokens.length; index += 1) {
213
393
  const token = tokens[index];
214
394
  if (token === undefined) {
215
395
  break;
216
396
  }
217
397
  if (token === '--') {
218
- return { options, passthrough: tokens.slice(index + 1), positionals };
398
+ return read(tokens.slice(index + 1), true);
219
399
  }
220
- if (!token.startsWith('-')) {
400
+ if (!isOptionToken(token)) {
221
401
  positionals.push(token);
222
402
  }
223
- else if (parseOption(spellings, { next: tokens[index + 1], token }, options)) {
403
+ else if (readOption(spellings, { index, next: tokens[index + 1], token }, state)) {
224
404
  index += 1;
225
405
  }
226
406
  }
227
- return { options, passthrough: [], positionals };
407
+ return read([], false);
408
+ }
409
+ /** Parses a complete invocation's local tokens, where a waiting option is a missing value. */
410
+ export function parseInputs(spellings, tokens) {
411
+ const { awaiting, options, passthrough, positionals } = scanInputs(spellings, tokens);
412
+ if (awaiting) {
413
+ throw new MissingValueError(awaiting.spelling);
414
+ }
415
+ return { options, passthrough, positionals };
228
416
  }
package/dist/output.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { RenderingPolicy } from './rendering.js';
4
4
  import type { Palette } from './style-state.js';
5
5
  import type { ActionChannel, Host, OpenResult, Out, ResultBinding, ViewContext } from './types.js';
6
6
  import type { ViewRegistry } from './view.js';
7
- type WriteState = {
7
+ export type WriteState = {
8
8
  kind: 'ok';
9
9
  } | {
10
10
  kind: 'failed';
@@ -19,6 +19,7 @@ export declare class Output {
19
19
  private readonly signal;
20
20
  private readonly destinations;
21
21
  private renderFault;
22
+ private readonly viewFaults;
22
23
  private readonly stops;
23
24
  private route;
24
25
  /**
@@ -26,6 +27,8 @@ export declare class Output {
26
27
  * declaration a call answers to is checked where the action was authored.
27
28
  */
28
29
  readonly out: Out<OpenResult>;
30
+ /** The channel a configuration source receives: `out` with the results call naming a source. */
31
+ readonly sourceOut: Out<OpenResult>;
29
32
  private palette;
30
33
  private policy;
31
34
  private registry;
@@ -54,7 +57,7 @@ export declare class Output {
54
57
  private resultFault;
55
58
  /** The registry one invocation resolves through, republished as each contributor is read. */
56
59
  useViews(registry: ViewRegistry): void;
57
- /** The routed path, published once routing resolved it, which an incomplete sequence names. */
60
+ /** The path routing walked, updated as each name routes, which an incomplete sequence names. */
58
61
  useRoute(path: readonly string[]): void;
59
62
  /** What this invocation's output raised beside its calls, in the order it was raised. */
60
63
  get stopped(): readonly unknown[];
@@ -110,6 +113,12 @@ export declare class Output {
110
113
  * awaits the call must not end the process with an unhandled rejection.
111
114
  */
112
115
  private renderFailed;
116
+ /**
117
+ * Whether one value is what a view or a message check failed with during this invocation. A
118
+ * broken view is a defect in the code that broke the view contract, so an action or a source that
119
+ * lets its rejection propagate reports it as that defect.
120
+ */
121
+ raisedByView(value: unknown): boolean;
113
122
  /** What a view failed with during this invocation, if one did. */
114
123
  get fault(): {
115
124
  cause: unknown;