@serverkgg/bridge 0.0.1 → 0.0.2

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 (59) hide show
  1. package/package.json +10 -3
  2. package/src/cli/main.ts +11 -1
  3. package/src/cli/schema.ts +62 -0
  4. package/src/cli/validate.ts +119 -36
  5. package/src/context/bridgeContext.ts +11 -1
  6. package/src/core/bridgeAction.ts +13 -0
  7. package/src/core/bridgeFailure.ts +16 -0
  8. package/src/core/bridgeKind.ts +2 -0
  9. package/src/core/bridgeNetError.ts +10 -0
  10. package/src/core/index.ts +2 -0
  11. package/src/guides/blocks.ts +167 -0
  12. package/src/guides/compile.ts +193 -0
  13. package/src/guides/compiled.ts +28 -0
  14. package/src/guides/index.ts +6 -0
  15. package/src/guides/markdown.test.ts +374 -0
  16. package/src/guides/markdown.ts +769 -0
  17. package/src/guides/variables.ts +52 -0
  18. package/src/guides/version.ts +1 -0
  19. package/src/main.ts +2 -1
  20. package/src/manifest/assets.ts +4 -4
  21. package/src/manifest/guide.ts +23 -18
  22. package/src/manifest/index.ts +3 -2
  23. package/src/manifest/jsonSchema.ts +16 -0
  24. package/src/manifest/localized.ts +7 -0
  25. package/src/manifest/manifest.test.ts +366 -0
  26. package/src/manifest/manifest.ts +354 -120
  27. package/src/manifest/primitives.ts +63 -0
  28. package/src/manifest/source.ts +5 -4
  29. package/src/modules/bridge.ts +19 -3
  30. package/src/modules/bridgeDriver.ts +4 -0
  31. package/src/modules/bridgeModule.ts +22 -24
  32. package/src/panel/bridgeAccess.ts +3 -0
  33. package/src/panel/bridgePanel.ts +6 -0
  34. package/src/panel/index.ts +1 -0
  35. package/src/protocol/bridgeCatalog.ts +56 -0
  36. package/src/protocol/bridgeEgress.ts +53 -0
  37. package/src/protocol/bridgeFrame.ts +17 -1
  38. package/src/protocol/bridgeLine.ts +41 -6
  39. package/src/protocol/bridgeManifest.ts +15 -1
  40. package/src/protocol/bridgeVersion.ts +1 -1
  41. package/src/protocol/index.ts +3 -0
  42. package/src/runtime/bridgeContext.ts +29 -9
  43. package/src/runtime/bridgeDriver.ts +1 -0
  44. package/src/runtime/bridgeEgress.ts +360 -0
  45. package/src/runtime/bridgeExec.ts +15 -5
  46. package/src/runtime/bridgeFiles.ts +58 -21
  47. package/src/runtime/bridgeLog.ts +26 -3
  48. package/src/runtime/bridgeNet.ts +75 -22
  49. package/src/runtime/bridgeRegistry.ts +51 -6
  50. package/src/runtime/bridgeServer.ts +48 -1
  51. package/src/runtime/bridgeSupervisor.ts +6 -6
  52. package/src/runtime/index.ts +0 -17
  53. package/src/runtime/serve.ts +44 -3
  54. package/src/terminal/bridgeTerminalArg.ts +11 -0
  55. package/src/terminal/bridgeTerminalCommand.ts +11 -0
  56. package/src/terminal/bridgeTerminalLevel.ts +6 -0
  57. package/src/terminal/bridgeTerminalRule.ts +8 -0
  58. package/src/terminal/index.ts +4 -0
  59. package/src/manifest/checks.ts +0 -50
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
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",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/serverkgg/serverk.git",
10
+ "directory": "modules/bridge"
11
+ },
7
12
  "type": "module",
8
13
  "bin": {
9
- "serverk-bridge": "./src/cli/main.ts"
14
+ "serverk-bridge": "src/cli/main.ts"
10
15
  },
11
16
  "files": [
12
17
  "src"
@@ -15,10 +20,12 @@
15
20
  ".": "./src/main.ts",
16
21
  "./protocol": "./src/protocol/index.ts",
17
22
  "./manifest": "./src/manifest/index.ts",
23
+ "./guides": "./src/guides/index.ts",
18
24
  "./package.json": "./package.json"
19
25
  },
