clawt 3.5.3 → 3.6.1
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/agents/docs-sync-updater.md +24 -44
- package/CLAUDE.md +6 -0
- package/dist/index.js +169 -16
- package/dist/postinstall.js +16 -2
- package/package.json +1 -1
- package/src/commands/home.ts +24 -2
- package/src/commands/init.ts +23 -6
- package/src/commands/resume.ts +91 -0
- package/src/constants/messages/home.ts +2 -0
- package/src/constants/messages/resume.ts +10 -0
- package/src/types/command.ts +12 -0
- package/src/types/index.ts +1 -1
- package/src/utils/git-core.ts +40 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/task-executor.ts +34 -8
- package/src/utils/validation.ts +1 -1
- package/tests/unit/commands/resume.test.ts +241 -3
- package/docs/alias.md +0 -114
- package/docs/completion.md +0 -55
- package/docs/config-file.md +0 -45
- package/docs/config.md +0 -93
- package/docs/cover-validate.md +0 -94
- package/docs/create.md +0 -101
- package/docs/home.md +0 -58
- package/docs/init.md +0 -81
- package/docs/list.md +0 -73
- package/docs/log.md +0 -67
- package/docs/merge.md +0 -137
- package/docs/notification.md +0 -94
- package/docs/project-config.md +0 -132
- package/docs/projects.md +0 -135
- package/docs/remove.md +0 -90
- package/docs/reset.md +0 -37
- package/docs/resume.md +0 -100
- package/docs/run.md +0 -146
- package/docs/spec.md +0 -415
- package/docs/status.md +0 -343
- package/docs/sync.md +0 -128
- package/docs/update-check.md +0 -95
- package/docs/validate.md +0 -416
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: docs-sync-updater
|
|
3
|
-
description: "Use this agent when the user explicitly requests to synchronize documentation files (
|
|
3
|
+
description: "Use this agent when the user explicitly requests to synchronize documentation files (README.md) based on recent code changes in the working area or staging area. This agent must NEVER be called proactively or automatically — it must only be invoked when the user explicitly asks for documentation synchronization.\n\nExamples:\n\n- Example 1:\n user: \"请同步更新文档\"\n assistant: \"好的,我来调用文档同步 agent 来根据当前代码变更更新相关文档。\"\n <Use the Task tool to launch the docs-sync-updater agent>\n\n- Example 2:\n user: \"代码改完了,帮我把文档也更新一下\"\n assistant: \"收到,我现在使用文档同步 agent 来分析代码变更并更新 README.md。\"\n <Use the Task tool to launch the docs-sync-updater agent>\n\n- Example 3:\n user: \"update docs based on my changes\"\n assistant: \"好的,我来启动文档同步 agent,根据工作区和暂存区的变更同步更新文档。\"\n <Use the Task tool to launch the docs-sync-updater agent>\n\n- Counter-example (DO NOT do this):\n user: \"我刚加了一个新命令\"\n assistant: (DO NOT proactively launch this agent. Wait for the user to explicitly request documentation updates.)"
|
|
4
4
|
model: opus
|
|
5
5
|
memory: project
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
你是一位资深的技术文档工程师,精通代码变更分析与文档同步维护。你的核心职责是根据当前工作区(working directory)和暂存区(staging area
|
|
8
|
+
你是一位资深的技术文档工程师,精通代码变更分析与文档同步维护。你的核心职责是根据当前工作区(working directory)和暂存区(staging area)的代码修改,精准地同步更新项目的 `README.md`。
|
|
9
9
|
|
|
10
10
|
## 文档结构
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- **`docs/spec.md`**:项目核心架构规范(全局设计、验证架构、公共模块等)
|
|
15
|
-
- **`docs/<command>.md`**:各命令的独立文档(如 `create.md`、`merge.md`、`validate.md`、`status.md` 等)
|
|
16
|
-
- **`docs/config.md`**:配置系统文档
|
|
17
|
-
- **`docs/config-file.md`**:配置文件规范
|
|
18
|
-
- **`README.md`**:面向用户的快速上手指南
|
|
19
|
-
|
|
20
|
-
当代码变更涉及某个命令时,应更新对应的 `docs/<command>.md` 而非将所有内容塞进 `docs/spec.md`。`docs/spec.md` 仅用于跨命令的全局架构和公共设计。
|
|
12
|
+
项目的文档为单一的 `README.md` 文件,是面向用户的快速上手指南。
|
|
21
13
|
|
|
22
14
|
## 重要约束
|
|
23
15
|
|
|
@@ -38,27 +30,19 @@ memory: project
|
|
|
38
30
|
5. 仔细分析变更内容,提取以下信息:
|
|
39
31
|
- 新增/修改/删除了哪些文件
|
|
40
32
|
- 新增/修改/删除了哪些功能、命令、函数、类型
|
|
41
|
-
- 架构层面的变化(新目录、新模块、依赖变更等)
|
|
42
33
|
- API 或配置的变化
|
|
43
34
|
- 构建流程的变化
|
|
44
35
|
|
|
45
36
|
### 第二步:阅读现有文档
|
|
46
37
|
|
|
47
|
-
1.
|
|
48
|
-
2. 根据代码变更涉及的模块,读取对应的 `docs/<command>.md` 文档。
|
|
49
|
-
3. 如果变更涉及全局架构或公共模块,读取 `docs/spec.md`。
|
|
50
|
-
4. 如果变更影响用户可见的功能,读取 `README.md`。
|
|
51
|
-
5. 理解每个相关文档的结构、风格和覆盖范围。
|
|
38
|
+
1. 读取 `README.md`,理解其结构、风格和覆盖范围。
|
|
52
39
|
|
|
53
40
|
### 第三步:确定需要更新的内容
|
|
54
41
|
|
|
55
|
-
|
|
42
|
+
根据代码变更的范围,判断 `README.md` 中哪些部分需要更新:
|
|
56
43
|
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
- **`docs/config.md` / `docs/config-file.md`**:当配置系统或配置文件格式发生变化时需要更新。
|
|
60
|
-
- **`README.md`**:当用户可见的功能、安装方式、使用方法、命令参数发生变化时需要更新。
|
|
61
|
-
- 如果代码变更新增了一个全新的命令,应创建对应的 `docs/<command>.md` 文件。
|
|
44
|
+
- 用户可见的功能、安装方式、使用方法、命令参数发生变化时需要更新。
|
|
45
|
+
- 如果代码变更只涉及内部重构而不改变用户可见行为,可能不需要更新 `README.md`,此时应告知用户无需更新并说明原因。
|
|
62
46
|
|
|
63
47
|
### 第四步:执行更新
|
|
64
48
|
|
|
@@ -66,40 +50,38 @@ memory: project
|
|
|
66
50
|
2. **只修改与代码变更相关的部分**——不要重写整个文档。
|
|
67
51
|
3. **增量更新**——新增内容放在逻辑上合适的位置。
|
|
68
52
|
4. **保持一致性**——术语、格式、缩进与现有文档保持一致。
|
|
69
|
-
5.
|
|
53
|
+
5. **如果不需要更新,明确说明原因并跳过**。
|
|
70
54
|
|
|
71
55
|
### 第五步:输出更新摘要
|
|
72
56
|
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
## 文档更新原则
|
|
78
|
-
|
|
79
|
-
1. **准确性优先**:文档内容必须准确反映代码的实际状态,不要编造或猜测。
|
|
80
|
-
2. **最小变更原则**:只更新与代码变更直接相关的部分,避免不必要的改动。
|
|
81
|
-
3. **向后兼容**:保留现有文档中仍然有效的内容,不要随意删除。
|
|
82
|
-
4. **中文优先**:新增的文档内容使用中文,除非原文档使用英文且保持一致性更好。
|
|
83
|
-
5. **代码示例同步**:如果文档中有代码示例受到变更影响,必须同步更新。
|
|
57
|
+
完成文档更新后,输出一份简洁的更新摘要:
|
|
58
|
+
- 列出 `README.md` 的具体修改内容
|
|
59
|
+
- 如果未做修改,说明原因
|
|
84
60
|
|
|
85
61
|
## README.md 编写规则
|
|
86
62
|
|
|
87
63
|
README.md 的定位是**面向新用户的快速上手指南**,必须严格遵守以下规则:
|
|
88
64
|
|
|
89
65
|
1. **只写"怎么用",不写"怎么实现"**:不涉及内部原理、实现细节、技术架构等内容。用户只需要知道命令怎么敲、参数怎么传。
|
|
90
|
-
2.
|
|
66
|
+
2. **保持简洁**:每个命令只展示最常用的用法和必要参数,避免罗列所有边界情况和细节行为。
|
|
91
67
|
3. **结构固定**:README.md 应保持以下结构顺序:安装 → 快速开始 → 命令一览 → 配置 → 全局选项 → 日志。不要添加"开发"、"测试"、"技术栈"等面向开发者的章节。
|
|
92
|
-
4.
|
|
68
|
+
4. **不包含开发相关内容**:测试命令、构建流程、技术选型、目录结构等开发者信息不放在 README.md。
|
|
93
69
|
5. **命令说明精简**:每个命令给出简短描述 + 核心用法示例即可,参数表只在确实需要时才添加,且只列必要参数。
|
|
94
|
-
|
|
70
|
+
|
|
71
|
+
## 文档更新原则
|
|
72
|
+
|
|
73
|
+
1. **准确性优先**:文档内容必须准确反映代码的实际状态,不要编造或猜测。
|
|
74
|
+
2. **最小变更原则**:只更新与代码变更直接相关的部分,避免不必要的改动。
|
|
75
|
+
3. **向后兼容**:保留现有文档中仍然有效的内容,不要随意删除。
|
|
76
|
+
4. **中文优先**:新增的文档内容使用中文,除非原文档使用英文且保持一致性更好。
|
|
77
|
+
5. **代码示例同步**:如果文档中有代码示例受到变更影响,必须同步更新。
|
|
95
78
|
|
|
96
79
|
## 质量检查
|
|
97
80
|
|
|
98
81
|
在完成更新前,自我检查:
|
|
99
|
-
- [ ]
|
|
82
|
+
- [ ] 所有变更的功能是否都在 README.md 中体现?
|
|
100
83
|
- [ ] 文档中的代码示例是否仍然正确?
|
|
101
84
|
- [ ] 命令列表、参数说明是否与代码一致?
|
|
102
|
-
- [ ] 目录结构描述是否与实际一致?
|
|
103
85
|
- [ ] 没有删除任何注释掉的代码或解释性注释?
|
|
104
86
|
- [ ] 新增注释是否使用中文?
|
|
105
87
|
- [ ] 文档格式是否与原有风格保持一致?
|
|
@@ -107,15 +89,13 @@ README.md 的定位是**面向新用户的快速上手指南**,必须严格遵
|
|
|
107
89
|
## 边界情况处理
|
|
108
90
|
|
|
109
91
|
- 如果工作区和暂存区都没有变更,检查最近的提交并告知用户当前没有未提交的变更,询问是否基于最近提交更新。
|
|
110
|
-
- 如果某个文档文件不存在,告知用户并询问是否需要创建。
|
|
111
92
|
- 如果变更内容过于复杂或不确定如何反映到文档中,列出你的理解并询问用户确认。
|
|
112
|
-
-
|
|
113
|
-
- 如果变更涉及新增命令,且 `docs/` 下还没有对应的命令文档,应创建 `docs/<command>.md`。
|
|
93
|
+
- 如果变更只涉及代码重构而不改变功能,可能不需要更新 README.md,告知用户即可。
|
|
114
94
|
|
|
115
95
|
**Update your agent memory** as you discover documentation patterns, document structure conventions, terminology usage, and relationships between code modules and their documentation sections. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.
|
|
116
96
|
|
|
117
97
|
Examples of what to record:
|
|
118
|
-
-
|
|
98
|
+
- README.md 的章节结构和更新模式
|
|
119
99
|
- 项目术语和命名惯例
|
|
120
100
|
- 代码模块与文档章节的对应关系
|
|
121
101
|
- 常见的文档更新场景和处理方式
|
package/CLAUDE.md
ADDED
package/dist/index.js
CHANGED
|
@@ -311,7 +311,17 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
311
311
|
/** 批量 resume 非 macOS 平台提示 */
|
|
312
312
|
RESUME_ALL_PLATFORM_UNSUPPORTED: "\u6279\u91CF resume \u76EE\u524D\u4EC5\u652F\u6301 macOS \u5E73\u53F0\uFF08\u901A\u8FC7 AppleScript \u6253\u5F00\u7EC8\u7AEF Tab\uFF09",
|
|
313
313
|
/** 批量 resume 无匹配分支提示 */
|
|
314
|
-
RESUME_ALL_NO_MATCH: (keyword) => `\u672A\u627E\u5230\u4E0E "${keyword}" \u5339\u914D\u7684\u5206\u652F
|
|
314
|
+
RESUME_ALL_NO_MATCH: (keyword) => `\u672A\u627E\u5230\u4E0E "${keyword}" \u5339\u914D\u7684\u5206\u652F`,
|
|
315
|
+
/** --prompt 必须配合 -b 指定目标分支 */
|
|
316
|
+
RESUME_PROMPT_REQUIRES_BRANCH: "--prompt \u5FC5\u987B\u914D\u5408 -b \u6307\u5B9A\u76EE\u6807\u5206\u652F",
|
|
317
|
+
/** --prompt 和 -f 不能同时使用 */
|
|
318
|
+
RESUME_PROMPT_FILE_CONFLICT: "--prompt \u548C -f \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528",
|
|
319
|
+
/** 未找到对应 worktree */
|
|
320
|
+
RESUME_WORKTREE_NOT_FOUND: (branch, available) => `\u672A\u627E\u5230\u5206\u652F "${branch}" \u5BF9\u5E94\u7684 worktree
|
|
321
|
+
\u53EF\u7528\u5206\u652F\uFF1A
|
|
322
|
+
${available.map((b) => ` - ${b}`).join("\n")}`,
|
|
323
|
+
/** 追问文件加载完成 */
|
|
324
|
+
RESUME_FOLLOW_UP_FILE_LOADED: (count, path2) => `\u4ECE ${path2} \u52A0\u8F7D\u4E86 ${count} \u4E2A\u8FFD\u95EE\u4EFB\u52A1`
|
|
315
325
|
};
|
|
316
326
|
|
|
317
327
|
// src/constants/messages/remove.ts
|
|
@@ -537,7 +547,11 @@ var HOME_MESSAGES = {
|
|
|
537
547
|
/** 已在主工作分支上 */
|
|
538
548
|
HOME_ALREADY_ON_MAIN: (branch) => `\u5DF2\u5728\u4E3B\u5DE5\u4F5C\u5206\u652F ${branch} \u4E0A\uFF0C\u65E0\u9700\u5207\u6362`,
|
|
539
549
|
/** 切换成功 */
|
|
540
|
-
HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}
|
|
550
|
+
HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}`,
|
|
551
|
+
/** 当前在子 worktree,提示用户手动 cd 到主 worktree */
|
|
552
|
+
HOME_NOT_IN_MAIN_WORKTREE: (mainPath) => `\u5F53\u524D\u4E0D\u5728\u4E3B worktree\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u4E3B worktree\uFF1A
|
|
553
|
+
|
|
554
|
+
cd ${mainPath}`
|
|
541
555
|
};
|
|
542
556
|
|
|
543
557
|
// src/constants/messages/tasks.ts
|
|
@@ -1074,9 +1088,29 @@ import { execSync as execSync2, execFileSync as execFileSync2 } from "child_proc
|
|
|
1074
1088
|
function getGitCommonDir(cwd) {
|
|
1075
1089
|
return execCommand("git rev-parse --git-common-dir", { cwd });
|
|
1076
1090
|
}
|
|
1091
|
+
function isMainWorktree(cwd) {
|
|
1092
|
+
try {
|
|
1093
|
+
return getGitCommonDir(cwd) === ".git";
|
|
1094
|
+
} catch {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function isInsideGitRepo(cwd) {
|
|
1099
|
+
try {
|
|
1100
|
+
execCommand("git rev-parse --is-inside-work-tree", { cwd });
|
|
1101
|
+
return true;
|
|
1102
|
+
} catch {
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1077
1106
|
function getGitTopLevel(cwd) {
|
|
1078
1107
|
return execCommand("git rev-parse --show-toplevel", { cwd });
|
|
1079
1108
|
}
|
|
1109
|
+
function getMainWorktreePath(cwd) {
|
|
1110
|
+
const output = execCommand("git worktree list --porcelain", { cwd });
|
|
1111
|
+
const firstLine = output.split("\n")[0] || "";
|
|
1112
|
+
return firstLine.replace("worktree ", "");
|
|
1113
|
+
}
|
|
1080
1114
|
function getProjectName(cwd) {
|
|
1081
1115
|
const topLevel = getGitTopLevel(cwd);
|
|
1082
1116
|
return basename(topLevel);
|
|
@@ -2762,10 +2796,14 @@ function parseStreamEvent(event) {
|
|
|
2762
2796
|
}
|
|
2763
2797
|
|
|
2764
2798
|
// src/utils/task-executor.ts
|
|
2765
|
-
function executeClaudeTask(worktree, task, onActivity) {
|
|
2799
|
+
function executeClaudeTask(worktree, task, onActivity, continueSession) {
|
|
2800
|
+
const args = ["-p", task, "--output-format", "stream-json", "--verbose", "--permission-mode", "bypassPermissions"];
|
|
2801
|
+
if (continueSession) {
|
|
2802
|
+
args.push("--continue");
|
|
2803
|
+
}
|
|
2766
2804
|
const child = spawnProcess(
|
|
2767
2805
|
"claude",
|
|
2768
|
-
|
|
2806
|
+
args,
|
|
2769
2807
|
{
|
|
2770
2808
|
cwd: worktree.path,
|
|
2771
2809
|
// stdin 必须设置为 'ignore',不能用 'pipe'
|
|
@@ -2875,7 +2913,7 @@ function updateRendererStatus(renderer, index, result, startTime) {
|
|
|
2875
2913
|
);
|
|
2876
2914
|
}
|
|
2877
2915
|
}
|
|
2878
|
-
async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses) {
|
|
2916
|
+
async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses, continueFlags) {
|
|
2879
2917
|
const total = tasks.length;
|
|
2880
2918
|
const results = new Array(total);
|
|
2881
2919
|
let nextIndex = 0;
|
|
@@ -2891,7 +2929,7 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
|
|
|
2891
2929
|
renderer.markRunning(index);
|
|
2892
2930
|
const handle = executeClaudeTask(wt, task, (activityText) => {
|
|
2893
2931
|
renderer.updateActivityText(index, activityText);
|
|
2894
|
-
});
|
|
2932
|
+
}, continueFlags?.[index]);
|
|
2895
2933
|
childProcesses.push(handle.child);
|
|
2896
2934
|
handle.promise.then((result) => {
|
|
2897
2935
|
results[index] = result;
|
|
@@ -2911,13 +2949,13 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
|
|
|
2911
2949
|
}
|
|
2912
2950
|
});
|
|
2913
2951
|
}
|
|
2914
|
-
async function executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses) {
|
|
2952
|
+
async function executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses, continueFlags) {
|
|
2915
2953
|
const handles = worktrees.map((wt, index) => {
|
|
2916
2954
|
const task = tasks[index];
|
|
2917
2955
|
logger.info(`\u542F\u52A8\u4EFB\u52A1 ${index + 1}: ${task} (worktree: ${wt.path})`);
|
|
2918
2956
|
const handle = executeClaudeTask(wt, task, (activityText) => {
|
|
2919
2957
|
renderer.updateActivityText(index, activityText);
|
|
2920
|
-
});
|
|
2958
|
+
}, continueFlags?.[index]);
|
|
2921
2959
|
childProcesses.push(handle.child);
|
|
2922
2960
|
return handle;
|
|
2923
2961
|
});
|
|
@@ -2933,8 +2971,13 @@ async function executeAllParallel(worktrees, tasks, renderer, startTime, isInter
|
|
|
2933
2971
|
);
|
|
2934
2972
|
return results;
|
|
2935
2973
|
}
|
|
2936
|
-
async function executeBatchTasks(worktrees, tasks, concurrency) {
|
|
2974
|
+
async function executeBatchTasks(worktrees, tasks, concurrency, continueFlags) {
|
|
2937
2975
|
const count = tasks.length;
|
|
2976
|
+
if (continueFlags && continueFlags.length !== count) {
|
|
2977
|
+
throw new ClawtError(
|
|
2978
|
+
`continueFlags \u957F\u5EA6 (${continueFlags.length}) \u4E0E\u4EFB\u52A1\u6570 (${count}) \u4E0D\u4E00\u81F4`
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2938
2981
|
if (concurrency > 0) {
|
|
2939
2982
|
printInfo(MESSAGES.CONCURRENCY_INFO(concurrency, count));
|
|
2940
2983
|
printInfo("");
|
|
@@ -2968,10 +3011,10 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
|
|
|
2968
3011
|
process.exit(1);
|
|
2969
3012
|
};
|
|
2970
3013
|
process.on("SIGINT", sigintHandler);
|
|
2971
|
-
const results = concurrency > 0 ? await executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses) : await executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses);
|
|
3014
|
+
const results = concurrency > 0 ? await executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses, continueFlags) : await executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses, continueFlags);
|
|
2972
3015
|
renderer.stop();
|
|
2973
3016
|
process.removeListener("SIGINT", sigintHandler);
|
|
2974
|
-
if (interrupted) return;
|
|
3017
|
+
if (interrupted) return [];
|
|
2975
3018
|
const totalDurationMs = Date.now() - startTime;
|
|
2976
3019
|
const summary = {
|
|
2977
3020
|
total: results.length,
|
|
@@ -2981,6 +3024,7 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
|
|
|
2981
3024
|
totalCostUsd: results.reduce((sum, r) => sum + (r.result?.total_cost_usd ?? 0), 0)
|
|
2982
3025
|
};
|
|
2983
3026
|
printTaskSummary(summary);
|
|
3027
|
+
return results;
|
|
2984
3028
|
}
|
|
2985
3029
|
|
|
2986
3030
|
// src/utils/dry-run.ts
|
|
@@ -3295,6 +3339,56 @@ async function checkForUpdates(currentVersion) {
|
|
|
3295
3339
|
}
|
|
3296
3340
|
}
|
|
3297
3341
|
|
|
3342
|
+
// src/utils/json.ts
|
|
3343
|
+
function primitiveToString(value) {
|
|
3344
|
+
if (value === void 0) {
|
|
3345
|
+
return "undefined";
|
|
3346
|
+
}
|
|
3347
|
+
if (value === null) {
|
|
3348
|
+
return "null";
|
|
3349
|
+
}
|
|
3350
|
+
if (typeof value === "symbol") {
|
|
3351
|
+
return value.toString();
|
|
3352
|
+
}
|
|
3353
|
+
if (typeof value === "function") {
|
|
3354
|
+
return `[Function: ${value.name || "anonymous"}]`;
|
|
3355
|
+
}
|
|
3356
|
+
return String(value);
|
|
3357
|
+
}
|
|
3358
|
+
function safeStringify(value, indent = 2) {
|
|
3359
|
+
if (value === null || typeof value !== "object") {
|
|
3360
|
+
return primitiveToString(value);
|
|
3361
|
+
}
|
|
3362
|
+
try {
|
|
3363
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
3364
|
+
return JSON.stringify(
|
|
3365
|
+
value,
|
|
3366
|
+
(_key, val) => {
|
|
3367
|
+
if (typeof val === "bigint") {
|
|
3368
|
+
return val.toString();
|
|
3369
|
+
}
|
|
3370
|
+
if (typeof val === "undefined" || typeof val === "function" || typeof val === "symbol") {
|
|
3371
|
+
return primitiveToString(val);
|
|
3372
|
+
}
|
|
3373
|
+
if (typeof val === "object" && val !== null) {
|
|
3374
|
+
if (seen.has(val)) {
|
|
3375
|
+
return "[Circular]";
|
|
3376
|
+
}
|
|
3377
|
+
seen.add(val);
|
|
3378
|
+
}
|
|
3379
|
+
return val;
|
|
3380
|
+
},
|
|
3381
|
+
indent
|
|
3382
|
+
);
|
|
3383
|
+
} catch {
|
|
3384
|
+
try {
|
|
3385
|
+
return JSON.stringify(String(value), null, indent);
|
|
3386
|
+
} catch {
|
|
3387
|
+
return "[Unserializable]";
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3298
3392
|
// src/utils/validate-runner.ts
|
|
3299
3393
|
function handleErrorClipboard(clipboardContent) {
|
|
3300
3394
|
const success = copyToClipboard(clipboardContent);
|
|
@@ -4439,12 +4533,26 @@ var RESUME_RESOLVE_MESSAGES = {
|
|
|
4439
4533
|
noMatch: MESSAGES.RESUME_NO_MATCH
|
|
4440
4534
|
};
|
|
4441
4535
|
function registerResumeCommand(program2) {
|
|
4442
|
-
program2.command("resume").description("\u5728\u5DF2\u6709 worktree \u4E2D\u6062\u590D Claude Code \u4F1A\u8BDD\uFF08\u652F\u6301\u591A\u9009\u6279\u91CF\u6062\u590D\uFF09").option("-b, --branch <branchName>", "\u8981\u6062\u590D\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").action(async (options) => {
|
|
4536
|
+
program2.command("resume").description("\u5728\u5DF2\u6709 worktree \u4E2D\u6062\u590D Claude Code \u4F1A\u8BDD\uFF08\u652F\u6301\u591A\u9009\u6279\u91CF\u6062\u590D\uFF09").option("-b, --branch <branchName>", "\u8981\u6062\u590D\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").option("--prompt <content>", "\u975E\u4EA4\u4E92\u5F0F\u8FFD\u95EE\uFF08\u9700\u914D\u5408 -b \u6307\u5B9A\u5206\u652F\uFF09").option("-f, --file <path>", "\u4ECE\u4EFB\u52A1\u6587\u4EF6\u6279\u91CF\u8FFD\u95EE\uFF08\u901A\u8FC7 branch \u540D\u5339\u914D\u5DF2\u6709 worktree\uFF09").option("-c, --concurrency <n>", "\u6279\u91CF\u8FFD\u95EE\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236").action(async (options) => {
|
|
4443
4537
|
await handleResume(options);
|
|
4444
4538
|
});
|
|
4445
4539
|
}
|
|
4446
4540
|
async function handleResume(options) {
|
|
4447
4541
|
await runPreChecks(PRE_CHECK_RESUME);
|
|
4542
|
+
if (options.prompt && options.file) {
|
|
4543
|
+
throw new ClawtError(MESSAGES.RESUME_PROMPT_FILE_CONFLICT);
|
|
4544
|
+
}
|
|
4545
|
+
if (options.prompt) {
|
|
4546
|
+
if (!options.branch) {
|
|
4547
|
+
throw new ClawtError(MESSAGES.RESUME_PROMPT_REQUIRES_BRANCH);
|
|
4548
|
+
}
|
|
4549
|
+
const worktrees2 = getProjectWorktrees();
|
|
4550
|
+
const worktree = resolveWorktreeByBranch(options.branch, worktrees2);
|
|
4551
|
+
return handleNonInteractiveSingleResume(worktree, options.prompt);
|
|
4552
|
+
}
|
|
4553
|
+
if (options.file) {
|
|
4554
|
+
return handleNonInteractiveBatchResume(options.file, options);
|
|
4555
|
+
}
|
|
4448
4556
|
logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F\u8FC7\u6EE4: ${options.branch ?? "(\u65E0)"}`);
|
|
4449
4557
|
const worktrees = getProjectWorktrees();
|
|
4450
4558
|
let targetWorktrees;
|
|
@@ -4468,6 +4576,35 @@ async function handleResume(options) {
|
|
|
4468
4576
|
await handleBatchResume(targetWorktrees);
|
|
4469
4577
|
}
|
|
4470
4578
|
}
|
|
4579
|
+
function resolveWorktreeByBranch(branch, worktrees) {
|
|
4580
|
+
const match = findExactMatch(worktrees, branch);
|
|
4581
|
+
if (!match) {
|
|
4582
|
+
const available = worktrees.map((wt) => wt.branch);
|
|
4583
|
+
throw new ClawtError(MESSAGES.RESUME_WORKTREE_NOT_FOUND(branch, available));
|
|
4584
|
+
}
|
|
4585
|
+
return match;
|
|
4586
|
+
}
|
|
4587
|
+
async function handleNonInteractiveSingleResume(worktree, prompt) {
|
|
4588
|
+
const hasPrevious = hasClaudeSessionHistory(worktree.path);
|
|
4589
|
+
await executeBatchTasks([worktree], [prompt], 0, [hasPrevious]);
|
|
4590
|
+
}
|
|
4591
|
+
async function handleNonInteractiveBatchResume(filePath, options) {
|
|
4592
|
+
const entries = loadTaskFile(filePath, { branchRequired: true });
|
|
4593
|
+
printSuccess(MESSAGES.RESUME_FOLLOW_UP_FILE_LOADED(entries.length, filePath));
|
|
4594
|
+
const allWorktrees = getProjectWorktrees();
|
|
4595
|
+
const worktrees = [];
|
|
4596
|
+
const tasks = [];
|
|
4597
|
+
const continueFlags = [];
|
|
4598
|
+
for (const entry of entries) {
|
|
4599
|
+
const worktree = resolveWorktreeByBranch(entry.branch, allWorktrees);
|
|
4600
|
+
worktrees.push(worktree);
|
|
4601
|
+
tasks.push(entry.task);
|
|
4602
|
+
continueFlags.push(hasClaudeSessionHistory(worktree.path));
|
|
4603
|
+
}
|
|
4604
|
+
const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
|
|
4605
|
+
logger.info(`resume \u547D\u4EE4\uFF08\u6279\u91CF\u8FFD\u95EE\u6A21\u5F0F\uFF09\u6267\u884C\uFF0C\u4EFB\u52A1\u6570: ${entries.length}\uFF0C\u5E76\u53D1\u6570: ${concurrency || "\u4E0D\u9650\u5236"}`);
|
|
4606
|
+
await executeBatchTasks(worktrees, tasks, concurrency, continueFlags);
|
|
4607
|
+
}
|
|
4471
4608
|
function printBatchResumePreview(worktrees, sessionMap) {
|
|
4472
4609
|
printInfo("\u5373\u5C06\u6062\u590D\u7684\u5206\u652F\uFF1A");
|
|
4473
4610
|
for (const wt of worktrees) {
|
|
@@ -5672,14 +5809,18 @@ function registerInitCommand(program2) {
|
|
|
5672
5809
|
await handleInit(options);
|
|
5673
5810
|
});
|
|
5674
5811
|
initCmd.addCommand(
|
|
5675
|
-
new Cmd("show").description("\u4EA4\u4E92\u5F0F\u67E5\u770B\u548C\u4FEE\u6539\u9879\u76EE\u914D\u7F6E").action(async () => {
|
|
5676
|
-
await handleInitShow();
|
|
5812
|
+
new Cmd("show").description("\u4EA4\u4E92\u5F0F\u67E5\u770B\u548C\u4FEE\u6539\u9879\u76EE\u914D\u7F6E\uFF08\u652F\u6301 --json \u683C\u5F0F\u8F93\u51FA\uFF09").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
|
|
5813
|
+
await handleInitShow(options);
|
|
5677
5814
|
})
|
|
5678
5815
|
);
|
|
5679
5816
|
}
|
|
5680
|
-
async function handleInitShow() {
|
|
5817
|
+
async function handleInitShow(options) {
|
|
5681
5818
|
await runPreChecks({ requireMainWorktree: true, requireProjectConfig: true });
|
|
5682
5819
|
const config2 = requireProjectConfig();
|
|
5820
|
+
if (options.json) {
|
|
5821
|
+
printInitShowAsJson(config2);
|
|
5822
|
+
return;
|
|
5823
|
+
}
|
|
5683
5824
|
logger.info("init show \u547D\u4EE4\u6267\u884C\uFF0C\u8FDB\u5165\u4EA4\u4E92\u5F0F\u9879\u76EE\u914D\u7F6E");
|
|
5684
5825
|
const { key, newValue } = await interactiveConfigEditor(
|
|
5685
5826
|
config2,
|
|
@@ -5690,6 +5831,9 @@ async function handleInitShow() {
|
|
|
5690
5831
|
saveProjectConfig(updatedConfig);
|
|
5691
5832
|
printSuccess(MESSAGES.INIT_SET_SUCCESS(key, String(newValue)));
|
|
5692
5833
|
}
|
|
5834
|
+
function printInitShowAsJson(config2) {
|
|
5835
|
+
console.log(safeStringify({ ...config2 }));
|
|
5836
|
+
}
|
|
5693
5837
|
async function handleInit(options) {
|
|
5694
5838
|
await runPreChecks({ requireMainWorktree: true });
|
|
5695
5839
|
const existingConfig = loadProjectConfig();
|
|
@@ -5717,7 +5861,16 @@ function registerHomeCommand(program2) {
|
|
|
5717
5861
|
});
|
|
5718
5862
|
}
|
|
5719
5863
|
async function handleHome() {
|
|
5720
|
-
|
|
5864
|
+
if (!isInsideGitRepo()) {
|
|
5865
|
+
printError(MESSAGES.NOT_MAIN_WORKTREE);
|
|
5866
|
+
return;
|
|
5867
|
+
}
|
|
5868
|
+
if (!isMainWorktree()) {
|
|
5869
|
+
const mainPath = getMainWorktreePath();
|
|
5870
|
+
printHint(MESSAGES.HOME_NOT_IN_MAIN_WORKTREE(mainPath));
|
|
5871
|
+
return;
|
|
5872
|
+
}
|
|
5873
|
+
await runPreChecks({ requireHead: true, requireProjectConfig: true, requireMainBranchExists: true });
|
|
5721
5874
|
const mainBranch = getMainWorkBranch();
|
|
5722
5875
|
const currentBranch = getCurrentBranch();
|
|
5723
5876
|
if (currentBranch === mainBranch) {
|
package/dist/postinstall.js
CHANGED
|
@@ -302,7 +302,17 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
302
302
|
/** 批量 resume 非 macOS 平台提示 */
|
|
303
303
|
RESUME_ALL_PLATFORM_UNSUPPORTED: "\u6279\u91CF resume \u76EE\u524D\u4EC5\u652F\u6301 macOS \u5E73\u53F0\uFF08\u901A\u8FC7 AppleScript \u6253\u5F00\u7EC8\u7AEF Tab\uFF09",
|
|
304
304
|
/** 批量 resume 无匹配分支提示 */
|
|
305
|
-
RESUME_ALL_NO_MATCH: (keyword) => `\u672A\u627E\u5230\u4E0E "${keyword}" \u5339\u914D\u7684\u5206\u652F
|
|
305
|
+
RESUME_ALL_NO_MATCH: (keyword) => `\u672A\u627E\u5230\u4E0E "${keyword}" \u5339\u914D\u7684\u5206\u652F`,
|
|
306
|
+
/** --prompt 必须配合 -b 指定目标分支 */
|
|
307
|
+
RESUME_PROMPT_REQUIRES_BRANCH: "--prompt \u5FC5\u987B\u914D\u5408 -b \u6307\u5B9A\u76EE\u6807\u5206\u652F",
|
|
308
|
+
/** --prompt 和 -f 不能同时使用 */
|
|
309
|
+
RESUME_PROMPT_FILE_CONFLICT: "--prompt \u548C -f \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528",
|
|
310
|
+
/** 未找到对应 worktree */
|
|
311
|
+
RESUME_WORKTREE_NOT_FOUND: (branch, available) => `\u672A\u627E\u5230\u5206\u652F "${branch}" \u5BF9\u5E94\u7684 worktree
|
|
312
|
+
\u53EF\u7528\u5206\u652F\uFF1A
|
|
313
|
+
${available.map((b) => ` - ${b}`).join("\n")}`,
|
|
314
|
+
/** 追问文件加载完成 */
|
|
315
|
+
RESUME_FOLLOW_UP_FILE_LOADED: (count, path) => `\u4ECE ${path} \u52A0\u8F7D\u4E86 ${count} \u4E2A\u8FFD\u95EE\u4EFB\u52A1`
|
|
306
316
|
};
|
|
307
317
|
|
|
308
318
|
// src/constants/messages/remove.ts
|
|
@@ -514,7 +524,11 @@ var HOME_MESSAGES = {
|
|
|
514
524
|
/** 已在主工作分支上 */
|
|
515
525
|
HOME_ALREADY_ON_MAIN: (branch) => `\u5DF2\u5728\u4E3B\u5DE5\u4F5C\u5206\u652F ${branch} \u4E0A\uFF0C\u65E0\u9700\u5207\u6362`,
|
|
516
526
|
/** 切换成功 */
|
|
517
|
-
HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}
|
|
527
|
+
HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}`,
|
|
528
|
+
/** 当前在子 worktree,提示用户手动 cd 到主 worktree */
|
|
529
|
+
HOME_NOT_IN_MAIN_WORKTREE: (mainPath) => `\u5F53\u524D\u4E0D\u5728\u4E3B worktree\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u4E3B worktree\uFF1A
|
|
530
|
+
|
|
531
|
+
cd ${mainPath}`
|
|
518
532
|
};
|
|
519
533
|
|
|
520
534
|
// src/constants/messages/tasks.ts
|
package/package.json
CHANGED
package/src/commands/home.ts
CHANGED
|
@@ -5,9 +5,13 @@ import {
|
|
|
5
5
|
ensureOnMainWorkBranch,
|
|
6
6
|
getCurrentBranch,
|
|
7
7
|
getMainWorkBranch,
|
|
8
|
+
isMainWorktree,
|
|
9
|
+
isInsideGitRepo,
|
|
10
|
+
getMainWorktreePath,
|
|
8
11
|
printSuccess,
|
|
9
12
|
printInfo,
|
|
10
|
-
|
|
13
|
+
printHint,
|
|
14
|
+
printError,
|
|
11
15
|
} from '../utils/index.js';
|
|
12
16
|
|
|
13
17
|
/**
|
|
@@ -25,9 +29,27 @@ export function registerHomeCommand(program: Command): void {
|
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* 执行 home 命令:切换回主工作分支
|
|
32
|
+
* 三种场景:
|
|
33
|
+
* 1. 不在 git 仓库中 → 报错
|
|
34
|
+
* 2. 在子 worktree 中 → 输出提示和 cd 命令
|
|
35
|
+
* 3. 在主 worktree 中 → 切换到主工作分支
|
|
28
36
|
*/
|
|
29
37
|
async function handleHome(): Promise<void> {
|
|
30
|
-
|
|
38
|
+
// 场景 1:不在 git 仓库中,直接报错
|
|
39
|
+
if (!isInsideGitRepo()) {
|
|
40
|
+
printError(MESSAGES.NOT_MAIN_WORKTREE);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 场景 2:在子 worktree 中,输出 cd 提示
|
|
45
|
+
if (!isMainWorktree()) {
|
|
46
|
+
const mainPath = getMainWorktreePath();
|
|
47
|
+
printHint(MESSAGES.HOME_NOT_IN_MAIN_WORKTREE(mainPath));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 场景 3:在主 worktree 中,切换到主工作分支
|
|
52
|
+
await runPreChecks({ requireHead: true, requireProjectConfig: true, requireMainBranchExists: true });
|
|
31
53
|
|
|
32
54
|
const mainBranch = getMainWorkBranch();
|
|
33
55
|
const currentBranch = getCurrentBranch();
|
package/src/commands/init.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Command } from 'commander';
|
|
|
2
2
|
import { Command as Cmd } from 'commander';
|
|
3
3
|
import { logger } from '../logger/index.js';
|
|
4
4
|
import { MESSAGES, PROJECT_CONFIG_DEFINITIONS } from '../constants/index.js';
|
|
5
|
-
import type { InitOptions, ProjectConfig } from '../types/index.js';
|
|
5
|
+
import type { InitOptions, InitShowOptions, ProjectConfig } from '../types/index.js';
|
|
6
6
|
import {
|
|
7
7
|
runPreChecks,
|
|
8
8
|
validateHeadExists,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
requireProjectConfig,
|
|
13
13
|
printSuccess,
|
|
14
14
|
interactiveConfigEditor,
|
|
15
|
+
safeStringify,
|
|
15
16
|
} from '../utils/index.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -27,23 +28,31 @@ export function registerInitCommand(program: Command): void {
|
|
|
27
28
|
await handleInit(options);
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
// 注册 show
|
|
31
|
+
// 注册 show 子命令:交互式查看和修改项目配置(支持 --json 格式输出)
|
|
31
32
|
initCmd.addCommand(
|
|
32
33
|
new Cmd('show')
|
|
33
|
-
.description('
|
|
34
|
-
.
|
|
35
|
-
|
|
34
|
+
.description('交互式查看和修改项目配置(支持 --json 格式输出)')
|
|
35
|
+
.option('--json', '以 JSON 格式输出')
|
|
36
|
+
.action(async (options: InitShowOptions) => {
|
|
37
|
+
await handleInitShow(options);
|
|
36
38
|
}),
|
|
37
39
|
);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* 处理 init show 子命令:交互式面板展示和修改项目配置
|
|
44
|
+
* @param {InitShowOptions} options - 子命令选项
|
|
42
45
|
*/
|
|
43
|
-
async function handleInitShow(): Promise<void> {
|
|
46
|
+
async function handleInitShow(options: InitShowOptions): Promise<void> {
|
|
44
47
|
await runPreChecks({ requireMainWorktree: true, requireProjectConfig: true });
|
|
45
48
|
const config = requireProjectConfig();
|
|
46
49
|
|
|
50
|
+
// --json 模式:直接输出 JSON 格式配置,跳过交互式流程
|
|
51
|
+
if (options.json) {
|
|
52
|
+
printInitShowAsJson(config);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
logger.info('init show 命令执行,进入交互式项目配置');
|
|
48
57
|
|
|
49
58
|
const { key, newValue } = await interactiveConfigEditor(
|
|
@@ -59,6 +68,14 @@ async function handleInitShow(): Promise<void> {
|
|
|
59
68
|
printSuccess(MESSAGES.INIT_SET_SUCCESS(key as string, String(newValue)));
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
/**
|
|
72
|
+
* 以 JSON 格式输出项目配置
|
|
73
|
+
* @param {ProjectConfig} config - 项目配置对象
|
|
74
|
+
*/
|
|
75
|
+
function printInitShowAsJson(config: ProjectConfig): void {
|
|
76
|
+
console.log(safeStringify({ ...config }));
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
/**
|
|
63
80
|
* 执行 init 命令的核心逻辑
|
|
64
81
|
* 无论是否已初始化,始终执行设置/切换主工作分支
|