legacyver 2.1.4 → 2.1.6

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 (79) hide show
  1. package/bin/legacyver.js +16 -0
  2. package/legacyver-docs/components.md +35 -31
  3. package/legacyver-docs/index.md +1 -1
  4. package/package.json +3 -1
  5. package/src/api/auth.js +69 -0
  6. package/src/cli/commands/analyze.js +22 -0
  7. package/src/cli/commands/login.js +68 -0
  8. package/src/cli/commands/logout.js +22 -0
  9. package/src/cli/commands/providers.js +13 -2
  10. package/src/cli/ui.js +13 -0
  11. package/src/db/config.js +18 -0
  12. package/src/db/index.js +145 -0
  13. package/src/llm/providers/groq.js +4 -2
  14. package/src/llm/validator.js +1 -1
  15. package/src/utils/config.js +27 -1
  16. package/.agent/skills/openspec-apply-change/SKILL.md +0 -156
  17. package/.agent/skills/openspec-archive-change/SKILL.md +0 -114
  18. package/.agent/skills/openspec-bulk-archive-change/SKILL.md +0 -246
  19. package/.agent/skills/openspec-continue-change/SKILL.md +0 -118
  20. package/.agent/skills/openspec-explore/SKILL.md +0 -290
  21. package/.agent/skills/openspec-ff-change/SKILL.md +0 -101
  22. package/.agent/skills/openspec-new-change/SKILL.md +0 -74
  23. package/.agent/skills/openspec-onboard/SKILL.md +0 -529
  24. package/.agent/skills/openspec-sync-specs/SKILL.md +0 -138
  25. package/.agent/skills/openspec-verify-change/SKILL.md +0 -168
  26. package/.agent/workflows/opsx-apply.md +0 -149
  27. package/.agent/workflows/opsx-archive.md +0 -154
  28. package/.agent/workflows/opsx-bulk-archive.md +0 -239
  29. package/.agent/workflows/opsx-continue.md +0 -111
  30. package/.agent/workflows/opsx-explore.md +0 -171
  31. package/.agent/workflows/opsx-ff.md +0 -91
  32. package/.agent/workflows/opsx-new.md +0 -66
  33. package/.agent/workflows/opsx-onboard.md +0 -522
  34. package/.agent/workflows/opsx-sync.md +0 -131
  35. package/.agent/workflows/opsx-verify.md +0 -161
  36. package/.github/prompts/opsx-apply.prompt.md +0 -149
  37. package/.github/prompts/opsx-archive.prompt.md +0 -154
  38. package/.github/prompts/opsx-bulk-archive.prompt.md +0 -239
  39. package/.github/prompts/opsx-continue.prompt.md +0 -111
  40. package/.github/prompts/opsx-explore.prompt.md +0 -171
  41. package/.github/prompts/opsx-ff.prompt.md +0 -91
  42. package/.github/prompts/opsx-new.prompt.md +0 -66
  43. package/.github/prompts/opsx-onboard.prompt.md +0 -522
  44. package/.github/prompts/opsx-sync.prompt.md +0 -131
  45. package/.github/prompts/opsx-verify.prompt.md +0 -161
  46. package/.github/skills/openspec-apply-change/SKILL.md +0 -156
  47. package/.github/skills/openspec-archive-change/SKILL.md +0 -114
  48. package/.github/skills/openspec-bulk-archive-change/SKILL.md +0 -246
  49. package/.github/skills/openspec-continue-change/SKILL.md +0 -118
  50. package/.github/skills/openspec-explore/SKILL.md +0 -290
  51. package/.github/skills/openspec-ff-change/SKILL.md +0 -101
  52. package/.github/skills/openspec-new-change/SKILL.md +0 -74
  53. package/.github/skills/openspec-onboard/SKILL.md +0 -529
  54. package/.github/skills/openspec-sync-specs/SKILL.md +0 -138
  55. package/.github/skills/openspec-verify-change/SKILL.md +0 -168
  56. package/.legacyverrc +0 -6
  57. package/.opencode/command/opsx-apply.md +0 -149
  58. package/.opencode/command/opsx-archive.md +0 -154
  59. package/.opencode/command/opsx-bulk-archive.md +0 -239
  60. package/.opencode/command/opsx-continue.md +0 -111
  61. package/.opencode/command/opsx-explore.md +0 -171
  62. package/.opencode/command/opsx-ff.md +0 -91
  63. package/.opencode/command/opsx-new.md +0 -66
  64. package/.opencode/command/opsx-onboard.md +0 -522
  65. package/.opencode/command/opsx-sync.md +0 -131
  66. package/.opencode/command/opsx-verify.md +0 -161
  67. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -156
  68. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -114
  69. package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +0 -246
  70. package/.opencode/skills/openspec-continue-change/SKILL.md +0 -118
  71. package/.opencode/skills/openspec-explore/SKILL.md +0 -290
  72. package/.opencode/skills/openspec-ff-change/SKILL.md +0 -101
  73. package/.opencode/skills/openspec-new-change/SKILL.md +0 -74
  74. package/.opencode/skills/openspec-onboard/SKILL.md +0 -529
  75. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -138
  76. package/.opencode/skills/openspec-verify-change/SKILL.md +0 -168
  77. package/legacyver-docs/config.md +0 -21
  78. package/legacyver-docs/errors.md +0 -63
  79. package/legacyver-docs/logger.md +0 -71
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.6",
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 };