@shakecodeslikecray/whiterose 0.1.0 → 0.2.2
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 +550 -214
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +224 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, statSy
|
|
|
6
6
|
import { join, isAbsolute, resolve, basename, relative, dirname } from 'path';
|
|
7
7
|
import { execa } from 'execa';
|
|
8
8
|
import { homedir, tmpdir } from 'os';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
import fg3 from 'fast-glob';
|
|
11
12
|
import { createHash } from 'crypto';
|
|
@@ -14,6 +15,7 @@ import { render, useApp, useInput, Box, Text } from 'ink';
|
|
|
14
15
|
import { useState, useEffect } from 'react';
|
|
15
16
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
16
17
|
import Spinner from 'ink-spinner';
|
|
18
|
+
import * as readline from 'readline';
|
|
17
19
|
|
|
18
20
|
var providerChecks = [
|
|
19
21
|
{
|
|
@@ -583,7 +585,7 @@ var ClaudeCodeProvider = class {
|
|
|
583
585
|
// Agentic Prompts
|
|
584
586
|
// ─────────────────────────────────────────────────────────────
|
|
585
587
|
buildAgenticAnalysisPrompt(understanding) {
|
|
586
|
-
return `You are whiterose, an expert bug hunter. Your task is to explore this codebase and find
|
|
588
|
+
return `You are whiterose, an expert bug hunter. Your task is to explore this codebase and find REAL bugs.
|
|
587
589
|
|
|
588
590
|
CODEBASE CONTEXT:
|
|
589
591
|
- Type: ${understanding.summary.type}
|
|
@@ -600,66 +602,78 @@ YOUR TASK:
|
|
|
600
602
|
- Edge cases (empty arrays, zero values, boundaries)
|
|
601
603
|
- Resource leaks (unclosed connections)
|
|
602
604
|
|
|
605
|
+
CRITICAL - INLINE VALIDATION:
|
|
606
|
+
Before reporting ANY bug, you MUST try to DISPROVE it:
|
|
607
|
+
1. Check if there's a guard/validation that prevents the issue
|
|
608
|
+
2. Check if the type system guarantees safety
|
|
609
|
+
3. Check if framework behavior handles this case
|
|
610
|
+
4. Check if the code path is actually reachable
|
|
611
|
+
5. Only report the bug if you CANNOT disprove it
|
|
612
|
+
|
|
603
613
|
PROTOCOL - You MUST output these markers:
|
|
604
|
-
-
|
|
605
|
-
- When you find a bug, output: ${MARKERS.BUG}<json>
|
|
614
|
+
- When reading a file, output: ${MARKERS.SCANNING}<filepath>
|
|
615
|
+
- When you find a VALIDATED bug, output: ${MARKERS.BUG}<json>
|
|
606
616
|
- When completely done, output: ${MARKERS.COMPLETE}
|
|
607
617
|
- If you encounter an error, output: ${MARKERS.ERROR}<message>
|
|
608
618
|
|
|
609
|
-
BUG JSON FORMAT:
|
|
610
|
-
${MARKERS.BUG}{"file":"src/api/users.ts","line":42,"title":"Null dereference in getUserById","description":"...","severity":"high","category":"null-reference","evidence":["
|
|
619
|
+
BUG JSON FORMAT (only after validation):
|
|
620
|
+
${MARKERS.BUG}{"file":"src/api/users.ts","line":42,"title":"Null dereference in getUserById","description":"...","severity":"high","category":"null-reference","confidence":"high","evidence":["line of code","why it's reachable"],"validationNotes":"Checked for guards - none found. Type allows null.","suggestedFix":"..."}
|
|
621
|
+
|
|
622
|
+
SEVERITY GUIDE:
|
|
623
|
+
- critical: Security vulnerabilities, data loss, crashes in production
|
|
624
|
+
- high: Bugs that will definitely cause issues for users
|
|
625
|
+
- medium: Edge cases that may cause issues under certain conditions
|
|
626
|
+
- low: Minor issues, potential improvements
|
|
611
627
|
|
|
612
628
|
IMPORTANT:
|
|
613
|
-
-
|
|
614
|
-
-
|
|
615
|
-
-
|
|
616
|
-
-
|
|
629
|
+
- Do NOT report style issues or potential improvements
|
|
630
|
+
- Do NOT report bugs you haven't validated
|
|
631
|
+
- Include EXACT line numbers from the files you read
|
|
632
|
+
- Include validation notes explaining why the bug is real
|
|
633
|
+
- Explore systematically - check entry points, API routes, data flows
|
|
617
634
|
|
|
618
|
-
Now explore this codebase and find bugs. Start by reading the main entry points.`;
|
|
635
|
+
Now explore this codebase and find VALIDATED bugs. Start by reading the main entry points.`;
|
|
619
636
|
}
|
|
620
637
|
buildAgenticUnderstandingPrompt(existingDocsSummary) {
|
|
621
638
|
const docsSection = existingDocsSummary ? `
|
|
622
639
|
|
|
623
|
-
EXISTING DOCUMENTATION
|
|
640
|
+
EXISTING DOCUMENTATION:
|
|
624
641
|
${existingDocsSummary}
|
|
625
642
|
` : "";
|
|
626
|
-
return `You are whiterose. Your task is to understand this codebase.
|
|
643
|
+
return `You are whiterose. Your task is to QUICKLY understand this codebase structure.
|
|
627
644
|
${docsSection}
|
|
628
|
-
YOUR TASK:
|
|
629
|
-
1.
|
|
630
|
-
2.
|
|
631
|
-
3.
|
|
632
|
-
4.
|
|
633
|
-
5. Identify main features, business rules, and behavioral contracts
|
|
645
|
+
YOUR TASK (be fast, read only key files):
|
|
646
|
+
1. Read package.json or similar config to understand the project type
|
|
647
|
+
2. Read the main entry point (index.ts, main.ts, app.ts, etc.)
|
|
648
|
+
3. Skim 2-3 core files to understand the architecture
|
|
649
|
+
4. Output your understanding - don't over-explore
|
|
634
650
|
|
|
635
651
|
PROTOCOL - You MUST output these markers:
|
|
636
|
-
-
|
|
637
|
-
- When you have
|
|
638
|
-
- When
|
|
652
|
+
- When reading a file, output: ${MARKERS.SCANNING}<filepath>
|
|
653
|
+
- When you have understanding, output: ${MARKERS.UNDERSTANDING}<json>
|
|
654
|
+
- When done, output: ${MARKERS.COMPLETE}
|
|
639
655
|
|
|
640
656
|
UNDERSTANDING JSON FORMAT:
|
|
641
657
|
${MARKERS.UNDERSTANDING}{
|
|
642
658
|
"summary": {
|
|
643
|
-
"type": "api|web-app|cli|library|
|
|
644
|
-
"framework": "next.js|express|react|
|
|
659
|
+
"type": "api|web-app|cli|library|monorepo",
|
|
660
|
+
"framework": "next.js|express|react|fastify|none",
|
|
645
661
|
"language": "typescript|javascript",
|
|
646
|
-
"description": "2
|
|
662
|
+
"description": "1-2 sentence description of what this project does"
|
|
647
663
|
},
|
|
648
664
|
"features": [
|
|
649
|
-
{"name": "Feature", "description": "
|
|
665
|
+
{"name": "Feature Name", "description": "Brief description", "priority": "critical|high|medium|low", "relatedFiles": ["path/to/file.ts"]}
|
|
650
666
|
],
|
|
651
|
-
"contracts": [
|
|
652
|
-
{"function": "functionName", "file": "path/to/file.ts", "inputs": [], "outputs": {}, "invariants": ["must do X before Y"], "sideEffects": [], "throws": []}
|
|
653
|
-
]
|
|
667
|
+
"contracts": []
|
|
654
668
|
}
|
|
655
669
|
|
|
656
670
|
IMPORTANT:
|
|
657
|
-
-
|
|
658
|
-
- Focus on
|
|
659
|
-
- Identify
|
|
660
|
-
-
|
|
671
|
+
- Be FAST - read 3-5 files max
|
|
672
|
+
- Focus on WHAT the project does, not implementation details
|
|
673
|
+
- Identify the main features/capabilities
|
|
674
|
+
- Don't deep-dive into every file
|
|
661
675
|
|
|
662
|
-
Now
|
|
676
|
+
Now QUICKLY understand this codebase.`;
|
|
663
677
|
}
|
|
664
678
|
buildAdversarialPrompt(bug, fileContent) {
|
|
665
679
|
return `You are a skeptical code reviewer. Try to DISPROVE this bug report.
|
|
@@ -688,26 +702,37 @@ Set "survived": true if you CANNOT disprove it (it's a real bug).`;
|
|
|
688
702
|
// Claude CLI Execution (Agentic Mode)
|
|
689
703
|
// ─────────────────────────────────────────────────────────────
|
|
690
704
|
async runAgenticClaude(prompt, cwd, callbacks) {
|
|
705
|
+
this.streamBuffer = "";
|
|
706
|
+
this.fullResponseBuffer = "";
|
|
691
707
|
const claudeCommand = getProviderCommand("claude-code");
|
|
692
|
-
const args = [
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
708
|
+
const args = [
|
|
709
|
+
"-p",
|
|
710
|
+
prompt,
|
|
711
|
+
"--output-format",
|
|
712
|
+
"stream-json",
|
|
713
|
+
"--verbose"
|
|
714
|
+
];
|
|
715
|
+
args.push("--dangerously-skip-permissions");
|
|
716
|
+
this.currentProcess = spawn(claudeCommand, args, {
|
|
717
|
+
cwd,
|
|
718
|
+
env: {
|
|
719
|
+
...process.env,
|
|
720
|
+
NO_COLOR: "1"
|
|
721
|
+
},
|
|
722
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
723
|
+
});
|
|
724
|
+
let lastActivity = Date.now();
|
|
725
|
+
const heartbeat = setInterval(() => {
|
|
726
|
+
const elapsed = Math.floor((Date.now() - lastActivity) / 1e3);
|
|
727
|
+
if (elapsed > 10) {
|
|
728
|
+
this.reportProgress(`Analyzing... (${elapsed}s since last update)`);
|
|
706
729
|
}
|
|
707
|
-
);
|
|
730
|
+
}, 5e3);
|
|
708
731
|
let buffer = "";
|
|
709
732
|
this.currentProcess.stdout?.on("data", (chunk) => {
|
|
710
|
-
|
|
733
|
+
lastActivity = Date.now();
|
|
734
|
+
const text3 = chunk.toString();
|
|
735
|
+
buffer += text3;
|
|
711
736
|
const lines = buffer.split("\n");
|
|
712
737
|
buffer = lines.pop() || "";
|
|
713
738
|
for (const line of lines) {
|
|
@@ -715,17 +740,95 @@ Set "survived": true if you CANNOT disprove it (it's a real bug).`;
|
|
|
715
740
|
}
|
|
716
741
|
});
|
|
717
742
|
this.currentProcess.stderr?.on("data", (chunk) => {
|
|
743
|
+
lastActivity = Date.now();
|
|
718
744
|
const text3 = chunk.toString().trim();
|
|
719
|
-
if (text3
|
|
745
|
+
if (text3) {
|
|
746
|
+
this.reportProgress(`Claude: ${text3.slice(0, 50)}...`);
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
await new Promise((resolve5, reject) => {
|
|
750
|
+
const timeout = setTimeout(() => {
|
|
751
|
+
this.currentProcess?.kill();
|
|
752
|
+
reject(new Error("Claude analysis timed out after 5 minutes"));
|
|
753
|
+
}, 3e5);
|
|
754
|
+
this.currentProcess?.on("exit", (code) => {
|
|
755
|
+
clearTimeout(timeout);
|
|
756
|
+
if (code !== 0 && code !== null) {
|
|
757
|
+
this.reportProgress(`Claude exited with code ${code}`);
|
|
758
|
+
}
|
|
759
|
+
resolve5();
|
|
760
|
+
});
|
|
761
|
+
this.currentProcess?.on("error", (err) => {
|
|
762
|
+
clearTimeout(timeout);
|
|
763
|
+
reject(err);
|
|
764
|
+
});
|
|
720
765
|
});
|
|
721
|
-
|
|
766
|
+
clearInterval(heartbeat);
|
|
722
767
|
if (buffer.trim()) {
|
|
723
768
|
this.processAgentOutput(buffer, callbacks);
|
|
724
769
|
}
|
|
770
|
+
if (this.fullResponseBuffer && callbacks.onUnderstanding) {
|
|
771
|
+
this.tryExtractUnderstandingJson(this.fullResponseBuffer, callbacks);
|
|
772
|
+
}
|
|
725
773
|
this.currentProcess = void 0;
|
|
726
774
|
}
|
|
727
775
|
processAgentOutput(line, callbacks) {
|
|
728
776
|
const trimmed = line.trim();
|
|
777
|
+
if (!trimmed) return;
|
|
778
|
+
try {
|
|
779
|
+
const event = JSON.parse(trimmed);
|
|
780
|
+
if (event.type === "system") {
|
|
781
|
+
this.reportProgress("Claude initialized...");
|
|
782
|
+
} else if (event.type === "stream_event") {
|
|
783
|
+
const innerEvent = event.event;
|
|
784
|
+
if (innerEvent?.type === "content_block_delta") {
|
|
785
|
+
const text3 = innerEvent.delta?.text;
|
|
786
|
+
if (text3) {
|
|
787
|
+
this.processTextContent(text3, callbacks);
|
|
788
|
+
}
|
|
789
|
+
} else if (innerEvent?.type === "content_block_start") {
|
|
790
|
+
if (innerEvent.content_block?.type === "tool_use") {
|
|
791
|
+
const toolName = innerEvent.content_block.name || "tool";
|
|
792
|
+
this.reportProgress(`Using ${toolName}...`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
} else if (event.type === "assistant") {
|
|
796
|
+
const content = event.message?.content || [];
|
|
797
|
+
for (const item of content) {
|
|
798
|
+
if (item.type === "text" && item.text) {
|
|
799
|
+
this.processTextContent(item.text, callbacks);
|
|
800
|
+
} else if (item.type === "tool_use") {
|
|
801
|
+
const toolName = item.name || "tool";
|
|
802
|
+
const input = item.input || {};
|
|
803
|
+
if (toolName === "Read" && input.file_path) {
|
|
804
|
+
callbacks.onScanning?.(input.file_path);
|
|
805
|
+
this.reportProgress(`Reading: ${input.file_path.split("/").pop()}`);
|
|
806
|
+
} else if (toolName === "Glob" || toolName === "Grep") {
|
|
807
|
+
this.reportProgress(`Searching: ${input.pattern || "..."}`);
|
|
808
|
+
} else {
|
|
809
|
+
this.reportProgress(`Using ${toolName}...`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
} else if (event.type === "user") {
|
|
814
|
+
if (event.tool_use_result) {
|
|
815
|
+
const numFiles = event.tool_use_result.numFiles;
|
|
816
|
+
if (numFiles) {
|
|
817
|
+
this.reportProgress(`Found ${numFiles} files...`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} else if (event.type === "result") {
|
|
821
|
+
if (event.result) {
|
|
822
|
+
this.processTextContent(event.result, callbacks);
|
|
823
|
+
this.tryExtractUnderstandingJson(event.result, callbacks);
|
|
824
|
+
}
|
|
825
|
+
callbacks.onComplete?.();
|
|
826
|
+
} else if (event.type === "error") {
|
|
827
|
+
callbacks.onError?.(event.error || event.message || "Unknown error");
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
729
832
|
if (trimmed.startsWith(MARKERS.SCANNING)) {
|
|
730
833
|
const file = trimmed.slice(MARKERS.SCANNING.length).trim();
|
|
731
834
|
callbacks.onScanning?.(file);
|
|
@@ -742,6 +845,77 @@ Set "survived": true if you CANNOT disprove it (it's a real bug).`;
|
|
|
742
845
|
callbacks.onError?.(error);
|
|
743
846
|
}
|
|
744
847
|
}
|
|
848
|
+
// Accumulated text for marker detection in streaming responses
|
|
849
|
+
streamBuffer = "";
|
|
850
|
+
// Full response text for fallback extraction
|
|
851
|
+
fullResponseBuffer = "";
|
|
852
|
+
processTextContent(text3, callbacks) {
|
|
853
|
+
this.streamBuffer += text3;
|
|
854
|
+
this.fullResponseBuffer += text3;
|
|
855
|
+
const lines = this.streamBuffer.split("\n");
|
|
856
|
+
this.streamBuffer = lines.pop() || "";
|
|
857
|
+
for (const line of lines) {
|
|
858
|
+
const trimmed = line.trim();
|
|
859
|
+
if (trimmed.startsWith(MARKERS.SCANNING)) {
|
|
860
|
+
callbacks.onScanning?.(trimmed.slice(MARKERS.SCANNING.length).trim());
|
|
861
|
+
} else if (trimmed.startsWith(MARKERS.BUG)) {
|
|
862
|
+
callbacks.onBugFound?.(trimmed.slice(MARKERS.BUG.length).trim());
|
|
863
|
+
} else if (trimmed.startsWith(MARKERS.UNDERSTANDING)) {
|
|
864
|
+
callbacks.onUnderstanding?.(trimmed.slice(MARKERS.UNDERSTANDING.length).trim());
|
|
865
|
+
} else if (trimmed.startsWith(MARKERS.COMPLETE)) {
|
|
866
|
+
callbacks.onComplete?.();
|
|
867
|
+
} else if (trimmed.startsWith(MARKERS.ERROR)) {
|
|
868
|
+
callbacks.onError?.(trimmed.slice(MARKERS.ERROR.length).trim());
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// Try to extract understanding JSON from text without markers
|
|
873
|
+
tryExtractUnderstandingJson(text3, callbacks) {
|
|
874
|
+
const codeBlockMatch = text3.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
875
|
+
if (codeBlockMatch) {
|
|
876
|
+
const jsonStr = codeBlockMatch[1].trim();
|
|
877
|
+
try {
|
|
878
|
+
const parsed = JSON.parse(jsonStr);
|
|
879
|
+
if (parsed.summary && (parsed.summary.type || parsed.summary.language)) {
|
|
880
|
+
callbacks.onUnderstanding?.(jsonStr);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const candidates = [];
|
|
887
|
+
let depth = 0;
|
|
888
|
+
let start = -1;
|
|
889
|
+
for (let i = 0; i < text3.length; i++) {
|
|
890
|
+
const char = text3[i];
|
|
891
|
+
if (char === "{") {
|
|
892
|
+
if (depth === 0) {
|
|
893
|
+
start = i;
|
|
894
|
+
}
|
|
895
|
+
depth++;
|
|
896
|
+
} else if (char === "}") {
|
|
897
|
+
depth--;
|
|
898
|
+
if (depth === 0 && start !== -1) {
|
|
899
|
+
const candidate = text3.slice(start, i + 1);
|
|
900
|
+
if (candidate.includes('"summary"') && candidate.includes('"type"')) {
|
|
901
|
+
candidates.push(candidate);
|
|
902
|
+
}
|
|
903
|
+
start = -1;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
candidates.sort((a, b) => b.length - a.length);
|
|
908
|
+
for (const jsonStr of candidates) {
|
|
909
|
+
try {
|
|
910
|
+
const parsed = JSON.parse(jsonStr);
|
|
911
|
+
if (parsed.summary && (parsed.summary.type || parsed.summary.language)) {
|
|
912
|
+
callbacks.onUnderstanding?.(jsonStr);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
} catch {
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
745
919
|
// Simple non-agentic mode for short prompts (adversarial validation)
|
|
746
920
|
async runSimpleClaude(prompt, cwd) {
|
|
747
921
|
const claudeCommand = getProviderCommand("claude-code");
|
|
@@ -1734,35 +1908,44 @@ async function initCommand(options) {
|
|
|
1734
1908
|
process.exit(1);
|
|
1735
1909
|
}
|
|
1736
1910
|
p3.intro(chalk.red("whiterose") + chalk.dim(" - initialization"));
|
|
1737
|
-
const providerSpinner = p3.spinner();
|
|
1738
|
-
providerSpinner.start("Detecting available LLM providers...");
|
|
1739
|
-
const availableProviders = await detectProvider();
|
|
1740
|
-
if (availableProviders.length === 0) {
|
|
1741
|
-
providerSpinner.stop("No LLM providers detected");
|
|
1742
|
-
p3.log.error("whiterose requires an LLM provider to function.");
|
|
1743
|
-
p3.log.info("Supported providers: claude-code, aider, codex, opencode");
|
|
1744
|
-
p3.log.info("Install one and ensure it's configured, then run init again.");
|
|
1745
|
-
process.exit(1);
|
|
1746
|
-
}
|
|
1747
|
-
providerSpinner.stop(`Detected providers: ${availableProviders.join(", ")}`);
|
|
1748
1911
|
let selectedProvider;
|
|
1749
|
-
if (options.
|
|
1750
|
-
selectedProvider =
|
|
1751
|
-
p3.log.info(`Using ${selectedProvider} as your LLM provider.`);
|
|
1912
|
+
if (options.skipProviderDetection && options.provider) {
|
|
1913
|
+
selectedProvider = options.provider;
|
|
1752
1914
|
} else {
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1915
|
+
const providerSpinner = p3.spinner();
|
|
1916
|
+
providerSpinner.start("Detecting available LLM providers...");
|
|
1917
|
+
const availableProviders = await detectProvider();
|
|
1918
|
+
if (availableProviders.length === 0) {
|
|
1919
|
+
providerSpinner.stop("No LLM providers detected");
|
|
1920
|
+
p3.log.error("whiterose requires an LLM provider to function.");
|
|
1921
|
+
p3.log.info("Supported providers: claude-code, aider, codex, opencode");
|
|
1922
|
+
p3.log.info("Install one and ensure it's configured, then run init again.");
|
|
1923
|
+
process.exit(1);
|
|
1924
|
+
}
|
|
1925
|
+
providerSpinner.stop(`Detected providers: ${availableProviders.join(", ")}`);
|
|
1926
|
+
const passedProvider = options.provider;
|
|
1927
|
+
const isPassedProviderAvailable = availableProviders.includes(passedProvider);
|
|
1928
|
+
if (isPassedProviderAvailable) {
|
|
1929
|
+
selectedProvider = passedProvider;
|
|
1930
|
+
p3.log.info(`Using ${selectedProvider} as your LLM provider.`);
|
|
1931
|
+
} else if (options.skipQuestions) {
|
|
1932
|
+
selectedProvider = availableProviders[0];
|
|
1933
|
+
p3.log.info(`Using ${selectedProvider} as your LLM provider.`);
|
|
1934
|
+
} else {
|
|
1935
|
+
const providerChoice = await p3.select({
|
|
1936
|
+
message: "Which LLM provider should whiterose use?",
|
|
1937
|
+
options: availableProviders.map((prov) => ({
|
|
1938
|
+
value: prov,
|
|
1939
|
+
label: prov,
|
|
1940
|
+
hint: prov === "claude-code" ? "recommended" : void 0
|
|
1941
|
+
}))
|
|
1942
|
+
});
|
|
1943
|
+
if (p3.isCancel(providerChoice)) {
|
|
1944
|
+
p3.cancel("Initialization cancelled.");
|
|
1945
|
+
process.exit(0);
|
|
1946
|
+
}
|
|
1947
|
+
selectedProvider = providerChoice;
|
|
1764
1948
|
}
|
|
1765
|
-
selectedProvider = providerChoice;
|
|
1766
1949
|
}
|
|
1767
1950
|
const verifySpinner = p3.spinner();
|
|
1768
1951
|
verifySpinner.start("Verifying provider CLI works...");
|
|
@@ -1820,10 +2003,6 @@ async function initCommand(options) {
|
|
|
1820
2003
|
let understanding;
|
|
1821
2004
|
try {
|
|
1822
2005
|
const provider = await getProvider(selectedProvider);
|
|
1823
|
-
if (options.unsafe && "setUnsafeMode" in provider) {
|
|
1824
|
-
provider.setUnsafeMode(true);
|
|
1825
|
-
p3.log.warn("Running in unsafe mode (--unsafe). LLM permission prompts are bypassed.");
|
|
1826
|
-
}
|
|
1827
2006
|
if ("setProgressCallback" in provider) {
|
|
1828
2007
|
provider.setProgressCallback((message) => {
|
|
1829
2008
|
understandingSpinner.message(message);
|
|
@@ -2330,10 +2509,10 @@ async function scanCommand(paths, options) {
|
|
|
2330
2509
|
if (options.full || paths.length > 0) {
|
|
2331
2510
|
scanType = "full";
|
|
2332
2511
|
if (!isQuiet) {
|
|
2333
|
-
const
|
|
2334
|
-
|
|
2512
|
+
const spinner6 = p3.spinner();
|
|
2513
|
+
spinner6.start("Scanning files...");
|
|
2335
2514
|
filesToScan = paths.length > 0 ? paths : await scanCodebase(cwd, config);
|
|
2336
|
-
|
|
2515
|
+
spinner6.stop(`Found ${filesToScan.length} files to scan`);
|
|
2337
2516
|
} else {
|
|
2338
2517
|
filesToScan = paths.length > 0 ? paths : await scanCodebase(cwd, config);
|
|
2339
2518
|
}
|
|
@@ -2364,14 +2543,6 @@ async function scanCommand(paths, options) {
|
|
|
2364
2543
|
}
|
|
2365
2544
|
const providerName = options.provider || config.provider;
|
|
2366
2545
|
const provider = await getProvider(providerName);
|
|
2367
|
-
if (options.unsafe) {
|
|
2368
|
-
if ("setUnsafeMode" in provider) {
|
|
2369
|
-
provider.setUnsafeMode(true);
|
|
2370
|
-
if (!isQuiet) {
|
|
2371
|
-
p3.log.warn("Running in unsafe mode (--unsafe). LLM permission prompts are bypassed.");
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
2546
|
let bugs;
|
|
2376
2547
|
if (!isQuiet) {
|
|
2377
2548
|
const llmSpinner = p3.spinner();
|
|
@@ -2408,47 +2579,6 @@ async function scanCommand(paths, options) {
|
|
|
2408
2579
|
staticAnalysisResults: staticResults
|
|
2409
2580
|
});
|
|
2410
2581
|
}
|
|
2411
|
-
if (options.adversarial && bugs.length > 0) {
|
|
2412
|
-
if (!isQuiet) {
|
|
2413
|
-
const advSpinner = p3.spinner();
|
|
2414
|
-
advSpinner.start("Running adversarial validation...");
|
|
2415
|
-
const validatedBugs = [];
|
|
2416
|
-
for (const bug of bugs) {
|
|
2417
|
-
const result2 = await provider.adversarialValidate(bug, {
|
|
2418
|
-
files: filesToScan,
|
|
2419
|
-
understanding,
|
|
2420
|
-
config,
|
|
2421
|
-
staticAnalysisResults: staticResults
|
|
2422
|
-
});
|
|
2423
|
-
if (result2.survived) {
|
|
2424
|
-
validatedBugs.push({
|
|
2425
|
-
...bug,
|
|
2426
|
-
confidence: result2.adjustedConfidence || bug.confidence
|
|
2427
|
-
});
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
const filtered = bugs.length - validatedBugs.length;
|
|
2431
|
-
advSpinner.stop(`Adversarial validation: ${filtered} false positives filtered`);
|
|
2432
|
-
bugs = validatedBugs;
|
|
2433
|
-
} else {
|
|
2434
|
-
const validatedBugs = [];
|
|
2435
|
-
for (const bug of bugs) {
|
|
2436
|
-
const result2 = await provider.adversarialValidate(bug, {
|
|
2437
|
-
files: filesToScan,
|
|
2438
|
-
understanding,
|
|
2439
|
-
config,
|
|
2440
|
-
staticAnalysisResults: staticResults
|
|
2441
|
-
});
|
|
2442
|
-
if (result2.survived) {
|
|
2443
|
-
validatedBugs.push({
|
|
2444
|
-
...bug,
|
|
2445
|
-
confidence: result2.adjustedConfidence || bug.confidence
|
|
2446
|
-
});
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
bugs = validatedBugs;
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
2582
|
const minConfidence = options.minConfidence;
|
|
2453
2583
|
const confidenceOrder = { high: 3, medium: 2, low: 1 };
|
|
2454
2584
|
bugs = bugs.filter((bug) => confidenceOrder[bug.confidence.overall] >= confidenceOrder[minConfidence]);
|
|
@@ -2481,14 +2611,23 @@ async function scanCommand(paths, options) {
|
|
|
2481
2611
|
} else if (options.sarif) {
|
|
2482
2612
|
console.log(JSON.stringify(outputSarif(result), null, 2));
|
|
2483
2613
|
} else {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2614
|
+
const outputDir = join(cwd, "whiterose-output");
|
|
2615
|
+
const reportsDir = join(whiterosePath, "reports");
|
|
2616
|
+
const { mkdirSync: mkdirSync2 } = await import('fs');
|
|
2617
|
+
try {
|
|
2618
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
2619
|
+
mkdirSync2(reportsDir, { recursive: true });
|
|
2620
|
+
} catch {
|
|
2491
2621
|
}
|
|
2622
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2623
|
+
const markdown = outputMarkdown(result);
|
|
2624
|
+
const mdPath = join(outputDir, "bugs.md");
|
|
2625
|
+
writeFileSync(mdPath, markdown);
|
|
2626
|
+
const sarifPath = join(outputDir, "bugs.sarif");
|
|
2627
|
+
writeFileSync(sarifPath, JSON.stringify(outputSarif(result), null, 2));
|
|
2628
|
+
const jsonPath = join(outputDir, "bugs.json");
|
|
2629
|
+
writeFileSync(jsonPath, JSON.stringify(result, null, 2));
|
|
2630
|
+
writeFileSync(join(reportsDir, `${timestamp}.sarif`), JSON.stringify(outputSarif(result), null, 2));
|
|
2492
2631
|
console.log();
|
|
2493
2632
|
p3.log.message(chalk.bold("Scan Results"));
|
|
2494
2633
|
console.log();
|
|
@@ -2499,6 +2638,11 @@ async function scanCommand(paths, options) {
|
|
|
2499
2638
|
console.log();
|
|
2500
2639
|
console.log(` ${chalk.bold("Total:")} ${result.summary.total} bugs found`);
|
|
2501
2640
|
console.log();
|
|
2641
|
+
p3.log.success("Reports saved:");
|
|
2642
|
+
console.log(` ${chalk.dim("\u251C")} ${chalk.cyan(mdPath)}`);
|
|
2643
|
+
console.log(` ${chalk.dim("\u251C")} ${chalk.cyan(sarifPath)}`);
|
|
2644
|
+
console.log(` ${chalk.dim("\u2514")} ${chalk.cyan(jsonPath)}`);
|
|
2645
|
+
console.log();
|
|
2502
2646
|
if (result.summary.total > 0) {
|
|
2503
2647
|
p3.log.info(`Run ${chalk.cyan("whiterose fix")} to fix bugs interactively.`);
|
|
2504
2648
|
}
|
|
@@ -3360,12 +3504,12 @@ function generateDiff(original, fixed, filename) {
|
|
|
3360
3504
|
return diff.join("\n");
|
|
3361
3505
|
}
|
|
3362
3506
|
async function startFixTUI(bugs, config, options) {
|
|
3363
|
-
return new Promise((
|
|
3507
|
+
return new Promise((resolve5) => {
|
|
3364
3508
|
const handleFix = async (bug) => {
|
|
3365
3509
|
await applyFix(bug, config, options);
|
|
3366
3510
|
};
|
|
3367
3511
|
const handleExit = () => {
|
|
3368
|
-
|
|
3512
|
+
resolve5();
|
|
3369
3513
|
};
|
|
3370
3514
|
const { unmount, waitUntilExit } = render(
|
|
3371
3515
|
/* @__PURE__ */ jsx(
|
|
@@ -3380,7 +3524,7 @@ async function startFixTUI(bugs, config, options) {
|
|
|
3380
3524
|
)
|
|
3381
3525
|
);
|
|
3382
3526
|
waitUntilExit().then(() => {
|
|
3383
|
-
|
|
3527
|
+
resolve5();
|
|
3384
3528
|
});
|
|
3385
3529
|
});
|
|
3386
3530
|
}
|
|
@@ -3702,21 +3846,21 @@ async function fixSingleBug(bug, config, options) {
|
|
|
3702
3846
|
console.log();
|
|
3703
3847
|
}
|
|
3704
3848
|
if (!options.dryRun) {
|
|
3705
|
-
const
|
|
3849
|
+
const confirm4 = await p3.confirm({
|
|
3706
3850
|
message: "Apply this fix?",
|
|
3707
3851
|
initialValue: true
|
|
3708
3852
|
});
|
|
3709
|
-
if (p3.isCancel(
|
|
3853
|
+
if (p3.isCancel(confirm4) || !confirm4) {
|
|
3710
3854
|
p3.cancel("Fix cancelled.");
|
|
3711
3855
|
process.exit(0);
|
|
3712
3856
|
}
|
|
3713
3857
|
}
|
|
3714
|
-
const
|
|
3715
|
-
|
|
3858
|
+
const spinner6 = p3.spinner();
|
|
3859
|
+
spinner6.start(options.dryRun ? "Generating fix preview..." : "Applying fix...");
|
|
3716
3860
|
try {
|
|
3717
3861
|
const result = await applyFix(bug, config, options);
|
|
3718
3862
|
if (result.success) {
|
|
3719
|
-
|
|
3863
|
+
spinner6.stop(options.dryRun ? "Fix preview generated" : "Fix applied");
|
|
3720
3864
|
if (result.diff) {
|
|
3721
3865
|
console.log();
|
|
3722
3866
|
console.log(chalk.dim(" Changes:"));
|
|
@@ -3736,12 +3880,12 @@ async function fixSingleBug(bug, config, options) {
|
|
|
3736
3880
|
}
|
|
3737
3881
|
p3.outro(chalk.green("Fix complete!"));
|
|
3738
3882
|
} else {
|
|
3739
|
-
|
|
3883
|
+
spinner6.stop("Fix failed");
|
|
3740
3884
|
p3.log.error(result.error || "Unknown error");
|
|
3741
3885
|
process.exit(1);
|
|
3742
3886
|
}
|
|
3743
3887
|
} catch (error) {
|
|
3744
|
-
|
|
3888
|
+
spinner6.stop("Fix failed");
|
|
3745
3889
|
p3.log.error(error.message);
|
|
3746
3890
|
process.exit(1);
|
|
3747
3891
|
}
|
|
@@ -3922,6 +4066,160 @@ async function reportCommand(options) {
|
|
|
3922
4066
|
p3.log.success(`Report written to ${options.output}`);
|
|
3923
4067
|
}
|
|
3924
4068
|
}
|
|
4069
|
+
function getPathCompletions(partial) {
|
|
4070
|
+
try {
|
|
4071
|
+
if (!partial) partial = ".";
|
|
4072
|
+
const resolved = resolve(partial);
|
|
4073
|
+
let dir;
|
|
4074
|
+
let prefix;
|
|
4075
|
+
if (partial.endsWith("/") || existsSync(resolved) && statSync(resolved).isDirectory()) {
|
|
4076
|
+
dir = resolved;
|
|
4077
|
+
prefix = "";
|
|
4078
|
+
} else {
|
|
4079
|
+
dir = dirname(resolved);
|
|
4080
|
+
prefix = basename(partial);
|
|
4081
|
+
}
|
|
4082
|
+
if (!existsSync(dir)) {
|
|
4083
|
+
return [];
|
|
4084
|
+
}
|
|
4085
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
4086
|
+
const matches = entries.filter((entry) => entry.isDirectory()).filter((entry) => entry.name.toLowerCase().startsWith(prefix.toLowerCase())).filter((entry) => !entry.name.startsWith(".") || prefix.startsWith(".")).map((entry) => {
|
|
4087
|
+
if (partial.endsWith("/") || prefix === "") {
|
|
4088
|
+
return partial + entry.name + "/";
|
|
4089
|
+
}
|
|
4090
|
+
const base = partial.slice(0, partial.length - prefix.length);
|
|
4091
|
+
return base + entry.name + "/";
|
|
4092
|
+
}).sort();
|
|
4093
|
+
return matches;
|
|
4094
|
+
} catch {
|
|
4095
|
+
return [];
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
function longestCommonPrefix(strings) {
|
|
4099
|
+
if (strings.length === 0) return "";
|
|
4100
|
+
if (strings.length === 1) return strings[0];
|
|
4101
|
+
let prefix = strings[0];
|
|
4102
|
+
for (let i = 1; i < strings.length; i++) {
|
|
4103
|
+
while (!strings[i].startsWith(prefix)) {
|
|
4104
|
+
prefix = prefix.slice(0, -1);
|
|
4105
|
+
if (prefix === "") return "";
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
return prefix;
|
|
4109
|
+
}
|
|
4110
|
+
function pathInput(options) {
|
|
4111
|
+
return new Promise((resolvePromise) => {
|
|
4112
|
+
const { message, defaultValue = "", validate } = options;
|
|
4113
|
+
const promptText = chalk.cyan("\u25C6") + " " + chalk.bold(message);
|
|
4114
|
+
const hint = defaultValue ? chalk.dim(` (default: ${defaultValue})`) : "";
|
|
4115
|
+
const tabHint = chalk.dim(" [Tab to autocomplete]");
|
|
4116
|
+
process.stdout.write(promptText + hint + tabHint + "\n");
|
|
4117
|
+
process.stdout.write(chalk.cyan("\u2502") + " ");
|
|
4118
|
+
let input = "";
|
|
4119
|
+
let cursorPos = 0;
|
|
4120
|
+
if (process.stdin.isTTY) {
|
|
4121
|
+
process.stdin.setRawMode(true);
|
|
4122
|
+
}
|
|
4123
|
+
process.stdin.resume();
|
|
4124
|
+
process.stdin.setEncoding("utf8");
|
|
4125
|
+
const redrawLine = () => {
|
|
4126
|
+
readline.clearLine(process.stdout, 0);
|
|
4127
|
+
readline.cursorTo(process.stdout, 0);
|
|
4128
|
+
process.stdout.write(chalk.cyan("\u2502") + " " + input);
|
|
4129
|
+
readline.cursorTo(process.stdout, 2 + cursorPos);
|
|
4130
|
+
};
|
|
4131
|
+
const cleanup = () => {
|
|
4132
|
+
if (process.stdin.isTTY) {
|
|
4133
|
+
process.stdin.setRawMode(false);
|
|
4134
|
+
}
|
|
4135
|
+
process.stdin.pause();
|
|
4136
|
+
process.stdin.removeAllListeners("data");
|
|
4137
|
+
};
|
|
4138
|
+
const finish = (result) => {
|
|
4139
|
+
cleanup();
|
|
4140
|
+
console.log();
|
|
4141
|
+
resolvePromise(result);
|
|
4142
|
+
};
|
|
4143
|
+
process.stdin.on("data", (key) => {
|
|
4144
|
+
const code = key.charCodeAt(0);
|
|
4145
|
+
if (code === 3) {
|
|
4146
|
+
console.log();
|
|
4147
|
+
finish(null);
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
if (code === 13) {
|
|
4151
|
+
const finalPath = input.trim() || defaultValue;
|
|
4152
|
+
if (validate) {
|
|
4153
|
+
const error = validate(finalPath);
|
|
4154
|
+
if (error) {
|
|
4155
|
+
console.log();
|
|
4156
|
+
process.stdout.write(chalk.red("\u2502") + " " + chalk.red(error) + "\n");
|
|
4157
|
+
process.stdout.write(chalk.cyan("\u2502") + " " + input);
|
|
4158
|
+
return;
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
finish(finalPath);
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
if (code === 9) {
|
|
4165
|
+
const completions = getPathCompletions(input || ".");
|
|
4166
|
+
if (completions.length === 1) {
|
|
4167
|
+
input = completions[0];
|
|
4168
|
+
cursorPos = input.length;
|
|
4169
|
+
redrawLine();
|
|
4170
|
+
} else if (completions.length > 1) {
|
|
4171
|
+
const common = longestCommonPrefix(completions);
|
|
4172
|
+
if (common.length > input.length) {
|
|
4173
|
+
input = common;
|
|
4174
|
+
cursorPos = input.length;
|
|
4175
|
+
redrawLine();
|
|
4176
|
+
} else {
|
|
4177
|
+
console.log();
|
|
4178
|
+
const maxShow = 10;
|
|
4179
|
+
const shown = completions.slice(0, maxShow);
|
|
4180
|
+
process.stdout.write(chalk.dim("\u2502 ") + shown.map((c) => chalk.cyan(basename(c.slice(0, -1)))).join(" "));
|
|
4181
|
+
if (completions.length > maxShow) {
|
|
4182
|
+
process.stdout.write(chalk.dim(` ... +${completions.length - maxShow} more`));
|
|
4183
|
+
}
|
|
4184
|
+
console.log();
|
|
4185
|
+
process.stdout.write(chalk.cyan("\u2502") + " " + input);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
if (code === 127 || code === 8) {
|
|
4191
|
+
if (cursorPos > 0) {
|
|
4192
|
+
input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
|
|
4193
|
+
cursorPos--;
|
|
4194
|
+
redrawLine();
|
|
4195
|
+
}
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
if (key === "\x1B[D") {
|
|
4199
|
+
if (cursorPos > 0) {
|
|
4200
|
+
cursorPos--;
|
|
4201
|
+
redrawLine();
|
|
4202
|
+
}
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
if (key === "\x1B[C") {
|
|
4206
|
+
if (cursorPos < input.length) {
|
|
4207
|
+
cursorPos++;
|
|
4208
|
+
redrawLine();
|
|
4209
|
+
}
|
|
4210
|
+
return;
|
|
4211
|
+
}
|
|
4212
|
+
if (key.startsWith("\x1B")) {
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
if (code >= 32 && code < 127) {
|
|
4216
|
+
input = input.slice(0, cursorPos) + key + input.slice(cursorPos);
|
|
4217
|
+
cursorPos++;
|
|
4218
|
+
redrawLine();
|
|
4219
|
+
}
|
|
4220
|
+
});
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
3925
4223
|
|
|
3926
4224
|
// src/cli/index.ts
|
|
3927
4225
|
var BANNER = `
|
|
@@ -3935,94 +4233,132 @@ ${chalk.red(" \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255
|
|
|
3935
4233
|
${chalk.dim(` "I've been staring at your code for a long time."`)}
|
|
3936
4234
|
`;
|
|
3937
4235
|
var program = new Command();
|
|
3938
|
-
program.name("whiterose").description("AI-powered bug hunter that uses your existing LLM subscription").version("0.
|
|
4236
|
+
program.name("whiterose").description("AI-powered bug hunter that uses your existing LLM subscription").version("0.2.2").hook("preAction", () => {
|
|
3939
4237
|
const args = process.argv.slice(2);
|
|
3940
4238
|
if (!args.includes("--help") && !args.includes("-h") && args.length > 0) {
|
|
3941
4239
|
console.log(BANNER);
|
|
3942
4240
|
}
|
|
3943
4241
|
});
|
|
3944
|
-
program.command("init").description("Initialize whiterose for this project (scans codebase, asks questions, generates config)").option("-p, --provider <provider>", "LLM provider to use", "claude-code").option("--skip-questions", "Skip interactive questions, use defaults").option("--force", "Overwrite existing .whiterose directory").
|
|
3945
|
-
program.command("scan [paths...]").description("Scan for bugs in the codebase").option("-f, --full", "Force full scan (ignore cache)").option("--json", "Output as JSON only").option("--sarif", "Output as SARIF only").option("-p, --provider <provider>", "Override LLM provider").option("-c, --category <categories...>", "Filter by bug categories").option("--min-confidence <level>", "Minimum confidence level to report", "low").
|
|
3946
|
-
program.command("fix [bugId]").description("Fix bugs interactively or by ID").option("--dry-run", "Show proposed fixes without applying").option("--branch <name>", "Create fixes in a new branch").option("--sarif <path>", "Load bugs from an external SARIF file").option("--github <url>", "Load bug from a GitHub issue URL").option("--describe", "Manually describe a bug to fix").action(fixCommand);
|
|
4242
|
+
program.command("init").description("Initialize whiterose for this project (scans codebase, asks questions, generates config)").option("-p, --provider <provider>", "LLM provider to use", "claude-code").option("--skip-questions", "Skip interactive questions, use defaults").option("--force", "Overwrite existing .whiterose directory").action(initCommand);
|
|
4243
|
+
program.command("scan [paths...]").description("Scan for bugs in the codebase (includes inline validation)").option("-f, --full", "Force full scan (ignore cache)").option("--json", "Output as JSON only").option("--sarif", "Output as SARIF only").option("-p, --provider <provider>", "Override LLM provider").option("-c, --category <categories...>", "Filter by bug categories").option("--min-confidence <level>", "Minimum confidence level to report", "low").action(scanCommand);
|
|
4244
|
+
program.command("fix [bugId]").description("Fix bugs interactively or by ID").option("--dry-run", "Show proposed fixes without applying").option("--branch <name>", "Create fixes in a new branch").option("--sarif <path>", "Load bugs from an external SARIF file").option("--github <url>", "Load bug from a GitHub issue URL").option("--describe", "Manually describe a bug to fix").option("--unsafe", "Skip all permission prompts (full trust mode)").action(fixCommand);
|
|
3947
4245
|
program.command("refresh").description("Rebuild codebase understanding from scratch").option("--keep-config", "Keep existing config, only regenerate understanding").action(refreshCommand);
|
|
3948
4246
|
program.command("status").description("Show whiterose status (cache, last scan, provider)").action(statusCommand);
|
|
3949
4247
|
program.command("report").description("Generate BUGS.md from last scan").option("-o, --output <path>", "Output path", "BUGS.md").option("--format <format>", "Output format (markdown, sarif, json)", "markdown").action(reportCommand);
|
|
3950
|
-
async function
|
|
4248
|
+
async function showInteractiveWizard() {
|
|
3951
4249
|
console.log(BANNER);
|
|
4250
|
+
p3.intro(chalk.red("whiterose") + chalk.dim(" - AI Bug Hunter"));
|
|
3952
4251
|
const cwd = process.cwd();
|
|
3953
|
-
const
|
|
4252
|
+
const defaultPath = cwd;
|
|
4253
|
+
const repoPath = await pathInput({
|
|
4254
|
+
message: "Repository path",
|
|
4255
|
+
defaultValue: defaultPath,
|
|
4256
|
+
validate: (value) => {
|
|
4257
|
+
const path = value || defaultPath;
|
|
4258
|
+
if (!existsSync(path)) {
|
|
4259
|
+
return "Directory does not exist";
|
|
4260
|
+
}
|
|
4261
|
+
return void 0;
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
if (repoPath === null) {
|
|
4265
|
+
p3.cancel("Cancelled.");
|
|
4266
|
+
process.exit(0);
|
|
4267
|
+
}
|
|
4268
|
+
const targetPath = resolve(repoPath || defaultPath);
|
|
4269
|
+
const projectName = basename(targetPath);
|
|
4270
|
+
const whiterosePath = join(targetPath, ".whiterose");
|
|
3954
4271
|
const isInitialized = existsSync(whiterosePath);
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
4272
|
+
const detectSpinner = p3.spinner();
|
|
4273
|
+
detectSpinner.start("Detecting LLM providers...");
|
|
4274
|
+
const availableProviders = await detectProvider();
|
|
4275
|
+
detectSpinner.stop(`Found ${availableProviders.length} provider(s)`);
|
|
4276
|
+
if (availableProviders.length === 0) {
|
|
4277
|
+
p3.log.error("No LLM providers detected on your system.");
|
|
3958
4278
|
console.log();
|
|
3959
|
-
|
|
3960
|
-
console.log(chalk.dim(
|
|
3961
|
-
console.log(chalk.dim(
|
|
4279
|
+
console.log(chalk.dim(" Supported providers:"));
|
|
4280
|
+
console.log(chalk.dim(" - claude-code: ") + chalk.cyan("npm install -g @anthropic-ai/claude-code"));
|
|
4281
|
+
console.log(chalk.dim(" - aider: ") + chalk.cyan("pip install aider-chat"));
|
|
3962
4282
|
console.log();
|
|
4283
|
+
p3.outro(chalk.red("Install a provider and try again."));
|
|
4284
|
+
process.exit(1);
|
|
4285
|
+
}
|
|
4286
|
+
let selectedProvider;
|
|
4287
|
+
if (availableProviders.length === 1) {
|
|
4288
|
+
selectedProvider = availableProviders[0];
|
|
4289
|
+
p3.log.info(`Using ${chalk.cyan(selectedProvider)} (only available provider)`);
|
|
4290
|
+
} else {
|
|
4291
|
+
const providerChoice = await p3.select({
|
|
4292
|
+
message: "Select LLM provider",
|
|
4293
|
+
options: availableProviders.map((provider) => ({
|
|
4294
|
+
value: provider,
|
|
4295
|
+
label: provider,
|
|
4296
|
+
hint: provider === "claude-code" ? "recommended" : void 0
|
|
4297
|
+
}))
|
|
4298
|
+
});
|
|
4299
|
+
if (p3.isCancel(providerChoice)) {
|
|
4300
|
+
p3.cancel("Cancelled.");
|
|
4301
|
+
process.exit(0);
|
|
4302
|
+
}
|
|
4303
|
+
selectedProvider = providerChoice;
|
|
3963
4304
|
}
|
|
3964
|
-
const menuOptions = [];
|
|
3965
4305
|
if (!isInitialized) {
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
4306
|
+
p3.log.warn(`Project "${projectName}" is not initialized.`);
|
|
4307
|
+
const shouldInit = await p3.confirm({
|
|
4308
|
+
message: "Initialize whiterose for this project?",
|
|
4309
|
+
initialValue: true
|
|
3970
4310
|
});
|
|
4311
|
+
if (p3.isCancel(shouldInit) || !shouldInit) {
|
|
4312
|
+
p3.cancel("Cannot scan without initialization.");
|
|
4313
|
+
process.exit(0);
|
|
4314
|
+
}
|
|
4315
|
+
process.chdir(targetPath);
|
|
4316
|
+
await initCommand({
|
|
4317
|
+
provider: selectedProvider,
|
|
4318
|
+
skipQuestions: false,
|
|
4319
|
+
force: false,
|
|
4320
|
+
// Read-only operations auto-approve, unsafe only matters for fix
|
|
4321
|
+
skipProviderDetection: true
|
|
4322
|
+
// Already verified in wizard
|
|
4323
|
+
});
|
|
4324
|
+
console.log();
|
|
3971
4325
|
} else {
|
|
3972
|
-
|
|
3973
|
-
{ value: "scan", label: "Scan", hint: "find bugs in the codebase" },
|
|
3974
|
-
{ value: "fix", label: "Fix", hint: "fix bugs interactively" },
|
|
3975
|
-
{ value: "status", label: "Status", hint: "show current status" },
|
|
3976
|
-
{ value: "report", label: "Report", hint: "generate bug report" },
|
|
3977
|
-
{ value: "refresh", label: "Refresh", hint: "rebuild codebase understanding" }
|
|
3978
|
-
);
|
|
4326
|
+
process.chdir(targetPath);
|
|
3979
4327
|
}
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
4328
|
+
const scanDepth = await p3.select({
|
|
4329
|
+
message: "Scan depth",
|
|
4330
|
+
options: [
|
|
4331
|
+
{
|
|
4332
|
+
value: "quick",
|
|
4333
|
+
label: "Quick scan",
|
|
4334
|
+
hint: "faster, incremental changes only"
|
|
4335
|
+
},
|
|
4336
|
+
{
|
|
4337
|
+
value: "deep",
|
|
4338
|
+
label: "Deep scan",
|
|
4339
|
+
hint: "thorough, full codebase analysis (recommended)"
|
|
4340
|
+
}
|
|
4341
|
+
],
|
|
4342
|
+
initialValue: "deep"
|
|
3985
4343
|
});
|
|
3986
|
-
if (p3.isCancel(
|
|
3987
|
-
p3.
|
|
4344
|
+
if (p3.isCancel(scanDepth)) {
|
|
4345
|
+
p3.cancel("Cancelled.");
|
|
3988
4346
|
process.exit(0);
|
|
3989
4347
|
}
|
|
3990
4348
|
console.log();
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
category: void 0,
|
|
4002
|
-
minConfidence: "low",
|
|
4003
|
-
adversarial: true,
|
|
4004
|
-
unsafe: false
|
|
4005
|
-
});
|
|
4006
|
-
break;
|
|
4007
|
-
case "fix":
|
|
4008
|
-
await fixCommand(void 0, { dryRun: false });
|
|
4009
|
-
break;
|
|
4010
|
-
case "status":
|
|
4011
|
-
await statusCommand();
|
|
4012
|
-
break;
|
|
4013
|
-
case "report":
|
|
4014
|
-
await reportCommand({ output: "BUGS.md", format: "markdown" });
|
|
4015
|
-
break;
|
|
4016
|
-
case "refresh":
|
|
4017
|
-
await refreshCommand();
|
|
4018
|
-
break;
|
|
4019
|
-
case "help":
|
|
4020
|
-
program.help();
|
|
4021
|
-
break;
|
|
4022
|
-
}
|
|
4349
|
+
p3.log.step("Starting scan...");
|
|
4350
|
+
console.log();
|
|
4351
|
+
await scanCommand([], {
|
|
4352
|
+
full: scanDepth === "deep",
|
|
4353
|
+
json: false,
|
|
4354
|
+
sarif: false,
|
|
4355
|
+
provider: selectedProvider,
|
|
4356
|
+
category: void 0,
|
|
4357
|
+
minConfidence: "low"
|
|
4358
|
+
});
|
|
4023
4359
|
}
|
|
4024
4360
|
if (process.argv.length === 2) {
|
|
4025
|
-
|
|
4361
|
+
showInteractiveWizard().catch((error) => {
|
|
4026
4362
|
console.error(chalk.red("Error:"), error.message);
|
|
4027
4363
|
process.exit(1);
|
|
4028
4364
|
});
|