@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.
Files changed (94) hide show
  1. package/package.json +3 -2
  2. package/rules/package.md +65 -0
  3. package/src/blog/articleFrontmatter.test.ts +10 -10
  4. package/src/blog/compile.test.ts +9 -9
  5. package/src/bot/botDeploy.test.ts +16 -16
  6. package/src/bot/botEnvironment.test.ts +8 -8
  7. package/src/bot/botIdentity.test.ts +12 -12
  8. package/src/bot/botPanel.test.ts +13 -13
  9. package/src/bot/botPanel.ts +4 -4
  10. package/src/bot/botState.test.ts +3 -3
  11. package/src/bot/botTerminal.test.ts +5 -5
  12. package/src/bot/botVariables.test.ts +8 -8
  13. package/src/bot/detect/botToken.test.ts +6 -6
  14. package/src/bot/detect/detect.test.ts +21 -21
  15. package/src/bot/detect/detectTable.test.ts +5 -5
  16. package/src/bot/detect/envExample.test.ts +2 -2
  17. package/src/bot/detect/envScan.test.ts +4 -4
  18. package/src/bot/detect/inferBotPlatform.test.ts +4 -4
  19. package/src/bot/detect/keys.test.ts +3 -3
  20. package/src/bot/detect/sensitiveKey.test.ts +5 -5
  21. package/src/bot/detect/serverkJson.test.ts +5 -5
  22. package/src/catalogs/curseforge.test.ts +35 -35
  23. package/src/catalogs/thunderstore.test.ts +9 -9
  24. package/src/cli/detectTable.test.ts +3 -3
  25. package/src/cli/link.test.ts +11 -11
  26. package/src/cli/main.ts +8 -0
  27. package/src/cli/rules.test.ts +198 -0
  28. package/src/cli/rules.ts +188 -0
  29. package/src/cli/validate.test.ts +179 -31
  30. package/src/cli/validate.ts +44 -0
  31. package/src/codecs/ini.test.ts +13 -13
  32. package/src/codecs/javaProperties.test.ts +11 -11
  33. package/src/codecs/sourceCfg.test.ts +14 -14
  34. package/src/codecs/xmlProperties.test.ts +10 -10
  35. package/src/core/bridgePort.test.ts +6 -6
  36. package/src/core/bridgeUserError.test.ts +4 -4
  37. package/src/core/bridgeValue.test.ts +5 -5
  38. package/src/core/describeDownloadFailure.test.ts +12 -12
  39. package/src/core/describeDownloadFailure.ts +10 -10
  40. package/src/guides/markdown.test.ts +31 -31
  41. package/src/install/installStamp.test.ts +9 -9
  42. package/src/manifest/extension.ts +36 -0
  43. package/src/manifest/index.ts +1 -0
  44. package/src/manifest/manifest.test.ts +208 -90
  45. package/src/manifest/manifest.ts +34 -0
  46. package/src/net/loopback.test.ts +10 -10
  47. package/src/presence/rosterSync.test.ts +7 -7
  48. package/src/probes/a2s.test.ts +5 -5
  49. package/src/probes/minecraftPing.test.ts +17 -17
  50. package/src/probes/raknetPing.test.ts +13 -13
  51. package/src/protocol/bridgePreview.ts +1 -0
  52. package/src/rcon/rconAccess.test.ts +20 -20
  53. package/src/rcon/rconAccess.ts +14 -14
  54. package/src/rcon/rconClient.test.ts +49 -49
  55. package/src/rcon/rconFrame.test.ts +14 -14
  56. package/src/rcon/rconIds.test.ts +4 -4
  57. package/src/rcon/rconWriter.test.ts +6 -6
  58. package/src/runtime/bridgeActivity.test.ts +15 -15
  59. package/src/runtime/bridgeBackupLease.test.ts +9 -9
  60. package/src/runtime/bridgeConfigReader.test.ts +5 -5
  61. package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
  62. package/src/runtime/bridgeContext.test.ts +5 -5
  63. package/src/runtime/bridgeDriver.test.ts +94 -94
  64. package/src/runtime/bridgeEvents.test.ts +15 -15
  65. package/src/runtime/bridgeFiles.test.ts +54 -54
  66. package/src/runtime/bridgeInstall.test.ts +10 -10
  67. package/src/runtime/bridgeLiveConfig.test.ts +6 -6
  68. package/src/runtime/bridgePending.test.ts +6 -6
  69. package/src/runtime/bridgeRedaction.test.ts +16 -16
  70. package/src/runtime/bridgeRegistry.test.ts +84 -52
  71. package/src/runtime/bridgeRegistry.ts +2 -2
  72. package/src/runtime/bridgeServer.test.ts +31 -31
  73. package/src/runtime/bridgeSetup.test.ts +27 -27
  74. package/src/runtime/bridgeSetup.ts +1 -1
  75. package/src/runtime/bridgeSetupHandlers.test.ts +13 -13
  76. package/src/runtime/bridgeSignal.test.ts +7 -7
  77. package/src/runtime/bridgeSupervisor.test.ts +42 -42
  78. package/src/runtime/serve.ts +4 -4
  79. package/src/steam/avatars.test.ts +13 -13
  80. package/src/steam/steamApp.test.ts +8 -8
  81. package/src/steam/steamcmd.test.ts +17 -17
  82. package/src/steam/steamcmd.ts +4 -4
  83. package/src/steam/steamcmdNarrator.test.ts +4 -4
  84. package/src/steam/workshop.test.ts +5 -5
  85. package/src/utils/changedSettingKeys.test.ts +5 -5
  86. package/src/utils/describeExecFailure.test.ts +4 -4
  87. package/src/utils/describeExecFailure.ts +1 -1
  88. package/src/utils/sameSettingValue.test.ts +7 -7
  89. package/src/utils/token.test.ts +7 -7
  90. package/src/wine/protonCommand.test.ts +5 -5
  91. package/src/wine/wineCommand.test.ts +6 -6
  92. package/src/wine/winePrefix.test.ts +6 -6
  93. package/src/wire/activity.test.ts +8 -8
  94. package/src/wire/panel.test.ts +28 -28
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
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 { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
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
- test("refuses a file: bridge dependency", async () => {
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
- test("refuses a link: bridge dependency", async () => {
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
- test("refuses a file: bridge devDependency", async () => {
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
- test("accepts a published bridge dependency and moves on to the manifest", async () => {
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
- test("ignores a package without a bridge dependency", async () => {
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
- test("ignores a package.json that is absent", async () => {
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
- test("says none for a driver that declares no setup", () => {
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
- test("names every step, its kind, whether it is required, and where it lands", () => {
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: "txadmin",
142
- title: text("txAdmin"),
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
- ' txadmin — open, optional → panel tab "license"',
178
+ ' dashboard — open, optional → panel tab "license"',
169
179
  " invite — open, optional → access",
170
180
  ]);
171
181
  });
172
182
 
173
- test("leaves a driver step that needs nothing running with no target", () => {
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
- test("reads an omitted required as required, the way the platform does", () => {
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
- test("says nothing about a roster table that declares where it belongs", () => {
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
- test("warns that the platform absorbs an unplaced players collection by convention", () => {
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
- test("warns about a tab that takes the name of a platform sidebar entry", () => {
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
- test("leaves a players tab alone while it is the roster tab", () => {
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
- test("warns about a players tab that holds no roster", () => {
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
- test("says nothing about a tab that declares no sections at all", () => {
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
- test("still warns about an empty players tab, which holds no roster", () => {
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
- test("says nothing about a tab that keeps a leftover section", () => {
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
- test("validates a bot with no ports and says which kind it read", async () => {
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
- test("says when a bot turns the environment vault on", async () => {
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
- test("fails a bot whose detection table is missing", async () => {
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
- test("fails a bot whose detection table is stale", async () => {
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
- test("warns about a port on a bot but still validates it", async () => {
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
- test("fails a bot that declares platform secrets", async () => {
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
- test("warns about a protected module that cannot say when its action is unavailable", () => {
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
- test("says nothing when the protected module declares the hook or protects nothing", () => {
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
+ });
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "bun:test";
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
- test("reads the keys of one section", () => {
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
- test("reads another section without leaking keys", () => {
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
- test("reads the keys before the first header when no section is asked for", () => {
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
- test("keeps the last of a repeated key", () => {
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
- test("returns nothing for an unknown section", () => {
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
- test("rewrites a value in place and keeps comments and order", () => {
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
- test("appends an unknown key at the end of its section", () => {
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
- test("creates a section that does not exist yet", () => {
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
- test("drops repeated occurrences of a key it rewrites", () => {
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
- test("writes into an empty file", () => {
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
- test("writes preamble keys when no section is asked for", () => {
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
- test("leaves other sections untouched", () => {
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, test } from "bun:test";
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
- test("decodes the unicode escapes the java server writes", () => {
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
- test("decodes backslash escapes in values", () => {
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
- test("repairs a value the server stored as latin-1 bytes of utf-8", () => {
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
- test("keeps plain latin-1 text that is not utf-8", () => {
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
- test("keeps comments and plain values", () => {
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
- test("writes non ascii text as unicode escapes", () => {
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
- test("escapes the characters java treats as separators", () => {
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
- test("escapes a leading space and control characters", () => {
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
- test("round trips astral characters", () => {
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
- test("appends a key the file does not have yet", () => {
60
+ it("appends a key the file does not have yet", () => {
61
61
  expect(
62
62
  mergeJavaProperties("", {
63
63
  motd: "hello",