@browserbridge/bbx 1.0.0 → 1.1.0

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/README.md +6 -4
  2. package/package.json +53 -53
  3. package/packages/agent-client/src/cli-helpers.js +43 -5
  4. package/packages/agent-client/src/cli.js +176 -171
  5. package/packages/agent-client/src/client.js +66 -21
  6. package/packages/agent-client/src/command-registry.js +104 -69
  7. package/packages/agent-client/src/detect.js +162 -54
  8. package/packages/agent-client/src/install.js +34 -28
  9. package/packages/agent-client/src/mcp-config.js +40 -40
  10. package/packages/agent-client/src/runtime.js +41 -20
  11. package/packages/agent-client/src/setup-status.js +23 -30
  12. package/packages/mcp-server/src/bin.js +57 -5
  13. package/packages/mcp-server/src/handlers.js +573 -256
  14. package/packages/mcp-server/src/server.js +568 -257
  15. package/packages/native-host/bin/bridge-daemon.js +39 -6
  16. package/packages/native-host/bin/install-manifest.js +26 -4
  17. package/packages/native-host/bin/postinstall.js +4 -2
  18. package/packages/native-host/src/config.js +142 -13
  19. package/packages/native-host/src/daemon-process.js +396 -0
  20. package/packages/native-host/src/daemon.js +350 -150
  21. package/packages/native-host/src/framing.js +131 -11
  22. package/packages/native-host/src/install-manifest.js +194 -29
  23. package/packages/native-host/src/native-host.js +154 -102
  24. package/packages/protocol/src/budget.js +3 -7
  25. package/packages/protocol/src/capabilities.js +6 -3
  26. package/packages/protocol/src/defaults.js +1 -0
  27. package/packages/protocol/src/errors.js +15 -11
  28. package/packages/protocol/src/payload-cost.js +19 -6
  29. package/packages/protocol/src/protocol.js +242 -73
  30. package/packages/protocol/src/registry.js +311 -45
  31. package/packages/protocol/src/summary.js +260 -109
  32. package/packages/protocol/src/types.js +29 -4
  33. package/skills/browser-bridge/SKILL.md +3 -2
  34. package/skills/browser-bridge/agents/openai.yaml +3 -3
  35. package/skills/browser-bridge/references/interaction.md +34 -11
  36. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  37. package/skills/browser-bridge/references/protocol.md +127 -71
  38. package/skills/browser-bridge/references/tailwind.md +12 -11
  39. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  40. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  41. package/CHANGELOG.md +0 -55
  42. package/assets/banner.jpg +0 -0
  43. package/assets/logo.png +0 -0
  44. package/assets/logo.svg +0 -65
  45. package/docs/api-reference.md +0 -157
  46. package/docs/cli-guide.md +0 -128
  47. package/docs/index.md +0 -25
  48. package/docs/manual-setup.md +0 -140
  49. package/docs/mcp-vs-cli.md +0 -258
  50. package/docs/publishing.md +0 -114
  51. package/docs/quickstart.md +0 -104
  52. package/docs/troubleshooting.md +0 -59
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -52
  55. package/packages/extension/assets/icon-128.png +0 -0
  56. package/packages/extension/assets/icon-16.png +0 -0
  57. package/packages/extension/assets/icon-32.png +0 -0
  58. package/packages/extension/assets/icon-48.png +0 -0
  59. package/packages/extension/src/background-helpers.js +0 -459
  60. package/packages/extension/src/background-routing.js +0 -91
  61. package/packages/extension/src/background.js +0 -3227
  62. package/packages/extension/src/content-script-helpers.js +0 -281
  63. package/packages/extension/src/content-script.js +0 -1977
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -102
  66. package/packages/extension/ui/offscreen.html +0 -6
  67. package/packages/extension/ui/offscreen.js +0 -61
  68. package/packages/extension/ui/popup.html +0 -35
  69. package/packages/extension/ui/popup.js +0 -279
  70. package/packages/extension/ui/sidepanel.html +0 -102
  71. package/packages/extension/ui/sidepanel.js +0 -1854
  72. package/packages/extension/ui/ui.css +0 -1159
