Merge branch 'main' into reproduce/issue-1720
Signed-off-by: Mayuresh K <23300+mskadu@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { clearAppImageArgv0FromProcessEnv } from '../inherited-env.js';
|
||||
import { mergePathValues } from './path-utils.js';
|
||||
|
||||
const SHELL_PROBE_TIMEOUT_MS = 5_000;
|
||||
@@ -13,6 +14,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) {
|
||||
@@ -88,14 +90,13 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
return isExecutable(trimmed) ? trimmed : null;
|
||||
};
|
||||
|
||||
const searchPathFor = (binaryName) => {
|
||||
const searchPathFor = (binaryName, searchPath = process.env.PATH || '') => {
|
||||
const trimmed = typeof binaryName === 'string' ? binaryName.trim() : '';
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const current = process.env.PATH || '';
|
||||
const parts = current.split(path.delimiter).filter(Boolean);
|
||||
const parts = searchPath.split(path.delimiter).filter(Boolean);
|
||||
const candidateNames = [];
|
||||
|
||||
if (process.platform === 'win32' && !path.extname(trimmed)) {
|
||||
@@ -230,12 +231,16 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
};
|
||||
|
||||
const applyLoginShellEnvSnapshot = () => {
|
||||
// Always clear AppImage ARGV0, even when no login-shell snapshot is available.
|
||||
// Otherwise a leaked process.env.ARGV0 survives into later child spawns (#2588).
|
||||
clearAppImageArgv0FromProcessEnv();
|
||||
|
||||
const snapshot = getLoginShellEnvSnapshot();
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skipKeys = new Set(['PWD', 'OLDPWD', 'SHLVL', '_']);
|
||||
const skipKeys = new Set(['PWD', 'OLDPWD', 'SHLVL', '_', 'ARGV0']);
|
||||
for (const [key, value] of Object.entries(snapshot)) {
|
||||
if (skipKeys.has(key)) {
|
||||
continue;
|
||||
@@ -263,6 +268,72 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
return /(^|[\\/])wsl(\.exe)?$/i.test(trimmed);
|
||||
};
|
||||
|
||||
const isWindowsOpenCodeDesktopAppPath = (candidate) => {
|
||||
if (process.platform !== 'win32' || typeof candidate !== 'string') {
|
||||
return false;
|
||||
}
|
||||
const normalized = path.resolve(candidate).toLowerCase();
|
||||
const localAppData = typeof process.env.LOCALAPPDATA === 'string' && process.env.LOCALAPPDATA.trim()
|
||||
? path.resolve(process.env.LOCALAPPDATA).toLowerCase()
|
||||
: '';
|
||||
if (!localAppData || !normalized.startsWith(`${localAppData}${path.sep}`)) {
|
||||
return false;
|
||||
}
|
||||
return normalized.endsWith(`${path.sep}programs${path.sep}opencode${path.sep}opencode.exe`);
|
||||
};
|
||||
|
||||
const bundledOpenCodeCliCandidates = () => {
|
||||
const names = process.platform === 'win32' ? ['opencode.exe'] : ['opencode'];
|
||||
const roots = [
|
||||
process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR,
|
||||
typeof process.resourcesPath === 'string' ? path.join(process.resourcesPath, 'opencode-cli') : null,
|
||||
]
|
||||
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
||||
.filter(Boolean);
|
||||
|
||||
const candidates = [];
|
||||
for (const root of roots) {
|
||||
for (const name of names) {
|
||||
candidates.push(path.join(root, name));
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
|
||||
const resolveBundledOpenCodeCliPath = () => {
|
||||
for (const candidate of bundledOpenCodeCliCandidates()) {
|
||||
if (isExecutable(candidate) && !isWindowsOpenCodeDesktopAppPath(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const canonicalExecutablePath = (candidate) => {
|
||||
if (typeof candidate !== 'string' || !candidate.trim()) return null;
|
||||
try {
|
||||
return fs.realpathSync.native(candidate.trim());
|
||||
} catch {
|
||||
return path.resolve(candidate.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const isBundledOpenCodeCliPath = (candidate) => {
|
||||
const canonicalCandidate = canonicalExecutablePath(candidate);
|
||||
if (!canonicalCandidate) return false;
|
||||
return bundledOpenCodeCliCandidates().some((bundledCandidate) => (
|
||||
canonicalExecutablePath(bundledCandidate) === canonicalCandidate
|
||||
));
|
||||
};
|
||||
|
||||
const bundledOpenCodeCliFallback = () => {
|
||||
const bundled = resolveBundledOpenCodeCliPath();
|
||||
if (!bundled) return null;
|
||||
clearWslOpencodeResolution();
|
||||
state.resolvedOpencodeBinarySource = 'bundled';
|
||||
return bundled;
|
||||
};
|
||||
|
||||
const clearWslOpencodeResolution = () => {
|
||||
state.useWslForOpencode = false;
|
||||
state.resolvedWslBinary = null;
|
||||
@@ -270,6 +341,19 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
state.resolvedWslDistro = null;
|
||||
};
|
||||
|
||||
// Strip a single wrapping quote pair (Windows "Copy as path" and quoted
|
||||
// shell snippets) — literal quotes are never part of a real path and break
|
||||
// every executable check.
|
||||
const stripWrappingQuotes = (value) => {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||
if (trimmed.length >= 2
|
||||
&& ((trimmed.startsWith('"') && trimmed.endsWith('"'))
|
||||
|| (trimmed.startsWith("'") && trimmed.endsWith("'")))) {
|
||||
return trimmed.slice(1, -1).trim();
|
||||
}
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const resolveOpencodeCliPath = () => {
|
||||
const explicit = [
|
||||
process.env.OPENCODE_BINARY,
|
||||
@@ -277,17 +361,20 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
process.env.OPENCHAMBER_OPENCODE_PATH,
|
||||
process.env.OPENCHAMBER_OPENCODE_BIN,
|
||||
]
|
||||
.map((v) => (typeof v === 'string' ? v.trim() : ''))
|
||||
.map(stripWrappingQuotes)
|
||||
.filter(Boolean);
|
||||
|
||||
for (const candidate of explicit) {
|
||||
if (isExecutable(candidate)) {
|
||||
if (isExecutable(candidate) && !isWindowsOpenCodeDesktopAppPath(candidate)) {
|
||||
clearWslOpencodeResolution();
|
||||
state.resolvedOpencodeBinarySource = 'env';
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
const bundled = bundledOpenCodeCliFallback();
|
||||
if (bundled) return bundled;
|
||||
|
||||
const resolvedFromPath = searchPathFor('opencode');
|
||||
if (resolvedFromPath) {
|
||||
clearWslOpencodeResolution();
|
||||
@@ -295,7 +382,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'),
|
||||
@@ -303,6 +390,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
path.join(home, 'bin', 'opencode'),
|
||||
'/opt/homebrew/bin/opencode',
|
||||
'/usr/local/bin/opencode',
|
||||
'/home/linuxbrew/.linuxbrew/bin/opencode',
|
||||
'/usr/bin/opencode',
|
||||
'/bin/opencode',
|
||||
];
|
||||
@@ -313,16 +401,21 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
const localAppData = process.env.LOCALAPPDATA || '';
|
||||
const programData = process.env.ProgramData || 'C:\\ProgramData';
|
||||
|
||||
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
||||
|
||||
return [
|
||||
path.join(userProfile, '.opencode', 'bin', 'opencode.exe'),
|
||||
path.join(userProfile, '.opencode', 'bin', 'opencode.cmd'),
|
||||
path.join(appData, 'npm', 'opencode.cmd'),
|
||||
// System-wide Node installer keeps the global npm prefix here
|
||||
// (npm i -g opencode-ai → opencode.cmd shim).
|
||||
path.join(programFiles, 'nodejs', 'opencode.cmd'),
|
||||
path.join(userProfile, 'scoop', 'shims', 'opencode.exe'),
|
||||
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);
|
||||
})();
|
||||
|
||||
@@ -347,7 +440,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const found = lines.find((line) => isExecutable(line));
|
||||
const found = lines.find((line) => isExecutable(line) && !isWindowsOpenCodeDesktopAppPath(line));
|
||||
if (found) {
|
||||
clearWslOpencodeResolution();
|
||||
state.resolvedOpencodeBinarySource = 'where';
|
||||
@@ -653,8 +746,15 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
};
|
||||
|
||||
const getWindowsNativeOpencodePackageNames = () => {
|
||||
// TEMPORARY WORKAROUND — Windows ARM64: native opencode.exe fails with a Bun
|
||||
// FFI/TinyCC dlopen error (https://github.com/anomalyco/opencode/issues/19130).
|
||||
// prepare-opencode-cli.mjs bundles x64-baseline instead; match that here so
|
||||
// the runtime resolver looks for the same x64-baseline package. Restore the
|
||||
// arm64 branch below when the upstream issue is resolved.
|
||||
if (process.arch === 'arm64') {
|
||||
return ['opencode-windows-arm64'];
|
||||
// --- ORIGINAL (restore when ARM64 is fixed) ---
|
||||
// return ['opencode-windows-arm64'];
|
||||
return ['opencode-windows-x64-baseline', 'opencode-windows-x64'];
|
||||
}
|
||||
if (process.arch === 'x64') {
|
||||
// Prefer the baseline build when bypassing package-manager wrappers so the
|
||||
@@ -843,6 +943,16 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: never hand a raw .cmd/.bat to spawn(shell:false) — cmd
|
||||
// shims need cmd.exe, and unquoted space-containing paths break there.
|
||||
if (WINDOWS_BATCH_EXTENSIONS.has(ext)) {
|
||||
return {
|
||||
binary: process.env.ComSpec || 'cmd.exe',
|
||||
args: ['/d', '/s', '/c', 'call', fallbackBinary],
|
||||
wrapperType: 'cmd-wrapper',
|
||||
};
|
||||
}
|
||||
|
||||
return { binary: fallbackBinary, args: [], wrapperType: null };
|
||||
};
|
||||
|
||||
@@ -898,16 +1008,20 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
if (process.platform !== 'darwin' || typeof candidate !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return /\/OpenCode\.app\/Contents\/MacOS\/(?:OpenCode|opencode-cli)$/i.test(candidate);
|
||||
return /\/OpenCode(?: Dev| Beta)?\.app\/Contents\/MacOS\/(?:OpenCode(?: Dev| Beta)?|opencode-cli)$/i.test(candidate);
|
||||
};
|
||||
|
||||
const isKnownOpenCodeDesktopAppPath = (candidate) => isMacOpenCodeAppBundlePath(candidate)
|
||||
|| isWindowsOpenCodeDesktopAppPath(candidate);
|
||||
|
||||
const createConfiguredOpencodeBinaryError = (raw, normalized) => {
|
||||
const configured = typeof raw === 'string' ? raw.trim() : '';
|
||||
const candidate = typeof normalized === 'string' && normalized.trim().length > 0 ? normalized.trim() : configured;
|
||||
const messageSuffix = 'OpenChamber needs the standalone opencode CLI. Install it and set settings.opencodeBinary to the CLI path, for example ~/.opencode/bin/opencode, or leave the setting empty to use PATH lookup.';
|
||||
const error = (() => {
|
||||
if (isMacOpenCodeAppBundlePath(candidate) || isMacOpenCodeAppBundlePath(configured)) {
|
||||
return new Error(`Configured OpenCode binary points at the macOS desktop app bundle, not the CLI: ${candidate}. ${messageSuffix}`);
|
||||
if (isKnownOpenCodeDesktopAppPath(candidate) || isKnownOpenCodeDesktopAppPath(configured)) {
|
||||
const platformName = process.platform === 'win32' ? 'Windows desktop app install' : 'macOS desktop app bundle';
|
||||
return new Error(`Configured OpenCode binary points at the ${platformName}, not the CLI: ${candidate}. ${messageSuffix}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1004,7 +1118,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalized && isExecutable(normalized) && !isMacOpenCodeAppBundlePath(normalized)) {
|
||||
if (normalized && isExecutable(normalized) && !isKnownOpenCodeDesktopAppPath(normalized)) {
|
||||
clearWslOpencodeResolution();
|
||||
process.env.OPENCODE_BINARY = normalized;
|
||||
prependToPath(path.dirname(normalized));
|
||||
@@ -1156,6 +1270,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
applyOpencodeBinaryFromSettings,
|
||||
getLoginShellEnvSnapshot,
|
||||
resolveOpencodeCliPath,
|
||||
isBundledOpenCodeCliPath,
|
||||
resolveManagedOpenCodeLaunchSpec,
|
||||
isExecutable,
|
||||
searchPathFor,
|
||||
|
||||
Reference in New Issue
Block a user