index.ts•16.8 kB
#!/usr/bin/env node
/**
 * Vibe CLI - Unified Natural Language Interface
 * Uses CENTRALIZED configuration and security systems
 * Leverages existing process-request tool (DRY principle)
 */
import { executeTool } from '../services/routing/toolRegistry.js';
import { ToolExecutionContext } from '../services/routing/toolRegistry.js';
import { OpenRouterConfig } from '../types/workflow.js';
import { CLIConfig } from './types/index.js';
import { EnhancedCLIUtils } from './utils/cli-formatter.js';
import { 
  parseCliArgs, 
  extractRequestArgs, 
  generateSessionId,
  validateEnvironment,
  hasForceFlag
} from './utils/config-loader.js';
import { UnifiedCommandGateway } from './gateway/unified-command-gateway.js';
import { appInitializer } from './core/app-initializer.js';
import { detectCLIMode } from './utils/mode-detector.js';
import ora, { Ora } from 'ora';
import chalk from 'chalk';
import logger from '../logger.js';
/**
 * Gracefully exit the process after flushing logs
 */
async function gracefulExit(code: number = 0): Promise<void> {
  try {
    // Wait for logger to flush
    if (logger && typeof logger.flush === 'function') {
      await new Promise<void>((resolve) => {
        logger.flush(() => resolve());
      });
    }
    // Give a small buffer for final cleanup
    await new Promise(resolve => setTimeout(resolve, 100));
  } catch (error) {
    console.error('Error during graceful exit:', error);
  } finally {
    process.exit(code);
  }
}
/**
 * Main CLI execution function with mode detection
 */
async function main(): Promise<void> {
  const args: string[] = process.argv.slice(2);
  const mode = detectCLIMode(args);
  
  // Handle different modes
  switch (mode) {
    case 'help':
      displayHelp();
      await gracefulExit(0);
      return;
      
    case 'interactive':
      await startInteractiveMode();
      return;
      
    case 'oneshot':
      await processOneShot(args);
      return;
  }
}
/**
 * Start interactive REPL mode
 */
