fix: MCP agor-start uses spawn+detach instead of blocking execSync

This commit is contained in:
Hibryda 2026-03-26 00:38:57 +01:00
parent 370d4d55a0
commit c04eb58e85

View file

@ -1,13 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* agor-launcher MCP manages Electrobun app lifecycle. * agor-launcher MCP manages Electrobun app lifecycle.
* Tools: start, stop, restart, clean, rebuild, status, kill-stale, build-native, logs * Tools: start, stop, restart, clean, rebuild, status, kill-stale, build-native
*/ */
import { execSync, spawn } from "child_process"; import { execSync, spawnSync, spawn } from "child_process";
import { createInterface } from "readline"; import { createInterface } from "readline";
const SCRIPT = "/home/hibryda/code/ai/agent-orchestrator/scripts/launch.sh"; const SCRIPT = "/home/hibryda/code/ai/agent-orchestrator/scripts/launch.sh";
const ROOT = "/home/hibryda/code/ai/agent-orchestrator"; const ROOT = "/home/hibryda/code/ai/agent-orchestrator";
const EBUN = `${ROOT}/ui-electrobun`; const EBUN = `${ROOT}/ui-electrobun`;
const PTYD = `${ROOT}/agor-pty/target/release/agor-ptyd`; const PTYD = `${ROOT}/agor-pty/target/release/agor-ptyd`;
@ -15,74 +14,62 @@ const PTYD = `${ROOT}/agor-pty/target/release/agor-ptyd`;
function run(cmd, timeout = 30000) { function run(cmd, timeout = 30000) {
try { try {
return execSync(`bash ${SCRIPT} ${cmd}`, { return execSync(`bash ${SCRIPT} ${cmd}`, {
timeout, timeout, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"],
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim(); }).trim();
} catch (e) { } catch (e) {
return `Error: ${e.stderr || e.message}`; return `Error: ${e.stderr || e.message}`;
} }
} }
/** Start the app: stop old → start PTY → start Vite → launch Electrobun (backgrounded) */ /** Non-blocking start: spawn everything detached, return immediately */
function startApp(clean = false) { function startApp(clean = false) {
const steps = []; const steps = [];
// Step 1: Stop old instances // Step 1: Kill old Electrobun/WebKit (NOT node — that's us)
try { spawnSync("bash", ["-c",
execSync('pkill -f "electrobun|WebKit|AgentOrch|launcher" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true', { 'pkill -f "AgentOrch" 2>/dev/null; pkill -f "WebKitWebProcess.*9760" 2>/dev/null; pkill -f "electrobun dev" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true'
timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], ], { timeout: 3000, stdio: "ignore" });
}); steps.push("[1/4] Stopped old instances");
steps.push("[1/4] Stopped old instances");
} catch { steps.push("[1/4] Stop (nothing running)"); }
// Step 1.5: Clean if requested // Step 1.5: Clean if requested
if (clean) { if (clean) {
try { spawnSync("rm", ["-rf", `${EBUN}/build/`, `${EBUN}/node_modules/.electrobun-cache/`], { timeout: 3000 });
execSync(`rm -rf ${EBUN}/build/ ${EBUN}/node_modules/.electrobun-cache/`, { timeout: 5000 }); steps.push("[1.5] Cleaned build cache");
steps.push("[1.5] Cleaned build cache");
} catch { steps.push("[1.5] Clean skipped"); }
} }
// Step 2: Start PTY daemon (if not running) // Step 2: Start PTY daemon (if not running)
try { try {
const ptydCount = execSync("pgrep -fc agor-ptyd 2>/dev/null || echo 0", { encoding: "utf-8" }).trim(); const r = spawnSync("pgrep", ["-fc", "agor-ptyd"], { encoding: "utf-8", timeout: 2000 });
if (ptydCount === "0") { const count = (r.stdout || "0").trim();
const ptyd = spawn(PTYD, [], { detached: true, stdio: "ignore" }); if (count === "0" || count === "") {
ptyd.unref(); const p = spawn(PTYD, [], { detached: true, stdio: "ignore" });
p.unref();
steps.push("[2/4] PTY daemon started"); steps.push("[2/4] PTY daemon started");
} else { } else {
steps.push(`[2/4] PTY daemon already running (${ptydCount})`); steps.push(`[2/4] PTY daemon already running (${count})`);
} }
} catch (e) { steps.push(`[2/4] PTY daemon error: ${e.message}`); } } catch (e) { steps.push(`[2/4] PTY error: ${e.message}`); }
// Step 3: Start Vite (if port 9760 not in use) // Step 3: Start Vite (if port 9760 not in use)
try { try {
const portCheck = execSync("fuser 9760/tcp 2>/dev/null || echo free", { encoding: "utf-8" }).trim(); const r = spawnSync("fuser", ["9760/tcp"], { encoding: "utf-8", timeout: 2000 });
if (portCheck === "free") { if (r.status !== 0) {
const vite = spawn("npx", ["vite", "dev", "--port", "9760", "--host", "localhost"], { const v = spawn("npx", ["vite", "dev", "--port", "9760", "--host", "localhost"], {
cwd: EBUN, detached: true, stdio: "ignore", cwd: EBUN, detached: true, stdio: "ignore",
}); });
vite.unref(); v.unref();
steps.push("[3/4] Vite started on :9760"); steps.push("[3/4] Vite started on :9760");
} else { } else {
steps.push("[3/4] Vite already running on :9760"); steps.push("[3/4] Vite already on :9760");
} }
} catch (e) { steps.push(`[3/4] Vite error: ${e.message}`); } } catch (e) { steps.push(`[3/4] Vite error: ${e.message}`); }
// Wait for Vite to be ready // Step 4: Launch Electrobun (detached) after a brief pause via a wrapper script
try { execSync("sleep 3", { timeout: 5000 }); } catch {} const wrapper = spawn("bash", ["-c", `sleep 4; cd "${EBUN}" && npx electrobun dev`], {
detached: true, stdio: "ignore",
// Step 4: Launch Electrobun (detached, output to log) });
try { wrapper.unref();
const ebun = spawn("npx", ["electrobun", "dev"], { steps.push(`[4/4] Electrobun launching in 4s (PID ${wrapper.pid})`);
cwd: EBUN,
detached: true,
stdio: ["ignore", "ignore", "ignore"],
});
ebun.unref();
steps.push(`[4/4] Electrobun launched (PID ${ebun.pid})`);
} catch (e) { steps.push(`[4/4] Electrobun error: ${e.message}`); }
return steps.join("\n"); return steps.join("\n");
} }
@ -96,12 +83,22 @@ const TOOLS = {
"agor-stop": { "agor-stop": {
description: "Stop all running Electrobun/PTY/Vite instances.", description: "Stop all running Electrobun/PTY/Vite instances.",
schema: { type: "object", properties: {} }, schema: { type: "object", properties: {} },
handler: () => run("stop"), handler: () => {
spawnSync("bash", ["-c",
'pkill -f "AgentOrch" 2>/dev/null; pkill -f "electrobun dev" 2>/dev/null; pkill -f "WebKitWebProcess" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true'
], { timeout: 5000, stdio: "ignore" });
return "Stopped all instances.";
},
}, },
"agor-restart": { "agor-restart": {
description: "Restart Electrobun app. Pass clean=true for clean restart.", description: "Restart Electrobun app. Pass clean=true for clean restart.",
schema: { type: "object", properties: { clean: { type: "boolean", default: false } } }, schema: { type: "object", properties: { clean: { type: "boolean", default: false } } },
handler: ({ clean }) => { run("stop"); return startApp(clean); }, handler: ({ clean }) => {
spawnSync("bash", ["-c",
'pkill -f "AgentOrch" 2>/dev/null; pkill -f "electrobun dev" 2>/dev/null; pkill -f "WebKitWebProcess" 2>/dev/null; fuser -k 9760/tcp 2>/dev/null; true'
], { timeout: 5000, stdio: "ignore" });
return startApp(clean);
},
}, },
"agor-clean": { "agor-clean": {
description: "Remove build artifacts, caches, and temp files.", description: "Remove build artifacts, caches, and temp files.",
@ -121,7 +118,11 @@ const TOOLS = {
"agor-kill-stale": { "agor-kill-stale": {
description: "Kill ALL stale agor-ptyd instances that accumulated over sessions.", description: "Kill ALL stale agor-ptyd instances that accumulated over sessions.",
schema: { type: "object", properties: {} }, schema: { type: "object", properties: {} },
handler: () => run("kill-stale"), handler: () => {
spawnSync("pkill", ["-9", "-f", "agor-ptyd"], { timeout: 3000, stdio: "ignore" });
const r = spawnSync("pgrep", ["-fc", "agor-ptyd"], { encoding: "utf-8", timeout: 2000 });
return `Killed stale ptyd. Remaining: ${(r.stdout || "0").trim()}`;
},
}, },
"agor-build-native": { "agor-build-native": {
description: "Rebuild libagor-resize.so (GTK resize) and agor-ptyd (PTY daemon).", description: "Rebuild libagor-resize.so (GTK resize) and agor-ptyd (PTY daemon).",
@ -142,7 +143,7 @@ rl.on("line", (line) => {
send({ jsonrpc: "2.0", id: msg.id, result: { send({ jsonrpc: "2.0", id: msg.id, result: {
protocolVersion: "2024-11-05", protocolVersion: "2024-11-05",
capabilities: { tools: {} }, capabilities: { tools: {} },
serverInfo: { name: "agor-launcher", version: "1.0.0" }, serverInfo: { name: "agor-launcher", version: "1.1.0" },
}}); }});
} else if (msg.method === "notifications/initialized") { } else if (msg.method === "notifications/initialized") {
// no-op // no-op
@ -160,7 +161,7 @@ rl.on("line", (line) => {
} }
const result = tool.handler(msg.params.arguments || {}); const result = tool.handler(msg.params.arguments || {});
send({ jsonrpc: "2.0", id: msg.id, result: { send({ jsonrpc: "2.0", id: msg.id, result: {
content: [{ type: "text", text: result }], content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result) }],
}}); }});
} }
}); });