UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

995 lines (891 loc) 39 kB
/** * AIOS Interactive Wizard - Main Entry Point * * Story 1.2: Interactive Wizard Foundation * Provides core wizard functionality with visual feedback and navigation * * @module wizard */ const inquirer = require('inquirer'); const path = require('path'); const fse = require('fs-extra'); const { execSync } = require('child_process'); const { colors } = require('../utils/aios-colors'); const { getLanguageQuestion, getUserProfileQuestion, getProjectTypeQuestion, getIDEQuestions, getTechPresetQuestion, } = require('./questions'); const { setLanguage, t } = require('./i18n'); const yaml = require('js-yaml'); const { showWelcome, showCompletion, showCancellation } = require('./feedback'); const { generateIDEConfigs, showSuccessSummary, copySkillFiles, copyExtraCommandFiles } = require('./ide-config-generator'); const { configureEnvironment, } = require('../config/configure-environment'); const { installDependencies, } = require('../installer/dependency-installer'); const { commandSync, commandValidate } = require('../../../../.aios-core/infrastructure/scripts/ide-sync/index'); const { installAiosCore, hasPackageJson, } = require('../installer/aios-core-installer'); const { validateInstallation, displayValidationReport, provideTroubleshooting, } = require('./validation'); const { installLLMRouting, isLLMRoutingInstalled, } = require('../../../../.aios-core/infrastructure/scripts/llm-routing/install-llm-routing'); // DISABLED: Legacy installation block superseded by squads flow (OSR-8) // /** // * Generate AntiGravity workflow content for squad agents // * @param {string} agentName - Agent name (e.g., 'data-collector') // * @param {string} packName - Starter squad name (e.g., 'etl') // * @returns {string} Workflow file content // */ // function generateExpansionPackWorkflow(agentName, packName) { // const displayName = agentName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); // // return `--- // description: Ativa o agente ${displayName} (${packName}) // --- // // # Ativação do Agente ${displayName} // // **Squad:** ${packName} // // **INSTRUÇÕES CRÍTICAS PARA O ANTIGRAVITY:** // // 1. Leia COMPLETAMENTE o arquivo \`.antigravity/agents/${packName}/${agentName}.md\` // 2. Siga EXATAMENTE as \`activation-instructions\` definidas no bloco YAML do agente // 3. Adote a persona conforme definido no agente // 4. Execute a saudação conforme \`greeting_levels\` definido no agente // 5. **MANTENHA esta persona até receber o comando \`*exit\`** // 6. Responda aos comandos com prefixo \`*\` conforme definido no agente // 7. Siga as regras globais do projeto em \`.antigravity/rules.md\` // // **Comandos disponíveis:** Use \`*help\` para ver todos os comandos do agente. // `; // } /** * Check for existing user_profile in core-config.yaml (Story 10.2 - Idempotency) * Returns the existing profile if found, null otherwise * * @param {string} targetDir - Target directory to check * @returns {Promise<string|null>} Existing user profile or null */ async function getExistingUserProfile(targetDir = process.cwd()) { const coreConfigPath = path.join(targetDir, '.aios-core', 'core-config.yaml'); try { if (await fse.pathExists(coreConfigPath)) { const content = await fse.readFile(coreConfigPath, 'utf8'); const config = yaml.load(content); if (config && config.user_profile) { // Validate the value const validProfiles = ['bob', 'advanced']; const normalizedProfile = String(config.user_profile).toLowerCase().trim(); if (validProfiles.includes(normalizedProfile)) { return normalizedProfile; } } } } catch { // Config doesn't exist or is invalid - will ask for profile } return null; } /** * Map wizard language code to Claude Code settings.json language name (Story ACT-12) * Claude Code uses full language names, not ISO codes. */ const LANGUAGE_MAP = { en: 'english', pt: 'portuguese', es: 'spanish', }; /** * Write language preference to Claude Code's native settings.json (Story ACT-12) * Replaces the old approach of storing language in core-config.yaml. * Claude Code v4.0.4+ natively supports a `language` field in settings.json * that is automatically injected into the system prompt. * * @param {string} language - Language code from wizard (en|pt|es) * @param {string} [projectDir] - Project directory (default: process.cwd()) * @returns {Promise<boolean>} true if written successfully */ async function writeClaudeSettings(language, projectDir = process.cwd()) { const claudeDir = path.join(projectDir, '.claude'); const settingsPath = path.join(claudeDir, 'settings.json'); try { await fse.ensureDir(claudeDir); let settings = {}; if (await fse.pathExists(settingsPath)) { const content = await fse.readFile(settingsPath, 'utf8'); settings = JSON.parse(content); } const claudeLanguage = LANGUAGE_MAP[language] || language; settings.language = claudeLanguage; await fse.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); return true; } catch { // Non-blocking: language is a preference, not critical return false; } } /** * Get existing language from Claude Code settings.json (Story ACT-12 - Idempotency) * Returns the existing language code if found, null otherwise. * * @param {string} [projectDir] - Project directory to check * @returns {Promise<string|null>} Existing language code or null */ async function getExistingLanguage(projectDir = process.cwd()) { const settingsPath = path.join(projectDir, '.claude', 'settings.json'); try { if (await fse.pathExists(settingsPath)) { const content = await fse.readFile(settingsPath, 'utf8'); const settings = JSON.parse(content); if (settings && settings.language) { // Reverse map: Claude Code language name → wizard code const reverseMap = Object.fromEntries( Object.entries(LANGUAGE_MAP).map(([k, v]) => [v, k]), ); const langValue = String(settings.language).toLowerCase().trim(); return reverseMap[langValue] || null; } } } catch { // Settings don't exist or invalid JSON } return null; } /** * Handle Ctrl+C gracefully */ let cancellationRequested = false; let sigintHandlerAdded = false; function setupCancellationHandler() { // Prevent adding multiple listeners (MaxListeners warning fix) if (sigintHandlerAdded) { return; } // Increase limit to prevent warning during testing process.setMaxListeners(15); const handleSigint = async () => { if (cancellationRequested) { // Second Ctrl+C - force exit console.log('\nForce exit'); process.exit(0); } cancellationRequested = true; console.log('\n'); const { t: translate } = require('./i18n'); const { confirmCancel } = await inquirer.prompt([ { type: 'confirm', name: 'confirmCancel', message: translate('cancelConfirm'), default: false, }, ]); if (confirmCancel) { showCancellation(); process.exit(0); } else { cancellationRequested = false; console.log(translate('continuing') + '\n'); // Note: inquirer will resume automatically } }; process.on('SIGINT', handleSigint); sigintHandlerAdded = true; } /** * Main wizard execution function * * @returns {Promise<Object>} Wizard answers object * * @example * const { runWizard } = require('./src/wizard'); * const answers = await runWizard(); * console.log(answers.projectType); // 'greenfield' or 'brownfield' */ async function runWizard(options = {}) { try { // Setup graceful cancellation setupCancellationHandler(); // Show welcome message with AIOS branding if (!options.quiet) { showWelcome(); } // Start i18n with default or detected language setLanguage(options.language || 'en'); let answers = {}; if (options.quiet) { // Quiet mode: Skip all prompts, use defaults // Story 10.2: Check for existing user_profile (idempotency) // Story ACT-12: Language delegated to Claude Code settings.json const existingProfile = await getExistingUserProfile(); const existingLang = await getExistingLanguage(); answers = { language: options.language || existingLang || 'en', userProfile: options.userProfile || existingProfile || 'advanced', // Story 10.2 projectType: options.projectType || 'brownfield', // Default to brownfield for safety selectedIDEs: options.ide ? [options.ide] : [], // Support single IDE flag if added later selectedTechPreset: 'none', ...options, // Merge any other options }; } else { // Interactive mode // Phase 1: Language selection (must be first to apply i18n) // Story ACT-12: Check idempotency via Claude Code settings.json let languageAnswer; const existingLanguage = await getExistingLanguage(); if (existingLanguage) { // Idempotent: Use existing language, don't re-ask console.log(`\n✓ ${t('languageSkipped') || 'Language already configured'}: ${existingLanguage}\n`); languageAnswer = { language: existingLanguage }; } else { languageAnswer = await inquirer.prompt([getLanguageQuestion()]); } setLanguage(languageAnswer.language); // Phase 1.5: User Profile selection (Story 10.2 - Epic 10) // Check for idempotency - if user_profile already exists, skip question let userProfileAnswer = {}; const existingProfile = await getExistingUserProfile(); if (existingProfile) { // Idempotent: Use existing profile, don't re-ask console.log(`\n✓ ${t('userProfileSkipped')}: ${existingProfile}\n`); userProfileAnswer = { userProfile: existingProfile }; } else { // New installation: Ask for user profile userProfileAnswer = await inquirer.prompt([getUserProfileQuestion()]); } // Phase 2: Build remaining questions with i18n applied const remainingQuestions = [ getProjectTypeQuestion(), ...getIDEQuestions(), ...getTechPresetQuestion(), ]; // Performance tracking (AC: < 100ms per question) const startTime = Date.now(); // Run wizard with remaining questions const remainingAnswers = await inquirer.prompt(remainingQuestions); // Merge all answers (including user profile from Story 10.2) answers = { ...languageAnswer, ...userProfileAnswer, ...remainingAnswers }; // Log performance metrics const duration = Date.now() - startTime; const totalQuestions = remainingQuestions.length + 2; // +1 for language, +1 for user profile const avgTimePerQuestion = totalQuestions > 0 ? duration / totalQuestions : 0; if (avgTimePerQuestion > 100) { console.warn( `Warning: Average question response time (${avgTimePerQuestion.toFixed(0)}ms) exceeds 100ms target`, ); } } // Story 1.4: Install AIOS core framework (agents, tasks, workflows, templates) console.log('\n📦 Installing AIOS core framework...'); let aiosCoreResult = null; try { aiosCoreResult = await installAiosCore({ targetDir: process.cwd(), onProgress: (_status) => { // Silent progress - spinner handles feedback }, }); if (aiosCoreResult.success) { console.log(`✅ AIOS core installed (${aiosCoreResult.installedFolders.length} folders)`); console.log( ` - Agents: ${aiosCoreResult.installedFolders.includes('agents') ? '✓' : '⨉'}`, ); console.log(` - Tasks: ${aiosCoreResult.installedFolders.includes('tasks') ? '✓' : '⨉'}`); console.log( ` - Workflows: ${aiosCoreResult.installedFolders.includes('workflows') ? '✓' : '⨉'}`, ); console.log( ` - Templates: ${aiosCoreResult.installedFolders.includes('templates') ? '✓' : '⨉'}`, ); } answers.aiosCoreInstalled = true; answers.aiosCoreResult = aiosCoreResult; } catch (error) { console.error('\n⚠️ AIOS core installation failed:', error.message); answers.aiosCoreInstalled = false; } // Install Tech Preset if selected if (answers.selectedTechPreset && answers.selectedTechPreset !== 'none') { console.log('\n📐 Configuring Tech Preset...'); try { // Find tech-presets source directory const possiblePresetDirs = [ path.join(__dirname, '..', '..', '.aios-core', 'data', 'tech-presets'), path.join(process.cwd(), '.aios-core', 'data', 'tech-presets'), ]; let sourcePresetDir = null; for (const dir of possiblePresetDirs) { if (fse.existsSync(dir)) { sourcePresetDir = dir; break; } } if (sourcePresetDir) { const presetFile = path.join(sourcePresetDir, `${answers.selectedTechPreset}.md`); if (fse.existsSync(presetFile)) { // Copy preset to project's .aios-core/data/tech-presets/ const targetPresetDir = path.join(process.cwd(), '.aios-core', 'data', 'tech-presets'); await fse.ensureDir(targetPresetDir); // BUG-5 fix (INS-1): Guard against source === dest (e.g., running inside aios-core repo) const targetPresetFile = path.join(targetPresetDir, `${answers.selectedTechPreset}.md`); const sourceResolved = path.resolve(presetFile); const targetResolved = path.resolve(targetPresetFile); if (sourceResolved === targetResolved) { console.log(' ℹ️ Tech preset already in place (framework-dev mode)'); } else { // Copy the selected preset await fse.copy(presetFile, targetPresetFile); // Copy the template too const templateFile = path.join(sourcePresetDir, '_template.md'); if (fse.existsSync(templateFile)) { const targetTemplate = path.join(targetPresetDir, '_template.md'); if (path.resolve(templateFile) !== path.resolve(targetTemplate)) { await fse.copy(templateFile, targetTemplate); } } // Update technical-preferences.md to mark the selected preset const techPrefsFile = path.join( process.cwd(), '.aios-core', 'data', 'technical-preferences.md', ); const techPrefsSource = path.join(sourcePresetDir, '..', 'technical-preferences.md'); if (fse.existsSync(techPrefsSource)) { const techPrefsSourceResolved = path.resolve(techPrefsSource); const techPrefsTargetResolved = path.resolve(techPrefsFile); if (techPrefsSourceResolved !== techPrefsTargetResolved) { // Prefer existing target file to preserve user customizations const baseFile = fse.existsSync(techPrefsFile) ? techPrefsFile : techPrefsSource; let techPrefsContent = await fse.readFile(baseFile, 'utf8'); // Add active preset marker only if not already present const activePresetSection = `\n## Active Preset\n\n**Selected:** \`${answers.selectedTechPreset}\`\n\nThis preset was selected during installation. The @architect and @dev agents will use these patterns by default.\n`; if (!techPrefsContent.includes('## Active Preset')) { // Insert after the first heading techPrefsContent = techPrefsContent.replace( '# User-Defined Preferred Patterns and Preferences', '# User-Defined Preferred Patterns and Preferences' + activePresetSection, ); await fse.writeFile(techPrefsFile, techPrefsContent, 'utf8'); } } } } console.log(` ✅ Tech Preset: ${answers.selectedTechPreset}`); console.log( ` 📁 Location: .aios-core/data/tech-presets/${answers.selectedTechPreset}.md`, ); answers.techPresetInstalled = true; answers.techPresetResult = { preset: answers.selectedTechPreset, success: true }; } else { console.log(` ⚠️ Preset file not found: ${answers.selectedTechPreset}`); answers.techPresetInstalled = false; } } else { console.log(' ⚠️ Tech presets directory not found'); answers.techPresetInstalled = false; } } catch (error) { console.error(` ⚠️ Tech Preset error: ${error.message}`); answers.techPresetInstalled = false; } } else { answers.techPresetInstalled = false; answers.techPresetResult = { preset: 'none', success: true }; } // Legacy squad installation path removed; unified squads flow is now the only supported path. // Story 1.4: Generate IDE configs if IDEs were selected let ideConfigResult = null; if (answers.selectedIDEs && answers.selectedIDEs.length > 0) { // Pass merge options from CLI to IDE config generator (Story 9.4) const ideOptions = { ...answers, forceMerge: options.forceMerge, noMerge: options.noMerge, }; ideConfigResult = await generateIDEConfigs(answers.selectedIDEs, ideOptions); if (ideConfigResult.success) { showSuccessSummary(ideConfigResult); } else { console.error('\n⚠️ Some IDE configurations could not be created:'); if (ideConfigResult.errors) { ideConfigResult.errors.forEach((err) => { console.error(` - ${err.ide || 'Unknown'}: ${err.error}`); }); } } // Legacy per-squad IDE copy path removed; sync pipeline handles IDE propagation. } // Story INS-4.3: Wire settings.json boundary generator after .aios-core/ copy console.log('\n🔒 Generating boundary rules...'); try { const settingsGenerator = require('../../../../.aios-core/infrastructure/scripts/generate-settings-json'); settingsGenerator.generate(process.cwd()); const settingsContent = await fse.readFile(path.join(process.cwd(), '.claude', 'settings.json'), 'utf8').catch(() => '{}'); const settingsParsed = JSON.parse(settingsContent); const denyCount = (settingsParsed.permissions && settingsParsed.permissions.deny) ? settingsParsed.permissions.deny.length : 0; console.log(`✅ settings.json: generated (${denyCount} deny rules)`); answers.settingsGenerated = true; answers.settingsDenyCount = denyCount; } catch (error) { console.warn(`⚠️ settings.json generation failed: ${error.message} — run 'aios doctor --fix' post-install`); answers.settingsGenerated = false; } // Story INS-4.3: Copy skills (Gap #11) console.log('\n📚 Copying skills...'); try { const skillsResult = await copySkillFiles(process.cwd()); if (skillsResult.skipped) { console.log(' ℹ️ Skills: source not found (skipped)'); } else { console.log(`✅ Skills: ${skillsResult.count} copied`); } answers.skillsCopied = skillsResult.count; answers.skillsSkipped = skillsResult.skipped; } catch (error) { console.warn(`⚠️ Skills copy failed: ${error.message}`); answers.skillsCopied = 0; } // Story INS-4.3: Copy extra commands (Gap #12) console.log('\n📋 Copying extra commands...'); try { const commandsResult = await copyExtraCommandFiles(process.cwd()); if (commandsResult.skipped) { console.log(' ℹ️ Extra commands: source not found (skipped)'); } else { console.log(`✅ Commands: ${commandsResult.count} extras copied`); } answers.extraCommandsCopied = commandsResult.count; answers.extraCommandsSkipped = commandsResult.skipped; } catch (error) { console.warn(`⚠️ Extra commands copy failed: ${error.message}`); answers.extraCommandsCopied = 0; } // Story INS-4.5: IDE Sync — transform agents/skills/commands for each configured IDE console.log('\n🔄 Running IDE sync...'); const targetProjectRoot = process.cwd(); const savedCwd = process.cwd(); try { process.chdir(targetProjectRoot); await commandSync({ quiet: true }); answers.ideSyncStatus = 'synced'; console.log('✅ IDE sync: synced'); // Validate sync output (commandValidate does not support quiet — suppress its console output) const _origLog = console.log; console.log = () => {}; try { await commandValidate({ quiet: true }); answers.ideSyncValidation = 'pass'; } catch (validateError) { answers.ideSyncValidation = 'drift'; } finally { console.log = _origLog; } if (answers.ideSyncValidation === 'drift') { console.warn('⚠️ IDE sync validation: drift detected — run \'aios doctor --fix\' post-install'); } } catch (syncError) { console.warn(`⚠️ IDE sync failed: ${syncError.message} — run 'aios doctor --fix' post-install`); answers.ideSyncStatus = 'failed'; answers.ideSyncValidation = 'skipped'; } finally { process.chdir(savedCwd); } // Story INS-4.6: Entity Registry Bootstrap — populate entity-registry.yaml on install // Story INS-4.12: Fix module resolution + bootstrap timing // Bootstrap runs AFTER .aios-core deps are installed (aios-core-installer.js:324-345) // NODE_PATH ensures spawned scripts can resolve packages from .aios-core/node_modules/ console.log('\n📇 Bootstrapping entity registry...'); try { const registryScript = path.join(process.cwd(), '.aios-core', 'development', 'scripts', 'populate-entity-registry.js'); if (fse.existsSync(registryScript)) { // INS-4.12 AC3: Guard — skip bootstrap if .aios-core deps are not installed const aiosCoreNodeModules = path.join(process.cwd(), '.aios-core', 'node_modules'); if (!fse.existsSync(aiosCoreNodeModules)) { console.warn('⚠️ .aios-core/node_modules/ not found — skipping entity registry bootstrap'); console.warn(' Run: cd .aios-core && npm install --production'); answers.entityRegistryStatus = 'skipped-no-deps'; } else { // INS-4.12 AC2: Set NODE_PATH so spawned scripts resolve deps from .aios-core/node_modules/ const parentNodeModules = path.join(process.cwd(), 'node_modules'); const nodePath = [aiosCoreNodeModules, parentNodeModules].join(path.delimiter); const startMs = Date.now(); execSync(`node "${registryScript}"`, { cwd: process.cwd(), encoding: 'utf8', timeout: 30000, stdio: 'pipe', env: { ...process.env, NODE_PATH: nodePath }, }); const elapsedMs = Date.now() - startMs; // Read entity count from generated registry const registryPath = path.join(process.cwd(), '.aios-core', 'data', 'entity-registry.yaml'); let entityCount = 0; if (fse.existsSync(registryPath)) { const registryContent = fse.readFileSync(registryPath, 'utf8'); const countMatch = registryContent.match(/entityCount:\s*(\d+)/); entityCount = countMatch ? parseInt(countMatch[1], 10) : 0; } console.log(`✅ Entity registry: populated (${entityCount} entities, ${(elapsedMs / 1000).toFixed(1)}s)`); answers.entityRegistryStatus = 'populated'; answers.entityRegistryCount = entityCount; answers.entityRegistryMs = elapsedMs; } // end else (deps exist) } else { console.log(' ℹ️ Entity registry script not found (skipped)'); answers.entityRegistryStatus = 'skipped'; } } catch (error) { console.warn(`⚠️ Entity registry bootstrap failed: ${error.message} — run 'aios doctor' post-install`); answers.entityRegistryStatus = 'failed'; } // Story 1.6: Environment Configuration console.log('\n📝 Configuring environment...'); try { const envResult = await configureEnvironment({ targetDir: process.cwd(), projectType: answers.projectType || 'greenfield', selectedIDEs: answers.selectedIDEs || [], mcpServers: answers.mcpServers || [], userProfile: answers.userProfile || 'advanced', // Story 10.2: User Profile skipPrompts: options.quiet || false, // Skip prompts in quiet mode forceMerge: options.forceMerge, // Story 9.4: Smart Merge support noMerge: options.noMerge, // Story 9.4: Smart Merge support }); // Story ACT-12: Write language to Claude Code settings.json if (answers.language) { const langWritten = await writeClaudeSettings(answers.language); if (langWritten) { console.log(' - Language written to .claude/settings.json'); } else { console.warn(' - Failed to write language to .claude/settings.json'); } } if (envResult.envCreated && envResult.coreConfigCreated) { console.log('\n✅ Environment configuration complete!'); console.log(' - .env file created'); console.log(' - .env.example file created'); console.log(' - .aios-core/core-config.yaml created'); if (envResult.gitignoreUpdated) { console.log(' - .gitignore updated'); } } // Store env config result for downstream stories answers.envConfigured = true; answers.envResult = envResult; } catch (error) { console.error('\n⚠️ Environment configuration failed:'); console.error(` ${error.message}`); // Ask user if they want to continue without env config const { continueWithoutEnv } = await inquirer.prompt([ { type: 'confirm', name: 'continueWithoutEnv', message: 'Continue installation without environment configuration?', default: false, }, ]); if (!continueWithoutEnv) { throw new Error('Installation cancelled - environment configuration required'); } answers.envConfigured = false; console.log('\n⚠️ Continuing without environment configuration...'); } // Story 1.7: Dependency Installation // Check if package.json exists first (greenfield projects won't have one) const { detectPackageManager } = require('../installer/dependency-installer'); const projectPath = process.cwd(); const packageJsonExists = await hasPackageJson(projectPath); if (!packageJsonExists) { // Greenfield project - no package.json, skip dependency installation console.log('\n📦 Dependency installation...'); console.log(' ℹ️ No package.json found (greenfield project)'); console.log(' 💡 Dependencies will be installed when you add a package.json'); answers.depsInstalled = true; // Mark as success since there's nothing to install answers.depsResult = { success: true, skipped: true, reason: 'no-package-json' }; answers.packageManager = detectPackageManager(); } else { // Brownfield project or existing project - has package.json console.log('\n📦 Installing dependencies...'); // Auto-detect package manager (no longer asked as question) const detectedPM = detectPackageManager(); answers.packageManager = detectedPM; try { const depsResult = await installDependencies({ packageManager: detectedPM, projectPath: projectPath, }); if (depsResult.success) { if (depsResult.offlineMode) { console.log('✅ Using existing dependencies (offline mode)'); } else { console.log(`✅ Dependencies installed with ${depsResult.packageManager}!`); } answers.depsInstalled = true; answers.depsResult = depsResult; } else { console.error('\n⚠️ Dependency installation failed:'); console.error(` ${depsResult.errorMessage}`); console.error(` Solution: ${depsResult.solution}`); // Ask user if they want to retry const { retryDeps } = await inquirer.prompt([ { type: 'confirm', name: 'retryDeps', message: 'Retry dependency installation?', default: true, }, ]); if (retryDeps) { // Recursive retry with exponential backoff (built into installDependencies) const retryResult = await installDependencies({ packageManager: answers.packageManager, projectPath: projectPath, }); if (retryResult.success) { console.log(`\n✅ Dependencies installed with ${retryResult.packageManager}!`); answers.depsInstalled = true; answers.depsResult = retryResult; } else { console.log( '\n⚠️ Installation still failed. You can run `npm install` manually later.', ); answers.depsInstalled = false; answers.depsResult = retryResult; } } else { console.log('\n⚠️ Skipping dependency installation. Run manually with `npm install`.'); answers.depsInstalled = false; answers.depsResult = depsResult; } } } catch (error) { console.error('\n⚠️ Dependency installation error:', error.message); answers.depsInstalled = false; } } // DISABLED: MCPs are advanced config that can confuse beginners // TODO: Remove entirely in future version - each project has unique MCP needs // Story 1.5/1.8: MCP Installation // if (answers.selectedMCPs && answers.selectedMCPs.length > 0) { // console.log('\n🔌 Installing MCPs...'); // // try { // const mcpResult = await installProjectMCPs({ // selectedMCPs: answers.selectedMCPs, // projectPath: process.cwd(), // apiKeys: answers.exaApiKey ? { EXA_API_KEY: answers.exaApiKey } : {}, // onProgress: (status) => { // if (status.mcp) { // console.log(` [${status.mcp}] ${status.message}`); // } else { // console.log(` ${status.message}`); // } // }, // }); // // if (mcpResult.success) { // const successCount = Object.values(mcpResult.installedMCPs).filter(r => r.status === 'success').length; // console.log(`\n✅ MCPs installed successfully! (${successCount}/${answers.selectedMCPs.length})`); // console.log(` Configuration: ${mcpResult.configPath}`); // } else { // console.error('\n⚠️ Some MCPs failed to install:'); // mcpResult.errors.forEach(err => console.error(` - ${err}`)); // console.log('\n💡 Check .aios/install-errors.log for details'); // } // // // Store MCP result for validation // answers.mcpsInstalled = mcpResult.success; // answers.mcpResult = mcpResult; // // } catch (error) { // console.error('\n⚠️ MCP installation error:', error.message); // answers.mcpsInstalled = false; // } // } // Story 6.7: LLM Routing Installation console.log('\nInstalling LLM Routing commands...'); try { // Check if already installed if (isLLMRoutingInstalled()) { console.log(' ℹ️ LLM Routing already installed'); answers.llmRoutingInstalled = true; answers.llmRoutingResult = { success: true, alreadyInstalled: true }; } else { const llmResult = installLLMRouting({ projectRoot: process.cwd(), onProgress: (msg) => console.log(` ${msg}`), onError: (msg) => console.error(` ${msg}`), }); if (llmResult.success) { console.log('\n✅ LLM Routing installed!'); console.log(' • claude-max → Uses Claude Max subscription'); console.log(' • claude-free → Uses DeepSeek (~$0.14/M tokens)'); console.log('\n 💡 For claude-free, add DEEPSEEK_API_KEY to your .env'); answers.llmRoutingInstalled = true; answers.llmRoutingResult = llmResult; } else { console.error('\n⚠️ LLM Routing installation had errors:'); llmResult.errors.forEach((err) => console.error(` - ${err}`)); answers.llmRoutingInstalled = false; answers.llmRoutingResult = llmResult; } } } catch (error) { console.error('\n⚠️ LLM Routing error:', error.message); answers.llmRoutingInstalled = false; } // Story INS-3.2: Pro Installation Wizard (optional phase) if (!options.skipPro) { try { const { runProWizard } = require('./pro-setup'); const isCI = process.env.CI === 'true' || !process.stdout.isTTY; const hasProKey = !!process.env.AIOS_PRO_KEY; const proOptions = { targetDir: process.cwd() }; if (isCI && hasProKey) { // CI mode: auto-run if AIOS_PRO_KEY is set console.log('\n🔑 Pro license key detected, running Pro setup...'); const proResult = await runProWizard({ ...proOptions, quiet: true }); answers.proInstalled = proResult.success; answers.proResult = proResult; } else if (!isCI && !options.quiet) { // Interactive mode: ask which edition to install const { edition } = await inquirer.prompt([ { type: 'list', name: 'edition', message: colors.primary('Which edition do you want to install?'), choices: [ { name: 'Community (free) — agents, workflows, squads, full CLI', value: 'community', }, { name: 'Pro (requires account) — premium squads, minds, priority support', value: 'pro', }, ], default: 'community', }, ]); if (edition === 'pro') { const proResult = await runProWizard(proOptions); answers.proInstalled = proResult.success; answers.proResult = proResult; if (!proResult.success && proResult.error) { console.error(`\n⚠️ Pro activation failed: ${proResult.error}`); const { fallback } = await inquirer.prompt([ { type: 'confirm', name: 'fallback', message: colors.primary('Continue with Community (free) edition instead?'), default: true, }, ]); if (!fallback) { console.log('\n👋 Installation cancelled. Run again when ready.'); return answers; } console.log('\n📦 Continuing with Community edition...\n'); } } else { answers.proInstalled = false; } } } catch (error) { console.error(`\n⚠️ Pro setup error: ${error.message}`); answers.proInstalled = false; } } // Story 1.8: Installation Validation console.log('\n🔍 Validating installation...\n'); try { const validation = await validateInstallation( { files: { ideConfigs: ideConfigResult?.files || [], env: '.env', coreConfig: '.aios-core/core-config.yaml', mcpConfig: '.mcp.json', }, configs: { env: answers.envResult, mcps: answers.mcpResult, coreConfig: '.aios-core/core-config.yaml', }, mcps: answers.mcpResult, dependencies: answers.depsResult, }, (status) => { console.log(` [${status.step}] ${status.message}`); }, ); // Display validation report await displayValidationReport(validation); // Offer troubleshooting if there are errors if (validation.errors && validation.errors.length > 0) { await provideTroubleshooting(validation.errors); } // Store validation result answers.validationResult = validation; } catch (error) { console.error('\n⚠️ Validation failed:', error.message); console.log('Installation may be incomplete. Check logs in .aios/ directory.'); } // Show completion showCompletion(); return answers; } catch (error) { if (error.isTtyError) { console.error("Error: Prompt couldn't be rendered in the current environment"); } else { console.error('Wizard error:', error.message); } throw error; } } /** * Answer object schema (for integration documentation) * * @typedef {Object} WizardAnswers * @property {string} projectType - 'greenfield' or 'brownfield' (Story 1.3) * @property {string[]} [selectedIDEs] - Selected IDEs array (Story 1.4) * @property {string[]} [mcpServers] - Selected MCP servers (Story 1.5) * @property {boolean} [envConfigured] - Whether env config succeeded (Story 1.6) * @property {Object} [envResult] - Environment configuration result (Story 1.6) * @property {boolean} envResult.envCreated - .env file created * @property {boolean} envResult.envExampleCreated - .env.example file created * @property {boolean} envResult.coreConfigCreated - core-config.yaml created * @property {boolean} envResult.gitignoreUpdated - .gitignore updated * @property {Array<string>} envResult.errors - Any errors encountered * @property {string} packageManager - Selected package manager (Story 1.7) * @property {boolean} [depsInstalled] - Whether dependencies installed successfully (Story 1.7) * @property {Object} [depsResult] - Dependency installation result (Story 1.7) * @property {boolean} depsResult.success - Installation succeeded * @property {boolean} [depsResult.offlineMode] - Used existing node_modules * @property {string} depsResult.packageManager - Package manager used * @property {string} [depsResult.error] - Error message if failed */ module.exports = { runWizard, // ACT-12: Exported for testing _testing: { writeClaudeSettings, getExistingLanguage, LANGUAGE_MAP, }, };