@loomcli/core 0.1.1 → 0.3.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 (57) hide show
  1. package/dist/application.d.ts +73 -28
  2. package/dist/application.js +334 -99
  3. package/dist/chain.d.ts +68 -0
  4. package/dist/chain.js +372 -0
  5. package/dist/command.d.ts +201 -46
  6. package/dist/command.js +713 -57
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -51
  10. package/dist/errors.js +58 -90
  11. package/dist/extension.d.ts +99 -0
  12. package/dist/extension.js +330 -0
  13. package/dist/facts.d.ts +39 -0
  14. package/dist/facts.js +95 -0
  15. package/dist/globals.d.ts +49 -28
  16. package/dist/globals.js +104 -58
  17. package/dist/glyphs.generated.d.ts +464 -0
  18. package/dist/glyphs.generated.js +491 -0
  19. package/dist/host.js +2 -1
  20. package/dist/index.d.ts +21 -6
  21. package/dist/index.js +7 -2
  22. package/dist/inspect.d.ts +69 -12
  23. package/dist/inspect.js +83 -26
  24. package/dist/lanes.d.ts +26 -0
  25. package/dist/lanes.js +45 -0
  26. package/dist/options.d.ts +7 -0
  27. package/dist/options.js +9 -0
  28. package/dist/output.d.ts +93 -15
  29. package/dist/output.js +307 -34
  30. package/dist/plugin.d.ts +132 -0
  31. package/dist/plugin.js +278 -0
  32. package/dist/rendering.d.ts +21 -0
  33. package/dist/rendering.js +72 -0
  34. package/dist/sequence.d.ts +41 -0
  35. package/dist/sequence.js +225 -0
  36. package/dist/signals.d.ts +52 -0
  37. package/dist/signals.js +85 -0
  38. package/dist/style-ansi.d.ts +13 -0
  39. package/dist/style-ansi.js +306 -0
  40. package/dist/style-layout.d.ts +29 -0
  41. package/dist/style-layout.js +228 -0
  42. package/dist/style-resolve.d.ts +6 -0
  43. package/dist/style-resolve.js +26 -0
  44. package/dist/style-state.d.ts +14 -0
  45. package/dist/style-state.js +179 -0
  46. package/dist/style-wire.d.ts +31 -0
  47. package/dist/style-wire.js +201 -0
  48. package/dist/style.d.ts +86 -0
  49. package/dist/style.js +201 -0
  50. package/dist/theme.d.ts +3 -0
  51. package/dist/theme.js +22 -0
  52. package/dist/types.d.ts +222 -26
  53. package/dist/validation.d.ts +12 -3
  54. package/dist/validation.js +34 -17
  55. package/dist/view.d.ts +180 -0
  56. package/dist/view.js +307 -0
  57. package/package.json +2 -1
