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.
- package/bin/legacyver.js +16 -0
- package/legacyver-docs/components.md +35 -31
- package/legacyver-docs/index.md +1 -1
- package/package.json +3 -1
- package/src/api/auth.js +69 -0
- package/src/cli/commands/analyze.js +22 -0
- package/src/cli/commands/login.js +68 -0
- package/src/cli/commands/logout.js +22 -0
- package/src/cli/commands/providers.js +13 -2
- package/src/cli/ui.js +13 -0
- package/src/db/config.js +18 -0
- package/src/db/index.js +145 -0
- package/src/llm/providers/groq.js +4 -2
- package/src/llm/validator.js +1 -1
- package/src/utils/config.js +27 -1
- package/.agent/skills/openspec-apply-change/SKILL.md +0 -156
- package/.agent/skills/openspec-archive-change/SKILL.md +0 -114
- package/.agent/skills/openspec-bulk-archive-change/SKILL.md +0 -246
- package/.agent/skills/openspec-continue-change/SKILL.md +0 -118
- package/.agent/skills/openspec-explore/SKILL.md +0 -290
- package/.agent/skills/openspec-ff-change/SKILL.md +0 -101
- package/.agent/skills/openspec-new-change/SKILL.md +0 -74
- package/.agent/skills/openspec-onboard/SKILL.md +0 -529
- package/.agent/skills/openspec-sync-specs/SKILL.md +0 -138
- package/.agent/skills/openspec-verify-change/SKILL.md +0 -168
- package/.agent/workflows/opsx-apply.md +0 -149
- package/.agent/workflows/opsx-archive.md +0 -154
- package/.agent/workflows/opsx-bulk-archive.md +0 -239
- package/.agent/workflows/opsx-continue.md +0 -111
- package/.agent/workflows/opsx-explore.md +0 -171
- package/.agent/workflows/opsx-ff.md +0 -91
- package/.agent/workflows/opsx-new.md +0 -66
- package/.agent/workflows/opsx-onboard.md +0 -522
- package/.agent/workflows/opsx-sync.md +0 -131
- package/.agent/workflows/opsx-verify.md +0 -161
- package/.github/prompts/opsx-apply.prompt.md +0 -149
- package/.github/prompts/opsx-archive.prompt.md +0 -154
- package/.github/prompts/opsx-bulk-archive.prompt.md +0 -239
- package/.github/prompts/opsx-continue.prompt.md +0 -111
- package/.github/prompts/opsx-explore.prompt.md +0 -171
- package/.github/prompts/opsx-ff.prompt.md +0 -91
- package/.github/prompts/opsx-new.prompt.md +0 -66
- package/.github/prompts/opsx-onboard.prompt.md +0 -522
- package/.github/prompts/opsx-sync.prompt.md +0 -131
- package/.github/prompts/opsx-verify.prompt.md +0 -161
- package/.github/skills/openspec-apply-change/SKILL.md +0 -156
- package/.github/skills/openspec-archive-change/SKILL.md +0 -114
- package/.github/skills/openspec-bulk-archive-change/SKILL.md +0 -246
- package/.github/skills/openspec-continue-change/SKILL.md +0 -118
- package/.github/skills/openspec-explore/SKILL.md +0 -290
- package/.github/skills/openspec-ff-change/SKILL.md +0 -101
- package/.github/skills/openspec-new-change/SKILL.md +0 -74
- package/.github/skills/openspec-onboard/SKILL.md +0 -529
- package/.github/skills/openspec-sync-specs/SKILL.md +0 -138
- package/.github/skills/openspec-verify-change/SKILL.md +0 -168
- package/.legacyverrc +0 -6
- package/.opencode/command/opsx-apply.md +0 -149
- package/.opencode/command/opsx-archive.md +0 -154
- package/.opencode/command/opsx-bulk-archive.md +0 -239
- package/.opencode/command/opsx-continue.md +0 -111
- package/.opencode/command/opsx-explore.md +0 -171
- package/.opencode/command/opsx-ff.md +0 -91
- package/.opencode/command/opsx-new.md +0 -66
- package/.opencode/command/opsx-onboard.md +0 -522
- package/.opencode/command/opsx-sync.md +0 -131
- package/.opencode/command/opsx-verify.md +0 -161
- package/.opencode/skills/openspec-apply-change/SKILL.md +0 -156
- package/.opencode/skills/openspec-archive-change/SKILL.md +0 -114
- package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +0 -246
- package/.opencode/skills/openspec-continue-change/SKILL.md +0 -118
- package/.opencode/skills/openspec-explore/SKILL.md +0 -290
- package/.opencode/skills/openspec-ff-change/SKILL.md +0 -101
- package/.opencode/skills/openspec-new-change/SKILL.md +0 -74
- package/.opencode/skills/openspec-onboard/SKILL.md +0 -529
- package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -138
- package/.opencode/skills/openspec-verify-change/SKILL.md +0 -168
- package/legacyver-docs/config.md +0 -21
- package/legacyver-docs/errors.md +0 -63
- 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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
|
9
|
-
| --- | --- | --- |
|
|
10
|
-
| label | string |
|
|
11
|
-
| onClick | () => void |
|
|
12
|
-
| disabled | boolean |
|
|
13
|
-
| variant | 'primary' | '
|
|
14
|
-
Return Value
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
|
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
|
|
22
|
-
| onClose | () => void | The function
|
|
23
|
-
Return Value
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
| --- | --- | --- |
|
|
30
|
-
| amount | number |
|
|
31
|
-
| currency | string |
|
|
32
|
-
Return Value
|
|
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
|
-
*
|
|
38
|
+
* React
|
|
39
|
+
* useState
|
|
40
|
+
* useEffect
|
|
36
41
|
|
|
37
42
|
## Usage Example
|
|
38
|
-
No clear usage example is visible in the provided code, but
|
|
39
|
-
```
|
|
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={
|
|
56
|
-
<p>Formatted
|
|
59
|
+
<UserCard userId={123} onClose={handleUserCardClose} />
|
|
60
|
+
<p>Formatted amount: {formatCurrency(12345.67)}</p>
|
|
57
61
|
</div>
|
|
58
62
|
);
|
|
59
63
|
};
|
package/legacyver-docs/index.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "legacyver",
|
|
3
|
-
"version": "2.1.
|
|
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"
|
package/src/api/auth.js
ADDED
|
@@ -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
|
-
|
|
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 ||
|
package/src/db/config.js
ADDED
|
@@ -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
|
+
};
|
package/src/db/index.js
ADDED
|
@@ -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';
|
package/src/llm/validator.js
CHANGED
|
@@ -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) {
|
package/src/utils/config.js
CHANGED
|
@@ -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 };
|