UNPKG

automagik-genie

Version:

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

1,056 lines â€ĸ 58.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runInit = runInit; const path_1 = __importDefault(require("path")); const fs_1 = require("fs"); const yaml_1 = __importDefault(require("yaml")); const view_helpers_1 = require("../lib/view-helpers"); const common_1 = require("../views/common"); const executor_registry_1 = require("../lib/executor-registry"); const collective_discovery_js_1 = require("../lib/collective-discovery.js"); const paths_1 = require("../lib/paths"); const fs_utils_1 = require("../lib/fs-utils"); const package_1 = require("../lib/package"); const migrate_1 = require("../lib/migrate"); const executor_auth_1 = require("../lib/executor-auth"); const version_utils_1 = require("../lib/version-utils"); const genesis_diff_1 = require("../lib/genesis-diff"); const update_helpers_js_1 = require("../lib/update-helpers.js"); const prompts_1 = __importDefault(require("prompts")); const child_process_1 = require("child_process"); const DEFAULT_MODE_DESCRIPTION = 'Workspace automation via Forge-backed executors.'; async function runInit(parsed, _config, _paths) { try { const flags = parseFlags(parsed.commandArgs); const cwd = process.cwd(); const packageRoot = (0, paths_1.getPackageRoot)(); // CRITICAL: Check version FIRST (before wizard) to detect upgrade scenarios // This must happen before any user interaction to correctly route fresh vs upgrade installations const versionPath = (0, paths_1.resolveWorkspaceVersionPath)(cwd); const currentPackageVersion = (0, package_1.getPackageVersion)(); let isUpgrade = false; let oldVersion; let isPartialInstall = false; if (await (0, fs_utils_1.pathExists)(versionPath)) { try { const versionData = JSON.parse(await fs_1.promises.readFile(versionPath, 'utf8')); oldVersion = versionData.version; if (oldVersion === currentPackageVersion) { // True partial installation (same version, incomplete setup) isPartialInstall = true; } else { // Version mismatch = upgrade scenario isUpgrade = true; } } catch { // Corrupted version.json, treat as fresh install isUpgrade = false; oldVersion = undefined; } } // Check if running in interactive mode (TTY) or automation mode (--yes flag or explicit template) // IMPORTANT: Skip wizard for upgrades (handled via diff generation flow) const isInteractive = process.stdout.isTTY && !flags.yes && !flags.template && !isUpgrade; let template; let executor; let model; let shouldInitGit = false; let shouldInstallHooks = false; let templates = []; // Array for multi-select if (isInteractive) { // Use dynamic import to load ESM Ink components const { runInitWizard } = await import('../views/init-wizard.js'); // Discover collectives dynamically from .genie/ directory const genieRoot = path_1.default.join(packageRoot, '.genie'); const discovered = await (0, collective_discovery_js_1.discoverCollectives)(genieRoot); const templateChoices = discovered.map(c => ({ value: c.id, label: c.label || c.name, description: c.description })); // Fallback if discovery fails - provide both code and create if (templateChoices.length === 0) { templateChoices.push({ value: 'code', label: 'đŸ’ģ Code', description: 'Software dev agents (Git, PR, tests, CI/CD workflows)' }, { value: 'create', label: 'âœī¸ Create', description: 'Content creation agents (writing, research, planning)' }); } const primaryExecutors = executor_registry_1.USER_EXECUTOR_ORDER.filter(key => key in executor_registry_1.EXECUTORS); const additionalExecutors = Object.keys(executor_registry_1.EXECUTORS).filter(key => key !== 'AMP' && !primaryExecutors.includes(key)); const orderedExecutors = [...primaryExecutors, ...additionalExecutors]; const executors = orderedExecutors.map(key => ({ label: executor_registry_1.EXECUTORS[key].label, value: key })); const hasGit = await (0, fs_utils_1.pathExists)(path_1.default.join(cwd, '.git')); const wizardConfig = await runInitWizard({ templates: templateChoices, executors, hasGit }); templates = wizardConfig.templates; // Array from multi-select template = templates[0]; // Keep first for backward compat executor = wizardConfig.executor; model = wizardConfig.model; shouldInitGit = wizardConfig.initGit; shouldInstallHooks = wizardConfig.installHooks; // Configure executor authentication (one-by-one, after wizard) if (wizardConfig.configureAuth) { await configureExecutorAuthentication(executor); } } else { // Automation mode OR upgrade mode: use flags or detect from existing installation const automationTargetGenie = (0, paths_1.resolveTargetGeniePath)(cwd); if (isUpgrade && await (0, fs_utils_1.pathExists)(automationTargetGenie)) { // Upgrade: detect installed collectives from existing .genie/ templates = await detectInstalledCollectives(automationTargetGenie); template = templates[0]; // Primary template (first one) console.log(`đŸ“Ļ Detected installed collectives: ${templates.join(', ')}`); } else { // Automation mode (fresh install): use flags or default template = (flags.template || 'code'); templates = [template]; } executor = executor_registry_1.DEFAULT_EXECUTOR_KEY; model = undefined; } const templateGenie = (0, paths_1.getTemplateGeniePath)(template); const targetGenie = (0, paths_1.resolveTargetGeniePath)(cwd); const templateExists = await (0, fs_utils_1.pathExists)(templateGenie); if (!templateExists) { await (0, view_helpers_1.emitView)((0, common_1.buildErrorView)('Template missing', `Could not locate packaged .genie templates at ${templateGenie}`), parsed.options, { stream: process.stderr }); process.exitCode = 1; return; } // Handle partial install scenario (version check already done above) if (isPartialInstall) { console.log(''); console.log('🔍 Detected partial installation'); console.log('đŸ“Ļ Templates already copied, resuming setup...'); console.log(''); // Skip file operations; go straight to executor setup const resumeExecutor = executor_registry_1.DEFAULT_EXECUTOR_KEY; const resumeModel = undefined; await applyExecutorDefaults(targetGenie, resumeExecutor, resumeModel); // Note: MCP configuration handled by Forge, not init await (0, view_helpers_1.emitView)(buildInitSummaryView({ executor: resumeExecutor, model: resumeModel, templateSource: templateGenie, target: targetGenie }), parsed.options); // Note: Install agent is launched by start.sh after init completes return; } // Show appropriate welcome message for fresh installs or upgrades if (isUpgrade && oldVersion) { console.log(''); console.log(`🔄 Upgrading from ${oldVersion} to ${currentPackageVersion}...`); console.log(''); } else if (!isUpgrade) { // No version.json = fresh installation console.log(''); console.log('🧞 Welcome to Genie! Setting up your workspace...'); console.log(''); } // Auto-detect old Genie structure and suggest migration const installType = (0, migrate_1.detectInstallType)(); if (installType === 'old_genie' && !flags.yes) { console.log(''); console.log('╭───────────────────────────────────────────────────────────╮'); console.log('│ âš ī¸ Old Genie Installation Detected │'); console.log('╰───────────────────────────────────────────────────────────╯'); console.log(''); console.log('Your project has an old Genie structure (v2.0.x) with core'); console.log('agents stored locally. The new architecture (v2.1.0+) loads'); console.log('core agents from the npm package for easier updates.'); console.log(''); console.log('Run `genie init --yes` to force reinitialize (creates backup).'); console.log(''); // FIX for issue #237: Write version file even when returning early // This prevents infinite loop where version stays old, triggering init again await writeVersionState(cwd, undefined, false); await (0, view_helpers_1.emitView)((0, common_1.buildInfoView)('Old Installation Detected', [ 'Use `genie init --yes` to force reinitialize (creates backup first).' ]), parsed.options); return; } // Initialize git if needed (wizard already prompted in interactive mode) if (shouldInitGit || (!isInteractive && !await (0, fs_utils_1.pathExists)(path_1.default.join(cwd, '.git')))) { if (!isInteractive && flags.yes) { const { execSync } = await import('child_process'); // Set default branch to main to suppress git init hints execSync('git config --global init.defaultBranch main 2>/dev/null || true', { cwd, stdio: 'pipe' }); execSync('git init', { cwd, stdio: 'pipe' }); } else if (shouldInitGit) { const { execSync } = await import('child_process'); // Set default branch to main to suppress git init hints execSync('git config --global init.defaultBranch main 2>/dev/null || true', { cwd, stdio: 'pipe' }); execSync('git init', { cwd, stdio: 'pipe' }); } } // CRITICAL: Backup/diff handling based on version let backupId; let tempBackupPath; let diffPath; const genieExists = await (0, fs_utils_1.pathExists)(targetGenie); // Check if .genie/ has actual content (not just empty state/ directory from version check) const hasActualContent = genieExists ? await genieHasContent(targetGenie) : false; const useKnowledgeDiff = isUpgrade && oldVersion && (0, version_utils_1.isVersionGte)(currentPackageVersion, '2.5.14'); if (genieExists && hasActualContent) { if (useKnowledgeDiff) { console.log(''); console.log('📊 Generating framework upgrade diff (non-destructive)...'); console.log(''); // NON-DESTRUCTIVE: Compare local workspace vs upstream package template // NO file copying, NO file moving - just generate diff const diffResult = await (0, genesis_diff_1.generateGenesisDiff)(cwd, packageRoot, oldVersion, currentPackageVersion); diffPath = diffResult.diffPath; console.log(` ✅ Diff generated: ${path_1.default.relative(cwd, diffPath)}`); console.log(` 📊 Changes: +${diffResult.summary.added} -${diffResult.summary.removed} ~${diffResult.summary.modified}`); console.log(''); if (!diffResult.hasChanges) { console.log(' â„šī¸ No framework changes detected. You are up to date!'); console.log(''); return; } // Commit the diff file to repository console.log('💾 Committing upgrade diff...'); try { (0, child_process_1.execSync)(`git add "${diffPath}"`, { cwd, stdio: 'pipe' }); const commitMsg = `docs: upgrade diff v${oldVersion} → v${currentPackageVersion}`; (0, child_process_1.execSync)(`git commit -m "${commitMsg}"`, { cwd, stdio: 'pipe' }); console.log(` ✅ Committed: ${commitMsg}`); } catch (error) { // Git commit might fail if no changes or not a git repo console.log(' âš ī¸ Could not commit diff (may not be a git repo or no changes)'); } console.log(''); // Launch agent task to learn and apply changes console.log('🚀 Launching update agent...'); console.log(''); try { const taskUrl = await (0, update_helpers_js_1.launchUpdateTask)({ diffPath, oldVersion, newVersion: currentPackageVersion, workspacePath: cwd }); console.log(''); console.log('đŸŽ¯ Update task created!'); console.log(` Monitor progress: ${taskUrl}`); console.log(''); console.log('The agent will:'); console.log(' 1. Read and understand the diff'); console.log(' 2. Learn new patterns and teachings'); console.log(' 3. Apply changes selectively (preserving your customizations)'); console.log(' 4. Generate a report of what was learned'); console.log(''); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.log(''); console.log('âš ī¸ Could not launch update agent automatically.'); // Check if this is a Forge-not-running error if (errorMsg.includes('Forge backend is not running')) { console.log(' Reason: Forge backend needs to be running'); console.log(''); console.log(' Steps to apply this update:'); console.log(' 1. Start Genie: genie run'); console.log(' 2. The update diff is ready at:'); console.log(` @${path_1.default.relative(cwd, diffPath)}`); console.log(' 3. Genie will automatically detect and offer to apply it'); } else { console.log(` Reason: ${errorMsg}`); console.log(''); console.log(' You can manually run: genie update'); console.log(` With diff file: @${path_1.default.relative(cwd, diffPath)}`); } console.log(''); } // EXIT EARLY - No destructive file operations // The agent will handle applying changes intelligently return; } else { console.log(''); console.log('💾 Creating backup before overwriting...'); const reason = installType === 'old_genie' ? 'old_genie' : 'pre_upgrade'; const backupResult = await (0, fs_utils_1.backupGenieDirectory)(cwd, reason); // Handle two return types: string (copy backup) or object (two-stage move) if (typeof backupResult === 'string') { backupId = backupResult; console.log(` Backup created: .genie/backups/${backupId}`); } else { backupId = backupResult.backupId; tempBackupPath = backupResult.tempPath; console.log(` Old .genie moved to: ${tempBackupPath}`); } console.log(''); // Create git checkpoint commit (clean rollback point before template modifications) if (backupId && oldVersion) { await createUpgradeCheckpoint(cwd, oldVersion, currentPackageVersion, backupId); console.log(''); } } } if (!useKnowledgeDiff) { // Copy ALL selected templates (not just the first one) for (const tmpl of templates) { await copyTemplateFiles(packageRoot, tmpl, targetGenie); } await copyTemplateRootFiles(packageRoot, cwd, template); } await migrateAgentsDocs(cwd); // Finalize two-stage backup if needed (move temp backup into .genie/backups/) if (tempBackupPath && backupId) { console.log('💾 Finalizing backup...'); await (0, fs_utils_1.finalizeBackup)(cwd, tempBackupPath, backupId); console.log(` Backup finalized: .genie/backups/${backupId}/genie/`); console.log(''); } if (backupId) { const backupInfoPath = path_1.default.join(targetGenie, 'state', 'backup-info.json'); await (0, fs_utils_1.writeJsonFile)(backupInfoPath, { backupId, oldVersion: oldVersion || 'unknown', newVersion: currentPackageVersion, timestamp: new Date().toISOString(), backupPath: `.genie/backups/${backupId}/genie` }); console.log('đŸ“Ļ Backup metadata saved for restoration'); console.log(''); } else if (diffPath) { const diffInfoPath = path_1.default.join(targetGenie, 'state', 'update-diff-info.json'); await (0, fs_utils_1.writeJsonFile)(diffInfoPath, { diffPath: path_1.default.relative(cwd, diffPath), oldVersion: oldVersion || 'unknown', newVersion: currentPackageVersion, timestamp: new Date().toISOString() }); console.log('📊 Diff metadata saved'); console.log(''); // Launch update task in Forge with diff as input try { console.log('🚀 Creating update task in Forge...'); const updateUrl = await (0, update_helpers_js_1.launchUpdateTask)({ diffPath, oldVersion: oldVersion || 'unknown', newVersion: currentPackageVersion, workspacePath: cwd }); console.log(''); console.log('📋 Update task created:'); console.log(updateUrl); console.log(''); // Open browser with task URL (view=chat for immediate interaction) try { const { execSync } = await import('child_process'); const { getBrowserOpenCommand } = await import('../lib/cli-utils.js'); // Change view from diffs to chat for better UX const chatUrl = updateUrl.replace('view=diffs', 'view=chat'); const openCommand = getBrowserOpenCommand(); execSync(`${openCommand} "${chatUrl}"`, { stdio: 'ignore' }); console.log('🌐 Opening browser with upgrade task...'); console.log(''); } catch (browserError) { // Non-fatal: browser opening failed, user can still access URL manually console.log('âš ī¸ Could not open browser automatically'); console.log(` Visit: ${updateUrl}`); console.log(''); } } catch (error) { // Non-blocking: update task creation is optional // If Forge is unavailable, user can still review diff manually const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.includes('Forge backend is not running')) { console.log(''); console.log('âš ī¸ Could not create update task automatically.'); console.log(' Reason: Forge backend needs to be running'); console.log(''); console.log(' Steps to apply this update:'); console.log(' 1. Start Genie: genie run'); console.log(' 2. The update diff is ready at:'); console.log(` @${path_1.default.relative(cwd, diffPath)}`); console.log(' 3. Genie will automatically detect and offer to apply it'); } else { console.warn('âš ī¸ Could not create update task (Forge may be unavailable)'); console.warn(` Reason: ${errorMsg}`); console.warn(' You can review the diff manually at: ' + path_1.default.relative(cwd, diffPath)); } console.log(''); } } // Copy INSTALL.md workflow guide (like UPDATE.md for update command) const templateInstallMd = path_1.default.join(templateGenie, 'INSTALL.md'); const targetInstallMd = path_1.default.join(targetGenie, 'INSTALL.md'); if (await (0, fs_utils_1.pathExists)(templateInstallMd)) { await fs_1.promises.copyFile(templateInstallMd, targetInstallMd); } // Create blank directories for user work (not blacklisted, created fresh) await (0, fs_utils_1.ensureDir)(path_1.default.join(targetGenie, 'backups')); await (0, fs_utils_1.ensureDir)(path_1.default.join(targetGenie, 'wishes')); await (0, fs_utils_1.ensureDir)(path_1.default.join(targetGenie, 'reports')); // Wizard or automation mode should have set executor by now // If still missing (shouldn't happen), use default if (!executor) { executor = executor_registry_1.DEFAULT_EXECUTOR_KEY; console.log(`âš ī¸ Warning: executor not set, using default: ${executor}`); } await writeVersionState(cwd, backupId, false); await initializeProviderStatus(cwd); await applyExecutorDefaults(targetGenie, executor, model); // Note: MCP configuration handled by Forge, not init // Install git hooks if user opted in during wizard await installGitHooksIfRequested(packageRoot, shouldInstallHooks); // Auto-commit template files if git repo exists (MUST happen before install task) const commitSucceeded = await commitTemplateFiles(cwd); // Launch install orchestration via Forge (ONLY if commit succeeded) let dashboardUrl; let installOrchestrationStarted = false; if (commitSucceeded) { try { const { runInstallFlow } = await import('../lib/install-helpers.js'); dashboardUrl = await runInstallFlow({ templates, executor, model }); installOrchestrationStarted = true; } catch (error) { // Forge unavailable or install flow failed - non-fatal console.warn('âš ī¸ Install orchestration skipped (Forge unavailable)'); console.log(` Run: genie run master "Run explorer to acquire context, when it ends run the install workflow. Templates: ${templates.join(', ')}"`); console.log(''); } } else { console.log('âš ī¸ Install task skipped - template files not committed'); console.log(' After committing files, run: genie run master "install"'); console.log(''); } // Show completion summary const summary = { executor, model, backupId, templateSource: templateGenie, target: targetGenie }; await (0, view_helpers_1.emitView)(buildInitSummaryView(summary, installOrchestrationStarted), parsed.options); if (installOrchestrationStarted && dashboardUrl) { console.log(''); console.log('🧞 Master Genie is orchestrating installation...'); console.log(`📊 Monitor progress: ${dashboardUrl}`); console.log(''); // Open browser with installation dashboard try { const { getBrowserOpenCommand } = await import('../lib/cli-utils.js'); const openCommand = getBrowserOpenCommand(); (0, child_process_1.execSync)(`${openCommand} "${dashboardUrl}"`, { stdio: 'ignore' }); console.log('🌐 Opening browser with installation dashboard...'); console.log(''); } catch (browserError) { // Non-fatal: browser opening failed, user can still access URL manually console.log('âš ī¸ Could not open browser automatically'); console.log(` Visit: ${dashboardUrl}`); console.log(''); } } } catch (error) { const message = error instanceof Error ? error.message : String(error); await (0, view_helpers_1.emitView)((0, common_1.buildErrorView)('Init failed', message), parsed.options, { stream: process.stderr }); process.exitCode = 1; } } function parseFlags(args) { const flags = {}; for (let i = 0; i < args.length; i++) { const token = args[i]; // Handle flags if (token === '--yes' || token === '-y') { flags.yes = true; continue; } if (token === '--force' || token === '-f') { flags.force = true; continue; } if (token === '--forge-base-url' && args[i + 1]) { flags.forgeBaseUrl = args[i + 1]; i++; continue; } if (token.startsWith('--forge-base-url=')) { flags.forgeBaseUrl = token.split('=')[1]; continue; } if (token === '--forge-port' && args[i + 1]) { flags.forgePort = args[i + 1]; i++; continue; } if (token.startsWith('--forge-port=')) { flags.forgePort = token.split('=')[1]; continue; } // Handle positional template argument (code | create) if (!token.startsWith('-') && !flags.template) { if (token === 'code' || token === 'create') { flags.template = token; } } } return flags; } async function copyTemplateFiles(packageRoot, template, targetGenie) { const blacklist = (0, paths_1.getTemplateRelativeBlacklist)(); await (0, fs_utils_1.ensureDir)(targetGenie); // 1. Copy root agents/skills/spells/neurons/product from package .genie/ const rootGenieDir = path_1.default.join(packageRoot, '.genie'); await (0, fs_utils_1.copyDirectory)(rootGenieDir, targetGenie, { filter: (relPath) => { if (!relPath) return true; const firstSeg = relPath.split(path_1.default.sep)[0]; // Blacklist takes priority (never copy these user directories) if (blacklist.has(firstSeg)) return false; // Only copy: agents, skills, spells, neurons, product, AGENTS.md, config.yaml, templates if (['agents', 'skills', 'spells', 'neurons', 'product'].includes(firstSeg)) return true; if (relPath === 'AGENTS.md' || relPath === 'config.yaml') return true; if (relPath.endsWith('.template.md')) return true; // Copy all template files return false; } }); // 2. Copy only scripts/helpers directory (generic user-facing utilities) const helpersSource = path_1.default.join(packageRoot, '.genie', 'scripts', 'helpers'); const helpersTarget = path_1.default.join(targetGenie, 'scripts', 'helpers'); if (await (0, fs_utils_1.pathExists)(helpersSource)) { await (0, fs_utils_1.copyDirectory)(helpersSource, helpersTarget); } // 3. Copy chosen collective DIRECTORY (preserving structure) const collectiveSource = path_1.default.join(packageRoot, '.genie', template); const collectiveTarget = path_1.default.join(targetGenie, template); await (0, fs_utils_1.copyDirectory)(collectiveSource, collectiveTarget, { filter: (relPath) => { if (!relPath) return true; const firstSeg = relPath.split(path_1.default.sep)[0]; return !blacklist.has(firstSeg); } }); } async function copyTemplateRootFiles(packageRoot, targetDir, template) { // Copy AGENTS.md and CLAUDE.md (overwrite) const simpleFiles = ['AGENTS.md', 'CLAUDE.md']; for (const file of simpleFiles) { const sourcePath = path_1.default.join(packageRoot, file); const targetPath = path_1.default.join(targetDir, file); if (await (0, fs_utils_1.pathExists)(sourcePath)) { await fs_1.promises.copyFile(sourcePath, targetPath); } } // Special handling for .gitignore (merge, don't overwrite) const sourceGitignore = path_1.default.join(packageRoot, '.gitignore'); const targetGitignore = path_1.default.join(targetDir, '.gitignore'); if (await (0, fs_utils_1.pathExists)(sourceGitignore)) { await mergeGitignore(sourceGitignore, targetGitignore); } } async function mergeGitignore(sourcePath, targetPath) { const sourceContent = await fs_1.promises.readFile(sourcePath, 'utf8'); const sourceLines = new Set(sourceContent.split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#'))); // If target .gitignore exists, merge if (await (0, fs_utils_1.pathExists)(targetPath)) { const targetContent = await fs_1.promises.readFile(targetPath, 'utf8'); const targetLines = targetContent.split('\n'); // Backup original const backupPath = `${targetPath}.backup.${Date.now()}`; await fs_1.promises.copyFile(targetPath, backupPath); console.log(` Backed up existing .gitignore: ${path_1.default.basename(backupPath)}`); // Merge: keep existing lines, add new ones from template const existingSet = new Set(targetLines.map(line => line.trim()).filter(line => line && !line.startsWith('#'))); const newLines = []; for (const line of sourceLines) { if (!existingSet.has(line)) { newLines.push(line); } } if (newLines.length > 0) { const mergedContent = targetContent.trimEnd() + '\n\n# Added by Genie init\n' + newLines.join('\n') + '\n'; await fs_1.promises.writeFile(targetPath, mergedContent, 'utf8'); console.log(` Merged ${newLines.length} new entries into .gitignore`); } else { console.log(' .gitignore already contains all Genie entries'); } } else { // No existing .gitignore, copy template as-is await fs_1.promises.copyFile(sourcePath, targetPath); console.log(' Created .gitignore from template'); } } async function migrateAgentsDocs(cwd) { try { // Remove mistaken .genie/agents.genie if present const mistaken = path_1.default.join(cwd, '.genie', 'agents.genie'); try { await fs_1.promises.rm(mistaken, { force: true }); } catch { } // Ensure domain AGENTS.md include the root AGENTS.md directly const domains = [ path_1.default.join(cwd, '.genie', 'code', 'AGENTS.md'), path_1.default.join(cwd, '.genie', 'create', 'AGENTS.md') ]; for (const domainFile of domains) { try { const raw = await fs_1.promises.readFile(domainFile, 'utf8'); if (!/@AGENTS\.md/i.test(raw)) { const next = raw.trimEnd() + `\n\n@AGENTS.md\n`; await fs_1.promises.writeFile(domainFile, next, 'utf8'); } } catch (_) { } } } catch (err) { console.log(`âš ī¸ Agents docs migration skipped: ${err?.message || String(err)}`); } } /** * Check if .genie/ has actual content worth backing up * Returns false if it only contains empty state/ directory (from version check) */ async function genieHasContent(geniePath) { try { const entries = await fs_1.promises.readdir(geniePath); // Filter out state/ directory and check if anything else exists const nonStateEntries = entries.filter(entry => entry !== 'state'); if (nonStateEntries.length > 0) { // Has other directories/files besides state/ return true; } // Only state/ exists - check if it has any actual content const statePath = path_1.default.join(geniePath, 'state'); if (await (0, fs_utils_1.pathExists)(statePath)) { const stateEntries = await fs_1.promises.readdir(statePath); // Fresh install might have version.json and provider.json from version check // Consider this "empty" since it's just metadata, not user content return false; } // Empty .genie/ directory return false; } catch { // If we can't read it, assume it has content to be safe return true; } } // Legacy selectExecutorAndModel function removed - wizard handles all prompts now // Legacy template choice function removed - wizard handles all prompts now async function writeVersionState(cwd, backupId, _legacyBackedUp) { const versionPath = (0, paths_1.resolveWorkspaceVersionPath)(cwd); const version = (0, package_1.getPackageVersion)(); const now = new Date().toISOString(); const gitCommit = await getGitCommit().catch(() => 'unknown'); // Read existing version data for migration const existing = await fs_1.promises.readFile(versionPath, 'utf8').catch(() => null); let installedAt = now; let previousVersion = null; let upgradeHistory = []; let customizedFiles = []; let deletedFiles = []; if (existing) { try { const parsed = JSON.parse(existing); installedAt = parsed.installedAt ?? now; previousVersion = parsed.version !== version ? parsed.version : (parsed.previousVersion ?? null); // Migrate from old format if (parsed.upgradeHistory) { upgradeHistory = parsed.upgradeHistory; } if (parsed.customizedFiles) { customizedFiles = parsed.customizedFiles; } if (parsed.deletedFiles) { deletedFiles = parsed.deletedFiles; } // Add to upgrade history if version changed if (parsed.version && parsed.version !== version) { upgradeHistory.push({ from: parsed.version, to: version, date: now, success: true }); } } catch { installedAt = now; } } // Write unified version.json (single source of truth) await (0, fs_utils_1.writeJsonFile)(versionPath, { version, installedAt, updatedAt: now, commit: gitCommit, packageName: 'automagik-genie', customizedFiles, deletedFiles, lastUpgrade: previousVersion ? now : null, previousVersion, upgradeHistory, // Keep migrationInfo for backward compatibility (will be removed in future) migrationInfo: { backupId: backupId ?? 'n/a', claudeBackedUp: false } }); } async function getGitCommit() { const { execSync } = await import('child_process'); try { return execSync('git rev-parse --short HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); } catch { return 'unknown'; } } /** * Create git checkpoint commit after backup but before template modifications * * Purpose: Provides clean rollback point if upgrade fails * * @param cwd - Workspace directory * @param oldVersion - Version before upgrade * @param newVersion - Version after upgrade * @param backupId - Backup timestamp ID */ async function createUpgradeCheckpoint(cwd, oldVersion, newVersion, backupId) { const { execSync } = await import('child_process'); try { // Verify git repo exists execSync('git rev-parse --git-dir', { cwd, stdio: 'pipe' }); // Check for uncommitted changes in .genie/ const status = execSync('git status --porcelain .genie/', { cwd, encoding: 'utf8' }); if (status.trim()) { // Stage .genie/ changes execSync('git add .genie/', { cwd, stdio: 'pipe' }); // Create checkpoint commit // chore(upgrade): prefix = exempt from traceability requirements (commit-advisory.cjs:345) // GENIE_DISABLE_COAUTHOR=1 = no co-author attribution (system operation, not Genie authoring) const message = `chore(upgrade): checkpoint before upgrading from ${oldVersion} to ${newVersion}\n\nBackup ID: ${backupId}`; execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd, stdio: 'pipe', env: { ...process.env, GENIE_DISABLE_COAUTHOR: '1' // System operation, not Genie authoring } }); console.log('✓ Checkpoint commit created'); } else { console.log('✓ No changes to checkpoint (clean state)'); } } catch (err) { // Non-fatal: not a git repo or git command failed console.log('âš ī¸ Skipped checkpoint commit (git not available)'); } } async function initializeProviderStatus(cwd) { const statusPath = (0, paths_1.resolveProviderStatusPath)(cwd); const existing = await (0, fs_utils_1.pathExists)(statusPath); if (!existing) { await (0, fs_utils_1.writeJsonFile)(statusPath, { entries: [] }); } } /** * Auto-commit template files after init completes * * Creates checkpoint commit with all Genie template files * Non-fatal: skips if not a git repo or commit fails */ async function commitTemplateFiles(cwd) { // Only commit if git repo exists if (!await (0, fs_utils_1.pathExists)(path_1.default.join(cwd, '.git'))) { console.warn('âš ī¸ No git repository - template files not committed'); console.log(' Run: git init && git add .genie/ AGENTS.md CLAUDE.md && git commit -m "chore: init Genie"'); console.log(''); return false; } const { execSync } = await import('child_process'); try { // Check if there are files to commit const status = execSync('git status --porcelain .genie/ AGENTS.md CLAUDE.md .gitignore', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); if (!status.trim()) { // No changes to commit - files already committed console.log('✓ Template files already committed'); console.log(''); return true; } // Stage Genie template files execSync('git add .genie/ AGENTS.md CLAUDE.md .gitignore', { cwd, stdio: 'pipe' }); // Create init commit // chore(init): prefix = exempt from traceability requirements // GENIE_DISABLE_COAUTHOR=1 = no co-author attribution (system operation) const message = 'chore(init): initialize Genie framework'; execSync(`git commit -m "${message}"`, { cwd, stdio: 'pipe', env: { ...process.env, GENIE_DISABLE_COAUTHOR: '1' // System operation, not Genie authoring } }); console.log('✓ Template files committed to git'); console.log(''); return true; } catch (err) { console.error('❌ Failed to commit template files:', err.message); console.log(' Install task may fail without committed files'); console.log(' Manually commit: git add .genie/ AGENTS.md CLAUDE.md && git commit -m "init"'); console.log(''); return false; } } function buildInitSummaryView(summary, includeInstallMessage = true) { const messages = [ `✅ Installed Genie template at ${summary.target}`, `🔌 Default executor: ${summary.executor}${summary.model ? ` (model: ${summary.model})` : ''}` ]; // Only show backup ID if there was actually a backup if (summary.backupId) { messages.push(`💾 Backup ID: ${summary.backupId}`); } // Only show template source in verbose mode if (process.env.GENIE_VERBOSE) { messages.push(`📚 Template source: ${summary.templateSource}`); } if (includeInstallMessage) { messages.push(`đŸ› ī¸ Started Install agent via Genie run`); } return (0, common_1.buildInfoView)('Genie initialization complete', messages.filter(Boolean)); } async function detectTemplateFromGenie(genieRoot) { // Detect template from .genie structure (code or create collective) const codeExists = await (0, fs_utils_1.pathExists)(path_1.default.join(genieRoot, 'code')); const createExists = await (0, fs_utils_1.pathExists)(path_1.default.join(genieRoot, 'create')); if (codeExists) return 'code'; if (createExists) return 'create'; return 'code'; // fallback } /** * Detect ALL installed collectives from existing .genie/ structure * Used during upgrades to preserve user's collective selection */ async function detectInstalledCollectives(genieRoot) { const collectives = []; // Check for known collective directories const knownCollectives = ['code', 'create']; for (const collective of knownCollectives) { const collectivePath = path_1.default.join(genieRoot, collective); if (await (0, fs_utils_1.pathExists)(collectivePath)) { collectives.push(collective); } } // Fallback if no collectives detected (corrupted .genie/) if (collectives.length === 0) { console.warn('âš ī¸ No collectives detected, defaulting to code'); return ['code']; } return collectives; } async function applyExecutorDefaults(genieRoot, executorKey, model) { await Promise.all([ updateProjectConfig(genieRoot, executorKey, model), updateAgentsForExecutor(genieRoot, executorKey, model) ]); } async function updateProjectConfig(genieRoot, executorKey, model) { // Prefer project-level .genie/config.yaml; fallback to legacy .genie/cli/config.yaml (user workspace) const primaryConfigPath = path_1.default.join(genieRoot, 'config.yaml'); const legacyConfigPath = path_1.default.join(genieRoot, 'cli', 'config.yaml'); const configPath = (await fs_1.promises .access(primaryConfigPath) .then(() => true) .catch(() => false)) ? primaryConfigPath : legacyConfigPath; const exists = await fs_1.promises .access(configPath) .then(() => true) .catch(() => false); if (!exists) { return; } const original = await fs_1.promises.readFile(configPath, 'utf8'); let updated = original; // defaults.executor updated = replaceFirst(updated, /(defaults:\s*\n\s*executor:\s*)([^\s#]+)/, `$1${executorKey}`); // executionModes.default block updated = replaceFirst(updated, /(executionModes:\s*\n default:\s*\n(?:(?: {4}.+\n)+?))/, // capture default block (match) => { let block = match; block = replaceFirst(block, /( description:\s*)(.*)/, `$1${DEFAULT_MODE_DESCRIPTION}`); block = replaceFirst(block, /( executor:\s*)([^\s#]+)/, `$1${executorKey}`); if (model) block = replaceFirst(block, /( model:\s*)([^\s#]+)/, `$1${model}`); return block; }); if (updated !== original) { await fs_1.promises.writeFile(configPath, updated, 'utf8'); } } async function updateAgentsForExecutor(genieRoot, executor, model) { const agentsDir = path_1.default.join(genieRoot, 'agents'); // Skip if agents directory doesn't exist (blacklisted during init) const agentsDirExists = await (0, fs_utils_1.pathExists)(agentsDir); if (!agentsDirExists) { return; } const files = await collectAgentFiles(agentsDir); await Promise.all(files.map(async (file) => { const original = await fs_1.promises.readFile(file, 'utf8'); if (!original.startsWith('---')) return; const end = original.indexOf('\n---', 3); if (end === -1) return; const frontMatterContent = original.slice(4, end); let data; try { data = yaml_1.default.parse(frontMatterContent) || {}; } catch { return; // skip files with invalid front matter } if (!data || typeof data !== 'object') return; if (!data.genie || typeof data.genie !== 'object') { data.genie = {}; } const genieMeta = data.genie; genieMeta.executor = executor; if (model) genieMeta.model = model; const nextFrontMatter = yaml_1.default.stringify(data, { indent: 2 }).trimEnd(); const nextContent = `---\n${nextFrontMatter}\n---${original.slice(end + 4)}`; if (nextContent !== original) { await fs_1.promises.writeFile(file, nextContent, 'utf8'); } })); } async function collectAgentFiles(dir) { const entries = await fs_1.promises.readdir(dir, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path_1.default.join(dir, entry.name); if (entry.isDirectory()) { const nested = await collectAgentFiles(fullPath); files.push(...nested); continue; } if (!entry.isFile()) continue; if (!entry.name.endsWith('.md')) continue; if (entry.name.toLowerCase() === 'README.md'.toLowerCase()) continue; files.push(fullPath); } return files; } /** * Install git hooks if user opted in during wizard */ async function installGitHooksIfRequested(packageRoot, shouldInstall) { if (!shouldInstall) { return; } console.log(''); console.log('🔧 Installing git hooks...'); // Copy hook dependencies before installing hooks const projectDir = process.cwd(); const targetGenie = path_1.default.join(projectDir, '.genie'); // Copy hooks directory (git hook templates) const hooksSource = path_1.default.join(packageRoot, '.genie', 'scripts', 'hooks'); const hooksTarget = path_1.default.join(targetGenie, 'scripts', 'hooks'); if (await (0, fs_utils_1.pathExists)(hooksSource)) { await (0, fs_utils_1.copyDirectory)(hooksSource, hooksTarget); } // Copy hook dependencies (scripts that hooks call) const hookDependencies = [ 'commit-advisory.cjs', 'forge-task-link.cjs', 'prevent-worktree-access.sh', 'run-tests.cjs', 'update-changelog.cjs', 'validate-cross-references.cjs', 'validate-mcp-build.cjs', 'validate-user-files-not-committed.cjs', ]; for (const script of hookDependencies) { const src = path_1.default.join(packageRoot, '.genie', 'scripts', script); const dst = path_1.default.join(targetGenie, 'scripts', script); if (await (0, fs_utils_1.pathExists)(src)) { await fs_1.promises.copyFile(src, dst); } } // Copy token-efficiency directory (used by pre-commit hook) const tokenEffSource = path_1.default.join(packageRoot, '.genie', 'scripts', 'token-efficiency'); const tokenEffTarget = path_1.default.join(targetGenie, 'scripts', 'token-efficiency'); if (await (0, fs_utils_1.pathExists)(tokenEffSource)) { await (0, fs_utils_1.copyDirectory)(tokenEffSource, tokenEffTarget); } // Copy install-hooks.cjs (used by hook installer) const installHooksSource = path_1.default.join(packageRoot, '.genie', 'scripts', 'install-hooks.cjs'); const installHooksTarget = path_1.default.join(targetGenie, 'scripts', 'install-hooks.cjs'); if (await (0, fs_utils_1.pathExists)(installHooksSource)) { await fs_1.promises.copyFile(installHooksSource, installHooksTarget); } const { spawnSync } = await import('child_process'); const installScript = path_1.default.join(targetGenie, 'scripts', 'install-hooks.cjs'); const result = spawnSync('node', [installScript, projectDir, packageRoot], { stdio: 'inherit' }); if (result.status !== 0) { console.warn('âš ī¸ Hook installation failed (non-fatal)'); console.warn(` You can install later with: node ${installScript} ${projectDir}