tickmarkr 1.79.0 → 1.80.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.
@@ -0,0 +1,179 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import { TEXT_EMPHASIS_TOKENS } from "./theme.js";
3
+ export { SPARKLINE_BUCKET_WINDOW } from "./derive.js";
4
+ export declare const PANEL_CHROME_ROWS = 3;
5
+ export declare const BAND_CONTINUATION_PREFIX = "\u21B3 ";
6
+ export type BandColumnContent = {
7
+ readonly title: string;
8
+ readonly lines: readonly string[];
9
+ };
10
+ export type BandAllocationOptions = {
11
+ readonly chromeColumns?: number;
12
+ readonly minimumReadableColumns?: number;
13
+ };
14
+ export type TextEmphasis = keyof typeof TEXT_EMPHASIS_TOKENS;
15
+ export type ComponentState = "active" | "inactive" | "pass" | "fail" | "warn" | "neutral";
16
+ export type KeybarSurface = "run" | "setup";
17
+ export type KeybarItem = {
18
+ readonly key: string;
19
+ readonly label: string;
20
+ };
21
+ export declare const KEYBAR_KEYS: {
22
+ readonly run: readonly [{
23
+ readonly key: "↑↓";
24
+ readonly label: "Move";
25
+ }, {
26
+ readonly key: "⏎";
27
+ readonly label: "Open";
28
+ }, {
29
+ readonly key: "←";
30
+ readonly label: "Back";
31
+ }, {
32
+ readonly key: "Tab";
33
+ readonly label: "Panel";
34
+ }, {
35
+ readonly key: "?";
36
+ readonly label: "Help";
37
+ }, {
38
+ readonly key: "q";
39
+ readonly label: "Quit";
40
+ }, {
41
+ readonly key: "f";
42
+ readonly label: "Follow";
43
+ }, {
44
+ readonly key: "/";
45
+ readonly label: "Filter";
46
+ }];
47
+ readonly setup: readonly [{
48
+ readonly key: "↑↓";
49
+ readonly label: "Move";
50
+ }, {
51
+ readonly key: "⏎";
52
+ readonly label: "Next";
53
+ }, {
54
+ readonly key: "←";
55
+ readonly label: "Back";
56
+ }, {
57
+ readonly key: "Tab";
58
+ readonly label: "Panel";
59
+ }, {
60
+ readonly key: "?";
61
+ readonly label: "Help";
62
+ }, {
63
+ readonly key: "q";
64
+ readonly label: "Quit";
65
+ }, {
66
+ readonly key: "␣";
67
+ readonly label: "Toggle";
68
+ }, {
69
+ readonly key: "a";
70
+ readonly label: "All";
71
+ }, {
72
+ readonly key: "r";
73
+ readonly label: "Re-probe";
74
+ }, {
75
+ readonly key: "s";
76
+ readonly label: "Save";
77
+ }, {
78
+ readonly key: "n";
79
+ readonly label: "Next";
80
+ }, {
81
+ readonly key: "p";
82
+ readonly label: "Prev";
83
+ }];
84
+ readonly setupAdditional: readonly [{
85
+ readonly key: "␣";
86
+ readonly label: "Toggle";
87
+ }, {
88
+ readonly key: "a";
89
+ readonly label: "All";
90
+ }, {
91
+ readonly key: "r";
92
+ readonly label: "Re-probe";
93
+ }, {
94
+ readonly key: "s";
95
+ readonly label: "Save";
96
+ }, {
97
+ readonly key: "n";
98
+ readonly label: "Next";
99
+ }, {
100
+ readonly key: "p";
101
+ readonly label: "Prev";
102
+ }];
103
+ };
104
+ export type JournalRow = {
105
+ readonly id?: string;
106
+ readonly time: string;
107
+ readonly state: ComponentState;
108
+ readonly text: string;
109
+ };
110
+ export declare function BodyText({ children, emphasis, }: {
111
+ children: ReactNode;
112
+ emphasis?: TextEmphasis;
113
+ }): ReactElement;
114
+ /**
115
+ * Give each panel the columns its own longest line calls for. Surplus is shared
116
+ * evenly after every line fits; scarcity is apportioned by unmet content need.
117
+ */
118
+ export declare function allocateBandColumns(columns: number, content: readonly BandColumnContent[], options?: BandAllocationOptions): readonly number[];
119
+ /**
120
+ * Compose one data line into visible rows. A fitting line is byte-for-byte
121
+ * untouched. Every wrapped row carries a continuation marker before any
122
+ * separator or data token, so it cannot masquerade as a new item.
123
+ */
124
+ export declare function composeBandLine(text: string, columns: number): readonly string[];
125
+ export declare function BandLines({ lines, columns, emphasis, }: {
126
+ lines: readonly string[];
127
+ columns: number;
128
+ emphasis?: TextEmphasis;
129
+ }): ReactElement;
130
+ /** Rows drawn by a band of bordered panels at the same allocated widths. */
131
+ export declare function measureBandRows(columns: number, content: readonly BandColumnContent[]): number;
132
+ export declare function StateGlyph({ state }: {
133
+ state: ComponentState;
134
+ }): ReactElement;
135
+ export declare function Panel({ title, focused, width, flexGrow, children, }: {
136
+ title: string;
137
+ focused?: boolean;
138
+ width?: number | string;
139
+ flexGrow?: number;
140
+ children: ReactNode;
141
+ }): ReactElement;
142
+ export declare function CockpitGrid({ children, columns, columnContents, allocation, }: {
143
+ children: ReactNode;
144
+ columns?: number;
145
+ columnContents?: readonly BandColumnContent[];
146
+ allocation?: BandAllocationOptions;
147
+ }): ReactElement;
148
+ export declare function Sparkline({ samples, }: {
149
+ samples: readonly (number | null)[];
150
+ }): ReactElement;
151
+ export declare function StatTile({ label, value, samples, focused, width, }: {
152
+ label: string;
153
+ value: ReactNode;
154
+ samples: readonly (number | null)[];
155
+ focused?: boolean;
156
+ width?: number | string;
157
+ }): ReactElement;
158
+ export declare function ProgressMeter({ value, width, }: {
159
+ value: number;
160
+ width?: number;
161
+ }): ReactElement;
162
+ export declare function Keybar({ surface, width, }: {
163
+ surface: KeybarSurface;
164
+ width: number;
165
+ }): ReactElement;
166
+ export type StatusStripItem = {
167
+ readonly state: ComponentState;
168
+ readonly text: string;
169
+ };
170
+ export declare function StatusStrip({ items, width, }: {
171
+ items: readonly StatusStripItem[];
172
+ width: number;
173
+ }): ReactElement;
174
+ export declare function JournalRowPanel({ rows, width, focused, title, }: {
175
+ rows: readonly JournalRow[];
176
+ width?: number | string;
177
+ focused?: boolean;
178
+ title?: string;
179
+ }): ReactElement;
@@ -0,0 +1,280 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { Children } from "react";
4
+ import stringWidth from "string-width";
5
+ import { GLYPHS } from "../../brand.js";
6
+ import { COCKPIT_DATA_RAMP, COCKPIT_INKS, TEXT_EMPHASIS_TOKENS, } from "./theme.js";
7
+ import { SPARKLINE_BUCKET_WINDOW } from "./derive.js";
8
+ export { SPARKLINE_BUCKET_WINDOW } from "./derive.js";
9
+ const SPARKLINE_GLYPHS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
10
+ const BAND_GAP_COLUMNS = 1;
11
+ const PANEL_HORIZONTAL_CHROME = 4;
12
+ const READABLE_PANEL_COLUMNS = 20;
13
+ export const PANEL_CHROME_ROWS = 3;
14
+ export const BAND_CONTINUATION_PREFIX = "↳ ";
15
+ const RUN_KEYS = [
16
+ { key: "↑↓", label: "Move" },
17
+ { key: "⏎", label: "Open" },
18
+ { key: "←", label: "Back" },
19
+ { key: "Tab", label: "Panel" },
20
+ { key: "?", label: "Help" },
21
+ { key: "q", label: "Quit" },
22
+ { key: "f", label: "Follow" },
23
+ { key: "/", label: "Filter" },
24
+ ];
25
+ const SETUP_ADDITIONAL_KEYS = [
26
+ { key: "␣", label: "Toggle" },
27
+ { key: "a", label: "All" },
28
+ { key: "r", label: "Re-probe" },
29
+ { key: "s", label: "Save" },
30
+ { key: "n", label: "Next" },
31
+ { key: "p", label: "Prev" },
32
+ ];
33
+ const SETUP_KEYS = [
34
+ RUN_KEYS[0],
35
+ { key: RUN_KEYS[1].key, label: "Next" },
36
+ RUN_KEYS[2],
37
+ RUN_KEYS[3],
38
+ RUN_KEYS[4],
39
+ RUN_KEYS[5],
40
+ ...SETUP_ADDITIONAL_KEYS,
41
+ ];
42
+ export const KEYBAR_KEYS = {
43
+ run: RUN_KEYS,
44
+ setup: SETUP_KEYS,
45
+ setupAdditional: SETUP_ADDITIONAL_KEYS,
46
+ };
47
+ export function BodyText({ children, emphasis = "normal", }) {
48
+ const token = TEXT_EMPHASIS_TOKENS[emphasis];
49
+ return (_jsx(Text, { bold: token.weight === "bold", dimColor: token.dimmed, children: children }));
50
+ }
51
+ function longestBandLine(column) {
52
+ return Math.max(1, ...[column.title, ...column.lines].map((line) => stringWidth(line)));
53
+ }
54
+ function apportion(available, weights) {
55
+ if (weights.length === 0)
56
+ return [];
57
+ if (available <= 0)
58
+ return weights.map(() => 0);
59
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
60
+ if (totalWeight <= 0) {
61
+ return weights.map((_, index) => Math.floor(available / weights.length)
62
+ + (index < available % weights.length ? 1 : 0));
63
+ }
64
+ const exact = weights.map((weight) => (available * weight) / totalWeight);
65
+ const granted = exact.map(Math.floor);
66
+ let remainder = available - granted.reduce((sum, value) => sum + value, 0);
67
+ const order = exact.map((value, index) => ({
68
+ index,
69
+ fraction: value - granted[index],
70
+ weight: weights[index],
71
+ })).sort((left, right) => right.fraction - left.fraction
72
+ || right.weight - left.weight
73
+ || left.index - right.index);
74
+ for (const item of order) {
75
+ if (remainder <= 0)
76
+ break;
77
+ granted[item.index] += 1;
78
+ remainder -= 1;
79
+ }
80
+ return granted;
81
+ }
82
+ /**
83
+ * Give each panel the columns its own longest line calls for. Surplus is shared
84
+ * evenly after every line fits; scarcity is apportioned by unmet content need.
85
+ */
86
+ export function allocateBandColumns(columns, content, options = {}) {
87
+ if (content.length === 0)
88
+ return [];
89
+ const chromeColumns = Math.max(0, Math.floor(options.chromeColumns ?? PANEL_HORIZONTAL_CHROME));
90
+ const minimumColumns = chromeColumns + 1;
91
+ const minimumReadableColumns = Math.max(minimumColumns, Math.floor(options.minimumReadableColumns ?? READABLE_PANEL_COLUMNS));
92
+ const total = Math.max(0, Math.floor(columns));
93
+ const usable = Math.max(0, total - BAND_GAP_COLUMNS * (content.length - 1));
94
+ if (usable < minimumColumns * content.length) {
95
+ return apportion(usable, content.map((column) => longestBandLine(column)));
96
+ }
97
+ const preferred = content.map((column) => longestBandLine(column) + chromeColumns);
98
+ const preferredTotal = preferred.reduce((sum, value) => sum + value, 0);
99
+ if (preferredTotal <= usable) {
100
+ const surplus = apportion(usable - preferredTotal, content.map(() => 1));
101
+ return preferred.map((value, index) => value + surplus[index]);
102
+ }
103
+ const readable = preferred.map((value) => Math.min(value, minimumReadableColumns));
104
+ const readableTotal = readable.reduce((sum, value) => sum + value, 0);
105
+ const base = readableTotal <= usable
106
+ ? readable
107
+ : content.map(() => minimumColumns);
108
+ const shares = apportion(usable - base.reduce((sum, value) => sum + value, 0), preferred.map((value, index) => value - base[index]));
109
+ return base.map((value, index) => value + shares[index]);
110
+ }
111
+ function takeDisplayColumns(value, columns) {
112
+ const characters = [...value];
113
+ let used = 0;
114
+ let taken = 0;
115
+ while (taken < characters.length) {
116
+ const width = stringWidth(characters[taken]);
117
+ if (taken > 0 && used + width > columns)
118
+ break;
119
+ if (taken === 0 && width > columns) {
120
+ taken = 1;
121
+ break;
122
+ }
123
+ used += width;
124
+ taken += 1;
125
+ }
126
+ const concatenate = (items) => items.reduce((result, character) => result + character, "");
127
+ return [
128
+ concatenate(characters.slice(0, taken)),
129
+ concatenate(characters.slice(taken)),
130
+ ];
131
+ }
132
+ /**
133
+ * Compose one data line into visible rows. A fitting line is byte-for-byte
134
+ * untouched. Every wrapped row carries a continuation marker before any
135
+ * separator or data token, so it cannot masquerade as a new item.
136
+ */
137
+ export function composeBandLine(text, columns) {
138
+ const width = Math.max(1, Math.floor(columns));
139
+ if (stringWidth(text) <= width)
140
+ return [text];
141
+ const words = text.split(" ");
142
+ const rendered = [];
143
+ let current = "";
144
+ let continuation = false;
145
+ const emptyLine = () => continuation
146
+ ? BAND_CONTINUATION_PREFIX.slice(0, width)
147
+ : "";
148
+ for (const sourceWord of words) {
149
+ let word = sourceWord;
150
+ for (;;) {
151
+ if (current.length === 0)
152
+ current = emptyLine();
153
+ const hasData = current !== emptyLine();
154
+ const separator = hasData ? " " : "";
155
+ const candidate = `${current}${separator}${word}`;
156
+ if (stringWidth(candidate) <= width) {
157
+ current = candidate;
158
+ break;
159
+ }
160
+ if (hasData) {
161
+ rendered.push(current);
162
+ continuation = true;
163
+ current = "";
164
+ continue;
165
+ }
166
+ const available = Math.max(1, width - stringWidth(current));
167
+ const [head, tail] = takeDisplayColumns(word, available);
168
+ current += head;
169
+ word = tail;
170
+ if (word.length === 0)
171
+ break;
172
+ rendered.push(current);
173
+ continuation = true;
174
+ current = "";
175
+ }
176
+ }
177
+ if (current.length > 0)
178
+ rendered.push(current);
179
+ return rendered.length > 0 ? rendered : [text];
180
+ }
181
+ export function BandLines({ lines, columns, emphasis, }) {
182
+ return (_jsx(Box, { flexDirection: "column", children: lines.flatMap((line, lineIndex) => composeBandLine(line, columns).map((visible, visibleIndex) => (_jsx(BodyText, { emphasis: emphasis, children: visible }, `${lineIndex}:${visibleIndex}:${visible}`)))) }));
183
+ }
184
+ /** Rows drawn by a band of bordered panels at the same allocated widths. */
185
+ export function measureBandRows(columns, content) {
186
+ const widths = allocateBandColumns(columns, content);
187
+ return Math.max(0, ...content.map((column, index) => PANEL_CHROME_ROWS
188
+ + column.lines.reduce((rows, line) => rows + composeBandLine(line, Math.max(1, widths[index] - PANEL_HORIZONTAL_CHROME)).length, 0)));
189
+ }
190
+ function statePresentation(state) {
191
+ switch (state) {
192
+ case "active":
193
+ return {
194
+ glyph: GLYPHS.toggleActive,
195
+ color: `ansi256(${COCKPIT_DATA_RAMP[1].xterm})`,
196
+ };
197
+ case "inactive":
198
+ return { glyph: GLYPHS.toggleInactive, dimmed: true };
199
+ case "pass":
200
+ return {
201
+ glyph: GLYPHS.pass,
202
+ color: `ansi256(${COCKPIT_DATA_RAMP[1].xterm})`,
203
+ };
204
+ case "fail":
205
+ return {
206
+ glyph: GLYPHS.fail,
207
+ color: `ansi256(${COCKPIT_INKS.failure.xterm})`,
208
+ };
209
+ case "warn":
210
+ return {
211
+ glyph: GLYPHS.attention,
212
+ color: `ansi256(${COCKPIT_INKS.warning.xterm})`,
213
+ };
214
+ case "neutral":
215
+ return { glyph: GLYPHS.neutral, dimmed: true };
216
+ }
217
+ }
218
+ export function StateGlyph({ state }) {
219
+ const presentation = statePresentation(state);
220
+ return (_jsx(Text, { color: presentation.color, dimColor: presentation.dimmed, children: presentation.glyph }));
221
+ }
222
+ export function Panel({ title, focused = false, width, flexGrow, children, }) {
223
+ const content = Children.map(children, (child) => typeof child === "string" || typeof child === "number"
224
+ ? _jsx(BodyText, { children: child })
225
+ : child);
226
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: flexGrow, width: width, paddingX: 1, borderStyle: focused ? "double" : "round", borderColor: `ansi256(${focused ? COCKPIT_DATA_RAMP[0].xterm : COCKPIT_DATA_RAMP[3].xterm})`, borderDimColor: !focused, children: [_jsx(Box, { children: focused ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: GLYPHS.pointer }), _jsxs(BodyText, { emphasis: "strong", children: [" ", title] })] })) : (_jsx(BodyText, { emphasis: "strong", children: title })) }), _jsx(Box, { flexDirection: "column", children: content })] }));
227
+ }
228
+ export function CockpitGrid({ children, columns, columnContents, allocation, }) {
229
+ const items = Children.toArray(children);
230
+ const widths = columns !== undefined
231
+ && columnContents?.length === items.length
232
+ ? allocateBandColumns(columns, columnContents, allocation)
233
+ : undefined;
234
+ return (_jsx(Box, { flexDirection: "row", flexWrap: "nowrap", gap: BAND_GAP_COLUMNS, width: columns, children: items.map((child, index) => (_jsx(Box, { flexDirection: widths ? "column" : undefined, flexGrow: widths ? 0 : 1, flexShrink: widths ? 0 : 1, flexBasis: widths ? undefined : 0, width: widths?.[index], children: child }, index))) }));
235
+ }
236
+ function sparklineLevel(value, minimum, maximum) {
237
+ if (maximum === minimum)
238
+ return 0;
239
+ return Math.round(((value - minimum) / (maximum - minimum)) * (SPARKLINE_GLYPHS.length - 1));
240
+ }
241
+ export function Sparkline({ samples, }) {
242
+ const buckets = samples
243
+ .slice(-SPARKLINE_BUCKET_WINDOW)
244
+ .map((sample) => typeof sample === "number" && Number.isFinite(sample) ? sample : null);
245
+ const populated = buckets.filter((sample) => sample !== null);
246
+ const minimum = populated.length > 0 ? Math.min(...populated) : 0;
247
+ const maximum = populated.length > 0 ? Math.max(...populated) : 0;
248
+ return (_jsx(Box, { flexDirection: "row", height: 1, "aria-label": "metric sparkline", children: buckets.map((sample, index) => {
249
+ if (sample === null)
250
+ return _jsx(Text, { children: " " }, `gap:${index}`);
251
+ const level = sparklineLevel(sample, minimum, maximum);
252
+ const rampIndex = COCKPIT_DATA_RAMP.length - 1 - Math.round((level / (SPARKLINE_GLYPHS.length - 1)) * (COCKPIT_DATA_RAMP.length - 1));
253
+ const ink = COCKPIT_DATA_RAMP[rampIndex] ?? COCKPIT_DATA_RAMP[0];
254
+ return (_jsx(Text, { color: `ansi256(${ink.xterm})`, children: SPARKLINE_GLYPHS[level] }, `${index}:${sample}`));
255
+ }) }));
256
+ }
257
+ export function StatTile({ label, value, samples, focused, width, }) {
258
+ return (_jsxs(Panel, { title: label, focused: focused, width: width, flexGrow: 1, children: [_jsx(BodyText, { emphasis: "strong", children: value }), _jsx(Sparkline, { samples: samples })] }));
259
+ }
260
+ export function ProgressMeter({ value, width = 24, }) {
261
+ const safeWidth = Math.max(1, Math.floor(width));
262
+ const percentage = Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0));
263
+ const filled = Math.round((percentage / 100) * safeWidth);
264
+ const valueLabel = Number.isInteger(percentage)
265
+ ? percentage.toFixed(0)
266
+ : percentage.toFixed(1);
267
+ return (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", "aria-role": "progressbar", "aria-label": `${valueLabel}% complete`, children: [_jsx(Text, { color: `ansi256(${COCKPIT_DATA_RAMP[0].xterm})`, children: "█".repeat(filled) }), _jsx(Text, { dimColor: true, children: "░".repeat(safeWidth - filled) }), _jsxs(BodyText, { emphasis: "strong", children: [" ", valueLabel, "%"] })] }));
268
+ }
269
+ function OneLineItems({ children, width, label, }) {
270
+ return (_jsx(Box, { flexDirection: "row", flexWrap: "nowrap", width: width, height: 1, overflow: "hidden", "aria-label": label, children: children }));
271
+ }
272
+ export function Keybar({ surface, width, }) {
273
+ return (_jsx(OneLineItems, { width: width, label: `${surface} cockpit keys`, children: KEYBAR_KEYS[surface].map((item, index) => (_jsxs(Box, { flexShrink: 0, children: [index > 0 && _jsx(BodyText, { emphasis: "dim", children: " \u00B7 " }), _jsx(BodyText, { emphasis: "strong", children: item.key }), _jsxs(BodyText, { emphasis: "dim", children: [" ", item.label] })] }, item.key))) }));
274
+ }
275
+ export function StatusStrip({ items, width, }) {
276
+ return (_jsx(OneLineItems, { width: width, label: "cockpit status", children: items.map((item, index) => (_jsxs(Box, { flexShrink: 0, children: [index > 0 && _jsx(BodyText, { emphasis: "dim", children: " \u00B7 " }), _jsx(StateGlyph, { state: item.state }), _jsxs(BodyText, { children: [" ", item.text] })] }, `${item.state}:${item.text}`))) }));
277
+ }
278
+ export function JournalRowPanel({ rows, width, focused, title = "JOURNAL", }) {
279
+ return (_jsx(Panel, { title: title, focused: focused, width: width, children: rows.map((row, index) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "dim", children: row.time }), _jsx(BodyText, { children: " " }), _jsx(StateGlyph, { state: row.state }), _jsxs(BodyText, { children: [" ", row.text] })] }, row.id ?? `${row.time}:${row.state}:${index}`))) }));
280
+ }
@@ -0,0 +1,41 @@
1
+ export declare const DEMO_KEYS: {
2
+ readonly surface: "v";
3
+ readonly capture: "c";
4
+ readonly quit: "q";
5
+ };
6
+ export type CaptureEvent = {
7
+ readonly ts: string;
8
+ readonly event: string;
9
+ readonly taskId?: string;
10
+ readonly data: Record<string, unknown>;
11
+ };
12
+ export type DemoFileCapture = {
13
+ readonly fileName: string;
14
+ readonly raw: string;
15
+ };
16
+ export type DemoJournalCapture = DemoFileCapture & {
17
+ readonly events: readonly CaptureEvent[];
18
+ };
19
+ export type DemoJsonCapture = DemoFileCapture & {
20
+ readonly data: Record<string, unknown>;
21
+ };
22
+ export type DemoCaptures = {
23
+ readonly files: readonly DemoFileCapture[];
24
+ readonly journals: readonly DemoJournalCapture[];
25
+ readonly doctor: DemoJsonCapture;
26
+ readonly config: {
27
+ readonly global: DemoFileCapture;
28
+ readonly repo: DemoFileCapture;
29
+ };
30
+ };
31
+ /**
32
+ * The only capture boundary for both cockpit demos. It returns the event
33
+ * captures, detection cache and both configuration layers in one immutable set.
34
+ */
35
+ export declare function loadDemoCaptures(directory?: string): DemoCaptures;
36
+ export declare function runCockpitDemo({ input, output, binaryVersion, debug, }: {
37
+ input: NodeJS.ReadStream;
38
+ output: NodeJS.WriteStream;
39
+ binaryVersion: string;
40
+ debug?: boolean;
41
+ }): Promise<void>;
@@ -0,0 +1,144 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { render, useApp, useInput } from "ink";
5
+ import { createElement, useMemo, useState } from "react";
6
+ import { resolveCockpitLayout } from "./layout.js";
7
+ import { deriveRunCockpitData, RunCockpitFrame, } from "./run-cockpit.js";
8
+ import { deriveSetupCockpitData, SetupCockpitFrame, } from "./setup-cockpit.js";
9
+ const CAPTURE_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), "../../../tests/fixtures/cockpit/sources");
10
+ const CAPTURE_FILE_NAMES = [
11
+ "run-20260724-194619.journal.jsonl",
12
+ "run-20260724-231138.journal.jsonl",
13
+ "run-20260725-025004.interrupted.journal.jsonl",
14
+ "doctor.json",
15
+ "config.global.yaml",
16
+ "config.repo.yaml",
17
+ ];
18
+ export const DEMO_KEYS = {
19
+ surface: "v",
20
+ capture: "c",
21
+ quit: "q",
22
+ };
23
+ function parseEvents(raw) {
24
+ return raw.split("\n").flatMap((line) => {
25
+ if (line.trim().length === 0)
26
+ return [];
27
+ return [JSON.parse(line)];
28
+ });
29
+ }
30
+ /**
31
+ * The only capture boundary for both cockpit demos. It returns the event
32
+ * captures, detection cache and both configuration layers in one immutable set.
33
+ */
34
+ export function loadDemoCaptures(directory = CAPTURE_DIRECTORY) {
35
+ const files = CAPTURE_FILE_NAMES.map((fileName) => ({
36
+ fileName,
37
+ raw: readFileSync(join(directory, fileName), "utf8"),
38
+ }));
39
+ const byName = new Map(files.map((capture) => [capture.fileName, capture]));
40
+ const required = (fileName) => {
41
+ const capture = byName.get(fileName);
42
+ if (!capture)
43
+ throw new Error(`missing cockpit capture: ${fileName}`);
44
+ return capture;
45
+ };
46
+ const journal = (fileName) => {
47
+ const capture = required(fileName);
48
+ return { ...capture, events: parseEvents(capture.raw) };
49
+ };
50
+ const doctor = required("doctor.json");
51
+ return {
52
+ files,
53
+ journals: [
54
+ journal("run-20260724-194619.journal.jsonl"),
55
+ journal("run-20260724-231138.journal.jsonl"),
56
+ journal("run-20260725-025004.interrupted.journal.jsonl"),
57
+ ],
58
+ doctor: {
59
+ ...doctor,
60
+ data: JSON.parse(doctor.raw),
61
+ },
62
+ config: {
63
+ global: required("config.global.yaml"),
64
+ repo: required("config.repo.yaml"),
65
+ },
66
+ };
67
+ }
68
+ function DemoApp({ binaryVersion, columns, rows, }) {
69
+ const { exit } = useApp();
70
+ const captures = useMemo(() => loadDemoCaptures(), []);
71
+ const [surface, setSurface] = useState("run");
72
+ const [captureIndex, setCaptureIndex] = useState(() => {
73
+ const eventful = captures.journals.findIndex((capture) => capture.fileName === "run-20260724-231138.journal.jsonl");
74
+ if (eventful < 0)
75
+ throw new Error("eventful cockpit capture is missing");
76
+ return eventful;
77
+ });
78
+ useInput((input, key) => {
79
+ if (input === DEMO_KEYS.quit || (key.ctrl && input === "c")) {
80
+ exit();
81
+ return;
82
+ }
83
+ if (input === DEMO_KEYS.surface) {
84
+ setSurface((current) => current === "run" ? "setup" : "run");
85
+ return;
86
+ }
87
+ if (input === DEMO_KEYS.capture) {
88
+ setCaptureIndex((current) => (current + 1) % captures.journals.length);
89
+ }
90
+ });
91
+ let frame;
92
+ if (surface === "setup") {
93
+ const data = deriveSetupCockpitData(captures, binaryVersion);
94
+ frame = createElement(SetupCockpitFrame, {
95
+ data,
96
+ columns,
97
+ rows,
98
+ });
99
+ }
100
+ else {
101
+ const data = deriveRunCockpitData(captures.journals[captureIndex], binaryVersion, { isDaemonAlive: () => false });
102
+ frame = createElement(RunCockpitFrame, {
103
+ data,
104
+ columns,
105
+ rows,
106
+ });
107
+ }
108
+ return frame;
109
+ }
110
+ export async function runCockpitDemo({ input, output, binaryVersion, debug = false, }) {
111
+ const columns = output.columns ?? 80;
112
+ const rows = output.rows ?? 24;
113
+ const layout = resolveCockpitLayout(columns, rows);
114
+ if (layout.renderer === "plain") {
115
+ const { captureCockpitOutput } = await import("./capture.js");
116
+ await captureCockpitOutput({
117
+ cockpit: "run",
118
+ output,
119
+ binaryVersion,
120
+ columns,
121
+ rows,
122
+ interactive: output.isTTY === true,
123
+ ci: process.env.CI === "true",
124
+ });
125
+ return;
126
+ }
127
+ const app = render(createElement(DemoApp, {
128
+ binaryVersion,
129
+ columns,
130
+ rows,
131
+ }), {
132
+ stdin: input,
133
+ stdout: output,
134
+ exitOnCtrlC: false,
135
+ patchConsole: false,
136
+ debug,
137
+ });
138
+ try {
139
+ await app.waitUntilExit();
140
+ }
141
+ finally {
142
+ app.unmount();
143
+ }
144
+ }
@@ -0,0 +1,39 @@
1
+ import type { JournalRow, StatusStripItem } from "./components.js";
2
+ export declare const SPARKLINE_BUCKET_WINDOW = 12;
3
+ export declare function selectSparklineBucketWidthMs(elapsedMs: number): number;
4
+ type MetricSample = number | null;
5
+ export type RunStatus = "done" | "failed" | "interrupted" | "running";
6
+ export type RunCockpitData = {
7
+ readonly binaryVersion: string;
8
+ readonly runId: string;
9
+ readonly branch: string;
10
+ readonly status: RunStatus;
11
+ readonly elapsed: string;
12
+ readonly tasks: {
13
+ readonly done: number;
14
+ readonly total: number;
15
+ readonly samples: readonly MetricSample[];
16
+ };
17
+ readonly gates: {
18
+ readonly passed: number;
19
+ readonly total: number;
20
+ readonly samples: readonly MetricSample[];
21
+ };
22
+ readonly passRate: {
23
+ readonly value: number;
24
+ readonly samples: readonly MetricSample[];
25
+ };
26
+ readonly progress: number;
27
+ readonly progressCaption: string;
28
+ readonly journalRows: readonly JournalRow[];
29
+ readonly statusItems: readonly StatusStripItem[];
30
+ };
31
+ export type RunCockpitSource = {
32
+ readonly fileName: string;
33
+ readonly raw: string;
34
+ };
35
+ export type RunCockpitDeriveOptions = {
36
+ readonly isDaemonAlive?: (pid: number) => boolean;
37
+ };
38
+ export declare function deriveRunCockpitData(source: RunCockpitSource, binaryVersion: string, options?: RunCockpitDeriveOptions): RunCockpitData;
39
+ export {};