@apifuse/provider-sdk 2.1.0-beta.4 → 2.1.0-beta.6
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/AUTHORING.md +24 -0
- package/CHANGELOG.md +11 -0
- package/README.md +23 -2
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +48 -5
- package/bin/apifuse-perf.ts +106 -26
- package/bin/apifuse-record.ts +142 -52
- package/bin/apifuse-submit-check.ts +1489 -3
- package/package.json +107 -92
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +1 -0
- package/src/cli/commands.ts +10 -8
- package/src/cli/create.ts +49 -1
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +18 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
- package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
- package/src/config/loader.ts +19 -1
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +215 -0
- package/src/define.ts +40 -5
- package/src/errors.ts +15 -0
- package/src/i18n/catalog.ts +156 -0
- package/src/index.ts +22 -1
- package/src/lint.ts +265 -46
- package/src/provider.ts +45 -2
- package/src/runtime/browser.ts +685 -30
- package/src/runtime/cache.ts +35 -89
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +19 -2
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/state.ts +487 -0
- package/src/runtime/stealth.ts +8 -1
- package/src/runtime/trace.ts +1 -1
- package/src/server/serve.ts +361 -46
- package/src/server/types.ts +2 -0
- package/src/testing/run.ts +16 -3
- package/src/types.ts +225 -18
package/AUTHORING.md
CHANGED
|
@@ -53,6 +53,30 @@ description:
|
|
|
53
53
|
|
|
54
54
|
Use `defineOperation()` when an operation is large enough to live beside helper functions or in a separate module. It preserves the same type inference as inline `defineProvider()` operations and can be placed directly in the provider `operations` map. `defineProvider()` accepts Zod and Standard Schema v1-compatible schemas. If config validation fails, the SDK names the field to fix, for example `runtime`, `auth.mode`, `operations.<id>.handler`, or `operations.<id>.fixtures.response`.
|
|
55
55
|
|
|
56
|
+
### Health assertion context
|
|
57
|
+
|
|
58
|
+
`healthCheck.cases[].assertions` receives a `HealthCheckAssertionContext` with
|
|
59
|
+
`data`, `status`, `durationMs`, and optional `meta`. `data` is typed from the
|
|
60
|
+
operation output schema, so assertions should inspect normalized output instead
|
|
61
|
+
of reaching into transport internals.
|
|
62
|
+
|
|
63
|
+
<!-- @magic-start:sample -->
|
|
64
|
+
```ts
|
|
65
|
+
healthCheck: {
|
|
66
|
+
interval: "5m",
|
|
67
|
+
cases: [{
|
|
68
|
+
name: "lookup baseline",
|
|
69
|
+
input: { q: "btc" },
|
|
70
|
+
assertions: ({ data, status, durationMs }) => {
|
|
71
|
+
if (status !== 200 || data.results.length === 0 || durationMs > 3000) {
|
|
72
|
+
return { status: "degraded", label: "lookup baseline changed" };
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}],
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
<!-- @magic-end:sample -->
|
|
79
|
+
|
|
56
80
|
### Strongly recommended (warn-level rules)
|
|
57
81
|
|
|
58
82
|
- `description` includes "use" AND "when" phrasing
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0-beta.6
|
|
4
|
+
|
|
5
|
+
- Public repository clean-import release for `APIFuseHQ/provider-sdk`.
|
|
6
|
+
- Preserves the monorepo SDK exports required by ApiFuse provider registry cutover, including `./contract` and provider i18n helpers.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## 2.1.0-beta.5
|
|
10
|
+
|
|
11
|
+
- Republish the bounty workspace DX hardening that accepts generated readonly metadata and factored `defineOperation()` maps during standalone TypeScript checks.
|
|
12
|
+
- Ensure new bounty workspaces can install the public SDK version that matches their generated scaffold and pass `bun run check` immediately after bootstrap.
|
|
13
|
+
|
|
3
14
|
## 2.1.0-beta.4
|
|
4
15
|
|
|
5
16
|
- Align `apifuse create` with the bounty program topology: external contributors use the standalone one-provider-repository scaffold even when their assigned repo contains workspace-like files.
|
package/README.md
CHANGED
|
@@ -149,6 +149,9 @@ const search = defineOperation({
|
|
|
149
149
|
async handler(ctx, input) {
|
|
150
150
|
return { count: input.q.length }
|
|
151
151
|
},
|
|
152
|
+
healthCheckUnsupported: {
|
|
153
|
+
reason: "Example operation only; replace with a real upstream probe.",
|
|
154
|
+
},
|
|
152
155
|
})
|
|
153
156
|
|
|
154
157
|
export default defineProvider({
|
|
@@ -174,6 +177,24 @@ Every operation must declare exactly one of:
|
|
|
174
177
|
The generated `ping` operation uses `healthCheckUnsupported` only because it is
|
|
175
178
|
a local scaffold check, not a real upstream API probe.
|
|
176
179
|
|
|
180
|
+
`healthCheck.cases[].assertions` receives `{ data, status, durationMs, meta }`.
|
|
181
|
+
`data` is the operation output parsed by the declared output schema:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
healthCheck: {
|
|
185
|
+
interval: "5m",
|
|
186
|
+
cases: [{
|
|
187
|
+
name: "search responds",
|
|
188
|
+
input: { q: "weather" },
|
|
189
|
+
assertions: ({ data, status, durationMs }) => {
|
|
190
|
+
if (status !== 200 || data.count < 1 || durationMs > 3000) {
|
|
191
|
+
return { status: "degraded", label: "unexpected search baseline" }
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
}],
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
177
198
|
### Operation annotations
|
|
178
199
|
|
|
179
200
|
Operations declare non-functional metadata via `annotations`:
|
|
@@ -195,11 +216,11 @@ Operations declare non-functional metadata via `annotations`:
|
|
|
195
216
|
apifuse create <name>
|
|
196
217
|
apifuse dev [path]
|
|
197
218
|
apifuse check [path]
|
|
198
|
-
apifuse record
|
|
219
|
+
apifuse record providers/korea-air-quality --operation realtime --params '{"stationName":"종로구"}'
|
|
199
220
|
apifuse test [path]
|
|
200
221
|
apifuse submit-check [path] --tier bronze --markdown submission-report.md
|
|
201
222
|
apifuse bounty-check [path]
|
|
202
|
-
apifuse perf
|
|
223
|
+
apifuse perf providers/korea-air-quality --operation realtime --params '{"stationName":"종로구"}'
|
|
203
224
|
```
|
|
204
225
|
|
|
205
226
|
`apifuse record` is for real upstream-backed operations that declare
|
package/SUBMISSION.md
CHANGED
|
@@ -48,8 +48,9 @@ Fix all blockers before submitting:
|
|
|
48
48
|
- Missing `healthCheck` or `healthCheckUnsupported` on any Operation.
|
|
49
49
|
- Credential-backed auth mode without declared credential keys.
|
|
50
50
|
- High-confidence secret or token material in source, README, package metadata, or fixtures.
|
|
51
|
+
- SDK-native source blockers: prefixed Provider ids, `vendor/` SDK shims or imports, raw `.describe()` prose instead of `describeKey`, raw global `fetch()` calls, and excessive `as Type` assertions.
|
|
51
52
|
|
|
52
|
-
Warnings do not fail the command, but they should be addressed when practical. For example, the generated starter `ping` operation warns because it is not a real upstream-backed bounty Operation.
|
|
53
|
+
Warnings do not fail the command, but they should be addressed when practical. For example, the generated starter `ping` operation warns because it is not a real upstream-backed bounty Operation. SDK-native warnings also flag moderate `as Type` assertion counts and credentialed Providers that never reference `ctx.credential`.
|
|
53
54
|
|
|
54
55
|
## Safe local smoke evidence
|
|
55
56
|
|
package/bin/apifuse-check.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
9
9
|
import type { ProviderDefinition } from "../src";
|
|
10
|
-
import { lintProvider } from "../src/lint";
|
|
10
|
+
import { lintProvider, type ProviderLintMode } from "../src/lint";
|
|
11
11
|
import { safeParseSchemaSync } from "../src/schema";
|
|
12
12
|
|
|
13
13
|
const HELP_TEXT = `Usage: apifuse check [path]
|
|
14
|
-
Example: apifuse check providers/
|
|
14
|
+
Example: apifuse check providers/korea-air-quality
|
|
15
15
|
Default: apifuse check .`;
|
|
16
16
|
|
|
17
17
|
export type CheckResult = {
|
|
@@ -20,6 +20,10 @@ export type CheckResult = {
|
|
|
20
20
|
details?: string[];
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
export type RunChecksOptions = {
|
|
24
|
+
lintMode?: ProviderLintMode;
|
|
25
|
+
};
|
|
26
|
+
|
|
23
27
|
type SafeParseResult =
|
|
24
28
|
| { success: true; data: unknown }
|
|
25
29
|
| { success: false; error: unknown };
|
|
@@ -104,7 +108,10 @@ function resolveFromParents(inputPath: string): string {
|
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
export async function runChecks(
|
|
111
|
+
export async function runChecks(
|
|
112
|
+
providerRoot: string,
|
|
113
|
+
options: RunChecksOptions = {},
|
|
114
|
+
): Promise<CheckResult[]> {
|
|
108
115
|
const indexPath = resolve(providerRoot, "index.ts");
|
|
109
116
|
const dockerfilePath = resolve(providerRoot, "Dockerfile");
|
|
110
117
|
const packageJsonPath = resolve(providerRoot, "package.json");
|
|
@@ -113,19 +120,61 @@ export async function runChecks(providerRoot: string): Promise<CheckResult[]> {
|
|
|
113
120
|
? await import(pathToFileURL(indexPath).href)
|
|
114
121
|
: undefined;
|
|
115
122
|
const provider = assertProviderDefinition(providerModule?.default);
|
|
123
|
+
const providerSourceFiles = collectProviderSourceFiles(providerRoot);
|
|
116
124
|
|
|
117
125
|
return [
|
|
118
126
|
checkIndex(indexPath, provider),
|
|
119
127
|
checkOperations(provider),
|
|
120
128
|
checkFixtures(provider),
|
|
121
129
|
checkSchemas(provider),
|
|
122
|
-
checkAuthoringLint(provider),
|
|
130
|
+
checkAuthoringLint(provider, providerSourceFiles, options.lintMode),
|
|
123
131
|
checkProviderMetadata(provider),
|
|
124
132
|
checkDockerfile(dockerfilePath),
|
|
125
133
|
checkPackageJson(packageJsonPath),
|
|
126
134
|
];
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
function isScannableProviderSourceFile(relativePath: string): boolean {
|
|
138
|
+
return (
|
|
139
|
+
/\.(?:ts|tsx|js|jsx|mjs|cjs|sh|bash)$/.test(relativePath) ||
|
|
140
|
+
/(?:^|\/)Dockerfile(?:\.|$)/.test(relativePath) ||
|
|
141
|
+
/(?:^|\/)entrypoint(?:\.|$)/.test(relativePath)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function collectProviderSourceFiles(
|
|
146
|
+
providerRoot: string,
|
|
147
|
+
): Record<string, string> {
|
|
148
|
+
const sources: Record<string, string> = {};
|
|
149
|
+
const skipDirectories = new Set([
|
|
150
|
+
".git",
|
|
151
|
+
"node_modules",
|
|
152
|
+
"dist",
|
|
153
|
+
"build",
|
|
154
|
+
".next",
|
|
155
|
+
]);
|
|
156
|
+
const visit = (directory: string) => {
|
|
157
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
158
|
+
const path = resolve(directory, entry.name);
|
|
159
|
+
if (entry.isDirectory()) {
|
|
160
|
+
if (!skipDirectories.has(entry.name)) {
|
|
161
|
+
visit(path);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
!entry.isFile() ||
|
|
167
|
+
!isScannableProviderSourceFile(path.slice(providerRoot.length + 1))
|
|
168
|
+
) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
sources[path.slice(providerRoot.length + 1)] = readFileSync(path, "utf8");
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
visit(providerRoot);
|
|
175
|
+
return sources;
|
|
176
|
+
}
|
|
177
|
+
|
|
129
178
|
function checkIndex(
|
|
130
179
|
indexPath: string,
|
|
131
180
|
provider: ProviderDefinition | undefined,
|
|
@@ -258,6 +307,8 @@ function checkSchemas(provider: ProviderDefinition | undefined): CheckResult {
|
|
|
258
307
|
|
|
259
308
|
function checkAuthoringLint(
|
|
260
309
|
provider: ProviderDefinition | undefined,
|
|
310
|
+
providerSourceFiles: Record<string, string>,
|
|
311
|
+
lintMode: ProviderLintMode = "official",
|
|
261
312
|
): CheckResult {
|
|
262
313
|
if (!provider) {
|
|
263
314
|
return {
|
|
@@ -266,7 +317,10 @@ function checkAuthoringLint(
|
|
|
266
317
|
};
|
|
267
318
|
}
|
|
268
319
|
|
|
269
|
-
const diagnostics = lintProvider(
|
|
320
|
+
const diagnostics = lintProvider(
|
|
321
|
+
{ ...provider, providerSourceFiles },
|
|
322
|
+
{ mode: lintMode },
|
|
323
|
+
);
|
|
270
324
|
const errors = diagnostics.filter(
|
|
271
325
|
(diagnostic) => diagnostic.level === "error",
|
|
272
326
|
);
|
package/bin/apifuse-dev.ts
CHANGED
|
@@ -9,16 +9,18 @@ import {
|
|
|
9
9
|
createEnvContext,
|
|
10
10
|
createHttpClient,
|
|
11
11
|
createProviderCache,
|
|
12
|
+
createProviderChoiceContext,
|
|
12
13
|
createStealthClient,
|
|
13
14
|
createSttClientFromEnv,
|
|
15
|
+
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
14
16
|
ProviderError,
|
|
15
17
|
} from "../src";
|
|
16
|
-
import {
|
|
18
|
+
import { createMemoryProviderRuntimeState } from "../src/runtime/state";
|
|
17
19
|
import { createTraceContext } from "../src/runtime/trace";
|
|
18
20
|
import type { BrowserClient, ProviderContext } from "../src/types";
|
|
19
21
|
|
|
20
22
|
const HELP_TEXT = `Usage: apifuse dev [path]
|
|
21
|
-
Example: apifuse dev providers/
|
|
23
|
+
Example: apifuse dev providers/korea-air-quality
|
|
22
24
|
Default: apifuse dev .`;
|
|
23
25
|
|
|
24
26
|
export async function main() {
|
|
@@ -76,22 +78,35 @@ export async function main() {
|
|
|
76
78
|
export function createProviderContext(provider: ProviderDefinition): {
|
|
77
79
|
ctx: ProviderContext;
|
|
78
80
|
} {
|
|
81
|
+
const env = createEnvContext([
|
|
82
|
+
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
83
|
+
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
84
|
+
]);
|
|
85
|
+
const credential = createCredentialContext();
|
|
86
|
+
const state = createMemoryProviderRuntimeState();
|
|
79
87
|
const ctx: ProviderContext = {
|
|
80
|
-
env
|
|
81
|
-
credential
|
|
88
|
+
env,
|
|
89
|
+
credential,
|
|
82
90
|
auth: createUnsupportedAuthStub(),
|
|
83
91
|
browser:
|
|
84
92
|
provider.runtime === "browser"
|
|
85
93
|
? createBrowserClient({
|
|
94
|
+
allowedHosts: provider.allowedHosts,
|
|
86
95
|
engine: provider.browser?.engine ?? "playwright-stealth",
|
|
87
96
|
})
|
|
88
97
|
: createUnsupportedBrowserStub(),
|
|
89
98
|
http: createHttpClient(),
|
|
90
99
|
cache: createProviderCache({ providerId: provider.id }),
|
|
91
|
-
state
|
|
100
|
+
state,
|
|
92
101
|
trace: createTraceContext(),
|
|
93
102
|
stealth: createStealthClient("http://localhost"),
|
|
94
103
|
stt: createSttClientFromEnv(provider.stt),
|
|
104
|
+
choice: createProviderChoiceContext({
|
|
105
|
+
providerId: provider.id,
|
|
106
|
+
env,
|
|
107
|
+
credential,
|
|
108
|
+
state,
|
|
109
|
+
}),
|
|
95
110
|
};
|
|
96
111
|
|
|
97
112
|
return { ctx };
|
|
@@ -149,6 +164,7 @@ function shellSingleQuote(value: string): string {
|
|
|
149
164
|
function createUnsupportedBrowserStub(): BrowserClient {
|
|
150
165
|
return {
|
|
151
166
|
engine: "playwright-stealth",
|
|
167
|
+
async close() {},
|
|
152
168
|
async newPage() {
|
|
153
169
|
throw new ProviderError(
|
|
154
170
|
"Browser runtime is not enabled for this provider",
|
|
@@ -158,6 +174,33 @@ function createUnsupportedBrowserStub(): BrowserClient {
|
|
|
158
174
|
},
|
|
159
175
|
);
|
|
160
176
|
},
|
|
177
|
+
async rawPage() {
|
|
178
|
+
throw new ProviderError(
|
|
179
|
+
"Browser runtime is not enabled for this provider",
|
|
180
|
+
{
|
|
181
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
182
|
+
fix: 'Set provider runtime to "browser" and APIFUSE__CDP_POOL__URL to use ctx.browser.rawPage',
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
async withIsolatedContext() {
|
|
187
|
+
throw new ProviderError(
|
|
188
|
+
"Browser runtime is not enabled for this provider",
|
|
189
|
+
{
|
|
190
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
191
|
+
fix: 'Set provider runtime to "browser" to use ctx.browser.withIsolatedContext',
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
async solveChallenge() {
|
|
196
|
+
throw new ProviderError(
|
|
197
|
+
"Browser runtime is not enabled for this provider",
|
|
198
|
+
{
|
|
199
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
200
|
+
fix: 'Set provider runtime to "browser" to use ctx.browser.solveChallenge',
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
},
|
|
161
204
|
};
|
|
162
205
|
}
|
|
163
206
|
|
package/bin/apifuse-perf.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ApiFuseConfig,
|
|
11
11
|
createBypassProviderCache,
|
|
12
12
|
createHttpClient,
|
|
13
|
+
createProviderChoiceContext,
|
|
13
14
|
createStealthClient,
|
|
14
15
|
createSttClientFromEnv,
|
|
15
16
|
executeOperation,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
groupSpansByName,
|
|
30
31
|
type PerfStats,
|
|
31
32
|
} from "../src/runtime/perf";
|
|
33
|
+
import { createMemoryProviderRuntimeState } from "../src/runtime/state";
|
|
32
34
|
import {
|
|
33
35
|
createTraceContext,
|
|
34
36
|
resolveTraceContextOptions,
|
|
@@ -43,6 +45,7 @@ type CliArgs = {
|
|
|
43
45
|
exportPath?: string;
|
|
44
46
|
flame: boolean;
|
|
45
47
|
operation: string;
|
|
48
|
+
params?: string;
|
|
46
49
|
runs: number;
|
|
47
50
|
warmup: number;
|
|
48
51
|
};
|
|
@@ -81,6 +84,21 @@ const DEFAULT_RUNS = 10;
|
|
|
81
84
|
const DEFAULT_WARMUP = 2;
|
|
82
85
|
const DEFAULT_CONCURRENCY = 1;
|
|
83
86
|
const BAR_WIDTH = 20;
|
|
87
|
+
const HELP_TEXT = `Usage: apifuse perf <provider-path> --operation <operation> [options]
|
|
88
|
+
|
|
89
|
+
Options:
|
|
90
|
+
--operation, -o <name> operation to profile (required)
|
|
91
|
+
--params, -p <json> JSON input template; falls back to fixtures.request or {}
|
|
92
|
+
--runs, -n <number> number of runs (default: 10)
|
|
93
|
+
--warmup <number> warmup runs (default: 2)
|
|
94
|
+
--concurrency, -c <n> concurrent requests (default: 1)
|
|
95
|
+
--compare-proxy run with proxy on/off and compare
|
|
96
|
+
--export <path> export results to JSON file
|
|
97
|
+
--flame generate flamegraph SVG
|
|
98
|
+
--help, -h show this help
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
apifuse perf providers/korea-air-quality --operation realtime --params '{"stationName":"jongno"}' --runs 5`;
|
|
84
102
|
|
|
85
103
|
export async function main() {
|
|
86
104
|
try {
|
|
@@ -94,7 +112,11 @@ export async function main() {
|
|
|
94
112
|
const inputSchema = getOperationSchema(provider, operation, "input");
|
|
95
113
|
const outputSchema = getOperationSchema(provider, operation, "output");
|
|
96
114
|
const fixtureReplay = await loadFixtureReplay(providerDirectory);
|
|
97
|
-
const inputTemplate = resolveInputTemplate(
|
|
115
|
+
const inputTemplate = resolveInputTemplate(
|
|
116
|
+
provider,
|
|
117
|
+
inputSchema,
|
|
118
|
+
args.params,
|
|
119
|
+
);
|
|
98
120
|
|
|
99
121
|
const directSuite = await runProfileSuite({
|
|
100
122
|
args,
|
|
@@ -184,6 +206,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
184
206
|
let compareProxy = false;
|
|
185
207
|
let exportPath: string | undefined;
|
|
186
208
|
let flame = false;
|
|
209
|
+
let params: string | undefined;
|
|
187
210
|
|
|
188
211
|
for (let index = 0; index < argv.length; index += 1) {
|
|
189
212
|
const arg = argv[index];
|
|
@@ -201,6 +224,11 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
201
224
|
continue;
|
|
202
225
|
}
|
|
203
226
|
|
|
227
|
+
if (arg === "--help" || arg === "-h") {
|
|
228
|
+
console.log(HELP_TEXT);
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
|
|
204
232
|
if (arg === "--compare-proxy") {
|
|
205
233
|
compareProxy = true;
|
|
206
234
|
continue;
|
|
@@ -222,6 +250,22 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
222
250
|
continue;
|
|
223
251
|
}
|
|
224
252
|
|
|
253
|
+
if (arg === "--params" || arg === "-p") {
|
|
254
|
+
params = requireArgValue(argv, index, arg);
|
|
255
|
+
index += 1;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (arg.startsWith("--params=")) {
|
|
260
|
+
params = arg.slice("--params=".length);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (arg.startsWith("-p=")) {
|
|
265
|
+
params = arg.slice("-p=".length);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
225
269
|
if (arg.startsWith("-o=")) {
|
|
226
270
|
operation = arg.slice("-o=".length);
|
|
227
271
|
continue;
|
|
@@ -294,20 +338,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
294
338
|
}
|
|
295
339
|
|
|
296
340
|
if (!providerPath || !operation) {
|
|
297
|
-
throw new Error(
|
|
298
|
-
[
|
|
299
|
-
"Usage: apifuse perf <provider-path> [options]",
|
|
300
|
-
"",
|
|
301
|
-
"Options:",
|
|
302
|
-
" --operation, -o <name> operation to profile (required)",
|
|
303
|
-
" --runs, -n <number> number of runs (default: 10)",
|
|
304
|
-
" --warmup <number> warmup runs (default: 2)",
|
|
305
|
-
" --concurrency, -c <n> concurrent requests (default: 1)",
|
|
306
|
-
" --compare-proxy run with proxy on/off and compare",
|
|
307
|
-
" --export <path> export results to JSON file",
|
|
308
|
-
" --flame generate flamegraph SVG",
|
|
309
|
-
].join("\n"),
|
|
310
|
-
);
|
|
341
|
+
throw new Error(HELP_TEXT);
|
|
311
342
|
}
|
|
312
343
|
|
|
313
344
|
return {
|
|
@@ -317,6 +348,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
317
348
|
exportPath,
|
|
318
349
|
flame,
|
|
319
350
|
operation,
|
|
351
|
+
params,
|
|
320
352
|
runs,
|
|
321
353
|
warmup,
|
|
322
354
|
};
|
|
@@ -411,7 +443,18 @@ function isSchema(value: unknown): value is { parse(input: unknown): unknown } {
|
|
|
411
443
|
function resolveInputTemplate(
|
|
412
444
|
provider: ProviderDefinition,
|
|
413
445
|
inputSchema: { parse(input: unknown): unknown },
|
|
446
|
+
params: string | undefined,
|
|
414
447
|
): unknown {
|
|
448
|
+
if (params !== undefined) {
|
|
449
|
+
try {
|
|
450
|
+
return inputSchema.parse(JSON.parse(params));
|
|
451
|
+
} catch (error) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`Failed to parse --params JSON or validate input: ${error instanceof Error ? error.message : String(error)}`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
415
458
|
const firstOp = Object.values(provider.operations)[0];
|
|
416
459
|
if (firstOp?.fixtures?.request !== undefined) {
|
|
417
460
|
return firstOp.fixtures.request;
|
|
@@ -647,24 +690,36 @@ function createBaseContext(options: {
|
|
|
647
690
|
upstream,
|
|
648
691
|
});
|
|
649
692
|
|
|
693
|
+
const env = {
|
|
694
|
+
get: (key: string) => process.env[key],
|
|
695
|
+
};
|
|
696
|
+
const credential = {
|
|
697
|
+
mode: "none" as const,
|
|
698
|
+
get: () => undefined,
|
|
699
|
+
getAll: () => ({}),
|
|
700
|
+
getAccessToken: () => undefined,
|
|
701
|
+
getScopes: () => [],
|
|
702
|
+
};
|
|
703
|
+
const state = createMemoryProviderRuntimeState();
|
|
650
704
|
return {
|
|
651
|
-
env
|
|
652
|
-
|
|
653
|
-
},
|
|
654
|
-
credential: {
|
|
655
|
-
mode: "none",
|
|
656
|
-
get: () => undefined,
|
|
657
|
-
getAll: () => ({}),
|
|
658
|
-
getAccessToken: () => undefined,
|
|
659
|
-
getScopes: () => [],
|
|
660
|
-
},
|
|
705
|
+
env,
|
|
706
|
+
credential,
|
|
707
|
+
request: { headers: {} },
|
|
661
708
|
http,
|
|
662
709
|
cache: createBypassProviderCache({ providerId: options.provider.id }),
|
|
710
|
+
state,
|
|
663
711
|
stealth,
|
|
664
712
|
browser: createBrowserStub(),
|
|
665
713
|
trace: options.traceContext,
|
|
666
714
|
auth: createAuthStub(),
|
|
667
715
|
stt: createSttClientFromEnv(options.provider.stt),
|
|
716
|
+
choice: createProviderChoiceContext({
|
|
717
|
+
providerId: options.provider.id,
|
|
718
|
+
env,
|
|
719
|
+
request: { headers: {} },
|
|
720
|
+
credential,
|
|
721
|
+
state,
|
|
722
|
+
}),
|
|
668
723
|
};
|
|
669
724
|
}
|
|
670
725
|
|
|
@@ -724,6 +779,7 @@ function createFixtureStealthClient(rawText: string): StealthClient {
|
|
|
724
779
|
function createBrowserStub(): BrowserClient {
|
|
725
780
|
return {
|
|
726
781
|
engine: "playwright-stealth",
|
|
782
|
+
async close() {},
|
|
727
783
|
async newPage() {
|
|
728
784
|
throw new ProviderError(
|
|
729
785
|
"Browser runtime is not supported by apifuse perf yet.",
|
|
@@ -732,6 +788,30 @@ function createBrowserStub(): BrowserClient {
|
|
|
732
788
|
},
|
|
733
789
|
);
|
|
734
790
|
},
|
|
791
|
+
async rawPage() {
|
|
792
|
+
throw new ProviderError(
|
|
793
|
+
"Browser runtime is not supported by apifuse perf yet.",
|
|
794
|
+
{
|
|
795
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
796
|
+
},
|
|
797
|
+
);
|
|
798
|
+
},
|
|
799
|
+
async withIsolatedContext() {
|
|
800
|
+
throw new ProviderError(
|
|
801
|
+
"Browser runtime is not supported by apifuse perf yet.",
|
|
802
|
+
{
|
|
803
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
804
|
+
},
|
|
805
|
+
);
|
|
806
|
+
},
|
|
807
|
+
async solveChallenge() {
|
|
808
|
+
throw new ProviderError(
|
|
809
|
+
"Browser runtime is not supported by apifuse perf yet.",
|
|
810
|
+
{
|
|
811
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
812
|
+
},
|
|
813
|
+
);
|
|
814
|
+
},
|
|
735
815
|
};
|
|
736
816
|
}
|
|
737
817
|
|
|
@@ -1095,7 +1175,7 @@ function cloneValue<T>(value: T): T {
|
|
|
1095
1175
|
|
|
1096
1176
|
function handleCliError(error: unknown): never {
|
|
1097
1177
|
const message = error instanceof Error ? error.message : String(error);
|
|
1098
|
-
console.error(message);
|
|
1178
|
+
console.error(`[apifuse perf] ${message}`);
|
|
1099
1179
|
process.exit(1);
|
|
1100
1180
|
}
|
|
1101
1181
|
|