@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync, rmSync, mkdtempSync, realpathSync } from 'fs';
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
- this.progress.onPassError?.(pass.name, error.message);
2218
- this.report(` \u2717 ${pass.name}: ${error.message}`);
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
- this.report(` Error: ${error.message}`);
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 jsonObjectMatch = output.match(/\{[\s\S]*\}/);
2298
- if (jsonObjectMatch) {
2299
- try {
2300
- parsed = JSON.parse(jsonObjectMatch[0]);
2301
- } catch {
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---|\$)/i);
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
- join(whiterosePath, "cache", "file-hashes.json"),
3374
- JSON.stringify({ version: "1", fileHashes: [], lastFullScan: null }, null, 2),
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
- onProgress(`Using ${toolName}...`);
6488
- } else if (block.type === "text" && block.text) {
6489
- const preview = block.text.substring(0, 80).replace(/\n/g, " ").trim();
6490
- if (preview) {
6491
- onProgress(preview + (block.text.length > 80 ? "..." : ""));
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: issue.title,
6941
- description: issue.body || issue.title,
7058
+ title: sanitizedTitle,
7059
+ description: sanitizedBody || sanitizedTitle,
6942
7060
  file: fileMatch?.[1] || "",
6943
7061
  line: parseInt(lineMatch?.[1] || "1", 10),
6944
7062
  kind: "bug",