qkpr 1.0.5-beta.1 → 1.0.6
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 +7 -1
- package/dist/index.mjs +491 -65
- package/dist/pr-Cv5LeZ_A.mjs +3 -0
- package/package.json +5 -1
- package/dist/pr-C2AR97YR.mjs +0 -3
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ You can directly access the PR creation feature:
|
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
qkpr pr
|
|
51
|
+
# or specify the target branch directly
|
|
52
|
+
qkpr pr main
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
The CLI will interactively guide you through creating a pull request:
|
|
@@ -189,7 +191,11 @@ Shows an interactive menu to choose from all available features
|
|
|
189
191
|
qkpr pr
|
|
190
192
|
```
|
|
191
193
|
|
|
192
|
-
Directly create a pull request with interactive branch selection
|
|
194
|
+
Directly create a pull request with interactive branch selection. You can also specify the target branch directly:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
qkpr pr [branch]
|
|
198
|
+
```
|
|
193
199
|
|
|
194
200
|
### Generate Commit Message
|
|
195
201
|
|
package/dist/index.mjs
CHANGED
|
@@ -12,8 +12,18 @@ import inquirer from "inquirer";
|
|
|
12
12
|
import inquirerAutoComplete from "inquirer-autocomplete-prompt";
|
|
13
13
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
14
14
|
import ora from "ora";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
import searchCheckbox from "inquirer-search-checkbox";
|
|
18
|
+
import ansiEscapes from "ansi-escapes";
|
|
19
|
+
import figures from "figures";
|
|
20
|
+
import Choices from "inquirer/lib/objects/choices.js";
|
|
21
|
+
import Base from "inquirer/lib/prompts/base.js";
|
|
22
|
+
import observe from "inquirer/lib/utils/events.js";
|
|
23
|
+
import Paginator from "inquirer/lib/utils/paginator.js";
|
|
24
|
+
import * as utils from "inquirer/lib/utils/readline.js";
|
|
25
|
+
import runAsync from "run-async";
|
|
26
|
+
import { takeWhile } from "rxjs";
|
|
17
27
|
|
|
18
28
|
//#region src/services/pr.ts
|
|
19
29
|
/**
|
|
@@ -123,9 +133,10 @@ function getBranchCategory(branchName) {
|
|
|
123
133
|
return "other";
|
|
124
134
|
}
|
|
125
135
|
/**
|
|
126
|
-
*
|
|
136
|
+
* 获取分支详细信息(包含时间和分类)- 优化版本
|
|
127
137
|
*/
|
|
128
138
|
function getBranchesWithInfo(branches) {
|
|
139
|
+
if (branches.length > 50) return getBranchesWithInfoBatch(branches);
|
|
129
140
|
return branches.map((branchName) => {
|
|
130
141
|
const { timestamp, formatted } = getBranchLastCommitTime(branchName);
|
|
131
142
|
return {
|
|
@@ -137,6 +148,75 @@ function getBranchesWithInfo(branches) {
|
|
|
137
148
|
});
|
|
138
149
|
}
|
|
139
150
|
/**
|
|
151
|
+
* 批量获取分支信息,性能优化版本
|
|
152
|
+
*/
|
|
153
|
+
function getBranchesWithInfoBatch(branches) {
|
|
154
|
+
try {
|
|
155
|
+
const timestamps = execSync(branches.map((branch) => {
|
|
156
|
+
return `git log -1 --format=%ct origin/${branch} 2>/dev/null || echo "0"`;
|
|
157
|
+
}).join("; echo \"---\";"), {
|
|
158
|
+
encoding: "utf-8",
|
|
159
|
+
stdio: [
|
|
160
|
+
"pipe",
|
|
161
|
+
"pipe",
|
|
162
|
+
"ignore"
|
|
163
|
+
]
|
|
164
|
+
}).split("---").map((line) => {
|
|
165
|
+
const timestamp = Number.parseInt(line.trim(), 10);
|
|
166
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
167
|
+
});
|
|
168
|
+
return branches.map((branchName, index) => {
|
|
169
|
+
const timestamp = timestamps[index] || 0;
|
|
170
|
+
const { formatted } = formatTimestamp(timestamp);
|
|
171
|
+
return {
|
|
172
|
+
name: branchName,
|
|
173
|
+
lastCommitTime: timestamp,
|
|
174
|
+
lastCommitTimeFormatted: formatted,
|
|
175
|
+
category: getBranchCategory(branchName)
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.warn("Batch fetch failed, falling back to individual fetch:", error);
|
|
180
|
+
return branches.map((branchName) => {
|
|
181
|
+
const { timestamp, formatted } = getBranchLastCommitTime(branchName);
|
|
182
|
+
return {
|
|
183
|
+
name: branchName,
|
|
184
|
+
lastCommitTime: timestamp,
|
|
185
|
+
lastCommitTimeFormatted: formatted,
|
|
186
|
+
category: getBranchCategory(branchName)
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 格式化时间戳 - 提取为独立函数供批量版本使用
|
|
193
|
+
*/
|
|
194
|
+
function formatTimestamp(timestamp) {
|
|
195
|
+
if (timestamp === 0) return {
|
|
196
|
+
timestamp: 0,
|
|
197
|
+
formatted: "unknown"
|
|
198
|
+
};
|
|
199
|
+
const date = /* @__PURE__ */ new Date(timestamp * 1e3);
|
|
200
|
+
const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
|
|
201
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
202
|
+
let formatted;
|
|
203
|
+
if (diffDays === 0) {
|
|
204
|
+
const diffHours = Math.floor(diffMs / (1e3 * 60 * 60));
|
|
205
|
+
if (diffHours === 0) {
|
|
206
|
+
const diffMinutes = Math.floor(diffMs / (1e3 * 60));
|
|
207
|
+
formatted = diffMinutes <= 1 ? "just now" : `${diffMinutes}m ago`;
|
|
208
|
+
} else formatted = `${diffHours}h ago`;
|
|
209
|
+
} else if (diffDays === 1) formatted = "yesterday";
|
|
210
|
+
else if (diffDays < 7) formatted = `${diffDays}d ago`;
|
|
211
|
+
else if (diffDays < 30) formatted = `${Math.floor(diffDays / 7)}w ago`;
|
|
212
|
+
else if (diffDays < 365) formatted = `${Math.floor(diffDays / 30)}mo ago`;
|
|
213
|
+
else formatted = `${Math.floor(diffDays / 365)}y ago`;
|
|
214
|
+
return {
|
|
215
|
+
timestamp,
|
|
216
|
+
formatted
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
140
220
|
* 解析 Git remote URL
|
|
141
221
|
*/
|
|
142
222
|
function parseRemoteUrl(remote) {
|
|
@@ -339,6 +419,25 @@ function createPullRequest(sourceBranch, targetBranch, remoteUrl) {
|
|
|
339
419
|
const CONFIG_DIR = join(homedir(), ".qkpr");
|
|
340
420
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
341
421
|
/**
|
|
422
|
+
* 获取当前仓库的唯一标识
|
|
423
|
+
* 使用 git remote URL 的 hash 值作为仓库标识
|
|
424
|
+
*/
|
|
425
|
+
function getRepositoryId() {
|
|
426
|
+
try {
|
|
427
|
+
const normalizedUrl = execSync("git remote get-url origin", {
|
|
428
|
+
encoding: "utf-8",
|
|
429
|
+
stdio: [
|
|
430
|
+
"pipe",
|
|
431
|
+
"pipe",
|
|
432
|
+
"ignore"
|
|
433
|
+
]
|
|
434
|
+
}).trim().replace(/\.git$/, "").replace(/^https?:\/\//, "").replace(/^git@/, "").replace(/:/g, "/").toLowerCase();
|
|
435
|
+
return createHash("md5").update(normalizedUrl).digest("hex").substring(0, 12);
|
|
436
|
+
} catch {
|
|
437
|
+
return createHash("md5").update(process.cwd()).digest("hex").substring(0, 12);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
342
441
|
* 确保配置目录存在
|
|
343
442
|
*/
|
|
344
443
|
function ensureConfigDir() {
|
|
@@ -393,33 +492,47 @@ function setGeminiModel(model) {
|
|
|
393
492
|
writeConfig(config);
|
|
394
493
|
}
|
|
395
494
|
/**
|
|
396
|
-
* 获取已固定的分支列表
|
|
495
|
+
* 获取已固定的分支列表(仓库级别)
|
|
397
496
|
*/
|
|
398
497
|
function getPinnedBranches() {
|
|
399
|
-
|
|
498
|
+
const config = readConfig();
|
|
499
|
+
const repoId = getRepositoryId();
|
|
500
|
+
if (config.repositoryPinnedBranches?.[repoId]) return config.repositoryPinnedBranches[repoId];
|
|
501
|
+
if (config.pinnedBranches && config.pinnedBranches.length > 0) {
|
|
502
|
+
if (!config.repositoryPinnedBranches) config.repositoryPinnedBranches = {};
|
|
503
|
+
config.repositoryPinnedBranches[repoId] = [...config.pinnedBranches];
|
|
504
|
+
delete config.pinnedBranches;
|
|
505
|
+
writeConfig(config);
|
|
506
|
+
return config.repositoryPinnedBranches[repoId];
|
|
507
|
+
}
|
|
508
|
+
return [];
|
|
400
509
|
}
|
|
401
510
|
/**
|
|
402
|
-
* 添加固定分支
|
|
511
|
+
* 添加固定分支(仓库级别)
|
|
403
512
|
*/
|
|
404
513
|
function addPinnedBranch(branch) {
|
|
405
514
|
const config = readConfig();
|
|
406
|
-
const
|
|
515
|
+
const repoId = getRepositoryId();
|
|
516
|
+
if (!config.repositoryPinnedBranches) config.repositoryPinnedBranches = {};
|
|
517
|
+
const pinnedBranches = config.repositoryPinnedBranches[repoId] || [];
|
|
407
518
|
if (!pinnedBranches.includes(branch)) {
|
|
408
519
|
pinnedBranches.push(branch);
|
|
409
|
-
config.
|
|
520
|
+
config.repositoryPinnedBranches[repoId] = pinnedBranches;
|
|
410
521
|
writeConfig(config);
|
|
411
522
|
}
|
|
412
523
|
}
|
|
413
524
|
/**
|
|
414
|
-
* 移除固定分支
|
|
525
|
+
* 移除固定分支(仓库级别)
|
|
415
526
|
*/
|
|
416
527
|
function removePinnedBranch(branch) {
|
|
417
528
|
const config = readConfig();
|
|
418
|
-
const
|
|
529
|
+
const repoId = getRepositoryId();
|
|
530
|
+
if (!config.repositoryPinnedBranches?.[repoId]) return;
|
|
531
|
+
const pinnedBranches = config.repositoryPinnedBranches[repoId];
|
|
419
532
|
const index = pinnedBranches.indexOf(branch);
|
|
420
533
|
if (index > -1) {
|
|
421
534
|
pinnedBranches.splice(index, 1);
|
|
422
|
-
config.
|
|
535
|
+
config.repositoryPinnedBranches[repoId] = pinnedBranches;
|
|
423
536
|
writeConfig(config);
|
|
424
537
|
}
|
|
425
538
|
}
|
|
@@ -1252,10 +1365,259 @@ async function handleConfigPromptsCommand() {
|
|
|
1252
1365
|
}
|
|
1253
1366
|
}
|
|
1254
1367
|
|
|
1368
|
+
//#endregion
|
|
1369
|
+
//#region src/utils/prompts/autocomplete-pin.ts
|
|
1370
|
+
function isSelectable(choice) {
|
|
1371
|
+
return choice.type !== "separator" && !choice.disabled;
|
|
1372
|
+
}
|
|
1373
|
+
var AutocompletePinPrompt = class extends Base {
|
|
1374
|
+
currentChoices;
|
|
1375
|
+
firstRender;
|
|
1376
|
+
selected;
|
|
1377
|
+
initialValue;
|
|
1378
|
+
paginator;
|
|
1379
|
+
searchedOnce;
|
|
1380
|
+
searching;
|
|
1381
|
+
lastSearchTerm;
|
|
1382
|
+
lastPromise;
|
|
1383
|
+
nbChoices;
|
|
1384
|
+
done;
|
|
1385
|
+
answer;
|
|
1386
|
+
answerName;
|
|
1387
|
+
shortAnswer;
|
|
1388
|
+
constructor(questions, rl, answers) {
|
|
1389
|
+
super(questions, rl, answers);
|
|
1390
|
+
const opt = this.opt;
|
|
1391
|
+
if (!opt.source) this.throwParamError("source");
|
|
1392
|
+
this.currentChoices = new Choices([], answers);
|
|
1393
|
+
this.firstRender = true;
|
|
1394
|
+
this.selected = 0;
|
|
1395
|
+
this.initialValue = opt.default;
|
|
1396
|
+
if (!opt.suggestOnly) opt.default = null;
|
|
1397
|
+
const shouldLoop = opt.loop === void 0 ? true : opt.loop;
|
|
1398
|
+
this.paginator = new Paginator(this.screen, { isInfinite: shouldLoop });
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Start the Inquiry session
|
|
1402
|
+
* @param {Function} cb Callback when prompt is done
|
|
1403
|
+
* @return {this}
|
|
1404
|
+
*/
|
|
1405
|
+
_run(cb) {
|
|
1406
|
+
this.done = cb;
|
|
1407
|
+
if (Array.isArray(this.rl.history)) this.rl.history = [];
|
|
1408
|
+
const events = observe(this.rl);
|
|
1409
|
+
const dontHaveAnswer = () => this.answer === void 0;
|
|
1410
|
+
events.line.pipe(takeWhile(dontHaveAnswer)).forEach(this.onSubmit.bind(this));
|
|
1411
|
+
events.keypress.pipe(takeWhile(dontHaveAnswer)).forEach(this.onKeypress.bind(this));
|
|
1412
|
+
this.search(void 0);
|
|
1413
|
+
return this;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Render the prompt to screen
|
|
1417
|
+
* @return {undefined}
|
|
1418
|
+
*/
|
|
1419
|
+
render(error) {
|
|
1420
|
+
let content = this.getQuestion();
|
|
1421
|
+
let bottomContent = "";
|
|
1422
|
+
const opt = this.opt;
|
|
1423
|
+
const suggestText = opt.suggestOnly ? ", tab to autocomplete" : "";
|
|
1424
|
+
content += dim(`(Use arrow keys or type to search${suggestText}, Ctrl+P to Pin)`);
|
|
1425
|
+
if (this.status === "answered") content += cyan(this.shortAnswer || this.answerName || this.answer);
|
|
1426
|
+
else if (this.searching) {
|
|
1427
|
+
content += this.rl.line;
|
|
1428
|
+
bottomContent += ` ${dim(opt.searchText || "Searching...")}`;
|
|
1429
|
+
} else if (this.nbChoices) {
|
|
1430
|
+
const choicesStr = listRender(this.currentChoices, this.selected);
|
|
1431
|
+
content += this.rl.line;
|
|
1432
|
+
const indexPosition = this.selected;
|
|
1433
|
+
let realIndexPosition = 0;
|
|
1434
|
+
this.currentChoices.choices.every((choice, index) => {
|
|
1435
|
+
if (index > indexPosition) return false;
|
|
1436
|
+
const name = choice.name;
|
|
1437
|
+
realIndexPosition += name ? name.split("\n").length : 0;
|
|
1438
|
+
return true;
|
|
1439
|
+
});
|
|
1440
|
+
bottomContent += this.paginator.paginate(choicesStr, realIndexPosition, opt.pageSize);
|
|
1441
|
+
} else {
|
|
1442
|
+
content += this.rl.line;
|
|
1443
|
+
bottomContent += ` ${yellow(opt.emptyText || "No results...")}`;
|
|
1444
|
+
}
|
|
1445
|
+
if (error) bottomContent += `\n${red(">> ")}${error}`;
|
|
1446
|
+
this.firstRender = false;
|
|
1447
|
+
this.screen.render(content, bottomContent);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* When user press `enter` key
|
|
1451
|
+
*/
|
|
1452
|
+
onSubmit(line) {
|
|
1453
|
+
let lineOrRl = line || this.rl.line;
|
|
1454
|
+
const opt = this.opt;
|
|
1455
|
+
if (opt.suggestOnly && !lineOrRl) lineOrRl = opt.default === null ? "" : opt.default;
|
|
1456
|
+
if (typeof opt.validate === "function") {
|
|
1457
|
+
const checkValidationResult = (validationResult$1) => {
|
|
1458
|
+
if (validationResult$1 !== true) this.render(validationResult$1 || "Enter something, tab to autocomplete!");
|
|
1459
|
+
else this.onSubmitAfterValidation(lineOrRl);
|
|
1460
|
+
};
|
|
1461
|
+
let validationResult;
|
|
1462
|
+
if (opt.suggestOnly) validationResult = opt.validate(lineOrRl, this.answers);
|
|
1463
|
+
else {
|
|
1464
|
+
const choice = this.currentChoices.getChoice(this.selected);
|
|
1465
|
+
validationResult = opt.validate(choice, this.answers);
|
|
1466
|
+
}
|
|
1467
|
+
if (isPromise(validationResult)) validationResult.then(checkValidationResult);
|
|
1468
|
+
else checkValidationResult(validationResult);
|
|
1469
|
+
} else this.onSubmitAfterValidation(lineOrRl);
|
|
1470
|
+
}
|
|
1471
|
+
onSubmitAfterValidation(line) {
|
|
1472
|
+
const opt = this.opt;
|
|
1473
|
+
let choice = {};
|
|
1474
|
+
if (this.nbChoices && this.nbChoices <= this.selected && !opt.suggestOnly) {
|
|
1475
|
+
this.rl.write(line);
|
|
1476
|
+
this.search(line);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (opt.suggestOnly) {
|
|
1480
|
+
choice.value = line || this.rl.line;
|
|
1481
|
+
this.answer = line || this.rl.line;
|
|
1482
|
+
this.answerName = line || this.rl.line;
|
|
1483
|
+
this.shortAnswer = line || this.rl.line;
|
|
1484
|
+
this.rl.line = "";
|
|
1485
|
+
} else if (this.nbChoices) {
|
|
1486
|
+
choice = this.currentChoices.getChoice(this.selected);
|
|
1487
|
+
this.answer = choice.value;
|
|
1488
|
+
this.answerName = choice.name;
|
|
1489
|
+
this.shortAnswer = choice.short;
|
|
1490
|
+
} else {
|
|
1491
|
+
this.rl.write(line);
|
|
1492
|
+
this.search(line);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
runAsync(opt.filter, (_err, value) => {
|
|
1496
|
+
choice.value = value;
|
|
1497
|
+
this.answer = value;
|
|
1498
|
+
if (opt.suggestOnly) this.shortAnswer = value;
|
|
1499
|
+
this.status = "answered";
|
|
1500
|
+
this.render();
|
|
1501
|
+
this.screen.done();
|
|
1502
|
+
this.done(choice.value);
|
|
1503
|
+
})(choice.value);
|
|
1504
|
+
}
|
|
1505
|
+
search(searchTerm) {
|
|
1506
|
+
const opt = this.opt;
|
|
1507
|
+
let currentValue;
|
|
1508
|
+
if (this.currentChoices && this.nbChoices && this.nbChoices > this.selected) {
|
|
1509
|
+
const currentChoice = this.currentChoices.getChoice(this.selected);
|
|
1510
|
+
if (currentChoice) currentValue = currentChoice.value;
|
|
1511
|
+
}
|
|
1512
|
+
this.selected = 0;
|
|
1513
|
+
if (this.searchedOnce) {
|
|
1514
|
+
this.searching = true;
|
|
1515
|
+
this.currentChoices = new Choices([], this.answers);
|
|
1516
|
+
this.render();
|
|
1517
|
+
} else this.searchedOnce = true;
|
|
1518
|
+
this.lastSearchTerm = searchTerm;
|
|
1519
|
+
let thisPromise;
|
|
1520
|
+
try {
|
|
1521
|
+
const result = opt.source(this.answers, searchTerm);
|
|
1522
|
+
thisPromise = Promise.resolve(result);
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
thisPromise = Promise.reject(error);
|
|
1525
|
+
}
|
|
1526
|
+
this.lastPromise = thisPromise;
|
|
1527
|
+
return thisPromise.then((choices) => {
|
|
1528
|
+
if (thisPromise !== this.lastPromise) return;
|
|
1529
|
+
this.currentChoices = new Choices(choices, this.answers);
|
|
1530
|
+
const realChoices = choices.filter((choice) => isSelectable(choice));
|
|
1531
|
+
this.nbChoices = realChoices.length;
|
|
1532
|
+
let selectedIndex = -1;
|
|
1533
|
+
if (currentValue !== void 0) selectedIndex = realChoices.findIndex((choice) => choice === currentValue || choice.value === currentValue);
|
|
1534
|
+
if (selectedIndex === -1) selectedIndex = realChoices.findIndex((choice) => choice === this.initialValue || choice.value === this.initialValue);
|
|
1535
|
+
if (selectedIndex >= 0) this.selected = selectedIndex;
|
|
1536
|
+
this.searching = false;
|
|
1537
|
+
this.render();
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
ensureSelectedInRange() {
|
|
1541
|
+
const selectedIndex = Math.min(this.selected, this.nbChoices);
|
|
1542
|
+
this.selected = Math.max(selectedIndex, 0);
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* When user type
|
|
1546
|
+
*/
|
|
1547
|
+
onKeypress(e) {
|
|
1548
|
+
const opt = this.opt;
|
|
1549
|
+
let len;
|
|
1550
|
+
const keyName = e.key && e.key.name || void 0;
|
|
1551
|
+
if (keyName === "p" && e.key.ctrl) {
|
|
1552
|
+
if (this.nbChoices && opt.onPin) {
|
|
1553
|
+
const choice = this.currentChoices.getChoice(this.selected);
|
|
1554
|
+
if (choice) Promise.resolve(opt.onPin(choice.value, choice)).then(() => {
|
|
1555
|
+
this.search(this.rl.line);
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (keyName === "tab" && opt.suggestOnly) {
|
|
1561
|
+
if (this.currentChoices.getChoice(this.selected)) {
|
|
1562
|
+
this.rl.write(ansiEscapes.cursorLeft);
|
|
1563
|
+
const autoCompleted = this.currentChoices.getChoice(this.selected).value;
|
|
1564
|
+
this.rl.write(ansiEscapes.cursorForward(autoCompleted.length));
|
|
1565
|
+
this.rl.line = autoCompleted;
|
|
1566
|
+
this.render();
|
|
1567
|
+
}
|
|
1568
|
+
} else if (keyName === "down" || keyName === "n" && e.key.ctrl) {
|
|
1569
|
+
len = this.nbChoices;
|
|
1570
|
+
this.selected = this.selected < len - 1 ? this.selected + 1 : 0;
|
|
1571
|
+
this.ensureSelectedInRange();
|
|
1572
|
+
this.render();
|
|
1573
|
+
utils.up(this.rl, 2);
|
|
1574
|
+
} else if (keyName === "up") {
|
|
1575
|
+
len = this.nbChoices;
|
|
1576
|
+
this.selected = this.selected > 0 ? this.selected - 1 : len - 1;
|
|
1577
|
+
this.ensureSelectedInRange();
|
|
1578
|
+
this.render();
|
|
1579
|
+
} else if (this.lastSearchTerm !== this.rl.line) this.search(this.rl.line);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
/**
|
|
1583
|
+
* Function for rendering list choices
|
|
1584
|
+
* @param {any} choices The choices to render
|
|
1585
|
+
* @param {number} pointer Position of the pointer
|
|
1586
|
+
* @return {string} Rendered content
|
|
1587
|
+
*/
|
|
1588
|
+
function listRender(choices, pointer) {
|
|
1589
|
+
let output = "";
|
|
1590
|
+
let separatorOffset = 0;
|
|
1591
|
+
choices.forEach((choice, i) => {
|
|
1592
|
+
if (choice.type === "separator") {
|
|
1593
|
+
separatorOffset++;
|
|
1594
|
+
output += ` ${choice}\n`;
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
if (choice.disabled) {
|
|
1598
|
+
separatorOffset++;
|
|
1599
|
+
output += ` - ${choice.name}`;
|
|
1600
|
+
output += ` (${typeof choice.disabled === "string" ? choice.disabled : "Disabled"})`;
|
|
1601
|
+
output += "\n";
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const isSelected = i - separatorOffset === pointer;
|
|
1605
|
+
let line = (isSelected ? `${figures.pointer} ` : " ") + choice.name;
|
|
1606
|
+
if (isSelected) line = cyan(line);
|
|
1607
|
+
output += `${line} \n`;
|
|
1608
|
+
});
|
|
1609
|
+
return output.replace(/\n$/, "");
|
|
1610
|
+
}
|
|
1611
|
+
function isPromise(value) {
|
|
1612
|
+
return typeof value === "object" && typeof value.then === "function";
|
|
1613
|
+
}
|
|
1614
|
+
var autocomplete_pin_default = AutocompletePinPrompt;
|
|
1615
|
+
|
|
1255
1616
|
//#endregion
|
|
1256
1617
|
//#region src/utils/pr-cli.ts
|
|
1257
1618
|
inquirer.registerPrompt("autocomplete", inquirerAutoComplete);
|
|
1258
1619
|
inquirer.registerPrompt("search-checkbox", searchCheckbox);
|
|
1620
|
+
inquirer.registerPrompt("autocomplete-pin", autocomplete_pin_default);
|
|
1259
1621
|
/**
|
|
1260
1622
|
* 通用的分支选择函数,支持单选和多选
|
|
1261
1623
|
*/
|
|
@@ -1268,59 +1630,104 @@ async function promptBranchSelection(branches, options) {
|
|
|
1268
1630
|
return mode === "single" ? "" : [];
|
|
1269
1631
|
}
|
|
1270
1632
|
const branchInfos = getBranchesWithInfo(branches);
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1633
|
+
if (mode === "single") {
|
|
1634
|
+
const sortingPinnedBranches = getPinnedBranches();
|
|
1635
|
+
const searchBranches = async (_answers, input = "") => {
|
|
1636
|
+
const currentPinnedBranches = getPinnedBranches();
|
|
1637
|
+
const addCancelOption = (list) => {
|
|
1638
|
+
list.push(new inquirer.Separator(" "));
|
|
1639
|
+
list.push({
|
|
1640
|
+
name: dim(" [Cancel PR creation]"),
|
|
1641
|
+
value: "__CANCEL__",
|
|
1642
|
+
short: "Cancel"
|
|
1643
|
+
});
|
|
1644
|
+
};
|
|
1645
|
+
const createBranchChoice = (branch) => {
|
|
1646
|
+
return {
|
|
1647
|
+
name: `${currentPinnedBranches.includes(branch.name) ? "📌" : " "} ${branch.name.padEnd(45)} ${dim(`(${branch.lastCommitTimeFormatted})`)}`,
|
|
1648
|
+
value: branch.name,
|
|
1649
|
+
short: branch.name
|
|
1650
|
+
};
|
|
1651
|
+
};
|
|
1652
|
+
const topGroupBranches = branchInfos.filter((b) => sortingPinnedBranches.includes(b.name));
|
|
1653
|
+
const bottomGroupBranches = branchInfos.filter((b) => !sortingPinnedBranches.includes(b.name));
|
|
1654
|
+
const effectiveTopGroup = filterPinned ? [] : topGroupBranches;
|
|
1655
|
+
effectiveTopGroup.sort((a, b) => {
|
|
1656
|
+
return sortingPinnedBranches.indexOf(a.name) - sortingPinnedBranches.indexOf(b.name);
|
|
1290
1657
|
});
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
regularBranches.forEach((branch) => {
|
|
1297
|
-
choices.push({
|
|
1298
|
-
name: ` ${branch.name.padEnd(45)} ${dim(`(${branch.lastCommitTimeFormatted})`)}`,
|
|
1299
|
-
value: branch.name,
|
|
1300
|
-
short: branch.name,
|
|
1301
|
-
checked: defaultSelected.includes(branch.name)
|
|
1658
|
+
bottomGroupBranches.sort((a, b) => a.name.localeCompare(b.name));
|
|
1659
|
+
const displayBottomGroup = bottomGroupBranches.slice(0, 100);
|
|
1660
|
+
const choices = [];
|
|
1661
|
+
[...effectiveTopGroup, ...displayBottomGroup].forEach((branch) => {
|
|
1662
|
+
choices.push(createBranchChoice(branch));
|
|
1302
1663
|
});
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
return
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1664
|
+
if (!filterPinned) addCancelOption(choices);
|
|
1665
|
+
const lowerInput = input.toLowerCase();
|
|
1666
|
+
if (lowerInput.trim()) {
|
|
1667
|
+
const searchChoices = branchInfos.filter((branch) => branch.name.toLowerCase().includes(lowerInput)).map(createBranchChoice);
|
|
1668
|
+
addCancelOption(searchChoices);
|
|
1669
|
+
return searchChoices;
|
|
1670
|
+
}
|
|
1671
|
+
return choices.filter((choice) => {
|
|
1672
|
+
if (!choice.value || choice.value === "__CANCEL__") return true;
|
|
1673
|
+
return choice.value.toLowerCase().includes(lowerInput);
|
|
1674
|
+
});
|
|
1675
|
+
};
|
|
1314
1676
|
const { selectedBranch } = await inquirer.prompt([{
|
|
1315
|
-
type: "autocomplete",
|
|
1677
|
+
type: "autocomplete-pin",
|
|
1316
1678
|
name: "selectedBranch",
|
|
1317
1679
|
message,
|
|
1318
1680
|
source: searchBranches,
|
|
1319
1681
|
pageSize: 20,
|
|
1320
|
-
|
|
1682
|
+
onPin: async (branchName) => {
|
|
1683
|
+
if (getPinnedBranches().includes(branchName)) removePinnedBranch(branchName);
|
|
1684
|
+
else addPinnedBranch(branchName);
|
|
1685
|
+
}
|
|
1321
1686
|
}]);
|
|
1322
1687
|
return selectedBranch;
|
|
1323
1688
|
} else {
|
|
1689
|
+
const pinnedBranchNames = getPinnedBranches();
|
|
1690
|
+
const allPinnedBranches = branchInfos.filter((b) => pinnedBranchNames.includes(b.name));
|
|
1691
|
+
const regularBranches = branchInfos.filter((b) => !pinnedBranchNames.includes(b.name));
|
|
1692
|
+
const pinnedBranches = filterPinned ? [] : allPinnedBranches;
|
|
1693
|
+
const choices = [];
|
|
1694
|
+
if (!filterPinned) {
|
|
1695
|
+
choices.push(new inquirer.Separator(" "));
|
|
1696
|
+
choices.push({
|
|
1697
|
+
name: dim(" [Cancel PR creation]"),
|
|
1698
|
+
value: "__CANCEL__",
|
|
1699
|
+
short: "Cancel"
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
pinnedBranches.sort((a, b) => {
|
|
1703
|
+
return pinnedBranchNames.indexOf(a.name) - pinnedBranchNames.indexOf(b.name);
|
|
1704
|
+
});
|
|
1705
|
+
regularBranches.sort((a, b) => a.name.localeCompare(b.name));
|
|
1706
|
+
if (regularBranches.length > 100) regularBranches.splice(100);
|
|
1707
|
+
if (pinnedBranches.length > 0) {
|
|
1708
|
+
choices.push(new inquirer.Separator(magenta("━━━━━━━━ 📌 Pinned Branches ━━━━━━━━")));
|
|
1709
|
+
pinnedBranches.forEach((branch) => {
|
|
1710
|
+
choices.push({
|
|
1711
|
+
name: `📌 ${branch.name.padEnd(45)} ${dim(`(${branch.lastCommitTimeFormatted})`)}`,
|
|
1712
|
+
value: branch.name,
|
|
1713
|
+
short: branch.name,
|
|
1714
|
+
checked: defaultSelected.includes(branch.name)
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
choices.push(new inquirer.Separator(" "));
|
|
1718
|
+
}
|
|
1719
|
+
if (regularBranches.length > 0) {
|
|
1720
|
+
choices.push(new inquirer.Separator(cyan("━━━━━━━━ 🌿 All Branches (Alphabetical) ━━━━━━━━")));
|
|
1721
|
+
regularBranches.forEach((branch) => {
|
|
1722
|
+
choices.push({
|
|
1723
|
+
name: ` ${branch.name.padEnd(45)} ${dim(`(${branch.lastCommitTimeFormatted})`)}`,
|
|
1724
|
+
value: branch.name,
|
|
1725
|
+
short: branch.name,
|
|
1726
|
+
checked: defaultSelected.includes(branch.name)
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
choices.push(new inquirer.Separator(" "));
|
|
1730
|
+
}
|
|
1324
1731
|
const { selectedBranches } = await inquirer.prompt([{
|
|
1325
1732
|
type: "search-checkbox",
|
|
1326
1733
|
name: "selectedBranches",
|
|
@@ -1340,6 +1747,10 @@ async function promptTargetBranch(branches, currentBranch) {
|
|
|
1340
1747
|
message: "Select target branch (type to search):",
|
|
1341
1748
|
mode: "single"
|
|
1342
1749
|
});
|
|
1750
|
+
if (targetBranch === "__CANCEL__") {
|
|
1751
|
+
console.log(yellow("\n🚫 PR creation cancelled."));
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1343
1754
|
if (!targetBranch) {
|
|
1344
1755
|
console.log(yellow("⚠️ No branch selected. Using \"main\" as default."));
|
|
1345
1756
|
return "main";
|
|
@@ -1400,7 +1811,7 @@ async function handlePinCommand(branchName) {
|
|
|
1400
1811
|
addPinnedBranch(branchName);
|
|
1401
1812
|
console.log(green(`✅ Branch '${branchName}' has been pinned`));
|
|
1402
1813
|
} else {
|
|
1403
|
-
const { getAllBranches: getAllBranches$1 } = await import("./pr-
|
|
1814
|
+
const { getAllBranches: getAllBranches$1 } = await import("./pr-Cv5LeZ_A.mjs");
|
|
1404
1815
|
const branches = getAllBranches$1();
|
|
1405
1816
|
if (branches.length === 0) {
|
|
1406
1817
|
console.log(yellow("⚠️ No branches found"));
|
|
@@ -1559,7 +1970,7 @@ async function promptForUpdate(packageName$1, result) {
|
|
|
1559
1970
|
type: "confirm",
|
|
1560
1971
|
name: "shouldUpdate",
|
|
1561
1972
|
message: "Would you like to update now?",
|
|
1562
|
-
default:
|
|
1973
|
+
default: true
|
|
1563
1974
|
}, {
|
|
1564
1975
|
type: "list",
|
|
1565
1976
|
name: "packageManager",
|
|
@@ -1798,7 +2209,7 @@ async function promptPushBranch(branchName) {
|
|
|
1798
2209
|
/**
|
|
1799
2210
|
* 处理 PR 命令
|
|
1800
2211
|
*/
|
|
1801
|
-
async function handlePRCommand() {
|
|
2212
|
+
async function handlePRCommand(targetBranchArg) {
|
|
1802
2213
|
printPRBanner();
|
|
1803
2214
|
const gitInfo = getGitInfo();
|
|
1804
2215
|
if (!gitInfo.isGitRepo) {
|
|
@@ -1827,7 +2238,13 @@ async function handlePRCommand() {
|
|
|
1827
2238
|
console.log(yellow("⚠️ No branches found."));
|
|
1828
2239
|
return;
|
|
1829
2240
|
}
|
|
1830
|
-
|
|
2241
|
+
let targetBranch = null;
|
|
2242
|
+
if (targetBranchArg) if (branches.includes(targetBranchArg)) {
|
|
2243
|
+
targetBranch = targetBranchArg;
|
|
2244
|
+
console.log(green(`✅ Using specified target branch: ${targetBranch}\n`));
|
|
2245
|
+
} else console.log(yellow(`⚠️ Branch '${targetBranchArg}' not found. Falling back to interactive selection.`));
|
|
2246
|
+
if (!targetBranch) targetBranch = await promptTargetBranch(branches, gitInfo.currentBranch);
|
|
2247
|
+
if (!targetBranch) return;
|
|
1831
2248
|
const prInfo = createPullRequest(gitInfo.currentBranch, targetBranch, gitInfo.remoteUrl);
|
|
1832
2249
|
if (!prInfo) {
|
|
1833
2250
|
console.log(red("❌ Failed to create PR information"));
|
|
@@ -1848,12 +2265,16 @@ async function handlePRCommand() {
|
|
|
1848
2265
|
if (!createMergeBranch(targetBranch, prInfo.mergeBranchName)) return;
|
|
1849
2266
|
if (await promptAutoMergeSource(gitInfo.currentBranch, targetBranch)) {
|
|
1850
2267
|
console.log(yellow(`\n🔄 Merging '${gitInfo.currentBranch}' to detect conflicts...`));
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2268
|
+
try {
|
|
2269
|
+
if (!mergeSourceToMergeBranch(gitInfo.currentBranch)) {
|
|
2270
|
+
console.log(yellow("\n⚠️ Merge conflicts detected! Please resolve them manually:"));
|
|
2271
|
+
console.log(dim(` 1. Resolve conflicts in your editor`));
|
|
2272
|
+
console.log(dim(` 2. Run: git add <resolved-files>`));
|
|
2273
|
+
console.log(dim(` 3. Run: git commit`));
|
|
2274
|
+
console.log(dim(` 4. Push the merge branch when ready`));
|
|
2275
|
+
}
|
|
2276
|
+
} catch {
|
|
2277
|
+
return;
|
|
1857
2278
|
}
|
|
1858
2279
|
} else {
|
|
1859
2280
|
console.log(green(`\n✅ Merge branch '${prInfo.mergeBranchName}' created without merging.`));
|
|
@@ -1866,8 +2287,13 @@ async function handlePRCommand() {
|
|
|
1866
2287
|
}
|
|
1867
2288
|
yargs(hideBin(process.argv)).scriptName("qkpr").usage("Usage: $0 <command> [options]").command("$0", "Show interactive menu to choose features", () => {}, async () => {
|
|
1868
2289
|
await showMainMenu();
|
|
1869
|
-
}).command("pr", "🔧 Create a Pull Request with interactive branch selection", () => {
|
|
1870
|
-
|
|
2290
|
+
}).command("pr [branch]", "🔧 Create a Pull Request with interactive branch selection", (yargs$1) => {
|
|
2291
|
+
return yargs$1.positional("branch", {
|
|
2292
|
+
describe: "Target branch name",
|
|
2293
|
+
type: "string"
|
|
2294
|
+
});
|
|
2295
|
+
}, async (argv) => {
|
|
2296
|
+
await handlePRCommand(argv.branch);
|
|
1871
2297
|
await checkAndNotifyUpdate(packageName, version);
|
|
1872
2298
|
}).command("commit", "🤖 Generate commit message using AI", () => {}, async () => {
|
|
1873
2299
|
await handleCommitCommand();
|
|
@@ -1909,4 +2335,4 @@ yargs(hideBin(process.argv)).scriptName("qkpr").usage("Usage: $0 <command> [opti
|
|
|
1909
2335
|
}).version(version).alias("v", "version").help("h").alias("h", "help").epilog("For more information, visit https://github.com/KazooTTT/qkpr").argv;
|
|
1910
2336
|
|
|
1911
2337
|
//#endregion
|
|
1912
|
-
export {
|
|
2338
|
+
export { generateMergeBranchName as a, getAllBranches as c, getBranchesWithInfo as d, getBranchesWithInfoBatch as f, parseRemoteUrl as g, mergeSourceToMergeBranch as h, formatTimestamp as i, getBranchCategory as l, getGitInfo as m, createMergeBranch as n, generatePRMessage as o, getCommitsBetweenBranches as p, createPullRequest as r, generatePRUrl as s, copyToClipboard as t, getBranchLastCommitTime as u };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as generateMergeBranchName, c as getAllBranches, d as getBranchesWithInfo, f as getBranchesWithInfoBatch, g as parseRemoteUrl, h as mergeSourceToMergeBranch, i as formatTimestamp, l as getBranchCategory, m as getGitInfo, n as createMergeBranch, o as generatePRMessage, p as getCommitsBetweenBranches, r as createPullRequest, s as generatePRUrl, t as copyToClipboard, u as getBranchLastCommitTime } from "./index.mjs";
|
|
2
|
+
|
|
3
|
+
export { copyToClipboard, createMergeBranch, createPullRequest, formatTimestamp, generateMergeBranchName, generatePRMessage, generatePRUrl, getAllBranches, getBranchCategory, getBranchLastCommitTime, getBranchesWithInfo, getBranchesWithInfoBatch, getCommitsBetweenBranches, getGitInfo, mergeSourceToMergeBranch, parseRemoteUrl };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qkpr",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.6",
|
|
5
5
|
"description": "Create a Pull Request with interactive branch selection",
|
|
6
6
|
"author": "KazooTTT <work@kazoottt.top>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@google/generative-ai": "^0.24.1",
|
|
37
|
+
"ansi-escapes": "^7.2.0",
|
|
38
|
+
"figures": "^6.1.0",
|
|
37
39
|
"inquirer": "^9.2.20",
|
|
38
40
|
"inquirer-autocomplete-prompt": "^3.0.1",
|
|
39
41
|
"inquirer-search-checkbox": "^1.0.0",
|
|
@@ -41,6 +43,8 @@
|
|
|
41
43
|
"kolorist": "^1.8.0",
|
|
42
44
|
"open": "^8.4.2",
|
|
43
45
|
"ora": "^9.0.0",
|
|
46
|
+
"run-async": "^4.0.6",
|
|
47
|
+
"rxjs": "^7.8.2",
|
|
44
48
|
"yargs": "^17.7.2"
|
|
45
49
|
},
|
|
46
50
|
"devDependencies": {
|
package/dist/pr-C2AR97YR.mjs
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import { a as generatePRMessage, c as getBranchCategory, d as getCommitsBetweenBranches, f as getGitInfo, i as generateMergeBranchName, l as getBranchLastCommitTime, m as parseRemoteUrl, n as createMergeBranch, o as generatePRUrl, p as mergeSourceToMergeBranch, r as createPullRequest, s as getAllBranches, t as copyToClipboard, u as getBranchesWithInfo } from "./index.mjs";
|
|
2
|
-
|
|
3
|
-
export { copyToClipboard, createMergeBranch, createPullRequest, generateMergeBranchName, generatePRMessage, generatePRUrl, getAllBranches, getBranchCategory, getBranchLastCommitTime, getBranchesWithInfo, getCommitsBetweenBranches, getGitInfo, mergeSourceToMergeBranch, parseRemoteUrl };
|