mstro-app 0.4.12 → 0.4.15
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/index.js +11 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/services/file-explorer-ops.d.ts +1 -1
- package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
- package/dist/server/services/file-explorer-ops.js +7 -2
- package/dist/server/services/file-explorer-ops.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +3 -2
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +5 -0
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +32 -1
- package/dist/server/services/plan/executor.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 +2 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +25 -3
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +2 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js +2 -1
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.js +29 -9
- package/dist/server/services/websocket/git-log-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +8 -0
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +5 -3
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +4 -1
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.js +1 -1
- package/dist/server/services/websocket/plan-helpers.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +67 -14
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +2 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +33 -2
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts +33 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +360 -72
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.js +1 -1
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +7 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +43 -3
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +5 -1
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +12 -4
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/package.json +1 -1
- package/server/index.ts +11 -0
- package/server/services/file-explorer-ops.ts +7 -2
- package/server/services/plan/composer.ts +3 -1
- package/server/services/plan/executor.ts +32 -1
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/review-gate.ts +28 -3
- package/server/services/plan/types.ts +2 -0
- package/server/services/websocket/file-explorer-handlers.ts +2 -1
- package/server/services/websocket/git-log-handlers.ts +30 -9
- package/server/services/websocket/git-worktree-handlers.ts +9 -0
- package/server/services/websocket/handler.ts +6 -3
- package/server/services/websocket/plan-execution-handlers.ts +4 -1
- package/server/services/websocket/plan-helpers.ts +1 -1
- package/server/services/websocket/quality-handlers.ts +69 -9
- package/server/services/websocket/quality-persistence.ts +32 -2
- package/server/services/websocket/quality-review-agent.ts +427 -72
- package/server/services/websocket/quality-service.ts +1 -1
- package/server/services/websocket/quality-tools.ts +48 -3
- package/server/services/websocket/quality-types.ts +15 -4
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
import { validatePathWithinWorkingDir } from '../pathUtils.js';
|
|
14
15
|
import type { HandlerContext } from './handler-context.js';
|
|
15
16
|
import type { FindingForFix } from './quality-fix-agent.js';
|
|
16
17
|
import { handleFixIssues } from './quality-fix-agent.js';
|
|
@@ -39,6 +40,26 @@ function resolvePath(workingDir: string, dirPath?: string): string {
|
|
|
39
40
|
return join(workingDir, dirPath);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Resolve and validate a directory path for sandboxed users.
|
|
45
|
+
* Returns null if the path escapes the working directory.
|
|
46
|
+
*/
|
|
47
|
+
function resolveAndValidatePath(
|
|
48
|
+
workingDir: string,
|
|
49
|
+
dirPath: string | undefined,
|
|
50
|
+
isSandboxed: boolean,
|
|
51
|
+
): { resolved: string; error?: string } {
|
|
52
|
+
const resolved = resolvePath(workingDir, dirPath);
|
|
53
|
+
if (isSandboxed) {
|
|
54
|
+
const validation = validatePathWithinWorkingDir(resolved, workingDir);
|
|
55
|
+
if (!validation.valid) {
|
|
56
|
+
return { resolved: '', error: validation.error || 'Path outside project directory' };
|
|
57
|
+
}
|
|
58
|
+
return { resolved: validation.resolvedPath };
|
|
59
|
+
}
|
|
60
|
+
return { resolved };
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
// ── Message router ────────────────────────────────────────────
|
|
43
64
|
|
|
44
65
|
export function handleQualityMessage(
|
|
@@ -47,25 +68,33 @@ export function handleQualityMessage(
|
|
|
47
68
|
msg: WebSocketMessage,
|
|
48
69
|
_tabId: string,
|
|
49
70
|
workingDir: string,
|
|
71
|
+
permission?: 'control' | 'view',
|
|
50
72
|
): void {
|
|
73
|
+
const isSandboxed = permission === 'control' || permission === 'view';
|
|
74
|
+
const sendPathError = (path: string, error: string) => {
|
|
75
|
+
ctx.send(ws, { type: 'qualityError', data: { path, error } });
|
|
76
|
+
};
|
|
77
|
+
|
|
51
78
|
const handlers: Record<string, () => void> = {
|
|
52
|
-
qualityDetectTools: () => handleDetectTools(ctx, ws, msg, workingDir),
|
|
53
|
-
qualityScan: () => handleScan(ctx, ws, msg, workingDir),
|
|
54
|
-
qualityInstallTools: () => handleInstallTools(ctx, ws, msg, workingDir),
|
|
79
|
+
qualityDetectTools: () => handleDetectTools(ctx, ws, msg, workingDir, isSandboxed),
|
|
80
|
+
qualityScan: () => handleScan(ctx, ws, msg, workingDir, isSandboxed),
|
|
81
|
+
qualityInstallTools: () => handleInstallTools(ctx, ws, msg, workingDir, isSandboxed),
|
|
55
82
|
qualityCodeReview: () => {
|
|
56
|
-
const dirPath =
|
|
83
|
+
const { resolved: dirPath, error } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
|
|
84
|
+
if (error) { sendPathError(msg.data?.path || '.', error); return; }
|
|
57
85
|
const reportPath = msg.data?.path || '.';
|
|
58
86
|
handleCodeReview(ctx, ws, reportPath, dirPath, workingDir, activeReviews, getPersistence);
|
|
59
87
|
},
|
|
60
88
|
qualityFixIssues: () => {
|
|
61
|
-
const dirPath =
|
|
89
|
+
const { resolved: dirPath, error } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
|
|
90
|
+
if (error) { sendPathError(msg.data?.path || '.', error); return; }
|
|
62
91
|
const reportPath = msg.data?.path || '.';
|
|
63
92
|
const section: string | undefined = msg.data?.section;
|
|
64
93
|
const findings: FindingForFix[] = msg.data?.findings || [];
|
|
65
94
|
handleFixIssues(ctx, ws, reportPath, dirPath, workingDir, section, findings, getPersistence);
|
|
66
95
|
},
|
|
67
96
|
qualityLoadState: () => handleLoadState(ctx, ws, workingDir),
|
|
68
|
-
qualitySaveDirectories: () => handleSaveDirectories(ctx, ws, msg, workingDir),
|
|
97
|
+
qualitySaveDirectories: () => handleSaveDirectories(ctx, ws, msg, workingDir, isSandboxed),
|
|
69
98
|
};
|
|
70
99
|
|
|
71
100
|
const handler = handlers[msg.type];
|
|
@@ -106,10 +135,26 @@ async function handleSaveDirectories(
|
|
|
106
135
|
ws: WSContext,
|
|
107
136
|
msg: WebSocketMessage,
|
|
108
137
|
workingDir: string,
|
|
138
|
+
isSandboxed = false,
|
|
109
139
|
): Promise<void> {
|
|
110
140
|
try {
|
|
111
141
|
const persistence = getPersistence(workingDir);
|
|
112
142
|
const directories: Array<{ path: string; label: string }> = msg.data?.directories || [];
|
|
143
|
+
|
|
144
|
+
// Validate all directory paths when sandboxed
|
|
145
|
+
if (isSandboxed) {
|
|
146
|
+
for (const dir of directories) {
|
|
147
|
+
const { error } = resolveAndValidatePath(workingDir, dir.path, true);
|
|
148
|
+
if (error) {
|
|
149
|
+
ctx.send(ws, {
|
|
150
|
+
type: 'qualityError',
|
|
151
|
+
data: { path: dir.path, error: `Cannot save directory: ${error}` },
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
113
158
|
persistence.saveConfig(directories);
|
|
114
159
|
} catch (error) {
|
|
115
160
|
ctx.send(ws, {
|
|
@@ -124,8 +169,13 @@ async function handleDetectTools(
|
|
|
124
169
|
ws: WSContext,
|
|
125
170
|
msg: WebSocketMessage,
|
|
126
171
|
workingDir: string,
|
|
172
|
+
isSandboxed = false,
|
|
127
173
|
): Promise<void> {
|
|
128
|
-
const dirPath =
|
|
174
|
+
const { resolved: dirPath, error: pathError } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
|
|
175
|
+
if (pathError) {
|
|
176
|
+
ctx.send(ws, { type: 'qualityError', data: { path: msg.data?.path || '.', error: pathError } });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
129
179
|
try {
|
|
130
180
|
const { tools, ecosystem } = await detectTools(dirPath);
|
|
131
181
|
ctx.send(ws, {
|
|
@@ -145,8 +195,13 @@ async function handleScan(
|
|
|
145
195
|
ws: WSContext,
|
|
146
196
|
msg: WebSocketMessage,
|
|
147
197
|
workingDir: string,
|
|
198
|
+
isSandboxed = false,
|
|
148
199
|
): Promise<void> {
|
|
149
|
-
const dirPath =
|
|
200
|
+
const { resolved: dirPath, error: pathError } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
|
|
201
|
+
if (pathError) {
|
|
202
|
+
ctx.send(ws, { type: 'qualityError', data: { path: msg.data?.path || '.', error: pathError } });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
150
205
|
const reportPath = msg.data?.path || '.';
|
|
151
206
|
|
|
152
207
|
try {
|
|
@@ -184,8 +239,13 @@ async function handleInstallTools(
|
|
|
184
239
|
ws: WSContext,
|
|
185
240
|
msg: WebSocketMessage,
|
|
186
241
|
workingDir: string,
|
|
242
|
+
isSandboxed = false,
|
|
187
243
|
): Promise<void> {
|
|
188
|
-
const dirPath =
|
|
244
|
+
const { resolved: dirPath, error: pathError } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
|
|
245
|
+
if (pathError) {
|
|
246
|
+
ctx.send(ws, { type: 'qualityError', data: { path: msg.data?.path || '.', error: pathError } });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
189
249
|
const reportPath = msg.data?.path || '.';
|
|
190
250
|
const toolNames: string[] | undefined = msg.data?.tools;
|
|
191
251
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* .mstro/quality/history.json — Score history entries for trend tracking
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import type { QualityResults } from './quality-service.js';
|
|
17
17
|
|
|
@@ -56,6 +56,7 @@ export interface QualityPersistedState {
|
|
|
56
56
|
// ============================================================================
|
|
57
57
|
|
|
58
58
|
const MAX_HISTORY_ENTRIES = 100;
|
|
59
|
+
const MAX_REPORT_HISTORY_FILES = 200;
|
|
59
60
|
|
|
60
61
|
function slugify(dirPath: string): string {
|
|
61
62
|
if (dirPath === '.' || dirPath === './') return '_root';
|
|
@@ -94,15 +95,18 @@ function writeJson(filePath: string, data: unknown): void {
|
|
|
94
95
|
export class QualityPersistence {
|
|
95
96
|
private qualityDir: string;
|
|
96
97
|
private reportsDir: string;
|
|
98
|
+
private reportHistoryDir: string;
|
|
97
99
|
private configPath: string;
|
|
98
100
|
private historyPath: string;
|
|
99
101
|
|
|
100
102
|
constructor(workingDir: string) {
|
|
101
103
|
this.qualityDir = join(workingDir, '.mstro', 'quality');
|
|
102
104
|
this.reportsDir = join(this.qualityDir, 'reports');
|
|
105
|
+
this.reportHistoryDir = join(this.reportsDir, 'history');
|
|
103
106
|
this.configPath = join(this.qualityDir, 'config.json');
|
|
104
107
|
this.historyPath = join(this.qualityDir, 'history.json');
|
|
105
108
|
ensureDir(this.reportsDir);
|
|
109
|
+
ensureDir(this.reportHistoryDir);
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
// ---- Config (directory list) ----
|
|
@@ -141,6 +145,12 @@ export class QualityPersistence {
|
|
|
141
145
|
const slug = slugify(dirPath);
|
|
142
146
|
const reportPath = join(this.reportsDir, `${slug}.json`);
|
|
143
147
|
writeJson(reportPath, results);
|
|
148
|
+
|
|
149
|
+
// Archive timestamped copy for historical tracking
|
|
150
|
+
const ts = Date.now();
|
|
151
|
+
const archivePath = join(this.reportHistoryDir, `${ts}_${slug}.json`);
|
|
152
|
+
writeJson(archivePath, results);
|
|
153
|
+
this.pruneReportHistory();
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
loadAllReports(directories: QualityDirectoryConfig[]): Record<string, QualityResults> {
|
|
@@ -154,6 +164,19 @@ export class QualityPersistence {
|
|
|
154
164
|
return reports;
|
|
155
165
|
}
|
|
156
166
|
|
|
167
|
+
// ---- Report history pruning ----
|
|
168
|
+
|
|
169
|
+
private pruneReportHistory(): void {
|
|
170
|
+
try {
|
|
171
|
+
const files = readdirSync(this.reportHistoryDir).filter((f) => f.endsWith('.json')).sort();
|
|
172
|
+
if (files.length <= MAX_REPORT_HISTORY_FILES) return;
|
|
173
|
+
const toRemove = files.slice(0, files.length - MAX_REPORT_HISTORY_FILES);
|
|
174
|
+
for (const file of toRemove) {
|
|
175
|
+
try { unlinkSync(join(this.reportHistoryDir, file)); } catch { /* ignore */ }
|
|
176
|
+
}
|
|
177
|
+
} catch { /* directory may not exist yet */ }
|
|
178
|
+
}
|
|
179
|
+
|
|
157
180
|
// ---- History (trend tracking) ----
|
|
158
181
|
|
|
159
182
|
loadHistory(): QualityHistoryEntry[] {
|
|
@@ -219,7 +242,14 @@ export class QualityPersistence {
|
|
|
219
242
|
saveCodeReview(dirPath: string, findings: Record<string, unknown>[], summary: string): void {
|
|
220
243
|
const slug = slugify(dirPath);
|
|
221
244
|
const reviewPath = join(this.reportsDir, `${slug}-review.json`);
|
|
222
|
-
|
|
245
|
+
const data = { findings, summary, timestamp: new Date().toISOString() };
|
|
246
|
+
writeJson(reviewPath, data);
|
|
247
|
+
|
|
248
|
+
// Archive timestamped copy for historical tracking
|
|
249
|
+
const ts = Date.now();
|
|
250
|
+
const archivePath = join(this.reportHistoryDir, `${ts}_${slug}-review.json`);
|
|
251
|
+
writeJson(archivePath, data);
|
|
252
|
+
this.pruneReportHistory();
|
|
223
253
|
}
|
|
224
254
|
|
|
225
255
|
// ---- Full state load ----
|