clawt 3.4.6 → 3.5.0
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 +425 -305
- package/dist/postinstall.js +12 -1
- 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/tasks.ts +51 -0
- package/src/constants/index.ts +3 -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/tasks.ts +9 -0
- package/src/constants/tasks-template.ts +28 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +6 -0
- package/src/types/index.ts +1 -1
- package/src/utils/formatter.ts +19 -0
- package/src/utils/git-branch.ts +116 -0
- package/src/utils/git-core.ts +369 -0
- package/src/utils/git-worktree.ts +40 -0
- package/src/utils/git.ts +3 -521
- package/src/utils/index.ts +1 -1
- 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/tasks.test.ts +153 -0
- package/tests/unit/utils/formatter.test.ts +26 -1
package/dist/index.js
CHANGED
|
@@ -513,6 +513,16 @@ var HOME_MESSAGES = {
|
|
|
513
513
|
HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}`
|
|
514
514
|
};
|
|
515
515
|
|
|
516
|
+
// src/constants/messages/tasks.ts
|
|
517
|
+
var TASKS_CMD_MESSAGES = {
|
|
518
|
+
/** 任务模板文件已存在 */
|
|
519
|
+
TASK_INIT_FILE_EXISTS: (path2) => `\u6587\u4EF6\u5DF2\u5B58\u5728: ${path2}\uFF0C\u5982\u9700\u8986\u76D6\u8BF7\u5148\u5220\u9664`,
|
|
520
|
+
/** 任务模板生成成功 */
|
|
521
|
+
TASK_INIT_SUCCESS: (path2) => `\u2713 \u4EFB\u52A1\u6A21\u677F\u5DF2\u751F\u6210: ${path2}`,
|
|
522
|
+
/** 任务模板使用提示 */
|
|
523
|
+
TASK_INIT_HINT: (path2) => `\u4F7F\u7528 clawt run -f ${path2} \u6267\u884C\u4EFB\u52A1`
|
|
524
|
+
};
|
|
525
|
+
|
|
516
526
|
// src/constants/messages/interactive-panel.ts
|
|
517
527
|
import chalk from "chalk";
|
|
518
528
|
|
|
@@ -544,6 +554,8 @@ var PANEL_SHORTCUT_KEYS = {
|
|
|
544
554
|
};
|
|
545
555
|
var PANEL_DATE_SEPARATOR_PREFIX = "\u2550\u2550\u2550\u2550";
|
|
546
556
|
var PANEL_FIXED_ROWS = 5;
|
|
557
|
+
var PANEL_SEPARATOR_MAX_WIDTH = 60;
|
|
558
|
+
var PANEL_DATE_COLOR = "#FF8C00";
|
|
547
559
|
|
|
548
560
|
// src/constants/messages/interactive-panel.ts
|
|
549
561
|
var SHORTCUT_LABELS = {
|
|
@@ -575,6 +587,10 @@ var PANEL_CONFIGURED_BRANCH = (branchName) => chalk.gray(`\u4E3B\u5DE5\u4F5C\u52
|
|
|
575
587
|
var PANEL_CONFIGURED_BRANCH_DELETED = (branchName) => chalk.red(`\u2717 \u4E3B\u5DE5\u4F5C\u5206\u652F: ${branchName}\uFF08\u5DF2\u4E0D\u5B58\u5728\uFF09`);
|
|
576
588
|
var PANEL_CONFIGURED_BRANCH_MISMATCH = (branchName) => chalk.yellow(`\u26A0 \u4E3B\u5DE5\u4F5C\u5206\u652F: ${branchName}\uFF08\u4E0D\u4E00\u81F4\uFF09`);
|
|
577
589
|
var PANEL_NOT_INITIALIZED = chalk.gray("\u672A\u521D\u59CB\u5316\uFF08\u6267\u884C clawt init \u8BBE\u7F6E\u4E3B\u5DE5\u4F5C\u5206\u652F\uFF09");
|
|
590
|
+
var PANEL_UNKNOWN_DATE = "\u672A\u77E5\u65E5\u671F";
|
|
591
|
+
var PANEL_SYNCED_WITH_MAIN = "\u4E0E\u4E3B\u5206\u652F\u540C\u6B65";
|
|
592
|
+
var PANEL_COMMITS_AHEAD = (count) => `${count} \u4E2A\u672C\u5730\u63D0\u4EA4`;
|
|
593
|
+
var PANEL_COMMITS_BEHIND = (count) => `\u843D\u540E\u4E3B\u5206\u652F ${count} \u4E2A\u63D0\u4EA4`;
|
|
578
594
|
|
|
579
595
|
// src/constants/messages/index.ts
|
|
580
596
|
var MESSAGES = {
|
|
@@ -594,7 +610,8 @@ var MESSAGES = {
|
|
|
594
610
|
...COMPLETION_MESSAGES,
|
|
595
611
|
...INIT_MESSAGES,
|
|
596
612
|
...COVER_VALIDATE_MESSAGES,
|
|
597
|
-
...HOME_MESSAGES
|
|
613
|
+
...HOME_MESSAGES,
|
|
614
|
+
...TASKS_CMD_MESSAGES
|
|
598
615
|
};
|
|
599
616
|
|
|
600
617
|
// src/constants/exitCodes.ts
|
|
@@ -777,6 +794,31 @@ var PRE_CHECK_RESUME = {
|
|
|
777
794
|
requireClaudeCode: true
|
|
778
795
|
};
|
|
779
796
|
|
|
797
|
+
// src/constants/tasks-template.ts
|
|
798
|
+
var TASK_TEMPLATE_OUTPUT_DIR = "clawt/tasks";
|
|
799
|
+
var TASK_TEMPLATE_FILENAME_PREFIX = "clawt-tasks";
|
|
800
|
+
var TASK_TEMPLATE_CONTENT = `# Clawt \u4EFB\u52A1\u6587\u4EF6
|
|
801
|
+
#
|
|
802
|
+
# \u4F7F\u7528\u65B9\u6CD5: clawt run -f tasks.md
|
|
803
|
+
# \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
|
|
804
|
+
#
|
|
805
|
+
# \u89C4\u5219:
|
|
806
|
+
# 1. \u6BCF\u4E2A\u4EFB\u52A1\u5757\u7528 <!-- CLAWT-TASKS:START --> \u548C <!-- CLAWT-TASKS:END --> \u5305\u88F9
|
|
807
|
+
# 2. \u5757\u5185 # branch: <\u5206\u652F\u540D> \u58F0\u660E\u5206\u652F\u540D\uFF08\u4F7F\u7528 -b \u53C2\u6570\u65F6\u53EF\u7701\u7565\uFF09
|
|
808
|
+
# 3. \u5757\u5185\u5176\u4F59\u884C\u4E3A\u4EFB\u52A1\u63CF\u8FF0\uFF08\u652F\u6301\u591A\u884C\uFF09
|
|
809
|
+
|
|
810
|
+
<!-- CLAWT-TASKS:START -->
|
|
811
|
+
# branch: feat-example-1
|
|
812
|
+
\u5728\u8FD9\u91CC\u5199\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u7684\u63CF\u8FF0
|
|
813
|
+
<!-- CLAWT-TASKS:END -->
|
|
814
|
+
|
|
815
|
+
<!-- CLAWT-TASKS:START -->
|
|
816
|
+
# branch: feat-example-2
|
|
817
|
+
\u5728\u8FD9\u91CC\u5199\u7B2C\u4E8C\u4E2A\u4EFB\u52A1\u7684\u63CF\u8FF0
|
|
818
|
+
\u652F\u6301\u591A\u884C\u63CF\u8FF0
|
|
819
|
+
<!-- CLAWT-TASKS:END -->
|
|
820
|
+
`;
|
|
821
|
+
|
|
780
822
|
// src/errors/index.ts
|
|
781
823
|
var ClawtError = class extends Error {
|
|
782
824
|
/** 退出码 */
|
|
@@ -900,7 +942,7 @@ function parseParallelCommands(commandString) {
|
|
|
900
942
|
}
|
|
901
943
|
function runParallelCommands(commands, options) {
|
|
902
944
|
const promises = commands.map((command) => {
|
|
903
|
-
return new Promise((
|
|
945
|
+
return new Promise((resolve4) => {
|
|
904
946
|
logger.debug(`\u5E76\u884C\u542F\u52A8\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
905
947
|
const child = spawn(command, {
|
|
906
948
|
cwd: options?.cwd,
|
|
@@ -908,17 +950,17 @@ function runParallelCommands(commands, options) {
|
|
|
908
950
|
shell: true
|
|
909
951
|
});
|
|
910
952
|
child.on("error", (err) => {
|
|
911
|
-
|
|
953
|
+
resolve4({ command, exitCode: 1, error: err.message });
|
|
912
954
|
});
|
|
913
955
|
child.on("close", (code) => {
|
|
914
|
-
|
|
956
|
+
resolve4({ command, exitCode: code ?? 1 });
|
|
915
957
|
});
|
|
916
958
|
});
|
|
917
959
|
});
|
|
918
960
|
return Promise.all(promises);
|
|
919
961
|
}
|
|
920
962
|
|
|
921
|
-
// src/utils/git.ts
|
|
963
|
+
// src/utils/git-core.ts
|
|
922
964
|
import { basename } from "path";
|
|
923
965
|
import { execSync as execSync2 } from "child_process";
|
|
924
966
|
function getGitCommonDir(cwd) {
|
|
@@ -931,26 +973,6 @@ function getProjectName(cwd) {
|
|
|
931
973
|
const topLevel = getGitTopLevel(cwd);
|
|
932
974
|
return basename(topLevel);
|
|
933
975
|
}
|
|
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
976
|
function getStatusPorcelain(cwd) {
|
|
955
977
|
return execCommand("git status --porcelain", { cwd });
|
|
956
978
|
}
|
|
@@ -988,32 +1010,6 @@ function gitStashPush(message, cwd) {
|
|
|
988
1010
|
function gitRestoreStaged(cwd) {
|
|
989
1011
|
execCommand("git restore --staged .", { cwd });
|
|
990
1012
|
}
|
|
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
1013
|
function parseShortStat(output) {
|
|
1018
1014
|
let insertions = 0;
|
|
1019
1015
|
let deletions = 0;
|
|
@@ -1034,9 +1030,6 @@ function getDiffStat(worktreePath) {
|
|
|
1034
1030
|
function gitApplyCachedFromStdin(patchContent, cwd) {
|
|
1035
1031
|
execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
|
|
1036
1032
|
}
|
|
1037
|
-
function getCurrentBranch(cwd) {
|
|
1038
|
-
return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
1039
|
-
}
|
|
1040
1033
|
function getHeadCommitHash(cwd) {
|
|
1041
1034
|
return execCommand("git rev-parse HEAD", { cwd });
|
|
1042
1035
|
}
|
|
@@ -1092,6 +1085,43 @@ function gitApplyCachedCheck(patchContent, cwd) {
|
|
|
1092
1085
|
return false;
|
|
1093
1086
|
}
|
|
1094
1087
|
}
|
|
1088
|
+
|
|
1089
|
+
// src/utils/git-branch.ts
|
|
1090
|
+
function checkBranchExists(branchName, cwd) {
|
|
1091
|
+
try {
|
|
1092
|
+
execCommand(`git show-ref --verify refs/heads/${branchName}`, { cwd });
|
|
1093
|
+
return true;
|
|
1094
|
+
} catch {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function deleteBranch(branchName, cwd) {
|
|
1099
|
+
logger.info(`\u5220\u9664\u5206\u652F: ${branchName}`);
|
|
1100
|
+
execCommand(`git branch -D ${branchName}`, { cwd });
|
|
1101
|
+
}
|
|
1102
|
+
function hasLocalCommits(branchName, cwd) {
|
|
1103
|
+
try {
|
|
1104
|
+
const output = execCommand(`git log HEAD..${branchName} --oneline`, { cwd });
|
|
1105
|
+
return output.trim() !== "";
|
|
1106
|
+
} catch {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function getCommitCountAhead(branchName, cwd) {
|
|
1111
|
+
const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
|
|
1112
|
+
return parseInt(output, 10) || 0;
|
|
1113
|
+
}
|
|
1114
|
+
function getCommitCountBehind(branchName, cwd) {
|
|
1115
|
+
try {
|
|
1116
|
+
const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
|
|
1117
|
+
return parseInt(output, 10) || 0;
|
|
1118
|
+
} catch {
|
|
1119
|
+
return 0;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function getCurrentBranch(cwd) {
|
|
1123
|
+
return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
1124
|
+
}
|
|
1095
1125
|
function gitCheckout(branchName, cwd) {
|
|
1096
1126
|
execCommand(`git checkout ${branchName}`, { cwd });
|
|
1097
1127
|
}
|
|
@@ -1099,6 +1129,22 @@ function createBranch(branchName, cwd) {
|
|
|
1099
1129
|
execCommand(`git branch ${branchName}`, { cwd });
|
|
1100
1130
|
}
|
|
1101
1131
|
|
|
1132
|
+
// src/utils/git-worktree.ts
|
|
1133
|
+
function createWorktree(branchName, worktreePath, cwd) {
|
|
1134
|
+
logger.info(`\u521B\u5EFA worktree: ${worktreePath}`);
|
|
1135
|
+
execCommand(`git worktree add -b ${branchName} "${worktreePath}"`, { cwd });
|
|
1136
|
+
}
|
|
1137
|
+
function removeWorktreeByPath(worktreePath, cwd) {
|
|
1138
|
+
logger.info(`\u79FB\u9664 worktree: ${worktreePath}`);
|
|
1139
|
+
execCommand(`git worktree remove -f "${worktreePath}"`, { cwd });
|
|
1140
|
+
}
|
|
1141
|
+
function gitWorktreeList(cwd) {
|
|
1142
|
+
return execCommand("git worktree list", { cwd });
|
|
1143
|
+
}
|
|
1144
|
+
function gitWorktreePrune(cwd) {
|
|
1145
|
+
execCommand("git worktree prune", { cwd });
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1102
1148
|
// src/utils/formatter.ts
|
|
1103
1149
|
import chalk4 from "chalk";
|
|
1104
1150
|
import { createInterface } from "readline";
|
|
@@ -1124,14 +1170,14 @@ function printDoubleSeparator() {
|
|
|
1124
1170
|
console.log(MESSAGES.DOUBLE_SEPARATOR);
|
|
1125
1171
|
}
|
|
1126
1172
|
function confirmAction(question) {
|
|
1127
|
-
return new Promise((
|
|
1173
|
+
return new Promise((resolve4) => {
|
|
1128
1174
|
const rl = createInterface({
|
|
1129
1175
|
input: process.stdin,
|
|
1130
1176
|
output: process.stdout
|
|
1131
1177
|
});
|
|
1132
1178
|
rl.question(`${question} (y/N) `, (answer) => {
|
|
1133
1179
|
rl.close();
|
|
1134
|
-
|
|
1180
|
+
resolve4(answer.toLowerCase() === "y");
|
|
1135
1181
|
});
|
|
1136
1182
|
});
|
|
1137
1183
|
}
|
|
@@ -1222,6 +1268,19 @@ function formatLocalISOString(date) {
|
|
|
1222
1268
|
const minutes = String(absMinutes % 60).padStart(2, "0");
|
|
1223
1269
|
return `${iso}${sign}${hours}:${minutes}`;
|
|
1224
1270
|
}
|
|
1271
|
+
function generateTaskFilename(prefix) {
|
|
1272
|
+
const now = /* @__PURE__ */ new Date();
|
|
1273
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1274
|
+
const timestamp = [
|
|
1275
|
+
now.getFullYear(),
|
|
1276
|
+
pad(now.getMonth() + 1),
|
|
1277
|
+
pad(now.getDate()),
|
|
1278
|
+
pad(now.getHours()),
|
|
1279
|
+
pad(now.getMinutes()),
|
|
1280
|
+
pad(now.getSeconds())
|
|
1281
|
+
].join("-");
|
|
1282
|
+
return `${prefix}-${timestamp}.md`;
|
|
1283
|
+
}
|
|
1225
1284
|
|
|
1226
1285
|
// src/utils/branch.ts
|
|
1227
1286
|
function sanitizeBranchName(branchName) {
|
|
@@ -1865,15 +1924,10 @@ function removeProjectSnapshots(projectName) {
|
|
|
1865
1924
|
}
|
|
1866
1925
|
|
|
1867
1926
|
// src/utils/worktree-matcher.ts
|
|
1868
|
-
import Enquirer3 from "enquirer";
|
|
1869
1927
|
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
|
-
}
|
|
1928
|
+
|
|
1929
|
+
// src/utils/ui-prompts.ts
|
|
1930
|
+
import Enquirer3 from "enquirer";
|
|
1877
1931
|
async function promptSelectBranch(worktrees, message) {
|
|
1878
1932
|
const selectedBranch = await new Enquirer3.Select({
|
|
1879
1933
|
message,
|
|
@@ -1923,6 +1977,77 @@ async function promptMultiSelectBranches(worktrees, message) {
|
|
|
1923
1977
|
}).run();
|
|
1924
1978
|
return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
|
|
1925
1979
|
}
|
|
1980
|
+
async function promptGroupedMultiSelectBranches(worktrees, message) {
|
|
1981
|
+
const groups = groupWorktreesByDate(worktrees);
|
|
1982
|
+
const choices = buildGroupedChoices(groups);
|
|
1983
|
+
const groupMembershipMap = buildGroupMembershipMap(groups);
|
|
1984
|
+
const groupSelectAllNames = new Set(groupMembershipMap.keys());
|
|
1985
|
+
const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
|
|
1986
|
+
const MultiSelect = Enquirer3.MultiSelect;
|
|
1987
|
+
class MultiSelectWithGroupSelectAll extends MultiSelect {
|
|
1988
|
+
space() {
|
|
1989
|
+
if (!this.focused) return;
|
|
1990
|
+
const focusedName = this.focused.name;
|
|
1991
|
+
if (focusedName === SELECT_ALL_NAME) {
|
|
1992
|
+
const willEnable = !this.focused.enabled;
|
|
1993
|
+
for (const ch of this.choices) {
|
|
1994
|
+
ch.enabled = willEnable;
|
|
1995
|
+
}
|
|
1996
|
+
return this.render();
|
|
1997
|
+
}
|
|
1998
|
+
if (groupSelectAllNames.has(focusedName)) {
|
|
1999
|
+
const willEnable = !this.focused.enabled;
|
|
2000
|
+
const memberNames = groupMembershipMap.get(focusedName);
|
|
2001
|
+
this.focused.enabled = willEnable;
|
|
2002
|
+
for (const ch of this.choices) {
|
|
2003
|
+
if (memberNames.includes(ch.name)) {
|
|
2004
|
+
ch.enabled = willEnable;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
syncGlobalSelectAll(this.choices);
|
|
2008
|
+
return this.render();
|
|
2009
|
+
}
|
|
2010
|
+
this.toggle(this.focused);
|
|
2011
|
+
syncGroupSelectAll(this.choices, focusedName);
|
|
2012
|
+
syncGlobalSelectAll(this.choices);
|
|
2013
|
+
return this.render();
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
function syncGlobalSelectAll(choiceList) {
|
|
2017
|
+
const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
|
|
2018
|
+
if (!selectAllChoice) return;
|
|
2019
|
+
const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
|
|
2020
|
+
selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
|
|
2021
|
+
}
|
|
2022
|
+
function syncGroupSelectAll(choiceList, branchName) {
|
|
2023
|
+
for (const [groupName, memberNames] of groupMembershipMap) {
|
|
2024
|
+
if (!memberNames.includes(branchName)) continue;
|
|
2025
|
+
const groupChoice = choiceList.find((ch) => ch.name === groupName);
|
|
2026
|
+
if (!groupChoice) continue;
|
|
2027
|
+
const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
|
|
2028
|
+
groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
const selectedBranches = await new MultiSelectWithGroupSelectAll({
|
|
2033
|
+
message,
|
|
2034
|
+
choices,
|
|
2035
|
+
// 使用空心圆/实心圆作为选中指示符
|
|
2036
|
+
symbols: {
|
|
2037
|
+
indicator: { on: "\u25CF", off: "\u25CB" }
|
|
2038
|
+
}
|
|
2039
|
+
}).run();
|
|
2040
|
+
return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/utils/worktree-matcher.ts
|
|
2044
|
+
function findExactMatch(worktrees, branchName) {
|
|
2045
|
+
return worktrees.find((wt) => wt.branch === branchName);
|
|
2046
|
+
}
|
|
2047
|
+
function findFuzzyMatches(worktrees, keyword) {
|
|
2048
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
2049
|
+
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
|
|
2050
|
+
}
|
|
1926
2051
|
async function resolveTargetWorktrees(worktrees, messages, branchName) {
|
|
1927
2052
|
if (worktrees.length === 0) {
|
|
1928
2053
|
throw new ClawtError(messages.noWorktrees);
|
|
@@ -2050,68 +2175,6 @@ function buildGroupMembershipMap(groups) {
|
|
|
2050
2175
|
}
|
|
2051
2176
|
return map;
|
|
2052
2177
|
}
|
|
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
2178
|
|
|
2116
2179
|
// src/utils/progress-render.ts
|
|
2117
2180
|
import chalk5 from "chalk";
|
|
@@ -2586,7 +2649,7 @@ function executeClaudeTask(worktree, task, onActivity) {
|
|
|
2586
2649
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2587
2650
|
}
|
|
2588
2651
|
);
|
|
2589
|
-
const promise = new Promise((
|
|
2652
|
+
const promise = new Promise((resolve4) => {
|
|
2590
2653
|
let stderr = "";
|
|
2591
2654
|
let finalResult = null;
|
|
2592
2655
|
const lineBuffer = createLineBuffer();
|
|
@@ -2622,7 +2685,7 @@ function executeClaudeTask(worktree, task, onActivity) {
|
|
|
2622
2685
|
if (finalResult) {
|
|
2623
2686
|
success = !finalResult.is_error;
|
|
2624
2687
|
}
|
|
2625
|
-
|
|
2688
|
+
resolve4({
|
|
2626
2689
|
task,
|
|
2627
2690
|
branch: worktree.branch,
|
|
2628
2691
|
worktreePath: worktree.path,
|
|
@@ -2632,7 +2695,7 @@ function executeClaudeTask(worktree, task, onActivity) {
|
|
|
2632
2695
|
});
|
|
2633
2696
|
});
|
|
2634
2697
|
child.on("error", (err) => {
|
|
2635
|
-
|
|
2698
|
+
resolve4({
|
|
2636
2699
|
task,
|
|
2637
2700
|
branch: worktree.branch,
|
|
2638
2701
|
worktreePath: worktree.path,
|
|
@@ -2691,7 +2754,7 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
|
|
|
2691
2754
|
const results = new Array(total);
|
|
2692
2755
|
let nextIndex = 0;
|
|
2693
2756
|
let completedCount = 0;
|
|
2694
|
-
return new Promise((
|
|
2757
|
+
return new Promise((resolve4) => {
|
|
2695
2758
|
function launchNext() {
|
|
2696
2759
|
if (nextIndex >= total || isInterrupted()) return;
|
|
2697
2760
|
const index = nextIndex;
|
|
@@ -2712,7 +2775,7 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
|
|
|
2712
2775
|
}
|
|
2713
2776
|
launchNext();
|
|
2714
2777
|
if (completedCount === total) {
|
|
2715
|
-
|
|
2778
|
+
resolve4(results);
|
|
2716
2779
|
}
|
|
2717
2780
|
});
|
|
2718
2781
|
}
|
|
@@ -2767,11 +2830,11 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
|
|
|
2767
2830
|
printWarning(MESSAGES.INTERRUPTED);
|
|
2768
2831
|
killAllChildProcesses(childProcesses);
|
|
2769
2832
|
await Promise.allSettled(childProcesses.map(
|
|
2770
|
-
(cp) => new Promise((
|
|
2833
|
+
(cp) => new Promise((resolve4) => {
|
|
2771
2834
|
if (cp.exitCode !== null) {
|
|
2772
|
-
|
|
2835
|
+
resolve4();
|
|
2773
2836
|
} else {
|
|
2774
|
-
cp.on("close", () =>
|
|
2837
|
+
cp.on("close", () => resolve4());
|
|
2775
2838
|
}
|
|
2776
2839
|
})
|
|
2777
2840
|
));
|
|
@@ -3013,7 +3076,7 @@ function isNewerVersion(latest, current) {
|
|
|
3013
3076
|
return false;
|
|
3014
3077
|
}
|
|
3015
3078
|
function fetchLatestVersion() {
|
|
3016
|
-
return new Promise((
|
|
3079
|
+
return new Promise((resolve4) => {
|
|
3017
3080
|
const req = request(NPM_REGISTRY_URL, { timeout: NPM_REGISTRY_TIMEOUT_MS }, (res) => {
|
|
3018
3081
|
let data = "";
|
|
3019
3082
|
res.on("data", (chunk) => {
|
|
@@ -3022,16 +3085,16 @@ function fetchLatestVersion() {
|
|
|
3022
3085
|
res.on("end", () => {
|
|
3023
3086
|
try {
|
|
3024
3087
|
const parsed = JSON.parse(data);
|
|
3025
|
-
|
|
3088
|
+
resolve4(parsed.version ?? null);
|
|
3026
3089
|
} catch {
|
|
3027
|
-
|
|
3090
|
+
resolve4(null);
|
|
3028
3091
|
}
|
|
3029
3092
|
});
|
|
3030
3093
|
});
|
|
3031
|
-
req.on("error", () =>
|
|
3094
|
+
req.on("error", () => resolve4(null));
|
|
3032
3095
|
req.on("timeout", () => {
|
|
3033
3096
|
req.destroy();
|
|
3034
|
-
|
|
3097
|
+
resolve4(null);
|
|
3035
3098
|
});
|
|
3036
3099
|
req.end();
|
|
3037
3100
|
});
|
|
@@ -3252,7 +3315,7 @@ import { createInterface as createInterface2 } from "readline";
|
|
|
3252
3315
|
import chalk9 from "chalk";
|
|
3253
3316
|
import stringWidth3 from "string-width";
|
|
3254
3317
|
function buildSeparatorWithHint(cols, hint) {
|
|
3255
|
-
const maxWidth = Math.min(cols,
|
|
3318
|
+
const maxWidth = Math.min(cols, PANEL_SEPARATOR_MAX_WIDTH);
|
|
3256
3319
|
if (!hint) {
|
|
3257
3320
|
return chalk9.gray("\u2500".repeat(maxWidth));
|
|
3258
3321
|
}
|
|
@@ -3337,10 +3400,10 @@ function buildGroupedWorktreeLines(worktrees, selectedIndex) {
|
|
|
3337
3400
|
function renderDateSeparator(dateKey) {
|
|
3338
3401
|
const leftPad = " ";
|
|
3339
3402
|
if (dateKey === UNKNOWN_DATE_GROUP) {
|
|
3340
|
-
return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex(
|
|
3403
|
+
return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex(PANEL_DATE_COLOR)(PANEL_UNKNOWN_DATE)} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
|
|
3341
3404
|
}
|
|
3342
3405
|
const relativeDate = formatRelativeDate(dateKey);
|
|
3343
|
-
return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex(
|
|
3406
|
+
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
3407
|
}
|
|
3345
3408
|
function renderWorktreeBlock(wt, isSelected) {
|
|
3346
3409
|
const lines = [];
|
|
@@ -3353,12 +3416,12 @@ function renderWorktreeBlock(wt, isSelected) {
|
|
|
3353
3416
|
lines.push(`${indent}${chalk9.green(`+${wt.insertions}`)} ${chalk9.red(`-${wt.deletions}`)}`);
|
|
3354
3417
|
}
|
|
3355
3418
|
if (wt.commitsAhead > 0) {
|
|
3356
|
-
lines.push(`${indent}${chalk9.yellow(
|
|
3419
|
+
lines.push(`${indent}${chalk9.yellow(PANEL_COMMITS_AHEAD(wt.commitsAhead))}`);
|
|
3357
3420
|
}
|
|
3358
3421
|
if (wt.commitsBehind > 0) {
|
|
3359
|
-
lines.push(`${indent}${chalk9.yellow(
|
|
3422
|
+
lines.push(`${indent}${chalk9.yellow(PANEL_COMMITS_BEHIND(wt.commitsBehind))}`);
|
|
3360
3423
|
} else {
|
|
3361
|
-
lines.push(`${indent}${chalk9.green(
|
|
3424
|
+
lines.push(`${indent}${chalk9.green(PANEL_SYNCED_WITH_MAIN)}`);
|
|
3362
3425
|
}
|
|
3363
3426
|
if (wt.createdAt) {
|
|
3364
3427
|
const relativeTime = formatRelativeTime(wt.createdAt);
|
|
@@ -3412,16 +3475,166 @@ function calculateVisibleRows(terminalRows) {
|
|
|
3412
3475
|
return Math.max(terminalRows - fixedRows, 3);
|
|
3413
3476
|
}
|
|
3414
3477
|
|
|
3415
|
-
// src/utils/
|
|
3416
|
-
var
|
|
3478
|
+
// src/utils/keyboard-controller.ts
|
|
3479
|
+
var KeyboardController = class {
|
|
3480
|
+
/** stdin 数据处理器引用(用于清理) */
|
|
3481
|
+
stdinDataHandler = null;
|
|
3482
|
+
/** 按键回调函数 */
|
|
3483
|
+
onKeypress;
|
|
3484
|
+
/**
|
|
3485
|
+
* 创建键盘事件控制器
|
|
3486
|
+
* @param {(data: Buffer) => void} onKeypress - 按键回调函数
|
|
3487
|
+
*/
|
|
3488
|
+
constructor(onKeypress) {
|
|
3489
|
+
this.onKeypress = onKeypress;
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* 启动键盘监听
|
|
3493
|
+
* 将 stdin 设为 raw 模式以捕获每个按键
|
|
3494
|
+
*/
|
|
3495
|
+
start() {
|
|
3496
|
+
if (process.stdin.isTTY) {
|
|
3497
|
+
process.stdin.setRawMode(true);
|
|
3498
|
+
}
|
|
3499
|
+
process.stdin.resume();
|
|
3500
|
+
this.stdinDataHandler = (data) => {
|
|
3501
|
+
this.onKeypress(data);
|
|
3502
|
+
};
|
|
3503
|
+
process.stdin.on("data", this.stdinDataHandler);
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* 停止键盘监听,恢复 stdin 状态
|
|
3507
|
+
*/
|
|
3508
|
+
stop() {
|
|
3509
|
+
if (this.stdinDataHandler) {
|
|
3510
|
+
process.stdin.removeListener("data", this.stdinDataHandler);
|
|
3511
|
+
this.stdinDataHandler = null;
|
|
3512
|
+
}
|
|
3513
|
+
if (process.stdin.isTTY) {
|
|
3514
|
+
process.stdin.setRawMode(false);
|
|
3515
|
+
}
|
|
3516
|
+
process.stdin.pause();
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
|
|
3520
|
+
// src/utils/interactive-panel-state.ts
|
|
3521
|
+
var PanelStateManager = class {
|
|
3417
3522
|
/** 当前状态数据 */
|
|
3418
|
-
statusResult;
|
|
3523
|
+
statusResult = null;
|
|
3419
3524
|
/** 当前选中的显示位置索引(对应 displayOrder 数组的下标) */
|
|
3420
|
-
selectedDisplayIndex;
|
|
3525
|
+
selectedDisplayIndex = 0;
|
|
3421
3526
|
/** 显示顺序到原始索引的映射(按日期分组后的排列顺序) */
|
|
3422
|
-
displayOrder;
|
|
3527
|
+
displayOrder = [];
|
|
3423
3528
|
/** 滚动偏移(基于行数) */
|
|
3424
|
-
scrollOffset;
|
|
3529
|
+
scrollOffset = 0;
|
|
3530
|
+
/**
|
|
3531
|
+
* 更新状态数据
|
|
3532
|
+
* @param {StatusResult} newStatus - 新的状态数据
|
|
3533
|
+
* @param {string} [previousBranch] - 刷新前选中的分支名
|
|
3534
|
+
*/
|
|
3535
|
+
updateData(newStatus, previousBranch) {
|
|
3536
|
+
this.statusResult = newStatus;
|
|
3537
|
+
this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
|
|
3538
|
+
if (previousBranch && this.displayOrder.length > 0) {
|
|
3539
|
+
const newDisplayIndex = this.displayOrder.findIndex(
|
|
3540
|
+
(origIdx) => this.statusResult.worktrees[origIdx]?.branch === previousBranch
|
|
3541
|
+
);
|
|
3542
|
+
if (newDisplayIndex >= 0) {
|
|
3543
|
+
this.selectedDisplayIndex = newDisplayIndex;
|
|
3544
|
+
} else {
|
|
3545
|
+
this.selectedDisplayIndex = Math.min(this.selectedDisplayIndex, Math.max(0, this.displayOrder.length - 1));
|
|
3546
|
+
}
|
|
3547
|
+
} else {
|
|
3548
|
+
this.selectedDisplayIndex = 0;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
/** 获取当前状态数据 */
|
|
3552
|
+
getStatusResult() {
|
|
3553
|
+
return this.statusResult;
|
|
3554
|
+
}
|
|
3555
|
+
/** 获取当前选中的原始索引 */
|
|
3556
|
+
getSelectedOriginalIndex() {
|
|
3557
|
+
return this.displayOrder[this.selectedDisplayIndex] ?? -1;
|
|
3558
|
+
}
|
|
3559
|
+
/** 获取当前滚动偏移 */
|
|
3560
|
+
getScrollOffset() {
|
|
3561
|
+
return this.scrollOffset;
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* 向上导航
|
|
3565
|
+
* @returns {boolean} 是否发生变化
|
|
3566
|
+
*/
|
|
3567
|
+
navigateUp() {
|
|
3568
|
+
if (!this.statusResult || this.displayOrder.length === 0) return false;
|
|
3569
|
+
if (this.selectedDisplayIndex > 0) {
|
|
3570
|
+
this.selectedDisplayIndex--;
|
|
3571
|
+
this.adjustScrollForSelection();
|
|
3572
|
+
return true;
|
|
3573
|
+
}
|
|
3574
|
+
return false;
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* 向下导航
|
|
3578
|
+
* @returns {boolean} 是否发生变化
|
|
3579
|
+
*/
|
|
3580
|
+
navigateDown() {
|
|
3581
|
+
if (!this.statusResult || this.displayOrder.length === 0) return false;
|
|
3582
|
+
if (this.selectedDisplayIndex < this.displayOrder.length - 1) {
|
|
3583
|
+
this.selectedDisplayIndex++;
|
|
3584
|
+
this.adjustScrollForSelection();
|
|
3585
|
+
return true;
|
|
3586
|
+
}
|
|
3587
|
+
return false;
|
|
3588
|
+
}
|
|
3589
|
+
/**
|
|
3590
|
+
* 获取当前选中的分支名
|
|
3591
|
+
* @returns {string | null} 分支名
|
|
3592
|
+
*/
|
|
3593
|
+
getSelectedBranch() {
|
|
3594
|
+
const originalIndex = this.getSelectedOriginalIndex();
|
|
3595
|
+
if (originalIndex === -1 || !this.statusResult) return null;
|
|
3596
|
+
return this.statusResult.worktrees[originalIndex]?.branch || null;
|
|
3597
|
+
}
|
|
3598
|
+
/**
|
|
3599
|
+
* 调整滚动位置以确保选中项在可见区域内
|
|
3600
|
+
*/
|
|
3601
|
+
adjustScrollForSelection() {
|
|
3602
|
+
if (!this.statusResult || this.displayOrder.length === 0) return;
|
|
3603
|
+
const originalIndex = this.getSelectedOriginalIndex();
|
|
3604
|
+
const rows = process.stdout.rows || 24;
|
|
3605
|
+
const visibleRows = calculateVisibleRows(rows);
|
|
3606
|
+
const panelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
|
|
3607
|
+
let firstLine = -1;
|
|
3608
|
+
let lastLine = -1;
|
|
3609
|
+
for (let i = 0; i < panelLines.length; i++) {
|
|
3610
|
+
if (panelLines[i].worktreeIndex === originalIndex) {
|
|
3611
|
+
if (firstLine === -1) firstLine = i;
|
|
3612
|
+
lastLine = i;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
if (firstLine === -1) return;
|
|
3616
|
+
let groupStart = firstLine;
|
|
3617
|
+
while (groupStart > 0 && panelLines[groupStart - 1].type === "separator") {
|
|
3618
|
+
groupStart--;
|
|
3619
|
+
}
|
|
3620
|
+
if (groupStart < this.scrollOffset) {
|
|
3621
|
+
this.scrollOffset = groupStart;
|
|
3622
|
+
}
|
|
3623
|
+
if (lastLine >= this.scrollOffset + visibleRows) {
|
|
3624
|
+
this.scrollOffset = lastLine - visibleRows + 1;
|
|
3625
|
+
}
|
|
3626
|
+
if (this.scrollOffset > groupStart) {
|
|
3627
|
+
this.scrollOffset = groupStart;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
};
|
|
3631
|
+
|
|
3632
|
+
// src/utils/interactive-panel.ts
|
|
3633
|
+
var InteractivePanel = class {
|
|
3634
|
+
/** 状态管理器 */
|
|
3635
|
+
stateManager;
|
|
3636
|
+
/** 键盘控制器 */
|
|
3637
|
+
keyboardController;
|
|
3425
3638
|
/** 数据刷新定时器引用 */
|
|
3426
3639
|
refreshTimer;
|
|
3427
3640
|
/** 倒计时定时器引用 */
|
|
@@ -3436,8 +3649,6 @@ var InteractivePanel = class {
|
|
|
3436
3649
|
resizeHandler;
|
|
3437
3650
|
/** exit 兜底处理器 */
|
|
3438
3651
|
exitHandler;
|
|
3439
|
-
/** stdin 数据处理器引用(用于清理) */
|
|
3440
|
-
stdinDataHandler;
|
|
3441
3652
|
/** 操作锁(防止操作期间响应按键) */
|
|
3442
3653
|
isOperating;
|
|
3443
3654
|
/** Promise resolve 函数(stop 时调用以完成 start 返回的 Promise) */
|
|
@@ -3449,10 +3660,8 @@ var InteractivePanel = class {
|
|
|
3449
3660
|
* @param {() => StatusResult} collectStatusFn - 数据收集函数
|
|
3450
3661
|
*/
|
|
3451
3662
|
constructor(collectStatusFn) {
|
|
3452
|
-
this.
|
|
3453
|
-
this.
|
|
3454
|
-
this.displayOrder = [];
|
|
3455
|
-
this.scrollOffset = 0;
|
|
3663
|
+
this.stateManager = new PanelStateManager();
|
|
3664
|
+
this.keyboardController = new KeyboardController(this.handleKeypress.bind(this));
|
|
3456
3665
|
this.refreshTimer = null;
|
|
3457
3666
|
this.countdownTimer = null;
|
|
3458
3667
|
this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1e3;
|
|
@@ -3460,7 +3669,6 @@ var InteractivePanel = class {
|
|
|
3460
3669
|
this.isTTY = !!process.stdout.isTTY;
|
|
3461
3670
|
this.resizeHandler = null;
|
|
3462
3671
|
this.exitHandler = null;
|
|
3463
|
-
this.stdinDataHandler = null;
|
|
3464
3672
|
this.isOperating = false;
|
|
3465
3673
|
this.resolveStart = null;
|
|
3466
3674
|
this.collectStatusFn = collectStatusFn;
|
|
@@ -3475,12 +3683,11 @@ var InteractivePanel = class {
|
|
|
3475
3683
|
console.log(PANEL_NOT_TTY);
|
|
3476
3684
|
return Promise.resolve();
|
|
3477
3685
|
}
|
|
3478
|
-
return new Promise((
|
|
3479
|
-
this.resolveStart =
|
|
3480
|
-
this.
|
|
3481
|
-
this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
|
|
3686
|
+
return new Promise((resolve4) => {
|
|
3687
|
+
this.resolveStart = resolve4;
|
|
3688
|
+
this.stateManager.updateData(this.collectStatusFn());
|
|
3482
3689
|
this.initTerminal();
|
|
3483
|
-
this.
|
|
3690
|
+
this.keyboardController.start();
|
|
3484
3691
|
this.startAutoRefresh();
|
|
3485
3692
|
this.render();
|
|
3486
3693
|
});
|
|
@@ -3492,7 +3699,7 @@ var InteractivePanel = class {
|
|
|
3492
3699
|
if (this.stopped) return;
|
|
3493
3700
|
this.stopped = true;
|
|
3494
3701
|
this.clearTimers();
|
|
3495
|
-
this.
|
|
3702
|
+
this.keyboardController.stop();
|
|
3496
3703
|
this.restoreTerminal();
|
|
3497
3704
|
if (this.resizeHandler) {
|
|
3498
3705
|
process.stdout.removeListener("resize", this.resizeHandler);
|
|
@@ -3516,6 +3723,7 @@ var InteractivePanel = class {
|
|
|
3516
3723
|
process.stdout.write(LINE_WRAP_DISABLE);
|
|
3517
3724
|
this.resizeHandler = () => {
|
|
3518
3725
|
if (!this.stopped && !this.isOperating) {
|
|
3726
|
+
this.stateManager.adjustScrollForSelection();
|
|
3519
3727
|
this.render();
|
|
3520
3728
|
}
|
|
3521
3729
|
};
|
|
@@ -3535,33 +3743,6 @@ var InteractivePanel = class {
|
|
|
3535
3743
|
process.stdout.write(CURSOR_SHOW);
|
|
3536
3744
|
process.stdout.write(ALT_SCREEN_LEAVE);
|
|
3537
3745
|
}
|
|
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
3746
|
/**
|
|
3566
3747
|
* 处理键盘输入
|
|
3567
3748
|
* @param {Buffer} data - 按键数据
|
|
@@ -3574,11 +3755,15 @@ var InteractivePanel = class {
|
|
|
3574
3755
|
return;
|
|
3575
3756
|
}
|
|
3576
3757
|
if (str === KEY_ARROW_UP) {
|
|
3577
|
-
this.navigateUp()
|
|
3758
|
+
if (this.stateManager.navigateUp()) {
|
|
3759
|
+
this.render();
|
|
3760
|
+
}
|
|
3578
3761
|
return;
|
|
3579
3762
|
}
|
|
3580
3763
|
if (str === KEY_ARROW_DOWN) {
|
|
3581
|
-
this.navigateDown()
|
|
3764
|
+
if (this.stateManager.navigateDown()) {
|
|
3765
|
+
this.render();
|
|
3766
|
+
}
|
|
3582
3767
|
return;
|
|
3583
3768
|
}
|
|
3584
3769
|
const key = str.toLowerCase();
|
|
@@ -3615,67 +3800,6 @@ var InteractivePanel = class {
|
|
|
3615
3800
|
return;
|
|
3616
3801
|
}
|
|
3617
3802
|
}
|
|
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
3803
|
/**
|
|
3680
3804
|
* 启动自动刷新:数据刷新定时器 + 倒计时定时器
|
|
3681
3805
|
*/
|
|
@@ -3711,45 +3835,25 @@ var InteractivePanel = class {
|
|
|
3711
3835
|
*/
|
|
3712
3836
|
refreshData() {
|
|
3713
3837
|
if (this.stopped || this.isOperating) return;
|
|
3714
|
-
const
|
|
3715
|
-
|
|
3716
|
-
this.
|
|
3717
|
-
this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
|
|
3718
|
-
this.restoreSelection(previousBranch);
|
|
3838
|
+
const previousBranch = this.stateManager.getSelectedBranch();
|
|
3839
|
+
this.stateManager.updateData(this.collectStatusFn(), previousBranch || void 0);
|
|
3840
|
+
this.stateManager.adjustScrollForSelection();
|
|
3719
3841
|
this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1e3;
|
|
3720
3842
|
this.render();
|
|
3721
3843
|
}
|
|
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
3844
|
/**
|
|
3742
3845
|
* 渲染一帧面板内容
|
|
3743
3846
|
* 使用同步输出防止闪烁
|
|
3744
3847
|
*/
|
|
3745
3848
|
render() {
|
|
3746
|
-
|
|
3849
|
+
const statusResult = this.stateManager.getStatusResult();
|
|
3850
|
+
if (this.stopped || this.isOperating || !statusResult) return;
|
|
3747
3851
|
const cols = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
3748
3852
|
const rows = process.stdout.rows || 24;
|
|
3749
3853
|
const frameLines = buildPanelFrame(
|
|
3750
|
-
|
|
3751
|
-
this.getSelectedOriginalIndex(),
|
|
3752
|
-
this.
|
|
3854
|
+
statusResult,
|
|
3855
|
+
this.stateManager.getSelectedOriginalIndex(),
|
|
3856
|
+
this.stateManager.getScrollOffset(),
|
|
3753
3857
|
rows,
|
|
3754
3858
|
cols,
|
|
3755
3859
|
this.refreshCountdown
|
|
@@ -3768,63 +3872,56 @@ var InteractivePanel = class {
|
|
|
3768
3872
|
* @param {() => void} action - 要执行的操作
|
|
3769
3873
|
*/
|
|
3770
3874
|
async executeOperation(action) {
|
|
3771
|
-
|
|
3875
|
+
const statusResult = this.stateManager.getStatusResult();
|
|
3876
|
+
if (!statusResult || this.stateManager.getSelectedOriginalIndex() === -1) return;
|
|
3772
3877
|
this.isOperating = true;
|
|
3773
3878
|
this.clearTimers();
|
|
3774
3879
|
this.restoreTerminal();
|
|
3775
|
-
this.
|
|
3880
|
+
this.keyboardController.stop();
|
|
3776
3881
|
action();
|
|
3777
3882
|
console.log(PANEL_PRESS_ENTER_TO_RETURN);
|
|
3778
3883
|
await this.waitForEnter();
|
|
3779
3884
|
this.initTerminal();
|
|
3780
|
-
this.
|
|
3885
|
+
this.keyboardController.start();
|
|
3781
3886
|
this.isOperating = false;
|
|
3782
3887
|
this.refreshData();
|
|
3783
3888
|
this.startAutoRefresh();
|
|
3784
3889
|
this.render();
|
|
3785
3890
|
}
|
|
3786
|
-
/**
|
|
3787
|
-
* 获取当前选中的分支名
|
|
3788
|
-
* @returns {string} 当前选中的分支名
|
|
3789
|
-
*/
|
|
3790
|
-
getSelectedBranch() {
|
|
3791
|
-
const originalIndex = this.getSelectedOriginalIndex();
|
|
3792
|
-
return this.statusResult.worktrees[originalIndex].branch;
|
|
3793
|
-
}
|
|
3794
3891
|
/**
|
|
3795
3892
|
* 执行验证操作
|
|
3796
3893
|
*/
|
|
3797
3894
|
handleValidate() {
|
|
3798
|
-
const branch = this.getSelectedBranch();
|
|
3799
|
-
runCommandInherited(`clawt validate -b ${branch}`);
|
|
3895
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
3896
|
+
if (branch) runCommandInherited(`clawt validate -b ${branch}`);
|
|
3800
3897
|
}
|
|
3801
3898
|
/**
|
|
3802
3899
|
* 执行合并操作
|
|
3803
3900
|
*/
|
|
3804
3901
|
handleMerge() {
|
|
3805
|
-
const branch = this.getSelectedBranch();
|
|
3806
|
-
runCommandInherited(`clawt merge -b ${branch}`);
|
|
3902
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
3903
|
+
if (branch) runCommandInherited(`clawt merge -b ${branch}`);
|
|
3807
3904
|
}
|
|
3808
3905
|
/**
|
|
3809
3906
|
* 执行删除操作
|
|
3810
3907
|
*/
|
|
3811
3908
|
handleDelete() {
|
|
3812
|
-
const branch = this.getSelectedBranch();
|
|
3813
|
-
runCommandInherited(`clawt remove -b ${branch}`);
|
|
3909
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
3910
|
+
if (branch) runCommandInherited(`clawt remove -b ${branch}`);
|
|
3814
3911
|
}
|
|
3815
3912
|
/**
|
|
3816
3913
|
* 执行恢复操作
|
|
3817
3914
|
*/
|
|
3818
3915
|
handleResume() {
|
|
3819
|
-
const branch = this.getSelectedBranch();
|
|
3820
|
-
runCommandInherited(`clawt resume -b ${branch}`);
|
|
3916
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
3917
|
+
if (branch) runCommandInherited(`clawt resume -b ${branch}`);
|
|
3821
3918
|
}
|
|
3822
3919
|
/**
|
|
3823
3920
|
* 执行同步操作
|
|
3824
3921
|
*/
|
|
3825
3922
|
handleSync() {
|
|
3826
|
-
const branch = this.getSelectedBranch();
|
|
3827
|
-
runCommandInherited(`clawt sync -b ${branch}`);
|
|
3923
|
+
const branch = this.stateManager.getSelectedBranch();
|
|
3924
|
+
if (branch) runCommandInherited(`clawt sync -b ${branch}`);
|
|
3828
3925
|
}
|
|
3829
3926
|
/**
|
|
3830
3927
|
* 执行覆盖操作
|
|
@@ -3838,14 +3935,14 @@ var InteractivePanel = class {
|
|
|
3838
3935
|
* @returns {Promise<void>} 用户按回车时 resolve
|
|
3839
3936
|
*/
|
|
3840
3937
|
waitForEnter() {
|
|
3841
|
-
return new Promise((
|
|
3938
|
+
return new Promise((resolve4) => {
|
|
3842
3939
|
const rl = createInterface2({
|
|
3843
3940
|
input: process.stdin,
|
|
3844
3941
|
output: process.stdout
|
|
3845
3942
|
});
|
|
3846
3943
|
rl.once("line", () => {
|
|
3847
3944
|
rl.close();
|
|
3848
|
-
|
|
3945
|
+
resolve4();
|
|
3849
3946
|
});
|
|
3850
3947
|
});
|
|
3851
3948
|
}
|
|
@@ -5386,6 +5483,28 @@ async function handleHome() {
|
|
|
5386
5483
|
printSuccess(MESSAGES.HOME_SWITCH_SUCCESS(currentBranch, mainBranch));
|
|
5387
5484
|
}
|
|
5388
5485
|
|
|
5486
|
+
// src/commands/tasks.ts
|
|
5487
|
+
import { resolve as resolve3, dirname as dirname2, join as join10 } from "path";
|
|
5488
|
+
import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
|
|
5489
|
+
function registerTasksCommand(program2) {
|
|
5490
|
+
const taskCmd = program2.command("tasks").description("\u4EFB\u52A1\u6587\u4EF6\u7BA1\u7406");
|
|
5491
|
+
taskCmd.command("init").description("\u751F\u6210\u4EFB\u52A1\u6A21\u677F\u6587\u4EF6").argument("[path]", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (path2) => {
|
|
5492
|
+
const filePath = path2 ?? join10(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
|
|
5493
|
+
await handleTasksInit(filePath);
|
|
5494
|
+
});
|
|
5495
|
+
}
|
|
5496
|
+
async function handleTasksInit(filePath) {
|
|
5497
|
+
const absolutePath = resolve3(filePath);
|
|
5498
|
+
logger.info(`tasks init \u547D\u4EE4\u6267\u884C\uFF0C\u76EE\u6807\u6587\u4EF6: ${absolutePath}`);
|
|
5499
|
+
if (existsSync13(absolutePath)) {
|
|
5500
|
+
throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
|
|
5501
|
+
}
|
|
5502
|
+
ensureDir(dirname2(absolutePath));
|
|
5503
|
+
writeFileSync6(absolutePath, TASK_TEMPLATE_CONTENT, "utf-8");
|
|
5504
|
+
printSuccess(MESSAGES.TASK_INIT_SUCCESS(filePath));
|
|
5505
|
+
printHint(MESSAGES.TASK_INIT_HINT(filePath));
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5389
5508
|
// src/index.ts
|
|
5390
5509
|
var require2 = createRequire(import.meta.url);
|
|
5391
5510
|
var { version } = require2("../package.json");
|
|
@@ -5414,6 +5533,7 @@ registerProjectsCommand(program);
|
|
|
5414
5533
|
registerCompletionCommand(program);
|
|
5415
5534
|
registerInitCommand(program);
|
|
5416
5535
|
registerHomeCommand(program);
|
|
5536
|
+
registerTasksCommand(program);
|
|
5417
5537
|
var config = loadConfig();
|
|
5418
5538
|
applyAliases(program, config.aliases);
|
|
5419
5539
|
process.on("uncaughtException", (error) => {
|