@serverkgg/bridge 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/rules/package.md +2 -1
  3. package/src/bot/botDeploy.test.ts +57 -0
  4. package/src/bot/botEnvironment.test.ts +21 -0
  5. package/src/bot/botEnvironment.ts +10 -0
  6. package/src/bot/botRun.test.ts +296 -0
  7. package/src/bot/botRun.ts +109 -0
  8. package/src/bot/botState.test.ts +34 -1
  9. package/src/bot/botState.ts +1 -0
  10. package/src/bot/botVariables.test.ts +22 -0
  11. package/src/bot/botVariables.ts +3 -0
  12. package/src/bot/detect/detect.test.ts +172 -0
  13. package/src/bot/detect/detect.ts +31 -0
  14. package/src/bot/detect/detectResult.ts +1 -0
  15. package/src/bot/detect/detectTable.test.ts +25 -2
  16. package/src/bot/detect/detectTable.ts +24 -1
  17. package/src/bot/detect/keys.test.ts +7 -0
  18. package/src/bot/detect/keys.ts +3 -0
  19. package/src/bot/detect/serverkJson.test.ts +21 -0
  20. package/src/bot/detect/serverkJson.ts +13 -1
  21. package/src/bot/index.ts +1 -0
  22. package/src/cli/link.ts +3 -3
  23. package/src/cli/main.ts +8 -0
  24. package/src/cli/readme.test.ts +470 -0
  25. package/src/cli/readme.ts +570 -0
  26. package/src/cli/validate.test.ts +41 -1
  27. package/src/cli/validate.ts +25 -0
  28. package/src/context/bridgeContext.ts +1 -0
  29. package/src/modules/bridgeModule.ts +1 -0
  30. package/src/protocol/bridgeManifest.ts +1 -0
  31. package/src/runtime/bridgeContext.test.ts +6 -0
  32. package/src/runtime/bridgeContext.ts +7 -2
  33. package/src/runtime/bridgeExec.test.ts +150 -0
  34. package/src/runtime/bridgeExec.ts +67 -19
  35. package/src/runtime/bridgeLog.ts +9 -0
  36. package/src/runtime/bridgeRedaction.test.ts +28 -2
  37. package/src/runtime/bridgeRedaction.ts +7 -1
  38. package/src/runtime/bridgeRegistry.test.ts +30 -0
  39. package/src/runtime/bridgeRegistry.ts +1 -0
  40. package/src/runtime/bridgeScopedContext.ts +3 -0
  41. package/src/runtime/bridgeSetupHandlers.test.ts +3 -0
  42. package/src/runtime/bridgeSupervisor.ts +2 -6
  43. package/src/runtime/serve.ts +9 -2
  44. package/src/wire/panel.test.ts +32 -0
  45. package/src/wire/panel.ts +1 -0
