mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
v26.10.3 (#1692)
* feat: shell controller, TabStage, layout and theme plugins with repair view * feat: move the classic layout into its own bundled plugin * feat: move color themes into a bundled themes plugin * fix: plugin tests and type checks work with a linked sdk * fix: home tab, layout switch and missing layout fallbacks for looks * chore: add classic layout and themes to the readme * chore: drop example plugin links from the plugin guide * feat: plugin filters by type, paged browse, hide plugins needing newer termix * feat: move desktop, status, import, audit and branding features into plugins * feat: host lock api so a plugin can load hosts from a config file (#1581) * chore: add hosts config to the plugin table * feat: login policy hook for plugins and optional https redirect (#1584) * chore: add login security to the plugin table * feat: api key socket tickets so a master can open remote sessions (#1654) * feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654) * chore: add sync to the plugin table * feat: move the sync screens and wizard out of core into the plugin (#1580) * fix: add a neutral linked desktop hook for plugins (#1580) * fix: show the linked account's name and admin flag in the app (#1580) * feat: ssh transport providers, core ssh relay and plugin folders api * feat: support FIDO2 security keys through ssh agent * feat: add companion panels beside host tabs * feat: mTLS client certificates for desktop server links * feat: native web view for plugins in the desktop app * fix: detect the desktop link window by Electron's default user agent Fixes #1675 * fix: changelog for desktop link fix * fix: keep text primary keys in the plugin data export * fix: ship sharp binaries for Windows arm64 builds * feat: client cert on the sync link types (sdk 1.0.18) * chore: update translations * feat: themed title bar for the desktop app (#1690) * chore: changelog for the themed title bar * fix: draw the title bar in the sidebar color * feat: per host layout for the terminal panel bar * chore: sdk 1.0.19 for the panel bar layout * chore: bundle all 43 plugins for 26.10.3 * chore: update translations --------- Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com> Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
This commit is contained in:
577 files changed
+76479
-101831
No files matched your search
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Local shells for plugins on the desktop app's embedded backend. Ported from
|
||||
* the old Electron local terminal handlers.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import type { PluginLocalPty } from "@termix-ssh/plugin-sdk/backend";
|
||||
|
||||
export function resolveLocalShell(
|
||||
platform: NodeJS.Platform,
|
||||
requestedShell?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
exists: (file: string) => boolean = fs.existsSync,
|
||||
): { file: string; args: string[] } {
|
||||
if (platform === "win32") {
|
||||
if (requestedShell === "wsl") return { file: "wsl.exe", args: [] };
|
||||
return {
|
||||
file: env.TERMIX_LOCAL_SHELL || "powershell.exe",
|
||||
args: ["-NoLogo"],
|
||||
};
|
||||
}
|
||||
|
||||
// A SHELL left pointing at an uninstalled shell makes the spawn fail.
|
||||
const fallbacks =
|
||||
platform === "darwin"
|
||||
? ["/bin/zsh", "/bin/bash", "/bin/sh"]
|
||||
: ["/bin/bash", "/bin/sh"];
|
||||
const usable = (file?: string): file is string =>
|
||||
!!file && (!path.isAbsolute(file) || exists(file));
|
||||
const file =
|
||||
[env.TERMIX_LOCAL_SHELL, env.SHELL, ...fallbacks].find(usable) ||
|
||||
fallbacks[0];
|
||||
return { file, args: ["-l"] };
|
||||
}
|
||||
|
||||
/**
|
||||
* node-pty starts every shell through its spawn-helper binary, and a packaged
|
||||
* copy that lost its execute bit fails with "posix_spawnp failed". Returns how
|
||||
* many helpers were fixed.
|
||||
*/
|
||||
export function repairSpawnHelpers(ptyDir: string, fsImpl = fs): number {
|
||||
// A universal macOS build ships app-arm64.asar and app-x64.asar.
|
||||
const dir = ptyDir.replace(
|
||||
/\b(app(?:-[a-z0-9]+)?)\.asar(?!\.unpacked)/,
|
||||
"$1.asar.unpacked",
|
||||
);
|
||||
const candidates = [
|
||||
path.join(dir, "build", "Release", "spawn-helper"),
|
||||
path.join(
|
||||
dir,
|
||||
"prebuilds",
|
||||
`${process.platform}-${process.arch}`,
|
||||
"spawn-helper",
|
||||
),
|
||||
];
|
||||
let fixed = 0;
|
||||
for (const helper of candidates) {
|
||||
try {
|
||||
const mode = fsImpl.statSync(helper).mode;
|
||||
if ((mode & 0o111) === 0) {
|
||||
fsImpl.chmodSync(helper, mode | 0o755);
|
||||
fixed += 1;
|
||||
}
|
||||
} catch {
|
||||
// Missing or read-only; nothing to fix here.
|
||||
}
|
||||
}
|
||||
return fixed;
|
||||
}
|
||||
|
||||
interface NodePty {
|
||||
spawn: (
|
||||
file: string,
|
||||
args: string[],
|
||||
options: Record<string, unknown>,
|
||||
) => {
|
||||
pid: number;
|
||||
write: (data: string) => void;
|
||||
resize: (cols: number, rows: number) => void;
|
||||
kill: () => void;
|
||||
onData: (cb: (data: string) => void) => { dispose: () => void };
|
||||
onExit: (cb: (e: { exitCode: number }) => void) => { dispose: () => void };
|
||||
};
|
||||
}
|
||||
|
||||
const requireFromHere = createRequire(import.meta.url);
|
||||
|
||||
function loadNodePty(): NodePty {
|
||||
try {
|
||||
return requireFromHere("node-pty") as NodePty;
|
||||
} catch {
|
||||
throw new Error("The local terminal is not available in this build");
|
||||
}
|
||||
}
|
||||
|
||||
export const MAX_PTY_COLS = 500;
|
||||
export const MAX_PTY_ROWS = 300;
|
||||
|
||||
export function spawnLocalPty(options?: {
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
shell?: string;
|
||||
}): PluginLocalPty {
|
||||
const pty = loadNodePty();
|
||||
const cols = Math.min(MAX_PTY_COLS, Math.max(2, Number(options?.cols) || 80));
|
||||
const rows = Math.min(MAX_PTY_ROWS, Math.max(1, Number(options?.rows) || 24));
|
||||
const shellConfig = resolveLocalShell(process.platform, options?.shell);
|
||||
|
||||
const start = () =>
|
||||
pty.spawn(shellConfig.file, shellConfig.args, {
|
||||
name: "xterm-256color",
|
||||
cols,
|
||||
rows,
|
||||
cwd: os.homedir(),
|
||||
env: { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor" },
|
||||
});
|
||||
|
||||
let child: ReturnType<NodePty["spawn"]>;
|
||||
try {
|
||||
child = start();
|
||||
} catch (error) {
|
||||
const ptyDir = path.dirname(
|
||||
requireFromHere.resolve("node-pty/package.json"),
|
||||
);
|
||||
if (
|
||||
!String((error as Error)?.message).includes("posix_spawnp") ||
|
||||
repairSpawnHelpers(ptyDir) === 0
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
child = start();
|
||||
}
|
||||
|
||||
return {
|
||||
pid: child.pid,
|
||||
shell: shellConfig.file,
|
||||
write: (data) => {
|
||||
if (typeof data === "string" && data.length <= 64 * 1024) {
|
||||
child.write(data);
|
||||
}
|
||||
},
|
||||
resize: (c, r) => {
|
||||
if (
|
||||
Number.isInteger(c) &&
|
||||
Number.isInteger(r) &&
|
||||
c >= 2 &&
|
||||
c <= MAX_PTY_COLS &&
|
||||
r >= 1 &&
|
||||
r <= MAX_PTY_ROWS
|
||||
) {
|
||||
child.resize(c, r);
|
||||
}
|
||||
},
|
||||
kill: () => child.kill(),
|
||||
onData: (handler) => {
|
||||
const sub = child.onData(handler);
|
||||
return () => sub.dispose();
|
||||
},
|
||||
onExit: (handler) => {
|
||||
const sub = child.onExit(({ exitCode }) => handler(exitCode));
|
||||
return () => sub.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user