devbonzai 1.8.0 → 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 +401 -0
  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'
@@ -1294,6 +1296,405 @@ app.post('/prompt_agent', (req, res) => {
1294
1296
  });
1295
1297
  });
1296
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
+
1297
1698
  // Shutdown endpoint to kill the server
1298
1699
  app.post('/shutdown', (req, res) => {
1299
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.8.0",
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": {