@blaxel/core 0.2.67-preview.88 → 0.2.67-preview.90

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.
Files changed (37) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/common/settings.js +2 -2
  3. package/dist/cjs/image/image.browser.js +84 -0
  4. package/dist/cjs/image/image.js +567 -0
  5. package/dist/cjs/image/image.test.js +545 -0
  6. package/dist/cjs/image/index.js +7 -0
  7. package/dist/cjs/index.js +1 -0
  8. package/dist/cjs/types/image/image.browser.d.ts +59 -0
  9. package/dist/cjs/types/image/image.d.ts +202 -0
  10. package/dist/cjs/types/image/image.test.d.ts +4 -0
  11. package/dist/cjs/types/image/index.d.ts +1 -0
  12. package/dist/cjs/types/index.d.ts +1 -0
  13. package/dist/cjs-browser/.tsbuildinfo +1 -1
  14. package/dist/cjs-browser/common/settings.js +2 -2
  15. package/dist/cjs-browser/image/image.js +84 -0
  16. package/dist/cjs-browser/image/image.test.js +545 -0
  17. package/dist/cjs-browser/image/index.js +7 -0
  18. package/dist/cjs-browser/index.js +1 -0
  19. package/dist/cjs-browser/types/image/image.browser.d.ts +59 -0
  20. package/dist/cjs-browser/types/image/image.d.ts +202 -0
  21. package/dist/cjs-browser/types/image/image.test.d.ts +4 -0
  22. package/dist/cjs-browser/types/image/index.d.ts +1 -0
  23. package/dist/cjs-browser/types/index.d.ts +1 -0
  24. package/dist/esm/.tsbuildinfo +1 -1
  25. package/dist/esm/common/settings.js +2 -2
  26. package/dist/esm/image/image.browser.js +80 -0
  27. package/dist/esm/image/image.js +560 -0
  28. package/dist/esm/image/image.test.js +543 -0
  29. package/dist/esm/image/index.js +1 -0
  30. package/dist/esm/index.js +1 -0
  31. package/dist/esm-browser/.tsbuildinfo +1 -1
  32. package/dist/esm-browser/common/settings.js +2 -2
  33. package/dist/esm-browser/image/image.js +80 -0
  34. package/dist/esm-browser/image/image.test.js +543 -0
  35. package/dist/esm-browser/image/index.js +1 -0
  36. package/dist/esm-browser/index.js +1 -0
  37. package/package.json +4 -1
@@ -9,8 +9,8 @@ const index_js_1 = require("../authentication/index.js");
9
9
  const env_js_1 = require("../common/env.js");
10
10
  const node_js_1 = require("../common/node.js");
11
11
  // Build info - these placeholders are replaced at build time by build:replace-imports
12
- const BUILD_VERSION = "0.2.67-preview.88";
13
- const BUILD_COMMIT = "5357b4855456af1c4f172b8489f4f5c520c79a24";
12
+ const BUILD_VERSION = "0.2.67-preview.90";
13
+ const BUILD_COMMIT = "0b5a3b81059ce9031873088b36eed87a0c589f65";
14
14
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
15
15
  // Cache for config.yaml tracking value
16
16
  let configTrackingValue = null;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ImageInstance = exports.SANDBOX_API_PATH = exports.SANDBOX_API_IMAGE = void 0;
