@apifuse/connector-sdk 2.0.0-beta.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.
Files changed (67) hide show
  1. package/README.md +44 -0
  2. package/bin/apifuse-check.ts +408 -0
  3. package/bin/apifuse-dev.ts +222 -0
  4. package/bin/apifuse-init.ts +390 -0
  5. package/bin/apifuse-perf.ts +1101 -0
  6. package/bin/apifuse-record.ts +446 -0
  7. package/bin/apifuse-test.ts +688 -0
  8. package/bin/apifuse.ts +51 -0
  9. package/package.json +64 -0
  10. package/src/__tests__/auth.test.ts +396 -0
  11. package/src/__tests__/browser-auth.test.ts +180 -0
  12. package/src/__tests__/browser.test.ts +632 -0
  13. package/src/__tests__/connectors-yaml.test.ts +135 -0
  14. package/src/__tests__/define.test.ts +225 -0
  15. package/src/__tests__/errors.test.ts +69 -0
  16. package/src/__tests__/executor.test.ts +214 -0
  17. package/src/__tests__/http.test.ts +238 -0
  18. package/src/__tests__/insights.test.ts +210 -0
  19. package/src/__tests__/instrumentation.test.ts +290 -0
  20. package/src/__tests__/otlp.test.ts +141 -0
  21. package/src/__tests__/perf.test.ts +60 -0
  22. package/src/__tests__/proxy.test.ts +359 -0
  23. package/src/__tests__/recipes.test.ts +36 -0
  24. package/src/__tests__/serve.test.ts +233 -0
  25. package/src/__tests__/session.test.ts +231 -0
  26. package/src/__tests__/state.test.ts +100 -0
  27. package/src/__tests__/stealth.test.ts +57 -0
  28. package/src/__tests__/testing.test.ts +97 -0
  29. package/src/__tests__/tls.test.ts +345 -0
  30. package/src/__tests__/types.test.ts +142 -0
  31. package/src/__tests__/utils.test.ts +62 -0
  32. package/src/__tests__/waterfall.test.ts +270 -0
  33. package/src/config/connectors-yaml.ts +373 -0
  34. package/src/config/loader.ts +122 -0
  35. package/src/define.ts +137 -0
  36. package/src/dev.ts +38 -0
  37. package/src/errors.ts +68 -0
  38. package/src/index.test.ts +1 -0
  39. package/src/index.ts +100 -0
  40. package/src/protocol.ts +183 -0
  41. package/src/recipes/gov-api.ts +97 -0
  42. package/src/recipes/rest-api.ts +152 -0
  43. package/src/runtime/auth.ts +245 -0
  44. package/src/runtime/browser.ts +724 -0
  45. package/src/runtime/connector.ts +20 -0
  46. package/src/runtime/executor.ts +51 -0
  47. package/src/runtime/http.ts +248 -0
  48. package/src/runtime/insights.ts +456 -0
  49. package/src/runtime/instrumentation.ts +424 -0
  50. package/src/runtime/otlp.ts +171 -0
  51. package/src/runtime/perf.ts +73 -0
  52. package/src/runtime/session.ts +573 -0
  53. package/src/runtime/state.ts +124 -0
  54. package/src/runtime/tls.ts +410 -0
  55. package/src/runtime/trace.ts +261 -0
  56. package/src/runtime/waterfall.ts +245 -0
  57. package/src/serve.ts +665 -0
  58. package/src/stealth/profiles.ts +391 -0
  59. package/src/testing/helpers.ts +144 -0
  60. package/src/testing/index.ts +2 -0
  61. package/src/testing/run.ts +88 -0
  62. package/src/types/playwright-stealth.d.ts +9 -0
  63. package/src/types.ts +243 -0
  64. package/src/utils/date.ts +163 -0
  65. package/src/utils/parse.ts +66 -0
  66. package/src/utils/text.ts +20 -0
  67. package/src/utils/transform.ts +62 -0
