@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.
package/README.md CHANGED
@@ -41,7 +41,7 @@ bunx serverk-bridge validate
41
41
  bunx serverk-bridge compile
42
42
  ```
43
43
 
44
- `validate` checks `serverk.yml` against the real platform schema, verifies the logo and banner, and validates the driver's panel wiring. `compile` produces `dist/bridge-amd64` and `dist/bridge-arm64`.
44
+ `validate` checks `serverk.yml` against the real platform schema, verifies the logo and banner, and validates the driver's panel wiring. `compile` produces `dist/bridge`, an amd64 binary — serverk publishes game images for `linux/amd64` only.
45
45
 
46
46
  ## Compatibility
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Serverk game bridge — driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -1,14 +1,10 @@
1
1
  import { parseArgs } from "node:util";
2
2
 
3
- const TARGETS: Record<string, string> = {
4
- amd64: "bun-linux-x64",
5
- arm64: "bun-linux-arm64",
6
- };
3
+ const TARGET = "bun-linux-x64";
7
4
 
8
5
  const HELP = `serverk-bridge compile [options]
9
6
 
10
7
  options:
11
- --arch <amd64|arm64> architecture to compile (repeatable, default: both)
12
8
  --entry <path> entrypoint (default: src/main.ts)
13
9
  --outdir <path> output directory (default: dist)
