@serverkgg/bridge 0.0.8 → 0.0.10

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 (72) hide show
  1. package/package.json +3 -3
  2. package/src/cli/validate.ts +2 -0
  3. package/src/context/bridgeContext.ts +17 -0
  4. package/src/context/index.ts +1 -0
  5. package/src/core/bridgeDownloadError.ts +10 -0
  6. package/src/core/bridgeHold.ts +12 -0
  7. package/src/core/bridgeKind.ts +3 -0
  8. package/src/core/bridgeProbeError.ts +16 -0
  9. package/src/core/bridgeSetup.ts +20 -0
  10. package/src/core/bridgeUserError.test.ts +23 -0
  11. package/src/core/bridgeUserError.ts +12 -0
  12. package/src/core/bridgeValue.test.ts +29 -0
  13. package/src/core/bridgeValue.ts +12 -0
  14. package/src/core/index.ts +5 -0
  15. package/src/guides/blocks.ts +8 -0
  16. package/src/guides/markdown.test.ts +18 -0
  17. package/src/guides/markdown.ts +7 -1
  18. package/src/manifest/index.ts +1 -0
  19. package/src/manifest/manifest.test.ts +132 -0
  20. package/src/manifest/manifest.ts +21 -0
  21. package/src/manifest/presence.ts +56 -0
  22. package/src/manifest/primitives.ts +6 -0
  23. package/src/modules/bridge.ts +65 -2
  24. package/src/modules/bridgeDriver.ts +4 -0
  25. package/src/modules/bridgeModule.ts +32 -3
  26. package/src/panel/bridgeDetail.ts +49 -0
  27. package/src/panel/bridgeLayout.ts +1 -0
  28. package/src/panel/bridgePanel.ts +30 -0
  29. package/src/panel/bridgeUploadMode.ts +4 -0
  30. package/src/panel/index.ts +2 -0
  31. package/src/probes/a2s.test.ts +71 -0
  32. package/src/probes/a2s.ts +38 -11
  33. package/src/protocol/bridgeEgress.ts +8 -1
  34. package/src/protocol/bridgeEvents.ts +4 -0
  35. package/src/protocol/bridgeFrame.ts +33 -1
  36. package/src/protocol/bridgeLimits.ts +12 -0
  37. package/src/protocol/bridgeManifest.ts +2 -0
  38. package/src/protocol/bridgeSetup.ts +97 -0
  39. package/src/protocol/index.ts +1 -0
  40. package/src/runtime/bridgeActions.ts +7 -0
  41. package/src/runtime/bridgeConfigReader.ts +12 -8
  42. package/src/runtime/bridgeContext.ts +28 -2
  43. package/src/runtime/bridgeDriver.test.ts +748 -3
  44. package/src/runtime/bridgeDriver.ts +198 -4
  45. package/src/runtime/bridgeEgress.ts +3 -0
  46. package/src/runtime/bridgeEvents.test.ts +96 -0
  47. package/src/runtime/bridgeEvents.ts +11 -6
  48. package/src/runtime/bridgeFiles.test.ts +625 -15
  49. package/src/runtime/bridgeFiles.ts +152 -45
  50. package/src/runtime/bridgeInstall.test.ts +118 -0
  51. package/src/runtime/bridgeInstall.ts +55 -0
  52. package/src/runtime/bridgeNet.ts +32 -3
  53. package/src/runtime/bridgePending.test.ts +148 -0
  54. package/src/runtime/bridgePending.ts +43 -0
  55. package/src/runtime/bridgeRegistry.test.ts +172 -1
  56. package/src/runtime/bridgeRegistry.ts +40 -5
  57. package/src/runtime/bridgeSecrets.test.ts +121 -0
  58. package/src/runtime/bridgeSecrets.ts +43 -0
  59. package/src/runtime/bridgeServer.test.ts +598 -0
  60. package/src/runtime/bridgeServer.ts +42 -3
  61. package/src/runtime/bridgeSetup.test.ts +189 -0
  62. package/src/runtime/bridgeSetup.ts +63 -0
  63. package/src/runtime/bridgeSignal.test.ts +114 -0
  64. package/src/runtime/bridgeSignal.ts +31 -0
  65. package/src/runtime/bridgeSupervisor.test.ts +111 -0
  66. package/src/runtime/bridgeSupervisor.ts +55 -5
  67. package/src/runtime/bridgeZip.ts +179 -58
  68. package/src/runtime/serve.ts +115 -4
  69. package/src/utils/index.ts +1 -0
  70. package/src/utils/stripAnsi.ts +12 -0
  71. package/src/wire/panel.test.ts +259 -0
  72. package/src/wire/panel.ts +72 -0
