From 95552a26224a4ecfeb268ec2d142751cadef2913 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:57:16 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20resolve=20editor=20comman?= =?UTF-8?q?ds=20using=20shell=20PATH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Electron spawns child processes without shell: true, it uses a minimal PATH that doesn't include directories like /opt/homebrew/bin. This caused 'Editor command not found' errors for commands like 'code' even when they were available in the user's terminal. Adding shell: true to both the command availability check and the spawn calls ensures we use the user's full shell environment. Arguments are properly quoted with shellQuote() to handle paths containing spaces. --- src/node/services/editorService.ts | 31 ++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/node/services/editorService.ts b/src/node/services/editorService.ts index d4458c9633..5cf3f89181 100644 --- a/src/node/services/editorService.ts +++ b/src/node/services/editorService.ts @@ -4,6 +4,15 @@ import { isSSHRuntime } from "@/common/types/runtime"; import { log } from "@/node/services/log"; import { createRuntime } from "@/node/runtime/runtimeFactory"; +/** + * Quote a string for safe use in shell commands. + * Uses single quotes with proper escaping for embedded single quotes. + */ +function shellQuote(value: string): string { + if (value.length === 0) return "''"; + return "'" + value.replace(/'/g, "'\"'\"'") + "'"; +} + export interface EditorConfig { editor: string; customCommand?: string; @@ -81,20 +90,24 @@ export class EditorService { const resolvedPath = await runtime.resolvePath(targetPath); // Build the remote command: code --remote ssh-remote+host /remote/path - const args = ["--remote", `ssh-remote+${runtimeConfig.host}`, resolvedPath]; + // Quote the path to handle spaces; the remote host arg doesn't need quoting + const shellCmd = `${editorCommand} --remote ${shellQuote(`ssh-remote+${runtimeConfig.host}`)} ${shellQuote(resolvedPath)}`; - log.info(`Opening SSH path in editor: ${editorCommand} ${args.join(" ")}`); - const child = spawn(editorCommand, args, { + log.info(`Opening SSH path in editor: ${shellCmd}`); + const child = spawn(shellCmd, [], { detached: true, stdio: "ignore", + shell: true, }); child.unref(); } else { - // Local - just open the path - log.info(`Opening local path in editor: ${editorCommand} ${targetPath}`); - const child = spawn(editorCommand, [targetPath], { + // Local - just open the path (quote to handle spaces) + const shellCmd = `${editorCommand} ${shellQuote(targetPath)}`; + log.info(`Opening local path in editor: ${shellCmd}`); + const child = spawn(shellCmd, [], { detached: true, stdio: "ignore", + shell: true, }); child.unref(); } @@ -108,11 +121,13 @@ export class EditorService { } /** - * Check if a command is available in the system PATH + * Check if a command is available in the system PATH. + * Uses shell: true to ensure we get the full PATH from user's shell profile, + * which is necessary for commands installed via Homebrew or similar. */ private isCommandAvailable(command: string): boolean { try { - const result = spawnSync("which", [command], { encoding: "utf8" }); + const result = spawnSync("which", [command], { encoding: "utf8", shell: true }); return result.status === 0; } catch { return false;