UNPKG

@oliverpople/agency-x

Version:

🚀 **Transform feature requests into production-ready code in seconds**

274 lines (234 loc) • 7.77 kB
import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; // Define interfaces for type safety interface AgentOutput { output?: any; completed: boolean; error?: string; executionTime?: number; retryCount?: number; startTime?: number; } interface Context { specId: string; featurePrompt: string; spec: any; agents: Record<string, AgentOutput>; finalBundle: any; log: string[]; metadata: { createdAt: string; lastUpdated: string; version: string; }; } let currentContext: Context | null = null; const sessionsDir = path.join(process.cwd(), 'sessions'); const backupDir = path.join(sessionsDir, 'backups'); // Ensure directories exist const ensureDirectories = async () => { await fs.mkdir(sessionsDir, { recursive: true }); await fs.mkdir(backupDir, { recursive: true }); }; const generateSpecId = () => { const date = new Date(); const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); const randomId = Math.random().toString(36).substring(2, 6).toUpperCase(); return `SPEC-${year}${month}${day}-${randomId}`; }; const validateContext = (context: any): Context => { if (!context.specId || typeof context.specId !== 'string') { throw new Error('Invalid specId in context'); } if (!context.featurePrompt || typeof context.featurePrompt !== 'string') { throw new Error('Invalid featurePrompt in context'); } if (!Array.isArray(context.log)) { throw new Error('Invalid log in context'); } return context as Context; }; const validateAgentOutput = (output: any): AgentOutput => { if (typeof output.completed !== 'boolean') { throw new Error('Agent output must have completed boolean'); } return output as AgentOutput; }; export const resetContext = (): void => { currentContext = null; }; export const createContext = (featurePrompt: string): string => { const specId = generateSpecId(); const now = new Date().toISOString(); currentContext = { specId, featurePrompt, spec: {}, agents: {}, finalBundle: {}, log: [], metadata: { createdAt: now, lastUpdated: now, version: '1.0.0', }, }; return currentContext.specId; }; export const getContext = (): Context => { if (!currentContext) { throw new Error('Context not initialized. Call createContext first.'); } return currentContext; }; export const updateContext = (updates: Partial<Context>): Context => { if (!currentContext) { throw new Error('Context not initialized. Call createContext first.'); } const updatedContext = { ...currentContext, ...updates, metadata: { ...currentContext.metadata, ...updates.metadata, lastUpdated: new Date().toISOString(), }, }; try { currentContext = validateContext(updatedContext); return currentContext; } catch (error) { console.warn('Context validation failed, keeping previous context:', error); throw new Error(`Context update failed validation: ${error}`); } }; export const updateAgentOutput = (agentName: string, output: Partial<AgentOutput>): void => { if (!currentContext) { throw new Error('Context not initialized'); } const existingAgent = currentContext.agents[agentName] || { completed: false }; const updatedAgent = { ...existingAgent, ...output }; try { validateAgentOutput(updatedAgent); updateContext({ agents: { ...currentContext.agents, [agentName]: updatedAgent, }, }); } catch (error) { console.error(`Agent output validation failed for ${agentName}:`, error); throw new Error(`Agent output validation failed: ${error}`); } }; export const logToContext = (message: string): void => { if (currentContext) { currentContext.log.push(message); currentContext.metadata.lastUpdated = new Date().toISOString(); } }; const createBackup = async (filePath: string): Promise<string> => { try { const stats = await fs.stat(filePath); if (stats.isFile()) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const fileName = path.basename(filePath, '.json'); const backupPath = path.join(backupDir, `${fileName}-${timestamp}.json`); await fs.copyFile(filePath, backupPath); console.log(`💾 Backup created: ${backupPath}`); return backupPath; } } catch (error: any) { // Silently handle ENOENT (file doesn't exist yet) - this is normal on first save if (error.code !== 'ENOENT') { console.warn('Failed to create backup:', error); } } return ''; }; export const saveContext = async (createBackupFlag = true): Promise<string> => { if (!currentContext) { throw new Error('Context not initialized.'); } await ensureDirectories(); try { validateContext(currentContext); } catch (error) { throw new Error(`Cannot save invalid context: ${error}`); } const filePath = path.join(sessionsDir, `${currentContext.specId}.json`); // Create backup if file exists and backup is requested if (createBackupFlag) { await createBackup(filePath); } // Write with atomic operation (write to temp file first) const tempPath = `${filePath}.tmp`; try { await fs.writeFile(tempPath, JSON.stringify(currentContext, null, 2)); await fs.rename(tempPath, filePath); console.log(`✅ Context saved successfully: ${filePath}`); return filePath; } catch (error) { // Clean up temp file if it exists try { await fs.unlink(tempPath); } catch {} throw new Error(`Failed to save context: ${error}`); } }; export const loadContext = async (specId: string): Promise<Context> => { const filePath = path.join(sessionsDir, `${specId}.json`); try { const data = await fs.readFile(filePath, 'utf-8'); const parsedContext = JSON.parse(data); currentContext = validateContext(parsedContext); console.log(`✅ Context loaded successfully: ${filePath}`); return currentContext; } catch (error) { throw new Error(`Failed to load or validate context from ${filePath}: ${error}`); } }; // Utility functions for agent management export const getAgentStatus = (agentName: string): AgentOutput | null => { if (!currentContext) return null; return currentContext.agents[agentName] || null; }; export const isAgentCompleted = (agentName: string): boolean => { const status = getAgentStatus(agentName); return status?.completed === true; }; export const getFailedAgents = (): string[] => { if (!currentContext) return []; return Object.entries(currentContext.agents) .filter(([_, agent]) => agent.error && !agent.completed) .map(([name]) => name); }; export const getCompletedAgents = (): string[] => { if (!currentContext) return []; return Object.entries(currentContext.agents) .filter(([_, agent]) => agent.completed) .map(([name]) => name); }; export const getPendingAgents = (allAgents: string[]): string[] => { if (!currentContext) return allAgents; return allAgents.filter(agent => !isAgentCompleted(agent)); }; export const getContextStats = () => { if (!currentContext) return null; const agents = Object.keys(currentContext.agents); const completed = getCompletedAgents(); const failed = getFailedAgents(); const pending = agents.filter(agent => !completed.includes(agent) && !failed.includes(agent)); return { total: agents.length, completed: completed.length, failed: failed.length, pending: pending.length, completionRate: agents.length > 0 ? (completed.length / agents.length) * 100 : 0, }; }; // Export types export type { Context, AgentOutput };