@shakecodeslikecray/whiterose 1.0.8 → 1.0.10

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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, readdirSync, statSync, mkdtempSync, realpathSync } from 'fs';
3
- import { join, dirname, isAbsolute, resolve, basename, relative } from 'path';
2
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, readdirSync, mkdtempSync, statSync, realpathSync } from 'fs';
3
+ import { join, dirname, relative, isAbsolute, resolve, basename } from 'path';
4
4
  import chalk3 from 'chalk';
5
5
  import * as readline from 'readline';
6
6
  import { Command } from 'commander';
@@ -307,14 +307,24 @@ var ClaudeCodeExecutor = class {
307
307
  }
308
308
  async runPrompt(prompt, options) {
309
309
  const claudeCommand = getProviderCommand("claude-code");
310
+ if (process.env.WHITEROSE_DEBUG) {
311
+ console.log("\n[DEBUG] Running Claude Code command:", claudeCommand);
312
+ console.log("[DEBUG] Prompt length:", prompt.length);
313
+ console.log("[DEBUG] CWD:", options.cwd);
314
+ console.log("[DEBUG] First 500 chars of prompt:");
315
+ console.log(prompt.substring(0, 500));
316
+ }
310
317
  try {
311
318
  const { stdout, stderr } = await execa(
312
319
  claudeCommand,
313
320
  [
314
321
  "-p",
315
322
  prompt,
316
- "--dangerously-skip-permissions"
323
+ "--dangerously-skip-permissions",
317
324
  // Allow file reads without prompts
325
+ "--output-format",
326
+ "text"
327
+ // Ensure non-interactive output
318
328
  ],
319
329
  {
320
330
  cwd: options.cwd,
@@ -323,7 +333,9 @@ var ClaudeCodeExecutor = class {
323
333
  ...process.env,
324
334
  NO_COLOR: "1"
325
335
  },
326
- reject: false
336
+ reject: false,
337
+ stdin: "ignore"
338
+ // Prevent waiting for stdin
327
339
  }
328
340
  );
329
341
  if (stderr) {
@@ -340,6 +352,13 @@ var ClaudeCodeExecutor = class {
340
352
  throw new Error(`Claude Code error: ${stderr.substring(0, 200)}`);
341
353
  }
342
354
  }
355
+ if (process.env.WHITEROSE_DEBUG) {
356
+ console.log("\n[DEBUG] Claude Code stdout length:", stdout?.length || 0);
357
+ console.log("[DEBUG] Claude Code stderr:", stderr?.substring(0, 300) || "(none)");
358
+ console.log("[DEBUG] First 1000 chars of stdout:");
359
+ console.log(stdout?.substring(0, 1e3) || "(empty)");
360
+ console.log("[DEBUG] End response\n");
361
+ }
343
362
  return {
344
363
  output: stdout || "",
345
364
  error: stderr || void 0
@@ -1195,23 +1214,18 @@ ${pass.falsePositiveHints.map((h) => `- ${h}`).join("\n")}
1195
1214
  ${cwePatterns ? `## KNOWN VULNERABILITY PATTERNS
1196
1215
  ${cwePatterns}` : ""}
1197
1216
 
1198
- ## CRITICAL RULES
1217
+ ## REPORTING GUIDELINES
1199
1218
 
1200
- 1. **ONLY ${pass.name.toUpperCase()} BUGS** - Ignore all other categories
1201
- 2. **MUST HAVE FIX** - No fix = not confirmed = don't report
1202
- 3. **TRACE THE DATA** - Follow user input to the vulnerable sink
1203
- 4. **CHECK GUARDS** - Verify there's no validation you missed
1204
- 5. **BE SPECIFIC** - Exact file, line, and triggering input
1205
- 6. **CHECK CONTROL FLOW** - CRITICAL: Verify the buggy line is actually REACHABLE:
1206
- - Look for early returns: \`if (x.length === 0) return;\` makes later \`x[0]\` SAFE
1207
- - Look for throws: \`if (!user) throw new Error()\` makes later \`user.name\` SAFE
1208
- - Look for guard clauses that exit before the buggy line
1209
- - If a condition causes function exit, code after is UNREACHABLE in that case
1210
- - Example FALSE POSITIVE: \`if (arr.length === 0) return false; ... arr[0].name\` - the access is SAFE because empty array caused early return
1219
+ 1. **ONLY ${pass.name.toUpperCase()} ISSUES** - Focus on this category
1220
+ 2. **REPORT POTENTIAL ISSUES** - If something looks suspicious, report it. Better to flag potential issues than miss real bugs.
1221
+ 3. **INCLUDE CODE SMELLS** - Report risky patterns even if not immediately exploitable (set kind: "smell")
1222
+ 4. **TRACE THE DATA** - Follow user input to potentially vulnerable sinks
1223
+ 5. **BE SPECIFIC** - Include exact file, line number, and what makes it suspicious
1224
+ 6. **SUGGESTED FIX IS OPTIONAL** - Nice to have but not required. Report the issue even without a fix.
1211
1225
 
1212
1226
  ## REPORTING FORMAT
1213
1227
 
1214
- When you find a CONFIRMED ${pass.name} bug:
1228
+ When you find a ${pass.name} issue (bug or code smell):
1215
1229
 
1216
1230
  <json>
1217
1231
  {
@@ -1220,25 +1234,23 @@ When you find a CONFIRMED ${pass.name} bug:
1220
1234
  "file": "src/api/users.ts",
1221
1235
  "line": 42,
1222
1236
  "endLine": 45,
1223
- "title": "Short description of the bug",
1224
- "description": "Detailed explanation of why this is a bug and how it can be exploited.",
1237
+ "title": "Short description of the issue",
1238
+ "description": "Explanation of why this is problematic and potential impact.",
1239
+ "kind": "bug|smell",
1225
1240
  "category": "${pass.category}",
1226
1241
  "severity": "critical|high|medium|low",
1227
1242
  "confidence": "high|medium|low",
1228
- "triggerInput": "Exact input that triggers the bug",
1229
- "codePath": [
1230
- {"step": 1, "file": "src/api/users.ts", "line": 38, "code": "const name = req.query.name", "explanation": "User input enters here"},
1231
- {"step": 2, "file": "src/api/users.ts", "line": 42, "code": "db.execute(query)", "explanation": "Used unsafely here"}
1232
- ],
1233
1243
  "evidence": [
1234
1244
  "Evidence point 1",
1235
1245
  "Evidence point 2"
1236
1246
  ],
1237
- "suggestedFix": "const result = await db.query('SELECT * FROM users WHERE name = $1', [req.query.name]);"
1247
+ "suggestedFix": "Optional: how to fix it"
1238
1248
  }
1239
1249
  }
1240
1250
  </json>
1241
1251
 
1252
+ Use kind="bug" for confirmed vulnerabilities, kind="smell" for risky patterns that need review.
1253
+
1242
1254
  Progress updates:
1243
1255
  ###SCANNING:path/to/file.ts
1244
1256
 
@@ -1247,9 +1259,13 @@ When done:
1247
1259
 
1248
1260
  ## BEGIN
1249
1261
 
1250
- Start by searching for ${pass.name} patterns using grep. Then read the files and trace the data flow. Report bugs as you confirm them.
1262
+ Start by searching for ${pass.name} patterns using grep. Read at least 10-15 files that match the patterns. Report issues as you find them.
1251
1263
 
1252
- REMEMBER: Only ${pass.name.toUpperCase()} bugs. Quality over quantity. Every bug must have an exact fix.`;
1264
+ IMPORTANT:
1265
+ - Report ANYTHING suspicious - we'll filter false positives later
1266
+ - Include code smells and risky patterns, not just confirmed exploits
1267
+ - If unsure, report it with confidence="low"
1268
+ - Aim for thoroughness - finding 10 potential issues is better than finding 0 confirmed bugs`;
1253
1269
  }
1254
1270
 
1255
1271
  // src/providers/prompts/constants.ts
@@ -2554,31 +2570,46 @@ var CoreScanner = class {
2554
2570
  return jobs;
2555
2571
  }
2556
2572
  buildQuickScanPrompt(context) {
2557
- const { understanding, staticResults } = context;
2573
+ const { understanding, staticResults, files } = context;
2558
2574
  const staticSignals = staticResults.length > 0 ? `
2559
2575
  Static analysis signals:
2560
2576
  ${staticResults.slice(0, 30).map((r) => `- ${r.file}:${r.line}: ${r.message}`).join("\n")}` : "";
2561
- return `You are a security auditor. Analyze this ${understanding.summary.type} codebase for bugs.
2577
+ const fileList = files.slice(0, 50).map((f) => `- ${f}`).join("\n");
2578
+ const moreFiles = files.length > 50 ? `
2579
+ ... and ${files.length - 50} more files` : "";
2580
+ return `You are a security auditor. Analyze this ${understanding.summary.type} codebase for bugs and code smells.
2562
2581
 
2563
2582
  Project: ${understanding.summary.description || "Unknown"}
2564
2583
  Framework: ${understanding.summary.framework || "None"}
2565
2584
  Language: ${understanding.summary.language}
2566
- ${staticSignals}
2567
-
2568
- Find bugs in these categories:
2569
- 1. Injection (SQL, command, XSS)
2570
- 2. Auth bypass
2571
- 3. Null/undefined dereference
2572
- 4. Logic errors
2573
- 5. Async/race conditions
2574
- 6. Resource leaks
2575
- 7. Data validation issues
2576
- 8. Secrets exposure
2577
2585
 
2578
- Output ONLY a JSON array:
2579
- [{"file": "path", "line": 42, "title": "Bug title", "description": "Details", "severity": "critical|high|medium|low", "category": "injection|auth-bypass|null-reference|logic-error|async-issue|resource-leak|data-validation|secrets-exposure", "evidence": ["evidence"], "suggestedFix": "fix"}]
2586
+ ## FILES TO ANALYZE
2587
+ Read and analyze these files for security issues:
2588
+ ${fileList}${moreFiles}
2589
+ ${staticSignals}
2580
2590
 
2581
- If no bugs, return: []`;
2591
+ ## INSTRUCTIONS
2592
+ 1. Read each file listed above
2593
+ 2. Look for security issues, bugs, and code smells
2594
+ 3. Report ALL suspicious patterns - false positives will be filtered later
2595
+ 4. Use kind="smell" for risky patterns, kind="bug" for confirmed issues
2596
+
2597
+ ## CATEGORIES TO CHECK
2598
+ - Injection (SQL, command, XSS) - unsanitized user input
2599
+ - Auth bypass - missing auth checks, IDOR
2600
+ - Null/undefined dereference - accessing properties on potentially null values
2601
+ - Logic errors - wrong conditions, off-by-one, inverted checks
2602
+ - Async/race conditions - unhandled promises, race conditions
2603
+ - Resource leaks - unclosed handles, missing cleanup
2604
+ - Data validation - missing input validation, type coercion issues
2605
+ - Secrets exposure - hardcoded keys, leaked credentials
2606
+ - Error handling - swallowed errors, missing try/catch
2607
+
2608
+ ## OUTPUT FORMAT
2609
+ Output a JSON array (one object per issue found):
2610
+ [{"file": "path", "line": 42, "title": "Issue title", "description": "Details", "kind": "bug|smell", "severity": "critical|high|medium|low", "category": "logic-error", "evidence": ["evidence"]}]
2611
+
2612
+ If no issues found after reading all files, return: []`;
2582
2613
  }
2583
2614
  // ─────────────────────────────────────────────────────────────
2584
2615
  // Response Parsing
@@ -2599,6 +2630,23 @@ If no bugs, return: []`;
2599
2630
  } catch {
2600
2631
  }
2601
2632
  }
2633
+ if (bugs.length === 0) {
2634
+ const codeBlockMatches = output.matchAll(/```(?:json)?\s*([\s\S]*?)```/g);
2635
+ for (const match of codeBlockMatches) {
2636
+ try {
2637
+ const parsed = JSON.parse(match[1].trim());
2638
+ const items = Array.isArray(parsed) ? parsed : [parsed];
2639
+ for (const item of items) {
2640
+ const bug = this.parseBugData(item, startIndex + bugs.length, files, passName);
2641
+ if (bug) {
2642
+ bugs.push(bug);
2643
+ this.progress.onBugFound?.(bug);
2644
+ }
2645
+ }
2646
+ } catch {
2647
+ }
2648
+ }
2649
+ }
2602
2650
  if (bugs.length === 0) {
2603
2651
  const arrayMatch = output.match(/\[[\s\S]*\]/);
2604
2652
  if (arrayMatch) {
@@ -2617,6 +2665,20 @@ If no bugs, return: []`;
2617
2665
  }
2618
2666
  }
2619
2667
  }
2668
+ if (bugs.length === 0) {
2669
+ const objectMatches = output.matchAll(/\{[^{}]*"file"[^{}]*"line"[^{}]*\}/g);
2670
+ for (const match of objectMatches) {
2671
+ try {
2672
+ const parsed = JSON.parse(match[0]);
2673
+ const bug = this.parseBugData(parsed, startIndex + bugs.length, files, passName);
2674
+ if (bug) {
2675
+ bugs.push(bug);
2676
+ this.progress.onBugFound?.(bug);
2677
+ }
2678
+ } catch {
2679
+ }
2680
+ }
2681
+ }
2620
2682
  return bugs;
2621
2683
  }
2622
2684
  parseBugData(data, index, files, passName) {
@@ -3039,8 +3101,8 @@ function splitIntoSections(content) {
3039
3101
  function parseListItems(content) {
3040
3102
  const items = [];
3041
3103
  const lines = content.split("\n");
3042
- for (const line of lines) {
3043
- const match = line.match(/^[-*]\s+(.+)/);
3104
+ for (const line2 of lines) {
3105
+ const match = line2.match(/^[-*]\s+(.+)/);
3044
3106
  if (match) {
3045
3107
  const item = match[1].trim();
3046
3108
  if (!item.includes("Add your") && !item.includes("Add files") && item !== "") {
@@ -3066,8 +3128,8 @@ function parseFeatureSection(section) {
3066
3128
  const constraintsMatch = section.match(/\*\*Constraints:\*\*\n((?:[-*]\s+[^\n]+\n?)+)/);
3067
3129
  if (constraintsMatch) {
3068
3130
  const constraintLines = constraintsMatch[1].split("\n");
3069
- for (const line of constraintLines) {
3070
- const match = line.match(/^[-*]\s+(.+)/);
3131
+ for (const line2 of constraintLines) {
3132
+ const match = line2.match(/^[-*]\s+(.+)/);
3071
3133
  if (match) {
3072
3134
  constraints.push(match[1].trim());
3073
3135
  }
@@ -3207,12 +3269,12 @@ function extractIntentFromDocs(docs) {
3207
3269
  if (docs.readme) {
3208
3270
  const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
3209
3271
  if (featuresMatch) {
3210
- 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);
3272
+ const featureLines = featuresMatch[1].split("\n").filter((line2) => line2.trim().startsWith("-") || line2.trim().startsWith("*")).map((line2) => line2.replace(/^[-*]\s*/, "").trim()).filter((line2) => line2.length > 0);
3211
3273
  intent.features.push(...featureLines.slice(0, 20));
3212
3274
  }
3213
3275
  }
3214
3276
  if (docs.envExample) {
3215
- const envLines = docs.envExample.split("\n").filter((line) => line.includes("=") && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter((line) => line.length > 0);
3277
+ const envLines = docs.envExample.split("\n").filter((line2) => line2.includes("=") && !line2.startsWith("#")).map((line2) => line2.split("=")[0].trim()).filter((line2) => line2.length > 0);
3216
3278
  intent.envVariables.push(...envLines);
3217
3279
  }
3218
3280
  for (const apiDoc of docs.apiDocs) {
@@ -3222,7 +3284,7 @@ function extractIntentFromDocs(docs) {
3222
3284
  }
3223
3285
  }
3224
3286
  if (docs.contributing) {
3225
- 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);
3287
+ const conventionLines = docs.contributing.split("\n").filter((line2) => line2.trim().startsWith("-") || line2.trim().startsWith("*")).map((line2) => line2.replace(/^[-*]\s*/, "").trim()).filter((line2) => line2.length > 10 && line2.length < 200).slice(0, 10);
3226
3288
  intent.conventions.push(...conventionLines);
3227
3289
  }
3228
3290
  return intent;
@@ -3716,6 +3778,26 @@ z.object({
3716
3778
  lastIncrementalScan: z.string().datetime().optional(),
3717
3779
  fileHashes: z.array(FileHash)
3718
3780
  });
3781
+ var SeverityBreakdown = z.object({
3782
+ critical: z.number(),
3783
+ high: z.number(),
3784
+ medium: z.number(),
3785
+ low: z.number(),
3786
+ total: z.number()
3787
+ });
3788
+ var ScanSummary = z.object({
3789
+ bugs: SeverityBreakdown,
3790
+ smells: SeverityBreakdown,
3791
+ total: z.number()
3792
+ });
3793
+ var ScanMeta = z.object({
3794
+ repoName: z.string(),
3795
+ provider: z.string(),
3796
+ duration: z.number(),
3797
+ // ms
3798
+ filesScanned: z.number(),
3799
+ linesOfCode: z.number()
3800
+ });
3719
3801
  z.object({
3720
3802
  id: z.string(),
3721
3803
  timestamp: z.string().datetime(),
@@ -3724,16 +3806,10 @@ z.object({
3724
3806
  filesChanged: z.number().optional(),
3725
3807
  duration: z.number(),
3726
3808
  // ms
3809
+ linesOfCode: z.number().optional(),
3727
3810
  bugs: z.array(Bug),
3728
- summary: z.object({
3729
- critical: z.number(),
3730
- high: z.number(),
3731
- medium: z.number(),
3732
- low: z.number(),
3733
- total: z.number(),
3734
- bugs: z.number(),
3735
- smells: z.number()
3736
- })
3811
+ summary: ScanSummary,
3812
+ meta: ScanMeta.optional()
3737
3813
  });
3738
3814
 
3739
3815
  // src/core/config.ts
@@ -3792,8 +3868,8 @@ async function runTypeScript(cwd) {
3792
3868
  } catch (error) {
3793
3869
  const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
3794
3870
  const lines = output.split("\n");
3795
- for (const line of lines) {
3796
- const match = line.match(/^(.+)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.+)$/);
3871
+ for (const line2 of lines) {
3872
+ const match = line2.match(/^(.+)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.+)$/);
3797
3873
  if (match) {
3798
3874
  const filePath = normalizeFilePath(match[1], cwd);
3799
3875
  results.push({
@@ -4012,15 +4088,15 @@ function outputMarkdown(result) {
4012
4088
  lines.push("");
4013
4089
  lines.push("## Summary");
4014
4090
  lines.push("");
4015
- lines.push(`| Severity | Count |`);
4016
- lines.push(`|----------|-------|`);
4017
- lines.push(`| Critical | ${result.summary.critical} |`);
4018
- lines.push(`| High | ${result.summary.high} |`);
4019
- lines.push(`| Medium | ${result.summary.medium} |`);
4020
- lines.push(`| Low | ${result.summary.low} |`);
4021
- lines.push(`| **Verified Bugs** | **${result.summary.bugs}** |`);
4022
- lines.push(`| **Smells** | **${result.summary.smells}** |`);
4023
- lines.push(`| **Total Findings** | **${result.summary.total}** |`);
4091
+ lines.push(`| | Bugs | Smells |`);
4092
+ lines.push(`|----------|-------|-------|`);
4093
+ lines.push(`| Critical | ${result.summary.bugs.critical} | ${result.summary.smells.critical} |`);
4094
+ lines.push(`| High | ${result.summary.bugs.high} | ${result.summary.smells.high} |`);
4095
+ lines.push(`| Medium | ${result.summary.bugs.medium} | ${result.summary.smells.medium} |`);
4096
+ lines.push(`| Low | ${result.summary.bugs.low} | ${result.summary.smells.low} |`);
4097
+ lines.push(`| **Total** | **${result.summary.bugs.total}** | **${result.summary.smells.total}** |`);
4098
+ lines.push("");
4099
+ lines.push(`**Total Findings:** ${result.summary.total}`);
4024
4100
  lines.push("");
4025
4101
  lines.push(`- **Scan Type:** ${result.scanType}`);
4026
4102
  lines.push(`- **Files Scanned:** ${result.filesScanned}`);
@@ -4338,18 +4414,19 @@ function outputHumanReadableMarkdown(result) {
4338
4414
  sections.push(`# Bug Report
4339
4415
 
4340
4416
  > Human-readable summary generated by whiterose on ${new Date(result.timestamp).toLocaleDateString()}
4341
- > **${result.summary.bugs} bugs found** in ${result.filesScanned} files
4417
+ > **${result.summary.bugs.total} bugs found** in ${result.filesScanned} files
4342
4418
 
4343
4419
  ---
4344
4420
 
4345
4421
  ## Summary
4346
4422
 
4347
- | Severity | Count | Description |
4348
- |----------|-------|-------------|
4349
- | \u{1F534} Critical | ${result.summary.critical} | Requires immediate attention |
4350
- | \u{1F7E0} High | ${result.summary.high} | Should be fixed soon |
4351
- | \u{1F7E1} Medium | ${result.summary.medium} | Fix when convenient |
4352
- | \u26AA Low | ${result.summary.low} | Minor issues |
4423
+ | Severity | Bugs | Smells |
4424
+ |----------|------|--------|
4425
+ | Critical | ${result.summary.bugs.critical} | ${result.summary.smells.critical} |
4426
+ | High | ${result.summary.bugs.high} | ${result.summary.smells.high} |
4427
+ | Medium | ${result.summary.bugs.medium} | ${result.summary.smells.medium} |
4428
+ | Low | ${result.summary.bugs.low} | ${result.summary.smells.low} |
4429
+ | **Total** | **${result.summary.bugs.total}** | **${result.summary.smells.total}** |
4353
4430
 
4354
4431
  ---
4355
4432
  `);
@@ -4627,18 +4704,18 @@ function extractFileEffects(filePath, content) {
4627
4704
  let currentFunction = "";
4628
4705
  const functionRegex = /(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>|(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{)/;
4629
4706
  for (let i = 0; i < lines.length; i++) {
4630
- const line = lines[i];
4707
+ const line2 = lines[i];
4631
4708
  const lineNum = i + 1;
4632
- const trimmed = line.trim();
4709
+ const trimmed = line2.trim();
4633
4710
  if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
4634
4711
  continue;
4635
4712
  }
4636
- const funcMatch = line.match(functionRegex);
4713
+ const funcMatch = line2.match(functionRegex);
4637
4714
  if (funcMatch) {
4638
4715
  currentFunction = funcMatch[1] || funcMatch[2] || funcMatch[3] || "";
4639
4716
  }
4640
4717
  for (const pattern of patterns) {
4641
- const match = line.match(pattern.regex);
4718
+ const match = line2.match(pattern.regex);
4642
4719
  if (match) {
4643
4720
  let target = match[1] || "unknown";
4644
4721
  target = target.trim().replace(/['"`,]/g, "");
@@ -4654,8 +4731,8 @@ function extractFileEffects(filePath, content) {
4654
4731
  /cache/
4655
4732
  ];
4656
4733
  for (const pp of pathPatterns) {
4657
- if (line.match(pp)) {
4658
- target = line.match(pp)?.[0] || target;
4734
+ if (line2.match(pp)) {
4735
+ target = line2.match(pp)?.[0] || target;
4659
4736
  break;
4660
4737
  }
4661
4738
  }
@@ -4664,7 +4741,7 @@ function extractFileEffects(filePath, content) {
4664
4741
  type: pattern.type,
4665
4742
  target,
4666
4743
  line: lineNum,
4667
- code: line.trim(),
4744
+ code: line2.trim(),
4668
4745
  functionName: currentFunction
4669
4746
  });
4670
4747
  }