@@ -0,0 +1,570 @@
1
+ import { join, resolve } from "node:path";
2
+ import { parseArgs } from "node:util";
3
+ import {
4
+ type GuideEntry,
5
+ type GuideTag,
6
+ type LocalizedText,
7
+ MANIFEST_FILE,
8
+ type Package,
9
+ PackageKind,
10
+ type PackageSource,
11
+ readPackageDocument,
12
+ } from "../manifest";
13
+ import { directorySource } from "./directorySource";
14
+ import { LINK_SOURCE } from "./link";
15
+ import { writeRenderedFile } from "./renderedFile";
16
+
17
+ export const README_FILE = "README.md";
18
+
19
+ export const README_COMMAND = "serverk-bridge readme";
20
+
21
+ const SITE_URL = "https://serverk.gg";
22
+
23
+ const PACKAGE_FILE = "package.json";
24
+
25
+ const HOOK_FILE = ".githooks/pre-commit";
26
+
27
+ const WORKFLOWS_FOLDER = ".github/workflows/";
28
+
29
+ const SENTENCE_END = /[.!?؟…]$/;
30
+
31
+ const HOOK_COMMAND = /^(bun run|bunx) /;
32
+
33
+ const GUIDE_PACKAGE_NAME: LocalizedText = {
34
+ ar: "شروحات المنصة",
35
+ en: "Platform guides",
36
+ };
37
+
38
+ const HELP = `serverk-bridge readme [options]
39
+
40
+ write ${README_FILE} from ${MANIFEST_FILE}, package.json and the pre-commit hook, Arabic first, then English.
41
+ the file is committed, and "serverk-bridge validate" fails while it is missing or out of date.
42
+
43
+ options:
44
+ --dir <path> package directory (default: current directory)
45
+ --check change nothing, fail when the file is missing or out of date
46
+ --help show this help`;
47
+
48
+ interface LayoutEntry {
49
+ path: string;
50
+ label: string;
51
+ kinds: readonly PackageKind[];
52
+ text: string;
53
+ }
54
+
55
+ const RUNNABLE_KINDS = [
56
+ PackageKind.Game,
57
+ PackageKind.Bot,
58
+ ];
59
+
60
+ const ALL_KINDS = [
61
+ PackageKind.Game,
62
+ PackageKind.Bot,
63
+ PackageKind.Guide,
64
+ ];
65
+
66
+ const LAUNCHER_TEXT =
67
+ "what the bridge starts; it puts the deployed code in place, installs its dependencies and starts the bot.";
68
+
69
+ const LAYOUT: readonly LayoutEntry[] = [
70
+ {
71
+ path: MANIFEST_FILE,
72
+ label: `\`${MANIFEST_FILE}\``,
73
+ kinds: [
74
+ PackageKind.Game,
75
+ ],
76
+ text: "the game manifest with its metadata, resources, ports, backup and reset rules, and the guide list.",
77
+ },
78
+ {
79
+ path: MANIFEST_FILE,
80
+ label: `\`${MANIFEST_FILE}\``,
81
+ kinds: [
82
+ PackageKind.Bot,
83
+ ],
84
+ text: "the bot manifest with its metadata, resources, environment vault, backup rules and the guide list.",
85
+ },
86
+ {
87
+ path: MANIFEST_FILE,
88
+ label: `\`${MANIFEST_FILE}\``,
89
+ kinds: [
90
+ PackageKind.Guide,
91
+ ],
92
+ text: "the guide package manifest with each guide's id, title, summary, tags and body paths.",
93
+ },
94
+ {
95
+ path: "serverk.schema.json",
96
+ label: "`serverk.schema.json`",
97
+ kinds: ALL_KINDS,
98
+ text: `the JSON Schema of \`${MANIFEST_FILE}\` for the editor, written by \`serverk-bridge schema\`.`,
99
+ },
100
+ {
101
+ path: "src/",
102
+ label: "`src/`",
103
+ kinds: RUNNABLE_KINDS,
104
+ text: "the driver, built on `@serverkgg/bridge`; `src/driver.ts` exports it and `src/main.ts` serves it.",
105
+ },
106
+ {
107
+ path: "image/Dockerfile",
108
+ label: "`image/Dockerfile`",
109
+ kinds: RUNNABLE_KINDS,
110
+ text: "the container image, with the compiled bridge as its entrypoint.",
111
+ },
112
+ {
113
+ path: "image/launcher.mjs",
114
+ label: "`image/launcher.mjs`",
115
+ kinds: [
116
+ PackageKind.Bot,
117
+ ],
118
+ text: LAUNCHER_TEXT,
119
+ },
120
+ {
121
+ path: "image/launcher.py",
122
+ label: "`image/launcher.py`",
123
+ kinds: [
124
+ PackageKind.Bot,
125
+ ],
126
+ text: LAUNCHER_TEXT,
127
+ },
128
+ {
129
+ path: "image/detect.json",
130
+ label: "`image/detect.json`",
131
+ kinds: [
132
+ PackageKind.Bot,
133
+ ],
134
+ text: "the start and install table the launcher reads, written by `serverk-bridge detect-table`.",
135
+ },
136
+ {
137
+ path: "templates/",
138
+ label: "`templates/`",
139
+ kinds: [
140
+ PackageKind.Bot,
141
+ ],
142
+ text: "starter bots, synced to the public `serverkgg/bot-template-*` repositories by `bunx outdo templates`.",
143
+ },
144
+ {
145
+ path: "assets/",
146
+ label: "`assets/`",
147
+ kinds: RUNNABLE_KINDS,
148
+ text: "the logo and the banner the site and the panel show.",
149
+ },
150
+ {
151
+ path: "guides/",
152
+ label: "`guides/<id>/ar.md` and `en.md`",
153
+ kinds: ALL_KINDS,
154
+ text: `the guide bodies; each guide's title, summary and tags live in \`${MANIFEST_FILE}\`.`,
155
+ },
156
+ {
157
+ path: "blog/",
158
+ label: "`blog/<id>/ar.md` and `en.md`",
159
+ kinds: [
160
+ PackageKind.Guide,
161
+ ],
162
+ text: `the blog articles at \`serverk.gg/blog/<id>\`, not listed in \`${MANIFEST_FILE}\`. Each file opens with YAML frontmatter holding \`title\` and \`summary\` per language; \`publishedAt\`, \`updatedAt\`, \`tags\` and \`video\` are read from \`ar.md\`.`,
163
+ },
164
+ ];
165
+
166
+ const SCRIPT_TEXTS: readonly (readonly [
167
+ string,
168
+ string,
169
+ ])[] = [
170
+ [
171
+ "tsc",
172
+ "type-checks the code.",
173
+ ],
174
+ [
175
+ "test",
176
+ "runs the tests.",
177
+ ],
178
+ [
179
+ "validate",
180
+ "runs the checks the platform runs at publish, and fails while this README is out of date.",
181
+ ],
182
+ [
183
+ "compile",
184
+ "builds `dist/bridge`, the linux/amd64 binary the image starts.",
185
+ ],
186
+ ];
187
+
188
+ const textOf = (bytes: Uint8Array | null) => {
189
+ return bytes === null ? null : new TextDecoder().decode(bytes);
190
+ };
191
+
192
+ const scriptsOf = (source: PackageSource): Record<string, string> => {
193
+ const text = textOf(source.read(PACKAGE_FILE));
194
+
195
+ if (text === null) {
196
+ return {};
197
+ }
198
+
199
+ let parsed: unknown;
200
+
201
+ try {
202
+ parsed = JSON.parse(text);
203
+ } catch {
204
+ return {};
205
+ }
206
+
207
+ if (typeof parsed !== "object" || parsed === null || !("scripts" in parsed)) {
208
+ return {};
209
+ }
210
+
211
+ const scripts: unknown = parsed.scripts;
212
+
213
+ if (typeof scripts !== "object" || scripts === null) {
214
+ return {};
215
+ }
216
+
217
+ return Object.fromEntries(
218
+ Object.entries(scripts).filter(
219
+ (
220
+ entry,
221
+ ): entry is [
222
+ string,
223
+ string,
224
+ ] => typeof entry[1] === "string",
225
+ ),
226
+ );
227
+ };
228
+
229
+ const hookCommandsOf = (source: PackageSource) => {
230
+ const text = textOf(source.read(HOOK_FILE));
231
+
232
+ if (text === null) {
233
+ return null;
234
+ }
235
+
236
+ return text
237
+ .split("\n")
238
+ .map((line) => line.trim())
239
+ .filter((line) => HOOK_COMMAND.test(line));
240
+ };
241
+
242
+ const sentenceOf = (text: string) => {
243
+ const trimmed = text.trim();
244
+
245
+ return SENTENCE_END.test(trimmed) ? trimmed : `${trimmed}.`;
246
+ };
247
+
248
+ const englishListOf = (items: readonly string[]) => {
249
+ if (items.length < 2) {
250
+ return items.join("");
251
+ }
252
+
253
+ return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
254
+ };
255
+
256
+ const arabicListOf = (items: readonly string[]) => {
257
+ return items.join("، و");
258
+ };
259
+
260
+ const codeOf = (text: string) => {
261
+ return `\`${text}\``;
262
+ };
263
+
264
+ const linkOf = (path: string) => {
265
+ return `[serverk.gg${path}](${SITE_URL}${path})`;
266
+ };
267
+
268
+ const linkTextOf = (text: string) => {
269
+ return text.replaceAll("[", "\\[").replaceAll("]", "\\]");
270
+ };
271
+
272
+ const nameOf = (document: Package) => {
273
+ return document.kind === PackageKind.Guide ? GUIDE_PACKAGE_NAME : document.metadata.name;
274
+ };
275
+
276
+ const pagePathOf = (document: Package) => {
277
+ switch (document.kind) {
278
+ case PackageKind.Game: {
279
+ return `/games/${document.metadata.id}`;
280
+ }
281
+
282
+ case PackageKind.Bot: {
283
+ return "/bots";
284
+ }
285
+
286
+ case PackageKind.Guide: {
287
+ return "/guides";
288
+ }
289
+ }
290
+ };
291
+
292
+ const guidePathOf = (document: Package, guide: GuideEntry) => {
293
+ switch (document.kind) {
294
+ case PackageKind.Game: {
295
+ return `/games/${document.metadata.id}/guides/${guide.id}`;
296
+ }
297
+
298
+ case PackageKind.Bot: {
299
+ return `/bots/${document.metadata.id}/guides/${guide.id}`;
300
+ }
301
+
302
+ case PackageKind.Guide: {
303
+ return `/guides/${guide.id}`;
304
+ }
305
+ }
306
+ };
307
+
308
+ const releaseCommandOf = (document: Package) => {
309
+ return document.kind === PackageKind.Guide
310
+ ? "bunx outdo platform --prod"
311
+ : `bunx outdo package --prod --name ${document.metadata.id}`;
312
+ };
313
+
314
+ const tagSentencesOf = (tags: readonly GuideTag[]): LocalizedText | null => {
315
+ if (tags.length === 0) {
316
+ return null;
317
+ }
318
+
319
+ return {
320
+ ar: ` وتتوزع على هذه الأقسام: ${arabicListOf(tags.map((tag) => `«${tag.name.ar}»`))}.`,
321
+ en: ` They are grouped under ${englishListOf(tags.map((tag) => `"${tag.name.en}"`))}.`,
322
+ };
323
+ };
324
+
325
+ const introOf = (document: Package): LocalizedText => {
326
+ const page = linkOf(pagePathOf(document));
327
+
328
+ switch (document.kind) {
329
+ case PackageKind.Game: {
330
+ const { name, description, tagline } = document.metadata;
331
+
332
+ return {
333
+ ar: `يضم هذا الريبو كل ما تحتاجه منصة سيرفرك لتشغيل سيرفرات ${name.ar}. ${sentenceOf(description.ar)} ${sentenceOf(tagline.ar)} صفحة اللعبة: ${page}`,
334
+ en: `This repository holds everything Serverk needs to run ${name.en} servers. ${sentenceOf(description.en)} ${sentenceOf(tagline.en)} Game page: ${page}`,
335
+ };
336
+ }
337
+
338
+ case PackageKind.Bot: {
339
+ const { name, description, tagline } = document.metadata;
340
+
341
+ return {
342
+ ar: `يضم هذا الريبو بيئة تشغيل ${name.ar}، وهي كل ما تحتاجه منصة سيرفرك لتشغيل بوتك. ${sentenceOf(description.ar)} ${sentenceOf(tagline.ar)} صفحة البوتات: ${page}`,
343
+ en: `This repository holds the ${name.en} runtime: everything Serverk needs to run your bot. ${sentenceOf(description.en)} ${sentenceOf(tagline.en)} Bots page: ${page}`,
344
+ };
345
+ }
346
+
347
+ case PackageKind.Guide: {
348
+ const tags = tagSentencesOf(document.guideTags);
349
+
350
+ return {
351
+ ar: `يضم هذا الريبو الشروحات العامة لمنصة سيرفرك، وهي كل شرح لا يرتبط بلعبة معيّنة.${tags?.ar ?? ""} صفحة الشروحات: ${page}`,
352
+ en: `This repository holds the general guides for Serverk: every guide that is not tied to one game.${tags?.en ?? ""} Guides page: ${page}`,
353
+ };
354
+ }
355
+ }
356
+ };
357
+
358
+ const headerOf = (document: Package) => {
359
+ const name = nameOf(document);
360
+ const intro = introOf(document);
361
+ const title = `# ${name.ar} (${name.en})`;
362
+
363
+ if (document.kind === PackageKind.Guide) {
364
+ return [
365
+ title,
366
+ intro.ar,
367
+ intro.en,
368
+ ];
369
+ }
370
+
371
+ return [
372
+ `![${linkTextOf(name.en)}](${document.metadata.banner})`,
373
+ title,
374
+ intro.ar,
375
+ intro.en,
376
+ ];
377
+ };
378
+
379
+ const layoutOf = (document: Package, paths: readonly string[]) => {
380
+ const present = (path: string) => {
381
+ return path.endsWith("/") ? paths.some((entry) => entry.startsWith(path)) : paths.includes(path);
382
+ };
383
+
384
+ const lines = LAYOUT.filter((entry) => entry.kinds.includes(document.kind) && present(entry.path)).map(
385
+ (entry) => `- ${entry.label}: ${entry.text}`,
386
+ );
387
+
388
+ return [
389
+ "## Layout",
390
+ lines.join("\n"),
391
+ ];
392
+ };
393
+
394
+ const guidesOf = (document: Package) => {
395
+ if (document.guides.length === 0) {
396
+ return [];
397
+ }
398
+
399
+ const lines = document.guides.map(
400
+ (guide) =>
401
+ `- [${linkTextOf(guide.title.ar)} · ${linkTextOf(guide.title.en)}](${SITE_URL}${guidePathOf(document, guide)})`,
402
+ );
403
+
404
+ return [
405
+ "## Guides",
406
+ lines.join("\n"),
407
+ ];
408
+ };
409
+
410
+ const installLineOf = (scripts: Record<string, string>) => {
411
+ const parts = [
412
+ "installs the dependencies",
413
+ ];
414
+
415
+ if (scripts.prepare?.includes("core.hooksPath")) {
416
+ parts.push("points Git at the hooks in `.githooks/`");
417
+ }
418
+
419
+ if (scripts.postinstall?.includes("serverk-bridge rules")) {
420
+ parts.push("writes `.claude/rules/package.md` from the bridge");
421
+ }
422
+
423
+ return `- \`bun install\`: ${englishListOf(parts)}.`;
424
+ };
425
+
426
+ const scriptLinesOf = (scripts: Record<string, string>) => {
427
+ const lines: string[] = [];
428
+
429
+ if (scripts.check !== undefined) {
430
+ const fix = scripts.fix === undefined ? "" : "; `bun run fix` applies the fixes";
431
+
432
+ lines.push(`- \`bun run check\`: checks lint and formatting with Biome${fix}.`);
433
+ }
434
+
435
+ for (const [script, text] of SCRIPT_TEXTS) {
436
+ if (scripts[script] !== undefined) {
437
+ lines.push(`- \`bun run ${script}\`: ${text}`);
438
+ }
439
+ }
440
+
441
+ return lines;
442
+ };
443
+
444
+ const hookParagraphOf = (source: PackageSource, paths: readonly string[]) => {
445
+ const commands = hookCommandsOf(source);
446
+
447
+ if (commands === null || commands.length === 0) {
448
+ return [];
449
+ }
450
+
451
+ const ci = paths.some((path) => path.startsWith(WORKFLOWS_FOLDER))
452
+ ? ""
453
+ : " There is no CI: a commit that passes the hook is ready.";
454
+
455
+ return [
456
+ `${codeOf(HOOK_FILE)} runs ${englishListOf(commands.map(codeOf))} before every commit.${ci}`,
457
+ ];
458
+ };
459
+
460
+ const linkParagraphOf = (scripts: Record<string, string>) => {
461
+ if (!scripts.postinstall?.includes("serverk-bridge link")) {
462
+ return [];
463
+ }
464
+
465
+ return [
466
+ `\`bun install\` runs \`serverk-bridge link\`, which links \`@serverkgg/bridge\` to a sibling \`serverk\` checkout at \`${LINK_SOURCE}\` when one exists. Never commit a \`file:\` or \`link:\` dependency.`,
467
+ ];
468
+ };
469
+
470
+ const developOf = (source: PackageSource, paths: readonly string[]) => {
471
+ const scripts = scriptsOf(source);
472
+
473
+ return [
474
+ "## Develop",
475
+ "Everything runs on [Bun](https://bun.sh).",
476
+ [
477
+ installLineOf(scripts),
478
+ ...scriptLinesOf(scripts),
479
+ ].join("\n"),
480
+ ...hookParagraphOf(source, paths),
481
+ ...linkParagraphOf(scripts),
482
+ ];
483
+ };
484
+
485
+ const contributeOf = (document: Package) => {
486
+ const release = codeOf(releaseCommandOf(document));
487
+ const ar = [
488
+ "افتح issue لأي مشكلة أو اقتراح، بالعربية أو بالإنجليزية.",
489
+ `ينشر فريق سيرفرك هذا الريبو من ريبو \`serverk\` بالأمر ${release}، ويصل كل تغيير مدموج مع النشر التالي.`,
490
+ "العربية هي لغة المصدر: يُكتب كل نص يقرؤه العميل بالعربية أولًا، بلغة رسمية مع لمسة خليجية خفيفة، ثم بالإنجليزية.",
491
+ "أبقِ أسماء الملفات والأوامر والمفاتيح بالحروف اللاتينية.",
492
+ ];
493
+ const en = [
494
+ "Open an issue for any problem or suggestion, in Arabic or English.",
495
+ `Serverk releases this repository from \`serverk\` with ${release}; merged changes ride the next release.`,
496
+ "Arabic is the source language: every string a customer reads is written in Arabic first, formal with a light Gulf touch, then in English.",
497
+ "Keep file names, commands and config keys in Latin script.",
498
+ ];
499
+
500
+ return [
501
+ "## Contribute",
502
+ ar.join(" "),
503
+ en.join(" "),
504
+ ];
505
+ };
506
+
507
+ const licenseOf = () => {
508
+ return [
509
+ "## License",
510
+ "الكود متاح للقراءة، وجميع الحقوق محفوظة لمنصة سيرفرك، ولا يُمنح أي ترخيص.",
511
+ "The code is public to read. All rights are reserved by Serverk, and no license is granted.",
512
+ ];
513
+ };
514
+
515
+ const footerOf = () => {
516
+ return [
517
+ `يُنشأ هذا الملف بالأمر \`${README_COMMAND}\` من \`${MANIFEST_FILE}\`، فعدّل ملف التعريف لا هذا الملف.`,
518
+ `Generated by \`${README_COMMAND}\` from \`${MANIFEST_FILE}\`; edit the manifest, not this file.`,
519
+ ];
520
+ };
521
+
522
+ export const renderReadme = (source: PackageSource, document: Package) => {
523
+ const paths = source.list();
524
+ const blocks = [
525
+ ...headerOf(document),
526
+ ...layoutOf(document, paths),
527
+ ...guidesOf(document),
528
+ ...developOf(source, paths),
529
+ ...contributeOf(document),
530
+ ...licenseOf(),
531
+ ...footerOf(),
532
+ ];
533
+
534
+ return `${blocks.join("\n\n")}\n`;
535
+ };
536
+
537
+ export const runReadme = async (args: string[]) => {
538
+ const { values } = parseArgs({
539
+ args,
540
+ options: {
541
+ dir: {
542
+ type: "string",
543
+ default: ".",
544
+ },
545
+ check: {
546
+ type: "boolean",
547
+ default: false,
548
+ },
549
+ help: {
550
+ type: "boolean",
551
+ default: false,
552
+ },
553
+ },
554
+ });
555
+
556
+ if (values.help) {
557
+ console.log(HELP);
558
+
559
+ return;
560
+ }
561
+
562
+ const source = directorySource(resolve(values.dir));
563
+
564
+ writeRenderedFile({
565
+ out: join(values.dir, README_FILE),
566
+ check: values.check,
567
+ rendered: renderReadme(source, readPackageDocument(source)),
568
+ command: README_COMMAND,
569
+ });
570
+ };
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { BOT_SWITCH_ON, BOT_WEB, BOT_WEB_PORT_KEY, renderDetectTable } from "../bot/detect";
6
6
  import { BridgeKind, BridgeSetupStepKind } from "../core";
