UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

377 lines 13.8 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 { AgentValidator } from './agent-validator.js'; import { AgentPackager } from './agent-packager.js'; export class AgentDeployer { validator; packager; constructor() { this.validator = new AgentValidator(); this.packager = new AgentPackager(); } /** * Deploy agents to target platform */ async deploy(target, options = {}) { const result = { 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, options = {}) { const results = new Map(); 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) { const agents = []; const searchPaths = []; 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(); 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 */ async loadAgentsFromPath(dirPath) { const agents = []; 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 */ async parseAgentFile(filePath) { 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 */ parseFrontmatter(content) { const metadata = {}; // Match frontmatter between --- delimiters const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); if (!frontmatterMatch) { return metadata; } 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; 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; } /** * Remove frontmatter from content */ removeFrontmatter(content) { const match = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/); return match ? match[1].trim() : content; } /** * Filter agents by options */ filterAgents(agents, options) { 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 */ async deployAgent(agent, target, options) { 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 */ getAgentsDirectory(target) { if (target.agentsPath) { return path.resolve(target.projectPath, target.agentsPath); } const platformDirs = { claude: '.claude/agents', codex: '.codex/agents', copilot: '.github/agents', cursor: '.cursor/agents', factory: '.factory/droids', opencode: '', // Agents are config-only in OpenCode — no directory scanned warp: '.warp/agents', generic: 'agents', windsurf: '.windsurf/agents', hermes: '', openclaw: '.openclaw/agents', }; return path.resolve(target.projectPath, platformDirs[target.platform]); } /** * Create backup of existing agents */ async createBackup(target) { 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, target) { 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, target) { 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; } } } //# sourceMappingURL=agent-deployer.js.map