devbonzai 2.0.9 → 2.1.1
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/cli.js +7 -1785
- package/package.json +5 -1
- package/templates/ignore.txt +53 -0
- package/templates/receiver.js +1729 -0
package/cli.js
CHANGED
|
@@ -4,1791 +4,13 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawn, exec } = require('child_process');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
package-lock.json
|
|
15
|
-
|
|
16
|
-
# IDE and editor files
|
|
17
|
-
.vscode/
|
|
18
|
-
.idea/
|
|
19
|
-
*.swp
|
|
20
|
-
*.swo
|
|
21
|
-
|
|
22
|
-
# OS generated files
|
|
23
|
-
.DS_Store
|
|
24
|
-
.DS_Store?
|
|
25
|
-
._*
|
|
26
|
-
.Spotlight-V100
|
|
27
|
-
.Trashes
|
|
28
|
-
ehthumbs.db
|
|
29
|
-
Thumbs.db
|
|
30
|
-
|
|
31
|
-
# Environment and config files
|
|
32
|
-
.env
|
|
33
|
-
.env.local
|
|
34
|
-
.env.production
|
|
35
|
-
.env.staging
|
|
36
|
-
|
|
37
|
-
# Version control
|
|
38
|
-
.git/
|
|
39
|
-
.gitignore
|
|
40
|
-
.ignore
|
|
41
|
-
|
|
42
|
-
# Logs
|
|
43
|
-
*.log
|
|
44
|
-
logs/
|
|
45
|
-
|
|
46
|
-
# Build outputs
|
|
47
|
-
dist/
|
|
48
|
-
build/
|
|
49
|
-
*.min.js
|
|
50
|
-
*.min.css
|
|
51
|
-
|
|
52
|
-
# Temporary files
|
|
53
|
-
*.tmp
|
|
54
|
-
*.temp
|
|
55
|
-
|
|
56
|
-
# Project-specific files
|
|
57
|
-
receiver.js
|
|
58
|
-
bonzai/
|
|
59
|
-
`;
|
|
60
|
-
|
|
61
|
-
const RECEIVER_JS_CONTENT = `#!/usr/bin/env node
|
|
62
|
-
|
|
63
|
-
const express = require('./node_modules/express');
|
|
64
|
-
const cors = require('./node_modules/cors');
|
|
65
|
-
const fs = require('fs');
|
|
66
|
-
const path = require('path');
|
|
67
|
-
const { exec, spawn, execSync } = require('child_process');
|
|
68
|
-
let babelParser = null;
|
|
69
|
-
try {
|
|
70
|
-
babelParser = require('./node_modules/@babel/parser');
|
|
71
|
-
} catch (e) {
|
|
72
|
-
// Babel parser not available, will fall back gracefully
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const app = express();
|
|
76
|
-
const ROOT = path.join(__dirname, '..');
|
|
77
|
-
|
|
78
|
-
app.use(cors());
|
|
79
|
-
app.use(express.json());
|
|
80
|
-
|
|
81
|
-
// Root route - simple API documentation
|
|
82
|
-
app.get('/', (req, res) => {
|
|
83
|
-
res.json({
|
|
84
|
-
message: 'Local File Server API',
|
|
85
|
-
endpoints: {
|
|
86
|
-
'GET /list': 'List all files in the directory',
|
|
87
|
-
'GET /read?path=<filepath>': 'Read file content',
|
|
88
|
-
'GET /git-churn?path=<filepath>&commits=30': 'Get git commit churn for a file',
|
|
89
|
-
'POST /write': 'Write file content (body: {path, content})',
|
|
90
|
-
'POST /write_dir': 'Create directory (body: {path})',
|
|
91
|
-
'POST /delete': 'Delete file or directory (body: {path})',
|
|
92
|
-
'POST /move': 'Move file or folder (body: {source, destination})',
|
|
93
|
-
'POST /open-cursor': 'Open Cursor (body: {path, line?})',
|
|
94
|
-
'POST /analyze_prompt': 'Analyze what files would be modified (body: {prompt})',
|
|
95
|
-
'POST /prompt_agent': 'Execute cursor-agent command (body: {prompt})',
|
|
96
|
-
'POST /prompt_agent_stream': 'Execute cursor-agent with SSE streaming (body: {prompt})',
|
|
97
|
-
'POST /revert_job': 'Revert to a previous commit (body: {beforeCommit})',
|
|
98
|
-
'POST /shutdown': 'Gracefully shutdown the server'
|
|
99
|
-
},
|
|
100
|
-
example: 'Try: /list or /read?path=README.md'
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Read and parse ignore patterns from .ignore file
|
|
105
|
-
function getIgnorePatterns() {
|
|
106
|
-
try {
|
|
107
|
-
const ignorePath = path.join(__dirname, '.ignore');
|
|
108
|
-
if (fs.existsSync(ignorePath)) {
|
|
109
|
-
const content = fs.readFileSync(ignorePath, 'utf8');
|
|
110
|
-
return content
|
|
111
|
-
.split('\\n')
|
|
112
|
-
.map(line => line.trim())
|
|
113
|
-
.filter(line => line && !line.startsWith('#'))
|
|
114
|
-
.map(pattern => {
|
|
115
|
-
// Convert simple glob patterns to regex
|
|
116
|
-
if (pattern.endsWith('/')) {
|
|
117
|
-
// Directory pattern
|
|
118
|
-
pattern = pattern.slice(0, -1);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Simple approach: escape dots and convert globs
|
|
122
|
-
pattern = pattern.replace(/\\./g, '\\\\.');
|
|
123
|
-
pattern = pattern.replace(/\\*\\*/g, '|||DOUBLESTAR|||');
|
|
124
|
-
pattern = pattern.replace(/\\*/g, '[^/]*');
|
|
125
|
-
pattern = pattern.replace(/\\|\\|\\|DOUBLESTAR\\|\\|\\|/g, '.*');
|
|
126
|
-
|
|
127
|
-
return new RegExp('^' + pattern + '(/.*)?$');
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
} catch (e) {
|
|
131
|
-
console.warn('Could not read .ignore file:', e.message);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Default ignore patterns if no .ignore file exists
|
|
135
|
-
return [
|
|
136
|
-
/^node_modules(\\/.*)?$/,
|
|
137
|
-
/^\\.git(\\/.*)?$/,
|
|
138
|
-
/^\\.DS_Store$/,
|
|
139
|
-
/^\\.env$/,
|
|
140
|
-
/^bonzai(\\/.*)?$/
|
|
141
|
-
];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Check if a path should be ignored
|
|
145
|
-
function shouldIgnore(relativePath, ignorePatterns) {
|
|
146
|
-
return ignorePatterns.some(pattern => pattern.test(relativePath));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Extract functions, classes, and methods from a Python file
|
|
150
|
-
function extractPythonFunctions(filePath) {
|
|
151
|
-
try {
|
|
152
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
153
|
-
const lines = content.split('\\n');
|
|
154
|
-
const functions = [];
|
|
155
|
-
const classes = [];
|
|
156
|
-
let currentFunction = null;
|
|
157
|
-
let currentClass = null;
|
|
158
|
-
let decorators = [];
|
|
159
|
-
let classIndent = -1;
|
|
160
|
-
|
|
161
|
-
for (let i = 0; i < lines.length; i++) {
|
|
162
|
-
const line = lines[i];
|
|
163
|
-
const trimmed = line.trim();
|
|
164
|
-
|
|
165
|
-
// Calculate indentation level
|
|
166
|
-
const match = line.match(/^\\s*/);
|
|
167
|
-
const currentIndent = match ? match[0].length : 0;
|
|
168
|
-
|
|
169
|
-
// Check for decorators (only at top level, before function/class)
|
|
170
|
-
if (trimmed.startsWith('@') && currentIndent === 0) {
|
|
171
|
-
decorators.push(line);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Check if this is a top-level class definition
|
|
176
|
-
const classMatch = trimmed.match(/^class\\s+(\\w+)/);
|
|
177
|
-
if (classMatch && currentIndent === 0) {
|
|
178
|
-
// Save previous function/class if exists
|
|
179
|
-
if (currentFunction) {
|
|
180
|
-
currentFunction.content = currentFunction.content.trim();
|
|
181
|
-
functions.push(currentFunction);
|
|
182
|
-
currentFunction = null;
|
|
183
|
-
}
|
|
184
|
-
if (currentClass) {
|
|
185
|
-
currentClass.content = currentClass.content.trim();
|
|
186
|
-
classes.push(currentClass);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Start new class
|
|
190
|
-
const className = classMatch[1];
|
|
191
|
-
let classContent = '';
|
|
192
|
-
|
|
193
|
-
// Add decorators if any
|
|
194
|
-
if (decorators.length > 0) {
|
|
195
|
-
classContent = decorators.join('\\n') + '\\n';
|
|
196
|
-
decorators = [];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
classContent += line;
|
|
200
|
-
classIndent = currentIndent;
|
|
201
|
-
|
|
202
|
-
currentClass = {
|
|
203
|
-
name: className,
|
|
204
|
-
content: classContent,
|
|
205
|
-
methods: [],
|
|
206
|
-
startLine: i + 1,
|
|
207
|
-
endLine: i + 1
|
|
208
|
-
};
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Check if this is a method definition (inside a class)
|
|
213
|
-
const methodMatch = trimmed.match(/^def\\s+(\\w+)\\s*\\(/);
|
|
214
|
-
if (methodMatch && currentClass && currentIndent > classIndent) {
|
|
215
|
-
// Save previous method if exists
|
|
216
|
-
if (currentFunction) {
|
|
217
|
-
currentFunction.content = currentFunction.content.trim();
|
|
218
|
-
currentClass.methods.push(currentFunction);
|
|
219
|
-
currentFunction = null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Start new method
|
|
223
|
-
const methodName = methodMatch[1];
|
|
224
|
-
let methodContent = '';
|
|
225
|
-
|
|
226
|
-
// Add decorators if any
|
|
227
|
-
if (decorators.length > 0) {
|
|
228
|
-
methodContent = decorators.join('\\n') + '\\n';
|
|
229
|
-
decorators = [];
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
methodContent += line;
|
|
233
|
-
|
|
234
|
-
currentFunction = {
|
|
235
|
-
name: currentClass.name + '.' + methodName,
|
|
236
|
-
content: methodContent,
|
|
237
|
-
startLine: i + 1,
|
|
238
|
-
endLine: i + 1,
|
|
239
|
-
isMethod: true,
|
|
240
|
-
className: currentClass.name,
|
|
241
|
-
methodName: methodName
|
|
242
|
-
};
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Check if this is a top-level function definition
|
|
247
|
-
const funcMatch = trimmed.match(/^def\\s+(\\w+)\\s*\\(/);
|
|
248
|
-
|
|
249
|
-
if (funcMatch && currentIndent === 0) {
|
|
250
|
-
// Save previous function/class if exists
|
|
251
|
-
if (currentFunction) {
|
|
252
|
-
currentFunction.content = currentFunction.content.trim();
|
|
253
|
-
if (currentFunction.isMethod && currentClass) {
|
|
254
|
-
currentClass.methods.push(currentFunction);
|
|
255
|
-
} else {
|
|
256
|
-
functions.push(currentFunction);
|
|
257
|
-
}
|
|
258
|
-
currentFunction = null;
|
|
259
|
-
}
|
|
260
|
-
if (currentClass) {
|
|
261
|
-
currentClass.content = currentClass.content.trim();
|
|
262
|
-
classes.push(currentClass);
|
|
263
|
-
currentClass = null;
|
|
264
|
-
classIndent = -1;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Start new function
|
|
268
|
-
const functionName = funcMatch[1];
|
|
269
|
-
let functionContent = '';
|
|
270
|
-
|
|
271
|
-
// Add decorators if any
|
|
272
|
-
if (decorators.length > 0) {
|
|
273
|
-
functionContent = decorators.join('\\n') + '\\n';
|
|
274
|
-
decorators = [];
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
functionContent += line;
|
|
278
|
-
|
|
279
|
-
currentFunction = {
|
|
280
|
-
name: functionName,
|
|
281
|
-
content: functionContent,
|
|
282
|
-
startLine: i + 1,
|
|
283
|
-
endLine: i + 1
|
|
284
|
-
};
|
|
285
|
-
} else if (currentFunction || currentClass) {
|
|
286
|
-
// We're processing lines after a function/class definition
|
|
287
|
-
if (currentIndent === 0 && trimmed && !trimmed.startsWith('#')) {
|
|
288
|
-
// Back to top level with non-comment content - function/class ended
|
|
289
|
-
if (currentFunction) {
|
|
290
|
-
currentFunction.content = currentFunction.content.trim();
|
|
291
|
-
if (currentFunction.isMethod && currentClass) {
|
|
292
|
-
currentClass.methods.push(currentFunction);
|
|
293
|
-
} else {
|
|
294
|
-
functions.push(currentFunction);
|
|
295
|
-
}
|
|
296
|
-
currentFunction = null;
|
|
297
|
-
}
|
|
298
|
-
if (currentClass) {
|
|
299
|
-
currentClass.content = currentClass.content.trim();
|
|
300
|
-
classes.push(currentClass);
|
|
301
|
-
currentClass = null;
|
|
302
|
-
classIndent = -1;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Check if this line starts a new function/class
|
|
306
|
-
if (funcMatch) {
|
|
307
|
-
const functionName = funcMatch[1];
|
|
308
|
-
let functionContent = '';
|
|
309
|
-
|
|
310
|
-
if (decorators.length > 0) {
|
|
311
|
-
functionContent = decorators.join('\\n') + '\\n';
|
|
312
|
-
decorators = [];
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
functionContent += line;
|
|
316
|
-
|
|
317
|
-
currentFunction = {
|
|
318
|
-
name: functionName,
|
|
319
|
-
content: functionContent,
|
|
320
|
-
startLine: i + 1,
|
|
321
|
-
endLine: i + 1
|
|
322
|
-
};
|
|
323
|
-
} else if (classMatch) {
|
|
324
|
-
const className = classMatch[1];
|
|
325
|
-
let classContent = '';
|
|
326
|
-
|
|
327
|
-
if (decorators.length > 0) {
|
|
328
|
-
classContent = decorators.join('\\n') + '\\n';
|
|
329
|
-
decorators = [];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
classContent += line;
|
|
333
|
-
classIndent = currentIndent;
|
|
334
|
-
|
|
335
|
-
currentClass = {
|
|
336
|
-
name: className,
|
|
337
|
-
content: classContent,
|
|
338
|
-
methods: [],
|
|
339
|
-
startLine: i + 1,
|
|
340
|
-
endLine: i + 1
|
|
341
|
-
};
|
|
342
|
-
} else if (trimmed.startsWith('@')) {
|
|
343
|
-
decorators.push(line);
|
|
344
|
-
}
|
|
345
|
-
} else {
|
|
346
|
-
// Still inside function/class (indented or empty/comment line)
|
|
347
|
-
if (currentFunction) {
|
|
348
|
-
currentFunction.content += '\\n' + line;
|
|
349
|
-
currentFunction.endLine = i + 1;
|
|
350
|
-
}
|
|
351
|
-
if (currentClass) {
|
|
352
|
-
currentClass.content += '\\n' + line;
|
|
353
|
-
currentClass.endLine = i + 1;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Don't forget the last function/class
|
|
360
|
-
if (currentFunction) {
|
|
361
|
-
currentFunction.content = currentFunction.content.trim();
|
|
362
|
-
if (currentFunction.isMethod && currentClass) {
|
|
363
|
-
currentClass.methods.push(currentFunction);
|
|
364
|
-
} else {
|
|
365
|
-
functions.push(currentFunction);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (currentClass) {
|
|
369
|
-
currentClass.content = currentClass.content.trim();
|
|
370
|
-
classes.push(currentClass);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return { functions, classes };
|
|
374
|
-
} catch (e) {
|
|
375
|
-
// If parsing fails (invalid Python, etc.), return empty arrays
|
|
376
|
-
console.warn('Failed to parse Python file:', filePath, e.message);
|
|
377
|
-
return { functions: [], classes: [] };
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Extract functions, classes, and methods from a JavaScript/TypeScript file
|
|
382
|
-
function extractJavaScriptFunctions(filePath) {
|
|
383
|
-
try {
|
|
384
|
-
if (!babelParser) {
|
|
385
|
-
return { functions: [], classes: [] };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Skip .d.ts files, minified files, and node_modules
|
|
389
|
-
if (filePath.endsWith('.d.ts') || filePath.endsWith('.min.js') || filePath.includes('node_modules')) {
|
|
390
|
-
return { functions: [], classes: [] };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
394
|
-
const functions = [];
|
|
395
|
-
const classes = [];
|
|
396
|
-
|
|
397
|
-
// Determine if it's TypeScript
|
|
398
|
-
const isTypeScript = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
const ast = babelParser.parse(content, {
|
|
402
|
-
sourceType: 'module',
|
|
403
|
-
plugins: [
|
|
404
|
-
'typescript',
|
|
405
|
-
'jsx',
|
|
406
|
-
'decorators-legacy',
|
|
407
|
-
'classProperties',
|
|
408
|
-
'objectRestSpread',
|
|
409
|
-
'asyncGenerators',
|
|
410
|
-
'functionBind',
|
|
411
|
-
'exportDefaultFrom',
|
|
412
|
-
'exportNamespaceFrom',
|
|
413
|
-
'dynamicImport',
|
|
414
|
-
'nullishCoalescingOperator',
|
|
415
|
-
'optionalChaining'
|
|
416
|
-
]
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
// Helper to extract code snippet from source
|
|
420
|
-
const getCode = (node) => {
|
|
421
|
-
return content.substring(node.start, node.end);
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
// Track visited nodes to avoid duplicates
|
|
425
|
-
const visitedNodes = new Set();
|
|
426
|
-
|
|
427
|
-
// Traverse AST
|
|
428
|
-
function traverse(node, parentType = null) {
|
|
429
|
-
if (!node) return;
|
|
430
|
-
|
|
431
|
-
// Skip if already visited (avoid processing same node twice)
|
|
432
|
-
if (visitedNodes.has(node)) return;
|
|
433
|
-
visitedNodes.add(node);
|
|
434
|
-
|
|
435
|
-
// Function declarations: function myFunc() {}
|
|
436
|
-
// Skip if inside ExportNamedDeclaration (will be handled below)
|
|
437
|
-
if (node.type === 'FunctionDeclaration' && node.id && parentType !== 'ExportNamedDeclaration') {
|
|
438
|
-
functions.push({
|
|
439
|
-
name: node.id.name,
|
|
440
|
-
content: getCode(node),
|
|
441
|
-
startLine: node.loc ? node.loc.start.line : 0,
|
|
442
|
-
endLine: node.loc ? node.loc.end.line : 0
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Arrow functions: const myFunc = () => {}
|
|
447
|
-
if (node.type === 'VariableDeclarator' &&
|
|
448
|
-
node.init &&
|
|
449
|
-
(node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression') &&
|
|
450
|
-
node.id && node.id.type === 'Identifier') {
|
|
451
|
-
const funcContent = getCode(node);
|
|
452
|
-
functions.push({
|
|
453
|
-
name: node.id.name,
|
|
454
|
-
content: funcContent,
|
|
455
|
-
startLine: node.loc ? node.loc.start.line : 0,
|
|
456
|
-
endLine: node.loc ? node.loc.end.line : 0
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Helper function to extract methods from a class body
|
|
461
|
-
const extractClassMethods = (classNode, className) => {
|
|
462
|
-
const methods = [];
|
|
463
|
-
if (classNode.body && classNode.body.body && Array.isArray(classNode.body.body)) {
|
|
464
|
-
for (const member of classNode.body.body) {
|
|
465
|
-
// Handle MethodDefinition (regular methods, constructors, getters, setters, static methods)
|
|
466
|
-
if (member && member.type === 'MethodDefinition' && member.key) {
|
|
467
|
-
let methodName;
|
|
468
|
-
if (member.key.type === 'Identifier') {
|
|
469
|
-
methodName = member.key.name;
|
|
470
|
-
} else if (member.key.type === 'PrivateName') {
|
|
471
|
-
methodName = '#' + member.key.id.name;
|
|
472
|
-
} else if (member.key.type === 'StringLiteral' || member.key.type === 'NumericLiteral') {
|
|
473
|
-
methodName = String(member.key.value);
|
|
474
|
-
} else {
|
|
475
|
-
methodName = String(member.key.value || member.key.name || 'unknown');
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Include kind (constructor, get, set, method) in the name for clarity
|
|
479
|
-
const kind = member.kind || 'method';
|
|
480
|
-
const isStatic = member.static || false;
|
|
481
|
-
|
|
482
|
-
// For getters and setters, include the kind in the method name to distinguish them
|
|
483
|
-
// e.g., "value" getter vs "value" setter -> "get value" and "set value"
|
|
484
|
-
let fullMethodName = methodName;
|
|
485
|
-
if (kind === 'get') {
|
|
486
|
-
fullMethodName = 'get ' + methodName;
|
|
487
|
-
} else if (kind === 'set') {
|
|
488
|
-
fullMethodName = 'set ' + methodName;
|
|
489
|
-
} else if (kind === 'constructor') {
|
|
490
|
-
fullMethodName = 'constructor';
|
|
491
|
-
} else if (isStatic) {
|
|
492
|
-
fullMethodName = 'static ' + methodName;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
methods.push({
|
|
496
|
-
name: className + '.' + methodName,
|
|
497
|
-
content: getCode(member),
|
|
498
|
-
startLine: member.loc ? member.loc.start.line : 0,
|
|
499
|
-
endLine: member.loc ? member.loc.end.line : 0,
|
|
500
|
-
isMethod: true,
|
|
501
|
-
className: className,
|
|
502
|
-
methodName: methodName,
|
|
503
|
-
kind: kind,
|
|
504
|
-
static: isStatic
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
return methods;
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
// Class declarations: class User { ... }
|
|
513
|
-
// Skip if inside ExportNamedDeclaration or ExportDefaultDeclaration (will be handled below)
|
|
514
|
-
if (node.type === 'ClassDeclaration' && node.id &&
|
|
515
|
-
parentType !== 'ExportNamedDeclaration' && parentType !== 'ExportDefaultDeclaration') {
|
|
516
|
-
const className = node.id.name;
|
|
517
|
-
const methods = extractClassMethods(node, className);
|
|
518
|
-
|
|
519
|
-
classes.push({
|
|
520
|
-
name: className,
|
|
521
|
-
content: getCode(node),
|
|
522
|
-
methods: methods,
|
|
523
|
-
startLine: node.loc ? node.loc.start.line : 0,
|
|
524
|
-
endLine: node.loc ? node.loc.end.line : 0
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Export declarations: export function, export class
|
|
529
|
-
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
530
|
-
if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
|
|
531
|
-
functions.push({
|
|
532
|
-
name: node.declaration.id.name,
|
|
533
|
-
content: getCode(node.declaration),
|
|
534
|
-
startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
|
|
535
|
-
endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
|
|
536
|
-
isExported: true
|
|
537
|
-
});
|
|
538
|
-
// Mark as visited to avoid duplicate processing
|
|
539
|
-
visitedNodes.add(node.declaration);
|
|
540
|
-
} else if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
|
|
541
|
-
const className = node.declaration.id.name;
|
|
542
|
-
const methods = extractClassMethods(node.declaration, className);
|
|
543
|
-
|
|
544
|
-
classes.push({
|
|
545
|
-
name: className,
|
|
546
|
-
content: getCode(node.declaration),
|
|
547
|
-
methods: methods,
|
|
548
|
-
startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
|
|
549
|
-
endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
|
|
550
|
-
isExported: true
|
|
551
|
-
});
|
|
552
|
-
// Mark as visited to avoid duplicate processing
|
|
553
|
-
visitedNodes.add(node.declaration);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Export default declarations: export default class
|
|
558
|
-
if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
|
|
559
|
-
if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
|
|
560
|
-
const className = node.declaration.id.name;
|
|
561
|
-
const methods = extractClassMethods(node.declaration, className);
|
|
562
|
-
|
|
563
|
-
classes.push({
|
|
564
|
-
name: className,
|
|
565
|
-
content: getCode(node.declaration),
|
|
566
|
-
methods: methods,
|
|
567
|
-
startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
|
|
568
|
-
endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
|
|
569
|
-
isExported: true,
|
|
570
|
-
isDefaultExport: true
|
|
571
|
-
});
|
|
572
|
-
// Mark as visited to avoid duplicate processing
|
|
573
|
-
visitedNodes.add(node.declaration);
|
|
574
|
-
} else if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
|
|
575
|
-
functions.push({
|
|
576
|
-
name: node.declaration.id.name,
|
|
577
|
-
content: getCode(node.declaration),
|
|
578
|
-
startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
|
|
579
|
-
endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
|
|
580
|
-
isExported: true,
|
|
581
|
-
isDefaultExport: true
|
|
582
|
-
});
|
|
583
|
-
visitedNodes.add(node.declaration);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Recursively traverse children
|
|
588
|
-
for (const key in node) {
|
|
589
|
-
if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
|
|
590
|
-
const child = node[key];
|
|
591
|
-
if (Array.isArray(child)) {
|
|
592
|
-
child.forEach(c => traverse(c, node.type));
|
|
593
|
-
} else if (child && typeof child === 'object' && child.type) {
|
|
594
|
-
traverse(child, node.type);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
traverse(ast);
|
|
600
|
-
} catch (parseError) {
|
|
601
|
-
// Silently skip parsing errors - these are expected for some files
|
|
602
|
-
// Only log if it's not in node_modules or a known problematic file type
|
|
603
|
-
if (!filePath.includes('node_modules') && !filePath.endsWith('.d.ts') && !filePath.endsWith('.min.js')) {
|
|
604
|
-
// Suppress warnings for common parsing issues
|
|
605
|
-
const errorMsg = parseError.message || '';
|
|
606
|
-
if (!errorMsg.includes('outside of function') &&
|
|
607
|
-
!errorMsg.includes('Missing initializer') &&
|
|
608
|
-
!errorMsg.includes('Export') &&
|
|
609
|
-
!errorMsg.includes('Unexpected token')) {
|
|
610
|
-
// Only log unexpected errors
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return { functions: [], classes: [] };
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return { functions, classes };
|
|
617
|
-
} catch (e) {
|
|
618
|
-
console.warn('Failed to read JavaScript/TypeScript file:', filePath, e.message);
|
|
619
|
-
return { functions: [], classes: [] };
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Extract script content from Vue file and parse it
|
|
624
|
-
function extractVueFunctions(filePath) {
|
|
625
|
-
try {
|
|
626
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
627
|
-
|
|
628
|
-
// Extract <script> section from Vue file
|
|
629
|
-
const scriptMatch = content.match(/<script[^>]*>([\\s\\S]*?)<\\/script>/);
|
|
630
|
-
if (!scriptMatch) {
|
|
631
|
-
return { functions: [], classes: [] };
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const scriptContent = scriptMatch[1];
|
|
635
|
-
|
|
636
|
-
// Create a temporary file path for parsing (just for reference)
|
|
637
|
-
// Parse the script content as JavaScript/TypeScript
|
|
638
|
-
if (!babelParser) {
|
|
639
|
-
return { functions: [], classes: [] };
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const functions = [];
|
|
643
|
-
const classes = [];
|
|
644
|
-
|
|
645
|
-
// Check if it's TypeScript
|
|
646
|
-
const isTypeScript = scriptMatch[0].includes('lang="ts"') || scriptMatch[0].includes("lang='ts'");
|
|
647
|
-
|
|
648
|
-
try {
|
|
649
|
-
const ast = babelParser.parse(scriptContent, {
|
|
650
|
-
sourceType: 'module',
|
|
651
|
-
plugins: [
|
|
652
|
-
'typescript',
|
|
653
|
-
'jsx',
|
|
654
|
-
'decorators-legacy',
|
|
655
|
-
'classProperties',
|
|
656
|
-
'objectRestSpread',
|
|
657
|
-
'asyncGenerators',
|
|
658
|
-
'functionBind',
|
|
659
|
-
'exportDefaultFrom',
|
|
660
|
-
'exportNamespaceFrom',
|
|
661
|
-
'dynamicImport',
|
|
662
|
-
'nullishCoalescingOperator',
|
|
663
|
-
'optionalChaining'
|
|
664
|
-
]
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
// Helper to extract code snippet from source
|
|
668
|
-
const getCode = (node) => {
|
|
669
|
-
return scriptContent.substring(node.start, node.end);
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
// Track visited nodes to avoid duplicates
|
|
673
|
-
const visitedNodes = new Set();
|
|
674
|
-
|
|
675
|
-
// Traverse AST (same logic as JavaScript parser)
|
|
676
|
-
function traverse(node, parentType = null) {
|
|
677
|
-
if (!node) return;
|
|
678
|
-
|
|
679
|
-
// Skip if already visited (avoid processing same node twice)
|
|
680
|
-
if (visitedNodes.has(node)) return;
|
|
681
|
-
visitedNodes.add(node);
|
|
682
|
-
|
|
683
|
-
// Function declarations: function myFunc() {}
|
|
684
|
-
// Skip if inside ExportNamedDeclaration (will be handled below)
|
|
685
|
-
if (node.type === 'FunctionDeclaration' && node.id && parentType !== 'ExportNamedDeclaration') {
|
|
686
|
-
functions.push({
|
|
687
|
-
name: node.id.name,
|
|
688
|
-
content: getCode(node),
|
|
689
|
-
startLine: node.loc ? node.loc.start.line : 0,
|
|
690
|
-
endLine: node.loc ? node.loc.end.line : 0
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// Arrow functions: const myFunc = () => {}
|
|
695
|
-
if (node.type === 'VariableDeclarator' &&
|
|
696
|
-
node.init &&
|
|
697
|
-
(node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression') &&
|
|
698
|
-
node.id && node.id.type === 'Identifier') {
|
|
699
|
-
const funcContent = getCode(node);
|
|
700
|
-
functions.push({
|
|
701
|
-
name: node.id.name,
|
|
702
|
-
content: funcContent,
|
|
703
|
-
startLine: node.loc ? node.loc.start.line : 0,
|
|
704
|
-
endLine: node.loc ? node.loc.end.line : 0
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// Helper function to extract methods from a class body
|
|
709
|
-
const extractClassMethods = (classNode, className) => {
|
|
710
|
-
const methods = [];
|
|
711
|
-
if (classNode.body && classNode.body.body && Array.isArray(classNode.body.body)) {
|
|
712
|
-
for (const member of classNode.body.body) {
|
|
713
|
-
// Handle MethodDefinition (regular methods, constructors, getters, setters, static methods)
|
|
714
|
-
if (member && member.type === 'MethodDefinition' && member.key) {
|
|
715
|
-
let methodName;
|
|
716
|
-
if (member.key.type === 'Identifier') {
|
|
717
|
-
methodName = member.key.name;
|
|
718
|
-
} else if (member.key.type === 'PrivateName') {
|
|
719
|
-
methodName = '#' + member.key.id.name;
|
|
720
|
-
} else if (member.key.type === 'StringLiteral' || member.key.type === 'NumericLiteral') {
|
|
721
|
-
methodName = String(member.key.value);
|
|
722
|
-
} else {
|
|
723
|
-
methodName = String(member.key.value || member.key.name || 'unknown');
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Include kind (constructor, get, set, method) in the name for clarity
|
|
727
|
-
const kind = member.kind || 'method';
|
|
728
|
-
const isStatic = member.static || false;
|
|
729
|
-
|
|
730
|
-
// For getters and setters, include the kind in the method name to distinguish them
|
|
731
|
-
// e.g., "value" getter vs "value" setter -> "get value" and "set value"
|
|
732
|
-
let fullMethodName = methodName;
|
|
733
|
-
if (kind === 'get') {
|
|
734
|
-
fullMethodName = 'get ' + methodName;
|
|
735
|
-
} else if (kind === 'set') {
|
|
736
|
-
fullMethodName = 'set ' + methodName;
|
|
737
|
-
} else if (kind === 'constructor') {
|
|
738
|
-
fullMethodName = 'constructor';
|
|
739
|
-
} else if (isStatic) {
|
|
740
|
-
fullMethodName = 'static ' + methodName;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
methods.push({
|
|
744
|
-
name: className + '.' + methodName,
|
|
745
|
-
content: getCode(member),
|
|
746
|
-
startLine: member.loc ? member.loc.start.line : 0,
|
|
747
|
-
endLine: member.loc ? member.loc.end.line : 0,
|
|
748
|
-
isMethod: true,
|
|
749
|
-
className: className,
|
|
750
|
-
methodName: methodName,
|
|
751
|
-
kind: kind,
|
|
752
|
-
static: isStatic
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
return methods;
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
// Class declarations: class User { ... }
|
|
761
|
-
// Skip if inside ExportNamedDeclaration or ExportDefaultDeclaration (will be handled below)
|
|
762
|
-
if (node.type === 'ClassDeclaration' && node.id &&
|
|
763
|
-
parentType !== 'ExportNamedDeclaration' && parentType !== 'ExportDefaultDeclaration') {
|
|
764
|
-
const className = node.id.name;
|
|
765
|
-
const methods = extractClassMethods(node, className);
|
|
766
|
-
|
|
767
|
-
classes.push({
|
|
768
|
-
name: className,
|
|
769
|
-
content: getCode(node),
|
|
770
|
-
methods: methods,
|
|
771
|
-
startLine: node.loc ? node.loc.start.line : 0,
|
|
772
|
-
endLine: node.loc ? node.loc.end.line : 0
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Export declarations: export function, export class
|
|
777
|
-
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
778
|
-
if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
|
|
779
|
-
functions.push({
|
|
780
|
-
name: node.declaration.id.name,
|
|
781
|
-
content: getCode(node.declaration),
|
|
782
|
-
startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
|
|
783
|
-
endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
|
|
784
|
-
isExported: true
|
|
785
|
-
});
|
|
786
|
-
// Mark as visited to avoid duplicate processing
|
|
787
|
-
visitedNodes.add(node.declaration);
|
|
788
|
-
} else if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
|
|
789
|
-
const className = node.declaration.id.name;
|
|
790
|
-
const methods = extractClassMethods(node.declaration, className);
|
|
791
|
-
|
|
792
|
-
classes.push({
|
|
793
|
-
name: className,
|
|
794
|
-
content: getCode(node.declaration),
|
|
795
|
-
methods: methods,
|
|
796
|
-
startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
|
|
797
|
-
endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
|
|
798
|
-
isExported: true
|
|
799
|
-
});
|
|
800
|
-
// Mark as visited to avoid duplicate processing
|
|
801
|
-
visitedNodes.add(node.declaration);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// Export default declarations: export default class
|
|
806
|
-
if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
|
|
807
|
-
if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
|
|
808
|
-
const className = node.declaration.id.name;
|
|
809
|
-
const methods = extractClassMethods(node.declaration, className);
|
|
810
|
-
|
|
811
|
-
classes.push({
|
|
812
|
-
name: className,
|
|
813
|
-
content: getCode(node.declaration),
|
|
814
|
-
methods: methods,
|
|
815
|
-
startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
|
|
816
|
-
endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
|
|
817
|
-
isExported: true,
|
|
818
|
-
isDefaultExport: true
|
|
819
|
-
});
|
|
820
|
-
// Mark as visited to avoid duplicate processing
|
|
821
|
-
visitedNodes.add(node.declaration);
|
|
822
|
-
} else if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
|
|
823
|
-
functions.push({
|
|
824
|
-
name: node.declaration.id.name,
|
|
825
|
-
content: getCode(node.declaration),
|
|
826
|
-
startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
|
|
827
|
-
endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
|
|
828
|
-
isExported: true,
|
|
829
|
-
isDefaultExport: true
|
|
830
|
-
});
|
|
831
|
-
visitedNodes.add(node.declaration);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// Recursively traverse children
|
|
836
|
-
for (const key in node) {
|
|
837
|
-
if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
|
|
838
|
-
const child = node[key];
|
|
839
|
-
if (Array.isArray(child)) {
|
|
840
|
-
child.forEach(c => traverse(c, node.type));
|
|
841
|
-
} else if (child && typeof child === 'object' && child.type) {
|
|
842
|
-
traverse(child, node.type);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
traverse(ast);
|
|
848
|
-
} catch (parseError) {
|
|
849
|
-
// Silently skip parsing errors for Vue files
|
|
850
|
-
return { functions: [], classes: [] };
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
return { functions, classes };
|
|
854
|
-
} catch (e) {
|
|
855
|
-
console.warn('Failed to read Vue file:', filePath, e.message);
|
|
856
|
-
return { functions: [], classes: [] };
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// Recursively list all files in a directory, respecting ignore patterns
|
|
861
|
-
function listAllFiles(dir, base = '', ignorePatterns = null) {
|
|
862
|
-
if (ignorePatterns === null) {
|
|
863
|
-
ignorePatterns = getIgnorePatterns();
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
let results = [];
|
|
867
|
-
const list = fs.readdirSync(dir);
|
|
868
|
-
|
|
869
|
-
for (const file of list) {
|
|
870
|
-
const fullPath = path.join(dir, file);
|
|
871
|
-
const relativePath = path.join(base, file);
|
|
872
|
-
|
|
873
|
-
// Check if this path should be ignored
|
|
874
|
-
if (shouldIgnore(relativePath, ignorePatterns)) {
|
|
875
|
-
continue;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
const stat = fs.statSync(fullPath);
|
|
879
|
-
if (stat && stat.isDirectory()) {
|
|
880
|
-
// Skip node_modules directories explicitly
|
|
881
|
-
if (file === 'node_modules' || relativePath.includes('node_modules/')) {
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
// Add the directory itself to results
|
|
885
|
-
results.push(relativePath + '/');
|
|
886
|
-
// Recursively list files inside the directory
|
|
887
|
-
results = results.concat(listAllFiles(fullPath, relativePath, ignorePatterns));
|
|
888
|
-
} else {
|
|
889
|
-
// Skip files in node_modules explicitly
|
|
890
|
-
if (relativePath.includes('node_modules/') || fullPath.includes('node_modules')) {
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
results.push(relativePath);
|
|
895
|
-
|
|
896
|
-
// Helper function to add functions, classes, and methods as virtual files
|
|
897
|
-
const addVirtualFiles = (parseResult, filePath) => {
|
|
898
|
-
// Add functions
|
|
899
|
-
for (const func of parseResult.functions) {
|
|
900
|
-
const functionFileName = func.name + '.function';
|
|
901
|
-
const functionFilePath = path.join(filePath, functionFileName).replace(/\\\\/g, '/');
|
|
902
|
-
results.push(functionFilePath);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// Add classes and their methods
|
|
906
|
-
for (const cls of parseResult.classes) {
|
|
907
|
-
// Add class itself (optional, but useful)
|
|
908
|
-
const className = cls.name + '.class';
|
|
909
|
-
const classFilePath = path.join(filePath, className).replace(/\\\\/g, '/');
|
|
910
|
-
results.push(classFilePath);
|
|
911
|
-
|
|
912
|
-
// Add methods nested under the class: ClassName.methodName
|
|
913
|
-
if (cls.methods && cls.methods.length > 0) {
|
|
914
|
-
for (const method of cls.methods) {
|
|
915
|
-
const methodFileName = method.name + '.method';
|
|
916
|
-
const methodFilePath = path.join(classFilePath, methodFileName).replace(/\\\\/g, '/');
|
|
917
|
-
results.push(methodFilePath);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
};
|
|
922
|
-
|
|
923
|
-
// Handle Python files
|
|
924
|
-
if (file.endsWith('.py')) {
|
|
925
|
-
const parseResult = extractPythonFunctions(fullPath);
|
|
926
|
-
addVirtualFiles(parseResult, relativePath);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Handle JavaScript/TypeScript files
|
|
930
|
-
// Skip .d.ts files (TypeScript declaration files) and .min.js files (minified)
|
|
931
|
-
if ((file.endsWith('.js') || file.endsWith('.jsx') || file.endsWith('.ts') || file.endsWith('.tsx')) &&
|
|
932
|
-
!file.endsWith('.d.ts') && !file.endsWith('.min.js')) {
|
|
933
|
-
const parseResult = extractJavaScriptFunctions(fullPath);
|
|
934
|
-
addVirtualFiles(parseResult, relativePath);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// Handle Vue files
|
|
938
|
-
if (file.endsWith('.vue')) {
|
|
939
|
-
const parseResult = extractVueFunctions(fullPath);
|
|
940
|
-
addVirtualFiles(parseResult, relativePath);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
return results;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
app.get('/list', (req, res) => {
|
|
948
|
-
try {
|
|
949
|
-
const rootName = path.basename(ROOT);
|
|
950
|
-
const files = listAllFiles(ROOT, rootName);
|
|
951
|
-
res.json({ files });
|
|
952
|
-
} catch (e) {
|
|
953
|
-
res.status(500).send(e.message);
|
|
954
|
-
}
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
app.get('/read', (req, res) => {
|
|
958
|
-
try {
|
|
959
|
-
const requestedPath = req.query.path || '';
|
|
960
|
-
const filePath = path.join(ROOT, requestedPath);
|
|
961
|
-
|
|
962
|
-
if (!filePath.startsWith(ROOT)) {
|
|
963
|
-
return res.status(400).send('Invalid path');
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Helper function to find and return content from parse result
|
|
967
|
-
const findAndReturn = (parseResult, name, type) => {
|
|
968
|
-
if (type === 'function') {
|
|
969
|
-
const target = parseResult.functions.find(f => f.name === name);
|
|
970
|
-
if (target) return target.content;
|
|
971
|
-
} else if (type === 'method') {
|
|
972
|
-
// Method name format: ClassName.methodName
|
|
973
|
-
for (const cls of parseResult.classes) {
|
|
974
|
-
const method = cls.methods.find(m => m.name === name);
|
|
975
|
-
if (method) return method.content;
|
|
976
|
-
}
|
|
977
|
-
} else if (type === 'class') {
|
|
978
|
-
const target = parseResult.classes.find(c => c.name === name);
|
|
979
|
-
if (target) return target.content;
|
|
980
|
-
}
|
|
981
|
-
return null;
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
// Check if this is a virtual file request (.function, .method, or .class)
|
|
985
|
-
if (requestedPath.endsWith('.function') || requestedPath.endsWith('.method') || requestedPath.endsWith('.class')) {
|
|
986
|
-
// Traverse up the path to find the actual source file
|
|
987
|
-
let currentPath = filePath;
|
|
988
|
-
let sourceFilePath = null;
|
|
989
|
-
let parser = null;
|
|
990
|
-
|
|
991
|
-
// Keep going up until we find a source file (.py, .js, .jsx, .ts, .tsx, .vue)
|
|
992
|
-
while (currentPath !== ROOT && currentPath !== path.dirname(currentPath)) {
|
|
993
|
-
const stat = fs.existsSync(currentPath) ? fs.statSync(currentPath) : null;
|
|
994
|
-
|
|
995
|
-
// Check if current path is a file with a supported extension
|
|
996
|
-
if (stat && stat.isFile()) {
|
|
997
|
-
if (currentPath.endsWith('.py')) {
|
|
998
|
-
parser = extractPythonFunctions;
|
|
999
|
-
sourceFilePath = currentPath;
|
|
1000
|
-
break;
|
|
1001
|
-
} else if (currentPath.endsWith('.js') || currentPath.endsWith('.jsx') ||
|
|
1002
|
-
currentPath.endsWith('.ts') || currentPath.endsWith('.tsx')) {
|
|
1003
|
-
parser = extractJavaScriptFunctions;
|
|
1004
|
-
sourceFilePath = currentPath;
|
|
1005
|
-
break;
|
|
1006
|
-
} else if (currentPath.endsWith('.vue')) {
|
|
1007
|
-
parser = extractVueFunctions;
|
|
1008
|
-
sourceFilePath = currentPath;
|
|
1009
|
-
break;
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// Move up one level
|
|
1014
|
-
const parentPath = path.dirname(currentPath);
|
|
1015
|
-
if (parentPath === currentPath) break; // Reached root
|
|
1016
|
-
currentPath = parentPath;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (!sourceFilePath || !parser) {
|
|
1020
|
-
return res.status(404).send('Source file not found for virtual file');
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// Extract the requested item name from the requested path
|
|
1024
|
-
let itemName = '';
|
|
1025
|
-
let itemType = '';
|
|
1026
|
-
|
|
1027
|
-
if (requestedPath.endsWith('.function')) {
|
|
1028
|
-
itemName = path.basename(requestedPath, '.function');
|
|
1029
|
-
itemType = 'function';
|
|
1030
|
-
} else if (requestedPath.endsWith('.method')) {
|
|
1031
|
-
itemName = path.basename(requestedPath, '.method');
|
|
1032
|
-
itemType = 'method';
|
|
1033
|
-
} else if (requestedPath.endsWith('.class')) {
|
|
1034
|
-
itemName = path.basename(requestedPath, '.class');
|
|
1035
|
-
itemType = 'class';
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// Check if the source file exists
|
|
1039
|
-
try {
|
|
1040
|
-
if (!fs.existsSync(sourceFilePath)) {
|
|
1041
|
-
return res.status(404).send('Source file not found');
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Parse the file
|
|
1045
|
-
const parseResult = parser(sourceFilePath);
|
|
1046
|
-
|
|
1047
|
-
// Find and return the content
|
|
1048
|
-
const content = findAndReturn(parseResult, itemName, itemType);
|
|
1049
|
-
|
|
1050
|
-
if (!content) {
|
|
1051
|
-
return res.status(404).send(\`\${itemType} '\${itemName}' not found in file\`);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
return res.json({ content });
|
|
1055
|
-
} catch (e) {
|
|
1056
|
-
const errorType = requestedPath.endsWith('.function') ? 'function' :
|
|
1057
|
-
requestedPath.endsWith('.method') ? 'method' : 'class';
|
|
1058
|
-
return res.status(500).send('Error reading ' + errorType + ': ' + e.message);
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// Regular file read
|
|
1063
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
1064
|
-
res.json({ content });
|
|
1065
|
-
} catch (e) {
|
|
1066
|
-
res.status(500).send(e.message);
|
|
1067
|
-
}
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
app.get('/git-churn', (req, res) => {
|
|
1071
|
-
try {
|
|
1072
|
-
const filePath = path.join(ROOT, req.query.path || '');
|
|
1073
|
-
if (!filePath.startsWith(ROOT)) {
|
|
1074
|
-
return res.status(400).json({ error: 'Invalid path' });
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// Get commits parameter, default to 30
|
|
1078
|
-
const commits = parseInt(req.query.commits) || 30;
|
|
1079
|
-
|
|
1080
|
-
// Get relative path from ROOT for git command
|
|
1081
|
-
const relativePath = path.relative(ROOT, filePath);
|
|
1082
|
-
|
|
1083
|
-
// Build git log command with relative path
|
|
1084
|
-
const gitCommand = \`git log --oneline --all -\${commits} -- "\${relativePath}"\`;
|
|
1085
|
-
|
|
1086
|
-
exec(gitCommand, { cwd: ROOT }, (error, stdout, stderr) => {
|
|
1087
|
-
// If git command fails (no repo, file not tracked, etc.), return 0 churn
|
|
1088
|
-
if (error) {
|
|
1089
|
-
// Check if it's because file is not in git or no git repo
|
|
1090
|
-
if (error.code === 128 || stderr.includes('not a git repository')) {
|
|
1091
|
-
return res.json({ churn: 0 });
|
|
1092
|
-
}
|
|
1093
|
-
// For other errors, still return 0 churn gracefully
|
|
1094
|
-
return res.json({ churn: 0 });
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// Count non-empty lines (each line is a commit)
|
|
1098
|
-
const commitCount = stdout.trim().split('\\n').filter(line => line.trim().length > 0).length;
|
|
1099
|
-
|
|
1100
|
-
res.json({ churn: commitCount });
|
|
1101
|
-
});
|
|
1102
|
-
} catch (e) {
|
|
1103
|
-
// Handle any other errors gracefully
|
|
1104
|
-
res.status(500).json({ error: e.message, churn: 0 });
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
app.post('/write', (req, res) => {
|
|
1109
|
-
try {
|
|
1110
|
-
const filePath = path.join(ROOT, req.body.path || '');
|
|
1111
|
-
if (!filePath.startsWith(ROOT)) {
|
|
1112
|
-
return res.status(400).send('Invalid path');
|
|
1113
|
-
}
|
|
1114
|
-
fs.writeFileSync(filePath, req.body.content, 'utf8');
|
|
1115
|
-
res.json({ status: 'ok' });
|
|
1116
|
-
} catch (e) {
|
|
1117
|
-
res.status(500).send(e.message);
|
|
1118
|
-
}
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
|
-
app.post('/write_dir', (req, res) => {
|
|
1122
|
-
try {
|
|
1123
|
-
const dirPath = path.join(ROOT, req.body.path || '');
|
|
1124
|
-
if (!dirPath.startsWith(ROOT)) {
|
|
1125
|
-
return res.status(400).send('Invalid path');
|
|
1126
|
-
}
|
|
1127
|
-
// Create directory recursively (creates parent directories if they don't exist)
|
|
1128
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
1129
|
-
res.json({ status: 'ok' });
|
|
1130
|
-
} catch (e) {
|
|
1131
|
-
res.status(500).send(e.message);
|
|
1132
|
-
}
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
app.post('/delete', (req, res) => {
|
|
1136
|
-
try {
|
|
1137
|
-
const targetPath = path.join(ROOT, req.body.path || '');
|
|
1138
|
-
if (!targetPath.startsWith(ROOT)) {
|
|
1139
|
-
return res.status(400).send('Invalid path');
|
|
1140
|
-
}
|
|
1141
|
-
// Delete file or directory recursively
|
|
1142
|
-
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
1143
|
-
res.json({ status: 'ok' });
|
|
1144
|
-
} catch (e) {
|
|
1145
|
-
res.status(500).send(e.message);
|
|
1146
|
-
}
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
app.post('/move', (req, res) => {
|
|
1150
|
-
try {
|
|
1151
|
-
const sourcePath = path.join(ROOT, req.body.source || '');
|
|
1152
|
-
const destinationPath = path.join(ROOT, req.body.destination || '');
|
|
1153
|
-
|
|
1154
|
-
// Validate both paths are within ROOT directory
|
|
1155
|
-
if (!sourcePath.startsWith(ROOT) || !destinationPath.startsWith(ROOT)) {
|
|
1156
|
-
return res.status(400).send('Invalid path');
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// Check if source exists
|
|
1160
|
-
if (!fs.existsSync(sourcePath)) {
|
|
1161
|
-
return res.status(400).send('Source path does not exist');
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// Ensure destination directory exists
|
|
1165
|
-
const destinationDir = path.dirname(destinationPath);
|
|
1166
|
-
if (!fs.existsSync(destinationDir)) {
|
|
1167
|
-
fs.mkdirSync(destinationDir, { recursive: true });
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// Move the file or folder
|
|
1171
|
-
fs.renameSync(sourcePath, destinationPath);
|
|
1172
|
-
res.json({ status: 'ok' });
|
|
1173
|
-
} catch (e) {
|
|
1174
|
-
res.status(500).send(e.message);
|
|
1175
|
-
}
|
|
1176
|
-
});
|
|
1177
|
-
|
|
1178
|
-
app.post('/open-cursor', (req, res) => {
|
|
1179
|
-
try {
|
|
1180
|
-
const requestedPath = req.body.path || '';
|
|
1181
|
-
|
|
1182
|
-
// Resolve path relative to ROOT (similar to other endpoints)
|
|
1183
|
-
// If path is absolute and within ROOT, use it directly
|
|
1184
|
-
// Otherwise, resolve it relative to ROOT
|
|
1185
|
-
let filePath;
|
|
1186
|
-
if (path.isAbsolute(requestedPath)) {
|
|
1187
|
-
// If absolute path, check if it's within ROOT
|
|
1188
|
-
if (requestedPath.startsWith(ROOT)) {
|
|
1189
|
-
filePath = requestedPath;
|
|
1190
|
-
} else {
|
|
1191
|
-
// Path might contain incorrect segments (like "codemaps")
|
|
1192
|
-
// Try to find ROOT in the path and extract the relative part
|
|
1193
|
-
const rootIndex = requestedPath.indexOf(ROOT);
|
|
1194
|
-
if (rootIndex !== -1) {
|
|
1195
|
-
// Extract the part after ROOT and remove leading slashes
|
|
1196
|
-
let relativePart = requestedPath.substring(rootIndex + ROOT.length);
|
|
1197
|
-
while (relativePart.startsWith('/')) {
|
|
1198
|
-
relativePart = relativePart.substring(1);
|
|
1199
|
-
}
|
|
1200
|
-
filePath = path.join(ROOT, relativePart);
|
|
1201
|
-
} else {
|
|
1202
|
-
return res.status(400).json({ error: 'Invalid path: path must be within project root' });
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
} else {
|
|
1206
|
-
// Relative path - resolve relative to ROOT
|
|
1207
|
-
// Remove root directory name prefix if present (from /list endpoint format)
|
|
1208
|
-
const rootName = path.basename(ROOT);
|
|
1209
|
-
let relativePath = requestedPath;
|
|
1210
|
-
if (relativePath.startsWith(rootName + '/')) {
|
|
1211
|
-
relativePath = relativePath.substring(rootName.length + 1);
|
|
1212
|
-
}
|
|
1213
|
-
filePath = path.join(ROOT, relativePath);
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
// Validate the resolved path is within ROOT
|
|
1217
|
-
if (!filePath.startsWith(ROOT)) {
|
|
1218
|
-
return res.status(400).json({ error: 'Invalid path' });
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
const { line } = req.body;
|
|
1222
|
-
|
|
1223
|
-
// Always use cursor CLI command first (it handles line numbers correctly)
|
|
1224
|
-
const cursorCommands = [
|
|
1225
|
-
'cursor',
|
|
1226
|
-
'/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
|
|
1227
|
-
'/usr/local/bin/cursor',
|
|
1228
|
-
'code'
|
|
1229
|
-
];
|
|
1230
|
-
|
|
1231
|
-
const tryCommand = (commandIndex = 0) => {
|
|
1232
|
-
if (commandIndex >= cursorCommands.length) {
|
|
1233
|
-
return res.status(500).json({
|
|
1234
|
-
error: 'Cursor not found. Please install Cursor CLI or check Cursor installation.'
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// Use proper Cursor CLI syntax for line numbers
|
|
1239
|
-
const command = line
|
|
1240
|
-
? \`\${cursorCommands[commandIndex]} --goto "\${filePath}:\${line}"\`
|
|
1241
|
-
: \`\${cursorCommands[commandIndex]} "\${filePath}"\`;
|
|
1242
|
-
|
|
1243
|
-
exec(command, (error, stdout, stderr) => {
|
|
1244
|
-
if (error && error.code === 127) {
|
|
1245
|
-
// Command not found, try next one
|
|
1246
|
-
tryCommand(commandIndex + 1);
|
|
1247
|
-
} else if (error) {
|
|
1248
|
-
console.error('Error opening Cursor:', error);
|
|
1249
|
-
return res.status(500).json({ error: error.message });
|
|
1250
|
-
} else {
|
|
1251
|
-
// File opened successfully, now bring Cursor to front
|
|
1252
|
-
const isMac = process.platform === 'darwin';
|
|
1253
|
-
if (isMac) {
|
|
1254
|
-
// Use AppleScript to bring Cursor to the front
|
|
1255
|
-
exec('osascript -e "tell application \\\\"Cursor\\\\" to activate"', (activateError) => {
|
|
1256
|
-
if (activateError) {
|
|
1257
|
-
console.log('Could not activate Cursor, but file opened successfully');
|
|
1258
|
-
}
|
|
1259
|
-
});
|
|
1260
|
-
|
|
1261
|
-
// Additional command to ensure it's really in front
|
|
1262
|
-
setTimeout(() => {
|
|
1263
|
-
exec('osascript -e "tell application \\\\"System Events\\\\" to set frontmost of process \\\\"Cursor\\\\" to true"', () => {
|
|
1264
|
-
// Don't worry if this fails
|
|
1265
|
-
});
|
|
1266
|
-
}, 500);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
res.json({ success: true, message: 'Cursor opened and focused successfully' });
|
|
1270
|
-
}
|
|
1271
|
-
});
|
|
1272
|
-
};
|
|
1273
|
-
|
|
1274
|
-
tryCommand();
|
|
1275
|
-
} catch (e) {
|
|
1276
|
-
res.status(500).json({ error: e.message });
|
|
1277
|
-
}
|
|
1278
|
-
});
|
|
1279
|
-
|
|
1280
|
-
// Analyze prompt endpoint - asks agent what files it would modify without making changes
|
|
1281
|
-
app.post('/analyze_prompt', (req, res) => {
|
|
1282
|
-
console.log('🔵 [analyze_prompt] Endpoint hit');
|
|
1283
|
-
const { prompt } = req.body;
|
|
1284
|
-
console.log('🔵 [analyze_prompt] Received prompt:', prompt ? \`\${prompt.substring(0, 50)}...\` : 'none');
|
|
1285
|
-
|
|
1286
|
-
if (!prompt || typeof prompt !== 'string') {
|
|
1287
|
-
console.log('❌ [analyze_prompt] Error: prompt required');
|
|
1288
|
-
return res.status(400).json({ error: 'prompt required' });
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
// Configurable timeout (default 2 minutes for analysis)
|
|
1292
|
-
const timeoutMs = parseInt(req.body.timeout) || 2 * 60 * 1000;
|
|
1293
|
-
let timeoutId = null;
|
|
1294
|
-
let responseSent = false;
|
|
1295
|
-
|
|
1296
|
-
// Build analysis prompt - ask agent to list files without making changes
|
|
1297
|
-
const analysisPrompt = \`You are analyzing a coding task. Do NOT make any changes to any files. Only analyze and list the files you would need to modify to complete this task.
|
|
1298
|
-
|
|
1299
|
-
Respond ONLY with valid JSON in this exact format (no other text):
|
|
1300
|
-
{"files": [{"path": "path/to/file.ext", "reason": "brief reason for modification"}]}
|
|
1301
|
-
|
|
1302
|
-
If no files need modification, respond with: {"files": []}
|
|
1303
|
-
|
|
1304
|
-
Task to analyze: \${prompt}\`;
|
|
1305
|
-
|
|
1306
|
-
const args = ['--print', '--force', '--workspace', '.', analysisPrompt];
|
|
1307
|
-
|
|
1308
|
-
console.log('🔵 [analyze_prompt] Spawning cursor-agent process...');
|
|
1309
|
-
const proc = spawn(
|
|
1310
|
-
'cursor-agent',
|
|
1311
|
-
args,
|
|
1312
|
-
{
|
|
1313
|
-
cwd: ROOT,
|
|
1314
|
-
env: process.env,
|
|
1315
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
1316
|
-
}
|
|
1317
|
-
);
|
|
1318
|
-
|
|
1319
|
-
console.log('🔵 [analyze_prompt] Process spawned, PID:', proc.pid);
|
|
1320
|
-
|
|
1321
|
-
let stdout = '';
|
|
1322
|
-
let stderr = '';
|
|
1323
|
-
|
|
1324
|
-
timeoutId = setTimeout(() => {
|
|
1325
|
-
if (!responseSent && proc && !proc.killed) {
|
|
1326
|
-
console.log('⏱️ [analyze_prompt] Timeout reached, killing process...');
|
|
1327
|
-
proc.kill('SIGTERM');
|
|
1328
|
-
setTimeout(() => {
|
|
1329
|
-
if (!proc.killed) proc.kill('SIGKILL');
|
|
1330
|
-
}, 5000);
|
|
1331
|
-
|
|
1332
|
-
if (!responseSent) {
|
|
1333
|
-
responseSent = true;
|
|
1334
|
-
res.status(500).json({
|
|
1335
|
-
error: 'Process timeout',
|
|
1336
|
-
message: \`Analysis exceeded timeout of \${timeoutMs / 1000} seconds\`
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
}, timeoutMs);
|
|
1341
|
-
|
|
1342
|
-
proc.stdout.on('data', (d) => {
|
|
1343
|
-
stdout += d.toString();
|
|
1344
|
-
});
|
|
1345
|
-
|
|
1346
|
-
proc.stderr.on('data', (d) => {
|
|
1347
|
-
stderr += d.toString();
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
proc.on('error', (error) => {
|
|
1351
|
-
console.log('❌ [analyze_prompt] Process error:', error.message);
|
|
1352
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
1353
|
-
if (!responseSent) {
|
|
1354
|
-
responseSent = true;
|
|
1355
|
-
return res.status(500).json({ error: error.message });
|
|
1356
|
-
}
|
|
1357
|
-
});
|
|
1358
|
-
|
|
1359
|
-
proc.on('close', (code, signal) => {
|
|
1360
|
-
console.log('🔵 [analyze_prompt] Process closed with code:', code);
|
|
1361
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
1362
|
-
|
|
1363
|
-
if (!responseSent) {
|
|
1364
|
-
responseSent = true;
|
|
1365
|
-
|
|
1366
|
-
// Try to parse JSON from the output
|
|
1367
|
-
try {
|
|
1368
|
-
// Look for JSON in the output - it might be wrapped in other text
|
|
1369
|
-
const jsonMatch = stdout.match(/\\{[\\s\\S]*"files"[\\s\\S]*\\}/);
|
|
1370
|
-
if (jsonMatch) {
|
|
1371
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
1372
|
-
console.log('✅ [analyze_prompt] Parsed files:', parsed.files);
|
|
1373
|
-
res.json({ files: parsed.files || [] });
|
|
1374
|
-
} else {
|
|
1375
|
-
console.log('⚠️ [analyze_prompt] No JSON found in output, returning raw');
|
|
1376
|
-
res.json({
|
|
1377
|
-
files: [],
|
|
1378
|
-
raw: stdout,
|
|
1379
|
-
warning: 'Could not parse structured response'
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
} catch (parseError) {
|
|
1383
|
-
console.log('⚠️ [analyze_prompt] JSON parse error:', parseError.message);
|
|
1384
|
-
res.json({
|
|
1385
|
-
files: [],
|
|
1386
|
-
raw: stdout,
|
|
1387
|
-
warning: 'Could not parse JSON: ' + parseError.message
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
});
|
|
1392
|
-
});
|
|
1393
|
-
|
|
1394
|
-
app.post('/prompt_agent', (req, res) => {
|
|
1395
|
-
console.log('🔵 [prompt_agent] Endpoint hit');
|
|
1396
|
-
const { prompt } = req.body;
|
|
1397
|
-
console.log('🔵 [prompt_agent] Received prompt:', prompt ? \`\${prompt.substring(0, 50)}...\` : 'none');
|
|
1398
|
-
|
|
1399
|
-
if (!prompt || typeof prompt !== 'string') {
|
|
1400
|
-
console.log('❌ [prompt_agent] Error: prompt required');
|
|
1401
|
-
return res.status(400).json({ error: 'prompt required' });
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// Capture beforeCommit
|
|
1405
|
-
let beforeCommit = '';
|
|
1406
|
-
try {
|
|
1407
|
-
beforeCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
|
|
1408
|
-
console.log('🔵 [prompt_agent] beforeCommit:', beforeCommit);
|
|
1409
|
-
} catch (e) {
|
|
1410
|
-
console.log('⚠️ [prompt_agent] Could not get beforeCommit:', e.message);
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// Capture initial state of modified files (files already dirty before job starts)
|
|
1414
|
-
const initiallyModifiedFiles = new Set();
|
|
1415
|
-
try {
|
|
1416
|
-
const initialStatus = execSync('git status --short', { cwd: ROOT }).toString();
|
|
1417
|
-
initialStatus.split('\\n').filter(Boolean).forEach(line => {
|
|
1418
|
-
const filePath = line.substring(3).trim();
|
|
1419
|
-
if (filePath) initiallyModifiedFiles.add(filePath);
|
|
1420
|
-
});
|
|
1421
|
-
console.log('🔵 [prompt_agent] Initially modified files:', Array.from(initiallyModifiedFiles));
|
|
1422
|
-
} catch (e) {
|
|
1423
|
-
console.log('⚠️ [prompt_agent] Could not get initial status:', e.message);
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Set up file change tracking - only track NEW changes during job
|
|
1427
|
-
const changedFiles = new Set();
|
|
1428
|
-
const pollInterval = setInterval(() => {
|
|
1429
|
-
try {
|
|
1430
|
-
const status = execSync('git status --short', { cwd: ROOT }).toString();
|
|
1431
|
-
status.split('\\n').filter(Boolean).forEach(line => {
|
|
1432
|
-
const filePath = line.substring(3).trim(); // Remove status prefix (XY + space)
|
|
1433
|
-
// Only add if this file was NOT already modified before the job started
|
|
1434
|
-
if (filePath && !initiallyModifiedFiles.has(filePath)) {
|
|
1435
|
-
const wasNew = !changedFiles.has(filePath);
|
|
1436
|
-
changedFiles.add(filePath);
|
|
1437
|
-
if (wasNew) {
|
|
1438
|
-
console.log('📁 [prompt_agent] New file changed:', filePath);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
});
|
|
1442
|
-
} catch (e) {
|
|
1443
|
-
// Ignore git status errors
|
|
1444
|
-
}
|
|
1445
|
-
}, 500);
|
|
1446
|
-
|
|
1447
|
-
// Configurable timeout (default 5 minutes)
|
|
1448
|
-
const timeoutMs = parseInt(req.body.timeout) || 5 * 60 * 1000;
|
|
1449
|
-
let timeoutId = null;
|
|
1450
|
-
let responseSent = false;
|
|
1451
|
-
|
|
1452
|
-
// Build command arguments
|
|
1453
|
-
const args = ['--print', '--force', '--workspace', '.', prompt];
|
|
1454
|
-
|
|
1455
|
-
console.log('🔵 [prompt_agent] Spawning cursor-agent process...');
|
|
1456
|
-
const proc = spawn(
|
|
1457
|
-
'cursor-agent',
|
|
1458
|
-
args,
|
|
1459
|
-
{
|
|
1460
|
-
cwd: ROOT,
|
|
1461
|
-
env: process.env,
|
|
1462
|
-
stdio: ['ignore', 'pipe', 'pipe'] // Ignore stdin, pipe stdout/stderr
|
|
1463
|
-
}
|
|
1464
|
-
);
|
|
1465
|
-
|
|
1466
|
-
console.log('🔵 [prompt_agent] Process spawned, PID:', proc.pid);
|
|
1467
|
-
|
|
1468
|
-
let stdout = '';
|
|
1469
|
-
let stderr = '';
|
|
1470
|
-
|
|
1471
|
-
// Set up timeout to kill process if it takes too long
|
|
1472
|
-
timeoutId = setTimeout(() => {
|
|
1473
|
-
if (!responseSent && proc && !proc.killed) {
|
|
1474
|
-
console.log('⏱️ [prompt_agent] Timeout reached, killing process...');
|
|
1475
|
-
clearInterval(pollInterval);
|
|
1476
|
-
proc.kill('SIGTERM');
|
|
1477
|
-
|
|
1478
|
-
// Force kill after a short grace period if SIGTERM doesn't work
|
|
1479
|
-
setTimeout(() => {
|
|
1480
|
-
if (!proc.killed) {
|
|
1481
|
-
console.log('💀 [prompt_agent] Force killing process...');
|
|
1482
|
-
proc.kill('SIGKILL');
|
|
1483
|
-
}
|
|
1484
|
-
}, 5000);
|
|
1485
|
-
|
|
1486
|
-
if (!responseSent) {
|
|
1487
|
-
responseSent = true;
|
|
1488
|
-
res.status(500).json({
|
|
1489
|
-
error: 'Process timeout',
|
|
1490
|
-
message: \`cursor-agent exceeded timeout of \${timeoutMs / 1000} seconds\`,
|
|
1491
|
-
code: -1,
|
|
1492
|
-
stdout,
|
|
1493
|
-
stderr,
|
|
1494
|
-
changedFiles: Array.from(changedFiles),
|
|
1495
|
-
beforeCommit,
|
|
1496
|
-
afterCommit: ''
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
}, timeoutMs);
|
|
1501
|
-
|
|
1502
|
-
proc.stdout.on('data', (d) => {
|
|
1503
|
-
const data = d.toString();
|
|
1504
|
-
console.log('📤 [prompt_agent] stdout data received:', data.length, 'bytes');
|
|
1505
|
-
stdout += data;
|
|
1506
|
-
});
|
|
1507
|
-
|
|
1508
|
-
proc.stderr.on('data', (d) => {
|
|
1509
|
-
const data = d.toString();
|
|
1510
|
-
console.log('⚠️ [prompt_agent] stderr data received:', data.length, 'bytes');
|
|
1511
|
-
stderr += data;
|
|
1512
|
-
});
|
|
1513
|
-
|
|
1514
|
-
proc.on('error', (error) => {
|
|
1515
|
-
console.log('❌ [prompt_agent] Process error:', error.message);
|
|
1516
|
-
clearInterval(pollInterval);
|
|
1517
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
1518
|
-
if (!responseSent) {
|
|
1519
|
-
responseSent = true;
|
|
1520
|
-
return res.status(500).json({ error: error.message });
|
|
1521
|
-
}
|
|
1522
|
-
});
|
|
1523
|
-
|
|
1524
|
-
proc.on('close', (code, signal) => {
|
|
1525
|
-
console.log('🔵 [prompt_agent] Process closed with code:', code, 'signal:', signal);
|
|
1526
|
-
console.log('🔵 [prompt_agent] stdout length:', stdout.length);
|
|
1527
|
-
console.log('🔵 [prompt_agent] stderr length:', stderr.length);
|
|
1528
|
-
|
|
1529
|
-
// Stop polling for file changes
|
|
1530
|
-
clearInterval(pollInterval);
|
|
1531
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
1532
|
-
|
|
1533
|
-
// Capture afterCommit
|
|
1534
|
-
let afterCommit = '';
|
|
1535
|
-
try {
|
|
1536
|
-
afterCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
|
|
1537
|
-
console.log('🔵 [prompt_agent] afterCommit:', afterCommit);
|
|
1538
|
-
} catch (e) {
|
|
1539
|
-
console.log('⚠️ [prompt_agent] Could not get afterCommit:', e.message);
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
if (!responseSent) {
|
|
1543
|
-
responseSent = true;
|
|
1544
|
-
// Check if process was killed due to timeout
|
|
1545
|
-
if (signal === 'SIGTERM' || signal === 'SIGKILL') {
|
|
1546
|
-
res.status(500).json({
|
|
1547
|
-
error: 'Process terminated',
|
|
1548
|
-
message: signal === 'SIGTERM' ? 'Process was terminated due to timeout' : 'Process was force killed',
|
|
1549
|
-
code: code || -1,
|
|
1550
|
-
stdout,
|
|
1551
|
-
stderr,
|
|
1552
|
-
changedFiles: Array.from(changedFiles),
|
|
1553
|
-
beforeCommit,
|
|
1554
|
-
afterCommit
|
|
1555
|
-
});
|
|
1556
|
-
} else {
|
|
1557
|
-
res.json({
|
|
1558
|
-
code,
|
|
1559
|
-
stdout,
|
|
1560
|
-
stderr,
|
|
1561
|
-
changedFiles: Array.from(changedFiles),
|
|
1562
|
-
beforeCommit,
|
|
1563
|
-
afterCommit
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
});
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
|
-
// Streaming version of prompt_agent using Server-Sent Events
|
|
1571
|
-
app.post('/prompt_agent_stream', (req, res) => {
|
|
1572
|
-
console.log('🔵 [prompt_agent_stream] Endpoint hit');
|
|
1573
|
-
const { prompt } = req.body;
|
|
1574
|
-
console.log('🔵 [prompt_agent_stream] Received prompt:', prompt ? \`\${prompt.substring(0, 50)}...\` : 'none');
|
|
1575
|
-
|
|
1576
|
-
if (!prompt || typeof prompt !== 'string') {
|
|
1577
|
-
console.log('❌ [prompt_agent_stream] Error: prompt required');
|
|
1578
|
-
return res.status(400).json({ error: 'prompt required' });
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// Set up SSE headers
|
|
1582
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
1583
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
1584
|
-
res.setHeader('Connection', 'keep-alive');
|
|
1585
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1586
|
-
res.flushHeaders();
|
|
1587
|
-
|
|
1588
|
-
// Helper to send SSE events
|
|
1589
|
-
const sendEvent = (type, data) => {
|
|
1590
|
-
res.write(\`data: \${JSON.stringify({ type, ...data })}\\n\\n\`);
|
|
1591
|
-
};
|
|
1592
|
-
|
|
1593
|
-
// Capture beforeCommit
|
|
1594
|
-
let beforeCommit = '';
|
|
1595
|
-
try {
|
|
1596
|
-
beforeCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
|
|
1597
|
-
console.log('🔵 [prompt_agent_stream] beforeCommit:', beforeCommit);
|
|
1598
|
-
} catch (e) {
|
|
1599
|
-
console.log('⚠️ [prompt_agent_stream] Could not get beforeCommit:', e.message);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
// Capture initial state of modified files
|
|
1603
|
-
const initiallyModifiedFiles = new Set();
|
|
1604
|
-
try {
|
|
1605
|
-
const initialStatus = execSync('git status --short', { cwd: ROOT }).toString();
|
|
1606
|
-
initialStatus.split('\\n').filter(Boolean).forEach(line => {
|
|
1607
|
-
const filePath = line.substring(3).trim();
|
|
1608
|
-
if (filePath) initiallyModifiedFiles.add(filePath);
|
|
1609
|
-
});
|
|
1610
|
-
} catch (e) {
|
|
1611
|
-
// Ignore
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
// Send starting event
|
|
1615
|
-
sendEvent('start', { beforeCommit });
|
|
1616
|
-
|
|
1617
|
-
// Set up file change tracking with real-time updates
|
|
1618
|
-
const changedFiles = new Set();
|
|
1619
|
-
const pollInterval = setInterval(() => {
|
|
1620
|
-
try {
|
|
1621
|
-
const status = execSync('git status --short', { cwd: ROOT }).toString();
|
|
1622
|
-
status.split('\\n').filter(Boolean).forEach(line => {
|
|
1623
|
-
const filePath = line.substring(3).trim();
|
|
1624
|
-
if (filePath && !initiallyModifiedFiles.has(filePath)) {
|
|
1625
|
-
if (!changedFiles.has(filePath)) {
|
|
1626
|
-
changedFiles.add(filePath);
|
|
1627
|
-
console.log('📁 [prompt_agent_stream] File changed:', filePath);
|
|
1628
|
-
// Send real-time update to client
|
|
1629
|
-
sendEvent('file_changed', { path: filePath });
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
});
|
|
1633
|
-
} catch (e) {
|
|
1634
|
-
// Ignore git status errors
|
|
1635
|
-
}
|
|
1636
|
-
}, 500);
|
|
1637
|
-
|
|
1638
|
-
const timeoutMs = parseInt(req.body.timeout) || 5 * 60 * 1000;
|
|
1639
|
-
let timeoutId = null;
|
|
1640
|
-
let responseSent = false;
|
|
1641
|
-
|
|
1642
|
-
const args = ['--print', '--force', '--workspace', '.', prompt];
|
|
1643
|
-
|
|
1644
|
-
console.log('🔵 [prompt_agent_stream] Spawning cursor-agent process...');
|
|
1645
|
-
const proc = spawn(
|
|
1646
|
-
'cursor-agent',
|
|
1647
|
-
args,
|
|
1648
|
-
{
|
|
1649
|
-
cwd: ROOT,
|
|
1650
|
-
env: process.env,
|
|
1651
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
1652
|
-
}
|
|
1653
|
-
);
|
|
1654
|
-
|
|
1655
|
-
console.log('🔵 [prompt_agent_stream] Process spawned, PID:', proc.pid);
|
|
1656
|
-
|
|
1657
|
-
let stdout = '';
|
|
1658
|
-
let stderr = '';
|
|
1659
|
-
|
|
1660
|
-
timeoutId = setTimeout(() => {
|
|
1661
|
-
if (!responseSent && proc && !proc.killed) {
|
|
1662
|
-
console.log('⏱️ [prompt_agent_stream] Timeout reached');
|
|
1663
|
-
clearInterval(pollInterval);
|
|
1664
|
-
proc.kill('SIGTERM');
|
|
1665
|
-
|
|
1666
|
-
setTimeout(() => {
|
|
1667
|
-
if (!proc.killed) proc.kill('SIGKILL');
|
|
1668
|
-
}, 5000);
|
|
1669
|
-
|
|
1670
|
-
if (!responseSent) {
|
|
1671
|
-
responseSent = true;
|
|
1672
|
-
sendEvent('error', {
|
|
1673
|
-
error: 'Process timeout',
|
|
1674
|
-
message: \`cursor-agent exceeded timeout of \${timeoutMs / 1000} seconds\`
|
|
1675
|
-
});
|
|
1676
|
-
sendEvent('complete', {
|
|
1677
|
-
code: -1,
|
|
1678
|
-
stdout,
|
|
1679
|
-
stderr,
|
|
1680
|
-
changedFiles: Array.from(changedFiles),
|
|
1681
|
-
beforeCommit,
|
|
1682
|
-
afterCommit: ''
|
|
1683
|
-
});
|
|
1684
|
-
res.end();
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
}, timeoutMs);
|
|
1688
|
-
|
|
1689
|
-
proc.stdout.on('data', (d) => {
|
|
1690
|
-
stdout += d.toString();
|
|
1691
|
-
});
|
|
1692
|
-
|
|
1693
|
-
proc.stderr.on('data', (d) => {
|
|
1694
|
-
stderr += d.toString();
|
|
1695
|
-
});
|
|
1696
|
-
|
|
1697
|
-
proc.on('error', (error) => {
|
|
1698
|
-
console.log('❌ [prompt_agent_stream] Process error:', error.message);
|
|
1699
|
-
clearInterval(pollInterval);
|
|
1700
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
1701
|
-
if (!responseSent) {
|
|
1702
|
-
responseSent = true;
|
|
1703
|
-
sendEvent('error', { error: error.message });
|
|
1704
|
-
res.end();
|
|
1705
|
-
}
|
|
1706
|
-
});
|
|
1707
|
-
|
|
1708
|
-
proc.on('close', (code, signal) => {
|
|
1709
|
-
console.log('🔵 [prompt_agent_stream] Process closed with code:', code);
|
|
1710
|
-
clearInterval(pollInterval);
|
|
1711
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
1712
|
-
|
|
1713
|
-
let afterCommit = '';
|
|
1714
|
-
try {
|
|
1715
|
-
afterCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
|
|
1716
|
-
} catch (e) {
|
|
1717
|
-
// Ignore
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
if (!responseSent) {
|
|
1721
|
-
responseSent = true;
|
|
1722
|
-
sendEvent('complete', {
|
|
1723
|
-
code,
|
|
1724
|
-
stdout,
|
|
1725
|
-
stderr,
|
|
1726
|
-
changedFiles: Array.from(changedFiles),
|
|
1727
|
-
beforeCommit,
|
|
1728
|
-
afterCommit
|
|
1729
|
-
});
|
|
1730
|
-
res.end();
|
|
1731
|
-
}
|
|
1732
|
-
});
|
|
1733
|
-
|
|
1734
|
-
// Handle client disconnect
|
|
1735
|
-
req.on('close', () => {
|
|
1736
|
-
console.log('🔵 [prompt_agent_stream] Client disconnected');
|
|
1737
|
-
clearInterval(pollInterval);
|
|
1738
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
1739
|
-
if (proc && !proc.killed) {
|
|
1740
|
-
proc.kill('SIGTERM');
|
|
1741
|
-
}
|
|
1742
|
-
});
|
|
1743
|
-
});
|
|
1744
|
-
|
|
1745
|
-
// Revert job endpoint to reset to a previous commit
|
|
1746
|
-
app.post('/revert_job', (req, res) => {
|
|
1747
|
-
console.log('🔵 [revert_job] Endpoint hit');
|
|
1748
|
-
const { beforeCommit } = req.body;
|
|
1749
|
-
|
|
1750
|
-
if (!beforeCommit || typeof beforeCommit !== 'string') {
|
|
1751
|
-
console.log('❌ [revert_job] Error: beforeCommit required');
|
|
1752
|
-
return res.status(400).json({ error: 'beforeCommit required' });
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
// Validate commit hash format (basic sanitization to prevent command injection)
|
|
1756
|
-
if (!/^[a-f0-9]{7,40}$/i.test(beforeCommit)) {
|
|
1757
|
-
console.log('❌ [revert_job] Error: invalid commit hash format');
|
|
1758
|
-
return res.status(400).json({ error: 'Invalid commit hash format' });
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
try {
|
|
1762
|
-
console.log('🔵 [revert_job] Resetting to commit:', beforeCommit);
|
|
1763
|
-
execSync(\`git reset --hard \${beforeCommit}\`, { cwd: ROOT });
|
|
1764
|
-
console.log('✅ [revert_job] Successfully reverted to commit:', beforeCommit);
|
|
1765
|
-
res.json({ success: true });
|
|
1766
|
-
} catch (e) {
|
|
1767
|
-
console.log('❌ [revert_job] Error:', e.message);
|
|
1768
|
-
res.status(500).json({ error: e.message });
|
|
1769
|
-
}
|
|
1770
|
-
});
|
|
1771
|
-
|
|
1772
|
-
// Shutdown endpoint to kill the server
|
|
1773
|
-
app.post('/shutdown', (req, res) => {
|
|
1774
|
-
console.log('🛑 Shutdown endpoint called - terminating server...');
|
|
1775
|
-
|
|
1776
|
-
res.json({
|
|
1777
|
-
success: true,
|
|
1778
|
-
message: 'Server shutting down...'
|
|
1779
|
-
});
|
|
1780
|
-
|
|
1781
|
-
// Close the server gracefully
|
|
1782
|
-
setTimeout(() => {
|
|
1783
|
-
process.exit(0);
|
|
1784
|
-
}, 100); // Small delay to ensure response is sent
|
|
1785
|
-
});
|
|
1786
|
-
|
|
1787
|
-
const port = 3001;
|
|
1788
|
-
app.listen(port, () => {
|
|
1789
|
-
console.log('📂 File server running on http://localhost:' + port);
|
|
1790
|
-
});
|
|
1791
|
-
`;
|
|
7
|
+
// Read templates at runtime from the templates directory
|
|
8
|
+
const IGNORE_FILE_CONTENT = fs.readFileSync(
|
|
9
|
+
path.join(__dirname, 'templates', 'ignore.txt'), 'utf8'
|
|
10
|
+
);
|
|
11
|
+
const RECEIVER_JS_CONTENT = fs.readFileSync(
|
|
12
|
+
path.join(__dirname, 'templates', 'receiver.js'), 'utf8'
|
|
13
|
+
);
|
|
1792
14
|
|
|
1793
15
|
async function main() {
|
|
1794
16
|
const currentDir = process.cwd();
|