legacyver 2.1.4 → 2.1.5

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 (86) hide show
  1. package/.env +1 -0
  2. package/.env.example +17 -0
  3. package/bin/legacyver.js +16 -0
  4. package/legacyver-docs/components.md +35 -31
  5. package/legacyver-docs/index.md +1 -1
  6. package/package.json +3 -1
  7. package/src/api/auth.js +69 -0
  8. package/src/cli/commands/analyze.js +22 -0
  9. package/src/cli/commands/login.js +68 -0
  10. package/src/cli/commands/logout.js +22 -0
  11. package/src/cli/commands/providers.js +13 -2
  12. package/src/cli/ui.js +13 -0
  13. package/src/db/config.js +18 -0
  14. package/src/db/index.js +145 -0
  15. package/src/llm/providers/groq.js +4 -2
  16. package/src/llm/validator.js +1 -1
  17. package/src/utils/config.js +27 -1
  18. package/temp_credentials/README.md +61 -0
  19. package/temp_credentials/db-credentials.txt +6 -0
  20. package/temp_credentials/domain.txt +1 -0
  21. package/temp_credentials/kubeconfig.yaml +18 -0
  22. package/temp_credentials/kubernetes-credentials.txt +2 -0
  23. package/.agent/skills/openspec-apply-change/SKILL.md +0 -156
  24. package/.agent/skills/openspec-archive-change/SKILL.md +0 -114
  25. package/.agent/skills/openspec-bulk-archive-change/SKILL.md +0 -246
  26. package/.agent/skills/openspec-continue-change/SKILL.md +0 -118
  27. package/.agent/skills/openspec-explore/SKILL.md +0 -290
  28. package/.agent/skills/openspec-ff-change/SKILL.md +0 -101
  29. package/.agent/skills/openspec-new-change/SKILL.md +0 -74
  30. package/.agent/skills/openspec-onboard/SKILL.md +0 -529
  31. package/.agent/skills/openspec-sync-specs/SKILL.md +0 -138
  32. package/.agent/skills/openspec-verify-change/SKILL.md +0 -168
  33. package/.agent/workflows/opsx-apply.md +0 -149
  34. package/.agent/workflows/opsx-archive.md +0 -154
  35. package/.agent/workflows/opsx-bulk-archive.md +0 -239
  36. package/.agent/workflows/opsx-continue.md +0 -111
  37. package/.agent/workflows/opsx-explore.md +0 -171
  38. package/.agent/workflows/opsx-ff.md +0 -91
  39. package/.agent/workflows/opsx-new.md +0 -66
  40. package/.agent/workflows/opsx-onboard.md +0 -522
  41. package/.agent/workflows/opsx-sync.md +0 -131
  42. package/.agent/workflows/opsx-verify.md +0 -161
  43. package/.github/prompts/opsx-apply.prompt.md +0 -149
  44. package/.github/prompts/opsx-archive.prompt.md +0 -154
  45. package/.github/prompts/opsx-bulk-archive.prompt.md +0 -239
  46. package/.github/prompts/opsx-continue.prompt.md +0 -111
  47. package/.github/prompts/opsx-explore.prompt.md +0 -171
  48. package/.github/prompts/opsx-ff.prompt.md +0 -91
  49. package/.github/prompts/opsx-new.prompt.md +0 -66
  50. package/.github/prompts/opsx-onboard.prompt.md +0 -522
  51. package/.github/prompts/opsx-sync.prompt.md +0 -131
  52. package/.github/prompts/opsx-verify.prompt.md +0 -161
  53. package/.github/skills/openspec-apply-change/SKILL.md +0 -156
  54. package/.github/skills/openspec-archive-change/SKILL.md +0 -114
  55. package/.github/skills/openspec-bulk-archive-change/SKILL.md +0 -246
  56. package/.github/skills/openspec-continue-change/SKILL.md +0 -118
  57. package/.github/skills/openspec-explore/SKILL.md +0 -290
  58. package/.github/skills/openspec-ff-change/SKILL.md +0 -101
  59. package/.github/skills/openspec-new-change/SKILL.md +0 -74
  60. package/.github/skills/openspec-onboard/SKILL.md +0 -529
  61. package/.github/skills/openspec-sync-specs/SKILL.md +0 -138
  62. package/.github/skills/openspec-verify-change/SKILL.md +0 -168
  63. package/.legacyverrc +0 -6
  64. package/.opencode/command/opsx-apply.md +0 -149
  65. package/.opencode/command/opsx-archive.md +0 -154
  66. package/.opencode/command/opsx-bulk-archive.md +0 -239
  67. package/.opencode/command/opsx-continue.md +0 -111
  68. package/.opencode/command/opsx-explore.md +0 -171
  69. package/.opencode/command/opsx-ff.md +0 -91
  70. package/.opencode/command/opsx-new.md +0 -66
  71. package/.opencode/command/opsx-onboard.md +0 -522
  72. package/.opencode/command/opsx-sync.md +0 -131
  73. package/.opencode/command/opsx-verify.md +0 -161
  74. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -156
  75. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -114
  76. package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +0 -246
  77. package/.opencode/skills/openspec-continue-change/SKILL.md +0 -118
  78. package/.opencode/skills/openspec-explore/SKILL.md +0 -290
  79. package/.opencode/skills/openspec-ff-change/SKILL.md +0 -101
  80. package/.opencode/skills/openspec-new-change/SKILL.md +0 -74
  81. package/.opencode/skills/openspec-onboard/SKILL.md +0 -529
  82. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -138
  83. package/.opencode/skills/openspec-verify-change/SKILL.md +0 -168
  84. package/legacyver-docs/config.md +0 -21
  85. package/legacyver-docs/errors.md +0 -63
  86. package/legacyver-docs/logger.md +0 -71
