legacyver 2.1.0 → 2.1.2

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 (126) hide show
  1. package/.agent/skills/openspec-apply-change/SKILL.md +156 -0
  2. package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
  3. package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  4. package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
  5. package/.agent/skills/openspec-explore/SKILL.md +290 -0
  6. package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
  7. package/.agent/skills/openspec-new-change/SKILL.md +74 -0
  8. package/.agent/skills/openspec-onboard/SKILL.md +529 -0
  9. package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
  10. package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
  11. package/.agent/workflows/opsx-apply.md +149 -0
  12. package/.agent/workflows/opsx-archive.md +154 -0
  13. package/.agent/workflows/opsx-bulk-archive.md +239 -0
  14. package/.agent/workflows/opsx-continue.md +111 -0
  15. package/.agent/workflows/opsx-explore.md +171 -0
  16. package/.agent/workflows/opsx-ff.md +91 -0
  17. package/.agent/workflows/opsx-new.md +66 -0
  18. package/.agent/workflows/opsx-onboard.md +522 -0
  19. package/.agent/workflows/opsx-sync.md +131 -0
  20. package/.agent/workflows/opsx-verify.md +161 -0
  21. package/.github/prompts/opsx-apply.prompt.md +149 -0
  22. package/.github/prompts/opsx-archive.prompt.md +154 -0
  23. package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
  24. package/.github/prompts/opsx-continue.prompt.md +111 -0
  25. package/.github/prompts/opsx-explore.prompt.md +171 -0
  26. package/.github/prompts/opsx-ff.prompt.md +91 -0
  27. package/.github/prompts/opsx-new.prompt.md +66 -0
  28. package/.github/prompts/opsx-onboard.prompt.md +522 -0
  29. package/.github/prompts/opsx-sync.prompt.md +131 -0
  30. package/.github/prompts/opsx-verify.prompt.md +161 -0
  31. package/.github/skills/openspec-apply-change/SKILL.md +156 -0
  32. package/.github/skills/openspec-archive-change/SKILL.md +114 -0
  33. package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  34. package/.github/skills/openspec-continue-change/SKILL.md +118 -0
  35. package/.github/skills/openspec-explore/SKILL.md +290 -0
  36. package/.github/skills/openspec-ff-change/SKILL.md +101 -0
  37. package/.github/skills/openspec-new-change/SKILL.md +74 -0
  38. package/.github/skills/openspec-onboard/SKILL.md +529 -0
  39. package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
  40. package/.github/skills/openspec-verify-change/SKILL.md +168 -0
  41. package/.legacyverignore.example +43 -0
  42. package/.legacyverrc +7 -0
  43. package/.opencode/command/opsx-apply.md +149 -0
  44. package/.opencode/command/opsx-archive.md +154 -0
  45. package/.opencode/command/opsx-bulk-archive.md +239 -0
  46. package/.opencode/command/opsx-continue.md +111 -0
  47. package/.opencode/command/opsx-explore.md +171 -0
  48. package/.opencode/command/opsx-ff.md +91 -0
  49. package/.opencode/command/opsx-new.md +66 -0
  50. package/.opencode/command/opsx-onboard.md +522 -0
  51. package/.opencode/command/opsx-sync.md +131 -0
  52. package/.opencode/command/opsx-verify.md +161 -0
  53. package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
  54. package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
  55. package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  56. package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
  57. package/.opencode/skills/openspec-explore/SKILL.md +290 -0
  58. package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
  59. package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
  60. package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
  61. package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
  62. package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
  63. package/LICENSE +1 -1
  64. package/README.md +128 -83
  65. package/bin/legacyver.js +48 -25
  66. package/legacyver-docs/SUMMARY.md +3 -0
  67. package/legacyver-docs/components.md +57 -0
  68. package/legacyver-docs/index.md +15 -0
  69. package/nul +2 -0
  70. package/package.json +23 -25
  71. package/src/cache/hash.js +9 -10
  72. package/src/cache/index.js +43 -65
  73. package/src/cli/commands/analyze.js +212 -190
  74. package/src/cli/commands/cache.js +15 -35
  75. package/src/cli/commands/init.js +63 -107
  76. package/src/cli/commands/providers.js +56 -81
  77. package/src/cli/commands/version.js +7 -10
  78. package/src/cli/ui.js +58 -77
  79. package/src/crawler/filters.js +41 -40
  80. package/src/crawler/index.js +52 -36
  81. package/src/crawler/manifest.js +31 -43
  82. package/src/crawler/walk.js +32 -38
  83. package/src/llm/chunker.js +34 -56
  84. package/src/llm/cost-estimator.js +68 -51
  85. package/src/llm/free-model.js +67 -0
  86. package/src/llm/index.js +22 -43
  87. package/src/llm/prompts.js +45 -33
  88. package/src/llm/providers/gemini.js +94 -0
  89. package/src/llm/providers/groq.js +55 -40
  90. package/src/llm/providers/ollama.js +38 -65
  91. package/src/llm/providers/openrouter.js +67 -0
  92. package/src/llm/queue.js +59 -88
  93. package/src/llm/re-prompter.js +41 -0
  94. package/src/llm/validator.js +72 -0
  95. package/src/parser/ast/generic.js +45 -222
  96. package/src/parser/ast/go.js +86 -205
  97. package/src/parser/ast/java.js +76 -146
  98. package/src/parser/ast/javascript.js +173 -241
  99. package/src/parser/ast/laravel/blade.js +56 -0
  100. package/src/parser/ast/laravel/classifier.js +30 -0
  101. package/src/parser/ast/laravel/controller.js +35 -0
  102. package/src/parser/ast/laravel/index.js +54 -0
  103. package/src/parser/ast/laravel/model.js +41 -0
  104. package/src/parser/ast/laravel/provider.js +28 -0
  105. package/src/parser/ast/laravel/routes.js +45 -0
  106. package/src/parser/ast/php.js +129 -0
  107. package/src/parser/ast/python.js +76 -199
  108. package/src/parser/ast/typescript.js +10 -244
  109. package/src/parser/body-extractor.js +40 -0
  110. package/src/parser/call-graph.js +50 -67
  111. package/src/parser/complexity-scorer.js +59 -0
  112. package/src/parser/index.js +61 -86
  113. package/src/parser/pattern-detector.js +71 -0
  114. package/src/parser/pkg-builder.js +36 -83
  115. package/src/renderer/html.js +63 -135
  116. package/src/renderer/index.js +23 -35
  117. package/src/renderer/json.js +17 -35
  118. package/src/renderer/markdown.js +83 -117
  119. package/src/utils/config.js +52 -53
  120. package/src/utils/errors.js +26 -41
  121. package/src/utils/logger.js +32 -53
  122. package/src/cli/flags.js +0 -87
  123. package/src/llm/providers/anthropic.js +0 -57
  124. package/src/llm/providers/google.js +0 -65
  125. package/src/llm/providers/openai.js +0 -52
  126. package/src/parser/ast/tree-sitter-init.js +0 -80
