@mastra/docker 0.11.1-alpha.1 → 0.12.0-alpha.2

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
@@ -582,6 +582,7 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
582
582
  /** Image the container boots from; rewritten by a template resolution on `start()`. */
583
583
  _image;
584
584
  _templateSpec;
585
+ _onTemplateBuildProgress;
585
586
  _workingDirectoryWasSet;
586
587
  _command;
587
588
  _env;
@@ -625,6 +626,7 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
625
626
  this._containerName = sanitizeContainerName(options.name ?? this.id);
626
627
  if (options.image !== void 0 && options.template !== void 0) throw new TypeError("DockerSandbox: `image` and `template` are mutually exclusive");
627
628
  this._templateSpec = options.template;
629
+ this._onTemplateBuildProgress = options.onTemplateBuildProgress;
628
630
  this._image = options.image ?? "node:22-slim";
629
631
  this._workingDirectoryWasSet = options.workingDirectory !== void 0 || options.workingDir !== void 0;
630
632
  this._command = options.command ?? ["sleep", "infinity"];
@@ -984,7 +986,8 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
984
986
  throwIfAborted(abortSignal, "start Docker sandbox");
985
987
  const result = await template.build({
986
988
  docker: this._docker,
987
- abortSignal
989
+ abortSignal,
990
+ ...this._onTemplateBuildProgress && { onProgress: this._onTemplateBuildProgress }
988
991
  });
