From b878b43b25e02da0d86d13e24df44861d625a238 Mon Sep 17 00:00:00 2001 From: boredinnyc Date: Sun, 26 Jul 2026 05:46:06 -0400 Subject: [PATCH] fix(browser-bridge): point remediation at the local extension checkout doctor and the no-extension BrowserConnectError both told the user to download a release zip. That is right for a global npm install -- the `files` array does not ship `extension/` -- but wrong when running from a source checkout, where extension/manifest.json and the built extension/dist/background.js are already on disk. Add hasLocalExtensionSource() and branch both messages on it, so a checkout gets "Load unpacked -> /extension" instead of being sent to a download page for files it already has. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WRqKZ1vQa29kYHJEr4iRGj --- src/browser/daemon-lifecycle.ts | 10 ++++++++-- src/doctor.ts | 14 +++++++++++--- src/package-paths.ts | 13 +++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/browser/daemon-lifecycle.ts b/src/browser/daemon-lifecycle.ts index 6123199b7..d6e40d827 100644 --- a/src/browser/daemon-lifecycle.ts +++ b/src/browser/daemon-lifecycle.ts @@ -4,6 +4,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { DEFAULT_DAEMON_PORT } from '../constants.js'; import { BrowserConnectError } from '../errors.js'; +import { findPackageRoot, hasLocalExtensionSource } from '../package-paths.js'; import { PKG_VERSION } from '../version.js'; import { waitForBridgeReady } from './bridge-readiness.js'; import { fetchDaemonStatus, getDaemonHealth, requestDaemonShutdown, type DaemonHealth, type DaemonStatus } from './daemon-transport.js'; @@ -181,12 +182,17 @@ function browserConnectErrorFromHealth(health: DaemonHealth, contextId?: string) ); } if (health.state === 'no-extension') { + const packageRoot = findPackageRoot(fileURLToPath(import.meta.url)); + const installSteps = hasLocalExtensionSource(packageRoot) + ? ' 1. Open chrome://extensions → Developer Mode → Load unpacked\n' + + ` 2. Select this checkout's extension/ folder (${path.join(packageRoot, 'extension')})` + : ' 1. Download: https://github.com/jackwener/opencli/releases\n' + + ' 2. Open chrome://extensions → Developer Mode → Load unpacked'; return new BrowserConnectError( 'Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the OpenCLI extension is enabled.\n' + 'If not installed:\n' + - ' 1. Download: https://github.com/jackwener/opencli/releases\n' + - ' 2. Open chrome://extensions → Developer Mode → Load unpacked', + installSteps, 'extension-not-connected', ); } diff --git a/src/doctor.ts b/src/doctor.ts index 53c6198a0..093087563 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -4,6 +4,8 @@ * Simplified for the daemon-based architecture. */ +import { fileURLToPath } from 'node:url'; +import * as path from 'node:path'; import { DEFAULT_DAEMON_PORT } from './constants.js'; import { BrowserBridge } from './browser/index.js'; import { setDaemonCommandTimeoutSeconds } from './browser/daemon-client.js'; @@ -15,6 +17,7 @@ import type { BrowserProfileStatus } from './browser/daemon-transport.js'; import { aliasForContextId, loadProfileConfig } from './browser/profile.js'; import { formatDaemonVersion, isDaemonStale, staleDaemonIssue } from './browser/daemon-version.js'; import { findShadowedUserAdapters, formatAdapterShadowIssue, type AdapterShadow } from './adapter-shadow.js'; +import { findPackageRoot, hasLocalExtensionSource } from './package-paths.js'; const DOCTOR_LIVE_TIMEOUT_SECONDS = 8; const DOCTOR_SESSION = '__doctor__'; @@ -151,13 +154,18 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise boolean = fs.existsSync, +): boolean { + return fileExists(path.join(packageRoot, 'extension', 'manifest.json')); +}