mstro-app 0.4.38 → 0.4.43
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/bin/commands/login.js +17 -7
- package/bin/commands/logout.js +14 -6
- package/bin/commands/status.js +9 -3
- package/bin/commands/whoami.js +10 -4
- package/bin/mstro.js +11 -1
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/index.d.ts +1 -0
- package/dist/server/cli/headless/index.d.ts.map +1 -1
- package/dist/server/cli/headless/index.js +2 -0
- package/dist/server/cli/headless/index.js.map +1 -1
- package/dist/server/cli/headless/resilient-runner.d.ts +47 -0
- package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -0
- package/dist/server/cli/headless/resilient-runner.js +234 -0
- package/dist/server/cli/headless/resilient-runner.js.map +1 -0
- package/dist/server/cli/headless/retry-strategies.d.ts +44 -0
- package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -0
- package/dist/server/cli/headless/retry-strategies.js +262 -0
- package/dist/server/cli/headless/retry-strategies.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +5 -0
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +2 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +31 -4
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +1 -30
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +16 -3
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/prompt-builders.d.ts.map +1 -1
- package/dist/server/cli/prompt-builders.js +31 -13
- package/dist/server/cli/prompt-builders.js.map +1 -1
- package/dist/server/index.js +1 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +5 -4
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +1 -1
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +14 -8
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-patterns.js +1 -1
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +19 -9
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +6 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +158 -76
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts +1 -0
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +6 -0
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-classification.d.ts +11 -0
- package/dist/server/services/plan/issue-classification.d.ts.map +1 -0
- package/dist/server/services/plan/issue-classification.js +20 -0
- package/dist/server/services/plan/issue-classification.js.map +1 -0
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +10 -5
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +0 -5
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
- package/dist/server/services/plan/issue-retry.js +12 -241
- package/dist/server/services/plan/issue-retry.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +9 -6
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.js +11 -4
- package/dist/server/services/platform-credentials.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +7 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +2 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +2 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +18 -7
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +6 -6
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-fix-agent.js +90 -42
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +48 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +22 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +48 -1
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +74 -32
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +18 -18
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/skill-handlers.d.ts +3 -1
- package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-handlers.js +52 -41
- package/dist/server/services/websocket/skill-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-watcher.d.ts +17 -0
- package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -0
- package/dist/server/services/websocket/skill-watcher.js +85 -0
- package/dist/server/services/websocket/skill-watcher.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +2 -268
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +0 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-stream.ts +1 -0
- package/server/cli/headless/index.ts +2 -0
- package/server/cli/headless/resilient-runner.ts +354 -0
- package/server/cli/headless/retry-strategies.ts +330 -0
- package/server/cli/headless/stall-assessor.ts +5 -0
- package/server/cli/headless/tool-watchdog.ts +40 -4
- package/server/cli/improvisation-retry.ts +1 -32
- package/server/cli/improvisation-session-manager.ts +17 -3
- package/server/cli/prompt-builders.ts +33 -12
- package/server/index.ts +1 -9
- package/server/mcp/bouncer-cli.ts +5 -4
- package/server/mcp/bouncer-haiku.ts +1 -1
- package/server/mcp/bouncer-integration.ts +15 -8
- package/server/mcp/security-patterns.ts +1 -1
- package/server/services/plan/agents/code-review.md +109 -0
- package/server/services/plan/agents/commit-message.md +26 -0
- package/server/services/plan/agents/execute-issue.md +10 -1
- package/server/services/plan/agents/fix-quality.md +24 -0
- package/server/services/plan/agents/pr-description.md +28 -0
- package/server/services/plan/composer.ts +20 -9
- package/server/services/plan/executor.ts +160 -76
- package/server/services/plan/front-matter.ts +7 -0
- package/server/services/plan/issue-classification.ts +21 -0
- package/server/services/plan/issue-prompt-builder.ts +11 -5
- package/server/services/plan/issue-retry.ts +15 -330
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/review-gate.ts +9 -6
- package/server/services/plan/types.ts +3 -0
- package/server/services/platform-credentials.ts +10 -4
- package/server/services/terminal/pty-manager.ts +7 -1
- package/server/services/websocket/handler-context.ts +2 -0
- package/server/services/websocket/handler.ts +18 -8
- package/server/services/websocket/plan-execution-handlers.ts +7 -7
- package/server/services/websocket/quality-fix-agent.ts +86 -44
- package/server/services/websocket/quality-handlers.ts +48 -7
- package/server/services/websocket/quality-persistence.ts +75 -1
- package/server/services/websocket/quality-review-agent.ts +70 -31
- package/server/services/websocket/quality-tools.ts +16 -14
- package/server/services/websocket/skill-handlers.ts +50 -40
- package/server/services/websocket/skill-watcher.ts +79 -0
- package/server/services/websocket/types.ts +0 -311
- package/dist/server/services/deploy/ai-broker.d.ts +0 -63
- package/dist/server/services/deploy/ai-broker.d.ts.map +0 -1
- package/dist/server/services/deploy/ai-broker.js +0 -360
- package/dist/server/services/deploy/ai-broker.js.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.d.ts +0 -114
- package/dist/server/services/deploy/board-execution-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/board-execution-handler.js +0 -621
- package/dist/server/services/deploy/board-execution-handler.js.map +0 -1
- package/dist/server/services/deploy/credentials.d.ts +0 -35
- package/dist/server/services/deploy/credentials.d.ts.map +0 -1
- package/dist/server/services/deploy/credentials.js +0 -177
- package/dist/server/services/deploy/credentials.js.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.d.ts +0 -107
- package/dist/server/services/deploy/deploy-ai-service.d.ts.map +0 -1
- package/dist/server/services/deploy/deploy-ai-service.js +0 -294
- package/dist/server/services/deploy/deploy-ai-service.js.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.d.ts +0 -94
- package/dist/server/services/deploy/headless-session-handler.d.ts.map +0 -1
- package/dist/server/services/deploy/headless-session-handler.js +0 -266
- package/dist/server/services/deploy/headless-session-handler.js.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.d.ts +0 -14
- package/dist/server/services/websocket/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/deploy-handlers.js +0 -409
- package/dist/server/services/websocket/deploy-handlers.js.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +0 -11
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +0 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.js +0 -176
- package/dist/server/services/websocket/handlers/deploy-handlers.js.map +0 -1
- package/server/cli/headless/RESEARCH.md +0 -627
- package/server/services/deploy/ai-broker.ts +0 -512
- package/server/services/deploy/board-execution-handler.ts +0 -847
- package/server/services/deploy/credentials.ts +0 -200
- package/server/services/deploy/deploy-ai-service.ts +0 -401
- package/server/services/deploy/headless-session-handler.ts +0 -414
- package/server/services/websocket/deploy-handlers.ts +0 -544
- package/server/services/websocket/handlers/deploy-handlers.ts +0 -228
|
@@ -33,7 +33,7 @@ export function handlePrompt(
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function wireExecutorEvents(executor: PlanExecutor, ctx: HandlerContext, workingDir: string): void {
|
|
36
|
+
function wireExecutorEvents(executor: PlanExecutor, ctx: HandlerContext, workingDir: string, boardId?: string): void {
|
|
37
37
|
executor.removeAllListeners();
|
|
38
38
|
|
|
39
39
|
executor.on('statusChanged', (status: string) => {
|
|
@@ -48,7 +48,7 @@ function wireExecutorEvents(executor: PlanExecutor, ctx: HandlerContext, working
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
executor.on('output', (data: { issueId: string; text: string }) => {
|
|
51
|
-
ctx.broadcastToAll({ type: 'planExecutionOutput', data });
|
|
51
|
+
ctx.broadcastToAll({ type: 'planExecutionOutput', data: { ...data, boardId: boardId ?? null } });
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
executor.on('issueCompleted', () => {
|
|
@@ -86,7 +86,7 @@ function wireExecutorEvents(executor: PlanExecutor, ctx: HandlerContext, working
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
executor.on('complete', (reason: string) => {
|
|
89
|
-
ctx.broadcastToAll({ type: 'planExecutionComplete', data: { reason, metrics: executor.getMetrics() } });
|
|
89
|
+
ctx.broadcastToAll({ type: 'planExecutionComplete', data: { reason, boardId: boardId ?? null, metrics: executor.getMetrics() } });
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
executor.on('error', (error: string) => {
|
|
@@ -107,11 +107,11 @@ export function handleExecute(
|
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
wireExecutorEvents(executor, ctx, workingDir);
|
|
111
|
-
|
|
112
110
|
const boardId = msg.data?.boardId as string | undefined;
|
|
111
|
+
wireExecutorEvents(executor, ctx, workingDir, boardId);
|
|
112
|
+
|
|
113
113
|
const executionDir = boardId ? ctx.gitDirectories.get(boardId) : undefined;
|
|
114
|
-
ctx.
|
|
114
|
+
ctx.broadcastToAll({ type: 'planExecutionStarted', data: { status: 'executing', boardId } });
|
|
115
115
|
const startPromise = boardId ? executor.startBoard(boardId, executionDir) : executor.start();
|
|
116
116
|
startPromise.catch(error => {
|
|
117
117
|
ctx.send(ws, {
|
|
@@ -142,7 +142,7 @@ export function handleExecuteEpic(
|
|
|
142
142
|
|
|
143
143
|
wireExecutorEvents(executor, ctx, workingDir);
|
|
144
144
|
|
|
145
|
-
ctx.
|
|
145
|
+
ctx.broadcastToAll({ type: 'planExecutionStarted', data: { status: 'executing', epicPath } });
|
|
146
146
|
executor.startEpic(epicPath).catch(error => {
|
|
147
147
|
ctx.send(ws, {
|
|
148
148
|
type: 'planExecutionError',
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* Builds the fix prompt, runs the agent, re-scans, and persists updated results.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
10
|
+
import { ResilientRunner } from '../../cli/headless/resilient-runner.js';
|
|
12
11
|
import type { ToolUseEvent } from '../../cli/headless/types.js';
|
|
13
12
|
import { loadSkillPrompt } from '../plan/agent-loader.js';
|
|
14
13
|
import type { HandlerContext } from './handler-context.js';
|
|
@@ -41,25 +40,45 @@ const TOOL_MESSAGES: Record<string, string> = {
|
|
|
41
40
|
export function createToolProgressCallback(ctx: HandlerContext, ws: WSContext, reportPath: string) {
|
|
42
41
|
const seenTools = new Set<string>();
|
|
43
42
|
return (event: ToolUseEvent) => {
|
|
44
|
-
|
|
45
|
-
seenTools.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
try {
|
|
44
|
+
if (event.type === 'tool_start' && event.toolName && !seenTools.has(event.toolName)) {
|
|
45
|
+
seenTools.add(event.toolName);
|
|
46
|
+
const message = TOOL_MESSAGES[event.toolName];
|
|
47
|
+
if (message) {
|
|
48
|
+
ctx.send(ws, { type: 'qualityFixProgress', data: { path: reportPath, message } });
|
|
49
|
+
}
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
51
|
+
if (event.type === 'tool_complete' && event.toolName === 'Edit' && event.completeInput?.file_path) {
|
|
52
|
+
ctx.send(ws, {
|
|
53
|
+
type: 'qualityFixProgress',
|
|
54
|
+
data: { path: reportPath, message: `Fixed ${String(event.completeInput.file_path).split('/').slice(-2).join('/')}` },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// WebSocket closed — progress lost but fix operation continues
|
|
56
59
|
}
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function startFixHeartbeat(ctx: HandlerContext, ws: WSContext, reportPath: string, intervalMs = 30_000): () => void {
|
|
64
|
+
let elapsed = 0;
|
|
65
|
+
const timer = setInterval(() => {
|
|
66
|
+
elapsed += intervalMs;
|
|
67
|
+
const mins = Math.floor(elapsed / 60_000);
|
|
68
|
+
const secs = Math.floor((elapsed % 60_000) / 1000);
|
|
69
|
+
const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
70
|
+
try {
|
|
71
|
+
ctx.send(ws, { type: 'qualityFixProgress', data: { path: reportPath, message: `Fixing issues (${timeStr} elapsed, still working...)` } });
|
|
72
|
+
} catch {
|
|
73
|
+
// WebSocket closed — heartbeat lost but fix continues
|
|
74
|
+
}
|
|
75
|
+
}, intervalMs);
|
|
76
|
+
return () => clearInterval(timer);
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
// ── Prompt ────────────────────────────────────────────────────
|
|
61
80
|
|
|
62
|
-
function buildFixPrompt(findings: FindingForFix[], section?: string
|
|
81
|
+
function buildFixPrompt(findings: FindingForFix[], section?: string): string {
|
|
63
82
|
const filtered = section ? findings.filter((f) => f.category === section) : findings;
|
|
64
83
|
const sorted = filtered.sort((a, b) => {
|
|
65
84
|
const order: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
@@ -78,7 +97,7 @@ function buildFixPrompt(findings: FindingForFix[], section?: string, workingDir?
|
|
|
78
97
|
issueList,
|
|
79
98
|
issueCount: String(sorted.length),
|
|
80
99
|
showCount: String(Math.min(30, sorted.length)),
|
|
81
|
-
}
|
|
100
|
+
});
|
|
82
101
|
if (fromSkill) return fromSkill;
|
|
83
102
|
|
|
84
103
|
return `You are a code quality fix agent. Fix the following quality issues in the codebase.\n\n## Issues to Fix (${sorted.length} total, showing top ${Math.min(30, sorted.length)})\n\n${issueList}\n\nFix each issue by editing the relevant file. Work from most to least severe. Do NOT introduce new issues.`;
|
|
@@ -115,47 +134,51 @@ export async function handleFixIssues(
|
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
activeFixes.add(dirPath);
|
|
137
|
+
const stopHeartbeat = startFixHeartbeat(ctx, ws, reportPath);
|
|
118
138
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
139
|
+
try {
|
|
140
|
+
ctx.send(ws, {
|
|
141
|
+
type: 'qualityFixProgress',
|
|
142
|
+
data: { path: reportPath, message: 'Starting Claude Code to fix issues...' },
|
|
143
|
+
});
|
|
144
|
+
} catch { /* WS closed */ }
|
|
123
145
|
|
|
124
|
-
const prompt = buildFixPrompt(findings, section
|
|
146
|
+
const prompt = buildFixPrompt(findings, section);
|
|
125
147
|
|
|
126
|
-
const runner = new
|
|
148
|
+
const runner = new ResilientRunner({
|
|
127
149
|
workingDir: dirPath,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
prompt,
|
|
151
|
+
policy: 'STANDARD',
|
|
152
|
+
stallWarningMs: 300_000,
|
|
153
|
+
stallKillMs: 1_200_000,
|
|
154
|
+
stallHardCapMs: 1_800_000,
|
|
132
155
|
toolUseCallback: createToolProgressCallback(ctx, ws, reportPath),
|
|
156
|
+
logLabel: 'code-review-fix',
|
|
133
157
|
});
|
|
134
158
|
|
|
135
|
-
await
|
|
159
|
+
await runner.run();
|
|
136
160
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
try {
|
|
162
|
+
ctx.send(ws, {
|
|
163
|
+
type: 'qualityFixProgress',
|
|
164
|
+
data: { path: reportPath, message: 'Fixes applied. Re-running quality checks...' },
|
|
165
|
+
});
|
|
166
|
+
} catch { /* WS closed */ }
|
|
141
167
|
|
|
142
168
|
// Re-run quality scan after fixing
|
|
143
169
|
const { tools: detectedTools } = await detectTools(dirPath);
|
|
144
170
|
const installedToolNames = detectedTools.filter((t) => t.installed).map((t) => t.name);
|
|
145
171
|
|
|
146
172
|
const results = await runQualityScan(dirPath, (progress) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
173
|
+
try {
|
|
174
|
+
ctx.send(ws, {
|
|
175
|
+
type: 'qualityScanProgress',
|
|
176
|
+
data: { path: reportPath, progress },
|
|
177
|
+
});
|
|
178
|
+
} catch { /* WS closed */ }
|
|
151
179
|
}, installedToolNames);
|
|
152
180
|
|
|
153
|
-
|
|
154
|
-
type: 'qualityFixComplete',
|
|
155
|
-
data: { path: reportPath, results },
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Persist
|
|
181
|
+
// Persist before sending — results survive WebSocket drops
|
|
159
182
|
try {
|
|
160
183
|
const persistence = getPersistence(workingDir);
|
|
161
184
|
persistence.saveReport(reportPath, results);
|
|
@@ -163,12 +186,31 @@ export async function handleFixIssues(
|
|
|
163
186
|
} catch {
|
|
164
187
|
// Persistence failure should not break the fix flow
|
|
165
188
|
}
|
|
189
|
+
|
|
190
|
+
const resultData = { path: reportPath, results };
|
|
191
|
+
try {
|
|
192
|
+
ctx.send(ws, { type: 'qualityFixComplete', data: resultData });
|
|
193
|
+
} catch {
|
|
194
|
+
// WebSocket closed — save as pending for delivery on reconnect
|
|
195
|
+
const persistence = getPersistence(workingDir);
|
|
196
|
+
persistence.addPendingResult({
|
|
197
|
+
type: 'fixComplete',
|
|
198
|
+
path: reportPath,
|
|
199
|
+
data: resultData as unknown as Record<string, unknown>,
|
|
200
|
+
completedAt: new Date().toISOString(),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
166
203
|
} catch (error) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
204
|
+
try {
|
|
205
|
+
ctx.send(ws, {
|
|
206
|
+
type: 'qualityError',
|
|
207
|
+
data: { path: reportPath, error: error instanceof Error ? error.message : String(error) },
|
|
208
|
+
});
|
|
209
|
+
} catch {
|
|
210
|
+
// WebSocket closed — error lost but operation tracked via activeOps
|
|
211
|
+
}
|
|
171
212
|
} finally {
|
|
213
|
+
stopHeartbeat();
|
|
172
214
|
activeFixes.delete(dirPath);
|
|
173
215
|
}
|
|
174
216
|
}
|
|
@@ -87,7 +87,10 @@ export function handleQualityMessage(
|
|
|
87
87
|
const { resolved: dirPath, error } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
|
|
88
88
|
if (error) { sendPathError(msg.data?.path || '.', error); return; }
|
|
89
89
|
const reportPath = msg.data?.path || '.';
|
|
90
|
-
|
|
90
|
+
const persistence = getPersistence(workingDir);
|
|
91
|
+
persistence.setActiveOperation(reportPath, 'reviewing');
|
|
92
|
+
handleCodeReview(ctx, ws, reportPath, dirPath, workingDir, activeReviews, getPersistence)
|
|
93
|
+
.finally(() => persistence.clearActiveOperation(reportPath));
|
|
91
94
|
},
|
|
92
95
|
qualityFixIssues: () => {
|
|
93
96
|
const { resolved: dirPath, error } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
|
|
@@ -95,9 +98,17 @@ export function handleQualityMessage(
|
|
|
95
98
|
const reportPath = msg.data?.path || '.';
|
|
96
99
|
const section: string | undefined = msg.data?.section;
|
|
97
100
|
const findings: FindingForFix[] = msg.data?.findings || [];
|
|
98
|
-
|
|
101
|
+
const persistence = getPersistence(workingDir);
|
|
102
|
+
persistence.setActiveOperation(reportPath, 'fixing');
|
|
103
|
+
handleFixIssues(ctx, ws, reportPath, dirPath, workingDir, section, findings, getPersistence)
|
|
104
|
+
.finally(() => persistence.clearActiveOperation(reportPath));
|
|
99
105
|
},
|
|
100
106
|
qualityLoadState: () => handleLoadState(ctx, ws, workingDir),
|
|
107
|
+
qualityClearPending: () => {
|
|
108
|
+
const persistence = getPersistence(workingDir);
|
|
109
|
+
const path: string | undefined = msg.data?.path;
|
|
110
|
+
persistence.clearPendingResults(path);
|
|
111
|
+
},
|
|
101
112
|
qualitySaveDirectories: () => handleSaveDirectories(ctx, ws, msg, workingDir, isSandboxed),
|
|
102
113
|
};
|
|
103
114
|
|
|
@@ -125,6 +136,22 @@ async function handleLoadState(
|
|
|
125
136
|
try {
|
|
126
137
|
const persistence = getPersistence(workingDir);
|
|
127
138
|
const state = persistence.loadState();
|
|
139
|
+
|
|
140
|
+
// Deliver pending results that were completed while the client was disconnected.
|
|
141
|
+
// Clear them after delivery so they don't re-send on the next reconnect.
|
|
142
|
+
if (state.pendingResults.length > 0) {
|
|
143
|
+
for (const pending of state.pendingResults) {
|
|
144
|
+
if (pending.type === 'scanResults') {
|
|
145
|
+
ctx.send(ws, { type: 'qualityScanResults', data: pending.data });
|
|
146
|
+
} else if (pending.type === 'codeReview') {
|
|
147
|
+
ctx.send(ws, { type: 'qualityCodeReview', data: pending.data });
|
|
148
|
+
} else if (pending.type === 'fixComplete') {
|
|
149
|
+
ctx.send(ws, { type: 'qualityFixComplete', data: pending.data });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
persistence.clearPendingResults();
|
|
153
|
+
}
|
|
154
|
+
|
|
128
155
|
ctx.send(ws, { type: 'qualityStateLoaded', data: state });
|
|
129
156
|
} catch (error) {
|
|
130
157
|
ctx.send(ws, {
|
|
@@ -207,8 +234,11 @@ async function handleScan(
|
|
|
207
234
|
return;
|
|
208
235
|
}
|
|
209
236
|
const reportPath = msg.data?.path || '.';
|
|
237
|
+
const persistence = getPersistence(workingDir);
|
|
210
238
|
|
|
211
239
|
try {
|
|
240
|
+
persistence.setActiveOperation(reportPath, 'scanning');
|
|
241
|
+
|
|
212
242
|
const { tools: detectedTools } = await detectTools(dirPath);
|
|
213
243
|
const installedToolNames = detectedTools.filter((t) => t.installed).map((t) => t.name);
|
|
214
244
|
|
|
@@ -218,23 +248,34 @@ async function handleScan(
|
|
|
218
248
|
data: { path: reportPath, progress },
|
|
219
249
|
});
|
|
220
250
|
}, installedToolNames);
|
|
221
|
-
ctx.send(ws, {
|
|
222
|
-
type: 'qualityScanResults',
|
|
223
|
-
data: { path: reportPath, results },
|
|
224
|
-
});
|
|
225
251
|
|
|
252
|
+
// Persist before sending — results survive if WebSocket drops
|
|
226
253
|
try {
|
|
227
|
-
const persistence = getPersistence(workingDir);
|
|
228
254
|
persistence.saveReport(reportPath, results);
|
|
229
255
|
persistence.appendHistory(results, reportPath);
|
|
230
256
|
} catch {
|
|
231
257
|
// Persistence failure should not break the scan flow
|
|
232
258
|
}
|
|
259
|
+
|
|
260
|
+
const resultData = { path: reportPath, results };
|
|
261
|
+
try {
|
|
262
|
+
ctx.send(ws, { type: 'qualityScanResults', data: resultData });
|
|
263
|
+
} catch {
|
|
264
|
+
// WebSocket closed — save as pending for delivery on reconnect
|
|
265
|
+
persistence.addPendingResult({
|
|
266
|
+
type: 'scanResults',
|
|
267
|
+
path: reportPath,
|
|
268
|
+
data: resultData as unknown as Record<string, unknown>,
|
|
269
|
+
completedAt: new Date().toISOString(),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
233
272
|
} catch (error) {
|
|
234
273
|
ctx.send(ws, {
|
|
235
274
|
type: 'qualityError',
|
|
236
275
|
data: { path: reportPath, error: error instanceof Error ? error.message : String(error) },
|
|
237
276
|
});
|
|
277
|
+
} finally {
|
|
278
|
+
persistence.clearActiveOperation(reportPath);
|
|
238
279
|
}
|
|
239
280
|
}
|
|
240
281
|
|
|
@@ -53,10 +53,27 @@ interface QualityHistory {
|
|
|
53
53
|
entries: QualityHistoryEntry[];
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export type ActiveOperationType = 'scanning' | 'reviewing' | 'fixing';
|
|
57
|
+
|
|
58
|
+
export interface ActiveOperation {
|
|
59
|
+
type: ActiveOperationType;
|
|
60
|
+
path: string;
|
|
61
|
+
startedAt: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PendingResult {
|
|
65
|
+
type: 'scanResults' | 'codeReview' | 'fixComplete';
|
|
66
|
+
path: string;
|
|
67
|
+
data: Record<string, unknown>;
|
|
68
|
+
completedAt: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
56
71
|
export interface QualityPersistedState {
|
|
57
72
|
directories: QualityDirectoryConfig[];
|
|
58
73
|
reports: Record<string, QualityResults>;
|
|
59
74
|
history: QualityHistoryEntry[];
|
|
75
|
+
activeOperations: ActiveOperation[];
|
|
76
|
+
pendingResults: PendingResult[];
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
// ============================================================================
|
|
@@ -268,6 +285,60 @@ export class QualityPersistence {
|
|
|
268
285
|
this.pruneReportHistory();
|
|
269
286
|
}
|
|
270
287
|
|
|
288
|
+
// ---- Active operations (survives WebSocket disconnects) ----
|
|
289
|
+
|
|
290
|
+
private get activeOpsPath(): string {
|
|
291
|
+
return join(this.qualityDir, 'active-ops.json');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private get pendingResultsPath(): string {
|
|
295
|
+
return join(this.qualityDir, 'pending-results.json');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
setActiveOperation(path: string, type: ActiveOperationType): void {
|
|
299
|
+
const ops = this.loadActiveOperations();
|
|
300
|
+
const existing = ops.findIndex((o) => o.path === path);
|
|
301
|
+
const entry: ActiveOperation = { type, path, startedAt: new Date().toISOString() };
|
|
302
|
+
if (existing >= 0) {
|
|
303
|
+
ops[existing] = entry;
|
|
304
|
+
} else {
|
|
305
|
+
ops.push(entry);
|
|
306
|
+
}
|
|
307
|
+
writeJson(this.activeOpsPath, ops);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
clearActiveOperation(path: string): void {
|
|
311
|
+
const ops = this.loadActiveOperations().filter((o) => o.path !== path);
|
|
312
|
+
writeJson(this.activeOpsPath, ops);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
loadActiveOperations(): ActiveOperation[] {
|
|
316
|
+
return readJson<ActiveOperation[]>(this.activeOpsPath, []);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---- Pending results (delivered on reconnect) ----
|
|
320
|
+
|
|
321
|
+
addPendingResult(result: PendingResult): void {
|
|
322
|
+
const pending = this.loadPendingResults();
|
|
323
|
+
// Replace any existing pending result of the same type for the same path
|
|
324
|
+
const filtered = pending.filter((p) => !(p.path === result.path && p.type === result.type));
|
|
325
|
+
filtered.push(result);
|
|
326
|
+
writeJson(this.pendingResultsPath, filtered);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
loadPendingResults(): PendingResult[] {
|
|
330
|
+
return readJson<PendingResult[]>(this.pendingResultsPath, []);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
clearPendingResults(path?: string): void {
|
|
334
|
+
if (!path) {
|
|
335
|
+
writeJson(this.pendingResultsPath, []);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const filtered = this.loadPendingResults().filter((p) => p.path !== path);
|
|
339
|
+
writeJson(this.pendingResultsPath, filtered);
|
|
340
|
+
}
|
|
341
|
+
|
|
271
342
|
// ---- Full state load ----
|
|
272
343
|
|
|
273
344
|
loadState(): QualityPersistedState {
|
|
@@ -283,7 +354,10 @@ export class QualityPersistence {
|
|
|
283
354
|
}
|
|
284
355
|
}
|
|
285
356
|
|
|
286
|
-
|
|
357
|
+
const activeOperations = this.loadActiveOperations();
|
|
358
|
+
const pendingResults = this.loadPendingResults();
|
|
359
|
+
|
|
360
|
+
return { directories, reports, history, activeOperations, pendingResults };
|
|
287
361
|
}
|
|
288
362
|
}
|
|
289
363
|
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
11
11
|
import { isAbsolute, join } from 'node:path';
|
|
12
|
-
import {
|
|
13
|
-
import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
12
|
+
import { ResilientRunner } from '../../cli/headless/resilient-runner.js';
|
|
14
13
|
import type { ToolUseEvent } from '../../cli/headless/types.js';
|
|
15
14
|
import { loadSkillPrompt } from '../plan/agent-loader.js';
|
|
16
15
|
import type { HandlerContext } from './handler-context.js';
|
|
@@ -40,7 +39,7 @@ export function buildCodeReviewPrompt(dirPath: string, cliFindings?: Array<{ sev
|
|
|
40
39
|
? `\n## CLI Tool Findings (already detected)\n\nThe following issues were found by automated CLI tools (linters, formatters, complexity analyzers). Review these for context — they are already included in the final report. Focus your analysis on DEEPER issues these tools cannot detect.\n\n${cliFindings.slice(0, 50).map((f, i) => `${i + 1}. [${f.severity.toUpperCase()}] ${f.category} — ${f.file}${f.line ? `:${f.line}` : ''} — ${f.title}: ${f.description}`).join('\n')}\n${cliFindings.length > 50 ? `\n...and ${cliFindings.length - 50} more issues from CLI tools.\n` : ''}`
|
|
41
40
|
: '';
|
|
42
41
|
|
|
43
|
-
const fromSkill = loadSkillPrompt('code-review', { dirPath, cliFindingsSection }
|
|
42
|
+
const fromSkill = loadSkillPrompt('code-review', { dirPath, cliFindingsSection });
|
|
44
43
|
if (fromSkill) return fromSkill;
|
|
45
44
|
|
|
46
45
|
// Inline fallback when Skill file is not available (e.g., standalone CLI install)
|
|
@@ -260,7 +259,7 @@ export function buildVerificationPrompt(
|
|
|
260
259
|
evidence: f.evidence || '(none provided)',
|
|
261
260
|
})), null, 2);
|
|
262
261
|
|
|
263
|
-
const fromSkill = loadSkillPrompt('verify-review', { dirPath, findingsJson }
|
|
262
|
+
const fromSkill = loadSkillPrompt('verify-review', { dirPath, findingsJson });
|
|
264
263
|
if (fromSkill) return fromSkill;
|
|
265
264
|
|
|
266
265
|
// Inline fallback
|
|
@@ -372,7 +371,11 @@ type ProgressSender = (message: string) => void;
|
|
|
372
371
|
|
|
373
372
|
function makeProgressSender(ctx: HandlerContext, ws: WSContext, reportPath: string): ProgressSender {
|
|
374
373
|
return (message: string) => {
|
|
375
|
-
|
|
374
|
+
try {
|
|
375
|
+
ctx.send(ws, { type: 'qualityCodeReviewProgress', data: { path: reportPath, message } });
|
|
376
|
+
} catch {
|
|
377
|
+
// WebSocket closed — progress lost but operation continues
|
|
378
|
+
}
|
|
376
379
|
};
|
|
377
380
|
}
|
|
378
381
|
|
|
@@ -384,6 +387,18 @@ function makeToolCallback(send: ProgressSender, prefix?: string): (event: ToolUs
|
|
|
384
387
|
};
|
|
385
388
|
}
|
|
386
389
|
|
|
390
|
+
function startHeartbeat(send: ProgressSender, phase: string, intervalMs = 30_000): () => void {
|
|
391
|
+
let elapsed = 0;
|
|
392
|
+
const timer = setInterval(() => {
|
|
393
|
+
elapsed += intervalMs;
|
|
394
|
+
const mins = Math.floor(elapsed / 60_000);
|
|
395
|
+
const secs = Math.floor((elapsed % 60_000) / 1000);
|
|
396
|
+
const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
397
|
+
send(`${phase} (${timeStr} elapsed, still working...)`);
|
|
398
|
+
}, intervalMs);
|
|
399
|
+
return () => clearInterval(timer);
|
|
400
|
+
}
|
|
401
|
+
|
|
387
402
|
function loadCliFindings(
|
|
388
403
|
getPersistence: (dir: string) => QualityPersistence,
|
|
389
404
|
workingDir: string,
|
|
@@ -404,26 +419,33 @@ async function runVerificationPass(
|
|
|
404
419
|
send: ProgressSender,
|
|
405
420
|
): Promise<CodeReviewFinding[]> {
|
|
406
421
|
send(`Verifying ${findings.length} findings against actual code...`);
|
|
422
|
+
const stopHeartbeat = startHeartbeat(send, 'Verification');
|
|
407
423
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
424
|
+
try {
|
|
425
|
+
const verificationRunner = new ResilientRunner({
|
|
426
|
+
workingDir: dirPath,
|
|
427
|
+
prompt: buildVerificationPrompt(dirPath, findings),
|
|
428
|
+
policy: 'STANDARD',
|
|
429
|
+
stallWarningMs: 300_000,
|
|
430
|
+
stallKillMs: 900_000,
|
|
431
|
+
stallHardCapMs: 1_200_000,
|
|
432
|
+
toolUseCallback: makeToolCallback(send, 'Verifying: '),
|
|
433
|
+
logLabel: 'code-review-verify',
|
|
434
|
+
});
|
|
416
435
|
|
|
417
|
-
|
|
418
|
-
|
|
436
|
+
const verifyResult = await verificationRunner.run();
|
|
437
|
+
const verdicts = parseVerificationResponse(verifyResult.assistantResponse || '');
|
|
419
438
|
|
|
420
|
-
|
|
439
|
+
if (verdicts.length === 0) return findings;
|
|
421
440
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
441
|
+
const { verified, rejected } = applyVerification(findings, verdicts);
|
|
442
|
+
if (rejected.length > 0) {
|
|
443
|
+
send(`Verification rejected ${rejected.length} inaccurate finding(s)`);
|
|
444
|
+
}
|
|
445
|
+
return verified;
|
|
446
|
+
} finally {
|
|
447
|
+
stopHeartbeat();
|
|
425
448
|
}
|
|
426
|
-
return verified;
|
|
427
449
|
}
|
|
428
450
|
|
|
429
451
|
function persistReviewResults(
|
|
@@ -476,17 +498,21 @@ export async function handleCodeReview(
|
|
|
476
498
|
const cliFindings = loadCliFindings(getPersistence, workingDir, reportPath);
|
|
477
499
|
|
|
478
500
|
// ── Pass 1: Initial AI code review ──────────────────────
|
|
479
|
-
const
|
|
501
|
+
const stopReviewHeartbeat = startHeartbeat(send, 'AI code review');
|
|
502
|
+
const runner = new ResilientRunner({
|
|
480
503
|
workingDir: dirPath,
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
504
|
+
prompt: buildCodeReviewPrompt(dirPath, cliFindings),
|
|
505
|
+
policy: 'STANDARD',
|
|
506
|
+
stallWarningMs: 300_000,
|
|
507
|
+
stallKillMs: 1_200_000,
|
|
508
|
+
stallHardCapMs: 1_800_000,
|
|
485
509
|
toolUseCallback: makeToolCallback(send),
|
|
510
|
+
logLabel: 'code-review',
|
|
486
511
|
});
|
|
487
512
|
|
|
488
513
|
send('Claude is analyzing your codebase...');
|
|
489
|
-
const result = await
|
|
514
|
+
const result = await runner.run();
|
|
515
|
+
stopReviewHeartbeat();
|
|
490
516
|
const reviewResult = parseCodeReviewResponse(result.assistantResponse || '');
|
|
491
517
|
|
|
492
518
|
// ── Phase 3: Deterministic post-validation ──────────────
|
|
@@ -517,12 +543,25 @@ export async function handleCodeReview(
|
|
|
517
543
|
// Persistence failure should not break the review flow
|
|
518
544
|
}
|
|
519
545
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
546
|
+
const resultData = { path: reportPath, findings: verifiedReviewResult.findings, summary: verifiedReviewResult.summary, results: updatedResults };
|
|
547
|
+
try {
|
|
548
|
+
ctx.send(ws, { type: 'qualityCodeReview', data: resultData });
|
|
549
|
+
} catch {
|
|
550
|
+
// WebSocket closed — save as pending for delivery on reconnect
|
|
551
|
+
const persistence = getPersistence(workingDir);
|
|
552
|
+
persistence.addPendingResult({
|
|
553
|
+
type: 'codeReview',
|
|
554
|
+
path: reportPath,
|
|
555
|
+
data: resultData as unknown as Record<string, unknown>,
|
|
556
|
+
completedAt: new Date().toISOString(),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
524
559
|
} catch (error) {
|
|
525
|
-
|
|
560
|
+
try {
|
|
561
|
+
ctx.send(ws, { type: 'qualityError', data: { path: reportPath, error: error instanceof Error ? error.message : String(error) } });
|
|
562
|
+
} catch {
|
|
563
|
+
// WebSocket closed — error lost but operation tracked via activeOps
|
|
564
|
+
}
|
|
526
565
|
} finally {
|
|
527
566
|
activeReviews.delete(dirPath);
|
|
528
567
|
}
|
|
@@ -98,6 +98,20 @@ export async function detectTools(dirPath: string): Promise<{ tools: QualityTool
|
|
|
98
98
|
return { tools, ecosystem: ecosystems };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
async function tryInstallCommands(tool: QualityTool, dirPath: string): Promise<string | null> {
|
|
102
|
+
if (tool.installCommand.startsWith('(')) return null;
|
|
103
|
+
const commands = tool.installCommand.split(' || ');
|
|
104
|
+
let lastStderr = '';
|
|
105
|
+
for (const cmd of commands) {
|
|
106
|
+
const parts = cmd.trim().split(' ');
|
|
107
|
+
const result = await runCommand(parts[0], parts.slice(1), dirPath);
|
|
108
|
+
if (result.exitCode === 0) return null;
|
|
109
|
+
lastStderr = result.stderr;
|
|
110
|
+
}
|
|
111
|
+
const detail = lastStderr ? ` (${lastStderr.trim().split('\n').pop()})` : '';
|
|
112
|
+
return `${tool.name}: install failed${detail}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
export async function installTools(
|
|
102
116
|
dirPath: string,
|
|
103
117
|
toolNames?: string[],
|
|
@@ -107,20 +121,8 @@ export async function installTools(
|
|
|
107
121
|
|
|
108
122
|
const failures: string[] = [];
|
|
109
123
|
for (const tool of toInstall) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let installed = false;
|
|
113
|
-
let lastStderr = '';
|
|
114
|
-
for (const cmd of commands) {
|
|
115
|
-
const parts = cmd.trim().split(' ');
|
|
116
|
-
const result = await runCommand(parts[0], parts.slice(1), dirPath);
|
|
117
|
-
if (result.exitCode === 0) { installed = true; break; }
|
|
118
|
-
lastStderr = result.stderr;
|
|
119
|
-
}
|
|
120
|
-
if (!installed) {
|
|
121
|
-
const detail = lastStderr ? ` (${lastStderr.trim().split('\n').pop()})` : '';
|
|
122
|
-
failures.push(`${tool.name}: install failed${detail}`);
|
|
123
|
-
}
|
|
124
|
+
const failure = await tryInstallCommands(tool, dirPath);
|
|
125
|
+
if (failure) failures.push(failure);
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
const detected = await detectTools(dirPath);
|