4
+ exports.SANDBOX_API_IMAGE = "ghcr.io/blaxel-ai/sandbox";
5
+ exports.SANDBOX_API_PATH = "/usr/local/bin/sandbox-api";
6
+ function throwBrowserError() {
7
+ throw new Error("ImageInstance is only available in Node.js environments. " +
8
+ "Image building requires file system operations that are not supported in browsers.");
9
+ }
10
+ /**
11
+ * A fluent builder for creating sandbox images programmatically.
12
+ *
13
+ * NOTE: This class is only available in Node.js environments.
14
+ * In browser environments, all methods will throw an error.
15
+ */
16
+ class ImageInstance {
17
+ _context;
18
+ constructor(_context) {
19
+ this._context = _context;
20
+ throwBrowserError();
21
+ }
22
+ static fromRegistry(_tag) {
23
+ throwBrowserError();
24
+ }
25
+ workdir(_path) {
26
+ throwBrowserError();
27
+ }
28
+ runCommands(..._commands) {
29
+ throwBrowserError();
30
+ }
31
+ env(_variables) {
32
+ throwBrowserError();
33
+ }
34
+ copy(_source, _destination) {
35
+ throwBrowserError();
36
+ }
37
+ addLocalFile(_sourcePath, _destination, _contextName) {
38
+ throwBrowserError();
39
+ }
40
+ addLocalDir(_sourcePath, _destination, _contextName) {
41
+ throwBrowserError();
42
+ }
43
+ expose(..._ports) {
44
+ throwBrowserError();
45
+ }
46
+ entrypoint(..._args) {
47
+ throwBrowserError();
48
+ }
49
+ user(_user) {
50
+ throwBrowserError();
51
+ }
52
+ label(_labels) {
53
+ throwBrowserError();
54
+ }
55
+ arg(_name, _defaultValue) {
56
+ throwBrowserError();
57
+ }
58
+ get dockerfile() {
59
+ throw new Error("ImageInstance is only available in Node.js environments. " +
60
+ "Image building requires file system operations that are not supported in browsers.");
61
+ }
62
+ get hash() {
63
+ throw new Error("ImageInstance is only available in Node.js environments. " +
64
+ "Image building requires file system operations that are not supported in browsers.");
65
+ }
66
+ get baseImage() {
67
+ throw new Error("ImageInstance is only available in Node.js environments. " +
68
+ "Image building requires file system operations that are not supported in browsers.");
69
+ }
70
+ write(_outputPath, _name) {
71
+ throwBrowserError();
72
+ }
73
+ writeTemp() {
74
+ throwBrowserError();
75
+ }
76
+ // eslint-disable-next-line @typescript-eslint/require-await
77
+ async build(_options) {
78
+ throwBrowserError();
79
+ }
80
+ buildSync(_options) {
81
+ throwBrowserError();
82
+ }
83
+ }
84
+ exports.ImageInstance = ImageInstance;
@@ -0,0 +1,567 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ImageInstance = exports.SANDBOX_API_PATH = exports.SANDBOX_API_IMAGE = void 0;
7
+ const dockerfile_ast_1 = require("dockerfile-ast");
8
+ const node_js_1 = require("../common/node.js");
9
+ const settings_js_1 = require("../common/settings.js");
10
+ const archiver_1 = __importDefault(require("archiver"));
11
+ function ensureNodeEnvironment() {
12
+ if (!node_js_1.fs || !node_js_1.os || !node_js_1.path || !node_js_1.crypto) {
13
+ throw new Error("Image is only available in Node.js environments. File system operations are not supported in browsers.");
14
+ }
15
+ }
16
+ exports.SANDBOX_API_IMAGE = "ghcr.io/blaxel-ai/sandbox";
17
+ exports.SANDBOX_API_PATH = "/usr/local/bin/sandbox-api";
18
+ function generateDockerfile(context) {
19
+ // Build the raw Dockerfile content
20
+ const lines = [`FROM ${context.baseImage}`];
21
+ lines.push(...context.instructions);
22
+ const rawContent = lines.join("\n") + "\n";
23
+ // Parse using dockerfile-ast to validate syntax
24
+ const dockerfile = dockerfile_ast_1.DockerfileParser.parse(rawContent);
25
+ // Check for any parsing errors by validating instruction count matches
26
+ const instructions = dockerfile.getInstructions();
27
+ if (instructions.length !== lines.length) {
28
+ throw new Error("Invalid Dockerfile syntax: instruction count mismatch after parsing");
29
+ }
30
+ // Return raw content to preserve escaping in JSON-format instructions (ENTRYPOINT, CMD)
31
+ // The AST reconstruction via toString() doesn't properly preserve escape sequences
32
+ return rawContent;
33
+ }
34
+ function computeHash(context) {
35
+ ensureNodeEnvironment();
36
+ let content = generateDockerfile(context);
37
+ for (const localFile of context.localFiles) {
38
+ if (!node_js_1.fs.existsSync(localFile.sourcePath)) {
39
+ throw new Error(`Local file not found: ${localFile.sourcePath}. Cannot compute hash for missing files.`);
40
+ }
41
+ const stat = node_js_1.fs.statSync(localFile.sourcePath);
42
+ content += `\n${localFile.contextName}:${stat.mtimeMs}`;
43
+ }
44
+ return node_js_1.crypto.createHash("sha256").update(content).digest("hex").substring(0, 12);
45
+ }
46
+ function cloneContext(context) {
47
+ return {
48
+ baseImage: context.baseImage,
49
+ instructions: [...context.instructions],
50
+ localFiles: [...context.localFiles],
51
+ hasEntrypoint: context.hasEntrypoint,
52
+ };
53
+ }
54
+ function sleep(ms) {
55
+ return new Promise((resolve) => setTimeout(resolve, ms));
56
+ }
57
+ /**
58
+ * A fluent builder for creating sandbox images programmatically.
59
+ *
60
+ * Similar to Modal's Image class, allows chaining operations to build
61
+ * a custom image from a base image.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const image = ImageInstance.fromRegistry("python:3.11-slim")
66
+ * .runCommands("apt-get update && apt-get install -y git curl")
67
+ * .workdir("/app")
68
+ * .runCommands("pip install --upgrade pip")
69
+ * .env({ PYTHONUNBUFFERED: "1" });
70
+ *
71
+ * await image.build({
72
+ * name: "my-sandbox",
73
+ * memory: 4096,
74
+ * timeout: 900000,
75
+ * onStatusChange: console.log,
76
+ * sandboxVersion: "latest",
77
+ * });
78
+ * ```
79
+ */
80
+ class ImageInstance {
81
+ _context;
82
+ constructor(context) {
83
+ this._context = context;
84
+ }
85
+ /**
86
+ * Create an image from a Docker registry image.
87
+ *
88
+ * @param tag - The image tag (e.g., "python:3.11-slim", "ubuntu:22.04")
89
+ * @returns A new Image instance
90
+ */
91
+ static fromRegistry(tag) {
92
+ const context = {
93
+ baseImage: tag,
94
+ instructions: [],
95
+ localFiles: [],
96
+ hasEntrypoint: false,
97
+ };
98
+ return new ImageInstance(context);
99
+ }
100
+ /**
101
+ * Set the working directory for subsequent instructions.
102
+ *
103
+ * @param path - The working directory path inside the container
104
+ * @returns A new Image instance with the working directory set
105
+ */
106
+ workdir(path) {
107
+ const newContext = cloneContext(this._context);
108
+ newContext.instructions.push(`WORKDIR ${path}`);
109
+ return new ImageInstance(newContext);
110
+ }
111
+ /**
112
+ * Run shell commands in the image.
113
+ *
114
+ * @param commands - One or more shell commands to run
115
+ * @returns A new Image instance with the commands added
116
+ */
117
+ runCommands(...commands) {
118
+ const newContext = cloneContext(this._context);
119
+ for (const cmd of commands) {
120
+ newContext.instructions.push(`RUN ${cmd}`);
121
+ }
122
+ return new ImageInstance(newContext);
123
+ }
124
+ /**
125
+ * Set environment variables.
126
+ *
127
+ * @param variables - Environment variables as an object
128
+ * @returns A new Image instance with the environment variables set
129
+ */
130
+ env(variables) {
131
+ const keys = Object.keys(variables);
132
+ if (keys.length === 0) {
133
+ return this;
134
+ }
135
+ const newContext = cloneContext(this._context);
136
+ for (const [key, value] of Object.entries(variables)) {
137
+ newContext.instructions.push(`ENV ${key}="${value}"`);
138
+ }
139
+ return new ImageInstance(newContext);
140
+ }
141
+ /**
142
+ * Copy files or directories from the build context to the image.
143
+ *
144
+ * @param source - Source path (relative to build context)
145
+ * @param destination - Destination path in the image
146
+ * @returns A new Image instance with the copy instruction
147
+ */
148
+ copy(source, destination) {
149
+ const newContext = cloneContext(this._context);
150
+ newContext.instructions.push(`COPY ${source} ${destination}`);
151
+ return new ImageInstance(newContext);
152
+ }
153
+ /**
154
+ * Add a local file to the build context and copy it to the image.
155
+ *
156
+ * @param sourcePath - Path to the local file
157
+ * @param destination - Destination path in the image
158
+ * @param contextName - Optional name for the file in the build context
159
+ * @returns A new Image instance with the file added
160
+ */
161
+ addLocalFile(sourcePath, destination, contextName) {
162
+ ensureNodeEnvironment();
163
+ const source = node_js_1.path.resolve(sourcePath);
164
+ const name = contextName ?? node_js_1.path.basename(source);
165
+ const newContext = cloneContext(this._context);
166
+ newContext.localFiles.push({
167
+ sourcePath: source,
168
+ destinationPath: destination,
169
+ contextName: name,
170
+ });
171
+ newContext.instructions.push(`COPY ${name} ${destination}`);
172
+ return new ImageInstance(newContext);
173
+ }
174
+ /**
175
+ * Add a local directory to the build context and copy it to the image.
176
+ *
177
+ * @param sourcePath - Path to the local directory
178
+ * @param destination - Destination path in the image
179
+ * @param contextName - Optional name for the directory in the build context
180
+ * @returns A new Image instance with the directory added
181
+ */
182
+ addLocalDir(sourcePath, destination, contextName) {
183
+ ensureNodeEnvironment();
184
+ const source = node_js_1.path.resolve(sourcePath);
185
+ const name = contextName ?? node_js_1.path.basename(source);
186
+ const newContext = cloneContext(this._context);
187
+ newContext.localFiles.push({
188
+ sourcePath: source,
189
+ destinationPath: destination,
190
+ contextName: name,
191
+ });
192
+ newContext.instructions.push(`COPY ${name} ${destination}`);
193
+ return new ImageInstance(newContext);
194
+ }
195
+ /**
196
+ * Expose ports.
197
+ *
198
+ * @param ports - Port numbers to expose
199
+ * @returns A new Image instance with the ports exposed
200
+ */
201
+ expose(...ports) {
202
+ if (ports.length === 0) {
203
+ return this;
204
+ }
205
+ const newContext = cloneContext(this._context);
206
+ for (const port of ports) {
207
+ newContext.instructions.push(`EXPOSE ${port}`);
208
+ }
209
+ return new ImageInstance(newContext);
210
+ }
211
+ /**
212
+ * Set the entrypoint for the image.
213
+ *
214
+ * @param args - Entrypoint command and arguments
215
+ * @returns A new Image instance with the entrypoint set
216
+ */
217
+ entrypoint(...args) {
218
+ if (args.length === 0) {
219
+ return this;
220
+ }
221
+ // Format as JSON array for exec form, using JSON.stringify to properly escape quotes and special characters
222
+ const argsJson = args.map((arg) => JSON.stringify(arg)).join(", ");
223
+ const newContext = cloneContext(this._context);
224
+ newContext.instructions.push(`ENTRYPOINT [${argsJson}]`);
225
+ newContext.hasEntrypoint = true;
226
+ return new ImageInstance(newContext);
227
+ }
228
+ /**
229
+ * Set the user for subsequent instructions.
230
+ *
231
+ * @param user - Username or UID
232
+ * @returns A new Image instance with the user set
233
+ */
234
+ user(user) {
235
+ const newContext = cloneContext(this._context);
236
+ newContext.instructions.push(`USER ${user}`);
237
+ return new ImageInstance(newContext);
238
+ }
239
+ /**
240
+ * Add labels to the image.
241
+ *
242
+ * @param labels - Labels as an object
243
+ * @returns A new Image instance with the labels added
244
+ */
245
+ label(labels) {
246
+ const keys = Object.keys(labels);
247
+ if (keys.length === 0) {
248
+ return this;
249
+ }
250
+ const newContext = cloneContext(this._context);
251
+ for (const [key, value] of Object.entries(labels)) {
252
+ newContext.instructions.push(`LABEL ${key}="${value}"`);
253
+ }
254
+ return new ImageInstance(newContext);
255
+ }
256
+ /**
257
+ * Define a build argument.
258
+ *
259
+ * @param name - Argument name
260
+ * @param defaultValue - Optional default value
261
+ * @returns A new Image instance with the argument defined
262
+ */
263
+ arg(name, defaultValue) {
264
+ const newContext = cloneContext(this._context);
265
+ if (defaultValue !== undefined) {
266
+ newContext.instructions.push(`ARG ${name}=${defaultValue}`);
267
+ }
268
+ else {
269
+ newContext.instructions.push(`ARG ${name}`);
270
+ }
271
+ return new ImageInstance(newContext);
272
+ }
273
+ /**
274
+ * Get the generated Dockerfile content.
275
+ */
276
+ get dockerfile() {
277
+ return generateDockerfile(this._context);
278
+ }
279
+ /**
280
+ * Get a hash of the image configuration.
281
+ */
282
+ get hash() {
283
+ return computeHash(this._context);
284
+ }
285
+ /**
286
+ * Get the base image tag.
287
+ */
288
+ get baseImage() {
289
+ return this._context.baseImage;
290
+ }
291
+ _hasSandboxApi() {
292
+ const dockerfile = generateDockerfile(this._context);
293
+ return dockerfile.includes("sandbox-api") || dockerfile.includes("blaxel-ai/sandbox");
294
+ }
295
+ _prepareForSandbox(sandboxVersion = "latest") {
296
+ const newContext = cloneContext(this._context);
297
+ // Add sandbox-api if not already present
298
+ if (!this._hasSandboxApi()) {
299
+ const sandboxImage = `${exports.SANDBOX_API_IMAGE}:${sandboxVersion}`;
300
+ const copyInstruction = `COPY --from=${sandboxImage} /sandbox-api ${exports.SANDBOX_API_PATH}`;
301
+ newContext.instructions.push(copyInstruction);
302
+ }
303
+ // Add default entrypoint if not set by user
304
+ if (!newContext.hasEntrypoint) {
305
+ newContext.instructions.push(`ENTRYPOINT ["${exports.SANDBOX_API_PATH}"]`);
306
+ newContext.hasEntrypoint = true;
307
+ }
308
+ return new ImageInstance(newContext);
309
+ }
310
+ /**
311
+ * Write the image to a deployable folder structure.
312
+ *
313
+ * @param outputPath - Path to the output directory
314
+ * @param name - Optional name for the generated folder (defaults to hash-based name)
315
+ * @returns Path to the generated folder
316
+ */
317
+ write(outputPath, name) {
318
+ ensureNodeEnvironment();
319
+ const outputDir = node_js_1.path.resolve(outputPath);
320
+ // Create folder name based on hash if not provided
321
+ if (!name) {
322
+ name = `image-${this.hash}`;
323
+ }
324
+ const buildDir = node_js_1.path.join(outputDir, name);
325
+ node_js_1.fs.mkdirSync(buildDir, { recursive: true });
326
+ // Generate Dockerfile
327
+ const dockerfilePath = node_js_1.path.join(buildDir, "Dockerfile");
328
+ node_js_1.fs.writeFileSync(dockerfilePath, generateDockerfile(this._context));
329
+ // Copy local files to build context
330
+ for (const localFile of this._context.localFiles) {
331
+ if (!node_js_1.fs.existsSync(localFile.sourcePath)) {
332
+ throw new Error(`Local file not found: ${localFile.sourcePath}`);
333
+ }
334
+ const dest = node_js_1.path.join(buildDir, localFile.contextName);
335
+ const stat = node_js_1.fs.statSync(localFile.sourcePath);
336
+ if (stat.isDirectory()) {
337
+ if (node_js_1.fs.existsSync(dest)) {
338
+ node_js_1.fs.rmSync(dest, { recursive: true });
339
+ }
340
+ node_js_1.fs.cpSync(localFile.sourcePath, dest, { recursive: true });
341
+ }
342
+ else {
343
+ node_js_1.fs.cpSync(localFile.sourcePath, dest);
344
+ }
345
+ }
346
+ // Generate a manifest file with metadata
347
+ const manifest = {
348
+ base_image: this._context.baseImage,
349
+ hash: this.hash,
350
+ instructions_count: this._context.instructions.length,
351
+ local_files_count: this._context.localFiles.length,
352
+ };
353
+ const manifestPath = node_js_1.path.join(buildDir, "manifest.json");
354
+ node_js_1.fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
355
+ return buildDir;
356
+ }
357
+ /**
358
+ * Write the image to a deployable folder in a temporary directory.
359
+ *
360
+ * @returns Path to the generated folder
361
+ */
362
+ writeTemp() {
363
+ ensureNodeEnvironment();
364
+ const tempDir = node_js_1.path.join(node_js_1.os.tmpdir(), `blaxel-image-${Date.now()}`);
365
+ node_js_1.fs.mkdirSync(tempDir, { recursive: true });
366
+ return this.write(tempDir);
367
+ }
368
+ async _createZip(buildDir) {
369
+ return new Promise((resolve, reject) => {
370
+ const chunks = [];
371
+ const archive = (0, archiver_1.default)("zip", {
372
+ zlib: { level: 9 },
373
+ });
374
+ archive.on("data", (chunk) => {
375
+ chunks.push(chunk);
376
+ });
377
+ archive.on("end", () => {
378
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
379
+ const result = new Uint8Array(totalLength);
380
+ let offset = 0;
381
+ for (const chunk of chunks) {
382
+ result.set(chunk, offset);
383
+ offset += chunk.length;
384
+ }
385
+ resolve(result);
386
+ });
387
+ archive.on("error", reject);
388
+ // Add all files from the build directory
389
+ archive.directory(buildDir, false);
390
+ void archive.finalize();
391
+ });
392
+ }
393
+ _createSandboxPayload(name, memory = 4096) {
394
+ const labels = {
395
+ "x-blaxel-auto-generated": "true",
396
+ };
397
+ const metadata = {
398
+ name,
399
+ labels,
400
+ };
401
+ const runtime = {
402
+ memory,
403
+ };
404
+ const spec = {
405
+ runtime,
406
+ };
407
+ return {
408
+ metadata,
409
+ spec,
410
+ };
411
+ }
412
+ async _createSandboxWithUpload(sandbox) {
413
+ const name = sandbox.metadata?.name || "";
414
+ const body = sandbox;
415
+ await settings_js_1.settings.authenticate();
416
+ const headers = {
417
+ "Content-Type": "application/json",
418
+ ...settings_js_1.settings.headers,
419
+ };
420
+ // Try PUT first (update), fall back to POST (create)
421
+ let response = await fetch(`${settings_js_1.settings.baseUrl}/sandboxes/${name}?upload=true`, {
422
+ method: "PUT",
423
+ headers,
424
+ body: JSON.stringify(body),
425
+ });
426
+ // If 404, try create
427
+ if (response.status === 404) {
428
+ response = await fetch(`${settings_js_1.settings.baseUrl}/sandboxes?upload=true`, {
429
+ method: "POST",
430
+ headers,
431
+ body: JSON.stringify(body),
432
+ });
433
+ }
434
+ const uploadUrl = response.headers.get("x-blaxel-upload-url");
435
+ return { response, uploadUrl };
436
+ }
437
+ async _uploadZip(uploadUrl, zipContent) {
438
+ const response = await fetch(uploadUrl, {
439
+ method: "PUT",
440
+ headers: {
441
+ "Content-Type": "application/zip",
442
+ },
443
+ body: zipContent,
444
+ });
445
+ if (response.status >= 400) {
446
+ const text = await response.text();
447
+ throw new Error(`Upload failed with status ${response.status}: ${text}`);
448
+ }
449
+ }
450
+ async _getSandboxStatus(name) {
451
+ await settings_js_1.settings.authenticate();
452
+ const response = await fetch(`${settings_js_1.settings.baseUrl}/sandboxes/${name}`, {
453
+ method: "GET",
454
+ headers: settings_js_1.settings.headers,
455
+ });
456
+ if (response.status === 200) {
457
+ const data = (await response.json());
458
+ return data.status || null;
459
+ }
460
+ return null;
461
+ }
462
+ async _waitForDeployment(name, timeout = 900000, // 15 minutes in ms
463
+ pollInterval = 3000, onStatusChange) {
464
+ const startTime = Date.now();
465
+ let lastStatus = null;
466
+ const terminalStates = new Set(["DEPLOYED", "FAILED", "TERMINATED"]);
467
+ let buildStarted = false;
468
+ while (Date.now() - startTime < timeout) {
469
+ const status = await this._getSandboxStatus(name);
470
+ if (status && status !== lastStatus) {
471
+ lastStatus = status;
472
+ if (onStatusChange) {
473
+ onStatusChange(status);
474
+ }
475
+ }
476
+ // Track if the build has started (status changed from DEPLOYED)
477
+ if (status && status !== "DEPLOYED") {
478
+ buildStarted = true;
479
+ }
480
+ // Only consider DEPLOYED as terminal if the build has started
481
+ // This handles re-builds where status starts as DEPLOYED
482
+ if (status && terminalStates.has(status)) {
483
+ if (status === "FAILED") {
484
+ throw new Error(`Deployment failed for sandbox '${name}'`);
485
+ }
486
+ if (status === "TERMINATED") {
487
+ throw new Error(`Sandbox '${name}' was terminated`);
488
+ }
489
+ if (status === "DEPLOYED" && buildStarted) {
490
+ return status;
491
+ }
492
+ }
493
+ await sleep(pollInterval);
494
+ }
495
+ throw new Error(`Deployment timed out after ${timeout / 1000} seconds`);
496
+ }
497
+ /**
498
+ * Build and deploy the image as a sandbox.
499
+ *
500
+ * This method:
501
+ * 1. Prepares the image for sandbox deployment (adds sandbox-api)
502
+ * 2. Builds the image folder
503
+ * 3. Creates a zip of the folder
504
+ * 4. Creates/updates the sandbox resource
505
+ * 5. Uploads the zip to Blaxel
506
+ * 6. Waits for deployment to complete
507
+ *
508
+ * @param options - Build options
509
+ * @returns The deployed Sandbox object
510
+ */
511
+ async build(options) {
512
+ const { name, memory = 4096, timeout = 900000, onStatusChange, sandboxVersion = "latest" } = options;
513
+ // Prepare image for sandbox deployment (add sandbox-api and entrypoint)
514
+ const preparedImage = this._prepareForSandbox(sandboxVersion);
515
+ // Write the image folder
516
+ const buildDir = preparedImage.writeTemp();
517
+ try {
518
+ // Create zip
519
+ const zipContent = await this._createZip(buildDir);
520
+ // Create sandbox payload
521
+ const sandboxPayload = this._createSandboxPayload(name, memory);
522
+ // Create/update sandbox and get upload URL
523
+ const { response, uploadUrl } = await this._createSandboxWithUpload(sandboxPayload);
524
+ if (response.status >= 400) {
525
+ const text = await response.text();
526
+ throw new Error(`Failed to create sandbox: ${response.status} - ${text}`);
527
+ }
528
+ if (!uploadUrl) {
529
+ throw new Error("No upload URL returned from API");
530
+ }
531
+ // Upload the zip
532
+ await this._uploadZip(uploadUrl, zipContent);
533
+ // Wait for deployment to complete
534
+ await this._waitForDeployment(name, timeout, 3000, onStatusChange);
535
+ // Get the final sandbox state
536
+ await settings_js_1.settings.authenticate();
537
+ const finalResponse = await fetch(`${settings_js_1.settings.baseUrl}/sandboxes/${name}`, {
538
+ method: "GET",
539
+ headers: settings_js_1.settings.headers,
540
+ });
541
+ if (finalResponse.status === 200) {
542
+ const sandbox = (await finalResponse.json());
543
+ return sandbox;
544
+ }
545
+ throw new Error(`Failed to get sandbox '${name}' after deployment`);
546
+ }
547
+ finally {
548
+ // Cleanup temp directory
549
+ const parentDir = node_js_1.path.resolve(buildDir, "..");
550
+ try {
551
+ node_js_1.fs.rmSync(parentDir, { recursive: true, force: true });
552
+ }
553
+ catch {
554
+ // Ignore cleanup errors
555
+ }
556
+ }
557
+ }
558
+ /**
559
+ * Build and deploy the image as a sandbox (sync version).
560
+ *
561
+ * @deprecated Use build() instead - this is provided for API parity with Python SDK
562
+ */
563
+ buildSync(options) {
564
+ return this.build(options);
565
+ }
566
+ }
567
+ exports.ImageInstance = ImageInstance;