@expo/code-review-cli 0.5.0 → 0.5.1
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 +25 -27
- package/build/cli.js +5 -0
- package/build/commands/doctor.js +25 -0
- package/build/commands/setup-auth.js +200 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -26,7 +26,12 @@ flowchart TD
|
|
|
26
26
|
## Usage
|
|
27
27
|
|
|
28
28
|
Run via `npx @expo/code-review-cli <command>` (or the `ecr` / `expo-code-review`
|
|
29
|
-
binary once installed).
|
|
29
|
+
binary once installed). On a repo that already has `.expo-code-review/` set up,
|
|
30
|
+
getting model credentials for local runs is one command:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @expo/code-review-cli setup-auth
|
|
34
|
+
```
|
|
30
35
|
|
|
31
36
|
Reviewing a PR (`--pr`/`ci`) needs the GitHub CLI — `brew install gh && gh auth login`.
|
|
32
37
|
Everything else the reviewer needs (including the `opencode` runtime) ships with the
|
|
@@ -34,34 +39,24 @@ package.
|
|
|
34
39
|
|
|
35
40
|
### First-time setup
|
|
36
41
|
|
|
37
|
-
Scaffold, add credentials, verify.
|
|
38
|
-
|
|
39
42
|
```bash
|
|
40
|
-
# Scaffold .expo-code-review/ + a CI workflow (--no-workflow to skip)
|
|
43
|
+
# 1. Scaffold .expo-code-review/ + a CI workflow (--no-workflow to skip)
|
|
41
44
|
npx @expo/code-review-cli init
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
config reviews with GPT via `auth.mode "api-key"`.
|
|
46
|
-
|
|
47
|
-
Create the key in the OpenAI dashboard, scoped to the minimum the reviewer needs:
|
|
48
|
-
|
|
49
|
-
- Put it in a **dedicated project** (not "Default project") so you can set a
|
|
50
|
-
monthly budget + alert on it and see the reviewer's spend in isolation.
|
|
51
|
-
- Make it a **Restricted** key with exactly two permissions, both under *Model
|
|
52
|
-
capabilities*: **Responses (/v1/responses) → Request** and **Chat completions
|
|
53
|
-
(/v1/chat/completions) → Request**. Everything else — including *List models* —
|
|
54
|
-
stays **None** (the reviewer resolves model ids from its own catalog and only
|
|
55
|
-
ever makes inference requests).
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
export OPENAI_API_KEY=sk-proj-...
|
|
59
|
-
# Check env, config, and credentials
|
|
45
|
+
# 2. Get model credentials — guided; prints the export lines for your shell config
|
|
46
|
+
npx @expo/code-review-cli setup-auth
|
|
47
|
+
# 3. Verify env, config, and credentials
|
|
60
48
|
npx @expo/code-review-cli doctor
|
|
61
49
|
```
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
`setup-auth` reads the repo's config and walks through each credential it needs:
|
|
52
|
+
an OpenAI **API key** (the scaffolded default — it prints where to create the key
|
|
53
|
+
and the exact restricted permissions to grant), and/or a **ChatGPT/Codex
|
|
54
|
+
subscription** sign-in (it runs OpenCode's browser login and extracts the token
|
|
55
|
+
for you). `doctor` offers to run it whenever a credential is missing.
|
|
56
|
+
|
|
57
|
+
In CI, store the same values as repo secrets (`OPENAI_API_KEY`; plus
|
|
58
|
+
`CODEX_OAUTH_REFRESH_TOKEN` for the mixed setup) — the scaffolded workflow
|
|
59
|
+
forwards them.
|
|
65
60
|
|
|
66
61
|
**Have a ChatGPT Plus/Pro (Codex) subscription? Use both.** The recommended
|
|
67
62
|
production setup pairs the subscription (runs the default models at no marginal
|
|
@@ -115,6 +110,7 @@ is a ready example to adapt.
|
|
|
115
110
|
| `ecr init [--no-workflow] [--force]` | Scaffold `.expo-code-review/` (config, agents, prompts) + a CI workflow. |
|
|
116
111
|
| `ecr init --monorepo` | …and add a `routing.jsonc` routing manifest (one default scope). |
|
|
117
112
|
| `ecr init --scope <dir>` | Scaffold a per-team scope under `<dir>` and register it in the manifest. |
|
|
113
|
+
| `ecr setup-auth [--yes]` | Walk through getting model credentials for local runs (ChatGPT sign-in and/or API keys), printing the `export` lines for your shell config. |
|
|
118
114
|
| `ecr review [options]` | Review local changes and print an advisory review (default command). |
|
|
119
115
|
| `ecr review --scope <name>` | Review only one routing scope over just that scope's changed files. |
|
|
120
116
|
| `ecr ci` | Review the current GitHub PR and post/update a comment. For GitHub Actions. |
|
|
@@ -508,9 +504,11 @@ set in `config.auth` (credentials come from OpenCode):
|
|
|
508
504
|
access tokens are short-lived, so the refresh token is the durable secret and
|
|
509
505
|
OpenCode mints access tokens on demand. Refresh-token reuse across runs is
|
|
510
506
|
verified, so a static CI secret works.
|
|
511
|
-
- **The API key needs
|
|
512
|
-
|
|
513
|
-
|
|
507
|
+
- **The API key needs exactly two permissions** — a *Restricted* key with
|
|
508
|
+
*Model capabilities*: **Responses → Request** and **Chat completions →
|
|
509
|
+
Request**; everything else (including *List models*) stays None. Create it
|
|
510
|
+
in a dedicated, budget-capped project. (`ecr setup-auth` prints these
|
|
511
|
+
instructions too.)
|
|
514
512
|
- **In CI**, set the `ECR_EXPECTED_TOKEN_ENV` repo variable to the
|
|
515
513
|
comma-separated set of both env names
|
|
516
514
|
(`CODEX_OAUTH_REFRESH_TOKEN,OPENAI_API_KEY`) and pass both secrets in the
|
package/build/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dismissCommand } from "./commands/dismiss.js";
|
|
|
4
4
|
import { doctorCommand } from "./commands/doctor.js";
|
|
5
5
|
import { initCommand } from "./commands/init.js";
|
|
6
6
|
import { reviewCommand } from "./commands/review.js";
|
|
7
|
+
import { setupAuthCommand } from "./commands/setup-auth.js";
|
|
7
8
|
import { verifyConfigCommand } from "./commands/verify-config.js";
|
|
8
9
|
const USAGE = `expo-code-review (ecr) — config-driven AI code reviewer
|
|
9
10
|
|
|
@@ -13,6 +14,7 @@ Usage:
|
|
|
13
14
|
ecr dismiss --pr <n> <id...> Hide a finding on a PR (see \`ecr dismiss --help\`).
|
|
14
15
|
ecr undismiss --pr <n> <id...> Restore a dismissed finding.
|
|
15
16
|
ecr init [--monorepo] [--scope <dir>] Scaffold .expo-code-review/ in this repo.
|
|
17
|
+
ecr setup-auth [--yes] Walk through getting model credentials for local runs.
|
|
16
18
|
ecr doctor [--list-scopes] Check environment, config, credentials, and scopes.
|
|
17
19
|
ecr verify-config [--expected <env>] [--json] Refuse to run if a config could redirect the credential (CI guard).
|
|
18
20
|
|
|
@@ -47,6 +49,9 @@ async function main() {
|
|
|
47
49
|
case "init":
|
|
48
50
|
await initCommand(rest);
|
|
49
51
|
break;
|
|
52
|
+
case "setup-auth":
|
|
53
|
+
await setupAuthCommand(rest);
|
|
54
|
+
break;
|
|
50
55
|
case "doctor":
|
|
51
56
|
await doctorCommand(rest);
|
|
52
57
|
break;
|
package/build/commands/doctor.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { loadReviewConfig, loadScopeConfig, loadAuthFromRoot, hasConfig, resolveConfigDir, tokenEnvMismatch, } from "../config/load.js";
|
|
2
2
|
import { loadRoutingManifest, resolveScopes, scopePassesBudgetMs, formatOwnerTable, } from "../config/routing.js";
|
|
3
|
+
import readline from "node:readline/promises";
|
|
4
|
+
import { setupAuthCommand } from "./setup-auth.js";
|
|
3
5
|
import { checkProviderAuth } from "../core/auth.js";
|
|
4
6
|
import { opencodeBinSource } from "../core/opencode.js";
|
|
5
7
|
import { git, onPath, repoRoot, run } from "../core/exec.js";
|
|
@@ -126,6 +128,29 @@ export async function doctorCommand(argv = []) {
|
|
|
126
128
|
if (readiness.warning) {
|
|
127
129
|
warn(`auth: ${readiness.warning}`);
|
|
128
130
|
}
|
|
131
|
+
// A missing credential has a guided fix — offer it right here when someone is
|
|
132
|
+
// at the terminal, rather than making them find the command in the README.
|
|
133
|
+
if (!readiness.ok) {
|
|
134
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
135
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
136
|
+
let runIt = false;
|
|
137
|
+
try {
|
|
138
|
+
const answer = (await rl.question(" Run `ecr setup-auth` to fix this now? [Y/n] "))
|
|
139
|
+
.trim()
|
|
140
|
+
.toLowerCase();
|
|
141
|
+
runIt = answer === "" || answer === "y" || answer === "yes";
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
rl.close();
|
|
145
|
+
}
|
|
146
|
+
if (runIt) {
|
|
147
|
+
await setupAuthCommand([]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
info("run `ecr setup-auth` for a guided credential setup");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
129
154
|
}
|
|
130
155
|
catch (error) {
|
|
131
156
|
line(false, `config invalid: ${errorMessage(error)}`);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
6
|
+
import { hasConfig, loadReviewConfig } from "../config/load.js";
|
|
7
|
+
import { opencodeBinSource } from "../core/opencode.js";
|
|
8
|
+
import { errorMessage } from "../core/util.js";
|
|
9
|
+
const USAGE = `ecr setup-auth — set up model credentials for local runs
|
|
10
|
+
|
|
11
|
+
Reads this repo's .expo-code-review/config.jsonc auth entries and walks through
|
|
12
|
+
getting each credential:
|
|
13
|
+
• a ChatGPT/Codex subscription (oauth/openai): runs the bundled
|
|
14
|
+
\`opencode auth login\` (interactive; opens your browser), then prints the
|
|
15
|
+
\`export <tokenEnv>=…\` line to add to your shell config. An existing
|
|
16
|
+
OpenCode ChatGPT sign-in is reused instead of re-authenticating.
|
|
17
|
+
• an API key (api-key entries): prints where to create the key, the exact
|
|
18
|
+
permissions it needs, and the export line to fill in.
|
|
19
|
+
|
|
20
|
+
Without a repo config, it offers the recommended ChatGPT/Codex subscription flow
|
|
21
|
+
with the default env name.
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--yes Skip confirmation prompts (still interactive during the login itself).
|
|
25
|
+
`;
|
|
26
|
+
export function planFromAuth(auth) {
|
|
27
|
+
const plan = { manualKeys: [], unsupported: [] };
|
|
28
|
+
for (const entry of auth) {
|
|
29
|
+
if (entry.mode === "oauth" && entry.provider === "openai" && entry.tokenEnv) {
|
|
30
|
+
plan.chatgptLogin = { tokenEnv: entry.tokenEnv };
|
|
31
|
+
}
|
|
32
|
+
else if (entry.mode === "api-key" && entry.tokenEnv) {
|
|
33
|
+
plan.manualKeys.push({
|
|
34
|
+
provider: entry.provider,
|
|
35
|
+
tokenEnv: entry.tokenEnv,
|
|
36
|
+
upstream: entry.upstream,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (entry.mode === "oauth") {
|
|
40
|
+
plan.unsupported.push(entry);
|
|
41
|
+
}
|
|
42
|
+
// api-key without tokenEnv relies on OpenCode's own login — nothing to set up.
|
|
43
|
+
}
|
|
44
|
+
return plan;
|
|
45
|
+
}
|
|
46
|
+
/** Where OpenCode's own (non-isolated) auth.json lives. */
|
|
47
|
+
export function opencodeAuthJsonPath(env = process.env) {
|
|
48
|
+
const dataHome = env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
|
|
49
|
+
return path.join(dataHome, "opencode", "auth.json");
|
|
50
|
+
}
|
|
51
|
+
/** The stored ChatGPT sign-in's refresh token, if OpenCode has one. */
|
|
52
|
+
async function readStoredRefreshToken() {
|
|
53
|
+
try {
|
|
54
|
+
const raw = await readFile(opencodeAuthJsonPath(), "utf8");
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
const openai = parsed.openai;
|
|
57
|
+
return openai?.type === "oauth" && openai.refresh ? openai.refresh : null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function confirm(question, skip) {
|
|
64
|
+
if (skip) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
68
|
+
try {
|
|
69
|
+
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
70
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
rl.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** The line to paste into a shell config. Single-quoted: tokens never contain '. */
|
|
77
|
+
export function exportLine(tokenEnv, value) {
|
|
78
|
+
return `export ${tokenEnv}='${value}'`;
|
|
79
|
+
}
|
|
80
|
+
export async function setupAuthCommand(argv = []) {
|
|
81
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
82
|
+
process.stdout.write(USAGE);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const yes = argv.includes("--yes");
|
|
86
|
+
const out = (line = "") => process.stdout.write(`${line}\n`);
|
|
87
|
+
const err = (line = "") => process.stderr.write(`${line}\n`);
|
|
88
|
+
try {
|
|
89
|
+
// Plan from the repo config when there is one; otherwise offer the
|
|
90
|
+
// recommended subscription flow with the default env name.
|
|
91
|
+
let plan;
|
|
92
|
+
if (hasConfig(process.cwd())) {
|
|
93
|
+
const config = await loadReviewConfig(process.cwd());
|
|
94
|
+
plan = planFromAuth(config.auth);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
err("No .expo-code-review config here — setting up the default ChatGPT/Codex flow.");
|
|
98
|
+
plan = planFromAuth([
|
|
99
|
+
{ provider: "openai", mode: "oauth", tokenEnv: "CODEX_OAUTH_REFRESH_TOKEN" },
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
if (!plan.chatgptLogin && plan.manualKeys.length === 0 && plan.unsupported.length === 0) {
|
|
103
|
+
out("This repo's auth config needs no local credential setup (OpenCode's own login covers it).");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const exports = [];
|
|
107
|
+
if (plan.chatgptLogin) {
|
|
108
|
+
const { tokenEnv } = plan.chatgptLogin;
|
|
109
|
+
if (process.env[tokenEnv]) {
|
|
110
|
+
err(`✓ ${tokenEnv} is already set in this shell — skipping the ChatGPT sign-in.`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
let refresh = await readStoredRefreshToken();
|
|
114
|
+
if (refresh) {
|
|
115
|
+
err("Found an existing ChatGPT sign-in in OpenCode.");
|
|
116
|
+
if (!(await confirm(`Reuse it for ${tokenEnv}?`, yes))) {
|
|
117
|
+
refresh = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!refresh) {
|
|
121
|
+
err("This will run the bundled `opencode auth login` (interactive).");
|
|
122
|
+
err("When it prompts:");
|
|
123
|
+
err(" 1. select the provider: OpenAI");
|
|
124
|
+
err(" 2. select the method: Sign in with ChatGPT (Codex subscription)");
|
|
125
|
+
err(" 3. your browser opens — sign in and authorize.");
|
|
126
|
+
if (!(await confirm("Run it now?", yes))) {
|
|
127
|
+
err("Skipped the ChatGPT sign-in.");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const binDir = opencodeBinSource().dir;
|
|
131
|
+
const opencode = binDir ? path.join(binDir, "opencode") : "opencode";
|
|
132
|
+
const result = spawnSync(opencode, ["auth", "login"], { stdio: "inherit" });
|
|
133
|
+
if (result.status !== 0) {
|
|
134
|
+
throw new Error(`\`opencode auth login\` exited with ${result.status ?? "a signal"}; nothing was changed.`);
|
|
135
|
+
}
|
|
136
|
+
refresh = await readStoredRefreshToken();
|
|
137
|
+
if (!refresh) {
|
|
138
|
+
throw new Error("The login finished but no ChatGPT sign-in was stored — did you select " +
|
|
139
|
+
'OpenAI → "Sign in with ChatGPT"? Re-run `ecr setup-auth` to try again.');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (refresh) {
|
|
144
|
+
// The REFRESH token is the durable secret: access tokens are short-lived,
|
|
145
|
+
// and OpenCode mints them from this on demand.
|
|
146
|
+
exports.push(exportLine(tokenEnv, refresh));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const key of plan.manualKeys) {
|
|
151
|
+
if (process.env[key.tokenEnv]) {
|
|
152
|
+
err(`✓ ${key.tokenEnv} is already set in this shell — skipping.`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const upstream = key.upstream ?? key.provider;
|
|
156
|
+
err("");
|
|
157
|
+
err(`${key.tokenEnv} (${key.provider}) is an API key — create it by hand:`);
|
|
158
|
+
if (upstream === "openai") {
|
|
159
|
+
err(" https://platform.openai.com/api-keys — in a dedicated project (set a");
|
|
160
|
+
err(" monthly budget), as a RESTRICTED key with exactly two permissions, both");
|
|
161
|
+
err(" under Model capabilities: Responses → Request, Chat completions → Request.");
|
|
162
|
+
err(" Everything else (including List models) stays None.");
|
|
163
|
+
}
|
|
164
|
+
else if (upstream === "anthropic") {
|
|
165
|
+
err(" https://console.anthropic.com/settings/keys — a workspace-scoped key");
|
|
166
|
+
err(" with a spend limit is all the reviewer needs.");
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
err(` mint a key for the "${upstream}" provider.`);
|
|
170
|
+
}
|
|
171
|
+
exports.push(exportLine(key.tokenEnv, "<paste the key here>"));
|
|
172
|
+
}
|
|
173
|
+
for (const entry of plan.unsupported) {
|
|
174
|
+
err("");
|
|
175
|
+
err(`auth for "${entry.provider}" is mode "oauth", which has no automated setup flow here` +
|
|
176
|
+
(entry.provider === "anthropic"
|
|
177
|
+
? " — and cannot work: Anthropic prohibits subscription tokens in third-party tools. Use an API key instead."
|
|
178
|
+
: `. Set ${entry.tokenEnv ?? "its token env"} manually.`));
|
|
179
|
+
}
|
|
180
|
+
if (exports.length > 0) {
|
|
181
|
+
const rc = process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "your shell config";
|
|
182
|
+
err("");
|
|
183
|
+
err(`Add ${exports.length === 1 ? "this line" : "these lines"} to ${rc}:`);
|
|
184
|
+
out("");
|
|
185
|
+
for (const line of exports) {
|
|
186
|
+
out(` ${line}`);
|
|
187
|
+
}
|
|
188
|
+
out("");
|
|
189
|
+
err(`Then restart your shell (or \`source ${rc}\`) and run \`ecr doctor\` to verify.`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
err("");
|
|
193
|
+
err("Nothing to add — run `ecr doctor` to verify your setup.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
err(`setup-auth failed: ${errorMessage(error)}`);
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/code-review-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
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": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"bin": {
|
|
12
12
|
"ecr": "build/cli.js",
|
|
13
|
-
"expo-code-review": "build/cli.js"
|
|
13
|
+
"expo-code-review": "build/cli.js",
|
|
14
|
+
"code-review-cli": "build/cli.js"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"build",
|