UNPKG

aios-core

Version:

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

362 lines (304 loc) 10.3 kB
'use strict'; const { CodeIntelSource } = require('./data-sources/code-intel-source'); const { RegistrySource } = require('./data-sources/registry-source'); const { MetricsSource } = require('./data-sources/metrics-source'); const { renderTree } = require('./renderers/tree-renderer'); const { renderStats } = require('./renderers/stats-renderer'); const { renderStatus } = require('./renderers/status-renderer'); const { formatAsJson } = require('./formatters/json-formatter'); const { formatAsDot } = require('./formatters/dot-formatter'); const { formatAsMermaid } = require('./formatters/mermaid-formatter'); const { formatAsHtml } = require('./formatters/html-formatter'); const fs = require('fs'); const path = require('path'); const { exec } = require('child_process'); const MAX_SUMMARY_PER_CATEGORY = 5; const DEFAULT_WATCH_INTERVAL_MS = 5000; const DEBOUNCE_MS = 300; const FORMAT_MAP = { json: formatAsJson, dot: formatAsDot, mermaid: formatAsMermaid, html: formatAsHtml, }; const VALID_FORMATS = ['ascii', ...Object.keys(FORMAT_MAP)]; const WATCH_FORMAT_MAP = { dot: { formatter: formatAsDot, filename: 'graph.dot' }, mermaid: { formatter: formatAsMermaid, filename: 'graph.mmd' }, html: { formatter: (graphData) => formatAsHtml(graphData, { autoRefresh: true, refreshInterval: 5 }), filename: 'graph.html', }, }; const COMMANDS = { '--deps': handleDeps, '--stats': handleStats, '--help': handleHelp, '-h': handleHelp, }; /** * Parse CLI arguments into structured args object. * @param {string[]} argv - Raw CLI arguments * @returns {Object} Parsed args */ function parseArgs(argv) { const args = { command: null, format: 'ascii', file: null, interval: 5, watch: false, help: false, }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === '--help' || arg === '-h') { args.help = true; args.command = '--help'; } else if (arg === '--deps') { args.command = '--deps'; } else if (arg === '--stats') { args.command = '--stats'; } else if (arg === '--watch') { args.watch = true; } else if (arg === '--format' && i + 1 < argv.length) { args.format = argv[++i]; } else if (arg.startsWith('--format=')) { args.format = arg.split('=')[1]; } else if (arg === '--interval' && i + 1 < argv.length) { args.interval = parseInt(argv[++i], 10); } else if (arg.startsWith('--interval=')) { args.interval = parseInt(arg.split('=')[1], 10); } else if (arg.startsWith('--') && !args.command) { args.command = arg; } } return args; } /** * Handle --deps command: render dependency tree or formatted output. * If --watch is set, delegates to handleWatch. * @param {Object} args - Parsed CLI args */ async function handleDeps(args) { const format = args.format || 'ascii'; if (format !== 'ascii' && !FORMAT_MAP[format]) { console.error(`Unknown format: ${format}. Valid formats: ${VALID_FORMATS.join(', ')}`); process.exit(1); } if (args.watch) { return handleWatch(args); } const source = new CodeIntelSource(); const graphData = await source.getData(); if (format === 'html') { return handleHtmlOutput(graphData); } if (format !== 'ascii') { const formatter = FORMAT_MAP[format]; process.stdout.write(formatter(graphData) + '\n'); return; } const isTTY = process.stdout.isTTY; const output = renderTree(graphData, { color: isTTY, unicode: isTTY }); console.log(output); } /** * Write HTML graph to .aios/graph.html and open in default browser. * @param {Object} graphData - Normalized graph data * @param {Object} [options] - Options passed to formatAsHtml * @returns {string} Output file path */ function handleHtmlOutput(graphData, options = {}) { const outputDir = path.resolve(process.cwd(), '.aios'); const outputPath = path.join(outputDir, 'graph.html'); fs.mkdirSync(outputDir, { recursive: true }); const html = formatAsHtml(graphData, options); fs.writeFileSync(outputPath, html, 'utf8'); const nodeCount = (graphData.nodes || []).length; console.log(`HTML graph written to ${outputPath} (${nodeCount} entities)`); openInBrowser(outputPath); return outputPath; } /** * Open a file in the default browser (cross-platform). * @param {string} filePath - Absolute path to file */ function openInBrowser(filePath) { const platform = process.platform; const cmd = platform === 'win32' ? 'start ""' : platform === 'darwin' ? 'open' : 'xdg-open'; exec(`${cmd} "${filePath}"`, (err) => { if (err) { console.log(`Could not open browser automatically. Open manually: ${filePath}`); } }); } /** * Handle --watch mode: regenerate graph file on interval and on file changes. * Writes to .aios/graph.dot, .aios/graph.mmd, or .aios/graph.html. * @param {Object} args - Parsed CLI args * @returns {Object} Watch state for cleanup (used by tests) */ async function handleWatch(args) { const watchFormat = WATCH_FORMAT_MAP[args.format] ? args.format : 'dot'; const { formatter, filename } = WATCH_FORMAT_MAP[watchFormat]; const intervalMs = (args.interval || 5) * 1000; const outputDir = path.resolve(process.cwd(), '.aios'); const outputPath = path.join(outputDir, filename); fs.mkdirSync(outputDir, { recursive: true }); const source = new CodeIntelSource(); async function regenerate() { try { const graphData = await source.getData(); const content = formatter(graphData); fs.writeFileSync(outputPath, content, 'utf8'); const nodeCount = (graphData.nodes || []).length; console.log(`[watch] ${filename} updated (${nodeCount} entities)`); } catch (err) { console.error(`[watch] regeneration failed: ${err.message}`); } } await regenerate(); const intervalId = setInterval(regenerate, intervalMs); let fileWatcher = null; let debounceTimer = null; const registryPath = path.resolve(process.cwd(), '.aios-core/data/entity-registry.yaml'); try { if (fs.existsSync(registryPath)) { fileWatcher = fs.watch(registryPath, () => { if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(regenerate, DEBOUNCE_MS); }); } } catch (_err) { // fs.watch not available or path inaccessible; interval-only mode } function cleanup() { clearInterval(intervalId); if (debounceTimer) { clearTimeout(debounceTimer); } if (fileWatcher) { fileWatcher.close(); } console.log('[watch] stopped'); } process.once('SIGINT', () => { cleanup(); process.exit(0); }); return { intervalId, fileWatcher, cleanup, outputPath }; } /** * Handle --stats command: render entity statistics and cache metrics. * @param {Object} args - Parsed CLI args */ async function handleStats(_args) { const registrySource = new RegistrySource(); const metricsSource = new MetricsSource(); const [registryData, metricsData] = await Promise.all([ registrySource.getData(), metricsSource.getData(), ]); const isTTY = process.stdout.isTTY; const output = renderStats(registryData, metricsData, { isTTY: !!isTTY }); process.stdout.write(output + '\n'); } /** * Handle --help command: show usage text. */ function handleHelp() { const usage = ` Usage: aios graph [command] [options] Commands: --deps Show dependency tree as ASCII text --stats Show entity statistics and cache metrics --help, -h Show this help message Options: --format=FORMAT Output format: ascii (default), json, dot, mermaid, html --watch Live mode: regenerate graph file on interval --interval=N Seconds between regeneration in watch mode (default: 5) Examples: aios graph --deps Show dependency tree aios graph --deps --format=json Output as JSON aios graph --deps --format=html Interactive HTML graph (opens browser) aios graph --deps --watch Live DOT file for VS Code preview aios graph --deps --watch --format=html Live HTML with auto-refresh aios graph --deps --watch --format=mermaid Live Mermaid file aios graph --deps --watch --interval=10 Refresh every 10 seconds aios graph --stats Show entity stats and cache metrics `.trim(); console.log(usage); } /** * Handle default summary view: dependency tree (compact) + stats + provider status. * @param {Object} args - Parsed CLI args */ async function handleSummary(args) { const codeIntelSource = new CodeIntelSource(); const registrySource = new RegistrySource(); const metricsSource = new MetricsSource(); const [graphData, registryData, metricsData] = await Promise.all([ codeIntelSource.getData(), registrySource.getData(), metricsSource.getData(), ]); const isTTY = !!process.stdout.isTTY; const sections = []; sections.push('AIOS Graph Dashboard'); sections.push(isTTY ? '\u2550'.repeat(35) : '='.repeat(35)); sections.push(''); const treeOutput = renderTree(graphData, { color: isTTY, unicode: isTTY, maxPerCategory: MAX_SUMMARY_PER_CATEGORY, }); sections.push(treeOutput); sections.push(''); const statsOutput = renderStats(registryData, metricsData, { isTTY }); sections.push(statsOutput); sections.push(''); const statusOutput = renderStatus(metricsData, { isTTY }); sections.push(statusOutput); process.stdout.write(sections.join('\n') + '\n'); } /** * Main CLI entry point. * @param {string[]} argv - Raw CLI arguments */ async function run(argv) { const args = parseArgs(argv); if (args.help) { handleHelp(); return; } if (args.command === null) { return handleSummary(args); } const handler = COMMANDS[args.command]; if (!handler) { console.error(`Unknown command: ${args.command}`); handleHelp(); process.exit(1); } return handler(args); } module.exports = { run, parseArgs, handleDeps, handleStats, handleHelp, handleWatch, handleSummary, handleHtmlOutput, openInBrowser, MAX_SUMMARY_PER_CATEGORY, DEFAULT_WATCH_INTERVAL_MS, DEBOUNCE_MS, FORMAT_MAP, VALID_FORMATS, WATCH_FORMAT_MAP, };