clawt 3.7.0 → 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.
Files changed (55) hide show
  1. package/.claude/agents/docs-sync-updater.md +44 -24
  2. package/.clawt/postCreate.sh +2 -0
  3. package/CLAUDE.md +10 -0
  4. package/README.md +50 -2
  5. package/dist/index.js +180 -29
  6. package/dist/postinstall.js +33 -1
  7. package/docs/alias.md +114 -0
  8. package/docs/completion.md +55 -0
  9. package/docs/config-file.md +49 -0
  10. package/docs/config.md +93 -0
  11. package/docs/cover-validate.md +94 -0
  12. package/docs/create.md +105 -0
  13. package/docs/home.md +70 -0
  14. package/docs/init.md +89 -0
  15. package/docs/list.md +73 -0
  16. package/docs/log.md +67 -0
  17. package/docs/merge.md +152 -0
  18. package/docs/notification.md +94 -0
  19. package/docs/post-create-hook.md +142 -0
  20. package/docs/project-config.md +139 -0
  21. package/docs/projects.md +135 -0
  22. package/docs/remove.md +90 -0
  23. package/docs/reset.md +37 -0
  24. package/docs/resume.md +132 -0
  25. package/docs/run.md +150 -0
  26. package/docs/spec.md +431 -0
  27. package/docs/status.md +345 -0
  28. package/docs/sync.md +128 -0
  29. package/docs/tasks.md +74 -0
  30. package/docs/update-check.md +95 -0
  31. package/docs/validate.md +416 -0
  32. package/package.json +1 -1
  33. package/src/commands/create.ts +5 -0
  34. package/src/commands/run.ts +12 -0
  35. package/src/constants/config.ts +1 -1
  36. package/src/constants/git.ts +3 -0
  37. package/src/constants/messages/index.ts +2 -0
  38. package/src/constants/messages/post-create.ts +29 -0
  39. package/src/constants/project-config.ts +4 -0
  40. package/src/hooks/index.ts +1 -0
  41. package/src/hooks/post-create.ts +198 -0
  42. package/src/types/command.ts +4 -0
  43. package/src/types/index.ts +1 -0
  44. package/src/types/postCreateHook.ts +24 -0
  45. package/src/types/projectConfig.ts +3 -1
  46. package/src/utils/claude.ts +2 -1
  47. package/src/utils/git-core.ts +4 -0
  48. package/src/utils/index.ts +1 -0
  49. package/src/utils/shell.ts +3 -0
  50. package/src/utils/task-executor.ts +5 -1
  51. package/tests/unit/commands/create.test.ts +1 -0
  52. package/tests/unit/commands/run.test.ts +1 -0
  53. package/tests/unit/constants/messages-post-create.test.ts +112 -0
  54. package/tests/unit/hooks/post-create.test.ts +434 -0
  55. package/tests/unit/utils/claude.test.ts +76 -1
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) {
@@ -762,6 +791,7 @@ var PROJECT_CONFIG_DESCRIPTIONS = deriveConfigDescriptions2(PROJECT_CONFIG_DEFIN
762
791
 
763
792
  // src/constants/git.ts
764
793
  var AUTO_SAVE_COMMIT_MESSAGE = "chore: auto-save before sync";
794
+ var EXEC_MAX_BUFFER = 200 * 1024 * 1024;
765
795
 
766
796
  // src/constants/logger.ts
767
797
  var DEBUG_TIMESTAMP_FORMAT = "HH:mm:ss.SSS";
@@ -967,7 +997,8 @@ function execCommand(command, options) {
967
997
  const result = execSync(command, {
968
998
  cwd: options?.cwd,
969
999
  encoding: "utf-8",
970
- stdio: ["pipe", "pipe", "pipe"]
1000
+ stdio: ["pipe", "pipe", "pipe"],
1001
+ maxBuffer: EXEC_MAX_BUFFER
971
1002
  });
972
1003
  return result.trim();
973
1004
  }
@@ -992,7 +1023,8 @@ function execCommandWithInput(command, args, options) {
992
1023
  cwd: options.cwd,
993
1024
  input: options.input,
994
1025
  encoding: "utf-8",
995
- stdio: ["pipe", "pipe", "pipe"]
1026
+ stdio: ["pipe", "pipe", "pipe"],
1027
+ maxBuffer: EXEC_MAX_BUFFER
996
1028
  });
997
1029
  return result.trim();
998
1030
  }
