aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
581 lines (493 loc) • 19.3 kB
JavaScript
/**
* CLI Commands - Story 0.9
*
* Epic: Epic 0 - ADE Master Orchestrator
*
* CLI command handlers for orchestrator control.
*
* Features:
* - AC1: *orchestrate {story-id} starts full pipeline
* - AC2: *orchestrate-status {story-id} shows progress
* - AC3: *orchestrate-stop {story-id} stops execution
* - AC4: *orchestrate-resume {story-id} continues execution
* - AC5: --epic N flag for specific epic start
* - AC6: --dry-run flag for preview
* - AC7: Commands available globally
*
* @module core/orchestration/cli-commands
* @version 1.0.0
*/
const fs = require('fs-extra');
const path = require('path');
const MasterOrchestrator = require('./master-orchestrator');
// Optional chalk for colored output
let chalk;
try {
chalk = require('chalk');
} catch {
chalk = {
blue: (s) => s,
green: (s) => s,
red: (s) => s,
yellow: (s) => s,
cyan: (s) => s,
gray: (s) => s,
bold: (s) => s,
dim: (s) => s,
magenta: (s) => s,
};
}
// ═══════════════════════════════════════════════════════════════════════════════════
// COMMAND: orchestrate (AC1)
// ═══════════════════════════════════════════════════════════════════════════════════
/**
* Execute orchestrate command (AC1)
*
* @param {string} storyId - Story ID to orchestrate
* @param {Object} options - Command options
* @param {number} [options.epic] - Start from specific epic (AC5)
* @param {boolean} [options.dryRun] - Preview without execution (AC6)
* @param {boolean} [options.strict] - Enable strict gate mode
* @param {string} [options.projectRoot] - Project root path
* @returns {Promise<Object>} Command result
*/
async function orchestrate(storyId, options = {}) {
const projectRoot = options.projectRoot || process.cwd();
if (!storyId) {
return {
success: false,
exitCode: 3,
error: 'Story ID is required',
};
}
console.log(chalk.cyan('\n═══════════════════════════════════════════════════════════'));
console.log(chalk.cyan.bold(` 🚀 AIOS Orchestrator: ${storyId}`));
console.log(chalk.cyan('═══════════════════════════════════════════════════════════\n'));
// Dry run mode (AC6)
if (options.dryRun) {
return await orchestrateDryRun(storyId, options);
}
try {
const orchestrator = new MasterOrchestrator(projectRoot, {
storyId,
strictGates: options.strict ?? false,
dashboardAutoUpdate: true,
});
// Start dashboard
await orchestrator.startDashboard();
// Setup event handlers
setupEventHandlers(orchestrator);
let result;
// Start from specific epic (AC5)
if (options.epic) {
console.log(chalk.yellow(`Starting from Epic ${options.epic}...`));
result = await orchestrator.resumeFromEpic(options.epic);
} else {
console.log(chalk.green('Starting full pipeline...'));
result = await orchestrator.executeFullPipeline();
}
// Stop dashboard
orchestrator.stopDashboard();
// Display final result
displayResult(result);
return {
success: result.success,
exitCode: result.success ? 0 : result.blocked ? 2 : 1,
result,
};
} catch (error) {
console.log(chalk.red(`\n❌ Orchestration failed: ${error.message}`));
return {
success: false,
exitCode: 1,
error: error.message,
};
}
}
/**
* Dry run orchestration (AC6)
* @private
*/
async function orchestrateDryRun(storyId, options) {
console.log(chalk.yellow('🔍 DRY RUN MODE - No actual execution\n'));
const projectRoot = options.projectRoot || process.cwd();
const orchestrator = new MasterOrchestrator(projectRoot, {
storyId,
strictGates: options.strict ?? false,
});
// Initialize to detect tech stack
await orchestrator.initialize();
// Display what would happen
console.log(chalk.bold('Pipeline Preview:'));
console.log(chalk.gray('─'.repeat(50)));
const epicConfig = orchestrator.constructor.EPIC_CONFIG;
const startEpic = options.epic || 3;
// Use dynamic epic list from config (excludes onDemand epics like Epic 5)
const epicNums = Object.keys(epicConfig)
.map(Number)
.filter((num) => !epicConfig[num].onDemand)
.sort((a, b) => a - b);
for (const epicNum of epicNums) {
const config = epicConfig[epicNum];
if (epicNum < startEpic) {
console.log(chalk.gray(` ⏭️ Epic ${epicNum}: ${config.name} (skipped)`));
} else {
console.log(chalk.cyan(` ▶️ Epic ${epicNum}: ${config.name}`));
}
}
console.log(chalk.gray('─'.repeat(50)));
if (orchestrator.executionState.techStackProfile) {
console.log(chalk.bold('\nDetected Tech Stack:'));
const tech = orchestrator.executionState.techStackProfile;
if (tech.hasDatabase)
console.log(chalk.green(` ✓ Database: ${tech.database?.type || 'detected'}`));
if (tech.hasFrontend)
console.log(chalk.green(` ✓ Frontend: ${tech.frontend?.framework || 'detected'}`));
if (tech.hasBackend)
console.log(chalk.green(` ✓ Backend: ${tech.backend?.framework || 'detected'}`));
}
console.log(chalk.yellow('\n✅ Dry run complete. Run without --dry-run to execute.\n'));
return {
success: true,
exitCode: 0,
dryRun: true,
};
}
// ═══════════════════════════════════════════════════════════════════════════════════
// COMMAND: orchestrate-status (AC2)
// ═══════════════════════════════════════════════════════════════════════════════════
/**
* Show orchestrator status (AC2)
*
* @param {string} storyId - Story ID to check
* @param {Object} options - Command options
* @param {string} [options.projectRoot] - Project root path
* @returns {Promise<Object>} Command result
*/
async function orchestrateStatus(storyId, options = {}) {
const projectRoot = options.projectRoot || process.cwd();
if (!storyId) {
return {
success: false,
exitCode: 3,
error: 'Story ID is required',
};
}
const statePath = path.join(projectRoot, '.aios', 'master-orchestrator', `${storyId}.json`);
if (!(await fs.pathExists(statePath))) {
console.log(chalk.yellow(`\n⚠️ No orchestrator state found for ${storyId}`));
console.log(chalk.gray(` Run *orchestrate ${storyId} to start.\n`));
return {
success: false,
exitCode: 1,
error: 'State not found',
};
}
try {
const state = await fs.readJson(statePath);
console.log(chalk.cyan(`\n📊 Orchestrator Status: ${storyId}`));
console.log(chalk.gray('═'.repeat(50)));
// State
console.log(`\nState: ${formatState(state.status)}`);
console.log(`Current Epic: ${state.currentEpic || 'N/A'}`);
// Progress
const progress = calculateProgress(state);
console.log(`Progress: ${formatProgress(progress)}%`);
// Epic status
console.log(chalk.bold('\nEpic Status:'));
const epicConfig = MasterOrchestrator.EPIC_CONFIG;
for (const [num, epic] of Object.entries(state.epics || {})) {
const config = epicConfig[num] || { name: `Epic ${num}` };
console.log(
` ${formatEpicStatus(epic.status)} Epic ${num}: ${config.name} - ${epic.status}`,
);
}
// Timing
console.log(chalk.bold('\nTiming:'));
console.log(` Started: ${formatDate(state.startedAt)}`);
console.log(` Updated: ${formatDate(state.updatedAt)}`);
// Errors
const errorCount = (state.errors || []).length;
console.log(`\nErrors: ${errorCount > 0 ? chalk.red(errorCount) : chalk.green('0')}`);
console.log(`Blocked: ${state.status === 'blocked' ? chalk.red('Yes') : chalk.green('No')}`);
console.log(chalk.gray('\n' + '═'.repeat(50) + '\n'));
return {
success: true,
exitCode: 0,
state,
};
} catch (error) {
console.log(chalk.red(`\n❌ Failed to read status: ${error.message}\n`));
return {
success: false,
exitCode: 1,
error: error.message,
};
}
}
// ═══════════════════════════════════════════════════════════════════════════════════
// COMMAND: orchestrate-stop (AC3)
// ═══════════════════════════════════════════════════════════════════════════════════
/**
* Stop orchestrator execution (AC3)
*
* @param {string} storyId - Story ID to stop
* @param {Object} options - Command options
* @param {string} [options.projectRoot] - Project root path
* @returns {Promise<Object>} Command result
*/
async function orchestrateStop(storyId, options = {}) {
const projectRoot = options.projectRoot || process.cwd();
if (!storyId) {
return {
success: false,
exitCode: 3,
error: 'Story ID is required',
};
}
const statePath = path.join(projectRoot, '.aios', 'master-orchestrator', `${storyId}.json`);
if (!(await fs.pathExists(statePath))) {
console.log(chalk.yellow(`\n⚠️ No orchestrator state found for ${storyId}\n`));
return {
success: false,
exitCode: 1,
error: 'State not found',
};
}
try {
console.log(chalk.yellow(`\n🛑 Stopping orchestrator for ${storyId}...`));
const state = await fs.readJson(statePath);
// Update state to stopped
state.status = 'stopped';
state.updatedAt = new Date().toISOString();
await fs.writeJson(statePath, state, { spaces: 2 });
console.log(chalk.gray(`\nCurrent state: ${state.status}`));
console.log(chalk.gray(`Current epic: ${state.currentEpic || 'N/A'}`));
console.log(chalk.gray(`\nState saved at: ${statePath}`));
console.log(chalk.green('\n✅ Orchestrator stopped successfully.'));
console.log(chalk.gray(` Run *orchestrate-resume ${storyId} to continue.\n`));
return {
success: true,
exitCode: 0,
};
} catch (error) {
console.log(chalk.red(`\n❌ Failed to stop: ${error.message}\n`));
return {
success: false,
exitCode: 1,
error: error.message,
};
}
}
// ═══════════════════════════════════════════════════════════════════════════════════
// COMMAND: orchestrate-resume (AC4)
// ═══════════════════════════════════════════════════════════════════════════════════
/**
* Resume orchestrator execution (AC4)
*
* @param {string} storyId - Story ID to resume
* @param {Object} options - Command options
* @param {string} [options.projectRoot] - Project root path
* @returns {Promise<Object>} Command result
*/
async function orchestrateResume(storyId, options = {}) {
const projectRoot = options.projectRoot || process.cwd();
if (!storyId) {
return {
success: false,
exitCode: 3,
error: 'Story ID is required',
};
}
const statePath = path.join(projectRoot, '.aios', 'master-orchestrator', `${storyId}.json`);
if (!(await fs.pathExists(statePath))) {
console.log(chalk.yellow(`\n⚠️ No saved state found for ${storyId}`));
console.log(chalk.gray(` Run *orchestrate ${storyId} to start fresh.\n`));
return {
success: false,
exitCode: 1,
error: 'State not found',
};
}
try {
const state = await fs.readJson(statePath);
// Check if resumable
if (state.status === 'complete') {
console.log(chalk.green(`\n✅ Story ${storyId} already completed.`));
console.log(chalk.gray(` Run *orchestrate ${storyId} --epic 3 to restart.\n`));
return {
success: false,
exitCode: 2,
error: 'Already completed',
};
}
console.log(chalk.cyan(`\n🔄 Resuming orchestrator for ${storyId}...`));
console.log(chalk.gray(`\nLoading state from: ${statePath}`));
console.log(chalk.bold('\nPrevious state:'));
console.log(chalk.gray(` Status: ${state.status}`));
console.log(chalk.gray(` Last Epic: ${state.currentEpic || 'N/A'}`));
console.log(chalk.gray(` Stopped at: ${formatDate(state.updatedAt)}`));
// Find resume point
let resumeEpic = state.currentEpic || 3;
const epicState = state.epics?.[resumeEpic];
if (epicState?.status === 'completed') {
// Find next incomplete epic
for (const num of [3, 4, 6, 7]) {
if (state.epics?.[num]?.status !== 'completed') {
resumeEpic = num;
break;
}
}
}
console.log(chalk.yellow(`\nResuming from Epic ${resumeEpic}...\n`));
// Create orchestrator and resume
const orchestrator = new MasterOrchestrator(projectRoot, {
storyId,
dashboardAutoUpdate: true,
});
await orchestrator.startDashboard();
setupEventHandlers(orchestrator);
const result = await orchestrator.resumeFromEpic(resumeEpic);
orchestrator.stopDashboard();
displayResult(result);
return {
success: result.success,
exitCode: result.success ? 0 : result.blocked ? 2 : 1,
result,
};
} catch (error) {
console.log(chalk.red(`\n❌ Failed to resume: ${error.message}\n`));
return {
success: false,
exitCode: 1,
error: error.message,
};
}
}
// ═══════════════════════════════════════════════════════════════════════════════════
// HELPER FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════════════
/**
* Setup event handlers for orchestrator
* @private
*/
function setupEventHandlers(orchestrator) {
orchestrator.on('stateChange', (data) => {
console.log(chalk.gray(` 📊 State: ${data.oldState} → ${data.newState}`));
});
orchestrator.on('epicStart', (data) => {
const epicConfig = orchestrator.constructor.EPIC_CONFIG;
const config = epicConfig[data.epicNum] || {};
console.log(chalk.cyan(`\n📝 Starting Epic ${data.epicNum}: ${config.name || 'Unknown'}`));
if (config.description) {
console.log(chalk.gray(` ${config.description}`));
}
});
orchestrator.on('epicComplete', (data) => {
console.log(chalk.green(` ✅ Epic ${data.epicNum} complete`));
if (data.gateResult) {
console.log(chalk.gray(` Gate: ${data.gateResult.verdict}`));
}
});
}
/**
* Display final result
* @private
*/
function displayResult(result) {
console.log(chalk.cyan('\n═══════════════════════════════════════════════════════════'));
if (result.success) {
console.log(chalk.green.bold(' ✅ ORCHESTRATION COMPLETE'));
} else if (result.blocked) {
console.log(chalk.red.bold(' 🚫 ORCHESTRATION BLOCKED'));
} else {
console.log(chalk.red.bold(' ❌ ORCHESTRATION FAILED'));
}
console.log(chalk.cyan('═══════════════════════════════════════════════════════════'));
console.log(chalk.gray(`\nDuration: ${result.duration || 'N/A'}`));
console.log(chalk.gray(`Epics Executed: ${result.epics?.executed?.length || 0}`));
if (result.errors?.length > 0) {
console.log(chalk.red(`\nErrors: ${result.errors.length}`));
for (const err of result.errors.slice(0, 3)) {
console.log(chalk.red(` - ${err.message || err}`));
}
}
console.log('');
}
/**
* Format state for display
* @private
*/
function formatState(status) {
const colors = {
initialized: chalk.gray,
ready: chalk.cyan,
in_progress: chalk.yellow,
complete: chalk.green,
blocked: chalk.red,
stopped: chalk.yellow,
failed: chalk.red,
};
return (colors[status] || chalk.white)(status);
}
/**
* Format epic status for display
* @private
*/
function formatEpicStatus(status) {
const icons = {
pending: '⏸️',
in_progress: '⏳',
completed: '✅',
failed: '❌',
skipped: '⏭️',
};
return icons[status] || '⬜';
}
/**
* Format progress as percentage
* @private
*/
function formatProgress(progress) {
if (progress >= 100) return chalk.green(progress);
if (progress >= 50) return chalk.yellow(progress);
return chalk.gray(progress);
}
/**
* Format date for display
* @private
*/
function formatDate(dateStr) {
if (!dateStr) return 'N/A';
try {
return new Date(dateStr).toLocaleString();
} catch {
return dateStr;
}
}
/**
* Calculate progress from state
* @private
*/
function calculateProgress(state) {
if (!state.epics) return 0;
const epics = [3, 4, 6, 7];
const completed = epics.filter((num) => state.epics[num]?.status === 'completed').length;
return Math.round((completed / epics.length) * 100);
}
// ═══════════════════════════════════════════════════════════════════════════════════
// EXPORTS
// ═══════════════════════════════════════════════════════════════════════════════════
module.exports = {
orchestrate,
orchestrateStatus,
orchestrateStop,
orchestrateResume,
// Aliases for command parsing
commands: {
orchestrate: orchestrate,
'orchestrate-status': orchestrateStatus,
'orchestrate-stop': orchestrateStop,
'orchestrate-resume': orchestrateResume,
},
};