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.
- package/.claude/settings.local.json +12 -0
- package/README.md +0 -4
- package/dist/index.js +583 -314
- package/dist/postinstall.js +37 -2
- package/docs/alias.md +7 -1
- package/docs/completion.md +1 -1
- package/docs/config.md +4 -3
- package/docs/cover-validate.md +4 -3
- package/docs/create.md +28 -12
- package/docs/home.md +12 -8
- package/docs/init.md +16 -9
- package/docs/list.md +13 -7
- package/docs/merge.md +12 -12
- package/docs/remove.md +24 -13
- package/docs/reset.md +6 -4
- package/docs/resume.md +3 -4
- package/docs/status.md +75 -30
- package/docs/sync.md +26 -26
- package/docs/validate.md +13 -7
- package/package.json +1 -1
- package/src/commands/merge.ts +20 -5
- package/src/commands/tasks.ts +51 -0
- package/src/constants/ai-prompts.ts +14 -0
- package/src/constants/config.ts +9 -0
- package/src/constants/index.ts +4 -0
- package/src/constants/interactive-panel.ts +6 -0
- package/src/constants/messages/index.ts +4 -2
- package/src/constants/messages/interactive-panel.ts +12 -0
- package/src/constants/messages/merge.ts +15 -0
- package/src/constants/messages/tasks.ts +9 -0
- package/src/constants/tasks-template.ts +28 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +8 -0
- package/src/types/config.ts +4 -0
- package/src/types/index.ts +1 -1
- package/src/utils/conflict-resolver.ts +170 -0
- package/src/utils/formatter.ts +19 -0
- package/src/utils/git-branch.ts +116 -0
- package/src/utils/git-core.ts +417 -0
- package/src/utils/git-worktree.ts +40 -0
- package/src/utils/git.ts +3 -521
- package/src/utils/index.ts +7 -2
- package/src/utils/interactive-panel-render.ts +12 -6
- package/src/utils/interactive-panel-state.ts +137 -0
- package/src/utils/interactive-panel.ts +44 -188
- package/src/utils/keyboard-controller.ts +48 -0
- package/src/utils/ui-prompts.ts +240 -0
- package/src/utils/worktree-matcher.ts +21 -251
- package/tests/unit/commands/merge.test.ts +59 -3
- package/tests/unit/commands/tasks.test.ts +153 -0
- package/tests/unit/utils/conflict-resolver.test.ts +250 -0
- package/tests/unit/utils/formatter.test.ts +26 -1
- 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((
|
|
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
|
-
|
|
992
|
+
resolve4({ command, exitCode: 1, error: err.message });
|
|
912
993
|
});
|
|
913
994
|
child.on("close", (code) => {
|
|
914
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1871
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
2890
|
+
(cp) => new Promise((resolve4) => {
|
|
2771
2891
|
if (cp.exitCode !== null) {
|
|
2772
|
-
|
|
2892
|
+
resolve4();
|
|
2773
2893
|
} else {
|
|
2774
|
-
cp.on("close", () =>
|
|
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((
|
|
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
|
-
|
|
3145
|
+
resolve4(parsed.version ?? null);
|
|
3026
3146
|
} catch {
|
|
3027
|
-
|
|
3147
|
+
resolve4(null);
|
|
3028
3148
|
}
|
|
3029
3149
|
});
|
|
3030
3150
|
});
|
|
3031
|
-
req.on("error", () =>
|
|
3151
|
+
req.on("error", () => resolve4(null));
|
|
3032
3152
|
req.on("timeout", () => {
|
|
3033
3153
|
req.destroy();
|
|
3034
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3479
|
+
lines.push(`${indent}${chalk9.yellow(PANEL_COMMITS_BEHIND(wt.commitsBehind))}`);
|
|
3360
3480
|
} else {
|
|
3361
|
-
lines.push(`${indent}${chalk9.green(
|
|
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/
|
|
3416
|
-
var
|
|
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.
|
|
3453
|
-
this.
|
|
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((
|
|
3479
|
-
this.resolveStart =
|
|
3480
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
|
3715
|
-
|
|
3716
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
3751
|
-
this.getSelectedOriginalIndex(),
|
|
3752
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
4747
|
+
mergeHadConflict = true;
|
|
4748
|
+
} else {
|
|
4749
|
+
throw error;
|
|
4510
4750
|
}
|
|
4511
|
-
throw error;
|
|
4512
4751
|
}
|
|
4513
|
-
if (hasMergeConflict(mainWorktreePath)) {
|
|
4514
|
-
|
|
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) => {
|