@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.
@@ -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 { stdout } = await run("gh", [
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 { stdout } = await run("gh", ["pr", "diff", String(this.options.prNumber), ...this.repoArgs()], { cwd: this.options.cwd });
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("git", [...GH_CREDENTIAL_HELPER_ARGS, "fetch", "--no-tags", "--depth=1", url, ref], { cwd });
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("git", ["worktree", "add", "--detach", dir, oid], { cwd });
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("git", ["worktree", "remove", "--force", dir], { cwd });
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 { stdout } = await run("git", ["diff", "--no-index", "--", "/dev/null", file], {
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.6.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",
@@ -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, set provider "anthropic", tokenEnv "ANTHROPIC_API_KEY",
53
- // and an anthropic/... model above. For another provider, omit `auth` and set
54
- // REVIEWER_MODEL after an `opencode auth login` for that provider.
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",
@@ -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: