@sandbox-engine/sdk 0.1.3 → 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/dist/index.d.mts CHANGED
@@ -8,7 +8,7 @@ declare class HttpClient {
8
8
  private headers;
9
9
  get<T>(path: string, query?: Record<string, string>): Promise<T>;
10
10
  post<T>(path: string, body?: unknown, timeoutMs?: number): Promise<T>;
11
- delete(path: string, query?: Record<string, string>): Promise<void>;
11
+ delete<T = void>(path: string, query?: Record<string, string>): Promise<T>;
12
12
  openWebSocket(path: string): WebSocket;
13
13
  }
14
14
 
@@ -30,6 +30,52 @@ interface SandboxOptions {
30
30
  */
31
31
  dockerfile?: string;
32
32
  timeout?: number;
33
+ /** Environment variables injected into the container for the entire session. */
34
+ env?: Record<string, string>;
35
+ }
36
+ interface SandboxConnectOptions {
37
+ serverUrl?: string;
38
+ apiKey?: string;
39
+ }
40
+ interface SandboxListOptions {
41
+ serverUrl?: string;
42
+ apiKey?: string;
43
+ }
44
+ interface SandboxSummary {
45
+ id: string;
46
+ status: string;
47
+ template: string;
48
+ createdAt: string;
49
+ timeout: number;
50
+ expiresAt: string | null;
51
+ agentUrl: string;
52
+ ports: Record<string, number>;
53
+ }
54
+ interface StreamLogsOptions {
55
+ onStdout?: (line: string) => void;
56
+ onStderr?: (line: string) => void;
57
+ }
58
+ interface StartProcessOptions {
59
+ command?: string;
60
+ cmd?: string;
61
+ args?: string[];
62
+ cwd?: string;
63
+ env?: Record<string, string>;
64
+ processId?: string;
65
+ }
66
+ interface BackgroundProcessInfo {
67
+ id: string;
68
+ pid: number;
69
+ command: string;
70
+ status: 'running' | 'exited';
71
+ exitCode: number | null;
72
+ }
73
+ interface WaitForPortOptions {
74
+ host?: string;
75
+ timeout?: number;
76
+ }
77
+ interface WaitForLogOptions {
78
+ timeout?: number;
33
79
  }
34
80
  interface ProxyEnvResult {
35
81
  proxyBase: string;
@@ -66,6 +112,23 @@ interface FileWriteManyResult {
66
112
  error?: string;
67
113
  }>;
68
114
  }
