fix(opencode): improve binary resolution and HOME handling
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user