devbonzai 1.6.5
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 +16 -0
- package/cli.js +811 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
# Dev Bonzai
|
|
3
|
+
|
|
4
|
+
This repository contains the code behind the command:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx devbonzai
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
npm publish
|
|
11
|
+
|
|
12
|
+
# how to test:
|
|
13
|
+
- node cli.js
|
|
14
|
+
- this will setup a receiver in this directory
|
|
15
|
+
|
|
16
|
+
Used for Bonzai's linking functionality from web to local development environment.
|
package/cli.js
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn, exec } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const IGNORE_FILE_CONTENT = `# Ignore patterns for file listing
|
|
8
|
+
# Lines starting with # are comments
|
|
9
|
+
# Use * for wildcards and ** for recursive patterns
|
|
10
|
+
|
|
11
|
+
# Dependencies
|
|
12
|
+
node_modules/
|
|
13
|
+
package.json
|
|
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 } = require('child_process');
|
|
68
|
+
|
|
69
|
+
const app = express();
|
|
70
|
+
const ROOT = path.join(__dirname, '..');
|
|
71
|
+
|
|
72
|
+
app.use(cors());
|
|
73
|
+
app.use(express.json());
|
|
74
|
+
|
|
75
|
+
// Root route - simple API documentation
|
|
76
|
+
app.get('/', (req, res) => {
|
|
77
|
+
res.json({
|
|
78
|
+
message: 'Local File Server API',
|
|
79
|
+
endpoints: {
|
|
80
|
+
'GET /list': 'List all files in the directory',
|
|
81
|
+
'GET /read?path=<filepath>': 'Read file content',
|
|
82
|
+
'GET /git-churn?path=<filepath>&commits=30': 'Get git commit churn for a file',
|
|
83
|
+
'POST /write': 'Write file content (body: {path, content})',
|
|
84
|
+
'POST /write_dir': 'Create directory (body: {path})',
|
|
85
|
+
'POST /delete': 'Delete file or directory (body: {path})',
|
|
86
|
+
'POST /move': 'Move file or folder (body: {source, destination})',
|
|
87
|
+
'POST /open-cursor': 'Open Cursor (body: {path, line?})',
|
|
88
|
+
'POST /prompt_agent': 'Execute cursor-agent command (body: {prompt})',
|
|
89
|
+
'POST /shutdown': 'Gracefully shutdown the server'
|
|
90
|
+
},
|
|
91
|
+
example: 'Try: /list or /read?path=README.md'
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Read and parse ignore patterns from .ignore file
|
|
96
|
+
function getIgnorePatterns() {
|
|
97
|
+
try {
|
|
98
|
+
const ignorePath = path.join(__dirname, '.ignore');
|
|
99
|
+
if (fs.existsSync(ignorePath)) {
|
|
100
|
+
const content = fs.readFileSync(ignorePath, 'utf8');
|
|
101
|
+
return content
|
|
102
|
+
.split('\\n')
|
|
103
|
+
.map(line => line.trim())
|
|
104
|
+
.filter(line => line && !line.startsWith('#'))
|
|
105
|
+
.map(pattern => {
|
|
106
|
+
// Convert simple glob patterns to regex
|
|
107
|
+
if (pattern.endsWith('/')) {
|
|
108
|
+
// Directory pattern
|
|
109
|
+
pattern = pattern.slice(0, -1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Simple approach: escape dots and convert globs
|
|
113
|
+
pattern = pattern.replace(/\\./g, '\\\\.');
|
|
114
|
+
pattern = pattern.replace(/\\*\\*/g, '|||DOUBLESTAR|||');
|
|
115
|
+
pattern = pattern.replace(/\\*/g, '[^/]*');
|
|
116
|
+
pattern = pattern.replace(/\\|\\|\\|DOUBLESTAR\\|\\|\\|/g, '.*');
|
|
117
|
+
|
|
118
|
+
return new RegExp('^' + pattern + '(/.*)?$');
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.warn('Could not read .ignore file:', e.message);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Default ignore patterns if no .ignore file exists
|
|
126
|
+
return [
|
|
127
|
+
/^node_modules(\\/.*)?$/,
|
|
128
|
+
/^\\.git(\\/.*)?$/,
|
|
129
|
+
/^\\.DS_Store$/,
|
|
130
|
+
/^\\.env$/
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if a path should be ignored
|
|
135
|
+
function shouldIgnore(relativePath, ignorePatterns) {
|
|
136
|
+
return ignorePatterns.some(pattern => pattern.test(relativePath));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extract top-level functions from a Python file
|
|
140
|
+
function extractPythonFunctions(filePath) {
|
|
141
|
+
try {
|
|
142
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
143
|
+
const lines = content.split('\\n');
|
|
144
|
+
const functions = [];
|
|
145
|
+
let currentFunction = null;
|
|
146
|
+
let decorators = [];
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < lines.length; i++) {
|
|
149
|
+
const line = lines[i];
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
|
|
152
|
+
// Calculate indentation level
|
|
153
|
+
const match = line.match(/^\\s*/);
|
|
154
|
+
const currentIndent = match ? match[0].length : 0;
|
|
155
|
+
|
|
156
|
+
// Check for decorators (only at top level, before function)
|
|
157
|
+
if (trimmed.startsWith('@') && currentIndent === 0) {
|
|
158
|
+
decorators.push(line);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check if this is a top-level function definition
|
|
163
|
+
const funcMatch = trimmed.match(/^def\\s+(\\w+)\\s*\\(/);
|
|
164
|
+
|
|
165
|
+
if (funcMatch && currentIndent === 0) {
|
|
166
|
+
// Save previous function if exists
|
|
167
|
+
if (currentFunction) {
|
|
168
|
+
currentFunction.content = currentFunction.content.trim();
|
|
169
|
+
functions.push(currentFunction);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Start new function
|
|
173
|
+
const functionName = funcMatch[1];
|
|
174
|
+
let functionContent = '';
|
|
175
|
+
|
|
176
|
+
// Add decorators if any
|
|
177
|
+
if (decorators.length > 0) {
|
|
178
|
+
functionContent = decorators.join('\\n') + '\\n';
|
|
179
|
+
decorators = [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
functionContent += line;
|
|
183
|
+
|
|
184
|
+
currentFunction = {
|
|
185
|
+
name: functionName,
|
|
186
|
+
content: functionContent,
|
|
187
|
+
startLine: i + 1,
|
|
188
|
+
endLine: i + 1
|
|
189
|
+
};
|
|
190
|
+
} else if (currentFunction) {
|
|
191
|
+
// We're processing lines after a function definition
|
|
192
|
+
if (currentIndent === 0 && trimmed && !trimmed.startsWith('#')) {
|
|
193
|
+
// Back to top level with non-comment content - function ended
|
|
194
|
+
currentFunction.content = currentFunction.content.trim();
|
|
195
|
+
functions.push(currentFunction);
|
|
196
|
+
currentFunction = null;
|
|
197
|
+
|
|
198
|
+
// Check if this line starts a new function
|
|
199
|
+
if (funcMatch) {
|
|
200
|
+
const functionName = funcMatch[1];
|
|
201
|
+
let functionContent = '';
|
|
202
|
+
|
|
203
|
+
if (decorators.length > 0) {
|
|
204
|
+
functionContent = decorators.join('\\n') + '\\n';
|
|
205
|
+
decorators = [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
functionContent += line;
|
|
209
|
+
|
|
210
|
+
currentFunction = {
|
|
211
|
+
name: functionName,
|
|
212
|
+
content: functionContent,
|
|
213
|
+
startLine: i + 1,
|
|
214
|
+
endLine: i + 1
|
|
215
|
+
};
|
|
216
|
+
} else if (trimmed.startsWith('@')) {
|
|
217
|
+
decorators.push(line);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// Still inside function (indented or empty/comment line)
|
|
221
|
+
currentFunction.content += '\\n' + line;
|
|
222
|
+
currentFunction.endLine = i + 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Don't forget the last function
|
|
228
|
+
if (currentFunction) {
|
|
229
|
+
currentFunction.content = currentFunction.content.trim();
|
|
230
|
+
functions.push(currentFunction);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return functions;
|
|
234
|
+
} catch (e) {
|
|
235
|
+
// If parsing fails (invalid Python, etc.), return empty array
|
|
236
|
+
console.warn('Failed to parse Python file:', filePath, e.message);
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Recursively list all files in a directory, respecting ignore patterns
|
|
242
|
+
function listAllFiles(dir, base = '', ignorePatterns = null) {
|
|
243
|
+
if (ignorePatterns === null) {
|
|
244
|
+
ignorePatterns = getIgnorePatterns();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let results = [];
|
|
248
|
+
const list = fs.readdirSync(dir);
|
|
249
|
+
|
|
250
|
+
for (const file of list) {
|
|
251
|
+
const fullPath = path.join(dir, file);
|
|
252
|
+
const relativePath = path.join(base, file);
|
|
253
|
+
|
|
254
|
+
// Check if this path should be ignored
|
|
255
|
+
if (shouldIgnore(relativePath, ignorePatterns)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const stat = fs.statSync(fullPath);
|
|
260
|
+
if (stat && stat.isDirectory()) {
|
|
261
|
+
// Add the directory itself to results
|
|
262
|
+
results.push(relativePath + '/');
|
|
263
|
+
// Recursively list files inside the directory
|
|
264
|
+
results = results.concat(listAllFiles(fullPath, relativePath, ignorePatterns));
|
|
265
|
+
} else {
|
|
266
|
+
results.push(relativePath);
|
|
267
|
+
|
|
268
|
+
// If this is a Python file, extract functions and add them as virtual files
|
|
269
|
+
if (file.endsWith('.py')) {
|
|
270
|
+
const functions = extractPythonFunctions(fullPath);
|
|
271
|
+
for (const func of functions) {
|
|
272
|
+
// Add function file as downstream from the Python file
|
|
273
|
+
// Format: <python_file_path>/<function_name.function>
|
|
274
|
+
const functionFileName = func.name + '.function';
|
|
275
|
+
const functionFilePath = relativePath + '/' + functionFileName;
|
|
276
|
+
results.push(functionFilePath);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return results;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
app.get('/list', (req, res) => {
|
|
285
|
+
try {
|
|
286
|
+
const rootName = path.basename(ROOT);
|
|
287
|
+
const files = listAllFiles(ROOT, rootName);
|
|
288
|
+
res.json({ files });
|
|
289
|
+
} catch (e) {
|
|
290
|
+
res.status(500).send(e.message);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
app.get('/read', (req, res) => {
|
|
295
|
+
try {
|
|
296
|
+
const requestedPath = req.query.path || '';
|
|
297
|
+
const filePath = path.join(ROOT, requestedPath);
|
|
298
|
+
|
|
299
|
+
if (!filePath.startsWith(ROOT)) {
|
|
300
|
+
return res.status(400).send('Invalid path');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if this is a .function file request
|
|
304
|
+
if (requestedPath.endsWith('.function')) {
|
|
305
|
+
// Extract function name and Python file path
|
|
306
|
+
// Path format: <python_file_path>/<function_name.function>
|
|
307
|
+
const functionFileName = path.basename(requestedPath, '.function');
|
|
308
|
+
const pythonFilePath = path.dirname(filePath);
|
|
309
|
+
|
|
310
|
+
// Check if the Python file exists
|
|
311
|
+
try {
|
|
312
|
+
if (!fs.existsSync(pythonFilePath) || !pythonFilePath.endsWith('.py')) {
|
|
313
|
+
return res.status(404).send('Parent Python file not found');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Extract functions from the Python file
|
|
317
|
+
const functions = extractPythonFunctions(pythonFilePath);
|
|
318
|
+
const targetFunction = functions.find(f => f.name === functionFileName);
|
|
319
|
+
|
|
320
|
+
if (!targetFunction) {
|
|
321
|
+
return res.status(404).send('Function not found in Python file');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return res.json({ content: targetFunction.content });
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return res.status(500).send('Error reading function: ' + e.message);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Regular file read
|
|
331
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
332
|
+
res.json({ content });
|
|
333
|
+
} catch (e) {
|
|
334
|
+
res.status(500).send(e.message);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
app.get('/git-churn', (req, res) => {
|
|
339
|
+
try {
|
|
340
|
+
const filePath = path.join(ROOT, req.query.path || '');
|
|
341
|
+
if (!filePath.startsWith(ROOT)) {
|
|
342
|
+
return res.status(400).json({ error: 'Invalid path' });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Get commits parameter, default to 30
|
|
346
|
+
const commits = parseInt(req.query.commits) || 30;
|
|
347
|
+
|
|
348
|
+
// Get relative path from ROOT for git command
|
|
349
|
+
const relativePath = path.relative(ROOT, filePath);
|
|
350
|
+
|
|
351
|
+
// Build git log command with relative path
|
|
352
|
+
const gitCommand = \`git log --oneline --all -\${commits} -- "\${relativePath}"\`;
|
|
353
|
+
|
|
354
|
+
exec(gitCommand, { cwd: ROOT }, (error, stdout, stderr) => {
|
|
355
|
+
// If git command fails (no repo, file not tracked, etc.), return 0 churn
|
|
356
|
+
if (error) {
|
|
357
|
+
// Check if it's because file is not in git or no git repo
|
|
358
|
+
if (error.code === 128 || stderr.includes('not a git repository')) {
|
|
359
|
+
return res.json({ churn: 0 });
|
|
360
|
+
}
|
|
361
|
+
// For other errors, still return 0 churn gracefully
|
|
362
|
+
return res.json({ churn: 0 });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Count non-empty lines (each line is a commit)
|
|
366
|
+
const commitCount = stdout.trim().split('\\n').filter(line => line.trim().length > 0).length;
|
|
367
|
+
|
|
368
|
+
res.json({ churn: commitCount });
|
|
369
|
+
});
|
|
370
|
+
} catch (e) {
|
|
371
|
+
// Handle any other errors gracefully
|
|
372
|
+
res.status(500).json({ error: e.message, churn: 0 });
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
app.post('/write', (req, res) => {
|
|
377
|
+
try {
|
|
378
|
+
const filePath = path.join(ROOT, req.body.path || '');
|
|
379
|
+
if (!filePath.startsWith(ROOT)) {
|
|
380
|
+
return res.status(400).send('Invalid path');
|
|
381
|
+
}
|
|
382
|
+
fs.writeFileSync(filePath, req.body.content, 'utf8');
|
|
383
|
+
res.json({ status: 'ok' });
|
|
384
|
+
} catch (e) {
|
|
385
|
+
res.status(500).send(e.message);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
app.post('/write_dir', (req, res) => {
|
|
390
|
+
try {
|
|
391
|
+
const dirPath = path.join(ROOT, req.body.path || '');
|
|
392
|
+
if (!dirPath.startsWith(ROOT)) {
|
|
393
|
+
return res.status(400).send('Invalid path');
|
|
394
|
+
}
|
|
395
|
+
// Create directory recursively (creates parent directories if they don't exist)
|
|
396
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
397
|
+
res.json({ status: 'ok' });
|
|
398
|
+
} catch (e) {
|
|
399
|
+
res.status(500).send(e.message);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
app.post('/delete', (req, res) => {
|
|
404
|
+
try {
|
|
405
|
+
const targetPath = path.join(ROOT, req.body.path || '');
|
|
406
|
+
if (!targetPath.startsWith(ROOT)) {
|
|
407
|
+
return res.status(400).send('Invalid path');
|
|
408
|
+
}
|
|
409
|
+
// Delete file or directory recursively
|
|
410
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
411
|
+
res.json({ status: 'ok' });
|
|
412
|
+
} catch (e) {
|
|
413
|
+
res.status(500).send(e.message);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
app.post('/move', (req, res) => {
|
|
418
|
+
try {
|
|
419
|
+
const sourcePath = path.join(ROOT, req.body.source || '');
|
|
420
|
+
const destinationPath = path.join(ROOT, req.body.destination || '');
|
|
421
|
+
|
|
422
|
+
// Validate both paths are within ROOT directory
|
|
423
|
+
if (!sourcePath.startsWith(ROOT) || !destinationPath.startsWith(ROOT)) {
|
|
424
|
+
return res.status(400).send('Invalid path');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Check if source exists
|
|
428
|
+
if (!fs.existsSync(sourcePath)) {
|
|
429
|
+
return res.status(400).send('Source path does not exist');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Ensure destination directory exists
|
|
433
|
+
const destinationDir = path.dirname(destinationPath);
|
|
434
|
+
if (!fs.existsSync(destinationDir)) {
|
|
435
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Move the file or folder
|
|
439
|
+
fs.renameSync(sourcePath, destinationPath);
|
|
440
|
+
res.json({ status: 'ok' });
|
|
441
|
+
} catch (e) {
|
|
442
|
+
res.status(500).send(e.message);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
app.post('/open-cursor', (req, res) => {
|
|
447
|
+
try {
|
|
448
|
+
const requestedPath = req.body.path || '';
|
|
449
|
+
|
|
450
|
+
// Resolve path relative to ROOT (similar to other endpoints)
|
|
451
|
+
// If path is absolute and within ROOT, use it directly
|
|
452
|
+
// Otherwise, resolve it relative to ROOT
|
|
453
|
+
let filePath;
|
|
454
|
+
if (path.isAbsolute(requestedPath)) {
|
|
455
|
+
// If absolute path, check if it's within ROOT
|
|
456
|
+
if (requestedPath.startsWith(ROOT)) {
|
|
457
|
+
filePath = requestedPath;
|
|
458
|
+
} else {
|
|
459
|
+
// Path might contain incorrect segments (like "codemaps")
|
|
460
|
+
// Try to find ROOT in the path and extract the relative part
|
|
461
|
+
const rootIndex = requestedPath.indexOf(ROOT);
|
|
462
|
+
if (rootIndex !== -1) {
|
|
463
|
+
// Extract the part after ROOT and remove leading slashes
|
|
464
|
+
let relativePart = requestedPath.substring(rootIndex + ROOT.length);
|
|
465
|
+
while (relativePart.startsWith('/')) {
|
|
466
|
+
relativePart = relativePart.substring(1);
|
|
467
|
+
}
|
|
468
|
+
filePath = path.join(ROOT, relativePart);
|
|
469
|
+
} else {
|
|
470
|
+
return res.status(400).json({ error: 'Invalid path: path must be within project root' });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
// Relative path - resolve relative to ROOT
|
|
475
|
+
// Remove root directory name prefix if present (from /list endpoint format)
|
|
476
|
+
const rootName = path.basename(ROOT);
|
|
477
|
+
let relativePath = requestedPath;
|
|
478
|
+
if (relativePath.startsWith(rootName + '/')) {
|
|
479
|
+
relativePath = relativePath.substring(rootName.length + 1);
|
|
480
|
+
}
|
|
481
|
+
filePath = path.join(ROOT, relativePath);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Validate the resolved path is within ROOT
|
|
485
|
+
if (!filePath.startsWith(ROOT)) {
|
|
486
|
+
return res.status(400).json({ error: 'Invalid path' });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const { line } = req.body;
|
|
490
|
+
|
|
491
|
+
// Always use cursor CLI command first (it handles line numbers correctly)
|
|
492
|
+
const cursorCommands = [
|
|
493
|
+
'cursor',
|
|
494
|
+
'/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
|
|
495
|
+
'/usr/local/bin/cursor',
|
|
496
|
+
'code'
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
const tryCommand = (commandIndex = 0) => {
|
|
500
|
+
if (commandIndex >= cursorCommands.length) {
|
|
501
|
+
return res.status(500).json({
|
|
502
|
+
error: 'Cursor not found. Please install Cursor CLI or check Cursor installation.'
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Use proper Cursor CLI syntax for line numbers
|
|
507
|
+
const command = line
|
|
508
|
+
? \`\${cursorCommands[commandIndex]} --goto "\${filePath}:\${line}"\`
|
|
509
|
+
: \`\${cursorCommands[commandIndex]} "\${filePath}"\`;
|
|
510
|
+
|
|
511
|
+
exec(command, (error, stdout, stderr) => {
|
|
512
|
+
if (error && error.code === 127) {
|
|
513
|
+
// Command not found, try next one
|
|
514
|
+
tryCommand(commandIndex + 1);
|
|
515
|
+
} else if (error) {
|
|
516
|
+
console.error('Error opening Cursor:', error);
|
|
517
|
+
return res.status(500).json({ error: error.message });
|
|
518
|
+
} else {
|
|
519
|
+
// File opened successfully, now bring Cursor to front
|
|
520
|
+
const isMac = process.platform === 'darwin';
|
|
521
|
+
if (isMac) {
|
|
522
|
+
// Use AppleScript to bring Cursor to the front
|
|
523
|
+
exec('osascript -e "tell application \\\\"Cursor\\\\" to activate"', (activateError) => {
|
|
524
|
+
if (activateError) {
|
|
525
|
+
console.log('Could not activate Cursor, but file opened successfully');
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Additional command to ensure it's really in front
|
|
530
|
+
setTimeout(() => {
|
|
531
|
+
exec('osascript -e "tell application \\\\"System Events\\\\" to set frontmost of process \\\\"Cursor\\\\" to true"', () => {
|
|
532
|
+
// Don't worry if this fails
|
|
533
|
+
});
|
|
534
|
+
}, 500);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
res.json({ success: true, message: 'Cursor opened and focused successfully' });
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
tryCommand();
|
|
543
|
+
} catch (e) {
|
|
544
|
+
res.status(500).json({ error: e.message });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
app.post('/prompt_agent', (req, res) => {
|
|
549
|
+
console.log('šµ [prompt_agent] Endpoint hit');
|
|
550
|
+
const { prompt } = req.body;
|
|
551
|
+
console.log('šµ [prompt_agent] Received prompt:', prompt ? \`\${prompt.substring(0, 50)}...\` : 'none');
|
|
552
|
+
|
|
553
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
554
|
+
console.log('ā [prompt_agent] Error: prompt required');
|
|
555
|
+
return res.status(400).json({ error: 'prompt required' });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Configurable timeout (default 5 minutes)
|
|
559
|
+
const timeoutMs = parseInt(req.body.timeout) || 5 * 60 * 1000;
|
|
560
|
+
let timeoutId = null;
|
|
561
|
+
let responseSent = false;
|
|
562
|
+
|
|
563
|
+
// Build command arguments
|
|
564
|
+
const args = ['--print', '--force', '--workspace', '.', prompt];
|
|
565
|
+
|
|
566
|
+
console.log('šµ [prompt_agent] Spawning cursor-agent process...');
|
|
567
|
+
const proc = spawn(
|
|
568
|
+
'cursor-agent',
|
|
569
|
+
args,
|
|
570
|
+
{
|
|
571
|
+
cwd: ROOT,
|
|
572
|
+
env: process.env,
|
|
573
|
+
stdio: ['ignore', 'pipe', 'pipe'] // Ignore stdin, pipe stdout/stderr
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
console.log('šµ [prompt_agent] Process spawned, PID:', proc.pid);
|
|
578
|
+
|
|
579
|
+
let stdout = '';
|
|
580
|
+
let stderr = '';
|
|
581
|
+
|
|
582
|
+
// Set up timeout to kill process if it takes too long
|
|
583
|
+
timeoutId = setTimeout(() => {
|
|
584
|
+
if (!responseSent && proc && !proc.killed) {
|
|
585
|
+
console.log('ā±ļø [prompt_agent] Timeout reached, killing process...');
|
|
586
|
+
proc.kill('SIGTERM');
|
|
587
|
+
|
|
588
|
+
// Force kill after a short grace period if SIGTERM doesn't work
|
|
589
|
+
setTimeout(() => {
|
|
590
|
+
if (!proc.killed) {
|
|
591
|
+
console.log('š [prompt_agent] Force killing process...');
|
|
592
|
+
proc.kill('SIGKILL');
|
|
593
|
+
}
|
|
594
|
+
}, 5000);
|
|
595
|
+
|
|
596
|
+
if (!responseSent) {
|
|
597
|
+
responseSent = true;
|
|
598
|
+
res.status(500).json({
|
|
599
|
+
error: 'Process timeout',
|
|
600
|
+
message: \`cursor-agent exceeded timeout of \${timeoutMs / 1000} seconds\`,
|
|
601
|
+
code: -1,
|
|
602
|
+
stdout,
|
|
603
|
+
stderr
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}, timeoutMs);
|
|
608
|
+
|
|
609
|
+
proc.stdout.on('data', (d) => {
|
|
610
|
+
const data = d.toString();
|
|
611
|
+
console.log('š¤ [prompt_agent] stdout data received:', data.length, 'bytes');
|
|
612
|
+
stdout += data;
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
proc.stderr.on('data', (d) => {
|
|
616
|
+
const data = d.toString();
|
|
617
|
+
console.log('ā ļø [prompt_agent] stderr data received:', data.length, 'bytes');
|
|
618
|
+
stderr += data;
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
proc.on('error', (error) => {
|
|
622
|
+
console.log('ā [prompt_agent] Process error:', error.message);
|
|
623
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
624
|
+
if (!responseSent) {
|
|
625
|
+
responseSent = true;
|
|
626
|
+
return res.status(500).json({ error: error.message });
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
proc.on('close', (code, signal) => {
|
|
631
|
+
console.log('šµ [prompt_agent] Process closed with code:', code, 'signal:', signal);
|
|
632
|
+
console.log('šµ [prompt_agent] stdout length:', stdout.length);
|
|
633
|
+
console.log('šµ [prompt_agent] stderr length:', stderr.length);
|
|
634
|
+
|
|
635
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
636
|
+
|
|
637
|
+
if (!responseSent) {
|
|
638
|
+
responseSent = true;
|
|
639
|
+
// Check if process was killed due to timeout
|
|
640
|
+
if (signal === 'SIGTERM' || signal === 'SIGKILL') {
|
|
641
|
+
res.status(500).json({
|
|
642
|
+
error: 'Process terminated',
|
|
643
|
+
message: signal === 'SIGTERM' ? 'Process was terminated due to timeout' : 'Process was force killed',
|
|
644
|
+
code: code || -1,
|
|
645
|
+
stdout,
|
|
646
|
+
stderr
|
|
647
|
+
});
|
|
648
|
+
} else {
|
|
649
|
+
res.json({
|
|
650
|
+
code,
|
|
651
|
+
stdout,
|
|
652
|
+
stderr
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Shutdown endpoint to kill the server
|
|
660
|
+
app.post('/shutdown', (req, res) => {
|
|
661
|
+
console.log('š Shutdown endpoint called - terminating server...');
|
|
662
|
+
|
|
663
|
+
res.json({
|
|
664
|
+
success: true,
|
|
665
|
+
message: 'Server shutting down...'
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Close the server gracefully
|
|
669
|
+
setTimeout(() => {
|
|
670
|
+
process.exit(0);
|
|
671
|
+
}, 100); // Small delay to ensure response is sent
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const port = 3001;
|
|
675
|
+
app.listen(port, () => {
|
|
676
|
+
console.log('š File server running on http://localhost:' + port);
|
|
677
|
+
});
|
|
678
|
+
`;
|
|
679
|
+
|
|
680
|
+
async function main() {
|
|
681
|
+
const currentDir = process.cwd();
|
|
682
|
+
const bonzaiDir = path.join(currentDir, 'bonzai');
|
|
683
|
+
const receiverPath = path.join(bonzaiDir, 'receiver.js');
|
|
684
|
+
|
|
685
|
+
console.log('š Setting up local file server...');
|
|
686
|
+
|
|
687
|
+
// Create bonzai directory
|
|
688
|
+
if (!fs.existsSync(bonzaiDir)) {
|
|
689
|
+
console.log('š Creating bonzai directory...');
|
|
690
|
+
fs.mkdirSync(bonzaiDir);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Write receiver.js to current directory (root level)
|
|
694
|
+
console.log('š Writing receiver.js...');
|
|
695
|
+
fs.writeFileSync(receiverPath, RECEIVER_JS_CONTENT);
|
|
696
|
+
|
|
697
|
+
// Make it executable
|
|
698
|
+
fs.chmodSync(receiverPath, '755');
|
|
699
|
+
|
|
700
|
+
// Write .ignore file in bonzai directory
|
|
701
|
+
const ignoreTargetPath = path.join(bonzaiDir, '.ignore');
|
|
702
|
+
|
|
703
|
+
if (!fs.existsSync(ignoreTargetPath)) {
|
|
704
|
+
console.log('š Writing .ignore file...');
|
|
705
|
+
fs.writeFileSync(ignoreTargetPath, IGNORE_FILE_CONTENT);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
console.log('š¦ Installing dependencies...');
|
|
709
|
+
|
|
710
|
+
// Check if package.json exists in bonzai directory
|
|
711
|
+
const packageJsonPath = path.join(bonzaiDir, 'package.json');
|
|
712
|
+
let packageJson = {};
|
|
713
|
+
|
|
714
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
715
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
716
|
+
} else {
|
|
717
|
+
packageJson = {
|
|
718
|
+
name: "bonzai-server",
|
|
719
|
+
version: "1.0.0",
|
|
720
|
+
description: "Dependencies for devbonzai file server",
|
|
721
|
+
main: "../receiver.js",
|
|
722
|
+
scripts: {
|
|
723
|
+
test: "echo \"Error: no test specified\" && exit 1"
|
|
724
|
+
},
|
|
725
|
+
author: "",
|
|
726
|
+
license: "ISC"
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Add dependencies
|
|
731
|
+
if (!packageJson.dependencies) {
|
|
732
|
+
packageJson.dependencies = {};
|
|
733
|
+
}
|
|
734
|
+
packageJson.dependencies.express = "^4.18.2";
|
|
735
|
+
packageJson.dependencies.cors = "^2.8.5";
|
|
736
|
+
packageJson.dependencies["body-parser"] = "^1.20.2";
|
|
737
|
+
packageJson.dependencies["raw-body"] = "^2.5.2";
|
|
738
|
+
|
|
739
|
+
// Add script to run receiver
|
|
740
|
+
if (!packageJson.scripts) {
|
|
741
|
+
packageJson.scripts = {};
|
|
742
|
+
}
|
|
743
|
+
packageJson.scripts["file-server"] = "node receiver.js";
|
|
744
|
+
|
|
745
|
+
// Write package.json to bonzai directory
|
|
746
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
747
|
+
|
|
748
|
+
// Install dependencies in bonzai directory
|
|
749
|
+
return new Promise((resolve, reject) => {
|
|
750
|
+
const npm = spawn('npm', ['install'], {
|
|
751
|
+
stdio: 'inherit',
|
|
752
|
+
cwd: bonzaiDir
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
npm.on('close', (code) => {
|
|
756
|
+
if (code === 0) {
|
|
757
|
+
console.log('Listner end points successfully deployed');
|
|
758
|
+
console.log('');
|
|
759
|
+
console.log('All code stays on your machine');
|
|
760
|
+
console.log('');
|
|
761
|
+
console.log('');
|
|
762
|
+
console.log('');
|
|
763
|
+
console.log('Relay server running on localhost:3001');
|
|
764
|
+
console.log('');
|
|
765
|
+
console.log('Diagram avaialble at https://www.codemaps.me/ ');
|
|
766
|
+
console.log('');
|
|
767
|
+
|
|
768
|
+
// Start the server automatically
|
|
769
|
+
const server = spawn('node', ['receiver.js'], {
|
|
770
|
+
stdio: 'inherit',
|
|
771
|
+
cwd: bonzaiDir
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// Open browser automatically
|
|
775
|
+
exec('open https://www.codemaps.me/');
|
|
776
|
+
|
|
777
|
+
// Handle server process
|
|
778
|
+
server.on('close', (serverCode) => {
|
|
779
|
+
console.log(`\nš” Server stopped with code ${serverCode}`);
|
|
780
|
+
process.exit(serverCode);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
server.on('error', (err) => {
|
|
784
|
+
console.error('ā Error starting server:', err.message);
|
|
785
|
+
process.exit(1);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// Handle cleanup on exit
|
|
789
|
+
process.on('SIGINT', () => {
|
|
790
|
+
console.log('\nš Shutting down server...');
|
|
791
|
+
server.kill('SIGINT');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
process.on('SIGTERM', () => {
|
|
795
|
+
console.log('\nš Shutting down server...');
|
|
796
|
+
server.kill('SIGTERM');
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
resolve();
|
|
800
|
+
} else {
|
|
801
|
+
reject(new Error('npm install failed with code ' + code));
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
npm.on('error', (err) => {
|
|
806
|
+
reject(err);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devbonzai",
|
|
3
|
+
"version": "1.6.5",
|
|
4
|
+
"description": "Quickly set up a local file server in any repository for browser-based file access",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"devbonzai": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"file-server",
|
|
14
|
+
"local-development",
|
|
15
|
+
"browser-access",
|
|
16
|
+
"cli"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"express": "^4.18.2",
|
|
22
|
+
"cors": "^2.8.5",
|
|
23
|
+
"body-parser": "^1.20.2",
|
|
24
|
+
"raw-body": "^2.5.2"
|
|
25
|
+
}
|
|
26
|
+
}
|