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,125 +1,81 @@
1
- /**
2
- * Init command — interactive wizard to configure legacyver.
3
- * Saves API key to OS user config using `conf`.
4
- * Creates example .legacyverrc file.
5
- */
1
+ 'use strict';
6
2
 
7
- import { createInterface } from 'node:readline';
8
- import { existsSync, writeFileSync } from 'node:fs';
9
- import { join } from 'node:path';
10
- import Conf from 'conf';
11
- import picocolors from 'picocolors';
12
- import { logger } from '../../utils/logger.js';
13
-
14
- const userConfig = new Conf({ projectName: 'legacyver' });
15
-
16
- const PROVIDERS = ['anthropic', 'openai', 'google', 'groq', 'ollama'];
17
-
18
- const ENV_VAR_MAP = {
19
- anthropic: 'ANTHROPIC_API_KEY',
20
- openai: 'OPENAI_API_KEY',
21
- google: 'GOOGLE_API_KEY',
22
- groq: 'GROQ_API_KEY',
23
- };
3
+ const { existsSync, writeFileSync } = require('fs');
4
+ const { join } = require('path');
5
+ const readline = require('readline');
6
+ const pc = require('picocolors');
24
7
 
25
8
  function ask(rl, question) {
26
- return new Promise((resolve) => {
27
- rl.question(question, resolve);
28
- });
9
+ return new Promise((resolve) => rl.question(question, resolve));
29
10
  }
30
11
 
