@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,52 @@
1
+ /** The signals a plugin may claim, which is the closed set core installs process listeners for. */
2
+ type ProcessSignal = 'SIGINT' | 'SIGTERM';
3
+ /**
4
+ * What aborted one run's private controller. Core owns this value, so a middleware reads `source`
5
+ * and never infers a signal name; `cause` carries the caller's own `signal.reason` when a caller
6
+ * aborted. The first cause to abort fixes the reason and the code, and a later cause changes
7
+ * neither.
8
+ */
9
+ interface CancellationReason {
10
+ source: ProcessSignal | 'caller';
11
+ cause?: unknown;
12
+ }
13
+ /** The status each cause resolves. A script that saw 0 after an interrupt would carry on. */
14
+ declare const codes: {
15
+ readonly SIGINT: 130;
16
+ readonly SIGTERM: 143;
17
+ readonly caller: 130;
18
+ };
19
+ /** The status one cancelled run resolves, which is the part of `ExitCode` a signal decides. */
20
+ type CancellationCode = (typeof codes)[CancellationReason['source']];
21
+ /** Whether one declared value names a signal core installs a listener for. */
22
+ declare function isProcessSignal(value: unknown): value is ProcessSignal;
23
+ /** The status one cancelled run resolves, which the first cause to abort fixed. */
24
+ declare function cancellationCode(reason: CancellationReason): CancellationCode;
25
+ /**
26
+ * Whether one thrown value is the cancellation the run already reports: the reason core aborted
27
+ * with, which an API that rejects with `signal.reason` throws back, or an error every runtime
28
+ * names `AbortError`. The chain wraps an unexpected throw, so the wrapped cause reads the same.
29
+ */
30
+ declare function isCancellationEcho(thrown: unknown, reason: unknown): boolean;
31
+ /**
32
+ * The process listeners one run holds, and the caller subscription it opened at run entry. Install
33
+ * and removal sit together so the bracket a run keeps is readable in one place.
34
+ */
35
+ interface SignalBracket {
36
+ /** Installs one listener per claimed signal, once the graph has built and validated. */
37
+ install: (owned: readonly ProcessSignal[]) => void;
38
+ /** The reason the first cause fixed, or `undefined` while nothing has aborted the run. */
39
+ reason: () => CancellationReason | undefined;
40
+ /** Removes every listener this run holds. Called on the run's last exit path. */
41
+ finish: () => void;
42
+ }
43
+ /**
44
+ * The bracket for one run. Core subscribes to a caller's signal at run entry, and installs its own
45
+ * process listeners only for the plugin that owns the signals slot, only after the graph has built,
46
+ * and only until the run resolves. A process signal that arrives once the run is already cancelled
47
+ * is the force path: core removes its own listeners and re-raises, so the default disposition ends
48
+ * the process when no other listener remains. Core does not own the process.
49
+ */
50
+ declare function bracketRun(controller: AbortController, caller: AbortSignal | undefined): SignalBracket;
51
+ export type { CancellationCode, CancellationReason, ProcessSignal, SignalBracket };
52
+ export { bracketRun, cancellationCode, isCancellationEcho, isProcessSignal };
@@ -0,0 +1,85 @@
1
+ import { InternalError } from './errors.js';
2
+ /** The status each cause resolves. A script that saw 0 after an interrupt would carry on. */
3
+ const codes = { SIGINT: 130, SIGTERM: 143, caller: 130 };
4
+ /** The closed set a claim is drawn from, as the values a runtime signal name may take. */
5
+ const claimable = new Set(['SIGINT', 'SIGTERM']);
6
+ /** Whether one declared value names a signal core installs a listener for. */
7
+ function isProcessSignal(value) {
8
+ return typeof value === 'string' && claimable.has(value);
9
+ }
10
+ /** The status one cancelled run resolves, which the first cause to abort fixed. */
11
+ function cancellationCode(reason) {
12
+ return codes[reason.source];
13
+ }
14
+ /**
15
+ * Whether one thrown value is the cancellation the run already reports: the reason core aborted
16
+ * with, which an API that rejects with `signal.reason` throws back, or an error every runtime
17
+ * names `AbortError`. The chain wraps an unexpected throw, so the wrapped cause reads the same.
18
+ */
19
+ function isCancellationEcho(thrown, reason) {
20
+ if (thrown === reason) {
21
+ return true;
22
+ }
23
+ if (thrown instanceof Error && thrown.name === 'AbortError') {
24
+ return true;
25
+ }
26
+ return thrown instanceof InternalError && isCancellationEcho(thrown.cause, reason);
27
+ }
28
+ /**
29
+ * The bracket for one run. Core subscribes to a caller's signal at run entry, and installs its own
30
+ * process listeners only for the plugin that owns the signals slot, only after the graph has built,
31
+ * and only until the run resolves. A process signal that arrives once the run is already cancelled
32
+ * is the force path: core removes its own listeners and re-raises, so the default disposition ends
33
+ * the process when no other listener remains. Core does not own the process.
34
+ */
35
+ function bracketRun(controller, caller) {
36
+ let reason = undefined;
37
+ let held = [];
38
+ const release = () => {
39
+ for (const entry of held) {
40
+ process.off(entry.signal, entry.handler);
41
+ }
42
+ held = [];
43
+ };
44
+ const cancel = (next) => {
45
+ if (reason) {
46
+ return;
47
+ }
48
+ reason = next;
49
+ controller.abort(next);
50
+ };
51
+ const received = (signal) => {
52
+ if (reason) {
53
+ release();
54
+ process.kill(process.pid, signal);
55
+ return;
56
+ }
57
+ cancel({ source: signal });
58
+ };
59
+ const aborted = () => {
60
+ cancel({ cause: caller?.reason, source: 'caller' });
61
+ };
62
+ if (caller?.aborted === true) {
63
+ aborted();
64
+ }
65
+ else {
66
+ caller?.addEventListener('abort', aborted);
67
+ }
68
+ return {
69
+ finish: () => {
70
+ release();
71
+ caller?.removeEventListener('abort', aborted);
72
+ },
73
+ install: (owned) => {
74
+ for (const signal of owned) {
75
+ const handler = () => {
76
+ received(signal);
77
+ };
78
+ process.on(signal, handler);
79
+ held.push({ handler, signal });
80
+ }
81
+ },
82
+ reason: () => reason,
83
+ };
84
+ }
85
+ export { bracketRun, cancellationCode, isCancellationEcho, isProcessSignal };
@@ -0,0 +1,13 @@
1
+ import type { Capabilities } from './rendering.js';
2
+ import type { Attributes } from './style-state.js';
3
+ import type { Boundary, ParsedText } from './style-wire.js';
4
+ interface TextUnit {
5
+ kind: 'text' | 'control' | 'hyperlink' | 'sgr';
6
+ text: string;
7
+ attributes: Attributes;
8
+ }
9
+ type Unit = TextUnit | Boundary;
10
+ /** Scans the full decoded stream before emitting padding or generated terminal bytes. */
11
+ declare function scanAnsi(parsed: ParsedText, caps: Capabilities): Unit[];
12
+ export type { TextUnit, Unit };
13
+ export { scanAnsi };
@@ -0,0 +1,306 @@
1
+ import { applyOperations, emptyAttributes } from './style-state.js';
2
+ import { colors, isColorName } from './style.js';
3
+ /** Control strings are opaque through their terminator; payload bytes are never rescanned. */
4
+ function controlString(text, start, osc) {
5
+ for (let cursor = start; cursor < text.length; cursor += 1) {
6
+ const character = text.charCodeAt(cursor);
7
+ if ((osc && character === 7) || character === 156) {
8
+ return { body: text.slice(start, cursor), end: cursor + 1, kind: osc ? 'osc' : 'control' };
9
+ }
10
+ if (character === 27 && text[cursor + 1] === '\\') {
11
+ return { body: text.slice(start, cursor), end: cursor + 2, kind: osc ? 'osc' : 'control' };
12
+ }
13
+ }
14
+ return { body: '', end: text.length, kind: 'incomplete' };
15
+ }
16
+ function csi(text, start) {
17
+ for (let cursor = start; cursor < text.length; cursor += 1) {
18
+ const character = text.charCodeAt(cursor);
19
+ if (character >= 64 && character <= 126) {
20
+ return {
21
+ body: text.slice(start, cursor),
22
+ end: cursor + 1,
23
+ kind: character === 109 ? 'sgr' : 'control',
24
+ };
25
+ }
26
+ if (character < 32 || character > 63) {
27
+ return { body: '', end: cursor, kind: 'incomplete' };
28
+ }
29
+ }
30
+ return { body: '', end: text.length, kind: 'incomplete' };
31
+ }
32
+ function command(text, cursor) {
33
+ const code = text.charCodeAt(cursor);
34
+ if (code === 27) {
35
+ const next = text[cursor + 1];
36
+ if (next === '[') {
37
+ return csi(text, cursor + 2);
38
+ }
39
+ if (next === ']') {
40
+ return controlString(text, cursor + 2, true);
41
+ }
42
+ if (next === 'P' || next === 'X' || next === '^' || next === '_') {
43
+ return controlString(text, cursor + 2, false);
44
+ }
45
+ for (let end = cursor + 1; end < text.length; end += 1) {
46
+ const value = text.charCodeAt(end);
47
+ if (value >= 48 && value <= 126) {
48
+ return { body: '', end: end + 1, kind: 'control' };
49
+ }
50
+ if (value < 32 || value > 47) {
51
+ return { body: '', end, kind: 'incomplete' };
52
+ }
53
+ }
54
+ return { body: '', end: text.length, kind: 'incomplete' };
55
+ }
56
+ if (code === 155) {
57
+ return csi(text, cursor + 1);
58
+ }
59
+ if (code === 157) {
60
+ return controlString(text, cursor + 1, true);
61
+ }
62
+ if (code === 144 || code === 152 || code === 158 || code === 159) {
63
+ return controlString(text, cursor + 1, false);
64
+ }
65
+ if ((code < 32 && code !== 9 && code !== 10 && !(code === 13 && text[cursor + 1] === '\n')) ||
66
+ (code >= 127 && code <= 159)) {
67
+ return { body: '', end: cursor + 1, kind: 'control' };
68
+ }
69
+ return undefined;
70
+ }
71
+ const enableModifiers = new Map([
72
+ [1, 'bold'],
73
+ [2, 'faint'],
74
+ [3, 'italic'],
75
+ [4, 'underline'],
76
+ [7, 'inverse'],
77
+ [8, 'hidden'],
78
+ [9, 'strikethrough'],
79
+ [53, 'overline'],
80
+ ]);
81
+ const disableModifiers = new Map([
82
+ [22, ['bold', 'faint']],
83
+ [23, ['italic']],
84
+ [24, ['underline']],
85
+ [27, ['inverse']],
86
+ [28, ['hidden']],
87
+ [29, ['strikethrough']],
88
+ [55, ['overline']],
89
+ ]);
90
+ /** Raw ANSI overrides participate in Loom's scoped attributes, and resets restore their base. */
91
+ class AnsiState {
92
+ base = emptyAttributes;
93
+ foreground;
94
+ background;
95
+ modifiers = new Map();
96
+ scope;
97
+ computed;
98
+ saved = new WeakMap();
99
+ palette;
100
+ constructor(root, palette) {
101
+ this.palette = palette;
102
+ this.scope = root;
103
+ this.saved.set(root, emptyAttributes);
104
+ }
105
+ sync(scope) {
106
+ if (scope === this.scope) {
107
+ return;
108
+ }
109
+ this.saved.set(this.scope, this.attributes());
110
+ const entering = [];
111
+ let ancestor = scope;
112
+ while (ancestor && !this.saved.has(ancestor)) {
113
+ entering.push(ancestor);
114
+ ancestor = ancestor.parent;
115
+ }
116
+ let effective = ancestor ? (this.saved.get(ancestor) ?? emptyAttributes) : emptyAttributes;
117
+ for (let index = entering.length - 1; index >= 0; index -= 1) {
118
+ const next = entering[index];
119
+ if (next) {
120
+ effective = applyOperations(effective, next.operations, this.palette);
121
+ this.saved.set(next, effective);
122
+ }
123
+ }
124
+ this.scope = scope;
125
+ this.base = scope.attributes;
126
+ this.foreground = effective.foreground;
127
+ this.background = effective.background;
128
+ this.modifiers.clear();
129
+ for (const key of this.base.modifiers) {
130
+ this.modifiers.set(key, false);
131
+ }
132
+ for (const key of effective.modifiers) {
133
+ this.modifiers.set(key, true);
134
+ }
135
+ this.computed = undefined;
136
+ }
137
+ attributes() {
138
+ if (this.computed !== undefined) {
139
+ return this.computed;
140
+ }
141
+ if (this.foreground === undefined &&
142
+ this.background === undefined &&
143
+ this.modifiers.size === 0) {
144
+ return this.base;
145
+ }
146
+ const modifiers = new Set(this.base.modifiers);
147
+ for (const [name, on] of this.modifiers) {
148
+ if (on) {
149
+ modifiers.add(name);
150
+ }
151
+ else {
152
+ modifiers.delete(name);
153
+ }
154
+ }
155
+ this.computed = {
156
+ background: this.background ?? this.base.background,
157
+ foreground: this.foreground ?? this.base.foreground,
158
+ modifiers,
159
+ };
160
+ return this.computed;
161
+ }
162
+ sgr(body) {
163
+ this.computed = undefined;
164
+ if (!/^[\d;:]*$/u.test(body)) {
165
+ return '';
166
+ }
167
+ const parameters = body.split(';');
168
+ const unknown = [];
169
+ for (let index = 0; index < parameters.length; index += 1) {
170
+ const raw = parameters[index] ?? '';
171
+ const colon = raw.split(':');
172
+ const code = Number(colon[0]);
173
+ if (code === 0) {
174
+ this.foreground = undefined;
175
+ this.background = undefined;
176
+ this.modifiers.clear();
177
+ continue;
178
+ }
179
+ if (code === 39 || code === 49) {
180
+ this[code === 39 ? 'foreground' : 'background'] = undefined;
181
+ continue;
182
+ }
183
+ const enabled = enableModifiers.get(code);
184
+ if (enabled) {
185
+ this.modifiers.set(enabled, true);
186
+ continue;
187
+ }
188
+ const disabled = disableModifiers.get(code);
189
+ if (disabled) {
190
+ for (const name of disabled) {
191
+ this.modifiers.delete(name);
192
+ }
193
+ continue;
194
+ }
195
+ const background = (code >= 40 && code <= 47) || (code >= 100 && code <= 107) || code === 48;
196
+ const paletteIndex = code >= 30 && code <= 47
197
+ ? code - (background ? 40 : 30)
198
+ : code - (background ? 100 : 90) + 8;
199
+ if ((code >= 30 && code <= 37) ||
200
+ (code >= 40 && code <= 47) ||
201
+ (code >= 90 && code <= 97) ||
202
+ (code >= 100 && code <= 107)) {
203
+ const name = Object.keys(colors).find((key) => isColorName(key) && colors[key] === paletteIndex);
204
+ if (isColorName(name)) {
205
+ this[background ? 'background' : 'foreground'] = name;
206
+ }
207
+ continue;
208
+ }
209
+ if (code === 38 || code === 48) {
210
+ const mode = Number(colon.length > 1 ? colon[1] : parameters[index + 1]);
211
+ let count = 0;
212
+ if (mode === 2) {
213
+ count = 3;
214
+ }
215
+ else if (mode === 5) {
216
+ count = 1;
217
+ }
218
+ const values = colon.length > 1
219
+ ? colon.slice(colon.length - count).map(Number)
220
+ : parameters.slice(index + 2, index + 2 + count).map(Number);
221
+ if (colon.length === 1) {
222
+ index += 1 + count;
223
+ }
224
+ if (values.length !== count ||
225
+ !values.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)) {
226
+ continue;
227
+ }
228
+ const [red, green, blue] = values;
229
+ let color = undefined;
230
+ if (mode === 5 && red !== undefined) {
231
+ color = ['ansi256', red];
232
+ }
233
+ else if (mode === 2 && red !== undefined && green !== undefined && blue !== undefined) {
234
+ color = ['rgb', red, green, blue];
235
+ }
236
+ if (color !== undefined) {
237
+ this[background ? 'background' : 'foreground'] = color;
238
+ }
239
+ continue;
240
+ }
241
+ unknown.push(raw);
242
+ }
243
+ return unknown.length ? `\u001b[${unknown.join(';')}m` : '';
244
+ }
245
+ }
246
+ /** Scans the full decoded stream before emitting padding or generated terminal bytes. */
247
+ function scanAnsi(parsed, caps) {
248
+ const result = [];
249
+ const state = new AnsiState(parsed.root, parsed.palette);
250
+ let hyperlinkOpen = false;
251
+ function boundaries(start, end) {
252
+ for (let cursor = start; cursor < end; cursor += 1) {
253
+ for (const boundary of parsed.boundaries.get(cursor) ?? []) {
254
+ if (boundary.kind === 'open') {
255
+ state.sync(boundary.scope);
256
+ result.push({ ...boundary, attributes: state.attributes() });
257
+ }
258
+ else {
259
+ result.push(boundary);
260
+ }
261
+ }
262
+ }
263
+ }
264
+ for (let cursor = 0; cursor < parsed.text.length;) {
265
+ state.sync(parsed.scopes[cursor] ?? parsed.root);
266
+ const found = command(parsed.text, cursor);
267
+ if (found) {
268
+ boundaries(cursor, found.end);
269
+ state.sync(parsed.scopes[found.end - 1] ?? parsed.root);
270
+ const raw = parsed.text.slice(cursor, found.end);
271
+ if (found.kind === 'sgr') {
272
+ const unknown = state.sgr(found.body);
273
+ if (caps.terminalControls && unknown) {
274
+ result.push({ attributes: state.attributes(), kind: 'sgr', text: unknown });
275
+ }
276
+ }
277
+ else if (found.kind === 'osc' && found.body.startsWith('8;')) {
278
+ const separator = found.body.indexOf(';', 2);
279
+ if (separator !== -1 && caps.hyperlinks) {
280
+ hyperlinkOpen = found.body.slice(separator + 1) !== '';
281
+ result.push({ attributes: state.attributes(), kind: 'hyperlink', text: raw });
282
+ }
283
+ }
284
+ else if (found.kind !== 'incomplete' && caps.terminalControls) {
285
+ result.push({ attributes: state.attributes(), kind: 'control', text: raw });
286
+ }
287
+ cursor = found.end;
288
+ continue;
289
+ }
290
+ boundaries(cursor, cursor + 1);
291
+ state.sync(parsed.scopes[cursor] ?? parsed.root);
292
+ const character = parsed.text[cursor] ?? '';
293
+ // The LF owns the complete line-ending atom, so deferred spaces cannot split CRLF.
294
+ if (!(character === '\r' && parsed.text[cursor + 1] === '\n')) {
295
+ const text = character === '\n' && parsed.text[cursor - 1] === '\r' ? '\r\n' : character;
296
+ result.push({ attributes: state.attributes(), kind: 'text', text });
297
+ }
298
+ cursor += 1;
299
+ }
300
+ boundaries(parsed.text.length, parsed.text.length + 1);
301
+ if (hyperlinkOpen) {
302
+ result.push({ attributes: emptyAttributes, kind: 'hyperlink', text: '\u001b]8;;\u001b\\' });
303
+ }
304
+ return result;
305
+ }
306
+ export { scanAnsi };
@@ -0,0 +1,29 @@
1
+ import type { TextUnit, Unit } from './style-ansi.js';
2
+ /** Width at each tab-stop offset composes without rescanning nested text. */
3
+ interface Metrics {
4
+ advance: readonly number[];
5
+ tabs: boolean;
6
+ text: boolean;
7
+ }
8
+ type Rope = ({
9
+ kind: 'leaf';
10
+ unit: TextUnit;
11
+ tab: boolean;
12
+ } | {
13
+ kind: 'join';
14
+ children: readonly Rope[];
15
+ }) & Metrics;
16
+ interface Line {
17
+ content: Rope;
18
+ ending: readonly TextUnit[];
19
+ }
20
+ interface Block {
21
+ lines: readonly Line[];
22
+ minimum: number;
23
+ tabs: boolean;
24
+ }
25
+ /** Nested padding resolves inside outward; unchanged content retains its rope and analysis. */
26
+ declare function layout(units: readonly Unit[]): Block;
27
+ declare function renderedUnits(block: Block): Generator<TextUnit>;
28
+ declare function blockWidth(block: Block): number;
29
+ export { blockWidth, layout, renderedUnits };