fix(opencode): improve binary resolution and HOME handling

This commit is contained in:
Bohdan Triapitsyn
2026-02-05 13:57:50 +02:00
parent 26ffcf4a7b
commit 62b5ddd426
3 changed files with 191 additions and 8 deletions
+19 -5
View File
@@ -858,6 +858,19 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result<String> {
let mut path_segments: Vec<String> = Vec::new();
let mut seen = std::collections::HashSet::<String>::new();
let resolved_home_dir = app
.path()
.home_dir()
.ok()
.and_then(|p| {
let s = p.to_string_lossy().to_string();
if s.trim().is_empty() {
None
} else {
Some(s)
}
});
let mut push_unique = |value: String| {
let trimmed = value.trim();
if trimmed.is_empty() {
@@ -895,16 +908,13 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result<String> {
push_unique("/usr/sbin".to_string());
push_unique("/sbin".to_string());
if let Ok(home) = env::var("HOME") {
let home = home.trim();
if !home.is_empty() {
if let Some(home) = resolved_home_dir.as_deref() {
// OpenCode installer default.
push_unique(format!("{home}/.opencode/bin"));
push_unique(format!("{home}/.local/bin"));
push_unique(format!("{home}/.bun/bin"));
push_unique(format!("{home}/.cargo/bin"));
push_unique(format!("{home}/bin"));
}
}
if let Ok(existing) = env::var("PATH") {
@@ -922,7 +932,7 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result<String> {
};
let url = build_local_url(port);
let cmd = app
let mut cmd = app
.shell()
.sidecar(SIDECAR_NAME)
.map_err(|err| anyhow!("Failed to resolve sidecar '{SIDECAR_NAME}': {err}"))?
@@ -934,6 +944,10 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result<String> {
.env("NO_PROXY", no_proxy)
.env("no_proxy", no_proxy);
if let Some(home) = resolved_home_dir.as_deref() {
cmd = cmd.env("HOME", home);
}
let (rx, child) = match cmd.spawn() {
Ok(v) => v,
Err(err) => {
+1
View File
@@ -102,6 +102,7 @@ function resolveOpencodeCliPath(): string | null {
const home = os.homedir();
const unixFallbacks = [
path.join(home, '.opencode', 'bin', 'opencode'),
path.join(home, '.bun', 'bin', 'opencode'),
path.join(home, '.local', 'bin', 'opencode'),
path.join(home, 'bin', 'opencode'),
];
+171 -3
View File
@@ -1866,6 +1866,170 @@ const ENV_CONFIGURED_API_PREFIX = normalizeApiPrefix(
let globalEventWatcherAbortController = null;
let resolvedOpencodeBinary = null;
function isExecutable(filePath) {
try {
const stat = fs.statSync(filePath);
if (!stat.isFile()) return false;
if (process.platform === 'win32') {
const ext = path.extname(filePath).toLowerCase();
if (!ext) return true;
return ['.exe', '.cmd', '.bat', '.com'].includes(ext);
}
fs.accessSync(filePath, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
function prependToPath(dir) {
const trimmed = typeof dir === 'string' ? dir.trim() : '';
if (!trimmed) return;
const current = process.env.PATH || '';
const parts = current.split(path.delimiter).filter(Boolean);
if (parts.includes(trimmed)) return;
process.env.PATH = [trimmed, ...parts].join(path.delimiter);
}
function searchPathFor(binaryName) {
const current = process.env.PATH || '';
const parts = current.split(path.delimiter).filter(Boolean);
for (const dir of parts) {
const candidate = path.join(dir, binaryName);
if (isExecutable(candidate)) {
return candidate;
}
}
return null;
}
function resolveOpencodeCliPath() {
const explicit = [
process.env.OPENCODE_BINARY,
process.env.OPENCODE_PATH,
process.env.OPENCHAMBER_OPENCODE_PATH,
process.env.OPENCHAMBER_OPENCODE_BIN,
]
.map((v) => (typeof v === 'string' ? v.trim() : ''))
.filter(Boolean);
for (const candidate of explicit) {
if (isExecutable(candidate)) {
return candidate;
}
}
const resolvedFromPath = searchPathFor('opencode');
if (resolvedFromPath) {
return resolvedFromPath;
}
const home = os.homedir();
const unixFallbacks = [
path.join(home, '.opencode', 'bin', 'opencode'),
path.join(home, '.bun', 'bin', 'opencode'),
path.join(home, '.local', 'bin', 'opencode'),
path.join(home, 'bin', 'opencode'),
'/opt/homebrew/bin/opencode',
'/usr/local/bin/opencode',
'/usr/bin/opencode',
'/bin/opencode',
];
const winFallbacks = (() => {
const userProfile = process.env.USERPROFILE || home;
const appData = process.env.APPDATA || '';
const localAppData = process.env.LOCALAPPDATA || '';
const programData = process.env.ProgramData || 'C:\\ProgramData';
return [
path.join(userProfile, '.opencode', 'bin', 'opencode.exe'),
path.join(userProfile, '.opencode', 'bin', 'opencode.cmd'),
path.join(appData, 'npm', 'opencode.cmd'),
path.join(userProfile, 'scoop', 'shims', 'opencode.cmd'),
path.join(programData, 'chocolatey', 'bin', 'opencode.exe'),
path.join(programData, 'chocolatey', 'bin', 'opencode.cmd'),
path.join(userProfile, '.bun', 'bin', 'opencode.exe'),
path.join(userProfile, '.bun', 'bin', 'opencode.cmd'),
localAppData ? path.join(localAppData, 'Programs', 'opencode', 'opencode.exe') : '',
].filter(Boolean);
})();
const fallbacks = process.platform === 'win32' ? winFallbacks : unixFallbacks;
for (const candidate of fallbacks) {
if (isExecutable(candidate)) {
return candidate;
}
}
if (process.platform === 'win32') {
try {
const result = spawnSync('where', ['opencode'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
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 {
// ignore
}
return null;
}
const shells = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
for (const shell of shells) {
if (!isExecutable(shell)) continue;
try {
const result = spawnSync(shell, ['-lic', 'command -v opencode'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
if (result.status === 0) {
const found = (result.stdout || '').trim().split(/\s+/).pop() || '';
if (found && isExecutable(found)) {
return found;
}
}
} catch {
// ignore
}
}
return null;
}
function ensureOpencodeCliEnv() {
if (resolvedOpencodeBinary) {
return resolvedOpencodeBinary;
}
const existing = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
if (existing && isExecutable(existing)) {
resolvedOpencodeBinary = existing;
prependToPath(path.dirname(existing));
return resolvedOpencodeBinary;
}
const resolved = resolveOpencodeCliPath();
if (resolved) {
process.env.OPENCODE_BINARY = resolved;
prependToPath(path.dirname(resolved));
resolvedOpencodeBinary = resolved;
console.log(`Resolved opencode CLI: ${resolved}`);
return resolved;
}
return null;
}
const startGlobalEventWatcher = async () => {
if (globalEventWatcherAbortController) {
return;
@@ -2710,6 +2874,8 @@ async function startOpenCode() {
);
// Note: SDK starts in current process CWD. openCodeWorkingDirectory is tracked but not used for spawn in SDK.
ensureOpencodeCliEnv();
try {
const serverInstance = await createOpencodeServer({
hostname: '127.0.0.1',
@@ -3019,13 +3185,15 @@ async function refreshOpenCodeAfterConfigChange(reason, options = {}) {
}
function setupProxy(app) {
if (!openCodePort) return;
if (app.get('opencodeProxyConfigured')) {
return;
}
console.log(`Setting up proxy to OpenCode on port ${openCodePort}`);
if (openCodePort) {
console.log(`Setting up proxy to OpenCode on port ${openCodePort}`);
} else {
console.log('Setting up OpenCode API gate (OpenCode not started yet)');
}
app.set('opencodeProxyConfigured', true);
app.use('/api', (req, res, next) => {