clawt 2.18.0 → 2.20.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/dist/index.js CHANGED
@@ -373,6 +373,32 @@ var ALIAS_MESSAGES = {
373
373
  ALIAS_LIST_TITLE: "\u5F53\u524D\u522B\u540D\u5217\u8868\uFF1A"
374
374
  };
375
375
 
376
+ // src/constants/messages/projects.ts
377
+ var PROJECTS_MESSAGES = {
378
+ /** projects 命令全局概览标题 */
379
+ PROJECTS_OVERVIEW_TITLE: "\u9879\u76EE\u6982\u89C8",
380
+ /** projects 命令指定项目详情标题 */
381
+ PROJECTS_DETAIL_TITLE: (projectName) => `\u9879\u76EE\u8BE6\u60C5: ${projectName}`,
382
+ /** 无项目提示 */
383
+ PROJECTS_NO_PROJECTS: "(\u6682\u65E0\u9879\u76EE\uFF0Cworktrees \u76EE\u5F55\u4E3A\u7A7A)",
384
+ /** 项目不存在提示 */
385
+ PROJECTS_NOT_FOUND: (name) => `\u9879\u76EE ${name} \u4E0D\u5B58\u5728`,
386
+ /** worktree 数量标签 */
387
+ PROJECTS_WORKTREE_COUNT: (count) => `${count} \u4E2A worktree`,
388
+ /** 最近活跃时间标签 */
389
+ PROJECTS_LAST_ACTIVE: (relativeTime) => `\u6700\u8FD1\u6D3B\u8DC3: ${relativeTime}`,
390
+ /** 磁盘占用标签 */
391
+ PROJECTS_DISK_USAGE: (size) => `\u78C1\u76D8\u5360\u7528: ${size}`,
392
+ /** 总磁盘占用标签 */
393
+ PROJECTS_TOTAL_DISK_USAGE: (size) => `\u603B\u5360\u7528: ${size}`,
394
+ /** projects 详情无 worktree */
395
+ PROJECTS_DETAIL_NO_WORKTREES: "(\u8BE5\u9879\u76EE\u4E0B\u65E0 worktree)",
396
+ /** 路径标签 */
397
+ PROJECTS_PATH: (path2) => `\u8DEF\u5F84: ${path2}`,
398
+ /** 最后修改时间标签 */
399
+ PROJECTS_LAST_MODIFIED: (relativeTime) => `\u6700\u540E\u4FEE\u6539: ${relativeTime}`
400
+ };
401
+
376
402
  // src/constants/messages/completion.ts
