@lengmoxxl/dsh-remote-workspace 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/README.zh.md +78 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +16344 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +87 -0
- package/lib/index.js +5741 -0
- package/lib/index.js.map +1 -0
- package/package.json +183 -0
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["state","FILE_MODE","DOCUMENT_VERSION","DOCUMENT_VERSION","requireChannel","Buffer","describe","Buffer","raw"],"sources":["../src/tty.ts","../src/local/tty.ts","../src/models/autoconnect.ts","../src/remote/protocol.ts","../src/remote/client.ts","../src/remote/ssh.ts","../src/remote/agent/release.ts","../src/remote/agent/install.ts","../src/models/machines.ts","../src/storage/document.ts","../src/storage/nodes.ts","../src/models/routing.ts","../src/storage/anchors.ts","../src/local/git.ts","../src/local/fs.ts","../src/models/worktrees.ts","../src/storage/repos.ts","../src/plugin/api.ts","../src/plugin/routing/fs.ts","../src/plugin/routing/shell.ts","../src/remote/tty.ts","../src/plugin/routing/remote.ts","../src/plugin/routing/tty.ts","../src/plugin/routing/subprocess.ts","../src/terminal/shared/display.ts","../src/terminal/host/display.ts","../src/terminal/host/registry.ts","../src/terminal/host/workspace.ts","../src/terminal/host/terminal.ts","../src/terminal/host/socket.ts","../src/terminal/shared/wire.ts","../src/tools/terminal.ts","../src/index.ts"],"sourcesContent":["/**\n * The terminal seam: one PTY, on whichever machine owns a workspace.\n *\n * A consumer allocates a terminal through `ctx.tty` and gets a handle. Two\n * shapes of provider satisfy this seam: one that owns a PTY on this host, and\n * one that proxies a PTY a node daemon holds. The consumer names a working\n * directory and never asks which — the provider composed for that directory\n * answers, exactly as `ctx.fs` and `ctx.subprocess` do.\n *\n * `resize` is why this seam exists beside `ctx.subprocess`. A terminal is a view\n * a person changes the shape of while the shell inside it keeps running, and\n * the subprocess seam has no verb for that: its terminal handle stops at\n * allocation, text, foreground groups, and teardown.\n *\n * The seam carries no foreground verbs yet. The consumer is a terminal on\n * screen, whose Ctrl-C travels as input bytes the line discipline turns into a\n * signal; the model-facing PTY tools keep using the seams that already carry\n * them. A verb earns its place here with a caller.\n *\n * @module dsh-remote-workspace/tty\n */\n\nimport { Service, type Context } from '@deepseek-ai/cordis'\nimport type { Readable } from 'node:stream'\n\n/** How one terminal's top-level process ended. */\nexport interface TtyOutcome {\n /** Exit code, or null when a signal ended it. */\n readonly exitCode: number | null\n /** Signal that ended it, or null for a normal exit. */\n readonly signal: NodeJS.Signals | null\n}\n\n/**\n * Grace a provider gives a terminal between the terminate signal and the kill\n * that follows it, when the caller names none.\n */\nexport const DEFAULT_TTY_GRACE_MS = 3000\n\n/** What one terminal is asked for. */\nexport interface TtySpawnRequest {\n /** Program and arguments, resolved on the machine that runs them. */\n readonly argv: readonly [string, ...string[]]\n /** Working directory on the machine that owns it. */\n readonly cwd: string\n /** Variables layered onto the provider's own environment. */\n readonly env?: Readonly<Record<string, string>>\n /** Initial window size, in columns. */\n readonly cols: number\n /** Initial window size, in rows. */\n readonly rows: number\n /**\n * Milliseconds between the terminate signal and the kill that follows it;\n * {@link DEFAULT_TTY_GRACE_MS} when omitted.\n */\n readonly graceMs?: number\n}\n\n/** One live terminal. */\nexport interface TtyHandle {\n /** Process id on the machine that owns the terminal. */\n readonly pid: number\n /** Output bytes in delivery order; ends when the terminal does. */\n readonly output: Readable\n /** Resolves when the top-level process exits. */\n readonly done: Promise<TtyOutcome>\n /**\n * Deliver input bytes.\n * @param data - UTF-8 text, sent without newline conversion.\n */\n write(data: string): Promise<void>\n /** Adopt a new window size. */\n resize(cols: number, rows: number): Promise<void>\n /**\n * Release the terminal, escalating to a kill after the grace period.\n * Idempotent: a second call joins the first.\n */\n terminate(): Promise<void>\n}\n\n/**\n * Service provider for terminals.\n *\n * Extending this class is what publishes the provider as `ctx.tty`; a routing\n * provider that is not a subclass is registered with `ctx.provide` instead, the\n * way the other routing seams in this workspace are.\n */\nexport abstract class TtyRuntime extends Service {\n constructor(ctx: Context) {\n super(ctx, 'tty')\n }\n\n /**\n * Allocate one terminal and start the program in it.\n * @param request - what to run, where, how large, and how to end it.\n * @returns the live handle, valid until the program exits.\n */\n abstract spawn(request: TtySpawnRequest): Promise<TtyHandle>\n}\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n /** Terminal provider composed for this deployment. */\n tty: TtyRuntime\n }\n}\n","/**\n * The local terminal provider: a PTY on this host.\n *\n * This is the shape a deployment gets when nothing routes a working directory\n * elsewhere. A routing provider composes it for the directories it does not own,\n * which is why the work lives in a class: the same code serves a standalone\n * deployment and one that answers for local paths inside a router.\n *\n * node-pty is the substrate because the seam needs `resize`, and the PTY is what\n * holds the window size: writing to a master descriptor cannot change it. The\n * size a terminal was born with is therefore not the size it keeps, which is the\n * whole point of this package.\n *\n * @module dsh-remote-workspace/local/tty\n */\n\nimport { constants } from 'node:os'\nimport { PassThrough } from 'node:stream'\nimport * as nodePty from 'node-pty'\nimport { DEFAULT_TTY_GRACE_MS, TtyRuntime } from '../tty.ts'\nimport type { TtyHandle, TtyOutcome, TtySpawnRequest } from '../tty.ts'\n\n/** What a PTY calls itself when the caller names no terminal type. */\nconst TERM = 'xterm-256color'\n\n/** How long a killed terminal is given to report its exit before the wait ends. */\nconst KILL_SETTLE_MS = 2000\n\n/**\n * The environment a shell starts in.\n *\n * This process's own environment, so a terminal sees the same PATH and tooling\n * an agent on this host would, with the terminal type and the caller's entries\n * layered over it. Variables Node leaves undefined are dropped rather than sent\n * as the string \"undefined\".\n * @param overrides - entries the caller supplied.\n * @returns the environment to spawn with.\n */\nfunction spawnEnv(overrides: Readonly<Record<string, string>> | undefined): Record<string, string> {\n const env: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value !== undefined) env[key] = value\n }\n return { ...env, TERM, ...overrides }\n}\n\n/**\n * The name of a signal node-pty reported as a number.\n * @param code - the number from the exit event; zero means no signal.\n * @returns the signal name, or null when the process exited on its own.\n */\nfunction signalName(code: number): NodeJS.Signals | null {\n if (code === 0) return null\n for (const [name, value] of Object.entries(constants.signals)) {\n if (value === code) return name as NodeJS.Signals\n }\n return null\n}\n\n/** Wait for one promise, or for its budget to run out. */\nasync function within(ms: number, promise: Promise<void>): Promise<void> {\n let timer: NodeJS.Timeout | undefined\n const expiry = new Promise<void>((resolve) => { timer = setTimeout(resolve, ms) })\n await Promise.race([promise, expiry])\n if (timer !== undefined) clearTimeout(timer)\n}\n\n/** One terminal this host owns. */\nclass LocalTtyHandle implements TtyHandle {\n readonly pid: number\n readonly output = new PassThrough()\n readonly done: Promise<TtyOutcome>\n private readonly terminal: nodePty.IPty\n private readonly graceMs: number\n private exited = false\n private stopping: Promise<void> | undefined\n private readonly exit: Promise<void>\n\n /**\n * @param terminal - the allocated node-pty process.\n * @param graceMs - milliseconds between the terminate signal and the kill.\n */\n constructor(terminal: nodePty.IPty, graceMs: number) {\n this.terminal = terminal\n this.pid = terminal.pid\n this.graceMs = graceMs\n let settleExit: () => void = () => {}\n this.exit = new Promise<void>((resolve) => { settleExit = resolve })\n let settleDone: (outcome: TtyOutcome) => void = () => {}\n this.done = new Promise<TtyOutcome>((resolve) => { settleDone = resolve })\n terminal.onData(data => { this.output.write(Buffer.from(data, 'utf8')) })\n terminal.onExit(({ exitCode, signal }) => {\n this.exited = true\n this.output.end()\n settleExit()\n settleDone({ exitCode, signal: signalName(signal ?? 0) })\n })\n }\n\n async write(data: string): Promise<void> {\n this.terminal.write(data)\n }\n\n async resize(cols: number, rows: number): Promise<void> {\n this.terminal.resize(cols, rows)\n }\n\n async terminate(): Promise<void> {\n this.stopping ??= this.stop()\n await this.stopping\n }\n\n /**\n * Signal the process, kill it if it outlives the grace period, and wait for\n * the exit it reports. A terminal that never reports one ends the wait rather\n * than hanging the caller on a process the kernel has already killed.\n */\n private async stop(): Promise<void> {\n if (this.exited) return\n this.terminal.kill('SIGTERM')\n await within(this.graceMs, this.exit)\n if (this.exited) return\n this.terminal.kill('SIGKILL')\n await within(KILL_SETTLE_MS, this.exit)\n }\n}\n\n/**\n * A terminal provider that owns PTYs on this host.\n *\n * It takes no config: every choice a terminal needs — what to run, where, how\n * large, how long to wait before killing — arrives on the request, so nothing\n * about a deployment has to be settled before a terminal is asked for.\n */\nexport class LocalTtyRuntime extends TtyRuntime {\n async spawn(request: TtySpawnRequest): Promise<TtyHandle> {\n const terminal = nodePty.spawn(request.argv[0], request.argv.slice(1), {\n name: TERM,\n cols: request.cols,\n rows: request.rows,\n cwd: request.cwd,\n env: spawnEnv(request.env),\n })\n return new LocalTtyHandle(terminal, request.graceMs ?? DEFAULT_TTY_GRACE_MS)\n }\n}\n","/**\n * Bring every configured machine up, and keep trying the ones that are down.\n *\n * Nothing connects on an operation's behalf: a machine is reachable because it\n * was connected, not because a file read happened to need it. Without the\n * startup pass a deployment would sit with every machine idle until a person\n * opened the section and clicked Connect — and a restored session would find\n * its workspace unreachable.\n *\n * One machine gets a few attempts in a row, because a failure right after\n * startup is often a machine still booting or an agent still starting. A\n * follow-up pass then revisits the ones still failed, because a first attempt\n * can also meet a link that is not up yet — a laptop that just woke, a VPN\n * still dialling — and that clears on its own. Only `failed` is revisited: a\n * machine a person disconnected stays down.\n *\n * The passes run in the background and never reject. Activation must not wait\n * for SSH, and a machine that cannot be reached is a state the section renders,\n * not a failure of the plugin.\n *\n * @module dsh-remote-workspace/models/autoconnect\n */\n\nimport type { NodeId, NodeRecord } from '../storage/nodes.ts'\nimport type { NodeConnections } from './machines.ts'\n\n/** How many times one machine is attempted before the startup pass gives up on it. */\nconst ATTEMPTS = 3\n\n/** Gap between those attempts, in milliseconds. */\nconst RETRY_GAP_MS = 2_000\n\n/** What one pass needs. */\nexport interface AutoconnectDeps {\n /** Every configured machine, read once per pass. */\n readonly records: () => readonly NodeRecord[]\n /** The connection manager that owns the attempts. */\n readonly connections: Pick<NodeConnections, 'connect'>\n /**\n * One machine's current state, so a follow-up pass revisits only the failures\n * a person did not ask to end. Omitted treats every machine as retryable.\n */\n readonly status?: (nodeId: NodeId) => { readonly state: string }\n /** Attempts per machine per pass; defaults to {@link ATTEMPTS}. */\n readonly attempts?: number\n /** Gap between those attempts; defaults to {@link RETRY_GAP_MS}. */\n readonly gapMs?: number\n /**\n * Gap between follow-up passes over the machines still failed. Omitted runs\n * the startup pass once and leaves the failures for a person.\n */\n readonly refreshMs?: number\n /** Sleeps between attempts; injectable so tests do not wait. */\n readonly delay?: (ms: number) => Promise<void>\n}\n\n/**\n * Start the connection passes over every configured machine.\n * @param deps - the machines, the connection manager, and the retry knobs.\n * @returns a function that stops the passes; an attempt already in flight is\n * left to finish or fail on its own, and no further attempt starts.\n */\nexport function autoconnect(deps: AutoconnectDeps): () => void {\n const attempts = deps.attempts ?? ATTEMPTS\n const gapMs = deps.gapMs ?? RETRY_GAP_MS\n const delay = deps.delay ?? ((ms: number): Promise<void> =>\n new Promise(resolve => { setTimeout(resolve, ms) }))\n let stopped = false\n\n /** Attempt one machine until it connects, the attempts run out, or this stops. */\n const pass = async (record: NodeRecord): Promise<void> => {\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n if (stopped) return\n if (attempt > 0) await delay(gapMs)\n if (stopped) return\n try {\n await deps.connections.connect(record)\n return\n } catch {\n // The attempt recorded its own failure on the machine's status; the\n // only thing left to try is the next attempt.\n }\n }\n }\n\n /** One machine's part of a pass; the local host has nothing to reach. */\n const consider = (record: NodeRecord, retryOnlyFailed: boolean): void => {\n if (record.transport.kind === 'local') return\n // A follow-up pass exists for a transient link failure, not for a machine a\n // person disconnected; `failed` is the only state it revisits.\n if (retryOnlyFailed && deps.status !== undefined && deps.status(record.nodeId).state !== 'failed') return\n void pass(record)\n }\n\n for (const record of deps.records()) consider(record, false)\n\n const timer = deps.refreshMs === undefined\n ? undefined\n : setInterval(() => {\n if (stopped) return\n for (const record of deps.records()) consider(record, true)\n }, deps.refreshMs)\n // A pending retry must never be what keeps the process alive.\n timer?.unref()\n\n return () => {\n stopped = true\n if (timer !== undefined) clearInterval(timer)\n }\n}\n","/**\n * The wire contract between this plugin and the `dsh-remote-agent` daemon\n * running on a remote machine.\n *\n * This is the plugin's half of the contract, and the half its own code is\n * typed against. The daemon is a Rust program with its own copy of the same\n * shapes (`agent/src/protocol.rs` and `agent/src/wire.rs`); the two are\n * maintained by hand, so a change here is a change there. What holds them\n * together is behavioural rather than structural: `tests/e2e/protocol.test.ts`\n * calls every method below against the shipped binary and fails when one of\n * them moves.\n *\n * Transport is JSON-RPC 2.0 over a byte stream framed with a `Content-Length`\n * header block. Binary payloads travel base64-encoded; every other field is\n * plain JSON.\n *\n * **Target identity is the canonical absolute path.** Every mutating and\n * reading method addresses a file by the `canonicalPath` a previous call\n * returned, which is realpath-normalized by the daemon. The plugin composes its\n * own opaque key from the node id plus that path, so containment, process\n * paths, and file URLs are derived locally without another round trip.\n *\n * @module dsh-remote-workspace/remote/protocol\n */\n\n/**\n * Protocol revision. A breaking change to any method name, parameter, or\n * result bumps this, and the daemon refuses a mismatched handshake instead of\n * degrading.\n */\nexport const PROTOCOL_VERSION = 1\n\n/** Prefix every composite target key the plugin mints carries. */\nexport const TARGET_KEY_PREFIX = 'node:'\n\n/** A byte payload, base64-encoded so it survives a JSON text line unchanged. */\nexport interface WireBytes {\n readonly data: string\n}\n\n/** Whether a target is a regular file, a directory, or something else. */\nexport type WireFileType = 'file' | 'directory' | 'other'\n\n/** Whether a path entry is a regular file, a directory, a symlink, or something else. */\nexport type WirePathType = 'file' | 'directory' | 'symlink' | 'other'\n\n/** Metadata about a resolved target. `version` is opaque to the plugin. */\nexport interface WireStat {\n readonly version: string\n readonly type: WireFileType\n readonly size?: number\n}\n\n/** Metadata about a path entry without following a final symlink. */\nexport interface WireLstat {\n readonly version: string\n readonly type: WirePathType\n readonly size?: number\n}\n\n/** A path resolved by the daemon into its canonical absolute form. */\nexport interface WireTarget {\n /** Realpath-normalized absolute path in the daemon's filesystem. */\n readonly canonicalPath: string\n}\n\n/** One direct child of a listed directory. */\nexport interface WireDirEntry {\n readonly name: string\n readonly type: WireFileType\n readonly target: WireTarget\n readonly version?: string\n readonly size?: number\n}\n\n/** Guarded write intent; omitted means unconditional create-or-overwrite. */\nexport interface WireWriteIntent {\n readonly kind: 'createIfAbsent' | 'replaceIfVersion'\n /** Present iff `kind` is `replaceIfVersion`. */\n readonly version?: string\n}\n\n/** Outcome of a whole-file write. */\nexport interface WireWriteOutcome {\n readonly operation: 'create' | 'update'\n readonly version: string\n readonly before: string | null\n readonly after: string\n}\n\n/** A literal-replacement edit request. */\nexport interface WireEditRequest {\n readonly oldString: string\n readonly newString: string\n readonly replaceAll: boolean\n}\n\n/** Outcome of a literal edit. */\nexport interface WireEditOutcome {\n readonly version: string\n readonly before: string\n readonly after: string\n}\n\n/**\n * One decoded text window. The daemon owns cross-chunk UTF-8 decoding and\n * binary rejection, so a caller never sees a split code point or a raw byte.\n */\nexport interface WireTextChunk {\n readonly text: string\n /** Whole-file offset to resume from on the next read. */\n readonly nextOffset: number\n /** True when the window reached the end of the file. */\n readonly eof: boolean\n}\n\n/**\n * Stable filesystem failure codes. These mirror the filesystem seam's typed\n * codes so the plugin can rethrow the same code it would have raised locally.\n */\nexport type WireFsErrorCode =\n | 'FS_NOT_FOUND'\n | 'FS_NOT_DIRECTORY'\n | 'FS_NOT_TEXT'\n | 'FS_NOT_REGULAR_FILE'\n | 'FS_TOO_LARGE'\n | 'FS_PERMISSION_DENIED'\n | 'FS_SANDBOX_DENIED'\n | 'FS_IO_ERROR'\n | 'FS_STALE_VERSION'\n | 'FS_NOT_OBSERVED'\n | 'FS_AMBIGUOUS_EDIT'\n | 'FS_EDIT_NOT_FOUND'\n | 'FS_ABORTED'\n\n/**\n * Stable worktree failure codes. A caller distinguishes \"the checkout is\n * already there\" from \"the branch name is taken\" from \"git itself refused\",\n * because each needs a different next move.\n */\nexport type WireGitErrorCode =\n | 'GIT_NOT_A_REPOSITORY'\n | 'GIT_WORKTREE_EXISTS'\n | 'GIT_BRANCH_EXISTS'\n | 'GIT_REF_NOT_FOUND'\n | 'GIT_DIRTY'\n | 'GIT_COMMAND_FAILED'\n\n/**\n * Stable subprocess failure codes. `SP_UNSUPPORTED_STDIO` is the honest answer\n * for a disposition this protocol revision cannot carry, so a caller learns\n * what is missing instead of watching a stream that never produces bytes.\n */\nexport type WireSubprocessErrorCode =\n | 'SP_NOT_FOUND'\n | 'SP_NOT_EXECUTABLE'\n | 'SP_SPAWN_FAILED'\n | 'SP_UNSUPPORTED_STDIO'\n | 'SP_NO_SUCH_PROCESS'\n | 'SP_NO_SUCH_TERMINAL'\n | 'SP_TERMINAL_FAILED'\n\n/** Every code a daemon failure may carry. */\nexport type WireFailureCode = WireFsErrorCode | WireGitErrorCode | WireSubprocessErrorCode\n\n/** The JSON-RPC `error.data` payload every daemon failure carries. */\nexport interface WireErrorData {\n readonly code: WireFailureCode\n /** Human-readable detail; the plugin logs it but does not parse it. */\n readonly message: string\n}\n\n/**\n * Whether a failure code belongs to the filesystem family.\n * @param code - the code a daemon reported.\n * @returns true when the code is one this module declares for filesystem failures.\n */\nexport function isFsErrorCode(code: WireFailureCode): code is WireFsErrorCode {\n return code.startsWith('FS_')\n}\n\n/** Signals the terminal primitive may deliver to a foreground group. */\nexport type WireTerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGKILL' | 'SIGTSTP' | 'SIGHUP'\n\n/** A fully specified terminal allocation. */\nexport interface WireTerminalSpawnSpec {\n /** Executable and arguments; never shell-interpreted by the daemon. */\n readonly argv: readonly string[]\n /** Absolute working directory in the daemon's filesystem. */\n readonly cwd: string\n /** Explicit environment entries layered over the daemon's own scrubbed base. */\n readonly env?: Readonly<Record<string, string>>\n /** Initial terminal row count. */\n readonly rows: number\n /** Initial terminal column count. */\n readonly cols: number\n /** TERM-to-KILL cleanup grace for the complete terminal session. */\n readonly graceMs: number\n}\n\n/** Current foreground process-group facts for one terminal. */\nexport interface WireTerminalForeground {\n /** Foreground process-group id published by the terminal driver. */\n readonly processGroupId: number\n /** Whether the daemon can currently prove that group is waiting on input. */\n readonly inputWaiting: boolean\n}\n\n/** One stdin disposition, mirroring the subprocess seam. */\nexport type WireStdinMode = 'ignore' | 'pipe' | { readonly data: string }\n\n/** Bounded in-memory collection for one output stream. */\nexport interface WireCollect {\n /** In-memory cap in bytes; overflow keeps the tail. */\n readonly maxBytes: number\n}\n\n/**\n * One stdout/stderr disposition. `'pipe'` is a live push stream, carried by\n * {@link SP_PIPE_NOTIFICATION} rather than by the retained window; a consumer\n * that needs raw bytes as they arrive (a language server's protocol decoder)\n * asks for it, and one that wants a bounded tail asks for {@link WireCollect}.\n */\nexport type WireOutputMode = 'inherit' | 'pipe' | WireCollect\n\n/** A fully specified spawn request; this seam applies no defaults. */\nexport interface WireSpawnSpec {\n /** Executable and arguments; never shell-interpreted by the daemon. */\n readonly argv: readonly string[]\n /** Absolute working directory in the daemon's filesystem. */\n readonly cwd: string\n /** stdin disposition. */\n readonly stdin: WireStdinMode\n /** stdout disposition. */\n readonly stdout: WireOutputMode\n /** stderr disposition. */\n readonly stderr: WireOutputMode\n /** Grace the daemon's termination ladder may spend before killing. */\n readonly graceMs: number\n /** Explicit environment entries layered over the daemon's own scrubbed base. */\n readonly env?: Readonly<Record<string, string>>\n}\n\n/**\n * One incremental read of a collected stream.\n *\n * The payload is raw bytes, not decoded text, because a collected reader is\n * addressed by whole-stream **byte** offset: only bytes let a client mirror the\n * daemon's window and answer an offset query exactly.\n */\nexport interface WireOutputRead {\n /** Raw bytes from the requested offset, base64; the retained tail when lossy. */\n readonly data: string\n /** Whole-stream byte offset to resume from. */\n readonly nextOffset: number\n /** True when the requested offset slid out of the in-memory window. */\n readonly lossy: boolean\n}\n\n/** Exit facts of one closed process. */\nexport interface WireOutcome {\n /** Exit code; null when the process died from a signal. */\n readonly exitCode: number | null\n /** Terminating signal name; null on a normal exit. */\n readonly signal: string | null\n}\n\n/**\n * The notification a daemon pushes for every chunk of a `'pipe'` stream.\n *\n * A raw piped stream is not a retained window: the consumer needs every byte in\n * order, so the daemon pushes instead of waiting to be polled. `seq` is\n * monotonic per stream so a client can tell that bytes were lost rather than\n * silently splicing a gap.\n */\nexport const SP_PIPE_NOTIFICATION = 'sp.pipe'\n\n/**\n * Compile-time brand for the ids this contract carries.\n *\n * Declared here rather than imported from `@deepseek-ai/dsh-brand` to keep the\n * wire contract self-contained: the daemon is a Rust program whose hand-written\n * copy of these shapes (`agent/src/protocol.rs`) shares no module with this\n * package. The mechanism is the same one that package uses — a `unique symbol`\n * keyed intersection that only the owning domain can mint.\n */\ndeclare const WIRE_BRAND: unique symbol\n\n/** A spawned process, as the daemon names it. */\nexport type ProcId = string & { readonly [WIRE_BRAND]: 'ProcId' }\n\n/** One terminal session, as the daemon names it. */\nexport type TermId = string & { readonly [WIRE_BRAND]: 'TermId' }\n\n/**\n * Admit a string as a terminal session id.\n * @param value - a string the daemon minted, or one the wire delivered.\n * @returns the same string, branded.\n */\nexport function asTermId(value: string): TermId {\n return value as TermId\n}\n\n/** One pushed chunk of a raw piped stream. */\nexport interface SpPipeFrame {\n /** The process the chunk belongs to. */\n readonly procId: ProcId\n /** Which of the two streams produced it. */\n readonly stream: 'stdout' | 'stderr'\n /** Monotonic per-stream sequence number, starting at 0. */\n readonly seq: number\n /** Raw bytes, base64. */\n readonly data: string\n}\n\n/** What the daemon reports it can do. */\nexport interface NodeCapability {\n /** Whether `spawnTerminal` is served by this build. */\n readonly pty: boolean\n /** Whether collected-output spill files are served by this build. */\n readonly spill: boolean\n /** Remote binary the packaged ripgrep is rewritten to, or null when absent. */\n readonly ripgrep: string | null\n}\n\n/** The daemon's identity, capabilities, and environment. */\nexport interface NodeInfo {\n readonly protocol: number\n readonly agentVersion: string\n readonly platform: string\n readonly arch: string\n readonly node: string\n readonly homedir: string\n readonly capability: NodeCapability\n}\n\n/** First client request on every connection. */\nexport interface HelloRequest {\n readonly protocol: number\n readonly token: string\n}\n\n/** One git worktree as the node reports it. */\nexport interface WireWorktree {\n /** Absolute path of the checkout on the node. */\n readonly path: string\n /** Short branch name, or null when the entry is detached or bare. */\n readonly branch: string | null\n /** Committed revision the checkout points at. */\n readonly head: string\n /** Whether the entry is the repository's main worktree. */\n readonly main: boolean\n}\n\n/** What the repository looked like when an operation ran. */\nexport interface WireRepoState {\n /** Checked-out branch, or null when HEAD is detached. */\n readonly branch: string | null\n /** Whether the index and working tree carry no changes. */\n readonly clean: boolean\n}\n\n/** Result of merging one branch into the repository's current branch. */\nexport interface WireMergeOutcome {\n /** The revision the merge produced. */\n readonly head: string\n /** Whether the merge was already contained and produced no new commit. */\n readonly alreadyMerged: boolean\n}\n\n/** Method names, parameters, and results in one map both sides compile against. */\nexport interface WireMethods {\n 'node.hello': { params: HelloRequest; result: NodeInfo }\n 'fs.resolve': { params: { path: string }; result: WireTarget }\n 'fs.stat': { params: { path: string }; result: WireStat | null }\n 'fs.lstat': { params: { path: string }; result: WireLstat | null }\n 'fs.listDir': { params: { path: string }; result: readonly WireDirEntry[] }\n 'fs.readTextChunk': {\n params: { path: string; offset: number; length: number }\n result: WireTextChunk\n }\n 'fs.readBytes': { params: { path: string; maxBytes: number }; result: WireBytes }\n 'fs.readByteRange': {\n params: { path: string; offset: number; length: number }\n result: WireBytes\n }\n 'fs.writeText': {\n params: { path: string; content: string; expected?: WireWriteIntent }\n result: WireWriteOutcome\n }\n 'fs.editText': {\n params: { path: string; edit: WireEditRequest; expected?: { version: string } }\n result: WireEditOutcome\n }\n 'git.worktreeAdd': {\n params: { repoPath: string; worktreePath: string; branch: string; baseRef?: string }\n result: WireWorktree\n }\n 'git.worktreeList': { params: { repoPath: string }; result: readonly WireWorktree[] }\n 'git.worktreeRemove': {\n params: { repoPath: string; worktreePath: string; force: boolean }\n result: Record<string, never>\n }\n 'git.branchDelete': {\n params: { repoPath: string; branch: string; force: boolean }\n result: Record<string, never>\n }\n 'git.repoState': { params: { repoPath: string }; result: WireRepoState }\n 'git.mergeBranch': { params: { repoPath: string; branch: string }; result: WireMergeOutcome }\n 'sp.resolveExecutable': {\n params: { command: string; env?: Readonly<Record<string, string>> }\n result: { path: string }\n }\n 'sp.spawn': { params: WireSpawnSpec; result: { procId: ProcId } }\n 'sp.readOutput': {\n params: { procId: ProcId; stream: 'stdout' | 'stderr'; fromByte: number }\n result: WireOutputRead\n }\n 'sp.writeStdin': { params: { procId: ProcId; data: string }; result: Record<string, never> }\n 'sp.closeStdin': { params: { procId: ProcId }; result: Record<string, never> }\n 'sp.terminate': { params: { procId: ProcId }; result: Record<string, never> }\n 'sp.waitForExit': { params: { procId: ProcId }; result: Record<string, never> }\n 'sp.outcome': { params: { procId: ProcId }; result: WireOutcome | null }\n 'term.spawn': { params: WireTerminalSpawnSpec; result: { termId: TermId; pid: number } }\n 'term.read': {\n params: { termId: TermId; fromByte: number }\n result: WireOutputRead\n }\n 'term.write': { params: { termId: TermId; data: string }; result: Record<string, never> }\n 'term.resize': {\n params: { termId: TermId; cols: number; rows: number }\n result: Record<string, never>\n }\n 'term.inspectForeground': {\n params: { termId: TermId }\n result: WireTerminalForeground | null\n }\n 'term.signalForeground': {\n params: { termId: TermId; signal: WireTerminalSignal }\n result: { processGroupId: number }\n }\n 'term.terminate': { params: { termId: TermId }; result: Record<string, never> }\n 'term.outcome': { params: { termId: TermId }; result: WireOutcome | null }\n}\n\n/** Every method this protocol revision defines. */\nexport type WireMethod = keyof WireMethods\n\n/** Parameters of one method. */\nexport type WireParams<M extends WireMethod> = WireMethods[M]['params']\n\n/** Result of one method. */\nexport type WireResult<M extends WireMethod> = WireMethods[M]['result']\n","/**\n * The node channel: what the rest of the plugin may ask one node, and the TCP\n * client that answers it.\n *\n * {@link NodeChannel} is the narrow view every consumer holds — round-trip one\n * protocol method, watch the pushed frames of a piped stream — so the file\n * layer, the process layer, and the worktree lifecycle never see a socket,\n * a handshake, or `vscode-jsonrpc`. {@link NodeRequestError} is the other half\n * of that view: a daemon failure with the code the call sites branch on.\n *\n * The client below is the only implementation: `vscode-jsonrpc` owns framing,\n * request correlation, and cancellation on top of the socket, so it only\n * establishes the connection, performs the handshake, and translates a daemon\n * failure into that error. What supplies the socket — an SSH forward today, the\n * recorded address for a `direct` record — is the caller's business, which is\n * what lets one client serve both.\n *\n * @module dsh-remote-workspace/remote/client\n */\n\nimport { Socket } from 'node:net'\n// The `.js` suffix is required: this package ships no `exports` map, so an\n// extensionless subpath is not resolvable from ESM even though the file is.\nimport { ResponseError, StreamMessageReader, StreamMessageWriter, createMessageConnection } from 'vscode-jsonrpc/node.js'\nimport type { NodeInfo, SpPipeFrame, WireErrorData, WireMethod, WireParams, WireResult } from './protocol.ts'\nimport { PROTOCOL_VERSION, SP_PIPE_NOTIFICATION } from './protocol.ts'\nimport type { NodeId } from '../storage/nodes.ts'\n\n/** One live connection to a node's daemon. */\nexport interface NodeChannel {\n /**\n * Round-trip one protocol method.\n * @param method - the wire method name.\n * @param params - that method's parameters.\n * @returns the method result.\n * @throws NodeRequestError when the daemon answers with a typed wire failure,\n * and a transport error when the connection drops.\n */\n request<M extends WireMethod>(method: M, params: WireParams<M>): Promise<WireResult<M>>\n /**\n * Observe the raw chunks the daemon pushes for `'pipe'` streams.\n *\n * A raw piped stream is pushed, not retained, so a consumer that misses a\n * frame has lost those bytes; a caller registers before it spawns the process\n * it cares about. One handler is active at a time, matching the connection's\n * own single notification slot.\n * @param handler - invoked per pushed chunk.\n * @returns a disposer that removes the handler.\n */\n onPipeFrame(handler: (frame: SpPipeFrame) => void): () => void\n}\n\n/** A typed failure the daemon reported, carrying the seam's own error code. */\nexport class NodeRequestError extends Error {\n /** The daemon's structured payload, verbatim. */\n readonly data: WireErrorData\n\n /**\n * @param data - the daemon's structured error payload.\n */\n constructor(data: WireErrorData) {\n super(`${data.message} (${data.code})`)\n this.name = 'NodeRequestError'\n this.data = data\n }\n}\n\n/** Resolves the live channel for one node, or undefined when it is not connected. */\nexport type ChannelLookup = (nodeId: NodeId) => NodeChannel | undefined\n\n/** How to reach one daemon. */\nexport interface ConnectOptions {\n /** Host or IP the daemon listens on. */\n readonly host: string\n /** TCP port the daemon listens on. */\n readonly port: number\n /** Shared secret from the daemon's token file. */\n readonly token: string\n /** Bound on connection establishment and the handshake, in milliseconds. */\n readonly timeoutMs?: number\n}\n\n/** A live, handshaken connection. */\nexport interface ConnectedNode {\n /** The channel the routers call. */\n readonly channel: NodeChannel\n /** What the daemon reported about itself. */\n readonly info: NodeInfo\n /** Close the connection and release the socket. Idempotent. */\n close(): void\n}\n\n/** Whether an unknown value is the daemon's structured failure payload. */\nfunction isWireErrorData(value: unknown): value is WireErrorData {\n return typeof value === 'object' && value !== null\n && typeof (value as { code?: unknown }).code === 'string'\n && typeof (value as { message?: unknown }).message === 'string'\n}\n\n/**\n * Translate a `vscode-jsonrpc` rejection into the plugin's typed failure.\n * @param error - whatever the connection raised.\n * @returns the error to reject with.\n */\nfunction toChannelError(error: unknown): unknown {\n if (error instanceof ResponseError && isWireErrorData(error.data)) {\n return new NodeRequestError(error.data)\n }\n return error\n}\n\n/**\n * Settle `work` or fail once the deadline passes.\n *\n * The handshake needs its own bound: a daemon that closes the socket without\n * answering leaves the request pending forever, and \"the machine answered\n * nothing\" must be a failure rather than a hang.\n * @param work - the operation to bound.\n * @param timeoutMs - the deadline, or undefined to wait indefinitely.\n * @param onTimeout - builds the failure to reject with.\n * @returns the operation's result.\n */\nasync function withTimeout<T>(\n work: Promise<T>,\n timeoutMs: number | undefined,\n onTimeout: () => Error,\n): Promise<T> {\n if (timeoutMs === undefined) return work\n let timer: NodeJS.Timeout | undefined\n try {\n return await Promise.race([\n work,\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(onTimeout()), timeoutMs)\n }),\n ])\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n}\n\n/**\n * A writer that hands a failed write to the connection instead of rejecting.\n *\n * `vscode-jsonrpc` reports a failed write by rejecting the request from inside\n * an `async` Promise executor, and nothing awaits that rejection. The host\n * process sees the orphan and reports a fatal load failure, which is what a\n * write to a socket destroyed underneath it — a teardown racing an in-flight\n * call, or a link that just dropped — produces. The failure is not dropped:\n * the connection is disposed, which rejects the request through the\n * pending-response path its caller already handles.\n */\nexport class SocketWriter extends StreamMessageWriter {\n /** Disposes the connection this writer serves; set once that connection exists. */\n private readonly onWriteFailure: () => void\n\n /**\n * @param socket - the socket the connection is written to.\n * @param onWriteFailure - tears down the owning connection.\n */\n constructor(socket: Socket, onWriteFailure: () => void) {\n super(socket)\n this.onWriteFailure = onWriteFailure\n }\n\n override async write(message: Parameters<StreamMessageWriter['write']>[0]): Promise<void> {\n try {\n await super.write(message)\n } catch {\n this.onWriteFailure()\n }\n }\n}\n\n/**\n * Connect to a daemon and complete the handshake.\n * @param options - address, token, and optional timeout.\n * @returns the live connection, already past `node.hello`.\n * @throws when the socket fails, the handshake times out, or the daemon\n * refuses the protocol revision or the token.\n */\nexport async function connectNode(options: ConnectOptions): Promise<ConnectedNode> {\n const socket = new Socket()\n socket.setNoDelay(true)\n\n await withTimeout(\n new Promise<void>((resolve, reject) => {\n socket.once('error', reject)\n socket.once('connect', () => resolve())\n socket.connect({ host: options.host, port: options.port })\n }),\n options.timeoutMs,\n () => {\n socket.destroy()\n return new Error(`timed out connecting to ${options.host}:${String(options.port)}`)\n },\n )\n\n let disposeConnection: () => void = () => {}\n const writer = new SocketWriter(socket, () => { disposeConnection() })\n const connection = createMessageConnection(new StreamMessageReader(socket), writer)\n disposeConnection = () => { connection.dispose() }\n connection.listen()\n\n const channel: NodeChannel = {\n async request<M extends WireMethod>(method: M, params: WireParams<M>): Promise<WireResult<M>> {\n try {\n return await connection.sendRequest<WireResult<M>>(method, params)\n } catch (error) {\n throw toChannelError(error)\n }\n },\n onPipeFrame(handler) {\n // `vscode-jsonrpc` keeps one notification handler per method, which is\n // exactly the lifetime this seam wants: the caller owns registration and\n // disposes it when its process is gone.\n connection.onNotification(SP_PIPE_NOTIFICATION, (frame: SpPipeFrame) => {\n handler(frame)\n })\n return () => {\n connection.onNotification(SP_PIPE_NOTIFICATION, () => {})\n }\n },\n }\n\n let info: NodeInfo\n try {\n info = await withTimeout(\n channel.request('node.hello', { protocol: PROTOCOL_VERSION, token: options.token }),\n options.timeoutMs,\n () => new Error(`handshake with ${options.host}:${String(options.port)} timed out`),\n )\n } catch (error) {\n connection.dispose()\n socket.destroy()\n throw toChannelError(error)\n }\n\n let closed = false\n return {\n channel,\n info,\n close() {\n if (closed) return\n closed = true\n connection.dispose()\n socket.destroy()\n },\n }\n}\n","/**\n * Reaching one machine over `ssh`: the one-shot commands that install and start\n * its daemon, and the port forward that then carries daemon traffic.\n *\n * Both jobs share the same two decisions, so both live here: the options that\n * keep `ssh` from ever prompting, and the translation of what it wrote on\n * failure into a remedy. A change to either reaches every caller.\n *\n * The two jobs report differently because their processes differ. A command\n * runs to completion and hands back its exit status and both streams, so\n * {@link runSsh} resolves for any normal exit, non-zero included — a command\n * that reports \"no\" is a result the caller inspects, not an exception — and\n * rejects only when `ssh` could not be started or the caller's deadline passed.\n * A forward is a long-lived process that either starts accepting connections or\n * does not, so {@link openTunnel} resolves once the local port is live and\n * never reports an exit status.\n *\n * The daemon binds the machine's own loopback, so the host reaches it the way a\n * person would by hand: a local port, a `ssh -L` forward, and a connection\n * through it. That port is chosen per connection from whatever the host has\n * free, because a fixed one would collide with whatever else the operator runs\n * and is not part of a machine's identity. The host never accepts a host key on\n * the operator's behalf: an unknown key fails the forward with a diagnostic\n * naming the command that would accept it, because this forward grants shell\n * access as the remote user.\n *\n * @module dsh-remote-workspace/remote/ssh\n */\n\nimport { spawn } from 'node:child_process'\nimport { connect, createServer } from 'node:net'\n\n/** How to reach a machine's SSH server. */\nexport interface SshTarget {\n /** `ssh` destination: `user@host`, or a `~/.ssh/config` alias. */\n readonly target: string\n /** SSH port; omitted defers to the operator's `ssh` configuration. */\n readonly sshPort?: number\n /** Identity file; omitted defers to the operator's `ssh` configuration. */\n readonly identityFile?: string\n}\n\n/** What one remote command produced. */\nexport interface SshCommandResult {\n /** Exit status; `0` means the command succeeded. */\n readonly code: number\n /** Everything the command wrote to stdout, UTF-8 decoded. */\n readonly stdout: string\n /** Everything `ssh` and the command wrote to stderr, UTF-8 decoded. */\n readonly stderr: string\n}\n\n/** Knobs one command run reads. */\nexport interface SshRunOptions {\n /** Bytes to write to the remote command's stdin; the stream then closes. */\n readonly input?: Buffer | string\n /** Bound on the whole run, in milliseconds; omitted waits indefinitely. */\n readonly timeoutMs?: number\n}\n\n/** The slice of a spawned `ssh` process one command run drives. */\nexport interface SshProcess {\n /** Resolves with the exit code, or rejects when `ssh` could not start. */\n readonly exited: Promise<number>\n /** Everything written to stdout; complete once `exited` settles. */\n readStdout(): string\n /** Everything written to stderr; complete once `exited` settles. */\n readStderr(): string\n /** Write the input to stdin and close it; no input just closes it. */\n send(input: Buffer | string | undefined): void\n /** Terminate the process. */\n kill(): void\n}\n\n/** Starts one `ssh` process; injectable so tests need no binary. */\nexport type StartSsh = (args: readonly string[]) => SshProcess\n\n/** Overrides {@link runSsh} accepts for its process handling. */\nexport interface SshRunDeps {\n /** Starts `ssh`; defaults to the real process. */\n readonly start?: StartSsh\n}\n\n/**\n * Build the `ssh` options that make a run non-interactive against this target.\n *\n * The SSH port and identity file default to the operator's own configuration,\n * so a `~/.ssh/config` alias reaches the machine exactly as `ssh` itself would.\n * @param target - the machine to reach.\n * @returns the arguments, excluding the subcommand and the destination.\n */\nexport function sshArgs(target: SshTarget): readonly string[] {\n return [\n // No terminal is attached, so an authentication or host-key prompt would\n // hang until a caller's deadline expired. Fail immediately instead, and\n // let the diagnostic name what the operator must do.\n '-o', 'BatchMode=yes',\n // Without this a forward that cannot bind leaves an ssh process running\n // that forwards nothing, which reads as a healthy connection.\n '-o', 'ExitOnForwardFailure=yes',\n // A forward has to die when the link does: the connection manager watches\n // this process to publish the loss, and a half-open link would otherwise\n // leave it running for hours, forwarding nothing.\n '-o', 'ServerAliveInterval=15',\n '-o', 'ServerAliveCountMax=3',\n ...target.sshPort === undefined ? [] : ['-p', String(target.sshPort)],\n ...target.identityFile === undefined ? [] : ['-i', target.identityFile],\n ]\n}\n\n/**\n * Turn what `ssh` wrote into a reason an operator can act on.\n *\n * The raw text stays in the message: this maps only the failures with a known\n * remedy, and everything else is more useful verbatim than paraphrased.\n * @param target - the destination the run named.\n * @param stderr - everything the process wrote to stderr.\n * @param fallback - what to say when nothing matches, without the raw text.\n * @returns the message to raise.\n */\nexport function sshFailure(target: string, stderr: string, fallback: string): string {\n const text = stderr.trim()\n const suffix = text === '' ? '' : `: ${text}`\n if (/host key verification failed/i.test(text)) {\n return `the SSH host key for \"${target}\" is not known yet; run \\`ssh ${target}\\` once to verify and accept it${suffix}`\n }\n if (/administratively prohibited/i.test(text)) {\n return `\"${target}\" refuses TCP forwarding; its sshd needs AllowTcpForwarding yes${suffix}`\n }\n if (/permission denied|no supported authentication/i.test(text)) {\n return `\"${target}\" rejected the key or agent; check the SSH key and ssh-agent${suffix}`\n }\n if (/could not resolve hostname/i.test(text)) {\n return `\"${target}\" cannot be resolved; check the SSH destination${suffix}`\n }\n if (/connection refused|connection timed out|no route to host/i.test(text)) {\n return `\"${target}\" is unreachable over SSH${suffix}`\n }\n return `${fallback}${suffix}`\n}\n\n/** Start the real `ssh` process for one command run. */\nfunction startSshCommand(args: readonly string[]): SshProcess {\n const child = spawn('ssh', [...args], { stdio: ['pipe', 'pipe', 'pipe'] })\n let stdout = ''\n let stderr = ''\n child.stdout.setEncoding('utf8')\n child.stderr.setEncoding('utf8')\n child.stdout.on('data', (chunk: string) => { stdout += chunk })\n child.stderr.on('data', (chunk: string) => { stderr += chunk })\n // A command that never reads stdin closes the pipe early; that is normal and\n // must not surface as an unhandled stream error.\n child.stdin.on('error', () => {})\n return {\n exited: new Promise<number>((resolve, reject) => {\n child.once('error', reject)\n child.once('close', (code) => { resolve(code ?? 0) })\n }),\n readStdout: () => stdout,\n readStderr: () => stderr,\n send: (input) => { child.stdin.end(input) },\n kill: () => { child.kill('SIGTERM') },\n }\n}\n\n/**\n * Run one command on a machine over SSH.\n *\n * Resolves with the exit status, stdout, and stderr for any exit the process\n * reached on its own, a non-zero one included. Rejects only when `ssh` could\n * not be started or `options.timeoutMs` elapsed; in the first case the\n * diagnostic says so, and in the second it carries whatever stderr arrived.\n * @param ssh - the machine to reach.\n * @param command - the command string the remote shell runs.\n * @param options - optional stdin bytes and a deadline.\n * @param deps - an optional process starter, for tests.\n * @returns the exit status and both streams.\n * @throws when `ssh` cannot start or the deadline passes.\n */\nexport async function runSsh(\n ssh: SshTarget,\n command: string,\n options: SshRunOptions = {},\n deps: SshRunDeps = {},\n): Promise<SshCommandResult> {\n const start = deps.start ?? startSshCommand\n const child = start([...sshArgs(ssh), ssh.target, command])\n child.send(options.input)\n\n const timeoutMs = options.timeoutMs\n let timer: NodeJS.Timeout | undefined\n let timedOut = false\n const deadline = timeoutMs === undefined\n ? undefined\n : new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n timedOut = true\n reject(new Error('timeout'))\n }, timeoutMs)\n })\n\n try {\n const code = await (deadline === undefined ? child.exited : Promise.race([child.exited, deadline]))\n return { code, stdout: child.readStdout(), stderr: child.readStderr() }\n } catch (error) {\n child.kill()\n if (timedOut) {\n throw new Error(\n `the SSH command on \"${ssh.target}\" did not finish within ${String(timeoutMs)}ms`,\n { cause: error },\n )\n }\n throw new Error(\n `could not start ssh for \"${ssh.target}\": ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n )\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n}\n\n/** One forward to open. */\nexport interface TunnelSpec {\n /** The machine to reach. */\n readonly ssh: SshTarget\n /** Port the daemon listens on, on the machine's own loopback. */\n readonly remotePort: number\n}\n\n/** A live forward. */\nexport interface Tunnel {\n /** The local port that reaches the machine's daemon. */\n readonly localPort: number\n /** Resolves when the `ssh` process exits, for any reason. */\n readonly exited: Promise<void>\n /** Stop forwarding and reap the process. Idempotent. */\n close(): void\n}\n\n/** Knobs a caller may override; tests use them to avoid real processes. */\nexport interface TunnelDeps {\n /** Binds a free local port. */\n readonly allocatePort?: () => Promise<number>\n /** Starts the forward. */\n readonly start?: (args: readonly string[]) => TunnelProcess\n /** How long the forward may take to accept a connection. */\n readonly readyTimeoutMs?: number\n /** How long to wait between readiness probes. */\n readonly readyPollMs?: number\n}\n\n/** The slice of a spawned process this module drives. */\nexport interface TunnelProcess {\n /** Resolves once the process has exited. */\n readonly exited: Promise<void>\n /** Everything the process wrote to stderr so far. */\n readonly diagnostics: () => string\n /** Terminate the process. */\n kill(): void\n}\n\n/**\n * Default budget for a forward to start accepting connections. The plugin\n * exposes this as `Config.sshForwardTimeoutMs`; it is the fallback for a caller\n * that composes the tunnel directly.\n */\nexport const DEFAULT_FORWARD_TIMEOUT_MS = 15_000\n\n/** Default gap between readiness probes. */\nconst READY_POLL_MS = 120\n\n/**\n * Resolve a free TCP port on the host.\n *\n * The port is released before it is returned, so a caller racing for it can\n * lose; a forward that loses reports a bind failure rather than silently\n * forwarding nothing.\n * @returns the port number the host had free.\n */\nexport function allocateLocalPort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const probe = createServer()\n probe.once('error', reject)\n probe.listen(0, '127.0.0.1', () => {\n const address = probe.address()\n if (address === null || typeof address === 'string') {\n probe.close()\n reject(new Error('could not determine a free local port'))\n return\n }\n const { port } = address\n probe.close(() => resolve(port))\n })\n })\n}\n\n/**\n * Build the `ssh` argument vector for one forward.\n * @param spec - the machine and the daemon port to reach.\n * @param localPort - the host port to forward.\n * @returns the arguments, excluding the executable.\n */\nexport function tunnelArgs(spec: TunnelSpec, localPort: number): readonly string[] {\n return [\n '-N',\n ...sshArgs(spec.ssh),\n '-L', `127.0.0.1:${String(localPort)}:127.0.0.1:${String(spec.remotePort)}`,\n spec.ssh.target,\n ]\n}\n\n/**\n * Turn what `ssh` wrote into a reason an operator can act on.\n * @param target - the destination the forward named.\n * @param stderr - everything the process wrote to stderr.\n * @returns the message to raise.\n */\nexport function tunnelFailure(target: string, stderr: string): string {\n return sshFailure(target, stderr, `could not open an SSH forward to \"${target}\"`)\n}\n\n/** Start the real `ssh` process for one forward. */\nfunction startSshForward(args: readonly string[]): TunnelProcess {\n const child = spawn('ssh', [...args], { stdio: ['ignore', 'ignore', 'pipe'] })\n let stderr = ''\n child.stderr.setEncoding('utf8')\n child.stderr.on('data', (chunk: string) => { stderr += chunk })\n return {\n exited: new Promise<void>((resolve) => {\n child.once('error', () => { resolve() })\n child.once('exit', () => { resolve() })\n }),\n diagnostics: () => stderr,\n kill: () => { child.kill('SIGTERM') },\n }\n}\n\n/** Probe one TCP port on the host's loopback. */\nfunction probePort(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = connect({ host: '127.0.0.1', port })\n const settle = (open: boolean): void => {\n socket.removeAllListeners()\n socket.destroy()\n resolve(open)\n }\n socket.once('connect', () => { settle(true) })\n socket.once('error', () => { settle(false) })\n })\n}\n\n/** Wait until a port accepts a connection, the process dies, or time runs out. */\nasync function waitForForward(\n target: string,\n port: number,\n process: TunnelProcess,\n timeoutMs: number,\n pollMs: number,\n): Promise<void> {\n const deadline = Date.now() + timeoutMs\n let exited = false\n void process.exited.then(() => { exited = true })\n for (;;) {\n if (await probePort(port)) return\n if (exited) throw new Error(tunnelFailure(target, process.diagnostics()))\n if (Date.now() >= deadline) {\n throw new Error(\n `the SSH forward to \"${target}\" did not start accepting connections within ${String(timeoutMs)}ms`,\n )\n }\n await new Promise(resolve => setTimeout(resolve, pollMs))\n }\n}\n\n/**\n * Open a forward from a free host port to one machine's daemon.\n *\n * Resolves once the forward accepts connections, so a caller that receives a\n * tunnel can connect through it immediately. Rejects — after killing the\n * process — when `ssh` refuses, exits, or never becomes ready.\n * @param spec - the machine and the daemon port to reach.\n * @param deps - overrides for tests.\n * @returns the live forward.\n * @throws when the forward could not be established.\n */\nexport async function openTunnel(spec: TunnelSpec, deps: TunnelDeps = {}): Promise<Tunnel> {\n const allocatePort = deps.allocatePort ?? allocateLocalPort\n const start = deps.start ?? startSshForward\n const localPort = await allocatePort()\n const process = start(tunnelArgs(spec, localPort))\n let closed = false\n const close = (): void => {\n if (closed) return\n closed = true\n process.kill()\n }\n try {\n await waitForForward(\n spec.ssh.target,\n localPort,\n process,\n deps.readyTimeoutMs ?? DEFAULT_FORWARD_TIMEOUT_MS,\n deps.readyPollMs ?? READY_POLL_MS,\n )\n } catch (error) {\n close()\n // A forward that died says why; a forward that never bound says what it\n // wrote, which is the only clue to which option the operator must change.\n const stderr = process.diagnostics()\n if (stderr !== '' && error instanceof Error) {\n throw new Error(tunnelFailure(spec.ssh.target, stderr), { cause: error })\n }\n throw error\n }\n return { localPort, exited: process.exited, close }\n}\n","/**\n * Resolve the agent binary for one machine's platform.\n *\n * The agent is a static Rust binary published on GitHub Releases inside a\n * per-platform `.tar.gz`, so \"install the agent\" reduces to naming the right\n * archive for `uname` and caching the binary it holds. The cache is keyed by\n * version and asset, which is what makes a version bump a fresh download and a\n * second machine of the same platform a cache hit.\n *\n * The archive is read from the release's own download address — the URL a\n * browser would follow, built from the tag and the asset name — so nothing here\n * calls the GitHub API: no release metadata, no asset listing, no media type to\n * negotiate, and no anonymous rate limit to spend. A release that carries no\n * such archive answers 404 and is reported with the address that failed.\n *\n * Every download is verified against the release's `SHA256SUMS` before it is\n * unpacked and cached: the bytes are executed on a remote machine, so a\n * truncated or substituted archive must fail here rather than at exec time\n * there.\n *\n * @module dsh-remote-workspace/remote/agent/release\n */\n\nimport { createHash, randomUUID } from 'node:crypto'\nimport { gunzipSync } from 'node:zlib'\nimport { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\n/** Repository whose releases carry the agent binaries. */\nconst RELEASE_REPOSITORY = 'lengmoXXL/dsh-remote-workspace'\n\n/** Address one release file is downloaded from, by tag and file name. */\nconst DOWNLOAD_ROOT = `https://github.com/${RELEASE_REPOSITORY}/releases/download`\n\n/** The regular file every release archive carries: the agent itself. */\nconst AGENT_MEMBER = 'dsh-remote-agent'\n\n/** The sums file every release carries beside its archives. */\nconst SUMS_FILE = 'SHA256SUMS'\n\n/** Bytes one tar header block holds. */\nconst TAR_BLOCK = 512\n\n/** Platform names `uname -s` reports, and the asset token each maps to. */\nconst PLATFORMS: Readonly<Record<string, string>> = {\n linux: 'linux',\n darwin: 'darwin',\n}\n\n/** Architecture names `uname -m` reports, and the asset token each maps to. */\nconst ARCHITECTURES: Readonly<Record<string, string>> = {\n x86_64: 'x86_64',\n amd64: 'x86_64',\n aarch64: 'aarch64',\n arm64: 'aarch64',\n}\n\n/** Downloads one URL; injectable so tests need no network. */\nexport type AgentFetcher = (url: string) => Promise<Buffer>\n\n/** Options {@link resolveAgentBinary} reads. */\nexport interface AgentBinaryOptions {\n /** Agent build to fetch, e.g. `0.0.2`. */\n readonly version: string\n /** Release asset for the machine's platform, from {@link agentAssetName}. */\n readonly assetName: string\n /** Host directory the binary cache lives under. */\n readonly cacheDir: string\n /** Downloads one URL; injectable so tests need no network. */\n readonly fetch?: AgentFetcher\n /**\n * Reports where the bytes come from, before any network read.\n *\n * A cache hit and a download look identical from the outside and take very\n * different amounts of time, so a caller that shows progress needs to tell\n * them apart.\n */\n readonly onSource?: (source: 'cache' | 'network') => void\n}\n\n/**\n * Name the release asset for a machine's reported platform.\n *\n * Matching is case-insensitive because `uname` casing is not portable, and\n * both the GNU and the BSD spelling of each architecture is accepted so the\n * plugin never depends on which userland `uname` came from.\n * @param platform - `uname -s` output, e.g. `Linux`.\n * @param arch - `uname -m` output, e.g. `x86_64`.\n * @returns the release asset name.\n * @throws when the machine reports a platform or architecture with no asset.\n */\nexport function agentAssetName(platform: string, arch: string): string {\n const os = PLATFORMS[platform.trim().toLowerCase()]\n const cpu = ARCHITECTURES[arch.trim().toLowerCase()]\n if (os === undefined || cpu === undefined) {\n throw new Error(\n `the machine reports platform \"${platform.trim()}\" and architecture \"${arch.trim()}\", `\n + 'which has no dsh-remote-agent release; it ships for Linux and Darwin on x86_64 and aarch64',\n )\n }\n return `dsh-remote-agent-${os}-${cpu}`\n}\n\n/** The release file one asset is published as. */\nfunction archiveName(assetName: string): string {\n return `${assetName}.tar.gz`\n}\n\n/** The direct download address of one platform's archive. */\nexport function agentArchiveUrl(version: string, assetName: string): string {\n return `${DOWNLOAD_ROOT}/v${version}/${archiveName(assetName)}`\n}\n\n/** The direct download address of one version's sums file. */\nexport function agentSumsUrl(version: string): string {\n return `${DOWNLOAD_ROOT}/v${version}/${SUMS_FILE}`\n}\n\n/** Download one URL, or fail with a message that names it. */\nasync function download(fetcher: AgentFetcher, url: string): Promise<Buffer> {\n try {\n return await fetcher(url)\n } catch (error) {\n throw new Error(\n `downloading ${url} failed: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n )\n }\n}\n\n/** The real HTTPS GET, refusing any non-2xx answer. */\nasync function fetchOverHttps(url: string): Promise<Buffer> {\n const response = await fetch(url)\n if (!response.ok) throw new Error(`HTTP ${String(response.status)}`)\n return Buffer.from(await response.arrayBuffer())\n}\n\n/** One tar header field, read to its NUL or its field boundary and trimmed. */\nfunction headerText(header: Buffer, start: number, length: number): string {\n const nul = header.indexOf(0, start)\n const end = nul === -1 || nul > start + length ? start + length : nul\n return header.toString('utf8', start, end).trim()\n}\n\n/** The octal byte count of one tar header's data. */\nfunction headerSize(header: Buffer): number {\n const text = headerText(header, 124, 12)\n return text === '' ? 0 : Number.parseInt(text, 8)\n}\n\n/**\n * The agent binary inside one release archive.\n *\n * Only what this repository's own release job writes needs to be understood:\n * one regular file, named {@link AGENT_MEMBER}, packed by `tar -czf`. A tar may\n * write metadata records ahead of it — a PAX header from a newer tar, a long\n * name — so every record is stepped over by its own size until the file is\n * found, rather than assuming it comes first.\n * @param archive - the `.tar.gz` bytes.\n * @param sourceUrl - the URL they came from, for the diagnostic.\n * @returns the member's bytes.\n * @throws when the archive carries no such member or is not a gzipped tar.\n */\nfunction archiveMember(archive: Buffer, sourceUrl: string): Buffer {\n let tar: Buffer\n try {\n tar = gunzipSync(archive)\n } catch (error) {\n // A proxy or a captive portal can answer a download with a readable page\n // instead of the archive; saying so beats a raw decompressor error.\n throw new Error(`${sourceUrl} is not a gzipped tar archive`, { cause: error })\n }\n for (let offset = 0; offset + TAR_BLOCK <= tar.length;) {\n const header = tar.subarray(offset, offset + TAR_BLOCK)\n // Two zero blocks mark the end of the archive.\n if (header.every(byte => byte === 0)) break\n const size = headerSize(header)\n const start = offset + TAR_BLOCK\n const type = String.fromCharCode(header[156] ?? 0)\n const name = headerText(header, 0, 100)\n // NUL and '0' are the regular-file records; every other type is metadata.\n if ((type === '0' || type === '\\0') && name.split('/').pop() === AGENT_MEMBER) {\n return tar.subarray(start, start + size)\n }\n offset = start + Math.ceil(size / TAR_BLOCK) * TAR_BLOCK\n }\n throw new Error(`${sourceUrl} carries no \"${AGENT_MEMBER}\"`)\n}\n\n/**\n * Read the expected hash for one release file out of a `SHA256SUMS` body.\n * @param sums - the decoded sums file.\n * @param fileName - the release file to look up.\n * @param sumsUrl - the URL the sums came from, for the diagnostic.\n * @returns the expected lowercase hex digest.\n * @throws when the sums file names no such file.\n */\nfunction expectedChecksum(sums: string, fileName: string, sumsUrl: string): string {\n for (const line of sums.split('\\n')) {\n // `<hex>␠␠<name>`, the format this repository's own release job writes.\n const match = /^([0-9a-f]{64})\\s+(.+)$/i.exec(line.trim())\n if (match !== null && match[2]?.trim() === fileName) return match[1]!.toLowerCase()\n }\n throw new Error(`${sumsUrl} names no \"${fileName}\"`)\n}\n\n/**\n * Fetch, verify, unpack, and cache one agent binary.\n *\n * A cached file is returned untouched: it was verified when it was written,\n * and re-hashing every connect would spend a slow link's budget on a file the\n * plugin itself produced.\n * @param options - version, asset, cache directory, and an optional fetch.\n * @returns the verified binary bytes.\n * @throws when the archive cannot be read, fails its checksum, carries no\n * agent, or the cache cannot be written.\n */\nexport async function resolveAgentBinary(options: AgentBinaryOptions): Promise<Buffer> {\n const fetcher = options.fetch ?? fetchOverHttps\n const cached = join(options.cacheDir, options.version, options.assetName)\n try {\n const bytes = await readFile(cached)\n options.onSource?.('cache')\n return bytes\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error\n }\n\n options.onSource?.('network')\n const archiveUrl = agentArchiveUrl(options.version, options.assetName)\n const sumsUrl = agentSumsUrl(options.version)\n const [archive, sums] = await Promise.all([\n download(fetcher, archiveUrl),\n download(fetcher, sumsUrl),\n ])\n const expected = expectedChecksum(sums.toString('utf8'), archiveName(options.assetName), sumsUrl)\n const actual = createHash('sha256').update(archive).digest('hex')\n if (actual !== expected) {\n throw new Error(\n `the download from ${archiveUrl} failed its SHA-256 check: expected ${expected}, got ${actual}`,\n )\n }\n const binary = archiveMember(archive, archiveUrl)\n\n // A reader of the cache must never observe a partial download, so the bytes\n // land on a private temp path and are renamed into place in one step. The\n // executable bit is set here because the file is copied verbatim to the\n // machine without a second local chmod.\n await mkdir(dirname(cached), { recursive: true, mode: 0o700 })\n const temp = `${cached}.${String(process.pid)}.${randomUUID()}`\n try {\n await writeFile(temp, binary, { mode: 0o755 })\n await rename(temp, cached)\n } catch (error) {\n await rm(temp, { force: true })\n throw error\n }\n return binary\n}\n","/**\n * Ensure the right agent is installed and running on one machine.\n *\n * A machine is configured by its SSH destination and a token, nothing else:\n * the plugin resolves the platform, downloads the matching binary if this\n * machine does not already run the expected build, uploads it over the same\n * SSH connection, and starts it detached. The agent then binds a random\n * loopback port and publishes it in `state.json`, which is where the forward\n * learns where to point.\n *\n * Reuse is decided from that state file and one marker beside the binary: a live\n * process running the expected build, started with the current launch recipe, is\n * left untouched, and the caller's own handshake is the real liveness check, so\n * this module never opens a TCP probe from the host.\n *\n * The agent is started through the account's interactive login shell rather\n * than directly, because the SSH command that reaches this module runs under\n * sshd, whose environment is minimal, and because rc files routinely hide their\n * PATH setup behind an interactive guard. Without the login shell the daemon\n * would inherit that minimal environment and pass it on to everything it starts\n * — the routing subprocess, git, and the Sidebar terminal — so a PATH the\n * profile extends or the user's own `SHELL` would never arrive. A launch-recipe\n * marker beside the binary is what keeps that from going stale: a machine still\n * running an agent started the old way is restarted instead of reused.\n *\n * Every remote snippet below is shaped for the same three reasons: bytes the\n * plugin owns travel on stdin rather than in the command string, where shell\n * quoting would mangle them and a token would become visible in `ps`; the\n * agent's stdio is redirected away from the SSH channel so `ssh` does not wait\n * on a process meant to outlive it; and `setsid`/`nohup` detach that process\n * from a session that is about to end.\n *\n * @module dsh-remote-workspace/remote/agent/install\n */\n\nimport { agentAssetName, resolveAgentBinary } from './release.ts'\nimport type { AgentBinaryOptions } from './release.ts'\nimport type { SshCommandResult, SshRunOptions, SshTarget } from '../ssh.ts'\nimport { runSsh, sshFailure } from '../ssh.ts'\n\n/**\n * Agent build this plugin installs on every machine it reaches.\n *\n * A single constant, not a config knob: the plugin and the release it\n * downloads are one artifact, and a plugin that let a deployment name a\n * different build would be describing a wire contract it cannot check. Bump\n * this together with the release tag and `agent/Cargo.toml`, which a unit test\n * keeps in step.\n */\nexport const AGENT_VERSION = '0.0.3'\n\n/**\n * Version of the launch recipe recorded in `launch-env.json`.\n *\n * The recipe is how the agent was started, not which build it runs: the\n * environment a start hands the agent is what everything it later spawns\n * inherits. Bump this whenever {@link startAgentCommand} changes what\n * environment that is, so a machine left running an agent from the previous\n * recipe is restarted rather than reused.\n */\nexport const LAUNCH_RECIPE_VERSION = 2\n\n/** A started agent, and where a forward can reach it. */\nexport interface AgentEndpoint {\n /** Loopback port the agent published. */\n readonly port: number\n /** Agent build the endpoint runs. */\n readonly version: string\n /** Whether an agent already running the expected build was left alone. */\n readonly reused: boolean\n}\n\n/**\n * What an install or update is doing, for a surface that can show it.\n *\n * The plugin runs this while the user waits on a connection, so the phase is\n * what a status line reports. `source` separates a cached fetch from a network\n * one because the two look identical from the outside and take very different\n * amounts of time.\n */\nexport interface AgentProgress {\n /** The step in flight. */\n readonly phase: 'checking' | 'reusing' | 'fetching' | 'uploading' | 'starting'\n /** Agent build the step concerns. */\n readonly version: string\n /** Release asset being fetched; present while `phase` is `fetching`. */\n readonly asset?: string\n /** Where the bytes came from, once the cache has been consulted. */\n readonly source?: 'cache' | 'network'\n}\n\n/** Runs one remote command; injectable so tests need no `ssh` process. */\nexport type AgentCommandRunner = (\n ssh: SshTarget,\n command: string,\n options?: SshRunOptions,\n) => Promise<SshCommandResult>\n\n/** What {@link ensureAgent} needs from its caller. */\nexport interface EnsureAgentOptions {\n /** The machine to install onto. */\n readonly ssh: SshTarget\n /** Shared secret the agent authenticates callers with. */\n readonly token: string\n /** Agent build to install and run. */\n readonly version: string\n /** Host directory holding cached agent binaries. */\n readonly cacheDir: string\n /** Command runner; defaults to {@link runSsh}. */\n readonly run?: AgentCommandRunner\n /** Binary resolver; defaults to {@link resolveAgentBinary}. */\n readonly resolveBinary?: (options: AgentBinaryOptions) => Promise<Buffer>\n /**\n * Login shell the start command uses instead of resolving the account's own.\n *\n * A test seam: a deployment never sets it, and production resolves `$SHELL`,\n * then the passwd entry, then `bash`, then `sh` on the machine.\n */\n readonly loginShell?: string\n /** Receives each step as it starts; omitted stays silent. */\n readonly onProgress?: (progress: AgentProgress) => void\n /** Budget for a fresh agent to publish its state, in milliseconds. */\n readonly startTimeoutMs?: number\n /** Gap between state-file polls, in milliseconds. */\n readonly pollMs?: number\n}\n\n/**\n * How long a freshly started agent may take to publish `state.json`. Generous\n * enough for a slow link, short enough that a binary that cannot exec is\n * reported rather than waited on.\n */\nconst DEFAULT_AGENT_START_TIMEOUT_MS = 10_000\n\n/** Default gap between state-file polls. */\nconst START_POLL_MS = 120\n\n/**\n * Read the state file; `|| true` keeps a machine that has never run the agent\n * from reading as a failed command, which is an expected state, not an error.\n */\nconst READ_STATE = 'cat \"$HOME/.dsh/remote-agent/state.json\" 2>/dev/null || true'\n\n/** Read the plugin's own marker for which build it installed. */\nconst READ_INSTALLED = 'cat \"$HOME/.dsh/remote-agent/installed.json\" 2>/dev/null || true'\n\n/** Read the plugin's own marker for how the running agent was launched. */\nconst READ_LAUNCH_ENV = 'cat \"$HOME/.dsh/remote-agent/launch-env.json\" 2>/dev/null || true'\n\n/** Seed the agent directory before anything is written into it. */\nconst ENSURE_DIR = 'mkdir -p \"$HOME/.dsh/remote-agent\"'\n\n/**\n * Replace the binary through a temp name, so a crash or a killed connection\n * never leaves a half-written executable in place, and stamp the executable\n * bit. The bytes arrive on stdin: a buffer interpolated into the command would\n * be mangled by the remote shell.\n */\nconst UPLOAD_BINARY = 'cat > \"$HOME/.dsh/remote-agent/dsh-remote-agent.new\"'\n + ' && chmod 755 \"$HOME/.dsh/remote-agent/dsh-remote-agent.new\"'\n + ' && mv \"$HOME/.dsh/remote-agent/dsh-remote-agent.new\" \"$HOME/.dsh/remote-agent/dsh-remote-agent\"'\n\n/** Record which build the step above installed, so a version bump reinstalls. */\nconst WRITE_INSTALLED = 'cat > \"$HOME/.dsh/remote-agent/installed.json\"'\n\n/** Write the secret on stdin too, and keep it owner-only. */\nconst WRITE_TOKEN = 'cat > \"$HOME/.dsh/remote-agent/token\" && chmod 600 \"$HOME/.dsh/remote-agent/token\"'\n\n/** Record how the agent was launched, so a changed recipe forces a restart. */\nconst WRITE_LAUNCH_ENV = 'cat > \"$HOME/.dsh/remote-agent/launch-env.json\"'\n\n/**\n * The agent invocation, run with `exec` from inside the interactive login shell.\n *\n * Quoted as one word so the outer non-interactive shell hands it to the login\n * shell untouched; it holds no single quote of its own. The paths stay relative\n * because the start command changed into the agent directory, and the login\n * shell inherits that directory.\n */\nconst AGENT_UNDER_LOGIN_SHELL =\n \"'exec ./dsh-remote-agent --listen 127.0.0.1:0 --token-file token --state-file state.json'\"\n\n/**\n * Quote one string as a single POSIX shell word.\n * @param value - the literal text to quote.\n * @returns the quoted word.\n */\nfunction shQuote(value: string): string {\n return `'${value.replaceAll(\"'\", \"'\\\\''\")}'`\n}\n\n/**\n * Build the command that starts the agent through the account's login shell.\n *\n * The shell is resolved on the machine in the order a person would expect:\n * {@link EnsureAgentOptions.loginShell} when a test names one, then `$SHELL`,\n * then the passwd entry (`getent` on Linux, `dscl` on Darwin), then `bash`, then\n * `sh`. `exec` inside `-ilc` replaces that login shell with the agent, so the\n * agent's environment is the login environment, its pid is the pid the state\n * file publishes, and it stays the session leader `setsid` created.\n *\n * The `i` is the whole point of the recipe: rc files commonly hide their PATH\n * setup behind an interactive guard — the node's `~/.bashrc` returns early\n * unless `$-` contains `i`, and only then adds `~/.local/bin` — so a\n * non-interactive login shell silently drops exactly the tool directories the\n * agent's commands need. VS Code's remote resolver runs the interactive login\n * shell for the same reason. Banners and job-control warnings an interactive\n * shell may print are harmless: they follow the same `agent.log` redirection,\n * the state file is read from disk, and the handshake runs over the socket.\n *\n * The start itself is unchanged: `setsid` gives the agent a session of its own\n * where the machine has it, `nohup` survives the hangup either way, every\n * stream goes to the log or `/dev/null` so `ssh` does not wait on a process\n * meant to outlive it, and `exit 0` keeps a successful backgrounding from\n * reading as a failed command.\n * @param loginShell - the shell to force, or undefined to resolve the account's.\n * @returns the command string the remote shell runs.\n */\nfunction startAgentCommand(loginShell?: string): string {\n const seed = loginShell === undefined ? '\"$SHELL\"' : shQuote(loginShell)\n const launch = `\"$agent_shell\" -ilc ${AGENT_UNDER_LOGIN_SHELL} >>agent.log 2>&1 </dev/null &`\n return 'cd \"$HOME/.dsh/remote-agent\" && {'\n + ` agent_shell=${seed};`\n + ' if [ ! -x \"$agent_shell\" ]; then agent_shell=\"$(getent passwd \"$(id -un)\" 2>/dev/null | cut -d: -f7)\"; fi;'\n + ' if [ ! -x \"$agent_shell\" ]; then agent_shell=\"$(dscl . -read \"/Users/$(id -un)\" UserShell 2>/dev/null | awk \\'NR==1 {print $2}\\')\"; fi;'\n + ' if [ ! -x \"$agent_shell\" ]; then agent_shell=\"$(command -v bash)\"; fi;'\n + ' if [ ! -x \"$agent_shell\" ]; then agent_shell=\"$(command -v sh)\"; fi;'\n + ` if command -v setsid >/dev/null 2>&1; then setsid nohup ${launch}`\n + ` else nohup ${launch} fi;`\n + ' }; exit 0'\n}\n\n/** The subset of the agent's published state this module reads. */\ninterface AgentState {\n readonly pid: number\n readonly port: number\n readonly version: string\n}\n\n/**\n * Parse the agent's state file.\n * @param stdout - the file's contents, or empty when it is absent.\n * @returns the fields a reuse decision needs, or undefined when unusable.\n */\nfunction parseState(stdout: string): AgentState | undefined {\n let value: unknown\n try {\n value = JSON.parse(stdout)\n } catch {\n return undefined\n }\n if (typeof value !== 'object' || value === null) return undefined\n const state = value as Record<string, unknown>\n const { pid, port, version } = state\n if (typeof version !== 'string' || version === '') return undefined\n if (typeof port !== 'number' || !Number.isInteger(port) || port <= 0) return undefined\n if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) return undefined\n return { pid, port, version }\n}\n\n/** Read and parse the state file. */\nasync function readState(\n run: AgentCommandRunner,\n ssh: SshTarget,\n): Promise<AgentState | undefined> {\n const result = await run(ssh, READ_STATE)\n return parseState(result.stdout)\n}\n\n/**\n * Run one command the install cannot continue without, and fail with the\n * shared SSH diagnostic rather than letting a silent non-zero exit surface\n * later as an unexplained timeout.\n * @param run - the command runner.\n * @param ssh - the machine to reach.\n * @param command - the command string to run.\n * @param fallback - what to say when the failure has no known remedy.\n * @param input - optional stdin bytes.\n * @throws when the command exits non-zero.\n */\nasync function runChecked(\n run: AgentCommandRunner,\n ssh: SshTarget,\n command: string,\n fallback: string,\n input?: Buffer | string,\n): Promise<void> {\n const result = await (input === undefined ? run(ssh, command) : run(ssh, command, { input }))\n if (result.code !== 0) throw new Error(sshFailure(ssh.target, result.stderr, fallback))\n}\n\n/** Read the version marker the plugin writes beside the binary. */\nasync function installedVersion(\n run: AgentCommandRunner,\n ssh: SshTarget,\n): Promise<string | undefined> {\n const result = await run(ssh, READ_INSTALLED)\n if (result.code !== 0) return undefined\n try {\n const marker = JSON.parse(result.stdout) as { version?: unknown }\n return typeof marker.version === 'string' ? marker.version : undefined\n } catch {\n return undefined\n }\n}\n\n/** Whether a process id is still alive on the machine, best effort. */\nasync function isAlive(\n run: AgentCommandRunner,\n ssh: SshTarget,\n pid: number,\n): Promise<boolean> {\n try {\n return (await run(ssh, `kill -0 ${String(pid)}`)).code === 0\n } catch {\n return false\n }\n}\n\n/** Whether the machine records this plugin's current launch recipe. */\nasync function launchRecipeMatches(run: AgentCommandRunner, ssh: SshTarget): Promise<boolean> {\n const result = await run(ssh, READ_LAUNCH_ENV)\n if (result.code !== 0) return false\n try {\n const marker = JSON.parse(result.stdout) as { recipe?: unknown }\n return marker.recipe === LAUNCH_RECIPE_VERSION\n } catch {\n return false\n }\n}\n\n/**\n * Ensure the agent is installed and running on one machine.\n * @param options - the machine, token, version, cache, and optional seams.\n * @returns the port and build the machine's agent is serving on.\n * @throws when the platform cannot be resolved, the binary cannot be fetched,\n * a remote command fails, or a started agent never publishes its state.\n */\nexport async function ensureAgent(options: EnsureAgentOptions): Promise<AgentEndpoint> {\n const run = options.run ?? runSsh\n const resolveBinary = options.resolveBinary ?? resolveAgentBinary\n const startTimeoutMs = options.startTimeoutMs ?? DEFAULT_AGENT_START_TIMEOUT_MS\n const pollMs = options.pollMs ?? START_POLL_MS\n const { ssh, token, version, cacheDir } = options\n const report = options.onProgress ?? (() => {})\n report({ phase: 'checking', version })\n\n // The platform is read once per call: every branch below either returns or\n // installs, so a second round trip would buy nothing.\n const uname = await run(ssh, 'uname -s; uname -m')\n if (uname.code !== 0) {\n throw new Error(\n sshFailure(ssh.target, uname.stderr, `could not read the platform of \"${ssh.target}\"`),\n )\n }\n const [platform, arch] = uname.stdout.split('\\n')\n const assetName = agentAssetName(platform?.trim() ?? '', arch?.trim() ?? '')\n\n const state = await readState(run, ssh)\n // A pid that does not answer `kill -0` is a stale state file, not a running\n // agent; a pid that does is a process the install below must replace.\n const runningPid = state !== undefined && await isAlive(run, ssh, state.pid) ? state.pid : undefined\n // The recipe is read only where a reuse could happen, so the common path that\n // installs or replaces an agent pays for no extra round trip. It answers\n // whether the live agent's environment is the one this plugin would start.\n if (state !== undefined && runningPid === state.pid && state.version === version\n && await launchRecipeMatches(run, ssh)) {\n report({ phase: 'reusing', version })\n return { port: state.port, version, reused: true }\n }\n // A state file left by the agent being replaced still names its pid and port.\n // Waiting for a different pid is what makes the poll below read the new\n // agent's publication rather than the one the start just superseded.\n const replacedPid = state?.pid\n\n await runChecked(run, ssh, ENSURE_DIR, `could not create ~/.dsh/remote-agent on \"${ssh.target}\"`)\n\n if (await installedVersion(run, ssh) !== version) {\n report({ phase: 'fetching', version, asset: assetName })\n const binary = await resolveBinary({\n version,\n assetName,\n cacheDir,\n // The cache check is immediate, so this replaces the phase above with\n // one that says whether the wait will be a download or a local read.\n onSource: source => { report({ phase: 'fetching', version, asset: assetName, source }) },\n })\n report({ phase: 'uploading', version })\n await runChecked(\n run,\n ssh,\n UPLOAD_BINARY,\n `could not install the agent binary on \"${ssh.target}\"`,\n binary,\n )\n await runChecked(\n run,\n ssh,\n WRITE_INSTALLED,\n `could not record the installed agent version on \"${ssh.target}\"`,\n JSON.stringify({ version }),\n )\n }\n\n // The token is rewritten on every install path, so rotating it in the plugin\n // is enough to rotate it on the machine.\n await runChecked(run, ssh, WRITE_TOKEN, `could not write the agent token on \"${ssh.target}\"`, token)\n\n if (runningPid !== undefined) {\n // Best effort: the process is being replaced, and a kill that races its\n // own exit must not fail an install that is otherwise fine.\n await run(ssh, `kill ${String(runningPid)}`).catch(() => {})\n }\n\n report({ phase: 'starting', version })\n await runChecked(\n run,\n ssh,\n startAgentCommand(options.loginShell),\n `could not start the agent on \"${ssh.target}\"`,\n )\n\n const deadline = Date.now() + startTimeoutMs\n for (;;) {\n const published = await readState(run, ssh).catch(() => undefined)\n if (published !== undefined && published.version === version && published.port > 0\n && published.pid !== replacedPid) {\n // The recipe is recorded once an agent started this way is answering, so\n // the marker never describes a start that did not survive.\n await runChecked(\n run,\n ssh,\n WRITE_LAUNCH_ENV,\n `could not record the agent launch recipe on \"${ssh.target}\"`,\n JSON.stringify({ recipe: LAUNCH_RECIPE_VERSION }),\n )\n return { port: published.port, version, reused: false }\n }\n if (Date.now() >= deadline) {\n throw new Error(\n `the agent on \"${ssh.target}\" did not publish a port within ${String(startTimeoutMs)}ms; `\n + 'check ~/.dsh/remote-agent/agent.log on the machine',\n )\n }\n await new Promise(resolve => setTimeout(resolve, pollMs))\n }\n}\n","/**\n * Connection lifecycle for configured nodes.\n *\n * One connection per node, owned here: the routers ask for a channel and get\n * either a live one or `undefined`, which is what makes \"the machine is\n * offline\" a typed failure at the call site instead of a hang.\n *\n * A dropped transport fails in-flight work and is never presented as\n * resumable. Reconnecting establishes a new connection with no carry-over;\n * remote identity alone cannot reconstruct pending calls, output cursors, or\n * process state.\n *\n * @module dsh-remote-workspace/models/machines\n */\n\nimport type { ConnectOptions, ConnectedNode, NodeChannel } from '../remote/client.ts'\nimport { NodeRequestError, connectNode } from '../remote/client.ts'\nimport type { NodeInfo } from '../remote/protocol.ts'\nimport type { NodeId, NodeRecord } from '../storage/nodes.ts'\nimport { DEFAULT_FORWARD_TIMEOUT_MS, openTunnel } from '../remote/ssh.ts'\nimport type { AgentEndpoint, AgentProgress, EnsureAgentOptions } from '../remote/agent/install.ts'\nimport { AGENT_VERSION, ensureAgent } from '../remote/agent/install.ts'\n\n/** Where one node's connection stands. */\nexport type NodeState = 'idle' | 'connecting' | 'ready' | 'failed' | 'disconnected'\n\n/** One node's connection state, as a surface may render it. */\nexport interface NodeStatus {\n readonly nodeId: NodeId\n readonly state: NodeState\n /** Present once the handshake succeeded. */\n readonly info?: NodeInfo\n /**\n * The local port carrying this node's traffic, once a forward is up. Absent\n * for a direct address, which needs no forward.\n */\n readonly localPort?: number\n /**\n * What the attempt is doing while it is not yet ready — installing or\n * updating the agent is slow enough that a surface should say so. Absent\n * once the node is ready or the attempt has ended.\n */\n readonly progress?: AgentProgress\n /** The failure message after a failed attempt or a dropped transport. */\n readonly error?: string\n}\n\n/**\n * The address a daemon is reachable at from this host, plus whatever carries\n * the traffic there.\n */\nexport interface ResolvedTransport {\n /** Host the daemon is reachable at, from this host. */\n readonly host: string\n /** TCP port the daemon is reachable at, from this host. */\n readonly port: number\n /**\n * Resolves when the transport stops carrying traffic, for a transport that\n * can fail on its own. A caller uses it to publish the loss; a direct\n * address never resolves and is closed only by its owner.\n */\n readonly exited?: Promise<void>\n /** Release whatever this transport holds. Idempotent. */\n close(): void\n}\n\n/** What the manager needs from its owner. */\nexport interface NodeConnectionsDeps {\n /** Establishes a connection; injectable so tests need no socket. */\n readonly connect?: (options: ConnectOptions) => Promise<ConnectedNode>\n /**\n * Turn a stored record into an address this host can dial. Defaults to the\n * SSH forward for an `ssh` record and the recorded address for a `direct`\n * one; injectable so tests need neither a network nor an `ssh` binary.\n */\n readonly openTransport?: (\n record: NodeRecord,\n report: (progress: AgentProgress) => void,\n ) => Promise<ResolvedTransport>\n /**\n * Deadline for the daemon handshake once a transport is up. Defaults to\n * {@link DEFAULT_HANDSHAKE_TIMEOUT_MS}; without one an unreachable daemon\n * leaves the attempt pending forever.\n */\n readonly daemonHandshakeTimeoutMs?: number\n /**\n * Budget for an SSH forward to start accepting connections. Defaults to\n * {@link DEFAULT_FORWARD_TIMEOUT_MS}.\n */\n readonly sshForwardTimeoutMs?: number\n /** Attempts after a connection drops; defaults to {@link RECOVERY_ATTEMPTS}. */\n readonly recoveryAttempts?: number\n /** Gap before the first recovery attempt, in milliseconds. */\n readonly recoveryGapMs?: number\n /**\n * Host directory the agent binaries are cached under. Required to reach an\n * `ssh` record with the default opener; tests that inject `openTransport`\n * never need it.\n */\n readonly cacheDir?: string\n /**\n * Agent build to ensure on every machine. Defaults to {@link AGENT_VERSION}.\n */\n readonly agentVersion?: string\n /**\n * Ensures the agent on a machine and reports the port it serves on. Defaults\n * to the SSH installer; injectable so tests need no `ssh` binary or network.\n */\n readonly ensureAgent?: (options: EnsureAgentOptions) => Promise<AgentEndpoint>\n}\n\n/** How many times a dropped connection is re-established before it is left failed. */\nconst RECOVERY_ATTEMPTS = 3\n\n/** Gap before the first recovery attempt, growing by that much for each later one. */\nconst RECOVERY_GAP_MS = 2_000\n\n/**\n * How long a handshake may take. Generous enough for a slow forward over a\n * long link, short enough that a daemon which is simply not running is\n * reported rather than waited on. The plugin exposes this as\n * `Config.daemonHandshakeTimeoutMs`; it is the fallback for a caller that\n * composes the manager directly.\n */\nexport const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000\n\n/**\n * Explain a failed connect attempt in the terms the operator can act on.\n *\n * A `ssh -L` forward binds its local port whether or not the agent listens\n * behind it on the machine, so a handshake that times out through a forward\n * means the agent is absent or wedged far more often than it means the network\n * failed — and the agent's own log is where the reason will be.\n * @param record - the machine that was being reached.\n * @param error - the failure the connector raised.\n * @returns the error to record and rethrow.\n */\nfunction describeFailure(record: NodeRecord, error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error)\n // Both deadlines the connector enforces mean the same thing here: the\n // socket opened, so something accepted it, but the agent never spoke.\n if (record.transport.kind === 'ssh' && /timed out connecting to|handshake with .* timed out/.test(message)) {\n return new Error(\n `the SSH forward to \"${record.transport.target}\" is up, but nothing answered; check ~/.dsh/remote-agent/agent.log on the machine`,\n { cause: error },\n )\n }\n return error instanceof Error ? error : new Error(message)\n}\n\n/** Everything the default transport opener needs from the manager's deps. */\ninterface OpenTransportDeps {\n /** Budget for an SSH forward to become ready. */\n readonly forwardTimeoutMs: number\n /** Agent build to ensure on a machine. */\n readonly agentVersion: string\n /** Agent binary cache directory; omitted refuses an `ssh` record. */\n readonly cacheDir: string | undefined\n /** Installs and starts the agent on a machine. */\n readonly ensureAgent: (options: EnsureAgentOptions) => Promise<AgentEndpoint>\n}\n\n/**\n * Build the default transport opener.\n *\n * An `ssh` record is reached in two steps — ensure the agent is running, then\n * forward the port it published — because the port is kernel-assigned and\n * known only from the machine. A `direct` address is dialled as recorded. The\n * local machine is refused here rather than dialled: it has no transport, and\n * connecting it means nothing.\n * @param deps - forward budget and the agent-ensuring seams.\n * @returns an opener for the transports that have somewhere to connect to.\n */\nfunction defaultOpenTransport(\n deps: OpenTransportDeps,\n): (record: NodeRecord, report: (progress: AgentProgress) => void) => Promise<ResolvedTransport> {\n return async (record, report) => {\n if (record.transport.kind === 'local') {\n throw new Error('the local machine needs no connection')\n }\n if (record.transport.kind === 'direct') {\n return {\n host: record.transport.host,\n port: record.transport.port,\n close: () => {},\n }\n }\n if (deps.cacheDir === undefined) {\n throw new Error('reaching a machine over SSH needs the plugin data directory to cache the agent')\n }\n const endpoint = await deps.ensureAgent({\n ssh: record.transport,\n token: record.token,\n version: deps.agentVersion,\n cacheDir: deps.cacheDir,\n onProgress: report,\n })\n const tunnel = await openTunnel(\n { ssh: record.transport, remotePort: endpoint.port },\n { readyTimeoutMs: deps.forwardTimeoutMs },\n )\n return {\n host: '127.0.0.1',\n port: tunnel.localPort,\n exited: tunnel.exited,\n close: () => { tunnel.close() },\n }\n }\n}\n\n/** The connection manager. */\nexport interface NodeConnections {\n /**\n * The live channel for one node.\n * @param nodeId - the record id.\n * @returns the channel, or undefined when the node is not connected.\n */\n channel(nodeId: NodeId): NodeChannel | undefined\n /**\n * One node's connection state.\n * @param nodeId - the record id.\n * @returns the status; `idle` for a node that was never connected.\n */\n status(nodeId: NodeId): NodeStatus\n /** Every node this manager has seen a state for, in insertion order. */\n list(): readonly NodeStatus[]\n /**\n * Connect one node, or return the handshake already in flight or completed.\n * @param record - the node to connect.\n * @returns what the daemon reported about itself.\n * @throws the connection or handshake failure; the node's status records it.\n */\n connect(record: NodeRecord): Promise<NodeInfo>\n /**\n * Close one node's connection. Idempotent.\n * @param nodeId - the record id.\n */\n disconnect(nodeId: NodeId): void\n /** Close every connection. Idempotent. */\n dispose(): void\n}\n\n/** One entry in the manager's table. Fields are explicitly nullable, not optional. */\ninterface Entry {\n state: NodeState\n info: NodeInfo | undefined\n error: string | undefined\n live: ConnectedNode | undefined\n pending: Promise<NodeInfo> | undefined\n transport: ResolvedTransport | undefined\n localPort: number | undefined\n progress: AgentProgress | undefined\n /** Identity of the recovery in flight for this entry, when one is. */\n recovery: symbol | undefined\n /** The channel handed out for this connection, wrapped to notice its loss. */\n published: NodeChannel | undefined\n}\n\n/**\n * Build the connection manager.\n * @param deps - an optional connection implementation, defaulting to the TCP client.\n * @returns the manager.\n */\nexport function createNodeConnections(deps: NodeConnectionsDeps = {}): NodeConnections {\n const connect = deps.connect ?? connectNode\n const openTransport = deps.openTransport ?? defaultOpenTransport({\n forwardTimeoutMs: deps.sshForwardTimeoutMs ?? DEFAULT_FORWARD_TIMEOUT_MS,\n agentVersion: deps.agentVersion ?? AGENT_VERSION,\n cacheDir: deps.cacheDir,\n ensureAgent: deps.ensureAgent ?? ensureAgent,\n })\n const handshakeTimeoutMs = deps.daemonHandshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS\n const recoveryAttempts = deps.recoveryAttempts ?? RECOVERY_ATTEMPTS\n const recoveryGapMs = deps.recoveryGapMs ?? RECOVERY_GAP_MS\n const entries = new Map<NodeId, Entry>()\n /**\n * Transports whose loss has already been published.\n *\n * A transport that answers `exited` again — a reused object, or one that was\n * already gone when it was handed over — must not start a second recovery:\n * the failure is the same event, and retrying it would reconnect forever.\n */\n const lost = new WeakSet<ResolvedTransport>()\n\n const entryFor = (nodeId: NodeId): Entry => {\n const existing = entries.get(nodeId)\n if (existing !== undefined) return existing\n const created: Entry = {\n state: 'idle',\n info: undefined,\n error: undefined,\n live: undefined,\n pending: undefined,\n transport: undefined,\n localPort: undefined,\n progress: undefined,\n recovery: undefined,\n published: undefined,\n }\n entries.set(nodeId, created)\n return created\n }\n\n /** Drop everything a connection holds, leaving the entry itself in place. */\n const clear = (entry: Entry): void => {\n entry.published = undefined\n entry.live?.close()\n entry.live = undefined\n entry.pending = undefined\n entry.info = undefined\n // The forward exists only to carry this connection, so a connection that\n // ended must not leave an `ssh` process running behind it.\n entry.transport?.close()\n entry.transport = undefined\n entry.localPort = undefined\n entry.progress = undefined\n }\n\n /** Publish a terminal state and drop everything the failure invalidates. */\n const fail = (entry: Entry, error: unknown): void => {\n clear(entry)\n entry.state = 'failed'\n entry.error = error instanceof Error ? error.message : String(error)\n }\n\n /** One entry as the status a caller reads. */\n const statusOf = (nodeId: NodeId, entry: Entry): NodeStatus => ({\n nodeId,\n state: entry.state,\n ...entry.info === undefined ? {} : { info: entry.info },\n ...entry.localPort === undefined ? {} : { localPort: entry.localPort },\n ...entry.progress === undefined ? {} : { progress: entry.progress },\n ...entry.error === undefined ? {} : { error: entry.error },\n })\n\n /**\n * Bring a dropped connection back without waiting to be asked.\n *\n * A drop is the one failure worth retrying unattended: the machine answered a\n * moment ago, so the reason is usually the link or a restarted daemon rather\n * than a configuration only a person could change. The attempts are bounded,\n * and whatever the last one reported stays on the entry.\n * @param entry - the entry whose connection dropped.\n * @param record - the machine to reach again.\n */\n const recover = (entry: Entry, record: NodeRecord): void => {\n const mine = Symbol('recovery')\n entry.recovery = mine\n void (async () => {\n for (let attempt = 1; attempt <= recoveryAttempts; attempt += 1) {\n await new Promise(resolve => {\n // Unref'd: a pending retry must never be what keeps a process alive.\n setTimeout(resolve, recoveryGapMs * attempt).unref()\n })\n // A disconnect, a disposal, or a newer drop has taken this over.\n if (entry.recovery !== mine) return\n try {\n await connectRecord(record)\n return\n } catch {\n // The attempt published its own failure; the next one may still work.\n }\n }\n })()\n }\n\n /**\n * The channel one connection hands out.\n *\n * The transport can die without the SSH process that carries it noticing: a\n * node whose daemon is killed leaves the forward open, so nothing arrives to\n * say the connection is gone until a call fails on it. A refusal the daemon\n * itself answered is a typed error and stays the caller's business; anything\n * else on the wire is a lost connection, which is published and recovered\n * from exactly like a forward that closed.\n * @param entry - the entry this connection belongs to.\n * @param record - the machine, for the retry.\n * @param live - the connection being published.\n * @returns the channel the routers call.\n */\n const publish = (entry: Entry, record: NodeRecord, live: ConnectedNode): NodeChannel => ({\n async request(method, params) {\n try {\n return await live.channel.request(method, params)\n } catch (error) {\n // Its loss was published already: a stale channel is not a new drop.\n if (entry.live === live && !(error instanceof NodeRequestError)) {\n fail(entry, error instanceof Error ? error : new Error(String(error)))\n recover(entry, record)\n }\n throw error\n }\n },\n onPipeFrame: handler => live.channel.onPipeFrame(handler),\n })\n\n /** Connect one node, or return the attempt already in flight. */\n const connectRecord = async (record: NodeRecord): Promise<NodeInfo> => {\n const entry = entryFor(record.nodeId)\n if (entry.state === 'ready' && entry.info !== undefined) return entry.info\n if (entry.pending !== undefined) return entry.pending\n\n entry.state = 'connecting'\n entry.error = undefined\n entry.progress = undefined\n const attempt = (async (): Promise<NodeInfo> => {\n try {\n // The forward comes first: without it there is no address to dial, and\n // its own failure is more specific than a refused connection would be.\n // Installing or updating the agent happens inside it, so its steps are\n // published as they start.\n const opened = await openTransport(record, (progress) => { entry.progress = progress })\n entry.transport = opened\n const live = await connect({\n host: opened.host,\n port: opened.port,\n token: record.token,\n timeoutMs: handshakeTimeoutMs,\n })\n entry.live = live\n entry.published = publish(entry, record, live)\n entry.info = live.info\n entry.localPort = record.transport.kind === 'ssh' ? opened.port : undefined\n entry.pending = undefined\n entry.progress = undefined\n entry.state = 'ready'\n // A forward can die while the socket it carried stays open long\n // enough to look healthy. Publish the loss rather than leaving a\n // `ready` node whose every call hangs, and start bringing it back.\n void opened.exited?.then(() => {\n if (lost.has(opened) || entry.transport !== opened) return\n lost.add(opened)\n fail(entry, new Error(`the SSH forward to \"${record.title}\" closed`))\n recover(entry, record)\n })\n return live.info\n } catch (error) {\n // Every failure settles the entry, including one from the opener: an\n // install that could not fetch or upload the agent must leave a\n // failed machine that can be retried, not one stuck in `connecting`\n // whose next attempt re-throws this same rejection.\n const reported = describeFailure(record, error)\n fail(entry, reported)\n throw reported\n }\n })()\n entry.pending = attempt\n return attempt\n }\n\n return {\n channel(nodeId) {\n return entries.get(nodeId)?.published\n },\n\n status(nodeId) {\n const entry = entries.get(nodeId)\n return entry === undefined ? { nodeId, state: 'idle' } : statusOf(nodeId, entry)\n },\n\n list() {\n return [...entries].map(([nodeId, entry]) => statusOf(nodeId, entry))\n },\n\n connect: connectRecord,\n\n disconnect(nodeId) {\n const entry = entries.get(nodeId)\n if (entry === undefined) return\n // A person asking for the connection to end also ends any retry of it.\n entry.recovery = undefined\n clear(entry)\n entry.error = undefined\n entry.state = 'disconnected'\n },\n\n dispose() {\n for (const entry of entries.values()) {\n entry.recovery = undefined\n clear(entry)\n }\n entries.clear()\n },\n }\n}\n","/**\n * The durable JSON document the node and repository stores share.\n *\n * A document is one file holding a versioned array of records, replaced\n * atomically under a cross-process lock, so two harness processes never\n * interleave a read-render-commit cycle. A caller owns its record shape, the\n * guard that recognizes one, and what the records mean; this module owns the\n * revision gate, the write lock, the atomic publication, and the diagnostics\n * that name a document this build cannot read.\n *\n * @module dsh-remote-workspace/storage/document\n */\n\nimport { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'\nimport { mkdir, readFile } from 'node:fs/promises'\nimport { dirname } from 'node:path'\n\n/** Owner-only permissions: these documents name paths and carry secrets. */\nconst FILE_MODE = 0o600\n\n/** How one document is laid out, recognized, and named in diagnostics. */\nexport interface DocumentSpec<T> {\n /** Absolute path of the JSON document. */\n readonly file: string\n /**\n * Revision this build writes. Revision 1 is read through\n * {@link DocumentSpec.migrate} when the caller supplies one.\n */\n readonly version: number\n /** Property holding the record array. */\n readonly key: string\n /** Names one record in diagnostics, e.g. `node`. */\n readonly label: string\n /** Whether a parsed entry is a record this build wrote. */\n readonly isRecord: (value: unknown) => value is T\n /**\n * Reads one entry of revision 1, for a document whose shape changed after its\n * first release. Supplied only while revision 1 must keep loading.\n */\n readonly migrate?: (value: unknown, index: number) => T\n}\n\n/**\n * Read a document, refusing anything this build did not write.\n * @param spec - layout, revision, and the record guard.\n * @returns the stored records in document order, or none when the file is absent.\n * @throws when the JSON is malformed, the revision is unsupported, or an entry\n * is not a record this build wrote.\n */\nexport async function readDocument<T>(spec: DocumentSpec<T>): Promise<readonly T[]> {\n let text: string\n try {\n text = await readFile(spec.file, 'utf8')\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []\n throw error\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch (error) {\n throw new Error(`${spec.file} is not valid JSON`, { cause: error })\n }\n if (typeof parsed !== 'object' || parsed === null) {\n throw new Error(`${spec.file} is not a ${spec.label} document`)\n }\n const document = parsed as Record<string, unknown>\n const entries = document[spec.key]\n if (!Array.isArray(entries)) throw new Error(`${spec.file} carries no ${spec.key} list`)\n if (document['version'] === 1 && spec.migrate !== undefined) return entries.map(spec.migrate)\n if (document['version'] !== spec.version) {\n throw new Error(\n `${spec.file} has document version ${String(document['version'])}; this build reads ${String(spec.version)}`,\n )\n }\n if (!entries.every(spec.isRecord)) {\n throw new Error(`${spec.file} carries a ${spec.label} entry this build does not understand`)\n }\n return entries\n}\n\n/**\n * Replace a document with one complete serialization.\n *\n * The candidate is serialized before the lock is taken and the caller's\n * in-memory list is replaced only after the write returns, so a failed commit\n * leaves it untouched instead of committing an unreported mutation whose caller\n * already saw an error.\n * @param spec - layout and revision.\n * @param records - the complete record list to publish.\n */\nexport async function writeDocument<T>(\n spec: DocumentSpec<T>,\n records: readonly T[],\n): Promise<void> {\n const content = `${JSON.stringify({ version: spec.version, [spec.key]: records }, null, 2)}\\n`\n // The lock is a `wx` create beside the document and never creates its\n // directory, so the first write into a fresh harness home must seed it.\n await mkdir(dirname(spec.file), { recursive: true, mode: 0o700 })\n await withFileLock(spec.file, async () => {\n await writeFileAtomic(spec.file, content, { mode: FILE_MODE })\n })\n}\n","/**\n * The durable record of the remote machines a user has configured.\n *\n * Records carry the node's shared secret; every projection that leaves this\n * module drops it, and {@link toNodeView} is the only supported way to produce\n * one.\n *\n * @module dsh-remote-workspace/storage/nodes\n */\n\nimport { brandString, type Branded } from '@deepseek-ai/dsh-brand'\nimport { randomUUID } from 'node:crypto'\nimport type { DocumentSpec } from './document.ts'\nimport { readDocument, writeDocument } from './document.ts'\n\n/**\n * Document revision. Revision 1 stored `host`/`port` on the record; revision 2\n * stores a transport that says how the host reaches the daemon.\n */\nconst DOCUMENT_VERSION = 2\n\n/**\n * The title a node gets when its caller named none.\n * @param transport - how the host reaches the daemon.\n * @returns the SSH destination, the direct address, or the local machine's name.\n */\nfunction defaultNodeTitle(transport: NodeTransport): string {\n if (transport.kind === 'local') return LOCAL_TITLE\n return transport.kind === 'ssh' ? transport.target : `${transport.host}:${String(transport.port)}`\n}\n\n/**\n * How the host reaches one machine's daemon.\n *\n * `ssh` is the only transport a caller may create: the host picks a free local\n * port and forwards it to the daemon's loopback port over an SSH connection,\n * which is what the deployment documentation tells operators to set up by\n * hand. `local` is this host itself, which needs no daemon and no connection;\n * `direct` exists so a document written before the SSH transport keeps loading\n * — nothing creates one, and it names an address the operator reached some\n * other way.\n */\nexport type NodeTransport =\n | {\n readonly kind: 'ssh'\n /** `ssh` destination: `user@host`, or a `~/.ssh/config` alias. */\n readonly target: string\n /** SSH port; omitted defers to the operator's `ssh` configuration. */\n readonly sshPort?: number\n /** Identity file; omitted defers to the operator's `ssh` configuration. */\n readonly identityFile?: string\n }\n | {\n /** This host: the machine the harness itself runs on. */\n readonly kind: 'local'\n }\n | {\n readonly kind: 'direct'\n /** Host the daemon is reachable at. */\n readonly host: string\n /** TCP port the daemon is reachable at. */\n readonly port: number\n }\n\n/**\n * One configured remote machine.\n *\n * Branded so a repository or anchor id cannot be passed where a machine is\n * expected: all three are generated strings that render identically in a log\n * or a URL, and the brand is the only thing that tells them apart. It lives in\n * the type system alone.\n */\nexport type NodeId = Branded<'NodeId'>\n\n/**\n * Admit a string as a machine id.\n *\n * Called where a string first becomes an id: a tool argument, a route segment,\n * or one this plugin derives from the coordinates it names. Every later hop\n * carries the type.\n * @param value - the string the parser produced.\n * @returns the same string, branded.\n */\nexport function asNodeId(value: string): NodeId {\n return brandString<NodeId>(value)\n}\n\n/**\n * The id, title, and instant the built-in local machine always carries.\n *\n * It is deliberately not a document entry: nothing configures it, so nothing\n * can leave it unreachable or removed, and a deployment that has never added a\n * machine still has this one to work in. The instant is the epoch because the\n * machine has been there since before the plugin was.\n */\nexport const LOCAL_NODE_ID = brandString<NodeId>('local')\n\n/** Title the local machine carries, in the record and in every view. */\nconst LOCAL_TITLE = 'Local'\n\n/**\n * The machine that is this host itself.\n *\n * Every surface treats it like any other machine — the same repository rows,\n * the same worktree lifecycle, the same dialogs — with two differences that\n * follow from where it is: there is no daemon to install and no connection to\n * make, and its paths are the paths this process already has.\n * @returns the local machine's record, for a caller that needs one.\n */\nfunction localNode(): NodeRecord {\n return {\n nodeId: LOCAL_NODE_ID,\n title: LOCAL_TITLE,\n transport: { kind: 'local' },\n token: '',\n createdAt: '1970-01-01T00:00:00.000Z',\n updatedAt: '1970-01-01T00:00:00.000Z',\n }\n}\n\n/** One configured remote machine. */\nexport interface NodeRecord {\n /** Stable generated id; never the target, so renaming a machine is free. */\n readonly nodeId: NodeId\n /** Display title. Defaults to the `ssh` destination. */\n readonly title: string\n /** How the host reaches this machine's daemon. */\n readonly transport: NodeTransport\n /**\n * The daemon's shared secret. Kept out of every view this module returns to\n * callers that render to a browser or a model.\n */\n readonly token: string\n /** ISO-8601 creation instant. */\n readonly createdAt: string\n /** ISO-8601 instant of the last accepted mutation. */\n readonly updatedAt: string\n}\n\n/** What a browser or another plugin may see: a record without its secret. */\nexport interface NodeView {\n readonly nodeId: NodeId\n readonly title: string\n readonly transport: NodeTransport\n /** Whether a secret is configured; the value itself never travels. */\n readonly hasToken: boolean\n readonly createdAt: string\n readonly updatedAt: string\n}\n\n/** Fields a caller supplies when creating or updating a node. */\nexport interface NodeDraft {\n readonly nodeId?: NodeId\n readonly title?: string\n readonly transport: NodeTransport\n readonly token: string\n}\n\n/** What the registry needs from its owner. */\nexport interface NodeRegistryDeps {\n /** Absolute path of the JSON document. */\n readonly file: string\n /** Injectable clock, so tests do not depend on wall time. */\n readonly now?: () => Date\n}\n\n/** The node registry. */\nexport interface NodeRegistry {\n /**\n * Read the document into memory. Missing is not an error: a fresh install has\n * no nodes. A malformed or future-versioned document fails loud rather than\n * being treated as empty, because silently starting empty would strand every\n * workspace anchored to a node.\n * @returns the loaded records, in document order.\n */\n load(): Promise<readonly NodeRecord[]>\n /** Every configured node, in stable document order. */\n list(): readonly NodeRecord[]\n /**\n * One node by id.\n * @param nodeId - the generated record id.\n * @returns the record, or undefined when no node carries that id.\n */\n get(nodeId: NodeId): NodeRecord | undefined\n /**\n * Create or update one node and persist the result.\n * @param draft - the caller's fields; omitted `nodeId` generates one.\n * @returns the stored record.\n */\n upsert(draft: NodeDraft): Promise<NodeRecord>\n /**\n * Remove one node and persist the result.\n * @param nodeId - the record to remove.\n * @returns true when a record was removed.\n */\n remove(nodeId: NodeId): Promise<boolean>\n}\n\n/** Whether an unknown parsed value is a transport this build understands. */\nfunction isTransport(value: unknown): value is NodeTransport {\n if (typeof value !== 'object' || value === null) return false\n const transport = value as Record<string, unknown>\n if (transport['kind'] === 'local') return true\n if (transport['kind'] === 'ssh') {\n return typeof transport['target'] === 'string'\n && (transport['sshPort'] === undefined || typeof transport['sshPort'] === 'number')\n && (transport['identityFile'] === undefined || typeof transport['identityFile'] === 'string')\n }\n if (transport['kind'] === 'direct') {\n return typeof transport['host'] === 'string' && typeof transport['port'] === 'number'\n }\n return false\n}\n\n/** Whether an unknown parsed value is a record this module wrote. */\nfunction isNodeRecord(value: unknown): value is NodeRecord {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Record<string, unknown>\n return typeof record['nodeId'] === 'string'\n && typeof record['title'] === 'string'\n && isTransport(record['transport'])\n && typeof record['token'] === 'string'\n && typeof record['createdAt'] === 'string'\n && typeof record['updatedAt'] === 'string'\n}\n\n/**\n * Read one revision-1 record as the revision-2 record.\n *\n * Revision 1 recorded a reachable address and nothing else, which is what the\n * `direct` transport still means. The record therefore carries over with no\n * field lost and no id change, so every repository and worktree already\n * anchored to this node keeps resolving.\n * @param value - the parsed revision-1 entry.\n * @param index - the entry's position, for the diagnostic.\n * @returns the equivalent revision-2 record.\n * @throws when the entry is not one this build can read.\n */\nfunction migrateV1(value: unknown, index: number): NodeRecord {\n const record = (typeof value === 'object' && value !== null ? value : {}) as Record<string, unknown>\n const fields = ['nodeId', 'title', 'host', 'token', 'createdAt', 'updatedAt'] as const\n for (const field of fields) {\n if (typeof record[field] !== 'string') {\n throw new Error(`node entry ${String(index)} from revision 1 carries no usable \"${field}\"`)\n }\n }\n if (typeof record['port'] !== 'number') {\n throw new Error(`node entry ${String(index)} from revision 1 carries no usable \"port\"`)\n }\n const port = record['port'] as number\n const host = record['host'] as string\n return {\n nodeId: brandString<NodeId>(record['nodeId'] as string),\n title: record['title'] as string,\n transport: { kind: 'direct', host, port },\n token: record['token'] as string,\n createdAt: record['createdAt'] as string,\n updatedAt: record['updatedAt'] as string,\n }\n}\n\n/**\n * Project a record for a caller that may render it.\n * @param record - the stored record.\n * @returns the record without its secret, plus the presence flag a form needs.\n */\nexport function toNodeView(record: NodeRecord): NodeView {\n return {\n nodeId: record.nodeId,\n title: record.title,\n transport: record.transport,\n hasToken: record.token.length > 0,\n createdAt: record.createdAt,\n updatedAt: record.updatedAt,\n }\n}\n\n/**\n * Build a node registry over one document.\n * @param deps - the document path and an optional clock.\n * @returns the registry; call {@link NodeRegistry.load} before serving reads.\n */\nexport function createNodeRegistry(deps: NodeRegistryDeps): NodeRegistry {\n const now = deps.now ?? (() => new Date())\n let nodes: NodeRecord[] = []\n let loaded = false\n\n const requireLoaded = (): void => {\n if (!loaded) throw new Error('node registry used before load()')\n }\n\n const document: DocumentSpec<NodeRecord> = {\n file: deps.file,\n version: DOCUMENT_VERSION,\n key: 'nodes',\n label: 'node',\n isRecord: isNodeRecord,\n migrate: migrateV1,\n }\n\n return {\n async load() {\n nodes = [...await readDocument(document)]\n loaded = true\n return nodes\n },\n\n list() {\n requireLoaded()\n // The local machine leads every list: it is the one machine a deployment\n // always has, and the one a person reaches for first.\n return [localNode(), ...nodes]\n },\n\n get(nodeId) {\n requireLoaded()\n if (nodeId === LOCAL_NODE_ID) return localNode()\n return nodes.find(node => node.nodeId === nodeId)\n },\n\n async upsert(draft) {\n requireLoaded()\n if (draft.nodeId === LOCAL_NODE_ID) {\n throw new Error('the local machine is built in and cannot be configured')\n }\n const stamp = now().toISOString()\n const existing = draft.nodeId === undefined\n ? undefined\n : nodes.find(node => node.nodeId === draft.nodeId)\n const record: NodeRecord = {\n nodeId: existing?.nodeId ?? draft.nodeId ?? brandString<NodeId>(randomUUID()),\n title: draft.title?.trim() || defaultNodeTitle(draft.transport),\n transport: draft.transport,\n token: draft.token,\n createdAt: existing?.createdAt ?? stamp,\n updatedAt: stamp,\n }\n const next = existing === undefined\n ? [...nodes, record]\n : nodes.map(node => (node.nodeId === record.nodeId ? record : node))\n await writeDocument(document, next)\n nodes = next\n return record\n },\n\n async remove(nodeId) {\n requireLoaded()\n // Not an error: the local machine is simply not a document entry, so\n // nothing was removed. Refusing it here keeps a caller from believing a\n // machine went away when the next read brings it back.\n if (nodeId === LOCAL_NODE_ID) return false\n const next = nodes.filter(node => node.nodeId !== nodeId)\n if (next.length === nodes.length) return false\n await writeDocument(document, next)\n nodes = next\n return true\n },\n }\n}\n","/**\n * Route classification: decide which execution world a model- or\n * plugin-supplied path belongs to.\n *\n * Two spellings name the same remote file, and both occur in practice:\n *\n * - the **anchor** path — a real local directory the session's cwd and\n * workspace point at, which is what the harness itself passes around;\n * - the **remote** path — the absolute path on the node, which is what the\n * model sees once the prompt's cwd variable is overridden.\n *\n * A remote absolute path is only routable while exactly one live anchor claims\n * it as its remote root. Two nodes commonly share `/home/<user>/<repo>`, so an\n * ambiguous match is a typed failure rather than a silent pick: guessing would\n * read one machine and write another.\n *\n * @module dsh-remote-workspace/models/routing\n */\n\nimport { posix } from 'node:path'\nimport type { AnchorRoute } from '../storage/anchors.ts'\nimport { asNodeId } from '../storage/nodes.ts'\nimport type { NodeId } from '../storage/nodes.ts'\n\n/** Where one path resolves to. */\nexport type Route =\n | { readonly kind: 'local' }\n | { readonly kind: 'remote'; readonly nodeId: NodeId; readonly remotePath: string }\n | {\n readonly kind: 'ambiguous'\n readonly remotePath: string\n /** The nodes whose remote root also claims `remotePath`, in discovery order. */\n readonly nodeIds: readonly NodeId[]\n }\n\n/**\n * What an ambiguous path is refused with. Shared because three seams refuse it\n * — the filesystem's typed error, a process spawn, and a terminal allocation —\n * and all three must name the same claiming nodes.\n */\nexport function ambiguousPathMessage(route: {\n readonly remotePath: string\n readonly nodeIds: readonly NodeId[]\n}): string {\n return `\"${route.remotePath}\" belongs to more than one node (${route.nodeIds.join(', ')}); `\n + 'address it as node:<id>:<path>'\n}\n\n/**\n * The explicit `node:<nodeId>:<path>` spelling. `nodeId` never contains a\n * colon, so the first one separates the id from an absolute POSIX path.\n */\nconst EXPLICIT = /^node:([^:/]+):(\\/.*)$/s\n\n/**\n * Whether `child` equals `parent` or lies below it, on whole path segments.\n *\n * A plain `startsWith` would match `/srv/app-old` against `/srv/app`.\n * @param parent - the canonical ancestor.\n * @param child - the canonical candidate.\n * @returns true when the candidate is the ancestor or one of its descendants.\n */\nexport function isWithin(parent: string, child: string): boolean {\n if (child === parent) return true\n const root = parent.endsWith('/') ? parent : `${parent}/`\n return child.startsWith(root)\n}\n\n/**\n * Make a path absolute and remove `.` and `..` segments without touching the\n * filesystem. A relative path resolves against `cwd`, falling back to `/` so\n * classification always yields a definite answer.\n * @param input - the path as supplied.\n * @param cwd - the resolving base, typically the session cwd.\n * @returns a normalized absolute POSIX path.\n */\nexport function toAbsolute(input: string, cwd: string | undefined): string {\n // Windows separators reach classification when the host is Windows; remote\n // paths are always POSIX and the two never mix inside one comparison.\n const unified = input.replaceAll('\\\\', '/')\n if (unified.startsWith('/')) return posix.normalize(unified)\n return posix.normalize(posix.join(cwd === undefined ? '/' : cwd, unified))\n}\n\n/**\n * Classify one path against the live anchors.\n *\n * Precedence is: the explicit `node:<id>:<path>` spelling, then the anchor\n * prefix, then a remote root claimed by exactly one anchor, then the local\n * world. The anchor branch wins over the remote-root branch because it carries\n * the node id unambiguously.\n * @param input - the path as supplied by a tool call or another plugin.\n * @param cwd - the resolving base for a relative path.\n * @param anchors - every anchor this plugin currently owns.\n * @returns the route, including the `ambiguous` verdict this module exists for.\n */\nexport function classifyPath(\n input: string,\n cwd: string | undefined,\n anchors: readonly AnchorRoute[],\n): Route {\n const explicit = EXPLICIT.exec(input)\n if (explicit !== null) {\n return { kind: 'remote', nodeId: asNodeId(explicit[1]!), remotePath: posix.normalize(explicit[2]!) }\n }\n\n const absolute = toAbsolute(input, cwd)\n\n for (const anchor of anchors) {\n if (isWithin(anchor.anchorPath, absolute)) {\n const suffix = absolute.slice(anchor.anchorPath.length)\n return { kind: 'remote', nodeId: anchor.nodeId, remotePath: posix.join(anchor.remoteRoot, suffix) }\n }\n }\n\n const claiming = anchors.filter(anchor => isWithin(anchor.remoteRoot, absolute))\n if (claiming.length === 1) {\n return { kind: 'remote', nodeId: claiming[0]!.nodeId, remotePath: absolute }\n }\n if (claiming.length > 1) {\n return {\n kind: 'ambiguous',\n remotePath: absolute,\n nodeIds: claiming.map(anchor => anchor.nodeId),\n }\n }\n\n return { kind: 'local' }\n}\n","/**\n * The anchor directory store.\n *\n * A remote worktree has no local directory of its own, but the harness gives a\n * session a local cwd, requires a workspace path to exist and to survive\n * `realpath`, and validates session membership against it. An anchor is the\n * answer: a real, empty local directory whose metadata names the remote\n * coordinates, so `ctx.fs` can route everything below it to the node while\n * every other subsystem keeps working on an ordinary local path.\n *\n * Layout, one directory per worktree:\n *\n * ```\n * <root>/<nodeId>/<repo base>/<name>/.dsh-remote-worktree.json\n * ```\n *\n * The directory is the identity; the metadata is the mapping. Removing the\n * metadata without the directory would leave a path that still routes, so both\n * move together.\n *\n * An anchor maps one remote directory, and there are two cases. A worktree\n * anchor maps the checkout `git worktree add` produced, and is what the\n * lifecycle cuts and removes. A directory anchor maps the repository directory\n * itself, which is what lets a machine's plain directory be opened as a\n * workspace before it is a git repository — and stay one after it becomes a\n * repository and worktrees are cut beside it.\n *\n * @module dsh-remote-workspace/storage/anchors\n */\n\nimport { brandString, type Branded } from '@deepseek-ai/dsh-brand'\nimport { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'\nimport { mkdir, readFile, readdir, realpath, rm } from 'node:fs/promises'\nimport { basename, dirname, join, resolve } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport type { NodeId } from './nodes.ts'\n\n/** Metadata file name inside every anchor directory. */\nexport const ANCHOR_FILE = '.dsh-remote-worktree.json'\n\n/** Metadata revision; a field change bumps it and refuses the old form. */\nconst DOCUMENT_VERSION = 1\n\n/** Segment a directory anchor lives at, beside the worktrees of its repository. */\nconst DIRECTORY_SEGMENT = '.self'\n\n/**\n * One anchor.\n *\n * Branded so a machine or repository id cannot be passed where an anchor is\n * expected: all three are generated strings that render identically in a log\n * or a URL, and the brand is the only thing that tells them apart. It lives in\n * the type system alone.\n */\nexport type AnchorId = Branded<'AnchorId'>\n\n/**\n * Admit a string as an anchor id.\n *\n * Called where a string first becomes an id: a tool argument, a route segment,\n * or one this plugin derives from the coordinates it names. Every later hop\n * carries the type.\n * @param value - the string the parser produced.\n * @returns the same string, branded.\n */\nexport function asAnchorId(value: string): AnchorId {\n return brandString<AnchorId>(value)\n}\n\n/** Owner-only permissions: the file is bookkeeping, not a secret. */\nconst FILE_MODE = 0o600\n\n/** How deep the loader walks below the anchor root: node / repo / name. */\nconst SCAN_DEPTH = 3\n\n/** What every anchor carries, whichever directory it maps. */\ninterface AnchorBase {\n /** Stable generated id; the anchor path is the identity, this is a handle. */\n readonly anchorId: AnchorId\n /** The node the remote root lives on. */\n readonly nodeId: NodeId\n /**\n * Name of the directory this anchor was created for: the worktree's name, or\n * the repository's own name for a directory anchor.\n */\n readonly name: string\n /** Absolute local directory: the session cwd and workspace path. */\n readonly anchorPath: string\n /** Absolute POSIX directory on the node that this anchor maps onto. */\n readonly remoteRoot: string\n /** Absolute POSIX path of the repository the directory belongs to. */\n readonly repoPath: string\n /** ISO-8601 creation instant. */\n readonly createdAt: string\n}\n\n/** An anchor on the checkout a worktree was cut into. */\nexport interface WorktreeAnchor extends AnchorBase {\n readonly kind: 'worktree'\n /** Full branch name the checkout is on. */\n readonly branch: string\n /**\n * How this plugin came to hold the checkout: the one it cut, wherever that\n * landed, or one it found on the machine and adopted.\n *\n * Absent on a record written before adoption existed, which was always cut.\n */\n readonly origin?: 'created' | 'adopted'\n}\n\n/** An anchor on the repository directory itself. */\nexport interface DirectoryAnchor extends AnchorBase {\n readonly kind: 'directory'\n}\n\n/** One remote directory's local handle. */\nexport type AnchorRecord = WorktreeAnchor | DirectoryAnchor\n\n/**\n * The anchor facts a router routes by: which node, which local directory, and\n * which remote root — without the identity or branch the lifecycle needs.\n */\nexport interface AnchorRoute {\n /** The node the anchor's remote root lives on. */\n readonly nodeId: NodeId\n /** Absolute local directory used as the session cwd and workspace path. */\n readonly anchorPath: string\n /** Absolute POSIX root the anchor maps onto. */\n readonly remoteRoot: string\n}\n\n/** Fields a caller supplies when creating a worktree anchor. */\nexport interface WorktreeAnchorDraft {\n readonly kind: 'worktree'\n readonly nodeId: NodeId\n readonly name: string\n readonly repoPath: string\n readonly remoteRoot: string\n /** The branch the checkout is on, as the daemon reported it. */\n readonly branch: string\n /** Defaults to `created`: the plugin cut this checkout itself. */\n readonly origin?: 'created' | 'adopted'\n}\n\n/** Fields a caller supplies when creating a directory anchor. */\nexport interface DirectoryAnchorDraft {\n readonly kind: 'directory'\n readonly nodeId: NodeId\n readonly name: string\n readonly repoPath: string\n /** The directory itself; a directory anchor maps one path onto itself. */\n readonly remoteRoot: string\n}\n\n/** Fields a caller supplies when creating an anchor. */\nexport type AnchorDraft = WorktreeAnchorDraft | DirectoryAnchorDraft\n\n/** What the store needs from its owner. */\nexport interface AnchorStoreDeps {\n /** Absolute root every anchor directory is created below. */\n readonly root: string\n /** Injectable clock, so tests do not depend on wall time. */\n readonly now?: () => Date\n}\n\n/** The anchor store. */\nexport interface AnchorStore {\n /**\n * Discover every anchor below the root.\n * @returns the records in scan order.\n * @throws when a metadata file is malformed or carries an unsupported\n * version — a stranded anchor must be visible, not silently dropped.\n */\n load(): Promise<readonly AnchorRecord[]>\n /** Every loaded anchor, in scan order. */\n list(): readonly AnchorRecord[]\n /**\n * One anchor by id.\n * @param anchorId - the generated handle.\n * @returns the record, or undefined when no anchor carries that id.\n */\n get(anchorId: AnchorId): AnchorRecord | undefined\n /**\n * Create the anchor directory and its metadata.\n * @param draft - the remote coordinates to record.\n * @returns the stored record.\n */\n create(draft: AnchorDraft): Promise<AnchorRecord>\n /**\n * Remove one anchor directory and its metadata.\n * @param anchorId - the generated handle.\n * @returns the removed record, or undefined when the id is unknown.\n */\n remove(anchorId: AnchorId): Promise<AnchorRecord | undefined>\n /** The routing table the filesystem consults. */\n routes(): readonly AnchorRoute[]\n}\n\n/** The metadata document written inside an anchor directory. */\ninterface AnchorDocument {\n readonly version: number\n readonly anchor: AnchorRecord\n}\n\n/** Every field a stored anchor may carry, as read from an untrusted document. */\ntype AnchorFields = Partial<Record<keyof WorktreeAnchor | keyof DirectoryAnchor, unknown>>\n\n/** Whether an unknown value is a record this module wrote. */\nfunction isAnchorRecord(value: unknown): value is AnchorRecord {\n if (typeof value !== 'object' || value === null) return false\n const record = value as AnchorFields\n const kind = record.kind\n return typeof record.anchorId === 'string'\n && typeof record.nodeId === 'string'\n && (kind === undefined || kind === 'worktree' || kind === 'directory')\n && typeof record.name === 'string'\n && typeof record.anchorPath === 'string'\n && typeof record.remoteRoot === 'string'\n && typeof record.repoPath === 'string'\n // Every anchor names a branch except a directory anchor, which maps a path\n // that has none: a document written before kinds existed still carries one,\n // so only the declared kind may omit it.\n && (typeof record.branch === 'string' || (record.branch === undefined && kind === 'directory'))\n && typeof record.createdAt === 'string'\n}\n\n/**\n * Parse one metadata document.\n * @param text - the file content.\n * @param file - the path, used only to name a failure.\n * @returns the record.\n * @throws when the JSON is malformed, the version is unsupported, or the\n * record does not match the stored fields.\n */\nfunction parseAnchor(text: string, file: string): AnchorRecord {\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch (error) {\n throw new Error(`${file} is not valid JSON`, { cause: error })\n }\n const document = parsed as Partial<AnchorDocument>\n if (document.version !== DOCUMENT_VERSION) {\n throw new Error(`${file} has document version ${String(document.version)}; this build reads ${String(DOCUMENT_VERSION)}`)\n }\n if (!isAnchorRecord(document.anchor)) {\n throw new Error(`${file} carries an anchor this build does not understand`)\n }\n const anchor = document.anchor\n // A document written before kinds existed carries none, and the guard has\n // already refused any that omit the branch a worktree must have. One written\n // before adoption existed names no origin, and was always cut by this plugin.\n if (anchor.kind === 'directory') return anchor\n return { ...anchor, kind: 'worktree', origin: anchor.origin === 'adopted' ? 'adopted' : 'created' }\n}\n\n/**\n * Walk the anchor tree and collect every metadata file it finds.\n *\n * Only directories are descended into, and a symlink is neither a directory\n * nor a metadata file here, so a link planted in the tree cannot make the scan\n * loop or escape the root.\n * @param dir - the directory to scan.\n * @param depth - levels still permitted below `dir`.\n * @returns the metadata file paths.\n */\nasync function findMetadataFiles(dir: string, depth: number): Promise<string[]> {\n if (depth < 0) return []\n const found: string[] = []\n const entries = await readdir(dir, { withFileTypes: true }).catch((error: NodeJS.ErrnoException) => {\n if (error.code === 'ENOENT') return []\n throw error\n })\n for (const entry of entries) {\n const child = join(dir, entry.name)\n if (entry.isFile() && entry.name === ANCHOR_FILE) {\n found.push(child)\n continue\n }\n if (entry.isDirectory()) found.push(...await findMetadataFiles(child, depth - 1))\n }\n return found\n}\n\n/**\n * Build an anchor store over one root.\n * @param deps - the root and an optional clock.\n * @returns the store; call {@link AnchorStore.load} before serving reads.\n */\nexport function createAnchorStore(deps: AnchorStoreDeps): AnchorStore {\n const now = deps.now ?? (() => new Date())\n // Resolved for real in `load`: the harness canonicalizes a workspace path\n // before a session runs in it, so an anchor spelled through a symlink would\n // never match the cwd a tool call arrives with.\n let root = resolve(deps.root)\n let anchors: AnchorRecord[] = []\n let loaded = false\n\n const requireLoaded = (): void => {\n if (!loaded) throw new Error('anchor store read before load()')\n }\n\n return {\n async load() {\n // A path the harness hands to a session has been through `realpath`, and\n // the router matches it against these anchors by prefix. Resolving the\n // root here — before anything is created below it — is what keeps the two\n // spellings equal; otherwise every remote path classifies as local and\n // the tools quietly run on this host instead of the machine.\n await mkdir(root, { recursive: true })\n root = await realpath(root)\n const files = (await findMetadataFiles(root, SCAN_DEPTH)).sort()\n anchors = []\n for (const file of files) {\n const record = parseAnchor(await readFile(file, 'utf8'), file)\n // A record written before the root was resolved carries whatever\n // spelling the root had then; its own metadata file is the truth.\n anchors.push({ ...record, anchorPath: await realpath(dirname(file)) })\n }\n loaded = true\n return anchors\n },\n\n list() {\n requireLoaded()\n return anchors\n },\n\n get(anchorId) {\n requireLoaded()\n return anchors.find(anchor => anchor.anchorId === anchorId)\n },\n\n async create(draft) {\n requireLoaded()\n // A worktree anchor sits at its own name; a directory anchor sits beside\n // the worktrees at a segment no checkout can occupy, so the two kinds\n // never nest and the routers never see one path under two anchors.\n const anchorPath = join(\n root,\n draft.nodeId,\n basename(draft.repoPath),\n draft.kind === 'directory' ? DIRECTORY_SEGMENT : draft.name,\n )\n // Two records pointing at one directory would make removal ambiguous and\n // leave the survivor routing into a deleted path.\n if (anchors.some(anchor => anchor.anchorPath === anchorPath)) {\n throw new Error(`an anchor already owns ${anchorPath}`)\n }\n const shared = {\n anchorId: brandString<AnchorId>(randomUUID()),\n nodeId: draft.nodeId,\n name: draft.name,\n anchorPath,\n remoteRoot: draft.remoteRoot,\n repoPath: draft.repoPath,\n createdAt: now().toISOString(),\n }\n // `satisfies` on each arm keeps the discriminator narrow; an annotation\n // on the whole conditional would widen both into one unusable union.\n const record = draft.kind === 'worktree'\n ? {\n ...shared,\n kind: 'worktree',\n branch: draft.branch,\n origin: draft.origin ?? 'created',\n } satisfies WorktreeAnchor\n : { ...shared, kind: 'directory' } satisfies DirectoryAnchor\n await mkdir(anchorPath, { recursive: true })\n await writeFileAtomic(\n join(anchorPath, ANCHOR_FILE),\n `${JSON.stringify({ version: DOCUMENT_VERSION, anchor: record } satisfies AnchorDocument, null, 2)}\\n`,\n { mode: FILE_MODE },\n )\n anchors = [...anchors, record]\n return record\n },\n\n async remove(anchorId) {\n requireLoaded()\n const record = anchors.find(anchor => anchor.anchorId === anchorId)\n if (record === undefined) return undefined\n await rm(record.anchorPath, { recursive: true, force: true })\n anchors = anchors.filter(anchor => anchor.anchorId !== anchorId)\n return record\n },\n\n routes() {\n requireLoaded()\n return anchors.map(anchor => ({\n nodeId: anchor.nodeId,\n anchorPath: anchor.anchorPath,\n remoteRoot: anchor.remoteRoot,\n }))\n },\n }\n}\n","/**\n * Git on this host, for the local machine's management operations.\n *\n * These are the same five answers the daemon gives over the wire —\n * `git.repoState`, `git.worktreeList`, `git.worktreeAdd`, `git.worktreeRemove`,\n * `git.branchDelete` — asked of the git binary in this process instead of over\n * a connection. That is the whole difference between a local machine and a\n * remote one: the same lifecycle, run where the checkout is.\n *\n * `GIT_OPTIONAL_LOCKS=0` is set on every command because the panel polls: a\n * `git status` that refreshes the index takes the lock a session's own `git`\n * command may be holding, and the answer it wanted never needed the lock.\n *\n * @module dsh-remote-workspace/local/git\n */\n\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\n\nconst run = promisify(execFile)\n\n/** Everything one git invocation is allowed to buffer. */\nconst MAX_BUFFER = 8 << 20\n\n/** Why a git command failed. */\ntype LocalGitCode = 'GIT_NOT_A_REPOSITORY' | 'GIT_COMMAND_FAILED'\n\n/** A git failure, carrying the same discriminant the daemon's errors carry. */\nclass LocalGitError extends Error {\n /** Which kind of failure this is. */\n readonly code: LocalGitCode\n\n /**\n * @param code - the failure's kind.\n * @param message - what git said.\n */\n constructor(code: LocalGitCode, message: string) {\n super(message)\n this.name = 'LocalGitError'\n this.code = code\n }\n}\n\n/** One checkout git knows about, as `git worktree list` reports it. */\nexport interface LocalWorktree {\n /** Absolute path of the checkout. */\n readonly path: string\n /** Short branch name, or null when the entry is detached or bare. */\n readonly branch: string | null\n /** Whether this is the repository's main worktree. */\n readonly main: boolean\n}\n\n/** How git spells a directory it does not consider a repository. */\nconst NOT_A_REPOSITORY = /not a git repository/i\n\n/**\n * Run one git command in a repository.\n * @param repoPath - the directory git runs in.\n * @param args - the arguments after `-C <repoPath>`.\n * @returns git's standard output.\n * @throws LocalGitError, distinguishing \"not a repository\" from every other failure.\n */\nasync function git(repoPath: string, args: readonly string[]): Promise<string> {\n try {\n const { stdout } = await run('git', ['-C', repoPath, ...args], {\n env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },\n maxBuffer: MAX_BUFFER,\n })\n return stdout\n } catch (error) {\n const stderr = String((error as { stderr?: unknown }).stderr ?? '').trim()\n const message = stderr === '' ? String(error instanceof Error ? error.message : error) : stderr\n throw new LocalGitError(\n NOT_A_REPOSITORY.test(message) ? 'GIT_NOT_A_REPOSITORY' : 'GIT_COMMAND_FAILED',\n message,\n )\n }\n}\n\n/**\n * Whether git owns a directory.\n * @param repoPath - the directory to ask about.\n * @returns true when the directory is inside a work tree.\n * @throws LocalGitError when git could not be asked at all.\n */\nexport async function isRepository(repoPath: string): Promise<boolean> {\n try {\n const inside = await git(repoPath, ['rev-parse', '--is-inside-work-tree'])\n return inside.trim() === 'true'\n } catch (error) {\n if (error instanceof LocalGitError && error.code === 'GIT_NOT_A_REPOSITORY') return false\n throw error\n }\n}\n\n/**\n * Every checkout git knows for a repository.\n * @param repoPath - the repository to ask.\n * @returns the entries in git's own order, the main worktree first.\n * @throws LocalGitError when the directory is not a repository.\n */\nexport async function listWorktrees(repoPath: string): Promise<readonly LocalWorktree[]> {\n const porcelain = await git(repoPath, ['worktree', 'list', '--porcelain'])\n const found: LocalWorktree[] = []\n for (const block of porcelain.split('\\n\\n')) {\n const lines = block.split('\\n').filter(line => line !== '')\n const path = lines.find(line => line.startsWith('worktree '))?.slice('worktree '.length)\n if (path === undefined) continue\n const ref = lines.find(line => line.startsWith('branch '))?.slice('branch '.length)\n found.push({\n path,\n // A detached entry has no `branch` line; a bare one has no work tree.\n branch: ref === undefined ? null : ref.replace(/^refs\\/heads\\//, ''),\n main: found.length === 0,\n })\n }\n return found\n}\n\n/**\n * Cut a worktree and its branch.\n * @param options - the repository, the checkout path, and the branch to create.\n * @throws LocalGitError when git refuses.\n */\nexport async function addWorktree(options: {\n readonly repoPath: string\n readonly worktreePath: string\n readonly branch: string\n readonly baseRef?: string\n}): Promise<void> {\n await git(options.repoPath, [\n 'worktree', 'add', '-b', options.branch, options.worktreePath,\n ...options.baseRef === undefined ? [] : [options.baseRef],\n ])\n}\n\n/**\n * Remove one checkout, leaving its branch.\n * @param options - the repository, the checkout, and whether to discard changes.\n * @throws LocalGitError when git refuses.\n */\nexport async function removeWorktree(options: {\n readonly repoPath: string\n readonly worktreePath: string\n readonly force: boolean\n}): Promise<void> {\n await git(options.repoPath, [\n 'worktree', 'remove', ...options.force ? ['--force'] : [], options.worktreePath,\n ])\n}\n\n/**\n * Delete one branch.\n * @param options - the repository, the branch, and whether an unmerged branch goes too.\n * @throws LocalGitError when git refuses.\n */\nexport async function deleteBranch(options: {\n readonly repoPath: string\n readonly branch: string\n readonly force: boolean\n}): Promise<void> {\n await git(options.repoPath, ['branch', options.force ? '-D' : '-d', options.branch])\n}\n","/**\n * This host's own filesystem, for the management plane.\n *\n * The panel's operations run where the plugin runs, so the local machine's\n * answers come from `node:fs` rather than from the routed `ctx.fs` seam: that\n * seam belongs to a session's execution world, and the panel has to keep\n * working from a deployment whose sessions are confined somewhere else — or\n * whose execution world is another machine entirely.\n *\n * @module dsh-remote-workspace/local/fs\n */\n\nimport { readdir, realpath, stat } from 'node:fs/promises'\nimport { resolve } from 'node:path'\n\n/** What one directory entry's type can be, in the vocabulary the wire uses. */\nexport type LocalPathType = 'file' | 'directory' | 'symlink' | 'other'\n\n/** One entry of a local directory listing, as the panel reads one. */\nexport interface LocalDirEntry {\n /** Entry name, without its directory. */\n readonly name: string\n /** What the entry is. */\n readonly type: LocalPathType\n /** Absolute path of the entry. */\n readonly path: string\n}\n\n/**\n * Resolve a path the way a machine resolves one.\n *\n * Existence is not part of the answer: a path that is not there still has a\n * canonical spelling, and keeping the two questions apart is what lets the\n * caller answer \"does not exist\" with the status that belongs to it rather\n * than with a resolver failure.\n * @param path - the caller's path, absolute or relative to the harness' cwd.\n * @returns the canonical absolute path.\n */\nexport async function resolveLocalPath(path: string): Promise<string> {\n try {\n return await realpath(path)\n } catch {\n return resolve(path)\n }\n}\n\n/**\n * Read one path's type.\n *\n * A symlink reports what it resolves to, because every caller is asking what a\n * workspace would run in. The listing below answers the other question.\n * @param path - the absolute path to probe.\n * @returns the type, or undefined when nothing is there.\n */\nexport async function localPathType(path: string): Promise<LocalPathType | undefined> {\n try {\n const info = await stat(path)\n if (info.isDirectory()) return 'directory'\n if (info.isFile()) return 'file'\n return 'other'\n } catch {\n return undefined\n }\n}\n\n/**\n * List one directory's entries.\n *\n * Sizes are not read: nothing in the panel shows them, and one `stat` per entry\n * would buy a column no surface renders.\n * @param path - the absolute directory to read.\n * @returns its entries in the order the filesystem reports them.\n * @throws the filesystem failure when the directory cannot be read.\n */\nexport async function listLocalDir(path: string): Promise<readonly LocalDirEntry[]> {\n const entries = await readdir(path, { withFileTypes: true })\n return entries.map(entry => ({\n name: entry.name,\n type: entry.isDirectory()\n ? 'directory'\n : entry.isFile() ? 'file' : entry.isSymbolicLink() ? 'symlink' : 'other',\n path: resolve(path, entry.name),\n }))\n}\n","/**\n * The remote worktree lifecycle: create, list, open, close, remove.\n *\n * It also owns the one workspace that is not a worktree: a repository directory\n * opened as itself. A machine's directory can be worked in before it is a git\n * repository, so opening it maps the directory onto its own anchor, and cutting\n * worktrees from it becomes possible later without re-registering anything —\n * whether it is a repository is a live fact, asked of the machine each time the\n * section reads it.\n *\n * The same lifecycle runs for the local machine, and it is the same to every\n * caller: the same rows, the same drafts, the same results. What differs is\n * where the truth lives. A machine reached over SSH needs an anchor — a local\n * directory standing in for a path this host cannot reach — and that anchor is\n * the record. This host needs nothing of the kind: its checkouts are real\n * directories here, so git itself is the record and an id names a path rather\n * than a handle. That is why a local worktree has no entry to create and none\n * to drop, and why forgetting a repository is still refused while git lists\n * checkouts under it: those rows would be stranded in the panel.\n *\n * Every operation is two-sided on purpose: git runs on the node, and the local\n * anchor is created or dropped around it. The order matters in both\n * directions — an anchor is only recorded after the checkout exists, and the\n * checkout is only removed before its anchor, so a failure never leaves a\n * local path routing into nothing. Within teardown the workspace entry goes\n * first, because dropping the anchor takes the directory that resolves it.\n *\n * Creating one also records the repository it was cut from. That is not\n * bookkeeping for its own sake: the surfaces group worktrees by repository, so\n * a worktree whose repository has no record is a worktree nobody can see or\n * remove from the settings section.\n *\n * The branch is out of scope. Creating a worktree creates the branch it needs,\n * that is unavoidable; deleting one leaves the branch behind unless the caller\n * explicitly asks for it, because deleting branches belongs to whatever plugin\n * owns branches. A branch that could not be deleted is reported rather than\n * swallowed: the worktree is gone either way, and the operator needs to know\n * the branch outlived it.\n *\n * @module dsh-remote-workspace/models/worktrees\n */\n\nimport { posix } from 'node:path'\nimport type { WireWorktree } from '../remote/protocol.ts'\nimport type { AnchorId, AnchorRecord, AnchorStore, DirectoryAnchor, WorktreeAnchor } from '../storage/anchors.ts'\nimport { asAnchorId } from '../storage/anchors.ts'\nimport type { ChannelLookup } from '../remote/client.ts'\nimport { NodeRequestError } from '../remote/client.ts'\nimport { asNodeId, type NodeId } from '../storage/nodes.ts'\nimport type { RepoRecord, RepoRef, RepoStore } from '../storage/repos.ts'\nimport {\n addWorktree,\n deleteBranch,\n isRepository,\n listWorktrees,\n removeWorktree,\n} from '../local/git.ts'\nimport { localPathType, resolveLocalPath } from '../local/fs.ts'\n\n/** Prefix every managed branch carries. */\nconst BRANCH_PREFIX = 'worktree/'\n\n/** What a caller supplies to cut a new worktree. */\nexport interface WorktreeDraft {\n /** The node whose repository is cut. */\n readonly nodeId: NodeId\n /** Absolute POSIX path of the repository on that node. */\n readonly repoPath: string\n /** Worktree name; the branch becomes `worktree/<name>`. */\n readonly name: string\n /**\n * Absolute POSIX path the checkout is created at, when the caller names one.\n * The configured root's `<repository>/<name>` is the default.\n */\n readonly path?: string\n /** Revision to branch from; the repository's HEAD when omitted. */\n readonly baseRef?: string\n}\n\n/** What a removal did. */\nexport interface WorktreeRemoval {\n /** The worktree anchor that was removed. */\n readonly anchor: WorktreeAnchor\n /** Whether the branch was deleted as well. */\n readonly branchDeleted: boolean\n /** Why the branch outlived the checkout, when it did. */\n readonly branchError?: string\n}\n\n/** One anchor with what this host can say about it without asking the node. */\nexport interface WorktreeStatus {\n /** The local anchor. */\n readonly anchor: AnchorRecord\n /** Whether the anchor is registered as a workspace, so a session can open on it. */\n readonly open: boolean\n /**\n * Whether this plugin cut the checkout itself.\n *\n * Both kinds can be removed — the operator's own checkout included, which is\n * what the confirmation says before it deletes one — so the flag only picks\n * that confirmation's wording.\n */\n readonly managed: boolean\n /**\n * Whether this plugin holds a record for the row.\n *\n * A checkout read straight from a machine's git has none until it is opened,\n * so there is nothing to release and only an open or a remove to offer.\n */\n readonly held: boolean\n /** Why the worktree cannot be used right now, when it cannot. */\n readonly error?: string\n}\n\n/** One checkout the machine's git already knows about. */\nexport interface ExistingWorktree {\n /** Absolute POSIX path of the checkout on that machine. */\n readonly path: string\n /** Name the checkout is shown under: its last path segment. */\n readonly name: string\n /** Branch the checkout sits on; empty when it is detached. */\n readonly branch: string\n /** Whether this plugin already holds it in the panel. */\n readonly registered: boolean\n}\n\n/** Best-effort workspace registration, so every caller behaves the same way. */\nexport interface WorkspaceHooks {\n /**\n * Record the anchor as a workspace.\n * @param anchor - the anchor just created.\n */\n register(anchor: AnchorRecord): Promise<void>\n /**\n * Drop the anchor's workspace registration.\n * @param anchor - the anchor just removed.\n */\n unregister(anchor: AnchorRecord): Promise<void>\n /**\n * Whether the anchor currently holds a workspace registration.\n * @param anchor - the anchor to ask about.\n */\n registered(anchor: AnchorRecord): Promise<boolean>\n}\n\n/** What the manager needs from its owner. */\nexport interface WorktreeManagerDeps {\n /** The anchor store that owns local identity. */\n readonly anchors: AnchorStore\n /**\n * The repository records. Cutting a worktree records the repository it came\n * from, because every surface groups worktrees by repository and a worktree\n * whose repository is missing is one nobody can see.\n */\n readonly repos: RepoStore\n /** Resolves the live channel for a node. */\n readonly channel: ChannelLookup\n /**\n * Whether one node id names this host rather than another machine.\n *\n * A local node reaches its own filesystem, so it takes the branch that reads\n * paths and runs git here instead of the branch that talks to a daemon. The\n * answer comes from the registry rather than from the channel lookup, because\n * \"not connected\" and \"this host\" are different states and only one of them\n * is a failure.\n */\n readonly isLocalNode: (nodeId: NodeId) => boolean\n /**\n * The directory managed checkouts live under on one machine.\n *\n * Resolved per node because a machine's home is its own: the default root is\n * that machine user's `~/.dsh/worktrees`, and a configured absolute path is\n * used verbatim. A checkout is placed at `<root>/<repository>/<name>`, never\n * inside the repository, so a worktree never makes its repository dirty.\n */\n readonly worktreeRoot: (nodeId: NodeId) => string\n /**\n * Workspace registration, when the deployment composes a registry. A failure\n * here never fails the git operation: the checkout and its anchor are durable\n * on their own, and the workspace entry is a convenience for opening it.\n */\n readonly workspace?: WorkspaceHooks\n}\n\n/** The worktree lifecycle. */\nexport interface WorktreeManager {\n /**\n * Cut a worktree on the node and record its anchor.\n * @param draft - node, repository, name, and optional base revision.\n * @returns the created worktree anchor.\n * @throws the daemon's typed failure when git refuses; no anchor is recorded.\n */\n create(draft: WorktreeDraft): Promise<WorktreeAnchor>\n /** Every managed worktree with its node's live state, in anchor order. */\n list(): Promise<readonly WorktreeStatus[]>\n /**\n * Every checkout the machine's git knows about for one repository.\n *\n * This is how a checkout cut by hand, or before this plugin existed, becomes\n * visible: git is the record on the machine, and the plugin only holds what\n * it was asked to hold. The repository's own checkout is left out.\n * @param ref - the machine and repository path.\n * @returns the checkouts in git's order, each marked when already held.\n */\n existing(ref: RepoRef): Promise<readonly ExistingWorktree[]>\n /**\n * Take an existing checkout under management, without touching git.\n *\n * The path must be one the machine's own git lists for that repository, so\n * nothing is adopted on a caller's word alone; the checkout itself stays\n * exactly where it is, and this host records an anchor and opens it as a\n * workspace. Adopting one this host already holds just opens it again.\n * @param ref - the machine and repository path.\n * @param path - absolute POSIX path of the checkout on that machine.\n * @returns the anchor that now holds it, registered as a workspace.\n * @throws when git does not list that path under that repository.\n */\n adopt(ref: RepoRef, path: string): Promise<WorktreeAnchor>\n /**\n * Stop managing one checkout, leaving it on the machine untouched.\n *\n * This is the counterpart of {@link adopt} and the reason it exists: a\n * checkout nobody asked this plugin to cut must never be deleted through it.\n * @param anchorId - the anchor handle.\n * @returns the anchor that was released.\n * @throws when no such anchor exists.\n */\n release(anchorId: AnchorId): Promise<AnchorRecord>\n /**\n * The worktrees cut under one repository.\n *\n * Answers the delete guard. For a machine it reads local records alone, so\n * the answer is the same whether or not the node is reachable; for this host\n * it asks git, because that is where a local checkout is recorded.\n * @param ref - the machine and repository path.\n * @returns the worktrees under that repository, in listing order.\n */\n anchorsIn(ref: RepoRef): Promise<readonly AnchorRecord[]>\n /**\n * Remove one worktree: the checkout on the node, then its anchor.\n * @param anchorId - the anchor handle.\n * @param options - `force` discards uncommitted changes; `deleteBranch`\n * also deletes the branch.\n * @returns what was removed, and whether the branch followed.\n * @throws the daemon's typed failure when git refuses; the anchor stays.\n */\n remove(anchorId: AnchorId, options: { force: boolean; deleteBranch: boolean }): Promise<WorktreeRemoval>\n /**\n * Register the anchor as a workspace again, so it can be opened.\n *\n * Unlike creation, this is an explicit ask: a registry that is missing or\n * refuses fails the call rather than being swallowed.\n * @param anchorId - the anchor handle.\n * @returns the anchor that is now open.\n * @throws when no such anchor exists, or the workspace registry refuses.\n */\n open(anchorId: AnchorId): Promise<AnchorRecord>\n /**\n * Drop the anchor's workspace registration, leaving the machine untouched.\n * @param anchorId - the anchor handle.\n * @returns the anchor that is now closed.\n * @throws when no such anchor exists.\n */\n close(anchorId: AnchorId): Promise<AnchorRecord>\n /**\n * Open a repository directory as a workspace in its own right.\n *\n * Git is not consulted: a directory that is not a repository yet can still be\n * worked in, and the anchors of the worktrees cut from it later sit beside\n * this one. Idempotent — opening an open directory registers it again instead\n * of creating a second anchor.\n * @param ref - the machine and the directory's path on it.\n * @returns the directory anchor that is now open.\n * @throws when the machine is unreachable or no workspace registry is composed.\n */\n openDirectory(ref: RepoRef): Promise<DirectoryAnchor>\n /**\n * Close a repository directory's workspace and drop its anchor.\n *\n * Nothing on the machine is touched: the anchor is this host's bookkeeping,\n * so closing the workspace is what removes it.\n * @param ref - the machine and the directory's path on it.\n * @returns the anchor that was dropped, or undefined when none was open.\n */\n closeDirectory(ref: RepoRef): Promise<DirectoryAnchor | undefined>\n}\n\n/** The path a managed checkout is created at, on whichever machine owns it. */\nfunction managedWorktreePath(root: string, repoPath: string, name: string): string {\n return posix.join(root, posix.basename(repoPath), name)\n}\n\n/** The branch a managed checkout is created on. */\nfunction branchFor(name: string): string {\n return `${BRANCH_PREFIX}${name}`\n}\n\n/**\n * Whether this plugin cut one checkout itself.\n *\n * Managed checkouts sit under the configured worktree root; anything else was\n * found on the machine, and must only ever be released. A local root reached\n * through a symlink (`/var` on macOS is one) spells one directory two ways, and\n * git reports the resolved one, so the root is resolved before comparing.\n * @param deps - the manager's dependencies.\n * @param anchor - the anchor to judge.\n * @returns true when this plugin is the one that cut it.\n */\nasync function isManaged(deps: WorktreeManagerDeps, anchor: AnchorRecord): Promise<boolean> {\n if (anchor.kind !== 'worktree') return false\n // A record says which it is, wherever the checkout landed.\n if (anchor.origin !== undefined) return anchor.origin === 'created'\n // A local checkout has no record beyond git, so the root is the only clue,\n // and it is resolved because `/var` and `/private/var` are one directory.\n const root = deps.worktreeRoot(anchor.nodeId).replace(/\\/+$/, '')\n const prefix = deps.isLocalNode(anchor.nodeId) ? await resolveLocalPath(root) : root\n return anchor.remoteRoot.startsWith(`${prefix}/`)\n}\n\n/**\n * Register an anchor as a workspace, best effort.\n * @param deps - the manager's dependencies.\n * @param anchor - the anchor to register as a workspace.\n */\nasync function registerWorkspace(deps: WorktreeManagerDeps, anchor: AnchorRecord): Promise<void> {\n try {\n await deps.workspace?.register(anchor)\n } catch {\n // The checkout and its anchor are durable on disk; a workspace entry is a\n // convenience, so a registry failure must not undo the work.\n }\n}\n\n/**\n * Drop an anchor's workspace registration.\n * @param deps - the manager's dependencies.\n * @param anchor - the anchor that was removed.\n */\nasync function unregisterWorkspace(deps: WorktreeManagerDeps, anchor: AnchorRecord): Promise<void> {\n try {\n await deps.workspace?.unregister(anchor)\n } catch {\n // A stale registration is harmless; the workspace path no longer resolves.\n }\n}\n\n/**\n * Prefix that marks an id as naming a path on this host.\n *\n * A machine's ids come from the anchor store and are generated once. This host\n * has no such store, so its ids are derived from what they name and stay opaque\n * to every caller: nothing outside this module reads more than this prefix.\n */\nconst LOCAL_ID_PREFIX = 'local:'\n\n/** The id one local path is addressed by. */\nfunction localAnchorId(kind: 'worktree' | 'directory', path: string, repoPath: string): AnchorId {\n return asAnchorId(kind === 'directory'\n ? `${LOCAL_ID_PREFIX}directory:${encodePart(path)}`\n : `${LOCAL_ID_PREFIX}worktree:${encodePart(path)}:${encodePart(repoPath)}`)\n}\n\n/** Prefix that marks an id as naming a checkout git reports on a machine. */\nconst REMOTE_ID_PREFIX = 'remote:'\n\n/** One URL-safe encoding of a value, for composing a derived id. */\nconst encodePart = (value: string): string => Buffer.from(value, 'utf8').toString('base64url')\n\n/** The inverse of {@link encodePart}. */\nconst decodePart = (value: string): string => Buffer.from(value, 'base64url').toString('utf8')\n\n/** The id one machine's git-reported checkout is addressed by before it is held. */\nfunction remoteAnchorId(nodeId: NodeId, repoPath: string, path: string): AnchorId {\n return asAnchorId(`${REMOTE_ID_PREFIX}${encodePart(nodeId)}:${encodePart(repoPath)}:${encodePart(path)}`)\n}\n\n/** The coordinates a git-reported id carries, or undefined for any other id. */\nfunction parseRemoteAnchorId(value: string): { nodeId: NodeId; repoPath: string; path: string } | undefined {\n if (!value.startsWith(REMOTE_ID_PREFIX)) return undefined\n const [node, repo, path] = value.slice(REMOTE_ID_PREFIX.length).split(':')\n if (node === undefined || repo === undefined || path === undefined) return undefined\n return { nodeId: asNodeId(decodePart(node)), repoPath: decodePart(repo), path: decodePart(path) }\n}\n\n/**\n * The record a git-reported checkout is shown as before this plugin holds it.\n *\n * The id is derived from the checkout's coordinates rather than minted, so the\n * same git worktree lists under the same id on every read; `anchorPath` is a\n * placeholder until an open adopts it and mints the real local directory.\n */\nfunction remotePlaceholder(\n nodeId: NodeId,\n repoPath: string,\n path: string,\n branch: string,\n createdAt: string,\n): WorktreeAnchor {\n return {\n anchorId: remoteAnchorId(nodeId, repoPath, path),\n nodeId,\n kind: 'worktree',\n name: posix.basename(path) || path,\n repoPath,\n anchorPath: path,\n remoteRoot: path,\n branch,\n origin: 'adopted',\n createdAt,\n }\n}\n\n/** The row a local repository itself is opened through. */\nfunction localDirectoryAnchor(record: RepoRecord): DirectoryAnchor {\n const name = posix.basename(record.repoPath) || record.repoPath\n return {\n anchorId: localAnchorId('directory', record.repoPath, record.repoPath),\n nodeId: record.nodeId,\n kind: 'directory',\n name,\n repoPath: record.repoPath,\n // A local directory maps onto itself: the checkout and the workspace path\n // are the same path, which is what makes routing unnecessary here.\n anchorPath: record.repoPath,\n remoteRoot: record.repoPath,\n createdAt: record.createdAt,\n }\n}\n\n/**\n * One row's live state, degrading a read failure to the row's own error.\n *\n * A workspace registry that refuses one anchor, or a machine that cannot answer\n * about it, must not blank every other row: the failure belongs to the row.\n * @param deps - the manager's dependencies.\n * @param anchor - the row to describe.\n * @param offlineError - the reason to report when the node is not connected.\n * @returns the status, carrying `error` when the state could not be read.\n */\nasync function rowStatus(\n deps: WorktreeManagerDeps,\n anchor: AnchorRecord,\n offlineError?: string,\n): Promise<WorktreeStatus> {\n try {\n const open = await deps.workspace?.registered(anchor) ?? false\n const managed = await isManaged(deps, anchor)\n return { anchor, open, managed, held: true, ...offlineError === undefined ? {} : { error: offlineError } }\n } catch (error) {\n return { anchor, open: false, managed: false, held: true, error: error instanceof Error ? error.message : String(error) }\n }\n}\n\n/**\n * Every checkout and repository directory this host manages.\n *\n * A machine's rows come from its anchors. This host has none, so they come from\n * git, which is where a local checkout actually lives — including one cut by\n * hand, outside the panel, which is then just as visible and openable as the\n * ones the panel made. The repository's own worktree is the row that opens the\n * repository directory itself.\n * @param deps - the manager's dependencies.\n * @returns the rows, the local repository records in their stored order.\n */\nasync function localStatuses(deps: WorktreeManagerDeps): Promise<readonly WorktreeStatus[]> {\n const statuses: WorktreeStatus[] = []\n for (const repo of deps.repos.list()) {\n if (!deps.isLocalNode(repo.nodeId)) continue\n const directory = localDirectoryAnchor(repo)\n statuses.push(await rowStatus(deps, directory))\n // A directory that is not a repository yet is a legitimate record: it can be\n // opened as a workspace and initialized later, so git is asked every time.\n if (!await isRepository(repo.repoPath).catch(() => false)) continue\n const checkouts = await listWorktrees(repo.repoPath).catch(() => [])\n for (const checkout of checkouts.slice(1)) {\n // A checkout whose directory is gone is git's own leftover rather than a\n // row: nothing can be opened or removed through it. An unreadable one is\n // skipped the same way, so one bad checkout cannot blank the others.\n if (await localPathType(checkout.path).catch(() => undefined) !== 'directory') continue\n const anchor: WorktreeAnchor = {\n anchorId: localAnchorId('worktree', checkout.path, repo.repoPath),\n nodeId: repo.nodeId,\n kind: 'worktree',\n name: posix.basename(checkout.path) || checkout.path,\n repoPath: repo.repoPath,\n anchorPath: checkout.path,\n remoteRoot: checkout.path,\n // A detached checkout has no branch to name, and an empty one is what\n // every surface already renders as silence.\n branch: checkout.branch ?? '',\n createdAt: repo.createdAt,\n }\n statuses.push(await rowStatus(deps, anchor))\n }\n }\n return statuses\n}\n\n/** Register a repository nobody has registered yet, keeping a known name. */\nasync function registerRepoIfUnknown(\n deps: WorktreeManagerDeps,\n nodeId: NodeId,\n repoPath: string,\n): Promise<void> {\n if (deps.repos.find({ nodeId, repoPath }) === undefined) {\n await deps.repos.upsert({ nodeId, repoPath })\n }\n}\n\n/** Narrow a record to a worktree, refusing the repository directory. */\nfunction requireWorktree(anchor: AnchorRecord): WorktreeAnchor {\n if (anchor.kind !== 'worktree') {\n throw new Error(`\"${anchor.name}\" is the repository directory, not a worktree; close it instead`)\n }\n return anchor\n}\n\n/** The directory anchor one machine path is held under, if this host holds one. */\nfunction directoryAnchorOf(deps: WorktreeManagerDeps, ref: RepoRef): DirectoryAnchor | undefined {\n return deps.anchors.list().find(\n (anchor): anchor is DirectoryAnchor =>\n anchor.kind === 'directory' && anchor.nodeId === ref.nodeId && anchor.repoPath === ref.repoPath,\n )\n}\n\n/** One local row by the id a caller holds, or undefined when none carries it. */\nasync function localEntry(\n deps: WorktreeManagerDeps,\n anchorId: AnchorId,\n): Promise<AnchorRecord | undefined> {\n const statuses = await localStatuses(deps)\n return statuses.find(status => status.anchor.anchorId === anchorId)?.anchor\n}\n\n/**\n * Cut a worktree on this host and register its checkout.\n * @param deps - the manager's dependencies.\n * @param draft - node, repository, name, and optional base revision.\n * @returns the created checkout's record.\n */\nasync function createLocalWorktree(deps: WorktreeManagerDeps, draft: WorktreeDraft): Promise<WorktreeAnchor> {\n // One spelling of the repository, settled before anything is written: it is\n // what the checkout, the record, and the panel's tree carry, so a later\n // lookup by path finds the same directory the caller meant.\n const repoPath = await resolveLocalPath(draft.repoPath)\n if (!await isRepository(repoPath)) {\n throw new Error(`\"${repoPath}\" is not a git repository on this machine`)\n }\n const branch = branchFor(draft.name)\n const plannedPath = draft.path ?? managedWorktreePath(deps.worktreeRoot(draft.nodeId), repoPath, draft.name)\n await addWorktree({\n repoPath,\n worktreePath: plannedPath,\n branch,\n ...draft.baseRef === undefined ? {} : { baseRef: draft.baseRef },\n })\n // Git records the checkout under its resolved spelling, and the listing that\n // later finds this anchor is built from what git reports. A root reached\n // through a symlink (`/var` on macOS is one) would otherwise leave the record\n // and the listing naming one directory two ways.\n const worktreePath = await resolveLocalPath(plannedPath)\n\n const anchor: WorktreeAnchor = {\n anchorId: localAnchorId('worktree', worktreePath, repoPath),\n nodeId: draft.nodeId,\n kind: 'worktree',\n name: draft.name,\n repoPath,\n anchorPath: worktreePath,\n remoteRoot: worktreePath,\n branch,\n createdAt: new Date().toISOString(),\n }\n await registerRepoIfUnknown(deps, draft.nodeId, repoPath)\n await registerWorkspace(deps, anchor)\n return anchor\n}\n\n/**\n * Remove one checkout on this host, leaving its branch.\n * @param deps - the manager's dependencies.\n * @param anchor - the checkout's record.\n * @param options - `force` discards uncommitted changes; `deleteBranch` also\n * deletes the branch.\n * @returns what was removed, and whether the branch followed.\n */\nasync function removeLocalWorktree(\n deps: WorktreeManagerDeps,\n anchor: WorktreeAnchor,\n options: { force: boolean; deleteBranch: boolean },\n): Promise<WorktreeRemoval> {\n await removeWorktree({\n repoPath: anchor.repoPath,\n worktreePath: anchor.anchorPath,\n force: options.force,\n })\n // The workspace entry resolves by path, so it goes before the path stops\n // existing under its feet.\n await unregisterWorkspace(deps, anchor)\n return await dropBranchOrReport(anchor, options, () =>\n deleteBranch({ repoPath: anchor.repoPath, branch: anchor.branch, force: options.force }))\n}\n\n/**\n * Open a freshly registered anchor, leaving nothing behind when that is refused.\n *\n * An anchor with nothing open behind it is a row nobody asked for, and a path\n * that routes into a directory nobody asked to open.\n * @param deps - the manager's dependencies.\n * @param anchor - the anchor that was just created.\n * @param open - how this kind of anchor is opened.\n * @throws whatever `open` threw, after the anchor is removed.\n */\nasync function openOrDrop(\n deps: WorktreeManagerDeps,\n anchor: AnchorRecord,\n open: () => Promise<unknown>,\n): Promise<void> {\n try {\n await open()\n } catch (error) {\n await deps.anchors.remove(anchor.anchorId)\n throw error\n }\n}\n\n/**\n * Delete a removed checkout's branch, reporting a refusal instead of failing.\n *\n * The checkout is already gone, so a branch that would not go is reported\n * rather than thrown: the caller shows why it outlived its worktree.\n * @param anchor - the worktree whose branch is in question.\n * @param options - whether to delete the branch, and how hard.\n * @param remove - the deletion itself, local or over the wire.\n * @returns the removal, with the branch's fate.\n */\nasync function dropBranchOrReport(\n anchor: WorktreeAnchor,\n options: { force: boolean; deleteBranch: boolean },\n remove: () => Promise<unknown>,\n): Promise<WorktreeRemoval> {\n if (!options.deleteBranch) return { anchor, branchDeleted: false }\n try {\n await remove()\n return { anchor, branchDeleted: true }\n } catch (error) {\n return {\n anchor,\n branchDeleted: false,\n branchError: error instanceof Error ? error.message : String(error),\n }\n }\n}\n\n/**\n * Register one path as a workspace, so a session can be opened on it.\n * @param deps - the manager's dependencies.\n * @param anchor - the worktree or directory to open.\n * @returns the anchor that is now open.\n * @throws when the deployment composes no workspace registry, or it refuses.\n */\nasync function openAsWorkspace<T extends AnchorRecord>(\n deps: WorktreeManagerDeps,\n anchor: T,\n): Promise<T> {\n const workspace = deps.workspace\n if (workspace === undefined) {\n throw new Error('this deployment composes no workspace registry, so a worktree cannot be opened')\n }\n await workspace.register(anchor)\n return anchor\n}\n\n/**\n * Build the worktree manager.\n * @param deps - the anchor store, the repository records, and the node lookups.\n * @returns the lifecycle handle.\n */\nexport function createWorktreeManager(deps: WorktreeManagerDeps): WorktreeManager {\n /** The live channel for an anchor's node, or the typed offline failure. */\n const channelFor = (nodeId: NodeId) => {\n const channel = deps.channel(nodeId)\n if (channel === undefined) {\n throw new NodeRequestError({\n code: 'GIT_COMMAND_FAILED',\n message: `remote node \"${nodeId}\" is not connected`,\n })\n }\n return channel\n }\n\n /** The machine's own checkouts for one repository, minus its main one. */\n const listExisting = async (ref: RepoRef): Promise<readonly ExistingWorktree[]> => {\n if (deps.isLocalNode(ref.nodeId)) {\n const checkouts = await listWorktrees(ref.repoPath).catch(() => [])\n // Every local checkout is a row already: the panel reads them from git.\n return checkouts.slice(1).map(checkout => ({\n path: checkout.path,\n name: posix.basename(checkout.path) || checkout.path,\n branch: checkout.branch ?? '',\n registered: true,\n }))\n }\n const channel = channelFor(ref.nodeId)\n const { canonicalPath } = await channel.request('fs.resolve', { path: ref.repoPath })\n const listed = await channel.request('git.worktreeList', { repoPath: canonicalPath })\n const held = deps.anchors.list()\n return listed\n .filter(entry => !entry.main)\n .map(entry => ({\n path: entry.path,\n name: posix.basename(entry.path) || entry.path,\n branch: entry.branch ?? '',\n registered: held.some(anchor => anchor.nodeId === ref.nodeId && anchor.remoteRoot === entry.path),\n }))\n }\n\n /** The held anchor for one git-reported checkout, if this plugin already holds it. */\n const heldRemote = (discovered: { nodeId: NodeId; repoPath: string; path: string }): WorktreeAnchor | undefined =>\n deps.anchors.list().find(\n (anchor): anchor is WorktreeAnchor =>\n anchor.kind === 'worktree'\n && anchor.nodeId === discovered.nodeId\n && anchor.repoPath === discovered.repoPath\n && anchor.remoteRoot === discovered.path,\n )\n\n /** Open a remote checkout git already lists, adopting it when it is new. */\n const adoptRemote = async (ref: RepoRef, path: string): Promise<WorktreeAnchor> => {\n const listed = await listExisting(ref)\n const entry = listed.find(candidate => candidate.path === path)\n if (entry === undefined) {\n throw new Error(`\"${path}\" is not a worktree of \"${ref.repoPath}\" on that machine`)\n }\n const held = heldRemote({ nodeId: ref.nodeId, repoPath: ref.repoPath, path })\n if (held !== undefined) return await openAsWorkspace(deps, held)\n\n const anchor = await deps.anchors.create({\n kind: 'worktree',\n nodeId: ref.nodeId,\n name: posix.basename(path) || path,\n repoPath: ref.repoPath,\n remoteRoot: path,\n branch: entry.branch,\n origin: 'adopted',\n })\n await openOrDrop(deps, anchor, () => openAsWorkspace(deps, anchor))\n return { ...anchor, kind: 'worktree', branch: entry.branch }\n }\n\n /** Remove one held remote checkout, dropping its record with the checkout. */\n const removeRemote = async (\n anchor: WorktreeAnchor,\n options: { force: boolean; deleteBranch: boolean },\n ): Promise<WorktreeRemoval> => {\n const channel = channelFor(anchor.nodeId)\n await channel.request('git.worktreeRemove', {\n repoPath: anchor.repoPath,\n worktreePath: anchor.remoteRoot,\n force: options.force,\n })\n // The checkout is gone, so the local handle must go with it — but the\n // workspace registry resolves an entry by path, which stops resolving the\n // moment the anchor directory is removed, so that goes first.\n await unregisterWorkspace(deps, anchor)\n await deps.anchors.remove(anchor.anchorId)\n return await dropBranchOrReport(anchor, options, () =>\n channel.request('git.branchDelete', {\n repoPath: anchor.repoPath,\n branch: anchor.branch,\n force: options.force,\n }))\n }\n\n /** Remove a checkout git reports but this plugin never adopted. */\n const removeDiscovered = async (\n discovered: { nodeId: NodeId; repoPath: string; path: string },\n options: { force: boolean; deleteBranch: boolean },\n ): Promise<WorktreeRemoval> => {\n const ref = { nodeId: discovered.nodeId, repoPath: discovered.repoPath }\n const entry = (await listExisting(ref)).find(candidate => candidate.path === discovered.path)\n if (entry === undefined) {\n throw new Error(`\"${discovered.path}\" is not a worktree of \"${discovered.repoPath}\" on that machine`)\n }\n const channel = channelFor(discovered.nodeId)\n await channel.request('git.worktreeRemove', {\n repoPath: discovered.repoPath,\n worktreePath: discovered.path,\n force: options.force,\n })\n const anchor = remotePlaceholder(\n discovered.nodeId,\n discovered.repoPath,\n discovered.path,\n entry.branch,\n new Date().toISOString(),\n )\n // A detached checkout has no branch to delete, and git is asked for one\n // only when there is one.\n if (!options.deleteBranch || entry.branch === '') return { anchor, branchDeleted: false }\n return await dropBranchOrReport(anchor, options, () =>\n channel.request('git.branchDelete', {\n repoPath: discovered.repoPath,\n branch: entry.branch,\n force: options.force,\n }))\n }\n\n /**\n * Rows for every checkout git reports on a machine that this plugin does not\n * hold yet, so the panel reads worktrees from git rather than from records a\n * person had to create by hand. A repository whose git cannot be read is\n * skipped, because one broken repository must not blank the others.\n */\n const discoveredRemote = async (held: ReadonlySet<string>): Promise<readonly WorktreeStatus[]> => {\n const statuses: WorktreeStatus[] = []\n for (const repo of deps.repos.list()) {\n if (deps.isLocalNode(repo.nodeId) || deps.channel(repo.nodeId) === undefined) continue\n try {\n for (const checkout of await listExisting(repo)) {\n if (held.has(`${repo.nodeId}\\u0000${checkout.path}`)) continue\n statuses.push({\n anchor: remotePlaceholder(repo.nodeId, repo.repoPath, checkout.path, checkout.branch, repo.createdAt),\n open: false,\n held: false,\n managed: false,\n })\n }\n } catch {\n // This repository's git is quiet; the held anchors still show.\n }\n }\n return statuses\n }\n\n /** The anchor a caller's id names, wherever it is recorded. */\n const entryById = async (anchorId: AnchorId): Promise<AnchorRecord | undefined> =>\n anchorId.startsWith(LOCAL_ID_PREFIX) ? await localEntry(deps, anchorId) : deps.anchors.get(anchorId)\n\n return {\n async create(draft) {\n if (deps.isLocalNode(draft.nodeId)) return await createLocalWorktree(deps, draft)\n const channel = channelFor(draft.nodeId)\n // One spelling of the repository, settled before anything is written: it\n // is what the worktree path, the anchor, and the repository record carry,\n // so a later lookup by path — the removal guard, the section's tree —\n // finds the same directory the caller meant.\n const { canonicalPath: repoPath } = await channel.request('fs.resolve', { path: draft.repoPath })\n const branch = branchFor(draft.name)\n const worktreePath = draft.path ?? managedWorktreePath(deps.worktreeRoot(draft.nodeId), repoPath, draft.name)\n const worktree: WireWorktree = await channel.request('git.worktreeAdd', {\n repoPath,\n worktreePath,\n branch,\n ...draft.baseRef === undefined ? {} : { baseRef: draft.baseRef },\n })\n\n // The daemon reports where the checkout actually landed and which branch\n // it settled on, which are the facts every later call must use.\n const checkedOut = worktree.branch ?? branch\n const anchor = await deps.anchors.create({\n kind: 'worktree',\n nodeId: draft.nodeId,\n name: draft.name,\n repoPath,\n remoteRoot: worktree.path,\n branch: checkedOut,\n origin: 'created',\n })\n await registerRepoIfUnknown(deps, draft.nodeId, repoPath)\n await registerWorkspace(deps, anchor)\n return { ...anchor, kind: 'worktree', branch: checkedOut }\n },\n\n async list() {\n // The local machine leads the list, as it leads the machine list: its\n // rows are read here rather than asked of anything.\n const statuses: WorktreeStatus[] = [...await localStatuses(deps)]\n const held = new Set<string>()\n for (const anchor of deps.anchors.list()) {\n // Listing is a local read. The branch a checkout sits on and whether it\n // is dirty belong to the machine's own git, so the only thing worth\n // reporting here is whether the worktree can be reached at all. One\n // anchor that cannot be read becomes its own error, never the list's.\n const offline = deps.channel(anchor.nodeId) === undefined\n ? `node \"${anchor.nodeId}\" is not connected`\n : undefined\n statuses.push(await rowStatus(deps, anchor, offline))\n if (anchor.kind === 'worktree') held.add(`${anchor.nodeId}\\u0000${anchor.remoteRoot}`)\n }\n // A machine's git is the second source: a checkout nobody adopted yet\n // shows as a row too, so the panel never waits on records made by hand.\n statuses.push(...await discoveredRemote(held))\n return statuses\n },\n\n existing: listExisting,\n\n async adopt(ref, path) {\n if (deps.isLocalNode(ref.nodeId)) {\n const listed = await listExisting(ref)\n if (!listed.some(candidate => candidate.path === path)) {\n throw new Error(`\"${path}\" is not a worktree of \"${ref.repoPath}\" on that machine`)\n }\n // A local checkout is already a row here; adopting it only opens it.\n const local = (await localStatuses(deps)).find(status =>\n status.anchor.nodeId === ref.nodeId && status.anchor.anchorPath === path)?.anchor\n if (local === undefined || local.kind !== 'worktree') {\n throw new Error(`no local checkout \"${path}\"`)\n }\n return await openAsWorkspace(deps, local)\n }\n return await adoptRemote(ref, path)\n },\n\n async anchorsIn(ref) {\n if (deps.isLocalNode(ref.nodeId)) {\n return (await localStatuses(deps))\n .filter(status => status.anchor.repoPath === ref.repoPath)\n .map(status => status.anchor)\n }\n return deps.anchors.list()\n .filter(anchor => anchor.nodeId === ref.nodeId && anchor.repoPath === ref.repoPath)\n },\n\n async remove(anchorId, options) {\n if (anchorId.startsWith(LOCAL_ID_PREFIX)) {\n const anchor = await localEntry(deps, anchorId)\n if (anchor === undefined) throw new Error(`no worktree \"${anchorId}\" on this machine`)\n return await removeLocalWorktree(deps, requireWorktree(anchor), options)\n }\n const discovered = parseRemoteAnchorId(anchorId)\n if (discovered !== undefined) {\n const held = heldRemote(discovered)\n return held === undefined ? await removeDiscovered(discovered, options) : await removeRemote(held, options)\n }\n const anchor = deps.anchors.get(anchorId)\n if (anchor === undefined) throw new Error(`no anchor \"${anchorId}\"`)\n return await removeRemote(requireWorktree(anchor), options)\n },\n\n async open(anchorId) {\n const discovered = parseRemoteAnchorId(anchorId)\n if (discovered !== undefined) {\n return await adoptRemote({ nodeId: discovered.nodeId, repoPath: discovered.repoPath }, discovered.path)\n }\n const anchor = await entryById(anchorId)\n if (anchor === undefined) throw new Error(`no worktree \"${anchorId}\" on this machine`)\n return await openAsWorkspace(deps, anchor)\n },\n\n async close(anchorId) {\n const discovered = parseRemoteAnchorId(anchorId)\n if (discovered !== undefined) {\n const held = heldRemote(discovered)\n if (held === undefined) throw new Error(`no worktree \"${anchorId}\" on this machine`)\n await deps.workspace?.unregister(held)\n return held\n }\n const anchor = await entryById(anchorId)\n if (anchor === undefined) throw new Error(`no worktree \"${anchorId}\" on this machine`)\n await deps.workspace?.unregister(anchor)\n return anchor\n },\n\n async release(anchorId) {\n const discovered = parseRemoteAnchorId(anchorId)\n if (discovered !== undefined) {\n const held = heldRemote(discovered)\n if (held === undefined) throw new Error(`no worktree \"${anchorId}\" on this machine`)\n await deps.workspace?.unregister(held)\n await deps.anchors.remove(held.anchorId)\n return held\n }\n const anchor = await entryById(anchorId)\n if (anchor === undefined) throw new Error(`no worktree \"${anchorId}\" on this machine`)\n // Only this host's bookkeeping goes: a local checkout has no record beyond\n // its registration, and a machine's checkout is not ours to delete.\n await deps.workspace?.unregister(anchor)\n if (!anchorId.startsWith(LOCAL_ID_PREFIX)) await deps.anchors.remove(anchorId)\n return anchor\n },\n\n async openDirectory(ref) {\n const workspace = deps.workspace\n if (workspace === undefined) {\n throw new Error('this deployment composes no workspace registry, so a directory cannot be opened')\n }\n if (deps.isLocalNode(ref.nodeId)) {\n const record = deps.repos.find(ref)\n if (record === undefined) throw new Error(`no repository record for \"${ref.repoPath}\"`)\n return await openAsWorkspace(deps, localDirectoryAnchor(record))\n }\n const existing = directoryAnchorOf(deps, ref)\n if (existing !== undefined) {\n await workspace.register(existing)\n return existing\n }\n\n const channel = channelFor(ref.nodeId)\n const { canonicalPath: repoPath } = await channel.request('fs.resolve', { path: ref.repoPath })\n const anchor = await deps.anchors.create({\n kind: 'directory',\n nodeId: ref.nodeId,\n name: posix.basename(repoPath) || repoPath,\n repoPath,\n // A directory anchor maps the directory onto itself: there is no\n // checkout to distinguish, so the two spellings are one path.\n remoteRoot: repoPath,\n })\n await openOrDrop(deps, anchor, () => workspace.register(anchor))\n return { ...anchor, kind: 'directory' }\n },\n\n async closeDirectory(ref) {\n if (deps.isLocalNode(ref.nodeId)) {\n const record = deps.repos.find(ref)\n if (record === undefined) return undefined\n const anchor = localDirectoryAnchor(record)\n // Closing is the registration going away; a local directory has no\n // record of its own to drop, so an unregistered one is already closed.\n const open = await deps.workspace?.registered(anchor) ?? false\n if (!open) return undefined\n await unregisterWorkspace(deps, anchor)\n return anchor\n }\n const anchor = directoryAnchorOf(deps, ref)\n if (anchor === undefined) return undefined\n // The registration resolves by path, which stops resolving once the\n // anchor directory is gone, so it goes first.\n await unregisterWorkspace(deps, anchor)\n await deps.anchors.remove(anchor.anchorId)\n return anchor\n },\n }\n}\n\n/** What a workspace label is composed from. */\nexport interface WorkspaceLabelParts {\n /** The machine's display title, falling back to its host. */\n readonly machine: string\n /** Absolute POSIX path of the repository on that machine. */\n readonly repoPath: string\n /** The repository's display name, when a record supplies one. */\n readonly repoName?: string | undefined\n /** The checkout's name; absent when the workspace is the directory itself. */\n readonly name?: string | undefined\n}\n\n/** Separator between the three parts. */\nconst SEPARATOR = ' · '\n\n/**\n * Build the display title a remote directory gets as a local workspace.\n *\n * A workspace title is read by a person scanning a sidebar, so it names the\n * things that distinguish one from another — which checkout, which repository,\n * and which machine — and never the opaque ids this plugin routes by. The\n * checkout leads because it is what the person chose and what they are looking\n * for; the machine trails because it is the context they already know. A\n * directory opened as itself has no checkout to name, so its title begins at\n * the repository. An unnamed repository falls back to its last path segment,\n * which is what a user would have called it; a path with no segment at all\n * falls back to the whole path so the label is never blank.\n * @param parts - the machine, repository, and checkout names.\n * @returns the composed title.\n */\nexport function workspaceLabel(parts: WorkspaceLabelParts): string {\n const base = posix.basename(parts.repoPath)\n const repo = parts.repoName?.trim()\n || (base === '' || base === '/' ? parts.repoPath : base)\n const segments = parts.name === undefined ? [repo, parts.machine] : [parts.name, repo, parts.machine]\n return segments.join(SEPARATOR)\n}\n","/**\n * The durable record of the git repositories a user has registered on their\n * machines.\n *\n * A repository is the middle layer of the management tree: a machine holds\n * repositories, and a repository holds the worktrees cut from it. The record\n * stores only what a user chose — which machine, which path, what to call it —\n * because branch and cleanliness are live facts read from the daemon on every\n * listing and would be stale the moment they were written down.\n *\n * @module dsh-remote-workspace/storage/repos\n */\n\nimport { brandString, type Branded } from '@deepseek-ai/dsh-brand'\nimport { posix } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport type { NodeId } from './nodes.ts'\nimport type { DocumentSpec } from './document.ts'\nimport { readDocument, writeDocument } from './document.ts'\n\n/** Document revision; a field change bumps it and refuses the old form. */\nconst DOCUMENT_VERSION = 1\n\n/**\n * One registered repository.\n *\n * Branded so a machine or anchor id cannot be passed where a repository is\n * expected: all three are generated strings that render identically in a log\n * or a URL, and the brand is the only thing that tells them apart. It lives in\n * the type system alone.\n */\nexport type RepoId = Branded<'RepoId'>\n\n/**\n * Admit a string as a repository id.\n *\n * Called where a string first becomes an id: a field a request carried. Every\n * later hop carries the type.\n * @param value - the string that request named.\n * @returns the same string, branded.\n */\nexport function asRepoId(value: string): RepoId {\n return brandString<RepoId>(value)\n}\n\n/** One registered repository. */\nexport interface RepoRecord {\n /** Stable generated id; never the path, so moving a checkout is free. */\n readonly repoId: RepoId\n /** The machine holding the checkout. */\n readonly nodeId: NodeId\n /** Absolute POSIX path of the repository on that machine. */\n readonly repoPath: string\n /** Display name. Defaults to the path's last segment. */\n readonly name: string\n /** ISO-8601 creation instant. */\n readonly createdAt: string\n}\n\n/** A caller's registration request. */\nexport interface RepoDraft {\n /** Existing id to update in place; omitted registers a new repository. */\n readonly repoId?: RepoId\n /** The machine holding the checkout. */\n readonly nodeId: NodeId\n /** Absolute POSIX path of the repository on that machine. */\n readonly repoPath: string\n /** Display name; omitted derives one from the path. */\n readonly name?: string\n}\n\n/** Where one repository lives, as callers address it. */\nexport type RepoRef = Pick<RepoRecord, 'nodeId' | 'repoPath'>\n\n/** The repository store. */\nexport interface RepoStore {\n /**\n * Read the document into memory.\n * @returns the loaded records, in document order.\n * @throws when the JSON is malformed, the version is unsupported, or a\n * record is not one this build wrote.\n */\n load(): Promise<readonly RepoRecord[]>\n /** Every registered repository, in stable document order. */\n list(): readonly RepoRecord[]\n /**\n * One repository by id.\n * @param repoId - the generated record id.\n * @returns the record, or undefined when no repository carries that id.\n */\n get(repoId: RepoId): RepoRecord | undefined\n /**\n * The record already covering one machine path.\n * @param ref - the machine and absolute path.\n * @returns the record, or undefined when that path is not registered.\n */\n find(ref: RepoRef): RepoRecord | undefined\n /**\n * Register or update one repository and persist the result.\n * @param draft - the caller's fields; omitted `repoId` generates one.\n * @returns the stored record.\n */\n upsert(draft: RepoDraft): Promise<RepoRecord>\n /**\n * Drop one repository and persist the result.\n * @param repoId - the record to drop.\n * @returns true when a record was removed.\n */\n remove(repoId: RepoId): Promise<boolean>\n /**\n * Drop every repository of one machine, for machine removal.\n * @param nodeId - the machine whose registrations go away.\n * @returns the number of records removed.\n */\n removeByNode(nodeId: NodeId): Promise<number>\n}\n\nexport interface RepoStoreDeps {\n /** Absolute path of the JSON document. */\n readonly file: string\n /** Injectable clock, so tests do not depend on wall time. */\n readonly now?: () => Date\n}\n\n/** Whether an unknown parsed value is a record this module wrote. */\nfunction isRepoRecord(value: unknown): value is RepoRecord {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<Record<keyof RepoRecord, unknown>>\n return typeof record.repoId === 'string'\n && typeof record.nodeId === 'string'\n && typeof record.repoPath === 'string'\n && typeof record.name === 'string'\n && typeof record.createdAt === 'string'\n}\n\n/**\n * The name to show for a path no caller named.\n * @param repoPath - absolute POSIX path of the checkout.\n * @returns the last path segment.\n */\nexport function defaultRepoName(repoPath: string): string {\n return posix.basename(repoPath)\n}\n\n/**\n * Build a repository store over one document.\n * @param deps - the document path and an optional clock.\n * @returns the store; call {@link RepoStore.load} before serving reads.\n */\nexport function createRepoStore(deps: RepoStoreDeps): RepoStore {\n const now = deps.now ?? (() => new Date())\n let repos: RepoRecord[] = []\n let loaded = false\n\n const requireLoaded = (): void => {\n if (!loaded) throw new Error('repository store read before load()')\n }\n\n const document: DocumentSpec<RepoRecord> = {\n file: deps.file,\n version: DOCUMENT_VERSION,\n key: 'repos',\n label: 'repository',\n isRecord: isRepoRecord,\n }\n\n return {\n async load() {\n repos = [...await readDocument(document)]\n loaded = true\n return repos\n },\n\n list() {\n requireLoaded()\n return repos\n },\n\n get(repoId) {\n requireLoaded()\n return repos.find(repo => repo.repoId === repoId)\n },\n\n find(ref) {\n requireLoaded()\n return repos.find(repo => repo.nodeId === ref.nodeId && repo.repoPath === ref.repoPath)\n },\n\n async upsert(draft) {\n requireLoaded()\n const existing = draft.repoId === undefined ? undefined : repos.find(repo => repo.repoId === draft.repoId)\n // A re-registration of the same path keeps the name the user chose; a\n // record moved to another path re-derives it, because a name taken from\n // the old path would misdescribe the new one.\n const kept = existing !== undefined && existing.repoPath === draft.repoPath\n const record: RepoRecord = {\n repoId: existing?.repoId ?? draft.repoId ?? brandString<RepoId>(randomUUID()),\n nodeId: draft.nodeId,\n repoPath: draft.repoPath,\n name: draft.name?.trim() || (kept ? existing.name : '') || defaultRepoName(draft.repoPath),\n createdAt: existing?.createdAt ?? now().toISOString(),\n }\n const next = existing === undefined\n ? [...repos, record]\n : repos.map(repo => (repo.repoId === record.repoId ? record : repo))\n await writeDocument(document, next)\n repos = next\n return record\n },\n\n async remove(repoId) {\n requireLoaded()\n const next = repos.filter(repo => repo.repoId !== repoId)\n if (next.length === repos.length) return false\n await writeDocument(document, next)\n repos = next\n return true\n },\n\n async removeByNode(nodeId) {\n requireLoaded()\n const next = repos.filter(repo => repo.nodeId !== nodeId)\n const removed = repos.length - next.length\n if (removed === 0) return 0\n await writeDocument(document, next)\n repos = next\n return removed\n },\n }\n}\n","/**\n * The management API behind the plugin's Web routes, and the adapter that\n * mounts it.\n *\n * The rules and the transport are separate halves of one module, in that order:\n * {@link handleNodeApi} takes a normalized request and returns a status plus a\n * JSON body, so validation, which fields may leave the host, and which failures\n * are client errors are all exercised without opening a socket; the adapter\n * below it owns only reading a bounded body, parsing the URL, and writing JSON\n * back.\n *\n * A node's token never appears in a response. {@link toNodeView} is the only\n * projection used here.\n *\n * The route is registered through `ctx.get('webServer')` rather than an\n * injected dependency, because a non-Web profile (headless, SDK) has no HTTP\n * server and the plugin must still load there.\n *\n * @module dsh-remote-workspace/plugin/api\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\n// Type-only: pulls the Web-server plugin's Context merge (ctx.get('webServer')),\n// which is how these routes register without injecting the service.\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport type { NodeConnections, NodeStatus } from '../models/machines.ts'\nimport type { NodeId, NodeRecord, NodeRegistry, NodeTransport } from '../storage/nodes.ts'\nimport { asNodeId, toNodeView } from '../storage/nodes.ts'\nimport { asAnchorId } from '../storage/anchors.ts'\nimport { asRepoId } from '../storage/repos.ts'\nimport type { RepoRecord, RepoStore } from '../storage/repos.ts'\nimport { NodeRequestError } from '../remote/client.ts'\nimport type { NodeChannel } from '../remote/client.ts'\nimport type { WorktreeManager } from '../models/worktrees.ts'\nimport { homedir } from 'node:os'\nimport type { LocalPathType } from '../local/fs.ts'\nimport { listLocalDir, localPathType, resolveLocalPath } from '../local/fs.ts'\nimport { isRepository } from '../local/git.ts'\nimport type { TerminalRegistry } from '../terminal/host/registry.ts'\n\n/** One normalized request, already routed to this API's prefix. */\nexport interface ApiRequest {\n /** Upper-case HTTP method. */\n readonly method: string\n /** Path below the API prefix; always starts with `/`. */\n readonly path: string\n /** Parsed query string. */\n readonly query: URLSearchParams\n /** Parsed JSON body, or undefined when the request carried none. */\n readonly body: unknown\n}\n\n/** One normalized response. */\nexport interface ApiResponse {\n /** HTTP status code. */\n readonly status: number\n /** JSON-serializable body. */\n readonly body: unknown\n}\n\n/** What the API needs from the plugin. */\nexport interface ManagementApiDeps {\n /** Durable node records. */\n readonly registry: NodeRegistry\n /** Durable repository records. */\n readonly repos: RepoStore\n /** Live connections. */\n readonly connections: NodeConnections\n /** The remote worktree lifecycle. */\n readonly worktrees: WorktreeManager\n /**\n * The shells a person's tabs have open.\n *\n * The panel reads the same table the agent's terminal tool addresses, so a\n * terminal a reload left without a tab is still findable — and closable.\n */\n readonly terminals: TerminalRegistry\n /**\n * Where one machine cuts its checkouts, for the panel's default path.\n *\n * It throws while the machine's home is still unknown.\n */\n readonly worktreeRoot: (nodeId: NodeId) => string\n}\n\n/** One repository as the API reports it: the record, and whether git owns it. */\ninterface RepoReport {\n readonly repo: RepoRecord\n /**\n * Whether the directory is inside a git repository on the machine, as of this\n * read. A plain directory is a legitimate record — it can be opened as a\n * workspace and initialized later — so this is asked every time rather than\n * written down at registration.\n */\n readonly git: boolean\n /**\n * Where this repository's machine cuts its checkouts, when its home is\n * already known. The panel shows it as the default path of a new worktree.\n */\n readonly worktreeRoot?: string\n /** Why the question could not be answered, when it could not. */\n readonly error?: string\n}\n\n/** A failure carrying the status this API answers with. */\nclass ApiError extends Error {\n /** HTTP status this failure answers with. */\n readonly status: number\n\n /**\n * @param status - the HTTP status to answer with.\n * @param message - the client-facing reason.\n */\n constructor(status: number, message: string) {\n super(message)\n this.name = 'ApiError'\n this.status = status\n }\n}\n\n/** The named string field of an unknown value, or undefined. */\nfunction stringField(value: unknown, key: string): string | undefined {\n if (typeof value !== 'object' || value === null) return undefined\n const field = (value as Record<string, unknown>)[key]\n return typeof field === 'string' ? field : undefined\n}\n\n/** The failure for a method one of these routes does not answer. */\nfunction notAllowed(request: ApiRequest): ApiError {\n return new ApiError(405, `${request.method} is not allowed on ${request.path}`)\n}\n\n/**\n * Read a required non-empty string field, or fail as a client error.\n * @param body - the parsed request body.\n * @param key - the field name.\n * @returns the trimmed value.\n * @throws ApiError 400 when the field is missing or empty.\n */\nfunction requireString(body: unknown, key: string): string {\n const value = stringField(body, key)?.trim()\n if (value === undefined || value === '') {\n throw new ApiError(400, `\"${key}\" is required and must be a non-empty string`)\n }\n return value\n}\n\n/**\n * Read the SSH destination a caller wants to reach a machine through.\n *\n * Only the destination is required: the SSH port and identity file default to\n * whatever the operator's own `ssh` configuration already says, so a\n * `~/.ssh/config` alias works exactly as written.\n * @param body - the parsed request body.\n * @returns the stored transport.\n * @throws ApiError 400 when the destination is missing or a field is unusable.\n */\nfunction requireTransport(body: unknown): NodeTransport {\n const ssh = typeof body === 'object' && body !== null\n ? (body as Record<string, unknown>)['ssh']\n : undefined\n if (typeof ssh !== 'object' || ssh === null) {\n throw new ApiError(400, '\"ssh\" is required and must name how to reach the machine')\n }\n const fields = ssh as Record<string, unknown>\n const target = typeof fields['target'] === 'string' ? fields['target'].trim() : ''\n if (target === '') {\n throw new ApiError(400, '\"ssh.target\" is required and must be a non-empty string')\n }\n const portField = fields['port']\n if (portField !== undefined\n && (typeof portField !== 'number' || !Number.isInteger(portField) || portField < 1 || portField > 65535)) {\n throw new ApiError(400, '\"ssh.port\" must be between 1 and 65535')\n }\n const identity = typeof fields['identityFile'] === 'string' ? fields['identityFile'].trim() : ''\n return {\n kind: 'ssh',\n target,\n ...portField === undefined ? {} : { sshPort: portField as number },\n ...identity === '' ? {} : { identityFile: identity },\n }\n}\n\n/** One machine's status, as this API reports it. */\nfunction statusOf(connections: NodeConnections, record: NodeRecord): NodeStatus {\n // The local machine is reachable by definition: it is where this process\n // runs, so it has no connection state to report and never a failure.\n return record.transport.kind === 'local'\n ? { nodeId: record.nodeId, state: 'ready' }\n : connections.status(record.nodeId)\n}\n\n/**\n * The live channel to one machine, or the client error this API answers with.\n * @param deps - the management dependencies.\n * @param nodeId - the machine to reach.\n * @returns the live channel.\n * @throws ApiError 409 when the machine is not connected.\n */\nfunction requireChannel(deps: ManagementApiDeps, nodeId: NodeId): NodeChannel {\n const channel = deps.connections.channel(nodeId)\n if (channel === undefined) throw new ApiError(409, `node \"${nodeId}\" is not connected`)\n return channel\n}\n\n/**\n * Resolve a node id to a stored record, or fail as a client error.\n * @param registry - the durable registry.\n * @param nodeId - the path segment.\n * @returns the record.\n * @throws ApiError 404 when no node carries that id.\n */\nfunction requireNode(registry: NodeRegistry, nodeId: NodeId) {\n const record = registry.get(nodeId)\n if (record === undefined) throw new ApiError(404, `no node \"${nodeId}\"`)\n return record\n}\n\n/**\n * Resolve a caller's path against a machine's own filesystem rules.\n * @param deps - the management dependencies.\n * @param record - the machine to ask.\n * @param path - the caller's path, absolute or starting with `~`.\n * @returns the canonical absolute path on that machine.\n * @throws ApiError 409 when the machine is not connected, 502 when it cannot\n * resolve the path.\n */\nasync function resolveOnNode(\n deps: ManagementApiDeps,\n record: NodeRecord,\n path: string,\n): Promise<string> {\n if (record.transport.kind === 'local') return await resolveLocalPath(path)\n const resolved = await requireChannel(deps, record.nodeId).request('fs.resolve', { path })\n return resolved.canonicalPath\n}\n\n/**\n * Read what one path is on a machine.\n * @param deps - the management dependencies.\n * @param record - the machine to ask.\n * @param path - the canonical absolute path to probe.\n * @returns the entry's type, or undefined when nothing is there.\n * @throws ApiError 409 when the machine is not connected.\n */\nasync function statOnNode(\n deps: ManagementApiDeps,\n record: NodeRecord,\n path: string,\n): Promise<LocalPathType | undefined> {\n if (record.transport.kind === 'local') return await localPathType(path)\n return (await requireChannel(deps, record.nodeId).request('fs.stat', { path }))?.type\n}\n\n/**\n * Ask one repository's machine whether git owns that directory.\n *\n * A record outlives the connection to its machine, so an unreachable one still\n * lists; only the answer goes missing, and `error` says why. The daemon's own\n * \"not a repository\" is the one failure that answers the question — everything\n * else means the question could not be put to the machine.\n * @param deps - the management dependencies.\n * @param record - the stored repository.\n * @returns the record and whether it is a repository right now.\n */\nasync function reportRepo(deps: ManagementApiDeps, record: RepoRecord): Promise<RepoReport> {\n // A machine that has not answered its handshake yet has no home to resolve\n // `~` against; the panel then shows no default and the host computes it.\n let root: string | undefined\n try {\n root = deps.worktreeRoot(record.nodeId)\n } catch {\n root = undefined\n }\n // Spread rather than assign: an unknown root is an absent key, not an\n // undefined one, under `exactOptionalPropertyTypes`.\n const placed = root === undefined ? {} : { worktreeRoot: root }\n const node = deps.registry.get(record.nodeId)\n let git = false\n let error: string | undefined\n try {\n if (node?.transport.kind === 'local') {\n git = await isRepository(record.repoPath)\n } else {\n const channel = deps.connections.channel(record.nodeId)\n if (channel === undefined) {\n error = `node \"${record.nodeId}\" is not connected`\n } else {\n await channel.request('git.repoState', { repoPath: record.repoPath })\n git = true\n }\n }\n } catch (caught) {\n // The daemon's own \"not a repository\" answers the question; anything else\n // means it could not be put to the machine.\n if (!(caught instanceof NodeRequestError && caught.data.code === 'GIT_NOT_A_REPOSITORY')) {\n error = caught instanceof Error ? caught.message : String(caught)\n }\n }\n return { repo: record, ...placed, git, ...error === undefined ? {} : { error } }\n}\n\n/**\n * Handle the repository half of the API.\n * @param request - the normalized request.\n * @param parts - path segments below `/repos`.\n * @param deps - the management dependencies.\n * @returns the status and JSON body to answer with.\n */\nasync function handleRepos(\n request: ApiRequest,\n parts: readonly string[],\n deps: ManagementApiDeps,\n): Promise<ApiResponse> {\n const [rawRepoId, action] = parts\n // A route segment is a string from an untrusted request; this is where it\n // becomes an id. Everything below passes the branded value.\n const repoId = rawRepoId === undefined ? undefined : asRepoId(rawRepoId)\n\n if (repoId === undefined) {\n if (request.method === 'GET') {\n const reports = await Promise.all(deps.repos.list().map(repo => reportRepo(deps, repo)))\n return { status: 200, body: { repos: reports } }\n }\n if (request.method === 'POST') {\n // Both required fields are read before any lookup, so a malformed body is\n // always a 400 rather than whichever existence check runs first.\n const nodeId = asNodeId(requireString(request.body, 'nodeId'))\n const requested = requireString(request.body, 'repoPath')\n const node = requireNode(deps.registry, nodeId)\n const repoPath = await resolveOnNode(deps, node, requested)\n // Any directory can be registered: a plain one is opened as a workspace\n // and may become a repository later, so git is not this moment's\n // business. It has to be a directory, though — a file holds no checkout\n // and no workspace.\n const target = await statOnNode(deps, node, repoPath)\n if (target === undefined) throw new ApiError(400, `\"${repoPath}\" does not exist on that machine`)\n if (target !== 'directory') {\n throw new ApiError(400, `\"${repoPath}\" is a ${target} on that machine, not a directory`)\n }\n const existing = deps.repos.find({ nodeId, repoPath })\n const name = stringField(request.body, 'name')?.trim()\n const record = await deps.repos.upsert({\n ...existing === undefined ? {} : { repoId: existing.repoId },\n nodeId,\n repoPath,\n ...name === undefined || name === '' ? {} : { name },\n })\n return { status: existing === undefined ? 201 : 200, body: { repo: await reportRepo(deps, record) } }\n }\n throw notAllowed(request)\n }\n\n const record = deps.repos.get(repoId)\n if (record === undefined) throw new ApiError(404, `no repository \"${repoId}\"`)\n\n const ref = { nodeId: record.nodeId, repoPath: record.repoPath }\n\n // Opening the directory itself is what makes a machine's plain directory a\n // workspace before it is a repository; git is never consulted for it.\n if (action === 'open' || action === 'close') {\n if (request.method !== 'POST') throw notAllowed(request)\n if (action === 'open') {\n return { status: 200, body: { anchor: await deps.worktrees.openDirectory(ref) } }\n }\n return { status: 200, body: { closed: await deps.worktrees.closeDirectory(ref) !== undefined } }\n }\n\n // The checkouts git already knows about for this repository: how one cut by\n // hand, or before this plugin existed, becomes usable. Adopting one records\n // it and opens it; the checkout itself is never written to.\n if (action === 'worktrees') {\n if (request.method === 'GET') {\n return { status: 200, body: { worktrees: await deps.worktrees.existing(ref) } }\n }\n if (request.method === 'POST') {\n const anchor = await deps.worktrees.adopt(ref, requireString(request.body, 'path'))\n return { status: 201, body: { worktree: anchor } }\n }\n throw notAllowed(request)\n }\n\n if (action !== undefined) throw new ApiError(404, `unknown endpoint ${request.method} ${request.path}`)\n\n if (request.method === 'GET') {\n return { status: 200, body: { repo: await reportRepo(deps, record) } }\n }\n if (request.method === 'DELETE') {\n // A worktree is work that only exists in that checkout, so forgetting the\n // repository would strand it; a directory workspace is this host's own\n // bookkeeping and goes with the record.\n const held = (await deps.worktrees.anchorsIn(ref)).filter(anchor => anchor.kind === 'worktree')\n if (held.length > 0) {\n throw new ApiError(\n 409,\n `${String(held.length)} worktree(s) still belong to this repository; remove them first`,\n )\n }\n await deps.worktrees.closeDirectory(ref)\n return { status: 200, body: { deleted: await deps.repos.remove(repoId) } }\n }\n throw notAllowed(request)\n}\n\n/**\n * Handle the worktree half of the API.\n * @param request - the normalized request.\n * @param parts - path segments below `/worktrees`.\n * @param deps - the management dependencies.\n * @returns the status and JSON body to answer with.\n */\nasync function handleWorktrees(\n request: ApiRequest,\n parts: readonly string[],\n deps: ManagementApiDeps,\n): Promise<ApiResponse> {\n const [rawAnchorId, action] = parts\n const anchorId = rawAnchorId === undefined ? undefined : asAnchorId(rawAnchorId)\n\n if (anchorId === undefined) {\n if (request.method === 'GET') {\n return { status: 200, body: { worktrees: await deps.worktrees.list() } }\n }\n if (request.method === 'POST') {\n const rawRepoId = stringField(request.body, 'repoId')?.trim()\n const repoId = rawRepoId === undefined || rawRepoId === '' ? undefined : asRepoId(rawRepoId)\n const target = repoId === undefined\n ? {\n nodeId: asNodeId(requireString(request.body, 'nodeId')),\n repoPath: requireString(request.body, 'repoPath'),\n }\n : (() => {\n const record = deps.repos.get(repoId)\n if (record === undefined) throw new ApiError(404, `no repository \"${repoId}\"`)\n return { nodeId: record.nodeId, repoPath: record.repoPath }\n })()\n const baseRef = stringField(request.body, 'baseRef')\n // A caller may place the checkout itself; the machine's own root is the\n // default. A relative path would be resolved against that root by the\n // daemon, which is never what a caller means here.\n const path = stringField(request.body, 'path')?.trim()\n if (path !== undefined && path !== '' && !path.startsWith('/')) {\n throw new ApiError(400, `\"path\" must be absolute: \"${path}\"`)\n }\n const anchor = await deps.worktrees.create({\n ...target,\n name: requireString(request.body, 'name'),\n ...path === undefined || path === '' ? {} : { path },\n ...baseRef === undefined ? {} : { baseRef },\n })\n return { status: 201, body: { worktree: anchor } }\n }\n throw notAllowed(request)\n }\n\n // Opening, closing, and releasing are workspace and record bookkeeping, not\n // git: the checkout on the machine is untouched by all three.\n if (action === 'open' || action === 'close' || action === 'release') {\n if (request.method !== 'POST') throw notAllowed(request)\n if (action === 'release') {\n return { status: 200, body: { worktree: await deps.worktrees.release(anchorId) } }\n }\n const worktree = action === 'open'\n ? await deps.worktrees.open(anchorId)\n : await deps.worktrees.close(anchorId)\n return { status: 200, body: { worktree } }\n }\n\n if (action === undefined && request.method === 'DELETE') {\n return {\n status: 200,\n body: {\n removal: await deps.worktrees.remove(anchorId, {\n force: request.query.get('force') === 'true',\n // Deleting a branch is an explicit ask: this plugin owns worktrees,\n // not branches, so the default leaves it behind.\n deleteBranch: request.query.get('deleteBranch') === 'true',\n }),\n },\n }\n }\n\n throw new ApiError(404, `unknown endpoint ${request.method} ${request.path}`)\n}\n\n/**\n * Handle the terminal half of the API.\n *\n * The list is the registry's own projection for one Session, so what the panel\n * offers is exactly what the agent's terminal tool would address — including a\n * detached shell whose tab a reload took away. Closing is the registry's kill\n * path: it ends the shell an explicit end names, and an id nobody holds is a\n * client error rather than a silent success.\n * @param request - the normalized request.\n * @param parts - path segments below `/terminals`.\n * @param deps - the management dependencies.\n * @returns the status and JSON body to answer with.\n */\nasync function handleTerminals(\n request: ApiRequest,\n parts: readonly string[],\n deps: ManagementApiDeps,\n): Promise<ApiResponse> {\n const [id, action] = parts\n if (id === undefined) {\n if (request.method !== 'GET') throw notAllowed(request)\n const sessionId = (request.query.get('sessionId') ?? '').trim()\n if (sessionId === '') throw new ApiError(400, '\"sessionId\" is required')\n return { status: 200, body: { terminals: deps.terminals.listFor(sessionId) } }\n }\n if (action !== 'close') throw new ApiError(404, `unknown endpoint ${request.method} ${request.path}`)\n if (request.method !== 'POST') throw notAllowed(request)\n if (!await deps.terminals.kill(id)) throw new ApiError(404, `no terminal \"${id}\"`)\n return { status: 200, body: { closed: true } }\n}\n\n/**\n * Handle one management request.\n * @param request - the normalized request.\n * @param deps - the registry, connection manager, worktree lifecycle, and terminals.\n * @returns the status and JSON body to answer with.\n */\nexport async function handleNodeApi(request: ApiRequest, deps: ManagementApiDeps): Promise<ApiResponse> {\n try {\n const parts = request.path.split('/').filter(segment => segment !== '')\n const head = parts[0]\n\n if (head === 'terminals') return await handleTerminals(request, parts.slice(1), deps)\n if (head === 'worktrees') return await handleWorktrees(request, parts.slice(1), deps)\n if (head === 'repos') return await handleRepos(request, parts.slice(1), deps)\n if (head !== 'nodes') {\n throw new ApiError(404, `unknown endpoint ${request.method} ${request.path}`)\n }\n\n const [, rawNodeId, action] = parts\n const nodeId = rawNodeId === undefined ? undefined : asNodeId(rawNodeId)\n\n if (nodeId === undefined) {\n if (request.method === 'GET') {\n const records = deps.registry.list()\n return {\n status: 200,\n body: {\n nodes: records.map(toNodeView),\n statuses: records.map(record => statusOf(deps.connections, record)),\n },\n }\n }\n if (request.method === 'POST') {\n const title = stringField(request.body, 'title')\n const record = await deps.registry.upsert({\n transport: requireTransport(request.body),\n token: requireString(request.body, 'token'),\n ...title === undefined ? {} : { title },\n })\n return { status: 201, body: { node: toNodeView(record) } }\n }\n throw notAllowed(request)\n }\n\n const record = requireNode(deps.registry, nodeId)\n\n if (action === undefined) {\n if (request.method === 'GET') {\n return { status: 200, body: { node: toNodeView(record), status: statusOf(deps.connections, record) } }\n }\n if (request.method === 'DELETE') {\n // The local machine is not a record: there is nothing to delete, and\n // answering otherwise would suggest it is gone when the next read\n // brings it back.\n if (record.transport.kind === 'local') {\n throw new ApiError(400, 'the local machine is built in and cannot be removed')\n }\n deps.connections.disconnect(nodeId)\n const deleted = await deps.registry.remove(nodeId)\n // A repository is only reachable through its machine, so its records\n // go with it rather than surviving as entries that can never load.\n if (deleted) await deps.repos.removeByNode(nodeId)\n return { status: 200, body: { deleted } }\n }\n if (request.method === 'PATCH') {\n if (record.transport.kind === 'local') {\n throw new ApiError(400, 'the local machine is built in and cannot be changed')\n }\n // A patch that names a destination replaces it; one that does not keeps\n // the stored one, so re-pointing a machine is a deliberate act.\n const ssh = typeof request.body === 'object' && request.body !== null\n ? (request.body as Record<string, unknown>)['ssh']\n : undefined\n const updated = await deps.registry.upsert({\n nodeId,\n transport: ssh === undefined ? record.transport : requireTransport(request.body),\n token: stringField(request.body, 'token') ?? record.token,\n title: stringField(request.body, 'title') ?? record.title,\n })\n return { status: 200, body: { node: toNodeView(updated) } }\n }\n throw notAllowed(request)\n }\n\n if (action === 'connect' || action === 'disconnect') {\n if (request.method !== 'POST') throw notAllowed(request)\n // Connecting this host, and disconnecting it, are both already true: it\n // answers without a connection, so neither needs to do anything.\n if (record.transport.kind !== 'local') {\n if (action === 'disconnect') deps.connections.disconnect(nodeId)\n else await deps.connections.connect(record)\n }\n return { status: 200, body: { status: statusOf(deps.connections, record) } }\n }\n\n if (action === 'dirs') {\n if (request.method !== 'GET') throw notAllowed(request)\n // A request without a path starts at the machine user's home: the home the\n // handshake reported for a node, and this user's home for this host.\n const requested = (request.query.get('path') ?? '').trim()\n if (record.transport.kind === 'local') {\n const path = await resolveLocalPath(requested === '' ? homedir() : requested)\n return { status: 200, body: { path, entries: await listLocalDir(path) } }\n }\n // The daemon itself expands no `~`, so the spelling never travels: the\n // home it named is asked for verbatim.\n const path = await resolveOnNode(deps, record, requested === ''\n ? deps.connections.status(nodeId).info?.homedir ?? '/'\n : requested)\n const listing = await requireChannel(deps, nodeId).request('fs.listDir', { path })\n return {\n status: 200,\n body: {\n path,\n entries: listing.map(entry => ({\n name: entry.name,\n type: entry.type,\n path: entry.target.canonicalPath,\n ...entry.size === undefined ? {} : { size: entry.size },\n })),\n },\n }\n }\n\n throw new ApiError(404, `unknown endpoint ${request.method} ${request.path}`)\n } catch (error) {\n if (error instanceof ApiError) return { status: error.status, body: { error: error.message } }\n return {\n status: 502,\n body: { error: error instanceof Error ? error.message : String(error) },\n }\n }\n}\n\n/** The path prefix this plugin owns. */\nconst API_PREFIX = '/dsh-remote-workspace'\n\n/** Bound on one management request body. */\nconst MAX_BODY_BYTES = 1 << 20\n\n/** Read a bounded request body and parse it as JSON. */\nasync function readJsonBody(request: IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = []\n let total = 0\n for await (const chunk of request) {\n const buffer = chunk as Buffer\n total += buffer.length\n if (total > MAX_BODY_BYTES) throw new Error('request body is too large')\n chunks.push(buffer)\n }\n if (total === 0) return undefined\n return JSON.parse(Buffer.concat(chunks).toString('utf8'))\n}\n\nfunction writeResponse(response: ServerResponse, result: ApiResponse): void {\n const payload = JSON.stringify(result.body)\n response.writeHead(result.status, {\n 'content-type': 'application/json; charset=utf-8',\n 'content-length': Buffer.byteLength(payload),\n 'cache-control': 'no-store',\n })\n response.end(payload)\n}\n\n/**\n * Register the management routes on the host's Web server.\n *\n * A missing Web server is not a failure: it means this profile serves no\n * browser, and the plugin's model-facing behavior is unaffected.\n * @param ctx - the host context.\n * @param deps - the registry and connection manager the API reads.\n */\nexport function registerNodeApi(ctx: Context, deps: ManagementApiDeps): void {\n const webServer = ctx.get('webServer')\n if (webServer === undefined) return\n\n ctx.effect(() => webServer.register({\n kind: 'prefix',\n path: API_PREFIX,\n handler: async (request: IncomingMessage, response: ServerResponse): Promise<void> => {\n const url = new URL(request.url ?? '/', 'http://localhost')\n const path = url.pathname.slice(API_PREFIX.length)\n let decoded: string\n try {\n // Each segment is decoded on its own, after the split, so an encoded\n // separator cannot invent a segment boundary. A caller percent-encodes\n // an id — the browser does, because an id is one opaque token — and an\n // id that arrived encoded would never match anything.\n decoded = path === ''\n ? '/'\n : path.split('/').map(segment => decodeURIComponent(segment)).join('/')\n } catch {\n writeResponse(response, { status: 400, body: { error: `${path} is not valid percent-encoding` } })\n return\n }\n let body: unknown\n try {\n body = await readJsonBody(request)\n } catch (error) {\n writeResponse(response, {\n status: 400,\n body: { error: error instanceof Error ? error.message : String(error) },\n })\n return\n }\n const result = await handleNodeApi({\n method: request.method ?? 'GET',\n path: decoded,\n query: url.searchParams,\n body,\n }, deps)\n writeResponse(response, result)\n },\n }))\n}\n","/**\n * The routing filesystem the plugin registers as `ctx.fs`.\n *\n * It is a plain object, not a subclass: `ctx.provide('fs', …)` is the primitive\n * Cordis' own `Service` constructor calls, so no implementation class — not\n * even the abstract seam class — is inherited here. `FileSystem` supplies the\n * contract type only, and `FileSystemContract` narrows it to the members this\n * provider must implement.\n *\n * The local branch delegates to the factory `SandboxedFileSystem` instance the\n * caller composed in an isolated scope, so sandbox fencing, atomic publication,\n * version guards, and cross-chunk decoding keep their shipped behavior. The\n * remote branch forwards to the node's daemon and maps its answers back onto\n * the same seam vocabulary.\n *\n * @module dsh-remote-workspace/plugin/routing/fs\n */\n\nimport type { FileSystem, FsDirEntry, FsEditOutcome, FsEditRequest, FsInfo, FsPathInfo, FsWriteOutcome } from '@deepseek-ai/dsh-fs'\nimport { FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'\nimport type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'\nimport { TARGET_KEY_PREFIX, isFsErrorCode } from '../../remote/protocol.ts'\nimport type { ChannelLookup, NodeChannel } from '../../remote/client.ts'\nimport { NodeRequestError } from '../../remote/client.ts'\nimport { asNodeId } from '../../storage/nodes.ts'\nimport type { NodeId } from '../../storage/nodes.ts'\nimport type { AnchorRoute } from '../../storage/anchors.ts'\nimport { ambiguousPathMessage, classifyPath, isWithin } from '../../models/routing.ts'\n\n/** Bytes per remote text pull. Bounds one round trip without capping file size. */\nconst TEXT_CHUNK_BYTES = 1 << 20\n\n/** Remote paths `workspace-write` always permits, matching the local provider's temp allowance. */\nconst REMOTE_TEMP_ROOT = '/tmp'\n\n/**\n * The members this provider implements, narrowed from the seam class so the\n * object literal is checked against the real contract without inheriting the\n * `Service` members that make the class nominally typed.\n */\nexport type FileSystemContract = Pick<\n FileSystem,\n | 'resolve'\n | 'processPath'\n | 'processPathFromHostPath'\n | 'fileUrl'\n | 'contains'\n | 'stat'\n | 'lstat'\n | 'readText'\n | 'streamText'\n | 'readBytes'\n | 'readByteRange'\n | 'listDir'\n | 'writeText'\n | 'editText'\n | 'sandboxMode'\n>\n\n/** What the routing filesystem needs from its owner. */\nexport interface RoutingFileSystemDeps {\n /** The composed factory implementation serving every local path. */\n readonly localFs: FileSystem\n /** Every anchor this plugin currently owns. */\n readonly anchors: () => readonly AnchorRoute[]\n /** Resolves the live channel for a node; undefined means \"not connected\". */\n readonly channel: ChannelLookup\n}\n\n/** A target key the plugin minted, decomposed back into its two facts. */\ntype ParsedKey =\n | { readonly kind: 'local' }\n | { readonly kind: 'remote'; readonly nodeId: NodeId; readonly remotePath: string }\n\n/** Compose the opaque key the harness passes back to this provider. */\nfunction composeKey(nodeId: NodeId, remotePath: string): FsTargetKey {\n return FsTargetKey(`${TARGET_KEY_PREFIX}${nodeId}:${remotePath}`)\n}\n\n/**\n * Decompose this provider's own key. The seam forbids a *consumer* from\n * parsing a key; the provider that mints one owns its format.\n * @param key - a key this provider previously returned.\n * @returns the local verdict, or the node and remote path for a remote target.\n */\nfunction parseKey(key: FsTargetKey): ParsedKey {\n const raw = key as string\n if (!raw.startsWith(TARGET_KEY_PREFIX)) return { kind: 'local' }\n const rest = raw.slice(TARGET_KEY_PREFIX.length)\n const separator = rest.indexOf(':')\n if (separator <= 0 || !rest.slice(separator + 1).startsWith('/')) return { kind: 'local' }\n // The key is a synthetic path this plugin both writes and parses, so its\n // node half becomes an id again here.\n return { kind: 'remote', nodeId: asNodeId(rest.slice(0, separator)), remotePath: rest.slice(separator + 1) }\n}\n\n/** The remote target the daemon needs, or a typed failure when the node is offline. */\nfunction requireChannel(deps: RoutingFileSystemDeps, nodeId: NodeId) {\n const channel = deps.channel(nodeId)\n if (channel === undefined) {\n throw new FsError(\n `remote node \"${nodeId}\" is not connected; open the machine before using this workspace`,\n 'FS_IO_ERROR',\n )\n }\n return channel\n}\n\n/**\n * Translate a daemon failure into the seam's typed error so callers branch on\n * the same codes they would see locally. A failure outside the filesystem\n * family is left as the transport error it is.\n * @param error - any failure raised by a channel request.\n * @returns the error to throw.\n */\nfunction toFsError(error: unknown): unknown {\n if (!(error instanceof NodeRequestError)) return error\n if (!isFsErrorCode(error.data.code)) return error\n return new FsError(error.data.message, error.data.code)\n}\n\n/**\n * Ask the daemon and hand back its answer.\n *\n * Every remote verb maps a failure the same way, so the mapping lives here\n * rather than at each of them: a daemon failure inside the filesystem family\n * becomes the typed error a caller branches on, and anything else stays the\n * transport error it is.\n * @param deps - the routing filesystem's dependencies.\n * @param nodeId - the machine to ask.\n * @param call - the request to issue against the live channel.\n * @returns the daemon's answer.\n */\nasync function ask<T>(\n deps: RoutingFileSystemDeps,\n nodeId: NodeId,\n call: (channel: NodeChannel) => Promise<T>,\n): Promise<T> {\n try {\n return await call(requireChannel(deps, nodeId))\n } catch (error) {\n throw toFsError(error)\n }\n}\n\n/** Reject an operation whose signal already fired, before any round trip. */\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted === true) throw new FsError('filesystem operation aborted', 'FS_ABORTED')\n}\n\n/**\n * The failure an ambiguous remote path raises. Two nodes commonly share a\n * remote root, and picking one would read on one machine while writing on\n * another, so the path is refused with the spellings that would work.\n * @param route - the ambiguous verdict naming every claiming node.\n * @returns the typed error to throw.\n */\nfunction ambiguousError(route: { readonly remotePath: string; readonly nodeIds: readonly NodeId[] }): FsError {\n return new FsError(ambiguousPathMessage(route), 'FS_IO_ERROR')\n}\n\n/**\n * Pull one remote text file as decoded chunks.\n *\n * The daemon decodes and rejects binary content, so this loop never splits a\n * code point; the plugin only reassembles what it is given.\n * @param channel - the live node channel.\n * @param remotePath - canonical remote path.\n * @param signal - aborts the pull between round trips.\n * @returns the chunk iterable.\n */\nfunction remoteTextStream(\n channel: NodeChannel,\n remotePath: string,\n signal: AbortSignal | undefined,\n): AsyncIterable<string> {\n return (async function* pull() {\n let offset = 0\n for (;;) {\n throwIfAborted(signal)\n let chunk\n try {\n chunk = await channel.request('fs.readTextChunk', {\n path: remotePath,\n offset,\n length: TEXT_CHUNK_BYTES,\n })\n } catch (error) {\n throw toFsError(error)\n }\n if (chunk.text.length > 0) yield chunk.text\n offset = chunk.nextOffset\n if (chunk.eof) return\n }\n })()\n}\n\n/**\n * Whether the resolved policy permits writing `remotePath` on a node.\n *\n * A remote world is not confined by `ctx.sandbox`, so the only enforceable\n * boundary is the one this provider applies per call: `workspace-write` allows\n * the anchor's remote root and the remote temp area, `read-only` allows\n * nothing, and `danger-full-access` allows everything.\n * @param policy - the per-call policy the caller resolved, when it supplied one.\n * @param remotePath - the canonical remote path about to be written.\n * @param remoteRoot - the anchor's remote root, or undefined when unknown.\n * @returns true when the write may proceed.\n */\nfunction remoteWriteAllowed(\n policy: SandboxExecutionPolicy | undefined,\n remotePath: string,\n remoteRoot: string | undefined,\n): boolean {\n if (policy === undefined) return true\n switch (policy.mode) {\n case 'danger-full-access': return true\n case 'read-only': return false\n case 'workspace-write':\n if (isWithin(REMOTE_TEMP_ROOT, remotePath)) return true\n return remoteRoot !== undefined && isWithin(remoteRoot, remotePath)\n }\n}\n\n/**\n * Build the routing filesystem.\n * @param deps - the composed local delegate, the live anchors, and channel lookup.\n * @returns an object satisfying the filesystem seam, ready for `ctx.provide`.\n */\nexport function createRoutingFileSystem(deps: RoutingFileSystemDeps): FileSystemContract {\n /** The anchor whose remote root owns `remotePath`, when one does. */\n const anchorFor = (nodeId: NodeId, remotePath: string): AnchorRoute | undefined =>\n deps.anchors().find(anchor =>\n anchor.nodeId === nodeId && isWithin(anchor.remoteRoot, remotePath))\n\n /**\n * Refuse a remote mutation the per-call policy does not allow.\n * @param nodeId - the machine the path lives on.\n * @param remotePath - the canonical remote path about to be written.\n * @param verb - the operation, for the message.\n * @param policy - the per-call policy the caller resolved, when it supplied one.\n * @throws the typed error naming the policy that refused it.\n */\n const requireRemoteWrite = (\n nodeId: NodeId,\n remotePath: string,\n verb: 'edit' | 'write',\n policy: SandboxExecutionPolicy | undefined,\n ): void => {\n if (remoteWriteAllowed(policy, remotePath, anchorFor(nodeId, remotePath)?.remoteRoot)) return\n throw new FsError(\n `remote ${verb} denied by the ${String(policy?.mode)} policy: ${remotePath}`,\n 'FS_SANDBOX_DENIED',\n )\n }\n\n const router: FileSystemContract = {\n // Delegated for the same reason as the shell executor: this provider really\n // does fence local mutations at the deployment's mode through the composed\n // sandboxed filesystem, and reporting `undefined` would misstate that.\n // Remote writes are bounded by the remote policy instead.\n get sandboxMode(): SandboxMode | undefined {\n return deps.localFs.sandboxMode\n },\n\n async resolve(path, opts) {\n throwIfAborted(opts?.signal)\n const route = classifyPath(path, opts?.cwd, deps.anchors())\n if (route.kind === 'local') return deps.localFs.resolve(path, opts)\n if (route.kind === 'ambiguous') throw ambiguousError(route)\n const resolved = await ask(deps, route.nodeId, channel =>\n channel.request('fs.resolve', { path: route.remotePath }))\n return {\n targetKey: composeKey(route.nodeId, resolved.canonicalPath),\n displayPath: resolved.canonicalPath,\n }\n },\n\n processPath(target) {\n const parsed = parseKey(target.targetKey)\n return parsed.kind === 'local'\n ? deps.localFs.processPath(target)\n : parsed.remotePath\n },\n\n // A host file is not a remote file. Delegating keeps local attachments\n // working; an attachment inside a remote session resolves to a host path\n // the remote world cannot read, and fails there with the daemon's own\n // error rather than silently reading a different file.\n processPathFromHostPath(hostPath) {\n return deps.localFs.processPathFromHostPath(hostPath)\n },\n\n fileUrl(target) {\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') return deps.localFs.fileUrl(target)\n return `file://${parsed.remotePath.split('/').map(encodeURIComponent).join('/')}`\n },\n\n contains(parent, child) {\n const left = parseKey(parent.targetKey)\n const right = parseKey(child.targetKey)\n if (left.kind === 'local' && right.kind === 'local') return deps.localFs.contains(parent, child)\n if (left.kind === 'local' || right.kind === 'local') return false\n return left.nodeId === right.nodeId && isWithin(left.remotePath, right.remotePath)\n },\n\n async stat(target, signal) {\n throwIfAborted(signal)\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') return deps.localFs.stat(target, signal)\n const info = await ask(deps, parsed.nodeId, channel =>\n channel.request('fs.stat', { path: parsed.remotePath }))\n if (info === null) return undefined\n return {\n version: FsVersion(info.version),\n type: info.type,\n ...info.size === undefined ? {} : { size: info.size },\n } satisfies FsInfo\n },\n\n async lstat(path, opts, signal) {\n throwIfAborted(signal)\n const route = classifyPath(path, opts?.cwd, deps.anchors())\n if (route.kind === 'local') return deps.localFs.lstat(path, opts, signal)\n if (route.kind === 'ambiguous') throw ambiguousError(route)\n const info = await ask(deps, route.nodeId, channel =>\n channel.request('fs.lstat', { path: route.remotePath }))\n if (info === null) return undefined\n return {\n version: FsVersion(info.version),\n type: info.type,\n ...info.size === undefined ? {} : { size: info.size },\n } satisfies FsPathInfo\n },\n\n async readText(target, signal) {\n throwIfAborted(signal)\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') return deps.localFs.readText(target, signal)\n const chunks: string[] = []\n const stream = remoteTextStream(\n requireChannel(deps, parsed.nodeId),\n parsed.remotePath,\n signal,\n )\n for await (const chunk of stream) chunks.push(chunk)\n return chunks.join('')\n },\n\n async streamText(target, signal) {\n throwIfAborted(signal)\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') return deps.localFs.streamText(target, signal)\n return remoteTextStream(\n requireChannel(deps, parsed.nodeId),\n parsed.remotePath,\n signal,\n )\n },\n\n async readBytes(target, signal, maxBytes) {\n throwIfAborted(signal)\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') return deps.localFs.readBytes(target, signal, maxBytes)\n const bytes = await ask(deps, parsed.nodeId, channel =>\n channel.request('fs.readBytes', { path: parsed.remotePath, maxBytes }))\n return new Uint8Array(Buffer.from(bytes.data, 'base64'))\n },\n\n async readByteRange(target, range, signal) {\n throwIfAborted(signal)\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') return deps.localFs.readByteRange(target, range, signal)\n const bytes = await ask(deps, parsed.nodeId, channel =>\n channel.request('fs.readByteRange', {\n path: parsed.remotePath,\n offset: range.offset,\n length: range.length,\n }))\n return new Uint8Array(Buffer.from(bytes.data, 'base64'))\n },\n\n async listDir(target, signal) {\n throwIfAborted(signal)\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') return deps.localFs.listDir(target, signal)\n const entries = await ask(deps, parsed.nodeId, channel =>\n channel.request('fs.listDir', { path: parsed.remotePath }))\n return entries.map((entry): FsDirEntry => ({\n name: entry.name,\n type: entry.type,\n target: {\n targetKey: composeKey(parsed.nodeId, entry.target.canonicalPath),\n displayPath: entry.target.canonicalPath,\n },\n ...entry.version === undefined ? {} : { version: FsVersion(entry.version) },\n ...entry.size === undefined ? {} : { size: entry.size },\n }))\n },\n\n async writeText(target, content, expected, signal, sandboxPolicy) {\n throwIfAborted(signal)\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') {\n return deps.localFs.writeText(target, content, expected, signal, sandboxPolicy)\n }\n requireRemoteWrite(parsed.nodeId, parsed.remotePath, 'write', sandboxPolicy)\n const outcome = await ask(deps, parsed.nodeId, channel => channel.request('fs.writeText', {\n path: parsed.remotePath,\n content,\n ...expected === undefined ? {} : {\n expected: expected.kind === 'createIfAbsent'\n ? { kind: 'createIfAbsent' as const }\n : { kind: 'replaceIfVersion' as const, version: expected.version as string },\n },\n }))\n return {\n operation: outcome.operation,\n version: FsVersion(outcome.version),\n before: outcome.before,\n after: outcome.after,\n } satisfies FsWriteOutcome\n },\n\n async editText(target, edit: FsEditRequest, expected, signal, sandboxPolicy) {\n throwIfAborted(signal)\n const parsed = parseKey(target.targetKey)\n if (parsed.kind === 'local') {\n return deps.localFs.editText(target, edit, expected, signal, sandboxPolicy)\n }\n requireRemoteWrite(parsed.nodeId, parsed.remotePath, 'edit', sandboxPolicy)\n const outcome = await ask(deps, parsed.nodeId, channel => channel.request('fs.editText', {\n path: parsed.remotePath,\n edit: { oldString: edit.oldString, newString: edit.newString, replaceAll: edit.replaceAll },\n ...expected === undefined ? {} : { expected: { version: expected.version as string } },\n }))\n return {\n version: FsVersion(outcome.version),\n before: outcome.before,\n after: outcome.after,\n } satisfies FsEditOutcome\n },\n }\n\n return router\n}\n","/**\n * The routing bash executor the plugin registers as `ctx.shell`.\n *\n * A plain object, like the other two routers. It composes **two** factory\n * executors rather than one: the sandboxed executor for local work, and the\n * bare one for remote work.\n *\n * The bare delegate is not redundant. `bash-sandbox` resolves its argv through\n * `ctx.sandbox`, which wraps a process for **this host's** kernel; handing that\n * argv to a remote cwd would ship a bwrap invocation to the node. On the node\n * the machine itself is the boundary, so the remote branch must take the path\n * that never asks the host sandbox anything.\n *\n * The request/spec split stays the delegate's: whichever executor owns the\n * workdir does both the resolving and the running, so timeout, output caps, and\n * managed-environment handling keep their shipped behavior on both sides.\n *\n * @module dsh-remote-workspace/plugin/routing/shell\n */\n\nimport type {\n ShellExecRequest,\n ShellExecSpec,\n ShellExecutor,\n ShellProcess,\n ShellRunResult,\n} from '@deepseek-ai/dsh-shell'\nimport type { SandboxMode } from '@deepseek-ai/dsh-sandbox'\nimport type { AnchorRoute } from '../../storage/anchors.ts'\nimport { classifyPath } from '../../models/routing.ts'\n\n/**\n * The members this provider implements, narrowed from the seam class so the\n * object literal is checkable without inheriting `Service`.\n */\nexport type ShellExecutorContract = Pick<\n ShellExecutor,\n 'resolve' | 'run' | 'start' | 'sandboxMode'\n>\n\n/** What the routing executor needs from its owner. */\nexport interface RoutingShellDeps {\n /** The composed sandboxed executor serving every local workdir. */\n readonly localShell: ShellExecutor\n /** The composed bare executor serving every remote workdir. */\n readonly remoteShell: ShellExecutor\n /** Every anchor this plugin currently owns. */\n readonly anchors: () => readonly AnchorRoute[]\n}\n\n/**\n * Build the routing bash executor.\n * @param deps - the two composed delegates and the live anchors.\n * @returns an object satisfying the shell seam, ready for `ctx.provide`.\n */\nexport function createRoutingShellExecutor(deps: RoutingShellDeps): ShellExecutorContract {\n /**\n * The delegate that owns one workdir.\n * @param workdir - the resolved working directory of the request or spec.\n * @returns the machine's executor for a remote path, this host's otherwise.\n */\n const delegateFor = (workdir: string): ShellExecutor =>\n classifyPath(workdir, undefined, deps.anchors()).kind === 'remote' ? deps.remoteShell : deps.localShell\n\n return {\n // The fact is \"the mode this executor confines at by default\", and the\n // executor genuinely confines every LOCAL command at the deployment's mode\n // through the sandboxed delegate. Reporting `undefined` instead would make\n // the plugin uncomposable with `dsh-base`: `permission-presets` refuses to\n // mount over an executor that claims not to confine at all. The remote\n // branch is bounded by the machine.\n get sandboxMode(): SandboxMode | undefined {\n return deps.localShell.sandboxMode\n },\n\n resolve(request: ShellExecRequest): ShellExecSpec {\n return delegateFor(request.workdir ?? '').resolve(request)\n },\n\n run(spec: ShellExecSpec): Promise<ShellRunResult> {\n return delegateFor(spec.workdir).run(spec)\n },\n\n start(spec: ShellExecSpec): ShellProcess {\n return delegateFor(spec.workdir).start(spec)\n },\n }\n}\n","/**\n * The remote terminal provider: a PTY a node daemon owns.\n *\n * A terminal on another machine cannot be streamed: the wire answers one\n * request at a time, and the daemon retains a bounded window of output rather\n * than pushing it. This provider therefore polls that window into a local\n * `PassThrough`, so a consumer reads the same `Readable` it would read from a\n * local PTY, one poll interval behind.\n *\n * What it drives is a port, not the harness wire directly: {@link TtyWire} is\n * the six terminal methods this provider needs, and the plugin that owns the\n * protocol adapts its channel to it. The shapes a port declares and the shapes\n * that adapter passes are checked against each other in one place, so a wire\n * that drifts fails to compile rather than failing at a terminal.\n *\n * @module dsh-remote-workspace/remote/tty\n */\n\nimport { StringDecoder } from 'node:string_decoder'\nimport { PassThrough } from 'node:stream'\nimport { DEFAULT_TTY_GRACE_MS } from '../tty.ts'\nimport type { TtyHandle, TtyOutcome, TtySpawnRequest } from '../tty.ts'\n\n/**\n * How often the proxy asks the daemon for new output.\n *\n * The wire serves retained windows rather than pushing, so this is the\n * interactive latency floor: small enough that a prompt appears promptly, large\n * enough that an idle terminal does not flood the connection.\n */\nconst POLL_MS = 40\n\n/** One terminal allocation, as the daemon's wire carries it. */\nexport interface TtyWireSpawnRequest {\n /** Executable and arguments; the daemon never shell-interprets them. */\n readonly argv: readonly string[]\n /** Absolute working directory in the daemon's filesystem. */\n readonly cwd: string\n /** Initial terminal row count. */\n readonly rows: number\n /** Initial terminal column count. */\n readonly cols: number\n /** TERM-to-KILL cleanup grace for the terminal session; the daemon requires it. */\n readonly graceMs: number\n /** Explicit environment entries layered over the daemon's own base. */\n readonly env?: Readonly<Record<string, string>>\n}\n\n/** What the daemon answered for one allocation. */\nexport interface TtyWireStarted {\n /** The session the daemon minted. */\n readonly termId: string\n /** Process id on the daemon's machine. */\n readonly pid: number\n}\n\n/** One read of a terminal's retained output window. */\nexport interface TtyWireRead {\n /** Raw bytes from the requested offset, base64. */\n readonly data: string\n /** Whole-stream byte offset to resume from. */\n readonly nextOffset: number\n}\n\n/** Exit facts of one closed terminal. */\nexport interface TtyWireOutcome {\n /** Exit code; null when a signal ended it. */\n readonly exitCode: number | null\n /** Terminating signal name; null on a normal exit. */\n readonly signal: string | null\n}\n\n/**\n * The terminal methods of one node's wire.\n *\n * Every method rejects when the node cannot be reached, which is what lets the\n * proxy settle a terminal whose transport is gone instead of polling forever.\n */\nexport interface TtyWire {\n /** Allocate one terminal and start the program in it. */\n spawn(request: TtyWireSpawnRequest): Promise<TtyWireStarted>\n /** Read retained output from one whole-stream byte offset. */\n read(termId: string, fromByte: number): Promise<TtyWireRead>\n /** Deliver input bytes. */\n write(termId: string, data: string): Promise<void>\n /** Adopt a new window size. */\n resize(termId: string, cols: number, rows: number): Promise<void>\n /** Release the terminal, escalating to a kill after its grace period. */\n terminate(termId: string): Promise<void>\n /** The exit facts, or null while the terminal is still running or unknown. */\n outcome(termId: string): Promise<TtyWireOutcome | null>\n}\n\n/**\n * Allocate one terminal on a node and proxy its live output.\n *\n * The handle settles once, with the facts it has: a terminal that exited, one\n * that was released, and one whose transport dropped all end the same way, and\n * a consumer that wanted the difference reads it from what came back rather\n * than waiting for output that can no longer arrive.\n * @param wire - the node's terminal methods.\n * @param request - what to run, where, how large, and how long to wait.\n * @param verbs - extra daemon verbs to hang on the same handle, for a seam that\n * has more than this port names e.g. the subprocess seam's foreground verbs.\n * @returns the live handle, carrying whatever `verbs` added.\n * @throws when the allocation itself fails, so a caller learns before it holds a handle.\n */\nexport async function createRemoteTty(wire: TtyWire, request: TtySpawnRequest): Promise<TtyHandle>\nexport async function createRemoteTty<Extra extends object>(\n wire: TtyWire,\n request: TtySpawnRequest,\n verbs: (termId: string) => Extra,\n): Promise<TtyHandle & Extra>\nexport async function createRemoteTty(\n wire: TtyWire,\n request: TtySpawnRequest,\n verbs?: (termId: string) => object,\n): Promise<TtyHandle> {\n const started = await wire.spawn({\n argv: [...request.argv],\n cwd: request.cwd,\n rows: request.rows,\n cols: request.cols,\n graceMs: request.graceMs ?? DEFAULT_TTY_GRACE_MS,\n ...request.env === undefined ? {} : { env: request.env },\n })\n\n const output = new PassThrough()\n // A poll reads a raw byte window, so it can end in the middle of a character;\n // the decoder holds the partial sequence until the next poll completes it.\n const decoder = new StringDecoder('utf8')\n let offset = 0\n let finished = false\n let pumping = false\n let timer: NodeJS.Timeout | undefined\n\n let settleDone: (outcome: TtyOutcome) => void = () => {}\n const done = new Promise<TtyOutcome>((resolve) => { settleDone = resolve })\n\n /** Publish the outcome once and stop polling. */\n const finish = (outcome: TtyOutcome): void => {\n if (finished) return\n finished = true\n if (timer !== undefined) clearInterval(timer)\n output.end(decoder.end())\n settleDone(outcome)\n }\n\n /** Pull whatever the daemon has written since the last offset. */\n const pull = async (): Promise<void> => {\n const read = await wire.read(started.termId, offset)\n offset = read.nextOffset\n if (read.data.length > 0) output.write(decoder.write(Buffer.from(read.data, 'base64')))\n }\n\n /** One poll: pull what the daemon retains, then ask whether it exited. */\n const tick = async (): Promise<void> => {\n if (pumping || finished) return\n pumping = true\n try {\n await pull()\n const outcome = await wire.outcome(started.termId)\n if (outcome !== null) {\n finish({ exitCode: outcome.exitCode, signal: outcome.signal as NodeJS.Signals | null })\n }\n } catch {\n // A dropped transport ends the terminal: the handle settles rather than\n // hanging on output that can no longer arrive.\n finish({ exitCode: null, signal: null })\n } finally {\n pumping = false\n }\n }\n\n timer = setInterval(() => void tick(), POLL_MS)\n // A terminal must not hold the host open by itself; the caller releases it.\n timer.unref()\n void tick()\n\n /** The teardown in flight, so a second `terminate` joins the first. */\n let stopping: Promise<void> | undefined\n\n const handle: TtyHandle = {\n pid: started.pid,\n output,\n done,\n async write(data: string): Promise<void> {\n await wire.write(started.termId, data)\n },\n async resize(cols: number, rows: number): Promise<void> {\n await wire.resize(started.termId, cols, rows)\n },\n async terminate(): Promise<void> {\n stopping ??= (async () => {\n if (finished) return\n await wire.terminate(started.termId)\n // One last pull, so output produced during teardown is not lost.\n await pull().catch(() => undefined)\n // A terminal the daemon no longer knows reads as \"no outcome\": the exit\n // facts are absent, which is exactly what a released terminal has.\n const outcome = await wire.outcome(started.termId).catch(() => null)\n finish({\n exitCode: outcome?.exitCode ?? null,\n signal: (outcome?.signal ?? null) as NodeJS.Signals | null,\n })\n })()\n await stopping\n },\n }\n return verbs === undefined ? handle : { ...handle, ...verbs(started.termId) }\n}\n","/**\n * The machine one working directory belongs to, when it is not this host.\n *\n * The terminal and the subprocess router dispatch on the same answer, so they\n * resolve it through here: a cwd claimed by exactly one node yields that node's\n * live channel, a path two nodes claim is refused, and a machine that is not\n * connected says so. A cwd no anchor claims belongs to this host, and the\n * callers serve it themselves.\n *\n * @module dsh-remote-workspace/plugin/routing/remote\n */\n\nimport type { AnchorRoute } from '../../storage/anchors.ts'\nimport type { ChannelLookup, NodeChannel } from '../../remote/client.ts'\nimport { ambiguousPathMessage, classifyPath } from '../../models/routing.ts'\n\n/** A reachable machine and the path this host asked it about. */\nexport interface RemoteTarget {\n /** The live channel to the machine. */\n readonly channel: NodeChannel\n /** The absolute path on that machine the caller's cwd maps to. */\n readonly remotePath: string\n}\n\n/**\n * Resolve one working directory to the machine that owns it.\n * @param cwd - the working directory the caller asked for.\n * @param anchors - every anchor this plugin currently owns.\n * @param channel - resolves the live channel for a node.\n * @returns the machine and its path, or undefined when this host owns the cwd.\n * @throws when more than one node claims the path, or the node is not connected.\n */\nexport function remoteTarget(\n cwd: string,\n anchors: readonly AnchorRoute[],\n channel: ChannelLookup,\n): RemoteTarget | undefined {\n const route = classifyPath(cwd, undefined, anchors)\n if (route.kind === 'local') return undefined\n if (route.kind === 'ambiguous') throw new Error(ambiguousPathMessage(route))\n const live = channel(route.nodeId)\n if (live === undefined) throw new Error(`remote node \"${route.nodeId}\" is not connected`)\n return { channel: live, remotePath: route.remotePath }\n}\n","/**\n * The routing terminal provider the plugin registers as `ctx.tty`.\n *\n * A working directory decides the machine: one that belongs to a node is served\n * by that node's daemon over the wire, and every other directory by the local\n * provider this plugin composes in an isolated scope. A consumer asks for a\n * terminal in a directory and never asks which machine owns it, exactly as the\n * file, shell, and subprocess routers answer.\n *\n * With no anchor configured every directory is local, so the router is a\n * pass-through to the composed provider.\n *\n * @module dsh-remote-workspace/plugin/routing/tty\n */\n\nimport type { TtyHandle, TtyRuntime, TtySpawnRequest } from '../../tty.ts'\nimport { createRemoteTty } from '../../remote/tty.ts'\nimport type { TtyWire } from '../../remote/tty.ts'\nimport type { ChannelLookup, NodeChannel } from '../../remote/client.ts'\nimport { asTermId } from '../../remote/protocol.ts'\nimport type { AnchorRoute } from '../../storage/anchors.ts'\nimport { remoteTarget } from './remote.ts'\n\n/**\n * The members this provider implements, narrowed from the seam class so the\n * object literal is checkable without inheriting `Service`.\n */\nexport type TtyRuntimeContract = Pick<TtyRuntime, 'spawn'>\n\n/** What the routing terminal provider needs from its owner. */\nexport interface RoutingTtyDeps {\n /** The composed provider serving every local directory. */\n readonly localTty: TtyRuntime\n /** Every anchor this plugin currently owns. */\n readonly anchors: () => readonly AnchorRoute[]\n /** Resolves the live channel for a node; undefined means \"not connected\". */\n readonly channel: ChannelLookup\n}\n\n/**\n * Adapt one node channel to the terminal port.\n *\n * Both seams that allocate a terminal on a node drive the same daemon methods,\n * so they drive them through this one adapter — and this is the one place the\n * daemon's opaque session id becomes the branded id the wire contract carries.\n * @param channel - the live node channel.\n * @returns the port `createRemoteTty` drives.\n */\nexport function terminalWire(channel: NodeChannel): TtyWire {\n return {\n spawn: request => channel.request('term.spawn', request),\n read: (termId, fromByte) => channel.request('term.read', { termId: asTermId(termId), fromByte }),\n // The daemon answers these with an empty object; the port promises nothing\n // back, so the answer is awaited and dropped.\n write: async (termId, data) => {\n await channel.request('term.write', { termId: asTermId(termId), data })\n },\n resize: async (termId, cols, rows) => {\n await channel.request('term.resize', { termId: asTermId(termId), cols, rows })\n },\n terminate: async (termId) => { await channel.request('term.terminate', { termId: asTermId(termId) }) },\n outcome: termId => channel.request('term.outcome', { termId: asTermId(termId) }),\n }\n}\n\n/**\n * Build the routing terminal runtime.\n * @param deps - the composed local provider, the live anchors, and channel lookup.\n * @returns an object satisfying the terminal seam, ready for `ctx.provide`.\n */\nexport function createRoutingTty(deps: RoutingTtyDeps): TtyRuntimeContract {\n return {\n async spawn(request: TtySpawnRequest): Promise<TtyHandle> {\n const remote = remoteTarget(request.cwd, deps.anchors(), deps.channel)\n if (remote === undefined) return await deps.localTty.spawn(request)\n return await createRemoteTty(terminalWire(remote.channel), { ...request, cwd: remote.remotePath })\n },\n }\n}\n","/**\n * The routing subprocess runtime the plugin registers as `ctx.subprocess`.\n *\n * A plain object, like the filesystem router: `ctx.provide` is the primitive\n * Cordis' own `Service` constructor calls, so nothing is inherited here.\n *\n * Two facts drive this module.\n *\n * First, `spawn` returns its handle **synchronously** while a remote start\n * needs a round trip. The handle is therefore a local proxy: it exists\n * immediately, queues `terminate` and `waitForExit` until the daemon has\n * answered, and lets `done` reject when the start itself failed.\n *\n * Second, a collected reader is read **synchronously** while the daemon is\n * reached asynchronously. The proxy keeps a local mirror of each stream and\n * fills it at exit, which is when every documented consumer — the bash\n * executor, the spill policy — actually reads. Collected output that this\n * design cannot fetch without an async reader is fetched once, completely,\n * before `done` settles.\n *\n * @module dsh-remote-workspace/plugin/routing/subprocess\n */\n\nimport { PassThrough } from 'node:stream'\nimport type { Writable } from 'node:stream'\nimport type {\n SubprocessCollectedOutputs,\n SubprocessHandle,\n SubprocessOutcome,\n SubprocessOutputRead,\n SubprocessOutputReader,\n SubprocessRuntime,\n SubprocessSpawnSpec,\n SubprocessTerminalForeground,\n SubprocessTerminalHandle,\n SubprocessTerminalSignal,\n SubprocessTerminalSpawnSpec,\n} from '@deepseek-ai/dsh-subprocess'\nimport { asTermId } from '../../remote/protocol.ts'\nimport type { ProcId, SpPipeFrame } from '../../remote/protocol.ts'\nimport type { ChannelLookup, NodeChannel } from '../../remote/client.ts'\nimport type { AnchorRoute } from '../../storage/anchors.ts'\nimport { terminalWire } from './tty.ts'\nimport { remoteTarget } from './remote.ts'\nimport type { TtySpawnRequest } from '../../tty.ts'\nimport { createRemoteTty } from '../../remote/tty.ts'\n\n/**\n * The members this provider implements, narrowed from the seam class so the\n * object literal is checkable without inheriting `Service`.\n */\nexport type SubprocessRuntimeContract = Pick<\n SubprocessRuntime,\n 'resolveExecutable' | 'spawn' | 'spawnTerminal'\n>\n\n/**\n * A remote terminal, plus the resize the seam has no verb for.\n *\n * `SubprocessTerminalHandle` stops at allocation, text, foreground groups, and\n * teardown, so a consumer that wants to keep a PTY in step with its window has\n * nowhere to ask. This provider publishes the capability beside the seam — the\n * object is still a `SubprocessTerminalHandle`, and a consumer probes for\n * `resize` — which is what `dsh-terminal` does.\n */\nexport interface RemoteTerminalHandle extends SubprocessTerminalHandle {\n /**\n * Ask the daemon to adopt a new terminal size. The kernel signals the\n * foreground process group itself when the size actually changes.\n * @param cols - column count.\n * @param rows - row count.\n */\n resize(cols: number, rows: number): Promise<void>\n}\n\n/** What the routing subprocess runtime needs from its owner. */\nexport interface RoutingSubprocessDeps {\n /** The composed factory implementation serving every local cwd. */\n readonly localProc: SubprocessRuntime\n /** Every anchor this plugin currently owns. */\n readonly anchors: () => readonly AnchorRoute[]\n /** Resolves the live channel for a node. */\n readonly channel: ChannelLookup\n /**\n * Remote binary a packaged ripgrep is rewritten to. The search tools resolve\n * a host-side `rg` and hand that absolute path to this seam, which does not\n * exist on the node.\n */\n readonly remoteRipgrep?: string\n}\n\n/** One stream's local mirror of the daemon's retained window. */\nclass CollectedMirror implements SubprocessOutputReader {\n private readonly chunks: Buffer[] = []\n /** Whole-stream offset of the first retained byte. */\n private start = 0\n /** Whole-stream offset one past the last retained byte. */\n private end = 0\n /** True when an earlier read reported that bytes had already been dropped. */\n private dropped = false\n private readonly maxBytes: number\n\n /**\n * @param maxBytes - the in-memory cap the caller asked the daemon for.\n */\n constructor(maxBytes: number) {\n this.maxBytes = maxBytes\n }\n\n /**\n * Replace the mirror with the window's tail.\n *\n * An answer to an offset that has slid out of the daemon's window carries the\n * retained tail rather than the bytes from that offset, so appending it would\n * splice two ranges into one stream. The tail becomes the whole content, and\n * the loss is reported.\n * @param bytes - the tail the daemon retained.\n * @param nextOffset - whole-stream offset one past `bytes`.\n */\n reset(bytes: Buffer, nextOffset: number): void {\n this.chunks.length = 0\n if (bytes.length > 0) this.chunks.push(bytes)\n this.start = nextOffset - bytes.length\n this.end = nextOffset\n this.dropped = true\n }\n\n /** Append one fetched window and trim the head to the cap. */\n push(bytes: Buffer): void {\n if (bytes.length === 0) return\n this.chunks.push(bytes)\n this.end += bytes.length\n let retained = this.end - this.start\n while (retained > this.maxBytes && this.chunks.length > 0) {\n const head = this.chunks[0]!\n const overflow = retained - this.maxBytes\n if (head.length <= overflow) {\n this.chunks.shift()\n this.start += head.length\n } else {\n this.chunks[0] = head.subarray(overflow)\n this.start += overflow\n }\n this.dropped = true\n retained = this.end - this.start\n }\n }\n\n /** The whole-stream offset a caller should resume from. */\n get nextOffset(): number {\n return this.end\n }\n\n /**\n * Read everything captured since `fromByte`.\n * @param fromByte - whole-stream byte offset to resume from.\n * @returns the delta text, the next offset, and whether the offset was lost.\n */\n readFrom(fromByte: number): SubprocessOutputRead {\n if (fromByte < this.start) {\n return {\n text: Buffer.concat(this.chunks).toString('utf8'),\n nextOffset: this.end,\n lossy: true,\n }\n }\n if (fromByte >= this.end) {\n return { text: '', nextOffset: this.end, lossy: this.dropped }\n }\n const slice = Buffer.concat(this.chunks).subarray(fromByte - this.start)\n return { text: slice.toString('utf8'), nextOffset: this.end, lossy: this.dropped }\n }\n}\n\n/**\n * Absorb a teardown request whose failure cannot matter.\n *\n * Every call runs after the decision to release a process, against a transport\n * that may already be gone; the daemon reaps the process either way, so a\n * rejection carries nothing the caller could act on.\n * @param request - the teardown request already issued.\n * @returns a promise that settles when the request does, whatever its outcome.\n */\nfunction settled(request: Promise<unknown>): Promise<void> {\n return request.then(() => {}, () => {})\n}\n\n/**\n * Build the proxy handle for one remote spawn.\n * @param channel - the live node channel.\n * @param remoteCwd - the canonical remote working directory.\n * @param spec - the caller's fully specified spawn request.\n * @returns the handle, valid before the daemon has answered.\n */\nfunction createRemoteHandle(\n channel: NodeChannel,\n remoteCwd: string,\n spec: SubprocessSpawnSpec,\n): SubprocessHandle {\n const stdoutMirror = typeof spec.stdio.stdout === 'object'\n ? new CollectedMirror(spec.stdio.stdout.maxBytes)\n : undefined\n const stderrMirror = typeof spec.stdio.stderr === 'object'\n ? new CollectedMirror(spec.stdio.stderr.maxBytes)\n : undefined\n\n // A piped stream is pushed, not retained, so its `Readable` is fed straight\n // from the daemon's frames. Registration happens before `sp.spawn` so no\n // chunk can arrive before there is a handler to receive it, and frames that\n // race the spawn answer are held until the id they belong to is known.\n const stdoutPipe = spec.stdio.stdout === 'pipe' ? new PassThrough() : undefined\n const stderrPipe = spec.stdio.stderr === 'pipe' ? new PassThrough() : undefined\n const bufferedFrames: SpPipeFrame[] = []\n let procId: ProcId | undefined\n let offPipe: (() => void) | undefined\n\n const deliverPipeFrame = (frame: SpPipeFrame): void => {\n if (frame.procId !== procId) return\n const target = frame.stream === 'stdout' ? stdoutPipe : stderrPipe\n target?.write(Buffer.from(frame.data, 'base64'))\n }\n\n if (stdoutPipe !== undefined || stderrPipe !== undefined) {\n offPipe = channel.onPipeFrame((frame) => {\n if (procId === undefined) {\n bufferedFrames.push(frame)\n return\n }\n deliverPipeFrame(frame)\n })\n }\n\n /** Stop pushing and end every piped stream, exactly once. */\n const closePipes = (): void => {\n offPipe?.()\n offPipe = undefined\n stdoutPipe?.end()\n stderrPipe?.end()\n }\n\n let startFailure: unknown\n let terminated = false\n\n let resolveDone: (outcome: SubprocessOutcome) => void = () => {}\n let rejectDone: (error: unknown) => void = () => {}\n const done = new Promise<SubprocessOutcome>((resolve, reject) => {\n resolveDone = resolve\n rejectDone = reject\n })\n\n const stdinStream: Writable | undefined = spec.stdio.stdin === 'pipe'\n ? new PassThrough()\n : undefined\n\n /** Fetch every remaining byte of one stream until the daemon stops advancing. */\n const drain = async (id: ProcId, stream: 'stdout' | 'stderr', mirror: CollectedMirror): Promise<void> => {\n for (;;) {\n const read = await channel.request('sp.readOutput', {\n procId: id,\n stream,\n fromByte: mirror.nextOffset,\n })\n const bytes = Buffer.from(read.data, 'base64')\n if (read.lossy) {\n mirror.reset(bytes, read.nextOffset)\n } else {\n if (bytes.length === 0) return\n mirror.push(bytes)\n }\n if (read.nextOffset <= mirror.nextOffset) return\n }\n }\n\n const run = async (): Promise<void> => {\n let id: ProcId\n try {\n const started = await channel.request('sp.spawn', {\n argv: [...spec.argv],\n cwd: remoteCwd,\n stdin: spec.stdio.stdin === 'pipe'\n ? 'pipe'\n : spec.stdio.stdin === 'ignore'\n ? 'ignore'\n : { data: spec.stdio.stdin.data },\n stdout: collectSpec(spec.stdio.stdout),\n stderr: collectSpec(spec.stdio.stderr),\n graceMs: spec.graceMs,\n ...spec.env === undefined ? {} : { env: definedEnv(spec.env) },\n })\n id = started.procId\n procId = id\n // Flush whatever arrived while the id was in flight; order is preserved\n // because the daemon pushes in order on one connection.\n for (const frame of bufferedFrames.splice(0)) deliverPipeFrame(frame)\n } catch (error) {\n startFailure = error\n closePipes()\n rejectDone(error)\n return\n }\n\n if (terminated) await settled(channel.request('sp.terminate', { procId: id }))\n if (typeof spec.stdio.stdin === 'object') {\n await settled(channel.request('sp.writeStdin', { procId: id, data: spec.stdio.stdin.data }))\n await settled(channel.request('sp.closeStdin', { procId: id }))\n }\n if (stdinStream !== undefined) {\n stdinStream.on('data', (chunk: Buffer) => {\n void settled(channel.request('sp.writeStdin', { procId: id, data: chunk.toString('utf8') }))\n })\n stdinStream.on('end', () => {\n void settled(channel.request('sp.closeStdin', { procId: id }))\n })\n }\n\n try {\n await channel.request('sp.waitForExit', { procId: id })\n if (stdoutMirror !== undefined) await drain(id, 'stdout', stdoutMirror)\n if (stderrMirror !== undefined) await drain(id, 'stderr', stderrMirror)\n const outcome = await channel.request('sp.outcome', { procId: id })\n closePipes()\n resolveDone({\n exitCode: outcome?.exitCode ?? null,\n signal: (outcome?.signal ?? null) as NodeJS.Signals | null,\n })\n } catch (error) {\n closePipes()\n rejectDone(error)\n }\n }\n\n void run()\n\n const collected: SubprocessCollectedOutputs = {\n ...stdoutMirror === undefined ? {} : { stdout: stdoutMirror },\n ...stderrMirror === undefined ? {} : { stderr: stderrMirror },\n }\n\n return {\n stdin: stdinStream,\n stdout: stdoutPipe,\n stderr: stderrPipe,\n collected,\n done,\n terminate() {\n terminated = true\n if (procId === undefined) return\n void settled(channel.request('sp.terminate', { procId }))\n },\n async waitForExit(signal?: AbortSignal): Promise<boolean> {\n if (startFailure !== undefined) return false\n // The daemon's own wait is the observable fact; the local `done` settles\n // only after the final drain, which is strictly later.\n await done\n return signal?.aborted !== true\n },\n }\n}\n\n/** Project a seam output disposition onto the wire form. */\nfunction collectSpec(mode: SubprocessSpawnSpec['stdio']['stdout']): 'inherit' | 'pipe' | { maxBytes: number } {\n if (mode === 'pipe') return 'pipe'\n return typeof mode === 'object' ? { maxBytes: mode.maxBytes } : 'inherit'\n}\n\n/** Drop undefined entries from a spawn environment. */\nfunction definedEnv(env: NodeJS.ProcessEnv): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(env)) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\n/**\n * Rewrite a host-only executable path into something the node can run.\n *\n * The search tools resolve a packaged `rg` on the host and hand this seam that\n * absolute path; the node has its own binary under a bare name. Any other\n * absolute path is passed through, and the daemon reports it missing.\n * @param argv - the caller's argv.\n * @param remoteRipgrep - the configured remote binary name.\n * @returns the argv to send.\n */\nfunction rewriteExecutable(argv: readonly string[], remoteRipgrep: string): readonly string[] {\n const head = argv[0]\n if (head === undefined || !head.startsWith('/')) return argv\n const base = head.slice(head.lastIndexOf('/') + 1)\n if (base !== 'rg') return argv\n return [remoteRipgrep, ...argv.slice(1)]\n}\n\n/**\n * Build the routing subprocess runtime.\n * @param deps - the composed local delegate, the live anchors, and channel lookup.\n * @returns an object satisfying the subprocess seam, ready for `ctx.provide`.\n */\nexport function createRoutingSubprocessRuntime(\n deps: RoutingSubprocessDeps,\n): SubprocessRuntimeContract {\n const remoteRipgrep = deps.remoteRipgrep ?? 'rg'\n\n return {\n // Executable lookup carries no working directory, so it cannot be routed:\n // a remote spawn resolves its own executable on the node instead.\n resolveExecutable(command, env, signal) {\n return deps.localProc.resolveExecutable(command, env, signal)\n },\n\n spawn(spec: SubprocessSpawnSpec): SubprocessHandle {\n const remote = remoteTarget(spec.cwd, deps.anchors(), deps.channel)\n if (remote === undefined) return deps.localProc.spawn(spec)\n return createRemoteHandle(\n remote.channel,\n remote.remotePath,\n { ...spec, argv: rewriteExecutable(spec.argv, remoteRipgrep) },\n )\n },\n\n async spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle> {\n const remote = remoteTarget(spec.cwd, deps.anchors(), deps.channel)\n if (remote === undefined) return deps.localProc.spawnTerminal(spec)\n const request: TtySpawnRequest = {\n // A terminal always has a program to run; the subprocess request is the\n // same non-empty argv without the type that says so.\n argv: [...spec.argv] as [string, ...string[]],\n cwd: remote.remotePath,\n cols: spec.cols,\n rows: spec.rows,\n graceMs: spec.graceMs,\n ...spec.env === undefined ? {} : { env: spec.env },\n }\n return await createRemoteTty(terminalWire(remote.channel), request, termId => ({\n async inspectForeground(): Promise<SubprocessTerminalForeground | undefined> {\n return await remote.channel.request('term.inspectForeground', { termId: asTermId(termId) }) ?? undefined\n },\n async signalForeground(signal: SubprocessTerminalSignal): Promise<number> {\n const result = await remote.channel.request('term.signalForeground', {\n termId: asTermId(termId),\n signal,\n })\n return result.processGroupId\n },\n }))\n },\n }\n}\n","/**\n * The terminal's display preferences, named once for both halves.\n *\n * The preferences are the plugin's own settings: they live in the Host's\n * user-settings document, under the namespace below, and the browser half draws\n * them as this plugin's card in the Plugins settings page. Nothing here depends\n * on either half's runtime, so the Host can build its schema from the same\n * bounds the browser steps between.\n *\n * @module dsh-remote-workspace/terminal/shared/display\n */\n\n/** Settings namespace the terminal's display preferences are stored under. */\nexport const TERMINAL_DISPLAY_NAMESPACE = 'dsh-remote-workspace'\n\n/** What one terminal is drawn with. */\nexport interface TerminalDisplaySettings {\n /** The family a terminal is drawn in, by name; one of `monospaceFonts`. */\n readonly fontFamily: string\n /** Cell height in pixels. */\n readonly fontSize: number\n /** Line box as a multiple of the font size. */\n readonly lineHeight: number\n /** Whether the cursor blinks while the shell waits. */\n readonly cursorBlink: boolean\n /** Lines kept in the browser, above what the host retains. */\n readonly scrollback: number\n}\n\n/**\n * The preferences in force before one is chosen.\n *\n * The family is the generic one, which every machine has, until the list this\n * machine offers is read.\n */\nexport const TERMINAL_DISPLAY_DEFAULTS: TerminalDisplaySettings = {\n fontFamily: 'monospace',\n fontSize: 12,\n // One line box per cell. The renderer draws the font's own box-drawing\n // glyphs, which fill the line box and nothing more, so a taller cell parts\n // every vertical run — a tree's guides, a window's frame — into dashes.\n lineHeight: 1,\n cursorBlink: true,\n scrollback: 50_000,\n}\n\n/** The bounds one numeric row steps between, and how far one step moves. */\nexport const TERMINAL_DISPLAY_BOUNDS = {\n fontSize: { min: 11, max: 16, step: 1 },\n lineHeight: { min: 1, max: 1.6, step: 0.1 },\n scrollback: { min: 1_000, max: 100_000, step: 1 },\n} as const\n","/**\n * The terminal's display preferences as a Host settings namespace.\n *\n * Registering the namespace is what lets the browser half bind a scope to it\n * and what makes the plugin's card appear in the Plugins settings page: the\n * page dispatches cards by namespace, and a namespace nobody registered is a\n * card nobody sees. Every field carries its default, so a document that has\n * never held one still describes a working terminal.\n *\n * @module dsh-remote-workspace/terminal/host/display\n */\n\nimport z from '@deepseek-ai/schemastery'\nimport {\n TERMINAL_DISPLAY_BOUNDS,\n TERMINAL_DISPLAY_DEFAULTS,\n type TerminalDisplaySettings,\n} from '../shared/display.ts'\n\nconst { fontSize, lineHeight, scrollback } = TERMINAL_DISPLAY_BOUNDS\n\n/** Durable display schema; also the wire envelope the browser scope validates against. */\nexport const TerminalDisplaySchema: z<TerminalDisplaySettings> = z.object({\n fontFamily: z.string().default(TERMINAL_DISPLAY_DEFAULTS.fontFamily),\n fontSize: z.number().step(fontSize.step).min(fontSize.min).max(fontSize.max)\n .default(TERMINAL_DISPLAY_DEFAULTS.fontSize),\n lineHeight: z.number().step(lineHeight.step).min(lineHeight.min).max(lineHeight.max)\n .default(TERMINAL_DISPLAY_DEFAULTS.lineHeight),\n cursorBlink: z.boolean().default(TERMINAL_DISPLAY_DEFAULTS.cursorBlink),\n scrollback: z.number().step(scrollback.step).min(scrollback.min).max(scrollback.max)\n .default(TERMINAL_DISPLAY_DEFAULTS.scrollback),\n})\n","/**\n * The host's terminals, as a lookup table of the shells a person's tabs have\n * open.\n *\n * A tab owns its shell, but the socket is only a view of it. The socket closing\n * detaches: the terminal stays in the table, its output keeps filling the ring\n * buffer, and the model's tool can still address it for as long as the process\n * lives. An explicit end — the chooser's close route — the process exiting, the\n * owning Session ending, or an optional detach valve releases it; nobody\n * watching is not by itself a reason to end a shell.\n *\n * The table exists for the model. The sidebar terminal never needs to look up\n * its own shell, but the model-facing terminal tool must find one that a\n * person opened — and must not create one. Each terminal therefore carries a\n * registry-unique id (`t1`, `t2`, …) and a human label, and every operation the\n * tool performs passes through {@link TerminalRegistry.requireOwned}, the one\n * place that decides whether the calling Session may touch a terminal at all.\n *\n * Output is retained in a byte ring buffer with absolute offsets, so the tool\n * can read the tail, resume from where it last read, and wait for a match\n * without holding a second PTY or a second stream. The buffer is bounded at\n * {@link BUFFER_BYTES}; the bytes it drops stay accounted for, so an offset\n * never silently changes meaning.\n *\n * @module dsh-remote-workspace/terminal/host/registry\n */\n\nimport { Buffer } from 'node:buffer'\nimport type { TtyHandle, TtyOutcome, TtySpawnRequest } from '../../tty.ts'\n\n/** How this deployment starts a shell. */\nexport interface TerminalSettings {\n /** The program to run, which is the shell the terminal is named after. */\n readonly shell: string\n /** Arguments after the program. */\n readonly shellArgs: readonly string[]\n /** Environment layered onto the provider's ambient scrub. */\n readonly env: Readonly<Record<string, string>>\n /** TERM-to-KILL grace for the whole terminal session, in milliseconds. */\n readonly graceMs: number\n /**\n * Safety valve: how long a terminal whose last socket went away is kept\n * alive, in milliseconds, before it is released.\n *\n * Unset or `0` — the default — keeps it alive for as long as its process\n * lives, because how long a browser is gone says nothing about whether the\n * shell should end. A positive value releases a detached terminal that long\n * after the last sink left, which is a bound the operator opts into.\n */\n readonly detachGraceMs?: number\n}\n\n/** One consumer of a live terminal's output. */\nexport interface TerminalSink {\n /**\n * Receive one chunk, in order.\n *\n * Returning a promise pauses the PTY until it settles, so a slow socket\n * applies backpressure instead of queueing a flood inside this process.\n * @param chunk - the output bytes.\n */\n output(chunk: Buffer): void | Promise<void>\n /**\n * The top-level process ended.\n * @param outcome - how it ended.\n */\n exit(outcome: TtyOutcome): void\n /**\n * The terminal failed before it could report an exit.\n * @param error - the failure.\n */\n fail(error: unknown): void\n}\n\n/** One terminal, as the model-facing list reports it. */\nexport interface TerminalView {\n /** Registry-unique identity; what the model passes as `terminal`. */\n readonly id: string\n /** Human label, also the tab title. */\n readonly label: string\n /** Working directory on the machine that owns the shell. */\n readonly cwd: string\n /** Machine title, for a reader that wants to know where the shell runs. */\n readonly machine: string\n /** Process id on that machine. */\n readonly pid: number\n /**\n * Lifecycle state; a released terminal is absent from the list entirely.\n *\n * `detached` is a live shell nobody is watching: its last socket went away\n * and it lives on until it is closed, its process exits, or its Session ends.\n * The model can still address it, and saying so is the difference between a\n * terminal that is gone and one that is waiting to be reattached.\n */\n readonly state: 'running' | 'detached' | 'exited'\n /** Current column count. */\n readonly cols: number\n /** Current row count. */\n readonly rows: number\n}\n\n/** One registered terminal, with the state its owner and the tool read. */\nexport interface TerminalEntry {\n readonly id: string\n readonly label: string\n readonly sessionId: string\n readonly machine: string\n readonly cwd: string\n readonly handle: TtyHandle\n /** Retained output, oldest first; `buffer.length === bytes - dropped`. */\n buffer: Buffer\n /** Absolute byte offset just past the newest byte ever produced. */\n bytes: number\n /** Bytes dropped from the front of the stream to keep `buffer` bounded. */\n dropped: number\n state: 'running' | 'detached' | 'exited'\n cols: number\n rows: number\n /** Consumers currently receiving output; the tab's socket is the usual one. */\n readonly attaches: Set<TerminalSink>\n /** Waiters to wake when output arrives or the process exits. */\n readonly waiters: Set<() => void>\n /** Pending release of a terminal whose last socket detached; absent while attached. */\n detachTimer?: NodeJS.Timeout | undefined\n}\n\n/** One bounded page of retained output. */\nexport interface TerminalRead {\n readonly id: string\n /** Absolute byte offset just past the returned text; the next read resumes there. */\n readonly offset: number\n readonly text: string\n /** Whether output before the returned text was dropped or cut by the line cap. */\n readonly truncated: boolean\n}\n\n/** What one wait is asked for. */\nexport interface TerminalWaitRequest {\n /**\n * Absolute byte offset to start searching from; omitted starts at the\n * beginning of the retained tail a default read returns, so output that\n * already arrived can match.\n */\n readonly offset?: number\n /** Plain substring to wait for. Exactly one of `match` and `regex` is required. */\n readonly match?: string\n /** Regular expression source to wait for. */\n readonly regex?: string\n /** Budget in milliseconds; defaults to {@link DEFAULT_WAIT_MS}. */\n readonly timeoutMs?: number\n}\n\n/** How one wait ended. */\nexport interface TerminalWait {\n readonly id: string\n /** Absolute byte offset just past the returned text. */\n readonly offset: number\n /** Everything searched, from the wait's start offset through the newest byte. */\n readonly text: string\n readonly matched: boolean\n readonly reason: 'match' | 'exit' | 'timeout'\n}\n\n/**\n * The terminal table the socket, the management API, and the model's tool share.\n */\nexport interface TerminalRegistry {\n /**\n * Register a terminal a person just opened, spawning it through the seam.\n * @param sessionId - the Session whose tab owns it.\n * @param cwd - the workspace directory, which also decides the machine.\n * @param size - the browser-measured geometry.\n * @returns the registered entry.\n */\n open(\n sessionId: string,\n cwd: string,\n size: { readonly cols: number; readonly rows: number },\n ): Promise<TerminalEntry>\n /**\n * Deliver a terminal's output to one consumer, replaying what it missed.\n *\n * The retained output is handed to the sink before any later chunk, so a\n * reattached socket shows history in order. Attaching also cancels a pending\n * detach valve: the terminal is watched again.\n * @param id - the terminal.\n * @param sink - where output, exit, and failure go.\n * @returns the entry, for the frame the socket answers with.\n * @throws TerminalRegistryError when the terminal is unknown or already exited.\n */\n attach(id: string, sink: TerminalSink): TerminalEntry\n /**\n * Stop delivering a terminal's output to one consumer, without ending it.\n *\n * The last sink leaving marks the terminal `detached` without ending it; an\n * already-exited terminal is released at once instead, and a configured\n * detach valve schedules that release for later.\n * @param id - the terminal.\n * @param sink - the consumer to remove.\n */\n detach(id: string, sink: TerminalSink): void\n /**\n * Adopt a browser-measured size.\n * @param id - the terminal.\n * @param cols - column count.\n * @param rows - row count.\n * @returns whether the provider accepted it; a refusal leaves the old size.\n */\n resize(id: string, cols: number, rows: number): Promise<boolean>\n /**\n * Every terminal one Session has open, in registration order.\n * @param sessionId - the Session identity.\n * @returns fresh views.\n */\n listFor(sessionId: string): TerminalView[]\n /**\n * Resolve the terminal one Session is addressing.\n *\n * The only authorization point in this module: nothing else decides whether a\n * Session may touch a terminal. A named terminal that is unknown, owned by\n * another Session, or already exited is refused without saying which, because\n * distinguishing them would report on another Session's terminal. A detached\n * terminal is still addressable — a dropped connection is not an exit.\n * @param sessionId - the calling Session.\n * @param id - the requested terminal, or undefined when the Session has exactly one.\n * @returns the entry.\n * @throws TerminalRegistryError with a reason the model can act on.\n */\n requireOwned(sessionId: string, id?: string | undefined): TerminalEntry\n /**\n * Deliver literal text.\n * @param id - the terminal.\n * @param text - the text, written verbatim.\n * @returns how many bytes were written.\n */\n write(id: string, text: string): Promise<number>\n /**\n * Deliver logical keystrokes, validating every name before writing anything.\n * @param id - the terminal.\n * @param names - logical key names, each in {@link KEY_NAMES}.\n * @returns the byte count and the key count actually written.\n * @throws TerminalRegistryError naming the first unknown key, having written nothing.\n */\n keys(id: string, names: readonly string[]): Promise<{ bytes: number; keys: number }>\n /**\n * Read retained output.\n * @param id - the terminal.\n * @param offset - absolute byte offset to read from; omitted reads the tail.\n * @param lines - tail line count; defaults to {@link DEFAULT_READ_LINES}.\n * @returns the page and the offset that resumes after it.\n */\n read(id: string, offset?: number | undefined, lines?: number | undefined): TerminalRead\n /**\n * Wait for output to match, for the process to exit, or for the budget.\n * @param id - the terminal.\n * @param request - the matcher, the start offset, and the budget.\n * @param signal - caller cancellation; aborting rejects without a result.\n * @returns the wait's outcome, never an exit or a timeout as a throw.\n */\n wait(id: string, request: TerminalWaitRequest, signal?: AbortSignal): Promise<TerminalWait>\n /**\n * End one terminal and remove it.\n * @param id - the terminal.\n * @returns whether a terminal with that id was open.\n */\n kill(id: string): Promise<boolean>\n /**\n * End every terminal one Session owns; the per-session disposal hook.\n * @param sessionId - the Session that ended.\n */\n releaseSession(sessionId: string): Promise<void>\n /** End every terminal; the plugin's own teardown. */\n disposeAll(): Promise<void>\n}\n\n/** What the registry needs to create a terminal. */\nexport interface TerminalRegistryOptions {\n /** The `ctx.tty` seam's allocation verb, read when a terminal opens. */\n readonly spawn: (request: TtySpawnRequest) => Promise<TtyHandle>\n /** How to start a shell. */\n readonly settings: TerminalSettings\n /**\n * Which machine owns a working directory.\n * @param cwd - the workspace directory.\n * @returns the node id and the title to show a reader.\n */\n readonly machine: (cwd: string) => { readonly label: string }\n /**\n * The directory a shell asked for one path actually runs in.\n *\n * A workspace routed to a machine is named here by its local anchor path,\n * while the shell runs at the checkout on that machine. The row and the\n * status line name where the shell is, not where its workspace is registered.\n * @param cwd - the workspace directory the shell was asked for.\n * @returns the directory the shell lands in.\n */\n readonly directory: (cwd: string) => string\n}\n\n/** A terminal request the registry refused. */\nexport class TerminalRegistryError extends Error {\n override readonly name = 'TerminalRegistryError'\n}\n\n/** Bytes one terminal retains before the oldest are dropped. */\nconst BUFFER_BYTES = 256 * 1024\n\n/** Tail line count a read keeps when the caller names none. */\nexport const DEFAULT_READ_LINES = 200\n\n/** Largest tail a read may ask for. */\nexport const MAX_READ_LINES = 2000\n\n/** Wait budget when the caller names none. */\nexport const DEFAULT_WAIT_MS = 30_000\n\n/** Longest wait budget a caller may ask for. */\nexport const MAX_WAIT_MS = 300_000\n\n/**\n * Logical key names a caller may send, in the order the refusal lists them.\n *\n * The values are the byte sequences a terminal emulator would send, so a caller\n * names an intent (`ctrl+c`) rather than spelling an escape.\n */\nexport const KEYS: Readonly<Record<string, string>> = {\n enter: '\\r',\n esc: '\\x1b',\n escape: '\\x1b',\n tab: '\\t',\n backspace: '\\x7f',\n delete: '\\x1b[3~',\n up: '\\x1b[A',\n down: '\\x1b[B',\n right: '\\x1b[C',\n left: '\\x1b[D',\n home: '\\x1b[H',\n end: '\\x1b[F',\n pageup: '\\x1b[5~',\n pagedown: '\\x1b[6~',\n space: ' ',\n ...Object.fromEntries(\n Array.from({ length: 26 }, (_, index) => [\n `ctrl+${String.fromCharCode(97 + index)}`,\n String.fromCharCode(index + 1),\n ]),\n ),\n}\n\n/** Every logical key name, for a refusal that tells the caller what is valid. */\nexport const KEY_NAMES: readonly string[] = Object.keys(KEYS)\n\n/**\n * Build the terminal registry.\n * @param options - what the registry needs to create a terminal.\n * @returns the registry the socket and the tool share.\n */\nexport function createTerminalRegistry(options: TerminalRegistryOptions): TerminalRegistry {\n const entries = new Map<string, TerminalEntry>()\n /** Monotonic id source; never reused, so a restart never inherits an id. */\n let nextOrdinal = 0\n const detachGraceMs = options.settings.detachGraceMs ?? 0\n\n /** Append output, dropping the oldest bytes once the cap is reached. */\n const append = (entry: TerminalEntry, chunk: Buffer): void => {\n entry.bytes += chunk.length\n if (chunk.length >= BUFFER_BYTES) {\n entry.buffer = Buffer.from(chunk.subarray(chunk.length - BUFFER_BYTES))\n entry.dropped = entry.bytes - entry.buffer.length\n return\n }\n const grown = entry.buffer.length === 0 ? Buffer.from(chunk) : Buffer.concat([entry.buffer, chunk])\n const overflow = grown.length - BUFFER_BYTES\n entry.buffer = overflow > 0 ? Buffer.from(grown.subarray(overflow)) : grown\n entry.dropped = entry.bytes - entry.buffer.length\n }\n\n /** The retained text from one absolute offset, and whether earlier bytes are gone. */\n const textFrom = (entry: TerminalEntry, from: number): { text: string; truncated: boolean } => {\n const start = Math.max(from, entry.dropped)\n return {\n text: entry.buffer.subarray(start - entry.dropped).toString('utf8'),\n truncated: from < entry.dropped,\n }\n }\n\n /** Keep only the last `lines` lines of one string. */\n const tailLines = (text: string, lines: number): { text: string; cut: boolean } => {\n const parts = text.split('\\n')\n if (parts.length <= lines) return { text, cut: false }\n return { text: parts.slice(parts.length - lines).join('\\n'), cut: true }\n }\n\n /**\n * The absolute offset that begins the tail a default read returns.\n *\n * An offset-less wait searches from here rather than from the current end, so\n * output that arrived before the wait can still match.\n */\n const retainedStart = (entry: TerminalEntry): number => {\n const seen = textFrom(entry, entry.dropped)\n const tail = tailLines(seen.text, DEFAULT_READ_LINES)\n return Math.max(entry.dropped, entry.bytes - Buffer.byteLength(tail.text, 'utf8'))\n }\n\n /** Wake every waiter, which re-checks the terminal's new state. */\n const notify = (entry: TerminalEntry): void => {\n for (const waiter of [...entry.waiters]) waiter()\n }\n\n /** Stream a spawned terminal into its buffer, its sinks, and its waiters. */\n const pump = (entry: TerminalEntry): void => {\n entry.handle.output.on('data', (chunk: Buffer) => {\n append(entry, chunk)\n notify(entry)\n const sinks = [...entry.attaches]\n if (sinks.length === 0) return\n // One chunk in flight per sink: a slow socket must not let the PTY queue\n // an unbounded flood inside this process. The buffer still holds the\n // chunk, so pausing loses nothing.\n entry.handle.output.pause()\n const resume = (): void => {\n if (entry.state === 'running') entry.handle.output.resume()\n }\n void Promise.all(sinks.map(sink => Promise.resolve().then(() => sink.output(chunk))))\n .then(resume, resume)\n })\n entry.handle.done.then(\n (outcome) => {\n settle(entry, sink => { sink.exit(outcome) })\n },\n (error: unknown) => {\n settle(entry, sink => { sink.fail(error) })\n },\n )\n }\n\n /**\n * Mark a terminal exited, report it, and release it if nobody is watching.\n *\n * A detached shell whose process exits has no browser to come back to and no\n * valve to wait on, so the entry goes with the process.\n */\n const settle = (entry: TerminalEntry, report: (sink: TerminalSink) => void): void => {\n entry.state = 'exited'\n for (const sink of entry.attaches) report(sink)\n notify(entry)\n if (entry.attaches.size === 0) void registry.kill(entry.id)\n }\n\n /** The entry, or a failure — for internal callers that already hold a live id. */\n const entryOf = (id: string): TerminalEntry => {\n const entry = entries.get(id)\n if (entry === undefined) throw new TerminalRegistryError(`terminal \"${id}\" is not open`)\n return entry\n }\n\n /** Refuse a request on a terminal that has already exited. */\n const requireLive = (entry: TerminalEntry): TerminalEntry => {\n if (entry.state === 'exited') {\n throw new TerminalRegistryError(`terminal \"${entry.id}\" has already exited`)\n }\n return entry\n }\n\n const registry: TerminalRegistry = {\n async open(sessionId, cwd, size): Promise<TerminalEntry> {\n const handle = await options.spawn({\n argv: [options.settings.shell, ...options.settings.shellArgs],\n cwd,\n env: { ...options.settings.env },\n cols: size.cols,\n rows: size.rows,\n graceMs: options.settings.graceMs,\n })\n const ordinal = nextOrdinal + 1\n nextOrdinal = ordinal\n const where = options.machine(cwd)\n const entry: TerminalEntry = {\n id: `t${String(ordinal)}`,\n label: `Terminal ${String(ordinal)}`,\n sessionId,\n machine: where.label,\n cwd: options.directory(cwd),\n handle,\n buffer: Buffer.alloc(0),\n bytes: 0,\n dropped: 0,\n state: 'running',\n cols: size.cols,\n rows: size.rows,\n attaches: new Set(),\n waiters: new Set(),\n }\n entries.set(entry.id, entry)\n pump(entry)\n return entry\n },\n\n attach(id, sink): TerminalEntry {\n const entry = requireLive(entryOf(id))\n // A reattach ends any pending valve: the shell is watched again.\n if (entry.detachTimer !== undefined) {\n clearTimeout(entry.detachTimer)\n entry.detachTimer = undefined\n }\n if (entry.state === 'detached') entry.state = 'running'\n entry.attaches.add(sink)\n // Replay before the pump can deliver anything new: the sink is already in\n // `attaches`, but data events run after this call returns, so the retained\n // bytes reach the socket first.\n if (entry.buffer.length > 0) void sink.output(entry.buffer)\n return entry\n },\n\n detach(id, sink): void {\n const entry = entries.get(id)\n if (entry === undefined) return\n entry.attaches.delete(sink)\n if (entry.attaches.size > 0) return\n if (entry.state === 'exited') {\n // Nothing is left to reattach to; release it rather than leaving a\n // corpse in the table until the Session ends.\n void registry.kill(id)\n return\n }\n entry.state = 'detached'\n if (entry.detachTimer !== undefined || detachGraceMs <= 0) return\n entry.detachTimer = setTimeout(() => {\n entry.detachTimer = undefined\n void registry.kill(id)\n }, detachGraceMs)\n // A pending release must not hold the host process open.\n entry.detachTimer.unref()\n },\n\n async resize(id, cols, rows): Promise<boolean> {\n const entry = entryOf(id)\n // A refusal is not a failure: the browser is told the size is stale, and\n // the terminal keeps the geometry it actually has.\n const accepted = await entry.handle.resize(cols, rows).then(() => true, () => false)\n if (accepted) {\n entry.cols = cols\n entry.rows = rows\n }\n return accepted\n },\n\n listFor(sessionId): TerminalView[] {\n return [...entries.values()]\n .filter(entry => entry.sessionId === sessionId)\n .map(entry => ({\n id: entry.id,\n label: entry.label,\n cwd: entry.cwd,\n machine: entry.machine,\n pid: entry.handle.pid,\n state: entry.state,\n cols: entry.cols,\n rows: entry.rows,\n }))\n },\n\n requireOwned(sessionId, id): TerminalEntry {\n const mine = [...entries.values()].filter(entry => entry.sessionId === sessionId)\n if (id !== undefined) {\n const entry = mine.find(candidate => candidate.id === id)\n // A terminal another Session owns and one that does not exist are the\n // same refusal: telling them apart would report on someone else's tab.\n if (entry === undefined) {\n throw new TerminalRegistryError(\n `no terminal \"${id}\" is open in this session`\n + (mine.length === 0 ? '; this session has no open terminal' : `; open terminals: ${describe(mine)}`),\n )\n }\n return requireLive(entry)\n }\n const live = mine.filter(entry => entry.state !== 'exited')\n if (live.length === 0) {\n throw new TerminalRegistryError(mine.length === 0\n ? 'this session has no open terminal; open one in the sidebar first'\n : `every terminal in this session has exited: ${describe(mine)}`)\n }\n if (live.length > 1) {\n throw new TerminalRegistryError(\n `this session has ${String(live.length)} open terminals; `\n + `pass \"terminal\" with one of: ${describe(live)}`,\n )\n }\n return live[0]!\n },\n\n async write(id, text): Promise<number> {\n const entry = requireLive(entryOf(id))\n await entry.handle.write(text)\n return Buffer.byteLength(text, 'utf8')\n },\n\n async keys(id, names): Promise<{ bytes: number; keys: number }> {\n const entry = requireLive(entryOf(id))\n // Every name is resolved before any byte is written, so an unknown key\n // fails the whole call rather than delivering half a chord.\n const bytes = names.map((name) => {\n const sequence = KEYS[name]\n if (sequence === undefined) {\n throw new TerminalRegistryError(\n `unknown key \"${name}\"; known keys are ${KEY_NAMES.join(', ')}`,\n )\n }\n return sequence\n }).join('')\n await entry.handle.write(bytes)\n return { bytes: Buffer.byteLength(bytes, 'utf8'), keys: names.length }\n },\n\n read(id, offset, lines): TerminalRead {\n const entry = entryOf(id)\n const cap = Math.min(Math.max(Math.trunc(lines ?? DEFAULT_READ_LINES), 1), MAX_READ_LINES)\n const from = offset ?? entry.dropped\n const seen = textFrom(entry, from)\n const tail = tailLines(seen.text, cap)\n return {\n id: entry.id,\n offset: entry.bytes,\n text: tail.text,\n truncated: seen.truncated || tail.cut,\n }\n },\n\n async wait(id, request, signal): Promise<TerminalWait> {\n const entry = entryOf(id)\n const budget = Math.min(Math.max(Math.trunc(request.timeoutMs ?? DEFAULT_WAIT_MS), 1), MAX_WAIT_MS)\n const start = request.offset ?? retainedStart(entry)\n const test = matcher(request)\n return new Promise<TerminalWait>((resolve, reject) => {\n let timer: NodeJS.Timeout | undefined\n const settle = (): void => {\n if (timer !== undefined) clearTimeout(timer)\n entry.waiters.delete(check)\n signal?.removeEventListener('abort', abort)\n }\n const finish = (matched: boolean, reason: TerminalWait['reason']): void => {\n settle()\n const seen = textFrom(entry, start)\n resolve({ id: entry.id, offset: entry.bytes, text: seen.text, matched, reason })\n }\n function abort(): void {\n settle()\n reject(signal?.reason instanceof Error\n ? signal.reason\n : new TerminalRegistryError('the wait was cancelled'))\n }\n function check(): void {\n if (signal?.aborted === true) {\n abort()\n return\n }\n const seen = textFrom(entry, start)\n if (test(seen.text)) {\n finish(true, 'match')\n return\n }\n if (entry.state === 'exited') finish(false, 'exit')\n }\n signal?.addEventListener('abort', abort, { once: true })\n entry.waiters.add(check)\n // The match may already be in the buffer, so the first check is\n // synchronous; only then does waiting on new output begin.\n check()\n if (entry.waiters.has(check)) {\n timer = setTimeout(() => { finish(false, 'timeout') }, budget)\n }\n })\n },\n\n async kill(id): Promise<boolean> {\n const entry = entries.get(id)\n if (entry === undefined) return false\n if (entry.detachTimer !== undefined) {\n clearTimeout(entry.detachTimer)\n entry.detachTimer = undefined\n }\n entries.delete(id)\n entry.attaches.clear()\n entry.state = 'exited'\n notify(entry)\n await entry.handle.terminate().catch(() => undefined)\n return true\n },\n\n async releaseSession(sessionId): Promise<void> {\n await Promise.all([...entries.values()]\n .filter(entry => entry.sessionId === sessionId)\n .map(entry => registry.kill(entry.id)))\n },\n\n async disposeAll(): Promise<void> {\n await Promise.all([...entries.keys()].map(id => registry.kill(id)))\n },\n }\n\n return registry\n}\n\n/** Compile a wait's matcher once, refusing a bad pattern and a missing one. */\nfunction matcher(request: TerminalWaitRequest): (text: string) => boolean {\n const hasMatch = request.match !== undefined\n const hasRegex = request.regex !== undefined\n if (hasMatch === hasRegex) {\n throw new TerminalRegistryError(\n hasMatch ? 'pass either \"match\" or \"regex\", not both' : 'wait requires \"match\" or \"regex\"',\n )\n }\n if (hasMatch) {\n const needle = request.match!\n return text => text.includes(needle)\n }\n let expression: RegExp\n try {\n expression = new RegExp(request.regex!)\n } catch (error) {\n throw new TerminalRegistryError(\n `\"regex\" is not a valid pattern: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n return text => expression.test(text)\n}\n\n/** The candidate list one refusal shows: id and human label. */\nfunction describe(entries: readonly TerminalEntry[]): string {\n return entries.map(entry => `${entry.id} (${entry.label})`).join(', ')\n}\n","/**\n * Which directory a terminal opens in.\n *\n * The browser sends a Session identity and never a path: the workspace is\n * derived on the host from that Session's own header, exactly as every other\n * workspace-scoped reader derives it. A live Session answers from its header; a\n * Session the host is not running — one restored from disk that the browser is\n * still showing — answers from its persisted header.\n *\n * The resulting path is what a terminal is started with, and it is also what\n * makes the machine choice for free: a workspace routed to a\n * dsh-remote-workspace node is named by its local anchor path, so the routing\n * terminal provider resolves that path to the node and runs the shell there.\n * Nothing here needs to know which machines exist.\n *\n * @module dsh-remote-workspace/terminal/host/workspace\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { SessionId } from '@deepseek-ai/dsh-session/types'\n// Type-only: pulls the persistence plugin's Context merge (ctx.sessionPersistence),\n// which is how a Session the host is not running still names its workspace.\nimport type {} from '@deepseek-ai/dsh-session-persistence'\n\n/** A terminal request the host refused. */\nexport class TerminalFailure extends Error {\n override readonly name = 'TerminalFailure'\n}\n\n/**\n * The workspace directory one Session's terminal belongs in.\n * @param ctx - the host context carrying the session store.\n * @param sessionId - the identity the browser supplied.\n * @returns the absolute workspace directory.\n * @throws TerminalFailure when neither a live nor a persisted header names one.\n */\nexport async function resolveWorkspace(ctx: Context, sessionId: string): Promise<string> {\n if (sessionId.trim() === '') {\n throw new TerminalFailure('no session identity was supplied, so the workspace is unknown')\n }\n const identity = sessionId as SessionId\n // Read rather than injected: the terminal is one surface of a plugin whose\n // activation does not depend on a session store existing.\n const live = ctx.get('sessions')?.get(identity)?.header\n const stored = live === undefined\n ? await ctx.get('sessionPersistence')?.stat(identity)\n : undefined\n const cwd = (live ?? stored?.header)?.cwd\n if (cwd === undefined || cwd === '') {\n throw new TerminalFailure(`session \"${sessionId}\" is unknown, so its workspace is too`)\n }\n return cwd\n}\n","/**\n * One browser socket, one terminal in the shared registry.\n *\n * The bridge owns the socket's half of the correspondence: it resolves the\n * Session's workspace, asks the registry to register the shell a person just\n * opened, and forwards keystrokes, resizes, and output. The registry owns the\n * terminal itself, because the model-facing terminal tool drives the same\n * shell; this module never allocates or releases one directly.\n *\n * A tab owns its shell, but the socket is only a view of it. A socket that\n * closes detaches: the registry keeps the PTY and its retained output, so a\n * reload, a closed tab, or a dropped connection can `attach` back and see the\n * same shell for as long as it lives. Ending one is the chooser's own route,\n * which reaches the registry without holding a socket, and a shell whose\n * process exits still closes its socket and is released. A Session ending\n * releases its terminals through the registry's own hook.\n *\n * Output is paced one chunk at a time through the sink the registry calls: a\n * command that floods the terminal pauses the PTY's output stream until the\n * socket has taken the chunk, instead of queueing the whole flood inside this\n * process.\n *\n * @module dsh-remote-workspace/terminal/host/terminal\n */\n\nimport { Buffer } from 'node:buffer'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { WebSocket, type RawData } from 'ws'\nimport type { AttachFrame, ClientFrame, HostFrame, OpenFrame } from '../shared/wire.ts'\nimport type { TerminalRegistry, TerminalSink } from './registry.ts'\nimport { resolveWorkspace } from './workspace.ts'\n\n/** Largest dimension a browser may ask a PTY for. */\nconst MAX_DIMENSION = 1000\n\n/** Keystrokes held while a shell is still being allocated, before they are dropped. */\nconst MAX_PENDING_INPUT = 256\n\n/**\n * Clamp a browser-measured dimension into something a PTY accepts.\n * @param value - the measured value.\n * @param fallback - the value to use when the measurement is not a number.\n * @returns a whole number of rows or columns in range.\n */\nfunction dimension(value: number, fallback: number): number {\n if (!Number.isFinite(value)) return fallback\n return Math.min(MAX_DIMENSION, Math.max(1, Math.floor(value)))\n}\n\n/**\n * Decode one WebSocket text message.\n * @param data - the message as the server delivered it.\n * @returns its UTF-8 text.\n */\nfunction textOf(data: RawData): string {\n if (Array.isArray(data)) return Buffer.concat(data).toString('utf8')\n if (Buffer.isBuffer(data)) return data.toString('utf8')\n return Buffer.from(data).toString('utf8')\n}\n\n/**\n * Report a failure the way the browser's status line reads it.\n * @param error - the thrown value.\n * @returns its message, or its string form when it is not an Error.\n */\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\n/**\n * Serve one terminal over one accepted socket.\n * @param ctx - the host context the workspace resolver reads.\n * @param registry - the terminals a person's tabs have open.\n * @param socket - the accepted browser socket.\n */\nexport function attachTerminal(ctx: Context, registry: TerminalRegistry, socket: WebSocket): void {\n let entryId: string | undefined\n let opening = false\n let closed = false\n /** The size the browser last asked for; the spawn uses it even if it changed mid-allocation. */\n let requested = { cols: 80, rows: 24 }\n /** The size last forwarded, so an unchanged resize is not asked for again. */\n let applied: { cols: number; rows: number } | undefined\n const typed: string[] = []\n\n const post = (frame: HostFrame): void => {\n if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(frame))\n }\n\n /**\n * The registry's view of this socket.\n *\n * Output is handed to the socket one chunk at a time and the returned promise\n * is what lets the registry pause the PTY, so a socket that cannot keep up\n * slows the shell rather than this process.\n */\n const sink: TerminalSink = {\n output(chunk): Promise<void> | void {\n if (socket.readyState !== WebSocket.OPEN) return\n return new Promise<void>((resolve) => {\n socket.send(chunk, () => { resolve() })\n })\n },\n exit(outcome): void {\n post({ t: 'exit', code: outcome.exitCode, signal: outcome.signal })\n socket.close(1000, 'terminal exited')\n },\n fail(error): void {\n post({ t: 'error', message: describe(error) })\n socket.close(1011, 'terminal failed')\n },\n }\n\n /** This socket went away: keep the shell it was viewing. */\n const stop = (): void => {\n if (closed) return\n closed = true\n const current = entryId\n entryId = undefined\n if (current !== undefined) registry.detach(current, sink)\n }\n\n /** Whether this socket may take on a terminal now, answering the browser if not. */\n const claim = (): boolean => {\n if (entryId === undefined && !opening) return true\n post({ t: 'error', message: 'this connection already owns a terminal' })\n return false\n }\n\n /** Deliver the keystrokes typed before the shell existed. */\n const flushTyped = (id: string): void => {\n for (const data of typed.splice(0)) void registry.write(id, data).catch(() => undefined)\n }\n\n /** Register the shell for one Session's workspace and start streaming it. */\n const open = async (frame: OpenFrame): Promise<void> => {\n if (closed) return\n if (!claim()) return\n requested = { cols: dimension(frame.cols, 80), rows: dimension(frame.rows, 24) }\n opening = true\n try {\n const cwd = await resolveWorkspace(ctx, frame.sessionId)\n const entry = await registry.open(frame.sessionId, cwd, requested)\n // The socket may have gone while the shell was being allocated; a\n // registered terminal owns its own lifetime and must be released here.\n if (closed) {\n void registry.kill(entry.id)\n return\n }\n entryId = entry.id\n applied = { ...requested }\n registry.attach(entry.id, sink)\n post({ t: 'ready', pid: entry.handle.pid, cwd: entry.cwd, id: entry.id, label: entry.label })\n flushTyped(entry.id)\n } catch (error: unknown) {\n post({ t: 'error', message: describe(error) })\n } finally {\n opening = false\n }\n }\n\n /** Adopt a browser-measured size, now or as the size the shell will start at. */\n const applySize = async (cols: number, rows: number): Promise<void> => {\n const next = {\n cols: dimension(cols, requested.cols),\n rows: dimension(rows, requested.rows),\n }\n requested = next\n const current = entryId\n if (current === undefined) return\n if (applied !== undefined && applied.cols === next.cols && applied.rows === next.rows) return\n applied = next\n const live = await registry.resize(current, next.cols, next.rows)\n post({ t: 'size', cols: next.cols, rows: next.rows, live })\n }\n\n /** Reattach this socket to a terminal it already knows by id. */\n const reattach = (frame: AttachFrame): void => {\n if (closed) return\n if (!claim()) return\n let entry\n try {\n // Ownership first: an id alone must not let one Session watch another's\n // shell. The registry replays the retained output to this sink before\n // anything new can arrive, so a remounted terminal shows its history in\n // order.\n registry.requireOwned(frame.sessionId, frame.id)\n entry = registry.attach(frame.id, sink)\n } catch (error: unknown) {\n // A terminal that is gone is the browser's cue to open a fresh one.\n post({ t: 'error', message: describe(error) })\n return\n }\n entryId = entry.id\n // The PTY's own geometry is what was applied; the frame's is what the\n // browser measures now, and any difference is a real resize.\n applied = { cols: entry.cols, rows: entry.rows }\n post({ t: 'ready', pid: entry.handle.pid, cwd: entry.cwd, id: entry.id, label: entry.label })\n flushTyped(entry.id)\n void applySize(frame.cols, frame.rows)\n }\n\n socket.on('close', stop)\n socket.on('error', stop)\n socket.on('message', (data: RawData, isBinary: boolean) => {\n if (isBinary) return\n let frame: ClientFrame\n try {\n frame = JSON.parse(textOf(data)) as ClientFrame\n } catch {\n return\n }\n switch (frame.t) {\n case 'open':\n void open(frame)\n return\n case 'attach':\n reattach(frame)\n return\n case 'input': {\n const current = entryId\n if (current === undefined) {\n if (typed.length < MAX_PENDING_INPUT) typed.push(frame.data)\n return\n }\n void registry.write(current, frame.data).catch(() => undefined)\n return\n }\n case 'resize':\n void applySize(frame.cols, frame.rows)\n return\n default:\n return\n }\n })\n}\n","/**\n * The host's WebSocket route.\n *\n * A terminal is a live byte stream in both directions, which no request/response\n * route carries, so this plugin registers an upgrade route on the deployment's\n * Web server. Two properties come with that:\n *\n * - the route is registered through `ctx.get('webServer')` rather than an\n * injected dependency, because a profile that serves no browser (headless,\n * SDK) has no HTTP server and this plugin must still load there;\n * - the upgrade passes the same browser-trust fence as every `/api` request\n * before the socket changes hands. WebSocket handshakes carry cookies and are\n * not covered by the browser's same-origin policy, so without that fence any\n * page in the browser could open a shell on this host.\n *\n * Unloading the plugin terminates every live socket; each socket's close\n * handler then detaches its registry entry, and the registry's own disposal\n * releases every terminal that would otherwise outlive the plugin.\n *\n * @module dsh-remote-workspace/terminal/host/socket\n */\n\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-client-connection'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { WebSocket, WebSocketServer } from 'ws'\nimport { attachTerminal } from './terminal.ts'\nimport type { TerminalRegistry } from './registry.ts'\n\n/**\n * Refuse an upgrade without giving the socket to the WebSocket server.\n * @param socket - the socket still owned by this handler.\n * @param status - the HTTP status to answer with.\n */\nfunction rejectUpgrade(socket: Duplex, status: 401 | 403): void {\n const reason = status === 401 ? 'Unauthorized' : 'Forbidden'\n socket.end(`HTTP/1.1 ${String(status)} ${reason}\\r\\nconnection: close\\r\\ncontent-length: 0\\r\\n\\r\\n`)\n}\n\n/**\n * Register the terminal socket for this plugin's lifetime.\n *\n * A missing Web server is not a failure: it means nobody can open a terminal,\n * not that the plugin is misconfigured.\n *\n * @param ctx - the host context.\n * @param path - the absolute pathname the socket is served at.\n * @param registry - the terminals a person's tabs have open.\n */\nexport function registerTerminalSocket(ctx: Context, path: string, registry: TerminalRegistry): void {\n const webServer = ctx.get('webServer')\n if (webServer === undefined) return\n\n const server = new WebSocketServer({ noServer: true, perMessageDeflate: false })\n const live = new Set<WebSocket>()\n\n ctx.effect(() => {\n const unregister = webServer.registerUpgrade({\n path,\n handler: (request: IncomingMessage, socket: Duplex, head: Buffer): void => {\n const rejection = ctx.get('connection')?.requestRejection(request)\n if (rejection !== undefined) {\n rejectUpgrade(socket, rejection)\n return\n }\n server.handleUpgrade(request, socket, head, (accepted) => {\n live.add(accepted)\n accepted.on('close', () => live.delete(accepted))\n attachTerminal(ctx, registry, accepted)\n })\n },\n })\n return () => {\n unregister()\n for (const socket of live) socket.terminate()\n live.clear()\n }\n }, `dsh-terminal: ${path} socket`)\n}\n","/**\n * The wire between the browser terminal and the host that owns its PTY.\n *\n * Two kinds of frame share one socket, told apart by how WebSocket carries\n * them rather than by a tag inside them:\n *\n * - **text** carries JSON control — opening a terminal, keystrokes, resizes,\n * and the host's answers;\n * - **binary** carries raw terminal bytes, host to browser only, so a shell's\n * output is not base64-encoded and re-decoded once per chunk.\n *\n * A socket's life is one *view* of a terminal, not the terminal itself. A\n * socket that closes detaches: the host keeps the PTY and its retained output\n * so a reload, a closed tab, or a dropped connection can `attach` back to the\n * same shell, which lives for as long as its process does.\n *\n * @module dsh-remote-workspace/terminal/shared/wire\n */\n\n/** Where the host serves the terminal socket. */\nexport const SOCKET_PATH = '/dsh-terminal/ws'\n\n/** Open one terminal in a Session's workspace. The first frame a browser sends. */\nexport interface OpenFrame {\n readonly t: 'open'\n /** The Session whose workspace directory the shell starts in. */\n readonly sessionId: string\n /** Initial column count, from the browser's own measurement. */\n readonly cols: number\n /** Initial row count, from the browser's own measurement. */\n readonly rows: number\n}\n\n/**\n * Reattach to a terminal this browser already knows by id.\n *\n * A socket that reconnects after a reload or a dropped connection uses this\n * instead of {@link OpenFrame}: the host replays the retained output tail to\n * this socket and keeps the same shell. An entry that is gone answers with an\n * {@link ErrorFrame}, and the browser then opens a fresh terminal.\n */\nexport interface AttachFrame {\n readonly t: 'attach'\n /** The Session that owns the terminal; attaching outside it is refused. */\n readonly sessionId: string\n /** The registry id a previous {@link ReadyFrame} assigned. */\n readonly id: string\n /** Column count, from the browser's own measurement. */\n readonly cols: number\n /** Row count, from the browser's own measurement. */\n readonly rows: number\n}\n\n/** Deliver keystrokes. */\nexport interface InputFrame {\n readonly t: 'input'\n /** Text to write verbatim; the browser's Enter key arrives as a carriage return. */\n readonly data: string\n}\n\n/** Ask the PTY to adopt a new size. */\nexport interface ResizeFrame {\n readonly t: 'resize'\n readonly cols: number\n readonly rows: number\n}\n\n/** Every frame the browser sends. */\nexport type ClientFrame = OpenFrame | AttachFrame | InputFrame | ResizeFrame\n\n/** The terminal is live; the browser may now write to it. */\nexport interface ReadyFrame {\n readonly t: 'ready'\n /** Top-level process id. */\n readonly pid: number\n /** The workspace directory the shell was started in. */\n readonly cwd: string\n /**\n * The registry id the model's terminal tool addresses this shell by.\n *\n * The tab title shows it too, so two terminal tabs are told apart on screen\n * exactly the way the model tells them apart in a call.\n */\n readonly id: string\n /** The human label behind {@link ReadyFrame.id}, and the tab's title. */\n readonly label: string\n}\n\n/** The top-level process exited; the host closes the socket next. */\nexport interface ExitFrame {\n readonly t: 'exit'\n /** Exit code, or null when a signal ended it. */\n readonly code: number | null\n /** Terminating signal name, or null on a normal exit. */\n readonly signal: string | null\n}\n\n/** Opening or running the terminal failed. */\nexport interface ErrorFrame {\n readonly t: 'error'\n /** Operator-readable reason. */\n readonly message: string\n}\n\n/**\n * How a resize settled.\n *\n * `live` is false when the provider refused the resize: a node may still run\n * an agent from before `term.resize` existed, so its terminals keep the size\n * they were opened with. Saying so is the difference between a stale layout\n * and a bug report.\n */\nexport interface SizeFrame {\n readonly t: 'size'\n readonly cols: number\n readonly rows: number\n readonly live: boolean\n}\n\n/** Every frame the host sends. */\nexport type HostFrame = ReadyFrame | ExitFrame | ErrorFrame | SizeFrame\n","/**\n * The one model-facing terminal tool.\n *\n * The tool drives the same shells the sidebar's terminal tabs hold.\n * Interrupting a foreground command is a keystroke (`ctrl+c`), which is how a\n * person does it too.\n *\n * One tool carries every operation, dispatched by `action`. A part of the\n * contract is which parameters belong to which action: sending `text` with\n * `action: \"read\"` is a caller mistake the tool reports instead of quietly\n * ignoring, because a silently dropped parameter is how a model learns the\n * wrong tool.\n *\n * Authorization is the calling Session, read from the tool run context and\n * never from an argument. A model cannot name a Session, so it cannot reach\n * another Session's terminal even by guessing an id; the registry's ownership\n * check sees the guessed id as unknown.\n *\n * @module dsh-remote-workspace/tools/terminal\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport { defineTool } from '@deepseek-ai/dsh-tools'\nimport { TerminalRegistryError, type TerminalRead, type TerminalRegistry, type TerminalView, type TerminalWait } from '../terminal/host/registry.ts'\n\n/** The one tool's actions, in the order the description lists them. */\nconst ACTIONS = ['list', 'read', 'send', 'keys', 'wait'] as const\n\n/** One of the tool's actions. */\ntype Action = (typeof ACTIONS)[number]\n\n/**\n * Which actions each optional parameter belongs to.\n *\n * This table is the whole per-action contract: a parameter present in a call\n * whose action is not in its list is refused, and no action handler has to\n * re-state the rule.\n */\nconst PARAM_ACTIONS: Readonly<Record<string, readonly Action[]>> = {\n terminal: ['read', 'send', 'keys', 'wait'],\n text: ['send'],\n enter: ['send'],\n keys: ['keys'],\n lines: ['read'],\n offset: ['read', 'wait'],\n match: ['wait'],\n regex: ['wait'],\n timeoutMs: ['wait'],\n}\n\n/** One terminal's canonical value, the shape `list` returns. */\ninterface TerminalListValue {\n readonly terminals: TerminalView[]\n}\n\n/** One write's canonical value, the shape `send` and `keys` return. */\ninterface TerminalWriteValue {\n readonly id: string\n readonly wrote: { readonly bytes: number; readonly keys: number }\n}\n\ntype TerminalValue = TerminalListValue | TerminalRead | TerminalWriteValue | TerminalWait\n\n/** The canonical schemas, one branch per distinct return shape. */\nconst TERMINAL_VALUE_SCHEMA = {\n oneOf: [\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n terminals: {\n type: 'array',\n required: true,\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n id: { type: 'string', required: true },\n label: { type: 'string', required: true },\n cwd: { type: 'string', required: true },\n machine: { type: 'string', required: true },\n pid: { type: 'number', required: true },\n state: { type: 'string', required: true, enum: ['running', 'detached', 'exited'] },\n cols: { type: 'number', required: true },\n rows: { type: 'number', required: true },\n },\n },\n },\n },\n },\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n id: { type: 'string', required: true },\n offset: { type: 'number', required: true },\n text: { type: 'string', required: true },\n truncated: { type: 'boolean', required: true },\n },\n },\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n id: { type: 'string', required: true },\n wrote: {\n type: 'object',\n additionalProperties: false,\n required: true,\n properties: {\n bytes: { type: 'number', required: true },\n keys: { type: 'number', required: true },\n },\n },\n },\n },\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n id: { type: 'string', required: true },\n offset: { type: 'number', required: true },\n text: { type: 'string', required: true },\n matched: { type: 'boolean', required: true },\n reason: { type: 'string', required: true, enum: ['match', 'exit', 'timeout'] },\n },\n },\n ],\n} as const\n\n/**\n * Render one page of a terminal's output with the offset that resumes after it.\n *\n * The model sees only this text, so the resumable offset is part of it: that is\n * what lets a caller chain a wait after a read without guessing.\n * @param id - the terminal.\n * @param offset - the absolute byte offset just past `text`.\n * @param text - the page's text, possibly empty.\n * @param label - what the page is: a wait reason, `truncated`, `no output`, or nothing.\n * @returns the model-facing text.\n */\nfunction renderPage(id: string, offset: number, text: string, label = ''): string {\n const marker = `[${id}${label === '' ? '' : ` ${label}`} offset ${String(offset)}]`\n return text === '' ? marker : `${marker}\\n${text}`\n}\n\n/**\n * Render one canonical value for the model.\n *\n * A read and a wait answer with the terminal's own text under a marker that\n * carries what the page is and the offset that resumes after it; the other\n * actions say what they did in one line.\n * @param value - the canonical value the body returned.\n * @returns the model-facing text.\n */\nfunction renderValue(value: TerminalValue): string {\n if ('terminals' in value) {\n if (value.terminals.length === 0) return 'No terminal is open in this session.'\n return value.terminals\n .map(terminal => `${terminal.id} (${terminal.label}) ${terminal.state} · ${terminal.cwd} · ${terminal.machine}`)\n .join('\\n')\n }\n if ('wrote' in value) {\n return value.wrote.keys === 0\n ? `Wrote ${String(value.wrote.bytes)} byte(s) to ${value.id}.`\n : `Sent ${String(value.wrote.keys)} key(s) to ${value.id}.`\n }\n if ('matched' in value) return renderPage(value.id, value.offset, value.text, value.reason)\n return renderPage(\n value.id,\n value.offset,\n value.text,\n value.text === '' ? 'no output' : value.truncated ? 'truncated' : '',\n )\n}\n\n/**\n * Refuse a parameter that belongs to another action.\n * @param args - the validated, frozen model arguments.\n * @param action - the action being executed.\n * @throws TerminalRegistryError naming the parameter and the action that owns it.\n */\nfunction rejectForeign(args: Record<string, unknown>, action: Action): void {\n for (const [name, allowed] of Object.entries(PARAM_ACTIONS)) {\n if (args[name] === undefined || allowed.includes(action)) continue\n throw new TerminalRegistryError(\n `\"${name}\" is valid only with action ${allowed.join(' or ')}, not \"${action}\"`,\n )\n }\n}\n\n/**\n * Register the terminal tool on the host plane.\n *\n * A profile with no tool runtime is not a failure: the terminal socket and the\n * sidebar still work, and the model simply has no way to drive a shell.\n * @param ctx - the host context.\n * @param registry - the terminals a person's tabs have open.\n */\nexport function registerTerminalTool(ctx: Context, registry: TerminalRegistry): void {\n const tools = ctx.get('tools')\n if (tools === undefined) return\n\n tools.register(defineTool({\n name: 'terminal',\n description: 'Work with the terminals a person has open in the sidebar: read their output, type into them, send named keys, and wait for output. This is one terminal tool with an \"action\"; the other parameters apply only to the actions named in their descriptions. It never opens a terminal — a person opening a sidebar tab does that — and it never closes one: a shell is ended from the list a person picks one in, not from a tool call. A terminal whose tab lost its connection without closing is \"detached\": it stays addressable until it is closed, its process exits, or its session ends. Use \"list\" to see what is open, \"read\" for recent output, \"send\" to run a command (\"enter\" defaults to true), \"keys\" for named keys such as ctrl+c, and \"wait\" to search the recent output and settle when it matches or the command exits.',\n parameters: {\n action: {\n type: 'string',\n required: true,\n enum: ACTIONS,\n description: 'list | read | send | keys | wait',\n },\n terminal: {\n type: 'string',\n description: 'Terminal id from list. Required when more than one terminal is open in this session; omit it when exactly one is.',\n },\n text: {\n type: 'string',\n description: 'Literal text to type; valid only with action send.',\n },\n enter: {\n type: 'boolean',\n description: 'Append a carriage return after text; defaults to true. Valid only with action send.',\n },\n keys: {\n type: 'array',\n items: { type: 'string' },\n description: 'Logical key names (enter, esc, tab, backspace, up, down, left, right, ctrl+a…ctrl+z); valid only with action keys. Every name is checked before anything is written.',\n },\n lines: {\n type: 'number',\n description: 'Tail line count to read, default 200, max 2000; valid only with action read.',\n },\n offset: {\n type: 'number',\n description: 'Absolute byte offset to read or wait from. Omitted reads the tail, and starts a wait at the beginning of that same retained tail, so output that already arrived can match. Valid with read and wait; the render carries the offset that resumes after it.',\n },\n match: {\n type: 'string',\n description: 'Plain text to wait for; pass exactly one of match and regex. Valid only with action wait.',\n },\n regex: {\n type: 'string',\n description: 'Regular expression source to wait for; pass exactly one of match and regex. Valid only with action wait.',\n },\n timeoutMs: {\n type: 'number',\n description: 'Wait budget in milliseconds, default 30000, max 300000. A timeout returns reason \"timeout\" instead of failing. Valid only with action wait.',\n },\n },\n output: {\n schema: TERMINAL_VALUE_SCHEMA,\n render: (_args, value) => [{ type: 'text', text: renderValue(value as TerminalValue) }],\n },\n async execute(args, exec): Promise<TerminalValue> {\n // The calling Session is the authorization: the run context's Agent\n // carries it, and the model has no parameter that could name another.\n if (exec.agent === undefined) {\n throw new TerminalRegistryError('the terminal tool requires an Agent Session')\n }\n const sessionId = String(exec.agent.id)\n const action = args.action\n const raw = args as unknown as Record<string, unknown>\n rejectForeign(raw, action)\n\n if (action === 'list') return { terminals: registry.listFor(sessionId) }\n\n const entry = registry.requireOwned(sessionId, args.terminal)\n\n if (action === 'read') return registry.read(entry.id, args.offset, args.lines)\n\n if (action === 'send') {\n if (args.text === undefined) {\n throw new TerminalRegistryError('send requires \"text\"')\n }\n // Text and its carriage return are one write, so a line cannot arrive\n // interleaved with another caller's bytes.\n const payload = args.enter === false ? args.text : `${args.text}\\r`\n const bytes = await registry.write(entry.id, payload)\n return { id: entry.id, wrote: { bytes, keys: 0 } }\n }\n\n if (action === 'keys') {\n if (args.keys === undefined || args.keys.length === 0) {\n throw new TerminalRegistryError('keys requires a non-empty \"keys\" array')\n }\n return { id: entry.id, wrote: await registry.keys(entry.id, args.keys) }\n }\n\n if ((args.match === undefined) === (args.regex === undefined)) {\n throw new TerminalRegistryError(args.match === undefined\n ? 'wait requires exactly one of \"match\" and \"regex\"'\n : 'wait accepts only one of \"match\" and \"regex\"')\n }\n const request = {\n ...args.offset === undefined ? {} : { offset: args.offset },\n ...args.match === undefined ? {} : { match: args.match },\n ...args.regex === undefined ? {} : { regex: args.regex },\n ...args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs },\n }\n return await registry.wait(entry.id, request, exec.signal)\n },\n }))\n}\n","/**\n * dsh-remote-workspace — remote execution worlds for DeepSeek Harness.\n *\n * The plugin replaces the execution-world seams with routing versions: a path\n * that belongs to a remote anchor is served by that node's daemon over the\n * wire, and every other path is served by the factory implementation this\n * plugin composes in an isolated scope. The stock file, shell, terminal, and\n * language-server tools therefore run against a remote worktree unchanged.\n *\n * Nothing here inherits an implementation class. Each seam is registered with\n * `ctx.provide`, which is the primitive Cordis' own `Service` constructor\n * calls; the factory implementations are composed as separate instances and\n * reached through delegation.\n *\n * With no anchor configured the plugin is inert: every path classifies as\n * local and the routers delegate every call to the factory implementation.\n *\n * The same mount also serves the right Sidebar's terminal: one WebSocket that\n * turns into a PTY, resolving the Session's workspace and allocating the shell\n * through the terminal seam this plugin routes. It never asks which machine\n * owns a directory — a routed workspace simply starts its shell there — and it\n * is deliberately not confined by the Session's sandbox mode, because that\n * policy bounds what the *agent* may do while a terminal is the person's own\n * shell.\n *\n * That shell is also what the plugin's one model-facing terminal tool drives,\n * so a person and the model share one handle: closing the tab only detaches the\n * shell, an explicit end or the process exiting ends it, and the tool can read,\n * type into, and wait on it while its process lives.\n *\n * @module dsh-remote-workspace\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'\nimport { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'\nimport { SandboxedFileSystem } from '@deepseek-ai/dsh-fs-sandbox'\nimport { LocalSubprocessRuntime } from '@deepseek-ai/dsh-subprocess-local'\nimport { dshHomePath } from '@deepseek-ai/dsh-home-paths'\n// Type-only: the settings service merge (ctx.settings) the display namespace is\n// registered through.\nimport type {} from '@deepseek-ai/dsh-settings'\nimport z from '@deepseek-ai/schemastery'\nimport { homedir } from 'node:os'\nimport { join, posix } from 'node:path'\nimport { LocalTtyRuntime } from './local/tty.ts'\nimport { DEFAULT_TTY_GRACE_MS } from './tty.ts'\nimport { autoconnect } from './models/autoconnect.ts'\nimport { createNodeConnections, DEFAULT_HANDSHAKE_TIMEOUT_MS } from './models/machines.ts'\nimport { classifyPath } from './models/routing.ts'\nimport { createWorktreeManager, workspaceLabel } from './models/worktrees.ts'\nimport { registerNodeApi } from './plugin/api.ts'\nimport { createRoutingFileSystem } from './plugin/routing/fs.ts'\nimport { createRoutingShellExecutor } from './plugin/routing/shell.ts'\nimport { createRoutingSubprocessRuntime } from './plugin/routing/subprocess.ts'\nimport { createRoutingTty } from './plugin/routing/tty.ts'\nimport { AGENT_VERSION } from './remote/agent/install.ts'\nimport { DEFAULT_FORWARD_TIMEOUT_MS } from './remote/ssh.ts'\nimport { createAnchorStore } from './storage/anchors.ts'\nimport { createNodeRegistry, LOCAL_NODE_ID } from './storage/nodes.ts'\nimport type { NodeId } from './storage/nodes.ts'\nimport { createRepoStore } from './storage/repos.ts'\nimport { TerminalDisplaySchema } from './terminal/host/display.ts'\nimport { createTerminalRegistry, type TerminalSettings } from './terminal/host/registry.ts'\nimport { TERMINAL_DISPLAY_NAMESPACE } from './terminal/shared/display.ts'\nimport { registerTerminalSocket } from './terminal/host/socket.ts'\nimport { SOCKET_PATH } from './terminal/shared/wire.ts'\nimport { registerTerminalTool } from './tools/terminal.ts'\n\n/** Plugin name used by the Loader and by diagnostics. */\nexport const name = 'dsh-remote-workspace'\n\n/**\n * Services this plugin needs before it activates. `sandboxPolicy` is required\n * by the composed local delegate, and declaring it here keeps provision of\n * `ctx.fs` behind that dependency rather than racing it. The Sidebar terminal\n * reads the session store and the terminal seam it serves, so it looks both up\n * dynamically rather than making them activation dependencies.\n */\nexport const inject = ['sandboxPolicy']\n\n/**\n * The stock rows providing the seams this plugin routes. The host plane holds\n * one implementation per service, so a deployment disables these in its own\n * patch layer before this plugin can publish; see the README's install section.\n */\nconst STOCK_ROWS = ['subprocess', 'fs-sandbox', 'bash-sandbox', 'pwsh-sandbox']\n\n/**\n * Publish one routing seam, naming the profile edit when a stock row got there\n * first.\n *\n * `ctx.provide` refuses a service that already has a provider: the failure a\n * deployment sees when it added this plugin without disabling the stock rows.\n * That refusal names the row, not the fix, so this reports the fix.\n *\n * The value is cast because each seam type extends `Service`, whose protected\n * members make it nominal: a plain object cannot be assigned structurally.\n * Every router is checked against its seam's contract factory instead.\n * @param ctx - the host context.\n * @param service - the service being published.\n * @param value - the routing implementation.\n * @returns the disposer `ctx.provide` returned.\n */\nfunction publishSeam(ctx: Context, service: 'fs' | 'subprocess' | 'shell' | 'tty', value: unknown): unknown {\n try {\n return ctx.provide(service as never, value as never)\n } catch (cause) {\n const hint = `${name}: ctx.${service} already has a provider, so this router is inert. Disable `\n + `${STOCK_ROWS.join(', ')} in the profile's cordis.patch.yml — see the README's install section.`\n // This runs inside an `inject` callback, whose rejection the registry keeps\n // rather than surfaces, and `dsh web` prints no logger record: without the\n // write, a deployment that skipped the profile edit boots looking healthy\n // while the stock provider keeps serving every remote path.\n ctx.logger.error(hint)\n process.stderr.write(`${hint}\\n`)\n throw new Error(hint, { cause })\n }\n}\n\n/** Deployment-varying choices for this plugin. */\nexport interface Config {\n /**\n * Directory holding the node registry. Defaults to\n * `$DSH_HOME/remote-worktrees`, which the anchor directory store also uses.\n */\n dataDir?: string\n /**\n * Remote binary a host-resolved ripgrep is rewritten to. Defaults to `rg`.\n */\n remoteRipgrep?: string\n /**\n * Root directory every managed worktree is cut under, on every machine.\n *\n * Defaults to `~/.dsh/worktrees`, resolved against the machine's own home; an\n * absolute path is used verbatim. A checkout lands at\n * `<root>/<repository>/<name>`, never inside the repository.\n */\n worktreeRoot?: string\n /**\n * How long the SSH forward may take to start accepting connections, in\n * milliseconds. Defaults to {@link DEFAULT_FORWARD_TIMEOUT_MS}.\n *\n * A deployment across a slow link or through a bastion raises this; the\n * failure it prevents is a forward that was still negotiating when the\n * budget ran out.\n */\n sshForwardTimeoutMs?: number\n /**\n * How long the daemon may take to answer the handshake once its forward is\n * up, in milliseconds. Defaults to {@link DEFAULT_HANDSHAKE_TIMEOUT_MS}.\n *\n * This is what turns \"the daemon is not running\" into a report instead of a\n * call that never returns, so a deployment should not raise it far.\n */\n daemonHandshakeTimeoutMs?: number\n /**\n * Program the Sidebar terminal runs. Unset — the default — runs the machine's\n * own login shell, resolved on whichever machine owns the workspace, so one\n * deployment spanning a Mac and a Linux node starts zsh on one and bash on\n * the other.\n */\n shell?: string\n /**\n * Arguments after {@link Config.shell}. Defaults to `['-l']`, a login shell,\n * which is what makes the user's own profile load. Ignored while `shell` is\n * unset.\n */\n shellArgs?: string[]\n /** TERM-to-KILL grace for one terminal session, in milliseconds. */\n graceMs?: number\n /**\n * Safety valve: how long a terminal whose browser socket went away is kept,\n * in milliseconds. Unset or `0` — the default — keeps it for as long as its\n * process lives, because a browser's absence is not the shell's business.\n *\n * A positive value releases a terminal nobody has come back for that long\n * after the last socket left. A tab that is closed ends its terminal\n * immediately regardless, because the browser sends `close` first.\n */\n detachGraceMs?: number\n}\n\n/** Validated plugin config. */\nexport const Config: z<Config> = z.object({\n dataDir: z.string(),\n remoteRipgrep: z.string(),\n worktreeRoot: z.string(),\n sshForwardTimeoutMs: z.number().step(1).min(1),\n daemonHandshakeTimeoutMs: z.number().step(1).min(1),\n shell: z.string(),\n shellArgs: z.array(z.string()),\n graceMs: z.number().step(1).min(1),\n detachGraceMs: z.number().step(1).min(0),\n})\n\n/** Root managed worktrees are cut under when the config names none. */\nconst DEFAULT_WORKTREE_ROOT = '~/.dsh/worktrees'\n\n/**\n * Mount the plugin.\n *\n * @param ctx - the host context this plugin was mounted on.\n * @param config - the validated plugin config.\n */\nexport async function apply(ctx: Context, config: Config): Promise<void> {\n const configuredRoot = config.worktreeRoot ?? DEFAULT_WORKTREE_ROOT\n if (configuredRoot !== '~' && !configuredRoot.startsWith('~/') && !configuredRoot.startsWith('/')) {\n throw new Error(`worktreeRoot must be absolute, \"~\", or \"~/…\": \"${configuredRoot}\"`)\n }\n\n const dataDir = config.dataDir ?? dshHomePath('remote-worktrees')\n\n const registry = createNodeRegistry({ file: join(dataDir, 'nodes.json') })\n await registry.load()\n\n const anchorStore = createAnchorStore({ root: join(dataDir, 'anchors') })\n await anchorStore.load()\n\n const repos = createRepoStore({ file: join(dataDir, 'repos.json') })\n await repos.load()\n\n const connections = createNodeConnections({\n cacheDir: join(dataDir, 'agents'),\n agentVersion: AGENT_VERSION,\n sshForwardTimeoutMs: config.sshForwardTimeoutMs ?? DEFAULT_FORWARD_TIMEOUT_MS,\n daemonHandshakeTimeoutMs: config.daemonHandshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS,\n })\n // A deployment that just loaded should not need a person to click Connect per\n // machine, and a restored session should find its workspace reachable. The\n // pass gives up quietly, so its failures are read from the section.\n ctx.effect(() => {\n const stop = autoconnect({\n records: () => registry.list(),\n connections,\n status: nodeId => connections.status(nodeId),\n refreshMs: 30_000,\n })\n return () => {\n stop()\n connections.dispose()\n }\n })\n\n // The local machine reads paths and runs git in this process instead of over\n // a connection, so which nodes those are is settled once.\n const isLocalNode = (nodeId: NodeId): boolean => registry.get(nodeId)?.transport.kind === 'local'\n\n /** Where managed checkouts live on one machine. */\n const worktreeRoot = (nodeId: NodeId): string => {\n if (!configuredRoot.startsWith('~')) return configuredRoot\n const home = isLocalNode(nodeId) ? homedir() : connections.status(nodeId).info?.homedir\n if (home === undefined) {\n throw new Error(`the home directory of \"${nodeId}\" is unknown; connect that machine first`)\n }\n return posix.join(home, configuredRoot.slice(1))\n }\n\n const worktrees = createWorktreeManager({\n anchors: anchorStore,\n repos,\n channel: nodeId => connections.channel(nodeId),\n isLocalNode,\n worktreeRoot,\n workspace: {\n async register(anchor) {\n // The title is what a person reads in the workspace list, so it names\n // the checkout, the repository, and the machine — never the opaque ids\n // this plugin routes by. A machine whose record is gone leaves its id\n // as the last word on the title.\n const node = registry.get(anchor.nodeId)\n await workspaceRegistry(ctx)?.create(anchor.anchorPath, workspaceLabel({\n machine: node?.title ?? anchor.nodeId,\n repoPath: anchor.repoPath,\n repoName: repos.find({ nodeId: anchor.nodeId, repoPath: anchor.repoPath })?.name,\n // A directory opened as itself names no checkout; the repository is\n // the end of its title.\n ...anchor.kind === 'worktree' ? { name: anchor.name } : {},\n }))\n },\n async unregister(anchor) {\n const service = workspaceRegistry(ctx)\n if (service === undefined) return\n const record = await service.resolveByPath(anchor.anchorPath)\n if (record !== undefined) await service.delete(record.id)\n },\n async registered(anchor) {\n const record = await workspaceRegistry(ctx)?.resolveByPath(anchor.anchorPath)\n return record !== undefined\n },\n },\n })\n\n const subprocessScope = ctx.isolate('subprocess')\n subprocessScope.plugin(LocalSubprocessRuntime)\n subprocessScope.inject(['subprocess'], (scoped) => {\n const router = createRoutingSubprocessRuntime({\n localProc: scoped.subprocess,\n anchors: () => anchorStore.routes(),\n channel: nodeId => connections.channel(nodeId),\n ...config.remoteRipgrep === undefined ? {} : { remoteRipgrep: config.remoteRipgrep },\n })\n return publishSeam(ctx, 'subprocess', router)\n })\n\n const fsScope = ctx.isolate('fs')\n fsScope.plugin(SandboxedFileSystem, {})\n fsScope.inject(['fs'], (scoped) => {\n const router = createRoutingFileSystem({\n localFs: scoped.fs,\n anchors: () => anchorStore.routes(),\n channel: nodeId => connections.channel(nodeId),\n })\n return publishSeam(ctx, 'fs', router)\n })\n\n // Two independent shell scopes: a local command keeps the host sandbox wrap,\n // a remote command must never touch it. Each scope's `subprocess` resolves to\n // the routing runtime above, so the remote delegate's spawn lands on the node.\n const localShellScope = ctx.isolate('shell')\n localShellScope.plugin(SandboxBashExecutor, {})\n const remoteShellScope = ctx.isolate('shell')\n remoteShellScope.plugin(LocalBashExecutor)\n // Each scope waits for its own delegate; the routing shell is published only\n // once both exist, so no consumer can observe a half-routed executor.\n localShellScope.inject(['shell'], (localScoped) => {\n remoteShellScope.inject(['shell'], (remoteScoped) => {\n const router = createRoutingShellExecutor({\n localShell: localScoped.shell,\n remoteShell: remoteScoped.shell,\n anchors: () => anchorStore.routes(),\n })\n return publishSeam(ctx, 'shell', router)\n })\n })\n\n // The terminal router composes the local PTY provider and answers for a\n // node's directories with the daemon's own terminals. The local provider is\n // the node-pty one rather than the subprocess seam because a terminal is a\n // view whose size a person changes while the shell keeps running, and only a\n // provider that holds the PTY can carry that.\n const ttyScope = ctx.isolate('tty')\n ttyScope.plugin(LocalTtyRuntime)\n ttyScope.inject(['tty'], (scoped) => {\n const router = createRoutingTty({\n localTty: scoped.tty,\n anchors: () => anchorStore.routes(),\n channel: nodeId => connections.channel(nodeId),\n })\n return publishSeam(ctx, 'tty', router)\n })\n\n // The Sidebar terminal is registered here rather than behind `ctx.tty`: the\n // socket handler reads the seam when a person opens a terminal, so a profile\n // that composes another provider still gets a working terminal.\n const configuredShell = config.shell !== undefined && config.shell.length > 0 ? config.shell : undefined\n const terminalSettings: TerminalSettings = {\n // An unnamed shell is the machine's own login shell, reached through `sh`\n // so the node's profile loads; a named one keeps the caller's arguments, or\n // gets a login shell's.\n shell: configuredShell ?? '/bin/sh',\n shellArgs: configuredShell === undefined\n ? ['-c', 'exec \"${SHELL:-/bin/sh}\" -l']\n : config.shellArgs !== undefined && config.shellArgs.length > 0 ? config.shellArgs : ['-l'],\n // A terminal is the only consumer here that cares, and both names are what\n // every full-screen program reads to decide what it may draw.\n env: { TERM: 'xterm-256color', COLORTERM: 'truecolor' },\n graceMs: config.graceMs ?? DEFAULT_TTY_GRACE_MS,\n detachGraceMs: config.detachGraceMs ?? 0,\n }\n\n // One directory answers two questions — which machine serves it, and where a\n // shell in it lands — so one helper answers both.\n const routeOf = (cwd: string) => classifyPath(cwd, undefined, anchorStore.routes())\n // The registry is the one handle on the shells a person's tabs have open: the\n // socket registers through it, the model-facing tool addresses it, and each\n // side releases only what it owns.\n const terminals = createTerminalRegistry({\n // Read at open time, not at composition time: the routing provider is\n // published asynchronously from its own scope, exactly as the socket's\n // comment below explains.\n spawn: (request) => {\n const tty = ctx.get('tty')\n if (tty === undefined) throw new Error('no terminal provider is composed')\n return tty.spawn(request)\n },\n settings: terminalSettings,\n machine: (cwd) => {\n // The route that owns this directory names the machine; a directory no\n // anchor claims runs locally.\n const route = routeOf(cwd)\n const nodeId = route.kind === 'remote' ? route.nodeId : LOCAL_NODE_ID\n return { label: registry.get(nodeId)?.title ?? nodeId }\n },\n directory: (cwd) => {\n // The same route decides where a shell really lands: one in a routed\n // workspace runs at the checkout on that machine.\n const route = routeOf(cwd)\n return route.kind === 'remote' ? route.remotePath : cwd\n },\n })\n // The management API reads the terminal table too: the panel lists the\n // shells a Session has open and can end one no tab holds, so it is registered\n // once the table exists.\n registerNodeApi(ctx, { registry, repos, connections, worktrees, worktreeRoot, terminals })\n registerTerminalSocket(ctx, SOCKET_PATH, terminals)\n registerTerminalTool(ctx, terminals)\n\n // The terminal's display preferences are this plugin's own settings: the\n // browser half binds a scope to the namespace and draws the card that edits\n // it in the Plugins settings page. Acquired softly — a deployment without the\n // settings service still gets a terminal, drawn with the schema's defaults.\n ctx.inject(['settings'], (settingsCtx) => {\n settingsCtx.settings.register(TERMINAL_DISPLAY_NAMESPACE, TerminalDisplaySchema)\n })\n\n // Session end releases that Session's terminals. There is no host-plane\n // per-Session disposer to hook, but `agent/disposed` is the event the agent\n // registry emits as one leaves, and it carries the exact Session identity.\n ctx.on('agent/disposed', ({ agent }) => {\n void terminals.releaseSession(String(agent.id))\n })\n // The plugin's own unload covers whatever a Session's end did not; the\n // socket's disposal above has already ended its own terminals by then.\n ctx.effect(() => () => terminals.disposeAll(), 'dsh-terminal: registry disposal')\n}\n\n/** The handle a workspace record is addressed by. */\ninterface WorkspaceHandle {\n readonly id: string\n}\n\n/**\n * The slice of the workspace seam this plugin uses.\n *\n * Declared structurally rather than imported: the plugin depends on the\n * operations it calls, not on the registry package, so a deployment composing\n * a different registry with the same operations still works.\n */\ninterface WorkspaceRegistry {\n /**\n * Register an existing directory as a workspace.\n * @param path - the canonical directory path.\n * @param title - the display title.\n * @returns the record, existing or created.\n */\n create(path: string, title?: string): Promise<WorkspaceHandle>\n resolveByPath(path: string): Promise<WorkspaceHandle | undefined>\n delete(id: string): Promise<boolean>\n}\n\n/**\n * The deployment's workspace registry, when one is composed.\n *\n * Read dynamically rather than injected: a headless or SDK profile may compose\n * no registry, and the plugin must still load and route there.\n * @param ctx - the host context.\n * @returns the registry, or undefined when this deployment composes none.\n */\nfunction workspaceRegistry(ctx: Context): WorkspaceRegistry | undefined {\n return ctx.get('workspaceRegistry')\n}\n\n"],"mappings":"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"}
|