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