summaryrefslogtreecommitdiffhomepage
path: root/packages/ssh/src/extension.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 01:09:39 +0900
committerAdam Malczewski <[email protected]>2026-06-27 01:09:39 +0900
commit61e45e60d699ed1ca46f94a8f181c92a940317c6 (patch)
tree2892d9773c5a8e367e1e58cdb1e88d9c6ad3fe6d /packages/ssh/src/extension.ts
parent63c7e64532e85e0bbdd6d9ac6825d8f86be98e7a (diff)
parent727c98c9dae516a2070eb950410314380a20c974 (diff)
downloaddispatch-61e45e60d699ed1ca46f94a8f181c92a940317c6.tar.gz
dispatch-61e45e60d699ed1ca46f94a8f181c92a940317c6.zip
Merge branch 'feature/indent-change' into dev
Diffstat (limited to 'packages/ssh/src/extension.ts')
-rw-r--r--packages/ssh/src/extension.ts240
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);
+ },
};