tickmarkr 1.78.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.
- package/dist/adapters/claude-code.d.ts +11 -0
- package/dist/adapters/claude-code.js +105 -16
- package/dist/adapters/kimi.d.ts +6 -0
- package/dist/adapters/kimi.js +88 -26
- package/dist/adapters/registry.js +6 -1
- package/dist/brand.d.ts +4 -0
- package/dist/brand.js +4 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +23 -0
- package/dist/cli/commands/status.d.ts +4 -0
- package/dist/cli/commands/status.js +237 -18
- package/dist/cli/commands/ui.d.ts +1 -1
- package/dist/cli/commands/ui.js +11 -1
- package/dist/drivers/herdr.js +36 -7
- package/dist/drivers/types.d.ts +22 -1
- package/dist/gates/acceptance.d.ts +5 -0
- package/dist/gates/acceptance.js +5 -3
- package/dist/gates/llm.d.ts +9 -0
- package/dist/gates/llm.js +74 -0
- package/dist/gates/review.d.ts +5 -0
- package/dist/gates/review.js +5 -3
- package/dist/run/journal.d.ts +1 -1
- package/dist/tui/cockpit/capture.d.ts +117 -0
- package/dist/tui/cockpit/capture.js +329 -0
- package/dist/tui/cockpit/components.d.ts +179 -0
- package/dist/tui/cockpit/components.js +280 -0
- package/dist/tui/cockpit/demo.d.ts +41 -0
- package/dist/tui/cockpit/demo.js +144 -0
- package/dist/tui/cockpit/derive.d.ts +39 -0
- package/dist/tui/cockpit/derive.js +390 -0
- package/dist/tui/cockpit/layout.d.ts +76 -0
- package/dist/tui/cockpit/layout.js +84 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +19 -0
- package/dist/tui/cockpit/run-cockpit.js +194 -0
- package/dist/tui/cockpit/setup-cockpit.d.ts +41 -0
- package/dist/tui/cockpit/setup-cockpit.js +488 -0
- package/dist/tui/cockpit/theme.d.ts +161 -0
- package/dist/tui/cockpit/theme.js +170 -0
- package/package.json +2 -1
|
@@ -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 {};
|