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