#!/usr/bin/env node // ai-bom-generator.mjs — AI Bill of Materials generator // Discovers AI components (models, MCP servers, plugins, knowledge, hooks) // and outputs a CycloneDX 1.6-compatible JSON BOM. // // CLI: node scanners/ai-bom-generator.mjs [--output-file ] // Scanner prefix: BOM. Always exits 0 (informational). // Zero external dependencies. import { resolve } from 'node:path'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { discoverComponents, buildAIBOM } from './lib/bom-builder.mjs'; function parseArgs(argv) { const args = { target: null, outputFile: null }; for (let i = 2; i < argv.length; i++) { if (argv[i] === '--output-file' && argv[i + 1]) { args.outputFile = argv[++i]; } else if (!args.target) { args.target = argv[i]; } } return args; } async function main() { const args = parseArgs(process.argv); if (!args.target) { process.stderr.write('Usage: node ai-bom-generator.mjs [--output-file ]\n'); process.exit(1); } const targetPath = resolve(args.target); if (!existsSync(targetPath)) { process.stderr.write(`Target path does not exist: ${targetPath}\n`); process.exit(1); } // Read project metadata from package.json if available let projectMeta = {}; try { const pkg = JSON.parse(readFileSync(resolve(targetPath, 'package.json'), 'utf-8')); projectMeta = { name: pkg.name, version: pkg.version }; } catch { /* no package.json */ } const components = await discoverComponents(targetPath); const bom = buildAIBOM(components, projectMeta); const jsonStr = JSON.stringify(bom, null, 2) + '\n'; if (args.outputFile) { writeFileSync(args.outputFile, jsonStr); process.stderr.write(`[ai-bom] BOM written to ${args.outputFile} (${components.length} components)\n`); } else { process.stdout.write(jsonStr); } process.stderr.write(`[ai-bom] Discovered ${components.length} AI components\n`); process.exit(0); } main().catch(err => { process.stderr.write(`Fatal error: ${err.message}\n`); process.exit(1); });