File size: 16,301 Bytes
fd8cdf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#!/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();