4bnode 4.0.0 → 4.0.2

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 (37) hide show
  1. package/README.md +10 -0
  2. package/add-login.js +2 -2
  3. package/index.js +57 -5
  4. package/lib/indexFile.js +60 -13
  5. package/package.json +1 -1
  6. package/skeleton/.4bnode/assets/js/App.js +115 -0
  7. package/skeleton/.4bnode/assets/js/Root.js +44 -0
  8. package/skeleton/.4bnode/assets/js/components/Modal.js +52 -0
  9. package/skeleton/.4bnode/assets/js/components/Sidebar.js +68 -0
  10. package/skeleton/.4bnode/assets/js/components/ToastContainer.js +15 -0
  11. package/skeleton/.4bnode/assets/js/context.js +7 -0
  12. package/skeleton/.4bnode/assets/js/lib.js +83 -0
  13. package/skeleton/.4bnode/assets/js/main.js +5 -0
  14. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +1594 -0
  15. package/skeleton/.4bnode/assets/js/pages/DashboardPage.js +435 -0
  16. package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +461 -0
  17. package/skeleton/.4bnode/assets/js/pages/EnvironmentPage.js +141 -0
  18. package/skeleton/.4bnode/assets/js/pages/RealtimePage.js +186 -0
  19. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +1656 -0
  20. package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +901 -0
  21. package/skeleton/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
  22. package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
  23. package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +66 -0
  24. package/skeleton/{dev-api.js → .4bnode/dev-api.js} +259 -114
  25. package/skeleton/.4bnode/ui.html +1556 -0
  26. package/skeleton/index.js +62 -16
  27. package/skeleton/package.json +1 -1
  28. package/skeleton/public/index.html +290 -7586
  29. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Bold.woff2 +0 -0
  30. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Light.woff2 +0 -0
  31. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Medium.woff2 +0 -0
  32. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-Regular.woff2 +0 -0
  33. /package/skeleton/{public → .4bnode/assets}/fonts/NotoSans-SemiBold.woff2 +0 -0
  34. /package/skeleton/{public → .4bnode/assets}/images/mongo.png +0 -0
  35. /package/skeleton/{public → .4bnode/assets}/images/mysql.png +0 -0
  36. /package/skeleton/{public → .4bnode/assets}/images/postgresql.png +0 -0
  37. /package/skeleton/{public → .4bnode/assets}/images/sqlite.png +0 -0
@@ -8,6 +8,47 @@ import crypto from 'crypto';
8
8
 
9
9
  const router = express.Router();
10
10
 
11
+ // Serve dashboard UI and assets
12
+ const __4bnodeDir = path.join(process.cwd(), '.4bnode');
13
+ router.get('/', (req, res) => {
14
+ res.sendFile('ui.html', { root: __4bnodeDir });
15
+ });
16
+ router.use('/assets', express.static(path.join(__4bnodeDir, 'assets')));
17
+
18
+ // ── Passkey Authentication ──────────────────────────────────────────────────
19
+ function getPasskeyHash() {
20
+ const file = path.join(__4bnodeDir, '.passkey');
21
+ if (!fs.existsSync(file)) return null;
22
+ return fs.readFileSync(file, 'utf8').trim();
23
+ }
24
+
25
+ function hashPasskey(pin) {
26
+ return crypto.createHash('sha256').update(pin).digest('hex');
27
+ }
28
+
29
+ // Verify passkey endpoint (no auth middleware needed here)
30
+ router.post('/api/auth', (req, res) => {
31
+ const { passkey } = req.body;
32
+ const stored = getPasskeyHash();
33
+ if (!stored) return res.json({ ok: true });
34
+ if (!passkey || hashPasskey(passkey) !== stored) {
35
+ return res.status(401).json({ error: 'Invalid passkey' });
36
+ }
37
+ res.json({ ok: true });
38
+ });
39
+
40
+ // Auth middleware — protect all /api/* routes below
41
+ router.use('/api', (req, res, next) => {
42
+ if (req.path === '/auth') return next();
43
+ const stored = getPasskeyHash();
44
+ if (!stored) return next();
45
+ const passkey = req.headers['x-passkey'];
46
+ if (!passkey || hashPasskey(passkey) !== stored) {
47
+ return res.status(401).json({ error: 'Unauthorized' });
48
+ }
49
+ next();
50
+ });
51
+
11
52
  // ---------------------------------------------------------------------------
