@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.10
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 +218 -21
- package/CHANGELOG.md +54 -0
- package/README.md +147 -10
- package/SUBMISSION.md +87 -0
- package/bin/apifuse-check.ts +86 -4
- package/bin/apifuse-dev.ts +87 -13
- package/bin/apifuse-pack-check.ts +120 -0
- package/bin/apifuse-pack-smoke.ts +423 -0
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +2538 -0
- package/bin/apifuse.ts +1 -1
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +762 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +160 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +706 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +18 -0
- package/dist/runtime/stealth.js +834 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1110 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +303 -0
- package/dist/types.d.ts +1326 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +57 -29
- package/src/ceremonies/index.ts +30 -3
- package/src/choice-token.ts +165 -0
- package/src/cli/commands.ts +34 -11
- package/src/cli/create.ts +214 -52
- 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 +134 -2
- package/src/cli/templates/provider/dev.ts.tpl +1 -1
- package/src/cli/templates/provider/domain/README.md.tpl +3 -0
- package/src/cli/templates/provider/index.ts.tpl +5 -44
- package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/src/cli/templates/provider/meta.ts.tpl +7 -0
- package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/start.ts.tpl +1 -1
- package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/src/config/loader.ts +1282 -7
- 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 +1726 -48
- package/src/errors.ts +27 -0
- package/src/i18n/catalog.ts +277 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +174 -15
- package/src/lint.ts +547 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +104 -5
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +762 -51
- package/src/runtime/cache.ts +528 -0
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +32 -3
- package/src/runtime/http.ts +945 -185
- package/src/runtime/insights.ts +11 -11
- package/src/runtime/instrumentation.ts +12 -4
- package/src/runtime/key-derivation.ts +1 -1
- package/src/runtime/keyring.ts +4 -3
- package/src/runtime/proxy-errors.ts +132 -0
- package/src/runtime/proxy-telemetry.ts +253 -0
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +563 -0
- package/src/runtime/stealth.ts +1159 -0
- package/src/runtime/stt.ts +629 -0
- package/src/runtime/trace.ts +1 -1
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +1172 -76
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +31 -5
- package/src/types.ts +1118 -44
- package/src/composite.ts +0 -43
- package/src/runtime/tls.ts +0 -425
- package/src/types/playwright-stealth.d.ts +0 -9
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, relative, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { cancel, intro, isCancel, note, outro, select, text, } from "@clack/prompts";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import packageJson from "../../package.json";
|
|
9
|
+
export const PROVIDER_NAME_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
|
+
export const CATEGORY_OPTIONS = [
|
|
11
|
+
"developer-tools",
|
|
12
|
+
"finance",
|
|
13
|
+
"commerce",
|
|
14
|
+
"productivity",
|
|
15
|
+
"marketing",
|
|
16
|
+
"data",
|
|
17
|
+
"communication",
|
|
18
|
+
"other",
|
|
19
|
+
];
|
|
20
|
+
export const AUTH_MODE_OPTIONS = [
|
|
21
|
+
"none",
|
|
22
|
+
"platform-managed",
|
|
23
|
+
"credentials",
|
|
24
|
+
"oauth2",
|
|
25
|
+
];
|
|
26
|
+
export const RUNTIME_OPTIONS = ["standard", "browser"];
|
|
27
|
+
export const PRESET_OPTIONS = ["standalone", "monorepo"];
|
|
28
|
+
const CREATE_CONFIG_SCHEMA = z.object({
|
|
29
|
+
authMode: z.enum(AUTH_MODE_OPTIONS).optional(),
|
|
30
|
+
category: z.enum(CATEGORY_OPTIONS).optional(),
|
|
31
|
+
displayName: z.string().optional(),
|
|
32
|
+
dryRun: z.boolean().optional(),
|
|
33
|
+
json: z.boolean().optional(),
|
|
34
|
+
name: z.string().optional(),
|
|
35
|
+
outputDir: z.string().optional(),
|
|
36
|
+
preset: z.enum(PRESET_OPTIONS).optional(),
|
|
37
|
+
runtime: z.enum(RUNTIME_OPTIONS).optional(),
|
|
38
|
+
sdkSpecifier: z.string().optional(),
|
|
39
|
+
yes: z.boolean().optional(),
|
|
40
|
+
});
|
|
41
|
+
const TEMPLATE_DIR = fileURLToPath(new URL("./templates/provider/", import.meta.url));
|
|
42
|
+
const HELP_TEXT = `Usage: apifuse create <provider-name> [options]
|
|
43
|
+
Examples:
|
|
44
|
+
apifuse create my-provider
|
|
45
|
+
apifuse create --config ./apifuse.create.json --json
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--config <path>
|
|
49
|
+
--output-dir <path>
|
|
50
|
+
--display-name <name>
|
|
51
|
+
--category <category>
|
|
52
|
+
--auth-mode <mode>
|
|
53
|
+
--runtime <standard|browser>
|
|
54
|
+
--yes
|
|
55
|
+
--dry-run
|
|
56
|
+
--json
|
|
57
|
+
--sdk-specifier <specifier> # internal/testing override for dependency resolution
|
|
58
|
+
--help, -h`;
|
|
59
|
+
export async function main() {
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
const normalizedArgs = normalizeArgs(args);
|
|
62
|
+
if (normalizedArgs.includes("--help") || normalizedArgs.includes("-h")) {
|
|
63
|
+
console.log(HELP_TEXT);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const parsed = parseArgs(normalizedArgs);
|
|
67
|
+
const config = parsed.configPath
|
|
68
|
+
? await loadConfig(parsed.configPath)
|
|
69
|
+
: undefined;
|
|
70
|
+
const resolved = await resolveCreateOptions(parsed, config, process.cwd());
|
|
71
|
+
const plan = await buildProviderCreatePlan(resolved, process.cwd());
|
|
72
|
+
if (resolved.dryRun) {
|
|
73
|
+
return printResult(plan, resolved.json, true);
|
|
74
|
+
}
|
|
75
|
+
await writePlan(plan);
|
|
76
|
+
await installDependencies(plan, resolved.json);
|
|
77
|
+
await runBaselineValidation(plan, resolved.json);
|
|
78
|
+
printResult(plan, resolved.json, false);
|
|
79
|
+
}
|
|
80
|
+
function normalizeArgs(argv) {
|
|
81
|
+
return argv[0] === "create" ? argv.slice(1) : argv;
|
|
82
|
+
}
|
|
83
|
+
function parseArgs(argv) {
|
|
84
|
+
const parsed = {
|
|
85
|
+
dryRun: false,
|
|
86
|
+
json: false,
|
|
87
|
+
yes: false,
|
|
88
|
+
};
|
|
89
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
90
|
+
const arg = argv[index];
|
|
91
|
+
if (!arg)
|
|
92
|
+
continue;
|
|
93
|
+
const [flag, inlineValue] = arg.split("=", 2);
|
|
94
|
+
const value = inlineValue ?? argv[index + 1];
|
|
95
|
+
const consumeValue = () => {
|
|
96
|
+
if (inlineValue === undefined) {
|
|
97
|
+
index += 1;
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
};
|
|
101
|
+
switch (flag) {
|
|
102
|
+
case "--preset":
|
|
103
|
+
parsed.preset = parseEnum("preset", consumeValue(), PRESET_OPTIONS);
|
|
104
|
+
break;
|
|
105
|
+
case "--config":
|
|
106
|
+
parsed.configPath = ensureValue(flag, consumeValue());
|
|
107
|
+
break;
|
|
108
|
+
case "--output-dir":
|
|
109
|
+
parsed.outputDir = ensureValue(flag, consumeValue());
|
|
110
|
+
break;
|
|
111
|
+
case "--display-name":
|
|
112
|
+
parsed.displayName = ensureValue(flag, consumeValue());
|
|
113
|
+
break;
|
|
114
|
+
case "--category":
|
|
115
|
+
parsed.category = parseEnum("category", consumeValue(), CATEGORY_OPTIONS);
|
|
116
|
+
break;
|
|
117
|
+
case "--auth-mode":
|
|
118
|
+
parsed.authMode = parseEnum("auth mode", consumeValue(), AUTH_MODE_OPTIONS);
|
|
119
|
+
break;
|
|
120
|
+
case "--runtime":
|
|
121
|
+
parsed.runtime = parseEnum("runtime", consumeValue(), RUNTIME_OPTIONS);
|
|
122
|
+
break;
|
|
123
|
+
case "--sdk-specifier":
|
|
124
|
+
parsed.sdkSpecifier = ensureValue(flag, consumeValue());
|
|
125
|
+
break;
|
|
126
|
+
case "--dry-run":
|
|
127
|
+
parsed.dryRun = true;
|
|
128
|
+
break;
|
|
129
|
+
case "--json":
|
|
130
|
+
parsed.json = true;
|
|
131
|
+
break;
|
|
132
|
+
case "--yes":
|
|
133
|
+
parsed.yes = true;
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
if (flag.startsWith("-")) {
|
|
137
|
+
throw new Error(`Unknown option: ${flag}`);
|
|
138
|
+
}
|
|
139
|
+
if (!parsed.name) {
|
|
140
|
+
parsed.name = arg;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
148
|
+
function ensureValue(flag, value) {
|
|
149
|
+
if (!value) {
|
|
150
|
+
throw new Error(`Missing value for ${flag}`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function parseEnum(label, value, options) {
|
|
155
|
+
const resolvedValue = ensureValue(`--${label}`, value);
|
|
156
|
+
const matchedValue = options.find((option) => option === resolvedValue);
|
|
157
|
+
if (!matchedValue) {
|
|
158
|
+
throw new Error(`Invalid ${label}: ${resolvedValue}. Expected one of: ${options.join(", ")}`);
|
|
159
|
+
}
|
|
160
|
+
return matchedValue;
|
|
161
|
+
}
|
|
162
|
+
async function loadConfig(configPath) {
|
|
163
|
+
const resolvedPath = resolve(process.cwd(), configPath);
|
|
164
|
+
const raw = await readFile(resolvedPath, "utf8");
|
|
165
|
+
return CREATE_CONFIG_SCHEMA.parse(JSON.parse(raw));
|
|
166
|
+
}
|
|
167
|
+
async function resolveCreateOptions(parsed, config, cwd) {
|
|
168
|
+
const internalWorkspaceRoot = findApifuseInternalWorkspaceRoot(cwd);
|
|
169
|
+
const partial = {
|
|
170
|
+
name: parsed.name ?? config?.name,
|
|
171
|
+
preset: parsed.preset ?? config?.preset ?? "standalone",
|
|
172
|
+
outputDir: parsed.outputDir ?? config?.outputDir,
|
|
173
|
+
displayName: parsed.displayName ?? config?.displayName,
|
|
174
|
+
category: parsed.category ?? config?.category,
|
|
175
|
+
authMode: parsed.authMode ?? config?.authMode,
|
|
176
|
+
runtime: parsed.runtime ?? config?.runtime,
|
|
177
|
+
sdkSpecifier: parsed.sdkSpecifier ??
|
|
178
|
+
config?.sdkSpecifier ??
|
|
179
|
+
process.env.APIFUSE__SDK__SPECIFIER,
|
|
180
|
+
dryRun: parsed.dryRun,
|
|
181
|
+
json: parsed.json,
|
|
182
|
+
yes: parsed.yes,
|
|
183
|
+
};
|
|
184
|
+
if (partial.preset === "monorepo" && !internalWorkspaceRoot) {
|
|
185
|
+
throw new Error("Monorepo preset is internal to the APIFuse repository. External bounty workspaces are one-provider repositories; use the standalone default create flow.");
|
|
186
|
+
}
|
|
187
|
+
if (partial.yes) {
|
|
188
|
+
if (!partial.name) {
|
|
189
|
+
throw new Error("--yes requires a provider name (positional or via config).");
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
name: validateProviderName(partial.name),
|
|
193
|
+
displayName: (partial.displayName ?? toDisplayName(partial.name)).trim(),
|
|
194
|
+
category: partial.category ?? "other",
|
|
195
|
+
authMode: partial.authMode ?? "none",
|
|
196
|
+
runtime: partial.runtime ?? "standard",
|
|
197
|
+
preset: partial.preset ?? "standalone",
|
|
198
|
+
outputDir: partial.outputDir,
|
|
199
|
+
dryRun: partial.dryRun ?? false,
|
|
200
|
+
json: partial.json ?? false,
|
|
201
|
+
sdkSpecifier: partial.sdkSpecifier,
|
|
202
|
+
yes: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (!partial.json) {
|
|
206
|
+
intro("Create a new APIFuse provider");
|
|
207
|
+
note("External bounty workspaces are one-provider repositories. The public create flow defaults to standalone.", "Provider workspace");
|
|
208
|
+
}
|
|
209
|
+
const name = validateProviderName(partial.name ??
|
|
210
|
+
(await promptValue(text({
|
|
211
|
+
message: "Provider name",
|
|
212
|
+
initialValue: undefined,
|
|
213
|
+
placeholder: "my-provider",
|
|
214
|
+
validate(value) {
|
|
215
|
+
try {
|
|
216
|
+
validateProviderName(value ?? "");
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return error instanceof Error ? error.message : String(error);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
}))));
|
|
223
|
+
return {
|
|
224
|
+
name,
|
|
225
|
+
displayName: (partial.displayName ??
|
|
226
|
+
(await promptValue(text({
|
|
227
|
+
message: "Display name",
|
|
228
|
+
initialValue: toDisplayName(name),
|
|
229
|
+
validate(value) {
|
|
230
|
+
if (!(value?.trim() ?? "")) {
|
|
231
|
+
return "Display name is required.";
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
})))).trim(),
|
|
235
|
+
category: partial.category ??
|
|
236
|
+
(await promptValue(select({
|
|
237
|
+
message: "Category",
|
|
238
|
+
options: CATEGORY_OPTIONS.map((value) => ({ label: value, value })),
|
|
239
|
+
initialValue: "other",
|
|
240
|
+
}))),
|
|
241
|
+
authMode: partial.authMode ??
|
|
242
|
+
(await promptValue(select({
|
|
243
|
+
message: "Auth mode",
|
|
244
|
+
options: AUTH_MODE_OPTIONS.map((value) => ({ label: value, value })),
|
|
245
|
+
initialValue: "none",
|
|
246
|
+
}))),
|
|
247
|
+
runtime: partial.runtime ??
|
|
248
|
+
(await promptValue(select({
|
|
249
|
+
message: "Runtime",
|
|
250
|
+
options: RUNTIME_OPTIONS.map((value) => ({ label: value, value })),
|
|
251
|
+
initialValue: "standard",
|
|
252
|
+
}))),
|
|
253
|
+
preset: partial.preset ?? "standalone",
|
|
254
|
+
outputDir: partial.outputDir,
|
|
255
|
+
dryRun: partial.dryRun ?? false,
|
|
256
|
+
json: partial.json ?? false,
|
|
257
|
+
sdkSpecifier: partial.sdkSpecifier,
|
|
258
|
+
yes: false,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async function promptValue(prompt) {
|
|
262
|
+
const result = await prompt;
|
|
263
|
+
if (isCancel(result)) {
|
|
264
|
+
cancel("Operation cancelled.");
|
|
265
|
+
process.exit(0);
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
export async function buildProviderCreatePlan(options, cwd) {
|
|
270
|
+
const resolvedWorkspaceRoot = options.preset === "monorepo"
|
|
271
|
+
? findApifuseInternalWorkspaceRoot(cwd)
|
|
272
|
+
: undefined;
|
|
273
|
+
if (options.preset === "monorepo" && !resolvedWorkspaceRoot) {
|
|
274
|
+
throw new Error("Monorepo preset is internal to the APIFuse repository. External bounty workspaces are one-provider repositories; use the standalone default create flow.");
|
|
275
|
+
}
|
|
276
|
+
let providerRoot;
|
|
277
|
+
let installCwd;
|
|
278
|
+
if (options.outputDir) {
|
|
279
|
+
providerRoot = resolve(cwd, options.outputDir);
|
|
280
|
+
installCwd =
|
|
281
|
+
options.preset === "monorepo" && resolvedWorkspaceRoot
|
|
282
|
+
? resolvedWorkspaceRoot
|
|
283
|
+
: providerRoot;
|
|
284
|
+
}
|
|
285
|
+
else if (options.preset === "monorepo" && resolvedWorkspaceRoot) {
|
|
286
|
+
providerRoot = resolve(resolvedWorkspaceRoot, "providers", options.name);
|
|
287
|
+
installCwd = resolvedWorkspaceRoot;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
providerRoot = resolve(cwd, options.name);
|
|
291
|
+
installCwd = providerRoot;
|
|
292
|
+
}
|
|
293
|
+
if (existsSync(providerRoot)) {
|
|
294
|
+
throw new Error(`Target directory already exists: ${providerRoot}`);
|
|
295
|
+
}
|
|
296
|
+
if (options.sdkSpecifier?.startsWith("workspace:") &&
|
|
297
|
+
!resolvedWorkspaceRoot) {
|
|
298
|
+
throw new Error("workspace:* is only valid inside the APIFuse monorepo because public Provider SDK scaffolds must install from npm or an explicit tarball/file specifier.");
|
|
299
|
+
}
|
|
300
|
+
const sdkSpecifier = options.sdkSpecifier ??
|
|
301
|
+
(options.preset === "monorepo" && resolvedWorkspaceRoot
|
|
302
|
+
? "workspace:*"
|
|
303
|
+
: `^${packageJson.version}`);
|
|
304
|
+
const relativeProviderRoot = relative(cwd, providerRoot) || options.name;
|
|
305
|
+
const nextDevCommand = `cd ${relativeProviderRoot} && bun run dev`;
|
|
306
|
+
const packageName = options.preset === "monorepo"
|
|
307
|
+
? `@apifuse/provider-${options.name}`
|
|
308
|
+
: `apifuse-provider-${options.name}`;
|
|
309
|
+
const templateValues = {
|
|
310
|
+
PROVIDER_ID: options.name,
|
|
311
|
+
DISPLAY_NAME: escapeTemplate(options.displayName),
|
|
312
|
+
CATEGORY: options.category,
|
|
313
|
+
RUNTIME: options.runtime,
|
|
314
|
+
BROWSER_BLOCK: options.runtime === "browser"
|
|
315
|
+
? ',\n browser: {\n engine: "playwright-stealth",\n }'
|
|
316
|
+
: "",
|
|
317
|
+
SECRETS_BLOCK: renderSecretsBlock(options.authMode),
|
|
318
|
+
CREDENTIAL_BLOCK: renderCredentialBlock(options.authMode),
|
|
319
|
+
AUTH_BLOCK: renderAuthBlock(options.authMode),
|
|
320
|
+
};
|
|
321
|
+
const files = [
|
|
322
|
+
{
|
|
323
|
+
path: resolve(providerRoot, ".dockerignore"),
|
|
324
|
+
content: await renderTemplate(".dockerignore.tpl", {}),
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
path: resolve(providerRoot, ".gitignore"),
|
|
328
|
+
content: await renderTemplate(".gitignore.tpl", {}),
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
path: resolve(providerRoot, "index.ts"),
|
|
332
|
+
content: await renderTemplate("index.ts.tpl", templateValues),
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
path: resolve(providerRoot, "meta.ts"),
|
|
336
|
+
content: await renderTemplate("meta.ts.tpl", {
|
|
337
|
+
PROVIDER_ID: options.name,
|
|
338
|
+
DISPLAY_NAME: escapeTemplate(options.displayName),
|
|
339
|
+
CATEGORY: options.category,
|
|
340
|
+
}),
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
path: resolve(providerRoot, "operations", "index.ts"),
|
|
344
|
+
content: await renderTemplate("operations/index.ts.tpl", {}),
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
path: resolve(providerRoot, "operations", "ping.ts"),
|
|
348
|
+
content: await renderTemplate("operations/ping.ts.tpl", {
|
|
349
|
+
DISPLAY_NAME: escapeTemplate(options.displayName),
|
|
350
|
+
HANDLER_CTX: options.runtime === "browser" ? "ctx" : "_ctx",
|
|
351
|
+
BROWSER_HANDLER_BLOCK: options.runtime === "browser"
|
|
352
|
+
? '\n const page = await ctx.browser.newPage();\n await page.goto("https://example.com");\n const title = await page.title();\n const frames = await page.frames();\n await page.close();\n'
|
|
353
|
+
: "",
|
|
354
|
+
BROWSER_RESPONSE_FIELDS: options.runtime === "browser"
|
|
355
|
+
? ",\n pageTitle: title,\n frameCount: frames.length"
|
|
356
|
+
: "",
|
|
357
|
+
}),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
path: resolve(providerRoot, "schemas", "ping.ts"),
|
|
361
|
+
content: await renderTemplate("schemas/ping.ts.tpl", {}),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
path: resolve(providerRoot, "upstream", "README.md"),
|
|
365
|
+
content: await renderTemplate("upstream/README.md.tpl", {}),
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
path: resolve(providerRoot, "mappers", "README.md"),
|
|
369
|
+
content: await renderTemplate("mappers/README.md.tpl", {}),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
path: resolve(providerRoot, "domain", "README.md"),
|
|
373
|
+
content: await renderTemplate("domain/README.md.tpl", {}),
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
path: resolve(providerRoot, "locales", "en.json"),
|
|
377
|
+
content: renderStarterLocaleCatalog(options.displayName, "en"),
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
path: resolve(providerRoot, "locales", "ko.json"),
|
|
381
|
+
content: renderStarterLocaleCatalog(options.displayName, "ko"),
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
path: resolve(providerRoot, "package.json"),
|
|
385
|
+
content: renderPackageJson({
|
|
386
|
+
packageName,
|
|
387
|
+
sdkSpecifier,
|
|
388
|
+
}),
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
path: resolve(providerRoot, "Dockerfile"),
|
|
392
|
+
content: await renderTemplate("Dockerfile.tpl", {}),
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
path: resolve(providerRoot, "dev.ts"),
|
|
396
|
+
content: await renderTemplate("dev.ts.tpl", {}),
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
path: resolve(providerRoot, "start.ts"),
|
|
400
|
+
content: await renderTemplate("start.ts.tpl", {}),
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
path: resolve(providerRoot, "tsconfig.json"),
|
|
404
|
+
content: renderTsconfig(),
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
path: resolve(providerRoot, "README.md"),
|
|
408
|
+
content: await renderTemplate("README.md.tpl", {
|
|
409
|
+
DISPLAY_NAME: escapeTemplate(options.displayName),
|
|
410
|
+
}),
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
path: resolve(providerRoot, "__fixtures__", "raw.json"),
|
|
414
|
+
content: "{}\n",
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
path: resolve(providerRoot, "__tests__", "index.test.ts"),
|
|
418
|
+
content: await renderTemplate("index.test.ts.tpl", {
|
|
419
|
+
PROVIDER_ID: options.name,
|
|
420
|
+
}),
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
return {
|
|
424
|
+
displayName: options.displayName,
|
|
425
|
+
files,
|
|
426
|
+
installCommand: "bun install",
|
|
427
|
+
installCwd,
|
|
428
|
+
name: options.name,
|
|
429
|
+
nextDevCommand,
|
|
430
|
+
outputDir: providerRoot,
|
|
431
|
+
packageName,
|
|
432
|
+
preset: options.preset,
|
|
433
|
+
providerRoot,
|
|
434
|
+
validationCommands: [
|
|
435
|
+
"bun run check",
|
|
436
|
+
"bun run type-check",
|
|
437
|
+
"bun run submit-check",
|
|
438
|
+
"bun run test",
|
|
439
|
+
],
|
|
440
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
async function renderTemplate(fileName, values) {
|
|
444
|
+
const templatePath = resolve(TEMPLATE_DIR, fileName);
|
|
445
|
+
const template = await readFile(templatePath, "utf8");
|
|
446
|
+
return template.replace(/\{\{([A-Z_]+)\}\}/g, (_match, key) => {
|
|
447
|
+
return values[key] ?? "";
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function renderPackageJson(input) {
|
|
451
|
+
return `${JSON.stringify({
|
|
452
|
+
name: input.packageName,
|
|
453
|
+
version: "1.0.0",
|
|
454
|
+
private: true,
|
|
455
|
+
type: "module",
|
|
456
|
+
main: "./index.ts",
|
|
457
|
+
scripts: {
|
|
458
|
+
dev: "apifuse dev .",
|
|
459
|
+
check: "apifuse check . && bun run type-check",
|
|
460
|
+
"type-check": "tsc --noEmit",
|
|
461
|
+
"submit-check": "apifuse submit-check . --markdown submission-report.md",
|
|
462
|
+
test: "apifuse test .",
|
|
463
|
+
record: "apifuse record .",
|
|
464
|
+
"perf:sample": "apifuse perf . --operation ping --runs 3",
|
|
465
|
+
start: "bun start.ts",
|
|
466
|
+
},
|
|
467
|
+
dependencies: {
|
|
468
|
+
"@apifuse/provider-sdk": input.sdkSpecifier,
|
|
469
|
+
},
|
|
470
|
+
devDependencies: {
|
|
471
|
+
"@types/bun": "latest",
|
|
472
|
+
typescript: "^6.0.3",
|
|
473
|
+
},
|
|
474
|
+
}, null, 2)}\n`;
|
|
475
|
+
}
|
|
476
|
+
function renderTsconfig() {
|
|
477
|
+
return `${JSON.stringify({
|
|
478
|
+
compilerOptions: {
|
|
479
|
+
target: "ES2022",
|
|
480
|
+
module: "ES2022",
|
|
481
|
+
moduleResolution: "bundler",
|
|
482
|
+
strict: true,
|
|
483
|
+
noEmit: true,
|
|
484
|
+
skipLibCheck: true,
|
|
485
|
+
resolveJsonModule: true,
|
|
486
|
+
types: ["bun"],
|
|
487
|
+
},
|
|
488
|
+
include: ["**/*.ts"],
|
|
489
|
+
exclude: ["node_modules"],
|
|
490
|
+
}, null, 2)}\n`;
|
|
491
|
+
}
|
|
492
|
+
function renderStarterLocaleCatalog(displayName, locale) {
|
|
493
|
+
const catalog = {
|
|
494
|
+
meta: {
|
|
495
|
+
displayName,
|
|
496
|
+
description: locale === "ko"
|
|
497
|
+
? `${displayName} APIFuse 커뮤니티 기여용 provider starter입니다.`
|
|
498
|
+
: `${displayName} provider starter for APIFuse community contributions.`,
|
|
499
|
+
},
|
|
500
|
+
operations: {
|
|
501
|
+
ping: {
|
|
502
|
+
description: locale === "ko"
|
|
503
|
+
? "생성된 provider wiring이 APIFuse runtime contract를 통해 작은 샘플 payload를 정상적으로 round-trip하는지 확인합니다. 로컬 개발, baseline check, 첫 bounty scaffold 검증에 사용합니다. production data retrieval이나 upstream-specific workflow에는 사용하지 마세요. 이 starter operation은 생성된 프로젝트가 compile, serve, input/output round-trip을 수행하는지 증명하기 위한 용도입니다."
|
|
504
|
+
: "Confirms the generated provider wiring is operational by echoing a small sample payload through the APIFuse runtime contract. Use when validating local development, baseline checks, or first-pass bounty scaffolds. Do NOT use for production data retrieval or upstream-specific workflows because this starter operation exists only to prove the generated project compiles, serves, and round-trips input/output correctly.",
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
schemaDescriptions: {
|
|
508
|
+
input: {
|
|
509
|
+
root: locale === "ko"
|
|
510
|
+
? "생성된 ping operation의 입력 payload"
|
|
511
|
+
: "Input payload for the generated ping operation.",
|
|
512
|
+
value: locale === "ko"
|
|
513
|
+
? "생성된 provider scaffold wiring 검증에 사용하는 샘플 입력값"
|
|
514
|
+
: "Sample input value used to verify the generated provider scaffold is wired correctly.",
|
|
515
|
+
},
|
|
516
|
+
output: {
|
|
517
|
+
root: locale === "ko"
|
|
518
|
+
? "생성된 ping operation이 반환하는 출력 payload"
|
|
519
|
+
: "Output payload returned by the generated ping operation.",
|
|
520
|
+
ok: locale === "ko"
|
|
521
|
+
? "생성된 provider가 샘플 요청을 성공적으로 처리했는지 여부"
|
|
522
|
+
: "Whether the generated provider handled the sample request successfully.",
|
|
523
|
+
message: locale === "ko"
|
|
524
|
+
? "생성된 provider가 샘플 payload를 round-trip했음을 보여주는 사람이 읽을 수 있는 확인 메시지"
|
|
525
|
+
: "Human-readable confirmation that the generated provider round-tripped the sample payload.",
|
|
526
|
+
pageTitle: locale === "ko"
|
|
527
|
+
? "browser 런타임 provider일 때 로드된 페이지의 제목 (해당되지 않으면 생략)"
|
|
528
|
+
: "Title of the loaded page when the provider uses the browser runtime; omitted otherwise.",
|
|
529
|
+
frameCount: locale === "ko"
|
|
530
|
+
? "browser 런타임 provider일 때 로드된 페이지의 frame 개수 (해당되지 않으면 생략)"
|
|
531
|
+
: "Number of frames in the loaded page when the provider uses the browser runtime; omitted otherwise.",
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
return `${JSON.stringify(catalog, null, 2)}\n`;
|
|
536
|
+
}
|
|
537
|
+
function renderAuthBlock(authMode) {
|
|
538
|
+
switch (authMode) {
|
|
539
|
+
case "none":
|
|
540
|
+
return '{ mode: "none" }';
|
|
541
|
+
case "platform-managed":
|
|
542
|
+
return '{ mode: "platform-managed" }';
|
|
543
|
+
case "credentials":
|
|
544
|
+
return `{
|
|
545
|
+
mode: "credentials",
|
|
546
|
+
flow: {
|
|
547
|
+
start: async () => ({
|
|
548
|
+
kind: "input",
|
|
549
|
+
turnId: crypto.randomUUID(),
|
|
550
|
+
expectedInput: {
|
|
551
|
+
schema: {
|
|
552
|
+
type: "object",
|
|
553
|
+
required: ["username", "password"],
|
|
554
|
+
properties: {
|
|
555
|
+
username: { type: "string", minLength: 1 },
|
|
556
|
+
password: { type: "string", minLength: 1 },
|
|
557
|
+
},
|
|
558
|
+
additionalProperties: false,
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
hint: "Replace the generated credential prompt with the real upstream login flow.",
|
|
562
|
+
}),
|
|
563
|
+
continue: async (_ctx, input = {}) => ({
|
|
564
|
+
kind: "complete",
|
|
565
|
+
turnId: crypto.randomUUID(),
|
|
566
|
+
data: {
|
|
567
|
+
credential: {
|
|
568
|
+
username: String(input.username ?? ""),
|
|
569
|
+
password: String(input.password ?? ""),
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
hint: "Generated placeholder credential flow completed. Replace this with real auth logic.",
|
|
573
|
+
}),
|
|
574
|
+
refresh: async () => ({
|
|
575
|
+
kind: "complete",
|
|
576
|
+
turnId: crypto.randomUUID(),
|
|
577
|
+
data: {
|
|
578
|
+
credential: {
|
|
579
|
+
username: "replace-with-refreshed-username",
|
|
580
|
+
password: "replace-with-refreshed-password",
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
hint: "Return refreshed credential data here, or throw AuthError with code AUTH_REQUIRED when silent refresh is not possible.",
|
|
584
|
+
}),
|
|
585
|
+
},
|
|
586
|
+
}`;
|
|
587
|
+
case "oauth2":
|
|
588
|
+
return `{
|
|
589
|
+
mode: "oauth2",
|
|
590
|
+
flow: {
|
|
591
|
+
start: async () => ({
|
|
592
|
+
kind: "redirect",
|
|
593
|
+
turnId: crypto.randomUUID(),
|
|
594
|
+
data: {
|
|
595
|
+
authorizeUrl: "https://example.com/oauth/authorize",
|
|
596
|
+
},
|
|
597
|
+
hint: "Replace the generated OAuth authorize URL and token exchange flow.",
|
|
598
|
+
}),
|
|
599
|
+
continue: async () => ({
|
|
600
|
+
kind: "complete",
|
|
601
|
+
turnId: crypto.randomUUID(),
|
|
602
|
+
data: {
|
|
603
|
+
credential: {},
|
|
604
|
+
},
|
|
605
|
+
hint: "Generated placeholder OAuth flow completed. Replace this with the real token exchange logic.",
|
|
606
|
+
}),
|
|
607
|
+
},
|
|
608
|
+
}`;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function renderSecretsBlock(authMode) {
|
|
612
|
+
switch (authMode) {
|
|
613
|
+
case "platform-managed":
|
|
614
|
+
return 'secrets: [{ name: "EXAMPLE_API_KEY", required: true }],\n ';
|
|
615
|
+
case "oauth2":
|
|
616
|
+
return 'secrets: [{ name: "EXAMPLE_OAUTH_CLIENT_ID", required: true }, { name: "EXAMPLE_OAUTH_CLIENT_SECRET", required: true }],\n ';
|
|
617
|
+
default:
|
|
618
|
+
return "";
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function renderCredentialBlock(authMode) {
|
|
622
|
+
if (authMode !== "credentials") {
|
|
623
|
+
return "";
|
|
624
|
+
}
|
|
625
|
+
return `credential: {
|
|
626
|
+
keys: ["username", "password"],
|
|
627
|
+
storesReusableSecret: true,
|
|
628
|
+
justification:
|
|
629
|
+
"The generated credential starter persists reusable login fields so contributors can replace the placeholder flow with a real upstream session exchange without rewriting the surrounding contract.",
|
|
630
|
+
},
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
function validateProviderName(value) {
|
|
634
|
+
const normalized = value.trim();
|
|
635
|
+
if (!normalized) {
|
|
636
|
+
throw new Error("Provider name is required.");
|
|
637
|
+
}
|
|
638
|
+
if (!PROVIDER_NAME_REGEX.test(normalized)) {
|
|
639
|
+
throw new Error("Use kebab-case, e.g. my-provider.");
|
|
640
|
+
}
|
|
641
|
+
return normalized;
|
|
642
|
+
}
|
|
643
|
+
export function toDisplayName(name) {
|
|
644
|
+
return name
|
|
645
|
+
.split("-")
|
|
646
|
+
.filter(Boolean)
|
|
647
|
+
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
648
|
+
.join(" ");
|
|
649
|
+
}
|
|
650
|
+
function findApifuseInternalWorkspaceRoot(cwd) {
|
|
651
|
+
let currentDirectory = cwd;
|
|
652
|
+
while (true) {
|
|
653
|
+
if (isApifuseInternalWorkspaceRoot(currentDirectory)) {
|
|
654
|
+
return currentDirectory;
|
|
655
|
+
}
|
|
656
|
+
const parentDirectory = dirname(currentDirectory);
|
|
657
|
+
if (parentDirectory === currentDirectory) {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
currentDirectory = parentDirectory;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function isApifuseInternalWorkspaceRoot(workspaceRoot) {
|
|
664
|
+
const providerSdkPackageJsonPath = resolve(workspaceRoot, "packages", "provider-sdk", "package.json");
|
|
665
|
+
if (!existsSync(providerSdkPackageJsonPath)) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
const packageJson = JSON.parse(readFileSync(providerSdkPackageJsonPath, "utf8"));
|
|
670
|
+
return (typeof packageJson === "object" &&
|
|
671
|
+
packageJson !== null &&
|
|
672
|
+
"name" in packageJson &&
|
|
673
|
+
packageJson.name === "@apifuse/provider-sdk");
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function writePlan(plan) {
|
|
680
|
+
for (const file of plan.files) {
|
|
681
|
+
await mkdir(dirname(file.path), { recursive: true });
|
|
682
|
+
await writeFile(file.path, file.content);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function installDependencies(plan, jsonMode) {
|
|
686
|
+
await runCommand(plan.installCommand, plan.installCwd, jsonMode);
|
|
687
|
+
}
|
|
688
|
+
async function runBaselineValidation(plan, jsonMode) {
|
|
689
|
+
for (const command of plan.validationCommands) {
|
|
690
|
+
await runCommand(command, plan.providerRoot, jsonMode);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async function runCommand(command, cwd, jsonMode) {
|
|
694
|
+
const [binary, ...args] = command.split(" ");
|
|
695
|
+
if (!binary) {
|
|
696
|
+
throw new Error(`Cannot run empty command in ${cwd}`);
|
|
697
|
+
}
|
|
698
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
699
|
+
const child = spawn(binary, args, {
|
|
700
|
+
cwd,
|
|
701
|
+
env: process.env,
|
|
702
|
+
stdio: jsonMode ? "pipe" : "inherit",
|
|
703
|
+
shell: false,
|
|
704
|
+
});
|
|
705
|
+
let stdout = "";
|
|
706
|
+
let stderr = "";
|
|
707
|
+
child.stdout?.on("data", (chunk) => {
|
|
708
|
+
stdout += chunk.toString();
|
|
709
|
+
});
|
|
710
|
+
child.stderr?.on("data", (chunk) => {
|
|
711
|
+
stderr += chunk.toString();
|
|
712
|
+
});
|
|
713
|
+
child.on("error", rejectPromise);
|
|
714
|
+
child.on("close", (code) => {
|
|
715
|
+
if (code === 0) {
|
|
716
|
+
resolvePromise();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
rejectPromise(new Error(`Command failed (${command}) in ${cwd}${stdout || stderr
|
|
720
|
+
? `\n${[stdout, stderr].filter(Boolean).join("\n")}`
|
|
721
|
+
: ""}`));
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function printResult(plan, jsonMode, dryRun) {
|
|
726
|
+
const payload = {
|
|
727
|
+
success: true,
|
|
728
|
+
dryRun,
|
|
729
|
+
preset: plan.preset,
|
|
730
|
+
provider: {
|
|
731
|
+
name: plan.name,
|
|
732
|
+
displayName: plan.displayName,
|
|
733
|
+
packageName: plan.packageName,
|
|
734
|
+
outputDir: plan.outputDir,
|
|
735
|
+
},
|
|
736
|
+
install: {
|
|
737
|
+
cwd: plan.installCwd,
|
|
738
|
+
command: plan.installCommand,
|
|
739
|
+
},
|
|
740
|
+
validationCommands: plan.validationCommands,
|
|
741
|
+
nextDevCommand: plan.nextDevCommand,
|
|
742
|
+
files: plan.files.map((file) => relative(plan.providerRoot, file.path) || file.path),
|
|
743
|
+
};
|
|
744
|
+
if (jsonMode) {
|
|
745
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
outro(`${dryRun ? "Planned" : "Created"} ${plan.outputDir}`);
|
|
749
|
+
console.log(`\nPreset: ${plan.preset}`);
|
|
750
|
+
console.log(`Install: (cd ${plan.installCwd} && ${plan.installCommand})`);
|
|
751
|
+
for (const command of plan.validationCommands) {
|
|
752
|
+
console.log(`Validation: (cd ${plan.providerRoot} && ${command})`);
|
|
753
|
+
}
|
|
754
|
+
console.log(`Next local dev: ${plan.nextDevCommand}`);
|
|
755
|
+
console.log("Submission evidence: run `bun run submit-check`, save the generated report, and note `/health` plus `POST /v1/{operation}` smoke results.");
|
|
756
|
+
if (plan.files.some((file) => file.content.includes('runtime: "browser"'))) {
|
|
757
|
+
console.log("Browser runtime: run `bunx playwright install chromium` locally or set `APIFUSE__CDP_POOL__URL` before browser-backed smoke tests.");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function escapeTemplate(value) {
|
|
761
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
762
|
+
}
|