@serverkgg/bridge 0.1.11 → 0.1.13

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 (51) hide show
  1. package/package.json +1 -1
  2. package/rules/package.md +2 -1
  3. package/src/bot/botDeploy.test.ts +57 -0
  4. package/src/bot/botEnvironment.test.ts +21 -0
  5. package/src/bot/botEnvironment.ts +10 -0
  6. package/src/bot/botRun.test.ts +296 -0
  7. package/src/bot/botRun.ts +109 -0
  8. package/src/bot/botState.test.ts +34 -1
  9. package/src/bot/botState.ts +1 -0
  10. package/src/bot/botVariables.test.ts +22 -0
  11. package/src/bot/botVariables.ts +3 -0
  12. package/src/bot/detect/detect.test.ts +172 -0
  13. package/src/bot/detect/detect.ts +31 -0
  14. package/src/bot/detect/detectResult.ts +1 -0
  15. package/src/bot/detect/detectTable.test.ts +25 -2
  16. package/src/bot/detect/detectTable.ts +24 -1
  17. package/src/bot/detect/keys.test.ts +7 -0
  18. package/src/bot/detect/keys.ts +3 -0
  19. package/src/bot/detect/serverkJson.test.ts +21 -0
  20. package/src/bot/detect/serverkJson.ts +13 -1
  21. package/src/bot/index.ts +1 -0
  22. package/src/cli/link.ts +3 -3
  23. package/src/cli/main.ts +8 -0
  24. package/src/cli/readme.test.ts +470 -0
  25. package/src/cli/readme.ts +570 -0
  26. package/src/cli/validate.test.ts +41 -1
  27. package/src/cli/validate.ts +25 -0
  28. package/src/context/bridgeContext.ts +1 -0
  29. package/src/modules/bridgeModule.ts +1 -0
  30. package/src/protocol/bridgeManifest.ts +1 -0
  31. package/src/runtime/bridgeContext.test.ts +6 -0
  32. package/src/runtime/bridgeContext.ts +7 -2
  33. package/src/runtime/bridgeExec.test.ts +150 -0
  34. package/src/runtime/bridgeExec.ts +67 -19
  35. package/src/runtime/bridgeLineSpacing.test.ts +132 -0
  36. package/src/runtime/bridgeLineSpacing.ts +49 -0
  37. package/src/runtime/bridgeLog.ts +9 -0
  38. package/src/runtime/bridgeRedaction.test.ts +28 -2
  39. package/src/runtime/bridgeRedaction.ts +7 -1
  40. package/src/runtime/bridgeRegistry.test.ts +30 -0
  41. package/src/runtime/bridgeRegistry.ts +1 -0
  42. package/src/runtime/bridgeScopedContext.ts +3 -0
  43. package/src/runtime/bridgeSetupHandlers.test.ts +3 -0
  44. package/src/runtime/bridgeSupervisor.ts +7 -9
  45. package/src/runtime/serve.ts +9 -2
  46. package/src/steam/steamcmdNarrator.test.ts +18 -5
  47. package/src/steam/steamcmdNarrator.ts +26 -2
  48. package/src/wine/protonCommand.test.ts +17 -1
  49. package/src/wine/protonCommand.ts +7 -1
  50. package/src/wire/panel.test.ts +32 -0
  51. package/src/wire/panel.ts +1 -0
