@mastra/docker 0.11.1-alpha.0 → 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
@@ -279,10 +279,11 @@ var DockerProcessHandle = class extends ProcessHandle {
279
279
  "sh",
280
280
  this._pgidFile
281
281
  ],
282
- AttachStdout: false,
283
- AttachStderr: false
282
+ AttachStdout: true,
283
+ AttachStderr: true
284
284
  });
285
285
  const killStream = await killExec.start({});
286
+ killStream.resume();
286
287
  try {
287
288
  let killInfo = await killExec.inspect();
288
289
  while (killInfo.Running) {
@@ -581,6 +582,7 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
581
582
  /** Image the container boots from; rewritten by a template resolution on `start()`. */
582
583
  _image;
583
584
  _templateSpec;
585
+ _onTemplateBuildProgress;
584
586
  _workingDirectoryWasSet;
585
587
  _command;
586
588
  _env;
@@ -624,6 +626,7 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
624
626
  this._containerName = sanitizeContainerName(options.name ?? this.id);
625
627
  if (options.image !== void 0 && options.template !== void 0) throw new TypeError("DockerSandbox: `image` and `template` are mutually exclusive");
626
628
  this._templateSpec = options.template;
629
+ this._onTemplateBuildProgress = options.onTemplateBuildProgress;
627
630
  this._image = options.image ?? "node:22-slim";
628
631
  this._workingDirectoryWasSet = options.workingDirectory !== void 0 || options.workingDir !== void 0;
629
632
  this._command = options.command ?? ["sleep", "infinity"];
@@ -983,7 +986,8 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
983
986
  throwIfAborted(abortSignal, "start Docker sandbox");
984
987
  const result = await template.build({
985
988
  docker: this._docker,
986
- abortSignal
989
+ abortSignal,
990
+ ...this._onTemplateBuildProgress && { onProgress: this._onTemplateBuildProgress }
987
991
  });
988
992
  if (result.status !== "ready") throw new SandboxError(`Docker template build failed: ${result.error ?? "unknown error"}`, "START_FAILED", {
989
993
  templateId: result.templateId,
@@ -1336,6 +1340,287 @@ const dockerSandboxProvider = {
1336
1340
  createSandbox: (config) => new DockerSandbox(config)
1337
1341
  };
1338
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
1339
1624
  //#region src/template/build-session.ts
1340
1625
  /**
1341
1626
  * BuildKit session that serves build secrets to the daemon.
@@ -1872,20 +2157,29 @@ var DockerTemplate = class DockerTemplate {
1872
2157
  * `options.secrets`, the template's `secrets` option, or `process.env`.
1873
2158
  */
1874
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");
1875
2162
  throwIfAborted(options.abortSignal, "build Docker template");
1876
2163
  const isPlain = !options.force && options.secrets === void 0 && options.docker === void 0;
1877
- 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);
1878
2165
  const previous = this.#queueTail?.promise.catch(() => void 0) ?? Promise.resolve();
1879
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
+ };
1880
2173
  const run = {
1881
2174
  controller,
1882
2175
  waiters: 0,
2176
+ listeners,
1883
2177
  promise: previous.then(() => {
1884
2178
  throwIfAborted(controller.signal, "build Docker template");
1885
2179
  return this.#build({
1886
2180
  ...options,
1887
2181
  abortSignal: controller.signal
1888
- });
2182
+ }, emit, maxOutputBytes);
1889
2183
  })
1890
2184
  };
1891
2185
  this.#queueTail = run;
@@ -1894,20 +2188,23 @@ var DockerTemplate = class DockerTemplate {
1894
2188
  if (this.#queueTail === run) this.#queueTail = void 0;
1895
2189
  if (this.#plainBuild === run) this.#plainBuild = void 0;
1896
2190
  }).catch(() => void 0);
1897
- return this.#waitForBuild(run, options.abortSignal);
2191
+ return this.#waitForBuild(run, options.abortSignal, options.onProgress);
1898
2192
  }
1899
- #waitForBuild(run, abortSignal) {
2193
+ #waitForBuild(run, abortSignal, onProgress) {
1900
2194
  run.waiters += 1;
