legacyver 2.1.6 → 2.1.8
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/README.md +52 -155
- package/bin/legacyver.js +8 -0
- package/legacyver-docs/components.md +23 -24
- package/legacyver-docs/index.md +1 -1
- package/nul +1 -0
- package/package.json +1 -1
- package/src/api/auth.js +39 -39
- package/src/cli/commands/analyze.js +1 -1
- package/src/cli/commands/login.js +94 -45
- package/src/cli/commands/logout.js +3 -3
- package/src/cli/commands/providers.js +1 -1
- package/src/cli/commands/push.js +99 -0
- package/src/cli/ui.js +1 -1
- package/src/db/index.js +23 -11
package/README.md
CHANGED
|
@@ -1,199 +1,96 @@
|
|
|
1
|
-
# Legacyver
|
|
1
|
+
# 🚀 Legacyver
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npx legacyver analyze ./src
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
No documentation written beforehand. No configuration required. One command.
|
|
3
|
+
**AI-powered CLI tool** untuk generate dokumentasi teknis secara otomatis dari codebase yang sudah ada (legacy) atau tidak memiliki dokumentasi. Menggunakan parsing AST yang mendalam dikombinasikan dengan LLM (Groq, Gemini, Ollama, dll.) untuk menjelaskan struktur, logika, dan pola kode kamu.
|
|
10
4
|
|
|
11
5
|
---
|
|
12
6
|
|
|
13
|
-
##
|
|
7
|
+
## ⚡ Panduan Cepat (Step-by-Step)
|
|
14
8
|
|
|
9
|
+
Ikuti urutan ini untuk membuat dokumentasi kamu siap dalam hitungan menit:
|
|
10
|
+
|
|
11
|
+
### 1. Instalasi
|
|
12
|
+
Instal package secara global melalui npm:
|
|
15
13
|
```bash
|
|
16
14
|
npm install -g legacyver
|
|
17
15
|
```
|
|
16
|
+
*Atau gunakan `npx legacyver` jika kamu tidak ingin menginstalnya secara global.*
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
### 2. Login (Cloud Sync)
|
|
19
|
+
Masuk ke akun Legacyver kamu untuk mengaktifkan sinkronisasi cloud dan menyimpan dokumentasi di dashboard:
|
|
21
20
|
```bash
|
|
22
|
-
|
|
21
|
+
legacyver login
|
|
23
22
|
```
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Quick Start
|
|
28
|
-
|
|
29
|
-
**1. Initialize (saves your API key):**
|
|
30
|
-
|
|
24
|
+
### 3. Inisialisasi Konfigurasi
|
|
25
|
+
Jalankan wizard setup untuk menyimpan API key (Groq, Gemini, dll.) dan membuat file konfigurasi `.legacyverrc`:
|
|
31
26
|
```bash
|
|
32
27
|
legacyver init
|
|
33
28
|
```
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
### 4. Analisis & Generate
|
|
31
|
+
Jalankan perintah utama untuk menganalisis folder project kamu:
|
|
37
32
|
```bash
|
|
38
|
-
legacyver analyze ./src
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**3. View the output:**
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
legacyver-docs/
|
|
45
|
-
index.md ← project overview + dependency graph + route map (Laravel)
|
|
46
|
-
SUMMARY.md ← GitBook/Docusaurus compatible table of contents
|
|
47
|
-
src/
|
|
48
|
-
app.md
|
|
49
|
-
routes/users.md
|
|
50
|
-
...
|
|
33
|
+
legacyver analyze ./src --incremental
|
|
51
34
|
```
|
|
35
|
+
*Flag `--incremental` memastikan hanya file yang dimodifikasi yang akan diproses ulang di jalankan berikutnya (lebih cepat & hemat).*
|
|
52
36
|
|
|
53
37
|
---
|
|
54
38
|
|
|
55
|
-
## CLI Commands
|
|
39
|
+
## 🛠️ Daftar Perintah (CLI Commands)
|
|
56
40
|
|
|
57
|
-
### `legacyver analyze
|
|
41
|
+
### `legacyver analyze [target]`
|
|
42
|
+
Perintah utama untuk memindai codebase dan membuat dokumentasi.
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
|
62
|
-
|
|
63
|
-
| `--
|
|
64
|
-
| `--
|
|
65
|
-
| `--
|
|
66
|
-
| `--
|
|
67
|
-
| `--incremental` | `false` | Skip files unchanged since last run |
|
|
68
|
-
| `--dry-run` | `false` | Estimate cost without calling LLM |
|
|
69
|
-
| `--concurrency <n>` | `3` | Max parallel LLM requests |
|
|
70
|
-
| `--max-file-size <kb>` | `500` | Skip files larger than this |
|
|
71
|
-
| `--no-confirm` | — | Skip cost confirmation prompt |
|
|
72
|
-
| `--verbose` | `false` | Enable debug logging |
|
|
44
|
+
| Flag | Default | Deskripsi |
|
|
45
|
+
|------|---------|-----------|
|
|
46
|
+
| `--out <dir>` | `./legacyver-docs` | Folder output hasil dokumentasi |
|
|
47
|
+
| `--format <fmt>` | `markdown` | Format output: `markdown`, `html`, `json` |
|
|
48
|
+
| `--provider <p>` | `groq` | Provider AI: `groq`, `gemini`, `ollama`, `openrouter` |
|
|
49
|
+
| `--incremental` | `false` | Hanya proses file yang berubah (lebih cepat) |
|
|
50
|
+
| `--dry-run` | `false` | Estimasi penggunaan token tanpa memanggil AI |
|
|
51
|
+
| `--no-confirm` | — | Lewati konfirmasi estimasi biaya |
|
|
73
52
|
|
|
74
53
|
### `legacyver init`
|
|
75
|
-
|
|
76
|
-
Interactive wizard. Detects existing `.legacyverrc` and warns before overwriting. Saves API key to OS user config.
|
|
54
|
+
Wizard interaktif untuk setup API key dan preferensi lokal.
|
|
77
55
|
|
|
78
56
|
### `legacyver providers`
|
|
57
|
+
Cek status API key dan daftar model AI yang tersedia.
|
|
79
58
|
|
|
80
|
-
|
|
59
|
+
### `legacyver login / logout`
|
|
60
|
+
Kelola sesi untuk sinkronisasi dokumentasi ke cloud.
|
|
81
61
|
|
|
82
|
-
|
|
62
|
+
---
|
|
83
63
|
|
|
84
|
-
|
|
64
|
+
## 🏗️ Dukungan Framework & Bahasa
|
|
85
65
|
|
|
86
|
-
|
|
66
|
+
| Kategori | Item yang Didukung |
|
|
67
|
+
|----------|--------------------|
|
|
68
|
+
| **Bahasa** | JavaScript, TypeScript, PHP, Python, Java, Go |
|
|
69
|
+
| **Framework** | **Laravel** (Deteksi Otomatis Routes, Models, ERD), **Express** |
|
|
70
|
+
| **Integrasi** | GitHub Actions, GitBook, Docusaurus |
|
|
87
71
|
|
|
88
|
-
|
|
72
|
+
### 🐘 Integrasi Mendalam Laravel
|
|
73
|
+
Jika file `artisan` terdeteksi, Legacyver otomatis mengekstrak:
|
|
74
|
+
- **Route Maps**: Daftar lengkap method, URI, dan Controller.
|
|
75
|
+
- **ER Diagrams**: Membuat diagram Mermaid otomatis untuk Model kamu.
|
|
76
|
+
- **Service Providers**: Mendokumentasikan binding dependency yang kompleks.
|
|
89
77
|
|
|
90
78
|
---
|
|
91
79
|
|
|
92
|
-
## `.legacyverrc`
|
|
93
|
-
|
|
94
|
-
Create a `.legacyverrc` (JSON or YAML) in your project root:
|
|
80
|
+
## ⚙️ Konfigurasi (`.legacyverrc`)
|
|
81
|
+
Sesuaikan alur kerja kamu dengan file konfigurasi:
|
|
95
82
|
|
|
96
83
|
```json
|
|
97
84
|
{
|
|
98
|
-
"provider": "
|
|
99
|
-
"model": "
|
|
85
|
+
"provider": "gemini",
|
|
86
|
+
"model": "gemini-1.5-flash",
|
|
100
87
|
"format": "markdown",
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"maxFileSizeKb": 500,
|
|
104
|
-
"incremental": true
|
|
88
|
+
"incremental": true,
|
|
89
|
+
"out": "./legacyver-docs"
|
|
105
90
|
}
|
|
106
91
|
```
|
|
107
92
|
|
|
108
|
-
All fields are optional. CLI flags override file config.
|
|
109
|
-
|
|
110
|
-
Also supported: `legacyver.config.js`, `legacyver.config.yaml`.
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## Supported Languages
|
|
115
|
-
|
|
116
|
-
| Language | Extensions | Framework Support |
|
|
117
|
-
|----------|-----------|-------------------|
|
|
118
|
-
| JavaScript | `.js`, `.jsx`, `.mjs` | Express (auto-detected) |
|
|
119
|
-
| TypeScript | `.ts`, `.tsx` | — |
|
|
120
|
-
| Python | `.py` | — |
|
|
121
|
-
| Java | `.java` | — |
|
|
122
|
-
| Go | `.go` | — |
|
|
123
|
-
| PHP | `.php`, `.blade.php` | Laravel (auto-detected) |
|
|
124
|
-
|
|
125
|
-
**Laravel extras:** When an `artisan` file is detected, Legacyver automatically extracts:
|
|
126
|
-
- Route Map (Method, URI, Controller, Middleware, Route Name)
|
|
127
|
-
- Model Relationships (as Mermaid `erDiagram`)
|
|
128
|
-
- Service Provider Bindings
|
|
129
|
-
|
|
130
93
|
---
|
|
131
94
|
|
|
132
|
-
##
|
|
133
|
-
|
|
134
|
-
| Provider | Env Var | Notes |
|
|
135
|
-
|----------|---------|-------|
|
|
136
|
-
| OpenRouter | `OPENROUTER_API_KEY` | Default. Free models available (`:free` suffix). Get a key at [openrouter.ai/keys](https://openrouter.ai/keys) |
|
|
137
|
-
| Ollama | — | Local/offline. Requires Ollama running: `ollama serve` |
|
|
138
|
-
|
|
139
|
-
**Free usage:** The default model `meta-llama/llama-3.3-70b-instruct:free` is free via OpenRouter (rate-limited). Legacyver automatically caps concurrency to 1 for free models.
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## Ignore Files
|
|
144
|
-
|
|
145
|
-
Create a `.legacyverignore` in your project root using gitignore syntax:
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
# .legacyverignore
|
|
149
|
-
dist/
|
|
150
|
-
build/
|
|
151
|
-
coverage/
|
|
152
|
-
*.min.js
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
See `.legacyverignore.example` for a documented example.
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## CI/CD Integration
|
|
160
|
-
|
|
161
|
-
```yaml
|
|
162
|
-
# GitHub Actions example
|
|
163
|
-
- name: Generate docs
|
|
164
|
-
env:
|
|
165
|
-
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
166
|
-
run: |
|
|
167
|
-
npx legacyver analyze ./src --no-confirm --incremental --out ./docs
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Legacyver detects non-TTY environments and disables spinners/progress bars automatically (plain log output only).
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## How It Works
|
|
175
|
-
|
|
176
|
-
1. **Crawl** — `fast-glob` walks the directory, respects `.legacyverignore`
|
|
177
|
-
2. **Parse** — Regex/heuristic AST extracts facts: function names, params, complexity scores, imports, exports, call patterns
|
|
178
|
-
3. **PKG** — Assembles a Project Knowledge Graph linking all facts
|
|
179
|
-
4. **LLM** — Sends grounded facts + raw source to LLM; system prompt enforces anti-hallucination contract
|
|
180
|
-
5. **Validate** — Post-generation checks: hallucination detection, completeness check (all exports documented)
|
|
181
|
-
6. **Render** — Writes Markdown/HTML/JSON to output directory
|
|
182
|
-
|
|
183
|
-
**Design principle:** The LLM is a _writer_, not an analyst. AST extracts facts; LLM only explains them.
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## Contributing
|
|
188
|
-
|
|
189
|
-
```bash
|
|
190
|
-
git clone https://github.com/user/legacyver
|
|
191
|
-
npm install
|
|
192
|
-
npx vitest run
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
---
|
|
196
|
-
|
|
197
|
-
## License
|
|
198
|
-
|
|
199
|
-
MIT
|
|
95
|
+
## 📄 Lisensi
|
|
96
|
+
MIT License. Dibuat dengan ❤️ untuk developer yang berjuang melawan legacy code.
|
package/bin/legacyver.js
CHANGED
|
@@ -69,4 +69,12 @@ program
|
|
|
69
69
|
.description('Log out and stop syncing docs to the cloud')
|
|
70
70
|
.action(logoutCmd);
|
|
71
71
|
|
|
72
|
+
// push command
|
|
73
|
+
const pushCmd = require('../src/cli/commands/push');
|
|
74
|
+
program
|
|
75
|
+
.command('push [target]')
|
|
76
|
+
.description('Manually push generated docs to the cloud database')
|
|
77
|
+
.option('--out <dir>', 'Docs output directory to read from', './legacyver-docs')
|
|
78
|
+
.action(pushCmd);
|
|
79
|
+
|
|
72
80
|
program.parse(process.argv);
|
|
@@ -1,51 +1,50 @@
|
|
|
1
1
|
## Overview
|
|
2
|
-
|
|
2
|
+
This file contains React components and a utility function for formatting currency. The components include a Button and a UserCard.
|
|
3
3
|
|
|
4
4
|
## Functions
|
|
5
5
|
### Button
|
|
6
|
-
The Button
|
|
6
|
+
The Button component is a React functional component that renders a button element.
|
|
7
7
|
#### Parameters
|
|
8
|
-
|
|
|
8
|
+
| Parameter | Type | Description |
|
|
9
9
|
| --- | --- | --- |
|
|
10
|
-
| label | string | The text
|
|
11
|
-
| onClick | () => void | The function
|
|
12
|
-
| disabled | boolean |
|
|
13
|
-
| variant | 'primary' | 'secondary' | 'danger' |
|
|
10
|
+
| label | string | The text to display on the button |
|
|
11
|
+
| onClick | () => void | The function to call when the button is clicked |
|
|
12
|
+
| disabled | boolean | Optional, whether the button is disabled |
|
|
13
|
+
| variant | 'primary' | 'secondary' | 'danger' | Optional, the style variant of the button |
|
|
14
14
|
#### Return Value
|
|
15
|
-
|
|
15
|
+
The Button component returns a JSX button element.
|
|
16
16
|
|
|
17
17
|
### UserCard
|
|
18
|
-
The UserCard
|
|
18
|
+
The UserCard component is a React functional component that fetches user data and displays it in a card.
|
|
19
19
|
#### Parameters
|
|
20
|
-
|
|
|
20
|
+
| Parameter | Type | Description |
|
|
21
21
|
| --- | --- | --- |
|
|
22
|
-
| userId | number | The ID of the user to fetch
|
|
23
|
-
| onClose | () => void | The function
|
|
22
|
+
| userId | number | The ID of the user to fetch |
|
|
23
|
+
| onClose | () => void | The function to call when the close button is clicked |
|
|
24
24
|
#### Return Value
|
|
25
|
-
|
|
25
|
+
The UserCard component returns a JSX div element containing the user's data or a loading/error message.
|
|
26
26
|
|
|
27
27
|
### formatCurrency
|
|
28
|
-
The formatCurrency function formats a
|
|
28
|
+
The formatCurrency function formats a number as a currency string.
|
|
29
29
|
#### Parameters
|
|
30
|
-
|
|
|
30
|
+
| Parameter | Type | Description |
|
|
31
31
|
| --- | --- | --- |
|
|
32
32
|
| amount | number | The amount to format |
|
|
33
|
-
| currency | string |
|
|
33
|
+
| currency | string | Optional, the currency to use (default: 'USD') |
|
|
34
34
|
#### Return Value
|
|
35
|
-
|
|
35
|
+
The formatCurrency function returns a string representing the formatted currency amount.
|
|
36
36
|
|
|
37
37
|
## Dependencies
|
|
38
38
|
* React
|
|
39
|
-
*
|
|
40
|
-
* useEffect
|
|
39
|
+
* Intl.NumberFormat
|
|
41
40
|
|
|
42
41
|
## Usage Example
|
|
43
|
-
No clear usage example is visible in the provided code,
|
|
42
|
+
No clear usage example is visible in the provided code. However, components can be used as follows:
|
|
44
43
|
```jsx
|
|
45
44
|
import { Button, UserCard, formatCurrency } from './components';
|
|
46
45
|
|
|
47
46
|
const App = () => {
|
|
48
|
-
const
|
|
47
|
+
const handleButtonClicked = () => {
|
|
49
48
|
console.log('Button clicked');
|
|
50
49
|
};
|
|
51
50
|
|
|
@@ -55,9 +54,9 @@ const App = () => {
|
|
|
55
54
|
|
|
56
55
|
return (
|
|
57
56
|
<div>
|
|
58
|
-
<Button label="Click me" onClick={
|
|
59
|
-
<UserCard userId={
|
|
60
|
-
<p>Formatted
|
|
57
|
+
<Button label="Click me" onClick={handleButtonClicked} />
|
|
58
|
+
<UserCard userId={1} onClose={handleUserCardClose} />
|
|
59
|
+
<p>Formatted currency: {formatCurrency(1000)}</p>
|
|
61
60
|
</div>
|
|
62
61
|
);
|
|
63
62
|
};
|
package/legacyver-docs/index.md
CHANGED
package/nul
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/usr/bin/bash: line 1: type: %USERPROFILE%\.legacyver\session.json: not found
|
package/package.json
CHANGED
package/src/api/auth.js
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const { Pool } = require('pg');
|
|
4
5
|
const dbConfig = require('../db/config');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @param {string}
|
|
8
|
+
* Validate a CLI session token against app.user_sessions.
|
|
9
|
+
* Returns user info if valid, null if expired/revoked/not found.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} token raw token from ~/.legacyver/session.json
|
|
11
12
|
* @param {object} [opts] optional overrides for testing
|
|
12
|
-
* @param {object} [opts.pool] pg Pool instance
|
|
13
|
-
* @returns {Promise<{userId: string, username: string, email: string
|
|
13
|
+
* @param {object} [opts.pool] pg Pool instance
|
|
14
|
+
* @returns {Promise<{userId: string, username: string, email: string} | null>}
|
|
14
15
|
*/
|
|
15
|
-
async function
|
|
16
|
+
async function validateToken(token, opts) {
|
|
17
|
+
if (!token) return null;
|
|
18
|
+
|
|
16
19
|
const ownPool = !(opts && opts.pool);
|
|
17
20
|
const pool = (opts && opts.pool) || new Pool(dbConfig);
|
|
18
21
|
try {
|
|
19
|
-
|
|
20
|
-
const existing = await pool.query(
|
|
21
|
-
'SELECT id, username, email FROM users WHERE email = $1',
|
|
22
|
-
[email]
|
|
23
|
-
);
|
|
22
|
+
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const usernameCheck = await pool.query(
|
|
34
|
-
'SELECT id FROM users WHERE username = $1',
|
|
35
|
-
[username]
|
|
24
|
+
const result = await pool.query(
|
|
25
|
+
`SELECT s.user_id, u.username, u.email
|
|
26
|
+
FROM app.user_sessions s
|
|
27
|
+
JOIN app.users u ON u.id = s.user_id
|
|
28
|
+
WHERE s.token_hash = $1
|
|
29
|
+
AND s.expires_at > NOW()
|
|
30
|
+
AND s.revoked_at IS NULL`,
|
|
31
|
+
[tokenHash]
|
|
36
32
|
);
|
|
37
|
-
if (usernameCheck.rows.length > 0) {
|
|
38
|
-
throw new Error(`Username "${username}" is already taken. Try a different one.`);
|
|
39
|
-
}
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
if (result.rows.length === 0) return null;
|
|
35
|
+
|
|
36
|
+
const row = result.rows[0];
|
|
37
|
+
return {
|
|
38
|
+
userId: String(row.user_id),
|
|
39
|
+
username: row.username || 'unknown',
|
|
40
|
+
email: row.email || '',
|
|
41
|
+
};
|
|
48
42
|
} finally {
|
|
49
43
|
if (ownPool) await pool.end().catch(() => {});
|
|
50
44
|
}
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
/**
|
|
54
|
-
*
|
|
55
|
-
* @param {string}
|
|
48
|
+
* Revoke a CLI session token (logout).
|
|
49
|
+
* @param {string} token raw token
|
|
56
50
|
* @param {object} [opts] optional overrides for testing
|
|
57
|
-
* @param {object} [opts.pool] pg Pool instance
|
|
51
|
+
* @param {object} [opts.pool] pg Pool instance
|
|
58
52
|
*/
|
|
59
|
-
async function
|
|
53
|
+
async function revokeToken(token, opts) {
|
|
54
|
+
if (!token) return;
|
|
55
|
+
|
|
60
56
|
const ownPool = !(opts && opts.pool);
|
|
61
57
|
const pool = (opts && opts.pool) || new Pool(dbConfig);
|
|
62
58
|
try {
|
|
63
|
-
|
|
59
|
+
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
|
60
|
+
await pool.query(
|
|
61
|
+
'UPDATE app.user_sessions SET revoked_at = NOW() WHERE token_hash = $1',
|
|
62
|
+
[tokenHash]
|
|
63
|
+
);
|
|
64
64
|
} finally {
|
|
65
65
|
if (ownPool) await pool.end().catch(() => {});
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
module.exports = {
|
|
69
|
+
module.exports = { validateToken, revokeToken };
|
|
@@ -260,7 +260,7 @@ module.exports = async function analyzeCommand(target, flags) {
|
|
|
260
260
|
// Only show spinner if user is logged in
|
|
261
261
|
const { loadSession } = require('../../utils/config');
|
|
262
262
|
const session = loadSession();
|
|
263
|
-
if (session.
|
|
263
|
+
if (session.token) {
|
|
264
264
|
syncSpinner.start();
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -1,68 +1,117 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const pc = require('picocolors');
|
|
5
6
|
const { saveSession, loadSession } = require('../../utils/config');
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
const WEB_URL = 'https://weci-holic.hackathon.sev-2.com';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* Open a URL in the default browser (cross-platform).
|
|
10
12
|
*/
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
});
|
|
13
|
+
function openBrowser(url) {
|
|
14
|
+
const { exec } = require('child_process');
|
|
15
|
+
const cmd = process.platform === 'win32' ? `start "" "${url}"`
|
|
16
|
+
: process.platform === 'darwin' ? `open "${url}"`
|
|
17
|
+
: `xdg-open "${url}"`;
|
|
18
|
+
exec(cmd);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Spawn a temporary local HTTP server, open the browser for GitHub OAuth,
|
|
23
|
+
* and wait for the web app to redirect the token back.
|
|
24
|
+
*
|
|
25
|
+
* Flow:
|
|
26
|
+
* 1. CLI starts local server on a random port
|
|
27
|
+
* 2. CLI opens browser to {WEB_URL}/cli-auth?code={CODE}&port={PORT}
|
|
28
|
+
* 3. User logs in with GitHub on the web app
|
|
29
|
+
* 4. Web app creates a session token and redirects to http://localhost:{PORT}/callback?token=...&username=...&email=...
|
|
30
|
+
* 5. CLI receives the token, saves session, and shuts down local server
|
|
31
|
+
*/
|
|
21
32
|
module.exports = async function loginCommand() {
|
|
22
33
|
const session = loadSession();
|
|
23
|
-
if (session.
|
|
34
|
+
if (session.token) {
|
|
24
35
|
console.log(pc.yellow(`Already logged in as ${session.username} (${session.email}).`));
|
|
25
36
|
console.log(`Run ${pc.cyan('legacyver logout')} first to switch accounts.`);
|
|
26
37
|
return;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
console.log(pc.bold('\nLegacyver Login\n'));
|
|
30
|
-
console.log(pc.dim('
|
|
31
|
-
console.log(pc.dim('If you don\'t have an account, one will be created automatically.\n'));
|
|
41
|
+
console.log(pc.dim('Opening your browser to log in with GitHub...\n'));
|
|
32
42
|
|
|
33
|
-
const
|
|
34
|
-
if (!email || !email.includes('@')) {
|
|
35
|
-
console.error(pc.red('Invalid email address.'));
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
43
|
+
const code = crypto.randomBytes(16).toString('hex');
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const server = http.createServer((req, res) => {
|
|
47
|
+
const url = new URL(req.url, `http://localhost`);
|
|
48
|
+
|
|
49
|
+
if (url.pathname === '/callback') {
|
|
50
|
+
const token = url.searchParams.get('token');
|
|
51
|
+
const username = url.searchParams.get('username');
|
|
52
|
+
const email = url.searchParams.get('email');
|
|
53
|
+
|
|
54
|
+
if (token && username) {
|
|
55
|
+
// Save session with the token
|
|
56
|
+
saveSession({
|
|
57
|
+
token,
|
|
58
|
+
username,
|
|
59
|
+
email: email || '',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Send a nice HTML response to the browser
|
|
63
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
64
|
+
res.end(`
|
|
65
|
+
<html>
|
|
66
|
+
<body style="background:#0a0a0a;color:#fff;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
67
|
+
<div style="text-align:center">
|
|
68
|
+
<h1 style="color:#22c55e">Logged in!</h1>
|
|
69
|
+
<p>You can close this tab and return to the CLI.</p>
|
|
70
|
+
</div>
|
|
71
|
+
</body>
|
|
72
|
+
</html>
|
|
73
|
+
`);
|
|
44
74
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
console.log(pc.green(` Logged in as ${username} (${email})`));
|
|
76
|
+
console.log(pc.dim(' Generated docs will now sync to the cloud after each analyze run.\n'));
|
|
77
|
+
|
|
78
|
+
// Shut down server after a short delay
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
server.close();
|
|
81
|
+
resolve();
|
|
82
|
+
}, 500);
|
|
83
|
+
} else {
|
|
84
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
85
|
+
res.end('Missing token or username in callback.');
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Any other path
|
|
91
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
92
|
+
res.end('Not found');
|
|
51
93
|
});
|
|
52
94
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
console.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
95
|
+
// Listen on a random available port
|
|
96
|
+
server.listen(0, '127.0.0.1', () => {
|
|
97
|
+
const port = server.address().port;
|
|
98
|
+
const authUrl = `${WEB_URL}/cli-auth?code=${code}&port=${port}`;
|
|
99
|
+
|
|
100
|
+
console.log(pc.dim(` Listening on http://localhost:${port}`));
|
|
101
|
+
console.log(pc.dim(` If the browser doesn't open, visit:\n`));
|
|
102
|
+
console.log(` ${pc.cyan(authUrl)}\n`);
|
|
103
|
+
console.log(pc.dim(' Waiting for authentication...\n'));
|
|
104
|
+
|
|
105
|
+
openBrowser(authUrl);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Timeout after 5 minutes
|
|
109
|
+
const timeout = setTimeout(() => {
|
|
110
|
+
console.error(pc.red('\n Login timed out. Please try again.'));
|
|
111
|
+
server.close();
|
|
112
|
+
reject(new Error('Login timed out'));
|
|
113
|
+
}, 5 * 60 * 1000);
|
|
114
|
+
|
|
115
|
+
server.on('close', () => clearTimeout(timeout));
|
|
116
|
+
});
|
|
68
117
|
};
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
const pc = require('picocolors');
|
|
4
4
|
const { loadSession, clearSession } = require('../../utils/config');
|
|
5
|
-
const {
|
|
5
|
+
const { revokeToken } = require('../../api/auth');
|
|
6
6
|
|
|
7
7
|
module.exports = async function logoutCommand() {
|
|
8
8
|
const session = loadSession();
|
|
9
|
-
if (!session.
|
|
9
|
+
if (!session.token) {
|
|
10
10
|
console.log('You are not logged in.');
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
|
-
await
|
|
15
|
+
await revokeToken(session.token);
|
|
16
16
|
} catch {
|
|
17
17
|
// DB update failed — still clear local session
|
|
18
18
|
}
|
|
@@ -20,7 +20,7 @@ module.exports = async function providersCommand() {
|
|
|
20
20
|
// ─── Legacyver Account ────────────────────────────────────────────────────
|
|
21
21
|
const session = loadSession();
|
|
22
22
|
console.log(pc.bold('\nLegacyver Account'));
|
|
23
|
-
if (session.
|
|
23
|
+
if (session.token) {
|
|
24
24
|
console.log(` ${pc.green('Logged in')} as ${session.username} (${session.email})`);
|
|
25
25
|
console.log(' Generated docs will sync to the cloud after each analyze run.');
|
|
26
26
|
} else {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const pc = require('picocolors');
|
|
6
|
+
const { loadSession } = require('../../utils/config');
|
|
7
|
+
const logger = require('../../utils/logger');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* legacyver push [target]
|
|
11
|
+
*
|
|
12
|
+
* Manually push existing generated docs to the cloud database.
|
|
13
|
+
* Reads markdown files from the output directory (default: ./legacyver-docs)
|
|
14
|
+
* and pushes them as documentation pages.
|
|
15
|
+
*
|
|
16
|
+
* Useful when:
|
|
17
|
+
* - analyze ran but DB was down at that time
|
|
18
|
+
* - you want to re-push after fixing DB issues
|
|
19
|
+
* - you generated docs on a machine without login and want to push from another
|
|
20
|
+
*/
|
|
21
|
+
module.exports = async function pushCommand(target, options) {
|
|
22
|
+
const session = loadSession();
|
|
23
|
+
if (!session.token) {
|
|
24
|
+
console.log(pc.red('\n Not logged in. Run ') + pc.cyan('legacyver login') + pc.red(' first.\n'));
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const targetDir = path.resolve(target || '.');
|
|
30
|
+
const outDir = path.resolve(options.out || './legacyver-docs');
|
|
31
|
+
|
|
32
|
+
// Check if output directory exists
|
|
33
|
+
if (!fs.existsSync(outDir)) {
|
|
34
|
+
console.log(pc.red(`\n Output directory not found: ${outDir}`));
|
|
35
|
+
console.log(pc.dim(' Run ') + pc.cyan('legacyver analyze') + pc.dim(' first to generate docs.\n'));
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Collect all .md files from the output directory
|
|
41
|
+
const fragments = [];
|
|
42
|
+
collectMarkdownFiles(outDir, outDir, fragments);
|
|
43
|
+
|
|
44
|
+
if (fragments.length === 0) {
|
|
45
|
+
console.log(pc.yellow('\n No markdown files found in ') + pc.cyan(outDir));
|
|
46
|
+
console.log(pc.dim(' Run ') + pc.cyan('legacyver analyze') + pc.dim(' first to generate docs.\n'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(pc.bold('\nLegacyver Push\n'));
|
|
51
|
+
console.log(pc.dim(` Source: ${targetDir}`));
|
|
52
|
+
console.log(pc.dim(` Docs: ${outDir}`));
|
|
53
|
+
console.log(pc.dim(` Files: ${fragments.length} markdown files\n`));
|
|
54
|
+
|
|
55
|
+
// Dynamically require ora for spinner
|
|
56
|
+
let spinner;
|
|
57
|
+
try {
|
|
58
|
+
const ora = require('ora');
|
|
59
|
+
spinner = ora('Pushing docs to cloud...').start();
|
|
60
|
+
} catch {
|
|
61
|
+
console.log(' Pushing docs to cloud...');
|
|
62
|
+
spinner = { succeed: (m) => console.log(pc.green(' ' + m)), fail: (m) => console.log(pc.red(' ' + m)) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const { pushToDatabase } = require('../../db/index');
|
|
67
|
+
const result = await pushToDatabase(fragments, targetDir);
|
|
68
|
+
|
|
69
|
+
if (result.skipped) {
|
|
70
|
+
spinner.fail('Push skipped — token may be invalid or expired. Try logging in again.');
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
} else {
|
|
73
|
+
spinner.succeed(`Pushed ${result.pushed} files to cloud`);
|
|
74
|
+
console.log(pc.dim('\n Docs are now visible on the web dashboard.\n'));
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
spinner.fail('Push failed: ' + err.message);
|
|
78
|
+
logger.error('Push error details:', err);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Recursively collect .md files from a directory.
|
|
85
|
+
* Each file becomes a fragment with { relativePath, content }.
|
|
86
|
+
*/
|
|
87
|
+
function collectMarkdownFiles(baseDir, dir, results) {
|
|
88
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const fullPath = path.join(dir, entry.name);
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
collectMarkdownFiles(baseDir, fullPath, results);
|
|
93
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
94
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
95
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
96
|
+
results.push({ relativePath, content });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/cli/ui.js
CHANGED
|
@@ -84,7 +84,7 @@ function printSummary(stats) {
|
|
|
84
84
|
// Show login tip if user is not logged in
|
|
85
85
|
const { loadSession } = require('../utils/config');
|
|
86
86
|
const session = loadSession();
|
|
87
|
-
if (!session.
|
|
87
|
+
if (!session.token) {
|
|
88
88
|
console.log(pc.dim('─────────────────────────────────────────────────'));
|
|
89
89
|
console.log(pc.cyan(' Sync docs to the cloud:'));
|
|
90
90
|
console.log('');
|
package/src/db/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const { Pool } = require('pg');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const dbConfig = require('./config');
|
|
6
6
|
const { loadSession } = require('../utils/config');
|
|
7
|
+
const { validateToken } = require('../api/auth');
|
|
7
8
|
const logger = require('../utils/logger');
|
|
8
9
|
|
|
9
10
|
let _pool = null;
|
|
@@ -21,7 +22,7 @@ function getPool() {
|
|
|
21
22
|
/**
|
|
22
23
|
* Find or create a repository for the given user + project path.
|
|
23
24
|
* @param {Pool} pool
|
|
24
|
-
* @param {string} userId
|
|
25
|
+
* @param {string} userId app.users.id (BIGINT as string)
|
|
25
26
|
* @param {string} projectPath absolute path of the analyzed directory
|
|
26
27
|
* @returns {Promise<string>} repository id (UUID)
|
|
27
28
|
*/
|
|
@@ -31,7 +32,7 @@ async function getOrCreateRepo(pool, userId, projectPath) {
|
|
|
31
32
|
|
|
32
33
|
// Try find existing
|
|
33
34
|
const existing = await pool.query(
|
|
34
|
-
'SELECT id FROM repositories WHERE user_id = $1 AND full_name = $2',
|
|
35
|
+
'SELECT id FROM app.repositories WHERE user_id = $1 AND full_name = $2',
|
|
35
36
|
[userId, fullName]
|
|
36
37
|
);
|
|
37
38
|
if (existing.rows.length > 0) {
|
|
@@ -40,7 +41,7 @@ async function getOrCreateRepo(pool, userId, projectPath) {
|
|
|
40
41
|
|
|
41
42
|
// Insert new
|
|
42
43
|
const inserted = await pool.query(
|
|
43
|
-
'INSERT INTO repositories (user_id, name, full_name) VALUES ($1, $2, $3) RETURNING id',
|
|
44
|
+
'INSERT INTO app.repositories (user_id, name, full_name) VALUES ($1, $2, $3) RETURNING id',
|
|
44
45
|
[userId, name, fullName]
|
|
45
46
|
);
|
|
46
47
|
return inserted.rows[0].id;
|
|
@@ -56,7 +57,7 @@ async function getOrCreateRepo(pool, userId, projectPath) {
|
|
|
56
57
|
*/
|
|
57
58
|
async function getOrCreateDocumentation(pool, repositoryId, repoName) {
|
|
58
59
|
const existing = await pool.query(
|
|
59
|
-
'SELECT id FROM documentations WHERE repository_id = $1',
|
|
60
|
+
'SELECT id FROM app.documentations WHERE repository_id = $1',
|
|
60
61
|
[repositoryId]
|
|
61
62
|
);
|
|
62
63
|
if (existing.rows.length > 0) {
|
|
@@ -64,7 +65,7 @@ async function getOrCreateDocumentation(pool, repositoryId, repoName) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
const inserted = await pool.query(
|
|
67
|
-
'INSERT INTO documentations (repository_id, title, description) VALUES ($1, $2, $3) RETURNING id',
|
|
68
|
+
'INSERT INTO app.documentations (repository_id, title, description) VALUES ($1, $2, $3) RETURNING id',
|
|
68
69
|
[repositoryId, `${repoName} Documentation`, `Auto-generated documentation for ${repoName}`]
|
|
69
70
|
);
|
|
70
71
|
return inserted.rows[0].id;
|
|
@@ -88,20 +89,20 @@ async function upsertPages(pool, documentationId, fragments) {
|
|
|
88
89
|
|
|
89
90
|
// Check if page exists
|
|
90
91
|
const existing = await pool.query(
|
|
91
|
-
'SELECT id FROM documentation_pages WHERE documentation_id = $1 AND slug = $2',
|
|
92
|
+
'SELECT id FROM app.documentation_pages WHERE documentation_id = $1 AND slug = $2',
|
|
92
93
|
[documentationId, slug]
|
|
93
94
|
);
|
|
94
95
|
|
|
95
96
|
if (existing.rows.length > 0) {
|
|
96
97
|
// Update existing
|
|
97
98
|
await pool.query(
|
|
98
|
-
'UPDATE documentation_pages SET content = $1, title = $2, page_order = $3, created_at = NOW() WHERE id = $4',
|
|
99
|
+
'UPDATE app.documentation_pages SET content = $1, title = $2, page_order = $3, created_at = NOW() WHERE id = $4',
|
|
99
100
|
[frag.content, title, i + 1, existing.rows[0].id]
|
|
100
101
|
);
|
|
101
102
|
} else {
|
|
102
103
|
// Insert new
|
|
103
104
|
await pool.query(
|
|
104
|
-
'INSERT INTO documentation_pages (documentation_id, slug, title, content, page_order) VALUES ($1, $2, $3, $4, $5)',
|
|
105
|
+
'INSERT INTO app.documentation_pages (documentation_id, slug, title, content, page_order) VALUES ($1, $2, $3, $4, $5)',
|
|
105
106
|
[documentationId, slug, title, frag.content, i + 1]
|
|
106
107
|
);
|
|
107
108
|
}
|
|
@@ -112,25 +113,36 @@ async function upsertPages(pool, documentationId, fragments) {
|
|
|
112
113
|
|
|
113
114
|
/**
|
|
114
115
|
* Top-level push function. Called from analyze command.
|
|
115
|
-
*
|
|
116
|
+
* Validates the CLI token, then pushes docs to DB.
|
|
117
|
+
* Short-circuits if user is not logged in or token is invalid.
|
|
116
118
|
* @param {Array<{relativePath: string, content: string}>} fragments
|
|
117
119
|
* @param {string} projectPath absolute path of the analyzed directory
|
|
118
120
|
* @param {object} [opts] optional overrides for testing
|
|
119
121
|
* @param {object} [opts.pool] pg Pool instance (skips singleton pool)
|
|
120
122
|
* @param {object} [opts.session] session object (skips loadSession)
|
|
123
|
+
* @param {object} [opts.user] user object (skips validateToken) — { userId, username, email }
|
|
121
124
|
* @returns {Promise<{skipped: boolean, pushed?: number}>}
|
|
122
125
|
*/
|
|
123
126
|
async function pushToDatabase(fragments, projectPath, opts) {
|
|
124
127
|
const session = (opts && opts.session) || loadSession();
|
|
125
|
-
if (!session.
|
|
128
|
+
if (!session.token) {
|
|
126
129
|
return { skipped: true };
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
// Validate the token to get user info
|
|
133
|
+
let user = (opts && opts.user) || null;
|
|
134
|
+
if (!user) {
|
|
135
|
+
user = await validateToken(session.token);
|
|
136
|
+
if (!user) {
|
|
137
|
+
return { skipped: true };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
const ownPool = !(opts && opts.pool);
|
|
130
142
|
const pool = (opts && opts.pool) || getPool();
|
|
131
143
|
try {
|
|
132
144
|
const repoName = path.basename(projectPath);
|
|
133
|
-
const repoId = await getOrCreateRepo(pool,
|
|
145
|
+
const repoId = await getOrCreateRepo(pool, user.userId, projectPath);
|
|
134
146
|
const docId = await getOrCreateDocumentation(pool, repoId, repoName);
|
|
135
147
|
const pushed = await upsertPages(pool, docId, fragments);
|
|
136
148
|
return { skipped: false, pushed };
|