@apitap/core 1.5.4 → 1.6.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/dist/cli.js +372 -20
- package/dist/cli.js.map +1 -1
- package/dist/discovery/openapi.js +23 -50
- package/dist/discovery/openapi.js.map +1 -1
- package/dist/replay/engine.d.ts +13 -0
- package/dist/replay/engine.js +20 -0
- package/dist/replay/engine.js.map +1 -1
- package/dist/skill/apis-guru.d.ts +35 -0
- package/dist/skill/apis-guru.js +128 -0
- package/dist/skill/apis-guru.js.map +1 -0
- package/dist/skill/merge.d.ts +29 -0
- package/dist/skill/merge.js +252 -0
- package/dist/skill/merge.js.map +1 -0
- package/dist/skill/openapi-converter.d.ts +31 -0
- package/dist/skill/openapi-converter.js +383 -0
- package/dist/skill/openapi-converter.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/package.json +1 -1
- package/src/cli.ts +400 -18
- package/src/discovery/openapi.ts +25 -56
- package/src/replay/engine.ts +19 -0
- package/src/skill/apis-guru.ts +163 -0
- package/src/skill/merge.ts +281 -0
- package/src/skill/openapi-converter.ts +426 -0
- package/src/types.ts +42 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// src/cli.ts
|
|
3
3
|
import { capture } from './capture/monitor.js';
|
|
4
4
|
import { writeSkillFile, readSkillFile, listSkillFiles } from './skill/store.js';
|
|
5
|
-
import { replayEndpoint } from './replay/engine.js';
|
|
5
|
+
import { replayEndpoint, getConfidenceHint } from './replay/engine.js';
|
|
6
6
|
import { AuthManager, getMachineId } from './auth/manager.js';
|
|
7
7
|
import { deriveSigningKey } from './auth/crypto.js';
|
|
8
8
|
import { signSkillFile } from './skill/signing.js';
|
|
@@ -26,6 +26,9 @@ import { stat, unlink } from 'node:fs/promises';
|
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import { createMcpServer } from './mcp.js';
|
|
28
28
|
import { attach, parseDomainPatterns } from './capture/cdp-attach.js';
|
|
29
|
+
import { isOpenAPISpec, convertOpenAPISpec } from './skill/openapi-converter.js';
|
|
30
|
+
import { mergeSkillFile } from './skill/merge.js';
|
|
31
|
+
import { fetchApisGuruList, filterEntries, fetchSpec } from './skill/apis-guru.js';
|
|
29
32
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
30
33
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
31
34
|
const VERSION = pkg.version;
|
|
@@ -66,7 +69,9 @@ function printUsage() {
|
|
|
66
69
|
apitap show <domain> Show endpoints for a domain
|
|
67
70
|
apitap replay <domain> <endpoint-id> [key=value...]
|
|
68
71
|
Replay an API endpoint
|
|
69
|
-
apitap import <file>
|
|
72
|
+
apitap import <file|url> Import a skill file or OpenAPI spec
|
|
73
|
+
apitap import --from apis-guru
|
|
74
|
+
Bulk-import from APIs.guru directory
|
|
70
75
|
apitap refresh <domain> Refresh auth tokens via browser
|
|
71
76
|
apitap auth [domain] View or manage stored auth
|
|
72
77
|
apitap mcp Run the full ApiTap MCP server over stdio
|
|
@@ -118,6 +123,11 @@ function printUsage() {
|
|
|
118
123
|
|
|
119
124
|
Import options:
|
|
120
125
|
--yes Skip confirmation prompt
|
|
126
|
+
--dry-run Show what would change without writing
|
|
127
|
+
--json Output machine-readable JSON
|
|
128
|
+
--from apis-guru Bulk-import from APIs.guru directory
|
|
129
|
+
--search <term> Filter APIs.guru entries by name/title
|
|
130
|
+
--limit <n> Max APIs to import (default: 100)
|
|
121
131
|
|
|
122
132
|
Serve options:
|
|
123
133
|
--json Output tool list as JSON on stderr
|
|
@@ -408,40 +418,145 @@ async function handleReplay(positional, flags) {
|
|
|
408
418
|
}, null, 2));
|
|
409
419
|
}
|
|
410
420
|
else {
|
|
421
|
+
const hint = endpoint ? getConfidenceHint(endpoint.confidence) : null;
|
|
422
|
+
if (hint) {
|
|
423
|
+
console.error(` Note: ${hint}`);
|
|
424
|
+
}
|
|
411
425
|
console.log(`\n Status: ${result.status}\n`);
|
|
412
426
|
console.log(JSON.stringify(result.data, null, 2));
|
|
413
427
|
console.log();
|
|
414
428
|
}
|
|
415
429
|
}
|
|
416
430
|
async function handleImport(positional, flags) {
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
|
|
431
|
+
// --from apis-guru: bulk import mode
|
|
432
|
+
if (flags['from'] === 'apis-guru') {
|
|
433
|
+
await handleApisGuruImport(flags);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const source = positional[0];
|
|
437
|
+
if (!source) {
|
|
438
|
+
console.error('Error: File path or URL required. Usage: apitap import <file|url>');
|
|
420
439
|
process.exit(1);
|
|
421
440
|
}
|
|
422
441
|
const json = flags.json === true;
|
|
442
|
+
// Reject YAML files with helpful message
|
|
443
|
+
if (/\.ya?ml$/i.test(source)) {
|
|
444
|
+
const msg = 'YAML specs not yet supported. Convert to JSON first: npx swagger-cli bundle spec.yaml -o spec.json';
|
|
445
|
+
if (json) {
|
|
446
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.error(`Error: ${msg}`);
|
|
450
|
+
}
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
// Load content — from URL or file
|
|
454
|
+
let rawText;
|
|
455
|
+
let sourceUrl = source;
|
|
456
|
+
const isUrl = source.startsWith('http://') || source.startsWith('https://');
|
|
457
|
+
if (isUrl) {
|
|
458
|
+
try {
|
|
459
|
+
const ssrfCheck = await resolveAndValidateUrl(source);
|
|
460
|
+
if (!ssrfCheck.safe) {
|
|
461
|
+
throw new Error(`SSRF check failed: ${ssrfCheck.reason}`);
|
|
462
|
+
}
|
|
463
|
+
const response = await fetch(source, { signal: AbortSignal.timeout(30_000) });
|
|
464
|
+
if (!response.ok) {
|
|
465
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
466
|
+
}
|
|
467
|
+
rawText = await response.text();
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
const msg = `Failed to fetch ${source}: ${err.message}`;
|
|
471
|
+
if (json) {
|
|
472
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
console.error(`Error: ${msg}`);
|
|
476
|
+
}
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
try {
|
|
482
|
+
const { readFile } = await import('node:fs/promises');
|
|
483
|
+
rawText = await readFile(source, 'utf-8');
|
|
484
|
+
sourceUrl = `file://${resolve(source)}`;
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
const msg = `Failed to read ${source}: ${err.message}`;
|
|
488
|
+
if (json) {
|
|
489
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.error(`Error: ${msg}`);
|
|
493
|
+
}
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Parse JSON
|
|
498
|
+
let parsed;
|
|
499
|
+
try {
|
|
500
|
+
parsed = JSON.parse(rawText);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
const msg = `Invalid JSON in ${source}`;
|
|
504
|
+
if (json) {
|
|
505
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
console.error(`Error: ${msg}`);
|
|
509
|
+
}
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
// Route: OpenAPI spec vs SkillFile
|
|
513
|
+
if (isOpenAPISpec(parsed)) {
|
|
514
|
+
await handleOpenAPIImport(parsed, sourceUrl, flags);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// --- Existing SkillFile import flow (unchanged) ---
|
|
518
|
+
if (isUrl) {
|
|
519
|
+
// SkillFile imports from URLs: write to temp file first
|
|
520
|
+
const { writeFile: writeTmp } = await import('node:fs/promises');
|
|
521
|
+
const { tmpdir } = await import('node:os');
|
|
522
|
+
const tmpPath = join(tmpdir(), `apitap-import-${Date.now()}.json`);
|
|
523
|
+
await writeTmp(tmpPath, rawText);
|
|
524
|
+
try {
|
|
525
|
+
await handleSkillFileImport(tmpPath, json);
|
|
526
|
+
}
|
|
527
|
+
finally {
|
|
528
|
+
const { unlink: unlinkTmp } = await import('node:fs/promises');
|
|
529
|
+
await unlinkTmp(tmpPath).catch(() => { });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
await handleSkillFileImport(source, json);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function handleSkillFileImport(filePath, json) {
|
|
423
537
|
// Get local key for signature verification
|
|
424
538
|
const machineId = await getMachineId();
|
|
425
539
|
const key = deriveSigningKey(machineId);
|
|
426
540
|
// DNS-resolving SSRF check before importing (prevents DNS rebinding attacks)
|
|
541
|
+
let raw;
|
|
427
542
|
try {
|
|
428
|
-
|
|
429
|
-
if (raw.baseUrl) {
|
|
430
|
-
const dnsCheck = await resolveAndValidateUrl(raw.baseUrl);
|
|
431
|
-
if (!dnsCheck.safe) {
|
|
432
|
-
const msg = `DNS rebinding risk: ${dnsCheck.reason}`;
|
|
433
|
-
if (json) {
|
|
434
|
-
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
console.error(`Error: ${msg}`);
|
|
438
|
-
}
|
|
439
|
-
process.exit(1);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
543
|
+
raw = JSON.parse(await import('node:fs/promises').then(fs => fs.readFile(filePath, 'utf-8')));
|
|
442
544
|
}
|
|
443
545
|
catch {
|
|
444
|
-
// Parse errors will be caught by importSkillFile
|
|
546
|
+
// Parse errors will be caught by importSkillFile below
|
|
547
|
+
}
|
|
548
|
+
if (raw?.baseUrl) {
|
|
549
|
+
const dnsCheck = await resolveAndValidateUrl(raw.baseUrl);
|
|
550
|
+
if (!dnsCheck.safe) {
|
|
551
|
+
const msg = `DNS rebinding risk: ${dnsCheck.reason}`;
|
|
552
|
+
if (json) {
|
|
553
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
console.error(`Error: ${msg}`);
|
|
557
|
+
}
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
445
560
|
}
|
|
446
561
|
const result = await importSkillFile(filePath, undefined, key);
|
|
447
562
|
if (!result.success) {
|
|
@@ -460,6 +575,243 @@ async function handleImport(positional, flags) {
|
|
|
460
575
|
console.log(`\n ✓ Imported skill file: ${result.skillFile}\n`);
|
|
461
576
|
}
|
|
462
577
|
}
|
|
578
|
+
async function handleOpenAPIImport(spec, specUrl, flags) {
|
|
579
|
+
const json = flags.json === true;
|
|
580
|
+
const dryRun = flags['dry-run'] === true;
|
|
581
|
+
const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
|
|
582
|
+
// Convert OpenAPI spec to endpoints
|
|
583
|
+
let importResult;
|
|
584
|
+
try {
|
|
585
|
+
importResult = convertOpenAPISpec(spec, specUrl);
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
const msg = `Failed to convert OpenAPI spec: ${err.message}`;
|
|
589
|
+
if (json) {
|
|
590
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
console.error(`Error: ${msg}`);
|
|
594
|
+
}
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
const { domain, endpoints, meta } = importResult;
|
|
598
|
+
const specVersion = spec.openapi || spec.swagger || 'unknown';
|
|
599
|
+
if (!json) {
|
|
600
|
+
console.log(`\n Importing ${domain} from OpenAPI ${specVersion} spec...\n`);
|
|
601
|
+
}
|
|
602
|
+
// SSRF validate the domain
|
|
603
|
+
const dnsCheck = await resolveAndValidateUrl(`https://${domain}`);
|
|
604
|
+
if (!dnsCheck.safe) {
|
|
605
|
+
const msg = `DNS rebinding risk for ${domain}: ${dnsCheck.reason}`;
|
|
606
|
+
if (json) {
|
|
607
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
console.error(`Error: ${msg}`);
|
|
611
|
+
}
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
// Read existing skill file (if any)
|
|
615
|
+
let existing = null;
|
|
616
|
+
try {
|
|
617
|
+
existing = await readSkillFile(domain, skillsDir, {
|
|
618
|
+
verifySignature: true,
|
|
619
|
+
trustUnsigned: true,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
if (err?.code !== 'ENOENT') {
|
|
624
|
+
if (!json)
|
|
625
|
+
console.error(` Warning: could not read existing skill file for ${domain}: ${err.message}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Merge
|
|
629
|
+
const { skillFile, diff } = mergeSkillFile(existing, endpoints, meta);
|
|
630
|
+
// Ensure domain and baseUrl reflect the API, not the spec source URL
|
|
631
|
+
skillFile.domain = domain;
|
|
632
|
+
skillFile.baseUrl = `https://${domain}`;
|
|
633
|
+
if (dryRun) {
|
|
634
|
+
if (json) {
|
|
635
|
+
console.log(JSON.stringify({
|
|
636
|
+
success: true,
|
|
637
|
+
dryRun: true,
|
|
638
|
+
domain,
|
|
639
|
+
diff,
|
|
640
|
+
totalEndpoints: skillFile.endpoints.length,
|
|
641
|
+
}));
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
printOpenAPIDiff(diff, skillFile, skillsDir);
|
|
645
|
+
console.log(' (dry run — no changes written)\n');
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
// Sign and write
|
|
650
|
+
const machineId = await getMachineId();
|
|
651
|
+
const key = deriveSigningKey(machineId);
|
|
652
|
+
const signed = signSkillFile(skillFile, key);
|
|
653
|
+
const filePath = await writeSkillFile(signed, skillsDir);
|
|
654
|
+
if (json) {
|
|
655
|
+
console.log(JSON.stringify({
|
|
656
|
+
success: true,
|
|
657
|
+
domain,
|
|
658
|
+
skillFile: filePath,
|
|
659
|
+
diff,
|
|
660
|
+
totalEndpoints: signed.endpoints.length,
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
printOpenAPIDiff(diff, signed, filePath);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function printOpenAPIDiff(diff, skillFile, pathOrDir) {
|
|
668
|
+
console.log(` ✓ ${diff.preserved} existing captured endpoints preserved`);
|
|
669
|
+
console.log(` + ${diff.added} new endpoints added from OpenAPI spec`);
|
|
670
|
+
console.log(` ~ ${diff.enriched} endpoints enriched with spec metadata`);
|
|
671
|
+
console.log(` · ${diff.skipped} skipped (already imported)`);
|
|
672
|
+
console.log();
|
|
673
|
+
console.log(` Skill file: ${pathOrDir} (${skillFile.endpoints.length} endpoints)\n`);
|
|
674
|
+
}
|
|
675
|
+
async function handleApisGuruImport(flags) {
|
|
676
|
+
const json = flags.json === true;
|
|
677
|
+
const dryRun = flags['dry-run'] === true;
|
|
678
|
+
const limit = typeof flags.limit === 'string' ? parseInt(flags.limit, 10) : 100;
|
|
679
|
+
const search = typeof flags.search === 'string' ? flags.search : undefined;
|
|
680
|
+
const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
|
|
681
|
+
if (!json) {
|
|
682
|
+
console.log(`\n Importing from APIs.guru (limit: ${limit})...\n`);
|
|
683
|
+
}
|
|
684
|
+
// Fetch and filter
|
|
685
|
+
let entries;
|
|
686
|
+
try {
|
|
687
|
+
const allEntries = await fetchApisGuruList();
|
|
688
|
+
entries = filterEntries(allEntries, { search, limit, preferOpenapi3: true });
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
const msg = `Failed to fetch APIs.guru list: ${err.message}`;
|
|
692
|
+
if (json) {
|
|
693
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
console.error(`Error: ${msg}`);
|
|
697
|
+
}
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
if (entries.length === 0) {
|
|
701
|
+
if (json) {
|
|
702
|
+
console.log(JSON.stringify({ success: true, imported: 0, failed: 0, skipped: 0, totalEndpoints: 0 }));
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
console.log(' No matching APIs found.\n');
|
|
706
|
+
}
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const total = entries.length;
|
|
710
|
+
let imported = 0;
|
|
711
|
+
let failed = 0;
|
|
712
|
+
let skippedApis = 0;
|
|
713
|
+
let totalEndpointsAdded = 0;
|
|
714
|
+
const machineId = await getMachineId();
|
|
715
|
+
const key = deriveSigningKey(machineId);
|
|
716
|
+
const results = [];
|
|
717
|
+
for (let i = 0; i < entries.length; i++) {
|
|
718
|
+
const entry = entries[i];
|
|
719
|
+
const idx = String(i + 1).padStart(String(total).length, ' ');
|
|
720
|
+
try {
|
|
721
|
+
// Fetch spec
|
|
722
|
+
const spec = await fetchSpec(entry.specUrl);
|
|
723
|
+
// Convert
|
|
724
|
+
const importResult = convertOpenAPISpec(spec, entry.specUrl);
|
|
725
|
+
const { domain, endpoints, meta } = importResult;
|
|
726
|
+
if (endpoints.length === 0) {
|
|
727
|
+
if (!json) {
|
|
728
|
+
console.log(` [${idx}/${total}] SKIP ${domain.padEnd(24)} 0 endpoints (${entry.title})`);
|
|
729
|
+
}
|
|
730
|
+
results.push({ index: i + 1, status: 'skip', domain, title: entry.title, endpointsAdded: 0 });
|
|
731
|
+
skippedApis++;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
// SSRF validate
|
|
735
|
+
const dnsCheck = await resolveAndValidateUrl(`https://${domain}`);
|
|
736
|
+
if (!dnsCheck.safe) {
|
|
737
|
+
if (!json) {
|
|
738
|
+
console.log(` [${idx}/${total}] SKIP ${domain.padEnd(24)} SSRF risk (${entry.title})`);
|
|
739
|
+
}
|
|
740
|
+
results.push({ index: i + 1, status: 'skip', domain, title: entry.title, endpointsAdded: 0 });
|
|
741
|
+
skippedApis++;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
// Read existing
|
|
745
|
+
let existing = null;
|
|
746
|
+
try {
|
|
747
|
+
existing = await readSkillFile(domain, skillsDir, {
|
|
748
|
+
verifySignature: true,
|
|
749
|
+
trustUnsigned: true,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
if (err?.code !== 'ENOENT') {
|
|
754
|
+
if (!json)
|
|
755
|
+
console.error(` Warning: could not read existing skill file for ${domain}: ${err.message}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Merge
|
|
759
|
+
const { skillFile, diff } = mergeSkillFile(existing, endpoints, meta);
|
|
760
|
+
// Ensure domain and baseUrl reflect the API, not the spec source URL
|
|
761
|
+
skillFile.domain = domain;
|
|
762
|
+
skillFile.baseUrl = `https://${domain}`;
|
|
763
|
+
if (!dryRun) {
|
|
764
|
+
// Sign and write
|
|
765
|
+
const signed = signSkillFile(skillFile, key);
|
|
766
|
+
await writeSkillFile(signed, skillsDir);
|
|
767
|
+
}
|
|
768
|
+
if (!json) {
|
|
769
|
+
console.log(` [${idx}/${total}] OK ${domain.padEnd(24)} +${diff.added} endpoints (${entry.title})`);
|
|
770
|
+
}
|
|
771
|
+
results.push({ index: i + 1, status: 'ok', domain, title: entry.title, endpointsAdded: diff.added });
|
|
772
|
+
imported++;
|
|
773
|
+
totalEndpointsAdded += diff.added;
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
if (!json) {
|
|
777
|
+
console.log(` [${idx}/${total}] FAIL ${entry.providerName.padEnd(24)} ${err.message}`);
|
|
778
|
+
}
|
|
779
|
+
results.push({
|
|
780
|
+
index: i + 1,
|
|
781
|
+
status: 'fail',
|
|
782
|
+
domain: entry.providerName,
|
|
783
|
+
title: entry.title,
|
|
784
|
+
endpointsAdded: 0,
|
|
785
|
+
error: err.message,
|
|
786
|
+
});
|
|
787
|
+
failed++;
|
|
788
|
+
}
|
|
789
|
+
// Small delay between requests to be polite to APIs.guru
|
|
790
|
+
if (i < entries.length - 1) {
|
|
791
|
+
await new Promise(r => setTimeout(r, 100));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (json) {
|
|
795
|
+
console.log(JSON.stringify({
|
|
796
|
+
success: true,
|
|
797
|
+
dryRun,
|
|
798
|
+
imported,
|
|
799
|
+
failed,
|
|
800
|
+
skipped: skippedApis,
|
|
801
|
+
totalEndpoints: totalEndpointsAdded,
|
|
802
|
+
results,
|
|
803
|
+
}));
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
console.log();
|
|
807
|
+
console.log(` Done: ${imported} imported, ${failed} failed, ${skippedApis} skipped`);
|
|
808
|
+
console.log(` ${totalEndpointsAdded.toLocaleString()} endpoints added across ${imported} APIs`);
|
|
809
|
+
if (dryRun) {
|
|
810
|
+
console.log(' (dry run — no changes written)');
|
|
811
|
+
}
|
|
812
|
+
console.log();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
463
815
|
async function handleRefresh(positional, flags) {
|
|
464
816
|
const domain = positional[0];
|
|
465
817
|
if (!domain) {
|