Skip to content

Commit 964b820

Browse files
Add AI Review preflight checks and bump Node.js to >=18
- Add preflight module that detects standalone `agent` CLI or falls back to `cursor agent`, and enforces Node >=18 - Update AI Review setup and orchestrator to use preflight validation - Use process.execPath for MCP server to ensure Node >=18 - Add unit tests for preflight logic with dependency injection - Update README with AI Review setup instructions Made-with: Cursor
1 parent 274cc03 commit 964b820

11 files changed

Lines changed: 419 additions & 64 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ You can also set `gerrit.auth.password` and `gerrit.auth.cookie` in settings; th
1818

1919
Additionally the extension requires the python package [git-review](https://pypi.org/project/git-review/) to be installed.
2020

21+
### AI Review Setup
22+
23+
The AI Review feature requires:
24+
25+
- **Node.js >= 18** — the extension host and Gerrit MCP server both need Node 18+.
26+
- **Cursor Agent CLI** (`agent`) — install it by running:
27+
28+
```bash
29+
curl https://cursor.com/install -fsS | bash
30+
```
31+
32+
Verify with `agent --version`. See the [Cursor CLI docs](https://cursor.com/docs/cli/installation) for details.
33+
34+
Once installed, run **Gerrit: Enable AI Review** from the command palette. This will:
35+
36+
1. Let you pick an AI model and checkout behavior.
37+
2. Write a `.cursor/mcp.json` with the `gerrit-review` MCP server configuration.
38+
3. Auto-enable the MCP server via `agent mcp enable gerrit-review`.
39+
40+
After setup, use **Gerrit: AI Review Change** (or the lightbulb icon in the Change Explorer) to run an AI-powered review on any Gerrit change.
41+
2142
## Features
2243

2344
### Changes panel

package-lock.json

Lines changed: 15 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"version": "1.2.58",
66
"engines": {
77
"vscode": "^1.76.0",
8-
"node": ">=14.16.0"
8+
"node": ">=18.0.0"
99
},
1010
"license": "MIT",
1111
"categories": [
@@ -1570,6 +1570,7 @@
15701570
"package": "npm run prepack && npm exec vsce package --no-yarn",
15711571
"package-dev": "npm run prepush && npm run build:debug && npm exec vsce package --no-yarn",
15721572
"generate-package": "generate-package-json generate --name Gerrit --input src/commands/defs.ts -w -p package.json --handler src/commands/commands.ts --validate --prefix Gerrit && prettier --write package.json",
1573+
"test": "TS_NODE_PROJECT=tests/tsconfig.json mocha --require ts-node/register 'tests/**/*.test.ts' --timeout 10000",
15731574
"prepush": "npm run generate-package && npm run format-staged && npm run lint -- --fix && npm run compile"
15741575
},
15751576
"devDependencies": {
@@ -1579,7 +1580,7 @@
15791580
"@types/fs-extra": "^9.0.13",
15801581
"@types/glob": "^7.1.4",
15811582
"@types/mocha": "^9.0.0",
1582-
"@types/node": "14.x",
1583+
"@types/node": "18.x",
15831584
"@types/react": "^17.0.37",
15841585
"@types/react-dom": "^17.0.11",
15851586
"@types/vscode": "1.76",

src/lib/ai-review/enableAiReview.ts

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { getGerritURLFromReviewFile } from '../credentials/enterCredentials';
22
import { getGitReviewFileCached } from '../credentials/gitReviewFile';
3-
import { writeMcpConfig, GerritCredentials } from '../mcp/mcpManager';
4-
import { window, workspace, ExtensionContext } from 'vscode';
53
import { GerritSecrets } from '../credentials/secrets';
6-
import { getConfiguration } from '../vscode/config';
7-
import { getGerritRepo } from '../gerrit/gerrit';
8-
import { selectAiModel } from './modelSelector';
94
import { tryExecAsync } from '../git/gitCLI';
5+
import { getGerritRepo } from '../gerrit/gerrit';
6+
import { writeMcpConfig, GerritCredentials } from '../mcp/mcpManager';
107
import { log } from '../util/log';
8+
import { getConfiguration } from '../vscode/config';
9+
import {
10+
runPreflight,
11+
buildMcpEnableCommand,
12+
AgentCommand,
13+
} from './preflight';
14+
import { selectAiModel } from './modelSelector';
15+
import { window, workspace, ExtensionContext } from 'vscode';
1116

1217
type CheckoutBehavior = 'ask' | 'always' | 'never';
1318

@@ -16,8 +21,12 @@ export async function enableAiReview(
1621
): Promise<void> {
1722
const config = getConfiguration();
1823

19-
const cursorOk = await verifyCursorCli();
20-
if (!cursorOk) {
24+
const preflight = await runPreflight();
25+
if (!preflight.ok || !preflight.agent) {
26+
void window.showErrorMessage(
27+
preflight.error
28+
?? 'AI Review prerequisites not met.'
29+
);
2130
return;
2231
}
2332

@@ -65,7 +74,7 @@ export async function enableAiReview(
6574
+ 'may not have full Gerrit integration.'
6675
);
6776
} else {
68-
await enableMcpServer();
77+
await enableMcpServer(preflight.agent);
6978
}
7079

7180
await config.update(
@@ -80,31 +89,6 @@ export async function enableAiReview(
8089
log('AI Review enabled successfully');
8190
}
8291

83-
async function verifyCursorCli(): Promise<boolean> {
84-
const { success, stdout } = await tryExecAsync(
85-
'which cursor',
86-
{ silent: true }
87-
);
88-
89-
if (!success || !stdout.trim()) {
90-
const action = await window.showErrorMessage(
91-
'Cursor CLI not found. Please ensure '
92-
+ 'the "cursor" command is available in '
93-
+ 'your PATH. You can install it from '
94-
+ 'Cursor settings (Command Palette > '
95-
+ '"Install \'cursor\' command").',
96-
'Retry',
97-
'Cancel'
98-
);
99-
if (action === 'Retry') {
100-
return verifyCursorCli();
101-
}
102-
return false;
103-
}
104-
105-
log('Cursor CLI found at: ' + stdout.trim());
106-
return true;
107-
}
10892

10993
async function pickCheckoutBehavior(): Promise<
11094
CheckoutBehavior | undefined
@@ -193,12 +177,17 @@ async function extractCredentials(
193177
};
194178
}
195179

196-
async function enableMcpServer(): Promise<void> {
180+
async function enableMcpServer(
181+
agent: AgentCommand
182+
): Promise<void> {
197183
const cwd =
198184
workspace.workspaceFolders?.[0]?.uri.fsPath;
199185

186+
const cmd = buildMcpEnableCommand(
187+
agent, 'gerrit-review'
188+
);
200189
const { success, stderr } = await tryExecAsync(
201-
'cursor agent mcp enable gerrit-review',
190+
cmd,
202191
{ silent: true, cwd }
203192
);
204193

src/lib/ai-review/preflight.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { exec } from 'child_process';
2+
3+
const MIN_NODE_MAJOR = 18;
4+
const INSTALL_URL =
5+
'https://cursor.com/docs/cli/installation';
6+
7+
export interface AgentCommand {
8+
cmd: string;
9+
baseArgs: string[];
10+
}
11+
12+
export interface PreflightDeps {
13+
whichCmd: (
14+
name: string
15+
) => Promise<boolean>;
16+
getNodeMajor: () => number;
17+
}
18+
19+
function defaultWhich(
20+
name: string
21+
): Promise<boolean> {
22+
return new Promise((resolve) => {
23+
exec(
24+
`which ${name}`,
25+
(err, stdout) => {
26+
resolve(!err && !!stdout.trim());
27+
}
28+
);
29+
});
30+
}
31+
32+
const defaultDeps: PreflightDeps = {
33+
whichCmd: defaultWhich,
34+
getNodeMajor: (): number => {
35+
return parseInt(
36+
process.versions.node.split('.')[0],
37+
10
38+
);
39+
},
40+
};
41+
42+
export interface PreflightResult {
43+
ok: boolean;
44+
agent?: AgentCommand;
45+
error?: string;
46+
}
47+
48+
export async function runPreflight(
49+
deps: PreflightDeps = defaultDeps
50+
): Promise<PreflightResult> {
51+
const nodeMajor = deps.getNodeMajor();
52+
if (nodeMajor < MIN_NODE_MAJOR) {
53+
return {
54+
ok: false,
55+
error:
56+
`Node.js >= ${MIN_NODE_MAJOR} is required `
57+
+ 'for AI Review, but found '
58+
+ `v${nodeMajor}. Please upgrade Node.js.`,
59+
};
60+
}
61+
62+
const hasAgent = await deps.whichCmd('agent');
63+
if (hasAgent) {
64+
return {
65+
ok: true,
66+
agent: { cmd: 'agent', baseArgs: [] },
67+
};
68+
}
69+
70+
const hasCursor = await deps.whichCmd('cursor');
71+
if (hasCursor) {
72+
return {
73+
ok: true,
74+
agent: {
75+
cmd: 'cursor',
76+
baseArgs: ['agent'],
77+
},
78+
};
79+
}
80+
81+
return {
82+
ok: false,
83+
error:
84+
'Cursor Agent CLI not found. '
85+
+ 'Install it with: '
86+
+ 'curl https://cursor.com/install '
87+
+ '-fsS | bash '
88+
+ `(${INSTALL_URL})`,
89+
};
90+
}
91+
92+
export function buildMcpEnableCommand(
93+
agent: AgentCommand,
94+
serverName: string
95+
): string {
96+
const parts = [
97+
agent.cmd, ...agent.baseArgs,
98+
'mcp', 'enable', serverName,
99+
];
100+
return parts.join(' ');
101+
}
102+

0 commit comments

Comments
 (0)