@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.
- package/package.json +10 -3
- package/src/cli/link.test.ts +199 -0
- package/src/cli/link.ts +319 -0
- package/src/cli/main.ts +19 -1
- package/src/cli/schema.ts +62 -0
- package/src/cli/validate.test.ts +89 -0
- package/src/cli/validate.ts +218 -35
- package/src/context/bridgeContext.ts +18 -2
- package/src/core/bridgeAction.ts +13 -0
- package/src/core/bridgeFailure.ts +16 -0
- package/src/core/bridgeKind.ts +3 -0
- package/src/core/bridgeNetError.ts +10 -0
- package/src/core/index.ts +2 -0
- package/src/guides/blocks.ts +167 -0
- package/src/guides/compile.ts +193 -0
- package/src/guides/compiled.ts +28 -0
- package/src/guides/index.ts +6 -0
- package/src/guides/markdown.test.ts +374 -0
- package/src/guides/markdown.ts +769 -0
- package/src/guides/variables.ts +52 -0
- package/src/guides/version.ts +1 -0
- package/src/main.ts +2 -1
- package/src/manifest/assets.ts +4 -4
- package/src/manifest/guide.ts +23 -18
- package/src/manifest/index.ts +3 -2
- package/src/manifest/jsonSchema.ts +16 -0
- package/src/manifest/localized.ts +7 -0
- package/src/manifest/manifest.test.ts +366 -0
- package/src/manifest/manifest.ts +354 -120
- package/src/manifest/primitives.ts +63 -0
- package/src/manifest/source.ts +5 -4
- package/src/modules/bridge.ts +25 -4
- package/src/modules/bridgeDriver.ts +4 -0
- package/src/modules/bridgeModule.ts +36 -24
- package/src/panel/bridgeAccess.ts +3 -0
- package/src/panel/bridgeLayout.ts +1 -0
- package/src/panel/bridgePanel.ts +40 -2
- package/src/panel/index.ts +1 -0
- package/src/protocol/bridgeCatalog.ts +57 -0
- package/src/protocol/bridgeEgress.ts +53 -0
- package/src/protocol/bridgeEvents.ts +20 -0
- package/src/protocol/bridgeFrame.ts +16 -0
- package/src/protocol/bridgeLimits.ts +41 -0
- package/src/protocol/bridgeLine.ts +41 -6
- package/src/protocol/bridgeManifest.ts +15 -1
- package/src/protocol/bridgeVersion.ts +1 -1
- package/src/protocol/index.ts +5 -0
- package/src/runtime/bridgeActions.ts +53 -0
- package/src/runtime/bridgeContext.ts +29 -9
- package/src/runtime/bridgeDriver.test.ts +450 -0
- package/src/runtime/bridgeDriver.ts +246 -6
- package/src/runtime/bridgeEgress.ts +360 -0
- package/src/runtime/bridgeEvents.ts +4 -1
- package/src/runtime/bridgeExec.ts +15 -5
- package/src/runtime/bridgeFiles.test.ts +334 -0
- package/src/runtime/bridgeFiles.ts +130 -31
- package/src/runtime/bridgeLog.ts +26 -3
- package/src/runtime/bridgeNet.ts +75 -22
- package/src/runtime/bridgeRegistry.test.ts +291 -0
- package/src/runtime/bridgeRegistry.ts +91 -52
- package/src/runtime/bridgeServer.ts +35 -3
- package/src/runtime/bridgeSupervisor.ts +6 -6
- package/src/runtime/bridgeZip.ts +14 -3
- package/src/runtime/index.ts +0 -17
- package/src/runtime/serve.ts +44 -3
- package/src/terminal/bridgeTerminalArg.ts +11 -0
- package/src/terminal/bridgeTerminalCommand.ts +11 -0
- package/src/terminal/bridgeTerminalLevel.ts +6 -0
- package/src/terminal/bridgeTerminalRule.ts +8 -0
- package/src/terminal/index.ts +4 -0
- 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
|
+
});
|
package/src/cli/validate.ts
CHANGED
|
@@ -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 {
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
91
|
+
throw error;
|
|
92
|
+
};
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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.
|
|
60
|
-
guide.
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
135
|
+
log: BridgeLog;
|
|
120
136
|
}
|
package/src/core/bridgeAction.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/core/bridgeKind.ts
CHANGED
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>;
|