UNPKG

@codesandbox/sdk

Version:
1,776 lines (1,763 loc) 138 kB
if (typeof window !== "undefined" && !window.process) { window.process = { env: {}, }; } // src/utils/event.ts var Emitter = class { constructor() { this.registeredListeners = /* @__PURE__ */ new Set(); } get event() { if (!this._event) { this._event = (listener) => { this.registeredListeners.add(listener); return Disposable.create(() => { this.registeredListeners.delete(listener); }); }; } return this._event; } /** Invoke all listeners registered to this event. */ fire(event) { this.registeredListeners.forEach((listener) => { listener(event); }); } dispose() { this.registeredListeners = /* @__PURE__ */ new Set(); } }; // src/utils/disposable.ts var Disposable = class _Disposable { constructor() { this.onWillDisposeEmitter = new Emitter(); this.onWillDispose = this.onWillDisposeEmitter.event; this.onDidDisposeEmitter = new Emitter(); this.onDidDispose = this.onDidDisposeEmitter.event; this.toDispose = []; this.isDisposed = false; } addDisposable(disposable) { this.toDispose.push(disposable); return disposable; } onDispose(cb) { this.toDispose.push(_Disposable.create(cb)); } dispose() { if (this.isDisposed) return; this.onWillDisposeEmitter.fire(null); this.isDisposed = true; this.toDispose.forEach((disposable) => { disposable.dispose(); }); this.onDidDisposeEmitter.fire(null); this.onWillDisposeEmitter.dispose(); this.onDidDisposeEmitter.dispose(); } static is(arg) { return typeof arg === "object" && arg !== null && "dispose" in arg && typeof arg.dispose === "function"; } static create(cb) { return { dispose: cb }; } }; var DisposableStore = class _DisposableStore { constructor() { this._toDispose = /* @__PURE__ */ new Set(); this._isDisposed = false; } static { this.DISABLE_DISPOSED_WARNING = true; } /** * Dispose of all registered disposables and mark this object as disposed. * * Any future disposables added to this object will be disposed of on `add`. */ dispose() { if (this._isDisposed) { return; } this._isDisposed = true; this.clear(); } /** * Dispose of all registered disposables but do not mark this object as disposed. */ clear() { try { for (const disposable of this._toDispose.values()) { disposable.dispose(); } } finally { this._toDispose.clear(); } } add(o) { if (!o) { return o; } if (o === this) { throw new Error("Cannot register a disposable on itself!"); } if (this._isDisposed) { if (!_DisposableStore.DISABLE_DISPOSED_WARNING) { console.warn( new Error( "Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!" ).stack ); } } else { this._toDispose.add(o); } return o; } }; // src/utils/api.ts async function retryWithDelay(callback, retries = 3, delay = 500) { let lastError; for (let attempt = 1; attempt <= retries; attempt++) { try { return await callback(); } catch (error) { lastError = error; if (attempt === retries) { throw lastError; } await new Promise((resolve) => setTimeout(resolve, delay)); } } throw lastError; } // node_modules/@opentelemetry/api/build/esm/trace/status.js var SpanStatusCode; (function(SpanStatusCode2) { SpanStatusCode2[SpanStatusCode2["UNSET"] = 0] = "UNSET"; SpanStatusCode2[SpanStatusCode2["OK"] = 1] = "OK"; SpanStatusCode2[SpanStatusCode2["ERROR"] = 2] = "ERROR"; })(SpanStatusCode || (SpanStatusCode = {})); // src/SandboxClient/filesystem.ts var FileSystem = class { constructor(sessionDisposable, agentClient, username, tracer) { this.agentClient = agentClient; this.username = username; this.disposable = new Disposable(); this.tracer = tracer; sessionDisposable.onWillDispose(() => { this.disposable.dispose(); }); } async withSpan(operationName, attributes = {}, operation) { if (!this.tracer) { return operation(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } /** * Write a file. */ async writeFile(path, content, opts = {}) { return this.withSpan( "fs.writeFile", { "fs.path": path, "fs.size": content.length, "fs.create": opts.create ?? true, "fs.overwrite": opts.overwrite ?? true }, async () => { const result = await this.agentClient.fs.writeFile( path, content, opts.create ?? true, opts.overwrite ?? true ); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } } ); } /** * Write a file as a string. */ async writeTextFile(path, content, opts = {}) { return this.withSpan( "fs.writeTextFile", { "fs.path": path, "fs.contentLength": content.length, "fs.create": opts.create ?? true, "fs.overwrite": opts.overwrite ?? true }, async () => { return this.writeFile(path, new TextEncoder().encode(content), opts); } ); } /** * Create a directory. */ async mkdir(path, recursive = false) { return this.withSpan( "fs.mkdir", { "fs.path": path, "fs.recursive": recursive }, async () => { const result = await this.agentClient.fs.mkdir(path, recursive); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } } ); } /** * Read a directory. */ async readdir(path) { return this.withSpan("fs.readdir", { "fs.path": path }, async () => { const result = await this.agentClient.fs.readdir(path); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } return result.result.entries.map((entry) => ({ ...entry, type: entry.type === 0 ? "file" : "directory" })); }); } /** * Read a file */ async readFile(path) { return this.withSpan("fs.readFile", { "fs.path": path }, async () => { const result = await this.agentClient.fs.readFile(path); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } return result.result.content; }); } /** * Read a file as a string. */ async readTextFile(path) { return this.withSpan("fs.readTextFile", { "fs.path": path }, async () => { const content = await this.readFile(path); return new TextDecoder("utf-8").decode(content); }); } /** * Get the stat of a file or directory. */ async stat(path) { return this.withSpan("fs.stat", { "fs.path": path }, async () => { const result = await this.agentClient.fs.stat(path); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } return { ...result.result, type: result.result.type === 0 ? "file" : "directory" }; }); } /** * Copy a file or directory. */ async copy(from, to, recursive = false, overwrite = false) { return this.withSpan( "fs.copy", { "fs.from": from, "fs.to": to, "fs.recursive": recursive, "fs.overwrite": overwrite }, async () => { const result = await this.agentClient.fs.copy( from, to, recursive, overwrite ); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } } ); } /** * Rename a file or directory. */ async rename(from, to, overwrite = false) { return this.withSpan( "fs.rename", { "fs.from": from, "fs.to": to, "fs.overwrite": overwrite }, async () => { const result = await this.agentClient.fs.rename(from, to, overwrite); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } } ); } /** * Remove a file or directory. */ async remove(path, recursive = false) { return this.withSpan( "fs.remove", { "fs.path": path, "fs.recursive": recursive }, async () => { const result = await this.agentClient.fs.remove(path, recursive); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } } ); } /** * Watch for changes in the filesystem. * * ```ts * const watcher = await sandbox.fs.watch("/path/to/watch"); * watcher.onEvent((event) => { * console.log(event); * }); * * // When done * watcher.dispose(); * ``` * * @param path - The path to watch. * @param options - The options for the watch. * @returns A watcher that can be disposed to stop the watch. */ async watch(path, options = {}) { return this.withSpan( "fs.watch", { "fs.path": path, "fs.recursive": options.recursive ?? false, "fs.excludeCount": options.excludes?.length ?? 0 }, async () => { const emitter = new Emitter(); const result = await this.agentClient.fs.watch( path, options, (event) => { if (this.username) { emitter.fire({ ...event, paths: event.paths.map( (path2) => path2.replace(`home/${this.username}/workspace/`, "sandbox/") ) }); } else { emitter.fire(event); } } ); if (result.type === "error") { throw new Error(`${result.errno}: ${result.error}`); } const watcher = { dispose: () => { result.dispose(); emitter.dispose(); }, onEvent: emitter.event }; this.disposable.addDisposable(watcher); return watcher; } ); } /** * Download a file or folder from the filesystem, can only be used to download * from within the workspace directory. A download URL that's valid for 5 minutes. */ async download(path) { return this.withSpan("fs.download", { "fs.path": path }, async () => { const result = await this.agentClient.fs.download(path); return result; }); } }; // src/SandboxClient/ports.ts var Ports = class { constructor(sessionDisposable, agentClient, tracer) { this.agentClient = agentClient; this.tracer = tracer; this.disposable = new Disposable(); this.onDidPortOpenEmitter = this.disposable.addDisposable( new Emitter() ); this.onDidPortCloseEmitter = this.disposable.addDisposable( new Emitter() ); this.lastOpenedPorts = /* @__PURE__ */ new Set(); sessionDisposable.onWillDispose(() => { this.disposable.dispose(); }); agentClient.ports.getPorts().then((ports) => { ports.forEach((port2) => { this.lastOpenedPorts.add(port2.port); }); }); this.disposable.addDisposable( agentClient.ports.onPortsUpdated((ports) => { const openedPorts = ports.filter( (port2) => !this.lastOpenedPorts.has(port2.port) ); const closedPorts = [...this.lastOpenedPorts].filter( (port2) => !ports.some((p) => p.port === port2) ); if (openedPorts.length) { for (const port2 of openedPorts) { this.onDidPortOpenEmitter.fire({ port: port2.port, host: port2.url }); } } if (closedPorts.length) { for (const port2 of closedPorts) { this.onDidPortCloseEmitter.fire(port2); } } this.lastOpenedPorts = new Set(ports.map((port2) => port2.port)); }) ); } get onDidPortOpen() { return this.onDidPortOpenEmitter.event; } get onDidPortClose() { return this.onDidPortCloseEmitter.event; } withSpan(operationName, attributes, fn) { if (!this.tracer) { return fn(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await fn(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } /** * Get a port by number. */ async get(port2) { return this.withSpan("ports.get", { "port.number": port2 }, async () => { const ports = await this.getAll(); return ports.find((p) => p.port === port2); }); } /** * Get all ports. */ async getAll() { return this.withSpan("ports.getAll", {}, async () => { const ports = await this.agentClient.ports.getPorts(); return ports.map(({ port: port2, url }) => ({ port: port2, host: url })); }); } /** * Wait for a port to be opened. * * @param port - The port to wait for. * @param options - Additional options * @param options.timeoutMs - Optional timeout in milliseconds. If specified, the promise will reject after this time if the port hasn't opened. * @returns A promise that resolves when the port is opened. * @throws {Error} If the timeout is reached before the port opens */ async waitForPort(port2, options) { return this.withSpan( "ports.waitForPort", { "port.number": port2, "port.timeout.ms": options?.timeoutMs }, async () => { return new Promise(async (resolve, reject) => { const portInfo = (await this.getAll()).find((p) => p.port === port2); if (portInfo) { resolve(portInfo); return; } let timeoutId; if (options?.timeoutMs !== void 0) { timeoutId = setTimeout(() => { reject( new Error( `Timeout of ${options.timeoutMs}ms exceeded waiting for port ${port2} to open` ) ); }, options.timeoutMs); } const disposable = this.disposable.addDisposable( this.onDidPortOpen((portInfo2) => { if (portInfo2.port === port2) { if (timeoutId !== void 0) { clearTimeout(timeoutId); } resolve(portInfo2); disposable.dispose(); } }) ); }); } ); } }; // src/utils/barrier.ts var Barrier = class { constructor() { this._isOpen = false; this._promise = new Promise((resolve) => { this._completePromise = resolve; }); } /** * Returns true if the barrier is open, false if it is closed * @returns true if the barrier is open, false if it is closed */ isOpen() { return this._isOpen; } /** * Opens the barrier. If the barrier is already open, this method does nothing. * @param value the value to return when the barrier is opened * @returns */ open(value) { if (this._isOpen) { return; } this._isOpen = true; this._completePromise({ status: "resolved", value }); } /** * * @returns a promise that resolves when the barrier is opened. If the barrier is already open, the promise resolves immediately. */ wait() { return this._promise; } /** * DO NOT USE THIS METHOD in production code. This is only for tests. * This is a convenience method that waits for the barrier to open and then returns the value. * If the Barrier is disposed while waiting to open, an error is thrown. * @returns the value if the barrier is open, otherwise throws an error */ async __waitAndThrowIfDisposed() { const r = await this.wait(); if (r.status === "disposed") { throw new Error("Barrier was disposed"); } return r.value; } /** * Disposes the barrier. * If there is a promise waiting for the barrier to open, it will be resolved with a status of "disposed". */ dispose() { this._completePromise({ status: "disposed" }); } }; // src/SandboxClient/commands.ts var DEFAULT_SHELL_SIZE = { cols: 128, rows: 24 }; var SandboxCommands = class { constructor(sessionDisposable, agentClient, tracer) { this.agentClient = agentClient; this.disposable = new Disposable(); this.tracer = tracer; sessionDisposable.onWillDispose(() => { this.disposable.dispose(); }); } async withSpan(operationName, attributes = {}, operation) { if (!this.tracer) { return operation(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } /** * Create and run command in a new shell. Allows you to listen to the output and kill the command. */ async runBackground(command, opts) { const cmdString = Array.isArray(command) ? command.join(" && ") : command; return this.withSpan( "commands.runBackground", { "command.text": cmdString, "command.cwd": opts?.cwd || "/project", "command.asGlobalSession": opts?.asGlobalSession || false, "command.name": opts?.name || "" }, async () => { const disposableStore = new DisposableStore(); const onOutput = new Emitter(); disposableStore.add(onOutput); command = Array.isArray(command) ? command.join(" && ") : command; const passedEnv = Object.assign(opts?.env ?? {}); const escapedCommand = command.replace(/'/g, "'\\''"); let commandWithEnv = Object.keys(passedEnv).length ? `source $HOME/.private/.env 2>/dev/null || true && env ${Object.entries( passedEnv ).map(([key, value]) => `${key}=${value}`).join(" ")} bash -c '${escapedCommand}'` : `source $HOME/.private/.env 2>/dev/null || true && bash -c '${escapedCommand}'`; if (opts?.cwd) { commandWithEnv = `cd ${opts.cwd} && ${commandWithEnv}`; } const shell2 = await this.agentClient.shells.create( this.agentClient.workspacePath, opts?.dimensions ?? DEFAULT_SHELL_SIZE, commandWithEnv, opts?.asGlobalSession ? "COMMAND" : "TERMINAL", true ); if (shell2.status === "ERROR" || shell2.status === "KILLED") { throw new Error(`Failed to create shell: ${shell2.buffer.join("\n")}`); } const details = { type: "command", command, name: opts?.name }; if (shell2.status !== "FINISHED") { this.agentClient.shells.rename( shell2.shellId, // We embed some details in the name to properly show the command that was run // , the name and that it is an actual command JSON.stringify(details) ).catch(() => { }); } const cmd = new Command( this.agentClient, shell2, details, this.tracer ); return cmd; } ); } /** * Run a command in a new shell and wait for it to finish, returning its output. */ async run(command, opts) { const cmdString = Array.isArray(command) ? command.join(" && ") : command; return this.withSpan( "commands.run", { "command.text": cmdString, "command.cwd": opts?.cwd || "/project", "command.asGlobalSession": opts?.asGlobalSession || false }, async () => { const cmd = await this.runBackground(command, opts); return cmd.waitUntilComplete(); } ); } /** * Get all running commands. */ async getAll() { return this.withSpan("commands.getAll", {}, async () => { const shells = await this.agentClient.shells.getShells(); return shells.filter( (shell2) => shell2.shellType === "TERMINAL" && isCommandShell(shell2) ).map( (shell2) => new Command( this.agentClient, shell2, JSON.parse(shell2.name), this.tracer ) ); }); } }; function isCommandShell(shell2) { try { const parsed = JSON.parse(shell2.name); return parsed.type === "command"; } catch { return false; } } var Command = class { constructor(agentClient, shell2, details, tracer) { this.agentClient = agentClient; this.shell = shell2; this.disposable = new Disposable(); // TODO: differentiate between stdout and stderr, also send back bytes instead of // strings this.onOutputEmitter = this.disposable.addDisposable( new Emitter() ); /** * An event that is emitted when the command outputs something. */ this.onOutput = this.onOutputEmitter.event; this.onStatusChangeEmitter = this.disposable.addDisposable( new Emitter() ); /** * An event that is emitted when the command status changes. */ this.onStatusChange = this.onStatusChangeEmitter.event; this.barrier = new Barrier(); this.output = []; /** * The status of the command. */ this.#status = "RUNNING"; this.command = details.command; this.name = details.name; this.tracer = tracer; this.disposable.addDisposable( agentClient.shells.onShellExited(({ shellId, exitCode }) => { if (shellId === this.shell.shellId) { this.status = exitCode === 0 ? "FINISHED" : "ERROR"; this.barrier.open(); } }) ); this.disposable.addDisposable( agentClient.shells.onShellTerminated(({ shellId }) => { if (shellId === this.shell.shellId) { this.status = "KILLED"; this.barrier.open(); } }) ); this.disposable.addDisposable( this.agentClient.shells.onShellOut(({ shellId, out }) => { if (shellId !== this.shell.shellId || out.startsWith("[CODESANDBOX]")) { return; } this.onOutputEmitter.fire(out); this.output.push(out); if (this.output.length > 1e3) { this.output.shift(); } }) ); } #status; get status() { return this.#status; } set status(value) { if (this.#status !== value) { this.#status = value; this.onStatusChangeEmitter.fire(this.#status); } } async withSpan(operationName, attributes = {}, operation) { if (!this.tracer) { return operation(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } /** * Open the command and get its current output, subscribes to future output */ async open(dimensions = DEFAULT_SHELL_SIZE) { return this.withSpan( "command.open", { "command.shellId": this.shell.shellId, "command.text": this.command, "command.dimensions.cols": dimensions.cols, "command.dimensions.rows": dimensions.rows }, async () => { const shell2 = await this.agentClient.shells.open( this.shell.shellId, dimensions ); this.output = shell2.buffer; return this.output.join("\n"); } ); } /** * Wait for the command to finish with its returned output */ async waitUntilComplete() { return this.withSpan( "command.waitUntilComplete", { "command.shellId": this.shell.shellId, "command.text": this.command, "command.status": this.status }, async () => { await this.barrier.wait(); const cleaned = this.output.join("\n").replace( /Error: failed to exec in podman container: exit status 1[\s\S]*$/, "" ); if (this.status === "FINISHED") { return cleaned; } throw new Error(`Command ERROR: ${cleaned}`); } ); } // TODO: allow for kill signals /** * Kill the command and remove it from the session. */ async kill() { return this.withSpan( "command.kill", { "command.shellId": this.shell.shellId, "command.text": this.command, "command.status": this.status }, async () => { this.disposable.dispose(); await this.agentClient.shells.delete(this.shell.shellId); } ); } /** * Restart the command. */ async restart() { return this.withSpan( "command.restart", { "command.shellId": this.shell.shellId, "command.text": this.command, "command.status": this.status }, async () => { if (this.status !== "RUNNING") { throw new Error("Command is not running"); } await this.agentClient.shells.restart(this.shell.shellId); } ); } }; // src/SandboxClient/terminals.ts var DEFAULT_SHELL_SIZE2 = { cols: 128, rows: 24 }; var Terminals = class { constructor(sessionDisposable, agentClient, tracer) { this.agentClient = agentClient; this.disposable = new Disposable(); this.tracer = tracer; sessionDisposable.onWillDispose(() => { this.disposable.dispose(); }); } async withSpan(operationName, attributes = {}, operation) { if (!this.tracer) { return operation(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } async create(command = "bash", opts) { return this.withSpan( "terminals.create", { command, cwd: opts?.cwd ?? "", name: opts?.name ?? "", envCount: Object.keys(opts?.env ?? {}).length, hasDimensions: !!opts?.dimensions }, async () => { const allEnv = Object.assign(opts?.env ?? {}); let commandWithEnv = Object.keys(allEnv).length ? `source $HOME/.private/.env 2>/dev/null || true && env ${Object.entries( allEnv ).map(([key, value]) => `${key}=${value}`).join(" ")} ${command}` : `source $HOME/.private/.env 2>/dev/null || true && ${command}`; if (opts?.cwd) { commandWithEnv = `cd ${opts.cwd} && ${commandWithEnv}`; } const shell2 = await this.agentClient.shells.create( this.agentClient.workspacePath, opts?.dimensions ?? DEFAULT_SHELL_SIZE2, commandWithEnv, "TERMINAL", true ); if (opts?.name) { this.agentClient.shells.rename(shell2.shellId, opts.name); } return new Terminal(shell2, this.agentClient, this.tracer); } ); } async get(shellId) { return this.withSpan("terminals.get", { shellId }, async () => { const shells = await this.agentClient.shells.getShells(); const shell2 = shells.find((shell3) => shell3.shellId === shellId); if (!shell2) { return; } return new Terminal(shell2, this.agentClient, this.tracer); }); } /** * Gets all terminals running in the current sandbox */ async getAll() { return this.withSpan("terminals.getAll", {}, async () => { const shells = await this.agentClient.shells.getShells(); return shells.filter( (shell2) => shell2.shellType === "TERMINAL" && !isCommandShell(shell2) ).map((shell2) => new Terminal(shell2, this.agentClient, this.tracer)); }); } }; var Terminal = class { constructor(shell2, agentClient, tracer) { this.shell = shell2; this.agentClient = agentClient; this.disposable = new Disposable(); // TODO: differentiate between stdout and stderr, also send back bytes instead of // strings this.onOutputEmitter = this.disposable.addDisposable( new Emitter() ); this.onOutput = this.onOutputEmitter.event; this.output = this.shell.buffer || []; this.tracer = tracer; this.disposable.addDisposable( this.agentClient.shells.onShellOut(({ shellId, out }) => { if (shellId === this.shell.shellId) { this.onOutputEmitter.fire(out); this.output.push(out); if (this.output.length > 1e3) { this.output.shift(); } } }) ); } /** * Gets the ID of the terminal. Can be used to open it again. */ get id() { return this.shell.shellId; } /** * Gets the name of the terminal. */ get name() { return this.shell.name; } async withSpan(operationName, attributes = {}, operation) { if (!this.tracer) { return operation(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } /** * Open the terminal and get its current output, subscribes to future output */ async open(dimensions = DEFAULT_SHELL_SIZE2) { return this.withSpan( "terminal.open", { shellId: this.shell.shellId, cols: dimensions.cols, rows: dimensions.rows }, async () => { const shell2 = await this.agentClient.shells.open( this.shell.shellId, dimensions ); this.output = shell2.buffer; return this.output.join("\n"); } ); } async write(input, dimensions = DEFAULT_SHELL_SIZE2) { return this.withSpan( "terminal.write", { shellId: this.shell.shellId, inputLength: input.length, cols: dimensions.cols, rows: dimensions.rows }, async () => { await this.agentClient.shells.send( this.shell.shellId, input, dimensions ); } ); } async run(input, dimensions = DEFAULT_SHELL_SIZE2) { return this.withSpan( "terminal.run", { shellId: this.shell.shellId, command: input, cols: dimensions.cols, rows: dimensions.rows }, async () => { return this.write(input + "\n", dimensions); } ); } // TODO: allow for kill signals async kill() { return this.withSpan( "terminal.kill", { shellId: this.shell.shellId }, async () => { this.disposable.dispose(); await this.agentClient.shells.delete(this.shell.shellId); } ); } }; // src/SandboxClient/setup.ts var Setup = class { constructor(sessionDisposable, agentClient, setupProgress, tracer) { this.agentClient = agentClient; this.setupProgress = setupProgress; this.tracer = tracer; this.disposable = new Disposable(); this.onSetupProgressChangeEmitter = this.disposable.addDisposable( new Emitter() ); this.onSetupProgressChange = this.onSetupProgressChangeEmitter.event; sessionDisposable.onWillDispose(() => { this.disposable.dispose(); }); this.steps = this.setupProgress.steps.map( (step, index) => new Step(index, step, agentClient, tracer) ); } get status() { return this.setupProgress.state; } get currentStepIndex() { return this.setupProgress.currentStepIndex; } withSpan(operationName, attributes, fn) { if (!this.tracer) { return fn(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await fn(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } getSteps() { return this.steps; } async run() { return this.withSpan( "setup.run", { "setup.state": this.setupProgress.state, "setup.currentStepIndex": this.setupProgress.currentStepIndex, "setup.totalSteps": this.setupProgress.steps.length }, async () => { await this.agentClient.setup.init(); } ); } async waitUntilComplete() { return this.withSpan( "setup.waitUntilComplete", { "setup.state": this.setupProgress.state, "setup.currentStepIndex": this.setupProgress.currentStepIndex, "setup.totalSteps": this.setupProgress.steps.length }, async () => { if (this.setupProgress.state === "STOPPED") { throw new Error("Setup Failed"); } if (this.setupProgress.state === "FINISHED") { return; } return new Promise((resolve, reject) => { const disposer = this.onSetupProgressChange(() => { if (this.setupProgress.state === "FINISHED") { disposer.dispose(); resolve(); } else if (this.setupProgress.state === "STOPPED") { disposer.dispose(); reject(new Error("Setup Failed")); } }); }); } ); } }; var Step = class { constructor(stepIndex, step, agentClient, tracer) { this.stepIndex = stepIndex; this.step = step; this.agentClient = agentClient; this.tracer = tracer; this.disposable = new Disposable(); // TODO: differentiate between stdout and stderr, also send back bytes instead of // strings this.onOutputEmitter = this.disposable.addDisposable( new Emitter() ); this.onOutput = this.onOutputEmitter.event; this.onStatusChangeEmitter = this.disposable.addDisposable( new Emitter() ); this.onStatusChange = this.onStatusChangeEmitter.event; this.output = []; this.disposable.addDisposable( this.agentClient.setup.onSetupProgressUpdate((progress) => { const oldStep = this.step; const newStep = progress.steps[stepIndex]; if (!newStep) { return; } this.step = newStep; if (newStep.finishStatus !== oldStep.finishStatus) { this.onStatusChangeEmitter.fire(newStep.finishStatus || "IDLE"); } }) ); this.disposable.addDisposable( this.agentClient.shells.onShellOut(({ shellId, out }) => { if (shellId === this.step.shellId) { this.onOutputEmitter.fire(out); this.output.push(out); if (this.output.length > 1e3) { this.output.shift(); } } }) ); } get name() { return this.step.name; } get command() { return this.step.command; } get status() { return this.step.finishStatus || "IDLE"; } withSpan(operationName, attributes, fn) { if (!this.tracer) { return fn(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await fn(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } async open(dimensions = DEFAULT_SHELL_SIZE2) { return this.withSpan( "setup.stepOpen", { "step.index": this.stepIndex, "step.name": this.step.name, "step.command": this.step.command, "step.status": this.step.finishStatus || "IDLE", "step.shellId": this.step.shellId, "dimensions.cols": dimensions.cols, "dimensions.rows": dimensions.rows }, async () => { const open = async (shellId) => { const shell2 = await this.agentClient.shells.open(shellId, dimensions); this.output = shell2.buffer; return this.output.join("\n"); }; if (this.step.shellId) { return open(this.step.shellId); } return new Promise((resolve) => { const disposable = this.onStatusChange(() => { if (this.step.shellId) { disposable.dispose(); resolve(open(this.step.shellId)); } }); }); } ); } async waitUntilComplete() { return this.withSpan( "setup.stepWaitUntilComplete", { "step.index": this.stepIndex, "step.name": this.step.name, "step.command": this.step.command, "step.status": this.step.finishStatus || "IDLE", "step.shellId": this.step.shellId }, async () => { if (this.step.finishStatus === "FAILED") { throw new Error("Step Failed"); } if (this.step.finishStatus === "SUCCEEDED" || this.step.finishStatus === "SKIPPED") { return; } return new Promise((resolve, reject) => { const disposable = this.onStatusChange((status) => { if (status === "SUCCEEDED" || status === "SKIPPED") { disposable.dispose(); resolve(); } else if (status === "FAILED") { disposable.dispose(); reject(new Error("Step Failed")); } }); }); } ); } }; // src/SandboxClient/tasks.ts var Tasks = class { constructor(sessionDisposable, agentClient, tracer) { this.agentClient = agentClient; this.disposable = new Disposable(); this.tracer = tracer; sessionDisposable.onWillDispose(() => { this.disposable.dispose(); }); } async withSpan(operationName, attributes = {}, operation) { if (!this.tracer) { return operation(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } /** * Gets all tasks that are available in the current sandbox. */ async getAll() { return this.withSpan( "tasks.getAll", { cached: !!this.cachedTasks }, async () => { if (!this.cachedTasks) { const [tasks, ports] = await Promise.all([ this.agentClient.tasks.getTasks(), this.agentClient.ports.getPorts() ]); this.cachedTasks = Object.values(tasks.tasks).map( (task2) => new Task(this.agentClient, task2, ports, this.tracer) ); } return this.cachedTasks; } ); } /** * Gets a task by its ID. */ async get(taskId) { return this.withSpan("tasks.get", { taskId }, async () => { const tasks = await this.getAll(); return tasks.find((task2) => task2.id === taskId); }); } }; var Task = class { constructor(agentClient, data, _ports, tracer) { this.agentClient = agentClient; this.data = data; this._ports = _ports; this.disposable = new Disposable(); this.onOutputEmitter = this.disposable.addDisposable( new Emitter() ); this.onOutput = this.onOutputEmitter.event; this.onStatusChangeEmitter = this.disposable.addDisposable( new Emitter() ); this.onStatusChange = this.onStatusChangeEmitter.event; this.tracer = tracer; this.disposable.addDisposable( agentClient.ports.onPortsUpdated((ports) => { this._ports = ports; }) ); this.disposable.addDisposable( agentClient.tasks.onTaskUpdate(async (task2) => { if (task2.id !== this.id) { return; } const lastStatus = this.status; const lastShellId = this.shell?.shellId; this.data = task2; if (lastStatus !== this.status) { this.onStatusChangeEmitter.fire(this.status); } if (this.openedShell && task2.shell && task2.shell.shellId !== lastShellId) { const openedShell = await this.agentClient.shells.open( task2.shell.shellId, this.openedShell.dimensions ); this.openedShell = { shellId: openedShell.shellId, output: openedShell.buffer, dimensions: this.openedShell.dimensions }; this.onOutputEmitter.fire("\x1B[2J\x1B[3J\x1B[1;1H"); openedShell.buffer.forEach((out) => this.onOutputEmitter.fire(out)); } }) ); this.disposable.addDisposable( this.agentClient.shells.onShellOut(({ shellId, out }) => { if (!this.shell || this.shell.shellId !== shellId || !this.openedShell) { return; } this.openedShell.output.push(out); this.onOutputEmitter.fire(out); }) ); } get shell() { return this.data.shell; } get id() { return this.data.id; } get name() { return this.data.name; } get command() { return this.data.command; } get runAtStart() { return Boolean(this.data.runAtStart); } get ports() { const configuredPort = this.data.preview?.port; if (configuredPort) { return this._ports.filter((port2) => port2.port === configuredPort); } return this.data.ports; } get status() { return this.shell?.status || "IDLE"; } async withSpan(operationName, attributes = {}, operation) { if (!this.tracer) { return operation(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } async open(dimensions = DEFAULT_SHELL_SIZE2) { return this.withSpan( "task.open", { taskId: this.id, taskName: this.name, cols: dimensions.cols, rows: dimensions.rows, hasShell: !!this.shell }, async () => { if (!this.shell) { throw new Error("Task is not running"); } const openedShell = await this.agentClient.shells.open( this.shell.shellId, dimensions ); this.openedShell = { shellId: openedShell.shellId, output: openedShell.buffer, dimensions }; return this.openedShell.output.join("\n"); } ); } async waitForPort(timeout = 3e4) { return this.withSpan( "task.waitForPort", { taskId: this.id, taskName: this.name, timeout, existingPortsCount: this.ports.length }, async () => { if (this.ports.length) { return this.ports[0]; } let disposer; const [port2] = await Promise.all([ new Promise((resolve) => { disposer = this.agentClient.tasks.onTaskUpdate((task2) => { if (task2.id !== this.id) { return; } if (task2.ports.length) { disposer?.dispose(); resolve(task2.ports[0]); } }); this.disposable.addDisposable(disposer); }), new Promise((resolve, reject) => { setTimeout(() => { disposer?.dispose(); reject(new Error("Timeout waiting for port")); }, timeout); }) ]); return port2; } ); } async run() { return this.withSpan( "task.run", { taskId: this.id, taskName: this.name, command: this.command, runAtStart: this.runAtStart }, async () => { await this.agentClient.tasks.runTask(this.id); } ); } async restart() { return this.withSpan( "task.restart", { taskId: this.id, taskName: this.name, command: this.command }, async () => { await this.run(); } ); } async stop() { return this.withSpan( "task.stop", { taskId: this.id, taskName: this.name, hasShell: !!this.shell }, async () => { if (this.shell) { await this.agentClient.tasks.stopTask(this.id); } } ); } dispose() { this.disposable.dispose(); } }; // src/SandboxClient/interpreters.ts var Interpreters = class { constructor(sessionDisposable, commands, tracer) { this.commands = commands; this.disposable = new Disposable(); this.tracer = tracer; sessionDisposable.onWillDispose(() => { this.disposable.dispose(); }); } async withSpan(operationName, attributes = {}, operation) { if (!this.tracer) { return operation(); } return this.tracer.startActiveSpan( operationName, { attributes }, async (span) => { try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } } ); } run(command, opts) { return this.commands.run(command, opts); } /** * Run a JavaScript code snippet in a new shell. */ javascript(code) { return this.withSpan( "interpreters.javascript", { "interpreter.language": "javascript", "interpreter.code": code, "interpreter.codeLength": code.length }, async () => { return this.run( `node -p "$(cat <<'EOF' (() => {${code.split("\n").map((line, index, lines) => { return index === lines.length - 1 && !line.startsWith("return") ? `return ${line}` : line; }).join("\n")}})() EOF )"`, { env: { NO_COLOR: "true" } } )