@fullstackhouse/cerber 0.30.0 โ 0.31.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 +131 -73
- package/dist/cli/index.js +56 -1
- package/dist/core/gh.js +7 -2
- package/dist/core/preflight.js +82 -0
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
# cerber ๐
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@fullstackhouse/cerber)
|
|
4
|
+
[](https://github.com/fullstackhouse/cerber/actions/workflows/ci.yml)
|
|
4
5
|
[](./LICENSE)
|
|
5
6
|
|
|
6
|
-
**AI code-review cockpit.** Claude reviews your pull requests into local
|
|
7
|
-
a summary, a chaptered walkthrough of the changes, draft inline
|
|
8
|
-
verdict with a confidence score. You
|
|
7
|
+
**AI code-review cockpit.** Claude reviews your pull requests into local
|
|
8
|
+
artifacts โ a summary, a chaptered walkthrough of the changes, draft inline
|
|
9
|
+
comments, and a verdict with a confidence score. You read it in a local web
|
|
10
|
+
cockpit, keep or rewrite or drop each comment, then press Send when you agree
|
|
11
|
+
with it.
|
|
9
12
|
|
|
10
13
|
**Nothing reaches GitHub until you explicitly say so.** Cerber's only GitHub
|
|
11
14
|
write is the Send button (plus opt-in daemon auto-send you turn on yourself) โ
|
|
12
|
-
reviewing is 100% local and read-only.
|
|
15
|
+
reviewing is 100% local and read-only. Reviews go out under your own account,
|
|
16
|
+
as your review, because you decided each line of it should.
|
|
17
|
+
|
|
18
|
+
That gate is the whole point, and the reason this is not another review bot:
|
|
19
|
+
|
|
20
|
+
> Reviewing pull requests with an AI model creates a specific danger: a system
|
|
21
|
+
> that can both draft opinions and hold GitHub credentials will eventually post
|
|
22
|
+
> one without a human deciding it should.
|
|
13
23
|
|
|
14
24
|
Named after Cerberus, the gatekeeper: cerber guards what gets merged.
|
|
15
25
|
|
|
26
|
+
**A drafted review, as you read it** โ verdict and confidence beside the PR
|
|
27
|
+
title, the walkthrough rail on the left, a summary written for someone who has
|
|
28
|
+
not read the diff:
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
**And the queue they arrive in** โ `cerber serve` polls GitHub for PRs awaiting
|
|
33
|
+
your review and drafts one for each, so they are written by the time you get
|
|
34
|
+
there:
|
|
35
|
+
|
|
16
36
|

|
|
17
37
|
|
|
18
38
|
*Every screenshot in this README is cerber reviewing cerber's own pull
|
|
@@ -20,11 +40,25 @@ requests.*
|
|
|
20
40
|
|
|
21
41
|
## Quick start
|
|
22
42
|
|
|
23
|
-
Requires Node 20+, an authenticated [`gh`](https://cli.github.com/) and
|
|
24
|
-
logged-in [`claude`](https://claude.com/claude-code) CLI. No API keys, no
|
|
43
|
+
Requires Node 20+, an authenticated [`gh`](https://cli.github.com/), `git`, and
|
|
44
|
+
a logged-in [`claude`](https://claude.com/claude-code) CLI. No API keys, no
|
|
45
|
+
config, no database.
|
|
46
|
+
|
|
47
|
+
**Claude Code is a hard requirement, and a paid one.** Cerber drafts reviews by
|
|
48
|
+
running `claude` as you: it rides your existing Claude Code login and draws on
|
|
49
|
+
that plan's usage limits, so you need a Claude subscription that includes Claude
|
|
50
|
+
Code. There is no `ANTHROPIC_API_KEY` path โ not as a fallback, not at all.
|
|
51
|
+
GitHub only; no GitLab or Bitbucket. Notifications are macOS and Linux.
|
|
52
|
+
|
|
53
|
+
Run `cerber doctor` first โ it checks all three tools and tells you which one to
|
|
54
|
+
fix, instead of letting a missing login turn into an empty cockpit later.
|
|
25
55
|
|
|
26
56
|
```bash
|
|
27
|
-
#
|
|
57
|
+
# Open the cockpit โ this is the whole product
|
|
58
|
+
npx @fullstackhouse/cerber serve # โ http://127.0.0.1:4820
|
|
59
|
+
|
|
60
|
+
# Or review one PR from the terminal (writes a local artifact to ~/.cerber,
|
|
61
|
+
# nothing else)
|
|
28
62
|
npx @fullstackhouse/cerber review https://github.com/owner/repo/pull/123
|
|
29
63
|
|
|
30
64
|
# Review several
|
|
@@ -33,22 +67,79 @@ npx @fullstackhouse/cerber review 123 124 --repo owner/repo
|
|
|
33
67
|
|
|
34
68
|
# See what's in the queue
|
|
35
69
|
npx @fullstackhouse/cerber list
|
|
36
|
-
|
|
37
|
-
# Open the cockpit โ it's an inbox: it polls GitHub for PRs awaiting your
|
|
38
|
-
# review and drafts a review for each, so they're ready when you arrive
|
|
39
|
-
npx @fullstackhouse/cerber serve # โ http://127.0.0.1:4820
|
|
40
70
|
```
|
|
41
71
|
|
|
42
72
|
`serve` does the whole loop by default: discover โ draft โ wait for you.
|
|
43
73
|
Sending stays a human click. Tame it with `--no-auto-review` (list awaiting
|
|
44
74
|
PRs, review on click) or `--no-poll` (no GitHub polling at all) โ or flip those
|
|
45
75
|
switches in the cockpit's Settings, which persist in `~/.cerber/config.json`
|
|
46
|
-
and apply on the next poll.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
and apply on the next poll.
|
|
77
|
+
|
|
78
|
+
## Contents
|
|
79
|
+
|
|
80
|
+
- [What a review looks like](#what-a-review-looks-like) โ what Claude writes and how you walk it
|
|
81
|
+
- [The inbox](#the-inbox) โ what the queue shows, what settles a row, the notifications
|
|
82
|
+
- [It reviews the code, not just the diff](#it-reviews-the-code-not-just-the-diff)
|
|
83
|
+
- [Trusted PRs: reviews that can run things](#trusted-prs-reviews-that-can-run-things)
|
|
84
|
+
- [When the PR moves under you](#when-the-pr-moves-under-you)
|
|
85
|
+
- [Arguing with the review before you send it](#arguing-with-the-review-before-you-send-it)
|
|
86
|
+
- [Every row remembers what happened to it](#every-row-remembers-what-happened-to-it)
|
|
87
|
+
- [Status](#status) ยท [Running on a VPS](#running-on-a-vps) ยท [How it works](#how-it-works) ยท [Development](#development)
|
|
88
|
+
|
|
89
|
+
## What a review looks like
|
|
90
|
+
|
|
91
|
+
Each review is a plain JSON artifact in `~/.cerber/reviews/` (override with
|
|
92
|
+
`CERBER_HOME`) containing:
|
|
93
|
+
|
|
94
|
+
- **Summary** โ what the PR actually does and why, written top-down
|
|
95
|
+
- **Chapters** โ the changed files grouped into a logical walkthrough, each
|
|
96
|
+
with a title, an explanation, and its slice of the diff
|
|
97
|
+
- **Draft comments** โ inline, anchored to file/line, only things worth a
|
|
98
|
+
human's time. Findings are graded **blocker / minor / nit** โ the words mean
|
|
99
|
+
what they mean everywhere, and only a blocker stands between the PR and
|
|
100
|
+
approval; a question or note carries no grade at all
|
|
101
|
+
- **Verdict** โ approve / comment / request changes, with a 0โ100 confidence
|
|
102
|
+
score and reasoning that names the worst finding still standing
|
|
103
|
+
- **Run metadata** โ model, whether it read the source or only the diff, and
|
|
104
|
+
the token spend as an API-rate equivalent
|
|
105
|
+
|
|
106
|
+
The cockpit (`cerber serve`) renders the queue and the per-PR walkthrough with
|
|
107
|
+
diffs. Draft comments sit inline in the diff, anchored to the line they're
|
|
108
|
+
about, each waiting for you to keep, rewrite, or drop it:
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
Any line of the diff is one click away from being talked about: hover it and a
|
|
113
|
+
`+` appears in the gutter, the way it does on GitHub. What opens is one box
|
|
114
|
+
with two exits โ write the comment yourself, or ask the reviewer about that
|
|
115
|
+
line and get the answer in the conversation below. A line the PR removes can be
|
|
116
|
+
asked about too; a comment on one posts on the file, since GitHub only takes
|
|
117
|
+
inline comments on the new side of a diff.
|
|
118
|
+
|
|
119
|
+
A markdown file the PR **adds** opens as the document it is, not as a thousand
|
|
120
|
+
rows of `+ ## Heading` โ because reviewing a new spec means reading it, and
|
|
121
|
+
every line being an addition means the diff's markers say nothing. It is still
|
|
122
|
+
the review: the draft comments sit in the document under the paragraph they
|
|
123
|
+
point at, and any paragraph takes a comment or a question where it stands. A
|
|
124
|
+
markdown file the PR only *edits* stays a diff โ there the change is the point
|
|
125
|
+
โ with "read as a document" in its header when you want the prose instead.
|
|
126
|
+
|
|
127
|
+
Artifacts are plain JSON you can `cat`, edit, or pipe into anything.
|
|
128
|
+
|
|
129
|
+
The queue is meant to be walked, not clicked through: `j`/`k` move the cursor
|
|
130
|
+
and the strip under the table explains whatever it lands on โ the verdict's own
|
|
131
|
+
reasoning, what the run read, how it sits against the auto-send bar โ so most
|
|
132
|
+
rows can be judged without opening them. `โต` opens one, `r` drafts (or
|
|
133
|
+
re-drafts) it. Inside a review, `[` and `]` walk to the previous/next PR still
|
|
134
|
+
awaiting you, `n` steps through the chapters, and `s` sends.
|
|
135
|
+
|
|
136
|
+
## The inbox
|
|
137
|
+
|
|
138
|
+
The queue only lists what still wants you: anything you have settled moves to
|
|
139
|
+
the filed tabs right of the thin rule in the filter row โ reviews you sent,
|
|
140
|
+
ones you marked reviewed or skipped, merged and closed PRs โ each tab appearing
|
|
141
|
+
only while it holds something. PRs in archived repos are never picked up at
|
|
142
|
+
all, since the repo is read-only and a review could never be sent.
|
|
52
143
|
|
|
53
144
|
### Settled means you settled it
|
|
54
145
|
|
|
@@ -129,56 +220,7 @@ anything, the favicon wears a red dot, and it goes away when the last review is
|
|
|
129
220
|
dealt with. No permission, no switch โ just the paw with a mark on it, so a
|
|
130
221
|
glance at the tab strip answers "is there anything for me?".
|
|
131
222
|
|
|
132
|
-
##
|
|
133
|
-
|
|
134
|
-
Each review is a plain JSON artifact in `~/.cerber/reviews/` (override with
|
|
135
|
-
`CERBER_HOME`) containing:
|
|
136
|
-
|
|
137
|
-
- **Summary** โ what the PR actually does and why, written top-down
|
|
138
|
-
- **Chapters** โ the changed files grouped into a logical walkthrough, each
|
|
139
|
-
with a title, an explanation, and its slice of the diff
|
|
140
|
-
- **Draft comments** โ inline, anchored to file/line, only things worth a
|
|
141
|
-
human's time. Findings are graded **blocker / minor / nit** โ the words mean
|
|
142
|
-
what they mean everywhere, and only a blocker stands between the PR and
|
|
143
|
-
approval; a question or note carries no grade at all
|
|
144
|
-
- **Verdict** โ approve / comment / request changes, with a 0โ100 confidence
|
|
145
|
-
score and reasoning that names the worst finding still standing
|
|
146
|
-
- **Run metadata** โ model, whether it read the source or only the diff, and
|
|
147
|
-
the token spend as an API-rate equivalent
|
|
148
|
-
|
|
149
|
-

|
|
150
|
-
|
|
151
|
-
The cockpit (`cerber serve`) renders the queue and the per-PR walkthrough with
|
|
152
|
-
diffs. Draft comments sit inline in the diff, anchored to the line they're
|
|
153
|
-
about, each waiting for you to keep, rewrite, or drop it:
|
|
154
|
-
|
|
155
|
-

|
|
156
|
-
|
|
157
|
-
Any line of the diff is one click away from being talked about: hover it and a
|
|
158
|
-
`+` appears in the gutter, the way it does on GitHub. What opens is one box
|
|
159
|
-
with two exits โ write the comment yourself, or ask the reviewer about that
|
|
160
|
-
line and get the answer in the conversation below. A line the PR removes can be
|
|
161
|
-
asked about too; a comment on one posts on the file, since GitHub only takes
|
|
162
|
-
inline comments on the new side of a diff.
|
|
163
|
-
|
|
164
|
-
A markdown file the PR **adds** opens as the document it is, not as a thousand
|
|
165
|
-
rows of `+ ## Heading` โ because reviewing a new spec means reading it, and
|
|
166
|
-
every line being an addition means the diff's markers say nothing. It is still
|
|
167
|
-
the review: the draft comments sit in the document under the paragraph they
|
|
168
|
-
point at, and any paragraph takes a comment or a question where it stands. A
|
|
169
|
-
markdown file the PR only *edits* stays a diff โ there the change is the point
|
|
170
|
-
โ with "read as a document" in its header when you want the prose instead.
|
|
171
|
-
|
|
172
|
-
Artifacts are plain JSON you can `cat`, edit, or pipe into anything.
|
|
173
|
-
|
|
174
|
-
The queue is meant to be walked, not clicked through: `j`/`k` move the cursor
|
|
175
|
-
and the strip under the table explains whatever it lands on โ the verdict's own
|
|
176
|
-
reasoning, what the run read, how it sits against the auto-send bar โ so most
|
|
177
|
-
rows can be judged without opening them. `โต` opens one, `r` drafts (or
|
|
178
|
-
re-drafts) it. Inside a review, `[` and `]` walk to the previous/next PR still
|
|
179
|
-
awaiting you, `n` steps through the chapters, and `s` sends.
|
|
180
|
-
|
|
181
|
-
### It reviews the code, not just the diff
|
|
223
|
+
## It reviews the code, not just the diff
|
|
182
224
|
|
|
183
225
|
Cerber checks the PR's head out locally and lets the review read it. That
|
|
184
226
|
matters because a reviewer holding only a diff hedges over things it could have
|
|
@@ -235,7 +277,7 @@ Details worth knowing:
|
|
|
235
277
|
review is currently reading. `cerber prune` reclaims the space now;
|
|
236
278
|
`cerber prune --all` takes the ones for reviews still awaiting you too.
|
|
237
279
|
|
|
238
|
-
|
|
280
|
+
## Trusted PRs: reviews that can run things
|
|
239
281
|
|
|
240
282
|
Reading beats guessing, but running beats reading. A review that ran the test
|
|
241
283
|
covering the change reports what happened; one that only read it guesses. So
|
|
@@ -304,7 +346,7 @@ unattended by default: with trust rules set, it will run matching PRs' code
|
|
|
304
346
|
with nobody watching. It warns at startup, the cockpit shows a โก badge while
|
|
305
347
|
it's the case, and `--no-trust` (or `--no-auto-review`) turns it off.
|
|
306
348
|
|
|
307
|
-
|
|
349
|
+
## When the PR moves under you
|
|
308
350
|
|
|
309
351
|
A review is written against one commit, but authors keep pushing. Opening a
|
|
310
352
|
review checks the PR's head and pulls the review forward: comments follow their
|
|
@@ -315,10 +357,17 @@ commit's SHA, so inline comments land where they were written.
|
|
|
315
357
|
|
|
316
358
|
The AI's summary and verdict still describe the commit that was reviewed. To
|
|
317
359
|
get its opinion of the new code, hit **Re-review at the new head** in the
|
|
318
|
-
cockpit (or `cerber review <pr> --force`)
|
|
319
|
-
|
|
360
|
+
cockpit (or `cerber review <pr> --force`).
|
|
361
|
+
|
|
362
|
+
> โ ๏ธ **A re-review replaces the whole draft โ including comments you wrote or
|
|
363
|
+
> rewrote, and a send body you wrote yourself.** They are dropped when the run
|
|
364
|
+
> starts and they do not come back, on success or on failure. This is a
|
|
365
|
+
> decision, not a bug ([`docs/lifecycle.md`](docs/lifecycle.md)): the run owns
|
|
366
|
+
> the draft. If you have written comments you want to keep, send the review or
|
|
367
|
+
> copy them out first. Your *decisions* do survive โ a send, a `reviewed` or
|
|
368
|
+
> `skipped` you set, and the chat transcript are all kept.
|
|
320
369
|
|
|
321
|
-
|
|
370
|
+
## Arguing with the review before you send it
|
|
322
371
|
|
|
323
372
|
A draft you disagree with used to leave two options: rewrite it by hand, or
|
|
324
373
|
re-review and hope. Neither lets you say *what was wrong*. So every review has
|
|
@@ -376,7 +425,7 @@ the summary, comments and verdict alone, and is still one deliberate click. A
|
|
|
376
425
|
review that has already been sent can't be argued with: that artifact is the
|
|
377
426
|
record of what GitHub has.
|
|
378
427
|
|
|
379
|
-
|
|
428
|
+
## Every row remembers what happened to it
|
|
380
429
|
|
|
381
430
|
A review keeps one "last updated" time, which means every write erases the
|
|
382
431
|
answer to *when did I skip this, and did they ask again afterwards?* So each
|
|
@@ -455,6 +504,15 @@ pnpm build # dist/ (CLI+server) + web/dist (cockpit)
|
|
|
455
504
|
Web cockpit dev with hot reload: `pnpm dev serve` in one terminal,
|
|
456
505
|
`npx vite --config web/vite.config.ts` in another (proxies `/api`).
|
|
457
506
|
|
|
507
|
+
## Contributing and security
|
|
508
|
+
|
|
509
|
+
Patches welcome โ [`CONTRIBUTING.md`](./CONTRIBUTING.md) has the gate
|
|
510
|
+
(`pnpm typecheck && pnpm test`) and the one rule that is not negotiable.
|
|
511
|
+
|
|
512
|
+
Cerber holds your GitHub credentials and, on a trusted PR, runs that PR's code.
|
|
513
|
+
[`SECURITY.md`](./SECURITY.md) says exactly what that means, what is stripped
|
|
514
|
+
from a trusted run, and where to report a vulnerability privately.
|
|
515
|
+
|
|
458
516
|
## License
|
|
459
517
|
|
|
460
518
|
MIT ยฉ [Full Stack House](https://fullstack.house)
|
package/dist/cli/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import readline from "node:readline/promises";
|
|
4
5
|
import { artifactId, artifactKey } from "../core/artifact.js";
|
|
5
6
|
import { listCheckouts, removeCheckout } from "../core/checkout.js";
|
|
6
7
|
import { toMarkdown } from "../core/export.js";
|
|
7
8
|
import { configPath, loadConfig, saveConfig } from "../core/config.js";
|
|
9
|
+
import { formatBlockers, isBlocking, preflight } from "../core/preflight.js";
|
|
8
10
|
import { withWriter } from "../core/history.js";
|
|
9
11
|
import { TrustRuleError, describeRule, explainRule, parseTrustRule } from "../core/trust.js";
|
|
10
12
|
import { parsePrRef, searchAwaitingMe, submitReview } from "../core/gh.js";
|
|
@@ -15,10 +17,34 @@ import { isReviewRunning } from "../runner/inflight.js";
|
|
|
15
17
|
import { startDaemon } from "../server/daemon.js";
|
|
16
18
|
import { cockpitUrl, startServer } from "../server/index.js";
|
|
17
19
|
const program = new Command();
|
|
20
|
+
/**
|
|
21
|
+
* The published version, read from the manifest rather than typed here โ a
|
|
22
|
+
* hand-maintained copy drifted to 0.5.0 while 0.30.0 shipped, and `--version`
|
|
23
|
+
* is the first thing anyone runs to decide whether a project is alive.
|
|
24
|
+
*/
|
|
25
|
+
const { version } = createRequire(import.meta.url)("../../package.json");
|
|
26
|
+
/**
|
|
27
|
+
* Stop before the work starts when a tool cerber shells out to is missing or
|
|
28
|
+
* logged out, rather than letting it surface as an empty queue or a retrying
|
|
29
|
+
* poll. `allowDegraded` is for `serve --no-poll`, which is still useful for
|
|
30
|
+
* reading artifacts already on disk.
|
|
31
|
+
*/
|
|
32
|
+
async function requireTools(allowDegraded = false) {
|
|
33
|
+
const checks = await preflight();
|
|
34
|
+
if (!checks.some(isBlocking))
|
|
35
|
+
return;
|
|
36
|
+
const message = formatBlockers(checks);
|
|
37
|
+
if (allowDegraded) {
|
|
38
|
+
console.warn(`\n${message}\n`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.error(`\n${message}\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
18
44
|
program
|
|
19
45
|
.name("cerber")
|
|
20
46
|
.description("AI code-review cockpit. Claude reviews PRs into local artifacts; nothing reaches GitHub until you explicitly send it.")
|
|
21
|
-
.version(
|
|
47
|
+
.version(version);
|
|
22
48
|
program
|
|
23
49
|
.command("review")
|
|
24
50
|
.description("Run an AI review of one or more PRs (local artifact only โ no GitHub writes)")
|
|
@@ -32,6 +58,7 @@ program
|
|
|
32
58
|
.option("-t, --trust", "let the review run commands in the checkout (tests, typecheck, git log) whatever the trust config says")
|
|
33
59
|
.option("--no-trust", "keep the review read-only even if a trust rule matches")
|
|
34
60
|
.action(async (prs, opts) => {
|
|
61
|
+
await requireTools();
|
|
35
62
|
let refs;
|
|
36
63
|
if (opts.awaitingMe) {
|
|
37
64
|
const found = await searchAwaitingMe(opts.repo);
|
|
@@ -349,6 +376,31 @@ program
|
|
|
349
376
|
`${wouldSend} shadow candidate(s), ${autoSent} auto-sent.`);
|
|
350
377
|
}
|
|
351
378
|
});
|
|
379
|
+
program
|
|
380
|
+
.command("doctor")
|
|
381
|
+
.description("Check that everything cerber needs is installed and logged in")
|
|
382
|
+
.action(async () => {
|
|
383
|
+
const checks = await preflight();
|
|
384
|
+
for (const c of checks) {
|
|
385
|
+
if (c.status === "ok") {
|
|
386
|
+
console.log(` โ ${c.name.padEnd(22)} ${c.detail ?? ""}`.trimEnd());
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
console.log(` โ ${c.name.padEnd(22)} ${c.status === "missing" ? "not installed" : "not logged in"}`);
|
|
390
|
+
if (c.fix)
|
|
391
|
+
console.log(` ${c.fix}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const bad = checks.filter(isBlocking);
|
|
395
|
+
console.log("");
|
|
396
|
+
if (bad.length === 0) {
|
|
397
|
+
console.log("Ready. `cerber serve` opens the cockpit.");
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
console.log(`${bad.length} thing(s) to fix before cerber can review anything.`);
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
352
404
|
program
|
|
353
405
|
.command("serve", { isDefault: true })
|
|
354
406
|
.description("Start the review cockpit โ an inbox: it polls GitHub for PRs awaiting your review and drafts " +
|
|
@@ -377,6 +429,9 @@ program
|
|
|
377
429
|
`Pass --token <secret> (or CERBER_TOKEN), or --insecure if the network is trusted.`);
|
|
378
430
|
process.exit(1);
|
|
379
431
|
}
|
|
432
|
+
// Without `gh` the cockpit binds, polls into an error loop and shows an
|
|
433
|
+
// empty queue; `--no-poll` is the one shape that still works offline.
|
|
434
|
+
await requireTools(!opts.poll);
|
|
380
435
|
const threshold = Math.min(100, Math.max(50, Number(opts.autoSendThreshold) || 90));
|
|
381
436
|
if (opts.autoSend) {
|
|
382
437
|
console.log(`โ auto-send is ON: APPROVE verdicts with confidence โฅ ${threshold}% will be sent to GitHub as you, without a per-review confirmation.\n` +
|
package/dist/core/gh.js
CHANGED
|
@@ -68,8 +68,13 @@ export async function fetchPrInfo(ref) {
|
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* Org and team membership, for `@org/*` and `@org/team` trust rules. A 404 is
|
|
71
|
-
* GitHub's "no"
|
|
72
|
-
*
|
|
71
|
+
* GitHub's "no"; anything else (an outage, a rate limit) throws. Either way the
|
|
72
|
+
* answer is never "trusted", which is the property that matters.
|
|
73
|
+
*
|
|
74
|
+
* Note that a token without `read:org` also gets a 404 rather than a 403 โ so a
|
|
75
|
+
* missing scope reads as "not a member" and silently narrows trust instead of
|
|
76
|
+
* announcing itself. Fail-closed, but quiet: if a rule you expect to match
|
|
77
|
+
* doesn't, check the scope before the rule.
|
|
73
78
|
*/
|
|
74
79
|
export async function isOrgMember(org, login) {
|
|
75
80
|
return membershipCheck(["api", `orgs/${org}/members/${login}`, "--silent"]);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export function isBlocking(check) {
|
|
5
|
+
return check.status !== "ok";
|
|
6
|
+
}
|
|
7
|
+
async function run(bin, args) {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout, stderr } = await execFileAsync(bin, args);
|
|
10
|
+
return { ok: true, out: (stdout || stderr).trim() };
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const e = err;
|
|
14
|
+
// ENOENT is "not installed"; a non-zero exit is the tool answering "no".
|
|
15
|
+
if (e.code === "ENOENT")
|
|
16
|
+
return { ok: false, out: "ENOENT" };
|
|
17
|
+
return { ok: false, out: (e.stderr || e.stdout || e.message || "").trim() };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function checkGh() {
|
|
21
|
+
const version = await run("gh", ["--version"]);
|
|
22
|
+
if (!version.ok && version.out === "ENOENT") {
|
|
23
|
+
return {
|
|
24
|
+
name: "gh (GitHub CLI)",
|
|
25
|
+
status: "missing",
|
|
26
|
+
fix: "Install it: https://cli.github.com โ then run `gh auth login`.",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// `gh auth status` exits non-zero when no account is logged in, which is the
|
|
30
|
+
// case that otherwise surfaces as an empty cockpit.
|
|
31
|
+
const auth = await run("gh", ["auth", "status"]);
|
|
32
|
+
if (!auth.ok) {
|
|
33
|
+
return {
|
|
34
|
+
name: "gh (GitHub CLI)",
|
|
35
|
+
status: "unauthenticated",
|
|
36
|
+
fix: "Run `gh auth login` โ cerber reads PRs as you, and sends as you.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { name: "gh (GitHub CLI)", status: "ok", detail: version.out.split("\n")[0] };
|
|
40
|
+
}
|
|
41
|
+
export async function checkClaude() {
|
|
42
|
+
const version = await run("claude", ["--version"]);
|
|
43
|
+
if (!version.ok && version.out === "ENOENT") {
|
|
44
|
+
return {
|
|
45
|
+
name: "claude (Claude Code)",
|
|
46
|
+
status: "missing",
|
|
47
|
+
fix: "Install it: https://claude.com/claude-code โ cerber rides your login, there is no API-key path.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!version.ok) {
|
|
51
|
+
return {
|
|
52
|
+
name: "claude (Claude Code)",
|
|
53
|
+
status: "unauthenticated",
|
|
54
|
+
fix: "Run `claude` once and log in โ a review is drafted through your Claude Code session.",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { name: "claude (Claude Code)", status: "ok", detail: version.out.split("\n")[0] };
|
|
58
|
+
}
|
|
59
|
+
export async function checkGit() {
|
|
60
|
+
const version = await run("git", ["--version"]);
|
|
61
|
+
if (!version.ok) {
|
|
62
|
+
return {
|
|
63
|
+
name: "git",
|
|
64
|
+
status: "missing",
|
|
65
|
+
fix: "Install git โ cerber fetches each PR head into ~/.cerber/src to review the code, not just the diff.",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { name: "git", status: "ok", detail: version.out.split("\n")[0] };
|
|
69
|
+
}
|
|
70
|
+
/** Everything cerber shells out to, checked in parallel. */
|
|
71
|
+
export async function preflight() {
|
|
72
|
+
return Promise.all([checkGh(), checkClaude(), checkGit()]);
|
|
73
|
+
}
|
|
74
|
+
/** The blocking checks, formatted as the message printed instead of failing later. */
|
|
75
|
+
export function formatBlockers(checks) {
|
|
76
|
+
const bad = checks.filter(isBlocking);
|
|
77
|
+
const lines = bad.map((c) => {
|
|
78
|
+
const what = c.status === "missing" ? "not installed" : "not logged in";
|
|
79
|
+
return ` โ ${c.name} โ ${what}\n ${c.fix ?? ""}`.trimEnd();
|
|
80
|
+
});
|
|
81
|
+
return `cerber needs these before it can do anything:\n\n${lines.join("\n")}\n\nRun \`cerber doctor\` to re-check.`;
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstackhouse/cerber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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
|
+
"keywords": [
|
|
7
|
+
"code-review",
|
|
8
|
+
"pull-request",
|
|
9
|
+
"ai",
|
|
10
|
+
"claude",
|
|
11
|
+
"claude-code",
|
|
12
|
+
"github",
|
|
13
|
+
"cli",
|
|
14
|
+
"local-first",
|
|
15
|
+
"developer-tools",
|
|
16
|
+
"human-in-the-loop"
|
|
17
|
+
],
|
|
6
18
|
"author": "Full Stack House (https://fullstack.house)",
|
|
7
19
|
"repository": "github:fullstackhouse/cerber",
|
|
8
20
|
"type": "module",
|