@serverkgg/bridge 0.0.21 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/bot/botDeploy.test.ts +363 -0
- package/src/bot/botDeploy.ts +136 -0
- package/src/bot/botEnvironment.test.ts +189 -0
- package/src/bot/botEnvironment.ts +141 -0
- package/src/bot/botIdentity.test.ts +251 -0
- package/src/bot/botIdentity.ts +140 -0
- package/src/bot/botPanel.test.ts +246 -0
- package/src/bot/botPanel.ts +129 -0
- package/src/bot/botState.test.ts +45 -0
- package/src/bot/botState.ts +54 -0
- package/src/bot/botTerminal.test.ts +79 -0
- package/src/bot/botTerminal.ts +62 -0
- package/src/bot/botVariables.test.ts +193 -0
- package/src/bot/botVariables.ts +73 -0
- package/src/bot/detect/botFramework.ts +23 -0
- package/src/bot/detect/botFrameworks.ts +198 -0
- package/src/bot/detect/botInspectWarning.ts +15 -0
- package/src/bot/detect/botPackageManager.ts +6 -0
- package/src/bot/detect/botPlatform.ts +10 -0
- package/src/bot/detect/botRuntime.ts +15 -0
- package/src/bot/detect/botToken.test.ts +71 -0
- package/src/bot/detect/botToken.ts +126 -0
- package/src/bot/detect/detect.test.ts +543 -0
- package/src/bot/detect/detect.ts +842 -0
- package/src/bot/detect/detectResult.ts +40 -0
- package/src/bot/detect/detectTable.test.ts +72 -0
- package/src/bot/detect/detectTable.ts +166 -0
- package/src/bot/detect/envExample.test.ts +29 -0
- package/src/bot/detect/envExample.ts +48 -0
- package/src/bot/detect/envKey.ts +7 -0
- package/src/bot/detect/envScan.test.ts +39 -0
- package/src/bot/detect/envScan.ts +87 -0
- package/src/bot/detect/index.ts +17 -0
- package/src/bot/detect/inferBotPlatform.test.ts +60 -0
- package/src/bot/detect/inferBotPlatform.ts +28 -0
- package/src/bot/detect/keys.test.ts +28 -0
- package/src/bot/detect/keys.ts +64 -0
- package/src/bot/detect/sensitiveKey.test.ts +91 -0
- package/src/bot/detect/sensitiveKey.ts +44 -0
- package/src/bot/detect/serverkJson.test.ts +88 -0
- package/src/bot/detect/serverkJson.ts +174 -0
- package/src/bot/index.ts +8 -0
- package/src/cli/detectTable.test.ts +66 -0
- package/src/cli/detectTable.ts +43 -0
- package/src/cli/main.ts +12 -4
- package/src/cli/renderedFile.ts +35 -0
- package/src/cli/schema.ts +7 -26
- package/src/cli/validate.test.ts +283 -3
- package/src/cli/validate.ts +105 -7
- package/src/context/bridgeContext.ts +17 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgePort.test.ts +45 -0
- package/src/core/bridgePort.ts +26 -0
- package/src/core/bridgeUserError.test.ts +10 -0
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/guides/compile.ts +2 -2
- package/src/manifest/index.ts +1 -0
- package/src/manifest/kind.ts +5 -0
- package/src/manifest/manifest.test.ts +630 -3
- package/src/manifest/manifest.ts +391 -87
- package/src/manifest/runtime.ts +2 -2
- package/src/manifest/source.ts +4 -4
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeLifecycleSubject.ts +8 -0
- package/src/modules/bridgeModule.ts +19 -1
- package/src/modules/bridgeStopSignal.ts +13 -0
- package/src/modules/index.ts +2 -0
- package/src/panel/bridgeDetail.ts +4 -1
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeConfig.ts +2 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeConfigReader.test.ts +80 -0
- package/src/runtime/bridgeConfigReader.ts +20 -0
- package/src/runtime/bridgeContext.test.ts +247 -0
- package/src/runtime/bridgeContext.ts +43 -10
- package/src/runtime/bridgeDriver.test.ts +153 -1
- package/src/runtime/bridgeDriver.ts +64 -8
- package/src/runtime/bridgeFiles.test.ts +62 -0
- package/src/runtime/bridgeFiles.ts +12 -2
- package/src/runtime/bridgeLiveConfig.ts +18 -0
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgeRedaction.test.ts +410 -0
- package/src/runtime/bridgeRedaction.ts +214 -0
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1056 -10
- package/src/runtime/bridgeRegistry.ts +464 -50
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +534 -4
- package/src/runtime/bridgeServer.ts +145 -38
- package/src/runtime/bridgeSetupHandlers.test.ts +156 -5
- package/src/runtime/bridgeSetupHandlers.ts +59 -14
- package/src/runtime/bridgeSupervisor.test.ts +670 -2
- package/src/runtime/bridgeSupervisor.ts +219 -67
- package/src/runtime/serve.ts +87 -11
- package/src/steam/steamcmd.test.ts +32 -1
- package/src/steam/steamcmd.ts +27 -4
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +67 -1
- package/src/wire/panel.ts +11 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BridgeKind, BridgeUserError, type BridgeValues } from "../core";
|
|
3
|
+
import { BridgeLayout, BridgePlace } from "../panel";
|
|
4
|
+
import { bridgePanelSchema } from "../wire";
|
|
5
|
+
import { DEPLOY_MARKER } from "./botDeploy";
|
|
6
|
+
import {
|
|
7
|
+
BOT_DEPLOY_MODULE,
|
|
8
|
+
BOT_IDENTITY_MODULE,
|
|
9
|
+
BOT_IDENTITY_REFRESH_SECONDS,
|
|
10
|
+
BOT_TAB,
|
|
11
|
+
BotDeployAction,
|
|
12
|
+
BotSection,
|
|
13
|
+
createBotDeployModule,
|
|
14
|
+
createBotIdentityModule,
|
|
15
|
+
createBotPanel,
|
|
16
|
+
deployRequestOf,
|
|
17
|
+
} from "./botPanel";
|
|
18
|
+
|
|
19
|
+
const SHA = "0123456789abcdef0123456789abcdef01234567";
|
|
20
|
+
|
|
21
|
+
describe("createBotPanel", () => {
|
|
22
|
+
test("is one bot tab holding only the identity card on the overview", () => {
|
|
23
|
+
const tab = createBotPanel();
|
|
24
|
+
|
|
25
|
+
expect(tab.id).toBe(BOT_TAB);
|
|
26
|
+
expect(tab.sections).toHaveLength(1);
|
|
27
|
+
|
|
28
|
+
const [identity] = tab.sections;
|
|
29
|
+
|
|
30
|
+
expect(identity?.id).toBe(BotSection.Identity);
|
|
31
|
+
expect(identity?.layout).toBe(BridgeLayout.Detail);
|
|
32
|
+
expect(identity?.layout === BridgeLayout.Detail && identity.place).toBe(BridgePlace.Overview);
|
|
33
|
+
expect(identity?.layout === BridgeLayout.Detail && identity.module).toBe(BOT_IDENTITY_MODULE);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("parses through the panel mirror the agent and the api read", () => {
|
|
37
|
+
expect(
|
|
38
|
+
bridgePanelSchema.safeParse({
|
|
39
|
+
tabs: [
|
|
40
|
+
createBotPanel(),
|
|
41
|
+
],
|
|
42
|
+
}).success,
|
|
43
|
+
).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("names the deploy module the platform invokes", () => {
|
|
47
|
+
expect(BOT_DEPLOY_MODULE).toBe("botDeploy");
|
|
48
|
+
expect(Object.values(BotDeployAction).map(String)).toEqual([
|
|
49
|
+
"redeploy",
|
|
50
|
+
"reinstall",
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("deployRequestOf", () => {
|
|
56
|
+
test("takes the deployment, the pinned ref and the reinstall flag from the invoke values", () => {
|
|
57
|
+
expect(
|
|
58
|
+
deployRequestOf(
|
|
59
|
+
{
|
|
60
|
+
deployment: "dep_a1b2c3d4",
|
|
61
|
+
ref: SHA,
|
|
62
|
+
reinstall: true,
|
|
63
|
+
},
|
|
64
|
+
false,
|
|
65
|
+
),
|
|
66
|
+
).toEqual({
|
|
67
|
+
deployment: "dep_a1b2c3d4",
|
|
68
|
+
ref: SHA,
|
|
69
|
+
snapshot: null,
|
|
70
|
+
reinstall: true,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("treats a bare press as a redeploy of whatever is configured", () => {
|
|
75
|
+
expect(deployRequestOf({}, false)).toEqual({
|
|
76
|
+
deployment: null,
|
|
77
|
+
ref: null,
|
|
78
|
+
snapshot: null,
|
|
79
|
+
reinstall: false,
|
|
80
|
+
});
|
|
81
|
+
expect(deployRequestOf({}, true).reinstall).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("refuses a short ref, a bad id and a ref beside a snapshot", () => {
|
|
85
|
+
const refusals: BridgeValues[] = [
|
|
86
|
+
{
|
|
87
|
+
ref: "0123456",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
ref: SHA.toUpperCase(),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
deployment: "dep a1",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
snapshot: "../snap",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
ref: SHA,
|
|
100
|
+
snapshot: "snap_1",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
deployment: 7,
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const args of refusals) {
|
|
108
|
+
expect(() => deployRequestOf(args, false)).toThrow(BridgeUserError);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("the bot modules", () => {
|
|
114
|
+
test("read the identity while the bot is stopped and refresh it every minute", () => {
|
|
115
|
+
const read = async () => null;
|
|
116
|
+
|
|
117
|
+
expect(createBotIdentityModule(read)).toEqual({
|
|
118
|
+
kind: BridgeKind.Detail,
|
|
119
|
+
requiresRunning: false,
|
|
120
|
+
refreshSeconds: BOT_IDENTITY_REFRESH_SECONDS,
|
|
121
|
+
read,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const deployContext = (running: boolean, markers: string[] = []) => {
|
|
126
|
+
const stored = new Map<string, string>(
|
|
127
|
+
markers.map((marker) => [
|
|
128
|
+
marker,
|
|
129
|
+
"{}",
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
132
|
+
let restarts = 0;
|
|
133
|
+
|
|
134
|
+
const context = {
|
|
135
|
+
server: {
|
|
136
|
+
code: "abc",
|
|
137
|
+
memoryMb: 512,
|
|
138
|
+
cpuPercent: 100,
|
|
139
|
+
running,
|
|
140
|
+
publicAddress: null,
|
|
141
|
+
},
|
|
142
|
+
files: {
|
|
143
|
+
async write(path: string, content: string) {
|
|
144
|
+
stored.set(path, content);
|
|
145
|
+
},
|
|
146
|
+
async remove(path: string) {
|
|
147
|
+
stored.delete(path);
|
|
148
|
+
},
|
|
149
|
+
async move(from: string, to: string) {
|
|
150
|
+
stored.set(to, stored.get(from) ?? "");
|
|
151
|
+
stored.delete(from);
|
|
152
|
+
},
|
|
153
|
+
async exists(path: string) {
|
|
154
|
+
return stored.has(path);
|
|
155
|
+
},
|
|
156
|
+
async read(path: string) {
|
|
157
|
+
return stored.get(path) ?? "";
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
log: Object.assign(() => {}, {
|
|
161
|
+
warn: () => {},
|
|
162
|
+
error: () => {},
|
|
163
|
+
}),
|
|
164
|
+
emit() {},
|
|
165
|
+
async restart() {
|
|
166
|
+
restarts += 1;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
context: context as never,
|
|
172
|
+
stored,
|
|
173
|
+
restarts: () => restarts,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
test("deploy while the bot is stopped too, so the platform can start the new code", async () => {
|
|
178
|
+
const module = createBotDeployModule();
|
|
179
|
+
const { context, stored, restarts } = deployContext(false);
|
|
180
|
+
|
|
181
|
+
expect(module.requiresRunning).toBe(false);
|
|
182
|
+
|
|
183
|
+
await module.actions[BotDeployAction.Redeploy]?.(context, {
|
|
184
|
+
deployment: "dep_a1b2c3d4",
|
|
185
|
+
ref: SHA,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(JSON.parse(stored.get(DEPLOY_MARKER) ?? "{}")).toMatchObject({
|
|
189
|
+
deployment: "dep_a1b2c3d4",
|
|
190
|
+
ref: SHA,
|
|
191
|
+
reinstall: false,
|
|
192
|
+
});
|
|
193
|
+
expect(restarts()).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("reinstall forces the flag and restarts a running bot", async () => {
|
|
197
|
+
const module = createBotDeployModule();
|
|
198
|
+
const { context, stored, restarts } = deployContext(true);
|
|
199
|
+
|
|
200
|
+
await module.actions[BotDeployAction.Reinstall]?.(context, {});
|
|
201
|
+
|
|
202
|
+
expect(JSON.parse(stored.get(DEPLOY_MARKER) ?? "{}")).toMatchObject({
|
|
203
|
+
reinstall: true,
|
|
204
|
+
});
|
|
205
|
+
expect(restarts()).toBe(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("refuses a second press while a running bot has not taken the first", async () => {
|
|
209
|
+
const module = createBotDeployModule();
|
|
210
|
+
const { context } = deployContext(true, [
|
|
211
|
+
DEPLOY_MARKER,
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
await expect(module.actions[BotDeployAction.Redeploy]?.(context, {})).rejects.toThrow(BridgeUserError);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("greys out both deploy buttons with the refusal's words while a running bot has not taken a deploy", async () => {
|
|
218
|
+
const module = createBotDeployModule();
|
|
219
|
+
const pending = deployContext(true, [
|
|
220
|
+
DEPLOY_MARKER,
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const unavailable = await module.unavailable?.(pending.context);
|
|
224
|
+
|
|
225
|
+
expect(Object.keys(unavailable ?? {}).sort()).toEqual([
|
|
226
|
+
BotDeployAction.Redeploy,
|
|
227
|
+
BotDeployAction.Reinstall,
|
|
228
|
+
]);
|
|
229
|
+
await expect(module.actions[BotDeployAction.Redeploy]?.(pending.context, {})).rejects.toThrow(
|
|
230
|
+
unavailable?.[BotDeployAction.Redeploy]?.en,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("offers both deploy buttons when nothing is pending or the bot is stopped", async () => {
|
|
235
|
+
const module = createBotDeployModule();
|
|
236
|
+
|
|
237
|
+
expect(await module.unavailable?.(deployContext(true).context)).toEqual({});
|
|
238
|
+
expect(
|
|
239
|
+
await module.unavailable?.(
|
|
240
|
+
deployContext(false, [
|
|
241
|
+
DEPLOY_MARKER,
|
|
242
|
+
]).context,
|
|
243
|
+
),
|
|
244
|
+
).toEqual({});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { BridgeContext } from "../context";
|
|
2
|
+
import { BridgeKind, type BridgeUnavailable, BridgeUserError, type BridgeValues, type LocalizedText } from "../core";
|
|
3
|
+
import type { BridgeActionsModule, BridgeDetailModule } from "../modules";
|
|
4
|
+
import { BridgeIcon, BridgeLayout, BridgePlace, type BridgeTab } from "../panel";
|
|
5
|
+
import { type DeployRequest, deployPending, requestDeploy } from "./botDeploy";
|
|
6
|
+
import { DEPLOYMENT_ID_PATTERN, REPO_REF_PATTERN, SNAPSHOT_ID_PATTERN } from "./botVariables";
|
|
7
|
+
|
|
8
|
+
export const BOT_TAB = "bot";
|
|
9
|
+
|
|
10
|
+
export enum BotSection {
|
|
11
|
+
Identity = "identity",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const BOT_IDENTITY_MODULE = "botIdentity";
|
|
15
|
+
|
|
16
|
+
export const BOT_DEPLOY_MODULE = "botDeploy";
|
|
17
|
+
|
|
18
|
+
export enum BotDeployAction {
|
|
19
|
+
Redeploy = "redeploy",
|
|
20
|
+
Reinstall = "reinstall",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const BOT_IDENTITY_REFRESH_SECONDS = 60;
|
|
24
|
+
|
|
25
|
+
const DEPLOY_PENDING: LocalizedText = {
|
|
26
|
+
ar: "فيه نشر شغّال الحين. انتظر لين يخلص وجرّب بعدها.",
|
|
27
|
+
en: "A deploy is already running. Wait for it to finish, then try again.",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const INVALID_DEPLOY: LocalizedText = {
|
|
31
|
+
ar: "طلب النشر هذا ناقص أو غلط. جرّب النشر من صفحة البوت.",
|
|
32
|
+
en: "This deploy request is incomplete or malformed. Try deploying from the bot page.",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const createBotPanel = (): BridgeTab => {
|
|
36
|
+
return {
|
|
37
|
+
id: BOT_TAB,
|
|
38
|
+
icon: BridgeIcon.Command,
|
|
39
|
+
title: {
|
|
40
|
+
ar: "البوت",
|
|
41
|
+
en: "Bot",
|
|
42
|
+
},
|
|
43
|
+
sections: [
|
|
44
|
+
{
|
|
45
|
+
layout: BridgeLayout.Detail,
|
|
46
|
+
id: BotSection.Identity,
|
|
47
|
+
place: BridgePlace.Overview,
|
|
48
|
+
module: BOT_IDENTITY_MODULE,
|
|
49
|
+
title: {
|
|
50
|
+
ar: "بوتك",
|
|
51
|
+
en: "Your bot",
|
|
52
|
+
},
|
|
53
|
+
empty: {
|
|
54
|
+
ar: "بنعرف بوتك أول ما يشتغل ويسجّل دخوله.",
|
|
55
|
+
en: "We learn who your bot is once it runs and logs in.",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const createBotIdentityModule = (read: BridgeDetailModule["read"]): BridgeDetailModule => {
|
|
63
|
+
return {
|
|
64
|
+
kind: BridgeKind.Detail,
|
|
65
|
+
requiresRunning: false,
|
|
66
|
+
refreshSeconds: BOT_IDENTITY_REFRESH_SECONDS,
|
|
67
|
+
read,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const optionalText = (args: BridgeValues, key: string, pattern: RegExp) => {
|
|
72
|
+
const value = args[key];
|
|
73
|
+
|
|
74
|
+
if (value === undefined || value === null || value === "") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof value !== "string" || !pattern.test(value)) {
|
|
79
|
+
throw new BridgeUserError(INVALID_DEPLOY);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return value;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const deployRequestOf = (args: BridgeValues, reinstall: boolean): DeployRequest => {
|
|
86
|
+
const request: DeployRequest = {
|
|
87
|
+
deployment: optionalText(args, "deployment", DEPLOYMENT_ID_PATTERN),
|
|
88
|
+
ref: optionalText(args, "ref", REPO_REF_PATTERN),
|
|
89
|
+
snapshot: optionalText(args, "snapshot", SNAPSHOT_ID_PATTERN),
|
|
90
|
+
reinstall: reinstall || args.reinstall === true,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (request.ref !== null && request.snapshot !== null) {
|
|
94
|
+
throw new BridgeUserError(INVALID_DEPLOY);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return request;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const deploy = (reinstall: boolean) => {
|
|
101
|
+
return async (context: BridgeContext, args: BridgeValues) => {
|
|
102
|
+
const request = deployRequestOf(args, reinstall);
|
|
103
|
+
|
|
104
|
+
if (await deployPending(context)) {
|
|
105
|
+
throw new BridgeUserError(DEPLOY_PENDING);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await requestDeploy(context, request);
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const createBotDeployModule = (): BridgeActionsModule => {
|
|
113
|
+
return {
|
|
114
|
+
kind: BridgeKind.Actions,
|
|
115
|
+
requiresRunning: false,
|
|
116
|
+
actions: {
|
|
117
|
+
[BotDeployAction.Redeploy]: deploy(false),
|
|
118
|
+
[BotDeployAction.Reinstall]: deploy(true),
|
|
119
|
+
},
|
|
120
|
+
async unavailable(context): Promise<BridgeUnavailable> {
|
|
121
|
+
return (await deployPending(context))
|
|
122
|
+
? {
|
|
123
|
+
[BotDeployAction.Redeploy]: DEPLOY_PENDING,
|
|
124
|
+
[BotDeployAction.Reinstall]: DEPLOY_PENDING,
|
|
125
|
+
}
|
|
126
|
+
: {};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BotDeploymentOutcome, BotDeploymentSource, DEPLOYMENT_STATE_FILE, readDeploymentState } from "./botState";
|
|
3
|
+
|
|
4
|
+
const filesWith = (content: string | null) => {
|
|
5
|
+
return {
|
|
6
|
+
async exists(path: string) {
|
|
7
|
+
return content !== null && path === DEPLOYMENT_STATE_FILE;
|
|
8
|
+
},
|
|
9
|
+
async read() {
|
|
10
|
+
return content ?? "";
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("readDeploymentState", () => {
|
|
16
|
+
test("reads what the launcher wrote", async () => {
|
|
17
|
+
const state = {
|
|
18
|
+
deployment: "dep_a1b2c3d4",
|
|
19
|
+
source: BotDeploymentSource.Snapshot,
|
|
20
|
+
ref: null,
|
|
21
|
+
commit: "abc1234abc1234abc1234abc1234abc1234abc12",
|
|
22
|
+
snapshot: "snap_1",
|
|
23
|
+
outcome: BotDeploymentOutcome.Ready,
|
|
24
|
+
stage: null,
|
|
25
|
+
detail: null,
|
|
26
|
+
finishedAt: "2026-09-27T10:00:00.000Z",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
expect(await readDeploymentState(filesWith(JSON.stringify(state)))).toEqual(state);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("answers null for a missing, broken or foreign file", async () => {
|
|
33
|
+
expect(await readDeploymentState(filesWith(null))).toBeNull();
|
|
34
|
+
expect(await readDeploymentState(filesWith("{"))).toBeNull();
|
|
35
|
+
expect(
|
|
36
|
+
await readDeploymentState(
|
|
37
|
+
filesWith(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
deployment: 1,
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { BridgeFiles } from "../context";
|
|
3
|
+
|
|
4
|
+
export const BOT_STATE_DIR = ".serverk";
|
|
5
|
+
|
|
6
|
+
export const DEPLOYMENT_STATE_FILE = `${BOT_STATE_DIR}/deployment.json`;
|
|
7
|
+
|
|
8
|
+
export enum BotDeploymentSource {
|
|
9
|
+
Git = "git",
|
|
10
|
+
Snapshot = "snapshot",
|
|
11
|
+
Local = "local",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export enum BotDeploymentOutcome {
|
|
15
|
+
Ready = "ready",
|
|
16
|
+
Failed = "failed",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export enum BotDeploymentStage {
|
|
20
|
+
Sync = "sync",
|
|
21
|
+
Snapshot = "snapshot",
|
|
22
|
+
Install = "install",
|
|
23
|
+
Start = "start",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const botDeploymentStateSchema = z.object({
|
|
27
|
+
deployment: z.string().nullable(),
|
|
28
|
+
source: z.enum(BotDeploymentSource),
|
|
29
|
+
ref: z.string().nullable(),
|
|
30
|
+
commit: z.string().nullable(),
|
|
31
|
+
snapshot: z.string().nullable(),
|
|
32
|
+
outcome: z.enum(BotDeploymentOutcome),
|
|
33
|
+
stage: z.enum(BotDeploymentStage).nullable(),
|
|
34
|
+
detail: z.string().nullable(),
|
|
35
|
+
finishedAt: z.string(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type BotDeploymentState = z.infer<typeof botDeploymentStateSchema>;
|
|
39
|
+
|
|
40
|
+
export const readDeploymentState = async (
|
|
41
|
+
files: Pick<BridgeFiles, "exists" | "read">,
|
|
42
|
+
): Promise<BotDeploymentState | null> => {
|
|
43
|
+
if (!(await files.exists(DEPLOYMENT_STATE_FILE))) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const parsed = botDeploymentStateSchema.safeParse(JSON.parse(await files.read(DEPLOYMENT_STATE_FILE)));
|
|
49
|
+
|
|
50
|
+
return parsed.success ? parsed.data : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BridgeTerminalLevel, type BridgeTerminalRule } from "../terminal";
|
|
3
|
+
import { bridgeTerminalRuleSchema } from "../wire";
|
|
4
|
+
import { LAUNCHER_RULES, NODE_RULES, PYTHON_RULES } from "./botTerminal";
|
|
5
|
+
|
|
6
|
+
const levelOf = (rules: BridgeTerminalRule[], line: string) => {
|
|
7
|
+
return rules.find((rule) => rule.match.test(line))?.level ?? null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("the bot terminal rules", () => {
|
|
11
|
+
test("colour the launcher's own lines, failures as errors", () => {
|
|
12
|
+
expect(levelOf(LAUNCHER_RULES, "serverk: install failed: npm ci exited 1")).toBe(BridgeTerminalLevel.Error);
|
|
13
|
+
expect(levelOf(LAUNCHER_RULES, "serverk: start failed: no bot token yet — open Setup")).toBe(
|
|
14
|
+
BridgeTerminalLevel.Error,
|
|
15
|
+
);
|
|
16
|
+
expect(levelOf(LAUNCHER_RULES, "serverk: bot process starting")).toBe(BridgeTerminalLevel.Info);
|
|
17
|
+
expect(levelOf(LAUNCHER_RULES, "Logged in as helper#0001")).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("catch node errors, stack frames and warnings", () => {
|
|
21
|
+
expect(levelOf(NODE_RULES, "TypeError: Cannot read properties of undefined (reading 'id')")).toBe(
|
|
22
|
+
BridgeTerminalLevel.Error,
|
|
23
|
+
);
|
|
24
|
+
expect(levelOf(NODE_RULES, "DiscordAPIError[50001]: Missing Access")).toBe(BridgeTerminalLevel.Error);
|
|
25
|
+
expect(levelOf(NODE_RULES, " at Client.<anonymous> (/home/container/index.js:12:7)")).toBe(
|
|
26
|
+
BridgeTerminalLevel.Error,
|
|
27
|
+
);
|
|
28
|
+
expect(levelOf(NODE_RULES, "npm error code ERESOLVE")).toBe(BridgeTerminalLevel.Error);
|
|
29
|
+
expect(levelOf(NODE_RULES, "(node:42) DeprecationWarning: ready is deprecated")).toBe(BridgeTerminalLevel.Warn);
|
|
30
|
+
expect(levelOf(NODE_RULES, "npm warn deprecated inflight@1.0.6")).toBe(BridgeTerminalLevel.Warn);
|
|
31
|
+
expect(levelOf(NODE_RULES, "Ready! Logged in as helper")).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("catch python tracebacks, exceptions, log levels and warnings", () => {
|
|
35
|
+
expect(levelOf(PYTHON_RULES, "Traceback (most recent call last):")).toBe(BridgeTerminalLevel.Error);
|
|
36
|
+
expect(levelOf(PYTHON_RULES, ' File "/home/container/bot.py", line 3, in <module>')).toBe(
|
|
37
|
+
BridgeTerminalLevel.Error,
|
|
38
|
+
);
|
|
39
|
+
expect(levelOf(PYTHON_RULES, "discord.errors.LoginFailure: Improper token has been passed.")).toBe(
|
|
40
|
+
BridgeTerminalLevel.Error,
|
|
41
|
+
);
|
|
42
|
+
expect(levelOf(PYTHON_RULES, "telegram.error.InvalidToken: The token was rejected by the server.")).toBe(
|
|
43
|
+
BridgeTerminalLevel.Error,
|
|
44
|
+
);
|
|
45
|
+
expect(levelOf(PYTHON_RULES, "ModuleNotFoundError: No module named 'discord'")).toBe(BridgeTerminalLevel.Error);
|
|
46
|
+
expect(levelOf(PYTHON_RULES, "[2026-09-27 10:00:00] [ERROR ] discord.client: Ignoring exception")).toBe(
|
|
47
|
+
BridgeTerminalLevel.Error,
|
|
48
|
+
);
|
|
49
|
+
expect(levelOf(PYTHON_RULES, "2026-09-27 10:00:00,123 - telegram.ext - ERROR - No error handlers")).toBe(
|
|
50
|
+
BridgeTerminalLevel.Error,
|
|
51
|
+
);
|
|
52
|
+
expect(levelOf(PYTHON_RULES, "ERROR: Could not find a version that satisfies the requirement")).toBe(
|
|
53
|
+
BridgeTerminalLevel.Error,
|
|
54
|
+
);
|
|
55
|
+
expect(levelOf(PYTHON_RULES, "[2026-09-27 10:00:00] [WARNING ] discord.client: PyNaCl is not installed")).toBe(
|
|
56
|
+
BridgeTerminalLevel.Warn,
|
|
57
|
+
);
|
|
58
|
+
expect(levelOf(PYTHON_RULES, "/home/container/bot.py:9: RuntimeWarning: coroutine 'main' was never awaited")).toBe(
|
|
59
|
+
BridgeTerminalLevel.Warn,
|
|
60
|
+
);
|
|
61
|
+
expect(levelOf(PYTHON_RULES, "[2026-09-27 10:00:00] [INFO ] discord.client: logging in")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("cross the wire the way the panel reads them", () => {
|
|
65
|
+
for (const rule of [
|
|
66
|
+
...LAUNCHER_RULES,
|
|
67
|
+
...NODE_RULES,
|
|
68
|
+
...PYTHON_RULES,
|
|
69
|
+
]) {
|
|
70
|
+
expect(
|
|
71
|
+
bridgeTerminalRuleSchema.safeParse({
|
|
72
|
+
match: rule.match.source,
|
|
73
|
+
flags: rule.match.flags,
|
|
74
|
+
level: rule.level,
|
|
75
|
+
}).success,
|
|
76
|
+
).toBe(true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { BridgeTerminalLevel, type BridgeTerminalRule } from "../terminal";
|
|
2
|
+
|
|
3
|
+
export const LAUNCHER_RULES: BridgeTerminalRule[] = [
|
|
4
|
+
{
|
|
5
|
+
match: /^serverk: .* failed/,
|
|
6
|
+
level: BridgeTerminalLevel.Error,
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
match: /^serverk:/,
|
|
10
|
+
level: BridgeTerminalLevel.Info,
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const NODE_RULES: BridgeTerminalRule[] = [
|
|
15
|
+
{
|
|
16
|
+
match: /^(?:Uncaught )?[A-Z][A-Za-z]*Error\b/,
|
|
17
|
+
level: BridgeTerminalLevel.Error,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
match: /^\s+at .+:\d+:\d+\)?$/,
|
|
21
|
+
level: BridgeTerminalLevel.Error,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
match: /^node:internal\//,
|
|
25
|
+
level: BridgeTerminalLevel.Error,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
match: /^npm (?:ERR!|error)/,
|
|
29
|
+
level: BridgeTerminalLevel.Error,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
match: /^\(node:\d+\) /,
|
|
33
|
+
level: BridgeTerminalLevel.Warn,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
match: /^npm (?:WARN|warn)/,
|
|
37
|
+
level: BridgeTerminalLevel.Warn,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const PYTHON_RULES: BridgeTerminalRule[] = [
|
|
42
|
+
{
|
|
43
|
+
match: /^Traceback \(most recent call last\):/,
|
|
44
|
+
level: BridgeTerminalLevel.Error,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
match: /^\s+File ".+", line \d+/,
|
|
48
|
+
level: BridgeTerminalLevel.Error,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
match: /^(?:[a-z_]\w*\.)*[A-Z]\w*(?:Error|Exception|Failure)\b|^(?:[a-z_]\w*\.)+[A-Z]\w*: /,
|
|
52
|
+
level: BridgeTerminalLevel.Error,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
match: /^(?:ERROR|CRITICAL):|^\[?\d{4}-\d{2}-\d{2} [\d:,.]+\]? (?:- \S+ - |\[)?(?:ERROR|CRITICAL)\b/,
|
|
56
|
+
level: BridgeTerminalLevel.Error,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
match: /^WARNING:|^\[?\d{4}-\d{2}-\d{2} [\d:,.]+\]? (?:- \S+ - |\[)?WARNING\b|: [A-Z]\w*Warning: /,
|
|
60
|
+
level: BridgeTerminalLevel.Warn,
|
|
61
|
+
},
|
|
62
|
+
];
|