#!/usr/bin/env node /** * ๐Ÿ” Devcontainer Status - Provides process tree and system info * * Usage: * node devcontainer-status.mjs # JSON output for VS Code extension * node devcontainer-status.mjs --watch # Continuous updates (SSE style) * node devcontainer-status.mjs --server # HTTP server on port 7890 with WebSocket * * Endpoints: * GET / - Interactive D3.js dashboard (for Simple Browser debugging) * GET /status - JSON status snapshot * GET /stream - SSE live updates * WS /ws - WebSocket for real-time updates */ import { exec, execSync, spawn } from 'child_process'; import { createServer } from 'http'; import { promisify } from 'util'; import { WebSocketServer } from 'ws'; import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const execAsync = promisify(exec); // Key processes we care about for Aesthetic Computer // IMPORTANT: Order matters! First match wins, so specific patterns must come before broad ones. const INTERESTING_PROCESSES = [ // Emacs & terminals { pattern: /emacs.*daemon|emacs.*--daemon/, name: 'Emacs Daemon', icon: '๐Ÿ”ฎ', category: 'editor' }, { pattern: /emacsclient/, name: 'Emacs Client', icon: '๐Ÿ“', category: 'editor' }, { pattern: /artery-tui|artery\.mjs/, name: 'Artery TUI', icon: '๐Ÿฉธ', category: 'tui' }, { pattern: /emacs-mcp/, name: 'Emacs MCP', icon: '๐Ÿง ', category: 'bridge' }, // Eat terminals (emacs tabs) { pattern: /eat.*fishy|๐ŸŸ.*fishy/, name: '๐ŸŸ Fishy', icon: '๐ŸŸ', category: 'shell' }, { pattern: /eat.*kidlisp|๐Ÿงช.*kidlisp/, name: '๐Ÿงช KidLisp', icon: '๐Ÿงช', category: 'shell' }, { pattern: /eat.*tunnel|๐Ÿš‡.*tunnel/, name: '๐Ÿš‡ Tunnel', icon: '๐Ÿš‡', category: 'shell' }, { pattern: /eat.*url|โšก.*url/, name: 'โšก URL', icon: 'โšก', category: 'shell' }, { pattern: /eat.*bookmarks|๐Ÿ”–.*bookmarks/, name: '๐Ÿ”– Bookmarks', icon: '๐Ÿ”–', category: 'shell' }, // AI - Claude Code (BEFORE vscode-server since their paths contain it) { pattern: /native-binary\/claude/, name: 'Claude Code', icon: '๐Ÿง ', category: 'ai' }, { pattern: /(?:^|\/)claude(?:\s|$)/, name: 'Claude CLI', icon: '๐Ÿค–', category: 'ai' }, { pattern: /ollama/, name: 'Ollama', icon: '๐Ÿค–', category: 'ai' }, // VS Code main servers (BEFORE LSP - their cmds contain extension names like mongodb-vscode) { pattern: /server-main\.js/, name: 'VS Code Server', icon: '๐Ÿ’ป', category: 'ide' }, { pattern: /extensionHost/, name: 'VS Code', icon: '๐Ÿ’ป', category: 'ide' }, { pattern: /bootstrap-fork.*fileWatcher/, name: 'VS Code Files', icon: '๐Ÿ’ป', category: 'ide' }, { pattern: /ptyHost/, name: 'VS Code PTY', icon: '๐Ÿ’ป', category: 'ide' }, // LSP servers (child processes of extensionHost with specific server scripts) { pattern: /tsserver/, name: 'TypeScript LSP', icon: '๐Ÿ“˜', category: 'lsp' }, { pattern: /vscode-pylance|server\.bundle\.js/, name: 'Pylance', icon: '๐Ÿ', category: 'lsp' }, { pattern: /mongodb.*languageServer/, name: 'MongoDB LSP', icon: '๐Ÿƒ', category: 'lsp' }, { pattern: /eslint.*server|eslintServer/, name: 'ESLint', icon: '๐Ÿ“', category: 'lsp' }, { pattern: /prettier/, name: 'Prettier', icon: 'โœจ', category: 'lsp' }, { pattern: /cssServerMain/, name: 'CSS LSP', icon: '๐ŸŽจ', category: 'lsp' }, { pattern: /jsonServerMain/, name: 'JSON LSP', icon: '๐Ÿ“‹', category: 'lsp' }, { pattern: /copilot-agent|github\.copilot/, name: 'Copilot', icon: 'โœจ', category: 'ai' }, // Node.js processes (specific before generic) { pattern: /node.*session\.mjs/, name: 'Session Server', icon: '๐Ÿ”—', category: 'dev' }, { pattern: /node.*server\.mjs/, name: 'Main Server', icon: '๐Ÿ–ฅ๏ธ', category: 'dev' }, { pattern: /node.*chat\.mjs/, name: 'Chat Service', icon: '๐Ÿ’ฌ', category: 'dev' }, { pattern: /node.*devcontainer-status/, name: 'Status Server', icon: '๐Ÿ“Š', category: 'dev' }, { pattern: /node.*stripe/, name: 'Stripe', icon: '๐Ÿ’ณ', category: 'dev' }, { pattern: /node.*netlify/, name: 'Netlify CLI', icon: '๐ŸŒ', category: 'dev' }, { pattern: /netlify-log-filter/, name: 'Log Filter', icon: '๐Ÿ“', category: 'dev' }, { pattern: /nodemon/, name: 'Nodemon', icon: '๐Ÿ‘€', category: 'dev' }, { pattern: /esbuild/, name: 'esbuild', icon: 'โšก', category: 'dev' }, // Infrastructure { pattern: /redis-server/, name: 'Redis', icon: '๐Ÿ“ฆ', category: 'db' }, { pattern: /caddy/, name: 'Caddy', icon: '๐ŸŒ', category: 'proxy' }, { pattern: /ngrok|cloudflared/, name: 'Tunnel', icon: '๐Ÿš‡', category: 'proxy' }, { pattern: /deno/, name: 'Deno', icon: '๐Ÿฆ•', category: 'dev' }, // Generic Node.js (last resort for node processes) { pattern: /node(?!.*vscode)(?!.*copilot)/, name: 'Node.js', icon: '๐ŸŸข', category: 'dev' }, // Shells { pattern: /fish(?!.*vscode)/, name: 'Fish Shell', icon: '๐Ÿš', category: 'shell' }, { pattern: /bash/, name: 'Bash', icon: '๐Ÿš', category: 'shell' }, ]; // Parse ps output into structured data (with PPID for tree structure) function parseProcessLine(line) { const parts = line.trim().split(/\s+/); if (parts.length < 12) return null; const [user, pid, ppid, cpu, mem, vsz, rss, tty, stat, start, time, ...cmdParts] = parts; const cmd = cmdParts.join(' '); return { pid: parseInt(pid), ppid: parseInt(ppid), user, cpu: parseFloat(cpu), mem: parseFloat(mem), rss: parseInt(rss), // Resident memory in KB stat, start, time, cmd, cmdShort: cmd.slice(0, 80), }; } // Identify interesting processes - extract meaningful names function categorizeProcess(proc) { const cmd = proc.cmd; for (const { pattern, name, icon, category } of INTERESTING_PROCESSES) { if (pattern.test(cmd)) { // Try to extract a more specific name for node processes let displayName = name; // For generic Node.js, extract the script name if (name === 'Node.js') { const scriptMatch = cmd.match(/node\s+(?:--[^\s]+\s+)*([^\s]+\.m?js)/); if (scriptMatch) { const script = scriptMatch[1].split('/').pop().replace(/\.m?js$/, ''); displayName = script.charAt(0).toUpperCase() + script.slice(1); } } // For chat services, extract which chat if (cmd.includes('chat.mjs')) { const chatMatch = cmd.match(/chat\.mjs\s+(\S+)/); if (chatMatch) { displayName = 'Chat: ' + chatMatch[1].replace('chat-', ''); } } // Make unique by PID for shells if (category === 'shell' && !displayName.includes('๐ŸŸ') && !displayName.includes('๐Ÿงช')) { displayName = displayName + ' #' + proc.pid; } return { ...proc, name: displayName, icon, category, interesting: true }; } } return { ...proc, interesting: false }; } // Get process tree with hierarchy async function getProcessTree() { try { // Include PPID in output for tree structure const { stdout } = await execAsync('ps -eo user,pid,ppid,%cpu,%mem,vsz,rss,tty,stat,start,time,args --sort=-rss 2>/dev/null || ps aux'); const lines = stdout.trim().split('\n').slice(1); // Skip header const allProcesses = lines .map(parseProcessLine) .filter(Boolean); const processes = allProcesses.map(p => categorizeProcess(p)); // Build a map of all PIDs for parent lookup const pidMap = new Map(); allProcesses.forEach(p => pidMap.set(p.pid, p)); // Separate interesting processes from others const interesting = processes.filter(p => p.interesting); // For each interesting process, find its parent chain up to another interesting process interesting.forEach(p => { // Find nearest interesting ancestor let parentPid = p.ppid; let depth = 0; while (parentPid && parentPid > 1 && depth < 20) { const parent = pidMap.get(parentPid); if (!parent) break; // Check if parent is also interesting const interestingParent = interesting.find(ip => ip.pid === parentPid); if (interestingParent) { p.parentInteresting = parentPid; break; } parentPid = parent.ppid; depth++; } }); const topMemory = processes .filter(p => !p.interesting && p.rss > 10000) // > 10MB .slice(0, 10); return { interesting, topMemory, total: processes.length, }; } catch (err) { return { error: err.message, interesting: [], topMemory: [], total: 0 }; } } // Get emacs buffer status via emacsclient async function getEmacsStatus() { try { const { stdout } = await execAsync( `/usr/sbin/emacsclient --eval '(ac-mcp-format-state)' 2>/dev/null`, { timeout: 2000 } ); // Parse the elisp string result const state = stdout.trim().replace(/^"|"$/g, ''); return { online: true, state }; } catch { return { online: false, state: null }; } } // Get system overview function getSystemInfo() { const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; const loadAvg = os.loadavg(); const uptime = os.uptime(); return { hostname: os.hostname(), platform: os.platform(), arch: os.arch(), cpus: os.cpus().length, memory: { total: Math.round(totalMem / 1024 / 1024), // MB used: Math.round(usedMem / 1024 / 1024), free: Math.round(freeMem / 1024 / 1024), percent: Math.round((usedMem / totalMem) * 100), }, load: { '1m': loadAvg[0].toFixed(2), '5m': loadAvg[1].toFixed(2), '15m': loadAvg[2].toFixed(2), }, uptime: { seconds: uptime, formatted: formatUptime(uptime), }, }; } function formatUptime(seconds) { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const mins = Math.floor((seconds % 3600) / 60); if (days > 0) return `${days}d ${hours}h`; if (hours > 0) return `${hours}h ${mins}m`; return `${mins}m`; } // Get full status async function getFullStatus() { const [processes, emacs, system] = await Promise.all([ getProcessTree(), getEmacsStatus(), Promise.resolve(getSystemInfo()), ]); return { timestamp: Date.now(), system, emacs, processes, }; } // Main entry point async function main() { const args = process.argv.slice(2); if (args.includes('--server')) { // HTTP + WebSocket server mode const PORT = parseInt(process.env.STATUS_PORT) || 7890; const server = createServer(async (req, res) => { // CORS headers for VS Code webview res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET'); // Parse URL to handle query strings const url = new URL(req.url, `http://${req.headers.host}`); const pathname = url.pathname; if (pathname === '/status') { res.setHeader('Content-Type', 'application/json'); const status = await getFullStatus(); res.writeHead(200); res.end(JSON.stringify(status, null, 2)); } else if (pathname === '/stream') { // SSE stream for live updates res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); const sendUpdate = async () => { const status = await getFullStatus(); res.write(`data: ${JSON.stringify(status)}\n\n`); }; await sendUpdate(); const interval = setInterval(sendUpdate, 2000); req.on('close', () => { clearInterval(interval); }); } else if (pathname === '/' || pathname === '/dashboard' || pathname === '/index.html') { // Serve the interactive D3.js dashboard HTML res.setHeader('Content-Type', 'text/html'); res.writeHead(200); res.end(getDashboardHTML()); } else { res.setHeader('Content-Type', 'application/json'); res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); } }); // WebSocket server for real-time updates const wss = new WebSocketServer({ server, path: '/ws' }); wss.on('connection', (ws) => { console.log('๐Ÿ”Œ WebSocket client connected'); // Send initial status immediately getFullStatus().then(status => { ws.send(JSON.stringify(status)); }); // Send updates every 1.5 seconds const interval = setInterval(async () => { if (ws.readyState === ws.OPEN) { const status = await getFullStatus(); ws.send(JSON.stringify(status)); } }, 1500); ws.on('close', () => { console.log('๐Ÿ”Œ WebSocket client disconnected'); clearInterval(interval); }); ws.on('error', (err) => { console.error('WebSocket error:', err); clearInterval(interval); }); }); server.listen(PORT, '0.0.0.0', () => { console.log(`๐Ÿ” Devcontainer Status Server running on http://localhost:${PORT}`); console.log(` GET / - Interactive D3.js dashboard`); console.log(` GET /status - JSON status snapshot`); console.log(` GET /stream - SSE live updates`); console.log(` WS /ws - WebSocket real-time updates`); }); } else if (args.includes('--watch')) { // Continuous output mode const update = async () => { const status = await getFullStatus(); console.clear(); console.log(JSON.stringify(status, null, 2)); }; await update(); setInterval(update, 2000); } else { // Single JSON output (default) const status = await getFullStatus(); console.log(JSON.stringify(status, null, 2)); } } // Generate the interactive dashboard HTML with D3.js function getDashboardHTML() { return ` Aesthetic Computer
Aesthetic.Computer Architecture
โ€”
โ€” cpus
0
processes
โ€” / โ€” MB
`; } main().catch(err => { console.error('Error:', err); process.exit(1); process.exit(1); });