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
JavaScript
/**
* 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