@sapiom/sandbox 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @sapiom/sandbox
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - de766f0: Add @sapiom/sandbox package for sandbox environment lifecycle management
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [c9ad2cb]
12
+ - @sapiom/fetch@0.4.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Sapiom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # @sapiom/sandbox
2
+
3
+ Sandbox environment management for the Sapiom SDK. Create isolated execution environments, manage files, run commands, and stream output in real time.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @sapiom/sandbox
9
+ # or
10
+ pnpm add @sapiom/sandbox
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```typescript
16
+ import { SapiomSandbox } from "@sapiom/sandbox";
17
+
18
+ // Create a sandbox (uses SAPIOM_API_KEY env var by default)
19
+ const sandbox = await SapiomSandbox.create({ name: "my-sandbox" });
20
+
21
+ // Write a file
22
+ await sandbox.writeFile("hello.py", 'print("Hello from the sandbox!")');
23
+
24
+ // Execute a command and wait for completion
25
+ const result = await sandbox.exec("python hello.py");
26
+ console.log(result.stdout); // "Hello from the sandbox!"
27
+
28
+ // Stream output from a long-running process
29
+ const proc = await sandbox.execStream("node runner.js");
30
+ for await (const line of proc.output) {
31
+ console.log(`[${line.stream}] ${line.data}`);
32
+ }
33
+ console.log("exit code:", proc.exitCode);
34
+
35
+ // Clean up
36
+ await sandbox.destroy();
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### `SapiomSandbox.create(opts)`
42
+
43
+ Creates a new sandbox and returns a handle for interacting with it.
44
+
45
+ ```typescript
46
+ const sandbox = await SapiomSandbox.create({
47
+ name: "my-sandbox",
48
+ tier: "m",
49
+ ttl: "1h",
50
+ image: "python:3.12",
51
+ envs: { NODE_ENV: "production" },
52
+ });
53
+ ```
54
+
55
+ **Options:**
56
+
57
+ | Option | Type | Required | Description |
58
+ |-----------|--------------------------|----------|-------------------------------------------------------|
59
+ | `name` | `string` | Yes | Sandbox name (lowercase alphanumeric + hyphens, 2-63 chars) |
60
+ | `apiKey` | `string` | No | Sapiom API key. Falls back to `SAPIOM_API_KEY` env var |
61
+ | `baseUrl` | `string` | No | Override the sandbox service URL |
62
+ | `fetch` | `typeof fetch` | No | Pre-configured fetch function (overrides `apiKey`) |
63
+ | `tier` | `SandboxTier` | No | Memory tier: `'xs'`, `'s'`, `'m'`, `'l'`, `'xl'` (default `'s'`) |
64
+ | `ttl` | `string` | No | Time-to-live (e.g. `'1h'`, `'24h'`, `'7d'`) |
65
+ | `envs` | `Record<string, string>` | No | Environment variables |
66
+ | `port` | `number` | No | Single port to expose (mutually exclusive with `ports`) |
67
+ | `ports` | `PortSpec[]` | No | Array of port specs to expose (mutually exclusive with `port`) |
68
+ | `image` | `string` | No | Pre-built Docker image for instant creation |
69
+
70
+ ### `sandbox.writeFile(path, content)`
71
+
72
+ Writes a file relative to the sandbox's workspace root.
73
+
74
+ ```typescript
75
+ await sandbox.writeFile("src/index.ts", 'console.log("hi")');
76
+ ```
77
+
78
+ ### `sandbox.readFile(path)`
79
+
80
+ Reads a file relative to the sandbox's workspace root and returns its content as a string.
81
+
82
+ ```typescript
83
+ const content = await sandbox.readFile("src/index.ts");
84
+ ```
85
+
86
+ ### `sandbox.exec(command, opts?)`
87
+
88
+ Executes a shell command inside the sandbox. By default waits for the process to finish.
89
+
90
+ ```typescript
91
+ // Wait for completion (default)
92
+ const result = await sandbox.exec("npm install");
93
+ console.log(result.exitCode); // 0
94
+ console.log(result.stdout);
95
+ console.log(result.stderr);
96
+
97
+ // Fire-and-forget
98
+ const bg = await sandbox.exec("npm start", { waitForCompletion: false });
99
+ console.log(bg.pid);
100
+
101
+ // Check on it later
102
+ const status = await sandbox.getProcess(bg.pid);
103
+ console.log(status.completed, status.exitCode);
104
+
105
+ // Or wait for it to finish
106
+ const final = await sandbox.waitForProcess(bg.pid);
107
+ console.log(final.exitCode);
108
+ ```
109
+
110
+ **Options:**
111
+
112
+ | Option | Type | Default | Description |
113
+ |----------------------|--------------------------|---------|--------------------------------------------------|
114
+ | `cwd` | `string` | — | Working directory (resolved relative to workspaceRoot) |
115
+ | `env` | `Record<string, string>` | — | Environment variables for the process |
116
+ | `waitForCompletion` | `boolean` | `true` | Wait for the process to finish |
117
+ | `pollInterval` | `number` | `1000` | Polling interval in ms |
118
+ | `timeout` | `number` | `60000` | Timeout in ms when waiting |
119
+ | `signal` | `AbortSignal` | — | Signal to cancel the operation |
120
+
121
+ ### `sandbox.execStream(command, opts?)`
122
+
123
+ Executes a command and streams output in real time via an async iterable. Ideal for long-running processes like AI agent runs.
124
+
125
+ ```typescript
126
+ const proc = await sandbox.execStream("node agent.js");
127
+ for await (const line of proc.output) {
128
+ // line.stream is 'stdout' or 'stderr'
129
+ process.stdout.write(line.data);
130
+ }
131
+ console.log("exit code:", proc.exitCode);
132
+ ```
133
+
134
+ Supports cancellation via `AbortSignal`:
135
+
136
+ ```typescript
137
+ const controller = new AbortController();
138
+ const proc = await sandbox.execStream("long-task", {
139
+ signal: controller.signal,
140
+ });
141
+
142
+ setTimeout(() => controller.abort(), 30_000);
143
+
144
+ for await (const line of proc.output) {
145
+ console.log(line.data);
146
+ }
147
+ ```
148
+
149
+ **Options:**
150
+
151
+ | Option | Type | Default | Description |
152
+ |-----------|--------------------------|---------|--------------------------------------------------|
153
+ | `cwd` | `string` | — | Working directory (resolved relative to workspaceRoot) |
154
+ | `env` | `Record<string, string>` | — | Environment variables for the process |
155
+ | `signal` | `AbortSignal` | — | Signal to cancel the operation |
156
+
157
+ ### `sandbox.getProcess(pid)`
158
+
159
+ Gets the current status of a process by PID.
160
+
161
+ ```typescript
162
+ const status = await sandbox.getProcess(pid);
163
+ console.log(status.completed, status.exitCode);
164
+ ```
165
+
166
+ ### `sandbox.waitForProcess(pid, opts?)`
167
+
168
+ Waits for a process to complete by polling its status. Returns the same `ExecResult` as `exec()`.
169
+
170
+ ```typescript
171
+ const result = await sandbox.waitForProcess(pid, { timeout: 120_000 });
172
+ console.log(result.exitCode, result.stdout);
173
+ ```
174
+
175
+ ### `sandbox.destroy()`
176
+
177
+ Destroys the sandbox and releases all associated resources.
178
+
179
+ ```typescript
180
+ await sandbox.destroy();
181
+ ```
182
+
183
+ ## Properties
184
+
185
+ | Property | Type | Description |
186
+ |-------------------------|----------|----------------------------------------------|
187
+ | `sandbox.name` | `string` | Sandbox identifier |
188
+ | `sandbox.workspaceRoot` | `string` | Absolute workspace root path in the sandbox |
189
+
190
+ ## License
191
+
192
+ MIT
@@ -0,0 +1,3 @@
1
+ export { SapiomSandbox } from "./sandbox.js";
2
+ export type { SandboxCreateOptions, SandboxCreateResponse, SandboxTier, PortSpec, ExecOptions, ExecStreamOptions, ExecResult, StreamingExecResult, OutputLine, ProcessCreateResponse, ProcessStatusResponse, } from "./types.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,WAAW,EACX,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,UAAU,EACV,mBAAmB,EACnB,UAAU,EACV,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SapiomSandbox = void 0;
4
+ var sandbox_js_1 = require("./sandbox.js");
5
+ Object.defineProperty(exports, "SapiomSandbox", { enumerable: true, get: function () { return sandbox_js_1.SapiomSandbox; } });
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAApC,2GAAA,aAAa,OAAA"}
@@ -0,0 +1,80 @@
1
+ import type { SandboxCreateOptions, ExecOptions, ExecResult, ExecStreamOptions, StreamingExecResult, ProcessStatusResponse } from "./types.js";
2
+ export declare class SapiomSandbox {
3
+ /** Name / identifier of the sandbox. */
4
+ readonly name: string;
5
+ /** Absolute workspace root path inside the sandbox. */
6
+ readonly workspaceRoot: string;
7
+ private readonly _fetch;
8
+ private readonly _baseUrl;
9
+ private constructor();
10
+ /**
11
+ * Create a new sandbox and return a handle for interacting with it.
12
+ */
13
+ static create(opts: SandboxCreateOptions): Promise<SapiomSandbox>;
14
+ /**
15
+ * Write a file inside the sandbox.
16
+ *
17
+ * @param path - File path relative to workspaceRoot.
18
+ * @param content - File content as a string.
19
+ */
20
+ writeFile(path: string, content: string): Promise<void>;
21
+ /**
22
+ * Read a file from the sandbox.
23
+ *
24
+ * @param path - File path relative to workspaceRoot.
25
+ * @returns The file content as a string.
26
+ */
27
+ readFile(path: string): Promise<string>;
28
+ /**
29
+ * Execute a command in the sandbox.
30
+ *
31
+ * By default waits for the process to finish (polls process status).
32
+ * Set `opts.waitForCompletion = false` for fire-and-forget execution.
33
+ * Use {@link execStream} for real-time streaming output.
34
+ *
35
+ * @param command - The shell command to run.
36
+ * @param opts - Execution options.
37
+ */
38
+ exec(command: string, opts?: ExecOptions): Promise<ExecResult>;
39
+ /**
40
+ * Execute a command and stream its output in real time.
41
+ *
42
+ * Returns a {@link StreamingExecResult} whose `output` property is an
43
+ * async iterable of {@link OutputLine} objects. `exitCode` is populated
44
+ * after the iterable is fully consumed.
45
+ *
46
+ * @param command - The shell command to run.
47
+ * @param opts - Stream execution options.
48
+ */
49
+ execStream(command: string, opts?: ExecStreamOptions): Promise<StreamingExecResult>;
50
+ /**
51
+ * Get the current status of a process by PID.
52
+ *
53
+ * Useful for checking on processes started with
54
+ * `exec(cmd, { waitForCompletion: false })`.
55
+ *
56
+ * @param pid - The process ID returned from exec.
57
+ */
58
+ getProcess(pid: string): Promise<ProcessStatusResponse>;
59
+ /**
60
+ * Wait for a process to complete by polling its status.
61
+ *
62
+ * Useful for processes started with
63
+ * `exec(cmd, { waitForCompletion: false })`.
64
+ *
65
+ * @param pid - The process ID returned from exec.
66
+ * @param opts - Polling options.
67
+ */
68
+ waitForProcess(pid: string, opts?: {
69
+ pollInterval?: number;
70
+ timeout?: number;
71
+ signal?: AbortSignal;
72
+ }): Promise<ExecResult>;
73
+ /**
74
+ * Destroy the sandbox and release all resources.
75
+ */
76
+ destroy(): Promise<void>;
77
+ private _createProcess;
78
+ private _pollProcess;
79
+ }
80
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EAEpB,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EAGnB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AA0DpB,qBAAa,aAAa;IACxB,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,uDAAuD;IACvD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO;IAYP;;OAEG;WACU,MAAM,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAiCvE;;;;;OAKG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7D;;;;;OAKG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiB7C;;;;;;;;;OASG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IA0BpE;;;;;;;;;OASG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,iBAAiB,GACvB,OAAO,CAAC,mBAAmB,CAAC;IA4H/B;;;;;;;OAOG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAe7D;;;;;;;;OAQG;IACG,cAAc,CAClB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACvE,OAAO,CAAC,UAAU,CAAC;IAItB;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBhB,cAAc;YA8Bd,YAAY;CAqC3B"}
@@ -0,0 +1,345 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SapiomSandbox = void 0;
4
+ const fetch_1 = require("@sapiom/fetch");
5
+ const DEFAULT_BASE_URL = "https://blaxel.services.sapiom.ai";
6
+ const DEFAULT_POLL_INTERVAL = 1000;
7
+ const DEFAULT_EXEC_TIMEOUT = 60000;
8
+ function assertRelativePath(path) {
9
+ const segments = path.split("/");
10
+ for (const seg of segments) {
11
+ if (seg === "..") {
12
+ throw new Error(`Path must not contain '..' segments: ${path}`);
13
+ }
14
+ }
15
+ }
16
+ function resolvePath(workspaceRoot, relativePath) {
17
+ assertRelativePath(relativePath);
18
+ const base = workspaceRoot.endsWith("/")
19
+ ? workspaceRoot.slice(0, -1)
20
+ : workspaceRoot;
21
+ const rel = relativePath.startsWith("/")
22
+ ? relativePath.slice(1)
23
+ : relativePath;
24
+ return `${base}/${rel}`;
25
+ }
26
+ function encodePathSegments(path) {
27
+ return path
28
+ .split("/")
29
+ .map((seg) => encodeURIComponent(seg))
30
+ .join("/");
31
+ }
32
+ function fileUrl(baseUrl, sandboxName, absolutePath) {
33
+ // Strip leading slash so the URL path is well-formed, then encode each segment
34
+ const cleanPath = absolutePath.startsWith("/")
35
+ ? absolutePath.slice(1)
36
+ : absolutePath;
37
+ return `${baseUrl}/v1/sandboxes/${encodeURIComponent(sandboxName)}/filesystem/${encodePathSegments(cleanPath)}`;
38
+ }
39
+ function parseOutputLine(line) {
40
+ if (line.startsWith("stdout:")) {
41
+ return { stream: "stdout", data: line.slice(7) };
42
+ }
43
+ if (line.startsWith("stderr:")) {
44
+ return { stream: "stderr", data: line.slice(7) };
45
+ }
46
+ // Unrecognized framing — treat as stdout rather than dropping
47
+ return { stream: "stdout", data: line };
48
+ }
49
+ class SapiomSandbox {
50
+ constructor(name, workspaceRoot, fetchFn, baseUrl) {
51
+ this.name = name;
52
+ this.workspaceRoot = workspaceRoot;
53
+ this._fetch = fetchFn;
54
+ this._baseUrl = baseUrl;
55
+ }
56
+ /**
57
+ * Create a new sandbox and return a handle for interacting with it.
58
+ */
59
+ static async create(opts) {
60
+ const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
61
+ const fetchFn = opts.fetch ?? (0, fetch_1.createFetch)({ apiKey: opts.apiKey });
62
+ if (opts.port !== undefined && opts.ports !== undefined) {
63
+ throw new Error("Cannot specify both 'port' and 'ports'");
64
+ }
65
+ const body = { name: opts.name };
66
+ if (opts.tier !== undefined)
67
+ body.tier = opts.tier;
68
+ if (opts.ttl !== undefined)
69
+ body.ttl = opts.ttl;
70
+ if (opts.envs !== undefined)
71
+ body.envs = opts.envs;
72
+ if (opts.port !== undefined)
73
+ body.port = opts.port;
74
+ if (opts.ports !== undefined)
75
+ body.ports = opts.ports;
76
+ if (opts.image !== undefined)
77
+ body.image = opts.image;
78
+ const response = await fetchFn(`${baseUrl}/v1/sandboxes`, {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify(body),
82
+ });
83
+ if (!response.ok) {
84
+ const text = await response.text();
85
+ throw new Error(`Failed to create sandbox: ${response.status} ${text}`);
86
+ }
87
+ const data = (await response.json());
88
+ return new SapiomSandbox(data.name, data.workspaceRoot, fetchFn, baseUrl);
89
+ }
90
+ /**
91
+ * Write a file inside the sandbox.
92
+ *
93
+ * @param path - File path relative to workspaceRoot.
94
+ * @param content - File content as a string.
95
+ */
96
+ async writeFile(path, content) {
97
+ assertRelativePath(path);
98
+ const url = fileUrl(this._baseUrl, this.name, path);
99
+ const response = await this._fetch(url, {
100
+ method: "PUT",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify({ content }),
103
+ });
104
+ if (!response.ok) {
105
+ const text = await response.text();
106
+ throw new Error(`Failed to write file '${path}': ${response.status} ${text}`);
107
+ }
108
+ }
109
+ /**
110
+ * Read a file from the sandbox.
111
+ *
112
+ * @param path - File path relative to workspaceRoot.
113
+ * @returns The file content as a string.
114
+ */
115
+ async readFile(path) {
116
+ assertRelativePath(path);
117
+ const url = fileUrl(this._baseUrl, this.name, path);
118
+ const response = await this._fetch(url);
119
+ if (!response.ok) {
120
+ const text = await response.text();
121
+ throw new Error(`Failed to read file '${path}': ${response.status} ${text}`);
122
+ }
123
+ const data = (await response.json());
124
+ return data.content;
125
+ }
126
+ /**
127
+ * Execute a command in the sandbox.
128
+ *
129
+ * By default waits for the process to finish (polls process status).
130
+ * Set `opts.waitForCompletion = false` for fire-and-forget execution.
131
+ * Use {@link execStream} for real-time streaming output.
132
+ *
133
+ * @param command - The shell command to run.
134
+ * @param opts - Execution options.
135
+ */
136
+ async exec(command, opts) {
137
+ const proc = await this._createProcess(command, opts);
138
+ // If the process already completed synchronously, return immediately
139
+ if (proc.status === "completed") {
140
+ return {
141
+ pid: proc.pid,
142
+ exitCode: proc.exitCode ?? 0,
143
+ stdout: proc.stdout ?? "",
144
+ stderr: proc.stderr ?? "",
145
+ };
146
+ }
147
+ const waitForCompletion = opts?.waitForCompletion ?? true;
148
+ if (!waitForCompletion) {
149
+ return {
150
+ pid: proc.pid,
151
+ exitCode: -1,
152
+ stdout: proc.stdout ?? "",
153
+ stderr: proc.stderr ?? "",
154
+ };
155
+ }
156
+ return this._pollProcess(proc.pid, opts);
157
+ }
158
+ /**
159
+ * Execute a command and stream its output in real time.
160
+ *
161
+ * Returns a {@link StreamingExecResult} whose `output` property is an
162
+ * async iterable of {@link OutputLine} objects. `exitCode` is populated
163
+ * after the iterable is fully consumed.
164
+ *
165
+ * @param command - The shell command to run.
166
+ * @param opts - Stream execution options.
167
+ */
168
+ async execStream(command, opts) {
169
+ const proc = await this._createProcess(command, opts);
170
+ // If the process already completed synchronously, return without
171
+ // opening the log stream — yield the stdout/stderr from the create
172
+ // response directly.
173
+ if (proc.status === "completed") {
174
+ const code = proc.exitCode ?? 0;
175
+ const stdout = proc.stdout ?? "";
176
+ const stderr = proc.stderr ?? "";
177
+ const output = (async function* () {
178
+ if (stdout)
179
+ yield { stream: "stdout", data: stdout };
180
+ if (stderr)
181
+ yield { stream: "stderr", data: stderr };
182
+ })();
183
+ return {
184
+ pid: proc.pid,
185
+ get exitCode() {
186
+ return code;
187
+ },
188
+ output,
189
+ };
190
+ }
191
+ let finalExitCode = -1;
192
+ // Capture references for the generator closure
193
+ const fetchFn = this._fetch;
194
+ const baseUrl = this._baseUrl;
195
+ const sandboxName = this.name;
196
+ const signal = opts?.signal;
197
+ async function* streamOutput() {
198
+ const response = await fetchFn(`${baseUrl}/v1/sandboxes/${encodeURIComponent(sandboxName)}/process/${proc.pid}/logs/stream`, { signal });
199
+ if (!response.ok) {
200
+ const text = await response.text();
201
+ throw new Error(`Failed to stream process ${proc.pid}: ${response.status} ${text}`);
202
+ }
203
+ if (!response.body) {
204
+ throw new Error(`No response body for process ${proc.pid} log stream`);
205
+ }
206
+ const reader = response.body.getReader();
207
+ const decoder = new TextDecoder();
208
+ let buffer = "";
209
+ try {
210
+ for (;;) {
211
+ const { done, value } = await reader.read();
212
+ if (done)
213
+ break;
214
+ buffer += decoder.decode(value, { stream: true });
215
+ const lines = buffer.split("\n");
216
+ buffer = lines.pop();
217
+ for (const line of lines) {
218
+ if (!line)
219
+ continue;
220
+ yield parseOutputLine(line);
221
+ }
222
+ }
223
+ // Flush remaining multibyte bytes from the decoder
224
+ buffer += decoder.decode();
225
+ if (buffer) {
226
+ yield parseOutputLine(buffer);
227
+ }
228
+ }
229
+ finally {
230
+ reader.releaseLock();
231
+ }
232
+ // Fetch final process status for the exit code
233
+ const statusResponse = await fetchFn(`${baseUrl}/v1/sandboxes/${encodeURIComponent(sandboxName)}/process/${proc.pid}`, { signal });
234
+ if (!statusResponse.ok) {
235
+ const text = await statusResponse.text();
236
+ throw new Error(`Failed to get final status for process ${proc.pid}: ${statusResponse.status} ${text}`);
237
+ }
238
+ let status = (await statusResponse.json());
239
+ // If the process hasn't completed yet (e.g. stream disconnected
240
+ // before the process finished), poll until it does.
241
+ while (status.status !== "completed") {
242
+ await new Promise((resolve) => setTimeout(resolve, 1000));
243
+ const retry = await fetchFn(`${baseUrl}/v1/sandboxes/${encodeURIComponent(sandboxName)}/process/${proc.pid}`, { signal });
244
+ if (!retry.ok) {
245
+ const text = await retry.text();
246
+ throw new Error(`Failed to get final status for process ${proc.pid}: ${retry.status} ${text}`);
247
+ }
248
+ status = (await retry.json());
249
+ }
250
+ finalExitCode = status.exitCode ?? 0;
251
+ }
252
+ return {
253
+ pid: proc.pid,
254
+ get exitCode() {
255
+ return finalExitCode;
256
+ },
257
+ output: streamOutput(),
258
+ };
259
+ }
260
+ /**
261
+ * Get the current status of a process by PID.
262
+ *
263
+ * Useful for checking on processes started with
264
+ * `exec(cmd, { waitForCompletion: false })`.
265
+ *
266
+ * @param pid - The process ID returned from exec.
267
+ */
268
+ async getProcess(pid) {
269
+ const response = await this._fetch(`${this._baseUrl}/v1/sandboxes/${encodeURIComponent(this.name)}/process/${pid}`);
270
+ if (!response.ok) {
271
+ const text = await response.text();
272
+ throw new Error(`Failed to get process ${pid}: ${response.status} ${text}`);
273
+ }
274
+ return (await response.json());
275
+ }
276
+ /**
277
+ * Wait for a process to complete by polling its status.
278
+ *
279
+ * Useful for processes started with
280
+ * `exec(cmd, { waitForCompletion: false })`.
281
+ *
282
+ * @param pid - The process ID returned from exec.
283
+ * @param opts - Polling options.
284
+ */
285
+ async waitForProcess(pid, opts) {
286
+ return this._pollProcess(pid, opts);
287
+ }
288
+ /**
289
+ * Destroy the sandbox and release all resources.
290
+ */
291
+ async destroy() {
292
+ const response = await this._fetch(`${this._baseUrl}/v1/sandboxes/${encodeURIComponent(this.name)}`, { method: "DELETE" });
293
+ if (!response.ok) {
294
+ const text = await response.text();
295
+ throw new Error(`Failed to destroy sandbox: ${response.status} ${text}`);
296
+ }
297
+ }
298
+ // ---------------------------------------------------------------------------
299
+ // Private helpers
300
+ // ---------------------------------------------------------------------------
301
+ async _createProcess(command, opts) {
302
+ const body = { command };
303
+ if (opts?.cwd !== undefined) {
304
+ body.cwd = resolvePath(this.workspaceRoot, opts.cwd);
305
+ }
306
+ if (opts?.env !== undefined)
307
+ body.env = opts.env;
308
+ const response = await this._fetch(`${this._baseUrl}/v1/sandboxes/${encodeURIComponent(this.name)}/process`, {
309
+ method: "POST",
310
+ headers: { "Content-Type": "application/json" },
311
+ body: JSON.stringify(body),
312
+ signal: opts?.signal,
313
+ });
314
+ if (!response.ok) {
315
+ const text = await response.text();
316
+ throw new Error(`Failed to execute command: ${response.status} ${text}`);
317
+ }
318
+ return (await response.json());
319
+ }
320
+ async _pollProcess(pid, opts) {
321
+ const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
322
+ const timeout = opts?.timeout ?? DEFAULT_EXEC_TIMEOUT;
323
+ const deadline = Date.now() + timeout;
324
+ while (Date.now() < deadline) {
325
+ const response = await this._fetch(`${this._baseUrl}/v1/sandboxes/${encodeURIComponent(this.name)}/process/${pid}`, { signal: opts?.signal });
326
+ if (!response.ok) {
327
+ const text = await response.text();
328
+ throw new Error(`Failed to poll process ${pid}: ${response.status} ${text}`);
329
+ }
330
+ const status = (await response.json());
331
+ if (status.status === "completed") {
332
+ return {
333
+ pid,
334
+ exitCode: status.exitCode ?? 0,
335
+ stdout: status.stdout ?? "",
336
+ stderr: status.stderr ?? "",
337
+ };
338
+ }
339
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
340
+ }
341
+ throw new Error(`Process ${pid} timed out after ${timeout}ms`);
342
+ }
343
+ }
344
+ exports.SapiomSandbox = SapiomSandbox;
345
+ //# sourceMappingURL=sandbox.js.map