UNPKG

browse

Version:

Unified Browserbase CLI for browser automation and cloud APIs.

191 lines (190 loc) 6.12 kB
import Browserbase, { APIConnectionError, APIConnectionTimeoutError, APIError, } from "@browserbasehq/sdk"; import { constants, createReadStream } from "node:fs"; import { access, mkdir, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { Readable } from "node:stream"; import { CommandFailure, fail } from "../errors.js"; export { outputJson } from "../output.js"; const defaultBrowserbaseApiUrl = "https://api.browserbase.com"; const browserbaseSettingsUrl = "https://browserbase.com/settings"; export function resolveApiKey(args) { const apiKey = args.apiKey ?? process.env.BROWSERBASE_API_KEY; return (apiKey || fail([ "Missing Browserbase API key. Set BROWSERBASE_API_KEY or pass --api-key.", `You can find your API key at ${browserbaseSettingsUrl}.`, ].join("\n"))); } export function resolveBaseUrl(args) { return args.baseUrl ?? process.env.BROWSERBASE_BASE_URL; } export function resolveApiBaseUrl(args) { return resolveBaseUrl(args) ?? defaultBrowserbaseApiUrl; } export function createBrowserbaseClient(args) { return new Browserbase({ apiKey: resolveApiKey(args), baseURL: resolveBaseUrl(args), }); } export async function withBrowserbaseApi(command, operation) { try { return await operation(); } catch (error) { rethrowBrowserbaseApiError(error, command); } } export function parseOptionalJsonObjectArg(rawValue, label) { if (!rawValue) { return {}; } if (typeof rawValue !== "string") { fail(`${label} must be provided as a JSON string.`); } let parsed; try { parsed = JSON.parse(rawValue); } catch (error) { fail(`Invalid JSON for ${label}: ${error.message}`); } if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") { fail(`${label} must be a JSON object.`); } return parsed; } export async function resolveUploadableFile(filePath, label) { const absolutePath = resolve(filePath); try { await access(absolutePath, constants.R_OK); } catch { fail(`Could not read ${label} file: ${absolutePath}`); } return createReadStream(absolutePath); } export async function readBrowserbaseError(response) { let text; try { text = await response.text(); } catch { return `${response.status} ${response.statusText}`; } if (!text) { return `${response.status} ${response.statusText}`; } try { const data = JSON.parse(text); if (typeof data === "object" && data !== null) { const message = data.message || data.error; if (message) { return message; } } } catch { return text; } return `${response.status} ${response.statusText}`; } export async function requestBrowserbase(args, pathname, init = {}) { let response; try { response = await fetch(new URL(pathname, resolveApiBaseUrl(args)), { ...init, headers: { "x-bb-api-key": resolveApiKey(args), ...(init.headers ?? {}), }, }); } catch (error) { if (error instanceof CommandFailure) { throw error; } fail(error instanceof Error ? error.message : String(error)); } if (!response.ok) { fail(await readBrowserbaseError(response)); } return response; } export async function requestBrowserbaseJson(args, pathname, init = {}) { const response = await requestBrowserbase(args, pathname, init); return (await response.json()); } export async function writeOutputFile(pathname, contents) { const absolutePath = resolve(pathname); try { await mkdir(dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, contents, "utf8"); } catch (error) { fail(error instanceof Error ? error.message : String(error)); } } export async function writeBinaryOutput(pathname, contents) { const absolutePath = resolve(pathname); try { await mkdir(dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, contents); } catch (error) { fail(error instanceof Error ? error.message : String(error)); } } export async function readStdin() { if (process.stdin.isTTY) { fail('--stdin requires piped input. Example: echo \'{"key":"value"}\' | browse cloud <command> --stdin'); } const chunks = []; for await (const chunk of Readable.toWeb(process.stdin)) { chunks.push(Buffer.from(chunk)); } return Buffer.concat(chunks).toString("utf8").trim(); } export async function resolveBody(options) { if (options.body && options.stdin) { fail("Cannot use both --body and --stdin. Provide one or the other."); } if (options.stdin) { const input = await readStdin(); return parseOptionalJsonObjectArg(input, "stdin"); } return parseOptionalJsonObjectArg(options.body, "body"); } export function deepMerge(base, override) { const result = { ...base }; for (const key of Object.keys(override)) { const baseValue = result[key]; const overrideValue = override[key]; if (baseValue && overrideValue && typeof baseValue === "object" && typeof overrideValue === "object" && !Array.isArray(baseValue) && !Array.isArray(overrideValue)) { result[key] = deepMerge(baseValue, overrideValue); } else { result[key] = overrideValue; } } return result; } function rethrowBrowserbaseApiError(error, command) { if (error instanceof CommandFailure) { throw error; } if (error instanceof APIConnectionTimeoutError || error instanceof APIConnectionError) { fail(error.message); } if (error instanceof APIError) { fail(error.message || `${command} request failed`); } throw error; }