@@ -0,0 +1,86 @@
1
+ import type { ApplicationEnvironment, RegisteredEnvironment } from './environment.js';
2
+ import type { Plugin, ThemeOf } from './plugin.js';
3
+ import type { Alignment } from './style-wire.js';
4
+ /** Concrete terminal foregrounds; backgrounds use the same palette indices. */
5
+ declare const colors: {
6
+ black: number;
7
+ blue: number;
8
+ brightBlack: number;
9
+ brightBlue: number;
10
+ brightCyan: number;
11
+ brightGreen: number;
12
+ brightMagenta: number;
13
+ brightRed: number;
14
+ brightWhite: number;
15
+ brightYellow: number;
16
+ cyan: number;
17
+ green: number;
18
+ magenta: number;
19
+ red: number;
20
+ white: number;
21
+ yellow: number;
22
+ };
23
+ type Ansi16Color = keyof typeof colors;
24
+ type ColorName = Ansi16Color;
25
+ interface ColorFallbacks {
26
+ readonly ansi256?: number | undefined;
27
+ readonly ansi16?: Ansi16Color | undefined;
28
+ }
29
+ type Ansi256Fallbacks = Pick<ColorFallbacks, 'ansi16'>;
30
+ type RgbArguments = [
31
+ red: number,
32
+ green: number,
33
+ blue: number,
34
+ fallbacks?: ColorFallbacks | undefined
35
+ ];
36
+ type Color = ColorName | readonly ['rgb', number, number, number, ColorFallbacks?] | readonly ['ansi256', number, Ansi256Fallbacks?];
37
+ declare const modifiers: readonly ['bold', 'faint', 'italic', 'underline', 'inverse', 'hidden', 'strikethrough', 'overline'];
38
+ type Modifier = (typeof modifiers)[number];
39
+ declare const tokens: readonly ['dim', 'primary', 'highlight', 'success', 'warning', 'error', 'info'];
40
+ type CoreToken = (typeof tokens)[number];
41
+ type Reset = 'reset' | 'resetForeground' | 'resetBackground';
42
+ type Operation = readonly ['foreground' | 'background', Color] | readonly ['modifier', Modifier] | readonly [Reset] | readonly ['token', string];
43
+ declare const semanticStyle: unique symbol;
44
+ type Style<Names extends string = CoreToken, Semantic extends boolean = false> = {
45
+ (text: string): string;
46
+ readonly [semanticStyle]: Semantic;
47
+ readonly escape: (text: string) => string;
48
+ readonly hex: (color: string, fallbacks?: ColorFallbacks) => Style<Names, Semantic>;
49
+ readonly bgHex: (color: string, fallbacks?: ColorFallbacks) => Style<Names, Semantic>;
50
+ readonly rgb: (...args: RgbArguments) => Style<Names, Semantic>;
51
+ readonly bgRgb: (...args: RgbArguments) => Style<Names, Semantic>;
52
+ readonly ansi256: (index: number, fallbacks?: Ansi256Fallbacks) => Style<Names, Semantic>;
53
+ readonly bgAnsi256: (index: number, fallbacks?: Ansi256Fallbacks) => Style<Names, Semantic>;
54
+ } & {
55
+ readonly [Name in ColorName | `bg${Capitalize<ColorName>}` | Modifier | Reset]: Style<Names, Semantic>;
56
+ } & {
57
+ readonly [Name in Names]: Style<Names, true>;
58
+ };
59
+ type ThemeNames<Contributor> = Contributor extends Plugin ? keyof ThemeOf<Contributor> & string : never;
60
+ type EnvironmentStyle<Environment extends ApplicationEnvironment<unknown, readonly Plugin[]>> = Style<CoreToken | ThemeNames<Environment['plugins'][number]>>;
61
+ type ContextualStyle = EnvironmentStyle<RegisteredEnvironment>;
62
+ type ConcreteStyle = Style;
63
+ type ThemeMapping = Readonly<Record<string, ConcreteStyle | undefined>>;
64
+ type ThemeConstraint<Mapping> = {
65
+ readonly [Key in keyof Mapping]: Key extends Exclude<keyof Style, CoreToken> | (typeof reservedCallableNames)[number] ? never : Mapping[Key];
66
+ };
67
+ declare const open = "\uE000";
68
+ declare const headerEnd = "\uE001";
69
+ declare const close = "\uE002";
70
+ declare const escape = "\uE003";
71
+ declare const chains: WeakMap<object, readonly Operation[]>;
72
+ declare const reservedCallableNames: readonly ['apply', 'arguments', 'bind', 'call', 'caller', 'constructor', 'length', 'name', 'prototype', 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', '__proto__', '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', 'then'];
73
+ declare const reservedStyleNames: Set<string>;
74
+ declare function frame(header: readonly unknown[], body: string): string;
75
+ declare function escapeText(text: string): string;
76
+ /** Copies options at the helper boundary; the wire parser uses the same value rules. */
77
+ declare function colorFallbacks(value: unknown, allow256: boolean): ColorFallbacks;
78
+ /** Properties are generated from the same catalogs the wire parser validates. */
79
+ declare function createStyle(names?: ReadonlySet<string>): ContextualStyle;
80
+ declare function isColorName(value: unknown): value is ColorName;
81
+ declare function pad(text: string, minimumWidth: number, options?: {
82
+ align?: Alignment;
83
+ }): string;
84
+ declare const style: Style;
85
+ export type { Ansi16Color, Ansi256Fallbacks, ColorFallbacks, ContextualStyle, Color, ColorName, ConcreteStyle, CoreToken, Modifier, Operation, Style, ThemeConstraint, ThemeMapping, };
86
+ export { colorFallbacks, chains, close, colors, createStyle, escape, escapeText, frame, headerEnd, isColorName, modifiers, open, pad, reservedStyleNames, style, tokens, };
package/dist/style.js ADDED
@@ -0,0 +1,201 @@
1
+ import { isPlainObject } from './facts.js';
2
+ /** Concrete terminal foregrounds; backgrounds use the same palette indices. */
3
+ const colors = {
4
+ black: 0,
5
+ blue: 4,
6
+ brightBlack: 8,
7
+ brightBlue: 12,
8
+ brightCyan: 14,
9
+ brightGreen: 10,
10
+ brightMagenta: 13,
11
+ brightRed: 9,
12
+ brightWhite: 15,
13
+ brightYellow: 11,
14
+ cyan: 6,
15
+ green: 2,
16
+ magenta: 5,
17
+ red: 1,
18
+ white: 7,
19
+ yellow: 3,
20
+ };
21
+ const modifiers = [
22
+ 'bold',
23
+ 'faint',
24
+ 'italic',
25
+ 'underline',
26
+ 'inverse',
27
+ 'hidden',
28
+ 'strikethrough',
29
+ 'overline',
30
+ ];
31
+ const tokens = ['dim', 'primary', 'highlight', 'success', 'warning', 'error', 'info'];
32
+ const open = '\uE000';
33
+ const headerEnd = '\uE001';
34
+ const close = '\uE002';
35
+ const escape = '\uE003';
36
+ const chains = new WeakMap();
37
+ const reservedCallableNames = [
38
+ 'apply',
39
+ 'arguments',
40
+ 'bind',
41
+ 'call',
42
+ 'caller',
43
+ 'constructor',
44
+ 'length',
45
+ 'name',
46
+ 'prototype',
47
+ 'toString',
48
+ 'toLocaleString',
49
+ 'valueOf',
50
+ 'hasOwnProperty',
51
+ 'isPrototypeOf',
52
+ 'propertyIsEnumerable',
53
+ '__proto__',
54
+ '__defineGetter__',
55
+ '__defineSetter__',
56
+ '__lookupGetter__',
57
+ '__lookupSetter__',
58
+ 'then',
59
+ ];
60
+ const reservedStyleNames = new Set([
61
+ ...Object.keys(colors),
62
+ ...Object.keys(colors).map((name) => `bg${name.charAt(0).toUpperCase()}${name.slice(1)}`),
63
+ ...modifiers,
64
+ 'reset',
65
+ 'resetForeground',
66
+ 'resetBackground',
67
+ 'hex',
68
+ 'bgHex',
69
+ 'rgb',
70
+ 'bgRgb',
71
+ 'ansi256',
72
+ 'bgAnsi256',
73
+ 'escape',
74
+ ...reservedCallableNames,
75
+ ]);
76
+ function textOnly(text) {
77
+ if (typeof text !== 'string') {
78
+ throw new TypeError('Style helpers require a string.');
79
+ }
80
+ return text;
81
+ }
82
+ function frame(header, body) {
83
+ const encoded = JSON.stringify(header).replace(/[\uE000-\uE003]/gu, (character) => `\\u${character.charCodeAt(0).toString(16)}`);
84
+ return `${open}${encoded}${headerEnd}${body}${close}`;
85
+ }
86
+ function escapeText(text) {
87
+ return textOnly(text).replace(/[\uE000-\uE003]/gu, (character) => `${escape}${character.charCodeAt(0).toString(16).toUpperCase()}`);
88
+ }
89
+ function byte(value) {
90
+ if (!Number.isInteger(value) || value < 0 || value > 255) {
91
+ throw new RangeError('Color channels and palette indices must be integers from 0 through 255.');
92
+ }
93
+ return value;
94
+ }
95
+ /** Copies options at the helper boundary; the wire parser uses the same value rules. */
96
+ function colorFallbacks(value, allow256) {
97
+ if (value === undefined) {
98
+ return {};
99
+ }
100
+ if (!isPlainObject(value) ||
101
+ Reflect.ownKeys(value).some((key) => key !== 'ansi16' && !(allow256 && key === 'ansi256'))) {
102
+ throw new TypeError('Color fallbacks must be a plain object with supported depth names.');
103
+ }
104
+ const ansi16 = value.ansi16;
105
+ const ansi256 = value.ansi256;
106
+ if (ansi16 !== undefined && !isColorName(ansi16)) {
107
+ throw new TypeError('The ansi16 fallback must be a terminal foreground color name.');
108
+ }
109
+ if (ansi256 !== undefined &&
110
+ (typeof ansi256 !== 'number' || !Number.isInteger(ansi256) || ansi256 < 0 || ansi256 > 255)) {
111
+ throw new RangeError('The ansi256 fallback must be an integer from 0 through 255.');
112
+ }
113
+ return {
114
+ ...(ansi16 === undefined ? {} : { ansi16 }),
115
+ ...(ansi256 === undefined ? {} : { ansi256 }),
116
+ };
117
+ }
118
+ function rgbColor([red, green, blue, fallbacks]) {
119
+ const rgb = ['rgb', byte(red), byte(green), byte(blue)];
120
+ const copied = colorFallbacks(fallbacks, true);
121
+ return Object.keys(copied).length === 0 ? rgb : [...rgb, copied];
122
+ }
123
+ function indexedColor(index, fallbacks) {
124
+ const color = ['ansi256', byte(index)];
125
+ const copied = colorFallbacks(fallbacks, false);
126
+ return Object.keys(copied).length === 0 ? color : [...color, copied];
127
+ }
128
+ function hexColor(value, fallbacks) {
129
+ if (typeof value !== 'string' || !/^#(?:[\da-f]{3}|[\da-f]{6})$/iu.test(value)) {
130
+ throw new TypeError('Hex colors must use #RGB or #RRGGBB.');
131
+ }
132
+ const full = value.length === 4
133
+ ? value.slice(1).replace(/[\da-f]/giu, (digit) => digit + digit)
134
+ : value.slice(1);
135
+ return rgbColor([
136
+ Number.parseInt(full.slice(0, 2), 16),
137
+ Number.parseInt(full.slice(2, 4), 16),
138
+ Number.parseInt(full.slice(4, 6), 16),
139
+ fallbacks,
140
+ ]);
141
+ }
142
+ /** Properties are generated from the same catalogs the wire parser validates. */
143
+ function createStyle(names = new Set(tokens)) {
144
+ function chain(operations) {
145
+ const callable = (text) => frame(['style', operations], textOnly(text));
146
+ const next = (operation) => chain([...operations, operation]);
147
+ const helpers = {
148
+ ansi256: (index, fallbacks) => next(['foreground', indexedColor(index, fallbacks)]),
149
+ bgAnsi256: (index, fallbacks) => next(['background', indexedColor(index, fallbacks)]),
150
+ bgHex: (value, fallbacks) => next(['background', hexColor(value, fallbacks)]),
151
+ bgRgb: (...args) => next(['background', rgbColor(args)]),
152
+ escape: escapeText,
153
+ hex: (value, fallbacks) => next(['foreground', hexColor(value, fallbacks)]),
154
+ rgb: (...args) => next(['foreground', rgbColor(args)]),
155
+ };
156
+ const properties = new Map(Object.entries(helpers).map(([name, helper]) => [name, () => helper]));
157
+ for (const name of Object.keys(colors)) {
158
+ if (isColorName(name)) {
159
+ properties.set(name, () => next(['foreground', name]));
160
+ properties.set(`bg${name.charAt(0).toUpperCase()}${name.slice(1)}`, () => next(['background', name]));
161
+ }
162
+ }
163
+ for (const name of modifiers) {
164
+ properties.set(name, () => next(['modifier', name]));
165
+ }
166
+ for (const name of ['reset', 'resetForeground', 'resetBackground']) {
167
+ properties.set(name, () => next([name]));
168
+ }
169
+ for (const name of names) {
170
+ properties.set(name, () => next(['token', name]));
171
+ }
172
+ const value = new Proxy(callable, {
173
+ get: (target, property, receiver) => typeof property === 'string' && properties.has(property)
174
+ ? properties.get(property)?.()
175
+ : Reflect.get(target, property, receiver),
176
+ });
177
+ chains.set(value, operations);
178
+ Object.freeze(value);
179
+ // Last resort: no typed path exists from runtime-generated callable properties to mapped keys.
180
+ // It holds because the catalogs above supply every declared helper and token; chains retain operations for validation.
181
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
182
+ return value;
183
+ }
184
+ return chain([]);
185
+ }
186
+ function isColorName(value) {
187
+ return typeof value === 'string' && Object.hasOwn(colors, value);
188
+ }
189
+ function pad(text, minimumWidth, options) {
190
+ textOnly(text);
191
+ if (!Number.isSafeInteger(minimumWidth) || minimumWidth < 0) {
192
+ throw new RangeError('Padding width must be a nonnegative safe integer.');
193
+ }
194
+ const align = options?.align ?? 'left';
195
+ if (align !== 'left' && align !== 'right' && align !== 'center') {
196
+ throw new TypeError('Padding alignment must be left, right, or center.');
197
+ }
198
+ return frame(['pad', minimumWidth, align], text);
199
+ }
200
+ const style = createStyle();
201
+ export { colorFallbacks, chains, close, colors, createStyle, escape, escapeText, frame, headerEnd, isColorName, modifiers, open, pad, reservedStyleNames, style, tokens, };
@@ -0,0 +1,3 @@
1
+ import type { Palette } from './style-state.js';
2
+ declare function buildTheme(value: unknown, identity: string): Palette;
3
+ export { buildTheme };
package/dist/theme.js ADDED
@@ -0,0 +1,22 @@
1
+ import { DeclarationError } from './errors.js';
2
+ import { isPlainObject } from './facts.js';
3
+ import { chains, reservedStyleNames } from './style.js';
4
+ function buildTheme(value, identity) {
5
+ if (!isPlainObject(value)) {
6
+ throw new DeclarationError(`Plugin "${identity}" must declare theme as a mapping of names to concrete style chains.`);
7
+ }
8
+ const palette = new Map();
9
+ for (const [name, chain] of Object.entries(value)) {
10
+ if (reservedStyleNames.has(name)) {
11
+ throw new DeclarationError(`Plugin "${identity}" theme name "${name}" shadows a built-in style member.`);
12
+ }
13
+ const operations = typeof chain === 'function' ? chains.get(chain) : undefined;
14
+ if (chain !== undefined &&
15
+ (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.`);
17
+ }
18
+ palette.set(name, operations ?? []);
19
+ }
20
+ return palette;
21
+ }
22
+ export { buildTheme };
package/dist/types.d.ts CHANGED
@@ -1,6 +1,10 @@
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 { ExtensionValue } from './extension.js';
5
+ import type { ResultNode } from './inspect.js';
6
+ import type { RenderingPolicy } from './rendering.js';
7
+ import type { ContextualStyle } from './style.js';
4
8
  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';
5
9
  type ShortAlias = LowercaseLetter | Uppercase<LowercaseLetter>;
6
10
  type OptionSpelling = {
@@ -32,6 +36,31 @@ type Omission = {
32
36
  } | {
33
37
  validateOmitted?: false;
34
38
  };
39
+ /**
40
+ * The one-line summary every projection reads. It is a core fact: optional, and a string that holds
41
+ * a character other than whitespace and no line terminator.
42
+ */
43
+ interface Described {
44
+ description?: string;
45
+ }
46
+ /**
47
+ * The two core facts a listing reads on a named Command and on an option.
48
+ * `hidden` keeps the member off every listing, and an omitted one reads `false`.
49
+ * `deprecated` is the one-line migration message a listing shows beside the member.
50
+ * Neither belongs to an argument, which cannot leave the grammar it sits in, or to the root.
51
+ */
52
+ interface Listed {
53
+ hidden?: boolean;
54
+ deprecated?: string;
55
+ }
56
+ /** The extension values one option declaration carries, whatever scope declares the option. */
57
+ interface OptionExtensions {
58
+ extensions?: readonly ExtensionValue<'option'>[];
59
+ }
60
+ /** The same slot on an argument declaration, typed by the target its values must name. */
61
+ interface ArgumentExtensions {
62
+ extensions?: readonly ExtensionValue<'argument'>[];
63
+ }
35
64
  /**
36
65
  * The tokens one declaration collects before validation: one string, or the whole collection. A
37
66
  * multiple option and a variadic argument collect alike, so they share this raw shape.
@@ -54,7 +83,7 @@ export type GlobalNameConstraint<Name extends string, Globals> = Name extends ke
54
83
  } : unknown;
55
84
  /** A required record key excludes open strings; distribution rejects each union member. */
56
85
  export type NameConstraint<Name extends string, Whole extends string = Name> = {} extends Record<Name, unknown> ? LiteralNameFault : Name extends Whole ? [Whole] extends [Name] ? unknown : LiteralNameFault : LiteralNameFault;
57
- export type ExitCode = 0 | 1 | 2;
86
+ export type ExitCode = 0 | 1 | 2 | 130 | 143;
58
87
  export interface InputTerminal {
59
88
  isTTY: boolean;
60
89
  }
@@ -63,6 +92,7 @@ export interface OutputTerminal extends InputTerminal {
63
92
  rows: number | undefined;
64
93
  }
65
94
  export interface Host {
95
+ platform: string;
66
96
  argv: string[];
67
97
  cwd: string;
68
98
  env: Record<string, string | undefined>;
@@ -76,7 +106,13 @@ export interface Host {
76
106
  stderr: Writable;
77
107
  }
78
108
  export interface RunOptions {
109
+ rendering?: RenderingPolicy;
79
110
  host?: Partial<Host>;
111
+ /**
112
+ * A caller-owned signal that cancels the run. Core subscribes to it at run entry and honors an
113
+ * abort at every phase boundary; it composes with an installed signals owner.
114
+ */
115
+ signal?: AbortSignal;
80
116
  }
81
117
  /** The declaration one schema call validates, under the name and the scope it was declared in. */
82
118
  export interface InputIdentity {
@@ -112,35 +148,173 @@ export type ValidationContext = {
112
148
  passthrough: readonly string[];
113
149
  supplied: SuppliedInputs;
114
150
  };
151
+ /** Immutable authoring and measurement context for one view destination. */
152
+ export interface ViewContext {
153
+ readonly style: ContextualStyle;
154
+ readonly width: (text: string) => number;
155
+ }
156
+ /** A pure synchronous view turns one typed value into the marked text core resolves. */
157
+ export interface View<Data> {
158
+ render: (data: Readonly<Data>, context: ViewContext) => string;
159
+ /** A view has one shape; the row view of Results is the other. */
160
+ row?: never;
161
+ }
162
+ /**
163
+ * A row view renders a sequence one row at a time.
164
+ * `head` and `tail` open and close the sequence, and each defaults to the empty string.
165
+ * Every function is pure and synchronous and owns the newlines in the text it returns.
166
+ */
167
+ export interface RowView<Row> {
168
+ row: (row: Readonly<Row>, index: number, context: ViewContext) => string;
169
+ head?: (context: ViewContext) => string;
170
+ tail?: (count: number, context: ViewContext) => string;
171
+ /** A row view has one shape; the whole view of Rendered output is the other. */
172
+ render?: never;
173
+ }
174
+ /** The views record of a value result: every entry renders the whole value. */
175
+ export type ResultViews<Value> = Readonly<Record<string, View<Value>>>;
176
+ /**
177
+ * The views record of a rows result: a whole view over the collected rows, which core buffers the
178
+ * sequence for, or a row view, which core feeds as the rows arrive.
179
+ */
180
+ export type RowViews<Row> = Readonly<Record<string, View<readonly Row[]> | RowView<Row>>>;
181
+ /**
182
+ * One view a result names, with its data type erased, as the view registry erases a declared
183
+ * view's. The write site reads each function back through the key that resolved it.
184
+ */
185
+ export type ResultView = View<never> | RowView<never>;
115
186
  /**
116
- * One value turned into the exact text core writes. A renderer owns every byte, the trailing
117
- * newline included. It is synchronous and pure: it receives the value alone, returns a string, and
118
- * holds no output handle. A throw or a non-string return is a renderer failure.
187
+ * One declared result as the write site reads it: the unit the action emits, the view names it
188
+ * declares in record order, and the key core renders when nothing selects another.
119
189
  */
120
- export interface Renderer<Data> {
121
- render: (data: Readonly<Data>) => string;
190
+ export interface DeclaredResult {
191
+ default: string;
192
+ kind: 'value' | 'rows';
193
+ views: ReadonlyMap<string, ResultView>;
122
194
  }
123
- export interface Out {
195
+ /** Phantom key. It brands the surface a lifecycle hook receives, so a forged value is not one. */
196
+ export declare const attachedCommand: unique symbol;
197
+ /**
198
+ * One Command as `onCommandAttach` receives it: the facts `inspect()` publishes, with their types
199
+ * erased, and the authoring calls a hook may make. Each call returns a new value whose facts hold
200
+ * what the call added, so a hook reads its own earlier calls back. The calls a hook cannot make are
201
+ * absent, because each of them changes what the action was compiled against or the graph's shape.
202
+ */
203
+ export interface AttachedCommand {
204
+ readonly [attachedCommand]: true;
205
+ /** The Command's own name, and `null` for the root. */
206
+ readonly name: string | null;
207
+ readonly path: readonly string[];
208
+ readonly hasAction: boolean;
209
+ /** The declared argument names, in declaration order. */
210
+ readonly arguments: readonly string[];
211
+ /** The declared local option names, in declaration order. */
212
+ readonly options: readonly string[];
213
+ readonly result: ResultNode | null;
214
+ argument(name: string, config: ArgumentConfig): AttachedCommand;
215
+ option(name: string, config: OptionConfig): AttachedCommand;
216
+ views(replacements: Readonly<Record<string, ResultView>>, options?: {
217
+ default?: string;
218
+ }): AttachedCommand;
219
+ extend(...values: readonly ExtensionValue<'command'>[]): AttachedCommand;
220
+ }
221
+ /**
222
+ * The lifecycle hook core calls once per Command at graph build, in installation order, each
223
+ * receiving what the previous plugin's hook returned.
224
+ */
225
+ export type CommandAttachHook = (command: AttachedCommand) => AttachedCommand;
226
+ /**
227
+ * What `out.results` accepts for one declared result. The declaration rides in the declared types
228
+ * as a closed discriminant, so a Command that declares none carries the neutral `unknown` and its
229
+ * `out.results` takes `never`. The parameter is never a union, so distribution reaches one member.
230
+ */
231
+ export type ResultInput<Result> = Result extends {
232
+ kind: 'value';
233
+ value: infer Value;
234
+ } ? Value : Result extends {
235
+ kind: 'rows';
236
+ row: infer Row;
237
+ } ? Iterable<Row> | AsyncIterable<Row> : never;
238
+ /**
239
+ * The result an `out` carries where the declaration is not in hand. Its `results` accepts any
240
+ * value, because the authoring call already checked what the Command declares, and the channel
241
+ * core builds for an action carries that declaration at run time.
242
+ */
243
+ export interface OpenResult {
244
+ kind: 'value';
245
+ value: unknown;
246
+ }
247
+ /** The record a `views()` call takes, which is the shape the carried result names. */
248
+ export type ResultViewsOf<Result> = Result extends {
249
+ kind: 'value';
250
+ value: infer Value;
251
+ } ? ResultViews<Value> : Result extends {
252
+ kind: 'rows';
253
+ row: infer Row;
254
+ } ? RowViews<Row> : never;
255
+ export interface Out<Result = unknown> {
124
256
  print(message: string): Promise<void>;
125
257
  info(message: string): Promise<void>;
126
258
  success(message: string): Promise<void>;
127
259
  warn(message: string): Promise<void>;
128
260
  error(message: string): Promise<void>;
129
- /** The neutral presentation call: a rendered value has no purpose and no destination. */
130
- render<Data>(data: Data, renderer: Renderer<Data>): Promise<void>;
261
+ /** The neutral view call: a rendered value has no purpose and no destination. */
262
+ render<Data>(data: Data, view: View<Data>): Promise<void>;
263
+ /** The same call over a sequence: core writes each row's text as the source yields it. */
264
+ render<Row>(rows: Iterable<Row> | AsyncIterable<Row>, view: RowView<Row>): Promise<void>;
265
+ /**
266
+ * The Command's own result, emitted once. Property syntax keeps the parameter contravariant, so
267
+ * a neutral `Out` never stands in for one that carries a declaration.
268
+ */
269
+ results: (value: ResultInput<Result>) => Promise<void>;
131
270
  fatal(message: string): never;
132
271
  }
133
- export type StringOption = OptionSpelling & Presence & Multiplicity & Omission & {
272
+ /**
273
+ * What the action's channel answers to: the routed path its diagnostics name, the result the
274
+ * routed Command declared, and the view this run selected. The declaration decides the
275
+ * destinations the channel carries, so the redirect is read from the graph once and never from the
276
+ * view a run selected; `view` decides the rendering alone, and is `null` when nothing selected one
277
+ * and the declaration's default stands.
278
+ */
279
+ export interface ResultBinding {
280
+ path: readonly string[];
281
+ result: DeclaredResult | undefined;
282
+ view: string | null;
283
+ }
284
+ /**
285
+ * The routed Command's invocation after parsing and validation, which every middleware reads. The
286
+ * values are what the action receives, the output of each declaration's schema, for that Command's
287
+ * own arguments and local options; global and plugin option values are not here. The records are
288
+ * untyped and frozen, because a middleware runs ahead of every action and the graph carries no
289
+ * type for a value.
290
+ */
291
+ export interface Request {
292
+ readonly args: Readonly<Record<string, unknown>>;
293
+ readonly options: Readonly<Record<string, unknown>>;
294
+ readonly passthrough: readonly string[];
295
+ }
296
+ /** The channel one action receives, with the emission the results lane holds it to. */
297
+ export interface ActionChannel {
298
+ out: Out<OpenResult>;
299
+ /** Whether the action emitted its result, which the missing rule reads after it returned. */
300
+ emitted: () => boolean;
301
+ /**
302
+ * Stops every sequence this channel still has pending. The action's own failure stays primary,
303
+ * so a sequence it never awaited is stopped rather than drained.
304
+ */
305
+ stop: () => void;
306
+ }
307
+ export type StringOption = OptionSpelling & Presence & Multiplicity & Omission & Described & Listed & OptionExtensions & {
134
308
  type: 'string';
135
309
  polarity?: never;
136
310
  validate?: StandardSchemaV1;
137
311
  };
138
312
  /** A variadic argument collects the remaining tokens, so it follows the multiple option rules. */
139
- export type VariadicArgument = Presence & {
313
+ export type VariadicArgument = Presence & Described & ArgumentExtensions & {
140
314
  variadic: true;
141
315
  validate?: StandardSchemaV1;
142
316
  };
143
- export type ScalarArgument = Presence & Omission & {
317
+ export type ScalarArgument = Presence & Omission & Described & ArgumentExtensions & {
144
318
  variadic?: false;
145
319
  validate?: StandardSchemaV1;
146
320
  };
@@ -198,7 +372,7 @@ export type ArgumentValue<Config extends ArgumentConfig> = Config extends {
198
372
  } | {
199
373
  validateOmitted: true;
200
374
  } ? never : undefined);
201
- export type BooleanOption = (OptionSpelling & {
375
+ export type BooleanOption = (OptionSpelling & Described & Listed & OptionExtensions & {
202
376
  type: 'boolean';
203
377
  validate?: never;
204
378
  default?: never;
@@ -206,7 +380,7 @@ export type BooleanOption = (OptionSpelling & {
206
380
  required?: never;
207
381
  validateOmitted?: never;
208
382
  polarity?: 'positive' | 'negative';
209
- }) | {
383
+ }) | (Described & Listed & OptionExtensions & {
210
384
  type: 'boolean';
211
385
  validate?: never;
212
386
  default?: never;
@@ -216,8 +390,24 @@ export type BooleanOption = (OptionSpelling & {
216
390
  polarity: 'both';
217
391
  short?: ShortAlias;
218
392
  shortOnly?: false;
219
- };
393
+ });
220
394
  export type OptionConfig = StringOption | BooleanOption;
395
+ /**
396
+ * The parsing part of a string option config, which is all a plugin option declares. A plugin
397
+ * option carries no schema and no presence rule, because the pre-scan consumes it ahead of routing,
398
+ * where the validation context every schema is promised cannot exist. Its middleware interprets
399
+ * the value.
400
+ * A Boolean plugin option is an ordinary `BooleanOption`, which already declares none of them.
401
+ */
402
+ export type PluginStringOption = OptionSpelling & Multiplicity & Described & Listed & OptionExtensions & {
403
+ type: 'string';
404
+ default?: string | string[];
405
+ polarity?: never;
406
+ required?: never;
407
+ validate?: never;
408
+ validateOmitted?: never;
409
+ };
410
+ export type PluginOptionConfig = PluginStringOption | BooleanOption;
221
411
  export type OptionValue<Config extends OptionConfig> = Config extends StringOption ? Config extends {
222
412
  multiple: true;
223
413
  } ? ValidatedValue<Config, string[]> : ValidatedValue<Config, string> | (Config extends {
@@ -227,34 +417,40 @@ export type OptionValue<Config extends OptionConfig> = Config extends StringOpti
227
417
  } | {
228
418
  validateOmitted: true;
229
419
  } ? never : undefined) : boolean;
230
- export interface ActionContext<Args, Options = {}> {
420
+ export interface ActionContext<Args, Options = {}, Result = unknown> {
421
+ readonly style: ContextualStyle;
231
422
  args: Args;
232
423
  options: Options;
233
424
  passthrough: string[];
234
- out: Out;
425
+ out: Out<Result>;
235
426
  host: Host;
427
+ /** The run's cancellation signal, which a caller or an installed signals owner aborts. */
428
+ signal: AbortSignal;
236
429
  }
237
- export type Action<Args, Options = {}> = (context: ActionContext<Args, Options>) => unknown;
430
+ export type Action<Args, Options = {}, Result = unknown> = (context: ActionContext<Args, Options, Result>) => unknown;
238
431
  /** Phantom key. It keeps the inferred declaration types exact and holds no runtime value. */
239
432
  export declare const declaredTypes: unique symbol;
240
433
  /**
241
- * The three inferred types one declaration carries. The phantom member keeps them exact. Args and
242
- * options widen, so a child can satisfy a looser reader. The globals appear in both a parameter and
243
- * a return position, which makes them invariant: a child's globals must be the parent's own type,
244
- * not a subset and not a superset, because one table serves every Command in the graph.
434
+ * Arguments and local options describe the action's own inputs. Globals are a requirement on the
435
+ * Receiving Application: a library that needs none can attach wherever its local keys are disjoint.
245
436
  */
246
- export interface DeclaredTypes<Args, Options, Globals> {
437
+ export interface DeclaredTypes<Args, Options, Globals, Result = unknown> {
247
438
  args: Args;
248
- globals: (value: Globals) => Globals;
439
+ globals: (value: Globals) => void;
249
440
  options: Options;
441
+ /**
442
+ * The result the Command declares, or `unknown` where it declares none. A plain field keeps it
443
+ * covariant, so a child that carries one still satisfies a neutral `Command` annotation.
444
+ */
445
+ result: Result;
250
446
  }
251
447
  /**
252
448
  * The handler one declaration accepts. It reads the phantom types, not the `action()` call, so it
253
449
  * holds on a fresh declaration, on a partly declared one, and on one that registered its action.
254
450
  */
255
451
  export type ActionHandler<Declaration> = Declaration extends {
256
- [declaredTypes]: DeclaredTypes<infer Args, infer Options, infer Globals>;
257
- } ? Action<Args, Globals & Options> : never;
452
+ [declaredTypes]: DeclaredTypes<infer Args, infer Options, infer Globals, infer Result>;
453
+ } ? Action<Args, Globals & Options, Result> : never;
258
454
  /** The args object an extracted handler receives for this declaration. */
259
455
  export type ActionArgs<Declaration> = ActionArgument<Declaration> extends {
260
456
  args: infer Args;