fix: drop WSL OpenCode binary support on Windows

This commit is contained in:
Bohdan Triapitsyn
2026-06-05 20:35:53 +03:00
parent 4548477b14
commit 341a4ac296
15 changed files with 79 additions and 284 deletions
+9 -161
View File
@@ -9,7 +9,6 @@ export const createOpenCodeEnvRuntime = (deps) => {
state,
normalizeDirectoryPath,
readSettingsFromDiskMigrated,
ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
} = deps;
const runSpawnSync = typeof deps.spawnSync === 'function' ? deps.spawnSync : spawnSync;
@@ -268,135 +267,6 @@ export const createOpenCodeEnvRuntime = (deps) => {
state.resolvedWslDistro = null;
};
const resolveWslExecutablePath = () => {
if (process.platform !== 'win32') {
return null;
}
const explicit = [process.env.WSL_BINARY, process.env.OPENCHAMBER_WSL_BINARY]
.map((v) => (typeof v === 'string' ? v.trim() : ''))
.filter(Boolean);
for (const candidate of explicit) {
if (isExecutable(candidate)) {
return candidate;
}
}
try {
const result = runSpawnSync('where', ['wsl'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
if (result.status === 0) {
const lines = (result.stdout || '')
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const found = lines.find((line) => isExecutable(line));
if (found) {
return found;
}
}
} catch {
}
const systemRoot = process.env.SystemRoot || 'C:\\Windows';
const fallback = path.join(systemRoot, 'System32', 'wsl.exe');
if (isExecutable(fallback)) {
return fallback;
}
return null;
};
const buildWslExecArgs = (execArgs, distroOverride = null) => {
const distro = typeof distroOverride === 'string' && distroOverride.trim().length > 0
? distroOverride.trim()
: ENV_CONFIGURED_OPENCODE_WSL_DISTRO;
const prefix = distro ? ['-d', distro] : [];
return [...prefix, '--exec', ...execArgs];
};
const wslOpencodeProbeScript = [
'found="$(command -v opencode 2>/dev/null || true)"',
'case "$found" in /*) case "$found" in /mnt/[a-zA-Z]/*) ;; *) printf "%s\\n" "$found"; exit 0 ;; esac ;; esac',
'for candidate in "$HOME/.opencode/bin/opencode" "$HOME/.bun/bin/opencode" "$HOME/.local/bin/opencode" "$HOME/bin/opencode" /usr/local/bin/opencode /usr/bin/opencode /bin/opencode; do',
' if [ -x "$candidate" ]; then printf "%s\\n" "$candidate"; exit 0; fi',
'done',
'if [ -n "${SHELL:-}" ] && [ -x "$SHELL" ]; then',
' found="$("$SHELL" -lic "command -v opencode" 2>/dev/null | sed -n "1p")"',
' case "$found" in /*) case "$found" in /mnt/[a-zA-Z]/*) ;; *) printf "%s\\n" "$found"; exit 0 ;; esac ;; esac',
'fi',
'exit 1',
].join('\n');
const probeWslForOpencode = () => {
if (process.platform !== 'win32') {
return null;
}
const wslBinary = resolveWslExecutablePath();
if (!wslBinary) {
return null;
}
try {
const result = runSpawnSync(
wslBinary,
buildWslExecArgs(['sh', '-lc', wslOpencodeProbeScript]),
{
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 6000,
windowsHide: true,
}
);
if (result.status !== 0) {
return null;
}
const lines = (result.stdout || '')
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const found = lines[0] || '';
if (!found) {
return null;
}
return {
wslBinary,
opencodePath: found,
distro: ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
};
} catch {
return null;
}
};
const applyWslOpencodeResolution = ({ wslBinary, opencodePath, source = 'wsl', distro = null } = {}) => {
const resolvedWsl = wslBinary || resolveWslExecutablePath();
if (!resolvedWsl) {
return null;
}
state.useWslForOpencode = true;
state.resolvedWslBinary = resolvedWsl;
state.resolvedWslOpencodePath = typeof opencodePath === 'string' && opencodePath.trim().length > 0
? opencodePath.trim()
: 'opencode';
state.resolvedWslDistro = typeof distro === 'string' && distro.trim().length > 0 ? distro.trim() : ENV_CONFIGURED_OPENCODE_WSL_DISTRO;
state.resolvedOpencodeBinary = `wsl:${state.resolvedWslOpencodePath}`;
state.resolvedOpencodeBinarySource = source;
delete process.env.OPENCODE_BINARY;
return state.resolvedOpencodeBinary;
};
const resolveOpencodeCliPath = () => {
const explicit = [
process.env.OPENCODE_BINARY,
@@ -483,15 +353,9 @@ export const createOpenCodeEnvRuntime = (deps) => {
}
} catch {
}
const wsl = probeWslForOpencode();
if (wsl) {
return applyWslOpencodeResolution({
wslBinary: wsl.wslBinary,
opencodePath: wsl.opencodePath,
source: 'wsl',
distro: wsl.distro,
});
}
// Do not auto-detect OpenCode from WSL. OpenCode sessions are keyed by
// server-visible directories, and mixing Windows paths with WSL paths
// creates duplicate/missing project state in the desktop app.
return null;
}
@@ -1046,35 +910,21 @@ export const createOpenCodeEnvRuntime = (deps) => {
: null;
if (explicitWslPath && explicitWslPath[1] && explicitWslPath[1].trim().length > 0) {
const probe = probeWslForOpencode();
const applied = applyWslOpencodeResolution({
wslBinary: probe?.wslBinary || resolveWslExecutablePath(),
opencodePath: explicitWslPath[1].trim(),
source: 'settings-wsl-path',
distro: probe?.distro || ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
});
if (applied) {
return applied;
}
clearWslOpencodeResolution();
if (strict) {
throw createConfiguredWslOpencodeError(raw);
}
console.warn(`Configured settings.opencodeBinary uses WSL, which is no longer supported by OpenChamber desktop: ${raw}`);
return null;
}
if (process.platform === 'win32' && (isWslExecutableValue(raw) || isWslExecutableValue(normalized || ''))) {
const probe = probeWslForOpencode();
const applied = applyWslOpencodeResolution({
wslBinary: probe?.wslBinary || normalized || raw || null,
opencodePath: probe?.opencodePath || 'opencode',
source: 'settings-wsl',
distro: probe?.distro || ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
});
if (applied) {
return applied;
}
clearWslOpencodeResolution();
if (strict) {
throw createConfiguredWslOpencodeError(raw);
}
console.warn(`Configured settings.opencodeBinary points to WSL, which is no longer supported by OpenChamber desktop: ${raw}`);
return null;
}
if (normalized && isExecutable(normalized) && !isMacOpenCodeAppBundlePath(normalized)) {
@@ -1233,8 +1083,6 @@ export const createOpenCodeEnvRuntime = (deps) => {
isExecutable,
searchPathFor,
resolveGitBinaryForSpawn,
resolveWslExecutablePath,
buildWslExecArgs,
clearResolvedOpenCodeBinary,
};
};
@@ -90,7 +90,6 @@ const createRuntime = (settings, options = {}) => {
state,
normalizeDirectoryPath: (value) => value,
readSettingsFromDiskMigrated: async () => settings,
ENV_CONFIGURED_OPENCODE_WSL_DISTRO: options.ENV_CONFIGURED_OPENCODE_WSL_DISTRO ?? null,
spawnSync: options.spawnSync,
});
@@ -139,7 +138,7 @@ describe('OpenCode env runtime', () => {
});
});
it('does not classify WSL settings as a native invalid configured binary in strict mode', async () => {
it('rejects WSL settings in strict mode', async () => {
setPlatform('win32');
const dir = createTempDir('openchamber-no-wsl-');
process.env.PATH = dir;
@@ -148,18 +147,12 @@ describe('OpenCode env runtime', () => {
process.env.OPENCHAMBER_WSL_BINARY = path.join(dir, 'missing-openchamber-wsl.exe');
const { runtime } = createRuntime({ opencodeBinary: 'wsl:/usr/local/bin/opencode' });
const rejection = runtime.applyOpencodeBinaryFromSettings({ strict: true });
try {
await rejection;
expect(runtime.resolveManagedOpenCodeLaunchSpec('opencode').wrapperType).not.toBe('cmd-wrapper');
} catch (error) {
expect(error.message).toContain('uses WSL');
expect(error.code).toBeUndefined();
}
await expect(runtime.applyOpencodeBinaryFromSettings({ strict: true })).rejects.toMatchObject({
message: expect.stringContaining('uses WSL'),
});
});
it('detects OpenCode installed in the default WSL home fallback path', () => {
it('does not auto-detect OpenCode from WSL fallback paths', () => {
setPlatform('win32');
const dir = createTempDir('openchamber-wsl-opencode-');
const wslBinary = path.join(dir, 'wsl.exe');
@@ -182,15 +175,14 @@ describe('OpenCode env runtime', () => {
};
const { runtime, state } = createRuntime({}, { spawnSync: spawnSyncMock });
expect(runtime.resolveOpencodeCliPath()).toBe('wsl:/home/alice/.opencode/bin/opencode');
expect(state.useWslForOpencode).toBe(true);
expect(state.resolvedWslBinary).toBe(wslBinary);
expect(state.resolvedWslOpencodePath).toBe('/home/alice/.opencode/bin/opencode');
expect(state.resolvedOpencodeBinarySource).toBe('wsl');
expect(runtime.resolveOpencodeCliPath()).toBeNull();
expect(state.useWslForOpencode).toBe(false);
expect(state.resolvedWslBinary).toBeNull();
expect(state.resolvedWslOpencodePath).toBeNull();
expect(state.resolvedOpencodeBinarySource).toBeNull();
const wslCall = calls.find((call) => call.command === wslBinary);
expect(wslCall?.args).toEqual(expect.arrayContaining(['sh', '-lc']));
expect(wslCall?.args.join('\n')).toContain('$HOME/.opencode/bin/opencode');
expect(wslCall).toBeUndefined();
});
it('launches Windows cmd shims through cmd call without embedded quotes', () => {
+1 -21
View File
@@ -28,8 +28,6 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
applyOpencodeBinaryFromSettings,
ensureOpencodeCliEnv,
ensureLocalOpenCodeServerPassword,
buildWslExecArgs,
resolveWslExecutablePath,
resolveManagedOpenCodeLaunchSpec,
setOpenCodePort,
setDetectedOpenCodeApiPrefix,
@@ -231,25 +229,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
let launchWrapperType = null;
if (process.platform === 'win32' && state.useWslForOpencode) {
const wslBinary = state.resolvedWslBinary || resolveWslExecutablePath();
if (!wslBinary) {
throw new Error('WSL executable not found while attempting to launch OpenCode from WSL');
}
const wslOpencode = state.resolvedWslOpencodePath && state.resolvedWslOpencodePath.trim().length > 0
? state.resolvedWslOpencodePath.trim()
: 'opencode';
const serveHost = hostname === '127.0.0.1' ? '0.0.0.0' : hostname;
binary = wslBinary;
args = buildWslExecArgs([
wslOpencode,
'serve',
'--hostname',
serveHost,
'--port',
String(port),
], state.resolvedWslDistro);
throw new Error('Launching OpenCode through WSL is no longer supported. Install OpenCode natively on Windows and configure opencode.cmd or opencode.exe.');
}
if (process.platform === 'win32' && !state.useWslForOpencode) {
@@ -85,8 +85,6 @@ const createRuntime = (overrides = {}) => {
applyOpencodeBinaryFromSettings: vi.fn(async () => null),
ensureOpencodeCliEnv: vi.fn(),
ensureLocalOpenCodeServerPassword: vi.fn(async () => 'password'),
buildWslExecArgs: vi.fn((args) => args),
resolveWslExecutablePath: vi.fn(),
resolveManagedOpenCodeLaunchSpec: vi.fn((binary) => ({ binary, args: [], wrapperType: null })),
setOpenCodePort: vi.fn((port) => {
state.openCodePort = port;