@shakecodeslikecray/whiterose 1.0.9 → 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';
@@ -3101,8 +3101,8 @@ function splitIntoSections(content) {
3101
3101
  function parseListItems(content) {
3102
3102
  const items = [];
3103
3103
  const lines = content.split("\n");
3104
- for (const line of lines) {
3105
- const match = line.match(/^[-*]\s+(.+)/);
3104
+ for (const line2 of lines) {
3105
+ const match = line2.match(/^[-*]\s+(.+)/);
3106
3106
  if (match) {
3107
3107
  const item = match[1].trim();
3108
3108
  if (!item.includes("Add your") && !item.includes("Add files") && item !== "") {
@@ -3128,8 +3128,8 @@ function parseFeatureSection(section) {
3128
3128
  const constraintsMatch = section.match(/\*\*Constraints:\*\*\n((?:[-*]\s+[^\n]+\n?)+)/);
3129
3129
  if (constraintsMatch) {
3130
3130
  const constraintLines = constraintsMatch[1].split("\n");
3131
- for (const line of constraintLines) {
3132
- const match = line.match(/^[-*]\s+(.+)/);
3131
+ for (const line2 of constraintLines) {
3132
+ const match = line2.match(/^[-*]\s+(.+)/);
3133
3133
  if (match) {
3134
3134
  constraints.push(match[1].trim());
3135
3135
  }
@@ -3269,12 +3269,12 @@ function extractIntentFromDocs(docs) {
3269
3269
  if (docs.readme) {
3270
3270
  const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
3271
3271
  if (featuresMatch) {
3272
- 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);
3273
3273
  intent.features.push(...featureLines.slice(0, 20));
3274
3274
  }
3275
3275
  }
3276
3276
  if (docs.envExample) {
3277
- 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);
3278
3278
  intent.envVariables.push(...envLines);
3279
3279
  }
3280
3280
  for (const apiDoc of docs.apiDocs) {
@@ -3284,7 +3284,7 @@ function extractIntentFromDocs(docs) {
3284
3284
  }
3285
3285
  }
3286
3286
  if (docs.contributing) {
3287
- 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);
3288
3288
  intent.conventions.push(...conventionLines);
3289
3289
  }
3290
3290
  return intent;
@@ -3778,6 +3778,26 @@ z.object({
3778
3778
  lastIncrementalScan: z.string().datetime().optional(),
3779
3779
  fileHashes: z.array(FileHash)
3780
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
+ });
3781
3801
  z.object({
3782
3802
  id: z.string(),
3783
3803
  timestamp: z.string().datetime(),
@@ -3786,16 +3806,10 @@ z.object({
3786
3806
  filesChanged: z.number().optional(),
3787
3807
  duration: z.number(),
3788
3808
  // ms
3809
+ linesOfCode: z.number().optional(),
3789
3810
  bugs: z.array(Bug),
3790
- summary: z.object({
3791
- critical: z.number(),
3792
- high: z.number(),
3793
- medium: z.number(),
3794
- low: z.number(),
3795
- total: z.number(),
3796
- bugs: z.number(),
3797
- smells: z.number()
3798
- })
3811
+ summary: ScanSummary,
3812
+ meta: ScanMeta.optional()
3799
3813
  });
3800
3814
 
3801
3815
  // src/core/config.ts
@@ -3854,8 +3868,8 @@ async function runTypeScript(cwd) {
3854
3868
  } catch (error) {
3855
3869
  const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
3856
3870
  const lines = output.split("\n");
3857
- for (const line of lines) {
3858
- 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+(.+)$/);
3859
3873
  if (match) {
3860
3874
  const filePath = normalizeFilePath(match[1], cwd);
3861
3875
  results.push({
@@ -4074,15 +4088,15 @@ function outputMarkdown(result) {
4074
4088
  lines.push("");
4075
4089
  lines.push("## Summary");
4076
4090
  lines.push("");
4077
- lines.push(`| Severity | Count |`);
4078
- lines.push(`|----------|-------|`);
4079
- lines.push(`| Critical | ${result.summary.critical} |`);
4080
- lines.push(`| High | ${result.summary.high} |`);
4081
- lines.push(`| Medium | ${result.summary.medium} |`);
4082
- lines.push(`| Low | ${result.summary.low} |`);
4083
- lines.push(`| **Verified Bugs** | **${result.summary.bugs}** |`);
4084
- lines.push(`| **Smells** | **${result.summary.smells}** |`);
4085
- 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}`);
4086
4100
  lines.push("");
4087
4101
  lines.push(`- **Scan Type:** ${result.scanType}`);
4088
4102
  lines.push(`- **Files Scanned:** ${result.filesScanned}`);
@@ -4400,18 +4414,19 @@ function outputHumanReadableMarkdown(result) {
4400
4414
  sections.push(`# Bug Report
4401
4415
 
4402
4416
  > Human-readable summary generated by whiterose on ${new Date(result.timestamp).toLocaleDateString()}
4403
- > **${result.summary.bugs} bugs found** in ${result.filesScanned} files
4417
+ > **${result.summary.bugs.total} bugs found** in ${result.filesScanned} files
4404
4418
 
4405
4419
  ---
4406
4420
 
4407
4421
  ## Summary
4408
4422
 
4409
- | Severity | Count | Description |
4410
- |----------|-------|-------------|
4411
- | \u{1F534} Critical | ${result.summary.critical} | Requires immediate attention |
4412
- | \u{1F7E0} High | ${result.summary.high} | Should be fixed soon |
4413
- | \u{1F7E1} Medium | ${result.summary.medium} | Fix when convenient |
4414
- | \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}** |
4415
4430
 
4416
4431
  ---
4417
4432
  `);
@@ -4689,18 +4704,18 @@ function extractFileEffects(filePath, content) {
4689
4704
  let currentFunction = "";
4690
4705
  const functionRegex = /(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>|(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{)/;
4691
4706
  for (let i = 0; i < lines.length; i++) {
4692
- const line = lines[i];
4707
+ const line2 = lines[i];
4693
4708
  const lineNum = i + 1;
4694
- const trimmed = line.trim();
4709
+ const trimmed = line2.trim();
4695
4710
  if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
4696
4711
  continue;
4697
4712
  }
4698
- const funcMatch = line.match(functionRegex);
4713
+ const funcMatch = line2.match(functionRegex);
4699
4714
  if (funcMatch) {
4700
4715
  currentFunction = funcMatch[1] || funcMatch[2] || funcMatch[3] || "";
4701
4716
  }
4702
4717
  for (const pattern of patterns) {
4703
- const match = line.match(pattern.regex);
4718
+ const match = line2.match(pattern.regex);
4704
4719
  if (match) {
4705
4720
  let target = match[1] || "unknown";
4706
4721
  target = target.trim().replace(/['"`,]/g, "");
@@ -4716,8 +4731,8 @@ function extractFileEffects(filePath, content) {
4716
4731
  /cache/
4717
4732
  ];
4718
4733
  for (const pp of pathPatterns) {
4719
- if (line.match(pp)) {
4720
- target = line.match(pp)?.[0] || target;
4734
+ if (line2.match(pp)) {
4735
+ target = line2.match(pp)?.[0] || target;
4721
4736
  break;
4722
4737
  }
4723
4738
  }
@@ -4726,7 +4741,7 @@ function extractFileEffects(filePath, content) {
4726
4741
  type: pattern.type,
4727
4742
  target,
4728
4743
  line: lineNum,
4729
- code: line.trim(),
4744
+ code: line2.trim(),
4730
4745
  functionName: currentFunction
4731
4746
  });
4732
4747
  }
@@ -5011,14 +5026,14 @@ function detectWeakValidation(block) {
5011
5026
  }
5012
5027
  for (let i = 0; i < lines.length; i++) {
5013
5028
  if (reportedLines.has(i)) continue;
5014
- const line = lines[i];
5029
+ const line2 = lines[i];
5015
5030
  const weakPatterns = [
5016
5031
  { regex: /matchCount\s*>=?\s*\d/, issue: "Accepts content if only a few lines match" },
5017
5032
  { regex: /\.length\s*>\s*\w+\.length\s*\*\s*0\.\d/, issue: "Accepts content based only on length ratio" }
5018
5033
  ];
5019
5034
  const issues = [];
5020
5035
  for (const pattern of weakPatterns) {
5021
- if (line.match(pattern.regex)) {
5036
+ if (line2.match(pattern.regex)) {
5022
5037
  issues.push(pattern.issue);
5023
5038
  }
5024
5039
  }
@@ -5032,7 +5047,7 @@ function detectWeakValidation(block) {
5032
5047
  line: block.startLine + i,
5033
5048
  evidence: [
5034
5049
  `Function: ${block.functionName}`,
5035
- `Pattern: ${line.trim()}`,
5050
+ `Pattern: ${line2.trim()}`,
5036
5051
  `Issues: ${issues.join(", ")}`
5037
5052
  ],
5038
5053
  severity: "medium"
@@ -5048,14 +5063,14 @@ function detectPartialFailure(block) {
5048
5063
  let forLoopStart = 0;
5049
5064
  let forLoopItem = "";
5050
5065
  for (let i = 0; i < lines.length; i++) {
5051
- const line = lines[i];
5052
- 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/);
5053
5068
  if (forMatch) {
5054
5069
  inForLoop = true;
5055
5070
  forLoopStart = i;
5056
5071
  forLoopItem = forMatch[1];
5057
5072
  }
5058
- if (inForLoop && line.includes("break")) {
5073
+ if (inForLoop && line2.includes("break")) {
5059
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");
5060
5075
  const hasSkippedReport = block.content.includes("skipped") || block.content.includes("remaining") || block.content.includes("not attempted");
5061
5076
  if (hasFailureCheck && !hasSkippedReport) {
@@ -5075,7 +5090,7 @@ function detectPartialFailure(block) {
5075
5090
  });
5076
5091
  }
5077
5092
  }
5078
- if (inForLoop && line.trim() === "}" && i > forLoopStart + 2) {
5093
+ if (inForLoop && line2.trim() === "}" && i > forLoopStart + 2) {
5079
5094
  inForLoop = false;
5080
5095
  }
5081
5096
  }
@@ -5087,21 +5102,21 @@ function extractFunctions(filePath, content) {
5087
5102
  const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/;
5088
5103
  let currentFunc = null;
5089
5104
  for (let i = 0; i < lines.length; i++) {
5090
- const line = lines[i];
5091
- const trimmed = line.trim();
5105
+ const line2 = lines[i];
5106
+ const trimmed = line2.trim();
5092
5107
  if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
5093
5108
  continue;
5094
5109
  }
5095
5110
  if (!currentFunc) {
5096
- const match = line.match(funcRegex);
5111
+ const match = line2.match(funcRegex);
5097
5112
  if (match) {
5098
5113
  const funcName = match[1] || match[2];
5099
5114
  currentFunc = { name: funcName, start: i, braceCount: 0 };
5100
5115
  }
5101
5116
  }
5102
5117
  if (currentFunc) {
5103
- currentFunc.braceCount += (line.match(/{/g) || []).length;
5104
- currentFunc.braceCount -= (line.match(/}/g) || []).length;
5118
+ currentFunc.braceCount += (line2.match(/{/g) || []).length;
5119
+ currentFunc.braceCount -= (line2.match(/}/g) || []).length;
5105
5120
  if (currentFunc.braceCount <= 0 && i > currentFunc.start) {
5106
5121
  blocks.push({
5107
5122
  file: filePath,
@@ -5310,10 +5325,146 @@ function makeIntentBug(cwd, contract, reason) {
5310
5325
  };
5311
5326
  }
5312
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
+
5313
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
+ }
5314
5463
  async function scanCommand(paths, options) {
5464
+ const scanStartTime = Date.now();
5315
5465
  const cwd = process.cwd();
5316
5466
  const whiterosePath = join(cwd, ".whiterose");
5467
+ const repoName = getRepoName(cwd);
5317
5468
  const isQuickScan = options.quick || options.ci;
5318
5469
  const isQuiet = options.json || options.sarif || options.ci;
5319
5470
  if (!isQuickScan && !existsSync(whiterosePath)) {
@@ -5326,8 +5477,10 @@ async function scanCommand(paths, options) {
5326
5477
  process.exit(1);
5327
5478
  }
5328
5479
  if (!isQuiet) {
5329
- const scanMode = isQuickScan ? "quick scan (pre-commit)" : "thorough scan";
5330
- 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();
5331
5484
  }
5332
5485
  let config;
5333
5486
  let understanding;
@@ -5373,28 +5526,19 @@ async function scanCommand(paths, options) {
5373
5526
  if (options.full || paths.length > 0) {
5374
5527
  scanType = "full";
5375
5528
  if (!isQuiet) {
5376
- const spinner6 = p3.spinner();
5377
- spinner6.start("Scanning files...");
5378
- if (paths.length > 0) {
5379
- filesToScan = await fg3(paths, {
5380
- cwd,
5381
- ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
5382
- absolute: false
5383
- });
5384
- } else {
5385
- filesToScan = await scanCodebase(cwd, config);
5386
- }
5387
- 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
+ });
5388
5537
  } else {
5389
- if (paths.length > 0) {
5390
- filesToScan = await fg3(paths, {
5391
- cwd,
5392
- ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
5393
- absolute: false
5394
- });
5395
- } else {
5396
- filesToScan = await scanCodebase(cwd, config);
5397
- }
5538
+ filesToScan = await scanCodebase(cwd, config);
5539
+ }
5540
+ if (!isQuiet) {
5541
+ console.log(chalk3.dim("\u2502") + ` Found ${chalk3.cyan(filesToScan.length)} files`);
5398
5542
  }
5399
5543
  } else {
5400
5544
  if (!config) {
@@ -5420,12 +5564,11 @@ async function scanCommand(paths, options) {
5420
5564
  }
5421
5565
  let staticResults;
5422
5566
  if (!isQuiet) {
5423
- const staticSpinner = p3.spinner();
5424
- staticSpinner.start("Running static analysis (tsc, eslint)...");
5425
- staticResults = await runStaticAnalysis(cwd, filesToScan, config);
5426
- staticSpinner.stop(`Static analysis: ${staticResults.length} signals found`);
5427
- } else {
5428
- 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`);
5429
5572
  }
5430
5573
  const providerName = options.provider || config?.provider || "claude-code";
5431
5574
  const executor = getExecutor(providerName);
@@ -5437,13 +5580,12 @@ async function scanCommand(paths, options) {
5437
5580
  }
5438
5581
  let bugs;
5439
5582
  if (!isQuiet) {
5440
- const llmSpinner = p3.spinner();
5441
- const analysisStartTime = Date.now();
5442
- 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();
5443
5586
  const scanner = new CoreScanner(executor, {}, {
5444
5587
  onProgress: (message) => {
5445
5588
  if (message.trim()) {
5446
- llmSpinner.stop("");
5447
5589
  if (message.includes("\u2550\u2550\u2550\u2550")) {
5448
5590
  console.log(chalk3.cyan(message));
5449
5591
  } else if (message.includes("\u2713")) {
@@ -5455,13 +5597,11 @@ async function scanCommand(paths, options) {
5455
5597
  } else {
5456
5598
  console.log(chalk3.dim(message));
5457
5599
  }
5458
- llmSpinner.start("Scanning...");
5459
5600
  }
5460
5601
  },
5461
5602
  onBugFound: (bug) => {
5462
- llmSpinner.stop("");
5463
- console.log(chalk3.magenta(` \u2605 Found: ${bug.title} (${bug.severity})`));
5464
- 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}`);
5465
5605
  }
5466
5606
  });
5467
5607
  try {
@@ -5480,11 +5620,11 @@ async function scanCommand(paths, options) {
5480
5620
  config
5481
5621
  });
5482
5622
  }
5483
- const totalTime = Math.floor((Date.now() - analysisStartTime) / 1e3);
5484
- 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`);
5485
5625
  if (scanner.hasPassErrors()) {
5486
5626
  const errors = scanner.getPassErrors();
5487
- p3.log.warn(`${errors.length} analysis pass(es) failed:`);
5627
+ console.log(chalk3.yellow("\u26A0") + ` ${errors.length} analysis pass(es) failed:`);
5488
5628
  for (const err of errors.slice(0, 5)) {
5489
5629
  console.log(chalk3.yellow(` - ${err.passName}: ${err.error}`));
5490
5630
  }
@@ -5493,8 +5633,8 @@ async function scanCommand(paths, options) {
5493
5633
  }
5494
5634
  }
5495
5635
  } catch (error) {
5496
- llmSpinner.stop("Analysis failed");
5497
- p3.log.error(String(error));
5636
+ console.log(chalk3.red("\u2717") + " Analysis failed");
5637
+ console.error(String(error));
5498
5638
  process.exit(1);
5499
5639
  }
5500
5640
  } else {
@@ -5527,61 +5667,53 @@ async function scanCommand(paths, options) {
5527
5667
  }
5528
5668
  if (!isQuickScan) {
5529
5669
  if (!isQuiet) {
5530
- const crossFileSpinner = p3.spinner();
5531
- crossFileSpinner.start("Running cross-file analysis...");
5532
- try {
5533
- const crossFileBugs = await analyzeCrossFile(cwd);
5534
- if (crossFileBugs.length > 0) {
5535
- bugs.push(...crossFileBugs);
5536
- crossFileSpinner.stop(`Cross-file analysis: ${crossFileBugs.length} issues found`);
5537
- } else {
5538
- crossFileSpinner.stop("Cross-file analysis: no issues");
5539
- }
5540
- } catch {
5541
- crossFileSpinner.stop("Cross-file analysis: skipped");
5542
- }
5543
- } else {
5544
- try {
5545
- 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) {
5546
5675
  bugs.push(...crossFileBugs);
5547
- } catch (err) {
5548
- if (options.ci) {
5549
- console.error(JSON.stringify({
5550
- error: "Cross-file analysis failed",
5551
- message: err instanceof Error ? err.message : String(err)
5552
- }));
5553
- process.exit(1);
5676
+ if (!isQuiet) {
5677
+ console.log(chalk3.dim("\u2502") + ` Cross-file: ${chalk3.cyan(crossFileBugs.length)} issues`);
5554
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);
5555
5691
  }
5556
5692
  }
5557
5693
  }
5558
5694
  if (!isQuickScan) {
5559
5695
  if (!isQuiet) {
5560
- const contractSpinner = p3.spinner();
5561
- contractSpinner.start("Running contract analysis...");
5562
- try {
5563
- const contractBugs = await analyzeContracts(cwd);
5564
- if (contractBugs.length > 0) {
5565
- bugs.push(...contractBugs);
5566
- contractSpinner.stop(`Contract analysis: ${contractBugs.length} issues found`);
5567
- } else {
5568
- contractSpinner.stop("Contract analysis: no issues");
5569
- }
5570
- } catch {
5571
- contractSpinner.stop("Contract analysis: skipped");
5572
- }
5573
- } else {
5574
- try {
5575
- 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) {
5576
5701
  bugs.push(...contractBugs);
5577
- } catch (err) {
5578
- if (options.ci) {
5579
- console.error(JSON.stringify({
5580
- error: "Contract analysis failed",
5581
- message: err instanceof Error ? err.message : String(err)
5582
- }));
5583
- process.exit(1);
5702
+ if (!isQuiet) {
5703
+ console.log(chalk3.dim("\u2502") + ` Contract: ${chalk3.cyan(contractBugs.length)} issues`);
5584
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);
5585
5717
  }
5586
5718
  }
5587
5719
  }
@@ -5621,25 +5753,45 @@ async function scanCommand(paths, options) {
5621
5753
  );
5622
5754
  }
5623
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
+ };
5624
5784
  const result = {
5625
5785
  id: `scan-${Date.now()}`,
5626
5786
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5627
5787
  scanType,
5628
5788
  filesScanned: filesToScan.length,
5629
5789
  filesChanged: scanType === "incremental" ? filesToScan.length : void 0,
5630
- duration: 0,
5631
- // TODO: track actual duration
5790
+ duration: scanDuration,
5791
+ linesOfCode,
5632
5792
  bugs: allBugs,
5633
- // Use accumulated bugs (union across all scans)
5634
- summary: {
5635
- critical: allBugs.filter((b) => b.kind === "bug" && b.severity === "critical").length,
5636
- high: allBugs.filter((b) => b.kind === "bug" && b.severity === "high").length,
5637
- medium: allBugs.filter((b) => b.kind === "bug" && b.severity === "medium").length,
5638
- low: allBugs.filter((b) => b.kind === "bug" && b.severity === "low").length,
5639
- total: allBugs.length,
5640
- bugs: allBugs.filter((b) => b.kind === "bug").length,
5641
- smells: allBugs.filter((b) => b.kind === "smell").length
5642
- }
5793
+ summary,
5794
+ meta
5643
5795
  };
5644
5796
  if (pendingHashState) {
5645
5797
  try {
@@ -5649,12 +5801,12 @@ async function scanCommand(paths, options) {
5649
5801
  }
5650
5802
  if (options.json || options.ci && !options.sarif) {
5651
5803
  console.log(JSON.stringify(result, null, 2));
5652
- if (options.ci && result.summary.bugs > 0) {
5804
+ if (options.ci && result.summary.bugs.total > 0) {
5653
5805
  process.exit(1);
5654
5806
  }
5655
5807
  } else if (options.sarif) {
5656
5808
  console.log(JSON.stringify(outputSarif(result), null, 2));
5657
- if (options.ci && result.summary.bugs > 0) {
5809
+ if (options.ci && result.summary.bugs.total > 0) {
5658
5810
  process.exit(1);
5659
5811
  }
5660
5812
  } else {
@@ -5677,32 +5829,24 @@ async function scanCommand(paths, options) {
5677
5829
  writeFileSync(sarifPath, JSON.stringify(outputSarif(result), null, 2));
5678
5830
  const jsonPath = join(outputDir, "bugs.json");
5679
5831
  writeFileSync(jsonPath, JSON.stringify(result, null, 2));
5832
+ const lastScanPath = join(whiterosePath, "last-scan.json");
5833
+ writeFileSync(lastScanPath, JSON.stringify(result, null, 2));
5680
5834
  writeFileSync(join(reportsDir, `${timestamp}.sarif`), JSON.stringify(outputSarif(result), null, 2));
5681
5835
  console.log();
5682
- p3.log.message(chalk3.bold("Scan Results"));
5683
- console.log();
5684
- console.log(` ${chalk3.red("\u25CF")} Critical: ${result.summary.critical}`);
5685
- console.log(` ${chalk3.yellow("\u25CF")} High: ${result.summary.high}`);
5686
- console.log(` ${chalk3.blue("\u25CF")} Medium: ${result.summary.medium}`);
5687
- console.log(` ${chalk3.dim("\u25CF")} Low: ${result.summary.low}`);
5688
- console.log();
5689
- if (newBugsThisScan > 0) {
5690
- console.log(` ${chalk3.green("+")} New this scan: ${newBugsThisScan}`);
5691
- }
5692
- console.log(
5693
- ` ${chalk3.bold("Total findings:")} ${result.summary.total} (bugs: ${result.summary.bugs}, smells: ${result.summary.smells})`
5694
- );
5836
+ const cardData = {
5837
+ meta,
5838
+ bugs: summary.bugs,
5839
+ smells: summary.smells,
5840
+ reportPath: "./" + relative(cwd, humanPath)
5841
+ };
5842
+ console.log(renderScanCard(cardData));
5695
5843
  console.log();
5696
- p3.log.success("Reports saved:");
5697
- console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(humanPath)} ${chalk3.dim("(tester-friendly)")}`);
5698
- console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(mdPath)} ${chalk3.dim("(technical)")}`);
5699
- console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(sarifPath)}`);
5700
- 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))}`);
5701
5849
  console.log();
5702
- if (result.summary.total > 0) {
5703
- p3.log.info(`Run ${chalk3.cyan("whiterose fix")} to fix bugs interactively.`);
5704
- }
5705
- p3.outro(chalk3.green("Scan complete"));
5706
5850
  }
5707
5851
  }
5708
5852
  var Dashboard = ({ bugs, onSelectCategory }) => {
@@ -6733,8 +6877,8 @@ async function runAgenticFix(bug, config, projectDir, onProgress) {
6733
6877
  lineBuffer += text2;
6734
6878
  const lines = lineBuffer.split("\n");
6735
6879
  lineBuffer = lines.pop() || "";
6736
- for (const line of lines) {
6737
- const trimmed = line.trim();
6880
+ for (const line2 of lines) {
6881
+ const trimmed = line2.trim();
6738
6882
  if (trimmed) {
6739
6883
  try {
6740
6884
  const event = JSON.parse(trimmed);
@@ -7120,7 +7264,7 @@ function loadBugsFromSarif(sarifPath) {
7120
7264
  const rawFile = r.locations?.[0]?.physicalLocation?.artifactLocation?.uri;
7121
7265
  const file = typeof rawFile === "string" ? rawFile : "unknown";
7122
7266
  const rawLine = r.locations?.[0]?.physicalLocation?.region?.startLine;
7123
- 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;
7124
7268
  const rawEndLine = r.locations?.[0]?.physicalLocation?.region?.endLine;
7125
7269
  const endLine = typeof rawEndLine === "number" && Number.isFinite(rawEndLine) ? Math.floor(rawEndLine) : void 0;
7126
7270
  const rawId = r.ruleId;
@@ -7133,7 +7277,7 @@ function loadBugsFromSarif(sarifPath) {
7133
7277
  title: sanitizeSarifText(String(rawTitle), "title"),
7134
7278
  description: sanitizeSarifText(String(rawDescription), "description"),
7135
7279
  file,
7136
- line,
7280
+ line: line2,
7137
7281
  endLine,
7138
7282
  kind: validatedKind.success ? validatedKind.data : "bug",
7139
7283
  severity: mapSarifLevel(r.level),
@@ -7415,13 +7559,13 @@ async function fixSingleBug(bug, config, options, cwd) {
7415
7559
  if (result.diff) {
7416
7560
  console.log();
7417
7561
  console.log(chalk3.dim(" Changes:"));
7418
- for (const line of result.diff.split("\n")) {
7419
- if (line.startsWith("+")) {
7420
- console.log(chalk3.green(` ${line}`));
7421
- } else if (line.startsWith("-")) {
7422
- 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}`));
7423
7567
  } else {
7424
- console.log(chalk3.dim(` ${line}`));
7568
+ console.log(chalk3.dim(` ${line2}`));
7425
7569
  }
7426
7570
  }
7427
7571
  console.log();
@@ -7505,6 +7649,18 @@ async function refreshCommand(_options) {
7505
7649
  }
7506
7650
  p3.outro(chalk3.green("Refresh complete!"));
7507
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
+ }
7508
7664
  async function statusCommand() {
7509
7665
  const cwd = process.cwd();
7510
7666
  const whiterosePath = join(cwd, ".whiterose");
@@ -7513,67 +7669,75 @@ async function statusCommand() {
7513
7669
  p3.log.info('Run "whiterose init" first.');
7514
7670
  process.exit(1);
7515
7671
  }
7516
- p3.intro(chalk3.red("whiterose") + chalk3.dim(" - status"));
7672
+ console.log();
7673
+ console.log(chalk3.red.bold("whiterose") + chalk3.dim(" status"));
7674
+ console.log();
7517
7675
  const config = await loadConfig(cwd);
7518
- const understanding = await loadUnderstanding(cwd);
7519
- 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
+ }
7520
7707
  console.log();
7521
- console.log(chalk3.bold(" Configuration"));
7522
- console.log(` ${chalk3.dim("Provider:")} ${config.provider}`);
7523
- 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"}`);
7524
7712
  console.log();
7713
+ const understanding = await loadUnderstanding(cwd);
7525
7714
  if (understanding) {
7526
- console.log(chalk3.bold(" Codebase Understanding"));
7527
- console.log(` ${chalk3.dim("Type:")} ${understanding.summary.type}`);
7528
- console.log(` ${chalk3.dim("Framework:")} ${understanding.summary.framework || "none"}`);
7529
- console.log(` ${chalk3.dim("Files:")} ${understanding.structure.totalFiles}`);
7530
- console.log(` ${chalk3.dim("Features:")} ${understanding.features.length}`);
7531
- console.log(` ${chalk3.dim("Contracts:")} ${understanding.contracts.length}`);
7532
- 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}`);
7533
7721
  console.log();
7534
7722
  }
7535
7723
  const hashesPath = join(whiterosePath, "cache", "file-hashes.json");
7536
7724
  if (existsSync(hashesPath)) {
7537
7725
  try {
7538
7726
  const hashes = JSON.parse(readFileSync(hashesPath, "utf-8"));
7539
- console.log(chalk3.bold(" Cache"));
7540
- console.log(` ${chalk3.dim("Files tracked:")} ${hashes.fileHashes?.length || 0}`);
7541
- 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"}`);
7542
7730
  console.log();
7543
7731
  } catch {
7544
7732
  }
7545
7733
  }
7546
7734
  const bugStats = getAccumulatedBugsStats(cwd);
7547
7735
  if (bugStats.total > 0) {
7548
- console.log(chalk3.bold(" Accumulated Bugs"));
7549
- console.log(` ${chalk3.dim("Total:")} ${bugStats.total}`);
7550
- if (Object.keys(bugStats.bySeverity).length > 0) {
7551
- for (const [severity, count] of Object.entries(bugStats.bySeverity)) {
7552
- const color = severity === "critical" ? "red" : severity === "high" ? "yellow" : severity === "medium" ? "blue" : "dim";
7553
- console.log(` ${chalk3[color]("\u25CF")} ${severity}: ${count}`);
7554
- }
7555
- }
7556
- console.log(` ${chalk3.dim("Last updated:")} ${new Date(bugStats.lastUpdated).toLocaleString()}`);
7557
- console.log();
7558
- }
7559
- const reportsDir = join(whiterosePath, "reports");
7560
- if (existsSync(reportsDir)) {
7561
- const reports = readdirSync(reportsDir).filter((f) => f.endsWith(".sarif"));
7562
- if (reports.length > 0) {
7563
- const latestReport = reports.sort().reverse()[0];
7564
- const reportPath = join(reportsDir, latestReport);
7565
- const stats = statSync(reportPath);
7566
- console.log(chalk3.bold(" Last Scan"));
7567
- console.log(` ${chalk3.dim("Report:")} ${latestReport}`);
7568
- console.log(` ${chalk3.dim("Date:")} ${stats.mtime.toISOString()}`);
7569
- console.log();
7570
- }
7571
- }
7572
- if (bugStats.total > 0) {
7573
- 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'));
7574
7737
  } else {
7575
- p3.outro(chalk3.dim('Run "whiterose scan" to scan for bugs'));
7738
+ console.log(chalk3.dim('Run "whiterose scan" to scan for bugs'));
7576
7739
  }
7740
+ console.log();
7577
7741
  }
7578
7742
  async function reportCommand(options) {
7579
7743
  const cwd = process.cwd();
@@ -7616,6 +7780,8 @@ async function reportCommand(options) {
7616
7780
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7617
7781
  status: "open"
7618
7782
  })) || [];
7783
+ const bugItems = bugs.filter((b) => b.kind === "bug");
7784
+ const smellItems = bugs.filter((b) => b.kind === "smell");
7619
7785
  const result = {
7620
7786
  id: "report",
7621
7787
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7624,13 +7790,21 @@ async function reportCommand(options) {
7624
7790
  duration: 0,
7625
7791
  bugs,
7626
7792
  summary: {
7627
- critical: bugs.filter((b) => b.kind === "bug" && b.severity === "critical").length,
7628
- high: bugs.filter((b) => b.kind === "bug" && b.severity === "high").length,
7629
- medium: bugs.filter((b) => b.kind === "bug" && b.severity === "medium").length,
7630
- low: bugs.filter((b) => b.kind === "bug" && b.severity === "low").length,
7631
- total: bugs.length,
7632
- bugs: bugs.filter((b) => b.kind === "bug").length,
7633
- 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
7634
7808
  }
7635
7809
  };
7636
7810
  let output;