clawt 3.9.7 → 3.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,7 @@ clawt init show # Interactively view and modify project configuration
56
56
  clawt init show --json # Output project configuration in JSON format
57
57
  ```
58
58
 
59
- Sets the project's main work branch. Re-running updates the main work branch configuration. `init show` provides an interactive panel for viewing and modifying project settings (e.g., commands to auto-run after validate succeeds, postCreate hook commands after worktree creation, etc.). `init show --json` outputs the current project configuration in JSON format.
59
+ Sets the project's main work branch. Re-running updates the main work branch configuration. `init show` provides an interactive panel for viewing and modifying project settings (e.g., commands to auto-run after validate succeeds, postCreate hook commands after worktree creation, claudeCodeCommand, etc.). `init show --json` outputs the current project configuration in JSON format.
60
60
 
61
61
  ### `clawt run` — Create worktree and execute tasks
62
62
 
@@ -334,7 +334,7 @@ Configuration file is located at `~/.clawt/config.json`, auto-generated after in
334
334
  | Config Item | Default | Description |
335
335
  | ------ | ------ | ---- |
336
336
  | `autoDeleteBranch` | `false` | Auto-delete merged/removed branches |
337
- | `claudeCodeCommand` | `"claude"` | Claude Code CLI launch command |
337
+ | `claudeCodeCommand` | `"claude"` | Claude Code CLI launch command (can be overridden per-project via `clawt init show`) |
338
338
  | `autoPullPush` | `false` | Auto pull/push after merge |
339
339
  | `confirmDestructiveOps` | `true` | Confirm before destructive operations |
340
340
  | `maxConcurrency` | `0` | Max concurrency for run command, `0` means unlimited |
package/dist/index.js CHANGED
@@ -704,7 +704,6 @@ var VALID_TERMINAL_APPS = ["auto", "iterm2", "terminal", "cmux"];
704
704
  var ITERM2_APP_PATH = "/Applications/iTerm.app";
705
705
 
706
706
  // src/constants/config.ts
707
- var APPEND_SYSTEM_PROMPT = "Currently, you are in the git worktree directory.";
708
707
  var CLAUDE_CODE_ENTRYPOINT_VALUE = "cli";
709
708
  var CONFIG_DEFINITIONS = {
710
709
  autoDeleteBranch: {
@@ -782,6 +781,10 @@ var PROJECT_CONFIG_DEFINITIONS = {
782
781
  postCreate: {
783
782
  defaultValue: void 0,
784
783
  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"
784
+ },
785
+ claudeCodeCommand: {
786
+ defaultValue: void 0,
787
+ description: "Claude Code CLI \u542F\u52A8\u6307\u4EE4\uFF08\u672A\u8BBE\u7F6E\u65F6\u56DE\u9000\u5230\u5168\u5C40\u914D\u7F6E\uFF09"
785
788
  }
786
789
  };
787
790
  function deriveDefaultConfig2(definitions) {
@@ -1662,7 +1665,7 @@ function validateBranchesNotExist(branchNames) {
1662
1665
  }
1663
1666
 
1664
1667
  // src/utils/project-config.ts
1665
- import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
1668
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1666
1669
  import { join as join4 } from "path";
1667
1670
 
1668
1671
  // src/utils/fs.ts
@@ -1704,6 +1707,101 @@ function calculateDirSize(dirPath) {
1704
1707
  return totalSize;
1705
1708
  }
1706
1709
 
1710
+ // src/utils/config.ts
1711
+ import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
1712
+ function loadConfig() {
1713
+ if (!existsSync3(CONFIG_PATH)) {
1714
+ return { ...DEFAULT_CONFIG };
1715
+ }
1716
+ try {
1717
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
1718
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1719
+ } catch {
1720
+ logger.warn(MESSAGES.CONFIG_CORRUPTED);
1721
+ writeDefaultConfig();
1722
+ return { ...DEFAULT_CONFIG };
1723
+ }
1724
+ }
1725
+ function writeConfig(config2) {
1726
+ writeFileSync(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
1727
+ }
1728
+ function writeDefaultConfig() {
1729
+ writeConfig(DEFAULT_CONFIG);
1730
+ }
1731
+ function saveConfig(config2) {
1732
+ writeFileSync(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
1733
+ }
1734
+ function getConfigValue(key) {
1735
+ const config2 = loadConfig();
1736
+ return config2[key];
1737
+ }
1738
+ function ensureClawtDirs() {
1739
+ ensureDir(CLAWT_HOME);
1740
+ ensureDir(LOGS_DIR);
1741
+ ensureDir(WORKTREES_DIR);
1742
+ ensureDir(PROJECTS_CONFIG_DIR);
1743
+ }
1744
+ function parseConcurrency(optionValue, configValue) {
1745
+ if (optionValue === void 0) {
1746
+ return configValue;
1747
+ }
1748
+ const parsed = parseInt(optionValue, 10);
1749
+ if (Number.isNaN(parsed) || parsed < 0) {
1750
+ throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
1751
+ }
1752
+ return parsed;
1753
+ }
1754
+
1755
+ // src/utils/json.ts
1756
+ function primitiveToString(value) {
1757
+ if (value === void 0) {
1758
+ return "undefined";
1759
+ }
1760
+ if (value === null) {
1761
+ return "null";
1762
+ }
1763
+ if (typeof value === "symbol") {
1764
+ return value.toString();
1765
+ }
1766
+ if (typeof value === "function") {
1767
+ return `[Function: ${value.name || "anonymous"}]`;
1768
+ }
1769
+ return String(value);
1770
+ }
1771
+ function safeStringify(value, indent = 2) {
1772
+ if (value === null || typeof value !== "object") {
1773
+ return primitiveToString(value);
1774
+ }
1775
+ try {
1776
+ const seen = /* @__PURE__ */ new WeakSet();
1777
+ return JSON.stringify(
1778
+ value,
1779
+ (_key, val) => {
1780
+ if (typeof val === "bigint") {
1781
+ return val.toString();
1782
+ }
1783
+ if (typeof val === "undefined" || typeof val === "function" || typeof val === "symbol") {
1784
+ return primitiveToString(val);
1785
+ }
1786
+ if (typeof val === "object" && val !== null) {
1787
+ if (seen.has(val)) {
1788
+ return "[Circular]";
1789
+ }
1790
+ seen.add(val);
1791
+ }
1792
+ return val;
1793
+ },
1794
+ indent
1795
+ );
1796
+ } catch {
1797
+ try {
1798
+ return JSON.stringify(String(value), null, indent);
1799
+ } catch {
1800
+ return "[Unserializable]";
1801
+ }
1802
+ }
1803
+ }
1804
+
1707
1805
  // src/utils/project-config.ts
1708
1806
  function getProjectConfigPath(projectName) {
1709
1807
  return join4(PROJECTS_CONFIG_DIR, projectName, "config.json");
@@ -1711,11 +1809,11 @@ function getProjectConfigPath(projectName) {
1711
1809
  function loadProjectConfig() {
1712
1810
  const projectName = getProjectName();
1713
1811
  const configPath = getProjectConfigPath(projectName);
1714
- if (!existsSync3(configPath)) {
1812
+ if (!existsSync4(configPath)) {
1715
1813
  return null;
1716
1814
  }
1717
1815
  try {
1718
- const raw = readFileSync(configPath, "utf-8");
1816
+ const raw = readFileSync2(configPath, "utf-8");
1719
1817
  return JSON.parse(raw);
1720
1818
  } catch (error) {
1721
1819
  logger.warn(`\u9879\u76EE\u914D\u7F6E\u6587\u4EF6\u89E3\u6790\u5931\u8D25: ${error}`);
@@ -1727,7 +1825,7 @@ function saveProjectConfig(config2) {
1727
1825
  const configPath = getProjectConfigPath(projectName);
1728
1826
  const projectDir = join4(PROJECTS_CONFIG_DIR, projectName);
1729
1827
  ensureDir(projectDir);
1730
- writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf-8");
1828
+ writeFileSync2(configPath, safeStringify({ ...config2 }, 2), "utf-8");
1731
1829
  logger.info(`\u9879\u76EE\u914D\u7F6E\u5DF2\u4FDD\u5B58: ${configPath}`);
1732
1830
  }
1733
1831
  function requireProjectConfig() {
@@ -1755,6 +1853,24 @@ function getValidateRunCommand() {
1755
1853
  const config2 = loadProjectConfig();
1756
1854
  return config2?.validateRunCommand || void 0;
1757
1855
  }
1856
+ function resolveClaudeCodeCommand() {
1857
+ const projectConfig = loadProjectConfig();
1858
+ if (projectConfig?.claudeCodeCommand) {
1859
+ return projectConfig.claudeCodeCommand;
1860
+ }
1861
+ return getConfigValue("claudeCodeCommand");
1862
+ }
1863
+ function normalizeProjectConfig(config2, key, value) {
1864
+ if (value === "") {
1865
+ const def = PROJECT_CONFIG_DEFINITIONS[key];
1866
+ if (def?.defaultValue === void 0) {
1867
+ const normalized = { ...config2 };
1868
+ delete normalized[key];
1869
+ return normalized;
1870
+ }
1871
+ }
1872
+ return config2;
1873
+ }
1758
1874
 
1759
1875
  // src/utils/validate-branch.ts
1760
1876
  import Enquirer from "enquirer";
@@ -1921,7 +2037,7 @@ async function runPreChecks(options) {
1921
2037
 
1922
2038
  // src/utils/worktree.ts
1923
2039
  import { join as join5 } from "path";
1924
- import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
2040
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
1925
2041
  function getProjectWorktreeDir() {
1926
2042
  const projectName = getProjectName();
1927
2043
  return join5(WORKTREES_DIR, projectName);
@@ -1958,7 +2074,7 @@ function createWorktreesByBranches(branchNames) {
1958
2074
  }
1959
2075
  function getProjectWorktrees() {
1960
2076
  const projectDir = getProjectWorktreeDir();
1961
- if (!existsSync4(projectDir)) {
2077
+ if (!existsSync5(projectDir)) {
1962
2078
  return [];
1963
2079
  }
1964
2080
  const worktreeListOutput = gitWorktreeList();
@@ -2008,51 +2124,6 @@ function getWorktreeStatus(worktree) {
2008
2124
  }
2009
2125
  }
2010
2126
 
2011
- // src/utils/config.ts
2012
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2013
- function loadConfig() {
2014
- if (!existsSync5(CONFIG_PATH)) {
2015
- return { ...DEFAULT_CONFIG };
2016
- }
2017
- try {
2018
- const raw = readFileSync2(CONFIG_PATH, "utf-8");
2019
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
2020
- } catch {
2021
- logger.warn(MESSAGES.CONFIG_CORRUPTED);
2022
- writeDefaultConfig();
2023
- return { ...DEFAULT_CONFIG };
2024
- }
2025
- }
2026
- function writeConfig(config2) {
2027
- writeFileSync2(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
2028
- }
2029
- function writeDefaultConfig() {
2030
- writeConfig(DEFAULT_CONFIG);
2031
- }
2032
- function saveConfig(config2) {
2033
- writeFileSync2(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
2034
- }
2035
- function getConfigValue(key) {
2036
- const config2 = loadConfig();
2037
- return config2[key];
2038
- }
2039
- function ensureClawtDirs() {
2040
- ensureDir(CLAWT_HOME);
2041
- ensureDir(LOGS_DIR);
2042
- ensureDir(WORKTREES_DIR);
2043
- ensureDir(PROJECTS_CONFIG_DIR);
2044
- }
2045
- function parseConcurrency(optionValue, configValue) {
2046
- if (optionValue === void 0) {
2047
- return configValue;
2048
- }
2049
- const parsed = parseInt(optionValue, 10);
2050
- if (Number.isNaN(parsed) || parsed < 0) {
2051
- throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
2052
- }
2053
- return parsed;
2054
- }
2055
-
2056
2127
  // src/utils/prompt.ts
2057
2128
  import Enquirer2 from "enquirer";
2058
2129
  async function promptCommitMessage(promptMessage, nonInteractiveErrorMessage) {
@@ -2224,13 +2295,11 @@ function hasClaudeSessionHistory(worktreePath) {
2224
2295
  return entries.some((entry) => entry.endsWith(".jsonl"));
2225
2296
  }
2226
2297
  function launchInteractiveClaude(worktree, options = {}) {
2227
- const commandStr = getConfigValue("claudeCodeCommand");
2298
+ const commandStr = resolveClaudeCodeCommand();
2228
2299
  const parts = commandStr.split(/\s+/).filter(Boolean);
2229
2300
  const cmd = parts[0];
2230
2301
  const args = [
2231
- ...parts.slice(1),
2232
- "--append-system-prompt",
2233
- APPEND_SYSTEM_PROMPT
2302
+ ...parts.slice(1)
2234
2303
  ];
2235
2304
  const hasPreviousSession = options.autoContinue === true && hasClaudeSessionHistory(worktree.path);
2236
2305
  if (hasPreviousSession) {
@@ -2259,12 +2328,10 @@ function escapeShellSingleQuote(str) {
2259
2328
  return str.replace(/'/g, "'\\''");
2260
2329
  }
2261
2330
  function buildClaudeCommand(worktree, hasPreviousSession) {
2262
- const commandStr = getConfigValue("claudeCodeCommand");
2263
- const systemPrompt = APPEND_SYSTEM_PROMPT;
2331
+ const commandStr = resolveClaudeCodeCommand();
2264
2332
  const escapedPath = escapeShellSingleQuote(worktree.path);
2265
- const escapedPrompt = escapeShellSingleQuote(systemPrompt);
2266
2333
  const continueFlag = hasPreviousSession ? " --continue" : "";
2267
- return `cd '${escapedPath}' && ${commandStr} --append-system-prompt '${escapedPrompt}'${continueFlag}`;
2334
+ return `cd '${escapedPath}' && ${commandStr}${continueFlag}`;
2268
2335
  }
2269
2336
  function launchInteractiveClaudeInNewTerminal(worktree, hasPreviousSession) {
2270
2337
  const command = buildClaudeCommand(worktree, hasPreviousSession);
@@ -3040,8 +3107,7 @@ function parseStreamEvent(event) {
3040
3107
 
3041
3108
  // src/utils/task-executor.ts
3042
3109
  function executeClaudeTask(worktree, task, onActivity, continueSession) {
3043
- const systemPrompt = APPEND_SYSTEM_PROMPT;
3044
- const args = ["-p", task, "--output-format", "stream-json", "--verbose", "--permission-mode", "bypassPermissions", "--append-system-prompt", systemPrompt];
3110
+ const args = ["-p", task, "--output-format", "stream-json", "--verbose", "--permission-mode", "bypassPermissions"];
3045
3111
  if (continueSession) {
3046
3112
  args.push("--continue");
3047
3113
  }
@@ -3586,56 +3652,6 @@ async function checkForUpdates(currentVersion) {
3586
3652
  }
3587
3653
  }
3588
3654
 
3589
- // src/utils/json.ts
3590
- function primitiveToString(value) {
3591
- if (value === void 0) {
3592
- return "undefined";
3593
- }
3594
- if (value === null) {
3595
- return "null";
3596
- }
3597
- if (typeof value === "symbol") {
3598
- return value.toString();
3599
- }
3600
- if (typeof value === "function") {
3601
- return `[Function: ${value.name || "anonymous"}]`;
3602
- }
3603
- return String(value);
3604
- }
3605
- function safeStringify(value, indent = 2) {
3606
- if (value === null || typeof value !== "object") {
3607
- return primitiveToString(value);
3608
- }
3609
- try {
3610
- const seen = /* @__PURE__ */ new WeakSet();
3611
- return JSON.stringify(
3612
- value,
3613
- (_key, val) => {
3614
- if (typeof val === "bigint") {
3615
- return val.toString();
3616
- }
3617
- if (typeof val === "undefined" || typeof val === "function" || typeof val === "symbol") {
3618
- return primitiveToString(val);
3619
- }
3620
- if (typeof val === "object" && val !== null) {
3621
- if (seen.has(val)) {
3622
- return "[Circular]";
3623
- }
3624
- seen.add(val);
3625
- }
3626
- return val;
3627
- },
3628
- indent
3629
- );
3630
- } catch {
3631
- try {
3632
- return JSON.stringify(String(value), null, indent);
3633
- } catch {
3634
- return "[Unserializable]";
3635
- }
3636
- }
3637
- }
3638
-
3639
3655
  // src/utils/validate-runner.ts
3640
3656
  function handleErrorClipboard(clipboardContent) {
3641
3657
  const success = copyToClipboard(clipboardContent);
@@ -6281,7 +6297,8 @@ async function handleInitShow(options) {
6281
6297
  PROJECT_CONFIG_DEFINITIONS,
6282
6298
  { selectPrompt: MESSAGES.INIT_SELECT_PROMPT }
6283
6299
  );
6284
- const updatedConfig = { ...config2, [key]: newValue };
6300
+ const mergedConfig = { ...config2, [key]: newValue };
6301
+ const updatedConfig = normalizeProjectConfig(mergedConfig, key, newValue);
6285
6302
  saveProjectConfig(updatedConfig);
6286
6303
  printSuccess(MESSAGES.INIT_SET_SUCCESS(key, String(newValue)));
6287
6304
  }
@@ -724,6 +724,10 @@ var PROJECT_CONFIG_DEFINITIONS = {
724
724
  postCreate: {
725
725
  defaultValue: void 0,
726
726
  description: "worktree \u521B\u5EFA\u540E\u81EA\u52A8\u6267\u884C\u7684\u547D\u4EE4\uFF0C\u7528\u4E8E\u5B89\u88C5\u4F9D\u8D56\u7B49\u521D\u59CB\u5316\u64CD\u4F5C"
727
+ },
728
+ claudeCodeCommand: {
729
+ defaultValue: void 0,
730
+ description: "Claude Code CLI \u542F\u52A8\u6307\u4EE4\uFF08\u672A\u8BBE\u7F6E\u65F6\u56DE\u9000\u5230\u5168\u5C40\u914D\u7F6E\uFF09"
727
731
  }
728
732
  };
729
733
  function deriveDefaultConfig2(definitions) {
@@ -35,7 +35,7 @@
35
35
  | 配置项 | 类型 | 默认值 | 说明 |
36
36
  | ------------------ | --------- | --------- | -------------------------------------------------- |
37
37
  | `autoDeleteBranch` | `boolean` | `false` | 移除 worktree 时是否自动删除对应本地分支(无需每次确认);merge 成功后是否自动清理 worktree 和分支;run 任务被中断(Ctrl+C)后是否自动清理本次创建的 worktree 和分支 |
38
- | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree 中打开交互式界面 |
38
+ | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree 中打开交互式界面。可被项目级配置 `claudeCodeCommand` 覆盖(优先级:项目级 > 全局级,详见 [project-config.md](./project-config.md)) |
39
39
  | `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
