apiforge-analyzer 1.3.0 → 1.3.1
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 +52 -3
- package/dist/esm/arch-detect.js +21 -0
- package/dist/esm/cli.js +52 -3
- 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.1';
|
|
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,10 @@ 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
|
+
}
|
|
394
398
|
async function microservicesFlow(config, opts) {
|
|
395
399
|
const cwd = process.cwd();
|
|
396
400
|
const allRoutes = [];
|
|
@@ -403,6 +407,29 @@ async function microservicesFlow(config, opts) {
|
|
|
403
407
|
tryUnlink((0, path_1.join)(cwd, STATUS_FILE));
|
|
404
408
|
tryUnlink((0, path_1.join)(cwd, LOCK_FILE));
|
|
405
409
|
persistApiKeyToEnv(opts.apiKey);
|
|
410
|
+
if (hasDockerCompose(cwd)) {
|
|
411
|
+
(0, prompts_1.note)([
|
|
412
|
+
`${yellow('Docker Compose detected — APIForge will NOT start, build, or touch Docker.')}`,
|
|
413
|
+
'',
|
|
414
|
+
dim('Each service below is run directly with node/tsx on this machine, just'),
|
|
415
|
+
dim('long enough to capture its routes, then reverted. Your containers (if'),
|
|
416
|
+
dim('running) are left completely alone — no ports, network, or images are used.'),
|
|
417
|
+
'',
|
|
418
|
+
bold('For this to work:'),
|
|
419
|
+
` ${dim('·')} ${bold('npm install')} ${dim('has been run at the project root (deps must resolve outside Docker)')}`,
|
|
420
|
+
` ${dim('·')} a readable ${bold('.env')} ${dim('at the project root (same values Docker passes via env_file)')}`,
|
|
421
|
+
'',
|
|
422
|
+
dim("Routes are captured before RabbitMQ/Redis/DB connections resolve, so those don't need to be reachable."),
|
|
423
|
+
].join('\n'), 'Docker Compose project');
|
|
424
|
+
const proceed = await (0, prompts_1.confirm)({
|
|
425
|
+
message: 'Run services locally to collect routes?',
|
|
426
|
+
initialValue: true,
|
|
427
|
+
});
|
|
428
|
+
if ((0, prompts_1.isCancel)(proceed) || !proceed) {
|
|
429
|
+
(0, prompts_1.cancel)(dim('Cancelled. Run `npm install` at the project root, then re-run apiforge.'));
|
|
430
|
+
process.exit(0);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
406
433
|
(0, prompts_1.note)([
|
|
407
434
|
bold(`Analyzing ${config.services.length} service${config.services.length > 1 ? 's' : ''}…`),
|
|
408
435
|
'',
|
|
@@ -432,11 +459,22 @@ async function microservicesFlow(config, opts) {
|
|
|
432
459
|
const startCmd = svc.startCommand ?? serviceNodeCommand(fullEntry);
|
|
433
460
|
const child = (0, child_process_1.spawn)(startCmd, {
|
|
434
461
|
cwd,
|
|
435
|
-
stdio: 'ignore',
|
|
462
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
436
463
|
shell: true,
|
|
437
464
|
env: { ...process.env },
|
|
438
465
|
detached: false,
|
|
439
466
|
});
|
|
467
|
+
// Capture output so failures are debuggable without re-running the service by hand.
|
|
468
|
+
// Bounded to the last ~4000 chars so a chatty/looping service can't blow up memory.
|
|
469
|
+
let output = '';
|
|
470
|
+
const MAX_OUTPUT = 4000;
|
|
471
|
+
const capture = (chunk) => {
|
|
472
|
+
output += chunk.toString();
|
|
473
|
+
if (output.length > MAX_OUTPUT)
|
|
474
|
+
output = output.slice(-MAX_OUTPUT);
|
|
475
|
+
};
|
|
476
|
+
child.stdout?.on('data', capture);
|
|
477
|
+
child.stderr?.on('data', capture);
|
|
440
478
|
// Wait up to 30 s for the status file to appear
|
|
441
479
|
const routes = await waitForServiceStatusFile(statusFile, 30000);
|
|
442
480
|
// Kill service process
|
|
@@ -453,6 +491,10 @@ async function microservicesFlow(config, opts) {
|
|
|
453
491
|
}
|
|
454
492
|
else {
|
|
455
493
|
svcSpin.stop(yellow(`◌ ${svc.name} — no routes collected (timeout or error)`));
|
|
494
|
+
const trimmed = output.trim();
|
|
495
|
+
if (trimmed) {
|
|
496
|
+
prompts_1.log.error(dim(` ${bold(svc.name)} output:\n` + trimmed.split('\n').slice(-20).map(l => ` ${l}`).join('\n')));
|
|
497
|
+
}
|
|
456
498
|
}
|
|
457
499
|
// Remove injected code
|
|
458
500
|
removeInjectedCode(fullEntry);
|
|
@@ -896,8 +938,15 @@ function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architec
|
|
|
896
938
|
const isESM = detectESM(filePath, content);
|
|
897
939
|
const appVar = detectAppVariable(content) ?? 'app';
|
|
898
940
|
const snippet = buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, architectureType, collectionOpts);
|
|
941
|
+
// Collection mode (microservices): append at end of file instead of before
|
|
942
|
+
// .listen(). Route mounting (app.use(...)) happens synchronously at module
|
|
943
|
+
// load; .listen() is often gated behind an async bootstrap (DB/broker
|
|
944
|
+
// connect) that can hang or fail outside its container/network. Appending
|
|
945
|
+
// at the end runs right after the synchronous portion of the module —
|
|
946
|
+
// independent of whether that bootstrap ever finishes — so route capture
|
|
947
|
+
// doesn't require RabbitMQ/Redis/DB to be reachable.
|
|
899
948
|
const listenRe = /\b(?:app|server|httpServer|http)\s*\.listen\s*\(/;
|
|
900
|
-
const listenMatch = listenRe.exec(content);
|
|
949
|
+
const listenMatch = collectionOpts ? undefined : listenRe.exec(content);
|
|
901
950
|
let newContent;
|
|
902
951
|
let injectionPoint;
|
|
903
952
|
if (listenMatch?.index !== undefined) {
|
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
|
@@ -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.1';
|
|
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,10 @@ 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
|
+
}
|
|
389
393
|
async function microservicesFlow(config, opts) {
|
|
390
394
|
const cwd = process.cwd();
|
|
391
395
|
const allRoutes = [];
|
|
@@ -398,6 +402,29 @@ async function microservicesFlow(config, opts) {
|
|
|
398
402
|
tryUnlink(join(cwd, STATUS_FILE));
|
|
399
403
|
tryUnlink(join(cwd, LOCK_FILE));
|
|
400
404
|
persistApiKeyToEnv(opts.apiKey);
|
|
405
|
+
if (hasDockerCompose(cwd)) {
|
|
406
|
+
note([
|
|
407
|
+
`${yellow('Docker Compose detected — APIForge will NOT start, build, or touch Docker.')}`,
|
|
408
|
+
'',
|
|
409
|
+
dim('Each service below is run directly with node/tsx on this machine, just'),
|
|
410
|
+
dim('long enough to capture its routes, then reverted. Your containers (if'),
|
|
411
|
+
dim('running) are left completely alone — no ports, network, or images are used.'),
|
|
412
|
+
'',
|
|
413
|
+
bold('For this to work:'),
|
|
414
|
+
` ${dim('·')} ${bold('npm install')} ${dim('has been run at the project root (deps must resolve outside Docker)')}`,
|
|
415
|
+
` ${dim('·')} a readable ${bold('.env')} ${dim('at the project root (same values Docker passes via env_file)')}`,
|
|
416
|
+
'',
|
|
417
|
+
dim("Routes are captured before RabbitMQ/Redis/DB connections resolve, so those don't need to be reachable."),
|
|
418
|
+
].join('\n'), 'Docker Compose project');
|
|
419
|
+
const proceed = await confirm({
|
|
420
|
+
message: 'Run services locally to collect routes?',
|
|
421
|
+
initialValue: true,
|
|
422
|
+
});
|
|
423
|
+
if (isCancel(proceed) || !proceed) {
|
|
424
|
+
cancel(dim('Cancelled. Run `npm install` at the project root, then re-run apiforge.'));
|
|
425
|
+
process.exit(0);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
401
428
|
note([
|
|
402
429
|
bold(`Analyzing ${config.services.length} service${config.services.length > 1 ? 's' : ''}…`),
|
|
403
430
|
'',
|
|
@@ -427,11 +454,22 @@ async function microservicesFlow(config, opts) {
|
|
|
427
454
|
const startCmd = svc.startCommand ?? serviceNodeCommand(fullEntry);
|
|
428
455
|
const child = spawn(startCmd, {
|
|
429
456
|
cwd,
|
|
430
|
-
stdio: 'ignore',
|
|
457
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
431
458
|
shell: true,
|
|
432
459
|
env: { ...process.env },
|
|
433
460
|
detached: false,
|
|
434
461
|
});
|
|
462
|
+
// Capture output so failures are debuggable without re-running the service by hand.
|
|
463
|
+
// Bounded to the last ~4000 chars so a chatty/looping service can't blow up memory.
|
|
464
|
+
let output = '';
|
|
465
|
+
const MAX_OUTPUT = 4000;
|
|
466
|
+
const capture = (chunk) => {
|
|
467
|
+
output += chunk.toString();
|
|
468
|
+
if (output.length > MAX_OUTPUT)
|
|
469
|
+
output = output.slice(-MAX_OUTPUT);
|
|
470
|
+
};
|
|
471
|
+
child.stdout?.on('data', capture);
|
|
472
|
+
child.stderr?.on('data', capture);
|
|
435
473
|
// Wait up to 30 s for the status file to appear
|
|
436
474
|
const routes = await waitForServiceStatusFile(statusFile, 30000);
|
|
437
475
|
// Kill service process
|
|
@@ -448,6 +486,10 @@ async function microservicesFlow(config, opts) {
|
|
|
448
486
|
}
|
|
449
487
|
else {
|
|
450
488
|
svcSpin.stop(yellow(`◌ ${svc.name} — no routes collected (timeout or error)`));
|
|
489
|
+
const trimmed = output.trim();
|
|
490
|
+
if (trimmed) {
|
|
491
|
+
log.error(dim(` ${bold(svc.name)} output:\n` + trimmed.split('\n').slice(-20).map(l => ` ${l}`).join('\n')));
|
|
492
|
+
}
|
|
451
493
|
}
|
|
452
494
|
// Remove injected code
|
|
453
495
|
removeInjectedCode(fullEntry);
|
|
@@ -891,8 +933,15 @@ function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architec
|
|
|
891
933
|
const isESM = detectESM(filePath, content);
|
|
892
934
|
const appVar = detectAppVariable(content) ?? 'app';
|
|
893
935
|
const snippet = buildSnippet(projectName, apiKey, baseUrl, backendUrl, appVar, isESM, architectureType, collectionOpts);
|
|
936
|
+
// Collection mode (microservices): append at end of file instead of before
|
|
937
|
+
// .listen(). Route mounting (app.use(...)) happens synchronously at module
|
|
938
|
+
// load; .listen() is often gated behind an async bootstrap (DB/broker
|
|
939
|
+
// connect) that can hang or fail outside its container/network. Appending
|
|
940
|
+
// at the end runs right after the synchronous portion of the module —
|
|
941
|
+
// independent of whether that bootstrap ever finishes — so route capture
|
|
942
|
+
// doesn't require RabbitMQ/Redis/DB to be reachable.
|
|
894
943
|
const listenRe = /\b(?:app|server|httpServer|http)\s*\.listen\s*\(/;
|
|
895
|
-
const listenMatch = listenRe.exec(content);
|
|
944
|
+
const listenMatch = collectionOpts ? undefined : listenRe.exec(content);
|
|
896
945
|
let newContent;
|
|
897
946
|
let injectionPoint;
|
|
898
947
|
if (listenMatch?.index !== undefined) {
|
package/package.json
CHANGED