clawt 2.9.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agent-memory/docs-sync-updater/MEMORY.md +6 -5
- package/README.md +22 -0
- package/dist/index.js +218 -0
- package/docs/spec.md +135 -0
- package/package.json +1 -1
- package/src/commands/status.ts +327 -0
- package/src/constants/messages.ts +22 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +6 -0
- package/src/types/index.ts +2 -1
- package/src/types/status.ts +49 -0
- package/src/utils/git.ts +16 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/validate-snapshot.ts +17 -0
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
### docs/spec.md
|
|
6
6
|
- 完整的软件规格说明,包含 7 大章节
|
|
7
|
-
- 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.
|
|
7
|
+
- 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.14)
|
|
8
8
|
- run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
|
|
9
9
|
- merge 命令对应 `5.6 合并验证过的分支`,-b 可选,支持模糊匹配(与 resume/validate 共享匹配逻辑),流程按步骤编号描述
|
|
10
10
|
- config 命令对应 `5.10 查看和管理全局配置`,包含查看配置和 config reset 子命令两部分(使用 `####` 子标题区分)
|
|
11
11
|
- resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,支持模糊匹配和交互式分支选择(-b 可选)
|
|
12
12
|
- validate 命令对应 `5.4 在主 Worktree 验证其他分支`,-b 可选,支持模糊匹配(与 resume 共享匹配逻辑)
|
|
13
13
|
- sync 命令对应 `5.12 将主分支代码同步到目标 Worktree`,-b 可选,支持模糊匹配(与 resume/validate/merge 共享匹配逻辑)
|
|
14
|
+
- status 命令对应 `5.14 项目全局状态总览`,支持 `--json` 格式输出,展示主 worktree 状态、各 worktree 详细状态、未清理快照
|
|
14
15
|
- 配置项说明在 `5.7 默认配置文件` 章节的表格中
|
|
15
16
|
- 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
|
|
16
17
|
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
- cleanupWorktrees 是 merge 和 run 共用的公共清理函数(在 src/utils/worktree.ts)
|
|
37
38
|
- `launchInteractiveClaude` 是 run(交互式模式)和 resume 共用的公共函数(在 src/utils/claude.ts)
|
|
38
39
|
- killAllChildProcesses 是 run 专用的子进程终止函数(在 src/utils/shell.ts)
|
|
39
|
-
- validate 快照管理函数在 `src/utils/validate-snapshot.ts`,被 validate、merge 和
|
|
40
|
+
- validate 快照管理函数在 `src/utils/validate-snapshot.ts`,被 validate、merge、remove 和 status 四个命令使用
|
|
40
41
|
- `confirmDestructiveAction` 在 `src/utils/formatter.ts`,被 reset、validate --clean 和 config reset 使用
|
|
41
42
|
- sanitizeBranchName 清理后为空串时抛出 BRANCH_NAME_EMPTY 错误
|
|
42
43
|
|
|
@@ -61,9 +62,9 @@ run 命令有两种模式(自 claudeCodeCommand 特性后):
|
|
|
61
62
|
- 不传 `--tasks`:交互式界面模式(单 worktree + `launchInteractiveClaude` + spawnSync)
|
|
62
63
|
- 传 `--tasks`:并行任务模式(多 worktree + `executeClaudeTask` + spawnProcess)
|
|
63
64
|
|
|
64
|
-
## 命令清单(
|
|
65
|
+
## 命令清单(11 个)
|
|
65
66
|
|
|
66
|
-
`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`、`sync`、`reset`
|
|
67
|
+
`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`、`sync`、`reset`、`status`
|
|
67
68
|
|
|
68
69
|
Notes:
|
|
69
70
|
- resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
|
|
@@ -71,7 +72,7 @@ Notes:
|
|
|
71
72
|
- reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
|
|
72
73
|
- `resolveTargetWorktree()` 是 resume、validate、merge 和 sync 共用的分支匹配函数(在 src/utils/worktree-matcher.ts)
|
|
73
74
|
- `WorktreeResolveMessages` 接口实现命令间消息解耦,每个命令传入各自的提示文案
|
|
74
|
-
- resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`,merge 的消息常量在 `MESSAGES.MERGE_*`,sync 的消息常量在 `MESSAGES.SYNC_*`
|
|
75
|
+
- resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`,merge 的消息常量在 `MESSAGES.MERGE_*`,sync 的消息常量在 `MESSAGES.SYNC_*`,status 的消息常量在 `MESSAGES.STATUS_*`
|
|
75
76
|
- resume、validate、merge 和 sync 的 `-b` 参数均为可选,匹配策略一致:精确→模糊(子串,大小写不敏感)→交互选择
|
|
76
77
|
- validate 的交互式选择和 resume 使用同一个 `promptSelectBranch()`(Enquirer.Select)
|
|
77
78
|
|
package/README.md
CHANGED
|
@@ -273,6 +273,28 @@ clawt list
|
|
|
273
273
|
clawt list --json
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
+
### `clawt status` — 显示项目全局状态总览
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
clawt status [--json]
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
| 参数 | 必填 | 说明 |
|
|
283
|
+
| ---- | ---- | ---- |
|
|
284
|
+
| `--json` | 否 | 以 JSON 格式输出完整状态数据 |
|
|
285
|
+
|
|
286
|
+
显示项目全局状态总览,包括:主 worktree 当前分支及干净状态、所有 worktree 的变更状态(已提交/未提交/合并冲突/无变更)、与主分支的差异(领先/落后提交数、行数变更)、未清理的 validate 快照检测(标识孤立快照)。
|
|
287
|
+
|
|
288
|
+
文本模式下输出分为三个区块:主 Worktree 状态、Worktree 列表(含变更状态标签和差异统计)、未清理的 Validate 快照。指定 `--json` 时以 JSON 格式输出完整状态数据,便于脚本解析。
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
# 文本格式输出(默认)
|
|
292
|
+
clawt status
|
|
293
|
+
|
|
294
|
+
# JSON 格式输出
|
|
295
|
+
clawt status --json
|
|
296
|
+
```
|
|
297
|
+
|
|
276
298
|
### `clawt reset` — 重置主 worktree 工作区和暂存区
|
|
277
299
|
|
|
278
300
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -167,6 +167,28 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
167
167
|
SYNC_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u540C\u6B65\u7684\u5206\u652F",
|
|
168
168
|
/** sync 模糊匹配到多个结果提示 */
|
|
169
169
|
SYNC_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
|
|
170
|
+
/** status 命令标题 */
|
|
171
|
+
STATUS_TITLE: (projectName) => `\u9879\u76EE\u72B6\u6001\u603B\u89C8: ${projectName}`,
|
|
172
|
+
/** status 主 worktree 区块标题 */
|
|
173
|
+
STATUS_MAIN_SECTION: "\u4E3B Worktree",
|
|
174
|
+
/** status worktrees 区块标题 */
|
|
175
|
+
STATUS_WORKTREES_SECTION: "Worktree \u5217\u8868",
|
|
176
|
+
/** status 快照区块标题 */
|
|
177
|
+
STATUS_SNAPSHOTS_SECTION: "\u672A\u6E05\u7406\u7684 Validate \u5FEB\u7167",
|
|
178
|
+
/** status 无 worktree */
|
|
179
|
+
STATUS_NO_WORKTREES: "(\u65E0\u6D3B\u8DC3 worktree)",
|
|
180
|
+
/** status 无未清理快照 */
|
|
181
|
+
STATUS_NO_SNAPSHOTS: "(\u65E0\u672A\u6E05\u7406\u7684\u5FEB\u7167)",
|
|
182
|
+
/** status 变更状态:已提交 */
|
|
183
|
+
STATUS_CHANGE_COMMITTED: "\u5DF2\u63D0\u4EA4",
|
|
184
|
+
/** status 变更状态:未提交修改 */
|
|
185
|
+
STATUS_CHANGE_UNCOMMITTED: "\u672A\u63D0\u4EA4\u4FEE\u6539",
|
|
186
|
+
/** status 变更状态:合并冲突 */
|
|
187
|
+
STATUS_CHANGE_CONFLICT: "\u5408\u5E76\u51B2\u7A81",
|
|
188
|
+
/** status 变更状态:无变更 */
|
|
189
|
+
STATUS_CHANGE_CLEAN: "\u65E0\u53D8\u66F4",
|
|
190
|
+
/** status 快照对应 worktree 已不存在 */
|
|
191
|
+
STATUS_SNAPSHOT_ORPHANED: "(\u5BF9\u5E94 worktree \u5DF2\u4E0D\u5B58\u5728)",
|
|
170
192
|
/** merge 后 pull 冲突 */
|
|
171
193
|
PULL_CONFLICT: "\u81EA\u52A8 pull \u65F6\u53D1\u751F\u51B2\u7A81\uFF0Cmerge \u5DF2\u5B8C\u6210\u4F46\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25\n \u8BF7\u624B\u52A8\u89E3\u51B3\u51B2\u7A81\uFF1A\n \u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add . && git commit\n \u7136\u540E\u6267\u884C git push \u63A8\u9001\u5230\u8FDC\u7A0B",
|
|
172
194
|
/** push 失败 */
|
|
@@ -420,6 +442,14 @@ function getCommitCountAhead(branchName, cwd) {
|
|
|
420
442
|
const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
|
|
421
443
|
return parseInt(output, 10) || 0;
|
|
422
444
|
}
|
|
445
|
+
function getCommitCountBehind(branchName, cwd) {
|
|
446
|
+
try {
|
|
447
|
+
const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
|
|
448
|
+
return parseInt(output, 10) || 0;
|
|
449
|
+
} catch {
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
423
453
|
function parseShortStat(output) {
|
|
424
454
|
let insertions = 0;
|
|
425
455
|
let deletions = 0;
|
|
@@ -798,6 +828,14 @@ function removeSnapshot(projectName, branchName) {
|
|
|
798
828
|
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
|
|
799
829
|
}
|
|
800
830
|
}
|
|
831
|
+
function getProjectSnapshotBranches(projectName) {
|
|
832
|
+
const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
833
|
+
if (!existsSync5(projectDir)) {
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
const files = readdirSync3(projectDir);
|
|
837
|
+
return files.filter((f) => f.endsWith(".tree")).map((f) => f.replace(/\.tree$/, ""));
|
|
838
|
+
}
|
|
801
839
|
function removeProjectSnapshots(projectName) {
|
|
802
840
|
const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
803
841
|
if (!existsSync5(projectDir)) {
|
|
@@ -1632,6 +1670,185 @@ async function handleReset() {
|
|
|
1632
1670
|
}
|
|
1633
1671
|
}
|
|
1634
1672
|
|
|
1673
|
+
// src/commands/status.ts
|
|
1674
|
+
import chalk5 from "chalk";
|
|
1675
|
+
function registerStatusCommand(program2) {
|
|
1676
|
+
program2.command("status").description("\u663E\u793A\u9879\u76EE\u5168\u5C40\u72B6\u6001\u603B\u89C8").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((options) => {
|
|
1677
|
+
handleStatus(options);
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
function handleStatus(options) {
|
|
1681
|
+
validateMainWorktree();
|
|
1682
|
+
const statusResult = collectStatus();
|
|
1683
|
+
logger.info(`status \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${statusResult.main.projectName}\uFF0C\u5171 ${statusResult.totalWorktrees} \u4E2A worktree`);
|
|
1684
|
+
if (options.json) {
|
|
1685
|
+
printStatusAsJson(statusResult);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
printStatusAsText(statusResult);
|
|
1689
|
+
}
|
|
1690
|
+
function collectStatus() {
|
|
1691
|
+
const projectName = getProjectName();
|
|
1692
|
+
const currentBranch = getCurrentBranch();
|
|
1693
|
+
const isClean = isWorkingDirClean();
|
|
1694
|
+
const main = {
|
|
1695
|
+
branch: currentBranch,
|
|
1696
|
+
isClean,
|
|
1697
|
+
projectName
|
|
1698
|
+
};
|
|
1699
|
+
const worktrees = getProjectWorktrees();
|
|
1700
|
+
const worktreeStatuses = worktrees.map((wt) => collectWorktreeDetailedStatus(wt, projectName));
|
|
1701
|
+
const snapshots = collectSnapshots(projectName, worktrees);
|
|
1702
|
+
return {
|
|
1703
|
+
main,
|
|
1704
|
+
worktrees: worktreeStatuses,
|
|
1705
|
+
snapshots,
|
|
1706
|
+
totalWorktrees: worktrees.length
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function collectWorktreeDetailedStatus(worktree, projectName) {
|
|
1710
|
+
const changeStatus = detectChangeStatus(worktree);
|
|
1711
|
+
const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
|
|
1712
|
+
const { insertions, deletions } = countDiffStat(worktree.path);
|
|
1713
|
+
return {
|
|
1714
|
+
path: worktree.path,
|
|
1715
|
+
branch: worktree.branch,
|
|
1716
|
+
changeStatus,
|
|
1717
|
+
commitsAhead,
|
|
1718
|
+
commitsBehind,
|
|
1719
|
+
hasSnapshot: hasSnapshot(projectName, worktree.branch),
|
|
1720
|
+
insertions,
|
|
1721
|
+
deletions
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function detectChangeStatus(worktree) {
|
|
1725
|
+
try {
|
|
1726
|
+
if (hasMergeConflict(worktree.path)) {
|
|
1727
|
+
return "conflict";
|
|
1728
|
+
}
|
|
1729
|
+
if (!isWorkingDirClean(worktree.path)) {
|
|
1730
|
+
return "uncommitted";
|
|
1731
|
+
}
|
|
1732
|
+
if (hasLocalCommits(worktree.branch)) {
|
|
1733
|
+
return "committed";
|
|
1734
|
+
}
|
|
1735
|
+
return "clean";
|
|
1736
|
+
} catch {
|
|
1737
|
+
return "clean";
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
function countCommitDivergence(branchName) {
|
|
1741
|
+
try {
|
|
1742
|
+
return {
|
|
1743
|
+
commitsAhead: getCommitCountAhead(branchName),
|
|
1744
|
+
commitsBehind: getCommitCountBehind(branchName)
|
|
1745
|
+
};
|
|
1746
|
+
} catch {
|
|
1747
|
+
return { commitsAhead: 0, commitsBehind: 0 };
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
function countDiffStat(worktreePath) {
|
|
1751
|
+
try {
|
|
1752
|
+
return getDiffStat(worktreePath);
|
|
1753
|
+
} catch {
|
|
1754
|
+
return { insertions: 0, deletions: 0 };
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
function collectSnapshots(projectName, worktrees) {
|
|
1758
|
+
const snapshotBranches = getProjectSnapshotBranches(projectName);
|
|
1759
|
+
const worktreeBranchSet = new Set(worktrees.map((wt) => wt.branch));
|
|
1760
|
+
return snapshotBranches.map((branch) => ({
|
|
1761
|
+
branch,
|
|
1762
|
+
worktreeExists: worktreeBranchSet.has(branch)
|
|
1763
|
+
}));
|
|
1764
|
+
}
|
|
1765
|
+
function printStatusAsJson(result) {
|
|
1766
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1767
|
+
}
|
|
1768
|
+
function printStatusAsText(result) {
|
|
1769
|
+
printDoubleSeparator();
|
|
1770
|
+
printInfo(` ${chalk5.bold.cyan(MESSAGES.STATUS_TITLE(result.main.projectName))}`);
|
|
1771
|
+
printDoubleSeparator();
|
|
1772
|
+
printInfo("");
|
|
1773
|
+
printMainSection(result.main);
|
|
1774
|
+
printSeparator();
|
|
1775
|
+
printInfo("");
|
|
1776
|
+
printWorktreesSection(result.worktrees, result.totalWorktrees);
|
|
1777
|
+
printSeparator();
|
|
1778
|
+
printInfo("");
|
|
1779
|
+
printSnapshotsSection(result.snapshots);
|
|
1780
|
+
printDoubleSeparator();
|
|
1781
|
+
}
|
|
1782
|
+
function printMainSection(main) {
|
|
1783
|
+
printInfo(` ${chalk5.bold("\u25C6")} ${chalk5.bold(MESSAGES.STATUS_MAIN_SECTION)}`);
|
|
1784
|
+
printInfo(` \u5206\u652F: ${chalk5.bold(main.branch)}`);
|
|
1785
|
+
if (main.isClean) {
|
|
1786
|
+
printInfo(` \u72B6\u6001: ${chalk5.green("\u2713 \u5E72\u51C0")}`);
|
|
1787
|
+
} else {
|
|
1788
|
+
printInfo(` \u72B6\u6001: ${chalk5.yellow("\u2717 \u6709\u672A\u63D0\u4EA4\u4FEE\u6539")}`);
|
|
1789
|
+
}
|
|
1790
|
+
printInfo("");
|
|
1791
|
+
}
|
|
1792
|
+
function printWorktreesSection(worktrees, total) {
|
|
1793
|
+
printInfo(` ${chalk5.bold("\u25C6")} ${chalk5.bold(MESSAGES.STATUS_WORKTREES_SECTION)} (${total} \u4E2A)`);
|
|
1794
|
+
printInfo("");
|
|
1795
|
+
if (worktrees.length === 0) {
|
|
1796
|
+
printInfo(` ${MESSAGES.STATUS_NO_WORKTREES}`);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
for (const wt of worktrees) {
|
|
1800
|
+
printWorktreeItem(wt);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function printWorktreeItem(wt) {
|
|
1804
|
+
const statusLabel = formatChangeStatusLabel(wt.changeStatus);
|
|
1805
|
+
printInfo(` ${chalk5.bold("\u25CF")} ${chalk5.bold(wt.branch)} [${statusLabel}]`);
|
|
1806
|
+
const parts = [];
|
|
1807
|
+
if (wt.insertions > 0 || wt.deletions > 0) {
|
|
1808
|
+
parts.push(`${chalk5.green(`+${wt.insertions}`)} ${chalk5.red(`-${wt.deletions}`)}`);
|
|
1809
|
+
}
|
|
1810
|
+
if (wt.commitsAhead > 0) {
|
|
1811
|
+
parts.push(chalk5.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`));
|
|
1812
|
+
}
|
|
1813
|
+
if (wt.commitsBehind > 0) {
|
|
1814
|
+
parts.push(chalk5.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`));
|
|
1815
|
+
} else {
|
|
1816
|
+
parts.push(chalk5.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65"));
|
|
1817
|
+
}
|
|
1818
|
+
printInfo(` ${parts.join(" ")}`);
|
|
1819
|
+
if (wt.hasSnapshot) {
|
|
1820
|
+
printInfo(` ${chalk5.blue("\u6709 validate \u5FEB\u7167")}`);
|
|
1821
|
+
}
|
|
1822
|
+
printInfo("");
|
|
1823
|
+
}
|
|
1824
|
+
function formatChangeStatusLabel(status) {
|
|
1825
|
+
switch (status) {
|
|
1826
|
+
case "committed":
|
|
1827
|
+
return chalk5.green(MESSAGES.STATUS_CHANGE_COMMITTED);
|
|
1828
|
+
case "uncommitted":
|
|
1829
|
+
return chalk5.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
|
|
1830
|
+
case "conflict":
|
|
1831
|
+
return chalk5.red(MESSAGES.STATUS_CHANGE_CONFLICT);
|
|
1832
|
+
case "clean":
|
|
1833
|
+
return chalk5.gray(MESSAGES.STATUS_CHANGE_CLEAN);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
function printSnapshotsSection(snapshots) {
|
|
1837
|
+
printInfo(` ${chalk5.bold("\u25C6")} ${chalk5.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.length} \u4E2A)`);
|
|
1838
|
+
printInfo("");
|
|
1839
|
+
if (snapshots.length === 0) {
|
|
1840
|
+
printInfo(` ${MESSAGES.STATUS_NO_SNAPSHOTS}`);
|
|
1841
|
+
printInfo("");
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
for (const snap of snapshots) {
|
|
1845
|
+
const orphanLabel = snap.worktreeExists ? "" : ` ${chalk5.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED)}`;
|
|
1846
|
+
const icon = snap.worktreeExists ? chalk5.blue("\u25CF") : chalk5.yellow("\u26A0");
|
|
1847
|
+
printInfo(` ${icon} ${snap.branch}${orphanLabel}`);
|
|
1848
|
+
}
|
|
1849
|
+
printInfo("");
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1635
1852
|
// src/index.ts
|
|
1636
1853
|
var require2 = createRequire(import.meta.url);
|
|
1637
1854
|
var { version } = require2("../package.json");
|
|
@@ -1653,6 +1870,7 @@ registerMergeCommand(program);
|
|
|
1653
1870
|
registerConfigCommand(program);
|
|
1654
1871
|
registerSyncCommand(program);
|
|
1655
1872
|
registerResetCommand(program);
|
|
1873
|
+
registerStatusCommand(program);
|
|
1656
1874
|
process.on("uncaughtException", (error) => {
|
|
1657
1875
|
if (error instanceof ClawtError) {
|
|
1658
1876
|
printError(error.message);
|
package/docs/spec.md
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- [5.11 在已有 Worktree 中恢复会话](#511-在已有-worktree-中恢复会话)
|
|
26
26
|
- [5.12 将主分支代码同步到目标 Worktree](#512-将主分支代码同步到目标-worktree)
|
|
27
27
|
- [5.13 重置主 Worktree 工作区和暂存区](#513-重置主-worktree-工作区和暂存区)
|
|
28
|
+
- [5.14 项目全局状态总览](#514-项目全局状态总览)
|
|
28
29
|
- [6. 错误处理规范](#6-错误处理规范)
|
|
29
30
|
- [7. 非功能性需求](#7-非功能性需求)
|
|
30
31
|
- [7.1 性能](#71-性能)
|
|
@@ -177,6 +178,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
|
|
|
177
178
|
| `clawt resume` | 在已有 worktree 中恢复 Claude Code 交互式会话 | 5.11 |
|
|
178
179
|
| `clawt sync` | 将主分支最新代码同步到目标 worktree | 5.12 |
|
|
179
180
|
| `clawt reset` | 重置主 worktree 工作区和暂存区 | 5.13 |
|
|
181
|
+
| `clawt status` | 显示项目全局状态总览(支持 `--json` 格式输出) | 5.14 |
|
|
180
182
|
|
|
181
183
|
**全局选项:**
|
|
182
184
|
|
|
@@ -1122,6 +1124,139 @@ clawt reset
|
|
|
1122
1124
|
|
|
1123
1125
|
---
|
|
1124
1126
|
|
|
1127
|
+
### 5.14 项目全局状态总览
|
|
1128
|
+
|
|
1129
|
+
**命令:**
|
|
1130
|
+
|
|
1131
|
+
```bash
|
|
1132
|
+
clawt status [--json]
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
**参数:**
|
|
1136
|
+
|
|
1137
|
+
| 参数 | 必填 | 说明 |
|
|
1138
|
+
| -------- | ---- | ---------------------------------------- |
|
|
1139
|
+
| `--json` | 否 | 以 JSON 格式输出完整状态数据 |
|
|
1140
|
+
|
|
1141
|
+
**使用场景:**
|
|
1142
|
+
|
|
1143
|
+
在管理多个 worktree 时,快速了解项目全局状态:主 worktree 当前分支及干净状态、所有 worktree 的变更情况和与主分支的同步状态、未清理的 validate 快照。
|
|
1144
|
+
|
|
1145
|
+
**运行流程:**
|
|
1146
|
+
|
|
1147
|
+
1. **主 worktree 校验** (2.1)
|
|
1148
|
+
2. **收集主 worktree 状态**:
|
|
1149
|
+
- 获取当前分支名(`getCurrentBranch()`)
|
|
1150
|
+
- 检测工作区是否干净(`isWorkingDirClean()`)
|
|
1151
|
+
- 获取项目名(`getProjectName()`)
|
|
1152
|
+
3. **收集各 worktree 详细状态**:
|
|
1153
|
+
- 获取项目所有 worktree(`getProjectWorktrees()`)
|
|
1154
|
+
- 对每个 worktree 收集以下信息:
|
|
1155
|
+
- **变更状态**(优先级:合并冲突 > 未提交修改 > 已提交 > 无变更)
|
|
1156
|
+
- **行数差异**(新增/删除行数,通过 `getDiffStat()` 获取)
|
|
1157
|
+
- **提交差异**(相对于主分支的领先提交数 `getCommitCountAhead()` 和落后提交数 `getCommitCountBehind()`)
|
|
1158
|
+
- **快照状态**(是否存在 validate 快照)
|
|
1159
|
+
4. **收集未清理的 validate 快照**:
|
|
1160
|
+
- 通过 `getProjectSnapshotBranches()` 扫描快照目录下的 `.tree` 文件获取所有存在快照的分支名
|
|
1161
|
+
- 对比现有 worktree 分支列表,标识孤立快照(对应 worktree 已不存在的快照)
|
|
1162
|
+
5. **输出状态信息**:
|
|
1163
|
+
- 指定 `--json` → 以 JSON 格式输出完整状态数据(`JSON.stringify`)
|
|
1164
|
+
- 未指定 → 以文本格式输出
|
|
1165
|
+
|
|
1166
|
+
**文本输出格式(默认):**
|
|
1167
|
+
|
|
1168
|
+
输出分为三个区块:主 Worktree、Worktree 列表、未清理的 Validate 快照。
|
|
1169
|
+
|
|
1170
|
+
```
|
|
1171
|
+
════════════════════════════════════════
|
|
1172
|
+
项目状态总览: main-project
|
|
1173
|
+
════════════════════════════════════════
|
|
1174
|
+
|
|
1175
|
+
◆ 主 Worktree
|
|
1176
|
+
分支: main
|
|
1177
|
+
状态: ✓ 干净
|
|
1178
|
+
|
|
1179
|
+
────────────────────────────────────────
|
|
1180
|
+
|
|
1181
|
+
◆ Worktree 列表 (2 个)
|
|
1182
|
+
|
|
1183
|
+
● feature-login [已提交]
|
|
1184
|
+
+120 -30 3 个本地提交 与主分支同步
|
|
1185
|
+
有 validate 快照
|
|
1186
|
+
|
|
1187
|
+
● feature-signup [未提交修改]
|
|
1188
|
+
+45 -10 1 个本地提交 落后主分支 2 个提交
|
|
1189
|
+
|
|
1190
|
+
────────────────────────────────────────
|
|
1191
|
+
|
|
1192
|
+
◆ 未清理的 Validate 快照 (1 个)
|
|
1193
|
+
|
|
1194
|
+
⚠ old-feature (对应 worktree 已不存在)
|
|
1195
|
+
|
|
1196
|
+
════════════════════════════════════════
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
**变更状态标签:**
|
|
1200
|
+
|
|
1201
|
+
| 状态 | 标签 | 颜色 | 说明 |
|
|
1202
|
+
| ----------- | -------------- | ------ | ----------------------------- |
|
|
1203
|
+
| `committed` | 已提交 | 绿色 | 有已提交内容,工作区干净 |
|
|
1204
|
+
| `uncommitted` | 未提交修改 | 黄色 | 有未提交的修改 |
|
|
1205
|
+
| `conflict` | 合并冲突 | 红色 | 存在合并冲突 |
|
|
1206
|
+
| `clean` | 无变更 | 灰色 | 工作区干净且无本地提交 |
|
|
1207
|
+
|
|
1208
|
+
**差异统计行展示规则:**
|
|
1209
|
+
|
|
1210
|
+
- 行数变更(`+N -N`)仅在有变更时展示
|
|
1211
|
+
- 本地提交数(`N 个本地提交`)仅在有提交时展示
|
|
1212
|
+
- 与主分支同步状态始终展示(落后时显示黄色,同步时显示绿色)
|
|
1213
|
+
|
|
1214
|
+
**快照区块:**
|
|
1215
|
+
|
|
1216
|
+
- 每个快照显示对应的分支名
|
|
1217
|
+
- 如果对应的 worktree 仍存在,显示蓝色圆点图标
|
|
1218
|
+
- 如果对应的 worktree 已不存在(孤立快照),显示黄色警告图标并标注 `(对应 worktree 已不存在)`
|
|
1219
|
+
|
|
1220
|
+
**JSON 输出格式(`--json`):**
|
|
1221
|
+
|
|
1222
|
+
```json
|
|
1223
|
+
{
|
|
1224
|
+
"main": {
|
|
1225
|
+
"branch": "main",
|
|
1226
|
+
"isClean": true,
|
|
1227
|
+
"projectName": "main-project"
|
|
1228
|
+
},
|
|
1229
|
+
"worktrees": [
|
|
1230
|
+
{
|
|
1231
|
+
"path": "~/.clawt/worktrees/main-project/feature-login",
|
|
1232
|
+
"branch": "feature-login",
|
|
1233
|
+
"changeStatus": "committed",
|
|
1234
|
+
"commitsAhead": 3,
|
|
1235
|
+
"commitsBehind": 0,
|
|
1236
|
+
"hasSnapshot": true,
|
|
1237
|
+
"insertions": 120,
|
|
1238
|
+
"deletions": 30
|
|
1239
|
+
}
|
|
1240
|
+
],
|
|
1241
|
+
"snapshots": [
|
|
1242
|
+
{
|
|
1243
|
+
"branch": "old-feature",
|
|
1244
|
+
"worktreeExists": false
|
|
1245
|
+
}
|
|
1246
|
+
],
|
|
1247
|
+
"totalWorktrees": 1
|
|
1248
|
+
}
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
**实现要点:**
|
|
1252
|
+
|
|
1253
|
+
- 类型定义在 `src/types/status.ts`:`WorktreeDetailedStatus`、`MainWorktreeStatus`、`SnapshotInfo`、`StatusResult`
|
|
1254
|
+
- 消息常量在 `MESSAGES.STATUS_*` 系列
|
|
1255
|
+
- `getCommitCountBehind()` 是新增的工具函数(在 `src/utils/git.ts`),通过 `git rev-list --count <branch>..HEAD` 计算落后提交数
|
|
1256
|
+
- `getProjectSnapshotBranches()` 是新增的工具函数(在 `src/utils/validate-snapshot.ts`),通过扫描快照目录下的 `.tree` 文件提取分支名列表
|
|
1257
|
+
|
|
1258
|
+
---
|
|
1259
|
+
|
|
1125
1260
|
## 6. 错误处理规范
|
|
1126
1261
|
|
|
1127
1262
|
### 6.1 通用错误处理
|
package/package.json
CHANGED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { MESSAGES } from '../constants/index.js';
|
|
4
|
+
import { logger } from '../logger/index.js';
|
|
5
|
+
import type { StatusOptions, WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, StatusResult, WorktreeInfo } from '../types/index.js';
|
|
6
|
+
import {
|
|
7
|
+
validateMainWorktree,
|
|
8
|
+
getProjectName,
|
|
9
|
+
getCurrentBranch,
|
|
10
|
+
isWorkingDirClean,
|
|
11
|
+
getProjectWorktrees,
|
|
12
|
+
getCommitCountAhead,
|
|
13
|
+
getCommitCountBehind,
|
|
14
|
+
getDiffStat,
|
|
15
|
+
hasMergeConflict,
|
|
16
|
+
hasLocalCommits,
|
|
17
|
+
hasSnapshot,
|
|
18
|
+
getProjectSnapshotBranches,
|
|
19
|
+
printInfo,
|
|
20
|
+
printDoubleSeparator,
|
|
21
|
+
printSeparator,
|
|
22
|
+
} from '../utils/index.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 注册 status 命令:显示项目全局状态总览
|
|
26
|
+
* @param {Command} program - Commander 实例
|
|
27
|
+
*/
|
|
28
|
+
export function registerStatusCommand(program: Command): void {
|
|
29
|
+
program
|
|
30
|
+
.command('status')
|
|
31
|
+
.description('显示项目全局状态总览')
|
|
32
|
+
.option('--json', '以 JSON 格式输出')
|
|
33
|
+
.action((options: StatusOptions) => {
|
|
34
|
+
handleStatus(options);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 执行 status 命令的核心逻辑
|
|
40
|
+
* @param {StatusOptions} options - 命令选项
|
|
41
|
+
*/
|
|
42
|
+
function handleStatus(options: StatusOptions): void {
|
|
43
|
+
validateMainWorktree();
|
|
44
|
+
|
|
45
|
+
const statusResult = collectStatus();
|
|
46
|
+
|
|
47
|
+
logger.info(`status 命令执行,项目: ${statusResult.main.projectName},共 ${statusResult.totalWorktrees} 个 worktree`);
|
|
48
|
+
|
|
49
|
+
if (options.json) {
|
|
50
|
+
printStatusAsJson(statusResult);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
printStatusAsText(statusResult);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 收集项目全局状态信息
|
|
59
|
+
* @returns {StatusResult} 完整的状态数据
|
|
60
|
+
*/
|
|
61
|
+
function collectStatus(): StatusResult {
|
|
62
|
+
const projectName = getProjectName();
|
|
63
|
+
const currentBranch = getCurrentBranch();
|
|
64
|
+
const isClean = isWorkingDirClean();
|
|
65
|
+
|
|
66
|
+
// 主 worktree 状态
|
|
67
|
+
const main: MainWorktreeStatus = {
|
|
68
|
+
branch: currentBranch,
|
|
69
|
+
isClean,
|
|
70
|
+
projectName,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// 各 worktree 详细状态
|
|
74
|
+
const worktrees = getProjectWorktrees();
|
|
75
|
+
const worktreeStatuses = worktrees.map((wt) => collectWorktreeDetailedStatus(wt, projectName));
|
|
76
|
+
|
|
77
|
+
// 未清理的 validate 快照
|
|
78
|
+
const snapshots = collectSnapshots(projectName, worktrees);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
main,
|
|
82
|
+
worktrees: worktreeStatuses,
|
|
83
|
+
snapshots,
|
|
84
|
+
totalWorktrees: worktrees.length,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 收集单个 worktree 的详细状态
|
|
90
|
+
* 变更状态判断优先级:冲突 > 未提交 > 已提交 > 干净
|
|
91
|
+
* @param {WorktreeInfo} worktree - worktree 信息
|
|
92
|
+
* @param {string} projectName - 项目名
|
|
93
|
+
* @returns {WorktreeDetailedStatus} 详细状态
|
|
94
|
+
*/
|
|
95
|
+
function collectWorktreeDetailedStatus(worktree: WorktreeInfo, projectName: string): WorktreeDetailedStatus {
|
|
96
|
+
const changeStatus = detectChangeStatus(worktree);
|
|
97
|
+
const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
|
|
98
|
+
const { insertions, deletions } = countDiffStat(worktree.path);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
path: worktree.path,
|
|
102
|
+
branch: worktree.branch,
|
|
103
|
+
changeStatus,
|
|
104
|
+
commitsAhead,
|
|
105
|
+
commitsBehind,
|
|
106
|
+
hasSnapshot: hasSnapshot(projectName, worktree.branch),
|
|
107
|
+
insertions,
|
|
108
|
+
deletions,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 检测 worktree 的变更状态
|
|
114
|
+
* 优先级:冲突 > 未提交 > 已提交 > 干净
|
|
115
|
+
* @param {WorktreeInfo} worktree - worktree 信息
|
|
116
|
+
* @returns {WorktreeDetailedStatus['changeStatus']} 变更状态
|
|
117
|
+
*/
|
|
118
|
+
function detectChangeStatus(worktree: WorktreeInfo): WorktreeDetailedStatus['changeStatus'] {
|
|
119
|
+
try {
|
|
120
|
+
if (hasMergeConflict(worktree.path)) {
|
|
121
|
+
return 'conflict';
|
|
122
|
+
}
|
|
123
|
+
if (!isWorkingDirClean(worktree.path)) {
|
|
124
|
+
return 'uncommitted';
|
|
125
|
+
}
|
|
126
|
+
if (hasLocalCommits(worktree.branch)) {
|
|
127
|
+
return 'committed';
|
|
128
|
+
}
|
|
129
|
+
return 'clean';
|
|
130
|
+
} catch {
|
|
131
|
+
return 'clean';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 统计分支与主分支的提交差异(领先/落后数)
|
|
137
|
+
* @param {string} branchName - 分支名
|
|
138
|
+
* @returns {{ commitsAhead: number; commitsBehind: number }} 领先和落后的提交数
|
|
139
|
+
*/
|
|
140
|
+
function countCommitDivergence(branchName: string): { commitsAhead: number; commitsBehind: number } {
|
|
141
|
+
try {
|
|
142
|
+
return {
|
|
143
|
+
commitsAhead: getCommitCountAhead(branchName),
|
|
144
|
+
commitsBehind: getCommitCountBehind(branchName),
|
|
145
|
+
};
|
|
146
|
+
} catch {
|
|
147
|
+
return { commitsAhead: 0, commitsBehind: 0 };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 统计 worktree 的差异行数
|
|
153
|
+
* @param {string} worktreePath - worktree 路径
|
|
154
|
+
* @returns {{ insertions: number; deletions: number }} 新增和删除行数
|
|
155
|
+
*/
|
|
156
|
+
function countDiffStat(worktreePath: string): { insertions: number; deletions: number } {
|
|
157
|
+
try {
|
|
158
|
+
return getDiffStat(worktreePath);
|
|
159
|
+
} catch {
|
|
160
|
+
return { insertions: 0, deletions: 0 };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 收集未清理的 validate 快照信息
|
|
166
|
+
* 对比快照分支与现有 worktree 分支,标识孤立快照
|
|
167
|
+
* @param {string} projectName - 项目名
|
|
168
|
+
* @param {WorktreeInfo[]} worktrees - 当前有效的 worktree 列表
|
|
169
|
+
* @returns {SnapshotInfo[]} 快照信息列表
|
|
170
|
+
*/
|
|
171
|
+
function collectSnapshots(projectName: string, worktrees: WorktreeInfo[]): SnapshotInfo[] {
|
|
172
|
+
const snapshotBranches = getProjectSnapshotBranches(projectName);
|
|
173
|
+
const worktreeBranchSet = new Set(worktrees.map((wt) => wt.branch));
|
|
174
|
+
|
|
175
|
+
return snapshotBranches.map((branch) => ({
|
|
176
|
+
branch,
|
|
177
|
+
worktreeExists: worktreeBranchSet.has(branch),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 以 JSON 格式输出状态信息
|
|
183
|
+
* @param {StatusResult} result - 状态数据
|
|
184
|
+
*/
|
|
185
|
+
function printStatusAsJson(result: StatusResult): void {
|
|
186
|
+
console.log(JSON.stringify(result, null, 2));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 以文本格式输出状态信息
|
|
191
|
+
* @param {StatusResult} result - 状态数据
|
|
192
|
+
*/
|
|
193
|
+
function printStatusAsText(result: StatusResult): void {
|
|
194
|
+
// 标题区域
|
|
195
|
+
printDoubleSeparator();
|
|
196
|
+
printInfo(` ${chalk.bold.cyan(MESSAGES.STATUS_TITLE(result.main.projectName))}`);
|
|
197
|
+
printDoubleSeparator();
|
|
198
|
+
printInfo('');
|
|
199
|
+
|
|
200
|
+
// 主 Worktree 区块
|
|
201
|
+
printMainSection(result.main);
|
|
202
|
+
printSeparator();
|
|
203
|
+
printInfo('');
|
|
204
|
+
|
|
205
|
+
// Worktree 列表区块
|
|
206
|
+
printWorktreesSection(result.worktrees, result.totalWorktrees);
|
|
207
|
+
printSeparator();
|
|
208
|
+
printInfo('');
|
|
209
|
+
|
|
210
|
+
// 未清理快照区块
|
|
211
|
+
printSnapshotsSection(result.snapshots);
|
|
212
|
+
|
|
213
|
+
printDoubleSeparator();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 输出主 Worktree 区块
|
|
218
|
+
* @param {MainWorktreeStatus} main - 主 worktree 状态
|
|
219
|
+
*/
|
|
220
|
+
function printMainSection(main: MainWorktreeStatus): void {
|
|
221
|
+
printInfo(` ${chalk.bold('◆')} ${chalk.bold(MESSAGES.STATUS_MAIN_SECTION)}`);
|
|
222
|
+
printInfo(` 分支: ${chalk.bold(main.branch)}`);
|
|
223
|
+
if (main.isClean) {
|
|
224
|
+
printInfo(` 状态: ${chalk.green('✓ 干净')}`);
|
|
225
|
+
} else {
|
|
226
|
+
printInfo(` 状态: ${chalk.yellow('✗ 有未提交修改')}`);
|
|
227
|
+
}
|
|
228
|
+
printInfo('');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 输出 Worktree 列表区块
|
|
233
|
+
* @param {WorktreeDetailedStatus[]} worktrees - worktree 详细状态列表
|
|
234
|
+
* @param {number} total - worktree 总数
|
|
235
|
+
*/
|
|
236
|
+
function printWorktreesSection(worktrees: WorktreeDetailedStatus[], total: number): void {
|
|
237
|
+
printInfo(` ${chalk.bold('◆')} ${chalk.bold(MESSAGES.STATUS_WORKTREES_SECTION)} (${total} 个)`);
|
|
238
|
+
printInfo('');
|
|
239
|
+
|
|
240
|
+
if (worktrees.length === 0) {
|
|
241
|
+
printInfo(` ${MESSAGES.STATUS_NO_WORKTREES}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const wt of worktrees) {
|
|
246
|
+
printWorktreeItem(wt);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 输出单个 worktree 状态项
|
|
252
|
+
* @param {WorktreeDetailedStatus} wt - worktree 详细状态
|
|
253
|
+
*/
|
|
254
|
+
function printWorktreeItem(wt: WorktreeDetailedStatus): void {
|
|
255
|
+
// 分支名 + 变更状态标签
|
|
256
|
+
const statusLabel = formatChangeStatusLabel(wt.changeStatus);
|
|
257
|
+
printInfo(` ${chalk.bold('●')} ${chalk.bold(wt.branch)} [${statusLabel}]`);
|
|
258
|
+
|
|
259
|
+
// 差异统计行
|
|
260
|
+
const parts: string[] = [];
|
|
261
|
+
|
|
262
|
+
// 行数变更(仅在有变更时展示)
|
|
263
|
+
if (wt.insertions > 0 || wt.deletions > 0) {
|
|
264
|
+
parts.push(`${chalk.green(`+${wt.insertions}`)} ${chalk.red(`-${wt.deletions}`)}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 本地提交数
|
|
268
|
+
if (wt.commitsAhead > 0) {
|
|
269
|
+
parts.push(chalk.yellow(`${wt.commitsAhead} 个本地提交`));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 与主分支的同步状态
|
|
273
|
+
if (wt.commitsBehind > 0) {
|
|
274
|
+
parts.push(chalk.yellow(`落后主分支 ${wt.commitsBehind} 个提交`));
|
|
275
|
+
} else {
|
|
276
|
+
parts.push(chalk.green('与主分支同步'));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
printInfo(` ${parts.join(' ')}`);
|
|
280
|
+
|
|
281
|
+
// 快照状态
|
|
282
|
+
if (wt.hasSnapshot) {
|
|
283
|
+
printInfo(` ${chalk.blue('有 validate 快照')}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
printInfo('');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 将变更状态枚举格式化为带颜色的标签文本
|
|
291
|
+
* @param {WorktreeDetailedStatus['changeStatus']} status - 变更状态
|
|
292
|
+
* @returns {string} 格式化后的标签
|
|
293
|
+
*/
|
|
294
|
+
function formatChangeStatusLabel(status: WorktreeDetailedStatus['changeStatus']): string {
|
|
295
|
+
switch (status) {
|
|
296
|
+
case 'committed':
|
|
297
|
+
return chalk.green(MESSAGES.STATUS_CHANGE_COMMITTED);
|
|
298
|
+
case 'uncommitted':
|
|
299
|
+
return chalk.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
|
|
300
|
+
case 'conflict':
|
|
301
|
+
return chalk.red(MESSAGES.STATUS_CHANGE_CONFLICT);
|
|
302
|
+
case 'clean':
|
|
303
|
+
return chalk.gray(MESSAGES.STATUS_CHANGE_CLEAN);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 输出未清理快照区块
|
|
309
|
+
* @param {SnapshotInfo[]} snapshots - 快照信息列表
|
|
310
|
+
*/
|
|
311
|
+
function printSnapshotsSection(snapshots: SnapshotInfo[]): void {
|
|
312
|
+
printInfo(` ${chalk.bold('◆')} ${chalk.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.length} 个)`);
|
|
313
|
+
printInfo('');
|
|
314
|
+
|
|
315
|
+
if (snapshots.length === 0) {
|
|
316
|
+
printInfo(` ${MESSAGES.STATUS_NO_SNAPSHOTS}`);
|
|
317
|
+
printInfo('');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const snap of snapshots) {
|
|
322
|
+
const orphanLabel = snap.worktreeExists ? '' : ` ${chalk.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED)}`;
|
|
323
|
+
const icon = snap.worktreeExists ? chalk.blue('●') : chalk.yellow('⚠');
|
|
324
|
+
printInfo(` ${icon} ${snap.branch}${orphanLabel}`);
|
|
325
|
+
}
|
|
326
|
+
printInfo('');
|
|
327
|
+
}
|
|
@@ -148,6 +148,28 @@ export const MESSAGES = {
|
|
|
148
148
|
SYNC_SELECT_BRANCH: '请选择要同步的分支',
|
|
149
149
|
/** sync 模糊匹配到多个结果提示 */
|
|
150
150
|
SYNC_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
151
|
+
/** status 命令标题 */
|
|
152
|
+
STATUS_TITLE: (projectName: string) => `项目状态总览: ${projectName}`,
|
|
153
|
+
/** status 主 worktree 区块标题 */
|
|
154
|
+
STATUS_MAIN_SECTION: '主 Worktree',
|
|
155
|
+
/** status worktrees 区块标题 */
|
|
156
|
+
STATUS_WORKTREES_SECTION: 'Worktree 列表',
|
|
157
|
+
/** status 快照区块标题 */
|
|
158
|
+
STATUS_SNAPSHOTS_SECTION: '未清理的 Validate 快照',
|
|
159
|
+
/** status 无 worktree */
|
|
160
|
+
STATUS_NO_WORKTREES: '(无活跃 worktree)',
|
|
161
|
+
/** status 无未清理快照 */
|
|
162
|
+
STATUS_NO_SNAPSHOTS: '(无未清理的快照)',
|
|
163
|
+
/** status 变更状态:已提交 */
|
|
164
|
+
STATUS_CHANGE_COMMITTED: '已提交',
|
|
165
|
+
/** status 变更状态:未提交修改 */
|
|
166
|
+
STATUS_CHANGE_UNCOMMITTED: '未提交修改',
|
|
167
|
+
/** status 变更状态:合并冲突 */
|
|
168
|
+
STATUS_CHANGE_CONFLICT: '合并冲突',
|
|
169
|
+
/** status 变更状态:无变更 */
|
|
170
|
+
STATUS_CHANGE_CLEAN: '无变更',
|
|
171
|
+
/** status 快照对应 worktree 已不存在 */
|
|
172
|
+
STATUS_SNAPSHOT_ORPHANED: '(对应 worktree 已不存在)',
|
|
151
173
|
/** merge 后 pull 冲突 */
|
|
152
174
|
PULL_CONFLICT:
|
|
153
175
|
'自动 pull 时发生冲突,merge 已完成但远程同步失败\n 请手动解决冲突:\n 解决冲突后执行 git add . && git commit\n 然后执行 git push 推送到远程',
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { registerMergeCommand } from './commands/merge.js';
|
|
|
14
14
|
import { registerConfigCommand } from './commands/config.js';
|
|
15
15
|
import { registerSyncCommand } from './commands/sync.js';
|
|
16
16
|
import { registerResetCommand } from './commands/reset.js';
|
|
17
|
+
import { registerStatusCommand } from './commands/status.js';
|
|
17
18
|
|
|
18
19
|
// 从 package.json 读取版本号,避免硬编码
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
@@ -48,6 +49,7 @@ registerMergeCommand(program);
|
|
|
48
49
|
registerConfigCommand(program);
|
|
49
50
|
registerSyncCommand(program);
|
|
50
51
|
registerResetCommand(program);
|
|
52
|
+
registerStatusCommand(program);
|
|
51
53
|
|
|
52
54
|
// 全局未捕获异常处理
|
|
53
55
|
process.on('uncaughtException', (error) => {
|
package/src/types/command.ts
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
|
|
2
|
-
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions } from './command.js';
|
|
2
|
+
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions } 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
|
+
export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, StatusResult } from './status.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** 单个 worktree 的详细状态信息 */
|
|
2
|
+
export interface WorktreeDetailedStatus {
|
|
3
|
+
/** worktree 路径 */
|
|
4
|
+
path: string;
|
|
5
|
+
/** 分支名 */
|
|
6
|
+
branch: string;
|
|
7
|
+
/** 变更状态: committed(有已提交内容) / uncommitted(有未提交修改) / conflict(存在合并冲突) / clean(无变更) */
|
|
8
|
+
changeStatus: 'committed' | 'uncommitted' | 'conflict' | 'clean';
|
|
9
|
+
/** 相对于主分支的新增提交数(领先) */
|
|
10
|
+
commitsAhead: number;
|
|
11
|
+
/** 落后于主分支的提交数 */
|
|
12
|
+
commitsBehind: number;
|
|
13
|
+
/** 是否存在 validate 快照 */
|
|
14
|
+
hasSnapshot: boolean;
|
|
15
|
+
/** 工作区和暂存区的新增行数 */
|
|
16
|
+
insertions: number;
|
|
17
|
+
/** 工作区和暂存区的删除行数 */
|
|
18
|
+
deletions: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 主 worktree 状态信息 */
|
|
22
|
+
export interface MainWorktreeStatus {
|
|
23
|
+
/** 当前分支名 */
|
|
24
|
+
branch: string;
|
|
25
|
+
/** 工作区是否干净 */
|
|
26
|
+
isClean: boolean;
|
|
27
|
+
/** 项目名 */
|
|
28
|
+
projectName: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** validate 快照信息 */
|
|
32
|
+
export interface SnapshotInfo {
|
|
33
|
+
/** 分支名 */
|
|
34
|
+
branch: string;
|
|
35
|
+
/** 是否对应的 worktree 仍然存在 */
|
|
36
|
+
worktreeExists: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** status 命令的完整输出结构 */
|
|
40
|
+
export interface StatusResult {
|
|
41
|
+
/** 主 worktree 状态 */
|
|
42
|
+
main: MainWorktreeStatus;
|
|
43
|
+
/** 各 worktree 的详细状态 */
|
|
44
|
+
worktrees: WorktreeDetailedStatus[];
|
|
45
|
+
/** 未清理的 validate 快照列表 */
|
|
46
|
+
snapshots: SnapshotInfo[];
|
|
47
|
+
/** worktree 总数 */
|
|
48
|
+
totalWorktrees: number;
|
|
49
|
+
}
|
package/src/utils/git.ts
CHANGED
|
@@ -263,6 +263,22 @@ export function getCommitCountAhead(branchName: string, cwd?: string): number {
|
|
|
263
263
|
return parseInt(output, 10) || 0;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* 获取目标分支落后于当前分支的提交数
|
|
268
|
+
* 即当前分支有多少提交是目标分支没有的
|
|
269
|
+
* @param {string} branchName - 目标分支名
|
|
270
|
+
* @param {string} [cwd] - 工作目录
|
|
271
|
+
* @returns {number} 落后的提交数
|
|
272
|
+
*/
|
|
273
|
+
export function getCommitCountBehind(branchName: string, cwd?: string): number {
|
|
274
|
+
try {
|
|
275
|
+
const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
|
|
276
|
+
return parseInt(output, 10) || 0;
|
|
277
|
+
} catch {
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
266
282
|
/**
|
|
267
283
|
* 解析 git diff --shortstat 输出,提取新增行数和删除行数
|
|
268
284
|
* @param {string} output - shortstat 输出字符串
|
package/src/utils/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export {
|
|
|
27
27
|
gitWorktreePrune,
|
|
28
28
|
hasLocalCommits,
|
|
29
29
|
getCommitCountAhead,
|
|
30
|
+
getCommitCountBehind,
|
|
30
31
|
getDiffStat,
|
|
31
32
|
gitDiffCachedBinary,
|
|
32
33
|
gitApplyCachedFromStdin,
|
|
@@ -52,6 +53,6 @@ export { printSuccess, printError, printWarning, printInfo, printSeparator, prin
|
|
|
52
53
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
53
54
|
export { multilineInput } from './prompt.js';
|
|
54
55
|
export { launchInteractiveClaude } from './claude.js';
|
|
55
|
-
export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
|
|
56
|
+
export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
|
|
56
57
|
export { findExactMatch, findFuzzyMatches, promptSelectBranch, resolveTargetWorktree } from './worktree-matcher.js';
|
|
57
58
|
export type { WorktreeResolveMessages } from './worktree-matcher.js';
|
|
@@ -98,6 +98,23 @@ export function removeSnapshot(projectName: string, branchName: string): void {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* 获取指定项目所有存在 validate 快照的分支名列表
|
|
103
|
+
* 通过扫描快照目录下的 .tree 文件名提取
|
|
104
|
+
* @param {string} projectName - 项目名
|
|
105
|
+
* @returns {string[]} 存在快照的分支名列表
|
|
106
|
+
*/
|
|
107
|
+
export function getProjectSnapshotBranches(projectName: string): string[] {
|
|
108
|
+
const projectDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
109
|
+
if (!existsSync(projectDir)) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const files = readdirSync(projectDir);
|
|
113
|
+
return files
|
|
114
|
+
.filter((f: string) => f.endsWith('.tree'))
|
|
115
|
+
.map((f: string) => f.replace(/\.tree$/, ''));
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
/**
|
|
102
119
|
* 删除指定项目的所有 validate 快照
|
|
103
120
|
* @param {string} projectName - 项目名
|