14
10
  --help show this help`;
@@ -17,10 +13,6 @@ export const runCompile = async (args: string[]) => {
17
13
  const { values } = parseArgs({
18
14
  args,
19
15
  options: {
20
- arch: {
21
- type: "string",
22
- multiple: true,
23
- },
24
16
  entry: {
25
17
  type: "string",
26
18
  default: "src/main.ts",
@@ -42,34 +34,24 @@ export const runCompile = async (args: string[]) => {
42
34
  return;
43
35
  }
44
36
 
45
- const arches = values.arch?.length ? values.arch : Object.keys(TARGETS);
46
-
47
- for (const arch of arches) {
48
- const target = TARGETS[arch];
49
-
50
- if (!target) {
51
- throw new Error(`unknown arch "${arch}" — expected amd64 or arm64`);
52
- }
53
-
54
- const outfile = `${values.outdir}/bridge-${arch}`;
55
- const child = Bun.spawn(
56
- [
57
- "bun",
58
- "build",
59
- "--compile",
60
- `--target=${target}`,
61
- values.entry,
62
- "--outfile",
63
- outfile,
64
- ],
65
- {
66
- stderr: "inherit",
67
- stdout: "inherit",
68
- },
69
- );
37
+ const outfile = `${values.outdir}/bridge`;
38
+ const child = Bun.spawn(
39
+ [
40
+ "bun",
41
+ "build",
42
+ "--compile",
43
+ `--target=${TARGET}`,
44
+ values.entry,
45
+ "--outfile",
46
+ outfile,
47
+ ],
48
+ {
49
+ stderr: "inherit",
50
+ stdout: "inherit",
51
+ },
52
+ );
70
53
 
71
- if ((await child.exited) !== 0) {
72
- throw new Error(`compile failed for ${arch}`);
73
- }
54
+ if ((await child.exited) !== 0) {
55
+ throw new Error("compile failed");
74
56
  }
75
57
  };
@@ -0,0 +1,199 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ realpathSync,
8
+ rmSync,
9
+ symlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { checkBridge, linkBridge, unlinkBridge } from "./link";
15
+
16
+ const SOURCE = "../serverk/modules/bridge";
17
+
18
+ describe("link", () => {
19
+ let root = "";
20
+ let cwd = "";
21
+ let source = "";
22
+ let target = "";
23
+
24
+ const writePackage = (dir: string, name: string, version: string) => {
25
+ mkdirSync(dir, {
26
+ recursive: true,
27
+ });
28
+ writeFileSync(
29
+ join(dir, "package.json"),
30
+ JSON.stringify({
31
+ name,
32
+ version,
33
+ }),
34
+ );
35
+ };
36
+
37
+ const link = (env: Record<string, string | undefined> = {}) => {
38
+ return linkBridge({
39
+ cwd,
40
+ source: SOURCE,
41
+ env,
42
+ });
43
+ };
44
+
45
+ beforeEach(() => {
46
+ root = realpathSync(mkdtempSync(`${tmpdir()}/serverk-link-`));
47
+ cwd = join(root, "game");
48
+ source = join(root, "serverk/modules/bridge");
49
+ target = join(cwd, "node_modules/@serverkgg/bridge");
50
+
51
+ mkdirSync(cwd, {
52
+ recursive: true,
53
+ });
54
+ });
55
+
56
+ afterEach(() => {
57
+ rmSync(root, {
58
+ force: true,
59
+ recursive: true,
60
+ });
61
+ });
62
+
63
+ test("skips when CI is set", () => {
64
+ writePackage(source, "@serverkgg/bridge", "0.0.3");
65
+ writePackage(target, "@serverkgg/bridge", "0.0.2");
66
+
67
+ expect(
68
+ link({
69
+ CI: "true",
70
+ }),
71
+ ).toMatchObject({
72
+ kind: "skipped",
73
+ reason: "ci",
74
+ });
75
+
76
+ expect(lstatSync(target).isSymbolicLink()).toBe(false);
77
+ });
78
+
79
+ test("skips when the local checkout is missing", () => {
80
+ expect(link()).toMatchObject({
81
+ kind: "skipped",
82
+ reason: "source-missing",
83
+ });
84
+ });
85
+
86
+ test("skips when the local checkout is another package", () => {
87
+ writePackage(source, "@serverkgg/minecraft", "0.0.0");
88
+
89
+ expect(link()).toMatchObject({
90
+ kind: "skipped",
91
+ reason: "source-mismatch",
92
+ });
93
+ });
94
+
95
+ test("replaces the registry directory with a symlink", () => {
96
+ writePackage(source, "@serverkgg/bridge", "0.0.3");
97
+ writePackage(target, "@serverkgg/bridge", "0.0.2");
98
+ writeFileSync(join(target, "installed.txt"), "registry");
99
+
100
+ expect(link()).toEqual({
101
+ kind: "linked",
102
+ source,
103
+ version: "0.0.3",
104
+ });
105
+
106
+ expect(lstatSync(target).isSymbolicLink()).toBe(true);
107
+ expect(realpathSync(target)).toBe(source);
108
+ expect(existsSync(join(target, "installed.txt"))).toBe(false);
109
+ expect(existsSync(join(source, "package.json"))).toBe(true);
110
+ });
111
+
112
+ test("creates the scope directory when nothing is installed", () => {
113
+ writePackage(source, "@serverkgg/bridge", "0.0.3");
114
+
115
+ expect(link()).toMatchObject({
116
+ kind: "linked",
117
+ });
118
+
119
+ expect(realpathSync(target)).toBe(source);
120
+ });
121
+
122
+ test("is idempotent once linked", () => {
123
+ writePackage(source, "@serverkgg/bridge", "0.0.3");
124
+
125
+ expect(link()).toMatchObject({
126
+ kind: "linked",
127
+ });
128
+
129
+ expect(link()).toEqual({
130
+ kind: "already-linked",
131
+ source,
132
+ });
133
+
134
+ expect(realpathSync(target)).toBe(source);
135
+ });
136
+
137
+ test("relinks a symlink pointing somewhere else", () => {
138
+ const other = join(root, "other");
139
+
140
+ writePackage(source, "@serverkgg/bridge", "0.0.3");
141
+ writePackage(other, "@serverkgg/bridge", "0.0.1");
142
+ mkdirSync(join(cwd, "node_modules/@serverkgg"), {
143
+ recursive: true,
144
+ });
145
+ symlinkSync(other, target, "dir");
146
+
147
+ expect(link()).toMatchObject({
148
+ kind: "linked",
149
+ version: "0.0.3",
150
+ });
151
+
152
+ expect(realpathSync(target)).toBe(source);
153
+ expect(existsSync(join(other, "package.json"))).toBe(true);
154
+ });
155
+
156
+ test("unlink removes the symlink and keeps the checkout", () => {
157
+ writePackage(source, "@serverkgg/bridge", "0.0.3");
158
+ link();
159
+
160
+ expect(unlinkBridge(cwd)).toEqual({
161
+ kind: "unlinked",
162
+ source,
163
+ });
164
+
165
+ expect(existsSync(target)).toBe(false);
166
+ expect(existsSync(join(source, "package.json"))).toBe(true);
167
+ });
168
+
169
+ test("unlink leaves a registry install alone", () => {
170
+ writePackage(target, "@serverkgg/bridge", "0.0.2");
171
+
172
+ expect(unlinkBridge(cwd)).toEqual({
173
+ kind: "not-linked",
174
+ });
175
+
176
+ expect(existsSync(join(target, "package.json"))).toBe(true);
177
+ });
178
+
179
+ test("check reports every state", () => {
180
+ expect(checkBridge(cwd)).toEqual({
181
+ kind: "missing",
182
+ });
183
+
184
+ writePackage(target, "@serverkgg/bridge", "0.0.2");
185
+
186
+ expect(checkBridge(cwd)).toEqual({
187
+ kind: "registry",
188
+ version: "0.0.2",
189
+ });
190
+
191
+ writePackage(source, "@serverkgg/bridge", "0.0.3");
192
+ link();
193
+
194
+ expect(checkBridge(cwd)).toEqual({
195
+ kind: "linked",
196
+ source,
197
+ });
198
+ });
199
+ });
@@ -0,0 +1,319 @@
1
+ import { lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+
5
+ const PACKAGE_NAME = "@serverkgg/bridge";
6
+
7
+ const DEFAULT_SOURCE = "../serverk/modules/bridge";
8
+
9
+ const INSTALL_PATH = `node_modules/${PACKAGE_NAME}`;
10
+
11
+ const HELP = `serverk-bridge link [options]
12
+
13
+ point ${INSTALL_PATH} at a local bridge checkout without touching package.json or bun.lock.
14
+ does nothing when CI is set or the checkout is absent, so the registry package is used everywhere else.
15
+
16
+ options:
17
+ --source <dir> local bridge checkout (default: ${DEFAULT_SOURCE})
18
+ --check print which bridge is installed, change nothing
19
+ --unlink drop the link so the next install restores the registry package
20
+ --help show this help`;
21
+
22
+ interface PackageFile {
23
+ name: string;
24
+ version: string;
25
+ }
26
+
27
+ interface LinkInput {
28
+ cwd: string;
29
+ source: string;
30
+ env: Record<string, string | undefined>;
31
+ }
32
+
33
+ export type LinkSkip = "ci" | "source-missing" | "source-mismatch";
34
+
35
+ export type LinkResult =
36
+ | {
37
+ kind: "skipped";
38
+ reason: LinkSkip;
39
+ source: string;
40
+ }
41
+ | {
42
+ kind: "already-linked";
43
+ source: string;
44
+ }
45
+ | {
46
+ kind: "linked";
47
+ source: string;
48
+ version: string;
49
+ };
50
+
51
+ export type CheckResult =
52
+ | {
53
+ kind: "linked";
54
+ source: string;
55
+ }
56
+ | {
57
+ kind: "registry";
58
+ version: string;
59
+ }
60
+ | {
61
+ kind: "missing";
62
+ };
63
+
64
+ export type UnlinkResult =
65
+ | {
66
+ kind: "unlinked";
67
+ source: string;
68
+ }
69
+ | {
70
+ kind: "not-linked";
71
+ };
72
+
73
+ const readPackageFile = (dir: string): PackageFile | null => {
74
+ let text: string;
75
+
76
+ try {
77
+ text = readFileSync(join(dir, "package.json"), "utf8");
78
+ } catch {
79
+ return null;
80
+ }
81
+
82
+ let parsed: unknown;
83
+
84
+ try {
85
+ parsed = JSON.parse(text);
86
+ } catch {
87
+ return null;
88
+ }
89
+
90
+ if (typeof parsed !== "object" || parsed === null || !("name" in parsed) || !("version" in parsed)) {
91
+ return null;
92
+ }
93
+
94
+ const { name, version } = parsed;
95
+
96
+ if (typeof name !== "string" || typeof version !== "string") {
97
+ return null;
98
+ }
99
+
100
+ return {
101
+ name,
102
+ version,
103
+ };
104
+ };
105
+
106
+ const linkedSource = (target: string): string | null => {
107
+ try {
108
+ if (!lstatSync(target).isSymbolicLink()) {
109
+ return null;
110
+ }
111
+
112
+ return realpathSync(target);
113
+ } catch {
114
+ return null;
115
+ }
116
+ };
117
+
118
+ const installPath = (cwd: string) => {
119
+ return resolve(cwd, INSTALL_PATH);
120
+ };
121
+
122
+ export const linkBridge = (input: LinkInput): LinkResult => {
123
+ const source = resolve(input.cwd, input.source);
124
+
125
+ if (input.env.CI) {
126
+ return {
127
+ kind: "skipped",
128
+ reason: "ci",
129
+ source,
130
+ };
131
+ }
132
+
133
+ const local = readPackageFile(source);
134
+
135
+ if (!local) {
136
+ return {
137
+ kind: "skipped",
138
+ reason: "source-missing",
139
+ source,
140
+ };
141
+ }
142
+
143
+ if (local.name !== PACKAGE_NAME) {
144
+ return {
145
+ kind: "skipped",
146
+ reason: "source-mismatch",
147
+ source,
148
+ };
149
+ }
150
+
151
+ const target = installPath(input.cwd);
152
+
153
+ if (linkedSource(target) === realpathSync(source)) {
154
+ return {
155
+ kind: "already-linked",
156
+ source,
157
+ };
158
+ }
159
+
160
+ rmSync(target, {
161
+ force: true,
162
+ recursive: true,
163
+ });
164
+ mkdirSync(dirname(target), {
165
+ recursive: true,
166
+ });
167
+ symlinkSync(source, target, "dir");
168
+
169
+ return {
170
+ kind: "linked",
171
+ source,
172
+ version: local.version,
173
+ };
174
+ };
175
+
176
+ export const checkBridge = (cwd: string): CheckResult => {
177
+ const target = installPath(cwd);
178
+ const source = linkedSource(target);
179
+
180
+ if (source) {
181
+ return {
182
+ kind: "linked",
183
+ source,
184
+ };
185
+ }
186
+
187
+ const installed = readPackageFile(target);
188
+
189
+ if (!installed) {
190
+ return {
191
+ kind: "missing",
192
+ };
193
+ }
194
+
195
+ return {
196
+ kind: "registry",
197
+ version: installed.version,
198
+ };
199
+ };
200
+
201
+ export const unlinkBridge = (cwd: string): UnlinkResult => {
202
+ const target = installPath(cwd);
203
+ const source = linkedSource(target);
204
+
205
+ if (!source) {
206
+ return {
207
+ kind: "not-linked",
208
+ };
209
+ }
210
+
211
+ rmSync(target, {
212
+ force: true,
213
+ });
214
+
215
+ return {
216
+ kind: "unlinked",
217
+ source,
218
+ };
219
+ };
220
+
221
+ const linkText = (result: LinkResult): string | null => {
222
+ switch (result.kind) {
223
+ case "skipped": {
224
+ return null;
225
+ }
226
+
227
+ case "already-linked": {
228
+ return `${PACKAGE_NAME} already linked -> ${result.source}`;
229
+ }
230
+
231
+ case "linked": {
232
+ return `${PACKAGE_NAME} linked -> ${result.source} (${result.version})`;
233
+ }
234
+ }
235
+ };
236
+
237
+ const checkText = (result: CheckResult): string => {
238
+ switch (result.kind) {
239
+ case "linked": {
240
+ return `${PACKAGE_NAME} linked -> ${result.source}`;
241
+ }
242
+
243
+ case "registry": {
244
+ return `${PACKAGE_NAME} registry ${result.version}`;
245
+ }
246
+
247
+ case "missing": {
248
+ return `${PACKAGE_NAME} missing`;
249
+ }
250
+ }
251
+ };
252
+
253
+ const unlinkText = (result: UnlinkResult): string => {
254
+ switch (result.kind) {
255
+ case "unlinked": {
256
+ return `${PACKAGE_NAME} unlinked — run "bun install" to restore the registry package`;
257
+ }
258
+
259
+ case "not-linked": {
260
+ return `${PACKAGE_NAME} is not linked`;
261
+ }
262
+ }
263
+ };
264
+
265
+ export const runLink = async (args: string[]) => {
266
+ const { values } = parseArgs({
267
+ args,
268
+ options: {
269
+ source: {
270
+ type: "string",
271
+ default: DEFAULT_SOURCE,
272
+ },
273
+ check: {
274
+ type: "boolean",
275
+ default: false,
276
+ },
277
+ unlink: {
278
+ type: "boolean",
279
+ default: false,
280
+ },
281
+ help: {
282
+ type: "boolean",
283
+ default: false,
284
+ },
285
+ },
286
+ });
287
+
288
+ if (values.help) {
289
+ console.log(HELP);
290
+
291
+ return;
292
+ }
293
+
294
+ const cwd = process.cwd();
295
+
296
+ if (values.check) {
297
+ console.log(checkText(checkBridge(cwd)));
298
+
299
+ return;
300
+ }
301
+
302
+ if (values.unlink) {
303
+ console.log(unlinkText(unlinkBridge(cwd)));
304
+
305
+ return;
306
+ }
307
+
308
+ const message = linkText(
309
+ linkBridge({
310
+ cwd,
311
+ source: values.source,
312
+ env: process.env,
313
+ }),
314
+ );
315
+
316
+ if (message) {
317
+ console.log(message);
318
+ }
319
+ };
package/src/cli/main.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { errorText } from "../runtime/bridgeLog";
4
4
  import { runCompile } from "./compile";
5
+ import { runLink } from "./link";
5
6
  import { runSchema } from "./schema";
6
7
  import { runValidate } from "./validate";
7
8
 
@@ -10,7 +11,8 @@ const USAGE = `serverk-bridge <command>
10
11
  commands:
11
12
  validate validate serverk.yml, assets, and driver wiring
12
13
  schema write the serverk.yml JSON Schema for editor tooling
13
- compile compile the bridge binary for amd64 and arm64
14
+ compile compile the bridge binary for amd64
15
+ link use a local bridge checkout while developing
14
16
 
15
17
  run "serverk-bridge <command> --help" for options`;
16
18
 
@@ -36,6 +38,12 @@ try {
36
38
  break;
37
39
  }
38
40
 
41
+ case "link": {
42
+ await runLink(args);
43
+
44
+ break;
45
+ }
46
+
39
47
  case undefined:
40
48
  case "--help": {
41
49
  console.log(USAGE);