apiforge-analyzer 1.1.0
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 +279 -0
- package/dist/cjs/cli.js +419 -0
- package/dist/cjs/detect.js +68 -0
- package/dist/cjs/export.js +321 -0
- package/dist/cjs/frameworks/express.js +245 -0
- package/dist/cjs/frameworks/nextjs-app.js +181 -0
- package/dist/cjs/frameworks/nextjs-pages.js +154 -0
- package/dist/cjs/frameworks/react-router.js +115 -0
- package/dist/cjs/frameworks/vite.js +84 -0
- package/dist/cjs/index.js +125 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/uploader.js +34 -0
- package/dist/esm/cli.js +417 -0
- package/dist/esm/detect.js +65 -0
- package/dist/esm/export.js +283 -0
- package/dist/esm/frameworks/express.js +242 -0
- package/dist/esm/frameworks/nextjs-app.js +145 -0
- package/dist/esm/frameworks/nextjs-pages.js +118 -0
- package/dist/esm/frameworks/react-router.js +79 -0
- package/dist/esm/frameworks/vite.js +48 -0
- package/dist/esm/index.js +115 -0
- package/dist/esm/uploader.js +28 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/detect.d.ts +13 -0
- package/dist/types/export.d.ts +38 -0
- package/dist/types/frameworks/express.d.ts +23 -0
- package/dist/types/frameworks/nextjs-app.d.ts +19 -0
- package/dist/types/frameworks/nextjs-pages.d.ts +17 -0
- package/dist/types/frameworks/react-router.d.ts +17 -0
- package/dist/types/frameworks/vite.d.ts +17 -0
- package/dist/types/index.d.ts +44 -0
- package/dist/types/uploader.d.ts +11 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# APIForge Analyzer
|
|
2
|
+
|
|
3
|
+
Universal API documentation generator for **Express**, **Next.js**, **Vite**, **React Router**, and **Node.js**.
|
|
4
|
+
|
|
5
|
+
Supports both **ESM** (`import`) and **CommonJS** (`require`).
|
|
6
|
+
|
|
7
|
+
## 📦 Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install apiforge-analyzer
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 🚀 Quick Start
|
|
14
|
+
|
|
15
|
+
### Express (CommonJS)
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const express = require('express')
|
|
19
|
+
const { analyze } = require('apiforge-analyzer')
|
|
20
|
+
|
|
21
|
+
const app = express()
|
|
22
|
+
|
|
23
|
+
app.get('/api/users', (req, res) => res.json({ users: [] }))
|
|
24
|
+
app.post('/api/users', (req, res) => res.json({ created: true }))
|
|
25
|
+
|
|
26
|
+
analyze(app, {
|
|
27
|
+
projectName: 'My API',
|
|
28
|
+
apiKey: process.env.APIFORGE_KEY,
|
|
29
|
+
upload: true
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
app.listen(3000)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Express (ESM)
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
import express from 'express'
|
|
39
|
+
import { analyze } from 'apiforge-analyzer'
|
|
40
|
+
|
|
41
|
+
const app = express()
|
|
42
|
+
|
|
43
|
+
app.get('/api/users', (req, res) => res.json({ users: [] }))
|
|
44
|
+
|
|
45
|
+
await analyze(app, {
|
|
46
|
+
projectName: 'My API',
|
|
47
|
+
apiKey: process.env.APIFORGE_KEY
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
app.listen(3000)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Next.js Pages Router
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
// pages/api/analyze.ts
|
|
57
|
+
import { analyzeNextPages } from 'apiforge-analyzer'
|
|
58
|
+
|
|
59
|
+
export default async function handler(req, res) {
|
|
60
|
+
await analyzeNextPages({
|
|
61
|
+
projectName: 'My Next.js API',
|
|
62
|
+
apiKey: process.env.APIFORGE_KEY,
|
|
63
|
+
upload: true
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
res.json({ success: true })
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Next.js App Router
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
// app/api/analyze/route.ts
|
|
74
|
+
import { analyzeNextApp } from 'apiforge-analyzer'
|
|
75
|
+
|
|
76
|
+
export async function GET() {
|
|
77
|
+
await analyzeNextApp({
|
|
78
|
+
projectName: 'My Next.js API',
|
|
79
|
+
apiKey: process.env.APIFORGE_KEY,
|
|
80
|
+
upload: true
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return Response.json({ success: true })
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Vite
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// vite-analyze.js
|
|
91
|
+
import { analyzeVite } from 'apiforge-analyzer'
|
|
92
|
+
|
|
93
|
+
// Analyzes proxy routes in vite.config.js
|
|
94
|
+
await analyzeVite({
|
|
95
|
+
projectName: 'My Vite App',
|
|
96
|
+
apiKey: process.env.APIFORGE_KEY,
|
|
97
|
+
upload: true
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Example Vite Config:**
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// vite.config.js
|
|
105
|
+
export default {
|
|
106
|
+
server: {
|
|
107
|
+
proxy: {
|
|
108
|
+
'/api': 'http://localhost:3000',
|
|
109
|
+
'/auth': 'http://localhost:3001'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### React Router
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
// analyze-routes.js
|
|
119
|
+
import { analyzeReactRouter } from 'apiforge-analyzer'
|
|
120
|
+
|
|
121
|
+
// Analyzes routes from src/routes.tsx
|
|
122
|
+
await analyzeReactRouter({
|
|
123
|
+
projectName: 'My React App',
|
|
124
|
+
apiKey: process.env.APIFORGE_KEY,
|
|
125
|
+
upload: true
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Example Routes File:**
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
// src/routes.tsx
|
|
133
|
+
import { createBrowserRouter } from 'react-router-dom'
|
|
134
|
+
|
|
135
|
+
export const router = createBrowserRouter([
|
|
136
|
+
{ path: '/', element: <Home /> },
|
|
137
|
+
{ path: '/users/:id', element: <UserDetail /> },
|
|
138
|
+
{ path: '/about', element: <About /> }
|
|
139
|
+
])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## ⚙️ Configuration
|
|
143
|
+
|
|
144
|
+
### Options
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
interface AnalyzerOptions {
|
|
148
|
+
// Required
|
|
149
|
+
projectName: string // Name of your project
|
|
150
|
+
|
|
151
|
+
// Optional
|
|
152
|
+
baseUrl?: string // Base URL of your API
|
|
153
|
+
apiKey?: string // APIForge API key
|
|
154
|
+
apiForgeUrl?: string // APIForge server URL (default: http://localhost:3000)
|
|
155
|
+
upload?: boolean // Upload to APIForge (default: true if apiKey provided)
|
|
156
|
+
saveJson?: boolean // Save routes to JSON file
|
|
157
|
+
jsonPath?: string // Path for JSON file (default: api-export.json)
|
|
158
|
+
framework?: string // Force framework detection
|
|
159
|
+
nextDir?: string // Next.js directory or config file path
|
|
160
|
+
skipInProduction?: boolean // Skip in production (default: true)
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Environment Variables
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
APIFORGE_KEY=your-api-key
|
|
168
|
+
APIFORGE_URL=http://localhost:3000 # or https://apiforge.dev
|
|
169
|
+
NODE_ENV=development # Analysis skipped if production
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 📚 Framework Support
|
|
173
|
+
|
|
174
|
+
| Framework | Status | Import |
|
|
175
|
+
|-----------|--------|--------|
|
|
176
|
+
| Express | ✅ Full | `analyzeExpress` or `analyze` |
|
|
177
|
+
| Next.js Pages | ✅ Full | `analyzeNextPages` |
|
|
178
|
+
| Next.js App Router | ✅ Full | `analyzeNextApp` |
|
|
179
|
+
| Vite | ✅ Full | `analyzeVite` |
|
|
180
|
+
| React Router | ✅ Full | `analyzeReactRouter` |
|
|
181
|
+
|
|
182
|
+
## 📖 Examples
|
|
183
|
+
|
|
184
|
+
### Export to JSON Only
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
import { analyze } from 'apiforge-analyzer'
|
|
188
|
+
|
|
189
|
+
await analyze(app, {
|
|
190
|
+
projectName: 'My API',
|
|
191
|
+
saveJson: true,
|
|
192
|
+
jsonPath: './docs/api.json',
|
|
193
|
+
upload: false // Don't upload
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Auto-Detection
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
// Framework is auto-detected
|
|
201
|
+
import { analyze } from 'apiforge-analyzer'
|
|
202
|
+
|
|
203
|
+
await analyze(app, { projectName: 'My API' })
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Custom Config Paths
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
// Vite with custom config
|
|
210
|
+
await analyzeVite({
|
|
211
|
+
projectName: 'My App',
|
|
212
|
+
nextDir: 'vite.config.ts' // Custom path
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// React Router with custom routes file
|
|
216
|
+
await analyzeReactRouter({
|
|
217
|
+
projectName: 'My App',
|
|
218
|
+
nextDir: 'src/app/routes.tsx' // Custom path
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Skip in Production
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
// Automatically skips if NODE_ENV=production
|
|
226
|
+
await analyze(app, {
|
|
227
|
+
projectName: 'My API',
|
|
228
|
+
skipInProduction: true // Default
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## 🔑 Getting an API Key
|
|
233
|
+
|
|
234
|
+
1. Sign up at [APIForge](https://apiforge.dev)
|
|
235
|
+
2. Go to Dashboard → API Keys
|
|
236
|
+
3. Generate a new key
|
|
237
|
+
4. Set `APIFORGE_KEY=your-key` in `.env`
|
|
238
|
+
|
|
239
|
+
## 🛠️ Module Support
|
|
240
|
+
|
|
241
|
+
This package supports both **ESM** and **CommonJS**:
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
// ESM
|
|
245
|
+
import { analyze } from 'apiforge-analyzer'
|
|
246
|
+
|
|
247
|
+
// CommonJS
|
|
248
|
+
const { analyze } = require('apiforge-analyzer')
|
|
249
|
+
|
|
250
|
+
// Default import (CommonJS)
|
|
251
|
+
const apiforge = require('apiforge-analyzer').default
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## 📝 What Gets Analyzed
|
|
255
|
+
|
|
256
|
+
- ✅ Route paths
|
|
257
|
+
- ✅ HTTP methods (GET, POST, PUT, DELETE, etc.)
|
|
258
|
+
- ✅ Path parameters (`:id`, `:userId`)
|
|
259
|
+
- ✅ Middleware names
|
|
260
|
+
- ✅ Route tags/groups
|
|
261
|
+
- ✅ Response codes
|
|
262
|
+
- ✅ Proxy configurations (Vite)
|
|
263
|
+
- ✅ Frontend routes (React Router)
|
|
264
|
+
|
|
265
|
+
## 🧪 Testing Locally
|
|
266
|
+
|
|
267
|
+
See [NPM_LINK_TEST.md](NPM_LINK_TEST.md) for detailed testing instructions using `npm link`.
|
|
268
|
+
|
|
269
|
+
## 🤝 Contributing
|
|
270
|
+
|
|
271
|
+
Pull requests welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
272
|
+
|
|
273
|
+
## 📄 License
|
|
274
|
+
|
|
275
|
+
MIT © APIForge
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
**Made with ❤️ by the APIForge team**
|
package/dist/cjs/cli.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const prompts_1 = require("@clack/prompts");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
8
|
+
const r = '\x1b[0m';
|
|
9
|
+
const b = '\x1b[1m';
|
|
10
|
+
const d = '\x1b[2m';
|
|
11
|
+
const cy = '\x1b[36m';
|
|
12
|
+
const gn = '\x1b[32m';
|
|
13
|
+
const yl = '\x1b[33m';
|
|
14
|
+
const mg = '\x1b[35m';
|
|
15
|
+
const wh = '\x1b[97m';
|
|
16
|
+
const bold = (s) => `${b}${s}${r}`;
|
|
17
|
+
const dim = (s) => `${d}${s}${r}`;
|
|
18
|
+
const cyan = (s) => `${cy}${s}${r}`;
|
|
19
|
+
const green = (s) => `${gn}${s}${r}`;
|
|
20
|
+
const yellow = (s) => `${yl}${s}${r}`;
|
|
21
|
+
const magenta = (s) => `${mg}${s}${r}`;
|
|
22
|
+
const white = (s) => `${wh}${s}${r}`;
|
|
23
|
+
// ─── Brand header ─────────────────────────────────────────────────────────────
|
|
24
|
+
function printHeader() {
|
|
25
|
+
const lines = [
|
|
26
|
+
'',
|
|
27
|
+
` ${cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`,
|
|
28
|
+
'',
|
|
29
|
+
` ${bold(white('⚡ APIForge'))} ${dim('v1.1.0')}`,
|
|
30
|
+
` ${dim('AI-powered API documentation, auto-generated')}`,
|
|
31
|
+
'',
|
|
32
|
+
` ${dim('Your entire API, documented in under 60 seconds.')}`,
|
|
33
|
+
'',
|
|
34
|
+
` ${cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}`,
|
|
35
|
+
'',
|
|
36
|
+
];
|
|
37
|
+
console.log(lines.join('\n'));
|
|
38
|
+
}
|
|
39
|
+
// ─── Shared state across tasks ────────────────────────────────────────────────
|
|
40
|
+
let detectedEntry = '';
|
|
41
|
+
let detectedAppVar = 'app';
|
|
42
|
+
let detectedIsESM = false;
|
|
43
|
+
let detectedFramework = 'unknown';
|
|
44
|
+
let injectionPoint = '';
|
|
45
|
+
let wasAlreadySetup = false;
|
|
46
|
+
let linesAdded = 0;
|
|
47
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
48
|
+
async function main() {
|
|
49
|
+
process.stdout.write('\x1Bc'); // clear screen
|
|
50
|
+
printHeader();
|
|
51
|
+
(0, prompts_1.intro)(bold(` ${cyan('⚡')} APIForge Setup `));
|
|
52
|
+
// ── Questions (grouped so cancel is handled once) ──────────────────────────
|
|
53
|
+
const answers = await (0, prompts_1.group)({
|
|
54
|
+
projectName: () => (0, prompts_1.text)({
|
|
55
|
+
message: 'Name your project',
|
|
56
|
+
placeholder: 'My Awesome API',
|
|
57
|
+
validate: (v) => (!v.trim() ? 'Project name cannot be empty' : undefined),
|
|
58
|
+
}),
|
|
59
|
+
apiKey: () => (0, prompts_1.password)({
|
|
60
|
+
message: `APIForge API key ${dim('(get one at apiforge.brainfogagency.com/dashboard)')}`,
|
|
61
|
+
validate: (v) => {
|
|
62
|
+
if (!v.trim())
|
|
63
|
+
return 'API key is required';
|
|
64
|
+
if (!v.trim().startsWith('afg_'))
|
|
65
|
+
return 'Key must start with afg_';
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
baseUrl: () => (0, prompts_1.text)({
|
|
69
|
+
message: 'Your server base URL',
|
|
70
|
+
placeholder: 'https://api.myapp.com',
|
|
71
|
+
initialValue: 'http://localhost:3000',
|
|
72
|
+
validate: (v) => (!v.trim() ? 'Base URL is required' : undefined),
|
|
73
|
+
}),
|
|
74
|
+
method: () => (0, prompts_1.select)({
|
|
75
|
+
message: 'Sync method',
|
|
76
|
+
options: [
|
|
77
|
+
{
|
|
78
|
+
value: 'runtime',
|
|
79
|
+
label: `${bold('Runtime')} ${dim('→')} auto-syncs on every server start`,
|
|
80
|
+
hint: 'recommended',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
value: 'json',
|
|
84
|
+
label: `${bold('Export JSON')} ${dim('→')} write api-export.json, import to dashboard`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
}, {
|
|
89
|
+
onCancel: () => {
|
|
90
|
+
(0, prompts_1.cancel)(dim('Setup cancelled. Come back when you\'re ready.'));
|
|
91
|
+
process.exit(0);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if ((0, prompts_1.isCancel)(answers))
|
|
95
|
+
process.exit(0);
|
|
96
|
+
const { projectName, apiKey, baseUrl, method } = answers;
|
|
97
|
+
prompts_1.log.info(dim('─────────────────────────────────────────────────'));
|
|
98
|
+
prompts_1.log.info(`${dim('Project:')} ${bold(projectName)}`);
|
|
99
|
+
prompts_1.log.info(`${dim('Mode:')} ${bold(method === 'runtime' ? 'Runtime sync' : 'Export JSON')}`);
|
|
100
|
+
prompts_1.log.info(`${dim('Base URL:')} ${cyan(baseUrl)}`);
|
|
101
|
+
prompts_1.log.info(dim('─────────────────────────────────────────────────'));
|
|
102
|
+
// ── Need to ask for entry file? Do pre-scan first ─────────────────────────
|
|
103
|
+
const preDetected = detectEntryFile();
|
|
104
|
+
let entryFile;
|
|
105
|
+
if (!preDetected) {
|
|
106
|
+
prompts_1.log.warn('Could not auto-detect your server entry file.');
|
|
107
|
+
const custom = await (0, prompts_1.text)({
|
|
108
|
+
message: 'Path to your server entry file',
|
|
109
|
+
placeholder: 'src/index.ts',
|
|
110
|
+
validate: (v) => {
|
|
111
|
+
if (!v.trim())
|
|
112
|
+
return 'Required';
|
|
113
|
+
if (!(0, fs_1.existsSync)((0, path_1.join)(process.cwd(), v.trim())))
|
|
114
|
+
return `Not found: ${v.trim()}`;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if ((0, prompts_1.isCancel)(custom)) {
|
|
118
|
+
(0, prompts_1.cancel)('Setup cancelled.');
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
entryFile = custom.trim();
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
entryFile = preDetected;
|
|
125
|
+
}
|
|
126
|
+
// ── Multi-step animated tasks ──────────────────────────────────────────────
|
|
127
|
+
await (0, prompts_1.tasks)([
|
|
128
|
+
{
|
|
129
|
+
title: `Scanning project`,
|
|
130
|
+
task: async (msg) => {
|
|
131
|
+
await sleep(350);
|
|
132
|
+
detectedEntry = entryFile;
|
|
133
|
+
msg(`Reading ${detectedEntry}`);
|
|
134
|
+
await sleep(250);
|
|
135
|
+
return `${green('✓')} Entry file: ${bold(detectedEntry)}`;
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
title: 'Analysing server file',
|
|
140
|
+
task: async (msg) => {
|
|
141
|
+
await sleep(200);
|
|
142
|
+
const fullPath = (0, path_1.join)(process.cwd(), detectedEntry);
|
|
143
|
+
const src = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
144
|
+
detectedIsESM = detectESM(detectedEntry, src);
|
|
145
|
+
detectedAppVar = detectAppVariable(src) ?? 'app';
|
|
146
|
+
detectedFramework = detectFramework(src);
|
|
147
|
+
msg(`Detected ${detectedFramework}`);
|
|
148
|
+
await sleep(300);
|
|
149
|
+
const moduleType = detectedIsESM ? 'ESM' : 'CJS';
|
|
150
|
+
return [
|
|
151
|
+
`${green('✓')} Framework: ${bold(detectedFramework)}`,
|
|
152
|
+
` Module: ${bold(moduleType)}`,
|
|
153
|
+
` App var: ${bold(detectedAppVar)}`,
|
|
154
|
+
].join('\n');
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
title: 'Patching server file',
|
|
159
|
+
task: async (msg) => {
|
|
160
|
+
await sleep(300);
|
|
161
|
+
msg('Generating snippet…');
|
|
162
|
+
await sleep(200);
|
|
163
|
+
const result = injectCode(detectedEntry, projectName, apiKey, baseUrl, method);
|
|
164
|
+
wasAlreadySetup = result.alreadyInjected;
|
|
165
|
+
injectionPoint = result.injectionPoint ?? 'end of file';
|
|
166
|
+
linesAdded = result.linesAdded ?? 0;
|
|
167
|
+
msg(result.alreadyInjected ? 'Already configured' : `Writing to ${detectedEntry}`);
|
|
168
|
+
await sleep(300);
|
|
169
|
+
if (result.alreadyInjected) {
|
|
170
|
+
return `${yellow('◌')} Already set up — no changes made`;
|
|
171
|
+
}
|
|
172
|
+
return [
|
|
173
|
+
`${green('✓')} Patched: ${bold(detectedEntry)} ${dim(`+${linesAdded} lines`)}`,
|
|
174
|
+
` At: ${dim(injectionPoint)}`,
|
|
175
|
+
].join('\n');
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
title: 'Verifying patch',
|
|
180
|
+
task: async (msg) => {
|
|
181
|
+
await sleep(200);
|
|
182
|
+
if (wasAlreadySetup) {
|
|
183
|
+
return `${yellow('◌')} Skipped — file was already configured`;
|
|
184
|
+
}
|
|
185
|
+
const src = (0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), detectedEntry), 'utf-8');
|
|
186
|
+
if (!src.includes('apiforge-analyzer')) {
|
|
187
|
+
throw new Error('Patch verification failed — please add the snippet manually');
|
|
188
|
+
}
|
|
189
|
+
msg('Confirmed apiforge-analyzer in file');
|
|
190
|
+
await sleep(200);
|
|
191
|
+
return `${green('✓')} Verified`;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
]);
|
|
195
|
+
// ── Summary note ───────────────────────────────────────────────────────────
|
|
196
|
+
if (wasAlreadySetup) {
|
|
197
|
+
(0, prompts_1.note)([
|
|
198
|
+
bold('Already good to go!'),
|
|
199
|
+
'',
|
|
200
|
+
dim(`APIForge is already configured in ${bold(detectedEntry)}.`),
|
|
201
|
+
dim('No changes were made.'),
|
|
202
|
+
].join('\n'), 'Status');
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const snippetPreview = getSnippetPreview(method, detectedAppVar);
|
|
206
|
+
(0, prompts_1.note)([
|
|
207
|
+
` ${dim('File')} ${bold(detectedEntry)} ${dim(`+${linesAdded} lines`)}`,
|
|
208
|
+
` ${dim('Mode')} ${bold(method === 'json' ? 'Export JSON' : 'Runtime sync')}`,
|
|
209
|
+
` ${dim('App var')} ${bold(detectedAppVar)} ${dim('(auto-detected)')}`,
|
|
210
|
+
` ${dim('Format')} ${bold(detectedIsESM ? 'ESM (import)' : 'CJS (require)')}`,
|
|
211
|
+
'',
|
|
212
|
+
dim('Injected snippet:'),
|
|
213
|
+
'',
|
|
214
|
+
...snippetPreview.map(line => ` ${cyan('│')} ${dim(line)}`),
|
|
215
|
+
].join('\n'), 'Patch summary');
|
|
216
|
+
}
|
|
217
|
+
// ── Next steps + outro ─────────────────────────────────────────────────────
|
|
218
|
+
if (method === 'json') {
|
|
219
|
+
(0, prompts_1.note)([
|
|
220
|
+
bold('What to do next:'),
|
|
221
|
+
'',
|
|
222
|
+
` ${dim('1.')} Start your server once:`,
|
|
223
|
+
` ${cyan(getStartCommand())}`,
|
|
224
|
+
'',
|
|
225
|
+
` ${dim('2.')} Upload ${bold('api-export.json')} to the dashboard:`,
|
|
226
|
+
` ${cyan('https://apiforge.brainfogagency.com/dashboard')}`,
|
|
227
|
+
'',
|
|
228
|
+
` ${dim('3.')} Your AI-enhanced docs appear in seconds.`,
|
|
229
|
+
].join('\n'), 'Next steps');
|
|
230
|
+
(0, prompts_1.outro)(`${green('✓')} All set — run your server to generate the export.`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
(0, prompts_1.note)([
|
|
234
|
+
bold('What to do next:'),
|
|
235
|
+
'',
|
|
236
|
+
` ${dim('1.')} Start your server as normal:`,
|
|
237
|
+
` ${cyan(getStartCommand())}`,
|
|
238
|
+
'',
|
|
239
|
+
` ${dim('2.')} APIForge detects all routes automatically.`,
|
|
240
|
+
'',
|
|
241
|
+
` ${dim('3.')} View your live docs:`,
|
|
242
|
+
` ${cyan('https://apiforge.brainfogagency.com/dashboard')}`,
|
|
243
|
+
].join('\n'), 'Next steps');
|
|
244
|
+
(0, prompts_1.outro)(`${green('✓')} All set — start your server and docs will sync automatically.`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// ─── Entry file detection ─────────────────────────────────────────────────────
|
|
248
|
+
function detectEntryFile() {
|
|
249
|
+
const cwd = process.cwd();
|
|
250
|
+
try {
|
|
251
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(cwd, 'package.json'), 'utf-8'));
|
|
252
|
+
if (pkg.main && (0, fs_1.existsSync)((0, path_1.join)(cwd, pkg.main)))
|
|
253
|
+
return pkg.main;
|
|
254
|
+
for (const script of ['start', 'dev', 'serve']) {
|
|
255
|
+
const cmd = pkg.scripts?.[script] ?? '';
|
|
256
|
+
const m = cmd.match(/(?:node|tsx|ts-node)\s+([^\s&|;]+\.(js|ts|mjs|cjs))/);
|
|
257
|
+
if (m && (0, fs_1.existsSync)((0, path_1.join)(cwd, m[1])))
|
|
258
|
+
return m[1];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch { }
|
|
262
|
+
const candidates = [
|
|
263
|
+
'index.ts', 'server.ts', 'app.ts',
|
|
264
|
+
'index.js', 'server.js', 'app.js',
|
|
265
|
+
'src/index.ts', 'src/server.ts', 'src/app.ts',
|
|
266
|
+
'src/index.js', 'src/server.js', 'src/app.js',
|
|
267
|
+
];
|
|
268
|
+
for (const c of candidates)
|
|
269
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(cwd, c)))
|
|
270
|
+
return c;
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
// ─── Framework detection from source ─────────────────────────────────────────
|
|
274
|
+
function detectFramework(src) {
|
|
275
|
+
if (/require\s*\(\s*['"]express['"]\s*\)|from\s+['"]express['"]/.test(src))
|
|
276
|
+
return 'Express';
|
|
277
|
+
if (/require\s*\(\s*['"]fastify['"]\s*\)|from\s+['"]fastify['"]/.test(src))
|
|
278
|
+
return 'Fastify';
|
|
279
|
+
if (/require\s*\(\s*['"]koa['"]\s*\)|from\s+['"]koa['"]/.test(src))
|
|
280
|
+
return 'Koa';
|
|
281
|
+
if (/require\s*\(\s*['"]hapi['"]\s*\)|from\s+['"]@hapi\/hapi['"]/.test(src))
|
|
282
|
+
return 'Hapi';
|
|
283
|
+
if (/from\s+['"]next\/server['"]|NextResponse/.test(src))
|
|
284
|
+
return 'Next.js';
|
|
285
|
+
if (/createServer|http\.createServer/.test(src))
|
|
286
|
+
return 'Node HTTP';
|
|
287
|
+
return 'Node.js';
|
|
288
|
+
}
|
|
289
|
+
function injectCode(filePath, projectName, apiKey, baseUrl, method) {
|
|
290
|
+
const fullPath = (0, path_1.join)(process.cwd(), filePath);
|
|
291
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
292
|
+
if (content.includes('apiforge-analyzer')) {
|
|
293
|
+
return { message: 'Already configured', alreadyInjected: true };
|
|
294
|
+
}
|
|
295
|
+
const isESM = detectESM(filePath, content);
|
|
296
|
+
const appVar = detectAppVariable(content) ?? 'app';
|
|
297
|
+
const snippet = getCodeSnippet(projectName, apiKey, baseUrl, method, appVar, isESM);
|
|
298
|
+
const listenRe = /\b(?:app|server|httpServer|http)\s*\.listen\s*\(/;
|
|
299
|
+
const listenMatch = listenRe.exec(content);
|
|
300
|
+
let newContent;
|
|
301
|
+
let injectionPoint;
|
|
302
|
+
if (listenMatch?.index !== undefined) {
|
|
303
|
+
const lineStart = content.lastIndexOf('\n', listenMatch.index - 1) + 1;
|
|
304
|
+
newContent = content.slice(0, lineStart) + snippet + '\n' + content.slice(lineStart);
|
|
305
|
+
injectionPoint = `before ${listenMatch[0].trim()}…`;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
newContent = content.trimEnd() + '\n\n' + snippet;
|
|
309
|
+
injectionPoint = 'end of file';
|
|
310
|
+
}
|
|
311
|
+
(0, fs_1.writeFileSync)(fullPath, newContent, 'utf-8');
|
|
312
|
+
const linesAdded = snippet.split('\n').length;
|
|
313
|
+
return {
|
|
314
|
+
message: `Patched ${filePath}`,
|
|
315
|
+
alreadyInjected: false,
|
|
316
|
+
appVar,
|
|
317
|
+
injectionPoint,
|
|
318
|
+
linesAdded,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function detectESM(filePath, content) {
|
|
322
|
+
try {
|
|
323
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), 'package.json'), 'utf-8'));
|
|
324
|
+
if (pkg.type === 'module')
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
catch { }
|
|
328
|
+
if (filePath.endsWith('.mjs'))
|
|
329
|
+
return true;
|
|
330
|
+
if (filePath.endsWith('.cjs'))
|
|
331
|
+
return false;
|
|
332
|
+
return /^(?:import|export)\s+/m.test(content);
|
|
333
|
+
}
|
|
334
|
+
function detectAppVariable(content) {
|
|
335
|
+
let m = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*express\s*(?:\.default)?\s*\(/);
|
|
336
|
+
if (m)
|
|
337
|
+
return m[1];
|
|
338
|
+
m = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*(?:fastify|Fastify)\s*\(/);
|
|
339
|
+
if (m)
|
|
340
|
+
return m[1];
|
|
341
|
+
m = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*new\s+Koa\s*\(/);
|
|
342
|
+
if (m)
|
|
343
|
+
return m[1];
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
// ─── Snippet builder ──────────────────────────────────────────────────────────
|
|
347
|
+
function getCodeSnippet(projectName, apiKey, baseUrl, method, appVar, isESM) {
|
|
348
|
+
const isJson = method === 'json';
|
|
349
|
+
const opts = isJson
|
|
350
|
+
? [
|
|
351
|
+
` apiKey: '${apiKey}',`,
|
|
352
|
+
` projectName: '${projectName}',`,
|
|
353
|
+
` baseUrl: '${baseUrl}',`,
|
|
354
|
+
` saveJson: true,`,
|
|
355
|
+
` jsonPath: './api-export.json',`,
|
|
356
|
+
` upload: false,`,
|
|
357
|
+
].join('\n')
|
|
358
|
+
: [
|
|
359
|
+
` apiKey: '${apiKey}',`,
|
|
360
|
+
` projectName: '${projectName}',`,
|
|
361
|
+
` baseUrl: '${baseUrl}',`,
|
|
362
|
+
` apiForgeUrl: 'https://apiforge.brainfogagency.com',`,
|
|
363
|
+
` upload: true,`,
|
|
364
|
+
].join('\n');
|
|
365
|
+
if (isESM) {
|
|
366
|
+
return [
|
|
367
|
+
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
368
|
+
`import('apiforge-analyzer').then(({ default: APIForge }) => {`,
|
|
369
|
+
` APIForge.analyze(${appVar}, {`,
|
|
370
|
+
opts,
|
|
371
|
+
` })`,
|
|
372
|
+
`}).catch(() => {})`,
|
|
373
|
+
'',
|
|
374
|
+
].join('\n');
|
|
375
|
+
}
|
|
376
|
+
return [
|
|
377
|
+
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
378
|
+
`Promise.resolve().then(() => require('apiforge-analyzer')).then(mod => {`,
|
|
379
|
+
` const APIForge = mod.default || mod`,
|
|
380
|
+
` APIForge.analyze(${appVar}, {`,
|
|
381
|
+
opts,
|
|
382
|
+
` })`,
|
|
383
|
+
`}).catch(() => {})`,
|
|
384
|
+
'',
|
|
385
|
+
].join('\n');
|
|
386
|
+
}
|
|
387
|
+
/** First 4 lines of the injected snippet for the summary preview. */
|
|
388
|
+
function getSnippetPreview(method, appVar) {
|
|
389
|
+
const isJson = method === 'json';
|
|
390
|
+
return [
|
|
391
|
+
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
392
|
+
`import('apiforge-analyzer').then(({ default: APIForge }) => {`,
|
|
393
|
+
` APIForge.analyze(${appVar}, {`,
|
|
394
|
+
` ${isJson ? 'saveJson: true, upload: false,' : 'upload: true,'}`,
|
|
395
|
+
` …`,
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
/** Best-guess start command from package.json scripts. */
|
|
399
|
+
function getStartCommand() {
|
|
400
|
+
try {
|
|
401
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), 'package.json'), 'utf-8'));
|
|
402
|
+
if (pkg.scripts?.start)
|
|
403
|
+
return 'npm start';
|
|
404
|
+
if (pkg.scripts?.dev)
|
|
405
|
+
return 'npm run dev';
|
|
406
|
+
if (pkg.scripts?.serve)
|
|
407
|
+
return 'npm run serve';
|
|
408
|
+
}
|
|
409
|
+
catch { }
|
|
410
|
+
return 'node ' + (detectedEntry || 'index.js');
|
|
411
|
+
}
|
|
412
|
+
// ─── Util ─────────────────────────────────────────────────────────────────────
|
|
413
|
+
function sleep(ms) {
|
|
414
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
415
|
+
}
|
|
416
|
+
main().catch((err) => {
|
|
417
|
+
console.error(err);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
});
|