Files
openchamber/packages/web/server/lib/opencode/openchamber-routes.js
T

288 lines
11 KiB
JavaScript

export const registerOpenChamberRoutes = (app, dependencies) => {
const {
fs,
path,
process,
server,
__dirname,
openchamberDataDir,
modelsDevApiUrl,
modelsMetadataCacheTtl,
readSettingsFromDiskMigrated,
fetchFreeZenModels,
getCachedZenModels,
} = dependencies;
app.get('/api/openchamber/update-check', async (req, res) => {
try {
const { checkForUpdates } = await import('../package-manager.js');
const parseString = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
const parseReportUsage = (value) => {
if (typeof value !== 'string') return true;
const normalized = value.trim().toLowerCase();
if (normalized === 'false' || normalized === '0' || normalized === 'no') return false;
return true;
};
const inferDeviceClass = (ua) => {
const value = (ua || '').toLowerCase();
if (!value) return 'unknown';
if (value.includes('ipad') || value.includes('tablet')) return 'tablet';
if (value.includes('mobi') || value.includes('android') || value.includes('iphone')) return 'mobile';
return 'desktop';
};
const userAgent = typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'] : '';
const updateInfo = await checkForUpdates({
appType: parseString(req.query.appType),
deviceClass: parseString(req.query.deviceClass) || inferDeviceClass(userAgent),
platform: parseString(req.query.platform),
arch: parseString(req.query.arch),
instanceMode: parseString(req.query.instanceMode),
currentVersion: parseString(req.query.currentVersion),
installId: parseString(req.query.installId),
reportUsage: parseReportUsage(parseString(req.query.reportUsage)),
});
res.json(updateInfo);
} catch (error) {
console.error('Failed to check for updates:', error);
res.status(500).json({
available: false,
error: error instanceof Error ? error.message : 'Failed to check for updates',
});
}
});
app.post('/api/openchamber/update-install', async (_req, res) => {
try {
const { spawn: spawnChild } = await import('child_process');
const {
checkForUpdates,
getUpdateCommand,
detectPackageManagerDetails,
} = await import('../package-manager.js');
const updateInfo = await checkForUpdates();
if (!updateInfo.available) {
return res.status(400).json({ error: 'No update available' });
}
const pmDetails = detectPackageManagerDetails();
const pm = pmDetails.packageManager;
const updateCmd = getUpdateCommand(pm);
const isContainer =
fs.existsSync('/.dockerenv') ||
Boolean(process.env.CONTAINER) ||
process.env.container === 'docker';
if (isContainer) {
res.json({
success: true,
message: 'Update starting, server will stay online',
version: updateInfo.version,
packageManager: pm,
autoRestart: false,
});
setTimeout(() => {
console.log(`\nInstalling update using ${pm} (container mode)...`);
console.log(`Running: ${updateCmd}`);
const shell = process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : 'sh';
const shellFlag = process.platform === 'win32' ? '/c' : '-c';
const child = spawnChild(shell, [shellFlag, updateCmd], {
detached: true,
stdio: 'ignore',
env: process.env,
});
child.unref();
}, 500);
return;
}
const currentPort = server.address()?.port || 3000;
const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
let storedOptions = { port: currentPort, daemon: true };
try {
const content = await fs.promises.readFile(instanceFilePath, 'utf8');
storedOptions = JSON.parse(content);
} catch {
}
const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
const isForegroundService = launchMode === 'foreground';
const isWindows = process.platform === 'win32';
const quotePosix = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
const quoteCmd = (value) => {
const stringValue = String(value);
return `"${stringValue.replace(/"/g, '""')}"`;
};
const cliPath = path.resolve(__dirname, '..', 'bin', 'cli.js');
const restartParts = [
isWindows ? quoteCmd(process.execPath) : quotePosix(process.execPath),
isWindows ? quoteCmd(cliPath) : quotePosix(cliPath),
'serve',
'--port',
String(storedOptions.port),
];
let restartCmdPrimary = restartParts.join(' ');
let restartCmdFallback = `openchamber serve --port ${storedOptions.port}`;
if (storedOptions.host) {
if (isWindows) {
const escapedHost = storedOptions.host.replace(/"/g, '""');
restartCmdPrimary += ` --host "${escapedHost}"`;
restartCmdFallback += ` --host "${escapedHost}"`;
} else {
const escapedHost = storedOptions.host.replace(/'/g, "'\\''");
restartCmdPrimary += ` --host '${escapedHost}'`;
restartCmdFallback += ` --host '${escapedHost}'`;
}
}
if (storedOptions.uiPassword) {
if (isWindows) {
const escapedPw = storedOptions.uiPassword.replace(/"/g, '""');
restartCmdPrimary += ` --ui-password "${escapedPw}"`;
restartCmdFallback += ` --ui-password "${escapedPw}"`;
} else {
const escapedPw = storedOptions.uiPassword.replace(/'/g, "'\\''");
restartCmdPrimary += ` --ui-password '${escapedPw}'`;
restartCmdFallback += ` --ui-password '${escapedPw}'`;
}
}
if (storedOptions.apiOnly === true) {
restartCmdPrimary += ' --api-only';
restartCmdFallback += ' --api-only';
}
const restartCmd = isForegroundService ? '' : `(${restartCmdPrimary}) || (${restartCmdFallback})`;
const updateLogPath = path.join(openchamberDataDir, 'update-install.log');
const logPreamble = [
'',
`=== OpenChamber update ${new Date().toISOString()} ===`,
`currentVersion=${updateInfo.currentVersion || 'unknown'}`,
`targetVersion=${updateInfo.version || 'unknown'}`,
`packageManager=${pm}`,
`packageManagerReason=${pmDetails.reason || 'unknown'}`,
`packageManagerCommand=${pmDetails.packageManagerCommand || 'unknown'}`,
`packagePath=${pmDetails.packagePath || 'unknown'}`,
`globalNodeModulesRoot=${pmDetails.globalNodeModulesRoot || 'unknown'}`,
`mode=${isContainer ? 'container' : 'restart'}`,
`launchMode=${launchMode}`,
`updateCommand=${updateCmd}`,
`restartCommand=${restartCmd || 'service-manager'}`,
`logPath=${updateLogPath}`,
].join('\n');
res.json({
success: true,
message: 'Update starting, server will restart shortly',
version: updateInfo.version,
packageManager: pm,
autoRestart: true,
restartManager: isForegroundService ? 'service' : 'cli',
});
setTimeout(() => {
console.log(`\nInstalling update using ${pm}...`);
console.log(`Running: ${updateCmd}`);
console.log(logPreamble);
const shell = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'sh';
const shellFlag = isWindows ? '/c' : '-c';
const script = isWindows
? `
echo ${quoteCmd(logPreamble)}
timeout /t 2 /nobreak >nul
${updateCmd}
if %ERRORLEVEL% EQU 0 (
echo Update successful, restarting OpenChamber...
${restartCmd || 'echo Service manager will restart OpenChamber.'}
) else (
echo Update failed
exit /b 1
)
`
: `
printf '%s\n' ${quotePosix(logPreamble)}
sleep 2
${updateCmd}
if [ $? -eq 0 ]; then
echo "Update successful, restarting OpenChamber..."
${restartCmd || 'echo "Service manager will restart OpenChamber."'}
else
echo "Update failed"
exit 1
fi
`;
let logFd = null;
try {
fs.mkdirSync(path.dirname(updateLogPath), { recursive: true });
logFd = fs.openSync(updateLogPath, 'a');
} catch (logError) {
console.warn('Failed to open update log file, continuing without log capture:', logError);
}
const child = spawnChild(shell, [shellFlag, script], {
detached: true,
stdio: logFd !== null ? ['ignore', logFd, logFd] : 'ignore',
env: process.env,
});
child.unref();
if (logFd !== null) {
try {
fs.closeSync(logFd);
} catch {
}
}
console.log('Update process spawned, shutting down server...');
setTimeout(() => {
process.exit(0);
}, 500);
}, 500);
} catch (error) {
console.error('Failed to install update:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Failed to install update',
});
}
});
app.get('/api/openchamber/models-metadata', async (_req, res) => {
try {
const { getModelsMetadata } = await import('./models-metadata.js');
const { metadata, fromCache, stale } = await getModelsMetadata({
url: modelsDevApiUrl,
ttlMs: modelsMetadataCacheTtl,
});
res.setHeader('Cache-Control', fromCache && !stale ? 'public, max-age=60' : 'public, max-age=300');
res.json(metadata);
} catch (error) {
console.warn('Failed to fetch models.dev metadata via server:', error);
const statusCode = error?.name === 'TimeoutError' || error?.name === 'AbortError' ? 504 : 502;
res.status(statusCode).json({ error: 'Failed to retrieve model metadata' });
}
});
app.get('/api/zen/models', async (_req, res) => {
try {
const models = await fetchFreeZenModels();
res.setHeader('Cache-Control', 'public, max-age=300');
res.json({ models });
} catch (error) {
console.warn('Failed to fetch zen models:', error);
const cachedZenModels = getCachedZenModels();
if (cachedZenModels) {
res.setHeader('Cache-Control', 'public, max-age=60');
res.json(cachedZenModels);
} else {
const statusCode = error?.name === 'AbortError' ? 504 : 502;
res.status(statusCode).json({ error: 'Failed to retrieve zen models' });
}
}
});
};