115
+ interface FileStat {
116
+ path: string;
117
+ size: number;
118
+ mtime: string;
119
+ isDirectory: boolean;
120
+ isFile: boolean;
121
+ }
122
+ interface FileWatchEvent {
123
+ type: 'change' | 'rename' | 'error';
124
+ eventType: 'create' | 'modify' | 'delete';
125
+ path: string;
126
+ data?: string;
127
+ }
128
+ interface FileWatchOptions {
129
+ recursive?: boolean;
130
+ onEvent: (event: FileWatchEvent) => void;
131
+ }
69
132
  interface PortMapping {
70
133
  containerPort: number;
71
134
  hostPort: number;
@@ -90,6 +153,12 @@ declare class Filesystem {
90
153
  writeMany(files: FileWriteEntry[]): Promise<FileWriteManyResult>;
91
154
  rename(oldPath: string, newPath: string): Promise<void>;
92
155
  delete(filePath: string): Promise<void>;
156
+ stat(filePath: string): Promise<FileStat>;
157
+ readBytes(filePath: string): Promise<Uint8Array>;
158
+ writeBytes(filePath: string, data: Uint8Array | Buffer): Promise<void>;
159
+ watch(dirPath: string, opts: FileWatchOptions): Promise<{
160
+ close: () => void;
161
+ }>;
93
162
  }
94
163
 
95
164
  declare class Process extends EventEmitter {
@@ -99,6 +168,40 @@ declare class Process extends EventEmitter {
99
168
  wait(): Promise<ExecResult>;
100
169
  }
101
170
 
171
+ declare class BackgroundProcess {
172
+ private readonly sandboxId;
173
+ private readonly client;
174
+ readonly id: string;
175
+ readonly pid: number;
176
+ readonly command: string;
177
+ constructor(sandboxId: string, client: HttpClient, info: BackgroundProcessInfo);
178
+ private path;
179
+ getStatus(): Promise<BackgroundProcessInfo>;
180
+ getLogs(): Promise<{
181
+ stdout: string;
182
+ stderr: string;
183
+ }>;
184
+ kill(): Promise<void>;
185
+ waitForExit(timeout?: number): Promise<{
186
+ exitCode: number;
187
+ }>;
188
+ waitForPort(port: number, opts?: WaitForPortOptions): Promise<void>;
189
+ waitForLog(pattern: string | RegExp, opts?: WaitForLogOptions): Promise<string>;
190
+ streamLogs(opts?: StreamLogsOptions): {
191
+ close: () => void;
192
+ };
193
+ }
194
+ declare class ProcessManager {
195
+ private readonly sandboxId;
196
+ private readonly client;
197
+ constructor(sandboxId: string, client: HttpClient);
198
+ start(opts: StartProcessOptions): Promise<BackgroundProcess>;
199
+ list(): Promise<BackgroundProcessInfo[]>;
200
+ get(processId: string): Promise<BackgroundProcess>;
201
+ kill(processId: string): Promise<void>;
202
+ killAll(): Promise<number>;
203
+ }
204
+
102
205
  declare class Terminal extends EventEmitter {
103
206
  private ws;
104
207
  constructor(ws: WebSocket);
@@ -121,8 +224,16 @@ declare class Sandbox {
121
224
  private readonly client;
122
225
  readonly id: string;
123
226
  readonly fs: Filesystem;
227
+ readonly processes: ProcessManager;
124
228
  private constructor();
229
+ private static resolveClient;
230
+ static list(opts?: SandboxListOptions): Promise<SandboxSummary[]>;
125
231
  static create(opts?: SandboxOptions): Promise<Sandbox>;
232
+ /**
233
+ * Reconnect to an existing sandbox by ID.
234
+ * Works after server restart if the container is still running (auto-adopt).
235
+ */
236
+ static connect(sandboxId: string, opts?: SandboxConnectOptions): Promise<Sandbox>;
126
237
  exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
127
238
  exec(cmd: string, opts: ExecOptions & {
128
239
  stream: true;
@@ -141,4 +252,4 @@ declare class Sandbox {
141
252
  close(): Promise<void>;
142
253
  }
143
254
 
144
- export { type ExecOptions, type ExecResult, type FileEntry, type FileWriteEntry, type FileWriteManyResult, Filesystem, type PortMapping, Process, type ProxyEnvResult, Sandbox, type SandboxOptions, Terminal, type WsMessage, type WsMessageType };
255
+ export { BackgroundProcess, type BackgroundProcessInfo, type ExecOptions, type ExecResult, type FileEntry, type FileStat, type FileWatchEvent, type FileWatchOptions, type FileWriteEntry, type FileWriteManyResult, Filesystem, type PortMapping, Process, ProcessManager, type ProxyEnvResult, Sandbox, type SandboxConnectOptions, type SandboxListOptions, type SandboxOptions, type SandboxSummary, type StartProcessOptions, type StreamLogsOptions, Terminal, type WaitForLogOptions, type WaitForPortOptions, type WsMessage, type WsMessageType };
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ declare class HttpClient {
8
8
  private headers;
9
9
  get<T>(path: string, query?: Record<string, string>): Promise<T>;
10
10
  post<T>(path: string, body?: unknown, timeoutMs?: number): Promise<T>;
11
- delete(path: string, query?: Record<string, string>): Promise<void>;
11
+ delete<T = void>(path: string, query?: Record<string, string>): Promise<T>;
12
12
  openWebSocket(path: string): WebSocket;
13
13
  }
14
14
 
@@ -30,6 +30,52 @@ interface SandboxOptions {
30
30
  */
31
31
  dockerfile?: string;
32
32
  timeout?: number;
33
+ /** Environment variables injected into the container for the entire session. */
34
+ env?: Record<string, string>;
35
+ }
36
+ interface SandboxConnectOptions {
37
+ serverUrl?: string;
38
+ apiKey?: string;
39
+ }
40
+ interface SandboxListOptions {
41
+ serverUrl?: string;
42
+ apiKey?: string;
43
+ }
44
+ interface SandboxSummary {
45
+ id: string;
46
+ status: string;
47
+ template: string;
48
+ createdAt: string;
49
+ timeout: number;
50
+ expiresAt: string | null;
51
+ agentUrl: string;
52
+ ports: Record<string, number>;
53
+ }
54
+ interface StreamLogsOptions {
55
+ onStdout?: (line: string) => void;
56
+ onStderr?: (line: string) => void;
57
+ }
58
+ interface StartProcessOptions {
59
+ command?: string;
60
+ cmd?: string;
61
+ args?: string[];
62
+ cwd?: string;
63
+ env?: Record<string, string>;
64
+ processId?: string;
65
+ }
66
+ interface BackgroundProcessInfo {
67
+ id: string;
68
+ pid: number;
69
+ command: string;
70
+ status: 'running' | 'exited';
71
+ exitCode: number | null;
72
+ }
73
+ interface WaitForPortOptions {
74
+ host?: string;
75
+ timeout?: number;
76
+ }
77
+ interface WaitForLogOptions {
78
+ timeout?: number;
33
79
  }
34
80
  interface ProxyEnvResult {
35
81
  proxyBase: string;
@@ -66,6 +112,23 @@ interface FileWriteManyResult {
66
112
  error?: string;
67
113
  }>;
68
114
  }
115
+ interface FileStat {
116
+ path: string;
117
+ size: number;
118
+ mtime: string;
119
+ isDirectory: boolean;
120
+ isFile: boolean;
121
+ }
122
+ interface FileWatchEvent {
123
+ type: 'change' | 'rename' | 'error';
124
+ eventType: 'create' | 'modify' | 'delete';
125
+ path: string;
126
+ data?: string;
127
+ }
128
+ interface FileWatchOptions {
129
+ recursive?: boolean;
130
+ onEvent: (event: FileWatchEvent) => void;
131
+ }
69
132
  interface PortMapping {
70
133
  containerPort: number;
71
134
  hostPort: number;
@@ -90,6 +153,12 @@ declare class Filesystem {
90
153
  writeMany(files: FileWriteEntry[]): Promise<FileWriteManyResult>;
91
154
  rename(oldPath: string, newPath: string): Promise<void>;
92
155
  delete(filePath: string): Promise<void>;
156
+ stat(filePath: string): Promise<FileStat>;
157
+ readBytes(filePath: string): Promise<Uint8Array>;
158
+ writeBytes(filePath: string, data: Uint8Array | Buffer): Promise<void>;
159
+ watch(dirPath: string, opts: FileWatchOptions): Promise<{
160
+ close: () => void;
161
+ }>;
93
162
  }
94
163
 
95
164
  declare class Process extends EventEmitter {
@@ -99,6 +168,40 @@ declare class Process extends EventEmitter {
99
168
  wait(): Promise<ExecResult>;
100
169
  }
101
170
 
171
+ declare class BackgroundProcess {
172
+ private readonly sandboxId;
173
+ private readonly client;
174
+ readonly id: string;
175
+ readonly pid: number;
176
+ readonly command: string;
177
+ constructor(sandboxId: string, client: HttpClient, info: BackgroundProcessInfo);
178
+ private path;
179
+ getStatus(): Promise<BackgroundProcessInfo>;
180
+ getLogs(): Promise<{
181
+ stdout: string;
182
+ stderr: string;
183
+ }>;
184
+ kill(): Promise<void>;
185
+ waitForExit(timeout?: number): Promise<{
186
+ exitCode: number;
187
+ }>;
188
+ waitForPort(port: number, opts?: WaitForPortOptions): Promise<void>;
189
+ waitForLog(pattern: string | RegExp, opts?: WaitForLogOptions): Promise<string>;
190
+ streamLogs(opts?: StreamLogsOptions): {
191
+ close: () => void;
192
+ };
193
+ }
194
+ declare class ProcessManager {
195
+ private readonly sandboxId;
196
+ private readonly client;
197
+ constructor(sandboxId: string, client: HttpClient);
198
+ start(opts: StartProcessOptions): Promise<BackgroundProcess>;
199
+ list(): Promise<BackgroundProcessInfo[]>;
200
+ get(processId: string): Promise<BackgroundProcess>;
201
+ kill(processId: string): Promise<void>;
202
+ killAll(): Promise<number>;
203
+ }
204
+
102
205
  declare class Terminal extends EventEmitter {
103
206
  private ws;
104
207
  constructor(ws: WebSocket);
@@ -121,8 +224,16 @@ declare class Sandbox {
121
224
  private readonly client;
122
225
  readonly id: string;
123
226
  readonly fs: Filesystem;
227
+ readonly processes: ProcessManager;
124
228
  private constructor();
229
+ private static resolveClient;
230
+ static list(opts?: SandboxListOptions): Promise<SandboxSummary[]>;
125
231
  static create(opts?: SandboxOptions): Promise<Sandbox>;
232
+ /**
233
+ * Reconnect to an existing sandbox by ID.
234
+ * Works after server restart if the container is still running (auto-adopt).
235
+ */
236
+ static connect(sandboxId: string, opts?: SandboxConnectOptions): Promise<Sandbox>;
126
237
  exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
127
238
  exec(cmd: string, opts: ExecOptions & {
128
239
  stream: true;
@@ -141,4 +252,4 @@ declare class Sandbox {
141
252
  close(): Promise<void>;
142
253
  }
143
254
 
144
- export { type ExecOptions, type ExecResult, type FileEntry, type FileWriteEntry, type FileWriteManyResult, Filesystem, type PortMapping, Process, type ProxyEnvResult, Sandbox, type SandboxOptions, Terminal, type WsMessage, type WsMessageType };
255
+ export { BackgroundProcess, type BackgroundProcessInfo, type ExecOptions, type ExecResult, type FileEntry, type FileStat, type FileWatchEvent, type FileWatchOptions, type FileWriteEntry, type FileWriteManyResult, Filesystem, type PortMapping, Process, ProcessManager, type ProxyEnvResult, Sandbox, type SandboxConnectOptions, type SandboxListOptions, type SandboxOptions, type SandboxSummary, type StartProcessOptions, type StreamLogsOptions, Terminal, type WaitForLogOptions, type WaitForPortOptions, type WsMessage, type WsMessageType };
package/dist/index.js CHANGED
@@ -30,8 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BackgroundProcess: () => BackgroundProcess,
33
34
  Filesystem: () => Filesystem,
34
35
  Process: () => Process,
36
+ ProcessManager: () => ProcessManager,
35
37
  Sandbox: () => Sandbox,
36
38
  Terminal: () => Terminal
37
39
  });
@@ -86,9 +88,15 @@ var HttpClient = class {
86
88
  signal: AbortSignal.timeout(3e4)
87
89
  });
88
90
  if (!res.ok && res.status !== 404) {
89
- const text = await res.text();
90
- throw new Error(`DELETE ${path} failed (${res.status}): ${text}`);
91
+ const text2 = await res.text();
92
+ throw new Error(`DELETE ${path} failed (${res.status}): ${text2}`);
93
+ }
94
+ if (res.status === 204 || res.headers.get("content-length") === "0") {
95
+ return void 0;
91
96
  }
97
+ const text = await res.text();
98
+ if (!text) return void 0;
99
+ return JSON.parse(text);
92
100
  }
93
101
  openWebSocket(path) {
94
102
  const wsUrl = this.baseUrl.replace(/^http/, "ws") + path;
@@ -141,6 +149,49 @@ var Filesystem = class {
141
149
  async delete(filePath) {
142
150
  await this.client.delete(`/api/sandboxes/${this.sandboxId}/files`, { path: filePath });
143
151
  }
152
+ async stat(filePath) {
153
+ return this.client.get(
154
+ `/api/sandboxes/${this.sandboxId}/files/stat`,
155
+ { path: filePath }
156
+ );
157
+ }
158
+ async readBytes(filePath) {
159
+ const res = await this.client.get(
160
+ `/api/sandboxes/${this.sandboxId}/files/bytes`,
161
+ { path: filePath }
162
+ );
163
+ return Uint8Array.from(Buffer.from(res.data, "base64"));
164
+ }
165
+ async writeBytes(filePath, data) {
166
+ await this.client.post(`/api/sandboxes/${this.sandboxId}/files/bytes`, {
167
+ path: filePath,
168
+ data: Buffer.from(data).toString("base64")
169
+ });
170
+ }
171
+ async watch(dirPath, opts) {
172
+ const ws = this.client.openWebSocket(`/api/sandboxes/${this.sandboxId}/files/watch`);
173
+ await new Promise((resolve, reject) => {
174
+ ws.once("open", () => {
175
+ ws.send(JSON.stringify({ path: dirPath, recursive: opts.recursive ?? false }));
176
+ resolve();
177
+ });
178
+ ws.once("error", reject);
179
+ });
180
+ ws.on("message", (raw) => {
181
+ try {
182
+ const event = JSON.parse(raw.toString());
183
+ opts.onEvent(event);
184
+ } catch {
185
+ }
186
+ });
187
+ return {
188
+ close: () => {
189
+ if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) {
190
+ ws.close();
191
+ }
192
+ }
193
+ };
194
+ }
144
195
  };
145
196
 
146
197
  // src/process.ts
@@ -186,6 +237,112 @@ var Process = class extends import_node_events.EventEmitter {
186
237
  }
187
238
  };
188
239
 
240
+ // src/background-process.ts
241
+ var BackgroundProcess = class {
242
+ constructor(sandboxId, client, info) {
243
+ this.sandboxId = sandboxId;
244
+ this.client = client;
245
+ this.id = info.id;
246
+ this.pid = info.pid;
247
+ this.command = info.command;
248
+ }
249
+ sandboxId;
250
+ client;
251
+ id;
252
+ pid;
253
+ command;
254
+ path(suffix) {
255
+ return `/api/sandboxes/${this.sandboxId}/processes/${this.id}${suffix}`;
256
+ }
257
+ async getStatus() {
258
+ return this.client.get(this.path(""));
259
+ }
260
+ async getLogs() {
261
+ return this.client.get(this.path("/logs"));
262
+ }
263
+ async kill() {
264
+ await this.client.delete(this.path(""));
265
+ }
266
+ async waitForExit(timeout) {
267
+ return this.client.post(this.path("/wait-exit"), { timeout }, (timeout ?? 3e5) + 5e3);
268
+ }
269
+ async waitForPort(port, opts = {}) {
270
+ await this.client.post(
271
+ this.path("/wait-for-port"),
272
+ { port, host: opts.host, timeout: opts.timeout },
273
+ (opts.timeout ?? 3e4) + 5e3
274
+ );
275
+ }
276
+ async waitForLog(pattern, opts = {}) {
277
+ const patternStr = pattern instanceof RegExp ? pattern.source : pattern;
278
+ const res = await this.client.post(
279
+ this.path("/wait-for-log"),
280
+ { pattern: patternStr, timeout: opts.timeout },
281
+ (opts.timeout ?? 3e4) + 5e3
282
+ );
283
+ return res.match;
284
+ }
285
+ streamLogs(opts = {}) {
286
+ const ws = this.client.openWebSocket(
287
+ `/api/sandboxes/${this.sandboxId}/processes/logs/stream`
288
+ );
289
+ ws.on("open", () => {
290
+ ws.send(JSON.stringify({ processId: this.id }));
291
+ });
292
+ ws.on("message", (raw) => {
293
+ try {
294
+ const msg = JSON.parse(raw.toString());
295
+ if (msg.type === "stdout") opts.onStdout?.(msg.data ?? "");
296
+ else if (msg.type === "stderr") opts.onStderr?.(msg.data ?? "");
297
+ } catch {
298
+ }
299
+ });
300
+ return {
301
+ close: () => {
302
+ if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) {
303
+ ws.close();
304
+ }
305
+ }
306
+ };
307
+ }
308
+ };
309
+ var ProcessManager = class {
310
+ constructor(sandboxId, client) {
311
+ this.sandboxId = sandboxId;
312
+ this.client = client;
313
+ }
314
+ sandboxId;
315
+ client;
316
+ async start(opts) {
317
+ const info = await this.client.post(
318
+ `/api/sandboxes/${this.sandboxId}/processes`,
319
+ opts
320
+ );
321
+ return new BackgroundProcess(this.sandboxId, this.client, info);
322
+ }
323
+ async list() {
324
+ const res = await this.client.get(
325
+ `/api/sandboxes/${this.sandboxId}/processes`
326
+ );
327
+ return res.processes;
328
+ }
329
+ async get(processId) {
330
+ const info = await this.client.get(
331
+ `/api/sandboxes/${this.sandboxId}/processes/${processId}`
332
+ );
333
+ return new BackgroundProcess(this.sandboxId, this.client, info);
334
+ }
335
+ async kill(processId) {
336
+ await this.client.delete(`/api/sandboxes/${this.sandboxId}/processes/${processId}`);
337
+ }
338
+ async killAll() {
339
+ const res = await this.client.delete(
340
+ `/api/sandboxes/${this.sandboxId}/processes`
341
+ );
342
+ return res?.killed ?? 0;
343
+ }
344
+ };
345
+
189
346
  // src/terminal.ts
190
347
  var import_node_events2 = require("events");
191
348
  var Terminal = class extends import_node_events2.EventEmitter {
@@ -227,26 +384,46 @@ var Sandbox = class _Sandbox {
227
384
  this.client = client;
228
385
  this.id = id;
229
386
  this.fs = new Filesystem(id, client);
387
+ this.processes = new ProcessManager(id, client);
230
388
  }
231
389
  client;
232
390
  id;
233
391
  fs;
392
+ processes;
393
+ static resolveClient(opts = {}) {
394
+ const serverUrl = opts.serverUrl ?? process.env.SANDBOX_SERVER_URL ?? "http://localhost:4000";
395
+ const apiKey = opts.apiKey ?? process.env.SANDBOX_API_KEY ?? "dev-api-key";
396
+ return new HttpClient(serverUrl, apiKey);
397
+ }
398
+ static async list(opts = {}) {
399
+ const client = _Sandbox.resolveClient(opts);
400
+ return client.get("/api/sandboxes");
401
+ }
234
402
  static async create(opts = {}) {
235
- const {
236
- serverUrl = process.env.SANDBOX_SERVER_URL ?? "http://localhost:4000",
237
- apiKey = process.env.SANDBOX_API_KEY ?? "dev-api-key",
238
- template,
239
- dockerfile,
240
- timeout
241
- } = opts;
242
- const client = new HttpClient(serverUrl, apiKey);
403
+ const client = _Sandbox.resolveClient(opts);
243
404
  const data = await client.post("/api/sandboxes", {
244
- template,
245
- dockerfile,
246
- timeout
405
+ template: opts.template,
406
+ dockerfile: opts.dockerfile,
407
+ timeout: opts.timeout,
408
+ env: opts.env
247
409
  });
248
410
  return new _Sandbox(client, data.id);
249
411
  }
412
+ /**
413
+ * Reconnect to an existing sandbox by ID.
414
+ * Works after server restart if the container is still running (auto-adopt).
415
+ */
416
+ static async connect(sandboxId, opts = {}) {
417
+ const client = _Sandbox.resolveClient(opts);
418
+ const data = await client.post(
419
+ `/api/sandboxes/${sandboxId}/connect`,
420
+ {}
421
+ );
422
+ if (data.status === "stopped") {
423
+ throw new Error(`Sandbox ${sandboxId} is stopped`);
424
+ }
425
+ return new _Sandbox(client, data.id);
426
+ }
250
427
  async exec(cmd, opts = {}) {
251
428
  const { args, cwd, env, timeout, stream } = opts;
252
429
  if (stream) {
@@ -325,8 +502,10 @@ var Sandbox = class _Sandbox {
325
502
  };
326
503
  // Annotate the CommonJS export names for ESM import in node:
327
504
  0 && (module.exports = {
505
+ BackgroundProcess,
328
506
  Filesystem,
329
507
  Process,
508
+ ProcessManager,
330
509
  Sandbox,
331
510
  Terminal
332
511
  });
package/dist/index.mjs CHANGED
@@ -47,9 +47,15 @@ var HttpClient = class {
47
47
  signal: AbortSignal.timeout(3e4)
48
48
  });
49
49
  if (!res.ok && res.status !== 404) {
50
- const text = await res.text();
51
- throw new Error(`DELETE ${path} failed (${res.status}): ${text}`);
50
+ const text2 = await res.text();
51
+ throw new Error(`DELETE ${path} failed (${res.status}): ${text2}`);
52
+ }
53
+ if (res.status === 204 || res.headers.get("content-length") === "0") {
54
+ return void 0;
52
55
  }
56
+ const text = await res.text();
57
+ if (!text) return void 0;
58
+ return JSON.parse(text);
53
59
  }
54
60
  openWebSocket(path) {
55
61
  const wsUrl = this.baseUrl.replace(/^http/, "ws") + path;
@@ -102,6 +108,49 @@ var Filesystem = class {
102
108
  async delete(filePath) {
103
109
  await this.client.delete(`/api/sandboxes/${this.sandboxId}/files`, { path: filePath });
104
110
  }
111
+ async stat(filePath) {
112
+ return this.client.get(
113
+ `/api/sandboxes/${this.sandboxId}/files/stat`,
114
+ { path: filePath }
115
+ );
116
+ }
117
+ async readBytes(filePath) {
118
+ const res = await this.client.get(
119
+ `/api/sandboxes/${this.sandboxId}/files/bytes`,
120
+ { path: filePath }
121
+ );
122
+ return Uint8Array.from(Buffer.from(res.data, "base64"));
123
+ }
124
+ async writeBytes(filePath, data) {
125
+ await this.client.post(`/api/sandboxes/${this.sandboxId}/files/bytes`, {
126
+ path: filePath,
127
+ data: Buffer.from(data).toString("base64")
128
+ });
129
+ }
130
+ async watch(dirPath, opts) {
131
+ const ws = this.client.openWebSocket(`/api/sandboxes/${this.sandboxId}/files/watch`);
132
+ await new Promise((resolve, reject) => {
133
+ ws.once("open", () => {
134
+ ws.send(JSON.stringify({ path: dirPath, recursive: opts.recursive ?? false }));
135
+ resolve();
136
+ });
137
+ ws.once("error", reject);
138
+ });
139
+ ws.on("message", (raw) => {
140
+ try {
141
+ const event = JSON.parse(raw.toString());
142
+ opts.onEvent(event);
143
+ } catch {
144
+ }
145
+ });
146
+ return {
147
+ close: () => {
148
+ if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) {
149
+ ws.close();
150
+ }
151
+ }
152
+ };
153
+ }
105
154
  };
106
155
 
107
156
  // src/process.ts
@@ -147,6 +196,112 @@ var Process = class extends EventEmitter {
147
196
  }
148
197
  };
149
198
 
199
+ // src/background-process.ts
200
+ var BackgroundProcess = class {
201
+ constructor(sandboxId, client, info) {
202
+ this.sandboxId = sandboxId;
203
+ this.client = client;
204
+ this.id = info.id;
205
+ this.pid = info.pid;
206
+ this.command = info.command;
207
+ }
208
+ sandboxId;
209
+ client;
210
+ id;
211
+ pid;
212
+ command;
213
+ path(suffix) {
214
+ return `/api/sandboxes/${this.sandboxId}/processes/${this.id}${suffix}`;
215
+ }
216
+ async getStatus() {
217
+ return this.client.get(this.path(""));
218
+ }
219
+ async getLogs() {
220
+ return this.client.get(this.path("/logs"));
221
+ }
222
+ async kill() {
223
+ await this.client.delete(this.path(""));
224
+ }
225
+ async waitForExit(timeout) {
226
+ return this.client.post(this.path("/wait-exit"), { timeout }, (timeout ?? 3e5) + 5e3);
227
+ }
228
+ async waitForPort(port, opts = {}) {
229
+ await this.client.post(
230
+ this.path("/wait-for-port"),
231
+ { port, host: opts.host, timeout: opts.timeout },
232
+ (opts.timeout ?? 3e4) + 5e3
233
+ );
234
+ }
235
+ async waitForLog(pattern, opts = {}) {
236
+ const patternStr = pattern instanceof RegExp ? pattern.source : pattern;
237
+ const res = await this.client.post(
238
+ this.path("/wait-for-log"),
239
+ { pattern: patternStr, timeout: opts.timeout },
240
+ (opts.timeout ?? 3e4) + 5e3
241
+ );
242
+ return res.match;
243
+ }
244
+ streamLogs(opts = {}) {
245
+ const ws = this.client.openWebSocket(
246
+ `/api/sandboxes/${this.sandboxId}/processes/logs/stream`
247
+ );
248
+ ws.on("open", () => {
249
+ ws.send(JSON.stringify({ processId: this.id }));
250
+ });
251
+ ws.on("message", (raw) => {
252
+ try {
253
+ const msg = JSON.parse(raw.toString());
254
+ if (msg.type === "stdout") opts.onStdout?.(msg.data ?? "");
255
+ else if (msg.type === "stderr") opts.onStderr?.(msg.data ?? "");
256
+ } catch {
257
+ }
258
+ });
259
+ return {
260
+ close: () => {
261
+ if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) {
262
+ ws.close();
263
+ }
264
+ }
265
+ };
266
+ }
267
+ };
268
+ var ProcessManager = class {
269
+ constructor(sandboxId, client) {
270
+ this.sandboxId = sandboxId;
271
+ this.client = client;
272
+ }
273
+ sandboxId;
274
+ client;
275
+ async start(opts) {
276
+ const info = await this.client.post(
277
+ `/api/sandboxes/${this.sandboxId}/processes`,
278
+ opts
279
+ );
280
+ return new BackgroundProcess(this.sandboxId, this.client, info);
281
+ }
282
+ async list() {
283
+ const res = await this.client.get(
284
+ `/api/sandboxes/${this.sandboxId}/processes`
285
+ );
286
+ return res.processes;
287
+ }
288
+ async get(processId) {
289
+ const info = await this.client.get(
290
+ `/api/sandboxes/${this.sandboxId}/processes/${processId}`
291
+ );
292
+ return new BackgroundProcess(this.sandboxId, this.client, info);
293
+ }
294
+ async kill(processId) {
295
+ await this.client.delete(`/api/sandboxes/${this.sandboxId}/processes/${processId}`);
296
+ }
297
+ async killAll() {
298
+ const res = await this.client.delete(
299
+ `/api/sandboxes/${this.sandboxId}/processes`
300
+ );
301
+ return res?.killed ?? 0;
302
+ }
303
+ };
304
+
150
305
  // src/terminal.ts
151
306
  import { EventEmitter as EventEmitter2 } from "events";
152
307
  var Terminal = class extends EventEmitter2 {
@@ -188,26 +343,46 @@ var Sandbox = class _Sandbox {
188
343
  this.client = client;
189
344
  this.id = id;
190
345
  this.fs = new Filesystem(id, client);
346
+ this.processes = new ProcessManager(id, client);
191
347
  }
192
348
  client;
193
349
  id;
194
350
  fs;
351
+ processes;
352
+ static resolveClient(opts = {}) {
353
+ const serverUrl = opts.serverUrl ?? process.env.SANDBOX_SERVER_URL ?? "http://localhost:4000";
354
+ const apiKey = opts.apiKey ?? process.env.SANDBOX_API_KEY ?? "dev-api-key";
355
+ return new HttpClient(serverUrl, apiKey);
356
+ }
357
+ static async list(opts = {}) {
358
+ const client = _Sandbox.resolveClient(opts);
359
+ return client.get("/api/sandboxes");
360
+ }
195
361
  static async create(opts = {}) {
196
- const {
197
- serverUrl = process.env.SANDBOX_SERVER_URL ?? "http://localhost:4000",
198
- apiKey = process.env.SANDBOX_API_KEY ?? "dev-api-key",
199
- template,
200
- dockerfile,
201
- timeout
202
- } = opts;
203
- const client = new HttpClient(serverUrl, apiKey);
362
+ const client = _Sandbox.resolveClient(opts);
204
363
  const data = await client.post("/api/sandboxes", {
205
- template,
206
- dockerfile,
207
- timeout
364
+ template: opts.template,
365
+ dockerfile: opts.dockerfile,
366
+ timeout: opts.timeout,
367
+ env: opts.env
208
368
  });
209
369
  return new _Sandbox(client, data.id);
210
370
  }
371
+ /**
372
+ * Reconnect to an existing sandbox by ID.
373
+ * Works after server restart if the container is still running (auto-adopt).
374
+ */
375
+ static async connect(sandboxId, opts = {}) {
376
+ const client = _Sandbox.resolveClient(opts);
377
+ const data = await client.post(
378
+ `/api/sandboxes/${sandboxId}/connect`,
379
+ {}
380
+ );
381
+ if (data.status === "stopped") {
382
+ throw new Error(`Sandbox ${sandboxId} is stopped`);
383
+ }
384
+ return new _Sandbox(client, data.id);
385
+ }
211
386
  async exec(cmd, opts = {}) {
212
387
  const { args, cwd, env, timeout, stream } = opts;
213
388
  if (stream) {
@@ -285,8 +460,10 @@ var Sandbox = class _Sandbox {
285
460
  }
286
461
  };
287
462
  export {
463
+ BackgroundProcess,
288
464
  Filesystem,
289
465
  Process,
466
+ ProcessManager,
290
467
  Sandbox,
291
468
  Terminal
292
469
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandbox-engine/sdk",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "SDK for creating and managing isolated sandbox environments",
5
5
  "keywords": ["sandbox", "docker", "code-execution", "isolated", "e2b"],
6
6
  "license": "MIT",