@serverkgg/bridge 0.0.9 → 0.0.11

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 (54) hide show
  1. package/package.json +1 -1
  2. package/src/cli/validate.test.ts +116 -1
  3. package/src/cli/validate.ts +50 -3
  4. package/src/context/bridgeContext.ts +8 -1
  5. package/src/core/bridgeKind.ts +1 -0
  6. package/src/core/bridgeProbeError.ts +16 -0
  7. package/src/core/bridgeSetup.ts +20 -0
  8. package/src/core/bridgeValue.test.ts +29 -0
  9. package/src/core/bridgeValue.ts +12 -0
  10. package/src/core/index.ts +2 -0
  11. package/src/guides/blocks.ts +8 -0
  12. package/src/guides/markdown.test.ts +18 -0
  13. package/src/guides/markdown.ts +7 -1
  14. package/src/manifest/guide.ts +1 -0
  15. package/src/manifest/manifest.test.ts +49 -2
  16. package/src/manifest/presence.ts +9 -1
  17. package/src/modules/bridge.ts +20 -0
  18. package/src/modules/bridgeDriver.ts +2 -0
  19. package/src/modules/bridgeModule.ts +7 -0
  20. package/src/panel/bridgeControl.ts +1 -0
  21. package/src/panel/bridgeDetail.ts +6 -5
  22. package/src/panel/bridgePanel.ts +2 -0
  23. package/src/probes/a2s.test.ts +71 -0
  24. package/src/probes/a2s.ts +38 -11
  25. package/src/protocol/bridgeFrame.ts +5 -0
  26. package/src/protocol/bridgeLimits.ts +10 -0
  27. package/src/protocol/bridgeManifest.ts +2 -0
  28. package/src/protocol/bridgeSetup.ts +97 -0
  29. package/src/protocol/index.ts +1 -0
  30. package/src/runtime/bridgeConfigReader.ts +12 -8
  31. package/src/runtime/bridgeContext.ts +28 -2
  32. package/src/runtime/bridgeDriver.test.ts +512 -2
  33. package/src/runtime/bridgeDriver.ts +219 -1
  34. package/src/runtime/bridgeEvents.test.ts +96 -0
  35. package/src/runtime/bridgeEvents.ts +4 -1
  36. package/src/runtime/bridgeRegistry.test.ts +38 -1
  37. package/src/runtime/bridgeRegistry.ts +14 -1
  38. package/src/runtime/bridgeSecrets.test.ts +121 -0
  39. package/src/runtime/bridgeSecrets.ts +43 -0
  40. package/src/runtime/bridgeServer.test.ts +262 -2
  41. package/src/runtime/bridgeServer.ts +17 -0
  42. package/src/runtime/bridgeSetup.test.ts +575 -0
  43. package/src/runtime/bridgeSetup.ts +303 -0
  44. package/src/runtime/bridgeSetupHandlers.test.ts +303 -0
  45. package/src/runtime/bridgeSetupHandlers.ts +71 -0
  46. package/src/runtime/bridgeSignal.test.ts +114 -0
  47. package/src/runtime/bridgeSignal.ts +31 -0
  48. package/src/runtime/bridgeSupervisor.test.ts +111 -0
  49. package/src/runtime/bridgeSupervisor.ts +37 -5
  50. package/src/runtime/serve.ts +19 -1
  51. package/src/utils/index.ts +1 -0
  52. package/src/utils/stripAnsi.ts +12 -0
  53. package/src/wire/panel.test.ts +155 -0
  54. package/src/wire/panel.ts +43 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -2,7 +2,15 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { runValidate } from "./validate";
5
+ import { BridgeKind, BridgeSetupStepKind } from "../core";
6
+ import { GuideOpenTab } from "../guides";
7
+ import type { BridgeSetupModule } from "../modules";
8
+ import { runValidate, setupReportLines } from "./validate";
9
+
10
+ const text = (value: string) => ({
11
+ ar: value,
12
+ en: value,
13
+ });
6
14
 
