UNPKG

aios-core

Version:

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

486 lines (413 loc) 13.1 kB
/** * AIOS Installer - Main installation logic * * Orchestrates the complete AIOS installation flow: * 1. OS detection * 2. Dependency checking * 3. Profile selection (bob vs advanced) * 4. User config setup (L5) * 5. Brownfield detection and migration * 6. Environment bootstrap * * @module installer */ 'use strict'; const path = require('path'); const os = require('os'); const fs = require('fs-extra'); const chalk = require('chalk'); const ora = require('ora'); const { intro, outro, select, confirm, note, isCancel, cancel } = require('@clack/prompts'); const { execa } = require('execa'); const { detectOS, getOSDisplayName } = require('./os-detector'); const { checkAllDependencies, displayResults } = require('./dep-checker'); /** * Installation timing tracker */ class InstallTimer { constructor() { this.startTime = Date.now(); } elapsed() { return Math.round((Date.now() - this.startTime) / 1000); } elapsedFormatted() { const seconds = this.elapsed(); if (seconds < 60) return `${seconds}s`; const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}m ${secs}s`; } checkTimeout(maxSeconds = 300) { return this.elapsed() > maxSeconds; } } /** * Logger with dry-run support */ class InstallLogger { constructor(options = {}) { this.dryRun = options.dryRun || false; this.verbose = options.verbose || false; this.prefix = this.dryRun ? chalk.yellow('[DRY-RUN] ') : ''; } info(message) { console.log(this.prefix + message); } success(message) { console.log(this.prefix + chalk.green('✓ ') + message); } warn(message) { console.log(this.prefix + chalk.yellow('⚠ ') + message); } error(message) { console.log(this.prefix + chalk.red('✗ ') + message); } debug(message) { if (this.verbose) { console.log(this.prefix + chalk.dim(' ' + message)); } } action(message) { if (this.dryRun) { console.log(chalk.yellow('[DRY-RUN] Would: ') + message); } } } /** * Check if a config-resolver is available (aios-core installed) * @param {string} projectRoot - Project root * @returns {Object|null} Config resolver module or null */ function tryLoadConfigResolver(projectRoot) { const possiblePaths = [ path.join(projectRoot, '.aios-core/core/config/config-resolver.js'), path.join(projectRoot, 'node_modules/aios-core/.aios-core/core/config/config-resolver.js'), ]; for (const configPath of possiblePaths) { try { if (fs.existsSync(configPath)) { return require(configPath); } } catch { // Ignore load errors } } return null; } /** * Create default user config directly (fallback when config-resolver not available) * @param {string} profile - User profile ('bob' or 'advanced') * @param {InstallLogger} logger - Logger instance * @param {boolean} dryRun - Whether this is a dry run */ async function createUserConfigDirect(profile, logger, dryRun) { const userConfigDir = path.join(os.homedir(), '.aios'); const userConfigPath = path.join(userConfigDir, 'user-config.yaml'); if (dryRun) { logger.action(`Create directory: ${userConfigDir}`); logger.action(`Write user config: ${userConfigPath}`); logger.action(`Set user_profile: ${profile}`); return; } await fs.ensureDir(userConfigDir); let config = {}; if (await fs.pathExists(userConfigPath)) { try { const yaml = require('js-yaml'); const content = await fs.readFile(userConfigPath, 'utf8'); config = yaml.load(content) || {}; } catch { config = {}; } } config.user_profile = profile; config.educational_mode = profile === 'bob'; const yaml = require('js-yaml'); const yamlContent = yaml.dump(config, { lineWidth: -1 }); await fs.writeFile(userConfigPath, yamlContent, 'utf8'); logger.success(`User config created at ${userConfigPath}`); } /** * Check if this is a brownfield installation (existing AIOS) * @param {string} projectRoot - Project root * @returns {Object} Brownfield detection result */ function detectBrownfield(projectRoot) { const result = { isBrownfield: false, hasLegacyConfig: false, hasLayeredConfig: false, configResolver: null, }; // Check for legacy monolithic config const legacyConfigPath = path.join(projectRoot, '.aios-core/core-config.yaml'); if (fs.existsSync(legacyConfigPath)) { result.isBrownfield = true; result.hasLegacyConfig = true; } // Check for layered config const frameworkConfigPath = path.join(projectRoot, '.aios-core/framework-config.yaml'); if (fs.existsSync(frameworkConfigPath)) { result.isBrownfield = true; result.hasLayeredConfig = true; } // Check for .aios-core directory const aiosCoreDir = path.join(projectRoot, '.aios-core'); if (fs.existsSync(aiosCoreDir)) { result.isBrownfield = true; } // Try to load config resolver result.configResolver = tryLoadConfigResolver(projectRoot); return result; } /** * Run the AIOS doctor command * @param {string} projectRoot - Project root * @param {InstallLogger} logger - Logger instance * @param {boolean} dryRun - Whether this is a dry run */ async function runDoctor(projectRoot, logger, dryRun) { if (dryRun) { logger.action('Run: npx aios-core doctor'); return; } const spinner = ora('Running AIOS doctor...').start(); try { const { stdout } = await execa('npx', ['aios-core', 'doctor'], { cwd: projectRoot, timeout: 60000, }); spinner.succeed('AIOS doctor completed'); if (logger.verbose) { console.log(chalk.dim(stdout)); } } catch (error) { spinner.warn('AIOS doctor had warnings'); if (logger.verbose) { console.log(chalk.yellow(error.message)); } } } /** * Install aios-core package * @param {string} projectRoot - Project root * @param {InstallLogger} logger - Logger instance * @param {boolean} dryRun - Whether this is a dry run */ async function installAiosCore(projectRoot, logger, dryRun) { if (dryRun) { logger.action('Run: npm install aios-core --save-dev'); return; } const spinner = ora('Installing aios-core...').start(); try { await execa('npm', ['install', 'aios-core', '--save-dev'], { cwd: projectRoot, timeout: 300000, // 5 minutes }); spinner.succeed('aios-core installed'); } catch (error) { spinner.fail('Failed to install aios-core'); throw error; } } /** * Initialize AIOS in the project * @param {string} projectRoot - Project root * @param {InstallLogger} logger - Logger instance * @param {boolean} dryRun - Whether this is a dry run */ async function initializeAios(projectRoot, logger, dryRun) { if (dryRun) { logger.action('Run: npx aios-core install'); return; } const spinner = ora('Initializing AIOS...').start(); try { await execa('npx', ['aios-core', 'install'], { cwd: projectRoot, timeout: 120000, // 2 minutes }); spinner.succeed('AIOS initialized'); } catch (error) { spinner.fail('Failed to initialize AIOS'); throw error; } } /** * Main installer entry point * @param {Object} options - Installation options * @param {boolean} options.dryRun - Preview without making changes * @param {boolean} options.verbose - Enable verbose output * @param {string} options.profile - Profile to use (bob or advanced) * @param {boolean} options.skipDeps - Skip dependency checking * @param {boolean} options.color - Enable/disable colors */ async function runInstaller(options = {}) { const timer = new InstallTimer(); const logger = new InstallLogger(options); const projectRoot = process.cwd(); // Disable colors if requested if (!options.color) { chalk.level = 0; } // Introduction intro(chalk.bgCyan(' AIOS Installer ')); if (options.dryRun) { note( 'Dry-run mode enabled.\nNo changes will be made to your system.', 'Preview Mode', ); } // Step 1: OS Detection console.log(''); const osInfo = detectOS(); logger.info(`Detected OS: ${chalk.bold(getOSDisplayName(osInfo))}`); if (osInfo.notes) { for (const noteText of osInfo.notes) { logger.debug(noteText); } } // Step 2: Dependency Check if (!options.skipDeps) { console.log(''); const depResults = checkAllDependencies(osInfo); if (options.verbose) { displayResults(depResults); } else { // Show summary only if (depResults.passed) { logger.success('All required dependencies installed'); } else { logger.error('Missing required dependencies'); console.log(''); console.log(depResults.summary); console.log(''); logger.info('Please install missing dependencies and try again.'); process.exit(1); } if (depResults.hasWarnings) { logger.warn(`${depResults.warnings.length} optional dependencies not installed`); if (options.verbose) { for (const warning of depResults.warnings) { logger.debug(`${warning.name}: ${warning.impact}`); } } } } } // Step 3: Profile Selection let profile = options.profile; if (!profile) { console.log(''); const profileSelection = await select({ message: 'Select your AIOS profile:', options: [ { value: 'bob', label: 'Bob Mode (Recommended)', hint: 'Simplified interface - perfect for getting started', }, { value: 'advanced', label: 'Advanced Mode', hint: 'Full access to all agents and commands', }, ], initialValue: 'bob', }); if (isCancel(profileSelection)) { cancel('Installation cancelled'); process.exit(0); } profile = profileSelection; } logger.info(`Selected profile: ${chalk.bold(profile)}`); // Step 4: Create User Config console.log(''); await createUserConfigDirect(profile, logger, options.dryRun); // Step 5: Brownfield Detection console.log(''); const brownfield = detectBrownfield(projectRoot); if (brownfield.isBrownfield) { logger.info('Existing AIOS installation detected'); if (brownfield.hasLegacyConfig && !brownfield.hasLayeredConfig) { logger.warn('Legacy configuration format detected'); const shouldMigrate = await confirm({ message: 'Would you like to migrate to the new layered configuration?', initialValue: true, }); if (isCancel(shouldMigrate)) { cancel('Installation cancelled'); process.exit(0); } if (shouldMigrate && !options.dryRun) { const spinner = ora('Migrating configuration...').start(); try { await execa('npx', ['aios-core', 'config', 'migrate'], { cwd: projectRoot, timeout: 60000, }); spinner.succeed('Configuration migrated'); } catch (error) { spinner.warn('Migration had warnings - manual review recommended'); logger.debug(error.message); } } else if (options.dryRun) { logger.action('Run: npx aios-core config migrate'); } } else { logger.success('Configuration is up to date'); } } else { // Greenfield: New installation logger.info('New installation detected'); // Check if package.json exists const packageJsonPath = path.join(projectRoot, 'package.json'); const hasPackageJson = await fs.pathExists(packageJsonPath); if (!hasPackageJson) { logger.warn('No package.json found - creating one'); if (!options.dryRun) { await execa('npm', ['init', '-y'], { cwd: projectRoot }); logger.success('package.json created'); } else { logger.action('Run: npm init -y'); } } // Install aios-core await installAiosCore(projectRoot, logger, options.dryRun); // Initialize AIOS await initializeAios(projectRoot, logger, options.dryRun); } // Step 6: Run doctor console.log(''); await runDoctor(projectRoot, logger, options.dryRun); // Check installation time const elapsed = timer.elapsedFormatted(); if (timer.checkTimeout(300)) { logger.warn(`Installation took ${elapsed} (exceeded 5 minute target)`); } // Completion console.log(''); if (options.dryRun) { outro(chalk.yellow(`Dry-run complete in ${elapsed}. No changes were made.`)); } else { outro(chalk.green(`AIOS installed successfully in ${elapsed}!`)); console.log(''); console.log(chalk.dim('Next steps:')); console.log(chalk.dim(' 1. Run `npx aios-core info` to see your configuration')); console.log(chalk.dim(' 2. Activate an agent with @agent-name (e.g., @dev)')); if (profile === 'bob') { console.log(chalk.dim(' 3. Just talk to Bob - he\'ll orchestrate everything!')); } } } module.exports = { runInstaller, InstallTimer, InstallLogger, detectBrownfield, tryLoadConfigResolver, createUserConfigDirect, };