@@ -6,12 +6,26 @@ const CENTRAL_SIGNATURE = 0x02_01_4b_50;
6
6
 
7
7
  const LOCAL_SIGNATURE = 0x04_03_4b_50;
8
8
 
9
+ const ZIP64_LOCATOR_SIGNATURE = 0x07_06_4b_50;
10
+
11
+ const ZIP64_EOCD_SIGNATURE = 0x06_06_4b_50;
12
+
9
13
  const EOCD_MIN_LENGTH = 22;
10
14
 
11
15
  const EOCD_SEARCH_LIMIT = 65_557;
12
16
 
17
+ const ZIP64_LOCATOR_LENGTH = 20;
18
+
19
+ const ZIP64_EOCD_MIN_LENGTH = 56;
20
+
21
+ const ZIP64_EXTRA_ID = 0x00_01;
22
+
23
+ const CENTRAL_HEADER_LENGTH = 46;
24
+
13
25
  const LOCAL_HEADER_LENGTH = 30;
14
26
 
27
+ const SIZE_SENTINEL = 0xff_ff_ff_ff;
28
+
15
29
  const STORED = 0;
16
30
 
17
31
  const DEFLATED = 8;
@@ -20,7 +34,7 @@ const ENCRYPTED_FLAG = 0x1;
20
34
 
21
35
  export const ZIP_ENTRY_LIMIT = 2048;
22
36
 
23
- export const ZIP_TREE_ENTRY_LIMIT = 8192;
37
+ export const ZIP_TREE_ENTRY_LIMIT = 16_384;
24
38
 
25
39
  const MAX_ENTRY_BYTES = 128 * 1024 * 1024;
26
40
 
@@ -30,14 +44,34 @@ const UNIX_MODE_SYMLINK = 0xa0_00;
30
44
 
31
45
  export interface ZipEntry {
32
46
  name: string;
33
- data: Uint8Array;
34
47
  symlink: boolean;
48
+ sizeBytes: number;
49
+ compressedSize: number;
50
+ method: number;
51
+ localOffset: number;
35
52
  }
36
53
 
37
- const findEndOfCentralDirectory = (buffer: Buffer) => {
38
- const start = Math.max(0, buffer.length - EOCD_SEARCH_LIMIT);
54
+ export interface ZipArchive {
55
+ entries: ZipEntry[];
56
+ read(entry: ZipEntry): Promise<Uint8Array>;
57
+ }
58
+
59
+ const readRange = async (path: string, start: number, end: number) => {
60
+ return Buffer.from(await Bun.file(path).slice(start, end).arrayBuffer());
61
+ };
62
+
63
+ const readOffset = (buffer: Buffer, offset: number) => {
64
+ const value = buffer.readBigUInt64LE(offset);
65
+
66
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
67
+ throw new Error("archive declares an offset this runtime cannot address");
68
+ }
69
+
70
+ return Number(value);
71
+ };
39
72
 
