feat: move terminal settings, look and ssh tools into ssh-terminal plugin

This commit is contained in:
LukeGus committed 2026-09-29 02:38:37 -05:00
1 parent ffa872ebd3
commit d9bba10612
242 files changed
+25071 -7207

No files matched your search

@@ -9,6 +9,7 @@ import {
type TestDb,
} from "@termix/plugin-sdk/testing";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import type { PluginHostSummary } from "@termix/plugin-sdk/backend";
import manifestJson from "../../manifest.json";
export const pluginDir = fileURLToPath(new URL("../..", import.meta.url));
@@ -32,7 +33,11 @@ export interface TestServer {
* the way core mounts it, with the acting user taken from a test header.
*/
export async function startServer(
options: { permissions?: string[]; settings?: Record<string, unknown> } = {},
options: {
permissions?: string[];
settings?: Record<string, unknown>;
hosts?: PluginHostSummary[];
} = {},
): Promise<TestServer> {
const db = await createTestDb(pluginDir);
for (const user of ["user-1", "user-2"]) {
@@ -51,6 +56,7 @@ export async function startServer(
router: () => (router = express.Router()),
permissions: options.permissions,
settings: options.settings,
hosts: options.hosts,
});
const { activate } = await import("../../src/backend/index.js");
@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import {
hostImportNormalizer,
hostPayloadLegacy,
} from "../../src/backend/host-import";
describe("hostImportNormalizer", () => {
it("reads a 2.8 export's terminalConfig into the host settings", () => {
expect(
hostImportNormalizer({
enableTerminal: false,
terminalConfig: {
fontSize: 16,
autoTmux: true,
keepaliveInterval: 30,
startupSnippetId: 2,
},
}),
).toEqual({
enableTerminal: false,
inheritAppearance: false,
fontSize: 16,
autoTmux: true,
});
});
it("reads the raw JSON a SQLite export carries", () => {
expect(
hostImportNormalizer({
terminalConfig: JSON.stringify({ localEcho: "on" }),
}),
).toEqual({ localEcho: "on" });
});
it("leaves terminalConfig alone when the export carries the plugin's own values", () => {
expect(
hostImportNormalizer({
pluginSettings: { "ssh-terminal": { autoTmux: false } },
terminalConfig: { autoTmux: true },
}),
).toBeNull();
});
it("imports nothing from a row without terminal values", () => {
expect(hostImportNormalizer({ name: "x" })).toBeNull();
});
});
describe("hostPayloadLegacy", () => {
it("puts the settings back in the 2.8 terminalConfig shape", () => {
const { terminalConfig } = hostPayloadLegacy({
autoTmux: true,
inheritAppearance: true,
fontSize: 20,
}) as { terminalConfig: Record<string, unknown> };
expect(terminalConfig.autoTmux).toBe(true);
expect(terminalConfig).not.toHaveProperty("fontSize");
expect(terminalConfig).not.toHaveProperty("localEcho");
});
it("includes the look of a host that has its own", () => {
const { terminalConfig } = hostPayloadLegacy({
inheritAppearance: false,
fontSize: 20,
}) as { terminalConfig: Record<string, unknown> };
expect(terminalConfig.fontSize).toBe(20);
expect(terminalConfig.theme).toBe("termix");
});
});
@@ -132,3 +132,78 @@ describe("client settings and image storage", () => {
expect(result.body.connected).toBe(false);
});
});
describe("terminal settings routes", () => {
const host = (id: number, userId = "user-1") => ({
id,
userId,
name: `h${id}`,
ip: "10.0.0.1",
port: 22,
username: "root",
tags: null,
folder: null,
authType: "password",
});
it("hands every user their own terminal settings and the new-host defaults", async () => {
server = await startServer({
settings: { newHostFontSize: 20, newHostTheme: "nord" },
});
await server.mock.ctx.settings.setUser(
"user-1",
"commandAutocomplete",
true,
);
await server.mock.ctx.settings.setUser("user-1", "terminalDefaults", {
fontSize: 18,
});
const { body } = await server.request("GET", "/client-settings");
expect(body.user).toMatchObject({
commandAutocomplete: true,
terminalDefaults: { fontSize: 18 },
localEcho: "auto",
linkClickBehavior: "confirm",
});
expect(body.newHostDefaults).toMatchObject({ fontSize: 20, theme: "nord" });
const other = await server.request("GET", "/client-settings", {
user: "user-2",
});
expect(other.body.user.commandAutocomplete).toBe(false);
});
it("saves the browser preferences a user moves over, for that user only", async () => {
server = await startServer();
const saved = await server.request("PUT", "/user-settings", {
body: { localEcho: "on", linkClickBehavior: "direct", theme: "x" },
});
expect(saved.status).toBe(200);
expect(await server.mock.ctx.settings.getUser("user-1", "localEcho")).toBe(
"on",
);
expect(
await server.mock.ctx.settings.getUser("user-2", "localEcho"),
).not.toBe("on");
});
it("turns auto tmux on for a host the user can edit", async () => {
server = await startServer({ hosts: [host(1)] });
const saved = await server.request("PUT", "/hosts/1/auto-tmux", {
body: { enabled: true },
});
expect(saved.status).toBe(200);
expect(await server.mock.ctx.settings.getHost(1, "autoTmux")).toBe(true);
});
it("refuses auto tmux on a host the user cannot reach, or a bad body", async () => {
server = await startServer({ hosts: [host(1)] });
const unknown = await server.request("PUT", "/hosts/2/auto-tmux", {
body: { enabled: true },
});
expect(unknown.status).toBe(403);
const bad = await server.request("PUT", "/hosts/1/auto-tmux", {
body: { enabled: "yes" },
});
expect(bad.status).toBe(400);
});
});
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_HOST_TERMINAL_SETTINGS,
hostSettingsFromTerminalConfig,
readHostTerminalSettings,
readUserSettings,
resolveTerminalSettings,
} from "../../src/shared/terminal-settings";
describe("hostSettingsFromTerminalConfig", () => {
it("keeps behavior and leaves a look-less host following the user", () => {
expect(
hostSettingsFromTerminalConfig({
autoMosh: true,
fastScrollModifier: "ctrl",
keepaliveInterval: 5,
}),
).toEqual({ autoMosh: true, fastScrollModifier: "ctrl" });
});
it("drops values a select field would refuse", () => {
expect(
hostSettingsFromTerminalConfig({ bellStyle: "loud", cursorStyle: "bar" }),
).toEqual({ inheritAppearance: false, cursorStyle: "bar" });
});
it("reads nothing from garbage", () => {
expect(hostSettingsFromTerminalConfig("{")).toEqual({});
});
});
describe("resolveTerminalSettings", () => {
it("uses the user's defaults while the host follows them", () => {
const host = readHostTerminalSettings({ fontSize: 30, autoTmux: true });
const resolved = resolveTerminalSettings(host, { fontSize: 18 });
expect(resolved.fontSize).toBe(18);
expect(resolved.autoTmux).toBe(true);
});
it("uses the host's own look once it opts out", () => {
const host = readHostTerminalSettings({
inheritAppearance: false,
fontSize: 30,
});
expect(resolveTerminalSettings(host, { fontSize: 18 }).fontSize).toBe(30);
});
it("falls back to the built-in defaults", () => {
expect(resolveTerminalSettings(null).theme).toBe(
DEFAULT_HOST_TERMINAL_SETTINGS.theme,
);
});
});
describe("readUserSettings", () => {
it("types what it reads and drops broken saved themes", () => {
expect(
readUserSettings({
terminalDefaults: { fontSize: "16", junk: 1 },
customThemes: [{ id: "a", name: "A", colors: {} }, { id: 1 }],
localEcho: "sideways",
linkClickBehavior: "direct",
}),
).toEqual({
terminalDefaults: { fontSize: 16 },
customThemes: [{ id: "a", name: "A", colors: {} }],
commandAutocomplete: false,
localEcho: "auto",
linkClickBehavior: "direct",
});
});
});