relay-companion
Version:
Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.
293 lines (274 loc) • 12.9 kB
JavaScript
;
const path = require("node:path");
const os = require("node:os");
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const { systemdImportEnvironmentArgs } = require("./linux-systemd.cjs");
const DAEMON_LABEL = "work.relay.companion";
const PILL_LABEL = "work.relay.companion.pill";
// A local checkout may run beside Relay without invalidating the installed
// runtime. Only installed relay-companion trees count as production services.
const SERVICE_TREE_RE = /node_modules[\\/]relay-companion[\\/]/i;
function commandOk(result) {
return Boolean(result && !result.error && result.status === 0);
}
function defaultRun(command, args, options = {}) {
return spawnSync(command, args, { encoding: "utf8", timeout: 30_000, windowsHide: true, ...options });
}
function runtimeProcessCommands(platform, run = defaultRun, userId = typeof process.getuid === "function" ? process.getuid() : 0) {
if (platform === "win32") {
const script = "Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | ForEach-Object { $_.CommandLine }";
const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script]);
return commandOk(result) ? String(result.stdout || "").split(/\r?\n/) : [];
}
const result = run("/bin/ps", ["-axo", "uid=,command="]);
if (!commandOk(result)) return [];
const lines = [];
for (const row of String(result.stdout || "").split(/\r?\n/)) {
const match = row.match(/^\s*(\d+)\s+(.*)$/);
if (match && Number(match[1]) === userId) lines.push(match[2]);
}
return lines;
}
function exactRuntimeHealth(target, {
platform = process.platform,
run = defaultRun,
commands = null,
userId = typeof process.getuid === "function" ? process.getuid() : 0,
} = {}) {
const api = platform === "win32" ? path.win32 : path.posix;
const lines = commands || runtimeProcessCommands(platform, run, userId);
const normalize = (value) => (platform === "win32" ? String(value).toLowerCase() : String(value)).replaceAll("\\", "/");
const daemonNeedle = normalize(target.bin);
const pillNeedle = normalize(api.join(target.packageRoot, "overlay", "main.cjs"));
const relayDaemons = lines.filter((line) => SERVICE_TREE_RE.test(line) && /(?:^|[\\/])relay\.js(?:"|'|\s).*\bdaemon\b/i.test(line));
const relayPills = lines.filter((line) => SERVICE_TREE_RE.test(line) && /(?:^|[\\/])overlay[\\/]main\.cjs(?:"|'|\s|$)/i.test(line));
const daemon = relayDaemons.some((line) => normalize(line).includes(daemonNeedle));
const pill = relayPills.some((line) => normalize(line).includes(pillNeedle));
const oldDaemon = relayDaemons.some((line) => !normalize(line).includes(daemonNeedle));
const oldPill = relayPills.some((line) => !normalize(line).includes(pillNeedle));
return { ok: daemon && pill && !oldDaemon && !oldPill, daemon, pill, oldDaemon, oldPill, packageRoot: target.packageRoot };
}
function linuxPillStatusPath({ homeDir = os.homedir(), env = process.env } = {}) {
const root = env.RELAY_HOME || env.RELAY_COMPANION_HOME || path.posix.join(homeDir, ".relay-companion");
return path.posix.join(root, "pill-status.json");
}
function exactLinuxPillReady(target, {
homeDir = os.homedir(),
env = process.env,
fsImpl = fs,
} = {}) {
const statusPath = linuxPillStatusPath({ homeDir, env });
try {
const status = JSON.parse(fsImpl.readFileSync(statusPath, "utf8"));
const packageRoot = path.posix.resolve(String(status?.packageRoot || ""));
const expected = path.posix.resolve(String(target?.packageRoot || ""));
return {
ok: status?.ready === true && Number.isInteger(status?.pid) && status.pid > 0 && packageRoot === expected,
status,
statusPath,
};
} catch (error) {
return { ok: false, status: null, statusPath, detail: error?.message || String(error) };
}
}
function installedServiceProcessRows(target, {
run = defaultRun,
includeTarget = false,
processId = process.pid,
userId = typeof process.getuid === "function" ? process.getuid() : 0,
} = {}) {
const result = run("/bin/ps", ["-axo", "uid=,pid=,command="]);
if (!commandOk(result)) {
return {
ok: false,
rows: [],
reason: "service-process-query-failed",
detail: result?.error?.message || String(result?.stderr || result?.stdout || "").trim(),
};
}
const targetNeedle = String(target?.packageRoot || "").replaceAll("\\", "/");
const rows = [];
for (const line of String(result.stdout || "").split("\n")) {
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
if (!match) continue;
const uid = Number(match[1]);
const pid = Number(match[2]);
const command = match[3];
if (!Number.isInteger(uid) || uid !== userId) continue;
// This is the safety boundary: only an immutable/global production install
// has node_modules/relay-companion in its command. A checkout's daemon or
// Electron pill is deliberately invisible and must never be terminated.
if (!SERVICE_TREE_RE.test(command)) continue;
const isDaemon = /(?:^|[\\/])relay\.js(?:"|'|\s).*\bdaemon\b/i.test(command);
const isPill = /(?:^|[\\/])overlay[\\/]main\.cjs(?:"|'|\s|$)/i.test(command);
if (!isDaemon && !isPill) continue;
if (!includeTarget && targetNeedle && command.replaceAll("\\", "/").includes(targetNeedle)) continue;
if (!Number.isInteger(pid) || pid <= 0 || pid === processId) continue;
rows.push({ pid, command });
}
return { ok: true, rows };
}
async function terminateInstalledServiceProcesses(target, {
run = defaultRun,
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
graceMs = 2000,
includeTarget = false,
processId = process.pid,
userId = typeof process.getuid === "function" ? process.getuid() : 0,
} = {}) {
const first = installedServiceProcessRows(target, { run, includeTarget, processId, userId });
if (!first.ok) return first;
if (!first.rows.length) return { ok: true, terminated: [] };
for (const row of first.rows) run("/bin/kill", ["-TERM", String(row.pid)]);
await sleep(graceMs);
const afterTerm = installedServiceProcessRows(target, { run, includeTarget, processId, userId });
if (!afterTerm.ok) return afterTerm;
const originalPids = new Set(first.rows.map((row) => row.pid));
const survivors = afterTerm.rows.filter((row) => originalPids.has(row.pid));
for (const row of survivors) run("/bin/kill", ["-KILL", String(row.pid)]);
if (survivors.length) await sleep(500);
const afterKill = installedServiceProcessRows(target, { run, includeTarget, processId, userId });
if (!afterKill.ok) return afterKill;
const remaining = afterKill.rows.filter((row) => originalPids.has(row.pid));
if (remaining.length) {
return { ok: false, reason: "service-process-stop-failed", detail: remaining.map((row) => row.pid).join(",") };
}
return { ok: true, terminated: first.rows.map((row) => row.pid) };
}
/**
* Restart Relay's two macOS jobs from the registrations already written by the
* caller, then prove that both processes belong to the requested package root.
* Registration remains owned by setup/repair; this helper owns only the narrow
* launchd handoff and never edits a plist, account file, or installed binary.
*/
async function activateMacRuntimeServices(target, {
homeDir = os.homedir(),
platform = process.platform,
run = defaultRun,
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
domainPollMs = 250,
bootstrapDelayMs = 500,
healthPollMs = 500,
activationDeadlineMs = 90_000,
now = Date.now,
processId = process.pid,
healthCheck = exactRuntimeHealth,
} = {}) {
if (platform !== "darwin") return { ok: false, reason: "activation-platform-unsupported" };
if (!target?.packageRoot || !target?.bin) return { ok: false, reason: "activation-target-invalid" };
const domain = `gui/${typeof process.getuid === "function" ? process.getuid() : 0}`;
const agents = path.posix.join(String(homeDir || ""), "Library", "LaunchAgents");
const labels = [PILL_LABEL, DAEMON_LABEL];
const deadline = now() + Math.max(1, activationDeadlineMs);
for (const label of labels) run("/bin/launchctl", ["bootout", `${domain}/${label}`]);
// launchctl bootout is asynchronous. Keep quiescence, bootstrap, and liveness
// under one deadline so a racing EIO cannot create an unbounded retry loop.
while (now() <= deadline) {
const labelsPresent = labels.filter((label) => commandOk(run("/bin/launchctl", ["print", `${domain}/${label}`])));
if (labelsPresent.length) {
for (const label of labelsPresent) run("/bin/launchctl", ["bootout", `${domain}/${label}`]);
await sleep(domainPollMs);
continue;
}
const stopped = await terminateInstalledServiceProcesses(target, {
run,
sleep,
includeTarget: true,
processId,
});
if (!stopped.ok) return stopped;
let bootstrapFailure = null;
for (const label of labels) {
const started = run("/bin/launchctl", ["bootstrap", domain, path.posix.join(agents, `${label}.plist`)]);
if (!commandOk(started)) {
bootstrapFailure = { label, result: started };
break;
}
}
if (bootstrapFailure) {
for (const label of labels) run("/bin/launchctl", ["bootout", `${domain}/${label}`]);
const detail = String(bootstrapFailure.result?.stderr || bootstrapFailure.result?.stdout || "");
if (!/(?:Bootstrap failed:\s*5|Input\/output error|I\/O error)/i.test(detail)) {
return { ok: false, reason: "service-bootstrap-failed", detail: `${bootstrapFailure.label}: ${detail}` };
}
await sleep(bootstrapDelayMs);
continue;
}
while (now() <= deadline) {
const health = await healthCheck(target, { platform, run });
if (health?.ok) return { ok: true, health };
if (health?.oldDaemon || health?.oldPill) {
for (const label of labels) run("/bin/launchctl", ["bootout", `${domain}/${label}`]);
break;
}
await sleep(healthPollMs);
}
}
return { ok: false, reason: "activation-deadline-exceeded", detail: target.packageRoot };
}
/** Restart Linux user services from their already-repaired unit files. */
async function activateLinuxRuntimeServices(target, {
homeDir = os.homedir(),
platform = process.platform,
env = process.env,
run = defaultRun,
fsImpl = fs,
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
attempts = 60,
healthPollMs = 500,
processId = process.pid,
healthCheck = exactRuntimeHealth,
pillReadyCheck = exactLinuxPillReady,
requiredHealthySamples = 4,
} = {}) {
if (platform !== "linux") return { ok: false, reason: "activation-platform-unsupported" };
if (!target?.packageRoot || !target?.bin) return { ok: false, reason: "activation-target-invalid" };
const units = [`${PILL_LABEL}.service`, `${DAEMON_LABEL}.service`];
for (const unit of units) run("systemctl", ["--user", "stop", unit]);
const stopped = await terminateInstalledServiceProcesses(target, {
run,
sleep,
includeTarget: true,
processId,
});
if (!stopped.ok) return stopped;
const statusPath = linuxPillStatusPath({ homeDir, env });
try { fsImpl.rmSync(statusPath, { force: true }); } catch {}
const reloaded = run("systemctl", ["--user", "daemon-reload"]);
if (!commandOk(reloaded)) {
return { ok: false, reason: "systemd-user-unavailable", detail: reloaded?.stderr || reloaded?.stdout || "" };
}
const importArgs = systemdImportEnvironmentArgs(env);
if (importArgs.length) {
const imported = run("systemctl", importArgs);
if (!commandOk(imported)) {
return { ok: false, reason: "graphical-environment-import-failed", detail: imported?.stderr || imported?.stdout || "" };
}
}
for (const unit of units) {
const started = run("systemctl", ["--user", "start", unit]);
if (!commandOk(started)) {
return { ok: false, reason: "service-start-failed", detail: `${unit}: ${started?.stderr || started?.stdout || ""}` };
}
}
let healthySamples = 0;
for (let attempt = 0; attempt < attempts; attempt += 1) {
const health = await healthCheck(target, { platform, run });
const ready = pillReadyCheck(target, { homeDir, env, fsImpl });
healthySamples = health?.ok && ready?.ok ? healthySamples + 1 : 0;
if (healthySamples >= Math.max(1, requiredHealthySamples)) return { ok: true, health, ready, healthySamples };
if (attempt + 1 < attempts) await sleep(healthPollMs);
}
return { ok: false, reason: "exact-root-readiness-failed", detail: `${target.packageRoot}; ${statusPath}` };
}
module.exports = {
activateLinuxRuntimeServices,
activateMacRuntimeServices,
exactRuntimeHealth,
exactLinuxPillReady,
installedServiceProcessRows,
linuxPillStatusPath,
runtimeProcessCommands,
terminateInstalledServiceProcesses,
};