legacyver 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -0
  3. package/bin/legacyver.js +33 -0
  4. package/package.json +51 -0
  5. package/src/cache/hash.js +17 -0
  6. package/src/cache/index.js +106 -0
  7. package/src/cli/commands/analyze.js +240 -0
  8. package/src/cli/commands/cache.js +35 -0
  9. package/src/cli/commands/init.js +125 -0
  10. package/src/cli/commands/providers.js +83 -0
  11. package/src/cli/commands/version.js +12 -0
  12. package/src/cli/flags.js +87 -0
  13. package/src/cli/ui.js +104 -0
  14. package/src/crawler/filters.js +57 -0
  15. package/src/crawler/index.js +49 -0
  16. package/src/crawler/manifest.js +51 -0
  17. package/src/crawler/walk.js +57 -0
  18. package/src/llm/chunker.js +77 -0
  19. package/src/llm/cost-estimator.js +67 -0
  20. package/src/llm/index.js +49 -0
  21. package/src/llm/prompts.js +52 -0
  22. package/src/llm/providers/anthropic.js +57 -0
  23. package/src/llm/providers/google.js +65 -0
  24. package/src/llm/providers/groq.js +52 -0
  25. package/src/llm/providers/ollama.js +85 -0
  26. package/src/llm/providers/openai.js +52 -0
  27. package/src/llm/queue.js +99 -0
  28. package/src/parser/ast/generic.js +249 -0
  29. package/src/parser/ast/go.js +236 -0
  30. package/src/parser/ast/java.js +173 -0
  31. package/src/parser/ast/javascript.js +273 -0
  32. package/src/parser/ast/python.js +227 -0
  33. package/src/parser/ast/tree-sitter-init.js +80 -0
  34. package/src/parser/ast/typescript.js +250 -0
  35. package/src/parser/call-graph.js +86 -0
  36. package/src/parser/index.js +106 -0
  37. package/src/parser/pkg-builder.js +105 -0
  38. package/src/renderer/html.js +152 -0
  39. package/src/renderer/index.js +44 -0
  40. package/src/renderer/json.js +46 -0
  41. package/src/renderer/markdown.js +149 -0
  42. package/src/utils/config.js +65 -0
  43. package/src/utils/errors.js +70 -0
  44. package/src/utils/logger.js +67 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 legacyver contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # legacyver
