@mastra/docker 0.11.1-alpha.1 → 0.12.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/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 = 65536;
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.
@@ -1618,9 +1902,7 @@ function synthesizeDockerfile(definition) {
1618
1902
  case "pipInstall":
1619
1903
  lines.push(renderPipInstall(operation.args[0], operation.args[1]));
1620
1904
  break;
1621
- case "npmInstall":
1622
- lines.push(renderNpmInstall(operation.args[0], operation.args[1]));
1623
- break;
1905
+ case "npmInstall": lines.push(renderNpmInstall(operation.args[0], operation.args[1]));
1624
1906
  }
1625
1907
  });
1626
1908
  return `${lines.join("\n")}\n`;
@@ -1705,7 +1987,7 @@ function templateImageTag(definition) {
1705
1987
  * ```
1706
1988
  */
1707
1989
  const MAX_OPERATIONS = 256;
1708
- const MAX_STRING_LENGTH = 32 * 1024;
1990
+ const MAX_STRING_LENGTH = 32768;
1709
1991
  const MAX_COLLECTION_ITEMS = 512;
1710
1992
  /**
1711
1993
  * Immutable, chainable builder + build/lifecycle for a local Docker template.
@@ -1873,20 +2155,29 @@ var DockerTemplate = class DockerTemplate {
1873
2155
  * `options.secrets`, the template's `secrets` option, or `process.env`.
1874
2156
  */
1875
2157
  async build(options = {}) {
2158
+ const { maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES } = options;
2159
+ if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0) throw new RangeError("maxOutputBytes must be a positive integer");
1876
2160
  throwIfAborted(options.abortSignal, "build Docker template");
1877
2161
  const isPlain = !options.force && options.secrets === void 0 && options.docker === void 0;
1878
- if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal);
2162
+ if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal, options.onProgress);
1879
2163
  const previous = this.#queueTail?.promise.catch(() => void 0) ?? Promise.resolve();
1880
2164
  const controller = new AbortController();
2165
+ const listeners = /* @__PURE__ */ new Set();
2166
+ const emit = (event) => {
2167
+ for (const listener of [...listeners]) try {
2168
+ listener(event);
2169
+ } catch {}
2170
+ };
1881
2171
  const run = {
1882
2172
  controller,
1883
2173
  waiters: 0,
2174
+ listeners,
1884
2175
  promise: previous.then(() => {
1885
2176
  throwIfAborted(controller.signal, "build Docker template");
1886
2177
  return this.#build({
1887
2178
  ...options,
1888
2179
  abortSignal: controller.signal
1889
- });
2180
+ }, emit, maxOutputBytes);
1890
2181
  })
1891
2182
  };
1892
2183
  this.#queueTail = run;
@@ -1895,20 +2186,23 @@ var DockerTemplate = class DockerTemplate {
1895
2186
  if (this.#queueTail === run) this.#queueTail = void 0;
1896
2187
  if (this.#plainBuild === run) this.#plainBuild = void 0;
1897
2188
  }).catch(() => void 0);
1898
- return this.#waitForBuild(run, options.abortSignal);
2189
+ return this.#waitForBuild(run, options.abortSignal, options.onProgress);
1899
2190
  }
1900
- #waitForBuild(run, abortSignal) {
2191
+ #waitForBuild(run, abortSignal, onProgress) {
1901
2192
  run.waiters += 1;
2193
+ const listener = onProgress && ((event) => onProgress(event));
2194
+ if (listener) run.listeners.add(listener);
1902
2195
  let settled = false;
1903
2196
  const release = (aborted) => {
1904
2197
  if (settled) return;
1905
2198
  settled = true;
2199
+ if (listener) run.listeners.delete(listener);
1906
2200
  run.waiters -= 1;
1907
2201
  if (aborted && run.waiters === 0) run.controller.abort();
1908
2202
  };
1909
2203
  return waitForAbortable(run.promise, abortSignal, "build Docker template", () => release(true)).finally(() => release(false));
1910
2204
  }
1911
- async #build(options) {
2205
+ async #build(options, emit, maxOutputBytes) {
1912
2206
  const docker = options.docker ?? this.#getDocker();
1913
2207
  const tag = this.templateId;
1914
2208
  const { abortSignal } = options;
@@ -1917,6 +2211,10 @@ var DockerTemplate = class DockerTemplate {
1917
2211
  await docker.getImage(tag).inspect();
1918
2212
  throwIfAborted(abortSignal, "build Docker template");
1919
2213
  this.#built = true;
2214
+ emit({
2215
+ type: "reused",
2216
+ templateId: tag
2217
+ });
1920
2218
  return {
1921
2219
  status: "ready",
1922
2220
  templateId: tag
@@ -1929,12 +2227,19 @@ var DockerTemplate = class DockerTemplate {
1929
2227
  const context = pack();
1930
2228
  context.entry({ name: "Dockerfile" }, this.dockerfile);
1931
2229
  context.finalize();
2230
+ const tail = new BoundedTail(maxOutputBytes);
2231
+ const normalizer = createBuildEventNormalizer(emit, tail);
2232
+ emit({
2233
+ type: "build-start",
2234
+ templateId: tag,
2235
+ builder: secrets ? "buildkit" : "legacy"
2236
+ });
1932
2237
  try {
1933
2238
  const nocache = options.force === true;
1934
2239
  if (secrets) {
1935
2240
  const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal);
1936
2241
  try {
1937
- await this.#followBuild(docker, stream, abortSignal);
2242
+ await this.#followBuild(docker, stream, normalizer, abortSignal);
1938
2243
  } finally {
1939
2244
  session.close();
1940
2245
  }
@@ -1952,18 +2257,31 @@ var DockerTemplate = class DockerTemplate {
1952
2257
  }), abortSignal, "build Docker template", () => {
1953
2258
  context.destroy?.();
1954
2259
  });
1955
- await this.#followBuild(docker, stream, abortSignal);
2260
+ await this.#followBuild(docker, stream, normalizer, abortSignal);
1956
2261
  }
1957
2262
  } catch (error) {
1958
2263
  this.#built = false;
1959
2264
  if (abortSignal?.aborted) throw normalizeAbortError(error, "build Docker template");
2265
+ emit({
2266
+ type: "build-complete",
2267
+ status: "failed"
2268
+ });
2269
+ const failedStep = normalizer.failure()?.step;
1960
2270
  return {
1961
2271
  status: "failed",
1962
2272
  templateId: tag,
1963
- error: error instanceof Error ? error.message : String(error)
2273
+ error: error instanceof Error ? error.message : String(error),
2274
+ ...failedStep && { failedStep },
2275
+ output: tail.toString(),
2276
+ outputTruncated: tail.truncated
1964
2277
  };
1965
2278
  }
2279
+ normalizer.finish();
1966
2280
  this.#built = true;
2281
+ emit({
2282
+ type: "build-complete",
2283
+ status: "ready"
2284
+ });
1967
2285
  return {
1968
2286
  status: "ready",
1969
2287
  templateId: tag
@@ -2041,7 +2359,7 @@ var DockerTemplate = class DockerTemplate {
2041
2359
  session
2042
2360
  };
2043
2361
  }
2044
- #followBuild(docker, stream, abortSignal) {
2362
+ #followBuild(docker, stream, normalizer, abortSignal) {
2045
2363
  return new Promise((resolve, reject) => {
2046
2364
  let settled = false;
2047
2365
  const finish = (error) => {
@@ -2064,6 +2382,11 @@ var DockerTemplate = class DockerTemplate {
2064
2382
  finish(err);
2065
2383
  return;
2066
2384
  }
2385
+ const reported = normalizer.failure();
2386
+ if (reported) {
2387
+ finish(new Error(reported.message));
2388
+ return;
2389
+ }
2067
2390
  const failure = output?.find((entry) => entry && (entry.error !== void 0 || entry.errorDetail !== void 0));
2068
2391
  if (failure) {
2069
2392
  const detail = failure.errorDetail;
@@ -2071,6 +2394,8 @@ var DockerTemplate = class DockerTemplate {
2071
2394
  return;
2072
2395
  }
2073
2396
  finish();
2397
+ }, (event) => {
2398
+ if (!settled) normalizer.handle(event);
2074
2399
  });
2075
2400
  });
2076
2401
  }
@@ -2209,11 +2534,18 @@ const REPO_DIR_PATTERN = /^[\w.-]+$/;
2209
2534
  function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
2210
2535
  if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
2211
2536
  if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
2212
- return `( cd "${repoDir}" && sh -c ${shellQuote$1(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
2537
+ return `( cd "${repoDir}" && sh -c ${shellQuote$2(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
2213
2538
  }
2214
- function shellQuote$1(value) {
2539
+ function shellQuote$2(value) {
2215
2540
  return `'${value.replace(/'/g, `'\\''`)}'`;
2216
2541
  }
2542
+ const { notFound, isDirectory, exists, notDirectory, destIsDirectory } = {
2543
+ notFound: 20,
2544
+ isDirectory: 21,
2545
+ exists: 22,
2546
+ notDirectory: 23,
2547
+ destIsDirectory: 24
2548
+ };
2217
2549
  //#endregion
2218
2550
  //#region src/template/repo-template.ts
2219
2551
  /**