7
7
  import { GuideOpenTab, type GuidePack, type ParsedBlock, type ParsedLeafBlock } from "../guides";
8
- import { MANIFEST_SCHEMA_VERSION, PackageKind, type RunnableManifest } from "../manifest";
8
+ import { MANIFEST_SCHEMA_VERSION, PackageKind, packageSchema, type RunnableManifest } from "../manifest";
9
9
  import type { BridgeDriver, BridgePendingModule, BridgeSetupModule } from "../modules";
10
10
  import {
11
11
  type BridgeCatalogSection,
@@ -19,6 +19,8 @@ import {
19
19
  type BridgeTab,
20
20
  type BridgeTableSection,
21
21
  } from "../panel";
22
+ import { directorySource } from "./directorySource";
23
+ import { README_FILE, renderReadme } from "./readme";
22
24
  import { RULES_FILE, readShippedRules, writeRules } from "./rules";
23
25
  import {
24
26
  availabilityReportLines,
@@ -508,6 +510,12 @@ describe("validating a bot package", () => {
508
510
  writeFileSync(join(root, "assets/banner.webp"), webp(1280, 720));
509
511
  writeFileSync(join(root, "src/driver.ts"), BOT_DRIVER);
510
512
  writeRules(root, readShippedRules());
513
+
514
+ const parsed = packageSchema.safeParse(manifest);
515
+
516
+ if (parsed.success) {
517
+ writeFileSync(join(root, README_FILE), renderReadme(directorySource(root), parsed.data));
518
+ }
511
519
  };
512
520
 
513
521
  const validate = () => {
@@ -547,9 +555,41 @@ describe("validating a bot package", () => {
547
555
  );
548
556
  expect(lines).toContain(" environment: vault off");
549
557
  expect(lines).toContain(" detect: image/detect.json matches this bridge");
558
+ expect(lines).toContain(" readme: README.md matches serverk.yml");
550
559
  expect(lines.some((line) => line.includes("warning"))).toBe(false);
551
560
  });
552
561
 
562
+ it("fails a bot whose README is missing", async () => {
563
+ writeBot(botManifest());
564
+ rmSync(join(root, README_FILE));
565
+
566
+ await expect(validate()).rejects.toThrow('README.md is missing — run "serverk-bridge readme" and commit it');
567
+ });
568
+
569
+ it("fails a bot whose README is out of date", async () => {
570
+ writeBot(botManifest());
571
+ writeFileSync(join(root, README_FILE), "# edited by hand\n");
572
+
573
+ await expect(validate()).rejects.toThrow('README.md is out of date — run "serverk-bridge readme" and commit it');
574
+ });
575
+
576
+ it("fails a bot whose README no longer matches its manifest", async () => {
577
+ writeBot(botManifest());
578
+ writeFileSync(
579
+ join(root, "serverk.yml"),
580
+ JSON.stringify(
581
+ botManifest({
582
+ metadata: {
583
+ ...botManifest().metadata,
584
+ name: text("Renamed bot"),
585
+ },
586
+ }),
587
+ ),
588
+ );
589
+
590
+ await expect(validate()).rejects.toThrow('README.md is out of date — run "serverk-bridge readme" and commit it');
591
+ });
592
+
553
593
  it("warns about a missing rule file and still validates", async () => {
554
594
  writeBot(botManifest());
555
595
  rmSync(join(root, RULES_FILE));
@@ -40,6 +40,7 @@ import {
40
40
  } from "../protocol";
41
41
  import { validateDriver, validatePortActivation } from "../runtime/bridgeDriver";
42
42
  import { directorySource } from "./directorySource";
43
+ import { README_COMMAND, README_FILE, renderReadme } from "./readme";
43
44
  import { rulesReportLines } from "./rules";
44
45
 
45
46
  const HELP = `serverk-bridge validate [options]
@@ -173,8 +174,14 @@ const validateGuidePackage = (source: PackageSource, document: GuidePackage) =>
173
174
 
174
175
  const pack = readGuidePackage(source, document);
175
176
  const articles = compileArticles(source);
177
+ const readme = readmeLines(source, document);
176
178
 
177
179
  console.log(`${document.metadata.id} is valid`);
180
+
181
+ for (const line of readme) {
182
+ console.log(line);
183
+ }
184
+
178
185
  reportGuides(pack);
179
186
  reportArticles(articles);
180
187
  };
@@ -644,6 +651,22 @@ export const botDetectLines = (source: PackageSource, manifest: RunnableManifest
644
651
  ];
645
652
  };
646
653
 
654
+ export const readmeLines = (source: PackageSource, document: Package): string[] => {
655
+ const bytes = source.read(README_FILE);
656
+
657
+ if (bytes === null) {
658
+ throw new Error(`${README_FILE} is missing — run "${README_COMMAND}" and commit it`);
659
+ }
660
+
661
+ if (new TextDecoder().decode(bytes) !== renderReadme(source, document)) {
662
+ throw new Error(`${README_FILE} is out of date — run "${README_COMMAND}" and commit it`);
663
+ }
664
+
665
+ return [
666
+ ` readme: ${README_FILE} matches ${MANIFEST_FILE}`,
667
+ ];
668
+ };
669
+
647
670
  const validateRunnablePackage = async (
648
671
  source: PackageSource,
649
672
  manifest: RunnableManifest,
@@ -670,6 +693,7 @@ const validateRunnablePackage = async (
670
693
  }
671
694
 
672
695
  const detectLines = botDetectLines(source, manifest);
696
+ const readme = readmeLines(source, manifest);
673
697
  const driverPath = driverArg ? resolve(driverArg) : join(dir, "src/driver.ts");
674
698
 
675
699
  if (!existsSync(driverPath)) {
@@ -721,6 +745,7 @@ const validateRunnablePackage = async (
721
745
  ...botPortLines(manifest),
722
746
  ...botEnvironmentLines(manifest),
723
747
  ...detectLines,
748
+ ...readme,
724
749
  ...rulesReportLines(dir),
725
750
  ]) {
726
751
  console.log(line);
@@ -220,4 +220,5 @@ export interface BridgeContext {
220
220
  emit(event: string, payload?: BridgeValues): void;
221
221
  restart(): Promise<void>;
222
222
  log: BridgeLog;
223
+ print(line: string, stream?: BridgeStream): void;
223
224
  }
@@ -108,6 +108,7 @@ export interface BridgeTerminalModule {
108
108
  kind: BridgeKind.Terminal;
109
109
  commands: BridgeTerminalCommand[];
110
110
  rules?: BridgeTerminalRule[];
111
+ shell?: boolean;
111
112
  run?(context: BridgeContext, input: string): Promise<BridgeReply | null>;
112
113
  }
113
114