mstro-app 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +18 -9
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/headless-logger.d.ts +10 -0
- package/dist/server/cli/headless/headless-logger.d.ts.map +1 -0
- package/dist/server/cli/headless/headless-logger.js +66 -0
- package/dist/server/cli/headless/headless-logger.js.map +1 -0
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +6 -5
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +4 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +21 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +70 -19
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +0 -12
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +22 -9
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +8 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +16 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +94 -11
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.d.ts +3 -0
- package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-cli.js +54 -0
- package/dist/server/mcp/bouncer-cli.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts +4 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -0
- package/dist/server/services/plan/composer.js +181 -0
- package/dist/server/services/plan/composer.js.map +1 -0
- package/dist/server/services/plan/dependency-resolver.d.ts +28 -0
- package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -0
- package/dist/server/services/plan/dependency-resolver.js +152 -0
- package/dist/server/services/plan/dependency-resolver.js.map +1 -0
- package/dist/server/services/plan/executor.d.ts +91 -0
- package/dist/server/services/plan/executor.d.ts.map +1 -0
- package/dist/server/services/plan/executor.js +545 -0
- package/dist/server/services/plan/executor.js.map +1 -0
- package/dist/server/services/plan/parser.d.ts +11 -0
- package/dist/server/services/plan/parser.d.ts.map +1 -0
- package/dist/server/services/plan/parser.js +415 -0
- package/dist/server/services/plan/parser.js.map +1 -0
- package/dist/server/services/plan/state-reconciler.d.ts +2 -0
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -0
- package/dist/server/services/plan/state-reconciler.js +105 -0
- package/dist/server/services/plan/state-reconciler.js.map +1 -0
- package/dist/server/services/plan/types.d.ts +120 -0
- package/dist/server/services/plan/types.d.ts.map +1 -0
- package/dist/server/services/plan/types.js +4 -0
- package/dist/server/services/plan/types.js.map +1 -0
- package/dist/server/services/plan/watcher.d.ts +14 -0
- package/dist/server/services/plan/watcher.d.ts.map +1 -0
- package/dist/server/services/plan/watcher.js +69 -0
- package/dist/server/services/plan/watcher.js.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +20 -0
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +21 -0
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts +6 -0
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-handlers.js +494 -0
- package/dist/server/services/websocket/plan-handlers.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +375 -11
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +45 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-persistence.js +187 -0
- package/dist/server/services/websocket/quality-persistence.js.map +1 -0
- package/dist/server/services/websocket/quality-service.d.ts +2 -2
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +62 -12
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +2 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/server/cli/headless/claude-invoker.ts +21 -9
- package/server/cli/headless/headless-logger.ts +78 -0
- package/server/cli/headless/mcp-config.ts +6 -5
- package/server/cli/headless/runner.ts +4 -0
- package/server/cli/headless/stall-assessor.ts +97 -19
- package/server/cli/headless/tool-watchdog.ts +10 -9
- package/server/cli/headless/types.ts +10 -1
- package/server/cli/improvisation-session-manager.ts +118 -11
- package/server/mcp/bouncer-cli.ts +73 -0
- package/server/services/plan/composer.ts +199 -0
- package/server/services/plan/dependency-resolver.ts +179 -0
- package/server/services/plan/executor.ts +604 -0
- package/server/services/plan/parser.ts +459 -0
- package/server/services/plan/state-reconciler.ts +132 -0
- package/server/services/plan/types.ts +164 -0
- package/server/services/plan/watcher.ts +73 -0
- package/server/services/websocket/file-explorer-handlers.ts +20 -0
- package/server/services/websocket/handler.ts +21 -0
- package/server/services/websocket/plan-handlers.ts +592 -0
- package/server/services/websocket/quality-handlers.ts +441 -11
- package/server/services/websocket/quality-persistence.ts +250 -0
- package/server/services/websocket/quality-service.ts +65 -12
- package/server/services/websocket/types.ts +48 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Quality Persistence — Persists quality config, reports, and history
|
|
6
|
+
* to .mstro/quality/ in the working directory.
|
|
7
|
+
*
|
|
8
|
+
* Files:
|
|
9
|
+
* .mstro/quality/config.json — Directory list (paths + labels)
|
|
10
|
+
* .mstro/quality/reports/<slug>.json — Latest full report per directory
|
|
11
|
+
* .mstro/quality/history.json — Score history entries for trend tracking
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import type { QualityResults } from './quality-service.js';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export interface QualityDirectoryConfig {
|
|
23
|
+
path: string;
|
|
24
|
+
label: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface QualityConfig {
|
|
28
|
+
directories: QualityDirectoryConfig[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface HistoryDirectoryEntry {
|
|
32
|
+
path: string;
|
|
33
|
+
score: number;
|
|
34
|
+
grade: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface QualityHistoryEntry {
|
|
38
|
+
timestamp: string;
|
|
39
|
+
overall: number;
|
|
40
|
+
grade: string;
|
|
41
|
+
directories: HistoryDirectoryEntry[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface QualityHistory {
|
|
45
|
+
entries: QualityHistoryEntry[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface QualityPersistedState {
|
|
49
|
+
directories: QualityDirectoryConfig[];
|
|
50
|
+
reports: Record<string, QualityResults>;
|
|
51
|
+
history: QualityHistoryEntry[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Helpers
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
const MAX_HISTORY_ENTRIES = 100;
|
|
59
|
+
|
|
60
|
+
function slugify(dirPath: string): string {
|
|
61
|
+
if (dirPath === '.' || dirPath === './') return '_root';
|
|
62
|
+
return dirPath.replace(/[/\\]/g, '_').replace(/^_+|_+$/g, '') || '_root';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ensureDir(dir: string): void {
|
|
66
|
+
if (!existsSync(dir)) {
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readJson<T>(filePath: string, fallback: T): T {
|
|
72
|
+
try {
|
|
73
|
+
if (existsSync(filePath)) {
|
|
74
|
+
return JSON.parse(readFileSync(filePath, 'utf-8')) as T;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Corrupted or unreadable — return fallback
|
|
78
|
+
}
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeJson(filePath: string, data: unknown): void {
|
|
83
|
+
try {
|
|
84
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('[QualityPersistence] Error writing:', filePath, error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Quality Persistence
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export class QualityPersistence {
|
|
95
|
+
private qualityDir: string;
|
|
96
|
+
private reportsDir: string;
|
|
97
|
+
private configPath: string;
|
|
98
|
+
private historyPath: string;
|
|
99
|
+
|
|
100
|
+
constructor(workingDir: string) {
|
|
101
|
+
this.qualityDir = join(workingDir, '.mstro', 'quality');
|
|
102
|
+
this.reportsDir = join(this.qualityDir, 'reports');
|
|
103
|
+
this.configPath = join(this.qualityDir, 'config.json');
|
|
104
|
+
this.historyPath = join(this.qualityDir, 'history.json');
|
|
105
|
+
ensureDir(this.reportsDir);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- Config (directory list) ----
|
|
109
|
+
|
|
110
|
+
loadConfig(): QualityDirectoryConfig[] {
|
|
111
|
+
const config = readJson<QualityConfig>(this.configPath, { directories: [] });
|
|
112
|
+
return config.directories;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
saveConfig(directories: QualityDirectoryConfig[]): void {
|
|
116
|
+
writeJson(this.configPath, { directories });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addDirectory(path: string, label: string): void {
|
|
120
|
+
const dirs = this.loadConfig();
|
|
121
|
+
if (!dirs.some((d) => d.path === path)) {
|
|
122
|
+
dirs.push({ path, label });
|
|
123
|
+
this.saveConfig(dirs);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
removeDirectory(path: string): void {
|
|
128
|
+
const dirs = this.loadConfig().filter((d) => d.path !== path);
|
|
129
|
+
this.saveConfig(dirs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- Reports (latest per directory) ----
|
|
133
|
+
|
|
134
|
+
loadReport(dirPath: string): QualityResults | null {
|
|
135
|
+
const slug = slugify(dirPath);
|
|
136
|
+
const reportPath = join(this.reportsDir, `${slug}.json`);
|
|
137
|
+
return readJson<QualityResults | null>(reportPath, null);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
saveReport(dirPath: string, results: QualityResults): void {
|
|
141
|
+
const slug = slugify(dirPath);
|
|
142
|
+
const reportPath = join(this.reportsDir, `${slug}.json`);
|
|
143
|
+
writeJson(reportPath, results);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
loadAllReports(directories: QualityDirectoryConfig[]): Record<string, QualityResults> {
|
|
147
|
+
const reports: Record<string, QualityResults> = {};
|
|
148
|
+
for (const dir of directories) {
|
|
149
|
+
const report = this.loadReport(dir.path);
|
|
150
|
+
if (report) {
|
|
151
|
+
reports[dir.path] = report;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return reports;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- History (trend tracking) ----
|
|
158
|
+
|
|
159
|
+
loadHistory(): QualityHistoryEntry[] {
|
|
160
|
+
const history = readJson<QualityHistory>(this.historyPath, { entries: [] });
|
|
161
|
+
return history.entries;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
appendHistory(results: QualityResults, dirPath: string): void {
|
|
165
|
+
const history = this.loadHistory();
|
|
166
|
+
|
|
167
|
+
// Find or create entry for this timestamp batch
|
|
168
|
+
// If the last entry was within 60 seconds, merge into it (for multi-dir scans)
|
|
169
|
+
const now = new Date();
|
|
170
|
+
const lastEntry = history[history.length - 1];
|
|
171
|
+
const lastTime = lastEntry ? new Date(lastEntry.timestamp).getTime() : 0;
|
|
172
|
+
const mergeWindow = 60_000; // 60 seconds
|
|
173
|
+
|
|
174
|
+
const dirEntry: HistoryDirectoryEntry = {
|
|
175
|
+
path: dirPath,
|
|
176
|
+
score: results.overall,
|
|
177
|
+
grade: results.grade,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (lastEntry && now.getTime() - lastTime < mergeWindow) {
|
|
181
|
+
// Merge: update or add this directory in the last entry
|
|
182
|
+
const existing = lastEntry.directories.findIndex((d) => d.path === dirPath);
|
|
183
|
+
if (existing >= 0) {
|
|
184
|
+
lastEntry.directories[existing] = dirEntry;
|
|
185
|
+
} else {
|
|
186
|
+
lastEntry.directories.push(dirEntry);
|
|
187
|
+
}
|
|
188
|
+
// Recompute overall as average of all directories in this entry
|
|
189
|
+
const totalScore = lastEntry.directories.reduce((sum, d) => sum + d.score, 0);
|
|
190
|
+
lastEntry.overall = Math.round(totalScore / lastEntry.directories.length);
|
|
191
|
+
lastEntry.grade = gradeFromScore(lastEntry.overall);
|
|
192
|
+
lastEntry.timestamp = now.toISOString();
|
|
193
|
+
} else {
|
|
194
|
+
// New entry
|
|
195
|
+
history.push({
|
|
196
|
+
timestamp: now.toISOString(),
|
|
197
|
+
overall: results.overall,
|
|
198
|
+
grade: results.grade,
|
|
199
|
+
directories: [dirEntry],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Trim to max entries
|
|
204
|
+
while (history.length > MAX_HISTORY_ENTRIES) {
|
|
205
|
+
history.shift();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
writeJson(this.historyPath, { entries: history });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- Code Review (persisted per directory) ----
|
|
212
|
+
|
|
213
|
+
loadCodeReview(dirPath: string): { findings: Record<string, unknown>[]; summary: string; timestamp: string } | null {
|
|
214
|
+
const slug = slugify(dirPath);
|
|
215
|
+
const reviewPath = join(this.reportsDir, `${slug}-review.json`);
|
|
216
|
+
return readJson<{ findings: Record<string, unknown>[]; summary: string; timestamp: string } | null>(reviewPath, null);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
saveCodeReview(dirPath: string, findings: Record<string, unknown>[], summary: string): void {
|
|
220
|
+
const slug = slugify(dirPath);
|
|
221
|
+
const reviewPath = join(this.reportsDir, `${slug}-review.json`);
|
|
222
|
+
writeJson(reviewPath, { findings, summary, timestamp: new Date().toISOString() });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---- Full state load ----
|
|
226
|
+
|
|
227
|
+
loadState(): QualityPersistedState {
|
|
228
|
+
const directories = this.loadConfig();
|
|
229
|
+
const reports = this.loadAllReports(directories);
|
|
230
|
+
const history = this.loadHistory();
|
|
231
|
+
|
|
232
|
+
// Merge persisted code reviews into reports
|
|
233
|
+
for (const dir of directories) {
|
|
234
|
+
const review = this.loadCodeReview(dir.path);
|
|
235
|
+
if (review && reports[dir.path]) {
|
|
236
|
+
reports[dir.path] = { ...reports[dir.path], codeReview: review.findings as unknown as QualityResults['codeReview'] };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { directories, reports, history };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function gradeFromScore(score: number): string {
|
|
245
|
+
if (score >= 90) return 'A';
|
|
246
|
+
if (score >= 80) return 'B';
|
|
247
|
+
if (score >= 70) return 'C';
|
|
248
|
+
if (score >= 60) return 'D';
|
|
249
|
+
return 'F';
|
|
250
|
+
}
|
|
@@ -54,7 +54,7 @@ export interface ScanProgress {
|
|
|
54
54
|
total: number;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'unknown';
|
|
57
|
+
type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'swift' | 'kotlin' | 'unknown';
|
|
58
58
|
|
|
59
59
|
interface ToolSpec {
|
|
60
60
|
name: string;
|
|
@@ -75,9 +75,9 @@ const ECOSYSTEM_TOOLS: Record<Ecosystem, ToolSpec[]> = {
|
|
|
75
75
|
{ name: 'typescript', check: ['npx', 'tsc', '--version'], category: 'general', installCmd: 'npm install -D typescript' },
|
|
76
76
|
],
|
|
77
77
|
python: [
|
|
78
|
-
{ name: 'ruff', check: ['ruff', '--version'], category: 'linter', installCmd: 'pip install ruff' },
|
|
79
|
-
{ name: 'black', check: ['black', '--version'], category: 'formatter', installCmd: 'pip install black' },
|
|
80
|
-
{ name: 'radon', check: ['radon', '--version'], category: 'complexity', installCmd: 'pip install radon' },
|
|
78
|
+
{ name: 'ruff', check: ['ruff', '--version'], category: 'linter', installCmd: 'uv tool install ruff || pip install ruff' },
|
|
79
|
+
{ name: 'black', check: ['black', '--version'], category: 'formatter', installCmd: 'uv tool install black || pip install black' },
|
|
80
|
+
{ name: 'radon', check: ['radon', '--version'], category: 'complexity', installCmd: 'uv tool install radon || pip install radon' },
|
|
81
81
|
],
|
|
82
82
|
rust: [
|
|
83
83
|
{ name: 'clippy', check: ['cargo', 'clippy', '--version'], category: 'linter', installCmd: 'rustup component add clippy' },
|
|
@@ -87,6 +87,14 @@ const ECOSYSTEM_TOOLS: Record<Ecosystem, ToolSpec[]> = {
|
|
|
87
87
|
{ name: 'golangci-lint', check: ['golangci-lint', '--version'], category: 'linter', installCmd: 'go install github.com/golangci-lint/golangci-lint/cmd/golangci-lint@latest' },
|
|
88
88
|
{ name: 'gofmt', check: ['gofmt', '-h'], category: 'formatter', installCmd: '(built-in with Go)' },
|
|
89
89
|
],
|
|
90
|
+
swift: [
|
|
91
|
+
{ name: 'swiftlint', check: ['swiftlint', '--version'], category: 'linter', installCmd: 'brew install swiftlint' },
|
|
92
|
+
{ name: 'swiftformat', check: ['swiftformat', '--version'], category: 'formatter', installCmd: 'brew install swiftformat' },
|
|
93
|
+
],
|
|
94
|
+
kotlin: [
|
|
95
|
+
{ name: 'ktlint', check: ['ktlint', '--version'], category: 'linter', installCmd: 'brew install ktlint' },
|
|
96
|
+
{ name: 'ktfmt', check: ['ktfmt', '--version'], category: 'formatter', installCmd: 'brew install ktfmt' },
|
|
97
|
+
],
|
|
90
98
|
unknown: [],
|
|
91
99
|
};
|
|
92
100
|
|
|
@@ -113,6 +121,20 @@ const FILE_LENGTH_THRESHOLD = 300;
|
|
|
113
121
|
const FUNCTION_LENGTH_THRESHOLD = 50;
|
|
114
122
|
const TOTAL_STEPS = 7;
|
|
115
123
|
|
|
124
|
+
function hasInstalledToolInCategory(
|
|
125
|
+
installedSet: Set<string>,
|
|
126
|
+
ecosystems: Ecosystem[],
|
|
127
|
+
category: QualityTool['category'],
|
|
128
|
+
): boolean {
|
|
129
|
+
for (const eco of ecosystems) {
|
|
130
|
+
const specs = ECOSYSTEM_TOOLS[eco] || [];
|
|
131
|
+
for (const spec of specs) {
|
|
132
|
+
if (spec.category === category && installedSet.has(spec.name)) return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
116
138
|
// ============================================================================
|
|
117
139
|
// Ecosystem Detection
|
|
118
140
|
// ============================================================================
|
|
@@ -125,6 +147,8 @@ export function detectEcosystem(dirPath: string): Ecosystem[] {
|
|
|
125
147
|
if (files.includes('pyproject.toml') || files.includes('setup.py') || files.includes('requirements.txt')) ecosystems.push('python');
|
|
126
148
|
if (files.includes('Cargo.toml')) ecosystems.push('rust');
|
|
127
149
|
if (files.includes('go.mod')) ecosystems.push('go');
|
|
150
|
+
if (files.includes('Package.swift') || files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) ecosystems.push('swift');
|
|
151
|
+
if (files.includes('build.gradle') || files.includes('build.gradle.kts')) ecosystems.push('kotlin');
|
|
128
152
|
} catch {
|
|
129
153
|
// Directory not readable
|
|
130
154
|
}
|
|
@@ -179,14 +203,33 @@ export async function installTools(
|
|
|
179
203
|
const { tools } = await detectTools(dirPath);
|
|
180
204
|
const toInstall = tools.filter((t) => !t.installed && (!toolNames || toolNames.includes(t.name)));
|
|
181
205
|
|
|
206
|
+
const failures: string[] = [];
|
|
182
207
|
for (const tool of toInstall) {
|
|
183
208
|
if (tool.installCommand.startsWith('(')) continue; // built-in, skip
|
|
184
|
-
|
|
185
|
-
|
|
209
|
+
// Support chained commands with || (try first, fallback to second)
|
|
210
|
+
const commands = tool.installCommand.split(' || ');
|
|
211
|
+
let installed = false;
|
|
212
|
+
for (const cmd of commands) {
|
|
213
|
+
const parts = cmd.trim().split(' ');
|
|
214
|
+
const result = await runCommand(parts[0], parts.slice(1), dirPath);
|
|
215
|
+
if (result.exitCode === 0) { installed = true; break; }
|
|
216
|
+
}
|
|
217
|
+
if (!installed) {
|
|
218
|
+
failures.push(`${tool.name}: all install methods failed`);
|
|
219
|
+
}
|
|
186
220
|
}
|
|
187
221
|
|
|
188
222
|
// Re-detect after install
|
|
189
|
-
|
|
223
|
+
const detected = await detectTools(dirPath);
|
|
224
|
+
|
|
225
|
+
if (failures.length > 0) {
|
|
226
|
+
const stillMissing = detected.tools.filter((t) => !t.installed).map((t) => t.name);
|
|
227
|
+
if (stillMissing.length > 0) {
|
|
228
|
+
throw new Error(`Failed to install: ${stillMissing.join(', ')}. ${failures.join('; ')}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return detected;
|
|
190
233
|
}
|
|
191
234
|
|
|
192
235
|
// ============================================================================
|
|
@@ -371,7 +414,7 @@ async function lintNode(dirPath: string, acc: LintAccumulator): Promise<void> {
|
|
|
371
414
|
|
|
372
415
|
async function lintPython(dirPath: string, acc: LintAccumulator): Promise<void> {
|
|
373
416
|
const result = await runCommand('ruff', ['check', '--output-format=json', '.'], dirPath);
|
|
374
|
-
if (result.exitCode
|
|
417
|
+
if (result.exitCode !== 0 && !result.stdout.trim().startsWith('[')) return;
|
|
375
418
|
|
|
376
419
|
acc.ran = true;
|
|
377
420
|
try {
|
|
@@ -823,9 +866,13 @@ export type ProgressCallback = (progress: ScanProgress) => void;
|
|
|
823
866
|
export async function runQualityScan(
|
|
824
867
|
dirPath: string,
|
|
825
868
|
onProgress?: ProgressCallback,
|
|
869
|
+
installedToolNames?: string[],
|
|
826
870
|
): Promise<QualityResults> {
|
|
827
871
|
const ecosystems = detectEcosystem(dirPath);
|
|
828
872
|
|
|
873
|
+
// Build set of installed tools for gating analyses
|
|
874
|
+
const installedSet = installedToolNames ? new Set(installedToolNames) : null;
|
|
875
|
+
|
|
829
876
|
const progress = (step: string, current: number) => {
|
|
830
877
|
onProgress?.({ step, current, total: TOTAL_STEPS });
|
|
831
878
|
};
|
|
@@ -834,13 +881,19 @@ export async function runQualityScan(
|
|
|
834
881
|
progress('Collecting source files', 1);
|
|
835
882
|
const files = collectSourceFiles(dirPath, dirPath);
|
|
836
883
|
|
|
837
|
-
// Step 2: Run linting
|
|
884
|
+
// Step 2: Run linting (only if a linter is installed)
|
|
838
885
|
progress('Running linters', 2);
|
|
839
|
-
const
|
|
886
|
+
const hasLinter = !installedSet || hasInstalledToolInCategory(installedSet, ecosystems, 'linter');
|
|
887
|
+
const lintResult = hasLinter
|
|
888
|
+
? await analyzeLinting(dirPath, ecosystems, files)
|
|
889
|
+
: { score: 0, findings: [], available: false, issueCount: 0 };
|
|
840
890
|
|
|
841
|
-
// Step 3: Check formatting
|
|
891
|
+
// Step 3: Check formatting (only if a formatter is installed)
|
|
842
892
|
progress('Checking formatting', 3);
|
|
843
|
-
const
|
|
893
|
+
const hasFormatter = !installedSet || hasInstalledToolInCategory(installedSet, ecosystems, 'formatter');
|
|
894
|
+
const fmtResult = hasFormatter
|
|
895
|
+
? await analyzeFormatting(dirPath, ecosystems, files)
|
|
896
|
+
: { score: 0, available: false, issueCount: 0 };
|
|
844
897
|
|
|
845
898
|
// Step 4: Analyze complexity
|
|
846
899
|
progress('Analyzing complexity', 4);
|
|
@@ -112,11 +112,31 @@ export interface WebSocketMessage {
|
|
|
112
112
|
| 'qualityScan'
|
|
113
113
|
| 'qualityInstallTools'
|
|
114
114
|
| 'qualityCodeReview'
|
|
115
|
+
| 'qualityFixIssues'
|
|
116
|
+
| 'qualityLoadState'
|
|
117
|
+
| 'qualitySaveDirectories'
|
|
115
118
|
// File upload message types (chunked remote uploads)
|
|
116
119
|
| 'fileUploadStart'
|
|
117
120
|
| 'fileUploadChunk'
|
|
118
121
|
| 'fileUploadComplete'
|
|
119
|
-
| 'fileUploadCancel'
|
|
122
|
+
| 'fileUploadCancel'
|
|
123
|
+
// Plan message types
|
|
124
|
+
| 'planInit'
|
|
125
|
+
| 'planGetState'
|
|
126
|
+
| 'planListIssues'
|
|
127
|
+
| 'planGetIssue'
|
|
128
|
+
| 'planGetSprint'
|
|
129
|
+
| 'planGetMilestone'
|
|
130
|
+
| 'planCreateIssue'
|
|
131
|
+
| 'planUpdateIssue'
|
|
132
|
+
| 'planDeleteIssue'
|
|
133
|
+
| 'planScaffold'
|
|
134
|
+
| 'planPrompt'
|
|
135
|
+
| 'planExecute'
|
|
136
|
+
| 'planExecuteEpic'
|
|
137
|
+
| 'planPause'
|
|
138
|
+
| 'planStop'
|
|
139
|
+
| 'planResume';
|
|
120
140
|
tabId?: string;
|
|
121
141
|
terminalId?: string;
|
|
122
142
|
// biome-ignore lint/suspicious/noExplicitAny: message envelope carries heterogeneous payloads
|
|
@@ -231,12 +251,38 @@ export interface WebSocketResponse {
|
|
|
231
251
|
| 'qualityInstallProgress'
|
|
232
252
|
| 'qualityInstallComplete'
|
|
233
253
|
| 'qualityCodeReview'
|
|
254
|
+
| 'qualityCodeReviewProgress'
|
|
234
255
|
| 'qualityPostSession'
|
|
256
|
+
| 'qualityFixProgress'
|
|
257
|
+
| 'qualityFixComplete'
|
|
235
258
|
| 'qualityError'
|
|
259
|
+
| 'qualityStateLoaded'
|
|
236
260
|
// File upload response types
|
|
237
261
|
| 'fileUploadAck'
|
|
238
262
|
| 'fileUploadReady'
|
|
239
|
-
| 'fileUploadError'
|
|
263
|
+
| 'fileUploadError'
|
|
264
|
+
// Plan response types
|
|
265
|
+
| 'planState'
|
|
266
|
+
| 'planIssueList'
|
|
267
|
+
| 'planIssue'
|
|
268
|
+
| 'planSprint'
|
|
269
|
+
| 'planMilestone'
|
|
270
|
+
| 'planNotFound'
|
|
271
|
+
| 'planStateUpdated'
|
|
272
|
+
| 'planIssueUpdated'
|
|
273
|
+
| 'planIssueCreated'
|
|
274
|
+
| 'planIssueDeleted'
|
|
275
|
+
| 'planScaffolded'
|
|
276
|
+
| 'planPromptStreaming'
|
|
277
|
+
| 'planPromptProgress'
|
|
278
|
+
| 'planPromptResponse'
|
|
279
|
+
| 'planExecutionStarted'
|
|
280
|
+
| 'planExecutionProgress'
|
|
281
|
+
| 'planExecutionOutput'
|
|
282
|
+
| 'planExecutionMetrics'
|
|
283
|
+
| 'planExecutionComplete'
|
|
284
|
+
| 'planExecutionError'
|
|
285
|
+
| 'planError';
|
|
240
286
|
tabId?: string;
|
|
241
287
|
terminalId?: string;
|
|
242
288
|
// biome-ignore lint/suspicious/noExplicitAny: message envelope carries heterogeneous payloads
|