@@ -1,240 +1,262 @@
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,
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { loadConfig } = require('../../utils/config');
5
+ const { createSpinner, createProgressBar, confirmPrompt, printSummary } = require('../ui');
6
+ const logger = require('../../utils/logger');
7
+ const pc = require('picocolors');
8
+
9
+ const { NoApiKeyError } = require('../../utils/errors');
10
+
11
+ module.exports = async function analyzeCommand(target, flags) {
12
+ target = target || '.';
13
+ // Merge CLI flags into config
14
+ let config = loadConfig({
15
+ provider: flags.provider,
16
+ model: flags.model,
17
+ format: flags.format,
18
+ out: flags.out,
19
+ concurrency: flags.concurrency ? parseInt(flags.concurrency) : undefined,
20
+ dryRun: flags.dryRun,
21
+ incremental: flags.incremental,
22
+ confirm: flags.confirm,
23
+ verbose: flags.verbose,
24
+ maxFileSizeKb: flags.maxFileSize ? parseInt(flags.maxFileSize) : undefined,
46
25
  });
47
26
 
48
- const resolvedTarget = resolve(effectivePath);
49
- const resolvedOutput = resolve(config.output);
27
+ if (config.verbose) logger.setLevel('debug');
28
+
29
+ const targetDir = path.resolve(target);
30
+ const outputDir = path.resolve(config.out);
50
31
 
51
- // Stage 1: Crawl
52
- const crawlSpinner = createSpinner('Discovering files...');
53
- crawlSpinner.start();
32
+ // ─── Stage 1: Crawler ────────────────────────────────────────────────────
33
+ const crawlerSpinner = createSpinner('Crawling files...');
34
+ crawlerSpinner.start();
54
35
 
36
+ const { crawl } = require('../../crawler/index');
55
37
  let manifest;
56
38
  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;
39
+ manifest = await crawl(targetDir, config);
40
+ crawlerSpinner.succeed(`Found ${manifest.files.length} files (${manifest.skipped.length} skipped)`);
41
+ } catch (e) {
42
+ crawlerSpinner.fail(`Crawler failed: ${e.message}`);
43
+ process.exit(1);
64
44
  }
