List ~/.ssh/config hosts in the panel source dropdown

Read-only group below saved connections; hosts resolve HostName/User/
Port/IdentityFile (Include, wildcards, %-tokens) and connect on click.
New ssh-agent auth kind covers hosts without a usable identity file;
ProxyJump/ProxyCommand hosts are shown disabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 14:10:52 +02:00
co-authored by Claude Fable 5
parent fdf65766b5
commit f4819a58fe
11 changed files with 240 additions and 18 deletions
+25 -12
View File
@@ -1,9 +1,9 @@
import dns from "node:dns/promises";
import { readFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { Client, type ConnectConfig, type SFTPWrapper } from "ssh2";
import type { ConnectionConfig, EntryType, FileEntry } from "../../shared/types";
import { expandTilde } from "../store/ssh-config";
import type { FileSystemProvider } from "./provider";
const S_IFMT = 0o170000;
@@ -17,12 +17,6 @@ function typeFromMode(mode: number): EntryType {
return "file";
}
function expandTilde(p: string): string {
if (p === "~") return os.homedir();
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
return p;
}
export class SftpProvider implements FileSystemProvider {
readonly kind = "sftp" as const;
readonly fsKey: string;
@@ -49,11 +43,30 @@ export class SftpProvider implements FileSystemProvider {
keepaliveInterval: 15_000,
keepaliveCountMax: 3,
};
if (config.auth.kind === "password") {
connectConfig.password = config.auth.password;
} else {
connectConfig.privateKey = readFileSync(expandTilde(config.auth.privateKeyPath));
if (config.auth.passphrase) connectConfig.passphrase = config.auth.passphrase;
const agentSock =
process.env.SSH_AUTH_SOCK ??
(process.platform === "win32" ? "\\\\.\\pipe\\openssh-ssh-agent" : undefined);
switch (config.auth.kind) {
case "password":
connectConfig.password = config.auth.password;
break;
case "agent":
if (!agentSock) throw new Error("ssh-agent not available (SSH_AUTH_SOCK is not set)");
connectConfig.agent = agentSock;
break;
case "key": {
// Alongside a passphrase-less key file, offer the agent too — it
// covers encrypted keys already loaded via ssh-add/UseKeychain.
try {
connectConfig.privateKey = readFileSync(expandTilde(config.auth.privateKeyPath));
} catch {
if (!agentSock)
throw new Error(`identity file not found: ${config.auth.privateKeyPath}`);
}
if (config.auth.passphrase) connectConfig.passphrase = config.auth.passphrase;
else if (agentSock) connectConfig.agent = agentSock;
break;
}
}
// Filesystem identity: resolve the host so a hostname and its IP compare