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