65
45
 
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;
46
+ if (manifest.files.length === 0) {
47
+ console.log(pc.yellow('No source files found. Check your target directory and .legacyverignore.'));
48
+ process.exit(0);
71
49
  }
72
50
 
73
- // Handle incremental cache
51
+ // ─── Incremental cache ────────────────────────────────────────────────────
74
52
  let cacheMap = {};
75
- let filesToAnalyze = manifest;
76
- let cachedCount = 0;
53
+ let cacheHits = [];
54
+ let cacheMisses = manifest.files;
77
55
 
78
56
  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
- }
57
+ const cache = require('../../cache/index');
58
+ const cacheDir = path.join(targetDir, '.legacyver-cache');
59
+ cacheMap = cache.loadCache(cacheDir);
60
+ const { hits, misses } = cache.getCacheHits(manifest.files, cacheMap);
61
+ cacheHits = hits;
62
+ cacheMisses = misses;
63
+ logger.info(`Cache: ${hits.length} hits, ${misses.length} misses`);
88
64
  }
89
65
 
90
- // Stage 2: Parse
66
+ const filesToAnalyze = cacheMisses;
67
+
68
+ // ─── Stage 2: AST Parser ─────────────────────────────────────────────────
91
69
  const parseSpinner = createSpinner('Parsing AST...');
92
70
  parseSpinner.start();
93
71
 
94
- let allFileFacts;
72
+ const { parseFiles } = require('../../parser/index');
73
+ let pkg;
95
74
  try {
96
- allFileFacts = await parseAll(filesToAnalyze, resolvedTarget);
97
- } catch (err) {
98
- parseSpinner.fail('AST parsing failed');
99
- throw err;
75
+ pkg = await parseFiles(filesToAnalyze, manifest.meta, config);
76
+ parseSpinner.succeed(`Parsed ${filesToAnalyze.length} files`);
77
+ } catch (e) {
78
+ parseSpinner.fail(`Parser failed: ${e.message}`);
79
+ logger.error(e.stack);
80
+ process.exit(1);
100
81
  }
101
82
 
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
-
83
+ // ─── Dry run ─────────────────────────────────────────────────────────────
115
84
  if (config.dryRun) {
85
+ const { estimateCost } = require('../../llm/cost-estimator');
86
+ const { buildChunks } = require('../../llm/chunker');
87
+ const chunks = buildChunks(pkg, config);
88
+ const est = await estimateCost(chunks, config);
89
+
116
90
  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;
91
+ console.log(pc.bold('Dry Run Results:'));
92
+ console.log(` Files: ${filesToAnalyze.length}`);
93
+ console.log(` Input tokens: ${est.totalInputTokens}`);
94
+ if (config.model && config.model.endsWith(':free')) {
95
+ console.log(` Estimated cost: $0.00 (free model)`);
96
+ } else {
97
+ console.log(` Estimated cost: $${est.estimatedCostUSD.toFixed(4)}`);
98
+ }
99
+ process.exit(0);
121
100
  }
122
101
 
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);
102
+ // ─── Free model policy ────────────────────────────────────────────────────
103
+ const { applyFreeModelPolicy } = require('../../llm/free-model');
104
+ config = applyFreeModelPolicy(config);
105
+
106
+ // ─── Cost gate ────────────────────────────────────────────────────────────
107
+ const errors = [];
108
+ let totalTokens = 0;
109
+ let totalCost = 0;
110
+
111
+ if (!config.isFreeModel) {
112
+ const { estimateCost } = require('../../llm/cost-estimator');
113
+ const { buildChunks } = require('../../llm/chunker');
114
+ const chunks = buildChunks(pkg, config);
115
+ const est = await estimateCost(chunks, config);
116
+ totalTokens = est.totalInputTokens;
117
+ totalCost = est.estimatedCostUSD;
118
+
119
+ if (est.estimatedCostUSD > 0.10 && config.confirm) {
120
+ console.log(`\nEstimated cost: ${pc.yellow('$' + est.estimatedCostUSD.toFixed(4))} for ${est.totalInputTokens} tokens`);
121
+ const ok = await confirmPrompt('Proceed with LLM analysis?');
122
+ if (!ok) {
123
+ console.log(pc.yellow('Aborted by user.'));
124
+ process.exit(0);
125
+ }
134
126
  }
135
127
  }
