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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/cli.js +811 -0
  3. 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
+ }