diff --git a/.changeset/bright-cobras-sing.md b/.changeset/bright-cobras-sing.md new file mode 100644 index 0000000..eadc7e4 --- /dev/null +++ b/.changeset/bright-cobras-sing.md @@ -0,0 +1,5 @@ +--- +"@vercel/sandbox": minor +--- + +Expose sandboxId-first top-level helpers and move the standalone sandbox operations into a dedicated module. diff --git a/packages/vercel-sandbox/src/index.ts b/packages/vercel-sandbox/src/index.ts index 5540c76..f1228ed 100644 --- a/packages/vercel-sandbox/src/index.ts +++ b/packages/vercel-sandbox/src/index.ts @@ -4,6 +4,22 @@ export { type NetworkPolicyRule, type NetworkTransformer, } from "./sandbox"; +export { + createSnapshot, + downloadFile, + extendSandboxTimeout, + getCommand, + getSandboxDomain, + mkDir, + mkdir, + readFile, + readFileToBuffer, + runCommand, + stopSandbox, + updateSandboxNetworkPolicy, + writeFile, + writeFiles, +} from "./sandbox-operations"; export { Snapshot } from "./snapshot"; export { Command, CommandFinished } from "./command"; export { StreamError } from "./api-client/api-error"; diff --git a/packages/vercel-sandbox/src/sandbox-operations.ts b/packages/vercel-sandbox/src/sandbox-operations.ts new file mode 100644 index 0000000..7b3376e --- /dev/null +++ b/packages/vercel-sandbox/src/sandbox-operations.ts @@ -0,0 +1,507 @@ +import { createWriteStream } from "fs"; +import { mkdir as mkdirLocal } from "fs/promises"; +import { dirname, resolve } from "path"; +import { type Writable } from "stream"; +import { pipeline } from "stream/promises"; +import { APIClient } from "./api-client"; +import { type WithFetchOptions } from "./api-client/api-client"; +import { Command, CommandFinished } from "./command"; +import { type NetworkPolicy } from "./network-policy"; +import { Snapshot } from "./snapshot"; +import { consumeReadable } from "./utils/consume-readable"; +import { convertSandbox, type ConvertedSandbox } from "./utils/convert-sandbox"; +import { type Credentials, getCredentials } from "./utils/get-credentials"; + +export interface RunCommandParams { + /** + * The command to execute. + */ + cmd: string; + /** + * Arguments to pass to the command. + */ + args?: string[]; + /** + * Working directory to execute the command in. + */ + cwd?: string; + /** + * Environment variables to set for this command. + */ + env?: Record; + /** + * If true, execute this command with root privileges. Defaults to false. + */ + sudo?: boolean; + /** + * If true, the command will return without waiting for `exitCode`. + */ + detached?: boolean; + /** + * A `Writable` stream where `stdout` from the command will be piped. + */ + stdout?: Writable; + /** + * A `Writable` stream where `stderr` from the command will be piped. + */ + stderr?: Writable; + /** + * An AbortSignal to cancel the command execution. + */ + signal?: AbortSignal; +} + +export interface SandboxAccessOptions extends WithFetchOptions { + client?: APIClient; + token?: string; + teamId?: string; + projectId?: string; +} + +interface SandboxClientOptions { + client?: APIClient; +} + +interface SandboxSignalOptions extends SandboxClientOptions { + signal?: AbortSignal; +} + +interface DownloadFileOptions extends SandboxSignalOptions { + mkdirRecursive?: boolean; +} + +interface StopSandboxOptions extends SandboxSignalOptions { + blocking?: boolean; +} + +interface CreateSnapshotOptions extends SandboxSignalOptions { + expiration?: number; +} + +function getSandboxAccessCredentials( + params?: SandboxAccessOptions, +): Pick | null { + if (params?.client) { + return null; + } + + if ( + typeof params?.token === "string" && + typeof params?.teamId === "string" + ) { + return { + token: params.token, + teamId: params.teamId, + }; + } + + if (params?.token !== undefined || params?.teamId !== undefined) { + const missing = [ + typeof params?.token === "string" ? null : "token", + typeof params?.teamId === "string" ? null : "teamId", + ].filter((value) => value !== null); + + throw new Error( + `Missing credentials parameters to access the Vercel API: ${missing.join(", ")}`, + ); + } + + return null; +} + +export async function createSandboxClient( + params?: SandboxAccessOptions, +): Promise { + if (params?.client) { + return params.client; + } + + const credentials = + getSandboxAccessCredentials(params) ?? (await getCredentials(params)); + + return new APIClient({ + teamId: credentials.teamId, + token: credentials.token, + fetch: params?.fetch, + }); +} + +async function getSandboxDetails( + sandboxId: string, + opts?: SandboxSignalOptions, +) { + const client = await createSandboxClient(opts); + const response = await client.getSandbox({ + sandboxId, + signal: opts?.signal, + }); + + return { + client, + sandbox: response.json.sandbox, + routes: response.json.routes, + }; +} + +async function runSandboxCommand( + client: APIClient, + sandboxId: string, + params: RunCommandParams, +) { + const wait = params.detached ? false : true; + const streamLogs = (command: Command) => { + if (params.stdout || params.stderr) { + (async () => { + try { + for await (const log of command.logs({ signal: params.signal })) { + if (log.stream === "stdout") { + params.stdout?.write(log.data); + } else if (log.stream === "stderr") { + params.stderr?.write(log.data); + } + } + } catch (err) { + if (params.signal?.aborted) { + return; + } + throw err; + } + })(); + } + }; + + if (wait) { + const commandStream = await client.runCommand({ + sandboxId, + command: params.cmd, + args: params.args ?? [], + cwd: params.cwd, + env: params.env ?? {}, + sudo: params.sudo ?? false, + wait: true, + signal: params.signal, + }); + + const command = new Command({ + client, + sandboxId, + cmd: commandStream.command, + }); + + streamLogs(command); + + const finished = await commandStream.finished; + return new CommandFinished({ + client, + sandboxId, + cmd: finished, + exitCode: finished.exitCode ?? 0, + }); + } + + const commandResponse = await client.runCommand({ + sandboxId, + command: params.cmd, + args: params.args ?? [], + cwd: params.cwd, + env: params.env ?? {}, + sudo: params.sudo ?? false, + signal: params.signal, + }); + + const command = new Command({ + client, + sandboxId, + cmd: commandResponse.json.command, + }); + + streamLogs(command); + + return command; +} + +/** + * Retrieve a previously run command from an existing sandbox by ID. + */ +export async function getCommand( + sandboxId: string, + cmdId: string, + opts?: SandboxSignalOptions, +): Promise { + const client = await createSandboxClient(opts); + const response = await client.getCommand({ + sandboxId, + cmdId, + signal: opts?.signal, + }); + + return new Command({ + client, + sandboxId, + cmd: response.json.command, + }); +} + +/** + * Start executing a command in an existing sandbox by ID. + */ +export async function runCommand( + sandboxId: string, + command: string, + args?: string[], + opts?: SandboxSignalOptions, +): Promise; +export async function runCommand( + sandboxId: string, + params: RunCommandParams & { detached: true }, + opts?: SandboxClientOptions, +): Promise; +export async function runCommand( + sandboxId: string, + params: RunCommandParams, + opts?: SandboxClientOptions, +): Promise; +export async function runCommand( + sandboxId: string, + commandOrParams: string | RunCommandParams, + argsOrOptions?: string[] | SandboxSignalOptions, + maybeOptions?: SandboxSignalOptions, +): Promise { + if (typeof commandOrParams === "string") { + const args = Array.isArray(argsOrOptions) ? argsOrOptions : undefined; + const options = + Array.isArray(argsOrOptions) || argsOrOptions === undefined + ? maybeOptions + : argsOrOptions; + const client = await createSandboxClient(options); + + return runSandboxCommand(client, sandboxId, { + cmd: commandOrParams, + args, + signal: options?.signal, + }); + } + + const options = !Array.isArray(argsOrOptions) ? argsOrOptions : maybeOptions; + const client = await createSandboxClient(options); + return runSandboxCommand(client, sandboxId, commandOrParams); +} + +/** + * Create a directory in an existing sandbox by ID. + */ +export async function mkDir( + sandboxId: string, + path: string, + opts?: SandboxSignalOptions, +): Promise { + const client = await createSandboxClient(opts); + await client.mkDir({ + sandboxId, + path, + signal: opts?.signal, + }); +} + +/** + * Alias for {@link mkDir}. + */ +export async function mkdir( + sandboxId: string, + path: string, + opts?: SandboxSignalOptions, +): Promise { + await mkDir(sandboxId, path, opts); +} + +/** + * Read a file from an existing sandbox by ID as a stream. + */ +export async function readFile( + sandboxId: string, + file: { path: string; cwd?: string }, + opts?: SandboxSignalOptions, +): Promise { + const client = await createSandboxClient(opts); + return client.readFile({ + sandboxId, + path: file.path, + cwd: file.cwd, + signal: opts?.signal, + }); +} + +/** + * Read a file from an existing sandbox by ID as a buffer. + */ +export async function readFileToBuffer( + sandboxId: string, + file: { path: string; cwd?: string }, + opts?: SandboxSignalOptions, +): Promise { + const stream = await readFile(sandboxId, file, opts); + + if (stream === null) { + return null; + } + + return consumeReadable(stream); +} + +/** + * Download a file from an existing sandbox by ID to the local filesystem. + */ +export async function downloadFile( + sandboxId: string, + src: { path: string; cwd?: string }, + dst: { path: string; cwd?: string }, + opts?: DownloadFileOptions, +): Promise { + if (!src?.path) { + throw new Error("downloadFile: source path is required"); + } + + if (!dst?.path) { + throw new Error("downloadFile: destination path is required"); + } + + const stream = await readFile(sandboxId, src, opts); + + if (stream === null) { + return null; + } + + try { + const dstPath = resolve(dst.cwd ?? "", dst.path); + if (opts?.mkdirRecursive) { + await mkdirLocal(dirname(dstPath), { recursive: true }); + } + await pipeline(stream, createWriteStream(dstPath), { + signal: opts?.signal, + }); + return dstPath; + } finally { + if ("destroy" in stream && typeof stream.destroy === "function") { + stream.destroy(); + } + } +} + +/** + * Write multiple files to an existing sandbox by ID. + */ +export async function writeFiles( + sandboxId: string, + files: { path: string; content: Buffer }[], + opts?: SandboxSignalOptions, +): Promise { + const { client, sandbox } = await getSandboxDetails(sandboxId, opts); + + await client.writeFiles({ + sandboxId, + cwd: sandbox.cwd, + extractDir: "/", + files, + signal: opts?.signal, + }); +} + +/** + * Write a single file to an existing sandbox by ID. + */ +export async function writeFile( + sandboxId: string, + file: { path: string; content: Buffer }, + opts?: SandboxSignalOptions, +): Promise { + await writeFiles(sandboxId, [file], opts); +} + +/** + * Resolve the public domain for a port exposed by an existing sandbox. + */ +export async function getSandboxDomain( + sandboxId: string, + port: number, + opts?: SandboxSignalOptions, +): Promise { + const { routes } = await getSandboxDetails(sandboxId, opts); + const route = routes.find((candidate) => candidate.port === port); + + if (!route) { + throw new Error(`No route for port ${port}`); + } + + return `https://${route.subdomain}.vercel.run`; +} + +/** + * Stop an existing sandbox by ID. + */ +export async function stopSandbox( + sandboxId: string, + opts?: StopSandboxOptions, +): Promise { + const client = await createSandboxClient(opts); + const response = await client.stopSandbox({ + sandboxId, + signal: opts?.signal, + blocking: opts?.blocking, + }); + + return convertSandbox(response.json.sandbox); +} + +/** + * Update the network policy for an existing sandbox by ID. + */ +export async function updateSandboxNetworkPolicy( + sandboxId: string, + networkPolicy: NetworkPolicy, + opts?: SandboxSignalOptions, +): Promise { + const client = await createSandboxClient(opts); + const response = await client.updateNetworkPolicy({ + sandboxId, + networkPolicy, + signal: opts?.signal, + }); + + return convertSandbox(response.json.sandbox).networkPolicy!; +} + +/** + * Extend the timeout of an existing sandbox by ID. + */ +export async function extendSandboxTimeout( + sandboxId: string, + duration: number, + opts?: SandboxSignalOptions, +): Promise { + const client = await createSandboxClient(opts); + await client.extendTimeout({ + sandboxId, + duration, + signal: opts?.signal, + }); +} + +/** + * Create a snapshot from an existing sandbox by ID. + */ +export async function createSnapshot( + sandboxId: string, + opts?: CreateSnapshotOptions, +): Promise { + const client = await createSandboxClient(opts); + const response = await client.createSnapshot({ + sandboxId, + expiration: opts?.expiration, + signal: opts?.signal, + }); + + return new Snapshot({ + client, + snapshot: response.json.snapshot, + }); +} diff --git a/packages/vercel-sandbox/src/sandbox.test.ts b/packages/vercel-sandbox/src/sandbox.test.ts index 6c796c9..f94c2d4 100644 --- a/packages/vercel-sandbox/src/sandbox.test.ts +++ b/packages/vercel-sandbox/src/sandbox.test.ts @@ -1,10 +1,18 @@ -import { it, beforeEach, afterEach, expect, describe } from "vitest"; +import { it, beforeEach, afterEach, expect, describe, vi } from "vitest"; import { consumeReadable } from "./utils/consume-readable"; import { Sandbox } from "./sandbox"; +import * as sandboxOperations from "./sandbox-operations"; +import { + mkdir, + readFile as readSandboxFile, + runCommand as runSandboxCommand, + writeFile as writeSandboxFile, +} from "./sandbox-operations"; import { APIError } from "./api-client/api-error"; import { mkdtemp, readFile, rm } from "fs/promises"; import { tmpdir } from "os"; import { join, resolve } from "path"; +import { Readable } from "stream"; import ms from "ms"; describe("downloadFile validation", () => { @@ -53,6 +61,163 @@ describe("downloadFile validation", () => { }); }); +describe("top-level sandbox helpers", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("runs commands by sandbox ID", async () => { + const command = { + id: "cmd_123", + name: "echo", + args: ["hello"], + cwd: "/vercel/sandbox", + sandboxId: "sandbox_123", + startedAt: Date.now(), + exitCode: 0, + }; + const client = { + runCommand: vi.fn().mockResolvedValue({ + command, + finished: Promise.resolve(command), + }), + } as any; + + const result = await runSandboxCommand( + "sandbox_123", + "echo", + ["hello"], + { client }, + ); + + expect(result.exitCode).toBe(0); + expect(client.runCommand).toHaveBeenCalledWith({ + sandboxId: "sandbox_123", + command: "echo", + args: ["hello"], + cwd: undefined, + env: {}, + sudo: false, + wait: true, + signal: undefined, + }); + }); + + it("creates directories by sandbox ID", async () => { + const client = { + mkDir: vi.fn().mockResolvedValue(undefined), + } as any; + + await mkdir("sandbox_123", "tmp/nested", { client }); + + expect(client.mkDir).toHaveBeenCalledWith({ + sandboxId: "sandbox_123", + path: "tmp/nested", + signal: undefined, + }); + }); + + it("reads files by sandbox ID", async () => { + const stream = Readable.from([Buffer.from("Hello")]); + const client = { + readFile: vi.fn().mockResolvedValue(stream), + } as any; + + const result = await readSandboxFile( + "sandbox_123", + { path: "hello.txt", cwd: "/tmp" }, + { client }, + ); + + expect(result).toBe(stream); + expect(client.readFile).toHaveBeenCalledWith({ + sandboxId: "sandbox_123", + path: "hello.txt", + cwd: "/tmp", + signal: undefined, + }); + }); + + it("writes a single file by sandbox ID", async () => { + const content = Buffer.from("Hello"); + const client = { + getSandbox: vi.fn().mockResolvedValue({ + json: { + sandbox: { cwd: "/vercel/sandbox" }, + routes: [], + }, + }), + writeFiles: vi.fn().mockResolvedValue(undefined), + } as any; + + await writeSandboxFile( + "sandbox_123", + { path: "hello.txt", content }, + { client }, + ); + + expect(client.getSandbox).toHaveBeenCalledWith({ + sandboxId: "sandbox_123", + signal: undefined, + }); + expect(client.writeFiles).toHaveBeenCalledWith({ + sandboxId: "sandbox_123", + cwd: "/vercel/sandbox", + extractDir: "/", + files: [{ path: "hello.txt", content }], + signal: undefined, + }); + }); +}); + +describe("Sandbox delegates to standalone helpers", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("delegates runCommand with the instance client", async () => { + const client = {} as any; + const sandbox = new Sandbox({ + client, + routes: [], + sandbox: { id: "sandbox_123" } as any, + }); + const command = { exitCode: 0 } as any; + const runCommandSpy = vi + .spyOn(sandboxOperations, "runCommand") + .mockResolvedValue(command); + + const result = await sandbox.runCommand({ cmd: "echo" }); + + expect(result).toBe(command); + expect(runCommandSpy).toHaveBeenCalledWith( + "sandbox_123", + { cmd: "echo" }, + { client }, + ); + }); + + it("delegates writeFiles with the instance client", async () => { + const client = {} as any; + const files = [{ path: "hello.txt", content: Buffer.from("Hello") }]; + const sandbox = new Sandbox({ + client, + routes: [], + sandbox: { id: "sandbox_123" } as any, + }); + const writeFilesSpy = vi + .spyOn(sandboxOperations, "writeFiles") + .mockResolvedValue(undefined); + + await sandbox.writeFiles(files); + + expect(writeFilesSpy).toHaveBeenCalledWith("sandbox_123", files, { + signal: undefined, + client, + }); + }); +}); + describe.skipIf(process.env.RUN_INTEGRATION_TESTS !== "1")("Sandbox", () => { const PORTS = [3000, 4000]; let sandbox: Sandbox; diff --git a/packages/vercel-sandbox/src/sandbox.ts b/packages/vercel-sandbox/src/sandbox.ts index c93c96a..72b9052 100644 --- a/packages/vercel-sandbox/src/sandbox.ts +++ b/packages/vercel-sandbox/src/sandbox.ts @@ -1,23 +1,33 @@ import type { SandboxMetaData, SandboxRouteData } from "./api-client"; -import { type Writable } from "stream"; -import { pipeline } from "stream/promises"; -import { createWriteStream } from "fs"; -import { mkdir } from "fs/promises"; -import { dirname, resolve } from "path"; import { APIClient } from "./api-client"; -import { Command, CommandFinished } from "./command"; -import { type Credentials, getCredentials } from "./utils/get-credentials"; -import { getPrivateParams, WithPrivate } from "./utils/types"; import { WithFetchOptions } from "./api-client/api-client"; +import { Command, CommandFinished } from "./command"; import { RUNTIMES } from "./constants"; +import { + createSandboxClient, + createSnapshot as createSnapshotBySandboxId, + downloadFile as downloadFileBySandboxId, + extendSandboxTimeout as extendSandboxTimeoutBySandboxId, + getCommand as getCommandBySandboxId, + mkDir as mkDirBySandboxId, + readFile as readFileBySandboxId, + readFileToBuffer as readFileToBufferBySandboxId, + runCommand as runCommandBySandboxId, + type RunCommandParams, + type SandboxAccessOptions, + stopSandbox as stopSandboxBySandboxId, + updateSandboxNetworkPolicy as updateSandboxNetworkPolicyBySandboxId, + writeFiles as writeFilesBySandboxId, +} from "./sandbox-operations"; import { Snapshot } from "./snapshot"; -import { consumeReadable } from "./utils/consume-readable"; +import { type Credentials, getCredentials } from "./utils/get-credentials"; +import { convertSandbox, type ConvertedSandbox } from "./utils/convert-sandbox"; +import { getPrivateParams, WithPrivate } from "./utils/types"; import { type NetworkPolicy, type NetworkPolicyRule, type NetworkTransformer, } from "./network-policy"; -import { convertSandbox, type ConvertedSandbox } from "./utils/convert-sandbox"; export type { NetworkPolicy, NetworkPolicyRule, NetworkTransformer }; @@ -115,46 +125,6 @@ interface GetSandboxParams { signal?: AbortSignal; } -/** @inline */ -interface RunCommandParams { - /** - * The command to execute - */ - cmd: string; - /** - * Arguments to pass to the command - */ - args?: string[]; - /** - * Working directory to execute the command in - */ - cwd?: string; - /** - * Environment variables to set for this command - */ - env?: Record; - /** - * If true, execute this command with root privileges. Defaults to false. - */ - sudo?: boolean; - /** - * If true, the command will return without waiting for `exitCode` - */ - detached?: boolean; - /** - * A `Writable` stream where `stdout` from the command will be piped - */ - stdout?: Writable; - /** - * A `Writable` stream where `stderr` from the command will be piped - */ - stderr?: Writable; - /** - * An AbortSignal to cancel the command execution - */ - signal?: AbortSignal; -} - /** * A Sandbox is an isolated Linux MicroVM to run commands in. * @@ -310,15 +280,9 @@ export class Sandbox { * @returns A promise resolving to the {@link Sandbox}. */ static async get( - params: WithPrivate & - WithFetchOptions, + params: WithPrivate, ): Promise { - const credentials = await getCredentials(params); - const client = new APIClient({ - teamId: credentials.teamId, - token: credentials.token, - fetch: params.fetch, - }); + const client = await createSandboxClient(params); const privateParams = getPrivateParams(params); const sandbox = await client.getSandbox({ @@ -360,16 +324,9 @@ export class Sandbox { cmdId: string, opts?: { signal?: AbortSignal }, ): Promise { - const command = await this.client.getCommand({ - sandboxId: this.sandbox.id, - cmdId, + return getCommandBySandboxId(this.sandbox.id, cmdId, { signal: opts?.signal, - }); - - return new Command({ client: this.client, - sandboxId: this.sandbox.id, - cmd: command.json.command, }); } @@ -412,88 +369,13 @@ export class Sandbox { opts?: { signal?: AbortSignal }, ): Promise { return typeof commandOrParams === "string" - ? this._runCommand({ cmd: commandOrParams, args, signal: opts?.signal }) - : this._runCommand(commandOrParams); - } - - /** - * Internal helper to start a command in the sandbox. - * - * @param params - Command execution parameters. - * @returns A {@link Command} or {@link CommandFinished}, depending on `detached`. - * @internal - */ - async _runCommand(params: RunCommandParams) { - const wait = params.detached ? false : true; - const getLogs = (command: Command) => { - if (params.stdout || params.stderr) { - (async () => { - try { - for await (const log of command.logs({ signal: params.signal })) { - if (log.stream === "stdout") { - params.stdout?.write(log.data); - } else if (log.stream === "stderr") { - params.stderr?.write(log.data); - } - } - } catch (err) { - if (params.signal?.aborted) { - return; - } - throw err; - } - })(); - } - } - - if (wait) { - const commandStream = await this.client.runCommand({ - sandboxId: this.sandbox.id, - command: params.cmd, - args: params.args ?? [], - cwd: params.cwd, - env: params.env ?? {}, - sudo: params.sudo ?? false, - wait: true, - signal: params.signal, - }); - - const command = new Command({ - client: this.client, - sandboxId: this.sandbox.id, - cmd: commandStream.command, - }); - - getLogs(command); - - const finished = await commandStream.finished; - return new CommandFinished({ - client: this.client, - sandboxId: this.sandbox.id, - cmd: finished, - exitCode: finished.exitCode ?? 0, - }); - } - - const commandResponse = await this.client.runCommand({ - sandboxId: this.sandbox.id, - command: params.cmd, - args: params.args ?? [], - cwd: params.cwd, - env: params.env ?? {}, - sudo: params.sudo ?? false, - signal: params.signal, - }); - - const command = new Command({ - client: this.client, - sandboxId: this.sandbox.id, - cmd: commandResponse.json.command, - }); - - getLogs(command); - - return command; + ? runCommandBySandboxId(this.sandbox.id, commandOrParams, args, { + signal: opts?.signal, + client: this.client, + }) + : runCommandBySandboxId(this.sandbox.id, commandOrParams, { + client: this.client, + }); } /** @@ -504,10 +386,9 @@ export class Sandbox { * @param opts.signal - An AbortSignal to cancel the operation. */ async mkDir(path: string, opts?: { signal?: AbortSignal }): Promise { - await this.client.mkDir({ - sandboxId: this.sandbox.id, - path: path, + await mkDirBySandboxId(this.sandbox.id, path, { signal: opts?.signal, + client: this.client, }); } @@ -523,11 +404,9 @@ export class Sandbox { file: { path: string; cwd?: string }, opts?: { signal?: AbortSignal }, ): Promise { - return this.client.readFile({ - sandboxId: this.sandbox.id, - path: file.path, - cwd: file.cwd, + return readFileBySandboxId(this.sandbox.id, file, { signal: opts?.signal, + client: this.client, }); } @@ -543,18 +422,10 @@ export class Sandbox { file: { path: string; cwd?: string }, opts?: { signal?: AbortSignal }, ): Promise { - const stream = await this.client.readFile({ - sandboxId: this.sandbox.id, - path: file.path, - cwd: file.cwd, + return readFileToBufferBySandboxId(this.sandbox.id, file, { signal: opts?.signal, + client: this.client, }); - - if (stream === null) { - return null; - } - - return consumeReadable(stream); } /** @@ -572,37 +443,11 @@ export class Sandbox { dst: { path: string; cwd?: string }, opts?: { mkdirRecursive?: boolean; signal?: AbortSignal }, ): Promise { - if (!src?.path) { - throw new Error("downloadFile: source path is required"); - } - - if (!dst?.path) { - throw new Error("downloadFile: destination path is required"); - } - - const stream = await this.client.readFile({ - sandboxId: this.sandbox.id, - path: src.path, - cwd: src.cwd, + return downloadFileBySandboxId(this.sandbox.id, src, dst, { + mkdirRecursive: opts?.mkdirRecursive, signal: opts?.signal, + client: this.client, }); - - if (stream === null) { - return null; - } - - try { - const dstPath = resolve(dst.cwd ?? "", dst.path); - if (opts?.mkdirRecursive) { - await mkdir(dirname(dstPath), { recursive: true }); - } - await pipeline(stream, createWriteStream(dstPath), { - signal: opts?.signal, - }); - return dstPath; - } finally { - stream.destroy() - } } /** @@ -618,13 +463,10 @@ export class Sandbox { async writeFiles( files: { path: string; content: Buffer }[], opts?: { signal?: AbortSignal }, - ) { - return this.client.writeFiles({ - sandboxId: this.sandbox.id, - cwd: this.sandbox.cwd, - extractDir: "/", - files: files, + ): Promise { + await writeFilesBySandboxId(this.sandbox.id, files, { signal: opts?.signal, + client: this.client, }); } @@ -653,12 +495,11 @@ export class Sandbox { * @returns The sandbox metadata at the time the stop was acknowledged, or after fully stopped if `blocking` is true. */ async stop(opts?: { signal?: AbortSignal; blocking?: boolean }): Promise { - const response = await this.client.stopSandbox({ - sandboxId: this.sandbox.id, + this.sandbox = await stopSandboxBySandboxId(this.sandbox.id, { signal: opts?.signal, blocking: opts?.blocking, + client: this.client, }); - this.sandbox = convertSandbox(response.json.sandbox); return this.sandbox; } @@ -697,15 +538,21 @@ export class Sandbox { networkPolicy: NetworkPolicy, opts?: { signal?: AbortSignal }, ): Promise { - const response = await this.client.updateNetworkPolicy({ - sandboxId: this.sandbox.id, - networkPolicy: networkPolicy, - signal: opts?.signal, - }); + const updatedNetworkPolicy = await updateSandboxNetworkPolicyBySandboxId( + this.sandbox.id, + networkPolicy, + { + signal: opts?.signal, + client: this.client, + }, + ); - // Update the internal sandbox metadata with the new timeout value - this.sandbox = convertSandbox(response.json.sandbox); - return this.sandbox.networkPolicy!; + this.sandbox = { + ...this.sandbox, + networkPolicy: updatedNetworkPolicy, + }; + + return updatedNetworkPolicy; } /** @@ -728,14 +575,15 @@ export class Sandbox { duration: number, opts?: { signal?: AbortSignal }, ): Promise { - const response = await this.client.extendTimeout({ - sandboxId: this.sandbox.id, - duration, + await extendSandboxTimeoutBySandboxId(this.sandbox.id, duration, { signal: opts?.signal, + client: this.client, }); - // Update the internal sandbox metadata with the new timeout value - this.sandbox = convertSandbox(response.json.sandbox); + this.sandbox = { + ...this.sandbox, + timeout: this.sandbox.timeout + duration, + }; } /** @@ -753,18 +601,19 @@ export class Sandbox { expiration?: number; signal?: AbortSignal; }): Promise { - const response = await this.client.createSnapshot({ - sandboxId: this.sandbox.id, + const snapshot = await createSnapshotBySandboxId(this.sandbox.id, { expiration: opts?.expiration, signal: opts?.signal, + client: this.client, }); - this.sandbox = convertSandbox(response.json.sandbox); - - return new Snapshot({ - client: this.client, - snapshot: response.json.snapshot, + const refreshedSandbox = await this.client.getSandbox({ + sandboxId: this.sandbox.id, + signal: opts?.signal, }); + this.sandbox = convertSandbox(refreshedSandbox.json.sandbox); + + return snapshot; } } @@ -782,3 +631,4 @@ class DisposableSandbox extends Sandbox implements AsyncDisposable { await this.stop(); } } +