#!/usr/bin/env node
/**
* Watchtower CLI - Command Line Interface
* Windows-native MCP ⇄ DAP bridge for C#, Node/TS, Python, and Dart
*/
import { Command } from 'commander';
import { MCPServer } from './server/server';
import { Logger } from './server/logger';
import { ConfigManager } from './server/config';
import { MetricsCollector } from './server/metrics';
import { version } from '../package.json';
const program = new Command();
const logger = new Logger('CLI');
program.name('watchtower').description('Windows-native MCP ⇄ DAP bridge').version(version);
/**
* Serve command - Start the MCP server
*/
program
.command('serve')
.description('Start the MCP server')
.option('-p, --port <port>', 'Port to bind to (0 for random)')
.option('-h, --host <host>', 'Host to bind to', '127.0.0.1')
.option('-v, --verbose', 'Enable verbose logging')
.option('--log-level <level>', 'Set log level (debug, info, warn, error)', 'info')
.action(async options => {
try {
// Set log level
if (options.verbose) {
process.env['LOG_LEVEL'] = 'debug';
} else {
process.env['LOG_LEVEL'] = options.logLevel;
}
// Update config if port/host provided
const config = ConfigManager.getInstance();
if (options.port) {
config.set('server.port', parseInt(options.port, 10));
}
if (options.host) {
config.set('server.host', options.host);
}
// Validate configuration
const validation = config.validate();
if (!validation.valid) {
logger.error('Configuration validation failed:', validation.errors.join(', '));
process.exit(1);
}
logger.info(`Starting Watchtower MCP Server v${version}`);
// Create and start server
const server = new MCPServer();
await server.start();
// Handle signals
process.on('SIGTERM', async () => {
logger.info('Received SIGTERM, shutting down...');
await server.stop();
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('Received SIGINT, shutting down...');
await server.stop();
process.exit(0);
});
// Keep process alive
setInterval(() => {
const status = server.getStatus();
logger.debug('Server status:', status);
}, 30000);
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
});
/**
* Config command - Manage configuration
*/
program
.command('config')
.description('Manage Watchtower configuration')
.option('--show', 'Show current configuration')
.option('--validate', 'Validate configuration')
.option('--set <key=value>', 'Set configuration value')
.option('--get <key>', 'Get configuration value')
.option('--reset', 'Reset configuration to defaults')
.action(async options => {
const config = ConfigManager.getInstance();
if (options.show) {
console.log(JSON.stringify(config.getAll(), null, 2));
}
if (options.validate) {
const validation = config.validate();
if (validation.valid) {
console.log('✅ Configuration is valid');
} else {
console.error('❌ Configuration validation failed:');
validation.errors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}
}
if (options.set) {
const [key, value] = options.set.split('=');
if (key && value) {
// Try to parse as JSON, fall back to string
let parsedValue;
try {
parsedValue = JSON.parse(value);
} catch {
parsedValue = value;
}
config.set(key, parsedValue);
console.log(`✅ Set ${key} = ${JSON.stringify(parsedValue)}`);
} else {
console.error('❌ Invalid format. Use: --set key=value');
process.exit(1);
}
}
if (options.get) {
const value = config.get(options.get);
if (value !== undefined) {
console.log(JSON.stringify(value, null, 2));
} else {
console.error(`❌ Configuration key "${options.get}" not found`);
process.exit(1);
}
}
if (options.reset) {
// This would require reloading the config with defaults
console.log('ℹ️ Please delete watchtower.config.json to reset to defaults');
}
});
/**
* Doctor command - System diagnostics
*/
program
.command('doctor')
.description('Run system diagnostics')
.option('--json', 'Output in JSON format')
.action(async options => {
const results: any = {
timestamp: new Date().toISOString(),
status: 'healthy',
checks: [],
};
// Check Node.js version
const nodeVersion = process.version;
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0] || '0', 10);
const nodeCheck = {
name: 'Node.js version',
status: nodeMajor >= 22 ? 'pass' : 'fail',
message:
nodeMajor >= 22
? `✅ Node.js ${nodeVersion} is supported`
: `❌ Node.js ${nodeVersion} requires v22+`,
details: { version: nodeVersion, required: '>=22' },
};
results.checks.push(nodeCheck);
// Check Windows
const isWindows = process.platform === 'win32';
const windowsCheck = {
name: 'Platform',
status: isWindows ? 'pass' : 'fail',
message: isWindows
? '✅ Running on Windows'
: '❌ Windows required - this is a Windows-native tool',
details: { platform: process.platform },
};
results.checks.push(windowsCheck);
// Check PATH for debug adapters
const pathCheck = await checkDebugAdapters();
results.checks.push(pathCheck);
// Check configuration
const config = ConfigManager.getInstance();
const validation = config.validate();
const configCheck = {
name: 'Configuration',
status: validation.valid ? 'pass' : 'fail',
message: validation.valid ? '✅ Configuration is valid' : '❌ Configuration errors',
details: validation.valid ? {} : { errors: validation.errors },
};
results.checks.push(configCheck);
// Overall status
const failedChecks = results.checks.filter((check: any) => check.status === 'fail');
results.status = failedChecks.length === 0 ? 'healthy' : 'unhealthy';
results.summary = {
total: results.checks.length,
passed: results.checks.length - failedChecks.length,
failed: failedChecks.length,
};
if (options.json) {
console.log(JSON.stringify(results, null, 2));
} else {
console.log('🏥 Watchtower System Diagnostics');
console.log('='.repeat(50));
console.log(`📊 Overall Status: ${results.status.toUpperCase()}`);
console.log(
`\n✅ Passed: ${results.summary.passed} | ❌ Failed: ${results.summary.failed} | 📋 Total: ${results.summary.total}\n`
);
results.checks.forEach((check: any) => {
const icon = check.status === 'pass' ? '✅' : '❌';
console.log(`${icon} ${check.name}: ${check.message}`);
if (check.details && Object.keys(check.details).length > 0) {
console.log(` Details: ${JSON.stringify(check.details, null, 2)}`);
}
});
if (failedChecks.length > 0) {
console.log('\n🔧 To fix issues:');
failedChecks.forEach((check: any) => {
if (check.name === 'PATH for debug adapters') {
console.log(' - Install missing debug adapters');
console.log(' - Add them to your PATH or specify full paths in config');
} else if (check.name === 'Configuration') {
check.details.errors?.forEach((error: string) => {
console.log(` - Fix configuration: ${error}`);
});
}
});
}
}
});
/**
* Metrics command - View metrics
*/
program
.command('metrics')
.description('View system metrics')
.option('--json', 'Output in JSON format')
.option('--watch', 'Watch metrics continuously')
.action(async options => {
const metrics = MetricsCollector.getInstance();
const showMetrics = () => {
const exported = metrics.export();
if (options.json) {
console.log(JSON.stringify(exported, null, 2));
} else {
console.log('📊 Watchtower Metrics');
console.log('='.repeat(50));
// Performance targets
if (exported['performance.targets']) {
const targets = exported['performance.targets'];
console.log('\n🎯 Performance Targets:');
console.log(
` TFFB: ${targets.tffb.current}ms (target: ${targets.tffb.target}ms) [${targets.tffb.status}]`
);
console.log(
` Step p95: ${targets.stepP95.current}ms (target: ${targets.stepP95.target}ms) [${targets.stepP95.status}]`
);
}
// Key metrics
const keyMetrics = [
'dap.start.count',
'dap.step.count',
'sessions.active',
'events.buffer.size',
];
console.log('\n📈 Key Metrics:');
keyMetrics.forEach(metric => {
const data = exported[metric];
if (data) {
console.log(` ${metric}: ${data.current}`);
}
});
}
};
if (options.watch) {
showMetrics();
setInterval(showMetrics, 5000);
} else {
showMetrics();
}
});
/**
* Version command
*/
program
.command('version')
.description('Show version information')
.action(() => {
console.log(`Watchtower MCP Server v${version}`);
console.log('Windows-native MCP ⇄ DAP bridge');
console.log('Supports C#, Node/TS, Python, and Dart debugging');
});
/**
* Helper function to check debug adapters in PATH
*/
async function checkDebugAdapters(): Promise<any> {
const { execa } = await import('execa');
const adapters = ['vsdbg.exe', 'netcoredbg.exe', 'debugpy.exe'];
const found = [];
const missing = [];
for (const adapter of adapters) {
try {
await execa('where', [adapter]);
found.push(adapter);
} catch {
missing.push(adapter);
}
}
// Check for VS Code extensions directory
const vscodeExtPath = process.env['USERPROFILE']
? `${process.env['USERPROFILE']}\\.vscode\\extensions`
: '';
let jsDebugFound = false;
if (vscodeExtPath) {
try {
const fs = await import('fs');
if (fs.existsSync(vscodeExtPath)) {
const extensions = fs.readdirSync(vscodeExtPath);
jsDebugFound = extensions.some(ext => ext.startsWith('ms-vscode.js-debug'));
}
} catch {
// ignore
}
}
if (jsDebugFound) {
found.push('vscode-js-debug');
} else {
missing.push('vscode-js-debug');
}
return {
name: 'PATH for debug adapters',
status: missing.length === 0 ? 'pass' : 'fail',
message:
missing.length === 0
? `✅ All ${found.length} debug adapters found in PATH`
: `❌ Missing ${missing.length} debug adapters: ${missing.join(', ')}`,
details: {
found,
missing,
suggestions: missing.map(adapter => {
switch (adapter) {
case 'vsdbg.exe':
return 'Install Visual Studio with "Desktop development with C#" workload';
case 'netcoredbg.exe':
return 'Install .NET SDK: https://dotnet.microsoft.com/download';
case 'debugpy.exe':
return 'Install Python and run: pip install debugpy';
case 'vscode-js-debug':
return 'Install VS Code with JavaScript Debugger extension';
default:
return `Install ${adapter}`;
}
}),
},
};
}
// Handle unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled rejection at:', { promise, reason });
process.exit(1);
});
process.on('uncaughtException', error => {
logger.error('Uncaught exception:', error);
process.exit(1);
});
// Parse command line arguments
program.parse();