diff --git a/packages/server/src/server/routes/settings.ts b/packages/server/src/server/routes/settings.ts index 1d6fe13a..5d18e4fd 100644 --- a/packages/server/src/server/routes/settings.ts +++ b/packages/server/src/server/routes/settings.ts @@ -1,6 +1,6 @@ import { FastifyInstance } from "fastify" import { z } from "zod" -import { probeBinaryVersion } from "../../workspaces/runtime" +import { probeBinaryVersion } from "../../workspaces/spawn" import type { SettingsService } from "../../settings/service" import type { Logger } from "../../logger" import { sanitizeConfigDoc, sanitizeConfigOwner } from "../../settings/public-config" diff --git a/packages/server/src/workspaces/__tests__/spawn.test.ts b/packages/server/src/workspaces/__tests__/spawn.test.ts new file mode 100644 index 00000000..8266dac9 --- /dev/null +++ b/packages/server/src/workspaces/__tests__/spawn.test.ts @@ -0,0 +1,70 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" + +import { buildWindowsSpawnSpec, parseWslUncPath, resolveWslWorkingDirectory } from "../spawn" + +describe("parseWslUncPath", () => { + it("parses WSL UNC paths into distro and linux path", () => { + assert.deepEqual(parseWslUncPath(String.raw`\\wsl.localhost\Ubuntu\home\dev\.opencode\bin\opencode`), { + distro: "Ubuntu", + linuxPath: "/home/dev/.opencode/bin/opencode", + }) + }) + + it("supports the legacy wsl$ UNC prefix", () => { + assert.deepEqual(parseWslUncPath(String.raw`\\wsl$\Ubuntu\home\dev`), { + distro: "Ubuntu", + linuxPath: "/home/dev", + }) + }) +}) + +describe("resolveWslWorkingDirectory", () => { + it("keeps WSL workspace folders in the same distro", () => { + assert.equal( + resolveWslWorkingDirectory(String.raw`\\wsl.localhost\Ubuntu\home\dev\workspace`, "Ubuntu"), + "/home/dev/workspace", + ) + }) + + it("maps Windows drive paths into /mnt when launching through WSL", () => { + assert.equal(resolveWslWorkingDirectory(String.raw`C:\Users\dev\workspace`, "Ubuntu"), "/mnt/c/Users/dev/workspace") + }) + + it("rejects WSL workspace folders from a different distro", () => { + assert.equal(resolveWslWorkingDirectory(String.raw`\\wsl.localhost\Debian\home\dev\workspace`, "Ubuntu"), null) + }) +}) + +describe("buildWindowsSpawnSpec", () => { + it("wraps WSL binaries with wsl.exe and propagates required env vars", () => { + const spec = buildWindowsSpawnSpec( + String.raw`\\wsl.localhost\Ubuntu\home\dev\.opencode\bin\opencode`, + ["serve", "--port", "0"], + { + cwd: String.raw`\\wsl.localhost\Ubuntu\home\dev\workspace`, + env: { + OPENCODE_CONFIG_DIR: String.raw`C:\Users\dev\AppData\Roaming\CodeNomad\opencode-config`, + CODENOMAD_INSTANCE_ID: "workspace-123", + OPENCODE_SERVER_PASSWORD: "secret", + }, + propagateEnvKeys: ["OPENCODE_CONFIG_DIR", "CODENOMAD_INSTANCE_ID", "OPENCODE_SERVER_PASSWORD"], + }, + ) + + assert.equal(spec.command, "wsl.exe") + assert.deepEqual(spec.args, [ + "--distribution", + "Ubuntu", + "--cd", + "/home/dev/workspace", + "--exec", + "/home/dev/.opencode/bin/opencode", + "serve", + "--port", + "0", + ]) + assert.equal(spec.cwd, undefined) + assert.equal(spec.env?.WSLENV, "OPENCODE_CONFIG_DIR/p:CODENOMAD_INSTANCE_ID:OPENCODE_SERVER_PASSWORD") + }) +}) diff --git a/packages/server/src/workspaces/runtime.ts b/packages/server/src/workspaces/runtime.ts index 1269f0b7..3b52ca39 100644 --- a/packages/server/src/workspaces/runtime.ts +++ b/packages/server/src/workspaces/runtime.ts @@ -4,98 +4,7 @@ import path from "path" import { EventBus } from "../events/bus" import { LogLevel, WorkspaceLogEntry } from "../api-types" import { Logger } from "../logger" - -export const WINDOWS_CMD_EXTENSIONS = new Set([".cmd", ".bat"]) -export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"]) - -const VERSION_REGEX = /([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/ - -export function buildSpawnSpec(binaryPath: string, args: string[]) { - if (process.platform !== "win32") { - return { command: binaryPath, args, options: {} as const } - } - - const extension = path.extname(binaryPath).toLowerCase() - - if (WINDOWS_CMD_EXTENSIONS.has(extension)) { - const comspec = process.env.ComSpec || "cmd.exe" - // cmd.exe requires the full command as a single string. - // Using the ""