computesdk 2.0.0 → 2.0.2

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.mjs CHANGED
@@ -1,671 +1,4428 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
1
+ // src/client/protocol.ts
2
+ var MessageType = /* @__PURE__ */ ((MessageType2) => {
3
+ MessageType2[MessageType2["Subscribe"] = 1] = "Subscribe";
4
+ MessageType2[MessageType2["Unsubscribe"] = 2] = "Unsubscribe";
5
+ MessageType2[MessageType2["Data"] = 3] = "Data";
6
+ MessageType2[MessageType2["Error"] = 4] = "Error";
7
+ MessageType2[MessageType2["Connected"] = 5] = "Connected";
8
+ return MessageType2;
9
+ })(MessageType || {});
10
+ var textEncoder = new TextEncoder();
11
+ var textDecoder = new TextDecoder();
12
+ function getValueSize(value) {
13
+ if (typeof value === "string") {
14
+ return textEncoder.encode(value).length;
15
+ } else if (typeof value === "number") {
16
+ return 8;
17
+ } else if (typeof value === "boolean") {
18
+ return 1;
19
+ } else if (value instanceof Uint8Array) {
20
+ return value.length;
21
+ }
22
+ return 0;
23
+ }
24
+ function encodeKeyValue(data) {
25
+ let totalSize = 2;
26
+ const fields = Object.entries(data);
27
+ for (const [key, value] of fields) {
28
+ const keyBytes = textEncoder.encode(key);
29
+ totalSize += 2;
30
+ totalSize += keyBytes.length;
31
+ totalSize += 1;
32
+ totalSize += 4;
33
+ totalSize += getValueSize(value);
34
+ }
35
+ const buffer = new Uint8Array(totalSize);
36
+ const view = new DataView(buffer.buffer);
37
+ let offset = 0;
38
+ view.setUint16(offset, fields.length, false);
39
+ offset += 2;
40
+ for (const [key, value] of fields) {
41
+ const keyBytes = textEncoder.encode(key);
42
+ view.setUint16(offset, keyBytes.length, false);
43
+ offset += 2;
44
+ buffer.set(keyBytes, offset);
45
+ offset += keyBytes.length;
46
+ if (typeof value === "string") {
47
+ buffer[offset] = 1 /* String */;
48
+ offset++;
49
+ const valueBytes = textEncoder.encode(value);
50
+ view.setUint32(offset, valueBytes.length, false);
51
+ offset += 4;
52
+ buffer.set(valueBytes, offset);
53
+ offset += valueBytes.length;
54
+ } else if (typeof value === "number") {
55
+ buffer[offset] = 2 /* Number */;
56
+ offset++;
57
+ view.setUint32(offset, 8, false);
58
+ offset += 4;
59
+ view.setFloat64(offset, value, false);
60
+ offset += 8;
61
+ } else if (typeof value === "boolean") {
62
+ buffer[offset] = 3 /* Boolean */;
63
+ offset++;
64
+ view.setUint32(offset, 1, false);
65
+ offset += 4;
66
+ buffer[offset] = value ? 1 : 0;
67
+ offset++;
68
+ } else if (value instanceof Uint8Array) {
69
+ buffer[offset] = 4 /* Bytes */;
70
+ offset++;
71
+ view.setUint32(offset, value.length, false);
72
+ offset += 4;
73
+ buffer.set(value, offset);
74
+ offset += value.length;
75
+ } else {
76
+ throw new Error(`Unsupported value type for key ${key}: ${typeof value}`);
77
+ }
78
+ }
79
+ return buffer;
80
+ }
81
+ function decodeKeyValue(data) {
82
+ if (data.length < 2) {
83
+ throw new Error("Data too short for key-value encoding");
84
+ }
85
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
86
+ const result = {};
87
+ let offset = 0;
88
+ const numFields = view.getUint16(offset, false);
89
+ offset += 2;
90
+ for (let i = 0; i < numFields; i++) {
91
+ if (offset + 2 > data.length) {
92
+ throw new Error(`Invalid key length at field ${i}`);
93
+ }
94
+ const keyLen = view.getUint16(offset, false);
95
+ offset += 2;
96
+ if (offset + keyLen > data.length) {
97
+ throw new Error(`Key data truncated at field ${i}`);
98
+ }
99
+ const key = textDecoder.decode(data.slice(offset, offset + keyLen));
100
+ offset += keyLen;
101
+ if (offset + 1 > data.length) {
102
+ throw new Error(`Invalid value type at field ${i}`);
103
+ }
104
+ const valueType = data[offset];
105
+ offset++;
106
+ if (offset + 4 > data.length) {
107
+ throw new Error(`Invalid value length at field ${i}`);
108
+ }
109
+ const valueLen = view.getUint32(offset, false);
110
+ offset += 4;
111
+ if (offset + valueLen > data.length) {
112
+ throw new Error(`Value data truncated at field ${i}`);
113
+ }
114
+ const valueData = data.slice(offset, offset + valueLen);
115
+ offset += valueLen;
116
+ switch (valueType) {
117
+ case 1 /* String */:
118
+ result[key] = textDecoder.decode(valueData);
119
+ break;
120
+ case 2 /* Number */:
121
+ if (valueData.length !== 8) {
122
+ throw new Error(`Invalid number length for field ${key}`);
123
+ }
124
+ const valueView = new DataView(valueData.buffer, valueData.byteOffset);
125
+ result[key] = valueView.getFloat64(0, false);
126
+ break;
127
+ case 3 /* Boolean */:
128
+ if (valueData.length !== 1) {
129
+ throw new Error(`Invalid boolean length for field ${key}`);
130
+ }
131
+ result[key] = valueData[0] !== 0;
132
+ break;
133
+ case 4 /* Bytes */:
134
+ result[key] = valueData;
135
+ break;
136
+ default:
137
+ throw new Error(`Unknown value type 0x${valueType.toString(16)} for field ${key}`);
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+ function encodeBinaryMessage(message) {
143
+ let messageType;
144
+ let channel = "";
145
+ let msgType = "";
146
+ let data = {};
147
+ if (message.type === "subscribe") {
148
+ messageType = 1 /* Subscribe */;
149
+ channel = message.channel || "";
150
+ msgType = "subscribe";
151
+ data = {};
152
+ } else if (message.type === "unsubscribe") {
153
+ messageType = 2 /* Unsubscribe */;
154
+ channel = message.channel || "";
155
+ msgType = "unsubscribe";
156
+ data = {};
157
+ } else {
158
+ messageType = 3 /* Data */;
159
+ channel = message.channel || "";
160
+ msgType = message.type || "";
161
+ data = message.data || message;
162
+ }
163
+ const channelBytes = encodeUTF8(channel);
164
+ const msgTypeBytes = encodeUTF8(msgType);
165
+ let dataBytes;
166
+ if (data === void 0 || data === null) {
167
+ dataBytes = new Uint8Array(0);
168
+ } else if (typeof data === "string") {
169
+ dataBytes = encodeUTF8(data);
170
+ } else if (data instanceof Uint8Array) {
171
+ dataBytes = data;
172
+ } else if (typeof data === "object") {
173
+ dataBytes = encodeKeyValue(data);
174
+ } else {
175
+ throw new Error(`Unsupported data type: ${typeof data}`);
176
+ }
177
+ const totalSize = 1 + 2 + channelBytes.length + 2 + msgTypeBytes.length + 4 + dataBytes.length;
178
+ const buffer = new ArrayBuffer(totalSize);
179
+ const view = new DataView(buffer);
180
+ let offset = 0;
181
+ view.setUint8(offset, messageType);
182
+ offset += 1;
183
+ view.setUint16(offset, channelBytes.length, false);
184
+ offset += 2;
185
+ const uint8View = new Uint8Array(buffer);
186
+ uint8View.set(channelBytes, offset);
187
+ offset += channelBytes.length;
188
+ view.setUint16(offset, msgTypeBytes.length, false);
189
+ offset += 2;
190
+ uint8View.set(msgTypeBytes, offset);
191
+ offset += msgTypeBytes.length;
192
+ view.setUint32(offset, dataBytes.length, false);
193
+ offset += 4;
194
+ uint8View.set(dataBytes, offset);
195
+ return buffer;
196
+ }
197
+ function decodeBinaryMessage(buffer) {
198
+ const arrayBuffer = buffer instanceof Uint8Array ? buffer.buffer : buffer;
199
+ const view = new DataView(arrayBuffer);
200
+ const uint8View = new Uint8Array(arrayBuffer);
201
+ let offset = 0;
202
+ const messageType = view.getUint8(offset);
203
+ offset += 1;
204
+ const channelLength = view.getUint16(offset, false);
205
+ offset += 2;
206
+ const channelBytes = uint8View.slice(offset, offset + channelLength);
207
+ const channel = decodeUTF8(channelBytes);
208
+ offset += channelLength;
209
+ const msgTypeLength = view.getUint16(offset, false);
210
+ offset += 2;
211
+ const msgTypeBytes = uint8View.slice(offset, offset + msgTypeLength);
212
+ const msgType = decodeUTF8(msgTypeBytes);
213
+ offset += msgTypeLength;
214
+ const dataLength = view.getUint32(offset, false);
215
+ offset += 4;
216
+ const dataBytes = uint8View.slice(offset, offset + dataLength);
217
+ const shouldTryKeyValue = ["terminal:input", "terminal:resize", "file:changed", "terminal:output", "signal", "test"].includes(msgType);
218
+ let data;
219
+ if (dataBytes.length === 0) {
220
+ data = {};
221
+ } else if (shouldTryKeyValue) {
222
+ try {
223
+ data = decodeKeyValue(dataBytes);
224
+ } catch {
225
+ data = dataBytes;
226
+ }
227
+ } else {
228
+ data = dataBytes;
229
+ }
230
+ if (messageType === 1 /* Subscribe */ || messageType === 2 /* Unsubscribe */) {
231
+ return {
232
+ type: msgType,
233
+ channel
234
+ };
235
+ }
236
+ return {
237
+ type: msgType,
238
+ channel,
239
+ data
240
+ };
241
+ }
242
+ function encodeUTF8(str) {
243
+ if (typeof TextEncoder !== "undefined") {
244
+ const encoder = new TextEncoder();
245
+ return encoder.encode(str);
246
+ }
247
+ const utf8 = [];
248
+ for (let i = 0; i < str.length; i++) {
249
+ let charcode = str.charCodeAt(i);
250
+ if (charcode < 128) {
251
+ utf8.push(charcode);
252
+ } else if (charcode < 2048) {
253
+ utf8.push(192 | charcode >> 6, 128 | charcode & 63);
254
+ } else if (charcode < 55296 || charcode >= 57344) {
255
+ utf8.push(224 | charcode >> 12, 128 | charcode >> 6 & 63, 128 | charcode & 63);
256
+ } else {
257
+ i++;
258
+ charcode = 65536 + ((charcode & 1023) << 10 | str.charCodeAt(i) & 1023);
259
+ utf8.push(
260
+ 240 | charcode >> 18,
261
+ 128 | charcode >> 12 & 63,
262
+ 128 | charcode >> 6 & 63,
263
+ 128 | charcode & 63
264
+ );
265
+ }
266
+ }
267
+ return new Uint8Array(utf8);
268
+ }
269
+ function decodeUTF8(bytes) {
270
+ if (typeof TextDecoder !== "undefined") {
271
+ const decoder = new TextDecoder();
272
+ return decoder.decode(bytes);
273
+ }
274
+ let str = "";
275
+ let i = 0;
276
+ while (i < bytes.length) {
277
+ const c = bytes[i++];
278
+ if (c < 128) {
279
+ str += String.fromCharCode(c);
280
+ } else if (c < 224) {
281
+ str += String.fromCharCode((c & 31) << 6 | bytes[i++] & 63);
282
+ } else if (c < 240) {
283
+ str += String.fromCharCode((c & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63);
284
+ } else {
285
+ const c2 = (c & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63;
286
+ const c3 = c2 - 65536;
287
+ str += String.fromCharCode(55296 | c3 >> 10, 56320 | c3 & 1023);
288
+ }
289
+ }
290
+ return str;
291
+ }
292
+ function isBinaryData(data) {
293
+ return data instanceof ArrayBuffer || data instanceof Uint8Array || data instanceof Blob;
294
+ }
295
+ async function blobToArrayBuffer(blob) {
296
+ if (blob.arrayBuffer) {
297
+ return blob.arrayBuffer();
298
+ }
299
+ return new Promise((resolve, reject) => {
300
+ const reader = new FileReader();
301
+ reader.onload = () => resolve(reader.result);
302
+ reader.onerror = reject;
303
+ reader.readAsArrayBuffer(blob);
304
+ });
305
+ }
306
+
307
+ // src/client/websocket.ts
308
+ var WebSocketManager = class {
309
+ constructor(config) {
310
+ this.ws = null;
311
+ this.eventHandlers = /* @__PURE__ */ new Map();
312
+ this.reconnectAttempts = 0;
313
+ this.reconnectTimer = null;
314
+ this.subscribedChannels = /* @__PURE__ */ new Set();
315
+ this.isManualClose = false;
316
+ this.config = {
317
+ url: config.url,
318
+ WebSocket: config.WebSocket,
319
+ autoReconnect: config.autoReconnect ?? true,
320
+ reconnectDelay: config.reconnectDelay ?? 1e3,
321
+ maxReconnectAttempts: config.maxReconnectAttempts ?? 5,
322
+ debug: config.debug ?? false,
323
+ protocol: config.protocol ?? "binary"
324
+ };
325
+ }
326
+ // ============================================================================
327
+ // Connection Management
328
+ // ============================================================================
329
+ /**
330
+ * Connect to WebSocket server
331
+ */
332
+ connect() {
333
+ return new Promise((resolve, reject) => {
334
+ try {
335
+ this.isManualClose = false;
336
+ this.log("Connecting to WebSocket URL:", this.config.url);
337
+ this.ws = new this.config.WebSocket(this.config.url);
338
+ this.ws.onopen = () => {
339
+ this.log("Connected to WebSocket server");
340
+ this.reconnectAttempts = 0;
341
+ if (this.subscribedChannels.size > 0) {
342
+ this.log("Resubscribing to channels:", Array.from(this.subscribedChannels));
343
+ this.subscribedChannels.forEach((channel) => {
344
+ this.sendRaw({ type: "subscribe", channel });
345
+ });
346
+ }
347
+ this.emit("open");
348
+ resolve();
349
+ };
350
+ this.ws.onmessage = async (event) => {
351
+ try {
352
+ let message;
353
+ if (this.config.protocol === "binary" && isBinaryData(event.data)) {
354
+ let buffer;
355
+ if (event.data instanceof Blob) {
356
+ buffer = await blobToArrayBuffer(event.data);
357
+ } else {
358
+ buffer = event.data;
359
+ }
360
+ message = decodeBinaryMessage(buffer);
361
+ this.log("Received binary message:", message);
362
+ } else {
363
+ message = JSON.parse(event.data);
364
+ this.log("Received JSON message:", message);
365
+ }
366
+ this.handleMessage(message);
367
+ } catch (error) {
368
+ this.log("Failed to parse message:", error);
369
+ }
370
+ };
371
+ this.ws.onerror = (error) => {
372
+ this.log("WebSocket error:", error);
373
+ this.emit("error", error);
374
+ reject(error);
375
+ };
376
+ this.ws.onclose = () => {
377
+ this.log("WebSocket connection closed");
378
+ this.emit("close");
379
+ if (this.config.autoReconnect && !this.isManualClose) {
380
+ this.attemptReconnect();
381
+ }
382
+ };
383
+ } catch (error) {
384
+ reject(error);
385
+ }
386
+ });
387
+ }
388
+ /**
389
+ * Disconnect from WebSocket server
390
+ */
391
+ disconnect() {
392
+ this.isManualClose = true;
393
+ if (this.reconnectTimer) {
394
+ clearTimeout(this.reconnectTimer);
395
+ this.reconnectTimer = null;
396
+ }
397
+ if (this.ws) {
398
+ this.ws.close();
399
+ this.ws = null;
400
+ }
401
+ }
402
+ /**
403
+ * Check if WebSocket is connected
404
+ */
405
+ isConnected() {
406
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
407
+ }
408
+ /**
409
+ * Attempt to reconnect to WebSocket server
410
+ */
411
+ attemptReconnect() {
412
+ if (this.config.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.config.maxReconnectAttempts) {
413
+ this.log("Max reconnection attempts reached");
414
+ this.emit("reconnect-failed");
415
+ return;
416
+ }
417
+ this.reconnectAttempts++;
418
+ this.log(`Reconnecting... (attempt ${this.reconnectAttempts})`);
419
+ this.reconnectTimer = setTimeout(() => {
420
+ this.connect().catch((error) => {
421
+ this.log("Reconnection failed:", error);
422
+ });
423
+ }, this.config.reconnectDelay);
424
+ }
425
+ // ============================================================================
426
+ // Channel Subscription
427
+ // ============================================================================
428
+ /**
429
+ * Subscribe to a channel
430
+ * @param channel - Channel name (e.g., 'terminal:term_abc123', 'watcher:watcher_xyz789', 'signals')
431
+ */
432
+ subscribe(channel) {
433
+ this.subscribedChannels.add(channel);
434
+ this.sendRaw({ type: "subscribe", channel });
435
+ this.log("Subscribed to channel:", channel);
436
+ }
437
+ /**
438
+ * Unsubscribe from a channel
439
+ */
440
+ unsubscribe(channel) {
441
+ this.subscribedChannels.delete(channel);
442
+ this.sendRaw({ type: "unsubscribe", channel });
443
+ this.log("Unsubscribed from channel:", channel);
444
+ }
445
+ /**
446
+ * Get list of subscribed channels
447
+ */
448
+ getSubscribedChannels() {
449
+ return Array.from(this.subscribedChannels);
450
+ }
451
+ // ============================================================================
452
+ // Message Sending
453
+ // ============================================================================
454
+ /**
455
+ * Send raw message to server
456
+ */
457
+ sendRaw(message) {
458
+ if (!this.isConnected()) {
459
+ throw new Error("WebSocket is not connected");
460
+ }
461
+ if (this.config.protocol === "binary") {
462
+ const buffer = encodeBinaryMessage(message);
463
+ this.ws.send(buffer);
464
+ this.log("Sent binary message:", message);
465
+ } else {
466
+ this.ws.send(JSON.stringify(message));
467
+ this.log("Sent JSON message:", message);
468
+ }
469
+ }
470
+ /**
471
+ * Send input to a terminal (sent as-is, not encoded)
472
+ */
473
+ sendTerminalInput(terminalId, input) {
474
+ this.sendRaw({
475
+ type: "terminal:input",
476
+ data: { terminal_id: terminalId, input }
477
+ });
478
+ }
479
+ /**
480
+ * Resize terminal window
481
+ */
482
+ resizeTerminal(terminalId, cols, rows) {
483
+ this.sendRaw({
484
+ type: "terminal:resize",
485
+ data: { terminal_id: terminalId, cols, rows }
486
+ });
487
+ }
488
+ /**
489
+ * Start a pending streaming command
490
+ * Used in two-phase streaming flow: HTTP request creates pending command,
491
+ * then this signal triggers execution after client has subscribed.
492
+ */
493
+ startCommand(cmdId) {
494
+ this.sendRaw({
495
+ type: "command:start",
496
+ data: { cmd_id: cmdId }
497
+ });
498
+ }
499
+ on(event, handler) {
500
+ if (!this.eventHandlers.has(event)) {
501
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
502
+ }
503
+ this.eventHandlers.get(event).add(handler);
504
+ }
505
+ /**
506
+ * Unregister event handler
507
+ */
508
+ off(event, handler) {
509
+ const handlers = this.eventHandlers.get(event);
510
+ if (handlers) {
511
+ handlers.delete(handler);
512
+ if (handlers.size === 0) {
513
+ this.eventHandlers.delete(event);
514
+ }
515
+ }
516
+ }
517
+ /**
518
+ * Unregister all event handlers for an event
519
+ */
520
+ offAll(event) {
521
+ this.eventHandlers.delete(event);
522
+ }
523
+ /**
524
+ * Emit event to registered handlers
525
+ */
526
+ emit(event, data) {
527
+ const handlers = this.eventHandlers.get(event);
528
+ if (handlers) {
529
+ handlers.forEach((handler) => {
530
+ try {
531
+ handler(data);
532
+ } catch (error) {
533
+ this.log("Error in event handler:", error);
534
+ }
535
+ });
536
+ }
537
+ }
538
+ /**
539
+ * Handle incoming message
540
+ */
541
+ handleMessage(message) {
542
+ this.emit(message.type, message);
543
+ if ("channel" in message && message.channel) {
544
+ this.emit(message.channel, message);
545
+ }
546
+ }
547
+ // ============================================================================
548
+ // Utility Methods
549
+ // ============================================================================
550
+ /**
551
+ * Log debug message if debug mode is enabled
552
+ */
553
+ log(...args) {
554
+ if (this.config.debug) {
555
+ console.log("[WebSocketManager]", ...args);
556
+ }
557
+ }
558
+ /**
559
+ * Get current connection state
560
+ */
561
+ getState() {
562
+ if (!this.ws) return "closed";
563
+ switch (this.ws.readyState) {
564
+ case WebSocket.CONNECTING:
565
+ return "connecting";
566
+ case WebSocket.OPEN:
567
+ return "open";
568
+ case WebSocket.CLOSING:
569
+ return "closing";
570
+ case WebSocket.CLOSED:
571
+ return "closed";
572
+ default:
573
+ return "closed";
574
+ }
575
+ }
576
+ /**
577
+ * Get reconnection attempt count
578
+ */
579
+ getReconnectAttempts() {
580
+ return this.reconnectAttempts;
581
+ }
582
+ };
583
+
584
+ // src/client/resources/command.ts
585
+ var Command = class {
586
+ constructor(data) {
587
+ this.id = data.cmdId;
588
+ this.terminalId = data.terminalId;
589
+ this.command = data.command;
590
+ this._status = data.status;
591
+ this._stdout = data.stdout;
592
+ this._stderr = data.stderr;
593
+ this._exitCode = data.exitCode;
594
+ this._durationMs = data.durationMs;
595
+ this._startedAt = data.startedAt;
596
+ this._finishedAt = data.finishedAt;
597
+ }
598
+ get status() {
599
+ return this._status;
600
+ }
601
+ get stdout() {
602
+ return this._stdout;
603
+ }
604
+ get stderr() {
605
+ return this._stderr;
606
+ }
607
+ get exitCode() {
608
+ return this._exitCode;
609
+ }
610
+ get durationMs() {
611
+ return this._durationMs;
612
+ }
613
+ get startedAt() {
614
+ return this._startedAt;
615
+ }
616
+ get finishedAt() {
617
+ return this._finishedAt;
618
+ }
619
+ /**
620
+ * Set the wait handler (called by TerminalCommands)
621
+ * @internal
622
+ */
623
+ setWaitHandler(handler) {
624
+ this.waitHandler = handler;
625
+ }
626
+ /**
627
+ * Set the retrieve handler (called by TerminalCommands)
628
+ * @internal
629
+ */
630
+ setRetrieveHandler(handler) {
631
+ this.retrieveHandler = handler;
632
+ }
633
+ /**
634
+ * Wait for the command to complete
635
+ * @param timeout - Optional timeout in seconds (0 = no timeout)
636
+ * @returns This command with updated status
637
+ */
638
+ async wait(timeout) {
639
+ if (!this.waitHandler) {
640
+ throw new Error("Wait handler not set");
641
+ }
642
+ const response = await this.waitHandler(timeout);
643
+ this.updateFromResponse(response);
644
+ return this;
645
+ }
646
+ /**
647
+ * Refresh the command status from the server
648
+ * @returns This command with updated status
649
+ */
650
+ async refresh() {
651
+ if (!this.retrieveHandler) {
652
+ throw new Error("Retrieve handler not set");
653
+ }
654
+ const response = await this.retrieveHandler();
655
+ this.updateFromResponse(response);
656
+ return this;
657
+ }
658
+ /**
659
+ * Update internal state from API response
660
+ */
661
+ updateFromResponse(response) {
662
+ this._status = response.data.status;
663
+ this._stdout = response.data.stdout;
664
+ this._stderr = response.data.stderr;
665
+ this._exitCode = response.data.exit_code;
666
+ this._durationMs = response.data.duration_ms;
667
+ this._finishedAt = response.data.finished_at;
668
+ }
669
+ };
670
+
671
+ // src/client/resources/terminal-command.ts
672
+ var TerminalCommand = class {
673
+ constructor(terminalId, handlers) {
674
+ this.terminalId = terminalId;
675
+ this.runHandler = handlers.run;
676
+ this.listHandler = handlers.list;
677
+ this.retrieveHandler = handlers.retrieve;
678
+ this.waitHandler = handlers.wait;
679
+ }
680
+ /**
681
+ * Run a command in the terminal
682
+ * @param command - The command to execute
683
+ * @param options - Execution options
684
+ * @param options.background - If true, returns immediately without waiting for completion
685
+ * @returns Command object with results or status
686
+ */
687
+ async run(command, options) {
688
+ const response = await this.runHandler(command, options?.background);
689
+ const cmd = new Command({
690
+ cmdId: response.data.cmd_id || "",
691
+ terminalId: this.terminalId,
692
+ command: response.data.command,
693
+ status: response.data.status || (options?.background ? "running" : "completed"),
694
+ stdout: response.data.stdout,
695
+ stderr: response.data.stderr,
696
+ exitCode: response.data.exit_code,
697
+ durationMs: response.data.duration_ms,
698
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
699
+ });
700
+ cmd.setWaitHandler((timeout) => this.waitHandler(cmd.id, timeout));
701
+ cmd.setRetrieveHandler(() => this.retrieveHandler(cmd.id));
702
+ return cmd;
703
+ }
704
+ /**
705
+ * List all commands executed in this terminal
706
+ * @returns Array of Command objects
707
+ */
708
+ async list() {
709
+ const response = await this.listHandler();
710
+ return response.data.commands.map((item) => {
711
+ const cmd = new Command({
712
+ cmdId: item.cmd_id,
713
+ terminalId: this.terminalId,
714
+ command: item.command,
715
+ status: item.status,
716
+ stdout: "",
717
+ // Not included in list response
718
+ stderr: "",
719
+ // Not included in list response
720
+ exitCode: item.exit_code,
721
+ durationMs: item.duration_ms,
722
+ startedAt: item.started_at,
723
+ finishedAt: item.finished_at
724
+ });
725
+ cmd.setWaitHandler((timeout) => this.waitHandler(cmd.id, timeout));
726
+ cmd.setRetrieveHandler(() => this.retrieveHandler(cmd.id));
727
+ return cmd;
728
+ });
729
+ }
730
+ /**
731
+ * Retrieve a specific command by ID
732
+ * @param cmdId - The command ID
733
+ * @returns Command object with full details
734
+ */
735
+ async retrieve(cmdId) {
736
+ const response = await this.retrieveHandler(cmdId);
737
+ const cmd = new Command({
738
+ cmdId: response.data.cmd_id,
739
+ terminalId: this.terminalId,
740
+ command: response.data.command,
741
+ status: response.data.status,
742
+ stdout: response.data.stdout,
743
+ stderr: response.data.stderr,
744
+ exitCode: response.data.exit_code,
745
+ durationMs: response.data.duration_ms,
746
+ startedAt: response.data.started_at,
747
+ finishedAt: response.data.finished_at
748
+ });
749
+ cmd.setWaitHandler((timeout) => this.waitHandler(cmd.id, timeout));
750
+ cmd.setRetrieveHandler(() => this.retrieveHandler(cmd.id));
751
+ return cmd;
752
+ }
753
+ };
754
+
755
+ // src/client/terminal.ts
756
+ function decodeBase64(str) {
757
+ if (typeof window !== "undefined" && typeof window.atob === "function") {
758
+ return window.atob(str);
759
+ } else if (typeof Buffer !== "undefined") {
760
+ return Buffer.from(str, "base64").toString("utf-8");
761
+ }
762
+ throw new Error("No base64 decoding available");
763
+ }
764
+ var TerminalInstance = class {
765
+ constructor(id, pty, status, channel, ws, encoding = "raw") {
766
+ this._eventHandlers = /* @__PURE__ */ new Map();
767
+ this._id = id;
768
+ this._pty = pty;
769
+ this._status = status === "active" ? "running" : status;
770
+ this._channel = channel;
771
+ this._ws = ws;
772
+ this._encoding = encoding;
773
+ this.command = new TerminalCommand(id, {
774
+ run: async (command, background) => {
775
+ if (!this._executeHandler) {
776
+ throw new Error("Execute handler not set");
777
+ }
778
+ return this._executeHandler(command, background);
779
+ },
780
+ list: async () => {
781
+ if (!this._listCommandsHandler) {
782
+ throw new Error("List commands handler not set");
783
+ }
784
+ return this._listCommandsHandler();
785
+ },
786
+ retrieve: async (cmdId) => {
787
+ if (!this._retrieveCommandHandler) {
788
+ throw new Error("Retrieve command handler not set");
789
+ }
790
+ return this._retrieveCommandHandler(cmdId);
791
+ },
792
+ wait: async (cmdId, timeout) => {
793
+ if (!this._waitCommandHandler) {
794
+ throw new Error("Wait command handler not set");
795
+ }
796
+ return this._waitCommandHandler(cmdId, timeout);
797
+ }
798
+ });
799
+ if (this._pty && this._ws && this._channel) {
800
+ this._ws.subscribe(this._channel);
801
+ this.setupWebSocketHandlers();
802
+ }
803
+ }
804
+ /**
805
+ * Set up WebSocket event handlers (PTY mode only)
806
+ */
807
+ setupWebSocketHandlers() {
808
+ if (!this._ws || !this._channel) {
809
+ return;
810
+ }
811
+ this._ws.on("terminal:output", (msg) => {
812
+ if (msg.channel === this._channel) {
813
+ const encoding = msg.data.encoding || this._encoding;
814
+ const output = encoding === "base64" ? decodeBase64(msg.data.output) : msg.data.output;
815
+ this.emit("output", output);
816
+ }
817
+ });
818
+ this._ws.on("terminal:error", (msg) => {
819
+ if (msg.channel === this._channel) {
820
+ this.emit("error", msg.data.error);
821
+ }
822
+ });
823
+ this._ws.on("terminal:destroyed", (msg) => {
824
+ if (msg.channel === this._channel) {
825
+ this._status = "stopped";
826
+ this.emit("destroyed");
827
+ this.cleanup();
828
+ }
829
+ });
830
+ }
831
+ /**
832
+ * Terminal ID
833
+ */
834
+ get id() {
835
+ return this._id;
836
+ }
837
+ /**
838
+ * Get terminal ID (deprecated, use .id property)
839
+ * @deprecated Use .id property instead
840
+ */
841
+ getId() {
842
+ return this._id;
843
+ }
844
+ /**
845
+ * Terminal status
846
+ */
847
+ get status() {
848
+ return this._status;
849
+ }
850
+ /**
851
+ * Get terminal status (deprecated, use .status property)
852
+ * @deprecated Use .status property instead
853
+ */
854
+ getStatus() {
855
+ return this._status;
856
+ }
857
+ /**
858
+ * Terminal channel (null for exec mode)
859
+ */
860
+ get channel() {
861
+ return this._channel;
862
+ }
863
+ /**
864
+ * Get terminal channel (deprecated, use .channel property)
865
+ * @deprecated Use .channel property instead
866
+ */
867
+ getChannel() {
868
+ return this._channel;
869
+ }
870
+ /**
871
+ * Whether this is a PTY terminal
872
+ */
873
+ get pty() {
874
+ return this._pty;
875
+ }
876
+ /**
877
+ * Get terminal PTY mode (deprecated, use .pty property)
878
+ * @deprecated Use .pty property instead
879
+ */
880
+ isPTY() {
881
+ return this._pty;
882
+ }
883
+ /**
884
+ * Check if terminal is running
885
+ */
886
+ isRunning() {
887
+ return this._status === "running";
888
+ }
889
+ /**
890
+ * Write input to the terminal (PTY mode only)
891
+ */
892
+ write(input) {
893
+ if (!this._pty) {
894
+ throw new Error("write() is only available for PTY terminals. Use commands.run() for exec mode terminals.");
895
+ }
896
+ if (!this._ws) {
897
+ throw new Error("WebSocket not available");
898
+ }
899
+ if (!this.isRunning()) {
900
+ console.warn('[Terminal] Warning: Terminal status is not "running", but attempting to write anyway. Status:', this._status);
901
+ }
902
+ this._ws.sendTerminalInput(this._id, input);
903
+ }
904
+ /**
905
+ * Resize terminal window (PTY mode only)
906
+ */
907
+ resize(cols, rows) {
908
+ if (!this._pty) {
909
+ throw new Error("resize() is only available for PTY terminals");
910
+ }
911
+ if (!this._ws) {
912
+ throw new Error("WebSocket not available");
913
+ }
914
+ if (!this.isRunning()) {
915
+ throw new Error("Terminal is not running");
916
+ }
917
+ this._ws.resizeTerminal(this._id, cols, rows);
918
+ }
919
+ /**
920
+ * Set execute command handler (called by Sandbox)
921
+ * @internal
922
+ */
923
+ setExecuteHandler(handler) {
924
+ this._executeHandler = handler;
925
+ }
926
+ /**
927
+ * Set list commands handler (called by Sandbox)
928
+ * @internal
929
+ */
930
+ setListCommandsHandler(handler) {
931
+ this._listCommandsHandler = handler;
932
+ }
933
+ /**
934
+ * Set retrieve command handler (called by Sandbox)
935
+ * @internal
936
+ */
937
+ setRetrieveCommandHandler(handler) {
938
+ this._retrieveCommandHandler = handler;
939
+ }
940
+ /**
941
+ * Set wait command handler (called by Sandbox)
942
+ * @internal
943
+ */
944
+ setWaitCommandHandler(handler) {
945
+ this._waitCommandHandler = handler;
946
+ }
947
+ /**
948
+ * Set destroy handler (called by Sandbox)
949
+ * @internal
950
+ */
951
+ setDestroyHandler(handler) {
952
+ this._destroyHandler = handler;
953
+ }
954
+ /**
955
+ * Execute a command in the terminal (deprecated, use command.run())
956
+ * @deprecated Use terminal.command.run() instead
957
+ */
958
+ async execute(command, options) {
959
+ if (!this._executeHandler) {
960
+ throw new Error("Execute handler not set");
961
+ }
962
+ return this._executeHandler(command, options?.background);
963
+ }
964
+ /**
965
+ * Destroy the terminal
966
+ */
967
+ async destroy() {
968
+ if (!this._destroyHandler) {
969
+ throw new Error("Destroy handler not set");
970
+ }
971
+ await this._destroyHandler();
972
+ this._status = "stopped";
973
+ this.cleanup();
974
+ }
975
+ /**
976
+ * Clean up resources
977
+ */
978
+ cleanup() {
979
+ if (this._ws && this._channel) {
980
+ this._ws.unsubscribe(this._channel);
981
+ }
982
+ this._eventHandlers.clear();
983
+ }
984
+ /**
985
+ * Register event handler
986
+ */
987
+ on(event, handler) {
988
+ if (!this._eventHandlers.has(event)) {
989
+ this._eventHandlers.set(event, /* @__PURE__ */ new Set());
990
+ }
991
+ this._eventHandlers.get(event).add(handler);
992
+ }
993
+ /**
994
+ * Unregister event handler
995
+ */
996
+ off(event, handler) {
997
+ const handlers = this._eventHandlers.get(event);
998
+ if (handlers) {
999
+ handlers.delete(handler);
1000
+ if (handlers.size === 0) {
1001
+ this._eventHandlers.delete(event);
1002
+ }
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Emit event to registered handlers
1007
+ */
1008
+ emit(event, ...args) {
1009
+ const handlers = this._eventHandlers.get(event);
1010
+ if (handlers) {
1011
+ handlers.forEach((handler) => {
1012
+ try {
1013
+ handler(...args);
1014
+ } catch (error) {
1015
+ console.error("Error in terminal event handler:", error);
1016
+ }
1017
+ });
1018
+ }
1019
+ }
1020
+ };
7
1021
 
8
- // src/errors.ts
9
- var ComputeError = class extends Error {
1022
+ // src/client/file-watcher.ts
1023
+ function decodeBase642(str) {
1024
+ if (typeof window !== "undefined" && typeof window.atob === "function") {
1025
+ return window.atob(str);
1026
+ } else if (typeof Buffer !== "undefined") {
1027
+ return Buffer.from(str, "base64").toString("utf-8");
1028
+ }
1029
+ throw new Error("No base64 decoding available");
1030
+ }
1031
+ var FileWatcher = class {
1032
+ constructor(id, path, status, channel, includeContent, ignored, ws, encoding = "raw") {
1033
+ this.eventHandlers = /* @__PURE__ */ new Map();
1034
+ this.id = id;
1035
+ this.path = path;
1036
+ this.status = status;
1037
+ this.channel = channel;
1038
+ this.includeContent = includeContent;
1039
+ this.ignored = ignored;
1040
+ this.encoding = encoding;
1041
+ this.ws = ws;
1042
+ this.ws.subscribe(this.channel);
1043
+ this.setupWebSocketHandlers();
1044
+ }
1045
+ /**
1046
+ * Set up WebSocket event handlers
1047
+ */
1048
+ setupWebSocketHandlers() {
1049
+ this.ws.on("file:changed", (msg) => {
1050
+ if (msg.channel === this.channel) {
1051
+ const encoding = msg.data.encoding || this.encoding;
1052
+ const content = msg.data.content && encoding === "base64" ? decodeBase642(msg.data.content) : msg.data.content;
1053
+ this.emit("change", {
1054
+ event: msg.data.event,
1055
+ path: msg.data.path,
1056
+ content
1057
+ });
1058
+ }
1059
+ });
1060
+ this.ws.on("watcher:destroyed", (msg) => {
1061
+ if (msg.channel === this.channel) {
1062
+ this.status = "stopped";
1063
+ this.emit("destroyed");
1064
+ this.cleanup();
1065
+ }
1066
+ });
1067
+ }
1068
+ /**
1069
+ * Get watcher ID
1070
+ */
1071
+ getId() {
1072
+ return this.id;
1073
+ }
1074
+ /**
1075
+ * Get watched path
1076
+ */
1077
+ getPath() {
1078
+ return this.path;
1079
+ }
1080
+ /**
1081
+ * Get watcher status
1082
+ */
1083
+ getStatus() {
1084
+ return this.status;
1085
+ }
1086
+ /**
1087
+ * Get watcher channel
1088
+ */
1089
+ getChannel() {
1090
+ return this.channel;
1091
+ }
1092
+ /**
1093
+ * Check if content is included in events
1094
+ */
1095
+ isIncludingContent() {
1096
+ return this.includeContent;
1097
+ }
1098
+ /**
1099
+ * Get ignored patterns
1100
+ */
1101
+ getIgnoredPatterns() {
1102
+ return [...this.ignored];
1103
+ }
1104
+ /**
1105
+ * Check if watcher is active
1106
+ */
1107
+ isActive() {
1108
+ return this.status === "active";
1109
+ }
1110
+ /**
1111
+ * Set destroy handler (called by client)
1112
+ */
1113
+ setDestroyHandler(handler) {
1114
+ this.destroyWatcher = handler;
1115
+ }
1116
+ /**
1117
+ * Destroy the watcher
1118
+ */
1119
+ async destroy() {
1120
+ if (!this.destroyWatcher) {
1121
+ throw new Error("Destroy handler not set");
1122
+ }
1123
+ await this.destroyWatcher();
1124
+ this.cleanup();
1125
+ }
1126
+ /**
1127
+ * Clean up resources
1128
+ */
1129
+ cleanup() {
1130
+ this.ws.unsubscribe(this.channel);
1131
+ this.eventHandlers.clear();
1132
+ }
1133
+ /**
1134
+ * Register event handler
1135
+ */
1136
+ on(event, handler) {
1137
+ if (!this.eventHandlers.has(event)) {
1138
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
1139
+ }
1140
+ this.eventHandlers.get(event).add(handler);
1141
+ }
1142
+ /**
1143
+ * Unregister event handler
1144
+ */
1145
+ off(event, handler) {
1146
+ const handlers = this.eventHandlers.get(event);
1147
+ if (handlers) {
1148
+ handlers.delete(handler);
1149
+ if (handlers.size === 0) {
1150
+ this.eventHandlers.delete(event);
1151
+ }
1152
+ }
1153
+ }
1154
+ /**
1155
+ * Emit event to registered handlers
1156
+ */
1157
+ emit(event, ...args) {
1158
+ const handlers = this.eventHandlers.get(event);
1159
+ if (handlers) {
1160
+ handlers.forEach((handler) => {
1161
+ try {
1162
+ handler(...args);
1163
+ } catch (error) {
1164
+ console.error("Error in file watcher event handler:", error);
1165
+ }
1166
+ });
1167
+ }
1168
+ }
1169
+ };
1170
+
1171
+ // src/client/signal-service.ts
1172
+ var SignalService = class {
1173
+ constructor(status, channel, ws) {
1174
+ this.eventHandlers = /* @__PURE__ */ new Map();
1175
+ this.status = status;
1176
+ this.channel = channel;
1177
+ this.ws = ws;
1178
+ this.ws.subscribe(this.channel);
1179
+ this.setupWebSocketHandlers();
1180
+ }
1181
+ /**
1182
+ * Set up WebSocket event handlers
1183
+ */
1184
+ setupWebSocketHandlers() {
1185
+ this.ws.on("signal", (msg) => {
1186
+ if (msg.channel === this.channel) {
1187
+ const event = {
1188
+ signal: msg.data.signal,
1189
+ ...msg.data.port && { port: msg.data.port },
1190
+ ...msg.data.url && { url: msg.data.url },
1191
+ ...msg.data.message && { message: msg.data.message }
1192
+ };
1193
+ if (msg.data.signal === "port" || msg.data.signal === "server-ready") {
1194
+ this.emit("port", event);
1195
+ } else if (msg.data.signal === "error") {
1196
+ this.emit("error", event);
1197
+ }
1198
+ this.emit("signal", event);
1199
+ }
1200
+ });
1201
+ }
1202
+ /**
1203
+ * Get service status
1204
+ */
1205
+ getStatus() {
1206
+ return this.status;
1207
+ }
1208
+ /**
1209
+ * Get service channel
1210
+ */
1211
+ getChannel() {
1212
+ return this.channel;
1213
+ }
1214
+ /**
1215
+ * Check if service is active
1216
+ */
1217
+ isActive() {
1218
+ return this.status === "active";
1219
+ }
1220
+ /**
1221
+ * Set stop handler (called by client)
1222
+ */
1223
+ setStopHandler(handler) {
1224
+ this.stopService = handler;
1225
+ }
1226
+ /**
1227
+ * Stop the signal service
1228
+ */
1229
+ async stop() {
1230
+ if (!this.stopService) {
1231
+ throw new Error("Stop handler not set");
1232
+ }
1233
+ await this.stopService();
1234
+ this.cleanup();
1235
+ }
1236
+ /**
1237
+ * Clean up resources
1238
+ */
1239
+ cleanup() {
1240
+ this.status = "stopped";
1241
+ this.ws.unsubscribe(this.channel);
1242
+ this.eventHandlers.clear();
1243
+ }
1244
+ /**
1245
+ * Register event handler
1246
+ */
1247
+ on(event, handler) {
1248
+ if (!this.eventHandlers.has(event)) {
1249
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
1250
+ }
1251
+ this.eventHandlers.get(event).add(handler);
1252
+ }
1253
+ /**
1254
+ * Unregister event handler
1255
+ */
1256
+ off(event, handler) {
1257
+ const handlers = this.eventHandlers.get(event);
1258
+ if (handlers) {
1259
+ handlers.delete(handler);
1260
+ if (handlers.size === 0) {
1261
+ this.eventHandlers.delete(event);
1262
+ }
1263
+ }
1264
+ }
1265
+ /**
1266
+ * Emit event to registered handlers
1267
+ */
1268
+ emit(event, ...args) {
1269
+ const handlers = this.eventHandlers.get(event);
1270
+ if (handlers) {
1271
+ handlers.forEach((handler) => {
1272
+ try {
1273
+ handler(...args);
1274
+ } catch (error) {
1275
+ console.error("Error in signal service event handler:", error);
1276
+ }
1277
+ });
1278
+ }
1279
+ }
1280
+ };
1281
+
1282
+ // src/client/index.ts
1283
+ import { escapeArgs, mkdir, test } from "@computesdk/cmd";
1284
+
1285
+ // src/client/resources/terminal.ts
1286
+ var Terminal = class {
1287
+ constructor(handlers) {
1288
+ this.createHandler = handlers.create;
1289
+ this.listHandler = handlers.list;
1290
+ this.retrieveHandler = handlers.retrieve;
1291
+ this.destroyHandler = handlers.destroy;
1292
+ }
1293
+ /**
1294
+ * Create a new terminal session
1295
+ *
1296
+ * @param options - Terminal creation options
1297
+ * @param options.shell - Shell to use (e.g., '/bin/bash') - PTY mode only
1298
+ * @param options.encoding - Encoding: 'raw' (default) or 'base64' (binary-safe)
1299
+ * @param options.pty - Terminal mode: true = PTY (interactive), false = exec (command tracking)
1300
+ * @returns TerminalInstance
1301
+ */
1302
+ async create(options) {
1303
+ return this.createHandler(options);
1304
+ }
1305
+ /**
1306
+ * List all active terminals
1307
+ * @returns Array of terminal responses
1308
+ */
1309
+ async list() {
1310
+ return this.listHandler();
1311
+ }
1312
+ /**
1313
+ * Retrieve a specific terminal by ID
1314
+ * @param id - The terminal ID
1315
+ * @returns Terminal instance
1316
+ */
1317
+ async retrieve(id) {
1318
+ return this.retrieveHandler(id);
1319
+ }
1320
+ /**
1321
+ * Destroy a terminal by ID
1322
+ * @param id - The terminal ID
1323
+ */
1324
+ async destroy(id) {
1325
+ return this.destroyHandler(id);
1326
+ }
1327
+ };
1328
+
1329
+ // src/client/resources/server.ts
1330
+ var Server = class {
1331
+ constructor(handlers) {
1332
+ this.startHandler = handlers.start;
1333
+ this.listHandler = handlers.list;
1334
+ this.retrieveHandler = handlers.retrieve;
1335
+ this.stopHandler = handlers.stop;
1336
+ this.deleteHandler = handlers.delete;
1337
+ this.restartHandler = handlers.restart;
1338
+ this.updateStatusHandler = handlers.updateStatus;
1339
+ this.logsHandler = handlers.logs;
1340
+ }
1341
+ /**
1342
+ * Start a new managed server with optional supervisor settings
1343
+ *
1344
+ * **Install Phase:**
1345
+ * If `install` is provided, it runs blocking before `start` (e.g., "npm install").
1346
+ * The server status will be `installing` during this phase.
1347
+ *
1348
+ * **Restart Policies:**
1349
+ * - `never` (default): No automatic restart on exit
1350
+ * - `on-failure`: Restart only on non-zero exit code
1351
+ * - `always`: Always restart on exit (including exit code 0)
1352
+ *
1353
+ * **Graceful Shutdown:**
1354
+ * When stopping a server, it first sends SIGTERM and waits for `stop_timeout_ms`
1355
+ * before sending SIGKILL if the process hasn't exited.
1356
+ *
1357
+ * @param options - Server configuration
1358
+ * @returns Server info
1359
+ *
1360
+ * @example
1361
+ * ```typescript
1362
+ * // Basic server
1363
+ * const server = await sandbox.server.start({
1364
+ * slug: 'web',
1365
+ * start: 'npm run dev',
1366
+ * path: '/app',
1367
+ * });
1368
+ *
1369
+ * // With install command
1370
+ * const server = await sandbox.server.start({
1371
+ * slug: 'api',
1372
+ * install: 'npm install',
1373
+ * start: 'node server.js',
1374
+ * environment: { NODE_ENV: 'production' },
1375
+ * restart_policy: 'always',
1376
+ * max_restarts: 0, // unlimited
1377
+ * });
1378
+ * ```
1379
+ */
1380
+ async start(options) {
1381
+ const response = await this.startHandler(options);
1382
+ return response.data.server;
1383
+ }
1384
+ /**
1385
+ * List all managed servers
1386
+ * @returns Array of server info
1387
+ */
1388
+ async list() {
1389
+ const response = await this.listHandler();
1390
+ return response.data.servers;
1391
+ }
1392
+ /**
1393
+ * Retrieve a specific server by slug
1394
+ * @param slug - The server slug
1395
+ * @returns Server info
1396
+ */
1397
+ async retrieve(slug) {
1398
+ const response = await this.retrieveHandler(slug);
1399
+ return response.data.server;
1400
+ }
1401
+ /**
1402
+ * Stop a server by slug (non-destructive)
1403
+ * @param slug - The server slug
1404
+ */
1405
+ async stop(slug) {
1406
+ await this.stopHandler(slug);
1407
+ }
1408
+ /**
1409
+ * Delete a server config by slug (stops + removes persistence)
1410
+ * @param slug - The server slug
1411
+ */
1412
+ async delete(slug) {
1413
+ await this.deleteHandler(slug);
1414
+ }
1415
+ /**
1416
+ * Restart a server by slug
1417
+ * @param slug - The server slug
1418
+ * @returns Server info
1419
+ */
1420
+ async restart(slug) {
1421
+ const response = await this.restartHandler(slug);
1422
+ return response.data.server;
1423
+ }
1424
+ /**
1425
+ * Update server status (internal use)
1426
+ * @param slug - The server slug
1427
+ * @param status - New status
1428
+ */
1429
+ async updateStatus(slug, status) {
1430
+ await this.updateStatusHandler(slug, status);
1431
+ }
1432
+ /**
1433
+ * Retrieve captured output (logs) for a managed server
1434
+ * @param slug - The server slug
1435
+ * @param options - Options for log retrieval
1436
+ * @returns Server logs info
1437
+ *
1438
+ * @example
1439
+ * ```typescript
1440
+ * // Get combined logs (default)
1441
+ * const logs = await sandbox.server.logs('api');
1442
+ * console.log(logs.logs);
1443
+ *
1444
+ * // Get only stdout
1445
+ * const stdout = await sandbox.server.logs('api', { stream: 'stdout' });
1446
+ *
1447
+ * // Get only stderr
1448
+ * const stderr = await sandbox.server.logs('api', { stream: 'stderr' });
1449
+ * ```
1450
+ */
1451
+ async logs(slug, options) {
1452
+ const response = await this.logsHandler(slug, options);
1453
+ return response.data;
1454
+ }
1455
+ };
1456
+
1457
+ // src/client/resources/watcher.ts
1458
+ var Watcher = class {
1459
+ constructor(handlers) {
1460
+ this.createHandler = handlers.create;
1461
+ this.listHandler = handlers.list;
1462
+ this.retrieveHandler = handlers.retrieve;
1463
+ this.destroyHandler = handlers.destroy;
1464
+ }
1465
+ /**
1466
+ * Create a new file watcher
1467
+ * @param path - Path to watch
1468
+ * @param options - Watcher options
1469
+ * @param options.includeContent - Include file content in change events
1470
+ * @param options.ignored - Patterns to ignore
1471
+ * @param options.encoding - Encoding: 'raw' (default) or 'base64' (binary-safe)
1472
+ * @returns FileWatcher instance
1473
+ */
1474
+ async create(path, options) {
1475
+ return this.createHandler(path, options);
1476
+ }
1477
+ /**
1478
+ * List all active file watchers
1479
+ * @returns Array of watcher info
1480
+ */
1481
+ async list() {
1482
+ const response = await this.listHandler();
1483
+ return response.data.watchers;
1484
+ }
1485
+ /**
1486
+ * Retrieve a specific watcher by ID
1487
+ * @param id - The watcher ID
1488
+ * @returns Watcher info
1489
+ */
1490
+ async retrieve(id) {
1491
+ const response = await this.retrieveHandler(id);
1492
+ return response.data;
1493
+ }
1494
+ /**
1495
+ * Destroy a watcher by ID
1496
+ * @param id - The watcher ID
1497
+ */
1498
+ async destroy(id) {
1499
+ return this.destroyHandler(id);
1500
+ }
1501
+ };
1502
+
1503
+ // src/client/resources/session-token.ts
1504
+ var SessionToken = class {
1505
+ constructor(handlers) {
1506
+ this.createHandler = handlers.create;
1507
+ this.listHandler = handlers.list;
1508
+ this.retrieveHandler = handlers.retrieve;
1509
+ this.revokeHandler = handlers.revoke;
1510
+ }
1511
+ /**
1512
+ * Create a new session token (requires access token)
1513
+ * @param options - Token configuration
1514
+ * @param options.description - Description for the token
1515
+ * @param options.expiresIn - Expiration time in seconds (default: 7 days)
1516
+ * @returns Session token info including the token value
1517
+ */
1518
+ async create(options) {
1519
+ const response = await this.createHandler(options);
1520
+ return {
1521
+ id: response.id,
1522
+ token: response.token,
1523
+ description: response.description,
1524
+ createdAt: response.createdAt,
1525
+ expiresAt: response.expiresAt
1526
+ };
1527
+ }
1528
+ /**
1529
+ * List all session tokens
1530
+ * @returns Array of session token info
1531
+ */
1532
+ async list() {
1533
+ const response = await this.listHandler();
1534
+ return response.data.tokens.map((t) => ({
1535
+ id: t.id,
1536
+ description: t.description,
1537
+ createdAt: t.created_at,
1538
+ expiresAt: t.expires_at,
1539
+ lastUsedAt: t.last_used_at
1540
+ }));
1541
+ }
1542
+ /**
1543
+ * Retrieve a specific session token by ID
1544
+ * @param id - The token ID
1545
+ * @returns Session token info
1546
+ */
1547
+ async retrieve(id) {
1548
+ const response = await this.retrieveHandler(id);
1549
+ return {
1550
+ id: response.id,
1551
+ description: response.description,
1552
+ createdAt: response.createdAt,
1553
+ expiresAt: response.expiresAt
1554
+ };
1555
+ }
1556
+ /**
1557
+ * Revoke a session token
1558
+ * @param id - The token ID to revoke
1559
+ */
1560
+ async revoke(id) {
1561
+ return this.revokeHandler(id);
1562
+ }
1563
+ };
1564
+
1565
+ // src/client/resources/magic-link.ts
1566
+ var MagicLink = class {
1567
+ constructor(handlers) {
1568
+ this.createHandler = handlers.create;
1569
+ }
1570
+ /**
1571
+ * Create a magic link for browser authentication (requires access token)
1572
+ *
1573
+ * Magic links are one-time URLs that automatically create a session token
1574
+ * and set it as a cookie in the user's browser.
1575
+ *
1576
+ * @param options - Magic link configuration
1577
+ * @param options.redirectUrl - URL to redirect to after authentication
1578
+ * @returns Magic link info including the URL
1579
+ */
1580
+ async create(options) {
1581
+ const response = await this.createHandler(options);
1582
+ return {
1583
+ url: response.data.magic_url,
1584
+ expiresAt: response.data.expires_at,
1585
+ redirectUrl: response.data.redirect_url
1586
+ };
1587
+ }
1588
+ };
1589
+
1590
+ // src/client/resources/signal.ts
1591
+ var Signal = class {
1592
+ constructor(handlers) {
1593
+ this.startHandler = handlers.start;
1594
+ this.statusHandler = handlers.status;
1595
+ this.stopHandler = handlers.stop;
1596
+ this.emitPortHandler = handlers.emitPort;
1597
+ this.emitErrorHandler = handlers.emitError;
1598
+ this.emitServerReadyHandler = handlers.emitServerReady;
1599
+ }
1600
+ /**
1601
+ * Start the signal service
1602
+ * @returns SignalService instance with event handling
1603
+ */
1604
+ async start() {
1605
+ return this.startHandler();
1606
+ }
1607
+ /**
1608
+ * Get the signal service status
1609
+ * @returns Signal service status info
1610
+ */
1611
+ async status() {
1612
+ const response = await this.statusHandler();
1613
+ return {
1614
+ status: response.data.status,
1615
+ channel: response.data.channel,
1616
+ wsUrl: response.data.ws_url
1617
+ };
1618
+ }
1619
+ /**
1620
+ * Stop the signal service
1621
+ */
1622
+ async stop() {
1623
+ return this.stopHandler();
1624
+ }
1625
+ /**
1626
+ * Emit a port signal
1627
+ * @param port - Port number
1628
+ * @param type - Signal type ('open' or 'close')
1629
+ * @param url - URL associated with the port
1630
+ */
1631
+ async emitPort(port, type, url) {
1632
+ await this.emitPortHandler(port, type, url);
1633
+ }
1634
+ /**
1635
+ * Emit an error signal
1636
+ * @param message - Error message
1637
+ */
1638
+ async emitError(message) {
1639
+ await this.emitErrorHandler(message);
1640
+ }
1641
+ /**
1642
+ * Emit a server ready signal
1643
+ * @param port - Port number
1644
+ * @param url - Server URL
1645
+ */
1646
+ async emitServerReady(port, url) {
1647
+ await this.emitServerReadyHandler(port, url);
1648
+ }
1649
+ };
1650
+
1651
+ // src/client/resources/file.ts
1652
+ var File = class {
1653
+ constructor(handlers) {
1654
+ this.createHandler = handlers.create;
1655
+ this.listHandler = handlers.list;
1656
+ this.retrieveHandler = handlers.retrieve;
1657
+ this.destroyHandler = handlers.destroy;
1658
+ this.batchWriteHandler = handlers.batchWrite;
1659
+ this.existsHandler = handlers.exists;
1660
+ }
1661
+ /**
1662
+ * Create a new file with optional content
1663
+ * @param path - File path
1664
+ * @param content - File content (optional)
1665
+ * @returns File info
1666
+ */
1667
+ async create(path, content) {
1668
+ const response = await this.createHandler(path, content);
1669
+ return response.data.file;
1670
+ }
1671
+ /**
1672
+ * List files at the specified path
1673
+ * @param path - Directory path (default: '/')
1674
+ * @returns Array of file info
1675
+ */
1676
+ async list(path = "/") {
1677
+ const response = await this.listHandler(path);
1678
+ return response.data.files;
1679
+ }
1680
+ /**
1681
+ * Retrieve file content
1682
+ * @param path - File path
1683
+ * @returns File content as string
1684
+ */
1685
+ async retrieve(path) {
1686
+ return this.retrieveHandler(path);
1687
+ }
1688
+ /**
1689
+ * Destroy (delete) a file or directory
1690
+ * @param path - File or directory path
1691
+ */
1692
+ async destroy(path) {
1693
+ return this.destroyHandler(path);
1694
+ }
1695
+ /**
1696
+ * Batch file operations (write or delete multiple files)
1697
+ *
1698
+ * Features:
1699
+ * - Deduplication: Last operation wins per path
1700
+ * - File locking: Prevents race conditions
1701
+ * - Deterministic ordering: Alphabetical path sorting
1702
+ * - Partial failure handling: Returns per-file results
1703
+ *
1704
+ * @param files - Array of file operations
1705
+ * @returns Results for each file operation
1706
+ */
1707
+ async batchWrite(files) {
1708
+ const response = await this.batchWriteHandler(files);
1709
+ return response.data.results;
1710
+ }
1711
+ /**
1712
+ * Check if a file exists
1713
+ * @param path - File path
1714
+ * @returns True if file exists
1715
+ */
1716
+ async exists(path) {
1717
+ return this.existsHandler(path);
1718
+ }
1719
+ };
1720
+
1721
+ // src/client/resources/env.ts
1722
+ var Env = class {
1723
+ constructor(handlers) {
1724
+ this.retrieveHandler = handlers.retrieve;
1725
+ this.updateHandler = handlers.update;
1726
+ this.removeHandler = handlers.remove;
1727
+ this.existsHandler = handlers.exists;
1728
+ }
1729
+ /**
1730
+ * Retrieve environment variables from a file
1731
+ * @param file - Path to the .env file (relative to sandbox root)
1732
+ * @returns Key-value map of environment variables
1733
+ */
1734
+ async retrieve(file) {
1735
+ const response = await this.retrieveHandler(file);
1736
+ return response.data.variables;
1737
+ }
1738
+ /**
1739
+ * Update (merge) environment variables in a file
1740
+ * @param file - Path to the .env file (relative to sandbox root)
1741
+ * @param variables - Key-value pairs to set
1742
+ * @returns Keys that were updated
1743
+ */
1744
+ async update(file, variables) {
1745
+ const response = await this.updateHandler(file, variables);
1746
+ return response.data.keys;
1747
+ }
1748
+ /**
1749
+ * Remove environment variables from a file
1750
+ * @param file - Path to the .env file (relative to sandbox root)
1751
+ * @param keys - Keys to remove
1752
+ * @returns Keys that were removed
1753
+ */
1754
+ async remove(file, keys) {
1755
+ const response = await this.removeHandler(file, keys);
1756
+ return response.data.keys;
1757
+ }
1758
+ /**
1759
+ * Check if an environment file exists
1760
+ * @param file - Path to the .env file (relative to sandbox root)
1761
+ * @returns True if file exists
1762
+ */
1763
+ async exists(file) {
1764
+ return this.existsHandler(file);
1765
+ }
1766
+ };
1767
+
1768
+ // src/client/resources/auth.ts
1769
+ var Auth = class {
1770
+ constructor(handlers) {
1771
+ this.statusHandler = handlers.status;
1772
+ this.infoHandler = handlers.info;
1773
+ }
1774
+ /**
1775
+ * Check authentication status
1776
+ * @returns Authentication status info
1777
+ */
1778
+ async status() {
1779
+ const response = await this.statusHandler();
1780
+ return {
1781
+ authenticated: response.data.authenticated,
1782
+ tokenType: response.data.token_type,
1783
+ expiresAt: response.data.expires_at
1784
+ };
1785
+ }
1786
+ /**
1787
+ * Get authentication information and usage instructions
1788
+ * @returns Authentication info
1789
+ */
1790
+ async info() {
1791
+ const response = await this.infoHandler();
1792
+ return {
1793
+ message: response.data.message,
1794
+ instructions: response.data.instructions,
1795
+ endpoints: {
1796
+ createSessionToken: response.data.endpoints.create_session_token,
1797
+ listSessionTokens: response.data.endpoints.list_session_tokens,
1798
+ getSessionToken: response.data.endpoints.get_session_token,
1799
+ revokeSessionToken: response.data.endpoints.revoke_session_token,
1800
+ createMagicLink: response.data.endpoints.create_magic_link,
1801
+ authStatus: response.data.endpoints.auth_status,
1802
+ authInfo: response.data.endpoints.auth_info
1803
+ }
1804
+ };
1805
+ }
1806
+ };
1807
+
1808
+ // src/client/resources/run.ts
1809
+ var Run = class {
1810
+ constructor(handlers) {
1811
+ this.codeHandler = handlers.code;
1812
+ this.commandHandler = handlers.command;
1813
+ this.waitHandler = handlers.wait;
1814
+ }
1815
+ /**
1816
+ * Execute code with automatic language detection
1817
+ *
1818
+ * Supports: python, python3, node, javascript, js, bash, sh, ruby
1819
+ *
1820
+ * @param code - The code to execute
1821
+ * @param options - Execution options
1822
+ * @param options.language - Programming language (auto-detected if not specified)
1823
+ * @returns Code execution result with output, exit code, and detected language
1824
+ */
1825
+ async code(code, options) {
1826
+ return this.codeHandler(code, options);
1827
+ }
1828
+ /**
1829
+ * Execute a shell command
1830
+ *
1831
+ * @param command - The command to execute
1832
+ * @param options - Execution options
1833
+ * @param options.shell - Shell to use (optional)
1834
+ * @param options.background - Run in background (optional)
1835
+ * @param options.cwd - Working directory for the command (optional)
1836
+ * @param options.env - Environment variables (optional)
1837
+ * @param options.waitForCompletion - If true (with background), wait for command to complete
1838
+ * @returns Command execution result with stdout, stderr, exit code, and duration
1839
+ */
1840
+ async command(command, options) {
1841
+ const result = await this.commandHandler(command, options);
1842
+ if (options?.background && options?.waitForCompletion && result.cmdId && result.terminalId) {
1843
+ if (!this.waitHandler) {
1844
+ throw new Error("Wait handler not configured");
1845
+ }
1846
+ const waitOptions = typeof options.waitForCompletion === "object" ? options.waitForCompletion : void 0;
1847
+ return this.waitHandler(result.terminalId, result.cmdId, waitOptions);
1848
+ }
1849
+ return result;
1850
+ }
1851
+ /**
1852
+ * Wait for a background command to complete
1853
+ *
1854
+ * Uses the configured wait handler to block until the command
1855
+ * is complete or fails (typically via server-side long-polling).
1856
+ * Throws an error if the command fails or times out.
1857
+ *
1858
+ * @param terminalId - Terminal ID from background command result
1859
+ * @param cmdId - Command ID from background command result
1860
+ * @param options - Wait options passed to the handler
1861
+ * @returns Command result with final status
1862
+ * @throws Error if command fails or times out
1863
+ */
1864
+ async waitForCompletion(terminalId, cmdId, options) {
1865
+ if (!this.waitHandler) {
1866
+ throw new Error("Wait handler not configured");
1867
+ }
1868
+ return this.waitHandler(terminalId, cmdId, options);
1869
+ }
1870
+ };
1871
+
1872
+ // src/client/resources/child.ts
1873
+ var Child = class {
1874
+ constructor(handlers) {
1875
+ this.createHandler = handlers.create;
1876
+ this.listHandler = handlers.list;
1877
+ this.retrieveHandler = handlers.retrieve;
1878
+ this.destroyHandler = handlers.destroy;
1879
+ }
1880
+ /**
1881
+ * Create a new child sandbox
1882
+ * @returns Child sandbox info including URL and subdomain
1883
+ */
1884
+ async create(options) {
1885
+ return this.createHandler(options);
1886
+ }
1887
+ /**
1888
+ * List all child sandboxes
1889
+ * @returns Array of child sandbox info
1890
+ */
1891
+ async list() {
1892
+ const response = await this.listHandler();
1893
+ return response.sandboxes;
1894
+ }
1895
+ /**
1896
+ * Retrieve a specific child sandbox by subdomain
1897
+ * @param subdomain - The child subdomain (e.g., 'sandbox-12345')
1898
+ * @returns Child sandbox info
1899
+ */
1900
+ async retrieve(subdomain) {
1901
+ return this.retrieveHandler(subdomain);
1902
+ }
1903
+ /**
1904
+ * Destroy (delete) a child sandbox
1905
+ * @param subdomain - The child subdomain
1906
+ * @param options - Destroy options
1907
+ * @param options.deleteFiles - Whether to delete the child's files (default: false)
1908
+ */
1909
+ async destroy(subdomain, options) {
1910
+ return this.destroyHandler(subdomain, options?.deleteFiles ?? false);
1911
+ }
1912
+ };
1913
+
1914
+ // src/client/resources/overlay.ts
1915
+ var Overlay = class {
1916
+ constructor(handlers) {
1917
+ this.createHandler = handlers.create;
1918
+ this.listHandler = handlers.list;
1919
+ this.retrieveHandler = handlers.retrieve;
1920
+ this.destroyHandler = handlers.destroy;
1921
+ }
1922
+ /**
1923
+ * Create a new overlay from a template directory
1924
+ *
1925
+ * The overlay copies files from the source directory into the target path
1926
+ * for better isolation. Heavy directories (node_modules, .venv, etc.) are
1927
+ * copied in the background. Use the `ignore` option to exclude files/directories.
1928
+ *
1929
+ * @param options - Overlay creation options
1930
+ * @param options.source - Absolute path to source directory
1931
+ * @param options.target - Relative path in sandbox
1932
+ * @param options.ignore - Glob patterns to ignore (e.g., ["node_modules", "*.log"])
1933
+ * @param options.strategy - Strategy to use ('copy' or 'smart')
1934
+ * @param options.waitForCompletion - If true or options object, wait for background copy to complete
1935
+ * @returns Overlay info with copy status
1936
+ */
1937
+ async create(options) {
1938
+ const response = await this.createHandler(options);
1939
+ const overlay = this.toOverlayInfo(response);
1940
+ if (options.waitForCompletion) {
1941
+ const waitOptions = typeof options.waitForCompletion === "object" ? options.waitForCompletion : void 0;
1942
+ return this.waitForCompletion(overlay.id, waitOptions);
1943
+ }
1944
+ return overlay;
1945
+ }
1946
+ /**
1947
+ * List all overlays for the current sandbox
1948
+ * @returns Array of overlay info
1949
+ */
1950
+ async list() {
1951
+ const response = await this.listHandler();
1952
+ return response.overlays.map((o) => this.toOverlayInfo(o));
1953
+ }
1954
+ /**
1955
+ * Retrieve a specific overlay by ID
1956
+ *
1957
+ * Useful for polling the copy status of an overlay.
1958
+ *
1959
+ * @param id - Overlay ID
1960
+ * @returns Overlay info
1961
+ */
1962
+ async retrieve(id) {
1963
+ const response = await this.retrieveHandler(id);
1964
+ return this.toOverlayInfo(response);
1965
+ }
1966
+ /**
1967
+ * Destroy (delete) an overlay
1968
+ * @param id - Overlay ID
1969
+ */
1970
+ async destroy(id) {
1971
+ return this.destroyHandler(id);
1972
+ }
1973
+ /**
1974
+ * Wait for an overlay's background copy to complete
1975
+ *
1976
+ * Polls the overlay status with exponential backoff until the copy
1977
+ * is complete or fails. Throws an error if the copy fails or times out.
1978
+ *
1979
+ * @param id - Overlay ID
1980
+ * @param options - Polling options
1981
+ * @returns Overlay info with final copy status
1982
+ * @throws Error if copy fails or times out
1983
+ */
1984
+ async waitForCompletion(id, options = {}) {
1985
+ const maxRetries = options.maxRetries ?? 60;
1986
+ const initialDelayMs = options.initialDelayMs ?? 500;
1987
+ const maxDelayMs = options.maxDelayMs ?? 5e3;
1988
+ const backoffFactor = options.backoffFactor ?? 1.5;
1989
+ let currentDelay = initialDelayMs;
1990
+ for (let i = 0; i < maxRetries; i++) {
1991
+ const overlay = await this.retrieve(id);
1992
+ if (overlay.copyStatus === "complete") {
1993
+ return overlay;
1994
+ }
1995
+ if (overlay.copyStatus === "failed") {
1996
+ throw new Error(
1997
+ `Overlay copy failed: ${overlay.copyError || "Unknown error"}
1998
+ Overlay ID: ${id}`
1999
+ );
2000
+ }
2001
+ if (i < maxRetries - 1) {
2002
+ await new Promise((resolve) => setTimeout(resolve, currentDelay));
2003
+ currentDelay = Math.min(currentDelay * backoffFactor, maxDelayMs);
2004
+ }
2005
+ }
2006
+ const finalOverlay = await this.retrieve(id);
2007
+ throw new Error(
2008
+ `Overlay copy timed out after ${maxRetries} attempts.
2009
+ Overlay ID: ${id}
2010
+ Current status: ${finalOverlay.copyStatus}
2011
+
2012
+ Try increasing maxRetries or check if the source directory is very large.`
2013
+ );
2014
+ }
2015
+ /**
2016
+ * Convert API response to OverlayInfo
2017
+ */
2018
+ toOverlayInfo(response) {
2019
+ return {
2020
+ id: response.id,
2021
+ source: response.source,
2022
+ target: response.target,
2023
+ strategy: this.validateStrategy(response.strategy),
2024
+ createdAt: response.created_at,
2025
+ stats: {
2026
+ copiedFiles: response.stats.copied_files,
2027
+ copiedDirs: response.stats.copied_dirs,
2028
+ skipped: response.stats.skipped
2029
+ },
2030
+ copyStatus: this.validateCopyStatus(response.copy_status),
2031
+ copyError: response.copy_error
2032
+ };
2033
+ }
2034
+ /**
2035
+ * Validate and return strategy, defaulting to 'copy' for unknown/missing values (legacy support)
2036
+ */
2037
+ validateStrategy(strategy) {
2038
+ const validStrategies = ["copy", "smart"];
2039
+ if (strategy && validStrategies.includes(strategy)) {
2040
+ return strategy;
2041
+ }
2042
+ return "copy";
2043
+ }
2044
+ /**
2045
+ * Validate and return copy status, defaulting to 'pending' for unknown values
2046
+ */
2047
+ validateCopyStatus(status) {
2048
+ const validStatuses = ["pending", "in_progress", "complete", "failed"];
2049
+ if (validStatuses.includes(status)) {
2050
+ return status;
2051
+ }
2052
+ return "pending";
2053
+ }
2054
+ };
2055
+
2056
+ // src/client/types.ts
2057
+ var CommandExitError = class extends Error {
2058
+ constructor(result) {
2059
+ super(`Command exited with code ${result.exitCode}`);
2060
+ this.result = result;
2061
+ this.name = "CommandExitError";
2062
+ }
2063
+ };
2064
+ function isCommandExitError(error) {
2065
+ return typeof error === "object" && error !== null && "name" in error && error.name === "CommandExitError" && "result" in error;
2066
+ }
2067
+
2068
+ // src/client/index.ts
2069
+ var MAX_TUNNEL_TIMEOUT_SECONDS = 300;
2070
+ var Sandbox = class {
2071
+ constructor(config) {
2072
+ this._token = null;
2073
+ this._ws = null;
2074
+ this._terminals = /* @__PURE__ */ new Map();
2075
+ this.sandboxId = config.sandboxId;
2076
+ this.provider = config.provider;
2077
+ let sandboxUrlResolved = config.sandboxUrl;
2078
+ let tokenFromUrl = null;
2079
+ let sandboxUrlFromUrl = null;
2080
+ if (typeof window !== "undefined" && window.location && typeof localStorage !== "undefined") {
2081
+ const params = new URLSearchParams(window.location.search);
2082
+ tokenFromUrl = params.get("session_token");
2083
+ sandboxUrlFromUrl = params.get("sandbox_url");
2084
+ let urlChanged = false;
2085
+ if (tokenFromUrl) {
2086
+ params.delete("session_token");
2087
+ localStorage.setItem("session_token", tokenFromUrl);
2088
+ urlChanged = true;
2089
+ }
2090
+ if (sandboxUrlFromUrl) {
2091
+ params.delete("sandbox_url");
2092
+ localStorage.setItem("sandbox_url", sandboxUrlFromUrl);
2093
+ urlChanged = true;
2094
+ }
2095
+ if (urlChanged) {
2096
+ const search = params.toString() ? `?${params.toString()}` : "";
2097
+ const newUrl = `${window.location.pathname}${search}${window.location.hash}`;
2098
+ window.history.replaceState({}, "", newUrl);
2099
+ }
2100
+ if (!config.sandboxUrl) {
2101
+ sandboxUrlResolved = sandboxUrlFromUrl || localStorage.getItem("sandbox_url") || "";
2102
+ }
2103
+ }
2104
+ this.config = {
2105
+ sandboxUrl: (sandboxUrlResolved || "").replace(/\/$/, ""),
2106
+ // Remove trailing slash
2107
+ sandboxId: config.sandboxId || "",
2108
+ provider: config.provider || "",
2109
+ token: config.token || "",
2110
+ headers: config.headers || {},
2111
+ timeout: config.timeout || 3e4,
2112
+ protocol: config.protocol || "binary",
2113
+ metadata: config.metadata,
2114
+ destroyHandler: config.destroyHandler
2115
+ };
2116
+ this.WebSocketImpl = config.WebSocket || globalThis.WebSocket;
2117
+ if (!this.WebSocketImpl) {
2118
+ throw new Error(
2119
+ 'WebSocket is not available. In Node.js, pass WebSocket implementation:\nimport WebSocket from "ws";\nnew Sandbox({ sandboxUrl: "...", WebSocket })'
2120
+ );
2121
+ }
2122
+ if (config.token) {
2123
+ this._token = config.token;
2124
+ } else if (tokenFromUrl) {
2125
+ this._token = tokenFromUrl;
2126
+ } else if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
2127
+ this._token = localStorage.getItem("session_token");
2128
+ }
2129
+ this.filesystem = {
2130
+ readFile: async (path) => this.readFile(path),
2131
+ writeFile: async (path, content) => {
2132
+ await this.writeFile(path, content);
2133
+ },
2134
+ mkdir: async (path) => {
2135
+ await this.runCommand(escapeArgs(mkdir(path)));
2136
+ },
2137
+ readdir: async (path) => {
2138
+ const response = await this.listFiles(path);
2139
+ return response.data.files.map((f) => ({
2140
+ name: f.name,
2141
+ type: f.is_dir ? "directory" : "file",
2142
+ size: f.size,
2143
+ modified: new Date(f.modified_at)
2144
+ }));
2145
+ },
2146
+ exists: async (path) => {
2147
+ const result = await this.runCommand(escapeArgs(test.exists(path)));
2148
+ return result.exitCode === 0;
2149
+ },
2150
+ remove: async (path) => {
2151
+ await this.deleteFile(path);
2152
+ },
2153
+ overlay: new Overlay({
2154
+ create: async (options) => this.createOverlay(options),
2155
+ list: async () => this.listOverlays(),
2156
+ retrieve: async (id) => this.getOverlay(id),
2157
+ destroy: async (id) => this.deleteOverlay(id)
2158
+ })
2159
+ };
2160
+ this.terminal = new Terminal({
2161
+ create: async (options) => this.createTerminal(options),
2162
+ list: async () => this.listTerminals(),
2163
+ retrieve: async (id) => this.getTerminal(id),
2164
+ destroy: async (id) => {
2165
+ await this.request(`/terminals/${id}`, { method: "DELETE" });
2166
+ }
2167
+ });
2168
+ this.run = new Run({
2169
+ code: async (code, options) => {
2170
+ const result = await this.runCodeRequest(code, options?.language);
2171
+ return {
2172
+ output: result.data.output,
2173
+ exitCode: result.data.exit_code,
2174
+ language: result.data.language
2175
+ };
2176
+ },
2177
+ command: async (command, options) => {
2178
+ const result = await this.runCommandRequest({
2179
+ command,
2180
+ shell: options?.shell,
2181
+ background: options?.background,
2182
+ cwd: options?.cwd,
2183
+ env: options?.env
2184
+ });
2185
+ return {
2186
+ stdout: result.data.stdout,
2187
+ stderr: result.data.stderr,
2188
+ exitCode: result.data.exit_code ?? 0,
2189
+ durationMs: result.data.duration_ms ?? 0,
2190
+ // Include cmdId and terminalId for background commands
2191
+ cmdId: result.data.cmd_id,
2192
+ terminalId: result.data.terminal_id,
2193
+ status: result.data.status
2194
+ };
2195
+ },
2196
+ wait: async (terminalId, cmdId, options) => {
2197
+ return this.waitForCommandCompletion(terminalId, cmdId, options);
2198
+ }
2199
+ });
2200
+ this.server = new Server({
2201
+ start: async (options) => this.startServer(options),
2202
+ list: async () => this.listServers(),
2203
+ retrieve: async (slug) => this.getServer(slug),
2204
+ stop: async (slug) => {
2205
+ await this.stopServer(slug);
2206
+ },
2207
+ delete: async (slug) => {
2208
+ await this.deleteServer(slug);
2209
+ },
2210
+ restart: async (slug) => this.restartServer(slug),
2211
+ updateStatus: async (slug, status) => {
2212
+ await this.updateServerStatus(slug, status);
2213
+ },
2214
+ logs: async (slug, options) => this.getServerLogs(slug, options)
2215
+ });
2216
+ this.watcher = new Watcher({
2217
+ create: async (path, options) => this.createWatcher(path, options),
2218
+ list: async () => this.listWatchers(),
2219
+ retrieve: async (id) => this.getWatcher(id),
2220
+ destroy: async (id) => {
2221
+ await this.request(`/watchers/${id}`, { method: "DELETE" });
2222
+ }
2223
+ });
2224
+ this.sessionToken = new SessionToken({
2225
+ create: async (options) => this.createSessionToken(options),
2226
+ list: async () => this.listSessionTokens(),
2227
+ retrieve: async (id) => this.getSessionToken(id),
2228
+ revoke: async (id) => this.revokeSessionToken(id)
2229
+ });
2230
+ this.magicLink = new MagicLink({
2231
+ create: async (options) => this.createMagicLink(options)
2232
+ });
2233
+ this.signal = new Signal({
2234
+ start: async () => this.startSignals(),
2235
+ status: async () => this.getSignalStatus(),
2236
+ stop: async () => {
2237
+ await this.request("/signals/stop", { method: "POST" });
2238
+ },
2239
+ emitPort: async (port, type, url) => this.emitPortSignal(port, type, url),
2240
+ emitError: async (message) => this.emitErrorSignal(message),
2241
+ emitServerReady: async (port, url) => this.emitServerReadySignal(port, url)
2242
+ });
2243
+ this.file = new File({
2244
+ create: async (path, content) => this.createFile(path, content),
2245
+ list: async (path) => this.listFiles(path),
2246
+ retrieve: async (path) => this.readFile(path),
2247
+ destroy: async (path) => this.deleteFile(path),
2248
+ batchWrite: async (files) => this.batchWriteFiles(files),
2249
+ exists: async (path) => this.checkFileExists(path)
2250
+ });
2251
+ this.env = new Env({
2252
+ retrieve: async (file) => this.getEnv(file),
2253
+ update: async (file, variables) => this.setEnv(file, variables),
2254
+ remove: async (file, keys) => this.deleteEnv(file, keys),
2255
+ exists: async (file) => this.checkEnvFile(file)
2256
+ });
2257
+ this.auth = new Auth({
2258
+ status: async () => this.getAuthStatus(),
2259
+ info: async () => this.getAuthInfo()
2260
+ });
2261
+ this.child = new Child({
2262
+ create: async (options) => this.createSandbox(options),
2263
+ list: async () => this.listSandboxes(),
2264
+ retrieve: async (subdomain) => this.getSandbox(subdomain),
2265
+ destroy: async (subdomain, deleteFiles) => this.deleteSandbox(subdomain, deleteFiles)
2266
+ });
2267
+ }
2268
+ /**
2269
+ * Get or create internal WebSocket manager
2270
+ */
2271
+ async ensureWebSocket() {
2272
+ if (!this._ws || this._ws.getState() === "closed") {
2273
+ this._ws = new WebSocketManager({
2274
+ url: this.getWebSocketUrl(),
2275
+ WebSocket: this.WebSocketImpl,
2276
+ autoReconnect: true,
2277
+ debug: false,
2278
+ protocol: this.config.protocol
2279
+ });
2280
+ await this._ws.connect();
2281
+ }
2282
+ return this._ws;
2283
+ }
2284
+ /**
2285
+ * Create and configure a TerminalInstance from response data
2286
+ */
2287
+ async hydrateTerminal(data, ws) {
2288
+ const terminal = new TerminalInstance(
2289
+ data.id,
2290
+ data.pty,
2291
+ data.status,
2292
+ data.channel || null,
2293
+ ws || null,
2294
+ data.encoding || "raw"
2295
+ );
2296
+ const terminalId = data.id;
2297
+ terminal.setExecuteHandler(async (command, background) => {
2298
+ return this.request(`/terminals/${terminalId}/execute`, {
2299
+ method: "POST",
2300
+ body: JSON.stringify({ command, background })
2301
+ });
2302
+ });
2303
+ terminal.setListCommandsHandler(async () => {
2304
+ return this.request(`/terminals/${terminalId}/commands`);
2305
+ });
2306
+ terminal.setRetrieveCommandHandler(async (cmdId) => {
2307
+ return this.request(`/terminals/${terminalId}/commands/${cmdId}`);
2308
+ });
2309
+ terminal.setWaitCommandHandler(async (cmdId, timeout) => {
2310
+ const params = timeout ? new URLSearchParams({ timeout: timeout.toString() }) : "";
2311
+ const endpoint = `/terminals/${terminalId}/commands/${cmdId}/wait${params ? `?${params}` : ""}`;
2312
+ return this.request(endpoint);
2313
+ });
2314
+ terminal.setDestroyHandler(async () => {
2315
+ await this.request(`/terminals/${terminalId}`, {
2316
+ method: "DELETE"
2317
+ });
2318
+ this._terminals.delete(terminalId);
2319
+ });
2320
+ return terminal;
2321
+ }
2322
+ // ============================================================================
2323
+ // Private Helper Methods
2324
+ // ============================================================================
2325
+ async request(endpoint, options = {}) {
2326
+ const controller = new AbortController();
2327
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
2328
+ try {
2329
+ const headers = {
2330
+ ...this.config.headers
2331
+ };
2332
+ if (options.body) {
2333
+ headers["Content-Type"] = "application/json";
2334
+ }
2335
+ if (this._token) {
2336
+ headers["Authorization"] = `Bearer ${this._token}`;
2337
+ }
2338
+ const response = await fetch(`${this.config.sandboxUrl}${endpoint}`, {
2339
+ ...options,
2340
+ headers: {
2341
+ ...headers,
2342
+ ...options.headers
2343
+ },
2344
+ signal: controller.signal
2345
+ });
2346
+ clearTimeout(timeoutId);
2347
+ if (response.status === 204) {
2348
+ return void 0;
2349
+ }
2350
+ const text = await response.text();
2351
+ let data;
2352
+ try {
2353
+ data = JSON.parse(text);
2354
+ } catch (jsonError) {
2355
+ throw new Error(
2356
+ `Failed to parse JSON response from ${endpoint}: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}
2357
+ Response body (first 200 chars): ${text.substring(0, 200)}${text.length > 200 ? "..." : ""}`
2358
+ );
2359
+ }
2360
+ if (!response.ok) {
2361
+ const error = data.error || response.statusText;
2362
+ if (response.status === 403 && endpoint.startsWith("/auth/")) {
2363
+ if (endpoint.includes("/session_tokens") || endpoint.includes("/magic-links")) {
2364
+ throw new Error(
2365
+ `Access token required. This operation requires an access token, not a session token.
2366
+ API request failed (${response.status}): ${error}`
2367
+ );
2368
+ }
2369
+ }
2370
+ throw new Error(`API request failed (${response.status}): ${error}`);
2371
+ }
2372
+ return data;
2373
+ } catch (error) {
2374
+ clearTimeout(timeoutId);
2375
+ if (error instanceof Error && error.name === "AbortError") {
2376
+ throw new Error(`Request timeout after ${this.config.timeout}ms`);
2377
+ }
2378
+ throw error;
2379
+ }
2380
+ }
2381
+ // ============================================================================
2382
+ // Health Check
2383
+ // ============================================================================
2384
+ /**
2385
+ * Check service health
2386
+ */
2387
+ async health() {
2388
+ return this.request("/health");
2389
+ }
2390
+ // ============================================================================
2391
+ // Authentication
2392
+ // ============================================================================
2393
+ /**
2394
+ * Create a session token (requires access token)
2395
+ *
2396
+ * Session tokens are delegated credentials that can authenticate API requests
2397
+ * without exposing your access token. Only access tokens can create session tokens.
2398
+ *
2399
+ * @param options - Token configuration
2400
+ * @throws {Error} 403 Forbidden if called with a session token
2401
+ */
2402
+ async createSessionToken(options) {
2403
+ return this.request("/auth/session_tokens", {
2404
+ method: "POST",
2405
+ body: JSON.stringify(options || {})
2406
+ });
2407
+ }
2408
+ /**
2409
+ * List all session tokens (requires access token)
2410
+ *
2411
+ * @throws {Error} 403 Forbidden if called with a session token
2412
+ */
2413
+ async listSessionTokens() {
2414
+ return this.request("/auth/session_tokens");
2415
+ }
2416
+ /**
2417
+ * Get details of a specific session token (requires access token)
2418
+ *
2419
+ * @param tokenId - The token ID
2420
+ * @throws {Error} 403 Forbidden if called with a session token
2421
+ */
2422
+ async getSessionToken(tokenId) {
2423
+ return this.request(`/auth/session_tokens/${tokenId}`);
2424
+ }
2425
+ /**
2426
+ * Revoke a session token (requires access token)
2427
+ *
2428
+ * @param tokenId - The token ID to revoke
2429
+ * @throws {Error} 403 Forbidden if called with a session token
2430
+ */
2431
+ async revokeSessionToken(tokenId) {
2432
+ return this.request(`/auth/session_tokens/${tokenId}`, {
2433
+ method: "DELETE"
2434
+ });
2435
+ }
2436
+ /**
2437
+ * Generate a magic link for browser authentication (requires access token)
2438
+ *
2439
+ * Magic links are one-time URLs that automatically create a session token
2440
+ * and set it as a cookie in the user's browser. This provides an easy way
2441
+ * to authenticate users in browser-based applications.
2442
+ *
2443
+ * The generated link:
2444
+ * - Expires after 5 minutes or first use (whichever comes first)
2445
+ * - Automatically creates a new session token (7 day expiry)
2446
+ * - Sets the session token as an HttpOnly cookie
2447
+ * - Redirects to the specified URL
2448
+ *
2449
+ * @param options - Magic link configuration
2450
+ * @throws {Error} 403 Forbidden if called with a session token
2451
+ */
2452
+ async createMagicLink(options) {
2453
+ return this.request("/auth/magic-links", {
2454
+ method: "POST",
2455
+ body: JSON.stringify(options || {})
2456
+ });
2457
+ }
2458
+ /**
2459
+ * Check authentication status
2460
+ * Does not require authentication
2461
+ */
2462
+ async getAuthStatus() {
2463
+ return this.request("/auth/status");
2464
+ }
2465
+ /**
2466
+ * Get authentication information and usage instructions
2467
+ * Does not require authentication
2468
+ */
2469
+ async getAuthInfo() {
2470
+ return this.request("/auth/info");
2471
+ }
2472
+ /**
2473
+ * Set authentication token manually
2474
+ * @param token - Access token or session token
2475
+ */
2476
+ setToken(token) {
2477
+ this._token = token;
2478
+ }
2479
+ /**
2480
+ * Get current authentication token
2481
+ */
2482
+ getToken() {
2483
+ return this._token;
2484
+ }
2485
+ /**
2486
+ * Get current sandbox URL
2487
+ */
2488
+ getSandboxUrl() {
2489
+ return this.config.sandboxUrl;
2490
+ }
2491
+ // ============================================================================
2492
+ // Command Execution
2493
+ // ============================================================================
2494
+ /**
2495
+ * Execute a one-off command without creating a persistent terminal
2496
+ *
2497
+ * @example
2498
+ * ```typescript
2499
+ * // Synchronous execution (waits for completion)
2500
+ * const result = await sandbox.execute({ command: 'npm test' });
2501
+ * console.log(result.data.exit_code);
2502
+ *
2503
+ * // Background execution (returns immediately)
2504
+ * const result = await sandbox.execute({
2505
+ * command: 'npm install',
2506
+ * background: true
2507
+ * });
2508
+ * // Use result.data.terminal_id and result.data.cmd_id to track
2509
+ * const cmd = await sandbox.getCommand(result.data.terminal_id!, result.data.cmd_id!);
2510
+ * ```
2511
+ */
2512
+ async execute(options) {
2513
+ return this.request("/execute", {
2514
+ method: "POST",
2515
+ body: JSON.stringify(options)
2516
+ });
2517
+ }
2518
+ /**
2519
+ * Execute code with automatic language detection (POST /run/code)
2520
+ *
2521
+ * @param code - The code to execute
2522
+ * @param language - Programming language (optional - auto-detects if not specified)
2523
+ * @returns Code execution result with output, exit code, and detected language
2524
+ *
2525
+ * @example
2526
+ * ```typescript
2527
+ * // Auto-detect language
2528
+ * const result = await sandbox.runCodeRequest('print("Hello")');
2529
+ * console.log(result.data.output); // "Hello\n"
2530
+ * console.log(result.data.language); // "python"
2531
+ *
2532
+ * // Explicit language
2533
+ * const result = await sandbox.runCodeRequest('console.log("Hi")', 'node');
2534
+ * ```
2535
+ */
2536
+ async runCodeRequest(code, language) {
2537
+ const body = { code };
2538
+ if (language) {
2539
+ body.language = language;
2540
+ }
2541
+ return this.request("/run/code", {
2542
+ method: "POST",
2543
+ body: JSON.stringify(body)
2544
+ });
2545
+ }
2546
+ /**
2547
+ * Execute a command and get the result
2548
+ * Lower-level method that returns the raw API response
2549
+ *
2550
+ * @param options.command - Command to execute
2551
+ * @param options.shell - Shell to use (optional)
2552
+ * @param options.background - Run in background (optional)
2553
+ * @param options.cwd - Working directory for the command (optional)
2554
+ * @param options.env - Environment variables (optional)
2555
+ * @returns Command execution result
2556
+ *
2557
+ * @example
2558
+ * ```typescript
2559
+ * const result = await sandbox.runCommandRequest({ command: 'ls -la' });
2560
+ * console.log(result.data.stdout);
2561
+ * ```
2562
+ */
2563
+ async runCommandRequest(options) {
2564
+ return this.request("/run/command", {
2565
+ method: "POST",
2566
+ body: JSON.stringify(options)
2567
+ });
2568
+ }
2569
+ // ============================================================================
2570
+ // File Operations
2571
+ // ============================================================================
2572
+ /**
2573
+ * List files at the specified path
2574
+ */
2575
+ async listFiles(path = "/") {
2576
+ const params = new URLSearchParams({ path });
2577
+ return this.request(`/files?${params}`);
2578
+ }
2579
+ /**
2580
+ * Create a new file with optional content
2581
+ */
2582
+ async createFile(path, content) {
2583
+ return this.request("/files", {
2584
+ method: "POST",
2585
+ body: JSON.stringify({ path, content })
2586
+ });
2587
+ }
2588
+ /**
2589
+ * Get file metadata (without content)
2590
+ */
2591
+ async getFile(path) {
2592
+ return this.request(`/files/${this.encodeFilePath(path)}`);
2593
+ }
2594
+ /**
2595
+ * Encode a file path for use in URLs
2596
+ * Strips leading slash and encodes each segment separately to preserve path structure
2597
+ */
2598
+ encodeFilePath(path) {
2599
+ const pathWithoutLeadingSlash = path.startsWith("/") ? path.slice(1) : path;
2600
+ const segments = pathWithoutLeadingSlash.split("/");
2601
+ return segments.map((s) => encodeURIComponent(s)).join("/");
2602
+ }
2603
+ /**
2604
+ * Read file content
2605
+ */
2606
+ async readFile(path) {
2607
+ const params = new URLSearchParams({ content: "true" });
2608
+ const response = await this.request(
2609
+ `/files/${this.encodeFilePath(path)}?${params}`
2610
+ );
2611
+ return response.data.content || "";
2612
+ }
2613
+ /**
2614
+ * Write file content (creates or updates)
2615
+ */
2616
+ async writeFile(path, content) {
2617
+ return this.request("/files", {
2618
+ method: "POST",
2619
+ body: JSON.stringify({ path, content })
2620
+ });
2621
+ }
2622
+ /**
2623
+ * Delete a file or directory
2624
+ */
2625
+ async deleteFile(path) {
2626
+ return this.request(`/files/${this.encodeFilePath(path)}`, {
2627
+ method: "DELETE"
2628
+ });
2629
+ }
2630
+ /**
2631
+ * Check if a file exists (HEAD request)
2632
+ * @returns true if file exists, false otherwise
2633
+ */
2634
+ async checkFileExists(path) {
2635
+ try {
2636
+ const controller = new AbortController();
2637
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
2638
+ const headers = {
2639
+ ...this.config.headers
2640
+ };
2641
+ if (this._token) {
2642
+ headers["Authorization"] = `Bearer ${this._token}`;
2643
+ }
2644
+ const response = await fetch(
2645
+ `${this.config.sandboxUrl}/files/${this.encodeFilePath(path)}`,
2646
+ {
2647
+ method: "HEAD",
2648
+ headers,
2649
+ signal: controller.signal
2650
+ }
2651
+ );
2652
+ clearTimeout(timeoutId);
2653
+ return response.ok;
2654
+ } catch {
2655
+ return false;
2656
+ }
2657
+ }
2658
+ /**
2659
+ * Batch file operations (write or delete multiple files)
2660
+ *
2661
+ * Features:
2662
+ * - Deduplication: Last operation wins per path
2663
+ * - File locking: Prevents race conditions
2664
+ * - Deterministic ordering: Alphabetical path sorting
2665
+ * - Partial failure handling: Returns 207 Multi-Status with per-file results
2666
+ *
2667
+ * @param files - Array of file operations
2668
+ * @returns Results for each file operation
2669
+ *
2670
+ * @example
2671
+ * ```typescript
2672
+ * // Write multiple files
2673
+ * const results = await sandbox.batchWriteFiles([
2674
+ * { path: '/app/file1.txt', operation: 'write', content: 'Hello' },
2675
+ * { path: '/app/file2.txt', operation: 'write', content: 'World' },
2676
+ * ]);
2677
+ *
2678
+ * // Mixed operations (write and delete)
2679
+ * const results = await sandbox.batchWriteFiles([
2680
+ * { path: '/app/new.txt', operation: 'write', content: 'New file' },
2681
+ * { path: '/app/old.txt', operation: 'delete' },
2682
+ * ]);
2683
+ * ```
2684
+ */
2685
+ async batchWriteFiles(files) {
2686
+ return this.request("/files/batch", {
2687
+ method: "POST",
2688
+ body: JSON.stringify({ files })
2689
+ });
2690
+ }
2691
+ // ============================================================================
2692
+ // Filesystem Overlays
2693
+ // ============================================================================
2694
+ /**
2695
+ * Create a new filesystem overlay from a template directory
2696
+ *
2697
+ * Overlays enable instant sandbox setup by symlinking template files first,
2698
+ * then copying heavy directories (node_modules, .venv, etc.) in the background.
2699
+ *
2700
+ * @param options - Overlay creation options
2701
+ * @param options.source - Absolute path to source directory (template)
2702
+ * @param options.target - Relative path in sandbox where overlay will be mounted
2703
+ * @returns Overlay response with copy status
2704
+ *
2705
+ * @example
2706
+ * ```typescript
2707
+ * // Prefer using sandbox.filesystem.overlay.create() for camelCase response
2708
+ * const overlay = await sandbox.filesystem.overlay.create({
2709
+ * source: '/templates/nextjs',
2710
+ * target: 'project',
2711
+ * });
2712
+ * console.log(overlay.copyStatus); // 'pending' | 'in_progress' | 'complete' | 'failed'
2713
+ * ```
2714
+ */
2715
+ async createOverlay(options) {
2716
+ return this.request("/filesystem/overlays", {
2717
+ method: "POST",
2718
+ body: JSON.stringify(options)
2719
+ });
2720
+ }
2721
+ /**
2722
+ * List all filesystem overlays for the current sandbox
2723
+ * @returns List of overlays with their copy status
2724
+ */
2725
+ async listOverlays() {
2726
+ return this.request("/filesystem/overlays");
2727
+ }
2728
+ /**
2729
+ * Get a specific filesystem overlay by ID
2730
+ *
2731
+ * Useful for polling the copy status of an overlay.
2732
+ *
2733
+ * @param id - Overlay ID
2734
+ * @returns Overlay details with current copy status
2735
+ */
2736
+ async getOverlay(id) {
2737
+ return this.request(`/filesystem/overlays/${id}`);
2738
+ }
2739
+ /**
2740
+ * Delete a filesystem overlay
2741
+ * @param id - Overlay ID
2742
+ */
2743
+ async deleteOverlay(id) {
2744
+ return this.request(`/filesystem/overlays/${id}`, {
2745
+ method: "DELETE"
2746
+ });
2747
+ }
2748
+ // ============================================================================
2749
+ // Terminal Management
2750
+ // ============================================================================
2751
+ /**
2752
+ * Create a new persistent terminal session
2753
+ *
2754
+ * Terminal Modes:
2755
+ * - **PTY mode** (pty: true): Interactive shell with real-time WebSocket streaming
2756
+ * - Use for: Interactive shells, vim/nano, real-time output
2757
+ * - Methods: write(), resize(), on('output')
2758
+ *
2759
+ * - **Exec mode** (pty: false, default): Command tracking with HTTP polling
2760
+ * - Use for: CI/CD, automation, command tracking, exit codes
2761
+ * - Methods: execute(), getCommand(), listCommands(), waitForCommand()
2762
+ *
2763
+ * @example
2764
+ * ```typescript
2765
+ * // PTY mode - Interactive shell
2766
+ * const pty = await sandbox.createTerminal({ pty: true, shell: '/bin/bash' });
2767
+ * pty.on('output', (data) => console.log(data));
2768
+ * pty.write('npm install\n');
2769
+ *
2770
+ * // Exec mode - Command tracking
2771
+ * const exec = await sandbox.createTerminal({ pty: false });
2772
+ * const result = await exec.execute('npm test', { background: true });
2773
+ * const cmd = await sandbox.waitForCommand(exec.getId(), result.data.cmd_id);
2774
+ * console.log(cmd.data.exit_code);
2775
+ *
2776
+ * // Backward compatible - creates PTY terminal
2777
+ * const terminal = await sandbox.createTerminal('/bin/bash');
2778
+ * ```
2779
+ *
2780
+ * @param options - Terminal creation options
2781
+ * @param options.shell - Shell to use (e.g., '/bin/bash', '/bin/sh') - PTY mode only
2782
+ * @param options.encoding - Encoding for terminal I/O: 'raw' (default) or 'base64' (binary-safe)
2783
+ * @param options.pty - Terminal mode: true = PTY (interactive shell), false = exec (command tracking, default)
2784
+ * @returns Terminal instance with event handling
2785
+ */
2786
+ async createTerminal(shellOrOptions, encoding) {
2787
+ let pty;
2788
+ let shell;
2789
+ let enc;
2790
+ if (typeof shellOrOptions === "string") {
2791
+ pty = true;
2792
+ shell = shellOrOptions;
2793
+ enc = encoding;
2794
+ } else {
2795
+ pty = shellOrOptions?.pty ?? false;
2796
+ enc = shellOrOptions?.encoding;
2797
+ shell = shellOrOptions?.shell;
2798
+ }
2799
+ const body = {};
2800
+ if (shell) body.shell = shell;
2801
+ if (enc) body.encoding = enc;
2802
+ if (pty !== void 0) body.pty = pty;
2803
+ const response = await this.request("/terminals", {
2804
+ method: "POST",
2805
+ body: JSON.stringify(body)
2806
+ });
2807
+ let ws = null;
2808
+ if (response.data.pty) {
2809
+ ws = await this.ensureWebSocket();
2810
+ await new Promise((resolve) => {
2811
+ const handler = (msg) => {
2812
+ if (msg.data?.id === response.data.id) {
2813
+ if (ws) ws.off("terminal:created", handler);
2814
+ resolve();
2815
+ }
2816
+ };
2817
+ if (ws) {
2818
+ ws.on("terminal:created", handler);
2819
+ setTimeout(() => {
2820
+ if (ws) ws.off("terminal:created", handler);
2821
+ resolve();
2822
+ }, 5e3);
2823
+ } else {
2824
+ resolve();
2825
+ }
2826
+ });
2827
+ }
2828
+ const terminal = await this.hydrateTerminal(response.data, ws);
2829
+ this._terminals.set(terminal.id, terminal);
2830
+ return terminal;
2831
+ }
2832
+ /**
2833
+ * List all active terminals (fetches from API)
2834
+ */
2835
+ async listTerminals() {
2836
+ const response = await this.request("/terminals");
2837
+ return response.data.terminals;
2838
+ }
2839
+ /**
2840
+ * Get terminal by ID
2841
+ */
2842
+ async getTerminal(id) {
2843
+ const cached = this._terminals.get(id);
2844
+ if (cached) {
2845
+ return cached;
2846
+ }
2847
+ const response = await this.request(`/terminals/${id}`);
2848
+ let ws = null;
2849
+ if (response.data.pty) {
2850
+ ws = await this.ensureWebSocket();
2851
+ }
2852
+ const terminal = await this.hydrateTerminal(response.data, ws);
2853
+ this._terminals.set(id, terminal);
2854
+ return terminal;
2855
+ }
2856
+ // ============================================================================
2857
+ // Command Tracking (Exec Mode Terminals)
2858
+ // ============================================================================
2859
+ /**
2860
+ * List all commands executed in a terminal (exec mode only)
2861
+ * @param terminalId - The terminal ID
2862
+ * @returns List of all commands with their status
2863
+ * @throws {Error} If terminal is in PTY mode (command tracking not available)
2864
+ */
2865
+ async listCommands(terminalId) {
2866
+ return this.request(`/terminals/${terminalId}/commands`);
2867
+ }
2868
+ /**
2869
+ * Get details of a specific command execution (exec mode only)
2870
+ * @param terminalId - The terminal ID
2871
+ * @param cmdId - The command ID
2872
+ * @returns Command execution details including stdout, stderr, and exit code
2873
+ * @throws {Error} If terminal is in PTY mode or command not found
2874
+ */
2875
+ async getCommand(terminalId, cmdId) {
2876
+ return this.request(`/terminals/${terminalId}/commands/${cmdId}`);
2877
+ }
2878
+ /**
2879
+ * Wait for a command to complete (HTTP long-polling, exec mode only)
2880
+ * @param terminalId - The terminal ID
2881
+ * @param cmdId - The command ID
2882
+ * @param timeout - Optional timeout in seconds (0 = no timeout)
2883
+ * @returns Command execution details when completed
2884
+ * @throws {Error} If terminal is in PTY mode, command not found, or timeout occurs
2885
+ */
2886
+ async waitForCommand(terminalId, cmdId, timeout) {
2887
+ const params = timeout ? new URLSearchParams({ timeout: timeout.toString() }) : "";
2888
+ const endpoint = `/terminals/${terminalId}/commands/${cmdId}/wait${params ? `?${params}` : ""}`;
2889
+ return this.request(endpoint);
2890
+ }
2891
+ /**
2892
+ * Wait for a background command to complete using long-polling
2893
+ *
2894
+ * Uses the server's long-polling endpoint with configurable timeout.
2895
+ * The tunnel supports up to 5 minutes (300 seconds) via X-Request-Timeout header.
2896
+ *
2897
+ * @param terminalId - The terminal ID
2898
+ * @param cmdId - The command ID
2899
+ * @param options - Wait options (timeoutSeconds, default 300)
2900
+ * @returns Command result with final status
2901
+ * @throws Error if command fails or times out
2902
+ * @internal
2903
+ */
2904
+ async waitForCommandCompletion(terminalId, cmdId, options) {
2905
+ const timeoutSeconds = options?.timeoutSeconds ?? MAX_TUNNEL_TIMEOUT_SECONDS;
2906
+ const response = await this.waitForCommandWithTimeout(terminalId, cmdId, timeoutSeconds);
2907
+ return {
2908
+ stdout: response.data.stdout,
2909
+ stderr: response.data.stderr,
2910
+ exitCode: response.data.exit_code ?? 0,
2911
+ durationMs: response.data.duration_ms ?? 0,
2912
+ cmdId: response.data.cmd_id,
2913
+ terminalId,
2914
+ status: response.data.status
2915
+ };
2916
+ }
2917
+ /**
2918
+ * Wait for a command with extended timeout support
2919
+ * Uses X-Request-Timeout header for tunnel timeout configuration
2920
+ * @internal
2921
+ */
2922
+ async waitForCommandWithTimeout(terminalId, cmdId, timeoutSeconds) {
2923
+ const params = new URLSearchParams({ timeout: timeoutSeconds.toString() });
2924
+ const endpoint = `/terminals/${terminalId}/commands/${cmdId}/wait?${params}`;
2925
+ const requestTimeout = Math.min(timeoutSeconds, MAX_TUNNEL_TIMEOUT_SECONDS);
2926
+ return this.request(endpoint, {
2927
+ headers: {
2928
+ "X-Request-Timeout": requestTimeout.toString()
2929
+ }
2930
+ });
2931
+ }
2932
+ // ============================================================================
2933
+ // File Watchers
2934
+ // ============================================================================
2935
+ /**
2936
+ * Create a new file watcher with WebSocket integration
2937
+ * @param path - Path to watch
2938
+ * @param options - Watcher options
2939
+ * @param options.includeContent - Include file content in change events
2940
+ * @param options.ignored - Patterns to ignore
2941
+ * @param options.encoding - Encoding for file content: 'raw' (default) or 'base64' (binary-safe)
2942
+ * @returns FileWatcher instance with event handling
2943
+ */
2944
+ async createWatcher(path, options) {
2945
+ const ws = await this.ensureWebSocket();
2946
+ const response = await this.request("/watchers", {
2947
+ method: "POST",
2948
+ body: JSON.stringify({ path, ...options })
2949
+ });
2950
+ const watcher = new FileWatcher(
2951
+ response.data.id,
2952
+ response.data.path,
2953
+ response.data.status,
2954
+ response.data.channel,
2955
+ response.data.includeContent,
2956
+ response.data.ignored,
2957
+ ws,
2958
+ response.data.encoding || "raw"
2959
+ );
2960
+ watcher.setDestroyHandler(async () => {
2961
+ await this.request(`/watchers/${response.data.id}`, {
2962
+ method: "DELETE"
2963
+ });
2964
+ });
2965
+ return watcher;
2966
+ }
2967
+ /**
2968
+ * List all active file watchers (fetches from API)
2969
+ */
2970
+ async listWatchers() {
2971
+ return this.request("/watchers");
2972
+ }
2973
+ /**
2974
+ * Get file watcher by ID
2975
+ */
2976
+ async getWatcher(id) {
2977
+ return this.request(`/watchers/${id}`);
2978
+ }
2979
+ // ============================================================================
2980
+ // Signal Service
2981
+ // ============================================================================
2982
+ /**
2983
+ * Start the signal service with WebSocket integration
2984
+ * @returns SignalService instance with event handling
2985
+ */
2986
+ async startSignals() {
2987
+ const ws = await this.ensureWebSocket();
2988
+ const response = await this.request("/signals/start", {
2989
+ method: "POST"
2990
+ });
2991
+ const signalService = new SignalService(
2992
+ response.data.status,
2993
+ response.data.channel,
2994
+ ws
2995
+ );
2996
+ signalService.setStopHandler(async () => {
2997
+ await this.request("/signals/stop", {
2998
+ method: "POST"
2999
+ });
3000
+ });
3001
+ return signalService;
3002
+ }
3003
+ /**
3004
+ * Get the signal service status (fetches from API)
3005
+ */
3006
+ async getSignalStatus() {
3007
+ return this.request("/signals/status");
3008
+ }
3009
+ /**
3010
+ * Emit a port signal
3011
+ */
3012
+ async emitPortSignal(port, type, url) {
3013
+ return this.request("/signals/port", {
3014
+ method: "POST",
3015
+ body: JSON.stringify({ port, type, url })
3016
+ });
3017
+ }
3018
+ /**
3019
+ * Emit a port signal (alternative endpoint using path parameters)
3020
+ */
3021
+ async emitPortSignalAlt(port, type) {
3022
+ return this.request(`/signals/port/${port}/${type}`, {
3023
+ method: "POST"
3024
+ });
3025
+ }
3026
+ /**
3027
+ * Emit an error signal
3028
+ */
3029
+ async emitErrorSignal(message) {
3030
+ return this.request("/signals/error", {
3031
+ method: "POST",
3032
+ body: JSON.stringify({ message })
3033
+ });
3034
+ }
3035
+ /**
3036
+ * Emit a server ready signal
3037
+ */
3038
+ async emitServerReadySignal(port, url) {
3039
+ return this.request("/signals/server-ready", {
3040
+ method: "POST",
3041
+ body: JSON.stringify({ port, url })
3042
+ });
3043
+ }
3044
+ // ============================================================================
3045
+ // Environment Variables
3046
+ // ============================================================================
3047
+ /**
3048
+ * Get environment variables from a .env file
3049
+ * @param file - Path to the .env file (relative to sandbox root)
3050
+ */
3051
+ async getEnv(file) {
3052
+ const params = new URLSearchParams({ file });
3053
+ return this.request(`/env?${params}`);
3054
+ }
3055
+ /**
3056
+ * Set (merge) environment variables in a .env file
3057
+ * @param file - Path to the .env file (relative to sandbox root)
3058
+ * @param variables - Key-value pairs to set
3059
+ */
3060
+ async setEnv(file, variables) {
3061
+ const params = new URLSearchParams({ file });
3062
+ return this.request(`/env?${params}`, {
3063
+ method: "POST",
3064
+ body: JSON.stringify({ variables })
3065
+ });
3066
+ }
3067
+ /**
3068
+ * Delete environment variables from a .env file
3069
+ * @param file - Path to the .env file (relative to sandbox root)
3070
+ * @param keys - Keys to delete
3071
+ */
3072
+ async deleteEnv(file, keys) {
3073
+ const params = new URLSearchParams({ file });
3074
+ return this.request(`/env?${params}`, {
3075
+ method: "DELETE",
3076
+ body: JSON.stringify({ keys })
3077
+ });
3078
+ }
3079
+ /**
3080
+ * Check if an environment file exists (HEAD request)
3081
+ * @param file - Path to the .env file (relative to sandbox root)
3082
+ * @returns true if file exists, false otherwise
3083
+ */
3084
+ async checkEnvFile(file) {
3085
+ try {
3086
+ const controller = new AbortController();
3087
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
3088
+ const headers = {
3089
+ ...this.config.headers
3090
+ };
3091
+ if (this._token) {
3092
+ headers["Authorization"] = `Bearer ${this._token}`;
3093
+ }
3094
+ const params = new URLSearchParams({ file });
3095
+ const response = await fetch(`${this.config.sandboxUrl}/env?${params}`, {
3096
+ method: "HEAD",
3097
+ headers,
3098
+ signal: controller.signal
3099
+ });
3100
+ clearTimeout(timeoutId);
3101
+ return response.ok;
3102
+ } catch {
3103
+ return false;
3104
+ }
3105
+ }
3106
+ // ============================================================================
3107
+ // Server Management
3108
+ // ============================================================================
3109
+ /**
3110
+ * List all managed servers
3111
+ */
3112
+ async listServers() {
3113
+ return this.request("/servers");
3114
+ }
3115
+ /**
3116
+ * Start a new managed server with optional supervisor settings
3117
+ *
3118
+ * @param options - Server configuration
3119
+ * @param options.slug - Unique server identifier
3120
+ * @param options.install - Install command (optional, runs blocking before start, e.g., "npm install")
3121
+ * @param options.start - Command to start the server (e.g., "npm run dev")
3122
+ * @param options.path - Working directory (optional)
3123
+ * @param options.env_file - Path to .env file relative to path (optional)
3124
+ * @param options.environment - Inline environment variables (merged with env_file if both provided)
3125
+ * @param options.port - Requested port (preallocated before start)
3126
+ * @param options.strict_port - If true, fail instead of auto-incrementing when port is taken
3127
+ * @param options.autostart - Auto-start on daemon boot (default: true)
3128
+ * @param options.overlay - Inline overlay to create before starting
3129
+ * @param options.overlays - Additional overlays to create before starting
3130
+ * @param options.depends_on - Overlay IDs this server depends on
3131
+ * @param options.restart_policy - When to automatically restart: 'never' (default), 'on-failure', 'always'
3132
+ * @param options.max_restarts - Maximum restart attempts, 0 = unlimited (default: 0)
3133
+ * @param options.restart_delay_ms - Delay between restart attempts in milliseconds (default: 1000)
3134
+ * @param options.stop_timeout_ms - Graceful shutdown timeout in milliseconds (default: 10000)
3135
+ *
3136
+ * @example
3137
+ * ```typescript
3138
+ * // Basic server
3139
+ * await sandbox.startServer({
3140
+ * slug: 'web',
3141
+ * start: 'npm run dev',
3142
+ * path: '/app',
3143
+ * });
3144
+ *
3145
+ * // With install command and supervisor settings
3146
+ * await sandbox.startServer({
3147
+ * slug: 'api',
3148
+ * install: 'npm install',
3149
+ * start: 'node server.js',
3150
+ * path: '/app',
3151
+ * environment: { NODE_ENV: 'production', PORT: '3000' },
3152
+ * restart_policy: 'on-failure',
3153
+ * max_restarts: 5,
3154
+ * restart_delay_ms: 2000,
3155
+ * stop_timeout_ms: 5000,
3156
+ * });
3157
+ *
3158
+ * // With inline overlay dependencies
3159
+ * await sandbox.startServer({
3160
+ * slug: 'web',
3161
+ * start: 'npm run dev',
3162
+ * path: '/app',
3163
+ * overlay: {
3164
+ * source: '/templates/nextjs',
3165
+ * target: 'app',
3166
+ * strategy: 'smart',
3167
+ * },
3168
+ * });
3169
+ * ```
3170
+ */
3171
+ async startServer(options) {
3172
+ return this.request("/servers", {
3173
+ method: "POST",
3174
+ body: JSON.stringify(options)
3175
+ });
3176
+ }
3177
+ /**
3178
+ * Get information about a specific server
3179
+ * @param slug - Server slug
3180
+ */
3181
+ async getServer(slug) {
3182
+ return this.request(`/servers/${encodeURIComponent(slug)}`);
3183
+ }
3184
+ /**
3185
+ * Stop a managed server (non-destructive)
3186
+ * @param slug - Server slug
3187
+ */
3188
+ async stopServer(slug) {
3189
+ return this.request(
3190
+ `/servers/${encodeURIComponent(slug)}/stop`,
3191
+ {
3192
+ method: "POST"
3193
+ }
3194
+ );
3195
+ }
3196
+ /**
3197
+ * Delete a managed server configuration
3198
+ * @param slug - Server slug
3199
+ */
3200
+ async deleteServer(slug) {
3201
+ await this.request(`/servers/${encodeURIComponent(slug)}`, {
3202
+ method: "DELETE"
3203
+ });
3204
+ }
3205
+ /**
3206
+ * Restart a managed server
3207
+ * @param slug - Server slug
3208
+ */
3209
+ async restartServer(slug) {
3210
+ return this.request(
3211
+ `/servers/${encodeURIComponent(slug)}/restart`,
3212
+ {
3213
+ method: "POST"
3214
+ }
3215
+ );
3216
+ }
3217
+ /**
3218
+ * Get logs for a managed server
3219
+ * @param slug - Server slug
3220
+ * @param options - Options for log retrieval
3221
+ */
3222
+ async getServerLogs(slug, options) {
3223
+ const params = new URLSearchParams();
3224
+ if (options?.stream) {
3225
+ params.set("stream", options.stream);
3226
+ }
3227
+ const queryString = params.toString();
3228
+ return this.request(
3229
+ `/servers/${encodeURIComponent(slug)}/logs${queryString ? `?${queryString}` : ""}`
3230
+ );
3231
+ }
3232
+ /**
3233
+ * Update server status (internal use)
3234
+ * @param slug - Server slug
3235
+ * @param status - New server status
3236
+ */
3237
+ async updateServerStatus(slug, status) {
3238
+ return this.request(
3239
+ `/servers/${encodeURIComponent(slug)}/status`,
3240
+ {
3241
+ method: "PATCH",
3242
+ body: JSON.stringify({ status })
3243
+ }
3244
+ );
3245
+ }
3246
+ // ============================================================================
3247
+ // Ready Management
3248
+ // ============================================================================
3249
+ /**
3250
+ * Get readiness status for autostarted servers and overlays
3251
+ */
3252
+ async ready() {
3253
+ const response = await this.request("/ready");
3254
+ return {
3255
+ ready: response.ready,
3256
+ servers: response.servers ?? [],
3257
+ overlays: response.overlays ?? []
3258
+ };
3259
+ }
3260
+ // ============================================================================
3261
+ // Sandbox Management
3262
+ // ============================================================================
3263
+ /**
3264
+ * Create a new sandbox environment
3265
+ */
3266
+ async createSandbox(options) {
3267
+ return this.request("/sandboxes", {
3268
+ method: "POST",
3269
+ body: JSON.stringify(options || {})
3270
+ });
3271
+ }
3272
+ /**
3273
+ * List all sandboxes
3274
+ */
3275
+ async listSandboxes() {
3276
+ return this.request("/sandboxes");
3277
+ }
3278
+ /**
3279
+ * Get sandbox details
3280
+ */
3281
+ async getSandbox(subdomain) {
3282
+ return this.request(`/sandboxes/${subdomain}`);
3283
+ }
3284
+ /**
3285
+ * Delete a sandbox
3286
+ */
3287
+ async deleteSandbox(subdomain, deleteFiles = false) {
3288
+ const params = new URLSearchParams({ delete_files: String(deleteFiles) });
3289
+ return this.request(`/sandboxes/${subdomain}?${params}`, {
3290
+ method: "DELETE"
3291
+ });
3292
+ }
3293
+ // ============================================================================
3294
+ // WebSocket Connection (Internal)
3295
+ // ============================================================================
10
3296
  /**
11
- * Create a new ComputeError
12
- *
13
- * @param message Error message
14
- * @param provider Provider identifier
15
- * @param sandboxId Optional sandbox identifier
3297
+ * Get WebSocket URL for real-time communication
3298
+ * @private
16
3299
  */
17
- constructor(message, provider, sandboxId) {
18
- super(message);
19
- this.name = this.constructor.name;
20
- this.provider = provider;
21
- this.sandboxId = sandboxId;
22
- Object.setPrototypeOf(this, new.target.prototype);
3300
+ getWebSocketUrl() {
3301
+ const wsProtocol = this.config.sandboxUrl.startsWith("https") ? "wss" : "ws";
3302
+ const url = this.config.sandboxUrl.replace(/^https?:/, `${wsProtocol}:`);
3303
+ const params = new URLSearchParams();
3304
+ if (this._token) {
3305
+ params.set("token", this._token);
3306
+ }
3307
+ params.set("protocol", this.config.protocol || "binary");
3308
+ const queryString = params.toString();
3309
+ return `${url}/ws${queryString ? `?${queryString}` : ""}`;
23
3310
  }
24
- };
25
- var ExecutionError = class extends ComputeError {
3311
+ // ============================================================================
3312
+ // Sandbox Interface Implementation
3313
+ // ============================================================================
26
3314
  /**
27
- * Create a new ExecutionError
28
- *
29
- * @param message Error message
30
- * @param provider Provider identifier
31
- * @param exitCode Exit code from the execution
32
- * @param sandboxId Optional sandbox identifier
33
- */
34
- constructor(message, provider, exitCode, sandboxId) {
35
- super(message, provider, sandboxId);
36
- /** Error code */
37
- this.code = "EXECUTION_ERROR";
38
- /** Execution errors are generally not retryable */
39
- this.isRetryable = false;
40
- this.exitCode = exitCode;
3315
+ * Execute code in the sandbox (convenience method)
3316
+ *
3317
+ * Delegates to sandbox.run.code() - prefer using that directly for new code.
3318
+ *
3319
+ * @param code - The code to execute
3320
+ * @param language - Programming language (auto-detected if not specified)
3321
+ * @returns Code execution result
3322
+ */
3323
+ async runCode(code, language) {
3324
+ return this.run.code(code, language ? { language } : void 0);
41
3325
  }
42
- };
43
- var TimeoutError = class extends ComputeError {
44
3326
  /**
45
- * Create a new TimeoutError
3327
+ * Execute shell command in the sandbox
3328
+ *
3329
+ * Sends clean command string to server - no preprocessing or shell wrapping.
3330
+ * The server handles shell invocation, working directory, and backgrounding.
3331
+ *
3332
+ * @param command - The command to execute (raw string, e.g., "npm install")
3333
+ * @param options - Execution options
3334
+ * @param options.background - Run in background (server uses goroutines)
3335
+ * @param options.cwd - Working directory (server uses cmd.Dir)
3336
+ * @param options.env - Environment variables (server uses cmd.Env)
3337
+ * @param options.onStdout - Callback for streaming stdout data
3338
+ * @param options.onStderr - Callback for streaming stderr data
3339
+ * @returns Command execution result
3340
+ *
3341
+ * @example
3342
+ * ```typescript
3343
+ * // Simple command
3344
+ * await sandbox.runCommand('ls -la')
46
3345
  *
47
- * @param message Error message
48
- * @param provider Provider identifier
49
- * @param timeoutMs Timeout duration in milliseconds
50
- * @param sandboxId Optional sandbox identifier
51
- */
52
- constructor(message, provider, timeoutMs, sandboxId) {
53
- super(message, provider, sandboxId);
54
- /** Error code */
55
- this.code = "TIMEOUT_ERROR";
56
- /** Timeout errors may be retryable with a longer timeout */
57
- this.isRetryable = true;
58
- this.timeoutMs = timeoutMs;
3346
+ * // With working directory
3347
+ * await sandbox.runCommand('npm install', { cwd: '/app' })
3348
+ *
3349
+ * // Background with env vars
3350
+ * await sandbox.runCommand('node server.js', {
3351
+ * background: true,
3352
+ * env: { PORT: '3000' }
3353
+ * })
3354
+ *
3355
+ * // With streaming output
3356
+ * await sandbox.runCommand('npm install', {
3357
+ * onStdout: (data) => console.log(data),
3358
+ * onStderr: (data) => console.error(data),
3359
+ * })
3360
+ * ```
3361
+ */
3362
+ async runCommand(command, options) {
3363
+ const hasStreamingCallbacks = options?.onStdout || options?.onStderr;
3364
+ if (!hasStreamingCallbacks) {
3365
+ return this.run.command(command, options);
3366
+ }
3367
+ const ws = await this.ensureWebSocket();
3368
+ const result = await this.runCommandRequest({
3369
+ command,
3370
+ stream: true,
3371
+ cwd: options?.cwd,
3372
+ env: options?.env
3373
+ });
3374
+ const { cmd_id, channel } = result.data;
3375
+ if (!cmd_id || !channel) {
3376
+ throw new Error("Server did not return streaming channel info");
3377
+ }
3378
+ ws.subscribe(channel);
3379
+ let stdout = "";
3380
+ let stderr = "";
3381
+ let exitCode = 0;
3382
+ let resolvePromise = null;
3383
+ const cleanup = () => {
3384
+ ws.off("command:stdout", handleStdout);
3385
+ ws.off("command:stderr", handleStderr);
3386
+ ws.off("command:exit", handleExit);
3387
+ ws.unsubscribe(channel);
3388
+ };
3389
+ const handleStdout = (msg) => {
3390
+ if (msg.channel === channel && msg.data.cmd_id === cmd_id) {
3391
+ stdout += msg.data.output;
3392
+ options?.onStdout?.(msg.data.output);
3393
+ }
3394
+ };
3395
+ const handleStderr = (msg) => {
3396
+ if (msg.channel === channel && msg.data.cmd_id === cmd_id) {
3397
+ stderr += msg.data.output;
3398
+ options?.onStderr?.(msg.data.output);
3399
+ }
3400
+ };
3401
+ const handleExit = (msg) => {
3402
+ if (msg.channel === channel && msg.data.cmd_id === cmd_id) {
3403
+ exitCode = msg.data.exit_code;
3404
+ cleanup();
3405
+ if (resolvePromise) {
3406
+ resolvePromise({ stdout, stderr, exitCode, durationMs: 0 });
3407
+ }
3408
+ }
3409
+ };
3410
+ ws.on("command:stdout", handleStdout);
3411
+ ws.on("command:stderr", handleStderr);
3412
+ ws.on("command:exit", handleExit);
3413
+ ws.startCommand(cmd_id);
3414
+ if (options?.background) {
3415
+ return {
3416
+ stdout: "",
3417
+ stderr: "",
3418
+ exitCode: 0,
3419
+ durationMs: 0
3420
+ };
3421
+ }
3422
+ return new Promise((resolve) => {
3423
+ resolvePromise = resolve;
3424
+ });
59
3425
  }
60
- };
61
- var ProviderError = class extends ComputeError {
62
3426
  /**
63
- * Create a new ProviderError
64
- *
65
- * @param message Error message
66
- * @param provider Provider identifier
67
- * @param originalError Optional original error from the provider
68
- * @param sandboxId Optional sandbox identifier
69
- */
70
- constructor(message, provider, originalError, sandboxId) {
71
- super(message, provider, sandboxId);
72
- /** Error code */
73
- this.code = "PROVIDER_ERROR";
74
- /** Provider errors may be retryable */
75
- this.isRetryable = true;
76
- this.originalError = originalError;
3427
+ * Get server information
3428
+ * Returns details about the server including auth status, main subdomain, sandbox count, and version
3429
+ */
3430
+ async getServerInfo() {
3431
+ return this.request("/info");
77
3432
  }
78
- };
79
- var ConfigurationError = class extends ComputeError {
80
3433
  /**
81
- * Create a new ConfigurationError
82
- *
83
- * @param message Error message
84
- * @param provider Provider identifier
85
- * @param sandboxId Optional sandbox identifier
3434
+ * Get sandbox information
86
3435
  */
87
- constructor(message, provider, sandboxId) {
88
- super(message, provider, sandboxId);
89
- /** Error code */
90
- this.code = "CONFIGURATION_ERROR";
91
- /** Configuration errors are not retryable without changes */
92
- this.isRetryable = false;
3436
+ async getInfo() {
3437
+ return {
3438
+ id: this.sandboxId || "",
3439
+ provider: this.provider || "",
3440
+ runtime: "node",
3441
+ status: "running",
3442
+ createdAt: /* @__PURE__ */ new Date(),
3443
+ timeout: this.config.timeout,
3444
+ metadata: this.config.metadata
3445
+ };
93
3446
  }
94
- };
95
- var AuthenticationError = class extends ComputeError {
96
3447
  /**
97
- * Create a new AuthenticationError
98
- *
99
- * @param message Error message
100
- * @param provider Provider identifier
101
- * @param sandboxId Optional sandbox identifier
3448
+ * Get URL for accessing sandbox on a specific port (Sandbox interface method)
102
3449
  */
103
- constructor(message, provider, sandboxId) {
104
- super(message, provider, sandboxId);
105
- /** Error code */
106
- this.code = "AUTHENTICATION_ERROR";
107
- /** Authentication errors are not retryable without new credentials */
108
- this.isRetryable = false;
3450
+ async getUrl(options) {
3451
+ const protocol = options.protocol || "https";
3452
+ const url = new URL(this.config.sandboxUrl);
3453
+ const parts = url.hostname.split(".");
3454
+ const subdomain = parts[0];
3455
+ const baseDomain = parts.slice(1).join(".");
3456
+ const previewDomain = baseDomain.replace("sandbox.computesdk.com", "preview.computesdk.com");
3457
+ return `${protocol}://${subdomain}-${options.port}.${previewDomain}`;
3458
+ }
3459
+ /**
3460
+ * Get provider instance
3461
+ * Note: Not available when using Sandbox directly - only available through gateway provider
3462
+ */
3463
+ getProvider() {
3464
+ throw new Error(
3465
+ "getProvider() is not available on Sandbox. This method is only available when using provider sandboxes through the gateway."
3466
+ );
109
3467
  }
110
- };
111
- var ProviderUnavailableError = class extends ComputeError {
112
3468
  /**
113
- * Create a new ProviderUnavailableError
3469
+ * Get native provider instance
3470
+ * Returns the Sandbox itself since this IS the sandbox implementation
3471
+ */
3472
+ getInstance() {
3473
+ return this;
3474
+ }
3475
+ /**
3476
+ * Destroy the sandbox (Sandbox interface method)
114
3477
  *
115
- * @param message Error message
116
- * @param provider Provider identifier
117
- * @param sandboxId Optional sandbox identifier
3478
+ * If a destroyHandler was provided (e.g., from gateway), calls it to destroy
3479
+ * the sandbox on the backend. Otherwise, only disconnects the WebSocket.
3480
+ */
3481
+ async destroy() {
3482
+ await this.disconnect();
3483
+ if (this.config.destroyHandler) {
3484
+ await this.config.destroyHandler();
3485
+ }
3486
+ }
3487
+ /**
3488
+ * Disconnect WebSocket
3489
+ *
3490
+ * Note: This only disconnects the WebSocket. Terminals, watchers, and signals
3491
+ * will continue running on the server until explicitly destroyed via their
3492
+ * respective destroy() methods or the DELETE endpoints.
118
3493
  */
119
- constructor(message, provider, sandboxId) {
120
- super(message, provider, sandboxId);
121
- /** Error code */
122
- this.code = "PROVIDER_UNAVAILABLE";
123
- /** Provider unavailability may be temporary */
124
- this.isRetryable = true;
3494
+ async disconnect() {
3495
+ if (this._ws) {
3496
+ this._ws.disconnect();
3497
+ this._ws = null;
3498
+ }
3499
+ this._terminals.clear();
125
3500
  }
126
3501
  };
127
3502
 
128
- // src/config.ts
129
- var DEFAULT_TIMEOUT = 3e5;
130
- var ENV_KEYS = {
131
- E2B: "E2B_API_KEY",
132
- VERCEL: "VERCEL_TOKEN",
133
- DAYTONA: "DAYTONA_API_KEY"
3503
+ // src/setup.ts
3504
+ var encodeBase64 = (value) => {
3505
+ if (typeof Buffer !== "undefined") {
3506
+ return Buffer.from(value, "utf8").toString("base64");
3507
+ }
3508
+ if (typeof btoa !== "undefined" && typeof TextEncoder !== "undefined") {
3509
+ const bytes = new TextEncoder().encode(value);
3510
+ let binary = "";
3511
+ for (const byte of bytes) {
3512
+ binary += String.fromCharCode(byte);
3513
+ }
3514
+ return btoa(binary);
3515
+ }
3516
+ throw new Error("Base64 encoding is not supported in this environment.");
3517
+ };
3518
+ var buildSetupPayload = (options) => {
3519
+ const overlays = options.overlays?.map((overlay) => {
3520
+ const { source, target, ignore, strategy } = overlay;
3521
+ return {
3522
+ source,
3523
+ target,
3524
+ ignore,
3525
+ strategy
3526
+ };
3527
+ });
3528
+ const servers = options.servers;
3529
+ return {
3530
+ overlays: overlays?.length ? overlays : void 0,
3531
+ servers: servers?.length ? servers : void 0
3532
+ };
3533
+ };
3534
+ var encodeSetupPayload = (options) => {
3535
+ const payload = buildSetupPayload(options);
3536
+ return encodeBase64(JSON.stringify(payload));
3537
+ };
3538
+
3539
+ // src/provider-config.ts
3540
+ var PROVIDER_AUTH = {
3541
+ e2b: [["E2B_API_KEY"]],
3542
+ modal: [["MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"]],
3543
+ railway: [["RAILWAY_API_KEY", "RAILWAY_PROJECT_ID", "RAILWAY_ENVIRONMENT_ID"]],
3544
+ render: [["RENDER_API_KEY", "RENDER_OWNER_ID"]],
3545
+ daytona: [["DAYTONA_API_KEY"]],
3546
+ vercel: [
3547
+ ["VERCEL_OIDC_TOKEN"],
3548
+ ["VERCEL_TOKEN", "VERCEL_TEAM_ID", "VERCEL_PROJECT_ID"]
3549
+ ],
3550
+ runloop: [["RUNLOOP_API_KEY"]],
3551
+ cloudflare: [["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"]],
3552
+ codesandbox: [["CSB_API_KEY"]],
3553
+ blaxel: [["BL_API_KEY", "BL_WORKSPACE"]],
3554
+ namespace: [["NSC_TOKEN"]]
134
3555
  };
135
- function detectAvailableProviders() {
136
- const available = [];
137
- const env = typeof process !== "undefined" ? process.env : {};
138
- if (env[ENV_KEYS.E2B]) {
139
- available.push("e2b");
3556
+ var PROVIDER_NAMES = Object.keys(PROVIDER_AUTH);
3557
+ var PROVIDER_HEADERS = {
3558
+ e2b: {
3559
+ apiKey: "X-E2B-API-Key"
3560
+ },
3561
+ modal: {
3562
+ tokenId: "X-Modal-Token-Id",
3563
+ tokenSecret: "X-Modal-Token-Secret"
3564
+ },
3565
+ railway: {
3566
+ apiToken: "X-Railway-API-Key",
3567
+ projectId: "X-Railway-Project-ID",
3568
+ environmentId: "X-Railway-Environment-ID"
3569
+ },
3570
+ render: {
3571
+ apiKey: "X-Render-API-Key",
3572
+ ownerId: "X-Render-Owner-ID"
3573
+ },
3574
+ daytona: {
3575
+ apiKey: "X-Daytona-API-Key"
3576
+ },
3577
+ vercel: {
3578
+ oidcToken: "X-Vercel-OIDC-Token",
3579
+ token: "X-Vercel-Token",
3580
+ teamId: "X-Vercel-Team-Id",
3581
+ projectId: "X-Vercel-Project-Id"
3582
+ },
3583
+ runloop: {
3584
+ apiKey: "X-Runloop-API-Key"
3585
+ },
3586
+ cloudflare: {
3587
+ apiToken: "X-Cloudflare-API-Token",
3588
+ accountId: "X-Cloudflare-Account-Id"
3589
+ },
3590
+ codesandbox: {
3591
+ apiKey: "X-CSB-API-Key"
3592
+ },
3593
+ blaxel: {
3594
+ apiKey: "X-Blaxel-API-Key",
3595
+ workspace: "X-Blaxel-Workspace"
3596
+ },
3597
+ namespace: {
3598
+ token: "X-Namespace-Token"
140
3599
  }
141
- if (env[ENV_KEYS.VERCEL]) {
142
- available.push("vercel");
3600
+ };
3601
+ var PROVIDER_ENV_MAP = {
3602
+ e2b: {
3603
+ E2B_API_KEY: "apiKey"
3604
+ },
3605
+ modal: {
3606
+ MODAL_TOKEN_ID: "tokenId",
3607
+ MODAL_TOKEN_SECRET: "tokenSecret"
3608
+ },
3609
+ railway: {
3610
+ RAILWAY_API_KEY: "apiToken",
3611
+ RAILWAY_PROJECT_ID: "projectId",
3612
+ RAILWAY_ENVIRONMENT_ID: "environmentId"
3613
+ },
3614
+ render: {
3615
+ RENDER_API_KEY: "apiKey",
3616
+ RENDER_OWNER_ID: "ownerId"
3617
+ },
3618
+ daytona: {
3619
+ DAYTONA_API_KEY: "apiKey"
3620
+ },
3621
+ vercel: {
3622
+ VERCEL_OIDC_TOKEN: "oidcToken",
3623
+ VERCEL_TOKEN: "token",
3624
+ VERCEL_TEAM_ID: "teamId",
3625
+ VERCEL_PROJECT_ID: "projectId"
3626
+ },
3627
+ runloop: {
3628
+ RUNLOOP_API_KEY: "apiKey"
3629
+ },
3630
+ cloudflare: {
3631
+ CLOUDFLARE_API_TOKEN: "apiToken",
3632
+ CLOUDFLARE_ACCOUNT_ID: "accountId"
3633
+ },
3634
+ codesandbox: {
3635
+ CSB_API_KEY: "apiKey"
3636
+ },
3637
+ blaxel: {
3638
+ BL_API_KEY: "apiKey",
3639
+ BL_WORKSPACE: "workspace"
3640
+ },
3641
+ namespace: {
3642
+ NSC_TOKEN: "token"
143
3643
  }
144
- if (env[ENV_KEYS.DAYTONA]) {
145
- available.push("daytona");
3644
+ };
3645
+ var PROVIDER_DASHBOARD_URLS = {
3646
+ e2b: "https://e2b.dev/dashboard",
3647
+ modal: "https://modal.com/settings",
3648
+ railway: "https://railway.app/account/tokens",
3649
+ render: "https://dashboard.render.com/account",
3650
+ daytona: "https://daytona.io/dashboard",
3651
+ vercel: "https://vercel.com/account/tokens",
3652
+ runloop: "https://runloop.ai/dashboard",
3653
+ cloudflare: "https://dash.cloudflare.com/profile/api-tokens",
3654
+ codesandbox: "https://codesandbox.io/dashboard/settings",
3655
+ blaxel: "https://blaxel.ai/dashboard",
3656
+ namespace: "https://cloud.namespace.so"
3657
+ };
3658
+ function isValidProvider(name) {
3659
+ return name in PROVIDER_AUTH;
3660
+ }
3661
+ function buildProviderHeaders(provider, config) {
3662
+ const headers = {};
3663
+ const headerMap = PROVIDER_HEADERS[provider];
3664
+ for (const [configKey, headerName] of Object.entries(headerMap)) {
3665
+ const value = config[configKey];
3666
+ if (value) {
3667
+ headers[headerName] = value;
3668
+ }
146
3669
  }
147
- return available;
3670
+ return headers;
148
3671
  }
149
- function autoSelectProvider() {
150
- const available = detectAvailableProviders();
151
- return available.length > 0 ? available[0] : void 0;
3672
+ function getProviderConfigFromEnv(provider) {
3673
+ const config = {};
3674
+ const envMap = PROVIDER_ENV_MAP[provider];
3675
+ for (const [envVar, configKey] of Object.entries(envMap)) {
3676
+ const value = process.env[envVar];
3677
+ if (value) {
3678
+ config[configKey] = value;
3679
+ }
3680
+ }
3681
+ return config;
152
3682
  }
153
- function normalizeContainerConfig(container) {
154
- if (!container) {
155
- return void 0;
3683
+ function isProviderAuthComplete(provider) {
3684
+ const authOptions = PROVIDER_AUTH[provider];
3685
+ for (const option of authOptions) {
3686
+ const allPresent = option.every((envVar) => !!process.env[envVar]);
3687
+ if (allPresent) return true;
156
3688
  }
157
- if (typeof container === "string") {
158
- return { image: container };
3689
+ return false;
3690
+ }
3691
+ function getMissingEnvVars(provider) {
3692
+ const authOptions = PROVIDER_AUTH[provider];
3693
+ let bestOption = null;
3694
+ for (const option of authOptions) {
3695
+ const missing = [];
3696
+ let presentCount = 0;
3697
+ for (const envVar of option) {
3698
+ if (process.env[envVar]) {
3699
+ presentCount++;
3700
+ } else {
3701
+ missing.push(envVar);
3702
+ }
3703
+ }
3704
+ if (missing.length === 0) return [];
3705
+ if (!bestOption || presentCount > bestOption.presentCount) {
3706
+ bestOption = { presentCount, missing };
3707
+ }
159
3708
  }
160
- if (!container.image) {
161
- throw new ConfigurationError("Container configuration must include an image", "config");
3709
+ return bestOption?.missing ?? [];
3710
+ }
3711
+
3712
+ // src/constants.ts
3713
+ var GATEWAY_URL = "https://gateway.computesdk.com";
3714
+ var PROVIDER_PRIORITY = [
3715
+ "e2b",
3716
+ "railway",
3717
+ "render",
3718
+ "daytona",
3719
+ "modal",
3720
+ "runloop",
3721
+ "vercel",
3722
+ "cloudflare",
3723
+ "codesandbox",
3724
+ "blaxel",
3725
+ "namespace"
3726
+ ];
3727
+ var PROVIDER_ENV_VARS = {
3728
+ e2b: ["E2B_API_KEY"],
3729
+ railway: ["RAILWAY_API_KEY", "RAILWAY_PROJECT_ID", "RAILWAY_ENVIRONMENT_ID"],
3730
+ render: ["RENDER_API_KEY", "RENDER_OWNER_ID"],
3731
+ daytona: ["DAYTONA_API_KEY"],
3732
+ modal: ["MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"],
3733
+ runloop: ["RUNLOOP_API_KEY"],
3734
+ vercel: ["VERCEL_TOKEN", "VERCEL_TEAM_ID", "VERCEL_PROJECT_ID"],
3735
+ cloudflare: ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"],
3736
+ codesandbox: ["CSB_API_KEY"],
3737
+ blaxel: ["BL_API_KEY", "BL_WORKSPACE"],
3738
+ namespace: ["NSC_TOKEN"]
3739
+ };
3740
+
3741
+ // src/auto-detect.ts
3742
+ function isGatewayModeEnabled() {
3743
+ return !!(typeof process !== "undefined" && process.env?.COMPUTESDK_API_KEY);
3744
+ }
3745
+ function hasProviderEnv(provider) {
3746
+ if (typeof process === "undefined") return false;
3747
+ const requiredVars = PROVIDER_ENV_VARS[provider];
3748
+ if (!requiredVars) return false;
3749
+ return requiredVars.every((varName) => !!process.env?.[varName]);
3750
+ }
3751
+ function getProviderEnvStatus(provider) {
3752
+ const requiredVars = PROVIDER_ENV_VARS[provider];
3753
+ if (typeof process === "undefined" || !requiredVars) {
3754
+ return { provider, present: [], missing: requiredVars ? [...requiredVars] : [], isComplete: false };
162
3755
  }
163
- return container;
3756
+ const present = requiredVars.filter((varName) => !!process.env?.[varName]);
3757
+ const missing = requiredVars.filter((varName) => !process.env?.[varName]);
3758
+ return {
3759
+ provider,
3760
+ present: [...present],
3761
+ missing: [...missing],
3762
+ isComplete: missing.length === 0
3763
+ };
164
3764
  }
165
- function getDefaultRuntime(provider, runtime) {
166
- if (runtime) {
167
- return runtime;
3765
+ function detectProvider() {
3766
+ if (typeof process === "undefined") return null;
3767
+ const explicit = process.env.COMPUTESDK_PROVIDER?.toLowerCase();
3768
+ if (explicit && hasProviderEnv(explicit)) {
3769
+ return explicit;
168
3770
  }
169
- switch (provider) {
170
- case "e2b":
171
- return "python";
172
- case "vercel":
173
- return "node";
174
- case "daytona":
175
- return "python";
176
- default:
177
- return "node";
3771
+ if (explicit && !hasProviderEnv(explicit)) {
3772
+ console.warn(
3773
+ `\u26A0\uFE0F COMPUTESDK_PROVIDER is set to "${explicit}" but required credentials are missing.
3774
+ Required: ${PROVIDER_ENV_VARS[explicit]?.join(", ") || "unknown"}
3775
+ Falling back to auto-detection...`
3776
+ );
3777
+ }
3778
+ for (const provider of PROVIDER_PRIORITY) {
3779
+ if (hasProviderEnv(provider)) {
3780
+ return provider;
3781
+ }
178
3782
  }
3783
+ return null;
179
3784
  }
180
- function validateProviderApiKey(provider) {
181
- let envKey;
3785
+ function getProviderHeaders(provider) {
3786
+ if (typeof process === "undefined") return {};
3787
+ const headers = {};
182
3788
  switch (provider) {
183
3789
  case "e2b":
184
- envKey = ENV_KEYS.E2B;
3790
+ if (process.env.E2B_API_KEY) {
3791
+ headers["X-E2B-API-Key"] = process.env.E2B_API_KEY;
3792
+ }
185
3793
  break;
186
- case "vercel":
187
- envKey = ENV_KEYS.VERCEL;
3794
+ case "railway":
3795
+ if (process.env.RAILWAY_API_KEY) {
3796
+ headers["X-Railway-API-Key"] = process.env.RAILWAY_API_KEY;
3797
+ }
3798
+ if (process.env.RAILWAY_PROJECT_ID) {
3799
+ headers["X-Railway-Project-ID"] = process.env.RAILWAY_PROJECT_ID;
3800
+ }
3801
+ if (process.env.RAILWAY_ENVIRONMENT_ID) {
3802
+ headers["X-Railway-Environment-ID"] = process.env.RAILWAY_ENVIRONMENT_ID;
3803
+ }
188
3804
  break;
189
3805
  case "daytona":
190
- envKey = ENV_KEYS.DAYTONA;
3806
+ if (process.env.DAYTONA_API_KEY) {
3807
+ headers["X-Daytona-API-Key"] = process.env.DAYTONA_API_KEY;
3808
+ }
3809
+ break;
3810
+ case "modal":
3811
+ if (process.env.MODAL_TOKEN_ID) {
3812
+ headers["X-Modal-Token-ID"] = process.env.MODAL_TOKEN_ID;
3813
+ }
3814
+ if (process.env.MODAL_TOKEN_SECRET) {
3815
+ headers["X-Modal-Token-Secret"] = process.env.MODAL_TOKEN_SECRET;
3816
+ }
3817
+ break;
3818
+ case "runloop":
3819
+ if (process.env.RUNLOOP_API_KEY) {
3820
+ headers["X-Runloop-API-Key"] = process.env.RUNLOOP_API_KEY;
3821
+ }
3822
+ break;
3823
+ case "vercel":
3824
+ if (process.env.VERCEL_TOKEN) {
3825
+ headers["X-Vercel-Token"] = process.env.VERCEL_TOKEN;
3826
+ }
3827
+ if (process.env.VERCEL_TEAM_ID) {
3828
+ headers["X-Vercel-Team-ID"] = process.env.VERCEL_TEAM_ID;
3829
+ }
3830
+ if (process.env.VERCEL_PROJECT_ID) {
3831
+ headers["X-Vercel-Project-ID"] = process.env.VERCEL_PROJECT_ID;
3832
+ }
3833
+ break;
3834
+ case "cloudflare":
3835
+ if (process.env.CLOUDFLARE_API_TOKEN) {
3836
+ headers["X-Cloudflare-API-Token"] = process.env.CLOUDFLARE_API_TOKEN;
3837
+ }
3838
+ if (process.env.CLOUDFLARE_ACCOUNT_ID) {
3839
+ headers["X-Cloudflare-Account-ID"] = process.env.CLOUDFLARE_ACCOUNT_ID;
3840
+ }
3841
+ break;
3842
+ case "codesandbox":
3843
+ if (process.env.CSB_API_KEY) {
3844
+ headers["X-CodeSandbox-API-Key"] = process.env.CSB_API_KEY;
3845
+ }
3846
+ break;
3847
+ case "blaxel":
3848
+ if (process.env.BL_API_KEY) {
3849
+ headers["X-Blaxel-API-Key"] = process.env.BL_API_KEY;
3850
+ }
3851
+ if (process.env.BL_WORKSPACE) {
3852
+ headers["X-Blaxel-Workspace"] = process.env.BL_WORKSPACE;
3853
+ }
3854
+ break;
3855
+ case "namespace":
3856
+ if (process.env.NSC_TOKEN) {
3857
+ headers["X-Namespace-Token"] = process.env.NSC_TOKEN;
3858
+ }
191
3859
  break;
192
- case "auto":
193
- return;
194
- // Will be handled by auto-selection
195
- default:
196
- throw new ConfigurationError(`Unknown provider: ${provider}`, "config");
197
- }
198
- const env = typeof process !== "undefined" ? process.env : {};
199
- if (!env[envKey]) {
200
- const available = detectAvailableProviders();
201
- const suggestions = available.length > 0 ? `Available providers: ${available.join(", ")}` : `No provider API keys found. Set ${Object.values(ENV_KEYS).join(" or ")} environment variables.`;
202
- throw new ConfigurationError(
203
- `Missing API key for provider '${provider}'. ${suggestions}`,
204
- provider
205
- );
206
3860
  }
3861
+ return headers;
207
3862
  }
208
- function normalizeSandboxConfig(config) {
209
- const normalized = {
210
- provider: config?.provider || "auto",
211
- timeout: config?.timeout || DEFAULT_TIMEOUT
3863
+ function autoConfigureCompute() {
3864
+ if (!isGatewayModeEnabled()) {
3865
+ return null;
3866
+ }
3867
+ const provider = detectProvider();
3868
+ if (!provider) {
3869
+ const detectionResults = PROVIDER_PRIORITY.map((p) => getProviderEnvStatus(p));
3870
+ const statusLines = detectionResults.map((result) => {
3871
+ const status = result.isComplete ? "\u2705" : result.present.length > 0 ? "\u26A0\uFE0F " : "\u274C";
3872
+ const ratio = `${result.present.length}/${result.present.length + result.missing.length}`;
3873
+ let line = ` ${status} ${result.provider.padEnd(12)} ${ratio} credentials`;
3874
+ if (result.present.length > 0 && result.missing.length > 0) {
3875
+ line += ` (missing: ${result.missing.join(", ")})`;
3876
+ }
3877
+ return line;
3878
+ });
3879
+ throw new Error(
3880
+ `COMPUTESDK_API_KEY is set but no provider detected.
3881
+
3882
+ Provider detection results:
3883
+ ` + statusLines.join("\n") + `
3884
+
3885
+ To fix this, set one of the following:
3886
+
3887
+ E2B: export E2B_API_KEY=xxx
3888
+ Railway: export RAILWAY_API_KEY=xxx RAILWAY_PROJECT_ID=xxx RAILWAY_ENVIRONMENT_ID=xxx
3889
+ Daytona: export DAYTONA_API_KEY=xxx
3890
+ Modal: export MODAL_TOKEN_ID=xxx MODAL_TOKEN_SECRET=xxx
3891
+ Runloop: export RUNLOOP_API_KEY=xxx
3892
+ Vercel: export VERCEL_TOKEN=xxx VERCEL_TEAM_ID=xxx VERCEL_PROJECT_ID=xxx
3893
+ Cloudflare: export CLOUDFLARE_API_TOKEN=xxx CLOUDFLARE_ACCOUNT_ID=xxx
3894
+ CodeSandbox: export CSB_API_KEY=xxx
3895
+ Blaxel: export BL_API_KEY=xxx BL_WORKSPACE=xxx
3896
+ Namespace: export NSC_TOKEN=xxx
3897
+
3898
+ Or set COMPUTESDK_PROVIDER to specify explicitly:
3899
+ export COMPUTESDK_PROVIDER=e2b
3900
+
3901
+ Docs: https://computesdk.com/docs/quickstart`
3902
+ );
3903
+ }
3904
+ const gatewayUrl = process.env.COMPUTESDK_GATEWAY_URL || GATEWAY_URL;
3905
+ const computesdkApiKey = process.env.COMPUTESDK_API_KEY;
3906
+ const providerHeaders = getProviderHeaders(provider);
3907
+ try {
3908
+ new URL(gatewayUrl);
3909
+ } catch (error) {
3910
+ throw new Error(
3911
+ `Invalid gateway URL: "${gatewayUrl}"
3912
+
3913
+ The URL must be a valid HTTP/HTTPS URL.
3914
+ Check your COMPUTESDK_GATEWAY_URL environment variable.`
3915
+ );
3916
+ }
3917
+ if (process.env.COMPUTESDK_DEBUG) {
3918
+ console.log(`\u2728 ComputeSDK: Auto-detected ${provider} provider`);
3919
+ console.log(`\u{1F310} Gateway: ${gatewayUrl}`);
3920
+ console.log(`\u{1F511} Provider headers:`, Object.keys(providerHeaders).join(", "));
3921
+ }
3922
+ const config = {
3923
+ apiKey: computesdkApiKey,
3924
+ gatewayUrl,
3925
+ provider,
3926
+ providerHeaders
212
3927
  };
213
- if (normalized.provider === "auto") {
214
- const autoProvider = autoSelectProvider();
215
- if (!autoProvider) {
216
- throw new ConfigurationError(
217
- `No provider API keys found. Set one of the following environment variables: ${Object.values(ENV_KEYS).join(", ")}`,
218
- "config"
219
- );
3928
+ return config;
3929
+ }
3930
+
3931
+ // src/explicit-config.ts
3932
+ function buildProviderHeaders2(config) {
3933
+ const headers = {};
3934
+ const provider = config.provider;
3935
+ const headerMap = PROVIDER_HEADERS[provider];
3936
+ const providerConfig = config[provider];
3937
+ if (!providerConfig || !headerMap) return headers;
3938
+ for (const [configKey, headerName] of Object.entries(headerMap)) {
3939
+ const value = providerConfig[configKey];
3940
+ if (value) {
3941
+ headers[headerName] = value;
220
3942
  }
221
- normalized.provider = autoProvider;
222
- } else {
223
- validateProviderApiKey(normalized.provider);
224
3943
  }
225
- if (config?.runtime) {
226
- normalized.runtime = config.runtime;
227
- } else if (!config?.container) {
228
- normalized.runtime = getDefaultRuntime(normalized.provider);
3944
+ return headers;
3945
+ }
3946
+ function validateProviderConfig(config) {
3947
+ const provider = config.provider;
3948
+ const authOptions = PROVIDER_AUTH[provider];
3949
+ const providerConfig = config[provider];
3950
+ const dashboardUrl = PROVIDER_DASHBOARD_URLS[provider];
3951
+ if (!authOptions) {
3952
+ throw new Error(`Unknown provider: ${provider}`);
229
3953
  }
230
- if (config?.container) {
231
- normalized.container = normalizeContainerConfig(config.container);
3954
+ for (const option of authOptions) {
3955
+ const allPresent = option.every((envVar) => {
3956
+ const configField = envVarToConfigField(provider, envVar);
3957
+ return providerConfig?.[configField];
3958
+ });
3959
+ if (allPresent) return;
232
3960
  }
233
- return normalized;
234
- }
3961
+ const configExample = buildConfigExample(provider, authOptions);
3962
+ throw new Error(
3963
+ `Missing ${provider} configuration. When using provider: '${provider}', you must provide:
3964
+ ${configExample}
235
3965
 
236
- // src/utils.ts
237
- async function executeSandbox(params) {
238
- return await params.sandbox.execute(params.code, params.runtime);
3966
+ Get your credentials at: ${dashboardUrl}`
3967
+ );
239
3968
  }
240
- async function runCode(params) {
241
- return await params.sandbox.runCode(params.code, params.runtime);
3969
+ function envVarToConfigField(provider, envVar) {
3970
+ return PROVIDER_ENV_MAP[provider]?.[envVar] ?? envVar.toLowerCase();
242
3971
  }
243
- async function runCommand(params) {
244
- return await params.sandbox.runCommand(params.command, params.args);
3972
+ function buildConfigExample(provider, authOptions) {
3973
+ if (authOptions.length === 1) {
3974
+ const fields = authOptions[0].map((envVar) => {
3975
+ const field = envVarToConfigField(provider, envVar);
3976
+ return `${field}: '...'`;
3977
+ });
3978
+ return ` ${provider}: { ${fields.join(", ")} }`;
3979
+ }
3980
+ const options = authOptions.map((option, i) => {
3981
+ const fields = option.map((envVar) => {
3982
+ const field = envVarToConfigField(provider, envVar);
3983
+ return `${field}: '...'`;
3984
+ });
3985
+ return ` Option ${i + 1}:
3986
+ ${provider}: { ${fields.join(", ")} }`;
3987
+ });
3988
+ return options.join("\n\n");
3989
+ }
3990
+ function createConfigFromExplicit(config) {
3991
+ const computesdkApiKey = config.computesdkApiKey || config.apiKey;
3992
+ if (!computesdkApiKey) {
3993
+ throw new Error(
3994
+ `Missing ComputeSDK API key. Set 'computesdkApiKey' in your config.
3995
+
3996
+ Example:
3997
+ compute.setConfig({
3998
+ provider: 'e2b',
3999
+ computesdkApiKey: process.env.COMPUTESDK_API_KEY,
4000
+ e2b: { apiKey: process.env.E2B_API_KEY }
4001
+ })
4002
+
4003
+ Get your API key at: https://computesdk.com/dashboard`
4004
+ );
4005
+ }
4006
+ validateProviderConfig(config);
4007
+ const providerHeaders = buildProviderHeaders2(config);
4008
+ return {
4009
+ apiKey: computesdkApiKey,
4010
+ gatewayUrl: config.gatewayUrl || GATEWAY_URL,
4011
+ provider: config.provider,
4012
+ providerHeaders,
4013
+ requestTimeoutMs: config.requestTimeoutMs,
4014
+ WebSocket: config.WebSocket
4015
+ };
245
4016
  }
246
- async function retry(fn, options = {}) {
247
- const {
248
- maxAttempts = 3,
249
- delay = 1e3,
250
- backoff = 2,
251
- onRetry
252
- } = options;
253
- let lastError;
254
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
4017
+
4018
+ // src/compute-daemon/lifecycle.ts
4019
+ async function waitForComputeReady(client, options = {}) {
4020
+ const maxRetries = options.maxRetries ?? 30;
4021
+ const initialDelayMs = options.initialDelayMs ?? 500;
4022
+ const maxDelayMs = options.maxDelayMs ?? 5e3;
4023
+ const backoffFactor = options.backoffFactor ?? 1.5;
4024
+ let lastError = null;
4025
+ let currentDelay = initialDelayMs;
4026
+ for (let i = 0; i < maxRetries; i++) {
255
4027
  try {
256
- return await fn();
4028
+ await client.health();
4029
+ if (process.env.COMPUTESDK_DEBUG) {
4030
+ console.log(`[Lifecycle] Sandbox ready after ${i + 1} attempt${i === 0 ? "" : "s"}`);
4031
+ }
4032
+ return;
257
4033
  } catch (error) {
258
4034
  lastError = error instanceof Error ? error : new Error(String(error));
259
- if (attempt === maxAttempts - 1) {
260
- throw lastError;
261
- }
262
- if (onRetry) {
263
- onRetry(lastError, attempt + 1);
4035
+ if (i === maxRetries - 1) {
4036
+ throw new Error(
4037
+ `Sandbox failed to become ready after ${maxRetries} attempts.
4038
+ Last error: ${lastError.message}
4039
+
4040
+ Possible causes:
4041
+ 1. Sandbox failed to start (check provider dashboard for errors)
4042
+ 2. Network connectivity issues between your app and the sandbox
4043
+ 3. Sandbox is taking longer than expected to initialize
4044
+ 4. Invalid sandbox URL or authentication credentials
4045
+
4046
+ Troubleshooting:
4047
+ - Check sandbox logs in your provider dashboard
4048
+ - Verify your network connection
4049
+ - Try increasing maxRetries if initialization is slow
4050
+ - Enable debug mode: export COMPUTESDK_DEBUG=1`
4051
+ );
264
4052
  }
265
- const currentDelay = delay * Math.pow(backoff, attempt);
266
4053
  await new Promise((resolve) => setTimeout(resolve, currentDelay));
4054
+ currentDelay = Math.min(currentDelay * backoffFactor, maxDelayMs);
267
4055
  }
268
4056
  }
269
- throw lastError;
270
4057
  }
271
4058
 
272
- // src/registry.ts
273
- function createComputeRegistry(providers) {
274
- if (!providers || Object.keys(providers).length === 0) {
275
- throw new ConfigurationError("Provider registry requires at least one provider", "registry");
4059
+ // src/compute.ts
4060
+ async function gatewayFetch(url, config, options = {}) {
4061
+ const timeout = config.requestTimeoutMs ?? 3e4;
4062
+ const controller = new AbortController();
4063
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
4064
+ try {
4065
+ const response = await fetch(url, {
4066
+ ...options,
4067
+ signal: controller.signal,
4068
+ headers: {
4069
+ "Content-Type": "application/json",
4070
+ "X-ComputeSDK-API-Key": config.apiKey,
4071
+ "X-Provider": config.provider,
4072
+ ...config.providerHeaders,
4073
+ ...options.headers
4074
+ }
4075
+ });
4076
+ clearTimeout(timeoutId);
4077
+ if (!response.ok) {
4078
+ if (response.status === 404) {
4079
+ return { success: false };
4080
+ }
4081
+ const errorText = await response.text().catch(() => response.statusText);
4082
+ let errorMessage = `Gateway API error: ${errorText}`;
4083
+ if (response.status === 401) {
4084
+ errorMessage = `Invalid ComputeSDK API key. Check your COMPUTESDK_API_KEY environment variable.`;
4085
+ } else if (response.status === 403) {
4086
+ errorMessage = `Access forbidden. Your API key may not have permission to use provider "${config.provider}".`;
4087
+ }
4088
+ throw new Error(errorMessage);
4089
+ }
4090
+ return await response.json();
4091
+ } catch (error) {
4092
+ clearTimeout(timeoutId);
4093
+ if (error instanceof Error && error.name === "AbortError") {
4094
+ throw new Error(`Request timed out after ${timeout}ms`);
4095
+ }
4096
+ throw error;
276
4097
  }
277
- function sandbox(id) {
278
- const parts = id.split(":");
279
- if (parts.length < 1) {
280
- throw new ConfigurationError(`Invalid sandbox ID format: ${id}`, "registry");
4098
+ }
4099
+ async function waitForSandboxStatus(config, endpoint, body, options = {}) {
4100
+ const maxWaitMs = options.maxWaitMs ?? 6e4;
4101
+ const initialDelayMs = 500;
4102
+ const maxDelayMs = 2e3;
4103
+ const backoffFactor = 1.5;
4104
+ const startTime = Date.now();
4105
+ let currentDelay = initialDelayMs;
4106
+ while (Date.now() - startTime < maxWaitMs) {
4107
+ const result = await gatewayFetch(endpoint, config, {
4108
+ method: "POST",
4109
+ body: JSON.stringify(body)
4110
+ });
4111
+ if (!result.success || !result.data) {
4112
+ return result;
281
4113
  }
282
- const providerName = parts[0];
283
- const providerFactory = providers[providerName];
284
- if (!providerFactory) {
285
- const availableProviders = Object.keys(providers).join(", ");
286
- throw new ConfigurationError(
287
- `Provider '${providerName}' not found in registry. Available providers: ${availableProviders}`,
288
- "registry"
289
- );
4114
+ if (result.data.status !== "creating") {
4115
+ return result;
290
4116
  }
291
- if (parts.length === 1) {
292
- return providerFactory();
293
- } else if (parts.length === 2) {
294
- const runtimeOrImage = parts[1];
295
- if (isRuntime(runtimeOrImage)) {
296
- return providerFactory({ runtime: runtimeOrImage });
297
- }
298
- return providerFactory({ container: { image: runtimeOrImage } });
299
- } else {
300
- const containerImage = parts.slice(1).join(":");
301
- return providerFactory({ container: { image: containerImage } });
4117
+ if (process.env.COMPUTESDK_DEBUG) {
4118
+ console.log(`[Compute] Sandbox still creating, waiting ${currentDelay}ms...`);
302
4119
  }
4120
+ await new Promise((resolve) => setTimeout(resolve, currentDelay));
4121
+ currentDelay = Math.min(currentDelay * backoffFactor, maxDelayMs);
303
4122
  }
304
- return { sandbox };
305
- }
306
- function isRuntime(value) {
307
- return ["node", "python"].includes(value);
4123
+ throw new Error(
4124
+ `Sandbox is still being created after ${maxWaitMs}ms. This may indicate the sandbox failed to start. Check your provider dashboard.`
4125
+ );
308
4126
  }
309
-
310
- // src/providers/base.ts
311
- import { v4 as uuidv4 } from "uuid";
312
- var BaseProvider = class {
313
- /**
314
- * Create a new base provider
315
- *
316
- * @param provider Provider identifier
317
- * @param timeout Execution timeout in milliseconds
318
- */
319
- constructor(provider, timeout) {
320
- /** Specification version */
321
- this.specificationVersion = "v1";
322
- this.provider = provider;
323
- this.sandboxId = uuidv4();
324
- this.timeout = timeout;
4127
+ var ComputeManager = class {
4128
+ constructor() {
4129
+ this.config = null;
4130
+ this.autoConfigured = false;
4131
+ this.sandbox = {
4132
+ /**
4133
+ * Create a new sandbox
4134
+ *
4135
+ * @example
4136
+ * ```typescript
4137
+ * const sandbox = await compute.sandbox.create({
4138
+ * directory: '/custom/path',
4139
+ * overlays: [
4140
+ * {
4141
+ * source: '/templates/nextjs',
4142
+ * target: 'app',
4143
+ * strategy: 'smart',
4144
+ * },
4145
+ * ],
4146
+ * servers: [
4147
+ * {
4148
+ * slug: 'web',
4149
+ * start: 'npm run dev',
4150
+ * path: '/app',
4151
+ * },
4152
+ * ],
4153
+ * });
4154
+ * ```
4155
+ */
4156
+ create: async (options) => {
4157
+ const config = this.getGatewayConfig();
4158
+ const result = await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes`, config, {
4159
+ method: "POST",
4160
+ body: JSON.stringify(options || {})
4161
+ });
4162
+ if (!result.success || !result.data) {
4163
+ throw new Error(`Gateway returned invalid response`);
4164
+ }
4165
+ const { sandboxId, url, token, provider, metadata, name, namespace, overlays, servers } = result.data;
4166
+ const sandbox = new Sandbox({
4167
+ sandboxUrl: url,
4168
+ sandboxId,
4169
+ provider,
4170
+ token: token || config.apiKey,
4171
+ metadata: {
4172
+ ...metadata,
4173
+ ...name && { name },
4174
+ ...namespace && { namespace },
4175
+ ...overlays && { overlays },
4176
+ ...servers && { servers }
4177
+ },
4178
+ WebSocket: config.WebSocket || globalThis.WebSocket,
4179
+ destroyHandler: async () => {
4180
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
4181
+ method: "DELETE"
4182
+ });
4183
+ }
4184
+ });
4185
+ await waitForComputeReady(sandbox);
4186
+ return sandbox;
4187
+ },
4188
+ /**
4189
+ * Get an existing sandbox by ID
4190
+ */
4191
+ getById: async (sandboxId) => {
4192
+ const config = this.getGatewayConfig();
4193
+ const result = await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config);
4194
+ if (!result.success || !result.data) {
4195
+ return null;
4196
+ }
4197
+ const { url, token, provider, metadata } = result.data;
4198
+ const sandbox = new Sandbox({
4199
+ sandboxUrl: url,
4200
+ sandboxId,
4201
+ provider,
4202
+ token: token || config.apiKey,
4203
+ metadata,
4204
+ WebSocket: config.WebSocket || globalThis.WebSocket,
4205
+ destroyHandler: async () => {
4206
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
4207
+ method: "DELETE"
4208
+ });
4209
+ }
4210
+ });
4211
+ await waitForComputeReady(sandbox);
4212
+ return sandbox;
4213
+ },
4214
+ /**
4215
+ * List all active sandboxes
4216
+ */
4217
+ list: async () => {
4218
+ throw new Error(
4219
+ "The gateway does not support listing sandboxes. Use getById() with a known sandbox ID instead."
4220
+ );
4221
+ },
4222
+ /**
4223
+ * Destroy a sandbox
4224
+ */
4225
+ destroy: async (sandboxId) => {
4226
+ const config = this.getGatewayConfig();
4227
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
4228
+ method: "DELETE"
4229
+ });
4230
+ },
4231
+ /**
4232
+ * Find existing or create new sandbox by (namespace, name)
4233
+ */
4234
+ findOrCreate: async (options) => {
4235
+ const config = this.getGatewayConfig();
4236
+ const { name, namespace, ...restOptions } = options;
4237
+ const result = await waitForSandboxStatus(
4238
+ config,
4239
+ `${config.gatewayUrl}/v1/sandboxes/find-or-create`,
4240
+ {
4241
+ namespace: namespace || "default",
4242
+ name,
4243
+ ...restOptions
4244
+ }
4245
+ );
4246
+ if (!result.success || !result.data) {
4247
+ throw new Error(`Gateway returned invalid response`);
4248
+ }
4249
+ const { sandboxId, url, token, provider, metadata } = result.data;
4250
+ const sandbox = new Sandbox({
4251
+ sandboxUrl: url,
4252
+ sandboxId,
4253
+ provider,
4254
+ token: token || config.apiKey,
4255
+ metadata: {
4256
+ ...metadata,
4257
+ name: result.data.name,
4258
+ namespace: result.data.namespace
4259
+ },
4260
+ WebSocket: config.WebSocket || globalThis.WebSocket,
4261
+ destroyHandler: async () => {
4262
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
4263
+ method: "DELETE"
4264
+ });
4265
+ }
4266
+ });
4267
+ await waitForComputeReady(sandbox);
4268
+ return sandbox;
4269
+ },
4270
+ /**
4271
+ * Find existing sandbox by (namespace, name) without creating
4272
+ */
4273
+ find: async (options) => {
4274
+ const config = this.getGatewayConfig();
4275
+ const result = await waitForSandboxStatus(
4276
+ config,
4277
+ `${config.gatewayUrl}/v1/sandboxes/find`,
4278
+ {
4279
+ namespace: options.namespace || "default",
4280
+ name: options.name
4281
+ }
4282
+ );
4283
+ if (!result.success || !result.data) {
4284
+ return null;
4285
+ }
4286
+ const { sandboxId, url, token, provider, metadata, name, namespace } = result.data;
4287
+ const sandbox = new Sandbox({
4288
+ sandboxUrl: url,
4289
+ sandboxId,
4290
+ provider,
4291
+ token: token || config.apiKey,
4292
+ metadata: {
4293
+ ...metadata,
4294
+ name,
4295
+ namespace
4296
+ },
4297
+ WebSocket: config.WebSocket || globalThis.WebSocket,
4298
+ destroyHandler: async () => {
4299
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
4300
+ method: "DELETE"
4301
+ });
4302
+ }
4303
+ });
4304
+ await waitForComputeReady(sandbox);
4305
+ return sandbox;
4306
+ },
4307
+ /**
4308
+ * Extend sandbox timeout/expiration
4309
+ */
4310
+ extendTimeout: async (sandboxId, options) => {
4311
+ const config = this.getGatewayConfig();
4312
+ const duration = options?.duration ?? 9e5;
4313
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}/extend`, config, {
4314
+ method: "POST",
4315
+ body: JSON.stringify({ duration })
4316
+ });
4317
+ }
4318
+ };
325
4319
  }
326
4320
  /**
327
- * Execute code in the sandbox
328
- *
329
- * @param code Code to execute
330
- * @param runtime Optional runtime to use
331
- * @returns Execution result
4321
+ * Lazy auto-configure from environment if not explicitly configured
332
4322
  */
333
- async execute(code, runtime) {
334
- const startTime = Date.now();
335
- try {
336
- const timeoutPromise = new Promise((_, reject) => {
337
- setTimeout(() => {
338
- reject(new TimeoutError(
339
- `Execution timed out after ${this.timeout}ms`,
340
- this.provider,
341
- this.timeout,
342
- this.sandboxId
343
- ));
344
- }, this.timeout);
345
- });
346
- const result = await Promise.race([
347
- this.doExecute(code, runtime),
348
- timeoutPromise
349
- ]);
350
- const executionTime = Date.now() - startTime;
351
- return {
352
- ...result,
353
- executionTime,
354
- sandboxId: this.sandboxId,
355
- provider: this.provider
356
- };
357
- } catch (error) {
358
- if (error instanceof Error && error.name.includes("Error") && "code" in error) {
359
- throw error;
360
- }
361
- throw new ProviderError(
362
- `Execution failed: ${error instanceof Error ? error.message : String(error)}`,
363
- this.provider,
364
- error instanceof Error ? error : void 0,
365
- this.sandboxId
366
- );
4323
+ ensureConfigured() {
4324
+ if (this.config) return;
4325
+ if (this.autoConfigured) return;
4326
+ const config = autoConfigureCompute();
4327
+ this.autoConfigured = true;
4328
+ if (config) {
4329
+ this.config = config;
367
4330
  }
368
4331
  }
369
4332
  /**
370
- * Kill the sandbox
371
- *
372
- * @returns Promise that resolves when the sandbox is killed
4333
+ * Get gateway config, throwing if not configured
373
4334
  */
374
- async kill() {
375
- try {
376
- await this.doKill();
377
- } catch (error) {
378
- throw new ProviderError(
379
- `Failed to kill sandbox: ${error instanceof Error ? error.message : String(error)}`,
380
- this.provider,
381
- error instanceof Error ? error : void 0,
382
- this.sandboxId
4335
+ getGatewayConfig() {
4336
+ this.ensureConfigured();
4337
+ if (!this.config) {
4338
+ throw new Error(
4339
+ `No ComputeSDK configuration found.
4340
+
4341
+ Options:
4342
+ 1. Zero-config: Set COMPUTESDK_API_KEY and provider credentials (e.g., E2B_API_KEY)
4343
+ 2. Explicit: Call compute.setConfig({ provider: "e2b", computesdkApiKey: "...", e2b: { apiKey: "..." } })
4344
+ 3. Use provider directly: import { e2b } from '@computesdk/e2b'
4345
+
4346
+ Docs: https://computesdk.com/docs/quickstart`
383
4347
  );
384
4348
  }
4349
+ return this.config;
385
4350
  }
386
4351
  /**
387
- * Get information about the sandbox
4352
+ * Explicitly configure the compute singleton
388
4353
  *
389
- * @returns Sandbox information
390
- */
391
- async getInfo() {
392
- try {
393
- return await this.doGetInfo();
394
- } catch (error) {
395
- throw new ProviderError(
396
- `Failed to get sandbox info: ${error instanceof Error ? error.message : String(error)}`,
397
- this.provider,
398
- error instanceof Error ? error : void 0,
399
- this.sandboxId
400
- );
401
- }
402
- }
403
- /**
404
- * Execute code in a runtime environment
4354
+ * @example
4355
+ * ```typescript
4356
+ * import { compute } from 'computesdk';
405
4357
  *
406
- * @param code Code to execute
407
- * @param runtime Optional runtime to use
408
- * @returns Execution result
409
- */
410
- async runCode(code, runtime) {
411
- const startTime = Date.now();
412
- try {
413
- const timeoutPromise = new Promise((_, reject) => {
414
- setTimeout(() => {
415
- reject(new TimeoutError(
416
- `Code execution timed out after ${this.timeout}ms`,
417
- this.provider,
418
- this.timeout,
419
- this.sandboxId
420
- ));
421
- }, this.timeout);
422
- });
423
- const result = await Promise.race([
424
- this.doRunCode ? this.doRunCode(code, runtime) : this.doExecute(code, runtime),
425
- timeoutPromise
426
- ]);
427
- const executionTime = Date.now() - startTime;
428
- return {
429
- ...result,
430
- executionTime,
431
- sandboxId: this.sandboxId,
432
- provider: this.provider
433
- };
434
- } catch (error) {
435
- if (error instanceof Error && error.name.includes("Error") && "code" in error) {
436
- throw error;
437
- }
438
- throw new ProviderError(
439
- `Code execution failed: ${error instanceof Error ? error.message : String(error)}`,
440
- this.provider,
441
- error instanceof Error ? error : void 0,
442
- this.sandboxId
443
- );
444
- }
445
- }
446
- /**
447
- * Execute shell commands
4358
+ * compute.setConfig({
4359
+ * provider: 'e2b',
4360
+ * apiKey: 'computesdk_xxx',
4361
+ * e2b: { apiKey: 'e2b_xxx' }
4362
+ * });
448
4363
  *
449
- * @param command Command to execute
450
- * @param args Command arguments
451
- * @returns Execution result
4364
+ * const sandbox = await compute.sandbox.create();
4365
+ * ```
452
4366
  */
453
- async runCommand(command, args = []) {
454
- const startTime = Date.now();
455
- try {
456
- const timeoutPromise = new Promise((_, reject) => {
457
- setTimeout(() => {
458
- reject(new TimeoutError(
459
- `Command execution timed out after ${this.timeout}ms`,
460
- this.provider,
461
- this.timeout,
462
- this.sandboxId
463
- ));
464
- }, this.timeout);
465
- });
466
- const result = await Promise.race([
467
- this.doRunCommand ? this.doRunCommand(command, args) : this.doExecute(`${command} ${args.join(" ")}`, "node"),
468
- timeoutPromise
469
- ]);
470
- const executionTime = Date.now() - startTime;
471
- return {
472
- ...result,
473
- executionTime,
474
- sandboxId: this.sandboxId,
475
- provider: this.provider
476
- };
477
- } catch (error) {
478
- if (error instanceof Error && error.name.includes("Error") && "code" in error) {
479
- throw error;
480
- }
481
- throw new ProviderError(
482
- `Command execution failed: ${error instanceof Error ? error.message : String(error)}`,
483
- this.provider,
484
- error instanceof Error ? error : void 0,
485
- this.sandboxId
486
- );
487
- }
488
- }
489
- };
490
- var BaseFileSystem = class {
491
- constructor(provider, sandboxId) {
492
- this.provider = provider;
493
- this.sandboxId = sandboxId;
494
- }
495
- async readFile(path) {
496
- try {
497
- return await this.doReadFile(path);
498
- } catch (error) {
499
- throw new ProviderError(
500
- `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
501
- this.provider,
502
- error instanceof Error ? error : void 0,
503
- this.sandboxId
504
- );
505
- }
506
- }
507
- async writeFile(path, content) {
508
- try {
509
- await this.doWriteFile(path, content);
510
- } catch (error) {
511
- throw new ProviderError(
512
- `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
513
- this.provider,
514
- error instanceof Error ? error : void 0,
515
- this.sandboxId
516
- );
517
- }
518
- }
519
- async mkdir(path) {
520
- try {
521
- await this.doMkdir(path);
522
- } catch (error) {
523
- throw new ProviderError(
524
- `Failed to create directory: ${error instanceof Error ? error.message : String(error)}`,
525
- this.provider,
526
- error instanceof Error ? error : void 0,
527
- this.sandboxId
528
- );
529
- }
530
- }
531
- async readdir(path) {
532
- try {
533
- return await this.doReaddir(path);
534
- } catch (error) {
535
- throw new ProviderError(
536
- `Failed to read directory: ${error instanceof Error ? error.message : String(error)}`,
537
- this.provider,
538
- error instanceof Error ? error : void 0,
539
- this.sandboxId
540
- );
541
- }
542
- }
543
- async exists(path) {
544
- try {
545
- return await this.doExists(path);
546
- } catch (error) {
547
- return false;
548
- }
549
- }
550
- async remove(path) {
551
- try {
552
- await this.doRemove(path);
553
- } catch (error) {
554
- throw new ProviderError(
555
- `Failed to remove: ${error instanceof Error ? error.message : String(error)}`,
556
- this.provider,
557
- error instanceof Error ? error : void 0,
558
- this.sandboxId
559
- );
560
- }
4367
+ setConfig(config) {
4368
+ const gatewayConfig = createConfigFromExplicit(config);
4369
+ this.config = gatewayConfig;
4370
+ this.autoConfigured = false;
561
4371
  }
562
4372
  };
563
- var BaseTerminal = class {
564
- constructor(provider, sandboxId) {
565
- this.provider = provider;
566
- this.sandboxId = sandboxId;
567
- }
568
- async create(options) {
569
- try {
570
- return await this.doCreate(options);
571
- } catch (error) {
572
- throw new ProviderError(
573
- `Failed to create terminal: ${error instanceof Error ? error.message : String(error)}`,
574
- this.provider,
575
- error instanceof Error ? error : void 0,
576
- this.sandboxId
577
- );
578
- }
579
- }
580
- async list() {
581
- try {
582
- return await this.doList();
583
- } catch (error) {
584
- throw new ProviderError(
585
- `Failed to list terminals: ${error instanceof Error ? error.message : String(error)}`,
586
- this.provider,
587
- error instanceof Error ? error : void 0,
588
- this.sandboxId
589
- );
590
- }
591
- }
592
- };
593
-
594
- // src/sdk.ts
595
- var ComputeSDK = class {
596
- /**
597
- * Create a new sandbox with the specified configuration
598
- *
599
- * @param config Optional sandbox configuration
600
- * @returns Configured sandbox instance
601
- */
602
- static createSandbox(config) {
603
- const normalizedConfig = normalizeSandboxConfig(config);
604
- const providerName = normalizedConfig.provider;
605
- try {
606
- const providerPackage = `@computesdk/${providerName}`;
607
- const provider = __require(providerPackage);
608
- const factory = provider[providerName];
609
- if (!factory) {
610
- throw new ConfigurationError(
611
- `Provider package ${providerPackage} does not export a '${providerName}' function`,
612
- "sdk"
613
- );
614
- }
615
- return factory(normalizedConfig);
616
- } catch (error) {
617
- if (error instanceof ConfigurationError) {
618
- throw error;
619
- }
620
- if (error.code === "MODULE_NOT_FOUND") {
621
- throw new ConfigurationError(
622
- `Provider '${providerName}' not installed. Run: npm install @computesdk/${providerName}`,
623
- "sdk"
624
- );
4373
+ var singletonInstance = new ComputeManager();
4374
+ function computeFactory(config) {
4375
+ const gatewayConfig = createConfigFromExplicit(config);
4376
+ const manager = new ComputeManager();
4377
+ manager["config"] = gatewayConfig;
4378
+ return manager;
4379
+ }
4380
+ var compute = new Proxy(
4381
+ computeFactory,
4382
+ {
4383
+ get(_target, prop, _receiver) {
4384
+ const singleton = singletonInstance;
4385
+ const value = singleton[prop];
4386
+ if (typeof value === "function") {
4387
+ return value.bind(singletonInstance);
625
4388
  }
626
- throw new ConfigurationError(
627
- `Failed to load provider '${providerName}': ${error.message}`,
628
- "sdk"
629
- );
4389
+ return value;
4390
+ },
4391
+ apply(_target, _thisArg, args) {
4392
+ return computeFactory(args[0]);
630
4393
  }
631
4394
  }
632
- /**
633
- * Detect available providers based on environment variables
634
- *
635
- * @returns Array of available provider types
636
- */
637
- static detectProviders() {
638
- return detectAvailableProviders();
639
- }
640
- };
641
-
642
- // src/index.ts
643
- var index_default = ComputeSDK;
4395
+ );
644
4396
  export {
645
- AuthenticationError,
646
- BaseFileSystem,
647
- BaseProvider,
648
- BaseTerminal,
649
- ComputeError,
650
- ComputeSDK,
651
- ConfigurationError,
652
- DEFAULT_TIMEOUT,
653
- ENV_KEYS,
654
- ExecutionError,
655
- ProviderError,
656
- ProviderUnavailableError,
657
- TimeoutError,
658
- autoSelectProvider,
659
- createComputeRegistry,
660
- index_default as default,
661
- detectAvailableProviders,
662
- executeSandbox,
663
- getDefaultRuntime,
664
- normalizeContainerConfig,
665
- normalizeSandboxConfig,
666
- retry,
667
- runCode,
668
- runCommand,
669
- validateProviderApiKey
4397
+ CommandExitError,
4398
+ FileWatcher,
4399
+ GATEWAY_URL,
4400
+ Sandbox as GatewaySandbox,
4401
+ MessageType,
4402
+ PROVIDER_AUTH,
4403
+ PROVIDER_DASHBOARD_URLS,
4404
+ PROVIDER_ENV_MAP,
4405
+ PROVIDER_ENV_VARS,
4406
+ PROVIDER_HEADERS,
4407
+ PROVIDER_NAMES,
4408
+ PROVIDER_PRIORITY,
4409
+ Sandbox,
4410
+ SignalService,
4411
+ TerminalInstance,
4412
+ autoConfigureCompute,
4413
+ buildProviderHeaders,
4414
+ buildSetupPayload,
4415
+ compute,
4416
+ decodeBinaryMessage,
4417
+ detectProvider,
4418
+ encodeBinaryMessage,
4419
+ encodeSetupPayload,
4420
+ getMissingEnvVars,
4421
+ getProviderConfigFromEnv,
4422
+ getProviderHeaders,
4423
+ isCommandExitError,
4424
+ isGatewayModeEnabled,
4425
+ isProviderAuthComplete,
4426
+ isValidProvider
670
4427
  };
671
4428
  //# sourceMappingURL=index.mjs.map