pika-review 2.0.1 → 2.1.0
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/README.md +54 -33
- package/dist/cmd/scan.js +19 -17
- package/dist/cmd/stats.js +55 -0
- package/dist/cmd/view.js +55 -0
- package/dist/core/ai.js +27 -8
- package/dist/core/reporter.js +443 -6
- package/dist/core/scanner.js +38 -10
- package/dist/index.js +25 -5
- package/dist/utils/config.js +41 -10
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
# Pika Review 🦊
|
|
2
|
-
### Enterprise-grade AI Architectural Sentinel
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
### Enterprise-grade AI Architectural Sentinel & Compliance Engine
|
|
4
|
+
|
|
5
|
+
Pika Review is a high-performance CLI tool designed to perform surgical code reviews using AI. It focuses on deep architectural debt, security vulnerabilities, and project-specific compliance that traditional linters miss.
|
|
5
6
|
|
|
6
7
|
**GitHub**: [HackX-IN/pika-review](https://github.com/HackX-IN/pika-review)
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
## 🚀 Key Features
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
12
|
+
|
|
13
|
+
- **🧠 Architecture Rules Engine**: Enforce project-specific standards via `.pika-rules.md`.
|
|
14
|
+
- **📊 Health Dashboard**: Track architectural health trends over time with `pika-review stats`.
|
|
15
|
+
- **🎨 Premium Interactive Reports**: Immersive, dark-mode HTML reports for deep triage.
|
|
16
|
+
- **🔍 Smart Context Scanning**: Uses `-U10` context windows for higher AI reasoning accuracy.
|
|
17
|
+
- **🛡️ "Pika-Ignore" support**: Suppress specific lines using `// pika-ignore` comments.
|
|
18
|
+
- **⚡ Parallel Orchestration**: Scans multiple files concurrently with `p-limit`.
|
|
19
|
+
- **🌍 Provider Agnostic**: Works with OpenAI, Claude, Grok, or any OpenAI-compatible endpoint.
|
|
16
20
|
|
|
17
21
|
---
|
|
18
22
|
|
|
19
23
|
## 📦 Installation
|
|
24
|
+
|
|
20
25
|
```bash
|
|
21
26
|
# Via Bun (Recommended)
|
|
22
27
|
bun add -g pika-review
|
|
@@ -26,53 +31,69 @@ npm install -g pika-review
|
|
|
26
31
|
```
|
|
27
32
|
|
|
28
33
|
## 🛠️ Setup
|
|
34
|
+
|
|
29
35
|
1. Initialize your configuration:
|
|
30
36
|
```bash
|
|
31
37
|
pika-review init
|
|
32
38
|
```
|
|
33
|
-
2.
|
|
39
|
+
2. Configure your AI provider in `~/.pika-review.yaml`:
|
|
34
40
|
```yaml
|
|
35
41
|
ai:
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
apiKey: "your-api-token"
|
|
43
|
+
model: "gpt-4o" # or your preferred model
|
|
44
|
+
baseURL: "https://api.openai.com/v1"
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
Pika Review works with any OpenAI-compatible API. To use a different provider, simply override the `baseURL` and `model` in your config:
|
|
47
|
+
---
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
ai:
|
|
45
|
-
apiKey: "your-openai-key"
|
|
46
|
-
model: "gpt-4o"
|
|
47
|
-
baseURL: "https://api.openai.com/v1"
|
|
48
|
-
```
|
|
49
|
+
## 🏗️ Enterprise Features
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
Scan staged git changes:
|
|
52
|
-
```bash
|
|
53
|
-
pika-review scan
|
|
54
|
-
```
|
|
51
|
+
### 1. Architecture Rules Engine
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
Create a `.pika-rules.md` file in your repository root to guide the AI with project-specific context:
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
# Architectural Rules
|
|
57
|
+
|
|
58
|
+
- Use Functional Components with Hooks, never Class Components.
|
|
59
|
+
- All service calls must go through `src/api/client.ts`.
|
|
60
|
+
- Database queries are restricted to the Repository layer.
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
### 2. Pika-Ignore
|
|
64
|
+
|
|
65
|
+
Suppress false positives or acknowledged risks directly in your code:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const secret = "12345"; // pika-ignore (intentional for testing)
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
###
|
|
67
|
-
|
|
71
|
+
### 3. Health Stats
|
|
72
|
+
|
|
73
|
+
Monitor your codebase's architectural debt over time:
|
|
74
|
+
|
|
68
75
|
```bash
|
|
69
|
-
pika-review
|
|
76
|
+
pika-review stats
|
|
70
77
|
```
|
|
71
78
|
|
|
72
79
|
---
|
|
73
80
|
|
|
81
|
+
## 🔍 Usage
|
|
82
|
+
|
|
83
|
+
| Command | Description |
|
|
84
|
+
| :---------------------- | :------------------------------------ |
|
|
85
|
+
| `pika-review scan` | Scan staged git changes (Default) |
|
|
86
|
+
| `pika-review scan -i` | Interactive file selection mode |
|
|
87
|
+
| `pika-review view` | Open the latest interactive report |
|
|
88
|
+
| `pika-review stats` | View architectural health dashboard |
|
|
89
|
+
| `pika-review scan --ci` | Fail pipeline on Critical/High issues |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
74
93
|
## 🛡️ Privacy & Security
|
|
75
|
-
|
|
94
|
+
|
|
95
|
+
Pika Review processes your local git diffs and transmits them directly to your configured AI provider via SSL. No code is stored or cached by the Pika Review engine.
|
|
76
96
|
|
|
77
97
|
## 📄 License
|
|
98
|
+
|
|
78
99
|
MIT © Pika Review Contributors
|
package/dist/cmd/scan.js
CHANGED
|
@@ -10,23 +10,25 @@ export async function scanAction(files, options) {
|
|
|
10
10
|
const target = options.unstaged ? "unstaged" : "staged";
|
|
11
11
|
const isCI = !!options.ci;
|
|
12
12
|
let targets = files.length > 0 ? files : undefined;
|
|
13
|
-
// Interactive Discovery: If no files provided and no git changes
|
|
14
|
-
if (!targets &&
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
// Interactive Discovery: If requested or if no files provided and no git changes
|
|
14
|
+
if (!isCI && (options.interactive || (!targets && getDiffFiles(target).length === 0))) {
|
|
15
|
+
logger.info("Entering interactive selection mode...");
|
|
16
|
+
const allFiles = listProjectFiles();
|
|
17
|
+
if (allFiles.length > 0) {
|
|
18
|
+
const selection = await prompts({
|
|
19
|
+
type: "multiselect",
|
|
20
|
+
name: "files",
|
|
21
|
+
message: "Select files to analyze (Space to select, Enter to confirm):",
|
|
22
|
+
choices: allFiles.map(f => ({ title: f, value: f })),
|
|
23
|
+
hint: "- Space to select. Return to submit",
|
|
24
|
+
instructions: false
|
|
25
|
+
});
|
|
26
|
+
if (selection.files && selection.files.length > 0) {
|
|
27
|
+
targets = selection.files;
|
|
28
|
+
}
|
|
29
|
+
else if (options.interactive) {
|
|
30
|
+
logger.warn("No files selected. Exiting.");
|
|
31
|
+
return;
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Stats Action: Displays architectural health trends.
|
|
7
|
+
*/
|
|
8
|
+
export async function statsAction() {
|
|
9
|
+
const historyPath = path.join(process.cwd(), ".pika-reports", "history.json");
|
|
10
|
+
if (!fs.existsSync(historyPath)) {
|
|
11
|
+
logger.error("No scan history found. Run some scans first!");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const history = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
16
|
+
if (history.length === 0) {
|
|
17
|
+
logger.error("Scan history is empty.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(`\n${chalk.bgBlue.white.bold(" PIKA ARCHITECTURAL HEALTH DASHBOARD ")}\n`);
|
|
21
|
+
console.log(`${chalk.dim("Tracking trends across the last " + history.length + " scans")}\n`);
|
|
22
|
+
// Table Header
|
|
23
|
+
console.log(chalk.bold("Date Files Issues Critical High Medium Low"));
|
|
24
|
+
console.log(chalk.dim("-".repeat(75)));
|
|
25
|
+
history.slice(-10).forEach((run) => {
|
|
26
|
+
const date = new Date(run.timestamp).toLocaleString().padEnd(20);
|
|
27
|
+
const files = String(run.totalFiles).padEnd(8);
|
|
28
|
+
const issues = String(run.totalIssues).padEnd(9);
|
|
29
|
+
const crit = chalk.red(String(run.severityCounts.Critical || 0).padEnd(11));
|
|
30
|
+
const high = chalk.hex("#fb923c")(String(run.severityCounts.High || 0).padEnd(7));
|
|
31
|
+
const med = chalk.yellow(String(run.severityCounts.Medium || 0).padEnd(9));
|
|
32
|
+
const low = chalk.green(String(run.severityCounts.Low || 0).padEnd(6));
|
|
33
|
+
console.log(`${date}${files}${issues}${crit}${high}${med}${low}`);
|
|
34
|
+
});
|
|
35
|
+
// Summary Insights
|
|
36
|
+
const latest = history[history.length - 1];
|
|
37
|
+
const previous = history.length > 1 ? history[history.length - 2] : null;
|
|
38
|
+
console.log(`\n${chalk.bold("Latest Insight:")}`);
|
|
39
|
+
if (previous) {
|
|
40
|
+
const diff = latest.totalIssues - previous.totalIssues;
|
|
41
|
+
const direction = diff > 0 ? chalk.red("increased") : diff < 0 ? chalk.green("decreased") : "remained stable";
|
|
42
|
+
console.log(`- Issue count ${direction} by ${Math.abs(diff)} since the last scan.`);
|
|
43
|
+
}
|
|
44
|
+
const critTotal = latest.severityCounts.Critical || 0;
|
|
45
|
+
if (critTotal > 0) {
|
|
46
|
+
console.log(`- ${chalk.red("🚨 Critical Action Required:")} ${critTotal} architectural risks detected.`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(`- ${chalk.green("✅ Zero Critical Issues:")} Maintain this standard!`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
logger.error("Failed to parse history data.");
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/cmd/view.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
/**
|
|
7
|
+
* View Action: Finds and opens the latest HTML report.
|
|
8
|
+
*/
|
|
9
|
+
export async function viewAction() {
|
|
10
|
+
const rootReportDir = path.join(process.cwd(), ".pika-reports");
|
|
11
|
+
if (!fs.existsSync(rootReportDir)) {
|
|
12
|
+
logger.error("No reports found. Run a scan first!");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Get project directories (e.g. .pika-reports/pika-review/)
|
|
16
|
+
const projectDirs = fs.readdirSync(rootReportDir, { withFileTypes: true })
|
|
17
|
+
.filter(dirent => dirent.isDirectory())
|
|
18
|
+
.map(dirent => dirent.name);
|
|
19
|
+
if (projectDirs.length === 0) {
|
|
20
|
+
logger.error("No reports found. Run a scan first!");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Find all report.html files across all projects and timestamps
|
|
24
|
+
const allReports = [];
|
|
25
|
+
for (const project of projectDirs) {
|
|
26
|
+
const projectPath = path.join(rootReportDir, project);
|
|
27
|
+
const timestamps = fs.readdirSync(projectPath, { withFileTypes: true })
|
|
28
|
+
.filter(dirent => dirent.isDirectory())
|
|
29
|
+
.map(dirent => dirent.name);
|
|
30
|
+
for (const ts of timestamps) {
|
|
31
|
+
const reportPath = path.join(projectPath, ts, "report.html");
|
|
32
|
+
if (fs.existsSync(reportPath)) {
|
|
33
|
+
allReports.push({
|
|
34
|
+
path: reportPath,
|
|
35
|
+
mtime: fs.statSync(reportPath).mtimeMs
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (allReports.length === 0) {
|
|
41
|
+
logger.error("No HTML reports found. Run a scan first!");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Sort by modification time (latest first)
|
|
45
|
+
allReports.sort((a, b) => b.mtime - a.mtime);
|
|
46
|
+
const latestReport = allReports[0].path;
|
|
47
|
+
logger.info(`Opening latest report: ${chalk.cyan(latestReport)}`);
|
|
48
|
+
try {
|
|
49
|
+
const command = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
50
|
+
execSync(`${command} "${latestReport}"`);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
logger.error("Failed to open the report automatically. Please open it manually.");
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/core/ai.js
CHANGED
|
@@ -2,6 +2,8 @@ import OpenAI from "openai";
|
|
|
2
2
|
import { ReviewSchema } from "../utils/schema.js";
|
|
3
3
|
import { getConfig } from "../utils/config.js";
|
|
4
4
|
import { logger } from "../utils/logger.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
5
7
|
/**
|
|
6
8
|
* Structural Extractor: Isolate the JSON object from AI chatter.
|
|
7
9
|
* MoE (Mixture of Experts) models often add preamble or postscript;
|
|
@@ -49,32 +51,49 @@ export function extractJSON(raw) {
|
|
|
49
51
|
return snippet;
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
|
-
* Deep Analysis Engine: Communicates with
|
|
54
|
+
* Deep Analysis Engine: Communicates with the AI provider.
|
|
53
55
|
* Uses XML-structured prompts for better instruction following in smaller LLMs.
|
|
54
56
|
*/
|
|
55
|
-
export async function analyzeDiff(diff) {
|
|
57
|
+
export async function analyzeDiff(diff, fileName) {
|
|
56
58
|
const config = getConfig();
|
|
57
59
|
if (!config.ai.apiKey || !config.ai.accountId) {
|
|
58
|
-
throw new Error("Missing
|
|
60
|
+
throw new Error("Missing AI provider credentials in ~/.pika-review.yaml");
|
|
59
61
|
}
|
|
60
|
-
//
|
|
62
|
+
// Rate Limit Safety: Truncate input to protect the daily free limit.
|
|
61
63
|
// 30,000 chars is roughly 7,500 tokens, leaving room for a detailed response.
|
|
62
64
|
const MAX_CHARS = 30000;
|
|
63
65
|
const safeDiff = diff.length > MAX_CHARS
|
|
64
|
-
? diff.substring(0, MAX_CHARS) +
|
|
66
|
+
? diff.substring(0, MAX_CHARS) +
|
|
67
|
+
"\n... [Content truncated for token safety]"
|
|
65
68
|
: diff;
|
|
66
|
-
const baseURL =
|
|
69
|
+
const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
|
|
67
70
|
? config.ai.baseURL
|
|
68
71
|
: `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`;
|
|
69
72
|
const openai = new OpenAI({
|
|
70
73
|
apiKey: config.ai.apiKey,
|
|
71
74
|
baseURL,
|
|
72
75
|
});
|
|
76
|
+
// Load custom architecture rules if they exist
|
|
77
|
+
let complianceSection = "";
|
|
78
|
+
try {
|
|
79
|
+
const rulesPath = path.join(process.cwd(), ".pika-rules.md");
|
|
80
|
+
if (fs.existsSync(rulesPath)) {
|
|
81
|
+
complianceSection = `
|
|
82
|
+
<compliance_standards>
|
|
83
|
+
${fs.readFileSync(rulesPath, "utf-8")}
|
|
84
|
+
</compliance_standards>`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) { }
|
|
73
88
|
const defaultPrompt = `
|
|
74
89
|
<role>
|
|
75
90
|
You are an Elite Senior Full-Stack Architect and Security Researcher.
|
|
76
91
|
Your task is to perform a surgical review of the provided code.
|
|
77
|
-
</role
|
|
92
|
+
</role>${complianceSection}
|
|
93
|
+
|
|
94
|
+
<context>
|
|
95
|
+
Reviewing File: ${fileName}
|
|
96
|
+
</context>
|
|
78
97
|
|
|
79
98
|
<task>
|
|
80
99
|
Identify high-impact issues related to:
|
|
@@ -112,7 +131,7 @@ export async function analyzeDiff(diff) {
|
|
|
112
131
|
${safeDiff}
|
|
113
132
|
</code_to_analyze>
|
|
114
133
|
`;
|
|
115
|
-
const finalPrompt =
|
|
134
|
+
const finalPrompt = config.ai.prompt && config.ai.prompt.trim()
|
|
116
135
|
? config.ai.prompt.replace("{{code}}", safeDiff)
|
|
117
136
|
: defaultPrompt;
|
|
118
137
|
try {
|
package/dist/core/reporter.js
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Helper to get a sanitized project name.
|
|
5
|
+
*/
|
|
6
|
+
function getProjectName() {
|
|
7
|
+
try {
|
|
8
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
9
|
+
if (fs.existsSync(pkgPath)) {
|
|
10
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
11
|
+
if (pkg.name)
|
|
12
|
+
return pkg.name.replace(/\//g, "-");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch (e) { }
|
|
16
|
+
return path.basename(process.cwd());
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Helper to generate a safe filename from a path.
|
|
20
|
+
*/
|
|
21
|
+
function getSafeFileName(fileName) {
|
|
22
|
+
return fileName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Report Orchestrator: Handles persistence of AI findings in a structured way.
|
|
26
|
+
* Stores reports in .pika-reports/<project-name>/<timestamp>/
|
|
5
27
|
*/
|
|
6
28
|
export function setupReportDir() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
29
|
+
const rootReportDir = path.join(process.cwd(), ".pika-reports");
|
|
30
|
+
const projectName = getProjectName();
|
|
31
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
32
|
+
const sessionDir = path.join(rootReportDir, projectName, timestamp);
|
|
33
|
+
if (!fs.existsSync(sessionDir)) {
|
|
34
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
35
|
+
}
|
|
10
36
|
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
11
37
|
if (fs.existsSync(gitignorePath)) {
|
|
12
38
|
const gitignore = fs.readFileSync(gitignorePath, "utf-8");
|
|
@@ -14,13 +40,13 @@ export function setupReportDir() {
|
|
|
14
40
|
fs.appendFileSync(gitignorePath, "\n.pika-reports/\n");
|
|
15
41
|
}
|
|
16
42
|
}
|
|
17
|
-
return
|
|
43
|
+
return sessionDir;
|
|
18
44
|
}
|
|
19
45
|
/**
|
|
20
46
|
* Generates a professional Markdown report.
|
|
21
47
|
*/
|
|
22
48
|
export function writeMarkdownReport(fileName, reviews, reportDir) {
|
|
23
|
-
const safeName = fileName
|
|
49
|
+
const safeName = getSafeFileName(fileName);
|
|
24
50
|
const reportPath = path.join(reportDir, `${safeName}_review.md`);
|
|
25
51
|
const timestamp = new Date().toLocaleString();
|
|
26
52
|
let mdContent = `# 🔍 Pika Review: \`${fileName}\`\n\n`;
|
|
@@ -46,3 +72,414 @@ export function writeMarkdownReport(fileName, reviews, reportDir) {
|
|
|
46
72
|
fs.writeFileSync(reportPath, mdContent);
|
|
47
73
|
return reportPath;
|
|
48
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Generates a premium Interactive HTML report.
|
|
77
|
+
*/
|
|
78
|
+
export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
|
|
79
|
+
const reportPath = path.join(sessionDir, `report.html`);
|
|
80
|
+
const projectName = getProjectName();
|
|
81
|
+
const timestamp = new Date().toLocaleString();
|
|
82
|
+
const data = JSON.stringify(allFindings);
|
|
83
|
+
const htmlContent = `
|
|
84
|
+
<!DOCTYPE html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<meta charset="UTF-8">
|
|
88
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
89
|
+
<title>Pika Review | ${projectName}</title>
|
|
90
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
91
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
92
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
93
|
+
<style>
|
|
94
|
+
:root {
|
|
95
|
+
--bg: #0b0f1a;
|
|
96
|
+
--sidebar-bg: #151b2d;
|
|
97
|
+
--card-bg: #1e253a;
|
|
98
|
+
--text: #f1f5f9;
|
|
99
|
+
--text-dim: #94a3b8;
|
|
100
|
+
--accent: #38bdf8;
|
|
101
|
+
--critical: #f43f5e;
|
|
102
|
+
--high: #fb923c;
|
|
103
|
+
--medium: #fbbf24;
|
|
104
|
+
--low: #4ade80;
|
|
105
|
+
--border: rgba(255,255,255,0.08);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
109
|
+
body {
|
|
110
|
+
font-family: 'Inter', sans-serif;
|
|
111
|
+
background: var(--bg);
|
|
112
|
+
color: var(--text);
|
|
113
|
+
display: flex;
|
|
114
|
+
height: 100vh;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Sidebar */
|
|
119
|
+
aside {
|
|
120
|
+
width: 350px;
|
|
121
|
+
background: var(--sidebar-bg);
|
|
122
|
+
border-right: 1px solid var(--border);
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
box-shadow: 10px 0 30px rgba(0,0,0,0.3);
|
|
126
|
+
z-index: 10;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.brand {
|
|
130
|
+
padding: 32px 24px;
|
|
131
|
+
border-bottom: 1px solid var(--border);
|
|
132
|
+
background: linear-gradient(135deg, rgba(56, 189, 248, 0.1), transparent);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.brand h1 { font-size: 1.5rem; font-weight: 800; letter-spacing: -0.02em; color: var(--accent); display: flex; align-items: center; gap: 10px; }
|
|
136
|
+
.brand p { font-size: 0.8rem; color: var(--text-dim); margin-top: 8px; font-weight: 500; }
|
|
137
|
+
|
|
138
|
+
.sidebar-stats {
|
|
139
|
+
padding: 20px 24px;
|
|
140
|
+
display: grid;
|
|
141
|
+
grid-template-columns: 1fr 1fr;
|
|
142
|
+
gap: 12px;
|
|
143
|
+
border-bottom: 1px solid var(--border);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.stat-box {
|
|
147
|
+
background: rgba(255,255,255,0.03);
|
|
148
|
+
padding: 12px;
|
|
149
|
+
border-radius: 10px;
|
|
150
|
+
text-align: center;
|
|
151
|
+
border: 1px solid var(--border);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.stat-val { display: block; font-size: 1.25rem; font-weight: 700; color: var(--accent); }
|
|
155
|
+
.stat-lab { display: block; font-size: 0.65rem; text-transform: uppercase; color: var(--text-dim); margin-top: 4px; letter-spacing: 0.05em; }
|
|
156
|
+
|
|
157
|
+
.file-list {
|
|
158
|
+
flex: 1;
|
|
159
|
+
overflow-y: auto;
|
|
160
|
+
padding: 16px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.file-item {
|
|
164
|
+
padding: 14px 16px;
|
|
165
|
+
border-radius: 12px;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
margin-bottom: 8px;
|
|
168
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
169
|
+
display: flex;
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
gap: 6px;
|
|
172
|
+
border: 1px solid transparent;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.file-item:hover { background: rgba(255,255,255,0.04); border-color: var(--border); }
|
|
176
|
+
.file-item.active { background: var(--accent); color: #000; box-shadow: 0 4px 15px rgba(56, 189, 248, 0.3); }
|
|
177
|
+
.file-item .name { font-size: 0.85rem; font-family: 'JetBrains Mono', monospace; font-weight: 500; word-break: break-all; }
|
|
178
|
+
|
|
179
|
+
.severity-pills { display: flex; gap: 4px; }
|
|
180
|
+
.pill { font-size: 0.65rem; padding: 2px 6px; border-radius: 4px; background: rgba(0,0,0,0.2); font-weight: 700; }
|
|
181
|
+
.file-item.active .pill { background: rgba(0,0,0,0.1); color: #000; }
|
|
182
|
+
|
|
183
|
+
/* Main Content */
|
|
184
|
+
main {
|
|
185
|
+
flex: 1;
|
|
186
|
+
overflow-y: auto;
|
|
187
|
+
padding: 60px;
|
|
188
|
+
background: radial-gradient(circle at top right, rgba(56, 189, 248, 0.05), transparent 40%);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.content-container { max-width: 900px; margin: 0 auto; }
|
|
192
|
+
|
|
193
|
+
.page-header {
|
|
194
|
+
margin-bottom: 50px;
|
|
195
|
+
animation: fadeInDown 0.5s ease-out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes fadeInDown {
|
|
199
|
+
from { opacity: 0; transform: translateY(-20px); }
|
|
200
|
+
to { opacity: 1; transform: translateY(0); }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.page-header h2 { font-size: 2rem; font-family: 'JetBrains Mono', monospace; margin-bottom: 12px; color: #fff; }
|
|
204
|
+
.page-header p { color: var(--text-dim); font-size: 1rem; }
|
|
205
|
+
|
|
206
|
+
.issue-card {
|
|
207
|
+
background: var(--card-bg);
|
|
208
|
+
border: 1px solid var(--border);
|
|
209
|
+
border-radius: 20px;
|
|
210
|
+
padding: 32px;
|
|
211
|
+
margin-bottom: 30px;
|
|
212
|
+
position: relative;
|
|
213
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
214
|
+
transition: transform 0.3s;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.issue-card:hover { transform: translateY(-4px); }
|
|
218
|
+
|
|
219
|
+
.card-accent {
|
|
220
|
+
position: absolute;
|
|
221
|
+
left: 0; top: 32px; bottom: 32px;
|
|
222
|
+
width: 5px;
|
|
223
|
+
border-radius: 0 4px 4px 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.issue-card.Critical .card-accent { background: var(--critical); box-shadow: 0 0 15px var(--critical); }
|
|
227
|
+
.issue-card.High .card-accent { background: var(--high); box-shadow: 0 0 15px var(--high); }
|
|
228
|
+
.issue-card.Medium .card-accent { background: var(--medium); box-shadow: 0 0 15px var(--medium); }
|
|
229
|
+
.issue-card.Low .card-accent { background: var(--low); box-shadow: 0 0 15px var(--low); }
|
|
230
|
+
|
|
231
|
+
.issue-meta {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
justify-content: space-between;
|
|
235
|
+
margin-bottom: 24px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.severity-tag {
|
|
239
|
+
font-size: 0.75rem;
|
|
240
|
+
text-transform: uppercase;
|
|
241
|
+
font-weight: 800;
|
|
242
|
+
padding: 6px 14px;
|
|
243
|
+
border-radius: 8px;
|
|
244
|
+
letter-spacing: 0.05em;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.severity-tag.Critical { background: rgba(244, 63, 94, 0.15); color: var(--critical); border: 1px solid var(--critical); }
|
|
248
|
+
.severity-tag.High { background: rgba(251, 146, 60, 0.15); color: var(--high); border: 1px solid var(--high); }
|
|
249
|
+
.severity-tag.Medium { background: rgba(251, 191, 36, 0.15); color: var(--medium); border: 1px solid var(--medium); }
|
|
250
|
+
.severity-tag.Low { background: rgba(74, 222, 128, 0.15); color: var(--low); border: 1px solid var(--low); }
|
|
251
|
+
|
|
252
|
+
.line-info { font-family: 'JetBrains Mono', monospace; color: var(--text-dim); font-size: 0.9rem; }
|
|
253
|
+
|
|
254
|
+
.field-group { margin-bottom: 24px; }
|
|
255
|
+
.field-label {
|
|
256
|
+
font-size: 0.7rem;
|
|
257
|
+
font-weight: 800;
|
|
258
|
+
color: var(--text-dim);
|
|
259
|
+
text-transform: uppercase;
|
|
260
|
+
letter-spacing: 0.1em;
|
|
261
|
+
margin-bottom: 10px;
|
|
262
|
+
display: flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
gap: 8px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.field-content { line-height: 1.7; color: #cbd5e1; font-size: 1.05rem; }
|
|
268
|
+
|
|
269
|
+
pre {
|
|
270
|
+
background: #000;
|
|
271
|
+
padding: 24px;
|
|
272
|
+
border-radius: 12px;
|
|
273
|
+
font-family: 'JetBrains Mono', monospace;
|
|
274
|
+
font-size: 0.9rem;
|
|
275
|
+
margin-top: 15px;
|
|
276
|
+
overflow-x: auto;
|
|
277
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
278
|
+
color: #e2e8f0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.empty-state {
|
|
282
|
+
display: flex;
|
|
283
|
+
flex-direction: column;
|
|
284
|
+
align-items: center;
|
|
285
|
+
justify-content: center;
|
|
286
|
+
height: 60vh;
|
|
287
|
+
text-align: center;
|
|
288
|
+
opacity: 0.6;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.empty-state svg { width: 80px; height: 80px; margin-bottom: 24px; stroke: var(--text-dim); }
|
|
292
|
+
|
|
293
|
+
/* Scrollbar */
|
|
294
|
+
::-webkit-scrollbar { width: 6px; }
|
|
295
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
296
|
+
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
|
|
297
|
+
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
|
|
298
|
+
</style>
|
|
299
|
+
</head>
|
|
300
|
+
<body>
|
|
301
|
+
<aside>
|
|
302
|
+
<div class="brand">
|
|
303
|
+
<h1>🦊 PIKA REVIEW</h1>
|
|
304
|
+
<p>${projectName} • ${timestamp}</p>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="sidebar-stats">
|
|
307
|
+
<div class="stat-box">
|
|
308
|
+
<span class="stat-val" id="totalFiles">0</span>
|
|
309
|
+
<span class="stat-lab">Files</span>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="stat-box">
|
|
312
|
+
<span class="stat-val" id="totalIssues">0</span>
|
|
313
|
+
<span class="stat-lab">Issues</span>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="file-list" id="fileList"></div>
|
|
317
|
+
</aside>
|
|
318
|
+
|
|
319
|
+
<main>
|
|
320
|
+
<div class="content-container" id="mainContent">
|
|
321
|
+
<div class="empty-state">
|
|
322
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
|
323
|
+
<h3>Select a file to inspect</h3>
|
|
324
|
+
<p>Architectural findings and security insights will appear here.</p>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</main>
|
|
328
|
+
|
|
329
|
+
<script id="pika-data" type="application/json">${data}</script>
|
|
330
|
+
<script>
|
|
331
|
+
(function() {
|
|
332
|
+
const findings = JSON.parse(document.getElementById('pika-data').textContent);
|
|
333
|
+
const fileList = document.getElementById('fileList');
|
|
334
|
+
const mainContent = document.getElementById('mainContent');
|
|
335
|
+
const totalFilesEl = document.getElementById('totalFiles');
|
|
336
|
+
const totalIssuesEl = document.getElementById('totalIssues');
|
|
337
|
+
|
|
338
|
+
function escape(str) {
|
|
339
|
+
if (!str) return '';
|
|
340
|
+
const div = document.createElement('div');
|
|
341
|
+
div.textContent = str;
|
|
342
|
+
return div.innerHTML;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function init() {
|
|
346
|
+
totalFilesEl.textContent = findings.length;
|
|
347
|
+
let issuesCount = 0;
|
|
348
|
+
|
|
349
|
+
findings.forEach((f, index) => {
|
|
350
|
+
issuesCount += f.reviews.length;
|
|
351
|
+
const item = document.createElement('div');
|
|
352
|
+
item.className = 'file-item';
|
|
353
|
+
|
|
354
|
+
const name = document.createElement('span');
|
|
355
|
+
name.className = 'name';
|
|
356
|
+
name.textContent = f.fileName;
|
|
357
|
+
item.appendChild(name);
|
|
358
|
+
|
|
359
|
+
if (f.reviews.length > 0) {
|
|
360
|
+
const pills = document.createElement('div');
|
|
361
|
+
pills.className = 'severity-pills';
|
|
362
|
+
|
|
363
|
+
const counts = f.reviews.reduce((acc, r) => {
|
|
364
|
+
acc[r.severity] = (acc[r.severity] || 0) + 1;
|
|
365
|
+
return acc;
|
|
366
|
+
}, {});
|
|
367
|
+
|
|
368
|
+
Object.entries(counts).forEach(([sev, count]) => {
|
|
369
|
+
const pill = document.createElement('span');
|
|
370
|
+
pill.className = 'pill';
|
|
371
|
+
pill.style.color = 'var(--' + sev.toLowerCase() + ')';
|
|
372
|
+
pill.textContent = count;
|
|
373
|
+
pills.appendChild(pill);
|
|
374
|
+
});
|
|
375
|
+
item.appendChild(pills);
|
|
376
|
+
} else {
|
|
377
|
+
const clean = document.createElement('span');
|
|
378
|
+
clean.className = 'pill';
|
|
379
|
+
clean.style.color = 'var(--low)';
|
|
380
|
+
clean.textContent = 'CLEAN';
|
|
381
|
+
item.appendChild(clean);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
item.onclick = () => selectFile(index, item);
|
|
385
|
+
fileList.appendChild(item);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
totalIssuesEl.textContent = issuesCount;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function selectFile(index, element) {
|
|
392
|
+
document.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
|
|
393
|
+
element.classList.add('active');
|
|
394
|
+
|
|
395
|
+
const f = findings[index];
|
|
396
|
+
mainContent.innerHTML = '';
|
|
397
|
+
|
|
398
|
+
const header = document.createElement('div');
|
|
399
|
+
header.className = 'page-header';
|
|
400
|
+
|
|
401
|
+
const title = document.createElement('h2');
|
|
402
|
+
title.textContent = f.fileName;
|
|
403
|
+
header.appendChild(title);
|
|
404
|
+
|
|
405
|
+
const subtitle = document.createElement('p');
|
|
406
|
+
subtitle.textContent = f.reviews.length + ' findings identified in this analysis.';
|
|
407
|
+
header.appendChild(subtitle);
|
|
408
|
+
|
|
409
|
+
mainContent.appendChild(header);
|
|
410
|
+
|
|
411
|
+
if (f.reviews.length === 0) {
|
|
412
|
+
const empty = document.createElement('div');
|
|
413
|
+
empty.className = 'empty-state';
|
|
414
|
+
empty.innerHTML = '<h3>✨ Code is pristine</h3><p>No architectural anomalies or security risks found in this file.</p>';
|
|
415
|
+
mainContent.appendChild(empty);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
f.reviews.forEach(r => {
|
|
420
|
+
const card = document.createElement('div');
|
|
421
|
+
card.className = 'issue-card ' + r.severity;
|
|
422
|
+
|
|
423
|
+
card.innerHTML = [
|
|
424
|
+
'<div class="card-accent"></div>',
|
|
425
|
+
'<div class="issue-meta">',
|
|
426
|
+
'<span class="severity-tag ' + r.severity + '">' + r.severity + '</span>',
|
|
427
|
+
'<span class="line-info">Line ' + (r.line || 'N/A') + '</span>',
|
|
428
|
+
'</div>',
|
|
429
|
+
'<div class="field-group">',
|
|
430
|
+
'<div class="field-label">💡 Finding</div>',
|
|
431
|
+
'<div class="field-content">' + escape(r.finding) + '</div>',
|
|
432
|
+
'</div>',
|
|
433
|
+
'<div class="field-group">',
|
|
434
|
+
'<div class="field-label">💥 Impact</div>',
|
|
435
|
+
'<div class="field-content">' + escape(r.impact) + '</div>',
|
|
436
|
+
'</div>',
|
|
437
|
+
'<div class="field-group">',
|
|
438
|
+
'<div class="field-label">🛠️ Recommendation</div>',
|
|
439
|
+
'<pre><code>' + escape(r.recommendation) + '</code></pre>',
|
|
440
|
+
'</div>'
|
|
441
|
+
].join('');
|
|
442
|
+
|
|
443
|
+
mainContent.appendChild(card);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
mainContent.scrollTop = 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
init();
|
|
450
|
+
})();
|
|
451
|
+
</script>
|
|
452
|
+
</body>
|
|
453
|
+
</html>
|
|
454
|
+
`;
|
|
455
|
+
fs.writeFileSync(reportPath, htmlContent);
|
|
456
|
+
// Point 2: Update history.json for stats tracking
|
|
457
|
+
try {
|
|
458
|
+
const historyPath = path.join(process.cwd(), ".pika-reports", "history.json");
|
|
459
|
+
let history = [];
|
|
460
|
+
if (fs.existsSync(historyPath)) {
|
|
461
|
+
history = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
462
|
+
}
|
|
463
|
+
const totalIssues = allFindings.reduce((sum, f) => sum + f.reviews.length, 0);
|
|
464
|
+
const severityCounts = allFindings.reduce((acc, f) => {
|
|
465
|
+
f.reviews.forEach(r => {
|
|
466
|
+
acc[r.severity] = (acc[r.severity] || 0) + 1;
|
|
467
|
+
});
|
|
468
|
+
return acc;
|
|
469
|
+
}, { Critical: 0, High: 0, Medium: 0, Low: 0 });
|
|
470
|
+
history.push({
|
|
471
|
+
timestamp: new Date().toISOString(),
|
|
472
|
+
projectName,
|
|
473
|
+
totalFiles,
|
|
474
|
+
totalIssues,
|
|
475
|
+
severityCounts,
|
|
476
|
+
reportPath: path.relative(path.join(process.cwd(), ".pika-reports"), reportPath)
|
|
477
|
+
});
|
|
478
|
+
// Keep only last 50 runs to prevent file bloat
|
|
479
|
+
if (history.length > 50)
|
|
480
|
+
history.shift();
|
|
481
|
+
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
482
|
+
}
|
|
483
|
+
catch (e) { }
|
|
484
|
+
return reportPath;
|
|
485
|
+
}
|
package/dist/core/scanner.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "fs";
|
|
|
3
3
|
import cliProgress from "cli-progress";
|
|
4
4
|
import pLimit from "p-limit";
|
|
5
5
|
import { analyzeDiff } from "./ai.js";
|
|
6
|
-
import { setupReportDir, writeMarkdownReport } from "./reporter.js";
|
|
6
|
+
import { setupReportDir, writeMarkdownReport, writeHTMLReport } from "./reporter.js";
|
|
7
7
|
import { getIgnoredFiles } from "../utils/config.js";
|
|
8
8
|
import { logger } from "../utils/logger.js";
|
|
9
9
|
import path from "path";
|
|
@@ -65,7 +65,7 @@ export function getDiffFiles(type) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* Scan Orchestrator: Manages the review lifecycle.
|
|
68
|
-
* Concurrency is limited to 3 to stay within
|
|
68
|
+
* Concurrency is limited to 3 to stay within default provider free tier constraints.
|
|
69
69
|
*/
|
|
70
70
|
export async function runScan(target, isCI, specificFiles) {
|
|
71
71
|
let files = [];
|
|
@@ -98,6 +98,7 @@ export async function runScan(target, isCI, specificFiles) {
|
|
|
98
98
|
if (bar)
|
|
99
99
|
bar.start(files.length, 0, { file: "Starting..." });
|
|
100
100
|
const generatedReports = [];
|
|
101
|
+
const allFindings = [];
|
|
101
102
|
let criticalIssuesFound = false;
|
|
102
103
|
const limit = pLimit(3);
|
|
103
104
|
let completed = 0;
|
|
@@ -115,7 +116,7 @@ export async function runScan(target, isCI, specificFiles) {
|
|
|
115
116
|
}
|
|
116
117
|
else {
|
|
117
118
|
try {
|
|
118
|
-
contentToScan = execFileSync("git", ["diff", target === "staged" ? "--cached" : "", "--", file], {
|
|
119
|
+
contentToScan = execFileSync("git", ["diff", "-U10", target === "staged" ? "--cached" : "", "--", file], {
|
|
119
120
|
encoding: "utf-8",
|
|
120
121
|
stdio: ["ignore", "pipe", "ignore"] // Suppress stderr
|
|
121
122
|
});
|
|
@@ -134,20 +135,42 @@ export async function runScan(target, isCI, specificFiles) {
|
|
|
134
135
|
}
|
|
135
136
|
// Token Estimator Check
|
|
136
137
|
validateTokenLimit(contentToScan);
|
|
137
|
-
const result = await analyzeDiff(contentToScan);
|
|
138
|
+
const result = await analyzeDiff(contentToScan, file);
|
|
138
139
|
if (result.reviews.length > 0) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
// Point 5: "Ignore" Comments Logic
|
|
141
|
+
const filteredReviews = result.reviews.filter(r => {
|
|
142
|
+
if (!r.line)
|
|
143
|
+
return true;
|
|
144
|
+
try {
|
|
145
|
+
const fileLines = fs.readFileSync(file, "utf-8").split("\n");
|
|
146
|
+
const targetLineContent = fileLines[r.line - 1] || "";
|
|
147
|
+
if (targetLineContent.includes("pika-ignore"))
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
catch (e) { }
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
153
|
+
if (filteredReviews.length > 0) {
|
|
154
|
+
const hasCritical = filteredReviews.some(r => r.severity === "Critical" || r.severity === "High");
|
|
155
|
+
if (hasCritical)
|
|
156
|
+
criticalIssuesFound = true;
|
|
157
|
+
const reportPath = writeMarkdownReport(file, filteredReviews, reportDir);
|
|
158
|
+
generatedReports.push(reportPath);
|
|
159
|
+
allFindings.push({ fileName: file, reviews: filteredReviews });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
allFindings.push({ fileName: file, reviews: [] });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
allFindings.push({ fileName: file, reviews: [] });
|
|
144
167
|
}
|
|
145
168
|
}
|
|
146
169
|
catch (e) {
|
|
147
170
|
if (e.message === "RATE_LIMIT") {
|
|
148
171
|
if (bar)
|
|
149
172
|
bar.stop();
|
|
150
|
-
logger.critical("DAILY
|
|
173
|
+
logger.critical("DAILY RATE LIMIT REACHED. Scanning aborted.");
|
|
151
174
|
process.exit(1);
|
|
152
175
|
}
|
|
153
176
|
logger.error(`Error scanning ${file}: ${e.message}`);
|
|
@@ -165,5 +188,10 @@ export async function runScan(target, isCI, specificFiles) {
|
|
|
165
188
|
logger.critical("CI Pipeline Failed: Critical issues detected.");
|
|
166
189
|
process.exit(1);
|
|
167
190
|
}
|
|
191
|
+
const htmlPath = writeHTMLReport(reportDir, files.length, allFindings);
|
|
192
|
+
if (!isCI) {
|
|
193
|
+
logger.success("\nScan complete!");
|
|
194
|
+
logger.info(`Interactive Report: ${htmlPath}`);
|
|
195
|
+
}
|
|
168
196
|
return generatedReports;
|
|
169
197
|
}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ import { Command } from "commander";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { initAction } from "./cmd/init.js";
|
|
5
5
|
import { scanAction } from "./cmd/scan.js";
|
|
6
|
+
import { viewAction } from "./cmd/view.js";
|
|
7
|
+
import { statsAction } from "./cmd/stats.js";
|
|
6
8
|
/**
|
|
7
9
|
* Pika Review: The Enterprise-Grade AI Code Reviewer.
|
|
8
10
|
* Modular entry point for CLI operations.
|
|
@@ -10,19 +12,22 @@ import { scanAction } from "./cmd/scan.js";
|
|
|
10
12
|
const program = new Command();
|
|
11
13
|
// Branding Header
|
|
12
14
|
const BRAND = `
|
|
13
|
-
${chalk.bgCyan.black.bold(" PIKA REVIEW ")} ${chalk.dim("v2.0.0")}
|
|
14
|
-
${chalk.italic.gray("
|
|
15
|
+
${chalk.bgCyan.black.bold(" PIKA REVIEW ")} ${chalk.dim("v2.0.0 Enterprise")}
|
|
16
|
+
${chalk.italic.gray("AI Architectural Sentinel & Compliance Engine")}
|
|
15
17
|
`;
|
|
16
18
|
const HELPER_TEXT = `
|
|
17
19
|
${chalk.bold.cyan("🚀 Quick Start:")}
|
|
18
20
|
$ pika-review scan ${chalk.dim("# Scan staged git changes (default)")}
|
|
19
|
-
$ pika-review scan -
|
|
21
|
+
$ pika-review scan -i ${chalk.dim("# Interactive file selection mode")}
|
|
22
|
+
$ pika-review view ${chalk.dim("# Open the latest interactive report")}
|
|
23
|
+
$ pika-review stats ${chalk.dim("# View architectural health trends")}
|
|
20
24
|
$ pika-review scan file.ts ${chalk.dim("# Scan a specific file")}
|
|
21
25
|
$ pika-review scan f1 f2 ${chalk.dim("# Scan multiple specific files")}
|
|
22
26
|
|
|
23
27
|
${chalk.bold.cyan("⌨️ Shortcuts & Tips:")}
|
|
24
28
|
- Use ${chalk.yellow("--ci")} in GitHub Actions to fail on Critical/High issues.
|
|
25
|
-
- Create
|
|
29
|
+
- Create ${chalk.yellow(".pika-rules.md")} to enforce project-specific architecture.
|
|
30
|
+
- Use ${chalk.yellow("pika-ignore")} in code comments to skip specific lines.
|
|
26
31
|
- Review artifacts are stored in ${chalk.yellow(".pika-reports/")} automatically.
|
|
27
32
|
|
|
28
33
|
${chalk.bold.cyan("📊 Progress & Concurrency:")}
|
|
@@ -31,7 +36,7 @@ ${chalk.bold.cyan("📊 Progress & Concurrency:")}
|
|
|
31
36
|
`;
|
|
32
37
|
program
|
|
33
38
|
.name("pika-review")
|
|
34
|
-
.description("Enterprise-grade AI Code Reviewer
|
|
39
|
+
.description("Enterprise-grade AI Architectural Code Reviewer")
|
|
35
40
|
.version("2.0.0")
|
|
36
41
|
.addHelpText("before", BRAND)
|
|
37
42
|
.addHelpText("after", HELPER_TEXT);
|
|
@@ -42,10 +47,25 @@ program
|
|
|
42
47
|
console.log(BRAND);
|
|
43
48
|
await initAction();
|
|
44
49
|
});
|
|
50
|
+
program
|
|
51
|
+
.command("view")
|
|
52
|
+
.description("Open the latest interactive HTML report in your browser")
|
|
53
|
+
.action(async () => {
|
|
54
|
+
console.log(BRAND);
|
|
55
|
+
await viewAction();
|
|
56
|
+
});
|
|
57
|
+
program
|
|
58
|
+
.command("stats")
|
|
59
|
+
.description("View architectural health trends and scan history")
|
|
60
|
+
.action(async () => {
|
|
61
|
+
console.log(BRAND);
|
|
62
|
+
await statsAction();
|
|
63
|
+
});
|
|
45
64
|
program
|
|
46
65
|
.command("scan [files...]", { isDefault: true }) // Set scan as the default command
|
|
47
66
|
.description("Scan git changes or specific files for architectural anomalies")
|
|
48
67
|
.option("-u, --unstaged", "Analyze unstaged changes instead of staged")
|
|
68
|
+
.option("-i, --interactive", "Interactively select files to scan")
|
|
49
69
|
.option("--ci", "Headless CI/CD mode (fails on Critical/High issues, strips visuals)")
|
|
50
70
|
.action(async (files, options) => {
|
|
51
71
|
// Only show branding if not in CI mode and if files were explicitly passed or if it's the default
|
package/dist/utils/config.js
CHANGED
|
@@ -6,11 +6,11 @@ import { logger } from "./logger.js";
|
|
|
6
6
|
const CONFIG_PATH = path.join(os.homedir(), ".pika-review.yaml");
|
|
7
7
|
const DEFAULT_CONFIG = {
|
|
8
8
|
ai: {
|
|
9
|
-
accountId: "", //
|
|
9
|
+
accountId: "", // Account ID if required by provider
|
|
10
10
|
apiKey: "",
|
|
11
11
|
model: "@cf/meta/llama-3-8b-instruct",
|
|
12
12
|
prompt: "",
|
|
13
|
-
baseURL: "", // Leave empty for
|
|
13
|
+
baseURL: "", // Leave empty for default provider, or set for custom API
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
@@ -23,7 +23,7 @@ export function initConfig() {
|
|
|
23
23
|
mode: 0o600,
|
|
24
24
|
});
|
|
25
25
|
logger.success(`Configuration initialized at ${CONFIG_PATH}`);
|
|
26
|
-
logger.info("Please edit this file and add your
|
|
26
|
+
logger.info("Please edit this file and add your AI provider credentials.");
|
|
27
27
|
}
|
|
28
28
|
else {
|
|
29
29
|
logger.warn(`Configuration already exists at ${CONFIG_PATH}`);
|
|
@@ -46,13 +46,44 @@ export function getIgnoredFiles() {
|
|
|
46
46
|
const ignorePath = path.join(process.cwd(), ".pikaignore");
|
|
47
47
|
if (!fs.existsSync(ignorePath)) {
|
|
48
48
|
return [
|
|
49
|
-
".svg",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
49
|
+
".svg",
|
|
50
|
+
".lock",
|
|
51
|
+
"package-lock.json",
|
|
52
|
+
".png",
|
|
53
|
+
".jpg",
|
|
54
|
+
".jpeg",
|
|
55
|
+
".ico",
|
|
56
|
+
"node_modules",
|
|
57
|
+
".git",
|
|
58
|
+
"dist",
|
|
59
|
+
"build",
|
|
60
|
+
"out",
|
|
61
|
+
".next",
|
|
62
|
+
"public",
|
|
63
|
+
".pika-reports",
|
|
64
|
+
".env",
|
|
65
|
+
".DS_Store",
|
|
66
|
+
"bun.lockb",
|
|
67
|
+
"pnpm-lock.yaml",
|
|
68
|
+
"venv",
|
|
69
|
+
".venv",
|
|
70
|
+
"__pycache__",
|
|
71
|
+
".pytest_cache",
|
|
72
|
+
"target",
|
|
73
|
+
".gradle",
|
|
74
|
+
".idea",
|
|
75
|
+
".vscode",
|
|
76
|
+
"vendor",
|
|
77
|
+
"coverage",
|
|
78
|
+
".turbo",
|
|
79
|
+
"tests",
|
|
80
|
+
"__tests__",
|
|
81
|
+
"spec",
|
|
82
|
+
"specs",
|
|
83
|
+
"cypress",
|
|
84
|
+
"playwright-report",
|
|
85
|
+
"test-results",
|
|
86
|
+
".nyc_output",
|
|
56
87
|
];
|
|
57
88
|
}
|
|
58
89
|
const content = fs.readFileSync(ignorePath, "utf-8");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pika-review",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "Enterprise-grade AI Architectural Code Reviewer
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Enterprise-grade AI Architectural Code Reviewer.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -29,8 +29,6 @@
|
|
|
29
29
|
"keywords": [
|
|
30
30
|
"ai",
|
|
31
31
|
"code-review",
|
|
32
|
-
"cloudflare",
|
|
33
|
-
"workers-ai",
|
|
34
32
|
"architectural-analysis",
|
|
35
33
|
"cli"
|
|
36
34
|
],
|