pika-review 2.0.1 → 2.2.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 +97 -29
- package/dist/cmd/hook.js +85 -0
- package/dist/cmd/models.js +68 -0
- package/dist/cmd/rules.js +114 -0
- package/dist/cmd/scan.js +79 -31
- package/dist/cmd/stats.js +55 -0
- package/dist/cmd/view.js +55 -0
- package/dist/core/ai.js +45 -11
- package/dist/core/reporter.js +691 -6
- package/dist/core/scanner.js +49 -13
- package/dist/index.js +72 -17
- package/dist/utils/config.js +56 -11
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
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
|
+
- **🦙 Native Local Ollama Support**: Run 100% private, free, offline scans using local LLMs (e.g. Qwen, Llama).
|
|
15
|
+
- **🎛️ Local Model Selector**: Interactively choose and configure local models from the command line.
|
|
16
|
+
- **🛡️ Git Commit Safeguard Hook**: Prevent pushing high-severity compliance debt and vulnerabilities automatically.
|
|
17
|
+
- **📊 Health Dashboard**: Track architectural health trends over time with `pika-review stats`.
|
|
18
|
+
- **🎨 Premium Interactive Reports**: Immersive, dark-mode HTML reports for deep triage.
|
|
19
|
+
- **🔍 Smart Context Scanning**: Uses `-U10` context windows for higher AI reasoning accuracy.
|
|
20
|
+
- **🛡️ "Pika-Ignore" support**: Suppress specific lines using `// pika-ignore` comments.
|
|
21
|
+
- **⚡ Parallel Orchestration**: Scans multiple files concurrently with `p-limit`.
|
|
22
|
+
- **🌍 Provider Agnostic**: Works with OpenAI, Claude, Grok, local Ollama, or any OpenAI-compatible endpoint.
|
|
16
23
|
|
|
17
24
|
---
|
|
18
25
|
|
|
19
26
|
## 📦 Installation
|
|
27
|
+
|
|
20
28
|
```bash
|
|
21
29
|
# Via Bun (Recommended)
|
|
22
30
|
bun add -g pika-review
|
|
@@ -26,53 +34,113 @@ npm install -g pika-review
|
|
|
26
34
|
```
|
|
27
35
|
|
|
28
36
|
## 🛠️ Setup
|
|
37
|
+
|
|
29
38
|
1. Initialize your configuration:
|
|
30
39
|
```bash
|
|
31
40
|
pika-review init
|
|
32
41
|
```
|
|
33
|
-
2.
|
|
42
|
+
2. Configure your AI provider in `~/.pika-review.yaml`:
|
|
34
43
|
```yaml
|
|
35
44
|
ai:
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
apiKey: "your-api-token"
|
|
46
|
+
model: "gpt-4o" # or your preferred model
|
|
47
|
+
baseURL: "https://api.openai.com/v1"
|
|
38
48
|
```
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🏗️ Enterprise Features
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
### 1. Architecture Rules Engine
|
|
55
|
+
|
|
56
|
+
Create a `.pika-rules.md` file in your repository root to guide the AI with project-specific context:
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
# Architectural Rules
|
|
60
|
+
|
|
61
|
+
- Use Functional Components with Hooks, never Class Components.
|
|
62
|
+
- All service calls must go through `src/api/client.ts`.
|
|
63
|
+
- Database queries are restricted to the Repository layer.
|
|
48
64
|
```
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
### 2. Pika-Ignore
|
|
67
|
+
|
|
68
|
+
Suppress false positives or acknowledged risks directly in your code:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const secret = "12345"; // pika-ignore (intentional for testing)
|
|
54
72
|
```
|
|
55
73
|
|
|
56
|
-
|
|
74
|
+
### 3. Health Stats
|
|
75
|
+
|
|
76
|
+
Monitor your codebase's architectural debt over time:
|
|
77
|
+
|
|
57
78
|
```bash
|
|
58
|
-
pika-review
|
|
79
|
+
pika-review stats
|
|
59
80
|
```
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🔍 Usage
|
|
85
|
+
|
|
86
|
+
| Command | Description |
|
|
87
|
+
| :---------------------- | :----------------------------------------------------------- |
|
|
88
|
+
| `pika-review scan` | Scan staged git changes (Default) |
|
|
89
|
+
| `pika-review scan -i` | Interactive file selection mode |
|
|
90
|
+
| `pika-review view` | Open the latest interactive report |
|
|
91
|
+
| `pika-review stats` | View architectural health trends and scan dashboard |
|
|
92
|
+
| `pika-review models` | Interactively select and configure local Ollama models |
|
|
93
|
+
| `pika-review hook <act>`| Install (`install`) or uninstall (`uninstall`) Git safeguards |
|
|
94
|
+
| `pika-review rules -g` | Auto-generate architectural `.pika-rules.md` guidelines |
|
|
95
|
+
| `pika-review scan --ci` | Fail pipeline on Critical/High issues |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🦙 Local Ollama & Offline Setup
|
|
100
|
+
|
|
101
|
+
Pika Review fully supports local, 100% private, offline code reviews via [Ollama](https://ollama.com).
|
|
102
|
+
|
|
103
|
+
### 1. Configure Ollama Provider
|
|
104
|
+
Initialize your configuration:
|
|
62
105
|
```bash
|
|
63
|
-
pika-review
|
|
106
|
+
pika-review init
|
|
64
107
|
```
|
|
108
|
+
Open `~/.pika-review.yaml` and configure the Ollama provider:
|
|
109
|
+
```yaml
|
|
110
|
+
ai:
|
|
111
|
+
provider: "ollama"
|
|
112
|
+
model: "qwen2.5-coder:7b" # Or your pulled model
|
|
113
|
+
baseURL: "http://localhost:11434/v1"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 2. Interactive Local Setup
|
|
117
|
+
To avoid editing files manually, you can manage everything directly from the command line:
|
|
65
118
|
|
|
66
|
-
### CI/CD Mode
|
|
67
|
-
Use the `--ci` flag in GitHub Actions or other pipelines to fail if Critical or High severity issues are found:
|
|
68
119
|
```bash
|
|
69
|
-
|
|
120
|
+
# Discover local pulled models and switch active model instantly
|
|
121
|
+
pika-review models
|
|
122
|
+
|
|
123
|
+
# Automatically bootstrap customized architectural rules for your tech stack
|
|
124
|
+
pika-review rules --generate
|
|
125
|
+
|
|
126
|
+
# Register git safeguard hooks to run scans automatically before commits
|
|
127
|
+
pika-review hook install
|
|
70
128
|
```
|
|
71
129
|
|
|
72
130
|
---
|
|
73
131
|
|
|
132
|
+
## 🤝 Contributing
|
|
133
|
+
|
|
134
|
+
We welcome contributions from the community to help make Pika Review the ultimate architectural code reviewer!
|
|
135
|
+
|
|
136
|
+
Please read our **[Contributing Guide](CONTRIBUTING.md)** for details on how to set up local development, run compiler checks, write custom CLI commands, and submit high-quality Pull Requests.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
74
140
|
## 🛡️ Privacy & Security
|
|
75
|
-
|
|
141
|
+
|
|
142
|
+
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
143
|
|
|
77
144
|
## 📄 License
|
|
145
|
+
|
|
78
146
|
MIT © Pika Review Contributors
|
package/dist/cmd/hook.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* CLI Command Action: hook [action]
|
|
7
|
+
* Manages the Git pre-commit hook installation/removal.
|
|
8
|
+
*/
|
|
9
|
+
export async function hookAction(action) {
|
|
10
|
+
const gitPath = path.join(process.cwd(), ".git");
|
|
11
|
+
if (!fs.existsSync(gitPath)) {
|
|
12
|
+
logger.error("Not a Git repository.");
|
|
13
|
+
logger.dim("Pika Review Git hooks can only be installed in the root of a Git repository.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const hooksPath = path.join(gitPath, "hooks");
|
|
17
|
+
const hookFile = path.join(hooksPath, "pre-commit");
|
|
18
|
+
// Ensure hooks directory exists
|
|
19
|
+
if (!fs.existsSync(hooksPath)) {
|
|
20
|
+
fs.mkdirSync(hooksPath, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
const HOOK_SCRIPT = `#!/bin/sh
|
|
23
|
+
# Pika Review - Architectural Sentinel Git Safeguard Hook
|
|
24
|
+
echo "🦊 Running local architectural compliance checks..."
|
|
25
|
+
pika-review scan --ci
|
|
26
|
+
|
|
27
|
+
EXIT_CODE=$?
|
|
28
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
29
|
+
echo "❌ [Pika Review] Commit rejected due to high-severity compliance anomalies."
|
|
30
|
+
exit $EXIT_CODE
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
34
|
+
`;
|
|
35
|
+
if (action === "install") {
|
|
36
|
+
let backupCreated = false;
|
|
37
|
+
if (fs.existsSync(hookFile)) {
|
|
38
|
+
const content = fs.readFileSync(hookFile, "utf-8");
|
|
39
|
+
if (content.includes("Pika Review")) {
|
|
40
|
+
logger.info("Pika Review hook is already installed!");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Create backup of existing hook
|
|
44
|
+
const backupPath = `${hookFile}.bak`;
|
|
45
|
+
fs.writeFileSync(backupPath, content);
|
|
46
|
+
backupCreated = true;
|
|
47
|
+
logger.info(`Existing hook backed up to ${chalk.cyan("pre-commit.bak")}`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(hookFile, HOOK_SCRIPT, { mode: 0o755 });
|
|
51
|
+
logger.success("Git pre-commit hook installed successfully!");
|
|
52
|
+
logger.info(`Path: ${chalk.dim(hookFile)}`);
|
|
53
|
+
logger.dim("Pika Review will now scan your staged files automatically before every commit.");
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
logger.error(`Failed to install Git hook: ${e.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (action === "uninstall") {
|
|
60
|
+
if (!fs.existsSync(hookFile)) {
|
|
61
|
+
logger.warn("No pre-commit hook found to uninstall.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const content = fs.readFileSync(hookFile, "utf-8");
|
|
65
|
+
if (!content.includes("Pika Review")) {
|
|
66
|
+
logger.warn("The existing pre-commit hook was not installed by Pika Review. Leaving it untouched.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
fs.unlinkSync(hookFile);
|
|
71
|
+
logger.success("Pika Review Git pre-commit hook uninstalled successfully.");
|
|
72
|
+
// Restore backup if it exists
|
|
73
|
+
const backupPath = `${hookFile}.bak`;
|
|
74
|
+
if (fs.existsSync(backupPath)) {
|
|
75
|
+
fs.renameSync(backupPath, hookFile);
|
|
76
|
+
// Make sure it remains executable
|
|
77
|
+
fs.chmodSync(hookFile, 0o755);
|
|
78
|
+
logger.success("Restored previous pre-commit hook from backup!");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
logger.error(`Failed to uninstall Git hook: ${e.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getConfig, saveConfig } from "../utils/config.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* CLI Command Action: models
|
|
7
|
+
* Queries local Ollama daemon for available models and updates active configuration.
|
|
8
|
+
*/
|
|
9
|
+
export async function modelsAction() {
|
|
10
|
+
const config = getConfig();
|
|
11
|
+
// Extract base host from configuration baseURL or fallback to localhost
|
|
12
|
+
let host = "http://localhost:11434";
|
|
13
|
+
if (config.ai.baseURL) {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(config.ai.baseURL);
|
|
16
|
+
host = `${url.protocol}//${url.host}`;
|
|
17
|
+
}
|
|
18
|
+
catch (e) { }
|
|
19
|
+
}
|
|
20
|
+
logger.info(`Connecting to Ollama daemon at ${chalk.cyan(host)}...`);
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${host}/api/tags`);
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(`Server returned HTTP ${res.status}`);
|
|
25
|
+
}
|
|
26
|
+
const data = (await res.json());
|
|
27
|
+
if (!data.models || data.models.length === 0) {
|
|
28
|
+
logger.warn(`No models found inside your local Ollama instance.`);
|
|
29
|
+
logger.info(`Try pulling a model first using: ${chalk.yellow("ollama pull llama3")}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const choices = data.models.map((m) => {
|
|
33
|
+
const sizeGB = m.size ? `${(m.size / (1024 * 1024 * 1024)).toFixed(2)} GB` : "Unknown size";
|
|
34
|
+
const paramSize = m.details?.parameter_size ? ` [${m.details.parameter_size}]` : "";
|
|
35
|
+
return {
|
|
36
|
+
title: `${chalk.bold(m.name)} ${chalk.dim(`(${sizeGB}${paramSize})`)}`,
|
|
37
|
+
value: m.name,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
const response = await prompts({
|
|
41
|
+
type: "select",
|
|
42
|
+
name: "model",
|
|
43
|
+
message: "Select your active local Ollama model:",
|
|
44
|
+
choices,
|
|
45
|
+
initial: 0,
|
|
46
|
+
});
|
|
47
|
+
if (response.model) {
|
|
48
|
+
config.ai.provider = "ollama";
|
|
49
|
+
config.ai.model = response.model;
|
|
50
|
+
// Auto-set standard baseURL if empty or not set to localhost
|
|
51
|
+
if (!config.ai.baseURL || config.ai.baseURL.includes("cloudflare")) {
|
|
52
|
+
config.ai.baseURL = "http://localhost:11434/v1";
|
|
53
|
+
}
|
|
54
|
+
saveConfig(config);
|
|
55
|
+
logger.success(`Configuration updated successfully!`);
|
|
56
|
+
logger.info(`Active Model: ${chalk.green(response.model)}`);
|
|
57
|
+
logger.info(`AI Provider: ${chalk.cyan("ollama")}`);
|
|
58
|
+
logger.info(`API Base URL: ${chalk.cyan(config.ai.baseURL)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.error(`Could not connect to Ollama daemon.`);
|
|
63
|
+
console.log(chalk.dim(`\n💡 To solve this, make sure:`));
|
|
64
|
+
console.log(` 1. Ollama is installed on your machine (https://ollama.com).`);
|
|
65
|
+
console.log(` 2. The daemon is running (${chalk.yellow("ollama serve")} or desktop application).`);
|
|
66
|
+
console.log(` 3. Your config baseURL is pointing to your active daemon (current: ${chalk.red(host)}).\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import OpenAI from "openai";
|
|
6
|
+
import { getConfig } from "../utils/config.js";
|
|
7
|
+
import { listProjectFiles } from "../core/scanner.js";
|
|
8
|
+
import { logger } from "../utils/logger.js";
|
|
9
|
+
/**
|
|
10
|
+
* CLI Command Action: rules generate
|
|
11
|
+
* Uses the configured AI provider to bootstrap architectural rules tailored to the current codebase.
|
|
12
|
+
*/
|
|
13
|
+
export async function rulesAction() {
|
|
14
|
+
const config = getConfig();
|
|
15
|
+
const isOllama = config.ai.provider === "ollama";
|
|
16
|
+
if (!isOllama && (!config.ai.apiKey || !config.ai.accountId)) {
|
|
17
|
+
logger.error("Missing AI credentials in ~/.pika-review.yaml.");
|
|
18
|
+
logger.info("Run 'pika-review init' to initialize config or set provider to 'ollama' for offline mode.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const rulesPath = path.join(process.cwd(), ".pika-rules.md");
|
|
22
|
+
if (fs.existsSync(rulesPath)) {
|
|
23
|
+
const overwrite = await prompts({
|
|
24
|
+
type: "confirm",
|
|
25
|
+
name: "confirm",
|
|
26
|
+
message: "An existing .pika-rules.md already exists. Do you want to overwrite it?",
|
|
27
|
+
initial: false,
|
|
28
|
+
});
|
|
29
|
+
if (!overwrite.confirm) {
|
|
30
|
+
logger.info("Operation cancelled.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const confirm = await prompts({
|
|
36
|
+
type: "confirm",
|
|
37
|
+
name: "confirm",
|
|
38
|
+
message: "Generate a custom, AI-crafted .pika-rules.md architectural guidelines guide for this codebase?",
|
|
39
|
+
initial: true,
|
|
40
|
+
});
|
|
41
|
+
if (!confirm.confirm) {
|
|
42
|
+
logger.info("Operation cancelled.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
logger.info("Analyzing codebase structure and dependencies...");
|
|
47
|
+
// 1. Gather package.json context
|
|
48
|
+
let projectTechStack = "";
|
|
49
|
+
try {
|
|
50
|
+
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
51
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
52
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
53
|
+
const deps = Object.keys(packageJson.dependencies || {}).join(", ");
|
|
54
|
+
const devDeps = Object.keys(packageJson.devDependencies || {}).join(", ");
|
|
55
|
+
projectTechStack += `Package Name: ${packageJson.name || "unknown"}\nDependencies: ${deps || "none"}\nDevDependencies: ${devDeps || "none"}\n`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) { }
|
|
59
|
+
// 2. Gather file layout context
|
|
60
|
+
const files = listProjectFiles();
|
|
61
|
+
const fileSummary = files.slice(0, 30).map(f => ` - ${f}`).join("\n");
|
|
62
|
+
const projectStructure = `Detected files (subset):\n${fileSummary}\nTotal files discovered: ${files.length}\n`;
|
|
63
|
+
logger.info(`Synthesizing tailored rules using AI model (${chalk.green(config.ai.model)})...`);
|
|
64
|
+
const prompt = `You are an Elite Senior Software Architect and Compliance Officer.
|
|
65
|
+
Your task is to write a highly detailed, professional, and practical architectural rules file called ".pika-rules.md" for a project with the following tech stack and structure:
|
|
66
|
+
|
|
67
|
+
<project_tech_stack>
|
|
68
|
+
${projectTechStack || "Generic web/software codebase"}
|
|
69
|
+
</project_tech_stack>
|
|
70
|
+
|
|
71
|
+
<project_structure>
|
|
72
|
+
${projectStructure}
|
|
73
|
+
</project_structure>
|
|
74
|
+
|
|
75
|
+
Instructions:
|
|
76
|
+
- Write the output in clean, professional Markdown format.
|
|
77
|
+
- Establish 5 to 8 strict, concrete, actionable architectural rules tailored to the libraries, patterns, and folder layout of this specific project (e.g. naming conventions, folder segregation, API patterns, state management, security).
|
|
78
|
+
- Provide a brief justification for each rule.
|
|
79
|
+
- Do NOT output any preamble, greeting, markdown backticks wrapper, or postscript.
|
|
80
|
+
- Start directly with "# Pika Architectural Rules" and start rules lists immediately.`;
|
|
81
|
+
const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
|
|
82
|
+
? config.ai.baseURL
|
|
83
|
+
: (isOllama
|
|
84
|
+
? "http://localhost:11434/v1"
|
|
85
|
+
: `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`);
|
|
86
|
+
const openai = new OpenAI({
|
|
87
|
+
apiKey: config.ai.apiKey || "ollama",
|
|
88
|
+
baseURL,
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
const response = await openai.chat.completions.create({
|
|
92
|
+
model: config.ai.model,
|
|
93
|
+
messages: [{ role: "user", content: prompt }],
|
|
94
|
+
temperature: 0.3,
|
|
95
|
+
max_tokens: 1500,
|
|
96
|
+
});
|
|
97
|
+
let rawMarkdown = response.choices[0]?.message?.content || "";
|
|
98
|
+
// Clean code block ticks if LLM ignored instructions
|
|
99
|
+
rawMarkdown = rawMarkdown
|
|
100
|
+
.replace(/```markdown/g, "")
|
|
101
|
+
.replace(/```/g, "")
|
|
102
|
+
.trim();
|
|
103
|
+
if (!rawMarkdown.startsWith("#")) {
|
|
104
|
+
rawMarkdown = `# Pika Architectural Rules\n\n${rawMarkdown}`;
|
|
105
|
+
}
|
|
106
|
+
fs.writeFileSync(rulesPath, rawMarkdown, "utf-8");
|
|
107
|
+
logger.success(".pika-rules.md generated successfully!");
|
|
108
|
+
logger.info(`Location: ${chalk.dim(rulesPath)}`);
|
|
109
|
+
logger.dim("The local Pika Review engine will now enforce these rules during future scans!");
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
logger.error(`Failed to generate rules: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/cmd/scan.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import prompts from "prompts";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import { execSync } from "child_process";
|
|
3
4
|
import { runScan, getDiffFiles, listProjectFiles } from "../core/scanner.js";
|
|
4
5
|
import { logger } from "../utils/logger.js";
|
|
5
6
|
/**
|
|
@@ -10,55 +11,102 @@ export async function scanAction(files, options) {
|
|
|
10
11
|
const target = options.unstaged ? "unstaged" : "staged";
|
|
11
12
|
const isCI = !!options.ci;
|
|
12
13
|
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
|
-
|
|
14
|
+
// Interactive Discovery: If requested or if no files provided and no git changes
|
|
15
|
+
if (!isCI && (options.interactive || (!targets && getDiffFiles(target).length === 0))) {
|
|
16
|
+
logger.info("Entering interactive selection mode...");
|
|
17
|
+
const allFiles = listProjectFiles();
|
|
18
|
+
if (allFiles.length > 0) {
|
|
19
|
+
const selection = await prompts({
|
|
20
|
+
type: "multiselect",
|
|
21
|
+
name: "files",
|
|
22
|
+
message: "Select files to analyze (Space to select, Enter to confirm):",
|
|
23
|
+
choices: allFiles.map(f => ({ title: f, value: f })),
|
|
24
|
+
hint: "- Space to select. Return to submit",
|
|
25
|
+
instructions: false
|
|
26
|
+
});
|
|
27
|
+
if (selection.files && selection.files.length > 0) {
|
|
28
|
+
targets = selection.files;
|
|
29
|
+
}
|
|
30
|
+
else if (options.interactive) {
|
|
31
|
+
logger.warn("No files selected. Exiting.");
|
|
32
|
+
return;
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
|
-
|
|
34
|
-
if (!
|
|
36
|
+
const { markdownReports, htmlReport, findings } = await runScan(target, isCI, targets);
|
|
37
|
+
if (!markdownReports || markdownReports.length === 0) {
|
|
35
38
|
if (!isCI) {
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
console.log(`\n ${chalk.green("✓")} ${chalk.bold("Scan complete. No architectural anomalies or security risks detected.")}`);
|
|
40
|
+
console.log(` ${chalk.dim("Try scanning with '--unstaged' if you have unstaged changes.")}\n`);
|
|
38
41
|
}
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
44
|
if (!isCI) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
// Count findings by severity
|
|
46
|
+
let criticalCount = 0;
|
|
47
|
+
let highCount = 0;
|
|
48
|
+
let mediumCount = 0;
|
|
49
|
+
let lowCount = 0;
|
|
50
|
+
let totalIssues = 0;
|
|
51
|
+
findings.forEach((f) => {
|
|
52
|
+
totalIssues += f.reviews.length;
|
|
53
|
+
f.reviews.forEach((r) => {
|
|
54
|
+
if (r.severity === "Critical")
|
|
55
|
+
criticalCount++;
|
|
56
|
+
else if (r.severity === "High")
|
|
57
|
+
highCount++;
|
|
58
|
+
else if (r.severity === "Medium")
|
|
59
|
+
mediumCount++;
|
|
60
|
+
else if (r.severity === "Low")
|
|
61
|
+
lowCount++;
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
// Sleek summary box
|
|
65
|
+
console.log(`\n ${chalk.cyan.bold("◆ Scan Completed Successfully")}`);
|
|
66
|
+
console.log(` ${chalk.dim("─".repeat(34))}`);
|
|
67
|
+
console.log(` ${chalk.bold("Files Audited:")} ${findings.length}`);
|
|
68
|
+
console.log(` ${chalk.bold("Total Findings:")} ${totalIssues}`);
|
|
69
|
+
if (totalIssues > 0) {
|
|
70
|
+
console.log(`\n ${chalk.bold("Severity Breakdown:")}`);
|
|
71
|
+
if (criticalCount > 0)
|
|
72
|
+
console.log(` 🚨 ${chalk.red.bold(criticalCount)} Critical`);
|
|
73
|
+
if (highCount > 0)
|
|
74
|
+
console.log(` 🔥 ${chalk.yellow.bold(highCount)} High`);
|
|
75
|
+
if (mediumCount > 0)
|
|
76
|
+
console.log(` ⚠️ ${chalk.blue.bold(mediumCount)} Medium`);
|
|
77
|
+
if (lowCount > 0)
|
|
78
|
+
console.log(` 📝 ${chalk.gray.bold(lowCount)} Low`);
|
|
79
|
+
}
|
|
80
|
+
console.log(`\n ${chalk.bold("Obsidian Report:")} ${chalk.cyan.underline(`file://${htmlReport}`)}\n`);
|
|
45
81
|
const response = await prompts({
|
|
46
82
|
type: "select",
|
|
47
83
|
name: "action",
|
|
48
|
-
message: "
|
|
84
|
+
message: "Choose post-scan action:",
|
|
49
85
|
choices: [
|
|
50
|
-
{ title: "
|
|
86
|
+
{ title: "✨ Open Interactive HTML Dashboard (Recommended)", value: "html" },
|
|
87
|
+
{ title: "📂 List generated Markdown file paths", value: "markdown" },
|
|
51
88
|
{ title: "🛑 Exit and start refactoring", value: "exit" },
|
|
52
89
|
],
|
|
53
90
|
initial: 0,
|
|
54
91
|
});
|
|
55
|
-
if (response.action === "
|
|
56
|
-
|
|
57
|
-
|
|
92
|
+
if (response.action === "html") {
|
|
93
|
+
console.log(`\n ${chalk.cyan("→")} Opening interactive report in browser...`);
|
|
94
|
+
try {
|
|
95
|
+
const command = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
96
|
+
execSync(`${command} "${htmlReport}"`);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.log(` ${chalk.red("✖")} Failed to open browser automatically.`);
|
|
100
|
+
console.log(` ${chalk.dim(`Manually open: file://${htmlReport}`)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (response.action === "markdown") {
|
|
104
|
+
console.log(`\n ${chalk.bold("Generated Markdown Artifacts:")}`);
|
|
105
|
+
markdownReports.forEach((r) => {
|
|
58
106
|
const fileName = r.split("/").pop();
|
|
59
|
-
console.log(`
|
|
107
|
+
console.log(` ${chalk.cyan("•")} ${chalk.bold(fileName)} ${chalk.dim(`(file://${r})`)}`);
|
|
60
108
|
});
|
|
61
|
-
console.log(chalk.dim("
|
|
109
|
+
console.log(`\n ${chalk.dim("Tip: Click any file:// link above to open directly in your editor.")}\n`);
|
|
62
110
|
}
|
|
63
111
|
}
|
|
64
112
|
}
|
|
@@ -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
|
+
}
|