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