@@ -11,6 +11,9 @@ import { MAX_NATIVE_MESSAGE_BYTES } from '../../protocol/src/index.js';
11
11
  */
12
12
  export async function writeNativeMessage(stream, message) {
13
13
  const payload = Buffer.from(JSON.stringify(message), 'utf8');
14
+ if (payload.length > MAX_NATIVE_MESSAGE_BYTES) {
15
+ throw new Error(`Native message exceeds ${MAX_NATIVE_MESSAGE_BYTES} bytes: ${payload.length}`);
16
+ }
14
17
  const header = Buffer.alloc(4);
15
18
  header.writeUInt32LE(payload.length, 0);
16
19
  if (!stream.write(header)) {
@@ -24,34 +27,151 @@ export async function writeNativeMessage(stream, message) {
24
27
  /**
25
28
  * @param {NodeJS.ReadableStream} stream
26
29
  * @param {(message: unknown) => void} onMessage
30
+ * @param {(error: Error) => void} [onProtocolError]
27
31
  * @returns {void}
28
32
  */
29
- export function createNativeMessageReader(stream, onMessage) {
30
- let buffer = Buffer.alloc(0);
33
+ export function createNativeMessageReader(stream, onMessage, onProtocolError) {
34
+ /** @type {Buffer[]} */
35
+ const chunks = [];
36
+ let bufferedBytes = 0;
37
+ let closed = false;
38
+
39
+ /**
40
+ * @param {number} length
41
+ * @returns {Buffer | null}
42
+ */
43
+ function peekBytes(length) {
44
+ if (length === 0) {
45
+ return Buffer.alloc(0);
46
+ }
47
+ if (bufferedBytes < length || chunks.length === 0) {
48
+ return null;
49
+ }
50
+
51
+ const firstChunk = chunks[0];
52
+ if (firstChunk.length >= length) {
53
+ return firstChunk.subarray(0, length);
54
+ }
55
+
56
+ const combined = Buffer.allocUnsafe(length);
57
+ let offset = 0;
58
+ for (const chunk of chunks) {
59
+ const copyLength = Math.min(chunk.length, length - offset);
60
+ chunk.copy(combined, offset, 0, copyLength);
61
+ offset += copyLength;
62
+ if (offset === length) {
63
+ return combined;
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * @param {number} length
72
+ * @returns {Buffer | null}
73
+ */
74
+ function consumeBytes(length) {
75
+ if (length === 0) {
76
+ return Buffer.alloc(0);
77
+ }
78
+ if (bufferedBytes < length || chunks.length === 0) {
79
+ return null;
80
+ }
81
+
82
+ const firstChunk = chunks[0];
83
+ if (firstChunk.length === length) {
84
+ chunks.shift();
85
+ bufferedBytes -= length;
86
+ return firstChunk;
87
+ }
88
+ if (firstChunk.length > length) {
89
+ const consumed = firstChunk.subarray(0, length);
90
+ chunks[0] = firstChunk.subarray(length);
91
+ bufferedBytes -= length;
92
+ return consumed;
93
+ }
94
+
95
+ const combined = Buffer.allocUnsafe(length);
96
+ let offset = 0;
97
+ let remaining = length;
98
+ while (remaining > 0 && chunks.length > 0) {
99
+ const chunk = chunks[0];
100
+ const copyLength = Math.min(chunk.length, remaining);
101
+ chunk.copy(combined, offset, 0, copyLength);
102
+ offset += copyLength;
103
+ remaining -= copyLength;
104
+ if (copyLength === chunk.length) {
105
+ chunks.shift();
106
+ } else {
107
+ chunks[0] = chunk.subarray(copyLength);
108
+ }
109
+ }
110
+
111
+ bufferedBytes -= length;
112
+ return combined;
113
+ }
114
+
115
+ /**
116
+ * @param {Error} error
117
+ * @returns {void}
118
+ */
119
+ function closeReader(error) {
120
+ if (closed) {
121
+ return;
122
+ }
123
+ closed = true;
124
+ stream.removeListener('data', handleData);
125
+ onProtocolError?.(error);
126
+ const destroy = /** @type {{ destroy?: (() => void) | undefined }} */ (stream).destroy;
127
+ if (typeof destroy === 'function') {
128
+ destroy.call(stream);
129
+ }
130
+ }
31
131
 
32
132
  /** @param {Buffer} chunk */
33
- stream.on('data', (chunk) => {
34
- buffer = Buffer.concat([buffer, chunk]);
133
+ function handleData(chunk) {
134
+ if (closed || chunk.length === 0) {
135
+ return;
136
+ }
137
+
138
+ chunks.push(chunk);
139
+ bufferedBytes += chunk.length;
35
140
 
36
- while (buffer.length >= 4) {
37
- const length = buffer.readUInt32LE(0);
141
+ while (bufferedBytes >= 4) {
142
+ const header = peekBytes(4);
143
+ if (!header) {
144
+ return;
145
+ }
146
+
147
+ const length = header.readUInt32LE(0);
38
148
  if (length > MAX_NATIVE_MESSAGE_BYTES) {
39
- buffer = Buffer.alloc(0);
149
+ closeReader(
150
+ new Error(`Native message exceeds ${MAX_NATIVE_MESSAGE_BYTES} bytes: ${length}`)
151
+ );
40
152
  return;
41
153
  }
42
- if (buffer.length < 4 + length) {
154
+
155
+ const frameLength = 4 + length;
156
+ if (bufferedBytes < frameLength) {
43
157
  return;
44
158
  }
45
159
 
46
- const payload = buffer.subarray(4, 4 + length);
47
- buffer = buffer.subarray(4 + length);
160
+ const frame = consumeBytes(frameLength);
161
+ if (!frame) {
162
+ return;
163
+ }
164
+
165
+ const payload = frame.subarray(4);
48
166
  try {
49
167
  onMessage(JSON.parse(payload.toString('utf8')));
50
168
  } catch {
51
169
  // Malformed JSON payload - skip it.
52
170
  }
53
171
  }
54
- });
172
+ }
173
+
174
+ stream.on('data', handleData);
55
175
  }
56
176
 
57
177
  /**
@@ -1,15 +1,57 @@
1
1
  // @ts-check
2
2
 
3
+ import { execFile } from 'node:child_process';
3
4
  import fs from 'node:fs';
4
5
  import path from 'node:path';
5
-
6
- import { APP_NAME, getBridgeDir, getLauncherFilename, getManifestInstallDir, PUBLISHED_EXTENSION_ID } from './config.js';
6
+ import { promisify } from 'node:util';
7
+
8
+ import {
9
+ APP_NAME,
10
+ BRIDGE_TCP_PORT_ENV,
11
+ DEFAULT_WINDOWS_TCP_PORT,
12
+ getBridgeDir,
13
+ getLauncherFilename,
14
+ getManifestInstallDir,
15
+ PUBLISHED_EXTENSION_ID,
16
+ } from './config.js';
7
17
 
8
18
  export const DEFAULT_EXTENSION_ID_ENV = 'BROWSER_BRIDGE_EXTENSION_ID';
9
19
  export const BUILT_IN_EXTENSION_ID_SOURCE = 'built_in';
20
+ export const INSTALL_NATIVE_MANIFEST_ERROR = 'INSTALL_NATIVE_MANIFEST_FAILED';
10
21
 
11
22
  /** @typedef {import('./config.js').SupportedBrowser} SupportedBrowser */
12
23
  /** @typedef {'env' | 'built_in' | 'none' | 'invalid_env'} ExtensionIdSource */
24
+ /** @typedef {NodeJS.ErrnoException & { cause?: unknown }} MaybeErrnoError */
25
+
26
+ const execFileAsync = promisify(execFile);
27
+
28
+ /**
29
+ * @returns {string}
30
+ */
31
+ function getWindowsRegistryExe() {
32
+ if (process.platform !== 'win32') {
33
+ return 'reg.exe';
34
+ }
35
+
36
+ const systemRoot = process.env.SystemRoot || process.env.WINDIR;
37
+ return systemRoot ? path.join(systemRoot, 'System32', 'reg.exe') : 'reg.exe';
38
+ }
39
+
40
+ export class NativeManifestInstallError extends Error {
41
+ /**
42
+ * @param {string} targetPath
43
+ * @param {unknown} cause
44
+ */
45
+ constructor(targetPath, cause) {
46
+ const detail = cause instanceof Error ? cause.message : String(cause);
47
+ super(`Failed to install native host files at ${targetPath}: ${detail}`, { cause });
48
+ this.name = 'NativeManifestInstallError';
49
+ this.code = INSTALL_NATIVE_MANIFEST_ERROR;
50
+ this.targetPath = targetPath;
51
+ this.cause = cause;
52
+ this.errnoCode = /** @type {MaybeErrnoError | undefined} */ (cause)?.code;
53
+ }
54
+ }
13
55
 
14
56
  /**
15
57
  * @typedef {{
@@ -21,6 +63,8 @@ export const BUILT_IN_EXTENSION_ID_SOURCE = 'built_in';
21
63
  * bridgeDir?: string | undefined,
22
64
  * stdout?: Pick<NodeJS.WriteStream, 'write'>,
23
65
  * stderr?: Pick<NodeJS.WriteStream, 'write'>,
66
+ * preserveCustomExtensionId?: boolean | undefined,
67
+ * writeRegistryValue?: ((keyPath: string, value: string) => Promise<void> | void) | undefined,
24
68
  * env?: NodeJS.ProcessEnv
25
69
  * }} InstallManifestOptions
26
70
  */
@@ -31,10 +75,64 @@ export const BUILT_IN_EXTENSION_ID_SOURCE = 'built_in';
31
75
  * installDir?: string | undefined,
32
76
  * bridgeDir?: string | undefined,
33
77
  * removeBridgeDir?: boolean | undefined,
78
+ * deleteRegistryKey?: ((keyPath: string) => Promise<boolean> | boolean) | undefined,
34
79
  * stdout?: Pick<NodeJS.WriteStream, 'write'>
35
80
  * }} UninstallManifestOptions
36
81
  */
37
82
 
83
+ /**
84
+ * @param {SupportedBrowser} [browser='chrome']
85
+ * @returns {string}
86
+ */
87
+ export function getWindowsRegistryKey(browser = 'chrome') {
88
+ const roots = {
89
+ chrome: 'HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts',
90
+ edge: 'HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts',
91
+ brave: 'HKCU\\Software\\BraveSoftware\\Brave-Browser\\NativeMessagingHosts',
92
+ chromium: 'HKCU\\Software\\Chromium\\NativeMessagingHosts',
93
+ // Arc is Chromium-based, and no verified Browser Company-specific native
94
+ // messaging registry path is documented in this repo yet.
95
+ arc: 'HKCU\\Software\\Chromium\\NativeMessagingHosts',
96
+ };
97
+
98
+ return `${roots[browser] || roots.chrome}\\${APP_NAME}`;
99
+ }
100
+
101
+ /**
102
+ * @param {string} keyPath
103
+ * @param {string} value
104
+ * @returns {Promise<void>}
105
+ */
106
+ async function writeRegistryValue(keyPath, value) {
107
+ await execFileAsync(getWindowsRegistryExe(), [
108
+ 'add',
109
+ keyPath,
110
+ '/ve',
111
+ '/t',
112
+ 'REG_SZ',
113
+ '/d',
114
+ value,
115
+ '/f',
116
+ ]);
117
+ }
118
+
119
+ /**
120
+ * @param {string} keyPath
121
+ * @returns {Promise<boolean>}
122
+ */
123
+ async function deleteRegistryKey(keyPath) {
124
+ try {
125
+ await execFileAsync(getWindowsRegistryExe(), ['delete', keyPath, '/f']);
126
+ return true;
127
+ } catch (error) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ if (/unable to find|cannot find|was unable to find/i.test(message)) {
130
+ return false;
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+
38
136
  /**
39
137
  * Parse and validate a Chrome extension ID from a CLI argument.
40
138
  * Accepts a raw 32-char ID or a full `chrome-extension://<id>/` origin.
@@ -62,13 +160,13 @@ export function resolveDefaultExtensionId(env = process.env) {
62
160
  const parsed = parseExtensionId(candidate);
63
161
  return {
64
162
  extensionId: parsed,
65
- source: parsed ? 'env' : 'invalid_env'
163
+ source: parsed ? 'env' : 'invalid_env',
66
164
  };
67
165
  }
68
166
 
69
167
  return {
70
168
  extensionId: PUBLISHED_EXTENSION_ID || null,
71
- source: PUBLISHED_EXTENSION_ID ? 'built_in' : 'none'
169
+ source: PUBLISHED_EXTENSION_ID ? 'built_in' : 'none',
72
170
  };
73
171
  }
74
172
 
@@ -90,9 +188,10 @@ export function getDefaultExtensionId(env = process.env) {
90
188
  * @returns {string[]}
91
189
  */
92
190
  export function getAllowedOrigins(existingManifest, extensionId) {
93
- const existing = (existingManifest && Array.isArray(existingManifest.allowed_origins))
94
- ? existingManifest.allowed_origins
95
- : [];
191
+ const existing =
192
+ existingManifest && Array.isArray(existingManifest.allowed_origins)
193
+ ? existingManifest.allowed_origins
194
+ : [];
96
195
 
97
196
  if (extensionId) {
98
197
  const origin = `chrome-extension://${extensionId}/`;
@@ -110,6 +209,25 @@ export function getAllowedOrigins(existingManifest, extensionId) {
110
209
  return ['chrome-extension://__REPLACE_WITH_EXTENSION_ID__/'];
111
210
  }
112
211
 
212
+ /**
213
+ * @param {string[] | undefined} allowedOrigins
214
+ * @returns {string[]}
215
+ */
216
+ function getExtensionIdsFromAllowedOrigins(allowedOrigins) {
217
+ if (!Array.isArray(allowedOrigins)) {
218
+ return [];
219
+ }
220
+
221
+ const ids = new Set();
222
+ for (const origin of allowedOrigins) {
223
+ const match = /^chrome-extension:\/\/([a-z]{32})\/?$/.exec(origin);
224
+ if (match?.[1]) {
225
+ ids.add(match[1]);
226
+ }
227
+ }
228
+ return [...ids];
229
+ }
230
+
113
231
  /**
114
232
  * @param {string} value
115
233
  * @returns {string}
@@ -144,7 +262,10 @@ export async function installNativeManifest(options) {
144
262
  installDir = getManifestInstallDir(browser),
145
263
  bridgeDir = getBridgeDir(),
146
264
  stdout = process.stdout,
147
- env = process.env
265
+ stderr = process.stderr,
266
+ preserveCustomExtensionId = false,
267
+ writeRegistryValue: writeRegistryValueFn = writeRegistryValue,
268
+ env = process.env,
148
269
  } = options;
149
270
 
150
271
  const parsedExtensionId = parseExtensionId(extensionIdArg);
@@ -160,19 +281,35 @@ export async function installNativeManifest(options) {
160
281
  `Invalid ${DEFAULT_EXTENSION_ID_ENV}: ${env[DEFAULT_EXTENSION_ID_ENV]}\nExpected 32 lowercase letters or chrome-extension://<id>/`
161
282
  );
162
283
  }
163
- const extensionId = parsedExtensionId || defaultExtensionId.extensionId;
284
+ const requestedExtensionId = parsedExtensionId || defaultExtensionId.extensionId;
164
285
  const hostPath = path.join(repoRoot, 'packages', 'native-host', 'bin', 'native-host.js');
165
286
  const launcherPath = path.join(bridgeDir, getLauncherFilename());
166
287
  const manifestPath = path.join(installDir, `${APP_NAME}.json`);
167
288
 
168
- const launcher = process.platform === 'win32'
169
- ? `@echo off\r\n"${nodePath}" "${hostPath}" %*\r\n`
170
- : `#!/bin/sh
289
+ const launcher =
290
+ process.platform === 'win32'
291
+ ? `@echo off\r\nset ${BRIDGE_TCP_PORT_ENV}=${DEFAULT_WINDOWS_TCP_PORT}\r\n"${nodePath}" "${hostPath}" %*\r\n`
292
+ : `#!/bin/sh
171
293
  exec '${escapeSingleQuotes(nodePath)}' '${escapeSingleQuotes(hostPath)}' "$@"
172
294
  `;
173
295
 
174
296
  const existingManifest = await readExistingManifest(manifestPath);
175
- const allowedOrigins = getAllowedOrigins(existingManifest, extensionId);
297
+ const existingExtensionIds = getExtensionIdsFromAllowedOrigins(existingManifest?.allowed_origins);
298
+ const hasStoreOrigin = existingExtensionIds.includes(PUBLISHED_EXTENSION_ID);
299
+ const customExtensionIds = existingExtensionIds.filter((id) => id !== PUBLISHED_EXTENSION_ID);
300
+ const preservedCustomExtensionId =
301
+ preserveCustomExtensionId &&
302
+ !parsedExtensionId &&
303
+ extensionIdArg == null &&
304
+ defaultExtensionId.source === BUILT_IN_EXTENSION_ID_SOURCE &&
305
+ customExtensionIds.length > 0 &&
306
+ !hasStoreOrigin;
307
+ const allowedOrigins = preservedCustomExtensionId
308
+ ? getAllowedOrigins(existingManifest, null)
309
+ : getAllowedOrigins(existingManifest, requestedExtensionId);
310
+ const extensionId = preservedCustomExtensionId
311
+ ? customExtensionIds[0] || requestedExtensionId
312
+ : requestedExtensionId;
176
313
 
177
314
  /** @type {{name: string, description: string, path: string, type: 'stdio', allowed_origins: string[]}} */
178
315
  const manifest = {
@@ -180,21 +317,36 @@ exec '${escapeSingleQuotes(nodePath)}' '${escapeSingleQuotes(hostPath)}' "$@"
180
317
  description: 'Browser Bridge native host',
181
318
  path: launcherPath,
182
319
  type: 'stdio',
183
- allowed_origins: allowedOrigins
320
+ allowed_origins: allowedOrigins,
184
321
  };
185
322
 
186
- await fs.promises.mkdir(installDir, { recursive: true });
187
- await fs.promises.mkdir(bridgeDir, { recursive: true });
188
- await fs.promises.writeFile(launcherPath, launcher, 'utf8');
189
- if (process.platform !== 'win32') {
190
- await fs.promises.chmod(launcherPath, 0o755);
323
+ let failingPath = installDir;
324
+ try {
325
+ await fs.promises.mkdir(installDir, { recursive: true });
326
+ failingPath = bridgeDir;
327
+ await fs.promises.mkdir(bridgeDir, { recursive: true });
328
+ failingPath = launcherPath;
329
+ await fs.promises.writeFile(launcherPath, launcher, 'utf8');
330
+ if (process.platform !== 'win32') {
331
+ await fs.promises.chmod(launcherPath, 0o755);
332
+ }
333
+ failingPath = manifestPath;
334
+ await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
335
+ if (process.platform === 'win32') {
336
+ failingPath = getWindowsRegistryKey(browser);
337
+ await writeRegistryValueFn(getWindowsRegistryKey(browser), manifestPath);
338
+ }
339
+ } catch (error) {
340
+ throw new NativeManifestInstallError(failingPath, error);
191
341
  }
192
- await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
193
342
 
194
343
  stdout.write(`Wrote ${manifestPath}\n`);
195
344
  stdout.write(`Wrote ${launcherPath}\n`);
345
+ if (process.platform === 'win32') {
346
+ stdout.write(`Registered ${getWindowsRegistryKey(browser)}\n`);
347
+ }
196
348
 
197
- if (!parsedExtensionId && extensionIdArg == null && extensionId) {
349
+ if (!preservedCustomExtensionId && !parsedExtensionId && extensionIdArg == null && extensionId) {
198
350
  if (defaultExtensionId.source === 'env') {
199
351
  stdout.write(`Used extension ID from ${DEFAULT_EXTENSION_ID_ENV}.\n`);
200
352
  } else if (defaultExtensionId.source === BUILT_IN_EXTENSION_ID_SOURCE) {
@@ -202,11 +354,19 @@ exec '${escapeSingleQuotes(nodePath)}' '${escapeSingleQuotes(hostPath)}' "$@"
202
354
  }
203
355
  }
204
356
 
205
- const hasPlaceholder = allowedOrigins.some((origin) => origin.includes('__REPLACE_WITH_EXTENSION_ID__'));
357
+ if (preservedCustomExtensionId) {
358
+ stderr.write(
359
+ `Warning: existing native host manifest keeps custom extension ID ${customExtensionIds.join(', ')} instead of the Browser Bridge store ID ${PUBLISHED_EXTENSION_ID}. Leaving allowed_origins unchanged.\n`
360
+ );
361
+ }
362
+
363
+ const hasPlaceholder = allowedOrigins.some((origin) =>
364
+ origin.includes('__REPLACE_WITH_EXTENSION_ID__')
365
+ );
206
366
  if (hasPlaceholder) {
207
367
  stdout.write(
208
368
  'Tip: pass the extension ID to set allowed_origins automatically:\n' +
209
- ' bbx install <extension-id>\n'
369
+ ' bbx install <extension-id>\n'
210
370
  );
211
371
  }
212
372
 
@@ -214,7 +374,7 @@ exec '${escapeSingleQuotes(nodePath)}' '${escapeSingleQuotes(hostPath)}' "$@"
214
374
  manifestPath,
215
375
  launcherPath,
216
376
  allowedOrigins,
217
- extensionId
377
+ extensionId,
218
378
  };
219
379
  }
220
380
 
@@ -228,18 +388,23 @@ export async function uninstallNativeManifest(options = {}) {
228
388
  installDir = getManifestInstallDir(browser),
229
389
  bridgeDir = getBridgeDir(),
230
390
  removeBridgeDir = false,
231
- stdout = process.stdout
391
+ deleteRegistryKey: deleteRegistryKeyFn = deleteRegistryKey,
392
+ stdout = process.stdout,
232
393
  } = options;
233
394
 
234
395
  const manifestPath = path.join(installDir, `${APP_NAME}.json`);
396
+ const registryKeyPath = process.platform === 'win32' ? getWindowsRegistryKey(browser) : null;
235
397
  const removedManifest = await removePathIfExists(manifestPath);
236
398
  if (removedManifest) {
237
399
  stdout.write(`Removed ${manifestPath}\n`);
238
400
  }
239
401
 
240
- const removedBridgeDir = removeBridgeDir
241
- ? await removePathIfExists(bridgeDir)
242
- : false;
402
+ const removedRegistryKey = registryKeyPath ? await deleteRegistryKeyFn(registryKeyPath) : false;
403
+ if (removedRegistryKey && registryKeyPath) {
404
+ stdout.write(`Removed ${registryKeyPath}\n`);
405
+ }
406
+
407
+ const removedBridgeDir = removeBridgeDir ? await removePathIfExists(bridgeDir) : false;
243
408
  if (removedBridgeDir) {
244
409
  stdout.write(`Removed ${bridgeDir}\n`);
245
410
  }
@@ -248,7 +413,7 @@ export async function uninstallNativeManifest(options = {}) {
248
413
  manifestPath,
249
414
  bridgeDir,
250
415
  removedManifest,
251
- removedBridgeDir
416
+ removedBridgeDir,
252
417
  };
253
418
  }
254
419