UNPKG

relay-companion

Version:

Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.

135 lines (120 loc) • 5.4 kB
import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { canonicalRuntimeLayout } from "./canonical-runtime.js"; export const MAC_UPDATE_AGENT_LABEL = "work.relay.companion.update"; export const MAC_UPDATE_AGENT_LABEL_PREFIX = `${MAC_UPDATE_AGENT_LABEL}.`; export const ACTIVE_UPDATE_WORKER_ENV = "RELAY_CANONICAL_UPDATE_WORKER_ACTIVE"; function commandOk(result) { return Boolean(result && (result.ok === true || (!result.error && result.status === 0))); } function commandOutput(result) { return String(result?.stdout ?? result?.out ?? result?.stderr ?? "").trim(); } function defaultRun(command, args) { return spawnSync(command, args, { encoding: "utf8", timeout: 10_000 }); } export function updateAgentJobsFromLaunchctl(output) { const jobs = new Map(); for (const line of String(output || "").split(/\r?\n/)) { const parts = line.trim().split(/\s+/); const pid = /^\d+$/.test(parts[0] || "") ? Number(parts[0]) : null; const label = parts.length >= 3 ? parts[2] : ""; if (label === MAC_UPDATE_AGENT_LABEL || label.startsWith(MAC_UPDATE_AGENT_LABEL_PREFIX)) { jobs.set(label, { label, pid }); } } return [...jobs.values()].sort((left, right) => left.label.localeCompare(right.label)); } export function updateAgentLabelsFromLaunchctl(output) { return updateAgentJobsFromLaunchctl(output).map((job) => job.label); } function relayUpdatePlists({ homeDir, fsImpl, preserveCurrentWorker }) { const launchAgentsDir = path.posix.join(homeDir, "Library", "LaunchAgents"); const runtimeDir = canonicalRuntimeLayout({ homeDir, platform: "darwin" }).root; const files = new Set([path.posix.join(launchAgentsDir, `${MAC_UPDATE_AGENT_LABEL}.plist`)]); if (!preserveCurrentWorker) files.add(path.posix.join(runtimeDir, "update-worker.plist")); for (const [directory, matches] of [ [launchAgentsDir, (name) => name.startsWith(MAC_UPDATE_AGENT_LABEL_PREFIX) && name.endsWith(".plist")], [runtimeDir, (name) => name.startsWith("update-worker-") && name.endsWith(".plist")], ]) { try { for (const name of fsImpl.readdirSync(directory)) { if (matches(name)) files.add(path.posix.join(directory, name)); } } catch (error) { if (error?.code !== "ENOENT") throw error; } } return [...files]; } /** * Remove every launchd job and plist owned by Relay's retired update workers. * The one exception is the currently admitted canonical worker: repair-runtime * runs as its child, so booting out that fixed label mid-repair would kill the * transaction that is making the new runtime authoritative. */ export function cleanupMacUpdateAgents({ platform = process.platform, homeDir = os.homedir(), userId = typeof process.getuid === "function" ? process.getuid() : 0, runCommand = defaultRun, fsImpl = fs, preserveCurrentWorker = process.env[ACTIVE_UPDATE_WORKER_ENV] === "1", parentPid = process.ppid, } = {}) { if (platform !== "darwin") return { ok: true, skipped: true, removedLabels: [], removedFiles: [], failures: [] }; const failures = []; const listing = runCommand("/bin/launchctl", ["list"]); const listed = commandOk(listing); if (!listed) { failures.push({ kind: "enumerate", detail: commandOutput(listing) || "launchctl list failed" }); } const listedJobs = updateAgentJobsFromLaunchctl(commandOutput(listing)); const listedLabels = new Set(listedJobs.map((job) => job.label)); const fixedWorker = listedJobs.find((job) => job.label === MAC_UPDATE_AGENT_LABEL); // Updaters released before the environment contract still launch candidate // repair synchronously, so the live fixed worker is the repair process's parent. preserveCurrentWorker = preserveCurrentWorker || (Number.isInteger(parentPid) && parentPid > 0 && fixedWorker?.pid === parentPid); const labels = new Set(listedLabels); if (!preserveCurrentWorker) labels.add(MAC_UPDATE_AGENT_LABEL); else labels.delete(MAC_UPDATE_AGENT_LABEL); const removedLabels = []; const domain = `gui/${userId}`; for (const label of [...labels].sort()) { runCommand("/bin/launchctl", ["bootout", `${domain}/${label}`]); runCommand("/bin/launchctl", ["remove", label]); const remaining = listedLabels.has(label) ? runCommand("/bin/launchctl", ["print", `${domain}/${label}`]) : null; if (remaining && commandOk(remaining)) { failures.push({ kind: "label", label, detail: "launchd job is still loaded" }); } else { removedLabels.push(label); } } const removedFiles = []; let files = []; try { files = relayUpdatePlists({ homeDir, fsImpl, preserveCurrentWorker }); } catch (error) { failures.push({ kind: "enumerate-files", detail: error?.message || String(error) }); } for (const file of files) { try { fsImpl.unlinkSync(file); removedFiles.push(file); } catch (error) { if (error?.code !== "ENOENT") failures.push({ kind: "file", file, detail: error?.message || String(error) }); } } return { ok: failures.length === 0, removedLabels, removedFiles, failures, preserveCurrentWorker }; } export function updateAgentCleanupDetail(result) { return (result?.failures || []) .map((failure) => failure.label || failure.file || failure.detail || failure.kind) .filter(Boolean) .join("; "); }