fix: detect WSL OpenCode installs on Windows
Finds OpenCode in common WSL install paths Avoids picking Windows shims from WSL PATH Adds coverage for WSL detection
This commit is contained in:
@@ -11,6 +11,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
readSettingsFromDiskMigrated,
|
readSettingsFromDiskMigrated,
|
||||||
ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
|
ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
const runSpawnSync = typeof deps.spawnSync === 'function' ? deps.spawnSync : spawnSync;
|
||||||
|
|
||||||
const parseNullSeparatedEnvSnapshot = (raw) => {
|
const parseNullSeparatedEnvSnapshot = (raw) => {
|
||||||
if (typeof raw !== 'string' || raw.length === 0) {
|
if (typeof raw !== 'string' || raw.length === 0) {
|
||||||
@@ -149,7 +150,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
|
|
||||||
for (const shellPath of powershellCandidates) {
|
for (const shellPath of powershellCandidates) {
|
||||||
try {
|
try {
|
||||||
const result = spawnSync(shellPath, ['-NoLogo', '-Command', psScript], {
|
const result = runSpawnSync(shellPath, ['-NoLogo', '-Command', psScript], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
@@ -168,7 +169,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
|
|
||||||
const comspec = process.env.ComSpec || 'cmd.exe';
|
const comspec = process.env.ComSpec || 'cmd.exe';
|
||||||
try {
|
try {
|
||||||
const result = spawnSync(comspec, ['/d', '/s', '/c', 'set'], {
|
const result = runSpawnSync(comspec, ['/d', '/s', '/c', 'set'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
@@ -202,7 +203,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = spawnSync(shellPath, ['-lic', 'env -0'], {
|
const result = runSpawnSync(shellPath, ['-lic', 'env -0'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
@@ -283,7 +284,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = spawnSync('where', ['wsl'], {
|
const result = runSpawnSync('where', ['wsl'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -319,6 +320,19 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
return [...prefix, '--exec', ...execArgs];
|
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 = () => {
|
const probeWslForOpencode = () => {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
return null;
|
return null;
|
||||||
@@ -330,9 +344,9 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = spawnSync(
|
const result = runSpawnSync(
|
||||||
wslBinary,
|
wslBinary,
|
||||||
buildWslExecArgs(['sh', '-lc', 'command -v opencode']),
|
buildWslExecArgs(['sh', '-lc', wslOpencodeProbeScript]),
|
||||||
{
|
{
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
@@ -450,7 +464,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
try {
|
try {
|
||||||
const result = spawnSync('where', ['opencode'], {
|
const result = runSpawnSync('where', ['opencode'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -485,7 +499,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
for (const shell of shells) {
|
for (const shell of shells) {
|
||||||
if (!isExecutable(shell)) continue;
|
if (!isExecutable(shell)) continue;
|
||||||
try {
|
try {
|
||||||
const result = spawnSync(shell, ['-lic', 'command -v opencode'], {
|
const result = runSpawnSync(shell, ['-lic', 'command -v opencode'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -530,7 +544,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
try {
|
try {
|
||||||
const result = spawnSync('where', ['node'], {
|
const result = runSpawnSync('where', ['node'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -552,7 +566,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
for (const shell of shells) {
|
for (const shell of shells) {
|
||||||
if (!isExecutable(shell)) continue;
|
if (!isExecutable(shell)) continue;
|
||||||
try {
|
try {
|
||||||
const result = spawnSync(shell, ['-lic', 'command -v node'], {
|
const result = runSpawnSync(shell, ['-lic', 'command -v node'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -611,7 +625,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = spawnSync('where', ['bun'], {
|
const result = runSpawnSync('where', ['bun'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -633,7 +647,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
for (const shell of shells) {
|
for (const shell of shells) {
|
||||||
if (!isExecutable(shell)) continue;
|
if (!isExecutable(shell)) continue;
|
||||||
try {
|
try {
|
||||||
const result = spawnSync(shell, ['-lic', 'command -v bun'], {
|
const result = runSpawnSync(shell, ['-lic', 'command -v bun'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -764,7 +778,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const launcherPath = path.resolve(path.dirname(wrapperPath), launcherMatch[0]);
|
const launcherPath = path.resolve(path.dirname(wrapperPath), launcherMatch[0].replace(/[\\/]+/g, path.sep));
|
||||||
return path.dirname(path.dirname(path.dirname(launcherPath)));
|
return path.dirname(path.dirname(path.dirname(launcherPath)));
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ afterEach(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const createRuntime = (settings) => {
|
const createRuntime = (settings, options = {}) => {
|
||||||
const state = {
|
const state = {
|
||||||
cachedLoginShellEnvSnapshot: null,
|
cachedLoginShellEnvSnapshot: null,
|
||||||
resolvedOpencodeBinary: null,
|
resolvedOpencodeBinary: null,
|
||||||
@@ -90,7 +90,8 @@ const createRuntime = (settings) => {
|
|||||||
state,
|
state,
|
||||||
normalizeDirectoryPath: (value) => value,
|
normalizeDirectoryPath: (value) => value,
|
||||||
readSettingsFromDiskMigrated: async () => settings,
|
readSettingsFromDiskMigrated: async () => settings,
|
||||||
ENV_CONFIGURED_OPENCODE_WSL_DISTRO: null,
|
ENV_CONFIGURED_OPENCODE_WSL_DISTRO: options.ENV_CONFIGURED_OPENCODE_WSL_DISTRO ?? null,
|
||||||
|
spawnSync: options.spawnSync,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { runtime, state };
|
return { runtime, state };
|
||||||
@@ -158,6 +159,40 @@ describe('OpenCode env runtime', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('detects OpenCode installed in the default WSL home fallback path', () => {
|
||||||
|
setPlatform('win32');
|
||||||
|
const dir = createTempDir('openchamber-wsl-opencode-');
|
||||||
|
const wslBinary = path.join(dir, 'wsl.exe');
|
||||||
|
fs.writeFileSync(wslBinary, '');
|
||||||
|
process.env.PATH = dir;
|
||||||
|
process.env.SystemRoot = dir;
|
||||||
|
process.env.WSL_BINARY = wslBinary;
|
||||||
|
delete process.env.OPENCODE_BINARY;
|
||||||
|
|
||||||
|
const calls = [];
|
||||||
|
const spawnSyncMock = (command, args) => {
|
||||||
|
calls.push({ command, args });
|
||||||
|
if (command === 'where') {
|
||||||
|
return { status: 1, stdout: '', stderr: '' };
|
||||||
|
}
|
||||||
|
if (command === wslBinary) {
|
||||||
|
return { status: 0, stdout: '/home/alice/.opencode/bin/opencode\n', stderr: '' };
|
||||||
|
}
|
||||||
|
return { status: 1, stdout: '', stderr: '' };
|
||||||
|
};
|
||||||
|
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');
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
it('launches Windows cmd shims through cmd call without embedded quotes', () => {
|
it('launches Windows cmd shims through cmd call without embedded quotes', () => {
|
||||||
setPlatform('win32');
|
setPlatform('win32');
|
||||||
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe';
|
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe';
|
||||||
|
|||||||
Reference in New Issue
Block a user