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,2 @@
1
+ #!/bin/bash
2
+ pnpm install --frozen-lockfile
package/CLAUDE.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Project Memory
2
2
 
3
+ ## 代码阅读规范(必须严格遵守的三步阅读流程)
4
+
5
+ 每次接到任务时,**必须**按以下顺序渐进式地理解项目,禁止跳步:
6
+
7
+ 1. **第一步:阅读 `docs/spec.md`** — 全面了解项目整体架构、核心概念与模块划分,建立全局认知。
8
+ 2. **第二步:根据用户问题,阅读 `docs/` 下对应模块的详细文档** — 例如用户问 merge 相关功能,则阅读 `docs/merge.md`;问 config 相关则阅读 `docs/config.md` 和 `docs/config-file.md`,以此类推。
9
+ 3. **第三步:阅读具体的源代码** — 在前两步建立充分上下文后,再深入代码实现,确保方案设计不会因阅读代码不全面而产生缺陷。
10
+
11
+ > **重要提示:文档可能滞后于代码。当文档描述与实际代码实现存在冲突时,以代码为准。**
12
+
3
13
  ## 编码规范
4
14
 
5
15
  - JSON 序列化必须使用项目封装的 `safeStringify`(位于 `src/utils/json.ts`),禁止直接使用原生 `JSON.stringify`。`safeStringify` 已通过 `src/utils/index.js` 统一导出。
package/README.md CHANGED
@@ -54,7 +54,7 @@ clawt init show # 交互式查看和修改项目配置
54
54
  clawt init show --json # 以 JSON 格式输出项目配置
55
55
  ```
56
56
 
57
- 设置项目的主工作分支。重复执行会更新主工作分支配置。`init show` 提供交互式面板,可查看和修改项目配置项(如 validate 成功后自动执行的命令)。`init show --json` 可以直接输出当前项目配置的 JSON 格式。
57
+ 设置项目的主工作分支。重复执行会更新主工作分支配置。`init show` 提供交互式面板,可查看和修改项目配置项(如 validate 成功后自动执行的命令、worktree 创建后自动执行的 postCreate hook 命令等)。`init show --json` 可以直接输出当前项目配置的 JSON 格式。
58
58
 
59
59
  ### `clawt run` — 创建 worktree 并执行任务
60
60
 
@@ -73,6 +73,9 @@ clawt run -f tasks.md -b feat
73
73
 
74
74
  # 试运行:仅预览将要创建的 worktree 和任务,不实际执行
75
75
  clawt run -b <branch> --tasks "任务1" --tasks "任务2" --dry-run
76
+
77
+ # 跳过 postCreate hook
78
+ clawt run -b <branch> --no-post-create
76
79
  ```
77
80
 
78
81
  **`--dry-run` 预览示例:**
@@ -139,6 +142,7 @@ clawt resume -f tasks.md -c 2 # 限制并发数
139
142
  ```bash
140
143
  clawt create -b <branch> # 创建 1 个
141
144
  clawt create -b <branch> -n 3 # 批量创建 3 个
145
+ clawt create -b <branch> --no-post-create # 跳过 postCreate hook
142
146
  ```
143
147
 
144
148
  ### `clawt validate` — 在主 worktree 中验证分支变更
@@ -337,6 +341,19 @@ clawt alias remove l
337
341
  | `conflictResolveMode` | `"ask"` | merge 冲突时的解决模式:`ask`(询问是否使用 AI)、`auto`(自动 AI 解决)、`manual`(手动解决) |
338
342
  | `conflictResolveTimeoutMs` | `300000` | Claude Code 冲突解决超时时间(毫秒),默认 5 分钟 |
339
343
 
344
+ ## postCreate Hook
345
+
346
+ worktree 创建后可自动执行任意初始化命令(如安装依赖、生成配置文件、编译资源等),`create` 和 `run` 命令均支持。
347
+
348
+ **配置方式(二选一):**
349
+
350
+ 1. **项目配置**:通过 `clawt init show` 设置 `postCreate` 字段(如 `npm install`)
351
+ 2. **脚本文件**:在项目根目录创建 `.clawt/postCreate.sh` 并赋予执行权限
352
+
353
+ 项目配置优先于脚本文件。使用 `--no-post-create` 可跳过 hook 执行。
354
+
355
+ hook 以 fire-and-forget 模式后台异步并行执行,不阻塞主流程。执行结果仅写入日志。
356
+
340
357
  ## 全局选项
341
358
 
342
359
  | 选项 | 说明 |
package/dist/index.js CHANGED
@@ -564,6 +564,30 @@ var TASKS_CMD_MESSAGES = {
564
564
  TASK_INIT_HINT: (path2) => `\u4F7F\u7528 clawt run -f ${path2} \u6267\u884C\u4EFB\u52A1`
565
565
  };
