mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* Add opt-in browser tools for desktop tabs Adds the daemon opt-in, desktop tab routing, MCP tools, and real browser automation surfaces for Paseo desktop browser tabs. * Fix browser tools CI expectations * Address browser tools review findings * Restrict browser file automation paths * Fix browser upload test on Windows * Harden browser navigation inputs * Make browser tools create usable tabs * Update browser MCP empty-state test * Fail browser tab creation when registration times out * Fix browser screenshots for agents * Hide disabled browser tools from agents * Address browser tools architecture review * Replace browser tools review tests * Wrap browser tab registration errors * Mock Expo Router in app unit tests * Handle invalid browser automation requests * Return browser failure on desktop disconnect * Update browser disconnect websocket test * Relax browser timeout polling test * Handle invalid browser responses * Return browser failure when send fails * Remove local diagnostics and fixture paths * Fix dev service home fallback * Use worktree home for dev services * Use managed daemon in desktop dev * fix(browser): keep agent tabs addressable Track agent-active browser targets separately from human-focused tabs and keep resident webviews alive for automation. Browser tool visibility now comes from registration while the broker reports disabled execution. * refactor(browser): register tools through catalog Move browser tool registration onto the shared Paseo tool catalog so the MCP server remains only the transport adapter. * fix(settings): translate browser tools host error
187 lines
4.3 KiB
JavaScript
187 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
import net from "node:net";
|
|
import path from "node:path";
|
|
import { spawn } from "node:child_process";
|
|
import { createRequire } from "node:module";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const desktopDir = path.resolve(scriptDir, "..");
|
|
const rootDir = path.resolve(desktopDir, "../..");
|
|
const appDir = path.resolve(desktopDir, "../app");
|
|
const require = createRequire(import.meta.url);
|
|
const electron = require("electron");
|
|
|
|
const expoPort = Number(process.env.EXPO_PORT);
|
|
if (!Number.isInteger(expoPort) || expoPort <= 0) {
|
|
console.error("[dev] EXPO_PORT must be set before running desktop dev");
|
|
process.exit(1);
|
|
}
|
|
|
|
const expoDevUrl = process.env.EXPO_DEV_URL || `http://localhost:${expoPort}`;
|
|
const colorEnv = {
|
|
FORCE_COLOR: process.env.FORCE_COLOR || "1",
|
|
npm_config_color: process.env.npm_config_color || "always",
|
|
};
|
|
|
|
const children = new Map();
|
|
let stopping = false;
|
|
let exitCode = 0;
|
|
|
|
function prefixStream(name, stream, target) {
|
|
let buffered = "";
|
|
stream.setEncoding("utf8");
|
|
stream.on("data", (chunk) => {
|
|
buffered += chunk;
|
|
const lines = buffered.split(/\r?\n/);
|
|
buffered = lines.pop() ?? "";
|
|
for (const line of lines) {
|
|
target.write(line ? `[${name}] ${line}\n` : `[${name}]\n`);
|
|
}
|
|
});
|
|
stream.on("end", () => {
|
|
if (buffered) {
|
|
target.write(`[${name}] ${buffered}\n`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function spawnChild(name, command, args, options = {}) {
|
|
const child = spawn(command, args, {
|
|
cwd: rootDir,
|
|
env: {
|
|
...process.env,
|
|
...colorEnv,
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
...options,
|
|
});
|
|
|
|
children.set(name, child);
|
|
prefixStream(name, child.stdout, process.stdout);
|
|
prefixStream(name, child.stderr, process.stderr);
|
|
|
|
child.on("error", (error) => {
|
|
console.error(`[${name}] failed to start: ${error.message}`);
|
|
exitCode = 1;
|
|
stopAll("SIGTERM");
|
|
});
|
|
|
|
child.on("exit", (code, signal) => {
|
|
children.delete(name);
|
|
if (!stopping) {
|
|
if (code !== 0) {
|
|
exitCode = code ?? 1;
|
|
console.error(`[${name}] exited with ${signal ?? code}`);
|
|
}
|
|
stopAll("SIGTERM");
|
|
}
|
|
});
|
|
|
|
return child;
|
|
}
|
|
|
|
function killChild(child, signal) {
|
|
if (!child.pid || child.killed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (child.detached) {
|
|
process.kill(-child.pid, signal);
|
|
} else {
|
|
child.kill(signal);
|
|
}
|
|
} catch {
|
|
// The child may have exited between the liveness check and the signal.
|
|
}
|
|
}
|
|
|
|
function stopAll(signal) {
|
|
if (stopping) {
|
|
return;
|
|
}
|
|
|
|
stopping = true;
|
|
for (const child of children.values()) {
|
|
killChild(child, signal);
|
|
}
|
|
|
|
const forceKill = setTimeout(() => {
|
|
for (const child of children.values()) {
|
|
killChild(child, "SIGKILL");
|
|
}
|
|
}, 2500);
|
|
forceKill.unref();
|
|
|
|
const finish = setInterval(() => {
|
|
if (children.size === 0) {
|
|
clearInterval(finish);
|
|
process.exit(exitCode);
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
async function waitForPort(port, host = "127.0.0.1", timeoutMs = 60_000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
while (Date.now() < deadline) {
|
|
if (await canConnect(port, host)) {
|
|
return;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for ${host}:${port}`);
|
|
}
|
|
|
|
function canConnect(port, host) {
|
|
return new Promise((resolve) => {
|
|
const socket = net.createConnection({ port, host });
|
|
socket.setTimeout(1000);
|
|
socket.once("connect", () => {
|
|
socket.destroy();
|
|
resolve(true);
|
|
});
|
|
socket.once("timeout", () => {
|
|
socket.destroy();
|
|
resolve(false);
|
|
});
|
|
socket.once("error", () => resolve(false));
|
|
});
|
|
}
|
|
|
|
process.on("SIGINT", () => stopAll("SIGTERM"));
|
|
process.on("SIGTERM", () => stopAll("SIGTERM"));
|
|
|
|
spawnChild("metro", "npx", ["expo", "start", "--port", String(expoPort)], {
|
|
cwd: appDir,
|
|
detached: true,
|
|
env: {
|
|
...process.env,
|
|
...colorEnv,
|
|
BROWSER: "none",
|
|
APP_VARIANT: "development",
|
|
PASEO_WEB_PLATFORM: "electron",
|
|
},
|
|
});
|
|
|
|
try {
|
|
await waitForPort(expoPort);
|
|
} catch (error) {
|
|
console.error(`[dev] ${error.message}`);
|
|
exitCode = 1;
|
|
stopAll("SIGTERM");
|
|
}
|
|
|
|
if (!stopping) {
|
|
spawnChild("electron", electron, [desktopDir], {
|
|
detached: true,
|
|
env: {
|
|
...process.env,
|
|
...colorEnv,
|
|
EXPO_DEV_URL: expoDevUrl,
|
|
},
|
|
});
|
|
}
|