tensorlake 0.4.40 → 0.4.42
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/bin/tensorlake-create-sandbox-image.cjs +6 -5
- package/dist/bin/darwin-arm64/tensorlake +0 -0
- package/dist/bin/darwin-arm64/tl +0 -0
- package/dist/bin/linux-x64/tensorlake +0 -0
- package/dist/bin/linux-x64/tl +0 -0
- package/dist/bin/win32-x64/tensorlake.exe +0 -0
- package/dist/bin/win32-x64/tl.exe +0 -0
- package/dist/index.cjs +1222 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -240
- package/dist/index.d.ts +57 -240
- package/dist/index.js +1206 -50
- package/dist/index.js.map +1 -1
- package/dist/sandbox-image-DKPhc4Lv.d.cts +375 -0
- package/dist/sandbox-image-DKPhc4Lv.d.ts +375 -0
- package/dist/sandbox-image.cjs +2109 -0
- package/dist/sandbox-image.cjs.map +1 -0
- package/dist/sandbox-image.d.cts +1 -0
- package/dist/sandbox-image.d.ts +1 -0
- package/dist/sandbox-image.js +2069 -0
- package/dist/sandbox-image.js.map +1 -0
- package/package.json +5 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -23,10 +33,13 @@ __export(index_exports, {
|
|
|
23
33
|
APIClient: () => APIClient,
|
|
24
34
|
CloudClient: () => CloudClient,
|
|
25
35
|
ContainerState: () => ContainerState,
|
|
36
|
+
Image: () => Image,
|
|
37
|
+
ImageBuildOperationType: () => ImageBuildOperationType,
|
|
26
38
|
OutputMode: () => OutputMode,
|
|
27
39
|
PoolInUseError: () => PoolInUseError,
|
|
28
40
|
PoolNotFoundError: () => PoolNotFoundError,
|
|
29
41
|
ProcessStatus: () => ProcessStatus,
|
|
42
|
+
Pty: () => Pty,
|
|
30
43
|
RemoteAPIError: () => RemoteAPIError,
|
|
31
44
|
RequestExecutionError: () => RequestExecutionError,
|
|
32
45
|
RequestFailedError: () => RequestFailedError,
|
|
@@ -39,7 +52,9 @@ __export(index_exports, {
|
|
|
39
52
|
SandboxNotFoundError: () => SandboxNotFoundError,
|
|
40
53
|
SandboxStatus: () => SandboxStatus,
|
|
41
54
|
SnapshotStatus: () => SnapshotStatus,
|
|
42
|
-
StdinMode: () => StdinMode
|
|
55
|
+
StdinMode: () => StdinMode,
|
|
56
|
+
createSandboxImage: () => createSandboxImage,
|
|
57
|
+
dockerfileContent: () => dockerfileContent
|
|
43
58
|
});
|
|
44
59
|
module.exports = __toCommonJS(index_exports);
|
|
45
60
|
|
|
@@ -141,7 +156,8 @@ var HttpClient = class {
|
|
|
141
156
|
timeoutMs;
|
|
142
157
|
abortController = null;
|
|
143
158
|
constructor(options) {
|
|
144
|
-
|
|
159
|
+
const url = options.baseUrl;
|
|
160
|
+
this.baseUrl = url.endsWith("/") ? url.slice(0, -1) : url;
|
|
145
161
|
this.maxRetries = options.maxRetries ?? MAX_RETRIES;
|
|
146
162
|
this.retryBackoffMs = options.retryBackoffMs ?? RETRY_BACKOFF_MS;
|
|
147
163
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
@@ -164,8 +180,8 @@ var HttpClient = class {
|
|
|
164
180
|
this.abortController = null;
|
|
165
181
|
}
|
|
166
182
|
/** Make a JSON request, returning the parsed response body. */
|
|
167
|
-
async requestJson(method,
|
|
168
|
-
const response = await this.requestResponse(method,
|
|
183
|
+
async requestJson(method, path2, options) {
|
|
184
|
+
const response = await this.requestResponse(method, path2, {
|
|
169
185
|
json: options?.body,
|
|
170
186
|
headers: options?.headers,
|
|
171
187
|
signal: options?.signal
|
|
@@ -175,14 +191,14 @@ var HttpClient = class {
|
|
|
175
191
|
return JSON.parse(text);
|
|
176
192
|
}
|
|
177
193
|
/** Make a request returning raw bytes. */
|
|
178
|
-
async requestBytes(method,
|
|
194
|
+
async requestBytes(method, path2, options) {
|
|
179
195
|
const headers = { ...options?.headers ?? {} };
|
|
180
196
|
if (options?.contentType) {
|
|
181
197
|
headers["Content-Type"] = options.contentType;
|
|
182
198
|
}
|
|
183
199
|
const response = await this.requestResponse(
|
|
184
200
|
method,
|
|
185
|
-
|
|
201
|
+
path2,
|
|
186
202
|
{
|
|
187
203
|
body: options?.body,
|
|
188
204
|
headers,
|
|
@@ -193,10 +209,10 @@ var HttpClient = class {
|
|
|
193
209
|
return new Uint8Array(buffer);
|
|
194
210
|
}
|
|
195
211
|
/** Make a request and return the raw Response (for SSE streaming). */
|
|
196
|
-
async requestStream(method,
|
|
212
|
+
async requestStream(method, path2, options) {
|
|
197
213
|
const response = await this.requestResponse(
|
|
198
214
|
method,
|
|
199
|
-
|
|
215
|
+
path2,
|
|
200
216
|
{
|
|
201
217
|
headers: { Accept: "text/event-stream" },
|
|
202
218
|
signal: options?.signal
|
|
@@ -208,7 +224,7 @@ var HttpClient = class {
|
|
|
208
224
|
return response.body;
|
|
209
225
|
}
|
|
210
226
|
/** Make a request and return the raw Response. */
|
|
211
|
-
async requestResponse(method,
|
|
227
|
+
async requestResponse(method, path2, options) {
|
|
212
228
|
const headers = {
|
|
213
229
|
...this.headers,
|
|
214
230
|
...options?.headers ?? {}
|
|
@@ -220,15 +236,15 @@ var HttpClient = class {
|
|
|
220
236
|
const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
|
|
221
237
|
return this.doFetch(
|
|
222
238
|
method,
|
|
223
|
-
|
|
239
|
+
path2,
|
|
224
240
|
body,
|
|
225
241
|
headers,
|
|
226
242
|
options?.signal,
|
|
227
243
|
options?.allowHttpErrors ?? false
|
|
228
244
|
);
|
|
229
245
|
}
|
|
230
|
-
async doFetch(method,
|
|
231
|
-
const url = `${this.baseUrl}${
|
|
246
|
+
async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
|
|
247
|
+
const url = `${this.baseUrl}${path2}`;
|
|
232
248
|
let lastError;
|
|
233
249
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
234
250
|
if (attempt > 0) {
|
|
@@ -261,7 +277,7 @@ var HttpClient = class {
|
|
|
261
277
|
return response;
|
|
262
278
|
}
|
|
263
279
|
const errorBody = await response.text().catch(() => "");
|
|
264
|
-
throwMappedError(response.status, errorBody,
|
|
280
|
+
throwMappedError(response.status, errorBody, path2);
|
|
265
281
|
} catch (err) {
|
|
266
282
|
clearTimeout(timeoutId);
|
|
267
283
|
if (err instanceof RemoteAPIError || err instanceof SandboxNotFoundError || err instanceof PoolNotFoundError || err instanceof PoolInUseError) {
|
|
@@ -292,7 +308,7 @@ function normalizeRequestBody(body) {
|
|
|
292
308
|
}
|
|
293
309
|
return body;
|
|
294
310
|
}
|
|
295
|
-
function throwMappedError(status, body,
|
|
311
|
+
function throwMappedError(status, body, path2) {
|
|
296
312
|
let message = body;
|
|
297
313
|
try {
|
|
298
314
|
const parsed = JSON.parse(body);
|
|
@@ -301,19 +317,19 @@ function throwMappedError(status, body, path) {
|
|
|
301
317
|
} catch {
|
|
302
318
|
}
|
|
303
319
|
if (status === 404) {
|
|
304
|
-
if (
|
|
305
|
-
const match =
|
|
320
|
+
if (path2.includes("sandbox-pools") || path2.includes("pools")) {
|
|
321
|
+
const match = path2.match(/sandbox-pools\/([^/]+)/);
|
|
306
322
|
if (match) throw new PoolNotFoundError(match[1]);
|
|
307
323
|
}
|
|
308
|
-
if (
|
|
309
|
-
const match =
|
|
324
|
+
if (path2.includes("sandboxes")) {
|
|
325
|
+
const match = path2.match(/sandboxes\/([^/]+)/);
|
|
310
326
|
if (match) throw new SandboxNotFoundError(match[1]);
|
|
311
327
|
}
|
|
312
328
|
throw new RemoteAPIError(404, message);
|
|
313
329
|
}
|
|
314
330
|
if (status === 409) {
|
|
315
|
-
if (
|
|
316
|
-
const match =
|
|
331
|
+
if (path2.includes("sandbox-pools") || path2.includes("pools")) {
|
|
332
|
+
const match = path2.match(/sandbox-pools\/([^/]+)/);
|
|
317
333
|
if (match) throw new PoolInUseError(match[1], message);
|
|
318
334
|
}
|
|
319
335
|
}
|
|
@@ -341,6 +357,7 @@ var SandboxStatus = /* @__PURE__ */ ((SandboxStatus2) => {
|
|
|
341
357
|
SandboxStatus2["PENDING"] = "pending";
|
|
342
358
|
SandboxStatus2["RUNNING"] = "running";
|
|
343
359
|
SandboxStatus2["SNAPSHOTTING"] = "snapshotting";
|
|
360
|
+
SandboxStatus2["SUSPENDING"] = "suspending";
|
|
344
361
|
SandboxStatus2["SUSPENDED"] = "suspended";
|
|
345
362
|
SandboxStatus2["TERMINATED"] = "terminated";
|
|
346
363
|
return SandboxStatus2;
|
|
@@ -468,6 +485,9 @@ async function* parseSSEStream(stream, signal) {
|
|
|
468
485
|
}
|
|
469
486
|
|
|
470
487
|
// src/url.ts
|
|
488
|
+
function trimTrailingSlashes(url) {
|
|
489
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
490
|
+
}
|
|
471
491
|
function isLocalhost(apiUrl) {
|
|
472
492
|
try {
|
|
473
493
|
const parsed = new URL(apiUrl);
|
|
@@ -497,7 +517,7 @@ function resolveProxyTarget(proxyUrl, sandboxId) {
|
|
|
497
517
|
const host = parsed.hostname;
|
|
498
518
|
if (host === "localhost" || host === "127.0.0.1") {
|
|
499
519
|
return {
|
|
500
|
-
baseUrl: proxyUrl
|
|
520
|
+
baseUrl: trimTrailingSlashes(proxyUrl),
|
|
501
521
|
hostHeader: `${sandboxId}.local`
|
|
502
522
|
};
|
|
503
523
|
}
|
|
@@ -508,23 +528,210 @@ function resolveProxyTarget(proxyUrl, sandboxId) {
|
|
|
508
528
|
};
|
|
509
529
|
} catch {
|
|
510
530
|
return {
|
|
511
|
-
baseUrl: `${proxyUrl
|
|
531
|
+
baseUrl: `${trimTrailingSlashes(proxyUrl)}/${sandboxId}`,
|
|
512
532
|
hostHeader: void 0
|
|
513
533
|
};
|
|
514
534
|
}
|
|
515
535
|
}
|
|
516
|
-
function lifecyclePath(
|
|
536
|
+
function lifecyclePath(path2, isLocal, namespace) {
|
|
517
537
|
if (isLocal) {
|
|
518
|
-
return `/v1/namespaces/${namespace}/${
|
|
538
|
+
return `/v1/namespaces/${namespace}/${path2}`;
|
|
519
539
|
}
|
|
520
|
-
return `/${
|
|
540
|
+
return `/${path2}`;
|
|
521
541
|
}
|
|
522
542
|
|
|
523
543
|
// src/sandbox.ts
|
|
544
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
545
|
+
var PTY_OP_DATA = 0;
|
|
546
|
+
var PTY_OP_RESIZE = 1;
|
|
547
|
+
var PTY_OP_READY = 2;
|
|
548
|
+
var PTY_OP_EXIT = 3;
|
|
549
|
+
var Pty = class {
|
|
550
|
+
sessionId;
|
|
551
|
+
token;
|
|
552
|
+
wsUrl;
|
|
553
|
+
wsHeaders;
|
|
554
|
+
killSession;
|
|
555
|
+
socket = null;
|
|
556
|
+
connectPromise = null;
|
|
557
|
+
intentionalDisconnect = false;
|
|
558
|
+
exitCode = null;
|
|
559
|
+
waitSettled = false;
|
|
560
|
+
dataHandlers = /* @__PURE__ */ new Set();
|
|
561
|
+
exitHandlers = /* @__PURE__ */ new Set();
|
|
562
|
+
waitPromise;
|
|
563
|
+
resolveWait;
|
|
564
|
+
rejectWait;
|
|
565
|
+
constructor(options) {
|
|
566
|
+
this.sessionId = options.sessionId;
|
|
567
|
+
this.token = options.token;
|
|
568
|
+
this.wsUrl = options.wsUrl;
|
|
569
|
+
this.wsHeaders = options.wsHeaders;
|
|
570
|
+
this.killSession = options.killSession;
|
|
571
|
+
this.waitPromise = new Promise((resolve, reject) => {
|
|
572
|
+
this.resolveWait = resolve;
|
|
573
|
+
this.rejectWait = reject;
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
onData(handler) {
|
|
577
|
+
this.dataHandlers.add(handler);
|
|
578
|
+
return () => this.dataHandlers.delete(handler);
|
|
579
|
+
}
|
|
580
|
+
onExit(handler) {
|
|
581
|
+
this.exitHandlers.add(handler);
|
|
582
|
+
if (this.exitCode != null) {
|
|
583
|
+
queueMicrotask(() => handler(this.exitCode));
|
|
584
|
+
}
|
|
585
|
+
return () => this.exitHandlers.delete(handler);
|
|
586
|
+
}
|
|
587
|
+
async connect() {
|
|
588
|
+
if (this.socket?.readyState === import_ws.default.OPEN) {
|
|
589
|
+
return this;
|
|
590
|
+
}
|
|
591
|
+
if (this.connectPromise) {
|
|
592
|
+
return this.connectPromise;
|
|
593
|
+
}
|
|
594
|
+
this.intentionalDisconnect = false;
|
|
595
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
596
|
+
let opened = false;
|
|
597
|
+
const socket = new import_ws.default(this.wsUrl, {
|
|
598
|
+
headers: this.wsHeaders
|
|
599
|
+
});
|
|
600
|
+
this.socket = socket;
|
|
601
|
+
socket.on("open", async () => {
|
|
602
|
+
try {
|
|
603
|
+
await sendPtyFrame(socket, Buffer.from([PTY_OP_READY]));
|
|
604
|
+
opened = true;
|
|
605
|
+
resolve(this);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
reject(error);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
socket.on("message", (message) => {
|
|
611
|
+
const bytes = normalizePtyMessage(message);
|
|
612
|
+
const opcode = bytes[0];
|
|
613
|
+
if (opcode === PTY_OP_DATA) {
|
|
614
|
+
const payload = bytes.subarray(1);
|
|
615
|
+
for (const handler of this.dataHandlers) {
|
|
616
|
+
handler(payload);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (opcode === PTY_OP_EXIT && bytes.length >= 5) {
|
|
621
|
+
this.finishWait(bytes.readInt32BE(1));
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
socket.on("close", (code, reason) => {
|
|
625
|
+
const closeReason = Buffer.isBuffer(reason) ? reason.toString("utf8") : String(reason);
|
|
626
|
+
if (this.socket === socket) {
|
|
627
|
+
this.socket = null;
|
|
628
|
+
}
|
|
629
|
+
this.connectPromise = null;
|
|
630
|
+
if (this.exitCode != null) {
|
|
631
|
+
this.finishWait(this.exitCode);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (closeReason.startsWith("exit:")) {
|
|
635
|
+
const parsed = Number.parseInt(closeReason.slice(5), 10);
|
|
636
|
+
this.finishWait(Number.isNaN(parsed) ? -1 : parsed);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (this.intentionalDisconnect) {
|
|
640
|
+
this.intentionalDisconnect = false;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (!opened) {
|
|
644
|
+
reject(new SandboxError(
|
|
645
|
+
`PTY websocket closed before READY completed: ${code} ${closeReason || "no reason"}`
|
|
646
|
+
));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (closeReason === "session terminated") {
|
|
650
|
+
this.failWait(new SandboxError("PTY session terminated"));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
this.failWait(
|
|
654
|
+
new SandboxError(
|
|
655
|
+
`PTY websocket closed unexpectedly: ${code} ${closeReason || "no reason"}`
|
|
656
|
+
)
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
socket.on("error", (error) => {
|
|
660
|
+
if (!opened) {
|
|
661
|
+
reject(error);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
return this.connectPromise;
|
|
666
|
+
}
|
|
667
|
+
async sendInput(input) {
|
|
668
|
+
const socket = this.requireOpenSocket();
|
|
669
|
+
await sendPtyFrame(socket, encodePtyInput(input));
|
|
670
|
+
}
|
|
671
|
+
async resize(cols, rows) {
|
|
672
|
+
const socket = this.requireOpenSocket();
|
|
673
|
+
await sendPtyFrame(socket, encodePtyResize(cols, rows));
|
|
674
|
+
}
|
|
675
|
+
disconnect(code = 1e3, reason = "client disconnect") {
|
|
676
|
+
if (!this.socket) return;
|
|
677
|
+
this.intentionalDisconnect = true;
|
|
678
|
+
this.socket.close(code, reason);
|
|
679
|
+
}
|
|
680
|
+
wait() {
|
|
681
|
+
return this.waitPromise;
|
|
682
|
+
}
|
|
683
|
+
async kill() {
|
|
684
|
+
await this.killSession();
|
|
685
|
+
}
|
|
686
|
+
requireOpenSocket() {
|
|
687
|
+
if (!this.socket || this.socket.readyState !== import_ws.default.OPEN) {
|
|
688
|
+
throw new SandboxError("PTY is not connected");
|
|
689
|
+
}
|
|
690
|
+
return this.socket;
|
|
691
|
+
}
|
|
692
|
+
finishWait(exitCode) {
|
|
693
|
+
if (this.waitSettled) return;
|
|
694
|
+
this.waitSettled = true;
|
|
695
|
+
this.exitCode = exitCode;
|
|
696
|
+
for (const handler of this.exitHandlers) {
|
|
697
|
+
handler(exitCode);
|
|
698
|
+
}
|
|
699
|
+
this.resolveWait(exitCode);
|
|
700
|
+
}
|
|
701
|
+
failWait(error) {
|
|
702
|
+
if (this.waitSettled) return;
|
|
703
|
+
this.waitSettled = true;
|
|
704
|
+
this.rejectWait(error);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
function normalizePtyMessage(message) {
|
|
708
|
+
if (Buffer.isBuffer(message)) return message;
|
|
709
|
+
if (Array.isArray(message)) {
|
|
710
|
+
return Buffer.concat(message.map((part) => Buffer.from(part)));
|
|
711
|
+
}
|
|
712
|
+
return Buffer.from(message);
|
|
713
|
+
}
|
|
714
|
+
function encodePtyInput(input) {
|
|
715
|
+
const payload = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
|
|
716
|
+
return Buffer.concat([Buffer.from([PTY_OP_DATA]), payload]);
|
|
717
|
+
}
|
|
718
|
+
function encodePtyResize(cols, rows) {
|
|
719
|
+
const frame = Buffer.alloc(5);
|
|
720
|
+
frame[0] = PTY_OP_RESIZE;
|
|
721
|
+
frame.writeUInt16BE(cols, 1);
|
|
722
|
+
frame.writeUInt16BE(rows, 3);
|
|
723
|
+
return frame;
|
|
724
|
+
}
|
|
725
|
+
function sendPtyFrame(socket, frame) {
|
|
726
|
+
return new Promise((resolve, reject) => {
|
|
727
|
+
socket.send(frame, (error) => error ? reject(error) : resolve());
|
|
728
|
+
});
|
|
729
|
+
}
|
|
524
730
|
var Sandbox = class {
|
|
525
731
|
sandboxId;
|
|
526
732
|
http;
|
|
527
733
|
baseUrl;
|
|
734
|
+
wsHeaders;
|
|
528
735
|
ownsSandbox = false;
|
|
529
736
|
lifecycleClient = null;
|
|
530
737
|
constructor(options) {
|
|
@@ -532,6 +739,19 @@ var Sandbox = class {
|
|
|
532
739
|
const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
|
|
533
740
|
const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
|
|
534
741
|
this.baseUrl = baseUrl;
|
|
742
|
+
this.wsHeaders = {};
|
|
743
|
+
if (options.apiKey) {
|
|
744
|
+
this.wsHeaders.Authorization = `Bearer ${options.apiKey}`;
|
|
745
|
+
}
|
|
746
|
+
if (options.organizationId) {
|
|
747
|
+
this.wsHeaders["X-Forwarded-Organization-Id"] = options.organizationId;
|
|
748
|
+
}
|
|
749
|
+
if (options.projectId) {
|
|
750
|
+
this.wsHeaders["X-Forwarded-Project-Id"] = options.projectId;
|
|
751
|
+
}
|
|
752
|
+
if (hostHeader) {
|
|
753
|
+
this.wsHeaders.Host = hostHeader;
|
|
754
|
+
}
|
|
535
755
|
this.http = new HttpClient({
|
|
536
756
|
baseUrl,
|
|
537
757
|
apiKey: options.apiKey,
|
|
@@ -710,29 +930,29 @@ var Sandbox = class {
|
|
|
710
930
|
}
|
|
711
931
|
}
|
|
712
932
|
// --- File operations ---
|
|
713
|
-
async readFile(
|
|
933
|
+
async readFile(path2) {
|
|
714
934
|
return this.http.requestBytes(
|
|
715
935
|
"GET",
|
|
716
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
936
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
717
937
|
);
|
|
718
938
|
}
|
|
719
|
-
async writeFile(
|
|
939
|
+
async writeFile(path2, content) {
|
|
720
940
|
await this.http.requestBytes(
|
|
721
941
|
"PUT",
|
|
722
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
942
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`,
|
|
723
943
|
{ body: content, contentType: "application/octet-stream" }
|
|
724
944
|
);
|
|
725
945
|
}
|
|
726
|
-
async deleteFile(
|
|
946
|
+
async deleteFile(path2) {
|
|
727
947
|
await this.http.requestJson(
|
|
728
948
|
"DELETE",
|
|
729
|
-
`/api/v1/files?path=${encodeURIComponent(
|
|
949
|
+
`/api/v1/files?path=${encodeURIComponent(path2)}`
|
|
730
950
|
);
|
|
731
951
|
}
|
|
732
|
-
async listDirectory(
|
|
952
|
+
async listDirectory(path2) {
|
|
733
953
|
const raw = await this.http.requestJson(
|
|
734
954
|
"GET",
|
|
735
|
-
`/api/v1/files/list?path=${encodeURIComponent(
|
|
955
|
+
`/api/v1/files/list?path=${encodeURIComponent(path2)}`
|
|
736
956
|
);
|
|
737
957
|
return fromSnakeKeys(raw);
|
|
738
958
|
}
|
|
@@ -753,6 +973,43 @@ var Sandbox = class {
|
|
|
753
973
|
);
|
|
754
974
|
return fromSnakeKeys(raw);
|
|
755
975
|
}
|
|
976
|
+
async createPty(options) {
|
|
977
|
+
const { onData, onExit, ...createOptions } = options;
|
|
978
|
+
const session = await this.createPtySession(createOptions);
|
|
979
|
+
try {
|
|
980
|
+
return await this.connectPty(session.sessionId, session.token, { onData, onExit });
|
|
981
|
+
} catch (error) {
|
|
982
|
+
try {
|
|
983
|
+
await this.http.requestResponse("DELETE", `/api/v1/pty/${session.sessionId}`);
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async connectPty(sessionId, token, options) {
|
|
990
|
+
const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
|
|
991
|
+
const authToken = wsUrl.searchParams.get("token") ?? token;
|
|
992
|
+
const pty = new Pty({
|
|
993
|
+
sessionId,
|
|
994
|
+
token: authToken,
|
|
995
|
+
wsUrl: wsUrl.toString(),
|
|
996
|
+
wsHeaders: {
|
|
997
|
+
...this.wsHeaders,
|
|
998
|
+
"X-PTY-Token": authToken
|
|
999
|
+
},
|
|
1000
|
+
killSession: async () => {
|
|
1001
|
+
await this.http.requestResponse("DELETE", `/api/v1/pty/${sessionId}`);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
if (options?.onData) {
|
|
1005
|
+
pty.onData(options.onData);
|
|
1006
|
+
}
|
|
1007
|
+
if (options?.onExit) {
|
|
1008
|
+
pty.onExit(options.onExit);
|
|
1009
|
+
}
|
|
1010
|
+
await pty.connect();
|
|
1011
|
+
return pty;
|
|
1012
|
+
}
|
|
756
1013
|
ptyWsUrl(sessionId, token) {
|
|
757
1014
|
let wsBase;
|
|
758
1015
|
if (this.baseUrl.startsWith("https://")) {
|
|
@@ -880,6 +1137,15 @@ var SandboxClient = class _SandboxClient {
|
|
|
880
1137
|
async update(sandboxId, options) {
|
|
881
1138
|
const body = {};
|
|
882
1139
|
if (options.name != null) body.name = options.name;
|
|
1140
|
+
if (options.allowUnauthenticatedAccess != null) {
|
|
1141
|
+
body.allow_unauthenticated_access = options.allowUnauthenticatedAccess;
|
|
1142
|
+
}
|
|
1143
|
+
if (options.exposedPorts != null) {
|
|
1144
|
+
body.exposed_ports = normalizeUserPorts(options.exposedPorts);
|
|
1145
|
+
}
|
|
1146
|
+
if (Object.keys(body).length === 0) {
|
|
1147
|
+
throw new SandboxError("At least one sandbox update field must be provided.");
|
|
1148
|
+
}
|
|
883
1149
|
const raw = await this.http.requestJson(
|
|
884
1150
|
"PATCH",
|
|
885
1151
|
this.path(`sandboxes/${sandboxId}`),
|
|
@@ -887,12 +1153,54 @@ var SandboxClient = class _SandboxClient {
|
|
|
887
1153
|
);
|
|
888
1154
|
return fromSnakeKeys(raw, "sandboxId");
|
|
889
1155
|
}
|
|
1156
|
+
async getPortAccess(sandboxId) {
|
|
1157
|
+
const info = await this.get(sandboxId);
|
|
1158
|
+
return {
|
|
1159
|
+
allowUnauthenticatedAccess: info.allowUnauthenticatedAccess ?? false,
|
|
1160
|
+
exposedPorts: dedupeAndSortPorts(info.exposedPorts ?? []),
|
|
1161
|
+
sandboxUrl: info.sandboxUrl
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
async exposePorts(sandboxId, ports, options) {
|
|
1165
|
+
const requestedPorts = normalizeUserPorts(ports);
|
|
1166
|
+
const current = await this.getPortAccess(sandboxId);
|
|
1167
|
+
const desiredPorts = dedupeAndSortPorts([
|
|
1168
|
+
...current.exposedPorts,
|
|
1169
|
+
...requestedPorts
|
|
1170
|
+
]);
|
|
1171
|
+
return this.update(sandboxId, {
|
|
1172
|
+
allowUnauthenticatedAccess: options?.allowUnauthenticatedAccess ?? current.allowUnauthenticatedAccess,
|
|
1173
|
+
exposedPorts: desiredPorts
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
async unexposePorts(sandboxId, ports) {
|
|
1177
|
+
const requestedPorts = normalizeUserPorts(ports);
|
|
1178
|
+
const current = await this.getPortAccess(sandboxId);
|
|
1179
|
+
const toRemove = new Set(requestedPorts);
|
|
1180
|
+
const desiredPorts = current.exposedPorts.filter((port) => !toRemove.has(port));
|
|
1181
|
+
return this.update(sandboxId, {
|
|
1182
|
+
allowUnauthenticatedAccess: desiredPorts.length ? current.allowUnauthenticatedAccess : false,
|
|
1183
|
+
exposedPorts: desiredPorts
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
890
1186
|
async delete(sandboxId) {
|
|
891
1187
|
await this.http.requestJson(
|
|
892
1188
|
"DELETE",
|
|
893
1189
|
this.path(`sandboxes/${sandboxId}`)
|
|
894
1190
|
);
|
|
895
1191
|
}
|
|
1192
|
+
async suspend(sandboxId) {
|
|
1193
|
+
await this.http.requestResponse(
|
|
1194
|
+
"POST",
|
|
1195
|
+
this.path(`sandboxes/${sandboxId}/suspend`)
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
async resume(sandboxId) {
|
|
1199
|
+
await this.http.requestResponse(
|
|
1200
|
+
"POST",
|
|
1201
|
+
this.path(`sandboxes/${sandboxId}/resume`)
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
896
1204
|
async claim(poolId) {
|
|
897
1205
|
const raw = await this.http.requestJson(
|
|
898
1206
|
"POST",
|
|
@@ -1015,10 +1323,10 @@ var SandboxClient = class _SandboxClient {
|
|
|
1015
1323
|
);
|
|
1016
1324
|
}
|
|
1017
1325
|
// --- Connect ---
|
|
1018
|
-
connect(
|
|
1326
|
+
connect(identifier, proxyUrl) {
|
|
1019
1327
|
const resolvedProxy = proxyUrl ?? resolveProxyUrl(this.apiUrl);
|
|
1020
1328
|
return new Sandbox({
|
|
1021
|
-
sandboxId,
|
|
1329
|
+
sandboxId: identifier,
|
|
1022
1330
|
proxyUrl: resolvedProxy,
|
|
1023
1331
|
apiKey: this.apiKey,
|
|
1024
1332
|
organizationId: this.organizationId,
|
|
@@ -1060,6 +1368,22 @@ var SandboxClient = class _SandboxClient {
|
|
|
1060
1368
|
function sleep3(ms) {
|
|
1061
1369
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1062
1370
|
}
|
|
1371
|
+
var RESERVED_SANDBOX_MANAGEMENT_PORT = 9501;
|
|
1372
|
+
function normalizeUserPorts(ports) {
|
|
1373
|
+
return dedupeAndSortPorts(ports.map(validateUserPort));
|
|
1374
|
+
}
|
|
1375
|
+
function validateUserPort(port) {
|
|
1376
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1377
|
+
throw new SandboxError(`invalid port '${port}'`);
|
|
1378
|
+
}
|
|
1379
|
+
if (port === RESERVED_SANDBOX_MANAGEMENT_PORT) {
|
|
1380
|
+
throw new SandboxError("port 9501 is reserved for sandbox management");
|
|
1381
|
+
}
|
|
1382
|
+
return port;
|
|
1383
|
+
}
|
|
1384
|
+
function dedupeAndSortPorts(ports) {
|
|
1385
|
+
return [...new Set(ports)].sort((a, b) => a - b);
|
|
1386
|
+
}
|
|
1063
1387
|
|
|
1064
1388
|
// src/cloud-client.ts
|
|
1065
1389
|
var CloudClient = class _CloudClient {
|
|
@@ -1126,19 +1450,19 @@ var CloudClient = class _CloudClient {
|
|
|
1126
1450
|
return fromSnakeKeys(raw);
|
|
1127
1451
|
}
|
|
1128
1452
|
async runRequest(applicationName, inputs = []) {
|
|
1129
|
-
const
|
|
1453
|
+
const path2 = this.namespacePath(
|
|
1130
1454
|
`applications/${encodeURIComponent(applicationName)}`
|
|
1131
1455
|
);
|
|
1132
|
-
const response = inputs.length === 0 ? await this.http.requestResponse("POST",
|
|
1456
|
+
const response = inputs.length === 0 ? await this.http.requestResponse("POST", path2, {
|
|
1133
1457
|
body: new Uint8Array(),
|
|
1134
1458
|
headers: { Accept: "application/json" }
|
|
1135
|
-
}) : inputs.length === 1 && inputs[0].name === "0" ? await this.http.requestResponse("POST",
|
|
1459
|
+
}) : inputs.length === 1 && inputs[0].name === "0" ? await this.http.requestResponse("POST", path2, {
|
|
1136
1460
|
body: toRequestBody(inputs[0].data),
|
|
1137
1461
|
headers: {
|
|
1138
1462
|
Accept: "application/json",
|
|
1139
1463
|
"Content-Type": inputs[0].contentType
|
|
1140
1464
|
}
|
|
1141
|
-
}) : await this.runMultipartRequest(
|
|
1465
|
+
}) : await this.runMultipartRequest(path2, inputs);
|
|
1142
1466
|
const body = await parseJsonResponse(response);
|
|
1143
1467
|
const requestId = body?.request_id;
|
|
1144
1468
|
if (!requestId) {
|
|
@@ -1239,7 +1563,7 @@ var CloudClient = class _CloudClient {
|
|
|
1239
1563
|
);
|
|
1240
1564
|
const response = await this.http.requestResponse(
|
|
1241
1565
|
"PUT",
|
|
1242
|
-
`${
|
|
1566
|
+
`${trimTrailingSlashes2(buildServicePath)}/builds`,
|
|
1243
1567
|
{ body: form }
|
|
1244
1568
|
);
|
|
1245
1569
|
const raw = await parseJsonResponse(response);
|
|
@@ -1249,7 +1573,7 @@ var CloudClient = class _CloudClient {
|
|
|
1249
1573
|
const form = createApplicationBuildForm(request, imageContexts);
|
|
1250
1574
|
const response = await this.http.requestResponse(
|
|
1251
1575
|
"POST",
|
|
1252
|
-
|
|
1576
|
+
trimTrailingSlashes2(buildServicePath),
|
|
1253
1577
|
{ body: form }
|
|
1254
1578
|
);
|
|
1255
1579
|
const raw = await parseJsonResponse(response);
|
|
@@ -1258,41 +1582,41 @@ var CloudClient = class _CloudClient {
|
|
|
1258
1582
|
async applicationBuildInfo(buildServicePath, applicationBuildId) {
|
|
1259
1583
|
const raw = await this.http.requestJson(
|
|
1260
1584
|
"GET",
|
|
1261
|
-
`${
|
|
1585
|
+
`${trimTrailingSlashes2(buildServicePath)}/${encodeURIComponent(applicationBuildId)}`
|
|
1262
1586
|
);
|
|
1263
1587
|
return fromSnakeKeys(raw);
|
|
1264
1588
|
}
|
|
1265
1589
|
async cancelApplicationBuild(buildServicePath, applicationBuildId) {
|
|
1266
1590
|
const raw = await this.http.requestJson(
|
|
1267
1591
|
"POST",
|
|
1268
|
-
`${
|
|
1592
|
+
`${trimTrailingSlashes2(buildServicePath)}/${encodeURIComponent(applicationBuildId)}/cancel`
|
|
1269
1593
|
);
|
|
1270
1594
|
return fromSnakeKeys(raw);
|
|
1271
1595
|
}
|
|
1272
1596
|
async buildInfo(buildServicePath, buildId) {
|
|
1273
1597
|
const raw = await this.http.requestJson(
|
|
1274
1598
|
"GET",
|
|
1275
|
-
`${
|
|
1599
|
+
`${trimTrailingSlashes2(buildServicePath)}/builds/${encodeURIComponent(buildId)}`
|
|
1276
1600
|
);
|
|
1277
1601
|
return fromSnakeKeys(raw);
|
|
1278
1602
|
}
|
|
1279
1603
|
async cancelBuild(buildServicePath, buildId) {
|
|
1280
1604
|
await this.http.requestResponse(
|
|
1281
1605
|
"POST",
|
|
1282
|
-
`${
|
|
1606
|
+
`${trimTrailingSlashes2(buildServicePath)}/builds/${encodeURIComponent(buildId)}/cancel`
|
|
1283
1607
|
);
|
|
1284
1608
|
}
|
|
1285
1609
|
async *streamBuildLogs(buildServicePath, buildId, signal) {
|
|
1286
1610
|
const stream = await this.http.requestStream(
|
|
1287
1611
|
"GET",
|
|
1288
|
-
`${
|
|
1612
|
+
`${trimTrailingSlashes2(buildServicePath)}/builds/${encodeURIComponent(buildId)}/logs`,
|
|
1289
1613
|
{ signal }
|
|
1290
1614
|
);
|
|
1291
1615
|
for await (const event of parseSSEStream(stream, signal)) {
|
|
1292
1616
|
yield fromSnakeKeys(event);
|
|
1293
1617
|
}
|
|
1294
1618
|
}
|
|
1295
|
-
async runMultipartRequest(
|
|
1619
|
+
async runMultipartRequest(path2, inputs) {
|
|
1296
1620
|
const form = new FormData();
|
|
1297
1621
|
for (const input of inputs) {
|
|
1298
1622
|
form.append(
|
|
@@ -1301,7 +1625,7 @@ var CloudClient = class _CloudClient {
|
|
|
1301
1625
|
input.name
|
|
1302
1626
|
);
|
|
1303
1627
|
}
|
|
1304
|
-
return this.http.requestResponse("POST",
|
|
1628
|
+
return this.http.requestResponse("POST", path2, {
|
|
1305
1629
|
body: form,
|
|
1306
1630
|
headers: { Accept: "application/json" }
|
|
1307
1631
|
});
|
|
@@ -1361,8 +1685,8 @@ function createApplicationBuildForm(request, imageContexts) {
|
|
|
1361
1685
|
}
|
|
1362
1686
|
return form;
|
|
1363
1687
|
}
|
|
1364
|
-
function
|
|
1365
|
-
return value.
|
|
1688
|
+
function trimTrailingSlashes2(value) {
|
|
1689
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
1366
1690
|
}
|
|
1367
1691
|
function toBlobPart(data) {
|
|
1368
1692
|
if (typeof data === "string" || data instanceof Blob) {
|
|
@@ -1436,15 +1760,860 @@ var APIClient = class {
|
|
|
1436
1760
|
return this.cloudClient.requestOutput(applicationName, requestId);
|
|
1437
1761
|
}
|
|
1438
1762
|
};
|
|
1763
|
+
|
|
1764
|
+
// src/sandbox-image.ts
|
|
1765
|
+
var import_promises = require("fs/promises");
|
|
1766
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1767
|
+
var import_node_util = require("util");
|
|
1768
|
+
|
|
1769
|
+
// src/image.ts
|
|
1770
|
+
var import_node_crypto = require("crypto");
|
|
1771
|
+
var ImageBuildOperationType = {
|
|
1772
|
+
ADD: "ADD",
|
|
1773
|
+
COPY: "COPY",
|
|
1774
|
+
ENV: "ENV",
|
|
1775
|
+
RUN: "RUN",
|
|
1776
|
+
WORKDIR: "WORKDIR"
|
|
1777
|
+
};
|
|
1778
|
+
function cloneOperation(op) {
|
|
1779
|
+
return {
|
|
1780
|
+
type: op.type,
|
|
1781
|
+
args: [...op.args],
|
|
1782
|
+
options: { ...op.options }
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
var Image = class {
|
|
1786
|
+
_id;
|
|
1787
|
+
_name;
|
|
1788
|
+
_tag;
|
|
1789
|
+
_baseImage;
|
|
1790
|
+
_buildOperations;
|
|
1791
|
+
constructor(nameOrOptions = {}, tag = "latest", baseImage = null) {
|
|
1792
|
+
this._id = (0, import_node_crypto.randomUUID)();
|
|
1793
|
+
this._buildOperations = [];
|
|
1794
|
+
if (typeof nameOrOptions === "string") {
|
|
1795
|
+
this._name = nameOrOptions;
|
|
1796
|
+
this._tag = tag;
|
|
1797
|
+
this._baseImage = baseImage;
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
this._name = nameOrOptions.name ?? "default";
|
|
1801
|
+
this._tag = nameOrOptions.tag ?? "latest";
|
|
1802
|
+
this._baseImage = nameOrOptions.baseImage ?? null;
|
|
1803
|
+
}
|
|
1804
|
+
get name() {
|
|
1805
|
+
return this._name;
|
|
1806
|
+
}
|
|
1807
|
+
get tag() {
|
|
1808
|
+
return this._tag;
|
|
1809
|
+
}
|
|
1810
|
+
get baseImage() {
|
|
1811
|
+
return this._baseImage;
|
|
1812
|
+
}
|
|
1813
|
+
get buildOperations() {
|
|
1814
|
+
return this._buildOperations.map(cloneOperation);
|
|
1815
|
+
}
|
|
1816
|
+
add(src, dest, options = void 0) {
|
|
1817
|
+
return this._addOperation({
|
|
1818
|
+
type: ImageBuildOperationType.ADD,
|
|
1819
|
+
args: [src, dest],
|
|
1820
|
+
options: options == null ? {} : { ...options }
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
copy(src, dest, options = void 0) {
|
|
1824
|
+
return this._addOperation({
|
|
1825
|
+
type: ImageBuildOperationType.COPY,
|
|
1826
|
+
args: [src, dest],
|
|
1827
|
+
options: options == null ? {} : { ...options }
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
env(key, value) {
|
|
1831
|
+
return this._addOperation({
|
|
1832
|
+
type: ImageBuildOperationType.ENV,
|
|
1833
|
+
args: [key, value],
|
|
1834
|
+
options: {}
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
run(commands, options = void 0) {
|
|
1838
|
+
return this._addOperation({
|
|
1839
|
+
type: ImageBuildOperationType.RUN,
|
|
1840
|
+
args: Array.isArray(commands) ? [...commands] : [commands],
|
|
1841
|
+
options: options == null ? {} : { ...options }
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
workdir(directory) {
|
|
1845
|
+
return this._addOperation({
|
|
1846
|
+
type: ImageBuildOperationType.WORKDIR,
|
|
1847
|
+
args: [directory],
|
|
1848
|
+
options: {}
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
_addOperation(op) {
|
|
1852
|
+
this._buildOperations.push(op);
|
|
1853
|
+
return this;
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
function renderOptions(options) {
|
|
1857
|
+
const entries = Object.entries(options);
|
|
1858
|
+
if (entries.length === 0) {
|
|
1859
|
+
return "";
|
|
1860
|
+
}
|
|
1861
|
+
return ` ${entries.map(([key, value]) => `--${key}=${value}`).join(" ")}`;
|
|
1862
|
+
}
|
|
1863
|
+
function renderBuildOp(op) {
|
|
1864
|
+
const options = renderOptions(op.options);
|
|
1865
|
+
if (op.type === ImageBuildOperationType.ENV) {
|
|
1866
|
+
return `ENV${options} ${op.args[0]}=${JSON.stringify(op.args[1])}`;
|
|
1867
|
+
}
|
|
1868
|
+
return `${op.type}${options} ${op.args.join(" ")}`;
|
|
1869
|
+
}
|
|
1870
|
+
function dockerfileContent(image) {
|
|
1871
|
+
const lines = image.baseImage == null ? [] : [`FROM ${image.baseImage}`];
|
|
1872
|
+
lines.push(...image.buildOperations.map((op) => renderBuildOp(op)));
|
|
1873
|
+
return lines.join("\n");
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/sandbox-image.ts
|
|
1877
|
+
var BUILD_SANDBOX_PIP_ENV = { PIP_BREAK_SYSTEM_PACKAGES: "1" };
|
|
1878
|
+
var IGNORED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
1879
|
+
"CMD",
|
|
1880
|
+
"ENTRYPOINT",
|
|
1881
|
+
"EXPOSE",
|
|
1882
|
+
"HEALTHCHECK",
|
|
1883
|
+
"LABEL",
|
|
1884
|
+
"STOPSIGNAL",
|
|
1885
|
+
"VOLUME"
|
|
1886
|
+
]);
|
|
1887
|
+
var UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
|
|
1888
|
+
"ARG",
|
|
1889
|
+
"ONBUILD",
|
|
1890
|
+
"SHELL",
|
|
1891
|
+
"USER"
|
|
1892
|
+
]);
|
|
1893
|
+
function defaultRegisteredName(dockerfilePath) {
|
|
1894
|
+
const parsed = import_node_path.default.parse(dockerfilePath);
|
|
1895
|
+
if (parsed.name.toLowerCase() === "dockerfile") {
|
|
1896
|
+
const parentName = import_node_path.default.basename(import_node_path.default.dirname(dockerfilePath)).trim();
|
|
1897
|
+
return parentName || "sandbox-image";
|
|
1898
|
+
}
|
|
1899
|
+
return parsed.name || "sandbox-image";
|
|
1900
|
+
}
|
|
1901
|
+
function logicalDockerfileLines(dockerfileText) {
|
|
1902
|
+
const logicalLines = [];
|
|
1903
|
+
let parts = [];
|
|
1904
|
+
let startLine = null;
|
|
1905
|
+
for (const [index, rawLine] of dockerfileText.split(/\r?\n/).entries()) {
|
|
1906
|
+
const lineNumber = index + 1;
|
|
1907
|
+
const stripped = rawLine.trim();
|
|
1908
|
+
if (parts.length === 0 && (!stripped || stripped.startsWith("#"))) {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
if (startLine == null) {
|
|
1912
|
+
startLine = lineNumber;
|
|
1913
|
+
}
|
|
1914
|
+
let line = rawLine.replace(/\s+$/, "");
|
|
1915
|
+
const continued = line.endsWith("\\");
|
|
1916
|
+
if (continued) {
|
|
1917
|
+
line = line.slice(0, -1);
|
|
1918
|
+
}
|
|
1919
|
+
const normalized = line.trim();
|
|
1920
|
+
if (normalized && !normalized.startsWith("#")) {
|
|
1921
|
+
parts.push(normalized);
|
|
1922
|
+
}
|
|
1923
|
+
if (continued) {
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
if (parts.length > 0) {
|
|
1927
|
+
logicalLines.push({
|
|
1928
|
+
lineNumber: startLine ?? lineNumber,
|
|
1929
|
+
line: parts.join(" ")
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
parts = [];
|
|
1933
|
+
startLine = null;
|
|
1934
|
+
}
|
|
1935
|
+
if (parts.length > 0) {
|
|
1936
|
+
logicalLines.push({
|
|
1937
|
+
lineNumber: startLine ?? 1,
|
|
1938
|
+
line: parts.join(" ")
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
return logicalLines;
|
|
1942
|
+
}
|
|
1943
|
+
function splitInstruction(line, lineNumber) {
|
|
1944
|
+
const trimmed = line.trim();
|
|
1945
|
+
if (!trimmed) {
|
|
1946
|
+
throw new Error(`line ${lineNumber}: empty Dockerfile instruction`);
|
|
1947
|
+
}
|
|
1948
|
+
const match = trimmed.match(/^(\S+)(?:\s+(.*))?$/);
|
|
1949
|
+
if (!match) {
|
|
1950
|
+
throw new Error(`line ${lineNumber}: invalid Dockerfile instruction`);
|
|
1951
|
+
}
|
|
1952
|
+
return {
|
|
1953
|
+
keyword: match[1].toUpperCase(),
|
|
1954
|
+
value: (match[2] ?? "").trim()
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
function shellSplit(input) {
|
|
1958
|
+
const tokens = [];
|
|
1959
|
+
let current = "";
|
|
1960
|
+
let quote = null;
|
|
1961
|
+
let escape = false;
|
|
1962
|
+
for (let i = 0; i < input.length; i++) {
|
|
1963
|
+
const char = input[i];
|
|
1964
|
+
if (escape) {
|
|
1965
|
+
current += char;
|
|
1966
|
+
escape = false;
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
if (quote == null) {
|
|
1970
|
+
if (/\s/.test(char)) {
|
|
1971
|
+
if (current) {
|
|
1972
|
+
tokens.push(current);
|
|
1973
|
+
current = "";
|
|
1974
|
+
}
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
if (char === "'" || char === '"') {
|
|
1978
|
+
quote = char;
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
if (char === "\\") {
|
|
1982
|
+
escape = true;
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
current += char;
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
if (quote === "'") {
|
|
1989
|
+
if (char === "'") {
|
|
1990
|
+
quote = null;
|
|
1991
|
+
} else {
|
|
1992
|
+
current += char;
|
|
1993
|
+
}
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
if (char === '"') {
|
|
1997
|
+
quote = null;
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
if (char === "\\") {
|
|
2001
|
+
const next = input[++i];
|
|
2002
|
+
if (next == null) {
|
|
2003
|
+
throw new Error(`unterminated escape sequence in '${input}'`);
|
|
2004
|
+
}
|
|
2005
|
+
current += next;
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
current += char;
|
|
2009
|
+
}
|
|
2010
|
+
if (escape) {
|
|
2011
|
+
throw new Error(`unterminated escape sequence in '${input}'`);
|
|
2012
|
+
}
|
|
2013
|
+
if (quote != null) {
|
|
2014
|
+
throw new Error(`unterminated quoted string in '${input}'`);
|
|
2015
|
+
}
|
|
2016
|
+
if (current) {
|
|
2017
|
+
tokens.push(current);
|
|
2018
|
+
}
|
|
2019
|
+
return tokens;
|
|
2020
|
+
}
|
|
2021
|
+
function shellQuote(value) {
|
|
2022
|
+
if (!value) {
|
|
2023
|
+
return "''";
|
|
2024
|
+
}
|
|
2025
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2026
|
+
}
|
|
2027
|
+
function stripLeadingFlags(value) {
|
|
2028
|
+
const flags = {};
|
|
2029
|
+
let remaining = value.trimStart();
|
|
2030
|
+
while (remaining.startsWith("--")) {
|
|
2031
|
+
const firstSpace = remaining.indexOf(" ");
|
|
2032
|
+
if (firstSpace === -1) {
|
|
2033
|
+
throw new Error(`invalid Dockerfile flag syntax: ${value}`);
|
|
2034
|
+
}
|
|
2035
|
+
const token = remaining.slice(0, firstSpace);
|
|
2036
|
+
const rest = remaining.slice(firstSpace + 1).trimStart();
|
|
2037
|
+
const flagBody = token.slice(2);
|
|
2038
|
+
if (flagBody.includes("=")) {
|
|
2039
|
+
const [key, flagValue2] = flagBody.split(/=(.*)/s, 2);
|
|
2040
|
+
flags[key] = flagValue2;
|
|
2041
|
+
remaining = rest;
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
const [flagValue, ...restTokens] = shellSplit(rest);
|
|
2045
|
+
if (flagValue == null) {
|
|
2046
|
+
throw new Error(`missing value for Dockerfile flag '${token}'`);
|
|
2047
|
+
}
|
|
2048
|
+
flags[flagBody] = flagValue;
|
|
2049
|
+
remaining = restTokens.join(" ");
|
|
2050
|
+
}
|
|
2051
|
+
return { flags, remaining };
|
|
2052
|
+
}
|
|
2053
|
+
function parseFromValue(value, lineNumber) {
|
|
2054
|
+
const { remaining } = stripLeadingFlags(value);
|
|
2055
|
+
const tokens = shellSplit(remaining);
|
|
2056
|
+
if (tokens.length === 0) {
|
|
2057
|
+
throw new Error(`line ${lineNumber}: FROM must include a base image`);
|
|
2058
|
+
}
|
|
2059
|
+
if (tokens.length > 1 && tokens[1].toLowerCase() !== "as") {
|
|
2060
|
+
throw new Error(`line ${lineNumber}: unsupported FROM syntax '${value}'`);
|
|
2061
|
+
}
|
|
2062
|
+
return tokens[0];
|
|
2063
|
+
}
|
|
2064
|
+
function parseCopyLikeValues(value, lineNumber, keyword) {
|
|
2065
|
+
const { flags, remaining } = stripLeadingFlags(value);
|
|
2066
|
+
if ("from" in flags) {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
const payload = remaining.trim();
|
|
2072
|
+
if (!payload) {
|
|
2073
|
+
throw new Error(
|
|
2074
|
+
`line ${lineNumber}: ${keyword} must include source and destination`
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
let parts;
|
|
2078
|
+
if (payload.startsWith("[")) {
|
|
2079
|
+
let parsed;
|
|
2080
|
+
try {
|
|
2081
|
+
parsed = JSON.parse(payload);
|
|
2082
|
+
} catch (error) {
|
|
2083
|
+
throw new Error(
|
|
2084
|
+
`line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
|
|
2088
|
+
throw new Error(
|
|
2089
|
+
`line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
parts = parsed;
|
|
2093
|
+
} else {
|
|
2094
|
+
parts = shellSplit(payload);
|
|
2095
|
+
if (parts.length < 2) {
|
|
2096
|
+
throw new Error(
|
|
2097
|
+
`line ${lineNumber}: ${keyword} must include at least one source and one destination`
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return {
|
|
2102
|
+
flags,
|
|
2103
|
+
sources: parts.slice(0, -1),
|
|
2104
|
+
destination: parts[parts.length - 1]
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
function parseEnvPairs(value, lineNumber) {
|
|
2108
|
+
const tokens = shellSplit(value);
|
|
2109
|
+
if (tokens.length === 0) {
|
|
2110
|
+
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
2111
|
+
}
|
|
2112
|
+
if (tokens.every((token) => token.includes("="))) {
|
|
2113
|
+
return tokens.map((token) => {
|
|
2114
|
+
const [key, envValue] = token.split(/=(.*)/s, 2);
|
|
2115
|
+
if (!key) {
|
|
2116
|
+
throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
|
|
2117
|
+
}
|
|
2118
|
+
return [key, envValue];
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
if (tokens.length < 2) {
|
|
2122
|
+
throw new Error(`line ${lineNumber}: ENV must include a key and value`);
|
|
2123
|
+
}
|
|
2124
|
+
return [[tokens[0], tokens.slice(1).join(" ")]];
|
|
2125
|
+
}
|
|
2126
|
+
function resolveContainerPath(containerPath, workingDir) {
|
|
2127
|
+
if (!containerPath) {
|
|
2128
|
+
return workingDir;
|
|
2129
|
+
}
|
|
2130
|
+
const normalized = containerPath.startsWith("/") ? import_node_path.default.posix.normalize(containerPath) : import_node_path.default.posix.normalize(import_node_path.default.posix.join(workingDir, containerPath));
|
|
2131
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
2132
|
+
}
|
|
2133
|
+
function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
|
|
2134
|
+
let baseImage;
|
|
2135
|
+
const instructions = [];
|
|
2136
|
+
for (const logicalLine of logicalDockerfileLines(dockerfileText)) {
|
|
2137
|
+
const { keyword, value } = splitInstruction(
|
|
2138
|
+
logicalLine.line,
|
|
2139
|
+
logicalLine.lineNumber
|
|
2140
|
+
);
|
|
2141
|
+
if (keyword === "FROM") {
|
|
2142
|
+
if (baseImage != null) {
|
|
2143
|
+
throw new Error(
|
|
2144
|
+
`line ${logicalLine.lineNumber}: multi-stage Dockerfiles are not supported for sandbox image creation`
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
baseImage = parseFromValue(value, logicalLine.lineNumber);
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
if (UNSUPPORTED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
2151
|
+
throw new Error(
|
|
2152
|
+
`line ${logicalLine.lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
instructions.push({
|
|
2156
|
+
keyword,
|
|
2157
|
+
value,
|
|
2158
|
+
lineNumber: logicalLine.lineNumber
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
if (!baseImage) {
|
|
2162
|
+
throw new Error("Dockerfile must contain a FROM instruction");
|
|
2163
|
+
}
|
|
2164
|
+
return {
|
|
2165
|
+
dockerfilePath,
|
|
2166
|
+
contextDir,
|
|
2167
|
+
registeredName: registeredName ?? defaultRegisteredName(dockerfilePath),
|
|
2168
|
+
dockerfileText,
|
|
2169
|
+
baseImage,
|
|
2170
|
+
instructions
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
async function loadDockerfilePlan(dockerfilePath, registeredName) {
|
|
2174
|
+
const resolvedPath = import_node_path.default.resolve(dockerfilePath);
|
|
2175
|
+
const fileStats = await (0, import_promises.stat)(resolvedPath).catch(() => null);
|
|
2176
|
+
if (!fileStats?.isFile()) {
|
|
2177
|
+
throw new Error(`Dockerfile not found: ${dockerfilePath}`);
|
|
2178
|
+
}
|
|
2179
|
+
const dockerfileText = await (0, import_promises.readFile)(resolvedPath, "utf8");
|
|
2180
|
+
return buildPlanFromDockerfileText(
|
|
2181
|
+
dockerfileText,
|
|
2182
|
+
resolvedPath,
|
|
2183
|
+
import_node_path.default.dirname(resolvedPath),
|
|
2184
|
+
registeredName
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
function loadImagePlan(image, options = {}) {
|
|
2188
|
+
const contextDir = import_node_path.default.resolve(options.contextDir ?? process.cwd());
|
|
2189
|
+
const dockerfileText = dockerfileContent(image);
|
|
2190
|
+
const logicalLines = logicalDockerfileLines(dockerfileText);
|
|
2191
|
+
const instructions = image.baseImage == null ? logicalLines : logicalLines.slice(1);
|
|
2192
|
+
return {
|
|
2193
|
+
dockerfilePath: import_node_path.default.join(contextDir, "Dockerfile"),
|
|
2194
|
+
contextDir,
|
|
2195
|
+
registeredName: options.registeredName ?? image.name,
|
|
2196
|
+
dockerfileText,
|
|
2197
|
+
baseImage: image.baseImage ?? void 0,
|
|
2198
|
+
instructions: instructions.map(({ line, lineNumber }) => {
|
|
2199
|
+
const parsed = splitInstruction(line, lineNumber);
|
|
2200
|
+
return {
|
|
2201
|
+
keyword: parsed.keyword,
|
|
2202
|
+
value: parsed.value,
|
|
2203
|
+
lineNumber
|
|
2204
|
+
};
|
|
2205
|
+
})
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
function defaultEmit(event) {
|
|
2209
|
+
process.stdout.write(`${JSON.stringify(event)}
|
|
2210
|
+
`);
|
|
2211
|
+
}
|
|
2212
|
+
function debugEnabled() {
|
|
2213
|
+
return ["1", "true", "yes", "on"].includes(
|
|
2214
|
+
(process.env.TENSORLAKE_DEBUG ?? "").toLowerCase()
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
function buildContextFromEnv() {
|
|
2218
|
+
return {
|
|
2219
|
+
apiUrl: process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai",
|
|
2220
|
+
apiKey: process.env.TENSORLAKE_API_KEY,
|
|
2221
|
+
personalAccessToken: process.env.TENSORLAKE_PAT,
|
|
2222
|
+
namespace: process.env.INDEXIFY_NAMESPACE ?? "default",
|
|
2223
|
+
organizationId: process.env.TENSORLAKE_ORGANIZATION_ID,
|
|
2224
|
+
projectId: process.env.TENSORLAKE_PROJECT_ID,
|
|
2225
|
+
debug: debugEnabled()
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
function createDefaultClient(context) {
|
|
2229
|
+
return new SandboxClient({
|
|
2230
|
+
apiUrl: context.apiUrl,
|
|
2231
|
+
apiKey: context.apiKey ?? context.personalAccessToken,
|
|
2232
|
+
organizationId: context.organizationId,
|
|
2233
|
+
projectId: context.projectId,
|
|
2234
|
+
namespace: context.namespace
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
async function runChecked(sandbox, command, args, env, workingDir) {
|
|
2238
|
+
const result = await sandbox.run(command, {
|
|
2239
|
+
args,
|
|
2240
|
+
env,
|
|
2241
|
+
workingDir
|
|
2242
|
+
});
|
|
2243
|
+
if (result.exitCode !== 0) {
|
|
2244
|
+
throw new Error(
|
|
2245
|
+
`Command '${command} ${args.join(" ")}' failed with exit code ${result.exitCode}`
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
return result;
|
|
2249
|
+
}
|
|
2250
|
+
async function runStreaming(sandbox, emit, sleep4, command, args = [], env, workingDir) {
|
|
2251
|
+
const proc = await sandbox.startProcess(command, {
|
|
2252
|
+
args,
|
|
2253
|
+
env,
|
|
2254
|
+
workingDir
|
|
2255
|
+
});
|
|
2256
|
+
let stdoutSeen = 0;
|
|
2257
|
+
let stderrSeen = 0;
|
|
2258
|
+
let info;
|
|
2259
|
+
while (true) {
|
|
2260
|
+
const stdoutResp = await sandbox.getStdout(proc.pid);
|
|
2261
|
+
emitOutputLines(emit, "stdout", stdoutResp, stdoutSeen);
|
|
2262
|
+
stdoutSeen = stdoutResp.lines.length;
|
|
2263
|
+
const stderrResp = await sandbox.getStderr(proc.pid);
|
|
2264
|
+
emitOutputLines(emit, "stderr", stderrResp, stderrSeen);
|
|
2265
|
+
stderrSeen = stderrResp.lines.length;
|
|
2266
|
+
info = await sandbox.getProcess(proc.pid);
|
|
2267
|
+
if (info.status !== "running" /* RUNNING */) {
|
|
2268
|
+
const finalStdout = await sandbox.getStdout(proc.pid);
|
|
2269
|
+
emitOutputLines(emit, "stdout", finalStdout, stdoutSeen);
|
|
2270
|
+
stdoutSeen = finalStdout.lines.length;
|
|
2271
|
+
const finalStderr = await sandbox.getStderr(proc.pid);
|
|
2272
|
+
emitOutputLines(emit, "stderr", finalStderr, stderrSeen);
|
|
2273
|
+
break;
|
|
2274
|
+
}
|
|
2275
|
+
await sleep4(300);
|
|
2276
|
+
}
|
|
2277
|
+
for (let i = 0; i < 10; i++) {
|
|
2278
|
+
if (info.exitCode != null || info.signal != null) {
|
|
2279
|
+
break;
|
|
2280
|
+
}
|
|
2281
|
+
await sleep4(200);
|
|
2282
|
+
info = await sandbox.getProcess(proc.pid);
|
|
2283
|
+
}
|
|
2284
|
+
const exitCode = info.exitCode != null ? info.exitCode : info.signal != null ? -info.signal : 0;
|
|
2285
|
+
if (exitCode !== 0) {
|
|
2286
|
+
throw new Error(
|
|
2287
|
+
`Command '${command} ${args.join(" ")}' failed with exit code ${exitCode}`
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
function emitOutputLines(emit, stream, response, seen) {
|
|
2292
|
+
for (const line of response.lines.slice(seen)) {
|
|
2293
|
+
emit({ type: "build_log", stream, message: line });
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
function isPathWithinContext(contextDir, localPath) {
|
|
2297
|
+
const relative = import_node_path.default.relative(contextDir, localPath);
|
|
2298
|
+
return relative === "" || !relative.startsWith("..") && !import_node_path.default.isAbsolute(relative);
|
|
2299
|
+
}
|
|
2300
|
+
function resolveContextSourcePath(contextDir, source) {
|
|
2301
|
+
const resolvedContextDir = import_node_path.default.resolve(contextDir);
|
|
2302
|
+
const resolvedSource = import_node_path.default.resolve(resolvedContextDir, source);
|
|
2303
|
+
if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
|
|
2304
|
+
throw new Error(`Local path escapes the build context: ${source}`);
|
|
2305
|
+
}
|
|
2306
|
+
return resolvedSource;
|
|
2307
|
+
}
|
|
2308
|
+
async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
|
|
2309
|
+
const fileStats = await (0, import_promises.stat)(localPath).catch(() => null);
|
|
2310
|
+
if (!fileStats) {
|
|
2311
|
+
throw new Error(`Local path not found: ${localPath}`);
|
|
2312
|
+
}
|
|
2313
|
+
if (fileStats.isFile()) {
|
|
2314
|
+
await runChecked(sandbox, "mkdir", ["-p", import_node_path.default.posix.dirname(remotePath)]);
|
|
2315
|
+
await sandbox.writeFile(remotePath, await (0, import_promises.readFile)(localPath));
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
if (!fileStats.isDirectory()) {
|
|
2319
|
+
throw new Error(`Local path not found: ${localPath}`);
|
|
2320
|
+
}
|
|
2321
|
+
const entries = await (0, import_promises.readdir)(localPath, { withFileTypes: true });
|
|
2322
|
+
for (const entry of entries) {
|
|
2323
|
+
const sourcePath = import_node_path.default.join(localPath, entry.name);
|
|
2324
|
+
const destinationPath = import_node_path.default.posix.join(remotePath, entry.name);
|
|
2325
|
+
if (entry.isDirectory()) {
|
|
2326
|
+
await runChecked(sandbox, "mkdir", ["-p", destinationPath]);
|
|
2327
|
+
await copyLocalPathToSandbox(sandbox, sourcePath, destinationPath);
|
|
2328
|
+
} else if (entry.isFile()) {
|
|
2329
|
+
await runChecked(
|
|
2330
|
+
sandbox,
|
|
2331
|
+
"mkdir",
|
|
2332
|
+
["-p", import_node_path.default.posix.dirname(destinationPath)]
|
|
2333
|
+
);
|
|
2334
|
+
await sandbox.writeFile(destinationPath, await (0, import_promises.readFile)(sourcePath));
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
async function persistEnvVar(sandbox, processEnv, key, value) {
|
|
2339
|
+
const exportLine = `export ${key}=${shellQuote(value)}`;
|
|
2340
|
+
await runChecked(
|
|
2341
|
+
sandbox,
|
|
2342
|
+
"sh",
|
|
2343
|
+
["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
|
|
2344
|
+
processEnv
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
|
|
2348
|
+
const destinationPath = resolveContainerPath(destination, workingDir);
|
|
2349
|
+
if (sources.length > 1 && !destinationPath.endsWith("/")) {
|
|
2350
|
+
throw new Error(
|
|
2351
|
+
`${keyword} with multiple sources requires a directory destination ending in '/'`
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
for (const source of sources) {
|
|
2355
|
+
const localSource = resolveContextSourcePath(contextDir, source);
|
|
2356
|
+
const localStats = await (0, import_promises.stat)(localSource).catch(() => null);
|
|
2357
|
+
if (!localStats) {
|
|
2358
|
+
throw new Error(`Local path not found: ${localSource}`);
|
|
2359
|
+
}
|
|
2360
|
+
let remoteDestination = destinationPath;
|
|
2361
|
+
if (sources.length > 1) {
|
|
2362
|
+
remoteDestination = import_node_path.default.posix.join(
|
|
2363
|
+
destinationPath.replace(/\/$/, ""),
|
|
2364
|
+
import_node_path.default.posix.basename(source.replace(/\/$/, ""))
|
|
2365
|
+
);
|
|
2366
|
+
} else if (localStats.isFile() && destinationPath.endsWith("/")) {
|
|
2367
|
+
remoteDestination = import_node_path.default.posix.join(
|
|
2368
|
+
destinationPath.replace(/\/$/, ""),
|
|
2369
|
+
import_node_path.default.basename(source)
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
emit({
|
|
2373
|
+
type: "status",
|
|
2374
|
+
message: `${keyword} ${source} -> ${remoteDestination}`
|
|
2375
|
+
});
|
|
2376
|
+
await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep4) {
|
|
2380
|
+
let destinationPath = resolveContainerPath(destination, workingDir);
|
|
2381
|
+
const parsedUrl = new URL(url);
|
|
2382
|
+
const fileName = import_node_path.default.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
|
|
2383
|
+
if (destinationPath.endsWith("/")) {
|
|
2384
|
+
destinationPath = import_node_path.default.posix.join(destinationPath.replace(/\/$/, ""), fileName);
|
|
2385
|
+
}
|
|
2386
|
+
const parentDir = import_node_path.default.posix.dirname(destinationPath) || "/";
|
|
2387
|
+
emit({
|
|
2388
|
+
type: "status",
|
|
2389
|
+
message: `ADD ${url} -> ${destinationPath}`
|
|
2390
|
+
});
|
|
2391
|
+
await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
|
|
2392
|
+
await runStreaming(
|
|
2393
|
+
sandbox,
|
|
2394
|
+
emit,
|
|
2395
|
+
sleep4,
|
|
2396
|
+
"sh",
|
|
2397
|
+
[
|
|
2398
|
+
"-c",
|
|
2399
|
+
`curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
|
|
2400
|
+
],
|
|
2401
|
+
processEnv,
|
|
2402
|
+
workingDir
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
async function executeDockerfilePlan(sandbox, plan, emit, sleep4) {
|
|
2406
|
+
const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
|
|
2407
|
+
let workingDir = "/";
|
|
2408
|
+
for (const instruction of plan.instructions) {
|
|
2409
|
+
const { keyword, value, lineNumber } = instruction;
|
|
2410
|
+
if (keyword === "RUN") {
|
|
2411
|
+
emit({ type: "status", message: `RUN ${value}` });
|
|
2412
|
+
await runStreaming(
|
|
2413
|
+
sandbox,
|
|
2414
|
+
emit,
|
|
2415
|
+
sleep4,
|
|
2416
|
+
"sh",
|
|
2417
|
+
["-c", value],
|
|
2418
|
+
processEnv,
|
|
2419
|
+
workingDir
|
|
2420
|
+
);
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
if (keyword === "WORKDIR") {
|
|
2424
|
+
const tokens = shellSplit(value);
|
|
2425
|
+
if (tokens.length !== 1) {
|
|
2426
|
+
throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
|
|
2427
|
+
}
|
|
2428
|
+
workingDir = resolveContainerPath(tokens[0], workingDir);
|
|
2429
|
+
emit({ type: "status", message: `WORKDIR ${workingDir}` });
|
|
2430
|
+
await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
if (keyword === "ENV") {
|
|
2434
|
+
for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
|
|
2435
|
+
emit({ type: "status", message: `ENV ${key}=${envValue}` });
|
|
2436
|
+
processEnv[key] = envValue;
|
|
2437
|
+
await persistEnvVar(sandbox, processEnv, key, envValue);
|
|
2438
|
+
}
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
if (keyword === "COPY") {
|
|
2442
|
+
const { sources, destination } = parseCopyLikeValues(
|
|
2443
|
+
value,
|
|
2444
|
+
lineNumber,
|
|
2445
|
+
keyword
|
|
2446
|
+
);
|
|
2447
|
+
await copyFromContext(
|
|
2448
|
+
sandbox,
|
|
2449
|
+
emit,
|
|
2450
|
+
plan.contextDir,
|
|
2451
|
+
sources,
|
|
2452
|
+
destination,
|
|
2453
|
+
workingDir,
|
|
2454
|
+
keyword
|
|
2455
|
+
);
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
if (keyword === "ADD") {
|
|
2459
|
+
const { sources, destination } = parseCopyLikeValues(
|
|
2460
|
+
value,
|
|
2461
|
+
lineNumber,
|
|
2462
|
+
keyword
|
|
2463
|
+
);
|
|
2464
|
+
if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
|
|
2465
|
+
await addUrlToSandbox(
|
|
2466
|
+
sandbox,
|
|
2467
|
+
emit,
|
|
2468
|
+
sources[0],
|
|
2469
|
+
destination,
|
|
2470
|
+
workingDir,
|
|
2471
|
+
processEnv,
|
|
2472
|
+
sleep4
|
|
2473
|
+
);
|
|
2474
|
+
} else {
|
|
2475
|
+
await copyFromContext(
|
|
2476
|
+
sandbox,
|
|
2477
|
+
emit,
|
|
2478
|
+
plan.contextDir,
|
|
2479
|
+
sources,
|
|
2480
|
+
destination,
|
|
2481
|
+
workingDir,
|
|
2482
|
+
keyword
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
|
|
2488
|
+
emit({
|
|
2489
|
+
type: "warning",
|
|
2490
|
+
message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
|
|
2491
|
+
});
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
throw new Error(
|
|
2495
|
+
`line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
async function registerImage(context, name, dockerfile, snapshotId, snapshotUri, isPublic) {
|
|
2500
|
+
if (!context.organizationId || !context.projectId) {
|
|
2501
|
+
throw new Error(
|
|
2502
|
+
"Organization ID and Project ID are required. Run 'tl login' and 'tl init'."
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
const bearerToken = context.apiKey ?? context.personalAccessToken;
|
|
2506
|
+
if (!bearerToken) {
|
|
2507
|
+
throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
|
|
2508
|
+
}
|
|
2509
|
+
const baseUrl = context.apiUrl.replace(/\/+$/, "");
|
|
2510
|
+
const url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
|
|
2511
|
+
const headers = {
|
|
2512
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
2513
|
+
"Content-Type": "application/json"
|
|
2514
|
+
};
|
|
2515
|
+
if (context.personalAccessToken && !context.apiKey) {
|
|
2516
|
+
headers["X-Forwarded-Organization-Id"] = context.organizationId;
|
|
2517
|
+
headers["X-Forwarded-Project-Id"] = context.projectId;
|
|
2518
|
+
}
|
|
2519
|
+
const response = await fetch(url, {
|
|
2520
|
+
method: "POST",
|
|
2521
|
+
headers,
|
|
2522
|
+
body: JSON.stringify({
|
|
2523
|
+
name,
|
|
2524
|
+
dockerfile,
|
|
2525
|
+
snapshotId,
|
|
2526
|
+
snapshotUri,
|
|
2527
|
+
isPublic
|
|
2528
|
+
})
|
|
2529
|
+
});
|
|
2530
|
+
if (!response.ok) {
|
|
2531
|
+
throw new Error(
|
|
2532
|
+
`${response.status} ${response.statusText}: ${await response.text()}`
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const text = await response.text();
|
|
2536
|
+
return text ? JSON.parse(text) : {};
|
|
2537
|
+
}
|
|
2538
|
+
async function createSandboxImage(source, options = {}, deps = {}) {
|
|
2539
|
+
const emit = deps.emit ?? defaultEmit;
|
|
2540
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2541
|
+
const context = buildContextFromEnv();
|
|
2542
|
+
const clientFactory = deps.createClient ?? createDefaultClient;
|
|
2543
|
+
const register = deps.registerImage ?? ((...args) => registerImage(...args));
|
|
2544
|
+
const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
|
|
2545
|
+
emit({ type: "status", message: `Loading ${sourceLabel}...` });
|
|
2546
|
+
const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
|
|
2547
|
+
emit({
|
|
2548
|
+
type: "status",
|
|
2549
|
+
message: plan.baseImage == null ? "Starting build sandbox with the default server image..." : `Starting build sandbox from ${plan.baseImage}...`
|
|
2550
|
+
});
|
|
2551
|
+
const client = clientFactory(context);
|
|
2552
|
+
let sandbox;
|
|
2553
|
+
try {
|
|
2554
|
+
sandbox = await client.createAndConnect({
|
|
2555
|
+
...plan.baseImage == null ? {} : { image: plan.baseImage },
|
|
2556
|
+
cpus: options.cpus ?? 2,
|
|
2557
|
+
memoryMb: options.memoryMb ?? 4096
|
|
2558
|
+
});
|
|
2559
|
+
emit({
|
|
2560
|
+
type: "status",
|
|
2561
|
+
message: `Materializing image in sandbox ${sandbox.sandboxId}...`
|
|
2562
|
+
});
|
|
2563
|
+
await executeDockerfilePlan(sandbox, plan, emit, sleep4);
|
|
2564
|
+
emit({ type: "status", message: "Creating snapshot..." });
|
|
2565
|
+
const snapshot = await client.snapshotAndWait(sandbox.sandboxId);
|
|
2566
|
+
emit({
|
|
2567
|
+
type: "snapshot_created",
|
|
2568
|
+
snapshot_id: snapshot.snapshotId,
|
|
2569
|
+
snapshot_uri: snapshot.snapshotUri ?? null
|
|
2570
|
+
});
|
|
2571
|
+
if (!snapshot.snapshotUri) {
|
|
2572
|
+
throw new Error(
|
|
2573
|
+
`Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
emit({
|
|
2577
|
+
type: "status",
|
|
2578
|
+
message: `Registering image '${plan.registeredName}'...`
|
|
2579
|
+
});
|
|
2580
|
+
const result = await register(
|
|
2581
|
+
context,
|
|
2582
|
+
plan.registeredName,
|
|
2583
|
+
plan.dockerfileText,
|
|
2584
|
+
snapshot.snapshotId,
|
|
2585
|
+
snapshot.snapshotUri,
|
|
2586
|
+
options.isPublic ?? false
|
|
2587
|
+
);
|
|
2588
|
+
emit({
|
|
2589
|
+
type: "image_registered",
|
|
2590
|
+
name: plan.registeredName,
|
|
2591
|
+
image_id: typeof result.id === "string" && result.id || typeof result.templateId === "string" && result.templateId || ""
|
|
2592
|
+
});
|
|
2593
|
+
emit({ type: "done" });
|
|
2594
|
+
return result;
|
|
2595
|
+
} finally {
|
|
2596
|
+
if (sandbox) {
|
|
2597
|
+
try {
|
|
2598
|
+
await sandbox.terminate();
|
|
2599
|
+
} catch {
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
client.close();
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
1439
2605
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1440
2606
|
0 && (module.exports = {
|
|
1441
2607
|
APIClient,
|
|
1442
2608
|
CloudClient,
|
|
1443
2609
|
ContainerState,
|
|
2610
|
+
Image,
|
|
2611
|
+
ImageBuildOperationType,
|
|
1444
2612
|
OutputMode,
|
|
1445
2613
|
PoolInUseError,
|
|
1446
2614
|
PoolNotFoundError,
|
|
1447
2615
|
ProcessStatus,
|
|
2616
|
+
Pty,
|
|
1448
2617
|
RemoteAPIError,
|
|
1449
2618
|
RequestExecutionError,
|
|
1450
2619
|
RequestFailedError,
|
|
@@ -1457,6 +2626,8 @@ var APIClient = class {
|
|
|
1457
2626
|
SandboxNotFoundError,
|
|
1458
2627
|
SandboxStatus,
|
|
1459
2628
|
SnapshotStatus,
|
|
1460
|
-
StdinMode
|
|
2629
|
+
StdinMode,
|
|
2630
|
+
createSandboxImage,
|
|
2631
|
+
dockerfileContent
|
|
1461
2632
|
});
|
|
1462
2633
|
//# sourceMappingURL=index.cjs.map
|