clawt 3.9.7 → 3.9.8

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
@@ -782,6 +782,10 @@ var PROJECT_CONFIG_DEFINITIONS = {
782
782
  postCreate: {
783
783
  defaultValue: void 0,
784
784
  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"
785
+ },
786
+ claudeCodeCommand: {
787
+ defaultValue: void 0,
788
+ description: "Claude Code CLI \u542F\u52A8\u6307\u4EE4\uFF08\u672A\u8BBE\u7F6E\u65F6\u56DE\u9000\u5230\u5168\u5C40\u914D\u7F6E\uFF09"
785
789
  }
786
790
  };
787
791
  function deriveDefaultConfig2(definitions) {
@@ -1662,7 +1666,7 @@ function validateBranchesNotExist(branchNames) {
1662
1666
  }
1663
1667
 
1664
1668
  // src/utils/project-config.ts
1665
- import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
1669
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1666
1670
  import { join as join4 } from "path";
1667
1671
 
1668
1672
  // src/utils/fs.ts
@@ -1704,6 +1708,101 @@ function calculateDirSize(dirPath) {
1704
1708
  return totalSize;
1705
1709
  }
1706
1710
 
1711
+ // src/utils/config.ts
1712
+ import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
1713
+ function loadConfig() {
1714
+ if (!existsSync3(CONFIG_PATH)) {
1715
+ return { ...DEFAULT_CONFIG };
1716
+ }
1717
+ try {
1718
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
1719
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1720
+ } catch {
1721
+ logger.warn(MESSAGES.CONFIG_CORRUPTED);
1722
+ writeDefaultConfig();
1723
+ return { ...DEFAULT_CONFIG };
1724
+ }
1725
+ }
1726
+ function writeConfig(config2) {
1727
+ writeFileSync(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
1728
+ }
1729
+ function writeDefaultConfig() {
1730
+ writeConfig(DEFAULT_CONFIG);
1731
+ }
1732
+ function saveConfig(config2) {
1733
+ writeFileSync(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
1734
+ }
1735
+ function getConfigValue(key) {
1736
+ const config2 = loadConfig();
1737
+ return config2[key];
1738
+ }
1739
+ function ensureClawtDirs() {
1740
+ ensureDir(CLAWT_HOME);
1741
+ ensureDir(LOGS_DIR);
1742
+ ensureDir(WORKTREES_DIR);
1743
+ ensureDir(PROJECTS_CONFIG_DIR);
1744
+ }
1745
+ function parseConcurrency(optionValue, configValue) {
1746
+ if (optionValue === void 0) {
1747
+ return configValue;
1748
+ }
1749
+ const parsed = parseInt(optionValue, 10);
1750
+ if (Number.isNaN(parsed) || parsed < 0) {
1751
+ throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
1752
+ }
1753
+ return parsed;
1754
+ }
1755
+
1756
+ // src/utils/json.ts
1757
+ function primitiveToString(value) {
1758
+ if (value === void 0) {
1759
+ return "undefined";
1760
+ }
1761
+ if (value === null) {
1762
+ return "null";
1763
+ }
1764
+ if (typeof value === "symbol") {
1765
+ return value.toString();
1766
+ }
1767
+ if (typeof value === "function") {
1768
+ return `[Function: ${value.name || "anonymous"}]`;
1769
+ }
1770
+ return String(value);
1771
+ }
1772
+ function safeStringify(value, indent = 2) {
1773
+ if (value === null || typeof value !== "object") {
1774
+ return primitiveToString(value);
1775
+ }
1776
+ try {
1777
+ const seen = /* @__PURE__ */ new WeakSet();
1778
+ return JSON.stringify(
1779
+ value,
1780
+ (_key, val) => {
1781
+ if (typeof val === "bigint") {
1782
+ return val.toString();
1783
+ }
1784
+ if (typeof val === "undefined" || typeof val === "function" || typeof val === "symbol") {
1785
+ return primitiveToString(val);
1786
+ }
1787
+ if (typeof val === "object" && val !== null) {
1788
+ if (seen.has(val)) {
1789
+ return "[Circular]";
1790
+ }
1791
+ seen.add(val);
1792
+ }
1793
+ return val;
1794
+ },
1795
+ indent
1796
+ );
1797
+ } catch {
1798
+ try {
1799
+ return JSON.stringify(String(value), null, indent);
1800
+ } catch {
1801
+ return "[Unserializable]";
1802
+ }
1803
+ }
1804
+ }
1805
+
1707
1806
  // src/utils/project-config.ts
1708
1807
  function getProjectConfigPath(projectName) {
1709
1808
  return join4(PROJECTS_CONFIG_DIR, projectName, "config.json");
@@ -1711,11 +1810,11 @@ function getProjectConfigPath(projectName) {
1711
1810
  function loadProjectConfig() {
1712
1811
  const projectName = getProjectName();
1713
1812
  const configPath = getProjectConfigPath(projectName);
1714
- if (!existsSync3(configPath)) {
1813
+ if (!existsSync4(configPath)) {
1715
1814
  return null;
1716
1815
  }
1717
1816
  try {
1718
- const raw = readFileSync(configPath, "utf-8");
1817
+ const raw = readFileSync2(configPath, "utf-8");
1719
1818
  return JSON.parse(raw);
1720
1819
  } catch (error) {
1721
1820
  logger.warn(`\u9879\u76EE\u914D\u7F6E\u6587\u4EF6\u89E3\u6790\u5931\u8D25: ${error}`);
@@ -1727,7 +1826,7 @@ function saveProjectConfig(config2) {
1727
1826
  const configPath = getProjectConfigPath(projectName);
1728
1827
  const projectDir = join4(PROJECTS_CONFIG_DIR, projectName);
1729
1828
  ensureDir(projectDir);
1730
- writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf-8");
1829
+ writeFileSync2(configPath, safeStringify({ ...config2 }, 2), "utf-8");
1731
1830
  logger.info(`\u9879\u76EE\u914D\u7F6E\u5DF2\u4FDD\u5B58: ${configPath}`);
1732
1831
  }
1733
1832
  function requireProjectConfig() {
@@ -1755,6 +1854,24 @@ function getValidateRunCommand() {
1755
1854
  const config2 = loadProjectConfig();
1756
1855
  return config2?.validateRunCommand || void 0;
1757
1856
  }
1857
+ function resolveClaudeCodeCommand() {
1858
+ const projectConfig = loadProjectConfig();
1859
+ if (projectConfig?.claudeCodeCommand) {
1860
+ return projectConfig.claudeCodeCommand;
1861
+ }
1862
+ return getConfigValue("claudeCodeCommand");
1863
+ }
1864
+ function normalizeProjectConfig(config2, key, value) {
1865
+ if (value === "") {
1866
+ const def = PROJECT_CONFIG_DEFINITIONS[key];
1867
+ if (def?.defaultValue === void 0) {
1868
+ const normalized = { ...config2 };
1869
+ delete normalized[key];
1870
+ return normalized;
1871
+ }
1872
+ }
1873
+ return config2;
1874
+ }
1758
1875
 
1759
1876
  // src/utils/validate-branch.ts
1760
1877
  import Enquirer from "enquirer";
@@ -1921,7 +2038,7 @@ async function runPreChecks(options) {
1921
2038
 
1922
2039
  // src/utils/worktree.ts
1923
2040
  import { join as join5 } from "path";
1924
- import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
2041
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
1925
2042
  function getProjectWorktreeDir() {
1926
2043
  const projectName = getProjectName();
1927
2044
  return join5(WORKTREES_DIR, projectName);
@@ -1958,7 +2075,7 @@ function createWorktreesByBranches(branchNames) {
1958
2075
  }
1959
2076
  function getProjectWorktrees() {
1960
2077
  const projectDir = getProjectWorktreeDir();
1961
- if (!existsSync4(projectDir)) {
2078
+ if (!existsSync5(projectDir)) {
1962
2079
  return [];
1963
2080
  }
1964
2081
  const worktreeListOutput = gitWorktreeList();
@@ -2008,51 +2125,6 @@ function getWorktreeStatus(worktree) {
2008
2125
  }
2009
2126
  }
2010
2127
 
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
2128
  // src/utils/prompt.ts
2057
2129
  import Enquirer2 from "enquirer";
2058
2130
  async function promptCommitMessage(promptMessage, nonInteractiveErrorMessage) {
@@ -2224,7 +2296,7 @@ function hasClaudeSessionHistory(worktreePath) {
2224
2296
  return entries.some((entry) => entry.endsWith(".jsonl"));
2225
2297
  }
2226
2298
  function launchInteractiveClaude(worktree, options = {}) {
2227
- const commandStr = getConfigValue("claudeCodeCommand");
2299
+ const commandStr = resolveClaudeCodeCommand();
2228
2300
  const parts = commandStr.split(/\s+/).filter(Boolean);
2229
2301
  const cmd = parts[0];
2230
2302
  const args = [
@@ -2259,7 +2331,7 @@ function escapeShellSingleQuote(str) {
2259
2331
  return str.replace(/'/g, "'\\''");
2260
2332
  }
2261
2333
  function buildClaudeCommand(worktree, hasPreviousSession) {
2262
- const commandStr = getConfigValue("claudeCodeCommand");
2334
+ const commandStr = resolveClaudeCodeCommand();
2263
2335
  const systemPrompt = APPEND_SYSTEM_PROMPT;
2264
2336
  const escapedPath = escapeShellSingleQuote(worktree.path);
2265
2337
  const escapedPrompt = escapeShellSingleQuote(systemPrompt);
@@ -3586,56 +3658,6 @@ async function checkForUpdates(currentVersion) {
3586
3658
  }
3587
3659
  }
3588
3660
 
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
3661
  // src/utils/validate-runner.ts
3640
3662
  function handleErrorClipboard(clipboardContent) {
3641
3663
  const success = copyToClipboard(clipboardContent);
@@ -6281,7 +6303,8 @@ async function handleInitShow(options) {
6281
6303
  PROJECT_CONFIG_DEFINITIONS,
6282
6304
  { selectPrompt: MESSAGES.INIT_SELECT_PROMPT }
6283
6305
  );
6284
- const updatedConfig = { ...config2, [key]: newValue };
6306
+ const mergedConfig = { ...config2, [key]: newValue };
6307
+ const updatedConfig = normalizeProjectConfig(mergedConfig, key, newValue);
6285
6308
  saveProjectConfig(updatedConfig);
6286
6309
  printSuccess(MESSAGES.INIT_SET_SUCCESS(key, String(newValue)));
6287
6310
  }
@@ -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
  ---
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.9.7",
3
+ "version": "3.9.8",
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)));
@@ -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
  /** 单个项目配置项的完整定义(默认值 + 描述) */
@@ -3,7 +3,7 @@ import { existsSync, readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { ClawtError } from '../errors/index.js';
5
5
  import { APPEND_SYSTEM_PROMPT, CLAUDE_PROJECTS_DIR } from '../constants/index.js';
6
- import { getConfigValue } from './config.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,7 +51,7 @@ 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 = [
@@ -107,7 +107,7 @@ function escapeShellSingleQuote(str: string): string {
107
107
  * @returns {string} 完整的 shell 命令字符串
108
108
  */
109
109
  export function buildClaudeCommand(worktree: WorktreeInfo, hasPreviousSession: boolean): string {
110
- const commandStr = getConfigValue('claudeCodeCommand');
110
+ const commandStr = resolveClaudeCodeCommand();
111
111
  const systemPrompt = APPEND_SYSTEM_PROMPT;
112
112
 
113
113
  const escapedPath = escapeShellSingleQuote(worktree.path);
@@ -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
+ }
@@ -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';
@@ -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
+ });