browse
Version:
Unified Browserbase CLI for browser automation and cloud APIs.
330 lines (329 loc) • 11.2 kB
JavaScript
import { promises as fs } from "node:fs";
import { CommandFailure } from "../errors.js";
import { getDriverStatus } from "./daemon/client.js";
import { getLockPath, getPidPath, getSocketPath, runtimeDir, } from "./daemon/paths.js";
import { isProcessAlive } from "./daemon/process.js";
import { discoverLocalCdp, } from "./local-cdp-discovery.js";
import { hasExplicitDriverTarget } from "./command-cli.js";
import { resolveConnectionTarget, targetsCompatible } from "./mode.js";
const DEFAULT_URL = "https://example.com";
export async function buildDoctorReport(options, deps = {}) {
const checks = [];
const env = deps.env ?? process.env;
const getStatus = deps.getDriverStatus ?? getDriverStatus;
const resolveTarget = deps.resolveConnectionTarget ?? resolveConnectionTarget;
const packageVersion = await (deps.readPackageVersion ?? readPackageVersion)();
checks.push({
details: { node: process.version, version: packageVersion },
message: `browse ${packageVersion}, node ${process.version}`,
name: "runtime",
status: "ok",
});
checks.push({
message: options.session,
name: "session",
status: "ok",
});
const paths = {
lock: getLockPath(options.session),
pid: getPidPath(options.session),
runtimeDir: runtimeDir(),
socket: getSocketPath(options.session),
};
const status = await getStatus(options.session).catch((error) => {
checks.push({
details: { error: errorMessage(error) },
fix: `browse stop --session ${options.session} --force`,
message: `could not read daemon status: ${errorMessage(error)}`,
name: "daemon",
status: "fail",
});
return null;
});
if (!checks.some((check) => check.name === "daemon")) {
checks.push(await daemonCheck(options.session, status, paths, deps));
}
const explicitTarget = hasExplicitDriverTarget(options.flags);
let target;
let targetFailed = false;
if (status?.target && !explicitTarget) {
target = status.target;
checks.push({
details: { target },
message: `reusing ${formatTarget(target)}`,
name: "target",
status: "ok",
});
}
else {
try {
target = await resolveTarget(options.flags);
const incompatible = status?.target && !targetsCompatible(status.target, target);
if (incompatible) {
targetFailed = true;
checks.push({
details: { requested: target, running: status.target },
fix: `browse stop --session ${options.session}`,
message: `session is already using ${formatTarget(status.target)}, requested ${formatTarget(target)}`,
name: "target",
status: "fail",
});
}
else {
checks.push({
details: { target },
message: status?.target
? `matches running ${formatTarget(target)}`
: formatTarget(target),
name: "target",
status: "ok",
});
}
}
catch (error) {
targetFailed = true;
checks.push({
message: errorMessage(error),
name: "target",
status: "fail",
});
}
}
if (target && !targetFailed && !status) {
const modeCheck = await checkTargetPrerequisite(target, options.flags, options.session, env, deps);
if (modeCheck)
checks.push(modeCheck);
}
const verdict = reportVerdict(checks);
return {
checks,
next: nextStep(verdict, checks, target, options.flags, options.session, status),
paths,
session: options.session,
target,
verdict,
};
}
export function renderDoctorReport(report) {
const lines = ["Browse doctor", ""];
const width = Math.max(...report.checks.map((check) => check.name.length), 7);
for (const check of report.checks) {
lines.push(`${statusLabel(check.status)} ${check.name.padEnd(width)} ${check.message}`);
}
lines.push("", `Status: ${report.verdict}`);
const fix = report.checks.find((check) => check.status === "fail" && check.fix)?.fix ??
report.checks.find((check) => check.status === "warn" && check.fix)?.fix;
if (fix) {
lines.push(`Fix: ${fix}`);
}
else if (report.next) {
lines.push(`Next: ${report.next}`);
}
return lines.join("\n");
}
async function daemonCheck(session, status, paths, deps) {
if (status) {
const pageCount = status.pages.length;
const state = status.initialized
? `connected, ${pageCount} page${pageCount === 1 ? "" : "s"}`
: "running";
return {
details: { mode: status.mode, pid: status.pid },
message: `${state}, pid ${status.pid}`,
name: "daemon",
status: "ok",
};
}
const inspection = await inspectDaemonFiles(paths, deps);
if (inspection.alivePid) {
return {
details: { pid: inspection.alivePid },
fix: `browse stop --session ${session} --force`,
message: "pid file exists but daemon is not responding",
name: "daemon",
status: "fail",
};
}
if (inspection.lock === "stale") {
return {
fix: `browse stop --session ${session} --force`,
message: "stale lock file detected",
name: "daemon",
status: "warn",
};
}
if (inspection.lock === "active") {
return {
message: "daemon startup lock is currently held",
name: "daemon",
status: "warn",
};
}
return {
message: "no active daemon",
name: "daemon",
status: "ok",
};
}
async function checkTargetPrerequisite(target, flags, session, env, deps) {
if (target.kind === "managed-local") {
return {
message: target.headless
? "managed local browser, headless"
: "managed local browser, headed",
name: "browser",
status: "ok",
};
}
if (target.kind === "remote") {
if (env.BROWSERBASE_API_KEY) {
return {
message: "BROWSERBASE_API_KEY is set",
name: "browserbase",
status: "ok",
};
}
return {
fix: "export BROWSERBASE_API_KEY=...",
message: "BROWSERBASE_API_KEY is not set",
name: "browserbase",
status: "fail",
};
}
if (target.kind === "auto-connect") {
const discovered = await (deps.discoverLocalCdp ?? discoverLocalCdp)();
if (discovered) {
return {
details: { source: discovered.source, wsUrl: discovered.wsUrl },
message: `found local browser via ${formatDiscoverySource(discovered)}`,
name: "cdp",
status: "ok",
};
}
return {
fix: "start Chrome with --remote-debugging-port=9222",
message: "no debuggable local browser found",
name: "cdp",
status: "fail",
};
}
if (target.kind === "cdp") {
return {
details: { endpoint: target.endpoint, targetId: target.targetId },
message: `resolved ${target.endpoint}${flags["target-id"] ? ` target ${flags["target-id"]}` : ""}`,
name: "cdp",
status: "ok",
};
}
return null;
}
async function inspectDaemonFiles(paths, deps) {
const isAlive = deps.isProcessAlive ?? isProcessAlive;
const inspection = {};
const pid = await readPositiveInteger(paths.pid);
if (pid && isAlive(pid)) {
inspection.alivePid = pid;
}
const lockPid = await readPositiveInteger(paths.lock);
if (lockPid) {
inspection.lock = isAlive(lockPid) ? "active" : "stale";
}
else if (await exists(paths.lock)) {
inspection.lock = "unreadable";
}
return inspection;
}
async function readPositiveInteger(file) {
try {
const value = Number((await fs.readFile(file, "utf8")).trim());
return Number.isInteger(value) && value > 0 ? value : null;
}
catch {
return null;
}
}
async function exists(file) {
try {
await fs.access(file);
return true;
}
catch {
return false;
}
}
async function readPackageVersion() {
try {
const pkg = JSON.parse(await fs.readFile(new URL("../../../package.json", import.meta.url), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "unknown";
}
catch {
return "unknown";
}
}
function reportVerdict(checks) {
if (checks.some((check) => check.status === "fail"))
return "fail";
if (checks.some((check) => check.status === "warn"))
return "warn";
return "ok";
}
function nextStep(verdict, checks, target, flags, session, status) {
if (verdict !== "ok" || !target)
return undefined;
if (status && !hasExplicitDriverTarget(flags)) {
return session === "default"
? "browse status"
: `browse status --session ${session}`;
}
const parts = ["browse open", DEFAULT_URL];
if (target.kind === "remote")
parts.push("--remote");
if (target.kind === "auto-connect")
parts.push("--auto-connect");
if (target.kind === "cdp") {
parts.push("--cdp", flags.cdp ?? target.endpoint);
if (target.targetId)
parts.push("--target-id", target.targetId);
}
if (target.kind === "managed-local") {
parts.push("--local");
if (!target.headless)
parts.push("--headed");
}
if (session !== "default")
parts.push("--session", session);
if (checks.some((check) => check.name === "browser" ||
check.name === "browserbase" ||
check.name === "cdp")) {
return parts.join(" ");
}
return undefined;
}
function formatTarget(target) {
if (target.kind === "managed-local")
return `managed-local, ${target.headless ? "headless" : "headed"}`;
if (target.kind === "cdp")
return target.targetId ? `cdp, target ${target.targetId}` : "cdp";
return target.kind;
}
function formatDiscoverySource(discovered) {
return discovered.source.startsWith("DevToolsActivePort:")
? "DevToolsActivePort"
: discovered.source;
}
function statusLabel(status) {
if (status === "ok")
return "[ok] ";
if (status === "warn")
return "[warn]";
if (status === "fail")
return "[fail]";
return "[skip]";
}
function errorMessage(error) {
if (error instanceof CommandFailure)
return error.message;
if (error instanceof Error)
return error.message;
return String(error);
}