31
- async function runInitWizard() {
32
- const rl = createInterface({
33
- input: process.stdin,
34
- output: process.stdout,
35
- });
36
-
37
- console.log('');
38
- console.log(picocolors.bold('Legacyver Setup Wizard'));
39
- console.log('');
40
-
41
- // Select provider
42
- console.log('Available LLM providers:');
43
- PROVIDERS.forEach((p, i) => {
44
- console.log(` ${i + 1}. ${p}${p === 'ollama' ? ' (no API key needed)' : ''}`);
45
- });
46
- console.log('');
12
+ module.exports = async function initCommand() {
13
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
47
14
 
48
- const providerChoice = await ask(rl, 'Select provider (1-5) [1]: ');
49
- const providerIndex = parseInt(providerChoice, 10) - 1;
50
- const provider = PROVIDERS[providerIndex >= 0 && providerIndex < PROVIDERS.length ? providerIndex : 0];
15
+ console.log(pc.bold('\nWelcome to Legacyver setup wizard!\n'));
51
16
 
52
- console.log(`Selected: ${picocolors.cyan(provider)}`);
53
-
54
- // API key (skip for ollama)
55
- if (provider !== 'ollama') {
56
- const envVar = ENV_VAR_MAP[provider];
57
- const existingKey = process.env[envVar] || userConfig.get(`apiKeys.${provider}`);
58
-
59
- if (existingKey) {
60
- console.log(` API key found ${process.env[envVar] ? '(from environment)' : '(from saved config)'}`);
61
- const overwrite = await ask(rl, ' Overwrite? (y/N): ');
62
- if (overwrite.toLowerCase() === 'y') {
63
- const newKey = await ask(rl, ` Enter ${envVar}: `);
64
- if (newKey.trim()) {
65
- userConfig.set(`apiKeys.${provider}`, newKey.trim());
66
- logger.success('API key saved to user config');
67
- }
68
- }
69
- } else {
70
- const newKey = await ask(rl, ` Enter ${envVar}: `);
71
- if (newKey.trim()) {
72
- userConfig.set(`apiKeys.${provider}`, newKey.trim());
73
- logger.success('API key saved to user config');
74
- } else {
75
- logger.warn(`No API key set. Set ${envVar} environment variable before running.`);
76
- }
77
- }
78
- }
79
-
80
- // Save default provider
81
- userConfig.set('defaultProvider', provider);
82
-
83
- // Create .legacyverrc
84
17
  const rcPath = join(process.cwd(), '.legacyverrc');
85
18
  if (existsSync(rcPath)) {
86
- const overwrite = await ask(rl, `.legacyverrc already exists. Overwrite? (y/N): `);
87
- if (overwrite.toLowerCase() !== 'y') {
19
+ const overwrite = await ask(rl, pc.yellow('⚠ .legacyverrc already exists. Overwrite? [y/N] '));
20
+ if (overwrite.trim().toLowerCase() !== 'y') {
21
+ console.log('Aborted.');
88
22
  rl.close();
89
- logger.info('Setup complete (existing .legacyverrc preserved).');
90
23
  return;
91
24
  }
92
25
  }
93
26
 
94
- const rcContent = JSON.stringify({
95
- provider,
96
- format: 'markdown',
97
- output: './legacyver-docs',
98
- concurrency: 5,
99
- maxFileSizeKb: 500,
100
- ignore: ['test/**', '**/*.test.*', '**/node_modules/**'],
101
- }, null, 2);
27
+ const providerRaw = await ask(rl, `LLM provider [openrouter/gemini/groq/ollama] (default: openrouter): `);
28
+ const providerChoice = providerRaw.trim() || 'openrouter';
29
+ const isOllama = providerChoice === 'ollama';
30
+ const isGroq = providerChoice === 'groq';
31
+ const isGemini = providerChoice === 'gemini';
32
+
33
+ const defaultModel = isOllama
34
+ ? 'llama3.2'
35
+ : isGroq
36
+ ? 'llama-3.3-70b-versatile'
37
+ : isGemini
38
+ ? 'gemini-2.0-flash'
39
+ : 'meta-llama/llama-3.3-70b-instruct:free';
40
+
41
+ let apiKey = '';
42
+ if (!isOllama) {
43
+ const keyLabel = isGemini ? 'Google Gemini' : isGroq ? 'Groq' : 'OpenRouter';
44
+ const keyHint = isGemini ? 'https://aistudio.google.com/apikey' : isGroq ? 'https://console.groq.com/keys' : 'https://openrouter.ai/keys';
45
+ apiKey = await ask(rl, `${keyLabel} API key (leave blank to set via env var — see ${keyHint}): `);
46
+ }
102
47
 
103
- writeFileSync(rcPath, rcContent + '\n', 'utf-8');
104
- logger.success(`Created ${rcPath}`);
48
+ const modelRaw = await ask(rl, `Default model (default: ${defaultModel}): `);
49
+ const formatRaw = await ask(rl, `Default output format [markdown/html/json] (default: markdown): `);
105
50
 
106
51
  rl.close();
107
- console.log('');
108
- console.log(picocolors.green('Setup complete!'));
109
- console.log(`Run ${picocolors.cyan('legacyver analyze ./src')} to generate documentation.`);
110
- console.log('');
111
- }
112
52
 
113
- export function registerInitCommand(program) {
114
- program
115
- .command('init')
116
- .description('Interactive setup wizard to configure legacyver')
117
- .action(async () => {
118
- try {
119
- await runInitWizard();
120
- } catch (err) {
121
- logger.error('Init failed:', err.message);
122
- process.exit(1);
123
- }
124
- });
125
- }
53
+ const config = {
54
+ provider: providerChoice,
55
+ model: modelRaw.trim() || defaultModel,
56
+ format: formatRaw.trim() || 'markdown',
57
+ out: './legacyver-docs',
58
+ };
59
+
60
+ if (apiKey.trim()) {
61
+ if (isGemini) {
62
+ config.geminiApiKey = apiKey.trim();
63
+ } else if (isGroq) {
64
+ config.groqApiKey = apiKey.trim();
65
+ } else {
66
+ config.apiKey = apiKey.trim();
67
+ }
68
+ }
69
+
70
+ writeFileSync(rcPath, JSON.stringify(config, null, 2), 'utf8');
71
+ console.log(pc.green('\n✓ Created .legacyverrc'));
72
+
73
+ const exampleCmd = isOllama
74
+ ? 'legacyver analyze --provider ollama'
75
+ : isGroq
76
+ ? 'legacyver analyze --provider groq'
77
+ : isGemini
78
+ ? 'legacyver analyze --provider gemini'
79
+ : 'legacyver analyze';
80
+ console.log(pc.cyan(`\nRun \`${exampleCmd}\` to generate documentation.`));
81
+ };
@@ -1,83 +1,58 @@
1
- /**
2
- * Providers subcommand — lists all supported LLM providers,
3
- * API key status (detected/missing), and cost per 1000 tokens.
4
- */
5
-
6
- import picocolors from 'picocolors';
7
- import Conf from 'conf';
8
-
9
- const userConfig = new Conf({ projectName: 'legacyver' });
10
-
11
- const PROVIDERS = [
12
- {
13
- name: 'anthropic',
14
- envVar: 'ANTHROPIC_API_KEY',
15
- defaultModel: 'claude-haiku-3-5',
16
- costPer1kInput: 0.00025,
17
- costPer1kOutput: 0.00125,
18
- },
19
- {
20
- name: 'openai',
21
- envVar: 'OPENAI_API_KEY',
22
- defaultModel: 'gpt-4o-mini',
23
- costPer1kInput: 0.00015,
24
- costPer1kOutput: 0.0006,
25
- },
26
- {
27
- name: 'google',
28
- envVar: 'GOOGLE_API_KEY',
29
- defaultModel: 'gemini-1.5-flash',
30
- costPer1kInput: 0.000075,
31
- costPer1kOutput: 0.0003,
32
- },
33
- {
34
- name: 'groq',
35
- envVar: 'GROQ_API_KEY',
36
- defaultModel: 'llama-3.3-70b-versatile',
37
- costPer1kInput: 0.00059,
38
- costPer1kOutput: 0.00079,
39
- },
40
- {
41
- name: 'ollama',
42
- envVar: null,
43
- defaultModel: 'llama3.2',
44
- costPer1kInput: 0,
45
- costPer1kOutput: 0,
46
- },
1
+ 'use strict';
2
+
3
+ const pc = require('picocolors');
4
+ const logger = require('../../utils/logger');
5
+
6
+ const RECOMMENDED_MODELS = [
7
+ { id: 'meta-llama/llama-3.3-70b-instruct:free', context: '128k', inputCost: 0, outputCost: 0, free: true },
8
+ { id: 'anthropic/claude-haiku-3-5', context: '200k', inputCost: 0.80, outputCost: 4.00, free: false },
9
+ { id: 'anthropic/claude-sonnet-4-5', context: '200k', inputCost: 3.00, outputCost: 15.00, free: false },
10
+ { id: 'openai/gpt-4o-mini', context: '128k', inputCost: 0.15, outputCost: 0.60, free: false },
11
+ { id: 'openai/gpt-4o', context: '128k', inputCost: 5.00, outputCost: 15.00, free: false },
12
+ { id: 'google/gemini-flash-1.5', context: '1M', inputCost: 0.075, outputCost: 0.30, free: false },
13
+ { id: 'mistralai/mistral-7b-instruct:free', context: '32k', inputCost: 0, outputCost: 0, free: true },
47
14
  ];
48
15
 
49
- function detectApiKey(provider) {
50
- if (!provider.envVar) return 'N/A (local)';
51
-
52
- if (process.env[provider.envVar]) return picocolors.green('detected (env)');
53
- if (userConfig.get(`apiKeys.${provider.name}`)) return picocolors.green('detected (config)');
54
- return picocolors.red('missing');
55
- }
56
-
57
- export function registerProvidersCommand(program) {
58
- program
59
- .command('providers')
60
- .description('List supported LLM providers and their status')
61
- .action(() => {
62
- console.log('');
63
- console.log(picocolors.bold('Supported LLM Providers'));
64
- console.log('');
65
-
66
- const header = ` ${'Provider'.padEnd(12)} ${'Default Model'.padEnd(28)} ${'API Key'.padEnd(22)} ${'Cost/1k input'.padEnd(14)} Cost/1k output`;
67
- console.log(picocolors.dim(header));
68
- console.log(picocolors.dim(' ' + ''.repeat(90)));
69
-
70
- for (const p of PROVIDERS) {
71
- const status = detectApiKey(p);
72
- const inputCost = p.costPer1kInput === 0 ? 'free' : `$${p.costPer1kInput.toFixed(5)}`;
73
- const outputCost = p.costPer1kOutput === 0 ? 'free' : `$${p.costPer1kOutput.toFixed(5)}`;
74
- console.log(` ${p.name.padEnd(12)} ${p.defaultModel.padEnd(28)} ${status.padEnd(22)} ${inputCost.padEnd(14)} ${outputCost}`);
75
- }
76
-
77
- console.log('');
78
- console.log(` Configure API keys with: ${picocolors.cyan('legacyver init')}`);
79
- console.log('');
80
- });
81
- }
82
-
83
- export { PROVIDERS };
16
+ module.exports = async function providersCommand() {
17
+ const { loadConfig } = require('../../utils/config');
18
+ const config = loadConfig({});
19
+
20
+ console.log(pc.bold('\nLegacyver — Supported LLM Providers\n'));
21
+ console.log(pc.bold('OpenRouter') + ' (https://openrouter.ai)');
22
+ console.log(' Unified gateway to 200+ models. Set OPENROUTER_API_KEY env variable.');
23
+ console.log(' Status: ' + (process.env.OPENROUTER_API_KEY ? pc.green('API key detected') : pc.yellow('No API key found')));
24
+ console.log('');
25
+ console.log(pc.bold('Ollama') + ' (https://ollama.ai)');
26
+ console.log(' Local offline LLM. No API key required. Run `ollama serve` first.');
27
+ console.log('');
28
+ console.log(pc.bold('Groq') + ' (https://groq.com)');
29
+ console.log(' Fastest free LLM inference. Set GROQ_API_KEY env variable.');
30
+ console.log(' Status: ' + (process.env.GROQ_API_KEY ? pc.green('API key detected') : pc.yellow('No API key found')));
31
+ console.log(' Get a free key at: https://console.groq.com/keys');
32
+ console.log('');
33
+ console.log(pc.bold('Google Gemini') + ' (https://ai.google.dev)');
34
+ console.log(' Free tier: 15 req/min, 1,500 req/day. Set GEMINI_API_KEY env variable.');
35
+ console.log(' Status: ' + (process.env.GEMINI_API_KEY ? pc.green('API key detected') : pc.yellow('No API key found')));
36
+ console.log(' Get a free key at: https://aistudio.google.com/apikey');
37
+ console.log('');
38
+
39
+ console.log(pc.bold('Recommended Models (via OpenRouter):'));
40
+ console.log('');
41
+ const header = ` ${'Model ID'.padEnd(48)} ${'Context'.padEnd(8)} ${'Input $/1M'.padEnd(12)} ${'Output $/1M'.padEnd(12)}`;
42
+ console.log(pc.dim(header));
43
+ console.log(pc.dim(' ' + '-'.repeat(84)));
44
+
45
+ for (const m of RECOMMENDED_MODELS) {
46
+ const badge = m.free ? pc.green(' [FREE]') : '';
47
+ const selected = m.id === config.model ? pc.cyan(' ◀ selected') : '';
48
+ const inputCostStr = m.free ? 'FREE' : `$${m.inputCost.toFixed(3)}`;
49
+ const outputCostStr = m.free ? 'FREE' : `$${m.outputCost.toFixed(3)}`;
50
+ console.log(
51
+ ` ${m.id.padEnd(48)} ${m.context.padEnd(8)} ${inputCostStr.padEnd(12)} ${outputCostStr.padEnd(12)}${badge}${selected}`
52
+ );
53
+ }
54
+
55
+ console.log('');
56
+ console.log(pc.dim('Fetch live model list from OpenRouter: https://openrouter.ai/api/v1/models'));
57
+ console.log('');
58
+ };
@@ -1,12 +1,9 @@
1
- /**
2
- * Version command — reads version from package.json and prints it.
3
- */
1
+ 'use strict';
4
2
 
5
- import { createRequire } from 'node:module';
3
+ const { readFileSync } = require('fs');
4
+ const { join } = require('path');
6
5
 
7
- const require = createRequire(import.meta.url);
8
-
9
- export function printVersion() {
10
- const pkg = require('../../../package.json');
11
- console.log(`legacyver v${pkg.version}`);
12
- }
6
+ module.exports = function versionCommand() {
7
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../../../package.json'), 'utf8'));
8
+ console.log(pkg.version);
9
+ };
package/src/cli/ui.js CHANGED
@@ -1,104 +1,85 @@
1
- /**
2
- * CLI UI helpers: spinners, progress bars, prompts, summary.
3
- */
1
+ 'use strict';
4
2
 
5
- import ora from 'ora';
6
- import cliProgress from 'cli-progress';
7
- import picocolors from 'picocolors';
8
- import { createInterface } from 'node:readline';
9
- import { isCIMode } from '../utils/logger.js';
3
+ const ora = require('ora');
4
+ const { SingleBar, Presets } = require('cli-progress');
5
+ const readline = require('readline');
6
+ const pc = require('picocolors');
10
7
 
11
- /**
12
- * Create a spinner (disabled in CI mode).
13
- * @param {string} text - Spinner text
14
- * @returns {object} Spinner-like object with start/stop/succeed/fail
15
- */
16
- export function createSpinner(text) {
17
- if (isCIMode()) {
8
+ const isCI = !process.stdout.isTTY;
9
+
10
+ function createSpinner(text) {
11
+ if (isCI) {
12
+ console.log(`[spinner] ${text}`);
18
13
  return {
19
- start() { console.log(`... ${text}`); return this; },
20
- stop() { return this; },
21
- succeed(msg) { console.log(`OK: ${msg || text}`); return this; },
22
- fail(msg) { console.error(`FAIL: ${msg || text}`); return this; },
23
- text,
14
+ start: (t) => t && console.log(`[spinner] ${t}`),
15
+ succeed: (t) => console.log(`[done] ${t || text}`),
16
+ fail: (t) => console.error(`[fail] ${t || text}`),
17
+ warn: (t) => console.warn(`[warn] ${t || text}`),
18
+ stop: () => {},
19
+ text: text,
24
20
  };
25
21
  }
26
22
  return ora({ text, spinner: 'dots' });
27
23
  }
28
24
 
29
- /**
30
- * Create a progress bar (plain log lines in CI mode).
31
- * @param {number} total - Total items
32
- * @returns {object} Progress bar with start/increment/stop methods
33
- */
34
- export function createProgressBar(total) {
35
- if (isCIMode()) {
25
+ function createProgressBar(total) {
26
+ if (isCI) {
36
27
  let current = 0;
37
28
  return {
38
- start() { console.log(`Processing 0/${total} files...`); },
39
- increment(amount = 1) {
40
- current += amount;
41
- if (current % 10 === 0 || current === total) {
42
- console.log(`Processing ${current}/${total} files...`);
43
- }
29
+ start: () => {},
30
+ increment: () => {
31
+ current++;
32
+ process.stdout.write(`Progress: ${current}/${total}\n`);
44
33
  },
45
- stop() { console.log(`Processed ${current}/${total} files.`); },
34
+ stop: () => {},
46
35
  };
47
36
  }
48
-
49
- const bar = new cliProgress.SingleBar({
50
- format: `${picocolors.cyan('{bar}')} {percentage}% | {value}/{total} files | ETA: {eta}s`,
51
- barCompleteChar: '\u2588',
52
- barIncompleteChar: '\u2591',
53
- hideCursor: true,
54
- });
55
-
37
+ const bar = new SingleBar(
38
+ {
39
+ format: `${pc.cyan('Analyzing')} [{bar}] {percentage}% | {value}/{total} files`,
40
+ clearOnComplete: false,
41
+ hideCursor: true,
42
+ },
43
+ Presets.shades_classic
44
+ );
56
45
  return {
57
- start() { bar.start(total, 0); },
58
- increment(amount = 1) { bar.increment(amount); },
59
- stop() { bar.stop(); },
46
+ start: () => bar.start(total, 0),
47
+ increment: () => bar.increment(),
48
+ stop: () => bar.stop(),
60
49
  };
61
50
  }
62
51
 
63
- /**
64
- * Prompt user for confirmation.
65
- * @param {string} message - Question to display
66
- * @returns {Promise<boolean>} true if user confirmed
67
- */
68
- export async function confirmPrompt(message) {
69
- if (isCIMode()) {
70
- return false; // CI mode requires --no-confirm
52
+ async function confirmPrompt(message) {
53
+ if (isCI) {
54
+ console.error(`[confirm] ${message} non-interactive mode, aborting. Use --no-confirm to skip.`);
55
+ process.exit(4);
71
56
  }
72
-
73
- const rl = createInterface({
74
- input: process.stdin,
75
- output: process.stdout,
76
- });
77
-
78
57
  return new Promise((resolve) => {
79
- rl.question(`${message} (y/N) `, (answer) => {
58
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
59
+ rl.question(`${pc.yellow('?')} ${message} [y/N] `, (answer) => {
80
60
  rl.close();
81
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
61
+ resolve(answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes');
82
62
  });
83
63
  });
84
64
  }
85
65
 
86
- /**
87
- * Print analysis summary.
88
- * @param {object} stats - Summary statistics
89
- */
90
- export function printSummary(stats) {
66
+ function printSummary(stats) {
91
67
  console.log('');
92
- console.log(picocolors.bold('─── Analysis Summary ───'));
93
- console.log(` Files analyzed: ${stats.filesAnalyzed || 0}`);
94
- console.log(` Files skipped: ${stats.filesSkipped || 0}`);
95
- console.log(` Files cached: ${stats.filesCached || 0}`);
96
- console.log(` Errors: ${stats.errors || 0}`);
97
- console.log(` Input tokens: ${stats.inputTokens || 0}`);
98
- console.log(` Output tokens: ${stats.outputTokens || 0}`);
99
- console.log(` Estimated cost: $${(stats.estimatedCost || 0).toFixed(4)}`);
100
- console.log(` Output: ${stats.outputDir || 'N/A'}`);
101
- console.log(` Duration: ${stats.duration || 'N/A'}`);
102
- console.log(picocolors.bold('────────────────────────'));
68
+ console.log(pc.bold('=== Legacyver Summary ==='));
69
+ console.log(` Files analyzed: ${pc.green(stats.filesAnalyzed)}`);
70
+ console.log(` Files cached: ${pc.cyan(stats.filesCached || 0)}`);
71
+ console.log(` Files skipped: ${pc.yellow(stats.filesSkipped || 0)}`);
72
+ console.log(` Tokens used: ${stats.tokensUsed !== undefined ? stats.tokensUsed : 'n/a'}`);
73
+ console.log(` Estimated cost: ${stats.estimatedCost !== undefined ? '$' + stats.estimatedCost.toFixed(4) : 'n/a'}`);
74
+ if (stats.qualityWarnings > 0) {
75
+ console.log(` Quality warnings: ${pc.yellow(stats.qualityWarnings)}`);
76
+ }
77
+ if (stats.errors && stats.errors.length > 0) {
78
+ console.log(` Errors: ${pc.red(stats.errors.length)}`);
79
+ stats.errors.forEach((e) => console.log(` - ${e}`));
80
+ }
81
+ console.log(` Output: ${stats.outputDir}`);
103
82
  console.log('');
104
83
  }
84
+
85
+ module.exports = { createSpinner, createProgressBar, confirmPrompt, printSummary };
@@ -1,57 +1,58 @@
1
- /**
2
- * Language extension map and default ignore patterns.
3
- */
1
+ 'use strict';
4
2
 
5
- export const LANGUAGE_EXTENSIONS = {
6
- javascript: ['.js', '.jsx', '.mjs'],
3
+ const LANGUAGE_EXTENSIONS = {
4
+ javascript: ['.js', '.jsx', '.mjs', '.cjs'],
7
5
  typescript: ['.ts', '.tsx'],
8
6
  python: ['.py'],
9
7
  java: ['.java'],
10
8
  go: ['.go'],
9
+ php: ['.php', '.blade.php'],
11
10
  };
12
11
 
13
- /**
14
- * Reverse map: extension -> language
15
- */
16
- export const EXTENSION_TO_LANGUAGE = {};
17
- for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
18
- for (const ext of exts) {
19
- EXTENSION_TO_LANGUAGE[ext] = lang;
20
- }
21
- }
12
+ const ALL_EXTENSIONS = Object.values(LANGUAGE_EXTENSIONS).flat();
22
13
 
23
- /**
24
- * All supported file extensions as glob patterns.
25
- */
26
- export const SUPPORTED_EXTENSIONS = Object.values(LANGUAGE_EXTENSIONS).flat();
27
-
28
- /**
29
- * Default ignore patterns (always applied).
30
- */
31
- export const DEFAULT_IGNORE_PATTERNS = [
14
+ const DEFAULT_IGNORE_PATTERNS = [
32
15
  '**/node_modules/**',
33
16
  '**/.git/**',
34
17
  '**/dist/**',
35
18
  '**/build/**',
36
- '**/.next/**',
37
- '**/.nuxt/**',
19
+ '**/vendor/**',
38
20
  '**/coverage/**',
39
21
  '**/__pycache__/**',
40
- '**/.pytest_cache/**',
41
- '**/venv/**',
42
- '**/.venv/**',
43
- '**/env/**',
44
- '**/.env/**',
45
- '**/vendor/**',
46
- '**/.idea/**',
47
- '**/.vscode/**',
48
- '**/*.min.js',
49
- '**/*.min.css',
50
- '**/*.map',
51
- '**/*.lock',
52
- '**/package-lock.json',
53
- '**/yarn.lock',
54
- '**/pnpm-lock.yaml',
22
+ '**/storage/**',
23
+ '**/bootstrap/cache/**',
55
24
  '**/.legacyver-cache/**',
56
25
  '**/legacyver-docs/**',
26
+ '**/test/**',
27
+ '**/tests/**',
28
+ '**/spec/**',
29
+ '**/openspec/**',
30
+ '**/*.test.js',
31
+ '**/*.test.ts',
32
+ '**/*.spec.js',
33
+ '**/*.spec.ts',
34
+ '**/*.test.py',
57
35
  ];
36
+
37
+ function detectLanguage(ext) {
38
+ for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
39
+ if (exts.includes(ext)) return lang;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function detectPrimaryLanguage(files) {
45
+ const counts = {};
46
+ for (const f of files) {
47
+ const lang = f.language;
48
+ if (lang) counts[lang] = (counts[lang] || 0) + 1;
49
+ }
50
+ let max = 0;
51
+ let primary = null;
52
+ for (const [lang, cnt] of Object.entries(counts)) {
53
+ if (cnt > max) { max = cnt; primary = lang; }
54
+ }
55
+ return primary;
56
+ }
57
+
58
+ module.exports = { LANGUAGE_EXTENSIONS, ALL_EXTENSIONS, DEFAULT_IGNORE_PATTERNS, detectLanguage, detectPrimaryLanguage };