apiforge-analyzer 1.3.0 → 1.3.2
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/dist/cjs/arch-detect.js +21 -0
- package/dist/cjs/cli.js +244 -10
- package/dist/esm/arch-detect.js +21 -0
- package/dist/esm/cli.js +245 -11
- package/package.json +1 -1
package/dist/cjs/arch-detect.js
CHANGED
|
@@ -44,6 +44,27 @@ function detectArchitectureType(rootDir) {
|
|
|
44
44
|
siblingPackages.forEach(p => servicePaths.push(p));
|
|
45
45
|
evidence.push(`${siblingPackages.length} sibling package.json files (${siblingPackages.join(', ')})`);
|
|
46
46
|
}
|
|
47
|
+
// services/, apps/, or microservices/ dir containing 2+ independently-built
|
|
48
|
+
// subdirs (own Dockerfile and/or package.json) — the common Docker-Compose
|
|
49
|
+
// microservices layout, where each service lives one level deeper than the
|
|
50
|
+
// sibling-package check above looks.
|
|
51
|
+
for (const parentDir of ['services', 'apps', 'microservices']) {
|
|
52
|
+
const parentPath = (0, path_1.join)(rootDir, parentDir);
|
|
53
|
+
if (!(0, fs_1.existsSync)(parentPath))
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
const subdirs = (0, fs_1.readdirSync)(parentPath, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
57
|
+
const ownedServices = subdirs.filter(d => (0, fs_1.existsSync)((0, path_1.join)(parentPath, d.name, 'Dockerfile')) ||
|
|
58
|
+
(0, fs_1.existsSync)((0, path_1.join)(parentPath, d.name, 'package.json')));
|
|
59
|
+
if (ownedServices.length >= 2) {
|
|
60
|
+
microScore += 45;
|
|
61
|
+
ownedServices.forEach(d => servicePaths.push(`./${parentDir}/${d.name}`));
|
|
62
|
+
evidence.push(`${parentDir}/ has ${ownedServices.length} independently-built services: ${ownedServices.map(d => d.name).join(', ')}`);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
}
|
|
47
68
|
// ── Modular Monolithic signals ────────────────────────────────────────────────
|
|
48
69
|
// NestJS module files anywhere in src/
|
|
49
70
|
const srcDir = (0, path_1.join)(rootDir, 'src');
|
package/dist/cjs/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ const green = (s) => `${gn}${s}${r}`;
|
|
|
27
27
|
const yellow = (s) => `${yl}${s}${r}`;
|
|
28
28
|
const red = (s) => `${rd}${s}${r}`;
|
|
29
29
|
const white = (s) => `${wh}${s}${r}`;
|
|
30
|
-
const VERSION = '1.3.
|
|
30
|
+
const VERSION = '1.3.2';
|
|
31
31
|
// Built-in URLs — users never need to configure these
|
|
32
32
|
const BACKEND_PROD = 'https://apiforgeapi.brainfogagency.com';
|
|
33
33
|
const DASHBOARD_PROD = 'https://apiforge.brainfogagency.com/dashboard';
|
|
@@ -391,6 +391,162 @@ function waitForServiceStatusFile(statusFile, timeoutMs) {
|
|
|
391
391
|
}, timeoutMs);
|
|
392
392
|
});
|
|
393
393
|
}
|
|
394
|
+
function hasDockerCompose(cwd) {
|
|
395
|
+
return ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml']
|
|
396
|
+
.some(f => (0, fs_1.existsSync)((0, path_1.join)(cwd, f)));
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Find the gateway among the discovered services (by name or by proxy-middleware
|
|
400
|
+
* usage) and parse its prefix → service map, e.g. { auth: '/auth', gold_rates: '/gold-rates' }.
|
|
401
|
+
* Matches the common `'/prefix': process.env.X_SERVICE_URL || ...` gateway route-table
|
|
402
|
+
* pattern (express-gateway / http-proxy-middleware style). Returns {} if no gateway
|
|
403
|
+
* or no recognizable mapping is found — callers should treat that as "no rewrite."
|
|
404
|
+
*/
|
|
405
|
+
function detectGatewayPrefixMap(cwd, services) {
|
|
406
|
+
const prefixMap = {};
|
|
407
|
+
for (const svc of services) {
|
|
408
|
+
const svcDir = svc.dir.replace(/^\.\//, '');
|
|
409
|
+
const relEntry = svc.entryFile ?? findEntryInDir((0, path_1.join)(cwd, svcDir));
|
|
410
|
+
if (!relEntry)
|
|
411
|
+
continue;
|
|
412
|
+
let source = '';
|
|
413
|
+
try {
|
|
414
|
+
source = (0, fs_1.readFileSync)((0, path_1.join)(cwd, svcDir, relEntry), 'utf-8');
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const looksLikeGateway = /gateway/i.test(svc.name) || /createProxyMiddleware|http-proxy-middleware/.test(source);
|
|
420
|
+
if (!looksLikeGateway)
|
|
421
|
+
continue;
|
|
422
|
+
// '/auth': process.env.AUTH_SERVICE_URL || `http://...`
|
|
423
|
+
const re = /['"](\/[a-zA-Z0-9_-]*)['"]\s*:\s*process\.env\.([A-Z][A-Z0-9_]*)/g;
|
|
424
|
+
let m;
|
|
425
|
+
while ((m = re.exec(source))) {
|
|
426
|
+
const prefix = m[1];
|
|
427
|
+
const serviceName = m[2].replace(/_SERVICE_URL$|_URL$/, '').toLowerCase();
|
|
428
|
+
if (!prefixMap[serviceName])
|
|
429
|
+
prefixMap[serviceName] = prefix; // first (primary) mount wins over aliases
|
|
430
|
+
}
|
|
431
|
+
if (Object.keys(prefixMap).length > 0)
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
return prefixMap;
|
|
435
|
+
}
|
|
436
|
+
function withGatewayPrefix(prefix, routePath) {
|
|
437
|
+
if (routePath === '/')
|
|
438
|
+
return prefix;
|
|
439
|
+
return prefix.replace(/\/$/, '') + (routePath.startsWith('/') ? routePath : `/${routePath}`);
|
|
440
|
+
}
|
|
441
|
+
// ─── Postman collection enrichment ─────────────────────────────────────────────
|
|
442
|
+
// Runtime introspection only sees field NAMES (via regex over handler source),
|
|
443
|
+
// never real example values — a Postman collection, when present, already has
|
|
444
|
+
// hand-written example bodies and descriptions. We fold those in as enrichment
|
|
445
|
+
// only: matched by method + path, never adds or removes an endpoint.
|
|
446
|
+
function findFileRecursive(dir, matches, maxDepth, depth = 0) {
|
|
447
|
+
if (depth > maxDepth)
|
|
448
|
+
return null;
|
|
449
|
+
let entries;
|
|
450
|
+
try {
|
|
451
|
+
entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
for (const e of entries) {
|
|
457
|
+
if (e.isFile() && matches(e.name))
|
|
458
|
+
return (0, path_1.join)(dir, e.name);
|
|
459
|
+
}
|
|
460
|
+
for (const e of entries) {
|
|
461
|
+
if (e.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.next'].includes(e.name)) {
|
|
462
|
+
const found = findFileRecursive((0, path_1.join)(dir, e.name), matches, maxDepth, depth + 1);
|
|
463
|
+
if (found)
|
|
464
|
+
return found;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
function findPostmanCollection(cwd) {
|
|
470
|
+
const isCollection = (name) => name.endsWith('.postman_collection.json');
|
|
471
|
+
for (const dir of ['postman', 'docs/postman', '.postman', 'collections']) {
|
|
472
|
+
const dirPath = (0, path_1.join)(cwd, dir);
|
|
473
|
+
if (!(0, fs_1.existsSync)(dirPath))
|
|
474
|
+
continue;
|
|
475
|
+
try {
|
|
476
|
+
const match = (0, fs_1.readdirSync)(dirPath).find(isCollection);
|
|
477
|
+
if (match)
|
|
478
|
+
return (0, path_1.join)(dirPath, match);
|
|
479
|
+
}
|
|
480
|
+
catch { }
|
|
481
|
+
}
|
|
482
|
+
return findFileRecursive(cwd, isCollection, 3);
|
|
483
|
+
}
|
|
484
|
+
/** "{{baseUrl}}/gold-rates/graph/{{userId}}?interval=x" -> "/gold-rates/graph/:userId" */
|
|
485
|
+
function postmanRawToRoutePath(raw) {
|
|
486
|
+
let p = raw.trim();
|
|
487
|
+
p = p.replace(/^\{\{[^}]+\}\}/, '');
|
|
488
|
+
p = p.split('?')[0];
|
|
489
|
+
p = p.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, ':$1');
|
|
490
|
+
if (!p)
|
|
491
|
+
p = '/';
|
|
492
|
+
if (!p.startsWith('/'))
|
|
493
|
+
p = '/' + p;
|
|
494
|
+
if (p !== '/' && p.endsWith('/'))
|
|
495
|
+
p = p.slice(0, -1);
|
|
496
|
+
return p;
|
|
497
|
+
}
|
|
498
|
+
/** Recursively walk a Postman collection's item tree (folders nest via `item`; requests carry `request`). */
|
|
499
|
+
function walkPostmanItems(items, index) {
|
|
500
|
+
for (const node of items ?? []) {
|
|
501
|
+
if (Array.isArray(node.item))
|
|
502
|
+
walkPostmanItems(node.item, index);
|
|
503
|
+
if (!node.request)
|
|
504
|
+
continue;
|
|
505
|
+
const req = node.request;
|
|
506
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
507
|
+
const rawUrl = typeof req.url === 'string' ? req.url : req.url?.raw;
|
|
508
|
+
if (!rawUrl)
|
|
509
|
+
continue;
|
|
510
|
+
const key = `${method} ${postmanRawToRoutePath(rawUrl)}`;
|
|
511
|
+
if (index.has(key))
|
|
512
|
+
continue; // first match wins — collection often repeats the same endpoint in a smoke-test folder
|
|
513
|
+
const entry = {};
|
|
514
|
+
if (req.body?.mode === 'raw' && typeof req.body.raw === 'string' && req.body.raw.trim()) {
|
|
515
|
+
entry.bodyExample = req.body.raw.trim();
|
|
516
|
+
}
|
|
517
|
+
if (typeof req.description === 'string' && req.description.trim()) {
|
|
518
|
+
entry.description = req.description.trim();
|
|
519
|
+
}
|
|
520
|
+
if (entry.bodyExample || entry.description)
|
|
521
|
+
index.set(key, entry);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** Enrich collected routes in-place with Postman example bodies/descriptions, matched by method+path. Never adds or drops routes. */
|
|
525
|
+
function enrichRoutesFromPostman(cwd, routes) {
|
|
526
|
+
const collectionPath = findPostmanCollection(cwd);
|
|
527
|
+
if (!collectionPath)
|
|
528
|
+
return;
|
|
529
|
+
try {
|
|
530
|
+
const collection = JSON.parse((0, fs_1.readFileSync)(collectionPath, 'utf-8'));
|
|
531
|
+
const index = new Map();
|
|
532
|
+
walkPostmanItems(collection.item, index);
|
|
533
|
+
let enriched = 0;
|
|
534
|
+
for (const route of routes) {
|
|
535
|
+
const match = index.get(`${route.method} ${route.path}`);
|
|
536
|
+
if (!match)
|
|
537
|
+
continue;
|
|
538
|
+
if (match.bodyExample)
|
|
539
|
+
route.bodyExample = match.bodyExample;
|
|
540
|
+
if (match.description && !route.description)
|
|
541
|
+
route.description = match.description;
|
|
542
|
+
enriched++;
|
|
543
|
+
}
|
|
544
|
+
if (enriched > 0) {
|
|
545
|
+
prompts_1.log.info(dim(`Enriched ${enriched} routes with examples from ${(0, path_1.basename)(collectionPath)}`));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch { }
|
|
549
|
+
}
|
|
394
550
|
async function microservicesFlow(config, opts) {
|
|
395
551
|
const cwd = process.cwd();
|
|
396
552
|
const allRoutes = [];
|
|
@@ -403,6 +559,29 @@ async function microservicesFlow(config, opts) {
|
|
|
403
559
|
tryUnlink((0, path_1.join)(cwd, STATUS_FILE));
|
|
404
560
|
tryUnlink((0, path_1.join)(cwd, LOCK_FILE));
|
|
405
561
|
persistApiKeyToEnv(opts.apiKey);
|
|
562
|
+
if (hasDockerCompose(cwd)) {
|
|
563
|
+
(0, prompts_1.note)([
|
|
564
|
+
`${yellow('Docker Compose detected — APIForge will NOT start, build, or touch Docker.')}`,
|
|
565
|
+
'',
|
|
566
|
+
dim('Each service below is run directly with node/tsx on this machine, just'),
|
|
567
|
+
dim('long enough to capture its routes, then reverted. Your containers (if'),
|
|
568
|
+
dim('running) are left completely alone — no ports, network, or images are used.'),
|
|
569
|
+
'',
|
|
570
|
+
bold('For this to work:'),
|
|
571
|
+
` ${dim('·')} ${bold('npm install')} ${dim('has been run at the project root (deps must resolve outside Docker)')}`,
|
|
572
|
+
` ${dim('·')} a readable ${bold('.env')} ${dim('at the project root (same values Docker passes via env_file)')}`,
|
|
573
|
+
'',
|
|
574
|
+
dim("Routes are captured before RabbitMQ/Redis/DB connections resolve, so those don't need to be reachable."),
|
|
575
|
+
].join('\n'), 'Docker Compose project');
|
|
576
|
+
const proceed = await (0, prompts_1.confirm)({
|
|
577
|
+
message: 'Run services locally to collect routes?',
|
|
578
|
+
initialValue: true,
|
|
579
|
+
});
|
|
580
|
+
if ((0, prompts_1.isCancel)(proceed) || !proceed) {
|
|
581
|
+
(0, prompts_1.cancel)(dim('Cancelled. Run `npm install` at the project root, then re-run apiforge.'));
|
|
582
|
+
process.exit(0);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
406
585
|
(0, prompts_1.note)([
|
|
407
586
|
bold(`Analyzing ${config.services.length} service${config.services.length > 1 ? 's' : ''}…`),
|
|
408
587
|
'',
|
|
@@ -410,6 +589,14 @@ async function microservicesFlow(config, opts) {
|
|
|
410
589
|
'',
|
|
411
590
|
dim('Each service is started individually and its routes are collected.'),
|
|
412
591
|
].join('\n'), 'Multi-service analysis');
|
|
592
|
+
// Routes are collected at each service's own internal paths (e.g. auth's /register),
|
|
593
|
+
// not the path clients actually call through the gateway (/auth/register) — Express
|
|
594
|
+
// strips the mount prefix before the service ever sees the request. Rewrite paths
|
|
595
|
+
// here so the docs match what's actually callable.
|
|
596
|
+
const gatewayPrefixes = detectGatewayPrefixMap(cwd, config.services);
|
|
597
|
+
if (Object.keys(gatewayPrefixes).length > 0) {
|
|
598
|
+
prompts_1.log.info(dim(`Gateway prefixes detected: ${Object.entries(gatewayPrefixes).map(([s, p]) => `${s}→${p}`).join(', ')}`));
|
|
599
|
+
}
|
|
413
600
|
for (const svc of config.services) {
|
|
414
601
|
const svcSpin = (0, prompts_1.spinner)();
|
|
415
602
|
svcSpin.start(`${bold(svc.name)} — starting…`);
|
|
@@ -432,11 +619,23 @@ async function microservicesFlow(config, opts) {
|
|
|
432
619
|
const startCmd = svc.startCommand ?? serviceNodeCommand(fullEntry);
|
|
433
620
|
const child = (0, child_process_1.spawn)(startCmd, {
|
|
434
621
|
cwd,
|
|
435
|
-
stdio: 'ignore',
|
|
622
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
436
623
|
shell: true,
|
|
437
624
|
env: { ...process.env },
|
|
438
625
|
detached: false,
|
|
439
626
|
});
|
|
627
|
+
// Capture output so failures are debuggable without re-running the service by hand.
|
|
628
|
+
// Keep the HEAD, not the tail: Node prints "Error: ..." / "Cannot find module ..."
|
|
629
|
+
// first, then unwinds the call stack — a long stack trace must not push that
|
|
630
|
+
// opening line out of the window, so stop growing once capped instead of sliding.
|
|
631
|
+
let output = '';
|
|
632
|
+
const MAX_OUTPUT = 6000;
|
|
633
|
+
const capture = (chunk) => {
|
|
634
|
+
if (output.length < MAX_OUTPUT)
|
|
635
|
+
output += chunk.toString();
|
|
636
|
+
};
|
|
637
|
+
child.stdout?.on('data', capture);
|
|
638
|
+
child.stderr?.on('data', capture);
|
|
440
639
|
// Wait up to 30 s for the status file to appear
|
|
441
640
|
const routes = await waitForServiceStatusFile(statusFile, 30000);
|
|
442
641
|
// Kill service process
|
|
@@ -446,13 +645,22 @@ async function microservicesFlow(config, opts) {
|
|
|
446
645
|
catch { }
|
|
447
646
|
tryUnlink((0, path_1.join)(cwd, statusFile));
|
|
448
647
|
if (routes && routes.length > 0) {
|
|
449
|
-
const
|
|
648
|
+
const prefix = gatewayPrefixes[svc.name];
|
|
649
|
+
const annotated = routes.map((r) => ({
|
|
650
|
+
...r,
|
|
651
|
+
service: svc.name,
|
|
652
|
+
path: prefix ? withGatewayPrefix(prefix, r.path) : r.path,
|
|
653
|
+
}));
|
|
450
654
|
allRoutes.push(...annotated);
|
|
451
655
|
serviceMeta.push({ name: svc.name, baseUrl: svc.baseUrl });
|
|
452
656
|
svcSpin.stop(green(`✓ ${svc.name} — ${routes.length} routes collected`));
|
|
453
657
|
}
|
|
454
658
|
else {
|
|
455
659
|
svcSpin.stop(yellow(`◌ ${svc.name} — no routes collected (timeout or error)`));
|
|
660
|
+
const trimmed = output.trim();
|
|
661
|
+
if (trimmed) {
|
|
662
|
+
prompts_1.log.error(dim(` ${bold(svc.name)} output:\n` + trimmed.split('\n').slice(0, 30).map(l => ` ${l}`).join('\n')));
|
|
663
|
+
}
|
|
456
664
|
}
|
|
457
665
|
// Remove injected code
|
|
458
666
|
removeInjectedCode(fullEntry);
|
|
@@ -461,6 +669,7 @@ async function microservicesFlow(config, opts) {
|
|
|
461
669
|
// Safety: clean up any remaining injections (e.g. if process was interrupted)
|
|
462
670
|
for (const f of injectedFiles)
|
|
463
671
|
removeInjectedCode(f);
|
|
672
|
+
enrichRoutesFromPostman(cwd, allRoutes);
|
|
464
673
|
if (allRoutes.length === 0) {
|
|
465
674
|
prompts_1.log.error(red('No routes were collected from any service.'));
|
|
466
675
|
(0, prompts_1.outro)(red('Analysis failed.'));
|
|
@@ -896,8 +1105,15 @@ function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architec
|
|
|
896
1105
|
const isESM = detectESM(filePath, content);
|
|
897
1106
|
const appVar = detectAppVariable(content) ?? 'app';
|
|
898
1107
|
const snippet = buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, architectureType, collectionOpts);
|
|
1108
|
+
// Collection mode (microservices): append at end of file instead of before
|
|
1109
|
+
// .listen(). Route mounting (app.use(...)) happens synchronously at module
|
|
1110
|
+
// load; .listen() is often gated behind an async bootstrap (DB/broker
|
|
1111
|
+
// connect) that can hang or fail outside its container/network. Appending
|
|
1112
|
+
// at the end runs right after the synchronous portion of the module —
|
|
1113
|
+
// independent of whether that bootstrap ever finishes — so route capture
|
|
1114
|
+
// doesn't require RabbitMQ/Redis/DB to be reachable.
|
|
899
1115
|
const listenRe = /\b(?:app|server|httpServer|http)\s*\.listen\s*\(/;
|
|
900
|
-
const listenMatch = listenRe.exec(content);
|
|
1116
|
+
const listenMatch = collectionOpts ? undefined : listenRe.exec(content);
|
|
901
1117
|
let newContent;
|
|
902
1118
|
let injectionPoint;
|
|
903
1119
|
if (listenMatch?.index !== undefined) {
|
|
@@ -974,13 +1190,33 @@ function buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, a
|
|
|
974
1190
|
` upload: true,`,
|
|
975
1191
|
` runOnce: true,`,
|
|
976
1192
|
].join('\n');
|
|
1193
|
+
// Collection mode: some services only mount routes inside an async bootstrap
|
|
1194
|
+
// gated behind a DB/broker connect (which may hang/fail outside Docker).
|
|
1195
|
+
// Poll briefly for app._router to be populated (Express-shaped apps only)
|
|
1196
|
+
// before giving up and collecting whatever is there — bounded well under
|
|
1197
|
+
// the CLI's 30s per-service timeout so a hung connect still surfaces.
|
|
1198
|
+
const runAnalyze = `APIForge.analyze(${appVar}, {\n${opts}\n })`;
|
|
1199
|
+
const call = collectionOpts
|
|
1200
|
+
? [
|
|
1201
|
+
` let __apiforgeTries = 0`,
|
|
1202
|
+
` const __apiforgeTryCollect = () => {`,
|
|
1203
|
+
` const hasRoutes = !${appVar} || !${appVar}._router`,
|
|
1204
|
+
` ? true`,
|
|
1205
|
+
` : Array.isArray(${appVar}._router.stack) && ${appVar}._router.stack.length > 0`,
|
|
1206
|
+
` if (hasRoutes || __apiforgeTries++ >= 26) {`,
|
|
1207
|
+
` ${runAnalyze}`,
|
|
1208
|
+
` } else {`,
|
|
1209
|
+
` setTimeout(__apiforgeTryCollect, 300)`,
|
|
1210
|
+
` }`,
|
|
1211
|
+
` }`,
|
|
1212
|
+
` __apiforgeTryCollect()`,
|
|
1213
|
+
].join('\n')
|
|
1214
|
+
: ` ${runAnalyze}`;
|
|
977
1215
|
if (isESM) {
|
|
978
1216
|
return [
|
|
979
1217
|
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
980
1218
|
`import('apiforge-analyzer').then(({ default: APIForge }) => {`,
|
|
981
|
-
|
|
982
|
-
opts,
|
|
983
|
-
` })`,
|
|
1219
|
+
call,
|
|
984
1220
|
`}).catch(() => {})`,
|
|
985
1221
|
'',
|
|
986
1222
|
].join('\n');
|
|
@@ -989,9 +1225,7 @@ function buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, a
|
|
|
989
1225
|
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
990
1226
|
`Promise.resolve().then(() => require('apiforge-analyzer')).then(mod => {`,
|
|
991
1227
|
` const APIForge = mod.default || mod`,
|
|
992
|
-
|
|
993
|
-
opts,
|
|
994
|
-
` })`,
|
|
1228
|
+
call,
|
|
995
1229
|
`}).catch(() => {})`,
|
|
996
1230
|
'',
|
|
997
1231
|
].join('\n');
|
package/dist/esm/arch-detect.js
CHANGED
|
@@ -41,6 +41,27 @@ export function detectArchitectureType(rootDir) {
|
|
|
41
41
|
siblingPackages.forEach(p => servicePaths.push(p));
|
|
42
42
|
evidence.push(`${siblingPackages.length} sibling package.json files (${siblingPackages.join(', ')})`);
|
|
43
43
|
}
|
|
44
|
+
// services/, apps/, or microservices/ dir containing 2+ independently-built
|
|
45
|
+
// subdirs (own Dockerfile and/or package.json) — the common Docker-Compose
|
|
46
|
+
// microservices layout, where each service lives one level deeper than the
|
|
47
|
+
// sibling-package check above looks.
|
|
48
|
+
for (const parentDir of ['services', 'apps', 'microservices']) {
|
|
49
|
+
const parentPath = join(rootDir, parentDir);
|
|
50
|
+
if (!existsSync(parentPath))
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
const subdirs = readdirSync(parentPath, { withFileTypes: true }).filter(d => d.isDirectory());
|
|
54
|
+
const ownedServices = subdirs.filter(d => existsSync(join(parentPath, d.name, 'Dockerfile')) ||
|
|
55
|
+
existsSync(join(parentPath, d.name, 'package.json')));
|
|
56
|
+
if (ownedServices.length >= 2) {
|
|
57
|
+
microScore += 45;
|
|
58
|
+
ownedServices.forEach(d => servicePaths.push(`./${parentDir}/${d.name}`));
|
|
59
|
+
evidence.push(`${parentDir}/ has ${ownedServices.length} independently-built services: ${ownedServices.map(d => d.name).join(', ')}`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
44
65
|
// ── Modular Monolithic signals ────────────────────────────────────────────────
|
|
45
66
|
// NestJS module files anywhere in src/
|
|
46
67
|
const srcDir = join(rootDir, 'src');
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { group, intro, outro, text, select, password, confirm, tasks, note, log, isCancel, cancel, spinner, } from '@clack/prompts';
|
|
3
3
|
import { readFileSync, existsSync, writeFileSync, unlinkSync, readdirSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
4
|
+
import { join, basename } from 'path';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
import { detectArchitectureType } from './arch-detect.js';
|
|
@@ -22,7 +22,7 @@ const green = (s) => `${gn}${s}${r}`;
|
|
|
22
22
|
const yellow = (s) => `${yl}${s}${r}`;
|
|
23
23
|
const red = (s) => `${rd}${s}${r}`;
|
|
24
24
|
const white = (s) => `${wh}${s}${r}`;
|
|
25
|
-
const VERSION = '1.3.
|
|
25
|
+
const VERSION = '1.3.2';
|
|
26
26
|
// Built-in URLs — users never need to configure these
|
|
27
27
|
const BACKEND_PROD = 'https://apiforgeapi.brainfogagency.com';
|
|
28
28
|
const DASHBOARD_PROD = 'https://apiforge.brainfogagency.com/dashboard';
|
|
@@ -386,6 +386,162 @@ function waitForServiceStatusFile(statusFile, timeoutMs) {
|
|
|
386
386
|
}, timeoutMs);
|
|
387
387
|
});
|
|
388
388
|
}
|
|
389
|
+
function hasDockerCompose(cwd) {
|
|
390
|
+
return ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml']
|
|
391
|
+
.some(f => existsSync(join(cwd, f)));
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Find the gateway among the discovered services (by name or by proxy-middleware
|
|
395
|
+
* usage) and parse its prefix → service map, e.g. { auth: '/auth', gold_rates: '/gold-rates' }.
|
|
396
|
+
* Matches the common `'/prefix': process.env.X_SERVICE_URL || ...` gateway route-table
|
|
397
|
+
* pattern (express-gateway / http-proxy-middleware style). Returns {} if no gateway
|
|
398
|
+
* or no recognizable mapping is found — callers should treat that as "no rewrite."
|
|
399
|
+
*/
|
|
400
|
+
function detectGatewayPrefixMap(cwd, services) {
|
|
401
|
+
const prefixMap = {};
|
|
402
|
+
for (const svc of services) {
|
|
403
|
+
const svcDir = svc.dir.replace(/^\.\//, '');
|
|
404
|
+
const relEntry = svc.entryFile ?? findEntryInDir(join(cwd, svcDir));
|
|
405
|
+
if (!relEntry)
|
|
406
|
+
continue;
|
|
407
|
+
let source = '';
|
|
408
|
+
try {
|
|
409
|
+
source = readFileSync(join(cwd, svcDir, relEntry), 'utf-8');
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const looksLikeGateway = /gateway/i.test(svc.name) || /createProxyMiddleware|http-proxy-middleware/.test(source);
|
|
415
|
+
if (!looksLikeGateway)
|
|
416
|
+
continue;
|
|
417
|
+
// '/auth': process.env.AUTH_SERVICE_URL || `http://...`
|
|
418
|
+
const re = /['"](\/[a-zA-Z0-9_-]*)['"]\s*:\s*process\.env\.([A-Z][A-Z0-9_]*)/g;
|
|
419
|
+
let m;
|
|
420
|
+
while ((m = re.exec(source))) {
|
|
421
|
+
const prefix = m[1];
|
|
422
|
+
const serviceName = m[2].replace(/_SERVICE_URL$|_URL$/, '').toLowerCase();
|
|
423
|
+
if (!prefixMap[serviceName])
|
|
424
|
+
prefixMap[serviceName] = prefix; // first (primary) mount wins over aliases
|
|
425
|
+
}
|
|
426
|
+
if (Object.keys(prefixMap).length > 0)
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
return prefixMap;
|
|
430
|
+
}
|
|
431
|
+
function withGatewayPrefix(prefix, routePath) {
|
|
432
|
+
if (routePath === '/')
|
|
433
|
+
return prefix;
|
|
434
|
+
return prefix.replace(/\/$/, '') + (routePath.startsWith('/') ? routePath : `/${routePath}`);
|
|
435
|
+
}
|
|
436
|
+
// ─── Postman collection enrichment ─────────────────────────────────────────────
|
|
437
|
+
// Runtime introspection only sees field NAMES (via regex over handler source),
|
|
438
|
+
// never real example values — a Postman collection, when present, already has
|
|
439
|
+
// hand-written example bodies and descriptions. We fold those in as enrichment
|
|
440
|
+
// only: matched by method + path, never adds or removes an endpoint.
|
|
441
|
+
function findFileRecursive(dir, matches, maxDepth, depth = 0) {
|
|
442
|
+
if (depth > maxDepth)
|
|
443
|
+
return null;
|
|
444
|
+
let entries;
|
|
445
|
+
try {
|
|
446
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
for (const e of entries) {
|
|
452
|
+
if (e.isFile() && matches(e.name))
|
|
453
|
+
return join(dir, e.name);
|
|
454
|
+
}
|
|
455
|
+
for (const e of entries) {
|
|
456
|
+
if (e.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.next'].includes(e.name)) {
|
|
457
|
+
const found = findFileRecursive(join(dir, e.name), matches, maxDepth, depth + 1);
|
|
458
|
+
if (found)
|
|
459
|
+
return found;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
function findPostmanCollection(cwd) {
|
|
465
|
+
const isCollection = (name) => name.endsWith('.postman_collection.json');
|
|
466
|
+
for (const dir of ['postman', 'docs/postman', '.postman', 'collections']) {
|
|
467
|
+
const dirPath = join(cwd, dir);
|
|
468
|
+
if (!existsSync(dirPath))
|
|
469
|
+
continue;
|
|
470
|
+
try {
|
|
471
|
+
const match = readdirSync(dirPath).find(isCollection);
|
|
472
|
+
if (match)
|
|
473
|
+
return join(dirPath, match);
|
|
474
|
+
}
|
|
475
|
+
catch { }
|
|
476
|
+
}
|
|
477
|
+
return findFileRecursive(cwd, isCollection, 3);
|
|
478
|
+
}
|
|
479
|
+
/** "{{baseUrl}}/gold-rates/graph/{{userId}}?interval=x" -> "/gold-rates/graph/:userId" */
|
|
480
|
+
function postmanRawToRoutePath(raw) {
|
|
481
|
+
let p = raw.trim();
|
|
482
|
+
p = p.replace(/^\{\{[^}]+\}\}/, '');
|
|
483
|
+
p = p.split('?')[0];
|
|
484
|
+
p = p.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, ':$1');
|
|
485
|
+
if (!p)
|
|
486
|
+
p = '/';
|
|
487
|
+
if (!p.startsWith('/'))
|
|
488
|
+
p = '/' + p;
|
|
489
|
+
if (p !== '/' && p.endsWith('/'))
|
|
490
|
+
p = p.slice(0, -1);
|
|
491
|
+
return p;
|
|
492
|
+
}
|
|
493
|
+
/** Recursively walk a Postman collection's item tree (folders nest via `item`; requests carry `request`). */
|
|
494
|
+
function walkPostmanItems(items, index) {
|
|
495
|
+
for (const node of items ?? []) {
|
|
496
|
+
if (Array.isArray(node.item))
|
|
497
|
+
walkPostmanItems(node.item, index);
|
|
498
|
+
if (!node.request)
|
|
499
|
+
continue;
|
|
500
|
+
const req = node.request;
|
|
501
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
502
|
+
const rawUrl = typeof req.url === 'string' ? req.url : req.url?.raw;
|
|
503
|
+
if (!rawUrl)
|
|
504
|
+
continue;
|
|
505
|
+
const key = `${method} ${postmanRawToRoutePath(rawUrl)}`;
|
|
506
|
+
if (index.has(key))
|
|
507
|
+
continue; // first match wins — collection often repeats the same endpoint in a smoke-test folder
|
|
508
|
+
const entry = {};
|
|
509
|
+
if (req.body?.mode === 'raw' && typeof req.body.raw === 'string' && req.body.raw.trim()) {
|
|
510
|
+
entry.bodyExample = req.body.raw.trim();
|
|
511
|
+
}
|
|
512
|
+
if (typeof req.description === 'string' && req.description.trim()) {
|
|
513
|
+
entry.description = req.description.trim();
|
|
514
|
+
}
|
|
515
|
+
if (entry.bodyExample || entry.description)
|
|
516
|
+
index.set(key, entry);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** Enrich collected routes in-place with Postman example bodies/descriptions, matched by method+path. Never adds or drops routes. */
|
|
520
|
+
function enrichRoutesFromPostman(cwd, routes) {
|
|
521
|
+
const collectionPath = findPostmanCollection(cwd);
|
|
522
|
+
if (!collectionPath)
|
|
523
|
+
return;
|
|
524
|
+
try {
|
|
525
|
+
const collection = JSON.parse(readFileSync(collectionPath, 'utf-8'));
|
|
526
|
+
const index = new Map();
|
|
527
|
+
walkPostmanItems(collection.item, index);
|
|
528
|
+
let enriched = 0;
|
|
529
|
+
for (const route of routes) {
|
|
530
|
+
const match = index.get(`${route.method} ${route.path}`);
|
|
531
|
+
if (!match)
|
|
532
|
+
continue;
|
|
533
|
+
if (match.bodyExample)
|
|
534
|
+
route.bodyExample = match.bodyExample;
|
|
535
|
+
if (match.description && !route.description)
|
|
536
|
+
route.description = match.description;
|
|
537
|
+
enriched++;
|
|
538
|
+
}
|
|
539
|
+
if (enriched > 0) {
|
|
540
|
+
log.info(dim(`Enriched ${enriched} routes with examples from ${basename(collectionPath)}`));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch { }
|
|
544
|
+
}
|
|
389
545
|
async function microservicesFlow(config, opts) {
|
|
390
546
|
const cwd = process.cwd();
|
|
391
547
|
const allRoutes = [];
|
|
@@ -398,6 +554,29 @@ async function microservicesFlow(config, opts) {
|
|
|
398
554
|
tryUnlink(join(cwd, STATUS_FILE));
|
|
399
555
|
tryUnlink(join(cwd, LOCK_FILE));
|
|
400
556
|
persistApiKeyToEnv(opts.apiKey);
|
|
557
|
+
if (hasDockerCompose(cwd)) {
|
|
558
|
+
note([
|
|
559
|
+
`${yellow('Docker Compose detected — APIForge will NOT start, build, or touch Docker.')}`,
|
|
560
|
+
'',
|
|
561
|
+
dim('Each service below is run directly with node/tsx on this machine, just'),
|
|
562
|
+
dim('long enough to capture its routes, then reverted. Your containers (if'),
|
|
563
|
+
dim('running) are left completely alone — no ports, network, or images are used.'),
|
|
564
|
+
'',
|
|
565
|
+
bold('For this to work:'),
|
|
566
|
+
` ${dim('·')} ${bold('npm install')} ${dim('has been run at the project root (deps must resolve outside Docker)')}`,
|
|
567
|
+
` ${dim('·')} a readable ${bold('.env')} ${dim('at the project root (same values Docker passes via env_file)')}`,
|
|
568
|
+
'',
|
|
569
|
+
dim("Routes are captured before RabbitMQ/Redis/DB connections resolve, so those don't need to be reachable."),
|
|
570
|
+
].join('\n'), 'Docker Compose project');
|
|
571
|
+
const proceed = await confirm({
|
|
572
|
+
message: 'Run services locally to collect routes?',
|
|
573
|
+
initialValue: true,
|
|
574
|
+
});
|
|
575
|
+
if (isCancel(proceed) || !proceed) {
|
|
576
|
+
cancel(dim('Cancelled. Run `npm install` at the project root, then re-run apiforge.'));
|
|
577
|
+
process.exit(0);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
401
580
|
note([
|
|
402
581
|
bold(`Analyzing ${config.services.length} service${config.services.length > 1 ? 's' : ''}…`),
|
|
403
582
|
'',
|
|
@@ -405,6 +584,14 @@ async function microservicesFlow(config, opts) {
|
|
|
405
584
|
'',
|
|
406
585
|
dim('Each service is started individually and its routes are collected.'),
|
|
407
586
|
].join('\n'), 'Multi-service analysis');
|
|
587
|
+
// Routes are collected at each service's own internal paths (e.g. auth's /register),
|
|
588
|
+
// not the path clients actually call through the gateway (/auth/register) — Express
|
|
589
|
+
// strips the mount prefix before the service ever sees the request. Rewrite paths
|
|
590
|
+
// here so the docs match what's actually callable.
|
|
591
|
+
const gatewayPrefixes = detectGatewayPrefixMap(cwd, config.services);
|
|
592
|
+
if (Object.keys(gatewayPrefixes).length > 0) {
|
|
593
|
+
log.info(dim(`Gateway prefixes detected: ${Object.entries(gatewayPrefixes).map(([s, p]) => `${s}→${p}`).join(', ')}`));
|
|
594
|
+
}
|
|
408
595
|
for (const svc of config.services) {
|
|
409
596
|
const svcSpin = spinner();
|
|
410
597
|
svcSpin.start(`${bold(svc.name)} — starting…`);
|
|
@@ -427,11 +614,23 @@ async function microservicesFlow(config, opts) {
|
|
|
427
614
|
const startCmd = svc.startCommand ?? serviceNodeCommand(fullEntry);
|
|
428
615
|
const child = spawn(startCmd, {
|
|
429
616
|
cwd,
|
|
430
|
-
stdio: 'ignore',
|
|
617
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
431
618
|
shell: true,
|
|
432
619
|
env: { ...process.env },
|
|
433
620
|
detached: false,
|
|
434
621
|
});
|
|
622
|
+
// Capture output so failures are debuggable without re-running the service by hand.
|
|
623
|
+
// Keep the HEAD, not the tail: Node prints "Error: ..." / "Cannot find module ..."
|
|
624
|
+
// first, then unwinds the call stack — a long stack trace must not push that
|
|
625
|
+
// opening line out of the window, so stop growing once capped instead of sliding.
|
|
626
|
+
let output = '';
|
|
627
|
+
const MAX_OUTPUT = 6000;
|
|
628
|
+
const capture = (chunk) => {
|
|
629
|
+
if (output.length < MAX_OUTPUT)
|
|
630
|
+
output += chunk.toString();
|
|
631
|
+
};
|
|
632
|
+
child.stdout?.on('data', capture);
|
|
633
|
+
child.stderr?.on('data', capture);
|
|
435
634
|
// Wait up to 30 s for the status file to appear
|
|
436
635
|
const routes = await waitForServiceStatusFile(statusFile, 30000);
|
|
437
636
|
// Kill service process
|
|
@@ -441,13 +640,22 @@ async function microservicesFlow(config, opts) {
|
|
|
441
640
|
catch { }
|
|
442
641
|
tryUnlink(join(cwd, statusFile));
|
|
443
642
|
if (routes && routes.length > 0) {
|
|
444
|
-
const
|
|
643
|
+
const prefix = gatewayPrefixes[svc.name];
|
|
644
|
+
const annotated = routes.map((r) => ({
|
|
645
|
+
...r,
|
|
646
|
+
service: svc.name,
|
|
647
|
+
path: prefix ? withGatewayPrefix(prefix, r.path) : r.path,
|
|
648
|
+
}));
|
|
445
649
|
allRoutes.push(...annotated);
|
|
446
650
|
serviceMeta.push({ name: svc.name, baseUrl: svc.baseUrl });
|
|
447
651
|
svcSpin.stop(green(`✓ ${svc.name} — ${routes.length} routes collected`));
|
|
448
652
|
}
|
|
449
653
|
else {
|
|
450
654
|
svcSpin.stop(yellow(`◌ ${svc.name} — no routes collected (timeout or error)`));
|
|
655
|
+
const trimmed = output.trim();
|
|
656
|
+
if (trimmed) {
|
|
657
|
+
log.error(dim(` ${bold(svc.name)} output:\n` + trimmed.split('\n').slice(0, 30).map(l => ` ${l}`).join('\n')));
|
|
658
|
+
}
|
|
451
659
|
}
|
|
452
660
|
// Remove injected code
|
|
453
661
|
removeInjectedCode(fullEntry);
|
|
@@ -456,6 +664,7 @@ async function microservicesFlow(config, opts) {
|
|
|
456
664
|
// Safety: clean up any remaining injections (e.g. if process was interrupted)
|
|
457
665
|
for (const f of injectedFiles)
|
|
458
666
|
removeInjectedCode(f);
|
|
667
|
+
enrichRoutesFromPostman(cwd, allRoutes);
|
|
459
668
|
if (allRoutes.length === 0) {
|
|
460
669
|
log.error(red('No routes were collected from any service.'));
|
|
461
670
|
outro(red('Analysis failed.'));
|
|
@@ -891,8 +1100,15 @@ function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architec
|
|
|
891
1100
|
const isESM = detectESM(filePath, content);
|
|
892
1101
|
const appVar = detectAppVariable(content) ?? 'app';
|
|
893
1102
|
const snippet = buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, architectureType, collectionOpts);
|
|
1103
|
+
// Collection mode (microservices): append at end of file instead of before
|
|
1104
|
+
// .listen(). Route mounting (app.use(...)) happens synchronously at module
|
|
1105
|
+
// load; .listen() is often gated behind an async bootstrap (DB/broker
|
|
1106
|
+
// connect) that can hang or fail outside its container/network. Appending
|
|
1107
|
+
// at the end runs right after the synchronous portion of the module —
|
|
1108
|
+
// independent of whether that bootstrap ever finishes — so route capture
|
|
1109
|
+
// doesn't require RabbitMQ/Redis/DB to be reachable.
|
|
894
1110
|
const listenRe = /\b(?:app|server|httpServer|http)\s*\.listen\s*\(/;
|
|
895
|
-
const listenMatch = listenRe.exec(content);
|
|
1111
|
+
const listenMatch = collectionOpts ? undefined : listenRe.exec(content);
|
|
896
1112
|
let newContent;
|
|
897
1113
|
let injectionPoint;
|
|
898
1114
|
if (listenMatch?.index !== undefined) {
|
|
@@ -969,13 +1185,33 @@ function buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, a
|
|
|
969
1185
|
` upload: true,`,
|
|
970
1186
|
` runOnce: true,`,
|
|
971
1187
|
].join('\n');
|
|
1188
|
+
// Collection mode: some services only mount routes inside an async bootstrap
|
|
1189
|
+
// gated behind a DB/broker connect (which may hang/fail outside Docker).
|
|
1190
|
+
// Poll briefly for app._router to be populated (Express-shaped apps only)
|
|
1191
|
+
// before giving up and collecting whatever is there — bounded well under
|
|
1192
|
+
// the CLI's 30s per-service timeout so a hung connect still surfaces.
|
|
1193
|
+
const runAnalyze = `APIForge.analyze(${appVar}, {\n${opts}\n })`;
|
|
1194
|
+
const call = collectionOpts
|
|
1195
|
+
? [
|
|
1196
|
+
` let __apiforgeTries = 0`,
|
|
1197
|
+
` const __apiforgeTryCollect = () => {`,
|
|
1198
|
+
` const hasRoutes = !${appVar} || !${appVar}._router`,
|
|
1199
|
+
` ? true`,
|
|
1200
|
+
` : Array.isArray(${appVar}._router.stack) && ${appVar}._router.stack.length > 0`,
|
|
1201
|
+
` if (hasRoutes || __apiforgeTries++ >= 26) {`,
|
|
1202
|
+
` ${runAnalyze}`,
|
|
1203
|
+
` } else {`,
|
|
1204
|
+
` setTimeout(__apiforgeTryCollect, 300)`,
|
|
1205
|
+
` }`,
|
|
1206
|
+
` }`,
|
|
1207
|
+
` __apiforgeTryCollect()`,
|
|
1208
|
+
].join('\n')
|
|
1209
|
+
: ` ${runAnalyze}`;
|
|
972
1210
|
if (isESM) {
|
|
973
1211
|
return [
|
|
974
1212
|
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
975
1213
|
`import('apiforge-analyzer').then(({ default: APIForge }) => {`,
|
|
976
|
-
|
|
977
|
-
opts,
|
|
978
|
-
` })`,
|
|
1214
|
+
call,
|
|
979
1215
|
`}).catch(() => {})`,
|
|
980
1216
|
'',
|
|
981
1217
|
].join('\n');
|
|
@@ -984,9 +1220,7 @@ function buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, a
|
|
|
984
1220
|
`// APIForge — https://apiforge.brainfogagency.com`,
|
|
985
1221
|
`Promise.resolve().then(() => require('apiforge-analyzer')).then(mod => {`,
|
|
986
1222
|
` const APIForge = mod.default || mod`,
|
|
987
|
-
|
|
988
|
-
opts,
|
|
989
|
-
` })`,
|
|
1223
|
+
call,
|
|
990
1224
|
`}).catch(() => {})`,
|
|
991
1225
|
'',
|
|
992
1226
|
].join('\n');
|
package/package.json
CHANGED