@@ -0,0 +1,688 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
5
+ import { basename, dirname, relative, resolve } from "node:path";
6
+
7
+ type CliArgs = {
8
+ connectorPath?: string;
9
+ isJson: boolean;
10
+ isVerbose: boolean;
11
+ };
12
+
13
+ type ConnectorLocation = {
14
+ inputPath: string;
15
+ rootDir: string;
16
+ testFilePath: string;
17
+ rawFixturePath?: string;
18
+ label: string;
19
+ };
20
+
21
+ type CommandResult = {
22
+ exitCode: number;
23
+ stdout: string;
24
+ stderr: string;
25
+ };
26
+
27
+ type TestSummary = {
28
+ durationText?: string;
29
+ errorCount: number;
30
+ expectCallCount?: number;
31
+ failedCount: number;
32
+ fileCount?: number;
33
+ passedCount: number;
34
+ totalCount?: number;
35
+ };
36
+
37
+ type ActionableError = {
38
+ candidateRawField?: string;
39
+ expected?: string;
40
+ field?: string;
41
+ fixHint: string;
42
+ received?: string;
43
+ testName?: string;
44
+ type: "zod";
45
+ };
46
+
47
+ async function main() {
48
+ try {
49
+ const args = parseArgs(process.argv.slice(2));
50
+ const location = resolveConnectorLocation(args.connectorPath);
51
+
52
+ if (args.isVerbose && !args.isJson) {
53
+ console.log(`[apifuse test] Connector: ${location.label}`);
54
+ console.log(`[apifuse test] Path: ${location.rootDir}`);
55
+ }
56
+
57
+ const result = await runConnectorTests(location.rootDir, args.isJson);
58
+ const combinedOutput = [result.stdout, result.stderr]
59
+ .filter(Boolean)
60
+ .join("\n");
61
+ const summary = parseTestSummary(combinedOutput);
62
+ const actionableError = parseActionableError(
63
+ combinedOutput,
64
+ location.rawFixturePath,
65
+ );
66
+
67
+ if (args.isJson) {
68
+ const payload = {
69
+ success: result.exitCode === 0,
70
+ connector: {
71
+ id: location.label,
72
+ inputPath: location.inputPath,
73
+ rootDir: location.rootDir,
74
+ testFilePath: location.testFilePath,
75
+ },
76
+ exitCode: result.exitCode,
77
+ summary: {
78
+ duration: summary.durationText,
79
+ errors: summary.errorCount,
80
+ expectCalls: summary.expectCallCount,
81
+ failed: summary.failedCount,
82
+ files: summary.fileCount,
83
+ passed: summary.passedCount,
84
+ total: summary.totalCount,
85
+ },
86
+ actionableErrors: actionableError ? [actionableError] : [],
87
+ ...(args.isVerbose
88
+ ? {
89
+ output: {
90
+ stderr: result.stderr,
91
+ stdout: result.stdout,
92
+ },
93
+ }
94
+ : {}),
95
+ };
96
+
97
+ console.log(JSON.stringify(payload, null, 2));
98
+ process.exit(result.exitCode);
99
+ }
100
+
101
+ printTextSummary({
102
+ actionableError,
103
+ connectorLabel: location.label,
104
+ exitCode: result.exitCode,
105
+ summary,
106
+ });
107
+
108
+ process.exit(result.exitCode);
109
+ } catch (error) {
110
+ handleCliError(error);
111
+ }
112
+ }
113
+
114
+ function parseArgs(argv: string[]): CliArgs {
115
+ let connectorPath: string | undefined;
116
+ let isJson = false;
117
+ let isVerbose = false;
118
+
119
+ for (const arg of argv) {
120
+ if (arg === "--json") {
121
+ isJson = true;
122
+ continue;
123
+ }
124
+
125
+ if (arg === "--verbose" || arg === "-v") {
126
+ isVerbose = true;
127
+ continue;
128
+ }
129
+
130
+ if (arg.startsWith("-")) {
131
+ throw new Error(`Unknown option: ${arg}`);
132
+ }
133
+
134
+ if (!connectorPath) {
135
+ connectorPath = arg;
136
+ continue;
137
+ }
138
+
139
+ throw new Error(`Unexpected argument: ${arg}`);
140
+ }
141
+
142
+ return { connectorPath, isJson, isVerbose };
143
+ }
144
+
145
+ function resolveConnectorLocation(inputPath?: string): ConnectorLocation {
146
+ const originalInput = inputPath ?? process.cwd();
147
+ const resolvedInput = resolve(process.cwd(), originalInput);
148
+
149
+ if (!existsSync(resolvedInput)) {
150
+ throw new Error(`Connector path not found: ${originalInput}`);
151
+ }
152
+
153
+ const initialDirectory = statSync(resolvedInput).isDirectory()
154
+ ? resolvedInput
155
+ : dirname(resolvedInput);
156
+ const connectorRoot =
157
+ findConnectorRoot(initialDirectory) ??
158
+ autoDetectSingleConnector(initialDirectory, originalInput);
159
+
160
+ const testFilePath = resolve(connectorRoot, "__tests__", "index.test.ts");
161
+ if (!existsSync(testFilePath)) {
162
+ throw new Error(
163
+ `Connector tests not found: ${relativeFromCwd(testFilePath)}. Expected __tests__/index.test.ts.`,
164
+ );
165
+ }
166
+
167
+ const rawFixturePath = resolve(connectorRoot, "__fixtures__", "raw.json");
168
+
169
+ return {
170
+ inputPath: originalInput,
171
+ label: basename(connectorRoot),
172
+ rootDir: connectorRoot,
173
+ rawFixturePath: existsSync(rawFixturePath) ? rawFixturePath : undefined,
174
+ testFilePath,
175
+ };
176
+ }
177
+
178
+ function findConnectorRoot(startDirectory: string): string | undefined {
179
+ let currentDirectory = startDirectory;
180
+
181
+ while (true) {
182
+ if (looksLikeConnectorRoot(currentDirectory)) {
183
+ return currentDirectory;
184
+ }
185
+
186
+ const parentDirectory = dirname(currentDirectory);
187
+ if (parentDirectory === currentDirectory) {
188
+ return undefined;
189
+ }
190
+
191
+ currentDirectory = parentDirectory;
192
+ }
193
+ }
194
+
195
+ function autoDetectSingleConnector(
196
+ searchDirectory: string,
197
+ originalInput: string,
198
+ ): string {
199
+ const matches = collectConnectorRoots(searchDirectory);
200
+
201
+ if (matches.length === 1) {
202
+ const [firstMatch] = matches;
203
+ if (firstMatch) {
204
+ return firstMatch;
205
+ }
206
+ }
207
+
208
+ if (matches.length > 1) {
209
+ throw new Error(
210
+ [
211
+ `Multiple connectors found under ${originalInput}.`,
212
+ "Pass an explicit connector path, for example:",
213
+ ...matches.map((match) => ` - apifuse test ${relativeFromCwd(match)}`),
214
+ ].join("\n"),
215
+ );
216
+ }
217
+
218
+ throw new Error(
219
+ [
220
+ `Could not find a connector under ${originalInput}.`,
221
+ "Expected a directory containing:",
222
+ " - manifest.json",
223
+ " - index.ts",
224
+ " - __tests__/index.test.ts",
225
+ ].join("\n"),
226
+ );
227
+ }
228
+
229
+ function collectConnectorRoots(directory: string): string[] {
230
+ const matches: string[] = [];
231
+ const seen = new Set<string>();
232
+ const queue = [directory];
233
+
234
+ while (queue.length > 0) {
235
+ const currentDirectory = queue.shift();
236
+ if (!currentDirectory || seen.has(currentDirectory)) {
237
+ continue;
238
+ }
239
+
240
+ seen.add(currentDirectory);
241
+
242
+ if (looksLikeConnectorRoot(currentDirectory)) {
243
+ matches.push(currentDirectory);
244
+ continue;
245
+ }
246
+
247
+ for (const entry of readdirSync(currentDirectory, {
248
+ withFileTypes: true,
249
+ })) {
250
+ if (!entry.isDirectory()) {
251
+ continue;
252
+ }
253
+
254
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) {
255
+ continue;
256
+ }
257
+
258
+ queue.push(resolve(currentDirectory, entry.name));
259
+ }
260
+ }
261
+
262
+ return matches;
263
+ }
264
+
265
+ function looksLikeConnectorRoot(directory: string): boolean {
266
+ return [
267
+ resolve(directory, "manifest.json"),
268
+ resolve(directory, "index.ts"),
269
+ resolve(directory, "__tests__", "index.test.ts"),
270
+ ].every((filePath) => existsSync(filePath));
271
+ }
272
+
273
+ function relativeFromCwd(filePath: string): string {
274
+ const relativePath = relative(process.cwd(), filePath);
275
+ return relativePath || ".";
276
+ }
277
+
278
+ async function runConnectorTests(
279
+ connectorRoot: string,
280
+ isJson: boolean,
281
+ ): Promise<CommandResult> {
282
+ return await new Promise<CommandResult>((resolveResult, reject) => {
283
+ const child = spawn("bun", ["test"], {
284
+ cwd: connectorRoot,
285
+ stdio: ["inherit", "pipe", "pipe"],
286
+ });
287
+
288
+ let stdout = "";
289
+ let stderr = "";
290
+
291
+ child.stdout.on("data", (chunk: Buffer | string) => {
292
+ const text = chunk.toString();
293
+ stdout += text;
294
+ if (!isJson) {
295
+ process.stdout.write(text);
296
+ }
297
+ });
298
+
299
+ child.stderr.on("data", (chunk: Buffer | string) => {
300
+ const text = chunk.toString();
301
+ stderr += text;
302
+ if (!isJson) {
303
+ process.stderr.write(text);
304
+ }
305
+ });
306
+
307
+ child.on("error", (error) => {
308
+ reject(new Error(`Failed to start bun test: ${error.message}`));
309
+ });
310
+
311
+ child.on("close", (code, signal) => {
312
+ if (signal) {
313
+ reject(new Error(`bun test terminated by signal: ${signal}`));
314
+ return;
315
+ }
316
+
317
+ resolveResult({
318
+ exitCode: code ?? 1,
319
+ stderr,
320
+ stdout,
321
+ });
322
+ });
323
+ });
324
+ }
325
+
326
+ function parseTestSummary(output: string): TestSummary {
327
+ const failedCount =
328
+ extractLastNumber(output, /(^|\n)\s*(\d+)\s+fail\b/gm, 2) ?? 0;
329
+ const errorCount =
330
+ extractLastNumber(output, /(^|\n)\s*(\d+)\s+error\b/gm, 2) ?? 0;
331
+ const passedCount =
332
+ extractLastNumber(output, /(^|\n)\s*(\d+)\s+pass\b/gm, 2) ?? 0;
333
+ const expectCallCount = extractLastNumber(
334
+ output,
335
+ /(^|\n)\s*(\d+)\s+expect\(\)\s+calls?\b/gm,
336
+ 2,
337
+ );
338
+ const runMatch = extractLastMatch(
339
+ output,
340
+ /Ran\s+(\d+)\s+tests?\s+across\s+(\d+)\s+files?\.\s+\[(.+?)\]/g,
341
+ );
342
+
343
+ return {
344
+ durationText: runMatch?.[3],
345
+ errorCount,
346
+ expectCallCount,
347
+ failedCount,
348
+ fileCount: runMatch?.[2] ? Number(runMatch[2]) : undefined,
349
+ passedCount,
350
+ totalCount: runMatch?.[1] ? Number(runMatch[1]) : undefined,
351
+ };
352
+ }
353
+
354
+ function parseActionableError(
355
+ output: string,
356
+ rawFixturePath?: string,
357
+ ): ActionableError | undefined {
358
+ const issue = extractLastZodIssue(output);
359
+ if (!issue) {
360
+ return undefined;
361
+ }
362
+
363
+ const pathSegments = Array.isArray(issue.path) ? issue.path : [];
364
+ const field = formatIssuePath(pathSegments);
365
+ const lastPathSegment = pathSegments.at(-1);
366
+ const fieldName =
367
+ typeof lastPathSegment === "string" ? lastPathSegment : undefined;
368
+ const candidateRawField = fieldName
369
+ ? findCandidateRawField(fieldName, rawFixturePath)
370
+ : undefined;
371
+ const fixLines = [
372
+ fieldName
373
+ ? `transformResponse에서 ${fieldName} 필드를 빠뜨리지 않았는지 확인하세요.`
374
+ : "transformResponse에서 누락된 필드 매핑이 없는지 확인하세요.",
375
+ candidateRawField && candidateRawField !== fieldName
376
+ ? `raw fixture에서 해당 필드명은 "${candidateRawField}"일 수 있습니다.`
377
+ : undefined,
378
+ ].filter(Boolean);
379
+
380
+ return {
381
+ candidateRawField,
382
+ expected: typeof issue.expected === "string" ? issue.expected : undefined,
383
+ field,
384
+ fixHint: fixLines.join("\n"),
385
+ received: extractReceivedValue(issue),
386
+ testName: extractFailedTestName(output),
387
+ type: "zod",
388
+ };
389
+ }
390
+
391
+ function extractLastZodIssue(
392
+ output: string,
393
+ ): Record<string, unknown> | undefined {
394
+ const zodIndex = output.lastIndexOf("ZodError:");
395
+ if (zodIndex === -1) {
396
+ return undefined;
397
+ }
398
+
399
+ const jsonText = extractBalancedJsonArray(
400
+ output.slice(zodIndex + "ZodError:".length).trimStart(),
401
+ );
402
+ if (!jsonText) {
403
+ return undefined;
404
+ }
405
+
406
+ try {
407
+ const parsed: unknown = JSON.parse(jsonText);
408
+ if (!Array.isArray(parsed) || parsed.length === 0) {
409
+ return undefined;
410
+ }
411
+
412
+ const [firstIssue] = parsed;
413
+ if (!isRecord(firstIssue)) {
414
+ return undefined;
415
+ }
416
+
417
+ return firstIssue;
418
+ } catch {
419
+ return undefined;
420
+ }
421
+ }
422
+
423
+ function isRecord(value: unknown): value is Record<string, unknown> {
424
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
425
+ }
426
+
427
+ function extractBalancedJsonArray(value: string): string | undefined {
428
+ const start = value.indexOf("[");
429
+ if (start === -1) {
430
+ return undefined;
431
+ }
432
+
433
+ let depth = 0;
434
+ let isEscaped = false;
435
+ let isInsideString = false;
436
+
437
+ for (let index = start; index < value.length; index += 1) {
438
+ const character = value[index];
439
+
440
+ if (!character) {
441
+ continue;
442
+ }
443
+
444
+ if (isInsideString) {
445
+ if (isEscaped) {
446
+ isEscaped = false;
447
+ continue;
448
+ }
449
+
450
+ if (character === "\\") {
451
+ isEscaped = true;
452
+ continue;
453
+ }
454
+
455
+ if (character === '"') {
456
+ isInsideString = false;
457
+ }
458
+
459
+ continue;
460
+ }
461
+
462
+ if (character === '"') {
463
+ isInsideString = true;
464
+ continue;
465
+ }
466
+
467
+ if (character === "[") {
468
+ depth += 1;
469
+ }
470
+
471
+ if (character === "]") {
472
+ depth -= 1;
473
+ if (depth === 0) {
474
+ return value.slice(start, index + 1);
475
+ }
476
+ }
477
+ }
478
+
479
+ return undefined;
480
+ }
481
+
482
+ function extractReceivedValue(
483
+ issue: Record<string, unknown>,
484
+ ): string | undefined {
485
+ if (typeof issue.received === "string") {
486
+ return issue.received;
487
+ }
488
+
489
+ if (typeof issue.message !== "string") {
490
+ return undefined;
491
+ }
492
+
493
+ const receivedMatch = /received\s+(.+)$/i.exec(issue.message);
494
+ return receivedMatch?.[1]?.trim();
495
+ }
496
+
497
+ function extractFailedTestName(output: string): string | undefined {
498
+ return extractLastMatch(
499
+ output,
500
+ /\(fail\)\s+(.+?)\s+\[[^\]]+\]/g,
501
+ )?.[1]?.trim();
502
+ }
503
+
504
+ function formatIssuePath(path: unknown[]): string | undefined {
505
+ if (path.length === 0) {
506
+ return undefined;
507
+ }
508
+
509
+ let formatted = "";
510
+
511
+ for (const segment of path) {
512
+ if (typeof segment === "number") {
513
+ formatted += `[${segment}]`;
514
+ continue;
515
+ }
516
+
517
+ if (typeof segment === "string") {
518
+ formatted += formatted ? `.${segment}` : segment;
519
+ }
520
+ }
521
+
522
+ return formatted || undefined;
523
+ }
524
+
525
+ function findCandidateRawField(
526
+ fieldName: string,
527
+ rawFixturePath?: string,
528
+ ): string | undefined {
529
+ if (!rawFixturePath || !existsSync(rawFixturePath)) {
530
+ return undefined;
531
+ }
532
+
533
+ try {
534
+ const rawFixture: unknown = JSON.parse(
535
+ readFileSync(rawFixturePath, "utf-8"),
536
+ );
537
+ const keys = Array.from(collectObjectKeys(rawFixture));
538
+ const normalizedFieldName = normalizeIdentifier(fieldName);
539
+ const ranked = keys
540
+ .map((key) => ({
541
+ key,
542
+ score: scoreKeySimilarity(normalizedFieldName, key),
543
+ }))
544
+ .filter((entry) => entry.score > 0)
545
+ .sort((left, right) => right.score - left.score);
546
+
547
+ return ranked[0]?.key;
548
+ } catch {
549
+ return undefined;
550
+ }
551
+ }
552
+
553
+ function collectObjectKeys(
554
+ value: unknown,
555
+ bucket = new Set<string>(),
556
+ ): Set<string> {
557
+ if (Array.isArray(value)) {
558
+ for (const item of value) {
559
+ collectObjectKeys(item, bucket);
560
+ }
561
+ return bucket;
562
+ }
563
+
564
+ if (!value || typeof value !== "object") {
565
+ return bucket;
566
+ }
567
+
568
+ for (const [key, nestedValue] of Object.entries(value)) {
569
+ bucket.add(key);
570
+ collectObjectKeys(nestedValue, bucket);
571
+ }
572
+
573
+ return bucket;
574
+ }
575
+
576
+ function normalizeIdentifier(value: string): string[] {
577
+ return value
578
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
579
+ .toLowerCase()
580
+ .split(/[^a-z0-9]+/)
581
+ .filter(Boolean);
582
+ }
583
+
584
+ function scoreKeySimilarity(targetTokens: string[], key: string): number {
585
+ const keyTokens = normalizeIdentifier(key);
586
+ if (keyTokens.length === 0) {
587
+ return 0;
588
+ }
589
+
590
+ let score = 0;
591
+
592
+ for (const token of targetTokens) {
593
+ if (keyTokens.includes(token)) {
594
+ score += 3;
595
+ continue;
596
+ }
597
+
598
+ if (key.toLowerCase().includes(token)) {
599
+ score += 2;
600
+ continue;
601
+ }
602
+
603
+ if (
604
+ token.length >= 3 &&
605
+ keyTokens.some((candidate) => candidate.startsWith(token))
606
+ ) {
607
+ score += 1;
608
+ }
609
+ }
610
+
611
+ return score;
612
+ }
613
+
614
+ function printTextSummary(options: {
615
+ actionableError?: ActionableError;
616
+ connectorLabel: string;
617
+ exitCode: number;
618
+ summary: TestSummary;
619
+ }) {
620
+ const { actionableError, connectorLabel, exitCode, summary } = options;
621
+ const totalFailures = summary.failedCount + summary.errorCount;
622
+ const durationSuffix = summary.durationText
623
+ ? ` (${summary.durationText})`
624
+ : "";
625
+ const ranLabel =
626
+ summary.totalCount !== undefined
627
+ ? `Ran ${summary.totalCount} tests`
628
+ : "Ran tests";
629
+
630
+ if (exitCode === 0) {
631
+ console.log(`\n✓ ${connectorLabel} tests passed`);
632
+ console.log(
633
+ `${ranLabel} | ${summary.passedCount} passed, ${totalFailures} failed${durationSuffix} | exit ${exitCode}`,
634
+ );
635
+ return;
636
+ }
637
+
638
+ if (actionableError) {
639
+ console.log(
640
+ `\n✗ ${actionableError.testName ?? "transformResponse(raw) → output schema"}`,
641
+ );
642
+ console.log("\n transformResponse output doesn't match schema:\n");
643
+ if (actionableError.field) {
644
+ console.log(` ┌ Field: ${actionableError.field}`);
645
+ }
646
+ if (actionableError.expected) {
647
+ console.log(` │ Expected: ${actionableError.expected}`);
648
+ }
649
+ if (actionableError.received) {
650
+ console.log(` │ Received: ${actionableError.received}`);
651
+ }
652
+ console.log(" │");
653
+ console.log(
654
+ ` └ Fix: ${actionableError.fixHint.replace(/\n/g, "\n ")}`,
655
+ );
656
+ }
657
+
658
+ console.log(`\n✗ ${connectorLabel} tests failed`);
659
+ console.log(
660
+ `${ranLabel} | ${summary.passedCount} passed, ${totalFailures} failed${durationSuffix} | exit ${exitCode}`,
661
+ );
662
+ }
663
+
664
+ function handleCliError(error: unknown): never {
665
+ const message = error instanceof Error ? error.message : String(error);
666
+ console.error(`[apifuse test] ${message}`);
667
+ process.exit(1);
668
+ }
669
+
670
+ function extractLastNumber(
671
+ value: string,
672
+ pattern: RegExp,
673
+ groupIndex: number,
674
+ ): number | undefined {
675
+ const match = extractLastMatch(value, pattern);
676
+ const resolved = match?.[groupIndex];
677
+ return resolved ? Number(resolved) : undefined;
678
+ }
679
+
680
+ function extractLastMatch(
681
+ value: string,
682
+ pattern: RegExp,
683
+ ): RegExpMatchArray | undefined {
684
+ const matches = Array.from(value.matchAll(pattern));
685
+ return matches[matches.length - 1];
686
+ }
687
+
688
+ void main();
package/bin/apifuse.ts ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import packageJson from "../package.json";
4
+
5
+ const command = process.argv[2];
6
+
7
+ switch (command) {
8
+ case "init": {
9
+ const module = await import("./apifuse-init");
10
+ await module.main();
11
+ break;
12
+ }
13
+ case "dev": {
14
+ const module = await import("./apifuse-dev");
15
+ await module.main();
16
+ break;
17
+ }
18
+ case "check": {
19
+ const module = await import("./apifuse-check");
20
+ await module.main();
21
+ break;
22
+ }
23
+ case "--help":
24
+ case "-h":
25
+ case undefined:
26
+ printHelp();
27
+ break;
28
+ case "--version":
29
+ case "-v":
30
+ console.log(packageJson.version);
31
+ break;
32
+ default:
33
+ console.error(`Unknown command: ${command}`);
34
+ printHelp();
35
+ process.exit(1);
36
+ }
37
+
38
+ function printHelp() {
39
+ console.log(`
40
+ apifuse - ApiFuse Connector SDK CLI
41
+
42
+ Commands:
43
+ init <name> Create a new connector
44
+ dev [path] Start dev server with hot reload
45
+ check [path] Validate connector structure
46
+
47
+ Options:
48
+ --help Show this help
49
+ --version Show version
50
+ `);
51
+ }