7
15
  describe("validate", () => {
8
16
  let root = "";
@@ -87,3 +95,110 @@ describe("validate", () => {
87
95
  await expect(validate()).rejects.toThrow("missing serverk.yml");
88
96
  });
89
97
  });
98
+
99
+ describe("the setup report", () => {
100
+ test("says none for a driver that declares no setup", () => {
101
+ expect(setupReportLines(undefined)).toEqual([
102
+ " setup: none",
103
+ ]);
104
+ });
105
+
106
+ test("names every step, its kind, whether it is required, and where it lands", () => {
107
+ const setup: BridgeSetupModule = {
108
+ kind: BridgeKind.Setup,
109
+ steps: [
110
+ {
111
+ kind: BridgeSetupStepKind.Form,
112
+ id: "license",
113
+ title: text("Licence"),
114
+ required: true,
115
+ tab: "license",
116
+ section: "license",
117
+ fields: [
118
+ "LICENSE_KEY",
119
+ ],
120
+ },
121
+ {
122
+ kind: BridgeSetupStepKind.Driver,
123
+ id: "verify",
124
+ title: text("Verify"),
125
+ required: true,
126
+ requiresRunning: true,
127
+ },
128
+ {
129
+ kind: BridgeSetupStepKind.Open,
130
+ id: "txadmin",
131
+ title: text("txAdmin"),
132
+ required: false,
133
+ target: {
134
+ tab: GuideOpenTab.Panel,
135
+ tabId: "license",
136
+ },
137
+ },
138
+ {
139
+ kind: BridgeSetupStepKind.Open,
140
+ id: "invite",
141
+ title: text("Invite"),
142
+ required: false,
143
+ target: {
144
+ tab: GuideOpenTab.Access,
145
+ },
146
+ },
147
+ ],
148
+ submit: async () => {
149
+ return;
150
+ },
151
+ };
152
+
153
+ expect(setupReportLines(setup)).toEqual([
154
+ " setup: 4 step(s)",
155
+ " license — form, required → license/license [LICENSE_KEY]",
156
+ " verify — driver, required → driver (needs the game running)",
157
+ ' txadmin — open, optional → panel tab "license"',
158
+ " invite — open, optional → access",
159
+ ]);
160
+ });
161
+
162
+ test("leaves a driver step that needs nothing running with no target", () => {
163
+ expect(
164
+ setupReportLines({
165
+ kind: BridgeKind.Setup,
166
+ steps: [
167
+ {
168
+ kind: BridgeSetupStepKind.Driver,
169
+ id: "sign-in",
170
+ title: text("Sign in"),
171
+ required: false,
172
+ },
173
+ ],
174
+ submit: async () => {
175
+ return;
176
+ },
177
+ }),
178
+ ).toEqual([
179
+ " setup: 1 step(s)",
180
+ " sign-in — driver, optional → driver",
181
+ ]);
182
+ });
183
+
184
+ test("reads an omitted required as required, the way the platform does", () => {
185
+ expect(
186
+ setupReportLines({
187
+ kind: BridgeKind.Setup,
188
+ steps: [
189
+ {
190
+ kind: BridgeSetupStepKind.Driver,
191
+ id: "sign-in",
192
+ title: text("Sign in"),
193
+ },
194
+ ],
195
+ submit: async () => {
196
+ return;
197
+ },
198
+ }),
199
+ ).toEqual([
200
+ " setup: 1 step(s)",
201
+ " sign-in — driver, required → driver",
202
+ ]);
203
+ });
204
+ });
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { parseArgs } from "node:util";
5
5
  import { z } from "zod";