12
53
  // Utility functions (self-contained, no external imports)
13
54
  // ---------------------------------------------------------------------------
@@ -405,7 +446,8 @@ router.get('/api/models/:name', (req, res) => {
405
446
  router.get('/api/models/:name/data', async (req, res) => {
406
447
  try {
407
448
  const name = req.params.name;
408
- const limit = Math.min(parseInt(req.query.limit) || 50, 500);
449
+ const rawLimit = parseInt(req.query.limit);
450
+ const limit = rawLimit === 0 ? 0 : Math.min(rawLimit || 50, 500);
409
451
  const skip = parseInt(req.query.skip) || 0;
410
452
  const pascalName = toPascalCase(name);
411
453
 
@@ -426,7 +468,9 @@ router.get('/api/models/:name/data', async (req, res) => {
426
468
 
427
469
  const collection = mongoose.connection.db.collection(name + 's');
428
470
  const total = await collection.countDocuments();
429
- const docs = await collection.find({}).skip(skip).limit(limit).toArray();
471
+ const query = collection.find({}).skip(skip);
472
+ if (limit > 0) query.limit(limit);
473
+ const docs = await query.toArray();
430
474
 
431
475
  res.json({ docs, total, limit, skip });
432
476
  } catch (err) {
@@ -729,9 +773,9 @@ router.post('/api/routes/:name/add-method', async (req, res) => {
729
773
  packagesToInstall.push('jsonwebtoken');
730
774
 
731
775
  if (hasPassword) {
732
- handlers += `\n// Login\nrouter.post('/', async (req, res) => {\n try {\n const { ${findField}, ${passwordField} } = req.body;\n const ${modelName} = await ${pascalModel}.findOne({ ${findField} });\n if (!${modelName}) return res.status(404).json({ message: '${pascalModel} not found' });\n\n const isMatch = await bcrypt.compare(${passwordField}, ${modelName}.${passwordField});\n if (!isMatch) return res.status(400).json({ message: 'Invalid credentials' });\n\n const token = jwt.sign({ id: ${modelName}._id }, process.env.JWT_SECRET, { expiresIn: '7d' });\n res.json({ token, ${modelName}: { id: ${modelName}._id, ${findField}: ${modelName}.${findField} } });\n } catch (err) {\n res.status(500).json({ message: err.message });\n }\n});\n`;
776
+ handlers += `\n// Login\nrouter.post('/', async (req, res) => {\n try {\n const { ${findField}, ${passwordField} } = req.body;\n const ${modelName} = await ${pascalModel}.findOne({ ${findField} });\n if (!${modelName}) return res.status(401).json({ message: 'Invalid credentials' });\n\n const isMatch = await bcrypt.compare(${passwordField}, ${modelName}.${passwordField});\n if (!isMatch) return res.status(401).json({ message: 'Invalid credentials' });\n\n const token = jwt.sign({ id: ${modelName}._id }, process.env.JWT_SECRET, { expiresIn: '7d' });\n res.json({ token, ${modelName}: { id: ${modelName}._id, ${findField}: ${modelName}.${findField} } });\n } catch (err) {\n res.status(500).json({ message: err.message });\n }\n});\n`;
733
777
  } else {
734
- handlers += `\n// Login\nrouter.post('/', async (req, res) => {\n try {\n const { ${findField} } = req.body;\n const ${modelName} = await ${pascalModel}.findOne({ ${findField} });\n if (!${modelName}) return res.status(404).json({ message: '${pascalModel} not found' });\n\n const token = jwt.sign({ id: ${modelName}._id }, process.env.JWT_SECRET, { expiresIn: '7d' });\n res.json({ token, ${modelName}: { id: ${modelName}._id, ${findField}: ${modelName}.${findField} } });\n } catch (err) {\n res.status(500).json({ message: err.message });\n }\n});\n`;
778
+ handlers += `\n// Login\nrouter.post('/', async (req, res) => {\n try {\n const { ${findField} } = req.body;\n const ${modelName} = await ${pascalModel}.findOne({ ${findField} });\n if (!${modelName}) return res.status(401).json({ message: 'Invalid credentials' });\n\n const token = jwt.sign({ id: ${modelName}._id }, process.env.JWT_SECRET, { expiresIn: '7d' });\n res.json({ token, ${modelName}: { id: ${modelName}._id, ${findField}: ${modelName}.${findField} } });\n } catch (err) {\n res.status(500).json({ message: err.message });\n }\n});\n`;
735
779
  }
736
780
 
737
781
  // Ensure JWT_SECRET
@@ -1028,16 +1072,27 @@ function addImportToIndex(importLine) {
1028
1072
  function insertBeforeListen(codeBlock) {
1029
1073
  let content = readIndexFile();
1030
1074
  const listenRegex = /(app|server)\.listen\(port/m;
1031
- const match = content.match(listenRegex);
1075
+ let match = content.match(listenRegex);
1076
+ if (!match) {
1077
+ // Match the startServer() call, not the function declaration
1078
+ match = content.match(/^startServer\(/m);
1079
+ }
1032
1080
  if (match) {
1033
- content = content.slice(0, match.index) + codeBlock.trim() + '\n\n' + content.slice(match.index);
1081
+ content = content.slice(0, match.index) + codeBlock.trim() + '\n' + content.slice(match.index);
1034
1082
  }
1035
1083
  writeIndexFile(content);
1036
1084
  }
1037
1085
 
1038
1086
  function replaceAppListen(newBlock) {
1039
1087
  let content = readIndexFile();
1040
- content = content.replace(/app\.listen\(port[\s\S]*?\}\);?\n?/m, newBlock.trim() + '\n');
1088
+ if (content.includes('startServer(')) {
1089
+ content = content.replace(/const listener = app\.listen\(tryPort\)/, 'const listener = server.listen(tryPort)');
1090
+ if (!content.includes('server.listen(tryPort)')) {
1091
+ content = content.replace(/app\.listen\(tryPort\)/, 'server.listen(tryPort)');
1092
+ }
1093
+ } else {
1094
+ content = content.replace(/app\.listen\(port[\s\S]*?\}\);?\n?/m, newBlock.trim() + '\n');
1095
+ }
1041
1096
  writeIndexFile(content);
1042
1097
  }
1043
1098
 
@@ -1125,8 +1180,8 @@ router.get('/api/status', (req, res) => {
1125
1180
  const mongoConfigured = indexContent.includes("import connectDB") || indexContent.includes("connectDB");
1126
1181
  const routesDirExists = fs.existsSync(path.join(process.cwd(), 'src', 'routes'));
1127
1182
  const authMiddlewareExists = fs.existsSync(path.join(process.cwd(), 'src', 'middleware', 'auth.js'));
1128
- const socketConfigured = indexContent.includes('socket.io') || indexContent.includes('Server(server');
1129
- const websocketConfigured = indexContent.includes('WebSocketServer') || indexContent.includes("from 'ws'");
1183
+ const socketConfigured = indexContent.includes('setupSocketIO') || indexContent.includes('socket.io') || indexContent.includes('Server(server');
1184
+ const websocketConfigured = indexContent.includes('setupWebSocket') || indexContent.includes('WebSocketServer') || indexContent.includes("from 'ws'");
1130
1185
  const serialportConfigured = indexContent.includes('SerialPort') || indexContent.includes("from 'serialport'");
1131
1186
 
1132
1187
  res.json({
@@ -1719,154 +1774,242 @@ export default router;
1719
1774
  });
1720
1775
 
1721
1776
  // ---------------------------------------------------------------------------
1722
- // POST /api/add-socket
1777
+ // POST /api/add-socket — create src/services/socket.js
1723
1778
  // ---------------------------------------------------------------------------
1724
1779
  router.post('/api/add-socket', async (req, res) => {
1725
1780
  try {
1726
1781
  const { corsOrigins } = req.body;
1727
1782
 
1728
- // Remove existing WebSocket code if present
1729
- if (readIndexFile().includes('WebSocketServer')) {
1730
- removeWebSocketCode();
1731
- try { await execAsync('npm uninstall ws', { cwd: process.cwd() }); } catch {}
1732
- }
1783
+ // Install packages first
1784
+ const currentContent = readIndexFile();
1785
+ const hasWebSocket = currentContent.includes('setupWebSocket') || currentContent.includes('WebSocketServer');
1733
1786
 
1734
- // Install FIRST before modifying index.js
1735
- await execAsync('npm install socket.io', { cwd: process.cwd() });
1787
+ if (hasWebSocket) {
1788
+ try { await execAsync('npm uninstall ws && npm install socket.io', { cwd: process.cwd() }); } catch {
1789
+ try { await execAsync('npm install socket.io', { cwd: process.cwd() }); } catch {}
1790
+ }
1791
+ } else {
1792
+ await execAsync('npm install socket.io', { cwd: process.cwd() });
1793
+ }
1736
1794
 
1737
- // Add imports to index.js
1738
- addImportToIndex('import { Server } from "socket.io";');
1739
- addImportToIndex('import http from "http";');
1795
+ // Send response before modifying files
1796
+ res.json({ message: 'Socket.IO setup complete.', files: ['src/services/socket.js', 'index.js'] });
1740
1797
 
1741
1798
  // Build cors origin value
1742
1799
  let originValue;
1743
- if (corsOrigins === '*') {
1744
- originValue = '"*"';
1745
- } else {
1746
- const origins = corsOrigins.split(',').map(s => s.trim());
1747
- originValue = JSON.stringify(origins);
1748
- }
1800
+ if (corsOrigins === '*') originValue = '"*"';
1801
+ else originValue = JSON.stringify(corsOrigins.split(',').map(s => s.trim()));
1749
1802
 
1750
- // Insert server + io setup before routes
1751
- const socketCode = `const server = http.createServer(app);
1752
- const io = new Server(server, {
1753
- cors: {
1754
- origin: ${originValue},
1755
- methods: ["GET", "POST"]
1756
- }
1757
- });
1803
+ // Create src/services/socket.js
1804
+ const servicesDir = path.join(process.cwd(), 'src', 'services');
1805
+ if (!fs.existsSync(servicesDir)) fs.mkdirSync(servicesDir, { recursive: true });
1758
1806
 
1759
- io.on("connection", (socket) => {
1760
- console.log("A user connected");
1761
- socket.on("disconnect", () => {
1762
- console.log("User disconnected");
1763
- });
1764
- });
1807
+ const socketFile = `import { Server } from "socket.io";
1808
+ import http from "http";
1765
1809
 
1766
- app.use((req, res, next) => {
1767
- req.io = io;
1768
- next();
1769
- });`;
1810
+ export function setupSocketIO(app) {
1811
+ const server = http.createServer(app);
1770
1812
 
1771
- // Send response BEFORE modifying index.js (nodemon restart kills connection)
1772
- res.json({
1773
- message: 'Socket.IO setup complete.',
1774
- files: ['index.js']
1813
+ const io = new Server(server, {
1814
+ cors: {
1815
+ origin: ${originValue},
1816
+ methods: ["GET", "POST"],
1817
+ },
1818
+ });
1819
+
1820
+ io.on("connection", (socket) => {
1821
+ console.log("A user connected");
1822
+ socket.on("disconnect", () => {
1823
+ console.log("User disconnected");
1775
1824
  });
1825
+ });
1776
1826
 
1777
- let indexContent = readIndexFile();
1778
- if (!indexContent.includes('new Server(server')) {
1779
- insertBeforeListen(socketCode);
1827
+ // Make io accessible in routes via req.io
1828
+ app.use((req, res, next) => {
1829
+ req.io = io;
1830
+ next();
1831
+ });
1780
1832
 
1781
- // Replace app.listen with server.listen
1782
- indexContent = readIndexFile();
1783
- if (indexContent.includes('app.listen(port')) {
1784
- const serverListenBlock = `server.listen(port, () => {
1785
- console.log(\`Server is running on port \${port}\`);
1786
- });`;
1787
- replaceAppListen(serverListenBlock);
1788
- }
1789
- }
1833
+ return { server, io };
1834
+ }
1835
+ `;
1836
+
1837
+ fs.writeFileSync(path.join(servicesDir, 'socket.js'), socketFile, 'utf8');
1838
+
1839
+ // Remove old WebSocket service file if present
1840
+ const wsFile = path.join(servicesDir, 'websocket.js');
1841
+ if (fs.existsSync(wsFile)) fs.unlinkSync(wsFile);
1842
+
1843
+ // Update index.js — clean approach
1844
+ let content = readIndexFile();
1845
+
1846
+ // Remove old inline socket.io/websocket code
1847
+ content = content.replace(/import\s*\{\s*Server\s*\}\s*from\s*["']socket\.io["'];\s*\n?/g, '');
1848
+ content = content.replace(/import\s*\{\s*WebSocketServer\s*\}\s*from\s*['"]ws['"];\s*\n?/g, '');
1849
+ content = content.replace(/import\s+http\s+from\s*["']http["'];\s*\n?/g, '');
1850
+ content = content.replace(/const\s+server\s*=\s*http\.createServer\(app\);\s*\n?/g, '');
1851
+ content = content.replace(/const\s+io\s*=\s*new\s+Server\(server[\s\S]*?\n\}\);\s*\n?/g, '');
1852
+ content = content.replace(/io\.on\(["']connection["'][\s\S]*?\n\}\);\s*\n?/g, '');
1853
+ content = content.replace(/app\.use\(\(req,\s*res,\s*next\)\s*=>\s*\{\s*\n?\s*req\.io\s*=\s*io;\s*\n?\s*next\(\);\s*\n?\s*\}\);\s*\n?/g, '');
1854
+ content = content.replace(/const\s+wss\s*=\s*new\s+WebSocketServer\(\{[\s\S]*?\n?\}\);\s*\n?/g, '');
1855
+ content = content.replace(/wss\.on\(['"]connection['"][\s\S]*?\n\}\);\s*\n?/g, '');
1856
+ content = content.replace(/console\.log\(['"]WebSocket server running on port.*?['"]?\);\s*\n?/g, '');
1857
+ // Remove old import lines for services
1858
+ content = content.replace(/import\s*\{\s*setupSocketIO\s*\}\s*from\s*['"]\.\/src\/services\/socket\.js['"];\s*\n?/g, '');
1859
+ content = content.replace(/import\s*\{\s*setupWebSocket\s*\}\s*from\s*['"]\.\/src\/services\/websocket\.js['"];\s*\n?/g, '');
1860
+ content = content.replace(/const\s*\{\s*server[^}]*\}\s*=\s*setupSocketIO\(app\);\s*\n?/g, '');
1861
+ content = content.replace(/const\s*\{\s*server[^}]*\}\s*=\s*setupWebSocket\(app\);\s*\n?/g, '');
1862
+ content = content.replace(/setupWebSocket\(app[^)]*\);\s*\n?/g, '');
1863
+
1864
+ // Add import for socket service
1865
+ const lastImport = content.lastIndexOf('import ');
1866
+ const lineEnd = content.indexOf('\n', lastImport);
1867
+ content = content.slice(0, lineEnd + 1) + 'import { setupSocketIO } from "./src/services/socket.js";\n' + content.slice(lineEnd + 1);
1868
+
1869
+ // Add setup call before listen
1870
+ const listenIdx = content.search(/\/\/ ── Start Server|async function startServer|function startServer/);
1871
+ if (listenIdx !== -1) {
1872
+ content = content.slice(0, listenIdx) + 'const { server } = setupSocketIO(app);\n\n' + content.slice(listenIdx);
1873
+ }
1874
+
1875
+ // Replace app.listen with server.listen in startServer
1876
+ content = content.replace('const listener = app.listen(tryPort)', 'const listener = server.listen(tryPort)');
1877
+ // Also handle direct app.listen
1878
+ if (content.includes('app.listen(port') && !content.includes('startServer')) {
1879
+ content = content.replace(/app\.listen\(port[\s\S]*?\}\);?\n?/m, 'server.listen(port, () => {\n console.log(`Server is running on port ${port}`);\n});\n');
1880
+ }
1881
+
1882
+ // Revert server.listen back to app.listen for old-style
1883
+ content = content.replace('server.listen(port', 'server.listen(port');
1884
+
1885
+ content = content.replace(/\n{3,}/g, '\n\n');
1886
+ writeIndexFile(content);
1790
1887
  } catch (err) {
1791
1888
  if (!res.headersSent) res.status(500).json({ error: err.message });
1792
1889
  }
1793
1890
  });
1794
1891
 
1795
1892
  // ---------------------------------------------------------------------------
1796
- // POST /api/add-websocket
1893
+ // POST /api/add-websocket — create src/services/websocket.js
1797
1894
  // ---------------------------------------------------------------------------
1798
1895
  router.post('/api/add-websocket', async (req, res) => {
1799
1896
  try {
1800
1897
  const { samePort, wsPort } = req.body;
1801
1898
 
1802
- // Remove existing Socket.IO code if present
1803
- if (readIndexFile().includes('socket.io') || readIndexFile().includes('new Server(server')) {
1804
- removeSocketIOCode();
1805
- try { await execAsync('npm uninstall socket.io', { cwd: process.cwd() }); } catch {}
1806
- }
1899
+ // Install packages first
1900
+ const currentContent = readIndexFile();
1901
+ const hasSocketIO = currentContent.includes('setupSocketIO') || currentContent.includes('socket.io');
1807
1902
 
1808
- // Install FIRST before modifying index.js
1809
- await execAsync('npm install ws', { cwd: process.cwd() });
1903
+ if (hasSocketIO) {
1904
+ try { await execAsync('npm uninstall socket.io && npm install ws', { cwd: process.cwd() }); } catch {
1905
+ try { await execAsync('npm install ws', { cwd: process.cwd() }); } catch {}
1906
+ }
1907
+ } else {
1908
+ await execAsync('npm install ws', { cwd: process.cwd() });
1909
+ }
1810
1910
 
1811
- // Send response BEFORE modifying index.js (nodemon restart kills connection)
1812
- res.json({
1813
- message: 'WebSocket setup complete.',
1814
- files: ['index.js']
1815
- });
1911
+ // Send response before modifying files
1912
+ res.json({ message: 'WebSocket setup complete.', files: ['src/services/websocket.js', 'index.js'] });
1816
1913
 
1817
- // Add import
1818
- addImportToIndex("import { WebSocketServer } from 'ws';");
1914
+ // Create src/services/websocket.js
1915
+ const servicesDir = path.join(process.cwd(), 'src', 'services');
1916
+ if (!fs.existsSync(servicesDir)) fs.mkdirSync(servicesDir, { recursive: true });
1819
1917
 
1918
+ let wsFileContent;
1820
1919
  if (samePort) {
1821
- // Also need http
1822
- addImportToIndex("import http from 'http';");
1920
+ wsFileContent = `import { WebSocketServer } from "ws";
1921
+ import http from "http";
1823
1922
 
1824
- const wsCode = `const server = http.createServer(app);
1825
- const wss = new WebSocketServer({ server });
1923
+ export function setupWebSocket(app) {
1924
+ const server = http.createServer(app);
1925
+ const wss = new WebSocketServer({ server });
1826
1926
 
1827
- wss.on('connection', (ws) => {
1828
- console.log('WebSocket client connected');
1829
- ws.on('message', (message) => {
1830
- console.log('Received:', message.toString());
1831
- });
1832
- ws.on('close', () => {
1833
- console.log('WebSocket client disconnected');
1927
+ wss.on("connection", (ws) => {
1928
+ console.log("WebSocket client connected");
1929
+ ws.on("message", (message) => {
1930
+ console.log("Received:", message.toString());
1931
+ });
1932
+ ws.on("close", () => {
1933
+ console.log("WebSocket client disconnected");
1934
+ });
1834
1935
  });
1835
- });`;
1836
1936
 
1837
- if (!readIndexFile().includes('new WebSocketServer')) {
1838
- insertBeforeListen(wsCode);
1839
-
1840
- // Replace app.listen with server.listen
1841
- let currentContent = readIndexFile();
1842
- if (currentContent.includes('app.listen(port')) {
1843
- const serverListenBlock = `server.listen(port, () => {
1844
- console.log(\`Server is running on port \${port}\`);
1845
- });`;
1846
- replaceAppListen(serverListenBlock);
1847
- }
1848
- }
1937
+ return { server, wss };
1938
+ }
1939
+ `;
1849
1940
  } else {
1850
- // Separate port
1851
- const port = wsPort || 8080;
1852
- const wsCode = `const wss = new WebSocketServer({ port: ${port} });
1853
-
1854
- wss.on('connection', (ws) => {
1855
- console.log('WebSocket client connected');
1856
- ws.on('message', (message) => {
1857
- console.log('Received:', message.toString());
1858
- });
1859
- ws.on('close', () => {
1860
- console.log('WebSocket client disconnected');
1941
+ const p = wsPort || 8080;
1942
+ wsFileContent = `import { WebSocketServer } from "ws";
1943
+
1944
+ export function setupWebSocket() {
1945
+ const wss = new WebSocketServer({ port: ${p} });
1946
+
1947
+ wss.on("connection", (ws) => {
1948
+ console.log("WebSocket client connected");
1949
+ ws.on("message", (message) => {
1950
+ console.log("Received:", message.toString());
1951
+ });
1952
+ ws.on("close", () => {
1953
+ console.log("WebSocket client disconnected");
1954
+ });
1861
1955
  });
1862
- });
1863
1956
 
1864
- console.log('WebSocket server running on port ${port}');`;
1957
+ console.log("WebSocket server running on port ${p}");
1958
+ return { wss };
1959
+ }
1960
+ `;
1961
+ }
1962
+
1963
+ fs.writeFileSync(path.join(servicesDir, 'websocket.js'), wsFileContent, 'utf8');
1865
1964
 
1866
- if (!readIndexFile().includes('new WebSocketServer')) {
1867
- insertBeforeListen(wsCode);
1965
+ // Remove old Socket.IO service file if present
1966
+ const sioFile = path.join(servicesDir, 'socket.js');
1967
+ if (fs.existsSync(sioFile)) fs.unlinkSync(sioFile);
1968
+
1969
+ // Update index.js — clean approach
1970
+ let content = readIndexFile();
1971
+
1972
+ // Remove old inline socket.io/websocket code
1973
+ content = content.replace(/import\s*\{\s*Server\s*\}\s*from\s*["']socket\.io["'];\s*\n?/g, '');
1974
+ content = content.replace(/import\s*\{\s*WebSocketServer\s*\}\s*from\s*['"]ws['"];\s*\n?/g, '');
1975
+ content = content.replace(/import\s+http\s+from\s*["']http["'];\s*\n?/g, '');
1976
+ content = content.replace(/const\s+server\s*=\s*http\.createServer\(app\);\s*\n?/g, '');
1977
+ content = content.replace(/const\s+io\s*=\s*new\s+Server\(server[\s\S]*?\n\}\);\s*\n?/g, '');
1978
+ content = content.replace(/io\.on\(["']connection["'][\s\S]*?\n\}\);\s*\n?/g, '');
1979
+ content = content.replace(/app\.use\(\(req,\s*res,\s*next\)\s*=>\s*\{\s*\n?\s*req\.io\s*=\s*io;\s*\n?\s*next\(\);\s*\n?\s*\}\);\s*\n?/g, '');
1980
+ content = content.replace(/const\s+wss\s*=\s*new\s+WebSocketServer\(\{[\s\S]*?\n?\}\);\s*\n?/g, '');
1981
+ content = content.replace(/wss\.on\(['"]connection['"][\s\S]*?\n\}\);\s*\n?/g, '');
1982
+ content = content.replace(/console\.log\(['"]WebSocket server running on port.*?['"]?\);\s*\n?/g, '');
1983
+ // Remove old service imports
1984
+ content = content.replace(/import\s*\{\s*setupSocketIO\s*\}\s*from\s*['"]\.\/src\/services\/socket\.js['"];\s*\n?/g, '');
1985
+ content = content.replace(/import\s*\{\s*setupWebSocket\s*\}\s*from\s*['"]\.\/src\/services\/websocket\.js['"];\s*\n?/g, '');
1986
+ content = content.replace(/const\s*\{\s*server[^}]*\}\s*=\s*setupSocketIO\(app\);\s*\n?/g, '');
1987
+ content = content.replace(/const\s*\{\s*server[^}]*\}\s*=\s*setupWebSocket\(app\);\s*\n?/g, '');
1988
+ content = content.replace(/setupWebSocket\(app[^)]*\);\s*\n?/g, '');
1989
+ content = content.replace(/setupWebSocket\(\);\s*\n?/g, '');
1990
+
1991
+ // Add import for websocket service
1992
+ const lastImport = content.lastIndexOf('import ');
1993
+ const lineEnd = content.indexOf('\n', lastImport);
1994
+ content = content.slice(0, lineEnd + 1) + 'import { setupWebSocket } from "./src/services/websocket.js";\n' + content.slice(lineEnd + 1);
1995
+
1996
+ // Add setup call before listen
1997
+ const listenIdx = content.search(/\/\/ ── Start Server|async function startServer|function startServer/);
1998
+ if (listenIdx !== -1) {
1999
+ if (samePort) {
2000
+ content = content.slice(0, listenIdx) + 'const { server } = setupWebSocket(app);\n\n' + content.slice(listenIdx);
2001
+ // Replace app.listen with server.listen
2002
+ content = content.replace('const listener = app.listen(tryPort)', 'const listener = server.listen(tryPort)');
2003
+ if (content.includes('app.listen(port') && !content.includes('startServer')) {
2004
+ content = content.replace(/app\.listen\(port[\s\S]*?\}\);?\n?/m, 'server.listen(port, () => {\n console.log(`Server is running on port ${port}`);\n});\n');
2005
+ }
2006
+ } else {
2007
+ content = content.slice(0, listenIdx) + 'setupWebSocket();\n\n' + content.slice(listenIdx);
1868
2008
  }
1869
2009
  }
2010
+
2011
+ content = content.replace(/\n{3,}/g, '\n\n');
2012
+ writeIndexFile(content);
1870
2013
  } catch (err) {
1871
2014
  if (!res.headersSent) res.status(500).json({ error: err.message });
1872
2015
  }
@@ -2266,6 +2409,8 @@ router.get('/shared/:id', (req, res) => {
2266
2409
  const filePath = path.join(sharedDir, req.params.id + '.json');
2267
2410
  if (!fs.existsSync(filePath)) return res.status(404).send('Collection not found');
2268
2411
  const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
2412
+ if (!data.tests) data.tests = [];
2413
+ if (!data.responses) data.responses = {};
2269
2414
  const rawAppName = data.appName || 'My App';
2270
2415
  // Convert kebab-case/snake_case to Title Case: "test-app" → "Test App"
2271
2416
  const appName = rawAppName.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());