diff options
Diffstat (limited to 'src/daemon/spawn.ts')
| -rw-r--r-- | src/daemon/spawn.ts | 34 |
1 files changed, 19 insertions, 15 deletions
diff --git a/src/daemon/spawn.ts b/src/daemon/spawn.ts index a88855f..ffba92d 100644 --- a/src/daemon/spawn.ts +++ b/src/daemon/spawn.ts @@ -1,6 +1,6 @@ import { spawn, type ChildProcess } from "node:child_process"; import { access, unlink } from "node:fs/promises"; -import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { createConnection } from "node:net"; @@ -8,6 +8,8 @@ import { DAEMON_SOCKET_PATH, serialize, type WireControl } from "./protocol"; const moduleDir = dirname(fileURLToPath(import.meta.url)); +const requireFromHere = createRequire(import.meta.url); + const isWindows = process.platform === "win32"; // No-op on Windows: a named pipe is not a filesystem entry and unlinking a pipe path throws. @@ -78,26 +80,28 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => { }); }; -export const spawnDaemon = (): ChildProcess | null => { +// Node's own resolver, not a guessed `node_modules/.bin` path: pi installs every extension into +// one flat, hoisted tree, so tsx lands beside the package rather than inside it and a fixed +// `<pkg>/node_modules/.bin/tsx` lookup always misses. Resolving the CLI entry also means no shell, +// so the Windows `.cmd` shim and its argument quoting are gone with it. +export const daemonSpawnCommand = (): { readonly command: string; readonly args: ReadonlyArray<string> } | null => { const daemonScript = join(moduleDir, "index.ts"); + try { + return { command: process.execPath, args: [requireFromHere.resolve("tsx/cli"), daemonScript] }; + } catch { + return null; + } +}; - // Windows npm binaries are `.cmd` shims, which only launch through a shell — spawning one directly yields EINVAL. - const tsxBinBase = join(moduleDir, "..", "..", "node_modules", ".bin", "tsx"); - const tsxBin = isWindows ? `${tsxBinBase}.cmd` : tsxBinBase; - const hasLocalTsx = existsSync(tsxBin); - - const cmd = hasLocalTsx ? tsxBin : isWindows ? "npx.cmd" : "npx"; - const args = hasLocalTsx ? [daemonScript] : ["tsx", daemonScript]; - - // With shell: true Node passes the command line verbatim, so a path containing spaces must be quoted or it splits into tokens. - const quote = (s: string): string => (isWindows ? `"${s}"` : s); +export const spawnDaemon = (): ChildProcess | null => { + const spec = daemonSpawnCommand(); + if (!spec) return null; - const child = spawn(quote(cmd), args.map(quote), { + const child = spawn(spec.command, [...spec.args], { detached: true, stdio: "ignore", env: { ...process.env }, - shell: isWindows, - windowsVerbatimArguments: isWindows, + windowsHide: true, }); child.unref(); |