40
40
  | `confirmDestructiveOps` | `boolean` | `true` | 执行破坏性操作(reset、validate --clean)前是否提示确认 |
41
41
  | `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
package/docs/init.md CHANGED
@@ -26,7 +26,7 @@ clawt init show --json
26
26
 
27
27
  **功能说明:**
28
28
 
29
- 初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。`init show` 子命令提供交互式面板,可查看和修改所有项目配置项(如 `validateRunCommand`、`postCreate`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
29
+ 初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。`init show` 子命令提供交互式面板,可查看和修改所有项目配置项(如 `validateRunCommand`、`postCreate`、`claudeCodeCommand`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
30
30
 
31
31
  **运行流程(设置模式):**
32
32
 
@@ -53,7 +53,7 @@ clawt init show --json
53
53
  3. **交互式配置编辑**:调用 `interactiveConfigEditor`(`src/utils/config-strategy.ts`),基于 `PROJECT_CONFIG_DEFINITIONS` 构建配置项列表(详见 [project-config.md](./project-config.md))
54
54
  - 列出所有项目配置项,显示名称、当前值和描述
55
55
  - 用户选择配置项后,根据值类型自动选择输入方式(与全局配置的交互式编辑逻辑一致)
56
- 4. **持久化修改**:将修改后的值合并到当前配置并写入配置文件
56
+ 4. **持久化修改**:将修改后的值合并到当前配置,经 `normalizeProjectConfig` 归一化处理后写入配置文件(可选字段的空字符串会被移除,等同于未设置)
57
57
  5. **输出成功提示**:`✓ 项目配置 <key> 已设置为 <value>`
58
58
 
59
59
  **输出格式:**
@@ -85,5 +85,6 @@ clawt init show --json
85
85
  - `init show` 子命令从 JSON 展示改为交互式面板,调用 `interactiveConfigEditor`(`src/utils/config-strategy.ts`)实现通用交互式配置编辑
86
86
  - 配置项定义来自 `PROJECT_CONFIG_DEFINITIONS`(`src/constants/project-config.ts`),详见 [项目级配置文档](./project-config.md)
87
87
  - 消息常量:`MESSAGES.INIT_SELECT_PROMPT`(选择配置项提示语)、`MESSAGES.INIT_SET_SUCCESS`(修改成功提示),定义在 `src/constants/messages/init.ts`
88
+ - `handleInitShow` 使用 `normalizeProjectConfig` 对修改后的配置进行归一化处理:可选字段(如 `validateRunCommand`、`postCreate`、`claudeCodeCommand`)设为空字符串时自动移除该键,避免 JSON 文件中出现冗余的 `"field": ""` 条目
88
89
 
89
90
  ---
@@ -4,7 +4,7 @@
4
4
 
5
5
  postCreate hook 是在 worktree 创建完成后自动执行的钩子命令,可用于执行任意初始化操作(如安装依赖、生成配置文件、编译资源等)。`create` 和 `run` 命令在创建 worktree 之后,会尝试解析并执行 postCreate hook。
6
6
 
7
- hook 以 **fire-and-forget** 模式后台异步并行执行,不阻塞主流程(不 await)。执行结果仅写入日志,不影响后续 Claude Code 的启动或系统提示。
7
+ hook 以 **fire-and-forget** 模式后台异步并行执行,不阻塞主流程(不 await)。执行结果仅写入日志,不影响后续 Claude Code 的启动。
8
8
 
9
9
  #### 配置方式
10
10
 
@@ -100,10 +100,6 @@ clawt run -b feat --no-post-create
100
100
  - **结果汇总**:后台执行完毕后通过 `.then()` 回调写入日志汇总(成功数 + 失败数)
101
101
  - **返回值**:`runPostCreateHooks()` 返回 `void`——以 fire-and-forget 模式后台执行,不等待结果
102
102
 
103
- #### 系统提示
104
-
105
- Claude Code 启动时统一使用 `APPEND_SYSTEM_PROMPT` 常量(定义在 `src/constants/config.ts`)作为 `--append-system-prompt` 参数值,内容为通用的 worktree 目录提示,不因 hook 执行结果而变化。
106
-
107
103
  #### 相关类型定义
108
104
 
109
105
  类型定义位于 `src/types/postCreateHook.ts`:
@@ -18,7 +18,8 @@
18
18
  {
19
19
  "clawtMainWorkBranch": "main",
20
20
  "validateRunCommand": "npm test",
21
- "postCreate": "npm install"
21
+ "postCreate": "npm install",
22
+ "claudeCodeCommand": "claude --model opus"
22
23
  }
23
24
  ```