6
- import { type GuidePack, readGuidePack, readGuidePackage } from "../guides";
6
+ import { GuideOpenTab, type GuideOpenTarget, type GuidePack, readGuidePack, readGuidePackage } from "../guides";
7
7
  import {
8
8
  declaredFiles,
9
9
  type GuidePackage,
@@ -13,8 +13,8 @@ import {
13
13
  readAssets,
14
14
  readPackageDocument,
15
15
  } from "../manifest";
16
- import type { BridgeDriver } from "../modules";
17
- import { isBridgeEventName } from "../protocol";
16
+ import type { BridgeDriver, BridgeSetupModule } from "../modules";
17
+ import { type BridgeSetupStep, BridgeSetupStepKind, isBridgeEventName } from "../protocol";
18
18
  import { validateDriver, validatePortActivation } from "../runtime/bridgeDriver";
19
19
  import { directorySource } from "./directorySource";
20
20
 
@@ -186,6 +186,49 @@ const reportActionTargets = (driver: BridgeDriver) => {
186
186
  }
187
187
  };
188
188
 
189
+ const openTargetOf = (target: GuideOpenTarget) => {
190
+ if (target.tab === GuideOpenTab.Panel) {
191
+ return `panel tab "${target.tabId}"`;
192
+ }
193
+
194
+ if (target.tab === GuideOpenTab.Files) {
195
+ return target.path ? `files at "${target.path}"` : "files";
196
+ }
197
+
198
+ return target.tab;
199
+ };
200
+
201
+ const setupTargetOf = (step: BridgeSetupStep) => {
202
+ if (step.kind === BridgeSetupStepKind.Form) {
203
+ const fields = step.fields ?? [];
204
+
205
+ return `→ ${step.tab}/${step.section}${fields.length === 0 ? "" : ` [${fields.join(", ")}]`}`;
206
+ }
207
+
208
+ if (step.kind === BridgeSetupStepKind.Open) {
209
+ return `→ ${openTargetOf(step.target)}`;
210
+ }
211
+
212
+ return step.requiresRunning ? "→ driver (needs the game running)" : "→ driver";
213
+ };
214
+
215
+ export const setupReportLines = (setup: BridgeSetupModule | undefined): string[] => {
216
+ if (!setup) {
217
+ return [
218
+ " setup: none",
219
+ ];
220
+ }
221
+
222
+ return [
223
+ ` setup: ${setup.steps.length} step(s)`,
224
+ ...setup.steps.map((step) => {
225
+ const target = setupTargetOf(step);
226
+
227
+ return ` ${step.id} — ${step.kind}, ${step.required === false ? "optional" : "required"}${target === "" ? "" : ` ${target}`}`;
228
+ }),
229
+ ];
230
+ };
231
+
189
232
  const validateGamePackage = async (source: PackageSource, manifest: Manifest, dir: string, driverArg?: string) => {
190
233
  const assets = readAssets(source, manifest);
191
234
  const guideFiles = manifest.guides.flatMap((guide) => [
@@ -230,6 +273,10 @@ const validateGamePackage = async (source: PackageSource, manifest: Manifest, di
230
273
  ` driver: ${Object.keys(driver.modules ?? {}).length} panel module(s), ${driver.panel?.tabs.length ?? 0} tab(s)`,
231
274
  );
232
275
  console.log(` pending: ${driver.pending ? `declared (${driver.pending.extensions.length} extension(s))` : "none"}`);
276
+ for (const line of setupReportLines(driver.setup)) {
277
+ console.log(line);
278
+ }
279
+
233
280
  reportEvents(driver);
234
281
  reportActionTargets(driver);
235
282
  };
@@ -1,5 +1,5 @@
1
1
  import type { BridgeStream, BridgeValues } from "../core";
2
- import type { BridgeHttpMethod } from "../protocol";
2
+ import type { BridgeHttpMethod, BridgeSetupRuntime } from "../protocol";
3
3
 
4
4
  export interface BridgeServer {
5
5
  code: string;
@@ -121,6 +121,12 @@ export interface BridgeExecResult {
121
121
  export interface BridgeLogs {
122
122
  tail(lines: number): Promise<string[]>;
123
123
  watch(pattern: RegExp, timeoutMs: number): Promise<RegExpMatchArray | null>;
124
+ follow(pattern: RegExp, onMatch: (match: RegExpMatchArray) => void): () => void;
125
+ }
126
+
127
+ export interface BridgeSetupReporter {
128
+ report(runtime: BridgeSetupRuntime): void;
129
+ clear(): void;
124
130
  }
125
131
 
126
132
  export interface BridgeLog {
@@ -141,6 +147,7 @@ export interface BridgeContext {
141
147
  command(input: string, options?: BridgeCommandOptions): Promise<BridgeReply>;
142
148
  exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
143
149
  logs: BridgeLogs;
150
+ setup: BridgeSetupReporter;
144
151
  emit(event: string, payload?: BridgeValues): void;
145
152
  log: BridgeLog;
146
153
  }
@@ -14,4 +14,5 @@ export enum BridgeKind {
14
14
  Preview = "preview",
15
15
  Query = "query",
16
16
  Settings = "settings",
17
+ Setup = "setup",
17
18
  }
@@ -0,0 +1,16 @@
1
+ export enum BridgeProbeCode {
2
+ Unreachable = "unreachable",
3
+ TimedOut = "timed-out",
4
+ Malformed = "malformed",
5
+ }
6
+
7
+ export class BridgeProbeError extends Error {
8
+ readonly code: BridgeProbeCode;
9
+
10
+ constructor(code: BridgeProbeCode, message: string) {
11
+ super(message);
12
+
13
+ this.name = "BridgeProbeError";
14
+ this.code = code;
15
+ }
16
+ }
@@ -0,0 +1,20 @@
1
+ export enum BridgeSetupStepKind {
2
+ Driver = "driver",
3
+ Form = "form",
4
+ Open = "open",
5
+ }
6
+
7
+ export enum BridgeSetupStepState {
8
+ Pending = "pending",
9
+ Active = "active",
10
+ Done = "done",
11
+ Failed = "failed",
12
+ }
13
+
14
+ export enum BridgeSetupPromptKind {
15
+ Wait = "wait",
16
+ Progress = "progress",
17
+ Link = "link",
18
+ Action = "action",
19
+ Failed = "failed",
20
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { isLocalizedText } from "./bridgeValue";
3
+
4
+ describe("isLocalizedText", () => {
5
+ test("recognises the shape a driver authors copy in", () => {
6
+ expect(
7
+ isLocalizedText({
8
+ ar: "التركيب",
9
+ en: "Setup",
10
+ }),
11
+ ).toBe(true);
12
+ });
13
+
14
+ test("leaves every other bridge value alone", () => {
15
+ expect(isLocalizedText("Hytale")).toBe(false);
16
+ expect(isLocalizedText(12)).toBe(false);
17
+ expect(isLocalizedText(true)).toBe(false);
18
+ expect(isLocalizedText(null)).toBe(false);
19
+ expect(isLocalizedText(undefined)).toBe(false);
20
+ });
21
+
22
+ test("refuses a half-written text rather than rendering an empty language", () => {
23
+ expect(
24
+ isLocalizedText({
25
+ en: "Setup",
26
+ }),
27
+ ).toBe(false);
28
+ });
29
+ });
@@ -11,6 +11,18 @@ export interface LocalizedText {
11
11
  en: string;
12
12
  }
13
13
 
14
+ export type BridgeCopy = LocalizedText | string;
15
+
16
+ export const isLocalizedText = (value: unknown): value is LocalizedText => {
17
+ if (typeof value !== "object" || value === null) {
18
+ return false;
19
+ }
20
+
21
+ const text = value as Partial<LocalizedText>;
22
+
23
+ return typeof text.ar === "string" && typeof text.en === "string";
24
+ };
25
+
14
26
  export interface BridgeOption {
15
27
  value: string;
16
28
  label: LocalizedText;
package/src/core/index.ts CHANGED
@@ -6,8 +6,10 @@ export * from "./bridgeId";
6
6
  export * from "./bridgeKind";
7
7
  export * from "./bridgeNetError";
8
8
  export * from "./bridgePort";
9
+ export * from "./bridgeProbeError";
9
10
  export * from "./bridgeRuntime";
10
11
  export * from "./bridgeSecretError";
12
+ export * from "./bridgeSetup";
11
13
  export * from "./bridgeStream";
12
14
  export * from "./bridgeUserError";
13
15
  export * from "./bridgeValue";
@@ -13,6 +13,8 @@ export enum GuideOpenTab {
13
13
  Console = "console",
14
14
  Files = "files",
15
15
  Backups = "backups",
16
+ Access = "access",
17
+ Setup = "setup",
16
18
  Panel = "panel",
17
19
  }
18
20
 
@@ -51,6 +53,12 @@ export const guideOpenTargetSchema = z.discriminatedUnion("tab", [
51
53
  z.object({
52
54
  tab: z.literal(GuideOpenTab.Backups),
53
55
  }),
56
+ z.object({
57
+ tab: z.literal(GuideOpenTab.Access),
58
+ }),
59
+ z.object({
60
+ tab: z.literal(GuideOpenTab.Setup),
61
+ }),
54
62
  z.object({
55
63
  tab: z.literal(GuideOpenTab.Files),
56
64
  path: zGuideFilePath.nullable(),
@@ -132,6 +132,8 @@ describe("directives", () => {
132
132
  "@[open](backups)",
133
133
  "@[open](files:server.properties)",
134
134
  "@[open](files)",
135
+ "@[open](access)",
136
+ "@[open](setup)",
135
137
  "@[open](panel:mods)",
136
138
  ].join("\n"),
137
139
  );
@@ -163,6 +165,18 @@ describe("directives", () => {
163
165
  path: null,
164
166
  },
165
167
  },
168
+ {
169
+ type: "open",
170
+ target: {
171
+ tab: GuideOpenTab.Access,
172
+ },
173
+ },
174
+ {
175
+ type: "open",
176
+ target: {
177
+ tab: GuideOpenTab.Setup,
178
+ },
179
+ },
166
180
  {
167
181
  type: "open",
168
182
  target: {
@@ -181,6 +195,10 @@ describe("directives", () => {
181
195
  expect(() => parse("@[open](nowhere)")).toThrow(/unknown open target "nowhere"/);
182
196
  });
183
197
 
198
+ test("rejects an argument on a target that takes none", () => {
199
+ expect(() => parse("@[open](setup:sign-in)")).toThrow(/"setup" target does not take an argument/);
200
+ });
201
+
184
202
  test("rejects a traversing file path", () => {
185
203
  expect(() => parse("@[open](files:../../etc/passwd)")).toThrow();
186
204
  });
@@ -312,7 +312,13 @@ const parseOpenTarget = (path: string, line: number, value: string): GuideOpenTa
312
312
  fail(path, line, `"${tab}" target does not take an argument`);
313
313
  }
314
314
 
315
- if (tab !== GuideOpenTab.Overview && tab !== GuideOpenTab.Console && tab !== GuideOpenTab.Backups) {
315
+ if (
316
+ tab !== GuideOpenTab.Overview
317
+ && tab !== GuideOpenTab.Console
318
+ && tab !== GuideOpenTab.Backups
319
+ && tab !== GuideOpenTab.Access
320
+ && tab !== GuideOpenTab.Setup
321
+ ) {
316
322
  fail(path, line, `unknown open target "${value}"`);
317
323
  }
318
324
 
@@ -16,6 +16,7 @@ export enum GuideSurface {
16
16
  Files = "files",
17
17
  Backups = "backups",
18
18
  Panel = "panel",
19
+ Setup = "setup",
19
20
  }
20
21
 
21
22
  export const guideTagSchema = z.object({
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { PortJoin } from "../core";
4
4
  import { GameCategory } from "./category";
5
5
  import { guidePackageSchema, MANIFEST_SCHEMA_VERSION, manifestSchema, packageSchema } from "./manifest";
6
+ import { isServerkAvatarTemplate, PRESENCE_AVATAR_LENGTH_LIMIT } from "./presence";
6
7
 
7
8
  const text = (value: string) => ({
8
9
  ar: value,
@@ -626,7 +627,21 @@ describe("presence", () => {
626
627
  expect(result.success && result.data.presence?.fields[0]?.inline).toBe(true);
627
628
  });
628
629
 
629
- test("refuses an avatar template that is not https", () => {
630
+ test("accepts a serverk avatar template", () => {
631
+ const result = manifestSchema.safeParse({
632
+ ...base(),
633
+ presence: {
634
+ name: "player",
635
+ id: "playerId",
636
+ avatar: "serverk://avatars/hytale/{playerId}",
637
+ },
638
+ });
639
+
640
+ expect(result.success).toBe(true);
641
+ expect(result.success && result.data.presence?.avatar).toBe("serverk://avatars/hytale/{playerId}");
642
+ });
643
+
644
+ test("refuses an avatar template that is neither https nor a serverk avatar", () => {
630
645
  const result = manifestSchema.safeParse({
631
646
  ...base(),
632
647
  presence: {
@@ -635,7 +650,39 @@ describe("presence", () => {
635
650
  });
636
651
 
637
652
  expect(result.success).toBe(false);
638
- expect(result.error && z.prettifyError(result.error)).toContain("must be an https url template");
653
+ expect(result.error && z.prettifyError(result.error)).toContain(
654
+ "must be an https or serverk://avatars/ url template",
655
+ );
656
+ });
657
+
658
+ test("refuses a serverk scheme that is not an avatar path", () => {
659
+ const result = manifestSchema.safeParse({
660
+ ...base(),
661
+ presence: {
662
+ avatar: "serverk://heads/hytale/{playerId}",
663
+ },
664
+ });
665
+
666
+ expect(result.success).toBe(false);
667
+ expect(result.error && z.prettifyError(result.error)).toContain(
668
+ "must be an https or serverk://avatars/ url template",
669
+ );
670
+ });
671
+
672
+ test("refuses a serverk avatar template longer than the limit", () => {
673
+ const result = manifestSchema.safeParse({
674
+ ...base(),
675
+ presence: {
676
+ avatar: `serverk://avatars/hytale/${"a".repeat(PRESENCE_AVATAR_LENGTH_LIMIT)}`,
677
+ },
678
+ });
679
+
680
+ expect(result.success).toBe(false);
681
+ });
682
+
683
+ test("tells a serverk avatar template apart from an https one", () => {
684
+ expect(isServerkAvatarTemplate("serverk://avatars/hytale/{playerId}")).toBe(true);
685
+ expect(isServerkAvatarTemplate("https://mc-heads.net/avatar/{skin}/128")).toBe(false);
639
686
  });
640
687
 
641
688
  test("refuses a payload key that could never be a capture group", () => {
@@ -6,6 +6,14 @@ export const PRESENCE_FIELDS_LIMIT = 6;
6
6
 
7
7
  export const PRESENCE_AVATAR_LENGTH_LIMIT = 300;
8
8
 
9
+ export const SERVERK_AVATAR_PREFIX = "serverk://avatars/";
10
+
11
+ export const PRESENCE_AVATAR_PATTERN = /^(https:\/\/|serverk:\/\/avatars\/)/;
12
+
13
+ export const isServerkAvatarTemplate = (template: string): boolean => {
14
+ return template.startsWith(SERVERK_AVATAR_PREFIX);
15
+ };
16
+
9
17
  export const manifestPresenceFieldSchema = z.object({
10
18
  key: zPayloadKey,
11
19
  label: localizedText(32),
@@ -20,7 +28,7 @@ export const manifestPresenceSchema = z.object({
20
28
  avatar: z
21
29
  .string()
22
30
  .max(PRESENCE_AVATAR_LENGTH_LIMIT)
23
- .regex(/^https:\/\//, "must be an https url template")
31
+ .regex(PRESENCE_AVATAR_PATTERN, "must be an https or serverk://avatars/ url template")
24
32
  .optional(),
25
33
  fields: z.array(manifestPresenceFieldSchema).max(PRESENCE_FIELDS_LIMIT).default([]),
26
34
  });
@@ -1,5 +1,6 @@
1
1
  import type { BridgeContext, BridgeExecResult, BridgeFileEntry, BridgeReply } from "../context";
2
2
  import type {
3
+ BridgeCopy,
3
4
  BridgeInstallHold,
4
5
  BridgeInstallOutcome,
5
6
  BridgeOption,
@@ -34,6 +35,15 @@ import type {
34
35
  } from "../protocol/bridgeCatalog";
35
36
  import type { BridgePendingFile } from "../protocol/bridgeFrame";
36
37
  import type { BridgePreview } from "../protocol/bridgePreview";
38
+ import type {
39
+ BridgeSetupAction,
40
+ BridgeSetupManifest,
41
+ BridgeSetupPrompt,
42
+ BridgeSetupRuntime,
43
+ BridgeSetupStep,
44
+ BridgeSetupStepStatus,
45
+ BridgeSetupSummary,
46
+ } from "../protocol/bridgeSetup";
37
47
  import type { BridgeTerminalArg, BridgeTerminalCommand, BridgeTerminalRule } from "../terminal";
38
48
  import type { BridgeDriver } from "./bridgeDriver";
39
49
  import type {
@@ -56,6 +66,7 @@ import type {
56
66
  BridgeSample,
57
67
  BridgeServerAction,
58
68
  BridgeSettingsModule,
69
+ BridgeSetupModule,
59
70
  BridgeTerminalModule,
60
71
  } from "./bridgeModule";
61
72
 
@@ -72,6 +83,7 @@ export namespace Bridge {
72
83
  export type CatalogQuery = BridgeCatalogQuery;
73
84
  export type Collection<Row extends BridgeRow = BridgeRow> = BridgeCollectionModule<Row>;
74
85
  export type Context = BridgeContext;
86
+ export type Copy = BridgeCopy;
75
87
  export type Detail = BridgeDetailModule;
76
88
  export type DetailAction = BridgeDetailAction;
77
89
  export type DetailBadge = BridgeDetailBadge;
@@ -106,6 +118,14 @@ export namespace Bridge {
106
118
  export type SectionAction = BridgeSectionAction;
107
119
  export type ServerAction = BridgeServerAction;
108
120
  export type Settings = BridgeSettingsModule;
121
+ export type Setup = BridgeSetupModule;
122
+ export type SetupAction = BridgeSetupAction;
123
+ export type SetupManifest = BridgeSetupManifest;
124
+ export type SetupPrompt = BridgeSetupPrompt;
125
+ export type SetupRuntime = BridgeSetupRuntime;
126
+ export type SetupStep = BridgeSetupStep;
127
+ export type SetupStepStatus = BridgeSetupStepStatus;
128
+ export type SetupSummary = BridgeSetupSummary;
109
129
  export type Terminal = BridgeTerminalModule;
110
130
  export type TerminalArg = BridgeTerminalArg;
111
131
  export type TerminalCommand = BridgeTerminalCommand;
@@ -8,6 +8,7 @@ import type {
8
8
  BridgePanelModule,
9
9
  BridgePendingModule,
10
10
  BridgeQueryModule,
11
+ BridgeSetupModule,
11
12
  BridgeTerminalModule,
12
13
  } from "./bridgeModule";
13
14
 
@@ -19,6 +20,7 @@ export interface BridgeDriver {
19
20
  backup?: BridgeBackupModule;
20
21
  announce?: BridgeAnnounceModule;
21
22
  pending?: BridgePendingModule;
23
+ setup?: BridgeSetupModule;
22
24
  terminal?: BridgeTerminalModule;
23
25
  panel?: BridgePanel;
24
26
  modules?: Record<string, BridgePanelModule>;
@@ -4,6 +4,7 @@ import type { BridgeDetail, BridgeDetailOutcome } from "../panel";
4
4
  import type { BridgeCatalogEntry, BridgeCatalogPage, BridgeCatalogQuery } from "../protocol/bridgeCatalog";
5
5
  import type { BridgeDescribeResult, BridgePendingFile } from "../protocol/bridgeFrame";
6
6
  import type { BridgePreview } from "../protocol/bridgePreview";
7
+ import type { BridgeSetupStep } from "../protocol/bridgeSetup";
7
8
  import type { BridgeTerminalCommand, BridgeTerminalRule } from "../terminal";
8
9
 
9
10
  export type BridgeDescription = Omit<BridgeDescribeResult, "supported">;
@@ -43,6 +44,12 @@ export interface BridgePendingModule {
43
44
  add(context: BridgeContext, input: string): Promise<void>;
44
45
  }
45
46
 
47
+ export interface BridgeSetupModule {
48
+ kind: BridgeKind.Setup;
49
+ steps: BridgeSetupStep[];
50
+ submit?(context: BridgeContext, step: string, action: string, args: BridgeValues): Promise<void>;
51
+ }
52
+
46
53
  export interface BridgeSample {
47
54
  online: number | null;
48
55
  max: number | null;
@@ -1,6 +1,7 @@
1
1
  export enum BridgeControl {
2
2
  Boolean = "boolean",
3
3
  Number = "number",
4
+ Secret = "secret",
4
5
  Select = "select",
5
6
  Slider = "slider",
6
7
  Text = "text",
@@ -1,4 +1,4 @@
1
- import type { BridgeValue, LocalizedText } from "../core";
1
+ import type { BridgeCopy, BridgeValue, LocalizedText } from "../core";
2
2
 
3
3
  export enum BridgeDetailTone {
4
4
  Neutral = "neutral",
@@ -11,6 +11,7 @@ export enum BridgeDetailFormat {
11
11
  Bytes = "bytes",
12
12
  Date = "date",
13
13
  Number = "number",
14
+ Secret = "secret",
14
15
  Text = "text",
15
16
  }
16
17
 
@@ -22,7 +23,7 @@ export interface BridgeDetailBadge {
22
23
  export interface BridgeDetailStat {
23
24
  key: string;
24
25
  label: LocalizedText;
25
- value: BridgeValue;
26
+ value: BridgeValue | LocalizedText;
26
27
  format: BridgeDetailFormat;
27
28
  }
28
29
 
@@ -33,9 +34,9 @@ export interface BridgeDetailLink {
33
34
 
34
35
  export interface BridgeDetail {
35
36
  id: string;
36
- title: string;
37
- subtitle: string | null;
38
- description: string | null;
37
+ title: BridgeCopy;
38
+ subtitle: BridgeCopy | null;
39
+ description: BridgeCopy | null;
39
40
  image: string | null;
40
41
  badges: BridgeDetailBadge[];
41
42
  stats: BridgeDetailStat[];
@@ -32,6 +32,8 @@ export interface BridgeField {
32
32
  max?: number;
33
33
  step?: number;
34
34
  maxLength?: number;
35
+ pattern?: string;
36
+ patternHint?: LocalizedText;
35
37
  options?: BridgeOption[] | BridgeOptionsRef;
36
38
  visibleWhen?: BridgeVisibleWhen;
37
39
  }