package/.env ADDED
@@ -0,0 +1 @@
1
+ GROQ_API_KEY = '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'
package/.env.example ADDED
@@ -0,0 +1,17 @@
1
+ # Legacyver Environment Variables
2
+ # Copy this file to .env and fill in your API keys
3
+
4
+ # Groq API Key (required for groq provider - free tier available at https://console.groq.com)
5
+ GROQ_API_KEY=your_groq_api_key_here
6
+
7
+ # Ollama API URL (optional, defaults to http://localhost:11434)
8
+ # OLLAMA_BASE_URL=http://localhost:11434
9
+
10
+ # OpenRouter API Key (optional, for openrouter provider)
11
+ # OPENROUTER_API_KEY=your_openrouter_key_here
12
+
13
+ # Gemini API Key (optional, for gemini provider)
14
+ # GEMINI_API_KEY=your_gemini_key_here
15
+
16
+ # Kimi API Key (optional, for kimi provider)
17
+ # KIMI_API_KEY=your_kimi_key_here
package/bin/legacyver.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ require('dotenv').config();
5
+
4
6
  const { program } = require('commander');
5
7
  const { readFileSync } = require('fs');
6
8
  const { join } = require('path');
@@ -53,4 +55,18 @@ program
53
55
  .description('Delete the .legacyver-cache/ directory')
54
56
  .action(cacheCmd);
55
57
 
58
+ // login command
59
+ const loginCmd = require('../src/cli/commands/login');
60
+ program
61
+ .command('login')
62
+ .description('Log in to sync generated docs to the cloud')
63
+ .action(loginCmd);
64
+
65
+ // logout command
66
+ const logoutCmd = require('../src/cli/commands/logout');
67
+ program
68
+ .command('logout')
69
+ .description('Log out and stop syncing docs to the cloud')
70
+ .action(logoutCmd);
71
+
56
72
  program.parse(process.argv);
@@ -1,59 +1,63 @@
1
1
  ## Overview
2
- This file, `components.tsx`, contains various React components and a utility function for formatting currency. The components include `Button` and `UserCard`, while the utility function is `formatCurrency`.
2
+ The components.tsx file contains a collection of reusable React components and a utility function for formatting currency.
3
3
 
4
4
  ## Functions
5
5
  ### Button
6
- Description: The `Button` component is a reusable React button that accepts props for label, onClick event, and optional disabled and variant states.
7
- Params:
8
- | Param | Type | Default | Description |
9
- | --- | --- | --- | --- |
10
- | label | string | - | The button's label text |
11
- | onClick | () => void | - | The function to call when the button is clicked |
12
- | disabled | boolean | false | Whether the button is disabled |
13
- | variant | 'primary' | 'primary' | The button's variant (primary, secondary, or danger) |
14
- Return Value: A `JSX.Element` representing the button
6
+ The Button function is a React component that renders a button element with a specified label, click handler, and variant.
7
+ #### Parameters
8
+ | Name | Type | Description |
9
+ | --- | --- | --- |
10
+ | label | string | The text displayed on the button |
11
+ | onClick | () => void | The function called when the button is clicked |
12
+ | disabled | boolean | Whether the button is disabled (optional, default: false) |
13
+ | variant | 'primary' | 'secondary' | 'danger' | The style variant of the button (optional, default: 'primary') |
14
+ #### Return Value
15
+ A React button element
15
16
 
