UNPKG

automagik-genie

Version:

Self-evolving AI agent orchestration framework with Model Context Protocol support

328 lines (327 loc) 13.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.detectInstallType = detectInstallType; exports.backupGenie = backupGenie; exports.analyzeAgents = analyzeAgents; exports.extractCustomizations = extractCustomizations; exports.copyTemplates = copyTemplates; exports.runMigration = runMigration; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const child_process_1 = require("child_process"); // Core agents that ship with npm package (should NOT be in user .genie/agents/) // Reflects current structure: workflows/, agents/, agents/modes/ const CORE_AGENT_IDS = [ // Workflow orchestrators (.genie/agents/workflows/) 'workflows/plan', 'workflows/wish', 'workflows/forge', 'workflows/review', 'workflows/vibe', 'workflows/qa', 'workflows/prompt', // Core agents (.genie/agents/agents/) 'agents/orchestrator', 'agents/commit', 'agents/git', 'agents/implementor', 'agents/install', 'agents/learn', 'agents/polish', 'agents/release', 'agents/roadmap', 'agents/tests', // Strategic thinking modes (.genie/agents/agents/modes/) 'agents/modes/analyze', 'agents/modes/audit', 'agents/modes/challenge', 'agents/modes/consensus', 'agents/modes/debug', 'agents/modes/docgen', 'agents/modes/explore', 'agents/modes/refactor', 'agents/modes/tracer', ]; /** * Detects if this is a clean install or needs migration * * CRITICAL FIX for issue #237 (infinite update loop): * Check version files FIRST - if modern version exists, never report as "old_genie" * The presence of .genie/agents/workflows/ and .genie/agents/agents/ with .md files * is NOT a reliable indicator of old installations, as the new structure (v2.1.0+) * also copies these directories from templates. */ function detectInstallType() { const genieDir = '.genie'; if (!fs_1.default.existsSync(genieDir)) { return 'clean'; } // FIX: Check for modern version file FIRST // If version.json exists and has a modern version (v2.1.0+), this is NOT old_genie const versionPath = path_1.default.join(genieDir, 'state', 'version.json'); if (fs_1.default.existsSync(versionPath)) { try { const versionData = JSON.parse(fs_1.default.readFileSync(versionPath, 'utf8')); const version = versionData.version; // If version.json exists with a version string, this is modern structure if (version && typeof version === 'string') { return 'already_new'; } } catch (e) { // Corrupted version file - treat as needs upgrade } } const agentsDir = path_1.default.join(genieDir, 'agents'); if (!fs_1.default.existsSync(agentsDir)) { return 'clean'; } // Check for old nested structure (workflows/ and agents/ subdirectories) const workflowsDir = path_1.default.join(agentsDir, 'workflows'); const legacyAgentsDir = path_1.default.join(agentsDir, 'agents'); if (fs_1.default.existsSync(workflowsDir) && fs_1.default.existsSync(legacyAgentsDir)) { // Has old nested structure - check if agents come from npm or are in repo const workflowAgents = fs_1.default.existsSync(workflowsDir) ? fs_1.default.readdirSync(workflowsDir).filter(f => f.endsWith('.md')).length : 0; const agentCount = fs_1.default.existsSync(legacyAgentsDir) ? fs_1.default.readdirSync(legacyAgentsDir).filter(f => f.endsWith('.md')).length : 0; // If agents exist in repo, old structure (should come from npm) // BUT: Only flag as old_genie if NO version file exists (modern installs always have version) if (workflowAgents > 0 || agentCount > 0) { return 'old_genie'; } return 'already_new'; } // Check for legacy core/ structure const coreDir = path_1.default.join(agentsDir, 'core'); if (fs_1.default.existsSync(coreDir)) { const coreAgents = fs_1.default.readdirSync(coreDir).filter(f => f.endsWith('.md')); // If core/ has agents, this is old structure if (coreAgents.length > 0) { return 'old_genie'; } } // Check for very old structure (top-level agents) const topLevelAgents = fs_1.default.readdirSync(agentsDir).filter(f => f.endsWith('.md')); const hasOldCoreAgents = topLevelAgents.some(f => ['plan', 'wish', 'forge', 'review', 'orchestrator', 'vibe'].includes(f.replace('.md', ''))); return hasOldCoreAgents ? 'old_genie' : 'already_new'; } /** * Creates timestamped backup of .genie/ directory */ function backupGenie() { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T').join('-').slice(0, -5); const backupPath = `.genie-backup-${timestamp}`; (0, child_process_1.execSync)(`cp -r .genie "${backupPath}"`); return backupPath; } /** * Analyzes which agents are custom (user-created) vs core (framework) */ function analyzeAgents() { const agentsDir = '.genie/agents'; const result = { core: [], custom: [], modified: [] }; if (!fs_1.default.existsSync(agentsDir)) { return result; } const walkDir = (dir, prefix = '') => { const entries = fs_1.default.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name === 'README.md') continue; const fullPath = path_1.default.join(dir, entry.name); const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; if (entry.isDirectory()) { walkDir(fullPath, relativePath); } else if (entry.name.endsWith('.md')) { const agentId = relativePath.replace(/\.md$/, ''); if (CORE_AGENT_IDS.includes(agentId)) { // TODO: Check if modified from framework default // For now, assume all core agents should be removed result.core.push(agentId); } else { result.custom.push(agentId); } } } }; walkDir(agentsDir); return result; } /** * Extracts customizations from modified core agents (custom folder retired) */ function extractCustomizations(coreAgents) { const extracted = []; // TODO: Implement diff-based extraction/merge into local agent/skill docs // For now: no-op (document that custom folder is retired) return extracted; } /** * Copies templates from npm package to user project */ function copyTemplates(options = {}) { // Resolve npm package location (from dist/cli/lib/) const packageRoot = path_1.default.resolve(__dirname, '../../..'); // Migration is for code projects (old Genie was for development) const templatesSource = path_1.default.join(packageRoot, 'templates', 'code'); if (!fs_1.default.existsSync(templatesSource)) { throw new Error(`Templates not found at ${templatesSource}`); } // Copy .claude/ directory (npm-referencing aliases) const claudeSource = path_1.default.join(templatesSource, '.claude'); const claudeDest = '.claude'; if (fs_1.default.existsSync(claudeDest) && !options.force) { console.warn(`⚠️ .claude/ exists, skipping (use --force to overwrite)`); } else { if (fs_1.default.existsSync(claudeDest)) { (0, child_process_1.execSync)(`rm -rf ${claudeDest}`); } (0, child_process_1.execSync)(`cp -r "${claudeSource}" "${claudeDest}"`); } // Note: `.genie/custom/` retired — no custom stubs copied // Copy product/ and standards/ templates if they don't exist const copyIfMissing = (subdir) => { const source = path_1.default.join(templatesSource, '.genie', subdir); const dest = path_1.default.join('.genie', subdir); if (!fs_1.default.existsSync(dest)) { (0, child_process_1.execSync)(`cp -r "${source}" "${dest}"`); } }; copyIfMissing('product'); copyIfMissing('standards'); // Ensure state/ and wishes/ directories exist ['state', 'wishes'].forEach(dir => { const dirPath = path_1.default.join('.genie', dir); if (!fs_1.default.existsSync(dirPath)) { fs_1.default.mkdirSync(dirPath, { recursive: true }); } }); // Copy root documentation files if missing const rootDocs = ['AGENTS.md', 'CLAUDE.md', 'README.md', '.gitignore']; for (const doc of rootDocs) { const source = path_1.default.join(templatesSource, doc); const dest = doc; if (fs_1.default.existsSync(source) && !fs_1.default.existsSync(dest)) { fs_1.default.copyFileSync(source, dest); } } } /** * Main migration orchestrator */ async function runMigration(options = {}) { const result = { status: 'failed', customAgentsPreserved: [], coreAgentsRemoved: [], customizationsExtracted: [], errors: [], warnings: [] }; try { // Step 1: Detect installation type const installType = detectInstallType(); console.log(`📊 Installation type: ${installType}`); if (installType === 'clean') { console.log('✨ Clean installation detected'); if (!options.dryRun) { copyTemplates(options); } result.status = 'clean_install'; return result; } if (installType === 'already_new') { console.log('✅ Already using new structure'); result.status = 'already_migrated'; return result; } // Step 2: Backup existing installation console.log('💾 Creating backup...'); if (!options.dryRun) { result.backupPath = backupGenie(); console.log(` Backup created: ${result.backupPath}`); } // Step 3: Analyze agents console.log('🔍 Analyzing agents...'); const analysis = analyzeAgents(); console.log(` Core agents: ${analysis.core.length}`); console.log(` Custom agents: ${analysis.custom.length}`); console.log(` Modified: ${analysis.modified.length}`); result.customAgentsPreserved = analysis.custom; // Step 4: Extract customizations from modified core agents if (analysis.modified.length > 0) { console.log('📝 Extracting customizations...'); if (!options.dryRun) { result.customizationsExtracted = extractCustomizations(analysis.modified); } } // Step 5: Remove core agents (they'll come from npm) console.log('🗑️ Removing core agents (now in npm package)...'); if (!options.dryRun) { for (const agentId of analysis.core) { const agentPath = path_1.default.join('.genie', 'agents', `${agentId}.md`); if (fs_1.default.existsSync(agentPath)) { fs_1.default.unlinkSync(agentPath); result.coreAgentsRemoved.push(agentId); } } // Clean up empty directories const cleanEmptyDirs = (dir) => { if (!fs_1.default.existsSync(dir)) return; const entries = fs_1.default.readdirSync(dir, { withFileTypes: true }); // Recursively clean subdirectories first for (const entry of entries) { if (entry.isDirectory()) { cleanEmptyDirs(path_1.default.join(dir, entry.name)); } } // If directory is empty, remove it const remaining = fs_1.default.readdirSync(dir); if (remaining.length === 0 || (remaining.length === 1 && remaining[0] === 'README.md')) { (0, child_process_1.execSync)(`rm -rf "${dir}"`); } }; // Clean up legacy structure directories (if they exist) cleanEmptyDirs(path_1.default.join('.genie', 'agents', 'core')); cleanEmptyDirs(path_1.default.join('.genie', 'agents', 'qa')); // Clean up current structure directories (should be empty after core removal) cleanEmptyDirs(path_1.default.join('.genie', 'agents', 'workflows')); cleanEmptyDirs(path_1.default.join('.genie', 'agents', 'agents')); } // Step 6: Copy new templates console.log('📦 Installing new template structure...'); if (!options.dryRun) { copyTemplates(options); } // Step 7: Success! console.log('✅ Migration complete!'); result.status = 'upgraded'; // Summary console.log('\n📋 Migration Summary:'); console.log(` Backup: ${result.backupPath}`); console.log(` Custom agents preserved: ${result.customAgentsPreserved.length}`); console.log(` Core agents removed: ${result.coreAgentsRemoved.length}`); if (result.customizationsExtracted.length > 0) { console.log(` Customizations extracted: ${result.customizationsExtracted.length}`); } } catch (error) { result.errors.push(error instanceof Error ? error.message : String(error)); console.error('❌ Migration failed:', error); } return result; }