@serverkgg/bridge 0.0.2 → 0.0.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.
@@ -0,0 +1,89 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { runValidate } from "./validate";
6
+
7
+ describe("validate", () => {
8
+ let root = "";
9
+
10
+ const writePackage = (dependencies: Record<string, string>) => {
11
+ writeFileSync(
12
+ join(root, "package.json"),
13
+ JSON.stringify({
14
+ name: "@serverkgg/minecraft",
15
+ dependencies,
16
+ }),
17
+ );
18
+ };
19
+
20
+ const validate = () => {
21
+ return runValidate([
22
+ "--dir",
23
+ root,
24
+ ]);
25
+ };
26
+
27
+ beforeEach(() => {
28
+ root = mkdtempSync(`${tmpdir()}/serverk-validate-`);
29
+ });
30
+
31
+ afterEach(() => {
32
+ rmSync(root, {
33
+ force: true,
34
+ recursive: true,
35
+ });
36
+ });
37
+
38
+ test("refuses a file: bridge dependency", async () => {
39
+ writePackage({
40
+ "@serverkgg/bridge": "file:../serverk/modules/bridge",
41
+ });
42
+
43
+ await expect(validate()).rejects.toThrow(
44
+ 'package.json depends on @serverkgg/bridge via "file:../serverk/modules/bridge"',
45
+ );
46
+ });
47
+
48
+ test("refuses a link: bridge dependency", async () => {
49
+ writePackage({
50
+ "@serverkgg/bridge": "link:@serverkgg/bridge",
51
+ });
52
+
53
+ await expect(validate()).rejects.toThrow('depends on @serverkgg/bridge via "link:@serverkgg/bridge"');
54
+ });
55
+
56
+ test("refuses a file: bridge devDependency", async () => {
57
+ writeFileSync(
58
+ join(root, "package.json"),
59
+ JSON.stringify({
60
+ name: "@serverkgg/platform",
61
+ devDependencies: {
62
+ "@serverkgg/bridge": "file:../serverk/modules/bridge",
63
+ },
64
+ }),
65
+ );
66
+
67
+ await expect(validate()).rejects.toThrow("pin a published version");
68
+ });
69
+
70
+ test("accepts a published bridge dependency and moves on to the manifest", async () => {
71
+ writePackage({
72
+ "@serverkgg/bridge": "0.0.3",
73
+ });
74
+
75
+ await expect(validate()).rejects.toThrow("missing serverk.yml");
76
+ });
77
+
78
+ test("ignores a package without a bridge dependency", async () => {
79
+ writePackage({
80
+ zod: "4.4.3",
81
+ });
82
+
83
+ await expect(validate()).rejects.toThrow("missing serverk.yml");
84
+ });
85
+
86
+ test("ignores a package.json that is absent", async () => {
87
+ await expect(validate()).rejects.toThrow("missing serverk.yml");
88
+ });
89
+ });
@@ -14,6 +14,7 @@ import {
14
14
  readPackageDocument,
15
15
  } from "../manifest";
16
16
  import type { BridgeDriver } from "../modules";
17
+ import { isBridgeEventName } from "../protocol";
17
18
  import { validateDriver } from "../runtime/bridgeDriver";
18
19
  import { directorySource } from "./directorySource";
19
20
 
@@ -24,6 +25,64 @@ options:
24
25
  --driver <path> driver module (default: <dir>/src/driver.ts), game packages only
