browse
Version:
Unified Browserbase CLI for browser automation and cloud APIs.
191 lines (190 loc) • 6.12 kB
JavaScript
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;
}