UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

473 lines (405 loc) 12.9 kB
/** * Agent Deployer * * Core deployment engine for multi-platform agent deployment. * * @implements @.aiwg/requirements/use-cases/UC-002-deploy-sdlc-framework.md * @architecture @.aiwg/architecture/software-architecture-doc.md - Section 2.1 CLI Entry Point * @nfr @.aiwg/requirements/nfr-modules/performance.md - NFR-PERF-002 (<10s deployment) * @tests @test/unit/agents/agent-deployer.test.ts * @depends @src/agents/agent-validator.ts * @depends @src/agents/agent-packager.ts * @agent-catalog @agentic/code/frameworks/sdlc-complete/agents/ */ import * as fs from 'fs/promises'; import * as path from 'path'; import { existsSync } from 'fs'; import type { AgentInfo, DeploymentTarget, DeploymentOptions, DeploymentResult, AgentDeploymentResult, AgentMetadata, Platform, } from './types.js'; import { AgentValidator } from './agent-validator.js'; import { AgentPackager } from './agent-packager.js'; export class AgentDeployer { private validator: AgentValidator; private packager: AgentPackager; constructor() { this.validator = new AgentValidator(); this.packager = new AgentPackager(); } /** * Deploy agents to target platform */ async deploy( target: DeploymentTarget, options: DeploymentOptions = {} ): Promise<DeploymentResult> { const result: DeploymentResult = { platform: target.platform, projectPath: target.projectPath, deployed: [], skipped: [], failed: [], totalAgents: 0, deployedCount: 0, skippedCount: 0, failedCount: 0, }; try { // Load agents from default locations const agents = await this.loadAgents(); result.totalAgents = agents.length; if (options.verbose) { console.log(`Loaded ${agents.length} agents`); } // Filter agents by options const filteredAgents = this.filterAgents(agents, options); if (options.verbose) { console.log(`Deploying ${filteredAgents.length} agents after filtering`); } // Validate agents const validationResults = await this.validator.validateBatch(filteredAgents); const validAgents = validationResults .filter((r) => r.valid) .map((r) => r.agent); // Report validation failures const invalidAgents = validationResults.filter((r) => !r.valid); for (const invalid of invalidAgents) { result.failed.push({ agent: invalid.agent, error: `Validation failed: ${invalid.issues .filter((i) => i.type === 'error') .map((i) => i.message) .join(', ')}`, }); result.failedCount++; } // Create backup if requested if (options.backup && !options.dryRun) { result.backupPath = await this.createBackup(target); if (options.verbose) { console.log(`Backup created: ${result.backupPath}`); } } // Deploy each valid agent for (const agent of validAgents) { try { const deployResult = await this.deployAgent(agent, target, options); if (deployResult.success) { result.deployed.push(deployResult); result.deployedCount++; } else { result.skipped.push(agent); result.skippedCount++; } } catch (error) { result.failed.push({ agent, error: error instanceof Error ? error.message : String(error), }); result.failedCount++; } } } catch (error) { throw new Error( `Deployment failed: ${error instanceof Error ? error.message : String(error)}` ); } return result; } /** * Deploy agents to multiple targets */ async deployBatch( targets: DeploymentTarget[], options: DeploymentOptions = {} ): Promise<Map<string, DeploymentResult>> { const results = new Map<string, DeploymentResult>(); for (const target of targets) { const key = `${target.platform}:${target.projectPath}`; const result = await this.deploy(target, options); results.set(key, result); } return results; } /** * Load agents from source directories */ async loadAgents(sourcePath?: string): Promise<AgentInfo[]> { const agents: AgentInfo[] = []; const searchPaths: string[] = []; if (sourcePath) { searchPaths.push(sourcePath); } else { // Default search paths const homeDir = process.env.HOME || process.env.USERPROFILE || ''; const aiwgPath = path.join(homeDir, '.local/share/ai-writing-guide/agents'); const localAgents = path.join(process.cwd(), 'agents'); const sdlcAgents = path.join( homeDir, '.local/share/ai-writing-guide/agentic/code/frameworks/sdlc-complete/agents' ); if (existsSync(aiwgPath)) searchPaths.push(aiwgPath); if (existsSync(sdlcAgents)) searchPaths.push(sdlcAgents); if (existsSync(localAgents)) searchPaths.push(localAgents); } // Load agents from all search paths for (const searchPath of searchPaths) { const pathAgents = await this.loadAgentsFromPath(searchPath); agents.push(...pathAgents); } // Remove duplicates by name (prefer earlier paths) const uniqueAgents = new Map<string, AgentInfo>(); for (const agent of agents) { if (!uniqueAgents.has(agent.metadata.name)) { uniqueAgents.set(agent.metadata.name, agent); } } return Array.from(uniqueAgents.values()); } /** * Load agents from a specific path */ private async loadAgentsFromPath(dirPath: string): Promise<AgentInfo[]> { const agents: AgentInfo[] = []; if (!existsSync(dirPath)) { return agents; } const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.md')) { // Skip template and README files if ( entry.name === 'agent-template.md' || entry.name === 'README.md' || entry.name === 'manifest.md' ) { continue; } const filePath = path.join(dirPath, entry.name); try { const agent = await this.parseAgentFile(filePath); if (agent) { agents.push(agent); } } catch (error) { // Skip files that fail to parse console.warn(`Warning: Failed to parse ${filePath}: ${error}`); } } } return agents; } /** * Parse agent file */ private async parseAgentFile(filePath: string): Promise<AgentInfo | null> { const content = await fs.readFile(filePath, 'utf-8'); const metadata = this.parseFrontmatter(content); if (!metadata.name) { // Use filename as fallback metadata.name = path.basename(filePath, '.md'); } // Load agent even if description is missing - validation will catch it // Extract content (remove frontmatter) const contentWithoutFrontmatter = this.removeFrontmatter(content); return { metadata, content: contentWithoutFrontmatter, filePath, fileName: path.basename(filePath), }; } /** * Parse YAML frontmatter */ private parseFrontmatter(content: string): AgentMetadata { const metadata: Partial<AgentMetadata> = {}; // Match frontmatter between --- delimiters const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); if (!frontmatterMatch) { return metadata as AgentMetadata; } const frontmatter = frontmatterMatch[1]; const lines = frontmatter.split('\n'); for (const line of lines) { const match = line.match(/^(\w+):\s*(.+)$/); if (match) { const key = match[1].trim(); const value = match[2].trim(); switch (key) { case 'name': metadata.name = value; break; case 'description': metadata.description = value; break; case 'category': metadata.category = value as any; break; case 'model': metadata.model = value; break; case 'version': metadata.version = value; break; case 'tools': metadata.tools = value.split(',').map((t) => t.trim()); break; case 'dependencies': metadata.dependencies = value.split(',').map((d) => d.trim()); break; } } } return metadata as AgentMetadata; } /** * Remove frontmatter from content */ private removeFrontmatter(content: string): string { const match = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/); return match ? match[1].trim() : content; } /** * Filter agents by options */ private filterAgents( agents: AgentInfo[], options: DeploymentOptions ): AgentInfo[] { let filtered = agents; // Filter by category if (options.categories && options.categories.length > 0) { filtered = filtered.filter( (agent) => agent.metadata.category && options.categories!.includes(agent.metadata.category) ); } // Filter by name if (options.agentNames && options.agentNames.length > 0) { filtered = filtered.filter((agent) => options.agentNames!.includes(agent.metadata.name) ); } return filtered; } /** * Deploy a single agent */ private async deployAgent( agent: AgentInfo, target: DeploymentTarget, options: DeploymentOptions ): Promise<AgentDeploymentResult> { const agentsDir = this.getAgentsDirectory(target); const fileName = this.packager.getFileName(agent, target.platform); const targetPath = path.join(agentsDir, fileName); // Check if file exists and force is not set if (existsSync(targetPath) && !options.force) { return { agent, success: false, targetPath, error: 'File exists (use --force to overwrite)', }; } // Dry run: don't write files if (options.dryRun) { return { agent, success: true, targetPath, }; } // Package agent for platform const packaged = await this.packager.package(agent, target.platform); // Create directory if needed await fs.mkdir(agentsDir, { recursive: true }); // Write file await fs.writeFile(targetPath, packaged.content, 'utf-8'); return { agent, success: true, targetPath, }; } /** * Get agents directory for platform */ private getAgentsDirectory(target: DeploymentTarget): string { if (target.agentsPath) { return path.resolve(target.projectPath, target.agentsPath); } const platformDirs: Record<Platform, string> = { claude: '.claude/agents', codex: '.codex/agents', copilot: '.github/agents', cursor: '.cursor/agents', factory: '.factory/droids', opencode: '.opencode/agent', warp: '.warp/agents', generic: 'agents', windsurf: '.windsurf/agents', }; return path.resolve(target.projectPath, platformDirs[target.platform]); } /** * Create backup of existing agents */ async createBackup(target: DeploymentTarget): Promise<string> { const agentsDir = this.getAgentsDirectory(target); if (!existsSync(agentsDir)) { return ''; // Nothing to backup } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = path.join( target.projectPath, `.agents-backup-${timestamp}` ); await fs.cp(agentsDir, backupPath, { recursive: true }); return backupPath; } /** * Rollback to previous backup */ async rollback(backupPath: string, target: DeploymentTarget): Promise<void> { if (!existsSync(backupPath)) { throw new Error(`Backup not found: ${backupPath}`); } const agentsDir = this.getAgentsDirectory(target); // Remove current agents directory if (existsSync(agentsDir)) { await fs.rm(agentsDir, { recursive: true, force: true }); } // Restore from backup await fs.cp(backupPath, agentsDir, { recursive: true }); } /** * Get installed version of an agent */ async getInstalledVersion( agentName: string, target: DeploymentTarget ): Promise<string | null> { const agentsDir = this.getAgentsDirectory(target); const fileName = `${agentName}${this.packager.getFileExtension(target.platform)}`; const filePath = path.join(agentsDir, fileName); if (!existsSync(filePath)) { return null; } try { const content = await fs.readFile(filePath, 'utf-8'); const metadata = this.parseFrontmatter(content); return metadata.version || null; } catch { return null; } } }