automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
1,056 lines âĸ 58.9 kB
JavaScript
"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}