24
25
 
@@ -27,6 +28,7 @@
27
28
  | `clawtMainWorkBranch` | `string` | 是 | `""` | 项目的主工作分支名,用于 create 时检测当前分支是否为主分支,以及 sync、merge 等命令获取主分支名 |
28
29
  | `validateRunCommand` | `string` | 否 | `undefined` | validate 成功后自动执行的命令(作为 `-r` 选项的默认值)。不传 `-r` 时,validate 命令会自动从此项读取 |
29
30
  | `postCreate` | `string` | 否 | `undefined` | worktree 创建后自动执行的初始化命令(如安装依赖、生成配置文件、编译资源等)。详见 [post-create-hook.md](./post-create-hook.md) |
31
+ | `claudeCodeCommand` | `string` | 否 | `undefined` | Claude Code CLI 启动指令,项目级覆盖全局配置。未设置时回退到全局配置 `claudeCodeCommand`(详见 [config-file.md](./config-file.md))。优先级:项目级 > 全局级 |
30
32
 
31
33
  #### 配置项定义数据源
32
34
 
@@ -48,6 +50,10 @@ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
48
50
  defaultValue: undefined as unknown as string | undefined,
49
51
  description: 'worktree 创建后自动执行的命令,用于安装依赖等初始化操作',
50
52
  },
