@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.
- package/package.json +1 -1
- package/rules/package.md +2 -1
- package/src/bot/botDeploy.test.ts +57 -0
- package/src/bot/botEnvironment.test.ts +21 -0
- package/src/bot/botEnvironment.ts +10 -0
- package/src/bot/botRun.test.ts +296 -0
- package/src/bot/botRun.ts +109 -0
- package/src/bot/botState.test.ts +34 -1
- package/src/bot/botState.ts +1 -0
- package/src/bot/botVariables.test.ts +22 -0
- package/src/bot/botVariables.ts +3 -0
- package/src/bot/detect/detect.test.ts +172 -0
- package/src/bot/detect/detect.ts +31 -0
- package/src/bot/detect/detectResult.ts +1 -0
- package/src/bot/detect/detectTable.test.ts +25 -2
- package/src/bot/detect/detectTable.ts +24 -1
- package/src/bot/detect/keys.test.ts +7 -0
- package/src/bot/detect/keys.ts +3 -0
- package/src/bot/detect/serverkJson.test.ts +21 -0
- package/src/bot/detect/serverkJson.ts +13 -1
- package/src/bot/index.ts +1 -0
- package/src/cli/link.ts +3 -3
- package/src/cli/main.ts +8 -0
- package/src/cli/readme.test.ts +470 -0
- package/src/cli/readme.ts +570 -0
- package/src/cli/validate.test.ts +41 -1
- package/src/cli/validate.ts +25 -0
- package/src/context/bridgeContext.ts +1 -0
- package/src/modules/bridgeModule.ts +1 -0
- package/src/protocol/bridgeManifest.ts +1 -0
- package/src/runtime/bridgeContext.test.ts +6 -0
- package/src/runtime/bridgeContext.ts +7 -2
- package/src/runtime/bridgeExec.test.ts +150 -0
- package/src/runtime/bridgeExec.ts +67 -19
- package/src/runtime/bridgeLog.ts +9 -0
- package/src/runtime/bridgeRedaction.test.ts +28 -2
- package/src/runtime/bridgeRedaction.ts +7 -1
- package/src/runtime/bridgeRegistry.test.ts +30 -0
- package/src/runtime/bridgeRegistry.ts +1 -0
- package/src/runtime/bridgeScopedContext.ts +3 -0
- package/src/runtime/bridgeSetupHandlers.test.ts +3 -0
- package/src/runtime/bridgeSupervisor.ts +2 -6
- package/src/runtime/serve.ts +9 -2
- package/src/wire/panel.test.ts +32 -0
- 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
|
+
``,
|
|
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
|
+
};
|
package/src/cli/validate.test.ts
CHANGED
|
@@ -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));
|
package/src/cli/validate.ts
CHANGED
|
@@ -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);
|