/** * Analyze Command * Scans PHP files for deprecations and suggests improvements */ import chalk from 'chalk'; import ora from 'ora'; import { writeFile, mkdir } from 'fs/promises'; import { resolve, join, basename } from 'path'; import { scanPhpFiles, extractClassInfo } from '../scanner.js'; import { OllamaClient } from '../ollama.js'; /** * Options for the analyze command */ export interface AnalyzeOptions { phpVersion: string; output: string; model: string; } /** * Execute the analyze command * @param targetPath - Path to analyze * @param options - Command options */ export async function analyzeCommand( targetPath: string, options: AnalyzeOptions ): Promise { const spinner = ora('Initializing...').start(); const startTime = Date.now(); try { // Initialize Ollama client const ollama = new OllamaClient({ model: options.model }); // Check Ollama connection spinner.text = 'Checking Ollama connection...'; const isConnected = await ollama.checkConnection(); if (!isConnected) { spinner.fail(chalk.red('Cannot connect to Ollama')); console.log(chalk.yellow('\nMake sure Ollama is running:')); console.log(chalk.gray(' 1. Install Ollama: https://ollama.ai')); console.log(chalk.gray(' 2. Start server: ollama serve')); console.log(chalk.gray(` 3. Pull model: ollama pull ${options.model}`)); process.exit(1); } // Scan for PHP files spinner.text = 'Scanning PHP files...'; const files = await scanPhpFiles(targetPath); if (files.length === 0) { spinner.fail(chalk.red('No PHP files found')); process.exit(1); } spinner.succeed(chalk.green(`Found ${files.length} PHP file(s)`)); // Ensure output directory exists const outputDir = resolve(options.output); await mkdir(outputDir, { recursive: true }); const generated: string[] = []; // Analyze each file for (let i = 0; i < files.length; i++) { const file = files[i]; const fileSpinner = ora( `Analyzing ${file.name} (${i + 1}/${files.length})...` ).start(); try { const analysis = await ollama.analyzeCode(file.content, options.phpVersion); // Get class name or use file name const classInfo = extractClassInfo(file.content); const reportName = classInfo.className || basename(file.name, '.php'); const reportFileName = `${reportName}.md`; const reportPath = join(outputDir, reportFileName); // Validate that the AI followed the format const hasTable = analysis.includes('| Line') || analysis.includes('|---') || analysis.includes('|'); const hasPython = analysis.includes('[PYTHON]') || analysis.includes('```python') || analysis.includes('def __init__(self'); // Build individual report let report = `# Analysis: ${reportName}\n\n`; report += `**File:** \`${file.name}\`\n`; report += `**Path:** \`${file.path}\`\n`; report += `**Lines:** ${file.lines}\n`; report += `**Target PHP:** ${options.phpVersion}\n`; report += `**Generated:** ${new Date().toISOString()}\n\n`; report += `---\n\n`; if (hasPython) { fileSpinner.fail(chalk.red(`${file.name} - model converted PHP to Python!`)); console.log(chalk.red('\n✗ The model incorrectly converted PHP code to Python.')); console.log(chalk.cyan('Try a different model:\n')); console.log(chalk.white(' ollama pull qwen2.5-coder')); console.log(chalk.white(` npx php-refactor analyze ${targetPath} --php-version ${options.phpVersion} --model qwen2.5-coder\n`)); report += `**Error:** Model converted PHP to Python. Try a different model.\n`; } else if (!hasTable) { report += analysis; fileSpinner.warn(chalk.yellow(`Analyzed ${file.name} - model generated description instead of analysis`)); console.log(chalk.yellow('\n⚠ The model did not follow the expected format.')); console.log(chalk.cyan('Try using a code-specific model:\n')); console.log(chalk.gray(' # Install codellama (if not already installed)')); console.log(chalk.white(' ollama pull codellama\n')); console.log(chalk.gray(' # Re-run with codellama')); console.log(chalk.white(` npx php-refactor analyze ${targetPath} --php-version ${options.phpVersion} --model codellama\n`)); } else { report += analysis; fileSpinner.succeed(chalk.green(`Analyzed ${file.name} → ${reportFileName}`)); } // Write individual report await writeFile(reportPath, report, 'utf-8'); generated.push(reportFileName); } catch (error) { const err = error as Error; fileSpinner.fail(chalk.red(`Failed to analyze ${file.name}: ${err.message}`)); } } const elapsed = Date.now() - startTime; const elapsedStr = formatDuration(elapsed); console.log(chalk.green(`\n✓ Reports saved to: ${outputDir}/`)); console.log(chalk.cyan('\nGenerated Reports:')); for (const name of generated) { console.log(chalk.gray(` • ${name}`)); } console.log(chalk.cyan('\nSummary:')); console.log(chalk.gray(` • Files analyzed: ${files.length}`)); console.log(chalk.gray(` • Target PHP version: ${options.phpVersion}`)); console.log(chalk.gray(` • AI model: ${options.model}`)); console.log(chalk.gray(` • Time elapsed: ${elapsedStr}`)); } catch (error) { const err = error as Error; spinner.fail(chalk.red(`Error: ${err.message}`)); process.exit(1); } } /** * Format duration in milliseconds to human readable string * @param ms - Duration in milliseconds * @returns Formatted string (e.g., "1m 23s" or "45s") */ function formatDuration(ms: number): string { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes > 0) { return `${minutes}m ${remainingSeconds}s`; } return `${seconds}s`; }