989
992
  if (result.status !== "ready") throw new SandboxError(`Docker template build failed: ${result.error ?? "unknown error"}`, "START_FAILED", {
990
993
  templateId: result.templateId,
@@ -1337,6 +1340,287 @@ const dockerSandboxProvider = {
1337
1340
  createSandbox: (config) => new DockerSandbox(config)
1338
1341
  };
1339
1342
  //#endregion
1343
+ //#region src/template/build-progress.ts
1344
+ /**
1345
+ * Build progress for `DockerTemplate.build()`.
1346
+ *
1347
+ * The daemon reports build progress as newline-delimited JSON. The legacy
1348
+ * builder sends plain text (`{stream: "Step 2/5 : RUN ..."}`); BuildKit sends
1349
+ * `{id: 'moby.buildkit.trace', aux: <base64 StatusResponse protobuf>}`. This
1350
+ * module normalizes both into {@link DockerTemplateBuildEvent}s and keeps a
1351
+ * bounded tail of raw output for failure diagnostics.
1352
+ */
1353
+ const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024;
1354
+ /** Keeps at most `maxBytes` (UTF-8) of the most recent text. */
1355
+ var BoundedTail = class {
1356
+ #maxBytes;
1357
+ #chunks = [];
1358
+ #bytes = 0;
1359
+ #truncated = false;
1360
+ constructor(maxBytes) {
1361
+ this.#maxBytes = maxBytes;
1362
+ }
1363
+ get truncated() {
1364
+ return this.#truncated;
1365
+ }
1366
+ push(text) {
1367
+ if (text.length === 0) return;
1368
+ let chunk = Buffer.from(text, "utf8");
1369
+ if (chunk.length > this.#maxBytes) {
1370
+ chunk = chunk.subarray(chunk.length - this.#maxBytes);
1371
+ this.#chunks = [];
1372
+ this.#bytes = 0;
1373
+ this.#truncated = true;
1374
+ }
1375
+ this.#chunks.push(chunk);
1376
+ this.#bytes += chunk.length;
1377
+ while (this.#bytes > this.#maxBytes) {
1378
+ const head = this.#chunks[0];
1379
+ const excess = this.#bytes - this.#maxBytes;
1380
+ this.#truncated = true;
1381
+ if (head.length <= excess) {
1382
+ this.#chunks.shift();
1383
+ this.#bytes -= head.length;
1384
+ } else {
1385
+ this.#chunks[0] = head.subarray(excess);
1386
+ this.#bytes -= excess;
1387
+ }
1388
+ }
1389
+ }
1390
+ toString() {
1391
+ let text = Buffer.concat(this.#chunks).toString("utf8");
1392
+ if (this.#truncated) text = text.replace(/^\uFFFD+/, "");
1393
+ return text;
1394
+ }
1395
+ };
1396
+ function readFields(buffer) {
1397
+ const fields = [];
1398
+ let offset = 0;
1399
+ const readVarint = () => {
1400
+ let result = 0;
1401
+ let shift = 0;
1402
+ for (;;) {
1403
+ if (offset >= buffer.length) throw new Error("truncated varint");
1404
+ const byte = buffer[offset++];
1405
+ result += (byte & 127) * 2 ** shift;
1406
+ if ((byte & 128) === 0) return result;
1407
+ shift += 7;
1408
+ }
1409
+ };
1410
+ while (offset < buffer.length) {
1411
+ const key = readVarint();
1412
+ const field = Math.floor(key / 8);
1413
+ const wireType = key % 8;
1414
+ switch (wireType) {
1415
+ case 0:
1416
+ fields.push({
1417
+ field,
1418
+ value: readVarint()
1419
+ });
1420
+ break;
1421
+ case 1:
1422
+ offset += 8;
1423
+ break;
1424
+ case 5:
1425
+ offset += 4;
1426
+ break;
1427
+ case 2: {
1428
+ const length = readVarint();
1429
+ if (offset + length > buffer.length) throw new Error("truncated field");
1430
+ fields.push({
1431
+ field,
1432
+ value: buffer.subarray(offset, offset + length)
1433
+ });
1434
+ offset += length;
1435
+ break;
1436
+ }
1437
+ default: throw new Error(`unsupported protobuf wire type ${wireType}`);
1438
+ }
1439
+ }
1440
+ return fields;
1441
+ }
1442
+ /** @internal exported for tests. Throws on malformed input. */
1443
+ function decodeBuildkitStatus(aux) {
1444
+ const vertexes = [];
1445
+ const logs = [];
1446
+ for (const { field, value } of readFields(Buffer.from(aux, "base64"))) {
1447
+ if (!Buffer.isBuffer(value)) continue;
1448
+ if (field === 1) {
1449
+ const vertex = {
1450
+ digest: "",
1451
+ cached: false,
1452
+ started: false,
1453
+ completed: false
1454
+ };
1455
+ for (const f of readFields(value)) if (f.field === 1 && Buffer.isBuffer(f.value)) vertex.digest = f.value.toString("utf8");
1456
+ else if (f.field === 3 && Buffer.isBuffer(f.value)) vertex.name = f.value.toString("utf8");
1457
+ else if (f.field === 4) vertex.cached = f.value !== 0;
1458
+ else if (f.field === 5) vertex.started = true;
1459
+ else if (f.field === 6) vertex.completed = true;
1460
+ else if (f.field === 7 && Buffer.isBuffer(f.value) && f.value.length > 0) vertex.error = f.value.toString("utf8");
1461
+ if (vertex.digest) vertexes.push(vertex);
1462
+ } else if (field === 3) {
1463
+ let vertex = "";
1464
+ let data = Buffer.alloc(0);
1465
+ for (const f of readFields(value)) if (f.field === 1 && Buffer.isBuffer(f.value)) vertex = f.value.toString("utf8");
1466
+ else if (f.field === 4 && Buffer.isBuffer(f.value)) data = f.value;
1467
+ logs.push({
1468
+ vertex,
1469
+ data
1470
+ });
1471
+ }
1472
+ }
1473
+ return {
1474
+ vertexes,
1475
+ logs
1476
+ };
1477
+ }
1478
+ const LEGACY_STEP = /^Step (\d+)\/(\d+) : (.*)$/;
1479
+ function createBuildEventNormalizer(emit, tail) {
1480
+ let failure;
1481
+ let failedStep;
1482
+ let legacyStep;
1483
+ let legacyCached;
1484
+ let pending = "";
1485
+ const vertexes = /* @__PURE__ */ new Map();
1486
+ const decoders = /* @__PURE__ */ new Map();
1487
+ const output = (text, stepId) => {
1488
+ if (!text) return;
1489
+ tail.push(text);
1490
+ emit({
1491
+ type: "output",
1492
+ ...stepId !== void 0 && { stepId },
1493
+ text
1494
+ });
1495
+ };
1496
+ const completeLegacyStep = (error) => {
1497
+ if (!legacyStep) return;
1498
+ const step = {
1499
+ ...legacyStep,
1500
+ ...legacyCached !== void 0 && { cached: legacyCached }
1501
+ };
1502
+ if (error !== void 0) failedStep = step;
1503
+ emit({
1504
+ type: "step-complete",
1505
+ step,
1506
+ ...error !== void 0 && { error }
1507
+ });
1508
+ legacyStep = void 0;
1509
+ legacyCached = void 0;
1510
+ };
1511
+ const handleLegacyLine = (line) => {
1512
+ const match = LEGACY_STEP.exec(line.replace(/\r?\n$/, ""));
1513
+ if (match) {
1514
+ completeLegacyStep();
1515
+ legacyStep = {
1516
+ id: match[1],
1517
+ name: match[3],
1518
+ index: Number(match[1]),
1519
+ total: Number(match[2])
1520
+ };
1521
+ emit({
1522
+ type: "step-start",
1523
+ step: { ...legacyStep }
1524
+ });
1525
+ return;
1526
+ }
1527
+ if (legacyStep) {
1528
+ const trimmed = line.trim();
1529
+ if (trimmed === "---> Using cache") legacyCached = true;
1530
+ else if (trimmed.startsWith("---> Running in ") && legacyCached === void 0) legacyCached = false;
1531
+ }
1532
+ output(line, legacyStep?.id);
1533
+ };
1534
+ const handleLegacyStream = (text) => {
1535
+ pending += text;
1536
+ let newline = pending.indexOf("\n");
1537
+ while (newline !== -1) {
1538
+ handleLegacyLine(pending.slice(0, newline + 1));
1539
+ pending = pending.slice(newline + 1);
1540
+ newline = pending.indexOf("\n");
1541
+ }
1542
+ };
1543
+ const flushLegacy = () => {
1544
+ if (pending) {
1545
+ handleLegacyLine(pending);
1546
+ pending = "";
1547
+ }
1548
+ };
1549
+ const handleBuildkit = (aux) => {
1550
+ const { vertexes: updates, logs } = decodeBuildkitStatus(aux);
1551
+ for (const update of updates) {
1552
+ let state = vertexes.get(update.digest);
1553
+ if (!state) {
1554
+ state = {
1555
+ step: {
1556
+ id: update.digest,
1557
+ name: update.name ?? ""
1558
+ },
1559
+ started: false,
1560
+ completed: false
1561
+ };
1562
+ vertexes.set(update.digest, state);
1563
+ }
1564
+ if (update.name && !state.step.name) state.step.name = update.name;
1565
+ if (update.cached) state.step.cached = true;
1566
+ if (state.step.name.startsWith("[internal]")) continue;
1567
+ if (!state.started && (update.started || update.completed || update.cached || update.error)) {
1568
+ state.started = true;
1569
+ emit({
1570
+ type: "step-start",
1571
+ step: { ...state.step }
1572
+ });
1573
+ }
1574
+ if (!state.completed && (update.completed || update.error)) {
1575
+ state.completed = true;
1576
+ if (state.step.cached === void 0) state.step.cached = false;
1577
+ const step = { ...state.step };
1578
+ if (update.error) failedStep = step;
1579
+ emit({
1580
+ type: "step-complete",
1581
+ step,
1582
+ ...update.error && { error: update.error }
1583
+ });
1584
+ }
1585
+ }
1586
+ for (const log of logs) {
1587
+ if (vertexes.get(log.vertex)?.step.name.startsWith("[internal]")) continue;
1588
+ let decoder = decoders.get(log.vertex);
1589
+ if (!decoder) {
1590
+ decoder = new StringDecoder("utf8");
1591
+ decoders.set(log.vertex, decoder);
1592
+ }
1593
+ output(decoder.write(log.data), log.vertex || void 0);
1594
+ }
1595
+ };
1596
+ const handleError = (raw) => {
1597
+ const detail = raw.errorDetail;
1598
+ const message = String(detail?.message ?? raw.error ?? "docker build failed");
1599
+ flushLegacy();
1600
+ completeLegacyStep(message);
1601
+ if (!failure) failure = {
1602
+ message,
1603
+ ...failedStep && { step: failedStep }
1604
+ };
1605
+ };
1606
+ return {
1607
+ handle(raw) {
1608
+ if (!raw || typeof raw !== "object") return;
1609
+ const event = raw;
1610
+ try {
1611
+ if (event.error !== void 0 || event.errorDetail !== void 0) handleError(event);
1612
+ else if (event.id === "moby.buildkit.trace" && typeof event.aux === "string") handleBuildkit(event.aux);
1613
+ else if (typeof event.stream === "string") handleLegacyStream(event.stream);
1614
+ } catch {}
1615
+ },
1616
+ finish() {
1617
+ flushLegacy();
1618
+ completeLegacyStep();
1619
+ },
1620
+ failure: () => failure
1621
+ };
1622
+ }
1623
+ //#endregion
1340
1624
  //#region src/template/build-session.ts
1341
1625
  /**
1342
1626
  * BuildKit session that serves build secrets to the daemon.
@@ -1873,20 +2157,29 @@ var DockerTemplate = class DockerTemplate {
1873
2157
  * `options.secrets`, the template's `secrets` option, or `process.env`.
1874
2158
  */
1875
2159
  async build(options = {}) {
2160
+ const { maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES } = options;
2161
+ if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0) throw new RangeError("maxOutputBytes must be a positive integer");
1876
2162
  throwIfAborted(options.abortSignal, "build Docker template");
1877
2163
  const isPlain = !options.force && options.secrets === void 0 && options.docker === void 0;
1878
- if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal);
2164
+ if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal, options.onProgress);
1879
2165
  const previous = this.#queueTail?.promise.catch(() => void 0) ?? Promise.resolve();
1880
2166
  const controller = new AbortController();
2167
+ const listeners = /* @__PURE__ */ new Set();
2168
+ const emit = (event) => {
2169
+ for (const listener of [...listeners]) try {
2170
+ listener(event);
2171
+ } catch {}
2172
+ };
1881
2173
  const run = {
1882
2174
  controller,
1883
2175
  waiters: 0,
2176
+ listeners,
1884
2177
  promise: previous.then(() => {
1885
2178
  throwIfAborted(controller.signal, "build Docker template");
1886
2179
  return this.#build({
1887
2180
  ...options,
1888
2181
  abortSignal: controller.signal
1889
- });
2182
+ }, emit, maxOutputBytes);
1890
2183
  })
