devbonzai 1.7.9 → 1.8.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.
Files changed (3) hide show
  1. package/README.md +69 -1
  2. package/cli.js +455 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,4 +13,72 @@ npm publish
13
13
  - node cli.js
14
14
  - this will setup a receiver in this directory
15
15
 
16
- Used for Bonzai's linking functionality from web to local development environment.
16
+ Used for Bonzai's linking functionality from web to local development environment.
17
+
18
+ ## API Endpoints
19
+
20
+ The server exposes several endpoints for file operations and code analysis:
21
+
22
+ ### Import Validation
23
+
24
+ **POST /validate-imports** - Validate imports in a single file
25
+
26
+ Request body:
27
+ ```json
28
+ {
29
+ "filePath": "src/components/Button.js",
30
+ "imports": [
31
+ {
32
+ "path": "./utils",
33
+ "line": 5,
34
+ "symbols": [
35
+ { "name": "formatDate", "isDefault": false },
36
+ { "name": "default", "isDefault": true }
37
+ ]
38
+ }
39
+ ]
40
+ }
41
+ ```
42
+
43
+ Response:
44
+ ```json
45
+ {
46
+ "file": "src/components/Button.js",
47
+ "errors": [
48
+ {
49
+ "line": 5,
50
+ "import": "./utils",
51
+ "symbol": "formatDate",
52
+ "error": "'formatDate' is not exported from './utils'"
53
+ }
54
+ ],
55
+ "errorCount": 1
56
+ }
57
+ ```
58
+
59
+ **POST /validate-imports-batch** - Validate imports for multiple files
60
+
61
+ Request body:
62
+ ```json
63
+ {
64
+ "files": [
65
+ {
66
+ "filePath": "src/components/Button.js",
67
+ "imports": [...]
68
+ },
69
+ {
70
+ "filePath": "src/components/Input.js",
71
+ "imports": [...]
72
+ }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ Response: Array of validation results (same format as single file endpoint)
78
+
79
+ ### Supported Features
80
+
81
+ - **JavaScript/TypeScript**: Validates default exports, named exports, and namespace imports
82
+ - **Python**: Validates function, class, and variable imports
83
+ - **File Resolution**: Automatically resolves relative imports and checks for files with common extensions (.js, .jsx, .ts, .tsx, .py, index files)
84
+ - **External Packages**: Skips validation for node_modules imports
package/cli.js CHANGED
@@ -92,6 +92,8 @@ app.get('/', (req, res) => {
92
92
  'POST /move': 'Move file or folder (body: {source, destination})',
93
93
  'POST /open-cursor': 'Open Cursor (body: {path, line?})',
94
94
  'POST /prompt_agent': 'Execute cursor-agent command (body: {prompt})',
95
+ 'POST /validate-imports': 'Validate imports in a file (body: {filePath, imports})',
96
+ 'POST /validate-imports-batch': 'Validate imports for multiple files (body: {files})',
95
97
  'POST /shutdown': 'Gracefully shutdown the server'
96
98
  },
97
99
  example: 'Try: /list or /read?path=README.md'
@@ -817,10 +819,10 @@ function listAllFiles(dir, base = '', ignorePatterns = null) {
817
819
  const classFilePath = path.join(filePath, className).replace(/\\\\/g, '/');
818
820
  results.push(classFilePath);
819
821
 
820
- // Add methods with dot notation: ClassName.methodName
822
+ // Add methods nested under the class: ClassName.methodName
821
823
  for (const method of cls.methods) {
822
824
  const methodFileName = method.name + '.method';
823
- const methodFilePath = path.join(filePath, methodFileName).replace(/\\\\/g, '/');
825
+ const methodFilePath = path.join(classFilePath, methodFileName).replace(/\\\\/g, '/');
824
826
  results.push(methodFilePath);
825
827
  }
826
828
  }
@@ -889,47 +891,66 @@ app.get('/read', (req, res) => {
889
891
 
890
892
  // Check if this is a virtual file request (.function, .method, or .class)
891
893
  if (requestedPath.endsWith('.function') || requestedPath.endsWith('.method') || requestedPath.endsWith('.class')) {
892
- const parentFilePath = path.dirname(filePath);
893
- const parentRelativePath = path.relative(ROOT, parentFilePath);
894
- const fileName = path.basename(parentRelativePath);
895
-
896
- // Determine file type and parser
897
- let parseResult = null;
894
+ // Traverse up the path to find the actual source file
895
+ let currentPath = filePath;
896
+ let sourceFilePath = null;
898
897
  let parser = null;
899
898
 
900
- if (fileName.endsWith('.py')) {
901
- parser = extractPythonFunctions;
902
- } else if (fileName.endsWith('.js') || fileName.endsWith('.jsx') || fileName.endsWith('.ts') || fileName.endsWith('.tsx')) {
903
- parser = extractJavaScriptFunctions;
904
- } else if (fileName.endsWith('.vue')) {
905
- parser = extractVueFunctions;
906
- } else {
907
- return res.status(404).send('Parent file type not supported');
899
+ // Keep going up until we find a source file (.py, .js, .jsx, .ts, .tsx, .vue)
900
+ while (currentPath !== ROOT && currentPath !== path.dirname(currentPath)) {
901
+ const stat = fs.existsSync(currentPath) ? fs.statSync(currentPath) : null;
902
+
903
+ // Check if current path is a file with a supported extension
904
+ if (stat && stat.isFile()) {
905
+ if (currentPath.endsWith('.py')) {
906
+ parser = extractPythonFunctions;
907
+ sourceFilePath = currentPath;
908
+ break;
909
+ } else if (currentPath.endsWith('.js') || currentPath.endsWith('.jsx') ||
910
+ currentPath.endsWith('.ts') || currentPath.endsWith('.tsx')) {
911
+ parser = extractJavaScriptFunctions;
912
+ sourceFilePath = currentPath;
913
+ break;
914
+ } else if (currentPath.endsWith('.vue')) {
915
+ parser = extractVueFunctions;
916
+ sourceFilePath = currentPath;
917
+ break;
918
+ }
919
+ }
920
+
921
+ // Move up one level
922
+ const parentPath = path.dirname(currentPath);
923
+ if (parentPath === currentPath) break; // Reached root
924
+ currentPath = parentPath;
925
+ }
926
+
927
+ if (!sourceFilePath || !parser) {
928
+ return res.status(404).send('Source file not found for virtual file');
929
+ }
930
+
931
+ // Extract the requested item name from the requested path
932
+ let itemName = '';
933
+ let itemType = '';
934
+
935
+ if (requestedPath.endsWith('.function')) {
936
+ itemName = path.basename(requestedPath, '.function');
937
+ itemType = 'function';
938
+ } else if (requestedPath.endsWith('.method')) {
939
+ itemName = path.basename(requestedPath, '.method');
940
+ itemType = 'method';
941
+ } else if (requestedPath.endsWith('.class')) {
942
+ itemName = path.basename(requestedPath, '.class');
943
+ itemType = 'class';
908
944
  }
909
945
 
910
- // Check if the parent file exists
946
+ // Check if the source file exists
911
947
  try {
912
- if (!fs.existsSync(parentFilePath)) {
913
- return res.status(404).send('Parent file not found');
948
+ if (!fs.existsSync(sourceFilePath)) {
949
+ return res.status(404).send('Source file not found');
914
950
  }
915
951
 
916
952
  // Parse the file
917
- parseResult = parser(parentFilePath);
918
-
919
- // Extract the requested item name
920
- let itemName = '';
921
- let itemType = '';
922
-
923
- if (requestedPath.endsWith('.function')) {
924
- itemName = path.basename(requestedPath, '.function');
925
- itemType = 'function';
926
- } else if (requestedPath.endsWith('.method')) {
927
- itemName = path.basename(requestedPath, '.method');
928
- itemType = 'method';
929
- } else if (requestedPath.endsWith('.class')) {
930
- itemName = path.basename(requestedPath, '.class');
931
- itemType = 'class';
932
- }
953
+ const parseResult = parser(sourceFilePath);
933
954
 
934
955
  // Find and return the content
935
956
  const content = findAndReturn(parseResult, itemName, itemType);
@@ -1275,6 +1296,405 @@ app.post('/prompt_agent', (req, res) => {
1275
1296
  });
1276
1297
  });
1277
1298
 
1299
+ // Helper function to resolve import path relative to source file
1300
+ function resolveImportPath(importPath, sourceFilePath) {
1301
+ // Remove root folder name if present
1302
+ let relativeSourcePath = sourceFilePath;
1303
+ const rootName = path.basename(ROOT);
1304
+ if (relativeSourcePath.startsWith(rootName + '/')) {
1305
+ relativeSourcePath = relativeSourcePath.substring(rootName.length + 1);
1306
+ }
1307
+
1308
+ const sourceDir = path.dirname(path.join(ROOT, relativeSourcePath));
1309
+
1310
+ // Handle relative imports
1311
+ if (importPath.startsWith('./') || importPath.startsWith('../')) {
1312
+ const resolved = path.resolve(sourceDir, importPath);
1313
+ return path.relative(ROOT, resolved).replace(/\\\\/g, '/');
1314
+ }
1315
+
1316
+ // Handle absolute imports (node_modules, etc.)
1317
+ // For now, just return as-is - we'll check node_modules separately
1318
+ return importPath;
1319
+ }
1320
+
1321
+ // Helper function to check if a file exists (with various extensions)
1322
+ function findFileWithExtensions(basePath) {
1323
+ const extensions = ['', '.js', '.jsx', '.ts', '.tsx', '.py', '/index.js', '/index.ts', '/index.tsx'];
1324
+ const rootName = path.basename(ROOT);
1325
+
1326
+ for (const ext of extensions) {
1327
+ const testPath = basePath + ext;
1328
+
1329
+ // Remove root folder name if present
1330
+ let relativePath = testPath;
1331
+ if (relativePath.startsWith(rootName + '/')) {
1332
+ relativePath = relativePath.substring(rootName.length + 1);
1333
+ }
1334
+ const actualPath = path.join(ROOT, relativePath);
1335
+
1336
+ if (fs.existsSync(actualPath) && fs.statSync(actualPath).isFile()) {
1337
+ return relativePath;
1338
+ }
1339
+ }
1340
+
1341
+ return null;
1342
+ }
1343
+
1344
+ // Helper function to extract exports from JavaScript/TypeScript file
1345
+ function getJavaScriptExports(filePath) {
1346
+ try {
1347
+ if (!babelParser) {
1348
+ return { default: false, named: [], all: false };
1349
+ }
1350
+
1351
+ const content = fs.readFileSync(filePath, 'utf8');
1352
+ const isTypeScript = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
1353
+
1354
+ const ast = babelParser.parse(content, {
1355
+ sourceType: 'module',
1356
+ plugins: [
1357
+ 'typescript',
1358
+ 'jsx',
1359
+ 'decorators-legacy',
1360
+ 'classProperties',
1361
+ 'objectRestSpread',
1362
+ 'asyncGenerators',
1363
+ 'functionBind',
1364
+ 'exportDefaultFrom',
1365
+ 'exportNamespaceFrom',
1366
+ 'dynamicImport',
1367
+ 'nullishCoalescingOperator',
1368
+ 'optionalChaining'
1369
+ ]
1370
+ });
1371
+
1372
+ const exports = { default: false, named: [], all: false };
1373
+
1374
+ function traverse(node) {
1375
+ if (!node) return;
1376
+
1377
+ // export default
1378
+ if (node.type === 'ExportDefaultDeclaration') {
1379
+ exports.default = true;
1380
+ }
1381
+
1382
+ // export { A, B }
1383
+ if (node.type === 'ExportNamedDeclaration') {
1384
+ if (node.specifiers) {
1385
+ node.specifiers.forEach(spec => {
1386
+ if (spec.type === 'ExportSpecifier') {
1387
+ const name = spec.exported.name || spec.exported.value;
1388
+ if (name === '*') {
1389
+ exports.all = true;
1390
+ } else {
1391
+ exports.named.push(name);
1392
+ }
1393
+ }
1394
+ });
1395
+ }
1396
+ // export function/class
1397
+ if (node.declaration) {
1398
+ if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
1399
+ exports.named.push(node.declaration.id.name);
1400
+ } else if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
1401
+ exports.named.push(node.declaration.id.name);
1402
+ }
1403
+ }
1404
+ }
1405
+
1406
+ // export * from './file'
1407
+ if (node.type === 'ExportAllDeclaration') {
1408
+ exports.all = true;
1409
+ }
1410
+
1411
+ // Recursively traverse
1412
+ for (const key in node) {
1413
+ if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
1414
+ const child = node[key];
1415
+ if (Array.isArray(child)) {
1416
+ child.forEach(c => traverse(c));
1417
+ } else if (child && typeof child === 'object' && child.type) {
1418
+ traverse(child);
1419
+ }
1420
+ }
1421
+ }
1422
+
1423
+ traverse(ast);
1424
+ return exports;
1425
+ } catch (e) {
1426
+ // If parsing fails, return empty exports
1427
+ return { default: false, named: [], all: false };
1428
+ }
1429
+ }
1430
+
1431
+ // Helper function to check if symbol exists in Python file
1432
+ function checkPythonSymbol(filePath, symbolName) {
1433
+ try {
1434
+ const content = fs.readFileSync(filePath, 'utf8');
1435
+ const lines = content.split('\\n');
1436
+
1437
+ // Check for function definitions
1438
+ const funcPattern = new RegExp(\`^\\\\s*def\\\\s+\${symbolName}\\\\s*\\\\(\`);
1439
+ // Check for class definitions
1440
+ const classPattern = new RegExp(\`^\\\\s*class\\\\s+\${symbolName}\\\\s*[(:]\`);
1441
+ // Check for variable assignments
1442
+ const varPattern = new RegExp(\`^\\\\s*\${symbolName}\\\\s*=\`);
1443
+
1444
+ for (const line of lines) {
1445
+ if (funcPattern.test(line) || classPattern.test(line) || varPattern.test(line)) {
1446
+ return true;
1447
+ }
1448
+ }
1449
+
1450
+ // Check __all__ if present
1451
+ const allMatch = content.match(/^__all__\\s*=\\s*\\[(.*?)\\]/m);
1452
+ if (allMatch) {
1453
+ const allExports = allMatch[1].split(',').map(s => s.trim().replace(/['"]/g, ''));
1454
+ return allExports.includes(symbolName);
1455
+ }
1456
+
1457
+ return false;
1458
+ } catch (e) {
1459
+ return false;
1460
+ }
1461
+ }
1462
+
1463
+ // Validate imports for a single file
1464
+ app.post('/validate-imports', (req, res) => {
1465
+ try {
1466
+ const { filePath, imports } = req.body;
1467
+
1468
+ if (!filePath || !imports || !Array.isArray(imports)) {
1469
+ return res.status(400).json({ error: 'filePath and imports array required' });
1470
+ }
1471
+
1472
+ const errors = [];
1473
+ const rootName = path.basename(ROOT);
1474
+
1475
+ // Remove root folder name if present
1476
+ let relativeFilePath = filePath;
1477
+ if (relativeFilePath.startsWith(rootName + '/')) {
1478
+ relativeFilePath = relativeFilePath.substring(rootName.length + 1);
1479
+ }
1480
+
1481
+ for (const imp of imports) {
1482
+ const importPath = imp.path;
1483
+ const symbols = imp.symbols || [];
1484
+
1485
+ // Resolve import path
1486
+ const resolvedPath = resolveImportPath(importPath, filePath);
1487
+
1488
+ // Check if file exists
1489
+ const foundFile = findFileWithExtensions(resolvedPath);
1490
+
1491
+ if (!foundFile) {
1492
+ // Check if it's a node_modules import (skip validation for external packages)
1493
+ if (!importPath.startsWith('./') && !importPath.startsWith('../') && !importPath.startsWith('/')) {
1494
+ // Likely a node_modules import, skip
1495
+ continue;
1496
+ }
1497
+
1498
+ errors.push({
1499
+ line: imp.line,
1500
+ import: importPath,
1501
+ error: \`Cannot find module '\${importPath}'\`
1502
+ });
1503
+ continue;
1504
+ }
1505
+
1506
+ // Check exports for each symbol
1507
+ const fullFilePath = path.join(ROOT, foundFile);
1508
+ const extension = foundFile.split('.').pop()?.toLowerCase();
1509
+
1510
+ if (['js', 'jsx', 'ts', 'tsx'].includes(extension)) {
1511
+ const exports = getJavaScriptExports(fullFilePath);
1512
+
1513
+ for (const symbol of symbols) {
1514
+ if (symbol.isNamespace || symbol.name === '*') {
1515
+ // Namespace import - check if file has any exports
1516
+ if (!exports.default && exports.named.length === 0 && !exports.all) {
1517
+ errors.push({
1518
+ line: imp.line,
1519
+ import: importPath,
1520
+ symbol: symbol.name,
1521
+ error: \`Module '\${importPath}' has no exports\`
1522
+ });
1523
+ }
1524
+ } else if (symbol.isDefault) {
1525
+ if (!exports.default) {
1526
+ errors.push({
1527
+ line: imp.line,
1528
+ import: importPath,
1529
+ symbol: symbol.name,
1530
+ error: \`'\${symbol.name}' is not exported as default from '\${importPath}'\`
1531
+ });
1532
+ }
1533
+ } else {
1534
+ // Named export
1535
+ if (!exports.named.includes(symbol.name) && !exports.all) {
1536
+ errors.push({
1537
+ line: imp.line,
1538
+ import: importPath,
1539
+ symbol: symbol.name,
1540
+ error: \`'\${symbol.name}' is not exported from '\${importPath}'\`
1541
+ });
1542
+ }
1543
+ }
1544
+ }
1545
+ } else if (extension === 'py') {
1546
+ // Python validation
1547
+ for (const symbol of symbols) {
1548
+ if (symbol.name === '*' || symbol.isNamespace) {
1549
+ // Wildcard import - just check if file exists (already done)
1550
+ continue;
1551
+ }
1552
+
1553
+ // Check if symbol exists in Python file
1554
+ if (!checkPythonSymbol(fullFilePath, symbol.name)) {
1555
+ errors.push({
1556
+ line: imp.line,
1557
+ import: importPath,
1558
+ symbol: symbol.name,
1559
+ error: \`'\${symbol.name}' is not defined in '\${importPath}'\`
1560
+ });
1561
+ }
1562
+ }
1563
+ }
1564
+ }
1565
+
1566
+ res.json({
1567
+ file: filePath,
1568
+ errors,
1569
+ errorCount: errors.length
1570
+ });
1571
+ } catch (e) {
1572
+ console.error('Error validating imports:', e);
1573
+ res.status(500).json({ error: e.message });
1574
+ }
1575
+ });
1576
+
1577
+ // Validate imports for multiple files (batch)
1578
+ app.post('/validate-imports-batch', (req, res) => {
1579
+ try {
1580
+ const { files } = req.body;
1581
+
1582
+ if (!files || !Array.isArray(files)) {
1583
+ return res.status(400).json({ error: 'files array required' });
1584
+ }
1585
+
1586
+ const results = [];
1587
+
1588
+ for (const file of files) {
1589
+ const { filePath, imports } = file;
1590
+
1591
+ if (!filePath || !imports || !Array.isArray(imports)) {
1592
+ results.push({ file: filePath || 'unknown', errors: [], errorCount: 0 });
1593
+ continue;
1594
+ }
1595
+
1596
+ const errors = [];
1597
+ const rootName = path.basename(ROOT);
1598
+
1599
+ // Remove root folder name if present
1600
+ let relativeFilePath = filePath;
1601
+ if (relativeFilePath.startsWith(rootName + '/')) {
1602
+ relativeFilePath = relativeFilePath.substring(rootName.length + 1);
1603
+ }
1604
+
1605
+ for (const imp of imports) {
1606
+ const importPath = imp.path;
1607
+ const symbols = imp.symbols || [];
1608
+
1609
+ // Resolve import path
1610
+ const resolvedPath = resolveImportPath(importPath, filePath);
1611
+
1612
+ // Check if file exists
1613
+ const foundFile = findFileWithExtensions(resolvedPath);
1614
+
1615
+ if (!foundFile) {
1616
+ // Check if it's a node_modules import
1617
+ if (!importPath.startsWith('./') && !importPath.startsWith('../') && !importPath.startsWith('/')) {
1618
+ continue;
1619
+ }
1620
+
1621
+ errors.push({
1622
+ line: imp.line,
1623
+ import: importPath,
1624
+ error: \`Cannot find module '\${importPath}'\`
1625
+ });
1626
+ continue;
1627
+ }
1628
+
1629
+ // Check exports
1630
+ const fullFilePath = path.join(ROOT, foundFile);
1631
+ const extension = foundFile.split('.').pop()?.toLowerCase();
1632
+
1633
+ if (['js', 'jsx', 'ts', 'tsx'].includes(extension)) {
1634
+ const exports = getJavaScriptExports(fullFilePath);
1635
+
1636
+ for (const symbol of symbols) {
1637
+ if (symbol.isNamespace || symbol.name === '*') {
1638
+ if (!exports.default && exports.named.length === 0 && !exports.all) {
1639
+ errors.push({
1640
+ line: imp.line,
1641
+ import: importPath,
1642
+ symbol: symbol.name,
1643
+ error: \`Module '\${importPath}' has no exports\`
1644
+ });
1645
+ }
1646
+ } else if (symbol.isDefault) {
1647
+ if (!exports.default) {
1648
+ errors.push({
1649
+ line: imp.line,
1650
+ import: importPath,
1651
+ symbol: symbol.name,
1652
+ error: \`'\${symbol.name}' is not exported as default from '\${importPath}'\`
1653
+ });
1654
+ }
1655
+ } else {
1656
+ if (!exports.named.includes(symbol.name) && !exports.all) {
1657
+ errors.push({
1658
+ line: imp.line,
1659
+ import: importPath,
1660
+ symbol: symbol.name,
1661
+ error: \`'\${symbol.name}' is not exported from '\${importPath}'\`
1662
+ });
1663
+ }
1664
+ }
1665
+ }
1666
+ } else if (extension === 'py') {
1667
+ for (const symbol of symbols) {
1668
+ if (symbol.name === '*' || symbol.isNamespace) {
1669
+ continue;
1670
+ }
1671
+
1672
+ if (!checkPythonSymbol(fullFilePath, symbol.name)) {
1673
+ errors.push({
1674
+ line: imp.line,
1675
+ import: importPath,
1676
+ symbol: symbol.name,
1677
+ error: \`'\${symbol.name}' is not defined in '\${importPath}'\`
1678
+ });
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+
1684
+ results.push({
1685
+ file: filePath,
1686
+ errors,
1687
+ errorCount: errors.length
1688
+ });
1689
+ }
1690
+
1691
+ res.json(results);
1692
+ } catch (e) {
1693
+ console.error('Error validating imports batch:', e);
1694
+ res.status(500).json({ error: e.message });
1695
+ }
1696
+ });
1697
+
1278
1698
  // Shutdown endpoint to kill the server
1279
1699
  app.post('/shutdown', (req, res) => {
1280
1700
  console.log('🛑 Shutdown endpoint called - terminating server...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devbonzai",
3
- "version": "1.7.9",
3
+ "version": "1.8.1",
4
4
  "description": "Quickly set up a local file server in any repository for browser-based file access",
5
5
  "main": "cli.js",
6
6
  "bin": {