qkpr 0.0.11
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/LICENSE.md +21 -0
- package/README.md +278 -0
- package/README.zh-CN.md +278 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1573 -0
- package/dist/pr-3u9dEVEc.mjs +3 -0
- package/package.json +82 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1573 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { bold, cyan, dim, green, magenta, red, yellow } from "kolorist";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import yargs from "yargs";
|
|
8
|
+
import { hideBin } from "yargs/helpers";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
import { isIP } from "is-ip";
|
|
11
|
+
import inquirer from "inquirer";
|
|
12
|
+
import inquirerAutoComplete from "inquirer-autocomplete-prompt";
|
|
13
|
+
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
14
|
+
import ora from "ora";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import searchCheckbox from "inquirer-search-checkbox";
|
|
17
|
+
|
|
18
|
+
//#region src/services/pr.ts
|
|
19
|
+
/**
|
|
20
|
+
* 获取当前 Git 仓库信息
|
|
21
|
+
*/
|
|
22
|
+
function getGitInfo() {
|
|
23
|
+
try {
|
|
24
|
+
return {
|
|
25
|
+
currentBranch: execSync("git symbolic-ref --quiet --short HEAD", {
|
|
26
|
+
encoding: "utf-8",
|
|
27
|
+
stdio: [
|
|
28
|
+
"pipe",
|
|
29
|
+
"pipe",
|
|
30
|
+
"ignore"
|
|
31
|
+
]
|
|
32
|
+
}).trim(),
|
|
33
|
+
remoteUrl: execSync("git config --get remote.origin.url", {
|
|
34
|
+
encoding: "utf-8",
|
|
35
|
+
stdio: [
|
|
36
|
+
"pipe",
|
|
37
|
+
"pipe",
|
|
38
|
+
"ignore"
|
|
39
|
+
]
|
|
40
|
+
}).trim(),
|
|
41
|
+
isGitRepo: true
|
|
42
|
+
};
|
|
43
|
+
} catch {
|
|
44
|
+
return {
|
|
45
|
+
currentBranch: "",
|
|
46
|
+
remoteUrl: "",
|
|
47
|
+
isGitRepo: false
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 获取所有分支列表
|
|
53
|
+
*/
|
|
54
|
+
function getAllBranches() {
|
|
55
|
+
try {
|
|
56
|
+
return execSync("git branch -a", {
|
|
57
|
+
encoding: "utf-8",
|
|
58
|
+
stdio: [
|
|
59
|
+
"pipe",
|
|
60
|
+
"pipe",
|
|
61
|
+
"ignore"
|
|
62
|
+
]
|
|
63
|
+
}).split("\n").map((b) => b.replace(/^\*?\s+/, "").replace(/^remotes\/origin\//, "")).filter((b) => b && b !== "HEAD" && !b.includes("->")).filter((b, index, self) => self.indexOf(b) === index).sort();
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 获取分支的最后提交时间
|
|
70
|
+
*/
|
|
71
|
+
function getBranchLastCommitTime(branchName) {
|
|
72
|
+
try {
|
|
73
|
+
const command = `git log -1 --format=%ct origin/${branchName} 2>/dev/null || git log -1 --format=%ct ${branchName}`;
|
|
74
|
+
const timestamp = Number.parseInt(execSync(command, {
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
stdio: [
|
|
77
|
+
"pipe",
|
|
78
|
+
"pipe",
|
|
79
|
+
"ignore"
|
|
80
|
+
]
|
|
81
|
+
}).trim(), 10);
|
|
82
|
+
const date = /* @__PURE__ */ new Date(timestamp * 1e3);
|
|
83
|
+
const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
|
|
84
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
85
|
+
let formatted;
|
|
86
|
+
if (diffDays === 0) {
|
|
87
|
+
const diffHours = Math.floor(diffMs / (1e3 * 60 * 60));
|
|
88
|
+
if (diffHours === 0) {
|
|
89
|
+
const diffMinutes = Math.floor(diffMs / (1e3 * 60));
|
|
90
|
+
formatted = diffMinutes <= 1 ? "just now" : `${diffMinutes}m ago`;
|
|
91
|
+
} else formatted = `${diffHours}h ago`;
|
|
92
|
+
} else if (diffDays === 1) formatted = "yesterday";
|
|
93
|
+
else if (diffDays < 7) formatted = `${diffDays}d ago`;
|
|
94
|
+
else if (diffDays < 30) formatted = `${Math.floor(diffDays / 7)}w ago`;
|
|
95
|
+
else if (diffDays < 365) formatted = `${Math.floor(diffDays / 30)}mo ago`;
|
|
96
|
+
else formatted = `${Math.floor(diffDays / 365)}y ago`;
|
|
97
|
+
return {
|
|
98
|
+
timestamp,
|
|
99
|
+
formatted
|
|
100
|
+
};
|
|
101
|
+
} catch {
|
|
102
|
+
return {
|
|
103
|
+
timestamp: 0,
|
|
104
|
+
formatted: "unknown"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 获取分支类别
|
|
110
|
+
*/
|
|
111
|
+
function getBranchCategory(branchName) {
|
|
112
|
+
const match = branchName.match(/^([^/]+)\//);
|
|
113
|
+
return match ? match[1] : "other";
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 获取分支详细信息(包含时间和分类)
|
|
117
|
+
*/
|
|
118
|
+
function getBranchesWithInfo(branches) {
|
|
119
|
+
return branches.map((branchName) => {
|
|
120
|
+
const { timestamp, formatted } = getBranchLastCommitTime(branchName);
|
|
121
|
+
return {
|
|
122
|
+
name: branchName,
|
|
123
|
+
lastCommitTime: timestamp,
|
|
124
|
+
lastCommitTimeFormatted: formatted,
|
|
125
|
+
category: getBranchCategory(branchName)
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 解析 Git remote URL
|
|
131
|
+
*/
|
|
132
|
+
function parseRemoteUrl(remote) {
|
|
133
|
+
let host = "";
|
|
134
|
+
let repoPath = "";
|
|
135
|
+
let protocol = "https";
|
|
136
|
+
if (remote.startsWith("git@")) {
|
|
137
|
+
const match = remote.match(/git@([^:]+):(.+?)(?:\.git)?$/);
|
|
138
|
+
if (match) {
|
|
139
|
+
host = match[1];
|
|
140
|
+
repoPath = match[2];
|
|
141
|
+
} else return null;
|
|
142
|
+
} else if (remote.startsWith("ssh://git@")) {
|
|
143
|
+
const match = remote.match(/ssh:\/\/git@([^/]+)\/(.+?)(?:\.git)?$/);
|
|
144
|
+
if (match) {
|
|
145
|
+
host = match[1];
|
|
146
|
+
repoPath = match[2];
|
|
147
|
+
} else return null;
|
|
148
|
+
} else if (remote.startsWith("https://")) {
|
|
149
|
+
protocol = "https";
|
|
150
|
+
const match = remote.match(/https:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
151
|
+
if (match) {
|
|
152
|
+
host = match[1];
|
|
153
|
+
repoPath = match[2];
|
|
154
|
+
} else return null;
|
|
155
|
+
} else if (remote.startsWith("http://")) {
|
|
156
|
+
protocol = "http";
|
|
157
|
+
const match = remote.match(/http:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
158
|
+
if (match) {
|
|
159
|
+
host = match[1];
|
|
160
|
+
repoPath = match[2];
|
|
161
|
+
} else return null;
|
|
162
|
+
} else return null;
|
|
163
|
+
if (isIP(host)) protocol = "http";
|
|
164
|
+
return {
|
|
165
|
+
host,
|
|
166
|
+
repoPath,
|
|
167
|
+
protocol
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 生成 PR 链接
|
|
172
|
+
*/
|
|
173
|
+
function generatePRUrl(host, repoPath, protocol, sourceBranch, targetBranch) {
|
|
174
|
+
const baseUrl = `${protocol}://${host}/${repoPath}`;
|
|
175
|
+
if (host.includes("github.com")) return `${baseUrl}/compare/${targetBranch}...${sourceBranch}`;
|
|
176
|
+
else return `${baseUrl}/merge_requests/new?merge_request%5Bsource_branch%5D=${encodeURIComponent(sourceBranch)}&merge_request%5Btarget_branch%5D=${encodeURIComponent(targetBranch)}`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 获取两个分支之间的提交信息
|
|
180
|
+
*/
|
|
181
|
+
function getCommitsBetweenBranches(targetBranch, sourceBranch) {
|
|
182
|
+
try {
|
|
183
|
+
return execSync(`git log --pretty=format:"- %s" ${targetBranch}..${sourceBranch}`, {
|
|
184
|
+
encoding: "utf-8",
|
|
185
|
+
stdio: [
|
|
186
|
+
"pipe",
|
|
187
|
+
"pipe",
|
|
188
|
+
"ignore"
|
|
189
|
+
]
|
|
190
|
+
}).trim().split("\n").filter((line) => line);
|
|
191
|
+
} catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 生成 PR 描述信息
|
|
197
|
+
*/
|
|
198
|
+
function generatePRMessage(sourceBranch, targetBranch) {
|
|
199
|
+
const commits = getCommitsBetweenBranches(targetBranch, sourceBranch);
|
|
200
|
+
let message = `### 🔧 PR: \`${sourceBranch}\` → \`${targetBranch}\`\n\n#### 📝 Commit Summary:\n`;
|
|
201
|
+
if (commits.length === 0) message += "\n(无差异提交)";
|
|
202
|
+
else message += commits.join("\n");
|
|
203
|
+
return message;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* 生成合并分支名称
|
|
207
|
+
*/
|
|
208
|
+
function generateMergeBranchName(sourceBranch, targetBranch) {
|
|
209
|
+
return `merge/${sourceBranch.replace(/\//g, "-")}-to-${targetBranch.replace(/\//g, "-")}`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 切换到目标分支并创建合并分支
|
|
213
|
+
*/
|
|
214
|
+
function createMergeBranch(targetBranch, mergeBranchName) {
|
|
215
|
+
try {
|
|
216
|
+
console.log(cyan(`\n🔀 Switching to target branch: ${targetBranch}`));
|
|
217
|
+
execSync(`git checkout ${targetBranch}`, { stdio: "inherit" });
|
|
218
|
+
console.log(cyan(`🌿 Creating merge branch: ${mergeBranchName}`));
|
|
219
|
+
execSync(`git checkout -b ${mergeBranchName}`, { stdio: "inherit" });
|
|
220
|
+
console.log(green(`✅ Successfully created merge branch: ${mergeBranchName}\n`));
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
console.log(red("❌ Failed to create merge branch"));
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 复制文本到剪贴板
|
|
229
|
+
*/
|
|
230
|
+
function copyToClipboard(text) {
|
|
231
|
+
try {
|
|
232
|
+
if (process.platform === "darwin") {
|
|
233
|
+
execSync("pbcopy", {
|
|
234
|
+
input: text,
|
|
235
|
+
stdio: [
|
|
236
|
+
"pipe",
|
|
237
|
+
"ignore",
|
|
238
|
+
"ignore"
|
|
239
|
+
]
|
|
240
|
+
});
|
|
241
|
+
return true;
|
|
242
|
+
} else if (process.platform === "linux") try {
|
|
243
|
+
execSync("which xclip", { stdio: "ignore" });
|
|
244
|
+
execSync("xclip -selection clipboard", {
|
|
245
|
+
input: text,
|
|
246
|
+
stdio: [
|
|
247
|
+
"pipe",
|
|
248
|
+
"ignore",
|
|
249
|
+
"ignore"
|
|
250
|
+
]
|
|
251
|
+
});
|
|
252
|
+
return true;
|
|
253
|
+
} catch {
|
|
254
|
+
try {
|
|
255
|
+
execSync("which wl-copy", { stdio: "ignore" });
|
|
256
|
+
execSync("wl-copy", {
|
|
257
|
+
input: text,
|
|
258
|
+
stdio: [
|
|
259
|
+
"pipe",
|
|
260
|
+
"ignore",
|
|
261
|
+
"ignore"
|
|
262
|
+
]
|
|
263
|
+
});
|
|
264
|
+
return true;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (process.platform === "win32") {
|
|
270
|
+
execSync("clip", {
|
|
271
|
+
input: text,
|
|
272
|
+
stdio: [
|
|
273
|
+
"pipe",
|
|
274
|
+
"ignore",
|
|
275
|
+
"ignore"
|
|
276
|
+
]
|
|
277
|
+
});
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 创建完整的 PR
|
|
287
|
+
*/
|
|
288
|
+
function createPullRequest(sourceBranch, targetBranch, remoteUrl) {
|
|
289
|
+
const parsed = parseRemoteUrl(remoteUrl);
|
|
290
|
+
if (!parsed) {
|
|
291
|
+
console.log(red("❌ 无法解析 remote URL"));
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const { host, repoPath, protocol } = parsed;
|
|
295
|
+
return {
|
|
296
|
+
sourceBranch,
|
|
297
|
+
targetBranch,
|
|
298
|
+
prUrl: generatePRUrl(host, repoPath, protocol, sourceBranch, targetBranch),
|
|
299
|
+
prMessage: generatePRMessage(sourceBranch, targetBranch),
|
|
300
|
+
mergeBranchName: generateMergeBranchName(sourceBranch, targetBranch)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/config/prompts.ts
|
|
306
|
+
const COMMIT_MESSAGE_PROMPT = `遵循 Angular Commit Message 规范,生成git commit message,
|
|
307
|
+
|
|
308
|
+
如果用户没有指示,默认为中文
|
|
309
|
+
尽量使用plaintext的语法,不要使用md的语法
|
|
310
|
+
生成的内容中不能包含emoji
|
|
311
|
+
格式:
|
|
312
|
+
<type>(<scope>): <subject>
|
|
313
|
+
|
|
314
|
+
- 详细描述1
|
|
315
|
+
- 详细描述2
|
|
316
|
+
- 详细描述3
|
|
317
|
+
|
|
318
|
+
其中 subject 必填,详细描述为可选的补充说明
|
|
319
|
+
|
|
320
|
+
type:
|
|
321
|
+
[
|
|
322
|
+
'feat', // 新功能
|
|
323
|
+
'fix', // 修复
|
|
324
|
+
'docs', // 文档变更
|
|
325
|
+
'style', // 代码格式
|
|
326
|
+
'refactor', // 重构
|
|
327
|
+
'perf', // 性能优化
|
|
328
|
+
'test', // 增加测试
|
|
329
|
+
'chore', // 构建过程或辅助工具的变动
|
|
330
|
+
'revert', // 回退
|
|
331
|
+
'build', // 打包
|
|
332
|
+
],
|
|
333
|
+
|
|
334
|
+
scope: 可选,表示影响范围(如模块名)
|
|
335
|
+
subject: 简明扼要的提交说明
|
|
336
|
+
详细描述: 使用列表形式简要说明主要改动点,每个列表项应简短清晰,数量限制在3-5个以内
|
|
337
|
+
|
|
338
|
+
重要规则:
|
|
339
|
+
1. 不要生成 body 和 footer 部分
|
|
340
|
+
2. 只生成 subject 和列表形式的详细描述
|
|
341
|
+
3. 列表项要简洁,每项不超过一行
|
|
342
|
+
4. 不要添加额外的解释或说明文字
|
|
343
|
+
|
|
344
|
+
示例:
|
|
345
|
+
feat(auth): 添加微信登录功能
|
|
346
|
+
|
|
347
|
+
- 支持微信扫码登录
|
|
348
|
+
- 支持微信账号绑定
|
|
349
|
+
- 添加微信用户信息同步
|
|
350
|
+
|
|
351
|
+
除了commit msg,其他不需要返回任何内容。
|
|
352
|
+
|
|
353
|
+
请你根据 git diff 生成 commit message。`;
|
|
354
|
+
const BRANCH_NAME_PROMPT = `请根据 git diff 生成分支名,遵循以下规范:
|
|
355
|
+
|
|
356
|
+
feat/ 新功能开发 feat/user-authentication
|
|
357
|
+
fix/ Bug修复 fix/login-error
|
|
358
|
+
hotfix/ 紧急线上问题修复 hotfix/payment-failure
|
|
359
|
+
refactor/ 代码重构 refactor/user-service
|
|
360
|
+
docs/ 文档更新 docs/api-reference
|
|
361
|
+
perf/ 性能优化 perf/image-loading
|
|
362
|
+
test/ 测试相关 test/user-profile
|
|
363
|
+
chore/ 构建/配置变更 chore/webpack-update
|
|
364
|
+
|
|
365
|
+
输出格式:直接输出分支名,无需其他内容`;
|
|
366
|
+
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/services/commit.ts
|
|
369
|
+
/**
|
|
370
|
+
* 获取暂存区的 git diff
|
|
371
|
+
*/
|
|
372
|
+
function getStagedDiff() {
|
|
373
|
+
try {
|
|
374
|
+
return execSync("git diff --cached", {
|
|
375
|
+
encoding: "utf-8",
|
|
376
|
+
stdio: [
|
|
377
|
+
"pipe",
|
|
378
|
+
"pipe",
|
|
379
|
+
"ignore"
|
|
380
|
+
],
|
|
381
|
+
maxBuffer: 10 * 1024 * 1024
|
|
382
|
+
}).trim();
|
|
383
|
+
} catch {
|
|
384
|
+
return "";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* 检查是否有暂存的更改
|
|
389
|
+
*/
|
|
390
|
+
function hasStagedChanges() {
|
|
391
|
+
try {
|
|
392
|
+
return execSync("git diff --cached --name-only", {
|
|
393
|
+
encoding: "utf-8",
|
|
394
|
+
stdio: [
|
|
395
|
+
"pipe",
|
|
396
|
+
"pipe",
|
|
397
|
+
"ignore"
|
|
398
|
+
]
|
|
399
|
+
}).trim().length > 0;
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* 获取常用的 Gemini 模型列表
|
|
406
|
+
* Last updated: 2025.11.17 (fetched from Google Gemini API)
|
|
407
|
+
* Total available models: 40
|
|
408
|
+
*/
|
|
409
|
+
function getCommonModels() {
|
|
410
|
+
return [
|
|
411
|
+
"gemini-2.5-pro",
|
|
412
|
+
"gemini-2.5-pro-preview-03-25",
|
|
413
|
+
"gemini-2.5-pro-preview-05-06",
|
|
414
|
+
"gemini-2.5-pro-preview-06-05",
|
|
415
|
+
"gemini-2.5-flash",
|
|
416
|
+
"gemini-2.5-flash-lite",
|
|
417
|
+
"gemini-2.5-flash-preview-05-20",
|
|
418
|
+
"gemini-2.5-flash-lite-preview-06-17",
|
|
419
|
+
"gemini-2.5-flash-image",
|
|
420
|
+
"gemini-2.5-computer-use-preview-10-2025",
|
|
421
|
+
"gemini-2.0-flash-exp",
|
|
422
|
+
"gemini-2.0-flash",
|
|
423
|
+
"gemini-2.0-flash-001",
|
|
424
|
+
"gemini-2.0-flash-lite",
|
|
425
|
+
"gemini-2.0-flash-lite-001",
|
|
426
|
+
"gemini-2.0-pro-exp",
|
|
427
|
+
"gemini-2.0-flash-thinking-exp",
|
|
428
|
+
"gemini-flash-latest",
|
|
429
|
+
"gemini-flash-lite-latest",
|
|
430
|
+
"gemini-pro-latest",
|
|
431
|
+
"gemini-exp-1206",
|
|
432
|
+
"learnlm-2.0-flash-experimental",
|
|
433
|
+
"gemma-3-27b-it",
|
|
434
|
+
"gemma-3-12b-it",
|
|
435
|
+
"gemma-3-4b-it",
|
|
436
|
+
"gemma-3-1b-it"
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* 从 Google API 动态获取可用的 Gemini 模型列表
|
|
441
|
+
*/
|
|
442
|
+
async function fetchAvailableModels(apiKey) {
|
|
443
|
+
try {
|
|
444
|
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
445
|
+
if (!response.ok) throw new Error(`Failed to fetch models: ${response.statusText}`);
|
|
446
|
+
return (await response.json()).models.map((model) => model.name.split("/")[1]).filter((name) => name);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
throw new Error(`Failed to fetch available models: ${error.message}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 使用 Gemini 生成 commit message
|
|
453
|
+
*/
|
|
454
|
+
async function generateCommitMessageStream(apiKey, diff, modelName) {
|
|
455
|
+
const startTime = Date.now();
|
|
456
|
+
const spinner = ora({
|
|
457
|
+
text: "Analyzing your changes with Gemini AI...",
|
|
458
|
+
color: "cyan"
|
|
459
|
+
}).start();
|
|
460
|
+
try {
|
|
461
|
+
const model = new GoogleGenerativeAI(apiKey).getGenerativeModel({ model: modelName });
|
|
462
|
+
spinner.text = "Generating commit message...";
|
|
463
|
+
const commitMessage = (await model.generateContent([COMMIT_MESSAGE_PROMPT, `\n\nGit Diff:\n${diff}`])).response.text().trim();
|
|
464
|
+
const duration = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
465
|
+
spinner.succeed(`AI generation completed in ${duration}s`);
|
|
466
|
+
console.log(green("\n✅ Generated commit message:\n"));
|
|
467
|
+
console.log(cyan("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
468
|
+
console.log(commitMessage);
|
|
469
|
+
console.log(cyan("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
|
|
470
|
+
return commitMessage;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
spinner.fail("Failed to generate commit message");
|
|
473
|
+
if (error.message?.includes("API key")) throw new Error("Invalid API key. Please check your Gemini API key.");
|
|
474
|
+
if (error.message?.includes("429") || error.message?.includes("Too Many Requests") || error.message?.includes("Resource exhausted")) throw new Error("API rate limit exceeded. Please wait a moment and try again.\n You can also try using a different API key or check your quota at:\n https://aistudio.google.com/apikey");
|
|
475
|
+
if (error.message?.includes("fetch") || error.message?.includes("network")) throw new Error("Network error. Please check your internet connection and try again.");
|
|
476
|
+
throw new Error(`Failed to generate commit message: ${error.message}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 生成分支名建议(可选)
|
|
481
|
+
*/
|
|
482
|
+
async function generateBranchName(apiKey, diff, modelName) {
|
|
483
|
+
const spinner = ora({
|
|
484
|
+
text: "Suggesting branch name...",
|
|
485
|
+
color: "yellow"
|
|
486
|
+
}).start();
|
|
487
|
+
try {
|
|
488
|
+
const branchName = (await new GoogleGenerativeAI(apiKey).getGenerativeModel({ model: modelName }).generateContent([BRANCH_NAME_PROMPT, `\n\nGit Diff:\n${diff}`])).response.text().trim();
|
|
489
|
+
spinner.succeed("Branch name suggested!");
|
|
490
|
+
return branchName;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
spinner.fail("Failed to generate branch name");
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 执行 git commit
|
|
498
|
+
*/
|
|
499
|
+
function performCommit(message) {
|
|
500
|
+
try {
|
|
501
|
+
execSync(`git commit -m "${message.replace(/"/g, "\\\"")}"`, { stdio: "inherit" });
|
|
502
|
+
return true;
|
|
503
|
+
} catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* 显示分支名建议
|
|
509
|
+
*/
|
|
510
|
+
function displayBranchName(branchName) {
|
|
511
|
+
console.log(yellow("💡 Suggested branch name:\n"));
|
|
512
|
+
console.log(green(` ${branchName}\n`));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/utils/config.ts
|
|
517
|
+
const CONFIG_DIR = join(homedir(), ".qkpr");
|
|
518
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
519
|
+
/**
|
|
520
|
+
* 确保配置目录存在
|
|
521
|
+
*/
|
|
522
|
+
function ensureConfigDir() {
|
|
523
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* 读取配置
|
|
527
|
+
*/
|
|
528
|
+
function readConfig() {
|
|
529
|
+
ensureConfigDir();
|
|
530
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
531
|
+
try {
|
|
532
|
+
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
533
|
+
return JSON.parse(content);
|
|
534
|
+
} catch {
|
|
535
|
+
return {};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* 写入配置
|
|
540
|
+
*/
|
|
541
|
+
function writeConfig(config) {
|
|
542
|
+
ensureConfigDir();
|
|
543
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* 获取 Gemini API Key
|
|
547
|
+
* 优先级:配置文件 > 环境变量 QUICK_PR_GEMINI_API_KEY > GEMINI_API_KEY
|
|
548
|
+
*
|
|
549
|
+
* You can set the API key in either:
|
|
550
|
+
* 1. Config file (~/.qkpr/config.json) via `qkpr config` command
|
|
551
|
+
* 2. Environment variable: export QKPR_GEMINI_API_KEY=your_api_key
|
|
552
|
+
* 3. Environment variable (legacy): export GEMINI_API_KEY=your_api_key
|
|
553
|
+
*/
|
|
554
|
+
function getGeminiApiKey() {
|
|
555
|
+
return readConfig().geminiApiKey || process.env.QUICK_PR_GEMINI_API_KEY || process.env.GEMINI_API_KEY;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* 设置 Gemini API Key
|
|
559
|
+
*/
|
|
560
|
+
function setGeminiApiKey(apiKey) {
|
|
561
|
+
const config = readConfig();
|
|
562
|
+
config.geminiApiKey = apiKey;
|
|
563
|
+
writeConfig(config);
|
|
564
|
+
}
|
|
565
|
+
function getGeminiModel() {
|
|
566
|
+
return readConfig().geminiModel || process.env.QUICK_PR_GEMINI_MODEL || process.env.GEMINI_MODEL || "gemini-2.0-flash";
|
|
567
|
+
}
|
|
568
|
+
function setGeminiModel(model) {
|
|
569
|
+
const config = readConfig();
|
|
570
|
+
config.geminiModel = model;
|
|
571
|
+
writeConfig(config);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* 获取已固定的分支列表
|
|
575
|
+
*/
|
|
576
|
+
function getPinnedBranches() {
|
|
577
|
+
return readConfig().pinnedBranches || [];
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* 添加固定分支
|
|
581
|
+
*/
|
|
582
|
+
function addPinnedBranch(branch) {
|
|
583
|
+
const config = readConfig();
|
|
584
|
+
const pinnedBranches = config.pinnedBranches || [];
|
|
585
|
+
if (!pinnedBranches.includes(branch)) {
|
|
586
|
+
pinnedBranches.push(branch);
|
|
587
|
+
config.pinnedBranches = pinnedBranches;
|
|
588
|
+
writeConfig(config);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* 移除固定分支
|
|
593
|
+
*/
|
|
594
|
+
function removePinnedBranch(branch) {
|
|
595
|
+
const config = readConfig();
|
|
596
|
+
const pinnedBranches = config.pinnedBranches || [];
|
|
597
|
+
const index = pinnedBranches.indexOf(branch);
|
|
598
|
+
if (index > -1) {
|
|
599
|
+
pinnedBranches.splice(index, 1);
|
|
600
|
+
config.pinnedBranches = pinnedBranches;
|
|
601
|
+
writeConfig(config);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/utils/commit-cli.ts
|
|
607
|
+
inquirer.registerPrompt("autocomplete", inquirerAutoComplete);
|
|
608
|
+
/**
|
|
609
|
+
* 提示用户输入 API Key
|
|
610
|
+
*/
|
|
611
|
+
async function promptApiKey() {
|
|
612
|
+
while (true) {
|
|
613
|
+
const { action } = await inquirer.prompt([{
|
|
614
|
+
type: "list",
|
|
615
|
+
name: "action",
|
|
616
|
+
message: "Please enter your Gemini API Key:",
|
|
617
|
+
choices: [
|
|
618
|
+
{
|
|
619
|
+
name: "✏️ Enter API Key",
|
|
620
|
+
value: "enter"
|
|
621
|
+
},
|
|
622
|
+
new inquirer.Separator(),
|
|
623
|
+
{
|
|
624
|
+
name: "↩️ Go back",
|
|
625
|
+
value: "back"
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
}]);
|
|
629
|
+
if (action === "back") return null;
|
|
630
|
+
const { apiKey } = await inquirer.prompt([{
|
|
631
|
+
type: "password",
|
|
632
|
+
name: "apiKey",
|
|
633
|
+
message: "API Key:",
|
|
634
|
+
mask: "*"
|
|
635
|
+
}]);
|
|
636
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
637
|
+
console.log(yellow("⚠️ Please enter a valid API Key, or go back"));
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
return apiKey.trim();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* 询问是否保存 API Key
|
|
645
|
+
*/
|
|
646
|
+
async function promptSaveApiKey() {
|
|
647
|
+
const { shouldSave } = await inquirer.prompt([{
|
|
648
|
+
type: "confirm",
|
|
649
|
+
name: "shouldSave",
|
|
650
|
+
message: "Save API Key for future use?",
|
|
651
|
+
default: true
|
|
652
|
+
}]);
|
|
653
|
+
return shouldSave;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 询问用户选择模型
|
|
657
|
+
*/
|
|
658
|
+
async function promptModelSelection(apiKey) {
|
|
659
|
+
let availableModels = getCommonModels();
|
|
660
|
+
const currentModel = getGeminiModel();
|
|
661
|
+
if (apiKey) try {
|
|
662
|
+
console.log(dim("Fetching available models..."));
|
|
663
|
+
const fetchedModels = await fetchAvailableModels(apiKey);
|
|
664
|
+
if (fetchedModels.length > 0) {
|
|
665
|
+
availableModels = fetchedModels;
|
|
666
|
+
console.log(green("✅ Successfully fetched available models\n"));
|
|
667
|
+
}
|
|
668
|
+
} catch (error) {
|
|
669
|
+
console.log(yellow(`⚠️ Could not fetch models dynamically: ${error.message}`));
|
|
670
|
+
console.log(dim("Using common models list instead\n"));
|
|
671
|
+
}
|
|
672
|
+
const { modelChoice } = await inquirer.prompt([{
|
|
673
|
+
type: "autocomplete",
|
|
674
|
+
name: "modelChoice",
|
|
675
|
+
message: "Select a Gemini model (use arrow keys to navigate, type to search):",
|
|
676
|
+
default: currentModel,
|
|
677
|
+
pageSize: 10,
|
|
678
|
+
source: (answersSoFar, input) => {
|
|
679
|
+
const choices = [
|
|
680
|
+
...availableModels.map((model) => ({
|
|
681
|
+
name: model === currentModel ? `${model} (current)` : model,
|
|
682
|
+
value: model
|
|
683
|
+
})),
|
|
684
|
+
{
|
|
685
|
+
name: "✏️ Enter custom model name",
|
|
686
|
+
value: "custom"
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: "↩️ Go back",
|
|
690
|
+
value: "back"
|
|
691
|
+
}
|
|
692
|
+
];
|
|
693
|
+
if (!input) return Promise.resolve(choices);
|
|
694
|
+
const filtered = choices.filter((choice) => choice.name.toLowerCase().includes(input.toLowerCase()) || choice.value.toString().toLowerCase().includes(input.toLowerCase()));
|
|
695
|
+
return Promise.resolve(filtered);
|
|
696
|
+
}
|
|
697
|
+
}]);
|
|
698
|
+
if (modelChoice === "back") return null;
|
|
699
|
+
if (modelChoice === "custom") {
|
|
700
|
+
const { customModel } = await inquirer.prompt([{
|
|
701
|
+
type: "input",
|
|
702
|
+
name: "customModel",
|
|
703
|
+
message: "Enter model name (leave empty to go back):",
|
|
704
|
+
default: ""
|
|
705
|
+
}]);
|
|
706
|
+
if (!customModel || customModel.trim().length === 0) return null;
|
|
707
|
+
return customModel.trim();
|
|
708
|
+
}
|
|
709
|
+
return modelChoice;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* 询问用户操作选项
|
|
713
|
+
*/
|
|
714
|
+
async function promptCommitAction() {
|
|
715
|
+
const { action } = await inquirer.prompt([{
|
|
716
|
+
type: "list",
|
|
717
|
+
name: "action",
|
|
718
|
+
message: "What would you like to do?",
|
|
719
|
+
choices: [
|
|
720
|
+
{
|
|
721
|
+
name: "✅ Commit with this message",
|
|
722
|
+
value: "commit"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "📋 Copy to clipboard",
|
|
726
|
+
value: "copy"
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
name: "🌿 Generate branch name suggestion",
|
|
730
|
+
value: "branch"
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: "✏️ Regenerate",
|
|
734
|
+
value: "edit"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
name: "❌ Cancel",
|
|
738
|
+
value: "cancel"
|
|
739
|
+
}
|
|
740
|
+
]
|
|
741
|
+
}]);
|
|
742
|
+
return action;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* 处理 commit 命令
|
|
746
|
+
*/
|
|
747
|
+
async function handleCommitCommand() {
|
|
748
|
+
console.log(cyan("\n╔══════════════════════════════════════════════════════════════╗"));
|
|
749
|
+
console.log(cyan("║ 🤖 AI Commit Message Generator ║"));
|
|
750
|
+
console.log(cyan("╚══════════════════════════════════════════════════════════════╝\n"));
|
|
751
|
+
const model = getGeminiModel();
|
|
752
|
+
console.log(dim(`Using model: ${model}\n`));
|
|
753
|
+
if (!hasStagedChanges()) {
|
|
754
|
+
console.log(yellow("⚠️ No staged changes found."));
|
|
755
|
+
console.log(dim("Please stage your changes using: git add <files>\n"));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
let apiKey = getGeminiApiKey();
|
|
759
|
+
if (!apiKey) {
|
|
760
|
+
console.log(yellow("ℹ️ Gemini API Key not found.\n"));
|
|
761
|
+
console.log(dim("You can get your API Key from: https://aistudio.google.com/apikey\n"));
|
|
762
|
+
const newApiKey = await promptApiKey();
|
|
763
|
+
if (!newApiKey) {
|
|
764
|
+
console.log(yellow("\n⚠️ Cancelled\n"));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
apiKey = newApiKey;
|
|
768
|
+
if (await promptSaveApiKey()) {
|
|
769
|
+
setGeminiApiKey(apiKey);
|
|
770
|
+
console.log(green("\n✅ API Key saved successfully!\n"));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const diff = getStagedDiff();
|
|
774
|
+
if (!diff) {
|
|
775
|
+
console.log(red("❌ Failed to get git diff"));
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
const commitMessage = await generateCommitMessageStream(apiKey, diff, model);
|
|
780
|
+
let action = await promptCommitAction();
|
|
781
|
+
while (action === "branch") try {
|
|
782
|
+
displayBranchName(await generateBranchName(apiKey, diff, model));
|
|
783
|
+
action = await promptCommitAction();
|
|
784
|
+
} catch (error) {
|
|
785
|
+
console.log(red(`\n❌ Error generating branch name: ${error.message}\n`));
|
|
786
|
+
action = await promptCommitAction();
|
|
787
|
+
}
|
|
788
|
+
switch (action) {
|
|
789
|
+
case "commit":
|
|
790
|
+
if (performCommit(commitMessage)) console.log(green("\n✅ Commit successful!\n"));
|
|
791
|
+
else console.log(red("\n❌ Commit failed\n"));
|
|
792
|
+
break;
|
|
793
|
+
case "copy":
|
|
794
|
+
if (copyToClipboard(commitMessage)) console.log(green("\n✅ Commit message copied to clipboard\n"));
|
|
795
|
+
else console.log(yellow("\n⚠️ Could not copy to clipboard\n"));
|
|
796
|
+
break;
|
|
797
|
+
case "edit":
|
|
798
|
+
console.log(yellow("\n🔄 Regenerating...\n"));
|
|
799
|
+
await handleCommitCommand();
|
|
800
|
+
break;
|
|
801
|
+
case "cancel": console.log(dim("\n❌ Cancelled\n"));
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
console.log(red(`\n❌ Error: ${error.message}\n`));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* 配置 API Key
|
|
809
|
+
*/
|
|
810
|
+
async function handleConfigCommand() {
|
|
811
|
+
console.log(cyan("\n══════════════════════════════════════════════════════════════╗"));
|
|
812
|
+
console.log(cyan("║ ⚙️ Configuration ║"));
|
|
813
|
+
console.log(cyan("╚══════════════════════════════════════════════════════════════╝\n"));
|
|
814
|
+
console.log(dim("Get your API Key from: https://aistudio.google.com/apikey\n"));
|
|
815
|
+
const apiKey = await promptApiKey();
|
|
816
|
+
if (!apiKey) {
|
|
817
|
+
console.log(yellow("\n⚠️ Cancelled\n"));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
setGeminiApiKey(apiKey);
|
|
821
|
+
console.log(green("\n✅ API Key configured successfully!\n"));
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* 配置模型
|
|
825
|
+
*/
|
|
826
|
+
async function handleConfigModelCommand() {
|
|
827
|
+
console.log(cyan("\n╔══════════════════════════════════════════════════════════════╗"));
|
|
828
|
+
console.log(cyan("║ 🤖 Model Configuration ║"));
|
|
829
|
+
console.log(cyan("╚══════════════════════════════════════════════════════════════╝\n"));
|
|
830
|
+
const currentModel = getGeminiModel();
|
|
831
|
+
console.log(dim(`Current model: ${currentModel}\n`));
|
|
832
|
+
const apiKey = getGeminiApiKey();
|
|
833
|
+
if (!apiKey) {
|
|
834
|
+
console.log(yellow("ℹ️ No API Key found. Using common models list."));
|
|
835
|
+
console.log(dim("Configure API Key first to fetch all available models dynamically.\n"));
|
|
836
|
+
}
|
|
837
|
+
const model = await promptModelSelection(apiKey);
|
|
838
|
+
if (!model) {
|
|
839
|
+
console.log(yellow("\n⚠️ Cancelled\n"));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
setGeminiModel(model);
|
|
843
|
+
console.log(green(`\n✅ Model configured successfully: ${model}\n`));
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* 创建并切换到新分支
|
|
847
|
+
*/
|
|
848
|
+
async function createAndCheckoutBranch(branchName) {
|
|
849
|
+
try {
|
|
850
|
+
console.log(cyan(`🌿 Creating and switching to branch: ${branchName}`));
|
|
851
|
+
execSync(`git checkout -b ${branchName}`, { stdio: "inherit" });
|
|
852
|
+
console.log(green(`✅ Successfully created and switched to: ${branchName}\n`));
|
|
853
|
+
return true;
|
|
854
|
+
} catch {
|
|
855
|
+
console.log(red("❌ Failed to create branch"));
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* 检查分支是否已推送到远程
|
|
861
|
+
*/
|
|
862
|
+
function isBranchPushed(branchName) {
|
|
863
|
+
try {
|
|
864
|
+
return execSync(`git ls-remote --heads origin ${branchName}`, {
|
|
865
|
+
encoding: "utf-8",
|
|
866
|
+
stdio: [
|
|
867
|
+
"pipe",
|
|
868
|
+
"pipe",
|
|
869
|
+
"ignore"
|
|
870
|
+
]
|
|
871
|
+
}).trim().length > 0;
|
|
872
|
+
} catch {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* 推送分支到远程
|
|
878
|
+
*/
|
|
879
|
+
function pushBranchToRemote(branchName) {
|
|
880
|
+
try {
|
|
881
|
+
console.log(cyan(`📤 Pushing branch to remote: ${branchName}`));
|
|
882
|
+
execSync(`git push -u origin ${branchName}`, { stdio: "inherit" });
|
|
883
|
+
console.log(green(`✅ Branch pushed successfully: ${branchName}\n`));
|
|
884
|
+
return true;
|
|
885
|
+
} catch {
|
|
886
|
+
console.log(red("❌ Failed to push branch to remote"));
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* 询问是否创建并切换到建议的分支
|
|
892
|
+
*/
|
|
893
|
+
async function promptCreateBranch(branchName) {
|
|
894
|
+
const { shouldCreate } = await inquirer.prompt([{
|
|
895
|
+
type: "confirm",
|
|
896
|
+
name: "shouldCreate",
|
|
897
|
+
message: `Create and switch to branch '${branchName}'?`,
|
|
898
|
+
default: false
|
|
899
|
+
}]);
|
|
900
|
+
return shouldCreate;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* 生成分支名称
|
|
904
|
+
*/
|
|
905
|
+
async function handleBranchCommand() {
|
|
906
|
+
console.log(cyan("\n╔══════════════════════════════════════════════════════════════╗"));
|
|
907
|
+
console.log(cyan("║ 🌿 AI Branch Name Generator ║"));
|
|
908
|
+
console.log(cyan("╚══════════════════════════════════════════════════════════════╝\n"));
|
|
909
|
+
const model = getGeminiModel();
|
|
910
|
+
console.log(dim(`Using model: ${model}\n`));
|
|
911
|
+
if (!hasStagedChanges()) {
|
|
912
|
+
console.log(yellow("⚠️ No staged changes found."));
|
|
913
|
+
console.log(dim("Please stage your changes using: git add <files>\n"));
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
let apiKey = getGeminiApiKey();
|
|
917
|
+
if (!apiKey) {
|
|
918
|
+
console.log(yellow("ℹ️ Gemini API Key not found.\n"));
|
|
919
|
+
console.log(dim("You can get your API Key from: https://aistudio.google.com/apikey\n"));
|
|
920
|
+
const newApiKey = await promptApiKey();
|
|
921
|
+
if (!newApiKey) {
|
|
922
|
+
console.log(yellow("\n⚠️ Cancelled\n"));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
apiKey = newApiKey;
|
|
926
|
+
if (await promptSaveApiKey()) {
|
|
927
|
+
setGeminiApiKey(apiKey);
|
|
928
|
+
console.log(green("\n✅ API Key saved successfully!\n"));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const diff = getStagedDiff();
|
|
932
|
+
if (!diff) {
|
|
933
|
+
console.log(red("❌ Failed to get git diff"));
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
const branchName = await generateBranchName(apiKey, diff, model);
|
|
938
|
+
displayBranchName(branchName);
|
|
939
|
+
if (await promptCreateBranch(branchName)) {
|
|
940
|
+
if (!await createAndCheckoutBranch(branchName)) {}
|
|
941
|
+
} else {
|
|
942
|
+
const { shouldCopy } = await inquirer.prompt([{
|
|
943
|
+
type: "confirm",
|
|
944
|
+
name: "shouldCopy",
|
|
945
|
+
message: "Copy branch name to clipboard?",
|
|
946
|
+
default: true
|
|
947
|
+
}]);
|
|
948
|
+
if (shouldCopy) if (copyToClipboard(branchName)) console.log(green("\n✅ Branch name copied to clipboard\n"));
|
|
949
|
+
else console.log(yellow("\n⚠️ Could not copy to clipboard\n"));
|
|
950
|
+
else console.log(dim("\n"));
|
|
951
|
+
}
|
|
952
|
+
} catch (error) {
|
|
953
|
+
console.log(red(`\n❌ Error: ${error.message}\n`));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/utils/pr-cli.ts
|
|
959
|
+
inquirer.registerPrompt("autocomplete", inquirerAutoComplete);
|
|
960
|
+
inquirer.registerPrompt("search-checkbox", searchCheckbox);
|
|
961
|
+
/**
|
|
962
|
+
* 通用的分支选择函数,支持单选和多选
|
|
963
|
+
*/
|
|
964
|
+
async function promptBranchSelection(branches, options) {
|
|
965
|
+
const { title, message, mode, filterPinned = false } = options;
|
|
966
|
+
console.log(cyan(`\n${title}`));
|
|
967
|
+
console.log(dim(""));
|
|
968
|
+
if (branches.length === 0) {
|
|
969
|
+
console.log(yellow("⚠️ No branches found"));
|
|
970
|
+
return mode === "single" ? "" : [];
|
|
971
|
+
}
|
|
972
|
+
const branchInfos = getBranchesWithInfo(branches);
|
|
973
|
+
const pinnedBranchNames = getPinnedBranches();
|
|
974
|
+
const allPinnedBranches = branchInfos.filter((b) => pinnedBranchNames.includes(b.name));
|
|
975
|
+
const regularBranches = branchInfos.filter((b) => !pinnedBranchNames.includes(b.name));
|
|
976
|
+
const pinnedBranches = filterPinned ? [] : allPinnedBranches;
|
|
977
|
+
pinnedBranches.sort((a, b) => {
|
|
978
|
+
return pinnedBranchNames.indexOf(a.name) - pinnedBranchNames.indexOf(b.name);
|
|
979
|
+
});
|
|
980
|
+
const categorizedBranches = /* @__PURE__ */ new Map();
|
|
981
|
+
regularBranches.forEach((branch) => {
|
|
982
|
+
if (!categorizedBranches.has(branch.category)) categorizedBranches.set(branch.category, []);
|
|
983
|
+
categorizedBranches.get(branch.category).push(branch);
|
|
984
|
+
});
|
|
985
|
+
categorizedBranches.forEach((branches$1) => {
|
|
986
|
+
branches$1.sort((a, b) => b.lastCommitTime - a.lastCommitTime);
|
|
987
|
+
});
|
|
988
|
+
const categoryOrder = [
|
|
989
|
+
"feat",
|
|
990
|
+
"fix",
|
|
991
|
+
"merge",
|
|
992
|
+
"refactor",
|
|
993
|
+
"hotfix",
|
|
994
|
+
"chore",
|
|
995
|
+
"docs",
|
|
996
|
+
"test",
|
|
997
|
+
"style"
|
|
998
|
+
];
|
|
999
|
+
const sortedCategories = Array.from(categorizedBranches.keys()).sort((a, b) => {
|
|
1000
|
+
const aIndex = categoryOrder.indexOf(a);
|
|
1001
|
+
const bIndex = categoryOrder.indexOf(b);
|
|
1002
|
+
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
|
1003
|
+
if (aIndex !== -1) return -1;
|
|
1004
|
+
if (bIndex !== -1) return 1;
|
|
1005
|
+
if (a === "other") return 1;
|
|
1006
|
+
if (b === "other") return -1;
|
|
1007
|
+
return a.localeCompare(b);
|
|
1008
|
+
});
|
|
1009
|
+
const choices = [];
|
|
1010
|
+
if (pinnedBranches.length > 0) {
|
|
1011
|
+
choices.push(new inquirer.Separator(magenta("━━━━━━━━ 📌 Pinned Branches ━━━━━━━━")));
|
|
1012
|
+
pinnedBranches.forEach((branch) => {
|
|
1013
|
+
choices.push({
|
|
1014
|
+
name: `📌 ${branch.name.padEnd(45)} ${dim(`(${branch.lastCommitTimeFormatted})`)}`,
|
|
1015
|
+
value: branch.name,
|
|
1016
|
+
short: branch.name
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
choices.push(new inquirer.Separator(" "));
|
|
1020
|
+
}
|
|
1021
|
+
sortedCategories.forEach((category) => {
|
|
1022
|
+
const branches$1 = categorizedBranches.get(category);
|
|
1023
|
+
if (branches$1.length > 0) {
|
|
1024
|
+
const categoryLabel = category === "other" ? "Other Branches" : `${category}/*`;
|
|
1025
|
+
choices.push(new inquirer.Separator(cyan(`━━━━━━━━ ${categoryLabel} ━━━━━━━━`)));
|
|
1026
|
+
branches$1.forEach((branch) => {
|
|
1027
|
+
choices.push({
|
|
1028
|
+
name: ` ${branch.name.padEnd(45)} ${dim(`(${branch.lastCommitTimeFormatted})`)}`,
|
|
1029
|
+
value: branch.name,
|
|
1030
|
+
short: branch.name
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
choices.push(new inquirer.Separator(" "));
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
const searchBranches = async (_answers, input = "") => {
|
|
1037
|
+
const lowerInput = input.toLowerCase();
|
|
1038
|
+
return choices.filter((choice) => {
|
|
1039
|
+
if (!choice.value) return true;
|
|
1040
|
+
return choice.value.toLowerCase().includes(lowerInput);
|
|
1041
|
+
});
|
|
1042
|
+
};
|
|
1043
|
+
if (mode === "single") {
|
|
1044
|
+
const { selectedBranch } = await inquirer.prompt([{
|
|
1045
|
+
type: "autocomplete",
|
|
1046
|
+
name: "selectedBranch",
|
|
1047
|
+
message,
|
|
1048
|
+
source: searchBranches,
|
|
1049
|
+
pageSize: 20,
|
|
1050
|
+
default: pinnedBranches.length > 0 ? pinnedBranches[0].name : regularBranches[0]?.name
|
|
1051
|
+
}]);
|
|
1052
|
+
return selectedBranch;
|
|
1053
|
+
} else {
|
|
1054
|
+
const allBranches = [...pinnedBranches, ...regularBranches];
|
|
1055
|
+
allBranches.sort((a, b) => a.name.localeCompare(b.name));
|
|
1056
|
+
const simpleChoices = allBranches.map((branch) => {
|
|
1057
|
+
return {
|
|
1058
|
+
name: `${pinnedBranchNames.includes(branch.name) ? "📌 " : " "}${branch.name.padEnd(45)} ${dim(`(${branch.lastCommitTimeFormatted})`)}`,
|
|
1059
|
+
value: branch.name,
|
|
1060
|
+
short: branch.name
|
|
1061
|
+
};
|
|
1062
|
+
});
|
|
1063
|
+
const { selectedBranches } = await inquirer.prompt([{
|
|
1064
|
+
type: "search-checkbox",
|
|
1065
|
+
name: "selectedBranches",
|
|
1066
|
+
message,
|
|
1067
|
+
choices: simpleChoices
|
|
1068
|
+
}]);
|
|
1069
|
+
return selectedBranches || [];
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* 提示选择目标分支
|
|
1074
|
+
*/
|
|
1075
|
+
async function promptTargetBranch(branches, currentBranch) {
|
|
1076
|
+
console.log(dim(`Current branch: ${currentBranch}\n`));
|
|
1077
|
+
const targetBranch = await promptBranchSelection(branches.filter((b) => b !== currentBranch), {
|
|
1078
|
+
title: "🎯 Target Branch Selection",
|
|
1079
|
+
message: "Select target branch (type to search):",
|
|
1080
|
+
mode: "single"
|
|
1081
|
+
});
|
|
1082
|
+
if (!targetBranch) {
|
|
1083
|
+
console.log(yellow("⚠️ No branch selected. Using \"main\" as default."));
|
|
1084
|
+
return "main";
|
|
1085
|
+
}
|
|
1086
|
+
console.log(green(`✅ Selected target branch: ${targetBranch}\n`));
|
|
1087
|
+
return targetBranch;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* 确认是否创建合并分支
|
|
1091
|
+
*/
|
|
1092
|
+
async function promptCreateMergeBranch(mergeBranchName) {
|
|
1093
|
+
console.log(yellow(`\n💡 Suggested merge branch name: ${mergeBranchName}`));
|
|
1094
|
+
const { createMergeBranch: createMergeBranch$1 } = await inquirer.prompt([{
|
|
1095
|
+
type: "confirm",
|
|
1096
|
+
name: "createMergeBranch",
|
|
1097
|
+
message: "Do you want to create a merge branch for conflict resolution?",
|
|
1098
|
+
default: false
|
|
1099
|
+
}]);
|
|
1100
|
+
return createMergeBranch$1;
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* 显示 PR 信息
|
|
1104
|
+
*/
|
|
1105
|
+
function displayPRInfo(prMessage, prUrl) {
|
|
1106
|
+
console.log(cyan("\n📋 PR Description Generated:\n"));
|
|
1107
|
+
console.log(prMessage);
|
|
1108
|
+
console.log(cyan("\n👉 PR URL:\n"));
|
|
1109
|
+
console.log(green(prUrl));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
//#endregion
|
|
1113
|
+
//#region src/utils/pin-cli.ts
|
|
1114
|
+
/**
|
|
1115
|
+
* 处理 pin branch 命令
|
|
1116
|
+
*/
|
|
1117
|
+
async function handlePinCommand(branchName) {
|
|
1118
|
+
console.log(cyan("\n📌 Pin Branch"));
|
|
1119
|
+
console.log(dim("Pin frequently used branches for quick access\n"));
|
|
1120
|
+
if (branchName) {
|
|
1121
|
+
if (getPinnedBranches().includes(branchName)) {
|
|
1122
|
+
console.log(yellow(`⚠️ Branch '${branchName}' is already pinned`));
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
addPinnedBranch(branchName);
|
|
1126
|
+
console.log(green(`✅ Branch '${branchName}' has been pinned`));
|
|
1127
|
+
} else {
|
|
1128
|
+
const { getAllBranches: getAllBranches$1 } = await import("./pr-3u9dEVEc.mjs");
|
|
1129
|
+
const branches = getAllBranches$1();
|
|
1130
|
+
if (branches.length === 0) {
|
|
1131
|
+
console.log(yellow("⚠️ No branches found"));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const pinnedBranches = getPinnedBranches();
|
|
1135
|
+
const availableBranches = branches.filter((b) => !pinnedBranches.includes(b));
|
|
1136
|
+
if (availableBranches.length === 0) {
|
|
1137
|
+
console.log(yellow("⚠️ All branches are already pinned"));
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const selectedBranches = await promptBranchSelection(availableBranches, {
|
|
1141
|
+
title: "📌 Pin Branches",
|
|
1142
|
+
message: "Select branches to pin (type to search, Space to select, Enter to confirm):",
|
|
1143
|
+
mode: "multiple",
|
|
1144
|
+
filterPinned: true
|
|
1145
|
+
});
|
|
1146
|
+
if (selectedBranches.length === 0) {
|
|
1147
|
+
console.log(yellow("⚠️ No branches selected"));
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
selectedBranches.forEach((branch) => {
|
|
1151
|
+
addPinnedBranch(branch);
|
|
1152
|
+
});
|
|
1153
|
+
console.log(green(`✅ Pinned ${selectedBranches.length} branch(es)`));
|
|
1154
|
+
}
|
|
1155
|
+
const updatedPinnedBranches = getPinnedBranches();
|
|
1156
|
+
console.log(cyan("\n📌 Current pinned branches:"));
|
|
1157
|
+
updatedPinnedBranches.forEach((branch, index) => {
|
|
1158
|
+
console.log(dim(` ${index + 1}. ${branch}`));
|
|
1159
|
+
});
|
|
1160
|
+
console.log();
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* 处理 unpin branch 命令
|
|
1164
|
+
*/
|
|
1165
|
+
async function handleUnpinCommand(branchName) {
|
|
1166
|
+
console.log(cyan("\n📍 Unpin Branch"));
|
|
1167
|
+
console.log(dim("Remove a branch from pinned list\n"));
|
|
1168
|
+
const pinnedBranches = getPinnedBranches();
|
|
1169
|
+
if (pinnedBranches.length === 0) {
|
|
1170
|
+
console.log(yellow("⚠️ No pinned branches found"));
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (branchName) {
|
|
1174
|
+
if (!pinnedBranches.includes(branchName)) {
|
|
1175
|
+
console.log(red(`❌ Branch '${branchName}' is not pinned`));
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
removePinnedBranch(branchName);
|
|
1179
|
+
console.log(green(`✅ Branch '${branchName}' has been unpinned`));
|
|
1180
|
+
} else {
|
|
1181
|
+
const selectedBranches = await promptBranchSelection(pinnedBranches, {
|
|
1182
|
+
title: "📍 Unpin Branches",
|
|
1183
|
+
message: "Select branches to unpin (type to search, Space to select, Enter to confirm):",
|
|
1184
|
+
mode: "multiple"
|
|
1185
|
+
});
|
|
1186
|
+
if (selectedBranches.length === 0) {
|
|
1187
|
+
console.log(yellow("⚠️ No branches selected"));
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
selectedBranches.forEach((branch) => {
|
|
1191
|
+
removePinnedBranch(branch);
|
|
1192
|
+
});
|
|
1193
|
+
console.log(green(`✅ Unpinned ${selectedBranches.length} branch(es)`));
|
|
1194
|
+
}
|
|
1195
|
+
const updatedPinnedBranches = getPinnedBranches();
|
|
1196
|
+
if (updatedPinnedBranches.length > 0) {
|
|
1197
|
+
console.log(cyan("\n📌 Current pinned branches:"));
|
|
1198
|
+
updatedPinnedBranches.forEach((branch, index) => {
|
|
1199
|
+
console.log(dim(` ${index + 1}. ${branch}`));
|
|
1200
|
+
});
|
|
1201
|
+
} else console.log(dim("\nNo pinned branches"));
|
|
1202
|
+
console.log();
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* 显示所有固定的分支
|
|
1206
|
+
*/
|
|
1207
|
+
async function handleListPinnedCommand() {
|
|
1208
|
+
console.log(cyan("\n📌 Pinned Branches"));
|
|
1209
|
+
console.log(dim("List of all pinned branches\n"));
|
|
1210
|
+
const pinnedBranches = getPinnedBranches();
|
|
1211
|
+
if (pinnedBranches.length === 0) {
|
|
1212
|
+
console.log(yellow("⚠️ No pinned branches found"));
|
|
1213
|
+
console.log(dim("Use \"qkpr pin <branch-name>\" to pin a branch\n"));
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
pinnedBranches.forEach((branch, index) => {
|
|
1217
|
+
console.log(` ${green(`${index + 1}.`)} ${branch}`);
|
|
1218
|
+
});
|
|
1219
|
+
console.log();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/utils/version-check.ts
|
|
1224
|
+
/**
|
|
1225
|
+
* Check if there's a newer version available on npm
|
|
1226
|
+
*/
|
|
1227
|
+
async function checkForUpdates(packageName$1, currentVersion) {
|
|
1228
|
+
try {
|
|
1229
|
+
const latestVersion = execSync(`npm view ${packageName$1} version`, {
|
|
1230
|
+
encoding: "utf-8",
|
|
1231
|
+
stdio: [
|
|
1232
|
+
"pipe",
|
|
1233
|
+
"pipe",
|
|
1234
|
+
"ignore"
|
|
1235
|
+
],
|
|
1236
|
+
timeout: 3e3
|
|
1237
|
+
}).trim();
|
|
1238
|
+
return {
|
|
1239
|
+
hasUpdate: compareVersions(currentVersion, latestVersion) < 0,
|
|
1240
|
+
currentVersion,
|
|
1241
|
+
latestVersion
|
|
1242
|
+
};
|
|
1243
|
+
} catch {
|
|
1244
|
+
return {
|
|
1245
|
+
hasUpdate: false,
|
|
1246
|
+
currentVersion,
|
|
1247
|
+
latestVersion: currentVersion
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Compare two semantic versions
|
|
1253
|
+
* Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
|
|
1254
|
+
*/
|
|
1255
|
+
function compareVersions(v1, v2) {
|
|
1256
|
+
const parts1 = v1.split(".").map(Number);
|
|
1257
|
+
const parts2 = v2.split(".").map(Number);
|
|
1258
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
1259
|
+
const part1 = parts1[i] || 0;
|
|
1260
|
+
const part2 = parts2[i] || 0;
|
|
1261
|
+
if (part1 < part2) return -1;
|
|
1262
|
+
if (part1 > part2) return 1;
|
|
1263
|
+
}
|
|
1264
|
+
return 0;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Display update notification and prompt user to update
|
|
1268
|
+
*/
|
|
1269
|
+
async function promptForUpdate(packageName$1, result) {
|
|
1270
|
+
if (!result.hasUpdate) return;
|
|
1271
|
+
console.log(yellow("\n╔═══════════════════════════════════════════════════════════════╗"));
|
|
1272
|
+
console.log(yellow("║ 📦 Update Available ║"));
|
|
1273
|
+
console.log(yellow("╚═══════════════════════════════════════════════════════════════╝"));
|
|
1274
|
+
console.log(dim(` Current version: ${result.currentVersion}`));
|
|
1275
|
+
console.log(green(` Latest version: ${result.latestVersion}\n`));
|
|
1276
|
+
const { shouldUpdate, packageManager } = await inquirer.prompt([{
|
|
1277
|
+
type: "confirm",
|
|
1278
|
+
name: "shouldUpdate",
|
|
1279
|
+
message: "Would you like to update now?",
|
|
1280
|
+
default: false
|
|
1281
|
+
}, {
|
|
1282
|
+
type: "list",
|
|
1283
|
+
name: "packageManager",
|
|
1284
|
+
message: "Select package manager:",
|
|
1285
|
+
choices: [
|
|
1286
|
+
"npm",
|
|
1287
|
+
"pnpm",
|
|
1288
|
+
"yarn"
|
|
1289
|
+
],
|
|
1290
|
+
default: "npm",
|
|
1291
|
+
when: (answers) => answers.shouldUpdate
|
|
1292
|
+
}]);
|
|
1293
|
+
if (shouldUpdate) try {
|
|
1294
|
+
console.log(cyan(`\n⏳ Updating ${packageName$1}...\n`));
|
|
1295
|
+
let command;
|
|
1296
|
+
if (packageManager === "npm") command = `npm install -g ${packageName$1}`;
|
|
1297
|
+
else if (packageManager === "pnpm") command = `pnpm add -g ${packageName$1}`;
|
|
1298
|
+
else command = `yarn global add ${packageName$1}`;
|
|
1299
|
+
execSync(command, { stdio: "inherit" });
|
|
1300
|
+
console.log(green(`\n✅ Successfully updated to version ${result.latestVersion}!`));
|
|
1301
|
+
console.log(yellow("Please restart the command to use the new version.\n"));
|
|
1302
|
+
process.exit(0);
|
|
1303
|
+
} catch {
|
|
1304
|
+
console.log(red("\n❌ Failed to update. Please try manually:"));
|
|
1305
|
+
console.log(dim(` npm install -g ${packageName$1}`));
|
|
1306
|
+
console.log(dim(` or: pnpm add -g ${packageName$1}`));
|
|
1307
|
+
console.log(dim(` or: yarn global add ${packageName$1}\n`));
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
console.log(dim("\nYou can update later by running:"));
|
|
1311
|
+
console.log(yellow(` npm install -g ${packageName$1}\n`));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Check for updates and prompt user (non-blocking)
|
|
1316
|
+
*/
|
|
1317
|
+
async function checkAndNotifyUpdate(packageName$1, currentVersion) {
|
|
1318
|
+
try {
|
|
1319
|
+
await promptForUpdate(packageName$1, await checkForUpdates(packageName$1, currentVersion));
|
|
1320
|
+
} catch {}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
//#endregion
|
|
1324
|
+
//#region src/index.ts
|
|
1325
|
+
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
1326
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1327
|
+
const version = packageJson.version;
|
|
1328
|
+
const packageName = packageJson.name;
|
|
1329
|
+
/**
|
|
1330
|
+
* Show pinned branches management menu
|
|
1331
|
+
*/
|
|
1332
|
+
async function showPinnedBranchesMenu() {
|
|
1333
|
+
const inquirer$1 = (await import("inquirer")).default;
|
|
1334
|
+
while (true) {
|
|
1335
|
+
await handleListPinnedCommand();
|
|
1336
|
+
const { action } = await inquirer$1.prompt([{
|
|
1337
|
+
type: "list",
|
|
1338
|
+
name: "action",
|
|
1339
|
+
message: "What would you like to do?",
|
|
1340
|
+
choices: [
|
|
1341
|
+
{
|
|
1342
|
+
name: "1. 📌 Pin more branches",
|
|
1343
|
+
value: "pin",
|
|
1344
|
+
key: "1"
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
name: "2. 📍 Unpin branches",
|
|
1348
|
+
value: "unpin",
|
|
1349
|
+
key: "2"
|
|
1350
|
+
},
|
|
1351
|
+
new inquirer$1.Separator(),
|
|
1352
|
+
{
|
|
1353
|
+
name: "↩️ Back to main menu",
|
|
1354
|
+
value: "back"
|
|
1355
|
+
}
|
|
1356
|
+
]
|
|
1357
|
+
}]);
|
|
1358
|
+
switch (action) {
|
|
1359
|
+
case "pin":
|
|
1360
|
+
await handlePinCommand();
|
|
1361
|
+
break;
|
|
1362
|
+
case "unpin":
|
|
1363
|
+
await handleUnpinCommand();
|
|
1364
|
+
break;
|
|
1365
|
+
case "back": return;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Show main menu for feature selection
|
|
1371
|
+
*/
|
|
1372
|
+
async function showMainMenu() {
|
|
1373
|
+
console.log(bold(cyan("\n╔══════════════════════════════════════════════════════════════╗")));
|
|
1374
|
+
console.log(bold(cyan("║ 🚀 Quick PR Tool ║")));
|
|
1375
|
+
console.log(bold(cyan("║ ║")));
|
|
1376
|
+
console.log(bold(cyan("║ Your All-in-One Git Workflow Assistant ║")));
|
|
1377
|
+
console.log(bold(cyan("║ ║")));
|
|
1378
|
+
console.log(bold(cyan("║ Author: KazooTTT ║")));
|
|
1379
|
+
console.log(bold(cyan("║ GitHub: https://github.com/KazooTTT/qkpr ║")));
|
|
1380
|
+
console.log(bold(cyan("╚══════════════════════════════════════════════════════════════╝")));
|
|
1381
|
+
console.log(` Version: ${version}\n`);
|
|
1382
|
+
const inquirer$1 = (await import("inquirer")).default;
|
|
1383
|
+
const { feature } = await inquirer$1.prompt([{
|
|
1384
|
+
type: "list",
|
|
1385
|
+
name: "feature",
|
|
1386
|
+
message: "What would you like to do?",
|
|
1387
|
+
choices: [
|
|
1388
|
+
{
|
|
1389
|
+
name: "1. 🔧 Create Pull Request",
|
|
1390
|
+
value: "pr",
|
|
1391
|
+
key: "1"
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
name: "2. 🤖 Generate Commit Message",
|
|
1395
|
+
value: "commit",
|
|
1396
|
+
key: "2"
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
name: "3. 🌿 Generate Branch Name",
|
|
1400
|
+
value: "branch",
|
|
1401
|
+
key: "3"
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
name: "4. ⚙️ Configure API Key",
|
|
1405
|
+
value: "config",
|
|
1406
|
+
key: "4"
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
name: "5. 🔧 Configure Model",
|
|
1410
|
+
value: "config:model",
|
|
1411
|
+
key: "5"
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
name: "6. 📌 Manage Pinned Branches",
|
|
1415
|
+
value: "pinned",
|
|
1416
|
+
key: "6"
|
|
1417
|
+
},
|
|
1418
|
+
new inquirer$1.Separator(),
|
|
1419
|
+
{
|
|
1420
|
+
name: "❌ Exit",
|
|
1421
|
+
value: "exit"
|
|
1422
|
+
}
|
|
1423
|
+
]
|
|
1424
|
+
}]);
|
|
1425
|
+
switch (feature) {
|
|
1426
|
+
case "pr":
|
|
1427
|
+
await handlePRCommand();
|
|
1428
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1429
|
+
await showMainMenu();
|
|
1430
|
+
break;
|
|
1431
|
+
case "commit":
|
|
1432
|
+
await handleCommitCommand();
|
|
1433
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1434
|
+
await showMainMenu();
|
|
1435
|
+
break;
|
|
1436
|
+
case "branch":
|
|
1437
|
+
await handleBranchCommand();
|
|
1438
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1439
|
+
await showMainMenu();
|
|
1440
|
+
break;
|
|
1441
|
+
case "config":
|
|
1442
|
+
await handleConfigCommand();
|
|
1443
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1444
|
+
await showMainMenu();
|
|
1445
|
+
break;
|
|
1446
|
+
case "config:model":
|
|
1447
|
+
await handleConfigModelCommand();
|
|
1448
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1449
|
+
await showMainMenu();
|
|
1450
|
+
break;
|
|
1451
|
+
case "pinned":
|
|
1452
|
+
await showPinnedBranchesMenu();
|
|
1453
|
+
await showMainMenu();
|
|
1454
|
+
break;
|
|
1455
|
+
case "exit":
|
|
1456
|
+
console.log(dim("\n👋 Goodbye!\n"));
|
|
1457
|
+
process.exit(0);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function printPRBanner() {
|
|
1461
|
+
console.log(bold(cyan("\n╔══════════════════════════════════════════════════════════════╗")));
|
|
1462
|
+
console.log(bold(cyan("║ 🔧 Quick PR Creator ║")));
|
|
1463
|
+
console.log(bold(cyan("║ ║")));
|
|
1464
|
+
console.log(bold(cyan("║ Interactive PR Creation Tool ║")));
|
|
1465
|
+
console.log(bold(cyan("╚══════════════════════════════════════════════════════════════╝")));
|
|
1466
|
+
console.log(` Version: ${version}\n`);
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* 询问是否推送分支到远程
|
|
1470
|
+
*/
|
|
1471
|
+
async function promptPushBranch(branchName) {
|
|
1472
|
+
const { shouldPush } = await (await import("inquirer")).default.prompt([{
|
|
1473
|
+
type: "confirm",
|
|
1474
|
+
name: "shouldPush",
|
|
1475
|
+
message: `Branch '${branchName}' is not pushed to remote. Push now?`,
|
|
1476
|
+
default: true
|
|
1477
|
+
}]);
|
|
1478
|
+
return shouldPush;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* 处理 PR 命令
|
|
1482
|
+
*/
|
|
1483
|
+
async function handlePRCommand() {
|
|
1484
|
+
printPRBanner();
|
|
1485
|
+
const gitInfo = getGitInfo();
|
|
1486
|
+
if (!gitInfo.isGitRepo) {
|
|
1487
|
+
console.log(red("❌ Not a Git repository"));
|
|
1488
|
+
console.log(dim("Please run this command in a Git repository.\n"));
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
console.log(cyan("📍 Current Repository Information:"));
|
|
1492
|
+
console.log(dim(` Branch: ${gitInfo.currentBranch}`));
|
|
1493
|
+
console.log(dim(` Remote: ${gitInfo.remoteUrl}\n`));
|
|
1494
|
+
if (!isBranchPushed(gitInfo.currentBranch)) {
|
|
1495
|
+
console.log(yellow(`⚠️ Current branch '${gitInfo.currentBranch}' is not pushed to remote.`));
|
|
1496
|
+
if (await promptPushBranch(gitInfo.currentBranch)) {
|
|
1497
|
+
if (!pushBranchToRemote(gitInfo.currentBranch)) {
|
|
1498
|
+
console.log(red("❌ Cannot create PR without pushing branch to remote."));
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
} else {
|
|
1502
|
+
console.log(yellow("⚠️ PR creation skipped because branch is not pushed to remote."));
|
|
1503
|
+
console.log(dim("Please push the branch manually and try again.\n"));
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const branches = getAllBranches();
|
|
1508
|
+
if (branches.length === 0) {
|
|
1509
|
+
console.log(yellow("⚠️ No branches found."));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const targetBranch = await promptTargetBranch(branches, gitInfo.currentBranch);
|
|
1513
|
+
const prInfo = createPullRequest(gitInfo.currentBranch, targetBranch, gitInfo.remoteUrl);
|
|
1514
|
+
if (!prInfo) {
|
|
1515
|
+
console.log(red("❌ Failed to create PR information"));
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
displayPRInfo(prInfo.prMessage, prInfo.prUrl);
|
|
1519
|
+
if (copyToClipboard(prInfo.prMessage)) console.log(green("\n✅ PR description copied to clipboard"));
|
|
1520
|
+
else console.log(yellow("\n⚠️ Could not copy to clipboard"));
|
|
1521
|
+
console.log(cyan("\n🌐 Opening PR page in browser..."));
|
|
1522
|
+
try {
|
|
1523
|
+
await open(prInfo.prUrl);
|
|
1524
|
+
console.log(green("✅ Browser opened successfully"));
|
|
1525
|
+
} catch {
|
|
1526
|
+
console.log(yellow("⚠️ Could not open browser automatically"));
|
|
1527
|
+
console.log(dim(`Please open manually: ${prInfo.prUrl}`));
|
|
1528
|
+
}
|
|
1529
|
+
if (await promptCreateMergeBranch(prInfo.mergeBranchName)) {
|
|
1530
|
+
if (!createMergeBranch(targetBranch, prInfo.mergeBranchName)) return;
|
|
1531
|
+
}
|
|
1532
|
+
console.log(green("\n🎉 PR creation process completed!\n"));
|
|
1533
|
+
}
|
|
1534
|
+
yargs(hideBin(process.argv)).scriptName("qkpr").usage("Usage: $0 <command> [options]").command("$0", "Show interactive menu to choose features", () => {}, async () => {
|
|
1535
|
+
await showMainMenu();
|
|
1536
|
+
}).command("pr", "🔧 Create a Pull Request with interactive branch selection", () => {}, async () => {
|
|
1537
|
+
await handlePRCommand();
|
|
1538
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1539
|
+
}).command("commit", "🤖 Generate commit message using AI", () => {}, async () => {
|
|
1540
|
+
await handleCommitCommand();
|
|
1541
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1542
|
+
}).command("branch", "🌿 Generate branch name using AI", () => {}, async () => {
|
|
1543
|
+
await handleBranchCommand();
|
|
1544
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1545
|
+
}).command("config", "⚙️ Configure Gemini API Key", () => {}, async () => {
|
|
1546
|
+
await handleConfigCommand();
|
|
1547
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1548
|
+
}).command("config:model", "🔧 Configure Gemini Model", () => {}, async () => {
|
|
1549
|
+
await handleConfigModelCommand();
|
|
1550
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1551
|
+
}).command("pin [branch]", "📌 Pin a branch for quick access", (yargs$1) => {
|
|
1552
|
+
return yargs$1.positional("branch", {
|
|
1553
|
+
describe: "Branch name to pin",
|
|
1554
|
+
type: "string"
|
|
1555
|
+
});
|
|
1556
|
+
}, async (argv) => {
|
|
1557
|
+
await handlePinCommand(argv.branch);
|
|
1558
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1559
|
+
}).command("unpin [branch]", "📍 Unpin a branch", (yargs$1) => {
|
|
1560
|
+
return yargs$1.positional("branch", {
|
|
1561
|
+
describe: "Branch name to unpin",
|
|
1562
|
+
type: "string"
|
|
1563
|
+
});
|
|
1564
|
+
}, async (argv) => {
|
|
1565
|
+
await handleUnpinCommand(argv.branch);
|
|
1566
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1567
|
+
}).command("pinned", "📋 List all pinned branches", () => {}, async () => {
|
|
1568
|
+
await handleListPinnedCommand();
|
|
1569
|
+
await checkAndNotifyUpdate(packageName, version);
|
|
1570
|
+
}).version(version).alias("v", "version").help("h").alias("h", "help").epilog("For more information, visit https://github.com/KazooTTT/qkpr").argv;
|
|
1571
|
+
|
|
1572
|
+
//#endregion
|
|
1573
|
+
export { generatePRMessage as a, getBranchCategory as c, getCommitsBetweenBranches as d, getGitInfo as f, generateMergeBranchName as i, getBranchLastCommitTime as l, createMergeBranch as n, generatePRUrl as o, parseRemoteUrl as p, createPullRequest as r, getAllBranches as s, copyToClipboard as t, getBranchesWithInfo as u };
|