@codesandbox/sdk
Version:
The CodeSandbox SDK
1,776 lines (1,763 loc) • 138 kB
JavaScript
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"
}
}
)