clawt 3.7.1 → 3.8.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.
@@ -0,0 +1,142 @@
1
+ ### postCreate Hook 机制
2
+
3
+ #### 概述
4
+
5
+ postCreate hook 是在 worktree 创建完成后自动执行的钩子命令,可用于执行任意初始化操作(如安装依赖、生成配置文件、编译资源等)。`create` 和 `run` 命令在创建 worktree 之后,会尝试解析并执行 postCreate hook。
6
+
7
+ hook 以 **fire-and-forget** 模式后台异步并行执行,不阻塞主流程(不 await)。执行结果仅写入日志,不影响后续 Claude Code 的启动或系统提示。
8
+
9
+ #### 配置方式
10
+
11
+ 支持两种配置方式,按优先级从高到低:
12
+
13
+ | 优先级 | 配置方式 | 来源标识 | 说明 |
14
+ | --- | --- | --- | --- |
15
+ | 1 | 项目配置 `postCreate` 字段 | `projectConfig` | 通过 `clawt init show` 交互式设置,或直接编辑 `~/.clawt/projects/<projectName>/config.json` |
16
+ | 2 | 项目仓库 `.clawt/postCreate.sh` 脚本 | `postCreateScript` | 在项目根目录下创建 `.clawt/postCreate.sh` 脚本文件 |
17
+
18
+ **配置方式 1 — 项目配置:**
19
+
20
+ ```bash
21
+ # 通过 init show 交互式设置
22
+ clawt init show
23
+ # 选择 postCreate 配置项,输入命令(如 npm install)
24
+
25
+ # 或直接编辑配置文件
26
+ # ~/.clawt/projects/<projectName>/config.json
27
+ {
28
+ "clawtMainWorkBranch": "main",
29
+ "postCreate": "npm install"
30
+ }
31
+ ```
32
+
33
+ **配置方式 2 — postCreate.sh 脚本:**
34
+
35
+ ```bash
36
+ # 在项目根目录创建脚本
37
+ mkdir -p .clawt
38
+ cat > .clawt/postCreate.sh << 'EOF'
39
+ #!/bin/bash
40
+ npm install
41
+ EOF
42
+ chmod +x .clawt/postCreate.sh
43
+ ```
44
+
45
+ > **自动权限修复**:如果检测到 `.clawt/postCreate.sh` 文件存在但不可执行,会自动尝试 `chmod +x` 添加执行权限。自动 chmod 失败时会打印警告,提示用户手动添加权限。
46
+
47
+ #### 命令行选项
48
+
49
+ `create` 和 `run` 命令均支持 `--post-create` / `--no-post-create` 选项:
50
+
51
+ ```bash
52
+ # 默认行为:自动执行 postCreate hook(如果已配置)
53
+ clawt create -b feat
54
+ clawt run -b feat
55
+
56
+ # 跳过 postCreate hook
57
+ clawt create -b feat --no-post-create
58
+ clawt run -b feat --no-post-create
59
+ ```
60
+
61
+ | 选项 | 默认值 | 说明 |
62
+ | --- | --- | --- |
63
+ | `--post-create` | `true` | 执行 postCreate hook(默认开启) |
64
+ | `--no-post-create` | — | 跳过 postCreate hook |
65
+
66
+ #### 执行流程
67
+
68
+ ```
69
+ 创建 worktree 完成
70
+
71
+ --no-post-create? ── 是 ──→ 跳过,打印提示,直接返回
72
+ ↓ 否
73
+ 解析 hook 配置
74
+
75
+ 有配置? ── 否 ──→ 打印"未配置",直接返回
76
+ ↓ 是
77
+ 打印 hook 来源 + 后台执行提示
78
+
79
+ fire-and-forget:异步并行启动所有 worktree 的 hook
80
+ (spawn + shell: true + stdio: 'ignore',不阻塞主流程)
81
+
82
+ 主流程继续执行(输出日志、启动 Claude Code 等)
83
+
84
+ 后台 hook 完成后写入日志汇总(成功数 + 失败数)
85
+ ```
86
+
87
+ #### 解析优先级(resolvePostCreateHook)
88
+
89
+ 解析函数 `resolvePostCreateHook()`(位于 `src/hooks/post-create.ts`)按以下优先级查找 hook 配置:
90
+
91
+ 1. **项目配置**:读取 `loadProjectConfig()` 的 `postCreate` 字段,非空则使用
92
+ 2. **postCreate.sh 脚本**:检查主 worktree 路径下的 `.clawt/postCreate.sh` 是否存在
93
+
94
+ 两者都不存在时返回 `null`,表示无 hook 配置。
95
+
96
+ #### 执行细节
97
+
98
+ - **执行方式**:通过 `spawn(hook.command, { cwd: worktree.path, stdio: 'ignore', shell: true })` 在每个 worktree 目录下异步并行执行(`Promise.all`)
99
+ - **失败处理**:单个 worktree 的 hook 执行失败(非零退出码或异常)仅写入日志,不阻塞其他 worktree 和后续流程
100
+ - **结果汇总**:后台执行完毕后通过 `.then()` 回调写入日志汇总(成功数 + 失败数)
101
+ - **返回值**:`runPostCreateHooks()` 返回 `void`——以 fire-and-forget 模式后台执行,不等待结果
102
+
103
+ #### 系统提示
104
+
105
+ Claude Code 启动时统一使用 `APPEND_SYSTEM_PROMPT` 常量(定义在 `src/constants/config.ts`)作为 `--append-system-prompt` 参数值,内容为通用的 worktree 目录提示,不因 hook 执行结果而变化。
106
+
107
+ #### 相关类型定义
108
+
109
+ 类型定义位于 `src/types/postCreateHook.ts`:
110
+
111
+ | 类型 | 说明 |
112
+ | --- | --- |
113
+ | `PostCreateHookSource` | hook 来源枚举类型:`'projectConfig'` \| `'postCreateScript'` |
114
+ | `PostCreateHookResult` | 单个 worktree 的 hook 执行结果,包含 `worktreePath`、`branch`、`success`、`source`、`error?` |
115
+ | `ResolvedHook` | hook 解析结果,包含 `command`(待执行命令)和 `source`(来源) |
116
+
117
+ #### 提示消息
118
+
119
+ 消息常量定义在 `src/constants/messages/post-create.ts`:
120
+
121
+ | 消息 | 说明 |
122
+ | --- | --- |
123
+ | `HOOK_SKIPPED` | `--no-post-create` 跳过时的提示 |
124
+ | `HOOK_NOT_CONFIGURED` | 未配置 hook 时的提示 |
125
+ | `HOOK_SOURCE_INFO(source)` | hook 来源信息 |
126
+ | `HOOK_EXECUTING(branch, command)` | hook 开始执行提示 |
127
+ | `HOOK_SUCCESS(branch)` | hook 执行成功提示 |
128
+ | `HOOK_FAILED(branch, error)` | hook 执行失败警告 |
129
+ | `HOOK_SUMMARY(succeeded, failed)` | hook 执行汇总 |
130
+ | `HOOK_BACKGROUND_START(count, command)` | hook 后台执行中提示(worktree 数量和命令) |
131
+ | `POST_CREATE_SCRIPT_AUTO_CHMOD(path)` | postCreate.sh 自动添加执行权限提示 |
132
+ | `POST_CREATE_SCRIPT_NOT_EXECUTABLE(path)` | postCreate.sh 不可执行且自动 chmod 失败时的降级提示 |
133
+
134
+ #### 实现要点
135
+
136
+ - hook 解析和执行逻辑位于 `src/hooks/post-create.ts`,通过 `src/hooks/index.ts` 导出
137
+ - 入口函数 `runPostCreateHooks` 由 `src/utils/index.ts` 统一导出
138
+ - `create` 命令在 `createWorktrees()` 之后调用 `runPostCreateHooks(worktrees, !options.postCreate)`(fire-and-forget,不 await)
139
+ - `run` 命令在 `createWorktrees()` / `createWorktreesByBranches()` 之后调用 `runPostCreateHooks(worktrees, !options.postCreate)`(fire-and-forget,不 await)
140
+ - hook 内部通过 `executePostCreateHooks()` 并行启动所有 worktree 的子进程,`.then()` 回调写入日志汇总
141
+
142
+ ---
@@ -17,7 +17,8 @@
17
17
  ```json
18
18
  {
19
19
  "clawtMainWorkBranch": "main",
20
- "validateRunCommand": "npm test"
20
+ "validateRunCommand": "npm test",
21
+ "postCreate": "npm install"
21
22
  }
22
23
  ```
