UNPKG

mcard-js

Version:

MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers

252 lines (244 loc) 8.19 kB
/** * Static Server builtin for CLM execution. * * Provides deploy, status, and stop actions for HTTP static file servers. * Uses Node.js 'http' module. */ import { execSync, spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; /** * Resolve environment variables in config values like '${VAR}'. */ function resolveConfigValue(val) { if (typeof val === 'string' && val.startsWith('${') && val.endsWith('}')) { const envVar = val.slice(2, -1); return process.env[envVar] || val; } return val; } /** * Check if a port is in use using lsof (handles both IPv4 and IPv6). */ function isPortInUse(port) { try { const result = execSync(`lsof -ti :${port}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); return result.trim().length > 0; } catch { return false; } } /** * Get PID of process using a port. */ function getPidOnPort(port) { try { const result = execSync(`lsof -ti :${port}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); const pid = parseInt(result.trim().split('\n')[0], 10); return isNaN(pid) ? null : pid; } catch { return null; } } /** * Execute static server builtin. */ export async function executeStaticServer(config, context, chapterDir) { const action = context.action || config.action || 'status'; // Default to HTTP_PORT from .env matching the project configuration const defaultPort = parseInt(process.env.HTTP_PORT || '8080', 10); const rawPort = context.port || config.port || defaultPort; const port = parseInt(String(resolveConfigValue(rawPort)), 10); const host = context.host || config.host || 'localhost'; let rootDir = context.root_dir || config.root_dir || '.'; // Resolve root_dir - find project root by looking for package.json or .git let projectRoot = chapterDir; while (projectRoot !== path.dirname(projectRoot)) { if (fs.existsSync(path.join(projectRoot, 'package.json')) || fs.existsSync(path.join(projectRoot, '.git'))) { break; } projectRoot = path.dirname(projectRoot); } // Fallback to cwd if nothing found if (projectRoot === path.dirname(projectRoot)) { projectRoot = process.cwd(); } if (!path.isAbsolute(rootDir)) { rootDir = path.normalize(path.join(projectRoot, rootDir)); } const pidFile = path.join(projectRoot, `.static_server_${port}.pid`); if (action === 'deploy') { // Check if already running if (isPortInUse(port)) { const pid = getPidOnPort(port); return { success: true, message: 'Server already running', pid, port, url: `http://${host}:${port}`, status: 'already_running' }; } // Verify root_dir exists if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) { return { success: false, error: `Directory not found: ${rootDir}` }; } // Node.js HTTP static server script const serverCode = ` const http = require('http'); const fs = require('fs'); const path = require('path'); const port = ${port}; const host = '${host}'; const rootDir = '${rootDir.replace(/\\/g, '\\\\')}'; const mimeTypes = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.wav': 'audio/wav', '.mp4': 'video/mp4', '.woff': 'application/font-woff', '.ttf': 'application/font-ttf', '.eot': 'application/vnd.ms-fontobject', '.otf': 'application/font-otf', '.wasm': 'application/wasm' }; const server = http.createServer((req, res) => { let filePath = path.join(rootDir, req.url === '/' ? 'index.html' : req.url); // Prevent directory traversal if (!filePath.startsWith(rootDir)) { res.writeHead(403); res.end('Forbidden'); return; } const extname = String(path.extname(filePath)).toLowerCase(); const contentType = mimeTypes[extname] || 'application/octet-stream'; fs.readFile(filePath, (error, content) => { if (error) { if(error.code == 'ENOENT') { res.writeHead(404); res.end('404 Not Found'); } else { res.writeHead(500); res.end('Sorry, check with the site admin for error: '+error.code+' ..\\n'); } } else { res.writeHead(200, { 'Content-Type': contentType }); res.end(content, 'utf-8'); } }); }); server.listen(port, host, () => { console.log('Server running at http://' + host + ':' + port + '/'); }); // Keep process alive setInterval(() => {}, 1000); `; try { // Spawn Node.js server as detached background process const child = spawn(process.execPath, ['-e', serverCode], { detached: true, stdio: 'ignore' }); child.unref(); // Save PID fs.writeFileSync(pidFile, String(child.pid)); // Wait for server to start await new Promise(resolve => setTimeout(resolve, 1500)); if (isPortInUse(port)) { return { success: true, message: 'Server deployed successfully', pid: child.pid, port, url: `http://${host}:${port}`, root_dir: rootDir, status: 'running' }; } else { return { success: false, error: 'Server started but not responding' }; } } catch (error) { return { success: false, error: `Failed to start server: ${error}` }; } } if (action === 'status') { const isRunning = isPortInUse(port); const pid = getPidOnPort(port); let savedPid = null; if (fs.existsSync(pidFile)) { try { savedPid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); } catch { } } return { success: true, running: isRunning, pid, saved_pid: savedPid, port, url: isRunning ? `http://${host}:${port}` : null, status: isRunning ? 'running' : 'stopped' }; } if (action === 'stop') { const pid = getPidOnPort(port); if (!pid) { if (fs.existsSync(pidFile)) { fs.unlinkSync(pidFile); } return { success: true, message: 'No server running on this port', port, status: 'stopped' }; } try { process.kill(pid, 'SIGTERM'); await new Promise(resolve => setTimeout(resolve, 500)); if (isPortInUse(port)) { process.kill(pid, 'SIGKILL'); await new Promise(resolve => setTimeout(resolve, 500)); } if (fs.existsSync(pidFile)) { fs.unlinkSync(pidFile); } return { success: true, message: 'Server stopped', pid, port, status: 'stopped' }; } catch (error) { if (error.code === 'ESRCH') { if (fs.existsSync(pidFile)) { fs.unlinkSync(pidFile); } return { success: true, message: 'Server was not running', port, status: 'stopped' }; } return { success: false, error: `Failed to stop server: ${error}` }; } } return { success: false, error: `Unknown action: ${action}` }; } //# sourceMappingURL=static-server.js.map