fix: drop WSL OpenCode binary support on Windows
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user