/** * THE REGISTRY — the one place a transport kind is turned into an implementation. * * `resolveTransport` is deliberately total over `TransportKind`: adding a kind to * the union without registering it is a compile error here rather than a silent * fall-through at a call site. Until Task 3, tmux is the only registered * implementation and that is the rollback plan — the seam lands behind no config. */ import { HERDR, TMUX_PUSH_REMOTE, TRANSPORT_KINDS, type Transport, type TransportKind } from "./types.js"; import { TmuxTransport, type TmuxHost } from "./tmux.js"; import { HerdrTransport } from "./herdr.js"; import { configuredTransport } from "./config.js"; export * from "./types.js"; export * from "./config.js"; export { TmuxTransport, tmuxAvailable, paneExists, probePane, tmuxVersion, type TmuxHost } from "./tmux.js"; export { HerdrTransport, defaultHerdrRunner, interpretHerdrReply, herdrKeyName, herdrPaneExists, herdrMarkerPid, submitModeOf, SUBMIT_MODE_ENV_VAR, HERDR_KEYS, HERDR_ABSENT_MESSAGE, type HerdrRunner, type HerdrResult, type HerdrMarkerPid, type SubmitMode } from "./herdr.js"; let host: TmuxHost | undefined; /** Wire the process-layer implementation in once, at module init. */ export function registerTmuxHost(h: TmuxHost): void { host = h; } export function resolveTransport(kind: TransportKind): Transport { if (!host) { throw new Error( "transport host not registered — call registerTmuxHost() before resolveTransport(); " + "this is a wiring error, not a runtime condition", ); } switch (kind) { // ⟨q-ec020f6a⟩ local tmux-push deleted outright — TmuxTransport now only ever // backs the remote kind. Kept fully intact: only this construction site changed. case TMUX_PUSH_REMOTE: return new TmuxTransport(host, kind); case HERDR: // Task 4: the socket transport. No host object — there is no pusher process to // inject; every call is `herdr ` through the transport's own runner. return new HerdrTransport(); default: // Unreachable for a TransportKind; reachable from JS with a string. A designed // refusal, never a default that behaves like tmux. throw new Error(`unknown transport kind ${JSON.stringify(kind)} — valid: ${TRANSPORT_KINDS.join(", ")}`); } } /* ── the ACTIVE transport, and why `running` is not read from config ────────── */ /** * The instance this process would actually use to deliver. * * Kept as an OBJECT rather than re-derived from the config value on each call, * and that is the whole design. If `running` were computed by reading the config * and resolving it, then `configured` and `running` would be two names for one * fact and `agrees` could never be false — a check that cannot fail. The active * instance is what startup actually installed, so the two can genuinely differ: * a startup that refused, a process that never wired one, a test that installed * another, a future path that falls back. */ let active: Transport | undefined; export function setActiveTransport(t: Transport): void { active = t; } /** FOR TESTS ONLY — production wires the active transport once, at startup. */ export function clearActiveTransportForTests(): void { active = undefined; } export function activeTransport(): Transport | undefined { return active; } /** * Wire the transport this fleet is configured for. Called once at startup, and * it is where an unknown config value turns into a refusal. */ export function initTransportFromConfig(): { kind: TransportKind; source: string } { const conf = configuredTransport(); setActiveTransport(resolveTransport(conf.kind)); return { kind: conf.kind, source: conf.source }; } /** * WHAT THIS PROCESS IS ACTUALLY RUNNING, answered by CALLING the transport. * * *A config value is a label someone typed.* This asks the live object instead: * it calls `available()` and puts a synthetic marker through `probe()`, and * reports what came back as the evidence beside the answer. The probe is chosen * to be discriminating rather than decorative — a tmux transport answers a * bogus pane id with a reason that names the pane, and an implementation that * does not talk to panes cannot produce that. * * Returns `undefined` for `kind` when nothing is wired, which is NOT the same as * "tmux by default": a process with no transport delivers nothing, and reporting * a default here would be the exact substitution this function exists to refuse. */ export async function runningTransport(): Promise<{ kind: TransportKind | undefined; evidence: string; }> { const t = active; if (!t) { return { kind: undefined, evidence: "no transport is wired in this process — nothing was called, and no default is assumed", }; } const availability = (() => { try { return `available()=${t.available()}`; } catch (e) { return `available() threw: ${(e as Error).message}`; } })(); let probeEvidence: string; try { const live = await t.probe({ agentId: "__capability_probe__", transport: t.kind, pid: process.pid, target: "__no_such_target__", since: Date.now(), }); probeEvidence = `probe(bogus target)=${live.state}${live.state === "live" ? "" : `: ${live.reason}`}`; } catch (e) { probeEvidence = `probe threw: ${(e as Error).message}`; } return { kind: t.kind, evidence: `${availability}, ${probeEvidence}` }; }