2
+
3
+ Generate technical documentation from undocumented or legacy codebases using AST parsing and LLMs.
4
+
5
+ Legacyver crawls your source code, extracts structural facts with AST parsers (Tree-sitter + regex fallback), sends them to an LLM with anti-hallucination constraints, and outputs clean Markdown, HTML, or JSON documentation.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g legacyver
11
+ ```
12
+
13
+ Requires Node.js >= 18.0.0.
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # 1. Run the setup wizard
19
+ legacyver init
20
+
21
+ # 2. Generate documentation
22
+ legacyver analyze ./src
23
+
24
+ # 3. View output
25
+ open legacyver-docs/index.md
26
+ ```
27
+
28
+ ### Using Ollama (Free, Local)
29
+
30
+ No API key required — just have [Ollama](https://ollama.ai) running:
31
+
32
+ ```bash
33
+ legacyver analyze ./src --provider ollama
34
+ ```
35
+
36
+ ## CLI Reference
37
+
38
+ ### `legacyver analyze <path>`
39
+
40
+ Main command. Analyzes a codebase and generates documentation.
41
+
42
+ | Flag | Description | Default |
43
+ |---|---|---|
44
+ | `-p, --provider <name>` | LLM provider (`anthropic`, `openai`, `google`, `groq`, `ollama`) | `anthropic` |
45
+ | `-m, --model <name>` | Model name (overrides provider default) | Provider default |
46
+ | `-o, --output <dir>` | Output directory | `./legacyver-docs` |
47
+ | `-f, --format <type>` | Output format: `markdown`, `html`, `json` | `markdown` |
48
+ | `-c, --concurrency <n>` | Concurrent LLM requests | `5` |
49
+ | `--max-file-size <kb>` | Skip files larger than this (KB) | `500` |
50
+ | `--incremental` | Only re-analyze changed files | `false` |
51
+ | `--no-confirm` | Skip cost confirmation prompt | `false` |
52
+ | `--dry-run` | Show cost estimate, no LLM calls | `false` |
53
+ | `--ignore <patterns...>` | Additional glob patterns to ignore | `[]` |
54
+ | `--json-summary` | Output JSON summary to stdout | `false` |
55
+ | `--verbose` | Enable debug output | `false` |
56
+
57
+ ### `legacyver init`
58
+
59
+ Interactive setup wizard. Configures your preferred LLM provider, saves API keys to OS user config, and creates a `.legacyverrc` file.
60
+
61
+ ### `legacyver providers`
62
+
63
+ Lists all supported providers with API key status and pricing.
64
+
65
+ ### `legacyver cache clear`
66
+
67
+ Deletes the `.legacyver-cache/` directory.
68
+
69
+ ## Configuration
70
+
71
+ Legacyver uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) for configuration. It searches for:
72
+
73
+ - `.legacyverrc` (JSON)
74
+ - `.legacyverrc.json`
75
+ - `.legacyverrc.yaml`
76
+ - `legacyver.config.js`
77
+ - `legacyver.config.mjs`
78
+
79
+ ### `.legacyverrc` Schema
80
+
81
+ ```json
82
+ {
83
+ "provider": "anthropic",
84
+ "model": null,
85
+ "format": "markdown",
86
+ "output": "./legacyver-docs",
87
+ "concurrency": 5,
88
+ "maxFileSizeKb": 500,
89
+ "incremental": false,
90
+ "ignore": ["test/**", "**/*.test.*"],
91
+ "languages": ["javascript", "typescript", "python", "java", "go"]
92
+ }
93
+ ```
94
+
95
+ CLI flags always take precedence over config file values.
96
+
97
+ ## Supported Languages
98
+
99
+ | Language | Extensions | Parser |
100
+ |---|---|---|
101
+ | JavaScript | `.js`, `.jsx`, `.mjs` | Tree-sitter (WASM) or regex fallback |
102
+ | TypeScript | `.ts`, `.tsx` | Tree-sitter (WASM) or regex fallback |
103
+ | Python | `.py` | Tree-sitter (WASM) or regex fallback |
104
+ | Java | `.java` | Tree-sitter (WASM) or regex fallback |
105
+ | Go | `.go` | Tree-sitter (WASM) or regex fallback |
106
+
107
+ When Tree-sitter WASM grammars are not available, legacyver automatically falls back to a generic regex-based parser that extracts function signatures, class definitions, and imports.
108
+
109
+ ## Supported LLM Providers
110
+
111
+ | Provider | Default Model | Input Cost/1k tokens | Output Cost/1k tokens | API Key Env Var |
112
+ |---|---|---|---|---|
113
+ | Anthropic | `claude-haiku-3-5` | $0.00025 | $0.00125 | `ANTHROPIC_API_KEY` |
114
+ | OpenAI | `gpt-4o-mini` | $0.00015 | $0.00060 | `OPENAI_API_KEY` |
115
+ | Google | `gemini-1.5-flash` | $0.000075 | $0.00030 | `GOOGLE_API_KEY` |
116
+ | Groq | `llama-3.1-70b-versatile` | $0.00059 | $0.00079 | `GROQ_API_KEY` |
117
+ | Ollama | `llama3.2` | Free | Free | None (local) |
118
+
119
+ ## Ignore Rules
120
+
121
+ Legacyver respects a `.legacyverignore` file (same syntax as `.gitignore`) in your project root.
122
+
123
+ Default ignores include: `node_modules`, `.git`, `dist`, `build`, `__pycache__`, `venv`, `vendor`, minified files, lock files, and more.
124
+
125
+ ## CI/CD Integration
126
+
127
+ Legacyver auto-detects non-interactive environments (no TTY) and disables spinners/prompts. For CI pipelines:
128
+
129
+ ```bash
130
+ legacyver analyze ./src --provider openai --no-confirm --json-summary
131
+ ```
132
+
133
+ The `--json-summary` flag outputs a machine-readable summary to stdout. The `--no-confirm` flag is required in non-interactive mode (otherwise legacyver exits with code 4).
134
+
135
+ ### Incremental Builds
136
+
137
+ Use `--incremental` to skip unchanged files between CI runs:
138
+
139
+ ```bash
140
+ legacyver analyze ./src --incremental --no-confirm
141
+ ```
142
+
143
+ Cache is stored in `.legacyver-cache/` (auto-added to `.gitignore`).
144
+
145
+ ## How It Works
146
+
147
+ 1. **Crawler** discovers source files using fast-glob, respecting ignore rules
148
+ 2. **AST Parser** extracts structural facts (functions, classes, imports, exports, call graph) using Tree-sitter or regex fallback
149
+ 3. **LLM Engine** sends facts + source code to the LLM with anti-hallucination constraints — the LLM can only describe what's actually in the code
150
+ 4. **Renderer** assembles the output as Markdown (with Mermaid diagrams), self-contained HTML (with search), or structured JSON
151
+
152
+ ## License
153
+
154
+ MIT
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'node:module';
4
+ import { Command } from 'commander';
5
+ import { registerAnalyzeCommand } from '../src/cli/commands/analyze.js';
6
+ import { registerInitCommand } from '../src/cli/commands/init.js';
7
+ import { registerProvidersCommand } from '../src/cli/commands/providers.js';
8
+ import { registerCacheCommand } from '../src/cli/commands/cache.js';
9
+ import { setVerbose } from '../src/utils/logger.js';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const pkg = require('../package.json');
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('legacyver')
18
+ .description('Generate technical documentation from undocumented or legacy codebases')
19
+ .version(pkg.version)
20
+ .option('--verbose', 'Enable verbose debug output', false)
21
+ .hook('preAction', (thisCommand) => {
22
+ const opts = thisCommand.opts();
23
+ if (opts.verbose) {
24
+ setVerbose(true);
25
+ }
26
+ });
27
+
28
+ registerAnalyzeCommand(program);
29
+ registerInitCommand(program);
30
+ registerProvidersCommand(program);
31
+ registerCacheCommand(program);
32
+
33
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "legacyver",
3
+ "version": "1.0.0",
4
+ "description": "Generate technical documentation from undocumented or legacy codebases using AST parsing and LLMs",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "legacyver": "bin/legacyver.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "scripts": {
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "lint": "eslint src/ test/"
16
+ },
17
+ "keywords": [
18
+ "documentation",
19
+ "legacy",
20
+ "codebase",
21
+ "ast",
22
+ "llm",
23
+ "tree-sitter",
24
+ "cli"
25
+ ],
26
+ "license": "MIT",
27
+ "type": "module",
28
+ "dependencies": {
29
+ "@anthropic-ai/sdk": "^0.75.0",
30
+ "@google/generative-ai": "^0.24.1",
31
+ "chalk": "^5.6.2",
32
+ "cli-progress": "^3.12.0",
33
+ "commander": "^14.0.3",
34
+ "conf": "^15.1.0",
35
+ "cosmiconfig": "^9.0.0",
36
+ "fast-glob": "^3.3.3",
37
+ "groq-sdk": "^0.37.0",
38
+ "ignore": "^7.0.5",
39
+ "marked": "^17.0.3",
40
+ "openai": "^6.22.0",
41
+ "ora": "^9.3.0",
42
+ "p-limit": "^7.3.0",
43
+ "p-retry": "^7.1.1",
44
+ "picocolors": "^1.1.1",
45
+ "web-tree-sitter": "^0.26.5"
46
+ },
47
+ "devDependencies": {
48
+ "eslint": "^10.0.0",
49
+ "vitest": "^4.0.18"
50
+ }
51
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * File hash computation for incremental cache.
3
+ * Uses Node.js crypto SHA-256 to produce deterministic file fingerprints.
4
+ */
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { createHash } from 'node:crypto';
8
+
9
+ /**
10
+ * Compute SHA-256 hash of a file's contents.
11
+ * @param {string} filePath - Absolute path to file
12
+ * @returns {string} Hex-encoded SHA-256 hash with "sha256:" prefix
13
+ */
14
+ export function computeHash(filePath) {
15
+ const content = readFileSync(filePath);
16
+ return 'sha256:' + createHash('sha256').update(content).digest('hex');
17
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Incremental cache module.
3
+ * Persists file hashes to .legacyver-cache/hashes.json so that
4
+ * unchanged files can skip LLM re-analysis on subsequent runs.
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ const CACHE_FILE = 'hashes.json';
11
+
12
+ /**
13
+ * Load the cache map from disk.
14
+ * @param {string} cacheDir - Path to .legacyver-cache directory
15
+ * @returns {Record<string, { hash: string, generatedAt: string }>} Cache map (empty object if no cache)
16
+ */
17
+ export function loadCache(cacheDir) {
18
+ const cachePath = join(cacheDir, CACHE_FILE);
19
+ if (!existsSync(cachePath)) {
20
+ return {};
21
+ }
22
+ try {
23
+ const raw = readFileSync(cachePath, 'utf-8');
24
+ return JSON.parse(raw);
25
+ } catch {
26
+ // Corrupted cache — start fresh
27
+ return {};
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Save the cache map to disk.
33
+ * Creates the cache directory if it doesn't exist.
34
+ * @param {string} cacheDir - Path to .legacyver-cache directory
35
+ * @param {Record<string, { hash: string, generatedAt: string }>} cacheMap - Cache map to persist
36
+ */
37
+ export function saveCache(cacheDir, cacheMap) {
38
+ mkdirSync(cacheDir, { recursive: true });
39
+ const cachePath = join(cacheDir, CACHE_FILE);
40
+ writeFileSync(cachePath, JSON.stringify(cacheMap, null, 2), 'utf-8');
41
+ }
42
+
43
+ /**
44
+ * Separate manifest entries into cache hits and misses.
45
+ * A hit means the file hash matches the cached hash — no re-analysis needed.
46
+ * @param {import('../crawler/manifest.js').FileManifest[]} manifest - Current file manifest
47
+ * @param {Record<string, { hash: string }>} cacheMap - Loaded cache map
48
+ * @returns {{ hits: import('../crawler/manifest.js').FileManifest[], misses: import('../crawler/manifest.js').FileManifest[] }}
49
+ */
50
+ export function getCacheHits(manifest, cacheMap) {
51
+ const hits = [];
52
+ const misses = [];
53
+
54
+ for (const file of manifest) {
55
+ const cached = cacheMap[file.relativePath];
56
+ if (cached && cached.hash === file.hash) {
57
+ hits.push(file);
58
+ } else {
59
+ misses.push(file);
60
+ }
61
+ }
62
+
63
+ return { hits, misses };
64
+ }
65
+
66
+ /**
67
+ * Remove cache entries for files that no longer exist on disk.
68
+ * @param {Record<string, { hash: string }>} cacheMap - Current cache map
69
+ * @param {string[]} currentPaths - Relative paths of files currently on disk
70
+ * @returns {Record<string, { hash: string }>} Cleaned cache map
71
+ */
72
+ export function purgeDeleted(cacheMap, currentPaths) {
73
+ const pathSet = new Set(currentPaths);
74
+ const cleaned = {};
75
+ for (const [key, value] of Object.entries(cacheMap)) {
76
+ if (pathSet.has(key)) {
77
+ cleaned[key] = value;
78
+ }
79
+ }
80
+ return cleaned;
81
+ }
82
+
83
+ /**
84
+ * Auto-add .legacyver-cache/ to .gitignore if .gitignore exists
85
+ * and doesn't already contain the entry.
86
+ * @param {string} projectRoot - Root directory of the project being analyzed
87
+ */
88
+ export function addToGitignore(projectRoot) {
89
+ const gitignorePath = join(projectRoot, '.gitignore');
90
+ if (!existsSync(gitignorePath)) {
91
+ return;
92
+ }
93
+
94
+ const content = readFileSync(gitignorePath, 'utf-8');
95
+ const entry = '.legacyver-cache/';
96
+
97
+ // Check if already present (exact line match)
98
+ const lines = content.split('\n');
99
+ if (lines.some((line) => line.trim() === entry || line.trim() === '.legacyver-cache')) {
100
+ return;
101
+ }
102
+
103
+ // Append entry with a newline separator
104
+ const separator = content.endsWith('\n') ? '' : '\n';
105
+ writeFileSync(gitignorePath, content + separator + entry + '\n', 'utf-8');
106
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Analyze command — main pipeline orchestrator.
3
+ * Calls Crawler -> Parser -> LLM Engine -> Renderer in sequence.
4
+ * Manages progress bar, aggregates errors, prints final summary.
5
+ */
6
+
7
+ import { resolve } from 'node:path';
8
+ import { loadConfig, mergeWithFlags } from '../../utils/config.js';
9
+ import { logger } from '../../utils/logger.js';
10
+ import { LegacyverError } from '../../utils/errors.js';
11
+ import { createSpinner, createProgressBar, confirmPrompt, printSummary } from '../ui.js';
12
+ import { applyFlags } from '../flags.js';
13
+ import { crawl } from '../../crawler/index.js';
14
+ import { parseAll } from '../../parser/index.js';
15
+ import { buildPKG } from '../../parser/pkg-builder.js';
16
+ import { resolveCallGraph } from '../../parser/call-graph.js';
17
+ import { createLLMEngine } from '../../llm/index.js';
18
+ import { buildChunks } from '../../llm/chunker.js';
19
+ import { estimateCost } from '../../llm/cost-estimator.js';
20
+ import { createQueue } from '../../llm/queue.js';
21
+ import { render } from '../../renderer/index.js';
22
+ import { loadCache, saveCache, getCacheHits, purgeDeleted, addToGitignore } from '../../cache/index.js';
23
+
24
+ async function runAnalyze(targetPath, options) {
25
+ const startTime = Date.now();
26
+ const errors = [];
27
+
28
+ // If dry-run without a path, just show cost estimate info and exit early
29
+ const effectivePath = targetPath || process.cwd();
30
+
31
+ // Load and merge config
32
+ const fileConfig = await loadConfig(effectivePath);
33
+ const config = mergeWithFlags(fileConfig, {
34
+ provider: options.provider,
35
+ model: options.model,
36
+ output: options.output,
37
+ format: options.format,
38
+ concurrency: options.concurrency ? parseInt(options.concurrency, 10) : undefined,
39
+ maxFileSizeKb: options.maxFileSize ? parseInt(options.maxFileSize, 10) : undefined,
40
+ incremental: options.incremental,
41
+ noConfirm: !options.confirm, // commander inverts --no-confirm
42
+ dryRun: options.dryRun,
43
+ ignore: options.ignore,
44
+ jsonSummary: options.jsonSummary,
45
+ verbose: options.verbose,
46
+ });
47
+
48
+ const resolvedTarget = resolve(effectivePath);
49
+ const resolvedOutput = resolve(config.output);
50
+
51
+ // Stage 1: Crawl
52
+ const crawlSpinner = createSpinner('Discovering files...');
53
+ crawlSpinner.start();
54
+
55
+ let manifest;
56
+ try {
57
+ manifest = await crawl(resolvedTarget, {
58
+ maxFileSizeKb: config.maxFileSizeKb,
59
+ ignore: config.ignore,
60
+ });
61
+ } catch (err) {
62
+ crawlSpinner.fail('File discovery failed');
63
+ throw err;
64
+ }
65
+
66
+ crawlSpinner.succeed(`Found ${manifest.length} files`);
67
+
68
+ if (manifest.length === 0) {
69
+ logger.warn('No files found to analyze. Check your target path and ignore rules.');
70
+ return;
71
+ }
72
+
73
+ // Handle incremental cache
74
+ let cacheMap = {};
75
+ let filesToAnalyze = manifest;
76
+ let cachedCount = 0;
77
+
78
+ if (config.incremental) {
79
+ const cacheDir = resolve(resolvedTarget, '.legacyver-cache');
80
+ cacheMap = loadCache(cacheDir);
81
+ cacheMap = purgeDeleted(cacheMap, manifest.map((f) => f.relativePath));
82
+ const { hits, misses } = getCacheHits(manifest, cacheMap);
83
+ cachedCount = hits.length;
84
+ filesToAnalyze = misses;
85
+ if (cachedCount > 0) {
86
+ logger.info(`Cache: ${cachedCount} files unchanged, ${misses.length} files to analyze`);
87
+ }
88
+ }
89
+
90
+ // Stage 2: Parse
91
+ const parseSpinner = createSpinner('Parsing AST...');
92
+ parseSpinner.start();
93
+
94
+ let allFileFacts;
95
+ try {
96
+ allFileFacts = await parseAll(filesToAnalyze, resolvedTarget);
97
+ } catch (err) {
98
+ parseSpinner.fail('AST parsing failed');
99
+ throw err;
100
+ }
101
+
102
+ // Build call graph and PKG
103
+ const resolvedFacts = resolveCallGraph(allFileFacts);
104
+ const pkg = buildPKG(resolvedFacts, resolvedTarget);
105
+
106
+ parseSpinner.succeed(`Parsed ${allFileFacts.length} files (${pkg.meta.totalFiles} in PKG)`);
107
+
108
+ // Stage 3: LLM Engine
109
+ const chunks = buildChunks(resolvedFacts, config, resolvedTarget);
110
+
111
+ // Cost estimation
112
+ const costEstimate = estimateCost(chunks, config.provider);
113
+ logger.info(`Estimated cost: $${costEstimate.estimatedCostUSD.toFixed(4)} (${costEstimate.totalInputTokens} input tokens)`);
114
+
115
+ if (config.dryRun) {
116
+ console.log('');
117
+ console.log(JSON.stringify(costEstimate, null, 2));
118
+ console.log('');
119
+ logger.info('Dry run complete. No LLM calls made.');
120
+ return;
121
+ }
122
+
123
+ // Create engine only when we actually need it (not for dry runs)
124
+ const engine = createLLMEngine(config);
125
+
126
+ // Confirm cost if over $0.10 and not --no-confirm
127
+ if (costEstimate.estimatedCostUSD > 0.10 && !config.noConfirm) {
128
+ const confirmed = await confirmPrompt(
129
+ `Estimated cost is $${costEstimate.estimatedCostUSD.toFixed(4)}. Proceed?`
130
+ );
131
+ if (!confirmed) {
132
+ logger.info('Aborted by user.');
133
+ process.exit(4);
134
+ }
135
+ }
136
+
137
+ // Execute LLM calls
138
+ const progressBar = createProgressBar(chunks.length);
139
+ progressBar.start();
140
+
141
+ const queue = createQueue(engine, {
142
+ concurrency: config.concurrency,
143
+ onProgress: () => progressBar.increment(),
144
+ onError: (err, chunk) => {
145
+ errors.push({ file: chunk.relativePath, error: err.message });
146
+ },
147
+ });
148
+
149
+ const docFragments = await queue.processAll(chunks);
150
+ progressBar.stop();
151
+
152
+ logger.success(`Generated documentation for ${docFragments.length} files`);
153
+
154
+ // Update cache
155
+ if (config.incremental) {
156
+ const cacheDir = resolve(resolvedTarget, '.legacyver-cache');
157
+ for (const file of filesToAnalyze) {
158
+ cacheMap[file.relativePath] = {
159
+ hash: file.hash,
160
+ generatedAt: new Date().toISOString(),
161
+ };
162
+ }
163
+ saveCache(cacheDir, cacheMap);
164
+ addToGitignore(resolvedTarget);
165
+ }
166
+
167
+ // Stage 4: Render
168
+ const renderSpinner = createSpinner('Rendering output...');
169
+ renderSpinner.start();
170
+
171
+ try {
172
+ await render(docFragments, pkg, {
173
+ format: config.format,
174
+ outputDir: resolvedOutput,
175
+ });
176
+ } catch (err) {
177
+ renderSpinner.fail('Rendering failed');
178
+ throw err;
179
+ }
180
+
181
+ renderSpinner.succeed(`Output written to ${resolvedOutput}`);
182
+
183
+ // Summary
184
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
185
+ const stats = {
186
+ filesAnalyzed: docFragments.length,
187
+ filesSkipped: manifest.length - filesToAnalyze.length - cachedCount,
188
+ filesCached: cachedCount,
189
+ errors: errors.length,
190
+ inputTokens: costEstimate.totalInputTokens,
191
+ outputTokens: costEstimate.totalOutputTokens,
192
+ estimatedCost: costEstimate.estimatedCostUSD,
193
+ outputDir: resolvedOutput,
194
+ duration: `${duration}s`,
195
+ };
196
+
197
+ if (config.jsonSummary) {
198
+ console.log(JSON.stringify(stats, null, 2));
199
+ } else {
200
+ printSummary(stats);
201
+ }
202
+
203
+ if (errors.length > 0) {
204
+ logger.warn(`${errors.length} file(s) had errors during analysis:`);
205
+ for (const e of errors) {
206
+ logger.warn(` ${e.file}: ${e.error}`);
207
+ }
208
+ }
209
+ }
210
+
211
+ export function registerAnalyzeCommand(program) {
212
+ const cmd = program
213
+ .command('analyze [path]')
214
+ .description('Analyze a codebase and generate documentation');
215
+
216
+ applyFlags(cmd, [
217
+ 'provider', 'model', 'output', 'format', 'concurrency',
218
+ 'maxFileSize', 'incremental', 'noConfirm', 'dryRun',
219
+ 'ignore', 'jsonSummary',
220
+ ]);
221
+
222
+ cmd.action(async (targetPath, options) => {
223
+ try {
224
+ await runAnalyze(targetPath, options);
225
+ } catch (err) {
226
+ if (err instanceof LegacyverError) {
227
+ logger.error(err.message);
228
+ if (err.suggestion) {
229
+ logger.info(`Suggestion: ${err.suggestion}`);
230
+ }
231
+ process.exit(err.exitCode || 1);
232
+ }
233
+ logger.error('Unexpected error:', err.message);
234
+ if (process.env.DEBUG || options.verbose) {
235
+ console.error(err.stack);
236
+ }
237
+ process.exit(1);
238
+ }
239
+ });
240
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Cache subcommand — cache management utilities.
3
+ * Supports `cache clear` to delete .legacyver-cache/ directory.
4
+ */
5
+
6
+ import { rmSync, existsSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import { logger } from '../../utils/logger.js';
9
+
10
+ export function registerCacheCommand(program) {
11
+ const cache = program
12
+ .command('cache')
13
+ .description('Manage the incremental analysis cache');
14
+
15
+ cache
16
+ .command('clear')
17
+ .description('Delete the .legacyver-cache/ directory')
18
+ .option('--dir <path>', 'Project directory containing the cache', '.')
19
+ .action((options) => {
20
+ const cacheDir = resolve(options.dir, '.legacyver-cache');
21
+
22
+ if (!existsSync(cacheDir)) {
23
+ logger.info('No cache directory found. Nothing to clear.');
24
+ return;
25
+ }
26
+
27
+ try {
28
+ rmSync(cacheDir, { recursive: true, force: true });
29
+ logger.success(`Deleted ${cacheDir}`);
30
+ } catch (err) {
31
+ logger.error(`Failed to delete cache: ${err.message}`);
32
+ process.exit(1);
33
+ }
34
+ });
35
+ }