@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +53 -0
- package/CHANGELOG.md +8 -0
- package/README.md +5 -1
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +26 -1
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-submit-check.ts +193 -2
- package/bin/apifuse-sync-assets.ts +117 -0
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/executor.js +7 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/server/serve.d.ts +5 -0
- package/dist/server/serve.js +39 -0
- package/package.json +1 -1
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/index.ts +5 -0
- package/src/runtime/executor.ts +8 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/server/serve.ts +53 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
|
@@ -4,8 +4,8 @@ You are building an APIFuse provider. APIFuse turns messy upstream APIs into
|
|
|
4
4
|
normalized, typed, evidence-backed public APIs. A provider that merely proxies
|
|
5
5
|
the upstream is a failed provider, even if every check passes.
|
|
6
6
|
|
|
7
|
-
This file is the core contract. Detailed procedures live in
|
|
8
|
-
the matching skill BEFORE working on that area (index at the bottom).
|
|
7
|
+
This file is the core contract. Detailed procedures live in `.agents/skills/` —
|
|
8
|
+
load the matching skill BEFORE working on that area (index at the bottom).
|
|
9
9
|
|
|
10
10
|
## Non-negotiable principles
|
|
11
11
|
|
|
@@ -75,13 +75,22 @@ bun run submit-check # structural score; a high score does NOT prove quality
|
|
|
75
75
|
`submit-check` is a structural gate. Every principle above can be violated
|
|
76
76
|
while scoring 95/100 — reviewers and CI audit for exactly these classes.
|
|
77
77
|
|
|
78
|
+
## Managed prompt assets
|
|
79
|
+
|
|
80
|
+
`AGENTS.md` and `.agents/skills/**` are generated by the SDK; `CLAUDE.md`,
|
|
81
|
+
`.claude`, and `.codex` are symlinks onto them so every agent CLI reads the
|
|
82
|
+
same contract. Do not hand-edit these files — regenerate them with
|
|
83
|
+
`bun run sync-assets` (or `bunx apifuse sync-assets .`). `apifuse check` and
|
|
84
|
+
`submit-check` enforce a freshness gate: stale or modified prompt assets
|
|
85
|
+
(tracked in `.apifuse/prompt-assets.json`) block submission until re-synced.
|
|
86
|
+
|
|
78
87
|
## Skill index — load before working on:
|
|
79
88
|
|
|
80
89
|
| Area | Load |
|
|
81
90
|
| --- | --- |
|
|
82
|
-
| Output schemas, mappers, field naming, timestamps, enums |
|
|
83
|
-
| Upstream request params, new endpoint wiring, field mapping |
|
|
84
|
-
| Recording fixtures, writing tests against fixtures |
|
|
85
|
-
| List operations, paging, totals, client-side filtering |
|
|
86
|
-
| healthCheck blocks, error classification, fail-closed guards |
|
|
87
|
-
| Upstream-specific known pitfalls for THIS bounty |
|
|
91
|
+
| Output schemas, mappers, field naming, timestamps, enums | `.agents/skills/normalization-standards/SKILL.md` |
|
|
92
|
+
| Upstream request params, new endpoint wiring, field mapping | `.agents/skills/upstream-contract-verification/SKILL.md` |
|
|
93
|
+
| Recording fixtures, writing tests against fixtures | `.agents/skills/fixtures-and-recording/SKILL.md` |
|
|
94
|
+
| List operations, paging, totals, client-side filtering | `.agents/skills/pagination-and-counts/SKILL.md` |
|
|
95
|
+
| healthCheck blocks, error classification, fail-closed guards | `.agents/skills/health-checks-and-fail-closed/SKILL.md` |
|
|
96
|
+
| Upstream-specific known pitfalls for THIS bounty | `.agents/skills/upstream-notes/` (read every file) |
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export { generateInsights } from "./runtime/insights.js";
|
|
|
27
27
|
export { type InstrumentationOptions, type InstrumentedProviderContext, wrapWithInstrumentation, } from "./runtime/instrumentation.js";
|
|
28
28
|
export { type PrevalidateResult, prevalidate } from "./runtime/prevalidate.js";
|
|
29
29
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
30
|
+
export { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "./runtime/secrets.js";
|
|
30
31
|
export { createUnsupportedProviderRuntimeState, UnsupportedProviderStateError, } from "./runtime/state.js";
|
|
31
32
|
export { createStealthClient } from "./runtime/stealth.js";
|
|
32
33
|
export { APIFUSE__STT__BACKEND_ENV, APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__STT__MODEL_ENV, createSttClientFromEnv, createUnsupportedSttClient, extractVerificationCode, resolveSttPrompt, } from "./runtime/stt.js";
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { generateInsights } from "./runtime/insights.js";
|
|
|
24
24
|
export { wrapWithInstrumentation, } from "./runtime/instrumentation.js";
|
|
25
25
|
export { prevalidate } from "./runtime/prevalidate.js";
|
|
26
26
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
27
|
+
export { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "./runtime/secrets.js";
|
|
27
28
|
export { createUnsupportedProviderRuntimeState, UnsupportedProviderStateError, } from "./runtime/state.js";
|
|
28
29
|
export { createStealthClient } from "./runtime/stealth.js";
|
|
29
30
|
export { APIFUSE__STT__BACKEND_ENV, APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__STT__MODEL_ENV, createSttClientFromEnv, createUnsupportedSttClient, extractVerificationCode, resolveSttPrompt, } from "./runtime/stt.js";
|
package/dist/runtime/executor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isSessionExpiredError, ProviderError, SessionExpiredError } from "../errors.js";
|
|
2
2
|
import { parseSchema } from "../schema.js";
|
|
3
|
+
import { assertRequiredSecretsPresent } from "./secrets.js";
|
|
3
4
|
export function isStreamingOperation(provider, operationId) {
|
|
4
5
|
const kind = provider.operations[operationId]?.transport?.kind ?? "json";
|
|
5
6
|
return kind !== "json";
|
|
@@ -23,6 +24,12 @@ export async function executeOperation(provider, operationId, ctx, input, _optio
|
|
|
23
24
|
fix: `Valid operations: ${Object.keys(provider.operations).join(", ")}`,
|
|
24
25
|
});
|
|
25
26
|
}
|
|
27
|
+
// SDK-owned secret presence gate (single source of truth): declared
|
|
28
|
+
// `required: true` secrets are validated here, before input parsing and the
|
|
29
|
+
// handler, so every invocation path (serve /v1, self-test probes, perf,
|
|
30
|
+
// record) fails with the same structured MISSING_SECRET error instead of a
|
|
31
|
+
// handler-specific crash. Providers must not re-check presence locally.
|
|
32
|
+
assertRequiredSecretsPresent(provider, ctx.env);
|
|
26
33
|
const validatedInput = await parseSchema(operation.input, input, `operations.${operationId}.input`);
|
|
27
34
|
const execute = () => ctx.trace.span(`handler:${operationId}`, () => Promise.resolve(operation.handler(ctx, validatedInput)));
|
|
28
35
|
let result;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EnvContext, ProviderDefinition } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical error code for a declared-but-unprovisioned provider secret.
|
|
4
|
+
*
|
|
5
|
+
* The SDK is the single source of truth for env/secret presence validation:
|
|
6
|
+
* providers declare secrets in `defineProvider({ secrets: [...] })` and the
|
|
7
|
+
* runtime enforces presence before any handler or auth-flow code runs.
|
|
8
|
+
* Provider-local presence guards (requireServiceKey/requireApiKey style) are a
|
|
9
|
+
* deprecated antipattern — see the `sdk-owned-secret-presence` submit-check
|
|
10
|
+
* rule.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MISSING_SECRET_CODE = "MISSING_SECRET";
|
|
13
|
+
/**
|
|
14
|
+
* Names of declared `required: true` secrets whose env values are unset or
|
|
15
|
+
* whitespace-only. Whitespace-only values count as missing for parity with the
|
|
16
|
+
* `.trim()` guards well-built providers used before the SDK owned this check —
|
|
17
|
+
* a blank value provisioned by a broken secret pipeline must not pass the gate.
|
|
18
|
+
*/
|
|
19
|
+
export declare function listMissingRequiredSecrets(provider: ProviderDefinition, env: EnvContext): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Throws the canonical structured missing-secret error when any declared
|
|
22
|
+
* `required: true` secret is absent. All missing names are reported in a
|
|
23
|
+
* single error so operators can provision the full set in one pass instead of
|
|
24
|
+
* discovering them one deploy at a time (the 2026-07-22 unprovisioned-secret
|
|
25
|
+
* incident failure mode).
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertRequiredSecretsPresent(provider: ProviderDefinition, env: EnvContext): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ProviderSecretError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical error code for a declared-but-unprovisioned provider secret.
|
|
4
|
+
*
|
|
5
|
+
* The SDK is the single source of truth for env/secret presence validation:
|
|
6
|
+
* providers declare secrets in `defineProvider({ secrets: [...] })` and the
|
|
7
|
+
* runtime enforces presence before any handler or auth-flow code runs.
|
|
8
|
+
* Provider-local presence guards (requireServiceKey/requireApiKey style) are a
|
|
9
|
+
* deprecated antipattern — see the `sdk-owned-secret-presence` submit-check
|
|
10
|
+
* rule.
|
|
11
|
+
*/
|
|
12
|
+
export const MISSING_SECRET_CODE = "MISSING_SECRET";
|
|
13
|
+
/**
|
|
14
|
+
* Names of declared `required: true` secrets whose env values are unset or
|
|
15
|
+
* whitespace-only. Whitespace-only values count as missing for parity with the
|
|
16
|
+
* `.trim()` guards well-built providers used before the SDK owned this check —
|
|
17
|
+
* a blank value provisioned by a broken secret pipeline must not pass the gate.
|
|
18
|
+
*/
|
|
19
|
+
export function listMissingRequiredSecrets(provider, env) {
|
|
20
|
+
const missing = [];
|
|
21
|
+
for (const secret of provider.secrets ?? []) {
|
|
22
|
+
if (secret.required !== true) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const value = env.get(secret.name);
|
|
26
|
+
if (value === undefined || value.trim() === "") {
|
|
27
|
+
missing.push(secret.name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return missing;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Throws the canonical structured missing-secret error when any declared
|
|
34
|
+
* `required: true` secret is absent. All missing names are reported in a
|
|
35
|
+
* single error so operators can provision the full set in one pass instead of
|
|
36
|
+
* discovering them one deploy at a time (the 2026-07-22 unprovisioned-secret
|
|
37
|
+
* incident failure mode).
|
|
38
|
+
*/
|
|
39
|
+
export function assertRequiredSecretsPresent(provider, env) {
|
|
40
|
+
const missing = listMissingRequiredSecrets(provider, env);
|
|
41
|
+
if (missing.length === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const names = missing.join(", ");
|
|
45
|
+
throw new ProviderSecretError(`Missing required provider secret${missing.length > 1 ? "s" : ""}: ${names}`, {
|
|
46
|
+
code: MISSING_SECRET_CODE,
|
|
47
|
+
category: "credential_unavailable",
|
|
48
|
+
retryable: false,
|
|
49
|
+
fix: `Provision ${names} in the provider environment (e.g. Doppler). Declared in defineProvider({ secrets: [...] }).`,
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/server/serve.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
|
|
|
35
35
|
message: string;
|
|
36
36
|
}>;
|
|
37
37
|
}) | {
|
|
38
|
+
level: "warn";
|
|
39
|
+
event: "provider_secrets_missing";
|
|
40
|
+
providerId: string;
|
|
41
|
+
missingSecrets: string[];
|
|
42
|
+
} | {
|
|
38
43
|
level: "warn";
|
|
39
44
|
event: "provider_cleanup_failed";
|
|
40
45
|
providerId: string;
|
package/dist/server/serve.js
CHANGED
|
@@ -18,6 +18,7 @@ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
|
18
18
|
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
19
19
|
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
|
|
20
20
|
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
|
|
21
|
+
import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
|
|
21
22
|
import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
|
|
22
23
|
import { createStealthClient } from "../runtime/stealth.js";
|
|
23
24
|
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
@@ -357,6 +358,18 @@ function providerObservabilityDetails(error) {
|
|
|
357
358
|
retryable: error.options?.retryable ?? false,
|
|
358
359
|
};
|
|
359
360
|
}
|
|
361
|
+
// Missing-secret errors carry the canonical credential_unavailable category
|
|
362
|
+
// so Gateway/observability can attribute the failure to provisioning, not
|
|
363
|
+
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
364
|
+
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
365
|
+
// serialize identically, including across duplicate SDK module instances.
|
|
366
|
+
if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
|
|
367
|
+
return {
|
|
368
|
+
category: error.options?.category ?? "credential_unavailable",
|
|
369
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
370
|
+
retryable: error.options?.retryable ?? false,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
360
373
|
if (!isTransportError(error)) {
|
|
361
374
|
return undefined;
|
|
362
375
|
}
|
|
@@ -422,6 +435,10 @@ function toStatusCode(error) {
|
|
|
422
435
|
case "AUTH_REQUIRED":
|
|
423
436
|
case "reauth_required":
|
|
424
437
|
return 401;
|
|
438
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
439
|
+
// upstream failure — explicit 400 (was only reached via fallthrough).
|
|
440
|
+
case MISSING_SECRET_CODE:
|
|
441
|
+
return 400;
|
|
425
442
|
case "NOT_FOUND":
|
|
426
443
|
case "not_found":
|
|
427
444
|
case "NO_DATA":
|
|
@@ -885,8 +902,16 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
|
885
902
|
code: "AUTH_FLOW_NOT_CONFIGURED",
|
|
886
903
|
});
|
|
887
904
|
}
|
|
905
|
+
// Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
|
|
906
|
+
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
907
|
+
// any flow code runs instead of at whatever point the ceremony first reads
|
|
908
|
+
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
909
|
+
// stranded flow even when provisioning is broken.
|
|
888
910
|
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
889
911
|
try {
|
|
912
|
+
if (route !== "abort") {
|
|
913
|
+
assertRequiredSecretsPresent(provider, context.env);
|
|
914
|
+
}
|
|
890
915
|
const result = route === "start"
|
|
891
916
|
? await flow.start(context)
|
|
892
917
|
: route === "continue"
|
|
@@ -933,6 +958,20 @@ export function createServerApp(provider, options = {}) {
|
|
|
933
958
|
providerId: provider.id,
|
|
934
959
|
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
935
960
|
});
|
|
961
|
+
// Boot-time visibility for unprovisioned declared secrets: emit a structured
|
|
962
|
+
// warn so deploy tooling/alerting sees the gap the moment the pod boots,
|
|
963
|
+
// instead of discovering it request-by-request. Deliberately log-only — a
|
|
964
|
+
// boot crash would trade a structured MISSING_SECRET signal for
|
|
965
|
+
// CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
|
|
966
|
+
const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
|
|
967
|
+
if (missingSecretsAtBoot.length > 0) {
|
|
968
|
+
logger({
|
|
969
|
+
level: "warn",
|
|
970
|
+
event: "provider_secrets_missing",
|
|
971
|
+
providerId: provider.id,
|
|
972
|
+
missingSecrets: missingSecretsAtBoot,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
936
975
|
app.notFound((c) => c.json({
|
|
937
976
|
error: {
|
|
938
977
|
code: "not_found",
|
package/package.json
CHANGED
package/src/cli/commands.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type ApifuseCommandName =
|
|
|
2
2
|
| "create"
|
|
3
3
|
| "dev"
|
|
4
4
|
| "check"
|
|
5
|
+
| "sync-assets"
|
|
5
6
|
| "submit-check"
|
|
6
7
|
| "bounty-check"
|
|
7
8
|
| "record"
|
|
@@ -46,6 +47,14 @@ export const COMMAND_MANIFEST: Record<
|
|
|
46
47
|
examples: ["apifuse check .", "apifuse check providers/korea-air-quality"],
|
|
47
48
|
modulePath: "./apifuse-check",
|
|
48
49
|
},
|
|
50
|
+
"sync-assets": {
|
|
51
|
+
name: "sync-assets",
|
|
52
|
+
summary:
|
|
53
|
+
"Regenerate SDK-managed agent prompt assets (AGENTS.md, .agents/skills, symlinks, manifest) for the installed SDK version.",
|
|
54
|
+
usage: "apifuse sync-assets [path] [--check]",
|
|
55
|
+
examples: ["apifuse sync-assets .", "apifuse sync-assets . --check"],
|
|
56
|
+
modulePath: "./apifuse-sync-assets",
|
|
57
|
+
},
|
|
49
58
|
"submit-check": {
|
|
50
59
|
name: "submit-check",
|
|
51
60
|
summary:
|
|
@@ -103,6 +112,7 @@ export const COMMAND_ORDER: ApifuseCommandName[] = [
|
|
|
103
112
|
"create",
|
|
104
113
|
"dev",
|
|
105
114
|
"check",
|
|
115
|
+
"sync-assets",
|
|
106
116
|
"submit-check",
|
|
107
117
|
"record",
|
|
108
118
|
"test",
|
package/src/cli/create.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, relative, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
@@ -8,6 +8,11 @@ import { cancel, intro, isCancel, note, outro, select, text } from "@clack/promp
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
10
10
|
import packageJson from "../../package.json";
|
|
11
|
+
import {
|
|
12
|
+
buildPromptAssetManifest,
|
|
13
|
+
buildPromptAssetPlanEntries,
|
|
14
|
+
PROMPT_ASSET_MANIFEST_PATH,
|
|
15
|
+
} from "./prompt-assets.js";
|
|
11
16
|
|
|
12
17
|
export const PROVIDER_NAME_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
13
18
|
export const CATEGORY_OPTIONS = [
|
|
@@ -64,7 +69,10 @@ export type CreateResolvedOptions = {
|
|
|
64
69
|
|
|
65
70
|
export type ProviderPlanFile = {
|
|
66
71
|
path: string;
|
|
72
|
+
/** File content, or the symlink target (relative path) for kind "symlink". */
|
|
67
73
|
content: string;
|
|
74
|
+
/** Absent kind means "file" (backward compatible with older consumers). */
|
|
75
|
+
kind?: "file" | "symlink";
|
|
68
76
|
};
|
|
69
77
|
|
|
70
78
|
export type ProviderCreatePlan = {
|
|
@@ -410,11 +418,16 @@ export async function buildProviderCreatePlan(
|
|
|
410
418
|
);
|
|
411
419
|
}
|
|
412
420
|
|
|
421
|
+
// Exact pin, not a caret range: the prompt-asset freshness gate requires
|
|
422
|
+
// manifest.sdkVersion === installed SDK version, and the manifest records
|
|
423
|
+
// this CLI's own version. A caret range would let `bun install` resolve a
|
|
424
|
+
// newer SDK than the create binary, making a fresh scaffold fail its own
|
|
425
|
+
// gate. `sync-assets` after an explicit SDK upgrade re-records the version.
|
|
413
426
|
const sdkSpecifier =
|
|
414
427
|
options.sdkSpecifier ??
|
|
415
428
|
(options.preset === "monorepo" && resolvedWorkspaceRoot
|
|
416
429
|
? "workspace:*"
|
|
417
|
-
:
|
|
430
|
+
: packageJson.version);
|
|
418
431
|
const relativeProviderRoot = relative(cwd, providerRoot) || options.name;
|
|
419
432
|
const nextDevCommand = `cd ${relativeProviderRoot} && bun run dev`;
|
|
420
433
|
const packageName =
|
|
@@ -538,40 +551,23 @@ export async function buildProviderCreatePlan(
|
|
|
538
551
|
PROVIDER_ID: options.name,
|
|
539
552
|
}),
|
|
540
553
|
},
|
|
541
|
-
{
|
|
542
|
-
path: resolve(providerRoot, "AGENTS.md"),
|
|
543
|
-
content: await renderTemplate("AGENTS.md.tpl", {}),
|
|
544
|
-
},
|
|
545
|
-
{
|
|
546
|
-
path: resolve(providerRoot, "CLAUDE.md"),
|
|
547
|
-
content: await renderTemplate("CLAUDE.md.tpl", {}),
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
path: resolve(providerRoot, "skills", "normalization-standards", "SKILL.md"),
|
|
551
|
-
content: await renderTemplate("skills/normalization-standards/SKILL.md.tpl", {}),
|
|
552
|
-
},
|
|
553
|
-
{
|
|
554
|
-
path: resolve(providerRoot, "skills", "upstream-contract-verification", "SKILL.md"),
|
|
555
|
-
content: await renderTemplate("skills/upstream-contract-verification/SKILL.md.tpl", {}),
|
|
556
|
-
},
|
|
557
|
-
{
|
|
558
|
-
path: resolve(providerRoot, "skills", "fixtures-and-recording", "SKILL.md"),
|
|
559
|
-
content: await renderTemplate("skills/fixtures-and-recording/SKILL.md.tpl", {}),
|
|
560
|
-
},
|
|
561
|
-
{
|
|
562
|
-
path: resolve(providerRoot, "skills", "pagination-and-counts", "SKILL.md"),
|
|
563
|
-
content: await renderTemplate("skills/pagination-and-counts/SKILL.md.tpl", {}),
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
path: resolve(providerRoot, "skills", "health-checks-and-fail-closed", "SKILL.md"),
|
|
567
|
-
content: await renderTemplate("skills/health-checks-and-fail-closed/SKILL.md.tpl", {}),
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
path: resolve(providerRoot, "skills", "upstream-notes", "README.md"),
|
|
571
|
-
content: await renderTemplate("skills/upstream-notes/README.md.tpl", {}),
|
|
572
|
-
},
|
|
573
554
|
];
|
|
574
555
|
|
|
556
|
+
const promptAssetEntries = await buildPromptAssetPlanEntries(renderTemplate);
|
|
557
|
+
for (const entry of promptAssetEntries) {
|
|
558
|
+
files.push({
|
|
559
|
+
path: resolve(providerRoot, entry.path),
|
|
560
|
+
content: entry.content,
|
|
561
|
+
...(entry.kind === "symlink" ? { kind: "symlink" as const } : {}),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
// Manifest last: writePlan writes in order, so a crash mid-scaffold never
|
|
565
|
+
// leaves a manifest that claims assets which were not written.
|
|
566
|
+
files.push({
|
|
567
|
+
path: resolve(providerRoot, PROMPT_ASSET_MANIFEST_PATH),
|
|
568
|
+
content: buildPromptAssetManifest(promptAssetEntries, packageJson.version),
|
|
569
|
+
});
|
|
570
|
+
|
|
575
571
|
return {
|
|
576
572
|
displayName: options.displayName,
|
|
577
573
|
files,
|
|
@@ -614,6 +610,7 @@ function renderPackageJson(input: { packageName: string; sdkSpecifier: string })
|
|
|
614
610
|
check: "apifuse check . && bun run type-check",
|
|
615
611
|
"type-check": "tsc --noEmit",
|
|
616
612
|
"submit-check": "apifuse submit-check . --markdown submission-report.md",
|
|
613
|
+
"sync-assets": "apifuse sync-assets .",
|
|
617
614
|
test: "apifuse test .",
|
|
618
615
|
record: "apifuse record .",
|
|
619
616
|
start: "bun start.ts",
|
|
@@ -869,6 +866,13 @@ function isApifuseInternalWorkspaceRoot(workspaceRoot: string): boolean {
|
|
|
869
866
|
async function writePlan(plan: ProviderCreatePlan): Promise<void> {
|
|
870
867
|
for (const file of plan.files) {
|
|
871
868
|
await mkdir(dirname(file.path), { recursive: true });
|
|
869
|
+
if (file.kind === "symlink") {
|
|
870
|
+
// rm operates on the link itself (lstat semantics) and tolerates
|
|
871
|
+
// directories, so whatever occupies the path is replaced by the link.
|
|
872
|
+
await rm(file.path, { recursive: true, force: true });
|
|
873
|
+
await symlink(file.content, file.path);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
872
876
|
await writeFile(file.path, file.content);
|
|
873
877
|
}
|
|
874
878
|
}
|
|
@@ -940,7 +944,10 @@ function printResult(plan: ProviderCreatePlan, jsonMode: boolean, dryRun: boolea
|
|
|
940
944
|
},
|
|
941
945
|
validationCommands: plan.validationCommands,
|
|
942
946
|
nextDevCommand: plan.nextDevCommand,
|
|
943
|
-
files: plan.files.map((file) =>
|
|
947
|
+
files: plan.files.map((file) => {
|
|
948
|
+
const relativePath = relative(plan.providerRoot, file.path) || file.path;
|
|
949
|
+
return file.kind === "symlink" ? `${relativePath} -> ${file.content} (symlink)` : relativePath;
|
|
950
|
+
}),
|
|
944
951
|
};
|
|
945
952
|
|
|
946
953
|
if (jsonMode) {
|