@shakecodeslikecray/whiterose 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +135 -0
- package/README.md +578 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4033 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1188 -0
- package/dist/index.js +2794 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,4033 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as p3 from '@clack/prompts';
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, statSync, mkdtempSync, rmSync } from 'fs';
|
|
6
|
+
import { join, isAbsolute, resolve, basename, relative, dirname } from 'path';
|
|
7
|
+
import { execa } from 'execa';
|
|
8
|
+
import { homedir, tmpdir } from 'os';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import fg3 from 'fast-glob';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import YAML from 'yaml';
|
|
13
|
+
import { render, useApp, useInput, Box, Text } from 'ink';
|
|
14
|
+
import { useState, useEffect } from 'react';
|
|
15
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
16
|
+
import Spinner from 'ink-spinner';
|
|
17
|
+
|
|
18
|
+
var providerChecks = [
|
|
19
|
+
{
|
|
20
|
+
name: "claude-code",
|
|
21
|
+
command: "claude",
|
|
22
|
+
args: ["--version"],
|
|
23
|
+
paths: [
|
|
24
|
+
join(homedir(), ".local", "bin", "claude"),
|
|
25
|
+
"/usr/local/bin/claude",
|
|
26
|
+
"/opt/homebrew/bin/claude"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "aider",
|
|
31
|
+
command: "aider",
|
|
32
|
+
args: ["--version"],
|
|
33
|
+
paths: [
|
|
34
|
+
join(homedir(), ".local", "bin", "aider"),
|
|
35
|
+
"/usr/local/bin/aider",
|
|
36
|
+
"/opt/homebrew/bin/aider"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "codex",
|
|
41
|
+
command: "codex",
|
|
42
|
+
args: ["--version"],
|
|
43
|
+
paths: [
|
|
44
|
+
join(homedir(), ".local", "bin", "codex"),
|
|
45
|
+
"/usr/local/bin/codex",
|
|
46
|
+
"/opt/homebrew/bin/codex"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "opencode",
|
|
51
|
+
command: "opencode",
|
|
52
|
+
args: ["--version"],
|
|
53
|
+
paths: [
|
|
54
|
+
join(homedir(), ".local", "bin", "opencode"),
|
|
55
|
+
"/usr/local/bin/opencode",
|
|
56
|
+
"/opt/homebrew/bin/opencode"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "gemini",
|
|
61
|
+
command: "gemini",
|
|
62
|
+
args: ["--version"],
|
|
63
|
+
paths: [
|
|
64
|
+
join(homedir(), ".local", "bin", "gemini"),
|
|
65
|
+
"/usr/local/bin/gemini",
|
|
66
|
+
"/opt/homebrew/bin/gemini"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "ollama",
|
|
71
|
+
command: "ollama",
|
|
72
|
+
args: ["--version"],
|
|
73
|
+
paths: [
|
|
74
|
+
join(homedir(), ".local", "bin", "ollama"),
|
|
75
|
+
"/usr/local/bin/ollama",
|
|
76
|
+
"/opt/homebrew/bin/ollama"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
var resolvedPaths = /* @__PURE__ */ new Map();
|
|
81
|
+
async function findCommand(check) {
|
|
82
|
+
try {
|
|
83
|
+
await execa(check.command, check.args, { timeout: 5e3 });
|
|
84
|
+
return check.command;
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
if (check.paths) {
|
|
88
|
+
for (const path of check.paths) {
|
|
89
|
+
if (existsSync(path)) {
|
|
90
|
+
try {
|
|
91
|
+
await execa(path, check.args, { timeout: 5e3 });
|
|
92
|
+
return path;
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
async function detectProvider() {
|
|
101
|
+
const available = [];
|
|
102
|
+
for (const check of providerChecks) {
|
|
103
|
+
const commandPath = await findCommand(check);
|
|
104
|
+
if (commandPath) {
|
|
105
|
+
resolvedPaths.set(check.name, commandPath);
|
|
106
|
+
available.push(check.name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return available;
|
|
110
|
+
}
|
|
111
|
+
async function isProviderAvailable(name) {
|
|
112
|
+
const check = providerChecks.find((c) => c.name === name);
|
|
113
|
+
if (!check) return false;
|
|
114
|
+
const commandPath = await findCommand(check);
|
|
115
|
+
if (commandPath) {
|
|
116
|
+
resolvedPaths.set(name, commandPath);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
function getProviderCommand(name) {
|
|
122
|
+
return resolvedPaths.get(name) || name.replace("-code", "");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/core/utils.ts
|
|
126
|
+
function generateBugId(index) {
|
|
127
|
+
return `WR-${String(index + 1).padStart(3, "0")}`;
|
|
128
|
+
}
|
|
129
|
+
var BugSeverity = z.enum(["critical", "high", "medium", "low"]);
|
|
130
|
+
var BugCategory = z.enum([
|
|
131
|
+
"logic-error",
|
|
132
|
+
"security",
|
|
133
|
+
"async-race-condition",
|
|
134
|
+
"edge-case",
|
|
135
|
+
"null-reference",
|
|
136
|
+
"type-coercion",
|
|
137
|
+
"resource-leak",
|
|
138
|
+
"intent-violation"
|
|
139
|
+
]);
|
|
140
|
+
var ConfidenceLevel = z.enum(["high", "medium", "low"]);
|
|
141
|
+
var ConfidenceScore = z.object({
|
|
142
|
+
overall: ConfidenceLevel,
|
|
143
|
+
codePathValidity: z.number().min(0).max(1),
|
|
144
|
+
reachability: z.number().min(0).max(1),
|
|
145
|
+
intentViolation: z.boolean(),
|
|
146
|
+
staticToolSignal: z.boolean(),
|
|
147
|
+
adversarialSurvived: z.boolean()
|
|
148
|
+
});
|
|
149
|
+
var CodePathStep = z.object({
|
|
150
|
+
step: z.number(),
|
|
151
|
+
file: z.string(),
|
|
152
|
+
line: z.number(),
|
|
153
|
+
code: z.string(),
|
|
154
|
+
explanation: z.string()
|
|
155
|
+
});
|
|
156
|
+
var Bug = z.object({
|
|
157
|
+
id: z.string(),
|
|
158
|
+
title: z.string(),
|
|
159
|
+
description: z.string(),
|
|
160
|
+
file: z.string(),
|
|
161
|
+
line: z.number(),
|
|
162
|
+
endLine: z.number().optional(),
|
|
163
|
+
severity: BugSeverity,
|
|
164
|
+
category: BugCategory,
|
|
165
|
+
confidence: ConfidenceScore,
|
|
166
|
+
codePath: z.array(CodePathStep),
|
|
167
|
+
evidence: z.array(z.string()),
|
|
168
|
+
suggestedFix: z.string().optional(),
|
|
169
|
+
relatedContract: z.string().optional(),
|
|
170
|
+
staticAnalysisSignals: z.array(z.string()).optional(),
|
|
171
|
+
createdAt: z.string().datetime()
|
|
172
|
+
});
|
|
173
|
+
var ProviderType = z.enum([
|
|
174
|
+
"claude-code",
|
|
175
|
+
"aider",
|
|
176
|
+
"codex",
|
|
177
|
+
"opencode",
|
|
178
|
+
"ollama",
|
|
179
|
+
"gemini"
|
|
180
|
+
]);
|
|
181
|
+
var PriorityLevel = z.enum(["critical", "high", "medium", "low", "ignore"]);
|
|
182
|
+
var PackageConfig = z.object({
|
|
183
|
+
path: z.string(),
|
|
184
|
+
priority: PriorityLevel,
|
|
185
|
+
include: z.array(z.string()).optional(),
|
|
186
|
+
exclude: z.array(z.string()).optional()
|
|
187
|
+
});
|
|
188
|
+
var MonorepoConfig = z.object({
|
|
189
|
+
detection: z.enum(["auto", "explicit"]),
|
|
190
|
+
packages: z.array(PackageConfig).optional(),
|
|
191
|
+
crossPackageAnalysis: z.boolean().default(true)
|
|
192
|
+
});
|
|
193
|
+
var WhiteroseConfig = z.object({
|
|
194
|
+
version: z.string().default("1"),
|
|
195
|
+
provider: ProviderType.default("claude-code"),
|
|
196
|
+
providerFallback: z.array(ProviderType).optional(),
|
|
197
|
+
// Scan settings
|
|
198
|
+
include: z.array(z.string()).default(["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]),
|
|
199
|
+
exclude: z.array(z.string()).default(["node_modules", "dist", "build", ".next", "coverage"]),
|
|
200
|
+
// Priority areas
|
|
201
|
+
priorities: z.record(z.string(), PriorityLevel).default({}),
|
|
202
|
+
// Bug categories to scan for
|
|
203
|
+
categories: z.array(BugCategory).default([
|
|
204
|
+
"logic-error",
|
|
205
|
+
"security",
|
|
206
|
+
"async-race-condition",
|
|
207
|
+
"edge-case",
|
|
208
|
+
"null-reference"
|
|
209
|
+
]),
|
|
210
|
+
// Confidence threshold for reporting
|
|
211
|
+
minConfidence: ConfidenceLevel.default("low"),
|
|
212
|
+
// Monorepo settings
|
|
213
|
+
monorepo: MonorepoConfig.optional(),
|
|
214
|
+
// Static analysis integration
|
|
215
|
+
staticAnalysis: z.object({
|
|
216
|
+
typescript: z.boolean().default(true),
|
|
217
|
+
eslint: z.boolean().default(true)
|
|
218
|
+
}).default({}),
|
|
219
|
+
// Output settings
|
|
220
|
+
output: z.object({
|
|
221
|
+
sarif: z.boolean().default(true),
|
|
222
|
+
markdown: z.boolean().default(true),
|
|
223
|
+
sarifPath: z.string().default(".whiterose/reports"),
|
|
224
|
+
markdownPath: z.string().default("BUGS.md")
|
|
225
|
+
}).default({})
|
|
226
|
+
});
|
|
227
|
+
var BehavioralContract = z.object({
|
|
228
|
+
function: z.string(),
|
|
229
|
+
file: z.string(),
|
|
230
|
+
inputs: z.array(
|
|
231
|
+
z.object({
|
|
232
|
+
name: z.string(),
|
|
233
|
+
type: z.string(),
|
|
234
|
+
constraints: z.string().optional()
|
|
235
|
+
})
|
|
236
|
+
),
|
|
237
|
+
outputs: z.object({
|
|
238
|
+
type: z.string(),
|
|
239
|
+
constraints: z.string().optional()
|
|
240
|
+
}),
|
|
241
|
+
invariants: z.array(z.string()),
|
|
242
|
+
sideEffects: z.array(z.string()),
|
|
243
|
+
throws: z.array(z.string()).optional()
|
|
244
|
+
});
|
|
245
|
+
var FeatureIntent = z.object({
|
|
246
|
+
name: z.string(),
|
|
247
|
+
description: z.string(),
|
|
248
|
+
priority: PriorityLevel,
|
|
249
|
+
constraints: z.array(z.string()),
|
|
250
|
+
relatedFiles: z.array(z.string())
|
|
251
|
+
});
|
|
252
|
+
z.object({
|
|
253
|
+
version: z.string(),
|
|
254
|
+
generatedAt: z.string().datetime(),
|
|
255
|
+
summary: z.object({
|
|
256
|
+
framework: z.string().optional(),
|
|
257
|
+
language: z.string(),
|
|
258
|
+
type: z.string(),
|
|
259
|
+
// e-commerce, saas, api, etc.
|
|
260
|
+
description: z.string()
|
|
261
|
+
}),
|
|
262
|
+
features: z.array(FeatureIntent),
|
|
263
|
+
contracts: z.array(BehavioralContract),
|
|
264
|
+
dependencies: z.record(z.string(), z.string()),
|
|
265
|
+
structure: z.object({
|
|
266
|
+
totalFiles: z.number(),
|
|
267
|
+
totalLines: z.number(),
|
|
268
|
+
packages: z.array(z.string()).optional()
|
|
269
|
+
})
|
|
270
|
+
});
|
|
271
|
+
var FileHash = z.object({
|
|
272
|
+
path: z.string(),
|
|
273
|
+
hash: z.string(),
|
|
274
|
+
lastModified: z.string().datetime()
|
|
275
|
+
});
|
|
276
|
+
z.object({
|
|
277
|
+
version: z.string(),
|
|
278
|
+
lastFullScan: z.string().datetime().optional(),
|
|
279
|
+
lastIncrementalScan: z.string().datetime().optional(),
|
|
280
|
+
fileHashes: z.array(FileHash)
|
|
281
|
+
});
|
|
282
|
+
z.object({
|
|
283
|
+
id: z.string(),
|
|
284
|
+
timestamp: z.string().datetime(),
|
|
285
|
+
scanType: z.enum(["full", "incremental"]),
|
|
286
|
+
filesScanned: z.number(),
|
|
287
|
+
filesChanged: z.number().optional(),
|
|
288
|
+
duration: z.number(),
|
|
289
|
+
// ms
|
|
290
|
+
bugs: z.array(Bug),
|
|
291
|
+
summary: z.object({
|
|
292
|
+
critical: z.number(),
|
|
293
|
+
high: z.number(),
|
|
294
|
+
medium: z.number(),
|
|
295
|
+
low: z.number(),
|
|
296
|
+
total: z.number()
|
|
297
|
+
})
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// src/core/validation.ts
|
|
301
|
+
function safeParseJson(json, schema) {
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(json);
|
|
304
|
+
const result = schema.safeParse(parsed);
|
|
305
|
+
if (result.success) {
|
|
306
|
+
return { success: true, data: result.data };
|
|
307
|
+
}
|
|
308
|
+
return { success: false, error: formatZodError(result.error) };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return { success: false, error: error.message || "Invalid JSON" };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function formatZodError(error) {
|
|
314
|
+
return error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
315
|
+
}
|
|
316
|
+
var PartialBugFromLLM = z.object({
|
|
317
|
+
file: z.string(),
|
|
318
|
+
line: z.number(),
|
|
319
|
+
endLine: z.number().optional(),
|
|
320
|
+
title: z.string(),
|
|
321
|
+
description: z.string().optional().default(""),
|
|
322
|
+
severity: BugSeverity.optional().default("medium"),
|
|
323
|
+
category: BugCategory.optional().default("logic-error"),
|
|
324
|
+
codePath: z.array(z.object({
|
|
325
|
+
step: z.number().optional(),
|
|
326
|
+
file: z.string().optional(),
|
|
327
|
+
line: z.number().optional(),
|
|
328
|
+
code: z.string().optional().default(""),
|
|
329
|
+
explanation: z.string().optional().default("")
|
|
330
|
+
})).optional().default([]),
|
|
331
|
+
evidence: z.array(z.string()).optional().default([]),
|
|
332
|
+
suggestedFix: z.string().optional(),
|
|
333
|
+
confidence: z.object({
|
|
334
|
+
overall: ConfidenceLevel.optional().default("medium"),
|
|
335
|
+
codePathValidity: z.number().min(0).max(1).optional().default(0.5),
|
|
336
|
+
reachability: z.number().min(0).max(1).optional().default(0.5),
|
|
337
|
+
intentViolation: z.boolean().optional().default(false),
|
|
338
|
+
staticToolSignal: z.boolean().optional().default(false),
|
|
339
|
+
adversarialSurvived: z.boolean().optional().default(false)
|
|
340
|
+
}).optional().default({})
|
|
341
|
+
});
|
|
342
|
+
var PartialUnderstandingFromLLM = z.object({
|
|
343
|
+
summary: z.object({
|
|
344
|
+
type: z.string().optional().default("unknown"),
|
|
345
|
+
framework: z.string().optional(),
|
|
346
|
+
language: z.string().optional().default("unknown"),
|
|
347
|
+
description: z.string().optional().default("")
|
|
348
|
+
}).optional().default({}),
|
|
349
|
+
features: z.array(z.object({
|
|
350
|
+
name: z.string(),
|
|
351
|
+
description: z.string(),
|
|
352
|
+
priority: z.enum(["critical", "high", "medium", "low", "ignore"]).optional().default("medium"),
|
|
353
|
+
constraints: z.array(z.string()).optional().default([]),
|
|
354
|
+
relatedFiles: z.array(z.string()).optional().default([])
|
|
355
|
+
})).optional().default([]),
|
|
356
|
+
contracts: z.array(z.object({
|
|
357
|
+
function: z.string(),
|
|
358
|
+
file: z.string(),
|
|
359
|
+
inputs: z.array(z.object({
|
|
360
|
+
name: z.string(),
|
|
361
|
+
type: z.string(),
|
|
362
|
+
constraints: z.string().optional()
|
|
363
|
+
})).optional().default([]),
|
|
364
|
+
outputs: z.object({
|
|
365
|
+
type: z.string(),
|
|
366
|
+
constraints: z.string().optional()
|
|
367
|
+
}).optional().default({ type: "unknown" }),
|
|
368
|
+
invariants: z.array(z.string()).optional().default([]),
|
|
369
|
+
sideEffects: z.array(z.string()).optional().default([]),
|
|
370
|
+
throws: z.array(z.string()).optional()
|
|
371
|
+
})).optional().default([]),
|
|
372
|
+
dependencies: z.record(z.string(), z.string()).optional().default({})
|
|
373
|
+
});
|
|
374
|
+
var AdversarialResultSchema = z.object({
|
|
375
|
+
survived: z.boolean(),
|
|
376
|
+
counterArguments: z.array(z.string()).optional().default([]),
|
|
377
|
+
confidence: ConfidenceLevel.optional()
|
|
378
|
+
});
|
|
379
|
+
z.object({
|
|
380
|
+
$schema: z.string().optional(),
|
|
381
|
+
version: z.string(),
|
|
382
|
+
runs: z.array(z.object({
|
|
383
|
+
tool: z.object({
|
|
384
|
+
driver: z.object({
|
|
385
|
+
name: z.string(),
|
|
386
|
+
version: z.string().optional(),
|
|
387
|
+
informationUri: z.string().optional(),
|
|
388
|
+
rules: z.array(z.any()).optional()
|
|
389
|
+
})
|
|
390
|
+
}),
|
|
391
|
+
results: z.array(z.object({
|
|
392
|
+
ruleId: z.string().optional(),
|
|
393
|
+
level: z.enum(["none", "note", "warning", "error"]).optional(),
|
|
394
|
+
message: z.object({
|
|
395
|
+
text: z.string()
|
|
396
|
+
}),
|
|
397
|
+
locations: z.array(z.object({
|
|
398
|
+
physicalLocation: z.object({
|
|
399
|
+
artifactLocation: z.object({
|
|
400
|
+
uri: z.string()
|
|
401
|
+
}),
|
|
402
|
+
region: z.object({
|
|
403
|
+
startLine: z.number(),
|
|
404
|
+
endLine: z.number().optional()
|
|
405
|
+
}).optional()
|
|
406
|
+
})
|
|
407
|
+
})).optional()
|
|
408
|
+
})).optional().default([])
|
|
409
|
+
}))
|
|
410
|
+
});
|
|
411
|
+
z.object({
|
|
412
|
+
number: z.number(),
|
|
413
|
+
title: z.string(),
|
|
414
|
+
body: z.string().nullable(),
|
|
415
|
+
state: z.string(),
|
|
416
|
+
labels: z.array(z.object({
|
|
417
|
+
name: z.string()
|
|
418
|
+
})).optional().default([]),
|
|
419
|
+
url: z.string().optional()
|
|
420
|
+
});
|
|
421
|
+
z.array(z.object({
|
|
422
|
+
filePath: z.string(),
|
|
423
|
+
messages: z.array(z.object({
|
|
424
|
+
ruleId: z.string().nullable(),
|
|
425
|
+
severity: z.number(),
|
|
426
|
+
message: z.string(),
|
|
427
|
+
line: z.number(),
|
|
428
|
+
column: z.number()
|
|
429
|
+
})),
|
|
430
|
+
errorCount: z.number(),
|
|
431
|
+
warningCount: z.number()
|
|
432
|
+
}));
|
|
433
|
+
z.object({
|
|
434
|
+
name: z.string().optional(),
|
|
435
|
+
version: z.string().optional(),
|
|
436
|
+
description: z.string().optional(),
|
|
437
|
+
scripts: z.record(z.string(), z.string()).optional(),
|
|
438
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
439
|
+
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
440
|
+
workspaces: z.union([
|
|
441
|
+
z.array(z.string()),
|
|
442
|
+
z.object({ packages: z.array(z.string()) })
|
|
443
|
+
]).optional()
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// src/providers/adapters/claude-code.ts
|
|
447
|
+
var MARKERS = {
|
|
448
|
+
SCANNING: "###SCANNING:",
|
|
449
|
+
BUG: "###BUG:",
|
|
450
|
+
UNDERSTANDING: "###UNDERSTANDING:",
|
|
451
|
+
COMPLETE: "###COMPLETE",
|
|
452
|
+
ERROR: "###ERROR:"
|
|
453
|
+
};
|
|
454
|
+
var ClaudeCodeProvider = class {
|
|
455
|
+
name = "claude-code";
|
|
456
|
+
progressCallback;
|
|
457
|
+
bugFoundCallback;
|
|
458
|
+
currentProcess;
|
|
459
|
+
unsafeMode = false;
|
|
460
|
+
async detect() {
|
|
461
|
+
return isProviderAvailable("claude-code");
|
|
462
|
+
}
|
|
463
|
+
async isAvailable() {
|
|
464
|
+
return isProviderAvailable("claude-code");
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Enable unsafe mode (--dangerously-skip-permissions).
|
|
468
|
+
* WARNING: This bypasses Claude's permission prompts and should only be used
|
|
469
|
+
* when you trust the codebase being analyzed.
|
|
470
|
+
*/
|
|
471
|
+
setUnsafeMode(enabled) {
|
|
472
|
+
this.unsafeMode = enabled;
|
|
473
|
+
}
|
|
474
|
+
isUnsafeMode() {
|
|
475
|
+
return this.unsafeMode;
|
|
476
|
+
}
|
|
477
|
+
setProgressCallback(callback) {
|
|
478
|
+
this.progressCallback = callback;
|
|
479
|
+
}
|
|
480
|
+
setBugFoundCallback(callback) {
|
|
481
|
+
this.bugFoundCallback = callback;
|
|
482
|
+
}
|
|
483
|
+
reportProgress(message) {
|
|
484
|
+
if (this.progressCallback) {
|
|
485
|
+
this.progressCallback(message);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
reportBug(bug) {
|
|
489
|
+
if (this.bugFoundCallback) {
|
|
490
|
+
this.bugFoundCallback(bug);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Cancel any running analysis
|
|
494
|
+
cancel() {
|
|
495
|
+
if (this.currentProcess) {
|
|
496
|
+
this.currentProcess.kill();
|
|
497
|
+
this.currentProcess = void 0;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async analyze(context) {
|
|
501
|
+
const { files, understanding } = context;
|
|
502
|
+
if (files.length === 0) {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
const cwd = process.cwd();
|
|
506
|
+
const bugs = [];
|
|
507
|
+
let bugIndex = 0;
|
|
508
|
+
const prompt = this.buildAgenticAnalysisPrompt(understanding);
|
|
509
|
+
this.reportProgress("Starting agentic analysis...");
|
|
510
|
+
try {
|
|
511
|
+
await this.runAgenticClaude(prompt, cwd, {
|
|
512
|
+
onScanning: (file) => {
|
|
513
|
+
this.reportProgress(`Scanning: ${file}`);
|
|
514
|
+
},
|
|
515
|
+
onBugFound: (bugData) => {
|
|
516
|
+
const bug = this.parseBugData(bugData, bugIndex++, files);
|
|
517
|
+
if (bug) {
|
|
518
|
+
bugs.push(bug);
|
|
519
|
+
this.reportBug(bug);
|
|
520
|
+
this.reportProgress(`Found: ${bug.title} (${bug.severity})`);
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
onComplete: () => {
|
|
524
|
+
this.reportProgress(`Analysis complete. Found ${bugs.length} bugs.`);
|
|
525
|
+
},
|
|
526
|
+
onError: (error) => {
|
|
527
|
+
this.reportProgress(`Error: ${error}`);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (error.message?.includes("ENOENT")) {
|
|
532
|
+
throw new Error("Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code");
|
|
533
|
+
}
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
return bugs;
|
|
537
|
+
}
|
|
538
|
+
async adversarialValidate(bug, _context) {
|
|
539
|
+
let fileContent = "";
|
|
540
|
+
try {
|
|
541
|
+
if (existsSync(bug.file)) {
|
|
542
|
+
fileContent = readFileSync(bug.file, "utf-8");
|
|
543
|
+
const lines = fileContent.split("\n");
|
|
544
|
+
const start = Math.max(0, bug.line - 20);
|
|
545
|
+
const end = Math.min(lines.length, (bug.endLine || bug.line) + 20);
|
|
546
|
+
fileContent = lines.slice(start, end).join("\n");
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
const prompt = this.buildAdversarialPrompt(bug, fileContent);
|
|
551
|
+
const result = await this.runSimpleClaude(prompt, process.cwd());
|
|
552
|
+
return this.parseAdversarialResponse(result, bug);
|
|
553
|
+
}
|
|
554
|
+
async generateUnderstanding(files, existingDocsSummary) {
|
|
555
|
+
const cwd = process.cwd();
|
|
556
|
+
this.reportProgress(`Starting codebase analysis (${files.length} files)...`);
|
|
557
|
+
const prompt = this.buildAgenticUnderstandingPrompt(existingDocsSummary);
|
|
558
|
+
let understandingJson = "";
|
|
559
|
+
try {
|
|
560
|
+
await this.runAgenticClaude(prompt, cwd, {
|
|
561
|
+
onScanning: (file) => {
|
|
562
|
+
this.reportProgress(`Examining: ${file}`);
|
|
563
|
+
},
|
|
564
|
+
onUnderstanding: (json) => {
|
|
565
|
+
understandingJson = json;
|
|
566
|
+
},
|
|
567
|
+
onComplete: () => {
|
|
568
|
+
this.reportProgress("Understanding complete.");
|
|
569
|
+
},
|
|
570
|
+
onError: (error) => {
|
|
571
|
+
this.reportProgress(`Error: ${error}`);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
return this.parseUnderstandingResponse(understandingJson, files);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (error.message?.includes("ENOENT")) {
|
|
577
|
+
throw new Error("Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code");
|
|
578
|
+
}
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// ─────────────────────────────────────────────────────────────
|
|
583
|
+
// Agentic Prompts
|
|
584
|
+
// ─────────────────────────────────────────────────────────────
|
|
585
|
+
buildAgenticAnalysisPrompt(understanding) {
|
|
586
|
+
return `You are whiterose, an expert bug hunter. Your task is to explore this codebase and find real bugs.
|
|
587
|
+
|
|
588
|
+
CODEBASE CONTEXT:
|
|
589
|
+
- Type: ${understanding.summary.type}
|
|
590
|
+
- Framework: ${understanding.summary.framework || "Unknown"}
|
|
591
|
+
- Description: ${understanding.summary.description}
|
|
592
|
+
|
|
593
|
+
YOUR TASK:
|
|
594
|
+
1. Explore the codebase by reading files
|
|
595
|
+
2. Look for bugs in these categories:
|
|
596
|
+
- Logic errors (off-by-one, wrong operators, incorrect conditions)
|
|
597
|
+
- Null/undefined dereference
|
|
598
|
+
- Security vulnerabilities (injection, auth bypass, XSS)
|
|
599
|
+
- Async/race conditions (missing await, unhandled promises)
|
|
600
|
+
- Edge cases (empty arrays, zero values, boundaries)
|
|
601
|
+
- Resource leaks (unclosed connections)
|
|
602
|
+
|
|
603
|
+
PROTOCOL - You MUST output these markers:
|
|
604
|
+
- Before reading each file, output: ${MARKERS.SCANNING}<filepath>
|
|
605
|
+
- When you find a bug, output: ${MARKERS.BUG}<json>
|
|
606
|
+
- When completely done, output: ${MARKERS.COMPLETE}
|
|
607
|
+
- If you encounter an error, output: ${MARKERS.ERROR}<message>
|
|
608
|
+
|
|
609
|
+
BUG JSON FORMAT:
|
|
610
|
+
${MARKERS.BUG}{"file":"src/api/users.ts","line":42,"title":"Null dereference in getUserById","description":"...","severity":"high","category":"null-reference","evidence":["..."],"suggestedFix":"..."}
|
|
611
|
+
|
|
612
|
+
IMPORTANT:
|
|
613
|
+
- Only report bugs you have HIGH confidence in
|
|
614
|
+
- Include exact line numbers
|
|
615
|
+
- Focus on real bugs, not style issues
|
|
616
|
+
- Explore systematically - check API routes, data handling, auth flows
|
|
617
|
+
|
|
618
|
+
Now explore this codebase and find bugs. Start by reading the main entry points.`;
|
|
619
|
+
}
|
|
620
|
+
buildAgenticUnderstandingPrompt(existingDocsSummary) {
|
|
621
|
+
const docsSection = existingDocsSummary ? `
|
|
622
|
+
|
|
623
|
+
EXISTING DOCUMENTATION (merge this with your exploration):
|
|
624
|
+
${existingDocsSummary}
|
|
625
|
+
` : "";
|
|
626
|
+
return `You are whiterose. Your task is to understand this codebase.
|
|
627
|
+
${docsSection}
|
|
628
|
+
YOUR TASK:
|
|
629
|
+
1. Review the existing documentation above (if any)
|
|
630
|
+
2. Explore the codebase structure to fill in gaps
|
|
631
|
+
3. Read key files (main entry points, config files, core modules)
|
|
632
|
+
4. Build a comprehensive understanding merging docs + code exploration
|
|
633
|
+
5. Identify main features, business rules, and behavioral contracts
|
|
634
|
+
|
|
635
|
+
PROTOCOL - You MUST output these markers:
|
|
636
|
+
- Before reading each file, output: ${MARKERS.SCANNING}<filepath>
|
|
637
|
+
- When you have full understanding, output: ${MARKERS.UNDERSTANDING}<json>
|
|
638
|
+
- When completely done, output: ${MARKERS.COMPLETE}
|
|
639
|
+
|
|
640
|
+
UNDERSTANDING JSON FORMAT:
|
|
641
|
+
${MARKERS.UNDERSTANDING}{
|
|
642
|
+
"summary": {
|
|
643
|
+
"type": "api|web-app|cli|library|etc",
|
|
644
|
+
"framework": "next.js|express|react|etc",
|
|
645
|
+
"language": "typescript|javascript",
|
|
646
|
+
"description": "2-3 sentence description"
|
|
647
|
+
},
|
|
648
|
+
"features": [
|
|
649
|
+
{"name": "Feature", "description": "What it does", "priority": "critical|high|medium|low", "constraints": ["business rule 1", "invariant 2"], "relatedFiles": ["path/to/file.ts"]}
|
|
650
|
+
],
|
|
651
|
+
"contracts": [
|
|
652
|
+
{"function": "functionName", "file": "path/to/file.ts", "inputs": [], "outputs": {}, "invariants": ["must do X before Y"], "sideEffects": [], "throws": []}
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
IMPORTANT:
|
|
657
|
+
- Merge existing documentation with what you discover in the code
|
|
658
|
+
- Focus on business rules and invariants (what MUST be true)
|
|
659
|
+
- Identify critical paths (checkout, auth, payments, etc.)
|
|
660
|
+
- Document behavioral contracts for important functions
|
|
661
|
+
|
|
662
|
+
Now explore this codebase and build understanding.`;
|
|
663
|
+
}
|
|
664
|
+
buildAdversarialPrompt(bug, fileContent) {
|
|
665
|
+
return `You are a skeptical code reviewer. Try to DISPROVE this bug report.
|
|
666
|
+
|
|
667
|
+
REPORTED BUG:
|
|
668
|
+
- File: ${bug.file}:${bug.line}
|
|
669
|
+
- Title: ${bug.title}
|
|
670
|
+
- Description: ${bug.description}
|
|
671
|
+
- Severity: ${bug.severity}
|
|
672
|
+
|
|
673
|
+
CODE CONTEXT:
|
|
674
|
+
${fileContent}
|
|
675
|
+
|
|
676
|
+
Try to prove this is NOT a bug by finding:
|
|
677
|
+
1. Guards or validation that prevents this
|
|
678
|
+
2. Type system guarantees
|
|
679
|
+
3. Framework behavior that handles this
|
|
680
|
+
4. Unreachable code paths
|
|
681
|
+
|
|
682
|
+
OUTPUT AS JSON:
|
|
683
|
+
{"survived": true/false, "counterArguments": ["reason 1"], "confidence": "high/medium/low", "explanation": "..."}
|
|
684
|
+
|
|
685
|
+
Set "survived": true if you CANNOT disprove it (it's a real bug).`;
|
|
686
|
+
}
|
|
687
|
+
// ─────────────────────────────────────────────────────────────
|
|
688
|
+
// Claude CLI Execution (Agentic Mode)
|
|
689
|
+
// ─────────────────────────────────────────────────────────────
|
|
690
|
+
async runAgenticClaude(prompt, cwd, callbacks) {
|
|
691
|
+
const claudeCommand = getProviderCommand("claude-code");
|
|
692
|
+
const args = ["--verbose", "-p", prompt];
|
|
693
|
+
if (this.unsafeMode) {
|
|
694
|
+
args.unshift("--dangerously-skip-permissions");
|
|
695
|
+
}
|
|
696
|
+
this.currentProcess = execa(
|
|
697
|
+
claudeCommand,
|
|
698
|
+
args,
|
|
699
|
+
{
|
|
700
|
+
cwd,
|
|
701
|
+
env: {
|
|
702
|
+
...process.env,
|
|
703
|
+
NO_COLOR: "1"
|
|
704
|
+
},
|
|
705
|
+
reject: false
|
|
706
|
+
}
|
|
707
|
+
);
|
|
708
|
+
let buffer = "";
|
|
709
|
+
this.currentProcess.stdout?.on("data", (chunk) => {
|
|
710
|
+
buffer += chunk.toString();
|
|
711
|
+
const lines = buffer.split("\n");
|
|
712
|
+
buffer = lines.pop() || "";
|
|
713
|
+
for (const line of lines) {
|
|
714
|
+
this.processAgentOutput(line, callbacks);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
this.currentProcess.stderr?.on("data", (chunk) => {
|
|
718
|
+
const text3 = chunk.toString().trim();
|
|
719
|
+
if (text3 && !text3.includes("Loading")) ;
|
|
720
|
+
});
|
|
721
|
+
await this.currentProcess;
|
|
722
|
+
if (buffer.trim()) {
|
|
723
|
+
this.processAgentOutput(buffer, callbacks);
|
|
724
|
+
}
|
|
725
|
+
this.currentProcess = void 0;
|
|
726
|
+
}
|
|
727
|
+
processAgentOutput(line, callbacks) {
|
|
728
|
+
const trimmed = line.trim();
|
|
729
|
+
if (trimmed.startsWith(MARKERS.SCANNING)) {
|
|
730
|
+
const file = trimmed.slice(MARKERS.SCANNING.length).trim();
|
|
731
|
+
callbacks.onScanning?.(file);
|
|
732
|
+
} else if (trimmed.startsWith(MARKERS.BUG)) {
|
|
733
|
+
const json = trimmed.slice(MARKERS.BUG.length).trim();
|
|
734
|
+
callbacks.onBugFound?.(json);
|
|
735
|
+
} else if (trimmed.startsWith(MARKERS.UNDERSTANDING)) {
|
|
736
|
+
const json = trimmed.slice(MARKERS.UNDERSTANDING.length).trim();
|
|
737
|
+
callbacks.onUnderstanding?.(json);
|
|
738
|
+
} else if (trimmed.startsWith(MARKERS.COMPLETE)) {
|
|
739
|
+
callbacks.onComplete?.();
|
|
740
|
+
} else if (trimmed.startsWith(MARKERS.ERROR)) {
|
|
741
|
+
const error = trimmed.slice(MARKERS.ERROR.length).trim();
|
|
742
|
+
callbacks.onError?.(error);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// Simple non-agentic mode for short prompts (adversarial validation)
|
|
746
|
+
async runSimpleClaude(prompt, cwd) {
|
|
747
|
+
const claudeCommand = getProviderCommand("claude-code");
|
|
748
|
+
try {
|
|
749
|
+
const { stdout } = await execa(
|
|
750
|
+
claudeCommand,
|
|
751
|
+
["-p", prompt, "--output-format", "text"],
|
|
752
|
+
{
|
|
753
|
+
cwd,
|
|
754
|
+
timeout: 12e4,
|
|
755
|
+
// 2 min for simple prompts
|
|
756
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
return stdout;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (error.stdout) return error.stdout;
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// ─────────────────────────────────────────────────────────────
|
|
766
|
+
// Response Parsers
|
|
767
|
+
// ─────────────────────────────────────────────────────────────
|
|
768
|
+
parseBugData(json, index, files) {
|
|
769
|
+
const result = safeParseJson(json, PartialBugFromLLM);
|
|
770
|
+
if (!result.success) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
const data = result.data;
|
|
774
|
+
let filePath = data.file;
|
|
775
|
+
if (!filePath.startsWith("/")) {
|
|
776
|
+
const match = files.find((f) => f.endsWith(filePath) || f.includes(filePath));
|
|
777
|
+
if (match) filePath = match;
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
id: generateBugId(index),
|
|
781
|
+
title: String(data.title).slice(0, 100),
|
|
782
|
+
description: String(data.description || ""),
|
|
783
|
+
file: filePath,
|
|
784
|
+
line: data.line,
|
|
785
|
+
endLine: data.endLine,
|
|
786
|
+
severity: data.severity ?? "medium",
|
|
787
|
+
category: data.category ?? "logic-error",
|
|
788
|
+
confidence: {
|
|
789
|
+
overall: data.confidence?.overall || "medium",
|
|
790
|
+
codePathValidity: data.confidence?.codePathValidity ?? 0.8,
|
|
791
|
+
reachability: data.confidence?.reachability ?? 0.8,
|
|
792
|
+
intentViolation: data.confidence?.intentViolation ?? false,
|
|
793
|
+
staticToolSignal: data.confidence?.staticToolSignal ?? false,
|
|
794
|
+
adversarialSurvived: data.confidence?.adversarialSurvived ?? false
|
|
795
|
+
},
|
|
796
|
+
codePath: (data.codePath || []).map((step, idx) => ({
|
|
797
|
+
step: idx + 1,
|
|
798
|
+
file: step.file || filePath,
|
|
799
|
+
line: step.line || data.line,
|
|
800
|
+
code: step.code || "",
|
|
801
|
+
explanation: step.explanation || ""
|
|
802
|
+
})),
|
|
803
|
+
evidence: data.evidence || [],
|
|
804
|
+
suggestedFix: data.suggestedFix,
|
|
805
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
parseAdversarialResponse(response, bug) {
|
|
809
|
+
const json = this.extractJson(response);
|
|
810
|
+
if (!json) {
|
|
811
|
+
return { survived: true, counterArguments: [] };
|
|
812
|
+
}
|
|
813
|
+
const result = safeParseJson(json, AdversarialResultSchema);
|
|
814
|
+
if (!result.success) {
|
|
815
|
+
return { survived: true, counterArguments: [] };
|
|
816
|
+
}
|
|
817
|
+
const parsed = result.data;
|
|
818
|
+
const survived = parsed.survived !== false;
|
|
819
|
+
return {
|
|
820
|
+
survived,
|
|
821
|
+
counterArguments: parsed.counterArguments || [],
|
|
822
|
+
adjustedConfidence: survived ? {
|
|
823
|
+
...bug.confidence,
|
|
824
|
+
overall: parsed.confidence || bug.confidence.overall,
|
|
825
|
+
adversarialSurvived: true
|
|
826
|
+
} : void 0
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
parseUnderstandingResponse(response, files) {
|
|
830
|
+
let totalLines = 0;
|
|
831
|
+
for (const file of files.slice(0, 50)) {
|
|
832
|
+
try {
|
|
833
|
+
const content = readFileSync(file, "utf-8");
|
|
834
|
+
totalLines += content.split("\n").length;
|
|
835
|
+
} catch {
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const json = this.extractJson(response);
|
|
839
|
+
if (!json) {
|
|
840
|
+
return {
|
|
841
|
+
version: "1",
|
|
842
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
843
|
+
summary: {
|
|
844
|
+
type: "unknown",
|
|
845
|
+
language: "unknown",
|
|
846
|
+
description: "Failed to analyze codebase: No JSON found in response"
|
|
847
|
+
},
|
|
848
|
+
features: [],
|
|
849
|
+
contracts: [],
|
|
850
|
+
dependencies: {},
|
|
851
|
+
structure: { totalFiles: files.length, totalLines }
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
const result = safeParseJson(json, PartialUnderstandingFromLLM);
|
|
855
|
+
if (!result.success) {
|
|
856
|
+
return {
|
|
857
|
+
version: "1",
|
|
858
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
859
|
+
summary: {
|
|
860
|
+
type: "unknown",
|
|
861
|
+
language: "unknown",
|
|
862
|
+
description: `Failed to analyze codebase: ${result.error}`
|
|
863
|
+
},
|
|
864
|
+
features: [],
|
|
865
|
+
contracts: [],
|
|
866
|
+
dependencies: {},
|
|
867
|
+
structure: { totalFiles: files.length, totalLines }
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
const parsed = result.data;
|
|
871
|
+
return {
|
|
872
|
+
version: "1",
|
|
873
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
874
|
+
summary: {
|
|
875
|
+
type: parsed.summary?.type || "unknown",
|
|
876
|
+
framework: parsed.summary?.framework || void 0,
|
|
877
|
+
language: parsed.summary?.language || "typescript",
|
|
878
|
+
description: parsed.summary?.description || "No description available"
|
|
879
|
+
},
|
|
880
|
+
features: (parsed.features || []).map((f) => ({
|
|
881
|
+
name: f.name || "Unknown",
|
|
882
|
+
description: f.description || "",
|
|
883
|
+
priority: f.priority || "medium",
|
|
884
|
+
constraints: f.constraints || [],
|
|
885
|
+
relatedFiles: f.relatedFiles || []
|
|
886
|
+
})),
|
|
887
|
+
contracts: (parsed.contracts || []).map((c) => ({
|
|
888
|
+
function: c.function || "unknown",
|
|
889
|
+
file: c.file || "unknown",
|
|
890
|
+
inputs: c.inputs || [],
|
|
891
|
+
outputs: c.outputs || { type: "unknown" },
|
|
892
|
+
invariants: c.invariants || [],
|
|
893
|
+
sideEffects: c.sideEffects || [],
|
|
894
|
+
throws: c.throws
|
|
895
|
+
})),
|
|
896
|
+
dependencies: {},
|
|
897
|
+
structure: {
|
|
898
|
+
totalFiles: files.length,
|
|
899
|
+
totalLines
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
// ─────────────────────────────────────────────────────────────
|
|
904
|
+
// Utilities
|
|
905
|
+
// ─────────────────────────────────────────────────────────────
|
|
906
|
+
extractJson(text3) {
|
|
907
|
+
const codeBlockMatch = text3.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
908
|
+
if (codeBlockMatch) {
|
|
909
|
+
return codeBlockMatch[1].trim();
|
|
910
|
+
}
|
|
911
|
+
const arrayMatch = text3.match(/\[[\s\S]*\]/);
|
|
912
|
+
if (arrayMatch) {
|
|
913
|
+
return arrayMatch[0];
|
|
914
|
+
}
|
|
915
|
+
const objectMatch = text3.match(/\{[\s\S]*\}/);
|
|
916
|
+
if (objectMatch) {
|
|
917
|
+
return objectMatch[0];
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
var MAX_FILE_SIZE = 5e4;
|
|
923
|
+
var MAX_TOTAL_CONTEXT = 2e5;
|
|
924
|
+
var AIDER_TIMEOUT = 3e5;
|
|
925
|
+
var AiderProvider = class {
|
|
926
|
+
name = "aider";
|
|
927
|
+
async detect() {
|
|
928
|
+
return isProviderAvailable("aider");
|
|
929
|
+
}
|
|
930
|
+
async isAvailable() {
|
|
931
|
+
return isProviderAvailable("aider");
|
|
932
|
+
}
|
|
933
|
+
async analyze(context) {
|
|
934
|
+
const { files, understanding, staticAnalysisResults } = context;
|
|
935
|
+
if (files.length === 0) {
|
|
936
|
+
return [];
|
|
937
|
+
}
|
|
938
|
+
const fileContents = this.readFilesWithLimit(files, MAX_TOTAL_CONTEXT);
|
|
939
|
+
const prompt = this.buildAnalysisPrompt(fileContents, understanding, staticAnalysisResults);
|
|
940
|
+
const result = await this.runAider(prompt, files, dirname(files[0]));
|
|
941
|
+
return this.parseAnalysisResponse(result, files);
|
|
942
|
+
}
|
|
943
|
+
async adversarialValidate(bug, _context) {
|
|
944
|
+
let fileContent = "";
|
|
945
|
+
try {
|
|
946
|
+
if (existsSync(bug.file)) {
|
|
947
|
+
fileContent = readFileSync(bug.file, "utf-8");
|
|
948
|
+
const lines = fileContent.split("\n");
|
|
949
|
+
const start = Math.max(0, bug.line - 20);
|
|
950
|
+
const end = Math.min(lines.length, (bug.endLine || bug.line) + 20);
|
|
951
|
+
fileContent = lines.slice(start, end).join("\n");
|
|
952
|
+
}
|
|
953
|
+
} catch {
|
|
954
|
+
}
|
|
955
|
+
const prompt = this.buildAdversarialPrompt(bug, fileContent);
|
|
956
|
+
const result = await this.runAider(prompt, [bug.file], dirname(bug.file));
|
|
957
|
+
return this.parseAdversarialResponse(result, bug);
|
|
958
|
+
}
|
|
959
|
+
async generateUnderstanding(files, _existingDocsSummary) {
|
|
960
|
+
const sampledFiles = this.prioritizeFiles(files, 40);
|
|
961
|
+
const fileContents = this.readFilesWithLimit(sampledFiles, MAX_TOTAL_CONTEXT);
|
|
962
|
+
let packageJson = null;
|
|
963
|
+
const packageJsonPath = files.find((f) => f.endsWith("package.json"));
|
|
964
|
+
if (packageJsonPath) {
|
|
965
|
+
try {
|
|
966
|
+
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const prompt = this.buildUnderstandingPrompt(files.length, fileContents, packageJson);
|
|
971
|
+
const result = await this.runAider(prompt, sampledFiles.slice(0, 5), process.cwd());
|
|
972
|
+
return this.parseUnderstandingResponse(result, files);
|
|
973
|
+
}
|
|
974
|
+
// ─────────────────────────────────────────────────────────────
|
|
975
|
+
// File Reading Helpers
|
|
976
|
+
// ─────────────────────────────────────────────────────────────
|
|
977
|
+
readFilesWithLimit(files, maxTotal) {
|
|
978
|
+
const result = [];
|
|
979
|
+
let totalSize = 0;
|
|
980
|
+
for (const file of files) {
|
|
981
|
+
if (totalSize >= maxTotal) break;
|
|
982
|
+
try {
|
|
983
|
+
if (!existsSync(file)) continue;
|
|
984
|
+
let content = readFileSync(file, "utf-8");
|
|
985
|
+
if (content.length > MAX_FILE_SIZE) {
|
|
986
|
+
content = content.slice(0, MAX_FILE_SIZE) + "\n// ... truncated ...";
|
|
987
|
+
}
|
|
988
|
+
if (totalSize + content.length > maxTotal) {
|
|
989
|
+
const remaining = maxTotal - totalSize;
|
|
990
|
+
content = content.slice(0, remaining) + "\n// ... truncated ...";
|
|
991
|
+
}
|
|
992
|
+
result.push({ path: file, content });
|
|
993
|
+
totalSize += content.length;
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return result;
|
|
998
|
+
}
|
|
999
|
+
prioritizeFiles(files, count) {
|
|
1000
|
+
if (files.length <= count) return files;
|
|
1001
|
+
const priorityPatterns = [
|
|
1002
|
+
{ pattern: /package\.json$/, priority: 100 },
|
|
1003
|
+
{ pattern: /tsconfig\.json$/, priority: 90 },
|
|
1004
|
+
{ pattern: /README\.md$/i, priority: 80 },
|
|
1005
|
+
{ pattern: /\/index\.(ts|js|tsx|jsx)$/, priority: 70 },
|
|
1006
|
+
{ pattern: /\/app\.(ts|js|tsx|jsx)$/, priority: 70 },
|
|
1007
|
+
{ pattern: /\/main\.(ts|js|tsx|jsx)$/, priority: 70 },
|
|
1008
|
+
{ pattern: /\/api\//, priority: 60 },
|
|
1009
|
+
{ pattern: /\/routes?\//, priority: 55 },
|
|
1010
|
+
{ pattern: /\/pages\//, priority: 55 },
|
|
1011
|
+
{ pattern: /\/services?\//, priority: 50 },
|
|
1012
|
+
{ pattern: /\.(ts|tsx)$/, priority: 30 },
|
|
1013
|
+
{ pattern: /\.(js|jsx)$/, priority: 20 }
|
|
1014
|
+
];
|
|
1015
|
+
const scored = files.map((file) => {
|
|
1016
|
+
let score = 0;
|
|
1017
|
+
for (const { pattern, priority } of priorityPatterns) {
|
|
1018
|
+
if (pattern.test(file)) {
|
|
1019
|
+
score = Math.max(score, priority);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { file, score };
|
|
1023
|
+
});
|
|
1024
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1025
|
+
return scored.slice(0, count).map((s) => s.file);
|
|
1026
|
+
}
|
|
1027
|
+
// ─────────────────────────────────────────────────────────────
|
|
1028
|
+
// Prompt Builders (similar to Claude Code but adapted for Aider)
|
|
1029
|
+
// ─────────────────────────────────────────────────────────────
|
|
1030
|
+
buildAnalysisPrompt(fileContents, understanding, staticResults) {
|
|
1031
|
+
const filesSection = fileContents.map((f) => `=== ${f.path} ===
|
|
1032
|
+
${f.content}`).join("\n\n");
|
|
1033
|
+
const staticSignals = staticResults.length > 0 ? `
|
|
1034
|
+
Static analysis signals:
|
|
1035
|
+
${staticResults.slice(0, 50).map((r) => `- ${r.file}:${r.line}: ${r.message}`).join("\n")}` : "";
|
|
1036
|
+
return `Analyze the following code for bugs. This is a ${understanding.summary.type} application using ${understanding.summary.framework || "no specific framework"}.
|
|
1037
|
+
|
|
1038
|
+
${filesSection}
|
|
1039
|
+
${staticSignals}
|
|
1040
|
+
|
|
1041
|
+
Find bugs in these categories:
|
|
1042
|
+
1. Logic errors (off-by-one, wrong operators)
|
|
1043
|
+
2. Null/undefined dereference
|
|
1044
|
+
3. Security vulnerabilities
|
|
1045
|
+
4. Async/race conditions
|
|
1046
|
+
5. Edge cases not handled
|
|
1047
|
+
|
|
1048
|
+
Output as JSON array ONLY:
|
|
1049
|
+
[{"file": "path", "line": 42, "title": "Bug title", "description": "Description", "severity": "high", "category": "null-reference", "codePath": [{"step": 1, "file": "path", "line": 40, "code": "code", "explanation": "explanation"}], "evidence": ["evidence1"], "suggestedFix": "fix code"}]
|
|
1050
|
+
|
|
1051
|
+
If no bugs found, output: []`;
|
|
1052
|
+
}
|
|
1053
|
+
buildAdversarialPrompt(bug, fileContent) {
|
|
1054
|
+
return `Try to DISPROVE this bug report:
|
|
1055
|
+
|
|
1056
|
+
Bug: ${bug.title}
|
|
1057
|
+
File: ${bug.file}:${bug.line}
|
|
1058
|
+
Description: ${bug.description}
|
|
1059
|
+
|
|
1060
|
+
Code context:
|
|
1061
|
+
${fileContent}
|
|
1062
|
+
|
|
1063
|
+
Find reasons this is NOT a bug (guards, type checks, etc).
|
|
1064
|
+
|
|
1065
|
+
Output JSON ONLY:
|
|
1066
|
+
{"survived": true/false, "counterArguments": ["reason1", "reason2"], "confidence": "high/medium/low"}`;
|
|
1067
|
+
}
|
|
1068
|
+
buildUnderstandingPrompt(totalFiles, fileContents, packageJson) {
|
|
1069
|
+
const filesSection = fileContents.map((f) => `=== ${f.path} ===
|
|
1070
|
+
${f.content}`).join("\n\n");
|
|
1071
|
+
const depsSection = packageJson ? `
|
|
1072
|
+
Dependencies: ${JSON.stringify(packageJson.dependencies || {})}` : "";
|
|
1073
|
+
return `Analyze this codebase (${totalFiles} total files).
|
|
1074
|
+
${depsSection}
|
|
1075
|
+
|
|
1076
|
+
${filesSection}
|
|
1077
|
+
|
|
1078
|
+
Output JSON ONLY describing:
|
|
1079
|
+
{
|
|
1080
|
+
"summary": {"type": "app-type", "framework": "framework", "language": "typescript", "description": "description"},
|
|
1081
|
+
"features": [{"name": "Feature", "description": "desc", "priority": "critical", "constraints": ["constraint"], "relatedFiles": ["file"]}],
|
|
1082
|
+
"contracts": [{"function": "funcName", "file": "file.ts", "inputs": [{"name": "param", "type": "string"}], "outputs": {"type": "Result"}, "invariants": ["rule"], "sideEffects": ["effect"]}]
|
|
1083
|
+
}`;
|
|
1084
|
+
}
|
|
1085
|
+
// ─────────────────────────────────────────────────────────────
|
|
1086
|
+
// Aider CLI Execution
|
|
1087
|
+
// ─────────────────────────────────────────────────────────────
|
|
1088
|
+
async runAider(prompt, files, cwd) {
|
|
1089
|
+
const tempDir = mkdtempSync(join(tmpdir(), "whiterose-"));
|
|
1090
|
+
const promptFile = join(tempDir, "prompt.txt");
|
|
1091
|
+
try {
|
|
1092
|
+
writeFileSync(promptFile, prompt, "utf-8");
|
|
1093
|
+
const args = [
|
|
1094
|
+
"--no-auto-commits",
|
|
1095
|
+
"--no-git",
|
|
1096
|
+
"--yes",
|
|
1097
|
+
"--message-file",
|
|
1098
|
+
promptFile
|
|
1099
|
+
];
|
|
1100
|
+
for (const file of files.slice(0, 10)) {
|
|
1101
|
+
if (existsSync(file)) {
|
|
1102
|
+
args.push(file);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const aiderCommand = getProviderCommand("aider");
|
|
1106
|
+
const { stdout, stderr } = await execa(aiderCommand, args, {
|
|
1107
|
+
cwd,
|
|
1108
|
+
timeout: AIDER_TIMEOUT,
|
|
1109
|
+
env: {
|
|
1110
|
+
...process.env,
|
|
1111
|
+
NO_COLOR: "1"
|
|
1112
|
+
},
|
|
1113
|
+
reject: false
|
|
1114
|
+
});
|
|
1115
|
+
return stdout || stderr || "";
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
if (error.stdout) {
|
|
1118
|
+
return error.stdout;
|
|
1119
|
+
}
|
|
1120
|
+
if (error.message?.includes("ENOENT")) {
|
|
1121
|
+
throw new Error("Aider not found. Install it with: pip install aider-chat");
|
|
1122
|
+
}
|
|
1123
|
+
throw new Error(`Aider failed: ${error.message}`);
|
|
1124
|
+
} finally {
|
|
1125
|
+
try {
|
|
1126
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// ─────────────────────────────────────────────────────────────
|
|
1132
|
+
// Response Parsers
|
|
1133
|
+
// ─────────────────────────────────────────────────────────────
|
|
1134
|
+
parseAnalysisResponse(response, files) {
|
|
1135
|
+
try {
|
|
1136
|
+
const json = this.extractJson(response);
|
|
1137
|
+
if (!json) return [];
|
|
1138
|
+
const parsed = JSON.parse(json);
|
|
1139
|
+
if (!Array.isArray(parsed)) return [];
|
|
1140
|
+
const bugs = [];
|
|
1141
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
1142
|
+
const item = parsed[i];
|
|
1143
|
+
if (!item.file || !item.line || !item.title) continue;
|
|
1144
|
+
let filePath = item.file;
|
|
1145
|
+
if (!filePath.startsWith("/")) {
|
|
1146
|
+
const match = files.find((f) => f.endsWith(filePath) || f.includes(filePath));
|
|
1147
|
+
if (match) filePath = match;
|
|
1148
|
+
}
|
|
1149
|
+
const codePath = (item.codePath || []).map(
|
|
1150
|
+
(step, idx) => ({
|
|
1151
|
+
step: step.step || idx + 1,
|
|
1152
|
+
file: step.file || filePath,
|
|
1153
|
+
line: step.line || item.line,
|
|
1154
|
+
code: step.code || "",
|
|
1155
|
+
explanation: step.explanation || ""
|
|
1156
|
+
})
|
|
1157
|
+
);
|
|
1158
|
+
bugs.push({
|
|
1159
|
+
id: generateBugId(i),
|
|
1160
|
+
title: String(item.title).slice(0, 100),
|
|
1161
|
+
description: String(item.description || ""),
|
|
1162
|
+
file: filePath,
|
|
1163
|
+
line: Number(item.line) || 0,
|
|
1164
|
+
endLine: item.endLine ? Number(item.endLine) : void 0,
|
|
1165
|
+
severity: this.parseSeverity(item.severity),
|
|
1166
|
+
category: this.parseCategory(item.category),
|
|
1167
|
+
confidence: {
|
|
1168
|
+
overall: "medium",
|
|
1169
|
+
codePathValidity: 0.75,
|
|
1170
|
+
reachability: 0.75,
|
|
1171
|
+
intentViolation: false,
|
|
1172
|
+
staticToolSignal: false,
|
|
1173
|
+
adversarialSurvived: false
|
|
1174
|
+
},
|
|
1175
|
+
codePath,
|
|
1176
|
+
evidence: Array.isArray(item.evidence) ? item.evidence.map(String) : [],
|
|
1177
|
+
suggestedFix: item.suggestedFix ? String(item.suggestedFix) : void 0,
|
|
1178
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
return bugs;
|
|
1182
|
+
} catch {
|
|
1183
|
+
return [];
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
parseAdversarialResponse(response, bug) {
|
|
1187
|
+
try {
|
|
1188
|
+
const json = this.extractJson(response);
|
|
1189
|
+
if (!json) return { survived: true, counterArguments: [] };
|
|
1190
|
+
const parsed = JSON.parse(json);
|
|
1191
|
+
const survived = parsed.survived !== false;
|
|
1192
|
+
return {
|
|
1193
|
+
survived,
|
|
1194
|
+
counterArguments: Array.isArray(parsed.counterArguments) ? parsed.counterArguments.map(String) : [],
|
|
1195
|
+
adjustedConfidence: survived ? {
|
|
1196
|
+
...bug.confidence,
|
|
1197
|
+
overall: this.parseConfidence(parsed.confidence),
|
|
1198
|
+
adversarialSurvived: true
|
|
1199
|
+
} : void 0
|
|
1200
|
+
};
|
|
1201
|
+
} catch {
|
|
1202
|
+
return { survived: true, counterArguments: [] };
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
parseUnderstandingResponse(response, files) {
|
|
1206
|
+
try {
|
|
1207
|
+
const json = this.extractJson(response);
|
|
1208
|
+
if (!json) throw new Error("No JSON found");
|
|
1209
|
+
const parsed = JSON.parse(json);
|
|
1210
|
+
let totalLines = 0;
|
|
1211
|
+
for (const file of files.slice(0, 50)) {
|
|
1212
|
+
try {
|
|
1213
|
+
totalLines += readFileSync(file, "utf-8").split("\n").length;
|
|
1214
|
+
} catch {
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return {
|
|
1218
|
+
version: "1",
|
|
1219
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1220
|
+
summary: {
|
|
1221
|
+
type: parsed.summary?.type || "unknown",
|
|
1222
|
+
framework: parsed.summary?.framework,
|
|
1223
|
+
language: parsed.summary?.language || "typescript",
|
|
1224
|
+
description: parsed.summary?.description || "No description available"
|
|
1225
|
+
},
|
|
1226
|
+
features: (parsed.features || []).map((f) => ({
|
|
1227
|
+
name: f.name || "Unknown",
|
|
1228
|
+
description: f.description || "",
|
|
1229
|
+
priority: f.priority || "medium",
|
|
1230
|
+
constraints: Array.isArray(f.constraints) ? f.constraints : [],
|
|
1231
|
+
relatedFiles: Array.isArray(f.relatedFiles) ? f.relatedFiles : []
|
|
1232
|
+
})),
|
|
1233
|
+
contracts: (parsed.contracts || []).map((c) => ({
|
|
1234
|
+
function: c.function || "unknown",
|
|
1235
|
+
file: c.file || "unknown",
|
|
1236
|
+
inputs: Array.isArray(c.inputs) ? c.inputs : [],
|
|
1237
|
+
outputs: c.outputs || { type: "unknown" },
|
|
1238
|
+
invariants: Array.isArray(c.invariants) ? c.invariants : [],
|
|
1239
|
+
sideEffects: Array.isArray(c.sideEffects) ? c.sideEffects : []
|
|
1240
|
+
})),
|
|
1241
|
+
dependencies: {},
|
|
1242
|
+
structure: {
|
|
1243
|
+
totalFiles: files.length,
|
|
1244
|
+
totalLines
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
} catch {
|
|
1248
|
+
return {
|
|
1249
|
+
version: "1",
|
|
1250
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1251
|
+
summary: {
|
|
1252
|
+
type: "unknown",
|
|
1253
|
+
language: "typescript",
|
|
1254
|
+
description: "Failed to analyze codebase"
|
|
1255
|
+
},
|
|
1256
|
+
features: [],
|
|
1257
|
+
contracts: [],
|
|
1258
|
+
dependencies: {},
|
|
1259
|
+
structure: {
|
|
1260
|
+
totalFiles: files.length,
|
|
1261
|
+
totalLines: 0
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
// ─────────────────────────────────────────────────────────────
|
|
1267
|
+
// Utilities
|
|
1268
|
+
// ─────────────────────────────────────────────────────────────
|
|
1269
|
+
extractJson(text3) {
|
|
1270
|
+
const codeBlockMatch = text3.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1271
|
+
if (codeBlockMatch) return codeBlockMatch[1].trim();
|
|
1272
|
+
const arrayMatch = text3.match(/\[[\s\S]*\]/);
|
|
1273
|
+
if (arrayMatch) return arrayMatch[0];
|
|
1274
|
+
const objectMatch = text3.match(/\{[\s\S]*\}/);
|
|
1275
|
+
if (objectMatch) return objectMatch[0];
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
parseSeverity(value) {
|
|
1279
|
+
const str = String(value).toLowerCase();
|
|
1280
|
+
if (["critical", "high", "medium", "low"].includes(str)) {
|
|
1281
|
+
return str;
|
|
1282
|
+
}
|
|
1283
|
+
return "medium";
|
|
1284
|
+
}
|
|
1285
|
+
parseCategory(value) {
|
|
1286
|
+
const str = String(value).toLowerCase().replace(/_/g, "-");
|
|
1287
|
+
const validCategories = [
|
|
1288
|
+
"logic-error",
|
|
1289
|
+
"security",
|
|
1290
|
+
"async-race-condition",
|
|
1291
|
+
"edge-case",
|
|
1292
|
+
"null-reference",
|
|
1293
|
+
"type-coercion",
|
|
1294
|
+
"resource-leak",
|
|
1295
|
+
"intent-violation"
|
|
1296
|
+
];
|
|
1297
|
+
if (validCategories.includes(str)) {
|
|
1298
|
+
return str;
|
|
1299
|
+
}
|
|
1300
|
+
if (str.includes("null") || str.includes("undefined")) return "null-reference";
|
|
1301
|
+
if (str.includes("security")) return "security";
|
|
1302
|
+
if (str.includes("async") || str.includes("race")) return "async-race-condition";
|
|
1303
|
+
return "logic-error";
|
|
1304
|
+
}
|
|
1305
|
+
parseConfidence(value) {
|
|
1306
|
+
const str = String(value).toLowerCase();
|
|
1307
|
+
if (["high", "medium", "low"].includes(str)) {
|
|
1308
|
+
return str;
|
|
1309
|
+
}
|
|
1310
|
+
return "medium";
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
// src/providers/index.ts
|
|
1315
|
+
var providers = {
|
|
1316
|
+
"claude-code": () => new ClaudeCodeProvider(),
|
|
1317
|
+
aider: () => new AiderProvider(),
|
|
1318
|
+
codex: () => {
|
|
1319
|
+
throw new Error("Codex provider not yet implemented");
|
|
1320
|
+
},
|
|
1321
|
+
opencode: () => {
|
|
1322
|
+
throw new Error("OpenCode provider not yet implemented");
|
|
1323
|
+
},
|
|
1324
|
+
ollama: () => {
|
|
1325
|
+
throw new Error("Ollama provider not yet implemented");
|
|
1326
|
+
},
|
|
1327
|
+
gemini: () => {
|
|
1328
|
+
throw new Error("Gemini provider not yet implemented");
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
async function getProvider(name) {
|
|
1332
|
+
const factory = providers[name];
|
|
1333
|
+
if (!factory) {
|
|
1334
|
+
throw new Error(`Unknown provider: ${name}`);
|
|
1335
|
+
}
|
|
1336
|
+
const provider = factory();
|
|
1337
|
+
const available = await provider.isAvailable();
|
|
1338
|
+
if (!available) {
|
|
1339
|
+
throw new Error(`Provider ${name} is not available. Make sure it's installed and configured.`);
|
|
1340
|
+
}
|
|
1341
|
+
return provider;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/core/scanner/index.ts
|
|
1345
|
+
var DEFAULT_INCLUDE = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"];
|
|
1346
|
+
var DEFAULT_EXCLUDE = [
|
|
1347
|
+
"node_modules/**",
|
|
1348
|
+
"dist/**",
|
|
1349
|
+
"build/**",
|
|
1350
|
+
".next/**",
|
|
1351
|
+
"coverage/**",
|
|
1352
|
+
"**/*.test.*",
|
|
1353
|
+
"**/*.spec.*",
|
|
1354
|
+
"**/*.d.ts",
|
|
1355
|
+
".whiterose/**"
|
|
1356
|
+
];
|
|
1357
|
+
async function scanCodebase(cwd, config) {
|
|
1358
|
+
const include = config?.include || DEFAULT_INCLUDE;
|
|
1359
|
+
const exclude = config?.exclude || DEFAULT_EXCLUDE;
|
|
1360
|
+
const files = await fg3(include, {
|
|
1361
|
+
cwd,
|
|
1362
|
+
ignore: exclude,
|
|
1363
|
+
absolute: true,
|
|
1364
|
+
onlyFiles: true
|
|
1365
|
+
});
|
|
1366
|
+
return files.sort();
|
|
1367
|
+
}
|
|
1368
|
+
function hashFile(filePath) {
|
|
1369
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1370
|
+
return createHash("md5").update(content).digest("hex");
|
|
1371
|
+
}
|
|
1372
|
+
async function getChangedFiles(cwd, config) {
|
|
1373
|
+
const cachePath = join(cwd, ".whiterose", "cache", "file-hashes.json");
|
|
1374
|
+
const currentFiles = await scanCodebase(cwd, config);
|
|
1375
|
+
let cachedState = {
|
|
1376
|
+
fileHashes: []
|
|
1377
|
+
};
|
|
1378
|
+
if (existsSync(cachePath)) {
|
|
1379
|
+
cachedState = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
1380
|
+
}
|
|
1381
|
+
const cachedHashes = new Map(cachedState.fileHashes.map((h) => [h.path, h.hash]));
|
|
1382
|
+
const changedFiles = [];
|
|
1383
|
+
const newHashes = [];
|
|
1384
|
+
for (const file of currentFiles) {
|
|
1385
|
+
const relativePath = relative(cwd, file);
|
|
1386
|
+
const currentHash = hashFile(file);
|
|
1387
|
+
newHashes.push({
|
|
1388
|
+
path: relativePath,
|
|
1389
|
+
hash: currentHash,
|
|
1390
|
+
lastModified: (/* @__PURE__ */ new Date()).toISOString()
|
|
1391
|
+
});
|
|
1392
|
+
const cachedHash = cachedHashes.get(relativePath);
|
|
1393
|
+
if (!cachedHash || cachedHash !== currentHash) {
|
|
1394
|
+
changedFiles.push(file);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
const newState = {
|
|
1398
|
+
version: "1",
|
|
1399
|
+
lastIncrementalScan: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1400
|
+
lastFullScan: cachedState.lastFullScan,
|
|
1401
|
+
fileHashes: newHashes
|
|
1402
|
+
};
|
|
1403
|
+
writeFileSync(cachePath, JSON.stringify(newState, null, 2), "utf-8");
|
|
1404
|
+
return { files: changedFiles, hashes: newHashes };
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/core/contracts/intent.ts
|
|
1408
|
+
function generateIntentDocument(understanding) {
|
|
1409
|
+
const lines = [];
|
|
1410
|
+
lines.push(`# App Intent: ${understanding.summary.type}`);
|
|
1411
|
+
lines.push("");
|
|
1412
|
+
lines.push(`> Generated by whiterose on ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1413
|
+
lines.push("> Edit the sections above the line. Contracts below are auto-generated.");
|
|
1414
|
+
lines.push("");
|
|
1415
|
+
lines.push("## Overview");
|
|
1416
|
+
lines.push("");
|
|
1417
|
+
lines.push(understanding.summary.description);
|
|
1418
|
+
lines.push("");
|
|
1419
|
+
lines.push(`- **Framework:** ${understanding.summary.framework || "None detected"}`);
|
|
1420
|
+
lines.push(`- **Language:** ${understanding.summary.language}`);
|
|
1421
|
+
lines.push(`- **Files:** ${understanding.structure.totalFiles}`);
|
|
1422
|
+
if (understanding.structure.packages?.length) {
|
|
1423
|
+
lines.push(`- **Packages:** ${understanding.structure.packages.join(", ")}`);
|
|
1424
|
+
}
|
|
1425
|
+
lines.push("");
|
|
1426
|
+
if (understanding.features.length > 0) {
|
|
1427
|
+
lines.push("## Critical Features");
|
|
1428
|
+
lines.push("");
|
|
1429
|
+
const criticalFeatures = understanding.features.filter((f) => f.priority === "critical");
|
|
1430
|
+
const highFeatures = understanding.features.filter((f) => f.priority === "high");
|
|
1431
|
+
const otherFeatures = understanding.features.filter(
|
|
1432
|
+
(f) => f.priority !== "critical" && f.priority !== "high"
|
|
1433
|
+
);
|
|
1434
|
+
for (const feature of criticalFeatures) {
|
|
1435
|
+
lines.push(formatFeature(feature, "CRITICAL"));
|
|
1436
|
+
}
|
|
1437
|
+
if (highFeatures.length > 0) {
|
|
1438
|
+
lines.push("## High Priority Features");
|
|
1439
|
+
lines.push("");
|
|
1440
|
+
for (const feature of highFeatures) {
|
|
1441
|
+
lines.push(formatFeature(feature, "HIGH"));
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (otherFeatures.length > 0) {
|
|
1445
|
+
lines.push("## Other Features");
|
|
1446
|
+
lines.push("");
|
|
1447
|
+
for (const feature of otherFeatures) {
|
|
1448
|
+
lines.push(formatFeature(feature));
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
lines.push("## Known Constraints");
|
|
1453
|
+
lines.push("");
|
|
1454
|
+
lines.push("<!-- Add any known constraints or business rules here -->");
|
|
1455
|
+
lines.push("");
|
|
1456
|
+
lines.push("- (Add your constraints here)");
|
|
1457
|
+
lines.push("");
|
|
1458
|
+
lines.push("## Areas of Concern");
|
|
1459
|
+
lines.push("");
|
|
1460
|
+
lines.push("<!-- Add files or areas that need extra scrutiny -->");
|
|
1461
|
+
lines.push("");
|
|
1462
|
+
lines.push("- (Add files that have had bugs before)");
|
|
1463
|
+
lines.push("");
|
|
1464
|
+
lines.push("---");
|
|
1465
|
+
lines.push("");
|
|
1466
|
+
lines.push("<!-- \u26A0\uFE0F DO NOT EDIT BELOW THIS LINE - Auto-generated contracts -->");
|
|
1467
|
+
lines.push("");
|
|
1468
|
+
if (understanding.contracts.length > 0) {
|
|
1469
|
+
lines.push("## Behavioral Contracts");
|
|
1470
|
+
lines.push("");
|
|
1471
|
+
for (const contract of understanding.contracts) {
|
|
1472
|
+
lines.push(formatContract(contract));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return lines.join("\n");
|
|
1476
|
+
}
|
|
1477
|
+
function formatFeature(feature, badge) {
|
|
1478
|
+
const lines = [];
|
|
1479
|
+
const badgeStr = badge ? ` [${badge}]` : "";
|
|
1480
|
+
lines.push(`### ${feature.name}${badgeStr}`);
|
|
1481
|
+
lines.push("");
|
|
1482
|
+
lines.push(feature.description);
|
|
1483
|
+
lines.push("");
|
|
1484
|
+
if (feature.constraints.length > 0) {
|
|
1485
|
+
lines.push("**Constraints:**");
|
|
1486
|
+
for (const constraint of feature.constraints) {
|
|
1487
|
+
lines.push(`- ${constraint}`);
|
|
1488
|
+
}
|
|
1489
|
+
lines.push("");
|
|
1490
|
+
}
|
|
1491
|
+
if (feature.relatedFiles.length > 0) {
|
|
1492
|
+
lines.push(`**Files:** \`${feature.relatedFiles.join("`, `")}\``);
|
|
1493
|
+
lines.push("");
|
|
1494
|
+
}
|
|
1495
|
+
return lines.join("\n");
|
|
1496
|
+
}
|
|
1497
|
+
function formatContract(contract) {
|
|
1498
|
+
const lines = [];
|
|
1499
|
+
lines.push(`### \`${contract.file}:${contract.function}()\``);
|
|
1500
|
+
lines.push("");
|
|
1501
|
+
if (contract.inputs.length > 0) {
|
|
1502
|
+
lines.push("**Inputs:**");
|
|
1503
|
+
for (const input of contract.inputs) {
|
|
1504
|
+
const constraints = input.constraints ? ` (${input.constraints})` : "";
|
|
1505
|
+
lines.push(`- \`${input.name}\`: ${input.type}${constraints}`);
|
|
1506
|
+
}
|
|
1507
|
+
lines.push("");
|
|
1508
|
+
}
|
|
1509
|
+
lines.push("**Returns:** `" + contract.outputs.type + "`");
|
|
1510
|
+
if (contract.outputs.constraints) {
|
|
1511
|
+
lines.push(` - ${contract.outputs.constraints}`);
|
|
1512
|
+
}
|
|
1513
|
+
lines.push("");
|
|
1514
|
+
if (contract.invariants.length > 0) {
|
|
1515
|
+
lines.push("**Invariants:**");
|
|
1516
|
+
for (const invariant of contract.invariants) {
|
|
1517
|
+
lines.push(`- ${invariant}`);
|
|
1518
|
+
}
|
|
1519
|
+
lines.push("");
|
|
1520
|
+
}
|
|
1521
|
+
if (contract.sideEffects.length > 0) {
|
|
1522
|
+
lines.push("**Side Effects:**");
|
|
1523
|
+
for (const effect of contract.sideEffects) {
|
|
1524
|
+
lines.push(`- ${effect}`);
|
|
1525
|
+
}
|
|
1526
|
+
lines.push("");
|
|
1527
|
+
}
|
|
1528
|
+
if (contract.throws && contract.throws.length > 0) {
|
|
1529
|
+
lines.push("**Throws:**");
|
|
1530
|
+
for (const t of contract.throws) {
|
|
1531
|
+
lines.push(`- ${t}`);
|
|
1532
|
+
}
|
|
1533
|
+
lines.push("");
|
|
1534
|
+
}
|
|
1535
|
+
return lines.join("\n");
|
|
1536
|
+
}
|
|
1537
|
+
async function readExistingDocs(cwd) {
|
|
1538
|
+
const docs = {
|
|
1539
|
+
readme: null,
|
|
1540
|
+
contributing: null,
|
|
1541
|
+
apiDocs: [],
|
|
1542
|
+
changelog: null,
|
|
1543
|
+
packageJson: null,
|
|
1544
|
+
tsconfig: null,
|
|
1545
|
+
envExample: null,
|
|
1546
|
+
otherDocs: []
|
|
1547
|
+
};
|
|
1548
|
+
for (const name of ["README.md", "readme.md", "README", "Readme.md"]) {
|
|
1549
|
+
const path = join(cwd, name);
|
|
1550
|
+
if (existsSync(path)) {
|
|
1551
|
+
docs.readme = readFileSync(path, "utf-8");
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
for (const name of ["CONTRIBUTING.md", "contributing.md", "CONTRIBUTING"]) {
|
|
1556
|
+
const path = join(cwd, name);
|
|
1557
|
+
if (existsSync(path)) {
|
|
1558
|
+
docs.contributing = readFileSync(path, "utf-8");
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
for (const name of ["CHANGELOG.md", "changelog.md", "CHANGELOG", "HISTORY.md"]) {
|
|
1563
|
+
const path = join(cwd, name);
|
|
1564
|
+
if (existsSync(path)) {
|
|
1565
|
+
docs.changelog = readFileSync(path, "utf-8");
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
1570
|
+
if (existsSync(packageJsonPath)) {
|
|
1571
|
+
try {
|
|
1572
|
+
docs.packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
const tsconfigPath = join(cwd, "tsconfig.json");
|
|
1577
|
+
if (existsSync(tsconfigPath)) {
|
|
1578
|
+
try {
|
|
1579
|
+
docs.tsconfig = JSON.parse(readFileSync(tsconfigPath, "utf-8"));
|
|
1580
|
+
} catch {
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
for (const name of [".env.example", ".env.sample", ".env.template"]) {
|
|
1584
|
+
const path = join(cwd, name);
|
|
1585
|
+
if (existsSync(path)) {
|
|
1586
|
+
docs.envExample = readFileSync(path, "utf-8");
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const apiDocPaths = await fg3(["docs/**/*.md", "documentation/**/*.md", "api/**/*.md"], {
|
|
1591
|
+
cwd,
|
|
1592
|
+
absolute: true,
|
|
1593
|
+
ignore: ["**/node_modules/**"]
|
|
1594
|
+
});
|
|
1595
|
+
for (const path of apiDocPaths.slice(0, 10)) {
|
|
1596
|
+
try {
|
|
1597
|
+
docs.apiDocs.push(readFileSync(path, "utf-8"));
|
|
1598
|
+
} catch {
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
const otherDocPaths = await fg3(["*.md", "docs/*.md"], {
|
|
1602
|
+
cwd,
|
|
1603
|
+
absolute: true,
|
|
1604
|
+
ignore: ["**/node_modules/**", "README.md", "CONTRIBUTING.md", "CHANGELOG.md"]
|
|
1605
|
+
});
|
|
1606
|
+
for (const path of otherDocPaths.slice(0, 5)) {
|
|
1607
|
+
try {
|
|
1608
|
+
docs.otherDocs.push({
|
|
1609
|
+
name: basename(path),
|
|
1610
|
+
content: readFileSync(path, "utf-8")
|
|
1611
|
+
});
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return docs;
|
|
1616
|
+
}
|
|
1617
|
+
function extractIntentFromDocs(docs) {
|
|
1618
|
+
const intent = {
|
|
1619
|
+
projectName: "",
|
|
1620
|
+
description: "",
|
|
1621
|
+
features: [],
|
|
1622
|
+
techStack: [],
|
|
1623
|
+
conventions: [],
|
|
1624
|
+
apiEndpoints: [],
|
|
1625
|
+
envVariables: [],
|
|
1626
|
+
scripts: {}
|
|
1627
|
+
};
|
|
1628
|
+
if (docs.packageJson) {
|
|
1629
|
+
intent.projectName = docs.packageJson.name || "";
|
|
1630
|
+
intent.description = docs.packageJson.description || "";
|
|
1631
|
+
intent.scripts = docs.packageJson.scripts || {};
|
|
1632
|
+
const deps = {
|
|
1633
|
+
...docs.packageJson.dependencies,
|
|
1634
|
+
...docs.packageJson.devDependencies
|
|
1635
|
+
};
|
|
1636
|
+
if (deps) {
|
|
1637
|
+
if (deps["next"]) intent.techStack.push("Next.js");
|
|
1638
|
+
if (deps["react"]) intent.techStack.push("React");
|
|
1639
|
+
if (deps["vue"]) intent.techStack.push("Vue");
|
|
1640
|
+
if (deps["express"]) intent.techStack.push("Express");
|
|
1641
|
+
if (deps["fastify"]) intent.techStack.push("Fastify");
|
|
1642
|
+
if (deps["typescript"]) intent.techStack.push("TypeScript");
|
|
1643
|
+
if (deps["prisma"]) intent.techStack.push("Prisma");
|
|
1644
|
+
if (deps["mongoose"]) intent.techStack.push("MongoDB/Mongoose");
|
|
1645
|
+
if (deps["pg"] || deps["postgres"]) intent.techStack.push("PostgreSQL");
|
|
1646
|
+
if (deps["redis"]) intent.techStack.push("Redis");
|
|
1647
|
+
if (deps["stripe"]) intent.techStack.push("Stripe");
|
|
1648
|
+
if (deps["@auth/core"] || deps["next-auth"]) intent.techStack.push("Auth.js");
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
if (docs.readme) {
|
|
1652
|
+
const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n---|\$)/i);
|
|
1653
|
+
if (featuresMatch) {
|
|
1654
|
+
const featureLines = featuresMatch[1].split("\n").filter((line) => line.trim().startsWith("-") || line.trim().startsWith("*")).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter((line) => line.length > 0);
|
|
1655
|
+
intent.features.push(...featureLines.slice(0, 20));
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (docs.envExample) {
|
|
1659
|
+
const envLines = docs.envExample.split("\n").filter((line) => line.includes("=") && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter((line) => line.length > 0);
|
|
1660
|
+
intent.envVariables.push(...envLines);
|
|
1661
|
+
}
|
|
1662
|
+
for (const apiDoc of docs.apiDocs) {
|
|
1663
|
+
const endpointMatches = apiDoc.matchAll(/`(GET|POST|PUT|DELETE|PATCH)\s+([^`]+)`/g);
|
|
1664
|
+
for (const match of endpointMatches) {
|
|
1665
|
+
intent.apiEndpoints.push(`${match[1]} ${match[2]}`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (docs.contributing) {
|
|
1669
|
+
const conventionLines = docs.contributing.split("\n").filter((line) => line.trim().startsWith("-") || line.trim().startsWith("*")).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter((line) => line.length > 10 && line.length < 200).slice(0, 10);
|
|
1670
|
+
intent.conventions.push(...conventionLines);
|
|
1671
|
+
}
|
|
1672
|
+
return intent;
|
|
1673
|
+
}
|
|
1674
|
+
function buildDocsSummary(docs, extracted) {
|
|
1675
|
+
const parts = [];
|
|
1676
|
+
parts.push(`# Existing Documentation Summary
|
|
1677
|
+
`);
|
|
1678
|
+
if (extracted.projectName) {
|
|
1679
|
+
parts.push(`## Project: ${extracted.projectName}`);
|
|
1680
|
+
if (extracted.description) {
|
|
1681
|
+
parts.push(`
|
|
1682
|
+
${extracted.description}
|
|
1683
|
+
`);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (extracted.techStack.length > 0) {
|
|
1687
|
+
parts.push(`
|
|
1688
|
+
## Tech Stack`);
|
|
1689
|
+
parts.push(extracted.techStack.map((t) => `- ${t}`).join("\n"));
|
|
1690
|
+
}
|
|
1691
|
+
if (extracted.features.length > 0) {
|
|
1692
|
+
parts.push(`
|
|
1693
|
+
## Features (from README)`);
|
|
1694
|
+
parts.push(extracted.features.map((f) => `- ${f}`).join("\n"));
|
|
1695
|
+
}
|
|
1696
|
+
if (extracted.apiEndpoints.length > 0) {
|
|
1697
|
+
parts.push(`
|
|
1698
|
+
## API Endpoints`);
|
|
1699
|
+
parts.push(extracted.apiEndpoints.slice(0, 20).map((e) => `- ${e}`).join("\n"));
|
|
1700
|
+
}
|
|
1701
|
+
if (extracted.envVariables.length > 0) {
|
|
1702
|
+
parts.push(`
|
|
1703
|
+
## Environment Variables`);
|
|
1704
|
+
parts.push(extracted.envVariables.map((v) => `- ${v}`).join("\n"));
|
|
1705
|
+
}
|
|
1706
|
+
if (extracted.conventions.length > 0) {
|
|
1707
|
+
parts.push(`
|
|
1708
|
+
## Conventions (from CONTRIBUTING)`);
|
|
1709
|
+
parts.push(extracted.conventions.map((c) => `- ${c}`).join("\n"));
|
|
1710
|
+
}
|
|
1711
|
+
if (Object.keys(extracted.scripts).length > 0) {
|
|
1712
|
+
parts.push(`
|
|
1713
|
+
## NPM Scripts`);
|
|
1714
|
+
for (const [name, cmd] of Object.entries(extracted.scripts).slice(0, 10)) {
|
|
1715
|
+
parts.push(`- \`npm run ${name}\`: ${cmd}`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (docs.readme && docs.readme.length > 0) {
|
|
1719
|
+
const excerpt = docs.readme.slice(0, 2e3);
|
|
1720
|
+
parts.push(`
|
|
1721
|
+
## README Excerpt
|
|
1722
|
+
\`\`\`
|
|
1723
|
+
${excerpt}
|
|
1724
|
+
\`\`\``);
|
|
1725
|
+
}
|
|
1726
|
+
return parts.join("\n");
|
|
1727
|
+
}
|
|
1728
|
+
async function initCommand(options) {
|
|
1729
|
+
const cwd = process.cwd();
|
|
1730
|
+
const whiterosePath = join(cwd, ".whiterose");
|
|
1731
|
+
if (existsSync(whiterosePath) && !options.force) {
|
|
1732
|
+
p3.log.error("whiterose is already initialized in this directory.");
|
|
1733
|
+
p3.log.info('Use --force to reinitialize, or run "whiterose refresh" to update understanding.');
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - initialization"));
|
|
1737
|
+
const providerSpinner = p3.spinner();
|
|
1738
|
+
providerSpinner.start("Detecting available LLM providers...");
|
|
1739
|
+
const availableProviders = await detectProvider();
|
|
1740
|
+
if (availableProviders.length === 0) {
|
|
1741
|
+
providerSpinner.stop("No LLM providers detected");
|
|
1742
|
+
p3.log.error("whiterose requires an LLM provider to function.");
|
|
1743
|
+
p3.log.info("Supported providers: claude-code, aider, codex, opencode");
|
|
1744
|
+
p3.log.info("Install one and ensure it's configured, then run init again.");
|
|
1745
|
+
process.exit(1);
|
|
1746
|
+
}
|
|
1747
|
+
providerSpinner.stop(`Detected providers: ${availableProviders.join(", ")}`);
|
|
1748
|
+
let selectedProvider;
|
|
1749
|
+
if (options.skipQuestions) {
|
|
1750
|
+
selectedProvider = availableProviders[0];
|
|
1751
|
+
p3.log.info(`Using ${selectedProvider} as your LLM provider.`);
|
|
1752
|
+
} else {
|
|
1753
|
+
const providerChoice = await p3.select({
|
|
1754
|
+
message: "Which LLM provider should whiterose use?",
|
|
1755
|
+
options: availableProviders.map((prov) => ({
|
|
1756
|
+
value: prov,
|
|
1757
|
+
label: prov,
|
|
1758
|
+
hint: prov === "claude-code" ? "recommended" : void 0
|
|
1759
|
+
}))
|
|
1760
|
+
});
|
|
1761
|
+
if (p3.isCancel(providerChoice)) {
|
|
1762
|
+
p3.cancel("Initialization cancelled.");
|
|
1763
|
+
process.exit(0);
|
|
1764
|
+
}
|
|
1765
|
+
selectedProvider = providerChoice;
|
|
1766
|
+
}
|
|
1767
|
+
const verifySpinner = p3.spinner();
|
|
1768
|
+
verifySpinner.start("Verifying provider CLI works...");
|
|
1769
|
+
try {
|
|
1770
|
+
const command = getProviderCommand(selectedProvider);
|
|
1771
|
+
await execa(command, ["--version"], { timeout: 1e4 });
|
|
1772
|
+
verifySpinner.stop(`Using ${selectedProvider} at: ${command}`);
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
verifySpinner.stop("Provider CLI verification failed");
|
|
1775
|
+
const installHint = selectedProvider === "claude-code" ? "npm install -g @anthropic-ai/claude-code" : `Install ${selectedProvider} and ensure it's in your PATH`;
|
|
1776
|
+
p3.log.error(`Cannot run ${selectedProvider} CLI. ${installHint}`);
|
|
1777
|
+
p3.log.info(`Resolved path: ${getProviderCommand(selectedProvider)}`);
|
|
1778
|
+
if (error.message) {
|
|
1779
|
+
p3.log.info(`Error: ${error.message}`);
|
|
1780
|
+
}
|
|
1781
|
+
process.exit(1);
|
|
1782
|
+
}
|
|
1783
|
+
const scanSpinner = p3.spinner();
|
|
1784
|
+
scanSpinner.start("Scanning codebase...");
|
|
1785
|
+
let codebaseFiles;
|
|
1786
|
+
try {
|
|
1787
|
+
codebaseFiles = await scanCodebase(cwd);
|
|
1788
|
+
scanSpinner.stop(`Found ${codebaseFiles.length} source files`);
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
scanSpinner.stop("Failed to scan codebase");
|
|
1791
|
+
p3.log.error(String(error));
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
const docsSpinner = p3.spinner();
|
|
1795
|
+
docsSpinner.start("Reading existing documentation...");
|
|
1796
|
+
let docsSummary;
|
|
1797
|
+
try {
|
|
1798
|
+
const existingDocs = await readExistingDocs(cwd);
|
|
1799
|
+
const extractedIntent = extractIntentFromDocs(existingDocs);
|
|
1800
|
+
docsSummary = buildDocsSummary(existingDocs, extractedIntent);
|
|
1801
|
+
const docsFound = [];
|
|
1802
|
+
if (existingDocs.readme) docsFound.push("README");
|
|
1803
|
+
if (existingDocs.contributing) docsFound.push("CONTRIBUTING");
|
|
1804
|
+
if (existingDocs.packageJson) docsFound.push("package.json");
|
|
1805
|
+
if (existingDocs.envExample) docsFound.push(".env.example");
|
|
1806
|
+
if (existingDocs.apiDocs.length > 0) docsFound.push(`${existingDocs.apiDocs.length} API docs`);
|
|
1807
|
+
if (docsFound.length > 0) {
|
|
1808
|
+
docsSpinner.stop(`Found existing docs: ${docsFound.join(", ")}`);
|
|
1809
|
+
} else {
|
|
1810
|
+
docsSpinner.stop("No existing documentation found (will generate from code)");
|
|
1811
|
+
docsSummary = void 0;
|
|
1812
|
+
}
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
docsSpinner.stop("Could not read existing docs (continuing without)");
|
|
1815
|
+
docsSummary = void 0;
|
|
1816
|
+
}
|
|
1817
|
+
const understandingSpinner = p3.spinner();
|
|
1818
|
+
const startTime = Date.now();
|
|
1819
|
+
understandingSpinner.start("Starting codebase analysis...");
|
|
1820
|
+
let understanding;
|
|
1821
|
+
try {
|
|
1822
|
+
const provider = await getProvider(selectedProvider);
|
|
1823
|
+
if (options.unsafe && "setUnsafeMode" in provider) {
|
|
1824
|
+
provider.setUnsafeMode(true);
|
|
1825
|
+
p3.log.warn("Running in unsafe mode (--unsafe). LLM permission prompts are bypassed.");
|
|
1826
|
+
}
|
|
1827
|
+
if ("setProgressCallback" in provider) {
|
|
1828
|
+
provider.setProgressCallback((message) => {
|
|
1829
|
+
understandingSpinner.message(message);
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
understanding = await provider.generateUnderstanding(codebaseFiles, docsSummary);
|
|
1833
|
+
const totalTime = Math.floor((Date.now() - startTime) / 1e3);
|
|
1834
|
+
understandingSpinner.stop(`Analysis complete (${totalTime}s)`);
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
understandingSpinner.stop("Analysis failed");
|
|
1837
|
+
p3.log.error(String(error));
|
|
1838
|
+
process.exit(1);
|
|
1839
|
+
}
|
|
1840
|
+
if (!options.skipQuestions) {
|
|
1841
|
+
p3.log.message(chalk.bold("\nHere's what I understand about your codebase:\n"));
|
|
1842
|
+
p3.log.message(` ${chalk.cyan("Type:")} ${understanding.summary.type}`);
|
|
1843
|
+
p3.log.message(` ${chalk.cyan("Framework:")} ${understanding.summary.framework || "None detected"}`);
|
|
1844
|
+
p3.log.message(` ${chalk.cyan("Language:")} ${understanding.summary.language}`);
|
|
1845
|
+
p3.log.message(` ${chalk.cyan("Files:")} ${understanding.structure.totalFiles}`);
|
|
1846
|
+
p3.log.message(` ${chalk.cyan("Lines:")} ${understanding.structure.totalLines.toLocaleString()}`);
|
|
1847
|
+
p3.log.message(`
|
|
1848
|
+
${chalk.dim(understanding.summary.description)}
|
|
1849
|
+
`);
|
|
1850
|
+
if (understanding.features.length > 0) {
|
|
1851
|
+
p3.log.message(chalk.bold("Detected features:"));
|
|
1852
|
+
for (const feature of understanding.features.slice(0, 5)) {
|
|
1853
|
+
p3.log.message(` ${chalk.yellow("\u25CF")} ${feature.name} - ${chalk.dim(feature.description)}`);
|
|
1854
|
+
}
|
|
1855
|
+
if (understanding.features.length > 5) {
|
|
1856
|
+
p3.log.message(` ${chalk.dim(`...and ${understanding.features.length - 5} more`)}`);
|
|
1857
|
+
}
|
|
1858
|
+
console.log();
|
|
1859
|
+
}
|
|
1860
|
+
const isAccurate = await p3.confirm({
|
|
1861
|
+
message: "Is this understanding accurate?",
|
|
1862
|
+
initialValue: true
|
|
1863
|
+
});
|
|
1864
|
+
if (p3.isCancel(isAccurate)) {
|
|
1865
|
+
p3.cancel("Initialization cancelled.");
|
|
1866
|
+
process.exit(0);
|
|
1867
|
+
}
|
|
1868
|
+
if (!isAccurate) {
|
|
1869
|
+
p3.log.info("You can edit .whiterose/intent.md after initialization to correct the understanding.");
|
|
1870
|
+
}
|
|
1871
|
+
const priorities = {};
|
|
1872
|
+
for (const feature of understanding.features.filter((f) => f.priority === "critical").slice(0, 3)) {
|
|
1873
|
+
const priority = await p3.select({
|
|
1874
|
+
message: `I detected ${chalk.bold(feature.name)}. How should I prioritize bugs here?`,
|
|
1875
|
+
options: [
|
|
1876
|
+
{ value: "critical", label: "Critical", hint: "highest priority" },
|
|
1877
|
+
{ value: "high", label: "High" },
|
|
1878
|
+
{ value: "medium", label: "Medium" },
|
|
1879
|
+
{ value: "low", label: "Low" },
|
|
1880
|
+
{ value: "ignore", label: "Ignore", hint: "skip this area" }
|
|
1881
|
+
],
|
|
1882
|
+
initialValue: "critical"
|
|
1883
|
+
});
|
|
1884
|
+
if (p3.isCancel(priority)) {
|
|
1885
|
+
p3.cancel("Initialization cancelled.");
|
|
1886
|
+
process.exit(0);
|
|
1887
|
+
}
|
|
1888
|
+
for (const file of feature.relatedFiles) {
|
|
1889
|
+
priorities[file] = priority;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const areasOfConcern = await p3.text({
|
|
1893
|
+
message: "Any specific files or directories you want me to focus on? (comma-separated, or leave empty)",
|
|
1894
|
+
placeholder: "src/api/checkout.ts, src/hooks/useAuth.ts"
|
|
1895
|
+
});
|
|
1896
|
+
if (!p3.isCancel(areasOfConcern) && areasOfConcern) {
|
|
1897
|
+
for (const area of areasOfConcern.split(",").map((s) => s.trim())) {
|
|
1898
|
+
priorities[area] = "critical";
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
understanding._userPriorities = priorities;
|
|
1902
|
+
}
|
|
1903
|
+
const writeSpinner = p3.spinner();
|
|
1904
|
+
writeSpinner.start("Creating configuration...");
|
|
1905
|
+
try {
|
|
1906
|
+
mkdirSync(join(whiterosePath, "cache"), { recursive: true });
|
|
1907
|
+
mkdirSync(join(whiterosePath, "reports"), { recursive: true });
|
|
1908
|
+
const config = {
|
|
1909
|
+
version: "1",
|
|
1910
|
+
provider: selectedProvider,
|
|
1911
|
+
include: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
|
1912
|
+
exclude: ["node_modules", "dist", "build", ".next", "coverage", "**/*.test.*", "**/*.spec.*"],
|
|
1913
|
+
priorities: understanding._userPriorities || {},
|
|
1914
|
+
categories: ["logic-error", "security", "async-race-condition", "edge-case", "null-reference"],
|
|
1915
|
+
minConfidence: "low",
|
|
1916
|
+
staticAnalysis: {
|
|
1917
|
+
typescript: true,
|
|
1918
|
+
eslint: true
|
|
1919
|
+
},
|
|
1920
|
+
output: {
|
|
1921
|
+
sarif: true,
|
|
1922
|
+
markdown: true,
|
|
1923
|
+
sarifPath: ".whiterose/reports",
|
|
1924
|
+
markdownPath: "BUGS.md"
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
writeFileSync(join(whiterosePath, "config.yml"), YAML.stringify(config), "utf-8");
|
|
1928
|
+
writeFileSync(
|
|
1929
|
+
join(whiterosePath, "cache", "understanding.json"),
|
|
1930
|
+
JSON.stringify(understanding, null, 2),
|
|
1931
|
+
"utf-8"
|
|
1932
|
+
);
|
|
1933
|
+
const intentDoc = generateIntentDocument(understanding);
|
|
1934
|
+
writeFileSync(join(whiterosePath, "intent.md"), intentDoc, "utf-8");
|
|
1935
|
+
writeFileSync(
|
|
1936
|
+
join(whiterosePath, "cache", "file-hashes.json"),
|
|
1937
|
+
JSON.stringify({ version: "1", fileHashes: [], lastFullScan: null }, null, 2),
|
|
1938
|
+
"utf-8"
|
|
1939
|
+
);
|
|
1940
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
1941
|
+
if (existsSync(gitignorePath)) {
|
|
1942
|
+
const gitignore = await import('fs').then((fs) => fs.readFileSync(gitignorePath, "utf-8"));
|
|
1943
|
+
if (!gitignore.includes(".whiterose/cache")) {
|
|
1944
|
+
writeFileSync(gitignorePath, gitignore + "\n# whiterose cache\n.whiterose/cache/\n", "utf-8");
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
writeSpinner.stop("Configuration created");
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
writeSpinner.stop("Failed to create configuration");
|
|
1950
|
+
p3.log.error(String(error));
|
|
1951
|
+
process.exit(1);
|
|
1952
|
+
}
|
|
1953
|
+
p3.outro(chalk.green("whiterose initialized successfully!"));
|
|
1954
|
+
console.log();
|
|
1955
|
+
console.log(chalk.dim(" Next steps:"));
|
|
1956
|
+
console.log(chalk.dim(" 1. Review .whiterose/intent.md and edit if needed"));
|
|
1957
|
+
console.log(chalk.dim(" 2. Run ") + chalk.cyan("whiterose scan") + chalk.dim(" to find bugs"));
|
|
1958
|
+
console.log(chalk.dim(" 3. Run ") + chalk.cyan("whiterose fix") + chalk.dim(" to fix them interactively"));
|
|
1959
|
+
console.log();
|
|
1960
|
+
}
|
|
1961
|
+
async function loadConfig(cwd) {
|
|
1962
|
+
const configPath = join(cwd, ".whiterose", "config.yml");
|
|
1963
|
+
if (!existsSync(configPath)) {
|
|
1964
|
+
throw new Error('Config file not found. Run "whiterose init" first.');
|
|
1965
|
+
}
|
|
1966
|
+
const content = readFileSync(configPath, "utf-8");
|
|
1967
|
+
const parsed = YAML.parse(content);
|
|
1968
|
+
return WhiteroseConfig.parse(parsed);
|
|
1969
|
+
}
|
|
1970
|
+
async function loadUnderstanding(cwd) {
|
|
1971
|
+
const understandingPath = join(cwd, ".whiterose", "cache", "understanding.json");
|
|
1972
|
+
if (!existsSync(understandingPath)) {
|
|
1973
|
+
return null;
|
|
1974
|
+
}
|
|
1975
|
+
const content = readFileSync(understandingPath, "utf-8");
|
|
1976
|
+
return JSON.parse(content);
|
|
1977
|
+
}
|
|
1978
|
+
async function runStaticAnalysis(cwd, files, config) {
|
|
1979
|
+
const results = [];
|
|
1980
|
+
if (config.staticAnalysis.typescript) {
|
|
1981
|
+
const tscResults = await runTypeScript(cwd);
|
|
1982
|
+
results.push(...tscResults);
|
|
1983
|
+
}
|
|
1984
|
+
if (config.staticAnalysis.eslint) {
|
|
1985
|
+
const eslintResults = await runEslint(cwd, files);
|
|
1986
|
+
results.push(...eslintResults);
|
|
1987
|
+
}
|
|
1988
|
+
return results;
|
|
1989
|
+
}
|
|
1990
|
+
async function runTypeScript(cwd) {
|
|
1991
|
+
const results = [];
|
|
1992
|
+
const tsconfigPath = join(cwd, "tsconfig.json");
|
|
1993
|
+
if (!existsSync(tsconfigPath)) {
|
|
1994
|
+
return results;
|
|
1995
|
+
}
|
|
1996
|
+
try {
|
|
1997
|
+
await execa("npx", ["tsc", "--noEmit", "--pretty", "false"], {
|
|
1998
|
+
cwd,
|
|
1999
|
+
timeout: 6e4
|
|
2000
|
+
});
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
const output = error.stdout || "";
|
|
2003
|
+
const lines = output.split("\n");
|
|
2004
|
+
for (const line of lines) {
|
|
2005
|
+
const match = line.match(/^(.+)\((\d+),(\d+)\):\s+(error|warning)\s+TS\d+:\s+(.+)$/);
|
|
2006
|
+
if (match) {
|
|
2007
|
+
results.push({
|
|
2008
|
+
tool: "typescript",
|
|
2009
|
+
file: match[1],
|
|
2010
|
+
line: parseInt(match[2], 10),
|
|
2011
|
+
message: match[5],
|
|
2012
|
+
severity: match[4] === "error" ? "error" : "warning",
|
|
2013
|
+
code: `TS${match[4]}`
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return results;
|
|
2019
|
+
}
|
|
2020
|
+
async function runEslint(cwd, files) {
|
|
2021
|
+
const results = [];
|
|
2022
|
+
const hasEslint = existsSync(join(cwd, ".eslintrc")) || existsSync(join(cwd, ".eslintrc.js")) || existsSync(join(cwd, ".eslintrc.json")) || existsSync(join(cwd, ".eslintrc.yml")) || existsSync(join(cwd, "eslint.config.js")) || existsSync(join(cwd, "eslint.config.mjs"));
|
|
2023
|
+
if (!hasEslint) {
|
|
2024
|
+
return results;
|
|
2025
|
+
}
|
|
2026
|
+
try {
|
|
2027
|
+
const { stdout } = await execa(
|
|
2028
|
+
"npx",
|
|
2029
|
+
["eslint", "--format", "json", "--no-error-on-unmatched-pattern", ...files.slice(0, 50)],
|
|
2030
|
+
{
|
|
2031
|
+
cwd,
|
|
2032
|
+
timeout: 6e4,
|
|
2033
|
+
reject: false
|
|
2034
|
+
}
|
|
2035
|
+
);
|
|
2036
|
+
const eslintResults = JSON.parse(stdout || "[]");
|
|
2037
|
+
for (const fileResult of eslintResults) {
|
|
2038
|
+
for (const message of fileResult.messages || []) {
|
|
2039
|
+
results.push({
|
|
2040
|
+
tool: "eslint",
|
|
2041
|
+
file: fileResult.filePath,
|
|
2042
|
+
line: message.line || 0,
|
|
2043
|
+
message: message.message,
|
|
2044
|
+
severity: message.severity === 2 ? "error" : "warning",
|
|
2045
|
+
code: message.ruleId
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
} catch {
|
|
2050
|
+
}
|
|
2051
|
+
return results;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/output/sarif.ts
|
|
2055
|
+
function outputSarif(result) {
|
|
2056
|
+
const rules = [];
|
|
2057
|
+
const results = [];
|
|
2058
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
2059
|
+
for (const bug of result.bugs) {
|
|
2060
|
+
if (!seenRules.has(bug.category)) {
|
|
2061
|
+
seenRules.add(bug.category);
|
|
2062
|
+
rules.push({
|
|
2063
|
+
id: bug.category,
|
|
2064
|
+
name: formatRuleName(bug.category),
|
|
2065
|
+
shortDescription: { text: getCategoryDescription(bug.category) },
|
|
2066
|
+
fullDescription: { text: getCategoryDescription(bug.category) },
|
|
2067
|
+
defaultConfiguration: { level: "warning" },
|
|
2068
|
+
properties: { category: bug.category }
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
results.push(bugToSarifResult(bug));
|
|
2072
|
+
}
|
|
2073
|
+
return {
|
|
2074
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
2075
|
+
version: "2.1.0",
|
|
2076
|
+
runs: [
|
|
2077
|
+
{
|
|
2078
|
+
tool: {
|
|
2079
|
+
driver: {
|
|
2080
|
+
name: "whiterose",
|
|
2081
|
+
version: "0.1.0",
|
|
2082
|
+
informationUri: "https://github.com/shakecodeslikecray/whiterose",
|
|
2083
|
+
rules
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
results
|
|
2087
|
+
}
|
|
2088
|
+
]
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
function bugToSarifResult(bug) {
|
|
2092
|
+
const result = {
|
|
2093
|
+
ruleId: bug.id,
|
|
2094
|
+
level: severityToLevel(bug.severity),
|
|
2095
|
+
message: {
|
|
2096
|
+
text: bug.title,
|
|
2097
|
+
markdown: `**${bug.title}**
|
|
2098
|
+
|
|
2099
|
+
${bug.description}
|
|
2100
|
+
|
|
2101
|
+
**Evidence:**
|
|
2102
|
+
${bug.evidence.map((e) => `- ${e}`).join("\n")}`
|
|
2103
|
+
},
|
|
2104
|
+
locations: [
|
|
2105
|
+
{
|
|
2106
|
+
physicalLocation: {
|
|
2107
|
+
artifactLocation: { uri: bug.file },
|
|
2108
|
+
region: { startLine: bug.line, endLine: bug.endLine }
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
]
|
|
2112
|
+
};
|
|
2113
|
+
if (bug.codePath.length > 0) {
|
|
2114
|
+
result.codeFlows = [
|
|
2115
|
+
{
|
|
2116
|
+
threadFlows: [
|
|
2117
|
+
{
|
|
2118
|
+
locations: bug.codePath.map((step) => ({
|
|
2119
|
+
location: {
|
|
2120
|
+
physicalLocation: {
|
|
2121
|
+
artifactLocation: { uri: step.file },
|
|
2122
|
+
region: { startLine: step.line }
|
|
2123
|
+
}
|
|
2124
|
+
},
|
|
2125
|
+
message: { text: step.explanation }
|
|
2126
|
+
}))
|
|
2127
|
+
}
|
|
2128
|
+
]
|
|
2129
|
+
}
|
|
2130
|
+
];
|
|
2131
|
+
}
|
|
2132
|
+
return result;
|
|
2133
|
+
}
|
|
2134
|
+
function severityToLevel(severity) {
|
|
2135
|
+
switch (severity) {
|
|
2136
|
+
case "critical":
|
|
2137
|
+
return "error";
|
|
2138
|
+
case "high":
|
|
2139
|
+
return "error";
|
|
2140
|
+
case "medium":
|
|
2141
|
+
return "warning";
|
|
2142
|
+
case "low":
|
|
2143
|
+
return "note";
|
|
2144
|
+
default:
|
|
2145
|
+
return "warning";
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function formatRuleName(category) {
|
|
2149
|
+
return category.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2150
|
+
}
|
|
2151
|
+
function getCategoryDescription(category) {
|
|
2152
|
+
const descriptions = {
|
|
2153
|
+
"logic-error": "Logic errors such as off-by-one, wrong operators, or incorrect conditions",
|
|
2154
|
+
security: "Security vulnerabilities including injection, auth bypass, and data exposure",
|
|
2155
|
+
"async-race-condition": "Async/concurrency issues like race conditions and missing awaits",
|
|
2156
|
+
"edge-case": "Edge cases that are not properly handled",
|
|
2157
|
+
"null-reference": "Potential null or undefined reference issues",
|
|
2158
|
+
"type-coercion": "Type coercion bugs that may cause unexpected behavior",
|
|
2159
|
+
"resource-leak": "Resource leaks such as unclosed handles or connections",
|
|
2160
|
+
"intent-violation": "Violations of documented behavioral contracts or business rules"
|
|
2161
|
+
};
|
|
2162
|
+
return descriptions[category] || "Unknown bug category";
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// src/output/markdown.ts
|
|
2166
|
+
function outputMarkdown(result) {
|
|
2167
|
+
const lines = [];
|
|
2168
|
+
lines.push("# Bug Report");
|
|
2169
|
+
lines.push("");
|
|
2170
|
+
lines.push(`> Generated by [whiterose](https://github.com/shakecodeslikecray/whiterose) on ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
2171
|
+
lines.push("");
|
|
2172
|
+
lines.push("## Summary");
|
|
2173
|
+
lines.push("");
|
|
2174
|
+
lines.push(`| Severity | Count |`);
|
|
2175
|
+
lines.push(`|----------|-------|`);
|
|
2176
|
+
lines.push(`| Critical | ${result.summary.critical} |`);
|
|
2177
|
+
lines.push(`| High | ${result.summary.high} |`);
|
|
2178
|
+
lines.push(`| Medium | ${result.summary.medium} |`);
|
|
2179
|
+
lines.push(`| Low | ${result.summary.low} |`);
|
|
2180
|
+
lines.push(`| **Total** | **${result.summary.total}** |`);
|
|
2181
|
+
lines.push("");
|
|
2182
|
+
lines.push(`- **Scan Type:** ${result.scanType}`);
|
|
2183
|
+
lines.push(`- **Files Scanned:** ${result.filesScanned}`);
|
|
2184
|
+
if (result.filesChanged !== void 0) {
|
|
2185
|
+
lines.push(`- **Files Changed:** ${result.filesChanged}`);
|
|
2186
|
+
}
|
|
2187
|
+
lines.push("");
|
|
2188
|
+
if (result.bugs.length === 0) {
|
|
2189
|
+
lines.push("No bugs found.");
|
|
2190
|
+
return lines.join("\n");
|
|
2191
|
+
}
|
|
2192
|
+
const bySeverity = {
|
|
2193
|
+
critical: [],
|
|
2194
|
+
high: [],
|
|
2195
|
+
medium: [],
|
|
2196
|
+
low: []
|
|
2197
|
+
};
|
|
2198
|
+
for (const bug of result.bugs) {
|
|
2199
|
+
bySeverity[bug.severity].push(bug);
|
|
2200
|
+
}
|
|
2201
|
+
if (bySeverity.critical.length > 0) {
|
|
2202
|
+
lines.push("## Critical");
|
|
2203
|
+
lines.push("");
|
|
2204
|
+
for (const bug of bySeverity.critical) {
|
|
2205
|
+
lines.push(formatBug(bug));
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (bySeverity.high.length > 0) {
|
|
2209
|
+
lines.push("## High");
|
|
2210
|
+
lines.push("");
|
|
2211
|
+
for (const bug of bySeverity.high) {
|
|
2212
|
+
lines.push(formatBug(bug));
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
if (bySeverity.medium.length > 0) {
|
|
2216
|
+
lines.push("## Medium");
|
|
2217
|
+
lines.push("");
|
|
2218
|
+
for (const bug of bySeverity.medium) {
|
|
2219
|
+
lines.push(formatBug(bug));
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
if (bySeverity.low.length > 0) {
|
|
2223
|
+
lines.push("## Low");
|
|
2224
|
+
lines.push("");
|
|
2225
|
+
for (const bug of bySeverity.low) {
|
|
2226
|
+
lines.push(formatBug(bug));
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return lines.join("\n");
|
|
2230
|
+
}
|
|
2231
|
+
function formatBug(bug) {
|
|
2232
|
+
const lines = [];
|
|
2233
|
+
const confidenceBadge = getConfidenceBadge(bug.confidence.overall);
|
|
2234
|
+
lines.push(`### ${bug.id}: ${bug.title} ${confidenceBadge}`);
|
|
2235
|
+
lines.push("");
|
|
2236
|
+
lines.push(`**Location:** \`${bug.file}:${bug.line}\``);
|
|
2237
|
+
lines.push(`**Category:** ${formatCategory(bug.category)}`);
|
|
2238
|
+
lines.push("");
|
|
2239
|
+
lines.push(bug.description);
|
|
2240
|
+
lines.push("");
|
|
2241
|
+
if (bug.codePath.length > 0) {
|
|
2242
|
+
lines.push("<details>");
|
|
2243
|
+
lines.push("<summary>Code Path</summary>");
|
|
2244
|
+
lines.push("");
|
|
2245
|
+
for (const step of bug.codePath) {
|
|
2246
|
+
lines.push(`${step.step}. \`${step.file}:${step.line}\``);
|
|
2247
|
+
lines.push(` \`\`\`${getLanguage(step.file)}`);
|
|
2248
|
+
lines.push(` ${step.code}`);
|
|
2249
|
+
lines.push(" ```");
|
|
2250
|
+
lines.push(` ${step.explanation}`);
|
|
2251
|
+
lines.push("");
|
|
2252
|
+
}
|
|
2253
|
+
lines.push("</details>");
|
|
2254
|
+
lines.push("");
|
|
2255
|
+
}
|
|
2256
|
+
if (bug.evidence.length > 0) {
|
|
2257
|
+
lines.push("**Evidence:**");
|
|
2258
|
+
for (const e of bug.evidence) {
|
|
2259
|
+
lines.push(`- ${e}`);
|
|
2260
|
+
}
|
|
2261
|
+
lines.push("");
|
|
2262
|
+
}
|
|
2263
|
+
if (bug.suggestedFix) {
|
|
2264
|
+
lines.push("**Suggested Fix:**");
|
|
2265
|
+
lines.push("```");
|
|
2266
|
+
lines.push(bug.suggestedFix);
|
|
2267
|
+
lines.push("```");
|
|
2268
|
+
lines.push("");
|
|
2269
|
+
}
|
|
2270
|
+
lines.push("---");
|
|
2271
|
+
lines.push("");
|
|
2272
|
+
return lines.join("\n");
|
|
2273
|
+
}
|
|
2274
|
+
function getConfidenceBadge(confidence) {
|
|
2275
|
+
switch (confidence) {
|
|
2276
|
+
case "high":
|
|
2277
|
+
return "`[HIGH CONFIDENCE]`";
|
|
2278
|
+
case "medium":
|
|
2279
|
+
return "`[MEDIUM CONFIDENCE]`";
|
|
2280
|
+
case "low":
|
|
2281
|
+
return "`[LOW CONFIDENCE]`";
|
|
2282
|
+
default:
|
|
2283
|
+
return "";
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
function formatCategory(category) {
|
|
2287
|
+
return category.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2288
|
+
}
|
|
2289
|
+
function getLanguage(file) {
|
|
2290
|
+
if (file.endsWith(".ts") || file.endsWith(".tsx")) return "typescript";
|
|
2291
|
+
if (file.endsWith(".js") || file.endsWith(".jsx")) return "javascript";
|
|
2292
|
+
if (file.endsWith(".py")) return "python";
|
|
2293
|
+
if (file.endsWith(".go")) return "go";
|
|
2294
|
+
return "";
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// src/cli/commands/scan.ts
|
|
2298
|
+
async function scanCommand(paths, options) {
|
|
2299
|
+
const cwd = process.cwd();
|
|
2300
|
+
const whiterosePath = join(cwd, ".whiterose");
|
|
2301
|
+
if (!existsSync(whiterosePath)) {
|
|
2302
|
+
if (!options.json && !options.sarif) {
|
|
2303
|
+
p3.log.error("whiterose is not initialized in this directory.");
|
|
2304
|
+
p3.log.info('Run "whiterose init" first.');
|
|
2305
|
+
} else {
|
|
2306
|
+
console.error(JSON.stringify({ error: "Not initialized. Run whiterose init first." }));
|
|
2307
|
+
}
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
const isQuiet = options.json || options.sarif;
|
|
2311
|
+
if (!isQuiet) {
|
|
2312
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - scanning for bugs"));
|
|
2313
|
+
}
|
|
2314
|
+
let config;
|
|
2315
|
+
try {
|
|
2316
|
+
config = await loadConfig(cwd);
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
if (!isQuiet) p3.log.error(`Failed to load config: ${error}`);
|
|
2319
|
+
process.exit(1);
|
|
2320
|
+
}
|
|
2321
|
+
const understanding = await loadUnderstanding(cwd);
|
|
2322
|
+
if (!understanding) {
|
|
2323
|
+
if (!isQuiet) {
|
|
2324
|
+
p3.log.error('No codebase understanding found. Run "whiterose refresh" to regenerate.');
|
|
2325
|
+
}
|
|
2326
|
+
process.exit(1);
|
|
2327
|
+
}
|
|
2328
|
+
let filesToScan;
|
|
2329
|
+
let scanType;
|
|
2330
|
+
if (options.full || paths.length > 0) {
|
|
2331
|
+
scanType = "full";
|
|
2332
|
+
if (!isQuiet) {
|
|
2333
|
+
const spinner5 = p3.spinner();
|
|
2334
|
+
spinner5.start("Scanning files...");
|
|
2335
|
+
filesToScan = paths.length > 0 ? paths : await scanCodebase(cwd, config);
|
|
2336
|
+
spinner5.stop(`Found ${filesToScan.length} files to scan`);
|
|
2337
|
+
} else {
|
|
2338
|
+
filesToScan = paths.length > 0 ? paths : await scanCodebase(cwd, config);
|
|
2339
|
+
}
|
|
2340
|
+
} else {
|
|
2341
|
+
scanType = "incremental";
|
|
2342
|
+
const changed = await getChangedFiles(cwd, config);
|
|
2343
|
+
filesToScan = changed.files;
|
|
2344
|
+
if (filesToScan.length === 0) {
|
|
2345
|
+
if (!isQuiet) {
|
|
2346
|
+
p3.log.info("No files changed since last scan. Use --full for a complete scan.");
|
|
2347
|
+
} else {
|
|
2348
|
+
console.log(JSON.stringify({ bugs: [], message: "No changes detected" }));
|
|
2349
|
+
}
|
|
2350
|
+
process.exit(0);
|
|
2351
|
+
}
|
|
2352
|
+
if (!isQuiet) {
|
|
2353
|
+
p3.log.info(`Incremental scan: ${filesToScan.length} changed files`);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
let staticResults;
|
|
2357
|
+
if (!isQuiet) {
|
|
2358
|
+
const staticSpinner = p3.spinner();
|
|
2359
|
+
staticSpinner.start("Running static analysis (tsc, eslint)...");
|
|
2360
|
+
staticResults = await runStaticAnalysis(cwd, filesToScan, config);
|
|
2361
|
+
staticSpinner.stop(`Static analysis: ${staticResults.length} signals found`);
|
|
2362
|
+
} else {
|
|
2363
|
+
staticResults = await runStaticAnalysis(cwd, filesToScan, config);
|
|
2364
|
+
}
|
|
2365
|
+
const providerName = options.provider || config.provider;
|
|
2366
|
+
const provider = await getProvider(providerName);
|
|
2367
|
+
if (options.unsafe) {
|
|
2368
|
+
if ("setUnsafeMode" in provider) {
|
|
2369
|
+
provider.setUnsafeMode(true);
|
|
2370
|
+
if (!isQuiet) {
|
|
2371
|
+
p3.log.warn("Running in unsafe mode (--unsafe). LLM permission prompts are bypassed.");
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
let bugs;
|
|
2376
|
+
if (!isQuiet) {
|
|
2377
|
+
const llmSpinner = p3.spinner();
|
|
2378
|
+
const analysisStartTime = Date.now();
|
|
2379
|
+
llmSpinner.start(`Analyzing with ${providerName}... (this may take 1-2 minutes)`);
|
|
2380
|
+
const analysisTimeInterval = setInterval(() => {
|
|
2381
|
+
const elapsed = Math.floor((Date.now() - analysisStartTime) / 1e3);
|
|
2382
|
+
const minutes = Math.floor(elapsed / 60);
|
|
2383
|
+
const seconds = elapsed % 60;
|
|
2384
|
+
const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
2385
|
+
llmSpinner.message(`Analyzing with ${providerName}... (${timeStr} elapsed)`);
|
|
2386
|
+
}, 5e3);
|
|
2387
|
+
try {
|
|
2388
|
+
bugs = await provider.analyze({
|
|
2389
|
+
files: filesToScan,
|
|
2390
|
+
understanding,
|
|
2391
|
+
config,
|
|
2392
|
+
staticAnalysisResults: staticResults
|
|
2393
|
+
});
|
|
2394
|
+
clearInterval(analysisTimeInterval);
|
|
2395
|
+
const totalTime = Math.floor((Date.now() - analysisStartTime) / 1e3);
|
|
2396
|
+
llmSpinner.stop(`Found ${bugs.length} potential bugs (${totalTime}s)`);
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
clearInterval(analysisTimeInterval);
|
|
2399
|
+
llmSpinner.stop("Analysis failed");
|
|
2400
|
+
p3.log.error(String(error));
|
|
2401
|
+
process.exit(1);
|
|
2402
|
+
}
|
|
2403
|
+
} else {
|
|
2404
|
+
bugs = await provider.analyze({
|
|
2405
|
+
files: filesToScan,
|
|
2406
|
+
understanding,
|
|
2407
|
+
config,
|
|
2408
|
+
staticAnalysisResults: staticResults
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
if (options.adversarial && bugs.length > 0) {
|
|
2412
|
+
if (!isQuiet) {
|
|
2413
|
+
const advSpinner = p3.spinner();
|
|
2414
|
+
advSpinner.start("Running adversarial validation...");
|
|
2415
|
+
const validatedBugs = [];
|
|
2416
|
+
for (const bug of bugs) {
|
|
2417
|
+
const result2 = await provider.adversarialValidate(bug, {
|
|
2418
|
+
files: filesToScan,
|
|
2419
|
+
understanding,
|
|
2420
|
+
config,
|
|
2421
|
+
staticAnalysisResults: staticResults
|
|
2422
|
+
});
|
|
2423
|
+
if (result2.survived) {
|
|
2424
|
+
validatedBugs.push({
|
|
2425
|
+
...bug,
|
|
2426
|
+
confidence: result2.adjustedConfidence || bug.confidence
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const filtered = bugs.length - validatedBugs.length;
|
|
2431
|
+
advSpinner.stop(`Adversarial validation: ${filtered} false positives filtered`);
|
|
2432
|
+
bugs = validatedBugs;
|
|
2433
|
+
} else {
|
|
2434
|
+
const validatedBugs = [];
|
|
2435
|
+
for (const bug of bugs) {
|
|
2436
|
+
const result2 = await provider.adversarialValidate(bug, {
|
|
2437
|
+
files: filesToScan,
|
|
2438
|
+
understanding,
|
|
2439
|
+
config,
|
|
2440
|
+
staticAnalysisResults: staticResults
|
|
2441
|
+
});
|
|
2442
|
+
if (result2.survived) {
|
|
2443
|
+
validatedBugs.push({
|
|
2444
|
+
...bug,
|
|
2445
|
+
confidence: result2.adjustedConfidence || bug.confidence
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
bugs = validatedBugs;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
const minConfidence = options.minConfidence;
|
|
2453
|
+
const confidenceOrder = { high: 3, medium: 2, low: 1 };
|
|
2454
|
+
bugs = bugs.filter((bug) => confidenceOrder[bug.confidence.overall] >= confidenceOrder[minConfidence]);
|
|
2455
|
+
if (options.category && options.category.length > 0) {
|
|
2456
|
+
bugs = bugs.filter((bug) => options.category.includes(bug.category));
|
|
2457
|
+
}
|
|
2458
|
+
bugs = bugs.map((bug, index) => ({
|
|
2459
|
+
...bug,
|
|
2460
|
+
id: bug.id || generateBugId(index)
|
|
2461
|
+
}));
|
|
2462
|
+
const result = {
|
|
2463
|
+
id: `scan-${Date.now()}`,
|
|
2464
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2465
|
+
scanType,
|
|
2466
|
+
filesScanned: filesToScan.length,
|
|
2467
|
+
filesChanged: scanType === "incremental" ? filesToScan.length : void 0,
|
|
2468
|
+
duration: 0,
|
|
2469
|
+
// TODO: track actual duration
|
|
2470
|
+
bugs,
|
|
2471
|
+
summary: {
|
|
2472
|
+
critical: bugs.filter((b) => b.severity === "critical").length,
|
|
2473
|
+
high: bugs.filter((b) => b.severity === "high").length,
|
|
2474
|
+
medium: bugs.filter((b) => b.severity === "medium").length,
|
|
2475
|
+
low: bugs.filter((b) => b.severity === "low").length,
|
|
2476
|
+
total: bugs.length
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
if (options.json) {
|
|
2480
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2481
|
+
} else if (options.sarif) {
|
|
2482
|
+
console.log(JSON.stringify(outputSarif(result), null, 2));
|
|
2483
|
+
} else {
|
|
2484
|
+
if (config.output.sarif) {
|
|
2485
|
+
const sarifPath = join(whiterosePath, "reports", `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.sarif`);
|
|
2486
|
+
writeFileSync(sarifPath, JSON.stringify(outputSarif(result), null, 2));
|
|
2487
|
+
}
|
|
2488
|
+
if (config.output.markdown) {
|
|
2489
|
+
const markdown = outputMarkdown(result);
|
|
2490
|
+
writeFileSync(join(cwd, config.output.markdownPath), markdown);
|
|
2491
|
+
}
|
|
2492
|
+
console.log();
|
|
2493
|
+
p3.log.message(chalk.bold("Scan Results"));
|
|
2494
|
+
console.log();
|
|
2495
|
+
console.log(` ${chalk.red("\u25CF")} Critical: ${result.summary.critical}`);
|
|
2496
|
+
console.log(` ${chalk.yellow("\u25CF")} High: ${result.summary.high}`);
|
|
2497
|
+
console.log(` ${chalk.blue("\u25CF")} Medium: ${result.summary.medium}`);
|
|
2498
|
+
console.log(` ${chalk.dim("\u25CF")} Low: ${result.summary.low}`);
|
|
2499
|
+
console.log();
|
|
2500
|
+
console.log(` ${chalk.bold("Total:")} ${result.summary.total} bugs found`);
|
|
2501
|
+
console.log();
|
|
2502
|
+
if (result.summary.total > 0) {
|
|
2503
|
+
p3.log.info(`Run ${chalk.cyan("whiterose fix")} to fix bugs interactively.`);
|
|
2504
|
+
}
|
|
2505
|
+
p3.outro(chalk.green("Scan complete"));
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
var Dashboard = ({ bugs, onSelectCategory }) => {
|
|
2509
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
2510
|
+
const summary = {
|
|
2511
|
+
critical: bugs.filter((b) => b.severity === "critical").length,
|
|
2512
|
+
high: bugs.filter((b) => b.severity === "high").length,
|
|
2513
|
+
medium: bugs.filter((b) => b.severity === "medium").length,
|
|
2514
|
+
low: bugs.filter((b) => b.severity === "low").length
|
|
2515
|
+
};
|
|
2516
|
+
const byCategory = {};
|
|
2517
|
+
for (const bug of bugs) {
|
|
2518
|
+
byCategory[bug.category] = (byCategory[bug.category] || 0) + 1;
|
|
2519
|
+
}
|
|
2520
|
+
const menuItems = [
|
|
2521
|
+
{ key: "all", label: "All Bugs", count: bugs.length, color: "white" },
|
|
2522
|
+
{ key: "critical", label: "Critical", count: summary.critical, color: "red" },
|
|
2523
|
+
{ key: "high", label: "High", count: summary.high, color: "yellow" },
|
|
2524
|
+
{ key: "medium", label: "Medium", count: summary.medium, color: "blue" },
|
|
2525
|
+
{ key: "low", label: "Low", count: summary.low, color: "gray" }
|
|
2526
|
+
];
|
|
2527
|
+
const categoryColors = {
|
|
2528
|
+
"logic-error": "magenta",
|
|
2529
|
+
security: "red",
|
|
2530
|
+
"async-race-condition": "cyan",
|
|
2531
|
+
"edge-case": "yellow",
|
|
2532
|
+
"null-reference": "blue",
|
|
2533
|
+
"type-coercion": "green",
|
|
2534
|
+
"resource-leak": "yellow",
|
|
2535
|
+
"intent-violation": "magenta"
|
|
2536
|
+
};
|
|
2537
|
+
for (const [category, count] of Object.entries(byCategory)) {
|
|
2538
|
+
menuItems.push({
|
|
2539
|
+
key: category,
|
|
2540
|
+
label: formatCategory2(category),
|
|
2541
|
+
count,
|
|
2542
|
+
color: categoryColors[category] || "white"
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
useInput((input, key) => {
|
|
2546
|
+
if (key.upArrow) {
|
|
2547
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
2548
|
+
} else if (key.downArrow) {
|
|
2549
|
+
setSelectedIndex((i) => Math.min(menuItems.length - 1, i + 1));
|
|
2550
|
+
} else if (key.return) {
|
|
2551
|
+
const item = menuItems[selectedIndex];
|
|
2552
|
+
if (item.key === "all") {
|
|
2553
|
+
onSelectCategory(null);
|
|
2554
|
+
} else {
|
|
2555
|
+
onSelectCategory(item.key);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
if (bugs.length === 0) {
|
|
2560
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
2561
|
+
/* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "\u2713 No bugs found!" }),
|
|
2562
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Your codebase looks clean." })
|
|
2563
|
+
] });
|
|
2564
|
+
}
|
|
2565
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2566
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
|
|
2567
|
+
/* @__PURE__ */ jsx(Text, { bold: true, underline: true, children: "Summary" }),
|
|
2568
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
2569
|
+
/* @__PURE__ */ jsx(Box, { marginRight: 3, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
2570
|
+
"\u25CF Critical: ",
|
|
2571
|
+
summary.critical
|
|
2572
|
+
] }) }),
|
|
2573
|
+
/* @__PURE__ */ jsx(Box, { marginRight: 3, children: /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
2574
|
+
"\u25CF High: ",
|
|
2575
|
+
summary.high
|
|
2576
|
+
] }) }),
|
|
2577
|
+
/* @__PURE__ */ jsx(Box, { marginRight: 3, children: /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
|
|
2578
|
+
"\u25CF Medium: ",
|
|
2579
|
+
summary.medium
|
|
2580
|
+
] }) }),
|
|
2581
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
2582
|
+
"\u25CF Low: ",
|
|
2583
|
+
summary.low
|
|
2584
|
+
] }) })
|
|
2585
|
+
] })
|
|
2586
|
+
] }),
|
|
2587
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
2588
|
+
/* @__PURE__ */ jsx(Text, { bold: true, underline: true, children: "Filter by" }),
|
|
2589
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: menuItems.map((item, index) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
2590
|
+
/* @__PURE__ */ jsx(Text, { color: index === selectedIndex ? "cyan" : "white", children: index === selectedIndex ? "\u25B6 " : " " }),
|
|
2591
|
+
/* @__PURE__ */ jsx(Text, { color: item.color, children: item.label }),
|
|
2592
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
2593
|
+
" (",
|
|
2594
|
+
item.count,
|
|
2595
|
+
")"
|
|
2596
|
+
] })
|
|
2597
|
+
] }, item.key)) })
|
|
2598
|
+
] }),
|
|
2599
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "[\u2191\u2193] Navigate [Enter] Select [q] Quit" }) })
|
|
2600
|
+
] });
|
|
2601
|
+
};
|
|
2602
|
+
function formatCategory2(category) {
|
|
2603
|
+
return category.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2604
|
+
}
|
|
2605
|
+
var VISIBLE_ITEMS = 10;
|
|
2606
|
+
var BugList = ({ bugs, selectedIndex, onSelect, onBack }) => {
|
|
2607
|
+
const [localIndex, setLocalIndex] = useState(selectedIndex);
|
|
2608
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
2609
|
+
useEffect(() => {
|
|
2610
|
+
setLocalIndex(selectedIndex);
|
|
2611
|
+
}, [selectedIndex]);
|
|
2612
|
+
useEffect(() => {
|
|
2613
|
+
if (localIndex < scrollOffset) {
|
|
2614
|
+
setScrollOffset(localIndex);
|
|
2615
|
+
} else if (localIndex >= scrollOffset + VISIBLE_ITEMS) {
|
|
2616
|
+
setScrollOffset(localIndex - VISIBLE_ITEMS + 1);
|
|
2617
|
+
}
|
|
2618
|
+
}, [localIndex, scrollOffset]);
|
|
2619
|
+
useInput((input, key) => {
|
|
2620
|
+
if (key.upArrow) {
|
|
2621
|
+
setLocalIndex((i) => Math.max(0, i - 1));
|
|
2622
|
+
} else if (key.downArrow) {
|
|
2623
|
+
setLocalIndex((i) => Math.min(bugs.length - 1, i + 1));
|
|
2624
|
+
} else if (key.return) {
|
|
2625
|
+
onSelect(localIndex);
|
|
2626
|
+
} else if (input === "b" || key.escape) {
|
|
2627
|
+
onBack();
|
|
2628
|
+
} else if (key.pageUp) {
|
|
2629
|
+
setLocalIndex((i) => Math.max(0, i - VISIBLE_ITEMS));
|
|
2630
|
+
} else if (key.pageDown) {
|
|
2631
|
+
setLocalIndex((i) => Math.min(bugs.length - 1, i + VISIBLE_ITEMS));
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
if (bugs.length === 0) {
|
|
2635
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2636
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "No bugs in this category." }),
|
|
2637
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "[b] Back to dashboard" })
|
|
2638
|
+
] });
|
|
2639
|
+
}
|
|
2640
|
+
const visibleBugs = bugs.slice(scrollOffset, scrollOffset + VISIBLE_ITEMS);
|
|
2641
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2642
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
2643
|
+
/* @__PURE__ */ jsx(Box, { width: 8, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "gray", children: "ID" }) }),
|
|
2644
|
+
/* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "gray", children: "Severity" }) }),
|
|
2645
|
+
/* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "gray", children: "Confidence" }) }),
|
|
2646
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "gray", children: "Title" }) })
|
|
2647
|
+
] }),
|
|
2648
|
+
visibleBugs.map((bug, index) => {
|
|
2649
|
+
const actualIndex = scrollOffset + index;
|
|
2650
|
+
const isSelected = actualIndex === localIndex;
|
|
2651
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
2652
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : "white", children: isSelected ? "\u25B6 " : " " }),
|
|
2653
|
+
/* @__PURE__ */ jsx(Box, { width: 6, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: bug.id }) }),
|
|
2654
|
+
/* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { color: getSeverityColor(bug.severity), children: bug.severity.toUpperCase().padEnd(8) }) }),
|
|
2655
|
+
/* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { color: getConfidenceColor(bug.confidence.overall), children: bug.confidence.overall.toUpperCase().padEnd(8) }) }),
|
|
2656
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { color: isSelected ? "white" : "gray", children: truncate(bug.title, 50) }) })
|
|
2657
|
+
] }, bug.id);
|
|
2658
|
+
}),
|
|
2659
|
+
bugs.length > VISIBLE_ITEMS && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
2660
|
+
"Showing ",
|
|
2661
|
+
scrollOffset + 1,
|
|
2662
|
+
"-",
|
|
2663
|
+
Math.min(scrollOffset + VISIBLE_ITEMS, bugs.length),
|
|
2664
|
+
" of ",
|
|
2665
|
+
bugs.length,
|
|
2666
|
+
scrollOffset > 0 && " [\u2191 more above]",
|
|
2667
|
+
scrollOffset + VISIBLE_ITEMS < bugs.length && " [\u2193 more below]"
|
|
2668
|
+
] }) }),
|
|
2669
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "[\u2191\u2193] Navigate [Enter] View details [PgUp/PgDn] Page [b] Back" }) })
|
|
2670
|
+
] });
|
|
2671
|
+
};
|
|
2672
|
+
function getSeverityColor(severity) {
|
|
2673
|
+
switch (severity) {
|
|
2674
|
+
case "critical":
|
|
2675
|
+
return "red";
|
|
2676
|
+
case "high":
|
|
2677
|
+
return "yellow";
|
|
2678
|
+
case "medium":
|
|
2679
|
+
return "blue";
|
|
2680
|
+
case "low":
|
|
2681
|
+
return "gray";
|
|
2682
|
+
default:
|
|
2683
|
+
return "white";
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
function getConfidenceColor(confidence) {
|
|
2687
|
+
switch (confidence) {
|
|
2688
|
+
case "high":
|
|
2689
|
+
return "green";
|
|
2690
|
+
case "medium":
|
|
2691
|
+
return "yellow";
|
|
2692
|
+
case "low":
|
|
2693
|
+
return "red";
|
|
2694
|
+
default:
|
|
2695
|
+
return "white";
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
function truncate(str, maxLen) {
|
|
2699
|
+
if (str.length <= maxLen) return str;
|
|
2700
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
2701
|
+
}
|
|
2702
|
+
var BugDetail = ({
|
|
2703
|
+
bug,
|
|
2704
|
+
index,
|
|
2705
|
+
total,
|
|
2706
|
+
onFix,
|
|
2707
|
+
onNext,
|
|
2708
|
+
onPrev,
|
|
2709
|
+
onBack
|
|
2710
|
+
}) => {
|
|
2711
|
+
const [activeTab, setActiveTab] = useState("overview");
|
|
2712
|
+
useInput((input, key) => {
|
|
2713
|
+
if (input === "f") {
|
|
2714
|
+
onFix();
|
|
2715
|
+
} else if (input === "n" || key.rightArrow) {
|
|
2716
|
+
onNext();
|
|
2717
|
+
} else if (input === "p" || key.leftArrow) {
|
|
2718
|
+
onPrev();
|
|
2719
|
+
} else if (input === "b" || key.escape) {
|
|
2720
|
+
onBack();
|
|
2721
|
+
} else if (input === "1") {
|
|
2722
|
+
setActiveTab("overview");
|
|
2723
|
+
} else if (input === "2") {
|
|
2724
|
+
setActiveTab("codepath");
|
|
2725
|
+
} else if (input === "3") {
|
|
2726
|
+
setActiveTab("evidence");
|
|
2727
|
+
} else if (input === "4") {
|
|
2728
|
+
setActiveTab("fix");
|
|
2729
|
+
} else if (key.tab) {
|
|
2730
|
+
const tabs = ["overview", "codepath", "evidence", "fix"];
|
|
2731
|
+
const currentIndex = tabs.indexOf(activeTab);
|
|
2732
|
+
setActiveTab(tabs[(currentIndex + 1) % tabs.length]);
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2736
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
2737
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2738
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
2739
|
+
bug.id,
|
|
2740
|
+
" "
|
|
2741
|
+
] }),
|
|
2742
|
+
/* @__PURE__ */ jsxs(Text, { color: getSeverityColor2(bug.severity), bold: true, children: [
|
|
2743
|
+
"[",
|
|
2744
|
+
bug.severity.toUpperCase(),
|
|
2745
|
+
"]"
|
|
2746
|
+
] }),
|
|
2747
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
2748
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: bug.title })
|
|
2749
|
+
] }),
|
|
2750
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
2751
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "File: " }),
|
|
2752
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: bug.file }),
|
|
2753
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: ":" }),
|
|
2754
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: bug.line }),
|
|
2755
|
+
bug.endLine && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2756
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "-" }),
|
|
2757
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: bug.endLine })
|
|
2758
|
+
] })
|
|
2759
|
+
] }),
|
|
2760
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2761
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Category: " }),
|
|
2762
|
+
/* @__PURE__ */ jsx(Text, { children: formatCategory3(bug.category) }),
|
|
2763
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " | Confidence: " }),
|
|
2764
|
+
/* @__PURE__ */ jsx(Text, { color: getConfidenceColor2(bug.confidence.overall), children: bug.confidence.overall.toUpperCase() }),
|
|
2765
|
+
bug.confidence.adversarialSurvived && /* @__PURE__ */ jsx(Text, { color: "green", children: " \u2713 Validated" })
|
|
2766
|
+
] })
|
|
2767
|
+
] }),
|
|
2768
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
2769
|
+
/* @__PURE__ */ jsx(TabButton, { label: "1:Overview", active: activeTab === "overview" }),
|
|
2770
|
+
/* @__PURE__ */ jsx(TabButton, { label: "2:Code Path", active: activeTab === "codepath" }),
|
|
2771
|
+
/* @__PURE__ */ jsx(TabButton, { label: "3:Evidence", active: activeTab === "evidence" }),
|
|
2772
|
+
/* @__PURE__ */ jsx(TabButton, { label: "4:Fix", active: activeTab === "fix" })
|
|
2773
|
+
] }),
|
|
2774
|
+
/* @__PURE__ */ jsxs(
|
|
2775
|
+
Box,
|
|
2776
|
+
{
|
|
2777
|
+
flexDirection: "column",
|
|
2778
|
+
borderStyle: "single",
|
|
2779
|
+
borderColor: "gray",
|
|
2780
|
+
padding: 1,
|
|
2781
|
+
minHeight: 10,
|
|
2782
|
+
children: [
|
|
2783
|
+
activeTab === "overview" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2784
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Description" }),
|
|
2785
|
+
/* @__PURE__ */ jsx(Text, { wrap: "wrap", children: bug.description })
|
|
2786
|
+
] }),
|
|
2787
|
+
activeTab === "codepath" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2788
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
2789
|
+
"Code Path (",
|
|
2790
|
+
bug.codePath.length,
|
|
2791
|
+
" steps)"
|
|
2792
|
+
] }),
|
|
2793
|
+
bug.codePath.length === 0 ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "No code path available" }) : bug.codePath.map((step, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: i > 0 ? 1 : 0, children: [
|
|
2794
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2795
|
+
/* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
|
|
2796
|
+
step.step,
|
|
2797
|
+
". "
|
|
2798
|
+
] }),
|
|
2799
|
+
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
2800
|
+
step.file,
|
|
2801
|
+
":",
|
|
2802
|
+
step.line
|
|
2803
|
+
] })
|
|
2804
|
+
] }),
|
|
2805
|
+
step.code && /* @__PURE__ */ jsx(Box, { marginLeft: 3, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: step.code }) }),
|
|
2806
|
+
/* @__PURE__ */ jsx(Box, { marginLeft: 3, children: /* @__PURE__ */ jsx(Text, { children: step.explanation }) })
|
|
2807
|
+
] }, i))
|
|
2808
|
+
] }),
|
|
2809
|
+
activeTab === "evidence" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2810
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
2811
|
+
"Evidence (",
|
|
2812
|
+
bug.evidence.length,
|
|
2813
|
+
" items)"
|
|
2814
|
+
] }),
|
|
2815
|
+
bug.evidence.length === 0 ? /* @__PURE__ */ jsx(Text, { color: "gray", children: "No evidence provided" }) : bug.evidence.map((e, i) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
2816
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2022 " }),
|
|
2817
|
+
/* @__PURE__ */ jsx(Text, { children: e })
|
|
2818
|
+
] }, i)),
|
|
2819
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 2, children: [
|
|
2820
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Confidence Breakdown" }),
|
|
2821
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2822
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Code Path Validity: " }),
|
|
2823
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
2824
|
+
(bug.confidence.codePathValidity * 100).toFixed(0),
|
|
2825
|
+
"%"
|
|
2826
|
+
] })
|
|
2827
|
+
] }),
|
|
2828
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2829
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Reachability: " }),
|
|
2830
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
2831
|
+
(bug.confidence.reachability * 100).toFixed(0),
|
|
2832
|
+
"%"
|
|
2833
|
+
] })
|
|
2834
|
+
] }),
|
|
2835
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2836
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Intent Violation: " }),
|
|
2837
|
+
/* @__PURE__ */ jsx(Text, { children: bug.confidence.intentViolation ? "Yes" : "No" })
|
|
2838
|
+
] }),
|
|
2839
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2840
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Static Tool Signal: " }),
|
|
2841
|
+
/* @__PURE__ */ jsx(Text, { children: bug.confidence.staticToolSignal ? "Yes" : "No" })
|
|
2842
|
+
] }),
|
|
2843
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2844
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Adversarial Validated: " }),
|
|
2845
|
+
/* @__PURE__ */ jsx(Text, { color: bug.confidence.adversarialSurvived ? "green" : "gray", children: bug.confidence.adversarialSurvived ? "Yes \u2713" : "No" })
|
|
2846
|
+
] })
|
|
2847
|
+
] })
|
|
2848
|
+
] }),
|
|
2849
|
+
activeTab === "fix" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2850
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Suggested Fix" }),
|
|
2851
|
+
bug.suggestedFix ? /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx(Text, { color: "green", children: bug.suggestedFix }) }) : /* @__PURE__ */ jsx(Text, { color: "gray", children: "No suggested fix available. Press [f] to generate one." })
|
|
2852
|
+
] })
|
|
2853
|
+
]
|
|
2854
|
+
}
|
|
2855
|
+
),
|
|
2856
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [
|
|
2857
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
2858
|
+
"Bug ",
|
|
2859
|
+
index + 1,
|
|
2860
|
+
" of ",
|
|
2861
|
+
total
|
|
2862
|
+
] }),
|
|
2863
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "[\u2190/p] Prev [\u2192/n] Next [f] Fix [Tab] Switch tab [b] Back" })
|
|
2864
|
+
] })
|
|
2865
|
+
] });
|
|
2866
|
+
};
|
|
2867
|
+
var TabButton = ({ label, active }) => /* @__PURE__ */ jsx(Box, { marginRight: 2, children: /* @__PURE__ */ jsx(Text, { color: active ? "cyan" : "gray", bold: active, underline: active, children: label }) });
|
|
2868
|
+
function getSeverityColor2(severity) {
|
|
2869
|
+
switch (severity) {
|
|
2870
|
+
case "critical":
|
|
2871
|
+
return "red";
|
|
2872
|
+
case "high":
|
|
2873
|
+
return "yellow";
|
|
2874
|
+
case "medium":
|
|
2875
|
+
return "blue";
|
|
2876
|
+
case "low":
|
|
2877
|
+
return "gray";
|
|
2878
|
+
default:
|
|
2879
|
+
return "white";
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
function getConfidenceColor2(confidence) {
|
|
2883
|
+
switch (confidence) {
|
|
2884
|
+
case "high":
|
|
2885
|
+
return "green";
|
|
2886
|
+
case "medium":
|
|
2887
|
+
return "yellow";
|
|
2888
|
+
case "low":
|
|
2889
|
+
return "red";
|
|
2890
|
+
default:
|
|
2891
|
+
return "white";
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
function formatCategory3(category) {
|
|
2895
|
+
return category.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2896
|
+
}
|
|
2897
|
+
var FixConfirm = ({ bug, dryRun, onConfirm, onCancel }) => {
|
|
2898
|
+
const [status, setStatus] = useState("confirm");
|
|
2899
|
+
const [error, setError] = useState(null);
|
|
2900
|
+
useInput(async (input, key) => {
|
|
2901
|
+
if (status !== "confirm") return;
|
|
2902
|
+
if (input === "y" || key.return) {
|
|
2903
|
+
setStatus("fixing");
|
|
2904
|
+
try {
|
|
2905
|
+
await onConfirm();
|
|
2906
|
+
setStatus("done");
|
|
2907
|
+
setTimeout(() => {
|
|
2908
|
+
onCancel();
|
|
2909
|
+
}, 1500);
|
|
2910
|
+
} catch (e) {
|
|
2911
|
+
setError(e.message || "Unknown error");
|
|
2912
|
+
setStatus("error");
|
|
2913
|
+
}
|
|
2914
|
+
} else if (input === "n" || key.escape) {
|
|
2915
|
+
onCancel();
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2919
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
2920
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2921
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Fix: " }),
|
|
2922
|
+
/* @__PURE__ */ jsx(Text, { children: bug.title })
|
|
2923
|
+
] }),
|
|
2924
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2925
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "File: " }),
|
|
2926
|
+
/* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
|
|
2927
|
+
bug.file,
|
|
2928
|
+
":",
|
|
2929
|
+
bug.line
|
|
2930
|
+
] })
|
|
2931
|
+
] })
|
|
2932
|
+
] }),
|
|
2933
|
+
/* @__PURE__ */ jsxs(
|
|
2934
|
+
Box,
|
|
2935
|
+
{
|
|
2936
|
+
flexDirection: "column",
|
|
2937
|
+
borderStyle: "single",
|
|
2938
|
+
borderColor: "gray",
|
|
2939
|
+
padding: 1,
|
|
2940
|
+
marginBottom: 1,
|
|
2941
|
+
children: [
|
|
2942
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Proposed Fix:" }),
|
|
2943
|
+
bug.suggestedFix ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "green", children: bug.suggestedFix }) }) : /* @__PURE__ */ jsx(Text, { color: "yellow", children: "No suggested fix available. Will ask AI to generate and apply a fix." })
|
|
2944
|
+
]
|
|
2945
|
+
}
|
|
2946
|
+
),
|
|
2947
|
+
status === "confirm" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2948
|
+
dryRun && /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "DRY RUN MODE - Changes will NOT be applied" }) }),
|
|
2949
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2950
|
+
/* @__PURE__ */ jsx(Text, { children: "Apply this fix? " }),
|
|
2951
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "[y]es" }),
|
|
2952
|
+
/* @__PURE__ */ jsx(Text, { children: " / " }),
|
|
2953
|
+
/* @__PURE__ */ jsx(Text, { color: "red", children: "[n]o" })
|
|
2954
|
+
] })
|
|
2955
|
+
] }),
|
|
2956
|
+
status === "fixing" && /* @__PURE__ */ jsxs(Box, { children: [
|
|
2957
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
|
|
2958
|
+
/* @__PURE__ */ jsx(Text, { children: " Applying fix..." })
|
|
2959
|
+
] }),
|
|
2960
|
+
status === "done" && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713 Fix applied successfully!" }) }),
|
|
2961
|
+
status === "error" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2962
|
+
/* @__PURE__ */ jsx(Text, { color: "red", children: "\u2717 Failed to apply fix" }),
|
|
2963
|
+
error && /* @__PURE__ */ jsx(Text, { color: "gray", children: error }),
|
|
2964
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Press any key to go back" }) })
|
|
2965
|
+
] })
|
|
2966
|
+
] });
|
|
2967
|
+
};
|
|
2968
|
+
var App = ({ bugs, config, fixOptions, onFix, onExit }) => {
|
|
2969
|
+
const { exit } = useApp();
|
|
2970
|
+
const [state, setState] = useState({
|
|
2971
|
+
screen: "dashboard",
|
|
2972
|
+
bugs,
|
|
2973
|
+
selectedCategory: null,
|
|
2974
|
+
selectedBugIndex: 0,
|
|
2975
|
+
config,
|
|
2976
|
+
fixOptions
|
|
2977
|
+
});
|
|
2978
|
+
const filteredBugs = state.selectedCategory ? state.bugs.filter((b) => b.category === state.selectedCategory || b.severity === state.selectedCategory) : state.bugs;
|
|
2979
|
+
const selectedBug = filteredBugs[state.selectedBugIndex];
|
|
2980
|
+
useInput((input, key) => {
|
|
2981
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
2982
|
+
onExit();
|
|
2983
|
+
exit();
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
if (key.escape) {
|
|
2987
|
+
if (state.screen === "fix") {
|
|
2988
|
+
setState((s) => ({ ...s, screen: "detail" }));
|
|
2989
|
+
} else if (state.screen === "detail") {
|
|
2990
|
+
setState((s) => ({ ...s, screen: "list" }));
|
|
2991
|
+
} else if (state.screen === "list") {
|
|
2992
|
+
setState((s) => ({ ...s, screen: "dashboard", selectedCategory: null }));
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
const handleSelectCategory = (category) => {
|
|
2997
|
+
setState((s) => ({
|
|
2998
|
+
...s,
|
|
2999
|
+
screen: "list",
|
|
3000
|
+
selectedCategory: category,
|
|
3001
|
+
selectedBugIndex: 0
|
|
3002
|
+
}));
|
|
3003
|
+
};
|
|
3004
|
+
const handleSelectBug = (index) => {
|
|
3005
|
+
setState((s) => ({
|
|
3006
|
+
...s,
|
|
3007
|
+
screen: "detail",
|
|
3008
|
+
selectedBugIndex: index
|
|
3009
|
+
}));
|
|
3010
|
+
};
|
|
3011
|
+
const handleStartFix = () => {
|
|
3012
|
+
setState((s) => ({ ...s, screen: "fix" }));
|
|
3013
|
+
};
|
|
3014
|
+
const handleConfirmFix = async () => {
|
|
3015
|
+
if (selectedBug) {
|
|
3016
|
+
await onFix(selectedBug);
|
|
3017
|
+
if (state.selectedBugIndex < filteredBugs.length - 1) {
|
|
3018
|
+
setState((s) => ({
|
|
3019
|
+
...s,
|
|
3020
|
+
screen: "detail",
|
|
3021
|
+
selectedBugIndex: s.selectedBugIndex + 1
|
|
3022
|
+
}));
|
|
3023
|
+
} else {
|
|
3024
|
+
setState((s) => ({ ...s, screen: "list" }));
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
const handleCancelFix = () => {
|
|
3029
|
+
setState((s) => ({ ...s, screen: "detail" }));
|
|
3030
|
+
};
|
|
3031
|
+
const handleBack = () => {
|
|
3032
|
+
if (state.screen === "fix") {
|
|
3033
|
+
setState((s) => ({ ...s, screen: "detail" }));
|
|
3034
|
+
} else if (state.screen === "detail") {
|
|
3035
|
+
setState((s) => ({ ...s, screen: "list" }));
|
|
3036
|
+
} else if (state.screen === "list") {
|
|
3037
|
+
setState((s) => ({ ...s, screen: "dashboard", selectedCategory: null }));
|
|
3038
|
+
}
|
|
3039
|
+
};
|
|
3040
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
3041
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
3042
|
+
/* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: "whiterose" }),
|
|
3043
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " - fix mode" }),
|
|
3044
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " | " }),
|
|
3045
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
3046
|
+
state.screen === "dashboard" && "Dashboard",
|
|
3047
|
+
state.screen === "list" && `Bugs${state.selectedCategory ? ` (${state.selectedCategory})` : ""}`,
|
|
3048
|
+
state.screen === "detail" && `Bug ${state.selectedBugIndex + 1}/${filteredBugs.length}`,
|
|
3049
|
+
state.screen === "fix" && "Confirm Fix"
|
|
3050
|
+
] })
|
|
3051
|
+
] }),
|
|
3052
|
+
state.screen === "dashboard" && /* @__PURE__ */ jsx(Dashboard, { bugs: state.bugs, onSelectCategory: handleSelectCategory }),
|
|
3053
|
+
state.screen === "list" && /* @__PURE__ */ jsx(
|
|
3054
|
+
BugList,
|
|
3055
|
+
{
|
|
3056
|
+
bugs: filteredBugs,
|
|
3057
|
+
selectedIndex: state.selectedBugIndex,
|
|
3058
|
+
onSelect: handleSelectBug,
|
|
3059
|
+
onBack: handleBack
|
|
3060
|
+
}
|
|
3061
|
+
),
|
|
3062
|
+
state.screen === "detail" && selectedBug && /* @__PURE__ */ jsx(
|
|
3063
|
+
BugDetail,
|
|
3064
|
+
{
|
|
3065
|
+
bug: selectedBug,
|
|
3066
|
+
index: state.selectedBugIndex,
|
|
3067
|
+
total: filteredBugs.length,
|
|
3068
|
+
onFix: handleStartFix,
|
|
3069
|
+
onNext: () => setState((s) => ({
|
|
3070
|
+
...s,
|
|
3071
|
+
selectedBugIndex: Math.min(s.selectedBugIndex + 1, filteredBugs.length - 1)
|
|
3072
|
+
})),
|
|
3073
|
+
onPrev: () => setState((s) => ({
|
|
3074
|
+
...s,
|
|
3075
|
+
selectedBugIndex: Math.max(s.selectedBugIndex - 1, 0)
|
|
3076
|
+
})),
|
|
3077
|
+
onBack: handleBack
|
|
3078
|
+
}
|
|
3079
|
+
),
|
|
3080
|
+
state.screen === "fix" && selectedBug && /* @__PURE__ */ jsx(
|
|
3081
|
+
FixConfirm,
|
|
3082
|
+
{
|
|
3083
|
+
bug: selectedBug,
|
|
3084
|
+
dryRun: fixOptions.dryRun,
|
|
3085
|
+
onConfirm: handleConfirmFix,
|
|
3086
|
+
onCancel: handleCancelFix
|
|
3087
|
+
}
|
|
3088
|
+
),
|
|
3089
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "[q] Quit [esc] Back [\u2191\u2193] Navigate [enter] Select [f] Fix" }) })
|
|
3090
|
+
] });
|
|
3091
|
+
};
|
|
3092
|
+
var GIT_TIMEOUT = 3e4;
|
|
3093
|
+
async function createFixBranch(branchName, bug, cwd = process.cwd()) {
|
|
3094
|
+
const safeBugId = bug.id.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
3095
|
+
const safeTitle = bug.title.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, "-").slice(0, 30);
|
|
3096
|
+
const fullBranchName = branchName || `whiterose/fix-${safeBugId}-${safeTitle}`;
|
|
3097
|
+
try {
|
|
3098
|
+
try {
|
|
3099
|
+
await execa("git", ["rev-parse", "--verify", fullBranchName], {
|
|
3100
|
+
cwd,
|
|
3101
|
+
timeout: GIT_TIMEOUT
|
|
3102
|
+
});
|
|
3103
|
+
await execa("git", ["checkout", fullBranchName], { cwd, timeout: GIT_TIMEOUT });
|
|
3104
|
+
} catch {
|
|
3105
|
+
await execa("git", ["checkout", "-b", fullBranchName], { cwd, timeout: GIT_TIMEOUT });
|
|
3106
|
+
}
|
|
3107
|
+
return fullBranchName;
|
|
3108
|
+
} catch (error) {
|
|
3109
|
+
throw new Error(`Failed to create branch: ${error.message}`);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
async function commitFix(bug, cwd = process.cwd()) {
|
|
3113
|
+
try {
|
|
3114
|
+
await execa("git", ["add", bug.file], { cwd, timeout: GIT_TIMEOUT });
|
|
3115
|
+
const { stdout: diff } = await execa("git", ["diff", "--cached", "--name-only"], {
|
|
3116
|
+
cwd,
|
|
3117
|
+
timeout: GIT_TIMEOUT
|
|
3118
|
+
});
|
|
3119
|
+
if (!diff.trim()) {
|
|
3120
|
+
return "";
|
|
3121
|
+
}
|
|
3122
|
+
const commitMessage = `fix(${bug.category}): ${bug.title}
|
|
3123
|
+
|
|
3124
|
+
Bug ID: ${bug.id}
|
|
3125
|
+
File: ${bug.file}:${bug.line}
|
|
3126
|
+
Severity: ${bug.severity}
|
|
3127
|
+
|
|
3128
|
+
${bug.description}
|
|
3129
|
+
|
|
3130
|
+
Fixed by whiterose`;
|
|
3131
|
+
await execa("git", ["commit", "-m", commitMessage], {
|
|
3132
|
+
cwd,
|
|
3133
|
+
timeout: GIT_TIMEOUT
|
|
3134
|
+
});
|
|
3135
|
+
const { stdout: hash } = await execa("git", ["rev-parse", "HEAD"], {
|
|
3136
|
+
cwd,
|
|
3137
|
+
timeout: GIT_TIMEOUT
|
|
3138
|
+
});
|
|
3139
|
+
return hash.trim();
|
|
3140
|
+
} catch (error) {
|
|
3141
|
+
throw new Error(`Failed to commit fix: ${error.message}`);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
// src/core/fixer.ts
|
|
3146
|
+
function isPathWithinProject(filePath, projectDir) {
|
|
3147
|
+
const resolvedPath = resolve(projectDir, filePath);
|
|
3148
|
+
const relativePath = relative(projectDir, resolvedPath);
|
|
3149
|
+
return !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
3150
|
+
}
|
|
3151
|
+
function validateFilePath(filePath, projectDir) {
|
|
3152
|
+
const resolvedPath = isAbsolute(filePath) ? filePath : resolve(projectDir, filePath);
|
|
3153
|
+
if (!isPathWithinProject(resolvedPath, projectDir)) {
|
|
3154
|
+
throw new Error(`Security: Refusing to access file outside project directory: ${filePath}`);
|
|
3155
|
+
}
|
|
3156
|
+
if (filePath.includes("\0") || filePath.includes("..")) {
|
|
3157
|
+
throw new Error(`Security: Invalid file path contains suspicious characters: ${filePath}`);
|
|
3158
|
+
}
|
|
3159
|
+
return resolvedPath;
|
|
3160
|
+
}
|
|
3161
|
+
async function applyFix(bug, config, options) {
|
|
3162
|
+
const { dryRun, branch } = options;
|
|
3163
|
+
const projectDir = process.cwd();
|
|
3164
|
+
let safePath;
|
|
3165
|
+
try {
|
|
3166
|
+
safePath = validateFilePath(bug.file, projectDir);
|
|
3167
|
+
} catch (error) {
|
|
3168
|
+
return {
|
|
3169
|
+
success: false,
|
|
3170
|
+
error: error.message
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
if (!existsSync(safePath)) {
|
|
3174
|
+
return {
|
|
3175
|
+
success: false,
|
|
3176
|
+
error: `File not found: ${bug.file}`
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
const originalContent = readFileSync(safePath, "utf-8");
|
|
3180
|
+
const lines = originalContent.split("\n");
|
|
3181
|
+
let fixedContent;
|
|
3182
|
+
let diff;
|
|
3183
|
+
if (bug.suggestedFix) {
|
|
3184
|
+
const result = applySimpleFix(lines, bug.line, bug.endLine || bug.line, bug.suggestedFix);
|
|
3185
|
+
if (result.success) {
|
|
3186
|
+
fixedContent = result.content;
|
|
3187
|
+
diff = result.diff;
|
|
3188
|
+
} else {
|
|
3189
|
+
const llmResult = await generateAndApplyFix(bug, config, originalContent, safePath);
|
|
3190
|
+
if (!llmResult.success) {
|
|
3191
|
+
return llmResult;
|
|
3192
|
+
}
|
|
3193
|
+
fixedContent = llmResult.content;
|
|
3194
|
+
diff = llmResult.diff;
|
|
3195
|
+
}
|
|
3196
|
+
} else {
|
|
3197
|
+
const llmResult = await generateAndApplyFix(bug, config, originalContent, safePath);
|
|
3198
|
+
if (!llmResult.success) {
|
|
3199
|
+
return llmResult;
|
|
3200
|
+
}
|
|
3201
|
+
fixedContent = llmResult.content;
|
|
3202
|
+
diff = llmResult.diff;
|
|
3203
|
+
}
|
|
3204
|
+
if (dryRun) {
|
|
3205
|
+
console.log("\n--- Dry Run: Proposed changes ---");
|
|
3206
|
+
console.log(diff);
|
|
3207
|
+
console.log("--- End of proposed changes ---\n");
|
|
3208
|
+
return {
|
|
3209
|
+
success: true,
|
|
3210
|
+
diff
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
let branchName;
|
|
3214
|
+
if (branch) {
|
|
3215
|
+
branchName = await createFixBranch(branch, bug);
|
|
3216
|
+
}
|
|
3217
|
+
writeFileSync(safePath, fixedContent, "utf-8");
|
|
3218
|
+
if (branchName || !branch) {
|
|
3219
|
+
await commitFix(bug);
|
|
3220
|
+
}
|
|
3221
|
+
return {
|
|
3222
|
+
success: true,
|
|
3223
|
+
diff,
|
|
3224
|
+
branchName
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
function applySimpleFix(lines, startLine, endLine, fix) {
|
|
3228
|
+
const lineIndex = startLine - 1;
|
|
3229
|
+
const endIndex = endLine - 1;
|
|
3230
|
+
if (lineIndex < 0 || lineIndex >= lines.length) {
|
|
3231
|
+
return { success: false, content: "", diff: "" };
|
|
3232
|
+
}
|
|
3233
|
+
const originalLine = lines[lineIndex];
|
|
3234
|
+
const indentMatch = originalLine.match(/^(\s*)/);
|
|
3235
|
+
const indent = indentMatch ? indentMatch[1] : "";
|
|
3236
|
+
const fixLines = fix.split("\n").map((line, i) => {
|
|
3237
|
+
if (i === 0 || line.trim() === "") return line;
|
|
3238
|
+
return indent + line.trimStart();
|
|
3239
|
+
});
|
|
3240
|
+
const removedLines = lines.slice(lineIndex, endIndex + 1);
|
|
3241
|
+
const diff = [
|
|
3242
|
+
`--- ${lines[lineIndex]}`,
|
|
3243
|
+
...removedLines.map((l) => `- ${l}`),
|
|
3244
|
+
...fixLines.map((l) => `+ ${l}`)
|
|
3245
|
+
].join("\n");
|
|
3246
|
+
const newLines = [...lines.slice(0, lineIndex), ...fixLines, ...lines.slice(endIndex + 1)];
|
|
3247
|
+
return {
|
|
3248
|
+
success: true,
|
|
3249
|
+
content: newLines.join("\n"),
|
|
3250
|
+
diff
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
async function generateAndApplyFix(bug, _config, originalContent, safePath) {
|
|
3254
|
+
try {
|
|
3255
|
+
const prompt = buildFixPrompt(bug, originalContent);
|
|
3256
|
+
const workingDir = safePath ? dirname(safePath) : process.cwd();
|
|
3257
|
+
const { stdout } = await execa(
|
|
3258
|
+
"claude",
|
|
3259
|
+
["-p", prompt, "--output-format", "text"],
|
|
3260
|
+
{
|
|
3261
|
+
cwd: workingDir,
|
|
3262
|
+
timeout: 12e4,
|
|
3263
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
3264
|
+
}
|
|
3265
|
+
);
|
|
3266
|
+
const fixedContent = parseFixResponse(stdout, originalContent);
|
|
3267
|
+
if (!fixedContent) {
|
|
3268
|
+
return {
|
|
3269
|
+
success: false,
|
|
3270
|
+
error: "Failed to parse fix from LLM response"
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
const diff = generateDiff(originalContent, fixedContent, bug.file);
|
|
3274
|
+
return {
|
|
3275
|
+
success: true,
|
|
3276
|
+
content: fixedContent,
|
|
3277
|
+
diff
|
|
3278
|
+
};
|
|
3279
|
+
} catch (error) {
|
|
3280
|
+
return {
|
|
3281
|
+
success: false,
|
|
3282
|
+
error: error.message || "Unknown error generating fix"
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
function buildFixPrompt(bug, originalContent) {
|
|
3287
|
+
return `Fix the following bug in the code.
|
|
3288
|
+
|
|
3289
|
+
BUG DETAILS:
|
|
3290
|
+
- Title: ${bug.title}
|
|
3291
|
+
- Description: ${bug.description}
|
|
3292
|
+
- File: ${bug.file}
|
|
3293
|
+
- Line: ${bug.line}${bug.endLine ? `-${bug.endLine}` : ""}
|
|
3294
|
+
- Category: ${bug.category}
|
|
3295
|
+
- Severity: ${bug.severity}
|
|
3296
|
+
|
|
3297
|
+
EVIDENCE:
|
|
3298
|
+
${bug.evidence.map((e) => `- ${e}`).join("\n")}
|
|
3299
|
+
|
|
3300
|
+
CODE PATH:
|
|
3301
|
+
${bug.codePath.map((s) => `${s.step}. ${s.file}:${s.line} - ${s.explanation}`).join("\n")}
|
|
3302
|
+
|
|
3303
|
+
ORIGINAL FILE CONTENT:
|
|
3304
|
+
\`\`\`
|
|
3305
|
+
${originalContent}
|
|
3306
|
+
\`\`\`
|
|
3307
|
+
|
|
3308
|
+
${bug.suggestedFix ? `SUGGESTED FIX APPROACH:
|
|
3309
|
+
${bug.suggestedFix}
|
|
3310
|
+
|
|
3311
|
+
` : ""}
|
|
3312
|
+
|
|
3313
|
+
Please provide the COMPLETE fixed file content. Output ONLY the fixed code, no explanations.
|
|
3314
|
+
Wrap the code in \`\`\` code blocks.
|
|
3315
|
+
|
|
3316
|
+
IMPORTANT:
|
|
3317
|
+
- Fix ONLY the identified bug
|
|
3318
|
+
- Do not refactor or change anything else
|
|
3319
|
+
- Preserve all formatting and style
|
|
3320
|
+
- Ensure the fix actually addresses the bug`;
|
|
3321
|
+
}
|
|
3322
|
+
function parseFixResponse(response, originalContent) {
|
|
3323
|
+
const codeBlockMatch = response.match(/```(?:\w+)?\s*([\s\S]*?)```/);
|
|
3324
|
+
if (codeBlockMatch) {
|
|
3325
|
+
const extracted = codeBlockMatch[1].trim();
|
|
3326
|
+
const originalLines = originalContent.split("\n").slice(0, 5);
|
|
3327
|
+
const extractedLines = extracted.split("\n").slice(0, 5);
|
|
3328
|
+
let matchCount = 0;
|
|
3329
|
+
for (const origLine of originalLines) {
|
|
3330
|
+
if (extractedLines.some((l) => l.trim() === origLine.trim())) {
|
|
3331
|
+
matchCount++;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
if (matchCount >= 2 || extracted.length > originalContent.length * 0.5) {
|
|
3335
|
+
return extracted;
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
if (response.includes("function") || response.includes("const ") || response.includes("import ")) {
|
|
3339
|
+
return response.trim();
|
|
3340
|
+
}
|
|
3341
|
+
return null;
|
|
3342
|
+
}
|
|
3343
|
+
function generateDiff(original, fixed, filename) {
|
|
3344
|
+
const origLines = original.split("\n");
|
|
3345
|
+
const fixedLines = fixed.split("\n");
|
|
3346
|
+
const diff = [`--- a/${basename(filename)}`, `+++ b/${basename(filename)}`];
|
|
3347
|
+
const maxLen = Math.max(origLines.length, fixedLines.length);
|
|
3348
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3349
|
+
const origLine = origLines[i];
|
|
3350
|
+
const fixedLine = fixedLines[i];
|
|
3351
|
+
if (origLine === void 0) {
|
|
3352
|
+
diff.push(`+ ${fixedLine}`);
|
|
3353
|
+
} else if (fixedLine === void 0) {
|
|
3354
|
+
diff.push(`- ${origLine}`);
|
|
3355
|
+
} else if (origLine !== fixedLine) {
|
|
3356
|
+
diff.push(`- ${origLine}`);
|
|
3357
|
+
diff.push(`+ ${fixedLine}`);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
return diff.join("\n");
|
|
3361
|
+
}
|
|
3362
|
+
async function startFixTUI(bugs, config, options) {
|
|
3363
|
+
return new Promise((resolve3) => {
|
|
3364
|
+
const handleFix = async (bug) => {
|
|
3365
|
+
await applyFix(bug, config, options);
|
|
3366
|
+
};
|
|
3367
|
+
const handleExit = () => {
|
|
3368
|
+
resolve3();
|
|
3369
|
+
};
|
|
3370
|
+
const { unmount, waitUntilExit } = render(
|
|
3371
|
+
/* @__PURE__ */ jsx(
|
|
3372
|
+
App,
|
|
3373
|
+
{
|
|
3374
|
+
bugs,
|
|
3375
|
+
config,
|
|
3376
|
+
fixOptions: options,
|
|
3377
|
+
onFix: handleFix,
|
|
3378
|
+
onExit: handleExit
|
|
3379
|
+
}
|
|
3380
|
+
)
|
|
3381
|
+
);
|
|
3382
|
+
waitUntilExit().then(() => {
|
|
3383
|
+
resolve3();
|
|
3384
|
+
});
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// src/cli/commands/fix.ts
|
|
3389
|
+
async function fixCommand(bugId, options) {
|
|
3390
|
+
const cwd = process.cwd();
|
|
3391
|
+
const whiterosePath = join(cwd, ".whiterose");
|
|
3392
|
+
if (options.sarif) {
|
|
3393
|
+
const sarifPath = isAbsolute(options.sarif) ? options.sarif : resolve(cwd, options.sarif);
|
|
3394
|
+
if (!existsSync(sarifPath)) {
|
|
3395
|
+
p3.log.error(`SARIF file not found: ${sarifPath}`);
|
|
3396
|
+
process.exit(1);
|
|
3397
|
+
}
|
|
3398
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - fixing bugs from external SARIF"));
|
|
3399
|
+
const bugs2 = loadBugsFromSarif(sarifPath);
|
|
3400
|
+
if (bugs2.length === 0) {
|
|
3401
|
+
p3.log.success("No bugs found in SARIF file!");
|
|
3402
|
+
process.exit(0);
|
|
3403
|
+
}
|
|
3404
|
+
p3.log.info(`Loaded ${bugs2.length} bugs from ${options.sarif}`);
|
|
3405
|
+
const config2 = existsSync(whiterosePath) ? await loadConfig(cwd) : getDefaultConfig();
|
|
3406
|
+
return await processBugList(bugs2, config2, options, bugId);
|
|
3407
|
+
}
|
|
3408
|
+
if (options.github) {
|
|
3409
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - fixing bug from GitHub issue"));
|
|
3410
|
+
const bug = await loadBugFromGitHub(options.github, cwd);
|
|
3411
|
+
if (!bug) {
|
|
3412
|
+
p3.log.error("Failed to parse GitHub issue as a bug");
|
|
3413
|
+
process.exit(1);
|
|
3414
|
+
}
|
|
3415
|
+
p3.log.info(`Loaded bug from GitHub: ${bug.title}`);
|
|
3416
|
+
const config2 = existsSync(whiterosePath) ? await loadConfig(cwd) : getDefaultConfig();
|
|
3417
|
+
return await fixSingleBug(bug, config2, options);
|
|
3418
|
+
}
|
|
3419
|
+
if (options.describe) {
|
|
3420
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - fixing manually described bug"));
|
|
3421
|
+
const bug = await collectManualBugDescription(cwd);
|
|
3422
|
+
if (!bug) {
|
|
3423
|
+
p3.cancel("Bug description cancelled.");
|
|
3424
|
+
process.exit(0);
|
|
3425
|
+
}
|
|
3426
|
+
const config2 = existsSync(whiterosePath) ? await loadConfig(cwd) : getDefaultConfig();
|
|
3427
|
+
return await fixSingleBug(bug, config2, options);
|
|
3428
|
+
}
|
|
3429
|
+
if (!existsSync(whiterosePath)) {
|
|
3430
|
+
p3.log.error("whiterose is not initialized in this directory.");
|
|
3431
|
+
p3.log.info('Run "whiterose init" first, or use:');
|
|
3432
|
+
p3.log.info(" --sarif <path> Fix bugs from an external SARIF file");
|
|
3433
|
+
p3.log.info(" --github <url> Fix bug from a GitHub issue");
|
|
3434
|
+
p3.log.info(" --describe Manually describe a bug to fix");
|
|
3435
|
+
process.exit(1);
|
|
3436
|
+
}
|
|
3437
|
+
const config = await loadConfig(cwd);
|
|
3438
|
+
const reportsDir = join(whiterosePath, "reports");
|
|
3439
|
+
if (!existsSync(reportsDir)) {
|
|
3440
|
+
p3.log.error('No scan results found. Run "whiterose scan" first.');
|
|
3441
|
+
p3.log.info("Or use --sarif, --github, or --describe for external bugs.");
|
|
3442
|
+
process.exit(1);
|
|
3443
|
+
}
|
|
3444
|
+
const reports = readdirSync(reportsDir).filter((f) => f.endsWith(".sarif")).sort().reverse();
|
|
3445
|
+
if (reports.length === 0) {
|
|
3446
|
+
p3.log.error('No scan results found. Run "whiterose scan" first.');
|
|
3447
|
+
p3.log.info("Or use --sarif, --github, or --describe for external bugs.");
|
|
3448
|
+
process.exit(1);
|
|
3449
|
+
}
|
|
3450
|
+
const latestReport = join(reportsDir, reports[0]);
|
|
3451
|
+
const bugs = loadBugsFromSarif(latestReport);
|
|
3452
|
+
return await processBugList(bugs, config, options, bugId);
|
|
3453
|
+
}
|
|
3454
|
+
function loadBugsFromSarif(sarifPath) {
|
|
3455
|
+
const sarif = JSON.parse(readFileSync(sarifPath, "utf-8"));
|
|
3456
|
+
return sarif.runs?.[0]?.results?.map((r, i) => {
|
|
3457
|
+
const props = r.properties || {};
|
|
3458
|
+
return {
|
|
3459
|
+
id: r.ruleId || `WR-${String(i + 1).padStart(3, "0")}`,
|
|
3460
|
+
title: r.message?.text || "Unknown bug",
|
|
3461
|
+
description: r.message?.markdown || r.message?.text || "",
|
|
3462
|
+
file: r.locations?.[0]?.physicalLocation?.artifactLocation?.uri || "unknown",
|
|
3463
|
+
line: r.locations?.[0]?.physicalLocation?.region?.startLine || 0,
|
|
3464
|
+
endLine: r.locations?.[0]?.physicalLocation?.region?.endLine,
|
|
3465
|
+
severity: mapSarifLevel(r.level),
|
|
3466
|
+
category: props.category || "logic-error",
|
|
3467
|
+
confidence: {
|
|
3468
|
+
overall: props.confidence || "medium",
|
|
3469
|
+
codePathValidity: props.codePathValidity || 0.8,
|
|
3470
|
+
reachability: props.reachability || 0.8,
|
|
3471
|
+
intentViolation: props.intentViolation || false,
|
|
3472
|
+
staticToolSignal: props.staticToolSignal || false,
|
|
3473
|
+
adversarialSurvived: props.adversarialSurvived || false
|
|
3474
|
+
},
|
|
3475
|
+
codePath: r.codeFlows?.[0]?.threadFlows?.[0]?.locations?.map((loc, idx) => ({
|
|
3476
|
+
step: idx + 1,
|
|
3477
|
+
file: loc.location?.physicalLocation?.artifactLocation?.uri || "",
|
|
3478
|
+
line: loc.location?.physicalLocation?.region?.startLine || 0,
|
|
3479
|
+
code: "",
|
|
3480
|
+
explanation: loc.message?.text || ""
|
|
3481
|
+
})) || [],
|
|
3482
|
+
evidence: props.evidence || [],
|
|
3483
|
+
suggestedFix: props.suggestedFix,
|
|
3484
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3485
|
+
};
|
|
3486
|
+
}) || [];
|
|
3487
|
+
}
|
|
3488
|
+
async function loadBugFromGitHub(issueUrl, cwd) {
|
|
3489
|
+
try {
|
|
3490
|
+
const match = issueUrl.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
3491
|
+
if (!match) {
|
|
3492
|
+
p3.log.error("Invalid GitHub issue URL format. Expected: https://github.com/owner/repo/issues/123");
|
|
3493
|
+
return null;
|
|
3494
|
+
}
|
|
3495
|
+
const [, owner, repo, issueNumber] = match;
|
|
3496
|
+
const { stdout } = await execa("gh", [
|
|
3497
|
+
"issue",
|
|
3498
|
+
"view",
|
|
3499
|
+
issueNumber,
|
|
3500
|
+
"--repo",
|
|
3501
|
+
`${owner}/${repo}`,
|
|
3502
|
+
"--json",
|
|
3503
|
+
"title,body,labels"
|
|
3504
|
+
], { cwd });
|
|
3505
|
+
const issue = JSON.parse(stdout);
|
|
3506
|
+
const fileMatch = issue.body?.match(/(?:file|path|location):\s*[`"]?([^\s`"]+)[`"]?/i) || issue.body?.match(/```[\w]*\n(?:\/\/|#)\s*([^\s:]+):(\d+)/);
|
|
3507
|
+
const lineMatch = issue.body?.match(/(?:line|L|:)(\d+)/);
|
|
3508
|
+
let severity = "medium";
|
|
3509
|
+
const labels = issue.labels?.map((l) => l.name.toLowerCase()) || [];
|
|
3510
|
+
if (labels.some((l) => l.includes("critical") || l.includes("security"))) {
|
|
3511
|
+
severity = "critical";
|
|
3512
|
+
} else if (labels.some((l) => l.includes("bug") || l.includes("high"))) {
|
|
3513
|
+
severity = "high";
|
|
3514
|
+
} else if (labels.some((l) => l.includes("low") || l.includes("minor"))) {
|
|
3515
|
+
severity = "low";
|
|
3516
|
+
}
|
|
3517
|
+
let category = "logic-error";
|
|
3518
|
+
if (labels.some((l) => l.includes("security"))) {
|
|
3519
|
+
category = "security";
|
|
3520
|
+
} else if (labels.some((l) => l.includes("null") || l.includes("undefined"))) {
|
|
3521
|
+
category = "null-reference";
|
|
3522
|
+
} else if (labels.some((l) => l.includes("async") || l.includes("race"))) {
|
|
3523
|
+
category = "async-race-condition";
|
|
3524
|
+
}
|
|
3525
|
+
return {
|
|
3526
|
+
id: `GH-${issueNumber}`,
|
|
3527
|
+
title: issue.title,
|
|
3528
|
+
description: issue.body || issue.title,
|
|
3529
|
+
file: fileMatch?.[1] || "",
|
|
3530
|
+
line: parseInt(lineMatch?.[1] || "1", 10),
|
|
3531
|
+
severity,
|
|
3532
|
+
category,
|
|
3533
|
+
confidence: {
|
|
3534
|
+
overall: "medium",
|
|
3535
|
+
codePathValidity: 0.5,
|
|
3536
|
+
reachability: 0.5,
|
|
3537
|
+
intentViolation: false,
|
|
3538
|
+
staticToolSignal: false,
|
|
3539
|
+
adversarialSurvived: false
|
|
3540
|
+
},
|
|
3541
|
+
codePath: [],
|
|
3542
|
+
evidence: [`GitHub issue: ${issueUrl}`],
|
|
3543
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3544
|
+
};
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
if (error.message?.includes("gh")) {
|
|
3547
|
+
p3.log.error("GitHub CLI (gh) is required for --github option.");
|
|
3548
|
+
p3.log.info("Install it: https://cli.github.com/");
|
|
3549
|
+
} else {
|
|
3550
|
+
p3.log.error(`Failed to fetch GitHub issue: ${error.message}`);
|
|
3551
|
+
}
|
|
3552
|
+
return null;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
async function collectManualBugDescription(cwd) {
|
|
3556
|
+
const file = await p3.text({
|
|
3557
|
+
message: "File path containing the bug:",
|
|
3558
|
+
placeholder: "src/components/Button.tsx",
|
|
3559
|
+
validate: (value) => {
|
|
3560
|
+
if (!value) return "File path is required";
|
|
3561
|
+
const fullPath = isAbsolute(value) ? value : resolve(cwd, value);
|
|
3562
|
+
if (!existsSync(fullPath)) return `File not found: ${value}`;
|
|
3563
|
+
return void 0;
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
if (p3.isCancel(file)) return null;
|
|
3567
|
+
const lineStr = await p3.text({
|
|
3568
|
+
message: "Line number (approximate is fine):",
|
|
3569
|
+
placeholder: "42",
|
|
3570
|
+
validate: (value) => {
|
|
3571
|
+
if (!value) return "Line number is required";
|
|
3572
|
+
if (isNaN(parseInt(value, 10))) return "Must be a number";
|
|
3573
|
+
return void 0;
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
if (p3.isCancel(lineStr)) return null;
|
|
3577
|
+
const title = await p3.text({
|
|
3578
|
+
message: "Bug title (brief description):",
|
|
3579
|
+
placeholder: "Null reference when user is not logged in"
|
|
3580
|
+
});
|
|
3581
|
+
if (p3.isCancel(title)) return null;
|
|
3582
|
+
const description = await p3.text({
|
|
3583
|
+
message: "Detailed description (what happens, how to trigger):",
|
|
3584
|
+
placeholder: "When user.profile is accessed before login check, TypeError is thrown"
|
|
3585
|
+
});
|
|
3586
|
+
if (p3.isCancel(description)) return null;
|
|
3587
|
+
const severity = await p3.select({
|
|
3588
|
+
message: "Bug severity:",
|
|
3589
|
+
options: [
|
|
3590
|
+
{ value: "critical", label: "Critical", hint: "security issue, data loss" },
|
|
3591
|
+
{ value: "high", label: "High", hint: "crash, incorrect behavior" },
|
|
3592
|
+
{ value: "medium", label: "Medium", hint: "bug with workaround" },
|
|
3593
|
+
{ value: "low", label: "Low", hint: "minor issue" }
|
|
3594
|
+
],
|
|
3595
|
+
initialValue: "medium"
|
|
3596
|
+
});
|
|
3597
|
+
if (p3.isCancel(severity)) return null;
|
|
3598
|
+
const category = await p3.select({
|
|
3599
|
+
message: "Bug category:",
|
|
3600
|
+
options: [
|
|
3601
|
+
{ value: "logic-error", label: "Logic Error" },
|
|
3602
|
+
{ value: "null-reference", label: "Null Reference" },
|
|
3603
|
+
{ value: "security", label: "Security" },
|
|
3604
|
+
{ value: "async-race-condition", label: "Async/Race Condition" },
|
|
3605
|
+
{ value: "edge-case", label: "Edge Case" },
|
|
3606
|
+
{ value: "type-coercion", label: "Type Coercion" },
|
|
3607
|
+
{ value: "resource-leak", label: "Resource Leak" },
|
|
3608
|
+
{ value: "intent-violation", label: "Intent Violation" }
|
|
3609
|
+
],
|
|
3610
|
+
initialValue: "logic-error"
|
|
3611
|
+
});
|
|
3612
|
+
if (p3.isCancel(category)) return null;
|
|
3613
|
+
const filePath = isAbsolute(file) ? file : resolve(cwd, file);
|
|
3614
|
+
const relativePath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
|
|
3615
|
+
return {
|
|
3616
|
+
id: `MANUAL-${Date.now()}`,
|
|
3617
|
+
title: title || "Manual bug",
|
|
3618
|
+
description: description || title || "Manual bug",
|
|
3619
|
+
file: relativePath,
|
|
3620
|
+
line: parseInt(lineStr || "1", 10),
|
|
3621
|
+
severity,
|
|
3622
|
+
category,
|
|
3623
|
+
confidence: {
|
|
3624
|
+
overall: "high",
|
|
3625
|
+
// User-reported bugs are high confidence
|
|
3626
|
+
codePathValidity: 1,
|
|
3627
|
+
reachability: 1,
|
|
3628
|
+
intentViolation: true,
|
|
3629
|
+
staticToolSignal: false,
|
|
3630
|
+
adversarialSurvived: false
|
|
3631
|
+
},
|
|
3632
|
+
codePath: [],
|
|
3633
|
+
evidence: ["Manually reported by user"],
|
|
3634
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
function getDefaultConfig() {
|
|
3638
|
+
return {
|
|
3639
|
+
version: "1",
|
|
3640
|
+
provider: "claude-code",
|
|
3641
|
+
include: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
|
3642
|
+
exclude: ["node_modules", "dist", "build"],
|
|
3643
|
+
priorities: {},
|
|
3644
|
+
categories: ["logic-error", "security", "null-reference"],
|
|
3645
|
+
minConfidence: "low",
|
|
3646
|
+
staticAnalysis: { typescript: true, eslint: true },
|
|
3647
|
+
output: { sarif: true, markdown: true, sarifPath: ".whiterose/reports", markdownPath: "BUGS.md" }
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
async function processBugList(bugs, config, options, bugId) {
|
|
3651
|
+
if (bugs.length === 0) {
|
|
3652
|
+
p3.log.success("No bugs to fix!");
|
|
3653
|
+
process.exit(0);
|
|
3654
|
+
}
|
|
3655
|
+
if (bugId) {
|
|
3656
|
+
const bug = bugs.find((b) => b.id === bugId || b.id.toLowerCase() === bugId.toLowerCase());
|
|
3657
|
+
if (!bug) {
|
|
3658
|
+
p3.log.error(`Bug ${bugId} not found.`);
|
|
3659
|
+
p3.log.info("Available bugs: " + bugs.map((b) => b.id).join(", "));
|
|
3660
|
+
process.exit(1);
|
|
3661
|
+
}
|
|
3662
|
+
return await fixSingleBug(bug, config, options);
|
|
3663
|
+
}
|
|
3664
|
+
try {
|
|
3665
|
+
await startFixTUI(bugs, config, options);
|
|
3666
|
+
} catch (error) {
|
|
3667
|
+
if (error.message?.includes("stdin") || error.message?.includes("TTY")) {
|
|
3668
|
+
p3.log.warn('Interactive mode not available. Use "whiterose fix <bug-id>" to fix specific bugs.');
|
|
3669
|
+
p3.log.info("Available bugs:");
|
|
3670
|
+
for (const bug of bugs) {
|
|
3671
|
+
const severityColor = bug.severity === "critical" ? "red" : bug.severity === "high" ? "yellow" : "blue";
|
|
3672
|
+
console.log(` ${chalk[severityColor]("\u25CF")} ${bug.id}: ${bug.title}`);
|
|
3673
|
+
}
|
|
3674
|
+
} else {
|
|
3675
|
+
throw error;
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
async function fixSingleBug(bug, config, options) {
|
|
3680
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - fixing bug"));
|
|
3681
|
+
if (!bug.file) {
|
|
3682
|
+
const file = await p3.text({
|
|
3683
|
+
message: "File path containing the bug (required for fix):",
|
|
3684
|
+
placeholder: "src/components/Button.tsx"
|
|
3685
|
+
});
|
|
3686
|
+
if (p3.isCancel(file) || !file) {
|
|
3687
|
+
p3.cancel("Fix cancelled - file path required.");
|
|
3688
|
+
process.exit(0);
|
|
3689
|
+
}
|
|
3690
|
+
bug.file = file;
|
|
3691
|
+
}
|
|
3692
|
+
console.log();
|
|
3693
|
+
console.log(chalk.bold(` ${bug.id}: ${bug.title}`));
|
|
3694
|
+
console.log(` ${chalk.dim("File:")} ${bug.file}:${bug.line}`);
|
|
3695
|
+
console.log(` ${chalk.dim("Severity:")} ${bug.severity}`);
|
|
3696
|
+
console.log();
|
|
3697
|
+
console.log(` ${bug.description}`);
|
|
3698
|
+
console.log();
|
|
3699
|
+
if (bug.suggestedFix) {
|
|
3700
|
+
console.log(chalk.dim(" Suggested fix:"));
|
|
3701
|
+
console.log(` ${chalk.green(bug.suggestedFix)}`);
|
|
3702
|
+
console.log();
|
|
3703
|
+
}
|
|
3704
|
+
if (!options.dryRun) {
|
|
3705
|
+
const confirm3 = await p3.confirm({
|
|
3706
|
+
message: "Apply this fix?",
|
|
3707
|
+
initialValue: true
|
|
3708
|
+
});
|
|
3709
|
+
if (p3.isCancel(confirm3) || !confirm3) {
|
|
3710
|
+
p3.cancel("Fix cancelled.");
|
|
3711
|
+
process.exit(0);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
const spinner5 = p3.spinner();
|
|
3715
|
+
spinner5.start(options.dryRun ? "Generating fix preview..." : "Applying fix...");
|
|
3716
|
+
try {
|
|
3717
|
+
const result = await applyFix(bug, config, options);
|
|
3718
|
+
if (result.success) {
|
|
3719
|
+
spinner5.stop(options.dryRun ? "Fix preview generated" : "Fix applied");
|
|
3720
|
+
if (result.diff) {
|
|
3721
|
+
console.log();
|
|
3722
|
+
console.log(chalk.dim(" Changes:"));
|
|
3723
|
+
for (const line of result.diff.split("\n")) {
|
|
3724
|
+
if (line.startsWith("+")) {
|
|
3725
|
+
console.log(chalk.green(` ${line}`));
|
|
3726
|
+
} else if (line.startsWith("-")) {
|
|
3727
|
+
console.log(chalk.red(` ${line}`));
|
|
3728
|
+
} else {
|
|
3729
|
+
console.log(chalk.dim(` ${line}`));
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
console.log();
|
|
3733
|
+
}
|
|
3734
|
+
if (result.branchName) {
|
|
3735
|
+
p3.log.info(`Changes committed to branch: ${result.branchName}`);
|
|
3736
|
+
}
|
|
3737
|
+
p3.outro(chalk.green("Fix complete!"));
|
|
3738
|
+
} else {
|
|
3739
|
+
spinner5.stop("Fix failed");
|
|
3740
|
+
p3.log.error(result.error || "Unknown error");
|
|
3741
|
+
process.exit(1);
|
|
3742
|
+
}
|
|
3743
|
+
} catch (error) {
|
|
3744
|
+
spinner5.stop("Fix failed");
|
|
3745
|
+
p3.log.error(error.message);
|
|
3746
|
+
process.exit(1);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
function mapSarifLevel(level) {
|
|
3750
|
+
switch (level) {
|
|
3751
|
+
case "error":
|
|
3752
|
+
return "high";
|
|
3753
|
+
case "warning":
|
|
3754
|
+
return "medium";
|
|
3755
|
+
case "note":
|
|
3756
|
+
return "low";
|
|
3757
|
+
default:
|
|
3758
|
+
return "medium";
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
async function refreshCommand(options) {
|
|
3762
|
+
const cwd = process.cwd();
|
|
3763
|
+
const whiterosePath = join(cwd, ".whiterose");
|
|
3764
|
+
if (!existsSync(whiterosePath)) {
|
|
3765
|
+
p3.log.error("whiterose is not initialized in this directory.");
|
|
3766
|
+
p3.log.info('Run "whiterose init" first.');
|
|
3767
|
+
process.exit(1);
|
|
3768
|
+
}
|
|
3769
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - refreshing understanding"));
|
|
3770
|
+
const config = await loadConfig(cwd);
|
|
3771
|
+
const scanSpinner = p3.spinner();
|
|
3772
|
+
scanSpinner.start("Scanning codebase...");
|
|
3773
|
+
const files = await scanCodebase(cwd, config);
|
|
3774
|
+
scanSpinner.stop(`Found ${files.length} source files`);
|
|
3775
|
+
const understandingSpinner = p3.spinner();
|
|
3776
|
+
understandingSpinner.start("Regenerating understanding with AI...");
|
|
3777
|
+
try {
|
|
3778
|
+
const provider = await getProvider(config.provider);
|
|
3779
|
+
const understanding = await provider.generateUnderstanding(files);
|
|
3780
|
+
writeFileSync(
|
|
3781
|
+
join(whiterosePath, "cache", "understanding.json"),
|
|
3782
|
+
JSON.stringify(understanding, null, 2),
|
|
3783
|
+
"utf-8"
|
|
3784
|
+
);
|
|
3785
|
+
const intentDoc = generateIntentDocument(understanding);
|
|
3786
|
+
writeFileSync(join(whiterosePath, "intent.md"), intentDoc, "utf-8");
|
|
3787
|
+
writeFileSync(
|
|
3788
|
+
join(whiterosePath, "cache", "file-hashes.json"),
|
|
3789
|
+
JSON.stringify({ version: "1", fileHashes: [], lastFullScan: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
3790
|
+
"utf-8"
|
|
3791
|
+
);
|
|
3792
|
+
understandingSpinner.stop("Understanding regenerated");
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
understandingSpinner.stop("Failed to regenerate understanding");
|
|
3795
|
+
p3.log.error(String(error));
|
|
3796
|
+
process.exit(1);
|
|
3797
|
+
}
|
|
3798
|
+
p3.outro(chalk.green("Refresh complete!"));
|
|
3799
|
+
}
|
|
3800
|
+
async function statusCommand() {
|
|
3801
|
+
const cwd = process.cwd();
|
|
3802
|
+
const whiterosePath = join(cwd, ".whiterose");
|
|
3803
|
+
if (!existsSync(whiterosePath)) {
|
|
3804
|
+
p3.log.error("whiterose is not initialized in this directory.");
|
|
3805
|
+
p3.log.info('Run "whiterose init" first.');
|
|
3806
|
+
process.exit(1);
|
|
3807
|
+
}
|
|
3808
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - status"));
|
|
3809
|
+
const config = await loadConfig(cwd);
|
|
3810
|
+
const understanding = await loadUnderstanding(cwd);
|
|
3811
|
+
const availableProviders = await detectProvider();
|
|
3812
|
+
console.log();
|
|
3813
|
+
console.log(chalk.bold(" Configuration"));
|
|
3814
|
+
console.log(` ${chalk.dim("Provider:")} ${config.provider}`);
|
|
3815
|
+
console.log(` ${chalk.dim("Available:")} ${availableProviders.join(", ") || "none"}`);
|
|
3816
|
+
console.log();
|
|
3817
|
+
if (understanding) {
|
|
3818
|
+
console.log(chalk.bold(" Codebase Understanding"));
|
|
3819
|
+
console.log(` ${chalk.dim("Type:")} ${understanding.summary.type}`);
|
|
3820
|
+
console.log(` ${chalk.dim("Framework:")} ${understanding.summary.framework || "none"}`);
|
|
3821
|
+
console.log(` ${chalk.dim("Files:")} ${understanding.structure.totalFiles}`);
|
|
3822
|
+
console.log(` ${chalk.dim("Features:")} ${understanding.features.length}`);
|
|
3823
|
+
console.log(` ${chalk.dim("Contracts:")} ${understanding.contracts.length}`);
|
|
3824
|
+
console.log(` ${chalk.dim("Generated:")} ${understanding.generatedAt}`);
|
|
3825
|
+
console.log();
|
|
3826
|
+
}
|
|
3827
|
+
const hashesPath = join(whiterosePath, "cache", "file-hashes.json");
|
|
3828
|
+
if (existsSync(hashesPath)) {
|
|
3829
|
+
const hashes = JSON.parse(readFileSync(hashesPath, "utf-8"));
|
|
3830
|
+
console.log(chalk.bold(" Cache"));
|
|
3831
|
+
console.log(` ${chalk.dim("Files tracked:")} ${hashes.fileHashes?.length || 0}`);
|
|
3832
|
+
console.log(` ${chalk.dim("Last full scan:")} ${hashes.lastFullScan || "never"}`);
|
|
3833
|
+
console.log();
|
|
3834
|
+
}
|
|
3835
|
+
const reportsDir = join(whiterosePath, "reports");
|
|
3836
|
+
if (existsSync(reportsDir)) {
|
|
3837
|
+
const reports = readdirSync(reportsDir).filter((f) => f.endsWith(".sarif"));
|
|
3838
|
+
if (reports.length > 0) {
|
|
3839
|
+
const latestReport = reports.sort().reverse()[0];
|
|
3840
|
+
const reportPath = join(reportsDir, latestReport);
|
|
3841
|
+
const stats = statSync(reportPath);
|
|
3842
|
+
console.log(chalk.bold(" Last Scan"));
|
|
3843
|
+
console.log(` ${chalk.dim("Report:")} ${latestReport}`);
|
|
3844
|
+
console.log(` ${chalk.dim("Date:")} ${stats.mtime.toISOString()}`);
|
|
3845
|
+
try {
|
|
3846
|
+
const sarif = JSON.parse(readFileSync(reportPath, "utf-8"));
|
|
3847
|
+
const bugCount = sarif.runs?.[0]?.results?.length || 0;
|
|
3848
|
+
console.log(` ${chalk.dim("Bugs found:")} ${bugCount}`);
|
|
3849
|
+
} catch {
|
|
3850
|
+
}
|
|
3851
|
+
console.log();
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
p3.outro(chalk.dim('Run "whiterose scan" to scan for bugs'));
|
|
3855
|
+
}
|
|
3856
|
+
async function reportCommand(options) {
|
|
3857
|
+
const cwd = process.cwd();
|
|
3858
|
+
const whiterosePath = join(cwd, ".whiterose");
|
|
3859
|
+
if (!existsSync(whiterosePath)) {
|
|
3860
|
+
p3.log.error("whiterose is not initialized in this directory.");
|
|
3861
|
+
p3.log.info('Run "whiterose init" first.');
|
|
3862
|
+
process.exit(1);
|
|
3863
|
+
}
|
|
3864
|
+
const reportsDir = join(whiterosePath, "reports");
|
|
3865
|
+
if (!existsSync(reportsDir)) {
|
|
3866
|
+
p3.log.error('No scan results found. Run "whiterose scan" first.');
|
|
3867
|
+
process.exit(1);
|
|
3868
|
+
}
|
|
3869
|
+
const reports = readdirSync(reportsDir).filter((f) => f.endsWith(".sarif")).sort().reverse();
|
|
3870
|
+
if (reports.length === 0) {
|
|
3871
|
+
p3.log.error('No scan results found. Run "whiterose scan" first.');
|
|
3872
|
+
process.exit(1);
|
|
3873
|
+
}
|
|
3874
|
+
const latestReport = join(reportsDir, reports[0]);
|
|
3875
|
+
const sarif = JSON.parse(readFileSync(latestReport, "utf-8"));
|
|
3876
|
+
const bugs = sarif.runs?.[0]?.results?.map((r, i) => ({
|
|
3877
|
+
id: r.ruleId || `WR-${String(i + 1).padStart(3, "0")}`,
|
|
3878
|
+
title: r.message?.text || "Unknown bug",
|
|
3879
|
+
description: r.message?.markdown || r.message?.text || "",
|
|
3880
|
+
file: r.locations?.[0]?.physicalLocation?.artifactLocation?.uri || "unknown",
|
|
3881
|
+
line: r.locations?.[0]?.physicalLocation?.region?.startLine || 0,
|
|
3882
|
+
severity: r.level === "error" ? "critical" : r.level === "warning" ? "high" : "medium",
|
|
3883
|
+
category: "logic-error",
|
|
3884
|
+
confidence: { overall: "high", codePathValidity: 0.9, reachability: 0.9, intentViolation: false, staticToolSignal: false, adversarialSurvived: true },
|
|
3885
|
+
codePath: [],
|
|
3886
|
+
evidence: [],
|
|
3887
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3888
|
+
})) || [];
|
|
3889
|
+
const result = {
|
|
3890
|
+
id: "report",
|
|
3891
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3892
|
+
scanType: "full",
|
|
3893
|
+
filesScanned: 0,
|
|
3894
|
+
duration: 0,
|
|
3895
|
+
bugs,
|
|
3896
|
+
summary: {
|
|
3897
|
+
critical: bugs.filter((b) => b.severity === "critical").length,
|
|
3898
|
+
high: bugs.filter((b) => b.severity === "high").length,
|
|
3899
|
+
medium: bugs.filter((b) => b.severity === "medium").length,
|
|
3900
|
+
low: bugs.filter((b) => b.severity === "low").length,
|
|
3901
|
+
total: bugs.length
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3904
|
+
let output;
|
|
3905
|
+
switch (options.format) {
|
|
3906
|
+
case "markdown":
|
|
3907
|
+
output = outputMarkdown(result);
|
|
3908
|
+
break;
|
|
3909
|
+
case "sarif":
|
|
3910
|
+
output = readFileSync(latestReport, "utf-8");
|
|
3911
|
+
break;
|
|
3912
|
+
case "json":
|
|
3913
|
+
output = JSON.stringify(result, null, 2);
|
|
3914
|
+
break;
|
|
3915
|
+
default:
|
|
3916
|
+
output = outputMarkdown(result);
|
|
3917
|
+
}
|
|
3918
|
+
if (options.output === "-") {
|
|
3919
|
+
console.log(output);
|
|
3920
|
+
} else {
|
|
3921
|
+
writeFileSync(options.output, output);
|
|
3922
|
+
p3.log.success(`Report written to ${options.output}`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// src/cli/index.ts
|
|
3927
|
+
var BANNER = `
|
|
3928
|
+
${chalk.red("\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
3929
|
+
${chalk.red("\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D")}
|
|
3930
|
+
${chalk.red("\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 ")}
|
|
3931
|
+
${chalk.red("\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D ")}
|
|
3932
|
+
${chalk.red("\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
3933
|
+
${chalk.red(" \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
|
|
3934
|
+
|
|
3935
|
+
${chalk.dim(` "I've been staring at your code for a long time."`)}
|
|
3936
|
+
`;
|
|
3937
|
+
var program = new Command();
|
|
3938
|
+
program.name("whiterose").description("AI-powered bug hunter that uses your existing LLM subscription").version("0.1.0").hook("preAction", () => {
|
|
3939
|
+
const args = process.argv.slice(2);
|
|
3940
|
+
if (!args.includes("--help") && !args.includes("-h") && args.length > 0) {
|
|
3941
|
+
console.log(BANNER);
|
|
3942
|
+
}
|
|
3943
|
+
});
|
|
3944
|
+
program.command("init").description("Initialize whiterose for this project (scans codebase, asks questions, generates config)").option("-p, --provider <provider>", "LLM provider to use", "claude-code").option("--skip-questions", "Skip interactive questions, use defaults").option("--force", "Overwrite existing .whiterose directory").option("--unsafe", "Bypass LLM permission prompts (use with caution)").action(initCommand);
|
|
3945
|
+
program.command("scan [paths...]").description("Scan for bugs in the codebase").option("-f, --full", "Force full scan (ignore cache)").option("--json", "Output as JSON only").option("--sarif", "Output as SARIF only").option("-p, --provider <provider>", "Override LLM provider").option("-c, --category <categories...>", "Filter by bug categories").option("--min-confidence <level>", "Minimum confidence level to report", "low").option("--no-adversarial", "Skip adversarial validation (faster, less accurate)").option("--unsafe", "Bypass LLM permission prompts (use with caution)").action(scanCommand);
|
|
3946
|
+
program.command("fix [bugId]").description("Fix bugs interactively or by ID").option("--dry-run", "Show proposed fixes without applying").option("--branch <name>", "Create fixes in a new branch").option("--sarif <path>", "Load bugs from an external SARIF file").option("--github <url>", "Load bug from a GitHub issue URL").option("--describe", "Manually describe a bug to fix").action(fixCommand);
|
|
3947
|
+
program.command("refresh").description("Rebuild codebase understanding from scratch").option("--keep-config", "Keep existing config, only regenerate understanding").action(refreshCommand);
|
|
3948
|
+
program.command("status").description("Show whiterose status (cache, last scan, provider)").action(statusCommand);
|
|
3949
|
+
program.command("report").description("Generate BUGS.md from last scan").option("-o, --output <path>", "Output path", "BUGS.md").option("--format <format>", "Output format (markdown, sarif, json)", "markdown").action(reportCommand);
|
|
3950
|
+
async function showInteractiveMenu() {
|
|
3951
|
+
console.log(BANNER);
|
|
3952
|
+
const cwd = process.cwd();
|
|
3953
|
+
const whiterosePath = join(cwd, ".whiterose");
|
|
3954
|
+
const isInitialized = existsSync(whiterosePath);
|
|
3955
|
+
if (isInitialized) {
|
|
3956
|
+
console.log(chalk.dim(` Project: ${chalk.white(cwd.split("/").pop())}`));
|
|
3957
|
+
console.log(chalk.dim(` Status: ${chalk.green("initialized")}`));
|
|
3958
|
+
console.log();
|
|
3959
|
+
} else {
|
|
3960
|
+
console.log(chalk.dim(` Project: ${chalk.white(cwd.split("/").pop())}`));
|
|
3961
|
+
console.log(chalk.dim(` Status: ${chalk.yellow("not initialized")}`));
|
|
3962
|
+
console.log();
|
|
3963
|
+
}
|
|
3964
|
+
const menuOptions = [];
|
|
3965
|
+
if (!isInitialized) {
|
|
3966
|
+
menuOptions.push({
|
|
3967
|
+
value: "init",
|
|
3968
|
+
label: "Initialize",
|
|
3969
|
+
hint: "set up whiterose for this project"
|
|
3970
|
+
});
|
|
3971
|
+
} else {
|
|
3972
|
+
menuOptions.push(
|
|
3973
|
+
{ value: "scan", label: "Scan", hint: "find bugs in the codebase" },
|
|
3974
|
+
{ value: "fix", label: "Fix", hint: "fix bugs interactively" },
|
|
3975
|
+
{ value: "status", label: "Status", hint: "show current status" },
|
|
3976
|
+
{ value: "report", label: "Report", hint: "generate bug report" },
|
|
3977
|
+
{ value: "refresh", label: "Refresh", hint: "rebuild codebase understanding" }
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
3980
|
+
menuOptions.push({ value: "help", label: "Help", hint: "show all commands" });
|
|
3981
|
+
menuOptions.push({ value: "exit", label: "Exit" });
|
|
3982
|
+
const choice = await p3.select({
|
|
3983
|
+
message: "What would you like to do?",
|
|
3984
|
+
options: menuOptions
|
|
3985
|
+
});
|
|
3986
|
+
if (p3.isCancel(choice) || choice === "exit") {
|
|
3987
|
+
p3.outro(chalk.dim("Goodbye."));
|
|
3988
|
+
process.exit(0);
|
|
3989
|
+
}
|
|
3990
|
+
console.log();
|
|
3991
|
+
switch (choice) {
|
|
3992
|
+
case "init":
|
|
3993
|
+
await initCommand({ provider: "claude-code", skipQuestions: false, force: false, unsafe: false });
|
|
3994
|
+
break;
|
|
3995
|
+
case "scan":
|
|
3996
|
+
await scanCommand([], {
|
|
3997
|
+
full: false,
|
|
3998
|
+
json: false,
|
|
3999
|
+
sarif: false,
|
|
4000
|
+
provider: void 0,
|
|
4001
|
+
category: void 0,
|
|
4002
|
+
minConfidence: "low",
|
|
4003
|
+
adversarial: true,
|
|
4004
|
+
unsafe: false
|
|
4005
|
+
});
|
|
4006
|
+
break;
|
|
4007
|
+
case "fix":
|
|
4008
|
+
await fixCommand(void 0, { dryRun: false });
|
|
4009
|
+
break;
|
|
4010
|
+
case "status":
|
|
4011
|
+
await statusCommand();
|
|
4012
|
+
break;
|
|
4013
|
+
case "report":
|
|
4014
|
+
await reportCommand({ output: "BUGS.md", format: "markdown" });
|
|
4015
|
+
break;
|
|
4016
|
+
case "refresh":
|
|
4017
|
+
await refreshCommand();
|
|
4018
|
+
break;
|
|
4019
|
+
case "help":
|
|
4020
|
+
program.help();
|
|
4021
|
+
break;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
if (process.argv.length === 2) {
|
|
4025
|
+
showInteractiveMenu().catch((error) => {
|
|
4026
|
+
console.error(chalk.red("Error:"), error.message);
|
|
4027
|
+
process.exit(1);
|
|
4028
|
+
});
|
|
4029
|
+
} else {
|
|
4030
|
+
program.parse();
|
|
4031
|
+
}
|
|
4032
|
+
//# sourceMappingURL=index.js.map
|
|
4033
|
+
//# sourceMappingURL=index.js.map
|