diff options
Diffstat (limited to 'packages/ssh/src/extension.ts')
| -rw-r--r-- | packages/ssh/src/extension.ts | 240 |
1 files changed, 120 insertions, 120 deletions
diff --git a/packages/ssh/src/extension.ts b/packages/ssh/src/extension.ts index 3294908..c098ba0 100644 --- a/packages/ssh/src/extension.ts +++ b/packages/ssh/src/extension.ts @@ -22,24 +22,24 @@ import type { Extension, HostAPI, Logger, Manifest } from "@dispatch/kernel"; import { computerServiceHandle } from "@dispatch/transport-http/dist/seam.js"; import { Client } from "ssh2"; import { - resolveComputer as resolveComputerFromConfig, - type SshConfigResolveEnv, + resolveComputer as resolveComputerFromConfig, + type SshConfigResolveEnv, } from "./config.js"; import { createSshService, type SshServiceDeps } from "./service.js"; export const manifest: Manifest = { - id: "ssh", - name: "SSH Remote Execution", - version: "0.0.0", - apiVersion: "^0.1.0", - trust: "bundled", - activation: "eager", - // exec-backend owns the resolver; ssh provides the remote factory it looks up - // at runtime (lazy, post-activation). Declaring dependsOn keeps the DAG honest - // even though the lookup itself is deferred to tool-execute time. - dependsOn: ["exec-backend"], - capabilities: { fs: true, network: true }, - contributes: { services: ["ssh", "exec-backend/remote-factory"] }, + id: "ssh", + name: "SSH Remote Execution", + version: "0.0.0", + apiVersion: "^0.1.0", + trust: "bundled", + activation: "eager", + // exec-backend owns the resolver; ssh provides the remote factory it looks up + // at runtime (lazy, post-activation). Declaring dependsOn keeps the DAG honest + // even though the lookup itself is deferred to tool-execute time. + dependsOn: ["exec-backend"], + capabilities: { fs: true, network: true }, + contributes: { services: ["ssh", "exec-backend/remote-factory"] }, }; /** @@ -48,35 +48,35 @@ export const manifest: Manifest = { * against a live sshd (the integration test — no `@dispatch/*` mocking). */ export function makeSshExtension(deps: SshServiceDeps): Extension { - const store: { close: (() => Promise<void>) | null } = { close: null }; - - return { - manifest, - activate(host: HostAPI) { - const { service, pool, remoteFactory } = createSshService(deps); - store.close = () => pool.closeAll(); - - host.provideService(remoteExecBackendFactoryHandle, remoteFactory); - host.provideService(computerServiceHandle, service); - - host.logger.info("ssh extension activated"); - }, - async deactivate() { - await store.close?.(); - store.close = null; - }, - }; + const store: { close: (() => Promise<void>) | null } = { close: null }; + + return { + manifest, + activate(host: HostAPI) { + const { service, pool, remoteFactory } = createSshService(deps); + store.close = () => pool.closeAll(); + + host.provideService(remoteExecBackendFactoryHandle, remoteFactory); + host.provideService(computerServiceHandle, service); + + host.logger.info("ssh extension activated"); + }, + async deactivate() { + await store.close?.(); + store.close = null; + }, + }; } // ─── real node:fs + ssh2 adapters (production wiring) ───────────────────── /** Path candidates for `dispatch.toml` (global + project-local). */ function dispatchTomlPaths(): readonly string[] { - const paths = [ - join(homedir(), ".config", "dispatch", "dispatch.toml"), // global - join(process.cwd(), "dispatch.toml"), // project-local - ]; - return paths; + const paths = [ + join(homedir(), ".config", "dispatch", "dispatch.toml"), // global + join(process.cwd(), "dispatch.toml"), // project-local + ]; + return paths; } /** @@ -85,30 +85,30 @@ function dispatchTomlPaths(): readonly string[] { * Uses `Bun.TOML.parse` (Bun's built-in TOML parser — zero deps). */ async function readRejectPatternsImpl(): Promise<readonly string[]> { - const patterns: string[] = []; - const seen = new Set<string>(); - - for (const path of dispatchTomlPaths()) { - try { - const text = await readFile(path, "utf8"); - const parsed = Bun.TOML.parse(text) as { - ssh?: { reject?: readonly string[] }; - }; - const list = parsed.ssh?.reject; - if (list !== undefined) { - for (const p of list) { - if (typeof p === "string" && !seen.has(p)) { - seen.add(p); - patterns.push(p); - } - } - } - } catch { - // File missing or parse error → skip silently. - } - } - - return patterns; + const patterns: string[] = []; + const seen = new Set<string>(); + + for (const path of dispatchTomlPaths()) { + try { + const text = await readFile(path, "utf8"); + const parsed = Bun.TOML.parse(text) as { + ssh?: { reject?: readonly string[] }; + }; + const list = parsed.ssh?.reject; + if (list !== undefined) { + for (const p of list) { + if (typeof p === "string" && !seen.has(p)) { + seen.add(p); + patterns.push(p); + } + } + } + } catch { + // File missing or parse error → skip silently. + } + } + + return patterns; } /** @@ -118,67 +118,67 @@ async function readRejectPatternsImpl(): Promise<readonly string[]> { * resolved fresh from `~/.ssh/config` on each acquire (decision #4). */ export function createSshServiceDeps(hostLogger: Logger): SshServiceDeps { - const sshDir = join(homedir(), ".ssh"); - const configPath = join(sshDir, "config"); - const knownHostsPath = join(sshDir, "known_hosts"); - - const readConfigText = async (): Promise<string> => readFile(configPath, "utf8"); - const readFileText = async (path: string): Promise<string> => readFile(path, "utf8"); - const defaultUser = process.env.USER ?? homedir().split("/").pop() ?? "root"; - - /** Read the reject list fresh from `dispatch.toml` on each call. */ - const readRejectPatterns = async (): Promise<readonly string[]> => readRejectPatternsImpl(); - - /** - * Build the resolve env (config + known_hosts + reject patterns) — shared by - * the service methods and the pool's resolveComputer dep. - */ - async function readEnv(): Promise<SshConfigResolveEnv> { - const [configText, knownHostsText, rejectPatterns] = await Promise.all([ - readConfigText().catch(async () => ""), - readFileText(knownHostsPath).catch(async () => ""), - readRejectPatterns(), - ]); - const base: SshConfigResolveEnv = { - configText, - knownHostsText, - defaultUser, - homeDir: homedir(), - }; - return rejectPatterns.length > 0 ? { ...base, rejectPatterns } : base; - } - - return { - logger: hostLogger, - homeDir: homedir(), - defaultUser, - knownHostsPath, - readConfigText, - readFileText, - readRejectPatterns, - pathExists: async (path: string) => - access(path) - .then(() => true) - .catch(() => false), - appendKnownHosts: async (path: string, line: string) => - appendFile(path, `${line}\n`, { encoding: "utf8" }), - newClient: () => new Client(), - // Resolve a computer alias → `Computer` by reading the live config + - // known_hosts. Reads fresh on each call (a Host block or known_hosts - // entry added between turns is picked up). Does NOT apply the reject - // list — the pool needs to connect even to hosts hidden from the catalog. - resolveComputer: async (alias: string) => { - const env = await readEnv(); - return resolveComputerFromConfig(alias, env); - }, - }; + const sshDir = join(homedir(), ".ssh"); + const configPath = join(sshDir, "config"); + const knownHostsPath = join(sshDir, "known_hosts"); + + const readConfigText = async (): Promise<string> => readFile(configPath, "utf8"); + const readFileText = async (path: string): Promise<string> => readFile(path, "utf8"); + const defaultUser = process.env.USER ?? homedir().split("/").pop() ?? "root"; + + /** Read the reject list fresh from `dispatch.toml` on each call. */ + const readRejectPatterns = async (): Promise<readonly string[]> => readRejectPatternsImpl(); + + /** + * Build the resolve env (config + known_hosts + reject patterns) — shared by + * the service methods and the pool's resolveComputer dep. + */ + async function readEnv(): Promise<SshConfigResolveEnv> { + const [configText, knownHostsText, rejectPatterns] = await Promise.all([ + readConfigText().catch(async () => ""), + readFileText(knownHostsPath).catch(async () => ""), + readRejectPatterns(), + ]); + const base: SshConfigResolveEnv = { + configText, + knownHostsText, + defaultUser, + homeDir: homedir(), + }; + return rejectPatterns.length > 0 ? { ...base, rejectPatterns } : base; + } + + return { + logger: hostLogger, + homeDir: homedir(), + defaultUser, + knownHostsPath, + readConfigText, + readFileText, + readRejectPatterns, + pathExists: async (path: string) => + access(path) + .then(() => true) + .catch(() => false), + appendKnownHosts: async (path: string, line: string) => + appendFile(path, `${line}\n`, { encoding: "utf8" }), + newClient: () => new Client(), + // Resolve a computer alias → `Computer` by reading the live config + + // known_hosts. Reads fresh on each call (a Host block or known_hosts + // entry added between turns is picked up). Does NOT apply the reject + // list — the pool needs to connect even to hosts hidden from the catalog. + resolveComputer: async (alias: string) => { + const env = await readEnv(); + return resolveComputerFromConfig(alias, env); + }, + }; } /** Production extension: real `node:fs` + real `ssh2`. */ export const extension: Extension = { - manifest, - activate(host: HostAPI) { - const deps = createSshServiceDeps(host.logger); - makeSshExtension(deps).activate(host); - }, + manifest, + activate(host: HostAPI) { + const deps = createSshServiceDeps(host.logger); + makeSshExtension(deps).activate(host); + }, }; |
