@serverkgg/bridge 0.1.2 → 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.
Files changed (93) hide show
  1. package/package.json +3 -2
  2. package/rules/package.md +60 -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 +52 -30
  30. package/src/cli/validate.ts +2 -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/rcon/rconAccess.test.ts +20 -20
  52. package/src/rcon/rconAccess.ts +13 -13
  53. package/src/rcon/rconClient.test.ts +49 -49
  54. package/src/rcon/rconFrame.test.ts +14 -14
  55. package/src/rcon/rconIds.test.ts +4 -4
  56. package/src/rcon/rconWriter.test.ts +6 -6
  57. package/src/runtime/bridgeActivity.test.ts +15 -15
  58. package/src/runtime/bridgeBackupLease.test.ts +9 -9
  59. package/src/runtime/bridgeConfigReader.test.ts +5 -5
  60. package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
  61. package/src/runtime/bridgeContext.test.ts +5 -5
  62. package/src/runtime/bridgeDriver.test.ts +94 -94
  63. package/src/runtime/bridgeEvents.test.ts +15 -15
  64. package/src/runtime/bridgeFiles.test.ts +54 -54
  65. package/src/runtime/bridgeInstall.test.ts +10 -10
  66. package/src/runtime/bridgeLiveConfig.test.ts +6 -6
  67. package/src/runtime/bridgePending.test.ts +6 -6
  68. package/src/runtime/bridgeRedaction.test.ts +16 -16
  69. package/src/runtime/bridgeRegistry.test.ts +52 -52
  70. package/src/runtime/bridgeRegistry.ts +2 -2
  71. package/src/runtime/bridgeServer.test.ts +31 -31
  72. package/src/runtime/bridgeSetup.test.ts +27 -27
  73. package/src/runtime/bridgeSetup.ts +1 -1
  74. package/src/runtime/bridgeSetupHandlers.test.ts +13 -13
  75. package/src/runtime/bridgeSignal.test.ts +7 -7
  76. package/src/runtime/bridgeSupervisor.test.ts +42 -42
  77. package/src/runtime/serve.ts +4 -4
  78. package/src/steam/avatars.test.ts +13 -13
  79. package/src/steam/steamApp.test.ts +8 -8
  80. package/src/steam/steamcmd.test.ts +17 -17
  81. package/src/steam/steamcmd.ts +4 -4
  82. package/src/steam/steamcmdNarrator.test.ts +4 -4
  83. package/src/steam/workshop.test.ts +5 -5
  84. package/src/utils/changedSettingKeys.test.ts +5 -5
  85. package/src/utils/describeExecFailure.test.ts +4 -4
  86. package/src/utils/describeExecFailure.ts +1 -1
  87. package/src/utils/sameSettingValue.test.ts +7 -7
  88. package/src/utils/token.test.ts +7 -7
  89. package/src/wine/protonCommand.test.ts +5 -5
  90. package/src/wine/wineCommand.test.ts +6 -6
  91. package/src/wine/winePrefix.test.ts +6 -6
  92. package/src/wire/activity.test.ts +8 -8
  93. package/src/wire/panel.test.ts +28 -28
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import type { BridgeContext, BridgeRequestOptions } from "../context";
3
3
  import { BridgeFailureCode, BridgeFailureError, BridgeNetError, BridgeSecretError } from "../core";
4
4
  import { BridgeHttpMethod } from "../protocol";
@@ -39,7 +39,7 @@ const file = (overrides: Partial<CurseforgeFile> = {}): CurseforgeFile => {
39
39
  };
40
40
 