1891
2184
  };
1892
2185
  this.#queueTail = run;
@@ -1895,20 +2188,23 @@ var DockerTemplate = class DockerTemplate {
1895
2188
  if (this.#queueTail === run) this.#queueTail = void 0;
1896
2189
  if (this.#plainBuild === run) this.#plainBuild = void 0;
1897
2190
  }).catch(() => void 0);
1898
- return this.#waitForBuild(run, options.abortSignal);
2191
+ return this.#waitForBuild(run, options.abortSignal, options.onProgress);
1899
2192
  }
1900
- #waitForBuild(run, abortSignal) {
2193
+ #waitForBuild(run, abortSignal, onProgress) {
1901
2194
  run.waiters += 1;
2195
+ const listener = onProgress && ((event) => onProgress(event));
2196
+ if (listener) run.listeners.add(listener);
1902
2197
  let settled = false;
1903
2198
  const release = (aborted) => {
1904
2199
  if (settled) return;
1905
2200
  settled = true;
2201
+ if (listener) run.listeners.delete(listener);
1906
2202
  run.waiters -= 1;
1907
2203
  if (aborted && run.waiters === 0) run.controller.abort();
1908
2204
  };
1909
2205
  return waitForAbortable(run.promise, abortSignal, "build Docker template", () => release(true)).finally(() => release(false));