2195
+ const listener = onProgress && ((event) => onProgress(event));
2196
+ if (listener) run.listeners.add(listener);
1901
2197
  let settled = false;
1902
2198
  const release = (aborted) => {
1903
2199
  if (settled) return;
1904
2200
  settled = true;
2201
+ if (listener) run.listeners.delete(listener);
1905
2202
  run.waiters -= 1;
1906
2203
  if (aborted && run.waiters === 0) run.controller.abort();
1907
2204
  };
1908
2205
  return waitForAbortable(run.promise, abortSignal, "build Docker template", () => release(true)).finally(() => release(false));
1909
2206
  }
1910
- async #build(options) {
2207
+ async #build(options, emit, maxOutputBytes) {
1911
2208
  const docker = options.docker ?? this.#getDocker();
1912
2209
  const tag = this.templateId;
1913
2210
  const { abortSignal } = options;
@@ -1916,6 +2213,10 @@ var DockerTemplate = class DockerTemplate {
1916
2213
  await docker.getImage(tag).inspect();
1917
2214
  throwIfAborted(abortSignal, "build Docker template");
1918
2215
  this.#built = true;
2216
+ emit({
2217
+ type: "reused",
2218
+ templateId: tag
2219
+ });
1919
2220
  return {
1920
2221
  status: "ready",
1921
2222
  templateId: tag
@@ -1928,12 +2229,19 @@ var DockerTemplate = class DockerTemplate {
1928
2229
  const context = pack();
1929
2230
  context.entry({ name: "Dockerfile" }, this.dockerfile);
1930
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
+ });
1931
2239
  try {
1932
2240
  const nocache = options.force === true;
1933
2241
  if (secrets) {
1934
2242
  const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal);
1935
2243
  try {
1936
- await this.#followBuild(docker, stream, abortSignal);
2244
+ await this.#followBuild(docker, stream, normalizer, abortSignal);
1937
2245
  } finally {
1938
2246
  session.close();
1939
2247
  }
@@ -1951,18 +2259,31 @@ var DockerTemplate = class DockerTemplate {
1951
2259
  }), abortSignal, "build Docker template", () => {
1952
2260
  context.destroy?.();
1953
2261
  });
1954
- await this.#followBuild(docker, stream, abortSignal);
2262
+ await this.#followBuild(docker, stream, normalizer, abortSignal);
1955
2263
  }
1956
2264
  } catch (error) {
1957
2265
  this.#built = false;
1958
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;
1959
2272
  return {
1960
2273
  status: "failed",
1961
2274
  templateId: tag,
1962
- 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
1963
2279
  };
1964
2280
  }
2281
+ normalizer.finish();
1965
2282
  this.#built = true;
2283
+ emit({
2284
+ type: "build-complete",
2285
+ status: "ready"
2286
+ });
1966
2287
  return {
1967
2288
  status: "ready",
1968
2289
  templateId: tag
@@ -2040,7 +2361,7 @@ var DockerTemplate = class DockerTemplate {
2040
2361
  session
2041
2362
  };
2042
2363
  }
2043
- #followBuild(docker, stream, abortSignal) {
2364
+ #followBuild(docker, stream, normalizer, abortSignal) {
2044
2365
  return new Promise((resolve, reject) => {
2045
2366
  let settled = false;
2046
2367
  const finish = (error) => {
@@ -2063,6 +2384,11 @@ var DockerTemplate = class DockerTemplate {
2063
2384
  finish(err);
2064
2385
  return;
2065
2386
  }
2387
+ const reported = normalizer.failure();
2388
+ if (reported) {
2389
+ finish(new Error(reported.message));
2390
+ return;
2391
+ }
2066
2392
  const failure = output?.find((entry) => entry && (entry.error !== void 0 || entry.errorDetail !== void 0));
2067
2393
  if (failure) {
2068
2394
  const detail = failure.errorDetail;
@@ -2070,6 +2396,8 @@ var DockerTemplate = class DockerTemplate {
2070
2396
  return;
2071
2397
  }
2072
2398
  finish();
2399
+ }, (event) => {
2400
+ if (!settled) normalizer.handle(event);
2073
2401
  });
2074
2402
  });
2075
2403
  }