23
24
 
@@ -25,6 +26,7 @@
25
26
  | --- | --- | --- | --- | --- |
26
27
  | `clawtMainWorkBranch` | `string` | 是 | `""` | 项目的主工作分支名,用于 create 时检测当前分支是否为主分支,以及 sync、merge 等命令获取主分支名 |
27
28
  | `validateRunCommand` | `string` | 否 | `undefined` | validate 成功后自动执行的命令(作为 `-r` 选项的默认值)。不传 `-r` 时,validate 命令会自动从此项读取 |
29
+ | `postCreate` | `string` | 否 | `undefined` | worktree 创建后自动执行的初始化命令(如安装依赖、生成配置文件、编译资源等)。详见 [post-create-hook.md](./post-create-hook.md) |
28
30
 
29
31
  #### 配置项定义数据源
30
32
 
@@ -42,6 +44,10 @@ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
42
44
  defaultValue: undefined as unknown as string | undefined,
43
45
  description: 'validate 成功后自动执行的命令(-r 的默认值)',
44
46
  },
47
+ postCreate: {
48
+ defaultValue: undefined as unknown as string | undefined,
49
+ description: 'worktree 创建后自动执行的命令,用于安装依赖等初始化操作',
50
+ },
45
51
  };
46
52
 
47
53
  /** 项目默认配置(从 PROJECT_CONFIG_DEFINITIONS 自动派生) */
