UNPKG

ursamu-mud

Version:

Ursamu - Modular MUD Engine with sandboxed scripting and plugin system

789 lines 29.7 kB
/** * Interactive debugging commands */ import { WebSocket } from 'ws'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { BaseCommand } from './BaseCommand.js'; import { RestClient } from '../clients/RestClient.js'; export class DebugCommands extends BaseCommand { restClient; activeSession; debugHistory = []; constructor(config) { super(config); this.restClient = new RestClient(config); } register(program) { const debug = program .command('debug') .description('Interactive debugging commands'); // Start debug session debug .command('start') .description('Start debugging session for script') .argument('<script>', 'script name') .option('-b, --breakpoint <line>', 'set initial breakpoint') .option('-c, --condition <expr>', 'breakpoint condition') .option('--entry', 'break on script entry') .action(async (scriptName, options) => { await this.handleCommand(() => this.startDebugging(scriptName, options)); }); // Stop debug session debug .command('stop') .description('Stop current debugging session') .action(async () => { await this.handleCommand(() => this.stopDebugging()); }); // Set breakpoint debug .command('break') .alias('bp') .description('Set breakpoint') .argument('<line>', 'line number') .option('-c, --condition <expr>', 'breakpoint condition') .option('-f, --file <file>', 'source file (if different)') .action(async (line, options) => { await this.handleCommand(() => this.setBreakpoint(parseInt(line), options)); }); // Remove breakpoint debug .command('clear') .description('Clear breakpoint') .argument('<id>', 'breakpoint ID or line number') .action(async (identifier) => { await this.handleCommand(() => this.clearBreakpoint(identifier)); }); // List breakpoints debug .command('list') .alias('ls') .description('List all breakpoints') .action(async () => { await this.handleCommand(() => this.listBreakpoints()); }); // Continue execution debug .command('continue') .alias('c') .description('Continue execution') .action(async () => { await this.handleCommand(() => this.continueExecution()); }); // Step into debug .command('step') .alias('s') .description('Step into next statement') .action(async () => { await this.handleCommand(() => this.stepInto()); }); // Step over debug .command('next') .alias('n') .description('Step over next statement') .action(async () => { await this.handleCommand(() => this.stepOver()); }); // Step out debug .command('out') .alias('o') .description('Step out of current function') .action(async () => { await this.handleCommand(() => this.stepOut()); }); // Show stack trace debug .command('stack') .description('Show call stack') .option('-v, --verbose', 'show variables') .action(async (options) => { await this.handleCommand(() => this.showStack(options)); }); // Inspect variables debug .command('vars') .description('Show variables in current scope') .argument('[pattern]', 'variable name pattern') .action(async (pattern) => { await this.handleCommand(() => this.showVariables(pattern)); }); // Evaluate expression debug .command('eval') .description('Evaluate expression in current context') .argument('<expression>', 'expression to evaluate') .action(async (expression) => { await this.handleCommand(() => this.evaluateExpression(expression)); }); // Watch variable debug .command('watch') .description('Watch variable for changes') .argument('<variable>', 'variable name') .action(async (variable) => { await this.handleCommand(() => this.watchVariable(variable)); }); // Interactive debug mode debug .command('interactive') .alias('i') .description('Enter interactive debugging mode') .argument('[script]', 'script to debug') .action(async (scriptName) => { await this.handleCommand(() => this.interactiveMode(scriptName)); }); // Show debug history debug .command('history') .description('Show debug session history') .option('-n, --lines <count>', 'number of entries to show', '20') .action(async (options) => { await this.handleCommand(() => this.showHistory(parseInt(options.lines))); }); } async startDebugging(scriptName, options = {}) { if (this.activeSession) { this.warn('Debug session already active. Stop current session first.'); return; } const spinner = this.startSpinner('Starting debug session...'); try { // Start debug session with server this.updateSpinner('Connecting to debug server...'); const result = await this.restClient.startDebugSession(scriptName); if (!result.success || !result.sessionId) { this.failSpinner('Failed to start debug session'); return; } // Connect WebSocket for debugging this.updateSpinner('Establishing debug connection...'); const websocket = await this.connectDebugWebSocket(result.sessionId); // Create debug session this.activeSession = { sessionId: result.sessionId, scriptName, websocket, breakpoints: new Map(), callStack: [], variables: {}, status: 'running' }; // Set initial breakpoint if requested if (options.breakpoint) { const line = parseInt(options.breakpoint); await this.setBreakpoint(line, { condition: options.condition }); } if (options.entry) { await this.setBreakpoint(1, { condition: null }); } this.succeedSpinner(`Debug session started: ${chalk.green(scriptName)}`); console.log(chalk.gray(`Session ID: ${result.sessionId}`)); console.log(chalk.gray('Use `mud debug interactive` to enter interactive mode')); } catch (error) { this.failSpinner('Failed to start debugging'); throw error; } } async stopDebugging() { if (!this.activeSession) { this.warn('No active debug session'); return; } const spinner = this.startSpinner('Stopping debug session...'); try { // Close WebSocket connection if (this.activeSession.websocket.readyState === WebSocket.OPEN) { this.activeSession.websocket.close(); } // Stop server-side session await this.restClient.stopDebugSession(this.activeSession.sessionId); this.succeedSpinner(`Debug session stopped: ${chalk.yellow(this.activeSession.scriptName)}`); this.activeSession = undefined; } catch (error) { this.failSpinner('Error stopping debug session'); this.activeSession = undefined; throw error; } } async setBreakpoint(line, options = {}) { if (!this.activeSession) { this.error('No active debug session'); return; } const spinner = this.startSpinner(`Setting breakpoint at line ${line}...`); try { const breakpointId = `bp_${line}_${Date.now()}`; // Send breakpoint to debugger const message = { type: 'set_breakpoint', data: { id: breakpointId, line, condition: options.condition, file: options.file } }; this.sendDebugMessage(message); // Store breakpoint info const breakpoint = { id: breakpointId, line, condition: options.condition, enabled: true, hitCount: 0 }; this.activeSession.breakpoints.set(breakpointId, breakpoint); this.succeedSpinner(`Breakpoint set at line ${chalk.yellow(line.toString())}`); if (options.condition) { console.log(chalk.gray(`Condition: ${options.condition}`)); } } catch (error) { this.failSpinner('Failed to set breakpoint'); throw error; } } async clearBreakpoint(identifier) { if (!this.activeSession) { this.error('No active debug session'); return; } const spinner = this.startSpinner('Clearing breakpoint...'); try { let breakpointId; // Find breakpoint by ID or line number if (this.activeSession.breakpoints.has(identifier)) { breakpointId = identifier; } else { // Search by line number const line = parseInt(identifier); if (!isNaN(line)) { for (const [id, bp] of this.activeSession.breakpoints.entries()) { if (bp.line === line) { breakpointId = id; break; } } } } if (!breakpointId) { this.failSpinner('Breakpoint not found'); return; } const breakpoint = this.activeSession.breakpoints.get(breakpointId); // Send clear message to debugger this.sendDebugMessage({ type: 'clear_breakpoint', data: { id: breakpointId } }); // Remove from local storage this.activeSession.breakpoints.delete(breakpointId); this.succeedSpinner(`Breakpoint cleared at line ${chalk.yellow(breakpoint.line.toString())}`); } catch (error) { this.failSpinner('Failed to clear breakpoint'); throw error; } } async listBreakpoints() { if (!this.activeSession) { this.error('No active debug session'); return; } if (this.activeSession.breakpoints.size === 0) { console.log(chalk.yellow('No breakpoints set')); return; } console.log(chalk.bold('Breakpoints:')); console.log('ID'.padEnd(20) + 'Line'.padEnd(8) + 'Hits'.padEnd(8) + 'Condition'); console.log('─'.repeat(60)); for (const [id, bp] of this.activeSession.breakpoints.entries()) { const status = bp.enabled ? chalk.green('●') : chalk.red('○'); const condition = bp.condition || chalk.gray('none'); console.log(`${status} ${id.slice(0, 16).padEnd(16)} ` + `${bp.line.toString().padEnd(6)} ` + `${bp.hitCount.toString().padEnd(6)} ` + `${condition}`); } } async continueExecution() { if (!this.activeSession) { this.error('No active debug session'); return; } this.sendDebugMessage({ type: 'continue', data: {} }); this.activeSession.status = 'running'; console.log(chalk.blue('Continuing execution...')); } async stepInto() { if (!this.activeSession) { this.error('No active debug session'); return; } this.sendDebugMessage({ type: 'step_into', data: {} }); console.log(chalk.blue('Stepping into...')); } async stepOver() { if (!this.activeSession) { this.error('No active debug session'); return; } this.sendDebugMessage({ type: 'step_over', data: {} }); console.log(chalk.blue('Stepping over...')); } async stepOut() { if (!this.activeSession) { this.error('No active debug session'); return; } this.sendDebugMessage({ type: 'step_out', data: {} }); console.log(chalk.blue('Stepping out...')); } async showStack(options = {}) { if (!this.activeSession) { this.error('No active debug session'); return; } if (this.activeSession.callStack.length === 0) { console.log(chalk.yellow('No call stack available')); return; } console.log(chalk.bold('Call Stack:')); for (let i = 0; i < this.activeSession.callStack.length; i++) { const frame = this.activeSession.callStack[i]; const current = i === 0 ? chalk.yellow('→ ') : ' '; console.log(`${current}${i}: ${chalk.cyan(frame.functionName)} ` + `${chalk.gray(`${frame.fileName}:${frame.line}:${frame.column}`)}`); if (options.verbose && frame.variables) { const vars = Object.entries(frame.variables); if (vars.length > 0) { for (const [name, value] of vars) { console.log(` ${name}: ${this.formatValue(value)}`); } } } } } async showVariables(pattern) { if (!this.activeSession) { this.error('No active debug session'); return; } const variables = this.activeSession.variables; let filteredVars = Object.entries(variables); if (pattern) { const regex = new RegExp(pattern, 'i'); filteredVars = filteredVars.filter(([name]) => regex.test(name)); } if (filteredVars.length === 0) { console.log(chalk.yellow(pattern ? 'No matching variables' : 'No variables in scope')); return; } console.log(chalk.bold('Variables:')); for (const [name, value] of filteredVars) { console.log(` ${chalk.cyan(name)}: ${this.formatValue(value)}`); } } async evaluateExpression(expression) { if (!this.activeSession) { this.error('No active debug session'); return; } const spinner = this.startSpinner('Evaluating expression...'); try { // Send evaluation request this.sendDebugMessage({ type: 'evaluate', data: { expression } }); // Wait for response (this is simplified - real implementation would use promises) this.succeedSpinner('Expression evaluated (check debug output)'); } catch (error) { this.failSpinner('Failed to evaluate expression'); throw error; } } async watchVariable(variable) { if (!this.activeSession) { this.error('No active debug session'); return; } this.sendDebugMessage({ type: 'watch_variable', data: { variable } }); console.log(chalk.blue(`Watching variable: ${chalk.cyan(variable)}`)); } async interactiveMode(scriptName) { if (!scriptName && !this.activeSession) { this.error('No active debug session. Specify script name to start new session.'); return; } if (scriptName && !this.activeSession) { await this.startDebugging(scriptName, {}); } if (!this.activeSession) { return; } console.log(chalk.blue('🐛 Entering interactive debug mode')); console.log(chalk.gray('Type "help" for commands, "exit" to leave interactive mode')); let inInteractiveMode = true; while (inInteractiveMode) { try { const { command } = await inquirer.prompt([ { type: 'input', name: 'command', message: chalk.blue('debug>'), prefix: '' } ]); const trimmed = command.trim(); if (!trimmed) continue; if (trimmed === 'exit' || trimmed === 'quit') { inInteractiveMode = false; continue; } if (trimmed === 'help') { this.showInteractiveHelp(); continue; } // Parse and execute debug command await this.executeInteractiveCommand(trimmed); } catch (error) { if (error.message.includes('User force closed')) { inInteractiveMode = false; } else { console.error(chalk.red('Interactive mode error:'), error.message); } } } console.log(chalk.blue('👋 Exited interactive debug mode')); } async executeInteractiveCommand(command) { const parts = command.split(/\s+/); const cmd = parts[0]; const args = parts.slice(1); switch (cmd) { case 'c': case 'continue': await this.continueExecution(); break; case 's': case 'step': await this.stepInto(); break; case 'n': case 'next': await this.stepOver(); break; case 'o': case 'out': await this.stepOut(); break; case 'bp': case 'break': if (args.length > 0) { const line = parseInt(args[0]); const condition = args.slice(1).join(' ') || undefined; await this.setBreakpoint(line, { condition }); } else { console.log(chalk.red('Usage: break <line> [condition]')); } break; case 'clear': if (args.length > 0) { await this.clearBreakpoint(args[0]); } else { console.log(chalk.red('Usage: clear <breakpoint-id>')); } break; case 'ls': case 'list': await this.listBreakpoints(); break; case 'stack': await this.showStack({ verbose: args.includes('-v') }); break; case 'vars': await this.showVariables(args[0]); break; case 'eval': if (args.length > 0) { const expression = args.join(' '); await this.evaluateExpression(expression); } else { console.log(chalk.red('Usage: eval <expression>')); } break; case 'watch': if (args.length > 0) { await this.watchVariable(args[0]); } else { console.log(chalk.red('Usage: watch <variable>')); } break; case 'status': this.showSessionStatus(); break; default: console.log(chalk.red(`Unknown command: ${cmd}. Type "help" for available commands.`)); } } showInteractiveHelp() { console.log(chalk.bold('\nAvailable Commands:')); console.log(chalk.cyan(' c, continue') + ' - Continue execution'); console.log(chalk.cyan(' s, step') + ' - Step into next statement'); console.log(chalk.cyan(' n, next') + ' - Step over next statement'); console.log(chalk.cyan(' o, out') + ' - Step out of current function'); console.log(chalk.cyan(' bp, break <line>') + ' - Set breakpoint'); console.log(chalk.cyan(' clear <id>') + ' - Clear breakpoint'); console.log(chalk.cyan(' ls, list') + ' - List breakpoints'); console.log(chalk.cyan(' stack') + ' - Show call stack'); console.log(chalk.cyan(' vars [pattern]') + ' - Show variables'); console.log(chalk.cyan(' eval <expr>') + ' - Evaluate expression'); console.log(chalk.cyan(' watch <var>') + ' - Watch variable'); console.log(chalk.cyan(' status') + ' - Show session status'); console.log(chalk.cyan(' help') + ' - Show this help'); console.log(chalk.cyan(' exit, quit') + ' - Exit interactive mode'); console.log(); } showSessionStatus() { if (!this.activeSession) { console.log(chalk.red('No active debug session')); return; } console.log(chalk.bold('Debug Session Status:')); console.log(` Script: ${chalk.cyan(this.activeSession.scriptName)}`); console.log(` Session ID: ${chalk.gray(this.activeSession.sessionId)}`); console.log(` Status: ${this.formatStatus(this.activeSession.status)}`); console.log(` Breakpoints: ${this.activeSession.breakpoints.size}`); console.log(` Call Stack Depth: ${this.activeSession.callStack.length}`); const wsStatus = this.getWebSocketStatus(); console.log(` Connection: ${wsStatus}`); } async showHistory(maxEntries) { if (this.debugHistory.length === 0) { console.log(chalk.yellow('No debug history')); return; } const entries = this.debugHistory.slice(-maxEntries); console.log(chalk.bold('Debug History:')); for (const event of entries) { const timestamp = new Date(event.timestamp).toLocaleTimeString(); const type = this.formatEventType(event.type); console.log(`${chalk.gray(timestamp)} ${type} ${this.formatEventData(event.data)}`); } } async connectDebugWebSocket(sessionId) { return new Promise((resolve, reject) => { const url = this.config.getDebuggerUrl() + `/${sessionId}`; const ws = new WebSocket(url); ws.on('open', () => { this.verbose('Debug WebSocket connected'); resolve(ws); }); ws.on('error', (error) => { reject(new Error(`WebSocket connection failed: ${error.message}`)); }); ws.on('message', (data) => { this.handleDebugMessage(JSON.parse(data.toString())); }); ws.on('close', () => { this.verbose('Debug WebSocket disconnected'); }); // Connection timeout setTimeout(() => { if (ws.readyState === WebSocket.CONNECTING) { ws.close(); reject(new Error('WebSocket connection timeout')); } }, 10000); }); } sendDebugMessage(message) { if (!this.activeSession || this.activeSession.websocket.readyState !== WebSocket.OPEN) { this.error('Debug connection not available'); return; } this.activeSession.websocket.send(JSON.stringify(message)); } handleDebugMessage(message) { if (!this.activeSession) return; const event = { type: message.type, data: message.data, timestamp: Date.now() }; this.debugHistory.push(event); switch (message.type) { case 'breakpoint_hit': this.handleBreakpointHit(message.data); break; case 'step_complete': this.handleStepComplete(message.data); break; case 'variable_changed': this.handleVariableChanged(message.data); break; case 'error': this.handleDebugError(message.data); break; case 'execution_complete': this.handleExecutionComplete(message.data); break; } } handleBreakpointHit(data) { if (!this.activeSession) return; this.activeSession.status = 'paused'; console.log(chalk.yellow('\n🔍 Breakpoint hit!')); console.log(chalk.gray(` Line: ${data.line}`)); console.log(chalk.gray(` Function: ${data.functionName || 'global'}`)); if (data.condition) { console.log(chalk.gray(` Condition: ${data.condition}`)); } // Update breakpoint hit count const breakpoint = Array.from(this.activeSession.breakpoints.values()) .find(bp => bp.line === data.line); if (breakpoint) { breakpoint.hitCount++; } // Update call stack and variables if (data.callStack) { this.activeSession.callStack = data.callStack; } if (data.variables) { this.activeSession.variables = data.variables; } } handleStepComplete(data) { console.log(chalk.blue('Step completed')); if (data.line) { console.log(chalk.gray(` Current line: ${data.line}`)); } } handleVariableChanged(data) { console.log(chalk.magenta('Variable changed:')); console.log(` ${chalk.cyan(data.name)}: ${this.formatValue(data.oldValue)} → ${this.formatValue(data.newValue)}`); } handleDebugError(data) { console.log(chalk.red('Debug error:'), data.message); if (data.stack) { console.log(chalk.gray(data.stack)); } } handleExecutionComplete(data) { if (!this.activeSession) return; this.activeSession.status = 'stopped'; console.log(chalk.green('\n✅ Script execution completed')); if (data.result) { console.log('Result:', this.formatValue(data.result)); } if (data.metrics) { console.log(chalk.gray(`Execution time: ${data.metrics.executionTime}ms`)); console.log(chalk.gray(`Instructions: ${data.metrics.instructionsExecuted}`)); } } formatValue(value) { if (value === null) return chalk.gray('null'); if (value === undefined) return chalk.gray('undefined'); if (typeof value === 'string') return chalk.green(`"${value}"`); if (typeof value === 'number') return chalk.yellow(value.toString()); if (typeof value === 'boolean') return chalk.cyan(value.toString()); if (typeof value === 'object') { try { return chalk.white(JSON.stringify(value, null, 2)); } catch { return chalk.gray('[Object]'); } } return String(value); } formatStatus(status) { switch (status) { case 'running': return chalk.green(status); case 'paused': return chalk.yellow(status); case 'stopped': return chalk.red(status); default: return chalk.gray(status); } } getWebSocketStatus() { if (!this.activeSession) { return chalk.red('none'); } switch (this.activeSession.websocket.readyState) { case WebSocket.CONNECTING: return chalk.yellow('connecting'); case WebSocket.OPEN: return chalk.green('connected'); case WebSocket.CLOSING: return chalk.yellow('closing'); case WebSocket.CLOSED: return chalk.red('closed'); default: return chalk.gray('unknown'); } } formatEventType(type) { const colors = { breakpoint_hit: 'yellow', step_complete: 'blue', variable_changed: 'magenta', error: 'red', execution_complete: 'green' }; const color = colors[type] || 'gray'; return chalk[color](type); } formatEventData(data) { if (data.line) return `line ${data.line}`; if (data.name) return data.name; if (data.message) return data.message; return JSON.stringify(data); } } //# sourceMappingURL=DebugCommands.js.map