@shakecodeslikecray/whiterose 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +160 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +69 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync,
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, readdirSync, statSync, mkdtempSync, realpathSync } from 'fs';
|
|
3
3
|
import { join, dirname, isAbsolute, resolve, basename, relative } from 'path';
|
|
4
4
|
import chalk3 from 'chalk';
|
|
5
5
|
import * as readline from 'readline';
|
|
@@ -2116,11 +2116,25 @@ var CoreScanner = class {
|
|
|
2116
2116
|
executor;
|
|
2117
2117
|
config;
|
|
2118
2118
|
progress;
|
|
2119
|
+
passErrors = [];
|
|
2119
2120
|
constructor(executor, config = {}, progress = {}) {
|
|
2120
2121
|
this.executor = executor;
|
|
2121
2122
|
this.config = { ...DEFAULT_SCANNER_CONFIG, ...config };
|
|
2122
2123
|
this.progress = progress;
|
|
2123
2124
|
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Get errors that occurred during the last scan.
|
|
2127
|
+
* Returns an array of pass names and their error messages.
|
|
2128
|
+
*/
|
|
2129
|
+
getPassErrors() {
|
|
2130
|
+
return this.passErrors;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Check if any passes failed during the last scan.
|
|
2134
|
+
*/
|
|
2135
|
+
hasPassErrors() {
|
|
2136
|
+
return this.passErrors.length > 0;
|
|
2137
|
+
}
|
|
2124
2138
|
/**
|
|
2125
2139
|
* Run a thorough 19-pass scan with findings flowing through pipeline:
|
|
2126
2140
|
*
|
|
@@ -2134,6 +2148,7 @@ var CoreScanner = class {
|
|
|
2134
2148
|
async scan(context) {
|
|
2135
2149
|
const cwd = process.cwd();
|
|
2136
2150
|
const startTime = Date.now();
|
|
2151
|
+
this.passErrors = [];
|
|
2137
2152
|
const pipeline = getFullAnalysisPipeline();
|
|
2138
2153
|
const unitPasses = pipeline[0].passes;
|
|
2139
2154
|
const integrationPasses = pipeline[1].passes;
|
|
@@ -2214,8 +2229,10 @@ var CoreScanner = class {
|
|
|
2214
2229
|
this.report(` \u2713 ${pass.name}: ${bugs.length} bugs`);
|
|
2215
2230
|
return bugs;
|
|
2216
2231
|
} catch (error) {
|
|
2217
|
-
|
|
2218
|
-
this.
|
|
2232
|
+
const errorMsg = error.message || String(error);
|
|
2233
|
+
this.progress.onPassError?.(pass.name, errorMsg);
|
|
2234
|
+
this.report(` \u2717 ${pass.name}: ${errorMsg}`);
|
|
2235
|
+
this.passErrors.push({ passName: pass.name, error: errorMsg });
|
|
2219
2236
|
return [];
|
|
2220
2237
|
}
|
|
2221
2238
|
});
|
|
@@ -2236,6 +2253,7 @@ var CoreScanner = class {
|
|
|
2236
2253
|
*/
|
|
2237
2254
|
async quickScan(context) {
|
|
2238
2255
|
const cwd = process.cwd();
|
|
2256
|
+
this.passErrors = [];
|
|
2239
2257
|
this.report(`
|
|
2240
2258
|
\u2550\u2550\u2550\u2550 QUICK SCAN \u2550\u2550\u2550\u2550`);
|
|
2241
2259
|
this.report(` Provider: ${this.executor.name}`);
|
|
@@ -2249,7 +2267,9 @@ var CoreScanner = class {
|
|
|
2249
2267
|
this.report(` Found ${bugs.length} bugs`);
|
|
2250
2268
|
return bugs;
|
|
2251
2269
|
} catch (error) {
|
|
2252
|
-
|
|
2270
|
+
const errorMsg = error.message || String(error);
|
|
2271
|
+
this.report(` Error: ${errorMsg}`);
|
|
2272
|
+
this.passErrors.push({ passName: "quick-scan", error: errorMsg });
|
|
2253
2273
|
return [];
|
|
2254
2274
|
}
|
|
2255
2275
|
}
|
|
@@ -2294,11 +2314,41 @@ var CoreScanner = class {
|
|
|
2294
2314
|
}
|
|
2295
2315
|
}
|
|
2296
2316
|
} else {
|
|
2297
|
-
const
|
|
2298
|
-
if (
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2317
|
+
const firstBrace = output.indexOf("{");
|
|
2318
|
+
if (firstBrace !== -1) {
|
|
2319
|
+
const substring = output.slice(firstBrace);
|
|
2320
|
+
let depth = 0;
|
|
2321
|
+
let inString = false;
|
|
2322
|
+
let escape = false;
|
|
2323
|
+
for (let i = 0; i < substring.length; i++) {
|
|
2324
|
+
const char = substring[i];
|
|
2325
|
+
if (escape) {
|
|
2326
|
+
escape = false;
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
if (char === "\\" && inString) {
|
|
2330
|
+
escape = true;
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
if (char === '"') {
|
|
2334
|
+
inString = !inString;
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
if (inString) continue;
|
|
2338
|
+
if (char === "{") {
|
|
2339
|
+
depth++;
|
|
2340
|
+
} else if (char === "}") {
|
|
2341
|
+
depth--;
|
|
2342
|
+
if (depth === 0) {
|
|
2343
|
+
const candidate = substring.slice(0, i + 1);
|
|
2344
|
+
try {
|
|
2345
|
+
parsed = JSON.parse(candidate);
|
|
2346
|
+
break;
|
|
2347
|
+
} catch {
|
|
2348
|
+
depth = 1;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2302
2352
|
}
|
|
2303
2353
|
}
|
|
2304
2354
|
}
|
|
@@ -3089,7 +3139,7 @@ function extractIntentFromDocs(docs) {
|
|
|
3089
3139
|
}
|
|
3090
3140
|
}
|
|
3091
3141
|
if (docs.readme) {
|
|
3092
|
-
const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n
|
|
3142
|
+
const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
|
|
3093
3143
|
if (featuresMatch) {
|
|
3094
3144
|
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);
|
|
3095
3145
|
intent.features.push(...featureLines.slice(0, 20));
|
|
@@ -3326,6 +3376,9 @@ async function initCommand(options) {
|
|
|
3326
3376
|
}
|
|
3327
3377
|
const writeSpinner = p3.spinner();
|
|
3328
3378
|
writeSpinner.start("Creating configuration...");
|
|
3379
|
+
const whiteroseExistedBefore = existsSync(whiterosePath);
|
|
3380
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
3381
|
+
const originalGitignore = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : null;
|
|
3329
3382
|
try {
|
|
3330
3383
|
mkdirSync(join(whiterosePath, "cache"), { recursive: true });
|
|
3331
3384
|
mkdirSync(join(whiterosePath, "reports"), { recursive: true });
|
|
@@ -3361,29 +3414,37 @@ async function initCommand(options) {
|
|
|
3361
3414
|
markdownPath: "BUGS.md"
|
|
3362
3415
|
}
|
|
3363
3416
|
};
|
|
3364
|
-
writeFileSync(join(whiterosePath, "config.yml"), YAML.stringify(config), "utf-8");
|
|
3365
|
-
writeFileSync(
|
|
3366
|
-
join(whiterosePath, "cache", "understanding.json"),
|
|
3367
|
-
JSON.stringify(understanding, null, 2),
|
|
3368
|
-
"utf-8"
|
|
3369
|
-
);
|
|
3370
3417
|
const intentDoc = generateIntentDocument(understanding);
|
|
3418
|
+
const configContent = YAML.stringify(config);
|
|
3419
|
+
const understandingContent = JSON.stringify(understanding, null, 2);
|
|
3420
|
+
const hashesContent = JSON.stringify({ version: "1", fileHashes: [], lastFullScan: null }, null, 2);
|
|
3421
|
+
writeFileSync(join(whiterosePath, "config.yml"), configContent, "utf-8");
|
|
3422
|
+
writeFileSync(join(whiterosePath, "cache", "understanding.json"), understandingContent, "utf-8");
|
|
3371
3423
|
writeFileSync(join(whiterosePath, "intent.md"), intentDoc, "utf-8");
|
|
3372
|
-
writeFileSync(
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
"utf-8"
|
|
3376
|
-
);
|
|
3377
|
-
const gitignorePath = join(cwd, ".gitignore");
|
|
3378
|
-
if (existsSync(gitignorePath)) {
|
|
3379
|
-
const gitignore = await import('fs').then((fs) => fs.readFileSync(gitignorePath, "utf-8"));
|
|
3380
|
-
if (!gitignore.includes(".whiterose/cache")) {
|
|
3381
|
-
writeFileSync(gitignorePath, gitignore + "\n# whiterose cache\n.whiterose/cache/\n", "utf-8");
|
|
3382
|
-
}
|
|
3424
|
+
writeFileSync(join(whiterosePath, "cache", "file-hashes.json"), hashesContent, "utf-8");
|
|
3425
|
+
if (originalGitignore !== null && !originalGitignore.includes(".whiterose/cache")) {
|
|
3426
|
+
writeFileSync(gitignorePath, originalGitignore + "\n# whiterose cache\n.whiterose/cache/\n", "utf-8");
|
|
3383
3427
|
}
|
|
3384
3428
|
writeSpinner.stop("Configuration created");
|
|
3385
3429
|
} catch (error) {
|
|
3386
3430
|
writeSpinner.stop("Failed to create configuration");
|
|
3431
|
+
if (!whiteroseExistedBefore && existsSync(whiterosePath)) {
|
|
3432
|
+
try {
|
|
3433
|
+
rmSync(whiterosePath, { recursive: true, force: true });
|
|
3434
|
+
p3.log.info("Rolled back: removed .whiterose directory");
|
|
3435
|
+
} catch {
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
if (originalGitignore !== null && existsSync(gitignorePath)) {
|
|
3439
|
+
try {
|
|
3440
|
+
const currentGitignore = readFileSync(gitignorePath, "utf-8");
|
|
3441
|
+
if (currentGitignore !== originalGitignore) {
|
|
3442
|
+
writeFileSync(gitignorePath, originalGitignore, "utf-8");
|
|
3443
|
+
p3.log.info("Rolled back: restored .gitignore");
|
|
3444
|
+
}
|
|
3445
|
+
} catch {
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3387
3448
|
p3.log.error(String(error));
|
|
3388
3449
|
process.exit(1);
|
|
3389
3450
|
}
|
|
@@ -5146,7 +5207,19 @@ async function scanCommand(paths, options) {
|
|
|
5146
5207
|
try {
|
|
5147
5208
|
config = await loadConfig(cwd);
|
|
5148
5209
|
understanding = await loadUnderstanding(cwd);
|
|
5149
|
-
} catch {
|
|
5210
|
+
} catch (err) {
|
|
5211
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
5212
|
+
if (!isQuiet) {
|
|
5213
|
+
p3.log.warn(`Failed to load config: ${errorMessage}`);
|
|
5214
|
+
p3.log.info('Continuing with default settings. Run "whiterose init" to fix.');
|
|
5215
|
+
} else if (options.ci) {
|
|
5216
|
+
console.error(JSON.stringify({
|
|
5217
|
+
error: "Config parse error",
|
|
5218
|
+
message: errorMessage,
|
|
5219
|
+
hint: 'Fix config.yml or run "whiterose init" to regenerate'
|
|
5220
|
+
}));
|
|
5221
|
+
process.exit(1);
|
|
5222
|
+
}
|
|
5150
5223
|
}
|
|
5151
5224
|
}
|
|
5152
5225
|
if (!understanding) {
|
|
@@ -5265,6 +5338,16 @@ async function scanCommand(paths, options) {
|
|
|
5265
5338
|
}
|
|
5266
5339
|
const totalTime = Math.floor((Date.now() - analysisStartTime) / 1e3);
|
|
5267
5340
|
llmSpinner.stop(`Found ${bugs.length} potential bugs (${totalTime}s)`);
|
|
5341
|
+
if (scanner.hasPassErrors()) {
|
|
5342
|
+
const errors = scanner.getPassErrors();
|
|
5343
|
+
p3.log.warn(`${errors.length} analysis pass(es) failed:`);
|
|
5344
|
+
for (const err of errors.slice(0, 5)) {
|
|
5345
|
+
console.log(chalk3.yellow(` - ${err.passName}: ${err.error}`));
|
|
5346
|
+
}
|
|
5347
|
+
if (errors.length > 5) {
|
|
5348
|
+
console.log(chalk3.yellow(` ... and ${errors.length - 5} more`));
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5268
5351
|
} catch (error) {
|
|
5269
5352
|
llmSpinner.stop("Analysis failed");
|
|
5270
5353
|
p3.log.error(String(error));
|
|
@@ -5287,6 +5370,16 @@ async function scanCommand(paths, options) {
|
|
|
5287
5370
|
config
|
|
5288
5371
|
});
|
|
5289
5372
|
}
|
|
5373
|
+
if (options.ci && scanner.hasPassErrors()) {
|
|
5374
|
+
const errors = scanner.getPassErrors();
|
|
5375
|
+
if (bugs.length === 0) {
|
|
5376
|
+
console.error(JSON.stringify({
|
|
5377
|
+
error: "Analysis failed",
|
|
5378
|
+
passErrors: errors
|
|
5379
|
+
}));
|
|
5380
|
+
process.exit(1);
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5290
5383
|
}
|
|
5291
5384
|
if (!isQuickScan) {
|
|
5292
5385
|
if (!isQuiet) {
|
|
@@ -5307,7 +5400,14 @@ async function scanCommand(paths, options) {
|
|
|
5307
5400
|
try {
|
|
5308
5401
|
const crossFileBugs = await analyzeCrossFile(cwd);
|
|
5309
5402
|
bugs.push(...crossFileBugs);
|
|
5310
|
-
} catch {
|
|
5403
|
+
} catch (err) {
|
|
5404
|
+
if (options.ci) {
|
|
5405
|
+
console.error(JSON.stringify({
|
|
5406
|
+
error: "Cross-file analysis failed",
|
|
5407
|
+
message: err instanceof Error ? err.message : String(err)
|
|
5408
|
+
}));
|
|
5409
|
+
process.exit(1);
|
|
5410
|
+
}
|
|
5311
5411
|
}
|
|
5312
5412
|
}
|
|
5313
5413
|
}
|
|
@@ -5330,7 +5430,14 @@ async function scanCommand(paths, options) {
|
|
|
5330
5430
|
try {
|
|
5331
5431
|
const contractBugs = await analyzeContracts(cwd);
|
|
5332
5432
|
bugs.push(...contractBugs);
|
|
5333
|
-
} catch {
|
|
5433
|
+
} catch (err) {
|
|
5434
|
+
if (options.ci) {
|
|
5435
|
+
console.error(JSON.stringify({
|
|
5436
|
+
error: "Contract analysis failed",
|
|
5437
|
+
message: err instanceof Error ? err.message : String(err)
|
|
5438
|
+
}));
|
|
5439
|
+
process.exit(1);
|
|
5440
|
+
}
|
|
5334
5441
|
}
|
|
5335
5442
|
}
|
|
5336
5443
|
}
|
|
@@ -5340,7 +5447,14 @@ async function scanCommand(paths, options) {
|
|
|
5340
5447
|
if (intentBugs.length > 0) {
|
|
5341
5448
|
bugs.push(...intentBugs);
|
|
5342
5449
|
}
|
|
5343
|
-
} catch {
|
|
5450
|
+
} catch (err) {
|
|
5451
|
+
if (options.ci) {
|
|
5452
|
+
console.error(JSON.stringify({
|
|
5453
|
+
error: "Intent validation failed",
|
|
5454
|
+
message: err instanceof Error ? err.message : String(err)
|
|
5455
|
+
}));
|
|
5456
|
+
process.exit(1);
|
|
5457
|
+
}
|
|
5344
5458
|
}
|
|
5345
5459
|
}
|
|
5346
5460
|
const confidenceOrder = { high: 3, medium: 2, low: 1 };
|
|
@@ -6484,19 +6598,21 @@ async function runAgenticFix(bug, config, projectDir, onProgress) {
|
|
|
6484
6598
|
for (const block of event.message.content) {
|
|
6485
6599
|
if (block.type === "tool_use") {
|
|
6486
6600
|
const toolName = block.name || "tool";
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6601
|
+
const friendlyNames = {
|
|
6602
|
+
"Read": "Reading file",
|
|
6603
|
+
"Edit": "Editing file",
|
|
6604
|
+
"Write": "Writing file",
|
|
6605
|
+
"Bash": "Running command",
|
|
6606
|
+
"Glob": "Searching files",
|
|
6607
|
+
"Grep": "Searching content",
|
|
6608
|
+
"Task": "Running task"
|
|
6609
|
+
};
|
|
6610
|
+
const displayName = friendlyNames[toolName] || `Using ${toolName}`;
|
|
6611
|
+
onProgress(`${displayName}...`);
|
|
6493
6612
|
}
|
|
6494
6613
|
}
|
|
6495
6614
|
}
|
|
6496
6615
|
} catch {
|
|
6497
|
-
if (trimmed.length > 3 && trimmed.length < 100) {
|
|
6498
|
-
onProgress(trimmed);
|
|
6499
|
-
}
|
|
6500
6616
|
}
|
|
6501
6617
|
}
|
|
6502
6618
|
}
|
|
@@ -6935,10 +7051,12 @@ async function loadBugFromGitHub(issueUrl, cwd) {
|
|
|
6935
7051
|
} else if (labels.some((l) => l.includes("leak") || l.includes("memory"))) {
|
|
6936
7052
|
category = "resource-leak";
|
|
6937
7053
|
}
|
|
7054
|
+
const sanitizedTitle = sanitizeSarifText(String(issue.title || ""), "github.title");
|
|
7055
|
+
const sanitizedBody = sanitizeSarifText(String(issue.body || ""), "github.body");
|
|
6938
7056
|
return {
|
|
6939
7057
|
id: `GH-${issueNumber}`,
|
|
6940
|
-
title:
|
|
6941
|
-
description:
|
|
7058
|
+
title: sanitizedTitle,
|
|
7059
|
+
description: sanitizedBody || sanitizedTitle,
|
|
6942
7060
|
file: fileMatch?.[1] || "",
|
|
6943
7061
|
line: parseInt(lineMatch?.[1] || "1", 10),
|
|
6944
7062
|
kind: "bug",
|