@@ -1184,7 +1216,8 @@ function gitDiffBinaryAgainstBranch(branchName, cwd) {
1184
1216
  logger.debug(`\u6267\u884C\u547D\u4EE4: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
1185
1217
  return execSync2(`git diff HEAD...${branchName} --binary`, {
1186
1218
  cwd,
1187
- stdio: ["pipe", "pipe", "pipe"]
1219
+ stdio: ["pipe", "pipe", "pipe"],
1220
+ maxBuffer: EXEC_MAX_BUFFER
1188
1221
  });
1189
1222
  }
1190
1223
  function gitApplyFromStdin(patchContent, cwd) {
@@ -1221,7 +1254,8 @@ function gitDiffTree(baseTreeHash, targetTreeHash, cwd) {
1221
1254
  logger.debug(`\u6267\u884C\u547D\u4EE4: git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}${cwd ? ` (cwd: ${cwd})` : ""}`);
1222
1255
  return execSync2(`git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}`, {
1223
1256
  cwd,
1224
- stdio: ["pipe", "pipe", "pipe"]
1257
+ stdio: ["pipe", "pipe", "pipe"],
1258
+ maxBuffer: EXEC_MAX_BUFFER
1225
1259
  });
1226
1260
  }
1227
1261
  function gitApplyCachedCheck(patchContent, cwd) {
@@ -2015,8 +2049,9 @@ function escapeShellSingleQuote(str) {
2015
2049
  }
2016
2050
  function buildClaudeCommand(worktree, hasPreviousSession) {
2017
2051
  const commandStr = getConfigValue("claudeCodeCommand");
2052
+ const systemPrompt = APPEND_SYSTEM_PROMPT;
2018
2053
  const escapedPath = escapeShellSingleQuote(worktree.path);
2019
- const escapedPrompt = escapeShellSingleQuote(APPEND_SYSTEM_PROMPT);
2054
+ const escapedPrompt = escapeShellSingleQuote(systemPrompt);
2020
2055
  const continueFlag = hasPreviousSession ? " --continue" : "";
2021
2056
  return `cd '${escapedPath}' && ${commandStr} --append-system-prompt '${escapedPrompt}'${continueFlag}`;
2022
2057
  }
@@ -2836,7 +2871,8 @@ function parseStreamEvent(event) {
2836
2871
 
2837
2872
  // src/utils/task-executor.ts
2838
2873
  function executeClaudeTask(worktree, task, onActivity, continueSession) {
2839
- 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];
2840
2876
  if (continueSession) {
2841
2877
  args.push("--continue");
2842
2878
  }
@@ -4321,6 +4357,117 @@ async function handleMergeConflict(currentBranch, incomingBranch, cwd, autoFlag)
4321
4357
  return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
4322
4358
  }
4323
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
+
4324
4471
  // src/commands/list.ts
4325
4472
  import chalk10 from "chalk";
4326
4473
  function registerListCommand(program2) {
@@ -4374,7 +4521,7 @@ function printListAsText(projectName, worktrees) {
4374
4521
 
4375
4522
  // src/commands/create.ts
4376
4523
  function registerCreateCommand(program2) {
4377
- 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) => {
4378
4525
  await handleCreate(options);
4379
4526
  });
4380
4527
  }
@@ -4389,6 +4536,7 @@ async function handleCreate(options) {
4389
4536
  }
4390
4537
  logger.info(`create \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u6570\u91CF: ${count}`);
4391
4538
  const worktrees = createWorktrees(options.branch, count);
4539
+ runPostCreateHooks(worktrees, !options.postCreate);
4392
4540
  printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
4393
4541
  printInfo("");
4394
4542
  worktrees.forEach((wt, index) => {
@@ -4482,7 +4630,7 @@ async function handleRemove(options) {
4482
4630
 
4483
4631
  // src/commands/run.ts
4484
4632
  function registerRunCommand(program2) {
4485
- 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) => {
4486
4634
  await handleRun(options);
4487
4635
  });
4488
4636
  }
@@ -4505,6 +4653,7 @@ async function handleRunFromFile(options) {
4505
4653
  const branches = entries.map((e) => sanitizeBranchName(e.branch));
4506
4654
  worktrees = createWorktreesByBranches(branches);
4507
4655
  }
4656
+ runPostCreateHooks(worktrees, !options.postCreate);
4508
4657
  const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
4509
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"}`);
4510
4659
  await executeBatchTasks(worktrees, tasks, concurrency);
