tensorlake 0.4.40 → 0.4.42

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