@serverkgg/bridge 0.1.2 → 0.1.4
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 +65 -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 +13 -13
- package/src/bot/botPanel.ts +4 -4
- 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 +179 -31
- package/src/cli/validate.ts +44 -0
- 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/core/bridgePort.test.ts +6 -6
- package/src/core/bridgeUserError.test.ts +4 -4
- package/src/core/bridgeValue.test.ts +5 -5
- package/src/core/describeDownloadFailure.test.ts +12 -12
- package/src/core/describeDownloadFailure.ts +10 -10
- 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/net/loopback.test.ts +10 -10
- 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/bridgePreview.ts +1 -0
- package/src/rcon/rconAccess.test.ts +20 -20
- package/src/rcon/rconAccess.ts +14 -14
- 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/bridgeActivity.test.ts +15 -15
- 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 +5 -5
- package/src/runtime/bridgeDriver.test.ts +94 -94
- 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/bridgePending.test.ts +6 -6
- package/src/runtime/bridgeRedaction.test.ts +16 -16
- package/src/runtime/bridgeRegistry.test.ts +84 -52
- package/src/runtime/bridgeRegistry.ts +2 -2
- package/src/runtime/bridgeServer.test.ts +31 -31
- package/src/runtime/bridgeSetup.test.ts +27 -27
- package/src/runtime/bridgeSetup.ts +1 -1
- package/src/runtime/bridgeSetupHandlers.test.ts +13 -13
- package/src/runtime/bridgeSignal.test.ts +7 -7
- package/src/runtime/bridgeSupervisor.test.ts +42 -42
- package/src/runtime/serve.ts +4 -4
- package/src/steam/avatars.test.ts +13 -13
- package/src/steam/steamApp.test.ts +8 -8
- package/src/steam/steamcmd.test.ts +17 -17
- package/src/steam/steamcmd.ts +4 -4
- package/src/steam/steamcmdNarrator.test.ts +4 -4
- package/src/steam/workshop.test.ts +5 -5
- package/src/utils/changedSettingKeys.test.ts +5 -5
- package/src/utils/describeExecFailure.test.ts +4 -4
- package/src/utils/describeExecFailure.ts +1 -1
- package/src/utils/sameSettingValue.test.ts +7 -7
- 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 +8 -8
- package/src/wire/panel.test.ts +28 -28
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";
|
|
@@ -8,7 +8,10 @@ import { GuideOpenTab } from "../guides";
|
|
|
8
8
|
import { MANIFEST_SCHEMA_VERSION, PackageKind } from "../manifest";
|
|
9
9
|
import type { BridgeDriver, BridgeSetupModule } from "../modules";
|
|
10
10
|
import {
|
|
11
|
+
type BridgeCatalogSection,
|
|
12
|
+
BridgeConfirm,
|
|
11
13
|
BridgeControl,
|
|
14
|
+
type BridgeDetailSection,
|
|
12
15
|
type BridgeFormSection,
|
|
13
16
|
BridgeFormTarget,
|
|
14
17
|
BridgeLayout,
|
|
@@ -16,7 +19,14 @@ import {
|
|
|
16
19
|
type BridgeTab,
|
|
17
20
|
type BridgeTableSection,
|
|
18
21
|
} from "../panel";
|
|
19
|
-
import {
|
|
22
|
+
import { RULES_FILE, readShippedRules, writeRules } from "./rules";
|
|
23
|
+
import {
|
|
24
|
+
availabilityReportLines,
|
|
25
|
+
confirmReportLines,
|
|
26
|
+
placementReportLines,
|
|
27
|
+
runValidate,
|
|
28
|
+
setupReportLines,
|
|
29
|
+
} from "./validate";
|
|
20
30
|
|
|
21
31
|
const text = (value: string) => ({
|
|
22
32
|
ar: value,
|
|
@@ -54,7 +64,7 @@ describe("validate", () => {
|
|
|
54
64
|
});
|
|
55
65
|
});
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
it("refuses a file: bridge dependency", async () => {
|
|
58
68
|
writePackage({
|
|
59
69
|
"@serverkgg/bridge": "file:../serverk/modules/bridge",
|
|
60
70
|
});
|
|
@@ -64,7 +74,7 @@ describe("validate", () => {
|
|
|
64
74
|
);
|
|
65
75
|
});
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
it("refuses a link: bridge dependency", async () => {
|
|
68
78
|
writePackage({
|
|
69
79
|
"@serverkgg/bridge": "link:@serverkgg/bridge",
|
|
70
80
|
});
|
|
@@ -72,7 +82,7 @@ describe("validate", () => {
|
|
|
72
82
|
await expect(validate()).rejects.toThrow('depends on @serverkgg/bridge via "link:@serverkgg/bridge"');
|
|
73
83
|
});
|
|
74
84
|
|
|
75
|
-
|
|
85
|
+
it("refuses a file: bridge devDependency", async () => {
|
|
76
86
|
writeFileSync(
|
|
77
87
|
join(root, "package.json"),
|
|
78
88
|
JSON.stringify({
|
|
@@ -86,7 +96,7 @@ describe("validate", () => {
|
|
|
86
96
|
await expect(validate()).rejects.toThrow("pin a published version");
|
|
87
97
|
});
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
it("accepts a published bridge dependency and moves on to the manifest", async () => {
|
|
90
100
|
writePackage({
|
|
91
101
|
"@serverkgg/bridge": "0.0.3",
|
|
92
102
|
});
|
|
@@ -94,7 +104,7 @@ describe("validate", () => {
|
|
|
94
104
|
await expect(validate()).rejects.toThrow("missing serverk.yml");
|
|
95
105
|
});
|
|
96
106
|
|
|
97
|
-
|
|
107
|
+
it("ignores a package without a bridge dependency", async () => {
|
|
98
108
|
writePackage({
|
|
99
109
|
zod: "4.4.3",
|
|
100
110
|
});
|
|
@@ -102,19 +112,19 @@ describe("validate", () => {
|
|
|
102
112
|
await expect(validate()).rejects.toThrow("missing serverk.yml");
|
|
103
113
|
});
|
|
104
114
|
|
|
105
|
-
|
|
115
|
+
it("ignores a package.json that is absent", async () => {
|
|
106
116
|
await expect(validate()).rejects.toThrow("missing serverk.yml");
|
|
107
117
|
});
|
|
108
118
|
});
|
|
109
119
|
|
|
110
120
|
describe("the setup report", () => {
|
|
111
|
-
|
|
121
|
+
it("says none for a driver that declares no setup", () => {
|
|
112
122
|
expect(setupReportLines(undefined)).toEqual([
|
|
113
123
|
" setup: none",
|
|
114
124
|
]);
|
|
115
125
|
});
|
|
116
126
|
|
|
117
|
-
|
|
127
|
+
it("names every step, its kind, whether it is required, and where it lands", () => {
|
|
118
128
|
const setup: BridgeSetupModule = {
|
|
119
129
|
kind: BridgeKind.Setup,
|
|
120
130
|
steps: [
|
|
@@ -138,8 +148,8 @@ describe("the setup report", () => {
|
|
|
138
148
|
},
|
|
139
149
|
{
|
|
140
150
|
kind: BridgeSetupStepKind.Open,
|
|
141
|
-
id: "
|
|
142
|
-
title: text("
|
|
151
|
+
id: "dashboard",
|
|
152
|
+
title: text("Dashboard"),
|
|
143
153
|
required: false,
|
|
144
154
|
target: {
|
|
145
155
|
tab: GuideOpenTab.Panel,
|
|
@@ -165,12 +175,12 @@ describe("the setup report", () => {
|
|
|
165
175
|
" setup: 4 step(s)",
|
|
166
176
|
" license — form, required → license/license [LICENSE_KEY]",
|
|
167
177
|
" verify — driver, required → driver (needs the game running)",
|
|
168
|
-
'
|
|
178
|
+
' dashboard — open, optional → panel tab "license"',
|
|
169
179
|
" invite — open, optional → access",
|
|
170
180
|
]);
|
|
171
181
|
});
|
|
172
182
|
|
|
173
|
-
|
|
183
|
+
it("leaves a driver step that needs nothing running with no target", () => {
|
|
174
184
|
expect(
|
|
175
185
|
setupReportLines({
|
|
176
186
|
kind: BridgeKind.Setup,
|
|
@@ -192,7 +202,7 @@ describe("the setup report", () => {
|
|
|
192
202
|
]);
|
|
193
203
|
});
|
|
194
204
|
|
|
195
|
-
|
|
205
|
+
it("reads an omitted required as required, the way the platform does", () => {
|
|
196
206
|
expect(
|
|
197
207
|
setupReportLines({
|
|
198
208
|
kind: BridgeKind.Setup,
|
|
@@ -259,7 +269,7 @@ describe("the placement report", () => {
|
|
|
259
269
|
},
|
|
260
270
|
});
|
|
261
271
|
|
|
262
|
-
|
|
272
|
+
it("says nothing about a roster table that declares where it belongs", () => {
|
|
263
273
|
expect(
|
|
264
274
|
placementReportLines(
|
|
265
275
|
driverWithTabs([
|
|
@@ -277,7 +287,7 @@ describe("the placement report", () => {
|
|
|
277
287
|
]);
|
|
278
288
|
});
|
|
279
289
|
|
|
280
|
-
|
|
290
|
+
it("warns that the platform absorbs an unplaced players collection by convention", () => {
|
|
281
291
|
expect(
|
|
282
292
|
placementReportLines(
|
|
283
293
|
driverWithTabs([
|
|
@@ -295,7 +305,7 @@ describe("the placement report", () => {
|
|
|
295
305
|
]);
|
|
296
306
|
});
|
|
297
307
|
|
|
298
|
-
|
|
308
|
+
it("warns about a tab that takes the name of a platform sidebar entry", () => {
|
|
299
309
|
expect(
|
|
300
310
|
placementReportLines(
|
|
301
311
|
driverWithTabs([
|
|
@@ -313,7 +323,7 @@ describe("the placement report", () => {
|
|
|
313
323
|
]);
|
|
314
324
|
});
|
|
315
325
|
|
|
316
|
-
|
|
326
|
+
it("leaves a players tab alone while it is the roster tab", () => {
|
|
317
327
|
expect(
|
|
318
328
|
placementReportLines(
|
|
319
329
|
driverWithTabs([
|
|
@@ -331,7 +341,7 @@ describe("the placement report", () => {
|
|
|
331
341
|
]);
|
|
332
342
|
});
|
|
333
343
|
|
|
334
|
-
|
|
344
|
+
it("warns about a players tab that holds no roster", () => {
|
|
335
345
|
expect(
|
|
336
346
|
placementReportLines(
|
|
337
347
|
driverWithTabs([
|
|
@@ -349,7 +359,7 @@ describe("the placement report", () => {
|
|
|
349
359
|
]);
|
|
350
360
|
});
|
|
351
361
|
|
|
352
|
-
|
|
362
|
+
it("says nothing about a tab that declares no sections at all", () => {
|
|
353
363
|
expect(
|
|
354
364
|
placementReportLines(
|
|
355
365
|
driverWithTabs([
|
|
@@ -363,7 +373,7 @@ describe("the placement report", () => {
|
|
|
363
373
|
).toEqual([]);
|
|
364
374
|
});
|
|
365
375
|
|
|
366
|
-
|
|
376
|
+
it("still warns about an empty players tab, which holds no roster", () => {
|
|
367
377
|
expect(
|
|
368
378
|
placementReportLines(
|
|
369
379
|
driverWithTabs([
|
|
@@ -379,7 +389,7 @@ describe("the placement report", () => {
|
|
|
379
389
|
]);
|
|
380
390
|
});
|
|
381
391
|
|
|
382
|
-
|
|
392
|
+
it("says nothing about a tab that keeps a leftover section", () => {
|
|
383
393
|
expect(
|
|
384
394
|
placementReportLines(
|
|
385
395
|
driverWithTabs([
|
|
@@ -493,6 +503,7 @@ describe("validating a bot package", () => {
|
|
|
493
503
|
writeFileSync(join(root, "assets/logo.webp"), webp(256, 256));
|
|
494
504
|
writeFileSync(join(root, "assets/banner.webp"), webp(1280, 720));
|
|
495
505
|
writeFileSync(join(root, "src/driver.ts"), BOT_DRIVER);
|
|
506
|
+
writeRules(root, readShippedRules());
|
|
496
507
|
};
|
|
497
508
|
|
|
498
509
|
const validate = () => {
|
|
@@ -521,7 +532,7 @@ describe("validating a bot package", () => {
|
|
|
521
532
|
});
|
|
522
533
|
});
|
|
523
534
|
|
|
524
|
-
|
|
535
|
+
it("validates a bot with no ports and says which kind it read", async () => {
|
|
525
536
|
writeBot(botManifest());
|
|
526
537
|
|
|
527
538
|
await validate();
|
|
@@ -535,7 +546,27 @@ describe("validating a bot package", () => {
|
|
|
535
546
|
expect(lines.some((line) => line.includes("warning"))).toBe(false);
|
|
536
547
|
});
|
|
537
548
|
|
|
538
|
-
|
|
549
|
+
it("warns about a missing rule file and still validates", async () => {
|
|
550
|
+
writeBot(botManifest());
|
|
551
|
+
rmSync(join(root, RULES_FILE));
|
|
552
|
+
|
|
553
|
+
await validate();
|
|
554
|
+
|
|
555
|
+
expect(lines).toContain("bot-node is valid");
|
|
556
|
+
expect(lines).toContain(` warning: ${RULES_FILE} is missing — run "serverk-bridge rules"`);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("warns about a stale rule file and still validates", async () => {
|
|
560
|
+
writeBot(botManifest());
|
|
561
|
+
writeFileSync(join(root, RULES_FILE), "# edited by hand\n");
|
|
562
|
+
|
|
563
|
+
await validate();
|
|
564
|
+
|
|
565
|
+
expect(lines).toContain("bot-node is valid");
|
|
566
|
+
expect(lines).toContain(` warning: ${RULES_FILE} is out of date — run "serverk-bridge rules"`);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("says when a bot turns the environment vault on", async () => {
|
|
539
570
|
writeBot(
|
|
540
571
|
botManifest({
|
|
541
572
|
environment: {
|
|
@@ -549,7 +580,7 @@ describe("validating a bot package", () => {
|
|
|
549
580
|
expect(lines).toContain(" environment: vault on");
|
|
550
581
|
});
|
|
551
582
|
|
|
552
|
-
|
|
583
|
+
it("fails a bot whose detection table is missing", async () => {
|
|
553
584
|
writeBot(botManifest(), null);
|
|
554
585
|
|
|
555
586
|
await expect(validate()).rejects.toThrow(
|
|
@@ -557,7 +588,7 @@ describe("validating a bot package", () => {
|
|
|
557
588
|
);
|
|
558
589
|
});
|
|
559
590
|
|
|
560
|
-
|
|
591
|
+
it("fails a bot whose detection table is stale", async () => {
|
|
561
592
|
writeBot(botManifest(), "{}\n");
|
|
562
593
|
|
|
563
594
|
await expect(validate()).rejects.toThrow(
|
|
@@ -565,7 +596,7 @@ describe("validating a bot package", () => {
|
|
|
565
596
|
);
|
|
566
597
|
});
|
|
567
598
|
|
|
568
|
-
|
|
599
|
+
it("warns about a port on a bot but still validates it", async () => {
|
|
569
600
|
writeBot(
|
|
570
601
|
botManifest({
|
|
571
602
|
container: {
|
|
@@ -585,7 +616,7 @@ describe("validating a bot package", () => {
|
|
|
585
616
|
);
|
|
586
617
|
});
|
|
587
618
|
|
|
588
|
-
|
|
619
|
+
it("fails a bot that declares platform secrets", async () => {
|
|
589
620
|
writeBot(
|
|
590
621
|
botManifest({
|
|
591
622
|
secrets: [
|
|
@@ -620,7 +651,7 @@ describe("availability report", () => {
|
|
|
620
651
|
modules,
|
|
621
652
|
});
|
|
622
653
|
|
|
623
|
-
|
|
654
|
+
it("warns about a protected module that cannot say when its action is unavailable", () => {
|
|
624
655
|
expect(
|
|
625
656
|
availabilityReportLines(
|
|
626
657
|
driverWithModules({
|
|
@@ -643,7 +674,7 @@ describe("availability report", () => {
|
|
|
643
674
|
]);
|
|
644
675
|
});
|
|
645
676
|
|
|
646
|
-
|
|
677
|
+
it("says nothing when the protected module declares the hook or protects nothing", () => {
|
|
647
678
|
expect(
|
|
648
679
|
availabilityReportLines(
|
|
649
680
|
driverWithModules({
|
|
@@ -674,3 +705,120 @@ describe("availability report", () => {
|
|
|
674
705
|
).toEqual([]);
|
|
675
706
|
});
|
|
676
707
|
});
|
|
708
|
+
|
|
709
|
+
describe("the confirm report", () => {
|
|
710
|
+
const driverWithSections = (sections: BridgeTab["sections"]): BridgeDriver => ({
|
|
711
|
+
install: {
|
|
712
|
+
kind: BridgeKind.Install,
|
|
713
|
+
run: async () => {
|
|
714
|
+
return;
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
lifecycle: {
|
|
718
|
+
kind: BridgeKind.Lifecycle,
|
|
719
|
+
command: async () => [
|
|
720
|
+
"run",
|
|
721
|
+
],
|
|
722
|
+
},
|
|
723
|
+
panel: {
|
|
724
|
+
tabs: [
|
|
725
|
+
{
|
|
726
|
+
id: "settings",
|
|
727
|
+
title: text("Settings"),
|
|
728
|
+
sections,
|
|
729
|
+
},
|
|
730
|
+
],
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
const versionForm = (confirm?: BridgeConfirm): BridgeFormSection => ({
|
|
735
|
+
layout: BridgeLayout.Form,
|
|
736
|
+
id: "version",
|
|
737
|
+
target: BridgeFormTarget.Variables,
|
|
738
|
+
reinstall: true,
|
|
739
|
+
confirm,
|
|
740
|
+
fields: [
|
|
741
|
+
{
|
|
742
|
+
key: "VERSION",
|
|
743
|
+
control: BridgeControl.Text,
|
|
744
|
+
label: text("Version"),
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
const modpacks = (confirm?: BridgeConfirm): BridgeCatalogSection => ({
|
|
750
|
+
layout: BridgeLayout.Catalog,
|
|
751
|
+
id: "modpacks",
|
|
752
|
+
module: "modpacks",
|
|
753
|
+
confirm,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
const modpack = (confirm?: BridgeConfirm, reinstall?: boolean): BridgeDetailSection => ({
|
|
757
|
+
layout: BridgeLayout.Detail,
|
|
758
|
+
id: "modpack",
|
|
759
|
+
module: "modpack",
|
|
760
|
+
variable: "MODPACK",
|
|
761
|
+
reinstall,
|
|
762
|
+
confirm,
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const worlds = (): BridgeTableSection => ({
|
|
766
|
+
layout: BridgeLayout.Table,
|
|
767
|
+
id: "worlds",
|
|
768
|
+
module: "worlds",
|
|
769
|
+
columns: [
|
|
770
|
+
{
|
|
771
|
+
key: "name",
|
|
772
|
+
label: text("Name"),
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
actions: [
|
|
776
|
+
{
|
|
777
|
+
id: "delete",
|
|
778
|
+
label: text("Delete"),
|
|
779
|
+
confirm: BridgeConfirm.Strong,
|
|
780
|
+
},
|
|
781
|
+
],
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("fails a form or a catalog that asks for a strong confirmation", () => {
|
|
785
|
+
expect(
|
|
786
|
+
confirmReportLines(
|
|
787
|
+
driverWithSections([
|
|
788
|
+
versionForm(BridgeConfirm.Strong),
|
|
789
|
+
modpacks(BridgeConfirm.Strong),
|
|
790
|
+
]),
|
|
791
|
+
),
|
|
792
|
+
).toEqual([
|
|
793
|
+
' error: tab "settings" section "version" is a form with `confirm: strong` — the platform takes a restore point before applying it, so declare `normal`; `strong` is only for what nothing brings back',
|
|
794
|
+
' error: tab "settings" section "modpacks" is a catalog with `confirm: strong` — the platform takes a restore point before applying it, so declare `normal`; `strong` is only for what nothing brings back',
|
|
795
|
+
]);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it("fails any section that reinstalls behind a strong confirmation", () => {
|
|
799
|
+
expect(
|
|
800
|
+
confirmReportLines(
|
|
801
|
+
driverWithSections([
|
|
802
|
+
modpack(BridgeConfirm.Strong, true),
|
|
803
|
+
]),
|
|
804
|
+
),
|
|
805
|
+
).toEqual([
|
|
806
|
+
' error: tab "settings" section "modpack" reinstalls with `confirm: strong` — the platform takes a restore point before every reinstall, so declare `normal`; `strong` is only for what nothing brings back',
|
|
807
|
+
]);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("leaves a strong action that deletes data and every normal confirmation alone", () => {
|
|
811
|
+
expect(
|
|
812
|
+
confirmReportLines(
|
|
813
|
+
driverWithSections([
|
|
814
|
+
versionForm(BridgeConfirm.Normal),
|
|
815
|
+
versionForm(),
|
|
816
|
+
modpacks(BridgeConfirm.Normal),
|
|
817
|
+
modpack(BridgeConfirm.Normal, true),
|
|
818
|
+
modpack(BridgeConfirm.Strong),
|
|
819
|
+
worlds(),
|
|
820
|
+
]),
|
|
821
|
+
),
|
|
822
|
+
).toEqual([]);
|
|
823
|
+
});
|
|
824
|
+
});
|
package/src/cli/validate.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "../manifest";
|
|
22
22
|
import type { BridgeDriver, BridgeSetupModule } from "../modules";
|
|
23
23
|
import {
|
|
24
|
+
BridgeConfirm,
|
|
24
25
|
BridgeLayout,
|
|
25
26
|
BridgePlace,
|
|
26
27
|
type BridgeSection,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
} from "../protocol";
|
|
32
33
|
import { validateDriver, validatePortActivation } from "../runtime/bridgeDriver";
|
|
33
34
|
import { directorySource } from "./directorySource";
|
|
35
|
+
import { rulesReportLines } from "./rules";
|
|
34
36
|
|
|
35
37
|
const HELP = `serverk-bridge validate [options]
|
|
36
38
|
|
|
@@ -312,6 +314,39 @@ const reportAvailability = (driver: BridgeDriver) => {
|
|
|
312
314
|
}
|
|
313
315
|
};
|
|
314
316
|
|
|
317
|
+
const RESTORE_POINT_LAYOUTS: ReadonlySet<BridgeLayout> = new Set([
|
|
318
|
+
BridgeLayout.Form,
|
|
319
|
+
BridgeLayout.Catalog,
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
export const confirmReportLines = (driver: BridgeDriver): string[] => {
|
|
323
|
+
const lines: string[] = [];
|
|
324
|
+
|
|
325
|
+
for (const tab of driver.panel?.tabs ?? []) {
|
|
326
|
+
for (const section of tab.sections) {
|
|
327
|
+
if (!("confirm" in section) || section.confirm !== BridgeConfirm.Strong) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (RESTORE_POINT_LAYOUTS.has(section.layout)) {
|
|
332
|
+
lines.push(
|
|
333
|
+
` error: tab "${tab.id}" section "${section.id}" is a ${section.layout} with \`confirm: strong\` — the platform takes a restore point before applying it, so declare \`normal\`; \`strong\` is only for what nothing brings back`,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if ("reinstall" in section && section.reinstall === true) {
|
|
340
|
+
lines.push(
|
|
341
|
+
` error: tab "${tab.id}" section "${section.id}" reinstalls with \`confirm: strong\` — the platform takes a restore point before every reinstall, so declare \`normal\`; \`strong\` is only for what nothing brings back`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return lines;
|
|
348
|
+
};
|
|
349
|
+
|
|
315
350
|
const openTargetOf = (target: GuideOpenTarget) => {
|
|
316
351
|
if (target.tab === GuideOpenTab.Panel) {
|
|
317
352
|
return `panel tab "${target.tabId}"`;
|
|
@@ -433,6 +468,14 @@ const validateRunnablePackage = async (
|
|
|
433
468
|
validateDriver(driver);
|
|
434
469
|
validatePortActivation(manifest.container.ports, driver);
|
|
435
470
|
|
|
471
|
+
const confirmLines = confirmReportLines(driver);
|
|
472
|
+
|
|
473
|
+
if (confirmLines.length > 0) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
`the panel asks for a strong confirmation where a restore point is taken:\n${confirmLines.join("\n")}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
436
479
|
const conditionalPorts = manifest.container.ports.filter((port) => port.activeWhen.length > 0).length;
|
|
437
480
|
const pairedPorts = manifest.container.ports.filter((port) => port.protocol === PortProtocol.TcpUdp).length;
|
|
438
481
|
const blockPorts = manifest.container.ports.filter((port) => port.count > 1).length;
|
|
@@ -446,6 +489,7 @@ const validateRunnablePackage = async (
|
|
|
446
489
|
...botPortLines(manifest),
|
|
447
490
|
...botEnvironmentLines(manifest),
|
|
448
491
|
...detectLines,
|
|
492
|
+
...rulesReportLines(dir),
|
|
449
493
|
]) {
|
|
450
494
|
console.log(line);
|
|
451
495
|
}
|
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",
|