@declaw/sdk 1.4.0 → 1.5.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,42 @@ All notable changes to the Declaw TypeScript / JavaScript SDK are documented in
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.0]
9
+
10
+ _2026-09 train: working template builds._
11
+
12
+ ### Added
13
+
14
+ - The `buildTimeout` option on `Template.build()`, `BuildInfo.logs`,
15
+ `TemplateBuildStatus.templateId`, `buildId` / `logs` on `BuildError`, and
16
+ `TemplateBase.hasCopies()`.
17
+ - Status checks that fail temporarily while waiting (5xx, 408, 429, network
18
+ errors) are retried for up to two minutes instead of ending the wait. The
19
+ server keeps the newest 2,000 lines of a build's output; if more arrive
20
+ between two status checks, a `... [earlier build output truncated]` line
21
+ marks the gap.
22
+
23
+ ### Changed
24
+
25
+ - `Template.build()` now waits for the build to finish, as documented, and
26
+ resolves to a `BuildInfo` whose `logs` hold the output. A failed build
27
+ rejects with `BuildError` carrying its `buildId` and `logs`. A build still
28
+ running after `buildTimeout` milliseconds (default one hour) rejects with
29
+ `TimeoutError` naming it; the build keeps running, and
30
+ `Template.getBuildStatus()` follows it. To return immediately, use
31
+ `Template.buildInBackground()`.
32
+ - A template that uses `copy()` now rejects with `InvalidArgumentError` when
33
+ built, before anything is sent. `copy()` never copied anything: a build
34
+ cannot upload local files, and the copies were silently dropped. Fetch files
35
+ in a `runCmd` step, or use `fromDockerfile()`.
36
+
37
+ ### Fixed
38
+
39
+ - Packages added with `aptInstall()` were sent under a name the API ignores,
40
+ so builds succeeded without installing them. They are now installed.
41
+ - `onBuildLogs` never fired. It now receives each new line of build output
42
+ while `Template.build()` waits.
43
+
8
44
  ## [1.4.0]
9
45
 
10
46
  _2026-08 train: idempotent sandbox creation._
package/dist/index.cjs CHANGED
@@ -263,9 +263,15 @@ var TemplateError = class extends SandboxError {
263
263
  }
264
264
  };
265
265
  var BuildError = class extends TemplateError {
266
+ /** The failed build's ID, when the error comes from a build that ran (`Template.build`). */
267
+ buildId;
268
+ /** The failed build's full output, when the error comes from a build that ran. */
269
+ logs;
266
270
  constructor(message, opts) {
267
271
  super(message, opts);
268
272
  this.name = "BuildError";
273
+ this.buildId = opts?.buildId;
274
+ this.logs = opts?.logs ?? [];
269
275
  }
270
276
  };
