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
package/dist/gates/review.js
CHANGED
|
@@ -4,7 +4,7 @@ import { renderAcceptanceItem } from "../graph/schema.js";
|
|
|
4
4
|
import { getAdapter } from "../adapters/registry.js";
|
|
5
5
|
import { shOk } from "../run/git.js";
|
|
6
6
|
import { marginalCostRank } from "../route/router.js";
|
|
7
|
-
import { COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, verdictNonceLine } from "./llm.js";
|
|
7
|
+
import { appendAnchoredReview, COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, verdictNonceLine } from "./llm.js";
|
|
8
8
|
// legacy flat `issues` shape — every issue blocks; the approve flag must agree with the list.
|
|
9
9
|
function classifyReviewIssues(approve, issues) {
|
|
10
10
|
const inconsistencies = [];
|
|
@@ -158,8 +158,9 @@ block approval. For a minor concern you have decided not to block on, set "defer
|
|
|
158
158
|
one-line "rationale" — it is recorded in the review, never dropped.
|
|
159
159
|
|
|
160
160
|
Respond with ONLY this JSON:
|
|
161
|
-
{"nonce": "${nonce}", "approve": true|false, "findings": [{"note": "...", "severity": "material"|"minor", "defer": false, "rationale": ""}]}
|
|
161
|
+
{"nonce": "${nonce}", "approve": true|false, "findings": [{"note": "...", "severity": "material"|"minor", "defer": false, "rationale": ""}], "comments": [{"path": "path/to/file", "line": 42, "body": "actionable feedback"}]}
|
|
162
162
|
Approve iff no material finding remains; an empty findings list is a clean approval.
|
|
163
|
+
The top-level comments array is optional. Use it only for actionable line-anchored feedback.
|
|
163
164
|
`;
|
|
164
165
|
const raw = await runLlm(getAdapter(reviewer.adapter, adapters), reviewer.model, prompt, worktree, via ? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("review", reviewer.adapter), label: via.labelFor("review") } : undefined,
|
|
165
166
|
// frontier reviewers routinely need >5min on a configured-cap-sized diff, and `claude -p` buffers all
|
|
@@ -182,10 +183,11 @@ Approve iff no material finding remains; an empty findings list is a clean appro
|
|
|
182
183
|
const decided = findings !== null
|
|
183
184
|
? classifyReviewFindings(findings)
|
|
184
185
|
: classifyReviewIssues(v.approve, v.issues);
|
|
186
|
+
const prose = `reviewer ${reviewer.adapter}:${reviewer.model} (${reviewer.vendor}): ${decided.headline}${decided.lines.length ? "\n" + decided.lines.join("\n") : ""}`;
|
|
185
187
|
return {
|
|
186
188
|
gate: "review",
|
|
187
189
|
pass: decided.pass,
|
|
188
|
-
details:
|
|
190
|
+
details: appendAnchoredReview(prose, v),
|
|
189
191
|
meta: { reviewer: channelKey(reviewer) },
|
|
190
192
|
};
|
|
191
193
|
}
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -85,12 +85,12 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
85
85
|
}>>;
|
|
86
86
|
signalQuality: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<0.25>, z.ZodLiteral<0.5>, z.ZodLiteral<0.75>, z.ZodLiteral<1>]>>;
|
|
87
87
|
signalBasis: z.ZodOptional<z.ZodEnum<{
|
|
88
|
+
skipped: "skipped";
|
|
88
89
|
proved: "proved";
|
|
89
90
|
"review-agree": "review-agree";
|
|
90
91
|
"judge-only": "judge-only";
|
|
91
92
|
legacy: "legacy";
|
|
92
93
|
vacuous: "vacuous";
|
|
93
|
-
skipped: "skipped";
|
|
94
94
|
}>>;
|
|
95
95
|
kind: z.ZodOptional<z.ZodLiteral<"judge">>;
|
|
96
96
|
judgeOutcome: z.ZodOptional<z.ZodEnum<{
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type DemoCaptures } from "./demo.js";
|
|
3
|
+
export type CockpitName = "run" | "setup";
|
|
4
|
+
export type CockpitRenderer = "frame" | "plain";
|
|
5
|
+
export type RendererCaptureOptions = {
|
|
6
|
+
readonly columns: number;
|
|
7
|
+
readonly rows: number;
|
|
8
|
+
readonly colour: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type FrameRenderer = (node: ReactNode, options: RendererCaptureOptions) => Promise<string>;
|
|
11
|
+
export type CockpitEmission = {
|
|
12
|
+
readonly renderer: CockpitRenderer;
|
|
13
|
+
readonly output: string;
|
|
14
|
+
};
|
|
15
|
+
export type CaptureCockpitOptions = {
|
|
16
|
+
readonly cockpit: CockpitName;
|
|
17
|
+
readonly output: NodeJS.WriteStream;
|
|
18
|
+
readonly binaryVersion: string;
|
|
19
|
+
readonly columns?: number;
|
|
20
|
+
readonly rows?: number;
|
|
21
|
+
readonly interactive?: boolean;
|
|
22
|
+
readonly colour?: boolean;
|
|
23
|
+
readonly ci?: boolean;
|
|
24
|
+
readonly captures?: DemoCaptures;
|
|
25
|
+
readonly plainRenderer?: (cockpit: CockpitName, binaryVersion: string, captures: DemoCaptures) => string;
|
|
26
|
+
readonly frameRenderer?: FrameRenderer;
|
|
27
|
+
};
|
|
28
|
+
export type WidthBandCase = {
|
|
29
|
+
readonly arrangement: "stacked" | "folded-keys" | "three-column";
|
|
30
|
+
readonly columns: number;
|
|
31
|
+
};
|
|
32
|
+
export declare const WIDTH_BAND_CASES: readonly [{
|
|
33
|
+
readonly arrangement: "stacked";
|
|
34
|
+
readonly columns: 80;
|
|
35
|
+
}, {
|
|
36
|
+
readonly arrangement: "folded-keys";
|
|
37
|
+
readonly columns: 100;
|
|
38
|
+
}, {
|
|
39
|
+
readonly arrangement: "three-column";
|
|
40
|
+
readonly columns: 140;
|
|
41
|
+
}];
|
|
42
|
+
export declare const HEIGHT_TIER_BOUNDARIES: readonly [14, 18, 24, 40];
|
|
43
|
+
export type GoldenFrameCase = {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly fixture: string;
|
|
46
|
+
readonly cockpit: CockpitName;
|
|
47
|
+
readonly axis: "width" | "height" | "variant";
|
|
48
|
+
readonly columns: number;
|
|
49
|
+
readonly rows: number;
|
|
50
|
+
readonly interactive: boolean;
|
|
51
|
+
readonly colour: boolean;
|
|
52
|
+
readonly ci: boolean;
|
|
53
|
+
};
|
|
54
|
+
export declare const GOLDEN_FRAME_CASES: readonly GoldenFrameCase[];
|
|
55
|
+
export type ColourFrameCase = {
|
|
56
|
+
readonly id: "healthy-colour" | "healthy-no-colour" | "warning-failure-colour";
|
|
57
|
+
readonly fixture: string;
|
|
58
|
+
readonly sourceFileName: string;
|
|
59
|
+
readonly columns: number;
|
|
60
|
+
readonly rows: number;
|
|
61
|
+
readonly colour: boolean;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The colour corpus has a deliberately separate manifest from the layout
|
|
65
|
+
* corpus. Layout cases stay colourless; these cases alone exercise real ink.
|
|
66
|
+
*/
|
|
67
|
+
export declare const COLOUR_FRAME_CASES: readonly [{
|
|
68
|
+
readonly id: "healthy-colour";
|
|
69
|
+
readonly fixture: "run-20260718-000943.colour.140x24.txt";
|
|
70
|
+
readonly sourceFileName: "run-20260718-000943.journal.jsonl";
|
|
71
|
+
readonly columns: 140;
|
|
72
|
+
readonly rows: 24;
|
|
73
|
+
readonly colour: true;
|
|
74
|
+
}, {
|
|
75
|
+
readonly id: "healthy-no-colour";
|
|
76
|
+
readonly fixture: "run-20260718-000943.no-colour.140x24.txt";
|
|
77
|
+
readonly sourceFileName: "run-20260718-000943.journal.jsonl";
|
|
78
|
+
readonly columns: 140;
|
|
79
|
+
readonly rows: 24;
|
|
80
|
+
readonly colour: false;
|
|
81
|
+
}, {
|
|
82
|
+
readonly id: "warning-failure-colour";
|
|
83
|
+
readonly fixture: "run-20260725-025004.interrupted.colour.140x24.txt";
|
|
84
|
+
readonly sourceFileName: "run-20260725-025004.interrupted.journal.jsonl";
|
|
85
|
+
readonly columns: 140;
|
|
86
|
+
readonly rows: 24;
|
|
87
|
+
readonly colour: true;
|
|
88
|
+
}];
|
|
89
|
+
/**
|
|
90
|
+
* Capture exactly the bytes Ink writes for one static paint. Debug mode is
|
|
91
|
+
* deliberate: it exposes the complete frame rather than terminal patch
|
|
92
|
+
* instructions, so the committed corpus is the renderer's frame output.
|
|
93
|
+
*/
|
|
94
|
+
export declare function captureRendererOutput(node: ReactNode, options: RendererCaptureOptions): Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* The single interactive/plain boundary. It decides before mounting Ink, then
|
|
97
|
+
* falls back again if a renderer returns an empty paint.
|
|
98
|
+
*/
|
|
99
|
+
export declare function captureCockpitOutput(options: CaptureCockpitOptions): Promise<CockpitEmission>;
|
|
100
|
+
export type RegeneratedGoldenFrame = GoldenFrameCase & {
|
|
101
|
+
readonly renderer: CockpitRenderer;
|
|
102
|
+
readonly output: string;
|
|
103
|
+
readonly emitted: string;
|
|
104
|
+
};
|
|
105
|
+
export declare function regenerateGoldenFrames(binaryVersion?: string): Promise<readonly RegeneratedGoldenFrame[]>;
|
|
106
|
+
export declare function findGoldenFrameMismatches(regenerated: readonly RegeneratedGoldenFrame[], committed: ReadonlyMap<string, string>): string[];
|
|
107
|
+
export type RegeneratedColourFrame = ColourFrameCase & {
|
|
108
|
+
readonly output: string;
|
|
109
|
+
readonly emitted: string;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Regenerate only the colour contract corpus from its own manifest. Historical
|
|
113
|
+
* run sources are read and rendered byte-for-byte with their captured daemon
|
|
114
|
+
* stopped, matching the demo path and avoiding live-process state in committed
|
|
115
|
+
* bytes.
|
|
116
|
+
*/
|
|
117
|
+
export declare function regenerateColourFrames(binaryVersion?: string): Promise<readonly RegeneratedColourFrame[]>;
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { PassThrough } from "node:stream";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { render } from "ink";
|
|
7
|
+
import { cloneElement, createElement, isValidElement, } from "react";
|
|
8
|
+
import { GLYPHS } from "../../brand.js";
|
|
9
|
+
import { version } from "../../cli/commands/version.js";
|
|
10
|
+
import { KEYBAR_KEYS } from "./components.js";
|
|
11
|
+
import { loadDemoCaptures } from "./demo.js";
|
|
12
|
+
import { resolveCockpitLayout, } from "./layout.js";
|
|
13
|
+
import { deriveRunCockpitData, RunCockpitFrame, } from "./run-cockpit.js";
|
|
14
|
+
import { deriveSetupCockpitData, SetupCockpitFrame, } from "./setup-cockpit.js";
|
|
15
|
+
export const WIDTH_BAND_CASES = [
|
|
16
|
+
{ arrangement: "stacked", columns: 80 },
|
|
17
|
+
{ arrangement: "folded-keys", columns: 100 },
|
|
18
|
+
{ arrangement: "three-column", columns: 140 },
|
|
19
|
+
];
|
|
20
|
+
export const HEIGHT_TIER_BOUNDARIES = [14, 18, 24, 40];
|
|
21
|
+
const fixtureName = (cockpit, id, columns, rows) => `${cockpit}.${id}.${columns}x${rows}.txt`;
|
|
22
|
+
const goldenCasesFor = (cockpit) => [
|
|
23
|
+
...WIDTH_BAND_CASES.map(({ arrangement, columns }) => ({
|
|
24
|
+
id: `${cockpit}-width-${arrangement}`,
|
|
25
|
+
fixture: fixtureName(cockpit, `width-${arrangement}`, columns, 24),
|
|
26
|
+
cockpit,
|
|
27
|
+
axis: "width",
|
|
28
|
+
columns,
|
|
29
|
+
rows: 24,
|
|
30
|
+
interactive: true,
|
|
31
|
+
colour: false,
|
|
32
|
+
ci: false,
|
|
33
|
+
})),
|
|
34
|
+
...HEIGHT_TIER_BOUNDARIES.map((rows) => ({
|
|
35
|
+
id: `${cockpit}-height-${rows}`,
|
|
36
|
+
fixture: fixtureName(cockpit, `height-${rows}`, 140, rows),
|
|
37
|
+
cockpit,
|
|
38
|
+
axis: "height",
|
|
39
|
+
columns: 140,
|
|
40
|
+
rows,
|
|
41
|
+
interactive: true,
|
|
42
|
+
colour: false,
|
|
43
|
+
ci: false,
|
|
44
|
+
})),
|
|
45
|
+
{
|
|
46
|
+
id: `${cockpit}-no-colour`,
|
|
47
|
+
fixture: fixtureName(cockpit, "no-colour", 140, 24),
|
|
48
|
+
cockpit,
|
|
49
|
+
axis: "variant",
|
|
50
|
+
columns: 140,
|
|
51
|
+
rows: 24,
|
|
52
|
+
interactive: true,
|
|
53
|
+
colour: false,
|
|
54
|
+
ci: false,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: `${cockpit}-non-tty`,
|
|
58
|
+
fixture: fixtureName(cockpit, "non-tty", 140, 24),
|
|
59
|
+
cockpit,
|
|
60
|
+
axis: "variant",
|
|
61
|
+
columns: 140,
|
|
62
|
+
rows: 24,
|
|
63
|
+
interactive: false,
|
|
64
|
+
colour: false,
|
|
65
|
+
ci: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: `${cockpit}-ci`,
|
|
69
|
+
fixture: fixtureName(cockpit, "ci", 140, 24),
|
|
70
|
+
cockpit,
|
|
71
|
+
axis: "variant",
|
|
72
|
+
columns: 140,
|
|
73
|
+
rows: 24,
|
|
74
|
+
interactive: true,
|
|
75
|
+
colour: false,
|
|
76
|
+
ci: true,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
export const GOLDEN_FRAME_CASES = [
|
|
80
|
+
...goldenCasesFor("run"),
|
|
81
|
+
...goldenCasesFor("setup"),
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* The colour corpus has a deliberately separate manifest from the layout
|
|
85
|
+
* corpus. Layout cases stay colourless; these cases alone exercise real ink.
|
|
86
|
+
*/
|
|
87
|
+
export const COLOUR_FRAME_CASES = [
|
|
88
|
+
{
|
|
89
|
+
id: "healthy-colour",
|
|
90
|
+
fixture: "run-20260718-000943.colour.140x24.txt",
|
|
91
|
+
sourceFileName: "run-20260718-000943.journal.jsonl",
|
|
92
|
+
columns: 140,
|
|
93
|
+
rows: 24,
|
|
94
|
+
colour: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "healthy-no-colour",
|
|
98
|
+
fixture: "run-20260718-000943.no-colour.140x24.txt",
|
|
99
|
+
sourceFileName: "run-20260718-000943.journal.jsonl",
|
|
100
|
+
columns: 140,
|
|
101
|
+
rows: 24,
|
|
102
|
+
colour: false,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "warning-failure-colour",
|
|
106
|
+
fixture: "run-20260725-025004.interrupted.colour.140x24.txt",
|
|
107
|
+
sourceFileName: "run-20260725-025004.interrupted.journal.jsonl",
|
|
108
|
+
columns: 140,
|
|
109
|
+
rows: 24,
|
|
110
|
+
colour: true,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
const COLOUR_SOURCE_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), "../../../tests/fixtures/cockpit/colour/sources");
|
|
114
|
+
function derivedData(cockpit, binaryVersion, captures) {
|
|
115
|
+
if (cockpit === "setup") {
|
|
116
|
+
return {
|
|
117
|
+
cockpit,
|
|
118
|
+
data: deriveSetupCockpitData(captures, binaryVersion),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const source = captures.journals.find((capture) => capture.fileName === "run-20260724-231138.journal.jsonl");
|
|
122
|
+
if (!source)
|
|
123
|
+
throw new Error("healthy cockpit capture is missing");
|
|
124
|
+
return {
|
|
125
|
+
cockpit,
|
|
126
|
+
data: deriveRunCockpitData(source, binaryVersion),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function stateGlyph(state) {
|
|
130
|
+
if (state === "active" || state === "pass")
|
|
131
|
+
return GLYPHS.pass;
|
|
132
|
+
if (state === "inactive")
|
|
133
|
+
return GLYPHS.toggleInactive;
|
|
134
|
+
if (state === "fail")
|
|
135
|
+
return GLYPHS.fail;
|
|
136
|
+
if (state === "warn")
|
|
137
|
+
return GLYPHS.attention;
|
|
138
|
+
return GLYPHS.neutral;
|
|
139
|
+
}
|
|
140
|
+
function defaultPlainRenderer(cockpit, binaryVersion, captures) {
|
|
141
|
+
const derived = derivedData(cockpit, binaryVersion, captures);
|
|
142
|
+
if (derived.cockpit === "run") {
|
|
143
|
+
const data = derived.data;
|
|
144
|
+
return [
|
|
145
|
+
`tickmarkr v${binaryVersion}`,
|
|
146
|
+
`${data.runId} ${data.status} ${data.elapsed}`,
|
|
147
|
+
...data.statusItems.map((item) => `${stateGlyph(item.state)} ${item.text}`),
|
|
148
|
+
`keys ${KEYBAR_KEYS.run.map((item) => `${item.key} ${item.label}`).join(" · ")}`,
|
|
149
|
+
"",
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
152
|
+
const data = derived.data;
|
|
153
|
+
return [
|
|
154
|
+
`tickmarkr v${binaryVersion}`,
|
|
155
|
+
`setup ${data.counts.found} found ${data.counts.authenticated} authed ${data.counts.routable} routable`,
|
|
156
|
+
...data.harnesses.map((harness) => `${stateGlyph(harness.state)} ${harness.stateWord} ${harness.id}`),
|
|
157
|
+
...data.deniedChannels.map((denied) => `${GLYPHS.fail} denied ${denied.channel} - ${denied.reason}`),
|
|
158
|
+
`${GLYPHS.pass} base untouched`,
|
|
159
|
+
`${GLYPHS.attention} ${data.stagedChanges.length} changes unsaved`,
|
|
160
|
+
`keys ${KEYBAR_KEYS.setup.map((item) => `${item.key} ${item.label}`).join(" · ")}`,
|
|
161
|
+
"",
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
|
164
|
+
function frameNode(derived, columns, rows) {
|
|
165
|
+
if (derived.cockpit === "run") {
|
|
166
|
+
return createElement(RunCockpitFrame, {
|
|
167
|
+
data: derived.data,
|
|
168
|
+
columns,
|
|
169
|
+
rows,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return createElement(SetupCockpitFrame, {
|
|
173
|
+
data: derived.data,
|
|
174
|
+
columns,
|
|
175
|
+
rows,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Capture exactly the bytes Ink writes for one static paint. Debug mode is
|
|
180
|
+
* deliberate: it exposes the complete frame rather than terminal patch
|
|
181
|
+
* instructions, so the committed corpus is the renderer's frame output.
|
|
182
|
+
*/
|
|
183
|
+
export async function captureRendererOutput(node, options) {
|
|
184
|
+
const sizedNode = isValidElement(node)
|
|
185
|
+
&& (node.type === RunCockpitFrame || node.type === SetupCockpitFrame)
|
|
186
|
+
? cloneElement(node, { rows: options.rows })
|
|
187
|
+
: node;
|
|
188
|
+
const output = new PassThrough();
|
|
189
|
+
output.isTTY = true;
|
|
190
|
+
output.columns = options.columns;
|
|
191
|
+
output.rows = options.rows;
|
|
192
|
+
const writes = [];
|
|
193
|
+
const write = output.write.bind(output);
|
|
194
|
+
output.write = ((chunk, ...args) => {
|
|
195
|
+
writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
196
|
+
return Reflect.apply(write, output, [chunk, ...args]);
|
|
197
|
+
});
|
|
198
|
+
const previousColourLevel = chalk.level;
|
|
199
|
+
const previousNoColour = process.env.NO_COLOR;
|
|
200
|
+
chalk.level = options.colour ? 3 : 0;
|
|
201
|
+
if (options.colour)
|
|
202
|
+
delete process.env.NO_COLOR;
|
|
203
|
+
else
|
|
204
|
+
process.env.NO_COLOR = "1";
|
|
205
|
+
let painted;
|
|
206
|
+
const firstPaint = new Promise((resolve) => {
|
|
207
|
+
painted = resolve;
|
|
208
|
+
});
|
|
209
|
+
const app = render(sizedNode, {
|
|
210
|
+
stdout: output,
|
|
211
|
+
debug: true,
|
|
212
|
+
patchConsole: false,
|
|
213
|
+
onRender: painted,
|
|
214
|
+
});
|
|
215
|
+
try {
|
|
216
|
+
await firstPaint;
|
|
217
|
+
return writes.at(-1) ?? "";
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
app.unmount();
|
|
221
|
+
app.cleanup();
|
|
222
|
+
chalk.level = previousColourLevel;
|
|
223
|
+
if (previousNoColour === undefined)
|
|
224
|
+
delete process.env.NO_COLOR;
|
|
225
|
+
else
|
|
226
|
+
process.env.NO_COLOR = previousNoColour;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The single interactive/plain boundary. It decides before mounting Ink, then
|
|
231
|
+
* falls back again if a renderer returns an empty paint.
|
|
232
|
+
*/
|
|
233
|
+
export async function captureCockpitOutput(options) {
|
|
234
|
+
const columns = options.columns ?? options.output.columns ?? 80;
|
|
235
|
+
const rows = options.rows ?? options.output.rows ?? 24;
|
|
236
|
+
const interactive = options.interactive
|
|
237
|
+
?? options.output.isTTY === true;
|
|
238
|
+
const colour = options.colour
|
|
239
|
+
?? process.env.NO_COLOR === undefined;
|
|
240
|
+
const ci = options.ci ?? process.env.CI === "true";
|
|
241
|
+
const captures = options.captures ?? loadDemoCaptures();
|
|
242
|
+
const plain = () => (options.plainRenderer ?? defaultPlainRenderer)(options.cockpit, options.binaryVersion, captures);
|
|
243
|
+
const layout = resolveCockpitLayout(columns, rows);
|
|
244
|
+
if (!interactive || options.output.isTTY !== true || ci || layout.renderer === "plain") {
|
|
245
|
+
const plainOutput = plain();
|
|
246
|
+
options.output.write(plainOutput);
|
|
247
|
+
return { renderer: "plain", output: plainOutput };
|
|
248
|
+
}
|
|
249
|
+
const derived = derivedData(options.cockpit, options.binaryVersion, captures);
|
|
250
|
+
const frameOutput = await (options.frameRenderer ?? captureRendererOutput)(frameNode(derived, columns, rows), { columns, rows, colour });
|
|
251
|
+
if (frameOutput.length === 0) {
|
|
252
|
+
const plainOutput = plain();
|
|
253
|
+
options.output.write(plainOutput);
|
|
254
|
+
return { renderer: "plain", output: plainOutput };
|
|
255
|
+
}
|
|
256
|
+
options.output.write(frameOutput);
|
|
257
|
+
return { renderer: "frame", output: frameOutput };
|
|
258
|
+
}
|
|
259
|
+
export async function regenerateGoldenFrames(binaryVersion) {
|
|
260
|
+
const resolvedVersion = binaryVersion ?? await version();
|
|
261
|
+
const captures = loadDemoCaptures();
|
|
262
|
+
const regenerated = [];
|
|
263
|
+
for (const golden of GOLDEN_FRAME_CASES) {
|
|
264
|
+
const output = new PassThrough();
|
|
265
|
+
output.isTTY = golden.interactive;
|
|
266
|
+
output.columns = golden.columns;
|
|
267
|
+
output.rows = golden.rows;
|
|
268
|
+
const writes = [];
|
|
269
|
+
const write = output.write.bind(output);
|
|
270
|
+
output.write = ((chunk, ...args) => {
|
|
271
|
+
writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
272
|
+
return Reflect.apply(write, output, [chunk, ...args]);
|
|
273
|
+
});
|
|
274
|
+
const emission = await captureCockpitOutput({
|
|
275
|
+
cockpit: golden.cockpit,
|
|
276
|
+
output: output,
|
|
277
|
+
binaryVersion: resolvedVersion,
|
|
278
|
+
columns: golden.columns,
|
|
279
|
+
rows: golden.rows,
|
|
280
|
+
interactive: golden.interactive,
|
|
281
|
+
colour: golden.colour,
|
|
282
|
+
ci: golden.ci,
|
|
283
|
+
captures,
|
|
284
|
+
});
|
|
285
|
+
regenerated.push({
|
|
286
|
+
...golden,
|
|
287
|
+
...emission,
|
|
288
|
+
emitted: writes.join(""),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return regenerated;
|
|
292
|
+
}
|
|
293
|
+
export function findGoldenFrameMismatches(regenerated, committed) {
|
|
294
|
+
const expected = new Set(regenerated.map((frame) => frame.fixture));
|
|
295
|
+
return [
|
|
296
|
+
...regenerated.flatMap((frame) => committed.get(frame.fixture) === frame.output ? [] : [frame.fixture]),
|
|
297
|
+
...[...committed.keys()].filter((fixture) => !expected.has(fixture)),
|
|
298
|
+
].sort();
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Regenerate only the colour contract corpus from its own manifest. Historical
|
|
302
|
+
* run sources are read and rendered byte-for-byte with their captured daemon
|
|
303
|
+
* stopped, matching the demo path and avoiding live-process state in committed
|
|
304
|
+
* bytes.
|
|
305
|
+
*/
|
|
306
|
+
export async function regenerateColourFrames(binaryVersion) {
|
|
307
|
+
const resolvedVersion = binaryVersion ?? await version();
|
|
308
|
+
const regenerated = [];
|
|
309
|
+
for (const colourCase of COLOUR_FRAME_CASES) {
|
|
310
|
+
const source = {
|
|
311
|
+
fileName: colourCase.sourceFileName,
|
|
312
|
+
raw: readFileSync(join(COLOUR_SOURCE_DIRECTORY, colourCase.sourceFileName), "utf8"),
|
|
313
|
+
};
|
|
314
|
+
const data = deriveRunCockpitData(source, resolvedVersion, {
|
|
315
|
+
isDaemonAlive: () => false,
|
|
316
|
+
});
|
|
317
|
+
const output = await captureRendererOutput(frameNode({ cockpit: "run", data }, colourCase.columns, colourCase.rows), {
|
|
318
|
+
columns: colourCase.columns,
|
|
319
|
+
rows: colourCase.rows,
|
|
320
|
+
colour: colourCase.colour,
|
|
321
|
+
});
|
|
322
|
+
regenerated.push({
|
|
323
|
+
...colourCase,
|
|
324
|
+
output,
|
|
325
|
+
emitted: output,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return regenerated;
|
|
329
|
+
}
|
|
@@ -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;
|