@expo/code-review-cli 0.6.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 +33 -12
- package/build/commands/ci.js +8 -8
- package/build/commands/doctor.js +167 -33
- package/build/commands/setup-auth.js +83 -11
- package/build/config/schema.js +7 -3
- package/build/core/auth.js +122 -9
- package/build/core/claude-code.js +680 -0
- package/build/core/exec.js +278 -9
- package/build/core/opencode.js +95 -15
- package/build/core/prompts.js +19 -2
- package/build/core/render.js +21 -2
- package/build/core/review.js +158 -25
- package/build/core/schema.js +6 -1
- package/build/core/scrub.js +59 -1
- package/build/core/throttle.js +10 -0
- package/build/core/util.js +17 -0
- package/build/core/verify.js +13 -1
- package/build/reporters/github.js +79 -13
- package/build/sources/github-pr.js +14 -7
- package/build/sources/local-git.js +3 -2
- package/package.json +3 -3
- package/templates/config.jsonc +21 -3
- package/templates/shared.md +28 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { run } from "../core/exec.js";
|
|
4
|
+
import { resolveTrustedTool, run } from "../core/exec.js";
|
|
5
5
|
import { parseUnifiedDiff } from "../core/diff.js";
|
|
6
|
-
import { scrubAmbientRuntimeConfig } from "../core/scrub.js";
|
|
6
|
+
import { removeEscapingSymlinks, scrubAmbientRuntimeConfig } from "../core/scrub.js";
|
|
7
7
|
/** A full 40-hex-char commit OID — the only ref form passed to security-sensitive git calls. */
|
|
8
8
|
export function isCommitOid(value) {
|
|
9
9
|
return typeof value === "string" && /^[0-9a-f]{40}$/i.test(value);
|
|
@@ -38,7 +38,8 @@ export class GitHubPRSource {
|
|
|
38
38
|
return (this.metadataPromise ??= this.fetchMetadata());
|
|
39
39
|
}
|
|
40
40
|
async fetchMetadata() {
|
|
41
|
-
const
|
|
41
|
+
const gh = await resolveTrustedTool("gh");
|
|
42
|
+
const { stdout } = await run(gh, [
|
|
42
43
|
"pr",
|
|
43
44
|
"view",
|
|
44
45
|
String(this.options.prNumber),
|
|
@@ -60,7 +61,8 @@ export class GitHubPRSource {
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
async getChangedFiles() {
|
|
63
|
-
const
|
|
64
|
+
const gh = await resolveTrustedTool("gh");
|
|
65
|
+
const { stdout } = await run(gh, ["pr", "diff", String(this.options.prNumber), ...this.repoArgs()], { cwd: this.options.cwd });
|
|
64
66
|
return parseUnifiedDiff(stdout);
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
@@ -77,21 +79,22 @@ export class GitHubPRSource {
|
|
|
77
79
|
}
|
|
78
80
|
const cwd = this.options.cwd;
|
|
79
81
|
const url = `https://github.com/${this.options.repo}.git`;
|
|
82
|
+
const gitPath = await resolveTrustedTool("git");
|
|
80
83
|
let parent;
|
|
81
84
|
try {
|
|
82
|
-
await run(
|
|
85
|
+
await run(gitPath, [...GH_CREDENTIAL_HELPER_ARGS, "fetch", "--no-tags", "--depth=1", url, ref], { cwd });
|
|
83
86
|
parent = await mkdtemp(path.join(tmpdir(), "ecr-tree-"));
|
|
84
87
|
const dir = path.join(parent, "tree"); // must not pre-exist for `worktree add`
|
|
85
88
|
// Check out the OID (not FETCH_HEAD): if the ref moved between the API call
|
|
86
89
|
// and this fetch, the OID is absent and this fails instead of silently
|
|
87
90
|
// materializing a different tree than the one the diff was fetched for.
|
|
88
|
-
await run(
|
|
91
|
+
await run(gitPath, ["worktree", "add", "--detach", dir, oid], { cwd });
|
|
89
92
|
const removeParent = parent;
|
|
90
93
|
return {
|
|
91
94
|
dir,
|
|
92
95
|
cleanup: async () => {
|
|
93
96
|
try {
|
|
94
|
-
await run(
|
|
97
|
+
await run(gitPath, ["worktree", "remove", "--force", dir], { cwd });
|
|
95
98
|
}
|
|
96
99
|
catch {
|
|
97
100
|
// best effort — fall through to removing the temp dir
|
|
@@ -118,6 +121,9 @@ export class GitHubPRSource {
|
|
|
118
121
|
* OpenCode server is started with this directory as its project root, and
|
|
119
122
|
* anything it discovers there is attacker-controlled PR content executing or
|
|
120
123
|
* injecting inside a process that holds the model credential and GH_TOKEN.
|
|
124
|
+
* Out-of-tree symlinks are stripped in the same pass: read tools are scoped by
|
|
125
|
+
* the literal path argument but follow symlinks underneath, so a PR-committed
|
|
126
|
+
* link escaping the tree would otherwise read arbitrary host files.
|
|
121
127
|
*
|
|
122
128
|
* Returns null only when no owner/repo is configured (a local `--pr` run
|
|
123
129
|
* without --repo, where the current checkout is an acceptable read root).
|
|
@@ -136,6 +142,7 @@ export class GitHubPRSource {
|
|
|
136
142
|
const root = await this.materializeWorktreeAsync(`refs/pull/${this.options.prNumber}/head`, metadata.headOid);
|
|
137
143
|
try {
|
|
138
144
|
await scrubAmbientRuntimeConfig(root.dir);
|
|
145
|
+
await removeEscapingSymlinks(root.dir);
|
|
139
146
|
}
|
|
140
147
|
catch (error) {
|
|
141
148
|
// A half-scrubbed tree must never become the runtime's project root.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { git, run } from "../core/exec.js";
|
|
1
|
+
import { git, resolveTrustedTool, run } from "../core/exec.js";
|
|
2
2
|
import { parseUnifiedDiff } from "../core/diff.js";
|
|
3
3
|
/**
|
|
4
4
|
* Reads local git state. No network calls. Default compares the working tree
|
|
@@ -94,7 +94,8 @@ export class LocalGitSource {
|
|
|
94
94
|
const chunks = [];
|
|
95
95
|
for (const file of files) {
|
|
96
96
|
// `--` so a filename beginning with `-` can't be read as a git option.
|
|
97
|
-
const
|
|
97
|
+
const gitPath = await resolveTrustedTool("git");
|
|
98
|
+
const { stdout } = await run(gitPath, ["diff", "--no-index", "--", "/dev/null", file], {
|
|
98
99
|
cwd: this.cwd,
|
|
99
100
|
check: false,
|
|
100
101
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/code-review-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Generic, config-driven AI code reviewer engine. Repos supply their agents via .expo-code-review/.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"clean": "rimraf build",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"lint": "oxlint src",
|
|
31
|
-
"fmt": "oxfmt src",
|
|
32
|
-
"fmt:check": "oxfmt --check src",
|
|
31
|
+
"fmt": "oxfmt --threads=1 src",
|
|
32
|
+
"fmt:check": "oxfmt --threads=1 --check src",
|
|
33
33
|
"dev": "bun run src/cli.ts",
|
|
34
34
|
"test:unit": "bun test",
|
|
35
35
|
"release": "bash scripts/release.sh",
|
package/templates/config.jsonc
CHANGED
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
// store the key as a repo secret and pass it under that env var
|
|
50
50
|
// (the scaffolded workflow does). If you omit `auth` entirely,
|
|
51
51
|
// OpenCode's own login / ambient provider env vars are used.
|
|
52
|
-
// For Anthropic/Claude,
|
|
53
|
-
//
|
|
54
|
-
//
|
|
52
|
+
// For Anthropic/Claude, see the dedicated block below (it now runs through the
|
|
53
|
+
// Claude Code CLI). For another provider, omit `auth` and set REVIEWER_MODEL
|
|
54
|
+
// after an `opencode auth login` for that provider.
|
|
55
55
|
//
|
|
56
56
|
// MIXED setup (a ChatGPT/Codex subscription for the default models, plus a
|
|
57
57
|
// metered API key for pro-tier models the subscription doesn't offer): use the
|
|
@@ -65,6 +65,24 @@
|
|
|
65
65
|
// (openai oauth: tokenEnv holds the ACCESS token from an `opencode auth login`
|
|
66
66
|
// ChatGPT sign-in — `ecr setup-auth` extracts it. NEVER share the refresh
|
|
67
67
|
// token: it is single-use and dies on first rotation.)
|
|
68
|
+
//
|
|
69
|
+
// Anthropic runs through the Claude Code CLI, inferred from the model — set your
|
|
70
|
+
// models to "anthropic/…" (e.g. anthropic/claude-opus-5, anthropic/claude-sonnet-5)
|
|
71
|
+
// and run `claude setup-token`, exporting the token as CLAUDE_CODE_OAUTH_TOKEN (CI)
|
|
72
|
+
// or rely on your local `claude` login. An auth entry is OPTIONAL (tokenEnv just
|
|
73
|
+
// names the credential env); `ecr setup-auth` walks you through it.
|
|
74
|
+
// "auth": { "providers": {
|
|
75
|
+
// "anthropic": { "tokenEnv": "CLAUDE_CODE_OAUTH_TOKEN" }
|
|
76
|
+
// } }
|
|
77
|
+
//
|
|
78
|
+
// MIXING engines is supported: the engine is inferred per agent from its model, so
|
|
79
|
+
// an anthropic entry may coexist with an openai (or any other) OpenCode provider.
|
|
80
|
+
// Each agent's `model` selects its engine — an `anthropic/…` agent runs through the
|
|
81
|
+
// Claude Code CLI while an `openai/…` agent runs through OpenCode, in the SAME run.
|
|
82
|
+
// "auth": { "providers": {
|
|
83
|
+
// "anthropic": { "tokenEnv": "CLAUDE_CODE_OAUTH_TOKEN" },
|
|
84
|
+
// "openai": { "mode": "oauth", "tokenEnv": "CODEX_OAUTH_ACCESS_TOKEN" }
|
|
85
|
+
// } }
|
|
68
86
|
"auth": {
|
|
69
87
|
"mode": "api-key",
|
|
70
88
|
"provider": "openai",
|
package/templates/shared.md
CHANGED
|
@@ -63,6 +63,34 @@ firehose. When in doubt, stay silent.
|
|
|
63
63
|
**For now, report only `critical` and `warning` findings. Do not emit
|
|
64
64
|
`suggestion`-level items at all.**
|
|
65
65
|
|
|
66
|
+
## Write findings in Simplified Technical English
|
|
67
|
+
|
|
68
|
+
Your findings are read by engineers in many countries. Many of them do not speak
|
|
69
|
+
English as a first language. Write every piece of prose you emit — `title`,
|
|
70
|
+
`rationale`, `suggestion` — under the ASD-STE100 Simplified Technical English
|
|
71
|
+
rules:
|
|
72
|
+
|
|
73
|
+
- **One word, one meaning.** Choose one term for a thing and reuse it. Do not
|
|
74
|
+
alternate between synonyms for the same object ("the handler" / "the callback"
|
|
75
|
+
/ "the hook").
|
|
76
|
+
- **Short sentences.** Use 20 words or fewer. Split a long sentence into two.
|
|
77
|
+
- **Active voice.** Write "the parser drops the flag", not "the flag is dropped
|
|
78
|
+
by the parser". Name the actor.
|
|
79
|
+
- **Plain words.** Write "use", not "utilize"; "before", not "prior to";
|
|
80
|
+
"because", not "due to the fact that". Remove hedges ("arguably", "it seems
|
|
81
|
+
that") and intensifiers ("very", "extremely").
|
|
82
|
+
- **One topic per paragraph.** Keep paragraphs short.
|
|
83
|
+
- **No idiom, metaphor, or sarcasm.** State what happens.
|
|
84
|
+
|
|
85
|
+
This rule is about prose only. `evidence` and any code you quote are copied
|
|
86
|
+
verbatim and are never rewritten to fit these rules. Identifiers, file paths,
|
|
87
|
+
error strings, and the `severity`/`category` values also stay exactly as they
|
|
88
|
+
are.
|
|
89
|
+
|
|
90
|
+
Simple language must not cost precision. Keep the concrete failure path, the
|
|
91
|
+
condition that triggers it, and the names of the affected code. Short sentences
|
|
92
|
+
are a way to say the same thing, not a way to say less.
|
|
93
|
+
|
|
66
94
|
## Output contract
|
|
67
95
|
|
|
68
96
|
Return **only** a single fenced ```json code block, an object of this shape:
|