@@ -4556,6 +4705,7 @@ async function handleRun(options) {
4556
4705
  }
4557
4706
  const worktrees2 = createWorktrees(options.branch, 1);
4558
4707
  const worktree = worktrees2[0];
4708
+ runPostCreateHooks(worktrees2, !options.postCreate);
4559
4709
  printSuccess(MESSAGES.WORKTREE_CREATED(1));
4560
4710
  launchInteractiveClaude(worktree);
4561
4711
  return;
@@ -4565,6 +4715,7 @@ async function handleRun(options) {
4565
4715
  const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
4566
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"}`);
4567
4717
  const worktrees = createWorktrees(options.branch, count);
4718
+ runPostCreateHooks(worktrees, !options.postCreate);
4568
4719
  await executeBatchTasks(worktrees, tasks, concurrency);
4569
4720
  }
4570
4721
 
@@ -5455,8 +5606,8 @@ function registerAliasCommand(program2) {
5455
5606
  }
5456
5607
 
5457
5608
  // src/commands/projects.ts
5458
- import { existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
5459
- 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";
5460
5611
  import chalk13 from "chalk";
5461
5612
  function registerProjectsCommand(program2) {
5462
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) => {
@@ -5480,8 +5631,8 @@ function handleProjectsOverview(json) {
5480
5631
  printProjectsOverviewAsText(result);
5481
5632
  }
5482
5633
  function handleProjectDetail(name, json) {
5483
- const projectDir = join8(WORKTREES_DIR, name);
5484
- if (!existsSync10(projectDir)) {
5634
+ const projectDir = join9(WORKTREES_DIR, name);
5635
+ if (!existsSync11(projectDir)) {
5485
5636
  printError(MESSAGES.PROJECTS_NOT_FOUND(name));
5486
5637
  process.exit(1);
5487
5638
  }
@@ -5494,7 +5645,7 @@ function handleProjectDetail(name, json) {
5494
5645
  printProjectDetailAsText(result);
5495
5646
  }
5496
5647
  function collectProjectsOverview() {
5497
- if (!existsSync10(WORKTREES_DIR)) {
5648
+ if (!existsSync11(WORKTREES_DIR)) {
5498
5649
  return { projects: [], totalProjects: 0, totalDiskUsage: 0 };
5499
5650
  }
5500
5651
  const entries = readdirSync5(WORKTREES_DIR, { withFileTypes: true });
@@ -5503,7 +5654,7 @@ function collectProjectsOverview() {
5503
5654
  if (!entry.isDirectory()) {
5504
5655
  continue;
5505
5656
  }
5506
- const projectDir = join8(WORKTREES_DIR, entry.name);
5657
+ const projectDir = join9(WORKTREES_DIR, entry.name);
5507
5658
  const overview = collectSingleProjectOverview(entry.name, projectDir);
5508
5659
  projects.push(overview);
5509
5660
  }
@@ -5520,7 +5671,7 @@ function collectSingleProjectOverview(name, projectDir) {
5520
5671
  const worktreeDirs = subEntries.filter((e) => e.isDirectory());
5521
5672
  const worktreeCount = worktreeDirs.length;
5522
5673
  const diskUsage = calculateDirSize(projectDir);
5523
- const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join8(projectDir, e.name)));
5674
+ const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join9(projectDir, e.name)));
5524
5675
  return {
5525
5676
  name,
5526
5677
  worktreeCount,
@@ -5535,7 +5686,7 @@ function collectProjectDetail(name, projectDir) {
5535
5686
  if (!entry.isDirectory()) {
5536
5687
  continue;
5537
5688
  }
5538
- const wtPath = join8(projectDir, entry.name);
5689
+ const wtPath = join9(projectDir, entry.name);
5539
5690
  const detail = collectSingleWorktreeDetail(entry.name, wtPath);
5540
5691
  worktrees.push(detail);
5541
5692
  }
@@ -5634,7 +5785,7 @@ function printWorktreeDetailItem(wt) {
5634
5785
  }
5635
5786
 
5636
5787
  // src/commands/completion.ts
5637
- 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";
5638
5789
  import { resolve as resolve2 } from "path";
5639
5790
  import { homedir as homedir2 } from "os";
5640
5791
 
@@ -5685,14 +5836,14 @@ compdef _clawt_completion clawt
5685
5836
  }
5686
5837
 
5687
5838
  // src/utils/completion-engine.ts
5688
- import { existsSync as existsSync11, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
5689
- 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";
5690
5841
  function completeFilePath(partial) {
5691
5842
  const cwd = process.cwd();
5692
5843
  const hasDir = partial.includes("/");
5693
- const searchDir = hasDir ? join9(cwd, dirname(partial)) : cwd;
5844
+ const searchDir = hasDir ? join10(cwd, dirname(partial)) : cwd;
5694
5845
  const prefix = hasDir ? basename2(partial) : partial;
5695
- if (!existsSync11(searchDir)) {
5846
+ if (!existsSync12(searchDir)) {
5696
5847
  return [];
5697
5848
  }
5698
5849
  const entries = readdirSync6(searchDir);
@@ -5701,7 +5852,7 @@ function completeFilePath(partial) {
5701
5852
  for (const entry of entries) {
5702
5853
  if (!entry.startsWith(prefix)) continue;
5703
5854
  if (entry.startsWith(".")) continue;
5704
- const fullPath = join9(searchDir, entry);
5855
+ const fullPath = join10(searchDir, entry);
5705
5856
  try {
5706
5857
  const stat = statSync5(fullPath);
5707
5858
  if (stat.isDirectory()) {
@@ -5790,7 +5941,7 @@ function generateCompletions(program2, args) {
5790
5941
 
5791
5942
  // src/commands/completion.ts
5792
5943
  function appendToFile(filePath, content) {
5793
- if (existsSync12(filePath)) {
5944
+ if (existsSync13(filePath)) {
5794
5945
  const current = readFileSync6(filePath, "utf-8");
5795
5946
  if (current.includes("clawt completion")) {
5796
5947
  printInfo(MESSAGES.COMPLETION_INSTALL_EXISTS + ": " + filePath);
@@ -5925,19 +6076,19 @@ async function handleHome() {
5925
6076
  }
5926
6077
 
5927
6078
  // src/commands/tasks.ts
5928
- import { resolve as resolve3, dirname as dirname2, join as join10 } from "path";
5929
- 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";
5930
6081
  function registerTasksCommand(program2) {
5931
6082
  const taskCmd = program2.command("tasks").description("\u4EFB\u52A1\u6587\u4EF6\u7BA1\u7406");
5932
6083
  taskCmd.command("init").description("\u751F\u6210\u4EFB\u52A1\u6A21\u677F\u6587\u4EF6").argument("[path]", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (path2) => {
5933
- 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));
5934
6085
  await handleTasksInit(filePath);
5935
6086
  });
5936
6087
  }
5937
6088
  async function handleTasksInit(filePath) {
5938
6089
  const absolutePath = resolve3(filePath);
5939
6090
  logger.info(`tasks init \u547D\u4EE4\u6267\u884C\uFF0C\u76EE\u6807\u6587\u4EF6: ${absolutePath}`);
5940
- if (existsSync13(absolutePath)) {
6091
+ if (existsSync14(absolutePath)) {
5941
6092
  throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
5942
6093
  }
5943
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) {
@@ -696,6 +725,9 @@ function deriveConfigDescriptions2(definitions) {
696
725
  var PROJECT_DEFAULT_CONFIG = deriveDefaultConfig2(PROJECT_CONFIG_DEFINITIONS);
697
726
  var PROJECT_CONFIG_DESCRIPTIONS = deriveConfigDescriptions2(PROJECT_CONFIG_DEFINITIONS);
698
727
 
728
+ // src/constants/git.ts
729
+ var EXEC_MAX_BUFFER = 200 * 1024 * 1024;
730
+
699
731
  // src/constants/update.ts
700
732
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
701
733
 
package/docs/alias.md ADDED
@@ -0,0 +1,114 @@
1
+ ### 5.15 命令别名管理
2
+
3
+ **命令:**
4
+
5
+ ```bash
6
+ # 列出所有命令别名
7
+ clawt alias
8
+ clawt alias list
9
+
10
+ # 设置命令别名
11
+ clawt alias set <alias> <command>
12
+
13
+ # 移除命令别名
14
+ clawt alias remove <alias>
15
+ ```
16
+
17
+ **子命令:**
18
+
19
+ | 子命令 | 说明 |
20
+ | ------ | ---- |
21
+ | `clawt alias` / `clawt alias list` | 列出所有已配置的命令别名 |
22
+ | `clawt alias set <alias> <command>` | 设置命令别名,将 `<alias>` 映射到 `<command>` |
23
+ | `clawt alias remove <alias>` | 移除指定的命令别名 |
24
+
25
+ **参数:**
26
+
27
+ | 参数 | 必填 | 说明 |
28
+ | ---- | ---- | ---- |
29
+ | `<alias>` | 是(set / remove) | 别名名称 |
30
+ | `<command>` | 是(set) | 目标内置命令名 |
31
+
32
+ **约束规则:**
33
+
34
+ 1. **别名不能覆盖内置命令名**:别名不能与任何已注册的内置命令同名(动态检测,当前包括 `list`、`create`、`remove`、`run`、`resume`、`validate`、`cover`、`merge`、`config`、`sync`、`reset`、`status`、`alias`、`projects`、`completion`、`init`、`home`、`tasks`)。如果用户尝试设置与内置命令同名的别名,输出错误提示并返回
35
+ 2. **目标必须是内置命令**:别名的目标(`<command>`)必须是已注册的内置命令名。如果指定了不存在的目标命令,输出错误提示并返回
36
+ 3. **参数透传**:通过别名调用时,所有选项和参数会完全透传给目标命令,行为与直接调用目标命令完全一致
37
+
38
+ **持久化:**
39
+
40
+ 别名配置存储在 `~/.clawt/config.json` 的 `aliases` 字段中(类型 `Record<string, string>`,默认 `{}`)。
41
+
42
+ **运行流程:**
43
+
44
+ #### `alias list`(默认)
45
+
46
+ 1. 读取配置文件中的 `aliases` 字段
47
+ 2. 如果没有配置任何别名,输出提示 `(无别名)`
48
+ 3. 如果有别名,逐行输出所有别名映射
49
+
50
+ **输出格式:**
51
+
52
+ ```
53
+ 当前别名列表:
54
+ ────────────────────────────────────────
55
+
56
+ l → list
57
+ r → run
58
+ v → validate
59
+
60
+ ────────────────────────────────────────
61
+ ```
62
+
63
+ #### `alias set <alias> <command>`
64
+
65
+ 1. **校验别名不与内置命令冲突**:检查 `<alias>` 是否为内置命令名,是则输出错误提示并返回
66
+ 2. **校验目标命令存在**:检查 `<command>` 是否为已注册的内置命令名,不是则输出错误提示并返回
67
+ 3. 将别名写入配置文件的 `aliases` 字段(如果别名已存在,覆盖旧值)
68
+ 4. 输出成功提示
69
+
70
+ **输出格式:**
71
+
72
+ ```
73
+ ✓ 已设置别名: l → list
74
+ ```
75
+
76
+ #### `alias remove <alias>`
77
+
78
+ 1. 读取配置文件中的 `aliases` 字段
79
+ 2. 检查指定的别名是否存在,不存在则输出错误提示并返回
80
+ 3. 从 `aliases` 中删除该别名并写入配置文件
81
+ 4. 输出成功提示
82
+
83
+ **输出格式:**
84
+
85
+ ```
86
+ ✓ 已移除别名: l
87
+ ```
88
+
89
+ **别名使用示例:**
90
+
91
+ ```bash
92
+ # 设置别名
93
+ clawt alias set l list
94
+ clawt alias set r run
95
+ clawt alias set v validate
96
+
97
+ # 使用别名(等同于对应的完整命令)
98
+ clawt l # 等同于 clawt list
99
+ clawt r task.md # 等同于 clawt run task.md
100
+
101
+ # 查看所有别名
102
+ clawt alias list
103
+
104
+ # 移除别名
105
+ clawt alias remove l
106
+ ```
107
+
108
+ **实现要点:**
109
+
110
+ - 消息常量定义在 `src/constants/messages/alias.ts`(`ALIAS_MESSAGES`),包括列表为空提示、设置/移除成功、别名不存在、与内置命令冲突、目标命令不存在等消息
111
+ - 别名应用逻辑位于 `src/utils/alias.ts` 的 `applyAliases` 函数:在主入口 `src/index.ts` 中,所有命令注册完成后调用,遍历配置中的 `aliases` 映射,通过 Commander.js 的 `.alias()` 方法为对应命令注册别名。如果目标命令不存在则跳过并输出 warn 级别日志
112
+ - 内置命令冲突检测通过 `getRegisteredCommandNames(program)` 动态获取所有已注册命令名,而非硬编码命令列表
113
+
114
+ ---
@@ -0,0 +1,55 @@
1
+ ### 5.16 `clawt completion` 命令
2
+
3
+ 为终端环境(bash/zsh)生成并安装 `clawt` 的命令、选项及参数的自动补全脚本。
4
+
5
+ #### 语法
6
+ ```bash
7
+ clawt completion bash
8
+ clawt completion zsh
9
+ clawt completion install
10
+ ```
11
+
12
+ #### 子命令说明
13
+
14
+ | 子命令 | 说明 |
15
+ | --------- | ----------------------------------------------------------------------------------- |
16
+ | `bash` | 输出适用于 bash 的补全脚本(用户可重定向到 `~/.bashrc`) |
17
+ | `zsh` | 输出适用于 zsh 的补全脚本(用户可重定向到 `~/.zshrc`) |
18
+ | `install` | 自动检测当前 shell 类型,将补全脚本追加到对应的配置文件中 |
19
+
20
+ #### `install` 子命令流程
21
+
22
+ 1. 通过 `process.env.SHELL` 检测当前 shell 类型
23
+ 2. 根据 shell 类型确定目标配置文件:
24
+ - zsh → `~/.zshrc`(追加 `source <(clawt completion zsh)`)
25
+ - bash → `~/.bashrc`(追加 `eval "$(clawt completion bash)"`)
26
+ 3. 检查目标文件中是否已包含 `clawt completion`,已存在则跳过并提示
27
+ 4. 追加成功后提示用户重启终端或 source 配置文件
28
+ 5. 未知 shell 类型时输出警告,提示手动配置
29
+
30
+ #### 动态补全特性
31
+
32
+ 补全脚本通过内部子命令 `_complete` 实现动态补全,不对外公开。补全引擎基于 Commander.js 的命令树结构遍历,支持以下场景:
33
+
34
+ | 场景 | 补全行为 |
35
+ | ---------------------------- | ---------------------------------------------------------- |
36
+ | `-b` / `--branch` 参数之后 | 动态列出当前项目所有 worktree 分支名(通过 `getProjectWorktrees`) |
37
+ | `-f` / `--file` 参数之后 | 动态列出匹配的文件和子目录(不限制文件类型,支持子目录递归浏览) |
38
+ | `config set` / `config get` 之后 | 动态列出所有配置项键名(从 `CONFIG_DEFINITIONS` 获取) |
39
+ | 输入以 `-` 开头 | 列出当前命令层级的可用选项(short/long) |
40
+ | 其他情况 | 列出当前命令层级的可用子命令及别名;若输入为空,同时列出可用选项 |
41
+
42
+ **文件路径补全细节:**
43
+ - 支持子目录递归浏览(如 `tasks/` 后继续 Tab 可深入子目录)
44
+ - 目录候选项以 `/` 结尾,补全时不自动追加空格
45
+ - 不限制文件类型,列出所有非隐藏文件
46
+ - 跳过隐藏文件和目录(以 `.` 开头)
47
+
48
+ #### 实现说明
49
+
50
+ - 补全命令注册函数:`registerCompletionCommand()`(在 `src/commands/completion.ts`)
51
+ - 消息常量:`COMPLETION_MESSAGES`(在 `src/constants/messages/completion.ts`)
52
+ - 核心函数:`generateCompletions()` 解析当前输入上下文并输出候选项,`completeFilePath()` 处理文件路径补全,`tryCompleteSpecialArg()` 处理特殊参数(分支名、文件路径、配置键)的动态补全,`completeFromCommandTree()` 基于命令树遍历生成子命令和选项候选项
53
+ - shell 脚本生成:`getBashScript()`、`getZshScript()` 分别生成对应 shell 的补全脚本
54
+
55
+ ---