@serverkgg/bridge 0.0.13 → 0.0.15
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 +1 -1
- package/src/cli/validate.test.ts +194 -2
- package/src/cli/validate.ts +83 -1
- package/src/context/bridgeContext.ts +9 -2
- package/src/guides/variables.ts +1 -0
- package/src/manifest/manifest.ts +1 -1
- package/src/modules/bridgeModule.ts +2 -1
- package/src/panel/bridgePanel.ts +11 -0
- package/src/panel/bridgePlace.ts +4 -0
- package/src/panel/index.ts +1 -0
- package/src/rcon/index.ts +3 -2
- package/src/rcon/rconAccess.test.ts +304 -0
- package/src/rcon/rconAccess.ts +260 -0
- package/src/rcon/rconClient.test.ts +1544 -0
- package/src/rcon/rconClient.ts +275 -0
- package/src/rcon/rconError.ts +21 -0
- package/src/rcon/rconFrame.test.ts +269 -0
- package/src/rcon/{sourceRconFrame.ts → rconFrame.ts} +23 -14
- package/src/rcon/rconIds.test.ts +59 -0
- package/src/rcon/rconIds.ts +33 -0
- package/src/rcon/rconLink.ts +427 -0
- package/src/rcon/rconWriter.test.ts +95 -0
- package/src/rcon/rconWriter.ts +63 -0
- package/src/runtime/bridgeConsoleHandler.test.ts +196 -0
- package/src/runtime/bridgeConsoleHandler.ts +26 -0
- package/src/runtime/bridgeDriver.test.ts +145 -0
- package/src/runtime/bridgeDriver.ts +44 -2
- package/src/runtime/bridgeRcon.ts +6 -2
- package/src/runtime/bridgeRegistry.test.ts +96 -0
- package/src/runtime/bridgeRegistry.ts +33 -1
- package/src/runtime/serve.ts +10 -1
- package/src/steam/avatars.test.ts +306 -0
- package/src/steam/avatars.ts +107 -0
- package/src/steam/index.ts +1 -0
- package/src/wire/panel.test.ts +79 -0
- package/src/wire/panel.ts +11 -0
- package/src/rcon/sourceRcon.test.ts +0 -308
- package/src/rcon/sourceRcon.ts +0 -276
- package/src/rcon/sourceRconFrame.test.ts +0 -152
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { BridgeCommandOptions, BridgeContext, BridgeReply } from "../context";
|
|
3
|
+
import { BridgeKind, BridgeUserError } from "../core";
|
|
4
|
+
import type { BridgeDriver, BridgeTerminalModule } from "../modules";
|
|
5
|
+
import { createConsoleHandler } from "./bridgeConsoleHandler";
|
|
6
|
+
|
|
7
|
+
const context = {} as unknown as BridgeContext;
|
|
8
|
+
|
|
9
|
+
const driverWith = (terminal: BridgeTerminalModule | undefined): BridgeDriver => ({
|
|
10
|
+
install: {
|
|
11
|
+
kind: BridgeKind.Install,
|
|
12
|
+
run: async () => {
|
|
13
|
+
return;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
lifecycle: {
|
|
17
|
+
kind: BridgeKind.Lifecycle,
|
|
18
|
+
command: async () => [
|
|
19
|
+
"run",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
terminal,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const terminalWith = (run: BridgeTerminalModule["run"]): BridgeTerminalModule => ({
|
|
26
|
+
kind: BridgeKind.Terminal,
|
|
27
|
+
commands: [
|
|
28
|
+
{
|
|
29
|
+
name: "Save",
|
|
30
|
+
summary: {
|
|
31
|
+
ar: "يحفظ العالم.",
|
|
32
|
+
en: "Save the world.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
run,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const handlerFor = (driver: BridgeDriver) => {
|
|
40
|
+
const written: string[] = [];
|
|
41
|
+
|
|
42
|
+
const handler = createConsoleHandler({
|
|
43
|
+
driver,
|
|
44
|
+
context,
|
|
45
|
+
async onStdin(input: string, options: BridgeCommandOptions): Promise<BridgeReply> {
|
|
46
|
+
written.push(input);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
sent: input,
|
|
50
|
+
line: options.expect ? "matched" : null,
|
|
51
|
+
groups: {},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
handler,
|
|
58
|
+
written,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe("routing a console line to the driver before the game's stdin", () => {
|
|
63
|
+
test("answers with the driver's reply when the driver handles the line", async () => {
|
|
64
|
+
const { handler, written } = handlerFor(
|
|
65
|
+
driverWith(
|
|
66
|
+
terminalWith(async (_context, input) => {
|
|
67
|
+
return {
|
|
68
|
+
sent: input,
|
|
69
|
+
line: "Save requested",
|
|
70
|
+
groups: {},
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(await handler("Save", {})).toEqual({
|
|
77
|
+
sent: "Save",
|
|
78
|
+
line: "Save requested",
|
|
79
|
+
groups: {},
|
|
80
|
+
});
|
|
81
|
+
expect(written).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("hands the driver the same line the caller typed", async () => {
|
|
85
|
+
const seen: string[] = [];
|
|
86
|
+
|
|
87
|
+
const { handler } = handlerFor(
|
|
88
|
+
driverWith(
|
|
89
|
+
terminalWith(async (_context, input) => {
|
|
90
|
+
seen.push(input);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
sent: input,
|
|
94
|
+
line: null,
|
|
95
|
+
groups: {},
|
|
96
|
+
};
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await handler("Broadcast hello world", {});
|
|
102
|
+
|
|
103
|
+
expect(seen).toEqual([
|
|
104
|
+
"Broadcast hello world",
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("writes the line to stdin when the driver answers null", async () => {
|
|
109
|
+
const { handler, written } = handlerFor(
|
|
110
|
+
driverWith(
|
|
111
|
+
terminalWith(async () => {
|
|
112
|
+
return null;
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(await handler("save-all", {})).toEqual({
|
|
118
|
+
sent: "save-all",
|
|
119
|
+
line: null,
|
|
120
|
+
groups: {},
|
|
121
|
+
});
|
|
122
|
+
expect(written).toEqual([
|
|
123
|
+
"save-all",
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("keeps the expect and timeout of a line the driver leaves alone", async () => {
|
|
128
|
+
const { handler } = handlerFor(
|
|
129
|
+
driverWith(
|
|
130
|
+
terminalWith(async () => {
|
|
131
|
+
return null;
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(
|
|
137
|
+
(
|
|
138
|
+
await handler("list", {
|
|
139
|
+
expect: /players online/,
|
|
140
|
+
timeoutMs: 1000,
|
|
141
|
+
})
|
|
142
|
+
).line,
|
|
143
|
+
).toBe("matched");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("writes the line to stdin when the driver declares no handler", async () => {
|
|
147
|
+
const { handler, written } = handlerFor(
|
|
148
|
+
driverWith({
|
|
149
|
+
kind: BridgeKind.Terminal,
|
|
150
|
+
commands: [],
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await handler("list", {});
|
|
155
|
+
|
|
156
|
+
expect(written).toEqual([
|
|
157
|
+
"list",
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("writes the line to stdin when the game declares no terminal at all", async () => {
|
|
162
|
+
const { handler, written } = handlerFor(driverWith(undefined));
|
|
163
|
+
|
|
164
|
+
await handler("list", {});
|
|
165
|
+
|
|
166
|
+
expect(written).toEqual([
|
|
167
|
+
"list",
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("refuses the line with the driver's own text when the driver rejects it", async () => {
|
|
172
|
+
const refusal = {
|
|
173
|
+
ar: "هذا الأمر يشتغل داخل اللعبة بس.",
|
|
174
|
+
en: "That command only works inside the game.",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const { handler, written } = handlerFor(
|
|
178
|
+
driverWith(
|
|
179
|
+
terminalWith(async () => {
|
|
180
|
+
throw new BridgeUserError(refusal);
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await handler("TeleportToPlayer meslzy", {});
|
|
187
|
+
|
|
188
|
+
expect.unreachable();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
expect(error).toBeInstanceOf(BridgeUserError);
|
|
191
|
+
expect((error as BridgeUserError).text).toEqual(refusal);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
expect(written).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BridgeCommandOptions, BridgeContext, BridgeReply } from "../context";
|
|
2
|
+
import type { BridgeDriver } from "../modules";
|
|
3
|
+
|
|
4
|
+
export type BridgeConsoleHandler = (input: string, options: BridgeCommandOptions) => Promise<BridgeReply>;
|
|
5
|
+
|
|
6
|
+
export interface BridgeConsoleOptions {
|
|
7
|
+
driver: BridgeDriver;
|
|
8
|
+
context: BridgeContext;
|
|
9
|
+
onStdin: BridgeConsoleHandler;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const createConsoleHandler = (options: BridgeConsoleOptions): BridgeConsoleHandler => {
|
|
13
|
+
return async (input, commandOptions) => {
|
|
14
|
+
const run = options.driver.terminal?.run;
|
|
15
|
+
|
|
16
|
+
if (run) {
|
|
17
|
+
const reply = await run(options.context, input);
|
|
18
|
+
|
|
19
|
+
if (reply) {
|
|
20
|
+
return reply;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return await options.onStdin(input, commandOptions);
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
type BridgeField,
|
|
10
10
|
BridgeFormTarget,
|
|
11
11
|
BridgeLayout,
|
|
12
|
+
BridgePlace,
|
|
12
13
|
type BridgeSectionAction,
|
|
13
14
|
type BridgeSectionUpload,
|
|
15
|
+
type BridgeTableSection,
|
|
14
16
|
BridgeUploadMode,
|
|
15
17
|
type BridgeVisibleWhen,
|
|
16
18
|
} from "../panel";
|
|
@@ -1636,3 +1638,146 @@ describe("field patterns", () => {
|
|
|
1636
1638
|
).toThrow("is a secret control with options — a secret is typed, never chosen from a list");
|
|
1637
1639
|
});
|
|
1638
1640
|
});
|
|
1641
|
+
|
|
1642
|
+
describe("section placement", () => {
|
|
1643
|
+
const anyVisibleWhen: BridgeVisibleWhen = {
|
|
1644
|
+
variable: "MODPACK",
|
|
1645
|
+
empty: true,
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
const driverWithRoster = (section: Partial<BridgeTableSection>): BridgeDriver => ({
|
|
1649
|
+
install: {
|
|
1650
|
+
kind: BridgeKind.Install,
|
|
1651
|
+
run: async () => {
|
|
1652
|
+
return;
|
|
1653
|
+
},
|
|
1654
|
+
},
|
|
1655
|
+
lifecycle: {
|
|
1656
|
+
kind: BridgeKind.Lifecycle,
|
|
1657
|
+
command: async () => [
|
|
1658
|
+
"run",
|
|
1659
|
+
],
|
|
1660
|
+
},
|
|
1661
|
+
modules: {
|
|
1662
|
+
players: {
|
|
1663
|
+
kind: BridgeKind.Collection,
|
|
1664
|
+
list: async () => [],
|
|
1665
|
+
actions: {
|
|
1666
|
+
ban: async () => {
|
|
1667
|
+
return;
|
|
1668
|
+
},
|
|
1669
|
+
},
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
panel: {
|
|
1673
|
+
tabs: [
|
|
1674
|
+
{
|
|
1675
|
+
id: "roster",
|
|
1676
|
+
title: text("Roster"),
|
|
1677
|
+
sections: [
|
|
1678
|
+
{
|
|
1679
|
+
layout: BridgeLayout.Table,
|
|
1680
|
+
id: "online",
|
|
1681
|
+
module: "players",
|
|
1682
|
+
columns: [
|
|
1683
|
+
{
|
|
1684
|
+
key: "name",
|
|
1685
|
+
label: text("Name"),
|
|
1686
|
+
},
|
|
1687
|
+
],
|
|
1688
|
+
...section,
|
|
1689
|
+
},
|
|
1690
|
+
],
|
|
1691
|
+
},
|
|
1692
|
+
],
|
|
1693
|
+
},
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
const placedForm = (place: BridgePlace): BridgeDriver => {
|
|
1697
|
+
const driver = driverWith(anyVisibleWhen);
|
|
1698
|
+
|
|
1699
|
+
Object.assign(formSectionOf(driver), {
|
|
1700
|
+
place,
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
return driver;
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
test("accepts a table placed on the players page", () => {
|
|
1707
|
+
expect(() =>
|
|
1708
|
+
validateDriver(
|
|
1709
|
+
driverWithRoster({
|
|
1710
|
+
place: BridgePlace.Players,
|
|
1711
|
+
}),
|
|
1712
|
+
),
|
|
1713
|
+
).not.toThrow();
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
test("rejects a form placed on the players page", () => {
|
|
1717
|
+
expect(() => validateDriver(placedForm(BridgePlace.Players))).toThrow(
|
|
1718
|
+
'tab "version" section "version" places a form section on players, which that page cannot render',
|
|
1719
|
+
);
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
test("rejects a form placed on the overview page", () => {
|
|
1723
|
+
expect(() => validateDriver(placedForm(BridgePlace.Overview))).toThrow(
|
|
1724
|
+
"places a form section on overview, which that page cannot render",
|
|
1725
|
+
);
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
test("rejects a table placed on the overview page", () => {
|
|
1729
|
+
expect(() =>
|
|
1730
|
+
validateDriver(
|
|
1731
|
+
driverWithRoster({
|
|
1732
|
+
place: BridgePlace.Overview,
|
|
1733
|
+
}),
|
|
1734
|
+
),
|
|
1735
|
+
).toThrow('tab "roster" section "online" places a table section on overview, which that page cannot render');
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
test("accepts an offline row action on a table", () => {
|
|
1739
|
+
expect(() =>
|
|
1740
|
+
validateDriver(
|
|
1741
|
+
driverWithRoster({
|
|
1742
|
+
place: BridgePlace.Players,
|
|
1743
|
+
actions: [
|
|
1744
|
+
{
|
|
1745
|
+
id: "ban",
|
|
1746
|
+
label: text("Ban"),
|
|
1747
|
+
offline: true,
|
|
1748
|
+
},
|
|
1749
|
+
],
|
|
1750
|
+
}),
|
|
1751
|
+
),
|
|
1752
|
+
).not.toThrow();
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
test("rejects an offline action on an actions section", () => {
|
|
1756
|
+
expect(() =>
|
|
1757
|
+
validateDriver(
|
|
1758
|
+
driverWithActions([
|
|
1759
|
+
{
|
|
1760
|
+
id: "say",
|
|
1761
|
+
label: text("Say"),
|
|
1762
|
+
offline: true,
|
|
1763
|
+
},
|
|
1764
|
+
]),
|
|
1765
|
+
),
|
|
1766
|
+
).toThrow(
|
|
1767
|
+
'tab "gameplay" section "broadcast" action "say" is offline-only, which only a table or cards row action can be',
|
|
1768
|
+
);
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
test("rejects a form help that is missing a locale", () => {
|
|
1772
|
+
const driver = driverWith(anyVisibleWhen);
|
|
1773
|
+
|
|
1774
|
+
formSectionOf(driver).help = {
|
|
1775
|
+
ar: "",
|
|
1776
|
+
en: "The version the server runs",
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
expect(() => validateDriver(driver)).toThrow(
|
|
1780
|
+
'tab "version" section "version" help needs both arabic and english text',
|
|
1781
|
+
);
|
|
1782
|
+
});
|
|
1783
|
+
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type BridgeField,
|
|
14
14
|
BridgeFormTarget,
|
|
15
15
|
BridgeLayout,
|
|
16
|
+
BridgePlace,
|
|
16
17
|
type BridgeSection,
|
|
17
18
|
type BridgeSectionAction,
|
|
18
19
|
type BridgeTab,
|
|
@@ -333,12 +334,53 @@ const validateActions = (modules: Record<string, BridgePanelModule>, actions: Br
|
|
|
333
334
|
}
|
|
334
335
|
};
|
|
335
336
|
|
|
337
|
+
const PLACE_LAYOUTS: Record<BridgePlace, BridgeLayout[]> = {
|
|
338
|
+
[BridgePlace.Overview]: [
|
|
339
|
+
BridgeLayout.Actions,
|
|
340
|
+
BridgeLayout.Detail,
|
|
341
|
+
],
|
|
342
|
+
[BridgePlace.Players]: [
|
|
343
|
+
BridgeLayout.Actions,
|
|
344
|
+
BridgeLayout.Cards,
|
|
345
|
+
BridgeLayout.Table,
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const validatePlace = (section: BridgeSection, where: string) => {
|
|
350
|
+
const place = "place" in section ? section.place : undefined;
|
|
351
|
+
|
|
352
|
+
if (place === undefined) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!(PLACE_LAYOUTS[place] ?? []).includes(section.layout)) {
|
|
357
|
+
fail(`${where} places a ${section.layout} section on ${place}, which that page cannot render`);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const validateOffline = (section: BridgeSection, where: string) => {
|
|
362
|
+
if (section.layout === BridgeLayout.Table || section.layout === BridgeLayout.Cards) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const actions = "actions" in section ? (section.actions ?? []) : [];
|
|
367
|
+
|
|
368
|
+
for (const action of actions) {
|
|
369
|
+
if (action.offline !== undefined) {
|
|
370
|
+
fail(`${where} action "${action.id}" is offline-only, which only a table or cards row action can be`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
336
375
|
const validateSection = (modules: Record<string, BridgePanelModule>, section: BridgeSection, where: string) => {
|
|
337
376
|
validateVisibleWhen(section.visibleWhen, where);
|
|
338
377
|
validateText(section.title, "title", where);
|
|
378
|
+
validateText(section.help, "help", where);
|
|
379
|
+
validatePlace(section, where);
|
|
380
|
+
validateOffline(section, where);
|
|
339
381
|
|
|
340
|
-
if (section.layout !== BridgeLayout.Form) {
|
|
341
|
-
validateText(section.
|
|
382
|
+
if (section.layout !== BridgeLayout.Form && section.layout !== BridgeLayout.Actions) {
|
|
383
|
+
validateText(section.empty, "empty text", where);
|
|
342
384
|
}
|
|
343
385
|
|
|
344
386
|
switch (section.layout) {
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { BridgeRcons } from "../context";
|
|
2
|
-
import {
|
|
2
|
+
import { RconClient } from "../rcon";
|
|
3
3
|
|
|
4
4
|
const RCON_HOST = "127.0.0.1";
|
|
5
5
|
|
|
6
6
|
export const createBridgeRcons = (): BridgeRcons => {
|
|
7
7
|
return {
|
|
8
8
|
async source(options) {
|
|
9
|
-
|
|
9
|
+
const client = new RconClient({
|
|
10
10
|
...options,
|
|
11
11
|
host: RCON_HOST,
|
|
12
12
|
});
|
|
13
|
+
|
|
14
|
+
await client.connect();
|
|
15
|
+
|
|
16
|
+
return client;
|
|
13
17
|
},
|
|
14
18
|
};
|
|
15
19
|
};
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { BridgeAction, BridgeKind, type BridgeRow, BridgeSetupStepKind, type BridgeValues } from "../core";
|
|
4
4
|
import type { BridgeDriver } from "../modules";
|
|
5
|
+
import { BridgeLayout } from "../panel";
|
|
5
6
|
import { type BridgeConfig, BridgeConfirm, BridgeEventName } from "../protocol";
|
|
6
7
|
import { createContext } from "./bridgeContext";
|
|
7
8
|
import { createEventRing } from "./bridgeEvents";
|
|
@@ -296,6 +297,101 @@ describe("collection row actions", () => {
|
|
|
296
297
|
});
|
|
297
298
|
});
|
|
298
299
|
|
|
300
|
+
describe("offline row actions", () => {
|
|
301
|
+
const rosterDriver = (
|
|
302
|
+
offline: boolean,
|
|
303
|
+
list: () => Promise<BridgeRow[]>,
|
|
304
|
+
ban: (row: BridgeRow) => void,
|
|
305
|
+
): BridgeDriver => ({
|
|
306
|
+
...baseDriver(),
|
|
307
|
+
panel: {
|
|
308
|
+
tabs: [
|
|
309
|
+
{
|
|
310
|
+
id: "players",
|
|
311
|
+
title: {
|
|
312
|
+
ar: "اللاعبين",
|
|
313
|
+
en: "Players",
|
|
314
|
+
},
|
|
315
|
+
sections: [
|
|
316
|
+
{
|
|
317
|
+
layout: BridgeLayout.Table,
|
|
318
|
+
id: "online",
|
|
319
|
+
module: "players",
|
|
320
|
+
columns: [
|
|
321
|
+
{
|
|
322
|
+
key: "name",
|
|
323
|
+
label: {
|
|
324
|
+
ar: "الاسم",
|
|
325
|
+
en: "Name",
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
actions: [
|
|
330
|
+
{
|
|
331
|
+
id: "ban",
|
|
332
|
+
label: {
|
|
333
|
+
ar: "حظر",
|
|
334
|
+
en: "Ban",
|
|
335
|
+
},
|
|
336
|
+
offline,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
modules: {
|
|
345
|
+
players: {
|
|
346
|
+
kind: BridgeKind.Collection,
|
|
347
|
+
list,
|
|
348
|
+
actions: {
|
|
349
|
+
ban: async (_context, row) => {
|
|
350
|
+
ban(row);
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("hands an offline action the id alone when the row is not listed", async () => {
|
|
358
|
+
const seen: BridgeRow[] = [];
|
|
359
|
+
const registry = registryFor(
|
|
360
|
+
rosterDriver(
|
|
361
|
+
true,
|
|
362
|
+
async () => [],
|
|
363
|
+
(row) => seen.push(row),
|
|
364
|
+
),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
await registry.invoke("players", "ban", {
|
|
368
|
+
id: "steam_1",
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(seen).toEqual([
|
|
372
|
+
{
|
|
373
|
+
id: "steam_1",
|
|
374
|
+
},
|
|
375
|
+
]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("still refuses an unlisted row for an action that is not offline", async () => {
|
|
379
|
+
const registry = registryFor(
|
|
380
|
+
rosterDriver(
|
|
381
|
+
false,
|
|
382
|
+
async () => [],
|
|
383
|
+
() => undefined,
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
await expect(
|
|
388
|
+
registry.invoke("players", "ban", {
|
|
389
|
+
id: "steam_1",
|
|
390
|
+
}),
|
|
391
|
+
).rejects.toThrow('module "players" has no row "steam_1"');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
299
395
|
describe("preview modules", () => {
|
|
300
396
|
test("lists the preview action in the manifest", () => {
|
|
301
397
|
const registry = registryFor({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BridgeContext } from "../context";
|
|
2
2
|
import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues, isReadAction } from "../core";
|
|
3
3
|
import type { BridgeDriver, BridgePanelModule, BridgeSetupModule, BridgeTerminalModule } from "../modules";
|
|
4
|
+
import { BridgeLayout } from "../panel";
|
|
4
5
|
import {
|
|
5
6
|
BRIDGE_VERSION,
|
|
6
7
|
type BridgeManifest,
|
|
@@ -148,6 +149,29 @@ const asPage = (args: BridgeValues) => {
|
|
|
148
149
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
149
150
|
};
|
|
150
151
|
|
|
152
|
+
const offlineActionsOf = (driver: BridgeDriver): Map<string, Set<string>> => {
|
|
153
|
+
const offline = new Map<string, Set<string>>();
|
|
154
|
+
|
|
155
|
+
for (const tab of driver.panel?.tabs ?? []) {
|
|
156
|
+
for (const section of tab.sections) {
|
|
157
|
+
if (section.layout !== BridgeLayout.Table && section.layout !== BridgeLayout.Cards) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const action of section.actions ?? []) {
|
|
162
|
+
if (action.offline === true) {
|
|
163
|
+
const actions = offline.get(section.module) ?? new Set<string>();
|
|
164
|
+
|
|
165
|
+
actions.add(action.id);
|
|
166
|
+
offline.set(section.module, actions);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return offline;
|
|
173
|
+
};
|
|
174
|
+
|
|
151
175
|
export const createRegistry = (
|
|
152
176
|
driver: BridgeDriver,
|
|
153
177
|
context: BridgeContext,
|
|
@@ -157,6 +181,7 @@ export const createRegistry = (
|
|
|
157
181
|
const cache = new Map<string, CacheEntry>();
|
|
158
182
|
const inflight = new Map<string, Promise<unknown>>();
|
|
159
183
|
const modules = driver.modules ?? {};
|
|
184
|
+
const offlineActions = offlineActionsOf(driver);
|
|
160
185
|
|
|
161
186
|
for (const [id, module] of Object.entries(modules)) {
|
|
162
187
|
if (module.kind !== BridgeKind.Actions && module.kind !== BridgeKind.Detail) {
|
|
@@ -297,7 +322,14 @@ export const createRegistry = (
|
|
|
297
322
|
if (rowAction) {
|
|
298
323
|
const rows = await module.list(context);
|
|
299
324
|
const id = asString(args, "id");
|
|
300
|
-
const
|
|
325
|
+
const listed = rows.find((entry) => entry.id === id);
|
|
326
|
+
const row =
|
|
327
|
+
listed
|
|
328
|
+
?? (offlineActions.get(name)?.has(action)
|
|
329
|
+
? {
|
|
330
|
+
id,
|
|
331
|
+
}
|
|
332
|
+
: undefined);
|
|
301
333
|
|
|
302
334
|
if (!row) {
|
|
303
335
|
throw new BridgeDriverError(`module "${name}" has no row "${id}"`);
|
package/src/runtime/serve.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type BridgeStatus,
|
|
11
11
|
} from "../protocol";
|
|
12
12
|
import { configPath, readConfig, socketPath, statePath } from "./bridgeConfigReader";
|
|
13
|
+
import { createConsoleHandler } from "./bridgeConsoleHandler";
|
|
13
14
|
import { createContext } from "./bridgeContext";
|
|
14
15
|
import { validateDriver } from "./bridgeDriver";
|
|
15
16
|
import { createEventRing } from "./bridgeEvents";
|
|
@@ -111,6 +112,14 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
111
112
|
|
|
112
113
|
const registry = createRegistry(driver, context, supervisor, ring);
|
|
113
114
|
|
|
115
|
+
const runConsole = createConsoleHandler({
|
|
116
|
+
driver,
|
|
117
|
+
context,
|
|
118
|
+
async onStdin(input, commandOptions) {
|
|
119
|
+
return await supervisor.command(input, commandOptions);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
114
123
|
let current = desired;
|
|
115
124
|
|
|
116
125
|
const status = (): BridgeStatus => {
|
|
@@ -205,7 +214,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
205
214
|
await supervisor.stop();
|
|
206
215
|
},
|
|
207
216
|
async onCommand(input, expect, timeoutMs) {
|
|
208
|
-
return await
|
|
217
|
+
return await runConsole(input, {
|
|
209
218
|
expect: expect === undefined ? undefined : new RegExp(expect),
|
|
210
219
|
timeoutMs,
|
|
211
220
|
});
|