@serverkgg/bridge 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/package.json +1 -1
  2. package/rules/package.md +5 -0
  3. package/src/bot/botCauses.test.ts +284 -0
  4. package/src/bot/botCauses.ts +58 -0
  5. package/src/bot/botDeploy.test.ts +260 -1
  6. package/src/bot/botDeploy.ts +125 -6
  7. package/src/bot/botState.ts +6 -0
  8. package/src/bot/detect/botInspectWarning.ts +3 -1
  9. package/src/bot/detect/botPackageManager.ts +3 -0
  10. package/src/bot/detect/corpus/alexmubarakshin-ts-telegraf-boilerplate.json +37 -0
  11. package/src/bot/detect/corpus/alsarmad-discord-quran-bot.json +40 -0
  12. package/src/bot/detect/corpus/anasty17-mirror-leech-telegram-bot.json +50 -0
  13. package/src/bot/detect/corpus/anidiotsguide-guidebot.json +36 -0
  14. package/src/bot/detect/corpus/anonymousx1025-anonxmusic.json +45 -0
  15. package/src/bot/detect/corpus/bot-base-telegram-bot-template.json +35 -0
  16. package/src/bot/detect/corpus/bracketed-templates-discordjs.json +40 -0
  17. package/src/bot/detect/corpus/cog-creators-red-discordbot.json +49 -0
  18. package/src/bot/detect/corpus/cranberriez-discord.ts-bot-template.json +35 -0
  19. package/src/bot/detect/corpus/crazycat256-nextcord-bot-template.json +38 -0
  20. package/src/bot/detect/corpus/discord-tickets-bot.json +45 -0
  21. package/src/bot/detect/corpus/edjopato-telegram-typescript-bot-template.json +37 -0
  22. package/src/bot/detect/corpus/haniali3161-bot-adhkar-telegram.json +33 -0
  23. package/src/bot/detect/corpus/hellosverre-discord-ts-template.json +34 -0
  24. package/src/bot/detect/corpus/kevinnovak-discord-bot-typescript-template.json +43 -0
  25. package/src/bot/detect/corpus/kkrypt0nn-python-discord-bot-template.json +41 -0
  26. package/src/bot/detect/corpus/kolezka-telegram-grammy-template.json +37 -0
  27. package/src/bot/detect/corpus/latand-tgbot_template_v3.json +37 -0
  28. package/src/bot/detect/corpus/mahmoudbm77-zekr-telegram-bot.json +36 -0
  29. package/src/bot/detect/corpus/mericcan41-discordjs-v14-template-ts.json +33 -0
  30. package/src/bot/detect/corpus/modmail-dev-modmail.json +52 -0
  31. package/src/bot/detect/corpus/nezdemkovski-telegram-bot-boilerplate.json +39 -0
  32. package/src/bot/detect/corpus/notchum-discord-bot-template.json +39 -0
  33. package/src/bot/detect/corpus/python-discord-bot.json +44 -0
  34. package/src/bot/detect/corpus/python-telegram-bot-rules-bot.json +39 -0
  35. package/src/bot/detect/corpus/rajdave69-pycord-bot-template.json +31 -0
  36. package/src/bot/detect/corpus/rapptz-robodanny.json +36 -0
  37. package/src/bot/detect/corpus/ry2x-typescript-discord.js-v.14-template.json +35 -0
  38. package/src/bot/detect/corpus/saiteja-madha-discord-js-bot.json +45 -0
  39. package/src/bot/detect/corpus/seegreatness-discord.js-for-heroku.json +37 -0
  40. package/src/bot/detect/corpus/serverk-discord-node.json +36 -0
  41. package/src/bot/detect/corpus/serverk-discord-python.json +35 -0
  42. package/src/bot/detect/corpus/serverk-telegram-node.json +36 -0
  43. package/src/bot/detect/corpus/serverk-telegram-python.json +35 -0
  44. package/src/bot/detect/corpus/shuruhatik-quran-radio-bot-discord.json +33 -0
  45. package/src/bot/detect/corpus/skyra-project-skyra.json +46 -0
  46. package/src/bot/detect/corpus/tgbot-collection-ytdlbot.json +41 -0
  47. package/src/bot/detect/corpus/zeriodev-music-bot.json +38 -0
  48. package/src/bot/detect/detect.corpus.test.ts +77 -0
  49. package/src/bot/detect/detect.test.ts +414 -17
  50. package/src/bot/detect/detect.ts +347 -36
  51. package/src/bot/detect/detectResult.ts +4 -0
  52. package/src/bot/detect/detectTable.test.ts +22 -6
  53. package/src/bot/detect/detectTable.ts +187 -19
  54. package/src/bot/detect/detectedVersion.ts +5 -0
  55. package/src/bot/detect/envExample.test.ts +55 -1
  56. package/src/bot/detect/envExample.ts +14 -0
  57. package/src/bot/detect/index.ts +1 -0
  58. package/src/bot/detect/keys.test.ts +32 -1
  59. package/src/bot/detect/keys.ts +15 -0
  60. package/src/bot/detect/serverkJson.test.ts +2 -0
  61. package/src/bot/detect/serverkJson.ts +4 -1
  62. package/src/bot/index.ts +1 -0
  63. package/src/cli/validate.test.ts +127 -1
  64. package/src/cli/validate.ts +42 -0
  65. package/src/manifest/manifest.ts +2 -1
  66. package/src/modules/bridge.ts +2 -0
  67. package/src/modules/bridgeModule.ts +5 -0
  68. package/src/protocol/bridgeCrash.ts +49 -0
  69. package/src/protocol/bridgeEvents.ts +9 -0
  70. package/src/protocol/bridgePreview.ts +1 -0
  71. package/src/rcon/rconAccess.ts +1 -1
  72. package/src/runtime/bridgeDriver.test.ts +45 -0
  73. package/src/runtime/bridgeDriver.ts +25 -0
  74. package/src/runtime/bridgeEvents.ts +2 -0
  75. package/src/runtime/bridgeRegistry.test.ts +32 -0
  76. package/src/runtime/bridgeSupervisor.test.ts +163 -3
  77. package/src/runtime/bridgeSupervisor.ts +42 -1
  78. package/src/runtime/serve.ts +26 -5
