Files
Termix/plugins/ssh-terminal/backend/index.mjs
T
LukeGus c94a453f1e feat: ship the ssh terminal as a first-party in-process plugin
Adds a hardcoded process:transport-owner tier so the terminal can own its
WebSocket server and ssh2 clients, which cannot cross the worker boundary.
2026-09-21 08:28:50 -05:00

84 lines
3.1 KiB
JavaScript

/**
* SSH Terminal - first-party, in-process plugin.
*
* This plugin runs on the main thread rather than in a worker, because it IS
* the SSH transport rather than a consumer of one: it owns a WebSocket server
* and long-lived ssh2 clients, and neither a live socket nor an ssh2 Client can
* cross the structured-clone postMessage boundary a worker plugin talks over.
* See src/backend/plugins/first-party.ts for why that tier is a hardcoded
* allowlist and not something a manifest can ask for.
*
* The terminal's implementation stays in core (src/backend/hosts/terminal/)
* rather than being copied in here. Two reasons:
* - session-manager has six consumers outside the terminal (terminal routes,
* open-tabs, user-image-storage, collab, and session-sharing twice), so it
* is shared infrastructure, not plugin-private code.
* - the terminal needs plaintext host credentials to connect, which ctx.hosts
* deliberately never returns (PluginHostView is a 13-field allowlist that
* excludes password, key and passphrase).
*
* What this plugin owns is the lifecycle: enabling it starts the WS server,
* disabling it closes live sessions and frees the port.
*/
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
/**
* Resolves the compiled core terminal module.
*
* The plugin ships beside dist/backend, so in a built server the module is at
* dist/backend/backend/hosts/terminal/index.js. Under vitest and dev the
* plugin is loaded from the repo instead, where the source .ts is the thing
* that exists and tsx is already handling it.
*/
async function loadTerminalModule() {
const here = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const candidates = [
// Built: plugins/ssh-terminal/backend -> dist/backend/backend/...
path.resolve(here, "../../../backend/backend/hosts/terminal/index.js"),
// Repo layout, compiled output present.
path.resolve(here, "../../../dist/backend/backend/hosts/terminal/index.js"),
// Repo layout, running from source under tsx.
path.resolve(here, "../../../src/backend/hosts/terminal/index.ts"),
];
for (const candidate of candidates) {
try {
require.resolve(candidate);
} catch {
continue;
}
return import(new URL(`file://${candidate.replace(/\\/g, "/")}`).href);
}
throw new Error(
`ssh-terminal could not locate the terminal module. Looked in:\n ${candidates.join("\n ")}`,
);
}
let terminal = null;
export async function activate(ctx) {
terminal = await loadTerminalModule();
await terminal.startTerminalServer();
// Published so collab and session-sharing can reach live sessions without
// importing a plugin. Revoked automatically when this plugin is disabled.
ctx.registry.provide("terminal.sessions", terminal.sessionManager ?? null);
ctx.log.info(
`SSH terminal listening on ${terminal.TERMINAL_WS_PORT ?? 30002}`,
);
}
export async function deactivate() {
if (!terminal) return;
await terminal.stopTerminalServer();
terminal = null;
}