UNPKG

devflow-ai

Version:

Enterprise-grade AI agent orchestration with swarm management UI dashboard

416 lines (348 loc) 11.3 kB
import * as path from 'path'; import { EventEmitter } from 'events'; import { copyPromptsEnhanced, EnhancedPromptCopier } from './prompt-copier-enhanced.js'; import type { CopyOptions, CopyResult } from './prompt-copier.js'; import { PromptConfigManager, PromptPathResolver, PromptValidator, formatDuration, formatFileSize, } from './prompt-utils.js'; import { logger } from '../core/logger.js'; export interface PromptManagerOptions { configPath?: string; basePath?: string; autoDiscovery?: boolean; defaultProfile?: string; } export interface SyncOptions { bidirectional?: boolean; deleteOrphaned?: boolean; compareHashes?: boolean; incrementalOnly?: boolean; } export interface ValidationReport { totalFiles: number; validFiles: number; invalidFiles: number; issues: Array<{ file: string; issues: string[]; metadata?: any; }>; } export class PromptManager extends EventEmitter { private configManager: PromptConfigManager; private pathResolver: PromptPathResolver; private options: Required<PromptManagerOptions>; constructor(options: PromptManagerOptions = {}) { super(); this.options = { configPath: options.configPath || '.prompt-config.json', basePath: options.basePath || process.cwd(), autoDiscovery: options.autoDiscovery ?? true, defaultProfile: options.defaultProfile || 'sparc', }; this.configManager = new PromptConfigManager( path.resolve(this.options.basePath, this.options.configPath), ); this.pathResolver = new PromptPathResolver(this.options.basePath); } async initialize(): Promise<void> { logger.info('Initializing PromptManager...'); // Load configuration await this.configManager.loadConfig(); // Auto-discover prompt directories if enabled if (this.options.autoDiscovery) { const discovered = await this.pathResolver.discoverPromptDirectories(); if (discovered.length > 0) { logger.info(`Auto-discovered ${discovered.length} prompt directories`); // Update config with discovered directories const config = this.configManager.getConfig(); const uniqueDirs = Array.from( new Set([ ...config.sourceDirectories, ...discovered.map((dir) => path.relative(this.options.basePath, dir)), ]), ); await this.configManager.saveConfig({ sourceDirectories: uniqueDirs, }); } } this.emit('initialized'); } async copyPrompts(options: Partial<CopyOptions> = {}): Promise<CopyResult> { const config = this.configManager.getConfig(); const profile = this.options.defaultProfile; // Resolve paths const resolved = this.pathResolver.resolvePaths( config.sourceDirectories, config.destinationDirectory, ); if (resolved.sources.length === 0) { throw new Error('No valid source directories found'); } // Build copy options const copyOptions: CopyOptions = { source: resolved.sources[0], // Use first available source destination: resolved.destination, ...this.configManager.getProfile(profile), ...options, }; logger.info('Starting prompt copy operation', { source: copyOptions.source, destination: copyOptions.destination, profile, }); this.emit('copyStart', copyOptions); try { const result = await (copyOptions.parallel ? copyPromptsEnhanced(copyOptions) : copyPrompts(copyOptions)); this.emit('copyComplete', result); return result; } catch (error) { this.emit('copyError', error); throw error; } } async copyFromMultipleSources(options: Partial<CopyOptions> = {}): Promise<CopyResult[]> { const config = this.configManager.getConfig(); const resolved = this.pathResolver.resolvePaths( config.sourceDirectories, config.destinationDirectory, ); const results: CopyResult[] = []; for (const source of resolved.sources) { try { const copyOptions: CopyOptions = { source, destination: resolved.destination, ...this.configManager.getProfile(this.options.defaultProfile), ...options, }; logger.info(`Copying from source: ${source}`); const result = await copyPrompts(copyOptions); results.push(result); this.emit('sourceComplete', { source, result }); } catch (error) { logger.error(`Failed to copy from ${source}:`, error); this.emit('sourceError', { source, error }); // Add error result results.push({ success: false, totalFiles: 0, copiedFiles: 0, failedFiles: 0, skippedFiles: 0, errors: [ { file: source, error: error instanceof Error ? error.message : String(error), phase: 'read', }, ], duration: 0, }); } } return results; } async validatePrompts(sourcePath?: string): Promise<ValidationReport> { const config = this.configManager.getConfig(); const sources = sourcePath ? [sourcePath] : config.sourceDirectories; const resolved = this.pathResolver.resolvePaths(sources, config.destinationDirectory); let totalFiles = 0; let validFiles = 0; let invalidFiles = 0; const issues: ValidationReport['issues'] = []; for (const source of resolved.sources) { await this.validateDirectory(source, issues); } totalFiles = issues.length; validFiles = issues.filter((issue) => issue.issues.length === 0).length; invalidFiles = totalFiles - validFiles; const report: ValidationReport = { totalFiles, validFiles, invalidFiles, issues: issues.filter((issue) => issue.issues.length > 0), // Only include files with issues }; this.emit('validationComplete', report); return report; } private async validateDirectory( dirPath: string, issues: ValidationReport['issues'], ): Promise<void> { const fs = require('fs').promises; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isFile() && this.isPromptFile(entry.name)) { const result = await PromptValidator.validatePromptFile(fullPath); issues.push({ file: fullPath, issues: result.issues, metadata: result.metadata, }); } else if (entry.isDirectory()) { await this.validateDirectory(fullPath, issues); } } } catch (error) { logger.error(`Failed to validate directory ${dirPath}:`, error); } } private isPromptFile(fileName: string): boolean { const config = this.configManager.getConfig(); const patterns = config.defaultOptions.includePatterns; return patterns.some((pattern) => { const regex = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); return new RegExp(regex).test(fileName); }); } async syncPrompts(options: SyncOptions = {}): Promise<{ forward: CopyResult; backward?: CopyResult; }> { const config = this.configManager.getConfig(); const resolved = this.pathResolver.resolvePaths( config.sourceDirectories, config.destinationDirectory, ); const syncOptions: SyncOptions = { bidirectional: false, deleteOrphaned: false, compareHashes: true, incrementalOnly: true, ...options, }; // Forward sync (source to destination) const forwardResult = await this.performIncrementalSync( resolved.sources[0], resolved.destination, syncOptions, ); let backwardResult: CopyResult | undefined; // Backward sync if bidirectional if (syncOptions.bidirectional) { backwardResult = await this.performIncrementalSync( resolved.destination, resolved.sources[0], syncOptions, ); } return { forward: forwardResult, backward: backwardResult, }; } private async performIncrementalSync( source: string, destination: string, options: SyncOptions, ): Promise<CopyResult> { // This would implement incremental sync logic // For now, we'll use the regular copy with overwrite return copyPrompts({ source, destination, conflictResolution: 'overwrite', verify: options.compareHashes, }); } async generateReport(): Promise<{ configuration: any; sources: Array<{ path: string; exists: boolean; fileCount?: number; totalSize?: number; }>; validation?: ValidationReport; lastOperation?: { type: string; timestamp: Date; result: any; }; }> { const config = this.configManager.getConfig(); const resolved = this.pathResolver.resolvePaths( config.sourceDirectories, config.destinationDirectory, ); // Analyze sources const sources = await Promise.all( resolved.sources.map(async (sourcePath) => { try { const fs = require('fs').promises; const stats = await fs.stat(sourcePath); if (!stats.isDirectory()) { return { path: sourcePath, exists: false }; } // Count files and calculate total size let fileCount = 0; let totalSize = 0; const scanDir = async (dir: string) => { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isFile() && this.isPromptFile(entry.name)) { const fileStats = await fs.stat(fullPath); fileCount++; totalSize += fileStats.size; } else if (entry.isDirectory()) { await scanDir(fullPath); } } }; await scanDir(sourcePath); return { path: sourcePath, exists: true, fileCount, totalSize, }; } catch { return { path: sourcePath, exists: false }; } }), ); return { configuration: config, sources, }; } // Utility methods getConfig() { return this.configManager.getConfig(); } async updateConfig(updates: any): Promise<void> { await this.configManager.saveConfig(updates); } getProfiles(): string[] { return this.configManager.listProfiles(); } getProfile(name: string) { return this.configManager.getProfile(name); } async discoverPromptDirectories(): Promise<string[]> { return this.pathResolver.discoverPromptDirectories(); } } // Export factory function export function createPromptManager(options?: PromptManagerOptions): PromptManager { return new PromptManager(options); } // Export singleton instance let defaultManager: PromptManager | null = null; export function getDefaultPromptManager(): PromptManager { if (!defaultManager) { defaultManager = new PromptManager(); } return defaultManager; }