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 CHANGED
@@ -1,22 +1,30 @@
1
1
  # Pika Review 🦊
2
- ### Enterprise-grade AI Architectural Sentinel
3
2
 
4
- 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 UI/UX anomalies that traditional linters miss.
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
- - **Provider Agnostic**: Seamlessly works with Cloudflare Workers AI, OpenAI, Grok, or local LLMs via OpenAI-compatible endpoints.
12
- - **Mixture of Experts (MoE) Reasoning**: Leverages advanced LLMs for deep structural analysis.
13
- - **Polyglot Heuristics**: Idiom-aware reviews across Python, JS/TS, Go, Rust, and React.
14
- - **Interactive UI**: Claude-inspired terminal experience with real-time progress and multi-select discovery.
15
- - **Enterprise-Ready**: Built-in token safety, JSON self-healing, and CI/CD integration.
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. Open `~/.pika-review.yaml` and add your Cloudflare credentials:
42
+ 2. Configure your AI provider in `~/.pika-review.yaml`:
34
43
  ```yaml
35
44
  ai:
36
- accountId: "your-account-id"
37
- apiKey: "your-scoped-api-token"
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
- ### 🌍 Custom AI Providers (OpenAI, Grok, etc.)
41
- Pika Review works with any OpenAI-compatible API. To use a different provider, simply override the `baseURL` and `model` in your config:
50
+ ---
51
+
52
+ ## 🏗️ Enterprise Features
42
53
 
43
- ```yaml
44
- ai:
45
- apiKey: "your-openai-key"
46
- model: "gpt-4o"
47
- baseURL: "https://api.openai.com/v1"
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
- ## 🔍 Usage
51
- Scan staged git changes:
52
- ```bash
53
- pika-review scan
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
- Scan specific files:
74
+ ### 3. Health Stats
75
+
76
+ Monitor your codebase's architectural debt over time:
77
+
57
78
  ```bash
58
- pika-review scan src/index.ts src/utils/token.ts
79
+ pika-review stats
59
80
  ```
60
81
 
61
- Scan unstaged changes:
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 scan --unstaged
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
- pika-review scan --ci
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
- Pika Review uses your local git diffs and sends them directly to your Cloudflare Workers AI instance. No code is stored by Pika Review.
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
@@ -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, ask the user
14
- if (!targets && !isCI) {
15
- const gitChanges = getDiffFiles(target);
16
- if (gitChanges.length === 0) {
17
- logger.info("No git changes detected. Entering interactive selection mode...");
18
- const allFiles = listProjectFiles();
19
- if (allFiles.length > 0) {
20
- const selection = await prompts({
21
- type: "multiselect",
22
- name: "files",
23
- message: "Select files to analyze (Space to select, Enter to confirm):",
24
- choices: allFiles.map(f => ({ title: f, value: f })),
25
- hint: "- Space to select. Return to submit"
26
- });
27
- if (selection.files && selection.files.length > 0) {
28
- targets = selection.files;
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
- let reports = await runScan(target, isCI, targets);
34
- if (!reports || reports.length === 0) {
36
+ const { markdownReports, htmlReport, findings } = await runScan(target, isCI, targets);
37
+ if (!markdownReports || markdownReports.length === 0) {
35
38
  if (!isCI) {
36
- logger.success("Scan complete. No critical architectural anomalies detected.");
37
- logger.dim("Try scanning with '--unstaged' if you have uncommitted changes.");
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
- console.log(chalk.bold(`\n${"=".repeat(50)}`));
43
- logger.warn(`ANALYTICAL REPORT READY`);
44
- logger.dim(`${reports.length} file(s) require your immediate attention.\n`);
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: "What is your next step?",
84
+ message: "Choose post-scan action:",
49
85
  choices: [
50
- { title: "📂 Open generated Markdown reports", value: "view" },
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 === "view") {
56
- logger.info("\nGenerated Artifacts:");
57
- reports.forEach((r) => {
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(` ${chalk.cyan("")} ${chalk.bold(fileName)} ${chalk.dim(`(${r})`)}`);
107
+ console.log(` ${chalk.cyan("")} ${chalk.bold(fileName)} ${chalk.dim(`(file://${r})`)}`);
60
108
  });
61
- console.log(chalk.dim("\nTip: Use 'cat' or a Markdown viewer to read the findings."));
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
+ }