clawt 3.6.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +53 -7
- package/package.json +1 -1
- package/src/commands/merge.ts +4 -3
- package/src/commands/remove.ts +1 -0
- package/src/index.ts +7 -2
- package/src/utils/config-strategy.ts +7 -0
- package/src/utils/conflict-resolver.ts +3 -1
- package/src/utils/formatter.ts +9 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/interactive.ts +27 -0
- package/src/utils/shell.ts +12 -0
- package/src/utils/ui-prompts.ts +14 -0
- package/src/utils/validate-branch.ts +13 -1
- package/tests/unit/commands/cover-validate.test.ts +1 -0
- package/tests/unit/commands/merge.test.ts +1 -0
- package/tests/unit/commands/remove.test.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -958,6 +958,10 @@ function enableConsoleTransport() {
|
|
|
958
958
|
|
|
959
959
|
// src/utils/shell.ts
|
|
960
960
|
import { execSync, execFileSync, spawn, spawnSync } from "child_process";
|
|
961
|
+
function getEnvWithoutNestedSessionFlag() {
|
|
962
|
+
const { CLAUDECODE: _, ...env } = process.env;
|
|
963
|
+
return env;
|
|
964
|
+
}
|
|
961
965
|
function execCommand(command, options) {
|
|
962
966
|
logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
963
967
|
const result = execSync(command, {
|
|
@@ -971,7 +975,8 @@ function spawnProcess(command, args, options) {
|
|
|
971
975
|
logger.debug(`\u542F\u52A8\u5B50\u8FDB\u7A0B: ${command} ${args.join(" ")}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
972
976
|
return spawn(command, args, {
|
|
973
977
|
cwd: options?.cwd,
|
|
974
|
-
stdio: options?.stdio ?? ["pipe", "pipe", "pipe"]
|
|
978
|
+
stdio: options?.stdio ?? ["pipe", "pipe", "pipe"],
|
|
979
|
+
env: getEnvWithoutNestedSessionFlag()
|
|
975
980
|
});
|
|
976
981
|
}
|
|
977
982
|
function killAllChildProcesses(children) {
|
|
@@ -1308,6 +1313,20 @@ function gitWorktreePrune(cwd) {
|
|
|
1308
1313
|
// src/utils/formatter.ts
|
|
1309
1314
|
import chalk4 from "chalk";
|
|
1310
1315
|
import { createInterface } from "readline";
|
|
1316
|
+
|
|
1317
|
+
// src/utils/interactive.ts
|
|
1318
|
+
var nonInteractiveFlag = false;
|
|
1319
|
+
function setNonInteractive(value) {
|
|
1320
|
+
nonInteractiveFlag = value;
|
|
1321
|
+
}
|
|
1322
|
+
function isNonInteractive() {
|
|
1323
|
+
if (nonInteractiveFlag) return true;
|
|
1324
|
+
if (process.env.CI === "true" || process.env.CI === "1") return true;
|
|
1325
|
+
if (process.env.CLAWT_NON_INTERACTIVE === "true" || process.env.CLAWT_NON_INTERACTIVE === "1") return true;
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/utils/formatter.ts
|
|
1311
1330
|
function printSuccess(message) {
|
|
1312
1331
|
console.log(chalk4.green(message));
|
|
1313
1332
|
}
|
|
@@ -1329,7 +1348,10 @@ function printSeparator() {
|
|
|
1329
1348
|
function printDoubleSeparator() {
|
|
1330
1349
|
console.log(MESSAGES.DOUBLE_SEPARATOR);
|
|
1331
1350
|
}
|
|
1332
|
-
function confirmAction(question) {
|
|
1351
|
+
function confirmAction(question, nonInteractiveDefault = true) {
|
|
1352
|
+
if (isNonInteractive()) {
|
|
1353
|
+
return Promise.resolve(nonInteractiveDefault);
|
|
1354
|
+
}
|
|
1333
1355
|
return new Promise((resolve4) => {
|
|
1334
1356
|
const rl = createInterface({
|
|
1335
1357
|
input: process.stdin,
|
|
@@ -1605,6 +1627,14 @@ async function rebuildValidateBranch(branchName, cwd) {
|
|
|
1605
1627
|
logger.info(`\u9A8C\u8BC1\u5206\u652F\u5DF2\u91CD\u5EFA: ${getValidateBranchName(branchName)}`);
|
|
1606
1628
|
}
|
|
1607
1629
|
async function handleDirtyWorkingDir(cwd) {
|
|
1630
|
+
if (isNonInteractive()) {
|
|
1631
|
+
gitAddAll(cwd);
|
|
1632
|
+
gitStashPush("clawt:auto-stash", cwd);
|
|
1633
|
+
if (!isWorkingDirClean(cwd)) {
|
|
1634
|
+
throw new ClawtError("\u5DE5\u4F5C\u533A\u4ECD\u7136\u4E0D\u5E72\u51C0\uFF0C\u8BF7\u624B\u52A8\u5904\u7406");
|
|
1635
|
+
}
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1608
1638
|
printWarning("\u5F53\u524D\u5206\u652F\u6709\u672A\u63D0\u4EA4\u7684\u66F4\u6539\uFF0C\u8BF7\u9009\u62E9\u5904\u7406\u65B9\u5F0F\uFF1A\n");
|
|
1609
1639
|
const choice = await new Enquirer.Select({
|
|
1610
1640
|
message: "\u9009\u62E9\u5904\u7406\u65B9\u5F0F",
|
|
@@ -1654,7 +1684,7 @@ async function ensureOnMainWorkBranch(cwd) {
|
|
|
1654
1684
|
return;
|
|
1655
1685
|
}
|
|
1656
1686
|
printWarning(MESSAGES.GUARD_BRANCH_MISMATCH(mainBranch, currentBranch));
|
|
1657
|
-
const confirmed = await confirmAction("\u662F\u5426\u7EE7\u7EED\u6267\u884C\uFF1F");
|
|
1687
|
+
const confirmed = isNonInteractive() ? true : await confirmAction("\u662F\u5426\u7EE7\u7EED\u6267\u884C\uFF1F");
|
|
1658
1688
|
if (!confirmed) {
|
|
1659
1689
|
throw new ClawtError(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
|
|
1660
1690
|
}
|
|
@@ -2089,6 +2119,9 @@ import { statSync as statSync3 } from "fs";
|
|
|
2089
2119
|
// src/utils/ui-prompts.ts
|
|
2090
2120
|
import Enquirer3 from "enquirer";
|
|
2091
2121
|
async function promptSelectBranch(worktrees, message) {
|
|
2122
|
+
if (isNonInteractive()) {
|
|
2123
|
+
throw new ClawtError("\u975E\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u65E0\u6CD5\u8FDB\u884C\u5206\u652F\u9009\u62E9\uFF0C\u8BF7\u901A\u8FC7 -b \u53C2\u6570\u7CBE\u786E\u6307\u5B9A\u5206\u652F\u540D");
|
|
2124
|
+
}
|
|
2092
2125
|
const selectedBranch = await new Enquirer3.Select({
|
|
2093
2126
|
message,
|
|
2094
2127
|
choices: worktrees.map((wt) => ({
|
|
@@ -2099,6 +2132,9 @@ async function promptSelectBranch(worktrees, message) {
|
|
|
2099
2132
|
return worktrees.find((wt) => wt.branch === selectedBranch);
|
|
2100
2133
|
}
|
|
2101
2134
|
async function promptMultiSelectBranches(worktrees, message) {
|
|
2135
|
+
if (isNonInteractive()) {
|
|
2136
|
+
throw new ClawtError("\u975E\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u65E0\u6CD5\u8FDB\u884C\u5206\u652F\u591A\u9009\uFF0C\u8BF7\u901A\u8FC7 -b \u53C2\u6570\u7CBE\u786E\u6307\u5B9A\u5206\u652F\u540D");
|
|
2137
|
+
}
|
|
2102
2138
|
const branchChoices = worktrees.map((wt) => ({
|
|
2103
2139
|
name: wt.branch,
|
|
2104
2140
|
message: wt.branch
|
|
@@ -2138,6 +2174,9 @@ async function promptMultiSelectBranches(worktrees, message) {
|
|
|
2138
2174
|
return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
|
|
2139
2175
|
}
|
|
2140
2176
|
async function promptGroupedMultiSelectBranches(worktrees, message) {
|
|
2177
|
+
if (isNonInteractive()) {
|
|
2178
|
+
throw new ClawtError("\u975E\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u65E0\u6CD5\u8FDB\u884C\u5206\u652F\u591A\u9009\uFF0C\u8BF7\u901A\u8FC7 -b \u53C2\u6570\u7CBE\u786E\u6307\u5B9A\u5206\u652F\u540D");
|
|
2179
|
+
}
|
|
2141
2180
|
const groups = groupWorktreesByDate(worktrees);
|
|
2142
2181
|
const choices = buildGroupedChoices(groups);
|
|
2143
2182
|
const groupMembershipMap = buildGroupMembershipMap(groups);
|
|
@@ -3145,6 +3184,9 @@ function formatConfigValue(value) {
|
|
|
3145
3184
|
return chalk7.cyan(String(value));
|
|
3146
3185
|
}
|
|
3147
3186
|
async function interactiveConfigEditor(config2, definitions, options) {
|
|
3187
|
+
if (isNonInteractive()) {
|
|
3188
|
+
throw new ClawtError("\u975E\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u65E0\u6CD5\u4F7F\u7528\u4EA4\u4E92\u5F0F\u914D\u7F6E\u7F16\u8F91\u5668\uFF0C\u8BF7\u4F7F\u7528 clawt config set <key> <value>");
|
|
3189
|
+
}
|
|
3148
3190
|
const keys = Object.keys(definitions);
|
|
3149
3191
|
const disabledKeys = options?.disabledKeys ?? {};
|
|
3150
3192
|
const configRecord = config2;
|
|
@@ -4216,7 +4258,8 @@ function invokeClaudeForConflictResolve(prompt, cwd) {
|
|
|
4216
4258
|
cwd,
|
|
4217
4259
|
encoding: "utf-8",
|
|
4218
4260
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4219
|
-
timeout: getConflictResolveTimeout()
|
|
4261
|
+
timeout: getConflictResolveTimeout(),
|
|
4262
|
+
env: getEnvWithoutNestedSessionFlag()
|
|
4220
4263
|
});
|
|
4221
4264
|
return output;
|
|
4222
4265
|
} catch (error) {
|
|
@@ -4914,7 +4957,7 @@ async function handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branch
|
|
|
4914
4957
|
if (!hasCommitWithMessage(branchName, AUTO_SAVE_COMMIT_MESSAGE, mainWorktreePath)) {
|
|
4915
4958
|
return false;
|
|
4916
4959
|
}
|
|
4917
|
-
const shouldSquash = await confirmAction(MESSAGES.MERGE_SQUASH_PROMPT);
|
|
4960
|
+
const shouldSquash = await confirmAction(MESSAGES.MERGE_SQUASH_PROMPT, false);
|
|
4918
4961
|
if (!shouldSquash) {
|
|
4919
4962
|
return false;
|
|
4920
4963
|
}
|
|
@@ -4936,7 +4979,7 @@ async function shouldCleanupAfterMerge(branchName) {
|
|
|
4936
4979
|
printInfo(`\u5DF2\u914D\u7F6E\u81EA\u52A8\u5220\u9664\uFF0Cmerge \u6210\u529F\u540E\u5C06\u81EA\u52A8\u6E05\u7406 worktree \u548C\u5206\u652F: ${branchName}`);
|
|
4937
4980
|
return true;
|
|
4938
4981
|
}
|
|
4939
|
-
return confirmAction(`\u662F\u5426\u5220\u9664\u5BF9\u5E94\u7684 worktree \u548C\u5206\u652F (${branchName})\uFF1F
|
|
4982
|
+
return confirmAction(`\u662F\u5426\u5220\u9664\u5BF9\u5E94\u7684 worktree \u548C\u5206\u652F (${branchName})\uFF1F`, true);
|
|
4940
4983
|
}
|
|
4941
4984
|
function cleanupWorktreeAndBranch(worktreePath, branchName) {
|
|
4942
4985
|
cleanupWorktrees([{ path: worktreePath, branch: branchName }]);
|
|
@@ -5908,11 +5951,14 @@ var require2 = createRequire(import.meta.url);
|
|
|
5908
5951
|
var { version } = require2("../package.json");
|
|
5909
5952
|
ensureClawtDirs();
|
|
5910
5953
|
var program = new Command();
|
|
5911
|
-
program.name("clawt").description("\u672C\u5730\u5E76\u884C\u6267\u884C\u591A\u4E2AClaude Code Agent\u4EFB\u52A1\uFF0C\u878D\u5408 Git Worktree \u4E0E Claude Code CLI \u7684\u547D\u4EE4\u884C\u5DE5\u5177").version(version).option("--debug", "\u8F93\u51FA\u8BE6\u7EC6\u8C03\u8BD5\u4FE1\u606F\u5230\u7EC8\u7AEF");
|
|
5954
|
+
program.name("clawt").description("\u672C\u5730\u5E76\u884C\u6267\u884C\u591A\u4E2AClaude Code Agent\u4EFB\u52A1\uFF0C\u878D\u5408 Git Worktree \u4E0E Claude Code CLI \u7684\u547D\u4EE4\u884C\u5DE5\u5177").version(version).option("--debug", "\u8F93\u51FA\u8BE6\u7EC6\u8C03\u8BD5\u4FE1\u606F\u5230\u7EC8\u7AEF").option("-y, --yes", "\u8DF3\u8FC7\u6240\u6709\u4EA4\u4E92\u5F0F\u786E\u8BA4\uFF0C\u9002\u7528\u4E8E\u811A\u672C/CI \u73AF\u5883");
|
|
5912
5955
|
program.hook("preAction", (thisCommand) => {
|
|
5913
5956
|
if (thisCommand.opts().debug) {
|
|
5914
5957
|
enableConsoleTransport();
|
|
5915
5958
|
}
|
|
5959
|
+
if (thisCommand.opts().yes) {
|
|
5960
|
+
setNonInteractive(true);
|
|
5961
|
+
}
|
|
5916
5962
|
});
|
|
5917
5963
|
registerListCommand(program);
|
|
5918
5964
|
registerCreateCommand(program);
|
package/package.json
CHANGED
package/src/commands/merge.ts
CHANGED
|
@@ -82,8 +82,8 @@ async function handleSquashIfNeeded(
|
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
const shouldSquash = await confirmAction(MESSAGES.MERGE_SQUASH_PROMPT);
|
|
85
|
+
// 提示用户是否压缩(非交互模式下默认跳过,保留原始提交)
|
|
86
|
+
const shouldSquash = await confirmAction(MESSAGES.MERGE_SQUASH_PROMPT, false);
|
|
87
87
|
if (!shouldSquash) {
|
|
88
88
|
return false;
|
|
89
89
|
}
|
|
@@ -120,7 +120,8 @@ async function shouldCleanupAfterMerge(branchName: string): Promise<boolean> {
|
|
|
120
120
|
printInfo(`已配置自动删除,merge 成功后将自动清理 worktree 和分支: ${branchName}`);
|
|
121
121
|
return true;
|
|
122
122
|
}
|
|
123
|
-
|
|
123
|
+
// 非交互模式下自动删除
|
|
124
|
+
return confirmAction(`是否删除对应的 worktree 和分支 (${branchName})?`, true);
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/**
|
package/src/commands/remove.ts
CHANGED
|
@@ -100,6 +100,7 @@ async function handleRemove(options: RemoveOptions): Promise<void> {
|
|
|
100
100
|
let shouldDeleteBranch = autoDelete;
|
|
101
101
|
|
|
102
102
|
if (!autoDelete) {
|
|
103
|
+
// 非交互模式下默认删除分支(删除 worktree 时分支通常也不再需要)
|
|
103
104
|
shouldDeleteBranch = await confirmAction(MESSAGES.REMOVE_CONFIRM_DELETE_BRANCHES);
|
|
104
105
|
if (!shouldDeleteBranch) {
|
|
105
106
|
printHint(MESSAGES.REMOVE_BRANCHES_KEPT);
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { ClawtError } from './errors/index.js';
|
|
4
4
|
import { logger, enableConsoleTransport } from './logger/index.js';
|
|
5
5
|
import { EXIT_CODES } from './constants/index.js';
|
|
6
|
-
import { printError, ensureClawtDirs, loadConfig, applyAliases, checkForUpdates } from './utils/index.js';
|
|
6
|
+
import { printError, ensureClawtDirs, loadConfig, applyAliases, checkForUpdates, setNonInteractive } from './utils/index.js';
|
|
7
7
|
import { registerListCommand } from './commands/list.js';
|
|
8
8
|
import { registerCreateCommand } from './commands/create.js';
|
|
9
9
|
import { registerRemoveCommand } from './commands/remove.js';
|
|
@@ -36,13 +36,18 @@ program
|
|
|
36
36
|
.name('clawt')
|
|
37
37
|
.description('本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具')
|
|
38
38
|
.version(version)
|
|
39
|
-
.option('--debug', '输出详细调试信息到终端')
|
|
39
|
+
.option('--debug', '输出详细调试信息到终端')
|
|
40
|
+
.option('-y, --yes', '跳过所有交互式确认,适用于脚本/CI 环境');
|
|
40
41
|
|
|
41
42
|
// 在子命令 action 执行前检查 --debug 选项,按需启用控制台日志
|
|
42
43
|
program.hook('preAction', (thisCommand) => {
|
|
43
44
|
if (thisCommand.opts().debug) {
|
|
44
45
|
enableConsoleTransport();
|
|
45
46
|
}
|
|
47
|
+
// 检测 --yes 选项,启用非交互模式
|
|
48
|
+
if (thisCommand.opts().yes) {
|
|
49
|
+
setNonInteractive(true);
|
|
50
|
+
}
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
// 注册所有命令
|
|
@@ -2,6 +2,8 @@ import chalk from 'chalk';
|
|
|
2
2
|
import Enquirer from 'enquirer';
|
|
3
3
|
import { DEFAULT_CONFIG, CONFIG_DEFINITIONS, MESSAGES } from '../constants/index.js';
|
|
4
4
|
import type { ClawtConfig } from '../types/index.js';
|
|
5
|
+
import { isNonInteractive } from './interactive.js';
|
|
6
|
+
import { ClawtError } from '../errors/index.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* 校验 key 是否为有效的配置项名称
|
|
@@ -132,6 +134,11 @@ export async function interactiveConfigEditor<T extends object>(
|
|
|
132
134
|
definitions: Record<string, { description: string; allowedValues?: readonly string[] }>,
|
|
133
135
|
options?: { selectPrompt?: string; disabledKeys?: Record<string, string> },
|
|
134
136
|
): Promise<{ key: keyof T; newValue: unknown }> {
|
|
137
|
+
// 非交互模式下无法进行配置编辑,引导使用 config set 命令
|
|
138
|
+
if (isNonInteractive()) {
|
|
139
|
+
throw new ClawtError('非交互模式下无法使用交互式配置编辑器,请使用 clawt config set <key> <value>');
|
|
140
|
+
}
|
|
141
|
+
|
|
135
142
|
const keys = Object.keys(definitions);
|
|
136
143
|
const disabledKeys = options?.disabledKeys ?? {};
|
|
137
144
|
const configRecord = config as Record<string, unknown>;
|
|
@@ -7,6 +7,7 @@ import { printInfo, printSuccess, printWarning } from './formatter.js';
|
|
|
7
7
|
import { confirmAction } from './formatter.js';
|
|
8
8
|
import { MESSAGES } from '../constants/index.js';
|
|
9
9
|
import { CONFLICT_RESOLVE_PROMPT } from '../constants/ai-prompts.js';
|
|
10
|
+
import { getEnvWithoutNestedSessionFlag } from './shell.js';
|
|
10
11
|
|
|
11
12
|
/** 默认 Claude Code 冲突解决超时时间(毫秒) */
|
|
12
13
|
const DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS = 300000;
|
|
@@ -51,6 +52,7 @@ export function invokeClaudeForConflictResolve(prompt: string, cwd: string): str
|
|
|
51
52
|
encoding: 'utf-8',
|
|
52
53
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
54
|
timeout: getConflictResolveTimeout(),
|
|
55
|
+
env: getEnvWithoutNestedSessionFlag(),
|
|
54
56
|
});
|
|
55
57
|
return output;
|
|
56
58
|
} catch (error: unknown) {
|
|
@@ -160,7 +162,7 @@ export async function handleMergeConflict(
|
|
|
160
162
|
return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
// mode === 'ask'
|
|
165
|
+
// mode === 'ask'(非交互模式下默认使用 AI 解决,因为无法手动解决)
|
|
164
166
|
const shouldUseAI = await confirmAction(MESSAGES.MERGE_CONFLICT_ASK_AI);
|
|
165
167
|
if (!shouldUseAI) {
|
|
166
168
|
throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
|
package/src/utils/formatter.ts
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { MESSAGES } from '../constants/index.js';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import type { WorktreeStatus } from '../types/index.js';
|
|
5
|
+
import { isNonInteractive } from './interactive.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 输出成功信息
|
|
@@ -58,11 +59,17 @@ export function printDoubleSeparator(): void {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
|
-
* 简易 yes/no
|
|
62
|
+
* 简易 yes/no 确认
|
|
63
|
+
* 非交互模式下根据 nonInteractiveDefault 参数自动返回默认值
|
|
62
64
|
* @param {string} question - 确认问题
|
|
65
|
+
* @param {boolean} [nonInteractiveDefault=true] - 非交互模式下的默认返回值,true 表示自动确认,false 表示自动拒绝
|
|
63
66
|
* @returns {Promise<boolean>} 用户是否确认
|
|
64
67
|
*/
|
|
65
|
-
export function confirmAction(question: string): Promise<boolean> {
|
|
68
|
+
export function confirmAction(question: string, nonInteractiveDefault: boolean = true): Promise<boolean> {
|
|
69
|
+
// 非交互模式下返回调用方声明的默认值
|
|
70
|
+
if (isNonInteractive()) {
|
|
71
|
+
return Promise.resolve(nonInteractiveDefault);
|
|
72
|
+
}
|
|
66
73
|
return new Promise((resolve) => {
|
|
67
74
|
const rl = createInterface({
|
|
68
75
|
input: process.stdin,
|
package/src/utils/index.ts
CHANGED
|
@@ -82,6 +82,7 @@ export { checkForUpdates } from './update-checker.js';
|
|
|
82
82
|
export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand } from './project-config.js';
|
|
83
83
|
export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
|
|
84
84
|
export { safeStringify } from './json.js';
|
|
85
|
+
export { isNonInteractive, setNonInteractive } from './interactive.js';
|
|
85
86
|
export { executeRunCommand } from './validate-runner.js';
|
|
86
87
|
export { migrateChangesViaPatch, computeCurrentTreeHash, saveCurrentSnapshotTree, loadOldSnapshotToStage, switchToValidateBranch } from './validate-core.js';
|
|
87
88
|
export { InteractivePanel } from './interactive-panel.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 非交互模式判断工具
|
|
3
|
+
* 优先级:CLI --yes > 环境变量 CI / CLAWT_NON_INTERACTIVE > 默认交互模式
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 运行时标志,由 --yes 全局选项在 preAction hook 中设置 */
|
|
7
|
+
let nonInteractiveFlag = false;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 设置非交互模式运行时标志
|
|
11
|
+
* @param {boolean} value - 是否启用非交互模式
|
|
12
|
+
*/
|
|
13
|
+
export function setNonInteractive(value: boolean): void {
|
|
14
|
+
nonInteractiveFlag = value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 判断当前是否处于非交互模式
|
|
19
|
+
* 检查顺序:运行时标志(--yes)→ CI 环境变量 → CLAWT_NON_INTERACTIVE 环境变量
|
|
20
|
+
* @returns {boolean} 是否为非交互模式
|
|
21
|
+
*/
|
|
22
|
+
export function isNonInteractive(): boolean {
|
|
23
|
+
if (nonInteractiveFlag) return true;
|
|
24
|
+
if (process.env.CI === 'true' || process.env.CI === '1') return true;
|
|
25
|
+
if (process.env.CLAWT_NON_INTERACTIVE === 'true' || process.env.CLAWT_NON_INTERACTIVE === '1') return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
package/src/utils/shell.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { execSync, execFileSync, spawn, spawnSync, type ChildProcess, type SpawnSyncReturns, type StdioOptions } from 'node:child_process';
|
|
2
2
|
import { logger } from '../logger/index.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 获取移除了 CLAUDECODE 嵌套会话标记的环境变量副本
|
|
6
|
+
* 仅用于 claude -p 等非交互式子进程,避免被 Claude Code 误判为嵌套会话而拒绝启动
|
|
7
|
+
* 不适用于交互式启动 Claude Code(如 clawt resume),交互式场景应保留原始环境变量
|
|
8
|
+
* @returns {NodeJS.ProcessEnv} 移除 CLAUDECODE 后的环境变量
|
|
9
|
+
*/
|
|
10
|
+
export function getEnvWithoutNestedSessionFlag(): NodeJS.ProcessEnv {
|
|
11
|
+
const { CLAUDECODE: _, ...env } = process.env;
|
|
12
|
+
return env;
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
/** 并行命令执行的单个结果 */
|
|
5
16
|
export interface ParallelCommandResult {
|
|
6
17
|
/** 执行的命令字符串 */
|
|
@@ -63,6 +74,7 @@ export function spawnProcess(
|
|
|
63
74
|
return spawn(command, args, {
|
|
64
75
|
cwd: options?.cwd,
|
|
65
76
|
stdio: options?.stdio ?? ['pipe', 'pipe', 'pipe'],
|
|
77
|
+
env: getEnvWithoutNestedSessionFlag(),
|
|
66
78
|
});
|
|
67
79
|
}
|
|
68
80
|
|
package/src/utils/ui-prompts.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
} from '../constants/index.js';
|
|
6
6
|
import type { WorktreeInfo } from '../types/index.js';
|
|
7
7
|
import { groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap } from './worktree-matcher.js';
|
|
8
|
+
import { isNonInteractive } from './interactive.js';
|
|
9
|
+
import { ClawtError } from '../errors/index.js';
|
|
8
10
|
|
|
9
11
|
/** enquirer MultiSelect 选项条目的运行时结构 */
|
|
10
12
|
export interface MultiSelectChoice {
|
|
@@ -41,6 +43,10 @@ export type GroupedChoice = { name: string; message: string } | MultiSelectSepar
|
|
|
41
43
|
* @returns {Promise<WorktreeInfo>} 用户选择的 worktree
|
|
42
44
|
*/
|
|
43
45
|
export async function promptSelectBranch(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo> {
|
|
46
|
+
// 非交互模式下无法进行交互选择,要求用户通过 -b 精确指定
|
|
47
|
+
if (isNonInteractive()) {
|
|
48
|
+
throw new ClawtError('非交互模式下无法进行分支选择,请通过 -b 参数精确指定分支名');
|
|
49
|
+
}
|
|
44
50
|
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
45
51
|
const selectedBranch: string = await new Enquirer.Select({
|
|
46
52
|
message,
|
|
@@ -62,6 +68,10 @@ export async function promptSelectBranch(worktrees: WorktreeInfo[], message: str
|
|
|
62
68
|
* @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
|
|
63
69
|
*/
|
|
64
70
|
export async function promptMultiSelectBranches(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo[]> {
|
|
71
|
+
// 非交互模式下无法进行交互多选,要求用户通过 -b 精确指定
|
|
72
|
+
if (isNonInteractive()) {
|
|
73
|
+
throw new ClawtError('非交互模式下无法进行分支多选,请通过 -b 参数精确指定分支名');
|
|
74
|
+
}
|
|
65
75
|
// 构建 choices 列表,顶部插入全选选项
|
|
66
76
|
const branchChoices = worktrees.map((wt) => ({
|
|
67
77
|
name: wt.branch,
|
|
@@ -131,6 +141,10 @@ export async function promptGroupedMultiSelectBranches(
|
|
|
131
141
|
worktrees: WorktreeInfo[],
|
|
132
142
|
message: string,
|
|
133
143
|
): Promise<WorktreeInfo[]> {
|
|
144
|
+
// 非交互模式下无法进行交互多选,要求用户通过 -b 精确指定
|
|
145
|
+
if (isNonInteractive()) {
|
|
146
|
+
throw new ClawtError('非交互模式下无法进行分支多选,请通过 -b 参数精确指定分支名');
|
|
147
|
+
}
|
|
134
148
|
const groups = groupWorktreesByDate(worktrees);
|
|
135
149
|
const choices = buildGroupedChoices(groups);
|
|
136
150
|
const groupMembershipMap = buildGroupMembershipMap(groups);
|
|
@@ -5,6 +5,7 @@ import { checkBranchExists, createBranch, deleteBranch, getCurrentBranch, gitChe
|
|
|
5
5
|
import { getMainWorkBranch } from './project-config.js';
|
|
6
6
|
import { printWarning, confirmAction } from './formatter.js';
|
|
7
7
|
import { ClawtError } from '../errors/index.js';
|
|
8
|
+
import { isNonInteractive } from './interactive.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 生成验证分支名
|
|
@@ -88,6 +89,16 @@ export async function rebuildValidateBranch(branchName: string, cwd?: string): P
|
|
|
88
89
|
* @param {string} [cwd] - 工作目录
|
|
89
90
|
*/
|
|
90
91
|
export async function handleDirtyWorkingDir(cwd?: string): Promise<void> {
|
|
92
|
+
// 非交互模式下默认执行 stash(安全策略,不丢弃代码)
|
|
93
|
+
if (isNonInteractive()) {
|
|
94
|
+
gitAddAll(cwd);
|
|
95
|
+
gitStashPush('clawt:auto-stash', cwd);
|
|
96
|
+
if (!isWorkingDirClean(cwd)) {
|
|
97
|
+
throw new ClawtError('工作区仍然不干净,请手动处理');
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
printWarning('当前分支有未提交的更改,请选择处理方式:\n');
|
|
92
103
|
|
|
93
104
|
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
@@ -159,7 +170,8 @@ export async function ensureOnMainWorkBranch(cwd?: string): Promise<void> {
|
|
|
159
170
|
|
|
160
171
|
// 当前在其他分支上,警告并确认后处理脏工作区再切换
|
|
161
172
|
printWarning(MESSAGES.GUARD_BRANCH_MISMATCH(mainBranch, currentBranch));
|
|
162
|
-
|
|
173
|
+
// 非交互模式下自动确认继续
|
|
174
|
+
const confirmed = isNonInteractive() ? true : await confirmAction('是否继续执行?');
|
|
163
175
|
if (!confirmed) {
|
|
164
176
|
throw new ClawtError(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
|
|
165
177
|
}
|
|
@@ -50,6 +50,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
50
50
|
confirmAction: vi.fn().mockResolvedValue(true),
|
|
51
51
|
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
52
52
|
guardMainWorkBranchExists: vi.fn(),
|
|
53
|
+
isNonInteractive: vi.fn().mockReturnValue(false),
|
|
53
54
|
}));
|
|
54
55
|
|
|
55
56
|
import { registerCoverValidateCommand, extractTargetBranchName, findTargetWorktreePath, computeIncrementalPatch } from '../../../src/commands/cover-validate.js';
|
|
@@ -78,6 +78,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
78
78
|
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
79
79
|
guardMainWorkBranchExists: vi.fn(),
|
|
80
80
|
handleMergeConflict: vi.fn(),
|
|
81
|
+
isNonInteractive: vi.fn().mockReturnValue(false),
|
|
81
82
|
}));
|
|
82
83
|
|
|
83
84
|
import { registerMergeCommand } from '../../../src/commands/merge.js';
|
|
@@ -56,6 +56,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
56
56
|
getCurrentBranch: vi.fn(),
|
|
57
57
|
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
58
58
|
guardMainWorkBranchExists: vi.fn(),
|
|
59
|
+
isNonInteractive: vi.fn().mockReturnValue(false),
|
|
59
60
|
}));
|
|
60
61
|
|
|
61
62
|
import { registerRemoveCommand } from '../../../src/commands/remove.js';
|