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:
committed by
GitHub
co-authored by
Claude Opus 4.6
parent
6ca2b0da05
commit
dc0f9b3e62
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user