async function startInteractiveMode(): Promise<void> {
  let repl: { start: (config: OpenRouterConfig, resumeSessionId?: string) => Promise<void>; waitForExit: () => Promise<void>; stop: () => void } | null = null;
  
  try {
    // Initialize core services first
    const openRouterConfig = await appInitializer.initializeCoreServices();
    
    // Dynamic import to avoid circular dependencies
    const { VibeInteractiveREPL } = await import('./interactive/repl.js');
    repl = new VibeInteractiveREPL();
    
    // Start REPL with timeout protection
    await repl.start(openRouterConfig);
    
    // Log successful start
    logger.info('REPL started successfully, waiting for user input');
    
    // Wait for REPL to exit with safeguards
    try {
      await repl.waitForExit();
      logger.info('REPL exited normally');
    } catch (waitError) {
      logger.error({ err: waitError }, 'REPL wait error, attempting graceful shutdown');
      
      // Attempt graceful shutdown
      if (repl !== null) {
        repl.stop();
      }
      
      // Give it a moment to clean up
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
    
  } catch (error) {
    logger.error({ err: error }, 'Failed to start interactive mode');
    console.error(chalk.red('Failed to start interactive mode:'), error instanceof Error ? error.message : 'Unknown error');
    
    // Cleanup attempt
    if (repl !== null) {
      try {
        repl.stop();
      } catch (cleanupError) {
        logger.error({ err: cleanupError }, 'Error during REPL cleanup');
      }
    }
    
    await gracefulExit(1);
  }
}
/**
 * Process one-shot command
 */
async function processOneShot(args: string[]): Promise<void> {
  // Parse CLI configuration
  const cliConfig: CLIConfig = parseCliArgs(args);
  const requestArgs: ReadonlyArray<string> = extractRequestArgs(args);
  const forceExecution = hasForceFlag(args);
  
  logger.debug({ 
    args, 
    forceExecution, 
    hasForceInArgs: args.includes('--force') || args.includes('-f') 
  }, 'CLI arguments parsing');
  
  if (requestArgs.length === 0) {
    // No request provided, show interactive prompt hint
    console.log(chalk.cyan('💡 Tip: Run `vibe` without arguments to start interactive mode'));
    console.log();
    displayUsageExample();
    await gracefulExit(0);
    return;
  }
  // Initialize core services BEFORE validation (same as interactive mode)
  try {
    await appInitializer.initializeCoreServices();
  } catch (error) {
    logger.error({ err: error }, 'Failed to initialize core services');
    console.error(chalk.red('Failed to initialize services:'), error instanceof Error ? error.message : 'Unknown error');
    await gracefulExit(1);
    return;
  }
  // Environment validation using centralized systems (AFTER initialization)
  const environmentValidation = await validateEnvironment();
  if (!environmentValidation.valid) {
    EnhancedCLIUtils.formatError('Environment validation failed:');
    environmentValidation.errors.forEach(error => {
      console.error(`  • ${error}`);
    });
    await gracefulExit(1);
    return;
  }
  const request: string = requestArgs.join(' ');
  let spinner: Ora | null = null;
  
  try {
    // Initialize spinner based on CLI config
    if (!cliConfig.quiet) {
      spinner = ora({
        text: 'Processing your request...',
        color: cliConfig.color ? 'cyan' : undefined
      }).start();
    }
    // Initialize configuration using NEW centralized initializer
    const openRouterConfig = await appInitializer.initializeCoreServices();
    
    // Initialize Unified Command Gateway for 95-99% accuracy
    const unifiedGateway = UnifiedCommandGateway.getInstance(openRouterConfig);
    const sessionId = generateSessionId();
    
    // Create unified command context
    const unifiedContext = {
      sessionId,
      userId: undefined,
      currentProject: undefined,
      currentTask: undefined,
      conversationHistory: [],
      userPreferences: {},
      activeWorkflow: undefined,
      workflowStack: [],
      toolHistory: [],
      preferredTools: {}
    };
    // Use UnifiedCommandGateway for enhanced accuracy (DRY compliant)
    const processingResult = await unifiedGateway.processUnifiedCommand(request, unifiedContext);
    
    logger.debug({
      success: processingResult.success,
      requiresConfirmation: processingResult.metadata?.requiresConfirmation,
      forceExecution,
      willExecute: processingResult.success && (!processingResult.metadata?.requiresConfirmation || forceExecution)
    }, 'CLI processing decision');
    
    let result;
    
    // When using --force, bypass UnifiedCommandGateway and use process-request directly for proper parameter extraction
    if (forceExecution) {
      // Force flag - use process-request tool directly for better parameter extraction
      logger.info('Using process-request tool directly with --force flag');
      const context: ToolExecutionContext = {
        sessionId,
        transportType: 'cli',
        metadata: {
          startTime: Date.now(),
          cliVersion: '1.0.0',
          cliConfig: cliConfig,
          forceExecution: true
        }
      };
      
      result = await executeTool(
        'process-request',
        { request },
        openRouterConfig,
        context
      );
    } else if (processingResult.success && !processingResult.metadata?.requiresConfirmation) {
      // High confidence - execute directly using UnifiedCommandGateway
      logger.info({ tool: processingResult.selectedTool }, 'Executing tool with high confidence');
      const executionResult = await unifiedGateway.executeUnifiedCommand(request, unifiedContext);
      result = executionResult.result;
    } else if (processingResult.success && processingResult.metadata?.requiresConfirmation) {
      // Requires confirmation and no force flag - display processing result for user review
      result = {
        content: [{
          type: 'text',
          text: `Command: "${request}"\nSelected Tool: ${processingResult.selectedTool}\nConfidence: ${((processingResult.intent?.confidence || 0) * 100).toFixed(1)}%\n\nValidation:\n${processingResult.validationErrors.join('\n')}\n\nSuggestions:\n${processingResult.suggestions.join('\n')}\n\nUse --force to execute without confirmation.`
        }]
      };
    } else {
      // Processing failed - fallback to existing process-request tool (DRY principle)
      const context: ToolExecutionContext = {
        sessionId,
        transportType: 'cli',
        metadata: {
          startTime: Date.now(),
          cliVersion: '1.0.0',
          cliConfig: cliConfig,
          fallbackReason: 'Unified gateway processing failed'
        }
      };
      result = await executeTool(
        'process-request',
        { request },
        openRouterConfig,
        context
      );
    }
    // Success handling
    if (spinner) {
      spinner.succeed('Request processed successfully!');
    }
    // Format output based on CLI configuration
    await formatAndDisplayResult(result as { content: { [key: string]: unknown; type: string; text?: string | undefined; data?: string | undefined; mimeType?: string | undefined; }[]; }, cliConfig);
    
    await gracefulExit(0);
  } catch (error: unknown) {
    // Error handling with proper typing
    if (spinner) {
      spinner.fail('Request failed');
    }
    await handleCliError(error, cliConfig);
    await gracefulExit(1);
  }
}
/**
 * Format and display result based on CLI configuration
 */
async function formatAndDisplayResult(
  result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string; [key: string]: unknown }> },
  config: CLIConfig
): Promise<void> {
  try {
    if (config.outputFormat === 'json') {
      console.log(JSON.stringify(result, null, 2));
      return;
    }
    if (config.outputFormat === 'yaml') {
      // Simple YAML-like output for CLI results
      console.log('result:');
      console.log('  content:');
      result.content.forEach((item, _index) => {
        console.log(`    - type: ${item.type}`);
        if (item.text) {
          console.log(`      text: |`);
          item.text.split('\n').forEach(line => {
            console.log(`        ${line}`);
          });
        }
        if (item.data) {
          console.log(`      data: ${item.data}`);
        }
        if (item.mimeType) {
          console.log(`      mimeType: ${item.mimeType}`);
        }
      });
      return;
    }
    // Default text format with beautiful styling
    const content = result.content[0];
    if (content && content.type === 'text' && content.text) {
      EnhancedCLIUtils.formatBox(
        content.text,
        '🤖 Vibe Coder Result'
      );
    } else if (content) {
      // Handle non-text content
      console.log(`Content type: ${content.type}`);
      if (content.data) {
        console.log('Data received (use --json for full output)');
      }
    } else {
      console.log('No content returned from request');
    }
  } catch (error) {
    console.error('Error formatting result:', error instanceof Error ? error.message : 'Unknown error');
    // Fallback to simple output
    console.log(JSON.stringify(result, null, 2));
  }
}
/**
 * Handle CLI errors with proper typing and formatting
 */