271
277
  var FileUploadError = class extends SandboxError {
@@ -3248,7 +3254,13 @@ var TemplateBase = class {
3248
3254
  this._runCmds.push(cmds);
3249
3255
  return this;
3250
3256
  }
3251
- /** Add a file copy operation. */
3257
+ /**
3258
+ * Copy a local file into the template.
3259
+ *
3260
+ * Not supported yet: a template build cannot upload local files, so
3261
+ * building a template that uses `copy()` throws `InvalidArgumentError`.
3262
+ * Fetch the file in a `runCmd` step, or use `fromDockerfile`.
3263
+ */
3252
3264
  copy(src, dst, mode) {
3253
3265
  this._copies.push({ src, dst, mode });
3254
3266
  return this;
@@ -3268,6 +3280,14 @@ var TemplateBase = class {
3268
3280
  this._startCmd = cmd;
3269
3281
  return this;
3270
3282
  }
3283
+ /**
3284
+ * Whether `copy()` was used. Builds reject such a template until builds can
3285
+ * upload local files.
3286
+ * @internal
3287
+ */
3288
+ hasCopies() {
3289
+ return this._copies.length > 0;
3290
+ }
3271
3291
  /** Serialize the template to a JSON-friendly object. */
3272
3292
  toJSON() {
3273
3293
  if (this._dockerfile !== void 0) {
@@ -3279,14 +3299,11 @@ var TemplateBase = class {
3279
3299
  if (this._runCmds.length > 0) {
3280
3300
  result.run_cmds = this._runCmds.map((c) => c.join(" "));
3281
3301
  }
3282
- if (this._copies.length > 0) {
3283
- result.copies = this._copies;
3284
- }
3285
3302
  if (Object.keys(this._envs).length > 0) {
3286
3303
  result.envs = this._envs;
3287
3304
  }
3288
3305
  if (this._aptPackages.length > 0) {
3289
- result.apt_packages = this._aptPackages;
3306
+ result.packages = this._aptPackages;
3290
3307
  }
3291
3308
  if (this._startCmd !== void 0) {
3292
3309
  result.start_cmd = this._startCmd;
@@ -3298,19 +3315,70 @@ function parseBuildInfo(data) {
3298
3315
  return {
3299
3316
  buildId: data.build_id ?? data.buildId ?? "",
3300
3317
  status: data.status ?? "",
3301
- templateId: data.template_id ?? data.templateId ?? void 0
3318
+ templateId: data.template_id ?? data.templateId ?? void 0,
3319
+ logs: data.logs ?? []
3302
3320
  };
3303
3321
  }
3304
3322
  function parseTemplateBuildStatus(data) {
3305
3323
  return {
3306
3324
  buildId: data.build_id ?? data.buildId ?? "",
3307
3325
  status: data.status ?? "",
3308
- logs: data.logs ?? []
3326
+ // A build with no output yet serializes its logs as null.
3327
+ logs: data.logs ?? [],
3328
+ templateId: data.template_id ?? data.templateId ?? void 0
3309
3329
  };
3310
3330
  }
3311
3331
 
3312
3332
  // src/template/template.ts
3313
3333
  var VALID_BUILD_ID_RE = /^[a-zA-Z0-9_-]+$/;
3334
+ var BUILD_STATUS_COMPLETED = "completed";
3335
+ var BUILD_STATUS_FAILED = "failed";
3336
+ var DEFAULT_BUILD_TIMEOUT_MS = 60 * 60 * 1e3;
3337
+ var FAILURE_LOG_LINES = 20;
3338
+ var buildPolling = {
3339
+ /** How often a waiting build is re-read. */
3340
+ intervalMs: 3e3,
3341
+ /**
3342
+ * How long status checks may keep failing temporarily (5xx, 408, 429, or no
3343
+ * response) before the wait gives up. It rides out a sandbox-manager
3344
+ * restart; the build itself keeps running.
3345
+ */
3346
+ errorWindowMs: 2 * 60 * 1e3
3347
+ };
3348
+ var BUILD_LOG_TRUNCATION_NOTICE = "... [earlier build output truncated]";
3349
+ var LOG_ANCHOR_LINES = 64;
3350
+ var REJECTIONS = [
3351
+ AuthenticationError,
3352
+ ConflictError,
3353
+ InvalidArgumentError,
3354
+ NotEnoughSpaceError,
3355
+ NotFoundError
3356
+ ];
3357
+ var LogCursor = class {
3358
+ seen = 0;
3359
+ tail = [];
3360
+ newLines(logs) {
3361
+ const fresh = logs.length === 0 || logs[0] !== BUILD_LOG_TRUNCATION_NOTICE ? logs.slice(this.seen) : this.afterTail(logs);
3362
+ this.seen = logs.length;
3363
+ this.tail = [...this.tail, ...fresh].slice(-LOG_ANCHOR_LINES);
3364
+ return fresh;
3365
+ }
3366
+ afterTail(logs) {
3367
+ const window = logs.slice(1);
3368
+ const n = this.tail.length;
3369
+ if (n > 0) {
3370
+ for (let end = window.length; end >= n; end--) {
3371
+ if (window[end - 1] === this.tail[n - 1] && this.tail.every((line, i) => window[end - n + i] === line)) {
3372
+ return window.slice(end);
3373
+ }
3374
+ }
3375
+ }
3376
+ return logs;
3377
+ }
3378
+ };
3379
+ function isTemporaryPollError(err) {
3380
+ return err instanceof SandboxError && !REJECTIONS.some((Rejection) => err instanceof Rejection);
3381
+ }
3314
3382
  function assertValidBuildId(buildId) {
3315
3383
  if (!buildId || !VALID_BUILD_ID_RE.test(buildId)) {
3316
3384
  throw new InvalidArgumentError(
@@ -3318,72 +3386,131 @@ function assertValidBuildId(buildId) {
3318
3386
  );
3319
3387
  }
3320
3388
  }
3389
+ function buildRequestBody(template, alias, opts) {
3390
+ if (template.hasCopies()) {
3391
+ throw new InvalidArgumentError(
3392
+ "TemplateBase.copy() is not supported yet: a template build cannot upload local files. Fetch them in a runCmd step, or use fromDockerfile()."
3393
+ );
3394
+ }
3395
+ const body = {
3396
+ template: template.toJSON(),
3397
+ alias
3398
+ };
3399
+ if (opts?.cpuCount !== void 0) {
3400
+ body.cpu_count = opts.cpuCount;
3401
+ }
3402
+ if (opts?.memoryMb !== void 0) {
3403
+ body.memory_mb = opts.memoryMb;
3404
+ }
3405
+ if (opts?.diskMb !== void 0) {
3406
+ body.disk_mb = opts.diskMb;
3407
+ }
3408
+ return body;
3409
+ }
3410
+ function buildFailedError(status) {
3411
+ let message = `template build ${status.buildId} failed`;
3412
+ const tail = status.logs.slice(-FAILURE_LOG_LINES);
3413
+ if (tail.length > 0) {
3414
+ message += ":\n" + tail.join("\n");
3415
+ }
3416
+ return new BuildError(message, { buildId: status.buildId, logs: status.logs });
3417
+ }
3321
3418
  var Template = class {
3322
3419
  /**
3323
- * Build a template and wait for completion.
3420
+ * Build a template and wait for the build to finish.
3421
+ *
3422
+ * Builds usually take several minutes. While waiting, each new line of build
3423
+ * output is passed to `onBuildLogs`. Sandboxes are created from the finished
3424
+ * template by its alias: `Sandbox.create({ template: alias })`.
3324
3425
  *
3325
- * Sends POST /templates/build with the template definition.
3326
- * Invokes onBuildLogs for each log entry in the response.
3426
+ * @throws {BuildError} The build failed; the error carries its logs.
3427
+ * @throws {TimeoutError} The build was still running after `buildTimeout`.
3428
+ * It keeps running; follow it with `getBuildStatus()`.
3429
+ * @throws {InvalidArgumentError} The template uses `copy()`, which is not
3430
+ * supported yet.
3327
3431
  */
3328
3432
  static async build(template, alias, opts) {
3433
+ const body = buildRequestBody(template, alias, opts);
3329
3434
  const config = new ConnectionConfig({
3330
3435
  apiKey: opts?.apiKey,
3331
3436
  domain: opts?.domain,
3332
3437
  requestTimeout: opts?.requestTimeout
3333
3438
  });
3334
3439
  const client = getSharedClient(config);
3335
- const body = {
3336
- template: template.toJSON(),
3337
- alias
3338
- };
3339
- if (opts?.cpuCount !== void 0) {
3340
- body.cpu_count = opts.cpuCount;
3341
- }
3342
- if (opts?.memoryMb !== void 0) {
3343
- body.memory_mb = opts.memoryMb;
3344
- }
3345
- if (opts?.diskMb !== void 0) {
3346
- body.disk_mb = opts.diskMb;
3347
- }
3348
3440
  const data = await client.post("/templates/build", {
3349
3441
  json: body,
3350
3442
  timeout: opts?.requestTimeout
3351
3443
  });
3352
- const response = data;
3353
- const result = parseBuildInfo(response);
3354
- if (opts?.onBuildLogs && Array.isArray(response.logs)) {
3355
- for (const log of response.logs) {
3356
- opts.onBuildLogs(log);
3444
+ const info = parseBuildInfo(data);
3445
+ assertValidBuildId(info.buildId);
3446
+ let status = {
3447
+ buildId: info.buildId,
3448
+ status: info.status,
3449
+ logs: info.logs,
3450
+ templateId: info.templateId
3451
+ };
3452
+ const buildTimeout = opts?.buildTimeout ?? DEFAULT_BUILD_TIMEOUT_MS;
3453
+ const deadline = Date.now() + buildTimeout;
3454
+ const timedOut = (cause) => new TimeoutError(
3455
+ `template build ${info.buildId} was still running after ${buildTimeout}ms. It keeps running; follow it with Template.getBuildStatus('${info.buildId}').` + (cause instanceof Error ? ` Last status check: ${cause.message}` : "")
3456
+ );
3457
+ const cursor = new LogCursor();
3458
+ for (; ; ) {
3459
+ if (opts?.onBuildLogs) {
3460
+ for (const line of cursor.newLines(status.logs)) {
3461
+ opts.onBuildLogs(line);
3462
+ }
3463
+ }
3464
+ if (status.status === BUILD_STATUS_COMPLETED) {
3465
+ return {
3466
+ buildId: status.buildId,
3467
+ status: status.status,
3468
+ templateId: status.templateId ?? info.templateId,
3469
+ logs: status.logs
3470
+ };
3471
+ }
3472
+ if (status.status === BUILD_STATUS_FAILED) {
3473
+ throw buildFailedError(status);
3474
+ }
3475
+ if (Date.now() >= deadline) {
3476
+ throw timedOut();
3477
+ }
3478
+ let failingSince;
3479
+ for (; ; ) {
3480
+ await new Promise((resolve) => setTimeout(resolve, buildPolling.intervalMs));
3481
+ try {
3482
+ const next = await client.get(`/templates/builds/${info.buildId}`, {
3483
+ timeout: opts?.requestTimeout
3484
+ });
3485
+ status = parseTemplateBuildStatus(next);
3486
+ break;
3487
+ } catch (err) {
3488
+ if (!isTemporaryPollError(err)) {
3489
+ throw err;
3490
+ }
3491
+ failingSince ??= Date.now();
3492
+ if (Date.now() - failingSince >= buildPolling.errorWindowMs) {
3493
+ throw err;
3494
+ }
3495
+ if (Date.now() >= deadline) {
3496
+ throw timedOut(err);
3497
+ }
3498
+ }
3357
3499
  }
3358
3500
  }
3359
- return result;
3360
3501
  }
3361
3502
  /**
3362
- * Start a template build in the background.
3363
- *
3364
- * Sends POST /templates/build with `background: true`.
3503
+ * Start a template build and return as soon as the server has accepted it,
3504
+ * with status `building`. Follow the build with `getBuildStatus()`.
3365
3505
  */
3366
3506
  static async buildInBackground(template, alias, opts) {
3507
+ const body = buildRequestBody(template, alias, opts);
3367
3508
  const config = new ConnectionConfig({
3368
3509
  apiKey: opts?.apiKey,
3369
3510
  domain: opts?.domain,
3370
3511
  requestTimeout: opts?.requestTimeout
3371
3512
  });
3372
3513
  const client = getSharedClient(config);
3373
- const body = {
3374
- template: template.toJSON(),
3375
- alias,
3376
- background: true
3377
- };
3378
- if (opts?.cpuCount !== void 0) {
3379
- body.cpu_count = opts.cpuCount;
3380
- }
3381
- if (opts?.memoryMb !== void 0) {
3382
- body.memory_mb = opts.memoryMb;
3383
- }
3384
- if (opts?.diskMb !== void 0) {
3385
- body.disk_mb = opts.diskMb;
3386
- }
3387
3514
  const data = await client.post("/templates/build", {
3388
3515
  json: body,
3389
3516
  timeout: opts?.requestTimeout
@@ -3391,7 +3518,7 @@ var Template = class {
3391
3518
  return parseBuildInfo(data);
3392
3519
  }
3393
3520
  /**
3394
- * Get the status of a template build.
3521
+ * Get the status of a template build, including its logs so far.
3395
3522
  *
3396
3523
  * Sends GET /templates/builds/:buildId.
3397
3524
  */