20
26
  "scripts": {
21
- "tsc": "tsc"
27
+ "tsc": "tsc",
28
+ "test": "bun test"
22
29
  },
23
30
  "dependencies": {
24
31
  "zod": "4.4.3"
package/src/cli/main.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env bun
2
+
3
+ import { errorText } from "../runtime/bridgeLog";
2
4
  import { runCompile } from "./compile";
5
+ import { runSchema } from "./schema";
3
6
  import { runValidate } from "./validate";
4
7
 
5
8
  const USAGE = `serverk-bridge <command>
6
9
 
7
10
  commands:
8
11
  validate validate serverk.yml, assets, and driver wiring
12
+ schema write the serverk.yml JSON Schema for editor tooling
9
13
  compile compile the bridge binary for amd64 and arm64
10
14
 
11
15
  run "serverk-bridge <command> --help" for options`;
@@ -20,6 +24,12 @@ try {
20
24
  break;
21
25
  }
22
26
 
27
+ case "schema": {
28
+ await runSchema(args);
29
+
30
+ break;
31
+ }
32
+
23
33
  case "compile": {
24
34
  await runCompile(args);
25
35
 
@@ -39,6 +49,6 @@ try {
39
49
  }
40
50
  }
41
51
  } catch (error) {
42
- console.error(error instanceof Error ? error.message : String(error));
52
+ console.error(errorText(error));
43
53
  process.exit(1);
44
54
  }
@@ -0,0 +1,62 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { renderJsonSchema, SCHEMA_FILE } from "../manifest";
5
+
6
+ const HELP = `serverk-bridge schema [options]
7
+
8
+ options:
9
+ --out <path> file to write (default: ${SCHEMA_FILE})
10
+ --check fail when the file on disk is out of date instead of writing
11
+ --help show this help`;
12
+
13
+ export const runSchema = async (args: string[]) => {
14
+ const { values } = parseArgs({
15
+ args,
16
+ options: {
17
+ out: {
18
+ type: "string",
19
+ default: SCHEMA_FILE,
20
+ },
21
+ check: {
22
+ type: "boolean",
23
+ default: false,
24
+ },
25
+ help: {
26
+ type: "boolean",
27
+ default: false,
28
+ },
29
+ },
30
+ });
31
+
32
+ if (values.help) {
33
+ console.log(HELP);
34
+
35
+ return;
36
+ }
37
+
38
+ const target = resolve(values.out);
39
+ const rendered = renderJsonSchema();
40
+
41
+ if (values.check) {
42
+ let current: string;
43
+
44
+ try {
45
+ current = readFileSync(target, "utf8");
46
+ } catch {
47
+ throw new Error(`${values.out} is missing — run "serverk-bridge schema" and commit it`);
48
+ }
49
+
50
+ if (current !== rendered) {
51
+ throw new Error(`${values.out} is out of date — run "serverk-bridge schema" and commit it`);
52
+ }
53
+
54
+ console.log(`${values.out} is up to date`);
55
+
56
+ return;
57
+ }
58
+
59
+ writeFileSync(target, rendered);
60
+
61
+ console.log(`wrote ${values.out}`);
62
+ };
@@ -3,7 +3,16 @@ 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 { declaredFiles, type Manifest, readAssets, readPackage } from "../manifest";
6
+ import { type GuidePack, readGuidePack, readGuidePackage } from "../guides";
7
+ import {
8
+ declaredFiles,
9
+ type GuidePackage,
10
+ MANIFEST_FILE,
11
+ type Manifest,
12
+ type PackageSource,
13
+ readAssets,
14
+ readPackageDocument,
15
+ } from "../manifest";
7
16
  import type { BridgeDriver } from "../modules";
8
17
  import { validateDriver } from "../runtime/bridgeDriver";
9
18
  import { directorySource } from "./directorySource";
