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 +2 -2
- package/dist/index.js +130 -113
- package/dist/postinstall.js +4 -0
- package/docs/config-file.md +1 -1
- package/docs/init.md +3 -2
- package/docs/post-create-hook.md +1 -5
- package/docs/project-config.md +11 -2
- package/docs/run.md +1 -2
- package/package.json +1 -1
- package/src/commands/init.ts +3 -1
- package/src/constants/config.ts +0 -4
- package/src/constants/index.ts +1 -1
- package/src/constants/project-config.ts +4 -0
- package/src/types/projectConfig.ts +2 -0
- package/src/utils/claude.ts +6 -10
- package/src/utils/index.ts +1 -1
- package/src/utils/project-config.ts +37 -2
- package/src/utils/task-executor.ts +1 -5
- package/tests/unit/commands/init.test.ts +1 -0
- package/tests/unit/utils/claude.test.ts +6 -9
- package/tests/unit/utils/project-config.test.ts +50 -0
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
|
|
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 (!
|
|
1812
|
+
if (!existsSync4(configPath)) {
|
|
1715
1813
|
return null;
|
|
1716
1814
|
}
|
|
1717
1815
|
try {
|
|
1718
|
-
const raw =
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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}
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/postinstall.js
CHANGED
|
@@ -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) {
|
package/docs/config-file.md
CHANGED
|
@@ -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
|
---
|
package/docs/post-create-hook.md
CHANGED
|
@@ -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`:
|
package/docs/project-config.md
CHANGED
|
@@ -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`(可选)和 `
|
|
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
|
|
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
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
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)));
|
package/src/constants/config.ts
CHANGED
|
@@ -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 恢复
|
package/src/constants/index.ts
CHANGED
|
@@ -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,
|
|
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
|
/**
|
package/src/utils/claude.ts
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
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 =
|
|
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>
|
|
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 =
|
|
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}
|
|
113
|
+
return `cd '${escapedPath}' && ${commandStr}${continueFlag}`;
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
/**
|
package/src/utils/index.ts
CHANGED
|
@@ -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,
|
|
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'
|
|
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.
|
|
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'
|
|
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('
|
|
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
|
-
|
|
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('
|
|
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(
|
|
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
|
+
});
|