136
128
 
137
- // Execute LLM calls
138
- const progressBar = createProgressBar(chunks.length);
129
+ // ─── Stage 3: LLM Engine ─────────────────────────────────────────────────
130
+ const llmSpinner = createSpinner('Generating documentation...');
131
+ llmSpinner.start();
132
+
133
+ const progressBar = createProgressBar(filesToAnalyze.length);
139
134
  progressBar.start();
140
135
 
141
- const queue = createQueue(engine, {
142
- concurrency: config.concurrency,
136
+ const { buildChunks } = require('../../llm/chunker');
137
+ const { createQueue } = require('../../llm/queue');
138
+ const { createProvider } = require('../../llm/index');
139
+ const { validateFragment } = require('../../llm/validator');
140
+ const { reprompt } = require('../../llm/re-prompter');
141
+
142
+ let provider;
143
+ try {
144
+ provider = createProvider(config);
145
+ } catch (e) {
146
+ if (e.code === 'NO_API_KEY') {
147
+ const providerName = (config.provider || '').toLowerCase();
148
+ const isGroq = providerName === 'groq';
149
+ const isGemini = providerName === 'gemini';
150
+ const label = isGemini ? 'Google Gemini' : isGroq ? 'Groq' : 'OpenRouter';
151
+ console.error(pc.red(`\n No API key found for ${label}.\n`));
152
+ console.error(' To fix, choose one of:\n');
153
+ if (isGroq) {
154
+ console.error(pc.cyan(' 1. Run the setup wizard:'));
155
+ console.error(' legacyver init\n');
156
+ console.error(pc.cyan(' 2. Set an environment variable:'));
157
+ console.error(' export GROQ_API_KEY=your_key_here\n');
158
+ console.error(' Get a free Groq key at: https://console.groq.com/keys\n');
159
+ } else if (isGemini) {
160
+ console.error(pc.cyan(' 1. Run the setup wizard:'));
161
+ console.error(' legacyver init\n');
162
+ console.error(pc.cyan(' 2. Set an environment variable:'));
163
+ console.error(' export GEMINI_API_KEY=your_key_here\n');
164
+ console.error(' Get a free key at: https://aistudio.google.com/apikey\n');
165
+ } else {
166
+ console.error(pc.cyan(' 1. Run the setup wizard:'));
167
+ console.error(' legacyver init\n');
168
+ console.error(pc.cyan(' 2. Set an environment variable:'));
169
+ console.error(' export OPENROUTER_API_KEY=your_key_here\n');
170
+ console.error(pc.cyan(' 3. Use Google Gemini instead (free, 15 req/min):'));
171
+ console.error(' legacyver analyze --provider gemini\n');
172
+ console.error(pc.cyan(' 4. Use Groq instead (fast & free):'));
173
+ console.error(' legacyver analyze --provider groq\n');
174
+ console.error(pc.cyan(' 5. Use local Ollama instead (no key needed):'));
175
+ console.error(' legacyver analyze --provider ollama\n');
176
+ console.error(' Get a free OpenRouter key at: https://openrouter.ai/keys\n');
177
+ }
178
+ process.exit(1);
179
+ }
180
+ throw e;
181
+ }
182
+ const chunks = buildChunks(pkg, config);
183
+ let qualityWarnings = 0;
184
+
185
+ const docFragments = await createQueue(chunks, provider, config, {
143
186
  onProgress: () => progressBar.increment(),
144
- onError: (err, chunk) => {
145
- errors.push({ file: chunk.relativePath, error: err.message });
146
- },
187
+ onError: (e, chunk) => errors.push(`${chunk.relativePath}: ${e.message}`),
147
188
  });
148
189
 
149
- const docFragments = await queue.processAll(chunks);
150
190
  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
- };
191
+ llmSpinner.succeed('Documentation generated');
192
+
193
+ // Quality validation
194
+ for (const frag of docFragments) {
195
+ const fileFacts = pkg.files[frag.relativePath];
196
+ const result = validateFragment(frag, fileFacts);
197
+ frag._qualityWarnings = result.hallucinations.concat(result.missingExports.map(s => `Missing export: ${s}`));
198
+ qualityWarnings += frag._qualityWarnings.length;
199
+
200
+ if (result.missingExports.length > 0) {
201
+ const pct = result.missingExports.length / (fileFacts && fileFacts.exports ? fileFacts.exports.length : 1);
202
+ if (pct > 0.3) {
203
+ const improved = await reprompt(frag, fileFacts, provider, config);
204
+ if (improved) frag.content = improved.content;
205
+ }
162
206
  }
163
- saveCache(cacheDir, cacheMap);
164
- addToGitignore(resolvedTarget);
165
207
  }
166
208
 
167
- // Stage 4: Render
209
+ // Merge cached fragments
210
+ const allFragments = [...docFragments];
211
+ // (cached fragments would be loaded here)
212
+
213
+ // ─── Stage 4: Renderer ───────────────────────────────────────────────────
168
214
  const renderSpinner = createSpinner('Rendering output...');
169
215
  renderSpinner.start();
170
216
 
217
+ const { render } = require('../../renderer/index');
171
218
  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;
219
+ await render(allFragments, pkg, outputDir, config);
220
+ renderSpinner.succeed(`Output written to ${outputDir}`);
221
+ } catch (e) {
222
+ renderSpinner.fail(`Renderer failed: ${e.message}`);
223
+ errors.push(e.message);
179
224
  }