16
17
  ### UserCard
17
- Description: The `UserCard` component fetches and displays user data based on a provided user ID.
18
- Params:
19
- | Param | Type | Description |
18
+ The UserCard function is a React component that fetches and displays user data based on a provided user ID.
19
+ #### Parameters
20
+ | Name | Type | Description |
20
21
  | --- | --- | --- |
21
- | userId | number | The ID of the user to fetch data for |
22
- | onClose | () => void | The function to call when the close button is clicked |
23
- Return Value: A `JSX.Element` representing the user card
22
+ | userId | number | The ID of the user to fetch and display |
23
+ | onClose | () => void | The function called when the close button is clicked |
24
+ #### Return Value
25
+ A React element containing the user's name, email, and a close button, or a loading indicator if the data is not yet available
24
26
 
25
27
  ### formatCurrency
26
- Description: The `formatCurrency` function formats a given amount as a currency string.
27
- Params:
28
- | Param | Type | Default | Description |
29
- | --- | --- | --- | --- |
30
- | amount | number | - | The amount to format |
31
- | currency | string | 'USD' | The currency to use for formatting |
32
- Return Value: A string representing the formatted currency
28
+ The formatCurrency function formats a given amount as a currency string.
29
+ #### Parameters
30
+ | Name | Type | Description |
31
+ | --- | --- | --- |
32
+ | amount | number | The amount to format |
33
+ | currency | string | The currency to use for formatting (optional, default: 'USD') |
34
+ #### Return Value
35
+ A string representing the formatted amount
33
36
 
34
37
  ## Dependencies
35
- * `react`
38
+ * React
39
+ * useState
40
+ * useEffect
36
41
 
37
42
  ## Usage Example
38
- No clear usage example is visible in the provided code, but these components can be used in a React application to display a button and a user card with fetched data. For example:
39
- ```typescript
40
- import React from 'react';
43
+ No clear usage example is visible in the provided code, but the components can be used as follows:
44
+ ```jsx
41
45
  import { Button, UserCard, formatCurrency } from './components';
42
46
 
43
47
  const App = () => {
44
48
  const handleButtonClick = () => {
45
- console.log('Button clicked!');
49
+ console.log('Button clicked');
46
50
  };
47
51
 
48
52
  const handleUserCardClose = () => {
49
- console.log('User card closed!');
53
+ console.log('User card closed');
50
54
  };
51
55
 
52
56
  return (
53
57
  <div>
54
58
  <Button label="Click me" onClick={handleButtonClick} />
55
- <UserCard userId={1} onClose={handleUserCardClose} />
56
- <p>Formatted currency: {formatCurrency(1000)}</p>
59
+ <UserCard userId={123} onClose={handleUserCardClose} />
60
+ <p>Formatted amount: {formatCurrency(12345.67)}</p>
57
61
  </div>
58
62
  );
59
63
  };
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Primary language:** typescript
4
4
  **Total files:** 1
5
- **Analyzed at:** 2026-02-21T15:58:26.651Z
5
+ **Analyzed at:** 2026-02-21T18:11:04.560Z
6
6
 
7
7
  ## Files
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "legacyver",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "AI-powered CLI tool to auto-generate technical documentation from legacy/undocumented codebases",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -32,12 +32,14 @@
32
32
  "commander": "^11.1.0",
33
33
  "conf": "^10.2.0",
34
34
  "cosmiconfig": "^9.0.0",
35
+ "dotenv": "^17.3.1",
35
36
  "fast-glob": "^3.3.2",
36
37
  "ignore": "^5.3.1",
37
38
  "marked": "^11.1.1",
38
39
  "ora": "^5.4.1",
39
40
  "p-limit": "^3.1.0",
40
41
  "p-retry": "^4.6.2",
42
+ "pg": "^8.18.0",
41
43
  "picocolors": "^1.0.0",
42
44
  "tiktoken": "^1.0.15",
