clawt 3.4.6 → 3.5.1

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.
Files changed (53) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/README.md +0 -4
  3. package/dist/index.js +583 -314
  4. package/dist/postinstall.js +37 -2
  5. package/docs/alias.md +7 -1
  6. package/docs/completion.md +1 -1
  7. package/docs/config.md +4 -3
  8. package/docs/cover-validate.md +4 -3
  9. package/docs/create.md +28 -12
  10. package/docs/home.md +12 -8
  11. package/docs/init.md +16 -9
  12. package/docs/list.md +13 -7
  13. package/docs/merge.md +12 -12
  14. package/docs/remove.md +24 -13
  15. package/docs/reset.md +6 -4
  16. package/docs/resume.md +3 -4
  17. package/docs/status.md +75 -30
  18. package/docs/sync.md +26 -26
  19. package/docs/validate.md +13 -7
  20. package/package.json +1 -1
  21. package/src/commands/merge.ts +20 -5
  22. package/src/commands/tasks.ts +51 -0
  23. package/src/constants/ai-prompts.ts +14 -0
  24. package/src/constants/config.ts +9 -0
  25. package/src/constants/index.ts +4 -0
  26. package/src/constants/interactive-panel.ts +6 -0
  27. package/src/constants/messages/index.ts +4 -2
  28. package/src/constants/messages/interactive-panel.ts +12 -0
  29. package/src/constants/messages/merge.ts +15 -0
  30. package/src/constants/messages/tasks.ts +9 -0
  31. package/src/constants/tasks-template.ts +28 -0
  32. package/src/index.ts +2 -0
  33. package/src/types/command.ts +8 -0
  34. package/src/types/config.ts +4 -0
  35. package/src/types/index.ts +1 -1
  36. package/src/utils/conflict-resolver.ts +170 -0
  37. package/src/utils/formatter.ts +19 -0
  38. package/src/utils/git-branch.ts +116 -0
  39. package/src/utils/git-core.ts +417 -0
  40. package/src/utils/git-worktree.ts +40 -0
  41. package/src/utils/git.ts +3 -521
  42. package/src/utils/index.ts +7 -2
  43. package/src/utils/interactive-panel-render.ts +12 -6
  44. package/src/utils/interactive-panel-state.ts +137 -0
  45. package/src/utils/interactive-panel.ts +44 -188
  46. package/src/utils/keyboard-controller.ts +48 -0
  47. package/src/utils/ui-prompts.ts +240 -0
  48. package/src/utils/worktree-matcher.ts +21 -251
  49. package/tests/unit/commands/merge.test.ts +59 -3
  50. package/tests/unit/commands/tasks.test.ts +153 -0
  51. package/tests/unit/utils/conflict-resolver.test.ts +250 -0
  52. package/tests/unit/utils/formatter.test.ts +26 -1
  53. package/src/constants/messages.ts +0 -179
package/dist/index.js CHANGED
@@ -173,7 +173,22 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
173
173
  /** merge 交互选择提示 */
174
174
  MERGE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u5408\u5E76\u7684\u5206\u652F",
175
175
  /** merge 模糊匹配到多个结果提示 */
176
- MERGE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
176
+ MERGE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
177
+ /** 询问是否使用 AI 辅助解决冲突 */
178
+ MERGE_CONFLICT_ASK_AI: "\u68C0\u6D4B\u5230\u5408\u5E76\u51B2\u7A81\uFF0C\u662F\u5426\u4F7F\u7528 Claude Code \u81EA\u52A8\u89E3\u51B3\uFF1F",
179
+ /** AI 冲突解决开始 */
180
+ MERGE_CONFLICT_AI_START: (fileCount) => `\u6B63\u5728\u4F7F\u7528 Claude Code \u5206\u6790\u5E76\u89E3\u51B3 ${fileCount} \u4E2A\u51B2\u7A81\u6587\u4EF6...`,
181
+ /** AI 冲突解决成功 */
182
+ MERGE_CONFLICT_AI_SUCCESS: "\u2713 Claude Code \u5DF2\u6210\u529F\u89E3\u51B3\u6240\u6709\u51B2\u7A81",
183
+ /** AI 冲突解决后仍有未解决的冲突 */
184
+ MERGE_CONFLICT_AI_PARTIAL: (remaining) => `Claude Code \u5DF2\u5904\u7406\u51B2\u7A81\u6587\u4EF6\uFF0C\u4F46\u4ECD\u6709 ${remaining} \u4E2A\u6587\u4EF6\u5B58\u5728\u51B2\u7A81
185
+ \u8BF7\u624B\u52A8\u5904\u7406\u5269\u4F59\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue`,
186
+ /** AI 冲突解决失败 */
187
+ MERGE_CONFLICT_AI_FAILED: (errorMsg) => `Claude Code \u89E3\u51B3\u51B2\u7A81\u5931\u8D25: ${errorMsg}
188
+ \u8BF7\u624B\u52A8\u5904\u7406\uFF1A
189
+ \u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue`,
190
+ /** --auto 模式下的冲突手动解决(配置为 manual) */
191
+ MERGE_CONFLICT_MANUAL: "\u5408\u5E76\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u624B\u52A8\u5904\u7406\uFF1A\n \u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue"
177
192
  };
178
193
 
179
194
  // src/constants/messages/validate.ts
@@ -513,6 +528,16 @@ var HOME_MESSAGES = {
513
528
  HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}`
514
529
  };
515
530
 
531
+ // src/constants/messages/tasks.ts
532
+ var TASKS_CMD_MESSAGES = {
533
+ /** 任务模板文件已存在 */
534
+ TASK_INIT_FILE_EXISTS: (path2) => `\u6587\u4EF6\u5DF2\u5B58\u5728: ${path2}\uFF0C\u5982\u9700\u8986\u76D6\u8BF7\u5148\u5220\u9664`,
535
+ /** 任务模板生成成功 */
536
+ TASK_INIT_SUCCESS: (path2) => `\u2713 \u4EFB\u52A1\u6A21\u677F\u5DF2\u751F\u6210: ${path2}`,
537
+ /** 任务模板使用提示 */
538
+ TASK_INIT_HINT: (path2) => `\u4F7F\u7528 clawt run -f ${path2} \u6267\u884C\u4EFB\u52A1`
539
+ };
540
+
516
541
  // src/constants/messages/interactive-panel.ts
517
542
  import chalk from "chalk";
518
543
 
@@ -544,6 +569,8 @@ var PANEL_SHORTCUT_KEYS = {
544
569
  };
545
570
  var PANEL_DATE_SEPARATOR_PREFIX = "\u2550\u2550\u2550\u2550";
546
571
  var PANEL_FIXED_ROWS = 5;
572
+ var PANEL_SEPARATOR_MAX_WIDTH = 60;
573
+ var PANEL_DATE_COLOR = "#FF8C00";
547
574
 
548
575
  // src/constants/messages/interactive-panel.ts
549
576
  var SHORTCUT_LABELS = {
@@ -575,6 +602,10 @@ var PANEL_CONFIGURED_BRANCH = (branchName) => chalk.gray(`\u4E3B\u5DE5\u4F5C\u52
575
602
  var PANEL_CONFIGURED_BRANCH_DELETED = (branchName) => chalk.red(`\u2717 \u4E3B\u5DE5\u4F5C\u5206\u652F: ${branchName}\uFF08\u5DF2\u4E0D\u5B58\u5728\uFF09`);
576
603
  var PANEL_CONFIGURED_BRANCH_MISMATCH = (branchName) => chalk.yellow(`\u26A0 \u4E3B\u5DE5\u4F5C\u5206\u652F: ${branchName}\uFF08\u4E0D\u4E00\u81F4\uFF09`);
577
604
  var PANEL_NOT_INITIALIZED = chalk.gray("\u672A\u521D\u59CB\u5316\uFF08\u6267\u884C clawt init \u8BBE\u7F6E\u4E3B\u5DE5\u4F5C\u5206\u652F\uFF09");
605
+ var PANEL_UNKNOWN_DATE = "\u672A\u77E5\u65E5\u671F";
606
+ var PANEL_SYNCED_WITH_MAIN = "\u4E0E\u4E3B\u5206\u652F\u540C\u6B65";
607
+ var PANEL_COMMITS_AHEAD = (count) => `${count} \u4E2A\u672C\u5730\u63D0\u4EA4`;
608
+ var PANEL_COMMITS_BEHIND = (count) => `\u843D\u540E\u4E3B\u5206\u652F ${count} \u4E2A\u63D0\u4EA4`;
578
609
 
579
610
  // src/constants/messages/index.ts
