pika-review 2.1.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 +56 -9
- package/dist/cmd/autofix.js +61 -0
- package/dist/cmd/chat.js +87 -0
- package/dist/cmd/github.js +79 -0
- package/dist/cmd/hook.js +85 -0
- package/dist/cmd/models.js +68 -0
- package/dist/cmd/rules.js +131 -0
- package/dist/cmd/scan.js +95 -14
- package/dist/core/ai.js +23 -8
- package/dist/core/cache.js +55 -0
- package/dist/core/reporter.js +335 -83
- package/dist/core/scanner.js +44 -4
- package/dist/core/stats.js +81 -0
- package/dist/index.js +79 -19
- package/dist/utils/config.js +17 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Pika Review š¦
|
|
1
|
+
# Pika Review š¦ [](https://github.com/HackX-IN/pika-review)
|
|
2
2
|
|
|
3
3
|
### Enterprise-grade AI Architectural Sentinel & Compliance Engine
|
|
4
4
|
|
|
@@ -11,12 +11,15 @@ Pika Review is a high-performance CLI tool designed to perform surgical code rev
|
|
|
11
11
|
## š Key Features
|
|
12
12
|
|
|
13
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.
|
|
14
17
|
- **š Health Dashboard**: Track architectural health trends over time with `pika-review stats`.
|
|
15
18
|
- **šØ Premium Interactive Reports**: Immersive, dark-mode HTML reports for deep triage.
|
|
16
19
|
- **š Smart Context Scanning**: Uses `-U10` context windows for higher AI reasoning accuracy.
|
|
17
20
|
- **š”ļø "Pika-Ignore" support**: Suppress specific lines using `// pika-ignore` comments.
|
|
18
21
|
- **ā” Parallel Orchestration**: Scans multiple files concurrently with `p-limit`.
|
|
19
|
-
- **š Provider Agnostic**: Works with OpenAI, Claude, Grok, or any OpenAI-compatible endpoint.
|
|
22
|
+
- **š Provider Agnostic**: Works with OpenAI, Claude, Grok, local Ollama, or any OpenAI-compatible endpoint.
|
|
20
23
|
|
|
21
24
|
---
|
|
22
25
|
|
|
@@ -80,13 +83,57 @@ pika-review stats
|
|
|
80
83
|
|
|
81
84
|
## š Usage
|
|
82
85
|
|
|
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
|
|
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:
|
|
105
|
+
```bash
|
|
106
|
+
pika-review init
|
|
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:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
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
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
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.
|
|
90
137
|
|
|
91
138
|
---
|
|
92
139
|
|
|
@@ -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/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,131 @@
|
|
|
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
|
+
const frameworkClassifications = [];
|
|
50
|
+
try {
|
|
51
|
+
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
52
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
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");
|
|
67
|
+
const deps = Object.keys(packageJson.dependencies || {}).join(", ");
|
|
68
|
+
const devDeps = Object.keys(packageJson.devDependencies || {}).join(", ");
|
|
69
|
+
projectTechStack += `Package Name: ${packageJson.name || "unknown"}\nDependencies: ${deps || "none"}\nDevDependencies: ${devDeps || "none"}\n`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) { }
|
|
73
|
+
if (frameworkClassifications.length > 0) {
|
|
74
|
+
projectTechStack += `Detected Frameworks & Archetypes: ${frameworkClassifications.join(", ")}\n`;
|
|
75
|
+
}
|
|
76
|
+
// 2. Gather file layout context
|
|
77
|
+
const files = listProjectFiles();
|
|
78
|
+
const fileSummary = files.slice(0, 30).map(f => ` - ${f}`).join("\n");
|
|
79
|
+
const projectStructure = `Detected files (subset):\n${fileSummary}\nTotal files discovered: ${files.length}\n`;
|
|
80
|
+
logger.info(`Synthesizing tailored rules using AI model (${chalk.green(config.ai.model)})...`);
|
|
81
|
+
const prompt = `You are an Elite Senior Software Architect and Compliance Officer.
|
|
82
|
+
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:
|
|
83
|
+
|
|
84
|
+
<project_tech_stack>
|
|
85
|
+
${projectTechStack || "Generic web/software codebase"}
|
|
86
|
+
</project_tech_stack>
|
|
87
|
+
|
|
88
|
+
<project_structure>
|
|
89
|
+
${projectStructure}
|
|
90
|
+
</project_structure>
|
|
91
|
+
|
|
92
|
+
Instructions:
|
|
93
|
+
- Write the output in clean, professional Markdown format.
|
|
94
|
+
- 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).
|
|
95
|
+
- Provide a brief justification for each rule.
|
|
96
|
+
- Do NOT output any preamble, greeting, markdown backticks wrapper, or postscript.
|
|
97
|
+
- Start directly with "# Pika Architectural Rules" and start rules lists immediately.`;
|
|
98
|
+
const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
|
|
99
|
+
? config.ai.baseURL
|
|
100
|
+
: (isOllama
|
|
101
|
+
? "http://localhost:11434/v1"
|
|
102
|
+
: `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`);
|
|
103
|
+
const openai = new OpenAI({
|
|
104
|
+
apiKey: config.ai.apiKey || "ollama",
|
|
105
|
+
baseURL,
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
const response = await openai.chat.completions.create({
|
|
109
|
+
model: config.ai.model,
|
|
110
|
+
messages: [{ role: "user", content: prompt }],
|
|
111
|
+
temperature: 0.3,
|
|
112
|
+
max_tokens: 1500,
|
|
113
|
+
});
|
|
114
|
+
let rawMarkdown = response.choices[0]?.message?.content || "";
|
|
115
|
+
// Clean code block ticks if LLM ignored instructions
|
|
116
|
+
rawMarkdown = rawMarkdown
|
|
117
|
+
.replace(/```markdown/g, "")
|
|
118
|
+
.replace(/```/g, "")
|
|
119
|
+
.trim();
|
|
120
|
+
if (!rawMarkdown.startsWith("#")) {
|
|
121
|
+
rawMarkdown = `# Pika Architectural Rules\n\n${rawMarkdown}`;
|
|
122
|
+
}
|
|
123
|
+
fs.writeFileSync(rulesPath, rawMarkdown, "utf-8");
|
|
124
|
+
logger.success(".pika-rules.md generated successfully!");
|
|
125
|
+
logger.info(`Location: ${chalk.dim(rulesPath)}`);
|
|
126
|
+
logger.dim("The local Pika Review engine will now enforce these rules during future scans!");
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
logger.error(`Failed to generate rules: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|