mr4's picture
Upload 136 files
fd8cdf5 verified
#!/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 <path>: 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 <file>\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();