clawt 3.5.0 → 3.5.2
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 +278 -33
- package/dist/postinstall.js +38 -2
- package/package.json +1 -1
- package/src/commands/merge.ts +20 -5
- package/src/constants/ai-prompts.ts +14 -0
- package/src/constants/config.ts +9 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/messages/merge.ts +15 -0
- package/src/constants/messages/validate.ts +12 -0
- package/src/types/command.ts +2 -0
- package/src/types/config.ts +4 -0
- package/src/utils/clipboard.ts +52 -0
- package/src/utils/conflict-resolver.ts +170 -0
- package/src/utils/git-core.ts +49 -1
- package/src/utils/index.ts +9 -3
- package/src/utils/shell.ts +96 -0
- package/src/utils/validate-runner.ts +63 -16
- package/tests/unit/commands/merge.test.ts +59 -3
- package/tests/unit/utils/conflict-resolver.test.ts +250 -0
- package/src/constants/messages.ts +0 -179
package/src/types/command.ts
CHANGED
package/src/types/config.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface ClawtConfig {
|
|
|
18
18
|
aliases: Record<string, string>;
|
|
19
19
|
/** 是否启用自动更新检查 */
|
|
20
20
|
autoUpdate: boolean;
|
|
21
|
+
/** merge 冲突时的解决模式:ask(询问)、auto(自动 AI 解决)、manual(手动解决) */
|
|
22
|
+
conflictResolveMode: 'ask' | 'auto' | 'manual';
|
|
23
|
+
/** Claude Code 冲突解决超时时间(毫秒),默认 300000(5 分钟) */
|
|
24
|
+
conflictResolveTimeoutMs: number;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/** 单个配置项的完整定义(默认值 + 描述) */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { logger } from '../logger/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 根据当前操作系统获取剪贴板写入命令
|
|
6
|
+
* @returns {{ command: string, args: string[] } | null} 剪贴板命令配置,不支持的平台返回 null
|
|
7
|
+
*/
|
|
8
|
+
function getClipboardCommand(): { command: string; args: string[] } | null {
|
|
9
|
+
switch (process.platform) {
|
|
10
|
+
case 'darwin':
|
|
11
|
+
return { command: 'pbcopy', args: [] };
|
|
12
|
+
case 'linux':
|
|
13
|
+
return { command: 'xclip', args: ['-selection', 'clipboard'] };
|
|
14
|
+
case 'win32':
|
|
15
|
+
return { command: 'clip', args: [] };
|
|
16
|
+
default:
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 将文本复制到系统剪贴板
|
|
23
|
+
* 跨平台支持:macOS (pbcopy)、Linux (xclip)、Windows (clip)
|
|
24
|
+
* 失败时静默返回 false,不影响主流程
|
|
25
|
+
* @param {string} text - 要复制到剪贴板的文本
|
|
26
|
+
* @returns {boolean} 复制是否成功
|
|
27
|
+
*/
|
|
28
|
+
export function copyToClipboard(text: string): boolean {
|
|
29
|
+
try {
|
|
30
|
+
const clipboardCmd = getClipboardCommand();
|
|
31
|
+
if (!clipboardCmd) {
|
|
32
|
+
logger.debug(`不支持的平台: ${process.platform},跳过剪贴板复制`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = spawnSync(clipboardCmd.command, clipboardCmd.args, {
|
|
37
|
+
input: text,
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (result.status !== 0) {
|
|
43
|
+
logger.debug(`剪贴板命令执行失败,退出码: ${result.status}`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.debug(`剪贴板复制异常: ${(error as Error).message}`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { logger } from '../logger/index.js';
|
|
3
|
+
import { ClawtError } from '../errors/index.js';
|
|
4
|
+
import { getConfigValue } from './config.js';
|
|
5
|
+
import { getConflictFiles, gitAddFiles, gitMergeContinue } from './git.js';
|
|
6
|
+
import { printInfo, printSuccess, printWarning } from './formatter.js';
|
|
7
|
+
import { confirmAction } from './formatter.js';
|
|
8
|
+
import { MESSAGES } from '../constants/index.js';
|
|
9
|
+
import { CONFLICT_RESOLVE_PROMPT } from '../constants/ai-prompts.js';
|
|
10
|
+
|
|
11
|
+
/** 默认 Claude Code 冲突解决超时时间(毫秒) */
|
|
12
|
+
const DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS = 300000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 构建发送给 Claude Code 的冲突解决 prompt
|
|
16
|
+
* Claude Code 会自动读取当前工作目录下的冲突文件,无需手动传入
|
|
17
|
+
* @returns {string} 纯指令性 AI prompt
|
|
18
|
+
*/
|
|
19
|
+
export function buildConflictResolvePrompt(): string {
|
|
20
|
+
return CONFLICT_RESOLVE_PROMPT;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取冲突解决超时时间(毫秒)
|
|
25
|
+
* 优先使用配置项,未配置时使用默认值
|
|
26
|
+
* @returns {number} 超时时间(毫秒)
|
|
27
|
+
*/
|
|
28
|
+
function getConflictResolveTimeout(): number {
|
|
29
|
+
const configValue = getConfigValue('conflictResolveTimeoutMs');
|
|
30
|
+
if (typeof configValue === 'number' && configValue > 0) {
|
|
31
|
+
return configValue;
|
|
32
|
+
}
|
|
33
|
+
return DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 调用 Claude Code CLI 以非交互模式解决冲突
|
|
38
|
+
* 使用 execFileSync 数组参数形式避免 shell 注入
|
|
39
|
+
* @param {string} prompt - AI prompt
|
|
40
|
+
* @param {string} cwd - 工作目录
|
|
41
|
+
* @returns {string} Claude Code 的输出
|
|
42
|
+
*/
|
|
43
|
+
export function invokeClaudeForConflictResolve(prompt: string, cwd: string): string {
|
|
44
|
+
const args = ['-p', prompt, '--permission-mode', 'bypassPermissions'];
|
|
45
|
+
|
|
46
|
+
logger.info(`调用 Claude Code 解决冲突,命令: claude -p "..." --permission-mode bypassPermissions`);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const output = execFileSync('claude', args, {
|
|
50
|
+
cwd,
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
+
timeout: getConflictResolveTimeout(),
|
|
54
|
+
});
|
|
55
|
+
return output;
|
|
56
|
+
} catch (error: unknown) {
|
|
57
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
58
|
+
logger.error(`Claude Code 冲突解决失败: ${errMsg}`);
|
|
59
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_AI_FAILED(errMsg));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 执行 AI 辅助冲突解决的完整流程
|
|
65
|
+
* 1. 获取冲突文件列表
|
|
66
|
+
* 2. 构建 AI prompt
|
|
67
|
+
* 3. 调用 Claude Code 解决冲突
|
|
68
|
+
* 4. 检查冲突是否解决
|
|
69
|
+
* 5. git add 已解决的文件并 merge --continue
|
|
70
|
+
* @param {string} currentBranch - 当前分支名
|
|
71
|
+
* @param {string} incomingBranch - 传入分支名
|
|
72
|
+
* @param {string} cwd - 工作目录
|
|
73
|
+
* @returns {boolean} 是否所有冲突都已解决
|
|
74
|
+
*/
|
|
75
|
+
export function resolveConflictsWithAI(
|
|
76
|
+
currentBranch: string,
|
|
77
|
+
incomingBranch: string,
|
|
78
|
+
cwd: string,
|
|
79
|
+
): boolean {
|
|
80
|
+
const conflictFiles = getConflictFiles(cwd);
|
|
81
|
+
if (conflictFiles.length === 0) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
printInfo(MESSAGES.MERGE_CONFLICT_AI_START(conflictFiles.length));
|
|
86
|
+
|
|
87
|
+
// 构建 prompt 并调用 Claude Code,捕获异常提供恢复提示
|
|
88
|
+
const prompt = buildConflictResolvePrompt();
|
|
89
|
+
try {
|
|
90
|
+
invokeClaudeForConflictResolve(prompt, cwd);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// AI 调用失败时输出友好提示,告知用户可手动解决或重试
|
|
93
|
+
const errMsg = error instanceof ClawtError ? error.message : String(error);
|
|
94
|
+
printWarning(errMsg);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 检查冲突是否已解决
|
|
99
|
+
const remainingConflicts = getConflictFiles(cwd);
|
|
100
|
+
|
|
101
|
+
if (remainingConflicts.length === 0) {
|
|
102
|
+
// 所有冲突已解决,git add 所有原冲突文件并完成 merge
|
|
103
|
+
gitAddFiles(conflictFiles, cwd);
|
|
104
|
+
gitMergeContinue(cwd);
|
|
105
|
+
printSuccess(MESSAGES.MERGE_CONFLICT_AI_SUCCESS);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 部分冲突已解决,git add 已解决的文件
|
|
110
|
+
const resolvedFiles = conflictFiles.filter((f) => !remainingConflicts.includes(f));
|
|
111
|
+
if (resolvedFiles.length > 0) {
|
|
112
|
+
gitAddFiles(resolvedFiles, cwd);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
printWarning(MESSAGES.MERGE_CONFLICT_AI_PARTIAL(remainingConflicts.length));
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 根据配置和命令选项判断冲突解决模式
|
|
121
|
+
* @param {boolean} [autoFlag] - --auto 命令行参数
|
|
122
|
+
* @returns {'ask' | 'auto' | 'manual'} 冲突解决模式
|
|
123
|
+
*/
|
|
124
|
+
export function determineConflictResolveMode(autoFlag?: boolean): 'ask' | 'auto' | 'manual' {
|
|
125
|
+
// --auto 命令行参数优先级最高
|
|
126
|
+
if (autoFlag === true) {
|
|
127
|
+
return 'auto';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configMode = getConfigValue('conflictResolveMode');
|
|
131
|
+
if (configMode === 'auto' || configMode === 'manual') {
|
|
132
|
+
return configMode;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return 'ask';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 处理合并冲突的入口函数
|
|
140
|
+
* 根据冲突解决模式决定处理方式:auto 直接 AI 解决,ask 询问用户,manual 抛出错误
|
|
141
|
+
* @param {string} currentBranch - 当前分支名
|
|
142
|
+
* @param {string} incomingBranch - 传入分支名
|
|
143
|
+
* @param {string} cwd - 工作目录
|
|
144
|
+
* @param {boolean} [autoFlag] - --auto 命令行参数
|
|
145
|
+
* @returns {Promise<boolean>} 是否成功解决了所有冲突
|
|
146
|
+
*/
|
|
147
|
+
export async function handleMergeConflict(
|
|
148
|
+
currentBranch: string,
|
|
149
|
+
incomingBranch: string,
|
|
150
|
+
cwd: string,
|
|
151
|
+
autoFlag?: boolean,
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
const mode = determineConflictResolveMode(autoFlag);
|
|
154
|
+
|
|
155
|
+
if (mode === 'manual') {
|
|
156
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (mode === 'auto') {
|
|
160
|
+
return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// mode === 'ask'
|
|
164
|
+
const shouldUseAI = await confirmAction(MESSAGES.MERGE_CONFLICT_ASK_AI);
|
|
165
|
+
if (!shouldUseAI) {
|
|
166
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
|
|
170
|
+
}
|
package/src/utils/git-core.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
2
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
3
3
|
import { execCommand, execCommandWithInput } from './shell.js';
|
|
4
4
|
import { logger } from '../logger/index.js';
|
|
5
5
|
|
|
@@ -367,3 +367,51 @@ export function gitApplyCachedCheck(patchContent: Buffer, cwd?: string): boolean
|
|
|
367
367
|
return false;
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 获取冲突文件列表
|
|
373
|
+
* 通过 git status --porcelain 解析 UU/AA/DD 等冲突标记
|
|
374
|
+
* @param {string} [cwd] - 工作目录
|
|
375
|
+
* @returns {string[]} 冲突文件路径列表
|
|
376
|
+
*/
|
|
377
|
+
export function getConflictFiles(cwd?: string): string[] {
|
|
378
|
+
const status = getStatusPorcelain(cwd);
|
|
379
|
+
if (!status) return [];
|
|
380
|
+
return status
|
|
381
|
+
.split('\n')
|
|
382
|
+
.filter((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line))
|
|
383
|
+
.map((line) => line.slice(3));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* git add 指定文件
|
|
388
|
+
* 使用 execFileSync 数组参数形式避免文件名中特殊字符导致的注入风险
|
|
389
|
+
* @param {string[]} files - 文件路径列表
|
|
390
|
+
* @param {string} [cwd] - 工作目录
|
|
391
|
+
*/
|
|
392
|
+
export function gitAddFiles(files: string[], cwd?: string): void {
|
|
393
|
+
if (files.length === 0) return;
|
|
394
|
+
const args = ['add', '--', ...files];
|
|
395
|
+
logger.debug(`执行命令: git ${args.join(' ')}${cwd ? ` (cwd: ${cwd})` : ''}`);
|
|
396
|
+
execFileSync('git', args, {
|
|
397
|
+
cwd,
|
|
398
|
+
encoding: 'utf-8',
|
|
399
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* git merge --continue(非交互式)
|
|
405
|
+
* @param {string} [cwd] - 工作目录
|
|
406
|
+
*/
|
|
407
|
+
export function gitMergeContinue(cwd?: string): void {
|
|
408
|
+
execCommand('GIT_EDITOR=true git merge --continue', { cwd });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* git merge --abort
|
|
413
|
+
* @param {string} [cwd] - 工作目录
|
|
414
|
+
*/
|
|
415
|
+
export function gitMergeAbort(cwd?: string): void {
|
|
416
|
+
execCommand('git merge --abort', { cwd });
|
|
417
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands } from './shell.js';
|
|
2
|
-
export type { ParallelCommandResult } from './shell.js';
|
|
1
|
+
export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands, runCommandWithStderrCapture, runParallelCommandsWithStderrCapture } from './shell.js';
|
|
2
|
+
export type { ParallelCommandResult, CommandResultWithStderr, ParallelCommandResultWithStderr } from './shell.js';
|
|
3
|
+
export { copyToClipboard } from './clipboard.js';
|
|
3
4
|
export {
|
|
4
5
|
getGitCommonDir,
|
|
5
6
|
getGitTopLevel,
|
|
@@ -48,6 +49,10 @@ export {
|
|
|
48
49
|
getBranchCreatedAt,
|
|
49
50
|
gitCheckout,
|
|
50
51
|
createBranch,
|
|
52
|
+
getConflictFiles,
|
|
53
|
+
gitAddFiles,
|
|
54
|
+
gitMergeContinue,
|
|
55
|
+
gitMergeAbort,
|
|
51
56
|
} from './git.js';
|
|
52
57
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
53
58
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, validateWorkingDirClean, runPreChecks } from './validation.js';
|
|
@@ -59,7 +64,7 @@ export { ensureDir, removeEmptyDir, calculateDirSize } from './fs.js';
|
|
|
59
64
|
export { multilineInput } from './prompt.js';
|
|
60
65
|
export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
|
|
61
66
|
export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
|
|
62
|
-
export { findExactMatch, findFuzzyMatches,
|
|
67
|
+
export { findExactMatch, findFuzzyMatches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate, getWorktreeCreatedTime } from './worktree-matcher.js';
|
|
63
68
|
export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
|
|
64
69
|
export { ProgressRenderer } from './progress.js';
|
|
65
70
|
export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
|
|
@@ -79,4 +84,5 @@ export { migrateChangesViaPatch, computeCurrentTreeHash, saveCurrentSnapshotTree
|
|
|
79
84
|
export { InteractivePanel } from './interactive-panel.js';
|
|
80
85
|
export { buildPanelFrame, buildGroupedWorktreeLines, buildDisplayOrder, renderDateSeparator, renderWorktreeBlock, renderSnapshotSummary, renderFooter, calculateVisibleRows } from './interactive-panel-render.js';
|
|
81
86
|
export type { PanelLine } from './interactive-panel-render.js';
|
|
87
|
+
export { buildConflictResolvePrompt, invokeClaudeForConflictResolve, resolveConflictsWithAI, determineConflictResolveMode, handleMergeConflict } from './conflict-resolver.js';
|
|
82
88
|
|
package/src/utils/shell.ts
CHANGED
|
@@ -11,6 +11,22 @@ export interface ParallelCommandResult {
|
|
|
11
11
|
error?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** 带 stderr 捕获的命令执行结果 */
|
|
15
|
+
export interface CommandResultWithStderr {
|
|
16
|
+
/** 进程退出码 */
|
|
17
|
+
exitCode: number;
|
|
18
|
+
/** 进程启动失败时的错误信息 */
|
|
19
|
+
error?: string;
|
|
20
|
+
/** 捕获的 stderr 输出内容 */
|
|
21
|
+
stderr: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 带 stderr 捕获的并行命令执行结果 */
|
|
25
|
+
export interface ParallelCommandResultWithStderr extends ParallelCommandResult {
|
|
26
|
+
/** 捕获的 stderr 输出内容 */
|
|
27
|
+
stderr: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
/**
|
|
15
31
|
* 同步执行 shell 命令并返回 stdout
|
|
16
32
|
* @param {string} command - 要执行的命令
|
|
@@ -157,3 +173,83 @@ export function runParallelCommands(
|
|
|
157
173
|
|
|
158
174
|
return Promise.all(promises);
|
|
159
175
|
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 以 shell 模式启动子进程,捕获 stderr 同时实时回显到终端
|
|
179
|
+
* stdout 直接继承父进程(实时输出),stderr 通过 pipe 捕获并同步回显
|
|
180
|
+
* @param {string} command - 要执行的命令字符串
|
|
181
|
+
* @param {object} options - 可选配置
|
|
182
|
+
* @param {string} options.cwd - 工作目录
|
|
183
|
+
* @returns {Promise<{ exitCode: number, error?: string, stderr: string }>} 退出码、错误信息和 stderr 内容
|
|
184
|
+
*/
|
|
185
|
+
function spawnWithStderrCapture(
|
|
186
|
+
command: string,
|
|
187
|
+
options?: { cwd?: string },
|
|
188
|
+
): Promise<{ exitCode: number; error?: string; stderr: string }> {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
const child = spawn(command, {
|
|
191
|
+
cwd: options?.cwd,
|
|
192
|
+
stdio: ['inherit', 'inherit', 'pipe'],
|
|
193
|
+
shell: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const stderrChunks: Buffer[] = [];
|
|
197
|
+
|
|
198
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
199
|
+
// 实时回显到终端
|
|
200
|
+
process.stderr.write(chunk);
|
|
201
|
+
// 累积到 buffer
|
|
202
|
+
stderrChunks.push(chunk);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
child.on('error', (err) => {
|
|
206
|
+
resolve({
|
|
207
|
+
exitCode: 1,
|
|
208
|
+
error: err.message,
|
|
209
|
+
stderr: Buffer.concat(stderrChunks).toString('utf-8'),
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
child.on('close', (code) => {
|
|
214
|
+
resolve({
|
|
215
|
+
exitCode: code ?? 1,
|
|
216
|
+
stderr: Buffer.concat(stderrChunks).toString('utf-8'),
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 异步执行命令,捕获 stderr 同时实时回显到终端
|
|
224
|
+
* @param {string} command - 要执行的命令字符串
|
|
225
|
+
* @param {object} options - 可选配置
|
|
226
|
+
* @param {string} options.cwd - 工作目录
|
|
227
|
+
* @returns {Promise<CommandResultWithStderr>} 包含退出码和 stderr 内容的结果
|
|
228
|
+
*/
|
|
229
|
+
export function runCommandWithStderrCapture(
|
|
230
|
+
command: string,
|
|
231
|
+
options?: { cwd?: string },
|
|
232
|
+
): Promise<CommandResultWithStderr> {
|
|
233
|
+
logger.debug(`执行命令(stderr捕获): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
|
|
234
|
+
return spawnWithStderrCapture(command, options);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 并行执行多个命令,捕获每个命令的 stderr 同时实时回显到终端
|
|
239
|
+
* @param {string[]} commands - 要并行执行的命令数组
|
|
240
|
+
* @param {object} options - 可选配置
|
|
241
|
+
* @param {string} options.cwd - 工作目录
|
|
242
|
+
* @returns {Promise<ParallelCommandResultWithStderr[]>} 各命令的执行结果(含 stderr)
|
|
243
|
+
*/
|
|
244
|
+
export function runParallelCommandsWithStderrCapture(
|
|
245
|
+
commands: string[],
|
|
246
|
+
options?: { cwd?: string },
|
|
247
|
+
): Promise<ParallelCommandResultWithStderr[]> {
|
|
248
|
+
const promises = commands.map(async (command): Promise<ParallelCommandResultWithStderr> => {
|
|
249
|
+
logger.debug(`并行启动命令(stderr捕获): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
|
|
250
|
+
const result = await spawnWithStderrCapture(command, options);
|
|
251
|
+
return { command, ...result };
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return Promise.all(promises);
|
|
255
|
+
}
|
|
@@ -3,57 +3,101 @@ import {
|
|
|
3
3
|
printInfo,
|
|
4
4
|
printSuccess,
|
|
5
5
|
printError,
|
|
6
|
+
printWarning,
|
|
6
7
|
printSeparator,
|
|
7
|
-
runCommandInherited,
|
|
8
8
|
parseParallelCommands,
|
|
9
|
-
runParallelCommands,
|
|
10
9
|
} from './index.js';
|
|
11
|
-
import
|
|
10
|
+
import { runCommandWithStderrCapture, runParallelCommandsWithStderrCapture } from './shell.js';
|
|
11
|
+
import type { ParallelCommandResultWithStderr } from './shell.js';
|
|
12
|
+
import { copyToClipboard } from './clipboard.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
+
* 处理命令执行失败后的剪贴板复制逻辑
|
|
16
|
+
* 将格式化的错误信息复制到系统剪贴板,并输出操作结果提示
|
|
17
|
+
* @param {string} clipboardContent - 要复制到剪贴板的完整错误信息
|
|
18
|
+
*/
|
|
19
|
+
function handleErrorClipboard(clipboardContent: string): void {
|
|
20
|
+
const success = copyToClipboard(clipboardContent);
|
|
21
|
+
if (success) {
|
|
22
|
+
printInfo(MESSAGES.VALIDATE_RUN_ERROR_COPIED);
|
|
23
|
+
} else {
|
|
24
|
+
printWarning(MESSAGES.VALIDATE_RUN_ERROR_COPY_FAILED);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 构建单命令失败时的剪贴板内容
|
|
30
|
+
* @param {string} command - 失败的命令
|
|
31
|
+
* @param {string} stderr - 捕获的 stderr 内容
|
|
32
|
+
* @param {number} exitCode - 进程退出码
|
|
33
|
+
* @returns {string} 格式化后的剪贴板内容
|
|
34
|
+
*/
|
|
35
|
+
function buildSingleErrorClipboard(command: string, stderr: string, exitCode: number): string {
|
|
36
|
+
if (stderr.trim()) {
|
|
37
|
+
return MESSAGES.VALIDATE_CLIPBOARD_SINGLE_ERROR(command, stderr.trim());
|
|
38
|
+
}
|
|
39
|
+
return `${command} 指令执行出错,退出码: ${exitCode}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 执行单个命令(异步方式,捕获 stderr 并在失败时复制到剪贴板)
|
|
15
44
|
* @param {string} command - 要执行的命令字符串
|
|
16
45
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
17
46
|
*/
|
|
18
|
-
function executeSingleCommand(command: string, mainWorktreePath: string): void {
|
|
47
|
+
async function executeSingleCommand(command: string, mainWorktreePath: string): Promise<void> {
|
|
19
48
|
printInfo(MESSAGES.VALIDATE_RUN_START(command));
|
|
20
49
|
printSeparator();
|
|
21
50
|
|
|
22
|
-
const result =
|
|
51
|
+
const result = await runCommandWithStderrCapture(command, { cwd: mainWorktreePath });
|
|
23
52
|
|
|
24
53
|
printSeparator();
|
|
25
54
|
|
|
26
55
|
if (result.error) {
|
|
27
56
|
// 进程启动失败(如命令不存在)
|
|
28
|
-
printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error
|
|
57
|
+
printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error));
|
|
58
|
+
const clipboardContent = MESSAGES.VALIDATE_CLIPBOARD_SINGLE_ERROR(command, result.error);
|
|
59
|
+
handleErrorClipboard(clipboardContent);
|
|
29
60
|
return;
|
|
30
61
|
}
|
|
31
62
|
|
|
32
|
-
|
|
33
|
-
if (exitCode === 0) {
|
|
63
|
+
if (result.exitCode === 0) {
|
|
34
64
|
printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
|
|
35
65
|
} else {
|
|
36
|
-
printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
|
|
66
|
+
printError(MESSAGES.VALIDATE_RUN_FAILED(command, result.exitCode));
|
|
67
|
+
const clipboardContent = buildSingleErrorClipboard(command, result.stderr, result.exitCode);
|
|
68
|
+
handleErrorClipboard(clipboardContent);
|
|
37
69
|
}
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
/**
|
|
41
|
-
*
|
|
42
|
-
* @param {
|
|
73
|
+
* 汇总输出并行命令的执行结果,并将失败命令的错误信息复制到剪贴板
|
|
74
|
+
* @param {ParallelCommandResultWithStderr[]} results - 各命令的执行结果数组
|
|
43
75
|
*/
|
|
44
|
-
function reportParallelResults(results:
|
|
76
|
+
function reportParallelResults(results: ParallelCommandResultWithStderr[]): void {
|
|
45
77
|
printSeparator();
|
|
46
78
|
|
|
47
79
|
const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
|
|
48
80
|
const failedCount = results.length - successCount;
|
|
81
|
+
const errorClipboardParts: string[] = [];
|
|
49
82
|
|
|
50
83
|
for (const result of results) {
|
|
51
84
|
if (result.error) {
|
|
52
85
|
printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
|
|
86
|
+
// 收集错误信息用于剪贴板
|
|
87
|
+
errorClipboardParts.push(
|
|
88
|
+
MESSAGES.VALIDATE_CLIPBOARD_PARALLEL_ERROR(result.command, result.error),
|
|
89
|
+
);
|
|
53
90
|
} else if (result.exitCode === 0) {
|
|
54
91
|
printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
|
|
55
92
|
} else {
|
|
56
93
|
printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
|
|
94
|
+
// 收集错误信息用于剪贴板
|
|
95
|
+
const errorContent = result.stderr.trim()
|
|
96
|
+
? result.stderr.trim()
|
|
97
|
+
: `退出码: ${result.exitCode}`;
|
|
98
|
+
errorClipboardParts.push(
|
|
99
|
+
MESSAGES.VALIDATE_CLIPBOARD_PARALLEL_ERROR(result.command, errorContent),
|
|
100
|
+
);
|
|
57
101
|
}
|
|
58
102
|
}
|
|
59
103
|
|
|
@@ -61,6 +105,9 @@ function reportParallelResults(results: ParallelCommandResult[]): void {
|
|
|
61
105
|
printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
|
|
62
106
|
} else {
|
|
63
107
|
printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
|
|
108
|
+
// 将所有失败命令的错误信息拼接后一次性复制到剪贴板
|
|
109
|
+
const clipboardContent = errorClipboardParts.join(MESSAGES.VALIDATE_CLIPBOARD_SEPARATOR);
|
|
110
|
+
handleErrorClipboard(clipboardContent);
|
|
64
111
|
}
|
|
65
112
|
}
|
|
66
113
|
|
|
@@ -78,7 +125,7 @@ async function executeParallelCommands(commands: string[], mainWorktreePath: str
|
|
|
78
125
|
|
|
79
126
|
printSeparator();
|
|
80
127
|
|
|
81
|
-
const results = await
|
|
128
|
+
const results = await runParallelCommandsWithStderrCapture(commands, { cwd: mainWorktreePath });
|
|
82
129
|
|
|
83
130
|
reportParallelResults(results);
|
|
84
131
|
}
|
|
@@ -96,8 +143,8 @@ export async function executeRunCommand(command: string, mainWorktreePath: strin
|
|
|
96
143
|
const commands = parseParallelCommands(command);
|
|
97
144
|
|
|
98
145
|
if (commands.length <= 1) {
|
|
99
|
-
// 单命令(包括含 &&
|
|
100
|
-
executeSingleCommand(commands[0] || command, mainWorktreePath);
|
|
146
|
+
// 单命令(包括含 && 的串行命令),走异步路径并捕获 stderr
|
|
147
|
+
await executeSingleCommand(commands[0] || command, mainWorktreePath);
|
|
101
148
|
} else {
|
|
102
149
|
// 多命令,并行执行
|
|
103
150
|
await executeParallelCommands(commands, mainWorktreePath);
|