188 lines
7.8 KiB
JavaScript
188 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* agor-launcher MCP — manages Electrobun app lifecycle.
|
|
* Tools: start, stop, restart, clean, rebuild, status, kill-stale, build-native
|
|
*/
|
|
import { execSync, spawnSync, spawn } from "child_process";
|
|
import { createInterface } from "readline";
|
|
|
|
const SCRIPT = "/home/hibryda/code/ai/agent-orchestrator/scripts/launch.sh";
|
|
const ROOT = "/home/hibryda/code/ai/agent-orchestrator";
|
|
const EBUN = `${ROOT}/ui-electrobun`;
|
|
const PTYD = `${ROOT}/agor-pty/target/release/agor-ptyd`;
|
|
|
|
function run(cmd, timeout = 30000) {
|
|
try {
|
|
return execSync(`bash ${SCRIPT} ${cmd}`, {
|
|
timeout, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"],
|
|
}).trim();
|
|
} catch (e) {
|
|
return `Error: ${e.stderr || e.message}`;
|
|
}
|
|
}
|
|
|
|
/** Non-blocking start: spawn everything detached, return immediately */
|
|
function startApp(clean = false) {
|
|
const steps = [];
|
|
|
|
// Step 1: Kill old Electrobun/WebKit (NOT node — that's us)
|
|
spawnSync("bash", ["-c",
|
|
'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: 3000, stdio: "ignore" });
|
|
steps.push("[1/4] Stopped old instances");
|
|
|
|
// Step 1.5: Clean if requested
|
|
if (clean) {
|
|
spawnSync("rm", ["-rf", `${EBUN}/build/`, `${EBUN}/node_modules/.electrobun-cache/`], { timeout: 3000 });
|
|
steps.push("[1.5] Cleaned build cache");
|
|
}
|
|
|
|
// Step 2: Start PTY daemon (if not running)
|
|
try {
|
|
const r = spawnSync("pgrep", ["-fc", "agor-ptyd"], { encoding: "utf-8", timeout: 2000 });
|
|
const count = (r.stdout || "0").trim();
|
|
if (count === "0" || count === "") {
|
|
const p = spawn(PTYD, [], { detached: true, stdio: "ignore" });
|
|
p.unref();
|
|
steps.push("[2/4] PTY daemon started");
|
|
} else {
|
|
steps.push(`[2/4] PTY daemon already running (${count})`);
|
|
}
|
|
} catch (e) { steps.push(`[2/4] PTY error: ${e.message}`); }
|
|
|
|
// Step 3: Start Vite (if port 9760 not in use)
|
|
try {
|
|
const r = spawnSync("fuser", ["9760/tcp"], { encoding: "utf-8", timeout: 2000 });
|
|
if (r.status !== 0) {
|
|
const v = spawn("npx", ["vite", "dev", "--port", "9760", "--host", "localhost"], {
|
|
cwd: EBUN, detached: true, stdio: "ignore",
|
|
});
|
|
v.unref();
|
|
steps.push("[3/4] Vite started on :9760");
|
|
} else {
|
|
steps.push("[3/4] Vite already on :9760");
|
|
}
|
|
} catch (e) { steps.push(`[3/4] Vite error: ${e.message}`); }
|
|
|
|
// Step 4: Launch Electrobun (detached) after a brief pause via a wrapper script
|
|
const wrapper = spawn("bash", ["-c", `sleep 4; cd "${EBUN}" && npx electrobun dev`], {
|
|
detached: true, stdio: "ignore",
|
|
});
|
|
wrapper.unref();
|
|
steps.push(`[4/4] Electrobun launching in 4s (PID ${wrapper.pid})`);
|
|
|
|
return steps.join("\n");
|
|
}
|
|
|
|
const TOOLS = {
|
|
"agor-start": {
|
|
description: "Start Electrobun app (kills old instances first). Pass clean=true to remove build cache.",
|
|
schema: { type: "object", properties: { clean: { type: "boolean", default: false } } },
|
|
handler: ({ clean }) => startApp(clean),
|
|
},
|
|
"agor-stop": {
|
|
description: "Stop all running Electrobun/PTY/Vite instances.",
|
|
schema: { type: "object", properties: {} },
|
|
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": {
|
|
description: "Restart Electrobun app. Pass clean=true for clean restart.",
|
|
schema: { type: "object", properties: { clean: { type: "boolean", default: false } } },
|
|
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": {
|
|
description: "Remove build artifacts, caches, and temp files.",
|
|
schema: { type: "object", properties: {} },
|
|
handler: () => run("clean"),
|
|
},
|
|
"agor-rebuild": {
|
|
description: "Full rebuild: clean + npm install + vite build + native C library + PTY daemon.",
|
|
schema: { type: "object", properties: {} },
|
|
handler: () => {
|
|
// Stop app (targeted kill — don't kill this Node/MCP process)
|
|
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" });
|
|
// Clean
|
|
spawnSync("rm", ["-rf", `${EBUN}/build/`, `${EBUN}/node_modules/.electrobun-cache/`], { timeout: 5000 });
|
|
// npm install
|
|
const npm = spawnSync("npm", ["install", "--legacy-peer-deps"], { cwd: EBUN, timeout: 60000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
// vite build
|
|
const vite = spawnSync("npx", ["vite", "build"], { cwd: EBUN, timeout: 60000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
// native build
|
|
const native = spawnSync("bash", ["-c", `cd ${ROOT}/agor-pty/native && gcc -shared -fPIC -o libagor-resize.so agor_resize.c $(pkg-config --cflags --libs gtk+-3.0) 2>&1`], { timeout: 30000, encoding: "utf-8" });
|
|
const cargo = spawnSync("cargo", ["build", "--release"], { cwd: `${ROOT}/agor-pty`, timeout: 120000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
return [
|
|
"Rebuild complete:",
|
|
`npm: ${npm.status === 0 ? 'ok' : 'FAIL'}`,
|
|
`vite: ${vite.status === 0 ? 'ok' : 'FAIL'}`,
|
|
`native: ${native.status === 0 ? 'ok' : 'FAIL'}`,
|
|
`cargo: ${cargo.status === 0 ? 'ok' : 'FAIL'}`,
|
|
].join("\n");
|
|
},
|
|
},
|
|
"agor-status": {
|
|
description: "Show running processes, ports, and window status.",
|
|
schema: { type: "object", properties: {} },
|
|
handler: () => run("status"),
|
|
},
|
|
"agor-kill-stale": {
|
|
description: "Kill ALL stale agor-ptyd instances that accumulated over sessions.",
|
|
schema: { type: "object", properties: {} },
|
|
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": {
|
|
description: "Rebuild libagor-resize.so (GTK resize) and agor-ptyd (PTY daemon).",
|
|
schema: { type: "object", properties: {} },
|
|
handler: () => run("build-native", 120000),
|
|
},
|
|
};
|
|
|
|
// Minimal MCP stdio server
|
|
const rl = createInterface({ input: process.stdin });
|
|
function send(msg) { process.stdout.write(JSON.stringify(msg) + "\n"); }
|
|
|
|
rl.on("line", (line) => {
|
|
let msg;
|
|
try { msg = JSON.parse(line); } catch { return; }
|
|
|
|
if (msg.method === "initialize") {
|
|
send({ jsonrpc: "2.0", id: msg.id, result: {
|
|
protocolVersion: "2024-11-05",
|
|
capabilities: { tools: {} },
|
|
serverInfo: { name: "agor-launcher", version: "1.1.0" },
|
|
}});
|
|
} else if (msg.method === "notifications/initialized") {
|
|
// no-op
|
|
} else if (msg.method === "tools/list") {
|
|
send({ jsonrpc: "2.0", id: msg.id, result: {
|
|
tools: Object.entries(TOOLS).map(([name, t]) => ({
|
|
name, description: t.description, inputSchema: t.schema,
|
|
})),
|
|
}});
|
|
} else if (msg.method === "tools/call") {
|
|
const tool = TOOLS[msg.params.name];
|
|
if (!tool) {
|
|
send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Unknown tool: ${msg.params.name}` }});
|
|
return;
|
|
}
|
|
const result = tool.handler(msg.params.arguments || {});
|
|
send({ jsonrpc: "2.0", id: msg.id, result: {
|
|
content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result) }],
|
|
}});
|
|
}
|
|
});
|