Files
oh-my-pi/packages/coding-agent/src/ssh/sshfs-mount.ts
T
can1357 779ca4872b style(deps): migrated from Prettier to Biome and updated formatting rules
- Removed Prettier configuration files (.prettierignore and .prettierrc) and migrated formatting to Biome.
- Updated Biome configuration from version 2.3.11 to 2.3.12 and changed arrowParentheses rule from 'always' to 'asNeeded'.
- Pinned @biomejs/biome dependency to exact version 2.3.12 in package.json and bun.lock.
- Applied consistent arrow function formatting across 489 files by removing unnecessary parentheses around single parameters.
- Removed blank lines after comment blocks and reorganized imports for consistency across the codebase.
2026-01-24 04:20:19 +01:00

137 lines
3.6 KiB
TypeScript

import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { $ } from "bun";
import { CONFIG_DIR_NAME } from "../config";
import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
const REMOTE_DIR = path.join(os.homedir(), CONFIG_DIR_NAME, "remote");
const CONTROL_DIR = getControlDir();
const CONTROL_PATH = getControlPathTemplate();
const mountedPaths = new Set<string>();
async function ensureDir(path: string, mode = 0o700): Promise<void> {
try {
await fs.promises.mkdir(path, { recursive: true, mode });
} catch {
await fs.promises.chmod(path, mode).catch(e => void e);
}
}
function getMountName(host: SSHConnectionTarget): string {
const raw = (host.name ?? host.host).trim();
const sanitized = raw.replace(/[^a-zA-Z0-9._-]+/g, "_");
return sanitized.length > 0 ? sanitized : "remote";
}
function getMountPath(host: SSHConnectionTarget): string {
return path.join(REMOTE_DIR, getMountName(host));
}
function buildSshTarget(host: SSHConnectionTarget): string {
return host.username ? `${host.username}@${host.host}` : host.host;
}
function buildSshfsArgs(host: SSHConnectionTarget): string[] {
const args = [
"-o",
"reconnect",
"-o",
"ServerAliveInterval=15",
"-o",
"ServerAliveCountMax=3",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=accept-new",
"-o",
"ControlMaster=auto",
"-o",
`ControlPath=${CONTROL_PATH}`,
"-o",
"ControlPersist=3600",
];
if (host.port) {
args.push("-p", String(host.port));
}
if (host.keyPath) {
args.push("-o", `IdentityFile=${host.keyPath}`);
}
return args;
}
async function unmountPath(path: string): Promise<boolean> {
const fusermount = Bun.which("fusermount") ?? Bun.which("fusermount3");
if (fusermount) {
const result = await $`${fusermount} -u ${path}`.quiet().nothrow();
if (result.exitCode === 0) return true;
}
const umount = Bun.which("umount");
if (!umount) return false;
const result = await $`${umount} ${path}`.quiet().nothrow();
return result.exitCode === 0;
}
export function hasSshfs(): boolean {
return Bun.which("sshfs") !== null;
}
export async function isMounted(path: string): Promise<boolean> {
const mountpoint = Bun.which("mountpoint");
if (!mountpoint) return false;
const result = await $`${mountpoint} -q ${path}`.quiet().nothrow();
return result.exitCode === 0;
}
export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"): Promise<string | undefined> {
if (!hasSshfs()) return undefined;
const mountPath = getMountPath(host);
await Promise.all([ensureDir(REMOTE_DIR), ensureDir(CONTROL_DIR), ensureDir(mountPath)]);
if (await isMounted(mountPath)) {
mountedPaths.add(mountPath);
return mountPath;
}
const target = `${buildSshTarget(host)}:${remotePath}`;
const args = buildSshfsArgs(host);
const result = await $`sshfs ${args} ${target} ${mountPath}`.nothrow();
if (result.exitCode !== 0) {
const detail = result.stderr.toString().trim();
const suffix = detail ? `: ${detail}` : "";
throw new Error(`Failed to mount ${target}${suffix}`);
}
mountedPaths.add(mountPath);
return mountPath;
}
export async function unmountRemote(host: SSHConnectionTarget): Promise<boolean> {
const mountPath = getMountPath(host);
if (!(await isMounted(mountPath))) {
mountedPaths.delete(mountPath);
return false;
}
const success = await unmountPath(mountPath);
if (success) {
mountedPaths.delete(mountPath);
}
return success;
}
export async function unmountAll(): Promise<void> {
for (const mountPath of Array.from(mountedPaths)) {
await unmountPath(mountPath);
}
mountedPaths.clear();
}