@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/theme.js CHANGED
@@ -1,19 +1,39 @@
1
- import { DeclarationError } from './errors.js';
2
- import { isPlainObject } from './facts.js';
1
+ import { DeclarationError, quoted } from './errors.js';
2
+ import { partFinding, slotSite } from './facts.js';
3
+ import { isPlainObject } from './plain.js';
4
+ import { themeMapping, themeNameTaken } from './plugin-rules.js';
3
5
  import { chains, reservedStyleNames } from './style.js';
6
+ /**
7
+ * One plugin's theme, read as the palette core resolves semantic styles through. Each fault marks
8
+ * the theme, or the one entry at fault, in `plugin(identity, { theme })`.
9
+ */
4
10
  function buildTheme(value, identity) {
11
+ const subject = `Plugin ${quoted(identity)}`;
12
+ const site = slotSite({ call: 'plugin', named: identity, subject }, 'theme', value);
5
13
  if (!isPlainObject(value)) {
6
- throw new DeclarationError(`Plugin "${identity}" must declare theme as a mapping of names to concrete style chains.`);
14
+ throw new DeclarationError(themeMapping, {
15
+ correction: 'Supply a mapping of names to concrete style chains.',
16
+ findings: [partFinding(site, [])],
17
+ sentence: `${subject} declares a theme that is not a mapping.`,
18
+ });
7
19
  }
8
20
  const palette = new Map();
9
21
  for (const [name, chain] of Object.entries(value)) {
10
22
  if (reservedStyleNames.has(name)) {
11
- throw new DeclarationError(`Plugin "${identity}" theme name "${name}" shadows a built-in style member.`);
23
+ throw new DeclarationError(themeNameTaken, {
24
+ correction: 'Rename the theme entry.',
25
+ findings: [partFinding(site, [name])],
26
+ sentence: `${subject} theme name ${quoted(name)} shadows a built-in style member.`,
27
+ });
12
28
  }
13
29
  const operations = typeof chain === 'function' ? chains.get(chain) : undefined;
14
30
  if (chain !== undefined &&
15
31
  (operations === undefined || operations.some((entry) => entry[0] === 'token'))) {
16
- throw new DeclarationError(`Plugin "${identity}" theme mapping "${name}" must be an unapplied concrete style chain without semantic tokens.`);
32
+ throw new DeclarationError(themeMapping, {
33
+ correction: 'Map the name to a concrete chain such as style.cyan.bold, without calling it or naming a semantic style.',
34
+ findings: [partFinding(site, [name])],
35
+ sentence: `${subject} theme mapping ${quoted(name)} is not an unapplied concrete style chain without semantic tokens.`,
36
+ });
17
37
  }
18
38
  palette.set(name, operations ?? []);
19
39
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Whether one value is a promise or another thenable. A thenable object and a promise from another
3
+ * realm are as unwaitable to a synchronous caller as a native one, so the test is the contract and
4
+ * not the class, and a function with a callable `then` counts, as Promises/A+ says. A value whose
5
+ * `then` cannot be read, such as a proxy whose trap throws, is not a thenable, so the test itself
6
+ * never throws.
7
+ */
8
+ export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
9
+ /**
10
+ * Attaches a rejection handler to a thenable a synchronous function returned, and otherwise
11
+ * ignores it. An unobserved rejection would end the process before the run could report anything.
12
+ * The thenable is adopted inside a fresh promise, so a `then` that throws rejects that promise
13
+ * instead of throwing here.
14
+ */
15
+ export declare function ignoreRejection(value: PromiseLike<unknown>): void;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Whether one value is a promise or another thenable. A thenable object and a promise from another
3
+ * realm are as unwaitable to a synchronous caller as a native one, so the test is the contract and
4
+ * not the class, and a function with a callable `then` counts, as Promises/A+ says. A value whose
5
+ * `then` cannot be read, such as a proxy whose trap throws, is not a thenable, so the test itself
6
+ * never throws.
7
+ */
8
+ export function isThenable(value) {
9
+ try {
10
+ return ((typeof value === 'object' || typeof value === 'function') &&
11
+ value !== null &&
12
+ 'then' in value &&
13
+ typeof value.then === 'function');
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /**
20
+ * Attaches a rejection handler to a thenable a synchronous function returned, and otherwise
21
+ * ignores it. An unobserved rejection would end the process before the run could report anything.
22
+ * The thenable is adopted inside a fresh promise, so a `then` that throws rejects that promise
23
+ * instead of throwing here.
24
+ */
25
+ export function ignoreRejection(value) {
26
+ void new Promise((resolve) => {
27
+ resolve(value);
28
+ }).catch(() => undefined);
29
+ }
@@ -0,0 +1,69 @@
1
+ import { LoomError } from './errors.js';
2
+ import type { FactSite } from './facts.js';
3
+ /** Phantom key. It brands a translation and holds no runtime value. */
4
+ declare const translation: unique symbol;
5
+ /**
6
+ * A class a translation is keyed on, such as `SyntaxError`. Its instance type is the type the
7
+ * translator receives, so the function needs no narrowing of its own.
8
+ */
9
+ type ErrorClass<Thrown extends object> = abstract new (...args: never[]) => Thrown;
10
+ /**
11
+ * A function that turns one foreign throw into one of the author's failures, or answers
12
+ * `undefined` to pass the throw on to the next translator.
13
+ */
14
+ type Translator<Thrown extends object> = (error: Thrown) => LoomError | undefined;
15
+ /** One translation as core reads it back: the key's prototype, its name, and the typed call. */
16
+ interface TranslationRecord {
17
+ /** The object a thrown value's prototype chain holds when the key's class constructed it. */
18
+ prototype: object;
19
+ /** The key's name, which a broken translator's diagnostic reports. */
20
+ name: string;
21
+ /**
22
+ * The translator, typed at `translate()` against its key. It answers `undefined` without calling
23
+ * the translator for a value its key does not claim as an instance.
24
+ */
25
+ offer: (thrown: object) => unknown;
26
+ }
27
+ /** The runtime value `translate()` returns. Its record lives in the registry above. */
28
+ declare class TranslationDeclaration {
29
+ readonly [translation]: true;
30
+ constructor(record: TranslationRecord);
31
+ }
32
+ /** An opaque translation pairing one error class with the translator that answers it. */
33
+ type Translation = Pick<TranslationDeclaration, typeof translation>;
34
+ /**
35
+ * Pairs an error class with the translator that turns its instances into a failure. The
36
+ * translator receives the thrown instance typed from the class, and a `translators` list on the
37
+ * Application or a plugin registers the pair.
38
+ */
39
+ declare function translate<Thrown extends object>(key: ErrorClass<Thrown>, translator: Translator<Thrown>): Translation;
40
+ /**
41
+ * One contributor's translations: how a diagnostic names it, and its records by key prototype,
42
+ * each list in the order the contributor listed it.
43
+ */
44
+ interface TranslationContributor {
45
+ /** The contributor as a diagnostic's subject, such as `the Application` or `plugin "@acme/http"`. */
46
+ subject: string;
47
+ byPrototype: ReadonlyMap<object, readonly TranslationRecord[]>;
48
+ }
49
+ /**
50
+ * Every contributor in resolution order: the application's translations, then each installed
51
+ * plugin's in installation order.
52
+ */
53
+ type TranslatorRegistry = readonly TranslationContributor[];
54
+ /**
55
+ * One contributor's `translators` list. `site` names the contributor at the start of a sentence,
56
+ * `The Application` or `Plugin "@acme/http"`, and holds the call that declared the list, which a
57
+ * fault marks. The slot is read defensively, because a JavaScript author reaches it with any value.
58
+ */
59
+ declare function readTranslations(site: FactSite, declared: unknown): TranslationContributor;
60
+ /**
61
+ * The failure the translators answer for one foreign throw, or `undefined` when the throw is never
62
+ * offered or every translator passed. Resolution follows the override walk: each contributor in
63
+ * order, the thrown value's chain walked in full at each, most derived first, and within one
64
+ * class the list order. A broken translator ends the walk with its defect, so no later translator
65
+ * hides it.
66
+ */
67
+ declare function translateThrow(registry: TranslatorRegistry, thrown: unknown): LoomError | undefined;
68
+ export type { ErrorClass, TranslationContributor, Translation, Translator, TranslatorRegistry };
69
+ export { readTranslations, translate, translateThrow };
@@ -0,0 +1,253 @@
1
+ import { spelled } from './diagnostic-text.js';
2
+ import { asSentence, DeclarationError, InternalError, LoomError, quoted, reasonOf, } from './errors.js';
3
+ import { partFinding } from './facts.js';
4
+ import { foreignValue, notAFunction, notAList, translationKey } from './plugin-rules.js';
5
+ import { prototypeChain } from './prototypes.js';
6
+ import { brokenTranslator, brokenTranslatorCorrection } from './rules.js';
7
+ import { ignoreRejection, isThenable } from './thenable.js';
8
+ /** Authored translations register here, so the public type publishes nothing to reach. */
9
+ const records = new WeakMap();
10
+ /** The runtime value `translate()` returns. Its record lives in the registry above. */
11
+ class TranslationDeclaration {
12
+ constructor(record) {
13
+ records.set(this, record);
14
+ Object.freeze(this);
15
+ }
16
+ }
17
+ /**
18
+ * The prototype a class key carries, or `undefined` for a value that is not a class. A class is a
19
+ * function whose `prototype` is the object its instances' chains hold, so an arrow function, a
20
+ * bound function, and every non-function are no key at all. A key whose `prototype` cannot be
21
+ * read, such as a proxy whose trap throws, is no key either.
22
+ */
23
+ function keyPrototype(key) {
24
+ let prototype = undefined;
25
+ try {
26
+ if (typeof key !== 'function' || !('prototype' in key)) {
27
+ return undefined;
28
+ }
29
+ prototype = key.prototype;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ return typeof prototype === 'object' && prototype !== null ? prototype : undefined;
35
+ }
36
+ /** A key's own name, or `undefined` when it has none that can be read. */
37
+ function nameOf(key) {
38
+ let name = undefined;
39
+ try {
40
+ name = 'name' in key ? key.name : undefined;
41
+ }
42
+ catch {
43
+ name = undefined;
44
+ }
45
+ return typeof name === 'string' && name !== '' ? name : undefined;
46
+ }
47
+ /**
48
+ * How a diagnostic names one key class: its name, quoted and escaped, or what it is when it has no
49
+ * name that can be read.
50
+ */
51
+ function keyName(key) {
52
+ const name = nameOf(key);
53
+ return name === undefined ? 'an anonymous class' : quoted(name);
54
+ }
55
+ /** A name JavaScript source can spell as an identifier, which a finding prints a class key as. */
56
+ const identifier = /^[A-Za-z_$][\w$]*$/u;
57
+ /**
58
+ * The finding for one `translate()` call, marking the argument at `mark`. A class key prints as
59
+ * its name, as its author wrote it, where any other function prints as an ellipsis.
60
+ */
61
+ function translateFinding(key, translator, mark) {
62
+ const name = typeof key === 'function' ? nameOf(key) : undefined;
63
+ const code = name !== undefined && identifier.test(name) ? spelled(name) : key;
64
+ return { arguments: [code, translator], call: 'translate', mark };
65
+ }
66
+ /**
67
+ * Whether a key claims a thrown value as its instance. A value whose chain cannot be read is not
68
+ * claimed, so its translator is never called and is not blamed for the failed read.
69
+ */
70
+ function claims(thrown, key) {
71
+ try {
72
+ return thrown instanceof key;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Pairs an error class with the translator that turns its instances into a failure. The
80
+ * translator receives the thrown instance typed from the class, and a `translators` list on the
81
+ * Application or a plugin registers the pair.
82
+ */
83
+ function translate(key, translator) {
84
+ const prototype = keyPrototype(key);
85
+ // A key whose prototype's chain cannot be read, such as through a proxy whose trap throws, is no
86
+ // Key either, so it is reported as a non-class before any failure-class check.
87
+ const chain = prototype === undefined ? undefined : prototypeChain(prototype);
88
+ if (prototype === undefined || chain === undefined) {
89
+ throw new DeclarationError(translationKey, {
90
+ correction: 'Supply an error class, such as SyntaxError.',
91
+ findings: [translateFinding(key, translator, '0')],
92
+ sentence: 'translate() received a key that is not a class.',
93
+ });
94
+ }
95
+ // Core never offers a failure to translators, so a translation keyed on a failure class never runs.
96
+ if (prototype === LoomError.prototype || chain.includes(LoomError.prototype)) {
97
+ throw new DeclarationError(translationKey, {
98
+ correction: 'Key the translation on the foreign class it replaces.',
99
+ findings: [translateFinding(key, translator, '0')],
100
+ sentence: 'translate() received a failure class as its key.',
101
+ });
102
+ }
103
+ if (typeof translator !== 'function') {
104
+ throw new DeclarationError(notAFunction, {
105
+ correction: 'Supply a function that returns a failure or undefined.',
106
+ findings: [translateFinding(key, translator, '1')],
107
+ sentence: 'translate() received a translator that is not a function.',
108
+ });
109
+ }
110
+ return new TranslationDeclaration({
111
+ name: keyName(key),
112
+ offer: (thrown) => (claims(thrown, key) ? translator(thrown) : undefined),
113
+ prototype,
114
+ });
115
+ }
116
+ /**
117
+ * One contributor's `translators` list. `site` names the contributor at the start of a sentence,
118
+ * `The Application` or `Plugin "@acme/http"`, and holds the call that declared the list, which a
119
+ * fault marks. The slot is read defensively, because a JavaScript author reaches it with any value.
120
+ */
121
+ function readTranslations(site, declared) {
122
+ const sentence = site.subject;
123
+ const byPrototype = new Map();
124
+ for (const record of translationRecords(site, declared)) {
125
+ const listed = byPrototype.get(record.prototype) ?? [];
126
+ listed.push(record);
127
+ byPrototype.set(record.prototype, listed);
128
+ }
129
+ return {
130
+ byPrototype,
131
+ subject: `${sentence.slice(0, 1).toLowerCase()}${sentence.slice(1)}`,
132
+ };
133
+ }
134
+ /** The fix a `translators` slot's list fault and entry fault share. */
135
+ const translationSupply = 'translate(ErrorClass, translator)';
136
+ /** The record behind each entry of one `translators` slot, whose every other value is its fault. */
137
+ function translationRecords(site, declared) {
138
+ if (declared !== undefined && !Array.isArray(declared)) {
139
+ throw new DeclarationError(notAList, {
140
+ correction: `Supply a list of values returned by ${translationSupply}.`,
141
+ findings: [partFinding(site, [])],
142
+ sentence: `${site.subject} declares translators that are not an array.`,
143
+ });
144
+ }
145
+ const list = declared ?? [];
146
+ // Array.from visits a hole as undefined, where map would skip it and leave the hole in place.
147
+ return Array.from(list, (entry, index) => {
148
+ const record = typeof entry === 'object' && entry !== null ? records.get(entry) : undefined;
149
+ if (!record) {
150
+ throw new DeclarationError(foreignValue, {
151
+ correction: `Supply the value returned by ${translationSupply}.`,
152
+ findings: [partFinding(site, [index])],
153
+ sentence: `${site.subject} holds a translator entry that is not a translation.`,
154
+ });
155
+ }
156
+ return record;
157
+ });
158
+ }
159
+ /**
160
+ * Every prototype in one thrown value's chain, most derived first, or `undefined` when the value is
161
+ * never offered: a value whose chain holds a failure class, and a value whose chain cannot be read,
162
+ * such as a proxy whose trap throws, or whose chain repeats a link or never ends. A value with a
163
+ * `null` prototype has an empty chain, which no key matches.
164
+ */
165
+ function offeredChain(thrown) {
166
+ const chain = prototypeChain(thrown);
167
+ return chain?.includes(LoomError.prototype) ? undefined : chain;
168
+ }
169
+ /** How a broken translator's diagnostic names a value that is not a failure. */
170
+ function returnedKind(value) {
171
+ if (value === null) {
172
+ return 'null';
173
+ }
174
+ if (isThenable(value)) {
175
+ return 'a promise';
176
+ }
177
+ const kind = typeof value;
178
+ return /^[aeiou]/u.test(kind) ? `an ${kind}` : `a ${kind}`;
179
+ }
180
+ /** Whether a translator's answer is a failure, read without letting a hostile value throw. */
181
+ function isFailure(value) {
182
+ try {
183
+ return value instanceof LoomError;
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ }
189
+ /** The defect of a broken translator under its rule, with the sentence and cause each shape gives. */
190
+ function brokenDefect(sentence, cause) {
191
+ return new InternalError(brokenTranslator, {
192
+ cause,
193
+ correction: brokenTranslatorCorrection,
194
+ sentence,
195
+ });
196
+ }
197
+ /**
198
+ * The defect of a translator that threw. Its cause is a new `AggregateError` that holds the
199
+ * translator's throw and then the original throw, so neither is lost and neither is mutated. The
200
+ * reason is read through `reasonOf`, which escapes it onto one line.
201
+ */
202
+ function threwDefect(who, error, thrown) {
203
+ const reason = reasonOf(error);
204
+ const cause = new AggregateError([error, thrown], 'The translator threw while translating the original throw.');
205
+ return brokenDefect(`${who} threw: ${asSentence(reason)}`, cause);
206
+ }
207
+ /** The one translator call, whose throw or non-failure answer is the defect of that translator. */
208
+ function consult(contributor, record, thrown) {
209
+ const who = `The translator ${contributor.subject} registered for ${record.name}`;
210
+ let answer = undefined;
211
+ try {
212
+ answer = record.offer(thrown);
213
+ }
214
+ catch (error) {
215
+ return threwDefect(who, error, thrown);
216
+ }
217
+ if (answer === undefined || isFailure(answer)) {
218
+ return answer;
219
+ }
220
+ // A translator is synchronous, so a returned promise is ignored once its rejection is observed.
221
+ if (isThenable(answer)) {
222
+ ignoreRejection(answer);
223
+ }
224
+ return brokenDefect(`${who} returned ${returnedKind(answer)} instead of a failure.`, thrown);
225
+ }
226
+ /**
227
+ * The failure the translators answer for one foreign throw, or `undefined` when the throw is never
228
+ * offered or every translator passed. Resolution follows the override walk: each contributor in
229
+ * order, the thrown value's chain walked in full at each, most derived first, and within one
230
+ * class the list order. A broken translator ends the walk with its defect, so no later translator
231
+ * hides it.
232
+ */
233
+ function translateThrow(registry, thrown) {
234
+ if ((typeof thrown !== 'object' && typeof thrown !== 'function') || thrown === null) {
235
+ return undefined;
236
+ }
237
+ // With no translation registered, the chain is never read.
238
+ const chain = registry.some((contributor) => contributor.byPrototype.size > 0)
239
+ ? (offeredChain(thrown) ?? [])
240
+ : [];
241
+ for (const contributor of registry) {
242
+ for (const prototype of chain) {
243
+ for (const record of contributor.byPrototype.get(prototype) ?? []) {
244
+ const answer = consult(contributor, record, thrown);
245
+ if (answer !== undefined) {
246
+ return answer;
247
+ }
248
+ }
249
+ }
250
+ }
251
+ return undefined;
252
+ }
253
+ export { readTranslations, translate, translateThrow };
package/dist/types.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /// <reference types="node" preserve="true" />
2
2
  import type { Readable, Writable } from 'node:stream';
3
3
  import type { StandardSchemaV1 } from '@standard-schema/spec';
4
+ import type { FailureExitCode } from './exit-codes.js';
4
5
  import type { ExtensionValue } from './extension.js';
5
- import type { ResultNode } from './inspect.js';
6
+ import type { CommandGraph, CommandNode, ResultNode } from './inspect.js';
6
7
  import type { RenderingPolicy } from './rendering.js';
7
8
  import type { ContextualStyle } from './style.js';
8
9
  type LowercaseLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
@@ -21,12 +22,23 @@ type Presence = {
21
22
  required?: false;
22
23
  default?: unknown;
23
24
  };
24
- /** The literal member keeps `multiple: true` exact under contextual typing, as `Presence` does. */
25
+ /**
26
+ * The literal member keeps `multiple: true` exact under contextual typing, as `Presence` does.
27
+ * A multiple option takes its list from the configuration source, so it binds no variable.
28
+ */
25
29
  type Multiplicity = {
26
30
  multiple: true;
27
- } | {
31
+ env?: never;
32
+ } | ({
28
33
  multiple?: false;
29
- };
34
+ } & EnvBinding);
35
+ /**
36
+ * The environment binding: the variable the input-source stage fills the option from when argv
37
+ * supplies none. The declaration names it explicitly, and core derives no name.
38
+ */
39
+ interface EnvBinding {
40
+ env?: string;
41
+ }
30
42
  /**
31
43
  * `validateOmitted: true` sends an omitted optional scalar to its own schema. The literal member
32
44
  * keeps the flag exact under contextual typing, as `Multiplicity` does.
@@ -62,15 +74,22 @@ interface ArgumentExtensions {
62
74
  extensions?: readonly ExtensionValue<'argument'>[];
63
75
  }
64
76
  /**
65
- * The tokens one declaration collects before validation: one string, or the whole collection. A
66
- * multiple option and a variadic argument collect alike, so they share this raw shape.
77
+ * The tokens one declaration collects before validation: one string, or several. A multiple option
78
+ * and a variadic argument collect alike, so they share this raw shape.
67
79
  */
68
80
  type RawValue<Config> = Config extends {
69
81
  multiple: true;
70
82
  } | {
71
83
  variadic: true;
72
84
  } ? string[] : string;
73
- type SchemaOutput<Schema, Raw> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<Schema> : Raw;
85
+ /** A multiple option and a variadic argument pass each value through the same validator. */
86
+ type PerValue<Config, Value> = Config extends {
87
+ multiple: true;
88
+ } | {
89
+ variadic: true;
90
+ } ? Value[] : Value;
91
+ /** The validated value: the validator's output for each value, or the raw shape without one. */
92
+ type SchemaOutput<Config, Schema, Raw> = Schema extends StandardSchemaV1 ? PerValue<Config, StandardSchemaV1.InferOutput<Schema>> : Raw;
74
93
  type SchemaInput<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<Schema> : string;
75
94
  /** The named key states the rule, so a rejected declaration name reads as its own diagnostic. */
76
95
  interface LiteralNameFault {
@@ -83,7 +102,8 @@ export type GlobalNameConstraint<Name extends string, Globals> = Name extends ke
83
102
  } : unknown;
84
103
  /** A required record key excludes open strings; distribution rejects each union member. */
85
104
  export type NameConstraint<Name extends string, Whole extends string = Name> = {} extends Record<Name, unknown> ? LiteralNameFault : Name extends Whole ? [Whole] extends [Name] ? unknown : LiteralNameFault : LiteralNameFault;
86
- export type ExitCode = 0 | 1 | 2 | 130 | 143;
105
+ /** The status `run()` resolves: success, a failure class's code, or a cancellation code. */
106
+ export type ExitCode = 0 | FailureExitCode | 130 | 143;
87
107
  export interface InputTerminal {
88
108
  isTTY: boolean;
89
109
  }
@@ -104,6 +124,14 @@ export interface Host {
104
124
  stdin: Readable;
105
125
  stdout: Writable;
106
126
  stderr: Writable;
127
+ /**
128
+ * Reads one source file for a defect's Developer Diagnostic, or answers `undefined`. Core calls
129
+ * it only in a development build, only while it reports a defect, and only for a path under the
130
+ * working directory. Process capture supplies a reader that refuses a file outside `cwd` after
131
+ * resolving symbolic links, and answers `undefined` for a path that is not a regular file or
132
+ * that exceeds 1 MiB.
133
+ */
134
+ readSource?: (path: string, cwd: string) => string | undefined;
107
135
  }
108
136
  export interface RunOptions {
109
137
  rendering?: RenderingPolicy;
@@ -324,19 +352,24 @@ export type ScalarArgument = Presence & Omission & Described & ArgumentExtension
324
352
  validate?: StandardSchemaV1;
325
353
  };
326
354
  export type ArgumentConfig = VariadicArgument | ScalarArgument;
327
- export type ValidatedValue<Config, Raw> = Config extends unknown ? 'validate' extends keyof Config ? SchemaOutput<Config['validate'], Raw> : Raw : never;
328
- /** Keep each conditional declaration paired with its own schema input type. */
355
+ export type ValidatedValue<Config, Raw> = Config extends unknown ? 'validate' extends keyof Config ? SchemaOutput<Config, Config['validate'], Raw> : Raw : never;
356
+ /** Keep each conditional declaration paired with its own validator input type. */
329
357
  export type DefaultConstraint<Config> = Config extends unknown ? Config & {
330
- default?: 'validate' extends keyof Config ? SchemaInput<Config['validate']> : RawValue<Config>;
358
+ default?: 'validate' extends keyof Config ? PerValue<Config, SchemaInput<Config['validate']>> : RawValue<Config>;
331
359
  } : never;
332
360
  /**
333
- * A multiple option hands its whole collection to one schema, so the declared schema must accept a
334
- * `string[]` input. The key names the fault, the way the other declaration constraints do.
361
+ * A multiple option or a variadic argument passes each value to its validator alone, so the
362
+ * declared validator must accept one `string`. The key names the fault, the way the other
363
+ * declaration constraints do.
335
364
  */
336
- export type MultipleConstraint<Config> = Config extends {
365
+ export type PerValueConstraint<Config> = Config extends {
337
366
  multiple: true;
338
- } ? 'validate' extends keyof Config ? string[] extends SchemaInput<Config['validate']> ? unknown : {
339
- 'A multiple option schema must accept a string[] input': Config['validate'];
367
+ } | {
368
+ variadic: true;
369
+ } ? 'validate' extends keyof Config ? [
370
+ SchemaInput<Config['validate']>
371
+ ] extends [never] ? unknown : string extends SchemaInput<Config['validate']> ? unknown : {
372
+ 'A validator of several values must accept one string input': Config['validate'];
340
373
  } : unknown : unknown;
341
374
  /**
342
375
  * The declaration rules `validateOmitted: true` needs: a schema that accepts `undefined`, an
@@ -362,12 +395,28 @@ export type ValidateOmittedConstraint<Config> = Config extends {
362
395
  } | {
363
396
  variadic: true;
364
397
  } ? {
365
- 'A collected input validates its omission as an empty array': never;
398
+ 'An input of several values receives no values as an empty array': never;
366
399
  } : 'validate' extends keyof Config ? undefined extends SchemaInput<Config['validate']> ? unknown : {
367
- 'A validateOmitted schema must accept an undefined input': Config['validate'];
400
+ 'A validateOmitted validator must accept an undefined input': Config['validate'];
368
401
  } : {
369
- 'validateOmitted needs a validate schema to receive the omission': never;
402
+ 'validateOmitted needs a validator to receive the omission': never;
370
403
  } : unknown;
404
+ /**
405
+ * A global option declares no presence rule, so its omission is always plain absence. A union
406
+ * config fails when any member declares the key, and a wide `OptionConfig` passes, because its
407
+ * members only allow the key.
408
+ */
409
+ export type GlobalOmissionConstraint<Config> = [Extract<Config, {
410
+ required: unknown;
411
+ }>] extends [
412
+ never
413
+ ] ? [Extract<Config, {
414
+ validateOmitted: unknown;
415
+ }>] extends [never] ? unknown : {
416
+ 'A global option declares no validateOmitted; its omission is plain absence': never;
417
+ } : {
418
+ 'A global option declares no required; the Commands that read it check for it': never;
419
+ };
371
420
  export type ArgumentValue<Config extends ArgumentConfig> = Config extends {
372
421
  variadic: true;
373
422
  } ? ValidatedValue<Config, string[]> : ValidatedValue<Config, string> | (Config extends {
@@ -377,7 +426,7 @@ export type ArgumentValue<Config extends ArgumentConfig> = Config extends {
377
426
  } | {
378
427
  validateOmitted: true;
379
428
  } ? never : undefined);
380
- export type BooleanOption = (OptionSpelling & Described & Listed & OptionExtensions & {
429
+ export type BooleanOption = (OptionSpelling & Described & Listed & EnvBinding & OptionExtensions & {
381
430
  type: 'boolean';
382
431
  validate?: never;
383
432
  default?: never;
@@ -385,7 +434,7 @@ export type BooleanOption = (OptionSpelling & Described & Listed & OptionExtensi
385
434
  required?: never;
386
435
  validateOmitted?: never;
387
436
  polarity?: 'positive' | 'negative';
388
- }) | (Described & Listed & OptionExtensions & {
437
+ }) | (Described & Listed & EnvBinding & OptionExtensions & {
389
438
  type: 'boolean';
390
439
  validate?: never;
391
440
  default?: never;
@@ -422,10 +471,16 @@ export type OptionValue<Config extends OptionConfig> = Config extends StringOpti
422
471
  } | {
423
472
  validateOmitted: true;
424
473
  } ? never : undefined) : boolean;
474
+ /**
475
+ * What an action receives. `graph` is the frozen graph `inspect()` returns for this run, and
476
+ * `command` is the routed node inside it: the same two values the run's middleware receive.
477
+ */
425
478
  export interface ActionContext<Args, Options = {}, Result = unknown> {
426
479
  readonly style: ContextualStyle;
427
480
  args: Args;
428
481
  options: Options;
482
+ readonly graph: CommandGraph;
483
+ readonly command: CommandNode;
429
484
  passthrough: string[];
430
485
  out: Out<Result>;
431
486
  host: Host;