clawt 2.18.0 → 2.19.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/README.md CHANGED
@@ -214,6 +214,16 @@ clawt status --json # JSON 格式
214
214
  clawt reset
215
215
  ```
216
216
 
217
+ ### `clawt projects` — 跨项目 worktree 概览
218
+
219
+ ```bash
220
+ clawt projects # 查看所有项目概览
221
+ clawt projects my-project # 查看指定项目的 worktree 详情
222
+ clawt projects --json # JSON 格式输出
223
+ ```
224
+
225
+ 展示所有项目的 worktree 数量、磁盘占用和最近活跃时间,或查看指定项目下每个 worktree 的详细信息。
226
+
217
227
  ### `clawt config` — 交互式查看和修改配置
218
228
 
219
229
  ```bash
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
 
@@ -967,6 +994,32 @@ function formatRelativeTime(isoDateString) {
967
994
  const years = Math.floor(diffDays / 365);
968
995
  return `${years} \u5E74\u524D`;
969
996
  }
997
+ function formatDiskSize(bytes) {
998
+ const KB = 1024;
999
+ const MB = KB * 1024;
1000
+ const GB = MB * 1024;
1001
+ if (bytes >= GB) {
1002
+ return `${(bytes / GB).toFixed(1)} GB`;
1003
+ }
1004
+ if (bytes >= MB) {
1005
+ return `${(bytes / MB).toFixed(1)} MB`;
1006
+ }
1007
+ if (bytes >= KB) {
1008
+ return `${(bytes / KB).toFixed(1)} KB`;
1009
+ }
1010
+ return `${bytes} B`;
1011
+ }
1012
+ function formatLocalISOString(date) {
1013
+ const tzOffsetMs = date.getTimezoneOffset() * 60 * 1e3;
1014
+ const localDate = new Date(date.getTime() - tzOffsetMs);
1015
+ const iso = localDate.toISOString().slice(0, -1);
1016
+ const totalMinutes = -date.getTimezoneOffset();
1017
+ const sign = totalMinutes >= 0 ? "+" : "-";
1018
+ const absMinutes = Math.abs(totalMinutes);
1019
+ const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
1020
+ const minutes = String(absMinutes % 60).padStart(2, "0");
1021
+ return `${iso}${sign}${hours}:${minutes}`;
1022
+ }
970
1023
 
971
1024
  // src/utils/branch.ts
