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 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
- return readConfig().pinnedBranches || [];
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 pinnedBranches = config.pinnedBranches || [];
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.pinnedBranches = pinnedBranches;
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 pinnedBranches = config.pinnedBranches || [];
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.pinnedBranches = pinnedBranches;
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
- const pinnedBranchNames = getPinnedBranches();
1272
- const allPinnedBranches = branchInfos.filter((b) => pinnedBranchNames.includes(b.name));
1273
- const regularBranches = branchInfos.filter((b) => !pinnedBranchNames.includes(b.name));
1274
- const pinnedBranches = filterPinned ? [] : allPinnedBranches;
1275
- pinnedBranches.sort((a, b) => {
1276
- return pinnedBranchNames.indexOf(a.name) - pinnedBranchNames.indexOf(b.name);
1277
- });
1278
- regularBranches.sort((a, b) => a.name.localeCompare(b.name));
1279
- const MAX_BRANCHES = 100;
1280
- if (regularBranches.length > MAX_BRANCHES) regularBranches.splice(MAX_BRANCHES);
1281
- const choices = [];
1282
- if (pinnedBranches.length > 0) {
1283
- choices.push(new inquirer.Separator(magenta("━━━━━━━━ 📌 Pinned Branches ━━━━━━━━")));
1284
- pinnedBranches.forEach((branch) => {
1285
- choices.push({
1286
- name: `📌 ${branch.name.padEnd(45)} ${dim(`(${branch.lastCommitTimeFormatted})`)}`,
1287
- value: branch.name,
1288
- short: branch.name,
1289
- checked: defaultSelected.includes(branch.name)
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
- choices.push(new inquirer.Separator(" "));
1293
- }
1294
- if (regularBranches.length > 0) {
1295
- choices.push(new inquirer.Separator(cyan("━━━━━━━━ 🌿 All Branches (Alphabetical) ━━━━━━━━")));
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
- choices.push(new inquirer.Separator(" "));
1305
- }
1306
- const searchBranches = async (_answers, input = "") => {
1307
- const lowerInput = input.toLowerCase();
1308
- return choices.filter((choice) => {
1309
- if (!choice.value) return true;
1310
- return choice.value.toLowerCase().includes(lowerInput);
1311
- });
1312
- };
1313
- if (mode === "single") {
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
- default: pinnedBranches.length > 0 ? pinnedBranches[0].name : regularBranches[0]?.name
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-C2AR97YR.mjs");
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: false
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
- const targetBranch = await promptTargetBranch(branches, gitInfo.currentBranch);
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
- if (!mergeSourceToMergeBranch(gitInfo.currentBranch)) {
1852
- console.log(yellow("\n⚠️ Merge conflicts detected! Please resolve them manually:"));
1853
- console.log(dim(` 1. Resolve conflicts in your editor`));
1854
- console.log(dim(` 2. Run: git add <resolved-files>`));
1855
- console.log(dim(` 3. Run: git commit`));
1856
- console.log(dim(` 4. Push the merge branch when ready`));
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", () => {}, async () => {
1870
- await handlePRCommand();
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 { generatePRMessage as a, getBranchCategory as c, getCommitsBetweenBranches as d, getGitInfo as f, generateMergeBranchName as i, getBranchLastCommitTime as l, parseRemoteUrl as m, createMergeBranch as n, generatePRUrl as o, mergeSourceToMergeBranch as p, createPullRequest as r, getAllBranches as s, copyToClipboard as t, getBranchesWithInfo as u };
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.5-beta.1",
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": {
@@ -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 };