async function handleCliError(error: unknown, config: CLIConfig): Promise<void> {
  if (error instanceof Error) {
    EnhancedCLIUtils.formatError(error.message);
    
    if (config.verbose && error.stack) {
      console.log();
      console.log(chalk.gray('Stack trace:'));
      console.log(chalk.gray(error.stack));
    }
  } else {
    EnhancedCLIUtils.formatError('An unexpected error occurred');
    
    if (config.verbose) {
      console.log();
      console.log(chalk.gray('Error details:'));
      console.log(chalk.gray(String(error)));
    }
  }
  if (!config.quiet) {
    console.log();
    EnhancedCLIUtils.formatInfo('Try running with --verbose for more details');
  }
}
/**
 * Display comprehensive help information
 */
function displayHelp(): void {
  const helpText = `
${chalk.cyan.bold('🤖 Vibe CLI - Natural Language Development Assistant')}
${chalk.yellow('DESCRIPTION:')}
  Unified command-line interface for all Vibe Coder MCP tools.
  Process natural language requests and route them to the appropriate tool.
${chalk.yellow('USAGE:')}
  ${chalk.green('vibe')}                          ${chalk.gray('Start interactive mode (REPL)')}
  ${chalk.green('vibe')} ${chalk.blue('<request>')} ${chalk.gray('[options]')}   ${chalk.gray('Process one-shot request')}
${chalk.yellow('OPTIONS:')}
  ${chalk.green('-v, --verbose')}     Show detailed output and error traces
  ${chalk.green('-q, --quiet')}       Suppress non-error output  
  ${chalk.green('--format <type>')}   Output format: text, json, yaml (default: text)
  ${chalk.green('--json')}            Shorthand for --format json
  ${chalk.green('--yaml')}            Shorthand for --format yaml
  ${chalk.green('--no-color')}        Disable colored output
  ${chalk.green('-i, --interactive')} Force interactive mode
  ${chalk.green('-h, --help')}        Show this help message
${chalk.yellow('EXAMPLES:')}`;
  const examples = [
    ['vibe "research best practices for React hooks"', 'Research technical topics and best practices'],
    ['vibe "create a PRD for an e-commerce platform"', 'Generate Product Requirements Documents'],
    ['vibe "generate user stories for authentication"', 'Create user stories with acceptance criteria'],
    ['vibe "create task list from user stories"', 'Break down user stories into development tasks'],
    ['vibe "map the codebase structure"', 'Analyze and visualize code architecture'],
    ['vibe "create coding standards for TypeScript"', 'Generate development rules and guidelines'],
    ['vibe "create a React app with Node backend"', 'Generate full-stack project templates'],
    ['vibe "create context for implementing auth"', 'Curate AI-optimized context packages'],
    ['vibe "create project MyApp" --verbose', 'Task management with detailed output'],
    ['vibe "check job status 12345" --json', 'Get job results in JSON format']
  ];
  examples.forEach(([command, description]) => {
    EnhancedCLIUtils.formatExample(command, description);
  });
  const toolsText = `${chalk.yellow('AVAILABLE TOOLS:')}
  ${chalk.cyan('•')} Research Manager - Technical research and analysis
  ${chalk.cyan('•')} PRD Generator - Product requirements documents  
  ${chalk.cyan('•')} User Stories Generator - Agile user stories
  ${chalk.cyan('•')} Task List Generator - Development task lists
  ${chalk.cyan('•')} Rules Generator - Coding standards and guidelines
  ${chalk.cyan('•')} Starter Kit Generator - Full-stack project templates
  ${chalk.cyan('•')} Code Map Generator - Codebase analysis and mapping
  ${chalk.cyan('•')} Context Curator - AI-optimized context packages
  ${chalk.cyan('•')} Vibe Task Manager - Task management and tracking
  ${chalk.cyan('•')} Workflow Runner - Multi-step workflow execution
  ${chalk.cyan('•')} Agent Coordination - Multi-agent task distribution
${chalk.yellow('CONFIGURATION:')}
  Configuration is loaded from centralized systems:
  ${chalk.cyan('•')} OpenRouter API settings from environment variables
  ${chalk.cyan('•')} Security boundaries from unified security config
  ${chalk.cyan('•')} LLM model mappings from llm_config.json
${chalk.yellow('ENVIRONMENT VARIABLES:')}
  ${chalk.green('OPENROUTER_API_KEY')}    Required: Your OpenRouter API key
  ${chalk.green('OPENROUTER_BASE_URL')}   Optional: OpenRouter API base URL
  ${chalk.green('GEMINI_MODEL')}          Optional: Default Gemini model
  ${chalk.green('PERPLEXITY_MODEL')}      Optional: Default Perplexity model
${chalk.gray('For more information, visit: https://github.com/freshtechbro/Vibe-Coder-MCP')}
`;
  console.log(toolsText);
  console.log(helpText);
}
/**
 * Display usage examples for error cases
 */
function displayUsageExample(): void {
  console.log(`${chalk.yellow('Usage:')} ${chalk.green('vibe')} ${chalk.blue('"your natural language request"')}`);
  console.log(`${chalk.yellow('Example:')} ${chalk.green('vibe')} ${chalk.blue('"research best practices for React"')}`);
  console.log(`${chalk.gray('Run')} ${chalk.cyan('vibe --help')} ${chalk.gray('for more information')}`);
}
/**
 * Execute main function with proper error handling
 */
main().catch(async (error: unknown) => {
  console.error(chalk.red('🚨 Fatal error:'));
  if (error instanceof Error) {
    console.error(chalk.red(error.message));
    if (error.stack) {
      console.error(chalk.gray(error.stack));
    }
  } else {
    console.error(chalk.red('Unknown error occurred'));
    console.error(chalk.gray(String(error)));
  }
  await gracefulExit(1);
});