#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { parseArgs, getUsageText, SUPPORTED_TYPES } from './arg-parser.js'; import { resolveInput } from './input-resolver.js'; import { createServer } from './server.js'; import { startWatcher } from './watcher.js'; import { log } from './logger.js'; import { getBundlePath } from './bundle-path.js'; import { generateTemplates } from './template-generator.js'; import { analyze } from './analyzer/index.js'; import { generateDomainContext } from './analyzer/domain-context.js'; import open from 'open'; /** * Opens the default system browser at the given URL. * If the browser fails to open, logs a warning and continues. */ async function openBrowser(url) { try { await open(url); } catch { log('warn', 'Could not open browser automatically'); } } async function main() { // Parse CLI arguments const parseResult = parseArgs(process.argv.slice(2)); if (!parseResult.success) { log('error', parseResult.error); process.stdout.write(getUsageText() + '\n'); process.exit(1); } const { options } = parseResult; // Default behavior (no command) or --help → show help if (options.command === 'help') { process.stdout.write(getUsageText() + '\n'); process.exit(0); } // --preview : start the preview server if (options.command === 'preview') { await runPreview(options); } // --analyze [path]: run static analysis and generate knowledge graph if (options.command === 'analyze') { await runAnalyze(options); } // --create-md [path]: generate agent config files if (options.command === 'create-md') { await runCreateMd(options.inputPath || '.', options.type, options.init); } } async function runPreview(options) { // Validate input path exists on filesystem if (!fs.existsSync(options.inputPath)) { log('error', `Path does not exist: ${options.inputPath}`); process.exit(1); } // Resolve input to manifest and file mapping const resolveResult = resolveInput(options.inputPath); if (!resolveResult.success) { log('error', resolveResult.error); process.exit(1); } const { manifest, fileMapping } = resolveResult; // Log discovery summary const projectCount = manifest.length; const graphFileCount = manifest.reduce((sum, entry) => sum + entry.graphFiles.length, 0); log('info', `Found ${projectCount} projects, ${graphFileCount} graph files`); // Resolve bundle path let bundlePath; try { bundlePath = getBundlePath(); } catch (err) { const message = err instanceof Error ? err.message : String(err); log('error', message); process.exit(1); } // Create and start server const server = createServer({ port: options.port, manifest, fileMapping, bundlePath, }); try { await server.start(); } catch (err) { const message = err instanceof Error ? err.message : String(err); log('error', message); process.exit(1); } log('info', `Server running at http://localhost:${options.port}`); // Open browser unless --no-open if (options.open) { await openBrowser(`http://localhost:${options.port}`); } // Start watcher unless --no-watch let watcher = null; if (options.watch) { watcher = startWatcher({ inputPath: options.inputPath, onManifestUpdate: (updatedManifest, updatedFileMapping) => { server.updateManifest(updatedManifest, updatedFileMapping); }, }); } // Register SIGINT handler for graceful shutdown process.on('SIGINT', async () => { if (watcher) { watcher.close(); } await server.stop(); process.exit(0); }); } async function runAnalyze(options) { // Resolve target path (default to current directory if not provided) const targetPath = path.resolve(options.inputPath || '.'); // Validate the target path exists if (!fs.existsSync(targetPath)) { log('error', `Path does not exist: ${targetPath}`); process.exit(1); } log('info', `Analyzing: ${targetPath}`); // Run the analysis pipeline const { dashboard, stats, files } = await analyze(targetPath, { full: options.full }); // Create the .understand-anything/ output directory if it doesn't exist const outputDir = path.join(targetPath, '.understand-anything'); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Write knowledge-graph.json with full schema const knowledgeGraphPath = path.join(outputDir, 'knowledge-graph.json'); fs.writeFileSync(knowledgeGraphPath, JSON.stringify(dashboard, null, 2)); // Write meta.json with timestamp, git commit hash, version, and analyzedFiles count const meta = { lastAnalyzedAt: new Date().toISOString(), gitCommitHash: dashboard.project.gitCommitHash || '', version: '1.0.0', analyzedFiles: stats.filesAnalyzed, }; const metaPath = path.join(outputDir, 'meta.json'); fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2)); // Generate domain context const domainContext = generateDomainContext(targetPath, files); const domainContextPath = path.join(outputDir, 'domain-context.json'); fs.writeFileSync(domainContextPath, JSON.stringify(domainContext, null, 2)); // Log summary const nodeTypeSummary = Object.entries(stats.nodesByType) .map(([type, count]) => `${type}: ${count}`) .join(', '); const tourSteps = dashboard.tour ? dashboard.tour.length : 0; log('info', `Analysis complete:`); log('info', ` Files analyzed: ${stats.filesAnalyzed}`); log('info', ` Nodes: ${nodeTypeSummary || '(none)'}`); log('info', ` Edges: ${stats.edgesCreated}`); log('info', ` Layers: ${stats.layersIdentified}`); log('info', ` Tour steps: ${tourSteps}`); log('info', ` Domain context: ${domainContext.entryPoints.length} entry points, ${domainContext.fileSignatures.length} signatures`); log('info', `Output written to: ${outputDir}`); process.exit(0); } async function runOpenspecInit(targetDir) { const { execSync } = await import('node:child_process'); // Check if openspec is installed try { execSync('openspec --version', { stdio: 'pipe' }); log('info', 'openspec is already installed'); } catch { log('info', 'Installing openspec globally...'); try { execSync('npm install -g openspec@latest', { stdio: 'inherit' }); log('info', 'openspec installed successfully'); } catch (err) { const message = err instanceof Error ? err.message : String(err); log('error', `Failed to install openspec: ${message}`); process.exit(1); } } // Check if openspec is already initialized (look for openspec config files) const openspecConfig = ['openspec.json', '.openspec.json', 'openspec.yaml', '.openspec.yaml', 'openspec/AGENTS.md'] .some(f => fs.existsSync(`${targetDir}/${f}`)); if (openspecConfig) { log('info', 'openspec already initialized (config file found)'); } else { log('info', 'Running openspec init...'); try { execSync('openspec init', { stdio: 'inherit', cwd: targetDir }); log('info', 'openspec init completed'); } catch (err) { const message = err instanceof Error ? err.message : String(err); log('warn', `openspec init failed: ${message}. Continuing with file generation...`); } } } async function runCreateMd(targetPath, type, init) { // If no type specified, prompt user to choose interactively if (!type) { type = await promptForType(); } const targetDir = targetPath || '.'; if (!fs.existsSync(targetDir)) { log('error', `Target directory does not exist: ${targetDir}`); process.exit(1); } // --init: install openspec and run openspec init if (init) { await runOpenspecInit(targetDir); } log('info', `Generating ${type} templates in: ${targetDir}`); const result = generateTemplates(targetDir, type); if (!result.success) { log('error', result.error || 'Failed to generate templates'); process.exit(1); } if (result.filesCreated.length === 0) { log('info', 'No new files created (all files already exist)'); } else { log('info', `Created ${result.filesCreated.length} files:`); for (const file of result.filesCreated) { log('info', ` ${file}`); } // Show skill descriptions and usage guide process.stdout.write('\n'); process.stdout.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); process.stdout.write('\n'); process.stdout.write(' 📦 Available skills:\n'); process.stdout.write('\n'); process.stdout.write(' understand-chat Ask questions about the codebase using the knowledge graph\n'); process.stdout.write(' understand-diff Analyze code changes to identify affected components and risks\n'); process.stdout.write(' understand-domain Extract business domain knowledge, flows, and process steps\n'); process.stdout.write(' understand-explain Deep-dive explanation of a specific file, function, or module\n'); process.stdout.write(' understand-knowledge Analyze a wiki/knowledge base and generate a knowledge graph\n'); process.stdout.write(' understand-onboard Generate an onboarding guide for new team members\n'); process.stdout.write('\n'); process.stdout.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); process.stdout.write('\n'); process.stdout.write(' 🚀 Getting started:\n'); process.stdout.write('\n'); process.stdout.write(' 1. Generate a knowledge graph for your project:\n'); process.stdout.write(' Run the /understand skill in your AI coding tool\n'); process.stdout.write(' This creates .understand-anything/knowledge-graph.json\n'); process.stdout.write('\n'); process.stdout.write(' 2. Use skills in your AI tool:\n'); process.stdout.write(' /understand-chat "How does authentication work?"\n'); process.stdout.write(' /understand-explain src/auth/login.ts\n'); process.stdout.write(' /understand-diff\n'); process.stdout.write(' /understand-onboard\n'); process.stdout.write('\n'); process.stdout.write(' 3. Preview the knowledge graph in browser:\n'); process.stdout.write(' project-understand --preview .understand-anything\n'); process.stdout.write('\n'); process.stdout.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); process.stdout.write('\n'); process.stdout.write(' 📝 Notes:\n'); process.stdout.write(' • Steering/config files teach the AI agent about the knowledge graph structure\n'); process.stdout.write(' • Skills define specific actions the agent can perform\n'); process.stdout.write(' • Hooks (where supported) auto-trigger updates after code changes\n'); process.stdout.write(' • The knowledge graph must exist before skills can be used\n'); process.stdout.write('\n'); process.stdout.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); process.stdout.write('\n'); process.stdout.write(' 💡 Usage examples by platform:\n'); process.stdout.write('\n'); process.stdout.write(' Kiro:\n'); process.stdout.write(' • Skills appear in chat via # context key (e.g., #understand-chat)\n'); process.stdout.write(' • Steering files auto-load into every conversation\n'); process.stdout.write(' • Hooks trigger automatically (e.g., post-commit graph update)\n'); process.stdout.write('\n'); process.stdout.write(' Claude Code:\n'); process.stdout.write(' • Use /understand-chat "query" to invoke skills\n'); process.stdout.write(' • PostToolUse hooks run after git commits to check graph staleness\n'); process.stdout.write(' • Plugin metadata in plugin.json registers all skills\n'); process.stdout.write('\n'); process.stdout.write(' Codex / OpenCode:\n'); process.stdout.write(' • AGENTS.md is read automatically at session start\n'); process.stdout.write(' • Skills in .codex/skills/ or .opencode/skills/ are discovered by the agent\n'); process.stdout.write(' • Invoke: /understand-chat, /understand-explain \n'); process.stdout.write('\n'); process.stdout.write(' OpenClaw:\n'); process.stdout.write(' • AGENT.md provides steering context\n'); process.stdout.write(' • Skills in .agent/skills/ are auto-discovered\n'); process.stdout.write(' • Hooks in .agent/hooks/ trigger on events (e.g., post-commit)\n'); process.stdout.write('\n'); process.stdout.write(' Cursor:\n'); process.stdout.write(' • Rules in .cursor/rules/ are loaded based on globs and alwaysApply\n'); process.stdout.write(' • No skills/hooks — use rules to guide the agent behavior\n'); process.stdout.write(' • The agent reads the knowledge graph when rules reference it\n'); process.stdout.write('\n'); process.stdout.write(' OpenSpec (GitHub Copilot):\n'); process.stdout.write(' • openspec/AGENTS.md provides agent-level steering\n'); process.stdout.write(' • .github/instructions/ files are loaded as project context\n'); process.stdout.write(' • .github/prompts/ files are available as reusable prompts\n'); process.stdout.write('\n'); } } async function promptForType() { const { createInterface } = await import('node:readline'); const rl = createInterface({ input: process.stdin, output: process.stdout }); const descriptions = { 'kiro': '.kiro/ (steering, skills, hooks)', 'codex': 'AGENTS.md + .codex/skills/ (OpenAI Codex)', 'opencode': 'AGENTS.md + .opencode/skills/ (OpenCode)', 'claude-code': '.claude-plugin/ (plugin.json + skills/)', 'openclaw': '.agent/AGENT.md + .agent/skills/ (OpenClaw)', 'cursor': '.cursor/rules/ (Cursor)', 'openspec': 'openspec/AGENTS.md + .github/ (GitHub Copilot)', }; process.stdout.write('\nSelect a template type:\n\n'); SUPPORTED_TYPES.forEach((t, i) => { process.stdout.write(` ${i + 1}) ${t.padEnd(14)} → ${descriptions[t]}\n`); }); process.stdout.write('\n'); return new Promise((resolve) => { rl.question('Enter number or type name: ', (answer) => { rl.close(); const trimmed = answer.trim().toLowerCase(); // Try as number const num = parseInt(trimmed, 10); if (num >= 1 && num <= SUPPORTED_TYPES.length) { resolve(SUPPORTED_TYPES[num - 1]); return; } // Try as type name if (SUPPORTED_TYPES.includes(trimmed)) { resolve(trimmed); return; } log('error', `Invalid selection: "${answer.trim()}". Supported types: ${SUPPORTED_TYPES.join(', ')}`); process.exit(1); }); }); } main();