From a15e1340496ec2c67602e44b8a3515f983703b4c Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 05:28:57 +0000 Subject: [PATCH 1/3] refactor(cli): consume shared Functions core --- .changeset/calm-functions-core.md | 5 + packages/cli/package.json | 5 +- packages/cli/src/lib/functions/dev.ts | 749 +----------------- packages/cli/src/lib/functions/init.ts | 149 +--- packages/cli/src/lib/functions/invoke.ts | 85 +- packages/cli/src/lib/functions/publish.ts | 281 +------ packages/cli/src/lib/functions/shared.ts | 193 ++--- .../cli/tests/cli-functions-contract.test.ts | 9 + pnpm-lock.yaml | 45 +- pnpm-workspace.yaml | 4 + 10 files changed, 216 insertions(+), 1309 deletions(-) create mode 100644 .changeset/calm-functions-core.md diff --git a/.changeset/calm-functions-core.md b/.changeset/calm-functions-core.md new file mode 100644 index 000000000..a81a93429 --- /dev/null +++ b/.changeset/calm-functions-core.md @@ -0,0 +1,5 @@ +--- +"browse": patch +--- + +Use the shared `@browserbasehq/sdk-functions/core` implementation for Functions scaffolding, local development, publishing, and invocation. diff --git a/packages/cli/package.json b/packages/cli/package.json index bad7b52f2..4e1f4eb40 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -48,24 +48,21 @@ }, "dependencies": { "@browserbasehq/sdk": "^2.14.0", + "@browserbasehq/sdk-functions": "catalog:", "@oclif/core": "^4.11.0", "@vercel/detect-agent": "^1.2.3", - "archiver": "^7.0.1", "deepmerge": "^4.3.1", "dotenv": "^16.5.0", "fastest-levenshtein": "^1.0.16", "http-status-codes": "^2.3.0", - "ignore": "^7.0.5", "node-html-markdown": "^1.3.0", "semver": "^7.7.4", "stagehand-v3": "npm:@browserbasehq/stagehand@3.7.1", - "tsx": "^4.20.6", "ws": "^8.18.3", "zod": "^4.2.1" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/archiver": "^6.0.3", "@types/node": "^20.11.30", "@types/semver": "^7.7.1", "@types/ws": "^8.18.1", diff --git a/packages/cli/src/lib/functions/dev.ts b/packages/cli/src/lib/functions/dev.ts index e1811625d..bc4678f84 100644 --- a/packages/cli/src/lib/functions/dev.ts +++ b/packages/cli/src/lib/functions/dev.ts @@ -1,18 +1,7 @@ -import { createRequire } from "node:module"; -import { spawn } from "node:child_process"; -import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; -import { mkdir, readdir, readFile } from "node:fs/promises"; -import { existsSync } from "node:fs"; -import { join } from "node:path"; -import { randomUUID } from "node:crypto"; +import { startDevServer, type DevServerHandle } from "@browserbasehq/sdk-functions/core"; import { fail } from "../errors.js"; -import { - functionsRequest, - resolveEntrypoint, - resolveFunctionsProjectConfig, - type FunctionsProjectConfig, -} from "./shared.js"; +import { resolveFunctionsCoreOptions, runFunctionsCore } from "./shared.js"; const DEFAULT_RUNTIME_STARTUP_TIMEOUT_MS = 10_000; @@ -26,441 +15,53 @@ export interface StartFunctionsDevServerOptions { verbose: boolean; } -interface InvocationContext { - invocation: { - id: string; - region: "local"; - }; - session: { - id: string; - connectUrl: string; - }; -} - -interface PendingConnection { - corsHeaders: Record; - response: ServerResponse; -} - -interface FunctionManifest { - name: string; - config?: { - sessionConfig?: Record; - }; -} - -class InvocationBridge { - private cleanupSessionCallback: ((sessionId: string) => Promise) | null = null; - private currentRequestId: string | null = null; - private currentSessionId: string | null = null; - private invokeConnection: PendingConnection | null = null; - private nextConnection: PendingConnection | null = null; - private runtimeConnected = false; - - setCleanupSessionCallback(callback: (sessionId: string) => Promise) { - this.cleanupSessionCallback = callback; - } - - holdNextConnection(response: ServerResponse, corsHeaders: Record) { - this.runtimeConnected = true; - if (this.nextConnection) { - this.nextConnection.response.writeHead(503, { - ...this.nextConnection.corsHeaders, - "content-type": "application/json", - }); - this.nextConnection.response.end( - JSON.stringify({ error: "Another runtime process connected." }), - ); - } - this.nextConnection = { corsHeaders, response }; - } - - isRuntimeConnected() { - return this.runtimeConnected && this.nextConnection !== null; - } - - hasActiveInvocation() { - return this.invokeConnection !== null; - } - - async completeWithSuccess(requestId: string, payload: unknown) { - if (requestId !== this.currentRequestId || !this.invokeConnection) { - return false; - } - - sendJson(this.invokeConnection.response, 200, payload ?? {}, this.invokeConnection.corsHeaders); - try { - await this.cleanupSession(); - } catch (error) { - this.reportCleanupError(error); - } finally { - this.reset(); - } - return true; - } - - async completeWithError( - requestId: string, - payload: { errorMessage: string; errorType: string; stackTrace: string[] }, - ) { - if (requestId !== this.currentRequestId || !this.invokeConnection) { - return false; - } - - sendJson( - this.invokeConnection.response, - 500, - { - error: { - message: payload.errorMessage, - stackTrace: payload.stackTrace, - type: payload.errorType, - }, - }, - this.invokeConnection.corsHeaders, - ); - try { - await this.cleanupSession(); - } catch (error) { - this.reportCleanupError(error); - } finally { - this.reset(); - } - return true; - } - - triggerInvocation( - functionName: string, - params: Record, - context: InvocationContext, - corsHeaders: Record, - response: ServerResponse, - ): boolean { - if (!this.nextConnection || this.invokeConnection) { - return false; - } - - const requestId = randomUUID(); - this.currentRequestId = requestId; - this.currentSessionId = context.session.id; - this.invokeConnection = { corsHeaders, response }; - - this.nextConnection.response.writeHead(200, { - ...this.nextConnection.corsHeaders, - "content-type": "application/json", - "Lambda-Runtime-Aws-Request-Id": requestId, - "Lambda-Runtime-Deadline-Ms": String(Date.now() + 300_000), - "Lambda-Runtime-Invoked-Function-Arn": `arn:aws:lambda:us-east-1:000000000000:function:${functionName}`, - }); - this.nextConnection.response.end( - JSON.stringify({ - context, - functionName, - params, - }), - ); - this.nextConnection = null; - return true; - } - - private async cleanupSession() { - if (this.cleanupSessionCallback && this.currentSessionId) { - await this.cleanupSessionCallback(this.currentSessionId); - } - } - - private reportCleanupError(error: unknown) { - const message = error instanceof Error ? error.message : String(error); - process.stderr.write(`Functions dev session cleanup failed: ${message}\n`); - } - - private reset() { - this.currentRequestId = null; - this.currentSessionId = null; - this.invokeConnection = null; - } -} - -class BrowserSessionManager { - constructor(private readonly config: FunctionsProjectConfig) {} - - async createSession( - sessionConfig: Record = {}, - ): Promise { - const response = await functionsRequest(this.config, "/v1/sessions", { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ - projectId: this.config.projectId, - ...sessionConfig, - }), - }); - const session = (await response.json()) as { - id?: string; - connectUrl?: string; - }; - if (!session.id || !session.connectUrl) { - fail("Browserbase session create completed without returning id and connectUrl."); - } - return { - connectUrl: session.connectUrl, - id: session.id, - }; - } - - async closeSession(sessionId: string): Promise { - await functionsRequest(this.config, `/v1/sessions/${sessionId}`, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ - projectId: this.config.projectId, - status: "REQUEST_RELEASE", - }), - }); - } -} - -class ManifestStore { - private readonly manifestsPath = join(process.cwd(), ".browserbase", "functions", "manifests"); - - private readonly manifests = new Map(); - - async load(): Promise { - this.manifests.clear(); - if (!existsSync(this.manifestsPath)) { - return; - } - - const entries = await readdir(this.manifestsPath); - for (const entry of entries) { - if (!entry.endsWith(".json")) { - continue; - } - const manifest = JSON.parse( - await readFile(join(this.manifestsPath, entry), "utf8"), - ) as FunctionManifest; - this.manifests.set(manifest.name, manifest); - } - } - - get(name: string): FunctionManifest | undefined { - return this.manifests.get(name); - } -} - -class RuntimeProcess { - private process: ReturnType | null = null; - - constructor( - private readonly entrypoint: string, - private readonly runtimeApi: string, - private readonly verbose: boolean, - ) {} - - async start() { - const require = createRequire(import.meta.url); - const tsxCli = require.resolve("tsx/cli"); - const nodeExecutable = "bun" in process.versions ? "node" : process.execPath; - const child = spawn( - nodeExecutable, - [tsxCli, "watch", "--clear-screen=false", this.entrypoint], - { - cwd: process.cwd(), - env: { - ...process.env, - AWS_LAMBDA_RUNTIME_API: this.runtimeApi, - BB_FUNCTIONS_PHASE: "runtime", - NODE_ENV: "local", - }, - stdio: ["ignore", "pipe", "pipe"], - }, - ); - this.process = child; - - child.stdout?.on("data", (chunk) => { - const text = chunk.toString().trim(); - if (text) { - process.stderr.write(`${this.verbose ? "[runtime] " : ""}${text}\n`); - } - }); - - child.stderr?.on("data", (chunk) => { - const text = chunk.toString().trim(); - if (text) { - process.stderr.write(`${this.verbose ? "[runtime:error] " : ""}${text}\n`); - } - }); - - child.once("exit", () => { - if (this.process === child) { - this.process = null; - } - }); - - try { - await waitForChildSpawn(child); - } catch (error) { - this.process = null; - fail(`Failed to start functions runtime: ${formatErrorMessage(error)}`); - } - } - - async stop() { - const child = this.process; - if (!child) { - return; - } - - if (child.exitCode !== null || child.signalCode !== null) { - this.process = null; - return; - } - - await new Promise((resolvePromise) => { - const forceKillTimer = setTimeout(() => { - child.kill("SIGKILL"); - }, 5_000); - const finish = () => { - clearTimeout(forceKillTimer); - resolvePromise(); - }; - - child.once("exit", finish); - if (!child.kill("SIGTERM")) { - child.off("exit", finish); - finish(); - } - }); - this.process = null; - } -} - export async function startFunctionsDevServer( options: StartFunctionsDevServerOptions, ): Promise { - const entrypoint = await resolveEntrypoint(options.entrypoint); - if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65_535) { - fail("Port must be an integer between 1 and 65535."); - } - - const config = resolveFunctionsProjectConfig(options); - const runtimeApi = `${options.host}:${options.port}`; - const bridge = new InvocationBridge(); - const sessionManager = new BrowserSessionManager(config); - const manifestStore = new ManifestStore(); - - bridge.setCleanupSessionCallback(async (sessionId) => { - await sessionManager.closeSession(sessionId); - }); - - await mkdir(join(process.cwd(), ".browserbase", "functions", "manifests"), { - recursive: true, - }); - - const server = await startServer( - options.host, - options.port, - bridge, - manifestStore, - sessionManager, + const coreOptions = resolveFunctionsCoreOptions(options); + const handle = await runFunctionsCore(() => + startDevServer({ + ...coreOptions, + entrypoint: options.entrypoint, + host: options.host, + port: options.port, + ...(options.projectId ? { projectId: options.projectId } : {}), + startupTimeoutMs: getRuntimeStartupTimeoutMs(), + verbose: options.verbose, + onLog(event) { + process.stderr.write(`${event.message}\n`); + }, + }), ); - const runtime = new RuntimeProcess(entrypoint, runtimeApi, options.verbose); - await runtime.start(); - const runtimeConnected = await waitForRuntime( - bridge, - manifestStore, - getRuntimeStartupTimeoutMs(), + console.log( + JSON.stringify( + { + ok: handle.runtimeConnected, + runtimeConnected: handle.runtimeConnected, + url: handle.url, + ...(!handle.runtimeConnected + ? { + warning: [ + "Functions runtime has not connected yet.", + "Check the runtime logs, then retry once the entrypoint is healthy.", + ].join(" "), + } + : {}), + }, + null, + 2, + ), ); - const output: { - ok: boolean; - runtimeConnected: boolean; - url: string; - warning?: string; - } = { - ok: runtimeConnected, - runtimeConnected, - url: `http://${options.host}:${options.port}`, - }; - if (!runtimeConnected) { - output.warning = [ - "Functions runtime has not connected yet.", - "Check the runtime logs, then retry once the entrypoint is healthy.", - ].join(" "); - } - console.log(JSON.stringify(output, null, 2)); - - const shutdown = async () => { - await runtime.stop(); - await new Promise((resolvePromise) => server.close(() => resolvePromise())); - }; - - process.on("SIGINT", async () => { - await shutdown(); - process.exit(0); - }); - process.on("SIGTERM", async () => { - await shutdown(); - process.exit(0); - }); + installShutdownHandler("SIGINT", handle); + installShutdownHandler("SIGTERM", handle); } -async function startServer( - host: string, - port: number, - bridge: InvocationBridge, - manifestStore: ManifestStore, - sessionManager: BrowserSessionManager, -): Promise { - const server = createServer((request, response) => { - routeRequest(request, response, bridge, manifestStore, sessionManager).catch((error) => { - handleRouteError(response, error); - }); - }); - - await new Promise((resolvePromise, reject) => { - server.listen(port, host, () => resolvePromise()); - server.on("error", reject); +function installShutdownHandler(signal: "SIGINT" | "SIGTERM", handle: DevServerHandle): void { + process.once(signal, () => { + void handle.close().then(() => process.exit(0)); }); - - return server; -} - -function handleRouteError(response: ServerResponse, error: unknown): void { - const message = error instanceof Error ? error.message : String(error); - process.stderr.write(`Functions dev request failed: ${message}\n`); - - if (!response.headersSent && !response.writableEnded) { - sendJson(response, 500, { error: message }, baseCorsHeaders()); - return; - } - - if (!response.writableEnded) { - response.end(); - } -} - -async function waitForRuntime( - bridge: InvocationBridge, - manifestStore: ManifestStore, - timeoutMs: number, -): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (bridge.isRuntimeConnected()) { - await manifestStore.load(); - return true; - } - await new Promise((resolvePromise) => setTimeout(resolvePromise, 200)); - } - - await manifestStore.load(); - return bridge.isRuntimeConnected(); } function getRuntimeStartupTimeoutMs(): number { @@ -473,277 +74,5 @@ function getRuntimeStartupTimeoutMs(): number { if (!Number.isFinite(parsed) || parsed < 0) { fail("BROWSERBASE_FUNCTIONS_DEV_STARTUP_TIMEOUT_MS must be a non-negative number."); } - return parsed; } - -async function routeRequest( - request: IncomingMessage, - response: ServerResponse, - bridge: InvocationBridge, - manifestStore: ManifestStore, - sessionManager: BrowserSessionManager, -): Promise { - const method = request.method || "GET"; - const url = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`); - const path = url.pathname; - const corsHeaders = corsHeadersForRequest(request); - - if (!corsHeaders) { - sendForbiddenOrigin(response); - return; - } - - if (method === "OPTIONS") { - sendNoContent(response, 204, corsHeaders); - return; - } - - if (method === "GET" && path === "/") { - sendJson(response, 200, { ok: true }, corsHeaders); - return; - } - - if (method === "GET" && path === "/2018-06-01/runtime/invocation/next") { - bridge.holdNextConnection(response, corsHeaders); - return; - } - - const invokeMatch = path.match(/^\/v1\/functions\/([^/]+)\/invoke$/); - if (method === "POST" && invokeMatch?.[1]) { - await manifestStore.load(); - const functionName = invokeMatch[1]; - const manifest = manifestStore.get(functionName); - if (!manifest) { - sendJson( - response, - 404, - { - error: `Function "${functionName}" was not found in .browserbase/functions/manifests.`, - }, - corsHeaders, - ); - return; - } - - if (bridge.hasActiveInvocation()) { - sendJson(response, 503, { error: "Another invocation is already in progress." }, corsHeaders); - return; - } - - let body; - try { - body = await readJsonBody(request); - } catch (error) { - sendJson( - response, - 400, - { - error: error instanceof Error ? error.message : "Invalid JSON body.", - }, - corsHeaders, - ); - return; - } - - const params = - body && typeof body === "object" && !Array.isArray(body) - ? (body as { params?: Record }).params || {} - : {}; - - const session = await sessionManager.createSession(manifest.config?.sessionConfig); - const accepted = bridge.triggerInvocation( - functionName, - params, - { - invocation: { - id: randomUUID(), - region: "local", - }, - session, - }, - corsHeaders, - response, - ); - - if (!accepted) { - await sessionManager.closeSession(session.id); - sendJson(response, 503, { error: "No runtime is connected yet." }, corsHeaders); - } - return; - } - - const responseMatch = path.match(/^\/2018-06-01\/runtime\/invocation\/([^/]+)\/response$/); - if (method === "POST" && responseMatch?.[1]) { - const requestId = responseMatch[1]; - let payload; - try { - payload = await readJsonBody(request); - } catch (error) { - const message = `Invalid runtime response payload: ${formatErrorMessage(error)}`; - const completed = await bridge.completeWithError(requestId, { - errorMessage: message, - errorType: "RuntimeResponseError", - stackTrace: [], - }); - sendJson( - response, - 400, - completed ? { error: message } : { error: "Request ID mismatch." }, - corsHeaders, - ); - return; - } - const completed = await bridge.completeWithSuccess(requestId, payload); - sendJson( - response, - completed ? 202 : 400, - completed ? { ok: true } : { error: "Request ID mismatch." }, - corsHeaders, - ); - return; - } - - const errorMatch = path.match(/^\/2018-06-01\/runtime\/invocation\/([^/]+)\/error$/); - if (method === "POST" && errorMatch?.[1]) { - const requestId = errorMatch[1]; - let payload; - try { - payload = (await readJsonBody(request)) as { - errorMessage?: string; - errorType?: string; - stackTrace?: string[]; - }; - } catch (error) { - const message = `Invalid runtime error payload: ${formatErrorMessage(error)}`; - const completed = await bridge.completeWithError(requestId, { - errorMessage: message, - errorType: "RuntimeResponseError", - stackTrace: [], - }); - sendJson( - response, - 400, - completed ? { error: message } : { error: "Request ID mismatch." }, - corsHeaders, - ); - return; - } - const runtimeError = { - errorMessage: payload?.errorMessage || "Unknown runtime error", - errorType: payload?.errorType || "RuntimeError", - stackTrace: Array.isArray(payload?.stackTrace) ? payload.stackTrace : [], - }; - const completed = await bridge.completeWithError(requestId, runtimeError); - sendJson( - response, - completed ? 202 : 400, - completed ? { ok: true } : { error: "Request ID mismatch." }, - corsHeaders, - ); - return; - } - - sendJson(response, 404, { error: "Not found." }, corsHeaders); -} - -function formatErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -async function waitForChildSpawn(child: ReturnType): Promise { - await new Promise((resolvePromise, reject) => { - const cleanup = () => { - child.off("error", onError); - child.off("spawn", onSpawn); - }; - const onError = (error: Error) => { - cleanup(); - reject(error); - }; - const onSpawn = () => { - cleanup(); - resolvePromise(); - }; - child.once("error", onError); - child.once("spawn", onSpawn); - }); -} - -async function readJsonBody(request: IncomingMessage): Promise { - const chunks: Uint8Array[] = []; - for await (const chunk of request) { - chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); - } - - if (chunks.length === 0) { - return {}; - } - - const text = Buffer.concat(chunks).toString("utf8"); - if (!text) { - return {}; - } - - return JSON.parse(text); -} - -function sendJson( - response: ServerResponse, - statusCode: number, - body: unknown, - corsHeaders: Record, -): void { - response.writeHead(statusCode, { - ...corsHeaders, - "content-type": "application/json", - }); - response.end(JSON.stringify(body)); -} - -function sendNoContent( - response: ServerResponse, - statusCode: number, - corsHeaders: Record, -): void { - response.writeHead(statusCode, corsHeaders); - response.end(); -} - -function sendForbiddenOrigin(response: ServerResponse): void { - response.writeHead(403, { - "content-type": "application/json", - vary: "Origin", - }); - response.end(JSON.stringify({ error: "Origin is not allowed." })); -} - -function corsHeadersForRequest(request: IncomingMessage): Record | null { - const origin = request.headers.origin; - if (origin === undefined) return baseCorsHeaders(); - if (Array.isArray(origin)) return null; - if (!isAllowedLoopbackOrigin(origin)) return null; - - return { - ...baseCorsHeaders(), - "access-control-allow-origin": origin, - vary: "Origin", - }; -} - -function baseCorsHeaders(): Record { - return { - "access-control-allow-headers": "content-type", - "access-control-allow-methods": "GET, POST, OPTIONS", - }; -} - -function isAllowedLoopbackOrigin(origin: string): boolean { - try { - const url = new URL(origin); - if (url.protocol !== "http:" && url.protocol !== "https:") return false; - return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]"; - } catch { - return false; - } -} diff --git a/packages/cli/src/lib/functions/init.ts b/packages/cli/src/lib/functions/init.ts index 70eb4f0d2..870fc3ea9 100644 --- a/packages/cli/src/lib/functions/init.ts +++ b/packages/cli/src/lib/functions/init.ts @@ -1,57 +1,6 @@ -import { spawnSync } from "node:child_process"; -import { existsSync } from "node:fs"; -import { mkdir, writeFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; +import { createFunctionProject } from "@browserbasehq/sdk-functions/core"; -import { fail } from "../errors.js"; - -const envTemplate = `# Browserbase Configuration -# Get your API key from https://browserbase.com/settings - -BROWSERBASE_API_KEY=your_api_key_here -`; - -const gitignoreTemplate = `node_modules/ -.env -.env.local -dist/ -.browserbase/ -*.log -.DS_Store -`; - -const starterFunctionTemplate = `import { defineFn } from "@browserbasehq/sdk-functions"; -import { chromium } from "playwright-core"; - -defineFn("my-function", async (context) => { - const browser = await chromium.connectOverCDP(context.session.connectUrl); - const page = browser.contexts()[0]!.pages()[0]!; - - await page.goto("https://news.ycombinator.com"); - await page.waitForSelector(".athing", { timeout: 30_000 }); - - const titles = await page.$$eval(".athing .titleline > a", (elements) => - elements.slice(0, 3).map((element) => element.textContent), - ); - - return { - message: "Fetched top Hacker News titles", - titles, - }; -}); -`; - -const tsconfigTemplate = `{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "skipLibCheck": true, - "esModuleInterop": true - } -} -`; +import { runFunctionsCore } from "./shared.js"; export interface InitFunctionsProjectOptions { packageManager: "npm" | "pnpm"; @@ -62,62 +11,27 @@ export async function initFunctionsProject({ packageManager, projectName, }: InitFunctionsProjectOptions): Promise { - if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(projectName)) { - fail( - `Invalid project name "${projectName}". Use a leading letter, then letters, numbers, hyphens, or underscores.`, - ); - } - - const packageManagerVersion = ensureCommand(packageManager); - - const projectRoot = resolve(projectName); - if (existsSync(projectRoot)) { - fail(`Directory already exists: ${projectRoot}`); - } - - await mkdir(projectRoot, { recursive: true }); - - const packageJson = { - name: projectName, - version: "1.0.0", - private: true, - packageManager: `${packageManager}@${packageManagerVersion}`, - type: "module", - scripts: { - dev: "browse functions dev index.ts", - deploy: "browse functions publish index.ts", - }, - }; - - await writeFile(join(projectRoot, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`); - await writeFile(join(projectRoot, ".env"), envTemplate); - await writeFile(join(projectRoot, ".gitignore"), gitignoreTemplate); - await writeFile(join(projectRoot, "index.ts"), starterFunctionTemplate); - await writeFile(join(projectRoot, "tsconfig.json"), tsconfigTemplate); - - const install = packageManager === "pnpm" ? ["add"] : ["install"]; - const installDev = packageManager === "pnpm" ? ["add", "-D"] : ["install", "--save-dev"]; - - runPackageManager( - packageManager, - [...install, "@browserbasehq/sdk-functions", "playwright-core", "zod"], - projectRoot, + const result = await runFunctionsCore(() => + createFunctionProject({ + packageManager, + projectName, + scripts: { + deploy: "browse functions publish index.ts", + dev: "browse functions dev index.ts", + }, + onOutput(_stream, text) { + // Keep stdout parseable for the command's final JSON result. + process.stderr.write(text); + }, + }), ); - runPackageManager(packageManager, [...installDev, "typescript", "@types/node"], projectRoot); - - if (!existsSync(join(projectRoot, ".git"))) { - spawnSync("git", ["init"], { - cwd: projectRoot, - stdio: "ignore", - }); - } console.log( JSON.stringify( { ok: true, - packageManager, - projectRoot, + packageManager: result.packageManager, + projectRoot: result.projectRoot, nextSteps: [ `cd ${projectName}`, "Edit .env with your Browserbase API key", @@ -130,32 +44,3 @@ export async function initFunctionsProject({ ), ); } - -function ensureCommand(command: string): string { - const result = spawnSync(command, ["--version"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - if (result.error || result.status !== 0) { - fail(`${command} is required but was not found on PATH.`); - } - return result.stdout.trim(); -} - -function runPackageManager(packageManager: "npm" | "pnpm", args: string[], cwd: string): void { - const result = spawnSync(packageManager, args, { - cwd, - stdio: ["ignore", "pipe", "pipe"], - }); - - if (result.stdout.length > 0) { - process.stderr.write(result.stdout); - } - if (result.stderr.length > 0) { - process.stderr.write(result.stderr); - } - - if (result.error || result.status !== 0) { - fail(`Failed to install dependencies with ${packageManager}.`); - } -} diff --git a/packages/cli/src/lib/functions/invoke.ts b/packages/cli/src/lib/functions/invoke.ts index 91e2097c1..450b49058 100644 --- a/packages/cli/src/lib/functions/invoke.ts +++ b/packages/cli/src/lib/functions/invoke.ts @@ -1,12 +1,12 @@ -import { fail } from "../errors.js"; -import { setRunTelemetryCompletion } from "../run-telemetry.js"; import { - functionsGet, - functionsPost, - parseOptionalJsonValueArg, - pollUntil, - resolveFunctionsApiConfig, -} from "./shared.js"; + FunctionsCoreError, + invokeFunction as invokeFunctionCore, + parseJsonArgument, + type InvocationResponse, +} from "@browserbasehq/sdk-functions/core"; + +import { setRunTelemetryCompletion } from "../run-telemetry.js"; +import { rethrowFunctionsCoreError, resolveFunctionsCoreOptions } from "./shared.js"; export interface InvokeFunctionOptions { apiKey?: string; @@ -17,59 +17,24 @@ export interface InvokeFunctionOptions { params?: string; } -interface InvocationResponse { - id: string; - functionId: string; - status: string; - sessionId?: string; - startedAt?: string; - endedAt?: string; - results?: unknown; -} - export async function invokeFunction(options: InvokeFunctionOptions): Promise { - const config = resolveFunctionsApiConfig(options); - - if (options.checkStatus) { - const status = await functionsGet( - config, - `/v1/functions/invocations/${options.checkStatus}`, - ); - console.log(JSON.stringify(status, null, 2)); - return; - } - - if (!options.functionId) { - fail("functionId is required unless --check-status is used."); - } - - const params = parseOptionalJsonValueArg(options.params, "params"); - const invocation = await functionsPost( - config, - `/v1/functions/${options.functionId}/invoke`, - { params }, - ); - - if (options.noWait) { - console.log(JSON.stringify(invocation, null, 2)); - return; - } - - const finalStatus = await pollUntil( - () => functionsGet(config, `/v1/functions/invocations/${invocation.id}`), - { - done: (result) => !["PENDING", "RUNNING"].includes(result.status), - intervalMs: 1_000, - maxAttempts: 900, - }, - ); - - console.log(JSON.stringify(finalStatus, null, 2)); - - if (finalStatus.status === "FAILED") { - setRunTelemetryCompletion({ - resultCode: "functions_invocation_failed", + try { + const coreOptions = resolveFunctionsCoreOptions(options); + const result = await invokeFunctionCore({ + ...coreOptions, + ...(options.checkStatus ? { checkStatus: options.checkStatus } : {}), + ...(options.functionId ? { functionId: options.functionId } : {}), + noWait: options.noWait, + params: parseJsonArgument(options.params, "params"), }); - process.exitCode = 1; + console.log(JSON.stringify(result, null, 2)); + } catch (error) { + if (error instanceof FunctionsCoreError && error.code === "invocation_failed") { + console.log(JSON.stringify(error.responseBody as InvocationResponse, null, 2)); + setRunTelemetryCompletion({ resultCode: "functions_invocation_failed" }); + process.exitCode = 1; + return; + } + rethrowFunctionsCoreError(error); } } diff --git a/packages/cli/src/lib/functions/publish.ts b/packages/cli/src/lib/functions/publish.ts index 11ebd2e99..85f900192 100644 --- a/packages/cli/src/lib/functions/publish.ts +++ b/packages/cli/src/lib/functions/publish.ts @@ -1,28 +1,11 @@ -import archiver from "archiver"; -import ignore from "ignore"; import { - copyFileSync, - createWriteStream, - existsSync, - mkdirSync, - readFileSync, - rmSync, -} from "node:fs"; -import { readFile, readdir, stat } from "node:fs/promises"; -import { spawnSync } from "node:child_process"; -import { tmpdir } from "node:os"; -import { dirname, join, relative } from "node:path"; -import { randomUUID } from "node:crypto"; + FunctionsCoreError, + publishFunction as publishFunctionCore, + type BuildStatusResponse, +} from "@browserbasehq/sdk-functions/core"; -import { fail } from "../errors.js"; import { setRunTelemetryCompletion } from "../run-telemetry.js"; -import { - functionsGet, - functionsRequest, - pollUntil, - resolveEntrypoint, - resolveFunctionsProjectConfig, -} from "./shared.js"; +import { rethrowFunctionsCoreError, resolveFunctionsCoreOptions } from "./shared.js"; export interface PublishFunctionOptions { apiKey?: string; @@ -32,241 +15,37 @@ export interface PublishFunctionOptions { projectId?: string; } -interface BuildUploadResponse { - id?: string; -} - -interface BuildStatusResponse { - id: string; - status: string; - request?: { - entrypoint?: string; - }; - builtFunctions?: Array<{ - id: string; - name: string; - createdVersion?: { - id: string; - }; - }>; -} - -const defaultIgnorePatterns = [ - "node_modules/", - ".git/", - ".env", - ".env.*", - "*.log", - ".DS_Store", - "dist/", - "build/", - "*.zip", - "*.tar", - "*.tar.gz", - ".vscode/", - ".idea/", - ".browserbase/", -]; - -const maxArchiveSizeBytes = 50 * 1024 * 1024; - export async function publishFunction(options: PublishFunctionOptions): Promise { - const entrypoint = await resolveEntrypoint(options.entrypoint); - const config = resolveFunctionsProjectConfig(options); - const entrypointPath = relative(process.cwd(), entrypoint); - - if (options.dryRun) { - const entries = await listPublishEntries(process.cwd()); - console.log( - JSON.stringify( - { - archivePath: null, - baseUrl: config.baseUrl, - dryRun: true, - entrypoint: entrypointPath, - files: entries, - projectId: config.projectId, - }, - null, - 2, - ), - ); - return; - } - - const { archivePath } = await createArchive(process.cwd()); try { - const archiveStats = await stat(archivePath); - if (archiveStats.size > maxArchiveSizeBytes) { - fail( - `Functions archive is ${(archiveStats.size / 1024 / 1024).toFixed(2)} MB; the maximum is 50 MB. Add files to .gitignore to reduce its size.`, - ); - } - - const formData = new FormData(); - formData.append( - "metadata", - JSON.stringify({ entrypoint: entrypointPath, projectId: config.projectId }), - ); - formData.append( - "archive", - new Blob([await readFile(archivePath)], { type: "application/gzip" }), - "archive.tar.gz", - ); - - const uploadResponse = await functionsRequest(config, "/v1/functions/builds", { - method: "POST", - body: formData, + const coreOptions = resolveFunctionsCoreOptions(options); + const result = await publishFunctionCore({ + ...coreOptions, + dryRun: options.dryRun, + entrypoint: options.entrypoint, + ...(options.projectId ? { projectId: options.projectId } : {}), }); - const uploaded = (await uploadResponse.json()) as BuildUploadResponse; - if (!uploaded.id) { - fail("Build upload completed without returning a build ID.", 1, { - resultCode: "functions_build_missing_id", - }); + if (result.dryRun) { + console.log( + JSON.stringify( + { + archivePath: null, + ...result, + }, + null, + 2, + ), + ); + return; } - - const build = await pollUntil( - () => functionsGet(config, `/v1/functions/builds/${uploaded.id}`), - { - done: (result) => !["PENDING", "RUNNING"].includes(result.status), - intervalMs: 2_000, - maxAttempts: 100, - }, - ); - - console.log(JSON.stringify(build, null, 2)); - - if (build.status === "FAILED") { - setRunTelemetryCompletion({ - resultCode: "functions_build_failed", - }); + console.log(JSON.stringify(result.build, null, 2)); + } catch (error) { + if (error instanceof FunctionsCoreError && error.code === "build_failed") { + console.log(JSON.stringify(error.responseBody as BuildStatusResponse, null, 2)); + setRunTelemetryCompletion({ resultCode: "functions_build_failed" }); process.exitCode = 1; + return; } - } finally { - rmSync(archivePath, { force: true }); - } -} - -async function createArchive(root: string): Promise<{ - archivePath: string; - entries: string[]; -}> { - const archivePath = join(tmpdir(), `browserbase-functions-${randomUUID()}.tar.gz`); - const sourceEntries = await listPublishEntries(root); - const { entries, generatedLockfilePath } = ensureArchiveLockfile(root, sourceEntries); - - try { - await new Promise((resolvePromise, reject) => { - const output = createWriteStream(archivePath); - const archive = archiver("tar", { - gzip: true, - gzipOptions: { level: 9 }, - }); - - archive.on("error", reject); - archive.on("warning", (warning: Error & { code?: string }) => { - if (warning.code === "ENOENT") { - return; - } - reject(warning); - }); - output.on("close", () => resolvePromise()); - output.on("error", reject); - - archive.pipe(output); - - for (const entry of entries) { - if (entry === "package-lock.json" && generatedLockfilePath) { - archive.file(generatedLockfilePath, { name: entry }); - } else { - archive.file(join(root, entry), { name: entry }); - } - } - - archive.finalize().catch(reject); - }); - } finally { - if (generatedLockfilePath) { - rmSync(dirname(generatedLockfilePath), { recursive: true, force: true }); - } - } - - return { archivePath, entries }; -} - -async function listPublishEntries(root: string): Promise { - const ignoreMatcher = await loadIgnoreMatcher(root); - return await listArchiveEntries(root, root, ignoreMatcher); -} - -function ensureArchiveLockfile( - root: string, - entries: string[], -): { entries: string[]; generatedLockfilePath?: string } { - if (!entries.includes("package.json") || entries.includes("package-lock.json")) { - return { entries }; - } - - const tempDir = join(tmpdir(), `bb-functions-lockgen-${randomUUID()}`); - mkdirSync(tempDir, { recursive: true }); - copyFileSync(join(root, "package.json"), join(tempDir, "package.json")); - - const result = spawnSync("npm", ["install", "--package-lock-only"], { - cwd: tempDir, - stdio: "pipe", - }); - - if (result.status !== 0) { - rmSync(tempDir, { recursive: true, force: true }); - fail("Failed to generate package-lock.json for the Functions build archive."); - } - - return { - entries: [...entries, "package-lock.json"].sort(), - generatedLockfilePath: join(tempDir, "package-lock.json"), - }; -} - -async function loadIgnoreMatcher(root: string) { - const matcher = ignore(); - matcher.add(defaultIgnorePatterns); - - const gitignorePath = join(root, ".gitignore"); - if (existsSync(gitignorePath)) { - matcher.add(readFileSync(gitignorePath, "utf8")); - } - - return matcher; -} - -async function listArchiveEntries( - root: string, - current: string, - matcher: ignore.Ignore, -): Promise { - const entries = await readdir(current, { withFileTypes: true }); - const files: string[] = []; - - for (const entry of entries) { - const absolutePath = join(current, entry.name); - const relativePath = relative(root, absolutePath) || "."; - const ignorePath = entry.isDirectory() ? `${relativePath}/` : relativePath; - - if (relativePath !== "." && matcher.ignores(ignorePath)) { - continue; - } - - if (entry.isDirectory()) { - files.push(...(await listArchiveEntries(root, absolutePath, matcher))); - continue; - } - - const fileStats = await stat(absolutePath); - if (fileStats.isFile()) { - files.push(relativePath); - } + rethrowFunctionsCoreError(error); } - - return files.sort(); } diff --git a/packages/cli/src/lib/functions/shared.ts b/packages/cli/src/lib/functions/shared.ts index caa53a2fe..066a892e6 100644 --- a/packages/cli/src/lib/functions/shared.ts +++ b/packages/cli/src/lib/functions/shared.ts @@ -1,161 +1,74 @@ -import { stat } from "node:fs/promises"; -import { extname, resolve } from "node:path"; +import { + FunctionsCoreError, + type ResolveFunctionsApiConfigOptions, +} from "@browserbasehq/sdk-functions/core"; -import { CommandFailure, fail } from "../errors.js"; -import { classifyCommandHttpFailure, readBrowserbaseError, resolveApiKey } from "../cloud/api.js"; +import { classifyCommandHttpFailure, resolveApiKey } from "../cloud/api.js"; +import { fail } from "../errors.js"; import { setRunTelemetryCompletion } from "../run-telemetry.js"; -const defaultFunctionsBaseUrl = "https://api.browserbase.com"; - -export interface FunctionsApiConfig { - apiKey: string; - baseUrl: string; -} - -export interface FunctionsProjectConfig extends FunctionsApiConfig { - projectId?: string; -} - -export interface PollOptions { - done: (value: T) => boolean; - intervalMs?: number; - maxAttempts?: number; -} - -export function resolveFunctionsApiConfig(args: { +export interface FunctionsApiOverrides { apiKey?: string; baseUrl?: string; -}): FunctionsApiConfig { - return { - apiKey: resolveApiKey(args), - baseUrl: - args.baseUrl || - process.env.BROWSERBASE_BASE_URL || - process.env.BROWSERBASE_API_BASE_URL || - defaultFunctionsBaseUrl, - }; } -export function resolveFunctionsProjectConfig(args: { - apiKey?: string; - baseUrl?: string; - projectId?: string; -}): FunctionsProjectConfig { - const apiConfig = resolveFunctionsApiConfig(args); - const projectId = args.projectId ?? process.env.BROWSERBASE_PROJECT_ID; - return projectId ? { ...apiConfig, projectId } : apiConfig; -} - -export async function functionsRequest( - config: FunctionsApiConfig, - path: string, - init: RequestInit = {}, -): Promise { - let response: Response; - try { - response = await fetch(new URL(path, config.baseUrl), { - ...init, - headers: { - "x-bb-api-key": config.apiKey, - ...(init.headers ?? {}), - }, - }); - } catch (error) { - if (error instanceof CommandFailure) { - throw error; - } - fail(error instanceof Error ? error.message : String(error), 1, { - resultCode: "request_no_response", - requestHadHttpResponse: false, - }); - } - - setRunTelemetryCompletion({ - httpStatus: response.status, - requestHadHttpResponse: true, - }); - - if (!response.ok) { - fail(await readBrowserbaseError(response), 1, { - resultCode: classifyCommandHttpFailure("functions", response.status), - httpStatus: response.status, - requestHadHttpResponse: true, - }); - } - - return response; -} - -export async function functionsGet(config: FunctionsApiConfig, path: string): Promise { - const response = await functionsRequest(config, path); - return (await response.json()) as T; -} - -export async function functionsPost( - config: FunctionsApiConfig, - path: string, - body: unknown, -): Promise { - const response = await functionsRequest(config, path, { - method: "POST", - headers: { - "content-type": "application/json", +export function resolveFunctionsCoreOptions( + args: FunctionsApiOverrides, +): ResolveFunctionsApiConfigOptions { + const options: ResolveFunctionsApiConfigOptions = { + apiKey: resolveApiKey(args), + onResponse(response) { + setRunTelemetryCompletion({ + httpStatus: response.status, + requestHadHttpResponse: true, + }); }, - body: JSON.stringify(body), - }); - return (await response.json()) as T; -} - -export async function pollUntil(loader: () => Promise, options: PollOptions): Promise { - const intervalMs = options.intervalMs ?? 1_000; - const maxAttempts = options.maxAttempts ?? 120; - - for (let attempt = 0; attempt < maxAttempts; attempt += 1) { - const result = await loader(); - if (options.done(result)) { - return result; - } - await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs)); + }; + if (args.baseUrl) { + options.baseUrl = args.baseUrl; } - - fail("Timed out while waiting for the Browserbase Functions operation to complete.", 1, { - resultCode: "functions_timeout", - }); + return options; } -export async function resolveEntrypoint(entrypoint: string): Promise { - const absolutePath = resolve(entrypoint); - let stats; +export async function runFunctionsCore(operation: () => Promise): Promise { try { - stats = await stat(absolutePath); - } catch { - fail(`Entrypoint file not found: ${absolutePath}`); - } - - if (!stats.isFile()) { - fail(`Entrypoint must be a file: ${absolutePath}`); - } - - const extension = extname(absolutePath).toLowerCase(); - if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts"].includes(extension)) { - fail(`Unsupported entrypoint extension: ${extension}`); + return await operation(); + } catch (error) { + rethrowFunctionsCoreError(error); } - - return absolutePath; } -export function parseOptionalJsonValueArg(rawValue: unknown, label: string): unknown { - if (!rawValue) { - return {}; +export function rethrowFunctionsCoreError(error: unknown): never { + if (!(error instanceof FunctionsCoreError)) { + fail(error instanceof Error ? error.message : String(error)); } - if (typeof rawValue !== "string") { - fail(`${label} must be provided as a JSON string.`); + const metadata: { + httpStatus?: number; + requestHadHttpResponse?: boolean; + resultCode: string; + } = { + resultCode: resultCodeForCoreError(error), + }; + if (error.httpStatus !== undefined) { + metadata.httpStatus = error.httpStatus; + metadata.requestHadHttpResponse = true; + } else if (error.code === "request_failed") { + metadata.requestHadHttpResponse = false; } + fail(error.message, 1, metadata); +} - try { - return JSON.parse(rawValue); - } catch (error) { - fail(`Invalid JSON for ${label}: ${(error as Error).message}`); +function resultCodeForCoreError(error: FunctionsCoreError): string { + if (error.code === "http_error" && error.httpStatus !== undefined) { + return classifyCommandHttpFailure("functions", error.httpStatus) ?? "functions_http_error"; } + const codes: Partial> = { + build_failed: "functions_build_failed", + build_missing_id: "functions_build_missing_id", + invocation_failed: "functions_invocation_failed", + request_failed: "request_no_response", + timeout: "functions_timeout", + }; + return codes[error.code] ?? `functions_${error.code}`; } diff --git a/packages/cli/tests/cli-functions-contract.test.ts b/packages/cli/tests/cli-functions-contract.test.ts index d963dad88..b98afd107 100644 --- a/packages/cli/tests/cli-functions-contract.test.ts +++ b/packages/cli/tests/cli-functions-contract.test.ts @@ -43,6 +43,15 @@ afterEach(async () => { }); describe("functions API contracts", () => { + it("imports the SDK core without executing the bundled bb CLI", async () => { + const core = await import("@browserbasehq/sdk-functions/core"); + + expect(core.createFunctionProject).toBeTypeOf("function"); + expect(core.startDevServer).toBeTypeOf("function"); + expect(core.publishFunction).toBeTypeOf("function"); + expect(core.invokeFunction).toBeTypeOf("function"); + }); + itPosix("publishes a Functions archive and polls build status", async () => { const cwd = await createFunctionFixture("functions-publish-"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6074721c9..0dc68ee8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ catalogs: '@browserbasehq/sdk': specifier: ^2.16.0 version: 2.16.0 + '@browserbasehq/sdk-functions': + specifier: github:browserbase/sdk-functions-node#44304977d902a93d6ebc6b3c4fd6379fc6a89f78 + version: 1.0.2 '@changesets/changelog-github': specifier: 0.7.0 version: 0.7.0 @@ -218,15 +221,15 @@ importers: '@browserbasehq/sdk': specifier: ^2.14.0 version: 2.16.0 + '@browserbasehq/sdk-functions': + specifier: 'catalog:' + version: https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78 '@oclif/core': specifier: ^4.11.0 version: 4.13.0 '@vercel/detect-agent': specifier: ^1.2.3 version: 1.2.3 - archiver: - specifier: ^7.0.1 - version: 7.0.1 deepmerge: specifier: ^4.3.1 version: 4.3.1 @@ -239,9 +242,6 @@ importers: http-status-codes: specifier: ^2.3.0 version: 2.3.0 - ignore: - specifier: ^7.0.5 - version: 7.0.5 node-html-markdown: specifier: ^1.3.0 version: 1.3.0 @@ -251,9 +251,6 @@ importers: stagehand-v3: specifier: npm:@browserbasehq/stagehand@3.7.1 version: '@browserbasehq/stagehand@3.7.1(playwright-core@1.56.1)(zod@4.4.3)' - tsx: - specifier: ^4.20.6 - version: 4.23.1 ws: specifier: ^8.18.3 version: 8.21.0(bufferutil@4.1.0) @@ -264,9 +261,6 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.8.0(jiti@1.21.7)) - '@types/archiver': - specifier: ^6.0.3 - version: 6.0.4 '@types/node': specifier: ^20.11.30 version: 20.19.43 @@ -995,6 +989,11 @@ packages: '@types/react': optional: true + '@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78': + resolution: {gitHosted: true, integrity: sha512-4k72bB/TQlbflcZy6Ro6h07w9oPsmW0rHBWwGkkE/4WVG4QZjTNL8ZLQ8b5BdK9abHU3TdeJcktDj7ezDQ/3lw==, tarball: https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78} + version: 1.0.2 + hasBin: true + '@browserbasehq/sdk@2.16.0': resolution: {integrity: sha512-mPAuLRU9jWR7o0KJi9+gQnOBDUSIkoKbbFv4HjrA+80qWVcFacrNPlZmf4mguQnfZ0oP2t5c3ws6yuFyAX9vpA==} @@ -3672,6 +3671,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -8193,6 +8196,22 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78': + dependencies: + '@browserbasehq/sdk': 2.16.0 + archiver: 7.0.1 + chalk: 5.6.2 + commander: 14.0.3 + dotenv: 17.4.2 + ignore: 7.0.5 + tsx: 4.23.1 + zod: 4.4.3 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - encoding + - react-native-b4a + '@browserbasehq/sdk@2.16.0': dependencies: '@types/node': 18.19.130 @@ -11276,6 +11295,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 199f444d1..b7bfba53e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,7 @@ catalog: "@ast-grep/lang-go": 0.0.6 "@ast-grep/lang-python": 0.0.6 "@ast-grep/napi": 0.44.1 + "@browserbasehq/sdk-functions": github:browserbase/sdk-functions-node#44304977d902a93d6ebc6b3c4fd6379fc6a89f78 "@changesets/changelog-github": 0.7.0 "@changesets/cli": 2.31.1 "@mdx-js/mdx": 3.1.1 @@ -51,6 +52,9 @@ catalog: overrides: vite: "catalog:" allowBuilds: + # TODO(functions-core-release): remove this git-dependency build allowance once + # @browserbasehq/sdk-functions/core is available from npm. + "@browserbasehq/sdk-functions": true "@ast-grep/lang-go": true "@ast-grep/lang-python": true "@google/genai": false From 48ff7b3adb7218bae8719e55a9531d028d0bdae2 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 05:35:14 +0000 Subject: [PATCH 2/3] fix(ci): allow the pinned Functions core build --- pnpm-workspace.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b7bfba53e..38f9e6297 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -54,7 +54,7 @@ overrides: allowBuilds: # TODO(functions-core-release): remove this git-dependency build allowance once # @browserbasehq/sdk-functions/core is available from npm. - "@browserbasehq/sdk-functions": true + "@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78": true "@ast-grep/lang-go": true "@ast-grep/lang-python": true "@google/genai": false From 8b191dffed2f5e1e3d7db417e13318904c861d70 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 05:48:28 +0000 Subject: [PATCH 3/3] chore(cli): update the Functions core pin --- pnpm-lock.yaml | 10 +++++----- pnpm-workspace.yaml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dc68ee8a..883b72314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ catalogs: specifier: ^2.16.0 version: 2.16.0 '@browserbasehq/sdk-functions': - specifier: github:browserbase/sdk-functions-node#44304977d902a93d6ebc6b3c4fd6379fc6a89f78 + specifier: github:browserbase/sdk-functions-node#4d7db8bdc5917fed0af17e78fe4cc8ed33e64468 version: 1.0.2 '@changesets/changelog-github': specifier: 0.7.0 @@ -223,7 +223,7 @@ importers: version: 2.16.0 '@browserbasehq/sdk-functions': specifier: 'catalog:' - version: https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78 + version: https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/4d7db8bdc5917fed0af17e78fe4cc8ed33e64468 '@oclif/core': specifier: ^4.11.0 version: 4.13.0 @@ -989,8 +989,8 @@ packages: '@types/react': optional: true - '@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78': - resolution: {gitHosted: true, integrity: sha512-4k72bB/TQlbflcZy6Ro6h07w9oPsmW0rHBWwGkkE/4WVG4QZjTNL8ZLQ8b5BdK9abHU3TdeJcktDj7ezDQ/3lw==, tarball: https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78} + '@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/4d7db8bdc5917fed0af17e78fe4cc8ed33e64468': + resolution: {gitHosted: true, integrity: sha512-306uwW7KliLo25OF5Ybn+7fWsagT2wQrXBxYCgVqq1q6Re3rjPNkG5Fq3kKQVXfr4zD6Xm741E3+20fIh4d6xg==, tarball: https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/4d7db8bdc5917fed0af17e78fe4cc8ed33e64468} version: 1.0.2 hasBin: true @@ -8196,7 +8196,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78': + '@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/4d7db8bdc5917fed0af17e78fe4cc8ed33e64468': dependencies: '@browserbasehq/sdk': 2.16.0 archiver: 7.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 38f9e6297..057d5979f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,7 +8,7 @@ catalog: "@ast-grep/lang-go": 0.0.6 "@ast-grep/lang-python": 0.0.6 "@ast-grep/napi": 0.44.1 - "@browserbasehq/sdk-functions": github:browserbase/sdk-functions-node#44304977d902a93d6ebc6b3c4fd6379fc6a89f78 + "@browserbasehq/sdk-functions": github:browserbase/sdk-functions-node#4d7db8bdc5917fed0af17e78fe4cc8ed33e64468 "@changesets/changelog-github": 0.7.0 "@changesets/cli": 2.31.1 "@mdx-js/mdx": 3.1.1 @@ -54,7 +54,7 @@ overrides: allowBuilds: # TODO(functions-core-release): remove this git-dependency build allowance once # @browserbasehq/sdk-functions/core is available from npm. - "@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/44304977d902a93d6ebc6b3c4fd6379fc6a89f78": true + "@browserbasehq/sdk-functions@https://codeload.github.com/browserbase/sdk-functions-node/tar.gz/4d7db8bdc5917fed0af17e78fe4cc8ed33e64468": true "@ast-grep/lang-go": true "@ast-grep/lang-python": true "@google/genai": false