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.
- package/.clawt/postCreate.sh +2 -0
- package/CLAUDE.md +10 -0
- package/README.md +18 -1
- package/dist/index.js +171 -25
- package/dist/postinstall.js +30 -1
- package/docs/create.md +6 -2
- package/docs/init.md +2 -2
- package/docs/post-create-hook.md +142 -0
- package/docs/project-config.md +9 -2
- package/docs/run.md +10 -6
- package/docs/spec.md +4 -1
- package/package.json +1 -1
- package/src/commands/create.ts +5 -0
- package/src/commands/run.ts +12 -0
- package/src/constants/config.ts +1 -1
- package/src/constants/messages/index.ts +2 -0
- package/src/constants/messages/post-create.ts +29 -0
- package/src/constants/project-config.ts +4 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/post-create.ts +198 -0
- package/src/types/command.ts +4 -0
- package/src/types/index.ts +1 -0
- package/src/types/postCreateHook.ts +24 -0
- package/src/types/projectConfig.ts +2 -0
- package/src/utils/claude.ts +2 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/task-executor.ts +5 -1
- package/tests/unit/commands/create.test.ts +1 -0
- package/tests/unit/commands/run.test.ts +1 -0
- package/tests/unit/constants/messages-post-create.test.ts +112 -0
- package/tests/unit/hooks/post-create.test.ts +434 -0
- package/tests/unit/utils/claude.test.ts +76 -1
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
|
|
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.
|
|
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(
|
|
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
|
|
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
|
|
5464
|
-
import { join as
|
|
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 =
|
|
5489
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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
|
|
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
|
|
5694
|
-
import { join as
|
|
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 ?
|
|
5844
|
+
const searchDir = hasDir ? join10(cwd, dirname(partial)) : cwd;
|
|
5699
5845
|
const prefix = hasDir ? basename2(partial) : partial;
|
|
5700
|
-
if (!
|
|
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 =
|
|
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 (
|
|
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
|
|
5934
|
-
import { existsSync as
|
|
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 ??
|
|
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 (
|
|
6091
|
+
if (existsSync14(absolutePath)) {
|
|
5946
6092
|
throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
|
|
5947
6093
|
}
|
|
5948
6094
|
ensureDir(dirname2(absolutePath));
|
package/dist/postinstall.js
CHANGED
|
@@ -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 设置主工作分支
|