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 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**
@@ -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
+ });