aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
541 lines (464 loc) • 14.9 kB
JavaScript
/**
* IDE Sync - Main orchestrator for syncing agents to IDEs
* @story 6.19 - IDE Command Auto-Sync System
*
* Commands:
* sync - Sync agents to all enabled IDEs
* validate - Validate sync status (report mode)
* report - Generate sync status report
*
* Flags:
* --ide <name> - Sync specific IDE only
* --strict - Exit with code 1 if drift detected (CI mode)
* --dry-run - Preview changes without writing
* --verbose - Show detailed output
* --quiet - Minimal output (for pre-commit hooks)
*/
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
const { parseAllAgents } = require('./agent-parser');
const { generateAllRedirects, writeRedirects } = require('./redirect-generator');
const { validateAllIdes, formatValidationReport } = require('./validator');
const { syncGeminiCommands, buildGeminiCommandFiles } = require('./gemini-commands');
// Transformers
const claudeCodeTransformer = require('./transformers/claude-code');
const cursorTransformer = require('./transformers/cursor');
const antigravityTransformer = require('./transformers/antigravity');
const githubCopilotTransformer = require('./transformers/github-copilot');
// ANSI colors for output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
};
/**
* Load core-config.yaml and extract ideSync section
* @param {string} projectRoot - Project root directory
* @returns {object} - ideSync configuration
*/
function loadConfig(projectRoot) {
const configPath = path.join(projectRoot, '.aios-core', 'core-config.yaml');
// Default configuration
const defaultConfig = {
enabled: true,
source: '.aios-core/development/agents',
targets: {
'claude-code': {
enabled: true,
path: '.claude/commands/AIOS/agents',
format: 'full-markdown-yaml',
},
codex: {
enabled: true,
path: '.codex/agents',
format: 'full-markdown-yaml',
},
gemini: {
enabled: true,
path: '.gemini/rules/AIOS/agents',
format: 'full-markdown-yaml',
},
'github-copilot': {
enabled: true,
path: '.github/agents',
format: 'github-copilot',
},
cursor: {
enabled: true,
path: '.cursor/rules/agents',
format: 'condensed-rules',
},
antigravity: {
enabled: true,
path: '.antigravity/rules/agents',
format: 'cursor-style',
},
},
redirects: {
'aios-developer': 'aios-master',
'aios-orchestrator': 'aios-master',
'db-sage': 'data-engineer',
'github-devops': 'devops',
},
validation: {
strictMode: true,
failOnDrift: true,
failOnOrphaned: false,
},
};
try {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf8');
const config = yaml.load(content);
if (config && config.ideSync) {
return { ...defaultConfig, ...config.ideSync };
}
}
} catch (error) {
console.warn(`${colors.yellow}Warning: Could not load config, using defaults${colors.reset}`);
}
return defaultConfig;
}
/**
* Get transformer for IDE format
* @param {string} format - IDE format name
* @returns {object} - Transformer module
*/
function getTransformer(format) {
const transformers = {
'full-markdown-yaml': claudeCodeTransformer,
'condensed-rules': cursorTransformer,
'cursor-style': antigravityTransformer,
'github-copilot': githubCopilotTransformer,
};
return transformers[format] || claudeCodeTransformer;
}
/**
* Sync agents to a specific IDE
* @param {object[]} agents - Parsed agent data
* @param {object} ideConfig - IDE configuration
* @param {string} ideName - IDE name
* @param {string} projectRoot - Project root
* @param {object} options - Sync options
* @returns {object} - Sync result
*/
function syncIde(agents, ideConfig, ideName, projectRoot, options) {
const result = {
ide: ideName,
targetDir: path.join(projectRoot, ideConfig.path),
files: [],
errors: [],
};
if (!ideConfig.enabled) {
result.skipped = true;
return result;
}
const transformer = getTransformer(ideConfig.format);
// Ensure target directory exists
if (!options.dryRun) {
fs.ensureDirSync(result.targetDir);
}
// Transform and write each agent
for (const agent of agents) {
// Skip agents with fatal errors (no YAML block found or failed parse with no fallback)
if (agent.error && agent.error === 'Failed to parse YAML') {
result.errors.push({
agent: agent.id,
error: agent.error,
});
continue;
}
if (agent.error && agent.error === 'No YAML block found') {
result.errors.push({
agent: agent.id,
error: agent.error,
});
continue;
}
try {
const content = transformer.transform(agent);
const filename = transformer.getFilename(agent);
const targetPath = path.join(result.targetDir, filename);
if (!options.dryRun) {
fs.writeFileSync(targetPath, content, 'utf8');
}
result.files.push({
agent: agent.id,
filename,
path: targetPath,
content,
});
} catch (error) {
result.errors.push({
agent: agent.id,
error: error.message,
});
}
}
return result;
}
/**
* Execute sync command
* @param {object} options - Command options
*/
async function commandSync(options) {
const projectRoot = process.cwd();
const config = loadConfig(projectRoot);
if (!config.enabled) {
if (!options.quiet) {
console.log(`${colors.yellow}IDE sync is disabled in config${colors.reset}`);
}
return;
}
if (!options.quiet) {
console.log(`${colors.bright}${colors.blue}🔄 IDE Sync${colors.reset}`);
console.log('');
}
// Parse all agents
const agentsDir = path.join(projectRoot, config.source);
if (!options.quiet) {
console.log(`${colors.dim}Source: ${agentsDir}${colors.reset}`);
}
const agents = parseAllAgents(agentsDir);
if (!options.quiet) {
console.log(`${colors.dim}Found ${agents.length} agents${colors.reset}`);
console.log('');
}
// Filter IDEs if --ide flag specified
let targetIdes = Object.entries(config.targets);
if (options.ide) {
targetIdes = targetIdes.filter(([name]) => name === options.ide);
if (targetIdes.length === 0) {
console.error(`${colors.red}Error: IDE '${options.ide}' not found in config${colors.reset}`);
process.exit(1);
}
}
const results = [];
// Sync to each IDE
for (const [ideName, ideConfig] of targetIdes) {
if (!ideConfig.enabled) {
if (!options.quiet) {
console.log(`${colors.dim}⏭️ ${ideName}: skipped (disabled)${colors.reset}`);
}
continue;
}
if (!options.quiet) {
console.log(`${colors.cyan}📁 Syncing ${ideName}...${colors.reset}`);
}
const result = syncIde(agents, ideConfig, ideName, projectRoot, options);
// Gemini CLI: also sync slash launcher command files (.gemini/commands/*.toml)
if (ideName === 'gemini') {
const geminiCommands = syncGeminiCommands(agents, projectRoot, options);
result.commandFiles = geminiCommands.files;
} else {
result.commandFiles = [];
}
results.push(result);
// Generate redirects for this IDE
const redirects = generateAllRedirects(config.redirects, result.targetDir, ideConfig.format);
const redirectResult = writeRedirects(redirects, options.dryRun);
if (options.verbose && !options.quiet) {
console.log(` ${colors.dim}Target: ${result.targetDir}${colors.reset}`);
}
const agentCount = result.files.length;
const commandCount = (result.commandFiles || []).length;
const redirectCount = redirectResult.written.length;
const errorCount = result.errors.length;
if (!options.quiet) {
let status = `${colors.green}✓${colors.reset}`;
if (errorCount > 0) {
status = `${colors.yellow}⚠${colors.reset}`;
}
console.log(
` ${status} ${agentCount} agents${commandCount > 0 ? `, ${commandCount} commands` : ''}, ${redirectCount} redirects${errorCount > 0 ? `, ${errorCount} errors` : ''}`
);
if (options.verbose && result.errors.length > 0) {
for (const err of result.errors) {
console.log(` ${colors.red}✗ ${err.agent}: ${err.error}${colors.reset}`);
}
}
}
}
// Summary
const totalFiles = results.reduce((sum, r) => sum + r.files.length + (r.commandFiles || []).length, 0);
const totalRedirects =
Object.keys(config.redirects).length * targetIdes.filter(([, c]) => c.enabled).length;
const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
if (!options.quiet) {
console.log('');
if (options.dryRun) {
console.log(
`${colors.yellow}Dry run: ${totalFiles} agents + ${totalRedirects} redirects would be written${colors.reset}`
);
} else {
console.log(
`${colors.green}✅ Sync complete: ${totalFiles} agents + ${totalRedirects} redirects${colors.reset}`
);
}
if (totalErrors > 0) {
console.log(`${colors.yellow}⚠️ ${totalErrors} errors occurred${colors.reset}`);
}
}
}
/**
* Execute validate command
* @param {object} options - Command options
*/
async function commandValidate(options) {
const projectRoot = process.cwd();
const config = loadConfig(projectRoot);
if (!config.enabled) {
console.log(`${colors.yellow}IDE sync is disabled in config${colors.reset}`);
return;
}
console.log(`${colors.bright}${colors.blue}🔍 IDE Sync Validation${colors.reset}`);
console.log('');
// Parse all agents
const agentsDir = path.join(projectRoot, config.source);
const agents = parseAllAgents(agentsDir);
// Build expected files for each IDE
const ideConfigs = {};
let targetIdes = Object.entries(config.targets).filter(([, ideConfig]) => ideConfig.enabled);
// Filter IDEs if --ide flag specified
if (options.ide) {
targetIdes = targetIdes.filter(([name]) => name === options.ide);
if (targetIdes.length === 0) {
console.error(`${colors.red}Error: IDE '${options.ide}' not found in config${colors.reset}`);
process.exit(1);
}
}
for (const [ideName, ideConfig] of targetIdes) {
const transformer = getTransformer(ideConfig.format);
const expectedFiles = [];
for (const agent of agents) {
if (agent.error) continue;
try {
const content = transformer.transform(agent);
const filename = transformer.getFilename(agent);
expectedFiles.push({ filename, content });
} catch (error) {
// Skip agents that fail to transform
}
}
// Add redirect files
const redirects = generateAllRedirects(
config.redirects,
path.join(projectRoot, ideConfig.path),
ideConfig.format
);
for (const redirect of redirects) {
expectedFiles.push({
filename: redirect.filename,
content: redirect.content,
});
}
ideConfigs[ideName] = {
expectedFiles,
targetDir: path.join(projectRoot, ideConfig.path),
};
// Gemini CLI command launcher files are synced under .gemini/commands/*.toml
if (ideName === 'gemini') {
const commandFiles = buildGeminiCommandFiles(agents).map((entry) => ({
filename: entry.filename,
content: entry.content,
}));
ideConfigs['gemini-commands'] = {
expectedFiles: commandFiles,
targetDir: path.join(projectRoot, '.gemini', 'commands'),
};
}
}
// Validate
const results = validateAllIdes(ideConfigs, config.redirects);
// Output report
const report = formatValidationReport(results, options.verbose);
console.log(report);
// Exit code
if (options.strict && !results.summary.pass) {
console.log('');
console.log(`${colors.red}Validation failed in strict mode${colors.reset}`);
process.exit(1);
}
}
/**
* Parse command line arguments
* @returns {object} - Parsed options
*/
function parseArgs() {
const args = process.argv.slice(2);
const options = {
command: args[0] || 'sync',
ide: null,
strict: false,
dryRun: false,
verbose: false,
quiet: false,
};
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg === '--ide' && args[i + 1]) {
options.ide = args[++i];
} else if (arg === '--strict') {
options.strict = true;
} else if (arg === '--dry-run') {
options.dryRun = true;
} else if (arg === '--verbose' || arg === '-v') {
options.verbose = true;
} else if (arg === '--quiet' || arg === '-q') {
options.quiet = true;
}
}
return options;
}
/**
* Show help
*/
function showHelp() {
console.log(`
${colors.bright}IDE Sync${colors.reset} - Sync AIOS agents to IDE command files
${colors.bright}Usage:${colors.reset}
node ide-sync/index.js <command> [options]
${colors.bright}Commands:${colors.reset}
sync Sync agents to all enabled IDEs (default)
validate Validate sync status
report Generate sync status report (alias for validate)
${colors.bright}Options:${colors.reset}
--ide <name> Sync/validate specific IDE only
--strict Exit with code 1 if drift detected (CI mode)
--dry-run Preview changes without writing files
--verbose, -v Show detailed output
--quiet, -q Minimal output (for pre-commit hooks)
${colors.bright}Examples:${colors.reset}
node ide-sync/index.js sync
node ide-sync/index.js sync --ide codex
node ide-sync/index.js sync --ide gemini
node ide-sync/index.js sync --ide cursor
node ide-sync/index.js validate --ide gemini --strict
node ide-sync/index.js validate --strict
node ide-sync/index.js sync --dry-run --verbose
`);
}
/**
* Main entry point
*/
async function main() {
const options = parseArgs();
if (options.command === 'help' || options.command === '--help' || options.command === '-h') {
showHelp();
return;
}
switch (options.command) {
case 'sync':
await commandSync(options);
break;
case 'validate':
case 'report':
await commandValidate(options);
break;
default:
console.error(`${colors.red}Unknown command: ${options.command}${colors.reset}`);
showHelp();
process.exit(1);
}
}
// Run if executed directly
if (require.main === module) {
main().catch((error) => {
console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
process.exit(1);
});
}
module.exports = {
loadConfig,
getTransformer,
syncIde,
commandSync,
commandValidate,
};