@@ -45,6 +45,9 @@ const contextOf = (): BridgeContext => {
45
45
 
46
46
  return createContext(settings, supervisor, {
47
47
  onLog: log,
48
+ onPrint() {
49
+ return;
50
+ },
48
51
  onEmit() {
49
52
  return;
50
53
  },
@@ -4,8 +4,9 @@ import { BridgeLifecycleSubject } from "../modules/bridgeLifecycleSubject";
4
4
  import { BRIDGE_STOP_SIGNALS, type BridgeStopSignal } from "../modules/bridgeStopSignal";
5
5
  import { BRIDGE_CRASH_LINE_LIMIT, BridgeCrashPhase, type BridgeCrashRecord } from "../protocol";
6
6
  import { stripAnsi } from "../utils";
7
+ import { createLineSpacing } from "./bridgeLineSpacing";
7
8
  import type { BridgeLeveledLogger } from "./bridgeLog";
8
- import { errorText } from "./bridgeLog";
9
+ import { errorText, print } from "./bridgeLog";
9
10
  import { settleUid, unprivilegedArgv } from "./bridgePrivilege";
10
11
  import { readProcessGroup, signalGroup } from "./bridgeSignal";
11
12
 
@@ -286,11 +287,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
286
287
  }
287
288
  : raw;
288
289
 
289
- if (entry.stream === BridgeStream.Stderr) {
290
- process.stderr.write(`${entry.line}\n`);
291
- } else {
292
- process.stdout.write(`${entry.line}\n`);
293
- }
290
+ print(entry.line, entry.stream);
294
291
 
295
292
  tail.push(entry);
296
293
 
@@ -306,6 +303,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
306
303
  const pump = async (stream: ReadableStream<Uint8Array>, kind: BridgeStream) => {
307
304
  const decoder = new TextDecoder();
308
305
  const reader = stream.getReader();
306
+ const spacing = createLineSpacing();
309
307
 
310
308
  let buffer = "";
311
309
 
@@ -325,7 +323,7 @@ export const createSupervisor = (options: SupervisorOptions) => {
325
323
 
326
324
  buffer = split.rest;
327
325
 
328
- for (const line of split.lines) {
326
+ for (const line of split.lines.flatMap(spacing)) {
329
327
  emit({
330
328
  stream: kind,
331
329
  line,
@@ -333,10 +331,10 @@ export const createSupervisor = (options: SupervisorOptions) => {
333
331
  }
334
332
  }
335
333
 
336
- if (buffer.length > 0) {
334
+ for (const line of spacing(buffer)) {
337
335
  emit({
338
336
  stream: kind,
339
- line: buffer,
337
+ line,
340
338
  });
341
339
  }
342
340
  };
@@ -19,9 +19,15 @@ import { validateDriver } from "./bridgeDriver";
19
19
  import { createEventRing } from "./bridgeEvents";
20
20
  import { decideInstall, recountHold } from "./bridgeInstall";
21
21
  import { createBridgeLiveConfig } from "./bridgeLiveConfig";
22
- import { errorDetail, errorText, log as stdoutLog } from "./bridgeLog";
22
+ import { errorDetail, errorText, print, log as stdoutLog } from "./bridgeLog";
23
23
  import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
24
- import { createBridgeRedaction, redactLocalized, redactLogger, secretFieldKeys } from "./bridgeRedaction";
24
+ import {
25
+ createBridgeRedaction,
26
+ redactLocalized,
27
+ redactLogger,
28
+ redactPrinter,
29
+ secretFieldKeys,
30
+ } from "./bridgeRedaction";
25
31
  import { createRegistry } from "./bridgeRegistry";
26
32
  import { createServer } from "./bridgeServer";
27
33
  import { setupSummaryOf } from "./bridgeSetup";
@@ -182,6 +188,7 @@ export const serve = async (driver: BridgeDriver) => {
182
188
  supervisor,
183
189
  {
184
190
  onLog: log,
191
+ onPrint: redactPrinter(print, redact),
185
192
  onEmit(event, payload) {
186
193
  ring.emit(event, payload);
187
194
  },
@@ -20,7 +20,7 @@ const recorder = () => {
20
20
  };
21
21
 
22
22
  describe("steamcmdNarrator", () => {
23
- it("reports download progress in ten percent steps", () => {
23
+ it("reports download progress in ten percent steps, each line with the percent steamcmd printed", () => {
24
24
  const { log, lines, fields } = recorder();
25
25
  const narrate = steamcmdNarrator(log, "palworld");
26
26
 
@@ -36,10 +36,10 @@ describe("steamcmdNarrator", () => {
36
36
  }
37
37
 
38
38
  expect(lines).toEqual([
39
- "downloading palworld",
40
- "downloading palworld",
41
- "downloading palworld",
42
- "downloading palworld",
39
+ "downloading palworld 2%",
40
+ "downloading palworld 11%",
41
+ "downloading palworld 25%",
42
+ "downloading palworld 99%",
43
43
  ]);
44
44
  expect(fields).toEqual([
45
45
  {
@@ -74,6 +74,19 @@ describe("steamcmdNarrator", () => {
74
74
  ]);
75
75
  });
76
76
 
77
+ it("says how much of the download is done when steamcmd prints the byte counts", () => {
78
+ const { log, lines } = recorder();
79
+ const narrate = steamcmdNarrator(log, "palworld");
80
+
81
+ narrate("Update state (0x61) downloading, progress: 1.11 (127644881 / 11530459441)");
82
+ narrate("Update state (0x61) downloading, progress: 40.02 (4614180864 / 11530459441)");
83
+
84
+ expect(lines).toEqual([
85
+ "downloading palworld 1% (122 MB of 10.7 GB)",
86
+ "downloading palworld 40% (4.3 GB of 10.7 GB)",
87
+ ]);
88
+ });
89
+
77
90
  it("passes notable lines through with the ansi stripped", () => {
78
91
  const { log, lines } = recorder();
79
92
  const narrate = steamcmdNarrator(log, "rust");
@@ -1,12 +1,33 @@
1
1
  import type { BridgeLog } from "../context";
2
2
  import { stripAnsi } from "../utils";
3
3
 
4
- const PROGRESS = /Update state \(0x[0-9a-f]+\)\s*(?<phase>[^,]+),\s*progress:\s*(?<percent>\d+)\.\d+/i;
4
+ const PROGRESS =
5
+ /Update state \(0x[0-9a-f]+\)\s*(?<phase>[^,]+),\s*progress:\s*(?<percent>\d+)\.\d+(?:\s*\((?<done>\d+)\s*\/\s*(?<total>\d+)\))?/i;
6
+
7
+ const GIGABYTE = 1024 ** 3;
8
+
9
+ const MEGABYTE = 1024 ** 2;
5
10
 
6
11
  const PROGRESS_STEP = 10;
7
12
 
8
13
  const NOTABLE = /^(Error|Warning|Failed|Success|Logging in|Connecting|Update state \(0x\d+\) (?!downloading))/i;
9
14
 
15
+ const byteSizeOf = (bytes: number) => {
16
+ if (bytes >= GIGABYTE) {
17
+ return `${(bytes / GIGABYTE).toFixed(1)} GB`;
18
+ }
19
+
20
+ return `${Math.round(bytes / MEGABYTE)} MB`;
21
+ };
22
+
23
+ const progressLineOf = (phase: string, label: string, percent: number, done: number, total: number) => {
24
+ if (total < MEGABYTE) {
25
+ return `${phase} ${label} ${percent}%`;
26
+ }
27
+
28
+ return `${phase} ${label} ${percent}% (${byteSizeOf(done)} of ${byteSizeOf(total)})`;
29
+ };
30
+
10
31
  export const steamcmdNarrator = (log: BridgeLog, label: string) => {
11
32
  let phase = "";
12
33
  let reported = -1;
@@ -28,7 +49,10 @@ export const steamcmdNarrator = (log: BridgeLog, label: string) => {
28
49
  if (step > reported) {
29
50
  reported = step;
30
51
 
31
- log(`${phase} ${label}`, {
52
+ const done = Number(progress.groups.done ?? 0);
53
+ const total = Number(progress.groups.total ?? 0);
54
+
55
+ log(progressLineOf(phase, label, percent, done, total), {
32
56
  percent: step,
33
57
  });
34
58
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import type { BridgeContext } from "../context";
3
- import { protonCommand, protonCompatPath } from "./protonCommand";
3
+ import { ProtonVerb, protonCommand, protonCompatPath } from "./protonCommand";
4
4
 
5
5
  const ensured: string[] = [];
6
6
 
@@ -41,6 +41,22 @@ describe("protonCommand", () => {
41
41
  ]);
42
42
  });
43
43
 
44
+ it("runs straight in the prefix when asked, so the game keeps the container's stdout", async () => {
45
+ expect(
46
+ (
47
+ await protonCommand(context, {
48
+ appId: "2394010",
49
+ exe: "Pal/Binaries/Win64/PalServer-Win64-Shipping-Cmd.exe",
50
+ verb: ProtonVerb.RunInPrefix,
51
+ })
52
+ ).slice(-3),
53
+ ).toEqual([
54
+ "/opt/proton/proton",
55
+ "runinprefix",
56
+ "Pal/Binaries/Win64/PalServer-Win64-Shipping-Cmd.exe",
57
+ ]);
58
+ });
59
+
44
60
  it("ensures the compat and steam directories", async () => {
45
61
  ensured.length = 0;
46
62
 
@@ -7,6 +7,11 @@ export const PROTON_COMPAT_DIRECTORY = ".proton";
7
7
 
8
8
  export const STEAM_COMPAT_CLIENT_DIRECTORY = ".steam";
9
9
 
10
+ export enum ProtonVerb {
11
+ Run = "run",
12
+ RunInPrefix = "runinprefix",
13
+ }
14
+
10
15
  export interface ProtonCommandOptions {
11
16
  appId: string;
12
17
  exe: string;
@@ -16,6 +21,7 @@ export interface ProtonCommandOptions {
16
21
  steamDir?: string;
17
22
  home?: string;
18
23
  environment?: Record<string, string>;
24
+ verb?: ProtonVerb;
19
25
  }
20
26
 
21
27
  export const protonCompatPath = (appId: string, compatDir: string = PROTON_COMPAT_DIRECTORY) => {
@@ -42,7 +48,7 @@ export const protonCommand = async (context: BridgeContext, options: ProtonComma
42
48
  "env",
43
49
  ...Object.entries(environment).map(([key, value]) => `${key}=${value}`),
44
50
  `${protonDir}/proton`,
45
- "run",
51
+ options.verb ?? ProtonVerb.Run,
46
52
  options.exe,
47
53
  ...(options.args ?? []),
48
54
  ];
@@ -13,6 +13,7 @@ import {
13
13
  BridgePlace,
14
14
  type BridgeSetupManifest,
15
15
  BridgeSetupStepKind,
16
+ type BridgeTerminalManifest,
16
17
  BridgeUploadMode,
17
18
  } from "@serverkgg/bridge/protocol";
18
19
  import { bridgeAvailabilitySchema, bridgeCellsSchema, bridgeManifestSchema } from "./panel";
@@ -783,3 +784,34 @@ describe("the availability schema", () => {
783
784
  expect(parsed.modules[0]?.actions).toContain(BridgeAction.Availability);
784
785
  });
785
786
  });
787
+
788
+ const terminalManifest = (shell: boolean): BridgeManifest => ({
789
+ version: "1.0.0",
790
+ panel: null,
791
+ terminal: {
792
+ commands: [],
793
+ rules: [],
794
+ shell,
795
+ },
796
+ setup: null,
797
+ modules: [],
798
+ events: [],
799
+ });
800
+
801
+ describe("the terminal mirror", () => {
802
+ it("carries the shell flag through unchanged", () => {
803
+ expect(bridgeManifestSchema.parse(terminalManifest(true)).terminal?.shell).toBe(true);
804
+ });
805
+
806
+ it("defaults a terminal from an older bridge that carries no shell flag to false", () => {
807
+ const older = terminalManifest(true);
808
+
809
+ delete (older.terminal as Partial<BridgeTerminalManifest>).shell;
810
+
811
+ expect(bridgeManifestSchema.parse(older).terminal).toEqual({
812
+ commands: [],
813
+ rules: [],
814
+ shell: false,
815
+ });
816
+ });
817
+ });
package/src/wire/panel.ts CHANGED
@@ -318,6 +318,7 @@ export const bridgeTerminalRuleSchema: z.ZodType<BridgeTerminalRuleManifest> = z
318
318
  export const bridgeTerminalManifestSchema: z.ZodType<BridgeTerminalManifest> = z.object({
319
319
  commands: z.array(bridgeTerminalCommandSchema).max(128),
320
320
  rules: z.array(bridgeTerminalRuleSchema).max(64),
321
+ shell: z.boolean().default(false),
321
322
  });
322
323
 
323
324
  const setupStepBase = {