25
26
  --help show this help`;
26
27
 
28
+ const BRIDGE_PACKAGE = "@serverkgg/bridge";
29
+
30
+ const LOCAL_SPECS = [
31
+ "file:",
32
+ "link:",
33
+ ];
34
+
35
+ const localBridgeSpec = (source: PackageSource) => {
36
+ const bytes = source.read("package.json");
37
+
38
+ if (!bytes) {
39
+ return null;
40
+ }
41
+
42
+ let parsed: unknown;
43
+
44
+ try {
45
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
46
+ } catch {
47
+ return null;
48
+ }
49
+
50
+ if (typeof parsed !== "object" || parsed === null) {
51
+ return null;
52
+ }
53
+
54
+ const groups: unknown[] = [
55
+ "dependencies" in parsed ? parsed.dependencies : null,
56
+ "devDependencies" in parsed ? parsed.devDependencies : null,
57
+ ];
58
+
59
+ for (const group of groups) {
60
+ if (typeof group !== "object" || group === null || !(BRIDGE_PACKAGE in group)) {
61
+ continue;
62
+ }
63
+
64
+ const spec: unknown = group[BRIDGE_PACKAGE];
65
+
66
+ if (typeof spec === "string" && LOCAL_SPECS.some((prefix) => spec.startsWith(prefix))) {
67
+ return spec;
68
+ }
69
+ }
70
+
71
+ return null;
72
+ };
73
+
74
+ const assertRegistryBridge = (source: PackageSource) => {
75
+ const spec = localBridgeSpec(source);
76
+
77
+ if (!spec) {
78
+ return;
79
+ }
80
+
81
+ throw new Error(
82
+ `package.json depends on ${BRIDGE_PACKAGE} via "${spec}" — pin a published version and use "serverk-bridge link" for local development`,
83
+ );
84
+ };
85
+
27
86
  const invalidManifest = (error: unknown): never => {
28
87
  if (error instanceof z.ZodError) {
29
88
  throw new Error(`${MANIFEST_FILE} is invalid\n${z.prettifyError(error)}`);
@@ -90,6 +149,43 @@ const validateGuidePackage = (source: PackageSource, document: GuidePackage) =>
90
149
  reportGuides(pack);
91
150
  };
92
151
 
152
+ const ACTION_TARGET_BUDGET = 80;
153
+
154
+ const reportEvents = (driver: BridgeDriver) => {
155
+ const unknown = [
156
+ ...new Set([
157
+ ...(driver.events?.patterns ?? []).map((pattern) => pattern.emit),
158
+ ...(driver.events?.emits ?? []),
159
+ ]),
160
+ ].filter((name) => !isBridgeEventName(name));
161
+
162
+ if (unknown.length === 0) {
163
+ return;
164
+ }
165
+
166
+ console.log(
167
+ ` warning: ${unknown.join(", ")} — outside the platform event taxonomy, the platform drops what it does not know`,
168
+ );
169
+ };
170
+
171
+ const reportActionTargets = (driver: BridgeDriver) => {
172
+ for (const tab of driver.panel?.tabs ?? []) {
173
+ for (const section of tab.sections) {
174
+ const actions = "actions" in section ? (section.actions ?? []) : [];
175
+
176
+ for (const action of actions) {
177
+ const target = `${tab.id}.${section.id}:${action.id}`;
178
+
179
+ if (target.length > ACTION_TARGET_BUDGET) {
180
+ console.log(
181
+ ` warning: "${target}" is ${target.length} characters — keep it under ${ACTION_TARGET_BUDGET} so it fits a discord custom id`,
182
+ );
183
+ }
184
+ }
185
+ }
186
+ }
187
+ };
188
+
93
189
  const validateGamePackage = async (source: PackageSource, manifest: Manifest, dir: string, driverArg?: string) => {
94
190
  const assets = readAssets(source, manifest);
95
191
  const guideFiles = manifest.guides.flatMap((guide) => [
@@ -130,6 +226,8 @@ const validateGamePackage = async (source: PackageSource, manifest: Manifest, di
130
226
  console.log(
131
227
  ` driver: ${Object.keys(driver.modules ?? {}).length} panel module(s), ${driver.panel?.tabs.length ?? 0} tab(s)`,
132
228
  );
229
+ reportEvents(driver);
230
+ reportActionTargets(driver);
133
231
  };
134
232
 
135
233
  export const runValidate = async (args: string[]) => {
@@ -159,6 +257,8 @@ export const runValidate = async (args: string[]) => {
159
257
  const dir = resolve(values.dir);
160
258
  const source = directorySource(dir);
161
259
 
260
+ assertRegistryBridge(source);
261
+
162
262
  let document: Manifest | GuidePackage;
163
263
 
164
264
  try {
@@ -21,6 +21,12 @@ export interface BridgeDownloadOptions {
21
21
  cache?: boolean;
22
22
  }
23
23
 
24
+ export interface BridgeExtractOptions {
25
+ extensions?: string[];
26
+ tree?: boolean;
27
+ select?: string;
28
+ }
29
+
24
30
  export interface BridgeFiles {
25
31
  read(path: string): Promise<string>;
26
32
  write(path: string, content: string): Promise<void>;
@@ -31,7 +37,7 @@ export interface BridgeFiles {
31
37
  exists(path: string): Promise<boolean>;
32
38
  size(path: string): Promise<number>;
33
39
  download(path: string, url: string, options?: BridgeDownloadOptions): Promise<void>;
34
- extract(path: string, destination: string, extensions?: string[]): Promise<string[]>;
40
+ extract(path: string, destination: string, options?: BridgeExtractOptions): Promise<string[]>;
35
41
  }
36
42
 
37
43
  export interface BridgeCodecOptions {
@@ -1,4 +1,5 @@
1
1
  export enum BridgeKind {
2
+ Actions = "actions",
2
3
  Announce = "announce",
3
4
  Backup = "backup",
4
5
  Catalog = "catalog",
@@ -1,6 +1,6 @@
1
1
  import type { BridgeContext, BridgeExecResult, BridgeFileEntry, BridgeReply } from "../context";
2
2
  import type { BridgeOption, BridgeRow, BridgeValue, BridgeValues, LocalizedText } from "../core";
3
- import type { BridgeField, BridgePanel, BridgeSection, BridgeTab } from "../panel";
3
+ import type { BridgeField, BridgePanel, BridgeSection, BridgeSectionAction, BridgeTab } from "../panel";
4
4
  import type {
5
5
  BridgeCatalogEntry,
6
6
  BridgeCatalogFacet,
@@ -12,6 +12,7 @@ import type {
12
12
  import type { BridgeTerminalArg, BridgeTerminalCommand, BridgeTerminalRule } from "../terminal";
13
13
  import type { BridgeDriver } from "./bridgeDriver";
14
14
  import type {
15
+ BridgeActionsModule,
15
16
  BridgeAnnounceModule,
16
17
  BridgeBackupModule,
17
18
  BridgeCatalogModule,
@@ -24,11 +25,13 @@ import type {
24
25
  BridgeQueryModule,
25
26
  BridgeRowAction,
26
27
  BridgeSample,
28
+ BridgeServerAction,
27
29
  BridgeSettingsModule,
28
30
  BridgeTerminalModule,
29
31
  } from "./bridgeModule";
30
32
 
31
33
  export namespace Bridge {
34
+ export type Actions = BridgeActionsModule;
32
35
  export type Announce = BridgeAnnounceModule;
33
36
  export type Backup = BridgeBackupModule;
34
37
  export type Catalog = BridgeCatalogModule;
@@ -57,6 +60,8 @@ export namespace Bridge {
57
60
  export type RowAction<Row extends BridgeRow = BridgeRow> = BridgeRowAction<Row>;
58
61
  export type Sample = BridgeSample;
59
62
  export type Section = BridgeSection;
63
+ export type SectionAction = BridgeSectionAction;
64
+ export type ServerAction = BridgeServerAction;
60
65
  export type Settings = BridgeSettingsModule;
61
66
  export type Terminal = BridgeTerminalModule;
62
67
  export type TerminalArg = BridgeTerminalArg;
@@ -1,5 +1,5 @@
1
1
  import type { BridgeContext } from "../context";
2
- import type { BridgeKind, BridgeOption, BridgeRow, BridgeStream, BridgeValues, LocalizedText } from "../core";
2
+ import type { BridgeKind, BridgeOption, BridgeRow, BridgeStream, BridgeValues } from "../core";
3
3
  import type { BridgeCatalogEntry, BridgeCatalogPage, BridgeCatalogQuery } from "../protocol/bridgeCatalog";
4
4
  import type { BridgeDescribeResult } from "../protocol/bridgeFrame";
5
5
  import type { BridgeTerminalCommand, BridgeTerminalRule } from "../terminal";
@@ -23,7 +23,7 @@ export interface BridgeLifecycleModule {
23
23
 
24
24
  export interface BridgeAnnounceModule {
25
25
  kind: BridgeKind.Announce;
26
- announce(context: BridgeContext, message: LocalizedText): Promise<void>;
26
+ announce(context: BridgeContext, message: string): Promise<void>;
27
27
  }
28
28
 
29
29
  export interface BridgeBackupModule {
@@ -53,6 +53,7 @@ export interface BridgeEventPattern {
53
53
  export interface BridgeEventsModule {
54
54
  kind: BridgeKind.Events;
55
55
  patterns: BridgeEventPattern[];
56
+ emits?: string[];
56
57
  }
57
58
 
58
59
  export interface BridgeTerminalModule {
@@ -74,7 +75,19 @@ export interface BridgeSettingsModule {
74
75
  write?(context: BridgeContext, values: BridgeValues): Promise<void>;
75
76
  }
76
77
 
77
- export type BridgeRowAction<Row extends BridgeRow = BridgeRow> = (context: BridgeContext, row: Row) => Promise<void>;
78
+ export type BridgeRowAction<Row extends BridgeRow = BridgeRow> = (
79
+ context: BridgeContext,
80
+ row: Row,
81
+ args: BridgeValues,
82
+ ) => Promise<void>;
83
+
84
+ export type BridgeServerAction = (context: BridgeContext, args: BridgeValues) => Promise<void>;
85
+
86
+ export interface BridgeActionsModule {
87
+ kind: BridgeKind.Actions;
88
+ requiresRunning?: boolean;
89
+ actions: Record<string, BridgeServerAction>;
90
+ }
78
91
 
79
92
  export interface BridgeCollectionModule<Row extends BridgeRow = BridgeRow> {
80
93
  kind: BridgeKind.Collection;
@@ -96,6 +109,7 @@ export interface BridgeCatalogModule {
96
109
  }
97
110
 
98
111
  export type BridgePanelModule =
112
+ | BridgeActionsModule
99
113
  | BridgeCatalogModule
100
114
  | BridgeCollectionModule
101
115
  | BridgeOptionsModule
@@ -1,4 +1,5 @@
1
1
  export enum BridgeLayout {
2
+ Actions = "actions",
2
3
  Cards = "cards",
3
4
  Catalog = "catalog",
4
5
  Form = "form",
@@ -8,7 +8,8 @@ import type { BridgeLayout } from "./bridgeLayout";
8
8
 
9
9
  export interface BridgeVisibleWhen {
10
10
  variable: string;
11
- values: string[];
11
+ values?: string[];
12
+ empty?: boolean;
12
13
  }
13
14
 
14
15
  export interface BridgeOptionsRef {
@@ -34,6 +35,9 @@ export interface BridgeSectionAction {
34
35
  id: string;
35
36
  label: LocalizedText;
36
37
  confirm?: BridgeConfirm;
38
+ confirmText?: LocalizedText;
39
+ download?: boolean;
40
+ fields?: BridgeField[];
37
41
  }
38
42
 
39
43
  export interface BridgeSectionAdd {
@@ -41,6 +45,23 @@ export interface BridgeSectionAdd {
41
45
  placeholder?: string;
42
46
  }
43
47
 
48
+ export interface BridgeSectionUpload {
49
+ label: LocalizedText;
50
+ extensions: string[];
51
+ staging: string;
52
+ }
53
+
54
+ export interface BridgeActionsSection {
55
+ layout: BridgeLayout.Actions;
56
+ id: string;
57
+ title?: LocalizedText;
58
+ help?: LocalizedText;
59
+ module: string;
60
+ actions: BridgeSectionAction[];
61
+ visibleWhen?: BridgeVisibleWhen;
62
+ access?: BridgeAccess;
63
+ }
64
+
44
65
  export interface BridgeFormSection {
45
66
  layout: BridgeLayout.Form;
46
67
  id: string;
@@ -66,8 +87,10 @@ export interface BridgeTableSection {
66
87
  id: string;
67
88
  title?: LocalizedText;
68
89
  module: string;
90
+ restartHint?: boolean;
69
91
  columns: BridgeColumn[];
70
92
  add?: BridgeSectionAdd;
93
+ upload?: BridgeSectionUpload;
71
94
  actions?: BridgeSectionAction[];
72
95
  empty?: LocalizedText;
73
96
  visibleWhen?: BridgeVisibleWhen;
@@ -93,12 +116,21 @@ export interface BridgeCatalogSection {
93
116
  title?: LocalizedText;
94
117
  module: string;
95
118
  restartHint?: boolean;
119
+ reinstall?: boolean;
120
+ variable?: string;
121
+ confirm?: BridgeConfirm;
122
+ confirmText?: LocalizedText;
96
123
  empty?: LocalizedText;
97
124
  visibleWhen?: BridgeVisibleWhen;
98
125
  access?: BridgeAccess;
99
126
  }
100
127
 
101
- export type BridgeSection = BridgeCardsSection | BridgeCatalogSection | BridgeFormSection | BridgeTableSection;
128
+ export type BridgeSection =
129
+ | BridgeActionsSection
130
+ | BridgeCardsSection
131
+ | BridgeCatalogSection
132
+ | BridgeFormSection
133
+ | BridgeTableSection;
102
134
 
103
135
  export interface BridgeTab {
104
136
  id: string;
@@ -45,6 +45,7 @@ export interface BridgeCatalogEntry {
45
45
  stale: boolean;
46
46
  pageUrl: string | null;
47
47
  icon: string | null;
48
+ variables?: Record<string, string> | null;
48
49
  }
49
50
 
50
51
  export interface BridgeCatalogQuery {
@@ -0,0 +1,20 @@
1
+ export enum BridgeEventName {
2
+ GameModeChanged = "GameModeChanged",
3
+ MissingDependency = "MissingDependency",
4
+ PlayerJoined = "PlayerJoined",
5
+ PlayerLeft = "PlayerLeft",
6
+ PortBindFailed = "PortBindFailed",
7
+ ServerCrashed = "ServerCrashed",
8
+ ServerStarted = "ServerStarted",
9
+ ServerStopping = "ServerStopping",
10
+ TickLagging = "TickLagging",
11
+ WorldCorrupt = "WorldCorrupt",
12
+ WorldSaved = "WorldSaved",
13
+ WrongJavaVersion = "WrongJavaVersion",
14
+ }
15
+
16
+ export const BRIDGE_EVENT_NAMES: readonly string[] = Object.values(BridgeEventName);
17
+
18
+ export const isBridgeEventName = (name: string): boolean => {
19
+ return BRIDGE_EVENT_NAMES.includes(name);
20
+ };
@@ -1,4 +1,4 @@
1
- import type { BridgeDesired, BridgeRuntime, BridgeValues, LocalizedText } from "../core";
1
+ import type { BridgeDesired, BridgeRuntime, BridgeValues } from "../core";
2
2
 
3
3
  export const BRIDGE_SOCKET_NAME = "bridge.sock";
4
4
 
@@ -38,7 +38,7 @@ export interface BridgeRequest {
38
38
  action?: string;
39
39
  args?: BridgeValues;
40
40
  cursor?: number;
41
- message?: LocalizedText;
41
+ message?: string;
42
42
  }
43
43
 
44
44
  export interface BridgeCommandResult {
@@ -0,0 +1,41 @@
1
+ export const BRIDGE_PANEL_TABS_LIMIT = 8;
2
+
3
+ export const BRIDGE_TAB_SECTIONS_LIMIT = 8;
4
+
5
+ export const BRIDGE_FORM_FIELDS_LIMIT = 32;
6
+
7
+ export const BRIDGE_TABLE_COLUMNS_LIMIT = 8;
8
+
9
+ export const BRIDGE_ROW_ACTIONS_LIMIT = 8;
10
+
11
+ export const BRIDGE_SECTION_ACTIONS_LIMIT = 10;
12
+
13
+ export const BRIDGE_ACTION_FIELDS_LIMIT = 5;
14
+
15
+ export const BRIDGE_FIELD_OPTIONS_LIMIT = 64;
16
+
17
+ export const BRIDGE_VISIBLE_VALUES_LIMIT = 16;
18
+
19
+ export const BRIDGE_UPLOAD_EXTENSIONS_LIMIT = 8;
20
+
21
+ export const BRIDGE_MODULES_LIMIT = 32;
22
+
23
+ export const BRIDGE_MODULE_ACTIONS_LIMIT = 16;
24
+
25
+ export const BRIDGE_DEPENDS_ON_LIMIT = 8;
26
+
27
+ export const BRIDGE_EVENTS_LIMIT = 32;
28
+
29
+ export const BRIDGE_ID_LENGTH_LIMIT = 32;
30
+
31
+ export const BRIDGE_KEY_LENGTH_LIMIT = 64;
32
+
33
+ export const BRIDGE_TEXT_LENGTH_LIMIT = 512;
34
+
35
+ export const BRIDGE_OPTION_VALUE_LENGTH_LIMIT = 128;
36
+
37
+ export const BRIDGE_PLACEHOLDER_LENGTH_LIMIT = 256;
38
+
39
+ export const BRIDGE_STAGING_LENGTH_LIMIT = 128;
40
+
41
+ export const BRIDGE_EXTENSION_LENGTH_LIMIT = 16;
@@ -4,7 +4,9 @@ export * from "../terminal";
4
4
  export * from "./bridgeCatalog";
5
5
  export * from "./bridgeConfig";
6
6
  export * from "./bridgeEgress";
7
+ export * from "./bridgeEvents";
7
8
  export * from "./bridgeFrame";
9
+ export * from "./bridgeLimits";
8
10
  export * from "./bridgeLine";
9
11
  export * from "./bridgeManifest";
10
12
  export * from "./bridgeVersion";
@@ -0,0 +1,53 @@
1
+ import { BridgeAction, BridgeKind } from "../core";
2
+ import type { BridgePanelModule } from "../modules";
3
+
4
+ export const actionsOf = (module: BridgePanelModule): string[] => {
5
+ switch (module.kind) {
6
+ case BridgeKind.Actions: {
7
+ return Object.keys(module.actions);
8
+ }
9
+
10
+ case BridgeKind.Options: {
11
+ return [
12
+ BridgeAction.List,
13
+ ];
14
+ }
15
+
16
+ case BridgeKind.Settings: {
17
+ return [
18
+ BridgeAction.Read,
19
+ ...(module.write
20
+ ? [
21
+ BridgeAction.Write,
22
+ ]
23
+ : []),
24
+ ];
25
+ }
26
+
27
+ case BridgeKind.Collection: {
28
+ return [
29
+ BridgeAction.List,
30
+ ...(module.add
31
+ ? [
32
+ BridgeAction.Add,
33
+ ]
34
+ : []),
35
+ ...Object.keys(module.actions ?? {}),
36
+ ];
37
+ }
38
+
39
+ case BridgeKind.Catalog: {
40
+ return [
41
+ BridgeAction.Install,
42
+ BridgeAction.Installed,
43
+ BridgeAction.Remove,
44
+ BridgeAction.Search,
45
+ ...(module.toggle
46
+ ? [
47
+ BridgeAction.Toggle,
48
+ ]
49
+ : []),
50
+ ];
51
+ }
52
+ }
53
+ };