UNPKG

aios-core

Version:

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

621 lines (539 loc) 17.5 kB
#!/usr/bin/env node /** * AIOS Asset Inventory Generator * Story 2.1: Creates comprehensive inventory of all AIOS assets * * Usage: * node asset-inventory.js [--verbose] [--json] [--output path] * * @module asset-inventory */ const fs = require('fs').promises; const path = require('path'); const yaml = require('js-yaml'); // Asset locations relative to project root const ASSET_PATHS = { agents: '.aios-core/development/agents', tasks: '.aios-core/development/tasks', templates: '.aios-core/product/templates', checklists: '.aios-core/product/checklists', scripts: '.aios-core/infrastructure/scripts', schemas: '.aios-core/schemas', data: '.aios-core/development/data', }; // File patterns for each asset type const ASSET_PATTERNS = { agents: /\.md$/, tasks: /\.md$/, templates: /\.(yaml|yml|md)$/, checklists: /\.md$/, scripts: /\.js$/, schemas: /\.(json|js)$/, data: /\.(md|yaml|yml|json)$/, }; /** * Extract YAML content from markdown file */ function extractYamlFromMarkdown(content) { const yamlBlockMatch = content.match(/```yaml\n([\s\S]*?)\n```/); if (yamlBlockMatch) { try { return yaml.load(yamlBlockMatch[1]); } catch (e) { return null; } } const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); if (frontmatterMatch) { try { return yaml.load(frontmatterMatch[1]); } catch (e) { return null; } } return null; } /** * Extract dependencies from an agent file */ function extractAgentDependencies(parsed, content) { const deps = { tasks: [], templates: [], checklists: [], data: [], tools: [], }; if (parsed?.dependencies) { if (Array.isArray(parsed.dependencies.tasks)) { deps.tasks = parsed.dependencies.tasks; } if (Array.isArray(parsed.dependencies.templates)) { deps.templates = parsed.dependencies.templates; } if (Array.isArray(parsed.dependencies.checklists)) { deps.checklists = parsed.dependencies.checklists; } if (Array.isArray(parsed.dependencies.data)) { deps.data = parsed.dependencies.data; } if (Array.isArray(parsed.dependencies.tools)) { deps.tools = parsed.dependencies.tools; } } return deps; } /** * Extract dependencies from a task file */ function extractTaskDependencies(parsed, content) { const deps = { templates: [], checklists: [], tools: [], }; // Check frontmatter if (parsed?.templates) deps.templates = parsed.templates; if (parsed?.checklists) deps.checklists = parsed.checklists; if (parsed?.tools) deps.tools = parsed.tools; // Check content for references const templateRefs = content.match(/templates?\/[a-z0-9-]+\.(yaml|yml|md)/gi) || []; const checklistRefs = content.match(/checklists?\/[a-z0-9-]+\.md/gi) || []; templateRefs.forEach((ref) => { const name = path.basename(ref); if (!deps.templates.includes(name)) deps.templates.push(name); }); checklistRefs.forEach((ref) => { const name = path.basename(ref); if (!deps.checklists.includes(name)) deps.checklists.push(name); }); return deps; } /** * Detect schema version (V2 or V3) */ function detectVersion(parsed) { if (parsed?.autoClaude?.version === '3.0') { return 'v3'; } return 'v2'; } /** * Scan a directory for assets */ async function scanDirectory(dirPath, pattern, rootPath) { const assets = []; try { const files = await fs.readdir(dirPath); for (const file of files) { if (!pattern.test(file)) continue; const fullPath = path.join(dirPath, file); const stat = await fs.stat(fullPath); if (!stat.isFile()) continue; const relativePath = path.relative(rootPath, fullPath); const content = await fs.readFile(fullPath, 'utf-8'); assets.push({ file, path: relativePath, size: stat.size, modified: stat.mtime.toISOString(), content, }); } } catch (error) { // Directory doesn't exist return []; } return assets; } /** * Process agent assets */ function processAgents(rawAssets) { return rawAssets.map((asset) => { const parsed = extractYamlFromMarkdown(asset.content) || {}; return { type: 'agent', id: parsed.agent?.id || path.basename(asset.file, '.md'), name: parsed.agent?.name || 'Unknown', title: parsed.agent?.title || 'Unknown', path: asset.path, version: detectVersion(parsed), dependencies: extractAgentDependencies(parsed, asset.content), commandCount: Array.isArray(parsed.commands) ? parsed.commands.length : 0, size: asset.size, modified: asset.modified, }; }); } /** * Process task assets */ function processTasks(rawAssets) { return rawAssets.map((asset) => { const parsed = extractYamlFromMarkdown(asset.content) || {}; // Extract task name from various formats let taskName = parsed.task?.name || parsed.task || null; if (typeof taskName === 'object') taskName = taskName?.name || null; if (!taskName) taskName = path.basename(asset.file, '.md'); return { type: 'task', id: path.basename(asset.file, '.md'), name: taskName, responsavel: parsed.task?.responsavel || parsed.responsavel || 'Unknown', path: asset.path, version: detectVersion(parsed), dependencies: extractTaskDependencies(parsed, asset.content), elicit: parsed.elicit || parsed.task?.elicit || false, size: asset.size, modified: asset.modified, }; }); } /** * Process template assets */ function processTemplates(rawAssets) { return rawAssets.map((asset) => { const ext = path.extname(asset.file); let parsed = null; if (ext === '.yaml' || ext === '.yml') { try { parsed = yaml.load(asset.content); } catch (e) { // Invalid YAML } } else if (ext === '.md') { parsed = extractYamlFromMarkdown(asset.content); } return { type: 'template', id: path.basename(asset.file, ext), name: parsed?.name || path.basename(asset.file, ext), format: ext.substring(1), path: asset.path, size: asset.size, modified: asset.modified, }; }); } /** * Process checklist assets */ function processChecklists(rawAssets) { return rawAssets.map((asset) => { const parsed = extractYamlFromMarkdown(asset.content); // Count checklist items const itemCount = (asset.content.match(/^- \[[ x]\]/gm) || []).length; return { type: 'checklist', id: path.basename(asset.file, '.md'), name: parsed?.name || path.basename(asset.file, '.md'), path: asset.path, itemCount, size: asset.size, modified: asset.modified, }; }); } /** * Process script assets */ function processScripts(rawAssets) { return rawAssets.map((asset) => { // Extract module description from JSDoc const descMatch = asset.content.match(/@module\s+([^\n]+)/); const description = descMatch ? descMatch[1].trim() : null; // Check for exports const hasExports = asset.content.includes('module.exports') || asset.content.includes('exports.'); // Check if it's a CLI script const isCli = asset.content.includes('#!/usr/bin/env node'); return { type: 'script', id: path.basename(asset.file, '.js'), path: asset.path, description, hasExports, isCli, size: asset.size, modified: asset.modified, }; }); } /** * Find orphan assets (not referenced by any agent or task) */ function findOrphans(inventory) { const referenced = new Set(); // Collect all referenced assets inventory.agents.forEach((agent) => { agent.dependencies.tasks.forEach((t) => referenced.add(`task:${t.replace('.md', '')}`)); agent.dependencies.templates.forEach((t) => referenced.add(`template:${t.replace(/\.(yaml|yml|md)$/, '')}`) ); agent.dependencies.checklists.forEach((t) => referenced.add(`checklist:${t.replace('.md', '')}`) ); agent.dependencies.data.forEach((t) => referenced.add(`data:${t.replace(/\.(md|yaml)$/, '')}`)); }); inventory.tasks.forEach((task) => { task.dependencies.templates.forEach((t) => referenced.add(`template:${t.replace(/\.(yaml|yml|md)$/, '')}`) ); task.dependencies.checklists.forEach((t) => referenced.add(`checklist:${t.replace('.md', '')}`) ); }); // Find orphans const orphans = []; inventory.tasks.forEach((task) => { if (!referenced.has(`task:${task.id}`)) { orphans.push({ type: 'task', id: task.id, path: task.path }); } }); inventory.templates.forEach((template) => { if (!referenced.has(`template:${template.id}`)) { orphans.push({ type: 'template', id: template.id, path: template.path }); } }); inventory.checklists.forEach((checklist) => { if (!referenced.has(`checklist:${checklist.id}`)) { orphans.push({ type: 'checklist', id: checklist.id, path: checklist.path }); } }); return orphans; } /** * Count dependencies for each asset */ function countDependencies(inventory) { const counts = {}; // Count how many times each asset is referenced inventory.agents.forEach((agent) => { agent.dependencies.tasks.forEach((t) => { const key = `task:${t.replace('.md', '')}`; counts[key] = (counts[key] || 0) + 1; }); agent.dependencies.templates.forEach((t) => { const key = `template:${t.replace(/\.(yaml|yml|md)$/, '')}`; counts[key] = (counts[key] || 0) + 1; }); agent.dependencies.checklists.forEach((t) => { const key = `checklist:${t.replace('.md', '')}`; counts[key] = (counts[key] || 0) + 1; }); }); inventory.tasks.forEach((task) => { task.dependencies.templates.forEach((t) => { const key = `template:${t.replace(/\.(yaml|yml|md)$/, '')}`; counts[key] = (counts[key] || 0) + 1; }); task.dependencies.checklists.forEach((t) => { const key = `checklist:${t.replace('.md', '')}`; counts[key] = (counts[key] || 0) + 1; }); }); return counts; } /** * Generate inventory summary */ function generateSummary(inventory, orphans) { return { agents: inventory.agents.length, tasks: inventory.tasks.length, templates: inventory.templates.length, checklists: inventory.checklists.length, scripts: inventory.scripts.length, schemas: inventory.schemas?.length || 0, orphans: orphans.length, v2Assets: inventory.agents.filter((a) => a.version === 'v2').length + inventory.tasks.filter((t) => t.version === 'v2').length, v3Assets: inventory.agents.filter((a) => a.version === 'v3').length + inventory.tasks.filter((t) => t.version === 'v3').length, }; } /** * Main inventory generator */ async function generateInventory(rootPath, options = {}) { const { verbose = false } = options; // Scan all asset directories const [rawAgents, rawTasks, rawTemplates, rawChecklists, rawScripts, rawSchemas] = await Promise.all([ scanDirectory(path.join(rootPath, ASSET_PATHS.agents), ASSET_PATTERNS.agents, rootPath), scanDirectory(path.join(rootPath, ASSET_PATHS.tasks), ASSET_PATTERNS.tasks, rootPath), scanDirectory(path.join(rootPath, ASSET_PATHS.templates), ASSET_PATTERNS.templates, rootPath), scanDirectory( path.join(rootPath, ASSET_PATHS.checklists), ASSET_PATTERNS.checklists, rootPath ), scanDirectory(path.join(rootPath, ASSET_PATHS.scripts), ASSET_PATTERNS.scripts, rootPath), scanDirectory(path.join(rootPath, ASSET_PATHS.schemas), ASSET_PATTERNS.schemas, rootPath), ]); // Process each asset type const inventory = { agents: processAgents(rawAgents), tasks: processTasks(rawTasks), templates: processTemplates(rawTemplates), checklists: processChecklists(rawChecklists), scripts: processScripts(rawScripts), schemas: rawSchemas.map((s) => ({ type: 'schema', id: path.basename(s.file, path.extname(s.file)), path: s.path, format: path.extname(s.file).substring(1), size: s.size, modified: s.modified, })), }; // Find orphans and count dependencies const orphans = findOrphans(inventory); const dependencyCounts = countDependencies(inventory); // Add reference counts to assets inventory.tasks.forEach((task) => { task.referencedBy = dependencyCounts[`task:${task.id}`] || 0; }); inventory.templates.forEach((template) => { template.referencedBy = dependencyCounts[`template:${template.id}`] || 0; }); inventory.checklists.forEach((checklist) => { checklist.referencedBy = dependencyCounts[`checklist:${checklist.id}`] || 0; }); // Generate final report const report = { generated: new Date().toISOString(), rootPath, summary: generateSummary(inventory, orphans), orphans, assets: inventory, }; // Remove content from verbose output (it's huge) if (!verbose) { report.assets.agents.forEach((a) => delete a.content); report.assets.tasks.forEach((t) => delete t.content); } return report; } /** * Format output for console */ function formatConsoleOutput(report, verbose = false) { const lines = []; lines.push(''); lines.push('═══════════════════════════════════════════════════════════'); lines.push(' AIOS Asset Inventory'); lines.push(` Generated: ${report.generated}`); lines.push('═══════════════════════════════════════════════════════════'); lines.push(''); lines.push('SUMMARY'); lines.push('───────'); lines.push(` Agents: ${report.summary.agents}`); lines.push(` Tasks: ${report.summary.tasks}`); lines.push(` Templates: ${report.summary.templates}`); lines.push(` Checklists: ${report.summary.checklists}`); lines.push(` Scripts: ${report.summary.scripts}`); lines.push(` Schemas: ${report.summary.schemas}`); lines.push(''); lines.push(` V2 Assets: ${report.summary.v2Assets}`); lines.push(` V3 Assets: ${report.summary.v3Assets}`); lines.push(` Orphans: ${report.summary.orphans}`); lines.push(''); if (verbose) { lines.push('AGENTS'); lines.push('──────'); report.assets.agents.forEach((agent) => { const vBadge = agent.version === 'v3' ? '🆕' : '📦'; lines.push(` ${vBadge} ${agent.id} (${agent.name}) - ${agent.commandCount} commands`); if (agent.dependencies.tasks.length > 0) { lines.push(` └─ Tasks: ${agent.dependencies.tasks.slice(0, 5).join(', ')}...`); } }); lines.push(''); lines.push('TASKS (top 20)'); lines.push('──────────────'); report.assets.tasks.slice(0, 20).forEach((task) => { const vBadge = task.version === 'v3' ? '🆕' : '📦'; const refs = task.referencedBy > 0 ? `(${task.referencedBy} refs)` : '(orphan)'; lines.push(` ${vBadge} ${task.id} ${refs}`); }); lines.push(''); if (report.orphans.length > 0) { lines.push('ORPHAN ASSETS'); lines.push('─────────────'); report.orphans.forEach((orphan) => { lines.push(` ⚠️ ${orphan.type}: ${orphan.id}`); }); lines.push(''); } } lines.push('═══════════════════════════════════════════════════════════'); return lines.join('\n'); } /** * CLI handler */ async function main() { const args = process.argv.slice(2); if (args.includes('--help')) { console.log(` AIOS Asset Inventory Generator Usage: node asset-inventory.js [options] Options: --verbose Show detailed asset information --json Output as JSON --output <path> Save report to file (default: stdout) --help Show this help message `); return; } const verbose = args.includes('--verbose'); const jsonOutput = args.includes('--json'); const outputIndex = args.indexOf('--output'); const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : null; // Find project root (look for .aios-core directory) let rootPath = process.cwd(); while (rootPath !== '/') { try { await fs.access(path.join(rootPath, '.aios-core')); break; } catch { rootPath = path.dirname(rootPath); } } if (rootPath === '/') { console.error('Error: Could not find .aios-core directory. Run from project root.'); process.exit(1); } const report = await generateInventory(rootPath, { verbose }); let output; if (jsonOutput) { output = JSON.stringify(report, null, 2); } else { output = formatConsoleOutput(report, verbose); } if (outputPath) { const fullOutputPath = path.resolve(outputPath); await fs.writeFile(fullOutputPath, output); console.log(`Report saved to: ${fullOutputPath}`); } else { console.log(output); } } // Export for programmatic use module.exports = { generateInventory, ASSET_PATHS, }; // Run CLI if called directly if (require.main === module) { main().catch((error) => { console.error('Error:', error.message); process.exit(1); }); }