580
611
  var MESSAGES = {
@@ -594,7 +625,8 @@ var MESSAGES = {
594
625
  ...COMPLETION_MESSAGES,
595
626
  ...INIT_MESSAGES,
596
627
  ...COVER_VALIDATE_MESSAGES,
597
- ...HOME_MESSAGES
628
+ ...HOME_MESSAGES,
629
+ ...TASKS_CMD_MESSAGES
598
630
  };
599
631
 
600
632
  // src/constants/exitCodes.ts
@@ -650,6 +682,15 @@ var CONFIG_DEFINITIONS = {
650
682
  autoUpdate: {
651
683
  defaultValue: true,
652
684
  description: "\u662F\u5426\u542F\u7528\u81EA\u52A8\u66F4\u65B0\u68C0\u67E5\uFF08\u6BCF 24 \u5C0F\u65F6\u68C0\u67E5\u4E00\u6B21 npm registry\uFF09"
685
+ },
686
+ conflictResolveMode: {
687
+ defaultValue: "ask",
688
+ description: "merge \u51B2\u7A81\u65F6\u7684\u89E3\u51B3\u6A21\u5F0F\uFF1Aask\uFF08\u8BE2\u95EE\u662F\u5426\u4F7F\u7528 AI\uFF09\u3001auto\uFF08\u81EA\u52A8 AI \u89E3\u51B3\uFF09\u3001manual\uFF08\u624B\u52A8\u89E3\u51B3\uFF09",
689
+ allowedValues: ["ask", "auto", "manual"]
690
+ },
691
+ conflictResolveTimeoutMs: {
692
+ defaultValue: 3e5,
693
+ description: "Claude Code \u51B2\u7A81\u89E3\u51B3\u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09\uFF0C\u9ED8\u8BA4 300000\uFF085 \u5206\u949F\uFF09"
653
694
  }
654
695
  };
655
696
  function deriveDefaultConfig(definitions) {
@@ -751,6 +792,21 @@ var GROUP_SEPARATOR_LABEL = (dateLabel, relativeTime) => `\u2550\u2550\u2550\u25
751
792
  var UNKNOWN_DATE_GROUP = "\u672A\u77E5\u65E5\u671F";
752
793
  var UNKNOWN_DATE_SEPARATOR_LABEL = `\u2550\u2550\u2550\u2550 ${chalk2.bold.hex("#FF8C00")("\u672A\u77E5\u65E5\u671F")} \u2550\u2550\u2550\u2550`;
753
794
 
795
+ // src/constants/ai-prompts.ts
796
+ var CONFLICT_RESOLVE_PROMPT = `\u4F60\u662F\u4E00\u4E2A Git \u5408\u5E76\u51B2\u7A81\u89E3\u51B3\u4E13\u5BB6\u3002\u5F53\u524D\u4ED3\u5E93\u5904\u4E8E\u5408\u5E76\u51B2\u7A81\u72B6\u6001\u3002
797
+
798
+ ## \u4EFB\u52A1
799
+
800
+ 1. \u901A\u8FC7 git status \u548C git diff \u7B49\u547D\u4EE4\uFF0C\u81EA\u884C\u67E5\u770B\u5F53\u524D\u4ED3\u5E93\u7684\u51B2\u7A81\u6587\u4EF6\u5217\u8868\u53CA\u51B2\u7A81\u5185\u5BB9
801
+ 2. \u901A\u8FC7 git log \u7B49\u547D\u4EE4\uFF0C\u5206\u6790\u4E24\u4E2A\u5206\u652F\u5404\u81EA\u7684\u53D8\u66F4\u610F\u56FE
802
+ 3. \u76F4\u63A5\u7F16\u8F91\u6BCF\u4E2A\u51B2\u7A81\u6587\u4EF6\uFF0C\u79FB\u9664\u6240\u6709\u51B2\u7A81\u6807\u8BB0\uFF08<<<<<<<\u3001=======\u3001>>>>>>>\uFF09
803
+ 4. \u4FDD\u7559\u53CC\u65B9\u6709\u610F\u4E49\u7684\u53D8\u66F4\uFF0C\u5408\u7406\u5408\u5E76\u4EE3\u7801\u903B\u8F91
804
+ 5. \u5982\u679C\u4E24\u4E2A\u5206\u652F\u4FEE\u6539\u4E86\u540C\u4E00\u6BB5\u4EE3\u7801\u4F46\u610F\u56FE\u4E0D\u540C\uFF0C\u4F18\u5148\u4FDD\u8BC1\u4EE3\u7801\u7684\u6B63\u786E\u6027\u548C\u5B8C\u6574\u6027
805
+ 6. \u89E3\u51B3\u51B2\u7A81\u540E\uFF0C\u786E\u4FDD\u4EE3\u7801\u8BED\u6CD5\u6B63\u786E\u3001\u903B\u8F91\u5B8C\u6574
806
+ 7. \u4E0D\u8981\u6DFB\u52A0\u4EFB\u4F55\u6CE8\u91CA\u8BF4\u660E\u4F60\u505A\u4E86\u4EC0\u4E48\u4FEE\u6539\uFF0C\u53EA\u9700\u8981\u4FEE\u6539\u6587\u4EF6\u5185\u5BB9
807
+
808
+ \u8BF7\u76F4\u63A5\u5F00\u59CB\u3002`;
809
+
754
810
  // src/constants/pre-checks.ts
755
811
  var PRE_CHECK_CREATE = {
756
812
  requireMainWorktree: true,
@@ -777,6 +833,31 @@ var PRE_CHECK_RESUME = {
777
833
  requireClaudeCode: true
778
834
  };
779
835
 
836
+ // src/constants/tasks-template.ts
837
+ var TASK_TEMPLATE_OUTPUT_DIR = "clawt/tasks";
838
+ var TASK_TEMPLATE_FILENAME_PREFIX = "clawt-tasks";
839
+ var TASK_TEMPLATE_CONTENT = `# Clawt \u4EFB\u52A1\u6587\u4EF6
840
+ #
841
+ # \u4F7F\u7528\u65B9\u6CD5: clawt run -f tasks.md
842
+ # \u683C\u5F0F\u8BF4\u660E: \u6807\u7B7E\u5916\u7684\u6587\u672C\u4F1A\u88AB\u5FFD\u7565\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u7528 START/END \u6807\u7B7E\u5305\u88F9
843
+ #
844
+ # \u89C4\u5219:
845
+ # 1. \u6BCF\u4E2A\u4EFB\u52A1\u5757\u7528 <!-- CLAWT-TASKS:START --> \u548C <!-- CLAWT-TASKS:END --> \u5305\u88F9
846
+ # 2. \u5757\u5185 # branch: <\u5206\u652F\u540D> \u58F0\u660E\u5206\u652F\u540D\uFF08\u4F7F\u7528 -b \u53C2\u6570\u65F6\u53EF\u7701\u7565\uFF09
847
+ # 3. \u5757\u5185\u5176\u4F59\u884C\u4E3A\u4EFB\u52A1\u63CF\u8FF0\uFF08\u652F\u6301\u591A\u884C\uFF09
848
+
849
+ <!-- CLAWT-TASKS:START -->
850
+ # branch: feat-example-1
851
+ \u5728\u8FD9\u91CC\u5199\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u7684\u63CF\u8FF0
852
+ <!-- CLAWT-TASKS:END -->
853
+
854
+ <!-- CLAWT-TASKS:START -->
855
+ # branch: feat-example-2
856
+ \u5728\u8FD9\u91CC\u5199\u7B2C\u4E8C\u4E2A\u4EFB\u52A1\u7684\u63CF\u8FF0
857
+ \u652F\u6301\u591A\u884C\u63CF\u8FF0
858
+ <!-- CLAWT-TASKS:END -->
859
+ `;
860
+
780
861
  // src/errors/index.ts
781
862
  var ClawtError = class extends Error {
782
863
  /** 退出码 */
@@ -900,7 +981,7 @@ function parseParallelCommands(commandString) {
900
981
  }
901
982
  function runParallelCommands(commands, options) {
902
983
  const promises = commands.map((command) => {
903
- return new Promise((resolve3) => {
984
+ return new Promise((resolve4) => {
904
985
  logger.debug(`\u5E76\u884C\u542F\u52A8\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
905
986
  const child = spawn(command, {
906
987
  cwd: options?.cwd,
@@ -908,19 +989,19 @@ function runParallelCommands(commands, options) {
908
989
  shell: true
909
990
  });
910
991
  child.on("error", (err) => {
911
- resolve3({ command, exitCode: 1, error: err.message });
992
+ resolve4({ command, exitCode: 1, error: err.message });
912
993
  });
913
994
  child.on("close", (code) => {
914
- resolve3({ command, exitCode: code ?? 1 });
995
+ resolve4({ command, exitCode: code ?? 1 });
915
996
  });
916
997
  });
917
998
  });
918
999
  return Promise.all(promises);
919
1000
  }
920
1001
 
921
- // src/utils/git.ts
1002
+ // src/utils/git-core.ts
922
1003
  import { basename } from "path";
923
- import { execSync as execSync2 } from "child_process";
1004
+ import { execSync as execSync2, execFileSync as execFileSync2 } from "child_process";
924
1005
  function getGitCommonDir(cwd) {
925
1006
  return execCommand("git rev-parse --git-common-dir", { cwd });
926
1007
  }
@@ -931,26 +1012,6 @@ function getProjectName(cwd) {
931
1012
  const topLevel = getGitTopLevel(cwd);
932
1013
  return basename(topLevel);
933
1014
  }
