aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
1,231 lines (1,080 loc) • 44.9 kB
JavaScript
#!/usr/bin/env node
/**
* AIOS-FullStack Installation Wizard v5 (LEGACY)
* Based on the original beautiful visual design with ASCII art
* Version: 2.1.0
*
* ⚠️ DEPRECATED (since v3.11.3, scheduled for removal in v5.0.0):
* This file is the LEGACY installer.
* The new modular wizard is located at: packages/installer/src/wizard/index.js
*
* This file is kept as a fallback for edge cases where the new wizard
* is not available. All new development should use the new wizard.
*
* Migration path:
* - Use `npx aios-core` which routes through bin/aios.js to the new wizard
* - Do NOT call this file directly
*
* Supported IDEs (4 total):
* - Claude Code, Cursor, Gemini CLI, GitHub Copilot
*/
const path = require('path');
const fs = require('fs');
const fse = require('fs-extra');
const yaml = require('js-yaml');
const { execSync, exec, spawn } = require('child_process');
const { promisify } = require('util');
const inquirer = require('inquirer');
const chalk = require('chalk');
const ora = require('ora'); // INS-2 Performance: Progress indicators (AC9)
// INS-2 Performance: Promisified exec for async shell commands (AC7)
const execAsync = promisify(exec);
/**
* Execute command with inherited stdio (for npm install -g that needs user interaction)
* INS-2 Performance: Async version that doesn't block event loop
* @param {string} command - Command to execute
* @param {object} options - Spawn options
* @returns {Promise<void>}
*/
function spawnAsync(command, options = {}) {
return new Promise((resolve, reject) => {
const [cmd, ...args] = command.split(' ');
const child = spawn(cmd, args, { stdio: 'inherit', shell: true, ...options });
child.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Command failed with code ${code}`));
}
});
child.on('error', reject);
});
}
// ASCII Art Banner (Clean blocky style like reference image)
const BANNER = chalk.cyan(`
█████╗ ██╗ ██████╗ ███████╗ ███████╗██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
██╔══██╗██║██╔═══██╗██╔════╝ ██╔════╝██║ ██║██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
███████║██║██║ ██║███████╗█████╗█████╗ ██║ ██║██║ ██║ ███████╗ ██║ ███████║██║ █████╔╝
██╔══██║██║██║ ██║╚════██║╚════╝██╔══╝ ██║ ██║██║ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
██║ ██║██║╚██████╔╝███████║ ██║ ╚██████╔╝███████╗███████╗███████║ ██║ ██║ ██║╚██████╗██║ ██╗
╚═╝ ╚═╝╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
`);
const SUBTITLE = chalk.magenta('Universal AI Agent Framework for Any Domain');
// Read version from package.json dynamically
const packageJsonVersion = require(path.join(__dirname, '..', 'package.json')).version;
const VERSION = chalk.yellow(`Installer v${packageJsonVersion}`);
/**
* Smart path resolution for AIOS Core modules
*/
function resolveAiosCoreModule(modulePath) {
const aiosCoreModule = path.join(__dirname, '..', '.aios-core', modulePath);
const moduleExists =
fs.existsSync(aiosCoreModule + '.js') ||
fs.existsSync(aiosCoreModule + '/index.js') ||
fs.existsSync(aiosCoreModule);
if (!moduleExists) {
throw new Error(
`Cannot find AIOS Core module: ${modulePath}\n` +
`Searched: ${aiosCoreModule}\n` +
'Please ensure aios-core is installed correctly.'
);
}
return require(aiosCoreModule);
}
// Load AIOS Core modules
const { detectRepositoryContext } = resolveAiosCoreModule(
'infrastructure/scripts/repository-detector'
);
// PM adapters imported but not used directly (loaded dynamically)
// const { ClickUpAdapter } = resolveAiosCoreModule('utils/pm-adapters/clickup-adapter');
// const { GitHubProjectsAdapter } = resolveAiosCoreModule('utils/pm-adapters/github-adapter');
// const { JiraAdapter } = resolveAiosCoreModule('utils/pm-adapters/jira-adapter');
// Brownfield upgrade module (Story 6.18)
let brownfieldUpgrader;
try {
brownfieldUpgrader = require('../packages/installer/src/installer/brownfield-upgrader');
} catch (_err) {
// Module may not be available in older installations
brownfieldUpgrader = null;
}
async function main() {
console.clear();
// Check for minimal mode flag
const isMinimalMode = process.argv.includes('--minimal');
// Display beautiful banner
console.log(BANNER);
console.log(SUBTITLE);
console.log(VERSION);
if (isMinimalMode) {
console.log(chalk.yellow(' 🔹 Minimal Installation Mode'));
}
console.log('');
console.log(chalk.gray('═'.repeat(80)));
console.log('');
const projectRoot = process.cwd();
let context = detectRepositoryContext();
// Setup prerequisites if needed
if (!context) {
console.log(chalk.blue('⚙️ Setting up project prerequisites...\n'));
// Check for git repository
// INS-2 Performance: Use async exec (AC7)
let hasGit = false;
try {
await execAsync('git rev-parse --git-dir', { cwd: projectRoot });
hasGit = true;
} catch (_err) {
// Not a git repo
}
if (!hasGit) {
// INS-2 Performance: Add spinner for git init (AC9)
const gitSpinner = ora('Initializing git repository...').start();
try {
await execAsync('git init', { cwd: projectRoot });
gitSpinner.succeed('Git repository initialized');
} catch (_err) {
gitSpinner.fail('Failed to initialize git repository');
process.exit(1);
}
}
// Check for package.json
const packageJsonPath = path.join(projectRoot, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
const dirName = path.basename(projectRoot);
const defaultPackage = {
name: dirName.toLowerCase().replace(/\s+/g, '-'),
version: '1.0.0',
description: 'AIOS-FullStack project',
main: 'index.js',
scripts: { test: 'echo "Error: no test specified" && exit 1' },
keywords: [],
author: '',
license: 'ISC',
};
// INS-2 Performance: Use async write instead of sync
await fse.writeFile(packageJsonPath, JSON.stringify(defaultPackage, null, 2));
console.log(chalk.green('✓') + ' package.json created');
}
console.log(chalk.green('✓') + ' Prerequisites ready\n');
// Try to detect context again
context = detectRepositoryContext();
// If still no context, create minimal one
if (!context) {
// INS-2 Performance: Use async read instead of sync
const packageJson = JSON.parse(await fse.readFile(packageJsonPath, 'utf8'));
context = {
projectRoot,
packageName: packageJson.name,
packageVersion: packageJson.version,
repositoryUrl: 'local-repository',
frameworkLocation: path.join(__dirname, '..'),
};
}
}
console.log(chalk.cyan('📦 Package:') + ` ${context.packageName}`);
console.log('');
// Check for existing installation (Story 6.18 - Brownfield Upgrade)
const installedManifestPath = path.join(projectRoot, '.aios-core', '.installed-manifest.yaml');
const hasExistingInstall = fs.existsSync(installedManifestPath);
if (hasExistingInstall && brownfieldUpgrader) {
console.log(chalk.yellow('🔄 Existing AIOS installation detected!'));
console.log('');
const sourceDir = path.join(context.frameworkLocation, '.aios-core');
const upgradeCheck = brownfieldUpgrader.checkUpgradeAvailable(sourceDir, projectRoot);
if (upgradeCheck.available) {
console.log(chalk.green(` Upgrade available: ${upgradeCheck.from} → ${upgradeCheck.to}`));
console.log('');
// Generate upgrade report for display
const sourceManifest = brownfieldUpgrader.loadSourceManifest(sourceDir);
const installedManifest = brownfieldUpgrader.loadInstalledManifest(projectRoot);
const report = brownfieldUpgrader.generateUpgradeReport(
sourceManifest,
installedManifest,
projectRoot
);
console.log(chalk.gray('─'.repeat(80)));
const { upgradeChoice } = await inquirer.prompt([
{
type: 'list',
name: 'upgradeChoice',
message: chalk.white('What would you like to do?'),
choices: [
{
name:
` Upgrade to ${upgradeCheck.to} ` +
chalk.gray(
`(${report.newFiles.length} new, ${report.modifiedFiles.length} updated files)`
),
value: 'upgrade',
},
{
name: ' Dry Run ' + chalk.gray('(Show what would be changed without applying)'),
value: 'dry-run',
},
{
name: ' Fresh Install ' + chalk.gray('(Reinstall everything, overwrite all files)'),
value: 'fresh',
},
{
name: ' Cancel ' + chalk.gray('(Exit without changes)'),
value: 'cancel',
},
],
},
]);
if (upgradeChoice === 'cancel') {
console.log(chalk.yellow('\nInstallation cancelled.'));
process.exit(0);
}
if (upgradeChoice === 'dry-run') {
console.log('');
console.log(brownfieldUpgrader.formatUpgradeReport(report));
console.log('');
console.log(chalk.yellow('This was a dry run. No files were changed.'));
console.log(chalk.gray('Run again and select "Upgrade" to apply changes.'));
process.exit(0);
}
if (upgradeChoice === 'upgrade') {
console.log('');
console.log(chalk.blue('📦 Applying upgrade...'));
const result = await brownfieldUpgrader.applyUpgrade(report, sourceDir, projectRoot, {
dryRun: false,
});
if (result.success) {
// Update installed manifest
const packageJson = require(path.join(context.frameworkLocation, 'package.json'));
brownfieldUpgrader.updateInstalledManifest(
projectRoot,
sourceManifest,
`aios-core@${packageJson.version}`
);
console.log(chalk.green('✓') + ` Upgraded ${result.filesInstalled.length} files`);
if (result.filesSkipped.length > 0) {
console.log(
chalk.yellow('⚠') + ` Preserved ${result.filesSkipped.length} user-modified files`
);
}
console.log('');
console.log(chalk.green('✅ Upgrade complete!'));
console.log(chalk.gray(` From: ${upgradeCheck.from}`));
console.log(chalk.gray(` To: ${upgradeCheck.to}`));
process.exit(0);
} else {
console.error(chalk.red('✗') + ' Upgrade failed with errors:');
for (const err of result.errors) {
console.error(chalk.red(` - ${err.path}: ${err.error}`));
}
process.exit(1);
}
}
// If 'fresh' was selected, continue with normal installation flow below
if (upgradeChoice === 'fresh') {
console.log(chalk.yellow('\nProceeding with fresh installation...'));
console.log('');
}
} else {
console.log(chalk.green(` Current version: ${upgradeCheck.from || 'unknown'}`));
console.log(
chalk.gray(' No upgrade available. You can proceed with fresh install if needed.')
);
console.log('');
}
}
// Step 1: Installation Mode
console.log(chalk.gray('─'.repeat(80)));
const { installMode } = await inquirer.prompt([
{
type: 'list',
name: 'installMode',
message: chalk.white('How are you using AIOS-FullStack?'),
choices: [
{
name: ' Using AIOS in a project ' + chalk.gray('(Framework files added to .gitignore)'),
value: 'project-development',
},
{
name:
' Developing AIOS framework itself ' + chalk.gray('(Framework files are source code)'),
value: 'framework-development',
},
],
},
]);
// Save installation config
const config = {
installation: {
mode: installMode,
detected_at: new Date().toISOString(),
},
repository: {
url: context.repositoryUrl,
auto_detect: true,
},
framework: {
source: installMode === 'framework-development' ? 'local' : 'npm',
version: context.packageVersion,
location: context.frameworkLocation,
},
git_ignore_rules: {
mode: installMode,
ignore_framework_files: installMode === 'project-development',
},
};
const configPath = path.join(context.projectRoot, '.aios-installation-config.yaml');
// INS-2 Performance: Use async write instead of sync
await fse.writeFile(configPath, yaml.dump(config));
// Update .gitignore
// INS-2 Performance: Now async
await updateGitIgnore(installMode, context.projectRoot);
// Step 2: PM Tool
console.log('');
const { pmTool } = await inquirer.prompt([
{
type: 'list',
name: 'pmTool',
message: chalk.white('Do you use a project management tool?'),
choices: [
{ name: ' None (local YAML files only) ' + chalk.gray('- Recommended'), value: 'local' },
{ name: ' ClickUp ' + chalk.gray('- Requires API token'), value: 'clickup' },
{ name: ' GitHub Projects ' + chalk.gray('- Uses gh auth'), value: 'github-projects' },
{ name: ' Jira ' + chalk.gray('- Requires API token'), value: 'jira' },
],
},
]);
// Save PM config
// INS-2 Performance: Now async
await savePMConfig(pmTool, {}, context.projectRoot);
// Step 3: IDE Selection (CHECKBOX with instructions)
console.log('');
console.log(chalk.gray('─'.repeat(80)));
console.log(
chalk.dim(
' Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed'
)
);
console.log('');
const { ides } = await inquirer.prompt([
{
type: 'checkbox',
name: 'ides',
message: chalk.white('Which IDE(s) will you use?'),
choices: [
{
name: ' Claude Code ' + chalk.blue('(v4)') + chalk.gray(' - Recommended'),
value: 'claude',
checked: true,
},
{ name: ' Cursor ' + chalk.blue('(v4)'), value: 'cursor' },
{ name: ' Gemini CLI ' + chalk.blue('(v4)'), value: 'gemini' },
{ name: ' GitHub Copilot ' + chalk.blue('(v4)'), value: 'github-copilot' },
{
name: ' AntiGravity ' + chalk.blue('(v4)') + chalk.gray(' - Google AI IDE'),
value: 'antigravity',
},
new inquirer.Separator(chalk.gray('─'.repeat(40))),
{ name: ' Skip IDE setup', value: 'none' },
],
validate: function (answer) {
if (answer.length < 1) {
return 'You must choose at least one option.';
}
return true;
},
},
]);
// Step 4a: Check and offer to install CLI tools
const cliToolsToCheck = [];
if (ides.includes('claude')) {
cliToolsToCheck.push({ ide: 'claude', command: 'claude', name: 'Claude Code', npm: '@anthropic-ai/claude-code' });
}
if (ides.includes('gemini')) {
cliToolsToCheck.push({ ide: 'gemini', command: 'gemini', name: 'Gemini CLI', npm: '@google/gemini-cli' });
}
if (cliToolsToCheck.length > 0) {
console.log('');
console.log(chalk.blue('🔍 Checking CLI tools...'));
// INS-2 Performance: Check CLI tools in parallel (AC7)
const toolCheckResults = await Promise.all(
cliToolsToCheck.map(async (tool) => {
try {
const checkCmd = process.platform === 'win32' ? `where ${tool.command}` : `command -v ${tool.command}`;
await execAsync(checkCmd);
return { tool, installed: true };
} catch {
return { tool, installed: false };
}
})
);
const missingTools = [];
for (const result of toolCheckResults) {
if (result.installed) {
console.log(chalk.green('✓') + ` ${result.tool.name} is installed`);
} else {
console.log(chalk.yellow('⚠') + ` ${result.tool.name} is not installed`);
missingTools.push(result.tool);
}
}
if (missingTools.length > 0) {
console.log('');
const toolNames = missingTools.map(t => t.name).join(', ');
const { installClis } = await inquirer.prompt([
{
type: 'confirm',
name: 'installClis',
message: chalk.white(`Would you like to install ${toolNames}?`),
default: true,
},
]);
if (installClis) {
for (const tool of missingTools) {
console.log(chalk.blue(`📥 Installing ${tool.name}...`));
try {
// INS-2 Performance: Use async spawn instead of sync (AC7)
await spawnAsync(`npm install -g ${tool.npm}`);
console.log(chalk.green('✓') + ` ${tool.name} installed successfully`);
// Show post-install instructions
if (tool.ide === 'claude') {
console.log(chalk.gray(' Run `claude` to authenticate with your Anthropic account'));
} else if (tool.ide === 'gemini') {
console.log(chalk.gray(' Run `gemini` to authenticate with your Google account'));
}
} catch (error) {
console.log(chalk.red('✗') + ` Failed to install ${tool.name}: ${error.message}`);
console.log(chalk.gray(` You can install manually: npm install -g ${tool.npm}`));
}
}
} else {
console.log(chalk.gray(' Skipping CLI installation. You can install later:'));
for (const tool of missingTools) {
console.log(chalk.gray(` npm install -g ${tool.npm}`));
}
}
}
}
// Step 4b: Copy AIOS Core files
console.log('');
const sourceCoreDir = path.join(context.frameworkLocation, '.aios-core');
const targetCoreDir = path.join(context.projectRoot, '.aios-core');
if (fs.existsSync(sourceCoreDir)) {
// INS-2 Performance: Add spinner for file copy (AC9)
const copySpinner = ora('Installing AIOS Core files...').start();
await fse.copy(sourceCoreDir, targetCoreDir);
copySpinner.succeed(
'AIOS Core files installed ' +
chalk.gray('(11 agents, 68 tasks, 23 templates)')
);
// Create installed manifest for brownfield upgrades (Story 6.18)
if (brownfieldUpgrader) {
try {
const sourceManifest = brownfieldUpgrader.loadSourceManifest(sourceCoreDir);
if (sourceManifest) {
const packageJson = require(path.join(context.frameworkLocation, 'package.json'));
brownfieldUpgrader.updateInstalledManifest(
context.projectRoot,
sourceManifest,
`aios-core@${packageJson.version}`
);
console.log(
chalk.green('✓') +
' Installation manifest created ' +
chalk.gray('(enables future upgrades)')
);
}
} catch (manifestErr) {
// Non-critical - just log warning
console.log(
chalk.yellow('⚠') +
' Could not create installation manifest ' +
chalk.gray('(brownfield upgrades may not work)')
);
}
}
} else {
console.error(chalk.red('✗') + ' AIOS Core files not found');
process.exit(1);
}
// Copy IDE rules and commands if IDE was selected
if (!ides.includes('none')) {
console.log('');
console.log(chalk.blue('📝 Installing IDE configurations...'));
const ideRulesMap = {
claude: { source: 'claude-rules.md', target: '.claude/CLAUDE.md' },
cursor: { source: 'cursor-rules.md', target: '.cursor/rules.md' },
gemini: { source: 'gemini-rules.md', target: '.gemini/rules.md' },
'github-copilot': { source: 'copilot-rules.md', target: '.github/chatmodes/aios-agent.md' },
antigravity: { source: 'antigravity-rules.md', target: '.antigravity/rules.md' },
};
// Step 1: Copy basic IDE rules files
for (const ide of ides) {
if (ide !== 'none' && ideRulesMap[ide]) {
const ideConfig = ideRulesMap[ide];
const sourceRules = path.join(targetCoreDir, 'product', 'templates', 'ide-rules', ideConfig.source);
const targetRules = path.join(context.projectRoot, ideConfig.target);
if (fs.existsSync(sourceRules)) {
await fse.ensureDir(path.dirname(targetRules));
await fse.copy(sourceRules, targetRules);
console.log(
chalk.green('✓') + ` ${ide.charAt(0).toUpperCase() + ide.slice(1)} base rules installed`
);
}
}
}
// INS-2 Performance: Cache directory listings to avoid redundant readdirSync calls
// The same agent/task directories are read 6-7 times across IDE installations
// This reduces directory reads from 6-7x to 1x (AC3)
const coreAgentsSource = path.join(targetCoreDir, 'development', 'agents');
const coreTasksSource = path.join(targetCoreDir, 'development', 'tasks');
// Cache agent files list (read once, use many times)
const cachedAgentFiles = fs.existsSync(coreAgentsSource)
? fs.readdirSync(coreAgentsSource).filter((f) => f.endsWith('.md'))
: [];
// Cache task files list
const cachedTaskFiles = fs.existsSync(coreTasksSource)
? fs.readdirSync(coreTasksSource).filter((f) => f.endsWith('.md'))
: [];
// Step 2: Install AIOS CORE agents and tasks for Claude Code
// v4: Agents and tasks are in development/ module
if (ides.includes('claude')) {
const coreAgentsTarget = path.join(
context.projectRoot,
'.claude',
'commands',
'AIOS',
'agents'
);
const coreTasksTarget = path.join(
context.projectRoot,
'.claude',
'commands',
'AIOS',
'tasks'
);
if (cachedAgentFiles.length > 0) {
await fse.copy(coreAgentsSource, coreAgentsTarget);
console.log(chalk.green('✓') + ` Claude Code CORE agents installed (${cachedAgentFiles.length} agents)`);
}
if (cachedTaskFiles.length > 0) {
await fse.copy(coreTasksSource, coreTasksTarget);
console.log(chalk.green('✓') + ` Claude Code CORE tasks installed (${cachedTaskFiles.length} tasks)`);
}
// Create AIOS README for Claude Code
const aiossReadme = path.join(
context.projectRoot,
'.claude',
'commands',
'AIOS',
'README.md'
);
await fse.ensureDir(path.dirname(aiossReadme));
await fse.writeFile(
aiossReadme,
`# AIOS Core Commands
This directory contains the core AIOS-FullStack agents and tasks.
## Usage
- Agents: Use slash commands like /dev, /architect, /qa, /pm, etc.
- Tasks: Reference tasks in agent workflows
## Documentation
See .aios-core/user-guide.md for complete documentation.
`
);
// Silent statusline setup (graceful skip if user already has one)
await setupGlobalStatuslineLegacy(sourceCoreDir);
}
// Step 3: Install AIOS CORE agents for Cursor
// v4: Agents are in development/ module
// INS-2 Performance: Uses cached agent files list
if (ides.includes('cursor')) {
const cursorRulesTarget = path.join(
context.projectRoot,
'.cursor',
'rules',
'AIOS',
'agents'
);
if (cachedAgentFiles.length > 0) {
await fse.ensureDir(cursorRulesTarget);
// Convert .md files to .mdc for Cursor (using cached list)
for (const agentFile of cachedAgentFiles) {
const sourcePath = path.join(coreAgentsSource, agentFile);
const targetFileName = agentFile.replace('.md', '.mdc');
const targetPath = path.join(cursorRulesTarget, targetFileName);
await fse.copy(sourcePath, targetPath);
}
console.log(
chalk.green('✓') + ` Cursor CORE rules installed (${cachedAgentFiles.length} agents)`
);
}
// Create AIOS README for Cursor
const cursorReadme = path.join(context.projectRoot, '.cursor', 'rules', 'AIOS', 'README.md');
await fse.ensureDir(path.dirname(cursorReadme));
await fse.writeFile(
cursorReadme,
`# AIOS Core Rules
This directory contains the core AIOS-FullStack agent rules for Cursor.
## Usage
These rules are automatically loaded by Cursor to provide agent-specific context.
## Documentation
See .aios-core/user-guide.md for complete documentation.
`
);
}
// Step 4: Install AIOS CORE agents for other IDEs (Gemini, AntiGravity)
// v4: Agents are in development/ module
// INS-2 Performance: Uses cached agent files list
const otherIdeInstalls = ['gemini', 'antigravity'];
for (const ide of otherIdeInstalls) {
if (ides.includes(ide)) {
const ideRulesDir = ide === 'gemini' ? '.gemini' : `.${ide}`;
const ideRulesTarget = path.join(
context.projectRoot,
ideRulesDir,
'rules',
'AIOS',
'agents'
);
if (cachedAgentFiles.length > 0) {
await fse.ensureDir(ideRulesTarget);
// Copy agent files (using cached list)
for (const agentFile of cachedAgentFiles) {
const sourcePath = path.join(coreAgentsSource, agentFile);
const targetPath = path.join(ideRulesTarget, agentFile);
await fse.copy(sourcePath, targetPath);
}
const ideName = ide.charAt(0).toUpperCase() + ide.slice(1);
console.log(
chalk.green('✓') + ` ${ideName} CORE agents installed (${cachedAgentFiles.length} agents)`
);
}
}
}
// Step 5: Install GitHub Copilot chat modes
// v4: Agents are in development/ module
// INS-2 Performance: Uses cached agent files list
if (ides.includes('github-copilot')) {
const copilotModesDir = path.join(context.projectRoot, '.github', 'chatmodes');
if (cachedAgentFiles.length > 0) {
await fse.ensureDir(copilotModesDir);
// Copy agent files (using cached list)
for (const agentFile of cachedAgentFiles) {
const sourcePath = path.join(coreAgentsSource, agentFile);
const agentName = agentFile.replace('.md', '');
const targetPath = path.join(copilotModesDir, `aios-${agentName}.md`);
await fse.copy(sourcePath, targetPath);
}
console.log(
chalk.green('✓') + ` GitHub Copilot chat modes installed (${cachedAgentFiles.length} modes)`
);
}
}
}
// Step 7: Squads (CHECKBOX with visual)
// Try multiple locations for squads (npm package vs local development vs npx)
// __dirname is the 'bin/' directory of the package, so '..' gives us the package root
const packageRoot = path.resolve(__dirname, '..');
const possibleSquadsDirs = [
// Primary: relative to this script (works for npx and local) - squads/
path.join(packageRoot, 'squads'),
// Secondary: context-based framework location - squads/
path.join(context.frameworkLocation, 'squads'),
// Tertiary: installed in project's node_modules - squads/
path.join(context.projectRoot, 'node_modules', 'aios-core', 'squads'),
path.join(context.projectRoot, 'node_modules', '@aios', 'fullstack', 'squads'),
];
let sourceSquadsDir = null;
for (const dir of possibleSquadsDirs) {
if (fs.existsSync(dir)) {
sourceSquadsDir = dir;
break;
}
}
const availableSquads = [];
let selectedSquads = []; // Declare here to be accessible in summary
if (sourceSquadsDir && fs.existsSync(sourceSquadsDir)) {
// INS-2 Performance: Use withFileTypes to avoid separate statSync calls per entry
// This reduces N+1 syscalls to just 1 syscall for the entire directory
let squads = fs
.readdirSync(sourceSquadsDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
// Filter for minimal mode - only show squad-creator
if (isMinimalMode) {
squads = squads.filter((squad) => squad === 'squad-creator');
}
availableSquads.push(...squads);
}
if (availableSquads.length > 0) {
console.log('');
console.log(chalk.gray('─'.repeat(80)));
console.log(
chalk.dim(
' Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed'
)
);
console.log('');
const result = await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedSquads',
message: chalk.white('Select squads to install (optional)'),
choices: availableSquads.map((squad) => ({
name: ' ' + squad,
value: squad,
})),
},
]);
selectedSquads = result.selectedSquads; // Assign to outer scope variable
if (selectedSquads.length > 0) {
console.log('');
console.log(chalk.blue('📦 Installing squads...'));
// Always install to squads/ directory (modern naming)
const targetSquadsDir = path.join(context.projectRoot, 'squads');
// INS-2 Performance: Copy all squads in parallel first (AC6)
await Promise.all(
selectedSquads.map(async (squad) => {
const sourceSquad = path.join(sourceSquadsDir, squad);
const targetSquad = path.join(targetSquadsDir, squad);
await fse.copy(sourceSquad, targetSquad);
})
);
console.log(chalk.green('✓') + ` Squads copied: ${selectedSquads.join(', ')}`);
// Process IDE-specific installations sequentially for ordered logging
for (const squad of selectedSquads) {
const targetSquad = path.join(targetSquadsDir, squad);
// INS-2 Performance: Cache squad file lists once per squad (used by Claude, Cursor, etc.)
const squadAgentsSource = path.join(targetSquad, 'agents');
const squadTasksSource = path.join(targetSquad, 'tasks');
const squadReadmeSource = path.join(targetSquad, 'README.md');
// Cache squad agents/tasks lists (read once per squad, use for all IDEs)
const squadAgentFiles = fs.existsSync(squadAgentsSource)
? fs.readdirSync(squadAgentsSource).filter((f) => f.endsWith('.md'))
: [];
const squadTaskFiles = fs.existsSync(squadTasksSource)
? fs.readdirSync(squadTasksSource).filter((f) => f.endsWith('.md'))
: [];
const hasSquadReadme = fs.existsSync(squadReadmeSource);
// Install squad agents/tasks for Claude Code
if (ides.includes('claude')) {
const squadClaudeTarget = path.join(context.projectRoot, '.claude', 'commands', squad);
// Copy agents (using cached list)
if (squadAgentFiles.length > 0) {
const squadAgentsTarget = path.join(squadClaudeTarget, 'agents');
await fse.copy(squadAgentsSource, squadAgentsTarget);
console.log(chalk.green(' ✓') + ` Claude Code ${squad} agents (${squadAgentFiles.length} agents)`);
}
// Copy tasks (using cached list)
if (squadTaskFiles.length > 0) {
const squadTasksTarget = path.join(squadClaudeTarget, 'tasks');
await fse.copy(squadTasksSource, squadTasksTarget);
console.log(chalk.green(' ✓') + ` Claude Code ${squad} tasks (${squadTaskFiles.length} tasks)`);
}
// Copy README (using cached check)
if (hasSquadReadme) {
await fse.copy(squadReadmeSource, path.join(squadClaudeTarget, 'README.md'));
}
}
// Install squad agents for Cursor
if (ides.includes('cursor') && squadAgentFiles.length > 0) {
const cursorSquadTarget = path.join(
context.projectRoot,
'.cursor',
'rules',
squad,
'agents'
);
await fse.ensureDir(cursorSquadTarget);
// Convert .md files to .mdc for Cursor (using cached list)
for (const agentFile of squadAgentFiles) {
const sourcePath = path.join(squadAgentsSource, agentFile);
const targetFileName = agentFile.replace('.md', '.mdc');
const targetPath = path.join(cursorSquadTarget, targetFileName);
await fse.copy(sourcePath, targetPath);
}
console.log(chalk.green(' ✓') + ` Cursor ${squad} rules (${squadAgentFiles.length} agents)`);
// Copy README for Cursor (using cached check)
if (hasSquadReadme) {
await fse.copy(
squadReadmeSource,
path.join(context.projectRoot, '.cursor', 'rules', squad, 'README.md')
);
}
}
}
}
}
// Post-installation validation (Story 6.19)
console.log('');
// INS-2 Performance: Add spinner for validation (AC9)
const validationSpinner = ora('Validating installation integrity...').start();
let validationPassed = true;
try {
const { PostInstallValidator } = require('../packages/installer/src/installer/post-install-validator');
const validator = new PostInstallValidator(context.projectRoot, context.frameworkLocation, {
verifyHashes: false,
verbose: false,
// SECURITY NOTE: Signature verification is disabled during initial installation
// because the manifest signature (.minisig) may not yet be present in the package.
// This is acceptable for post-install validation which only checks file presence.
// For production integrity checks, users should run `aios validate` which
// enforces signature verification when the .minisig file is present.
requireSignature: false,
});
const report = await validator.validate();
if (
report.status === 'failed' ||
report.stats.missingFiles > 0 ||
report.stats.corruptedFiles > 0
) {
validationPassed = false;
validationSpinner.warn('Installation validation found issues:');
console.log(chalk.dim(` - Missing files: ${report.stats.missingFiles}`));
console.log(chalk.dim(` - Corrupted files: ${report.stats.corruptedFiles}`));
console.log('');
console.log(
chalk.yellow(' Run ') +
chalk.cyan('aios validate --repair') +
chalk.yellow(' to fix issues')
);
} else {
validationSpinner.succeed(`Installation verified (${report.stats.validFiles} files)`);
}
} catch (validationError) {
// Log validation errors but don't fail installation
// This allows installation to proceed even if validator module has issues
// However, users should investigate validation errors manually
validationPassed = false;
validationSpinner.warn('Post-installation validation encountered an error');
console.log(chalk.dim(` Error: ${validationError.message}`));
if (process.env.DEBUG || process.env.AIOS_DEBUG) {
console.log(chalk.dim(` Stack: ${validationError.stack}`));
}
console.log(chalk.dim(' Run `aios validate` to check installation integrity'));
}
// Summary
console.log('');
console.log(chalk.gray('═'.repeat(80)));
console.log('');
console.log(chalk.green.bold('✓ AIOS-FullStack installation complete! 🎉'));
console.log('');
console.log(chalk.cyan('📋 Configuration Summary:'));
console.log(' ' + chalk.dim('Mode: ') + installMode);
console.log(' ' + chalk.dim('Version: ') + packageJsonVersion);
console.log(' ' + chalk.dim('Repository: ') + context.repositoryUrl);
console.log(
' ' + chalk.dim('IDE(s): ') + (ides.includes('none') ? 'none' : ides.join(', '))
);
console.log(' ' + chalk.dim('PM Tool: ') + pmTool);
if (availableSquads.length > 0 && selectedSquads && selectedSquads.length > 0) {
console.log(' ' + chalk.dim('Squads: ') + ' ' + selectedSquads.join(', '));
}
console.log('');
console.log(chalk.cyan('📁 Installed Structure:'));
console.log(' ' + chalk.dim('.aios-core/') + ' - Framework core files');
if (ides.includes('claude')) {
console.log(' ' + chalk.dim('.claude/'));
console.log(' ' + chalk.dim('├─ CLAUDE.md') + ' - Main configuration');
console.log(' ' + chalk.dim('└─ commands/'));
console.log(' ' + chalk.dim(' ├─ AIOS/') + ' - Core agents & tasks');
if (selectedSquads && selectedSquads.length > 0) {
selectedSquads.forEach((squad) => {
console.log(' ' + chalk.dim(` └─ ${squad}/`) + ' - Squad commands');
});
}
}
if (ides.includes('cursor')) {
console.log(' ' + chalk.dim('.cursor/'));
console.log(' ' + chalk.dim('├─ rules.md') + ' - Main configuration');
console.log(' ' + chalk.dim('└─ rules/'));
console.log(' ' + chalk.dim(' ├─ AIOS/') + ' - Core agent rules');
if (selectedSquads && selectedSquads.length > 0) {
selectedSquads.forEach((squad) => {
console.log(' ' + chalk.dim(` └─ ${squad}/`) + ' - Squad rules');
});
}
}
// Show other IDE installations
const otherInstalledIdes = ['gemini', 'antigravity'].filter((ide) =>
ides.includes(ide)
);
for (const ide of otherInstalledIdes) {
const ideDir = ide === 'gemini' ? '.gemini' : `.${ide}`;
console.log(
' ' +
chalk.dim(`${ideDir}/`) +
' - ' +
ide.charAt(0).toUpperCase() +
ide.slice(1) +
' configuration'
);
}
if (ides.includes('github-copilot')) {
console.log(' ' + chalk.dim('.github/chatmodes/') + ' - GitHub Copilot agent modes');
}
console.log('');
console.log(chalk.cyan('📚 Next steps:'));
if (ides.includes('claude')) {
console.log(' ' + chalk.yellow('Claude Code:'));
console.log(' • Use slash commands: /dev, /architect, /qa, /pm, /github-devops');
console.log(' • Browse: .claude/commands/AIOS/agents/ for all available agents');
}
if (ides.includes('cursor')) {
console.log(' ' + chalk.yellow('Cursor:'));
console.log(' • Agent rules auto-loaded from .cursor/rules/');
console.log(' • Use @agent-name to activate agents in chat');
}
if (ides.includes('gemini')) {
console.log(' ' + chalk.yellow('Gemini CLI:'));
console.log(' • Include agent context in your prompts');
}
if (ides.includes('github-copilot')) {
console.log(' ' + chalk.yellow('GitHub Copilot:'));
console.log(' • Open Chat view and select Agent mode');
console.log(' • Requires VS Code 1.101+ with chat.agent.enabled: true');
}
if (ides.includes('antigravity')) {
console.log(' ' + chalk.yellow('AntiGravity:'));
console.log(' • Use Workspace Rules to activate agents');
console.log(' • Browse: .antigravity/rules/AIOS/agents/ for all available agents');
}
console.log(' ' + chalk.yellow('General:'));
console.log(' • Run ' + chalk.yellow('aios validate') + ' to verify installation integrity');
console.log(' • Run ' + chalk.yellow('aios validate --repair') + ' to fix any missing files');
console.log(' • Check .aios-core/user-guide.md for complete documentation');
console.log(' • Explore squads/ for additional capabilities');
console.log('');
console.log(chalk.gray('═'.repeat(80)));
console.log('');
}
/**
* Updates .gitignore file based on installation mode
* INS-2 Performance: Converted to async
*/
async function updateGitIgnore(mode, projectRoot) {
const gitignorePath = path.join(projectRoot, '.gitignore');
let gitignore = '';
if (fs.existsSync(gitignorePath)) {
// INS-2 Performance: Use async read
gitignore = await fse.readFile(gitignorePath, 'utf8');
}
if (mode === 'project-development') {
const frameworkRules = [
'',
'# AIOS-FullStack Framework Files (auto-managed - do not edit)',
'.aios-core/',
'node_modules/@aios/',
'outputs/minds/',
'.aios-installation-config.yaml',
'# End AIOS-FullStack auto-managed section',
'',
];
const hasFrameworkSection = gitignore.includes('# AIOS-FullStack Framework Files');
if (!hasFrameworkSection) {
gitignore += frameworkRules.join('\n');
// INS-2 Performance: Use async write
await fse.writeFile(gitignorePath, gitignore);
}
}
}
/**
* Save PM configuration
* INS-2 Performance: Converted to async
*/
async function savePMConfig(pmTool, config, projectRoot) {
const pmConfigData = {
pm_tool: {
type: pmTool,
configured_at: new Date().toISOString(),
config: config,
},
sync_behavior: {
auto_sync_on_status_change: true,
create_tasks_on_story_creation: false,
bidirectional_sync: false,
},
};
const configPath = path.join(projectRoot, '.aios-pm-config.yaml');
// INS-2 Performance: Use async write
await fse.writeFile(configPath, yaml.dump(pmConfigData));
}
/**
* Setup global statusline for Claude Code (legacy installer version)
* Graceful skip: returns silently if user already has a statusLine configured.
* @param {string} sourceCoreDir - Path to installed .aios-core directory
*/
async function setupGlobalStatuslineLegacy(sourceCoreDir) {
const os = require('os');
const homeDir = os.homedir();
const globalSettingsPath = path.join(homeDir, '.claude', 'settings.json');
// Read existing global settings
let settings = {};
try {
if (fs.existsSync(globalSettingsPath)) {
settings = JSON.parse(fs.readFileSync(globalSettingsPath, 'utf8'));
}
} catch {
settings = {};
}
// GRACEFUL SKIP: User already has a statusLine
if (settings.statusLine) {
return;
}
// Source templates
const templatesDir = path.join(sourceCoreDir, 'product', 'templates', 'statusline');
const scriptSource = path.join(templatesDir, 'statusline-script.js');
const hookSource = path.join(templatesDir, 'track-agent.sh');
if (!fs.existsSync(scriptSource) || !fs.existsSync(hookSource)) {
return;
}
// Target paths
const scriptTarget = path.join(homeDir, '.claude', 'statusline-script.js');
const hookTarget = path.join(homeDir, '.claude', 'hooks', 'track-agent.sh');
try {
await fse.ensureDir(path.join(homeDir, '.claude', 'hooks'));
await fse.ensureDir(path.join(homeDir, '.claude', 'session-cache'));
await fse.copy(scriptSource, scriptTarget);
await fse.copy(hookSource, hookTarget);
} catch {
return;
}
// Add statusLine + hook to settings
const scriptPathEscaped = scriptTarget.replace(/\\/g, '\\\\');
settings.statusLine = {
type: 'command',
command: `node "${scriptPathEscaped}"`,
};
if (!settings.hooks) {
settings.hooks = {};
}
if (!Array.isArray(settings.hooks.UserPromptSubmit)) {
settings.hooks.UserPromptSubmit = [];
}
const hookPathEscaped = hookTarget.replace(/\\/g, '\\\\');
const alreadyHasTrackAgent = settings.hooks.UserPromptSubmit.some(entry => {
if (Array.isArray(entry.hooks)) {
return entry.hooks.some(h => h.command && h.command.includes('track-agent'));
}
return entry.command && entry.command.includes('track-agent');
});
if (!alreadyHasTrackAgent) {
settings.hooks.UserPromptSubmit.push({
matcher: '',
hooks: [
{
type: 'command',
command: `bash "${hookPathEscaped}"`,
},
],
});
}
try {
await fse.ensureDir(path.dirname(globalSettingsPath));
await fse.writeFile(globalSettingsPath, JSON.stringify(settings, null, 2), 'utf8');
} catch {
// Silent failure — statusline is non-critical
}
}
// Run installer with error handling
main().catch((error) => {
console.error('');
console.error(chalk.red('✗ Installation failed: ') + error.message);
console.error('');
process.exit(1);
});