computesdk 3.0.0 → 4.0.0

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