fix(desktop): prefer the user's OpenCode install over the bundled CLI

The bundled CLI used to outrank PATH and known install locations, so
updating the desktop app silently switched people off their own OpenCode.
It is now the last resort: explicit env/settings, PATH, known install
locations, and shell discovery all win; the bundle only serves machines
with no OpenCode install at all. The env-runtime accepts an injectable
homedir so the fallback test stays hermetic on machines with a real
~/.opencode install.
This commit is contained in:
Bohdan Triapitsyn
2026-07-05 10:06:09 +03:00
parent 0820778763
commit 7ec45785d9
2 changed files with 27 additions and 15 deletions
+15 -10
View File
@@ -11,6 +11,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
readSettingsFromDiskMigrated,
} = deps;
const runSpawnSync = typeof deps.spawnSync === 'function' ? deps.spawnSync : spawnSync;
const resolveHomeDir = typeof deps.homedir === 'function' ? deps.homedir : () => os.homedir();
const parseNullSeparatedEnvSnapshot = (raw) => {
if (typeof raw !== 'string' || raw.length === 0) {
@@ -301,6 +302,14 @@ export const createOpenCodeEnvRuntime = (deps) => {
return null;
};
const bundledOpenCodeCliFallback = () => {
const bundled = resolveBundledOpenCodeCliPath();
if (!bundled) return null;
clearWslOpencodeResolution();
state.resolvedOpencodeBinarySource = 'bundled';
return bundled;
};
const clearWslOpencodeResolution = () => {
state.useWslForOpencode = false;
state.resolvedWslBinary = null;
@@ -326,13 +335,9 @@ export const createOpenCodeEnvRuntime = (deps) => {
}
}
const bundled = resolveBundledOpenCodeCliPath();
if (bundled) {
clearWslOpencodeResolution();
state.resolvedOpencodeBinarySource = 'bundled';
return bundled;
}
// The bundled CLI is the LAST resort (see bundledOpenCodeCliFallback at the
// exit points below): a user's own OpenCode install — PATH, known install
// locations, or shell-resolved — must win over the pinned bundled copy.
const resolvedFromPath = searchPathFor('opencode');
if (resolvedFromPath) {
clearWslOpencodeResolution();
@@ -340,7 +345,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
return resolvedFromPath;
}
const home = os.homedir();
const home = resolveHomeDir();
const unixFallbacks = [
path.join(home, '.opencode', 'bin', 'opencode'),
path.join(home, '.bun', 'bin', 'opencode'),
@@ -403,7 +408,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
// 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;
return bundledOpenCodeCliFallback();
}
const shells = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
@@ -427,7 +432,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
}
}
return null;
return bundledOpenCodeCliFallback();
};
const resolveNodeCliPath = () => {
@@ -111,6 +111,7 @@ const createRuntime = (settings, options = {}) => {
normalizeDirectoryPath: (value) => value,
readSettingsFromDiskMigrated: async () => settings,
spawnSync: options.spawnSync,
homedir: options.homedir,
});
return { runtime, state };
@@ -149,7 +150,7 @@ describe('OpenCode env runtime', () => {
expect(state.resolvedOpencodeBinarySource).toBe('settings');
});
it('resolves bundled OpenCode CLI before PATH lookup', () => {
it('prefers a user-installed OpenCode from PATH over the bundled CLI', () => {
const bundledDir = createTempDir('openchamber-bundled-opencode-');
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
const pathDir = createTempDir('openchamber-path-opencode-');
@@ -165,8 +166,8 @@ describe('OpenCode env runtime', () => {
delete process.env.OPENCODE_BINARY;
const { runtime, state } = createRuntime({});
expect(runtime.resolveOpencodeCliPath()).toBe(bundledBinary);
expect(state.resolvedOpencodeBinarySource).toBe('bundled');
expect(runtime.resolveOpencodeCliPath()).toBe(pathBinary);
expect(state.resolvedOpencodeBinarySource).toBe('path');
});
it('keeps explicit OpenCode binary ahead of bundled CLI', () => {
@@ -188,7 +189,7 @@ describe('OpenCode env runtime', () => {
expect(state.resolvedOpencodeBinarySource).toBe('env');
});
it('resolves bundled OpenCode CLI from Electron resourcesPath', () => {
it('falls back to the bundled OpenCode CLI from Electron resourcesPath when nothing else is installed', () => {
const resourcesPath = createTempDir('openchamber-resources-');
const bundledDir = path.join(resourcesPath, 'opencode-cli');
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
@@ -204,7 +205,13 @@ describe('OpenCode env runtime', () => {
process.env.PATH = createTempDir('openchamber-empty-path-');
delete process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR;
delete process.env.OPENCODE_BINARY;
const { runtime, state } = createRuntime({});
// The bundled CLI is the LAST resort now — hide the machine's own installs
// from the home-directory fallbacks and shell discovery.
const emptyHome = createTempDir('openchamber-empty-home-');
const { runtime, state } = createRuntime({}, {
spawnSync: () => ({ status: 1, stdout: '', stderr: '' }),
homedir: () => emptyHome,
});
expect(runtime.resolveOpencodeCliPath()).toBe(bundledBinary);
expect(state.resolvedOpencodeBinarySource).toBe('bundled');