clawt 3.9.6 → 3.9.8
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 +2 -2
- package/dist/index.js +275 -160
- package/dist/postinstall.js +8 -2
- package/docs/config-file.md +1 -1
- package/docs/init.md +3 -2
- package/docs/project-config.md +11 -2
- package/docs/tasks.md +1 -1
- package/package.json +1 -1
- package/src/commands/init.ts +3 -1
- package/src/commands/status.ts +73 -41
- package/src/constants/messages/tasks.ts +3 -2
- package/src/constants/project-config.ts +4 -0
- package/src/constants/tasks-template.ts +1 -1
- package/src/types/projectConfig.ts +2 -0
- package/src/utils/claude.ts +3 -3
- package/src/utils/git-branch.ts +46 -1
- package/src/utils/git-core.ts +22 -1
- package/src/utils/index.ts +6 -2
- package/src/utils/interactive-panel-render.ts +4 -2
- package/src/utils/interactive-panel-state.ts +36 -2
- package/src/utils/interactive-panel.ts +63 -27
- package/src/utils/project-config.ts +37 -2
- package/src/utils/shell.ts +25 -1
- package/tests/unit/commands/init.test.ts +1 -0
- package/tests/unit/commands/status.test.ts +18 -25
- package/tests/unit/utils/project-config.test.ts +50 -0
package/dist/postinstall.js
CHANGED
|
@@ -551,8 +551,10 @@ var TASKS_CMD_MESSAGES = {
|
|
|
551
551
|
TASK_INIT_FILE_EXISTS: (path) => `\u6587\u4EF6\u5DF2\u5B58\u5728: ${path}\uFF0C\u5982\u9700\u8986\u76D6\u8BF7\u5148\u5220\u9664`,
|
|
552
552
|
/** 任务模板生成成功 */
|
|
553
553
|
TASK_INIT_SUCCESS: (path) => `\u2713 \u4EFB\u52A1\u6A21\u677F\u5DF2\u751F\u6210: ${path}`,
|
|
554
|
-
/**
|
|
555
|
-
TASK_INIT_HINT: (path) => `\
|
|
554
|
+
/** 任务模板使用提示(分行列出 run 和 resume 两种用法) */
|
|
555
|
+
TASK_INIT_HINT: (path) => `\u6267\u884C\u4EFB\u52A1:
|
|
556
|
+
clawt run -f ${path} # \u521B\u5EFA worktree \u5E76\u6267\u884C\uFF08\u5206\u652F\u540D\u9700\u4E0D\u5B58\u5728\uFF09
|
|
557
|
+
clawt resume -f ${path} # \u5728\u5DF2\u6709 worktree \u4E2D\u8FFD\u95EE\uFF08\u5206\u652F\u540D\u9700\u5DF2\u5B58\u5728\uFF09`
|
|
556
558
|
};
|
|
557
559
|
|
|
558
560
|
// src/constants/messages/post-create.ts
|
|
@@ -722,6 +724,10 @@ var PROJECT_CONFIG_DEFINITIONS = {
|
|
|
722
724
|
postCreate: {
|
|
723
725
|
defaultValue: void 0,
|
|
724
726
|
description: "worktree \u521B\u5EFA\u540E\u81EA\u52A8\u6267\u884C\u7684\u547D\u4EE4\uFF0C\u7528\u4E8E\u5B89\u88C5\u4F9D\u8D56\u7B49\u521D\u59CB\u5316\u64CD\u4F5C"
|
|
727
|
+
},
|
|
728
|
+
claudeCodeCommand: {
|
|
729
|
+
defaultValue: void 0,
|
|
730
|
+
description: "Claude Code CLI \u542F\u52A8\u6307\u4EE4\uFF08\u672A\u8BBE\u7F6E\u65F6\u56DE\u9000\u5230\u5168\u5C40\u914D\u7F6E\uFF09"
|
|
725
731
|
}
|
|
726
732
|
};
|
|
727
733
|
function deriveDefaultConfig2(definitions) {
|
package/docs/config-file.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
36
36
|
| ------------------ | --------- | --------- | -------------------------------------------------- |
|
|
37
37
|
| `autoDeleteBranch` | `boolean` | `false` | 移除 worktree 时是否自动删除对应本地分支(无需每次确认);merge 成功后是否自动清理 worktree 和分支;run 任务被中断(Ctrl+C)后是否自动清理本次创建的 worktree 和分支 |
|
|
38
|
-
| `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree
|
|
38
|
+
| `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree 中打开交互式界面。可被项目级配置 `claudeCodeCommand` 覆盖(优先级:项目级 > 全局级,详见 [project-config.md](./project-config.md)) |
|
|
39
39
|
| `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
|
|
40
40
|
| `confirmDestructiveOps` | `boolean` | `true` | 执行破坏性操作(reset、validate --clean)前是否提示确认 |
|
|
41
41
|
| `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
|
package/docs/init.md
CHANGED
|
@@ -26,7 +26,7 @@ clawt init show --json
|
|
|
26
26
|
|
|
27
27
|
**功能说明:**
|
|
28
28
|
|
|
29
|
-
初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。`init show` 子命令提供交互式面板,可查看和修改所有项目配置项(如 `validateRunCommand`、`postCreate`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
|
|
29
|
+
初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。`init show` 子命令提供交互式面板,可查看和修改所有项目配置项(如 `validateRunCommand`、`postCreate`、`claudeCodeCommand`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
|
|
30
30
|
|
|
31
31
|
**运行流程(设置模式):**
|
|
32
32
|
|
|
@@ -53,7 +53,7 @@ clawt init show --json
|
|
|
53
53
|
3. **交互式配置编辑**:调用 `interactiveConfigEditor`(`src/utils/config-strategy.ts`),基于 `PROJECT_CONFIG_DEFINITIONS` 构建配置项列表(详见 [project-config.md](./project-config.md))
|
|
54
54
|
- 列出所有项目配置项,显示名称、当前值和描述
|
|
55
55
|
- 用户选择配置项后,根据值类型自动选择输入方式(与全局配置的交互式编辑逻辑一致)
|
|
56
|
-
4.
|
|
56
|
+
4. **持久化修改**:将修改后的值合并到当前配置,经 `normalizeProjectConfig` 归一化处理后写入配置文件(可选字段的空字符串会被移除,等同于未设置)
|
|
57
57
|
5. **输出成功提示**:`✓ 项目配置 <key> 已设置为 <value>`
|
|
58
58
|
|
|
59
59
|
**输出格式:**
|
|
@@ -85,5 +85,6 @@ clawt init show --json
|
|
|
85
85
|
- `init show` 子命令从 JSON 展示改为交互式面板,调用 `interactiveConfigEditor`(`src/utils/config-strategy.ts`)实现通用交互式配置编辑
|
|
86
86
|
- 配置项定义来自 `PROJECT_CONFIG_DEFINITIONS`(`src/constants/project-config.ts`),详见 [项目级配置文档](./project-config.md)
|
|
87
87
|
- 消息常量:`MESSAGES.INIT_SELECT_PROMPT`(选择配置项提示语)、`MESSAGES.INIT_SET_SUCCESS`(修改成功提示),定义在 `src/constants/messages/init.ts`
|
|
88
|
+
- `handleInitShow` 使用 `normalizeProjectConfig` 对修改后的配置进行归一化处理:可选字段(如 `validateRunCommand`、`postCreate`、`claudeCodeCommand`)设为空字符串时自动移除该键,避免 JSON 文件中出现冗余的 `"field": ""` 条目
|
|
88
89
|
|
|
89
90
|
---
|
package/docs/project-config.md
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
{
|
|
19
19
|
"clawtMainWorkBranch": "main",
|
|
20
20
|
"validateRunCommand": "npm test",
|
|
21
|
-
"postCreate": "npm install"
|
|
21
|
+
"postCreate": "npm install",
|
|
22
|
+
"claudeCodeCommand": "claude --model opus"
|
|
22
23
|
}
|
|
23
24
|
```
|
|
24
25
|
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
| `clawtMainWorkBranch` | `string` | 是 | `""` | 项目的主工作分支名,用于 create 时检测当前分支是否为主分支,以及 sync、merge 等命令获取主分支名 |
|
|
28
29
|
| `validateRunCommand` | `string` | 否 | `undefined` | validate 成功后自动执行的命令(作为 `-r` 选项的默认值)。不传 `-r` 时,validate 命令会自动从此项读取 |
|
|
29
30
|
| `postCreate` | `string` | 否 | `undefined` | worktree 创建后自动执行的初始化命令(如安装依赖、生成配置文件、编译资源等)。详见 [post-create-hook.md](./post-create-hook.md) |
|
|
31
|
+
| `claudeCodeCommand` | `string` | 否 | `undefined` | Claude Code CLI 启动指令,项目级覆盖全局配置。未设置时回退到全局配置 `claudeCodeCommand`(详见 [config-file.md](./config-file.md))。优先级:项目级 > 全局级 |
|
|
30
32
|
|
|
31
33
|
#### 配置项定义数据源
|
|
32
34
|
|
|
@@ -48,6 +50,10 @@ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
|
|
|
48
50
|
defaultValue: undefined as unknown as string | undefined,
|
|
49
51
|
description: 'worktree 创建后自动执行的命令,用于安装依赖等初始化操作',
|
|
50
52
|
},
|
|
53
|
+
claudeCodeCommand: {
|
|
54
|
+
defaultValue: undefined as unknown as string | undefined,
|
|
55
|
+
description: 'Claude Code CLI 启动指令(未设置时回退到全局配置)',
|
|
56
|
+
},
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
/** 项目默认配置(从 PROJECT_CONFIG_DEFINITIONS 自动派生) */
|
|
@@ -63,7 +69,7 @@ export const PROJECT_CONFIG_DESCRIPTIONS: Record<keyof Required<ProjectConfig>,
|
|
|
63
69
|
|
|
64
70
|
| 类型 | 说明 |
|
|
65
71
|
| --- | --- |
|
|
66
|
-
| `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)、`validateRunCommand`(可选)和 `
|
|
72
|
+
| `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)、`validateRunCommand`(可选)、`postCreate`(可选)和 `claudeCodeCommand`(可选) |
|
|
67
73
|
| `ProjectConfigItemDefinition<T>` | 单个配置项定义,含 `defaultValue`(默认值)、`description`(描述)、可选 `allowedValues`(枚举值列表,仅对 string 类型有效) |
|
|
68
74
|
| `ProjectConfigDefinitions` | 所有配置项的完整定义映射,键为 `ProjectConfig` 的所有属性名,值为对应的 `ProjectConfigItemDefinition` |
|
|
69
75
|
|
|
@@ -74,6 +80,7 @@ export interface ProjectConfig {
|
|
|
74
80
|
clawtMainWorkBranch: string;
|
|
75
81
|
validateRunCommand?: string;
|
|
76
82
|
postCreate?: string;
|
|
83
|
+
claudeCodeCommand?: string;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export interface ProjectConfigItemDefinition<T> {
|
|
@@ -99,6 +106,8 @@ export type ProjectConfigDefinitions = {
|
|
|
99
106
|
| `requireProjectConfig` | `() => ProjectConfig` | 获取当前项目配置,不存在或缺少 `clawtMainWorkBranch` 时抛出 `ClawtError` |
|
|
100
107
|
| `getMainWorkBranch` | `() => string` | 从项目配置中获取主工作分支名(内部调用 `requireProjectConfig`) |
|
|
101
108
|
| `getValidateRunCommand` | `() => string \| undefined` | 从项目配置中获取 validate 自动执行命令,未配置时返回 `undefined` |
|
|
109
|
+
| `resolveClaudeCodeCommand` | `() => string` | 解析当前项目生效的 Claude Code 启动指令,优先级:项目级配置 > 全局配置 |
|
|
110
|
+
| `normalizeProjectConfig` | `(config: ProjectConfig, key: string, value: unknown) => ProjectConfig` | 归一化项目配置:可选字段的空字符串等同于未设置,从对象中删除该键以保持 JSON 文件整洁 |
|
|
102
111
|
|
|
103
112
|
#### 设置方式
|
|
104
113
|
|
package/docs/tasks.md
CHANGED
|
@@ -46,7 +46,7 @@ clawt tasks init [path]
|
|
|
46
46
|
# 格式说明: 标签外的文本会被忽略,每个任务用 START/END 标签包裹
|
|
47
47
|
#
|
|
48
48
|
# 规则:
|
|
49
|
-
# 1. 每个任务块用
|
|
49
|
+
# 1. 每个任务块用 <START> 和 <END> 标签包裹(实际标签见下方示例)
|
|
50
50
|
# 2. 块内 # branch: <分支名> 声明分支名(使用 -b 参数时可省略)
|
|
51
51
|
# 3. 块内其余行为任务描述(支持多行)
|
|
52
52
|
|
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
printSuccess,
|
|
14
14
|
interactiveConfigEditor,
|
|
15
15
|
safeStringify,
|
|
16
|
+
normalizeProjectConfig,
|
|
16
17
|
} from '../utils/index.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -62,7 +63,8 @@ async function handleInitShow(options: InitShowOptions): Promise<void> {
|
|
|
62
63
|
);
|
|
63
64
|
|
|
64
65
|
// 合并修改后的值并持久化
|
|
65
|
-
const
|
|
66
|
+
const mergedConfig: ProjectConfig = { ...config, [key]: newValue };
|
|
67
|
+
const updatedConfig = normalizeProjectConfig(mergedConfig, key as string, newValue);
|
|
66
68
|
saveProjectConfig(updatedConfig);
|
|
67
69
|
|
|
68
70
|
printSuccess(MESSAGES.INIT_SET_SUCCESS(key as string, String(newValue)));
|
package/src/commands/status.ts
CHANGED
|
@@ -9,11 +9,10 @@ import {
|
|
|
9
9
|
getCurrentBranch,
|
|
10
10
|
isWorkingDirClean,
|
|
11
11
|
getProjectWorktrees,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
getCommitDivergenceAsync,
|
|
13
|
+
getDiffStatAsync,
|
|
14
|
+
getStatusPorcelainAsync,
|
|
14
15
|
getDiffStat,
|
|
15
|
-
hasMergeConflict,
|
|
16
|
-
hasLocalCommits,
|
|
17
16
|
getSnapshotModifiedTime,
|
|
18
17
|
getProjectSnapshotBranches,
|
|
19
18
|
getWorktreeCreatedTime,
|
|
@@ -55,7 +54,7 @@ async function handleStatus(options: StatusOptions): Promise<void> {
|
|
|
55
54
|
return;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
const statusResult = collectStatus();
|
|
57
|
+
const statusResult = await collectStatus();
|
|
59
58
|
|
|
60
59
|
logger.info(`status 命令执行,项目: ${statusResult.main.projectName},共 ${statusResult.totalWorktrees} 个 worktree`);
|
|
61
60
|
|
|
@@ -69,9 +68,10 @@ async function handleStatus(options: StatusOptions): Promise<void> {
|
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
70
|
* 收集项目全局状态信息
|
|
72
|
-
*
|
|
71
|
+
* 各 worktree 的数据通过 Promise.all 并行收集,避免串行阻塞
|
|
72
|
+
* @returns {Promise<StatusResult>} 完整的状态数据
|
|
73
73
|
*/
|
|
74
|
-
export function collectStatus(): StatusResult {
|
|
74
|
+
export async function collectStatus(): Promise<StatusResult> {
|
|
75
75
|
const projectName = getProjectName();
|
|
76
76
|
const currentBranch = getCurrentBranch();
|
|
77
77
|
const isClean = isWorkingDirClean();
|
|
@@ -95,9 +95,11 @@ export function collectStatus(): StatusResult {
|
|
|
95
95
|
deletions,
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
// 各 worktree
|
|
98
|
+
// 各 worktree 详细状态(异步并行收集)
|
|
99
99
|
const worktrees = getProjectWorktrees();
|
|
100
|
-
const worktreeStatuses =
|
|
100
|
+
const worktreeStatuses = await Promise.all(
|
|
101
|
+
worktrees.map((wt) => collectWorktreeDetailedStatusAsync(wt, projectName)),
|
|
102
|
+
);
|
|
101
103
|
|
|
102
104
|
// 未清理的 validate 快照
|
|
103
105
|
const snapshots = collectSnapshots(projectName, worktrees);
|
|
@@ -111,70 +113,100 @@ export function collectStatus(): StatusResult {
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
+
* 异步收集单个 worktree 的详细状态
|
|
117
|
+
* 内部 3 个 git 命令通过 Promise.all 并行执行,每个 worktree 内部也是并行的
|
|
116
118
|
* @param {WorktreeInfo} worktree - worktree 信息
|
|
117
119
|
* @param {string} projectName - 项目名
|
|
118
|
-
* @returns {WorktreeDetailedStatus} 详细状态
|
|
120
|
+
* @returns {Promise<WorktreeDetailedStatus>} 详细状态
|
|
119
121
|
*/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
122
|
+
async function collectWorktreeDetailedStatusAsync(worktree: WorktreeInfo, projectName: string): Promise<WorktreeDetailedStatus> {
|
|
123
|
+
// 3 个 git 命令并行执行:提交差异、工作区状态、diff 统计
|
|
124
|
+
const [divergence, porcelain, diffStat] = await Promise.all([
|
|
125
|
+
countCommitDivergenceAsync(worktree.branch),
|
|
126
|
+
detectStatusPorcelainAsync(worktree.path),
|
|
127
|
+
countDiffStatAsync(worktree.path),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
const changeStatus = detectChangeStatusFromPorcelain(porcelain, divergence.commitsAhead);
|
|
124
131
|
const createdAt = getWorktreeCreatedTime(worktree.path);
|
|
125
132
|
|
|
126
133
|
return {
|
|
127
134
|
path: worktree.path,
|
|
128
135
|
branch: worktree.branch,
|
|
129
136
|
changeStatus,
|
|
130
|
-
commitsAhead,
|
|
131
|
-
commitsBehind,
|
|
137
|
+
commitsAhead: divergence.commitsAhead,
|
|
138
|
+
commitsBehind: divergence.commitsBehind,
|
|
132
139
|
snapshotTime: resolveSnapshotTime(projectName, worktree.branch),
|
|
133
|
-
insertions,
|
|
134
|
-
deletions,
|
|
140
|
+
insertions: diffStat.insertions,
|
|
141
|
+
deletions: diffStat.deletions,
|
|
135
142
|
createdAt,
|
|
136
143
|
};
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
/**
|
|
140
|
-
*
|
|
147
|
+
* 从 porcelain 输出判断变更状态
|
|
141
148
|
* 优先级:冲突 > 未提交 > 已提交 > 干净
|
|
142
|
-
* @param {
|
|
149
|
+
* @param {string} porcelain - git status --porcelain 输出
|
|
150
|
+
* @param {number} commitsAhead - 领先提交数
|
|
143
151
|
* @returns {WorktreeDetailedStatus['changeStatus']} 变更状态
|
|
144
152
|
*/
|
|
145
|
-
function
|
|
153
|
+
function detectChangeStatusFromPorcelain(porcelain: string, commitsAhead: number): WorktreeDetailedStatus['changeStatus'] {
|
|
154
|
+
// 检测合并冲突(UU/AA/DD/DU/UD/AU/UA 开头的行)
|
|
155
|
+
const hasConflict = porcelain.split('\n').some((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line));
|
|
156
|
+
if (hasConflict) {
|
|
157
|
+
return 'conflict';
|
|
158
|
+
}
|
|
159
|
+
// 检测未提交修改(porcelain 非空即有未提交变更)
|
|
160
|
+
if (porcelain !== '') {
|
|
161
|
+
return 'uncommitted';
|
|
162
|
+
}
|
|
163
|
+
// 用 commitsAhead > 0 判断是否有本地提交
|
|
164
|
+
if (commitsAhead > 0) {
|
|
165
|
+
return 'committed';
|
|
166
|
+
}
|
|
167
|
+
return 'clean';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 异步获取工作区 porcelain 状态,出错时返回空字符串
|
|
172
|
+
* @param {string} worktreePath - worktree 目录路径
|
|
173
|
+
* @returns {Promise<string>} porcelain 格式输出
|
|
174
|
+
*/
|
|
175
|
+
async function detectStatusPorcelainAsync(worktreePath: string): Promise<string> {
|
|
146
176
|
try {
|
|
147
|
-
|
|
148
|
-
return 'conflict';
|
|
149
|
-
}
|
|
150
|
-
if (!isWorkingDirClean(worktree.path)) {
|
|
151
|
-
return 'uncommitted';
|
|
152
|
-
}
|
|
153
|
-
if (hasLocalCommits(worktree.branch)) {
|
|
154
|
-
return 'committed';
|
|
155
|
-
}
|
|
156
|
-
return 'clean';
|
|
177
|
+
return await getStatusPorcelainAsync(worktreePath);
|
|
157
178
|
} catch {
|
|
158
|
-
return '
|
|
179
|
+
return '';
|
|
159
180
|
}
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
/**
|
|
163
|
-
*
|
|
184
|
+
* 异步统计分支与主分支的提交差异(领先/落后数)
|
|
164
185
|
* @param {string} branchName - 分支名
|
|
165
|
-
* @returns {{ commitsAhead: number; commitsBehind: number }} 领先和落后的提交数
|
|
186
|
+
* @returns {Promise<{ commitsAhead: number; commitsBehind: number }>} 领先和落后的提交数
|
|
166
187
|
*/
|
|
167
|
-
function
|
|
188
|
+
async function countCommitDivergenceAsync(branchName: string): Promise<{ commitsAhead: number; commitsBehind: number }> {
|
|
168
189
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
commitsBehind: getCommitCountBehind(branchName),
|
|
172
|
-
};
|
|
190
|
+
const { ahead, behind } = await getCommitDivergenceAsync(branchName);
|
|
191
|
+
return { commitsAhead: ahead, commitsBehind: behind };
|
|
173
192
|
} catch {
|
|
174
193
|
return { commitsAhead: 0, commitsBehind: 0 };
|
|
175
194
|
}
|
|
176
195
|
}
|
|
177
196
|
|
|
197
|
+
/**
|
|
198
|
+
* 异步统计 worktree 的差异行数
|
|
199
|
+
* @param {string} worktreePath - worktree 目录路径
|
|
200
|
+
* @returns {Promise<{ insertions: number; deletions: number }>} 新增和删除行数
|
|
201
|
+
*/
|
|
202
|
+
async function countDiffStatAsync(worktreePath: string): Promise<{ insertions: number; deletions: number }> {
|
|
203
|
+
try {
|
|
204
|
+
return await getDiffStatAsync(worktreePath);
|
|
205
|
+
} catch {
|
|
206
|
+
return { insertions: 0, deletions: 0 };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
178
210
|
/**
|
|
179
211
|
* 统计 worktree 的差异行数
|
|
180
212
|
* @param {string} worktreePath - worktree 路径
|
|
@@ -4,6 +4,7 @@ export const TASKS_CMD_MESSAGES = {
|
|
|
4
4
|
TASK_INIT_FILE_EXISTS: (path: string) => `文件已存在: ${path},如需覆盖请先删除`,
|
|
5
5
|
/** 任务模板生成成功 */
|
|
6
6
|
TASK_INIT_SUCCESS: (path: string) => `✓ 任务模板已生成: ${path}`,
|
|
7
|
-
/**
|
|
8
|
-
TASK_INIT_HINT: (path: string) =>
|
|
7
|
+
/** 任务模板使用提示(分行列出 run 和 resume 两种用法) */
|
|
8
|
+
TASK_INIT_HINT: (path: string) =>
|
|
9
|
+
`执行任务:\n clawt run -f ${path} # 创建 worktree 并执行(分支名需不存在)\n clawt resume -f ${path} # 在已有 worktree 中追问(分支名需已存在)`,
|
|
9
10
|
} as const;
|
|
@@ -17,6 +17,10 @@ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
|
|
|
17
17
|
defaultValue: undefined as unknown as string | undefined,
|
|
18
18
|
description: 'worktree 创建后自动执行的命令,用于安装依赖等初始化操作',
|
|
19
19
|
},
|
|
20
|
+
claudeCodeCommand: {
|
|
21
|
+
defaultValue: undefined as unknown as string | undefined,
|
|
22
|
+
description: 'Claude Code CLI 启动指令(未设置时回退到全局配置)',
|
|
23
|
+
},
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
/**
|
|
@@ -11,7 +11,7 @@ export const TASK_TEMPLATE_CONTENT = `# Clawt 任务文件
|
|
|
11
11
|
# 格式说明: 标签外的文本会被忽略,每个任务用 START/END 标签包裹
|
|
12
12
|
#
|
|
13
13
|
# 规则:
|
|
14
|
-
# 1. 每个任务块用
|
|
14
|
+
# 1. 每个任务块用 <START> 和 <END> 标签包裹(实际标签见下方示例)
|
|
15
15
|
# 2. 块内 # branch: <分支名> 声明分支名(使用 -b 参数时可省略)
|
|
16
16
|
# 3. 块内其余行为任务描述(支持多行)
|
|
17
17
|
|
package/src/utils/claude.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { ClawtError } from '../errors/index.js';
|
|
5
5
|
import { APPEND_SYSTEM_PROMPT, CLAUDE_PROJECTS_DIR } from '../constants/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { resolveClaudeCodeCommand } from './project-config.js';
|
|
7
7
|
import { printInfo, printWarning } from './formatter.js';
|
|
8
8
|
import { openCommandInNewTerminalTab } from './terminal.js';
|
|
9
9
|
import type { WorktreeInfo } from '../types/index.js';
|
|
@@ -51,7 +51,7 @@ interface LaunchClaudeOptions {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function launchInteractiveClaude(worktree: WorktreeInfo, options: LaunchClaudeOptions = {}): void {
|
|
54
|
-
const commandStr =
|
|
54
|
+
const commandStr = resolveClaudeCodeCommand();
|
|
55
55
|
const parts = commandStr.split(/\s+/).filter(Boolean);
|
|
56
56
|
const cmd = parts[0];
|
|
57
57
|
const args = [
|
|
@@ -107,7 +107,7 @@ function escapeShellSingleQuote(str: string): string {
|
|
|
107
107
|
* @returns {string} 完整的 shell 命令字符串
|
|
108
108
|
*/
|
|
109
109
|
export function buildClaudeCommand(worktree: WorktreeInfo, hasPreviousSession: boolean): string {
|
|
110
|
-
const commandStr =
|
|
110
|
+
const commandStr = resolveClaudeCodeCommand();
|
|
111
111
|
const systemPrompt = APPEND_SYSTEM_PROMPT;
|
|
112
112
|
|
|
113
113
|
const escapedPath = escapeShellSingleQuote(worktree.path);
|
package/src/utils/git-branch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execCommand } from './shell.js';
|
|
1
|
+
import { execCommand, execCommandAsync } from './shell.js';
|
|
2
2
|
import { logger } from '../logger/index.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -68,6 +68,51 @@ export function getCommitCountBehind(branchName: string, cwd?: string): number {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* 解析 git rev-list --left-right --count 的输出
|
|
73
|
+
* 输出格式:<left_count>\t<right_count>
|
|
74
|
+
* left = HEAD 侧独有提交数(即 behind),right = branch 侧独有提交数(即 ahead)
|
|
75
|
+
* @param {string} output - rev-list 命令输出
|
|
76
|
+
* @returns {{ ahead: number; behind: number }} 领先和落后的提交数
|
|
77
|
+
*/
|
|
78
|
+
function parseDivergenceOutput(output: string): { ahead: number; behind: number } {
|
|
79
|
+
const [leftStr, rightStr] = output.trim().split(/\s+/);
|
|
80
|
+
return { ahead: parseInt(rightStr, 10) || 0, behind: parseInt(leftStr, 10) || 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 获取当前分支与目标分支的双向提交差异
|
|
85
|
+
* 使用单条 git rev-list --left-right --count 命令同时获取领先和落后提交数,
|
|
86
|
+
* 替代分别调用 getCommitCountAhead 和 getCommitCountBehind 两条命令
|
|
87
|
+
* @param {string} branchName - 目标分支名
|
|
88
|
+
* @param {string} [cwd] - 工作目录
|
|
89
|
+
* @returns {{ ahead: number; behind: number }} 领先和落后的提交数
|
|
90
|
+
*/
|
|
91
|
+
export function getCommitDivergence(branchName: string, cwd?: string): { ahead: number; behind: number } {
|
|
92
|
+
try {
|
|
93
|
+
const output = execCommand(`git rev-list --left-right --count HEAD...${branchName}`, { cwd });
|
|
94
|
+
return parseDivergenceOutput(output);
|
|
95
|
+
} catch {
|
|
96
|
+
return { ahead: 0, behind: 0 };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 异步获取当前分支与目标分支的双向提交差异
|
|
102
|
+
* 用于并行收集多个 worktree 的提交差异时避免串行阻塞
|
|
103
|
+
* @param {string} branchName - 目标分支名
|
|
104
|
+
* @param {string} [cwd] - 工作目录
|
|
105
|
+
* @returns {Promise<{ ahead: number; behind: number }>} 领先和落后的提交数
|
|
106
|
+
*/
|
|
107
|
+
export async function getCommitDivergenceAsync(branchName: string, cwd?: string): Promise<{ ahead: number; behind: number }> {
|
|
108
|
+
try {
|
|
109
|
+
const output = await execCommandAsync(`git rev-list --left-right --count HEAD...${branchName}`, { cwd });
|
|
110
|
+
return parseDivergenceOutput(output);
|
|
111
|
+
} catch {
|
|
112
|
+
return { ahead: 0, behind: 0 };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
71
116
|
/**
|
|
72
117
|
* 获取当前分支名
|
|
73
118
|
* @param {string} [cwd] - 工作目录
|
package/src/utils/git-core.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { execSync, execFileSync } from 'node:child_process';
|
|
3
|
-
import { execCommand, execCommandWithInput } from './shell.js';
|
|
3
|
+
import { execCommand, execCommandAsync, execCommandWithInput } from './shell.js';
|
|
4
4
|
import { logger } from '../logger/index.js';
|
|
5
5
|
import { EXEC_MAX_BUFFER, AUTO_SAVE_COMMIT_MESSAGE_PREFIX } from '../constants/git.js';
|
|
6
6
|
|
|
@@ -81,6 +81,16 @@ export function getStatusPorcelain(cwd?: string): string {
|
|
|
81
81
|
return execCommand('git status --porcelain', { cwd });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* 异步获取工作区状态(git status --porcelain)
|
|
86
|
+
* 用于并行收集多个 worktree 状态时避免串行阻塞
|
|
87
|
+
* @param {string} cwd - 工作目录
|
|
88
|
+
* @returns {Promise<string>} porcelain 格式输出,为空表示干净
|
|
89
|
+
*/
|
|
90
|
+
export async function getStatusPorcelainAsync(cwd?: string): Promise<string> {
|
|
91
|
+
return execCommandAsync('git status --porcelain', { cwd });
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
/**
|
|
85
95
|
* 判断工作区是否干净
|
|
86
96
|
* @param {string} cwd - 工作目录
|
|
@@ -256,6 +266,17 @@ export function getDiffStat(worktreePath: string): { insertions: number; deletio
|
|
|
256
266
|
return parseShortStat(output);
|
|
257
267
|
}
|
|
258
268
|
|
|
269
|
+
/**
|
|
270
|
+
* 异步获取 worktree 中工作区和暂存区的变更统计
|
|
271
|
+
* 用于并行收集多个 worktree 的 diff 统计时避免串行阻塞
|
|
272
|
+
* @param {string} worktreePath - worktree 目录路径
|
|
273
|
+
* @returns {Promise<{ insertions: number; deletions: number }>} 新增和删除行数
|
|
274
|
+
*/
|
|
275
|
+
export async function getDiffStatAsync(worktreePath: string): Promise<{ insertions: number; deletions: number }> {
|
|
276
|
+
const output = await execCommandAsync('git diff --shortstat HEAD', { cwd: worktreePath });
|
|
277
|
+
return parseShortStat(output);
|
|
278
|
+
}
|
|
279
|
+
|
|
259
280
|
/**
|
|
260
281
|
* 获取暂存区相对于 HEAD 的完整 diff(含二进制文件)
|
|
261
282
|
* 注意:返回原始输出不做 trim,保留 patch 格式完整性
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands, runCommandWithStderrCapture, runParallelCommandsWithStderrCapture } from './shell.js';
|
|
1
|
+
export { execCommand, execCommandAsync, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands, runCommandWithStderrCapture, runParallelCommandsWithStderrCapture } from './shell.js';
|
|
2
2
|
export type { ParallelCommandResult, CommandResultWithStderr, ParallelCommandResultWithStderr } from './shell.js';
|
|
3
3
|
export { copyToClipboard } from './clipboard.js';
|
|
4
4
|
export {
|
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
removeWorktreeByPath,
|
|
14
14
|
deleteBranch,
|
|
15
15
|
getStatusPorcelain,
|
|
16
|
+
getStatusPorcelainAsync,
|
|
16
17
|
isWorkingDirClean,
|
|
17
18
|
gitAddAll,
|
|
18
19
|
gitCommit,
|
|
@@ -34,7 +35,10 @@ export {
|
|
|
34
35
|
hasLocalCommits,
|
|
35
36
|
getCommitCountAhead,
|
|
36
37
|
getCommitCountBehind,
|
|
38
|
+
getCommitDivergence,
|
|
39
|
+
getCommitDivergenceAsync,
|
|
37
40
|
getDiffStat,
|
|
41
|
+
getDiffStatAsync,
|
|
38
42
|
gitDiffCachedBinary,
|
|
39
43
|
gitApplyCachedFromStdin,
|
|
40
44
|
getCurrentBranch,
|
|
@@ -82,7 +86,7 @@ export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
|
|
|
82
86
|
export { applyAliases } from './alias.js';
|
|
83
87
|
export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue, interactiveConfigEditor } from './config-strategy.js';
|
|
84
88
|
export { checkForUpdates } from './update-checker.js';
|
|
85
|
-
export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand } from './project-config.js';
|
|
89
|
+
export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand, resolveClaudeCodeCommand, normalizeProjectConfig } from './project-config.js';
|
|
86
90
|
export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
|
|
87
91
|
export { safeStringify } from './json.js';
|
|
88
92
|
export { isNonInteractive, setNonInteractive } from './interactive.js';
|
|
@@ -69,6 +69,7 @@ function buildSeparatorWithHint(cols: number, hint: string): string {
|
|
|
69
69
|
* @param {number} rows - 终端行数
|
|
70
70
|
* @param {number} cols - 终端列数
|
|
71
71
|
* @param {number} countdown - 刷新倒计时秒数
|
|
72
|
+
* @param {PanelLine[]} [cachedPanelLines] - 缓存的面板行列表(传入时复用,不传则重新构建)
|
|
72
73
|
* @returns {string[]} 帧内容行数组
|
|
73
74
|
*/
|
|
74
75
|
export function buildPanelFrame(
|
|
@@ -78,6 +79,7 @@ export function buildPanelFrame(
|
|
|
78
79
|
rows: number,
|
|
79
80
|
cols: number,
|
|
80
81
|
countdown: number,
|
|
82
|
+
cachedPanelLines?: PanelLine[],
|
|
81
83
|
): string[] {
|
|
82
84
|
const lines: string[] = [];
|
|
83
85
|
|
|
@@ -100,8 +102,8 @@ export function buildPanelFrame(
|
|
|
100
102
|
// 底部分隔线(无溢出提示)
|
|
101
103
|
lines.push(buildSeparatorWithHint(cols, ''));
|
|
102
104
|
} else {
|
|
103
|
-
// 构建分组的 worktree
|
|
104
|
-
const panelLines = buildGroupedWorktreeLines(statusResult.worktrees, selectedIndex);
|
|
105
|
+
// 构建分组的 worktree 行列表(优先使用缓存)
|
|
106
|
+
const panelLines = cachedPanelLines ?? buildGroupedWorktreeLines(statusResult.worktrees, selectedIndex);
|
|
105
107
|
|
|
106
108
|
// 判断溢出状态
|
|
107
109
|
const hasOverflowUp = scrollOffset > 0;
|