934
- function checkBranchExists(branchName, cwd) {
935
- try {
936
- execCommand(`git show-ref --verify refs/heads/${branchName}`, { cwd });
937
- return true;
938
- } catch {
939
- return false;
940
- }
941
- }
942
- function createWorktree(branchName, worktreePath, cwd) {
943
- logger.info(`\u521B\u5EFA worktree: ${worktreePath}`);
944
- execCommand(`git worktree add -b ${branchName} "${worktreePath}"`, { cwd });
945
- }
946
- function removeWorktreeByPath(worktreePath, cwd) {
947
- logger.info(`\u79FB\u9664 worktree: ${worktreePath}`);
948
- execCommand(`git worktree remove -f "${worktreePath}"`, { cwd });
949
- }
950
- function deleteBranch(branchName, cwd) {
951
- logger.info(`\u5220\u9664\u5206\u652F: ${branchName}`);
952
- execCommand(`git branch -D ${branchName}`, { cwd });
953
- }
954
1015
  function getStatusPorcelain(cwd) {
955
1016
  return execCommand("git status --porcelain", { cwd });
956
1017
  }
@@ -988,32 +1049,6 @@ function gitStashPush(message, cwd) {
988
1049
  function gitRestoreStaged(cwd) {
989
1050
  execCommand("git restore --staged .", { cwd });
990
1051
  }
991
- function gitWorktreeList(cwd) {
992
- return execCommand("git worktree list", { cwd });
993
- }
994
- function gitWorktreePrune(cwd) {
995
- execCommand("git worktree prune", { cwd });
996
- }
997
- function hasLocalCommits(branchName, cwd) {
998
- try {
999
- const output = execCommand(`git log HEAD..${branchName} --oneline`, { cwd });
1000
- return output.trim() !== "";
1001
- } catch {
1002
- return false;
1003
- }
1004
- }
1005
- function getCommitCountAhead(branchName, cwd) {
1006
- const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
1007
- return parseInt(output, 10) || 0;
1008
- }
1009
- function getCommitCountBehind(branchName, cwd) {
1010
- try {
1011
- const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
1012
- return parseInt(output, 10) || 0;
1013
- } catch {
1014
- return 0;
1015
- }
1016
- }
1017
1052
  function parseShortStat(output) {
1018
1053
  let insertions = 0;
1019
1054
  let deletions = 0;
@@ -1034,9 +1069,6 @@ function getDiffStat(worktreePath) {
1034
1069
  function gitApplyCachedFromStdin(patchContent, cwd) {
1035
1070
  execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
1036
1071
  }
1037
- function getCurrentBranch(cwd) {
1038
- return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
1039
- }
1040
1072
  function getHeadCommitHash(cwd) {
1041
1073
  return execCommand("git rev-parse HEAD", { cwd });
1042
1074
  }
@@ -1092,6 +1124,61 @@ function gitApplyCachedCheck(patchContent, cwd) {
1092
1124
  return false;
1093
1125
  }
1094
1126
  }
1127
+ function getConflictFiles(cwd) {
1128
+ const status = getStatusPorcelain(cwd);
1129
+ if (!status) return [];
1130
+ return status.split("\n").filter((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line)).map((line) => line.slice(3));
1131
+ }
1132
+ function gitAddFiles(files, cwd) {
1133
+ if (files.length === 0) return;
1134
+ const args = ["add", "--", ...files];
1135
+ logger.debug(`\u6267\u884C\u547D\u4EE4: git ${args.join(" ")}${cwd ? ` (cwd: ${cwd})` : ""}`);
1136
+ execFileSync2("git", args, {
1137
+ cwd,
1138
+ encoding: "utf-8",
1139
+ stdio: ["pipe", "pipe", "pipe"]
1140
+ });
1141
+ }
1142
+ function gitMergeContinue(cwd) {
1143
+ execCommand("GIT_EDITOR=true git merge --continue", { cwd });
1144
+ }
1145
+
1146
+ // src/utils/git-branch.ts
1147
+ function checkBranchExists(branchName, cwd) {
1148
+ try {
1149
+ execCommand(`git show-ref --verify refs/heads/${branchName}`, { cwd });
1150
+ return true;
1151
+ } catch {
1152
+ return false;
1153
+ }
1154
+ }
1155
+ function deleteBranch(branchName, cwd) {
1156
+ logger.info(`\u5220\u9664\u5206\u652F: ${branchName}`);
1157
+ execCommand(`git branch -D ${branchName}`, { cwd });
1158
+ }
1159
+ function hasLocalCommits(branchName, cwd) {
1160
+ try {
1161
+ const output = execCommand(`git log HEAD..${branchName} --oneline`, { cwd });
1162
+ return output.trim() !== "";
1163
+ } catch {
1164
+ return false;
1165
+ }
1166
+ }
1167
+ function getCommitCountAhead(branchName, cwd) {
1168
+ const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
1169
+ return parseInt(output, 10) || 0;
1170
+ }
1171
+ function getCommitCountBehind(branchName, cwd) {
1172
+ try {
1173
+ const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
1174
+ return parseInt(output, 10) || 0;
1175
+ } catch {
1176
+ return 0;
1177
+ }
1178
+ }
1179
+ function getCurrentBranch(cwd) {
1180
+ return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
1181
+ }
1095
1182
  function gitCheckout(branchName, cwd) {
1096
1183
  execCommand(`git checkout ${branchName}`, { cwd });
1097
1184
  }
@@ -1099,6 +1186,22 @@ function createBranch(branchName, cwd) {
1099
1186
  execCommand(`git branch ${branchName}`, { cwd });
1100
1187
  }
1101
1188
 
1189
+ // src/utils/git-worktree.ts
1190
+ function createWorktree(branchName, worktreePath, cwd) {
1191
+ logger.info(`\u521B\u5EFA worktree: ${worktreePath}`);
1192
+ execCommand(`git worktree add -b ${branchName} "${worktreePath}"`, { cwd });
1193
+ }
1194
+ function removeWorktreeByPath(worktreePath, cwd) {
1195
+ logger.info(`\u79FB\u9664 worktree: ${worktreePath}`);
1196
+ execCommand(`git worktree remove -f "${worktreePath}"`, { cwd });
1197
+ }
1198
+ function gitWorktreeList(cwd) {
1199
+ return execCommand("git worktree list", { cwd });
1200
+ }
1201
+ function gitWorktreePrune(cwd) {
1202
+ execCommand("git worktree prune", { cwd });
1203
+ }
1204
+
1102
1205
  // src/utils/formatter.ts
1103
1206
  import chalk4 from "chalk";
1104
1207
  import { createInterface } from "readline";
@@ -1124,14 +1227,14 @@ function printDoubleSeparator() {
1124
1227
  console.log(MESSAGES.DOUBLE_SEPARATOR);
1125
1228
  }
1126
1229
  function confirmAction(question) {
1127
- return new Promise((resolve3) => {
1230
+ return new Promise((resolve4) => {
1128
1231
  const rl = createInterface({
1129
1232
  input: process.stdin,
1130
1233
  output: process.stdout
1131
1234
  });
1132
1235
  rl.question(`${question} (y/N) `, (answer) => {
1133
1236
  rl.close();
1134
- resolve3(answer.toLowerCase() === "y");
1237
+ resolve4(answer.toLowerCase() === "y");
1135
1238
  });
1136
1239
  });
1137
1240
  }
@@ -1222,6 +1325,19 @@ function formatLocalISOString(date) {
1222
1325
  const minutes = String(absMinutes % 60).padStart(2, "0");
1223
1326
  return `${iso}${sign}${hours}:${minutes}`;
1224
1327
  }
1328
+ function generateTaskFilename(prefix) {
1329
+ const now = /* @__PURE__ */ new Date();
1330
+ const pad = (n) => String(n).padStart(2, "0");
1331
+ const timestamp = [
1332
+ now.getFullYear(),
1333
+ pad(now.getMonth() + 1),
1334
+ pad(now.getDate()),
1335
+ pad(now.getHours()),
1336
+ pad(now.getMinutes()),
1337
+ pad(now.getSeconds())
1338
+ ].join("-");
1339
+ return `${prefix}-${timestamp}.md`;
1340
+ }
1225
1341
 
1226
1342
  // src/utils/branch.ts
1227
1343
  function sanitizeBranchName(branchName) {
@@ -1646,7 +1762,7 @@ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "fs";
1646
1762
  import { join as join5 } from "path";
1647
1763
 
1648
1764
  // src/utils/terminal.ts
1649
- import { execFileSync as execFileSync2 } from "child_process";
1765
+ import { execFileSync as execFileSync3 } from "child_process";
1650
1766
  import { existsSync as existsSync6 } from "fs";
1651
1767
  function isITerm2Installed() {
1652
1768
  return existsSync6(ITERM2_APP_PATH);
@@ -1705,7 +1821,7 @@ function openCommandInNewTerminalTab(command, tabTitle) {
1705
1821
  logger.debug(`\u6253\u5F00\u7EC8\u7AEF Tab [${terminalApp}]: ${tabTitle}`);
1706
1822
  logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}`);
1707
1823
  try {
1708
- execFileSync2("osascript", ["-e", script], {
1824
+ execFileSync3("osascript", ["-e", script], {
1709
1825
  encoding: "utf-8",
1710
1826
  stdio: ["pipe", "pipe", "pipe"]
1711
1827
  });
@@ -1865,15 +1981,10 @@ function removeProjectSnapshots(projectName) {
1865
1981
  }
1866
1982
 
1867
1983
  // src/utils/worktree-matcher.ts
1868
- import Enquirer3 from "enquirer";
1869
1984
  import { statSync as statSync3 } from "fs";
1870
- function findExactMatch(worktrees, branchName) {
1871
- return worktrees.find((wt) => wt.branch === branchName);
1872
- }
1873
- function findFuzzyMatches(worktrees, keyword) {
1874
- const lowerKeyword = keyword.toLowerCase();
1875
- return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
1876
- }
1985
+
1986
+ // src/utils/ui-prompts.ts
1987
+ import Enquirer3 from "enquirer";
1877
1988
  async function promptSelectBranch(worktrees, message) {
1878
1989
  const selectedBranch = await new Enquirer3.Select({
1879
1990
  message,
@@ -1923,6 +2034,77 @@ async function promptMultiSelectBranches(worktrees, message) {
1923
2034
  }).run();
1924
2035
  return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
1925
2036
  }
2037
+ async function promptGroupedMultiSelectBranches(worktrees, message) {
2038
+ const groups = groupWorktreesByDate(worktrees);
2039
+ const choices = buildGroupedChoices(groups);
2040
+ const groupMembershipMap = buildGroupMembershipMap(groups);
2041
+ const groupSelectAllNames = new Set(groupMembershipMap.keys());
2042
+ const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
2043
+ const MultiSelect = Enquirer3.MultiSelect;
2044
+ class MultiSelectWithGroupSelectAll extends MultiSelect {
2045
+ space() {
2046
+ if (!this.focused) return;
2047
+ const focusedName = this.focused.name;
2048
+ if (focusedName === SELECT_ALL_NAME) {
2049
+ const willEnable = !this.focused.enabled;
2050
+ for (const ch of this.choices) {
2051
+ ch.enabled = willEnable;
2052
+ }
2053
+ return this.render();
2054
+ }
2055
+ if (groupSelectAllNames.has(focusedName)) {
2056
+ const willEnable = !this.focused.enabled;
2057
+ const memberNames = groupMembershipMap.get(focusedName);
2058
+ this.focused.enabled = willEnable;
2059
+ for (const ch of this.choices) {
2060
+ if (memberNames.includes(ch.name)) {
2061
+ ch.enabled = willEnable;
2062
+ }
2063
+ }
2064
+ syncGlobalSelectAll(this.choices);
2065
+ return this.render();
2066
+ }
2067
+ this.toggle(this.focused);
2068
+ syncGroupSelectAll(this.choices, focusedName);
2069
+ syncGlobalSelectAll(this.choices);
2070
+ return this.render();
2071
+ }
2072
+ }
2073
+ function syncGlobalSelectAll(choiceList) {
2074
+ const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
2075
+ if (!selectAllChoice) return;
2076
+ const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
2077
+ selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
2078
+ }
2079
+ function syncGroupSelectAll(choiceList, branchName) {
2080
+ for (const [groupName, memberNames] of groupMembershipMap) {
2081
+ if (!memberNames.includes(branchName)) continue;
2082
+ const groupChoice = choiceList.find((ch) => ch.name === groupName);
2083
+ if (!groupChoice) continue;
2084
+ const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
2085
+ groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
2086
+ break;
2087
+ }
2088
+ }
2089
+ const selectedBranches = await new MultiSelectWithGroupSelectAll({
2090
+ message,
2091
+ choices,
2092
+ // 使用空心圆/实心圆作为选中指示符
2093
+ symbols: {
2094
+ indicator: { on: "\u25CF", off: "\u25CB" }
2095
+ }
2096
+ }).run();
2097
+ return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
2098
+ }
2099
+
2100
+ // src/utils/worktree-matcher.ts
2101
+ function findExactMatch(worktrees, branchName) {
2102
+ return worktrees.find((wt) => wt.branch === branchName);
2103
+ }
2104
+ function findFuzzyMatches(worktrees, keyword) {
2105
+ const lowerKeyword = keyword.toLowerCase();
2106
+ return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
2107
+ }
1926
2108
  async function resolveTargetWorktrees(worktrees, messages, branchName) {
1927
2109
  if (worktrees.length === 0) {
1928
2110
  throw new ClawtError(messages.noWorktrees);
@@ -2050,68 +2232,6 @@ function buildGroupMembershipMap(groups) {
2050
2232
  }
2051
2233
  return map;
2052
2234
  }
2053
- async function promptGroupedMultiSelectBranches(worktrees, message) {
2054
- const groups = groupWorktreesByDate(worktrees);
2055
- const choices = buildGroupedChoices(groups);
2056
- const groupMembershipMap = buildGroupMembershipMap(groups);
2057
- const groupSelectAllNames = new Set(groupMembershipMap.keys());
2058
- const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
2059
- const MultiSelect = Enquirer3.MultiSelect;
2060
- class MultiSelectWithGroupSelectAll extends MultiSelect {
2061
- space() {
2062
- if (!this.focused) return;
2063
- const focusedName = this.focused.name;
2064
- if (focusedName === SELECT_ALL_NAME) {
2065
- const willEnable = !this.focused.enabled;
2066
- for (const ch of this.choices) {
2067
- ch.enabled = willEnable;
2068
- }
2069
- return this.render();
2070
- }
2071
- if (groupSelectAllNames.has(focusedName)) {
2072
- const willEnable = !this.focused.enabled;
2073
- const memberNames = groupMembershipMap.get(focusedName);
2074
- this.focused.enabled = willEnable;
2075
- for (const ch of this.choices) {
2076
- if (memberNames.includes(ch.name)) {
2077
- ch.enabled = willEnable;
2078
- }
2079
- }
2080
- syncGlobalSelectAll(this.choices);
2081
- return this.render();
2082
- }
2083
- this.toggle(this.focused);
2084
- syncGroupSelectAll(this.choices, focusedName);
2085
- syncGlobalSelectAll(this.choices);
2086
- return this.render();
2087
- }
2088
- }
2089
- function syncGlobalSelectAll(choiceList) {
2090
- const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
2091
- if (!selectAllChoice) return;
2092
- const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
2093
- selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
2094
- }
2095
- function syncGroupSelectAll(choiceList, branchName) {
2096
- for (const [groupName, memberNames] of groupMembershipMap) {
2097
- if (!memberNames.includes(branchName)) continue;
2098
- const groupChoice = choiceList.find((ch) => ch.name === groupName);
2099
- if (!groupChoice) continue;
2100
- const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
2101
- groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
2102
- break;
2103
- }
2104
- }
2105
- const selectedBranches = await new MultiSelectWithGroupSelectAll({
2106
- message,
2107
- choices,
2108
- // 使用空心圆/实心圆作为选中指示符
2109
- symbols: {
2110
- indicator: { on: "\u25CF", off: "\u25CB" }
2111
- }
2112
- }).run();
2113
- return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
2114
- }
2115
2235
 
2116
2236
  // src/utils/progress-render.ts
2117
2237
  import chalk5 from "chalk";
@@ -2586,7 +2706,7 @@ function executeClaudeTask(worktree, task, onActivity) {
2586
2706
  stdio: ["ignore", "pipe", "pipe"]
2587
2707
  }
2588
2708
  );
2589
- const promise = new Promise((resolve3) => {
2709
+ const promise = new Promise((resolve4) => {
2590
2710
  let stderr = "";
2591
2711
  let finalResult = null;
2592
2712
  const lineBuffer = createLineBuffer();
@@ -2622,7 +2742,7 @@ function executeClaudeTask(worktree, task, onActivity) {
2622
2742
  if (finalResult) {
2623
2743
  success = !finalResult.is_error;
2624
2744
  }
2625
- resolve3({
2745
+ resolve4({
2626
2746
  task,
2627
2747
  branch: worktree.branch,
2628
2748
  worktreePath: worktree.path,
@@ -2632,7 +2752,7 @@ function executeClaudeTask(worktree, task, onActivity) {
2632
2752
  });
2633
2753
  });
2634
2754
  child.on("error", (err) => {
2635
- resolve3({
2755
+ resolve4({
2636
2756
  task,
2637
2757
  branch: worktree.branch,
2638
2758
  worktreePath: worktree.path,
@@ -2691,7 +2811,7 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
2691
2811
  const results = new Array(total);
2692
2812
  let nextIndex = 0;
2693
2813
  let completedCount = 0;
2694
- return new Promise((resolve3) => {
2814
+ return new Promise((resolve4) => {
2695
2815
  function launchNext() {
2696
2816
  if (nextIndex >= total || isInterrupted()) return;
2697
2817
  const index = nextIndex;
@@ -2712,7 +2832,7 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
2712
2832
  }
2713
2833
  launchNext();
2714
2834
  if (completedCount === total) {
2715
- resolve3(results);
2835
+ resolve4(results);
2716
2836
  }
2717
2837
  });
2718
2838
  }
@@ -2767,11 +2887,11 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
2767
2887
  printWarning(MESSAGES.INTERRUPTED);
2768
2888
  killAllChildProcesses(childProcesses);
2769
2889
  await Promise.allSettled(childProcesses.map(
2770
- (cp) => new Promise((resolve3) => {
2890
+ (cp) => new Promise((resolve4) => {
2771
2891
  if (cp.exitCode !== null) {
2772
- resolve3();
2892
+ resolve4();
2773
2893
  } else {
2774
- cp.on("close", () => resolve3());
2894
+ cp.on("close", () => resolve4());
2775
2895
  }
2776
2896
  })
2777
2897
  ));
@@ -3013,7 +3133,7 @@ function isNewerVersion(latest, current) {
3013
3133
  return false;
3014
3134
  }
3015
3135
  function fetchLatestVersion() {
3016
- return new Promise((resolve3) => {
3136
+ return new Promise((resolve4) => {
3017
3137
  const req = request(NPM_REGISTRY_URL, { timeout: NPM_REGISTRY_TIMEOUT_MS }, (res) => {
3018
3138
  let data = "";
3019
3139
  res.on("data", (chunk) => {
@@ -3022,16 +3142,16 @@ function fetchLatestVersion() {
3022
3142
  res.on("end", () => {
3023
3143
  try {
3024
3144
  const parsed = JSON.parse(data);
3025
- resolve3(parsed.version ?? null);
3145
+ resolve4(parsed.version ?? null);
3026
3146
  } catch {
3027
- resolve3(null);
3147
+ resolve4(null);
3028
3148
  }
3029
3149
  });
3030
3150
  });
3031
- req.on("error", () => resolve3(null));
3151
+ req.on("error", () => resolve4(null));
3032
3152
  req.on("timeout", () => {
3033
3153
  req.destroy();
3034
- resolve3(null);
3154
+ resolve4(null);
3035
3155
  });
3036
3156
  req.end();
3037
3157
  });
@@ -3252,7 +3372,7 @@ import { createInterface as createInterface2 } from "readline";
3252
3372
  import chalk9 from "chalk";
3253
3373
  import stringWidth3 from "string-width";
3254
3374
  function buildSeparatorWithHint(cols, hint) {
3255
- const maxWidth = Math.min(cols, 60);
3375
+ const maxWidth = Math.min(cols, PANEL_SEPARATOR_MAX_WIDTH);
3256
3376
  if (!hint) {
3257
3377
  return chalk9.gray("\u2500".repeat(maxWidth));
3258
3378
  }
@@ -3337,10 +3457,10 @@ function buildGroupedWorktreeLines(worktrees, selectedIndex) {
3337
3457
  function renderDateSeparator(dateKey) {
3338
3458
  const leftPad = " ";
3339
3459
  if (dateKey === UNKNOWN_DATE_GROUP) {
3340
- return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex("#FF8C00")("\u672A\u77E5\u65E5\u671F")} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
3460
+ return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex(PANEL_DATE_COLOR)(PANEL_UNKNOWN_DATE)} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
3341
3461
  }
3342
3462
  const relativeDate = formatRelativeDate(dateKey);
3343
- return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex("#FF8C00")(dateKey)}${chalk9.hex("#FF8C00")(`\uFF08${relativeDate}\uFF09`)} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
3463
+ return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex(PANEL_DATE_COLOR)(dateKey)}${chalk9.hex(PANEL_DATE_COLOR)(`\uFF08${relativeDate}\uFF09`)} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
3344
3464
  }
3345
3465
  function renderWorktreeBlock(wt, isSelected) {
3346
3466
  const lines = [];
@@ -3353,12 +3473,12 @@ function renderWorktreeBlock(wt, isSelected) {
3353
3473
  lines.push(`${indent}${chalk9.green(`+${wt.insertions}`)} ${chalk9.red(`-${wt.deletions}`)}`);
3354
3474
  }
3355
3475
  if (wt.commitsAhead > 0) {
3356
- lines.push(`${indent}${chalk9.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`)}`);
3476
+ lines.push(`${indent}${chalk9.yellow(PANEL_COMMITS_AHEAD(wt.commitsAhead))}`);
3357
3477
  }
3358
3478
  if (wt.commitsBehind > 0) {
3359
- lines.push(`${indent}${chalk9.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`)}`);
3479
+ lines.push(`${indent}${chalk9.yellow(PANEL_COMMITS_BEHIND(wt.commitsBehind))}`);
3360
3480
  } else {
3361
- lines.push(`${indent}${chalk9.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65")}`);
3481
+ lines.push(`${indent}${chalk9.green(PANEL_SYNCED_WITH_MAIN)}`);
3362
3482
  }
3363
3483
  if (wt.createdAt) {
3364
3484
  const relativeTime = formatRelativeTime(wt.createdAt);
@@ -3412,16 +3532,166 @@ function calculateVisibleRows(terminalRows) {
3412
3532
  return Math.max(terminalRows - fixedRows, 3);
3413
3533
  }
3414
3534
 
3415
- // src/utils/interactive-panel.ts
3416
- var InteractivePanel = class {
3535
+ // src/utils/keyboard-controller.ts
3536
+ var KeyboardController = class {
3537
+ /** stdin 数据处理器引用(用于清理) */
3538
+ stdinDataHandler = null;
3539
+ /** 按键回调函数 */
3540
+ onKeypress;
3541
+ /**
3542
+ * 创建键盘事件控制器
3543
+ * @param {(data: Buffer) => void} onKeypress - 按键回调函数
3544
+ */
3545
+ constructor(onKeypress) {
3546
+ this.onKeypress = onKeypress;
3547
+ }
3548
+ /**
3549
+ * 启动键盘监听
3550
+ * 将 stdin 设为 raw 模式以捕获每个按键
3551
+ */
3552
+ start() {
3553
+ if (process.stdin.isTTY) {
3554
+ process.stdin.setRawMode(true);
3555
+ }
3556
+ process.stdin.resume();
3557
+ this.stdinDataHandler = (data) => {
3558
+ this.onKeypress(data);
3559
+ };
3560
+ process.stdin.on("data", this.stdinDataHandler);
3561
+ }
3562
+ /**
3563
+ * 停止键盘监听,恢复 stdin 状态
3564
+ */
3565
+ stop() {
3566
+ if (this.stdinDataHandler) {
3567
+ process.stdin.removeListener("data", this.stdinDataHandler);
3568
+ this.stdinDataHandler = null;
3569
+ }
3570
+ if (process.stdin.isTTY) {
3571
+ process.stdin.setRawMode(false);
3572
+ }
3573
+ process.stdin.pause();
3574
+ }
3575
+ };
3576
+
3577
+ // src/utils/interactive-panel-state.ts
3578
+ var PanelStateManager = class {
3417
3579
  /** 当前状态数据 */
3418
- statusResult;
3580
+ statusResult = null;
3419
3581
  /** 当前选中的显示位置索引(对应 displayOrder 数组的下标) */
3420
- selectedDisplayIndex;
3582
+ selectedDisplayIndex = 0;
3421
3583
  /** 显示顺序到原始索引的映射(按日期分组后的排列顺序) */
3422
- displayOrder;
3584
+ displayOrder = [];
3423
3585
  /** 滚动偏移(基于行数) */
3424
- scrollOffset;
3586
+ scrollOffset = 0;
3587
+ /**
3588
+ * 更新状态数据
3589
+ * @param {StatusResult} newStatus - 新的状态数据
3590
+ * @param {string} [previousBranch] - 刷新前选中的分支名
3591
+ */
3592
+ updateData(newStatus, previousBranch) {
3593
+ this.statusResult = newStatus;
3594
+ this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
3595
+ if (previousBranch && this.displayOrder.length > 0) {
3596
+ const newDisplayIndex = this.displayOrder.findIndex(
3597
+ (origIdx) => this.statusResult.worktrees[origIdx]?.branch === previousBranch
3598
+ );
3599
+ if (newDisplayIndex >= 0) {
3600
+ this.selectedDisplayIndex = newDisplayIndex;
3601
+ } else {
3602
+ this.selectedDisplayIndex = Math.min(this.selectedDisplayIndex, Math.max(0, this.displayOrder.length - 1));
3603
+ }
3604
+ } else {
3605
+ this.selectedDisplayIndex = 0;
3606
+ }
3607
+ }
3608
+ /** 获取当前状态数据 */
3609
+ getStatusResult() {
3610
+ return this.statusResult;
3611
+ }
3612
+ /** 获取当前选中的原始索引 */
3613
+ getSelectedOriginalIndex() {
3614
+ return this.displayOrder[this.selectedDisplayIndex] ?? -1;
3615
+ }
3616
+ /** 获取当前滚动偏移 */
3617
+ getScrollOffset() {
3618
+ return this.scrollOffset;
3619
+ }
3620
+ /**
3621
+ * 向上导航
3622
+ * @returns {boolean} 是否发生变化
3623
+ */
3624
+ navigateUp() {
3625
+ if (!this.statusResult || this.displayOrder.length === 0) return false;
3626
+ if (this.selectedDisplayIndex > 0) {
3627
+ this.selectedDisplayIndex--;
3628
+ this.adjustScrollForSelection();
3629
+ return true;
3630
+ }
3631
+ return false;
3632
+ }
3633
+ /**
3634
+ * 向下导航
3635
+ * @returns {boolean} 是否发生变化
3636
+ */
3637
+ navigateDown() {
3638
+ if (!this.statusResult || this.displayOrder.length === 0) return false;
3639
+ if (this.selectedDisplayIndex < this.displayOrder.length - 1) {
3640
+ this.selectedDisplayIndex++;
3641
+ this.adjustScrollForSelection();
3642
+ return true;
3643
+ }
3644
+ return false;
3645
+ }
3646
+ /**
3647
+ * 获取当前选中的分支名
3648
+ * @returns {string | null} 分支名
3649
+ */
3650
+ getSelectedBranch() {
3651
+ const originalIndex = this.getSelectedOriginalIndex();
3652
+ if (originalIndex === -1 || !this.statusResult) return null;
3653
+ return this.statusResult.worktrees[originalIndex]?.branch || null;
3654
+ }
3655
+ /**
3656
+ * 调整滚动位置以确保选中项在可见区域内
3657
+ */
3658
+ adjustScrollForSelection() {
3659
+ if (!this.statusResult || this.displayOrder.length === 0) return;
3660
+ const originalIndex = this.getSelectedOriginalIndex();
3661
+ const rows = process.stdout.rows || 24;
3662
+ const visibleRows = calculateVisibleRows(rows);
3663
+ const panelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
3664
+ let firstLine = -1;
3665
+ let lastLine = -1;
3666
+ for (let i = 0; i < panelLines.length; i++) {
3667
+ if (panelLines[i].worktreeIndex === originalIndex) {
3668
+ if (firstLine === -1) firstLine = i;
3669
+ lastLine = i;
3670
+ }
3671
+ }
3672
+ if (firstLine === -1) return;
3673
+ let groupStart = firstLine;
3674
+ while (groupStart > 0 && panelLines[groupStart - 1].type === "separator") {
3675
+ groupStart--;
3676
+ }
3677
+ if (groupStart < this.scrollOffset) {
3678
+ this.scrollOffset = groupStart;
3679
+ }
3680
+ if (lastLine >= this.scrollOffset + visibleRows) {
3681
+ this.scrollOffset = lastLine - visibleRows + 1;
3682
+ }
3683
+ if (this.scrollOffset > groupStart) {
3684
+ this.scrollOffset = groupStart;
3685
+ }
3686
+ }
3687
+ };
3688
+
3689
+ // src/utils/interactive-panel.ts
3690
+ var InteractivePanel = class {
3691
+ /** 状态管理器 */
3692
+ stateManager;
3693
+ /** 键盘控制器 */
3694
+ keyboardController;
3425
3695
  /** 数据刷新定时器引用 */
3426
3696
  refreshTimer;
3427
3697
  /** 倒计时定时器引用 */
@@ -3436,8 +3706,6 @@ var InteractivePanel = class {
3436
3706
  resizeHandler;
3437
3707
  /** exit 兜底处理器 */
3438
3708
  exitHandler;
3439
- /** stdin 数据处理器引用(用于清理) */
3440
- stdinDataHandler;
3441
3709
  /** 操作锁(防止操作期间响应按键) */
3442
3710
  isOperating;
3443
3711
  /** Promise resolve 函数(stop 时调用以完成 start 返回的 Promise) */
@@ -3449,10 +3717,8 @@ var InteractivePanel = class {
3449
3717
  * @param {() => StatusResult} collectStatusFn - 数据收集函数
3450
3718
  */
3451
3719
  constructor(collectStatusFn) {
3452
- this.statusResult = null;
3453
- this.selectedDisplayIndex = 0;
3454
- this.displayOrder = [];
3455
- this.scrollOffset = 0;
3720
+ this.stateManager = new PanelStateManager();
3721
+ this.keyboardController = new KeyboardController(this.handleKeypress.bind(this));
3456
3722
  this.refreshTimer = null;
3457
3723
  this.countdownTimer = null;
3458
3724
  this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1e3;
@@ -3460,7 +3726,6 @@ var InteractivePanel = class {
3460
3726
  this.isTTY = !!process.stdout.isTTY;
3461
3727
  this.resizeHandler = null;
3462
3728
  this.exitHandler = null;
3463
- this.stdinDataHandler = null;
3464
3729
  this.isOperating = false;
3465
3730
  this.resolveStart = null;
3466
3731
  this.collectStatusFn = collectStatusFn;
@@ -3475,12 +3740,11 @@ var InteractivePanel = class {
3475
3740
  console.log(PANEL_NOT_TTY);
3476
3741
  return Promise.resolve();
3477
3742
  }
3478
- return new Promise((resolve3) => {
3479
- this.resolveStart = resolve3;
3480
- this.statusResult = this.collectStatusFn();
3481
- this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
3743
+ return new Promise((resolve4) => {
3744
+ this.resolveStart = resolve4;
3745
+ this.stateManager.updateData(this.collectStatusFn());
3482
3746
  this.initTerminal();
3483
- this.startKeyboardListener();
3747
+ this.keyboardController.start();
3484
3748
  this.startAutoRefresh();
3485
3749
  this.render();
3486
3750
  });
@@ -3492,7 +3756,7 @@ var InteractivePanel = class {
3492
3756
  if (this.stopped) return;
3493
3757
  this.stopped = true;
3494
3758
  this.clearTimers();
3495
- this.stopKeyboardListener();
3759
+ this.keyboardController.stop();
3496
3760
  this.restoreTerminal();
3497
3761
  if (this.resizeHandler) {
3498
3762
  process.stdout.removeListener("resize", this.resizeHandler);
@@ -3516,6 +3780,7 @@ var InteractivePanel = class {
3516
3780
  process.stdout.write(LINE_WRAP_DISABLE);
3517
3781
  this.resizeHandler = () => {
3518
3782
  if (!this.stopped && !this.isOperating) {
3783
+ this.stateManager.adjustScrollForSelection();
3519
3784
  this.render();
3520
3785
  }
3521
3786
  };
@@ -3535,33 +3800,6 @@ var InteractivePanel = class {
3535
3800
  process.stdout.write(CURSOR_SHOW);
3536
3801
  process.stdout.write(ALT_SCREEN_LEAVE);
3537
3802
  }
3538
- /**
3539
- * 启动键盘监听
3540
- * 将 stdin 设为 raw 模式以捕获每个按键
3541
- */
3542
- startKeyboardListener() {
3543
- if (process.stdin.isTTY) {
3544
- process.stdin.setRawMode(true);
3545
- }
3546
- process.stdin.resume();
3547
- this.stdinDataHandler = (data) => {
3548
- this.handleKeypress(data);
3549
- };
3550
- process.stdin.on("data", this.stdinDataHandler);
3551
- }
3552
- /**
3553
- * 停止键盘监听,恢复 stdin 状态
3554
- */
3555
- stopKeyboardListener() {
3556
- if (this.stdinDataHandler) {
3557
- process.stdin.removeListener("data", this.stdinDataHandler);
3558
- this.stdinDataHandler = null;
3559
- }
3560
- if (process.stdin.isTTY) {
3561
- process.stdin.setRawMode(false);
3562
- }
3563
- process.stdin.pause();
3564
- }
3565
3803
  /**
3566
3804
  * 处理键盘输入
3567
3805
  * @param {Buffer} data - 按键数据
@@ -3574,11 +3812,15 @@ var InteractivePanel = class {
3574
3812
  return;
3575
3813
  }
3576
3814
  if (str === KEY_ARROW_UP) {
3577
- this.navigateUp();
3815
+ if (this.stateManager.navigateUp()) {
3816
+ this.render();
3817
+ }
3578
3818
  return;
3579
3819
  }
3580
3820
  if (str === KEY_ARROW_DOWN) {
3581
- this.navigateDown();
3821
+ if (this.stateManager.navigateDown()) {
3822
+ this.render();
3823
+ }
3582
3824
  return;
3583
3825
  }
3584
3826
  const key = str.toLowerCase();
@@ -3615,67 +3857,6 @@ var InteractivePanel = class {
3615
3857
  return;
3616
3858
  }
3617
3859
  }
3618
- /**
3619
- * 向上导航,选中显示顺序中的上一个 worktree
3620
- */
3621
- navigateUp() {
3622
- if (!this.statusResult || this.displayOrder.length === 0) return;
3623
- if (this.selectedDisplayIndex > 0) {
3624
- this.selectedDisplayIndex--;
3625
- this.adjustScrollForSelection();
3626
- this.render();
3627
- }
3628
- }
3629
- /**
3630
- * 向下导航,选中显示顺序中的下一个 worktree
3631
- */
3632
- navigateDown() {
3633
- if (!this.statusResult || this.displayOrder.length === 0) return;
3634
- if (this.selectedDisplayIndex < this.displayOrder.length - 1) {
3635
- this.selectedDisplayIndex++;
3636
- this.adjustScrollForSelection();
3637
- this.render();
3638
- }
3639
- }
3640
- /**
3641
- * 获取当前选中的原始 worktree 索引
3642
- * @returns {number} 原始 worktrees 数组中的索引
3643
- */
3644
- getSelectedOriginalIndex() {
3645
- return this.displayOrder[this.selectedDisplayIndex];
3646
- }
3647
- /**
3648
- * 调整滚动偏移以确保选中项在可见区域内
3649
- */
3650
- adjustScrollForSelection() {
3651
- if (!this.statusResult || this.displayOrder.length === 0) return;
3652
- const originalIndex = this.getSelectedOriginalIndex();
3653
- const rows = process.stdout.rows || 24;
3654
- const visibleRows = calculateVisibleRows(rows);
3655
- const panelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
3656
- let firstLine = -1;
3657
- let lastLine = -1;
3658
- for (let i = 0; i < panelLines.length; i++) {
3659
- if (panelLines[i].worktreeIndex === originalIndex) {
3660
- if (firstLine === -1) firstLine = i;
3661
- lastLine = i;
3662
- }
3663
- }
3664
- if (firstLine === -1) return;
3665
- let groupStart = firstLine;
3666
- while (groupStart > 0 && panelLines[groupStart - 1].type === "separator") {
3667
- groupStart--;
3668
- }
3669
- if (groupStart < this.scrollOffset) {
3670
- this.scrollOffset = groupStart;
3671
- }
3672
- if (lastLine >= this.scrollOffset + visibleRows) {
3673
- this.scrollOffset = lastLine - visibleRows + 1;
3674
- }
3675
- if (this.scrollOffset > groupStart) {
3676
- this.scrollOffset = groupStart;
3677
- }
3678
- }
3679
3860
  /**
3680
3861
  * 启动自动刷新:数据刷新定时器 + 倒计时定时器
3681
3862
  */
@@ -3711,45 +3892,25 @@ var InteractivePanel = class {
3711
3892
  */
3712
3893
  refreshData() {
3713
3894
  if (this.stopped || this.isOperating) return;
3714
- const originalIndex = this.displayOrder[this.selectedDisplayIndex];
3715
- const previousBranch = this.statusResult?.worktrees[originalIndex]?.branch;
3716
- this.statusResult = this.collectStatusFn();
3717
- this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
3718
- this.restoreSelection(previousBranch);
3895
+ const previousBranch = this.stateManager.getSelectedBranch();
3896
+ this.stateManager.updateData(this.collectStatusFn(), previousBranch || void 0);
3897
+ this.stateManager.adjustScrollForSelection();
3719
3898
  this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1e3;
3720
3899
  this.render();
3721
3900
  }
3722
- /**
3723
- * 按分支名恢复选中位置(基于显示顺序)
3724
- * @param {string | undefined} previousBranch - 之前选中的分支名
3725
- */
3726
- restoreSelection(previousBranch) {
3727
- if (!this.statusResult || !previousBranch || this.displayOrder.length === 0) {
3728
- this.selectedDisplayIndex = 0;
3729
- return;
3730
- }
3731
- const newDisplayIndex = this.displayOrder.findIndex(
3732
- (origIdx) => this.statusResult.worktrees[origIdx]?.branch === previousBranch
3733
- );
3734
- if (newDisplayIndex >= 0) {
3735
- this.selectedDisplayIndex = newDisplayIndex;
3736
- } else {
3737
- this.selectedDisplayIndex = Math.min(this.selectedDisplayIndex, Math.max(0, this.displayOrder.length - 1));
3738
- }
3739
- this.adjustScrollForSelection();
3740
- }
3741
3901
  /**
3742
3902
  * 渲染一帧面板内容
3743
3903
  * 使用同步输出防止闪烁
3744
3904
  */
3745
3905
  render() {
3746
- if (this.stopped || this.isOperating || !this.statusResult) return;
3906
+ const statusResult = this.stateManager.getStatusResult();
3907
+ if (this.stopped || this.isOperating || !statusResult) return;
3747
3908
  const cols = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
3748
3909
  const rows = process.stdout.rows || 24;
3749
3910
  const frameLines = buildPanelFrame(
3750
- this.statusResult,
3751
- this.getSelectedOriginalIndex(),
3752
- this.scrollOffset,
3911
+ statusResult,
3912
+ this.stateManager.getSelectedOriginalIndex(),
3913
+ this.stateManager.getScrollOffset(),
3753
3914
  rows,
3754
3915
  cols,
3755
3916
  this.refreshCountdown
@@ -3768,63 +3929,56 @@ var InteractivePanel = class {
3768
3929
  * @param {() => void} action - 要执行的操作
3769
3930
  */
3770
3931
  async executeOperation(action) {
3771
- if (!this.statusResult || this.displayOrder.length === 0) return;
3932
+ const statusResult = this.stateManager.getStatusResult();
3933
+ if (!statusResult || this.stateManager.getSelectedOriginalIndex() === -1) return;
3772
3934
  this.isOperating = true;
3773
3935
  this.clearTimers();
3774
3936
  this.restoreTerminal();
3775
- this.stopKeyboardListener();
3937
+ this.keyboardController.stop();
3776
3938
  action();
3777
3939
  console.log(PANEL_PRESS_ENTER_TO_RETURN);
3778
3940
  await this.waitForEnter();
3779
3941
  this.initTerminal();
3780
- this.startKeyboardListener();
3942
+ this.keyboardController.start();
3781
3943
  this.isOperating = false;
3782
3944
  this.refreshData();
3783
3945
  this.startAutoRefresh();
3784
3946
  this.render();
3785
3947
  }
3786
- /**
3787
- * 获取当前选中的分支名
3788
- * @returns {string} 当前选中的分支名
3789
- */
3790
- getSelectedBranch() {
3791
- const originalIndex = this.getSelectedOriginalIndex();
3792
- return this.statusResult.worktrees[originalIndex].branch;
3793
- }
3794
3948
  /**
3795
3949
  * 执行验证操作
3796
3950
  */
3797
3951
  handleValidate() {
3798
- const branch = this.getSelectedBranch();
3799
- runCommandInherited(`clawt validate -b ${branch}`);
3952
+ const branch = this.stateManager.getSelectedBranch();
3953
+ if (branch) runCommandInherited(`clawt validate -b ${branch}`);
3800
3954
  }
3801
3955
  /**
3802
3956
  * 执行合并操作
3803
3957
  */
3804
3958
  handleMerge() {
3805
- const branch = this.getSelectedBranch();
3806
- runCommandInherited(`clawt merge -b ${branch}`);
3959
+ const branch = this.stateManager.getSelectedBranch();
3960
+ if (branch) runCommandInherited(`clawt merge -b ${branch}`);
3807
3961
  }
3808
3962
  /**
3809
3963
  * 执行删除操作
3810
3964
  */
3811
3965
  handleDelete() {
3812
- const branch = this.getSelectedBranch();
3813
- runCommandInherited(`clawt remove -b ${branch}`);
3966
+ const branch = this.stateManager.getSelectedBranch();
3967
+ if (branch) runCommandInherited(`clawt remove -b ${branch}`);
3814
3968
  }
3815
3969
  /**
3816
3970
  * 执行恢复操作
3817
3971
  */
3818
3972
  handleResume() {
3819
- const branch = this.getSelectedBranch();
3820
- runCommandInherited(`clawt resume -b ${branch}`);
3973
+ const branch = this.stateManager.getSelectedBranch();
3974
+ if (branch) runCommandInherited(`clawt resume -b ${branch}`);
3821
3975
  }
3822
3976
  /**
3823
3977
  * 执行同步操作
3824
3978
  */
3825
3979
  handleSync() {
3826
- const branch = this.getSelectedBranch();
3827
- runCommandInherited(`clawt sync -b ${branch}`);
3980
+ const branch = this.stateManager.getSelectedBranch();
3981
+ if (branch) runCommandInherited(`clawt sync -b ${branch}`);
3828
3982
  }
3829
3983
  /**
3830
3984
  * 执行覆盖操作
@@ -3838,19 +3992,102 @@ var InteractivePanel = class {
3838
3992
  * @returns {Promise<void>} 用户按回车时 resolve
3839
3993
  */
3840
3994
  waitForEnter() {
3841
- return new Promise((resolve3) => {
3995
+ return new Promise((resolve4) => {
3842
3996
  const rl = createInterface2({
3843
3997
  input: process.stdin,
3844
3998
  output: process.stdout
3845
3999
  });
3846
4000
  rl.once("line", () => {
3847
4001
  rl.close();
3848
- resolve3();
4002
+ resolve4();
3849
4003
  });
3850
4004
  });
3851
4005
  }
3852
4006
  };
3853
4007
 
4008
+ // src/utils/conflict-resolver.ts
4009
+ import { execFileSync as execFileSync4 } from "child_process";
4010
+ var DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS = 3e5;
4011
+ function buildConflictResolvePrompt() {
4012
+ return CONFLICT_RESOLVE_PROMPT;
4013
+ }
4014
+ function getConflictResolveTimeout() {
4015
+ const configValue = getConfigValue("conflictResolveTimeoutMs");
4016
+ if (typeof configValue === "number" && configValue > 0) {
4017
+ return configValue;
4018
+ }
4019
+ return DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS;
4020
+ }
4021
+ function invokeClaudeForConflictResolve(prompt, cwd) {
4022
+ const args = ["-p", prompt, "--permission-mode", "bypassPermissions"];
4023
+ logger.info(`\u8C03\u7528 Claude Code \u89E3\u51B3\u51B2\u7A81\uFF0C\u547D\u4EE4: claude -p "..." --permission-mode bypassPermissions`);
4024
+ try {
4025
+ const output = execFileSync4("claude", args, {
4026
+ cwd,
4027
+ encoding: "utf-8",
4028
+ stdio: ["pipe", "pipe", "pipe"],
4029
+ timeout: getConflictResolveTimeout()
4030
+ });
4031
+ return output;
4032
+ } catch (error) {
4033
+ const errMsg = error instanceof Error ? error.message : String(error);
4034
+ logger.error(`Claude Code \u51B2\u7A81\u89E3\u51B3\u5931\u8D25: ${errMsg}`);
4035
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_AI_FAILED(errMsg));
4036
+ }
4037
+ }
4038
+ function resolveConflictsWithAI(currentBranch, incomingBranch, cwd) {
4039
+ const conflictFiles = getConflictFiles(cwd);
4040
+ if (conflictFiles.length === 0) {
4041
+ return true;
4042
+ }
4043
+ printInfo(MESSAGES.MERGE_CONFLICT_AI_START(conflictFiles.length));
4044
+ const prompt = buildConflictResolvePrompt();
4045
+ try {
4046
+ invokeClaudeForConflictResolve(prompt, cwd);
4047
+ } catch (error) {
4048
+ const errMsg = error instanceof ClawtError ? error.message : String(error);
4049
+ printWarning(errMsg);
4050
+ return false;
4051
+ }
4052
+ const remainingConflicts = getConflictFiles(cwd);
4053
+ if (remainingConflicts.length === 0) {
4054
+ gitAddFiles(conflictFiles, cwd);
4055
+ gitMergeContinue(cwd);
4056
+ printSuccess(MESSAGES.MERGE_CONFLICT_AI_SUCCESS);
4057
+ return true;
4058
+ }
4059
+ const resolvedFiles = conflictFiles.filter((f) => !remainingConflicts.includes(f));
4060
+ if (resolvedFiles.length > 0) {
4061
+ gitAddFiles(resolvedFiles, cwd);
4062
+ }
4063
+ printWarning(MESSAGES.MERGE_CONFLICT_AI_PARTIAL(remainingConflicts.length));
4064
+ return false;
4065
+ }
4066
+ function determineConflictResolveMode(autoFlag) {
4067
+ if (autoFlag === true) {
4068
+ return "auto";
4069
+ }
4070
+ const configMode = getConfigValue("conflictResolveMode");
4071
+ if (configMode === "auto" || configMode === "manual") {
4072
+ return configMode;
4073
+ }
4074
+ return "ask";
4075
+ }
4076
+ async function handleMergeConflict(currentBranch, incomingBranch, cwd, autoFlag) {
4077
+ const mode = determineConflictResolveMode(autoFlag);
4078
+ if (mode === "manual") {
4079
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
4080
+ }
4081
+ if (mode === "auto") {
4082
+ return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
4083
+ }
4084
+ const shouldUseAI = await confirmAction(MESSAGES.MERGE_CONFLICT_ASK_AI);
4085
+ if (!shouldUseAI) {
4086
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
4087
+ }
4088
+ return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
4089
+ }
4090
+
3854
4091
  // src/commands/list.ts
3855
4092
  import chalk10 from "chalk";
3856
4093
  function registerListCommand(program2) {
@@ -4430,7 +4667,7 @@ async function handleCoverValidate() {
4430
4667
 
4431
4668
  // src/commands/merge.ts
4432
4669
  function registerMergeCommand(program2) {
4433
- program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").option("-b, --branch <branchName>", "\u8981\u5408\u5E76\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\u4F9B\u9009\u62E9\uFF09").option("-m, --message <commitMessage>", "\u63D0\u4EA4\u4FE1\u606F\uFF08\u76EE\u6807 worktree \u5DE5\u4F5C\u533A\u6709\u4FEE\u6539\u65F6\u5FC5\u586B\uFF09").action(async (options) => {
4670
+ program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").option("-b, --branch <branchName>", "\u8981\u5408\u5E76\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\u4F9B\u9009\u62E9\uFF09").option("-m, --message <commitMessage>", "\u63D0\u4EA4\u4FE1\u606F\uFF08\u76EE\u6807 worktree \u5DE5\u4F5C\u533A\u6709\u4FEE\u6539\u65F6\u5FC5\u586B\uFF09").option("--auto", "\u9047\u5230\u51B2\u7A81\u76F4\u63A5\u8C03\u7528 AI \u89E3\u51B3\uFF0C\u4E0D\u518D\u8BE2\u95EE").action(async (options) => {
4434
4671
  await handleMerge(options);
4435
4672
  });
4436
4673
  }
@@ -4502,16 +4739,25 @@ async function handleMerge(options) {
4502
4739
  throw new ClawtError(MESSAGES.TARGET_WORKTREE_NO_CHANGES);
4503
4740
  }
4504
4741
  }
4742
+ let mergeHadConflict = false;
4505
4743
  try {
4506
4744
  gitMerge(branch, mainWorktreePath);
4507
4745
  } catch (error) {
4508
4746
  if (hasMergeConflict(mainWorktreePath)) {
4509
- throw new ClawtError(MESSAGES.MERGE_CONFLICT);
4747
+ mergeHadConflict = true;
4748
+ } else {
4749
+ throw error;
4510
4750
  }
4511
- throw error;
4512
4751
  }
4513
- if (hasMergeConflict(mainWorktreePath)) {
4514
- throw new ClawtError(MESSAGES.MERGE_CONFLICT);
4752
+ if (!mergeHadConflict && hasMergeConflict(mainWorktreePath)) {
4753
+ mergeHadConflict = true;
4754
+ }
4755
+ if (mergeHadConflict) {
4756
+ const currentBranch = getCurrentBranch(mainWorktreePath);
4757
+ const resolved = await handleMergeConflict(currentBranch, branch, mainWorktreePath, options.auto);
4758
+ if (!resolved) {
4759
+ return;
4760
+ }
4515
4761
  }
4516
4762
  const autoPullPush = getConfigValue("autoPullPush");
4517
4763
  if (autoPullPush) {
@@ -5386,6 +5632,28 @@ async function handleHome() {
5386
5632
  printSuccess(MESSAGES.HOME_SWITCH_SUCCESS(currentBranch, mainBranch));
5387
5633
  }
5388
5634
 
5635
+ // src/commands/tasks.ts
5636
+ import { resolve as resolve3, dirname as dirname2, join as join10 } from "path";
5637
+ import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
5638
+ function registerTasksCommand(program2) {
5639
+ const taskCmd = program2.command("tasks").description("\u4EFB\u52A1\u6587\u4EF6\u7BA1\u7406");
5640
+ taskCmd.command("init").description("\u751F\u6210\u4EFB\u52A1\u6A21\u677F\u6587\u4EF6").argument("[path]", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (path2) => {
5641
+ const filePath = path2 ?? join10(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
5642
+ await handleTasksInit(filePath);
5643
+ });
5644
+ }
5645
+ async function handleTasksInit(filePath) {
5646
+ const absolutePath = resolve3(filePath);
5647
+ logger.info(`tasks init \u547D\u4EE4\u6267\u884C\uFF0C\u76EE\u6807\u6587\u4EF6: ${absolutePath}`);
5648
+ if (existsSync13(absolutePath)) {
5649
+ throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
5650
+ }
5651
+ ensureDir(dirname2(absolutePath));
5652
+ writeFileSync6(absolutePath, TASK_TEMPLATE_CONTENT, "utf-8");
5653
+ printSuccess(MESSAGES.TASK_INIT_SUCCESS(filePath));
5654
+ printHint(MESSAGES.TASK_INIT_HINT(filePath));
5655
+ }
5656
+
5389
5657
  // src/index.ts
5390
5658
  var require2 = createRequire(import.meta.url);
5391
5659
  var { version } = require2("../package.json");
@@ -5414,6 +5682,7 @@ registerProjectsCommand(program);
5414
5682
  registerCompletionCommand(program);
5415
5683
  registerInitCommand(program);
5416
5684
  registerHomeCommand(program);
5685
+ registerTasksCommand(program);
5417
5686
  var config = loadConfig();
5418
5687
  applyAliases(program, config.aliases);
5419
5688
  process.on("uncaughtException", (error) => {