40
- for (let offset = buffer.length - EOCD_MIN_LENGTH; offset >= start; offset--) {
73
+ const findEndOfCentralDirectory = (buffer: Buffer) => {
74
+ for (let offset = buffer.length - EOCD_MIN_LENGTH; offset >= 0; offset--) {
41
75
  if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE) {
42
76
  return offset;
43
77
  }
@@ -46,88 +80,175 @@ const findEndOfCentralDirectory = (buffer: Buffer) => {
46
80
  return -1;
47
81
  };
48
82
 
49
- export const readZip = (buffer: Buffer, maxEntries: number = ZIP_ENTRY_LIMIT): ZipEntry[] => {
50
- const eocd = findEndOfCentralDirectory(buffer);
83
+ interface CentralDirectory {
84
+ total: number;
85
+ offset: number;
86
+ sizeBytes: number;
87
+ }
51
88
 
52
- if (eocd === -1) {
53
- throw new Error("archive has no zip central directory");
89
+ const readZip64Directory = async (path: string, tail: Buffer, eocd: number): Promise<CentralDirectory | null> => {
90
+ const locator = eocd - ZIP64_LOCATOR_LENGTH;
91
+
92
+ if (locator < 0 || tail.readUInt32LE(locator) !== ZIP64_LOCATOR_SIGNATURE) {
93
+ return null;
54
94
  }
55
95
 
56
- const total = buffer.readUInt16LE(eocd + 10);
57
- const directoryOffset = buffer.readUInt32LE(eocd + 16);
96
+ const record = readOffset(tail, locator + 8);
97
+ const buffer = await readRange(path, record, record + ZIP64_EOCD_MIN_LENGTH);
58
98
 
59
- if (total > maxEntries) {
60
- throw new Error(`archive has too many entries (${total})`);
99
+ if (buffer.length < ZIP64_EOCD_MIN_LENGTH || buffer.readUInt32LE(0) !== ZIP64_EOCD_SIGNATURE) {
100
+ throw new Error("archive has a corrupt zip64 central directory record");
61
101
  }
62
102
 
63
- const entries: ZipEntry[] = [];
103
+ return {
104
+ total: readOffset(buffer, 32),
105
+ sizeBytes: readOffset(buffer, 40),
106
+ offset: readOffset(buffer, 48),
107
+ };
108
+ };
64
109
 
65
- let cursor = directoryOffset;
110
+ interface Zip64Extended {
111
+ sizeBytes: number | null;
112
+ compressedSize: number | null;
113
+ localOffset: number | null;
114
+ }
66
115
 
67
- for (let index = 0; index < total; index++) {
68
- if (cursor + 46 > buffer.length || buffer.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) {
69
- throw new Error("archive central directory is corrupt");
70
- }
116
+ const readZip64Extra = (extra: Buffer, sizeBytes: number, compressedSize: number, localOffset: number) => {
117
+ const extended: Zip64Extended = {
118
+ sizeBytes: null,
119
+ compressedSize: null,
120
+ localOffset: null,
121
+ };
71
122
 
72
- const flags = buffer.readUInt16LE(cursor + 8);
73
- const method = buffer.readUInt16LE(cursor + 10);
74
- const compressedSize = buffer.readUInt32LE(cursor + 20);
75
- const uncompressedSize = buffer.readUInt32LE(cursor + 24);
76
- const nameLength = buffer.readUInt16LE(cursor + 28);
77
- const extraLength = buffer.readUInt16LE(cursor + 30);
78
- const commentLength = buffer.readUInt16LE(cursor + 32);
79
- const externalAttributes = buffer.readUInt32LE(cursor + 38);
80
- const localOffset = buffer.readUInt32LE(cursor + 42);
81
- const name = buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
82
- const symlink = ((externalAttributes >>> 16) & UNIX_MODE_FORMAT_MASK) === UNIX_MODE_SYMLINK;
123
+ let cursor = 0;
83
124
 
84
- cursor += 46 + nameLength + extraLength + commentLength;
125
+ while (cursor + 4 <= extra.length) {
126
+ const id = extra.readUInt16LE(cursor);
127
+ const length = extra.readUInt16LE(cursor + 2);
128
+ const body = extra.subarray(cursor + 4, cursor + 4 + length);
85
129
 
86
- if (name.endsWith("/") || (flags & ENCRYPTED_FLAG) !== 0) {
130
+ cursor += 4 + length;
131
+
132
+ if (id !== ZIP64_EXTRA_ID) {
87
133
  continue;
88
134
  }
89
135
 
90
- if (uncompressedSize > MAX_ENTRY_BYTES) {
91
- throw new Error(`archive entry "${name}" is too large`);
136
+ let field = 0;
137
+
138
+ if (sizeBytes === SIZE_SENTINEL && field + 8 <= body.length) {
139
+ extended.sizeBytes = readOffset(body, field);
140
+ field += 8;
141
+ }
142
+
143
+ if (compressedSize === SIZE_SENTINEL && field + 8 <= body.length) {
144
+ extended.compressedSize = readOffset(body, field);
145
+ field += 8;
92
146
  }
93
147
 
94
- if (localOffset + LOCAL_HEADER_LENGTH > buffer.length || buffer.readUInt32LE(localOffset) !== LOCAL_SIGNATURE) {
95
- throw new Error(`archive entry "${name}" has a corrupt local header`);
148
+ if (localOffset === SIZE_SENTINEL && field + 8 <= body.length) {
149
+ extended.localOffset = readOffset(body, field);
96
150
  }
97
151
 
98
- const localNameLength = buffer.readUInt16LE(localOffset + 26);
99
- const localExtraLength = buffer.readUInt16LE(localOffset + 28);
100
- const dataOffset = localOffset + LOCAL_HEADER_LENGTH + localNameLength + localExtraLength;
101
- const compressed = buffer.subarray(dataOffset, dataOffset + compressedSize);
152
+ return extended;
153
+ }
102
154
 
103
- if (method === STORED) {
104
- entries.push({
105
- name,
106
- data: compressed,
107
- symlink,
108
- });
155
+ return extended;
156
+ };
109
157
 
110
- continue;
111
- }
158
+ export const openZip = async (path: string, maxEntries: number = ZIP_ENTRY_LIMIT): Promise<ZipArchive> => {
159
+ const size = Bun.file(path).size;
160
+ const tailStart = Math.max(0, size - (EOCD_SEARCH_LIMIT + ZIP64_LOCATOR_LENGTH));
161
+ const tail = await readRange(path, tailStart, size);
162
+ const eocd = tail.length < EOCD_MIN_LENGTH ? -1 : findEndOfCentralDirectory(tail);
163
+
164
+ if (eocd === -1) {
165
+ throw new Error("archive has no zip central directory");
166
+ }
167
+
168
+ const directory = (await readZip64Directory(path, tail, eocd)) ?? {
169
+ total: tail.readUInt16LE(eocd + 10),
170
+ sizeBytes: tail.readUInt32LE(eocd + 12),
171
+ offset: tail.readUInt32LE(eocd + 16),
172
+ };
173
+
174
+ if (directory.total > maxEntries) {
175
+ throw new Error(`archive has too many entries (${directory.total})`);
176
+ }
177
+
178
+ const central = await readRange(path, directory.offset, directory.offset + directory.sizeBytes);
179
+ const entries: ZipEntry[] = [];
112
180
 
113
- if (method !== DEFLATED) {
114
- throw new Error(`archive entry "${name}" uses unsupported compression ${method}`);
181
+ let cursor = 0;
182
+
183
+ for (let index = 0; index < directory.total; index++) {
184
+ if (cursor + CENTRAL_HEADER_LENGTH > central.length || central.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) {
185
+ throw new Error("archive central directory is corrupt");
115
186
  }
116
187
 
117
- const data = inflateRawSync(compressed, {
118
- maxOutputLength: MAX_ENTRY_BYTES,
119
- });
188
+ const flags = central.readUInt16LE(cursor + 8);
189
+ const method = central.readUInt16LE(cursor + 10);
190
+ const compressedSize = central.readUInt32LE(cursor + 20);
191
+ const sizeBytes = central.readUInt32LE(cursor + 24);
192
+ const nameLength = central.readUInt16LE(cursor + 28);
193
+ const extraLength = central.readUInt16LE(cursor + 30);
194
+ const commentLength = central.readUInt16LE(cursor + 32);
195
+ const externalAttributes = central.readUInt32LE(cursor + 38);
196
+ const localOffset = central.readUInt32LE(cursor + 42);
197
+ const nameStart = cursor + CENTRAL_HEADER_LENGTH;
198
+ const name = central.subarray(nameStart, nameStart + nameLength).toString("utf8");
199
+ const extra = central.subarray(nameStart + nameLength, nameStart + nameLength + extraLength);
200
+ const extended = readZip64Extra(extra, sizeBytes, compressedSize, localOffset);
201
+
202
+ cursor = nameStart + nameLength + extraLength + commentLength;
120
203
 
121
- if (data.length !== uncompressedSize) {
122
- throw new Error(`archive entry "${name}" did not inflate to its declared size`);
204
+ if (name.endsWith("/") || (flags & ENCRYPTED_FLAG) !== 0) {
205
+ continue;
123
206
  }
124
207
 
125
208
  entries.push({
126
209
  name,
127
- data,
128
- symlink,
210
+ symlink: ((externalAttributes >>> 16) & UNIX_MODE_FORMAT_MASK) === UNIX_MODE_SYMLINK,
211
+ sizeBytes: extended.sizeBytes ?? sizeBytes,
212
+ compressedSize: extended.compressedSize ?? compressedSize,
213
+ method,
214
+ localOffset: extended.localOffset ?? localOffset,
129
215
  });
130
216
  }
131
217
 
132
- return entries;
218
+ return {
219
+ entries,
220
+
221
+ async read(entry) {
222
+ if (entry.sizeBytes > MAX_ENTRY_BYTES || entry.compressedSize > MAX_ENTRY_BYTES) {
223
+ throw new Error(`archive entry "${entry.name}" is too large`);
224
+ }
225
+
226
+ const header = await readRange(path, entry.localOffset, entry.localOffset + LOCAL_HEADER_LENGTH);
227
+
228
+ if (header.length < LOCAL_HEADER_LENGTH || header.readUInt32LE(0) !== LOCAL_SIGNATURE) {
229
+ throw new Error(`archive entry "${entry.name}" has a corrupt local header`);
230
+ }
231
+
232
+ const dataOffset = entry.localOffset + LOCAL_HEADER_LENGTH + header.readUInt16LE(26) + header.readUInt16LE(28);
233
+ const compressed = await readRange(path, dataOffset, dataOffset + entry.compressedSize);
234
+
235
+ if (entry.method === STORED) {
236
+ return compressed;
237
+ }
238
+
239
+ if (entry.method !== DEFLATED) {
240
+ throw new Error(`archive entry "${entry.name}" uses unsupported compression ${entry.method}`);
241
+ }
242
+
243
+ const data = inflateRawSync(compressed, {
244
+ maxOutputLength: MAX_ENTRY_BYTES,
245
+ });
246
+
247
+ if (data.length !== entry.sizeBytes) {
248
+ throw new Error(`archive entry "${entry.name}" did not inflate to its declared size`);
249
+ }
250
+
251
+ return data;
252
+ },
253
+ };
133
254
  };
@@ -1,20 +1,26 @@
1
1
  import { chmod, chown, mkdir, rm } from "node:fs/promises";
2
- import { BridgeDesired, BridgeRuntime } from "../core";
2
+ import { BridgeDesired, type BridgeInstallHold, BridgeRuntime, BridgeUserError, type LocalizedText } from "../core";
3
3
  import type { BridgeDriver } from "../modules";
4
4
  import {
5
5
  BRIDGE_INSTALL_ERROR_LIMIT,
6
6
  BRIDGE_VERSION,
7
7
  type BridgeConfig,
8
+ type BridgePendingResult,
8
9
  type BridgeSampleResult,
10
+ type BridgeSetupResult,
11
+ type BridgeSetupRuntime,
9
12
  type BridgeStatus,
10
13
  } from "../protocol";
11
14
  import { configPath, readConfig, socketPath, statePath } from "./bridgeConfigReader";
12
15
  import { createContext } from "./bridgeContext";
13
16
  import { validateDriver } from "./bridgeDriver";
14
17
  import { createEventRing } from "./bridgeEvents";
18
+ import { decideInstall, recountHold } from "./bridgeInstall";
15
19
  import { errorText, log } from "./bridgeLog";
20
+ import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
16
21
  import { createRegistry } from "./bridgeRegistry";
17
22
  import { createServer } from "./bridgeServer";
23
+ import { failedSetup, setupSummaryOf, unsupportedSetup } from "./bridgeSetup";
18
24
  import { readDesired, writeDesired } from "./bridgeState";
19
25
  import { createSupervisor } from "./bridgeSupervisor";
20
26
 
@@ -64,6 +70,12 @@ export const serve = async (driver: BridgeDriver) => {
64
70
 
65
71
  let installError: string | null = null;
66
72
 
73
+ let installErrorText: LocalizedText | null = null;
74
+
75
+ let installHold: BridgeInstallHold | null = null;
76
+
77
+ let setupRuntime: BridgeSetupRuntime | null = null;
78
+
67
79
  const supervisor = createSupervisor({
68
80
  cwd: config.cwd,
69
81
  uid: config.gameUid,
@@ -93,6 +105,9 @@ export const serve = async (driver: BridgeDriver) => {
93
105
  onEmit(event, payload) {
94
106
  ring.emit(event, payload);
95
107
  },
108
+ onSetup(runtime) {
109
+ setupRuntime = runtime;
110
+ },
96
111
  });
97
112
 
98
113
  const registry = createRegistry(driver, context, supervisor, ring);
@@ -105,8 +120,12 @@ export const serve = async (driver: BridgeDriver) => {
105
120
  runtime: supervisor.runtime,
106
121
  desired: current,
107
122
  exitCode: supervisor.exitCode,
123
+ outOfMemory: supervisor.outOfMemory,
108
124
  restarts: supervisor.restarts,
109
125
  installError,
126
+ installErrorText,
127
+ installHold,
128
+ setup: setupSummaryOf(setupRuntime),
110
129
  };
111
130
  };
112
131
 
@@ -152,6 +171,20 @@ export const serve = async (driver: BridgeDriver) => {
152
171
  return value;
153
172
  };
154
173
 
174
+ const pending = async (): Promise<BridgePendingResult> => {
175
+ const module = driver.pending;
176
+
177
+ if (!module) {
178
+ return unsupportedPending();
179
+ }
180
+
181
+ const files = await module.list(context);
182
+
183
+ installHold = recountHold(installHold, files.length);
184
+
185
+ return pendingResult(module.staging, module.extensions, files, log);
186
+ };
187
+
155
188
  const server = createServer({
156
189
  socket: socketPath(config),
157
190
  token: config.token,
@@ -260,6 +293,70 @@ export const serve = async (driver: BridgeDriver) => {
260
293
  delivered: true,
261
294
  };
262
295
  },
296
+ async onPendingList() {
297
+ return await pending();
298
+ },
299
+ async onPendingAdd(input) {
300
+ const module = driver.pending;
301
+
302
+ if (!module) {
303
+ return unsupportedPending();
304
+ }
305
+
306
+ try {
307
+ await module.add(context, input);
308
+ } catch (error) {
309
+ if (!(error instanceof BridgeUserError)) {
310
+ throw error;
311
+ }
312
+
313
+ log.warn("pending upload rejected", {
314
+ error: error.message,
315
+ });
316
+
317
+ return rejectedPending(await pending());
318
+ }
319
+
320
+ return await pending();
321
+ },
322
+ onSetupStatus() {
323
+ if (!driver.setup) {
324
+ return unsupportedSetup();
325
+ }
326
+
327
+ return {
328
+ supported: true,
329
+ runtime: setupRuntime,
330
+ };
331
+ },
332
+ async onSetupSubmit(step, action, args): Promise<BridgeSetupResult> {
333
+ const module = driver.setup;
334
+
335
+ if (!module?.submit) {
336
+ return unsupportedSetup();
337
+ }
338
+
339
+ try {
340
+ await module.submit(context, step, action, args);
341
+ } catch (error) {
342
+ if (!(error instanceof BridgeUserError)) {
343
+ throw error;
344
+ }
345
+
346
+ log.warn("setup submit rejected", {
347
+ step,
348
+ action,
349
+ error: error.message,
350
+ });
351
+
352
+ return failedSetup(setupRuntime, error.text);
353
+ }
354
+
355
+ return {
356
+ supported: true,
357
+ runtime: setupRuntime,
358
+ };
359
+ },
263
360
  });
264
361
 
265
362
  await chmod(socketPath(config), SOCKET_MODE);
@@ -303,12 +400,15 @@ export const serve = async (driver: BridgeDriver) => {
303
400
 
304
401
  log("installing");
305
402
 
403
+ let decision = decideInstall(undefined, current);
404
+
306
405
  try {
307
- await driver.install.run(context);
406
+ decision = decideInstall(await driver.install.run(context), current);
308
407
 
309
408
  log("install complete");
310
409
  } catch (error) {
311
410
  installError = errorText(error).slice(0, BRIDGE_INSTALL_ERROR_LIMIT);
411
+ installErrorText = error instanceof BridgeUserError ? error.text : null;
312
412
 
313
413
  supervisor.setRuntime(BridgeRuntime.Crashed);
314
414
 
@@ -319,9 +419,20 @@ export const serve = async (driver: BridgeDriver) => {
319
419
  return;
320
420
  }
321
421
 
322
- supervisor.setRuntime(current === BridgeDesired.Running ? BridgeRuntime.Starting : BridgeRuntime.Stopped);
422
+ installHold = decision.hold;
423
+
424
+ supervisor.setRuntime(decision.runtime);
425
+
426
+ if (installHold) {
427
+ log("install held", {
428
+ reason: installHold.reason,
429
+ count: installHold.count,
430
+ });
431
+
432
+ return;
433
+ }
323
434
 
324
- if (current === BridgeDesired.Running) {
435
+ if (decision.start) {
325
436
  try {
326
437
  await supervisor.start();
327
438
  } catch (error) {
@@ -1 +1,2 @@
1
1
  export * from "./glob";
2
+ export * from "./stripAnsi";
@@ -0,0 +1,12 @@
1
+ const ANSI_PATTERN = new RegExp(
2
+ [
3
+ "\\u001B\\][^\\u0007\\u001B]*(?:\\u0007|\\u001B\\\\)",
4
+ "\\u001B\\[[0-9;?]*[ -/]*[@-~]",
5
+ "\\u001B[@-Z\\\\-_]",
6
+ ].join("|"),
7
+ "g",
8
+ );
9
+
10
+ export const stripAnsi = (line: string): string => {
11
+ return line.replace(ANSI_PATTERN, "");
12
+ };