@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 +36 -0
- package/dist/index.cjs +174 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -10
- package/dist/index.d.ts +45 -10
- package/dist/index.js +174 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
3326
|
-
*
|
|
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
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
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
|
|
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
|
*/
|