Fix Windows compatibility: git status check, OpenCode spawn, path normalization, session merge (#527)

- Add isGitRepository check before getStatus() to prevent noisy errors on non-git directories
- Resolve npm shell wrapper shims on Windows so Bun can spawn OpenCode via node interpreter
- Add Win32PathFix middleware to normalize forward slashes to backslashes in directory params
- Merge sessions from all project directories on bare GET /session for Windows path compat

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
marco mereu
2026-02-26 20:21:31 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 6ca2b0da05
commit dc0f9b3e62
+119 -3
View File
@@ -4672,8 +4672,40 @@ async function createManagedOpenCodeServerProcess({
cwd,
env,
}) {
const binary = (process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
let binary = (process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
const args = ['serve', '--hostname', hostname, '--port', String(port)];
// On Windows, Bun/Node cannot directly spawn shell wrapper scripts (#!/bin/sh).
// Detect if the resolved binary is a shim that wraps a Node/Bun script and
// resolve the actual target so we can spawn it with the correct interpreter.
if (process.platform === 'win32') {
const interpreter = opencodeShimInterpreter(binary);
if (interpreter) {
// Binary itself has a node/bun shebang spawn via that interpreter.
args.unshift(binary);
binary = interpreter;
} else {
// The wrapper might be a shell shim generated by npm. Try to find the
// real JS entry point next to it (e.g. node_modules/opencode-ai/bin/opencode).
try {
const shimContent = fs.readFileSync(binary, 'utf8');
const jsMatch = shimContent.match(/node_modules[\\/]opencode[^\s"']*/);
if (jsMatch) {
const candidate = path.resolve(path.dirname(binary), jsMatch[0]);
if (fs.existsSync(candidate)) {
const realInterp = opencodeShimInterpreter(candidate);
if (realInterp) {
args.unshift(candidate);
binary = realInterp;
}
}
}
} catch {
// ignore fall through to default spawn
}
}
}
const child = spawn(binary, args, {
cwd,
env,
@@ -5145,6 +5177,30 @@ function setupProxy(app) {
}
app.set('opencodeProxyConfigured', true);
// Windows path normalization: OpenCode CLI stores paths with backslashes in the DB,
// but the frontend sends forward slashes. Rewrite directory query params on Windows.
// Must run BEFORE all other /api middleware.
if (process.platform === 'win32') {
app.use('/api', (req, _res, next) => {
// Parse directory from the raw URL since Express query parsing may not be available
const rawUrl = req.originalUrl || req.url || '';
const dirMatch = rawUrl.match(/[?&]directory=([^&]*)/);
if (dirMatch) {
const decoded = decodeURIComponent(dirMatch[1]);
if (decoded.includes('/')) {
const fixed = decoded.replace(/\//g, '\\');
const fixedEncoded = encodeURIComponent(fixed);
const newUrl = rawUrl.replace(/([?&]directory=)[^&]*/, '$1' + fixedEncoded);
console.log(`[Win32PathFix] Rewrote directory: "${decoded}" → "${fixed}"`);
console.log(`[Win32PathFix] URL: "${rawUrl}" → "${newUrl}"`);
req.originalUrl = newUrl;
req.url = newUrl;
}
}
next();
});
}
app.use('/api', (req, res, next) => {
if (
req.path.startsWith('/themes/custom') ||
@@ -5492,7 +5548,7 @@ function setupProxy(app) {
}
});
app.use('/api', (req, res, next) => {
app.use('/api', async (req, res, next) => {
if (isSseApiPath(req.path)) {
return next();
}
@@ -5501,6 +5557,61 @@ function setupProxy(app) {
return next();
}
// Windows: Merge sessions from all project directories on bare GET /session
if (process.platform === 'win32' && req.method === 'GET' && req.path === '/session') {
const rawUrl = req.originalUrl || req.url || '';
if (!rawUrl.includes('directory=')) {
try {
const authHeaders = getOpenCodeAuthHeaders();
const fetchOpts = {
method: 'GET',
headers: { Accept: 'application/json', ...authHeaders },
signal: AbortSignal.timeout(10000),
};
const globalRes = await fetch(buildOpenCodeUrl('/session', ''), fetchOpts);
const globalSessions = globalRes.ok ? (await globalRes.json()) : [];
const settingsPath = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
let projectDirs = [];
try {
const settingsRaw = fs.readFileSync(settingsPath, 'utf8');
const settings = JSON.parse(settingsRaw);
projectDirs = (settings.projects || [])
.map(p => p.path)
.filter(p => typeof p === 'string' && p.length > 0);
} catch {}
const seen = new Set(globalSessions.map(s => s.id));
const extraSessions = [];
for (const dir of projectDirs) {
const backslashDir = dir.replace(/\//g, '\\');
const encoded = encodeURIComponent(backslashDir);
try {
const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts);
if (dirRes.ok) {
const dirSessions = await dirRes.json();
if (Array.isArray(dirSessions)) {
for (const s of dirSessions) {
if (s && s.id && !seen.has(s.id)) {
seen.add(s.id);
extraSessions.push(s);
}
}
}
}
} catch {}
}
const merged = [...globalSessions, ...extraSessions];
merged.sort((a, b) => (b.time_updated || 0) - (a.time_updated || 0));
console.log(`[SessionMerge] ${globalSessions.length} global + ${extraSessions.length} extra = ${merged.length} total`);
return res.json(merged);
} catch (error) {
console.log(`[SessionMerge] Error: ${error.message}, falling through`);
}
}
}
return forwardGenericApiRequest(req, res);
});
}
@@ -9604,13 +9715,18 @@ async function main(options = {}) {
});
app.get('/api/git/status', async (req, res) => {
const { getStatus } = await getGitLibraries();
const { getStatus, isGitRepository } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const isRepo = await isGitRepository(directory);
if (!isRepo) {
return res.json({ isGitRepository: false, files: [], branch: null, ahead: 0, behind: 0 });
}
const status = await getStatus(directory);
res.json(status);
} catch (error) {