53
+ claudeCodeCommand: {
54
+ defaultValue: undefined as unknown as string | undefined,
55
+ description: 'Claude Code CLI 启动指令(未设置时回退到全局配置)',
56
+ },
51
57
  };
52
58
 
53
59
  /** 项目默认配置(从 PROJECT_CONFIG_DEFINITIONS 自动派生) */
@@ -63,7 +69,7 @@ export const PROJECT_CONFIG_DESCRIPTIONS: Record<keyof Required<ProjectConfig>,
63
69
 
64
70
  | 类型 | 说明 |
65
71
  | --- | --- |
66
- | `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)、`validateRunCommand`(可选)和 `postCreate`(可选) |
72
+ | `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)、`validateRunCommand`(可选)、`postCreate`(可选)和 `claudeCodeCommand`(可选) |
67
73
  | `ProjectConfigItemDefinition<T>` | 单个配置项定义,含 `defaultValue`(默认值)、`description`(描述)、可选 `allowedValues`(枚举值列表,仅对 string 类型有效) |
68
74
  | `ProjectConfigDefinitions` | 所有配置项的完整定义映射,键为 `ProjectConfig` 的所有属性名,值为对应的 `ProjectConfigItemDefinition` |
69
75
 
@@ -74,6 +80,7 @@ export interface ProjectConfig {
74
80
  clawtMainWorkBranch: string;
75
81
  validateRunCommand?: string;
76
82
  postCreate?: string;
83
+ claudeCodeCommand?: string;
77
84
  }
