@serverkgg/bridge 0.1.1 → 0.1.3
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 +3 -2
- package/rules/package.md +60 -0
- package/src/blog/articleFrontmatter.test.ts +10 -10
- package/src/blog/compile.test.ts +9 -9
- package/src/bot/botDeploy.test.ts +16 -16
- package/src/bot/botEnvironment.test.ts +8 -8
- package/src/bot/botIdentity.test.ts +12 -12
- package/src/bot/botPanel.test.ts +41 -11
- package/src/bot/botPanel.ts +13 -5
- package/src/bot/botState.test.ts +3 -3
- package/src/bot/botTerminal.test.ts +5 -5
- package/src/bot/botVariables.test.ts +8 -8
- package/src/bot/detect/botToken.test.ts +6 -6
- package/src/bot/detect/detect.test.ts +21 -21
- package/src/bot/detect/detectTable.test.ts +5 -5
- package/src/bot/detect/envExample.test.ts +2 -2
- package/src/bot/detect/envScan.test.ts +4 -4
- package/src/bot/detect/inferBotPlatform.test.ts +4 -4
- package/src/bot/detect/keys.test.ts +3 -3
- package/src/bot/detect/sensitiveKey.test.ts +5 -5
- package/src/bot/detect/serverkJson.test.ts +5 -5
- package/src/catalogs/curseforge.test.ts +35 -35
- package/src/catalogs/thunderstore.test.ts +9 -9
- package/src/cli/detectTable.test.ts +3 -3
- package/src/cli/link.test.ts +11 -11
- package/src/cli/main.ts +8 -0
- package/src/cli/rules.test.ts +198 -0
- package/src/cli/rules.ts +188 -0
- package/src/cli/validate.test.ts +123 -29
- package/src/cli/validate.ts +37 -1
- package/src/codecs/ini.test.ts +13 -13
- package/src/codecs/javaProperties.test.ts +11 -11
- package/src/codecs/sourceCfg.test.ts +14 -14
- package/src/codecs/xmlProperties.test.ts +10 -10
- package/src/context/bridgeContext.ts +6 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgePort.test.ts +6 -6
- package/src/core/bridgeUserError.test.ts +13 -3
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/bridgeValue.test.ts +5 -5
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/guides/markdown.test.ts +31 -31
- package/src/install/installStamp.test.ts +9 -9
- package/src/manifest/extension.ts +36 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +208 -90
- package/src/manifest/manifest.ts +34 -0
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeModule.ts +13 -1
- package/src/net/loopback.test.ts +10 -10
- package/src/panel/bridgeDetail.ts +2 -1
- package/src/presence/rosterSync.test.ts +7 -7
- package/src/probes/a2s.test.ts +5 -5
- package/src/probes/minecraftPing.test.ts +17 -17
- package/src/probes/raknetPing.test.ts +13 -13
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/rcon/rconAccess.test.ts +20 -20
- package/src/rcon/rconAccess.ts +13 -13
- package/src/rcon/rconClient.test.ts +49 -49
- package/src/rcon/rconFrame.test.ts +14 -14
- package/src/rcon/rconIds.test.ts +4 -4
- package/src/rcon/rconWriter.test.ts +6 -6
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeBackupLease.test.ts +9 -9
- package/src/runtime/bridgeConfigReader.test.ts +5 -5
- package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
- package/src/runtime/bridgeContext.test.ts +11 -5
- package/src/runtime/bridgeContext.ts +8 -1
- package/src/runtime/bridgeDriver.test.ts +114 -93
- package/src/runtime/bridgeDriver.ts +10 -7
- package/src/runtime/bridgeEvents.test.ts +15 -15
- package/src/runtime/bridgeFiles.test.ts +54 -54
- package/src/runtime/bridgeInstall.test.ts +10 -10
- package/src/runtime/bridgeLiveConfig.test.ts +6 -6
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgePending.test.ts +6 -6
- package/src/runtime/bridgeRedaction.test.ts +16 -16
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1077 -34
- package/src/runtime/bridgeRegistry.ts +464 -65
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +424 -24
- package/src/runtime/bridgeServer.ts +125 -35
- package/src/runtime/bridgeSetup.test.ts +27 -27
- package/src/runtime/bridgeSetup.ts +1 -1
- package/src/runtime/bridgeSetupHandlers.test.ts +80 -17
- package/src/runtime/bridgeSetupHandlers.ts +45 -10
- package/src/runtime/bridgeSignal.test.ts +7 -7
- package/src/runtime/bridgeSupervisor.test.ts +42 -42
- package/src/runtime/serve.ts +45 -3
- package/src/steam/avatars.test.ts +13 -13
- package/src/steam/steamApp.test.ts +8 -8
- package/src/steam/steamcmd.test.ts +44 -13
- package/src/steam/steamcmd.ts +27 -4
- package/src/steam/steamcmdNarrator.test.ts +4 -4
- package/src/steam/workshop.test.ts +5 -5
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/utils/token.test.ts +7 -7
- package/src/wine/protonCommand.test.ts +5 -5
- package/src/wine/wineCommand.test.ts +6 -6
- package/src/wine/winePrefix.test.ts +6 -6
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +91 -25
- package/src/wire/panel.ts +11 -0
package/src/cli/validate.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect,
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
2
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
type BridgeTab,
|
|
17
17
|
type BridgeTableSection,
|
|
18
18
|
} from "../panel";
|
|
19
|
-
import {
|
|
19
|
+
import { RULES_FILE, readShippedRules, writeRules } from "./rules";
|
|
20
|
+
import { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
|
|
20
21
|
|
|
21
22
|
const text = (value: string) => ({
|
|
22
23
|
ar: value,
|
|
@@ -54,7 +55,7 @@ describe("validate", () => {
|
|
|
54
55
|
});
|
|
55
56
|
});
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
it("refuses a file: bridge dependency", async () => {
|
|
58
59
|
writePackage({
|
|
59
60
|
"@serverkgg/bridge": "file:../serverk/modules/bridge",
|
|
60
61
|
});
|
|
@@ -64,7 +65,7 @@ describe("validate", () => {
|
|
|
64
65
|
);
|
|
65
66
|
});
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
it("refuses a link: bridge dependency", async () => {
|
|
68
69
|
writePackage({
|
|
69
70
|
"@serverkgg/bridge": "link:@serverkgg/bridge",
|
|
70
71
|
});
|
|
@@ -72,7 +73,7 @@ describe("validate", () => {
|
|
|
72
73
|
await expect(validate()).rejects.toThrow('depends on @serverkgg/bridge via "link:@serverkgg/bridge"');
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
it("refuses a file: bridge devDependency", async () => {
|
|
76
77
|
writeFileSync(
|
|
77
78
|
join(root, "package.json"),
|
|
78
79
|
JSON.stringify({
|
|
@@ -86,7 +87,7 @@ describe("validate", () => {
|
|
|
86
87
|
await expect(validate()).rejects.toThrow("pin a published version");
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
it("accepts a published bridge dependency and moves on to the manifest", async () => {
|
|
90
91
|
writePackage({
|
|
91
92
|
"@serverkgg/bridge": "0.0.3",
|
|
92
93
|
});
|
|
@@ -94,7 +95,7 @@ describe("validate", () => {
|
|
|
94
95
|
await expect(validate()).rejects.toThrow("missing serverk.yml");
|
|
95
96
|
});
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
it("ignores a package without a bridge dependency", async () => {
|
|
98
99
|
writePackage({
|
|
99
100
|
zod: "4.4.3",
|
|
100
101
|
});
|
|
@@ -102,19 +103,19 @@ describe("validate", () => {
|
|
|
102
103
|
await expect(validate()).rejects.toThrow("missing serverk.yml");
|
|
103
104
|
});
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
it("ignores a package.json that is absent", async () => {
|
|
106
107
|
await expect(validate()).rejects.toThrow("missing serverk.yml");
|
|
107
108
|
});
|
|
108
109
|
});
|
|
109
110
|
|
|
110
111
|
describe("the setup report", () => {
|
|
111
|
-
|
|
112
|
+
it("says none for a driver that declares no setup", () => {
|
|
112
113
|
expect(setupReportLines(undefined)).toEqual([
|
|
113
114
|
" setup: none",
|
|
114
115
|
]);
|
|
115
116
|
});
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
it("names every step, its kind, whether it is required, and where it lands", () => {
|
|
118
119
|
const setup: BridgeSetupModule = {
|
|
119
120
|
kind: BridgeKind.Setup,
|
|
120
121
|
steps: [
|
|
@@ -138,8 +139,8 @@ describe("the setup report", () => {
|
|
|
138
139
|
},
|
|
139
140
|
{
|
|
140
141
|
kind: BridgeSetupStepKind.Open,
|
|
141
|
-
id: "
|
|
142
|
-
title: text("
|
|
142
|
+
id: "dashboard",
|
|
143
|
+
title: text("Dashboard"),
|
|
143
144
|
required: false,
|
|
144
145
|
target: {
|
|
145
146
|
tab: GuideOpenTab.Panel,
|
|
@@ -165,12 +166,12 @@ describe("the setup report", () => {
|
|
|
165
166
|
" setup: 4 step(s)",
|
|
166
167
|
" license — form, required → license/license [LICENSE_KEY]",
|
|
167
168
|
" verify — driver, required → driver (needs the game running)",
|
|
168
|
-
'
|
|
169
|
+
' dashboard — open, optional → panel tab "license"',
|
|
169
170
|
" invite — open, optional → access",
|
|
170
171
|
]);
|
|
171
172
|
});
|
|
172
173
|
|
|
173
|
-
|
|
174
|
+
it("leaves a driver step that needs nothing running with no target", () => {
|
|
174
175
|
expect(
|
|
175
176
|
setupReportLines({
|
|
176
177
|
kind: BridgeKind.Setup,
|
|
@@ -192,7 +193,7 @@ describe("the setup report", () => {
|
|
|
192
193
|
]);
|
|
193
194
|
});
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
it("reads an omitted required as required, the way the platform does", () => {
|
|
196
197
|
expect(
|
|
197
198
|
setupReportLines({
|
|
198
199
|
kind: BridgeKind.Setup,
|
|
@@ -259,7 +260,7 @@ describe("the placement report", () => {
|
|
|
259
260
|
},
|
|
260
261
|
});
|
|
261
262
|
|
|
262
|
-
|
|
263
|
+
it("says nothing about a roster table that declares where it belongs", () => {
|
|
263
264
|
expect(
|
|
264
265
|
placementReportLines(
|
|
265
266
|
driverWithTabs([
|
|
@@ -277,7 +278,7 @@ describe("the placement report", () => {
|
|
|
277
278
|
]);
|
|
278
279
|
});
|
|
279
280
|
|
|
280
|
-
|
|
281
|
+
it("warns that the platform absorbs an unplaced players collection by convention", () => {
|
|
281
282
|
expect(
|
|
282
283
|
placementReportLines(
|
|
283
284
|
driverWithTabs([
|
|
@@ -295,7 +296,7 @@ describe("the placement report", () => {
|
|
|
295
296
|
]);
|
|
296
297
|
});
|
|
297
298
|
|
|
298
|
-
|
|
299
|
+
it("warns about a tab that takes the name of a platform sidebar entry", () => {
|
|
299
300
|
expect(
|
|
300
301
|
placementReportLines(
|
|
301
302
|
driverWithTabs([
|
|
@@ -313,7 +314,7 @@ describe("the placement report", () => {
|
|
|
313
314
|
]);
|
|
314
315
|
});
|
|
315
316
|
|
|
316
|
-
|
|
317
|
+
it("leaves a players tab alone while it is the roster tab", () => {
|
|
317
318
|
expect(
|
|
318
319
|
placementReportLines(
|
|
319
320
|
driverWithTabs([
|
|
@@ -331,7 +332,7 @@ describe("the placement report", () => {
|
|
|
331
332
|
]);
|
|
332
333
|
});
|
|
333
334
|
|
|
334
|
-
|
|
335
|
+
it("warns about a players tab that holds no roster", () => {
|
|
335
336
|
expect(
|
|
336
337
|
placementReportLines(
|
|
337
338
|
driverWithTabs([
|
|
@@ -349,7 +350,7 @@ describe("the placement report", () => {
|
|
|
349
350
|
]);
|
|
350
351
|
});
|
|
351
352
|
|
|
352
|
-
|
|
353
|
+
it("says nothing about a tab that declares no sections at all", () => {
|
|
353
354
|
expect(
|
|
354
355
|
placementReportLines(
|
|
355
356
|
driverWithTabs([
|
|
@@ -363,7 +364,7 @@ describe("the placement report", () => {
|
|
|
363
364
|
).toEqual([]);
|
|
364
365
|
});
|
|
365
366
|
|
|
366
|
-
|
|
367
|
+
it("still warns about an empty players tab, which holds no roster", () => {
|
|
367
368
|
expect(
|
|
368
369
|
placementReportLines(
|
|
369
370
|
driverWithTabs([
|
|
@@ -379,7 +380,7 @@ describe("the placement report", () => {
|
|
|
379
380
|
]);
|
|
380
381
|
});
|
|
381
382
|
|
|
382
|
-
|
|
383
|
+
it("says nothing about a tab that keeps a leftover section", () => {
|
|
383
384
|
expect(
|
|
384
385
|
placementReportLines(
|
|
385
386
|
driverWithTabs([
|
|
@@ -493,6 +494,7 @@ describe("validating a bot package", () => {
|
|
|
493
494
|
writeFileSync(join(root, "assets/logo.webp"), webp(256, 256));
|
|
494
495
|
writeFileSync(join(root, "assets/banner.webp"), webp(1280, 720));
|
|
495
496
|
writeFileSync(join(root, "src/driver.ts"), BOT_DRIVER);
|
|
497
|
+
writeRules(root, readShippedRules());
|
|
496
498
|
};
|
|
497
499
|
|
|
498
500
|
const validate = () => {
|
|
@@ -521,7 +523,7 @@ describe("validating a bot package", () => {
|
|
|
521
523
|
});
|
|
522
524
|
});
|
|
523
525
|
|
|
524
|
-
|
|
526
|
+
it("validates a bot with no ports and says which kind it read", async () => {
|
|
525
527
|
writeBot(botManifest());
|
|
526
528
|
|
|
527
529
|
await validate();
|
|
@@ -535,7 +537,27 @@ describe("validating a bot package", () => {
|
|
|
535
537
|
expect(lines.some((line) => line.includes("warning"))).toBe(false);
|
|
536
538
|
});
|
|
537
539
|
|
|
538
|
-
|
|
540
|
+
it("warns about a missing rule file and still validates", async () => {
|
|
541
|
+
writeBot(botManifest());
|
|
542
|
+
rmSync(join(root, RULES_FILE));
|
|
543
|
+
|
|
544
|
+
await validate();
|
|
545
|
+
|
|
546
|
+
expect(lines).toContain("bot-node is valid");
|
|
547
|
+
expect(lines).toContain(` warning: ${RULES_FILE} is missing — run "serverk-bridge rules"`);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("warns about a stale rule file and still validates", async () => {
|
|
551
|
+
writeBot(botManifest());
|
|
552
|
+
writeFileSync(join(root, RULES_FILE), "# edited by hand\n");
|
|
553
|
+
|
|
554
|
+
await validate();
|
|
555
|
+
|
|
556
|
+
expect(lines).toContain("bot-node is valid");
|
|
557
|
+
expect(lines).toContain(` warning: ${RULES_FILE} is out of date — run "serverk-bridge rules"`);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("says when a bot turns the environment vault on", async () => {
|
|
539
561
|
writeBot(
|
|
540
562
|
botManifest({
|
|
541
563
|
environment: {
|
|
@@ -549,7 +571,7 @@ describe("validating a bot package", () => {
|
|
|
549
571
|
expect(lines).toContain(" environment: vault on");
|
|
550
572
|
});
|
|
551
573
|
|
|
552
|
-
|
|
574
|
+
it("fails a bot whose detection table is missing", async () => {
|
|
553
575
|
writeBot(botManifest(), null);
|
|
554
576
|
|
|
555
577
|
await expect(validate()).rejects.toThrow(
|
|
@@ -557,7 +579,7 @@ describe("validating a bot package", () => {
|
|
|
557
579
|
);
|
|
558
580
|
});
|
|
559
581
|
|
|
560
|
-
|
|
582
|
+
it("fails a bot whose detection table is stale", async () => {
|
|
561
583
|
writeBot(botManifest(), "{}\n");
|
|
562
584
|
|
|
563
585
|
await expect(validate()).rejects.toThrow(
|
|
@@ -565,7 +587,7 @@ describe("validating a bot package", () => {
|
|
|
565
587
|
);
|
|
566
588
|
});
|
|
567
589
|
|
|
568
|
-
|
|
590
|
+
it("warns about a port on a bot but still validates it", async () => {
|
|
569
591
|
writeBot(
|
|
570
592
|
botManifest({
|
|
571
593
|
container: {
|
|
@@ -585,7 +607,7 @@ describe("validating a bot package", () => {
|
|
|
585
607
|
);
|
|
586
608
|
});
|
|
587
609
|
|
|
588
|
-
|
|
610
|
+
it("fails a bot that declares platform secrets", async () => {
|
|
589
611
|
writeBot(
|
|
590
612
|
botManifest({
|
|
591
613
|
secrets: [
|
|
@@ -602,3 +624,75 @@ describe("validating a bot package", () => {
|
|
|
602
624
|
);
|
|
603
625
|
});
|
|
604
626
|
});
|
|
627
|
+
|
|
628
|
+
describe("availability report", () => {
|
|
629
|
+
const driverWithModules = (modules: BridgeDriver["modules"]): BridgeDriver => ({
|
|
630
|
+
install: {
|
|
631
|
+
kind: BridgeKind.Install,
|
|
632
|
+
run: async () => {
|
|
633
|
+
return;
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
lifecycle: {
|
|
637
|
+
kind: BridgeKind.Lifecycle,
|
|
638
|
+
command: async () => [
|
|
639
|
+
"run",
|
|
640
|
+
],
|
|
641
|
+
},
|
|
642
|
+
modules,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("warns about a protected module that cannot say when its action is unavailable", () => {
|
|
646
|
+
expect(
|
|
647
|
+
availabilityReportLines(
|
|
648
|
+
driverWithModules({
|
|
649
|
+
worlds: {
|
|
650
|
+
kind: BridgeKind.Collection,
|
|
651
|
+
protectedActions: [
|
|
652
|
+
"delete",
|
|
653
|
+
],
|
|
654
|
+
list: async () => [],
|
|
655
|
+
actions: {
|
|
656
|
+
delete: async () => {
|
|
657
|
+
return;
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
}),
|
|
662
|
+
),
|
|
663
|
+
).toEqual([
|
|
664
|
+
' warning: module "worlds" protects delete but declares no `unavailable` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it',
|
|
665
|
+
]);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("says nothing when the protected module declares the hook or protects nothing", () => {
|
|
669
|
+
expect(
|
|
670
|
+
availabilityReportLines(
|
|
671
|
+
driverWithModules({
|
|
672
|
+
worlds: {
|
|
673
|
+
kind: BridgeKind.Collection,
|
|
674
|
+
protectedActions: [
|
|
675
|
+
"delete",
|
|
676
|
+
],
|
|
677
|
+
list: async () => [],
|
|
678
|
+
actions: {
|
|
679
|
+
delete: async () => {
|
|
680
|
+
return;
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
unavailable: async () => ({}),
|
|
684
|
+
},
|
|
685
|
+
players: {
|
|
686
|
+
kind: BridgeKind.Collection,
|
|
687
|
+
list: async () => [],
|
|
688
|
+
actions: {
|
|
689
|
+
kick: async () => {
|
|
690
|
+
return;
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
}),
|
|
695
|
+
),
|
|
696
|
+
).toEqual([]);
|
|
697
|
+
});
|
|
698
|
+
});
|
package/src/cli/validate.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { type ArticlePack, compileArticles } from "../blog";
|
|
7
7
|
import { DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
|
|
8
|
-
import { PortProtocol } from "../core";
|
|
8
|
+
import { BridgeKind, PortProtocol } from "../core";
|
|
9
9
|
import { GuideOpenTab, type GuideOpenTarget, type GuidePack, readGuidePack, readGuidePackage } from "../guides";
|
|
10
10
|
import {
|
|
11
11
|
declaredFiles,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../protocol";
|
|
32
32
|
import { validateDriver, validatePortActivation } from "../runtime/bridgeDriver";
|
|
33
33
|
import { directorySource } from "./directorySource";
|
|
34
|
+
import { rulesReportLines } from "./rules";
|
|
34
35
|
|
|
35
36
|
const HELP = `serverk-bridge validate [options]
|
|
36
37
|
|
|
@@ -279,6 +280,39 @@ const reportPlacement = (driver: BridgeDriver) => {
|
|
|
279
280
|
}
|
|
280
281
|
};
|
|
281
282
|
|
|
283
|
+
export const availabilityReportLines = (driver: BridgeDriver): string[] => {
|
|
284
|
+
const lines: string[] = [];
|
|
285
|
+
|
|
286
|
+
for (const [id, module] of Object.entries(driver.modules ?? {})) {
|
|
287
|
+
if (
|
|
288
|
+
module.kind !== BridgeKind.Actions
|
|
289
|
+
&& module.kind !== BridgeKind.Catalog
|
|
290
|
+
&& module.kind !== BridgeKind.Collection
|
|
291
|
+
&& module.kind !== BridgeKind.Settings
|
|
292
|
+
) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const protectedActions = module.protectedActions ?? [];
|
|
297
|
+
|
|
298
|
+
if (protectedActions.length === 0 || module.unavailable) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
lines.push(
|
|
303
|
+
` warning: module "${id}" protects ${protectedActions.join(", ")} but declares no \`unavailable\` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return lines;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const reportAvailability = (driver: BridgeDriver) => {
|
|
311
|
+
for (const line of availabilityReportLines(driver)) {
|
|
312
|
+
console.log(line);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
282
316
|
const openTargetOf = (target: GuideOpenTarget) => {
|
|
283
317
|
if (target.tab === GuideOpenTab.Panel) {
|
|
284
318
|
return `panel tab "${target.tabId}"`;
|
|
@@ -413,6 +447,7 @@ const validateRunnablePackage = async (
|
|
|
413
447
|
...botPortLines(manifest),
|
|
414
448
|
...botEnvironmentLines(manifest),
|
|
415
449
|
...detectLines,
|
|
450
|
+
...rulesReportLines(dir),
|
|
416
451
|
]) {
|
|
417
452
|
console.log(line);
|
|
418
453
|
}
|
|
@@ -432,6 +467,7 @@ const validateRunnablePackage = async (
|
|
|
432
467
|
reportEvents(driver);
|
|
433
468
|
reportActionTargets(driver);
|
|
434
469
|
reportPlacement(driver);
|
|
470
|
+
reportAvailability(driver);
|
|
435
471
|
};
|
|
436
472
|
|
|
437
473
|
export const runValidate = async (args: string[]) => {
|
package/src/codecs/ini.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { mergeIni, parseIni } from "./ini";
|
|
3
3
|
|
|
4
4
|
const SAMPLE = [
|
|
@@ -14,38 +14,38 @@ const SAMPLE = [
|
|
|
14
14
|
].join("\n");
|
|
15
15
|
|
|
16
16
|
describe("parseIni", () => {
|
|
17
|
-
|
|
17
|
+
it("reads the keys of one section", () => {
|
|
18
18
|
expect(parseIni(SAMPLE, "Server")).toEqual({
|
|
19
19
|
name: "serverk",
|
|
20
20
|
maxPlayers: "10",
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
it("reads another section without leaking keys", () => {
|
|
25
25
|
expect(parseIni(SAMPLE, "Admin")).toEqual({
|
|
26
26
|
password: "secret",
|
|
27
27
|
});
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
it("reads the keys before the first header when no section is asked for", () => {
|
|
31
31
|
expect(parseIni("welcome=hello\n[Server]\nname=x\n")).toEqual({
|
|
32
32
|
welcome: "hello",
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
it("keeps the last of a repeated key", () => {
|
|
37
37
|
expect(parseIni("[S]\nmod=a\nmod=b\n", "S")).toEqual({
|
|
38
38
|
mod: "b",
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
it("returns nothing for an unknown section", () => {
|
|
43
43
|
expect(parseIni(SAMPLE, "Nothing")).toEqual({});
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe("mergeIni", () => {
|
|
48
|
-
|
|
48
|
+
it("rewrites a value in place and keeps comments and order", () => {
|
|
49
49
|
expect(
|
|
50
50
|
mergeIni(
|
|
51
51
|
SAMPLE,
|
|
@@ -69,7 +69,7 @@ describe("mergeIni", () => {
|
|
|
69
69
|
);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
it("appends an unknown key at the end of its section", () => {
|
|
73
73
|
expect(
|
|
74
74
|
mergeIni(
|
|
75
75
|
SAMPLE,
|
|
@@ -93,7 +93,7 @@ describe("mergeIni", () => {
|
|
|
93
93
|
);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
it("creates a section that does not exist yet", () => {
|
|
97
97
|
expect(
|
|
98
98
|
mergeIni(
|
|
99
99
|
"[Server]\nname=x\n",
|
|
@@ -105,7 +105,7 @@ describe("mergeIni", () => {
|
|
|
105
105
|
).toBe("[Server]\nname=x\n\n[Game]\nlevel=3\n");
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
it("drops repeated occurrences of a key it rewrites", () => {
|
|
109
109
|
expect(
|
|
110
110
|
mergeIni(
|
|
111
111
|
"[S]\nmod=a\nmod=b\nother=1\n",
|
|
@@ -117,7 +117,7 @@ describe("mergeIni", () => {
|
|
|
117
117
|
).toBe("[S]\nmod=c\nother=1\n");
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
it("writes into an empty file", () => {
|
|
121
121
|
expect(
|
|
122
122
|
mergeIni(
|
|
123
123
|
"",
|
|
@@ -129,7 +129,7 @@ describe("mergeIni", () => {
|
|
|
129
129
|
).toBe("[Server]\nname=x\n");
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
it("writes preamble keys when no section is asked for", () => {
|
|
133
133
|
expect(
|
|
134
134
|
mergeIni("welcome=hello\n[Server]\nname=x\n", {
|
|
135
135
|
welcome: "salam",
|
|
@@ -138,7 +138,7 @@ describe("mergeIni", () => {
|
|
|
138
138
|
).toBe("welcome=salam\nextra=1\n[Server]\nname=x\n");
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
it("leaves other sections untouched", () => {
|
|
142
142
|
expect(
|
|
143
143
|
mergeIni(
|
|
144
144
|
"[A]\nkey=1\n[B]\nkey=2\n",
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { escapeJavaProperty, mergeJavaProperties, parseJavaProperties, unescapeJavaProperty } from "./javaProperties";
|
|
3
3
|
|
|
4
4
|
const ARABIC = "سيرفرك";
|
|
5
5
|
|
|
6
6
|
describe("parseJavaProperties", () => {
|
|
7
|
-
|
|
7
|
+
it("decodes the unicode escapes the java server writes", () => {
|
|
8
8
|
expect(parseJavaProperties("motd=\\u0633\\u064A\\u0631\\u0641\\u0631\\u0643\n")).toEqual({
|
|
9
9
|
motd: ARABIC,
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
it("decodes backslash escapes in values", () => {
|
|
14
14
|
expect(parseJavaProperties("resource-pack=https\\://example.com/pack.zip\nmotd=a\\tb\\\\c\n")).toEqual({
|
|
15
15
|
"resource-pack": "https://example.com/pack.zip",
|
|
16
16
|
motd: "a\tb\\c",
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
it("repairs a value the server stored as latin-1 bytes of utf-8", () => {
|
|
21
21
|
expect(parseJavaProperties("motd=ddss \\u00D9\\u008A\\u00D8\\u00AA\n")).toEqual({
|
|
22
22
|
motd: "ddss يت",
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
it("keeps plain latin-1 text that is not utf-8", () => {
|
|
27
27
|
expect(unescapeJavaProperty("caf\\u00E9")).toBe("café");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
it("keeps comments and plain values", () => {
|
|
31
31
|
expect(parseJavaProperties("#Minecraft server properties\nmax-players=20\n")).toEqual({
|
|
32
32
|
"max-players": "20",
|
|
33
33
|
});
|
|
@@ -35,7 +35,7 @@ describe("parseJavaProperties", () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
describe("mergeJavaProperties", () => {
|
|
38
|
-
|
|
38
|
+
it("writes non ascii text as unicode escapes", () => {
|
|
39
39
|
expect(
|
|
40
40
|
mergeJavaProperties("motd=A Minecraft Server\nmax-players=20\n", {
|
|
41
41
|
motd: ARABIC,
|
|
@@ -43,21 +43,21 @@ describe("mergeJavaProperties", () => {
|
|
|
43
43
|
).toBe("motd=\\u0633\\u064A\\u0631\\u0641\\u0631\\u0643\nmax-players=20\n");
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
it("escapes the characters java treats as separators", () => {
|
|
47
47
|
expect(escapeJavaProperty("https://a.b#c=d!e")).toBe("https\\://a.b\\#c\\=d\\!e");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
it("escapes a leading space and control characters", () => {
|
|
51
51
|
expect(escapeJavaProperty(" a\tb\\c")).toBe("\\ a\\tb\\\\c");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
it("round trips astral characters", () => {
|
|
55
55
|
const value = "hi 🎮";
|
|
56
56
|
|
|
57
57
|
expect(unescapeJavaProperty(escapeJavaProperty(value))).toBe(value);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
it("appends a key the file does not have yet", () => {
|
|
61
61
|
expect(
|
|
62
62
|
mergeJavaProperties("", {
|
|
63
63
|
motd: "hello",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
decodeSourceCfgValue,
|
|
4
4
|
encodeSourceCfgValue,
|
|
@@ -8,21 +8,21 @@ import {
|
|
|
8
8
|
} from "./sourceCfg";
|
|
9
9
|
|
|
10
10
|
describe("readSourceCfgEntry", () => {
|
|
11
|
-
|
|
11
|
+
it("splits a key from its value", () => {
|
|
12
12
|
expect(readSourceCfgEntry('hostname "serverk"')).toEqual({
|
|
13
13
|
key: "hostname",
|
|
14
14
|
value: '"serverk"',
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
it("accepts a key with no value", () => {
|
|
19
19
|
expect(readSourceCfgEntry("sv_lan")).toEqual({
|
|
20
20
|
key: "sv_lan",
|
|
21
21
|
value: "",
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
it("ignores comments and blank lines", () => {
|
|
26
26
|
expect(readSourceCfgEntry("// a comment")).toBeNull();
|
|
27
27
|
expect(readSourceCfgEntry("# a comment")).toBeNull();
|
|
28
28
|
expect(readSourceCfgEntry(" ")).toBeNull();
|
|
@@ -31,32 +31,32 @@ describe("readSourceCfgEntry", () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
describe("decodeSourceCfgValue", () => {
|
|
34
|
-
|
|
34
|
+
it("unquotes a string and unescapes it", () => {
|
|
35
35
|
expect(decodeSourceCfgValue('"a \\"b\\" c"')).toBe('a "b" c');
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
it("reads booleans and numbers", () => {
|
|
39
39
|
expect(decodeSourceCfgValue("true")).toBe(true);
|
|
40
40
|
expect(decodeSourceCfgValue("false")).toBe(false);
|
|
41
41
|
expect(decodeSourceCfgValue("42")).toBe(42);
|
|
42
42
|
expect(decodeSourceCfgValue("-1.5")).toBe(-1.5);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
it("leaves a bare word alone", () => {
|
|
46
46
|
expect(decodeSourceCfgValue("de_dust2")).toBe("de_dust2");
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
describe("encodeSourceCfgValue", () => {
|
|
51
|
-
|
|
51
|
+
it("quotes and escapes text", () => {
|
|
52
52
|
expect(encodeSourceCfgValue('a "b"')).toBe('"a \\"b\\""');
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
it("flattens newlines", () => {
|
|
56
56
|
expect(encodeSourceCfgValue("a\nb")).toBe('"a b"');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
it("writes booleans and numbers bare", () => {
|
|
60
60
|
expect(encodeSourceCfgValue(true)).toBe("true");
|
|
61
61
|
expect(encodeSourceCfgValue(7)).toBe("7");
|
|
62
62
|
expect(encodeSourceCfgValue(null)).toBe('""');
|
|
@@ -64,7 +64,7 @@ describe("encodeSourceCfgValue", () => {
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
describe("parseSourceCfg", () => {
|
|
67
|
-
|
|
67
|
+
it("reads a source cfg file", () => {
|
|
68
68
|
expect(parseSourceCfg('// header\nhostname "serverk"\nsv_maxplayers 24\nsv_cheats false\n')).toEqual({
|
|
69
69
|
hostname: "serverk",
|
|
70
70
|
sv_maxplayers: 24,
|
|
@@ -74,7 +74,7 @@ describe("parseSourceCfg", () => {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
describe("mergeSourceCfg", () => {
|
|
77
|
-
|
|
77
|
+
it("rewrites a value and keeps the comments", () => {
|
|
78
78
|
expect(
|
|
79
79
|
mergeSourceCfg('// header\nhostname "old"\nsv_maxplayers 24\n', {
|
|
80
80
|
hostname: "new",
|
|
@@ -82,7 +82,7 @@ describe("mergeSourceCfg", () => {
|
|
|
82
82
|
).toBe('// header\nhostname "new"\nsv_maxplayers 24\n');
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
it("appends an unknown key", () => {
|
|
86
86
|
expect(
|
|
87
87
|
mergeSourceCfg('hostname "x"\n', {
|
|
88
88
|
rcon_password: "abc",
|
|
@@ -90,7 +90,7 @@ describe("mergeSourceCfg", () => {
|
|
|
90
90
|
).toBe('hostname "x"\nrcon_password "abc"\n');
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
it("collapses a repeated key it rewrites", () => {
|
|
94
94
|
expect(
|
|
95
95
|
mergeSourceCfg("map de_dust2\nmap de_nuke\n", {
|
|
96
96
|
map: "de_inferno",
|