pika-review 2.2.0 → 2.3.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 +1 -1
- package/dist/cmd/autofix.js +61 -0
- package/dist/cmd/chat.js +87 -0
- package/dist/cmd/github.js +79 -0
- package/dist/cmd/rules.js +17 -0
- package/dist/cmd/scan.js +35 -0
- package/dist/core/ai.js +4 -4
- package/dist/core/cache.js +55 -0
- package/dist/core/reporter.js +5 -1
- package/dist/core/scanner.js +33 -1
- package/dist/core/stats.js +81 -0
- package/dist/index.js +28 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the first markdown fenced code block from a recommendation string.
|
|
6
|
+
*/
|
|
7
|
+
export function extractCodeBlock(markdown) {
|
|
8
|
+
// Regex matches triple backtick block with optional language identifier
|
|
9
|
+
const match = markdown.match(/```[a-zA-Z]*\n([\s\S]*?)\n```/);
|
|
10
|
+
return match ? match[1] : null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Patches a target file in-place with an AI code block at a specific line number.
|
|
14
|
+
* Preserves the original indentation of the line being replaced.
|
|
15
|
+
*/
|
|
16
|
+
export function applyAutoFix(filePath, lineNum, recommendation) {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
console.log(` ${chalk.red("✖")} Target file not found: ${filePath}`);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const code = extractCodeBlock(recommendation);
|
|
23
|
+
if (!code) {
|
|
24
|
+
console.log(` ${chalk.red("✖")} No valid code block found in recommendation to apply.`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
28
|
+
const lines = content.split(/\r?\n/);
|
|
29
|
+
if (lineNum !== undefined && lineNum > 0 && lineNum <= lines.length) {
|
|
30
|
+
// Find indentation of the original line to preserve nesting structure
|
|
31
|
+
const indent = lines[lineNum - 1].match(/^\s*/)?.[0] || "";
|
|
32
|
+
const indentedCode = code.split("\n").map(l => indent + l).join("\n");
|
|
33
|
+
lines[lineNum - 1] = indentedCode;
|
|
34
|
+
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
35
|
+
console.log(`\n ${chalk.green("✓")} Clean code applied to ${chalk.bold(filePath)} at line ${lineNum}.`);
|
|
36
|
+
// Render Git Diff for immediate visual confirmation
|
|
37
|
+
try {
|
|
38
|
+
console.log(`\n ${chalk.cyan.bold("◆ Git Diff Confirmation:")}`);
|
|
39
|
+
const diff = execSync(`git diff --color "${filePath}"`, { encoding: "utf-8" });
|
|
40
|
+
if (diff.trim()) {
|
|
41
|
+
console.log(diff);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(` ${chalk.dim("Lines matched exactly; no raw text diff produced.")}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (diffErr) {
|
|
48
|
+
// Fail silently if git is not initialized or diff fails
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(` ${chalk.red("✖")} Target line number is out of bounds or undefined.`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.log(` ${chalk.red("✖")} Failed to apply auto-fix: ${e.message}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/cmd/chat.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import OpenAI from "openai";
|
|
5
|
+
import { getConfig } from "../utils/config.js";
|
|
6
|
+
/**
|
|
7
|
+
* CLI Command: discuss [file]
|
|
8
|
+
* Launches an interactive Socratic chat session inside the console focusing on design context.
|
|
9
|
+
*/
|
|
10
|
+
export async function discussAction(filePath) {
|
|
11
|
+
if (!fs.existsSync(filePath)) {
|
|
12
|
+
console.log(` ${chalk.red("✖")} Target file not found: ${filePath}`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const config = getConfig();
|
|
16
|
+
const isOllama = config.ai.provider === "ollama";
|
|
17
|
+
const isCloudflare = config.ai.provider === "cloudflare";
|
|
18
|
+
const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
|
|
19
|
+
? config.ai.baseURL
|
|
20
|
+
: (isOllama
|
|
21
|
+
? "http://localhost:11434/v1"
|
|
22
|
+
: (isCloudflare
|
|
23
|
+
? `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`
|
|
24
|
+
: "https://api.openai.com/v1"));
|
|
25
|
+
const openai = new OpenAI({
|
|
26
|
+
apiKey: config.ai.apiKey || "ollama",
|
|
27
|
+
baseURL,
|
|
28
|
+
});
|
|
29
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
30
|
+
// Load custom architecture standards rules if they exist
|
|
31
|
+
let complianceSection = "";
|
|
32
|
+
try {
|
|
33
|
+
const rulesPath = ".pika-rules.md";
|
|
34
|
+
if (fs.existsSync(rulesPath)) {
|
|
35
|
+
complianceSection = `\nCustom Rules:\n${fs.readFileSync(rulesPath, "utf-8")}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (e) { }
|
|
39
|
+
const messages = [
|
|
40
|
+
{
|
|
41
|
+
role: "system",
|
|
42
|
+
content: `You are an Elite Senior Full-Stack Architect and Security Mentor.
|
|
43
|
+
Your task is to have an interactive Socratic discussion with the developer regarding the provided file.
|
|
44
|
+
Help them brainstorm alternative patterns, explain the reasoning behind quality benchmarks, and walk them through refactoring steps.
|
|
45
|
+
|
|
46
|
+
Target File Path: ${filePath}
|
|
47
|
+
File Content:
|
|
48
|
+
\`\`\`
|
|
49
|
+
${fileContent}
|
|
50
|
+
\`\`\`
|
|
51
|
+
${complianceSection}`
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
console.log(`\n ${chalk.cyan.bold("◆ Socratic Chat Started")}`);
|
|
55
|
+
console.log(` Discussing: ${chalk.bold(filePath)}`);
|
|
56
|
+
console.log(` Type ${chalk.yellow("exit")} or ${chalk.yellow("quit")} to end the session.\n`);
|
|
57
|
+
while (true) {
|
|
58
|
+
const userInput = await prompts({
|
|
59
|
+
type: "text",
|
|
60
|
+
name: "message",
|
|
61
|
+
message: `${chalk.green("💬 You:")}`,
|
|
62
|
+
});
|
|
63
|
+
if (!userInput.message || userInput.message.trim() === "")
|
|
64
|
+
continue;
|
|
65
|
+
const trimmed = userInput.message.trim();
|
|
66
|
+
if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
|
|
67
|
+
console.log(`\n ${chalk.cyan("→")} Discuss session finished. Happy coding!\n`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
messages.push({ role: "user", content: trimmed });
|
|
71
|
+
process.stdout.write(`\n ${chalk.cyan.bold("◆ Pika Sentinel:")}\n `);
|
|
72
|
+
try {
|
|
73
|
+
const response = await openai.chat.completions.create({
|
|
74
|
+
model: config.ai.model,
|
|
75
|
+
messages,
|
|
76
|
+
temperature: 0.2,
|
|
77
|
+
});
|
|
78
|
+
const reply = response.choices[0]?.message?.content || "";
|
|
79
|
+
console.log(reply.split("\n").join("\n "));
|
|
80
|
+
console.log();
|
|
81
|
+
messages.push({ role: "assistant", content: reply });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.log(`${chalk.red("✖")} Error communicating with AI: ${e.message}\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
/**
|
|
4
|
+
* Automates Pull Request inline code-reviews by posting reviews directly to GitHub.
|
|
5
|
+
* Utilizes lightweight native fetch calls to keep dependencies at zero.
|
|
6
|
+
*/
|
|
7
|
+
export async function postGitHubPRComments(findings) {
|
|
8
|
+
const token = process.env.GITHUB_TOKEN;
|
|
9
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
10
|
+
if (!token || !eventPath) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.existsSync(eventPath)) {
|
|
15
|
+
console.log(` ${chalk.red("✖")} GITHUB_EVENT_PATH file not found.`);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const event = JSON.parse(fs.readFileSync(eventPath, "utf-8"));
|
|
19
|
+
const prNumber = event.pull_request?.number;
|
|
20
|
+
const repoFullName = event.repository?.full_name; // e.g. "owner/repo"
|
|
21
|
+
if (!prNumber || !repoFullName) {
|
|
22
|
+
console.log(` ${chalk.red("✖")} Missing PR number or Repository Name in GitHub Event.`);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
// Get current head commit SHA required for target PR diff annotations
|
|
26
|
+
const commitSha = event.pull_request?.head?.sha;
|
|
27
|
+
if (!commitSha) {
|
|
28
|
+
console.log(` ${chalk.red("✖")} Head Commit SHA is missing.`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
console.log(`\n ${chalk.cyan("→")} Syncing ${findings.length} findings with GitHub PR #${prNumber}...`);
|
|
32
|
+
for (const f of findings) {
|
|
33
|
+
const fileName = f.fileName;
|
|
34
|
+
for (const r of f.reviews) {
|
|
35
|
+
if (!r.line)
|
|
36
|
+
continue;
|
|
37
|
+
const body = `### 🚨 Pika Sentinel: ${r.severity} Finding
|
|
38
|
+
**Finding:** ${r.finding}
|
|
39
|
+
**Impact:** ${r.impact}
|
|
40
|
+
|
|
41
|
+
**Recommendation:**
|
|
42
|
+
\`\`\`
|
|
43
|
+
${r.recommendation}
|
|
44
|
+
\`\`\`
|
|
45
|
+
`;
|
|
46
|
+
const url = `https://api.github.com/repos/${repoFullName}/pulls/${prNumber}/comments`;
|
|
47
|
+
const payload = {
|
|
48
|
+
body,
|
|
49
|
+
commit_id: commitSha,
|
|
50
|
+
path: fileName,
|
|
51
|
+
side: "RIGHT",
|
|
52
|
+
line: r.line,
|
|
53
|
+
};
|
|
54
|
+
const response = await fetch(url, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
"Authorization": `Bearer ${token}`,
|
|
58
|
+
"Accept": "application/vnd.github.v3+json",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"User-Agent": "Pika-Sentinel-Reviewer"
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(payload),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const errMsg = await response.text();
|
|
66
|
+
console.log(` ${chalk.red("✖")} Failed to post comment on ${fileName}:${r.line}: ${errMsg}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(` ${chalk.green("✓")} Posted inline review on ${fileName}:${r.line}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
console.log(` ${chalk.red("✖")} Failed to post comments to GitHub PR: ${e.message}`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/cmd/rules.js
CHANGED
|
@@ -46,16 +46,33 @@ export async function rulesAction() {
|
|
|
46
46
|
logger.info("Analyzing codebase structure and dependencies...");
|
|
47
47
|
// 1. Gather package.json context
|
|
48
48
|
let projectTechStack = "";
|
|
49
|
+
const frameworkClassifications = [];
|
|
49
50
|
try {
|
|
50
51
|
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
51
52
|
if (fs.existsSync(packageJsonPath)) {
|
|
52
53
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
54
|
+
const allDeps = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}) };
|
|
55
|
+
if (allDeps["next"])
|
|
56
|
+
frameworkClassifications.push("Next.js Framework");
|
|
57
|
+
if (allDeps["react"])
|
|
58
|
+
frameworkClassifications.push("React UI Library");
|
|
59
|
+
if (allDeps["express"])
|
|
60
|
+
frameworkClassifications.push("Express.js Server");
|
|
61
|
+
if (allDeps["tailwindcss"])
|
|
62
|
+
frameworkClassifications.push("Tailwind CSS v4 styling");
|
|
63
|
+
if (allDeps["typescript"])
|
|
64
|
+
frameworkClassifications.push("TypeScript Static Typings");
|
|
65
|
+
if (allDeps["bun-types"] || allDeps["@types/bun"])
|
|
66
|
+
frameworkClassifications.push("Bun High-Performance Runtime");
|
|
53
67
|
const deps = Object.keys(packageJson.dependencies || {}).join(", ");
|
|
54
68
|
const devDeps = Object.keys(packageJson.devDependencies || {}).join(", ");
|
|
55
69
|
projectTechStack += `Package Name: ${packageJson.name || "unknown"}\nDependencies: ${deps || "none"}\nDevDependencies: ${devDeps || "none"}\n`;
|
|
56
70
|
}
|
|
57
71
|
}
|
|
58
72
|
catch (e) { }
|
|
73
|
+
if (frameworkClassifications.length > 0) {
|
|
74
|
+
projectTechStack += `Detected Frameworks & Archetypes: ${frameworkClassifications.join(", ")}\n`;
|
|
75
|
+
}
|
|
59
76
|
// 2. Gather file layout context
|
|
60
77
|
const files = listProjectFiles();
|
|
61
78
|
const fileSummary = files.slice(0, 30).map(f => ` - ${f}`).join("\n");
|
package/dist/cmd/scan.js
CHANGED
|
@@ -3,6 +3,8 @@ import chalk from "chalk";
|
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import { runScan, getDiffFiles, listProjectFiles } from "../core/scanner.js";
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
|
+
import { applyAutoFix } from "./autofix.js";
|
|
7
|
+
import { postGitHubPRComments } from "./github.js";
|
|
6
8
|
/**
|
|
7
9
|
* CLI Command: scan
|
|
8
10
|
* Executes the scanning logic and handles user interaction for reports.
|
|
@@ -34,6 +36,10 @@ export async function scanAction(files, options) {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
const { markdownReports, htmlReport, findings } = await runScan(target, isCI, targets);
|
|
39
|
+
// If in CI and GITHUB_TOKEN is present, automatically post reviews
|
|
40
|
+
if (isCI && process.env.GITHUB_TOKEN && process.env.GITHUB_EVENT_PATH) {
|
|
41
|
+
await postGitHubPRComments(findings);
|
|
42
|
+
}
|
|
37
43
|
if (!markdownReports || markdownReports.length === 0) {
|
|
38
44
|
if (!isCI) {
|
|
39
45
|
console.log(`\n ${chalk.green("✓")} ${chalk.bold("Scan complete. No architectural anomalies or security risks detected.")}`);
|
|
@@ -84,6 +90,7 @@ export async function scanAction(files, options) {
|
|
|
84
90
|
message: "Choose post-scan action:",
|
|
85
91
|
choices: [
|
|
86
92
|
{ title: "✨ Open Interactive HTML Dashboard (Recommended)", value: "html" },
|
|
93
|
+
{ title: "🔧 Launch Interactive Auto-Fixer", value: "autofix" },
|
|
87
94
|
{ title: "📂 List generated Markdown file paths", value: "markdown" },
|
|
88
95
|
{ title: "🛑 Exit and start refactoring", value: "exit" },
|
|
89
96
|
],
|
|
@@ -100,6 +107,34 @@ export async function scanAction(files, options) {
|
|
|
100
107
|
console.log(` ${chalk.dim(`Manually open: file://${htmlReport}`)}`);
|
|
101
108
|
}
|
|
102
109
|
}
|
|
110
|
+
else if (response.action === "autofix") {
|
|
111
|
+
const fixableIssues = [];
|
|
112
|
+
findings.forEach((f) => {
|
|
113
|
+
f.reviews.forEach((r) => {
|
|
114
|
+
if (r.line) {
|
|
115
|
+
fixableIssues.push({
|
|
116
|
+
title: `[${r.severity}] ${f.fileName}:${r.line} - ${r.finding.substring(0, 45)}...`,
|
|
117
|
+
value: { filePath: f.fileName, review: r }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
if (fixableIssues.length === 0) {
|
|
123
|
+
console.log(`\n ${chalk.yellow("⚠")} No fixable issues with valid line numbers found.`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const issueSelect = await prompts({
|
|
127
|
+
type: "select",
|
|
128
|
+
name: "issue",
|
|
129
|
+
message: "Select an architectural issue to auto-patch:",
|
|
130
|
+
choices: fixableIssues,
|
|
131
|
+
});
|
|
132
|
+
if (issueSelect.issue) {
|
|
133
|
+
const { filePath, review } = issueSelect.issue;
|
|
134
|
+
applyAutoFix(filePath, review.line, review.recommendation);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
103
138
|
else if (response.action === "markdown") {
|
|
104
139
|
console.log(`\n ${chalk.bold("Generated Markdown Artifacts:")}`);
|
|
105
140
|
markdownReports.forEach((r) => {
|
package/dist/core/ai.js
CHANGED
|
@@ -112,10 +112,10 @@ ${fs.readFileSync(rulesPath, "utf-8")}
|
|
|
112
112
|
|
|
113
113
|
<task>
|
|
114
114
|
Identify high-impact issues related to:
|
|
115
|
-
1. SYSTEM COMPROMISE:
|
|
116
|
-
2. COMPUTATIONAL INEFFICIENCY: Algorithmic bottlenecks, memory leaks.
|
|
117
|
-
3. ARCHITECTURAL DEBT: Logic flaws, redundant complexity.
|
|
118
|
-
4. UI/UX ANOMALIES: Layout shifts, accessibility (a11y) violations, and CSS
|
|
115
|
+
1. SYSTEM COMPROMISE & SECURITY: Vulnerabilities (OWASP Top 10 API/Web standard compliance), hardcoded credentials, private keys, API secrets, or insecure default configurations.
|
|
116
|
+
2. COMPUTATIONAL INEFFICIENCY: Algorithmic bottlenecks, memory leaks, nested processing.
|
|
117
|
+
3. ARCHITECTURAL DEBT: Logic flaws, breaking structures, redundant modular complexity.
|
|
118
|
+
4. UI/UX ANOMALIES: Layout shifts, accessibility (a11y) standard violations, and inefficient CSS paths.
|
|
119
119
|
</task>
|
|
120
120
|
|
|
121
121
|
<instructions>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
const CACHE_FILE = path.join(".pika-reports", "cache.json");
|
|
5
|
+
/**
|
|
6
|
+
* Computes a unique SHA-256 hash representing the current files scan state and custom rules.
|
|
7
|
+
*/
|
|
8
|
+
export function computeScanHash(files, rulesContent) {
|
|
9
|
+
const hash = crypto.createHash("sha256");
|
|
10
|
+
// Sort files by path to ensure consistent hash ordering
|
|
11
|
+
const sortedFiles = [...files].sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
12
|
+
for (const file of sortedFiles) {
|
|
13
|
+
hash.update(`file:${file.filePath}\ncontent:${file.content}\n`);
|
|
14
|
+
}
|
|
15
|
+
hash.update(`rules:${rulesContent}`);
|
|
16
|
+
return hash.digest("hex");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves the cached scan finding results if it exists.
|
|
20
|
+
*/
|
|
21
|
+
export function getCache(hash) {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(CACHE_FILE))
|
|
24
|
+
return null;
|
|
25
|
+
const data = fs.readFileSync(CACHE_FILE, "utf-8");
|
|
26
|
+
const json = JSON.parse(data);
|
|
27
|
+
return json[hash] || null;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Caches scan finding results locally inside cache.json.
|
|
35
|
+
*/
|
|
36
|
+
export function setCache(hash, payload) {
|
|
37
|
+
try {
|
|
38
|
+
let cache = {};
|
|
39
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
40
|
+
const data = fs.readFileSync(CACHE_FILE, "utf-8");
|
|
41
|
+
cache = JSON.parse(data);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const dir = path.dirname(CACHE_FILE);
|
|
45
|
+
if (!fs.existsSync(dir)) {
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
cache[hash] = payload;
|
|
50
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// Fail silently to prevent cache issues from blocking core review execution
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/core/reporter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { calculateGrade, getGradeColor, generateBadge } from "./stats.js";
|
|
3
4
|
/**
|
|
4
5
|
* Helper to get a sanitized project name.
|
|
5
6
|
*/
|
|
@@ -80,6 +81,9 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
|
|
|
80
81
|
const projectName = getProjectName();
|
|
81
82
|
const timestamp = new Date().toLocaleString();
|
|
82
83
|
const data = JSON.stringify(allFindings);
|
|
84
|
+
const grade = calculateGrade(allFindings);
|
|
85
|
+
const gradeColor = getGradeColor(grade);
|
|
86
|
+
generateBadge(grade);
|
|
83
87
|
const htmlContent = `
|
|
84
88
|
<!DOCTYPE html>
|
|
85
89
|
<html lang="en">
|
|
@@ -502,7 +506,7 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
|
|
|
502
506
|
<polygon points="60,52 54,50 56,56" fill="#00F0FF" />
|
|
503
507
|
</svg>
|
|
504
508
|
<div class="brand-text">
|
|
505
|
-
<h1>Pika <span>Sentinel</span></h1>
|
|
509
|
+
<h1 style="display: flex; align-items: center; gap: 8px;">Pika <span>Sentinel</span> <span style="font-size: 0.8rem; background: ${gradeColor}; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: 800; text-shadow: none;">${grade}</span></h1>
|
|
506
510
|
<p>${projectName} • ${timestamp}</p>
|
|
507
511
|
</div>
|
|
508
512
|
</div>
|
package/dist/core/scanner.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getIgnoredFiles } from "../utils/config.js";
|
|
|
9
9
|
import { logger } from "../utils/logger.js";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { validateTokenLimit } from "../utils/token.js";
|
|
12
|
+
import { computeScanHash, getCache, setCache } from "./cache.js";
|
|
12
13
|
/**
|
|
13
14
|
* Project Discovery: List all relevant files for interactive selection.
|
|
14
15
|
*/
|
|
@@ -91,6 +92,35 @@ export async function runScan(target, isCI, specificFiles) {
|
|
|
91
92
|
findings: []
|
|
92
93
|
};
|
|
93
94
|
}
|
|
95
|
+
// Load custom architecture rules content
|
|
96
|
+
let rulesContent = "";
|
|
97
|
+
try {
|
|
98
|
+
const rulesPath = path.join(process.cwd(), ".pika-rules.md");
|
|
99
|
+
if (fs.existsSync(rulesPath)) {
|
|
100
|
+
rulesContent = fs.readFileSync(rulesPath, "utf-8");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (e) { }
|
|
104
|
+
// Gather target file content profiles
|
|
105
|
+
const fileStates = [];
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
try {
|
|
108
|
+
if (fs.existsSync(file)) {
|
|
109
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
110
|
+
fileStates.push({ filePath: file, content });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (e) { }
|
|
114
|
+
}
|
|
115
|
+
// Compute semantic hash and check database
|
|
116
|
+
const scanHash = computeScanHash(fileStates, rulesContent);
|
|
117
|
+
const cachedResult = getCache(scanHash);
|
|
118
|
+
if (cachedResult) {
|
|
119
|
+
if (!isCI) {
|
|
120
|
+
console.log(`\n ${chalk.cyan("✓")} ${chalk.bold("Instant Cache Hit: Loading results from local cache database (0ms).")}\n`);
|
|
121
|
+
}
|
|
122
|
+
return cachedResult;
|
|
123
|
+
}
|
|
94
124
|
const reportDir = setupReportDir();
|
|
95
125
|
if (!isCI)
|
|
96
126
|
logger.info(`Initializing Pika Review on ${files.length} file(s)...`);
|
|
@@ -197,9 +227,11 @@ export async function runScan(target, isCI, specificFiles) {
|
|
|
197
227
|
if (!isCI) {
|
|
198
228
|
logger.success("\nScan complete!");
|
|
199
229
|
}
|
|
200
|
-
|
|
230
|
+
const payload = {
|
|
201
231
|
markdownReports: generatedReports,
|
|
202
232
|
htmlReport: htmlPath,
|
|
203
233
|
findings: allFindings
|
|
204
234
|
};
|
|
235
|
+
setCache(scanHash, payload);
|
|
236
|
+
return payload;
|
|
205
237
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const BADGE_FILE = path.join(process.cwd(), "badge.svg");
|
|
4
|
+
/**
|
|
5
|
+
* Computes the architectural grade based on the severities of identified findings.
|
|
6
|
+
*/
|
|
7
|
+
export function calculateGrade(findings) {
|
|
8
|
+
let criticalCount = 0;
|
|
9
|
+
let highCount = 0;
|
|
10
|
+
let mediumCount = 0;
|
|
11
|
+
findings.forEach((f) => {
|
|
12
|
+
f.reviews.forEach((r) => {
|
|
13
|
+
const severity = r.severity;
|
|
14
|
+
if (severity === "Critical")
|
|
15
|
+
criticalCount++;
|
|
16
|
+
else if (severity === "High")
|
|
17
|
+
highCount++;
|
|
18
|
+
else if (severity === "Medium")
|
|
19
|
+
mediumCount++;
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
if (criticalCount >= 2)
|
|
23
|
+
return "F";
|
|
24
|
+
if (criticalCount === 1)
|
|
25
|
+
return "C";
|
|
26
|
+
if (highCount >= 3)
|
|
27
|
+
return "C";
|
|
28
|
+
if (highCount >= 1 || mediumCount >= 4)
|
|
29
|
+
return "B";
|
|
30
|
+
if (mediumCount >= 1)
|
|
31
|
+
return "A";
|
|
32
|
+
return "A+";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves the HSL color code corresponding to a Sentinel Grade.
|
|
36
|
+
*/
|
|
37
|
+
export function getGradeColor(grade) {
|
|
38
|
+
switch (grade) {
|
|
39
|
+
case "A+": return "#00F0FF"; // Neon Cyan
|
|
40
|
+
case "A": return "#10B981"; // Emerald Green
|
|
41
|
+
case "B": return "#3B82F6"; // Royal Blue
|
|
42
|
+
case "C": return "#F59E0B"; // Amber Yellow
|
|
43
|
+
case "F": return "#EF4444"; // Ruby Red
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Renders and saves a beautiful, high-quality, scalable SVG badge displaying the current Pika grade.
|
|
48
|
+
*/
|
|
49
|
+
export function generateBadge(grade) {
|
|
50
|
+
const color = getGradeColor(grade);
|
|
51
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="20" role="img" aria-label="Pika Sentinel: Grade ${grade}">
|
|
52
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
53
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
54
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
55
|
+
</linearGradient>
|
|
56
|
+
<clipPath id="r">
|
|
57
|
+
<rect width="140" height="20" rx="3" fill="#fff"/>
|
|
58
|
+
</clipPath>
|
|
59
|
+
<g clip-path="url(#r)">
|
|
60
|
+
<rect width="90" height="20" fill="#2d3748"/>
|
|
61
|
+
<rect x="90" width="50" height="20" fill="${color}"/>
|
|
62
|
+
<rect width="140" height="20" fill="url(#s)"/>
|
|
63
|
+
</g>
|
|
64
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,sans-serif" font-size="110">
|
|
65
|
+
<text aria-hidden="true" x="460" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="700">Pika Sentinel</text>
|
|
66
|
+
<text x="460" y="140" transform="scale(.1)" fill="#fff" textLength="700">Pika Sentinel</text>
|
|
67
|
+
<text aria-hidden="true" x="1150" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="300">${grade}</text>
|
|
68
|
+
<text x="1150" y="140" transform="scale(.1)" fill="#fff" textLength="300">${grade}</text>
|
|
69
|
+
</g>
|
|
70
|
+
</svg>`;
|
|
71
|
+
try {
|
|
72
|
+
const dir = path.dirname(BADGE_FILE);
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
fs.writeFileSync(BADGE_FILE, svg, "utf-8");
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
// Fail silently to keep stats errors isolated from blocking CLI scan operations
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
4
7
|
import { initAction } from "./cmd/init.js";
|
|
5
8
|
import { scanAction } from "./cmd/scan.js";
|
|
6
9
|
import { viewAction } from "./cmd/view.js";
|
|
@@ -8,15 +11,29 @@ import { statsAction } from "./cmd/stats.js";
|
|
|
8
11
|
import { modelsAction } from "./cmd/models.js";
|
|
9
12
|
import { hookAction } from "./cmd/hook.js";
|
|
10
13
|
import { rulesAction } from "./cmd/rules.js";
|
|
14
|
+
import { discussAction } from "./cmd/chat.js";
|
|
11
15
|
import { logger } from "./utils/logger.js";
|
|
12
16
|
/**
|
|
13
17
|
* Pika Review: The Enterprise-Grade AI Code Reviewer.
|
|
14
18
|
* Modular entry point for CLI operations.
|
|
15
19
|
*/
|
|
20
|
+
// Get package.json version dynamically
|
|
21
|
+
let version = "2.3.0";
|
|
22
|
+
try {
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
const pkgPath = path.join(__dirname, "../package.json");
|
|
26
|
+
if (fs.existsSync(pkgPath)) {
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
28
|
+
if (pkg.version)
|
|
29
|
+
version = pkg.version;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (e) { }
|
|
16
33
|
const program = new Command();
|
|
17
34
|
// Branding Header
|
|
18
35
|
const BRAND = `
|
|
19
|
-
${chalk.cyan.bold("◆ Pika Sentinel")} ${chalk.dim(`
|
|
36
|
+
${chalk.cyan.bold("◆ Pika Sentinel")} ${chalk.dim(`v${version} (Enterprise)`)}
|
|
20
37
|
${chalk.dim("─".repeat(42))}
|
|
21
38
|
${chalk.italic.gray("AI Architectural & Security Safeguard")}
|
|
22
39
|
`;
|
|
@@ -27,9 +44,10 @@ const HELPER_TEXT = `
|
|
|
27
44
|
-i, --interactive Interactively pick files to scan
|
|
28
45
|
--ci Fail CI pipeline if critical/high issues are found
|
|
29
46
|
${chalk.bold("view")} Open the latest interactive HTML report in browser
|
|
47
|
+
${chalk.bold("discuss [file]")} Launch an interactive Socratic chat session inside the console
|
|
30
48
|
${chalk.bold("stats")} Print scan history & key quality trends
|
|
31
49
|
${chalk.bold("models")} Interactively select Ollama models for offline audit
|
|
32
|
-
${chalk.bold("hook")} Install Git pre-commit
|
|
50
|
+
${chalk.bold("hook")} Install Git pre-commit safeguard hook
|
|
33
51
|
${chalk.bold("rules")} AI-generate architectural '.pika-rules.md'
|
|
34
52
|
|
|
35
53
|
${chalk.cyan.bold("Options & Advanced:")}
|
|
@@ -40,7 +58,7 @@ const HELPER_TEXT = `
|
|
|
40
58
|
program
|
|
41
59
|
.name("pika-review")
|
|
42
60
|
.description("Enterprise-grade AI Architectural Code Reviewer")
|
|
43
|
-
.version(
|
|
61
|
+
.version(version)
|
|
44
62
|
.addHelpText("before", BRAND)
|
|
45
63
|
.addHelpText("after", HELPER_TEXT);
|
|
46
64
|
program
|
|
@@ -96,6 +114,13 @@ program
|
|
|
96
114
|
logger.info("Use 'pika-review rules --generate' to auto-generate architecture rules.");
|
|
97
115
|
}
|
|
98
116
|
});
|
|
117
|
+
program
|
|
118
|
+
.command("discuss <file>")
|
|
119
|
+
.description("Launch an interactive Socratic chat session inside the console focusing on design context")
|
|
120
|
+
.action(async (file) => {
|
|
121
|
+
console.log(BRAND);
|
|
122
|
+
await discussAction(file);
|
|
123
|
+
});
|
|
99
124
|
program
|
|
100
125
|
.command("scan [files...]", { isDefault: true }) // Set scan as the default command
|
|
101
126
|
.description("Scan git changes or specific files for architectural anomalies")
|