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.
- package/.agent/skills/openspec-apply-change/SKILL.md +156 -0
- package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
- package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
- package/.agent/skills/openspec-explore/SKILL.md +290 -0
- package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
- package/.agent/skills/openspec-new-change/SKILL.md +74 -0
- package/.agent/skills/openspec-onboard/SKILL.md +529 -0
- package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
- package/.agent/workflows/opsx-apply.md +149 -0
- package/.agent/workflows/opsx-archive.md +154 -0
- package/.agent/workflows/opsx-bulk-archive.md +239 -0
- package/.agent/workflows/opsx-continue.md +111 -0
- package/.agent/workflows/opsx-explore.md +171 -0
- package/.agent/workflows/opsx-ff.md +91 -0
- package/.agent/workflows/opsx-new.md +66 -0
- package/.agent/workflows/opsx-onboard.md +522 -0
- package/.agent/workflows/opsx-sync.md +131 -0
- package/.agent/workflows/opsx-verify.md +161 -0
- package/.github/prompts/opsx-apply.prompt.md +149 -0
- package/.github/prompts/opsx-archive.prompt.md +154 -0
- package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
- package/.github/prompts/opsx-continue.prompt.md +111 -0
- package/.github/prompts/opsx-explore.prompt.md +171 -0
- package/.github/prompts/opsx-ff.prompt.md +91 -0
- package/.github/prompts/opsx-new.prompt.md +66 -0
- package/.github/prompts/opsx-onboard.prompt.md +522 -0
- package/.github/prompts/opsx-sync.prompt.md +131 -0
- package/.github/prompts/opsx-verify.prompt.md +161 -0
- package/.github/skills/openspec-apply-change/SKILL.md +156 -0
- package/.github/skills/openspec-archive-change/SKILL.md +114 -0
- package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.github/skills/openspec-continue-change/SKILL.md +118 -0
- package/.github/skills/openspec-explore/SKILL.md +290 -0
- package/.github/skills/openspec-ff-change/SKILL.md +101 -0
- package/.github/skills/openspec-new-change/SKILL.md +74 -0
- package/.github/skills/openspec-onboard/SKILL.md +529 -0
- package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.github/skills/openspec-verify-change/SKILL.md +168 -0
- package/.legacyverignore.example +43 -0
- package/.legacyverrc +7 -0
- package/.opencode/command/opsx-apply.md +149 -0
- package/.opencode/command/opsx-archive.md +154 -0
- package/.opencode/command/opsx-bulk-archive.md +239 -0
- package/.opencode/command/opsx-continue.md +111 -0
- package/.opencode/command/opsx-explore.md +171 -0
- package/.opencode/command/opsx-ff.md +91 -0
- package/.opencode/command/opsx-new.md +66 -0
- package/.opencode/command/opsx-onboard.md +522 -0
- package/.opencode/command/opsx-sync.md +131 -0
- package/.opencode/command/opsx-verify.md +161 -0
- package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
- package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
- package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
- package/.opencode/skills/openspec-explore/SKILL.md +290 -0
- package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
- package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
- package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
- package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
- package/LICENSE +1 -1
- package/README.md +128 -83
- package/bin/legacyver.js +48 -25
- package/legacyver-docs/SUMMARY.md +3 -0
- package/legacyver-docs/components.md +57 -0
- package/legacyver-docs/index.md +15 -0
- package/nul +2 -0
- package/package.json +23 -25
- package/src/cache/hash.js +9 -10
- package/src/cache/index.js +43 -65
- package/src/cli/commands/analyze.js +212 -190
- package/src/cli/commands/cache.js +15 -35
- package/src/cli/commands/init.js +63 -107
- package/src/cli/commands/providers.js +56 -81
- package/src/cli/commands/version.js +7 -10
- package/src/cli/ui.js +58 -77
- package/src/crawler/filters.js +41 -40
- package/src/crawler/index.js +52 -36
- package/src/crawler/manifest.js +31 -43
- package/src/crawler/walk.js +32 -38
- package/src/llm/chunker.js +34 -56
- package/src/llm/cost-estimator.js +68 -51
- package/src/llm/free-model.js +67 -0
- package/src/llm/index.js +22 -43
- package/src/llm/prompts.js +45 -33
- package/src/llm/providers/gemini.js +94 -0
- package/src/llm/providers/groq.js +55 -40
- package/src/llm/providers/ollama.js +38 -65
- package/src/llm/providers/openrouter.js +67 -0
- package/src/llm/queue.js +59 -88
- package/src/llm/re-prompter.js +41 -0
- package/src/llm/validator.js +72 -0
- package/src/parser/ast/generic.js +45 -222
- package/src/parser/ast/go.js +86 -205
- package/src/parser/ast/java.js +76 -146
- package/src/parser/ast/javascript.js +173 -241
- package/src/parser/ast/laravel/blade.js +56 -0
- package/src/parser/ast/laravel/classifier.js +30 -0
- package/src/parser/ast/laravel/controller.js +35 -0
- package/src/parser/ast/laravel/index.js +54 -0
- package/src/parser/ast/laravel/model.js +41 -0
- package/src/parser/ast/laravel/provider.js +28 -0
- package/src/parser/ast/laravel/routes.js +45 -0
- package/src/parser/ast/php.js +129 -0
- package/src/parser/ast/python.js +76 -199
- package/src/parser/ast/typescript.js +10 -244
- package/src/parser/body-extractor.js +40 -0
- package/src/parser/call-graph.js +50 -67
- package/src/parser/complexity-scorer.js +59 -0
- package/src/parser/index.js +61 -86
- package/src/parser/pattern-detector.js +71 -0
- package/src/parser/pkg-builder.js +36 -83
- package/src/renderer/html.js +63 -135
- package/src/renderer/index.js +23 -35
- package/src/renderer/json.js +17 -35
- package/src/renderer/markdown.js +83 -117
- package/src/utils/config.js +52 -53
- package/src/utils/errors.js +26 -41
- package/src/utils/logger.js +32 -53
- package/src/cli/flags.js +0 -87
- package/src/llm/providers/anthropic.js +0 -57
- package/src/llm/providers/google.js +0 -65
- package/src/llm/providers/openai.js +0 -52
- package/src/parser/ast/tree-sitter-init.js +0 -80
|
@@ -1,240 +1,262 @@
|
|
|
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
|
-
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
|
-
|
|
49
|
-
|
|
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:
|
|
52
|
-
const
|
|
53
|
-
|
|
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(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
51
|
+
// ─── Incremental cache ────────────────────────────────────────────────────
|
|
74
52
|
let cacheMap = {};
|
|
75
|
-
let
|
|
76
|
-
let
|
|
53
|
+
let cacheHits = [];
|
|
54
|
+
let cacheMisses = manifest.files;
|
|
77
55
|
|
|
78
56
|
if (config.incremental) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
cacheMap =
|
|
82
|
-
const { hits, misses } = getCacheHits(manifest, cacheMap);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
66
|
+
const filesToAnalyze = cacheMisses;
|
|
67
|
+
|
|
68
|
+
// ─── Stage 2: AST Parser ─────────────────────────────────────────────────
|
|
91
69
|
const parseSpinner = createSpinner('Parsing AST...');
|
|
92
70
|
parseSpinner.start();
|
|
93
71
|
|
|
94
|
-
|
|
72
|
+
const { parseFiles } = require('../../parser/index');
|
|
73
|
+
let pkg;
|
|
95
74
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
//
|
|
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(
|
|
118
|
-
console.log(
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
//
|
|
138
|
-
const
|
|
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
|
|
142
|
-
|
|
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: (
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
//
|
|
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(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
});
|
|
176
|
-
|
|
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
|
-
|
|
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:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
outputDir
|
|
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 (
|
|
198
|
-
console.log(JSON.stringify(stats
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
};
|