export * as PtyHandoff from "./pty-handoff.js" import type { PersistentPty } from "@opencode/schema/persistent-pty" import { readFile, rename, rm, writeFile } from "node:fs/promises" import type { Info } from "./service.js" import { headers } from "./service-probe.js" type Sidecar = { readonly source: Pick readonly handoff: PersistentPty.Handoff | null readonly expiresAt: number } /** * Publish the ticket before stopping its owner so every replacement contender can adopt it. * Never throws: terminals are best-effort, so any failure replaces the service without a handoff. */ export async function prepare(file: string, info: Info, timeout: number) { const existing = await read(file) if (existing !== undefined && existing.expiresAt > Date.now() && same(existing.source, info)) return const { OpenCode } = await import("./promise/index.js") const client = OpenCode.make({ baseUrl: info.url, headers: headers({ url: info.url, auth: info.password === undefined ? undefined : { type: "basic", username: "opencode", password: info.password }, }), }) const handoff = await client.experimental.persistentPty .handoff({ signal: AbortSignal.timeout(timeout) }) .then(parse) .catch(async (cause: unknown) => { // Another caller may already have prepared and stopped this server. const concurrent = await read(file) if (concurrent !== undefined && concurrent.expiresAt > Date.now() && same(concurrent.source, info)) return console.warn("Background service cannot hand off persistent terminals; shutting them down before replacement", cause) await client.experimental.persistentPty.shutdown({ signal: AbortSignal.timeout(timeout) }).catch(() => {}) return null }) if (handoff === undefined) return await publish(file, info, handoff).catch((cause: unknown) => console.warn("Failed to publish persistent terminal handoff", cause), ) } function parse(body: unknown) { if (typeof body !== "object" || body === null || !("handoff" in body)) throw new Error("Invalid persistent terminal handoff response") if (body.handoff === null) return null if (!isHandoff(body.handoff) || body.handoff.expiresAt <= Date.now()) throw new Error("Invalid or expired persistent terminal handoff") return body.handoff } async function publish(file: string, info: Info, handoff: PersistentPty.Handoff | null) { const temporary = `${file}.pty-handoff.${crypto.randomUUID()}.tmp` await writeFile( temporary, JSON.stringify({ source: { id: info.id, pid: info.pid, url: info.url }, handoff, expiresAt: handoff?.expiresAt ?? Date.now() + 30_000, } satisfies Sidecar), { mode: 0o600, flag: "wx" }, ) await rename(temporary, file + ".pty-handoff").finally(() => rm(temporary, { force: true })) } export async function environment(file: string, env?: Readonly>) { const record = await read(file) const current: Info | undefined = await readFile(file, "utf8") .then((text) => JSON.parse(text)) .catch(() => undefined) const handoff = record !== undefined && record.expiresAt > Date.now() && (current === undefined || same(record.source, current)) ? record.handoff : undefined return { ...env, OPENCODE_PTY_HANDOFF: handoff == null ? undefined : JSON.stringify(handoff) } } export async function complete(file: string, info: Info) { const record = await read(file) if (record !== undefined && !same(record.source, info)) await clear(file) } export async function clear(file: string) { await rm(file + ".pty-handoff", { force: true }) } async function read(file: string): Promise { const value: unknown = await readFile(file + ".pty-handoff", "utf8") .then((text) => JSON.parse(text)) .catch(() => undefined) if (typeof value !== "object" || value === null || !("source" in value) || !("handoff" in value)) return if (typeof value.source !== "object" || value.source === null) return if (!("pid" in value.source) || typeof value.source.pid !== "number") return if (!("url" in value.source) || typeof value.source.url !== "string") return if ("id" in value.source && typeof value.source.id !== "string") return if (value.handoff !== null && !isHandoff(value.handoff)) return if (!("expiresAt" in value) || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt)) return return { source: { id: "id" in value.source && typeof value.source.id === "string" ? value.source.id : undefined, pid: value.source.pid, url: value.source.url, }, handoff: value.handoff, expiresAt: value.expiresAt, } } function same(left: Sidecar["source"], right: Info) { return left.id === right.id && left.pid === right.pid && left.url === right.url } function isHandoff(value: unknown): value is PersistentPty.Handoff { return ( typeof value === "object" && value !== null && "directory" in value && typeof value.directory === "string" && "instanceID" in value && typeof value.instanceID === "string" && "ticket" in value && typeof value.ticket === "string" && "expiresAt" in value && typeof value.expiresAt === "number" && Number.isFinite(value.expiresAt) ) }