180
225
 
181
- renderSpinner.succeed(`Output written to ${resolvedOutput}`);
226
+ // ─── Save cache ───────────────────────────────────────────────────────────
227
+ if (config.incremental || filesToAnalyze.length > 0) {
228
+ const cache = require('../../cache/index');
229
+ const cacheDir = path.join(targetDir, '.legacyver-cache');
230
+ const updatedMap = { ...cacheMap };
231
+ for (const file of filesToAnalyze) {
232
+ updatedMap[file.relativePath] = {
233
+ hash: file.hash,
234
+ docFile: path.join(outputDir, file.relativePath.replace(/\.[^.]+$/, '.md')),
235
+ generatedAt: new Date().toISOString(),
236
+ };
237
+ }
238
+ // Purge deleted
239
+ const currentPaths = manifest.files.map(f => f.relativePath);
240
+ cache.purgeDeleted(updatedMap, currentPaths);
241
+ cache.saveCache(cacheDir, updatedMap);
242
+ cache.autoAddToGitignore(targetDir);
243
+ }
182
244
 
183
- // Summary
184
- const duration = ((Date.now() - startTime) / 1000).toFixed(1);
245
+ // ─── Summary ─────────────────────────────────────────────────────────────
185
246
  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`,
247
+ filesAnalyzed: filesToAnalyze.length,
248
+ filesCached: cacheHits.length,
249
+ filesSkipped: manifest.skipped.length,
250
+ tokensUsed: totalTokens || undefined,
251
+ estimatedCost: totalCost || undefined,
252
+ qualityWarnings,
253
+ errors,
254
+ outputDir,
195
255
  };
196
256
 
197
- if (config.jsonSummary) {
198
- console.log(JSON.stringify(stats, null, 2));
257
+ if (flags.jsonSummary) {
258
+ console.log(JSON.stringify(stats));
199
259
  } else {
200
260
  printSummary(stats);
201
261
  }
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
- }
262
+ };
@@ -1,35 +1,15 @@
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
- }
1
+ 'use strict';
2
+
3
+ const { existsSync, rmSync } = require('fs');
4
+ const { join } = require('path');
5
+ const pc = require('picocolors');
6
+
7
+ module.exports = async function cacheClearCommand() {
8
+ const cacheDir = join(process.cwd(), '.legacyver-cache');
9
+ if (existsSync(cacheDir)) {
10
+ rmSync(cacheDir, { recursive: true, force: true });
11
+ console.log(pc.green('✓ Cache cleared: .legacyver-cache/'));
12
+ } else {
13
+ console.log(pc.yellow('No cache directory found (.legacyver-cache/).'));
14
+ }
15
+ };