@ryuhq/sdk 0.1.14 → 0.2.2
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/README.md +69 -7
- package/dist/action.cjs +839 -0
- package/dist/action.d.cts +88 -0
- package/dist/action.d.ts +88 -0
- package/dist/action.js +8 -0
- package/dist/agent-plugin.cjs +46 -0
- package/dist/agent-plugin.d.cts +43 -33
- package/dist/agent-plugin.d.ts +43 -33
- package/dist/agent-plugin.js +1 -1
- package/dist/agent.cjs +16 -0
- package/dist/agent.d.cts +377 -1
- package/dist/agent.d.ts +377 -1
- package/dist/agent.js +4 -2
- package/dist/app-B0Z9Ew_R.d.cts +141 -0
- package/dist/app-C-BDJwfG.d.ts +141 -0
- package/dist/builder.cjs +1194 -0
- package/dist/builder.d.cts +217 -0
- package/dist/builder.d.ts +217 -0
- package/dist/builder.js +29 -0
- package/dist/chunk-4TPUZDTI.js +94 -0
- package/dist/chunk-BC3A7HMO.js +164 -0
- package/dist/chunk-FZSFZOIN.js +200 -0
- package/dist/{chunk-ODFEUVPW.js → chunk-HACAGK65.js} +1 -18
- package/dist/chunk-HLKJZAFK.js +361 -0
- package/dist/chunk-IKEDLLFY.js +19 -0
- package/dist/{chunk-G6FLVEC4.js → chunk-JX6DDRXV.js} +46 -0
- package/dist/{chunk-AO2KJRDD.js → chunk-NZKVOSC2.js} +192 -3
- package/dist/chunk-QYFUNJOH.js +83 -0
- package/dist/chunk-SN2QBJUF.js +148 -0
- package/dist/chunk-TLDPEGC7.js +21 -0
- package/dist/chunk-TXSHHZF2.js +0 -0
- package/dist/chunk-VLIRNNAE.js +154 -0
- package/dist/{chunk-MTUBUPIV.js → chunk-Z57QDDJR.js} +8 -227
- package/dist/chunk-ZTJWBRUL.js +282 -0
- package/dist/cli.cjs +347 -18
- package/dist/cli.js +128 -23
- package/dist/client-D5U6ssPc.d.cts +84 -0
- package/dist/client-D5U6ssPc.d.ts +84 -0
- package/dist/index.cjs +810 -86
- package/dist/index.d.cts +19 -634
- package/dist/index.d.ts +19 -634
- package/dist/index.js +82 -699
- package/dist/manifest.cjs +200 -4
- package/dist/manifest.d.cts +260 -5
- package/dist/manifest.d.ts +260 -5
- package/dist/manifest.js +15 -1
- package/dist/mcp/client.cjs +180 -0
- package/dist/mcp/client.d.cts +49 -0
- package/dist/mcp/client.d.ts +49 -0
- package/dist/mcp/client.js +10 -0
- package/dist/mcp/server.cjs +370 -0
- package/dist/mcp/server.d.cts +127 -0
- package/dist/mcp/server.d.ts +127 -0
- package/dist/mcp/server.js +9 -0
- package/dist/mcp.cjs +376 -0
- package/dist/mcp.d.cts +2 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +17 -0
- package/dist/model.cjs +141 -0
- package/dist/model.d.cts +33 -0
- package/dist/model.d.ts +33 -0
- package/dist/model.js +18 -0
- package/dist/plugin.cjs +46 -0
- package/dist/plugin.d.cts +215 -0
- package/dist/plugin.d.ts +215 -0
- package/dist/plugin.js +8 -0
- package/dist/runnable.cjs +1438 -0
- package/dist/runnable.d.cts +274 -0
- package/dist/runnable.d.ts +274 -0
- package/dist/runnable.js +34 -0
- package/dist/{index-B6SkaAjJ.d.ts → tool-AjkdFvhE.d.ts} +88 -461
- package/dist/{index-BvAB5eMk.d.cts → tool-CgzW92O_.d.cts} +88 -461
- package/package.json +48 -3
- package/src/agent/loop.test.ts +4 -4
- package/src/agent/tools.ts +3 -3
- package/src/agent-plugin.test.ts +58 -0
- package/src/agent-plugin.ts +109 -36
- package/src/builder.ts +9 -0
- package/src/cli/dev.test.ts +26 -47
- package/src/cli/dev.ts +10 -2
- package/src/cli-security.test.ts +109 -0
- package/src/cli.ts +141 -25
- package/src/contracts-lockstep.test.ts +16 -2
- package/src/exports-lockstep.test.ts +93 -0
- package/src/generated/plugin-manifest.ts +322 -29
- package/src/index.ts +53 -0
- package/src/manifest-schema.test.ts +30 -1
- package/src/manifest.fixtures.test.ts +22 -6
- package/src/manifest.test.ts +165 -10
- package/src/manifest.ts +454 -188
- package/src/mcp/index.ts +18 -0
- package/src/mcp/server.ts +2 -1
- package/src/model/index.ts +22 -0
- package/src/plugin/ryu-plugin.ts +82 -0
- package/src/runnable/action.test.ts +128 -0
- package/src/runnable/action.ts +202 -0
- package/src/runnable/app.test.ts +2 -0
- package/src/runnable/app.ts +50 -14
- package/src/runnable/index.ts +20 -3
- package/src/runnable/primitives.test.ts +34 -0
- package/src/runnable/primitives.ts +116 -0
- package/src/runnable/runnable-types.ts +3 -0
- package/src/runnable/tool.ts +36 -5
- package/src/runnable/turn-hook.ts +8 -3
- package/src/slash-command.test.ts +69 -0
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* bunx ryu pack <dir>
|
|
7
|
-
* bunx ryu publish <
|
|
7
|
+
* bunx ryu github-publish <repository-or-package-url>
|
|
8
|
+
* bunx ryu publish <dir> (legacy migration escape hatch)
|
|
8
9
|
* bunx ryu agent-plugin <dir>
|
|
9
10
|
*
|
|
10
11
|
* Commands:
|
|
@@ -12,9 +13,10 @@
|
|
|
12
13
|
* Plugin bundle at <dir>/dist/plugin.bundle.json, plus the
|
|
13
14
|
* Agent Plugins interop pair in <dir> itself.
|
|
14
15
|
* Exits 0 on success; exits 1 with the failing field on error.
|
|
15
|
-
* publish <
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* github-publish <url>
|
|
17
|
+
* Submit a seller-owned GitHub repository/package URL to the
|
|
18
|
+
* GitHub-backed marketplace bridge.
|
|
19
|
+
* publish <dir> Deprecated server-artifact publishing; disabled by default.
|
|
18
20
|
* agent-plugin <dir>
|
|
19
21
|
* Emit only the Agent Plugins v1 interop pair (plugin.json and,
|
|
20
22
|
* when servers exist, mcp.json) derived from the manifest.json.
|
|
@@ -25,10 +27,12 @@ import {
|
|
|
25
27
|
existsSync,
|
|
26
28
|
mkdirSync,
|
|
27
29
|
readFileSync,
|
|
30
|
+
realpathSync,
|
|
28
31
|
rmSync,
|
|
29
32
|
writeFileSync,
|
|
33
|
+
writeSync,
|
|
30
34
|
} from "node:fs";
|
|
31
|
-
import { join, resolve } from "node:path";
|
|
35
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
32
36
|
import {
|
|
33
37
|
AGENT_PLUGIN_MANIFEST_FILE,
|
|
34
38
|
AGENT_PLUGIN_MCP_FILE,
|
|
@@ -56,7 +60,9 @@ function printUsage(): void {
|
|
|
56
60
|
"",
|
|
57
61
|
"Usage:",
|
|
58
62
|
" bunx ryu pack <dir> Validate and bundle a manifest.json Plugin",
|
|
59
|
-
" bunx ryu publish <
|
|
63
|
+
" bunx ryu github-publish <url>",
|
|
64
|
+
" Submit a GitHub repository/package URL to the Marketplace",
|
|
65
|
+
" bunx ryu publish <dir> Deprecated server-artifact publishing (migration only)",
|
|
60
66
|
" bunx ryu dev <entry> Run a Runnable locally with an interactive chat loop",
|
|
61
67
|
" bunx ryu agent-plugin <dir>",
|
|
62
68
|
" Emit the Agent Plugins v1 interop pair (plugin.json + mcp.json)",
|
|
@@ -66,7 +72,7 @@ function printUsage(): void {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
function exitError(message: string): never {
|
|
69
|
-
process.stderr.
|
|
75
|
+
writeSync(process.stderr.fd, `error: ${message}\n`);
|
|
70
76
|
process.exit(1);
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -91,17 +97,21 @@ const MANIFEST_FILE_NAMES = [
|
|
|
91
97
|
// `manifest.json` — and then fail validation for missing `id`. The `$schema`
|
|
92
98
|
// discriminator separates them (see `isAgentPluginManifest`).
|
|
93
99
|
function resolveNativeManifestPath(dir: string): string | undefined {
|
|
94
|
-
return MANIFEST_FILE_NAMES.map((name) => join(dir, name)).find(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
return MANIFEST_FILE_NAMES.map((name) => join(dir, name)).find(
|
|
101
|
+
(candidate) => {
|
|
102
|
+
if (!existsSync(candidate)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return !isAgentPluginManifest(
|
|
107
|
+
JSON.parse(readFileSync(candidate, "utf8"))
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
// Unparseable: let the caller surface the JSON error against this path.
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
103
113
|
}
|
|
104
|
-
|
|
114
|
+
);
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
// Read + parse + validate the manifest in `dir`. Exits with the failing
|
|
@@ -140,6 +150,28 @@ function loadManifest(dir: string): LoadedManifest {
|
|
|
140
150
|
const CODE_FILE_DIRS = ["hooks", "adapters"];
|
|
141
151
|
const CODE_FILE_PATH = /^(hooks|adapters)\/[A-Za-z0-9_][A-Za-z0-9._-]*\.m?js$/;
|
|
142
152
|
|
|
153
|
+
function containedPackageFile(dir: string, rel: string, label: string): string {
|
|
154
|
+
let realRoot: string;
|
|
155
|
+
let realTarget: string;
|
|
156
|
+
try {
|
|
157
|
+
realRoot = realpathSync(dir);
|
|
158
|
+
realTarget = realpathSync(join(dir, rel));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
exitError(`${label}: could not resolve '${rel}': ${String(error)}`);
|
|
161
|
+
}
|
|
162
|
+
const fromRoot = relative(realRoot, realTarget);
|
|
163
|
+
if (
|
|
164
|
+
fromRoot === ".." ||
|
|
165
|
+
fromRoot.startsWith(`..${sep}`) ||
|
|
166
|
+
isAbsolute(fromRoot)
|
|
167
|
+
) {
|
|
168
|
+
exitError(
|
|
169
|
+
`${label}: '${rel}' resolves outside the plugin package (${realTarget})`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return realTarget;
|
|
173
|
+
}
|
|
174
|
+
|
|
143
175
|
/**
|
|
144
176
|
* Read one `code_file` and return its contents, or exit with a clear error.
|
|
145
177
|
*
|
|
@@ -153,7 +185,7 @@ function readCodeFile(dir: string, rel: string, label: string): string {
|
|
|
153
185
|
`${label}: code_file '${rel}' must be exactly '<${CODE_FILE_DIRS.join("|")}>/<name>.js' with no traversal`
|
|
154
186
|
);
|
|
155
187
|
}
|
|
156
|
-
const path =
|
|
188
|
+
const path = containedPackageFile(dir, rel, label);
|
|
157
189
|
let body: string;
|
|
158
190
|
try {
|
|
159
191
|
body = readFileSync(path, "utf8");
|
|
@@ -267,7 +299,7 @@ function inlineOutputStyleFiles(
|
|
|
267
299
|
}
|
|
268
300
|
let body: string;
|
|
269
301
|
try {
|
|
270
|
-
body = readFileSync(
|
|
302
|
+
body = readFileSync(containedPackageFile(dir, rel, label), "utf8");
|
|
271
303
|
} catch (err) {
|
|
272
304
|
exitError(`${label}: could not read file '${rel}': ${String(err)}`);
|
|
273
305
|
}
|
|
@@ -316,12 +348,12 @@ function resolveUiEntry(manifest: LoadedManifest): string | null {
|
|
|
316
348
|
return null;
|
|
317
349
|
}
|
|
318
350
|
|
|
319
|
-
// Resolve a
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
351
|
+
// Resolve a UI entry's format. `"html"` means the `ui_entry` file is ALREADY a
|
|
352
|
+
// self-contained HTML document (a vite-plugin-singlefile build for a heavy
|
|
353
|
+
// companion, or a hand-authored widget) and must be shipped VERBATIM as
|
|
354
|
+
// `ui_code` — NOT run through `Bun.build`, which would try to bundle an HTML
|
|
355
|
+
// file as an ESM entry and fail. Anything else (absent / `"js"`) is the default:
|
|
356
|
+
// `ui_entry` is an ESM module `Bun.build` bundles into `ui_code`.
|
|
325
357
|
function resolveUiFormat(manifest: LoadedManifest): "html" | "js" {
|
|
326
358
|
for (const runnable of manifest.runnables) {
|
|
327
359
|
if (runnable.kind !== "companion") {
|
|
@@ -333,6 +365,15 @@ function resolveUiFormat(manifest: LoadedManifest): "html" | "js" {
|
|
|
333
365
|
return "html";
|
|
334
366
|
}
|
|
335
367
|
}
|
|
368
|
+
for (const widget of manifest.contributes?.widgets ?? []) {
|
|
369
|
+
const entry = widget.ui_entry;
|
|
370
|
+
if (
|
|
371
|
+
typeof entry === "string" &&
|
|
372
|
+
entry.trim().toLowerCase().endsWith(".html")
|
|
373
|
+
) {
|
|
374
|
+
return "html";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
336
377
|
return "js";
|
|
337
378
|
}
|
|
338
379
|
|
|
@@ -572,8 +613,16 @@ async function commandPublish(rawDir: string): Promise<void> {
|
|
|
572
613
|
...(manifest.description ? { description: manifest.description } : {}),
|
|
573
614
|
...(manifest.tagline ? { tagline: manifest.tagline } : {}),
|
|
574
615
|
...(developer ? { developer } : {}),
|
|
616
|
+
...(manifest.author ? { author: manifest.author } : {}),
|
|
617
|
+
...(manifest.repository ? { repository: manifest.repository } : {}),
|
|
618
|
+
...(manifest.external ? { external: true } : {}),
|
|
575
619
|
...(manifest.category ? { category: manifest.category } : {}),
|
|
620
|
+
...(manifest.license ? { license: manifest.license } : {}),
|
|
621
|
+
...(manifest.keywords?.length ? { keywords: manifest.keywords } : {}),
|
|
576
622
|
...(manifest.iconUrl ? { iconUrl: manifest.iconUrl } : {}),
|
|
623
|
+
...(manifest.icon ? { icon: manifest.icon } : {}),
|
|
624
|
+
...(manifest.iconDither ? { iconDither: manifest.iconDither } : {}),
|
|
625
|
+
...(manifest.banner ? { banner: manifest.banner } : {}),
|
|
577
626
|
...(manifest.screenshots?.length
|
|
578
627
|
? { screenshots: manifest.screenshots }
|
|
579
628
|
: {}),
|
|
@@ -648,6 +697,58 @@ async function commandPublish(rawDir: string): Promise<void> {
|
|
|
648
697
|
);
|
|
649
698
|
}
|
|
650
699
|
|
|
700
|
+
async function commandGithubPublish(repositoryUrl: string): Promise<void> {
|
|
701
|
+
const input = repositoryUrl.trim();
|
|
702
|
+
if (!input) {
|
|
703
|
+
exitError(
|
|
704
|
+
"github-publish requires a GitHub repository or package URL, for example https://github.com/acme/my-app"
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
const token = authToken();
|
|
708
|
+
const url = `${publishBaseUrl()}/api/marketplace/github/publish`;
|
|
709
|
+
const proof = process.env.RYU_GITHUB_INSTALLATION_PROOF?.trim();
|
|
710
|
+
const body = {
|
|
711
|
+
url: input,
|
|
712
|
+
...(proof ? { installationProof: proof } : {}),
|
|
713
|
+
};
|
|
714
|
+
let response: Response;
|
|
715
|
+
try {
|
|
716
|
+
response = await fetch(url, {
|
|
717
|
+
method: "POST",
|
|
718
|
+
headers: {
|
|
719
|
+
authorization: `Bearer ${token}`,
|
|
720
|
+
"content-type": "application/json",
|
|
721
|
+
},
|
|
722
|
+
body: JSON.stringify(body),
|
|
723
|
+
});
|
|
724
|
+
} catch (error) {
|
|
725
|
+
exitError(`could not reach ${url}: ${String(error)}`);
|
|
726
|
+
}
|
|
727
|
+
const text = await response.text();
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
try {
|
|
730
|
+
const error = JSON.parse(text) as {
|
|
731
|
+
code?: string;
|
|
732
|
+
installationUrl?: string;
|
|
733
|
+
error?: string;
|
|
734
|
+
};
|
|
735
|
+
if (error.installationUrl) {
|
|
736
|
+
exitError(
|
|
737
|
+
`${error.error ?? "GitHub App installation required"}. Open ${error.installationUrl}, then set RYU_GITHUB_INSTALLATION_PROOF to the callback proof and retry.`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
exitError(
|
|
741
|
+
`github-publish failed (${response.status}): ${error.error ?? text}`
|
|
742
|
+
);
|
|
743
|
+
} catch {
|
|
744
|
+
exitError(`github-publish failed (${response.status}): ${text}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
process.stdout.write(
|
|
748
|
+
`GitHub-backed listing submitted for moderation:\n${text}\n`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
651
752
|
// ── main ──────────────────────────────────────────────────────────────────────
|
|
652
753
|
|
|
653
754
|
const [, , command, ...args] = process.argv;
|
|
@@ -670,9 +771,24 @@ if (command === "pack") {
|
|
|
670
771
|
if (!dir) {
|
|
671
772
|
exitError("publish requires a directory argument: bunx ryu publish <dir>");
|
|
672
773
|
}
|
|
774
|
+
if (process.env.RYU_ENABLE_LEGACY_MARKETPLACE_WRITES !== "true") {
|
|
775
|
+
exitError(
|
|
776
|
+
"ryu publish is deprecated because marketplace package contents now live in GitHub Releases; use bunx ryu github-publish <repository-or-package-url>"
|
|
777
|
+
);
|
|
778
|
+
}
|
|
673
779
|
commandPublish(dir).catch((err: unknown) => {
|
|
674
780
|
exitError(String(err));
|
|
675
781
|
});
|
|
782
|
+
} else if (command === "github-publish") {
|
|
783
|
+
const repositoryUrl = args[0];
|
|
784
|
+
if (!repositoryUrl) {
|
|
785
|
+
exitError(
|
|
786
|
+
"github-publish requires a GitHub repository or package URL, for example https://github.com/acme/my-app"
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
commandGithubPublish(repositoryUrl).catch((err: unknown) => {
|
|
790
|
+
exitError(String(err));
|
|
791
|
+
});
|
|
676
792
|
} else if (command === "agent-plugin") {
|
|
677
793
|
const dir = args[0];
|
|
678
794
|
if (!dir) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Schema, and the generated types stay describing the same manifest:
|
|
10
10
|
*
|
|
11
11
|
* 1. a known-good repo manifest (apps-store/mail/manifest.json — the first
|
|
12
|
-
* fully manifest-driven app) parses
|
|
12
|
+
* fully manifest-driven app) parses without losing any top-level wire field;
|
|
13
13
|
* 2. every key the schema marks required exists in the schema, the generated
|
|
14
14
|
* TS, and the fixture;
|
|
15
15
|
* 3. every top-level key the fixture uses is a key the wire model knows.
|
|
@@ -42,12 +42,26 @@ const schema = JSON.parse(readFileSync(SCHEMA_PATH, "utf8")) as {
|
|
|
42
42
|
properties: Record<string, unknown>;
|
|
43
43
|
};
|
|
44
44
|
const generatedSource = readFileSync(GENERATED_PATH, "utf8");
|
|
45
|
+
const CORE_FIELDS_PREVIOUSLY_STRIPPED = [
|
|
46
|
+
"permission_levels",
|
|
47
|
+
"sidecars",
|
|
48
|
+
"stability",
|
|
49
|
+
"surfaces",
|
|
50
|
+
] as const;
|
|
45
51
|
|
|
46
52
|
describe("contracts lockstep (zod ↔ blessed JSON Schema ↔ generated TS)", () => {
|
|
47
|
-
test("the known-good mail manifest
|
|
53
|
+
test("the known-good mail manifest survives the zod pack path without field loss", () => {
|
|
48
54
|
const parsed = PluginManifestSchema.parse(fixture);
|
|
49
55
|
expect(parsed.id).toBe("@ryu/mail");
|
|
50
56
|
expect(parsed.runnables.length).toBeGreaterThan(0);
|
|
57
|
+
for (const key of Object.keys(fixture)) {
|
|
58
|
+
expect(Object.hasOwn(parsed, key), key).toBe(true);
|
|
59
|
+
}
|
|
60
|
+
for (const key of CORE_FIELDS_PREVIOUSLY_STRIPPED) {
|
|
61
|
+
expect(schema.properties).toHaveProperty(key);
|
|
62
|
+
expect(fixture).toHaveProperty(key);
|
|
63
|
+
expect(parsed[key]).toEqual(fixture[key]);
|
|
64
|
+
}
|
|
51
65
|
});
|
|
52
66
|
|
|
53
67
|
test("blessed schema describes PluginManifest with the required identity keys", () => {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package exports ↔ tsup entries lockstep guard.
|
|
3
|
+
*
|
|
4
|
+
* The SDK's public surface is split across `package.json` `exports` subpaths
|
|
5
|
+
* (`@ryuhq/sdk/model`, `@ryuhq/sdk/mcp/server`, …) and `tsup.config.ts`
|
|
6
|
+
* entrypoints that compile each one. They must stay a bijection: a subpath
|
|
7
|
+
* with no entry never builds, and an entry with no subpath is unreachable.
|
|
8
|
+
* This is the cheap structural guard that keeps the two from drifting again
|
|
9
|
+
* (a past drift left docs importing `@ryuhq/sdk/model` / `@ryuhq/sdk/mcp/server`
|
|
10
|
+
* that resolved to nothing).
|
|
11
|
+
*
|
|
12
|
+
* Deterministic, filesystem-only, no network.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from "bun:test";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import tsupConfig from "../tsup.config.ts";
|
|
19
|
+
|
|
20
|
+
const PKG_PATH = join(import.meta.dir, "../package.json");
|
|
21
|
+
const pkg = JSON.parse(readFileSync(PKG_PATH, "utf8")) as {
|
|
22
|
+
exports: Record<
|
|
23
|
+
string,
|
|
24
|
+
{ types?: string; import?: string; require?: string }
|
|
25
|
+
>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// tsup's `defineConfig` type is the `Options | Options[] | fn` union, which the
|
|
29
|
+
// spread doesn't narrow; the entry map is the one shape this package uses.
|
|
30
|
+
const entryKeys = Object.keys(
|
|
31
|
+
(tsupConfig as { entry?: Record<string, string> }).entry ?? {}
|
|
32
|
+
);
|
|
33
|
+
const exportKeys = Object.keys(pkg.exports);
|
|
34
|
+
|
|
35
|
+
describe("package exports ↔ tsup entries lockstep", () => {
|
|
36
|
+
test("every tsup entry has a matching exports subpath", () => {
|
|
37
|
+
// `.` in the exports map corresponds to the `index` entry.
|
|
38
|
+
const expected = entryKeys.map((key) =>
|
|
39
|
+
key === "index" ? "." : `./${key}`
|
|
40
|
+
);
|
|
41
|
+
for (const key of expected) {
|
|
42
|
+
expect(exportKeys).toContain(key);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("every exports subpath has a matching tsup entry", () => {
|
|
47
|
+
const subpaths = exportKeys
|
|
48
|
+
.filter((key) => key !== ".")
|
|
49
|
+
.map((key) => key.replace(/^\.\//, ""));
|
|
50
|
+
for (const key of subpaths) {
|
|
51
|
+
expect(entryKeys).toContain(key);
|
|
52
|
+
}
|
|
53
|
+
expect(entryKeys).toContain("index");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("every subpath the docs/examples import resolves in the exports map", () => {
|
|
57
|
+
// Each of these is referenced by real consumers (fumadocs cookbooks,
|
|
58
|
+
// examples/, the desktop host, create-ryu-app) and must never 404.
|
|
59
|
+
const documented = [
|
|
60
|
+
"action",
|
|
61
|
+
"manifest",
|
|
62
|
+
"agent",
|
|
63
|
+
"model",
|
|
64
|
+
"mcp",
|
|
65
|
+
"mcp/server",
|
|
66
|
+
"mcp/client",
|
|
67
|
+
"plugin",
|
|
68
|
+
"runnable",
|
|
69
|
+
"builder",
|
|
70
|
+
];
|
|
71
|
+
for (const sub of documented) {
|
|
72
|
+
expect(
|
|
73
|
+
pkg.exports[`./${sub}`],
|
|
74
|
+
`@ryuhq/sdk/${sub} must be exported`
|
|
75
|
+
).toBeTruthy();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("each subpath points at its own dist files (no collisions)", () => {
|
|
80
|
+
const distFiles = new Set<string>();
|
|
81
|
+
for (const entry of Object.values(pkg.exports)) {
|
|
82
|
+
for (const kind of ["types", "import", "require"] as const) {
|
|
83
|
+
const path = entry?.[kind];
|
|
84
|
+
if (path) {
|
|
85
|
+
expect(distFiles.has(path), `dist file collision: ${path}`).toBe(
|
|
86
|
+
false
|
|
87
|
+
);
|
|
88
|
+
distFiles.add(path);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|