1910
2206
  }
1911
- async #build(options) {
2207
+ async #build(options, emit, maxOutputBytes) {
1912
2208
  const docker = options.docker ?? this.#getDocker();
1913
2209
  const tag = this.templateId;
1914
2210
  const { abortSignal } = options;
@@ -1917,6 +2213,10 @@ var DockerTemplate = class DockerTemplate {
1917
2213
  await docker.getImage(tag).inspect();
1918
2214
  throwIfAborted(abortSignal, "build Docker template");
1919
2215
  this.#built = true;
2216
+ emit({
2217
+ type: "reused",
2218
+ templateId: tag
2219
+ });
1920
2220
  return {
1921
2221
  status: "ready",
1922
2222
  templateId: tag
@@ -1929,12 +2229,19 @@ var DockerTemplate = class DockerTemplate {
1929
2229
  const context = pack();
1930
2230
  context.entry({ name: "Dockerfile" }, this.dockerfile);
1931
2231
  context.finalize();
2232
+ const tail = new BoundedTail(maxOutputBytes);
2233
+ const normalizer = createBuildEventNormalizer(emit, tail);
2234
+ emit({
2235
+ type: "build-start",
2236
+ templateId: tag,
2237
+ builder: secrets ? "buildkit" : "legacy"
2238
+ });
1932
2239
  try {
1933
2240
  const nocache = options.force === true;
1934
2241
  if (secrets) {
1935
2242
  const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal);
1936
2243
  try {
1937
- await this.#followBuild(docker, stream, abortSignal);
2244
+ await this.#followBuild(docker, stream, normalizer, abortSignal);
1938
2245
  } finally {
1939
2246
  session.close();
1940
2247
  }
@@ -1952,18 +2259,31 @@ var DockerTemplate = class DockerTemplate {
1952
2259
  }), abortSignal, "build Docker template", () => {
1953
2260
  context.destroy?.();
1954
2261
  });
1955
- await this.#followBuild(docker, stream, abortSignal);
2262
+ await this.#followBuild(docker, stream, normalizer, abortSignal);
1956
2263
  }
1957
2264
  } catch (error) {
1958
2265
  this.#built = false;
1959
2266
  if (abortSignal?.aborted) throw normalizeAbortError(error, "build Docker template");
2267
+ emit({
2268
+ type: "build-complete",
2269
+ status: "failed"
2270
+ });
2271
+ const failedStep = normalizer.failure()?.step;
1960
2272
  return {
1961
2273
  status: "failed",
1962
2274
  templateId: tag,
1963
- error: error instanceof Error ? error.message : String(error)
2275
+ error: error instanceof Error ? error.message : String(error),
2276
+ ...failedStep && { failedStep },
2277
+ output: tail.toString(),
2278
+ outputTruncated: tail.truncated
1964
2279
  };
1965
2280
  }
