@oliverpople/agency-x
Version:
🚀 **Transform feature requests into production-ready code in seconds**
274 lines (234 loc) • 7.77 kB
text/typescript
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 };