@@ -9,7 +9,7 @@ import {
9
9
  SERVERK_JSON_EXAMPLE_LENGTH_LIMIT,
10
10
  } from "./serverkJson";
11
11
 
12
- export const DETECT_TABLE_VERSION = 1;
12
+ export const DETECT_TABLE_VERSION = 2;
13
13
 
14
14
  export const DETECT_TABLE_FILE = "image/detect.json";
15
15
 
@@ -18,6 +18,13 @@ export interface DetectLockfile {
18
18
  manager: string;
19
19
  }
20
20
 
21
+ export interface DetectVersionFile {
22
+ file: string;
23
+ tool?: string;
24
+ prefix?: string;
25
+ key?: string;
26
+ }
27
+
21
28
  export interface DetectProcfile {
22
29
  file: string;
23
30
  line: string;
@@ -38,16 +45,28 @@ export interface ServerkJsonDetectTable {
38
45
 
39
46
  export interface NodeDetectTable {
40
47
  manifest: string;
48
+ managerField: string;
49
+ berryFile: string;
50
+ enginesKey: string;
51
+ buildScript: string;
41
52
  startFiles: string[];
42
53
  lockfiles: DetectLockfile[];
43
- installCommands: Record<string, string[]>;
44
- unlockedManager: string;
54
+ installCommands: {
55
+ locked: Record<string, string[]>;
56
+ unlocked: Record<string, string[]>;
57
+ };
58
+ runCommands: Record<string, string[]>;
59
+ versionFiles: DetectVersionFile[];
60
+ defaultVersion: string;
45
61
  }
46
62
 
47
63
  export interface PythonDetectTable {
48
64
  startFiles: string[];
49
65
  manifests: DetectLockfile[];
50
- installArguments: Record<string, string[]>;
66
+ installCommands: Record<string, string[]>;
67
+ unhonouredManagers: string[];
68
+ versionFiles: DetectVersionFile[];
69
+ defaultVersion: string;
51
70
  }
52
71
 
53
72
  export interface DetectTable {
@@ -81,6 +100,10 @@ export const DETECT_TABLE: DetectTable = {
81
100
  },
82
101
  node: {
83
102
  manifest: "package.json",
103
+ managerField: "packageManager",
104
+ berryFile: ".yarnrc.yml",
105
+ enginesKey: "node",
106
+ buildScript: "build",
84
107
  startFiles: [
85
108
  "index.js",
86
109
  "src/index.js",
@@ -90,44 +113,125 @@ export const DETECT_TABLE: DetectTable = {
90
113
  ],
91
114
  lockfiles: [
92
115
  {
93
- file: "package-lock.json",
94
- manager: "npm",
116
+ file: "pnpm-lock.yaml",
117
+ manager: "pnpm",
95
118
  },
96
119
  {
97
- file: "npm-shrinkwrap.json",
98
- manager: "npm",
120
+ file: "bun.lock",
121
+ manager: "bun",
99
122
  },
100
123
  {
101
- file: "pnpm-lock.yaml",
102
- manager: "pnpm",
124
+ file: "bun.lockb",
125
+ manager: "bun",
103
126
  },
104
127
  {
105
128
  file: "yarn.lock",
106
129
  manager: "yarn",
107
130
  },
131
+ {
132
+ file: "package-lock.json",
133
+ manager: "npm",
134
+ },
135
+ {
136
+ file: "npm-shrinkwrap.json",
137
+ manager: "npm",
138
+ },
108
139
  ],
109
140
  installCommands: {
141
+ locked: {
142
+ npm: [
143
+ "npm",
144
+ "ci",
145
+ ],
146
+ pnpm: [
147
+ "corepack",
148
+ "pnpm",
149
+ "install",
150
+ "--frozen-lockfile",
151
+ ],
152
+ yarn: [
153
+ "corepack",
154
+ "yarn",
155
+ "install",
156
+ "--frozen-lockfile",
157
+ ],
158
+ "yarn-berry": [
159
+ "corepack",
160
+ "yarn",
161
+ "install",
162
+ "--immutable",
163
+ ],
164
+ bun: [
165
+ "bun",
166
+ "install",
167
+ "--frozen-lockfile",
168
+ ],
169
+ },
170
+ unlocked: {
171
+ npm: [
172
+ "npm",
173
+ "install",
174
+ ],
175
+ pnpm: [
176
+ "corepack",
177
+ "pnpm",
178
+ "install",
179
+ ],
180
+ yarn: [
181
+ "corepack",
182
+ "yarn",
183
+ "install",
184
+ ],
185
+ "yarn-berry": [
186
+ "corepack",
187
+ "yarn",
188
+ "install",
189
+ "--no-immutable",
190
+ ],
191
+ bun: [
192
+ "bun",
193
+ "install",
194
+ ],
195
+ },
196
+ },
197
+ runCommands: {
110
198
  npm: [
111
199
  "npm",
112
- "ci",
200
+ "run",
113
201
  ],
114
202
  pnpm: [
115
203
  "corepack",
116
204
  "pnpm",
117
- "install",
118
- "--frozen-lockfile",
205
+ "run",
119
206
  ],
120
207
  yarn: [
121
208
  "corepack",
122
209
  "yarn",
123
- "install",
210
+ "run",
124
211
  ],
125
- "npm-install": [
126
- "npm",
127
- "install",
212
+ "yarn-berry": [
213
+ "corepack",
214
+ "yarn",
215
+ "run",
216
+ ],
217
+ bun: [
218
+ "bun",
219
+ "run",
128
220
  ],
129
221
  },
130
- unlockedManager: "npm-install",
222
+ versionFiles: [
223
+ {
224
+ file: ".nvmrc",
225
+ },
226
+ {
227
+ file: ".node-version",
228
+ },
229
+ {
230
+ file: ".tool-versions",
231
+ tool: "nodejs",
232
+ },
233
+ ],
234
+ defaultVersion: "24",
131
235
  },
132
236
  python: {
133
237
  startFiles: [
@@ -142,22 +246,86 @@ export const DETECT_TABLE: DetectTable = {
142
246
  file: "requirements.txt",
143
247
  manager: "requirements",
144
248
  },
249
+ {
250
+ file: "uv.lock",
251
+ manager: "uv",
252
+ },
253
+ {
254
+ file: "poetry.lock",
255
+ manager: "poetry",
256
+ },
257
+ {
258
+ file: "pdm.lock",
259
+ manager: "pdm",
260
+ },
261
+ {
262
+ file: "Pipfile",
263
+ manager: "pipenv",
264
+ },
145
265
  {
146
266
  file: "pyproject.toml",
147
267
  manager: "project",
148
268
  },
149
269
  ],
150
- installArguments: {
270
+ installCommands: {
151
271
  requirements: [
272
+ "uv",
273
+ "pip",
152
274
  "install",
153
275
  "-r",
154
276
  "requirements.txt",
155
277
  ],
278
+ uv: [
279
+ "uv",
280
+ "sync",
281
+ "--locked",
282
+ "--no-dev",
283
+ ],
284
+ poetry: [
285
+ "uv",
286
+ "pip",
287
+ "install",
288
+ ".",
289
+ ],
290
+ pdm: [
291
+ "uv",
292
+ "pip",
293
+ "install",
294
+ ".",
295
+ ],
296
+ pipenv: [
297
+ "pipenv",
298
+ "install",
299
+ ],
156
300
  project: [
301
+ "uv",
302
+ "pip",
157
303
  "install",
158
304
  ".",
159
305
  ],
160
306
  },
307
+ unhonouredManagers: [
308
+ "poetry",
309
+ "pdm",
310
+ ],
311
+ versionFiles: [
312
+ {
313
+ file: ".python-version",
314
+ },
315
+ {
316
+ file: "runtime.txt",
317
+ prefix: "python-",
318
+ },
319
+ {
320
+ file: "Pipfile",
321
+ key: "python_version",
322
+ },
323
+ {
324
+ file: ".tool-versions",
325
+ tool: "python",
326
+ },
327
+ ],
328
+ defaultVersion: "3.12",
161
329
  },
162
330
  };
163
331
 
@@ -0,0 +1,5 @@
1
+ export const DETECTED_VERSION_PATTERN = /^\d+(?:\.\d+)?$/;
2
+
3
+ export const isDetectedVersion = (value: string | null): value is string => {
4
+ return value !== null && DETECTED_VERSION_PATTERN.test(value);
5
+ };
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { parseEnvExample } from "./envExample";
2
+ import { isSecretEnvFile, parseEnvExample } from "./envExample";
3
3
 
4
4
  describe("parseEnvExample", () => {
5
5
  it("reads keys and example values the way dotenv files write them", () => {
@@ -27,3 +27,57 @@ describe("parseEnvExample", () => {
27
27
  ]);
28
28
  });
29
29
  });
30
+
31
+ describe("isSecretEnvFile", () => {
32
+ const CASES: readonly {
33
+ name: string;
34
+ secret: boolean;
35
+ }[] = [
36
+ {
37
+ name: ".env",
38
+ secret: true,
39
+ },
40
+ {
41
+ name: ".env.local",
42
+ secret: true,
43
+ },
44
+ {
45
+ name: ".env.production",
46
+ secret: true,
47
+ },
48
+ {
49
+ name: ".env.example",
50
+ secret: false,
51
+ },
52
+ {
53
+ name: ".env.sample",
54
+ secret: false,
55
+ },
56
+ {
57
+ name: ".env.template",
58
+ secret: false,
59
+ },
60
+ {
61
+ name: ".env.dist",
62
+ secret: false,
63
+ },
64
+ {
65
+ name: ".env.defaults",
66
+ secret: false,
67
+ },
68
+ {
69
+ name: "env.py",
70
+ secret: false,
71
+ },
72
+ {
73
+ name: ".envrc",
74
+ secret: false,
75
+ },
76
+ ];
77
+
78
+ for (const item of CASES) {
79
+ it(`${item.secret ? "holds values" : "holds no values"} in ${item.name}`, () => {
80
+ expect(isSecretEnvFile(item.name)).toBe(item.secret);
81
+ });
82
+ }
83
+ });
@@ -6,6 +6,16 @@ export const ENV_EXAMPLE_FILES: readonly string[] = [
6
6
  ".env.template",
7
7
  ];
8
8
 
9
+ const ENV_TEMPLATE_FILES: ReadonlySet<string> = new Set([
10
+ ...ENV_EXAMPLE_FILES,
11
+ ".env.dist",
12
+ ".env.defaults",
13
+ ]);
14
+
15
+ export const ENV_FILE = ".env";
16
+
17
+ const ENV_FILE_NAME = /^\.env(?:\..+)?$/;
18
+
9
19
  export interface EnvExampleEntry {
10
20
  key: string;
11
21
  value: string;
@@ -46,3 +56,7 @@ export const parseEnvExample = (text: string): EnvExampleEntry[] => {
46
56
 
47
57
  return entries;
48
58
  };
59
+
60
+ export const isSecretEnvFile = (name: string) => {
61
+ return ENV_FILE_NAME.test(name) && !ENV_TEMPLATE_FILES.has(name);
62
+ };
@@ -6,6 +6,7 @@ export * from "./botPlatform";
6
6
  export * from "./botRuntime";
7
7
  export * from "./botToken";
8
8
  export * from "./detect";
9
+ export * from "./detectedVersion";
9
10
  export * from "./detectResult";
10
11
  export * from "./detectTable";
11
12
  export * from "./envExample";
@@ -1,5 +1,12 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { BOT_PLATFORM_KEYS, BOT_PLATFORM_SECRET_KEYS, BOT_PROTECTED_KEYS, isReservedBotKey } from "./keys";
2
+ import {
3
+ BOT_AMBIENT_KEYS,
4
+ BOT_PLATFORM_KEYS,
5
+ BOT_PLATFORM_SECRET_KEYS,
6
+ BOT_PROTECTED_KEYS,
7
+ BOT_PROVIDED_KEYS,
8
+ isReservedBotKey,
9
+ } from "./keys";
3
10
 
4
11
  describe("isReservedBotKey", () => {
5
12
  it("reserves every platform-owned key, the protected runtime keys and the SERVERK_ prefix", () => {
@@ -26,3 +33,27 @@ describe("isReservedBotKey", () => {
26
33
  }
27
34
  });
28
35
  });
36
+
37
+ describe("BOT_AMBIENT_KEYS", () => {
38
+ it("holds every key the runtime protects or the platform provides, beside the ones code reads with a default", () => {
39
+ for (const key of [
40
+ ...BOT_PROTECTED_KEYS,
41
+ ...BOT_PROVIDED_KEYS,
42
+ "NODE_ENV",
43
+ "PORT",
44
+ "PYTHONPATH",
45
+ ]) {
46
+ expect(BOT_AMBIENT_KEYS.includes(key)).toBe(true);
47
+ }
48
+ });
49
+
50
+ it("leaves a bot's own keys out", () => {
51
+ for (const key of [
52
+ "DISCORD_TOKEN",
53
+ "GUILD_ID",
54
+ "node_env",
55
+ ]) {
56
+ expect(BOT_AMBIENT_KEYS.includes(key)).toBe(false);
57
+ }
58
+ });
59
+ });
@@ -54,6 +54,21 @@ export const BOT_PROVIDED_KEYS: readonly string[] = [
54
54
  BOT_PLATFORM,
55
55
  ];
56
56
 
57
+ export const BOT_AMBIENT_KEYS: readonly string[] = [
58
+ ...BOT_PROTECTED_KEYS,
59
+ ...BOT_PROVIDED_KEYS,
60
+ "NODE_ENV",
61
+ "NODE_OPTIONS",
62
+ "PORT",
63
+ "HOST",
64
+ "HOSTNAME",
65
+ "DEBUG",
66
+ "LOG_LEVEL",
67
+ "CI",
68
+ "PYTHONPATH",
69
+ "PYTHONDONTWRITEBYTECODE",
70
+ ];
71
+
57
72
  export const isReservedBotKey = (key: string) => {
58
73
  return (
59
74
  key.startsWith(SERVERK_PREFIX)
@@ -30,6 +30,7 @@ describe("parseServerkJson", () => {
30
30
  framework: BotFramework.DiscordPy,
31
31
  start: "python -m bot",
32
32
  install: "pip install -r requirements/prod.txt",
33
+ build: null,
33
34
  env: [
34
35
  {
35
36
  key: "GUILD_ID",
@@ -52,6 +53,7 @@ describe("parseServerkJson", () => {
52
53
  framework: null,
53
54
  start: null,
54
55
  install: null,
56
+ build: null,
55
57
  env: [],
56
58
  });
57
59
  });
@@ -17,6 +17,7 @@ export interface ServerkJson {
17
17
  framework: BotFramework | null;
18
18
  start: string | null;
19
19
  install: string | null;
20
+ build: string | null;
20
21
  env: ServerkJsonEnv[];
21
22
  }
22
23
 
@@ -157,9 +158,10 @@ export const parseServerkJson = (text: string): ServerkJson | null => {
157
158
  const framework = optionalOf(document.framework, isBotFramework);
158
159
  const start = commandOf(document.start);
159
160
  const install = commandOf(document.install);
161
+ const build = commandOf(document.build);
160
162
  const env = envOf(document.env);
161
163
 
162
- if (!runtime.ok || !platform.ok || !framework.ok || !start.ok || !install.ok || !env.ok) {
164
+ if (!runtime.ok || !platform.ok || !framework.ok || !start.ok || !install.ok || !build.ok || !env.ok) {
163
165
  return null;
164
166
  }
165
167
 
@@ -169,6 +171,7 @@ export const parseServerkJson = (text: string): ServerkJson | null => {
169
171
  framework: framework.value,
170
172
  start: start.value,
171
173
  install: install.value,
174
+ build: build.value,
172
175
  env: env.value,
173
176
  };
174
177
  };
package/src/bot/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./botCauses";
1
2
  export * from "./botDeploy";
2
3
  export * from "./botEnvironment";
3
4
  export * from "./botIdentity";
@@ -8,7 +8,10 @@ import { GuideOpenTab } from "../guides";
8
8
  import { MANIFEST_SCHEMA_VERSION, PackageKind } from "../manifest";
9
9
  import type { BridgeDriver, BridgeSetupModule } from "../modules";
10
10
  import {
11
+ type BridgeCatalogSection,
12
+ BridgeConfirm,
11
13
  BridgeControl,
14
+ type BridgeDetailSection,
12
15
  type BridgeFormSection,
13
16
  BridgeFormTarget,
14
17
  BridgeLayout,
@@ -17,7 +20,13 @@ import {
17
20
  type BridgeTableSection,
18
21
  } from "../panel";
19
22
  import { RULES_FILE, readShippedRules, writeRules } from "./rules";
20
- import { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
23
+ import {
24
+ availabilityReportLines,
25
+ confirmReportLines,
26
+ placementReportLines,
27
+ runValidate,
28
+ setupReportLines,
29
+ } from "./validate";
21
30
 
22
31
  const text = (value: string) => ({
23
32
  ar: value,
@@ -696,3 +705,120 @@ describe("availability report", () => {
696
705
  ).toEqual([]);
697
706
  });
698
707
  });
708
+
709
+ describe("the confirm report", () => {
710
+ const driverWithSections = (sections: BridgeTab["sections"]): BridgeDriver => ({
711
+ install: {
712
+ kind: BridgeKind.Install,
713
+ run: async () => {
714
+ return;
715
+ },
716
+ },
717
+ lifecycle: {
718
+ kind: BridgeKind.Lifecycle,
719
+ command: async () => [
720
+ "run",
721
+ ],
722
+ },
723
+ panel: {
724
+ tabs: [
725
+ {
726
+ id: "settings",
727
+ title: text("Settings"),
728
+ sections,
729
+ },
730
+ ],
731
+ },
732
+ });
733
+
734
+ const versionForm = (confirm?: BridgeConfirm): BridgeFormSection => ({
735
+ layout: BridgeLayout.Form,
736
+ id: "version",
737
+ target: BridgeFormTarget.Variables,
738
+ reinstall: true,
739
+ confirm,
740
+ fields: [
741
+ {
742
+ key: "VERSION",
743
+ control: BridgeControl.Text,
744
+ label: text("Version"),
745
+ },
746
+ ],
747
+ });
748
+
749
+ const modpacks = (confirm?: BridgeConfirm): BridgeCatalogSection => ({
750
+ layout: BridgeLayout.Catalog,
751
+ id: "modpacks",
752
+ module: "modpacks",
753
+ confirm,
754
+ });
755
+
756
+ const modpack = (confirm?: BridgeConfirm, reinstall?: boolean): BridgeDetailSection => ({
757
+ layout: BridgeLayout.Detail,
758
+ id: "modpack",
759
+ module: "modpack",
760
+ variable: "MODPACK",
761
+ reinstall,
762
+ confirm,
763
+ });
764
+
765
+ const worlds = (): BridgeTableSection => ({
766
+ layout: BridgeLayout.Table,
767
+ id: "worlds",
768
+ module: "worlds",
769
+ columns: [
770
+ {
771
+ key: "name",
772
+ label: text("Name"),
773
+ },
774
+ ],
775
+ actions: [
776
+ {
777
+ id: "delete",
778
+ label: text("Delete"),
779
+ confirm: BridgeConfirm.Strong,
780
+ },
781
+ ],
782
+ });
783
+
784
+ it("fails a form or a catalog that asks for a strong confirmation", () => {
785
+ expect(
786
+ confirmReportLines(
787
+ driverWithSections([
788
+ versionForm(BridgeConfirm.Strong),
789
+ modpacks(BridgeConfirm.Strong),
790
+ ]),
791
+ ),
792
+ ).toEqual([
793
+ ' error: tab "settings" section "version" is a form with `confirm: strong` — the platform takes a restore point before applying it, so declare `normal`; `strong` is only for what nothing brings back',
794
+ ' error: tab "settings" section "modpacks" is a catalog with `confirm: strong` — the platform takes a restore point before applying it, so declare `normal`; `strong` is only for what nothing brings back',
795
+ ]);
796
+ });
797
+
798
+ it("fails any section that reinstalls behind a strong confirmation", () => {
799
+ expect(
800
+ confirmReportLines(
801
+ driverWithSections([
802
+ modpack(BridgeConfirm.Strong, true),
803
+ ]),
804
+ ),
805
+ ).toEqual([
806
+ ' error: tab "settings" section "modpack" reinstalls with `confirm: strong` — the platform takes a restore point before every reinstall, so declare `normal`; `strong` is only for what nothing brings back',
807
+ ]);
808
+ });
809
+
810
+ it("leaves a strong action that deletes data and every normal confirmation alone", () => {
811
+ expect(
812
+ confirmReportLines(
813
+ driverWithSections([
814
+ versionForm(BridgeConfirm.Normal),
815
+ versionForm(),
816
+ modpacks(BridgeConfirm.Normal),
817
+ modpack(BridgeConfirm.Normal, true),
818
+ modpack(BridgeConfirm.Strong),
819
+ worlds(),
820
+ ]),
821
+ ),
822
+ ).toEqual([]);
823
+ });
824
+ });