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