clawt 2.17.1 → 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.
@@ -10,6 +10,7 @@ var LOGS_DIR = join(CLAWT_HOME, "logs");
10
10
  var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
11
11
  var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
12
12
  var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
13
+ var UPDATE_CHECK_PATH = join(CLAWT_HOME, "update-check.json");
13
14
 
14
15
  // src/constants/messages/common.ts
15
16
  var COMMON_MESSAGES = {
@@ -363,6 +364,32 @@ var ALIAS_MESSAGES = {
363
364
  ALIAS_LIST_TITLE: "\u5F53\u524D\u522B\u540D\u5217\u8868\uFF1A"
364
365
  };
365
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
+
366
393
  // src/constants/messages/completion.ts
367
394
  var COMPLETION_MESSAGES = {
368
395
  /** completion 命令的主描述 */
@@ -399,6 +426,7 @@ var MESSAGES = {
399
426
  ...CONFIG_CMD_MESSAGES,
400
427
  ...STATUS_MESSAGES,
401
428
  ...ALIAS_MESSAGES,
429
+ ...PROJECTS_MESSAGES,
402
430
  ...COMPLETION_MESSAGES
403
431
  };
404
432
 
@@ -435,6 +463,10 @@ var CONFIG_DEFINITIONS = {
435
463
  aliases: {
436
464
  defaultValue: {},
437
465
  description: "\u547D\u4EE4\u522B\u540D\u6620\u5C04"
466
+ },
467
+ autoUpdate: {
468
+ defaultValue: true,
469
+ description: "\u662F\u5426\u542F\u7528\u81EA\u52A8\u66F4\u65B0\u68C0\u67E5\uFF08\u6BCF 24 \u5C0F\u65F6\u68C0\u67E5\u4E00\u6B21 npm registry\uFF09"
438
470
  }
439
471
  };
440
472
  function deriveDefaultConfig(definitions) {
@@ -452,6 +484,9 @@ function deriveConfigDescriptions(definitions) {
452
484
  var DEFAULT_CONFIG = deriveDefaultConfig(CONFIG_DEFINITIONS);
453
485
  var CONFIG_DESCRIPTIONS = deriveConfigDescriptions(CONFIG_DEFINITIONS);
454
486
 
487
+ // src/constants/update.ts
488
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
489
+
455
490
  // scripts/postinstall.ts
456
491
  function ensureDirectory(dirPath) {
457
492
  if (!existsSync(dirPath)) {
package/docs/spec.md CHANGED
@@ -28,6 +28,8 @@
28
28
  - [5.14 项目全局状态总览](#514-项目全局状态总览)
29
29
  - [5.15 命令别名管理](#515-命令别名管理)
30
30
  - [5.16 Shell 自动补全](#516-clawt-completion-命令)
31
+ - [5.17 自动更新检查](#517-自动更新检查)
32
+ - [5.18 跨项目 Worktree 概览](#518-跨项目-worktree-概览)
31
33
  - [6. 错误处理规范](#6-错误处理规范)
32
34
  - [7. 非功能性需求](#7-非功能性需求)
33
35
  - [7.1 性能](#71-性能)
@@ -146,6 +148,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
146
148
  ```
147
149
  ~/.clawt/
148
150
  ├── config.json # 全局配置文件
151
+ ├── update-check.json # 更新检查缓存文件(自动生成)
149
152
  ├── logs/ # 日志目录
150
153
  │ ├── clawt-2025-02-06.log
151
154
  │ └── ...
@@ -186,6 +189,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
186
189
  | `clawt status` | 显示项目全局状态总览(支持 `--json` 格式输出) | 5.14 |
187
190
  | `clawt alias` | 管理命令别名(列出 / 设置 / 移除) | 5.15 |
188
191
  | `clawt completion` | 为终端提供 shell 自动补全功能(bash/zsh) | 5.16 |
192
+ | `clawt projects` | 展示所有项目的 worktree 概览,或查看指定项目的 worktree 详情 | 5.17 |
189
193
 
190
194
  **全局选项:**
191
195
 
@@ -1067,7 +1071,8 @@ clawt merge [-m <commitMessage>]
1067
1071
  "confirmDestructiveOps": true,
1068
1072
  "maxConcurrency": 0,
1069
1073
  "terminalApp": "auto",
1070
- "aliases": {}
1074
+ "aliases": {},
1075
+ "autoUpdate": true
1071
1076
  }
1072
1077
  ```
1073
1078
 
@@ -1082,6 +1087,7 @@ clawt merge [-m <commitMessage>]
1082
1087
  | `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
1083
1088
  | `terminalApp` | `string` | `"auto"` | 批量 resume 使用的终端应用:`auto`(自动检测)、`iterm2`、`terminal`(macOS) |
1084
1089
  | `aliases` | `Record<string, string>` | `{}` | 命令别名映射,键为别名,值为目标内置命令名 |
1090
+ | `autoUpdate` | `boolean` | `true` | 是否启用自动更新检查(每 24 小时通过 npm registry 检查一次新版本) |
1085
1091
 
1086
1092
  ---
1087
1093
 
@@ -1651,6 +1657,9 @@ clawt status [--json]
1651
1657
  - `formatRelativeTime()` 是新增的格式化函数(在 `src/utils/formatter.ts`),将 ISO 8601 日期字符串转换为中文相对时间描述(如"3 天前"、"2 小时前"、"刚刚"),无效日期时返回 null
1652
1658
  - `getCommitCountBehind()` 是新增的工具函数(在 `src/utils/git.ts`),通过 `git rev-list --count <branch>..HEAD` 计算落后提交数
1653
1659
  - `getProjectSnapshotBranches()` 是新增的工具函数(在 `src/utils/validate-snapshot.ts`),通过扫描快照目录下的 `.tree` 文件提取分支名列表
1660
+ - `formatDiskSize()` 是新增的格式化函数(在 `src/utils/formatter.ts`),将字节数格式化为带单位的磁盘大小字符串(如 `"1.5 GB"`、`"256.0 MB"`、`"10.2 KB"`、`"512 B"`)
1661
+ - `formatLocalISOString()` 是新增的格式化函数(在 `src/utils/formatter.ts`),将 Date 对象格式化为本机时区的 ISO 8601 字符串(输出格式: `YYYY-MM-DDTHH:mm:ss.sss+HH:MM`),替代 `Date.toISOString()` 的 UTC 时区输出
1662
+ - `calculateDirSize()` 是新增的文件系统工具函数(在 `src/utils/fs.ts`),递归计算目录占用的磁盘大小(字节),遇到无法访问的文件或目录时静默跳过
1654
1663
 
1655
1664
  ---
1656
1665
 
@@ -1816,6 +1825,238 @@ clawt completion install
1816
1825
 
1817
1826
  ---
1818
1827
 
1828
+ ### 5.17 自动更新检查
1829
+
1830
+ CLI 在每次命令执行完毕后,根据配置项 `autoUpdate` 决定是否检查 npm registry 上的最新版本。当发现新版本时,以带边框的提示框在终端输出版本更新信息和升级命令。
1831
+
1832
+ #### 触发条件
1833
+
1834
+ - 配置项 `autoUpdate` 为 `true`(默认启用)
1835
+ - 命令正常执行完毕后触发(在 `program.parseAsync()` 之后)
1836
+
1837
+ #### 检查流程
1838
+
1839
+ 1. 读取缓存文件 `~/.clawt/update-check.json`
1840
+ 2. 判断缓存是否有效:
1841
+ - 缓存不存在或解析失败 → 视为过期
1842
+ - 缓存中的 `currentVersion` 与本地版本不一致 → 视为过期
1843
+ - 距离上次检查超过 24 小时 → 视为过期
1844
+ 3. **缓存有效**:直接使用缓存中的 `latestVersion` 与本地版本比较,有新版本则打印提示
1845
+ 4. **缓存过期**:向 npm registry 发起 HTTPS 请求获取最新版本号(5 秒超时),更新缓存文件后判断并打印提示
1846
+
1847
+ #### 缓存文件
1848
+
1849
+ **路径:** `~/.clawt/update-check.json`
1850
+
1851
+ **结构:**
1852
+
1853
+ ```json
1854
+ {
1855
+ "lastCheck": 1709000000000,
1856
+ "latestVersion": "2.18.0",
1857
+ "currentVersion": "2.17.1"
1858
+ }
1859
+ ```
1860
+
1861
+ | 字段 | 类型 | 说明 |
1862
+ | ---- | ---- | ---- |
1863
+ | `lastCheck` | `number` | 上次检查时间戳(毫秒) |
1864
+ | `latestVersion` | `string` | 从 registry 获取的最新版本号 |
1865
+ | `currentVersion` | `string` | 检查时的本地版本号 |
1866
+
1867
+ #### 版本比较
1868
+
1869
+ 使用简易 semver 比较(不引入额外依赖),逐级比较 `major.minor.patch`:
1870
+
1871
+ - `latest > current` → 提示更新
1872
+ - `latest <= current` → 不提示
1873
+
1874
+ #### 包管理器检测
1875
+
1876
+ 更新提示中会显示与用户安装方式匹配的升级命令。检测逻辑依次尝试:
1877
+
1878
+ 1. `pnpm list -g --depth=0 clawt` → 匹配则提示 `pnpm add -g clawt`
1879
+ 2. `yarn global list --depth=0` → 输出含 `clawt` 则提示 `yarn global add clawt`
1880
+ 3. 以上均未匹配 → 默认提示 `npm i -g clawt`
1881
+
1882
+ #### 提示框格式
1883
+
1884
+ 当检测到新版本时,输出带 Unicode 圆角边框的居中提示框:
1885
+
1886
+ ```
1887
+ ╭──────────────────────────────────────────────╮
1888
+ │ │
1889
+ │ clawt 有新版本可用: 2.17.1 → 2.18.0 │
1890
+ │ 执行 npm i -g clawt 进行更新 │
1891
+ │ │
1892
+ ╰──────────────────────────────────────────────╯
1893
+ ```
1894
+
1895
+ 版本号和命令使用 chalk 着色:当前版本红色、最新版本绿色、更新命令青色。
1896
+
1897
+ #### 容错设计
1898
+
1899
+ 所有异常静默处理,不影响 CLI 正常功能:
1900
+
1901
+ - 网络请求失败或超时(5 秒) → 静默忽略
1902
+ - registry 返回无效 JSON 或缺少 `version` 字段 → 静默忽略
1903
+ - 缓存文件读写失败 → 静默忽略
1904
+ - `checkForUpdates()` 入口函数的最外层 `try/catch` 确保任何未预期异常都不会中断 CLI
1905
+
1906
+ #### 常量定义
1907
+
1908
+ | 常量 | 值 | 位置 |
1909
+ | ---- | -- | ---- |
1910
+ | `UPDATE_CHECK_INTERVAL_MS` | `86400000`(24 小时) | `src/constants/update.ts` |
1911
+ | `NPM_REGISTRY_URL` | `https://registry.npmjs.org/clawt/latest` | `src/constants/update.ts` |
1912
+ | `NPM_REGISTRY_TIMEOUT_MS` | `5000` | `src/constants/update.ts` |
1913
+ | `PACKAGE_NAME` | `clawt` | `src/constants/update.ts` |
1914
+ | `UPDATE_CHECK_PATH` | `~/.clawt/update-check.json` | `src/constants/paths.ts` |
1915
+
1916
+ #### 实现说明
1917
+
1918
+ - 入口函数:`checkForUpdates()`(在 `src/utils/update-checker.ts`)
1919
+ - 消息常量:`UPDATE_MESSAGES`、`UPDATE_COMMANDS`(在 `src/constants/messages/update.ts`)
1920
+ - 入口调用点:`src/index.ts` 的 `main()` 异步函数中,`program.parseAsync()` 之后根据 `config.autoUpdate` 条件调用
1921
+
1922
+ ---
1923
+
1924
+ ### 5.18 跨项目 Worktree 概览
1925
+
1926
+ **命令:**
1927
+
1928
+ ```bash
1929
+ clawt projects [name] [--json]
1930
+ ```
1931
+
1932
+ **参数:**
1933
+
1934
+ | 参数 | 必填 | 说明 |
1935
+ | -------- | ---- | ---------------------------------------------- |
1936
+ | `[name]` | 否 | 指定项目名,查看该项目的 worktree 详情 |
1937
+ | `--json` | 否 | 以 JSON 格式输出完整数据 |
1938
+
1939
+ **使用场景:**
1940
+
1941
+ 当使用 clawt 管理多个不同项目时,快速了解所有项目的 worktree 数量、磁盘占用和最近活跃时间。也可以指定项目名查看该项目下每个 worktree 的分支、路径、最后修改时间和磁盘占用。
1942
+
1943
+ **注意:** `projects` 命令不需要在主 worktree 中执行(与其他命令不同),它直接扫描 `~/.clawt/worktrees/` 目录。
1944
+
1945
+ **运行流程:**
1946
+
1947
+ #### 无参数模式(项目概览)
1948
+
1949
+ 1. 扫描 `~/.clawt/worktrees/` 目录,列出所有项目子目录
1950
+ 2. 对每个项目收集以下信息:
1951
+ - **项目名**(目录名即项目名)
1952
+ - **worktree 数量**(项目目录下的子目录数)
1953
+ - **最近活跃时间**(取项目目录自身和所有 worktree 目录 mtime 的最大值,通过 `formatLocalISOString()` 格式化为本机时区的 ISO 8601 字符串)
1954
+ - **磁盘占用**(通过 `calculateDirSize()` 递归计算整个项目目录的总大小)
1955
+ 3. 按最近活跃时间降序排序
1956
+ 4. 输出概览信息(文本或 JSON)
1957
+
1958
+ #### 指定项目模式(worktree 详情)
1959
+
1960
+ 1. 检查 `~/.clawt/worktrees/<name>/` 是否存在,不存在则报错退出
1961
+ 2. 扫描项目目录,对每个 worktree 子目录收集:
1962
+ - **分支名**(目录名即分支名)
1963
+ - **worktree 路径**
1964
+ - **最后修改时间**(目录 mtime,通过 `formatLocalISOString()` 格式化)
1965
+ - **磁盘占用**(通过 `calculateDirSize()` 递归计算)
1966
+ 3. 按最后修改时间降序排序
1967
+ 4. 输出详情信息(文本或 JSON)
1968
+
1969
+ **文本输出格式(概览模式):**
1970
+
1971
+ ```
1972
+ ════════════════════════════════════════
1973
+ 项目概览
1974
+ ════════════════════════════════════════
1975
+
1976
+ ● my-project
1977
+ 3 个 worktree 最近活跃: 2 小时前 磁盘占用: 1.5 GB
1978
+
1979
+ ● another-project
1980
+ 1 个 worktree 最近活跃: 3 天前 磁盘占用: 256.0 MB
1981
+
1982
+ ────────────────────────────────────────
1983
+
1984
+ 共 2 个项目 总占用: 1.8 GB
1985
+
1986
+ ════════════════════════════════════════
1987
+ ```
1988
+
1989
+ **文本输出格式(详情模式):**
1990
+
1991
+ ```
1992
+ ════════════════════════════════════════
1993
+ 项目详情: my-project
1994
+ ════════════════════════════════════════
1995
+
1996
+ ◆ 路径: ~/.clawt/worktrees/my-project
1997
+ 总占用: 1.5 GB
1998
+
1999
+ ────────────────────────────────────────
2000
+
2001
+ ● feature-login
2002
+ ~/.clawt/worktrees/my-project/feature-login
2003
+ 最后修改: 2 小时前 磁盘占用: 800.0 MB
2004
+
2005
+ ● feature-signup
2006
+ ~/.clawt/worktrees/my-project/feature-signup
2007
+ 最后修改: 1 天前 磁盘占用: 700.0 MB
2008
+
2009
+ ════════════════════════════════════════
2010
+ ```
2011
+
2012
+ **JSON 输出格式(概览模式,`--json`):**
2013
+
2014
+ ```json
2015
+ {
2016
+ "projects": [
2017
+ {
2018
+ "name": "my-project",
2019
+ "worktreeCount": 3,
2020
+ "lastActiveTime": "2025-06-15T18:30:00.000+08:00",
2021
+ "diskUsage": 1610612736
2022
+ }
2023
+ ],
2024
+ "totalProjects": 1,
2025
+ "totalDiskUsage": 1610612736
2026
+ }
2027
+ ```
2028
+
2029
+ **JSON 输出格式(详情模式,`--json`):**
2030
+
2031
+ ```json
2032
+ {
2033
+ "name": "my-project",
2034
+ "projectDir": "~/.clawt/worktrees/my-project",
2035
+ "worktrees": [
2036
+ {
2037
+ "branch": "feature-login",
2038
+ "path": "~/.clawt/worktrees/my-project/feature-login",
2039
+ "lastModifiedTime": "2025-06-15T18:30:00.000+08:00",
2040
+ "diskUsage": 838860800
2041
+ }
2042
+ ],
2043
+ "totalDiskUsage": 838860800
2044
+ }
2045
+ ```
2046
+
2047
+ **实现要点:**
2048
+
2049
+ - 命令注册函数:`registerProjectsCommand()`(在 `src/commands/projects.ts`)
2050
+ - 类型定义在 `src/types/project.ts`:`ProjectOverview`、`ProjectWorktreeDetail`、`ProjectDetailResult`、`ProjectsOverviewResult`
2051
+ - 命令选项类型:`ProjectsOptions`(在 `src/types/command.ts`)
2052
+ - 消息常量在 `PROJECTS_MESSAGES`(在 `src/constants/messages/projects.ts`)
2053
+ - 时间格式化使用 `formatLocalISOString()`(在 `src/utils/formatter.ts`),输出本机时区的 ISO 8601 字符串(替代 `Date.toISOString()` 的 UTC 输出)
2054
+ - 磁盘大小展示使用 `formatDiskSize()`(在 `src/utils/formatter.ts`),将字节数格式化为带单位的可读字符串
2055
+ - 目录大小计算使用 `calculateDirSize()`(在 `src/utils/fs.ts`),递归遍历目录计算总字节数
2056
+ - 时间的相对展示使用 `formatRelativeTime()`(在 `src/utils/formatter.ts`),将 ISO 8601 日期转换为中文相对时间(如"2 小时前")
2057
+
2058
+ ---
2059
+
1819
2060
  ## 6. 错误处理规范
1820
2061
 
1821
2062
  ### 6.1 通用错误处理
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.17.1",
3
+ "version": "2.19.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,324 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { MESSAGES, WORKTREES_DIR } from '../constants/index.js';
6
+ import { logger } from '../logger/index.js';
7
+ import type {
8
+ ProjectsOptions,
9
+ ProjectOverview,
10
+ ProjectWorktreeDetail,
11
+ ProjectDetailResult,
12
+ ProjectsOverviewResult,
13
+ } from '../types/index.js';
14
+ import {
15
+ formatRelativeTime,
16
+ formatDiskSize,
17
+ formatLocalISOString,
18
+ calculateDirSize,
19
+ printInfo,
20
+ printDoubleSeparator,
21
+ printSeparator,
22
+ printError,
23
+ } from '../utils/index.js';
24
+
25
+ /**
26
+ * 注册 projects 命令:统一管理多个项目的 worktree 状态
27
+ * @param {Command} program - Commander 实例
28
+ */
29
+ export function registerProjectsCommand(program: Command): void {
30
+ program
31
+ .command('projects [name]')
32
+ .description('展示所有项目的 worktree 概览,或查看指定项目的 worktree 详情')
33
+ .option('--json', '以 JSON 格式输出')
34
+ .action((name: string | undefined, options: { json?: boolean }) => {
35
+ handleProjects({ name, json: options.json });
36
+ });
37
+ }
38
+
39
+ /**
40
+ * 执行 projects 命令的核心逻辑
41
+ * @param {ProjectsOptions} options - 命令选项
42
+ */
43
+ function handleProjects(options: ProjectsOptions): void {
44
+ if (options.name) {
45
+ handleProjectDetail(options.name, options.json);
46
+ } else {
47
+ handleProjectsOverview(options.json);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 处理项目概览展示:列出所有项目
53
+ * @param {boolean} [json] - 是否以 JSON 格式输出
54
+ */
55
+ function handleProjectsOverview(json?: boolean): void {
56
+ const result = collectProjectsOverview();
57
+
58
+ logger.info(`projects 命令执行,共 ${result.totalProjects} 个项目`);
59
+
60
+ if (json) {
61
+ console.log(JSON.stringify(result, null, 2));
62
+ return;
63
+ }
64
+
65
+ printProjectsOverviewAsText(result);
66
+ }
67
+
68
+ /**
69
+ * 处理指定项目的 worktree 详情展示
70
+ * @param {string} name - 项目名
71
+ * @param {boolean} [json] - 是否以 JSON 格式输出
72
+ */
73
+ function handleProjectDetail(name: string, json?: boolean): void {
74
+ const projectDir = join(WORKTREES_DIR, name);
75
+
76
+ if (!existsSync(projectDir)) {
77
+ printError(MESSAGES.PROJECTS_NOT_FOUND(name));
78
+ process.exit(1);
79
+ }
80
+
81
+ const result = collectProjectDetail(name, projectDir);
82
+
83
+ logger.info(`projects 命令执行,项目: ${name},共 ${result.worktrees.length} 个 worktree`);
84
+
85
+ if (json) {
86
+ console.log(JSON.stringify(result, null, 2));
87
+ return;
88
+ }
89
+
90
+ printProjectDetailAsText(result);
91
+ }
92
+
93
+ // ─── 数据收集函数 ─────────────────────────────────
94
+
95
+ /**
96
+ * 收集所有项目的概览信息
97
+ * @returns {ProjectsOverviewResult} 所有项目概览结果
98
+ */
99
+ function collectProjectsOverview(): ProjectsOverviewResult {
100
+ if (!existsSync(WORKTREES_DIR)) {
101
+ return { projects: [], totalProjects: 0, totalDiskUsage: 0 };
102
+ }
103
+
104
+ const entries = readdirSync(WORKTREES_DIR, { withFileTypes: true });
105
+ const projects: ProjectOverview[] = [];
106
+
107
+ for (const entry of entries) {
108
+ if (!entry.isDirectory()) {
109
+ continue;
110
+ }
111
+
112
+ const projectDir = join(WORKTREES_DIR, entry.name);
113
+ const overview = collectSingleProjectOverview(entry.name, projectDir);
114
+ projects.push(overview);
115
+ }
116
+
117
+ // 按最近活跃时间降序排序
118
+ sortByLastActiveTimeDesc(projects);
119
+
120
+ const totalDiskUsage = projects.reduce((sum, p) => sum + p.diskUsage, 0);
121
+
122
+ return {
123
+ projects,
124
+ totalProjects: projects.length,
125
+ totalDiskUsage,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * 收集单个项目的概览信息
131
+ * @param {string} name - 项目名
132
+ * @param {string} projectDir - 项目目录路径
133
+ * @returns {ProjectOverview} 项目概览
134
+ */
135
+ function collectSingleProjectOverview(name: string, projectDir: string): ProjectOverview {
136
+ const subEntries = readdirSync(projectDir, { withFileTypes: true });
137
+ const worktreeDirs = subEntries.filter((e) => e.isDirectory());
138
+
139
+ const worktreeCount = worktreeDirs.length;
140
+ const diskUsage = calculateDirSize(projectDir);
141
+ const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join(projectDir, e.name)));
142
+
143
+ return {
144
+ name,
145
+ worktreeCount,
146
+ lastActiveTime,
147
+ diskUsage,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * 收集指定项目的 worktree 详情
153
+ * @param {string} name - 项目名
154
+ * @param {string} projectDir - 项目目录路径
155
+ * @returns {ProjectDetailResult} 项目详情结果
156
+ */
157
+ function collectProjectDetail(name: string, projectDir: string): ProjectDetailResult {
158
+ const subEntries = readdirSync(projectDir, { withFileTypes: true });
159
+ const worktrees: ProjectWorktreeDetail[] = [];
160
+
161
+ for (const entry of subEntries) {
162
+ if (!entry.isDirectory()) {
163
+ continue;
164
+ }
165
+
166
+ const wtPath = join(projectDir, entry.name);
167
+ const detail = collectSingleWorktreeDetail(entry.name, wtPath);
168
+ worktrees.push(detail);
169
+ }
170
+
171
+ // 按最近修改时间降序排序
172
+ worktrees.sort((a, b) => new Date(b.lastModifiedTime).getTime() - new Date(a.lastModifiedTime).getTime());
173
+
174
+ const totalDiskUsage = worktrees.reduce((sum, wt) => sum + wt.diskUsage, 0);
175
+
176
+ return {
177
+ name,
178
+ projectDir,
179
+ worktrees,
180
+ totalDiskUsage,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * 收集单个 worktree 的详情信息
186
+ * @param {string} branch - 分支名(目录名即分支名)
187
+ * @param {string} wtPath - worktree 路径
188
+ * @returns {ProjectWorktreeDetail} worktree 详情
189
+ */
190
+ function collectSingleWorktreeDetail(branch: string, wtPath: string): ProjectWorktreeDetail {
191
+ const stat = statSync(wtPath);
192
+ const diskUsage = calculateDirSize(wtPath);
193
+
194
+ return {
195
+ branch,
196
+ path: wtPath,
197
+ lastModifiedTime: formatLocalISOString(stat.mtime),
198
+ diskUsage,
199
+ };
200
+ }
201
+
202
+ // ─── 工具函数 ─────────────────────────────────────
203
+
204
+ /**
205
+ * 确定项目的最近活跃时间
206
+ * 取项目目录自身和所有子 worktree 目录修改时间的最大值
207
+ * @param {string} projectDir - 项目目录路径
208
+ * @param {string[]} worktreePaths - worktree 路径列表
209
+ * @returns {string} ISO 8601 格式的最近活跃时间
210
+ */
211
+ function resolveProjectLastActiveTime(projectDir: string, worktreePaths: string[]): string {
212
+ let latestTime = statSync(projectDir).mtime;
213
+
214
+ for (const wtPath of worktreePaths) {
215
+ try {
216
+ const wtStat = statSync(wtPath);
217
+ if (wtStat.mtime > latestTime) {
218
+ latestTime = wtStat.mtime;
219
+ }
220
+ } catch {
221
+ // 忽略无法访问的目录
222
+ }
223
+ }
224
+
225
+ return formatLocalISOString(latestTime);
226
+ }
227
+
228
+ /**
229
+ * 按最近活跃时间降序排序项目概览列表(原地排序)
230
+ * @param {ProjectOverview[]} projects - 项目概览列表
231
+ */
232
+ function sortByLastActiveTimeDesc(projects: ProjectOverview[]): void {
233
+ projects.sort((a, b) => new Date(b.lastActiveTime).getTime() - new Date(a.lastActiveTime).getTime());
234
+ }
235
+
236
+ // ─── 文本输出函数 ─────────────────────────────────
237
+
238
+ /**
239
+ * 以文本格式输出所有项目概览
240
+ * @param {ProjectsOverviewResult} result - 项目概览结果
241
+ */
242
+ function printProjectsOverviewAsText(result: ProjectsOverviewResult): void {
243
+ printDoubleSeparator();
244
+ printInfo(` ${chalk.bold.cyan(MESSAGES.PROJECTS_OVERVIEW_TITLE)}`);
245
+ printDoubleSeparator();
246
+ printInfo('');
247
+
248
+ if (result.projects.length === 0) {
249
+ printInfo(` ${MESSAGES.PROJECTS_NO_PROJECTS}`);
250
+ printInfo('');
251
+ printDoubleSeparator();
252
+ return;
253
+ }
254
+
255
+ for (const project of result.projects) {
256
+ printProjectOverviewItem(project);
257
+ }
258
+
259
+ printSeparator();
260
+ printInfo('');
261
+ printInfo(` 共 ${chalk.bold(String(result.totalProjects))} 个项目 ${chalk.gray(MESSAGES.PROJECTS_TOTAL_DISK_USAGE(formatDiskSize(result.totalDiskUsage)))}`);
262
+ printInfo('');
263
+ printDoubleSeparator();
264
+ }
265
+
266
+ /**
267
+ * 输出单个项目概览项
268
+ * @param {ProjectOverview} project - 项目概览
269
+ */
270
+ function printProjectOverviewItem(project: ProjectOverview): void {
271
+ const relativeTime = formatRelativeTime(project.lastActiveTime);
272
+ const activeLabel = relativeTime ? MESSAGES.PROJECTS_LAST_ACTIVE(relativeTime) : '';
273
+ const diskLabel = MESSAGES.PROJECTS_DISK_USAGE(formatDiskSize(project.diskUsage));
274
+
275
+ printInfo(` ${chalk.bold('●')} ${chalk.bold(project.name)}`);
276
+ printInfo(` ${MESSAGES.PROJECTS_WORKTREE_COUNT(project.worktreeCount)} ${chalk.gray(activeLabel)} ${chalk.gray(diskLabel)}`);
277
+ printInfo('');
278
+ }
279
+
280
+ /**
281
+ * 以文本格式输出指定项目的 worktree 详情
282
+ * @param {ProjectDetailResult} result - 项目详情结果
283
+ */
284
+ function printProjectDetailAsText(result: ProjectDetailResult): void {
285
+ printDoubleSeparator();
286
+ printInfo(` ${chalk.bold.cyan(MESSAGES.PROJECTS_DETAIL_TITLE(result.name))}`);
287
+ printDoubleSeparator();
288
+ printInfo('');
289
+
290
+ printInfo(` ${chalk.bold('◆')} ${chalk.bold(MESSAGES.PROJECTS_PATH(result.projectDir))}`);
291
+ printInfo(` ${MESSAGES.PROJECTS_TOTAL_DISK_USAGE(formatDiskSize(result.totalDiskUsage))}`);
292
+ printInfo('');
293
+
294
+ printSeparator();
295
+ printInfo('');
296
+
297
+ if (result.worktrees.length === 0) {
298
+ printInfo(` ${MESSAGES.PROJECTS_DETAIL_NO_WORKTREES}`);
299
+ printInfo('');
300
+ printDoubleSeparator();
301
+ return;
302
+ }
303
+
304
+ for (const wt of result.worktrees) {
305
+ printWorktreeDetailItem(wt);
306
+ }
307
+
308
+ printDoubleSeparator();
309
+ }
310
+
311
+ /**
312
+ * 输出单个 worktree 详情项
313
+ * @param {ProjectWorktreeDetail} wt - worktree 详情
314
+ */
315
+ function printWorktreeDetailItem(wt: ProjectWorktreeDetail): void {
316
+ const relativeTime = formatRelativeTime(wt.lastModifiedTime);
317
+ const modifiedLabel = relativeTime ? MESSAGES.PROJECTS_LAST_MODIFIED(relativeTime) : '';
318
+ const diskLabel = MESSAGES.PROJECTS_DISK_USAGE(formatDiskSize(wt.diskUsage));
319
+
320
+ printInfo(` ${chalk.bold('●')} ${chalk.bold(wt.branch)}`);
321
+ printInfo(` ${wt.path}`);
322
+ printInfo(` ${chalk.gray(modifiedLabel)} ${chalk.gray(diskLabel)}`);
323
+ printInfo('');
324
+ }