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,149 +1,115 @@
1
- /**
2
- * Markdown renderer.
3
- * Takes DocFragment[] and PKG.
4
- * Mirrors source directory structure in output dir.
5
- * Writes one .md per file.
6
- * Generates index.md with project metadata + Mermaid dependency diagram + module index.
7
- * Generates SUMMARY.md in GitBook format.
8
- */
1
+ 'use strict';
9
2
 
10
- import { mkdirSync, writeFileSync } from 'node:fs';
11
- import { join, dirname, relative } from 'node:path';
3
+ const { writeFileSync, mkdirSync, existsSync } = require('fs');
4
+ const path = require('path');
12
5
 
13
6
  /**
14
- * Render documentation as Markdown files.
15
- * @param {import('../llm/queue.js').DocFragment[]} fragments
16
- * @param {import('../parser/pkg-builder.js').ProjectKnowledgeGraph} pkg
17
- * @param {string} outputDir - Output directory path
7
+ * Markdown renderer produces one .md per source file + index.md + SUMMARY.md.
18
8
  */
19
- export async function renderMarkdown(fragments, pkg, outputDir) {
9
+ async function render(fragments, pkg, outputDir, config) {
20
10
  mkdirSync(outputDir, { recursive: true });
21
11
 
22
- // Write individual file docs
23
- for (const fragment of fragments) {
24
- const mdPath = join(outputDir, fragment.relativePath.replace(/\.\w+$/, '.md'));
25
- mkdirSync(dirname(mdPath), { recursive: true });
26
- writeFileSync(mdPath, fragment.content, 'utf-8');
12
+ const summaryLines = ['# Summary\n'];
13
+
14
+ for (const frag of fragments) {
15
+ const relOut = frag.relativePath.replace(/\.[^.]+$/, '.md');
16
+ const outPath = path.join(outputDir, relOut);
17
+ mkdirSync(path.dirname(outPath), { recursive: true });
18
+ writeFileSync(outPath, frag.content, 'utf8');
19
+ summaryLines.push(`* [${frag.relativePath}](${relOut})`);
27
20
  }
28
21
 
29
- // Generate index.md
30
- const indexContent = generateIndex(pkg, fragments);
31
- writeFileSync(join(outputDir, 'index.md'), indexContent, 'utf-8');
22
+ // SUMMARY.md (GitBook / Docusaurus compatible)
23
+ writeFileSync(path.join(outputDir, 'SUMMARY.md'), summaryLines.join('\n'), 'utf8');
32
24
 
33
- // Generate SUMMARY.md
34
- const summaryContent = generateSummary(fragments);
35
- writeFileSync(join(outputDir, 'SUMMARY.md'), summaryContent, 'utf-8');
25
+ // index.md
26
+ writeFileSync(path.join(outputDir, 'index.md'), buildIndexMd(pkg, fragments), 'utf8');
36
27
  }
37
28
 
38
- function generateIndex(pkg, fragments) {
29
+ function buildIndexMd(pkg, fragments) {
30
+ const meta = pkg.meta || {};
39
31
  const lines = [];
40
-
41
- lines.push(`# ${pkg.meta.name}`);
32
+ lines.push(`# ${meta.name || 'Project'} — Documentation`);
42
33
  lines.push('');
43
- lines.push(`> Auto-generated documentation by Legacyver`);
34
+ lines.push(`**Primary language:** ${meta.primaryLanguage || 'unknown'} `);
35
+ lines.push(`**Total files:** ${meta.totalFiles || fragments.length} `);
36
+ lines.push(`**Analyzed at:** ${meta.analyzedAt || new Date().toISOString()} `);
37
+ if (meta.framework) lines.push(`**Framework:** ${meta.framework} `);
44
38
  lines.push('');
45
- lines.push('## Project Overview');
39
+
40
+ // File tree
41
+ lines.push('## Files');
46
42
  lines.push('');
47
- lines.push(`| Property | Value |`);
48
- lines.push(`|---|---|`);
49
- lines.push(`| Primary Language | ${pkg.meta.primaryLanguage} |`);
50
- lines.push(`| Total Files | ${pkg.meta.totalFiles} |`);
51
- lines.push(`| Analyzed At | ${pkg.meta.analyzedAt} |`);
43
+ for (const frag of fragments) {
44
+ const relOut = frag.relativePath.replace(/\.[^.]+$/, '.md');
45
+ lines.push(`- [${frag.relativePath}](${relOut})`);
46
+ }
52
47
  lines.push('');
53
48
 
54
- // Entry points
55
- if (pkg.entryPoints.length > 0) {
56
- lines.push('## Entry Points');
57
- lines.push('');
58
- for (const ep of pkg.entryPoints) {
59
- const mdLink = ep.replace(/\.\w+$/, '.md');
60
- lines.push(`- [${ep}](${mdLink})`);
49
+ // Mermaid dependency graph
50
+ lines.push('## Dependency Graph');
51
+ lines.push('');
52
+ lines.push('```mermaid');
53
+ lines.push('graph TD');
54
+ const graph = pkg.graph || {};
55
+ for (const [from, targets] of Object.entries(graph)) {
56
+ for (const to of targets) {
57
+ const fromId = sanitizeMermaid(from);
58
+ const toId = sanitizeMermaid(to);
59
+ lines.push(` ${fromId}["${from}"] --> ${toId}["${to}"]`);
61
60
  }
62
- lines.push('');
63
61
  }
62
+ lines.push('```');
63
+ lines.push('');
64
64
 
65
- // Dependency diagram (Mermaid)
66
- if (Object.keys(pkg.graph).length > 0) {
67
- lines.push('## Dependency Graph');
68
- lines.push('');
69
- lines.push('```mermaid');
70
- lines.push('graph TD');
71
-
72
- const nodeIds = new Map();
73
- let nodeCounter = 0;
74
-
75
- function getNodeId(path) {
76
- if (!nodeIds.has(path)) {
77
- nodeIds.set(path, `N${nodeCounter++}`);
65
+ // Laravel-specific sections
66
+ if (pkg.laravelMeta) {
67
+ const lm = pkg.laravelMeta;
68
+
69
+ if (lm.routeMap && lm.routeMap.length > 0) {
70
+ lines.push('## Route Map');
71
+ lines.push('');
72
+ lines.push('| Method | URI | Controller | Middleware | Route Name |');
73
+ lines.push('|--------|-----|------------|------------|------------|');
74
+ for (const r of lm.routeMap) {
75
+ lines.push(`| ${r.method || ''} | ${r.uri || ''} | ${r.controller || ''}${r.action ? '@' + r.action : ''} | ${r.middleware || ''} | ${r.name || ''} |`);
78
76
  }
79
- return nodeIds.get(path);
77
+ lines.push('');
80
78
  }
81
79
 
82
- for (const [source, targets] of Object.entries(pkg.graph)) {
83
- const sourceId = getNodeId(source);
84
- for (const target of targets) {
85
- const targetId = getNodeId(target);
86
- lines.push(` ${sourceId}["${source}"] --> ${targetId}["${target}"]`);
80
+ if (lm.relationships && lm.relationships.length > 0) {
81
+ lines.push('## Model Relationships');
82
+ lines.push('');
83
+ lines.push('```mermaid');
84
+ lines.push('erDiagram');
85
+ for (const rel of lm.relationships) {
86
+ const from = rel.fromModel || 'Model';
87
+ const to = rel.relatedModel || 'Related';
88
+ const label = rel.type || 'relates';
89
+ lines.push(` ${from} ||--o{ ${to} : "${label}"`);
87
90
  }
91
+ lines.push('```');
92
+ lines.push('');
88
93
  }
89
94
 
90
- lines.push('```');
91
- lines.push('');
92
- }
93
-
94
- // Module index
95
- lines.push('## Module Index');
96
- lines.push('');
97
- lines.push('| File | Language | Lines | Functions | Classes |');
98
- lines.push('|---|---|---|---|---|');
99
-
100
- const sortedPaths = Object.keys(pkg.files).sort();
101
- for (const filePath of sortedPaths) {
102
- const facts = pkg.files[filePath];
103
- const mdLink = filePath.replace(/\.\w+$/, '.md');
104
- lines.push(
105
- `| [${filePath}](${mdLink}) | ${facts.language} | ${facts.linesOfCode} | ${facts.functions.length} | ${facts.classes.length} |`
106
- );
95
+ if (lm.providerBindings && lm.providerBindings.length > 0) {
96
+ lines.push('## Service Provider Bindings');
97
+ lines.push('');
98
+ for (const pb of lm.providerBindings) {
99
+ lines.push(`### ${pb.provider}`);
100
+ for (const b of pb.bindings) {
101
+ lines.push(`- \`${b}\``);
102
+ }
103
+ }
104
+ lines.push('');
105
+ }
107
106
  }
108
107
 
109
- lines.push('');
110
- lines.push('---');
111
- lines.push('*Generated by [Legacyver](https://github.com/legacyver/legacyver)*');
112
- lines.push('');
113
-
114
108
  return lines.join('\n');
115
109
  }
116
110
 
117
- function generateSummary(fragments) {
118
- const lines = [];
119
-
120
- lines.push('# Summary');
121
- lines.push('');
122
- lines.push('* [Index](index.md)');
123
-
124
- // Group by directory
125
- const dirs = new Map();
126
- for (const fragment of fragments) {
127
- const dir = dirname(fragment.relativePath);
128
- if (!dirs.has(dir)) {
129
- dirs.set(dir, []);
130
- }
131
- dirs.get(dir).push(fragment);
132
- }
133
-
134
- const sortedDirs = [...dirs.keys()].sort();
135
- for (const dir of sortedDirs) {
136
- if (dir !== '.') {
137
- lines.push(`* ${dir}`);
138
- }
139
- const files = dirs.get(dir).sort((a, b) => a.relativePath.localeCompare(b.relativePath));
140
- for (const f of files) {
141
- const mdPath = f.relativePath.replace(/\.\w+$/, '.md');
142
- const indent = dir !== '.' ? ' ' : '';
143
- lines.push(`${indent}* [${f.relativePath}](${mdPath})`);
144
- }
145
- }
146
-
147
- lines.push('');
148
- return lines.join('\n');
111
+ function sanitizeMermaid(str) {
112
+ return str.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^_+/, '').slice(0, 40) || 'node';
149
113
  }
114
+
115
+ module.exports = { render };
@@ -1,65 +1,64 @@
1
- /**
2
- * Configuration loader using cosmiconfig.
3
- * Loads .legacyverrc / legacyver.config.js / legacyver.config.yaml
4
- * Merges with CLI flags (CLI wins).
5
- */
6
-
7
- import { cosmiconfig } from 'cosmiconfig';
1
+ 'use strict';
8
2
 
9
- const DEFAULTS = {
10
- provider: 'anthropic',
11
- model: null,
12
- concurrency: 5,
13
- maxFileSizeKb: 500,
14
- output: './legacyver-docs',
15
- format: 'markdown',
16
- incremental: false,
17
- noConfirm: false,
18
- verbose: false,
19
- languages: ['javascript', 'typescript', 'python', 'java', 'go'],
20
- ignore: [],
21
- };
3
+ const { cosmiconfigSync } = require('cosmiconfig');
22
4
 
23
- let cachedConfig = null;
5
+ const explorer = cosmiconfigSync('legacyver', {
6
+ searchPlaces: [
7
+ '.legacyverrc',
8
+ '.legacyverrc.json',
9
+ '.legacyverrc.yaml',
10
+ '.legacyverrc.yml',
11
+ '.legacyverrc.js',
12
+ 'legacyver.config.js',
13
+ 'legacyver.config.yaml',
14
+ 'legacyver.config.yml',
15
+ ],
16
+ });
24
17
 
25
18
  /**
26
- * Load configuration from rc files.
27
- * @param {string} [searchFrom] - Directory to search from (defaults to cwd)
28
- * @returns {Promise<object>} Merged config
19
+ * Load configuration from file and merge with CLI flags.
20
+ * CLI flags always win over file config.
21
+ * @param {Object} cliFlags
22
+ * @returns {Object}
29
23
  */
30
- export async function loadConfig(searchFrom) {
31
- if (cachedConfig) return cachedConfig;
24
+ function loadConfig(cliFlags = {}) {
25
+ let fileConfig = {};
26
+ try {
27
+ const result = explorer.search();
28
+ if (result && result.config) {
29
+ fileConfig = { ...result.config }; // shallow copy — never mutate the cached object
30
+ }
31
+ } catch (e) {
32
+ // no config file found — use defaults
33
+ }
32
34
 
33
- const explorer = cosmiconfig('legacyver');
34
- const result = await explorer.search(searchFrom || process.cwd());
35
+ const defaults = {
36
+ provider: 'openrouter',
37
+ model: undefined,
38
+ format: 'markdown',
39
+ out: './legacyver-docs',
40
+ concurrency: 3,
41
+ maxFileSizeKb: 500,
42
+ dryRun: false,
43
+ incremental: false,
44
+ confirm: true,
45
+ verbose: false,
46
+ };
35
47
 
36
- const fileConfig = result ? result.config : {};
37
- cachedConfig = { ...DEFAULTS, ...fileConfig };
38
- return cachedConfig;
39
- }
48
+ // Merge: defaults < fileConfig < cliFlags (strip undefined cliFlags)
49
+ const cleanCli = Object.fromEntries(
50
+ Object.entries(cliFlags).filter(([, v]) => v !== undefined)
51
+ );
40
52
 
41
- /**
42
- * Merge loaded config with CLI flags. CLI flags take precedence.
43
- * Only non-undefined CLI values override config.
44
- * @param {object} config - Loaded config
45
- * @param {object} cliFlags - CLI flag values
46
- * @returns {object} Merged config
47
- */
48
- export function mergeWithFlags(config, cliFlags) {
49
- const merged = { ...config };
50
- for (const [key, value] of Object.entries(cliFlags)) {
51
- if (value !== undefined && value !== null) {
52
- merged[key] = value;
53
- }
53
+ // If CLI specifies a different provider than what's in the config file,
54
+ // discard the file's model it belongs to the old provider.
55
+ const effectiveProvider = cleanCli.provider || fileConfig.provider || defaults.provider;
56
+ const fileConfigProvider = fileConfig.provider || defaults.provider;
57
+ if (effectiveProvider !== fileConfigProvider && !cleanCli.model) {
58
+ delete fileConfig.model;
54
59
  }
55
- return merged;
56
- }
57
60
 
58
- /**
59
- * Reset cached config (for testing).
60
- */
61
- export function resetConfig() {
62
- cachedConfig = null;
61
+ return { ...defaults, ...fileConfig, ...cleanCli };
63
62
  }
64
63
 
65
- export { DEFAULTS };
64
+ module.exports = { loadConfig };
@@ -1,70 +1,55 @@
1
+ 'use strict';
2
+
1
3
  /**
2
4
  * Custom error classes for Legacyver.
3
5
  */
4
6
 
5
- export class LegacyverError extends Error {
6
- constructor(message, options = {}) {
7
+ class LegacyverError extends Error {
8
+ constructor(message, code) {
7
9
  super(message);
8
10
  this.name = 'LegacyverError';
9
- this.exitCode = options.exitCode || 1;
10
- this.suggestion = options.suggestion || null;
11
+ this.code = code || 'LEGACYVER_ERROR';
11
12
  }
12
13
  }
13
14
 
14
- export class NoApiKeyError extends LegacyverError {
15
+ class NoApiKeyError extends LegacyverError {
15
16
  constructor(provider) {
16
- const envVarMap = {
17
- anthropic: 'ANTHROPIC_API_KEY',
18
- openai: 'OPENAI_API_KEY',
19
- google: 'GOOGLE_API_KEY',
20
- groq: 'GROQ_API_KEY',
21
- ollama: null,
22
- };
23
- const envVar = envVarMap[provider] || `${provider.toUpperCase()}_API_KEY`;
24
- const suggestion = provider === 'ollama'
25
- ? 'Ensure Ollama is running locally at http://localhost:11434'
26
- : `Set the ${envVar} environment variable or run "legacyver init" to configure`;
27
-
28
- super(`No API key found for provider "${provider}"`, {
29
- exitCode: 1,
30
- suggestion,
31
- });
17
+ super(
18
+ `No API key found for provider "${provider}". ` +
19
+ `Set the OPENROUTER_API_KEY environment variable or run \`legacyver init\`. ` +
20
+ `Get a key at https://openrouter.ai/keys`,
21
+ 'NO_API_KEY'
22
+ );
32
23
  this.name = 'NoApiKeyError';
33
24
  this.provider = provider;
34
- this.envVar = envVar;
35
25
  }
36
26
  }
37
27
 
38
- export class RateLimitError extends LegacyverError {
39
- constructor(provider, retryAfterMs = null) {
40
- super(`Rate limit exceeded for provider "${provider}"`, {
41
- exitCode: 2,
42
- suggestion: 'Reduce concurrency with --concurrency flag or wait and retry',
43
- });
28
+ class RateLimitError extends LegacyverError {
29
+ constructor(provider, retryAfter) {
30
+ super(`Rate limit exceeded for provider "${provider}". Retrying...`, 'RATE_LIMIT');
44
31
  this.name = 'RateLimitError';
45
32
  this.provider = provider;
46
- this.retryAfterMs = retryAfterMs;
33
+ this.retryAfter = retryAfter || 1000;
47
34
  }
48
35
  }
49
36
 
50
- export class ParseError extends LegacyverError {
51
- constructor(filePath, reason) {
52
- super(`Failed to parse "${filePath}": ${reason}`, {
53
- exitCode: 0, // recoverable — don't exit
54
- suggestion: 'File will be processed with the generic fallback parser',
55
- });
37
+ class ParseError extends LegacyverError {
38
+ constructor(filePath, originalError) {
39
+ super(`Failed to parse file "${filePath}": ${originalError && originalError.message || originalError}`, 'PARSE_ERROR');
56
40
  this.name = 'ParseError';
57
41
  this.filePath = filePath;
42
+ this.originalError = originalError;
58
43
  }
59
44
  }
60
45
 
61
- export class RenderError extends LegacyverError {
62
- constructor(format, reason) {
63
- super(`Render failed for format "${format}": ${reason}`, {
64
- exitCode: 3,
65
- suggestion: 'Check write permissions on the output directory',
66
- });
46
+ class RenderError extends LegacyverError {
47
+ constructor(format, originalError) {
48
+ super(`Renderer failed for format "${format}": ${originalError && originalError.message || originalError}`, 'RENDER_ERROR');
67
49
  this.name = 'RenderError';
68
50
  this.format = format;
51
+ this.originalError = originalError;
69
52
  }
70
53
  }
54
+
55
+ module.exports = { LegacyverError, NoApiKeyError, RateLimitError, ParseError, RenderError };
@@ -1,67 +1,46 @@
1
- /**
2
- * Structured logger with log levels and TTY detection for CI mode.
3
- */
1
+ 'use strict';
4
2
 
5
- import picocolors from 'picocolors';
3
+ const pc = require('picocolors');
6
4
 
7
- let verbose = false;
8
- const isTTY = process.stdout.isTTY === true;
5
+ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
9
6
 
10
- export function setVerbose(value) {
11
- verbose = value;
12
- }
7
+ let currentLevel = 'info';
8
+ let isCI = !process.stdout.isTTY;
13
9
 
14
- export function isVerbose() {
15
- return verbose;
10
+ function setLevel(level) {
11
+ if (LOG_LEVELS[level] !== undefined) currentLevel = level;
16
12
  }
17
13
 
18
- export function isCIMode() {
19
- return !isTTY;
14
+ function setCI(val) {
15
+ isCI = val;
20
16
  }
21
17
 
22
- function timestamp() {
23
- return new Date().toISOString();
18
+ function shouldLog(level) {
19
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
24
20
  }
25
21
 
26
- export const logger = {
27
- info(message, ...args) {
28
- if (isTTY) {
29
- console.log(picocolors.blue('ℹ'), message, ...args);
30
- } else {
31
- console.log(`[${timestamp()}] INFO: ${message}`, ...args);
32
- }
33
- },
22
+ function debug(...args) {
23
+ if (shouldLog('debug')) {
24
+ console.debug(pc.gray('[debug]'), ...args);
25
+ }
26
+ }
34
27
 
35
- warn(message, ...args) {
36
- if (isTTY) {
37
- console.warn(picocolors.yellow(''), message, ...args);
38
- } else {
39
- console.warn(`[${timestamp()}] WARN: ${message}`, ...args);
40
- }
41
- },
28
+ function info(...args) {
29
+ if (shouldLog('info')) {
30
+ console.log(pc.cyan('[info]'), ...args);
31
+ }
32
+ }
42
33
 
43
- error(message, ...args) {
44
- if (isTTY) {
45
- console.error(picocolors.red(''), message, ...args);
46
- } else {
47
- console.error(`[${timestamp()}] ERROR: ${message}`, ...args);
48
- }
49
- },
34
+ function warn(...args) {
35
+ if (shouldLog('warn')) {
36
+ console.warn(pc.yellow('[warn]'), ...args);
37
+ }
38
+ }
50
39
 
51
- debug(message, ...args) {
52
- if (!verbose) return;
53
- if (isTTY) {
54
- console.log(picocolors.gray('●'), picocolors.gray(message), ...args);
55
- } else {
56
- console.log(`[${timestamp()}] DEBUG: ${message}`, ...args);
57
- }
58
- },
40
+ function error(...args) {
41
+ if (shouldLog('error')) {
42
+ console.error(pc.red('[error]'), ...args);
43
+ }
44
+ }
59
45
 
60
- success(message, ...args) {
61
- if (isTTY) {
62
- console.log(picocolors.green('✔'), message, ...args);
63
- } else {
64
- console.log(`[${timestamp()}] OK: ${message}`, ...args);
65
- }
66
- },
67
- };
46
+ module.exports = { debug, info, warn, error, setLevel, setCI };
package/src/cli/flags.js DELETED
@@ -1,87 +0,0 @@
1
- /**
2
- * Shared CLI flag definitions with types, defaults, and descriptions.
3
- */
4
-
5
- export const FLAG_DEFINITIONS = {
6
- provider: {
7
- flags: '-p, --provider <name>',
8
- description: 'LLM provider to use (anthropic, openai, google, groq, ollama)',
9
- default: 'anthropic',
10
- },
11
- model: {
12
- flags: '-m, --model <name>',
13
- description: 'Model name to use (overrides provider default)',
14
- default: undefined,
15
- },
16
- output: {
17
- flags: '-o, --output <dir>',
18
- description: 'Output directory for generated documentation',
19
- default: './legacyver-docs',
20
- },
21
- format: {
22
- flags: '-f, --format <type>',
23
- description: 'Output format: markdown, html, json',
24
- default: 'markdown',
25
- },
26
- concurrency: {
27
- flags: '-c, --concurrency <number>',
28
- description: 'Number of concurrent LLM requests',
29
- default: '5',
30
- parseArg: (val) => parseInt(val, 10),
31
- },
32
- maxFileSize: {
33
- flags: '--max-file-size <kb>',
34
- description: 'Skip files larger than this size in KB',
35
- default: '500',
36
- parseArg: (val) => parseInt(val, 10),
37
- },
38
- incremental: {
39
- flags: '--incremental',
40
- description: 'Only analyze files changed since last run',
41
- default: false,
42
- },
43
- noConfirm: {
44
- flags: '--no-confirm',
45
- description: 'Skip cost confirmation prompt (required for CI)',
46
- default: false,
47
- },
48
- dryRun: {
49
- flags: '--dry-run',
50
- description: 'Show cost estimate without running analysis',
51
- default: false,
52
- },
53
- ignore: {
54
- flags: '--ignore <patterns...>',
55
- description: 'Additional glob patterns to ignore',
56
- default: [],
57
- },
58
- jsonSummary: {
59
- flags: '--json-summary',
60
- description: 'Output machine-readable JSON summary to stdout',
61
- default: false,
62
- },
63
- verbose: {
64
- flags: '--verbose',
65
- description: 'Enable verbose debug output',
66
- default: false,
67
- },
68
- };
69
-
70
- /**
71
- * Apply standard flags to a commander command.
72
- * @param {import('commander').Command} command
73
- * @param {string[]} flagNames - Which flags to apply
74
- * @returns {import('commander').Command}
75
- */
76
- export function applyFlags(command, flagNames) {
77
- for (const name of flagNames) {
78
- const def = FLAG_DEFINITIONS[name];
79
- if (!def) continue;
80
- if (def.parseArg) {
81
- command.option(def.flags, def.description, def.parseArg, def.default);
82
- } else {
83
- command.option(def.flags, def.description, def.default);
84
- }
85
- }
86
- return command;
87
- }