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/README.md +147 -0
- package/bin/jstar.js +170 -0
- package/package.json +64 -0
- package/scripts/config.ts +21 -0
- package/scripts/dashboard.ts +227 -0
- package/scripts/detective.ts +137 -0
- package/scripts/gemini-embedding.ts +97 -0
- package/scripts/indexer.ts +103 -0
- package/scripts/local-embedding.ts +55 -0
- package/scripts/mock-llm.ts +18 -0
- package/scripts/reviewer.ts +295 -0
- package/scripts/types.ts +61 -0
- package/setup.js +364 -0
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);
|