jstar-reviewer 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,10 +37,13 @@ curl -fsSL https://raw.githubusercontent.com/JStaRFilms/jstar-code-review/v2.0.0
37
37
 
38
38
  ### After Install:
39
39
 
40
- 1. Copy `.env.example` `.env.local`
41
- 2. Add your `GOOGLE_API_KEY` and `GROQ_API_KEY`
42
- 3. Run `jstar init` to build the brain
43
- 4. Run `jstar review` to review staged changes
40
+ 1. **Check Config**: The tool now **auto-creates** `.env.example` and `.jstar/` when you run it.
41
+ 2. **Add Keys**: Copy `.env.example` → `.env.local` and add your `GOOGLE_API_KEY` and `GROQ_API_KEY`.
42
+ 3. **Index**: Run `jstar init` (or `pnpm run index:init`) to build the brain.
43
+ 4. **Review**: Stage changes (`git add`) and run `jstar review` (or `pnpm run review`).
44
+
45
+ For a detailed walkthrough, see **[ONBOARDING.md](./ONBOARDING.md)**.
46
+
44
47
 
45
48
  ---
46
49
 
package/bin/jstar.js CHANGED
@@ -70,27 +70,51 @@ function commandExists(cmd) {
70
70
  }
71
71
 
72
72
  function runScript(scriptName) {
73
+ // Look for transpiled files in dist/
74
+ // .ts becomes .js in dist
75
+ const jsName = scriptName.replace('.ts', '.js');
76
+ const distDir = path.join(__dirname, '..', 'dist', 'scripts');
77
+ const jsPath = path.join(distDir, jsName);
78
+
79
+ // Fallback for local development if dist doesn't exist
73
80
  const scriptsDir = path.join(__dirname, '..', 'scripts');
74
- const scriptPath = path.join(scriptsDir, scriptName);
81
+ const tsPath = path.join(scriptsDir, scriptName);
82
+
83
+ const isWin = process.platform === 'win32';
84
+
85
+ if (fs.existsSync(jsPath)) {
86
+ log(`${COLORS.dim}Running ${jsName}...${COLORS.reset}`);
87
+ const child = spawn('node', [jsPath, ...process.argv.slice(3)], {
88
+ cwd: process.cwd(),
89
+ stdio: 'inherit',
90
+ shell: isWin,
91
+ env: {
92
+ ...process.env,
93
+ JSTAR_CWD: process.cwd()
94
+ }
95
+ });
96
+
97
+ child.on('close', (code) => process.exit(code || 0));
98
+ child.on('error', (err) => {
99
+ log(`${COLORS.red}Error running script: ${err.message}${COLORS.reset}`);
100
+ process.exit(1);
101
+ });
102
+ return;
103
+ }
75
104
 
76
- if (!fs.existsSync(scriptPath)) {
105
+ if (!fs.existsSync(tsPath)) {
77
106
  log(`${COLORS.red}Error: Script not found: ${scriptPath}${COLORS.reset}`);
78
107
  process.exit(1);
79
108
  }
80
109
 
81
- // Prioritize pnpm if available
110
+ // Fallback to ts-node if dist is not built (mostly for local development)
82
111
  const hasPnpm = commandExists('pnpm');
83
112
  const runner = hasPnpm ? 'pnpm' : 'npx';
84
113
  const runnerArgs = hasPnpm ? ['dlx', 'ts-node'] : ['ts-node'];
85
114
 
86
- log(`${COLORS.dim}Using ${runner} to run ${scriptName}...${COLORS.reset}`);
115
+ log(`${COLORS.dim}Using ${runner} (fallback) to run ${scriptName}...${COLORS.reset}`);
87
116
 
88
- // On Windows, global commands like pnpm/npx are .cmd files.
89
- // spawn without shell: true often fails with EINVAL or ENOENT on Windows.
90
- // We use shell: true only on Windows for reliability.
91
- const isWin = process.platform === 'win32';
92
-
93
- const child = spawn(runner, [...runnerArgs, scriptPath, ...process.argv.slice(3)], {
117
+ const child = spawn(runner, [...runnerArgs, tsPath, ...process.argv.slice(3)], {
94
118
  cwd: process.cwd(),
95
119
  stdio: 'inherit',
96
120
  shell: isWin,
@@ -110,35 +134,48 @@ function runScript(scriptName) {
110
134
  });
111
135
  }
112
136
 
137
+ const REQUIRED_ENV_VARS = {
138
+ 'GOOGLE_API_KEY': '# Required: Google API key for Gemini embeddings\nGOOGLE_API_KEY=your_google_api_key_here',
139
+ 'GROQ_API_KEY': '# Required: Groq API key for LLM reviews\nGROQ_API_KEY=your_groq_api_key_here',
140
+ 'REVIEW_MODEL_NAME': '# Optional: Override the default model\n# REVIEW_MODEL_NAME=moonshotai/kimi-k2-instruct-0905'
141
+ };
142
+
113
143
  function createSetupFiles() {
114
144
  const cwd = process.cwd();
115
145
 
116
- // Create .jstar directory
146
+ // 1. Create .jstar directory
117
147
  const jstarDir = path.join(cwd, '.jstar');
118
148
  if (!fs.existsSync(jstarDir)) {
119
149
  fs.mkdirSync(jstarDir, { recursive: true });
120
150
  log(`${COLORS.green}✓${COLORS.reset} Created .jstar/`);
121
151
  }
122
152
 
123
- // Create .env.example
124
- const envExample = `# J-Star Reviewer Configuration
125
- # Copy this to .env.local and fill in your keys
126
-
127
- # Required: Google API key for Gemini embeddings
128
- GOOGLE_API_KEY=your_google_api_key_here
129
-
130
- # Required: Groq API key for LLM reviews
131
- GROQ_API_KEY=your_groq_api_key_here
132
- `;
153
+ // 2. Create/Update .env.example
154
+ const envExamplePath = path.join(cwd, '.env.example');
155
+ if (fs.existsSync(envExamplePath)) {
156
+ let content = fs.readFileSync(envExamplePath, 'utf-8');
157
+ let addedKeys = [];
158
+
159
+ for (const [key, template] of Object.entries(REQUIRED_ENV_VARS)) {
160
+ if (!content.includes(key)) {
161
+ content += '\n' + template + '\n';
162
+ addedKeys.push(key);
163
+ }
164
+ }
133
165
 
134
- const envPath = path.join(cwd, '.env.example');
135
- if (!fs.existsSync(envPath)) {
136
- fs.writeFileSync(envPath, envExample);
137
- log(`${COLORS.green}✓${COLORS.reset} Created .env.example`);
166
+ if (addedKeys.length > 0) {
167
+ fs.writeFileSync(envExamplePath, content);
168
+ log(`${COLORS.green}✓${COLORS.reset} Updated .env.example with missing keys: ${addedKeys.join(', ')}`);
169
+ } else {
170
+ log(`${COLORS.dim} .env.example already exists and is up to date${COLORS.reset}`);
171
+ }
138
172
  } else {
139
- log(`${COLORS.dim} .env.example already exists${COLORS.reset}`);
173
+ const initialEnv = "# J-Star Code Reviewer\n" + Object.values(REQUIRED_ENV_VARS).join("\n") + "\n";
174
+ fs.writeFileSync(envExamplePath, initialEnv);
175
+ log(`${COLORS.green}✓${COLORS.reset} Created .env.example`);
140
176
  }
141
177
 
178
+
142
179
  // Update .gitignore
143
180
  const gitignorePath = path.join(cwd, '.gitignore');
144
181
  const gitignoreAdditions = `
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.Config = void 0;
40
+ const dotenv_1 = __importDefault(require("dotenv"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ // --- Auto-Setup Logic ---
44
+ const REQUIRED_ENV_VARS = {
45
+ 'GOOGLE_API_KEY': '# Required: Google API key for Gemini embeddings\nGOOGLE_API_KEY=your_google_api_key_here',
46
+ 'GROQ_API_KEY': '# Required: Groq API key for LLM reviews\nGROQ_API_KEY=your_groq_api_key_here',
47
+ 'REVIEW_MODEL_NAME': '# Optional: Override the default model\n# REVIEW_MODEL_NAME=moonshotai/kimi-k2-instruct-0905'
48
+ };
49
+ function ensureSetup() {
50
+ const cwd = process.cwd();
51
+ const jstarDir = path.join(cwd, ".jstar");
52
+ const envExamplePath = path.join(cwd, ".env.example");
53
+ // 1. Ensure .jstar exists
54
+ if (!fs.existsSync(jstarDir)) {
55
+ fs.mkdirSync(jstarDir, { recursive: true });
56
+ }
57
+ // 2. Ensure .env.example is up to date
58
+ if (fs.existsSync(envExamplePath)) {
59
+ let content = fs.readFileSync(envExamplePath, 'utf-8');
60
+ let missing = false;
61
+ for (const [key, template] of Object.entries(REQUIRED_ENV_VARS)) {
62
+ if (!content.includes(key)) {
63
+ content += `\n${template}\n`;
64
+ missing = true;
65
+ }
66
+ }
67
+ if (missing) {
68
+ fs.writeFileSync(envExamplePath, content);
69
+ }
70
+ }
71
+ else {
72
+ const initialEnv = "# J-Star Code Reviewer\n" + Object.values(REQUIRED_ENV_VARS).join("\n") + "\n";
73
+ fs.writeFileSync(envExamplePath, initialEnv);
74
+ }
75
+ }
76
+ // Run setup check
77
+ ensureSetup();
78
+ // Load .env.local first, then .env
79
+ dotenv_1.default.config({ path: ".env.local" });
80
+ dotenv_1.default.config();
81
+ /**
82
+ * Default fallback values.
83
+ */
84
+ const DEFAULT_MODEL = "moonshotai/kimi-k2-instruct-0905";
85
+ exports.Config = {
86
+ MODEL_NAME: process.env.REVIEW_MODEL_NAME || DEFAULT_MODEL,
87
+ DEFAULT_SEVERITY: 'P2_MEDIUM',
88
+ THRESHOLDS: {
89
+ MEDIUM: 5
90
+ }
91
+ };
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /**
3
+ * J-Star Dashboard Renderer
4
+ * Generates professional markdown dashboard from review findings
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.renderDashboard = renderDashboard;
8
+ exports.determineStatus = determineStatus;
9
+ exports.generateRecommendation = generateRecommendation;
10
+ const config_1 = require("./config");
11
+ const SEVERITY_EMOJI = {
12
+ 'P0_CRITICAL': '🛑',
13
+ 'P1_HIGH': '⚠️',
14
+ 'P2_MEDIUM': '📝',
15
+ 'LGTM': '✅'
16
+ };
17
+ const SEVERITY_LABEL = {
18
+ 'P0_CRITICAL': 'CRITICAL',
19
+ 'P1_HIGH': 'HIGH',
20
+ 'P2_MEDIUM': 'MEDIUM',
21
+ 'LGTM': 'PASSED'
22
+ };
23
+ function getStatusEmoji(status) {
24
+ switch (status) {
25
+ case 'CRITICAL_FAILURE': return '🔴';
26
+ case 'NEEDS_REVIEW': return '🟡';
27
+ case 'APPROVED': return '🟢';
28
+ }
29
+ }
30
+ function formatDate() {
31
+ return new Date().toISOString().split('T')[0];
32
+ }
33
+ /**
34
+ * Renders a single issue row for the markdown table.
35
+ * Includes fallback handling for unexpected severity values to ensure
36
+ * the dashboard renders gracefully even if parsing produced invalid data.
37
+ */
38
+ function renderIssueRow(file, issue, severity) {
39
+ // Fallback to '❓' and raw severity if the value is not in our known maps
40
+ const emoji = SEVERITY_EMOJI[severity] ?? '❓';
41
+ const label = SEVERITY_LABEL[severity] ?? severity;
42
+ return `| \`${file}\` | ${emoji} **${label}** | ${issue.title} |`;
43
+ }
44
+ function renderFixPrompt(issue) {
45
+ if (!issue.fixPrompt)
46
+ return '';
47
+ return `
48
+ <details>
49
+ <summary>🤖 <strong>Fix Prompt:</strong> ${issue.title}</summary>
50
+
51
+ \`\`\`
52
+ ${issue.fixPrompt}
53
+ \`\`\`
54
+
55
+ </details>
56
+ `;
57
+ }
58
+ /**
59
+ * Validates that the report object has the required structure.
60
+ * This guards against runtime errors from malformed LLM responses or corrupted data.
61
+ */
62
+ function validateReport(report) {
63
+ if (!report || typeof report !== 'object')
64
+ return false;
65
+ const r = report;
66
+ // Check required fields exist
67
+ if (typeof r.status !== 'string')
68
+ return false;
69
+ if (typeof r.recommendedAction !== 'string')
70
+ return false;
71
+ if (!Array.isArray(r.findings))
72
+ return false;
73
+ if (!r.metrics || typeof r.metrics !== 'object')
74
+ return false;
75
+ // Validate metrics structure
76
+ const m = r.metrics;
77
+ const requiredMetrics = ['filesScanned', 'totalTokens', 'violations', 'critical', 'high', 'medium', 'lgtm'];
78
+ for (const key of requiredMetrics) {
79
+ if (typeof m[key] !== 'number')
80
+ return false;
81
+ }
82
+ return true;
83
+ }
84
+ function renderDashboard(report) {
85
+ // Runtime validation to catch malformed reports early
86
+ if (!validateReport(report)) {
87
+ throw new Error('Invalid DashboardReport: missing or malformed required fields');
88
+ }
89
+ const { metrics, findings, status, recommendedAction } = report;
90
+ // Group findings by severity
91
+ const critical = findings.filter(f => f.severity === 'P0_CRITICAL');
92
+ const high = findings.filter(f => f.severity === 'P1_HIGH');
93
+ const medium = findings.filter(f => f.severity === 'P2_MEDIUM');
94
+ const lgtm = findings.filter(f => f.severity === 'LGTM');
95
+ let md = `# 📊 J-STAR CODE REVIEW DASHBOARD
96
+
97
+ **Date:** \`${formatDate()}\` | **Reviewer:** \`Detective Engine & Judge\` | **Status:** ${getStatusEmoji(status)} **${status.replace('_', ' ')}**
98
+
99
+ ---
100
+
101
+ ## 1. 📈 EXECUTIVE SUMMARY
102
+
103
+ | Metric | Value | Status |
104
+ | --- | --- | --- |
105
+ | **Files Scanned** | **${metrics.filesScanned}** | 🔎 Complete |
106
+ | **Total Tokens** | **~${metrics.totalTokens.toLocaleString()}** | ⚖️ Processed |
107
+ | **Total Violations** | **${metrics.violations}** | ${metrics.violations > 0 ? '🚨 Action Required' : '✅ Clean'} |
108
+ | **Critical (P0)** | **${metrics.critical}** | ${metrics.critical > 0 ? '🛑 **BLOCKER**' : '✅ None'} |
109
+ | **High (P1)** | **${metrics.high}** | ${metrics.high > 0 ? '⚠️ Needs Fix' : '✅ None'} |
110
+ | **Medium (P2)** | **${metrics.medium}** | ${metrics.medium > 0 ? '📝 Review' : '✅ None'} |
111
+ | **Passed (LGTM)** | **${metrics.lgtm}** | ✅ Clean |
112
+
113
+ ---
114
+
115
+ `;
116
+ // Critical Section
117
+ if (critical.length > 0) {
118
+ md += `## 2. 🛑 CRITICAL SECURITY VULNERABILITIES (P0)
119
+
120
+ > **These files contain blockers that must be fixed before any merge.**
121
+
122
+ | File | Severity | Issue |
123
+ | --- | --- | --- |
124
+ `;
125
+ for (const finding of critical) {
126
+ for (const issue of finding.issues) {
127
+ md += renderIssueRow(finding.file, issue, 'P0_CRITICAL') + '\n';
128
+ }
129
+ }
130
+ md += '\n### 🤖 Fix Prompts (P0)\n\n';
131
+ for (const finding of critical) {
132
+ for (const issue of finding.issues) {
133
+ md += renderFixPrompt(issue);
134
+ }
135
+ }
136
+ md += '\n---\n\n';
137
+ }
138
+ // High Section
139
+ if (high.length > 0) {
140
+ md += `## 3. ⚠️ HIGH PRIORITY ISSUES (P1)
141
+
142
+ > **Architecture and logic issues requiring significant attention.**
143
+
144
+ | File | Severity | Issue |
145
+ | --- | --- | --- |
146
+ `;
147
+ for (const finding of high) {
148
+ for (const issue of finding.issues) {
149
+ md += renderIssueRow(finding.file, issue, 'P1_HIGH') + '\n';
150
+ }
151
+ }
152
+ md += '\n### 🤖 Fix Prompts (P1)\n\n';
153
+ for (const finding of high) {
154
+ for (const issue of finding.issues) {
155
+ md += renderFixPrompt(issue);
156
+ }
157
+ }
158
+ md += '\n---\n\n';
159
+ }
160
+ // Medium Section
161
+ if (medium.length > 0) {
162
+ md += `## 4. 📝 MEDIUM PRIORITY ISSUES (P2)
163
+
164
+ > **Code quality and maintenance items.**
165
+
166
+ | File | Severity | Issue |
167
+ | --- | --- | --- |
168
+ `;
169
+ for (const finding of medium) {
170
+ for (const issue of finding.issues) {
171
+ md += renderIssueRow(finding.file, issue, 'P2_MEDIUM') + '\n';
172
+ }
173
+ }
174
+ md += '\n---\n\n';
175
+ }
176
+ // LGTM Section
177
+ if (lgtm.length > 0) {
178
+ md += `## 5. ✅ PASSED REVIEW (LGTM)
179
+
180
+ > **No issues found in these files.**
181
+
182
+ `;
183
+ for (const finding of lgtm) {
184
+ md += `- \`${finding.file}\`\n`;
185
+ }
186
+ md += '\n---\n\n';
187
+ }
188
+ // Recommended Action
189
+ md += `## 🎯 RECOMMENDED ACTION
190
+
191
+ > ${recommendedAction}
192
+
193
+ ---
194
+
195
+ *Generated by J-Star Code Reviewer v2*
196
+ `;
197
+ return md;
198
+ }
199
+ function determineStatus(metrics) {
200
+ if (metrics.critical > 0)
201
+ return 'CRITICAL_FAILURE';
202
+ if (metrics.high > 0 || metrics.medium > config_1.Config.THRESHOLDS.MEDIUM)
203
+ return 'NEEDS_REVIEW';
204
+ return 'APPROVED';
205
+ }
206
+ function generateRecommendation(metrics) {
207
+ if (metrics.critical > 0) {
208
+ return `**BLOCK MERGE:** Fix ${metrics.critical} critical issue(s) immediately. Review P0 fix prompts above.`;
209
+ }
210
+ if (metrics.high > 0) {
211
+ return `**Request Changes:** Address ${metrics.high} high-priority issue(s) before merging.`;
212
+ }
213
+ if (metrics.medium > 0) {
214
+ return `**Approve with Notes:** ${metrics.medium} medium issues found. Consider fixing in follow-up PR.`;
215
+ }
216
+ return `**Approve:** All files passed review. Ship it! 🚀`;
217
+ }
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.Detective = void 0;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const chalk_1 = __importDefault(require("chalk"));
43
+ const RULES = [
44
+ {
45
+ id: 'SEC-001',
46
+ severity: 'high',
47
+ message: 'Possible Hardcoded Secret detected',
48
+ pattern: /(api_key|secret|password|token)\s*[:=]\s*['"`][a-zA-Z0-9_\-\.]{10,}['"`]/i
49
+ },
50
+ {
51
+ id: 'ARCH-001',
52
+ severity: 'medium',
53
+ message: 'Avoid using console.log in production code',
54
+ pattern: /console\.log\(/
55
+ },
56
+ ];
57
+ // File-level rules that check the whole content
58
+ const FILE_RULES = [
59
+ {
60
+ id: 'ARCH-002',
61
+ severity: 'high',
62
+ message: 'Next.js "use client" must be at the very top of the file',
63
+ pattern: /^(?!['"]use client['"]).*['"]use client['"]/s,
64
+ filePattern: /\.tsx?$/
65
+ }
66
+ ];
67
+ class Detective {
68
+ constructor(directory) {
69
+ this.directory = directory;
70
+ this.violations = [];
71
+ }
72
+ async scan() {
73
+ this.walk(this.directory);
74
+ return this.violations;
75
+ }
76
+ walk(dir) {
77
+ if (!fs.existsSync(dir))
78
+ return;
79
+ const files = fs.readdirSync(dir);
80
+ for (const file of files) {
81
+ const filePath = path.join(dir, file);
82
+ const stat = fs.statSync(filePath);
83
+ if (stat.isDirectory()) {
84
+ if (file !== 'node_modules' && file !== '.git' && file !== '.jstar') {
85
+ this.walk(filePath);
86
+ }
87
+ }
88
+ else {
89
+ this.checkFile(filePath);
90
+ }
91
+ }
92
+ }
93
+ checkFile(filePath) {
94
+ if (!filePath.match(/\.(ts|tsx|js|jsx)$/))
95
+ return;
96
+ const content = fs.readFileSync(filePath, 'utf-8');
97
+ const lines = content.split('\n');
98
+ // Line-based rules
99
+ for (const rule of RULES) {
100
+ if (rule.filePattern && !filePath.match(rule.filePattern))
101
+ continue;
102
+ lines.forEach((line, index) => {
103
+ if (rule.pattern.test(line)) {
104
+ this.addViolation(filePath, index + 1, rule);
105
+ }
106
+ });
107
+ }
108
+ // File-based rules
109
+ for (const rule of FILE_RULES) {
110
+ if (rule.filePattern && !filePath.match(rule.filePattern))
111
+ continue;
112
+ if (rule.pattern.test(content)) {
113
+ this.addViolation(filePath, 1, rule);
114
+ }
115
+ }
116
+ }
117
+ addViolation(filePath, line, rule) {
118
+ this.violations.push({
119
+ file: path.relative(process.cwd(), filePath),
120
+ line,
121
+ message: rule.message,
122
+ severity: rule.severity,
123
+ code: rule.id
124
+ });
125
+ }
126
+ report() {
127
+ if (this.violations.length === 0) {
128
+ console.log(chalk_1.default.green("✅ Detective Engine: No violations found."));
129
+ return;
130
+ }
131
+ console.log(chalk_1.default.red(`🚨 Detective Engine found ${this.violations.length} violations:`));
132
+ // Only show first 10 to avoid wall of text
133
+ const total = this.violations.length;
134
+ const toShow = this.violations.slice(0, 10);
135
+ toShow.forEach(v => {
136
+ const color = v.severity === 'high' ? chalk_1.default.red : chalk_1.default.yellow;
137
+ console.log(color(`[${v.code}] ${v.file}:${v.line} - ${v.message}`));
138
+ });
139
+ if (total > 10) {
140
+ console.log(chalk_1.default.dim(`... and ${total - 10} more.`));
141
+ }
142
+ }
143
+ }
144
+ exports.Detective = Detective;
145
+ // CLI Integration
146
+ if (require.main === module) {
147
+ const detective = new Detective(path.join(process.cwd(), 'src'));
148
+ detective.scan();
149
+ detective.report();
150
+ }