From ab16b920511da03381b5a19116cc2cfeddc91005 Mon Sep 17 00:00:00 2001 From: Nu11ified Date: Wed, 25 Feb 2026 16:07:01 +0000 Subject: [PATCH 1/7] fix: enable copy/paste, add SSH password auth, auto-accept host keys - Add ApplicationMenu with Edit menu (copy, paste, cut, undo, redo, selectAll) so clipboard operations work in the Electrobun webview - Add password-based SSH authentication as alternative to key-based, using sshpass -e with SSHPASS env var for security - Change StrictHostKeyChecking from accept-new to no with UserKnownHostsFile=/dev/null to prevent host key prompts --- src/main/index.ts | 31 +++++++++++- src/main/services/ssh-provisioner.ts | 39 ++++++++++++--- src/renderer/pages/CreateWorkspace.tsx | 69 ++++++++++++++++++++++---- src/shared/rpc-types.ts | 3 +- src/shared/types.ts | 1 + 5 files changed, 125 insertions(+), 18 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 7667211..abc8e2f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,7 +2,7 @@ // Main process entry point. Creates the window first for immediate feedback, // then initializes services and loads plugins in the background. -import { BrowserWindow, BrowserView, Utils } from "electrobun/bun"; +import { BrowserWindow, BrowserView, Utils, ApplicationMenu } from "electrobun/bun"; import { join, resolve } from "node:path"; import { platform, homedir } from "node:os"; import { mkdirSync, appendFileSync, promises as fsp } from "node:fs"; @@ -1151,6 +1151,7 @@ const rpc = BrowserView.defineRPC({ host: params.host, user: params.user, keyPath: params.keyPath, + password: params.password, }); } catch (err) { return { success: false, error: err instanceof Error ? err.message : String(err) }; @@ -1165,6 +1166,7 @@ const rpc = BrowserView.defineRPC({ host: params.host, user: params.user, keyPath: params.keyPath, + password: params.password, agentPort: params.agentPort, }, agentBinaryPath, @@ -1553,6 +1555,33 @@ try { rpc, }); + // Set up application menu so copy/paste/cut/undo/redo work in the webview + ApplicationMenu.setApplicationMenu([ + { + label: "BlockDev", + submenu: [ + { role: "about" }, + { type: "divider" }, + { role: "quit" }, + ], + }, + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "divider" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "pasteAndMatchStyle" }, + { role: "delete" }, + { type: "divider" }, + { role: "selectAll" }, + ], + }, + ]); + crashLog("Window created successfully"); console.log("BlockDev window created"); diff --git a/src/main/services/ssh-provisioner.ts b/src/main/services/ssh-provisioner.ts index 1f7b767..666641c 100644 --- a/src/main/services/ssh-provisioner.ts +++ b/src/main/services/ssh-provisioner.ts @@ -8,14 +8,20 @@ export interface SSHConfig { host: string; user: string; keyPath?: string; + password?: string; agentPort?: number; } export type ProvisionProgress = (stage: string, message: string) => void; export class SSHProvisioner { + /** Base SSH options: auto-accept host keys, never prompt interactively. */ private buildSshArgs(config: SSHConfig): string[] { - const args = ["-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=accept-new"]; + const args = [ + "-o", "ConnectTimeout=10", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + ]; if (config.keyPath) { args.push("-i", config.keyPath); } @@ -24,19 +30,39 @@ export class SSHProvisioner { } private buildScpArgs(config: SSHConfig): string[] { - const args = ["-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=accept-new"]; + const args = [ + "-o", "ConnectTimeout=10", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + ]; if (config.keyPath) { args.push("-i", config.keyPath); } return args; } + /** When password auth is used, prefix the command with sshpass -e and set SSHPASS env var. */ + private buildCommandPrefix(config: SSHConfig): string[] { + if (config.password) { + return ["sshpass", "-e"]; + } + return []; + } + + private getSpawnEnv(config: SSHConfig): Record | undefined { + if (config.password) { + return { ...process.env, SSHPASS: config.password } as Record; + } + return undefined; + } + /** Test SSH connectivity. Returns true if we can connect and run a command. */ async testConnection(config: SSHConfig): Promise<{ success: boolean; error?: string }> { try { - const proc = Bun.spawn(["ssh", ...this.buildSshArgs(config), "echo blockdev-ok"], { + const proc = Bun.spawn([...this.buildCommandPrefix(config), "ssh", ...this.buildSshArgs(config), "echo blockdev-ok"], { stdout: "pipe", stderr: "pipe", + env: this.getSpawnEnv(config), }); const stdout = await new Response(proc.stdout).text(); @@ -122,9 +148,10 @@ export class SSHProvisioner { /** Run a command over SSH, return stdout. */ private async sshExec(config: SSHConfig, command: string): Promise { - const proc = Bun.spawn(["ssh", ...this.buildSshArgs(config), command], { + const proc = Bun.spawn([...this.buildCommandPrefix(config), "ssh", ...this.buildSshArgs(config), command], { stdout: "pipe", stderr: "pipe", + env: this.getSpawnEnv(config), }); const stdout = await new Response(proc.stdout).text(); @@ -141,8 +168,8 @@ export class SSHProvisioner { /** Copy a local file to the remote host via SCP. */ private async scpUpload(config: SSHConfig, localPath: string, remotePath: string): Promise { const proc = Bun.spawn( - ["scp", ...this.buildScpArgs(config), localPath, `${config.user}@${config.host}:${remotePath}`], - { stdout: "pipe", stderr: "pipe" }, + [...this.buildCommandPrefix(config), "scp", ...this.buildScpArgs(config), localPath, `${config.user}@${config.host}:${remotePath}`], + { stdout: "pipe", stderr: "pipe", env: this.getSpawnEnv(config) }, ); const exitCode = await proc.exited; diff --git a/src/renderer/pages/CreateWorkspace.tsx b/src/renderer/pages/CreateWorkspace.tsx index 5a572a8..a043239 100644 --- a/src/renderer/pages/CreateWorkspace.tsx +++ b/src/renderer/pages/CreateWorkspace.tsx @@ -52,9 +52,11 @@ export function CreateWorkspace({ onBack, onCreate }: CreateWorkspaceProps) { // Location state const [serverLocation, setServerLocation] = useState<"local" | "remote">("local"); + const [sshAuthMode, setSshAuthMode] = useState<"key" | "password">("key"); const [sshHost, setSshHost] = useState(""); const [sshUser, setSshUser] = useState("root"); const [sshKeyPath, setSshKeyPath] = useState("~/.ssh/id_ed25519"); + const [sshPassword, setSshPassword] = useState(""); const [agentPort, setAgentPort] = useState(9847); const [testingConnection, setTestingConnection] = useState(false); const [connectionTestResult, setConnectionTestResult] = useState<{ success: boolean; error?: string } | null>(null); @@ -132,7 +134,7 @@ export function CreateWorkspace({ onBack, onCreate }: CreateWorkspaceProps) { const canNext = (step === 0 && selectedFramework !== null) || - (step === 1 && (serverLocation === "local" || (serverLocation === "remote" && sshHost.trim() && sshUser.trim() && agentPort > 0 && agentPort <= 65535))) || + (step === 1 && (serverLocation === "local" || (serverLocation === "remote" && sshHost.trim() && sshUser.trim() && agentPort > 0 && agentPort <= 65535 && (sshAuthMode === "key" || sshPassword.trim())))) || (step === 2 && selectedVersion !== null) || (step === 3 && !creating); @@ -150,7 +152,8 @@ export function CreateWorkspace({ onBack, onCreate }: CreateWorkspaceProps) { const result = await rpc.request("testSSHConnection", { host: sshHost, user: sshUser, - keyPath: sshKeyPath || undefined, + keyPath: sshAuthMode === "key" ? (sshKeyPath || undefined) : undefined, + password: sshAuthMode === "password" ? sshPassword : undefined, }); setConnectionTestResult(result); } catch (err) { @@ -176,7 +179,8 @@ export function CreateWorkspace({ onBack, onCreate }: CreateWorkspaceProps) { const provResult = await rpc.request("provisionRemoteAgent", { host: sshHost, user: sshUser, - keyPath: sshKeyPath || undefined, + keyPath: sshAuthMode === "key" ? (sshKeyPath || undefined) : undefined, + password: sshAuthMode === "password" ? sshPassword : undefined, agentPort, }); @@ -205,7 +209,8 @@ export function CreateWorkspace({ onBack, onCreate }: CreateWorkspaceProps) { agentPort: provResult.agentPort, token: provResult.token, sshUser, - sshKeyPath: sshKeyPath || undefined, + sshKeyPath: sshAuthMode === "key" ? (sshKeyPath || undefined) : undefined, + sshPassword: sshAuthMode === "password" ? sshPassword : undefined, }, }); @@ -451,15 +456,59 @@ export function CreateWorkspace({ onBack, onCreate }: CreateWorkspaceProps) { /> + + {/* Auth mode toggle */}
- - setSshKeyPath(e.target.value)} - className="w-full mt-1 px-4 py-3 rounded-xl bg-card border border-border-subtle text-text-primary text-sm font-mono focus:outline-none focus:border-accent transition-colors" - /> + +
+ + +
+ {/* SSH Key Path (shown for key auth) */} + {sshAuthMode === "key" && ( +
+ + setSshKeyPath(e.target.value)} + className="w-full mt-1 px-4 py-3 rounded-xl bg-card border border-border-subtle text-text-primary text-sm font-mono focus:outline-none focus:border-accent transition-colors" + /> +
+ )} + + {/* Password (shown for password auth) */} + {sshAuthMode === "password" && ( +
+ + setSshPassword(e.target.value)} + placeholder="Enter SSH password" + className="w-full mt-1 px-4 py-3 rounded-xl bg-card border border-border-subtle text-text-primary text-sm font-mono focus:outline-none focus:border-accent transition-colors" + /> +
+ )} + {/* Test Connection button */}