tensorlake 0.4.39 → 0.4.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -119,8 +119,8 @@ var HttpClient = class {
119
119
  this.abortController = null;
120
120
  }
121
121
  /** Make a JSON request, returning the parsed response body. */
122
- async requestJson(method, path, options) {
123
- const response = await this.requestResponse(method, path, {
122
+ async requestJson(method, path2, options) {
123
+ const response = await this.requestResponse(method, path2, {
124
124
  json: options?.body,
125
125
  headers: options?.headers,
126
126
  signal: options?.signal
@@ -130,14 +130,14 @@ var HttpClient = class {
130
130
  return JSON.parse(text);
131
131
  }
132
132
  /** Make a request returning raw bytes. */
133
- async requestBytes(method, path, options) {
133
+ async requestBytes(method, path2, options) {
134
134
  const headers = { ...options?.headers ?? {} };
135
135
  if (options?.contentType) {
136
136
  headers["Content-Type"] = options.contentType;
137
137
  }
138
138
  const response = await this.requestResponse(
139
139
  method,
140
- path,
140
+ path2,
141
141
  {
142
142
  body: options?.body,
143
143
  headers,
@@ -148,10 +148,10 @@ var HttpClient = class {
148
148
  return new Uint8Array(buffer);
149
149
  }
150
150
  /** Make a request and return the raw Response (for SSE streaming). */
151
- async requestStream(method, path, options) {
151
+ async requestStream(method, path2, options) {
152
152
  const response = await this.requestResponse(
153
153
  method,
154
- path,
154
+ path2,
155
155
  {
156
156
  headers: { Accept: "text/event-stream" },
157
157
  signal: options?.signal
@@ -163,7 +163,7 @@ var HttpClient = class {
163
163
  return response.body;
164
164
  }
165
165
  /** Make a request and return the raw Response. */
166
- async requestResponse(method, path, options) {
166
+ async requestResponse(method, path2, options) {
167
167
  const headers = {
168
168
  ...this.headers,
169
169
  ...options?.headers ?? {}
@@ -175,15 +175,15 @@ var HttpClient = class {
175
175
  const body = hasJsonBody ? JSON.stringify(options?.json) : normalizeRequestBody(options?.body);
176
176
  return this.doFetch(
177
177
  method,
178
- path,
178
+ path2,
179
179
  body,
180
180
  headers,
181
181
  options?.signal,
182
182
  options?.allowHttpErrors ?? false
183
183
  );
184
184
  }
185
- async doFetch(method, path, body, headers, signal, allowHttpErrors = false) {
186
- const url = `${this.baseUrl}${path}`;
185
+ async doFetch(method, path2, body, headers, signal, allowHttpErrors = false) {
186
+ const url = `${this.baseUrl}${path2}`;
187
187
  let lastError;
188
188
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
189
189
  if (attempt > 0) {
@@ -216,7 +216,7 @@ var HttpClient = class {
216
216
  return response;
217
217
  }
218
218
  const errorBody = await response.text().catch(() => "");
219
- throwMappedError(response.status, errorBody, path);
219
+ throwMappedError(response.status, errorBody, path2);
220
220
  } catch (err) {
221
221
  clearTimeout(timeoutId);
222
222
  if (err instanceof RemoteAPIError || err instanceof SandboxNotFoundError || err instanceof PoolNotFoundError || err instanceof PoolInUseError) {
@@ -247,7 +247,7 @@ function normalizeRequestBody(body) {
247
247
  }
248
248
  return body;
249
249
  }
250
- function throwMappedError(status, body, path) {
250
+ function throwMappedError(status, body, path2) {
251
251
  let message = body;
252
252
  try {
253
253
  const parsed = JSON.parse(body);
@@ -256,19 +256,19 @@ function throwMappedError(status, body, path) {
256
256
  } catch {
257
257
  }
258
258
  if (status === 404) {
259
- if (path.includes("sandbox-pools") || path.includes("pools")) {
260
- const match = path.match(/sandbox-pools\/([^/]+)/);
259
+ if (path2.includes("sandbox-pools") || path2.includes("pools")) {
260
+ const match = path2.match(/sandbox-pools\/([^/]+)/);
261
261
  if (match) throw new PoolNotFoundError(match[1]);
262
262
  }
263
- if (path.includes("sandboxes")) {
264
- const match = path.match(/sandboxes\/([^/]+)/);
263
+ if (path2.includes("sandboxes")) {
264
+ const match = path2.match(/sandboxes\/([^/]+)/);
265
265
  if (match) throw new SandboxNotFoundError(match[1]);
266
266
  }
267
267
  throw new RemoteAPIError(404, message);
268
268
  }
269
269
  if (status === 409) {
270
- if (path.includes("sandbox-pools") || path.includes("pools")) {
271
- const match = path.match(/sandbox-pools\/([^/]+)/);
270
+ if (path2.includes("sandbox-pools") || path2.includes("pools")) {
271
+ const match = path2.match(/sandbox-pools\/([^/]+)/);
272
272
  if (match) throw new PoolInUseError(match[1], message);
273
273
  }
274
274
  }
@@ -296,6 +296,7 @@ var SandboxStatus = /* @__PURE__ */ ((SandboxStatus2) => {
296
296
  SandboxStatus2["PENDING"] = "pending";
297
297
  SandboxStatus2["RUNNING"] = "running";
298
298
  SandboxStatus2["SNAPSHOTTING"] = "snapshotting";
299
+ SandboxStatus2["SUSPENDING"] = "suspending";
299
300
  SandboxStatus2["SUSPENDED"] = "suspended";
300
301
  SandboxStatus2["TERMINATED"] = "terminated";
301
302
  return SandboxStatus2;
@@ -468,18 +469,205 @@ function resolveProxyTarget(proxyUrl, sandboxId) {
468
469
  };
469
470
  }
470
471
  }
471
- function lifecyclePath(path, isLocal, namespace) {
472
+ function lifecyclePath(path2, isLocal, namespace) {
472
473
  if (isLocal) {
473
- return `/v1/namespaces/${namespace}/${path}`;
474
+ return `/v1/namespaces/${namespace}/${path2}`;
474
475
  }
475
- return `/${path}`;
476
+ return `/${path2}`;
476
477
  }
477
478
 
478
479
  // src/sandbox.ts
480
+ import WebSocket from "ws";
481
+ var PTY_OP_DATA = 0;
482
+ var PTY_OP_RESIZE = 1;
483
+ var PTY_OP_READY = 2;
484
+ var PTY_OP_EXIT = 3;
485
+ var Pty = class {
486
+ sessionId;
487
+ token;
488
+ wsUrl;
489
+ wsHeaders;
490
+ killSession;
491
+ socket = null;
492
+ connectPromise = null;
493
+ intentionalDisconnect = false;
494
+ exitCode = null;
495
+ waitSettled = false;
496
+ dataHandlers = /* @__PURE__ */ new Set();
497
+ exitHandlers = /* @__PURE__ */ new Set();
498
+ waitPromise;
499
+ resolveWait;
500
+ rejectWait;
501
+ constructor(options) {
502
+ this.sessionId = options.sessionId;
503
+ this.token = options.token;
504
+ this.wsUrl = options.wsUrl;
505
+ this.wsHeaders = options.wsHeaders;
506
+ this.killSession = options.killSession;
507
+ this.waitPromise = new Promise((resolve, reject) => {
508
+ this.resolveWait = resolve;
509
+ this.rejectWait = reject;
510
+ });
511
+ }
512
+ onData(handler) {
513
+ this.dataHandlers.add(handler);
514
+ return () => this.dataHandlers.delete(handler);
515
+ }
516
+ onExit(handler) {
517
+ this.exitHandlers.add(handler);
518
+ if (this.exitCode != null) {
519
+ queueMicrotask(() => handler(this.exitCode));
520
+ }
521
+ return () => this.exitHandlers.delete(handler);
522
+ }
523
+ async connect() {
524
+ if (this.socket?.readyState === WebSocket.OPEN) {
525
+ return this;
526
+ }
527
+ if (this.connectPromise) {
528
+ return this.connectPromise;
529
+ }
530
+ this.intentionalDisconnect = false;
531
+ this.connectPromise = new Promise((resolve, reject) => {
532
+ let opened = false;
533
+ const socket = new WebSocket(this.wsUrl, {
534
+ headers: this.wsHeaders
535
+ });
536
+ this.socket = socket;
537
+ socket.on("open", async () => {
538
+ try {
539
+ await sendPtyFrame(socket, Buffer.from([PTY_OP_READY]));
540
+ opened = true;
541
+ resolve(this);
542
+ } catch (error) {
543
+ reject(error);
544
+ }
545
+ });
546
+ socket.on("message", (message) => {
547
+ const bytes = normalizePtyMessage(message);
548
+ const opcode = bytes[0];
549
+ if (opcode === PTY_OP_DATA) {
550
+ const payload = bytes.subarray(1);
551
+ for (const handler of this.dataHandlers) {
552
+ handler(payload);
553
+ }
554
+ return;
555
+ }
556
+ if (opcode === PTY_OP_EXIT && bytes.length >= 5) {
557
+ this.finishWait(bytes.readInt32BE(1));
558
+ }
559
+ });
560
+ socket.on("close", (code, reason) => {
561
+ const closeReason = Buffer.isBuffer(reason) ? reason.toString("utf8") : String(reason);
562
+ if (this.socket === socket) {
563
+ this.socket = null;
564
+ }
565
+ this.connectPromise = null;
566
+ if (this.exitCode != null) {
567
+ this.finishWait(this.exitCode);
568
+ return;
569
+ }
570
+ if (closeReason.startsWith("exit:")) {
571
+ const parsed = Number.parseInt(closeReason.slice(5), 10);
572
+ this.finishWait(Number.isNaN(parsed) ? -1 : parsed);
573
+ return;
574
+ }
575
+ if (this.intentionalDisconnect) {
576
+ this.intentionalDisconnect = false;
577
+ return;
578
+ }
579
+ if (!opened) {
580
+ reject(new SandboxError(
581
+ `PTY websocket closed before READY completed: ${code} ${closeReason || "no reason"}`
582
+ ));
583
+ return;
584
+ }
585
+ if (closeReason === "session terminated") {
586
+ this.failWait(new SandboxError("PTY session terminated"));
587
+ return;
588
+ }
589
+ this.failWait(
590
+ new SandboxError(
591
+ `PTY websocket closed unexpectedly: ${code} ${closeReason || "no reason"}`
592
+ )
593
+ );
594
+ });
595
+ socket.on("error", (error) => {
596
+ if (!opened) {
597
+ reject(error);
598
+ }
599
+ });
600
+ });
601
+ return this.connectPromise;
602
+ }
603
+ async sendInput(input) {
604
+ const socket = this.requireOpenSocket();
605
+ await sendPtyFrame(socket, encodePtyInput(input));
606
+ }
607
+ async resize(cols, rows) {
608
+ const socket = this.requireOpenSocket();
609
+ await sendPtyFrame(socket, encodePtyResize(cols, rows));
610
+ }
611
+ disconnect(code = 1e3, reason = "client disconnect") {
612
+ if (!this.socket) return;
613
+ this.intentionalDisconnect = true;
614
+ this.socket.close(code, reason);
615
+ }
616
+ wait() {
617
+ return this.waitPromise;
618
+ }
619
+ async kill() {
620
+ await this.killSession();
621
+ }
622
+ requireOpenSocket() {
623
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
624
+ throw new SandboxError("PTY is not connected");
625
+ }
626
+ return this.socket;
627
+ }
628
+ finishWait(exitCode) {
629
+ if (this.waitSettled) return;
630
+ this.waitSettled = true;
631
+ this.exitCode = exitCode;
632
+ for (const handler of this.exitHandlers) {
633
+ handler(exitCode);
634
+ }
635
+ this.resolveWait(exitCode);
636
+ }
637
+ failWait(error) {
638
+ if (this.waitSettled) return;
639
+ this.waitSettled = true;
640
+ this.rejectWait(error);
641
+ }
642
+ };
643
+ function normalizePtyMessage(message) {
644
+ if (Buffer.isBuffer(message)) return message;
645
+ if (Array.isArray(message)) {
646
+ return Buffer.concat(message.map((part) => Buffer.from(part)));
647
+ }
648
+ return Buffer.from(message);
649
+ }
650
+ function encodePtyInput(input) {
651
+ const payload = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
652
+ return Buffer.concat([Buffer.from([PTY_OP_DATA]), payload]);
653
+ }
654
+ function encodePtyResize(cols, rows) {
655
+ const frame = Buffer.alloc(5);
656
+ frame[0] = PTY_OP_RESIZE;
657
+ frame.writeUInt16BE(cols, 1);
658
+ frame.writeUInt16BE(rows, 3);
659
+ return frame;
660
+ }
661
+ function sendPtyFrame(socket, frame) {
662
+ return new Promise((resolve, reject) => {
663
+ socket.send(frame, (error) => error ? reject(error) : resolve());
664
+ });
665
+ }
479
666
  var Sandbox = class {
480
667
  sandboxId;
481
668
  http;
482
669
  baseUrl;
670
+ wsHeaders;
483
671
  ownsSandbox = false;
484
672
  lifecycleClient = null;
485
673
  constructor(options) {
@@ -487,6 +675,19 @@ var Sandbox = class {
487
675
  const proxyUrl = options.proxyUrl ?? SANDBOX_PROXY_URL;
488
676
  const { baseUrl, hostHeader } = resolveProxyTarget(proxyUrl, options.sandboxId);
489
677
  this.baseUrl = baseUrl;
678
+ this.wsHeaders = {};
679
+ if (options.apiKey) {
680
+ this.wsHeaders.Authorization = `Bearer ${options.apiKey}`;
681
+ }
682
+ if (options.organizationId) {
683
+ this.wsHeaders["X-Forwarded-Organization-Id"] = options.organizationId;
684
+ }
685
+ if (options.projectId) {
686
+ this.wsHeaders["X-Forwarded-Project-Id"] = options.projectId;
687
+ }
688
+ if (hostHeader) {
689
+ this.wsHeaders.Host = hostHeader;
690
+ }
490
691
  this.http = new HttpClient({
491
692
  baseUrl,
492
693
  apiKey: options.apiKey,
@@ -665,29 +866,29 @@ var Sandbox = class {
665
866
  }
666
867
  }
667
868
  // --- File operations ---
668
- async readFile(path) {
869
+ async readFile(path2) {
669
870
  return this.http.requestBytes(
670
871
  "GET",
671
- `/api/v1/files?path=${encodeURIComponent(path)}`
872
+ `/api/v1/files?path=${encodeURIComponent(path2)}`
672
873
  );
673
874
  }
674
- async writeFile(path, content) {
875
+ async writeFile(path2, content) {
675
876
  await this.http.requestBytes(
676
877
  "PUT",
677
- `/api/v1/files?path=${encodeURIComponent(path)}`,
878
+ `/api/v1/files?path=${encodeURIComponent(path2)}`,
678
879
  { body: content, contentType: "application/octet-stream" }
679
880
  );
680
881
  }
681
- async deleteFile(path) {
882
+ async deleteFile(path2) {
682
883
  await this.http.requestJson(
683
884
  "DELETE",
684
- `/api/v1/files?path=${encodeURIComponent(path)}`
885
+ `/api/v1/files?path=${encodeURIComponent(path2)}`
685
886
  );
686
887
  }
687
- async listDirectory(path) {
888
+ async listDirectory(path2) {
688
889
  const raw = await this.http.requestJson(
689
890
  "GET",
690
- `/api/v1/files/list?path=${encodeURIComponent(path)}`
891
+ `/api/v1/files/list?path=${encodeURIComponent(path2)}`
691
892
  );
692
893
  return fromSnakeKeys(raw);
693
894
  }
@@ -708,6 +909,43 @@ var Sandbox = class {
708
909
  );
709
910
  return fromSnakeKeys(raw);
710
911
  }
912
+ async createPty(options) {
913
+ const { onData, onExit, ...createOptions } = options;
914
+ const session = await this.createPtySession(createOptions);
915
+ try {
916
+ return await this.connectPty(session.sessionId, session.token, { onData, onExit });
917
+ } catch (error) {
918
+ try {
919
+ await this.http.requestResponse("DELETE", `/api/v1/pty/${session.sessionId}`);
920
+ } catch {
921
+ }
922
+ throw error;
923
+ }
924
+ }
925
+ async connectPty(sessionId, token, options) {
926
+ const wsUrl = new URL(this.ptyWsUrl(sessionId, token));
927
+ const authToken = wsUrl.searchParams.get("token") ?? token;
928
+ const pty = new Pty({
929
+ sessionId,
930
+ token: authToken,
931
+ wsUrl: wsUrl.toString(),
932
+ wsHeaders: {
933
+ ...this.wsHeaders,
934
+ "X-PTY-Token": authToken
935
+ },
936
+ killSession: async () => {
937
+ await this.http.requestResponse("DELETE", `/api/v1/pty/${sessionId}`);
938
+ }
939
+ });
940
+ if (options?.onData) {
941
+ pty.onData(options.onData);
942
+ }
943
+ if (options?.onExit) {
944
+ pty.onExit(options.onExit);
945
+ }
946
+ await pty.connect();
947
+ return pty;
948
+ }
711
949
  ptyWsUrl(sessionId, token) {
712
950
  let wsBase;
713
951
  if (this.baseUrl.startsWith("https://")) {
@@ -835,6 +1073,15 @@ var SandboxClient = class _SandboxClient {
835
1073
  async update(sandboxId, options) {
836
1074
  const body = {};
837
1075
  if (options.name != null) body.name = options.name;
1076
+ if (options.allowUnauthenticatedAccess != null) {
1077
+ body.allow_unauthenticated_access = options.allowUnauthenticatedAccess;
1078
+ }
1079
+ if (options.exposedPorts != null) {
1080
+ body.exposed_ports = normalizeUserPorts(options.exposedPorts);
1081
+ }
1082
+ if (Object.keys(body).length === 0) {
1083
+ throw new SandboxError("At least one sandbox update field must be provided.");
1084
+ }
838
1085
  const raw = await this.http.requestJson(
839
1086
  "PATCH",
840
1087
  this.path(`sandboxes/${sandboxId}`),
@@ -842,12 +1089,54 @@ var SandboxClient = class _SandboxClient {
842
1089
  );
843
1090
  return fromSnakeKeys(raw, "sandboxId");
844
1091
  }
1092
+ async getPortAccess(sandboxId) {
1093
+ const info = await this.get(sandboxId);
1094
+ return {
1095
+ allowUnauthenticatedAccess: info.allowUnauthenticatedAccess ?? false,
1096
+ exposedPorts: dedupeAndSortPorts(info.exposedPorts ?? []),
1097
+ sandboxUrl: info.sandboxUrl
1098
+ };
1099
+ }
1100
+ async exposePorts(sandboxId, ports, options) {
1101
+ const requestedPorts = normalizeUserPorts(ports);
1102
+ const current = await this.getPortAccess(sandboxId);
1103
+ const desiredPorts = dedupeAndSortPorts([
1104
+ ...current.exposedPorts,
1105
+ ...requestedPorts
1106
+ ]);
1107
+ return this.update(sandboxId, {
1108
+ allowUnauthenticatedAccess: options?.allowUnauthenticatedAccess ?? current.allowUnauthenticatedAccess,
1109
+ exposedPorts: desiredPorts
1110
+ });
1111
+ }
1112
+ async unexposePorts(sandboxId, ports) {
1113
+ const requestedPorts = normalizeUserPorts(ports);
1114
+ const current = await this.getPortAccess(sandboxId);
1115
+ const toRemove = new Set(requestedPorts);
1116
+ const desiredPorts = current.exposedPorts.filter((port) => !toRemove.has(port));
1117
+ return this.update(sandboxId, {
1118
+ allowUnauthenticatedAccess: desiredPorts.length ? current.allowUnauthenticatedAccess : false,
1119
+ exposedPorts: desiredPorts
1120
+ });
1121
+ }
845
1122
  async delete(sandboxId) {
846
1123
  await this.http.requestJson(
847
1124
  "DELETE",
848
1125
  this.path(`sandboxes/${sandboxId}`)
849
1126
  );
850
1127
  }
1128
+ async suspend(sandboxId) {
1129
+ await this.http.requestResponse(
1130
+ "POST",
1131
+ this.path(`sandboxes/${sandboxId}/suspend`)
1132
+ );
1133
+ }
1134
+ async resume(sandboxId) {
1135
+ await this.http.requestResponse(
1136
+ "POST",
1137
+ this.path(`sandboxes/${sandboxId}/resume`)
1138
+ );
1139
+ }
851
1140
  async claim(poolId) {
852
1141
  const raw = await this.http.requestJson(
853
1142
  "POST",
@@ -1015,6 +1304,22 @@ var SandboxClient = class _SandboxClient {
1015
1304
  function sleep3(ms) {
1016
1305
  return new Promise((resolve) => setTimeout(resolve, ms));
1017
1306
  }
1307
+ var RESERVED_SANDBOX_MANAGEMENT_PORT = 9501;
1308
+ function normalizeUserPorts(ports) {
1309
+ return dedupeAndSortPorts(ports.map(validateUserPort));
1310
+ }
1311
+ function validateUserPort(port) {
1312
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1313
+ throw new SandboxError(`invalid port '${port}'`);
1314
+ }
1315
+ if (port === RESERVED_SANDBOX_MANAGEMENT_PORT) {
1316
+ throw new SandboxError("port 9501 is reserved for sandbox management");
1317
+ }
1318
+ return port;
1319
+ }
1320
+ function dedupeAndSortPorts(ports) {
1321
+ return [...new Set(ports)].sort((a, b) => a - b);
1322
+ }
1018
1323
 
1019
1324
  // src/cloud-client.ts
1020
1325
  var CloudClient = class _CloudClient {
@@ -1081,19 +1386,19 @@ var CloudClient = class _CloudClient {
1081
1386
  return fromSnakeKeys(raw);
1082
1387
  }
1083
1388
  async runRequest(applicationName, inputs = []) {
1084
- const path = this.namespacePath(
1389
+ const path2 = this.namespacePath(
1085
1390
  `applications/${encodeURIComponent(applicationName)}`
1086
1391
  );
1087
- const response = inputs.length === 0 ? await this.http.requestResponse("POST", path, {
1392
+ const response = inputs.length === 0 ? await this.http.requestResponse("POST", path2, {
1088
1393
  body: new Uint8Array(),
1089
1394
  headers: { Accept: "application/json" }
1090
- }) : inputs.length === 1 && inputs[0].name === "0" ? await this.http.requestResponse("POST", path, {
1395
+ }) : inputs.length === 1 && inputs[0].name === "0" ? await this.http.requestResponse("POST", path2, {
1091
1396
  body: toRequestBody(inputs[0].data),
1092
1397
  headers: {
1093
1398
  Accept: "application/json",
1094
1399
  "Content-Type": inputs[0].contentType
1095
1400
  }
1096
- }) : await this.runMultipartRequest(path, inputs);
1401
+ }) : await this.runMultipartRequest(path2, inputs);
1097
1402
  const body = await parseJsonResponse(response);
1098
1403
  const requestId = body?.request_id;
1099
1404
  if (!requestId) {
@@ -1247,7 +1552,7 @@ var CloudClient = class _CloudClient {
1247
1552
  yield fromSnakeKeys(event);
1248
1553
  }
1249
1554
  }
1250
- async runMultipartRequest(path, inputs) {
1555
+ async runMultipartRequest(path2, inputs) {
1251
1556
  const form = new FormData();
1252
1557
  for (const input of inputs) {
1253
1558
  form.append(
@@ -1256,7 +1561,7 @@ var CloudClient = class _CloudClient {
1256
1561
  input.name
1257
1562
  );
1258
1563
  }
1259
- return this.http.requestResponse("POST", path, {
1564
+ return this.http.requestResponse("POST", path2, {
1260
1565
  body: form,
1261
1566
  headers: { Accept: "application/json" }
1262
1567
  });
@@ -1391,14 +1696,859 @@ var APIClient = class {
1391
1696
  return this.cloudClient.requestOutput(applicationName, requestId);
1392
1697
  }
1393
1698
  };
1699
+
1700
+ // src/sandbox-image.ts
1701
+ import { readFile, readdir, stat } from "fs/promises";
1702
+ import path from "path";
1703
+ import { parseArgs } from "util";
1704
+
1705
+ // src/image.ts
1706
+ import { randomUUID } from "crypto";
1707
+ var ImageBuildOperationType = {
1708
+ ADD: "ADD",
1709
+ COPY: "COPY",
1710
+ ENV: "ENV",
1711
+ RUN: "RUN",
1712
+ WORKDIR: "WORKDIR"
1713
+ };
1714
+ function cloneOperation(op) {
1715
+ return {
1716
+ type: op.type,
1717
+ args: [...op.args],
1718
+ options: { ...op.options }
1719
+ };
1720
+ }
1721
+ var Image = class {
1722
+ _id;
1723
+ _name;
1724
+ _tag;
1725
+ _baseImage;
1726
+ _buildOperations;
1727
+ constructor(nameOrOptions = {}, tag = "latest", baseImage = null) {
1728
+ this._id = randomUUID();
1729
+ this._buildOperations = [];
1730
+ if (typeof nameOrOptions === "string") {
1731
+ this._name = nameOrOptions;
1732
+ this._tag = tag;
1733
+ this._baseImage = baseImage;
1734
+ return;
1735
+ }
1736
+ this._name = nameOrOptions.name ?? "default";
1737
+ this._tag = nameOrOptions.tag ?? "latest";
1738
+ this._baseImage = nameOrOptions.baseImage ?? null;
1739
+ }
1740
+ get name() {
1741
+ return this._name;
1742
+ }
1743
+ get tag() {
1744
+ return this._tag;
1745
+ }
1746
+ get baseImage() {
1747
+ return this._baseImage;
1748
+ }
1749
+ get buildOperations() {
1750
+ return this._buildOperations.map(cloneOperation);
1751
+ }
1752
+ add(src, dest, options = void 0) {
1753
+ return this._addOperation({
1754
+ type: ImageBuildOperationType.ADD,
1755
+ args: [src, dest],
1756
+ options: options == null ? {} : { ...options }
1757
+ });
1758
+ }
1759
+ copy(src, dest, options = void 0) {
1760
+ return this._addOperation({
1761
+ type: ImageBuildOperationType.COPY,
1762
+ args: [src, dest],
1763
+ options: options == null ? {} : { ...options }
1764
+ });
1765
+ }
1766
+ env(key, value) {
1767
+ return this._addOperation({
1768
+ type: ImageBuildOperationType.ENV,
1769
+ args: [key, value],
1770
+ options: {}
1771
+ });
1772
+ }
1773
+ run(commands, options = void 0) {
1774
+ return this._addOperation({
1775
+ type: ImageBuildOperationType.RUN,
1776
+ args: Array.isArray(commands) ? [...commands] : [commands],
1777
+ options: options == null ? {} : { ...options }
1778
+ });
1779
+ }
1780
+ workdir(directory) {
1781
+ return this._addOperation({
1782
+ type: ImageBuildOperationType.WORKDIR,
1783
+ args: [directory],
1784
+ options: {}
1785
+ });
1786
+ }
1787
+ _addOperation(op) {
1788
+ this._buildOperations.push(op);
1789
+ return this;
1790
+ }
1791
+ };
1792
+ function renderOptions(options) {
1793
+ const entries = Object.entries(options);
1794
+ if (entries.length === 0) {
1795
+ return "";
1796
+ }
1797
+ return ` ${entries.map(([key, value]) => `--${key}=${value}`).join(" ")}`;
1798
+ }
1799
+ function renderBuildOp(op) {
1800
+ const options = renderOptions(op.options);
1801
+ if (op.type === ImageBuildOperationType.ENV) {
1802
+ return `ENV${options} ${op.args[0]}=${JSON.stringify(op.args[1])}`;
1803
+ }
1804
+ return `${op.type}${options} ${op.args.join(" ")}`;
1805
+ }
1806
+ function dockerfileContent(image) {
1807
+ const lines = image.baseImage == null ? [] : [`FROM ${image.baseImage}`];
1808
+ lines.push(...image.buildOperations.map((op) => renderBuildOp(op)));
1809
+ return lines.join("\n");
1810
+ }
1811
+
1812
+ // src/sandbox-image.ts
1813
+ var BUILD_SANDBOX_PIP_ENV = { PIP_BREAK_SYSTEM_PACKAGES: "1" };
1814
+ var IGNORED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
1815
+ "CMD",
1816
+ "ENTRYPOINT",
1817
+ "EXPOSE",
1818
+ "HEALTHCHECK",
1819
+ "LABEL",
1820
+ "STOPSIGNAL",
1821
+ "VOLUME"
1822
+ ]);
1823
+ var UNSUPPORTED_DOCKERFILE_INSTRUCTIONS = /* @__PURE__ */ new Set([
1824
+ "ARG",
1825
+ "ONBUILD",
1826
+ "SHELL",
1827
+ "USER"
1828
+ ]);
1829
+ function defaultRegisteredName(dockerfilePath) {
1830
+ const parsed = path.parse(dockerfilePath);
1831
+ if (parsed.name.toLowerCase() === "dockerfile") {
1832
+ const parentName = path.basename(path.dirname(dockerfilePath)).trim();
1833
+ return parentName || "sandbox-image";
1834
+ }
1835
+ return parsed.name || "sandbox-image";
1836
+ }
1837
+ function logicalDockerfileLines(dockerfileText) {
1838
+ const logicalLines = [];
1839
+ let parts = [];
1840
+ let startLine = null;
1841
+ for (const [index, rawLine] of dockerfileText.split(/\r?\n/).entries()) {
1842
+ const lineNumber = index + 1;
1843
+ const stripped = rawLine.trim();
1844
+ if (parts.length === 0 && (!stripped || stripped.startsWith("#"))) {
1845
+ continue;
1846
+ }
1847
+ if (startLine == null) {
1848
+ startLine = lineNumber;
1849
+ }
1850
+ let line = rawLine.replace(/\s+$/, "");
1851
+ const continued = line.endsWith("\\");
1852
+ if (continued) {
1853
+ line = line.slice(0, -1);
1854
+ }
1855
+ const normalized = line.trim();
1856
+ if (normalized && !normalized.startsWith("#")) {
1857
+ parts.push(normalized);
1858
+ }
1859
+ if (continued) {
1860
+ continue;
1861
+ }
1862
+ if (parts.length > 0) {
1863
+ logicalLines.push({
1864
+ lineNumber: startLine ?? lineNumber,
1865
+ line: parts.join(" ")
1866
+ });
1867
+ }
1868
+ parts = [];
1869
+ startLine = null;
1870
+ }
1871
+ if (parts.length > 0) {
1872
+ logicalLines.push({
1873
+ lineNumber: startLine ?? 1,
1874
+ line: parts.join(" ")
1875
+ });
1876
+ }
1877
+ return logicalLines;
1878
+ }
1879
+ function splitInstruction(line, lineNumber) {
1880
+ const trimmed = line.trim();
1881
+ if (!trimmed) {
1882
+ throw new Error(`line ${lineNumber}: empty Dockerfile instruction`);
1883
+ }
1884
+ const match = trimmed.match(/^(\S+)(?:\s+(.*))?$/);
1885
+ if (!match) {
1886
+ throw new Error(`line ${lineNumber}: invalid Dockerfile instruction`);
1887
+ }
1888
+ return {
1889
+ keyword: match[1].toUpperCase(),
1890
+ value: (match[2] ?? "").trim()
1891
+ };
1892
+ }
1893
+ function shellSplit(input) {
1894
+ const tokens = [];
1895
+ let current = "";
1896
+ let quote = null;
1897
+ let escape = false;
1898
+ for (let i = 0; i < input.length; i++) {
1899
+ const char = input[i];
1900
+ if (escape) {
1901
+ current += char;
1902
+ escape = false;
1903
+ continue;
1904
+ }
1905
+ if (quote == null) {
1906
+ if (/\s/.test(char)) {
1907
+ if (current) {
1908
+ tokens.push(current);
1909
+ current = "";
1910
+ }
1911
+ continue;
1912
+ }
1913
+ if (char === "'" || char === '"') {
1914
+ quote = char;
1915
+ continue;
1916
+ }
1917
+ if (char === "\\") {
1918
+ escape = true;
1919
+ continue;
1920
+ }
1921
+ current += char;
1922
+ continue;
1923
+ }
1924
+ if (quote === "'") {
1925
+ if (char === "'") {
1926
+ quote = null;
1927
+ } else {
1928
+ current += char;
1929
+ }
1930
+ continue;
1931
+ }
1932
+ if (char === '"') {
1933
+ quote = null;
1934
+ continue;
1935
+ }
1936
+ if (char === "\\") {
1937
+ const next = input[++i];
1938
+ if (next == null) {
1939
+ throw new Error(`unterminated escape sequence in '${input}'`);
1940
+ }
1941
+ current += next;
1942
+ continue;
1943
+ }
1944
+ current += char;
1945
+ }
1946
+ if (escape) {
1947
+ throw new Error(`unterminated escape sequence in '${input}'`);
1948
+ }
1949
+ if (quote != null) {
1950
+ throw new Error(`unterminated quoted string in '${input}'`);
1951
+ }
1952
+ if (current) {
1953
+ tokens.push(current);
1954
+ }
1955
+ return tokens;
1956
+ }
1957
+ function shellQuote(value) {
1958
+ if (!value) {
1959
+ return "''";
1960
+ }
1961
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1962
+ }
1963
+ function stripLeadingFlags(value) {
1964
+ const flags = {};
1965
+ let remaining = value.trimStart();
1966
+ while (remaining.startsWith("--")) {
1967
+ const firstSpace = remaining.indexOf(" ");
1968
+ if (firstSpace === -1) {
1969
+ throw new Error(`invalid Dockerfile flag syntax: ${value}`);
1970
+ }
1971
+ const token = remaining.slice(0, firstSpace);
1972
+ const rest = remaining.slice(firstSpace + 1).trimStart();
1973
+ const flagBody = token.slice(2);
1974
+ if (flagBody.includes("=")) {
1975
+ const [key, flagValue2] = flagBody.split(/=(.*)/s, 2);
1976
+ flags[key] = flagValue2;
1977
+ remaining = rest;
1978
+ continue;
1979
+ }
1980
+ const [flagValue, ...restTokens] = shellSplit(rest);
1981
+ if (flagValue == null) {
1982
+ throw new Error(`missing value for Dockerfile flag '${token}'`);
1983
+ }
1984
+ flags[flagBody] = flagValue;
1985
+ remaining = restTokens.join(" ");
1986
+ }
1987
+ return { flags, remaining };
1988
+ }
1989
+ function parseFromValue(value, lineNumber) {
1990
+ const { remaining } = stripLeadingFlags(value);
1991
+ const tokens = shellSplit(remaining);
1992
+ if (tokens.length === 0) {
1993
+ throw new Error(`line ${lineNumber}: FROM must include a base image`);
1994
+ }
1995
+ if (tokens.length > 1 && tokens[1].toLowerCase() !== "as") {
1996
+ throw new Error(`line ${lineNumber}: unsupported FROM syntax '${value}'`);
1997
+ }
1998
+ return tokens[0];
1999
+ }
2000
+ function parseCopyLikeValues(value, lineNumber, keyword) {
2001
+ const { flags, remaining } = stripLeadingFlags(value);
2002
+ if ("from" in flags) {
2003
+ throw new Error(
2004
+ `line ${lineNumber}: ${keyword} --from is not supported for sandbox image creation`
2005
+ );
2006
+ }
2007
+ const payload = remaining.trim();
2008
+ if (!payload) {
2009
+ throw new Error(
2010
+ `line ${lineNumber}: ${keyword} must include source and destination`
2011
+ );
2012
+ }
2013
+ let parts;
2014
+ if (payload.startsWith("[")) {
2015
+ let parsed;
2016
+ try {
2017
+ parsed = JSON.parse(payload);
2018
+ } catch (error) {
2019
+ throw new Error(
2020
+ `line ${lineNumber}: invalid JSON array syntax for ${keyword}: ${error.message}`
2021
+ );
2022
+ }
2023
+ if (!Array.isArray(parsed) || parsed.length < 2 || parsed.some((item) => typeof item !== "string")) {
2024
+ throw new Error(
2025
+ `line ${lineNumber}: ${keyword} JSON array form requires at least two string values`
2026
+ );
2027
+ }
2028
+ parts = parsed;
2029
+ } else {
2030
+ parts = shellSplit(payload);
2031
+ if (parts.length < 2) {
2032
+ throw new Error(
2033
+ `line ${lineNumber}: ${keyword} must include at least one source and one destination`
2034
+ );
2035
+ }
2036
+ }
2037
+ return {
2038
+ flags,
2039
+ sources: parts.slice(0, -1),
2040
+ destination: parts[parts.length - 1]
2041
+ };
2042
+ }
2043
+ function parseEnvPairs(value, lineNumber) {
2044
+ const tokens = shellSplit(value);
2045
+ if (tokens.length === 0) {
2046
+ throw new Error(`line ${lineNumber}: ENV must include a key and value`);
2047
+ }
2048
+ if (tokens.every((token) => token.includes("="))) {
2049
+ return tokens.map((token) => {
2050
+ const [key, envValue] = token.split(/=(.*)/s, 2);
2051
+ if (!key) {
2052
+ throw new Error(`line ${lineNumber}: invalid ENV token '${token}'`);
2053
+ }
2054
+ return [key, envValue];
2055
+ });
2056
+ }
2057
+ if (tokens.length < 2) {
2058
+ throw new Error(`line ${lineNumber}: ENV must include a key and value`);
2059
+ }
2060
+ return [[tokens[0], tokens.slice(1).join(" ")]];
2061
+ }
2062
+ function resolveContainerPath(containerPath, workingDir) {
2063
+ if (!containerPath) {
2064
+ return workingDir;
2065
+ }
2066
+ const normalized = containerPath.startsWith("/") ? path.posix.normalize(containerPath) : path.posix.normalize(path.posix.join(workingDir, containerPath));
2067
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
2068
+ }
2069
+ function buildPlanFromDockerfileText(dockerfileText, dockerfilePath, contextDir, registeredName) {
2070
+ let baseImage;
2071
+ const instructions = [];
2072
+ for (const logicalLine of logicalDockerfileLines(dockerfileText)) {
2073
+ const { keyword, value } = splitInstruction(
2074
+ logicalLine.line,
2075
+ logicalLine.lineNumber
2076
+ );
2077
+ if (keyword === "FROM") {
2078
+ if (baseImage != null) {
2079
+ throw new Error(
2080
+ `line ${logicalLine.lineNumber}: multi-stage Dockerfiles are not supported for sandbox image creation`
2081
+ );
2082
+ }
2083
+ baseImage = parseFromValue(value, logicalLine.lineNumber);
2084
+ continue;
2085
+ }
2086
+ if (UNSUPPORTED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
2087
+ throw new Error(
2088
+ `line ${logicalLine.lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
2089
+ );
2090
+ }
2091
+ instructions.push({
2092
+ keyword,
2093
+ value,
2094
+ lineNumber: logicalLine.lineNumber
2095
+ });
2096
+ }
2097
+ if (!baseImage) {
2098
+ throw new Error("Dockerfile must contain a FROM instruction");
2099
+ }
2100
+ return {
2101
+ dockerfilePath,
2102
+ contextDir,
2103
+ registeredName: registeredName ?? defaultRegisteredName(dockerfilePath),
2104
+ dockerfileText,
2105
+ baseImage,
2106
+ instructions
2107
+ };
2108
+ }
2109
+ async function loadDockerfilePlan(dockerfilePath, registeredName) {
2110
+ const resolvedPath = path.resolve(dockerfilePath);
2111
+ const fileStats = await stat(resolvedPath).catch(() => null);
2112
+ if (!fileStats?.isFile()) {
2113
+ throw new Error(`Dockerfile not found: ${dockerfilePath}`);
2114
+ }
2115
+ const dockerfileText = await readFile(resolvedPath, "utf8");
2116
+ return buildPlanFromDockerfileText(
2117
+ dockerfileText,
2118
+ resolvedPath,
2119
+ path.dirname(resolvedPath),
2120
+ registeredName
2121
+ );
2122
+ }
2123
+ function loadImagePlan(image, options = {}) {
2124
+ const contextDir = path.resolve(options.contextDir ?? process.cwd());
2125
+ const dockerfileText = dockerfileContent(image);
2126
+ const logicalLines = logicalDockerfileLines(dockerfileText);
2127
+ const instructions = image.baseImage == null ? logicalLines : logicalLines.slice(1);
2128
+ return {
2129
+ dockerfilePath: path.join(contextDir, "Dockerfile"),
2130
+ contextDir,
2131
+ registeredName: options.registeredName ?? image.name,
2132
+ dockerfileText,
2133
+ baseImage: image.baseImage ?? void 0,
2134
+ instructions: instructions.map(({ line, lineNumber }) => {
2135
+ const parsed = splitInstruction(line, lineNumber);
2136
+ return {
2137
+ keyword: parsed.keyword,
2138
+ value: parsed.value,
2139
+ lineNumber
2140
+ };
2141
+ })
2142
+ };
2143
+ }
2144
+ function defaultEmit(event) {
2145
+ process.stdout.write(`${JSON.stringify(event)}
2146
+ `);
2147
+ }
2148
+ function debugEnabled() {
2149
+ return ["1", "true", "yes", "on"].includes(
2150
+ (process.env.TENSORLAKE_DEBUG ?? "").toLowerCase()
2151
+ );
2152
+ }
2153
+ function buildContextFromEnv() {
2154
+ return {
2155
+ apiUrl: process.env.TENSORLAKE_API_URL ?? "https://api.tensorlake.ai",
2156
+ apiKey: process.env.TENSORLAKE_API_KEY,
2157
+ personalAccessToken: process.env.TENSORLAKE_PAT,
2158
+ namespace: process.env.INDEXIFY_NAMESPACE ?? "default",
2159
+ organizationId: process.env.TENSORLAKE_ORGANIZATION_ID,
2160
+ projectId: process.env.TENSORLAKE_PROJECT_ID,
2161
+ debug: debugEnabled()
2162
+ };
2163
+ }
2164
+ function createDefaultClient(context) {
2165
+ return new SandboxClient({
2166
+ apiUrl: context.apiUrl,
2167
+ apiKey: context.apiKey ?? context.personalAccessToken,
2168
+ organizationId: context.organizationId,
2169
+ projectId: context.projectId,
2170
+ namespace: context.namespace
2171
+ });
2172
+ }
2173
+ async function runChecked(sandbox, command, args, env, workingDir) {
2174
+ const result = await sandbox.run(command, {
2175
+ args,
2176
+ env,
2177
+ workingDir
2178
+ });
2179
+ if (result.exitCode !== 0) {
2180
+ throw new Error(
2181
+ `Command '${command} ${args.join(" ")}' failed with exit code ${result.exitCode}`
2182
+ );
2183
+ }
2184
+ return result;
2185
+ }
2186
+ async function runStreaming(sandbox, emit, sleep4, command, args = [], env, workingDir) {
2187
+ const proc = await sandbox.startProcess(command, {
2188
+ args,
2189
+ env,
2190
+ workingDir
2191
+ });
2192
+ let stdoutSeen = 0;
2193
+ let stderrSeen = 0;
2194
+ let info;
2195
+ while (true) {
2196
+ const stdoutResp = await sandbox.getStdout(proc.pid);
2197
+ emitOutputLines(emit, "stdout", stdoutResp, stdoutSeen);
2198
+ stdoutSeen = stdoutResp.lines.length;
2199
+ const stderrResp = await sandbox.getStderr(proc.pid);
2200
+ emitOutputLines(emit, "stderr", stderrResp, stderrSeen);
2201
+ stderrSeen = stderrResp.lines.length;
2202
+ info = await sandbox.getProcess(proc.pid);
2203
+ if (info.status !== "running" /* RUNNING */) {
2204
+ const finalStdout = await sandbox.getStdout(proc.pid);
2205
+ emitOutputLines(emit, "stdout", finalStdout, stdoutSeen);
2206
+ stdoutSeen = finalStdout.lines.length;
2207
+ const finalStderr = await sandbox.getStderr(proc.pid);
2208
+ emitOutputLines(emit, "stderr", finalStderr, stderrSeen);
2209
+ break;
2210
+ }
2211
+ await sleep4(300);
2212
+ }
2213
+ for (let i = 0; i < 10; i++) {
2214
+ if (info.exitCode != null || info.signal != null) {
2215
+ break;
2216
+ }
2217
+ await sleep4(200);
2218
+ info = await sandbox.getProcess(proc.pid);
2219
+ }
2220
+ const exitCode = info.exitCode != null ? info.exitCode : info.signal != null ? -info.signal : 0;
2221
+ if (exitCode !== 0) {
2222
+ throw new Error(
2223
+ `Command '${command} ${args.join(" ")}' failed with exit code ${exitCode}`
2224
+ );
2225
+ }
2226
+ }
2227
+ function emitOutputLines(emit, stream, response, seen) {
2228
+ for (const line of response.lines.slice(seen)) {
2229
+ emit({ type: "build_log", stream, message: line });
2230
+ }
2231
+ }
2232
+ function isPathWithinContext(contextDir, localPath) {
2233
+ const relative = path.relative(contextDir, localPath);
2234
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
2235
+ }
2236
+ function resolveContextSourcePath(contextDir, source) {
2237
+ const resolvedContextDir = path.resolve(contextDir);
2238
+ const resolvedSource = path.resolve(resolvedContextDir, source);
2239
+ if (!isPathWithinContext(resolvedContextDir, resolvedSource)) {
2240
+ throw new Error(`Local path escapes the build context: ${source}`);
2241
+ }
2242
+ return resolvedSource;
2243
+ }
2244
+ async function copyLocalPathToSandbox(sandbox, localPath, remotePath) {
2245
+ const fileStats = await stat(localPath).catch(() => null);
2246
+ if (!fileStats) {
2247
+ throw new Error(`Local path not found: ${localPath}`);
2248
+ }
2249
+ if (fileStats.isFile()) {
2250
+ await runChecked(sandbox, "mkdir", ["-p", path.posix.dirname(remotePath)]);
2251
+ await sandbox.writeFile(remotePath, await readFile(localPath));
2252
+ return;
2253
+ }
2254
+ if (!fileStats.isDirectory()) {
2255
+ throw new Error(`Local path not found: ${localPath}`);
2256
+ }
2257
+ const entries = await readdir(localPath, { withFileTypes: true });
2258
+ for (const entry of entries) {
2259
+ const sourcePath = path.join(localPath, entry.name);
2260
+ const destinationPath = path.posix.join(remotePath, entry.name);
2261
+ if (entry.isDirectory()) {
2262
+ await runChecked(sandbox, "mkdir", ["-p", destinationPath]);
2263
+ await copyLocalPathToSandbox(sandbox, sourcePath, destinationPath);
2264
+ } else if (entry.isFile()) {
2265
+ await runChecked(
2266
+ sandbox,
2267
+ "mkdir",
2268
+ ["-p", path.posix.dirname(destinationPath)]
2269
+ );
2270
+ await sandbox.writeFile(destinationPath, await readFile(sourcePath));
2271
+ }
2272
+ }
2273
+ }
2274
+ async function persistEnvVar(sandbox, processEnv, key, value) {
2275
+ const exportLine = `export ${key}=${shellQuote(value)}`;
2276
+ await runChecked(
2277
+ sandbox,
2278
+ "sh",
2279
+ ["-c", `printf '%s\\n' ${shellQuote(exportLine)} >> /etc/environment`],
2280
+ processEnv
2281
+ );
2282
+ }
2283
+ async function copyFromContext(sandbox, emit, contextDir, sources, destination, workingDir, keyword) {
2284
+ const destinationPath = resolveContainerPath(destination, workingDir);
2285
+ if (sources.length > 1 && !destinationPath.endsWith("/")) {
2286
+ throw new Error(
2287
+ `${keyword} with multiple sources requires a directory destination ending in '/'`
2288
+ );
2289
+ }
2290
+ for (const source of sources) {
2291
+ const localSource = resolveContextSourcePath(contextDir, source);
2292
+ const localStats = await stat(localSource).catch(() => null);
2293
+ if (!localStats) {
2294
+ throw new Error(`Local path not found: ${localSource}`);
2295
+ }
2296
+ let remoteDestination = destinationPath;
2297
+ if (sources.length > 1) {
2298
+ remoteDestination = path.posix.join(
2299
+ destinationPath.replace(/\/$/, ""),
2300
+ path.posix.basename(source.replace(/\/$/, ""))
2301
+ );
2302
+ } else if (localStats.isFile() && destinationPath.endsWith("/")) {
2303
+ remoteDestination = path.posix.join(
2304
+ destinationPath.replace(/\/$/, ""),
2305
+ path.basename(source)
2306
+ );
2307
+ }
2308
+ emit({
2309
+ type: "status",
2310
+ message: `${keyword} ${source} -> ${remoteDestination}`
2311
+ });
2312
+ await copyLocalPathToSandbox(sandbox, localSource, remoteDestination);
2313
+ }
2314
+ }
2315
+ async function addUrlToSandbox(sandbox, emit, url, destination, workingDir, processEnv, sleep4) {
2316
+ let destinationPath = resolveContainerPath(destination, workingDir);
2317
+ const parsedUrl = new URL(url);
2318
+ const fileName = path.posix.basename(parsedUrl.pathname.replace(/\/$/, "")) || "downloaded";
2319
+ if (destinationPath.endsWith("/")) {
2320
+ destinationPath = path.posix.join(destinationPath.replace(/\/$/, ""), fileName);
2321
+ }
2322
+ const parentDir = path.posix.dirname(destinationPath) || "/";
2323
+ emit({
2324
+ type: "status",
2325
+ message: `ADD ${url} -> ${destinationPath}`
2326
+ });
2327
+ await runChecked(sandbox, "mkdir", ["-p", parentDir], processEnv);
2328
+ await runStreaming(
2329
+ sandbox,
2330
+ emit,
2331
+ sleep4,
2332
+ "sh",
2333
+ [
2334
+ "-c",
2335
+ `curl -fsSL --location ${shellQuote(url)} -o ${shellQuote(destinationPath)}`
2336
+ ],
2337
+ processEnv,
2338
+ workingDir
2339
+ );
2340
+ }
2341
+ async function executeDockerfilePlan(sandbox, plan, emit, sleep4) {
2342
+ const processEnv = { ...BUILD_SANDBOX_PIP_ENV };
2343
+ let workingDir = "/";
2344
+ for (const instruction of plan.instructions) {
2345
+ const { keyword, value, lineNumber } = instruction;
2346
+ if (keyword === "RUN") {
2347
+ emit({ type: "status", message: `RUN ${value}` });
2348
+ await runStreaming(
2349
+ sandbox,
2350
+ emit,
2351
+ sleep4,
2352
+ "sh",
2353
+ ["-c", value],
2354
+ processEnv,
2355
+ workingDir
2356
+ );
2357
+ continue;
2358
+ }
2359
+ if (keyword === "WORKDIR") {
2360
+ const tokens = shellSplit(value);
2361
+ if (tokens.length !== 1) {
2362
+ throw new Error(`line ${lineNumber}: WORKDIR must include exactly one path`);
2363
+ }
2364
+ workingDir = resolveContainerPath(tokens[0], workingDir);
2365
+ emit({ type: "status", message: `WORKDIR ${workingDir}` });
2366
+ await runChecked(sandbox, "mkdir", ["-p", workingDir], processEnv);
2367
+ continue;
2368
+ }
2369
+ if (keyword === "ENV") {
2370
+ for (const [key, envValue] of parseEnvPairs(value, lineNumber)) {
2371
+ emit({ type: "status", message: `ENV ${key}=${envValue}` });
2372
+ processEnv[key] = envValue;
2373
+ await persistEnvVar(sandbox, processEnv, key, envValue);
2374
+ }
2375
+ continue;
2376
+ }
2377
+ if (keyword === "COPY") {
2378
+ const { sources, destination } = parseCopyLikeValues(
2379
+ value,
2380
+ lineNumber,
2381
+ keyword
2382
+ );
2383
+ await copyFromContext(
2384
+ sandbox,
2385
+ emit,
2386
+ plan.contextDir,
2387
+ sources,
2388
+ destination,
2389
+ workingDir,
2390
+ keyword
2391
+ );
2392
+ continue;
2393
+ }
2394
+ if (keyword === "ADD") {
2395
+ const { sources, destination } = parseCopyLikeValues(
2396
+ value,
2397
+ lineNumber,
2398
+ keyword
2399
+ );
2400
+ if (sources.length === 1 && /^https?:\/\//.test(sources[0])) {
2401
+ await addUrlToSandbox(
2402
+ sandbox,
2403
+ emit,
2404
+ sources[0],
2405
+ destination,
2406
+ workingDir,
2407
+ processEnv,
2408
+ sleep4
2409
+ );
2410
+ } else {
2411
+ await copyFromContext(
2412
+ sandbox,
2413
+ emit,
2414
+ plan.contextDir,
2415
+ sources,
2416
+ destination,
2417
+ workingDir,
2418
+ keyword
2419
+ );
2420
+ }
2421
+ continue;
2422
+ }
2423
+ if (IGNORED_DOCKERFILE_INSTRUCTIONS.has(keyword)) {
2424
+ emit({
2425
+ type: "warning",
2426
+ message: `Skipping Dockerfile instruction '${keyword}' during snapshot materialization. It is still preserved in the registered Dockerfile.`
2427
+ });
2428
+ continue;
2429
+ }
2430
+ throw new Error(
2431
+ `line ${lineNumber}: Dockerfile instruction '${keyword}' is not supported for sandbox image creation`
2432
+ );
2433
+ }
2434
+ }
2435
+ async function registerImage(context, name, dockerfile, snapshotId, snapshotUri, isPublic) {
2436
+ if (!context.organizationId || !context.projectId) {
2437
+ throw new Error(
2438
+ "Organization ID and Project ID are required. Run 'tl login' and 'tl init'."
2439
+ );
2440
+ }
2441
+ const bearerToken = context.apiKey ?? context.personalAccessToken;
2442
+ if (!bearerToken) {
2443
+ throw new Error("Missing TENSORLAKE_API_KEY or TENSORLAKE_PAT.");
2444
+ }
2445
+ const baseUrl = context.apiUrl.replace(/\/+$/, "");
2446
+ const url = `${baseUrl}/platform/v1/organizations/${encodeURIComponent(context.organizationId)}/projects/${encodeURIComponent(context.projectId)}/sandbox-templates`;
2447
+ const headers = {
2448
+ Authorization: `Bearer ${bearerToken}`,
2449
+ "Content-Type": "application/json"
2450
+ };
2451
+ if (context.personalAccessToken && !context.apiKey) {
2452
+ headers["X-Forwarded-Organization-Id"] = context.organizationId;
2453
+ headers["X-Forwarded-Project-Id"] = context.projectId;
2454
+ }
2455
+ const response = await fetch(url, {
2456
+ method: "POST",
2457
+ headers,
2458
+ body: JSON.stringify({
2459
+ name,
2460
+ dockerfile,
2461
+ snapshotId,
2462
+ snapshotUri,
2463
+ isPublic
2464
+ })
2465
+ });
2466
+ if (!response.ok) {
2467
+ throw new Error(
2468
+ `${response.status} ${response.statusText}: ${await response.text()}`
2469
+ );
2470
+ }
2471
+ const text = await response.text();
2472
+ return text ? JSON.parse(text) : {};
2473
+ }
2474
+ async function createSandboxImage(source, options = {}, deps = {}) {
2475
+ const emit = deps.emit ?? defaultEmit;
2476
+ const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
2477
+ const context = buildContextFromEnv();
2478
+ const clientFactory = deps.createClient ?? createDefaultClient;
2479
+ const register = deps.registerImage ?? ((...args) => registerImage(...args));
2480
+ const sourceLabel = typeof source === "string" ? source : `Image(${source.name})`;
2481
+ emit({ type: "status", message: `Loading ${sourceLabel}...` });
2482
+ const plan = typeof source === "string" ? await loadDockerfilePlan(source, options.registeredName) : loadImagePlan(source, options);
2483
+ emit({
2484
+ type: "status",
2485
+ message: plan.baseImage == null ? "Starting build sandbox with the default server image..." : `Starting build sandbox from ${plan.baseImage}...`
2486
+ });
2487
+ const client = clientFactory(context);
2488
+ let sandbox;
2489
+ try {
2490
+ sandbox = await client.createAndConnect({
2491
+ ...plan.baseImage == null ? {} : { image: plan.baseImage },
2492
+ cpus: options.cpus ?? 2,
2493
+ memoryMb: options.memoryMb ?? 4096
2494
+ });
2495
+ emit({
2496
+ type: "status",
2497
+ message: `Materializing image in sandbox ${sandbox.sandboxId}...`
2498
+ });
2499
+ await executeDockerfilePlan(sandbox, plan, emit, sleep4);
2500
+ emit({ type: "status", message: "Creating snapshot..." });
2501
+ const snapshot = await client.snapshotAndWait(sandbox.sandboxId);
2502
+ emit({
2503
+ type: "snapshot_created",
2504
+ snapshot_id: snapshot.snapshotId,
2505
+ snapshot_uri: snapshot.snapshotUri ?? null
2506
+ });
2507
+ if (!snapshot.snapshotUri) {
2508
+ throw new Error(
2509
+ `Snapshot ${snapshot.snapshotId} is missing snapshotUri and cannot be registered as a sandbox image.`
2510
+ );
2511
+ }
2512
+ emit({
2513
+ type: "status",
2514
+ message: `Registering image '${plan.registeredName}'...`
2515
+ });
2516
+ const result = await register(
2517
+ context,
2518
+ plan.registeredName,
2519
+ plan.dockerfileText,
2520
+ snapshot.snapshotId,
2521
+ snapshot.snapshotUri,
2522
+ options.isPublic ?? false
2523
+ );
2524
+ emit({
2525
+ type: "image_registered",
2526
+ name: plan.registeredName,
2527
+ image_id: typeof result.id === "string" && result.id || typeof result.templateId === "string" && result.templateId || ""
2528
+ });
2529
+ emit({ type: "done" });
2530
+ return result;
2531
+ } finally {
2532
+ if (sandbox) {
2533
+ try {
2534
+ await sandbox.terminate();
2535
+ } catch {
2536
+ }
2537
+ }
2538
+ client.close();
2539
+ }
2540
+ }
1394
2541
  export {
1395
2542
  APIClient,
1396
2543
  CloudClient,
1397
2544
  ContainerState,
2545
+ Image,
2546
+ ImageBuildOperationType,
1398
2547
  OutputMode,
1399
2548
  PoolInUseError,
1400
2549
  PoolNotFoundError,
1401
2550
  ProcessStatus,
2551
+ Pty,
1402
2552
  RemoteAPIError,
1403
2553
  RequestExecutionError,
1404
2554
  RequestFailedError,
@@ -1411,6 +2561,8 @@ export {
1411
2561
  SandboxNotFoundError,
1412
2562
  SandboxStatus,
1413
2563
  SnapshotStatus,
1414
- StdinMode
2564
+ StdinMode,
2565
+ createSandboxImage,
2566
+ dockerfileContent
1415
2567
  };
1416
2568
  //# sourceMappingURL=index.js.map