@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.
- package/LICENSE +21 -0
- package/README.md +429 -0
- package/dist/chunk-TTOMORIY.js +502 -0
- package/dist/chunk-TTOMORIY.js.map +1 -0
- package/dist/hooks/post-receive.cjs +7822 -0
- package/dist/hooks/post-receive.cjs.map +1 -0
- package/dist/hooks/pre-receive.cjs +7890 -0
- package/dist/hooks/pre-receive.cjs.map +1 -0
- package/dist/index.js +5385 -0
- package/dist/index.js.map +1 -0
- package/dist/ui-4V2HDHOS.js +349 -0
- package/dist/ui-4V2HDHOS.js.map +1 -0
- package/package.json +55 -0
|
@@ -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
|
+
}
|