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 +10 -0
- package/dist/index.js +281 -28
- package/dist/postinstall.js +27 -0
- package/docs/spec.md +141 -0
- package/package.json +1 -1
- package/src/commands/projects.ts +324 -0
- package/src/constants/messages/index.ts +2 -0
- package/src/constants/messages/projects.ts +25 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +8 -0
- package/src/types/index.ts +2 -1
- package/src/types/project.ts +45 -0
- package/src/utils/formatter.ts +46 -0
- package/src/utils/fs.ts +32 -1
- package/src/utils/index.ts +2 -2
- package/tests/unit/utils/formatter.test.ts +91 -1
- package/tests/unit/utils/fs.test.ts +125 -2
package/docs/spec.md
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
- [5.15 命令别名管理](#515-命令别名管理)
|
|
30
30
|
- [5.16 Shell 自动补全](#516-clawt-completion-命令)
|
|
31
31
|
- [5.17 自动更新检查](#517-自动更新检查)
|
|
32
|
+
- [5.18 跨项目 Worktree 概览](#518-跨项目-worktree-概览)
|
|
32
33
|
- [6. 错误处理规范](#6-错误处理规范)
|
|
33
34
|
- [7. 非功能性需求](#7-非功能性需求)
|
|
34
35
|
- [7.1 性能](#71-性能)
|
|
@@ -188,6 +189,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
|
|
|
188
189
|
| `clawt status` | 显示项目全局状态总览(支持 `--json` 格式输出) | 5.14 |
|
|
189
190
|
| `clawt alias` | 管理命令别名(列出 / 设置 / 移除) | 5.15 |
|
|
190
191
|
| `clawt completion` | 为终端提供 shell 自动补全功能(bash/zsh) | 5.16 |
|
|
192
|
+
| `clawt projects` | 展示所有项目的 worktree 概览,或查看指定项目的 worktree 详情 | 5.17 |
|
|
191
193
|
|
|
192
194
|
**全局选项:**
|
|
193
195
|
|
|
@@ -1655,6 +1657,9 @@ clawt status [--json]
|
|
|
1655
1657
|
- `formatRelativeTime()` 是新增的格式化函数(在 `src/utils/formatter.ts`),将 ISO 8601 日期字符串转换为中文相对时间描述(如"3 天前"、"2 小时前"、"刚刚"),无效日期时返回 null
|
|
1656
1658
|
- `getCommitCountBehind()` 是新增的工具函数(在 `src/utils/git.ts`),通过 `git rev-list --count <branch>..HEAD` 计算落后提交数
|
|
1657
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`),递归计算目录占用的磁盘大小(字节),遇到无法访问的文件或目录时静默跳过
|
|
1658
1663
|
|
|
1659
1664
|
---
|
|
1660
1665
|
|
|
@@ -1916,6 +1921,142 @@ CLI 在每次命令执行完毕后,根据配置项 `autoUpdate` 决定是否
|
|
|
1916
1921
|
|
|
1917
1922
|
---
|
|
1918
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
|
+
|
|
1919
2060
|
## 6. 错误处理规范
|
|
1920
2061
|
|
|
1921
2062
|
### 6.1 通用错误处理
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
|
@@ -10,6 +10,7 @@ import { RESET_MESSAGES } from './reset.js';
|
|
|
10
10
|
import { CONFIG_CMD_MESSAGES, CONFIG_ALIAS_DISABLED_HINT } from './config.js';
|
|
11
11
|
import { STATUS_MESSAGES } from './status.js';
|
|
12
12
|
import { ALIAS_MESSAGES } from './alias.js';
|
|
13
|
+
import { PROJECTS_MESSAGES } from './projects.js';
|
|
13
14
|
import { COMPLETION_MESSAGES } from './completion.js';
|
|
14
15
|
import { UPDATE_MESSAGES, UPDATE_COMMANDS } from './update.js';
|
|
15
16
|
|
|
@@ -33,5 +34,6 @@ export const MESSAGES = {
|
|
|
33
34
|
...CONFIG_CMD_MESSAGES,
|
|
34
35
|
...STATUS_MESSAGES,
|
|
35
36
|
...ALIAS_MESSAGES,
|
|
37
|
+
...PROJECTS_MESSAGES,
|
|
36
38
|
...COMPLETION_MESSAGES,
|
|
37
39
|
} as const;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** projects 命令专属提示消息 */
|
|
2
|
+
export const PROJECTS_MESSAGES = {
|
|
3
|
+
/** projects 命令全局概览标题 */
|
|
4
|
+
PROJECTS_OVERVIEW_TITLE: '项目概览',
|
|
5
|
+
/** projects 命令指定项目详情标题 */
|
|
6
|
+
PROJECTS_DETAIL_TITLE: (projectName: string) => `项目详情: ${projectName}`,
|
|
7
|
+
/** 无项目提示 */
|
|
8
|
+
PROJECTS_NO_PROJECTS: '(暂无项目,worktrees 目录为空)',
|
|
9
|
+
/** 项目不存在提示 */
|
|
10
|
+
PROJECTS_NOT_FOUND: (name: string) => `项目 ${name} 不存在`,
|
|
11
|
+
/** worktree 数量标签 */
|
|
12
|
+
PROJECTS_WORKTREE_COUNT: (count: number) => `${count} 个 worktree`,
|
|
13
|
+
/** 最近活跃时间标签 */
|
|
14
|
+
PROJECTS_LAST_ACTIVE: (relativeTime: string) => `最近活跃: ${relativeTime}`,
|
|
15
|
+
/** 磁盘占用标签 */
|
|
16
|
+
PROJECTS_DISK_USAGE: (size: string) => `磁盘占用: ${size}`,
|
|
17
|
+
/** 总磁盘占用标签 */
|
|
18
|
+
PROJECTS_TOTAL_DISK_USAGE: (size: string) => `总占用: ${size}`,
|
|
19
|
+
/** projects 详情无 worktree */
|
|
20
|
+
PROJECTS_DETAIL_NO_WORKTREES: '(该项目下无 worktree)',
|
|
21
|
+
/** 路径标签 */
|
|
22
|
+
PROJECTS_PATH: (path: string) => `路径: ${path}`,
|
|
23
|
+
/** 最后修改时间标签 */
|
|
24
|
+
PROJECTS_LAST_MODIFIED: (relativeTime: string) => `最后修改: ${relativeTime}`,
|
|
25
|
+
} as const;
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { registerSyncCommand } from './commands/sync.js';
|
|
|
16
16
|
import { registerResetCommand } from './commands/reset.js';
|
|
17
17
|
import { registerStatusCommand } from './commands/status.js';
|
|
18
18
|
import { registerAliasCommand } from './commands/alias.js';
|
|
19
|
+
import { registerProjectsCommand } from './commands/projects.js';
|
|
19
20
|
import { registerCompletionCommand } from './commands/completion.js';
|
|
20
21
|
|
|
21
22
|
// 从 package.json 读取版本号,避免硬编码
|
|
@@ -53,6 +54,7 @@ registerSyncCommand(program);
|
|
|
53
54
|
registerResetCommand(program);
|
|
54
55
|
registerStatusCommand(program);
|
|
55
56
|
registerAliasCommand(program);
|
|
57
|
+
registerProjectsCommand(program);
|
|
56
58
|
registerCompletionCommand(program);
|
|
57
59
|
|
|
58
60
|
// 加载配置并应用命令别名
|
package/src/types/command.ts
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
|
|
2
|
-
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions } from './command.js';
|
|
2
|
+
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions } from './command.js';
|
|
3
3
|
export type { WorktreeInfo, WorktreeStatus } from './worktree.js';
|
|
4
4
|
export type { ClaudeCodeResult } from './claudeCode.js';
|
|
5
5
|
export type { TaskResult, TaskSummary } from './taskResult.js';
|
|
6
6
|
export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, SnapshotSummary, StatusResult } from './status.js';
|
|
7
7
|
export type { TaskFileEntry, ParseTaskFileOptions } from './taskFile.js';
|
|
8
|
+
export type { ProjectOverview, ProjectWorktreeDetail, ProjectDetailResult, ProjectsOverviewResult } from './project.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** 单个项目的概览信息 */
|
|
2
|
+
export interface ProjectOverview {
|
|
3
|
+
/** 项目名称 */
|
|
4
|
+
name: string;
|
|
5
|
+
/** worktree 数量 */
|
|
6
|
+
worktreeCount: number;
|
|
7
|
+
/** 最近活跃时间(ISO 8601 格式),无 worktree 时为目录修改时间 */
|
|
8
|
+
lastActiveTime: string;
|
|
9
|
+
/** 磁盘占用(字节) */
|
|
10
|
+
diskUsage: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 单个项目的 worktree 详情 */
|
|
14
|
+
export interface ProjectWorktreeDetail {
|
|
15
|
+
/** 分支名 */
|
|
16
|
+
branch: string;
|
|
17
|
+
/** worktree 路径 */
|
|
18
|
+
path: string;
|
|
19
|
+
/** 最后修改时间(ISO 8601 格式) */
|
|
20
|
+
lastModifiedTime: string;
|
|
21
|
+
/** 磁盘占用(字节) */
|
|
22
|
+
diskUsage: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** projects 命令展示指定项目时的完整结果 */
|
|
26
|
+
export interface ProjectDetailResult {
|
|
27
|
+
/** 项目名称 */
|
|
28
|
+
name: string;
|
|
29
|
+
/** 项目 worktree 根目录 */
|
|
30
|
+
projectDir: string;
|
|
31
|
+
/** worktree 详情列表(按最近活跃时间排序) */
|
|
32
|
+
worktrees: ProjectWorktreeDetail[];
|
|
33
|
+
/** 总磁盘占用(字节) */
|
|
34
|
+
totalDiskUsage: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** projects 命令展示所有项目概览时的完整结果 */
|
|
38
|
+
export interface ProjectsOverviewResult {
|
|
39
|
+
/** 所有项目概览列表(按最近活跃时间排序) */
|
|
40
|
+
projects: ProjectOverview[];
|
|
41
|
+
/** 项目总数 */
|
|
42
|
+
totalProjects: number;
|
|
43
|
+
/** 总磁盘占用(字节) */
|
|
44
|
+
totalDiskUsage: number;
|
|
45
|
+
}
|
package/src/utils/formatter.ts
CHANGED
|
@@ -184,3 +184,49 @@ export function formatRelativeTime(isoDateString: string): string | null {
|
|
|
184
184
|
const years = Math.floor(diffDays / 365);
|
|
185
185
|
return `${years} 年前`;
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 将字节数格式化为带单位的磁盘大小字符串
|
|
190
|
+
* @param {number} bytes - 字节数
|
|
191
|
+
* @returns {string} 格式化后的大小字符串,如 "1.5 GB"、"256.0 MB"、"10.2 KB"
|
|
192
|
+
*/
|
|
193
|
+
export function formatDiskSize(bytes: number): string {
|
|
194
|
+
/** 1 KB 的字节数 */
|
|
195
|
+
const KB = 1024;
|
|
196
|
+
/** 1 MB 的字节数 */
|
|
197
|
+
const MB = KB * 1024;
|
|
198
|
+
/** 1 GB 的字节数 */
|
|
199
|
+
const GB = MB * 1024;
|
|
200
|
+
|
|
201
|
+
if (bytes >= GB) {
|
|
202
|
+
return `${(bytes / GB).toFixed(1)} GB`;
|
|
203
|
+
}
|
|
204
|
+
if (bytes >= MB) {
|
|
205
|
+
return `${(bytes / MB).toFixed(1)} MB`;
|
|
206
|
+
}
|
|
207
|
+
if (bytes >= KB) {
|
|
208
|
+
return `${(bytes / KB).toFixed(1)} KB`;
|
|
209
|
+
}
|
|
210
|
+
return `${bytes} B`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 将 Date 对象格式化为本机时区的 ISO 8601 字符串
|
|
215
|
+
* 输出格式: YYYY-MM-DDTHH:mm:ss.sss+HH:MM
|
|
216
|
+
* @param {Date} date - 日期对象
|
|
217
|
+
* @returns {string} 本机时区的 ISO 8601 格式字符串
|
|
218
|
+
*/
|
|
219
|
+
export function formatLocalISOString(date: Date): string {
|
|
220
|
+
const tzOffsetMs = date.getTimezoneOffset() * 60 * 1000;
|
|
221
|
+
const localDate = new Date(date.getTime() - tzOffsetMs);
|
|
222
|
+
const iso = localDate.toISOString().slice(0, -1);
|
|
223
|
+
|
|
224
|
+
// 拼接时区偏移量
|
|
225
|
+
const totalMinutes = -date.getTimezoneOffset();
|
|
226
|
+
const sign = totalMinutes >= 0 ? '+' : '-';
|
|
227
|
+
const absMinutes = Math.abs(totalMinutes);
|
|
228
|
+
const hours = String(Math.floor(absMinutes / 60)).padStart(2, '0');
|
|
229
|
+
const minutes = String(absMinutes % 60).padStart(2, '0');
|
|
230
|
+
|
|
231
|
+
return `${iso}${sign}${hours}:${minutes}`;
|
|
232
|
+
}
|