@serverkgg/bridge 0.0.1 → 0.0.3

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 (71) hide show
  1. package/package.json +10 -3
  2. package/src/cli/link.test.ts +199 -0
  3. package/src/cli/link.ts +319 -0
  4. package/src/cli/main.ts +19 -1
  5. package/src/cli/schema.ts +62 -0
  6. package/src/cli/validate.test.ts +89 -0
  7. package/src/cli/validate.ts +218 -35
  8. package/src/context/bridgeContext.ts +18 -2
  9. package/src/core/bridgeAction.ts +13 -0
  10. package/src/core/bridgeFailure.ts +16 -0
  11. package/src/core/bridgeKind.ts +3 -0
  12. package/src/core/bridgeNetError.ts +10 -0
  13. package/src/core/index.ts +2 -0
  14. package/src/guides/blocks.ts +167 -0
  15. package/src/guides/compile.ts +193 -0
  16. package/src/guides/compiled.ts +28 -0
  17. package/src/guides/index.ts +6 -0
  18. package/src/guides/markdown.test.ts +374 -0
  19. package/src/guides/markdown.ts +769 -0
  20. package/src/guides/variables.ts +52 -0
  21. package/src/guides/version.ts +1 -0
  22. package/src/main.ts +2 -1
  23. package/src/manifest/assets.ts +4 -4
  24. package/src/manifest/guide.ts +23 -18
  25. package/src/manifest/index.ts +3 -2
  26. package/src/manifest/jsonSchema.ts +16 -0
  27. package/src/manifest/localized.ts +7 -0
  28. package/src/manifest/manifest.test.ts +366 -0
  29. package/src/manifest/manifest.ts +354 -120
  30. package/src/manifest/primitives.ts +63 -0
  31. package/src/manifest/source.ts +5 -4
  32. package/src/modules/bridge.ts +25 -4
  33. package/src/modules/bridgeDriver.ts +4 -0
  34. package/src/modules/bridgeModule.ts +36 -24
  35. package/src/panel/bridgeAccess.ts +3 -0
  36. package/src/panel/bridgeLayout.ts +1 -0
  37. package/src/panel/bridgePanel.ts +40 -2
  38. package/src/panel/index.ts +1 -0
  39. package/src/protocol/bridgeCatalog.ts +57 -0
  40. package/src/protocol/bridgeEgress.ts +53 -0
  41. package/src/protocol/bridgeEvents.ts +20 -0
  42. package/src/protocol/bridgeFrame.ts +16 -0
  43. package/src/protocol/bridgeLimits.ts +41 -0
  44. package/src/protocol/bridgeLine.ts +41 -6
  45. package/src/protocol/bridgeManifest.ts +15 -1
  46. package/src/protocol/bridgeVersion.ts +1 -1
  47. package/src/protocol/index.ts +5 -0
  48. package/src/runtime/bridgeActions.ts +53 -0
  49. package/src/runtime/bridgeContext.ts +29 -9
  50. package/src/runtime/bridgeDriver.test.ts +450 -0
  51. package/src/runtime/bridgeDriver.ts +246 -6
  52. package/src/runtime/bridgeEgress.ts +360 -0
  53. package/src/runtime/bridgeEvents.ts +4 -1
  54. package/src/runtime/bridgeExec.ts +15 -5
  55. package/src/runtime/bridgeFiles.test.ts +334 -0
  56. package/src/runtime/bridgeFiles.ts +130 -31
  57. package/src/runtime/bridgeLog.ts +26 -3
  58. package/src/runtime/bridgeNet.ts +75 -22
  59. package/src/runtime/bridgeRegistry.test.ts +291 -0
  60. package/src/runtime/bridgeRegistry.ts +91 -52
  61. package/src/runtime/bridgeServer.ts +35 -3
  62. package/src/runtime/bridgeSupervisor.ts +6 -6
  63. package/src/runtime/bridgeZip.ts +14 -3
  64. package/src/runtime/index.ts +0 -17
  65. package/src/runtime/serve.ts +44 -3
  66. package/src/terminal/bridgeTerminalArg.ts +11 -0
  67. package/src/terminal/bridgeTerminalCommand.ts +11 -0
  68. package/src/terminal/bridgeTerminalLevel.ts +6 -0
  69. package/src/terminal/bridgeTerminalRule.ts +8 -0
  70. package/src/terminal/index.ts +4 -0
  71. package/src/manifest/checks.ts +0 -50
@@ -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
+ });
@@ -3,61 +3,194 @@ 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";
17
+ import { isBridgeEventName } from "../protocol";
8
18
  import { validateDriver } from "../runtime/bridgeDriver";