@@ -57,7 +63,7 @@ export const PROJECT_CONFIG_DESCRIPTIONS: Record<keyof Required<ProjectConfig>,
57
63
 
58
64
  | 类型 | 说明 |
59
65
  | --- | --- |
60
- | `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)和 `validateRunCommand`(可选) |
66
+ | `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)、`validateRunCommand`(可选)和 `postCreate`(可选) |
61
67
  | `ProjectConfigItemDefinition<T>` | 单个配置项定义,含 `defaultValue`(默认值)、`description`(描述)、可选 `allowedValues`(枚举值列表,仅对 string 类型有效) |
62
68
  | `ProjectConfigDefinitions` | 所有配置项的完整定义映射,键为 `ProjectConfig` 的所有属性名,值为对应的 `ProjectConfigItemDefinition` |
63
69
 
@@ -67,6 +73,7 @@ export const PROJECT_CONFIG_DESCRIPTIONS: Record<keyof Required<ProjectConfig>,
67
73
  export interface ProjectConfig {
68
74
  clawtMainWorkBranch: string;
69
75
  validateRunCommand?: string;
76
+ postCreate?: string;
70
77
  }
71
78
 
72
79
  export interface ProjectConfigItemDefinition<T> {
package/docs/run.md CHANGED
@@ -24,6 +24,7 @@ clawt run -b <branchName>
24
24
  | `-f` | 否 | 从任务文件读取任务列表(与 `--tasks` 互斥) |
25
25
  | `-c` | 否 | 最大并发数,`0` 表示不限制 |
26
26
  | `--dry-run` | 否 | 试运行模式,仅输出预览信息不实际执行 |
27
+ | `--post-create` | 否 | 执行 postCreate hook(默认开启,`--no-post-create` 跳过)。详见 [post-create-hook.md](./post-create-hook.md) |
27
28
 
28
29
  **互斥约束:**
29
30
 
@@ -73,21 +74,24 @@ clawt run -b <branchName>
73
74
  1. 调用 `loadTaskFile(options.file)` 读取解析文件
74
75
  2. **有 `-b` 参数**:忽略文件中的分支名,用 `-b` 值自动编号创建 worktree(`createWorktrees(branch, count)`)
75
76
  3. **无 `-b` 参数**:使用文件中每个任务的独立分支名,先 `sanitizeBranchName` 清理后调用 `createWorktreesByBranches(branches)`
76
- 4. 调用 `executeBatchTasks(worktrees, tasks, concurrency)` 执行
77
+ 4. **执行 postCreate hook**:调用 `runPostCreateHooks(worktrees, !options.postCreate)`,以 fire-and-forget 模式后台异步并行执行,不阻塞后续流程。详见 [post-create-hook.md](./post-create-hook.md)
78
+ 5. 调用 `executeBatchTasks(worktrees, tasks, concurrency)` 执行
77
79
 
78
80
  #### --tasks 模式运行流程
79
81
 
80
- 1. 若传了 `--tasks`,解析得到任务数组 `tasks[]`;若未传,先检测分支是否已存在(已存在则提示使用 `clawt resume -b <branchName>` 恢复会话),然后创建单个 worktree 并启动 Claude Code 交互式界面(流程结束,不进入后续并行执行阶段)
82
+ 1. 若传了 `--tasks`,解析得到任务数组 `tasks[]`;若未传,先检测分支是否已存在(已存在则提示使用 `clawt resume -b <branchName>` 恢复会话),然后创建单个 worktree,执行 postCreate hook(fire-and-forget)后启动 Claude Code 交互式界面(`launchInteractiveClaude(worktree)`),流程结束,不进入后续并行执行阶段
81
83
  2. `n = tasks.length`
82
84
  3. 按照 **5.1** 的流程创建 `n` 个 worktree
83
- 4. 通过公共函数 `executeBatchTasks`(`src/utils/task-executor.ts`)启动批量任务执行,该函数负责进度面板渲染、SIGINT 中断处理、并发控制和汇总输出。对每个 worktree 并行启动 Claude Code CLI:
85
+ 4. **执行 postCreate hook**:调用 `runPostCreateHooks(worktrees, !options.postCreate)`,以 fire-and-forget 模式后台异步并行执行,不阻塞后续流程。详见 [post-create-hook.md](./post-create-hook.md)
86
+ 5. 通过公共函数 `executeBatchTasks`(`src/utils/task-executor.ts`)启动批量任务执行,该函数负责进度面板渲染、SIGINT 中断处理、并发控制和汇总输出。对每个 worktree 并行启动 Claude Code CLI:
84
87
  ```bash
85
88
  cd ~/.clawt/worktrees/<project>/<branchName>-<i>
86
- claude -p "<tasks[i]>" --output-format stream-json --verbose --permission-mode bypassPermissions
89
+ claude -p "<tasks[i]>" --output-format stream-json --verbose --permission-mode bypassPermissions --append-system-prompt "<系统提示>"
87
90
  ```
91
+ 其中 `--append-system-prompt` 使用统一的 `APPEND_SYSTEM_PROMPT` 常量(定义在 `src/constants/config.ts`)。
88
92
  使用 `stream-json` 格式可实时获取 Claude Code 的流式事件(工具调用、文本输出、最终结果),用于在进度面板中显示每个任务的实时活动描述和结果预览。流式事件解析由 `src/utils/stream-parser.ts` 负责。
89
- 5. 进入**事件监听通知**阶段(见 [5.3](#53-任务完成通知机制))
90
- 6. **中断处理(Ctrl+C / SIGINT)**
93
+ 6. 进入**事件监听通知**阶段(见 [5.3](#53-任务完成通知机制))
94
+ 7. **中断处理(Ctrl+C / SIGINT)**
91
95
  - 监听 `SIGINT` 信号,用户按下 Ctrl+C 时触发
92
96
  - 向所有正在运行的 Claude Code 子进程发送 `SIGTERM` 终止信号
93
97
  - 等待所有子进程退出后,进入清理流程:
package/docs/spec.md CHANGED
@@ -37,6 +37,7 @@
37
37
  - [5.20 切换回主工作分支](./home.md)
38
38
  - [5.21 将验证分支修改覆盖回目标 Worktree](./cover-validate.md)
39
39
  - [5.22 任务文件管理](./tasks.md)
40
+ - [5.23 postCreate Hook 机制](./post-create-hook.md)
40
41
  - [6. 验证架构规则](#6-验证架构规则)
41
42
  - [7. 错误处理规范](#7-错误处理规范)
42
43
  - [8. 非功能性需求](#8-非功能性需求)
@@ -201,7 +202,7 @@ export const VALIDATE_BRANCH_PREFIX = 'clawt-validate-';
201
202
 
202
203
  ### 2.6 项目级配置
203
204
 
204
- 每个 Git 项目独立的 clawt 配置,存放在 `~/.clawt/projects/<projectName>/config.json`。包含项目的主工作分支名(`clawtMainWorkBranch`)、validate 自动运行命令(`validateRunCommand`)等配置项。通过 `clawt init` 命令设置,核心命令执行前会校验该配置是否存在。
205
+ 每个 Git 项目独立的 clawt 配置,存放在 `~/.clawt/projects/<projectName>/config.json`。包含项目的主工作分支名(`clawtMainWorkBranch`)、validate 自动运行命令(`validateRunCommand`)、worktree 创建后自动执行的初始化命令(`postCreate`)等配置项。通过 `clawt init` 命令设置,核心命令执行前会校验该配置是否存在。
205
206
 
206
207
  详细的配置项列表、类型定义、工具函数和设置方式见 [项目级配置文档](./project-config.md)。
207
208
 
@@ -344,6 +345,7 @@ async function interactiveConfigEditor<T extends object>(
344
345
  - [5.20 切换回主工作分支](./home.md)
345
346
  - [5.21 将验证分支修改覆盖回目标 Worktree](./cover-validate.md)
346
347
  - [5.22 任务文件管理](./tasks.md)
348
+ - [5.23 postCreate Hook 机制](./post-create-hook.md)
347
349
 
348
350
  ---
349
351
 
@@ -394,6 +396,7 @@ async function interactiveConfigEditor<T extends object>(
394
396
  - Worktree 创建为串行执行(Git worktree 不支持并行写入)
395
397
  - Claude Code 任务为并行执行(各自独立进程)
396
398
  - 任务完成检测:监听子进程 `close` 事件,事件驱动
399
+ - 命令执行缓冲区:所有 `execSync` / `execFileSync` 调用统一使用 `EXEC_MAX_BUFFER`(200MB,定义在 `src/constants/git.ts`),避免大分支 diff 输出超过 Node.js 默认 1MB 限制触发 `ENOBUFS` 错误
397
400
 
398
401
  ### 8.2 兼容性
399
402
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.7.1",
3
+ "version": "3.8.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,6 +11,7 @@ import {
11
11
  printSuccess,
12
12
  printInfo,
13
13
  printSeparator,
14
+ runPostCreateHooks,
14
15
  } from '../utils/index.js';
15
16
 
16
17
  /**
@@ -23,6 +24,7 @@ export function registerCreateCommand(program: Command): void {
23
24
  .description('批量创建 worktree 及对应分支(含验证分支)')
24
25
  .requiredOption('-b, --branch <branchName>', '分支名')
25
26
  .option('-n, --number <count>', '创建数量', '1')
27
+ .option('--post-create', '执行 postCreate hook(默认开启,--no-post-create 跳过)', true)
26
28
  .action(async (options: CreateOptions) => {
27
29
  await handleCreate(options);
28
30
  });
@@ -49,6 +51,9 @@ async function handleCreate(options: CreateOptions): Promise<void> {
49
51
 
50
52
  const worktrees = createWorktrees(options.branch, count);
51
53
 
54
+ // 执行 postCreate hook
55
+ runPostCreateHooks(worktrees, !options.postCreate);
56
+
52
57
  printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
53
58
  printInfo('');
54
59
 
@@ -20,6 +20,7 @@ import {
20
20
  parseTasksFromOptions,
21
21
  executeBatchTasks,
22
22
  printDryRunPreview,
23
+ runPostCreateHooks,
23
24
  } from '../utils/index.js';
24
25
 
25
26
  /**
@@ -35,6 +36,7 @@ export function registerRunCommand(program: Command): void {
35
36
  .option('-c, --concurrency <n>', '最大并发数,0 表示不限制')
36
37
  .option('-f, --file <path>', '从任务文件读取任务列表(与 --tasks 互斥)')
37
38
  .option('--dry-run', '预览模式,仅展示任务计划不实际执行')
39
+ .option('--post-create', '执行 postCreate hook(默认开启,--no-post-create 跳过)', true)
38
40
  .action(async (options: RunOptions) => {
39
41
  await handleRun(options);
40
42
  });
@@ -86,6 +88,9 @@ async function handleRunFromFile(options: RunOptions): Promise<void> {
86
88
  worktrees = createWorktreesByBranches(branches);
87
89
  }
88
90
 
91
+ // 执行 postCreate hook(后台异步,不阻塞)
92
+ runPostCreateHooks(worktrees, !options.postCreate);
93
+
89
94
  // 解析并发数
90
95
  const concurrency = parseConcurrency(options.concurrency, getConfigValue('maxConcurrency'));
91
96
 
@@ -176,6 +181,10 @@ async function handleRun(options: RunOptions): Promise<void> {
176
181
 
177
182
  const worktrees = createWorktrees(options.branch, 1);
178
183
  const worktree = worktrees[0];
184
+
185
+ // 执行 postCreate hook(后台异步,不阻塞)
186
+ runPostCreateHooks(worktrees, !options.postCreate);
187
+
179
188
  printSuccess(MESSAGES.WORKTREE_CREATED(1));
180
189
 
181
190
  launchInteractiveClaude(worktree);
@@ -193,5 +202,8 @@ async function handleRun(options: RunOptions): Promise<void> {
193
202
  // 创建 worktree
194
203
  const worktrees = createWorktrees(options.branch, count);
195
204
 
205
+ // 执行 postCreate hook(后台异步,不阻塞)
206
+ runPostCreateHooks(worktrees, !options.postCreate);
207
+
196
208
  await executeBatchTasks(worktrees, tasks, concurrency);
197
209
  }
@@ -3,7 +3,7 @@ import { VALID_TERMINAL_APPS } from './terminal.js';
3
3
 
4
4
  /** Claude Code 系统约束提示 */
5
5
  export const APPEND_SYSTEM_PROMPT =
6
- 'Currently, you are in the git worktree directory. There are no dependencies. To save disk space and improve the speed of task completion, installing dependencies is prohibited.';
6
+ 'Currently, you are in the git worktree directory.';
7
7
 
8
8
  /**
9
9
  * 配置项完整定义(单一数据源)
@@ -17,6 +17,7 @@ import { INIT_MESSAGES } from './init.js';
17
17
  import { COVER_VALIDATE_MESSAGES } from './cover-validate.js';
18
18
  import { HOME_MESSAGES } from './home.js';
19
19
  import { TASKS_CMD_MESSAGES } from './tasks.js';
20
+ import { POST_CREATE_MESSAGES } from './post-create.js';
20
21
  import { PANEL_FOOTER_SHORTCUTS, PANEL_FOOTER_COUNTDOWN, PANEL_OVERFLOW_DOWN_HINT, PANEL_OVERFLOW_UP_HINT, PANEL_SNAPSHOT_SUMMARY, PANEL_NO_WORKTREES as PANEL_NO_WORKTREES_MSG, PANEL_PRESS_ENTER_TO_RETURN, PANEL_NOT_TTY, PANEL_TITLE, PANEL_CONFIGURED_BRANCH, PANEL_CONFIGURED_BRANCH_DELETED, PANEL_CONFIGURED_BRANCH_MISMATCH, PANEL_NOT_INITIALIZED, PANEL_UNKNOWN_DATE, PANEL_SYNCED_WITH_MAIN, PANEL_COMMITS_AHEAD, PANEL_COMMITS_BEHIND } from './interactive-panel.js';
21
22
 
22
23
  export { CONFIG_ALIAS_DISABLED_HINT };
@@ -46,4 +47,5 @@ export const MESSAGES = {
46
47
  ...COVER_VALIDATE_MESSAGES,
47
48
  ...HOME_MESSAGES,
48
49
  ...TASKS_CMD_MESSAGES,
50
+ ...POST_CREATE_MESSAGES,
49
51
  } as const;
@@ -0,0 +1,29 @@
1
+ /** postCreate hook 相关提示消息 */
2
+ export const POST_CREATE_MESSAGES = {
3
+ /** hook 执行跳过(--no-post-create) */
4
+ HOOK_SKIPPED: '已跳过 postCreate hook(--no-post-create)',
5
+ /** 无 hook 配置 */
6
+ HOOK_NOT_CONFIGURED: '未配置 postCreate hook,跳过',
7
+ /** hook 来源提示 */
8
+ HOOK_SOURCE_INFO: (source: string) => `postCreate hook 来源: ${source}`,
9
+ /** hook 开始执行 */
10
+ HOOK_EXECUTING: (branch: string, command: string) =>
11
+ `[${branch}] 正在执行 postCreate hook: ${command}`,
12
+ /** hook 执行成功 */
13
+ HOOK_SUCCESS: (branch: string) => `[${branch}] postCreate hook 执行成功`,
14
+ /** hook 执行失败 */
15
+ HOOK_FAILED: (branch: string, error: string) =>
16
+ `[${branch}] postCreate hook 执行失败: ${error}`,
17
+ /** hook 执行汇总 */
18
+ HOOK_SUMMARY: (succeeded: number, failed: number) =>
19
+ `postCreate hook 执行完成: ${succeeded} 成功, ${failed} 失败`,
20
+ /** hook 后台执行中提示 */
21
+ HOOK_BACKGROUND_START: (count: number, command: string) =>
22
+ `postCreate hook 正在后台执行 (${count} 个 worktree): ${command}`,
23
+ /** postCreate.sh 自动添加执行权限 */
24
+ POST_CREATE_SCRIPT_AUTO_CHMOD: (path: string) =>
25
+ `${path} 不可执行,已自动添加执行权限`,
26
+ /** postCreate.sh 不可执行(自动 chmod 失败时降级提示) */
27
+ POST_CREATE_SCRIPT_NOT_EXECUTABLE: (path: string) =>
28
+ `检测到 ${path} 但不可执行,自动添加权限失败,请手动执行 chmod +x ${path}`,
29
+ } as const;
@@ -13,6 +13,10 @@ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
13
13
  defaultValue: undefined as unknown as string | undefined,
14
14
  description: 'validate 成功后自动执行的命令(-r 的默认值)',
15
15
  },
16
+ postCreate: {
17
+ defaultValue: undefined as unknown as string | undefined,
18
+ description: 'worktree 创建后自动执行的命令,用于安装依赖等初始化操作',
19
+ },
16
20
  };
17
21
 
18
22
  /**
@@ -0,0 +1 @@
1
+ export { resolvePostCreateHook, executePostCreateHooks, runPostCreateHooks } from './post-create.js';
@@ -0,0 +1,198 @@
1
+ import { existsSync, accessSync, chmodSync, constants as fsConstants } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ import { logger } from '../logger/index.js';
5
+ import { MESSAGES } from '../constants/index.js';
6
+ import { loadProjectConfig } from '../utils/project-config.js';
7
+ import { getMainWorktreePath } from '../utils/git.js';
8
+ import { printInfo, printSuccess, printWarning } from '../utils/formatter.js';
9
+ import type { WorktreeInfo, ResolvedHook, PostCreateHookResult } from '../types/index.js';
10
+
11
+ /** 项目仓库中的 postCreate 脚本相对路径 */
12
+ const POST_CREATE_SCRIPT_RELATIVE_PATH = '.clawt/postCreate.sh';
13
+
14
+ /**
15
+ * 检测文件是否具有可执行权限
16
+ * @param {string} filePath - 文件绝对路径
17
+ * @returns {boolean} 是否可执行
18
+ */
19
+ function isExecutable(filePath: string): boolean {
20
+ try {
21
+ accessSync(filePath, fsConstants.X_OK);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 自动为脚本文件添加可执行权限
30
+ * chmod 失败时仅打印警告,不阻塞流程(后续执行阶段会自然报错)
31
+ * @param {string} filePath - 脚本文件绝对路径
32
+ */
33
+ function autoFixExecutablePermission(filePath: string): void {
34
+ try {
35
+ chmodSync(filePath, 0o755);
36
+ printInfo(MESSAGES.POST_CREATE_SCRIPT_AUTO_CHMOD(filePath));
37
+ } catch {
38
+ printWarning(MESSAGES.POST_CREATE_SCRIPT_NOT_EXECUTABLE(filePath));
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 将 postCreate 配置值规范化为单条命令
44
+ * 空字符串视为无配置
45
+ * @param {string | undefined} value - postCreate 配置值
46
+ * @returns {string | null} 规范化后的命令,无有效配置时返回 null
47
+ */
48
+ function normalizeCommand(value: string | undefined): string | null {
49
+ if (!value) return null;
50
+ const trimmed = value.trim();
51
+ return trimmed || null;
52
+ }
53
+
54
+ /**
55
+ * 按优先级解析 postCreate hook 来源和命令
56
+ * 优先级:项目配置 postCreate > 项目仓库 .clawt/postCreate.sh
57
+ * @returns {ResolvedHook | null} 解析到的 hook 配置,无配置时返回 null
58
+ */
59
+ export function resolvePostCreateHook(): ResolvedHook | null {
60
+ // 优先级 1:项目配置中的 postCreate
61
+ const config = loadProjectConfig();
62
+ if (config?.postCreate) {
63
+ const command = normalizeCommand(config.postCreate);
64
+ if (command) {
65
+ return { command, source: 'projectConfig' };
66
+ }
67
+ }
68
+
69
+ // 优先级 2:项目仓库中的 .clawt/postCreate.sh
70
+ const mainWorktreePath = getMainWorktreePath();
71
+ const scriptPath = join(mainWorktreePath, POST_CREATE_SCRIPT_RELATIVE_PATH);
72
+
73
+ if (existsSync(scriptPath)) {
74
+ if (!isExecutable(scriptPath)) {
75
+ autoFixExecutablePermission(scriptPath);
76
+ }
77
+ return { command: scriptPath, source: 'postCreateScript' };
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ /**
84
+ * 获取 hook 来源的中文描述
85
+ * @param {ResolvedHook} hook - 解析到的 hook 配置
86
+ * @returns {string} 来源描述文本
87
+ */
88
+ function getSourceLabel(hook: ResolvedHook): string {
89
+ return hook.source === 'projectConfig' ? '项目配置 (postCreate)' : '.clawt/postCreate.sh';
90
+ }
91
+
92
+ /**
93
+ * 异步执行单个 worktree 的 postCreate hook
94
+ * 通过 spawn 异步启动子进程,stdio 设为 'ignore' 静默执行
95
+ * @param {WorktreeInfo} worktree - worktree 信息
96
+ * @param {ResolvedHook} hook - 解析到的 hook 配置
97
+ * @returns {Promise<PostCreateHookResult>} 单个 worktree 的执行结果
98
+ */
99
+ function executeOneHook(worktree: WorktreeInfo, hook: ResolvedHook): Promise<PostCreateHookResult> {
100
+ return new Promise((resolve) => {
101
+ const result: PostCreateHookResult = {
102
+ worktreePath: worktree.path,
103
+ branch: worktree.branch,
104
+ success: true,
105
+ source: hook.source,
106
+ };
107
+
108
+ try {
109
+ const child = spawn(hook.command, {
110
+ cwd: worktree.path,
111
+ stdio: 'ignore',
112
+ shell: true,
113
+ });
114
+
115
+ child.on('error', (err) => {
116
+ result.success = false;
117
+ result.error = err.message;
118
+ logger.error(`postCreate hook 异常: ${hook.command} @ ${worktree.path}: ${result.error}`);
119
+ resolve(result);
120
+ });
121
+
122
+ child.on('close', (code) => {
123
+ if (code !== null && code !== 0) {
124
+ result.success = false;
125
+ result.error = `命令退出码: ${code}`;
126
+ logger.error(`postCreate hook 失败: ${hook.command} (退出码: ${code}) @ ${worktree.path}`);
127
+ } else {
128
+ logger.info(`postCreate hook 成功: ${hook.command} @ ${worktree.path}`);
129
+ }
130
+ resolve(result);
131
+ });
132
+ } catch (err) {
133
+ result.success = false;
134
+ result.error = err instanceof Error ? err.message : String(err);
135
+ logger.error(`postCreate hook 启动失败: ${hook.command} @ ${worktree.path}: ${result.error}`);
136
+ resolve(result);
137
+ }
138
+ });
139
+ }
140
+
141
+ /**
142
+ * 对一批 worktree 异步并行执行 postCreate hook
143
+ * 所有 worktree 同时启动,通过 Promise.all 等待全部完成
144
+ * 失败仅写日志,不影响主流程
145
+ * @param {WorktreeInfo[]} worktrees - worktree 列表
146
+ * @param {ResolvedHook} hook - 解析到的 hook 配置
147
+ * @returns {Promise<PostCreateHookResult[]>} 每个 worktree 的执行结果
148
+ */
149
+ export function executePostCreateHooks(
150
+ worktrees: WorktreeInfo[],
151
+ hook: ResolvedHook,
152
+ ): Promise<PostCreateHookResult[]> {
153
+ const promises = worktrees.map((worktree) => executeOneHook(worktree, hook));
154
+ return Promise.all(promises);
155
+ }
156
+
157
+ /**
158
+ * postCreate hook 完整入口(fire-and-forget)
159
+ * 根据 skip 标志和配置解析结果决定是否执行 hook
160
+ * 异步后台执行,不阻塞主流程
161
+ * @param {WorktreeInfo[]} worktrees - worktree 列表
162
+ * @param {boolean} skip - 是否跳过 postCreate hook(--no-post-create 标志)
163
+ */
164
+ export function runPostCreateHooks(
165
+ worktrees: WorktreeInfo[],
166
+ skip: boolean,
167
+ ): void {
168
+ // --no-post-create 直接跳过
169
+ if (skip) {
170
+ printInfo(MESSAGES.HOOK_SKIPPED);
171
+ return;
172
+ }
173
+
174
+ // 解析 hook 配置
175
+ const hook = resolvePostCreateHook();
176
+ if (!hook) {
177
+ printInfo(MESSAGES.HOOK_NOT_CONFIGURED);
178
+ return;
179
+ }
180
+
181
+ // 输出 hook 来源
182
+ printInfo(MESSAGES.HOOK_SOURCE_INFO(getSourceLabel(hook)));
183
+ printInfo('');
184
+
185
+ // 提示用户 hook 正在后台执行
186
+ printInfo(MESSAGES.HOOK_BACKGROUND_START(worktrees.length, hook.command));
187
+
188
+ // fire-and-forget:后台异步并行执行,不 await
189
+ executePostCreateHooks(worktrees, hook).then((results) => {
190
+ // 汇总日志
191
+ const succeeded = results.filter((r) => r.success).length;
192
+ const failed = results.filter((r) => !r.success).length;
193
+ logger.info(`postCreate hook 汇总: ${succeeded} 成功, ${failed} 失败`);
194
+ }).catch((err) => {
195
+ // 兜底:不应到达此分支,但确保不影响主流程
196
+ logger.error(`postCreate hook 执行异常: ${err instanceof Error ? err.message : String(err)}`);
197
+ });
198
+ }
@@ -4,6 +4,8 @@ export interface CreateOptions {
4
4
  branch: string;
5
5
  /** 创建数量(Commander 传入为字符串),默认 '1' */
6
6
  number: string;
7
+ /** 是否执行 postCreate hook,--no-post-create 时为 false */
8
+ postCreate?: boolean;
7
9
  }
8
10
 
9
11
  /** run 命令选项 */
@@ -18,6 +20,8 @@ export interface RunOptions {
18
20
  file?: string;
19
21
  /** 预览模式,仅展示任务计划不实际执行 */
20
22
  dryRun?: boolean;
23
+ /** 是否执行 postCreate hook,--no-post-create 时为 false */
24
+ postCreate?: boolean;
21
25
  }
22
26
 
23
27
  /** validate 命令选项 */
@@ -7,3 +7,4 @@ export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, Snapshot
7
7
  export type { TaskFileEntry, ParseTaskFileOptions } from './taskFile.js';
8
8
  export type { ProjectOverview, ProjectWorktreeDetail, ProjectDetailResult, ProjectsOverviewResult } from './project.js';
9
9
  export type { ProjectConfig, ProjectConfigItemDefinition, ProjectConfigDefinitions } from './projectConfig.js';
10
+ export type { PostCreateHookSource, PostCreateHookResult, ResolvedHook } from './postCreateHook.js';
@@ -0,0 +1,24 @@
1
+ /** postCreate hook 的来源 */
2
+ export type PostCreateHookSource = 'projectConfig' | 'postCreateScript';
3
+
4
+ /** 单个 worktree 的 hook 执行结果 */
5
+ export interface PostCreateHookResult {
6
+ /** worktree 绝对路径 */
7
+ worktreePath: string;
8
+ /** 分支名 */
9
+ branch: string;
10
+ /** 是否执行成功 */
11
+ success: boolean;
12
+ /** hook 来源 */
13
+ source: PostCreateHookSource;
14
+ /** 失败时的错误信息 */
15
+ error?: string;
16
+ }
17
+
18
+ /** hook 解析结果 */
19
+ export interface ResolvedHook {
20
+ /** 待执行的命令 */
21
+ command: string;
22
+ /** hook 来源 */
23
+ source: PostCreateHookSource;
24
+ }
@@ -4,6 +4,8 @@ export interface ProjectConfig {
4
4
  clawtMainWorkBranch: string;
5
5
  /** validate 成功后自动执行的命令(-r 的默认值) */
6
6
  validateRunCommand?: string;
7
+ /** worktree 创建后自动执行的命令,用于安装依赖等初始化操作 */
8
+ postCreate?: string;
7
9
  }
8
10
 
9
11
  /** 单个项目配置项的完整定义(默认值 + 描述) */