import * as plugins from './mod.plugins.js'; import { FormatContext } from './classes.formatcontext.js'; import { BaseFormatter } from './classes.baseformatter.js'; import type { IFormatPlan, IPlannedChange, IFormatWarning, } from './interfaces.format.js'; import { getModuleIcon } from './interfaces.format.js'; import { logger } from '../gitzone.logging.js'; import { DiffReporter } from './classes.diffreporter.js'; export class FormatPlanner { private plannedChanges: Map = new Map(); private diffReporter = new DiffReporter(); async planFormat(modules: BaseFormatter[]): Promise { const plan: IFormatPlan = { summary: { totalFiles: 0, filesAdded: 0, filesModified: 0, filesRemoved: 0, }, changes: [], warnings: [], }; for (const module of modules) { try { const changes = await module.analyze(); this.plannedChanges.set(module.name, changes); for (const change of changes) { plan.changes.push(change); switch (change.type) { case 'create': plan.summary.filesAdded++; break; case 'modify': plan.summary.filesModified++; break; case 'delete': plan.summary.filesRemoved++; break; } } const warnings = await module.validate(); plan.warnings.push(...warnings); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); plan.warnings.push({ level: 'error', message: `Failed to analyze module ${module.name}: ${errorMessage}`, module: module.name, }); } } plan.warnings.push(...this.detectConflictingChanges(plan.changes)); plan.summary.totalFiles = plan.summary.filesAdded + plan.summary.filesModified + plan.summary.filesRemoved; return plan; } async executePlan( plan: IFormatPlan, modules: BaseFormatter[], context: FormatContext, ): Promise { const startTime = Date.now(); const changesByModule = this.groupChangesByModule(plan.changes); for (const module of modules) { const changes = changesByModule.get(module.name) || []; if (changes.length > 0 || module.runsWithoutChanges) { logger.log('info', `Executing ${module.name} formatter...`); await module.execute(changes); } } const duration = Date.now() - startTime; logger.log('info', `Format operations completed in ${duration}ms`); } async displayPlan( plan: IFormatPlan, detailed: boolean = false, ): Promise { console.log('\nFormat Plan:'); console.log('━'.repeat(50)); console.log(`Summary: ${plan.summary.totalFiles} files will be changed`); console.log(` • ${plan.summary.filesAdded} new files`); console.log(` • ${plan.summary.filesModified} modified files`); console.log(` • ${plan.summary.filesRemoved} deleted files`); console.log(''); console.log('Changes by module:'); const changesByModule = new Map(); for (const change of plan.changes) { const moduleChanges = changesByModule.get(change.module) || []; moduleChanges.push(change); changesByModule.set(change.module, moduleChanges); } for (const [module, changes] of changesByModule) { console.log( `\n${getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`, ); for (const change of changes) { const icon = this.getChangeIcon(change.type); console.log(` ${icon} ${change.path} - ${change.description}`); if (detailed && change.type === 'modify') { const diff = await this.diffReporter.generateDiffForChange(change); if (diff) { this.diffReporter.displayDiff(change.path, diff); } } } } if (plan.warnings.length > 0) { console.log('\nWarnings:'); for (const warning of plan.warnings) { const icon = warning.level === 'error' ? '❌' : '⚠️'; console.log(` ${icon} ${warning.message}`); } } console.log('\n' + '━'.repeat(50)); } private getChangeIcon(type: 'create' | 'modify' | 'delete'): string { switch (type) { case 'create': return '✅'; case 'modify': return '✏️'; case 'delete': return '❌'; } } private groupChangesByModule( changes: IPlannedChange[], ): Map { const changesByModule = new Map(); for (const change of changes) { const moduleChanges = changesByModule.get(change.module) || []; moduleChanges.push(change); changesByModule.set(change.module, moduleChanges); } return changesByModule; } private detectConflictingChanges( changes: IPlannedChange[], ): IFormatWarning[] { const warnings: IFormatWarning[] = []; const changesByPath = new Map(); for (const change of changes) { if (!change.path || change.path === '') { continue; } const pathChanges = changesByPath.get(change.path) || []; pathChanges.push(change); changesByPath.set(change.path, pathChanges); } for (const [path, pathChanges] of changesByPath) { const modules = [...new Set(pathChanges.map((change) => change.module))]; if (modules.length < 2) { continue; } const hasDelete = pathChanges.some((change) => change.type === 'delete'); const plannedContents = pathChanges .map((change) => change.content) .filter((content): content is string => content !== undefined); const uniqueContents = new Set(plannedContents); const level = hasDelete || uniqueContents.size > 1 ? 'warning' : 'info'; warnings.push({ level, module: 'planner', message: `Multiple formatters plan changes for ${path}: ${modules.join(', ')}. They will run in formatter order.`, }); } return warnings; } }