9
19
  import { directorySource } from "./directorySource";
10
20
 
11
21
  const HELP = `serverk-bridge validate [options]
12
22
 
13
23
  options:
14
- --dir <path> game package directory (default: current directory)
15
- --driver <path> driver module (default: <dir>/src/driver.ts)
24
+ --dir <path> package directory (default: current directory)
25
+ --driver <path> driver module (default: <dir>/src/driver.ts), game packages only
16
26
  --help show this help`;
17
27
 
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
- });
28
+ const BRIDGE_PACKAGE = "@serverkgg/bridge";
35
29
 
36
- if (values.help) {
37
- console.log(HELP);
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
+ }
38
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) {
39
78
  return;
40
79
  }
41
80
 
42
- const dir = resolve(values.dir);
43
- const source = directorySource(dir);
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
+
86
+ const invalidManifest = (error: unknown): never => {
87
+ if (error instanceof z.ZodError) {
88
+ throw new Error(`${MANIFEST_FILE} is invalid\n${z.prettifyError(error)}`);
89
+ }
44
90
 
45
- let manifest: Manifest;
91
+ throw error;
92
+ };
46
93
 
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)}`);
94
+ const DYNAMIC_BLOCKS: ReadonlySet<string> = new Set([
95
+ "field",
96
+ "command",
97
+ "open",
98
+ "conditional",
99
+ ]);
100
+
101
+ const countDynamic = (pack: GuidePack) => {
102
+ return pack.drafts.reduce((total, draft) => {
103
+ const blocks = [
104
+ ...draft.body.ar,
105
+ ...draft.body.en,
106
+ ];
107
+
108
+ return total + blocks.filter((block) => DYNAMIC_BLOCKS.has(block.type)).length;
109
+ }, 0);
110
+ };
111
+
112
+ const reportGuides = (pack: GuidePack | null) => {
113
+ if (!pack) {
114
+ return;
115
+ }
116
+
117
+ console.log(
118
+ ` guides: ${pack.drafts.length} guide(s), ${countDynamic(pack)} dynamic block(s), ${pack.images.length} image(s)`,
119
+ );
120
+
121
+ if (pack.commands.length === 0) {
122
+ return;
123
+ }
124
+
125
+ console.log(` commands: ${pack.commands.length} runnable command(s) — review each one`);
126
+
127
+ for (const command of [
128
+ ...new Set(pack.commands),
129
+ ]) {
130
+ console.log(` ${command}`);
131
+ }
132
+ };
133
+
134
+ const validateGuidePackage = (source: PackageSource, document: GuidePackage) => {
135
+ const guideFiles = document.guides.flatMap((guide) => [
136
+ guide.bodyPath.ar,
137
+ guide.bodyPath.en,
138
+ ]);
139
+
140
+ for (const path of guideFiles) {
141
+ if (source.read(path) === null) {
142
+ throw new Error(`declared file "${path}" is missing`);
52
143
  }
144
+ }
145
+
146
+ const pack = readGuidePackage(source, document);
53
147
 
54
- throw error;
148
+ console.log(`${document.metadata.id} is valid`);
149
+ reportGuides(pack);
150
+ };
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;
55
164
  }
56
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
+
189
+ const validateGamePackage = async (source: PackageSource, manifest: Manifest, dir: string, driverArg?: string) => {
57
190
  const assets = readAssets(source, manifest);
58
191
  const guideFiles = manifest.guides.flatMap((guide) => [
59
- guide.body.ar,
60
- guide.body.en,
192
+ guide.bodyPath.ar,
193
+ guide.bodyPath.en,
61
194
  ]);
62
195
 
63
196
  for (const path of declaredFiles(manifest, guideFiles)) {
@@ -66,7 +199,8 @@ export const runValidate = async (args: string[]) => {
66
199
  }
67
200
  }
68
201
 
69
- const driverPath = values.driver ? resolve(values.driver) : join(dir, "src/driver.ts");
202
+ const pack = readGuidePack(source, manifest);
203
+ const driverPath = driverArg ? resolve(driverArg) : join(dir, "src/driver.ts");
70
204
 
71
205
  if (!existsSync(driverPath)) {
72
206
  throw new Error(`no driver module at ${driverPath} — export your BridgeDriver from src/driver.ts`);
@@ -83,12 +217,61 @@ export const runValidate = async (args: string[]) => {
83
217
 
84
218
  console.log(`${manifest.metadata.id} is valid`);
85
219
  console.log(
86
- ` manifest: ${manifest.runtime.ports.length} port(s), ${manifest.secrets.length} secret(s), ${manifest.guides.length} guide(s)`,
220
+ ` manifest: ${manifest.container.ports.length} port(s), ${manifest.secrets.length} secret(s), ${manifest.guides.length} guide(s)`,
87
221
  );
88
222
  console.log(
89
223
  ` assets: logo ${assets.logo.dimensions.width}x${assets.logo.dimensions.height}, banner ${assets.banner.dimensions.width}x${assets.banner.dimensions.height}`,
90
224
  );
225
+ reportGuides(pack);
91
226
  console.log(
92
227
  ` driver: ${Object.keys(driver.modules ?? {}).length} panel module(s), ${driver.panel?.tabs.length ?? 0} tab(s)`,
93
228
  );
229
+ reportEvents(driver);
230
+ reportActionTargets(driver);
231
+ };
232
+
233
+ export const runValidate = async (args: string[]) => {
234
+ const { values } = parseArgs({
235
+ args,
236
+ options: {
237
+ dir: {
238
+ type: "string",
239
+ default: ".",
240
+ },
241
+ driver: {
242
+ type: "string",
243
+ },
244
+ help: {
245
+ type: "boolean",
246
+ default: false,
247
+ },
248
+ },
249
+ });
250
+
251
+ if (values.help) {
252
+ console.log(HELP);
253
+
254
+ return;
255
+ }
256
+
257
+ const dir = resolve(values.dir);
258
+ const source = directorySource(dir);
259
+
260
+ assertRegistryBridge(source);
261
+
262
+ let document: Manifest | GuidePackage;
263
+
264
+ try {
265
+ document = readPackageDocument(source);
266
+ } catch (error) {
267
+ return invalidManifest(error);
268
+ }
269
+
270
+ if (document.kind === "GuidePackage") {
271
+ validateGuidePackage(source, document);
272
+
273
+ return;
274
+ }
275
+
276
+ await validateGamePackage(source, document, dir, values.driver);
94
277
  };
@@ -18,6 +18,13 @@ export interface BridgeFileEntry {
18
18
  export interface BridgeDownloadOptions {
19
19
  digest?: string;
20
20
  sizeBytes?: number;
21
+ cache?: boolean;
22
+ }
23
+
24
+ export interface BridgeExtractOptions {
25
+ extensions?: string[];
26
+ tree?: boolean;
27
+ select?: string;
21
28
  }
22
29
 
23
30
  export interface BridgeFiles {
@@ -30,7 +37,7 @@ export interface BridgeFiles {
30
37
  exists(path: string): Promise<boolean>;
31
38
  size(path: string): Promise<number>;
32
39
  download(path: string, url: string, options?: BridgeDownloadOptions): Promise<void>;
33
- extract(path: string, destination: string, extensions?: string[]): Promise<string[]>;
40
+ extract(path: string, destination: string, options?: BridgeExtractOptions): Promise<string[]>;
34
41
  }
35
42
 
36
43
  export interface BridgeCodecOptions {
@@ -52,6 +59,9 @@ export interface BridgeCodecs {
52
59
  export interface BridgeRequestOptions {
53
60
  headers?: Record<string, string>;
54
61
  timeoutMs?: number;
62
+ cache?: boolean;
63
+ cacheSeconds?: number;
64
+ refresh?: boolean;
55
65
  }
56
66
 
57
67
  export interface BridgeNet {
@@ -103,6 +113,12 @@ export interface BridgeLogs {
103
113
  watch(pattern: RegExp, timeoutMs: number): Promise<RegExpMatchArray | null>;
104
114
  }
105
115
 
116
+ export interface BridgeLog {
117
+ (message: string, fields?: BridgeValues): void;
118
+ warn(message: string, fields?: BridgeValues): void;
119
+ error(message: string, fields?: BridgeValues): void;
120
+ }
121
+
106
122
  export interface BridgeContext {
107
123
  readonly server: BridgeServer;
108
124
  variable(key: string): string | null;
@@ -116,5 +132,5 @@ export interface BridgeContext {
116
132
  exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
117
133
  logs: BridgeLogs;
118
134
  emit(event: string, payload?: BridgeValues): void;
119
- log(message: string, fields?: BridgeValues): void;
135
+ log: BridgeLog;
120
136
  }
@@ -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,10 @@
1
1
  export enum BridgeKind {
2
+ Actions = "actions",
3
+ Announce = "announce",
2
4
  Backup = "backup",
3
5
  Catalog = "catalog",
4
6
  Collection = "collection",
7
+ Terminal = "terminal",
5
8
  Events = "events",
6
9
  Install = "install",
7
10
  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>;