mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 09:13:36 +00:00
244 lines
7.0 KiB
JavaScript
244 lines
7.0 KiB
JavaScript
import { spawn } from 'child_process';
|
|
|
|
import crossSpawn from 'cross-spawn';
|
|
|
|
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
import { createNormalizedMessage } from './shared/utils.js';
|
|
|
|
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
|
|
const activeOpenCodeProcesses = new Map();
|
|
|
|
function readOpenCodeSessionId(event) {
|
|
if (!event || typeof event !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
return event.sessionID || event.sessionId || null;
|
|
}
|
|
|
|
async function spawnOpenCode(command, options = {}, ws) {
|
|
return new Promise((resolve, reject) => {
|
|
const { sessionId, projectPath, cwd, model, sessionSummary } = options;
|
|
const workingDir = cwd || projectPath || process.cwd();
|
|
const processKey = sessionId || Date.now().toString();
|
|
let capturedSessionId = sessionId || null;
|
|
let sessionCreatedSent = false;
|
|
let stdoutLineBuffer = '';
|
|
let terminalNotificationSent = false;
|
|
|
|
const args = ['run', '--format', 'json'];
|
|
if (sessionId) {
|
|
args.push('--session', sessionId);
|
|
}
|
|
if (model) {
|
|
args.push('--model', model);
|
|
}
|
|
if (command && command.trim()) {
|
|
args.push(command.trim());
|
|
}
|
|
|
|
const notifyTerminalState = ({ code = null, error = null } = {}) => {
|
|
if (terminalNotificationSent) {
|
|
return;
|
|
}
|
|
|
|
terminalNotificationSent = true;
|
|
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
if (code === 0 && !error) {
|
|
notifyRunStopped({
|
|
userId: ws?.userId || null,
|
|
provider: 'opencode',
|
|
sessionId: finalSessionId,
|
|
sessionName: sessionSummary,
|
|
stopReason: 'completed',
|
|
});
|
|
return;
|
|
}
|
|
|
|
notifyRunFailed({
|
|
userId: ws?.userId || null,
|
|
provider: 'opencode',
|
|
sessionId: finalSessionId,
|
|
sessionName: sessionSummary,
|
|
error: error || `OpenCode CLI exited with code ${code}`,
|
|
});
|
|
};
|
|
|
|
const opencodeProcess = spawnFunction('opencode', args, {
|
|
cwd: workingDir,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: { ...process.env },
|
|
});
|
|
|
|
activeOpenCodeProcesses.set(processKey, opencodeProcess);
|
|
opencodeProcess.sessionId = processKey;
|
|
opencodeProcess.stdin.end();
|
|
|
|
const registerSession = (nextSessionId) => {
|
|
if (!nextSessionId || capturedSessionId === nextSessionId) {
|
|
return;
|
|
}
|
|
|
|
capturedSessionId = nextSessionId;
|
|
if (processKey !== capturedSessionId) {
|
|
activeOpenCodeProcesses.delete(processKey);
|
|
activeOpenCodeProcesses.set(capturedSessionId, opencodeProcess);
|
|
}
|
|
opencodeProcess.sessionId = capturedSessionId;
|
|
|
|
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
ws.setSessionId(capturedSessionId);
|
|
}
|
|
|
|
if (!sessionId && !sessionCreatedSent) {
|
|
sessionCreatedSent = true;
|
|
ws.send(createNormalizedMessage({
|
|
kind: 'session_created',
|
|
newSessionId: capturedSessionId,
|
|
sessionId: capturedSessionId,
|
|
provider: 'opencode',
|
|
}));
|
|
}
|
|
};
|
|
|
|
const processOpenCodeOutputLine = (line) => {
|
|
if (!line || !line.trim()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = JSON.parse(line);
|
|
registerSession(readOpenCodeSessionId(response));
|
|
const normalized = sessionsService.normalizeMessage(
|
|
'opencode',
|
|
response,
|
|
capturedSessionId || sessionId || null,
|
|
);
|
|
for (const msg of normalized) {
|
|
ws.send(msg);
|
|
}
|
|
} catch {
|
|
ws.send(createNormalizedMessage({
|
|
kind: 'stream_delta',
|
|
content: line,
|
|
sessionId: capturedSessionId || sessionId || null,
|
|
provider: 'opencode',
|
|
}));
|
|
}
|
|
};
|
|
|
|
opencodeProcess.stdout.on('data', (data) => {
|
|
stdoutLineBuffer += data.toString();
|
|
const completeLines = stdoutLineBuffer.split(/\r?\n/);
|
|
stdoutLineBuffer = completeLines.pop() || '';
|
|
|
|
completeLines.forEach((line) => {
|
|
processOpenCodeOutputLine(line.trim());
|
|
});
|
|
});
|
|
|
|
opencodeProcess.stderr.on('data', (data) => {
|
|
const stderrText = data.toString();
|
|
if (!stderrText.trim()) {
|
|
return;
|
|
}
|
|
|
|
ws.send(createNormalizedMessage({
|
|
kind: 'error',
|
|
content: stderrText,
|
|
sessionId: capturedSessionId || sessionId || null,
|
|
provider: 'opencode',
|
|
}));
|
|
});
|
|
|
|
opencodeProcess.on('close', async (code) => {
|
|
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
activeOpenCodeProcesses.delete(finalSessionId);
|
|
activeOpenCodeProcesses.delete(processKey);
|
|
|
|
if (stdoutLineBuffer.trim()) {
|
|
processOpenCodeOutputLine(stdoutLineBuffer.trim());
|
|
stdoutLineBuffer = '';
|
|
}
|
|
|
|
ws.send(createNormalizedMessage({
|
|
kind: 'complete',
|
|
exitCode: code,
|
|
isNewSession: !sessionId && !!command,
|
|
sessionId: finalSessionId,
|
|
provider: 'opencode',
|
|
}));
|
|
|
|
if (code === 0) {
|
|
notifyTerminalState({ code });
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
if (code === 127 || code === null) {
|
|
const installed = await providerAuthService.isProviderInstalled('opencode');
|
|
if (!installed) {
|
|
ws.send(createNormalizedMessage({
|
|
kind: 'error',
|
|
content: 'OpenCode CLI is not installed. Install it from https://opencode.ai/docs/',
|
|
sessionId: finalSessionId,
|
|
provider: 'opencode',
|
|
}));
|
|
}
|
|
}
|
|
|
|
notifyTerminalState({ code });
|
|
reject(new Error(code === null ? 'OpenCode CLI process was terminated' : `OpenCode CLI exited with code ${code}`));
|
|
});
|
|
|
|
opencodeProcess.on('error', async (error) => {
|
|
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
activeOpenCodeProcesses.delete(finalSessionId);
|
|
activeOpenCodeProcesses.delete(processKey);
|
|
|
|
const installed = await providerAuthService.isProviderInstalled('opencode');
|
|
const errorContent = !installed
|
|
? 'OpenCode CLI is not installed. Install it from https://opencode.ai/docs/'
|
|
: error.message;
|
|
|
|
ws.send(createNormalizedMessage({
|
|
kind: 'error',
|
|
content: errorContent,
|
|
sessionId: finalSessionId,
|
|
provider: 'opencode',
|
|
}));
|
|
notifyTerminalState({ error });
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
function abortOpenCodeSession(sessionId) {
|
|
const process = activeOpenCodeProcesses.get(sessionId);
|
|
if (!process) {
|
|
return false;
|
|
}
|
|
|
|
process.kill('SIGTERM');
|
|
activeOpenCodeProcesses.delete(sessionId);
|
|
return true;
|
|
}
|
|
|
|
function isOpenCodeSessionActive(sessionId) {
|
|
return activeOpenCodeProcesses.has(sessionId);
|
|
}
|
|
|
|
function getActiveOpenCodeSessions() {
|
|
return Array.from(activeOpenCodeProcesses.keys());
|
|
}
|
|
|
|
export {
|
|
spawnOpenCode,
|
|
abortOpenCodeSession,
|
|
isOpenCodeSessionActive,
|
|
getActiveOpenCodeSessions,
|
|
};
|