2281
+ normalizer.finish();
1966
2282
  this.#built = true;
2283
+ emit({
2284
+ type: "build-complete",
2285
+ status: "ready"
2286
+ });
1967
2287
  return {
1968
2288
  status: "ready",
1969
2289
  templateId: tag
@@ -2041,7 +2361,7 @@ var DockerTemplate = class DockerTemplate {
2041
2361
  session
2042
2362
  };
2043
2363
  }
2044
- #followBuild(docker, stream, abortSignal) {
2364
+ #followBuild(docker, stream, normalizer, abortSignal) {
2045
2365
  return new Promise((resolve, reject) => {
2046
2366
  let settled = false;
2047
2367
  const finish = (error) => {
@@ -2064,6 +2384,11 @@ var DockerTemplate = class DockerTemplate {
2064
2384
  finish(err);
2065
2385
  return;
2066
2386
  }
2387
+ const reported = normalizer.failure();
2388
+ if (reported) {
2389
+ finish(new Error(reported.message));
2390
+ return;
2391
+ }
2067
2392
  const failure = output?.find((entry) => entry && (entry.error !== void 0 || entry.errorDetail !== void 0));
2068
2393
  if (failure) {
2069
2394
  const detail = failure.errorDetail;
@@ -2071,6 +2396,8 @@ var DockerTemplate = class DockerTemplate {
2071
2396
  return;
2072
2397
  }
2073
2398
  finish();
2399
+ }, (event) => {
2400
+ if (!settled) normalizer.handle(event);
2074
2401
  });
2075
2402
  });
2076
2403
  }