bonzai-burn 1.0.15 → 1.0.17
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/package.json +8 -3
- package/payload-bonzai/config.json +0 -1
- package/payload-bonzai/graph-templates/config.js +18 -0
- package/payload-bonzai/graph-templates/handlers/delete.js +20 -0
- package/payload-bonzai/graph-templates/handlers/index.js +19 -0
- package/payload-bonzai/graph-templates/handlers/list.js +16 -0
- package/payload-bonzai/graph-templates/handlers/open-cursor.js +108 -0
- package/payload-bonzai/graph-templates/handlers/read.js +120 -0
- package/payload-bonzai/graph-templates/handlers/scan_code_quality.js +144 -0
- package/payload-bonzai/graph-templates/handlers/shutdown.js +16 -0
- package/payload-bonzai/graph-templates/handlers/terminal.js +162 -0
- package/payload-bonzai/graph-templates/ignore.txt +58 -0
- package/payload-bonzai/graph-templates/receiver.js +42 -0
- package/payload-bonzai/graph-templates/utils/fileList.js +96 -0
- package/payload-bonzai/graph-templates/utils/ignore.js +53 -0
- package/payload-bonzai/graph-templates/utils/parsers.js +720 -0
- package/src/bgraph.js +192 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bonzai-burn",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Git branch-based cleanup tool with bburn, baccept, and brevert commands",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"bonzai-burn": "./src/index.js",
|
|
9
9
|
"bburn": "./src/bburn.js",
|
|
10
10
|
"baccept": "./src/baccept.js",
|
|
11
|
-
"brevert": "./src/brevert.js"
|
|
11
|
+
"brevert": "./src/brevert.js",
|
|
12
|
+
"bgraph": "./src/bgraph.js",
|
|
13
|
+
"bonzai-mcp": "./src/mcp-server.js"
|
|
12
14
|
},
|
|
13
15
|
"keywords": [
|
|
14
16
|
"git",
|
|
@@ -22,5 +24,8 @@
|
|
|
22
24
|
"files": [
|
|
23
25
|
"src",
|
|
24
26
|
"payload-bonzai"
|
|
25
|
-
]
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
30
|
+
}
|
|
26
31
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
// Root directory (one level up from templates/)
|
|
4
|
+
const ROOT = path.join(__dirname, '..');
|
|
5
|
+
|
|
6
|
+
// Initialize babelParser (optional dependency)
|
|
7
|
+
let babelParser = null;
|
|
8
|
+
try {
|
|
9
|
+
babelParser = require('./node_modules/@babel/parser');
|
|
10
|
+
} catch (e) {
|
|
11
|
+
// Babel parser not available, will fall back gracefully
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
ROOT,
|
|
16
|
+
babelParser
|
|
17
|
+
};
|
|
18
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ROOT } = require('../config');
|
|
4
|
+
|
|
5
|
+
function deleteHandler(req, res) {
|
|
6
|
+
try {
|
|
7
|
+
const targetPath = path.join(ROOT, req.body.path || '');
|
|
8
|
+
if (!targetPath.startsWith(ROOT)) {
|
|
9
|
+
return res.status(400).send('Invalid path');
|
|
10
|
+
}
|
|
11
|
+
// Delete file or directory recursively
|
|
12
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
13
|
+
res.json({ status: 'ok' });
|
|
14
|
+
} catch (e) {
|
|
15
|
+
res.status(500).send(e.message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = deleteHandler;
|
|
20
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Root route - simple API documentation
|
|
2
|
+
function indexHandler(req, res) {
|
|
3
|
+
res.json({
|
|
4
|
+
message: 'Local File Server API',
|
|
5
|
+
endpoints: {
|
|
6
|
+
'GET /list': 'List all files in the directory',
|
|
7
|
+
'GET /read?path=<filepath>': 'Read file content',
|
|
8
|
+
'POST /delete': 'Delete file or directory (body: {path})',
|
|
9
|
+
'POST /open-cursor': 'Open Cursor (body: {path, line?})',
|
|
10
|
+
'POST /shutdown': 'Gracefully shutdown the server',
|
|
11
|
+
'POST /scan_code_quality': 'Scan code quality (body: {projectPath})',
|
|
12
|
+
'WS /terminal': 'Interactive terminal via WebSocket'
|
|
13
|
+
},
|
|
14
|
+
example: 'Try: /list or /read?path=README.md'
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = indexHandler;
|
|
19
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { ROOT } = require('../config');
|
|
3
|
+
const { listAllFiles } = require('../utils/fileList');
|
|
4
|
+
|
|
5
|
+
function listHandler(req, res) {
|
|
6
|
+
try {
|
|
7
|
+
const rootName = path.basename(ROOT);
|
|
8
|
+
const files = listAllFiles(ROOT, rootName);
|
|
9
|
+
res.json({ files });
|
|
10
|
+
} catch (e) {
|
|
11
|
+
res.status(500).send(e.message);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = listHandler;
|
|
16
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { exec } = require('child_process');
|
|
3
|
+
const { ROOT } = require('../config');
|
|
4
|
+
|
|
5
|
+
function openCursorHandler(req, res) {
|
|
6
|
+
try {
|
|
7
|
+
const requestedPath = req.body.path || '';
|
|
8
|
+
|
|
9
|
+
// Resolve path relative to ROOT (similar to other endpoints)
|
|
10
|
+
// If path is absolute and within ROOT, use it directly
|
|
11
|
+
// Otherwise, resolve it relative to ROOT
|
|
12
|
+
let filePath;
|
|
13
|
+
if (path.isAbsolute(requestedPath)) {
|
|
14
|
+
// If absolute path, check if it's within ROOT
|
|
15
|
+
if (requestedPath.startsWith(ROOT)) {
|
|
16
|
+
filePath = requestedPath;
|
|
17
|
+
} else {
|
|
18
|
+
// Path might contain incorrect segments (like "codemaps")
|
|
19
|
+
// Try to find ROOT in the path and extract the relative part
|
|
20
|
+
const rootIndex = requestedPath.indexOf(ROOT);
|
|
21
|
+
if (rootIndex !== -1) {
|
|
22
|
+
// Extract the part after ROOT and remove leading slashes
|
|
23
|
+
let relativePart = requestedPath.substring(rootIndex + ROOT.length);
|
|
24
|
+
while (relativePart.startsWith('/')) {
|
|
25
|
+
relativePart = relativePart.substring(1);
|
|
26
|
+
}
|
|
27
|
+
filePath = path.join(ROOT, relativePart);
|
|
28
|
+
} else {
|
|
29
|
+
return res.status(400).json({ error: 'Invalid path: path must be within project root' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
// Relative path - resolve relative to ROOT
|
|
34
|
+
// Remove root directory name prefix if present (from /list endpoint format)
|
|
35
|
+
const rootName = path.basename(ROOT);
|
|
36
|
+
let relativePath = requestedPath;
|
|
37
|
+
if (relativePath.startsWith(rootName + '/')) {
|
|
38
|
+
relativePath = relativePath.substring(rootName.length + 1);
|
|
39
|
+
}
|
|
40
|
+
filePath = path.join(ROOT, relativePath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Validate the resolved path is within ROOT
|
|
44
|
+
if (!filePath.startsWith(ROOT)) {
|
|
45
|
+
return res.status(400).json({ error: 'Invalid path' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { line } = req.body;
|
|
49
|
+
|
|
50
|
+
// Always use cursor CLI command first (it handles line numbers correctly)
|
|
51
|
+
const cursorCommands = [
|
|
52
|
+
'cursor',
|
|
53
|
+
'/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
|
|
54
|
+
'/usr/local/bin/cursor',
|
|
55
|
+
'code'
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const tryCommand = (commandIndex = 0) => {
|
|
59
|
+
if (commandIndex >= cursorCommands.length) {
|
|
60
|
+
return res.status(500).json({
|
|
61
|
+
error: 'Cursor not found. Please install Cursor CLI or check Cursor installation.'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Use proper Cursor CLI syntax for line numbers
|
|
66
|
+
const command = line
|
|
67
|
+
? `${cursorCommands[commandIndex]} --goto "${filePath}:${line}"`
|
|
68
|
+
: `${cursorCommands[commandIndex]} "${filePath}"`;
|
|
69
|
+
|
|
70
|
+
exec(command, (error, stdout, stderr) => {
|
|
71
|
+
if (error && error.code === 127) {
|
|
72
|
+
// Command not found, try next one
|
|
73
|
+
tryCommand(commandIndex + 1);
|
|
74
|
+
} else if (error) {
|
|
75
|
+
console.error('Error opening Cursor:', error);
|
|
76
|
+
return res.status(500).json({ error: error.message });
|
|
77
|
+
} else {
|
|
78
|
+
// File opened successfully, now bring Cursor to front
|
|
79
|
+
const isMac = process.platform === 'darwin';
|
|
80
|
+
if (isMac) {
|
|
81
|
+
// Use AppleScript to bring Cursor to the front
|
|
82
|
+
exec('osascript -e "tell application \\"Cursor\\" to activate"', (activateError) => {
|
|
83
|
+
if (activateError) {
|
|
84
|
+
console.log('Could not activate Cursor, but file opened successfully');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Additional command to ensure it's really in front
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
exec('osascript -e "tell application \\"System Events\\" to set frontmost of process \\"Cursor\\" to true"', () => {
|
|
91
|
+
// Don't worry if this fails
|
|
92
|
+
});
|
|
93
|
+
}, 500);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
res.json({ success: true, message: 'Cursor opened and focused successfully' });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
tryCommand();
|
|
102
|
+
} catch (e) {
|
|
103
|
+
res.status(500).json({ error: e.message });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = openCursorHandler;
|
|
108
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ROOT } = require('../config');
|
|
4
|
+
const { extractPythonFunctions, extractJavaScriptFunctions, extractVueFunctions } = require('../utils/parsers');
|
|
5
|
+
|
|
6
|
+
function readHandler(req, res) {
|
|
7
|
+
try {
|
|
8
|
+
const requestedPath = req.query.path || '';
|
|
9
|
+
const filePath = path.join(ROOT, requestedPath);
|
|
10
|
+
|
|
11
|
+
if (!filePath.startsWith(ROOT)) {
|
|
12
|
+
return res.status(400).send('Invalid path');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Helper function to find and return content from parse result
|
|
16
|
+
const findAndReturn = (parseResult, name, type) => {
|
|
17
|
+
if (type === 'function') {
|
|
18
|
+
const target = parseResult.functions.find(f => f.name === name);
|
|
19
|
+
if (target) return target.content;
|
|
20
|
+
} else if (type === 'method') {
|
|
21
|
+
// Method name format: ClassName.methodName
|
|
22
|
+
for (const cls of parseResult.classes) {
|
|
23
|
+
const method = cls.methods.find(m => m.name === name);
|
|
24
|
+
if (method) return method.content;
|
|
25
|
+
}
|
|
26
|
+
} else if (type === 'class') {
|
|
27
|
+
const target = parseResult.classes.find(c => c.name === name);
|
|
28
|
+
if (target) return target.content;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Check if this is a virtual file request (.function, .method, or .class)
|
|
34
|
+
if (requestedPath.endsWith('.function') || requestedPath.endsWith('.method') || requestedPath.endsWith('.class')) {
|
|
35
|
+
// Traverse up the path to find the actual source file
|
|
36
|
+
let currentPath = filePath;
|
|
37
|
+
let sourceFilePath = null;
|
|
38
|
+
let parser = null;
|
|
39
|
+
|
|
40
|
+
// Keep going up until we find a source file (.py, .js, .jsx, .ts, .tsx, .vue)
|
|
41
|
+
while (currentPath !== ROOT && currentPath !== path.dirname(currentPath)) {
|
|
42
|
+
const stat = fs.existsSync(currentPath) ? fs.statSync(currentPath) : null;
|
|
43
|
+
|
|
44
|
+
// Check if current path is a file with a supported extension
|
|
45
|
+
if (stat && stat.isFile()) {
|
|
46
|
+
if (currentPath.endsWith('.py')) {
|
|
47
|
+
parser = extractPythonFunctions;
|
|
48
|
+
sourceFilePath = currentPath;
|
|
49
|
+
break;
|
|
50
|
+
} else if (currentPath.endsWith('.js') || currentPath.endsWith('.jsx') ||
|
|
51
|
+
currentPath.endsWith('.ts') || currentPath.endsWith('.tsx')) {
|
|
52
|
+
parser = extractJavaScriptFunctions;
|
|
53
|
+
sourceFilePath = currentPath;
|
|
54
|
+
break;
|
|
55
|
+
} else if (currentPath.endsWith('.vue')) {
|
|
56
|
+
parser = extractVueFunctions;
|
|
57
|
+
sourceFilePath = currentPath;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Move up one level
|
|
63
|
+
const parentPath = path.dirname(currentPath);
|
|
64
|
+
if (parentPath === currentPath) break; // Reached root
|
|
65
|
+
currentPath = parentPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!sourceFilePath || !parser) {
|
|
69
|
+
return res.status(404).send('Source file not found for virtual file');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract the requested item name from the requested path
|
|
73
|
+
let itemName = '';
|
|
74
|
+
let itemType = '';
|
|
75
|
+
|
|
76
|
+
if (requestedPath.endsWith('.function')) {
|
|
77
|
+
itemName = path.basename(requestedPath, '.function');
|
|
78
|
+
itemType = 'function';
|
|
79
|
+
} else if (requestedPath.endsWith('.method')) {
|
|
80
|
+
itemName = path.basename(requestedPath, '.method');
|
|
81
|
+
itemType = 'method';
|
|
82
|
+
} else if (requestedPath.endsWith('.class')) {
|
|
83
|
+
itemName = path.basename(requestedPath, '.class');
|
|
84
|
+
itemType = 'class';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if the source file exists
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
90
|
+
return res.status(404).send('Source file not found');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse the file
|
|
94
|
+
const parseResult = parser(sourceFilePath);
|
|
95
|
+
|
|
96
|
+
// Find and return the content
|
|
97
|
+
const content = findAndReturn(parseResult, itemName, itemType);
|
|
98
|
+
|
|
99
|
+
if (!content) {
|
|
100
|
+
return res.status(404).send(`${itemType} '${itemName}' not found in file`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return res.json({ content });
|
|
104
|
+
} catch (e) {
|
|
105
|
+
const errorType = requestedPath.endsWith('.function') ? 'function' :
|
|
106
|
+
requestedPath.endsWith('.method') ? 'method' : 'class';
|
|
107
|
+
return res.status(500).send('Error reading ' + errorType + ': ' + e.message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Regular file read
|
|
112
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
113
|
+
res.json({ content });
|
|
114
|
+
} catch (e) {
|
|
115
|
+
res.status(500).send(e.message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = readHandler;
|
|
120
|
+
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ROOT } = require('../config');
|
|
4
|
+
|
|
5
|
+
function scanCodeQualityHandler(req, res) {
|
|
6
|
+
try {
|
|
7
|
+
const { projectPath } = req.body;
|
|
8
|
+
|
|
9
|
+
if (!projectPath) {
|
|
10
|
+
return res.status(400).json({ error: 'projectPath is required' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const targetPath = path.join(ROOT, projectPath);
|
|
14
|
+
|
|
15
|
+
if (!targetPath.startsWith(ROOT)) {
|
|
16
|
+
return res.status(400).json({ error: 'Invalid path' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(targetPath)) {
|
|
20
|
+
return res.status(404).json({ error: 'Path not found' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const issues = [];
|
|
24
|
+
|
|
25
|
+
// Basic code quality checks
|
|
26
|
+
function scanDirectory(dir) {
|
|
27
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = path.join(dir, entry.name);
|
|
31
|
+
const relativePath = path.relative(ROOT, fullPath);
|
|
32
|
+
|
|
33
|
+
// Skip node_modules and other ignored directories
|
|
34
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'bonzai') {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
scanDirectory(fullPath);
|
|
40
|
+
} else if (entry.isFile()) {
|
|
41
|
+
// Check file size (warn if > 500KB)
|
|
42
|
+
const stats = fs.statSync(fullPath);
|
|
43
|
+
if (stats.size > 500 * 1024) {
|
|
44
|
+
issues.push({
|
|
45
|
+
file: relativePath,
|
|
46
|
+
severity: 'warning',
|
|
47
|
+
message: `Large file detected (${(stats.size / 1024).toFixed(2)}KB). Consider splitting into smaller modules.`,
|
|
48
|
+
type: 'file-size'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check for common code quality issues in JS/TS files
|
|
53
|
+
if (entry.name.endsWith('.js') || entry.name.endsWith('.jsx') ||
|
|
54
|
+
entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
|
|
59
|
+
// Check for very long lines
|
|
60
|
+
lines.forEach((line, index) => {
|
|
61
|
+
if (line.length > 200) {
|
|
62
|
+
issues.push({
|
|
63
|
+
file: relativePath,
|
|
64
|
+
line: index + 1,
|
|
65
|
+
severity: 'info',
|
|
66
|
+
message: `Long line detected (${line.length} characters). Consider breaking into multiple lines.`,
|
|
67
|
+
type: 'line-length'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Check for TODO/FIXME comments
|
|
73
|
+
lines.forEach((line, index) => {
|
|
74
|
+
if (line.match(/TODO|FIXME|XXX|HACK/i)) {
|
|
75
|
+
issues.push({
|
|
76
|
+
file: relativePath,
|
|
77
|
+
line: index + 1,
|
|
78
|
+
severity: 'info',
|
|
79
|
+
message: `TODO/FIXME comment found: ${line.trim().substring(0, 100)}`,
|
|
80
|
+
type: 'todo-comment'
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Check for console.log statements (potential debug code)
|
|
86
|
+
lines.forEach((line, index) => {
|
|
87
|
+
if (line.match(/console\.(log|debug|info|warn|error)/) && !line.includes('//')) {
|
|
88
|
+
issues.push({
|
|
89
|
+
file: relativePath,
|
|
90
|
+
line: index + 1,
|
|
91
|
+
severity: 'warning',
|
|
92
|
+
message: `Console statement found. Consider removing or using a proper logging library.`,
|
|
93
|
+
type: 'console-statement'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// Skip files that can't be read
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stat = fs.statSync(targetPath);
|
|
107
|
+
if (stat.isDirectory()) {
|
|
108
|
+
scanDirectory(targetPath);
|
|
109
|
+
} else {
|
|
110
|
+
// Single file scan
|
|
111
|
+
const relativePath = path.relative(ROOT, targetPath);
|
|
112
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
|
|
115
|
+
lines.forEach((line, index) => {
|
|
116
|
+
if (line.length > 200) {
|
|
117
|
+
issues.push({
|
|
118
|
+
file: relativePath,
|
|
119
|
+
line: index + 1,
|
|
120
|
+
severity: 'info',
|
|
121
|
+
message: `Long line detected (${line.length} characters)`,
|
|
122
|
+
type: 'line-length'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
res.json({
|
|
129
|
+
success: true,
|
|
130
|
+
issues: issues,
|
|
131
|
+
totalIssues: issues.length,
|
|
132
|
+
summary: {
|
|
133
|
+
errors: issues.filter(i => i.severity === 'error').length,
|
|
134
|
+
warnings: issues.filter(i => i.severity === 'warning').length,
|
|
135
|
+
info: issues.filter(i => i.severity === 'info').length
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
} catch (e) {
|
|
140
|
+
res.status(500).json({ error: e.message });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = scanCodeQualityHandler;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function shutdownHandler(req, res) {
|
|
2
|
+
console.log('🛑 Shutdown endpoint called - terminating server...');
|
|
3
|
+
|
|
4
|
+
res.json({
|
|
5
|
+
success: true,
|
|
6
|
+
message: 'Server shutting down...'
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Close the server gracefully
|
|
10
|
+
setTimeout(() => {
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}, 100); // Small delay to ensure response is sent
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = shutdownHandler;
|
|
16
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
console.log(`[Terminal] Node version: ${process.version}`);
|
|
2
|
+
console.log(`[Terminal] Platform: ${process.platform} ${process.arch}`);
|
|
3
|
+
|
|
4
|
+
let pty;
|
|
5
|
+
try {
|
|
6
|
+
pty = require('node-pty');
|
|
7
|
+
console.log('[Terminal] node-pty loaded successfully');
|
|
8
|
+
} catch (err) {
|
|
9
|
+
console.error('[Terminal] Failed to load node-pty:', err.message);
|
|
10
|
+
console.error('[Terminal] This usually means node-pty native binaries failed to install.');
|
|
11
|
+
console.error('[Terminal] Try running: cd bonzai && npm rebuild node-pty');
|
|
12
|
+
pty = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Store active terminal sessions
|
|
16
|
+
const terminals = new Map();
|
|
17
|
+
|
|
18
|
+
// Get default shell based on platform
|
|
19
|
+
function getDefaultShell() {
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
return process.env.COMSPEC || 'powershell.exe';
|
|
22
|
+
}
|
|
23
|
+
// Prefer SHELL env var, but always use full path
|
|
24
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
25
|
+
// Ensure we have a full path
|
|
26
|
+
if (!shell.startsWith('/')) {
|
|
27
|
+
return '/bin/bash';
|
|
28
|
+
}
|
|
29
|
+
return shell;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create a new terminal session
|
|
33
|
+
function createTerminal(sessionId, cols = 80, rows = 24) {
|
|
34
|
+
if (!pty) {
|
|
35
|
+
throw new Error('node-pty is not available. Native binaries may not have installed correctly.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const shell = getDefaultShell();
|
|
39
|
+
const cwd = process.env.HOME || process.cwd();
|
|
40
|
+
|
|
41
|
+
console.log(`[Terminal] Spawning shell: ${shell}`);
|
|
42
|
+
console.log(`[Terminal] Working directory: ${cwd}`);
|
|
43
|
+
console.log(`[Terminal] PATH: ${process.env.PATH}`);
|
|
44
|
+
|
|
45
|
+
let ptyProcess;
|
|
46
|
+
try {
|
|
47
|
+
ptyProcess = pty.spawn(shell, [], {
|
|
48
|
+
name: 'xterm-256color',
|
|
49
|
+
cols,
|
|
50
|
+
rows,
|
|
51
|
+
cwd,
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
TERM: 'xterm-256color',
|
|
55
|
+
PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
} catch (spawnErr) {
|
|
59
|
+
console.error('[Terminal] pty.spawn failed:', spawnErr);
|
|
60
|
+
console.error('[Terminal] Error code:', spawnErr.code);
|
|
61
|
+
console.error('[Terminal] Error errno:', spawnErr.errno);
|
|
62
|
+
// Try with /bin/bash as fallback
|
|
63
|
+
console.log('[Terminal] Retrying with /bin/bash...');
|
|
64
|
+
try {
|
|
65
|
+
ptyProcess = pty.spawn('/bin/bash', [], {
|
|
66
|
+
name: 'xterm-256color',
|
|
67
|
+
cols,
|
|
68
|
+
rows,
|
|
69
|
+
cwd,
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
TERM: 'xterm-256color',
|
|
73
|
+
PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
} catch (bashErr) {
|
|
77
|
+
console.error('[Terminal] /bin/bash also failed:', bashErr);
|
|
78
|
+
throw spawnErr; // throw original error
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
terminals.set(sessionId, {
|
|
83
|
+
pty: ptyProcess,
|
|
84
|
+
buffer: ''
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return ptyProcess;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// HTTP handler for terminal info
|
|
91
|
+
function terminalHandler(req, res) {
|
|
92
|
+
res.json({
|
|
93
|
+
message: 'Terminal WebSocket API',
|
|
94
|
+
usage: {
|
|
95
|
+
websocket: 'ws://localhost:3001/terminal',
|
|
96
|
+
events: {
|
|
97
|
+
'input': 'Send terminal input (data: string)',
|
|
98
|
+
'resize': 'Resize terminal (cols: number, rows: number)',
|
|
99
|
+
'output': 'Receive terminal output'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// WebSocket handler for terminal sessions
|
|
106
|
+
function setupTerminalWebSocket(wss) {
|
|
107
|
+
wss.on('connection', (ws) => {
|
|
108
|
+
const sessionId = Date.now().toString();
|
|
109
|
+
let ptyProcess;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
ptyProcess = createTerminal(sessionId);
|
|
113
|
+
console.log(`Terminal session ${sessionId} started`);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('Failed to create terminal:', err.message);
|
|
116
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Failed to create terminal: ' + err.message }));
|
|
117
|
+
ws.close();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Send terminal output to WebSocket client
|
|
122
|
+
ptyProcess.onData((data) => {
|
|
123
|
+
try {
|
|
124
|
+
ws.send(JSON.stringify({ type: 'output', data }));
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// Client disconnected
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
131
|
+
ws.send(JSON.stringify({ type: 'exit', exitCode }));
|
|
132
|
+
ws.close();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Handle incoming messages from client
|
|
136
|
+
ws.on('message', (message) => {
|
|
137
|
+
try {
|
|
138
|
+
const msg = JSON.parse(message);
|
|
139
|
+
|
|
140
|
+
switch (msg.type) {
|
|
141
|
+
case 'input':
|
|
142
|
+
ptyProcess.write(msg.data);
|
|
143
|
+
break;
|
|
144
|
+
case 'resize':
|
|
145
|
+
ptyProcess.resize(msg.cols, msg.rows);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error('Terminal message error:', e);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Cleanup on disconnect
|
|
154
|
+
ws.on('close', () => {
|
|
155
|
+
console.log(`Terminal session ${sessionId} closed`);
|
|
156
|
+
ptyProcess.kill();
|
|
157
|
+
terminals.delete(sessionId);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { terminalHandler, setupTerminalWebSocket };
|