Files
SuperCharged-Claude-Code-Up…/services/terminal-service.js
uroma 2c7037b9b7 debug: add comprehensive logging for terminal command flow
Phase 1 of systematic debugging: Gather evidence

Added detailed logging to trace the complete command flow:
- launchCommand: Shows when called, terminalId, command, WebSocket state
- waitForTerminalReady: Shows waiting period and ready state
- handleTerminalMessage: Shows all messages from backend with details
- WebSocket message content logged before sending

Also fixed duplicate 'ready' message (removed line 113-116).

Now when user creates "Claude Code CLI" terminal, console will show:
1. launchCommand called with terminalId and command
2. Waiting for terminal ready
3. Ready message received (or timeout)
4. Command sent to WebSocket
5. All backend messages logged

This will reveal exactly where the flow breaks.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 19:03:27 +00:00

385 lines
12 KiB
JavaScript

/**
* Terminal Service - Manages PTY processes and WebSocket connections
*/
const { spawn } = require('node-pty');
const { Server } = require('ws');
const fs = require('fs').promises;
const path = require('path');
class TerminalService {
constructor() {
this.terminals = new Map(); // terminalId -> { pty, ws, sessionId, workingDir, mode, createdAt }
this.wsServer = null;
this.logFile = path.join(process.env.HOME, 'obsidian-vault', '.claude-ide', 'terminal-logs.jsonl');
}
/**
* Setup WebSocket server for terminal I/O
*/
createServer(httpServer) {
this.wsServer = new Server({ noServer: true });
// Handle WebSocket upgrade requests
httpServer.on('upgrade', (request, socket, head) => {
const pathname = new URL(request.url, `http://${request.headers.host}`).pathname;
// Match /claude/api/terminals/:id/ws
const terminalMatch = pathname.match(/^\/claude\/api\/terminals\/([^/]+)\/ws$/);
if (terminalMatch) {
const terminalId = terminalMatch[1];
this.wsServer.handleUpgrade(request, socket, head, (ws) => {
this.wsServer.emit('connection', ws, request, terminalId);
});
}
});
// Handle WebSocket connections
this.wsServer.on('connection', (ws, request, terminalId) => {
this.handleConnection(terminalId, ws);
});
console.log('[TerminalService] WebSocket server initialized');
}
/**
* Create a new terminal PTY
*/
createTerminal(options) {
const {
workingDir = process.env.HOME,
sessionId = null,
mode = 'mixed',
shell = process.env.SHELL || '/bin/bash'
} = options;
// Generate unique terminal ID
const terminalId = `term-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
try {
// Spawn PTY process
const pty = spawn(shell, [], {
name: 'xterm-color',
cols: 80,
rows: 24,
cwd: workingDir,
env: process.env
});
// Store terminal info
const terminal = {
id: terminalId,
pty,
ws: null,
sessionId,
workingDir,
mode, // 'session', 'shell', or 'mixed'
createdAt: new Date().toISOString(),
lastActivity: new Date().toISOString()
};
this.terminals.set(terminalId, terminal);
// Log terminal creation
this.logCommand(terminalId, null, `Terminal created in ${workingDir} (mode: ${mode})`);
console.log(`[TerminalService] Created terminal ${terminalId} in ${workingDir}`);
return { success: true, terminalId, terminal };
} catch (error) {
console.error(`[TerminalService] Failed to create terminal:`, error);
return { success: false, error: error.message };
}
}
/**
* Handle WebSocket connection for terminal I/O
*/
handleConnection(terminalId, ws) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
ws.close(1008, 'Terminal not found');
return;
}
terminal.ws = ws;
terminal.lastActivity = new Date().toISOString();
console.log(`[TerminalService] WebSocket connected for terminal ${terminalId}`);
// Handle incoming messages from client (user input)
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
if (message.type === 'input') {
// User typed something - send to PTY
terminal.pty.write(message.data);
terminal.lastActivity = new Date().toISOString();
// Log commands (basic detection of Enter key)
if (message.data === '\r') {
// Could extract and log command here
}
} else if (message.type === 'resize') {
// Handle terminal resize
terminal.pty.resize(message.cols, message.rows);
}
} catch (error) {
console.error('[TerminalService] Error handling message:', error);
}
});
// Handle PTY output - send to client
terminal.pty.onData((data) => {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'data',
data: data
}));
}
});
// Handle PTY exit
terminal.pty.onExit(({ exitCode, signal }) => {
console.log(`[TerminalService] Terminal ${terminalId} exited: ${exitCode || signal}`);
this.logCommand(terminalId, null, `Terminal exited: ${exitCode || signal}`);
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'exit',
exitCode,
signal
}));
ws.close();
}
this.terminals.delete(terminalId);
});
// Handle WebSocket close
ws.on('close', () => {
console.log(`[TerminalService] WebSocket closed for terminal ${terminalId}`);
// Don't kill PTY immediately - allow reconnection
// PTY will be killed after timeout or explicit close
});
// Handle WebSocket error
ws.on('error', (error) => {
console.error(`[TerminalService] WebSocket error for terminal ${terminalId}:`, error);
});
// Send initial welcome message
ws.send(JSON.stringify({
type: 'ready',
terminalId,
workingDir: terminal.workingDir,
mode: terminal.mode
}));
}
/**
* Attach terminal to a Claude Code session
* Pipes session stdout/stderr to PTY
*/
attachToSession(terminalId, session) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
if (!session || !session.process) {
return { success: false, error: 'Invalid session' };
}
terminal.sessionId = session.id;
terminal.mode = 'session';
// Pipe session output to PTY
if (session.process.stdout) {
session.process.stdout.on('data', (data) => {
if (terminal.pty) {
terminal.pty.write(data.toString());
}
});
}
if (session.process.stderr) {
session.process.stderr.on('data', (data) => {
if (terminal.pty) {
// Write stderr in red
terminal.pty.write(`\x1b[31m${data.toString()}\x1b[0m`);
}
});
}
this.logCommand(terminalId, null, `Attached to session ${session.id}`);
console.log(`[TerminalService] Terminal ${terminalId} attached to session ${session.id}`);
return { success: true };
}
/**
* Set terminal mode (session/shell/mixed)
*/
setTerminalMode(terminalId, mode) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
terminal.mode = mode;
terminal.lastActivity = new Date().toISOString();
// Notify client of mode change
if (terminal.ws && terminal.ws.readyState === terminal.ws.OPEN) {
terminal.ws.send(JSON.stringify({
type: 'modeChanged',
mode
}));
}
return { success: true, mode };
}
/**
* Close terminal and kill PTY process
*/
closeTerminal(terminalId) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
try {
// Kill PTY process
if (terminal.pty) {
terminal.pty.kill();
}
// Close WebSocket
if (terminal.ws && terminal.ws.readyState === terminal.ws.OPEN) {
terminal.ws.close(1000, 'Terminal closed');
}
this.terminals.delete(terminalId);
this.logCommand(terminalId, null, 'Terminal closed');
console.log(`[TerminalService] Terminal ${terminalId} closed`);
return { success: true };
} catch (error) {
console.error(`[TerminalService] Error closing terminal ${terminalId}:`, error);
return { success: false, error: error.message };
}
}
/**
* Get list of active terminals
*/
listTerminals() {
const terminals = [];
for (const [id, terminal] of this.terminals.entries()) {
terminals.push({
id,
workingDir: terminal.workingDir,
sessionId: terminal.sessionId,
mode: terminal.mode,
createdAt: terminal.createdAt,
lastActivity: terminal.lastActivity
});
}
return terminals;
}
/**
* Get terminal by ID
*/
getTerminal(terminalId) {
const terminal = this.terminals.get(terminalId);
if (!terminal) {
return { success: false, error: 'Terminal not found' };
}
return {
success: true,
terminal: {
id: terminal.id,
workingDir: terminal.workingDir,
sessionId: terminal.sessionId,
mode: terminal.mode,
createdAt: terminal.createdAt,
lastActivity: terminal.lastActivity
}
};
}
/**
* Log command to file
*/
async logCommand(terminalId, command, action) {
try {
const logEntry = {
timestamp: new Date().toISOString(),
terminalId,
command: command || action,
user: process.env.USER || 'unknown'
};
// Ensure directory exists
const logDir = path.dirname(this.logFile);
await fs.mkdir(logDir, { recursive: true });
// Append to log file
await fs.appendFile(this.logFile, JSON.stringify(logEntry) + '\n');
} catch (error) {
console.error('[TerminalService] Failed to log command:', error);
}
}
/**
* Cleanup all terminals (called on server shutdown)
*/
async cleanup() {
console.log('[TerminalService] Cleaning up all terminals...');
for (const [id, terminal] of this.terminals.entries()) {
try {
if (terminal.pty) {
terminal.pty.kill();
}
if (terminal.ws && terminal.ws.readyState === terminal.ws.OPEN) {
terminal.ws.close(1000, 'Server shutting down');
}
} catch (error) {
console.error(`[TerminalService] Error cleaning up terminal ${id}:`, error);
}
}
this.terminals.clear();
// Close WebSocket server
if (this.wsServer) {
this.wsServer.close();
}
console.log('[TerminalService] Cleanup complete');
}
}
// Export singleton instance
const terminalService = new TerminalService();
module.exports = terminalService;