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.
@@ -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
+ }
@@ -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
+ }