972
1025
  function sanitizeBranchName(branchName) {
@@ -1017,11 +1070,12 @@ function validateClaudeCodeInstalled() {
1017
1070
  }
1018
1071
 
1019
1072
  // src/utils/worktree.ts
1020
- import { join as join2 } from "path";
1073
+ import { join as join3 } from "path";
1021
1074
  import { existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
1022
1075
 
1023
1076
  // src/utils/fs.ts
1024
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmdirSync } from "fs";
1077
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmdirSync, statSync } from "fs";
1078
+ import { join as join2 } from "path";
1025
1079
  function ensureDir(dirPath) {
1026
1080
  if (!existsSync2(dirPath)) {
1027
1081
  mkdirSync2(dirPath, { recursive: true });
@@ -1038,11 +1092,30 @@ function removeEmptyDir(dirPath) {
1038
1092
  }
1039
1093
  return false;
1040
1094
  }
1095
+ function calculateDirSize(dirPath) {
1096
+ let totalSize = 0;
1097
+ try {
1098
+ const entries = readdirSync(dirPath, { withFileTypes: true });
1099
+ for (const entry of entries) {
1100
+ const fullPath = join2(dirPath, entry.name);
1101
+ try {
1102
+ if (entry.isDirectory()) {
1103
+ totalSize += calculateDirSize(fullPath);
1104
+ } else if (entry.isFile()) {
1105
+ totalSize += statSync(fullPath).size;
1106
+ }
1107
+ } catch {
1108
+ }
1109
+ }
1110
+ } catch {
1111
+ }
1112
+ return totalSize;
1113
+ }
1041
1114
 
1042
1115
  // src/utils/worktree.ts
1043
1116
  function getProjectWorktreeDir() {
1044
1117
  const projectName = getProjectName();
1045
- return join2(WORKTREES_DIR, projectName);
1118
+ return join3(WORKTREES_DIR, projectName);
1046
1119
  }
1047
1120
  function createWorktrees(branchName, count) {
1048
1121
  const sanitized = sanitizeBranchName(branchName);
@@ -1052,7 +1125,7 @@ function createWorktrees(branchName, count) {
1052
1125
  ensureDir(projectDir);
1053
1126
  const results = [];
1054
1127
  for (const name of branchNames) {
1055
- const worktreePath = join2(projectDir, name);
1128
+ const worktreePath = join3(projectDir, name);
1056
1129
  createWorktree(name, worktreePath);
1057
1130
  results.push({ path: worktreePath, branch: name });
1058
1131
  logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
@@ -1065,7 +1138,7 @@ function createWorktreesByBranches(branchNames) {
1065
1138
  ensureDir(projectDir);
1066
1139
  const results = [];
1067
1140
  for (const name of branchNames) {
1068
- const worktreePath = join2(projectDir, name);
1141
+ const worktreePath = join3(projectDir, name);
1069
1142
  createWorktree(name, worktreePath);
1070
1143
  results.push({ path: worktreePath, branch: name });
1071
1144
  logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
@@ -1087,7 +1160,7 @@ function getProjectWorktrees() {
1087
1160
  if (!entry.isDirectory()) {
1088
1161
  continue;
1089
1162
  }
1090
- const fullPath = join2(projectDir, entry.name);
1163
+ const fullPath = join3(projectDir, entry.name);
1091
1164
  if (registeredPaths.has(fullPath)) {
1092
1165
  worktrees.push({
1093
1166
  path: fullPath,
@@ -1173,7 +1246,7 @@ import Enquirer from "enquirer";
1173
1246
  // src/utils/claude.ts
1174
1247
  import { spawnSync as spawnSync2 } from "child_process";
1175
1248
  import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
1176
- import { join as join3 } from "path";
1249
+ import { join as join4 } from "path";
1177
1250
 
1178
1251
  // src/utils/terminal.ts
1179
1252
  import { execFileSync as execFileSync2 } from "child_process";
@@ -1252,7 +1325,7 @@ function encodeClaudeProjectPath(absolutePath) {
1252
1325
  }
1253
1326
  function hasClaudeSessionHistory(worktreePath) {
1254
1327
  const encodedName = encodeClaudeProjectPath(worktreePath);
1255
- const projectDir = join3(CLAUDE_PROJECTS_DIR, encodedName);
1328
+ const projectDir = join4(CLAUDE_PROJECTS_DIR, encodedName);
1256
1329
  if (!existsSync6(projectDir)) {
1257
1330
  return false;
1258
1331
  }
@@ -1309,13 +1382,13 @@ function launchInteractiveClaudeInNewTerminal(worktree, hasPreviousSession) {
1309
1382
  }
1310
1383
 
1311
1384
  // 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";
1385
+ import { join as join5 } from "path";
1386
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2, statSync as statSync2 } from "fs";
1314
1387
  function getSnapshotPath(projectName, branchName) {
1315
- return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
1388
+ return join5(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
1316
1389
  }
1317
1390
  function getSnapshotHeadPath(projectName, branchName) {
1318
- return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
1391
+ return join5(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
1319
1392
  }
1320
1393
  function hasSnapshot(projectName, branchName) {
1321
1394
  return existsSync7(getSnapshotPath(projectName, branchName));
@@ -1323,7 +1396,7 @@ function hasSnapshot(projectName, branchName) {
1323
1396
  function getSnapshotModifiedTime(projectName, branchName) {
1324
1397
  const snapshotPath = getSnapshotPath(projectName, branchName);
1325
1398
  if (!existsSync7(snapshotPath)) return null;
1326
- const stat = statSync(snapshotPath);
1399
+ const stat = statSync2(snapshotPath);
1327
1400
  return stat.mtime.toISOString();
1328
1401
  }
1329
1402
  function readSnapshot(projectName, branchName) {
@@ -1337,7 +1410,7 @@ function readSnapshot(projectName, branchName) {
1337
1410
  function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
1338
1411
  const snapshotPath = getSnapshotPath(projectName, branchName);
1339
1412
  const headPath = getSnapshotHeadPath(projectName, branchName);
1340
- const snapshotDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1413
+ const snapshotDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
1341
1414
  ensureDir(snapshotDir);
1342
1415
  writeFileSync2(snapshotPath, treeHash, "utf-8");
1343
1416
  writeFileSync2(headPath, headCommitHash, "utf-8");
@@ -1356,7 +1429,7 @@ function removeSnapshot(projectName, branchName) {
1356
1429
  }
1357
1430
  }
1358
1431
  function getProjectSnapshotBranches(projectName) {
1359
- const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1432
+ const projectDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
1360
1433
  if (!existsSync7(projectDir)) {
1361
1434
  return [];
1362
1435
  }
@@ -1364,13 +1437,13 @@ function getProjectSnapshotBranches(projectName) {
1364
1437
  return files.filter((f) => f.endsWith(".tree")).map((f) => f.replace(/\.tree$/, ""));
1365
1438
  }
1366
1439
  function removeProjectSnapshots(projectName) {
1367
- const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1440
+ const projectDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
1368
1441
  if (!existsSync7(projectDir)) {
1369
1442
  return;
1370
1443
  }
1371
1444
  const files = readdirSync4(projectDir);
1372
1445
  for (const file of files) {
1373
- unlinkSync(join4(projectDir, file));
1446
+ unlinkSync(join5(projectDir, file));
1374
1447
  }
1375
1448
  try {
1376
1449
  rmdirSync2(projectDir);
@@ -2169,7 +2242,7 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
2169
2242
 
2170
2243
  // src/utils/dry-run.ts
2171
2244
  import chalk4 from "chalk";
2172
- import { join as join5 } from "path";
2245
+ import { join as join6 } from "path";
2173
2246
  var DRY_RUN_TASK_DESC_MAX_LENGTH = 80;
2174
2247
  function truncateTaskDesc(task) {
2175
2248
  const oneLine = task.replace(/\n/g, " ").trim();
@@ -2197,7 +2270,7 @@ function printDryRunPreview(branchNames, tasks, concurrency) {
2197
2270
  let hasConflict = false;
2198
2271
  for (let i = 0; i < branchNames.length; i++) {
2199
2272
  const branch = branchNames[i];
2200
- const worktreePath = join5(projectDir, branch);
2273
+ const worktreePath = join6(projectDir, branch);
2201
2274
  const exists = checkBranchExists(branch);
2202
2275
  if (exists) hasConflict = true;
2203
2276
  const indexLabel = `[${i + 1}/${branchNames.length}]`;
@@ -3560,8 +3633,187 @@ function registerAliasCommand(program2) {
3560
3633
  });
3561
3634
  }
3562
3635
 
3636
+ // src/commands/projects.ts
3637
+ import { existsSync as existsSync9, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
3638
+ import { join as join7 } from "path";
3639
+ import chalk11 from "chalk";
3640
+ function registerProjectsCommand(program2) {
3641
+ 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) => {
3642
+ handleProjects({ name, json: options.json });
3643
+ });
3644
+ }
3645
+ function handleProjects(options) {
3646
+ if (options.name) {
3647
+ handleProjectDetail(options.name, options.json);
3648
+ } else {
3649
+ handleProjectsOverview(options.json);
3650
+ }
3651
+ }
3652
+ function handleProjectsOverview(json) {
3653
+ const result = collectProjectsOverview();
3654
+ logger.info(`projects \u547D\u4EE4\u6267\u884C\uFF0C\u5171 ${result.totalProjects} \u4E2A\u9879\u76EE`);
3655
+ if (json) {
3656
+ console.log(JSON.stringify(result, null, 2));
3657
+ return;
3658
+ }
3659
+ printProjectsOverviewAsText(result);
3660
+ }
3661
+ function handleProjectDetail(name, json) {
3662
+ const projectDir = join7(WORKTREES_DIR, name);
3663
+ if (!existsSync9(projectDir)) {
3664
+ printError(MESSAGES.PROJECTS_NOT_FOUND(name));
3665
+ process.exit(1);
3666
+ }
3667
+ const result = collectProjectDetail(name, projectDir);
3668
+ logger.info(`projects \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${name}\uFF0C\u5171 ${result.worktrees.length} \u4E2A worktree`);
3669
+ if (json) {
3670
+ console.log(JSON.stringify(result, null, 2));
3671
+ return;
3672
+ }
3673
+ printProjectDetailAsText(result);
3674
+ }
3675
+ function collectProjectsOverview() {
3676
+ if (!existsSync9(WORKTREES_DIR)) {
3677
+ return { projects: [], totalProjects: 0, totalDiskUsage: 0 };
3678
+ }
3679
+ const entries = readdirSync5(WORKTREES_DIR, { withFileTypes: true });
3680
+ const projects = [];
3681
+ for (const entry of entries) {
3682
+ if (!entry.isDirectory()) {
3683
+ continue;
3684
+ }
3685
+ const projectDir = join7(WORKTREES_DIR, entry.name);
3686
+ const overview = collectSingleProjectOverview(entry.name, projectDir);
3687
+ projects.push(overview);
3688
+ }
3689
+ sortByLastActiveTimeDesc(projects);
3690
+ const totalDiskUsage = projects.reduce((sum, p) => sum + p.diskUsage, 0);
3691
+ return {
3692
+ projects,
3693
+ totalProjects: projects.length,
3694
+ totalDiskUsage
3695
+ };
3696
+ }
3697
+ function collectSingleProjectOverview(name, projectDir) {
3698
+ const subEntries = readdirSync5(projectDir, { withFileTypes: true });
3699
+ const worktreeDirs = subEntries.filter((e) => e.isDirectory());
3700
+ const worktreeCount = worktreeDirs.length;
3701
+ const diskUsage = calculateDirSize(projectDir);
3702
+ const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join7(projectDir, e.name)));
3703
+ return {
3704
+ name,
3705
+ worktreeCount,
3706
+ lastActiveTime,
3707
+ diskUsage
3708
+ };
3709
+ }
3710
+ function collectProjectDetail(name, projectDir) {
3711
+ const subEntries = readdirSync5(projectDir, { withFileTypes: true });
3712
+ const worktrees = [];
3713
+ for (const entry of subEntries) {
3714
+ if (!entry.isDirectory()) {
3715
+ continue;
3716
+ }
3717
+ const wtPath = join7(projectDir, entry.name);
3718
+ const detail = collectSingleWorktreeDetail(entry.name, wtPath);
3719
+ worktrees.push(detail);
3720
+ }
3721
+ worktrees.sort((a, b) => new Date(b.lastModifiedTime).getTime() - new Date(a.lastModifiedTime).getTime());
3722
+ const totalDiskUsage = worktrees.reduce((sum, wt) => sum + wt.diskUsage, 0);
3723
+ return {
3724
+ name,
3725
+ projectDir,
3726
+ worktrees,
3727
+ totalDiskUsage
3728
+ };
3729
+ }
3730
+ function collectSingleWorktreeDetail(branch, wtPath) {
3731
+ const stat = statSync3(wtPath);
3732
+ const diskUsage = calculateDirSize(wtPath);
3733
+ return {
3734
+ branch,
3735
+ path: wtPath,
3736
+ lastModifiedTime: formatLocalISOString(stat.mtime),
3737
+ diskUsage
3738
+ };
3739
+ }
3740
+ function resolveProjectLastActiveTime(projectDir, worktreePaths) {
3741
+ let latestTime = statSync3(projectDir).mtime;
3742
+ for (const wtPath of worktreePaths) {
3743
+ try {
3744
+ const wtStat = statSync3(wtPath);
3745
+ if (wtStat.mtime > latestTime) {
3746
+ latestTime = wtStat.mtime;
3747
+ }
3748
+ } catch {
3749
+ }
3750
+ }
3751
+ return formatLocalISOString(latestTime);
3752
+ }
3753
+ function sortByLastActiveTimeDesc(projects) {
3754
+ projects.sort((a, b) => new Date(b.lastActiveTime).getTime() - new Date(a.lastActiveTime).getTime());
3755
+ }
3756
+ function printProjectsOverviewAsText(result) {
3757
+ printDoubleSeparator();
3758
+ printInfo(` ${chalk11.bold.cyan(MESSAGES.PROJECTS_OVERVIEW_TITLE)}`);
3759
+ printDoubleSeparator();
3760
+ printInfo("");
3761
+ if (result.projects.length === 0) {
3762
+ printInfo(` ${MESSAGES.PROJECTS_NO_PROJECTS}`);
3763
+ printInfo("");
3764
+ printDoubleSeparator();
3765
+ return;
3766
+ }
3767
+ for (const project of result.projects) {
3768
+ printProjectOverviewItem(project);
3769
+ }
3770
+ printSeparator();
3771
+ printInfo("");
3772
+ printInfo(` \u5171 ${chalk11.bold(String(result.totalProjects))} \u4E2A\u9879\u76EE ${chalk11.gray(MESSAGES.PROJECTS_TOTAL_DISK_USAGE(formatDiskSize(result.totalDiskUsage)))}`);
3773
+ printInfo("");
3774
+ printDoubleSeparator();
3775
+ }
3776
+ function printProjectOverviewItem(project) {
3777
+ const relativeTime = formatRelativeTime(project.lastActiveTime);
3778
+ const activeLabel = relativeTime ? MESSAGES.PROJECTS_LAST_ACTIVE(relativeTime) : "";
3779
+ const diskLabel = MESSAGES.PROJECTS_DISK_USAGE(formatDiskSize(project.diskUsage));
3780
+ printInfo(` ${chalk11.bold("\u25CF")} ${chalk11.bold(project.name)}`);
3781
+ printInfo(` ${MESSAGES.PROJECTS_WORKTREE_COUNT(project.worktreeCount)} ${chalk11.gray(activeLabel)} ${chalk11.gray(diskLabel)}`);
3782
+ printInfo("");
3783
+ }
3784
+ function printProjectDetailAsText(result) {
3785
+ printDoubleSeparator();
3786
+ printInfo(` ${chalk11.bold.cyan(MESSAGES.PROJECTS_DETAIL_TITLE(result.name))}`);
3787
+ printDoubleSeparator();
3788
+ printInfo("");
3789
+ printInfo(` ${chalk11.bold("\u25C6")} ${chalk11.bold(MESSAGES.PROJECTS_PATH(result.projectDir))}`);
3790
+ printInfo(` ${MESSAGES.PROJECTS_TOTAL_DISK_USAGE(formatDiskSize(result.totalDiskUsage))}`);
3791
+ printInfo("");
3792
+ printSeparator();
3793
+ printInfo("");
3794
+ if (result.worktrees.length === 0) {
3795
+ printInfo(` ${MESSAGES.PROJECTS_DETAIL_NO_WORKTREES}`);
3796
+ printInfo("");
3797
+ printDoubleSeparator();
3798
+ return;
3799
+ }
3800
+ for (const wt of result.worktrees) {
3801
+ printWorktreeDetailItem(wt);
3802
+ }
3803
+ printDoubleSeparator();
3804
+ }
3805
+ function printWorktreeDetailItem(wt) {
3806
+ const relativeTime = formatRelativeTime(wt.lastModifiedTime);
3807
+ const modifiedLabel = relativeTime ? MESSAGES.PROJECTS_LAST_MODIFIED(relativeTime) : "";
3808
+ const diskLabel = MESSAGES.PROJECTS_DISK_USAGE(formatDiskSize(wt.diskUsage));
3809
+ printInfo(` ${chalk11.bold("\u25CF")} ${chalk11.bold(wt.branch)}`);
3810
+ printInfo(` ${wt.path}`);
3811
+ printInfo(` ${chalk11.gray(modifiedLabel)} ${chalk11.gray(diskLabel)}`);
3812
+ printInfo("");
3813
+ }
3814
+
3563
3815
  // src/commands/completion.ts
3564
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync10 } from "fs";
3816
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync11 } from "fs";
3565
3817
  import { resolve as resolve2 } from "path";
3566
3818
  import { homedir as homedir2 } from "os";
3567
3819
 
@@ -3612,25 +3864,25 @@ compdef _clawt_completion clawt
3612
3864
  }
3613
3865
 
3614
3866
  // 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";
3867
+ import { existsSync as existsSync10, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
3868
+ import { join as join8, dirname, basename as basename2 } from "path";
3617
3869
  function completeFilePath(partial) {
3618
3870
  const cwd = process.cwd();
3619
3871
  const hasDir = partial.includes("/");
3620
- const searchDir = hasDir ? join6(cwd, dirname(partial)) : cwd;
3872
+ const searchDir = hasDir ? join8(cwd, dirname(partial)) : cwd;
3621
3873
  const prefix = hasDir ? basename2(partial) : partial;
3622
- if (!existsSync9(searchDir)) {
3874
+ if (!existsSync10(searchDir)) {
3623
3875
  return [];
3624
3876
  }
3625
- const entries = readdirSync5(searchDir);
3877
+ const entries = readdirSync6(searchDir);
3626
3878
  const results = [];
3627
3879
  const dirPrefix = hasDir ? dirname(partial) + "/" : "";
3628
3880
  for (const entry of entries) {
3629
3881
  if (!entry.startsWith(prefix)) continue;
3630
3882
  if (entry.startsWith(".")) continue;
3631
- const fullPath = join6(searchDir, entry);
3883
+ const fullPath = join8(searchDir, entry);
3632
3884
  try {
3633
- const stat = statSync2(fullPath);
3885
+ const stat = statSync4(fullPath);
3634
3886
  if (stat.isDirectory()) {
3635
3887
  results.push(dirPrefix + entry + "/");
3636
3888
  } else if (stat.isFile()) {
@@ -3717,7 +3969,7 @@ function generateCompletions(program2, args) {
3717
3969
 
3718
3970
  // src/commands/completion.ts
3719
3971
  function appendToFile(filePath, content) {
3720
- if (existsSync10(filePath)) {
3972
+ if (existsSync11(filePath)) {
3721
3973
  const current = readFileSync5(filePath, "utf-8");
3722
3974
  if (current.includes("clawt completion")) {
3723
3975
  printInfo(MESSAGES.COMPLETION_INSTALL_EXISTS + ": " + filePath);
@@ -3795,6 +4047,7 @@ registerSyncCommand(program);
3795
4047
  registerResetCommand(program);
3796
4048
  registerStatusCommand(program);
3797
4049
  registerAliasCommand(program);
4050
+ registerProjectsCommand(program);
3798
4051
  registerCompletionCommand(program);
3799
4052
  var config = loadConfig();
3800
4053
  applyAliases(program, config.aliases);
@@ -364,6 +364,32 @@ var ALIAS_MESSAGES = {
364
364
  ALIAS_LIST_TITLE: "\u5F53\u524D\u522B\u540D\u5217\u8868\uFF1A"
365
365
  };
366
366
 
367
+ // src/constants/messages/projects.ts
368
+ var PROJECTS_MESSAGES = {
369
+ /** projects 命令全局概览标题 */
370
+ PROJECTS_OVERVIEW_TITLE: "\u9879\u76EE\u6982\u89C8",
371
+ /** projects 命令指定项目详情标题 */
372
+ PROJECTS_DETAIL_TITLE: (projectName) => `\u9879\u76EE\u8BE6\u60C5: ${projectName}`,
373
+ /** 无项目提示 */
374
+ PROJECTS_NO_PROJECTS: "(\u6682\u65E0\u9879\u76EE\uFF0Cworktrees \u76EE\u5F55\u4E3A\u7A7A)",
375
+ /** 项目不存在提示 */
376
+ PROJECTS_NOT_FOUND: (name) => `\u9879\u76EE ${name} \u4E0D\u5B58\u5728`,
377
+ /** worktree 数量标签 */
378
+ PROJECTS_WORKTREE_COUNT: (count) => `${count} \u4E2A worktree`,
379
+ /** 最近活跃时间标签 */
380
+ PROJECTS_LAST_ACTIVE: (relativeTime) => `\u6700\u8FD1\u6D3B\u8DC3: ${relativeTime}`,
381
+ /** 磁盘占用标签 */
382
+ PROJECTS_DISK_USAGE: (size) => `\u78C1\u76D8\u5360\u7528: ${size}`,
383
+ /** 总磁盘占用标签 */
384
+ PROJECTS_TOTAL_DISK_USAGE: (size) => `\u603B\u5360\u7528: ${size}`,
385
+ /** projects 详情无 worktree */
386
+ PROJECTS_DETAIL_NO_WORKTREES: "(\u8BE5\u9879\u76EE\u4E0B\u65E0 worktree)",
387
+ /** 路径标签 */
388
+ PROJECTS_PATH: (path) => `\u8DEF\u5F84: ${path}`,
389
+ /** 最后修改时间标签 */
390
+ PROJECTS_LAST_MODIFIED: (relativeTime) => `\u6700\u540E\u4FEE\u6539: ${relativeTime}`
391
+ };
392
+
367
393
  // src/constants/messages/completion.ts
368
394
  var COMPLETION_MESSAGES = {
369
395
  /** completion 命令的主描述 */
@@ -400,6 +426,7 @@ var MESSAGES = {
400
426
  ...CONFIG_CMD_MESSAGES,
401
427
  ...STATUS_MESSAGES,
402
428
  ...ALIAS_MESSAGES,
429
+ ...PROJECTS_MESSAGES,
403
430
  ...COMPLETION_MESSAGES
404
431
  };
405
432