diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 15:34:59 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 15:34:59 +0900 |
| commit | 350b9b8e247bb1c24f49a884fdade18e44b115eb (patch) | |
| tree | 502df8f103d53f1089eeee7278865d73077f9edc /packages/exec-backend/src/extension.ts | |
| parent | 3647acfb7f078b2f035dd325f6959980c5b46c9a (diff) | |
| download | dispatch-350b9b8e247bb1c24f49a884fdade18e44b115eb.tar.gz dispatch-350b9b8e247bb1c24f49a884fdade18e44b115eb.zip | |
feat(ssh): wave 5a — exec-backend remote-backend factory handle
exec-backend declares remoteExecBackendFactoryHandle (a consumer-defined
ServiceHandle<(computerId) => ExecBackend>) that the ssh package will provide
(standard→core layering). The resolver's computerId-set branch now lazy-looks-up
this factory (at tool-execute time, runtime) and calls it; if ssh isn't loaded,
getService throws → a clear 'SSH remote execution is not configured' error. The
computerId-undefined (local) branch is byte-identical to before.
This is the seam wave 5b (the ssh package) plugs into. +tests for both branches.
Verified: tsc -b EXIT 0, biome clean. No merge or push.
Diffstat (limited to 'packages/exec-backend/src/extension.ts')
| -rw-r--r-- | packages/exec-backend/src/extension.ts | 61 |
1 files changed, 43 insertions, 18 deletions
diff --git a/packages/exec-backend/src/extension.ts b/packages/exec-backend/src/extension.ts index c07b7a8..9d6840a 100644 --- a/packages/exec-backend/src/extension.ts +++ b/packages/exec-backend/src/extension.ts @@ -1,8 +1,11 @@ -import type { Extension, Manifest } from "@dispatch/kernel"; +import type { Extension, HostAPI, Manifest } from "@dispatch/kernel"; import type { ExecBackend } from "./backend.js"; import { localExecBackend } from "./local.js"; -import type { ExecBackendResolver } from "./service.js"; -import { execBackendHandle } from "./service.js"; +import { + type ExecBackendResolver, + execBackendHandle, + remoteExecBackendFactoryHandle, +} from "./service.js"; export const manifest: Manifest = { id: "exec-backend", @@ -15,33 +18,55 @@ export const manifest: Manifest = { }; /** - * The resolver provided by this extension. + * Build the `ExecBackendResolver` for a given host. * - * - `computerId` undefined → `LocalExecBackend` (today's local behavior). - * - `computerId` set → throws. Remote execution is wired by `host-bin` + the - * `ssh` package in a later wave (`SshExecBackend` implements the same - * `ExecBackend` interface). For now only the local path exists — failing - * loudly here is safer than silently running locally when remote was requested. + * - `computerId` undefined → `localExecBackend` (byte-identical local path; + * no host lookup, no remote machinery — unchanged from the original behavior). + * - `computerId` set → remote: lazily look up the factory the `ssh` extension + * provides via `remoteExecBackendFactoryHandle` and call it with the alias. + * The lookup is deferred to resolve time (tool-EXECUTE time, after every + * extension has activated), so a missing provider (ssh not loaded) degrades + * gracefully into a clear error instead of crashing activation. This mirrors + * the lazy `host.getService(lspServiceHandle)` try/catch pattern `tool-edit-file` + * uses for its diagnostics hook. + * + * The resolver stays SYNCHRONOUS and side-effect-free with respect to + * connections: looking up the factory and calling it returns a backend whose + * methods are async, so any remote connection acquisition happens lazily + * inside the first backend method call, not at resolve time. */ -function resolveBackend(computerId?: string): ExecBackend { - if (computerId === undefined) return localExecBackend; - throw new Error( - `Remote execution (computerId="${computerId}") is not yet configured. ` + - "The SSH backend will be wired by the ssh package.", - ); +function createResolver(host: HostAPI): ExecBackendResolver { + return (computerId?: string): ExecBackend => { + if (computerId === undefined) return localExecBackend; + // computerId set → remote. Look up the factory the `ssh` extension provides. + // `host.getService` throws when nothing provided the handle (ssh not loaded); + // convert that into a clear "not configured" error rather than a crash. + let factory: (computerId: string) => ExecBackend; + try { + factory = host.getService(remoteExecBackendFactoryHandle); + } catch { + throw new Error( + `SSH remote execution is not configured: the ssh extension is not loaded ` + + `(requested computerId="${computerId}"). Load the ssh package to enable remote execution.`, + ); + } + return factory(computerId); + }; } /** * Factory: create the `exec-backend` core extension. * - * `activate` provides the local-only `ExecBackendResolver` via the typed - * service handle. Remote resolution is added in a later wave. + * `activate` captures the host and provides the `ExecBackendResolver` via the + * typed service handle. The resolver lazily delegates the remote branch to a + * factory the `ssh` extension will provide (see `remoteExecBackendFactoryHandle`); + * until `ssh` is loaded, a remote request fails with a clear error. */ export function createExecBackendExtension(): Extension { return { manifest, activate(host) { - const resolver: ExecBackendResolver = resolveBackend; + const resolver: ExecBackendResolver = createResolver(host); host.provideService(execBackendHandle, resolver); }, }; |
