import type { ProcessInfo } from "@nibling/kernel"; import type { Files } from "./files.ts"; import { PROJECT_ROOT } from "./sandbox.ts"; /** A process and the processes it spawned that are still running. */ export interface ProcessNode { readonly process: ProcessInfo; readonly children: readonly ProcessNode[]; } /** Nests the table by ppid, oldest first. A process whose parent has exited is a root. */ export function processTree(table: readonly ProcessInfo[]): ProcessNode[] { const live = new Set(table.map((p) => p.pid)); const childrenOf = (ppid: number | null): ProcessNode[] => table .filter((p) => (ppid === null ? p.ppid === null || !live.has(p.ppid) : p.ppid === ppid)) .map((process) => ({ process, children: childrenOf(process.pid) })); return childrenOf(null); } const NPM_BIN = /^(?:\/usr\/local\/bin\/(npm|npx)|\/usr\/local\/lib\/node_modules\/npm\/bin\/(npm|npx)-cli\.js)$/; /** A process's command line as it was typed: `npm run dev`, `node server.js`, with project paths relative. */ export function commandLine(argv: readonly string[]): string { const [, entry = "", ...args] = argv; const npm = NPM_BIN.exec(entry); const words = npm ? [npm[1] ?? npm[2]!, ...args] : ["node", entry.startsWith(`${PROJECT_ROOT}/`) ? entry.slice(PROJECT_ROOT.length + 1) : entry, ...args]; return words.map((word) => (/^[\w./:@=+-]+$/.test(word) ? word : `'${word.replaceAll("'", `'\\''`)}'`)).join(" "); } /** The project's npm scripts, by name, from its package.json. A missing or unreadable one has none. */ export function projectScripts(files: Files): string[] { try { const scripts: unknown = (JSON.parse(files.read("package.json") ?? "{}") as { scripts?: unknown }).scripts; if (typeof scripts !== "object" || scripts === null) return []; return Object.entries(scripts).filter(([, command]) => typeof command === "string").map(([name]) => name); } catch { return []; } } /** The address a browser panel shows for a port the sandbox listens on. */ export const localUrl = (port: number) => `http://localhost:${port}/`; /** The sandbox port a browser panel's address names, when it names one: `http://localhost:/…`. */ export function sandboxPort(url: string): number | undefined { const parsed = URL.parse(url); if (!parsed || parsed.protocol !== "http:" || !["localhost", "127.0.0.1"].includes(parsed.hostname)) return; return Number(parsed.port || 80); }