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