review-mark 1.0.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.
@@ -0,0 +1,687 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/review.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/core/BeLinkReview.ts
7
+ import { spawn as spawn2 } from "child_process";
8
+ import { readFileSync, writeFileSync } from "fs";
9
+ import { join as join2 } from "path";
10
+
11
+ // src/utils/checkCli.ts
12
+ import { spawn, exec } from "child_process";
13
+ import { promisify } from "util";
14
+ import { existsSync } from "fs";
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+ var execAsync = promisify(exec);
18
+ var COMMON_AGENT_PATHS = [
19
+ join(homedir(), ".cursor", "agent"),
20
+ "/usr/local/bin/agent",
21
+ // macOS Homebrew 或手动安装的常见路径
22
+ "/opt/homebrew/bin/agent"
23
+ // macOS Apple Silicon Homebrew 路径
24
+ ];
25
+ async function findAgentExecutable(userAgentPath) {
26
+ if (userAgentPath && existsSync(userAgentPath)) {
27
+ return userAgentPath;
28
+ }
29
+ try {
30
+ const { stdout } = await execAsync("which agent");
31
+ const pathFromWhich = stdout.trim();
32
+ if (pathFromWhich && existsSync(pathFromWhich)) {
33
+ return pathFromWhich;
34
+ }
35
+ } catch (error) {
36
+ }
37
+ for (const path of COMMON_AGENT_PATHS) {
38
+ if (existsSync(path)) {
39
+ return path;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ async function isCheckCliInstall(options) {
45
+ const { silent = false, agentPath: userAgentPath } = options;
46
+ let actualAgentPath = await findAgentExecutable(userAgentPath);
47
+ if (actualAgentPath) {
48
+ if (!silent) {
49
+ console.log(
50
+ `[be-link-review] Cursor CLI (agent) \u5DF2\u5728 ${actualAgentPath} \u627E\u5230\u3002`
51
+ );
52
+ }
53
+ return { isInstalled: true, message: "Cursor CLI \u5DF2\u5B89\u88C5", actualAgentPath };
54
+ }
55
+ if (!silent) {
56
+ console.log("[be-link-review] Cursor CLI (agent) \u672A\u627E\u5230\uFF0C\u6B63\u5728\u5C1D\u8BD5\u5B89\u88C5...");
57
+ console.log(
58
+ "[be-link-review] \u6267\u884C\u5B89\u88C5\u547D\u4EE4: curl https://cursor.com/install -fsS | bash"
59
+ );
60
+ }
61
+ return new Promise((resolve, reject) => {
62
+ const installProcess = spawn(
63
+ "bash",
64
+ ["-c", "curl https://cursor.com/install -fsS | bash"],
65
+ {
66
+ stdio: "inherit"
67
+ // 将安装过程的输出直接显示给用户
68
+ }
69
+ );
70
+ installProcess.on("close", async (code) => {
71
+ if (code === 0) {
72
+ actualAgentPath = await findAgentExecutable(userAgentPath);
73
+ if (actualAgentPath) {
74
+ if (!silent) {
75
+ console.log("[be-link-review] Cursor CLI \u5B89\u88C5\u6210\u529F\u3002");
76
+ }
77
+ resolve({
78
+ isInstalled: true,
79
+ message: "Cursor CLI \u5B89\u88C5\u6210\u529F",
80
+ actualAgentPath
81
+ });
82
+ } else {
83
+ reject(
84
+ new Error(
85
+ `[be-link-review] Cursor CLI \u5B89\u88C5\u547D\u4EE4\u6267\u884C\u6210\u529F\uFF0C\u4F46\u672A\u627E\u5230 agent \u53EF\u6267\u884C\u6587\u4EF6\u3002\u8BF7\u624B\u52A8\u68C0\u67E5\u5B89\u88C5\uFF1Acurl https://cursor.com/install -fsS | bash\u3002`
86
+ )
87
+ );
88
+ }
89
+ } else {
90
+ reject(
91
+ new Error(
92
+ `[be-link-review] Cursor CLI \u5B89\u88C5\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${code}\u3002\u8BF7\u624B\u52A8\u5B89\u88C5\uFF1Acurl https://cursor.com/install -fsS | bash\u3002`
93
+ )
94
+ );
95
+ }
96
+ });
97
+ installProcess.on("error", (err) => {
98
+ reject(
99
+ new Error(
100
+ `[be-link-review] \u65E0\u6CD5\u542F\u52A8\u5B89\u88C5\u8FDB\u7A0B\uFF1A${err.message}\u3002\u8BF7\u624B\u52A8\u5B89\u88C5\uFF1Acurl https://cursor.com/install -fsS | bash`
101
+ )
102
+ );
103
+ });
104
+ });
105
+ }
106
+
107
+ // src/core/git.ts
108
+ import { exec as exec2 } from "child_process";
109
+ import { promisify as promisify2 } from "util";
110
+ var execAsync2 = promisify2(exec2);
111
+ async function getGitDiff(userIgnorePatterns = [], cwd = process.cwd()) {
112
+ try {
113
+ await execAsync2("git rev-parse --git-dir", { cwd });
114
+ } catch (error) {
115
+ console.error(`[be-link-review] \u5F53\u524D\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93: ${cwd}`);
116
+ return "";
117
+ }
118
+ const allIgnorePatterns = [.../* @__PURE__ */ new Set([...userIgnorePatterns])];
119
+ const excludeArgs = allIgnorePatterns.map((pattern) => `:(exclude)${pattern}`).join(" ");
120
+ let diff = "";
121
+ const baseCommand = `git diff --no-color --relative ${excludeArgs}`;
122
+ try {
123
+ const { stdout: cachedDiff } = await execAsync2(`${baseCommand} --cached`, {
124
+ cwd
125
+ });
126
+ diff = cachedDiff.trim();
127
+ if (diff) {
128
+ console.log("[be-link-review] \u68C0\u6D4B\u5230\u6682\u5B58\u533A\u6539\u52A8");
129
+ }
130
+ } catch (error) {
131
+ console.warn(`[be-link-review] \u83B7\u53D6\u6682\u5B58\u533A diff \u5931\u8D25: ${error.message}`);
132
+ }
133
+ if (!diff) {
134
+ try {
135
+ const { stdout: headDiff } = await execAsync2(`${baseCommand} HEAD`, {
136
+ cwd
137
+ });
138
+ diff = headDiff.trim();
139
+ if (diff) {
140
+ console.log("[be-link-review] \u68C0\u6D4B\u5230\u5DE5\u4F5C\u533A\u6539\u52A8\uFF08\u76F8\u5BF9\u4E8E HEAD\uFF09");
141
+ }
142
+ } catch (error) {
143
+ console.warn(`[be-link-review] \u83B7\u53D6\u5DE5\u4F5C\u533A diff \u5931\u8D25: ${error.message}`);
144
+ }
145
+ }
146
+ if (!diff) {
147
+ console.log("[be-link-review] \u672A\u68C0\u6D4B\u5230\u4EE3\u7801\u6539\u52A8\uFF08\u5DF2\u68C0\u67E5\u6682\u5B58\u533A\u548C\u5DE5\u4F5C\u533A\uFF09");
148
+ }
149
+ return diff;
150
+ }
151
+
152
+ // src/core/prompt.ts
153
+ function generateAIPrompt(diff) {
154
+ return `\u4F60\u662F\u4E00\u540D\u8D44\u6DF1\u8F6F\u4EF6\u5DE5\u7A0B\u5E08\uFF0C\u8BF7 review \u4EE5\u4E0B\u4EE3\u7801\u53D8\u66F4\u3002
155
+ Git diff:
156
+ ${diff}
157
+ \u8BF7\u5206\u6790\uFF1A
158
+ \u662F\u5426\u5B58\u5728 bug
159
+ \u662F\u5426\u5B58\u5728\u6F5C\u5728\u903B\u8F91\u95EE\u9898
160
+ \u662F\u5426\u5B58\u5728\u6027\u80FD\u95EE\u9898
161
+ \u662F\u5426\u5B58\u5728\u4EE3\u7801\u98CE\u683C\u95EE\u9898
162
+ \u7ED9\u51FA\u4F18\u5316\u5EFA\u8BAE`;
163
+ }
164
+
165
+ // src/core/feishu.ts
166
+ import * as lark from "@larksuiteoapi/node-sdk";
167
+
168
+ // src/constants.ts
169
+ var appId = "cli_a93822da7238dbb5";
170
+ var appSecret = "ZQdcpLUHFb4gFa8cGfrlJfVfSSyGtyzF";
171
+ var receiveId = "oc_482b6a04f95f4206c4fa9bc61829fd17";
172
+ var receiveIdType = "chat_id";
173
+ var messageType = "post";
174
+ var messageTitle = "\u{1F50D} Code Review \u7ED3\u679C";
175
+
176
+ // src/core/feishu.ts
177
+ async function sendReviewToFeishu(reviewContent) {
178
+ console.log("[be-link-review] \u6B63\u5728\u53D1\u9001\u6D88\u606F\u5230\u98DE\u4E66...");
179
+ console.log(`[be-link-review] \u6D88\u606F\u7C7B\u578B: ${messageType}`);
180
+ try {
181
+ const client = new lark.Client({
182
+ appId,
183
+ appSecret,
184
+ domain: lark.Domain.Feishu
185
+ });
186
+ let msgContent;
187
+ let msgType;
188
+ if (messageType === "interactive") {
189
+ msgContent = JSON.stringify({
190
+ config: {
191
+ wide_screen_mode: true
192
+ },
193
+ header: {
194
+ title: {
195
+ tag: "plain_text",
196
+ content: messageTitle
197
+ },
198
+ template: "blue"
199
+ },
200
+ elements: [
201
+ {
202
+ tag: "div",
203
+ text: {
204
+ tag: "lark_md",
205
+ content: reviewContent
206
+ }
207
+ },
208
+ {
209
+ tag: "hr"
210
+ },
211
+ {
212
+ tag: "note",
213
+ elements: [
214
+ {
215
+ tag: "plain_text",
216
+ content: `\u751F\u6210\u65F6\u95F4: ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN", {
217
+ timeZone: "Asia/Shanghai"
218
+ })}`
219
+ }
220
+ ]
221
+ }
222
+ ]
223
+ });
224
+ msgType = "interactive";
225
+ } else if (messageType === "post") {
226
+ msgContent = JSON.stringify({
227
+ zh_cn: {
228
+ title: messageTitle,
229
+ content: convertMarkdownToFeishuPost(reviewContent)
230
+ }
231
+ });
232
+ msgType = "post";
233
+ } else if (messageType === "text") {
234
+ msgContent = JSON.stringify({
235
+ text: `${messageTitle}
236
+
237
+ ${reviewContent}`
238
+ });
239
+ msgType = "text";
240
+ } else {
241
+ throw new Error(`[be-link-review] \u4E0D\u652F\u6301\u7684\u6D88\u606F\u7C7B\u578B: ${messageType}`);
242
+ }
243
+ console.log(`[be-link-review] \u53D1\u9001\u53C2\u6570:`);
244
+ console.log(` - receive_id_type: ${receiveIdType}`);
245
+ console.log(` - receive_id: ${receiveId}`);
246
+ console.log(` - msg_type: ${msgType}`);
247
+ const response = await client.im.message.create({
248
+ params: {
249
+ receive_id_type: receiveIdType
250
+ },
251
+ data: {
252
+ receive_id: receiveId,
253
+ msg_type: msgType,
254
+ content: msgContent
255
+ }
256
+ });
257
+ if (response.code !== 0) {
258
+ throw new Error(
259
+ `[be-link-review] \u98DE\u4E66 API \u8FD4\u56DE\u9519\u8BEF: ${response.msg || "\u672A\u77E5\u9519\u8BEF"}`
260
+ );
261
+ }
262
+ console.log("[be-link-review] \u2705 \u98DE\u4E66\u6D88\u606F\u53D1\u9001\u6210\u529F");
263
+ console.log(`[be-link-review] \u6D88\u606F ID: ${response.data?.message_id || "\u672A\u77E5"}`);
264
+ } catch (error) {
265
+ console.error(`[be-link-review] \u274C \u98DE\u4E66\u6D88\u606F\u53D1\u9001\u5931\u8D25: ${error.message}`);
266
+ throw error;
267
+ }
268
+ }
269
+ function convertMarkdownToFeishuPost(markdown) {
270
+ const lines = markdown.split("\n");
271
+ const result = [];
272
+ let inCodeBlock = false;
273
+ let codeBlockContent = [];
274
+ for (const line of lines) {
275
+ if (line.startsWith("```")) {
276
+ if (inCodeBlock) {
277
+ if (codeBlockContent.length > 0) {
278
+ result.push([
279
+ {
280
+ tag: "text",
281
+ text: codeBlockContent.join("\n"),
282
+ style: ["code"]
283
+ }
284
+ ]);
285
+ codeBlockContent = [];
286
+ }
287
+ inCodeBlock = false;
288
+ } else {
289
+ inCodeBlock = true;
290
+ }
291
+ continue;
292
+ }
293
+ if (inCodeBlock) {
294
+ codeBlockContent.push(line);
295
+ continue;
296
+ }
297
+ if (!line.trim()) {
298
+ result.push([{ tag: "text", text: "" }]);
299
+ continue;
300
+ }
301
+ if (line.startsWith("#")) {
302
+ const level = line.match(/^#+/)?.[0].length || 1;
303
+ const text = line.replace(/^#+\s*/, "");
304
+ result.push([
305
+ {
306
+ tag: "text",
307
+ text,
308
+ style: level <= 2 ? ["bold", "underline"] : ["bold"]
309
+ }
310
+ ]);
311
+ continue;
312
+ }
313
+ const parsedLine = parseLineStyles(line);
314
+ result.push(parsedLine);
315
+ }
316
+ if (codeBlockContent.length > 0) {
317
+ result.push([
318
+ {
319
+ tag: "text",
320
+ text: codeBlockContent.join("\n"),
321
+ style: ["code"]
322
+ }
323
+ ]);
324
+ }
325
+ return result;
326
+ }
327
+ function parseLineStyles(line) {
328
+ const elements = [];
329
+ let currentText = "";
330
+ let i = 0;
331
+ while (i < line.length) {
332
+ if (line[i] === "*" && line[i + 1] === "*") {
333
+ if (currentText) {
334
+ elements.push({ tag: "text", text: currentText });
335
+ currentText = "";
336
+ }
337
+ const endIndex = line.indexOf("**", i + 2);
338
+ if (endIndex !== -1) {
339
+ const boldText = line.substring(i + 2, endIndex);
340
+ elements.push({ tag: "text", text: boldText, style: ["bold"] });
341
+ i = endIndex + 2;
342
+ continue;
343
+ }
344
+ }
345
+ if (line[i] === "`" && line[i + 1] !== "`") {
346
+ if (currentText) {
347
+ elements.push({ tag: "text", text: currentText });
348
+ currentText = "";
349
+ }
350
+ const endIndex = line.indexOf("`", i + 1);
351
+ if (endIndex !== -1) {
352
+ const codeText = line.substring(i + 1, endIndex);
353
+ elements.push({
354
+ tag: "text",
355
+ text: codeText,
356
+ style: ["code"]
357
+ });
358
+ i = endIndex + 1;
359
+ continue;
360
+ }
361
+ }
362
+ if (line[i] === "[") {
363
+ const textEnd = line.indexOf("](", i);
364
+ const urlEnd = line.indexOf(")", textEnd + 2);
365
+ if (textEnd !== -1 && urlEnd !== -1) {
366
+ if (currentText) {
367
+ elements.push({ tag: "text", text: currentText });
368
+ currentText = "";
369
+ }
370
+ const linkText = line.substring(i + 1, textEnd);
371
+ const url = line.substring(textEnd + 2, urlEnd);
372
+ elements.push({
373
+ tag: "a",
374
+ text: linkText,
375
+ href: url
376
+ });
377
+ i = urlEnd + 1;
378
+ continue;
379
+ }
380
+ }
381
+ currentText += line[i];
382
+ i++;
383
+ }
384
+ if (currentText) {
385
+ elements.push({ tag: "text", text: currentText });
386
+ }
387
+ return elements.length > 0 ? elements : [{ tag: "text", text: line }];
388
+ }
389
+
390
+ // src/core/BeLinkReview.ts
391
+ var BeLinkReview = class _BeLinkReview {
392
+ static #instance = null;
393
+ #apiKey;
394
+ #agentPath;
395
+ #ignorePatterns;
396
+ #enableFeishu;
397
+ constructor(apiKey, agentPath, ignorePatterns, enableFeishu = true) {
398
+ this.#apiKey = apiKey;
399
+ this.#agentPath = agentPath;
400
+ this.#ignorePatterns = ignorePatterns;
401
+ this.#enableFeishu = enableFeishu;
402
+ }
403
+ /**
404
+ * 初始化单例并注入参数,在项目入口调用一次即可。
405
+ * 负责保存 apiKey 和自动写入 package.json 脚本。
406
+ * 此方法主要用于在用户项目中设置 apiKey 和脚本,CLI 运行时会优先从环境变量或命令行参数获取。
407
+ */
408
+ static init(options = {}) {
409
+ if (_BeLinkReview.#instance === null) {
410
+ _BeLinkReview.#instance = new _BeLinkReview(
411
+ options.apiKey,
412
+ options.agentPath,
413
+ options.ignore,
414
+ options.enableFeishu ?? true
415
+ );
416
+ } else {
417
+ _BeLinkReview.#instance.#apiKey = options.apiKey;
418
+ _BeLinkReview.#instance.#agentPath = options.agentPath;
419
+ _BeLinkReview.#instance.#ignorePatterns = options.ignore;
420
+ _BeLinkReview.#instance.#enableFeishu = options.enableFeishu ?? true;
421
+ }
422
+ _BeLinkReview.#instance.#setupProjectScript();
423
+ return _BeLinkReview.#instance;
424
+ }
425
+ /**
426
+ * 获取单例实例。如果未通过 init() 初始化,则尝试从环境变量 CURSOR_API_KEY 获取。
427
+ * @param cliApiKey 可选的命令行传入的 apiKey
428
+ * @param cliAgentPath 可选的命令行传入的 agentPath
429
+ * @param cliIgnorePatterns 可选的命令行传入的 ignorePatterns
430
+ * @param cliEnableFeishu 可选的命令行传入的飞书开关
431
+ */
432
+ static getInstance(cliApiKey, cliAgentPath, cliIgnorePatterns, cliEnableFeishu) {
433
+ if (_BeLinkReview.#instance === null) {
434
+ const apiKey = cliApiKey || process.env.CURSOR_API_KEY;
435
+ const agentPath = cliAgentPath || process.env.CURSOR_AGENT_PATH;
436
+ const ignorePatterns = cliIgnorePatterns || (process.env.BE_LINK_REVIEW_IGNORE ? process.env.BE_LINK_REVIEW_IGNORE.split(",") : void 0);
437
+ const enableFeishu = cliEnableFeishu ?? process.env.FEISHU_ENABLED !== "false";
438
+ if (!apiKey) {
439
+ throw new Error(
440
+ '[be-link-review] \u8BF7\u5148\u8C03\u7528 BeLinkReview.init({ apiKey: "..." }) \u521D\u59CB\u5316\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF CURSOR_API_KEY\uFF0C\u6216\u901A\u8FC7\u547D\u4EE4\u884C\u53C2\u6570 --apiKey \u4F20\u5165\u3002'
441
+ );
442
+ }
443
+ _BeLinkReview.#instance = new _BeLinkReview(
444
+ apiKey,
445
+ agentPath,
446
+ ignorePatterns,
447
+ enableFeishu
448
+ );
449
+ } else {
450
+ if (cliApiKey && !_BeLinkReview.#instance.#apiKey) {
451
+ _BeLinkReview.#instance.#apiKey = cliApiKey;
452
+ }
453
+ if (cliAgentPath && !_BeLinkReview.#instance.#agentPath) {
454
+ _BeLinkReview.#instance.#agentPath = cliAgentPath;
455
+ }
456
+ if (cliIgnorePatterns && !_BeLinkReview.#instance.#ignorePatterns) {
457
+ _BeLinkReview.#instance.#ignorePatterns = cliIgnorePatterns;
458
+ }
459
+ if (cliEnableFeishu !== void 0) {
460
+ _BeLinkReview.#instance.#enableFeishu = cliEnableFeishu;
461
+ }
462
+ }
463
+ return _BeLinkReview.#instance;
464
+ }
465
+ #getApiKey() {
466
+ const apiKey = this.#apiKey || process.env.CURSOR_API_KEY;
467
+ if (!apiKey) {
468
+ throw new Error(
469
+ '[be-link-review] \u8BF7\u5148\u5728 init({ apiKey: "..." }) \u4E2D\u4F20\u5165 apiKey\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF CURSOR_API_KEY\uFF0C\u6216\u901A\u8FC7\u547D\u4EE4\u884C\u53C2\u6570 --apiKey \u4F20\u5165\u3002'
470
+ );
471
+ }
472
+ return apiKey;
473
+ }
474
+ #getAgentPath() {
475
+ return this.#agentPath || process.env.CURSOR_AGENT_PATH;
476
+ }
477
+ #getIgnorePatterns() {
478
+ const envIgnore = process.env.BE_LINK_REVIEW_IGNORE ? process.env.BE_LINK_REVIEW_IGNORE.split(",") : [];
479
+ return this.#ignorePatterns || envIgnore;
480
+ }
481
+ #isFeishuEnabled() {
482
+ return this.#enableFeishu;
483
+ }
484
+ /**
485
+ * 自动往用户项目 package.json 写入 script
486
+ */
487
+ async #setupProjectScript() {
488
+ try {
489
+ const packageJsonPath = join2(process.cwd(), "package.json");
490
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
491
+ if (!packageJson.scripts) {
492
+ packageJson.scripts = {};
493
+ }
494
+ if (!packageJson.scripts.review) {
495
+ packageJson.scripts.review = "belink-review";
496
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
497
+ console.log(
498
+ "[be-link-review] \u5DF2\u5728 package.json \u4E2D\u6DFB\u52A0 'review' \u811A\u672C\u3002"
499
+ );
500
+ } else {
501
+ console.log("[be-link-review] 'review' \u811A\u672C\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u6DFB\u52A0\u3002");
502
+ }
503
+ } catch (error) {
504
+ console.error("[be-link-review] \u65E0\u6CD5\u66F4\u65B0 package.json: ", error);
505
+ }
506
+ }
507
+ /**
508
+ * 执行一次完整 CLI 流程:获取 git diff → 生成 prompt → 检查/安装 CLI → 与 Cursor 对话并打印结果。
509
+ */
510
+ async goCli() {
511
+ const apiKey = this.#getApiKey();
512
+ const agentPath = this.#getAgentPath();
513
+ const ignorePatterns = this.#getIgnorePatterns();
514
+ const ensureResult = await this.ensureAgentInstalled(false, agentPath);
515
+ if (!ensureResult.isInstalled) {
516
+ throw new Error(
517
+ "[be-link-review] Cursor CLI \u672A\u5B89\u88C5\u4E14\u81EA\u52A8\u5B89\u88C5\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u5B89\u88C5\u3002"
518
+ );
519
+ }
520
+ console.log("[be-link-review] Getting git diff...");
521
+ const diff = await getGitDiff(ignorePatterns, process.cwd());
522
+ if (!diff) {
523
+ console.log("[be-link-review] No code changes detected");
524
+ return "No code changes detected";
525
+ }
526
+ const prompt = generateAIPrompt(diff);
527
+ console.log("[be-link-review] Sending to AI...");
528
+ const response = await this.chat(prompt, {
529
+ agentPath: ensureResult.actualAgentPath,
530
+ force: true
531
+ });
532
+ console.log("===== AI Review =====");
533
+ console.log(response);
534
+ if (this.#isFeishuEnabled()) {
535
+ try {
536
+ await sendReviewToFeishu(response);
537
+ } catch (error) {
538
+ console.error(
539
+ `[be-link-review] \u98DE\u4E66\u901A\u77E5\u53D1\u9001\u5931\u8D25\uFF0C\u4F46\u4E0D\u5F71\u54CD review \u7ED3\u679C: ${error.message}`
540
+ );
541
+ }
542
+ }
543
+ return response;
544
+ }
545
+ /**
546
+ * 检查 Cursor CLI 是否已安装;未安装则自动安装(需网络)。
547
+ * @param silent 为 true 时不打印「已安装」等提示
548
+ * @param agentPath 可选的 agent 可执行文件路径
549
+ */
550
+ async ensureAgentInstalled(silent = false, agentPath) {
551
+ return isCheckCliInstall({ apiKey: this.#getApiKey(), silent, agentPath });
552
+ }
553
+ /**
554
+ * 通过 Cursor Headless CLI 与 Cursor 对话(发送 prompt,拿到回复)。
555
+ * 默认会先执行 ensureAgentInstalled(检查/安装 CLI),再启动 agent 执行提问。
556
+ * 参考:https://cursor.com/cn/docs/cli/headless
557
+ */
558
+ async chat(prompt, options = {}) {
559
+ const actualAgentPath = options.agentPath || this.#getAgentPath() || "agent";
560
+ const args = ["--yolo", "-p", prompt];
561
+ if (options.outputFormat === "json") args.push("--output-format", "json");
562
+ return new Promise((resolve, reject) => {
563
+ const env = { ...process.env, CURSOR_API_KEY: this.#getApiKey() };
564
+ const proc = spawn2(actualAgentPath, args, {
565
+ env,
566
+ cwd: process.cwd(),
567
+ shell: false,
568
+ stdio: ["ignore", "pipe", "pipe"]
569
+ });
570
+ let stdout = "";
571
+ let stderr = "";
572
+ proc.stdout?.on("data", (chunk) => {
573
+ stdout += chunk.toString();
574
+ });
575
+ proc.stderr?.on("data", (chunk) => {
576
+ stderr += chunk.toString();
577
+ });
578
+ proc.on("error", (err) => {
579
+ reject(
580
+ new Error(
581
+ `[be-link-review] \u65E0\u6CD5\u542F\u52A8 Cursor CLI (${actualAgentPath})\uFF0C\u8BF7\u5148\u5B89\u88C5\uFF1Acurl https://cursor.com/install -fsS | bash\u3002\u539F\u59CB\u9519\u8BEF: ${err.message}`
582
+ )
583
+ );
584
+ });
585
+ proc.on("close", (code) => {
586
+ if (code !== 0) {
587
+ reject(
588
+ new Error(
589
+ `[be-link-review] agent \u9000\u51FA\u7801 ${code}${stderr ? `: ${stderr.trim()}` : ""}`
590
+ )
591
+ );
592
+ return;
593
+ }
594
+ resolve(stdout.trim());
595
+ });
596
+ });
597
+ }
598
+ };
599
+
600
+ // src/cli/review.ts
601
+ var program = new Command();
602
+ program.name("belink-review").description("AI-powered code review tool").version("1.0.0").option(
603
+ "--apiKey <key>",
604
+ "Cursor API Key (overrides CURSOR_API_KEY environment variable)"
605
+ ).option(
606
+ "--agentPath <path>",
607
+ "Path to the Cursor CLI agent executable (overrides CURSOR_AGENT_PATH environment variable)"
608
+ ).option(
609
+ "--ignore <patterns>",
610
+ "Comma-separated list of glob patterns to ignore (e.g., *.lock,dist/**)"
611
+ ).option(
612
+ "--feishu-app-id <appId>",
613
+ "Feishu App ID (overrides FEISHU_APP_ID environment variable)"
614
+ ).option(
615
+ "--feishu-app-secret <appSecret>",
616
+ "Feishu App Secret (overrides FEISHU_APP_SECRET environment variable)"
617
+ ).option(
618
+ "--feishu-receive-id <receiveId>",
619
+ "Feishu Receive ID - user open_id or chat_id (overrides FEISHU_RECEIVE_ID environment variable)"
620
+ ).option(
621
+ "--feishu-receive-id-type <type>",
622
+ "Feishu Receive ID Type: open_id, user_id, chat_id, email, union_id (default: chat_id)"
623
+ ).option(
624
+ "--feishu-type <type>",
625
+ "Feishu message type: text, post, or interactive (default: interactive)"
626
+ ).option(
627
+ "--feishu-title <title>",
628
+ "Feishu message title (default: \u{1F50D} Code Review \u7ED3\u679C)"
629
+ ).option("--no-feishu", "Disable Feishu notification").action(async (options) => {
630
+ try {
631
+ const ignorePatterns = options.ignore ? options.ignore.split(",") : void 0;
632
+ const instance = BeLinkReview.getInstance(
633
+ options.apiKey,
634
+ options.agentPath,
635
+ ignorePatterns,
636
+ options.feishu !== false
637
+ );
638
+ await instance.goCli();
639
+ } catch (error) {
640
+ console.error(`[be-link-review] Error: ${error.message}`);
641
+ process.exit(1);
642
+ }
643
+ });
644
+ program.command("review").description("Perform an AI code review on git diff").option(
645
+ "--apiKey <key>",
646
+ "Cursor API Key (overrides CURSOR_API_KEY environment variable)"
647
+ ).option(
648
+ "--agentPath <path>",
649
+ "Path to the Cursor CLI agent executable (overrides CURSOR_AGENT_PATH environment variable)"
650
+ ).option(
651
+ "--ignore <patterns>",
652
+ "Comma-separated list of glob patterns to ignore (e.g., *.lock,dist/**)"
653
+ ).option(
654
+ "--feishu-app-id <appId>",
655
+ "Feishu App ID (overrides FEISHU_APP_ID environment variable)"
656
+ ).option(
657
+ "--feishu-app-secret <appSecret>",
658
+ "Feishu App Secret (overrides FEISHU_APP_SECRET environment variable)"
659
+ ).option(
660
+ "--feishu-receive-id <receiveId>",
661
+ "Feishu Receive ID - user open_id or chat_id (overrides FEISHU_RECEIVE_ID environment variable)"
662
+ ).option(
663
+ "--feishu-receive-id-type <type>",
664
+ "Feishu Receive ID Type: open_id, user_id, chat_id, email, union_id (default: chat_id)"
665
+ ).option(
666
+ "--feishu-type <type>",
667
+ "Feishu message type: text, post, or interactive (default: interactive)"
668
+ ).option(
669
+ "--feishu-title <title>",
670
+ "Feishu message title (default: \u{1F50D} Code Review \u7ED3\u679C)"
671
+ ).option("--no-feishu", "Disable Feishu notification").action(async (options) => {
672
+ try {
673
+ const ignorePatterns = options.ignore ? options.ignore.split(",") : void 0;
674
+ const instance = BeLinkReview.getInstance(
675
+ options.apiKey,
676
+ options.agentPath,
677
+ ignorePatterns,
678
+ options.feishu !== false
679
+ );
680
+ await instance.goCli();
681
+ } catch (error) {
682
+ console.error(`[be-link-review] Error: ${error.message}`);
683
+ process.exit(1);
684
+ }
685
+ });
686
+ program.parse(process.argv);
687
+ //# sourceMappingURL=review.js.map