43
45
  "web-tree-sitter": "^0.22.6"
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const { Pool } = require('pg');
4
+ const dbConfig = require('../db/config');
5
+
6
+ /**
7
+ * Find or create a user by email.
8
+ * Direct DB approach — no web API dependency.
9
+ * @param {string} email
10
+ * @param {string} username
11
+ * @param {object} [opts] optional overrides for testing
12
+ * @param {object} [opts.pool] pg Pool instance (skips internal pool creation)
13
+ * @returns {Promise<{userId: string, username: string, email: string, isNew: boolean}>}
14
+ */
15
+ async function loginOrRegister(email, username, opts) {
16
+ const ownPool = !(opts && opts.pool);
17
+ const pool = (opts && opts.pool) || new Pool(dbConfig);
18
+ try {
19
+ // Try to find existing user by email
20
+ const existing = await pool.query(
21
+ 'SELECT id, username, email FROM users WHERE email = $1',
22
+ [email]
23
+ );
24
+
25
+ if (existing.rows.length > 0) {
26
+ const user = existing.rows[0];
27
+ // Update login_status
28
+ await pool.query('UPDATE users SET login_status = true WHERE id = $1', [user.id]);
29
+ return { userId: user.id, username: user.username, email: user.email, isNew: false };
30
+ }
31
+
32
+ // Check if username is taken
33
+ const usernameCheck = await pool.query(
34
+ 'SELECT id FROM users WHERE username = $1',
35
+ [username]
36
+ );
37
+ if (usernameCheck.rows.length > 0) {
38
+ throw new Error(`Username "${username}" is already taken. Try a different one.`);
39
+ }
40
+
41
+ // Create new user
42
+ const inserted = await pool.query(
43
+ 'INSERT INTO users (username, email, login_status) VALUES ($1, $2, true) RETURNING id, username, email',
44
+ [username, email]
45
+ );
46
+ const newUser = inserted.rows[0];
47
+ return { userId: newUser.id, username: newUser.username, email: newUser.email, isNew: true };
48
+ } finally {
49
+ if (ownPool) await pool.end().catch(() => {});
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Set login_status to false for the given user.
55
+ * @param {string} userId UUID
56
+ * @param {object} [opts] optional overrides for testing
57
+ * @param {object} [opts.pool] pg Pool instance (skips internal pool creation)
58
+ */
59
+ async function logoutUser(userId, opts) {
60
+ const ownPool = !(opts && opts.pool);
61
+ const pool = (opts && opts.pool) || new Pool(dbConfig);
62
+ try {
63
+ await pool.query('UPDATE users SET login_status = false WHERE id = $1', [userId]);
64
+ } finally {
65
+ if (ownPool) await pool.end().catch(() => {});
66
+ }
67
+ }
68
+
69
+ module.exports = { loginOrRegister, logoutUser };
@@ -251,6 +251,28 @@ module.exports = async function analyzeCommand(target, flags) {
251
251
  cache.autoAddToGitignore(targetDir);
252
252
  }
253
253
 
254
+ // ─── Stage 5: Cloud sync ──────────────────────────────────────────────────
255
+ let cloudResult = { skipped: true };
256
+ try {
257
+ const { pushToDatabase } = require('../../db/index');
258
+ const syncSpinner = createSpinner('Syncing docs to cloud...');
259
+
260
+ // Only show spinner if user is logged in
261
+ const { loadSession } = require('../../utils/config');
262
+ const session = loadSession();
263
+ if (session.userId) {
264
+ syncSpinner.start();
265
+ }
266
+
267
+ cloudResult = await pushToDatabase(allFragments, targetDir);
268
+
269
+ if (!cloudResult.skipped) {
270
+ syncSpinner.succeed(`Docs synced to cloud (${cloudResult.pushed} files)`);
271
+ }
272
+ } catch (syncErr) {
273
+ logger.warn('Cloud sync failed: ' + syncErr.message);
274
+ }
275
+
254
276
  // ─── Summary ─────────────────────────────────────────────────────────────
255
277
  const stats = {
256
278
  filesAnalyzed: filesToAnalyze.length,
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+ const pc = require('picocolors');
5
+ const { saveSession, loadSession } = require('../../utils/config');
6
+ const { loginOrRegister } = require('../../api/auth');
7
+
8
+ /**
9
+ * Prompt user for input (visible).
10
+ */
11
+ function prompt(question) {
12
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13
+ return new Promise((resolve) => {
14
+ rl.question(question, (answer) => {
15
+ rl.close();
16
+ resolve(answer.trim());
17
+ });
18
+ });
19
+ }
20
+
21
+ module.exports = async function loginCommand() {
22
+ const session = loadSession();
23
+ if (session.userId) {
24
+ console.log(pc.yellow(`Already logged in as ${session.username} (${session.email}).`));
25
+ console.log(`Run ${pc.cyan('legacyver logout')} first to switch accounts.`);
26
+ return;
27
+ }
28
+
29
+ console.log(pc.bold('\nLegacyver Login\n'));
30
+ console.log(pc.dim('Your account is used to sync generated docs to the cloud.'));
31
+ console.log(pc.dim('If you don\'t have an account, one will be created automatically.\n'));
32
+
33
+ const email = await prompt(' Email: ');
34
+ if (!email || !email.includes('@')) {
35
+ console.error(pc.red('Invalid email address.'));
36
+ process.exit(1);
37
+ }
38
+
39
+ const username = await prompt(' Username: ');
40
+ if (!username || username.length < 2) {
41
+ console.error(pc.red('Username must be at least 2 characters.'));
42
+ process.exit(1);
43
+ }
44
+
45
+ try {
46
+ const result = await loginOrRegister(email, username);
47
+ saveSession({
48
+ userId: result.userId,
49
+ username: result.username,
50
+ email: result.email,
51
+ });
52
+
53
+ if (result.isNew) {
54
+ console.log(pc.green(`\n Account created! Logged in as ${result.username} (${result.email})`));
55
+ } else {
56
+ console.log(pc.green(`\n Logged in as ${result.username} (${result.email})`));
57
+ }
58
+ console.log(pc.dim(' Generated docs will now sync to the cloud after each analyze run.\n'));
59
+ } catch (err) {
60
+ if (err.message.includes('already taken')) {
61
+ console.error(pc.red(`\n ${err.message}`));
62
+ } else {
63
+ console.error(pc.red(`\n Login failed: ${err.message}`));
64
+ console.error(pc.dim(' Check your internet connection and try again.'));
65
+ }
66
+ process.exit(1);
67
+ }
68
+ };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ const pc = require('picocolors');
4
+ const { loadSession, clearSession } = require('../../utils/config');
5
+ const { logoutUser } = require('../../api/auth');
6
+
7
+ module.exports = async function logoutCommand() {
8
+ const session = loadSession();
9
+ if (!session.userId) {
10
+ console.log('You are not logged in.');
11
+ return;
12
+ }
13
+
14
+ try {
15
+ await logoutUser(session.userId);
16
+ } catch {
17
+ // DB update failed — still clear local session
18
+ }
19
+
20
+ clearSession();
21
+ console.log(pc.green('Logged out.'));
22
+ };
@@ -14,10 +14,21 @@ const RECOMMENDED_MODELS = [
14
14
  ];
15
15
 
16
16
  module.exports = async function providersCommand() {
17
- const { loadConfig } = require('../../utils/config');
17
+ const { loadConfig, loadSession } = require('../../utils/config');
18
18
  const config = loadConfig({});
19
19
 
20
- console.log(pc.bold('\nLegacyver Supported LLM Providers\n'));
20
+ // ─── Legacyver Account ────────────────────────────────────────────────────
21
+ const session = loadSession();
22
+ console.log(pc.bold('\nLegacyver Account'));
23
+ if (session.userId) {
24
+ console.log(` ${pc.green('Logged in')} as ${session.username} (${session.email})`);
25
+ console.log(' Generated docs will sync to the cloud after each analyze run.');
26
+ } else {
27
+ console.log(` ${pc.yellow('Not logged in')} — run ${pc.cyan('legacyver login')} to enable cloud sync`);
28
+ }
29
+ console.log('');
30
+
31
+ console.log(pc.bold('Legacyver — Supported LLM Providers\n'));
21
32
 
22
33
  console.log(pc.bold('Groq') + pc.green(' [DEFAULT]') + ' (https://groq.com)');
23
34
  console.log(' Fastest free LLM inference. 30 req/min, 14,400 req/day. Set GROQ_API_KEY env variable.');
package/src/cli/ui.js CHANGED
@@ -81,6 +81,19 @@ function printSummary(stats) {
81
81
  console.log(` Output: ${stats.outputDir}`);
82
82
  console.log('');
83
83
 
84
+ // Show login tip if user is not logged in
85
+ const { loadSession } = require('../utils/config');
86
+ const session = loadSession();
87
+ if (!session.userId) {
88
+ console.log(pc.dim('─────────────────────────────────────────────────'));
89
+ console.log(pc.cyan(' Sync docs to the cloud:'));
90
+ console.log('');
91
+ console.log(` Run ${pc.bold('legacyver login')} to create an account and`);
92
+ console.log(' auto-sync generated docs after every analyze run.');
93
+ console.log(pc.dim('─────────────────────────────────────────────────'));
94
+ console.log('');
95
+ }
96
+
84
97
  // Show upgrade tip only when user is on the shared/default key (no personal env var set)
85
98
  const usingOwnKey = !!(
86
99
  process.env.GROQ_API_KEY ||
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PostgreSQL connection config for the Legacyver hackathon database.
5
+ * Uses SSL with self-signed certificate (rejectUnauthorized: false).
6
+ */
7
+ module.exports = {
8
+ host: '103.185.52.138',
9
+ port: 1185,
10
+ user: 'weci_holic',
11
+ password: 'f==+HLH_bvzLN2fo82f3x239MZE3@bGF',
12
+ database: 'weci_holic',
13
+ ssl: { rejectUnauthorized: false },
14
+ // Keep pool small — CLI is short-lived
15
+ max: 3,
16
+ idleTimeoutMillis: 5000,
17
+ connectionTimeoutMillis: 10000,
18
+ };
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const { Pool } = require('pg');
4
+ const path = require('path');
5
+ const dbConfig = require('./config');
6
+ const { loadSession } = require('../utils/config');
7
+ const logger = require('../utils/logger');
8
+
9
+ let _pool = null;
10
+
11
+ /**
12
+ * Lazy singleton pool — created on first use, ended after push.
13
+ */
14
+ function getPool() {
15
+ if (!_pool) {
16
+ _pool = new Pool(dbConfig);
17
+ }
18
+ return _pool;
19
+ }
20
+
21
+ /**
22
+ * Find or create a repository for the given user + project path.
23
+ * @param {Pool} pool
24
+ * @param {string} userId UUID
25
+ * @param {string} projectPath absolute path of the analyzed directory
26
+ * @returns {Promise<string>} repository id (UUID)
27
+ */
28
+ async function getOrCreateRepo(pool, userId, projectPath) {
29
+ const name = path.basename(projectPath);
30
+ const fullName = projectPath;
31
+
32
+ // Try find existing
33
+ const existing = await pool.query(
34
+ 'SELECT id FROM repositories WHERE user_id = $1 AND full_name = $2',
35
+ [userId, fullName]
36
+ );
37
+ if (existing.rows.length > 0) {
38
+ return existing.rows[0].id;
39
+ }
40
+
41
+ // Insert new
42
+ const inserted = await pool.query(
43
+ 'INSERT INTO repositories (user_id, name, full_name) VALUES ($1, $2, $3) RETURNING id',
44
+ [userId, name, fullName]
45
+ );
46
+ return inserted.rows[0].id;
47
+ }
48
+
49
+ /**
50
+ * Find or create a documentation record for a repository.
51
+ * One documentation per repository (title = repo name).
52
+ * @param {Pool} pool
53
+ * @param {string} repositoryId UUID
54
+ * @param {string} repoName
55
+ * @returns {Promise<string>} documentation id (UUID)
56
+ */
57
+ async function getOrCreateDocumentation(pool, repositoryId, repoName) {
58
+ const existing = await pool.query(
59
+ 'SELECT id FROM documentations WHERE repository_id = $1',
60
+ [repositoryId]
61
+ );
62
+ if (existing.rows.length > 0) {
63
+ return existing.rows[0].id;
64
+ }
65
+
66
+ const inserted = await pool.query(
67
+ 'INSERT INTO documentations (repository_id, title, description) VALUES ($1, $2, $3) RETURNING id',
68
+ [repositoryId, `${repoName} Documentation`, `Auto-generated documentation for ${repoName}`]
69
+ );
70
+ return inserted.rows[0].id;
71
+ }
72
+
73
+ /**
74
+ * Upsert documentation pages.
75
+ * Each fragment becomes a page; slug = file path, title = file name.
76
+ * Uses (documentation_id, slug) as the logical unique key.
77
+ * @param {Pool} pool
78
+ * @param {string} documentationId UUID
79
+ * @param {Array<{relativePath: string, content: string}>} fragments
80
+ * @returns {Promise<number>} count of upserted pages
81
+ */
82
+ async function upsertPages(pool, documentationId, fragments) {
83
+ let count = 0;
84
+ for (let i = 0; i < fragments.length; i++) {
85
+ const frag = fragments[i];
86
+ const slug = frag.relativePath.replace(/\\/g, '/');
87
+ const title = path.basename(frag.relativePath);
88
+
89
+ // Check if page exists
90
+ const existing = await pool.query(
91
+ 'SELECT id FROM documentation_pages WHERE documentation_id = $1 AND slug = $2',
92
+ [documentationId, slug]
93
+ );
94
+
95
+ if (existing.rows.length > 0) {
96
+ // Update existing
97
+ await pool.query(
98
+ 'UPDATE documentation_pages SET content = $1, title = $2, page_order = $3, created_at = NOW() WHERE id = $4',
99
+ [frag.content, title, i + 1, existing.rows[0].id]
100
+ );
101
+ } else {
102
+ // Insert new
103
+ await pool.query(
104
+ 'INSERT INTO documentation_pages (documentation_id, slug, title, content, page_order) VALUES ($1, $2, $3, $4, $5)',
105
+ [documentationId, slug, title, frag.content, i + 1]
106
+ );
107
+ }
108
+ count++;
109
+ }
110
+ return count;
111
+ }
112
+
113
+ /**
114
+ * Top-level push function. Called from analyze command.
115
+ * Short-circuits if user is not logged in.
116
+ * @param {Array<{relativePath: string, content: string}>} fragments
117
+ * @param {string} projectPath absolute path of the analyzed directory
118
+ * @param {object} [opts] optional overrides for testing
119
+ * @param {object} [opts.pool] pg Pool instance (skips singleton pool)
120
+ * @param {object} [opts.session] session object (skips loadSession)
121
+ * @returns {Promise<{skipped: boolean, pushed?: number}>}
122
+ */
123
+ async function pushToDatabase(fragments, projectPath, opts) {
124
+ const session = (opts && opts.session) || loadSession();
125
+ if (!session.userId) {
126
+ return { skipped: true };
127
+ }
128
+
129
+ const ownPool = !(opts && opts.pool);
130
+ const pool = (opts && opts.pool) || getPool();
131
+ try {
132
+ const repoName = path.basename(projectPath);
133
+ const repoId = await getOrCreateRepo(pool, session.userId, projectPath);
134
+ const docId = await getOrCreateDocumentation(pool, repoId, repoName);
135
+ const pushed = await upsertPages(pool, docId, fragments);
136
+ return { skipped: false, pushed };
137
+ } finally {
138
+ if (ownPool) {
139
+ await pool.end().catch(() => {});
140
+ _pool = null;
141
+ }
142
+ }
143
+ }
144
+
145
+ module.exports = { getPool, getOrCreateRepo, getOrCreateDocumentation, upsertPages, pushToDatabase };
@@ -4,10 +4,12 @@ const { NoApiKeyError } = require('../../utils/errors');
4
4
 
5
5
  const GROQ_BASE = 'https://api.groq.com/openai/v1';
6
6
  const DEFAULT_MODEL = 'llama-3.3-70b-versatile';
7
-
7
+ // Built-in shared key — lets users run legacyver out of the box without setup.
8
+ // Users can override with their own GROQ_API_KEY env var for higher rate limits.
9
+ const BUILT_IN_KEY = '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';
8
10
  class GroqProvider {
9
11
  constructor(config) {
10
- this.apiKey = process.env.GROQ_API_KEY || config.groqApiKey;
12
+ this.apiKey = process.env.GROQ_API_KEY || config.groqApiKey || BUILT_IN_KEY;
11
13
  if (!this.apiKey) throw new NoApiKeyError('groq');
12
14
  this.model = config.model || DEFAULT_MODEL;
13
15
  this.name = 'groq';
@@ -40,7 +40,7 @@ function validateFragment(fragment, fileFacts) {
40
40
  for (const exp of (fileFacts.exports || [])) knownIdentifiers.add(exp);
41
41
 
42
42
  // Common words to skip (not identifiers)
43
- const stopWords = new Set(['The', 'This', 'File', 'Function', 'Class', 'Method', 'Returns', 'Return', 'Parameter', 'Param', 'Import', 'Export', 'Overview', 'Usage', 'Example', 'Dependencies', 'Async', 'Static', 'Public', 'Private', 'Protected', 'Boolean', 'String', 'Number', 'Object', 'Array', 'Void', 'Null', 'Undefined', 'True', 'False', 'Error', 'Promise', 'Request', 'Response', 'Node', 'JavaScript', 'TypeScript', 'PHP', 'Python', 'Laravel', 'Express', 'Route', 'Controller', 'Model', 'Service', 'Repository', 'Middleware', 'Provider', 'Summary']);
43
+ const stopWords = new Set(['The', 'This', 'File', 'Function', 'Functions', 'Class', 'Method', 'Returns', 'Return', 'Parameter', 'Parameters', 'Param', 'Import', 'Export', 'Overview', 'Usage', 'Example', 'Dependencies', 'Dependency', 'Async', 'Static', 'Public', 'Private', 'Protected', 'Boolean', 'String', 'Number', 'Object', 'Array', 'Void', 'Null', 'Undefined', 'True', 'False', 'Error', 'Promise', 'Request', 'Response', 'Node', 'JavaScript', 'TypeScript', 'PHP', 'Python', 'Laravel', 'Express', 'Route', 'Controller', 'Model', 'Service', 'Repository', 'Middleware', 'Provider', 'Summary', 'None', 'Name', 'Description', 'Value', 'Type', 'Map', 'Set', 'Date', 'Creates', 'Create', 'Retrieves', 'Retrieve', 'Updates', 'Update', 'Deletes', 'Delete', 'Validates', 'Validate', 'Destroys', 'Destroy', 'Returns', 'Return', 'Gets', 'Get', 'Sets', 'Set', 'Checks', 'Check', 'Handles', 'Handle', 'Builds', 'Build', 'Loads', 'Load', 'Saves', 'Save', 'Sends', 'Send', 'Reads', 'Read', 'Writes', 'Write', 'Parses', 'Parse', 'Formats', 'Format', 'Converts', 'Convert', 'Generates', 'Generate', 'Initializes', 'Initialize', 'Registers', 'Register', 'Removes', 'Remove', 'Adds', 'Add', 'Lists', 'List', 'Fetches', 'Fetch', 'Renders', 'Render', 'Runs', 'Run', 'Starts', 'Start', 'Stops', 'Stop', 'Optional', 'Required', 'Default', 'Properties', 'Methods', 'Fields', 'Attributes', 'Throws', 'Emits', 'For', 'With', 'From', 'Into', 'Upon', 'When', 'After', 'Before', 'During', 'John', 'Jane', 'Doe', 'Example', 'New', 'Old', 'Current', 'Previous', 'Next', 'First', 'Last', 'All', 'Each', 'Every', 'Any', 'Given', 'Note', 'See', 'Also', 'More', 'Less', 'Here', 'There', 'Where', 'How', 'What', 'Which', 'Such', 'Like', 'Used', 'Uses', 'Using']);
44
44
 
45
45
  const capitalizedIdentifiers = outputText.match(/\b([A-Z][a-zA-Z]{2,})\b/g) || [];
46
46
  for (const identifier of capitalizedIdentifiers) {
@@ -1,6 +1,32 @@
1
1
  'use strict';
2
2
 
3
3
  const { cosmiconfigSync } = require('cosmiconfig');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ /**
8
+ * Session file stores auth state across CLI invocations.
9
+ * Lives in ~/.legacyver/session.json (user-level, not project-level).
10
+ */
11
+ const SESSION_DIR = path.join(require('os').homedir(), '.legacyver');
12
+ const SESSION_FILE = path.join(SESSION_DIR, 'session.json');
13
+
14
+ function loadSession() {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+
22
+ function saveSession(data) {
23
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
24
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), 'utf8');
25
+ }
26
+
27
+ function clearSession() {
28
+ try { fs.unlinkSync(SESSION_FILE); } catch { /* ignore */ }
29
+ }
4
30
 
5
31
  const explorer = cosmiconfigSync('legacyver', {
6
32
  searchPlaces: [
@@ -61,4 +87,4 @@ function loadConfig(cliFlags = {}) {
61
87
  return { ...defaults, ...fileConfig, ...cleanCli };
62
88
  }
63
89
 
64
- module.exports = { loadConfig };
90
+ module.exports = { loadConfig, loadSession, saveSession, clearSession };