@serverkgg/bridge 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -1
- package/src/blog/articleFrontmatter.test.ts +95 -0
- package/src/blog/articleFrontmatter.ts +99 -0
- package/src/blog/compile.test.ts +148 -0
- package/src/blog/compile.ts +172 -0
- package/src/blog/compiled.ts +22 -0
- package/src/blog/index.ts +3 -0
- package/src/catalogs/curseforge.test.ts +580 -0
- package/src/catalogs/curseforge.ts +461 -0
- package/src/catalogs/index.ts +2 -0
- package/src/catalogs/thunderstore.test.ts +94 -0
- package/src/catalogs/thunderstore.ts +239 -0
- package/src/cli/validate.ts +7 -0
- package/src/codecs/index.ts +3 -0
- package/src/codecs/ini.test.ts +152 -0
- package/src/codecs/ini.ts +158 -0
- package/src/codecs/sourceCfg.test.ts +100 -0
- package/src/codecs/sourceCfg.ts +111 -0
- package/src/codecs/xmlProperties.test.ts +104 -0
- package/src/codecs/xmlProperties.ts +133 -0
- package/src/context/bridgeContext.ts +40 -0
- package/src/core/bridgeRconError.ts +19 -0
- package/src/core/index.ts +1 -0
- package/src/guides/compile.ts +70 -54
- package/src/install/index.ts +1 -0
- package/src/install/installStamp.test.ts +125 -0
- package/src/install/installStamp.ts +40 -0
- package/src/main.ts +1 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +460 -2
- package/src/manifest/manifest.ts +141 -13
- package/src/manifest/primitives.ts +6 -0
- package/src/manifest/runtime.ts +89 -0
- package/src/net/index.ts +2 -0
- package/src/net/loopback.test.ts +152 -0
- package/src/net/loopback.ts +109 -0
- package/src/net/loopbackMethod.ts +7 -0
- package/src/presence/index.ts +1 -0
- package/src/presence/rosterSync.test.ts +179 -0
- package/src/presence/rosterSync.ts +57 -0
- package/src/probes/index.ts +9 -0
- package/src/probes/minecraftPing.test.ts +344 -0
- package/src/probes/minecraftPing.ts +283 -0
- package/src/probes/raknetPing.test.ts +197 -0
- package/src/probes/raknetPing.ts +228 -0
- package/src/protocol/bridgeEvents.ts +4 -0
- package/src/rcon/index.ts +3 -0
- package/src/rcon/sourceRcon.test.ts +308 -0
- package/src/rcon/sourceRcon.ts +276 -0
- package/src/rcon/sourceRconFrame.test.ts +152 -0
- package/src/rcon/sourceRconFrame.ts +99 -0
- package/src/runtime/bridgeCodec.ts +45 -2
- package/src/runtime/bridgeContext.ts +13 -6
- package/src/runtime/bridgeDriver.test.ts +1 -0
- package/src/runtime/bridgeLiveConfig.test.ts +149 -0
- package/src/runtime/bridgeLiveConfig.ts +63 -0
- package/src/runtime/bridgeProbe.ts +13 -1
- package/src/runtime/bridgeRcon.ts +15 -0
- package/src/runtime/bridgeRegistry.ts +1 -0
- package/src/steam/index.ts +7 -0
- package/src/steam/steamApp.test.ts +78 -0
- package/src/steam/steamApp.ts +35 -0
- package/src/steam/steamcmd.test.ts +157 -0
- package/src/steam/steamcmd.ts +273 -0
- package/src/steam/steamcmdLogin.ts +34 -0
- package/src/steam/steamcmdNarrator.test.ts +90 -0
- package/src/steam/steamcmdNarrator.ts +43 -0
- package/src/steam/steamcmdPlatform.ts +4 -0
- package/src/steam/webApi.ts +70 -0
- package/src/steam/workshop.test.ts +45 -0
- package/src/steam/workshop.ts +148 -0
- package/src/utils/execDetail.ts +16 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/token.test.ts +50 -0
- package/src/utils/token.ts +35 -0
- package/src/wine/index.ts +3 -0
- package/src/wine/protonCommand.test.ts +67 -0
- package/src/wine/protonCommand.ts +49 -0
- package/src/wine/wineCommand.test.ts +92 -0
- package/src/wine/wineCommand.ts +54 -0
- package/src/wine/winePrefix.test.ts +111 -0
- package/src/wine/winePrefix.ts +88 -0
- package/src/runtime/bridgeSecrets.test.ts +0 -121
- package/src/runtime/bridgeSecrets.ts +0 -43
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { BridgeValue, BridgeValues } from "../core";
|
|
2
|
+
|
|
3
|
+
const ENTRY = /^(?<key>[A-Za-z_][A-Za-z0-9_.]*)(?:[ \t]+(?<value>.*))?$/;
|
|
4
|
+
|
|
5
|
+
const NUMERIC = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
6
|
+
|
|
7
|
+
const NEWLINES = /[\r\n]+/g;
|
|
8
|
+
|
|
9
|
+
export interface SourceCfgEntry {
|
|
10
|
+
key: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const readSourceCfgEntry = (line: string): SourceCfgEntry | null => {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
|
|
17
|
+
if (trimmed.length === 0 || trimmed.startsWith("//") || trimmed.startsWith("#")) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const match = trimmed.match(ENTRY);
|
|
22
|
+
|
|
23
|
+
if (!match?.groups?.key) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
key: match.groups.key,
|
|
29
|
+
value: (match.groups.value ?? "").trim(),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const decodeSourceCfgValue = (raw: string): BridgeValue => {
|
|
34
|
+
if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
|
|
35
|
+
return raw.slice(1, -1).replaceAll('\\"', '"').replaceAll("\\\\", "\\");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (raw === "true" || raw === "false") {
|
|
39
|
+
return raw === "true";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (NUMERIC.test(raw)) {
|
|
43
|
+
return Number(raw);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return raw;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const encodeSourceCfgValue = (value: BridgeValue): string => {
|
|
50
|
+
if (typeof value === "boolean") {
|
|
51
|
+
return value ? "true" : "false";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value === "number") {
|
|
55
|
+
return Number.isFinite(value) ? String(value) : '""';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const text = (value ?? "").replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll(NEWLINES, " ");
|
|
59
|
+
|
|
60
|
+
return `"${text}"`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const parseSourceCfg = (contents: string): BridgeValues => {
|
|
64
|
+
const values: BridgeValues = {};
|
|
65
|
+
|
|
66
|
+
for (const line of contents.split("\n")) {
|
|
67
|
+
const entry = readSourceCfgEntry(line);
|
|
68
|
+
|
|
69
|
+
if (entry) {
|
|
70
|
+
values[entry.key] = decodeSourceCfgValue(entry.value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return values;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const mergeSourceCfg = (contents: string, values: BridgeValues): string => {
|
|
78
|
+
const pending = new Map(Object.entries(values));
|
|
79
|
+
const written = new Set<string>();
|
|
80
|
+
const lines: string[] = [];
|
|
81
|
+
|
|
82
|
+
for (const line of contents.split("\n")) {
|
|
83
|
+
const entry = readSourceCfgEntry(line);
|
|
84
|
+
const key = entry?.key ?? "";
|
|
85
|
+
|
|
86
|
+
if (!entry || !pending.has(key)) {
|
|
87
|
+
lines.push(line);
|
|
88
|
+
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (written.has(key)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
written.add(key);
|
|
97
|
+
lines.push(`${key} ${encodeSourceCfgValue(pending.get(key) ?? null)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
while (lines.length > 0 && (lines.at(-1) ?? "").trim().length === 0) {
|
|
101
|
+
lines.pop();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const [key, value] of pending) {
|
|
105
|
+
if (!written.has(key)) {
|
|
106
|
+
lines.push(`${key} ${encodeSourceCfgValue(value)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `${lines.join("\n")}\n`;
|
|
111
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mergeXmlProperties, parseXmlProperties } from "./xmlProperties";
|
|
3
|
+
|
|
4
|
+
const SAMPLE = [
|
|
5
|
+
'<?xml version="1.0"?>',
|
|
6
|
+
"<ServerSettings>",
|
|
7
|
+
'\t<property name="ServerName" value="serverk"/>',
|
|
8
|
+
"\t<!-- a note -->",
|
|
9
|
+
'\t<property name="ServerPort" value="26900"/>',
|
|
10
|
+
'\t<property value="8" name="ServerMaxPlayerCount"/>',
|
|
11
|
+
"</ServerSettings>",
|
|
12
|
+
"",
|
|
13
|
+
].join("\n");
|
|
14
|
+
|
|
15
|
+
describe("parseXmlProperties", () => {
|
|
16
|
+
test("reads every property whichever way the attributes are ordered", () => {
|
|
17
|
+
expect(parseXmlProperties(SAMPLE)).toEqual({
|
|
18
|
+
ServerName: "serverk",
|
|
19
|
+
ServerPort: "26900",
|
|
20
|
+
ServerMaxPlayerCount: "8",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("decodes entities", () => {
|
|
25
|
+
expect(parseXmlProperties('<property name="ServerName" value="a & b <c>"/>')).toEqual({
|
|
26
|
+
ServerName: "a & b <c>",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("accepts single quoted attributes and an empty value", () => {
|
|
31
|
+
expect(parseXmlProperties("<property name='ServerPassword' value=''/>")).toEqual({
|
|
32
|
+
ServerPassword: "",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("ignores everything that is not a property", () => {
|
|
37
|
+
expect(parseXmlProperties('<ServerSettings>\n<other name="x" value="1"/>\n</ServerSettings>')).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("mergeXmlProperties", () => {
|
|
42
|
+
test("rewrites a value in place and keeps order, comments and the header", () => {
|
|
43
|
+
expect(
|
|
44
|
+
mergeXmlProperties(SAMPLE, {
|
|
45
|
+
ServerPort: "26950",
|
|
46
|
+
}),
|
|
47
|
+
).toBe(
|
|
48
|
+
[
|
|
49
|
+
'<?xml version="1.0"?>',
|
|
50
|
+
"<ServerSettings>",
|
|
51
|
+
'\t<property name="ServerName" value="serverk"/>',
|
|
52
|
+
"\t<!-- a note -->",
|
|
53
|
+
'\t<property name="ServerPort" value="26950"/>',
|
|
54
|
+
'\t<property value="8" name="ServerMaxPlayerCount"/>',
|
|
55
|
+
"</ServerSettings>",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("rewrites a property whose value attribute comes first", () => {
|
|
62
|
+
expect(
|
|
63
|
+
mergeXmlProperties(SAMPLE, {
|
|
64
|
+
ServerMaxPlayerCount: "16",
|
|
65
|
+
}),
|
|
66
|
+
).toContain('<property value="16" name="ServerMaxPlayerCount"/>');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("adds an unknown property before the closing tag with the same indent", () => {
|
|
70
|
+
expect(
|
|
71
|
+
mergeXmlProperties(SAMPLE, {
|
|
72
|
+
ServerPassword: "1234",
|
|
73
|
+
}),
|
|
74
|
+
).toBe(
|
|
75
|
+
[
|
|
76
|
+
'<?xml version="1.0"?>',
|
|
77
|
+
"<ServerSettings>",
|
|
78
|
+
'\t<property name="ServerName" value="serverk"/>',
|
|
79
|
+
"\t<!-- a note -->",
|
|
80
|
+
'\t<property name="ServerPort" value="26900"/>',
|
|
81
|
+
'\t<property value="8" name="ServerMaxPlayerCount"/>',
|
|
82
|
+
'\t<property name="ServerPassword" value="1234"/>',
|
|
83
|
+
"</ServerSettings>",
|
|
84
|
+
"",
|
|
85
|
+
].join("\n"),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("escapes a value it writes", () => {
|
|
90
|
+
expect(
|
|
91
|
+
mergeXmlProperties(SAMPLE, {
|
|
92
|
+
ServerName: 'a & b "c"',
|
|
93
|
+
}),
|
|
94
|
+
).toContain('<property name="ServerName" value="a & b "c""/>');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("adds a value attribute to a property that has none", () => {
|
|
98
|
+
expect(
|
|
99
|
+
mergeXmlProperties('<property name="ServerName"/>', {
|
|
100
|
+
ServerName: "x",
|
|
101
|
+
}),
|
|
102
|
+
).toBe('<property name="ServerName" value="x"/>');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const PROPERTY_TAG = /<property\b[^>]*>/gi;
|
|
2
|
+
|
|
3
|
+
const PROPERTY_LINE = /<property\b/i;
|
|
4
|
+
|
|
5
|
+
const CLOSING_TAG = /<\/[A-Za-z_][\w.:-]*\s*>/g;
|
|
6
|
+
|
|
7
|
+
const INDENT = /^[ \t]*/;
|
|
8
|
+
|
|
9
|
+
const DEFAULT_INDENT = "\t";
|
|
10
|
+
|
|
11
|
+
const ENTITIES: Record<string, string> = {
|
|
12
|
+
amp: "&",
|
|
13
|
+
apos: "'",
|
|
14
|
+
gt: ">",
|
|
15
|
+
lt: "<",
|
|
16
|
+
quot: '"',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ENTITY = /&(amp|apos|gt|lt|quot);/g;
|
|
20
|
+
|
|
21
|
+
const attributePattern = (name: string) => {
|
|
22
|
+
return new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const NAME_ATTRIBUTE = attributePattern("name");
|
|
26
|
+
|
|
27
|
+
const VALUE_ATTRIBUTE = attributePattern("value");
|
|
28
|
+
|
|
29
|
+
const decodeXml = (raw: string) => {
|
|
30
|
+
return raw.replace(ENTITY, (_match, entity: string) => ENTITIES[entity] ?? _match);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const encodeXml = (raw: string) => {
|
|
34
|
+
return raw
|
|
35
|
+
.replaceAll("&", "&")
|
|
36
|
+
.replaceAll("<", "<")
|
|
37
|
+
.replaceAll(">", ">")
|
|
38
|
+
.replaceAll('"', """)
|
|
39
|
+
.replaceAll("'", "'");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const attributeOf = (tag: string, pattern: RegExp) => {
|
|
43
|
+
const match = tag.match(pattern);
|
|
44
|
+
|
|
45
|
+
if (!match) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return decodeXml(match[1] ?? match[2] ?? "");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const propertyLine = (indent: string, key: string, value: string) => {
|
|
53
|
+
return `${indent}<property name="${encodeXml(key)}" value="${encodeXml(value)}"/>`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const existingIndent = (contents: string) => {
|
|
57
|
+
for (const line of contents.split("\n")) {
|
|
58
|
+
if (PROPERTY_LINE.test(line)) {
|
|
59
|
+
return line.match(INDENT)?.at(0) ?? DEFAULT_INDENT;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return DEFAULT_INDENT;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const parseXmlProperties = (contents: string) => {
|
|
67
|
+
const values: Record<string, string> = {};
|
|
68
|
+
|
|
69
|
+
for (const [tag] of contents.matchAll(PROPERTY_TAG)) {
|
|
70
|
+
const key = attributeOf(tag, NAME_ATTRIBUTE);
|
|
71
|
+
|
|
72
|
+
if (key !== null && key.length > 0) {
|
|
73
|
+
values[key] = attributeOf(tag, VALUE_ATTRIBUTE) ?? "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return values;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const mergeXmlProperties = (contents: string, values: Record<string, string>) => {
|
|
81
|
+
const pending = new Map(Object.entries(values));
|
|
82
|
+
const indent = existingIndent(contents);
|
|
83
|
+
|
|
84
|
+
const rewritten = contents.replace(PROPERTY_TAG, (tag) => {
|
|
85
|
+
const key = attributeOf(tag, NAME_ATTRIBUTE);
|
|
86
|
+
|
|
87
|
+
if (key === null || !pending.has(key)) {
|
|
88
|
+
return tag;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const value = encodeXml(pending.get(key) ?? "");
|
|
92
|
+
|
|
93
|
+
pending.delete(key);
|
|
94
|
+
|
|
95
|
+
if (VALUE_ATTRIBUTE.test(tag)) {
|
|
96
|
+
return tag.replace(VALUE_ATTRIBUTE, `value="${value}"`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return tag.replace(/\s*\/?>$/, ` value="${value}"/>`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (pending.size === 0) {
|
|
103
|
+
return rewritten;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const added = [
|
|
107
|
+
...pending,
|
|
108
|
+
].map(([key, value]) => propertyLine(indent, key, value));
|
|
109
|
+
|
|
110
|
+
const closings = [
|
|
111
|
+
...rewritten.matchAll(CLOSING_TAG),
|
|
112
|
+
];
|
|
113
|
+
const last = closings.at(-1);
|
|
114
|
+
|
|
115
|
+
if (last?.index === undefined) {
|
|
116
|
+
const lines = rewritten.split("\n");
|
|
117
|
+
|
|
118
|
+
while (lines.length > 0 && (lines.at(-1) ?? "").trim().length === 0) {
|
|
119
|
+
lines.pop();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `${[
|
|
123
|
+
...lines,
|
|
124
|
+
...added,
|
|
125
|
+
].join("\n")}\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const head = rewritten.slice(0, last.index);
|
|
129
|
+
const tail = rewritten.slice(last.index);
|
|
130
|
+
const lineStart = head.lastIndexOf("\n") + 1;
|
|
131
|
+
|
|
132
|
+
return `${head.slice(0, lineStart)}${added.join("\n")}\n${head.slice(lineStart)}${tail}`;
|
|
133
|
+
};
|
|
@@ -59,9 +59,12 @@ export interface BridgeCodec {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export interface BridgeCodecs {
|
|
62
|
+
ini: BridgeCodec;
|
|
62
63
|
json: BridgeCodec;
|
|
63
64
|
properties: BridgeCodec;
|
|
65
|
+
sourceCfg: BridgeCodec;
|
|
64
66
|
ueIni: BridgeCodec;
|
|
67
|
+
xmlProperties: BridgeCodec;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export interface BridgeRequestOptions {
|
|
@@ -91,8 +94,44 @@ export interface BridgeA2sInfo {
|
|
|
91
94
|
latencyMs: number;
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
export interface BridgeMinecraftPingStatus {
|
|
98
|
+
players: BridgePingPlayers;
|
|
99
|
+
version: string | null;
|
|
100
|
+
description: string | null;
|
|
101
|
+
latencyMs: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface BridgeRaknetPingStatus {
|
|
105
|
+
players: BridgePingPlayers;
|
|
106
|
+
edition: string | null;
|
|
107
|
+
motd: string | null;
|
|
108
|
+
subMotd: string | null;
|
|
109
|
+
version: string | null;
|
|
110
|
+
protocol: number | null;
|
|
111
|
+
gamemode: string | null;
|
|
112
|
+
latencyMs: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
export interface BridgeProbes {
|
|
95
116
|
a2s(port: number): Promise<BridgeA2sInfo>;
|
|
117
|
+
minecraftPing(port: number): Promise<BridgeMinecraftPingStatus>;
|
|
118
|
+
raknetPing(port: number): Promise<BridgeRaknetPingStatus>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface BridgeRconOptions {
|
|
122
|
+
port: number;
|
|
123
|
+
password: string;
|
|
124
|
+
timeoutMs?: number;
|
|
125
|
+
multiPacket?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface BridgeRconClient {
|
|
129
|
+
command(text: string): Promise<string>;
|
|
130
|
+
close(): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface BridgeRcons {
|
|
134
|
+
source(options: BridgeRconOptions): Promise<BridgeRconClient>;
|
|
96
135
|
}
|
|
97
136
|
|
|
98
137
|
export interface BridgeCommandOptions {
|
|
@@ -144,6 +183,7 @@ export interface BridgeContext {
|
|
|
144
183
|
codec: BridgeCodecs;
|
|
145
184
|
net: BridgeNet;
|
|
146
185
|
probe: BridgeProbes;
|
|
186
|
+
rcon: BridgeRcons;
|
|
147
187
|
command(input: string, options?: BridgeCommandOptions): Promise<BridgeReply>;
|
|
148
188
|
exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
|
|
149
189
|
logs: BridgeLogs;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export enum BridgeRconCode {
|
|
2
|
+
AuthFailed = "auth-failed",
|
|
3
|
+
Closed = "closed",
|
|
4
|
+
Malformed = "malformed",
|
|
5
|
+
Oversized = "oversized",
|
|
6
|
+
TimedOut = "timed-out",
|
|
7
|
+
Unreachable = "unreachable",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class BridgeRconError extends Error {
|
|
11
|
+
readonly code: BridgeRconCode;
|
|
12
|
+
|
|
13
|
+
constructor(code: BridgeRconCode, message: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
|
|
16
|
+
this.name = "BridgeRconError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./bridgeKind";
|
|
|
7
7
|
export * from "./bridgeNetError";
|
|
8
8
|
export * from "./bridgePort";
|
|
9
9
|
export * from "./bridgeProbeError";
|
|
10
|
+
export * from "./bridgeRconError";
|
|
10
11
|
export * from "./bridgeRuntime";
|
|
11
12
|
export * from "./bridgeSecretError";
|
|
12
13
|
export * from "./bridgeSetup";
|
package/src/guides/compile.ts
CHANGED
|
@@ -52,49 +52,65 @@ const compileTags = (guideTags: GuideTag[], tags: string[]): CompiledGuideTag[]
|
|
|
52
52
|
});
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
export
|
|
56
|
-
|
|
55
|
+
export interface MarkdownBodyReader {
|
|
56
|
+
files: string[];
|
|
57
|
+
commands: string[];
|
|
58
|
+
images: Map<string, GuideImageAsset>;
|
|
59
|
+
read(path: string, text?: string): ParsedBlock[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const markdownBodyReader = (source: PackageSource): MarkdownBodyReader => {
|
|
57
63
|
const files: string[] = [];
|
|
58
64
|
const commands: string[] = [];
|
|
59
|
-
const
|
|
65
|
+
const images = new Map<string, GuideImageAsset>();
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
return {
|
|
68
|
+
files,
|
|
69
|
+
commands,
|
|
70
|
+
images,
|
|
71
|
+
read(path, text) {
|
|
72
|
+
const body = source.read(path);
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
if (!body) {
|
|
75
|
+
throw new Error(`missing markdown body ${path}`);
|
|
76
|
+
}
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
files.push(path);
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
const parsed = parseGuideMarkdown(path, text ?? new TextDecoder().decode(body));
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
commands.push(...parsed.commands);
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
for (const imagePath of parsed.images) {
|
|
85
|
+
if (images.has(imagePath)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
78
88
|
|
|
79
|
-
|
|
89
|
+
const imageBytes = source.read(imagePath);
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
if (!imageBytes) {
|
|
92
|
+
throw new Error(`missing guide image ${imagePath}`);
|
|
93
|
+
}
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
const verified = verifyAsset(imagePath, imageBytes);
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
images.set(imagePath, {
|
|
98
|
+
path: imagePath,
|
|
99
|
+
bytes: imageBytes,
|
|
100
|
+
mediaType: verified.mediaType,
|
|
101
|
+
width: verified.dimensions?.width ?? null,
|
|
102
|
+
height: verified.dimensions?.height ?? null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
return parsed.blocks;
|
|
107
|
+
},
|
|
97
108
|
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const compileGuides = (source: PackageSource, guideTags: GuideTag[], entries: GuideEntry[]): GuidePack => {
|
|
112
|
+
const drafts: DraftGuide[] = [];
|
|
113
|
+
const reader = markdownBodyReader(source);
|
|
98
114
|
|
|
99
115
|
for (const entry of entries) {
|
|
100
116
|
drafts.push({
|
|
@@ -105,8 +121,8 @@ export const compileGuides = (source: PackageSource, guideTags: GuideTag[], entr
|
|
|
105
121
|
surfaces: entry.surfaces,
|
|
106
122
|
videoId: entry.video ?? null,
|
|
107
123
|
body: {
|
|
108
|
-
ar:
|
|
109
|
-
en:
|
|
124
|
+
ar: reader.read(entry.bodyPath.ar),
|
|
125
|
+
en: reader.read(entry.bodyPath.en),
|
|
110
126
|
},
|
|
111
127
|
});
|
|
112
128
|
}
|
|
@@ -114,13 +130,13 @@ export const compileGuides = (source: PackageSource, guideTags: GuideTag[], entr
|
|
|
114
130
|
return {
|
|
115
131
|
drafts,
|
|
116
132
|
files: [
|
|
117
|
-
...files,
|
|
118
|
-
...
|
|
133
|
+
...reader.files,
|
|
134
|
+
...reader.images.keys(),
|
|
119
135
|
],
|
|
120
136
|
images: [
|
|
121
|
-
...
|
|
137
|
+
...reader.images.values(),
|
|
122
138
|
],
|
|
123
|
-
commands,
|
|
139
|
+
commands: reader.commands,
|
|
124
140
|
};
|
|
125
141
|
};
|
|
126
142
|
|
|
@@ -142,10 +158,7 @@ export interface ResolvedGuideImage {
|
|
|
142
158
|
height: number | null;
|
|
143
159
|
}
|
|
144
160
|
|
|
145
|
-
export const
|
|
146
|
-
drafts: DraftGuide[],
|
|
147
|
-
resolved: Map<string, ResolvedGuideImage>,
|
|
148
|
-
): CompiledGuide[] => {
|
|
161
|
+
export const resolveGuideBlocks = (blocks: ParsedBlock[], resolved: Map<string, ResolvedGuideImage>): GuideBlock[] => {
|
|
149
162
|
const resolveLeaf = (block: ParsedLeafBlock): GuideLeafBlock => {
|
|
150
163
|
if (block.type !== "image") {
|
|
151
164
|
return block;
|
|
@@ -166,27 +179,30 @@ export const resolveGuideImages = (
|
|
|
166
179
|
};
|
|
167
180
|
};
|
|
168
181
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
182
|
+
return blocks.map((block) => {
|
|
183
|
+
if (block.type !== "conditional") {
|
|
184
|
+
return resolveLeaf(block);
|
|
185
|
+
}
|
|
174
186
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
return {
|
|
188
|
+
type: "conditional",
|
|
189
|
+
key: block.key,
|
|
190
|
+
when: block.when.map(resolveLeaf),
|
|
191
|
+
otherwise: block.otherwise.map(resolveLeaf),
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
};
|
|
183
195
|
|
|
196
|
+
export const resolveGuideImages = (
|
|
197
|
+
drafts: DraftGuide[],
|
|
198
|
+
resolved: Map<string, ResolvedGuideImage>,
|
|
199
|
+
): CompiledGuide[] => {
|
|
184
200
|
return drafts.map((draft) => {
|
|
185
201
|
return compiledGuideSchema.parse({
|
|
186
202
|
...draft,
|
|
187
203
|
body: {
|
|
188
|
-
ar:
|
|
189
|
-
en:
|
|
204
|
+
ar: resolveGuideBlocks(draft.body.ar, resolved),
|
|
205
|
+
en: resolveGuideBlocks(draft.body.en, resolved),
|
|
190
206
|
},
|
|
191
207
|
});
|
|
192
208
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./installStamp";
|