jstar-reviewer 2.0.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/setup.js ADDED
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * J-Star Code Reviewer - One-Curl Setup Script
4
+ *
5
+ * Usage:
6
+ * npx jstar-reviewer init
7
+ *
8
+ * Or curl:
9
+ * curl -fsSL https://raw.githubusercontent.com/YOUR_REPO/main/setup.js | node
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+
16
+ const COLORS = {
17
+ reset: '\x1b[0m',
18
+ green: '\x1b[32m',
19
+ yellow: '\x1b[33m',
20
+ blue: '\x1b[34m',
21
+ red: '\x1b[31m',
22
+ dim: '\x1b[2m'
23
+ };
24
+
25
+ const log = {
26
+ info: (msg) => console.log(`${COLORS.blue}ℹ${COLORS.reset} ${msg}`),
27
+ success: (msg) => console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`),
28
+ warn: (msg) => console.log(`${COLORS.yellow}⚠${COLORS.reset} ${msg}`),
29
+ error: (msg) => console.log(`${COLORS.red}✗${COLORS.reset} ${msg}`),
30
+ step: (msg) => console.log(`${COLORS.dim} →${COLORS.reset} ${msg}`)
31
+ };
32
+
33
+ // Files to copy from the J-Star repo
34
+ const SCRIPT_FILES = [
35
+ 'scripts/reviewer.ts',
36
+ 'scripts/indexer.ts',
37
+ 'scripts/detective.ts',
38
+ 'scripts/dashboard.ts',
39
+ 'scripts/gemini-embedding.ts',
40
+ 'scripts/mock-llm.ts',
41
+ 'scripts/types.ts',
42
+ 'scripts/config.ts'
43
+ ];
44
+
45
+ const DEPENDENCIES = {
46
+ "ai": "^4.0.0",
47
+ "@ai-sdk/groq": "^1.0.0",
48
+ "@ai-sdk/google": "^1.0.0",
49
+ "@google/generative-ai": "^0.24.0",
50
+ "chalk": "^4.1.2",
51
+ "dotenv": "^16.0.0",
52
+ "llamaindex": "^0.1.21",
53
+ "simple-git": "^3.20.0"
54
+ };
55
+
56
+ const DEV_DEPENDENCIES = {
57
+ "ts-node": "^10.9.0",
58
+ "typescript": "^5.0.0",
59
+ "@types/node": "^20.0.0"
60
+ };
61
+
62
+ const SCRIPTS = {
63
+ "review": "ts-node scripts/reviewer.ts",
64
+ "index:init": "ts-node scripts/indexer.ts --init"
65
+ };
66
+
67
+ const ENV_EXAMPLE = `# J-Star Code Reviewer Configuration
68
+ # Copy this to .env.local and fill in your keys
69
+
70
+ # Required: Google API key for Gemini embeddings
71
+ GOOGLE_API_KEY=your_google_api_key_here
72
+
73
+ # Required: Groq API key for LLM reviews
74
+ GROQ_API_KEY=your_groq_api_key_here
75
+
76
+ # Optional: Override the default model
77
+ # REVIEW_MODEL_NAME=moonshotai/kimi-k2-instruct-0905
78
+ `;
79
+
80
+ const GITIGNORE_ADDITIONS = `
81
+ # J-Star Code Reviewer
82
+ .jstar/
83
+ .env.local
84
+ `;
85
+
86
+ async function main() {
87
+ console.log('\n🌟 J-Star Code Reviewer Setup\n');
88
+
89
+ // 0. Check Node.js version (fetch requires Node 18+)
90
+ const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
91
+ const hasFetch = typeof globalThis.fetch === 'function';
92
+
93
+ if (nodeVersion < 18) {
94
+ log.warn(`Node.js ${process.versions.node} detected. Recommend Node 18+ for native fetch.`);
95
+ }
96
+
97
+ const cwd = process.cwd();
98
+
99
+ // 1. Check if package.json exists
100
+ const pkgPath = path.join(cwd, 'package.json');
101
+ if (!fs.existsSync(pkgPath)) {
102
+ log.error('No package.json found. Run this in a Node.js project.');
103
+ process.exit(1);
104
+ }
105
+
106
+ // 2. Create scripts/ directory
107
+ const scriptsDir = path.join(cwd, 'scripts');
108
+ if (!fs.existsSync(scriptsDir)) {
109
+ fs.mkdirSync(scriptsDir, { recursive: true });
110
+ log.success('Created scripts/ directory');
111
+ }
112
+
113
+ // 3. Download/copy script files with security validation
114
+ log.info('Downloading reviewer scripts...');
115
+
116
+ // HARDCODED: Tagged release URL - NOT configurable for security
117
+ // To update, modify this constant and publish a new version of setup.js
118
+ const BASE_URL = 'https://raw.githubusercontent.com/JStaRFilms/jstar-code-review/v2.0.0';
119
+
120
+ // Validate URL matches our exact expected pattern (defense in depth)
121
+ function isValidUrl(url) {
122
+ // Only allow URLs that start with our exact base URL
123
+ return url.startsWith(BASE_URL + '/scripts/');
124
+ }
125
+
126
+ // Allowed file extensions whitelist
127
+ const ALLOWED_EXTENSIONS = ['.ts', '.js', '.json', '.md', '.txt'];
128
+
129
+ // Enhanced path safety check
130
+ function isSafePath(filePath) {
131
+ // 1. Reject null bytes (common injection attack)
132
+ if (filePath.includes('\0')) {
133
+ log.error('Path contains null byte - rejected');
134
+ return false;
135
+ }
136
+
137
+ // 2. Reject absolute paths
138
+ if (path.isAbsolute(filePath)) {
139
+ log.error('Absolute paths not allowed - rejected');
140
+ return false;
141
+ }
142
+
143
+ // 3. Normalize the path
144
+ const normalized = path.normalize(filePath);
145
+
146
+ // 4. Reject path traversal attempts
147
+ if (normalized.includes('..')) {
148
+ log.error('Path traversal detected - rejected');
149
+ return false;
150
+ }
151
+
152
+ // 5. Must start with scripts/ directory
153
+ if (!normalized.startsWith('scripts' + path.sep) && !normalized.startsWith('scripts/')) {
154
+ log.error('Path must be within scripts/ directory - rejected');
155
+ return false;
156
+ }
157
+
158
+ // 6. Resolve and verify path stays within scripts directory
159
+ const scriptsDir = path.resolve(cwd, 'scripts');
160
+ const resolvedPath = path.resolve(cwd, normalized);
161
+ if (!resolvedPath.startsWith(scriptsDir)) {
162
+ log.error('Resolved path escapes scripts/ boundary - rejected');
163
+ return false;
164
+ }
165
+
166
+ // 7. Check file extension against whitelist
167
+ const ext = path.extname(normalized).toLowerCase();
168
+ if (!ALLOWED_EXTENSIONS.includes(ext)) {
169
+ log.error(`Extension ${ext} not in whitelist ${ALLOWED_EXTENSIONS.join(', ')} - rejected`);
170
+ return false;
171
+ }
172
+
173
+ return true;
174
+ }
175
+
176
+ // Secure download using native fetch (Node 18+) or https fallback
177
+ async function downloadFile(url, destPath) {
178
+ if (!isValidUrl(url)) {
179
+ throw new Error(`Invalid URL: ${url}`);
180
+ }
181
+
182
+ // Use native fetch if available (Node 18+)
183
+ if (hasFetch) {
184
+ const response = await fetch(url, {
185
+ headers: { 'User-Agent': 'jstar-reviewer-setup' },
186
+ redirect: 'follow'
187
+ });
188
+
189
+ if (!response.ok) {
190
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
191
+ }
192
+
193
+ const content = await response.text();
194
+ fs.writeFileSync(destPath, content, 'utf-8');
195
+ } else {
196
+ // Fallback: Use Node.js https module for older versions
197
+ const https = require('https');
198
+ return new Promise((resolve, reject) => {
199
+ const file = fs.createWriteStream(destPath);
200
+ https.get(url, {
201
+ headers: { 'User-Agent': 'jstar-reviewer-setup' }
202
+ }, (response) => {
203
+ // Handle redirects
204
+ if (response.statusCode === 301 || response.statusCode === 302) {
205
+ const redirectUrl = response.headers.location;
206
+ if (!isValidUrl(redirectUrl)) {
207
+ reject(new Error(`Invalid redirect URL: ${redirectUrl}`));
208
+ return;
209
+ }
210
+ https.get(redirectUrl, (res) => {
211
+ res.pipe(file);
212
+ file.on('finish', () => { file.close(); resolve(); });
213
+ }).on('error', reject);
214
+ return;
215
+ }
216
+
217
+ if (response.statusCode !== 200) {
218
+ reject(new Error(`HTTP ${response.statusCode}`));
219
+ return;
220
+ }
221
+
222
+ response.pipe(file);
223
+ file.on('finish', () => { file.close(); resolve(); });
224
+ }).on('error', (err) => {
225
+ fs.unlink(destPath, () => { }); // Clean up partial file
226
+ reject(err);
227
+ });
228
+ });
229
+ }
230
+ }
231
+
232
+ for (const file of SCRIPT_FILES) {
233
+ // Validate file path before processing
234
+ if (!isSafePath(file)) {
235
+ log.error(`Unsafe file path rejected: ${file}`);
236
+ continue;
237
+ }
238
+
239
+ const destPath = path.join(cwd, file);
240
+ const dir = path.dirname(destPath);
241
+ if (!fs.existsSync(dir)) {
242
+ fs.mkdirSync(dir, { recursive: true });
243
+ }
244
+
245
+ try {
246
+ const url = `${BASE_URL}/${file}`;
247
+ await downloadFile(url, destPath);
248
+ log.step(`Downloaded ${file}`);
249
+ } catch (e) {
250
+ log.warn(`Could not download ${file}: ${e.message}`);
251
+ }
252
+ }
253
+
254
+ // 4. Update package.json
255
+ log.info('Updating package.json...');
256
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
257
+
258
+ pkg.scripts = { ...pkg.scripts, ...SCRIPTS };
259
+ pkg.dependencies = { ...pkg.dependencies, ...DEPENDENCIES };
260
+ pkg.devDependencies = { ...pkg.devDependencies, ...DEV_DEPENDENCIES };
261
+
262
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
263
+ log.success('Updated package.json with scripts and dependencies');
264
+
265
+ // 5. Create .jstar directory
266
+ const jstarDir = path.join(cwd, '.jstar');
267
+ if (!fs.existsSync(jstarDir)) {
268
+ fs.mkdirSync(jstarDir, { recursive: true });
269
+ log.success('Created .jstar/ directory');
270
+ }
271
+
272
+ // 6. Update .env.example (intelligently merge, don't override)
273
+ const envExamplePath = path.join(cwd, '.env.example');
274
+ const REQUIRED_ENV_VARS = {
275
+ 'GOOGLE_API_KEY': '# Required: Google API key for Gemini embeddings\nGOOGLE_API_KEY=your_google_api_key_here',
276
+ 'GROQ_API_KEY': '# Required: Groq API key for LLM reviews\nGROQ_API_KEY=your_groq_api_key_here',
277
+ 'REVIEW_MODEL_NAME': '# Optional: Override the default model\n# REVIEW_MODEL_NAME=moonshotai/kimi-k2-instruct-0905'
278
+ };
279
+
280
+ if (fs.existsSync(envExamplePath)) {
281
+ // File exists - intelligently append missing keys
282
+ let existingContent = fs.readFileSync(envExamplePath, 'utf-8');
283
+ let addedKeys = [];
284
+
285
+ for (const [key, template] of Object.entries(REQUIRED_ENV_VARS)) {
286
+ if (!existingContent.includes(key)) {
287
+ existingContent += '\n' + template + '\n';
288
+ addedKeys.push(key);
289
+ }
290
+ }
291
+
292
+ if (addedKeys.length > 0) {
293
+ // Add J-Star header if not present
294
+ if (!existingContent.includes('J-Star')) {
295
+ existingContent = existingContent.trimEnd() + '\n\n# J-Star Code Reviewer\n' +
296
+ addedKeys.map(k => REQUIRED_ENV_VARS[k]).join('\n') + '\n';
297
+ }
298
+ fs.writeFileSync(envExamplePath, existingContent);
299
+ log.success(`Added missing env vars to .env.example: ${addedKeys.join(', ')}`);
300
+ } else {
301
+ log.step('.env.example already has all required keys');
302
+ }
303
+ } else {
304
+ // Create fresh .env.example
305
+ fs.writeFileSync(envExamplePath, ENV_EXAMPLE);
306
+ log.success('Created .env.example');
307
+ }
308
+
309
+ // 7. Update .gitignore
310
+ const gitignorePath = path.join(cwd, '.gitignore');
311
+ if (fs.existsSync(gitignorePath)) {
312
+ const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
313
+ if (!gitignore.includes('.jstar/')) {
314
+ fs.appendFileSync(gitignorePath, GITIGNORE_ADDITIONS);
315
+ log.success('Updated .gitignore');
316
+ }
317
+ } else {
318
+ fs.writeFileSync(gitignorePath, GITIGNORE_ADDITIONS.trim());
319
+ log.success('Created .gitignore');
320
+ }
321
+
322
+ // 8. Install dependencies
323
+ log.info('Installing dependencies...');
324
+ try {
325
+ // Hardcoded whitelist: lockfile -> package manager command
326
+ const ALLOWED_PM = {
327
+ 'pnpm-lock.yaml': 'pnpm',
328
+ 'yarn.lock': 'yarn',
329
+ 'package-lock.json': 'npm'
330
+ };
331
+
332
+ // Detect package manager from lockfile
333
+ let pm = 'npm'; // Default fallback
334
+ for (const [lock, cmd] of Object.entries(ALLOWED_PM)) {
335
+ if (fs.existsSync(path.join(cwd, lock))) {
336
+ pm = cmd;
337
+ break;
338
+ }
339
+ }
340
+
341
+ // Validate package manager is one of expected values (security check)
342
+ const ALLOWED_PACKAGE_MANAGERS = ['pnpm', 'yarn', 'npm'];
343
+ if (!ALLOWED_PACKAGE_MANAGERS.includes(pm)) {
344
+ throw new Error(`Invalid package manager detected: ${pm}`);
345
+ }
346
+
347
+ execSync(`${pm} install`, { stdio: 'inherit' });
348
+ log.success('Dependencies installed');
349
+ } catch (e) {
350
+ log.warn('Could not auto-install dependencies. Run: pnpm install');
351
+ }
352
+
353
+ // Done!
354
+ console.log('\n' + '─'.repeat(50));
355
+ console.log('\n🎉 J-Star Code Reviewer installed!\n');
356
+ console.log('Next steps:');
357
+ console.log(' 1. Copy .env.example to .env.local');
358
+ console.log(' 2. Add your GOOGLE_API_KEY and GROQ_API_KEY');
359
+ console.log(' 3. Run: pnpm run index:init');
360
+ console.log(' 4. Stage changes and run: pnpm run review');
361
+ console.log('\n' + '─'.repeat(50) + '\n');
362
+ }
363
+
364
+ main().catch(console.error);