apiforge-analyzer 1.3.5 → 1.3.7
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/cli.js +77 -6
- package/dist/esm/cli.js +77 -6
- package/package.json +1 -1
package/dist/cjs/cli.js
CHANGED
|
@@ -205,7 +205,9 @@ async function main() {
|
|
|
205
205
|
return `${yellow('◌')} Already configured — no changes made`;
|
|
206
206
|
}
|
|
207
207
|
return [
|
|
208
|
-
|
|
208
|
+
result.updated
|
|
209
|
+
? `${green('✓')} Updated ${bold(entryFile)} ${dim('(project name/API key changed since last run)')}`
|
|
210
|
+
: `${green('✓')} Patched ${bold(entryFile)} ${dim(`+${linesAdded} lines`)}`,
|
|
209
211
|
` ${green('✓')} API key saved to ${bold('.env.local')} ${dim('(gitignored)')}`,
|
|
210
212
|
].join('\n');
|
|
211
213
|
},
|
|
@@ -668,6 +670,7 @@ async function microservicesFlow(config, opts) {
|
|
|
668
670
|
// Kill service process (and its full descendant tree — see killProcessTree)
|
|
669
671
|
if (child.pid)
|
|
670
672
|
killProcessTree(child.pid);
|
|
673
|
+
await killOrphanFromEaddrinuse(output);
|
|
671
674
|
tryUnlink((0, path_1.join)(cwd, statusFile));
|
|
672
675
|
if (routes && routes.length > 0) {
|
|
673
676
|
const prefix = gatewayPrefixes[svc.name];
|
|
@@ -796,6 +799,46 @@ function killProcessTree(pid) {
|
|
|
796
799
|
catch { }
|
|
797
800
|
}
|
|
798
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* nodemon manages its own child process in a way that doesn't always show
|
|
804
|
+
* up as a descendant of the shell pid `killProcessTree` targets —
|
|
805
|
+
* `taskkill /T` walks the OS-reported parent/child tree, but nodemon's
|
|
806
|
+
* actual worker sometimes isn't linked into that tree on Windows, so it
|
|
807
|
+
* survives our kill and keeps holding its port. That port then collides
|
|
808
|
+
* with the very next run (EADDRINUSE), which gets killed the same
|
|
809
|
+
* incomplete way, and orphans keep piling up across retries.
|
|
810
|
+
*
|
|
811
|
+
* Rather than guess at process relationships, read the actual port back
|
|
812
|
+
* out of "EADDRINUSE...:<port>" in the crashed server's own output (Node
|
|
813
|
+
* always includes it) and kill whatever OS-level listener currently holds
|
|
814
|
+
* that port — this is exact, not a heuristic, and clears the pileup so the
|
|
815
|
+
* next run isn't born already colliding with the previous one.
|
|
816
|
+
*/
|
|
817
|
+
function killOrphanFromEaddrinuse(serverOutput) {
|
|
818
|
+
return new Promise(resolve => {
|
|
819
|
+
if (process.platform !== 'win32')
|
|
820
|
+
return resolve();
|
|
821
|
+
const match = serverOutput.match(/EADDRINUSE.*?:(\d+)\b/);
|
|
822
|
+
if (!match)
|
|
823
|
+
return resolve();
|
|
824
|
+
const port = match[1];
|
|
825
|
+
const script = `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`;
|
|
826
|
+
try {
|
|
827
|
+
// Must be awaited (not fire-and-forget) — the CLI process exits right
|
|
828
|
+
// after calling this, and an un-awaited spawn() can get cut off
|
|
829
|
+
// before PowerShell finishes actually killing anything, silently
|
|
830
|
+
// undoing the whole point of this cleanup.
|
|
831
|
+
const ps = (0, child_process_1.spawn)('powershell', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
|
|
832
|
+
ps.on('exit', () => resolve());
|
|
833
|
+
ps.on('error', () => resolve());
|
|
834
|
+
// Don't let a hung PowerShell block the CLI from ever exiting.
|
|
835
|
+
setTimeout(resolve, 5000);
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
resolve();
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
799
842
|
const NODEMON_IGNORE_PATTERNS = ['.apiforge-status.json', '.apiforge-last-run.json', '.env.local'];
|
|
800
843
|
/**
|
|
801
844
|
* If the project depends on nodemon, make sure it won't watch our own
|
|
@@ -875,7 +918,9 @@ function startAndWaitForAnalysis(command, timeoutMs) {
|
|
|
875
918
|
clearTimeout(timeout);
|
|
876
919
|
if (child.pid)
|
|
877
920
|
killProcessTree(child.pid);
|
|
878
|
-
|
|
921
|
+
killOrphanFromEaddrinuse(serverOutput).then(() => {
|
|
922
|
+
resolve({ ...result, serverOutput: serverOutput.trim() });
|
|
923
|
+
});
|
|
879
924
|
}
|
|
880
925
|
// Poll for the status file every 500 ms
|
|
881
926
|
const poller = setInterval(() => {
|
|
@@ -1228,11 +1273,37 @@ function detectScripts() {
|
|
|
1228
1273
|
return [];
|
|
1229
1274
|
}
|
|
1230
1275
|
}
|
|
1276
|
+
/** Reads `field: 'value'` out of an already-injected APIForge block. */
|
|
1277
|
+
function extractInjectedField(block, field) {
|
|
1278
|
+
const m = new RegExp(`${field}:\\s*'([^']*)'`).exec(block);
|
|
1279
|
+
return m?.[1];
|
|
1280
|
+
}
|
|
1231
1281
|
function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architectureType, collectionOpts) {
|
|
1232
1282
|
const fullPath = (0, path_1.join)(process.cwd(), filePath);
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1283
|
+
let content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
1284
|
+
const startIdx = content.indexOf('// APIForge');
|
|
1285
|
+
let wasStale = false;
|
|
1286
|
+
if (startIdx !== -1) {
|
|
1287
|
+
const afterStart = content.slice(startIdx);
|
|
1288
|
+
const endMatch = /\.catch\s*\(\s*\(\s*\)\s*=>\s*\{?\s*\}?\s*\)\s*\n?/.exec(afterStart);
|
|
1289
|
+
const block = endMatch ? afterStart.slice(0, endMatch.index + endMatch[0].length) : afterStart;
|
|
1290
|
+
const existingKey = extractInjectedField(block, 'apiKey');
|
|
1291
|
+
const existingName = extractInjectedField(block, 'projectName');
|
|
1292
|
+
// Same project name and API key as last time — nothing to do.
|
|
1293
|
+
if (existingKey === apiKey && existingName === projectName) {
|
|
1294
|
+
return { alreadyInjected: true };
|
|
1295
|
+
}
|
|
1296
|
+
// Prompt answers changed since this file was last configured. Silently
|
|
1297
|
+
// keeping the old block meant a stale (sometimes since-deleted) API key
|
|
1298
|
+
// stayed hardcoded forever — every later run kept uploading with it no
|
|
1299
|
+
// matter what was typed at the prompt. Re-inject fresh instead.
|
|
1300
|
+
if (endMatch) {
|
|
1301
|
+
const endIdx = startIdx + endMatch.index + endMatch[0].length;
|
|
1302
|
+
content = (content.slice(0, startIdx) + content.slice(endIdx))
|
|
1303
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
1304
|
+
.trimEnd() + '\n';
|
|
1305
|
+
wasStale = true;
|
|
1306
|
+
}
|
|
1236
1307
|
}
|
|
1237
1308
|
const isESM = detectESM(filePath, content);
|
|
1238
1309
|
const appVar = detectAppVariable(content) ?? 'app';
|
|
@@ -1258,7 +1329,7 @@ function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architec
|
|
|
1258
1329
|
injectionPoint = 'end of file';
|
|
1259
1330
|
}
|
|
1260
1331
|
(0, fs_1.writeFileSync)(fullPath, newContent, 'utf-8');
|
|
1261
|
-
return { alreadyInjected: false, injectionPoint, linesAdded: snippet.split('\n').length };
|
|
1332
|
+
return { alreadyInjected: false, injectionPoint, linesAdded: snippet.split('\n').length, updated: wasStale };
|
|
1262
1333
|
}
|
|
1263
1334
|
/**
|
|
1264
1335
|
* Write (or overwrite) the API key in .env.local.
|
package/dist/esm/cli.js
CHANGED
|
@@ -200,7 +200,9 @@ async function main() {
|
|
|
200
200
|
return `${yellow('◌')} Already configured — no changes made`;
|
|
201
201
|
}
|
|
202
202
|
return [
|
|
203
|
-
|
|
203
|
+
result.updated
|
|
204
|
+
? `${green('✓')} Updated ${bold(entryFile)} ${dim('(project name/API key changed since last run)')}`
|
|
205
|
+
: `${green('✓')} Patched ${bold(entryFile)} ${dim(`+${linesAdded} lines`)}`,
|
|
204
206
|
` ${green('✓')} API key saved to ${bold('.env.local')} ${dim('(gitignored)')}`,
|
|
205
207
|
].join('\n');
|
|
206
208
|
},
|
|
@@ -663,6 +665,7 @@ async function microservicesFlow(config, opts) {
|
|
|
663
665
|
// Kill service process (and its full descendant tree — see killProcessTree)
|
|
664
666
|
if (child.pid)
|
|
665
667
|
killProcessTree(child.pid);
|
|
668
|
+
await killOrphanFromEaddrinuse(output);
|
|
666
669
|
tryUnlink(join(cwd, statusFile));
|
|
667
670
|
if (routes && routes.length > 0) {
|
|
668
671
|
const prefix = gatewayPrefixes[svc.name];
|
|
@@ -791,6 +794,46 @@ function killProcessTree(pid) {
|
|
|
791
794
|
catch { }
|
|
792
795
|
}
|
|
793
796
|
}
|
|
797
|
+
/**
|
|
798
|
+
* nodemon manages its own child process in a way that doesn't always show
|
|
799
|
+
* up as a descendant of the shell pid `killProcessTree` targets —
|
|
800
|
+
* `taskkill /T` walks the OS-reported parent/child tree, but nodemon's
|
|
801
|
+
* actual worker sometimes isn't linked into that tree on Windows, so it
|
|
802
|
+
* survives our kill and keeps holding its port. That port then collides
|
|
803
|
+
* with the very next run (EADDRINUSE), which gets killed the same
|
|
804
|
+
* incomplete way, and orphans keep piling up across retries.
|
|
805
|
+
*
|
|
806
|
+
* Rather than guess at process relationships, read the actual port back
|
|
807
|
+
* out of "EADDRINUSE...:<port>" in the crashed server's own output (Node
|
|
808
|
+
* always includes it) and kill whatever OS-level listener currently holds
|
|
809
|
+
* that port — this is exact, not a heuristic, and clears the pileup so the
|
|
810
|
+
* next run isn't born already colliding with the previous one.
|
|
811
|
+
*/
|
|
812
|
+
function killOrphanFromEaddrinuse(serverOutput) {
|
|
813
|
+
return new Promise(resolve => {
|
|
814
|
+
if (process.platform !== 'win32')
|
|
815
|
+
return resolve();
|
|
816
|
+
const match = serverOutput.match(/EADDRINUSE.*?:(\d+)\b/);
|
|
817
|
+
if (!match)
|
|
818
|
+
return resolve();
|
|
819
|
+
const port = match[1];
|
|
820
|
+
const script = `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`;
|
|
821
|
+
try {
|
|
822
|
+
// Must be awaited (not fire-and-forget) — the CLI process exits right
|
|
823
|
+
// after calling this, and an un-awaited spawn() can get cut off
|
|
824
|
+
// before PowerShell finishes actually killing anything, silently
|
|
825
|
+
// undoing the whole point of this cleanup.
|
|
826
|
+
const ps = spawn('powershell', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
|
|
827
|
+
ps.on('exit', () => resolve());
|
|
828
|
+
ps.on('error', () => resolve());
|
|
829
|
+
// Don't let a hung PowerShell block the CLI from ever exiting.
|
|
830
|
+
setTimeout(resolve, 5000);
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
resolve();
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
}
|
|
794
837
|
const NODEMON_IGNORE_PATTERNS = ['.apiforge-status.json', '.apiforge-last-run.json', '.env.local'];
|
|
795
838
|
/**
|
|
796
839
|
* If the project depends on nodemon, make sure it won't watch our own
|
|
@@ -870,7 +913,9 @@ function startAndWaitForAnalysis(command, timeoutMs) {
|
|
|
870
913
|
clearTimeout(timeout);
|
|
871
914
|
if (child.pid)
|
|
872
915
|
killProcessTree(child.pid);
|
|
873
|
-
|
|
916
|
+
killOrphanFromEaddrinuse(serverOutput).then(() => {
|
|
917
|
+
resolve({ ...result, serverOutput: serverOutput.trim() });
|
|
918
|
+
});
|
|
874
919
|
}
|
|
875
920
|
// Poll for the status file every 500 ms
|
|
876
921
|
const poller = setInterval(() => {
|
|
@@ -1223,11 +1268,37 @@ function detectScripts() {
|
|
|
1223
1268
|
return [];
|
|
1224
1269
|
}
|
|
1225
1270
|
}
|
|
1271
|
+
/** Reads `field: 'value'` out of an already-injected APIForge block. */
|
|
1272
|
+
function extractInjectedField(block, field) {
|
|
1273
|
+
const m = new RegExp(`${field}:\\s*'([^']*)'`).exec(block);
|
|
1274
|
+
return m?.[1];
|
|
1275
|
+
}
|
|
1226
1276
|
function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architectureType, collectionOpts) {
|
|
1227
1277
|
const fullPath = join(process.cwd(), filePath);
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1278
|
+
let content = readFileSync(fullPath, 'utf-8');
|
|
1279
|
+
const startIdx = content.indexOf('// APIForge');
|
|
1280
|
+
let wasStale = false;
|
|
1281
|
+
if (startIdx !== -1) {
|
|
1282
|
+
const afterStart = content.slice(startIdx);
|
|
1283
|
+
const endMatch = /\.catch\s*\(\s*\(\s*\)\s*=>\s*\{?\s*\}?\s*\)\s*\n?/.exec(afterStart);
|
|
1284
|
+
const block = endMatch ? afterStart.slice(0, endMatch.index + endMatch[0].length) : afterStart;
|
|
1285
|
+
const existingKey = extractInjectedField(block, 'apiKey');
|
|
1286
|
+
const existingName = extractInjectedField(block, 'projectName');
|
|
1287
|
+
// Same project name and API key as last time — nothing to do.
|
|
1288
|
+
if (existingKey === apiKey && existingName === projectName) {
|
|
1289
|
+
return { alreadyInjected: true };
|
|
1290
|
+
}
|
|
1291
|
+
// Prompt answers changed since this file was last configured. Silently
|
|
1292
|
+
// keeping the old block meant a stale (sometimes since-deleted) API key
|
|
1293
|
+
// stayed hardcoded forever — every later run kept uploading with it no
|
|
1294
|
+
// matter what was typed at the prompt. Re-inject fresh instead.
|
|
1295
|
+
if (endMatch) {
|
|
1296
|
+
const endIdx = startIdx + endMatch.index + endMatch[0].length;
|
|
1297
|
+
content = (content.slice(0, startIdx) + content.slice(endIdx))
|
|
1298
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
1299
|
+
.trimEnd() + '\n';
|
|
1300
|
+
wasStale = true;
|
|
1301
|
+
}
|
|
1231
1302
|
}
|
|
1232
1303
|
const isESM = detectESM(filePath, content);
|
|
1233
1304
|
const appVar = detectAppVariable(content) ?? 'app';
|
|
@@ -1253,7 +1324,7 @@ function injectCode(filePath, projectName, apiKey, baseUrl, backendUrl, architec
|
|
|
1253
1324
|
injectionPoint = 'end of file';
|
|
1254
1325
|
}
|
|
1255
1326
|
writeFileSync(fullPath, newContent, 'utf-8');
|
|
1256
|
-
return { alreadyInjected: false, injectionPoint, linesAdded: snippet.split('\n').length };
|
|
1327
|
+
return { alreadyInjected: false, injectionPoint, linesAdded: snippet.split('\n').length, updated: wasStale };
|
|
1257
1328
|
}
|
|
1258
1329
|
/**
|
|
1259
1330
|
* Write (or overwrite) the API key in .env.local.
|
package/package.json
CHANGED