@@ -11,53 +20,81 @@ import { directorySource } from "./directorySource";
11
20
  const HELP = `serverk-bridge validate [options]
12
21
 
13
22
  options:
14
- --dir <path> game package directory (default: current directory)
15
- --driver <path> driver module (default: <dir>/src/driver.ts)
23
+ --dir <path> package directory (default: current directory)
24
+ --driver <path> driver module (default: <dir>/src/driver.ts), game packages only
16
25
  --help show this help`;
17
26
 
18
- export const runValidate = async (args: string[]) => {
19
- const { values } = parseArgs({
20
- args,
21
- options: {
22
- dir: {
23
- type: "string",
24
- default: ".",
25
- },
26
- driver: {
27
- type: "string",
28
- },
29
- help: {
30
- type: "boolean",
31
- default: false,
32
- },
33
- },
34
- });
27
+ const invalidManifest = (error: unknown): never => {
28
+ if (error instanceof z.ZodError) {
29
+ throw new Error(`${MANIFEST_FILE} is invalid\n${z.prettifyError(error)}`);
30
+ }
35
31
 
36
- if (values.help) {
37
- console.log(HELP);
32
+ throw error;
33
+ };
38
34
 
35
+ const DYNAMIC_BLOCKS: ReadonlySet<string> = new Set([
36
+ "field",
37
+ "command",
38
+ "open",
39
+ "conditional",
40
+ ]);
41
+
42
+ const countDynamic = (pack: GuidePack) => {
43
+ return pack.drafts.reduce((total, draft) => {
44
+ const blocks = [
45
+ ...draft.body.ar,
46
+ ...draft.body.en,
47
+ ];
48
+
49
+ return total + blocks.filter((block) => DYNAMIC_BLOCKS.has(block.type)).length;
50
+ }, 0);
51
+ };
52
+
53
+ const reportGuides = (pack: GuidePack | null) => {
54
+ if (!pack) {
39
55
  return;
40
56
  }
41
57
 
42
- const dir = resolve(values.dir);
43
- const source = directorySource(dir);
58
+ console.log(
59
+ ` guides: ${pack.drafts.length} guide(s), ${countDynamic(pack)} dynamic block(s), ${pack.images.length} image(s)`,
60
+ );
44
61
 
45
- let manifest: Manifest;
62
+ if (pack.commands.length === 0) {
63
+ return;
64
+ }
46
65
 
47
- try {
48
- manifest = readPackage(source).manifest;
49
- } catch (error) {
50
- if (error instanceof z.ZodError) {
51
- throw new Error(`serverk.yml is invalid\n${z.prettifyError(error)}`);
52
- }
66
+ console.log(` commands: ${pack.commands.length} runnable command(s) — review each one`);
67
+
68
+ for (const command of [
69
+ ...new Set(pack.commands),
70
+ ]) {
71
+ console.log(` ${command}`);
72
+ }
73
+ };
74
+
75
+ const validateGuidePackage = (source: PackageSource, document: GuidePackage) => {
76
+ const guideFiles = document.guides.flatMap((guide) => [
77
+ guide.bodyPath.ar,
78
+ guide.bodyPath.en,
79
+ ]);
53
80
 
54
- throw error;
81
+ for (const path of guideFiles) {
82
+ if (source.read(path) === null) {
83
+ throw new Error(`declared file "${path}" is missing`);
84
+ }
55
85
  }
56
86
 
87
+ const pack = readGuidePackage(source, document);
88
+
89
+ console.log(`${document.metadata.id} is valid`);
90
+ reportGuides(pack);
91
+ };
92
+
93
+ const validateGamePackage = async (source: PackageSource, manifest: Manifest, dir: string, driverArg?: string) => {
57
94
  const assets = readAssets(source, manifest);
58
95
  const guideFiles = manifest.guides.flatMap((guide) => [
59
- guide.body.ar,
60
- guide.body.en,
96
+ guide.bodyPath.ar,
97
+ guide.bodyPath.en,
61
98
  ]);
62
99
 
63
100
  for (const path of declaredFiles(manifest, guideFiles)) {
@@ -66,7 +103,8 @@ export const runValidate = async (args: string[]) => {
66
103
  }
67
104
  }
68
105
 
69
- const driverPath = values.driver ? resolve(values.driver) : join(dir, "src/driver.ts");
106
+ const pack = readGuidePack(source, manifest);
107
+ const driverPath = driverArg ? resolve(driverArg) : join(dir, "src/driver.ts");
70
108
 
71
109
  if (!existsSync(driverPath)) {
72
110
  throw new Error(`no driver module at ${driverPath} — export your BridgeDriver from src/driver.ts`);
@@ -83,12 +121,57 @@ export const runValidate = async (args: string[]) => {
83
121
 
84
122
  console.log(`${manifest.metadata.id} is valid`);
85
123
  console.log(
86
- ` manifest: ${manifest.runtime.ports.length} port(s), ${manifest.secrets.length} secret(s), ${manifest.guides.length} guide(s)`,
124
+ ` manifest: ${manifest.container.ports.length} port(s), ${manifest.secrets.length} secret(s), ${manifest.guides.length} guide(s)`,
87
125
  );
88
126
  console.log(
89
127
  ` assets: logo ${assets.logo.dimensions.width}x${assets.logo.dimensions.height}, banner ${assets.banner.dimensions.width}x${assets.banner.dimensions.height}`,
90
128
  );
129
+ reportGuides(pack);
91
130
  console.log(
92
131
  ` driver: ${Object.keys(driver.modules ?? {}).length} panel module(s), ${driver.panel?.tabs.length ?? 0} tab(s)`,
93
132
  );
94
133
  };
134
+
135
+ export const runValidate = async (args: string[]) => {
136
+ const { values } = parseArgs({
137
+ args,
138
+ options: {
139
+ dir: {
140
+ type: "string",
141
+ default: ".",
142
+ },
143
+ driver: {
144
+ type: "string",
145
+ },
146
+ help: {
147
+ type: "boolean",
148
+ default: false,
149
+ },
150
+ },
151
+ });
152
+
153
+ if (values.help) {
154
+ console.log(HELP);
155
+
156
+ return;
157
+ }
158
+
159
+ const dir = resolve(values.dir);
160
+ const source = directorySource(dir);
161
+
162
+ let document: Manifest | GuidePackage;
163
+
164
+ try {
165
+ document = readPackageDocument(source);
166
+ } catch (error) {
167
+ return invalidManifest(error);
168
+ }
169
+
170
+ if (document.kind === "GuidePackage") {
171
+ validateGuidePackage(source, document);
172
+
173
+ return;
174
+ }
175
+
176
+ await validateGamePackage(source, document, dir, values.driver);
177
+ };
@@ -18,6 +18,7 @@ export interface BridgeFileEntry {
18
18
  export interface BridgeDownloadOptions {
19
19
  digest?: string;
20
20
  sizeBytes?: number;
21
+ cache?: boolean;
21
22
  }
22
23
 
23
24
  export interface BridgeFiles {
@@ -52,6 +53,9 @@ export interface BridgeCodecs {
52
53
  export interface BridgeRequestOptions {
53
54
  headers?: Record<string, string>;
54
55
  timeoutMs?: number;
56
+ cache?: boolean;
57
+ cacheSeconds?: number;
58
+ refresh?: boolean;
55
59
  }
56
60
 
57
61
  export interface BridgeNet {
@@ -103,6 +107,12 @@ export interface BridgeLogs {
103
107
  watch(pattern: RegExp, timeoutMs: number): Promise<RegExpMatchArray | null>;
104
108
  }
105
109
 
110
+ export interface BridgeLog {
111
+ (message: string, fields?: BridgeValues): void;
112
+ warn(message: string, fields?: BridgeValues): void;
113
+ error(message: string, fields?: BridgeValues): void;
114
+ }
115
+
106
116
  export interface BridgeContext {
107
117
  readonly server: BridgeServer;
108
118
  variable(key: string): string | null;
@@ -116,5 +126,5 @@ export interface BridgeContext {
116
126
  exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
117
127
  logs: BridgeLogs;
118
128
  emit(event: string, payload?: BridgeValues): void;
119
- log(message: string, fields?: BridgeValues): void;
129
+ log: BridgeLog;
120
130
  }
@@ -9,5 +9,18 @@ export enum BridgeAction {
9
9
  Remove = "remove",
10
10
  Sample = "sample",
11
11
  Search = "search",
12
+ Toggle = "toggle",
12
13
  Write = "write",
13
14
  }
15
+
16
+ export const BRIDGE_READ_ACTIONS: readonly string[] = [
17
+ BridgeAction.Installed,
18
+ BridgeAction.List,
19
+ BridgeAction.Read,
20
+ BridgeAction.Sample,
21
+ BridgeAction.Search,
22
+ ];
23
+
24
+ export const isReadAction = (action: string): boolean => {
25
+ return BRIDGE_READ_ACTIONS.includes(action);
26
+ };
@@ -0,0 +1,16 @@
1
+ export enum BridgeFailureCode {
2
+ CatalogRateLimited = "catalog-rate-limited",
3
+ CatalogRestricted = "catalog-restricted",
4
+ NoCatalogVersionAvailable = "no-catalog-version-available",
5
+ }
6
+
7
+ export class BridgeFailureError extends Error {
8
+ readonly code: BridgeFailureCode;
9
+
10
+ constructor(code: BridgeFailureCode, message: string) {
11
+ super(message);
12
+
13
+ this.name = "BridgeFailureError";
14
+ this.code = code;
15
+ }
16
+ }
@@ -1,7 +1,9 @@
1
1
  export enum BridgeKind {
2
+ Announce = "announce",
2
3
  Backup = "backup",
3
4
  Catalog = "catalog",
4
5
  Collection = "collection",
6
+ Terminal = "terminal",
5
7
  Events = "events",
6
8
  Install = "install",
7
9
  Lifecycle = "lifecycle",
@@ -0,0 +1,10 @@
1
+ export class BridgeNetError extends Error {
2
+ readonly status: number | null;
3
+
4
+ constructor(status: number | null, message: string) {
5
+ super(message);
6
+
7
+ this.name = "BridgeNetError";
8
+ this.status = status;
9
+ }
10
+ }
package/src/core/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./bridgeAction";
2
+ export * from "./bridgeFailure";
2
3
  export * from "./bridgeId";
3
4
  export * from "./bridgeKind";
5
+ export * from "./bridgeNetError";
4
6
  export * from "./bridgePort";
5
7
  export * from "./bridgeRuntime";
6
8
  export * from "./bridgeSecretError";
@@ -0,0 +1,167 @@
1
+ import { z } from "zod";
2
+ import { zYoutubeId } from "../manifest/guide";
3
+ import { zGuideVariableRef } from "./variables";
4
+
5
+ export enum GuideCalloutTone {
6
+ Note = "note",
7
+ Warning = "warning",
8
+ Danger = "danger",
9
+ }
10
+
11
+ export enum GuideOpenTab {
12
+ Overview = "overview",
13
+ Console = "console",
14
+ Files = "files",
15
+ Backups = "backups",
16
+ Panel = "panel",
17
+ }
18
+
19
+ export const zGuideFilePath = z
20
+ .string()
21
+ .min(1)
22
+ .max(256)
23
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9./_-]*$/)
24
+ .refine((value) => !value.includes(".."), {
25
+ error: "file path must not traverse upwards",
26
+ });
27
+
28
+ export const zGuidePanelTabId = z
29
+ .string()
30
+ .min(1)
31
+ .max(32)
32
+ .regex(/^[a-z][a-z0-9-]*$/);
33
+
34
+ export const GUIDE_COMMAND_MAX_LENGTH = 512;
35
+
36
+ export const zGuideCommand = z
37
+ .string()
38
+ .min(1)
39
+ .max(GUIDE_COMMAND_MAX_LENGTH)
40
+ .regex(/^[^\p{Cc}]+$/u, {
41
+ error: "a command must be a single line without control characters",
42
+ });
43
+
44
+ export const guideOpenTargetSchema = z.discriminatedUnion("tab", [
45
+ z.object({
46
+ tab: z.literal(GuideOpenTab.Overview),
47
+ }),
48
+ z.object({
49
+ tab: z.literal(GuideOpenTab.Console),
50
+ }),
51
+ z.object({
52
+ tab: z.literal(GuideOpenTab.Backups),
53
+ }),
54
+ z.object({
55
+ tab: z.literal(GuideOpenTab.Files),
56
+ path: zGuideFilePath.nullable(),
57
+ }),
58
+ z.object({
59
+ tab: z.literal(GuideOpenTab.Panel),
60
+ tabId: zGuidePanelTabId,
61
+ }),
62
+ ]);
63
+
64
+ export type GuideOpenTarget = z.infer<typeof guideOpenTargetSchema>;
65
+
66
+ export const guideInlineSchema = z.discriminatedUnion("type", [
67
+ z.object({
68
+ type: z.literal("text"),
69
+ text: z.string().min(1).max(2048),
70
+ }),
71
+ z.object({
72
+ type: z.literal("bold"),
73
+ text: z.string().min(1).max(512),
74
+ }),
75
+ z.object({
76
+ type: z.literal("italic"),
77
+ text: z.string().min(1).max(512),
78
+ }),
79
+ z.object({
80
+ type: z.literal("code"),
81
+ text: z.string().min(1).max(512),
82
+ }),
83
+ z.object({
84
+ type: z.literal("link"),
85
+ href: z
86
+ .string()
87
+ .max(2048)
88
+ .regex(/^(https:\/\/|\/)/),
89
+ text: z.string().min(1).max(512),
90
+ }),
91
+ z.object({
92
+ type: z.literal("variable"),
93
+ key: zGuideVariableRef,
94
+ }),
95
+ ]);
96
+
97
+ export type GuideInline = z.infer<typeof guideInlineSchema>;
98
+
99
+ const guideLeafVariants = [
100
+ z.object({
101
+ type: z.literal("heading"),
102
+ level: z.union([
103
+ z.literal(2),
104
+ z.literal(3),
105
+ ]),
106
+ id: z.string().min(1).max(128),
107
+ children: z.array(guideInlineSchema).min(1),
108
+ }),
109
+ z.object({
110
+ type: z.literal("paragraph"),
111
+ children: z.array(guideInlineSchema).min(1),
112
+ }),
113
+ z.object({
114
+ type: z.literal("list"),
115
+ ordered: z.boolean(),
116
+ items: z.array(z.array(guideInlineSchema).min(1)).min(1).max(64),
117
+ }),
118
+ z.object({
119
+ type: z.literal("code"),
120
+ language: z.string().min(1).max(32).nullable(),
121
+ content: z.string().min(1).max(8192),
122
+ }),
123
+ z.object({
124
+ type: z.literal("callout"),
125
+ tone: z.enum(GuideCalloutTone),
126
+ children: z.array(guideInlineSchema).min(1),
127
+ }),
128
+ z.object({
129
+ type: z.literal("image"),
130
+ url: z.url(),
131
+ alt: z.string().max(256),
132
+ width: z.int().positive().nullable(),
133
+ height: z.int().positive().nullable(),
134
+ }),
135
+ z.object({
136
+ type: z.literal("youtube"),
137
+ videoId: zYoutubeId,
138
+ }),
139
+ z.object({
140
+ type: z.literal("field"),
141
+ key: zGuideVariableRef,
142
+ }),
143
+ z.object({
144
+ type: z.literal("command"),
145
+ command: zGuideCommand,
146
+ }),
147
+ z.object({
148
+ type: z.literal("open"),
149
+ target: guideOpenTargetSchema,
150
+ }),
151
+ ] as const;
152
+
153
+ export const guideLeafBlockSchema = z.discriminatedUnion("type", guideLeafVariants);
154
+
155
+ export type GuideLeafBlock = z.infer<typeof guideLeafBlockSchema>;
156
+
157
+ export const guideBlockSchema = z.discriminatedUnion("type", [
158
+ ...guideLeafVariants,
159
+ z.object({
160
+ type: z.literal("conditional"),
161
+ key: zGuideVariableRef,
162
+ when: z.array(guideLeafBlockSchema).min(1).max(32),
163
+ otherwise: z.array(guideLeafBlockSchema).max(32),
164
+ }),
165
+ ]);
166
+
167
+ export type GuideBlock = z.infer<typeof guideBlockSchema>;