41
41
  describe("curseforgeSha1", () => {
42
- test("finds the sha1 hash and lowercases it", () => {
42
+ it("finds the sha1 hash and lowercases it", () => {
43
43
  expect(
44
44
  curseforgeSha1(
45
45
  file({
@@ -58,13 +58,13 @@ describe("curseforgeSha1", () => {
58
58
  ).toBe("aabb");
59
59
  });
60
60
 
61
- test("returns null when the file carries no sha1", () => {
61
+ it("returns null when the file carries no sha1", () => {
62
62
  expect(curseforgeSha1(file())).toBeNull();
63
63
  });
64
64
  });
65
65
 
66
66
  describe("curseforgeDownloadUrl", () => {
67
- test("re-encodes the path of a curseforge cdn url", () => {
67
+ it("re-encodes the path of a curseforge cdn url", () => {
68
68
  expect(
69
69
  curseforgeDownloadUrl(
70
70
  file({
@@ -74,7 +74,7 @@ describe("curseforgeDownloadUrl", () => {
74
74
  ).toBe("https://edge.forgecdn.net/files/4711/12/my%20mod.jar");
75
75
  });
76
76
 
77
- test("refuses a url that is not on the curseforge cdn", () => {
77
+ it("refuses a url that is not on the curseforge cdn", () => {
78
78
  expect(
79
79
  curseforgeDownloadUrl(
80
80
  file({
@@ -84,7 +84,7 @@ describe("curseforgeDownloadUrl", () => {
84
84
  ).toBeNull();
85
85
  });
86
86
 
87
- test("keeps a name curseforge already escaped escaped once", () => {
87
+ it("keeps a name curseforge already escaped escaped once", () => {
88
88
  expect(
89
89
  curseforgeDownloadUrl(
90
90
  file({
@@ -94,7 +94,7 @@ describe("curseforgeDownloadUrl", () => {
94
94
  ).toBe("https://edge.forgecdn.net/files/1234/567/Some%20Mod%20%5B1.20%5D.jar");
95
95
  });
96
96
 
97
- test("refuses a path carrying a broken escape", () => {
97
+ it("refuses a path carrying a broken escape", () => {
98
98
  expect(
99
99
  curseforgeDownloadUrl(
100
100
  file({
@@ -104,7 +104,7 @@ describe("curseforgeDownloadUrl", () => {
104
104
  ).toBeNull();
105
105
  });
106
106
 
107
- test("refuses plain http and a missing url", () => {
107
+ it("refuses plain http and a missing url", () => {
108
108
  expect(
109
109
  curseforgeDownloadUrl(
110
110
  file({
@@ -123,15 +123,15 @@ describe("curseforgeDownloadUrl", () => {
123
123
  });
124
124
 
125
125
  describe("curseforgeFallbackUrl", () => {
126
- test("splits a modern file id into four and the rest", () => {
126
+ it("splits a modern file id into four and the rest", () => {
127
127
  expect(curseforgeFallbackUrl(4_711_123, "mod.jar")).toBe(`${CURSEFORGE_DOWNLOAD}/files/4711/123/mod.jar`);
128
128
  });
129
129
 
130
- test("divides a legacy file id by a thousand", () => {
130
+ it("divides a legacy file id by a thousand", () => {
131
131
  expect(curseforgeFallbackUrl(123_456, "mod.jar")).toBe(`${CURSEFORGE_DOWNLOAD}/files/123/123456/mod.jar`);
132
132
  });
133
133
 
134
- test("percent encodes a file name carrying spaces and brackets", () => {
134
+ it("percent encodes a file name carrying spaces and brackets", () => {
135
135
  expect(curseforgeFallbackUrl(4_711_123, "Butchersdelight beta 1.20.1 2.1.0.jar")).toBe(
136
136
  `${CURSEFORGE_DOWNLOAD}/files/4711/123/Butchersdelight%20beta%201.20.1%202.1.0.jar`,
137
137
  );
@@ -142,7 +142,7 @@ describe("curseforgeFallbackUrl", () => {
142
142
  });
143
143
 
144
144
  describe("isCurseforgeServerFile", () => {
145
- test("accepts a file tagged server", () => {
145
+ it("accepts a file tagged server", () => {
146
146
  expect(
147
147
  isCurseforgeServerFile(
148
148
  file({
@@ -155,7 +155,7 @@ describe("isCurseforgeServerFile", () => {
155
155
  ).toBe(true);
156
156
  });
157
157
 
158
- test("refuses a file tagged client only", () => {
158
+ it("refuses a file tagged client only", () => {
159
159
  expect(
160
160
  isCurseforgeServerFile(
161
161
  file({
@@ -167,7 +167,7 @@ describe("isCurseforgeServerFile", () => {
167
167
  ).toBe(false);
168
168
  });
169
169
 
170
- test("accepts a file with no side tag", () => {
170
+ it("accepts a file with no side tag", () => {
171
171
  expect(
172
172
  isCurseforgeServerFile(
173
173
  file({
@@ -179,7 +179,7 @@ describe("isCurseforgeServerFile", () => {
179
179
  ).toBe(true);
180
180
  });
181
181
 
182
- test("reads the side tags whatever their casing", () => {
182
+ it("reads the side tags whatever their casing", () => {
183
183
  expect(
184
184
  isCurseforgeServerFile(
185
185
  file({
@@ -203,7 +203,7 @@ describe("isCurseforgeServerFile", () => {
203
203
  });
204
204
 
205
205
  describe("selectCurseforgeFile", () => {
206
- test("returns null when nothing is available", () => {
206
+ it("returns null when nothing is available", () => {
207
207
  expect(
208
208
  selectCurseforgeFile([
209
209
  file({
@@ -214,7 +214,7 @@ describe("selectCurseforgeFile", () => {
214
214
  expect(selectCurseforgeFile([])).toBeNull();
215
215
  });
216
216
 
217
- test("prefers the newest file", () => {
217
+ it("prefers the newest file", () => {
218
218
  expect(
219
219
  selectCurseforgeFile([
220
220
  file({
@@ -230,7 +230,7 @@ describe("selectCurseforgeFile", () => {
230
230
  ).toBe(9);
231
231
  });
232
232
 
233
- test("prefers a file matching the game version", () => {
233
+ it("prefers a file matching the game version", () => {
234
234
  expect(
235
235
  selectCurseforgeFile(
236
236
  [
@@ -254,7 +254,7 @@ describe("selectCurseforgeFile", () => {
254
254
  ).toBe(2);
255
255
  });
256
256
 
257
- test("falls back to the newest when the game version is nowhere", () => {
257
+ it("falls back to the newest when the game version is nowhere", () => {
258
258
  expect(
259
259
  selectCurseforgeFile(
260
260
  [
@@ -272,7 +272,7 @@ describe("selectCurseforgeFile", () => {
272
272
  ).toBe(9);
273
273
  });
274
274
 
275
- test("keeps stable releases when asked and falls back when there are none", () => {
275
+ it("keeps stable releases when asked and falls back when there are none", () => {
276
276
  const files = [
277
277
  file({
278
278
  id: 9,
@@ -301,7 +301,7 @@ describe("selectCurseforgeFile", () => {
301
301
  ).toBe(9);
302
302
  });
303
303
 
304
- test("drops client-only files when asked for a server file", () => {
304
+ it("drops client-only files when asked for a server file", () => {
305
305
  expect(
306
306
  selectCurseforgeFile(
307
307
  [
@@ -393,7 +393,7 @@ const batchIds = (calls: CatalogCall[], index: number) => {
393
393
  };
394
394
 
395
395
  describe("createCurseforgeCatalog", () => {
396
- test("sends the key as a header and the search cache window", async () => {
396
+ it("sends the key as a header and the search cache window", async () => {
397
397
  const { catalog, calls } = catalogContext();
398
398
 
399
399
  await catalog.search();
@@ -405,7 +405,7 @@ describe("createCurseforgeCatalog", () => {
405
405
  expect(searchParams(calls).get("gameId")).toBe("432");
406
406
  });
407
407
 
408
- test("leaves the sort order out when no sort is asked for", async () => {
408
+ it("leaves the sort order out when no sort is asked for", async () => {
409
409
  const { catalog, calls } = catalogContext();
410
410
 
411
411
  await catalog.search();
@@ -413,7 +413,7 @@ describe("createCurseforgeCatalog", () => {
413
413
  expect(searchParams(calls).has("sortOrder")).toBe(false);
414
414
  });
415
415
 
416
- test("sorts a popularity search descending", async () => {
416
+ it("sorts a popularity search descending", async () => {
417
417
  const { catalog, calls } = catalogContext();
418
418
 
419
419
  await catalog.search({
@@ -424,7 +424,7 @@ describe("createCurseforgeCatalog", () => {
424
424
  expect(searchParams(calls).get("sortOrder")).toBe(CurseforgeSortOrder.Desc);
425
425
  });
426
426
 
427
- test("sorts a name search ascending", async () => {
427
+ it("sorts a name search ascending", async () => {
428
428
  const { catalog, calls } = catalogContext();
429
429
 
430
430
  await catalog.search({
@@ -434,7 +434,7 @@ describe("createCurseforgeCatalog", () => {
434
434
  expect(searchParams(calls).get("sortOrder")).toBe(CurseforgeSortOrder.Asc);
435
435
  });
436
436
 
437
- test("sorts an author search ascending", async () => {
437
+ it("sorts an author search ascending", async () => {
438
438
  const { catalog, calls } = catalogContext();
439
439
 
440
440
  await catalog.search({
@@ -444,7 +444,7 @@ describe("createCurseforgeCatalog", () => {
444
444
  expect(searchParams(calls).get("sortOrder")).toBe(CurseforgeSortOrder.Asc);
445
445
  });
446
446
 
447
- test("keeps an explicit sort order", async () => {
447
+ it("keeps an explicit sort order", async () => {
448
448
  const { catalog, calls } = catalogContext();
449
449
 
450
450
  await catalog.search({
@@ -455,7 +455,7 @@ describe("createCurseforgeCatalog", () => {
455
455
  expect(searchParams(calls).get("sortOrder")).toBe(CurseforgeSortOrder.Desc);
456
456
  });
457
457
 
458
- test("drops empty and absent filters but keeps a zero index", async () => {
458
+ it("drops empty and absent filters but keeps a zero index", async () => {
459
459
  const { catalog, calls } = catalogContext();
460
460
 
461
461
  await catalog.search({
@@ -472,7 +472,7 @@ describe("createCurseforgeCatalog", () => {
472
472
  expect(params.get("index")).toBe("0");
473
473
  });
474
474
 
475
- test("asks for the files of a mod without a query string", async () => {
475
+ it("asks for the files of a mod without a query string", async () => {
476
476
  const { catalog, calls } = catalogContext();
477
477
 
478
478
  await catalog.modFiles(4711);
@@ -480,7 +480,7 @@ describe("createCurseforgeCatalog", () => {
480
480
  expect(calls[0]?.url).toBe(`${CURSEFORGE_API}/mods/4711/files`);
481
481
  });
482
482
 
483
- test("passes every mod file filter through", async () => {
483
+ it("passes every mod file filter through", async () => {
484
484
  const { catalog, calls } = catalogContext();
485
485
 
486
486
  await catalog.modFiles(4711, {
@@ -498,7 +498,7 @@ describe("createCurseforgeCatalog", () => {
498
498
  expect(params.get("pageSize")).toBe("50");
499
499
  });
500
500
 
501
- test("caches categories for a day rather than for the project window", async () => {
501
+ it("caches categories for a day rather than for the project window", async () => {
502
502
  const { catalog, calls } = catalogContext({
503
503
  options: {
504
504
  projectCacheSeconds: 5,
@@ -511,7 +511,7 @@ describe("createCurseforgeCatalog", () => {
511
511
  expect(searchParams(calls).get("classId")).toBe("6");
512
512
  });
513
513
 
514
- test("dedupes, sorts and batches file ids into posted bodies", async () => {
514
+ it("dedupes, sorts and batches file ids into posted bodies", async () => {
515
515
  const { catalog, calls } = catalogContext();
516
516
 
517
517
  await catalog.files([
@@ -536,7 +536,7 @@ describe("createCurseforgeCatalog", () => {
536
536
  expect(second.at(-1)).toBe(150);
537
537
  });
538
538
 
539
- test("refuses to work without a key", async () => {
539
+ it("refuses to work without a key", async () => {
540
540
  const { catalog, calls } = catalogContext({
541
541
  key: null,
542
542
  });
@@ -546,7 +546,7 @@ describe("createCurseforgeCatalog", () => {
546
546
  expect(calls).toHaveLength(0);
547
547
  });
548
548
 
549
- test("turns a rejected key into a secret error", async () => {
549
+ it("turns a rejected key into a secret error", async () => {
550
550
  for (const status of [
551
551
  401,
552
552
  403,
@@ -559,7 +559,7 @@ describe("createCurseforgeCatalog", () => {
559
559
  }
560
560
  });
561
561
 
562
- test("turns throttling into a rate limited failure", async () => {
562
+ it("turns throttling into a rate limited failure", async () => {
563
563
  for (const status of [
564
564
  429,
565
565
  503,
@@ -1,20 +1,20 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import { parseThunderstoreManifest, thunderstoreDependency, thunderstoreDownloadUrl } from "./thunderstore";
3
3
 
4
4
  describe("thunderstoreDownloadUrl", () => {
5
- test("points straight at the cdn so no redirect has to be followed", () => {
5
+ it("points straight at the cdn so no redirect has to be followed", () => {
6
6
  expect(thunderstoreDownloadUrl("denikson-BepInExPack_Valheim-5.4.2202")).toBe(
7
7
  "https://gcdn.thunderstore.io/live/repository/packages/denikson-BepInExPack_Valheim-5.4.2202.zip",
8
8
  );
9
9
  });
10
10
 
11
- test("escapes a package name", () => {
11
+ it("escapes a package name", () => {
12
12
  expect(thunderstoreDownloadUrl("a b-c-1.0.0")).toContain("a%20b-c-1.0.0.zip");
13
13
  });
14
14
  });
15
15
 
16
16
  describe("parseThunderstoreManifest", () => {
17
- test("reads name, version and dependencies", () => {
17
+ it("reads name, version and dependencies", () => {
18
18
  expect(
19
19
  parseThunderstoreManifest(
20
20
  JSON.stringify({
@@ -36,7 +36,7 @@ describe("parseThunderstoreManifest", () => {
36
36
  });
37
37
  });
38
38
 
39
- test("defaults missing dependencies to an empty list", () => {
39
+ it("defaults missing dependencies to an empty list", () => {
40
40
  expect(
41
41
  parseThunderstoreManifest(
42
42
  JSON.stringify({
@@ -47,7 +47,7 @@ describe("parseThunderstoreManifest", () => {
47
47
  ).toEqual([]);
48
48
  });
49
49
 
50
- test("drops entries that are not strings", () => {
50
+ it("drops entries that are not strings", () => {
51
51
  expect(
52
52
  parseThunderstoreManifest(
53
53
  JSON.stringify({
@@ -64,7 +64,7 @@ describe("parseThunderstoreManifest", () => {
64
64
  ]);
65
65
  });
66
66
 
67
- test("returns null for anything that is not a manifest", () => {
67
+ it("returns null for anything that is not a manifest", () => {
68
68
  expect(parseThunderstoreManifest("")).toBeNull();
69
69
  expect(parseThunderstoreManifest("[]")).toBeNull();
70
70
  expect(
@@ -78,7 +78,7 @@ describe("parseThunderstoreManifest", () => {
78
78
  });
79
79
 
80
80
  describe("thunderstoreDependency", () => {
81
- test("splits a dependency reference into its three parts", () => {
81
+ it("splits a dependency reference into its three parts", () => {
82
82
  expect(thunderstoreDependency("denikson-BepInExPack_Valheim-5.4.2202")).toEqual({
83
83
  namespace: "denikson",
84
84
  name: "BepInExPack_Valheim",
@@ -87,7 +87,7 @@ describe("thunderstoreDependency", () => {
87
87
  });
88
88
  });
89
89
 
90
- test("returns null for a reference it cannot split", () => {
90
+ it("returns null for a reference it cannot split", () => {
91
91
  expect(thunderstoreDependency("denikson-BepInExPack")).toBeNull();
92
92
  expect(thunderstoreDependency("a-b-c-d")).toBeNull();
93
93
  });
@@ -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 { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -25,7 +25,7 @@ describe("serverk-bridge detect-table", () => {
25
25
  });
26
26
  });
27
27
 
28
- test("writes the table the launchers read, then passes its own check", async () => {
28
+ it("writes the table the launchers read, then passes its own check", async () => {
29
29
  const out = join(root, "detect.json");
30
30
 
31
31
  await runDetectTable([
@@ -42,7 +42,7 @@ describe("serverk-bridge detect-table", () => {
42
42
  ]);
43
43
  });
44
44
 
45
- test("fails the check on a stale or missing table", async () => {
45
+ it("fails the check on a stale or missing table", async () => {
46
46
  const out = join(root, "detect.json");
47
47
 
48
48
  await expect(
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
2
  import {
3
3
  existsSync,
4
4
  lstatSync,
@@ -60,7 +60,7 @@ describe("link", () => {
60
60
  });
61
61
  });
62
62
 
63
- test("skips when CI is set", () => {
63
+ it("skips when CI is set", () => {
64
64
  writePackage(source, "@serverkgg/bridge", "0.0.3");
65
65
  writePackage(target, "@serverkgg/bridge", "0.0.2");
66
66
 
@@ -76,14 +76,14 @@ describe("link", () => {
76
76
  expect(lstatSync(target).isSymbolicLink()).toBe(false);
77
77
  });
78
78
 
79
- test("skips when the local checkout is missing", () => {
79
+ it("skips when the local checkout is missing", () => {
80
80
  expect(link()).toMatchObject({
81
81
  kind: "skipped",
82
82
  reason: "source-missing",
83
83
  });
84
84
  });
85
85
 
86
- test("skips when the local checkout is another package", () => {
86
+ it("skips when the local checkout is another package", () => {
87
87
  writePackage(source, "@serverkgg/minecraft", "0.0.0");
88
88
 
89
89
  expect(link()).toMatchObject({
@@ -92,7 +92,7 @@ describe("link", () => {
92
92
  });
93
93
  });
94
94
 
95
- test("replaces the registry directory with a symlink", () => {
95
+ it("replaces the registry directory with a symlink", () => {
96
96
  writePackage(source, "@serverkgg/bridge", "0.0.3");
97
97
  writePackage(target, "@serverkgg/bridge", "0.0.2");
98
98
  writeFileSync(join(target, "installed.txt"), "registry");
@@ -109,7 +109,7 @@ describe("link", () => {
109
109
  expect(existsSync(join(source, "package.json"))).toBe(true);
110
110
  });
111
111
 
112
- test("creates the scope directory when nothing is installed", () => {
112
+ it("creates the scope directory when nothing is installed", () => {
113
113
  writePackage(source, "@serverkgg/bridge", "0.0.3");
114
114
 
115
115
  expect(link()).toMatchObject({
@@ -119,7 +119,7 @@ describe("link", () => {
119
119
  expect(realpathSync(target)).toBe(source);
120
120
  });
121
121
 
122
- test("is idempotent once linked", () => {
122
+ it("is idempotent once linked", () => {
123
123
  writePackage(source, "@serverkgg/bridge", "0.0.3");
124
124
 
125
125
  expect(link()).toMatchObject({
@@ -134,7 +134,7 @@ describe("link", () => {
134
134
  expect(realpathSync(target)).toBe(source);
135
135
  });
136
136
 
137
- test("relinks a symlink pointing somewhere else", () => {
137
+ it("relinks a symlink pointing somewhere else", () => {
138
138
  const other = join(root, "other");
139
139
 
140
140
  writePackage(source, "@serverkgg/bridge", "0.0.3");
@@ -153,7 +153,7 @@ describe("link", () => {
153
153
  expect(existsSync(join(other, "package.json"))).toBe(true);
154
154
  });
155
155
 
156
- test("unlink removes the symlink and keeps the checkout", () => {
156
+ it("unlink removes the symlink and keeps the checkout", () => {
157
157
  writePackage(source, "@serverkgg/bridge", "0.0.3");
158
158
  link();
159
159
 
@@ -166,7 +166,7 @@ describe("link", () => {
166
166
  expect(existsSync(join(source, "package.json"))).toBe(true);
167
167
  });
168
168
 
169
- test("unlink leaves a registry install alone", () => {
169
+ it("unlink leaves a registry install alone", () => {
170
170
  writePackage(target, "@serverkgg/bridge", "0.0.2");
171
171
 
172
172
  expect(unlinkBridge(cwd)).toEqual({
@@ -176,7 +176,7 @@ describe("link", () => {
176
176
  expect(existsSync(join(target, "package.json"))).toBe(true);
177
177
  });
178
178
 
179
- test("check reports every state", () => {
179
+ it("check reports every state", () => {
180
180
  expect(checkBridge(cwd)).toEqual({
181
181
  kind: "missing",
182
182
  });
package/src/cli/main.ts CHANGED
@@ -4,6 +4,7 @@ import { errorText } from "../runtime/bridgeLog";
4
4
  import { runCompile } from "./compile";
5
5
  import { runDetectTable } from "./detectTable";
6
6
  import { runLink } from "./link";
7
+ import { runRules } from "./rules";
7
8
  import { runSchema } from "./schema";
8
9
  import { runValidate } from "./validate";
9
10
 
@@ -15,6 +16,7 @@ commands:
15
16
  detect-table write the bot detection table the launchers read
16
17
  compile compile the bridge binary for amd64
17
18
  link use a local bridge checkout while developing
19
+ rules write the conventions for package code into .claude/rules
18
20
 
19
21
  run "serverk-bridge <command> --help" for options`;
20
22
 
@@ -52,6 +54,12 @@ try {
52
54
  break;
53
55
  }
54
56
 
57
+ case "rules": {
58
+ await runRules(args);
59
+
60
+ break;
61
+ }
62
+
55
63
  case undefined:
56
64
  case "--help": {
57
65
  console.log(USAGE);