566
566
 
567
+ // src/constants/messages/post-create.ts
568
+ var POST_CREATE_MESSAGES = {
569
+ /** hook 执行跳过(--no-post-create) */
570
+ HOOK_SKIPPED: "\u5DF2\u8DF3\u8FC7 postCreate hook\uFF08--no-post-create\uFF09",
571
+ /** 无 hook 配置 */
572
+ HOOK_NOT_CONFIGURED: "\u672A\u914D\u7F6E postCreate hook\uFF0C\u8DF3\u8FC7",
573
+ /** hook 来源提示 */
574
+ HOOK_SOURCE_INFO: (source) => `postCreate hook \u6765\u6E90: ${source}`,
575
+ /** hook 开始执行 */
576
+ HOOK_EXECUTING: (branch, command) => `[${branch}] \u6B63\u5728\u6267\u884C postCreate hook: ${command}`,
577
+ /** hook 执行成功 */
578
+ HOOK_SUCCESS: (branch) => `[${branch}] postCreate hook \u6267\u884C\u6210\u529F`,
579
+ /** hook 执行失败 */
580
+ HOOK_FAILED: (branch, error) => `[${branch}] postCreate hook \u6267\u884C\u5931\u8D25: ${error}`,
581
+ /** hook 执行汇总 */
582
+ HOOK_SUMMARY: (succeeded, failed) => `postCreate hook \u6267\u884C\u5B8C\u6210: ${succeeded} \u6210\u529F, ${failed} \u5931\u8D25`,
583
+ /** hook 后台执行中提示 */
584
+ HOOK_BACKGROUND_START: (count, command) => `postCreate hook \u6B63\u5728\u540E\u53F0\u6267\u884C (${count} \u4E2A worktree): ${command}`,
585
+ /** postCreate.sh 自动添加执行权限 */
586
+ POST_CREATE_SCRIPT_AUTO_CHMOD: (path2) => `${path2} \u4E0D\u53EF\u6267\u884C\uFF0C\u5DF2\u81EA\u52A8\u6DFB\u52A0\u6267\u884C\u6743\u9650`,
587
+ /** postCreate.sh 不可执行(自动 chmod 失败时降级提示) */
588
+ POST_CREATE_SCRIPT_NOT_EXECUTABLE: (path2) => `\u68C0\u6D4B\u5230 ${path2} \u4F46\u4E0D\u53EF\u6267\u884C\uFF0C\u81EA\u52A8\u6DFB\u52A0\u6743\u9650\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C chmod +x ${path2}`
589
+ };
590
+
567
591
  // src/constants/messages/interactive-panel.ts
568
592
  import chalk from "chalk";
569
593
 
@@ -652,7 +676,8 @@ var MESSAGES = {
652
676
  ...INIT_MESSAGES,
653
677
  ...COVER_VALIDATE_MESSAGES,
654
678
  ...HOME_MESSAGES,
655
- ...TASKS_CMD_MESSAGES
679
+ ...TASKS_CMD_MESSAGES,
680
+ ...POST_CREATE_MESSAGES
656
681
  };
657
682
 
658
683
  // src/constants/exitCodes.ts
@@ -670,7 +695,7 @@ var VALID_TERMINAL_APPS = ["auto", "iterm2", "terminal"];
670
695
  var ITERM2_APP_PATH = "/Applications/iTerm.app";
671
696
 
672
697
  // src/constants/config.ts
