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