codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
454 lines (393 loc) ⢠16.4 kB
text/typescript
/**
* CLI Command Handlers
* Handles the execution of specific CLI commands
*/
import chalk from 'chalk';
import ora from 'ora';
import { readFile, stat } from 'fs/promises';
import { join, extname, isAbsolute } from 'path';
import { glob } from 'glob';
import { logger } from '../logger.js';
import { CLIOptions, CLIContext } from './cli-types.js';
// import { CLIDisplay } from './cli-display.js';
// import { ProjectContext } from '../client.js';
import { ServerModeInterface } from '../../refactor/server-mode-interface.js';
import { analysisWorkerPool, AnalysisTask } from '../workers/analysis-worker.js';
import { randomUUID } from 'crypto';
export class CLICommands {
private context: CLIContext;
private workingDirectory: string;
constructor(context: CLIContext, workingDirectory: string = process.cwd()) {
this.context = context;
this.workingDirectory = workingDirectory;
}
/**
* Show system status including model connections and health
*/
async showStatus(): Promise<void> {
console.log(chalk.bold('\nš CodeCrucible Synth - System Status\n'));
// Model Client Status
console.log(chalk.cyan('š¤ Model Client:'));
try {
if (this.context.modelClient) {
logger.debug('About to call healthCheck');
const healthCheck = await this.context.modelClient.healthCheck();
logger.debug('HealthCheck completed', { healthCheck });
console.log(chalk.green(` ā
Status: ${healthCheck ? 'Connected' : 'Disconnected'}`));
if (typeof (this.context.modelClient as any).getCurrentModel === 'function') {
const currentModel = (this.context.modelClient as any).getCurrentModel();
console.log(chalk.cyan(` šÆ Current Model: ${currentModel || 'Auto-detect'}`));
}
} else {
console.log(chalk.red(' ā Model client not initialized'));
}
} catch (error) {
console.log(
chalk.red(` ā Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
);
}
// Voice System Status
console.log(chalk.cyan('\nš Voice System:'));
try {
if (this.context.voiceSystem) {
const voices = this.context.voiceSystem.getAvailableVoices();
console.log(chalk.green(` ā
Available Voices: ${voices.length}`));
console.log(chalk.cyan(` šÆ Voice Names: ${voices.map(v => v.name).join(', ')}`));
} else {
console.log(chalk.red(' ā Voice system not initialized'));
}
} catch (error) {
console.log(
chalk.red(` ā Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
);
}
// MCP Server Status
console.log(chalk.cyan('\nš§ MCP Servers:'));
try {
if (this.context.mcpManager) {
const serverCount = (this.context.mcpManager as any).servers?.size || 0;
console.log(chalk.green(` ā
Active Servers: ${serverCount}`));
if (typeof (this.context.mcpManager as any).isReady === 'function') {
const ready = (this.context.mcpManager as any).isReady();
console.log(chalk.cyan(` š Ready: ${ready ? 'Yes' : 'No'}`));
}
} else {
console.log(chalk.red(' ā MCP manager not initialized'));
}
} catch (error) {
console.log(
chalk.red(` ā Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
);
}
// Configuration Status
console.log(chalk.cyan('\nāļø Configuration:'));
try {
if (this.context.config) {
console.log(chalk.green(' ā
Configuration loaded'));
if (this.context.config.model) {
console.log(
chalk.cyan(` š Endpoint: ${this.context.config.model.endpoint || 'Default'}`)
);
console.log(
chalk.cyan(` ā±ļø Timeout: ${this.context.config.model.timeout || 'Default'}ms`)
);
}
} else {
console.log(chalk.red(' ā Configuration not loaded'));
}
} catch (error) {
console.log(
chalk.red(` ā Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
);
}
console.log(chalk.green('\n⨠System check complete!\n'));
}
/**
* List available AI models
*/
async listModels(): Promise<void> {
console.log(chalk.bold('\nš§ Available AI Models\n'));
const spinner = ora('Fetching available models...').start();
try {
if (!this.context.modelClient) {
spinner.fail('Model client not available');
console.log(chalk.red('ā Model client is not initialized'));
return;
}
// Check if getAllAvailableModels method exists
if (typeof this.context.modelClient.getAllAvailableModels === 'function') {
const models = await this.context.modelClient.getAllAvailableModels();
spinner.succeed(`Found ${models.length} models`);
if (models.length > 0) {
console.log(chalk.cyan('š Available Models:'));
models.forEach((model, index) => {
console.log(chalk.white(` ${index + 1}. ${model.name || model.id || model}`));
if (model.size) {
console.log(chalk.gray(` Size: ${model.size}`));
}
if (model.modified_at) {
console.log(
chalk.gray(` Modified: ${new Date(model.modified_at).toLocaleDateString()}`)
);
}
});
} else {
console.log(chalk.yellow('No models found. Make sure your AI service is running.'));
}
} else {
spinner.warn('Model listing not supported');
console.log(chalk.yellow('ā¹ļø Model listing is not supported by the current client'));
}
} catch (error) {
spinner.fail('Failed to fetch models');
console.error(chalk.red('ā Error fetching models:'), error);
}
console.log('');
}
/**
* Handle code generation requests
*/
async handleGeneration(prompt: string, options: CLIOptions = {}): Promise<void> {
console.log(chalk.bold(`\nšØ Generating Code\n`));
console.log(chalk.cyan(`Prompt: ${prompt}`));
const spinner = ora('Generating code...').start();
try {
if (!this.context.modelClient || !this.context.voiceSystem) {
spinner.fail('Required services not available');
console.log(chalk.red('ā Model client or voice system not initialized'));
return;
}
// Get voices for code generation
const voices = options.voices || this.context.voiceSystem.getDefaultVoices() || ['developer'];
// Generate multi-voice solutions
const results = await this.context.voiceSystem.generateMultiVoiceSolutions(
Array.isArray(voices) ? voices : [voices],
prompt,
{ files: [] }
);
spinner.succeed('Code generation complete');
if (results && results.length > 0) {
console.log(chalk.green('\n⨠Generated Solutions:\n'));
results.forEach((result: any, index: number) => {
console.log(chalk.cyan(`\n${index + 1}. ${result.voice || 'Voice'} Solution:`));
console.log(chalk.white(result.content || 'No content generated'));
if (result.confidence) {
console.log(chalk.gray(` Confidence: ${Math.round(result.confidence * 100)}%`));
}
});
} else {
console.log(chalk.yellow('No solutions generated'));
}
} catch (error) {
spinner.fail('Code generation failed');
console.error(chalk.red('ā Error during generation:'), error);
}
console.log('');
}
/**
* Handle file and directory analysis
*/
async handleAnalyze(files: string[] = [], options: CLIOptions = {}): Promise<void> {
if (files.length === 0) {
// Use our real codebase analysis for directory analysis
console.log(chalk.cyan('š Performing comprehensive codebase analysis...'));
try {
logger.debug('About to import CodebaseAnalyzer');
const { CodebaseAnalyzer } = await import('../analysis/codebase-analyzer.js');
logger.debug('CodebaseAnalyzer imported successfully');
const analyzer = new CodebaseAnalyzer(this.workingDirectory);
logger.debug('Analyzer created, starting analysis');
const analysis = await analyzer.performAnalysis();
logger.debug('Analysis completed');
console.log(analysis);
} catch (error) {
console.error(chalk.red('ā Analysis failed:'), error);
logger.debug('Falling back to original analysis');
// For now, just show a message instead of hanging
console.log(
chalk.yellow('š§ Real-time analysis temporarily unavailable. Using fallback method.')
);
console.log(chalk.cyan('ā¹ļø The comprehensive analysis feature is being refined.'));
}
} else {
// Analyze specified files using original logic
for (const file of files) {
const fullPath = isAbsolute(file) ? file : join(this.workingDirectory, file);
try {
const stats = await stat(fullPath);
if (stats.isDirectory()) {
await this.analyzeDirectory(fullPath, options);
} else {
await this.analyzeFile(fullPath, options);
}
} catch (error) {
console.error(chalk.red(`ā Cannot access ${file}:`), error);
}
}
}
}
/**
* Start server mode
*/
async startServer(options: CLIOptions, serverMode: ServerModeInterface): Promise<void> {
const port = parseInt(options.port || '3002', 10);
console.log(chalk.cyan(`\nš Starting CodeCrucible Server on port ${port}...`));
const serverOptions: any = {
port,
host: 'localhost',
cors: true,
};
try {
await serverMode.startServerMode(this.context, serverOptions);
console.log(chalk.green(`ā
Server running at http://localhost:${port}`));
} catch (error) {
console.error(chalk.red('ā Failed to start server:'), error);
process.exit(1);
}
}
/**
* Analyze a directory structure with worker pool
*/
private async analyzeDirectory(dirPath: string, options: CLIOptions): Promise<void> {
console.log(chalk.bold(`\nš Analyzing Directory: ${dirPath}`));
const spinner = ora('Scanning directory...').start();
try {
// Get file patterns for analysis
const patterns = [
'**/*.{ts,js,tsx,jsx,py,java,cpp,hpp,c,h,rs,go,php,rb,swift,kt}',
'**/package.json',
'**/tsconfig.json',
'**/README.md',
];
const allFiles: string[] = [];
for (const pattern of patterns) {
const files = await glob(pattern, {
cwd: dirPath,
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
});
allFiles.push(...files.map(f => join(dirPath, f)));
}
spinner.text = `Found ${allFiles.length} files, analyzing...`;
// PERFORMANCE FIX: Use worker pool for non-blocking analysis
if (this.context.voiceSystem && this.context.modelClient) {
const voices = options.voices || ['analyzer', 'architect'];
// Create analysis task for worker pool
const analysisTask: AnalysisTask = {
id: randomUUID(),
files: allFiles,
prompt:
(options.prompt as string) ||
'Analyze this codebase for architecture, quality, and potential improvements.',
options: {
voices,
maxFiles: 50, // Limit files to prevent memory overload
},
timeout: options.timeout || 300000, // 5 minutes timeout for complex analysis
};
// Execute analysis in worker thread
try {
const result = await analysisWorkerPool.executeAnalysis(analysisTask, {
endpoint: this.context.config.model?.endpoint || 'http://localhost:11434',
providers: [{ type: 'ollama' as const }],
executionMode: 'auto' as const,
fallbackChain: ['ollama' as const],
performanceThresholds: {
fastModeMaxTokens: 2048,
timeoutMs: 300000, // 5 minutes for complex operations
maxConcurrentRequests: 2,
},
security: {
enableSandbox: true,
maxInputLength: 100000,
allowedCommands: ['node', 'npm', 'git'],
},
});
spinner.succeed(`Analysis complete - processed ${result.result?.totalFiles || 0} files`);
// Display results
if (result.success && result.result) {
console.log(chalk.green('\nā
Analysis Results:'));
console.log(chalk.cyan(`š Files processed: ${result.result.totalFiles}`));
console.log(chalk.cyan(`ā±ļø Duration: ${result.duration}ms`));
console.log(chalk.cyan(`š Success rate: ${result.result.summary?.successRate || 0}%`));
// Display chunk results
result.result.chunks.forEach((chunk: any, index: number) => {
console.log(chalk.yellow(`\nš¦ Chunk ${index + 1}:`));
if (chunk.error) {
console.log(chalk.red(`ā Error: ${chunk.error}`));
} else if (chunk.analysis) {
console.log(chalk.green(`ā
Successfully analyzed ${chunk.files.length} files`));
if (chunk.analysis.length > 0) {
console.log(chalk.white(chunk.analysis[0].content.substring(0, 200) + '...'));
}
}
});
} else {
console.log(chalk.red(`ā Analysis failed: ${result.error || 'Unknown error'}`));
}
} catch (analysisError) {
spinner.fail('Analysis failed');
console.error(chalk.red('Error during analysis:'), analysisError);
}
} else {
spinner.succeed('Basic directory scan complete');
console.log(chalk.cyan(`\nš Summary:`));
console.log(` Files found: ${allFiles.length}`);
console.log(` Types: ${this.getFileTypes(allFiles)}`);
}
} catch (error) {
spinner.fail('Directory analysis failed');
console.error(chalk.red('Error:'), error);
}
}
/**
* Analyze a specific file
*/
private async analyzeFile(filePath: string, options: CLIOptions): Promise<void> {
console.log(chalk.bold(`\nš Analyzing File: ${filePath}`));
const spinner = ora('Reading file...').start();
try {
const content = await readFile(filePath, 'utf-8');
const stats = await stat(filePath);
spinner.text = 'Analyzing content...';
console.log(chalk.cyan('\nš File Analysis:'));
console.log(` Size: ${(stats.size / 1024).toFixed(2)} KB`);
console.log(` Lines: ${content.split('\n').length}`);
console.log(` Type: ${extname(filePath)}`);
if (this.context.voiceSystem && this.context.modelClient) {
const voices = options.voices || ['analyzer'];
const analysis = await this.context.voiceSystem.generateMultiVoiceSolutions(
Array.isArray(voices) ? voices : [voices],
`Analyze this file: ${filePath}\n\nContent:\n${content.substring(0, 2000)}${content.length > 2000 ? '...' : ''}`,
{ files: [filePath] }
);
spinner.succeed('File analysis complete');
if (analysis && analysis.length > 0) {
analysis.forEach((result: any, index: number) => {
console.log(chalk.cyan(`\n${index + 1}. ${result.voice || 'Voice'} Analysis:`));
console.log(result.content || 'No analysis available');
});
} else {
console.log(chalk.yellow('No analysis results available'));
}
} else {
spinner.succeed('Basic file scan complete');
}
} catch (error) {
spinner.fail('File analysis failed');
console.error(chalk.red('Error:'), error);
}
}
/**
* Get file type summary
*/
private getFileTypes(files: string[]): string {
const types = new Map<string, number>();
files.forEach(file => {
const ext = extname(file) || 'no extension';
types.set(ext, (types.get(ext) || 0) + 1);
});
return Array.from(types.entries())
.map(([ext, count]) => `${ext}(${count})`)
.join(', ');
}
}