673
- var APPEND_SYSTEM_PROMPT = "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.";
698
+ var APPEND_SYSTEM_PROMPT = "Currently, you are in the git worktree directory.";
674
699
  var CONFIG_DEFINITIONS = {
675
700
  autoDeleteBranch: {
676
701
  defaultValue: false,
@@ -743,6 +768,10 @@ var PROJECT_CONFIG_DEFINITIONS = {
743
768
  validateRunCommand: {
744
769
  defaultValue: void 0,
745
770
  description: "validate \u6210\u529F\u540E\u81EA\u52A8\u6267\u884C\u7684\u547D\u4EE4\uFF08-r \u7684\u9ED8\u8BA4\u503C\uFF09"
771
+ },
772
+ postCreate: {
773
+ defaultValue: void 0,
774
+ 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"
746
775
  }
747
776
  };
748
777
  function deriveDefaultConfig2(definitions) {
@@ -2020,8 +2049,9 @@ function escapeShellSingleQuote(str) {
2020
2049
  }
2021
2050
  function buildClaudeCommand(worktree, hasPreviousSession) {
2022
2051
  const commandStr = getConfigValue("claudeCodeCommand");
2052
+ const systemPrompt = APPEND_SYSTEM_PROMPT;
2023
2053
  const escapedPath = escapeShellSingleQuote(worktree.path);
2024
- const escapedPrompt = escapeShellSingleQuote(APPEND_SYSTEM_PROMPT);
2054
+ const escapedPrompt = escapeShellSingleQuote(systemPrompt);
2025
2055
  const continueFlag = hasPreviousSession ? " --continue" : "";
2026
2056
  return `cd '${escapedPath}' && ${commandStr} --append-system-prompt '${escapedPrompt}'${continueFlag}`;
2027
2057
  }
@@ -2841,7 +2871,8 @@ function parseStreamEvent(event) {
2841
2871
 
2842
2872
  // src/utils/task-executor.ts
2843
2873
  function executeClaudeTask(worktree, task, onActivity, continueSession) {
2844
- const args = ["-p", task, "--output-format", "stream-json", "--verbose", "--permission-mode", "bypassPermissions"];
2874
+ const systemPrompt = APPEND_SYSTEM_PROMPT;
2875
+ const args = ["-p", task, "--output-format", "stream-json", "--verbose", "--permission-mode", "bypassPermissions", "--append-system-prompt", systemPrompt];
2845
2876
  if (continueSession) {
2846
2877
  args.push("--continue");
2847
2878
  }
@@ -4326,6 +4357,117 @@ async function handleMergeConflict(currentBranch, incomingBranch, cwd, autoFlag)
4326
4357
  return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
4327
4358
  }
4328
4359
 
4360
+ // src/hooks/post-create.ts
4361
+ import { existsSync as existsSync10, accessSync, chmodSync, constants as fsConstants } from "fs";
4362
+ import { spawn as spawn2 } from "child_process";
4363
+ import { join as join8 } from "path";
4364
+ var POST_CREATE_SCRIPT_RELATIVE_PATH = ".clawt/postCreate.sh";
4365
+ function isExecutable(filePath) {
4366
+ try {
4367
+ accessSync(filePath, fsConstants.X_OK);
4368
+ return true;
4369
+ } catch {
4370
+ return false;
4371
+ }
4372
+ }
4373
+ function autoFixExecutablePermission(filePath) {
4374
+ try {
4375
+ chmodSync(filePath, 493);
4376
+ printInfo(MESSAGES.POST_CREATE_SCRIPT_AUTO_CHMOD(filePath));
4377
+ } catch {
4378
+ printWarning(MESSAGES.POST_CREATE_SCRIPT_NOT_EXECUTABLE(filePath));
4379
+ }
4380
+ }
4381
+ function normalizeCommand(value) {
4382
+ if (!value) return null;
4383
+ const trimmed = value.trim();
4384
+ return trimmed || null;
4385
+ }
4386
+ function resolvePostCreateHook() {
4387
+ const config2 = loadProjectConfig();
4388
+ if (config2?.postCreate) {
4389
+ const command = normalizeCommand(config2.postCreate);
4390
+ if (command) {
4391
+ return { command, source: "projectConfig" };
4392
+ }
4393
+ }
4394
+ const mainWorktreePath = getMainWorktreePath();
4395
+ const scriptPath = join8(mainWorktreePath, POST_CREATE_SCRIPT_RELATIVE_PATH);
4396
+ if (existsSync10(scriptPath)) {
4397
+ if (!isExecutable(scriptPath)) {
4398
+ autoFixExecutablePermission(scriptPath);
4399
+ }
4400
+ return { command: scriptPath, source: "postCreateScript" };
4401
+ }
4402
+ return null;
4403
+ }
4404
+ function getSourceLabel(hook) {
4405
+ return hook.source === "projectConfig" ? "\u9879\u76EE\u914D\u7F6E (postCreate)" : ".clawt/postCreate.sh";
4406
+ }
4407
+ function executeOneHook(worktree, hook) {
4408
+ return new Promise((resolve4) => {
4409
+ const result = {
4410
+ worktreePath: worktree.path,
4411
+ branch: worktree.branch,
4412
+ success: true,
4413
+ source: hook.source
4414
+ };
4415
+ try {
4416
+ const child = spawn2(hook.command, {
4417
+ cwd: worktree.path,
4418
+ stdio: "ignore",
4419
+ shell: true
4420
+ });
4421
+ child.on("error", (err) => {
4422
+ result.success = false;
4423
+ result.error = err.message;
4424
+ logger.error(`postCreate hook \u5F02\u5E38: ${hook.command} @ ${worktree.path}: ${result.error}`);
4425
+ resolve4(result);
4426
+ });
4427
+ child.on("close", (code) => {
4428
+ if (code !== null && code !== 0) {
4429
+ result.success = false;
4430
+ result.error = `\u547D\u4EE4\u9000\u51FA\u7801: ${code}`;
4431
+ logger.error(`postCreate hook \u5931\u8D25: ${hook.command} (\u9000\u51FA\u7801: ${code}) @ ${worktree.path}`);
4432
+ } else {
4433
+ logger.info(`postCreate hook \u6210\u529F: ${hook.command} @ ${worktree.path}`);
4434
+ }
4435
+ resolve4(result);
4436
+ });
4437
+ } catch (err) {
4438
+ result.success = false;
4439
+ result.error = err instanceof Error ? err.message : String(err);
4440
+ logger.error(`postCreate hook \u542F\u52A8\u5931\u8D25: ${hook.command} @ ${worktree.path}: ${result.error}`);
4441
+ resolve4(result);
4442
+ }
4443
+ });
4444
+ }
4445
+ function executePostCreateHooks(worktrees, hook) {
4446
+ const promises = worktrees.map((worktree) => executeOneHook(worktree, hook));
4447
+ return Promise.all(promises);
4448
+ }
4449
+ function runPostCreateHooks(worktrees, skip) {
4450
+ if (skip) {
4451
+ printInfo(MESSAGES.HOOK_SKIPPED);
4452
+ return;
4453
+ }
4454
+ const hook = resolvePostCreateHook();
4455
+ if (!hook) {
4456
+ printInfo(MESSAGES.HOOK_NOT_CONFIGURED);
4457
+ return;
4458
+ }
4459
+ printInfo(MESSAGES.HOOK_SOURCE_INFO(getSourceLabel(hook)));
4460
+ printInfo("");
4461
+ printInfo(MESSAGES.HOOK_BACKGROUND_START(worktrees.length, hook.command));
4462
+ executePostCreateHooks(worktrees, hook).then((results) => {
4463
+ const succeeded = results.filter((r) => r.success).length;
4464
+ const failed = results.filter((r) => !r.success).length;
4465
+ logger.info(`postCreate hook \u6C47\u603B: ${succeeded} \u6210\u529F, ${failed} \u5931\u8D25`);
4466
+ }).catch((err) => {
4467
+ logger.error(`postCreate hook \u6267\u884C\u5F02\u5E38: ${err instanceof Error ? err.message : String(err)}`);
4468
+ });
4469
+ }
4470
+
4329
4471
  // src/commands/list.ts
4330
4472
  import chalk10 from "chalk";
4331
4473
  function registerListCommand(program2) {
@@ -4379,7 +4521,7 @@ function printListAsText(projectName, worktrees) {
4379
4521
 
4380
4522
  // src/commands/create.ts
4381
4523
  function registerCreateCommand(program2) {
4382
- program2.command("create").description("\u6279\u91CF\u521B\u5EFA worktree \u53CA\u5BF9\u5E94\u5206\u652F\uFF08\u542B\u9A8C\u8BC1\u5206\u652F\uFF09").requiredOption("-b, --branch <branchName>", "\u5206\u652F\u540D").option("-n, --number <count>", "\u521B\u5EFA\u6570\u91CF", "1").action(async (options) => {
4524
+ program2.command("create").description("\u6279\u91CF\u521B\u5EFA worktree \u53CA\u5BF9\u5E94\u5206\u652F\uFF08\u542B\u9A8C\u8BC1\u5206\u652F\uFF09").requiredOption("-b, --branch <branchName>", "\u5206\u652F\u540D").option("-n, --number <count>", "\u521B\u5EFA\u6570\u91CF", "1").option("--post-create", "\u6267\u884C postCreate hook\uFF08\u9ED8\u8BA4\u5F00\u542F\uFF0C--no-post-create \u8DF3\u8FC7\uFF09", true).action(async (options) => {
4383
4525
  await handleCreate(options);
4384
4526
  });
4385
4527
  }
@@ -4394,6 +4536,7 @@ async function handleCreate(options) {
4394
4536
  }
4395
4537
  logger.info(`create \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u6570\u91CF: ${count}`);
4396
4538
  const worktrees = createWorktrees(options.branch, count);
4539
+ runPostCreateHooks(worktrees, !options.postCreate);
4397
4540
  printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
4398
4541
  printInfo("");
4399
4542
  worktrees.forEach((wt, index) => {
@@ -4487,7 +4630,7 @@ async function handleRemove(options) {
4487
4630
 
4488
4631
  // src/commands/run.ts
4489
4632
  function registerRunCommand(program2) {
4490
- program2.command("run").description("\u6279\u91CF\u521B\u5EFA worktree + \u542F\u52A8 Claude Code \u6267\u884C\u4EFB\u52A1\uFF08\u652F\u6301\u4EFB\u52A1\u6587\u4EF6\uFF09").option("-b, --branch <branchName>", "\u5206\u652F\u540D").option("--tasks <task...>", "\u4EFB\u52A1\u5217\u8868\uFF08\u53EF\u591A\u6B21\u6307\u5B9A\uFF09\uFF0C\u4E0D\u4F20\u5219\u5728 worktree \u4E2D\u6253\u5F00 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762").option("-c, --concurrency <n>", "\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236").option("-f, --file <path>", "\u4ECE\u4EFB\u52A1\u6587\u4EF6\u8BFB\u53D6\u4EFB\u52A1\u5217\u8868\uFF08\u4E0E --tasks \u4E92\u65A5\uFF09").option("--dry-run", "\u9884\u89C8\u6A21\u5F0F\uFF0C\u4EC5\u5C55\u793A\u4EFB\u52A1\u8BA1\u5212\u4E0D\u5B9E\u9645\u6267\u884C").action(async (options) => {
4633
+ program2.command("run").description("\u6279\u91CF\u521B\u5EFA worktree + \u542F\u52A8 Claude Code \u6267\u884C\u4EFB\u52A1\uFF08\u652F\u6301\u4EFB\u52A1\u6587\u4EF6\uFF09").option("-b, --branch <branchName>", "\u5206\u652F\u540D").option("--tasks <task...>", "\u4EFB\u52A1\u5217\u8868\uFF08\u53EF\u591A\u6B21\u6307\u5B9A\uFF09\uFF0C\u4E0D\u4F20\u5219\u5728 worktree \u4E2D\u6253\u5F00 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762").option("-c, --concurrency <n>", "\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236").option("-f, --file <path>", "\u4ECE\u4EFB\u52A1\u6587\u4EF6\u8BFB\u53D6\u4EFB\u52A1\u5217\u8868\uFF08\u4E0E --tasks \u4E92\u65A5\uFF09").option("--dry-run", "\u9884\u89C8\u6A21\u5F0F\uFF0C\u4EC5\u5C55\u793A\u4EFB\u52A1\u8BA1\u5212\u4E0D\u5B9E\u9645\u6267\u884C").option("--post-create", "\u6267\u884C postCreate hook\uFF08\u9ED8\u8BA4\u5F00\u542F\uFF0C--no-post-create \u8DF3\u8FC7\uFF09", true).action(async (options) => {
4491
4634
  await handleRun(options);
4492
4635
  });
4493
4636
  }
@@ -4510,6 +4653,7 @@ async function handleRunFromFile(options) {
4510
4653
  const branches = entries.map((e) => sanitizeBranchName(e.branch));
4511
4654
  worktrees = createWorktreesByBranches(branches);
4512
4655
  }
4656
+ runPostCreateHooks(worktrees, !options.postCreate);
4513
4657
  const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
4514
4658
  logger.info(`run \u547D\u4EE4\uFF08\u6587\u4EF6\u6A21\u5F0F\uFF09\u6267\u884C\uFF0C\u4EFB\u52A1\u6570: ${entries.length}\uFF0C\u5E76\u53D1\u6570: ${concurrency || "\u4E0D\u9650\u5236"}`);
4515
4659
  await executeBatchTasks(worktrees, tasks, concurrency);
@@ -4561,6 +4705,7 @@ async function handleRun(options) {
4561
4705
  }
4562
4706
  const worktrees2 = createWorktrees(options.branch, 1);
4563
4707
  const worktree = worktrees2[0];
4708
+ runPostCreateHooks(worktrees2, !options.postCreate);
4564
4709
  printSuccess(MESSAGES.WORKTREE_CREATED(1));
4565
4710
  launchInteractiveClaude(worktree);
4566
4711
  return;
@@ -4570,6 +4715,7 @@ async function handleRun(options) {
4570
4715
  const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
4571
4716
  logger.info(`run \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u4EFB\u52A1\u6570: ${count}\uFF0C\u5E76\u53D1\u6570: ${concurrency || "\u4E0D\u9650\u5236"}`);
4572
4717
  const worktrees = createWorktrees(options.branch, count);
4718
+ runPostCreateHooks(worktrees, !options.postCreate);
4573
4719
  await executeBatchTasks(worktrees, tasks, concurrency);
4574
4720
  }
4575
4721
 
@@ -5460,8 +5606,8 @@ function registerAliasCommand(program2) {
5460
5606
  }
5461
5607
 
5462
5608
  // src/commands/projects.ts
5463
- import { existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
5464
- import { join as join8 } from "path";
5609
+ import { existsSync as existsSync11, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
5610
+ import { join as join9 } from "path";
5465
5611
  import chalk13 from "chalk";
5466
5612
  function registerProjectsCommand(program2) {
5467
5613
  program2.command("projects [name]").description("\u5C55\u793A\u6240\u6709\u9879\u76EE\u7684 worktree \u6982\u89C8\uFF0C\u6216\u67E5\u770B\u6307\u5B9A\u9879\u76EE\u7684 worktree \u8BE6\u60C5").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((name, options) => {
@@ -5485,8 +5631,8 @@ function handleProjectsOverview(json) {
5485
5631
  printProjectsOverviewAsText(result);
5486
5632
  }
5487
5633
  function handleProjectDetail(name, json) {
5488
- const projectDir = join8(WORKTREES_DIR, name);
5489
- if (!existsSync10(projectDir)) {
5634
+ const projectDir = join9(WORKTREES_DIR, name);
5635
+ if (!existsSync11(projectDir)) {
5490
5636
  printError(MESSAGES.PROJECTS_NOT_FOUND(name));
5491
5637
  process.exit(1);
5492
5638
  }
@@ -5499,7 +5645,7 @@ function handleProjectDetail(name, json) {
5499
5645
  printProjectDetailAsText(result);
5500
5646
  }
5501
5647
  function collectProjectsOverview() {
5502
- if (!existsSync10(WORKTREES_DIR)) {
5648
+ if (!existsSync11(WORKTREES_DIR)) {
5503
5649
  return { projects: [], totalProjects: 0, totalDiskUsage: 0 };
5504
5650
  }
5505
5651
  const entries = readdirSync5(WORKTREES_DIR, { withFileTypes: true });
@@ -5508,7 +5654,7 @@ function collectProjectsOverview() {
5508
5654
  if (!entry.isDirectory()) {
5509
5655
  continue;
5510
5656
  }
5511
- const projectDir = join8(WORKTREES_DIR, entry.name);
5657
+ const projectDir = join9(WORKTREES_DIR, entry.name);
5512
5658
  const overview = collectSingleProjectOverview(entry.name, projectDir);
5513
5659
  projects.push(overview);
5514
5660
  }
@@ -5525,7 +5671,7 @@ function collectSingleProjectOverview(name, projectDir) {
5525
5671
  const worktreeDirs = subEntries.filter((e) => e.isDirectory());
5526
5672
  const worktreeCount = worktreeDirs.length;
5527
5673
  const diskUsage = calculateDirSize(projectDir);
5528
- const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join8(projectDir, e.name)));
5674
+ const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join9(projectDir, e.name)));
5529
5675
  return {
5530
5676
  name,
5531
5677
  worktreeCount,
@@ -5540,7 +5686,7 @@ function collectProjectDetail(name, projectDir) {
5540
5686
  if (!entry.isDirectory()) {
5541
5687
  continue;
5542
5688
  }
5543
- const wtPath = join8(projectDir, entry.name);
5689
+ const wtPath = join9(projectDir, entry.name);
5544
5690
  const detail = collectSingleWorktreeDetail(entry.name, wtPath);
5545
5691
  worktrees.push(detail);
5546
5692
  }
@@ -5639,7 +5785,7 @@ function printWorktreeDetailItem(wt) {
5639
5785
  }
5640
5786
 
5641
5787
  // src/commands/completion.ts
5642
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
5788
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync13 } from "fs";
5643
5789
  import { resolve as resolve2 } from "path";
5644
5790
  import { homedir as homedir2 } from "os";
5645
5791
 
@@ -5690,14 +5836,14 @@ compdef _clawt_completion clawt
5690
5836
  }
5691
5837
 
5692
5838
  // src/utils/completion-engine.ts
5693
- import { existsSync as existsSync11, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
5694
- import { join as join9, dirname, basename as basename2 } from "path";
5839
+ import { existsSync as existsSync12, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
5840
+ import { join as join10, dirname, basename as basename2 } from "path";
5695
5841
  function completeFilePath(partial) {
5696
5842
  const cwd = process.cwd();
5697
5843
  const hasDir = partial.includes("/");
5698
- const searchDir = hasDir ? join9(cwd, dirname(partial)) : cwd;
5844
+ const searchDir = hasDir ? join10(cwd, dirname(partial)) : cwd;
5699
5845
  const prefix = hasDir ? basename2(partial) : partial;
5700
- if (!existsSync11(searchDir)) {
5846
+ if (!existsSync12(searchDir)) {
5701
5847
  return [];
5702
5848
  }
5703
5849
  const entries = readdirSync6(searchDir);
@@ -5706,7 +5852,7 @@ function completeFilePath(partial) {
5706
5852
  for (const entry of entries) {
5707
5853
  if (!entry.startsWith(prefix)) continue;
5708
5854
  if (entry.startsWith(".")) continue;
5709
- const fullPath = join9(searchDir, entry);
5855
+ const fullPath = join10(searchDir, entry);
5710
5856
  try {
5711
5857
  const stat = statSync5(fullPath);
5712
5858
  if (stat.isDirectory()) {
@@ -5795,7 +5941,7 @@ function generateCompletions(program2, args) {
5795
5941
 
5796
5942
  // src/commands/completion.ts
5797
5943
  function appendToFile(filePath, content) {
5798
- if (existsSync12(filePath)) {
5944
+ if (existsSync13(filePath)) {
5799
5945
  const current = readFileSync6(filePath, "utf-8");
5800
5946
  if (current.includes("clawt completion")) {
5801
5947
  printInfo(MESSAGES.COMPLETION_INSTALL_EXISTS + ": " + filePath);
@@ -5930,19 +6076,19 @@ async function handleHome() {
5930
6076
  }
5931
6077
 
5932
6078
  // src/commands/tasks.ts
5933
- import { resolve as resolve3, dirname as dirname2, join as join10 } from "path";
5934
- import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
6079
+ import { resolve as resolve3, dirname as dirname2, join as join11 } from "path";
6080
+ import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "fs";
5935
6081
  function registerTasksCommand(program2) {
5936
6082
  const taskCmd = program2.command("tasks").description("\u4EFB\u52A1\u6587\u4EF6\u7BA1\u7406");
5937
6083
  taskCmd.command("init").description("\u751F\u6210\u4EFB\u52A1\u6A21\u677F\u6587\u4EF6").argument("[path]", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (path2) => {
5938
- const filePath = path2 ?? join10(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
6084
+ const filePath = path2 ?? join11(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
5939
6085
  await handleTasksInit(filePath);
5940
6086
  });
5941
6087
  }
5942
6088
  async function handleTasksInit(filePath) {
5943
6089
  const absolutePath = resolve3(filePath);
5944
6090
  logger.info(`tasks init \u547D\u4EE4\u6267\u884C\uFF0C\u76EE\u6807\u6587\u4EF6: ${absolutePath}`);
5945
- if (existsSync13(absolutePath)) {
6091
+ if (existsSync14(absolutePath)) {
5946
6092
  throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
5947
6093
  }
5948
6094
  ensureDir(dirname2(absolutePath));
@@ -541,6 +541,30 @@ var TASKS_CMD_MESSAGES = {
541
541
  TASK_INIT_HINT: (path) => `\u4F7F\u7528 clawt run -f ${path} \u6267\u884C\u4EFB\u52A1`
542
542
  };
543
543
 
544
+ // src/constants/messages/post-create.ts
545
+ var POST_CREATE_MESSAGES = {
546
+ /** hook 执行跳过(--no-post-create) */
547
+ HOOK_SKIPPED: "\u5DF2\u8DF3\u8FC7 postCreate hook\uFF08--no-post-create\uFF09",
548
+ /** 无 hook 配置 */
549
+ HOOK_NOT_CONFIGURED: "\u672A\u914D\u7F6E postCreate hook\uFF0C\u8DF3\u8FC7",
550
+ /** hook 来源提示 */
551
+ HOOK_SOURCE_INFO: (source) => `postCreate hook \u6765\u6E90: ${source}`,
552
+ /** hook 开始执行 */
553
+ HOOK_EXECUTING: (branch, command) => `[${branch}] \u6B63\u5728\u6267\u884C postCreate hook: ${command}`,
554
+ /** hook 执行成功 */
555
+ HOOK_SUCCESS: (branch) => `[${branch}] postCreate hook \u6267\u884C\u6210\u529F`,
556
+ /** hook 执行失败 */
557
+ HOOK_FAILED: (branch, error) => `[${branch}] postCreate hook \u6267\u884C\u5931\u8D25: ${error}`,
558
+ /** hook 执行汇总 */
559
+ HOOK_SUMMARY: (succeeded, failed) => `postCreate hook \u6267\u884C\u5B8C\u6210: ${succeeded} \u6210\u529F, ${failed} \u5931\u8D25`,
560
+ /** hook 后台执行中提示 */
561
+ HOOK_BACKGROUND_START: (count, command) => `postCreate hook \u6B63\u5728\u540E\u53F0\u6267\u884C (${count} \u4E2A worktree): ${command}`,
562
+ /** postCreate.sh 自动添加执行权限 */
563
+ POST_CREATE_SCRIPT_AUTO_CHMOD: (path) => `${path} \u4E0D\u53EF\u6267\u884C\uFF0C\u5DF2\u81EA\u52A8\u6DFB\u52A0\u6267\u884C\u6743\u9650`,
564
+ /** postCreate.sh 不可执行(自动 chmod 失败时降级提示) */
565
+ POST_CREATE_SCRIPT_NOT_EXECUTABLE: (path) => `\u68C0\u6D4B\u5230 ${path} \u4F46\u4E0D\u53EF\u6267\u884C\uFF0C\u81EA\u52A8\u6DFB\u52A0\u6743\u9650\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C chmod +x ${path}`
566
+ };
567
+
544
568
  // src/constants/messages/interactive-panel.ts
545
569
  import chalk from "chalk";
546
570
 
@@ -600,7 +624,8 @@ var MESSAGES = {
600
624
  ...INIT_MESSAGES,
601
625
  ...COVER_VALIDATE_MESSAGES,
602
626
  ...HOME_MESSAGES,
603
- ...TASKS_CMD_MESSAGES
627
+ ...TASKS_CMD_MESSAGES,
628
+ ...POST_CREATE_MESSAGES
604
629
  };
605
630
 
606
631
  // src/constants/terminal.ts
@@ -679,6 +704,10 @@ var PROJECT_CONFIG_DEFINITIONS = {
679
704
  validateRunCommand: {
680
705
  defaultValue: void 0,
681
706
  description: "validate \u6210\u529F\u540E\u81EA\u52A8\u6267\u884C\u7684\u547D\u4EE4\uFF08-r \u7684\u9ED8\u8BA4\u503C\uFF09"
707
+ },
708
+ postCreate: {
709
+ defaultValue: void 0,
710
+ 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"
682
711
  }
683
712
  };
684
713
  function deriveDefaultConfig2(definitions) {
package/docs/create.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **命令:**
4
4
 
5
5
  ```bash
6
- clawt create -b <branchName> [-n <count>]
6
+ clawt create -b <branchName> [-n <count>] [--no-post-create]
7
7
  ```
8
8
 
9
9
  **参数:**
@@ -12,6 +12,7 @@ clawt create -b <branchName> [-n <count>]
12
12
  | ---- | ---- | ----------------------------------------------------- |
13
13
  | `-b, --branch` | 是 | 分支名 |
14
14
  | `-n, --number` | 否 | 需要创建的 worktree 数量,默认 `1` |
15
+ | `--post-create` | 否 | 执行 postCreate hook(默认开启,`--no-post-create` 跳过)。详见 [post-create-hook.md](./post-create-hook.md) |
15
16
 
16
17
  **运行流程:**
17
18
 
@@ -64,7 +65,8 @@ clawt create -b <branchName> [-n <count>]
64
65
  git worktree add -b <branchName>-n ~/.clawt/worktrees/<project>/<branchName>-n
65
66
  git branch clawt-validate-<branchName>-n
66
67
  ```
67
- 9. **输出创建日志**
68
+ 9. **执行 postCreate hook**:调用 `runPostCreateHooks(worktrees, !options.postCreate)` 以 fire-and-forget 模式后台异步并行执行 postCreate hook(用户自定义的初始化操作)。`--no-post-create` 时跳过。详见 [post-create-hook.md](./post-create-hook.md)
69
+ 10. **输出创建日志**
68
70
 
69
71
  **输出格式:**
70
72
 
@@ -97,5 +99,7 @@ clawt create -b <branchName> [-n <count>]
97
99
  - 确保在主工作分支的逻辑在 `src/utils/validate-branch.ts` 的 `ensureOnMainWorkBranch` 中
98
100
  - 脏工作区交互处理逻辑在 `src/utils/validate-branch.ts` 的 `handleDirtyWorkingDir` 中
99
101
  - 相关消息常量定义在 `src/constants/messages/create.ts` 和 `src/constants/messages/common.ts` 中
102
+ - postCreate hook 执行逻辑在 `src/hooks/post-create.ts` 中,通过 `src/utils/index.js` 统一导出 `runPostCreateHooks`
103
+ - hook 相关消息常量定义在 `src/constants/messages/post-create.ts` 中
100
104
 
101
105
  ---
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`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
29
+ 初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。`init show` 子命令提供交互式面板,可查看和修改所有项目配置项(如 `validateRunCommand`、`postCreate`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
30
30
 
31
31
  **运行流程(设置模式):**
32
32
 
@@ -69,7 +69,7 @@ clawt init show --json
69
69
  ✓ 项目配置 validateRunCommand 已设置为 npm test
70
70
 
71
71
  # show --json 输出
72
- {"clawtMainWorkBranch":"main","validateRunCommand":"npm test"}
72
+ {"clawtMainWorkBranch":"main","validateRunCommand":"npm test","postCreate":"npm install"}
73
73
 
74
74
  # show 未初始化(抛出错误)
75
75
  项目尚未初始化,请先执行 clawt init 设置主工作分支