@@ -4949,14 +5026,14 @@ function detectWeakValidation(block) {
4949
5026
  }
4950
5027
  for (let i = 0; i < lines.length; i++) {
4951
5028
  if (reportedLines.has(i)) continue;
4952
- const line = lines[i];
5029
+ const line2 = lines[i];
4953
5030
  const weakPatterns = [
4954
5031
  { regex: /matchCount\s*>=?\s*\d/, issue: "Accepts content if only a few lines match" },
4955
5032
  { regex: /\.length\s*>\s*\w+\.length\s*\*\s*0\.\d/, issue: "Accepts content based only on length ratio" }
4956
5033
  ];
4957
5034
  const issues = [];
4958
5035
  for (const pattern of weakPatterns) {
4959
- if (line.match(pattern.regex)) {
5036
+ if (line2.match(pattern.regex)) {
4960
5037
  issues.push(pattern.issue);
4961
5038
  }
4962
5039
  }
@@ -4970,7 +5047,7 @@ function detectWeakValidation(block) {
4970
5047
  line: block.startLine + i,
4971
5048
  evidence: [
4972
5049
  `Function: ${block.functionName}`,
4973
- `Pattern: ${line.trim()}`,
5050
+ `Pattern: ${line2.trim()}`,
4974
5051
  `Issues: ${issues.join(", ")}`
4975
5052
  ],
4976
5053
  severity: "medium"
@@ -4986,14 +5063,14 @@ function detectPartialFailure(block) {
4986
5063
  let forLoopStart = 0;
4987
5064
  let forLoopItem = "";
4988
5065
  for (let i = 0; i < lines.length; i++) {
4989
- const line = lines[i];
4990
- const forMatch = line.match(/for\s*\(\s*(?:const|let|var)\s+(\w+)\s+of/);
5066
+ const line2 = lines[i];
5067
+ const forMatch = line2.match(/for\s*\(\s*(?:const|let|var)\s+(\w+)\s+of/);
4991
5068
  if (forMatch) {
4992
5069
  inForLoop = true;
4993
5070
  forLoopStart = i;
4994
5071
  forLoopItem = forMatch[1];
4995
5072
  }
4996
- if (inForLoop && line.includes("break")) {
5073
+ if (inForLoop && line2.includes("break")) {
4997
5074
  const hasFailureCheck = lines.slice(Math.max(0, i - 3), i + 1).join("\n").includes("!result.success") || lines.slice(Math.max(0, i - 3), i + 1).join("\n").includes("error") || lines.slice(Math.max(0, i - 3), i + 1).join("\n").includes("failed");
4998
5075
  const hasSkippedReport = block.content.includes("skipped") || block.content.includes("remaining") || block.content.includes("not attempted");
4999
5076
  if (hasFailureCheck && !hasSkippedReport) {
@@ -5013,7 +5090,7 @@ function detectPartialFailure(block) {
5013
5090
  });
5014
5091
  }
5015
5092
  }
5016
- if (inForLoop && line.trim() === "}" && i > forLoopStart + 2) {
5093
+ if (inForLoop && line2.trim() === "}" && i > forLoopStart + 2) {
5017
5094
  inForLoop = false;
5018
5095
  }
5019
5096
  }
@@ -5025,21 +5102,21 @@ function extractFunctions(filePath, content) {
5025
5102
  const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/;
5026
5103
  let currentFunc = null;
5027
5104
  for (let i = 0; i < lines.length; i++) {
5028
- const line = lines[i];
5029
- const trimmed = line.trim();
5105
+ const line2 = lines[i];
5106
+ const trimmed = line2.trim();
5030
5107
  if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
5031
5108
  continue;
5032
5109
  }
5033
5110
  if (!currentFunc) {
5034
- const match = line.match(funcRegex);
5111
+ const match = line2.match(funcRegex);
5035
5112
  if (match) {
5036
5113
  const funcName = match[1] || match[2];
5037
5114
  currentFunc = { name: funcName, start: i, braceCount: 0 };
5038
5115
  }
5039
5116
  }
5040
5117
  if (currentFunc) {
5041
- currentFunc.braceCount += (line.match(/{/g) || []).length;
5042
- currentFunc.braceCount -= (line.match(/}/g) || []).length;
5118
+ currentFunc.braceCount += (line2.match(/{/g) || []).length;
5119
+ currentFunc.braceCount -= (line2.match(/}/g) || []).length;
5043
5120
  if (currentFunc.braceCount <= 0 && i > currentFunc.start) {
5044
5121
  blocks.push({
5045
5122
  file: filePath,
@@ -5248,10 +5325,146 @@ function makeIntentBug(cwd, contract, reason) {
5248
5325
  };
5249
5326
  }
5250
5327
 
5328
+ // src/cli/components/progress.ts
5329
+ function formatDuration(ms) {
5330
+ const seconds = Math.floor(ms / 1e3);
5331
+ const hours = Math.floor(seconds / 3600);
5332
+ const mins = Math.floor(seconds % 3600 / 60);
5333
+ const secs = seconds % 60;
5334
+ if (hours > 0) {
5335
+ return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
5336
+ }
5337
+ if (mins > 0) {
5338
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
5339
+ }
5340
+ return `${secs}s`;
5341
+ }
5342
+
5343
+ // src/cli/components/card.ts
5344
+ var BOX = {
5345
+ topLeft: "\u250C",
5346
+ topRight: "\u2510",
5347
+ bottomLeft: "\u2514",
5348
+ bottomRight: "\u2518",
5349
+ horizontal: "\u2500",
5350
+ vertical: "\u2502",
5351
+ teeRight: "\u251C",
5352
+ teeLeft: "\u2524"
5353
+ };
5354
+ function line(char, width) {
5355
+ return char.repeat(width);
5356
+ }
5357
+ function padRight(str, width) {
5358
+ const stripped = str.replace(/\u001b\[\d+(;\d+)*m/g, "");
5359
+ const padding = Math.max(0, width - stripped.length);
5360
+ return str + " ".repeat(padding);
5361
+ }
5362
+ function row(content, width) {
5363
+ return BOX.vertical + " " + padRight(content, width - 4) + " " + BOX.vertical;
5364
+ }
5365
+ function divider(width) {
5366
+ return BOX.teeRight + line(BOX.horizontal, width) + BOX.teeLeft;
5367
+ }
5368
+ function topBorder(width) {
5369
+ return BOX.topLeft + line(BOX.horizontal, width) + BOX.topRight;
5370
+ }
5371
+ function bottomBorder(width) {
5372
+ return BOX.bottomLeft + line(BOX.horizontal, width) + BOX.bottomRight;
5373
+ }
5374
+ function severityDot(severity) {
5375
+ const colors = {
5376
+ critical: chalk3.red,
5377
+ high: chalk3.yellow,
5378
+ medium: chalk3.blue,
5379
+ low: chalk3.dim
5380
+ };
5381
+ return colors[severity]("\u25CF");
5382
+ }
5383
+ function formatNumber(n) {
5384
+ return String(n).padStart(3);
5385
+ }
5386
+ function renderScanCard(data) {
5387
+ const width = 61;
5388
+ const lines = [];
5389
+ lines.push(topBorder(width));
5390
+ lines.push(row(chalk3.bold.red("WHITEROSE SCAN COMPLETE"), width));
5391
+ lines.push(divider(width));
5392
+ const locFormatted = data.meta.linesOfCode.toLocaleString();
5393
+ lines.push(row(`${chalk3.dim("Repository")} ${data.meta.repoName}`, width));
5394
+ lines.push(row(`${chalk3.dim("Provider")} ${data.meta.provider}`, width));
5395
+ lines.push(row(`${chalk3.dim("Duration")} ${formatDuration(data.meta.duration)}`, width));
5396
+ lines.push(row(`${chalk3.dim("Files")} ${data.meta.filesScanned} files | ${locFormatted} LoC`, width));
5397
+ lines.push(divider(width));
5398
+ lines.push(row(`${chalk3.bold("BUGS")} ${chalk3.bold("SMELLS")}`, width));
5399
+ const severities = ["critical", "high", "medium", "low"];
5400
+ for (const sev of severities) {
5401
+ const bugLine = `${severityDot(sev)} ${sev.charAt(0).toUpperCase() + sev.slice(1).padEnd(8)} ${formatNumber(data.bugs[sev])}`;
5402
+ const smellLine = `${severityDot(sev)} ${sev.charAt(0).toUpperCase() + sev.slice(1).padEnd(8)} ${formatNumber(data.smells[sev])}`;
5403
+ lines.push(row(`${bugLine} ${smellLine}`, width));
5404
+ }
5405
+ lines.push(row(`${chalk3.dim("\u2500".repeat(13))} ${chalk3.dim("\u2500".repeat(13))}`, width));
5406
+ const bugTotal = `Total ${formatNumber(data.bugs.total)}`;
5407
+ const smellTotal = `Total ${formatNumber(data.smells.total)}`;
5408
+ lines.push(row(`${chalk3.bold(bugTotal)} ${chalk3.bold(smellTotal)}`, width));
5409
+ lines.push(divider(width));
5410
+ lines.push(row(`${chalk3.dim("Reports:")} ${chalk3.cyan(data.reportPath)}`, width));
5411
+ if (data.bugs.total > 0 || data.smells.total > 0) {
5412
+ lines.push(row(`${chalk3.dim("Run:")} ${chalk3.cyan("whiterose fix")}`, width));
5413
+ }
5414
+ lines.push(bottomBorder(width));
5415
+ return lines.join("\n");
5416
+ }
5417
+ function renderStatusCard(repoName, provider, totalBugs, totalSmells, lastScanDate) {
5418
+ const width = 45;
5419
+ const lines = [];
5420
+ lines.push(topBorder(width));
5421
+ lines.push(row(chalk3.bold.red("WHITEROSE STATUS"), width));
5422
+ lines.push(divider(width));
5423
+ lines.push(row(`${chalk3.dim("Repository")} ${repoName}`, width));
5424
+ lines.push(row(`${chalk3.dim("Provider")} ${provider}`, width));
5425
+ if (lastScanDate) {
5426
+ lines.push(row(`${chalk3.dim("Last scan")} ${lastScanDate}`, width));
5427
+ }
5428
+ lines.push(divider(width));
5429
+ lines.push(row(`${chalk3.bold("Open bugs:")} ${totalBugs}`, width));
5430
+ lines.push(row(`${chalk3.bold("Open smells:")} ${totalSmells}`, width));
5431
+ lines.push(bottomBorder(width));
5432
+ return lines.join("\n");
5433
+ }
5434
+
5251
5435
  // src/cli/commands/scan.ts
5436
+ function getRepoName(cwd) {
5437
+ try {
5438
+ const gitConfigPath = join(cwd, ".git", "config");
5439
+ if (existsSync(gitConfigPath)) {
5440
+ const config = readFileSync(gitConfigPath, "utf-8");
5441
+ const match = config.match(/url\s*=\s*.*[/:]([^/]+?)(?:\.git)?$/m);
5442
+ if (match) return match[1];
5443
+ }
5444
+ } catch {
5445
+ }
5446
+ return basename(cwd);
5447
+ }
5448
+ async function countLinesOfCode(cwd, files) {
5449
+ let total = 0;
5450
+ for (const file of files.slice(0, 500)) {
5451
+ try {
5452
+ const content = readFileSync(join(cwd, file), "utf-8");
5453
+ const lines = content.split("\n").filter((line2) => {
5454
+ const trimmed = line2.trim();
5455
+ return trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*");
5456
+ });
5457
+ total += lines.length;
5458
+ } catch {
5459
+ }
5460
+ }
5461
+ return total;
5462
+ }
5252
5463
  async function scanCommand(paths, options) {
5464
+ const scanStartTime = Date.now();
5253
5465
  const cwd = process.cwd();
5254
5466
  const whiterosePath = join(cwd, ".whiterose");
5467
+ const repoName = getRepoName(cwd);
5255
5468
  const isQuickScan = options.quick || options.ci;
5256
5469
  const isQuiet = options.json || options.sarif || options.ci;
5257
5470
  if (!isQuickScan && !existsSync(whiterosePath)) {
@@ -5264,8 +5477,10 @@ async function scanCommand(paths, options) {
5264
5477
  process.exit(1);
5265
5478
  }
5266
5479
  if (!isQuiet) {
5267
- const scanMode = isQuickScan ? "quick scan (pre-commit)" : "thorough scan";
5268
- p3.intro(chalk3.red("whiterose") + chalk3.dim(` - ${scanMode}`));
5480
+ const scanMode = isQuickScan ? "quick" : "full";
5481
+ console.log();
5482
+ console.log(chalk3.red.bold("whiterose") + chalk3.dim(` v1.0.9 | ${scanMode} scan`));
5483
+ console.log();
5269
5484
  }
5270
5485
  let config;
5271
5486
  let understanding;
@@ -5311,12 +5526,19 @@ async function scanCommand(paths, options) {
5311
5526
  if (options.full || paths.length > 0) {
5312
5527
  scanType = "full";
5313
5528
  if (!isQuiet) {
5314
- const spinner6 = p3.spinner();
5315
- spinner6.start("Scanning files...");
5316
- filesToScan = paths.length > 0 ? paths : await scanCodebase(cwd, config);
5317
- spinner6.stop(`Found ${filesToScan.length} files to scan`);
5529
+ console.log(chalk3.dim("\u2502") + " Discovering files...");
5530
+ }
5531
+ if (paths.length > 0) {
5532
+ filesToScan = await fg3(paths, {
5533
+ cwd,
5534
+ ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
5535
+ absolute: false
5536
+ });
5318
5537
  } else {
5319
- filesToScan = paths.length > 0 ? paths : await scanCodebase(cwd, config);
5538
+ filesToScan = await scanCodebase(cwd, config);
5539
+ }
5540
+ if (!isQuiet) {
5541
+ console.log(chalk3.dim("\u2502") + ` Found ${chalk3.cyan(filesToScan.length)} files`);
5320
5542
  }
5321
5543
  } else {
5322
5544
  if (!config) {
@@ -5342,12 +5564,11 @@ async function scanCommand(paths, options) {
5342
5564
  }
5343
5565
  let staticResults;
5344
5566
  if (!isQuiet) {
5345
- const staticSpinner = p3.spinner();
5346
- staticSpinner.start("Running static analysis (tsc, eslint)...");
5347
- staticResults = await runStaticAnalysis(cwd, filesToScan, config);
5348
- staticSpinner.stop(`Static analysis: ${staticResults.length} signals found`);
5349
- } else {
5350
- staticResults = await runStaticAnalysis(cwd, filesToScan, config);
5567
+ console.log(chalk3.dim("\u2502") + " Running static analysis (tsc, eslint)...");
5568
+ }
5569
+ staticResults = await runStaticAnalysis(cwd, filesToScan, config);
5570
+ if (!isQuiet) {
5571
+ console.log(chalk3.dim("\u2502") + ` Static analysis: ${chalk3.cyan(staticResults.length)} signals`);
5351
5572
  }
5352
5573
  const providerName = options.provider || config?.provider || "claude-code";
5353
5574
  const executor = getExecutor(providerName);
@@ -5359,13 +5580,12 @@ async function scanCommand(paths, options) {
5359
5580
  }
5360
5581
  let bugs;
5361
5582
  if (!isQuiet) {
5362
- const llmSpinner = p3.spinner();
5363
- const analysisStartTime = Date.now();
5364
- llmSpinner.start(`Analyzing with ${providerName}...`);
5583
+ console.log(chalk3.dim("\u2502"));
5584
+ console.log(chalk3.cyan("\u2550\u2550\u2550 Analyzing with " + providerName + " \u2550\u2550\u2550"));
5585
+ console.log();
5365
5586
  const scanner = new CoreScanner(executor, {}, {
5366
5587
  onProgress: (message) => {
5367
5588
  if (message.trim()) {
5368
- llmSpinner.stop("");
5369
5589
  if (message.includes("\u2550\u2550\u2550\u2550")) {
5370
5590
  console.log(chalk3.cyan(message));
5371
5591
  } else if (message.includes("\u2713")) {
@@ -5377,13 +5597,11 @@ async function scanCommand(paths, options) {
5377
5597
  } else {
5378
5598
  console.log(chalk3.dim(message));
5379
5599
  }
5380
- llmSpinner.start("Scanning...");
5381
5600
  }
5382
5601
  },
5383
5602
  onBugFound: (bug) => {
5384
- llmSpinner.stop("");
5385
- console.log(chalk3.magenta(` \u2605 Found: ${bug.title} (${bug.severity})`));
5386
- llmSpinner.start("Scanning...");
5603
+ const severityColor = bug.severity === "critical" ? chalk3.red : bug.severity === "high" ? chalk3.yellow : bug.severity === "medium" ? chalk3.blue : chalk3.dim;
5604
+ console.log(chalk3.magenta("\u2605") + ` ${severityColor("[" + bug.severity + "]")} ${bug.title}`);
5387
5605
  }
5388
5606
  });
5389
5607
  try {
@@ -5402,11 +5620,11 @@ async function scanCommand(paths, options) {
5402
5620
  config
5403
5621
  });
5404
5622
  }
5405
- const totalTime = Math.floor((Date.now() - analysisStartTime) / 1e3);
5406
- llmSpinner.stop(`Found ${bugs.length} potential bugs (${totalTime}s)`);
5623
+ console.log();
5624
+ console.log(chalk3.dim("\u2502") + ` Found ${chalk3.cyan(bugs.length)} potential bugs`);
5407
5625
  if (scanner.hasPassErrors()) {
5408
5626
  const errors = scanner.getPassErrors();
5409
- p3.log.warn(`${errors.length} analysis pass(es) failed:`);
5627
+ console.log(chalk3.yellow("\u26A0") + ` ${errors.length} analysis pass(es) failed:`);
5410
5628
  for (const err of errors.slice(0, 5)) {
5411
5629
  console.log(chalk3.yellow(` - ${err.passName}: ${err.error}`));
5412
5630
  }
@@ -5415,8 +5633,8 @@ async function scanCommand(paths, options) {
5415
5633
  }
5416
5634
  }
5417
5635
  } catch (error) {
5418
- llmSpinner.stop("Analysis failed");
5419
- p3.log.error(String(error));
5636
+ console.log(chalk3.red("\u2717") + " Analysis failed");
5637
+ console.error(String(error));
5420
5638
  process.exit(1);
5421
5639
  }
5422
5640
  } else {
@@ -5449,61 +5667,53 @@ async function scanCommand(paths, options) {
5449
5667
  }
5450
5668
  if (!isQuickScan) {
5451
5669
  if (!isQuiet) {
5452
- const crossFileSpinner = p3.spinner();
5453
- crossFileSpinner.start("Running cross-file analysis...");
5454
- try {
5455
- const crossFileBugs = await analyzeCrossFile(cwd);
5456
- if (crossFileBugs.length > 0) {
5457
- bugs.push(...crossFileBugs);
5458
- crossFileSpinner.stop(`Cross-file analysis: ${crossFileBugs.length} issues found`);
5459
- } else {
5460
- crossFileSpinner.stop("Cross-file analysis: no issues");
5461
- }
5462
- } catch {
5463
- crossFileSpinner.stop("Cross-file analysis: skipped");
5464
- }
5465
- } else {
5466
- try {
5467
- const crossFileBugs = await analyzeCrossFile(cwd);
5670
+ console.log(chalk3.dim("\u2502") + " Running cross-file analysis...");
5671
+ }
5672
+ try {
5673
+ const crossFileBugs = await analyzeCrossFile(cwd);
5674
+ if (crossFileBugs.length > 0) {
5468
5675
  bugs.push(...crossFileBugs);
5469
- } catch (err) {
5470
- if (options.ci) {
5471
- console.error(JSON.stringify({
5472
- error: "Cross-file analysis failed",
5473
- message: err instanceof Error ? err.message : String(err)
5474
- }));
5475
- process.exit(1);
5676
+ if (!isQuiet) {
5677
+ console.log(chalk3.dim("\u2502") + ` Cross-file: ${chalk3.cyan(crossFileBugs.length)} issues`);
5476
5678
  }
5679
+ } else if (!isQuiet) {
5680
+ console.log(chalk3.dim("\u2502") + " Cross-file: no issues");
5681
+ }
5682
+ } catch (err) {
5683
+ if (!isQuiet) {
5684
+ console.log(chalk3.dim("\u2502") + chalk3.dim(" Cross-file: skipped"));
5685
+ } else if (options.ci) {
5686
+ console.error(JSON.stringify({
5687
+ error: "Cross-file analysis failed",
5688
+ message: err instanceof Error ? err.message : String(err)
5689
+ }));
5690
+ process.exit(1);
5477
5691
  }
5478
5692
  }
5479
5693
  }
5480
5694
  if (!isQuickScan) {
5481
5695
  if (!isQuiet) {
5482
- const contractSpinner = p3.spinner();
5483
- contractSpinner.start("Running contract analysis...");
5484
- try {
5485
- const contractBugs = await analyzeContracts(cwd);
5486
- if (contractBugs.length > 0) {
5487
- bugs.push(...contractBugs);
5488
- contractSpinner.stop(`Contract analysis: ${contractBugs.length} issues found`);
5489
- } else {
5490
- contractSpinner.stop("Contract analysis: no issues");
5491
- }
5492
- } catch {
5493
- contractSpinner.stop("Contract analysis: skipped");
5494
- }
5495
- } else {
5496
- try {
5497
- const contractBugs = await analyzeContracts(cwd);
5696
+ console.log(chalk3.dim("\u2502") + " Running contract analysis...");
5697
+ }
5698
+ try {
5699
+ const contractBugs = await analyzeContracts(cwd);
5700
+ if (contractBugs.length > 0) {
5498
5701
  bugs.push(...contractBugs);
5499
- } catch (err) {
5500
- if (options.ci) {
5501
- console.error(JSON.stringify({
5502
- error: "Contract analysis failed",
5503
- message: err instanceof Error ? err.message : String(err)
5504
- }));
5505
- process.exit(1);
5702
+ if (!isQuiet) {
5703
+ console.log(chalk3.dim("\u2502") + ` Contract: ${chalk3.cyan(contractBugs.length)} issues`);
5506
5704
  }
5705
+ } else if (!isQuiet) {
5706
+ console.log(chalk3.dim("\u2502") + " Contract: no issues");
5707
+ }
5708
+ } catch (err) {
5709
+ if (!isQuiet) {
5710
+ console.log(chalk3.dim("\u2502") + chalk3.dim(" Contract: skipped"));
5711
+ } else if (options.ci) {
5712
+ console.error(JSON.stringify({
5713
+ error: "Contract analysis failed",
5714
+ message: err instanceof Error ? err.message : String(err)
5715
+ }));
5716
+ process.exit(1);
5507
5717
  }
5508
5718
  }
5509
5719
  }
@@ -5543,25 +5753,45 @@ async function scanCommand(paths, options) {
5543
5753
  );
5544
5754
  }
5545
5755
  const allBugs = mergeResult.bugs;
5756
+ const linesOfCode = await countLinesOfCode(cwd, filesToScan);
5757
+ const scanDuration = Date.now() - scanStartTime;
5758
+ const bugItems = allBugs.filter((b) => b.kind === "bug");
5759
+ const smellItems = allBugs.filter((b) => b.kind === "smell");
5760
+ const summary = {
5761
+ bugs: {
5762
+ critical: bugItems.filter((b) => b.severity === "critical").length,
5763
+ high: bugItems.filter((b) => b.severity === "high").length,
5764
+ medium: bugItems.filter((b) => b.severity === "medium").length,
5765
+ low: bugItems.filter((b) => b.severity === "low").length,
5766
+ total: bugItems.length
5767
+ },
5768
+ smells: {
5769
+ critical: smellItems.filter((b) => b.severity === "critical").length,
5770
+ high: smellItems.filter((b) => b.severity === "high").length,
5771
+ medium: smellItems.filter((b) => b.severity === "medium").length,
5772
+ low: smellItems.filter((b) => b.severity === "low").length,
5773
+ total: smellItems.length
5774
+ },
5775
+ total: allBugs.length
5776
+ };
5777
+ const meta = {
5778
+ repoName,
5779
+ provider: providerName,
5780
+ duration: scanDuration,
5781
+ filesScanned: filesToScan.length,
5782
+ linesOfCode
5783
+ };
5546
5784
  const result = {
5547
5785
  id: `scan-${Date.now()}`,
5548
5786
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5549
5787
  scanType,
5550
5788
  filesScanned: filesToScan.length,
5551
5789
  filesChanged: scanType === "incremental" ? filesToScan.length : void 0,
5552
- duration: 0,
5553
- // TODO: track actual duration
5790
+ duration: scanDuration,
5791
+ linesOfCode,
5554
5792
  bugs: allBugs,
5555
- // Use accumulated bugs (union across all scans)
5556
- summary: {
5557
- critical: allBugs.filter((b) => b.kind === "bug" && b.severity === "critical").length,
5558
- high: allBugs.filter((b) => b.kind === "bug" && b.severity === "high").length,
5559
- medium: allBugs.filter((b) => b.kind === "bug" && b.severity === "medium").length,
5560
- low: allBugs.filter((b) => b.kind === "bug" && b.severity === "low").length,
5561
- total: allBugs.length,
5562
- bugs: allBugs.filter((b) => b.kind === "bug").length,
5563
- smells: allBugs.filter((b) => b.kind === "smell").length
5564
- }
5793
+ summary,
5794
+ meta
5565
5795
  };
5566
5796
  if (pendingHashState) {
5567
5797
  try {
@@ -5571,12 +5801,12 @@ async function scanCommand(paths, options) {
5571
5801
  }
5572
5802
  if (options.json || options.ci && !options.sarif) {
5573
5803
  console.log(JSON.stringify(result, null, 2));
5574
- if (options.ci && result.summary.bugs > 0) {
5804
+ if (options.ci && result.summary.bugs.total > 0) {
5575
5805
  process.exit(1);
5576
5806
  }
5577
5807
  } else if (options.sarif) {
5578
5808
  console.log(JSON.stringify(outputSarif(result), null, 2));
5579
- if (options.ci && result.summary.bugs > 0) {
5809
+ if (options.ci && result.summary.bugs.total > 0) {
5580
5810
  process.exit(1);
5581
5811
  }
5582
5812
  } else {
@@ -5599,32 +5829,24 @@ async function scanCommand(paths, options) {
5599
5829
  writeFileSync(sarifPath, JSON.stringify(outputSarif(result), null, 2));
5600
5830
  const jsonPath = join(outputDir, "bugs.json");
5601
5831
  writeFileSync(jsonPath, JSON.stringify(result, null, 2));
5832
+ const lastScanPath = join(whiterosePath, "last-scan.json");
5833
+ writeFileSync(lastScanPath, JSON.stringify(result, null, 2));
5602
5834
  writeFileSync(join(reportsDir, `${timestamp}.sarif`), JSON.stringify(outputSarif(result), null, 2));
5603
5835
  console.log();
5604
- p3.log.message(chalk3.bold("Scan Results"));
5605
- console.log();
5606
- console.log(` ${chalk3.red("\u25CF")} Critical: ${result.summary.critical}`);
5607
- console.log(` ${chalk3.yellow("\u25CF")} High: ${result.summary.high}`);
5608
- console.log(` ${chalk3.blue("\u25CF")} Medium: ${result.summary.medium}`);
5609
- console.log(` ${chalk3.dim("\u25CF")} Low: ${result.summary.low}`);
5610
- console.log();
5611
- if (newBugsThisScan > 0) {
5612
- console.log(` ${chalk3.green("+")} New this scan: ${newBugsThisScan}`);
5613
- }
5614
- console.log(
5615
- ` ${chalk3.bold("Total findings:")} ${result.summary.total} (bugs: ${result.summary.bugs}, smells: ${result.summary.smells})`
5616
- );
5836
+ const cardData = {
5837
+ meta,
5838
+ bugs: summary.bugs,
5839
+ smells: summary.smells,
5840
+ reportPath: "./" + relative(cwd, humanPath)
5841
+ };
5842
+ console.log(renderScanCard(cardData));
5617
5843
  console.log();
5618
- p3.log.success("Reports saved:");
5619
- console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(humanPath)} ${chalk3.dim("(tester-friendly)")}`);
5620
- console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(mdPath)} ${chalk3.dim("(technical)")}`);
5621
- console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(sarifPath)}`);
5622
- console.log(` ${chalk3.dim("\u2514")} ${chalk3.cyan(jsonPath)}`);
5844
+ console.log(chalk3.dim("Reports:"));
5845
+ console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan("./" + relative(cwd, humanPath))} ${chalk3.dim("(tester-friendly)")}`);
5846
+ console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan("./" + relative(cwd, mdPath))} ${chalk3.dim("(technical)")}`);
5847
+ console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan("./" + relative(cwd, sarifPath))}`);
5848
+ console.log(` ${chalk3.dim("\u2514")} ${chalk3.cyan("./" + relative(cwd, jsonPath))}`);
5623
5849
  console.log();
5624
- if (result.summary.total > 0) {
5625
- p3.log.info(`Run ${chalk3.cyan("whiterose fix")} to fix bugs interactively.`);
5626
- }
5627
- p3.outro(chalk3.green("Scan complete"));
5628
5850
  }
5629
5851
  }
5630
5852
  var Dashboard = ({ bugs, onSelectCategory }) => {
@@ -6655,8 +6877,8 @@ async function runAgenticFix(bug, config, projectDir, onProgress) {
6655
6877
  lineBuffer += text2;
6656
6878
  const lines = lineBuffer.split("\n");
6657
6879
  lineBuffer = lines.pop() || "";
6658
- for (const line of lines) {
6659
- const trimmed = line.trim();
6880
+ for (const line2 of lines) {
6881
+ const trimmed = line2.trim();
6660
6882
  if (trimmed) {
6661
6883
  try {
6662
6884
  const event = JSON.parse(trimmed);
@@ -7042,7 +7264,7 @@ function loadBugsFromSarif(sarifPath) {
7042
7264
  const rawFile = r.locations?.[0]?.physicalLocation?.artifactLocation?.uri;
7043
7265
  const file = typeof rawFile === "string" ? rawFile : "unknown";
7044
7266
  const rawLine = r.locations?.[0]?.physicalLocation?.region?.startLine;
7045
- const line = typeof rawLine === "number" && Number.isFinite(rawLine) ? Math.floor(rawLine) : 0;
7267
+ const line2 = typeof rawLine === "number" && Number.isFinite(rawLine) ? Math.floor(rawLine) : 0;
7046
7268
  const rawEndLine = r.locations?.[0]?.physicalLocation?.region?.endLine;
7047
7269
  const endLine = typeof rawEndLine === "number" && Number.isFinite(rawEndLine) ? Math.floor(rawEndLine) : void 0;
7048
7270
  const rawId = r.ruleId;
@@ -7055,7 +7277,7 @@ function loadBugsFromSarif(sarifPath) {
7055
7277
  title: sanitizeSarifText(String(rawTitle), "title"),
7056
7278
  description: sanitizeSarifText(String(rawDescription), "description"),
7057
7279
  file,
7058
- line,
7280
+ line: line2,
7059
7281
  endLine,
7060
7282
  kind: validatedKind.success ? validatedKind.data : "bug",
7061
7283
  severity: mapSarifLevel(r.level),
@@ -7337,13 +7559,13 @@ async function fixSingleBug(bug, config, options, cwd) {
7337
7559
  if (result.diff) {
7338
7560
  console.log();
7339
7561
  console.log(chalk3.dim(" Changes:"));
7340
- for (const line of result.diff.split("\n")) {
7341
- if (line.startsWith("+")) {
7342
- console.log(chalk3.green(` ${line}`));
7343
- } else if (line.startsWith("-")) {
7344
- console.log(chalk3.red(` ${line}`));
7562
+ for (const line2 of result.diff.split("\n")) {
7563
+ if (line2.startsWith("+")) {
7564
+ console.log(chalk3.green(` ${line2}`));
7565
+ } else if (line2.startsWith("-")) {
7566
+ console.log(chalk3.red(` ${line2}`));
7345
7567
  } else {
7346
- console.log(chalk3.dim(` ${line}`));
7568
+ console.log(chalk3.dim(` ${line2}`));
7347
7569
  }
7348
7570
  }
7349
7571
  console.log();
@@ -7427,6 +7649,18 @@ async function refreshCommand(_options) {
7427
7649
  }
7428
7650
  p3.outro(chalk3.green("Refresh complete!"));
7429
7651
  }
7652
+ function getRepoName2(cwd) {
7653
+ try {
7654
+ const gitConfigPath = join(cwd, ".git", "config");
7655
+ if (existsSync(gitConfigPath)) {
7656
+ const config = readFileSync(gitConfigPath, "utf-8");
7657
+ const match = config.match(/url\s*=\s*.*[/:]([^/]+?)(?:\.git)?$/m);
7658
+ if (match) return match[1];
7659
+ }
7660
+ } catch {
7661
+ }
7662
+ return basename(cwd);
7663
+ }
7430
7664
  async function statusCommand() {
7431
7665
  const cwd = process.cwd();
7432
7666
  const whiterosePath = join(cwd, ".whiterose");
@@ -7435,67 +7669,75 @@ async function statusCommand() {
7435
7669
  p3.log.info('Run "whiterose init" first.');
7436
7670
  process.exit(1);
7437
7671
  }
7438
- p3.intro(chalk3.red("whiterose") + chalk3.dim(" - status"));
7672
+ console.log();
7673
+ console.log(chalk3.red.bold("whiterose") + chalk3.dim(" status"));
7674
+ console.log();
7439
7675
  const config = await loadConfig(cwd);
7440
- const understanding = await loadUnderstanding(cwd);
7441
- const availableProviders = await detectProvider();
7676
+ const repoName = getRepoName2(cwd);
7677
+ const lastScanPath = join(whiterosePath, "last-scan.json");
7678
+ let lastScan = null;
7679
+ if (existsSync(lastScanPath)) {
7680
+ try {
7681
+ lastScan = JSON.parse(readFileSync(lastScanPath, "utf-8"));
7682
+ } catch {
7683
+ }
7684
+ }
7685
+ if (lastScan && lastScan.meta) {
7686
+ const cardData = {
7687
+ meta: lastScan.meta,
7688
+ bugs: lastScan.summary.bugs,
7689
+ smells: lastScan.summary.smells,
7690
+ reportPath: "./whiterose-output/bugs-human.md"
7691
+ };
7692
+ console.log(renderScanCard(cardData));
7693
+ console.log();
7694
+ console.log(chalk3.dim(`Last scan: ${new Date(lastScan.timestamp).toLocaleString()}`));
7695
+ } else {
7696
+ const bugStats2 = getAccumulatedBugsStats(cwd);
7697
+ const totalBugs = bugStats2.bySeverity ? Object.values(bugStats2.bySeverity).reduce((a, b) => a + b, 0) : 0;
7698
+ console.log(renderStatusCard(
7699
+ repoName,
7700
+ config.provider,
7701
+ totalBugs,
7702
+ 0,
7703
+ // No smell tracking in accumulated bugs
7704
+ bugStats2.lastUpdated ? new Date(bugStats2.lastUpdated).toLocaleDateString() : void 0
7705
+ ));
7706
+ }
7442
7707
  console.log();
7443
- console.log(chalk3.bold(" Configuration"));
7444
- console.log(` ${chalk3.dim("Provider:")} ${config.provider}`);
7445
- console.log(` ${chalk3.dim("Available:")} ${availableProviders.join(", ") || "none"}`);
7708
+ const availableProviders = await detectProvider();
7709
+ console.log(chalk3.dim("Configuration"));
7710
+ console.log(` Provider: ${config.provider}`);
7711
+ console.log(` Available: ${availableProviders.join(", ") || "none"}`);
7446
7712
  console.log();
7713
+ const understanding = await loadUnderstanding(cwd);
7447
7714
  if (understanding) {
7448
- console.log(chalk3.bold(" Codebase Understanding"));
7449
- console.log(` ${chalk3.dim("Type:")} ${understanding.summary.type}`);
7450
- console.log(` ${chalk3.dim("Framework:")} ${understanding.summary.framework || "none"}`);
7451
- console.log(` ${chalk3.dim("Files:")} ${understanding.structure.totalFiles}`);
7452
- console.log(` ${chalk3.dim("Features:")} ${understanding.features.length}`);
7453
- console.log(` ${chalk3.dim("Contracts:")} ${understanding.contracts.length}`);
7454
- console.log(` ${chalk3.dim("Generated:")} ${understanding.generatedAt}`);
7715
+ console.log(chalk3.dim("Codebase"));
7716
+ console.log(` Type: ${understanding.summary.type}`);
7717
+ console.log(` Framework: ${understanding.summary.framework || "none"}`);
7718
+ console.log(` Files: ${understanding.structure.totalFiles}`);
7719
+ console.log(` Features: ${understanding.features.length}`);
7720
+ console.log(` Contracts: ${understanding.contracts.length}`);
7455
7721
  console.log();
7456
7722
  }
7457
7723
  const hashesPath = join(whiterosePath, "cache", "file-hashes.json");
7458
7724
  if (existsSync(hashesPath)) {
7459
7725
  try {
7460
7726
  const hashes = JSON.parse(readFileSync(hashesPath, "utf-8"));
7461
- console.log(chalk3.bold(" Cache"));
7462
- console.log(` ${chalk3.dim("Files tracked:")} ${hashes.fileHashes?.length || 0}`);
7463
- console.log(` ${chalk3.dim("Last full scan:")} ${hashes.lastFullScan || "never"}`);
7727
+ console.log(chalk3.dim("Cache"));
7728
+ console.log(` Files tracked: ${hashes.fileHashes?.length || 0}`);
7729
+ console.log(` Last full: ${hashes.lastFullScan ? new Date(hashes.lastFullScan).toLocaleDateString() : "never"}`);
7464
7730
  console.log();
7465
7731
  } catch {
7466
7732
  }
7467
7733
  }
7468
7734
  const bugStats = getAccumulatedBugsStats(cwd);
7469
7735
  if (bugStats.total > 0) {
7470
- console.log(chalk3.bold(" Accumulated Bugs"));
7471
- console.log(` ${chalk3.dim("Total:")} ${bugStats.total}`);
7472
- if (Object.keys(bugStats.bySeverity).length > 0) {
7473
- for (const [severity, count] of Object.entries(bugStats.bySeverity)) {
7474
- const color = severity === "critical" ? "red" : severity === "high" ? "yellow" : severity === "medium" ? "blue" : "dim";
7475
- console.log(` ${chalk3[color]("\u25CF")} ${severity}: ${count}`);
7476
- }
7477
- }
7478
- console.log(` ${chalk3.dim("Last updated:")} ${new Date(bugStats.lastUpdated).toLocaleString()}`);
7479
- console.log();
7480
- }
7481
- const reportsDir = join(whiterosePath, "reports");
7482
- if (existsSync(reportsDir)) {
7483
- const reports = readdirSync(reportsDir).filter((f) => f.endsWith(".sarif"));
7484
- if (reports.length > 0) {
7485
- const latestReport = reports.sort().reverse()[0];
7486
- const reportPath = join(reportsDir, latestReport);
7487
- const stats = statSync(reportPath);
7488
- console.log(chalk3.bold(" Last Scan"));
7489
- console.log(` ${chalk3.dim("Report:")} ${latestReport}`);
7490
- console.log(` ${chalk3.dim("Date:")} ${stats.mtime.toISOString()}`);
7491
- console.log();
7492
- }
7493
- }
7494
- if (bugStats.total > 0) {
7495
- p3.outro(chalk3.dim('Run "whiterose fix" to fix bugs, or "whiterose clear" to reset'));
7736
+ console.log(chalk3.dim('Run "whiterose fix" to fix bugs, or "whiterose clear" to reset'));
7496
7737
  } else {
7497
- p3.outro(chalk3.dim('Run "whiterose scan" to scan for bugs'));
7738
+ console.log(chalk3.dim('Run "whiterose scan" to scan for bugs'));
7498
7739
  }
7740
+ console.log();
7499
7741
  }
7500
7742
  async function reportCommand(options) {
7501
7743
  const cwd = process.cwd();
@@ -7538,6 +7780,8 @@ async function reportCommand(options) {
7538
7780
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7539
7781
  status: "open"
7540
7782
  })) || [];
7783
+ const bugItems = bugs.filter((b) => b.kind === "bug");
7784
+ const smellItems = bugs.filter((b) => b.kind === "smell");
7541
7785
  const result = {
7542
7786
  id: "report",
7543
7787
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7546,13 +7790,21 @@ async function reportCommand(options) {
7546
7790
  duration: 0,
7547
7791
  bugs,
7548
7792
  summary: {
7549
- critical: bugs.filter((b) => b.kind === "bug" && b.severity === "critical").length,
7550
- high: bugs.filter((b) => b.kind === "bug" && b.severity === "high").length,
7551
- medium: bugs.filter((b) => b.kind === "bug" && b.severity === "medium").length,
7552
- low: bugs.filter((b) => b.kind === "bug" && b.severity === "low").length,
7553
- total: bugs.length,
7554
- bugs: bugs.filter((b) => b.kind === "bug").length,
7555
- smells: bugs.filter((b) => b.kind === "smell").length
7793
+ bugs: {
7794
+ critical: bugItems.filter((b) => b.severity === "critical").length,
7795
+ high: bugItems.filter((b) => b.severity === "high").length,
7796
+ medium: bugItems.filter((b) => b.severity === "medium").length,
7797
+ low: bugItems.filter((b) => b.severity === "low").length,
7798
+ total: bugItems.length
7799
+ },
7800
+ smells: {
7801
+ critical: smellItems.filter((b) => b.severity === "critical").length,
7802
+ high: smellItems.filter((b) => b.severity === "high").length,
7803
+ medium: smellItems.filter((b) => b.severity === "medium").length,
7804
+ low: smellItems.filter((b) => b.severity === "low").length,
7805
+ total: smellItems.length
7806
+ },
7807
+ total: bugs.length
7556
7808
  }
7557
7809
  };
7558
7810
  let output;