377
403
  var COMPLETION_MESSAGES = {
378
404
  /** completion 命令的主描述 */
@@ -422,6 +448,7 @@ var MESSAGES = {
422
448
  ...CONFIG_CMD_MESSAGES,
423
449
  ...STATUS_MESSAGES,
424
450
  ...ALIAS_MESSAGES,
451
+ ...PROJECTS_MESSAGES,
425
452
  ...COMPLETION_MESSAGES
426
453
  };
427
454
 
@@ -540,8 +567,14 @@ var CLEAR_SCREEN = "\x1B[2J";
540
567
  var CURSOR_HOME = "\x1B[H";
541
568
 
542
569
  // src/constants/prompt.ts
570
+ import chalk from "chalk";
543
571
  var SELECT_ALL_NAME = "__select_all__";
544
572
  var SELECT_ALL_LABEL = "[select-all]";
573
+ var GROUP_SELECT_ALL_PREFIX = "__group_select_all_";
574
+ var GROUP_SELECT_ALL_LABEL = (dateLabel) => `[select-all: ${dateLabel}]`;
575
+ var GROUP_SEPARATOR_LABEL = (dateLabel, relativeTime) => `\u2550\u2550\u2550\u2550 ${chalk.bold.hex("#FF8C00")(dateLabel)}\uFF08${chalk.hex("#FF8C00")(relativeTime)}\uFF09 \u2550\u2550\u2550\u2550`;
576
+ var UNKNOWN_DATE_GROUP = "\u672A\u77E5\u65E5\u671F";
577
+ var UNKNOWN_DATE_SEPARATOR_LABEL = `\u2550\u2550\u2550\u2550 ${chalk.bold.hex("#FF8C00")("\u672A\u77E5\u65E5\u671F")} \u2550\u2550\u2550\u2550`;
545
578
 
546
579
  // src/errors/index.ts
547
580
  var ClawtError = class extends Error {
@@ -561,7 +594,7 @@ var ClawtError = class extends Error {
561
594
  // src/logger/index.ts
562
595
  import winston from "winston";
563
596
  import DailyRotateFile from "winston-daily-rotate-file";
564
- import chalk from "chalk";
597
+ import chalk2 from "chalk";
565
598
  import { existsSync, mkdirSync } from "fs";
566
599
  if (!existsSync(LOGS_DIR)) {
567
600
  mkdirSync(LOGS_DIR, { recursive: true });
@@ -586,13 +619,13 @@ var logger = winston.createLogger({
586
619
  transports: [dailyRotateTransport]
587
620
  });
588
621
  var LEVEL_COLORS = {
589
- error: chalk.red,
590
- warn: chalk.yellow,
591
- info: chalk.cyan,
592
- debug: chalk.gray
622
+ error: chalk2.red,
623
+ warn: chalk2.yellow,
624
+ info: chalk2.cyan,
625
+ debug: chalk2.gray
593
626
  };
594
627
  function colorizeLevel(level) {
595
- const colorFn = LEVEL_COLORS[level] || chalk.white;
628
+ const colorFn = LEVEL_COLORS[level] || chalk2.white;
596
629
  return colorFn(level.toUpperCase().padEnd(5));
597
630
  }
598
631
  function enableConsoleTransport() {
@@ -603,7 +636,7 @@ function enableConsoleTransport() {
603
636
  return;
604
637
  }
605
638
  const consoleFormat = winston.format.printf(({ level, message, timestamp }) => {
606
- return `${chalk.gray(timestamp)} ${colorizeLevel(level)} ${message}`;
639
+ return `${chalk2.gray(timestamp)} ${colorizeLevel(level)} ${message}`;
607
640
  });
608
641
  const consoleTransport = new winston.transports.Console({
609
642
  level: "debug",
@@ -871,22 +904,22 @@ function getBranchCreatedAt(branchName, cwd) {
871
904
  }
872
905
 
873
906
  // src/utils/formatter.ts
874
- import chalk2 from "chalk";
907
+ import chalk3 from "chalk";
875
908
  import { createInterface } from "readline";
876
909
  function printSuccess(message) {
877
- console.log(chalk2.green(message));
910
+ console.log(chalk3.green(message));
878
911
  }
879
912
  function printError(message) {
880
- console.error(chalk2.red(`\u2717 ${message}`));
913
+ console.error(chalk3.red(`\u2717 ${message}`));
881
914
  }
882
915
  function printWarning(message) {
883
- console.log(chalk2.yellow(`\u26A0 ${message}`));
916
+ console.log(chalk3.yellow(`\u26A0 ${message}`));
884
917
  }
885
918
  function printInfo(message) {
886
919
  console.log(message);
887
920
  }
888
921
  function printHint(message) {
889
- console.log(chalk2.hex("#FF8C00")(message));
922
+ console.log(chalk3.hex("#FF8C00")(message));
890
923
  }
891
924
  function printSeparator() {
892
925
  console.log(MESSAGES.SEPARATOR);
@@ -907,7 +940,7 @@ function confirmAction(question) {
907
940
  });
908
941
  }
909
942
  function confirmDestructiveAction(dangerousCommand, description) {
910
- printWarning(`\u5373\u5C06\u6267\u884C ${chalk2.red.bold(dangerousCommand)}\uFF0C${description}`);
943
+ printWarning(`\u5373\u5C06\u6267\u884C ${chalk3.red.bold(dangerousCommand)}\uFF0C${description}`);
911
944
  return confirmAction("\u662F\u5426\u7EE7\u7EED\uFF1F");
912
945
  }
913
946
  function isWorktreeIdle(status) {
@@ -915,17 +948,17 @@ function isWorktreeIdle(status) {
915
948
  }
916
949
  function formatWorktreeStatus(status) {
917
950
  const parts = [];
918
- parts.push(chalk2.yellow(`${status.commitCount} \u4E2A\u63D0\u4EA4`));
951
+ parts.push(chalk3.yellow(`${status.commitCount} \u4E2A\u63D0\u4EA4`));
919
952
  if (status.insertions === 0 && status.deletions === 0) {
920
953
  parts.push("\u65E0\u53D8\u66F4");
921
954
  } else {
922
955
  const diffParts = [];
923
- diffParts.push(chalk2.green(`+${status.insertions}`));
924
- diffParts.push(chalk2.red(`-${status.deletions}`));
956
+ diffParts.push(chalk3.green(`+${status.insertions}`));
957
+ diffParts.push(chalk3.red(`-${status.deletions}`));
925
958
  parts.push(diffParts.join(" "));
926
959
  }
927
960
  if (status.hasDirtyFiles) {
928
- parts.push(chalk2.gray("(\u672A\u63D0\u4EA4\u4FEE\u6539)"));
961
+ parts.push(chalk3.gray("(\u672A\u63D0\u4EA4\u4FEE\u6539)"));
929
962
  }
930
963
  return parts.join(" ");
931
964
  }
@@ -967,6 +1000,32 @@ function formatRelativeTime(isoDateString) {
967
1000
  const years = Math.floor(diffDays / 365);
968
1001
  return `${years} \u5E74\u524D`;
969
1002
  }
1003
+ function formatDiskSize(bytes) {
1004
+ const KB = 1024;
1005
+ const MB = KB * 1024;
1006
+ const GB = MB * 1024;
1007
+ if (bytes >= GB) {
1008
+ return `${(bytes / GB).toFixed(1)} GB`;
1009
+ }
1010
+ if (bytes >= MB) {
1011
+ return `${(bytes / MB).toFixed(1)} MB`;
1012
+ }
1013
+ if (bytes >= KB) {
1014
+ return `${(bytes / KB).toFixed(1)} KB`;
1015
+ }
1016
+ return `${bytes} B`;
1017
+ }
1018
+ function formatLocalISOString(date) {
1019
+ const tzOffsetMs = date.getTimezoneOffset() * 60 * 1e3;
1020
+ const localDate = new Date(date.getTime() - tzOffsetMs);
1021
+ const iso = localDate.toISOString().slice(0, -1);
1022
+ const totalMinutes = -date.getTimezoneOffset();
1023
+ const sign = totalMinutes >= 0 ? "+" : "-";
1024
+ const absMinutes = Math.abs(totalMinutes);
1025
+ const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
1026
+ const minutes = String(absMinutes % 60).padStart(2, "0");
1027
+ return `${iso}${sign}${hours}:${minutes}`;
1028
+ }
970
1029
 
971
1030
  // src/utils/branch.ts
972
1031
  function sanitizeBranchName(branchName) {
@@ -1017,11 +1076,12 @@ function validateClaudeCodeInstalled() {
1017
1076
  }
1018
1077
 
1019
1078
  // src/utils/worktree.ts
1020
- import { join as join2 } from "path";
1079
+ import { join as join3 } from "path";
1021
1080
  import { existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
1022
1081
 
1023
1082
  // src/utils/fs.ts
1024
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmdirSync } from "fs";
1083
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmdirSync, statSync } from "fs";
1084
+ import { join as join2 } from "path";
1025
1085
  function ensureDir(dirPath) {
1026
1086
  if (!existsSync2(dirPath)) {
1027
1087
  mkdirSync2(dirPath, { recursive: true });
@@ -1038,11 +1098,30 @@ function removeEmptyDir(dirPath) {
1038
1098
  }
1039
1099
  return false;
1040
1100
  }
1101
+ function calculateDirSize(dirPath) {
1102
+ let totalSize = 0;
1103
+ try {
1104
+ const entries = readdirSync(dirPath, { withFileTypes: true });
1105
+ for (const entry of entries) {
1106
+ const fullPath = join2(dirPath, entry.name);
1107
+ try {
1108
+ if (entry.isDirectory()) {
1109
+ totalSize += calculateDirSize(fullPath);
1110
+ } else if (entry.isFile()) {
1111
+ totalSize += statSync(fullPath).size;
1112
+ }
1113
+ } catch {
1114
+ }
1115
+ }
1116
+ } catch {
1117
+ }
1118
+ return totalSize;
1119
+ }
1041
1120
 
1042
1121
  // src/utils/worktree.ts
1043
1122
  function getProjectWorktreeDir() {
1044
1123
  const projectName = getProjectName();
1045
- return join2(WORKTREES_DIR, projectName);
1124
+ return join3(WORKTREES_DIR, projectName);
1046
1125
  }
1047
1126
  function createWorktrees(branchName, count) {
1048
1127
  const sanitized = sanitizeBranchName(branchName);
@@ -1052,7 +1131,7 @@ function createWorktrees(branchName, count) {
1052
1131
  ensureDir(projectDir);
1053
1132
  const results = [];
1054
1133
  for (const name of branchNames) {
1055
- const worktreePath = join2(projectDir, name);
1134
+ const worktreePath = join3(projectDir, name);
1056
1135
  createWorktree(name, worktreePath);
1057
1136
  results.push({ path: worktreePath, branch: name });
1058
1137
  logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
@@ -1065,7 +1144,7 @@ function createWorktreesByBranches(branchNames) {
1065
1144
  ensureDir(projectDir);
1066
1145
  const results = [];
1067
1146
  for (const name of branchNames) {
1068
- const worktreePath = join2(projectDir, name);
1147
+ const worktreePath = join3(projectDir, name);
1069
1148
  createWorktree(name, worktreePath);
1070
1149
  results.push({ path: worktreePath, branch: name });
1071
1150
  logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
@@ -1087,7 +1166,7 @@ function getProjectWorktrees() {
1087
1166
  if (!entry.isDirectory()) {
1088
1167
  continue;
1089
1168
  }
1090
- const fullPath = join2(projectDir, entry.name);
1169
+ const fullPath = join3(projectDir, entry.name);
1091
1170
  if (registeredPaths.has(fullPath)) {
1092
1171
  worktrees.push({
1093
1172
  path: fullPath,
@@ -1173,7 +1252,7 @@ import Enquirer from "enquirer";
1173
1252
  // src/utils/claude.ts
1174
1253
  import { spawnSync as spawnSync2 } from "child_process";
1175
1254
  import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
1176
- import { join as join3 } from "path";
1255
+ import { join as join4 } from "path";
1177
1256
 
1178
1257
  // src/utils/terminal.ts
1179
1258
  import { execFileSync as execFileSync2 } from "child_process";
@@ -1252,7 +1331,7 @@ function encodeClaudeProjectPath(absolutePath) {
1252
1331
  }
1253
1332
  function hasClaudeSessionHistory(worktreePath) {
1254
1333
  const encodedName = encodeClaudeProjectPath(worktreePath);
1255
- const projectDir = join3(CLAUDE_PROJECTS_DIR, encodedName);
1334
+ const projectDir = join4(CLAUDE_PROJECTS_DIR, encodedName);
1256
1335
  if (!existsSync6(projectDir)) {
1257
1336
  return false;
1258
1337
  }
@@ -1309,13 +1388,13 @@ function launchInteractiveClaudeInNewTerminal(worktree, hasPreviousSession) {
1309
1388
  }
1310
1389
 
1311
1390
  // src/utils/validate-snapshot.ts
1312
- import { join as join4 } from "path";
1313
- import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2, statSync } from "fs";
1391
+ import { join as join5 } from "path";
1392
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2, statSync as statSync2 } from "fs";
1314
1393
  function getSnapshotPath(projectName, branchName) {
1315
- return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
1394
+ return join5(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
1316
1395
  }
1317
1396
  function getSnapshotHeadPath(projectName, branchName) {
1318
- return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
1397
+ return join5(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
1319
1398
  }
1320
1399
  function hasSnapshot(projectName, branchName) {
1321
1400
  return existsSync7(getSnapshotPath(projectName, branchName));
@@ -1323,7 +1402,7 @@ function hasSnapshot(projectName, branchName) {
1323
1402
  function getSnapshotModifiedTime(projectName, branchName) {
1324
1403
  const snapshotPath = getSnapshotPath(projectName, branchName);
1325
1404
  if (!existsSync7(snapshotPath)) return null;
1326
- const stat = statSync(snapshotPath);
1405
+ const stat = statSync2(snapshotPath);
1327
1406
  return stat.mtime.toISOString();
1328
1407
  }
1329
1408
  function readSnapshot(projectName, branchName) {
@@ -1337,7 +1416,7 @@ function readSnapshot(projectName, branchName) {
1337
1416
  function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
1338
1417
  const snapshotPath = getSnapshotPath(projectName, branchName);
1339
1418
  const headPath = getSnapshotHeadPath(projectName, branchName);
1340
- const snapshotDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1419
+ const snapshotDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
1341
1420
  ensureDir(snapshotDir);
1342
1421
  writeFileSync2(snapshotPath, treeHash, "utf-8");
1343
1422
  writeFileSync2(headPath, headCommitHash, "utf-8");
@@ -1356,7 +1435,7 @@ function removeSnapshot(projectName, branchName) {
1356
1435
  }
1357
1436
  }
1358
1437
  function getProjectSnapshotBranches(projectName) {
1359
- const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1438
+ const projectDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
1360
1439
  if (!existsSync7(projectDir)) {
1361
1440
  return [];
1362
1441
  }
@@ -1364,13 +1443,13 @@ function getProjectSnapshotBranches(projectName) {
1364
1443
  return files.filter((f) => f.endsWith(".tree")).map((f) => f.replace(/\.tree$/, ""));
1365
1444
  }
1366
1445
  function removeProjectSnapshots(projectName) {
1367
- const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1446
+ const projectDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
1368
1447
  if (!existsSync7(projectDir)) {
1369
1448
  return;
1370
1449
  }
1371
1450
  const files = readdirSync4(projectDir);
1372
1451
  for (const file of files) {
1373
- unlinkSync(join4(projectDir, file));
1452
+ unlinkSync(join5(projectDir, file));
1374
1453
  }
1375
1454
  try {
1376
1455
  rmdirSync2(projectDir);
@@ -1381,6 +1460,7 @@ function removeProjectSnapshots(projectName) {
1381
1460
 
1382
1461
  // src/utils/worktree-matcher.ts
1383
1462
  import Enquirer2 from "enquirer";
1463
+ import { statSync as statSync3 } from "fs";
1384
1464
  function findExactMatch(worktrees, branchName) {
1385
1465
  return worktrees.find((wt) => wt.branch === branchName);
1386
1466
  }
@@ -1485,9 +1565,142 @@ async function resolveTargetWorktree(worktrees, messages, branchName) {
1485
1565
  const allBranches = worktrees.map((wt) => wt.branch);
1486
1566
  throw new ClawtError(messages.noMatch(branchName, allBranches));
1487
1567
  }
1568
+ function formatLocalDate(date) {
1569
+ const year = date.getFullYear();
1570
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1571
+ const day = String(date.getDate()).padStart(2, "0");
1572
+ return `${year}-${month}-${day}`;
1573
+ }
1574
+ function getWorktreeCreatedDate(dirPath) {
1575
+ try {
1576
+ const stat = statSync3(dirPath);
1577
+ return formatLocalDate(stat.birthtime);
1578
+ } catch {
1579
+ return null;
1580
+ }
1581
+ }
1582
+ function formatRelativeDate(dateStr) {
1583
+ const today = formatLocalDate(/* @__PURE__ */ new Date());
1584
+ const todayMs = new Date(today).getTime();
1585
+ const targetMs = new Date(dateStr).getTime();
1586
+ const diffDays = Math.round((todayMs - targetMs) / (1e3 * 60 * 60 * 24));
1587
+ if (diffDays === 0) return "\u4ECA\u5929";
1588
+ if (diffDays === 1) return "\u6628\u5929";
1589
+ if (diffDays < 30) return `${diffDays} \u5929\u524D`;
1590
+ if (diffDays < 365) {
1591
+ const months = Math.floor(diffDays / 30);
1592
+ return `${months} \u4E2A\u6708\u524D`;
1593
+ }
1594
+ const years = Math.floor(diffDays / 365);
1595
+ return `${years} \u5E74\u524D`;
1596
+ }
1597
+ function groupWorktreesByDate(worktrees) {
1598
+ const groups = /* @__PURE__ */ new Map();
1599
+ for (const wt of worktrees) {
1600
+ const dateKey = getWorktreeCreatedDate(wt.path) ?? UNKNOWN_DATE_GROUP;
1601
+ if (!groups.has(dateKey)) {
1602
+ groups.set(dateKey, []);
1603
+ }
1604
+ groups.get(dateKey).push(wt);
1605
+ }
1606
+ const sortedEntries = [...groups.entries()].sort((a, b) => {
1607
+ if (a[0] === UNKNOWN_DATE_GROUP) return 1;
1608
+ if (b[0] === UNKNOWN_DATE_GROUP) return -1;
1609
+ return b[0].localeCompare(a[0]);
1610
+ });
1611
+ return new Map(sortedEntries);
1612
+ }
1613
+ function buildGroupedChoices(groups) {
1614
+ const choices = [];
1615
+ choices.push({ name: SELECT_ALL_NAME, message: SELECT_ALL_LABEL });
1616
+ for (const [dateKey, worktreeList] of groups) {
1617
+ if (dateKey === UNKNOWN_DATE_GROUP) {
1618
+ choices.push({ role: "separator", message: UNKNOWN_DATE_SEPARATOR_LABEL });
1619
+ } else {
1620
+ const relativeTime = formatRelativeDate(dateKey);
1621
+ choices.push({ role: "separator", message: GROUP_SEPARATOR_LABEL(dateKey, relativeTime) });
1622
+ }
1623
+ const groupSelectAllName = `${GROUP_SELECT_ALL_PREFIX}${dateKey}`;
1624
+ choices.push({ name: groupSelectAllName, message: GROUP_SELECT_ALL_LABEL(dateKey) });
1625
+ for (const wt of worktreeList) {
1626
+ choices.push({ name: wt.branch, message: wt.branch });
1627
+ }
1628
+ }
1629
+ return choices;
1630
+ }
1631
+ function buildGroupMembershipMap(groups) {
1632
+ const map = /* @__PURE__ */ new Map();
1633
+ for (const [dateKey, worktreeList] of groups) {
1634
+ const groupSelectAllName = `${GROUP_SELECT_ALL_PREFIX}${dateKey}`;
1635
+ map.set(groupSelectAllName, worktreeList.map((wt) => wt.branch));
1636
+ }
1637
+ return map;
1638
+ }
1639
+ async function promptGroupedMultiSelectBranches(worktrees, message) {
1640
+ const groups = groupWorktreesByDate(worktrees);
1641
+ const choices = buildGroupedChoices(groups);
1642
+ const groupMembershipMap = buildGroupMembershipMap(groups);
1643
+ const groupSelectAllNames = new Set(groupMembershipMap.keys());
1644
+ const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
1645
+ const MultiSelect = Enquirer2.MultiSelect;
1646
+ class MultiSelectWithGroupSelectAll extends MultiSelect {
1647
+ space() {
1648
+ if (!this.focused) return;
1649
+ const focusedName = this.focused.name;
1650
+ if (focusedName === SELECT_ALL_NAME) {
1651
+ const willEnable = !this.focused.enabled;
1652
+ for (const ch of this.choices) {
1653
+ ch.enabled = willEnable;
1654
+ }
1655
+ return this.render();
1656
+ }
1657
+ if (groupSelectAllNames.has(focusedName)) {
1658
+ const willEnable = !this.focused.enabled;
1659
+ const memberNames = groupMembershipMap.get(focusedName);
1660
+ this.focused.enabled = willEnable;
1661
+ for (const ch of this.choices) {
1662
+ if (memberNames.includes(ch.name)) {
1663
+ ch.enabled = willEnable;
1664
+ }
1665
+ }
1666
+ syncGlobalSelectAll(this.choices);
1667
+ return this.render();
1668
+ }
1669
+ this.toggle(this.focused);
1670
+ syncGroupSelectAll(this.choices, focusedName);
1671
+ syncGlobalSelectAll(this.choices);
1672
+ return this.render();
1673
+ }
1674
+ }
1675
+ function syncGlobalSelectAll(choiceList) {
1676
+ const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
1677
+ if (!selectAllChoice) return;
1678
+ const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
1679
+ selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
1680
+ }
1681
+ function syncGroupSelectAll(choiceList, branchName) {
1682
+ for (const [groupName, memberNames] of groupMembershipMap) {
1683
+ if (!memberNames.includes(branchName)) continue;
1684
+ const groupChoice = choiceList.find((ch) => ch.name === groupName);
1685
+ if (!groupChoice) continue;
1686
+ const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
1687
+ groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
1688
+ break;
1689
+ }
1690
+ }
1691
+ const selectedBranches = await new MultiSelectWithGroupSelectAll({
1692
+ message,
1693
+ choices,
1694
+ // 使用空心圆/实心圆作为选中指示符
1695
+ symbols: {
1696
+ indicator: { on: "\u25CF", off: "\u25CB" }
1697
+ }
1698
+ }).run();
1699
+ return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
1700
+ }
1488
1701
 
1489
1702
  // src/utils/progress-render.ts
1490
- import chalk3 from "chalk";
1703
+ import chalk4 from "chalk";
1491
1704
  import stringWidth from "string-width";
1492
1705
  var ANSI_RESET = "\x1B[0m";
1493
1706
  function truncateToTerminalWidth(text, maxWidth) {
@@ -1524,23 +1737,23 @@ function renderTaskLine(task, total, maxPathWidth, spinnerChar) {
1524
1737
  const pathStr = task.path.padEnd(maxPathWidth);
1525
1738
  switch (task.status) {
1526
1739
  case "pending": {
1527
- return `${indexStr} ${pathStr} ${chalk3.gray(TASK_STATUS_ICONS.PENDING)} ${chalk3.gray(TASK_STATUS_LABELS.PENDING)}`;
1740
+ return `${indexStr} ${pathStr} ${chalk4.gray(TASK_STATUS_ICONS.PENDING)} ${chalk4.gray(TASK_STATUS_LABELS.PENDING)}`;
1528
1741
  }
1529
1742
  case "running": {
1530
1743
  const elapsed = formatDuration(Date.now() - task.startedAt);
1531
- const detail = task.activity ? ` ${chalk3.dim(task.activity)}` : "";
1532
- return `${indexStr} ${pathStr} ${chalk3.cyan(spinnerChar)} ${chalk3.cyan(TASK_STATUS_LABELS.RUNNING)} ${chalk3.gray(elapsed)}${detail}`;
1744
+ const detail = task.activity ? ` ${chalk4.dim(task.activity)}` : "";
1745
+ return `${indexStr} ${pathStr} ${chalk4.cyan(spinnerChar)} ${chalk4.cyan(TASK_STATUS_LABELS.RUNNING)} ${chalk4.gray(elapsed)}${detail}`;
1533
1746
  }
1534
1747
  case "done": {
1535
1748
  const duration = task.durationMs != null ? formatDuration(task.durationMs) : "N/A";
1536
1749
  const cost = task.costUsd != null ? `$${task.costUsd.toFixed(2)}` : "";
1537
- const preview = task.resultPreview ? ` ${chalk3.dim(task.resultPreview)}` : "";
1538
- return `${indexStr} ${pathStr} ${chalk3.green(TASK_STATUS_ICONS.DONE)} ${chalk3.green(TASK_STATUS_LABELS.DONE)} ${chalk3.gray(duration)} ${chalk3.yellow(cost)}${preview}`;
1750
+ const preview = task.resultPreview ? ` ${chalk4.dim(task.resultPreview)}` : "";
1751
+ return `${indexStr} ${pathStr} ${chalk4.green(TASK_STATUS_ICONS.DONE)} ${chalk4.green(TASK_STATUS_LABELS.DONE)} ${chalk4.gray(duration)} ${chalk4.yellow(cost)}${preview}`;
1539
1752
  }
1540
1753
  case "failed": {
1541
1754
  const duration = task.durationMs != null ? formatDuration(task.durationMs) : "N/A";
1542
- const preview = task.resultPreview ? ` ${chalk3.dim(task.resultPreview)}` : "";
1543
- return `${indexStr} ${pathStr} ${chalk3.red(TASK_STATUS_ICONS.FAILED)} ${chalk3.red(TASK_STATUS_LABELS.FAILED)} ${chalk3.gray(duration)}${preview}`;
1755
+ const preview = task.resultPreview ? ` ${chalk4.dim(task.resultPreview)}` : "";
1756
+ return `${indexStr} ${pathStr} ${chalk4.red(TASK_STATUS_ICONS.FAILED)} ${chalk4.red(TASK_STATUS_LABELS.FAILED)} ${chalk4.gray(duration)}${preview}`;
1544
1757
  }
1545
1758
  }
1546
1759
  }
@@ -1550,10 +1763,10 @@ function renderSummaryLine(tasks, total) {
1550
1763
  const failed = tasks.filter((t) => t.status === "failed").length;
1551
1764
  const pending = tasks.filter((t) => t.status === "pending").length;
1552
1765
  const parts = [];
1553
- if (running > 0) parts.push(chalk3.cyan(`${running}/${total} ${TASK_STATUS_LABELS.RUNNING}`));
1554
- if (done > 0) parts.push(chalk3.green(`${done}/${total} ${TASK_STATUS_LABELS.DONE}`));
1555
- if (failed > 0) parts.push(chalk3.red(`${failed}/${total} ${TASK_STATUS_LABELS.FAILED}`));
1556
- if (pending > 0) parts.push(chalk3.gray(`${pending}/${total} ${TASK_STATUS_LABELS.PENDING}`));
1766
+ if (running > 0) parts.push(chalk4.cyan(`${running}/${total} ${TASK_STATUS_LABELS.RUNNING}`));
1767
+ if (done > 0) parts.push(chalk4.green(`${done}/${total} ${TASK_STATUS_LABELS.DONE}`));
1768
+ if (failed > 0) parts.push(chalk4.red(`${failed}/${total} ${TASK_STATUS_LABELS.FAILED}`));
1769
+ if (pending > 0) parts.push(chalk4.gray(`${pending}/${total} ${TASK_STATUS_LABELS.PENDING}`));
1557
1770
  return `[${parts.join(", ")}]`;
1558
1771
  }
1559
1772
 
@@ -2168,8 +2381,8 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
2168
2381
  }
2169
2382
 
2170
2383
  // src/utils/dry-run.ts
2171
- import chalk4 from "chalk";
2172
- import { join as join5 } from "path";
2384
+ import chalk5 from "chalk";
2385
+ import { join as join6 } from "path";
2173
2386
  var DRY_RUN_TASK_DESC_MAX_LENGTH = 80;
2174
2387
  function truncateTaskDesc(task) {
2175
2388
  const oneLine = task.replace(/\n/g, " ").trim();
@@ -2182,7 +2395,7 @@ function printDryRunPreview(branchNames, tasks, concurrency) {
2182
2395
  const projectDir = getProjectWorktreeDir();
2183
2396
  const isInteractive = tasks.length === 0;
2184
2397
  printDoubleSeparator();
2185
- printInfo(` ${chalk4.bold(MESSAGES.DRY_RUN_TITLE)}`);
2398
+ printInfo(` ${chalk5.bold(MESSAGES.DRY_RUN_TITLE)}`);
2186
2399
  printDoubleSeparator();
2187
2400
  const summaryParts = [
2188
2401
  MESSAGES.DRY_RUN_TASK_COUNT(branchNames.length),
@@ -2192,31 +2405,31 @@ function printDryRunPreview(branchNames, tasks, concurrency) {
2192
2405
  if (isInteractive) {
2193
2406
  summaryParts.push(MESSAGES.DRY_RUN_INTERACTIVE_MODE);
2194
2407
  }
2195
- printInfo(summaryParts.join(chalk4.gray(" \u2502 ")));
2408
+ printInfo(summaryParts.join(chalk5.gray(" \u2502 ")));
2196
2409
  printSeparator();
2197
2410
  let hasConflict = false;
2198
2411
  for (let i = 0; i < branchNames.length; i++) {
2199
2412
  const branch = branchNames[i];
2200
- const worktreePath = join5(projectDir, branch);
2413
+ const worktreePath = join6(projectDir, branch);
2201
2414
  const exists = checkBranchExists(branch);
2202
2415
  if (exists) hasConflict = true;
2203
2416
  const indexLabel = `[${i + 1}/${branchNames.length}]`;
2204
2417
  if (exists) {
2205
- printInfo(`${chalk4.yellow("\u26A0")} ${indexLabel} ${chalk4.yellow(branch)} ${chalk4.gray("\u2014")} ${chalk4.yellow(MESSAGES.DRY_RUN_BRANCH_EXISTS_WARNING(branch))}`);
2418
+ printInfo(`${chalk5.yellow("\u26A0")} ${indexLabel} ${chalk5.yellow(branch)} ${chalk5.gray("\u2014")} ${chalk5.yellow(MESSAGES.DRY_RUN_BRANCH_EXISTS_WARNING(branch))}`);
2206
2419
  } else {
2207
- printInfo(`${chalk4.green("\u2713")} ${indexLabel} ${chalk4.cyan(branch)}`);
2420
+ printInfo(`${chalk5.green("\u2713")} ${indexLabel} ${chalk5.cyan(branch)}`);
2208
2421
  }
2209
- printInfo(` ${chalk4.gray("\u8DEF\u5F84:")} ${worktreePath}`);
2422
+ printInfo(` ${chalk5.gray("\u8DEF\u5F84:")} ${worktreePath}`);
2210
2423
  if (!isInteractive) {
2211
- printInfo(` ${chalk4.gray("\u4EFB\u52A1:")} ${truncateTaskDesc(tasks[i])}`);
2424
+ printInfo(` ${chalk5.gray("\u4EFB\u52A1:")} ${truncateTaskDesc(tasks[i])}`);
2212
2425
  }
2213
2426
  printInfo("");
2214
2427
  }
2215
2428
  printDoubleSeparator();
2216
2429
  if (hasConflict) {
2217
- printInfo(chalk4.yellow(`\u26A0 ${MESSAGES.DRY_RUN_HAS_CONFLICT}`));
2430
+ printInfo(chalk5.yellow(`\u26A0 ${MESSAGES.DRY_RUN_HAS_CONFLICT}`));
2218
2431
  } else {
2219
- printInfo(chalk4.green(`\u2713 ${MESSAGES.DRY_RUN_READY}`));
2432
+ printInfo(chalk5.green(`\u2713 ${MESSAGES.DRY_RUN_READY}`));
2220
2433
  }
2221
2434
  }
2222
2435
 
@@ -2234,7 +2447,7 @@ function applyAliases(program2, aliases) {
2234
2447
  }
2235
2448
 
2236
2449
  // src/utils/config-strategy.ts
2237
- import chalk5 from "chalk";
2450
+ import chalk6 from "chalk";
2238
2451
  import Enquirer3 from "enquirer";
2239
2452
  function isValidConfigKey(key) {
2240
2453
  return key in DEFAULT_CONFIG;
@@ -2278,9 +2491,9 @@ async function promptConfigValue(key, currentValue) {
2278
2491
  }
2279
2492
  function formatConfigValue(value) {
2280
2493
  if (typeof value === "boolean") {
2281
- return value ? chalk5.green("true") : chalk5.yellow("false");
2494
+ return value ? chalk6.green("true") : chalk6.yellow("false");
2282
2495
  }
2283
- return chalk5.cyan(String(value));
2496
+ return chalk6.cyan(String(value));
2284
2497
  }
2285
2498
  async function promptBooleanValue(key, currentValue) {
2286
2499
  const choices = [
@@ -2327,7 +2540,7 @@ async function promptStringValue(key, currentValue) {
2327
2540
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2328
2541
  import { execSync as execSync3 } from "child_process";
2329
2542
  import { request } from "https";
2330
- import chalk6 from "chalk";
2543
+ import chalk7 from "chalk";
2331
2544
  import stringWidth2 from "string-width";
2332
2545
  function readUpdateCache() {
2333
2546
  try {
@@ -2402,12 +2615,12 @@ function detectPackageManager() {
2402
2615
  }
2403
2616
  function printUpdateNotification(currentVersion, latestVersion) {
2404
2617
  const updateText = UPDATE_MESSAGES.UPDATE_AVAILABLE(
2405
- chalk6.red(currentVersion),
2406
- chalk6.green(latestVersion)
2618
+ chalk7.red(currentVersion),
2619
+ chalk7.green(latestVersion)
2407
2620
  );
2408
2621
  const pm = detectPackageManager();
2409
2622
  const updateCommand = UPDATE_COMMANDS[pm] || UPDATE_COMMANDS.npm;
2410
- const commandText = UPDATE_MESSAGES.UPDATE_HINT(chalk6.cyan(updateCommand));
2623
+ const commandText = UPDATE_MESSAGES.UPDATE_HINT(chalk7.cyan(updateCommand));
2411
2624
  const updateTextWidth = stringWidth2(updateText);
2412
2625
  const commandTextWidth = stringWidth2(commandText);
2413
2626
  const innerWidth = Math.max(updateTextWidth, commandTextWidth) + 4;
@@ -2455,7 +2668,7 @@ async function checkForUpdates(currentVersion) {
2455
2668
  }
2456
2669
 
2457
2670
  // src/commands/list.ts
2458
- import chalk7 from "chalk";
2671
+ import chalk8 from "chalk";
2459
2672
  function registerListCommand(program2) {
2460
2673
  program2.command("list").description("\u5217\u51FA\u5F53\u524D\u9879\u76EE\u6240\u6709 worktree").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((options) => {
2461
2674
  handleList(options);
@@ -2492,12 +2705,12 @@ function printListAsText(projectName, worktrees) {
2492
2705
  for (const wt of worktrees) {
2493
2706
  const status = getWorktreeStatus(wt);
2494
2707
  const isIdle = status ? isWorktreeIdle(status) : false;
2495
- const pathDisplay = isIdle ? chalk7.hex("#FF8C00")(wt.path) : wt.path;
2708
+ const pathDisplay = isIdle ? chalk8.hex("#FF8C00")(wt.path) : wt.path;
2496
2709
  printInfo(` ${pathDisplay} [${wt.branch}]`);
2497
2710
  if (status) {
2498
2711
  printInfo(` ${formatWorktreeStatus(status)}`);
2499
2712
  } else {
2500
- printInfo(` ${chalk7.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
2713
+ printInfo(` ${chalk8.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
2501
2714
  }
2502
2715
  printInfo("");
2503
2716
  }
@@ -2705,7 +2918,12 @@ async function handleResume(options) {
2705
2918
  validateClaudeCodeInstalled();
2706
2919
  logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F\u8FC7\u6EE4: ${options.branch ?? "(\u65E0)"}`);
2707
2920
  const worktrees = getProjectWorktrees();
2708
- const targetWorktrees = await resolveTargetWorktrees(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
2921
+ let targetWorktrees;
2922
+ if (!options.branch && worktrees.length > 1) {
2923
+ targetWorktrees = await promptGroupedMultiSelectBranches(worktrees, RESUME_RESOLVE_MESSAGES.selectBranch);
2924
+ } else {
2925
+ targetWorktrees = await resolveTargetWorktrees(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
2926
+ }
2709
2927
  if (targetWorktrees.length === 0) {
2710
2928
  return;
2711
2929
  }
@@ -3180,7 +3398,7 @@ async function handleMerge(options) {
3180
3398
  }
3181
3399
 
3182
3400
  // src/commands/config.ts
3183
- import chalk8 from "chalk";
3401
+ import chalk9 from "chalk";
3184
3402
  import Enquirer5 from "enquirer";
3185
3403
  function registerConfigCommand(program2) {
3186
3404
  const configCmd = program2.command("config").description("\u4EA4\u4E92\u5F0F\u67E5\u770B\u548C\u4FEE\u6539\u5168\u5C40\u914D\u7F6E").action(async () => {
@@ -3241,7 +3459,7 @@ async function handleInteractiveConfigSet() {
3241
3459
  const isObject = typeof DEFAULT_CONFIG[k] === "object";
3242
3460
  return {
3243
3461
  name: k,
3244
- message: `${k}: ${isObject ? chalk8.dim(JSON.stringify(config2[k])) : formatConfigValue(config2[k])} ${chalk8.dim(`\u2014 ${CONFIG_DESCRIPTIONS[k]}`)}`,
3462
+ message: `${k}: ${isObject ? chalk9.dim(JSON.stringify(config2[k])) : formatConfigValue(config2[k])} ${chalk9.dim(`\u2014 ${CONFIG_DESCRIPTIONS[k]}`)}`,
3245
3463
  ...isObject && { disabled: CONFIG_ALIAS_DISABLED_HINT }
3246
3464
  };
3247
3465
  });
@@ -3296,7 +3514,7 @@ async function handleReset() {
3296
3514
  }
3297
3515
 
3298
3516
  // src/commands/status.ts
3299
- import chalk9 from "chalk";
3517
+ import chalk10 from "chalk";
3300
3518
  function registerStatusCommand(program2) {
3301
3519
  program2.command("status").description("\u663E\u793A\u9879\u76EE\u5168\u5C40\u72B6\u6001\u603B\u89C8").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((options) => {
3302
3520
  handleStatus(options);
@@ -3409,7 +3627,7 @@ function printStatusAsJson(result) {
3409
3627
  }
3410
3628
  function printStatusAsText(result) {
3411
3629
  printDoubleSeparator();
3412
- printInfo(` ${chalk9.bold.cyan(MESSAGES.STATUS_TITLE(result.main.projectName))}`);
3630
+ printInfo(` ${chalk10.bold.cyan(MESSAGES.STATUS_TITLE(result.main.projectName))}`);
3413
3631
  printDoubleSeparator();
3414
3632
  printInfo("");
3415
3633
  printMainSection(result.main);
@@ -3422,17 +3640,17 @@ function printStatusAsText(result) {
3422
3640
  printDoubleSeparator();
3423
3641
  }
3424
3642
  function printMainSection(main2) {
3425
- printInfo(` ${chalk9.bold("\u25C6")} ${chalk9.bold(MESSAGES.STATUS_MAIN_SECTION)}`);
3426
- printInfo(` \u5206\u652F: ${chalk9.bold(main2.branch)}`);
3643
+ printInfo(` ${chalk10.bold("\u25C6")} ${chalk10.bold(MESSAGES.STATUS_MAIN_SECTION)}`);
3644
+ printInfo(` \u5206\u652F: ${chalk10.bold(main2.branch)}`);
3427
3645
  if (main2.isClean) {
3428
- printInfo(` \u72B6\u6001: ${chalk9.green("\u2713 \u5E72\u51C0")}`);
3646
+ printInfo(` \u72B6\u6001: ${chalk10.green("\u2713 \u5E72\u51C0")}`);
3429
3647
  } else {
3430
- printInfo(` \u72B6\u6001: ${chalk9.yellow("\u2717 \u6709\u672A\u63D0\u4EA4\u4FEE\u6539")}`);
3648
+ printInfo(` \u72B6\u6001: ${chalk10.yellow("\u2717 \u6709\u672A\u63D0\u4EA4\u4FEE\u6539")}`);
3431
3649
  }
3432
3650
  printInfo("");
3433
3651
  }
3434
3652
  function printWorktreesSection(worktrees, total) {
3435
- printInfo(` ${chalk9.bold("\u25C6")} ${chalk9.bold(MESSAGES.STATUS_WORKTREES_SECTION)} (${total} \u4E2A)`);
3653
+ printInfo(` ${chalk10.bold("\u25C6")} ${chalk10.bold(MESSAGES.STATUS_WORKTREES_SECTION)} (${total} \u4E2A)`);
3436
3654
  printInfo("");
3437
3655
  if (worktrees.length === 0) {
3438
3656
  printInfo(` ${MESSAGES.STATUS_NO_WORKTREES}`);
@@ -3444,56 +3662,56 @@ function printWorktreesSection(worktrees, total) {
3444
3662
  }
3445
3663
  function printWorktreeItem(wt) {
3446
3664
  const statusLabel = formatChangeStatusLabel(wt.changeStatus);
3447
- printInfo(` ${chalk9.bold("\u25CF")} ${chalk9.bold(wt.branch)} [${statusLabel}]`);
3665
+ printInfo(` ${chalk10.bold("\u25CF")} ${chalk10.bold(wt.branch)} [${statusLabel}]`);
3448
3666
  if (wt.insertions > 0 || wt.deletions > 0) {
3449
- printInfo(` ${chalk9.green(`+${wt.insertions}`)} ${chalk9.red(`-${wt.deletions}`)}`);
3667
+ printInfo(` ${chalk10.green(`+${wt.insertions}`)} ${chalk10.red(`-${wt.deletions}`)}`);
3450
3668
  }
3451
3669
  if (wt.commitsAhead > 0) {
3452
- printInfo(` ${chalk9.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`)}`);
3670
+ printInfo(` ${chalk10.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`)}`);
3453
3671
  }
3454
3672
  if (wt.commitsBehind > 0) {
3455
- printInfo(` ${chalk9.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`)}`);
3673
+ printInfo(` ${chalk10.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`)}`);
3456
3674
  } else {
3457
- printInfo(` ${chalk9.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65")}`);
3675
+ printInfo(` ${chalk10.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65")}`);
3458
3676
  }
3459
3677
  if (wt.createdAt) {
3460
3678
  const relativeTime = formatRelativeTime(wt.createdAt);
3461
3679
  if (relativeTime) {
3462
- printInfo(` ${chalk9.gray(MESSAGES.STATUS_CREATED_AT(relativeTime))}`);
3680
+ printInfo(` ${chalk10.gray(MESSAGES.STATUS_CREATED_AT(relativeTime))}`);
3463
3681
  }
3464
3682
  }
3465
3683
  if (wt.snapshotTime) {
3466
3684
  const relativeTime = formatRelativeTime(wt.snapshotTime);
3467
3685
  if (relativeTime) {
3468
- printInfo(` ${chalk9.green(MESSAGES.STATUS_LAST_VALIDATED(relativeTime))}`);
3686
+ printInfo(` ${chalk10.green(MESSAGES.STATUS_LAST_VALIDATED(relativeTime))}`);
3469
3687
  }
3470
3688
  } else {
3471
- printInfo(` ${chalk9.red(MESSAGES.STATUS_NOT_VALIDATED)}`);
3689
+ printInfo(` ${chalk10.red(MESSAGES.STATUS_NOT_VALIDATED)}`);
3472
3690
  }
3473
3691
  printInfo("");
3474
3692
  }
3475
3693
  function formatChangeStatusLabel(status) {
3476
3694
  switch (status) {
3477
3695
  case "committed":
3478
- return chalk9.green(MESSAGES.STATUS_CHANGE_COMMITTED);
3696
+ return chalk10.green(MESSAGES.STATUS_CHANGE_COMMITTED);
3479
3697
  case "uncommitted":
3480
- return chalk9.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
3698
+ return chalk10.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
3481
3699
  case "conflict":
3482
- return chalk9.red(MESSAGES.STATUS_CHANGE_CONFLICT);
3700
+ return chalk10.red(MESSAGES.STATUS_CHANGE_CONFLICT);
3483
3701
  case "clean":
3484
- return chalk9.gray(MESSAGES.STATUS_CHANGE_CLEAN);
3702
+ return chalk10.gray(MESSAGES.STATUS_CHANGE_CLEAN);
3485
3703
  }
3486
3704
  }
3487
3705
  function printSnapshotsSection(snapshots) {
3488
- printInfo(` ${chalk9.bold("\u25C6")} ${chalk9.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.total} \u4E2A)`);
3706
+ printInfo(` ${chalk10.bold("\u25C6")} ${chalk10.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.total} \u4E2A)`);
3489
3707
  if (snapshots.orphaned > 0) {
3490
- printInfo(` ${chalk9.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED(snapshots.orphaned))}`);
3708
+ printInfo(` ${chalk10.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED(snapshots.orphaned))}`);
3491
3709
  }
3492
3710
  printInfo("");
3493
3711
  }
3494
3712
 
3495
3713
  // src/commands/alias.ts
3496
- import chalk10 from "chalk";
3714
+ import chalk11 from "chalk";
3497
3715
  function getRegisteredCommandNames(program2) {
3498
3716
  return program2.commands.map((cmd) => cmd.name());
3499
3717
  }
@@ -3514,7 +3732,7 @@ ${MESSAGES.ALIAS_LIST_TITLE}
3514
3732
  `);
3515
3733
  printSeparator();
3516
3734
  for (const [alias, command] of entries) {
3517
- printInfo(` ${chalk10.bold(alias)} \u2192 ${chalk10.cyan(command)}`);
3735
+ printInfo(` ${chalk11.bold(alias)} \u2192 ${chalk11.cyan(command)}`);
3518
3736
  }
3519
3737
  printInfo("");
3520
3738
  printSeparator();
@@ -3560,8 +3778,187 @@ function registerAliasCommand(program2) {
3560
3778
  });
3561
3779
  }
3562
3780
 
3781
+ // src/commands/projects.ts
3782
+ import { existsSync as existsSync9, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
3783
+ import { join as join7 } from "path";
3784
+ import chalk12 from "chalk";
3785
+ function registerProjectsCommand(program2) {
3786
+ program2.command("projects [name]").description("\u5C55\u793A\u6240\u6709\u9879\u76EE\u7684 worktree \u6982\u89C8\uFF0C\u6216\u67E5\u770B\u6307\u5B9A\u9879\u76EE\u7684 worktree \u8BE6\u60C5").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((name, options) => {
3787
+ handleProjects({ name, json: options.json });
3788
+ });
3789
+ }
3790
+ function handleProjects(options) {
3791
+ if (options.name) {
3792
+ handleProjectDetail(options.name, options.json);
3793
+ } else {
3794
+ handleProjectsOverview(options.json);
3795
+ }
3796
+ }
3797
+ function handleProjectsOverview(json) {
3798
+ const result = collectProjectsOverview();
3799
+ logger.info(`projects \u547D\u4EE4\u6267\u884C\uFF0C\u5171 ${result.totalProjects} \u4E2A\u9879\u76EE`);
3800
+ if (json) {
3801
+ console.log(JSON.stringify(result, null, 2));
3802
+ return;
3803
+ }
3804
+ printProjectsOverviewAsText(result);
3805
+ }
3806
+ function handleProjectDetail(name, json) {
3807
+ const projectDir = join7(WORKTREES_DIR, name);
3808
+ if (!existsSync9(projectDir)) {
3809
+ printError(MESSAGES.PROJECTS_NOT_FOUND(name));
3810
+ process.exit(1);
3811
+ }
3812
+ const result = collectProjectDetail(name, projectDir);
3813
+ logger.info(`projects \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${name}\uFF0C\u5171 ${result.worktrees.length} \u4E2A worktree`);
3814
+ if (json) {
3815
+ console.log(JSON.stringify(result, null, 2));
3816
+ return;
3817
+ }
3818
+ printProjectDetailAsText(result);
3819
+ }
3820
+ function collectProjectsOverview() {
3821
+ if (!existsSync9(WORKTREES_DIR)) {
3822
+ return { projects: [], totalProjects: 0, totalDiskUsage: 0 };
3823
+ }
3824
+ const entries = readdirSync5(WORKTREES_DIR, { withFileTypes: true });
3825
+ const projects = [];
3826
+ for (const entry of entries) {
3827
+ if (!entry.isDirectory()) {
3828
+ continue;
3829
+ }
3830
+ const projectDir = join7(WORKTREES_DIR, entry.name);
3831
+ const overview = collectSingleProjectOverview(entry.name, projectDir);
3832
+ projects.push(overview);
3833
+ }
3834
+ sortByLastActiveTimeDesc(projects);
3835
+ const totalDiskUsage = projects.reduce((sum, p) => sum + p.diskUsage, 0);
3836
+ return {
3837
+ projects,
3838
+ totalProjects: projects.length,
3839
+ totalDiskUsage
3840
+ };
3841
+ }
3842
+ function collectSingleProjectOverview(name, projectDir) {
3843
+ const subEntries = readdirSync5(projectDir, { withFileTypes: true });
3844
+ const worktreeDirs = subEntries.filter((e) => e.isDirectory());
3845
+ const worktreeCount = worktreeDirs.length;
3846
+ const diskUsage = calculateDirSize(projectDir);
3847
+ const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join7(projectDir, e.name)));
3848
+ return {
3849
+ name,
3850
+ worktreeCount,
3851
+ lastActiveTime,
3852
+ diskUsage
3853
+ };
3854
+ }
3855
+ function collectProjectDetail(name, projectDir) {
3856
+ const subEntries = readdirSync5(projectDir, { withFileTypes: true });
3857
+ const worktrees = [];
3858
+ for (const entry of subEntries) {
3859
+ if (!entry.isDirectory()) {
3860
+ continue;
3861
+ }
3862
+ const wtPath = join7(projectDir, entry.name);
3863
+ const detail = collectSingleWorktreeDetail(entry.name, wtPath);
3864
+ worktrees.push(detail);
3865
+ }
3866
+ worktrees.sort((a, b) => new Date(b.lastModifiedTime).getTime() - new Date(a.lastModifiedTime).getTime());
3867
+ const totalDiskUsage = worktrees.reduce((sum, wt) => sum + wt.diskUsage, 0);
3868
+ return {
3869
+ name,
3870
+ projectDir,
3871
+ worktrees,
3872
+ totalDiskUsage
3873
+ };
3874
+ }
3875
+ function collectSingleWorktreeDetail(branch, wtPath) {
3876
+ const stat = statSync4(wtPath);
3877
+ const diskUsage = calculateDirSize(wtPath);
3878
+ return {
3879
+ branch,
3880
+ path: wtPath,
3881
+ lastModifiedTime: formatLocalISOString(stat.mtime),
3882
+ diskUsage
3883
+ };
3884
+ }
3885
+ function resolveProjectLastActiveTime(projectDir, worktreePaths) {
3886
+ let latestTime = statSync4(projectDir).mtime;
3887
+ for (const wtPath of worktreePaths) {
3888
+ try {
3889
+ const wtStat = statSync4(wtPath);
3890
+ if (wtStat.mtime > latestTime) {
3891
+ latestTime = wtStat.mtime;
3892
+ }
3893
+ } catch {
3894
+ }
3895
+ }
3896
+ return formatLocalISOString(latestTime);
3897
+ }
3898
+ function sortByLastActiveTimeDesc(projects) {
3899
+ projects.sort((a, b) => new Date(b.lastActiveTime).getTime() - new Date(a.lastActiveTime).getTime());
3900
+ }
3901
+ function printProjectsOverviewAsText(result) {
3902
+ printDoubleSeparator();
3903
+ printInfo(` ${chalk12.bold.cyan(MESSAGES.PROJECTS_OVERVIEW_TITLE)}`);
3904
+ printDoubleSeparator();
3905
+ printInfo("");
3906
+ if (result.projects.length === 0) {
3907
+ printInfo(` ${MESSAGES.PROJECTS_NO_PROJECTS}`);
3908
+ printInfo("");
3909
+ printDoubleSeparator();
3910
+ return;
3911
+ }
3912
+ for (const project of result.projects) {
3913
+ printProjectOverviewItem(project);
3914
+ }
3915
+ printSeparator();
3916
+ printInfo("");
3917
+ printInfo(` \u5171 ${chalk12.bold(String(result.totalProjects))} \u4E2A\u9879\u76EE ${chalk12.gray(MESSAGES.PROJECTS_TOTAL_DISK_USAGE(formatDiskSize(result.totalDiskUsage)))}`);
3918
+ printInfo("");
3919
+ printDoubleSeparator();
3920
+ }
3921
+ function printProjectOverviewItem(project) {
3922
+ const relativeTime = formatRelativeTime(project.lastActiveTime);
3923
+ const activeLabel = relativeTime ? MESSAGES.PROJECTS_LAST_ACTIVE(relativeTime) : "";
3924
+ const diskLabel = MESSAGES.PROJECTS_DISK_USAGE(formatDiskSize(project.diskUsage));
3925
+ printInfo(` ${chalk12.bold("\u25CF")} ${chalk12.bold(project.name)}`);
3926
+ printInfo(` ${MESSAGES.PROJECTS_WORKTREE_COUNT(project.worktreeCount)} ${chalk12.gray(activeLabel)} ${chalk12.gray(diskLabel)}`);
3927
+ printInfo("");
3928
+ }
3929
+ function printProjectDetailAsText(result) {
3930
+ printDoubleSeparator();
3931
+ printInfo(` ${chalk12.bold.cyan(MESSAGES.PROJECTS_DETAIL_TITLE(result.name))}`);
3932
+ printDoubleSeparator();
3933
+ printInfo("");
3934
+ printInfo(` ${chalk12.bold("\u25C6")} ${chalk12.bold(MESSAGES.PROJECTS_PATH(result.projectDir))}`);
3935
+ printInfo(` ${MESSAGES.PROJECTS_TOTAL_DISK_USAGE(formatDiskSize(result.totalDiskUsage))}`);
3936
+ printInfo("");
3937
+ printSeparator();
3938
+ printInfo("");
3939
+ if (result.worktrees.length === 0) {
3940
+ printInfo(` ${MESSAGES.PROJECTS_DETAIL_NO_WORKTREES}`);
3941
+ printInfo("");
3942
+ printDoubleSeparator();
3943
+ return;
3944
+ }
3945
+ for (const wt of result.worktrees) {
3946
+ printWorktreeDetailItem(wt);
3947
+ }
3948
+ printDoubleSeparator();
3949
+ }
3950
+ function printWorktreeDetailItem(wt) {
3951
+ const relativeTime = formatRelativeTime(wt.lastModifiedTime);
3952
+ const modifiedLabel = relativeTime ? MESSAGES.PROJECTS_LAST_MODIFIED(relativeTime) : "";
3953
+ const diskLabel = MESSAGES.PROJECTS_DISK_USAGE(formatDiskSize(wt.diskUsage));
3954
+ printInfo(` ${chalk12.bold("\u25CF")} ${chalk12.bold(wt.branch)}`);
3955
+ printInfo(` ${wt.path}`);
3956
+ printInfo(` ${chalk12.gray(modifiedLabel)} ${chalk12.gray(diskLabel)}`);
3957
+ printInfo("");
3958
+ }
3959
+
3563
3960
  // src/commands/completion.ts
3564
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync10 } from "fs";
3961
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync11 } from "fs";
3565
3962
  import { resolve as resolve2 } from "path";
3566
3963
  import { homedir as homedir2 } from "os";
3567
3964
 
@@ -3612,25 +4009,25 @@ compdef _clawt_completion clawt
3612
4009
  }
3613
4010
 
3614
4011
  // src/utils/completion-engine.ts
3615
- import { existsSync as existsSync9, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
3616
- import { join as join6, dirname, basename as basename2 } from "path";
4012
+ import { existsSync as existsSync10, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
4013
+ import { join as join8, dirname, basename as basename2 } from "path";
3617
4014
  function completeFilePath(partial) {
3618
4015
  const cwd = process.cwd();
3619
4016
  const hasDir = partial.includes("/");
3620
- const searchDir = hasDir ? join6(cwd, dirname(partial)) : cwd;
4017
+ const searchDir = hasDir ? join8(cwd, dirname(partial)) : cwd;
3621
4018
  const prefix = hasDir ? basename2(partial) : partial;
3622
- if (!existsSync9(searchDir)) {
4019
+ if (!existsSync10(searchDir)) {
3623
4020
  return [];
3624
4021
  }
3625
- const entries = readdirSync5(searchDir);
4022
+ const entries = readdirSync6(searchDir);
3626
4023
  const results = [];
3627
4024
  const dirPrefix = hasDir ? dirname(partial) + "/" : "";
3628
4025
  for (const entry of entries) {
3629
4026
  if (!entry.startsWith(prefix)) continue;
3630
4027
  if (entry.startsWith(".")) continue;
3631
- const fullPath = join6(searchDir, entry);
4028
+ const fullPath = join8(searchDir, entry);
3632
4029
  try {
3633
- const stat = statSync2(fullPath);
4030
+ const stat = statSync5(fullPath);
3634
4031
  if (stat.isDirectory()) {
3635
4032
  results.push(dirPrefix + entry + "/");
3636
4033
  } else if (stat.isFile()) {
@@ -3717,7 +4114,7 @@ function generateCompletions(program2, args) {
3717
4114
 
3718
4115
  // src/commands/completion.ts
3719
4116
  function appendToFile(filePath, content) {
3720
- if (existsSync10(filePath)) {
4117
+ if (existsSync11(filePath)) {
3721
4118
  const current = readFileSync5(filePath, "utf-8");
3722
4119
  if (current.includes("clawt completion")) {
3723
4120
  printInfo(MESSAGES.COMPLETION_INSTALL_EXISTS + ": " + filePath);
@@ -3795,6 +4192,7 @@ registerSyncCommand(program);
3795
4192
  registerResetCommand(program);
3796
4193
  registerStatusCommand(program);
3797
4194
  registerAliasCommand(program);
4195
+ registerProjectsCommand(program);
3798
4196
  registerCompletionCommand(program);
3799
4197
  var config = loadConfig();
3800
4198
  applyAliases(program, config.aliases);