78
85
 
79
86
  export interface ProjectConfigItemDefinition<T> {
@@ -99,6 +106,8 @@ export type ProjectConfigDefinitions = {
99
106
  | `requireProjectConfig` | `() => ProjectConfig` | 获取当前项目配置,不存在或缺少 `clawtMainWorkBranch` 时抛出 `ClawtError` |
100
107
  | `getMainWorkBranch` | `() => string` | 从项目配置中获取主工作分支名(内部调用 `requireProjectConfig`) |
101
108
  | `getValidateRunCommand` | `() => string \| undefined` | 从项目配置中获取 validate 自动执行命令,未配置时返回 `undefined` |
109
+ | `resolveClaudeCodeCommand` | `() => string` | 解析当前项目生效的 Claude Code 启动指令,优先级:项目级配置 > 全局配置 |
110
+ | `normalizeProjectConfig` | `(config: ProjectConfig, key: string, value: unknown) => ProjectConfig` | 归一化项目配置:可选字段的空字符串等同于未设置,从对象中删除该键以保持 JSON 文件整洁 |
102
111
 
103
112
  #### 设置方式
104
113
 
package/docs/run.md CHANGED
@@ -86,9 +86,8 @@ clawt run -b <branchName>
86
86
  5. 通过公共函数 `executeBatchTasks`(`src/utils/task-executor.ts`)启动批量任务执行,该函数负责进度面板渲染、SIGINT 中断处理、并发控制和汇总输出。对每个 worktree 并行启动 Claude Code CLI:
87
87
  ```bash
88
88
  cd ~/.clawt/worktrees/<project>/<branchName>-<i>
89
- claude -p "<tasks[i]>" --output-format stream-json --verbose --permission-mode bypassPermissions --append-system-prompt "<系统提示>"
89
+ claude -p "<tasks[i]>" --output-format stream-json --verbose --permission-mode bypassPermissions
90
90
  ```
91
- 其中 `--append-system-prompt` 使用统一的 `APPEND_SYSTEM_PROMPT` 常量(定义在 `src/constants/config.ts`)。
92
91
  子进程通过 `spawnProcess()`(`src/utils/shell.ts`)启动,会自动注入环境变量 `CLAUDE_CODE_ENTRYPOINT="cli"`(通过 `getEnvWithoutNestedSessionFlag()` 函数),使会话支持通过 `--continue` 恢复。
93
92
  使用 `stream-json` 格式可实时获取 Claude Code 的流式事件(工具调用、文本输出、最终结果),用于在进度面板中显示每个任务的实时活动描述和结果预览。流式事件解析由 `src/utils/stream-parser.ts` 负责。
94
93
  6. 进入**事件监听通知**阶段(见 [5.3](#53-任务完成通知机制))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.9.7",
3
+ "version": "3.9.9",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,6 +13,7 @@ import {
13
13
  printSuccess,
14
14
  interactiveConfigEditor,
15
15
  safeStringify,
16
+ normalizeProjectConfig,
16
17
  } from '../utils/index.js';
17
18
 
18
19
  /**
@@ -62,7 +63,8 @@ async function handleInitShow(options: InitShowOptions): Promise<void> {
62
63
  );
63
64
 
64
65
  // 合并修改后的值并持久化
65
- const updatedConfig: ProjectConfig = { ...config, [key]: newValue };
66
+ const mergedConfig: ProjectConfig = { ...config, [key]: newValue };
67
+ const updatedConfig = normalizeProjectConfig(mergedConfig, key as string, newValue);
66
68
  saveProjectConfig(updatedConfig);
67
69
 
68
70
  printSuccess(MESSAGES.INIT_SET_SUCCESS(key as string, String(newValue)));
@@ -1,10 +1,6 @@
1
1
  import type { ClawtConfig, ConfigDefinitions } from '../types/index.js';
2
2
  import { VALID_TERMINAL_APPS } from './terminal.js';
3
3
 
4
- /** Claude Code 系统约束提示 */
5
- export const APPEND_SYSTEM_PROMPT =
6
- 'Currently, you are in the git worktree directory.';
7
-
8
4
  /**
9
5
  * 通过 clawt 启动的 Claude Code 非交互式会话(claude -p)的 entrypoint 标识
10
6
  * 设置为 'cli' 使 claude -p 启动的会话可以通过 --continue 恢复
@@ -5,7 +5,7 @@ export { CONFIG_ALIAS_DISABLED_HINT } from './messages/index.js';
5
5
  export { UPDATE_MESSAGES, UPDATE_COMMANDS } from './messages/update.js';
6
6
  export { EXIT_CODES } from './exitCodes.js';
7
7
  export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS, VALID_TERMINAL_APPS, ITERM2_APP_PATH } from './terminal.js';
8
- export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, CONFIG_DEFINITIONS, APPEND_SYSTEM_PROMPT, CLAUDE_CODE_ENTRYPOINT_VALUE } from './config.js';
8
+ export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, CONFIG_DEFINITIONS, CLAUDE_CODE_ENTRYPOINT_VALUE } from './config.js';
9
9
  export { PROJECT_CONFIG_DEFINITIONS, PROJECT_DEFAULT_CONFIG, PROJECT_CONFIG_DESCRIPTIONS } from './project-config.js';
10
10
  export { AUTO_SAVE_COMMIT_MESSAGE_PREFIX } from './git.js';
11
11
  export { DEBUG_LOG_PREFIX, DEBUG_TIMESTAMP_FORMAT } from './logger.js';
@@ -17,6 +17,10 @@ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
17
17
  defaultValue: undefined as unknown as string | undefined,
18
18
  description: 'worktree 创建后自动执行的命令,用于安装依赖等初始化操作',
19
19
  },
20
+ claudeCodeCommand: {
21
+ defaultValue: undefined as unknown as string | undefined,
22
+ description: 'Claude Code CLI 启动指令(未设置时回退到全局配置)',
23
+ },
20
24
  };
21
25
 
22
26
  /**
@@ -6,6 +6,8 @@ export interface ProjectConfig {
6
6
  validateRunCommand?: string;
7
7
  /** worktree 创建后自动执行的命令,用于安装依赖等初始化操作 */
8
8
  postCreate?: string;
9
+ /** Claude Code CLI 启动指令(项目级覆盖全局配置,未设置时回退到全局配置) */
10
+ claudeCodeCommand?: string;
9
11
  }
10
12
 
11
13
  /** 单个项目配置项的完整定义(默认值 + 描述) */
@@ -2,8 +2,8 @@ import { spawnSync } from 'node:child_process';
2
2
  import { existsSync, readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { ClawtError } from '../errors/index.js';
5
- import { APPEND_SYSTEM_PROMPT, CLAUDE_PROJECTS_DIR } from '../constants/index.js';
6
- import { getConfigValue } from './config.js';
5
+ import { CLAUDE_PROJECTS_DIR } from '../constants/index.js';
6
+ import { resolveClaudeCodeCommand } from './project-config.js';
7
7
  import { printInfo, printWarning } from './formatter.js';
8
8
  import { openCommandInNewTerminalTab } from './terminal.js';
9
9
  import type { WorktreeInfo } from '../types/index.js';
@@ -51,13 +51,11 @@ interface LaunchClaudeOptions {
51
51
  }
52
52
 
53
53
  export function launchInteractiveClaude(worktree: WorktreeInfo, options: LaunchClaudeOptions = {}): void {
54
- const commandStr = getConfigValue('claudeCodeCommand');
54
+ const commandStr = resolveClaudeCodeCommand();
55
55
  const parts = commandStr.split(/\s+/).filter(Boolean);
56
56
  const cmd = parts[0];
57
57
  const args = [
58
58
  ...parts.slice(1),
59
- '--append-system-prompt',
60
- APPEND_SYSTEM_PROMPT,
61
59
  ];
62
60
 
63
61
  // 仅在启用 autoContinue 时检测历史会话并追加 --continue
@@ -101,20 +99,18 @@ function escapeShellSingleQuote(str: string): string {
101
99
 
102
100
  /**
103
101
  * 构建在指定 worktree 中启动 Claude Code 的完整 shell 命令
104
- * 生成格式:cd <path> && <claudeCommand> --append-system-prompt '...' [--continue]
102
+ * 生成格式:cd <path> && <claudeCommand> [--continue]
105
103
  * @param {WorktreeInfo} worktree - worktree 信息
106
104
  * @param {boolean} hasPreviousSession - 是否存在历史会话(由调用方预计算,避免重复 I/O)
107
105
  * @returns {string} 完整的 shell 命令字符串
108
106
  */
109
107
  export function buildClaudeCommand(worktree: WorktreeInfo, hasPreviousSession: boolean): string {
110
- const commandStr = getConfigValue('claudeCodeCommand');
111
- const systemPrompt = APPEND_SYSTEM_PROMPT;
108
+ const commandStr = resolveClaudeCodeCommand();
112
109
 
113
110
  const escapedPath = escapeShellSingleQuote(worktree.path);
114
- const escapedPrompt = escapeShellSingleQuote(systemPrompt);
115
111
  const continueFlag = hasPreviousSession ? ' --continue' : '';
116
112
 
117
- return `cd '${escapedPath}' && ${commandStr} --append-system-prompt '${escapedPrompt}'${continueFlag}`;
113
+ return `cd '${escapedPath}' && ${commandStr}${continueFlag}`;
118
114
  }
119
115
 
120
116
  /**
@@ -86,7 +86,7 @@ export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
86
86
  export { applyAliases } from './alias.js';
87
87
  export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue, interactiveConfigEditor } from './config-strategy.js';
88
88
  export { checkForUpdates } from './update-checker.js';
89
- export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand } from './project-config.js';
89
+ export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand, resolveClaudeCodeCommand, normalizeProjectConfig } from './project-config.js';
90
90
  export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
91
91
  export { safeStringify } from './json.js';
92
92
  export { isNonInteractive, setNonInteractive } from './interactive.js';
@@ -1,10 +1,12 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { PROJECTS_CONFIG_DIR, MESSAGES } from '../constants/index.js';
3
+ import { PROJECTS_CONFIG_DIR, MESSAGES, PROJECT_CONFIG_DEFINITIONS } from '../constants/index.js';
4
4
  import { ClawtError } from '../errors/index.js';
5
5
  import { logger } from '../logger/index.js';
6
6
  import { getProjectName, checkBranchExists } from './git.js';
7
7
  import { ensureDir } from './fs.js';
8
+ import { getConfigValue } from './config.js';
9
+ import { safeStringify } from './json.js';
8
10
  import type { ProjectConfig } from '../types/index.js';
9
11
 
10
12
  /**
@@ -47,7 +49,7 @@ export function saveProjectConfig(config: ProjectConfig): void {
47
49
  // 确保项目子目录存在
48
50
  const projectDir = join(PROJECTS_CONFIG_DIR, projectName);
49
51
  ensureDir(projectDir);
50
- writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
52
+ writeFileSync(configPath, safeStringify({ ...config }, 2), 'utf-8');
51
53
  logger.info(`项目配置已保存: ${configPath}`);
52
54
  }
53
55
 
@@ -99,3 +101,36 @@ export function getValidateRunCommand(): string | undefined {
99
101
  const config = loadProjectConfig();
100
102
  return config?.validateRunCommand || undefined;
101
103
  }
104
+
105
+ /**
106
+ * 解析当前项目生效的 Claude Code 启动指令
107
+ * 优先级:项目级配置 > 全局配置
108
+ * @returns {string} 生效的 Claude Code 启动指令
109
+ */
110
+ export function resolveClaudeCodeCommand(): string {
111
+ const projectConfig = loadProjectConfig();
112
+ if (projectConfig?.claudeCodeCommand) {
113
+ return projectConfig.claudeCodeCommand;
114
+ }
115
+ return getConfigValue('claudeCodeCommand');
116
+ }
117
+
118
+ /**
119
+ * 归一化项目配置:可选字段的空字符串等同于未设置,从对象中删除该键
120
+ * 保持 JSON 文件整洁,避免出现 "field": "" 的冗余条目
121
+ * @param {ProjectConfig} config - 原始项目配置
122
+ * @param {string} key - 被修改的配置项键名
123
+ * @param {unknown} value - 被修改的配置项新值
124
+ * @returns {ProjectConfig} 归一化后的项目配置
125
+ */
126
+ export function normalizeProjectConfig(config: ProjectConfig, key: string, value: unknown): ProjectConfig {
127
+ if (value === '') {
128
+ const def = PROJECT_CONFIG_DEFINITIONS[key as keyof typeof PROJECT_CONFIG_DEFINITIONS];
129
+ if (def?.defaultValue === undefined) {
130
+ const normalized = { ...config };
131
+ delete (normalized as unknown as Record<string, unknown>)[key];
132
+ return normalized;
133
+ }
134
+ }
135
+ return config;
136
+ }
@@ -5,7 +5,6 @@ import type { ClaudeCodeResult, TaskResult, TaskSummary, WorktreeInfo } from '..
5
5
  import { spawnProcess, killAllChildProcesses } from './shell.js';
6
6
  import { cleanupWorktrees } from './worktree.js';
7
7
  import { getConfigValue } from './config.js';
8
- import { APPEND_SYSTEM_PROMPT } from '../constants/index.js';
9
8
  import { printSuccess, printWarning, printInfo, printDoubleSeparator, confirmAction } from './formatter.js';
10
9
  import { ProgressRenderer } from './progress.js';
11
10
  import { createLineBuffer, parseStreamLine, parseStreamEvent, truncateText } from './stream-parser.js';
@@ -35,11 +34,8 @@ type ActivityCallback = (activityText: string) => void;
35
34
  * @returns {ClaudeTaskHandle} 包含子进程引用和结果 Promise
36
35
  */
37
36
  function executeClaudeTask(worktree: WorktreeInfo, task: string, onActivity?: ActivityCallback, continueSession?: boolean): ClaudeTaskHandle {
38
- // 使用统一的系统提示常量
39
- const systemPrompt = APPEND_SYSTEM_PROMPT;
40
-
41
37
  // 旧版使用 --output-format json,现改为 stream-json --verbose 以支持实时活动信息
42
- const args = ['-p', task, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions', '--append-system-prompt', systemPrompt];
38
+ const args = ['-p', task, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions'];
43
39
 
44
40
  // 追问模式:追加 --continue 继续该目录下最新会话
45
41
  if (continueSession) {
@@ -34,6 +34,7 @@ vi.mock('../../../src/utils/index.js', () => ({
34
34
  interactiveConfigEditor: vi.fn(),
35
35
  guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
36
36
  guardMainWorkBranchExists: vi.fn(),
37
+ normalizeProjectConfig: vi.fn((config: unknown) => config),
37
38
  }));
38
39
 
39
40
  import { registerInitCommand } from '../../../src/commands/init.js';
@@ -33,7 +33,6 @@ import { launchInteractiveClaude, hasClaudeSessionHistory, buildClaudeCommand }
33
33
  import { getConfigValue } from '../../../src/utils/config.js';
34
34
  import { printInfo, printWarning } from '../../../src/utils/formatter.js';
35
35
  import { ClawtError } from '../../../src/errors/index.js';
36
- import { APPEND_SYSTEM_PROMPT } from '../../../src/constants/config.js';
37
36
  import { createWorktreeInfo } from '../../helpers/fixtures.js';
38
37
 
39
38
  const mockedSpawnSync = vi.mocked(spawnSync);
@@ -104,7 +103,7 @@ describe('launchInteractiveClaude', () => {
104
103
  expect(mockedGetConfigValue).toHaveBeenCalledWith('claudeCodeCommand');
105
104
  expect(mockedSpawnSync).toHaveBeenCalledWith(
106
105
  'claude',
107
- expect.arrayContaining(['--append-system-prompt']),
106
+ expect.any(Array),
108
107
  expect.objectContaining({
109
108
  cwd: '/tmp/test-worktree',
110
109
  stdio: 'inherit',
@@ -148,7 +147,7 @@ describe('launchInteractiveClaude', () => {
148
147
 
149
148
  expect(mockedSpawnSync).toHaveBeenCalledWith(
150
149
  'npx',
151
- expect.arrayContaining(['claude', '--append-system-prompt']),
150
+ expect.arrayContaining(['claude']),
152
151
  expect.any(Object),
153
152
  );
154
153
  });
@@ -285,7 +284,7 @@ describe('launchInteractiveClaude', () => {
285
284
  expect(callArgs).not.toContain('--continue');
286
285
  });
287
286
 
288
- it('固定使用 APPEND_SYSTEM_PROMPT 作为系统提示', () => {
287
+ it('不包含 --append-system-prompt 参数', () => {
289
288
  mockedGetConfigValue.mockReturnValue('claude');
290
289
  mockedExistsSync.mockReturnValue(false);
291
290
  mockedSpawnSync.mockReturnValue({
@@ -301,8 +300,7 @@ describe('launchInteractiveClaude', () => {
301
300
  launchInteractiveClaude(worktree);
302
301
 
303
302
  const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
304
- const promptIndex = callArgs.indexOf('--append-system-prompt');
305
- expect(callArgs[promptIndex + 1]).toBe(APPEND_SYSTEM_PROMPT);
303
+ expect(callArgs).not.toContain('--append-system-prompt');
306
304
  });
307
305
  });
308
306
 
@@ -319,7 +317,6 @@ describe('buildClaudeCommand', () => {
319
317
 
320
318
  expect(cmd).toContain("cd '/tmp/test-worktree'");
321
319
  expect(cmd).toContain('claude');
322
- expect(cmd).toContain('--append-system-prompt');
323
320
  });
324
321
 
325
322
  it('hasPreviousSession 为 true 时包含 --continue', () => {
@@ -338,12 +335,12 @@ describe('buildClaudeCommand', () => {
338
335
  expect(cmd).not.toContain('--continue');
339
336
  });
340
337
 
341
- it('固定使用 APPEND_SYSTEM_PROMPT 作为系统提示', () => {
338
+ it('不包含 --append-system-prompt 参数', () => {
342
339
  mockedGetConfigValue.mockReturnValue('claude');
343
340
 
344
341
  const cmd = buildClaudeCommand(worktree, false);
345
342
 
346
- expect(cmd).toContain(APPEND_SYSTEM_PROMPT);
343
+ expect(cmd).not.toContain('--append-system-prompt');
347
344
  });
348
345
 
349
346
  it('路径中的单引号被正确转义', () => {
@@ -42,6 +42,16 @@ vi.mock('../../../src/utils/fs.js', () => ({
42
42
  ensureDir: vi.fn(),
43
43
  }));
44
44
 
45
+ // mock config
46
+ vi.mock('../../../src/utils/config.js', () => ({
47
+ getConfigValue: vi.fn().mockReturnValue('claude'),
48
+ }));
49
+
50
+ // mock json
51
+ vi.mock('../../../src/utils/json.js', () => ({
52
+ safeStringify: (value: unknown, indent: number = 2) => JSON.stringify(value, null, indent),
53
+ }));
54
+
45
55
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
46
56
  import {
47
57
  getProjectConfigPath,
@@ -50,7 +60,9 @@ import {
50
60
  requireProjectConfig,
51
61
  getMainWorkBranch,
52
62
  getValidateRunCommand,
63
+ resolveClaudeCodeCommand,
53
64
  } from '../../../src/utils/project-config.js';
65
+ import { getConfigValue } from '../../../src/utils/config.js';
54
66
 
55
67
  const mockedExistsSync = vi.mocked(existsSync);
56
68
  const mockedReadFileSync = vi.mocked(readFileSync);
@@ -166,3 +178,41 @@ describe('getValidateRunCommand', () => {
166
178
  expect(getValidateRunCommand()).toBeUndefined();
167
179
  });
168
180
  });
181
+
182
+ describe('resolveClaudeCodeCommand', () => {
183
+ const mockedGetConfigValue = vi.mocked(getConfigValue);
184
+
185
+ beforeEach(() => {
186
+ mockedGetConfigValue.mockReset();
187
+ mockedGetConfigValue.mockReturnValue('claude');
188
+ });
189
+
190
+ it('项目配置中有 claudeCodeCommand 时返回项目级值', () => {
191
+ mockedExistsSync.mockReturnValue(true);
192
+ mockedReadFileSync.mockReturnValue(JSON.stringify({
193
+ clawtMainWorkBranch: 'main',
194
+ claudeCodeCommand: 'claude --model opus',
195
+ }));
196
+ expect(resolveClaudeCodeCommand()).toBe('claude --model opus');
197
+ });
198
+
199
+ it('项目配置中无 claudeCodeCommand 时回退到全局配置', () => {
200
+ mockedExistsSync.mockReturnValue(true);
201
+ mockedReadFileSync.mockReturnValue(JSON.stringify({ clawtMainWorkBranch: 'main' }));
202
+ expect(resolveClaudeCodeCommand()).toBe('claude');
203
+ });
204
+
205
+ it('项目配置中 claudeCodeCommand 为空字符串时回退到全局配置', () => {
206
+ mockedExistsSync.mockReturnValue(true);
207
+ mockedReadFileSync.mockReturnValue(JSON.stringify({
208
+ clawtMainWorkBranch: 'main',
209
+ claudeCodeCommand: '',
210
+ }));
211
+ expect(resolveClaudeCodeCommand()).toBe('claude');
212
+ });
213
+
214
+ it('项目配置文件不存在时回退到全局配置', () => {
215
+ mockedExistsSync.mockReturnValue(false);
216
+ expect(resolveClaudeCodeCommand()).toBe('claude');
217
+ });
218
+ });