@fullstackhouse/cerber 0.5.0 → 0.7.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/README.md +22 -5
- package/dist/cli/index.js +1 -0
- package/dist/core/anchor.js +73 -0
- package/dist/core/artifact.js +18 -0
- package/dist/core/diff.js +16 -5
- package/dist/core/gh.js +29 -3
- package/dist/core/refresh.js +54 -0
- package/dist/core/send.js +15 -3
- package/dist/core/state.js +25 -0
- package/dist/runner/inflight.js +33 -0
- package/dist/runner/prompt.js +10 -3
- package/dist/runner/review.js +26 -2
- package/dist/server/daemon.js +7 -1
- package/dist/server/index.js +82 -3
- package/package.json +8 -1
- package/web/dist/assets/index-Bq-5Sr6-.js +236 -0
- package/web/dist/assets/{index-D00teMaV.css → index-CwjpyotK.css} +10 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-pyON6-n7.js +0 -229
package/dist/server/index.js
CHANGED
|
@@ -8,9 +8,12 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { ArtifactStatusSchema, VerdictSchema, artifactKey } from "../core/artifact.js";
|
|
10
10
|
import { toMarkdown } from "../core/export.js";
|
|
11
|
-
import { submitReview } from "../core/gh.js";
|
|
11
|
+
import { fetchPrDiff, fetchPrInfo, submitReview } from "../core/gh.js";
|
|
12
|
+
import { refreshArtifact } from "../core/refresh.js";
|
|
12
13
|
import { buildReviewPayload, computeCalibration } from "../core/send.js";
|
|
13
|
-
import { listArtifacts, loadArtifactByKey, updateArtifactByKey } from "../core/state.js";
|
|
14
|
+
import { listArtifacts, loadArtifactByKey, reconcileRunning, updateArtifactByKey, } from "../core/state.js";
|
|
15
|
+
import { isReviewRunning } from "../runner/inflight.js";
|
|
16
|
+
import { reviewPr } from "../runner/review.js";
|
|
14
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
18
|
export async function buildApp(opts) {
|
|
16
19
|
const app = new Hono();
|
|
@@ -117,6 +120,8 @@ export async function buildApp(opts) {
|
|
|
117
120
|
origin: "user",
|
|
118
121
|
status: "draft",
|
|
119
122
|
editedByUser: false,
|
|
123
|
+
originalLine: null,
|
|
124
|
+
drifted: false,
|
|
120
125
|
},
|
|
121
126
|
],
|
|
122
127
|
}));
|
|
@@ -134,6 +139,76 @@ export async function buildApp(opts) {
|
|
|
134
139
|
return c.json({ error: "not found" }, 404);
|
|
135
140
|
return c.json(updated);
|
|
136
141
|
});
|
|
142
|
+
// ---- Freshness: pull a review forward onto the PR's current head ----
|
|
143
|
+
// Read-only against GitHub (pr view + pr diff); writes only the local artifact.
|
|
144
|
+
app.post("/api/reviews/:key/refresh", async (c) => {
|
|
145
|
+
const artifact = await loadArtifactByKey(c.req.param("key"));
|
|
146
|
+
if (!artifact)
|
|
147
|
+
return c.json({ error: "not found" }, 404);
|
|
148
|
+
const ref = { owner: artifact.pr.owner, repo: artifact.pr.repo, number: artifact.pr.number };
|
|
149
|
+
let pr;
|
|
150
|
+
try {
|
|
151
|
+
pr = await fetchPrInfo(ref);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 502);
|
|
155
|
+
}
|
|
156
|
+
const stale = artifact.pr.headSha !== "" && artifact.pr.headSha !== pr.headSha;
|
|
157
|
+
// A sent review is a record of what was posted — never rewrite it. A running
|
|
158
|
+
// one is mid-run and would be overwritten from under the runner.
|
|
159
|
+
if (!stale || artifact.sent || artifact.status === "running") {
|
|
160
|
+
return c.json({ stale, changed: false, prState: pr.state, artifact });
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const diff = await fetchPrDiff(ref);
|
|
164
|
+
const result = refreshArtifact(artifact, pr, diff);
|
|
165
|
+
const saved = await updateArtifactByKey(c.req.param("key"), () => result.artifact);
|
|
166
|
+
return c.json({
|
|
167
|
+
stale: true,
|
|
168
|
+
changed: result.changed,
|
|
169
|
+
prState: pr.state,
|
|
170
|
+
moved: result.moved,
|
|
171
|
+
drifted: result.drifted,
|
|
172
|
+
artifact: saved,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 502);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// ---- Re-review: start an AI run for a PR that has moved on ----
|
|
180
|
+
app.post("/api/reviews/:key/rerun", async (c) => {
|
|
181
|
+
const key = c.req.param("key");
|
|
182
|
+
const artifact = await loadArtifactByKey(key);
|
|
183
|
+
if (!artifact)
|
|
184
|
+
return c.json({ error: "not found" }, 404);
|
|
185
|
+
if (artifact.sent)
|
|
186
|
+
return c.json({ error: `already sent at ${artifact.sent.at}` }, 409);
|
|
187
|
+
if (isReviewRunning(artifact.id)) {
|
|
188
|
+
return c.json({ error: "a review of this PR is already running" }, 409);
|
|
189
|
+
}
|
|
190
|
+
const ref = { owner: artifact.pr.owner, repo: artifact.pr.repo, number: artifact.pr.number };
|
|
191
|
+
// Mark it running before responding, so the cockpit's next poll can't catch
|
|
192
|
+
// the old "ready" status and conclude the run already finished.
|
|
193
|
+
const running = await updateArtifactByKey(key, (a) => ({
|
|
194
|
+
...a,
|
|
195
|
+
status: "running",
|
|
196
|
+
run: { model: null, startedAt: new Date().toISOString(), finishedAt: null, costUsd: null, error: null },
|
|
197
|
+
}));
|
|
198
|
+
// Minutes-long: run it detached and let the cockpit poll the artifact.
|
|
199
|
+
void reviewPr(ref, { force: true, onProgress: (m) => console.log(`[rerun ${artifact.id}] ${m}`) })
|
|
200
|
+
.catch(async (err) => {
|
|
201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
202
|
+
console.error(`[rerun ${artifact.id}] failed: ${message}`);
|
|
203
|
+
// The runner marks the artifact failed once it owns it; a failure before
|
|
204
|
+
// that (fetching the PR, or a run already in flight) would otherwise
|
|
205
|
+
// leave it stuck at "running" with nothing to clear it.
|
|
206
|
+
await updateArtifactByKey(key, (a) => a.status === "running"
|
|
207
|
+
? { ...a, status: "failed", run: a.run ? { ...a.run, error: message } : null }
|
|
208
|
+
: a).catch(() => { });
|
|
209
|
+
});
|
|
210
|
+
return c.json(running, 202);
|
|
211
|
+
});
|
|
137
212
|
// ---- Export (local file download) ----
|
|
138
213
|
app.get("/api/reviews/:key/export", async (c) => {
|
|
139
214
|
const artifact = await loadArtifactByKey(c.req.param("key"));
|
|
@@ -160,7 +235,7 @@ export async function buildApp(opts) {
|
|
|
160
235
|
}
|
|
161
236
|
const payload = buildReviewPayload(artifact, event);
|
|
162
237
|
try {
|
|
163
|
-
const { url } = await submitReview({ owner: artifact.pr.owner, repo: artifact.pr.repo, number: artifact.pr.number }, { event: payload.event, body: payload.body, comments: payload.comments });
|
|
238
|
+
const { url } = await submitReview({ owner: artifact.pr.owner, repo: artifact.pr.repo, number: artifact.pr.number }, { event: payload.event, body: payload.body, comments: payload.comments, commitId: payload.commitId });
|
|
164
239
|
const updated = await updateArtifactByKey(c.req.param("key"), (a) => ({
|
|
165
240
|
...a,
|
|
166
241
|
status: "sent",
|
|
@@ -198,6 +273,10 @@ export async function buildApp(opts) {
|
|
|
198
273
|
return app;
|
|
199
274
|
}
|
|
200
275
|
export async function startServer(opts) {
|
|
276
|
+
const cleared = await reconcileRunning();
|
|
277
|
+
if (cleared > 0) {
|
|
278
|
+
console.log(`Cleared ${cleared} review(s) left mid-run by a previous process — re-review to retry.`);
|
|
279
|
+
}
|
|
201
280
|
const app = await buildApp(opts);
|
|
202
281
|
serve({ fetch: app.fetch, port: opts.port, hostname: opts.host }, (info) => {
|
|
203
282
|
const tokenHint = opts.token ? `/?token=${opts.token}` : "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstackhouse/cerber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "AI code-review cockpit: Claude reviews your PRs into local artifacts; nothing reaches GitHub until you say so.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:fullstackhouse/cerber",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"README.md",
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"provenance": true
|
|
24
|
+
},
|
|
21
25
|
"scripts": {
|
|
22
26
|
"dev": "tsx src/cli/index.ts",
|
|
23
27
|
"build": "tsc -p tsconfig.build.json && vite build --config web/vite.config.ts",
|
|
@@ -33,14 +37,17 @@
|
|
|
33
37
|
"zod": "^3.24.1"
|
|
34
38
|
},
|
|
35
39
|
"devDependencies": {
|
|
40
|
+
"@semantic-release/git": "^11.0.1",
|
|
36
41
|
"@types/node": "^22.10.5",
|
|
37
42
|
"@types/react": "^19.0.4",
|
|
38
43
|
"@types/react-dom": "^19.0.2",
|
|
39
44
|
"@vitejs/plugin-react": "^4.3.4",
|
|
40
45
|
"dompurify": "^3.4.13",
|
|
46
|
+
"highlight.js": "^11.12.0",
|
|
41
47
|
"marked": "^18.0.10",
|
|
42
48
|
"react": "^19.0.0",
|
|
43
49
|
"react-dom": "^19.0.0",
|
|
50
|
+
"semantic-release": "^25.0.9",
|
|
44
51
|
"tsx": "^4.19.2",
|
|
45
52
|
"typescript": "^5.7.3",
|
|
46
53
|
"vite": "^6.0.7",
|