@openthink/stamp 1.0.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,349 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ commitMessage,
4
+ currentBranch,
5
+ findRepoRoot,
6
+ findTrustedKey,
7
+ firstParentCommits,
8
+ latestReviews,
9
+ openDb,
10
+ parseCommitAttestation,
11
+ stampStateDbPath,
12
+ verifyBytes
13
+ } from "./chunk-TTOMORIY.js";
14
+
15
+ // src/commands/ui.tsx
16
+ import { Box, render, Text, useApp, useInput, useStdout } from "ink";
17
+ import { existsSync } from "fs";
18
+ import { useMemo, useState } from "react";
19
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
20
+ var COMMITS_LIMIT = 30;
21
+ function loadRows(repoRoot, branch, limit) {
22
+ const commits = firstParentCommits(branch, limit, repoRoot);
23
+ return commits.map((c) => ({
24
+ sha: c.sha,
25
+ title: c.title,
26
+ attestation: parseCommitAttestation(c.body)?.payload ?? null
27
+ }));
28
+ }
29
+ function renderAttestationSummary(p) {
30
+ const approvals = p.approvals.map((a) => (a.verdict === "approved" ? "\u2713" : "\u2717") + a.reviewer).join(" ");
31
+ const checks = (p.checks ?? []).map((c) => (c.exit_code === 0 ? "\u2713" : "\u2717") + c.name).join(" ");
32
+ const checksPart = checks ? ` ${checks}` : "";
33
+ return `${approvals}${checksPart}`;
34
+ }
35
+ function CommitRow({ row, selected }) {
36
+ const marker = selected ? "\u25B6" : " ";
37
+ const sha = row.sha.slice(0, 10);
38
+ const body = row.attestation ? renderAttestationSummary(row.attestation) : "unstamped";
39
+ const bodyColor = row.attestation ? void 0 : "red";
40
+ return /* @__PURE__ */ jsxs(Box, { children: [
41
+ /* @__PURE__ */ jsxs(Text, { color: selected ? "yellow" : void 0, bold: selected, children: [
42
+ marker,
43
+ " ",
44
+ sha
45
+ ] }),
46
+ /* @__PURE__ */ jsx(Box, { marginRight: 2, children: /* @__PURE__ */ jsx(Text, { color: bodyColor, dimColor: !selected && !!row.attestation, children: body }) }),
47
+ /* @__PURE__ */ jsx(Text, { dimColor: !selected, children: row.title })
48
+ ] });
49
+ }
50
+ function loadDetail(repoRoot, sha) {
51
+ const msg = commitMessage(sha, repoRoot);
52
+ const title = (msg.split("\n")[0] ?? "").trim();
53
+ const parsed = parseCommitAttestation(msg);
54
+ if (!parsed) {
55
+ return { sha, title, parsed: null, sigStatus: null };
56
+ }
57
+ const trustedPem = findTrustedKey(repoRoot, parsed.payload.signer_key_id);
58
+ if (!trustedPem) {
59
+ return { sha, title, parsed, sigStatus: "untrusted" };
60
+ }
61
+ let ok = false;
62
+ try {
63
+ ok = verifyBytes(trustedPem, parsed.payloadBytes, parsed.signatureBase64);
64
+ } catch {
65
+ ok = false;
66
+ }
67
+ return { sha, title, parsed, sigStatus: ok ? "valid" : "invalid" };
68
+ }
69
+ function SigBadge({ status }) {
70
+ if (status === null) return /* @__PURE__ */ jsx(Text, { color: "red", children: "n/a" });
71
+ if (status === "valid") return /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713 valid" });
72
+ if (status === "invalid") return /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2717 INVALID" });
73
+ return /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2717 untrusted key (not in .stamp/trusted-keys/)" });
74
+ }
75
+ function Detail({
76
+ data,
77
+ hasReviewProse
78
+ }) {
79
+ const { sha, title, parsed, sigStatus } = data;
80
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
81
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: "yellow", bold: true, children: [
82
+ "detail \u2014 ",
83
+ sha.slice(0, 12)
84
+ ] }) }),
85
+ /* @__PURE__ */ jsx(Text, { children: title }),
86
+ !parsed ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: "no Stamp-Payload trailer \u2014 commit is unstamped" }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
87
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
88
+ /* @__PURE__ */ jsxs(Text, { children: [
89
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "target: " }),
90
+ parsed.payload.target_branch
91
+ ] }),
92
+ /* @__PURE__ */ jsxs(Text, { children: [
93
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "base\u2192head: " }),
94
+ parsed.payload.base_sha.slice(0, 10),
95
+ " \u2192",
96
+ " ",
97
+ parsed.payload.head_sha.slice(0, 10)
98
+ ] }),
99
+ /* @__PURE__ */ jsxs(Text, { children: [
100
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "signer: " }),
101
+ parsed.payload.signer_key_id
102
+ ] }),
103
+ /* @__PURE__ */ jsxs(Text, { children: [
104
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "signature: " }),
105
+ /* @__PURE__ */ jsx(SigBadge, { status: sigStatus })
106
+ ] })
107
+ ] }),
108
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
109
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "approvals:" }),
110
+ parsed.payload.approvals.map((a) => /* @__PURE__ */ jsxs(Text, { children: [
111
+ " ",
112
+ /* @__PURE__ */ jsx(Text, { color: a.verdict === "approved" ? "green" : "red", children: a.verdict === "approved" ? "\u2713" : "\u2717" }),
113
+ " ",
114
+ a.reviewer.padEnd(12),
115
+ " ",
116
+ a.verdict
117
+ ] }, a.reviewer))
118
+ ] }),
119
+ parsed.payload.checks && parsed.payload.checks.length > 0 && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
120
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "checks:" }),
121
+ parsed.payload.checks.map((c) => /* @__PURE__ */ jsxs(Text, { children: [
122
+ " ",
123
+ /* @__PURE__ */ jsx(Text, { color: c.exit_code === 0 ? "green" : "red", children: c.exit_code === 0 ? "\u2713" : "\u2717" }),
124
+ " ",
125
+ c.name.padEnd(12),
126
+ " exit ",
127
+ c.exit_code
128
+ ] }, c.name))
129
+ ] })
130
+ ] }),
131
+ /* @__PURE__ */ jsx(Box, { marginTop: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
132
+ "esc back q quit",
133
+ hasReviewProse ? " r reviews" : " (no review prose in local DB)"
134
+ ] }) })
135
+ ] });
136
+ }
137
+ function loadReviewProse(repoRoot, payload) {
138
+ const dbPath = stampStateDbPath(repoRoot);
139
+ if (!existsSync(dbPath)) return [];
140
+ const db = openDb(dbPath);
141
+ try {
142
+ const rows = latestReviews(db, payload.base_sha, payload.head_sha);
143
+ const byName = new Map(rows.map((r) => [r.reviewer, r]));
144
+ const ordered = [];
145
+ for (const a of payload.approvals) {
146
+ const row = byName.get(a.reviewer);
147
+ if (row) ordered.push(row);
148
+ }
149
+ return ordered;
150
+ } finally {
151
+ db.close();
152
+ }
153
+ }
154
+ function ReviewProse({
155
+ sha,
156
+ reviews,
157
+ index,
158
+ scrollOffset,
159
+ viewportHeight
160
+ }) {
161
+ const current = reviews[index];
162
+ const hasProse = (current.issues ?? "").trim().length > 0;
163
+ const lines = useMemo(() => (current.issues ?? "").split("\n"), [current]);
164
+ const visible = lines.slice(scrollOffset, scrollOffset + viewportHeight);
165
+ const hasMoreBelow = scrollOffset + viewportHeight < lines.length;
166
+ const hasMoreAbove = scrollOffset > 0;
167
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
168
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
169
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", bold: true, children: [
170
+ "review: ",
171
+ current.reviewer
172
+ ] }),
173
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
174
+ " \u2014 commit ",
175
+ sha.slice(0, 10)
176
+ ] })
177
+ ] }),
178
+ /* @__PURE__ */ jsxs(Text, { children: [
179
+ /* @__PURE__ */ jsx(Text, { color: current.verdict === "approved" ? "green" : "red", children: "verdict:" }),
180
+ " ",
181
+ current.verdict
182
+ ] }),
183
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, height: 1, children: hasMoreAbove ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191 more above" }) : null }),
184
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: !hasProse ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no prose recorded)" }) : visible.map((line, i) => /* @__PURE__ */ jsx(Text, { children: line || " " }, `${scrollOffset}-${i}`)) }),
185
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, height: 1, children: hasMoreBelow ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2193 more below" }) : null }),
186
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
187
+ "(",
188
+ index + 1,
189
+ "/",
190
+ reviews.length,
191
+ ") n next p prev \u2191\u2193/jk scroll esc back q quit"
192
+ ] }) })
193
+ ] });
194
+ }
195
+ function App({
196
+ repoRoot,
197
+ branch,
198
+ rows
199
+ }) {
200
+ const { exit } = useApp();
201
+ const { stdout } = useStdout();
202
+ const [selected, setSelected] = useState(0);
203
+ const [mode, setMode] = useState({ kind: "list" });
204
+ const proseViewportHeight = Math.max(5, (stdout?.rows ?? 30) - 12);
205
+ const contextSha = mode.kind === "detail" || mode.kind === "reviews" ? mode.sha : null;
206
+ const commitContext = useMemo(() => {
207
+ if (contextSha === null) return null;
208
+ const data = loadDetail(repoRoot, contextSha);
209
+ const reviews = data.parsed ? loadReviewProse(repoRoot, data.parsed.payload) : [];
210
+ return { data, reviews };
211
+ }, [contextSha, repoRoot]);
212
+ useInput((input, key) => {
213
+ if (input === "q") {
214
+ exit();
215
+ return;
216
+ }
217
+ if (mode.kind === "reviews") {
218
+ if (key.escape) {
219
+ setMode({ kind: "detail", sha: mode.sha });
220
+ return;
221
+ }
222
+ if (input === "n") {
223
+ setMode({
224
+ ...mode,
225
+ index: (mode.index + 1) % mode.reviews.length,
226
+ scroll: 0
227
+ });
228
+ return;
229
+ }
230
+ if (input === "p") {
231
+ setMode({
232
+ ...mode,
233
+ index: (mode.index - 1 + mode.reviews.length) % mode.reviews.length,
234
+ scroll: 0
235
+ });
236
+ return;
237
+ }
238
+ if (key.downArrow || input === "j") {
239
+ const current = mode.reviews[mode.index];
240
+ const lineCount = (current.issues ?? "").split("\n").length;
241
+ const maxScroll = Math.max(0, lineCount - proseViewportHeight);
242
+ setMode({ ...mode, scroll: Math.min(maxScroll, mode.scroll + 1) });
243
+ return;
244
+ }
245
+ if (key.upArrow || input === "k") {
246
+ setMode({ ...mode, scroll: Math.max(0, mode.scroll - 1) });
247
+ return;
248
+ }
249
+ return;
250
+ }
251
+ if (mode.kind === "detail") {
252
+ if (key.escape) {
253
+ setMode({ kind: "list" });
254
+ return;
255
+ }
256
+ if (input === "r" && commitContext && commitContext.reviews.length > 0) {
257
+ setMode({
258
+ kind: "reviews",
259
+ sha: mode.sha,
260
+ reviews: commitContext.reviews,
261
+ index: 0,
262
+ scroll: 0
263
+ });
264
+ return;
265
+ }
266
+ return;
267
+ }
268
+ if (key.escape) {
269
+ exit();
270
+ return;
271
+ }
272
+ if (key.upArrow || input === "k") {
273
+ setSelected((i) => Math.max(0, i - 1));
274
+ return;
275
+ }
276
+ if (key.downArrow || input === "j") {
277
+ setSelected((i) => Math.min(rows.length - 1, i + 1));
278
+ return;
279
+ }
280
+ if (key.return && rows[selected]) {
281
+ setMode({ kind: "detail", sha: rows[selected].sha });
282
+ return;
283
+ }
284
+ });
285
+ if (mode.kind === "reviews") {
286
+ return /* @__PURE__ */ jsx(
287
+ ReviewProse,
288
+ {
289
+ sha: mode.sha,
290
+ reviews: mode.reviews,
291
+ index: mode.index,
292
+ scrollOffset: mode.scroll,
293
+ viewportHeight: proseViewportHeight
294
+ }
295
+ );
296
+ }
297
+ if (mode.kind === "detail" && commitContext) {
298
+ return /* @__PURE__ */ jsx(
299
+ Detail,
300
+ {
301
+ data: commitContext.data,
302
+ hasReviewProse: commitContext.reviews.length > 0
303
+ }
304
+ );
305
+ }
306
+ if (rows.length === 0) {
307
+ return /* @__PURE__ */ jsxs(Box, { padding: 1, flexDirection: "column", children: [
308
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", bold: true, children: [
309
+ "stamp ui \u2014 ",
310
+ branch
311
+ ] }),
312
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: "No commits on this branch." }) }),
313
+ /* @__PURE__ */ jsx(Box, { marginTop: 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "q to quit" }) })
314
+ ] });
315
+ }
316
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
317
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
318
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", bold: true, children: [
319
+ "stamp ui \u2014 ",
320
+ branch
321
+ ] }),
322
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
323
+ "(",
324
+ rows.length,
325
+ " commit",
326
+ rows.length === 1 ? "" : "s",
327
+ ", first-parent)"
328
+ ] })
329
+ ] }),
330
+ rows.map((row, i) => /* @__PURE__ */ jsx(CommitRow, { row, selected: i === selected }, row.sha)),
331
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 / jk navigate \u23CE detail q quit" }) })
332
+ ] });
333
+ }
334
+ function runUi() {
335
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
336
+ console.error(
337
+ "error: `stamp ui` requires an interactive terminal (TTY). Run it directly, not under a pipe/redirect or non-interactive shell."
338
+ );
339
+ process.exit(1);
340
+ }
341
+ const repoRoot = findRepoRoot();
342
+ const branch = currentBranch(repoRoot);
343
+ const rows = loadRows(repoRoot, branch, COMMITS_LIMIT);
344
+ render(/* @__PURE__ */ jsx(App, { repoRoot, branch, rows }));
345
+ }
346
+ export {
347
+ runUi
348
+ };
349
+ //# sourceMappingURL=ui-4V2HDHOS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/ui.tsx"],"sourcesContent":["import { Box, render, Text, useApp, useInput, useStdout } from \"ink\";\nimport { existsSync } from \"node:fs\";\nimport { useMemo, useState } from \"react\";\nimport {\n parseCommitAttestation,\n type AttestationPayload,\n type ParsedAttestation,\n} from \"../lib/attestation.js\";\nimport { latestReviews, openDb, type LatestReview } from \"../lib/db.js\";\nimport {\n commitMessage,\n currentBranch,\n firstParentCommits,\n} from \"../lib/git.js\";\nimport { findTrustedKey } from \"../lib/keys.js\";\nimport { findRepoRoot, stampStateDbPath } from \"../lib/paths.js\";\nimport { verifyBytes } from \"../lib/signing.js\";\n\n/**\n * Phase 2.B/TUI step 4 — list + detail + review prose viewer.\n *\n * List:\n * ↑/↓ or j/k navigate\n * ⏎ open detail for selected commit\n * q/esc quit\n *\n * Detail:\n * r open review prose viewer (if any reviews in local DB)\n * esc back to list\n * q quit\n *\n * Reviews:\n * n next reviewer (cycles)\n * p previous reviewer\n * ↑/↓ or j/k scroll prose\n * esc back to detail\n * q quit\n *\n * Ctrl-C via ink's default in all modes.\n *\n * Exit codes: 0 on clean quit, 1 if no TTY is available.\n */\n\nconst COMMITS_LIMIT = 30;\n\ninterface Row {\n sha: string;\n title: string;\n attestation: AttestationPayload | null;\n}\n\nfunction loadRows(repoRoot: string, branch: string, limit: number): Row[] {\n const commits = firstParentCommits(branch, limit, repoRoot);\n return commits.map((c) => ({\n sha: c.sha,\n title: c.title,\n attestation: parseCommitAttestation(c.body)?.payload ?? null,\n }));\n}\n\nfunction renderAttestationSummary(p: AttestationPayload): string {\n const approvals = p.approvals\n .map((a) => (a.verdict === \"approved\" ? \"✓\" : \"✗\") + a.reviewer)\n .join(\" \");\n const checks = (p.checks ?? [])\n .map((c) => (c.exit_code === 0 ? \"✓\" : \"✗\") + c.name)\n .join(\" \");\n const checksPart = checks ? ` ${checks}` : \"\";\n return `${approvals}${checksPart}`;\n}\n\nfunction CommitRow({ row, selected }: { row: Row; selected: boolean }) {\n const marker = selected ? \"▶\" : \" \";\n const sha = row.sha.slice(0, 10);\n const body = row.attestation\n ? renderAttestationSummary(row.attestation)\n : \"unstamped\";\n const bodyColor = row.attestation ? undefined : \"red\";\n\n return (\n <Box>\n <Text color={selected ? \"yellow\" : undefined} bold={selected}>\n {marker} {sha}\n </Text>\n <Box marginRight={2}>\n <Text color={bodyColor} dimColor={!selected && !!row.attestation}>\n {body}\n </Text>\n </Box>\n <Text dimColor={!selected}>{row.title}</Text>\n </Box>\n );\n}\n\n// ---------- detail ----------\n\ninterface DetailData {\n sha: string;\n title: string;\n parsed: ParsedAttestation | null;\n sigStatus: \"valid\" | \"invalid\" | \"untrusted\" | null;\n}\n\nfunction loadDetail(repoRoot: string, sha: string): DetailData {\n const msg = commitMessage(sha, repoRoot);\n const title = (msg.split(\"\\n\")[0] ?? \"\").trim();\n const parsed = parseCommitAttestation(msg);\n if (!parsed) {\n return { sha, title, parsed: null, sigStatus: null };\n }\n const trustedPem = findTrustedKey(repoRoot, parsed.payload.signer_key_id);\n if (!trustedPem) {\n return { sha, title, parsed, sigStatus: \"untrusted\" };\n }\n let ok = false;\n try {\n ok = verifyBytes(trustedPem, parsed.payloadBytes, parsed.signatureBase64);\n } catch {\n ok = false;\n }\n return { sha, title, parsed, sigStatus: ok ? \"valid\" : \"invalid\" };\n}\n\nfunction SigBadge({ status }: { status: DetailData[\"sigStatus\"] }) {\n if (status === null) return <Text color=\"red\">n/a</Text>;\n if (status === \"valid\") return <Text color=\"green\">✓ valid</Text>;\n if (status === \"invalid\") return <Text color=\"red\">✗ INVALID</Text>;\n return <Text color=\"red\">✗ untrusted key (not in .stamp/trusted-keys/)</Text>;\n}\n\nfunction Detail({\n data,\n hasReviewProse,\n}: {\n data: DetailData;\n hasReviewProse: boolean;\n}) {\n const { sha, title, parsed, sigStatus } = data;\n\n return (\n <Box flexDirection=\"column\" padding={1}>\n <Box marginBottom={1}>\n <Text color=\"yellow\" bold>\n detail — {sha.slice(0, 12)}\n </Text>\n </Box>\n <Text>{title}</Text>\n\n {!parsed ? (\n <Box marginTop={1}>\n <Text color=\"red\">no Stamp-Payload trailer — commit is unstamped</Text>\n </Box>\n ) : (\n <>\n <Box marginTop={1} flexDirection=\"column\">\n <Text>\n <Text dimColor>target: </Text>\n {parsed.payload.target_branch}\n </Text>\n <Text>\n <Text dimColor>base→head: </Text>\n {parsed.payload.base_sha.slice(0, 10)} →{\" \"}\n {parsed.payload.head_sha.slice(0, 10)}\n </Text>\n <Text>\n <Text dimColor>signer: </Text>\n {parsed.payload.signer_key_id}\n </Text>\n <Text>\n <Text dimColor>signature: </Text>\n <SigBadge status={sigStatus} />\n </Text>\n </Box>\n\n <Box marginTop={1} flexDirection=\"column\">\n <Text bold>approvals:</Text>\n {parsed.payload.approvals.map((a) => (\n <Text key={a.reviewer}>\n {\" \"}\n <Text color={a.verdict === \"approved\" ? \"green\" : \"red\"}>\n {a.verdict === \"approved\" ? \"✓\" : \"✗\"}\n </Text>{\" \"}\n {a.reviewer.padEnd(12)} {a.verdict}\n </Text>\n ))}\n </Box>\n\n {parsed.payload.checks && parsed.payload.checks.length > 0 && (\n <Box marginTop={1} flexDirection=\"column\">\n <Text bold>checks:</Text>\n {parsed.payload.checks.map((c) => (\n <Text key={c.name}>\n {\" \"}\n <Text color={c.exit_code === 0 ? \"green\" : \"red\"}>\n {c.exit_code === 0 ? \"✓\" : \"✗\"}\n </Text>{\" \"}\n {c.name.padEnd(12)} exit {c.exit_code}\n </Text>\n ))}\n </Box>\n )}\n </>\n )}\n\n <Box marginTop={2}>\n <Text dimColor>\n esc back q quit\n {hasReviewProse ? \" r reviews\" : \" (no review prose in local DB)\"}\n </Text>\n </Box>\n </Box>\n );\n}\n\n// ---------- review prose viewer ----------\n\nfunction loadReviewProse(\n repoRoot: string,\n payload: AttestationPayload,\n): LatestReview[] {\n const dbPath = stampStateDbPath(repoRoot);\n if (!existsSync(dbPath)) return [];\n const db = openDb(dbPath);\n try {\n const rows = latestReviews(db, payload.base_sha, payload.head_sha);\n // Preserve attestation's reviewer order for consistent n/p cycling.\n const byName = new Map(rows.map((r) => [r.reviewer, r]));\n const ordered: LatestReview[] = [];\n for (const a of payload.approvals) {\n const row = byName.get(a.reviewer);\n if (row) ordered.push(row);\n }\n return ordered;\n } finally {\n db.close();\n }\n}\n\nfunction ReviewProse({\n sha,\n reviews,\n index,\n scrollOffset,\n viewportHeight,\n}: {\n sha: string;\n reviews: LatestReview[];\n index: number;\n scrollOffset: number;\n viewportHeight: number;\n}) {\n const current = reviews[index]!;\n const hasProse = (current.issues ?? \"\").trim().length > 0;\n const lines = useMemo(() => (current.issues ?? \"\").split(\"\\n\"), [current]);\n const visible = lines.slice(scrollOffset, scrollOffset + viewportHeight);\n const hasMoreBelow = scrollOffset + viewportHeight < lines.length;\n const hasMoreAbove = scrollOffset > 0;\n\n return (\n <Box flexDirection=\"column\" padding={1}>\n <Box marginBottom={1}>\n <Text color=\"yellow\" bold>\n review: {current.reviewer}\n </Text>\n <Text dimColor> — commit {sha.slice(0, 10)}</Text>\n </Box>\n <Text>\n <Text color={current.verdict === \"approved\" ? \"green\" : \"red\"}>\n verdict:\n </Text>{\" \"}\n {current.verdict}\n </Text>\n\n <Box marginTop={1} height={1}>\n {hasMoreAbove ? <Text dimColor>↑ more above</Text> : null}\n </Box>\n\n <Box flexDirection=\"column\">\n {!hasProse ? (\n <Text dimColor>(no prose recorded)</Text>\n ) : (\n visible.map((line, i) => (\n <Text key={`${scrollOffset}-${i}`}>{line || \" \"}</Text>\n ))\n )}\n </Box>\n\n <Box marginTop={1} height={1}>\n {hasMoreBelow ? <Text dimColor>↓ more below</Text> : null}\n </Box>\n\n <Box marginTop={1}>\n <Text dimColor>\n ({index + 1}/{reviews.length}) n next p prev ↑↓/jk scroll esc back q quit\n </Text>\n </Box>\n </Box>\n );\n}\n\n// ---------- app ----------\n\ntype Mode =\n | { kind: \"list\" }\n | { kind: \"detail\"; sha: string }\n | {\n kind: \"reviews\";\n sha: string;\n reviews: LatestReview[];\n index: number;\n scroll: number;\n };\n\nfunction App({\n repoRoot,\n branch,\n rows,\n}: {\n repoRoot: string;\n branch: string;\n rows: Row[];\n}) {\n const { exit } = useApp();\n const { stdout } = useStdout();\n const [selected, setSelected] = useState(0);\n const [mode, setMode] = useState<Mode>({ kind: \"list\" });\n\n // Leave room for the header/footer chrome — ~12 rows of non-prose per screen\n // (title, verdict line, scroll indicators above & below, key-hint line,\n // padding).\n const proseViewportHeight = Math.max(5, (stdout?.rows ?? 30) - 12);\n\n // Detail data + the matching review-prose rows are both keyed on the\n // commit SHA. Memoize them together so SQLite is opened once per commit\n // (re-runs only when the user opens detail for a different commit).\n const contextSha =\n mode.kind === \"detail\" || mode.kind === \"reviews\" ? mode.sha : null;\n const commitContext = useMemo(() => {\n if (contextSha === null) return null;\n const data = loadDetail(repoRoot, contextSha);\n const reviews = data.parsed\n ? loadReviewProse(repoRoot, data.parsed.payload)\n : [];\n return { data, reviews };\n }, [contextSha, repoRoot]);\n\n useInput((input, key) => {\n if (input === \"q\") {\n exit();\n return;\n }\n\n if (mode.kind === \"reviews\") {\n if (key.escape) {\n setMode({ kind: \"detail\", sha: mode.sha });\n return;\n }\n if (input === \"n\") {\n setMode({\n ...mode,\n index: (mode.index + 1) % mode.reviews.length,\n scroll: 0,\n });\n return;\n }\n if (input === \"p\") {\n setMode({\n ...mode,\n index: (mode.index - 1 + mode.reviews.length) % mode.reviews.length,\n scroll: 0,\n });\n return;\n }\n if (key.downArrow || input === \"j\") {\n const current = mode.reviews[mode.index]!;\n const lineCount = (current.issues ?? \"\").split(\"\\n\").length;\n const maxScroll = Math.max(0, lineCount - proseViewportHeight);\n setMode({ ...mode, scroll: Math.min(maxScroll, mode.scroll + 1) });\n return;\n }\n if (key.upArrow || input === \"k\") {\n setMode({ ...mode, scroll: Math.max(0, mode.scroll - 1) });\n return;\n }\n return;\n }\n\n if (mode.kind === \"detail\") {\n if (key.escape) {\n setMode({ kind: \"list\" });\n return;\n }\n if (input === \"r\" && commitContext && commitContext.reviews.length > 0) {\n setMode({\n kind: \"reviews\",\n sha: mode.sha,\n reviews: commitContext.reviews,\n index: 0,\n scroll: 0,\n });\n return;\n }\n return;\n }\n\n // list mode\n if (key.escape) {\n exit();\n return;\n }\n if (key.upArrow || input === \"k\") {\n setSelected((i) => Math.max(0, i - 1));\n return;\n }\n if (key.downArrow || input === \"j\") {\n setSelected((i) => Math.min(rows.length - 1, i + 1));\n return;\n }\n if (key.return && rows[selected]) {\n setMode({ kind: \"detail\", sha: rows[selected]!.sha });\n return;\n }\n });\n\n if (mode.kind === \"reviews\") {\n return (\n <ReviewProse\n sha={mode.sha}\n reviews={mode.reviews}\n index={mode.index}\n scrollOffset={mode.scroll}\n viewportHeight={proseViewportHeight}\n />\n );\n }\n\n if (mode.kind === \"detail\" && commitContext) {\n return (\n <Detail\n data={commitContext.data}\n hasReviewProse={commitContext.reviews.length > 0}\n />\n );\n }\n\n if (rows.length === 0) {\n return (\n <Box padding={1} flexDirection=\"column\">\n <Text color=\"yellow\" bold>\n stamp ui — {branch}\n </Text>\n <Box marginTop={1}>\n <Text>No commits on this branch.</Text>\n </Box>\n <Box marginTop={2}>\n <Text dimColor>q to quit</Text>\n </Box>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\" padding={1}>\n <Box marginBottom={1}>\n <Text color=\"yellow\" bold>\n stamp ui — {branch}\n </Text>\n <Text dimColor>\n ({rows.length} commit{rows.length === 1 ? \"\" : \"s\"}, first-parent)\n </Text>\n </Box>\n {rows.map((row, i) => (\n <CommitRow key={row.sha} row={row} selected={i === selected} />\n ))}\n <Box marginTop={1}>\n <Text dimColor>\n ↑↓ / jk navigate ⏎ detail q quit\n </Text>\n </Box>\n </Box>\n );\n}\n\nexport function runUi(): void {\n if (!process.stdin.isTTY || !process.stdout.isTTY) {\n console.error(\n \"error: `stamp ui` requires an interactive terminal (TTY). \" +\n \"Run it directly, not under a pipe/redirect or non-interactive shell.\",\n );\n process.exit(1);\n }\n\n const repoRoot = findRepoRoot();\n const branch = currentBranch(repoRoot);\n const rows = loadRows(repoRoot, branch, COMMITS_LIMIT);\n render(<App repoRoot={repoRoot} branch={branch} rows={rows} />);\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@openthink/stamp",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Local, headless pull-request system for agent-to-agent code review workflows",
6
+ "bin": {
7
+ "stamp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup && node -e \"require('fs').readdirSync('dist',{recursive:true,withFileTypes:true}).filter(e=>e.isFile()&&/\\.(js|cjs)$/.test(e.name)).forEach(e=>{const p=require('path').join(e.parentPath||e.path,e.name);const c=require('fs').readFileSync(p,'utf8').replace(/from \\\"sqlite\\\"/g,'from \\\"node:sqlite\\\"');require('fs').writeFileSync(p,c)})\"",
14
+ "dev": "tsx src/index.ts",
15
+ "typecheck": "tsc --noEmit",
16
+ "check-conventions": "bash scripts/check-conventions.sh",
17
+ "test:unit": "node --test --import tsx 'tests/*.test.ts'",
18
+ "test:integration": "node --test --import tsx --test-timeout=120000 'tests/integration/*.test.ts'",
19
+ "test": "npm run check-conventions && npm run test:unit",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "homepage": "https://openthink.dev",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/OpenThinkAi/stamp-cli.git"
26
+ },
27
+ "keywords": [
28
+ "cli",
29
+ "pull-request",
30
+ "code-review",
31
+ "ai",
32
+ "agents",
33
+ "local-first",
34
+ "git"
35
+ ],
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=22.5.0"
39
+ },
40
+ "dependencies": {
41
+ "@anthropic-ai/claude-agent-sdk": "^0.2.114",
42
+ "commander": "^13.1.0",
43
+ "ink": "^7.0.1",
44
+ "react": "^19.2.5",
45
+ "yaml": "^2.7.0",
46
+ "zod": "^4.4.2"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.14.1",
50
+ "@types/react": "^19.2.14",
51
+ "tsup": "^8.4.0",
52
+ "tsx": "^4.19.3",
53
+ "typescript": "^5.8.3"
54
+ }
55
+ }