agentful 0.4.16 → 0.4.17

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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/dist/index.cjs +225 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -44,9 +44,12 @@ Inside the interface, `/` lists the Agentful commands (`/setup`, `/push`,
44
44
  site, and asks you before it changes a file or pushes.
45
45
 
46
46
  `push` respects your `.gitignore` and always excludes `.git`, `node_modules`,
47
- build output and every `.env` file. A push carries at most about 4.4 MiB
48
- compressed, 25 MiB of files and 2,000 files, and a static site or a prebuilt upload needs
49
- an `index.html` at its top level. `push` checks all of this before it signs you
47
+ build output and every `.env` file. A push carries at most 25 MiB of files
48
+ (32 MiB compressed) and 2,000 files, and a static site or a prebuilt upload needs
49
+ an `index.html` at its top level. The preview does not deliver every file yet:
50
+ images, fonts and PDFs up to about 4.4 MiB each, pages and scripts up to about
51
+ 5.5 MiB, and no video or audio. `push` names the files this concerns and uploads
52
+ them all the same. `push` checks all of this before it signs you
50
53
  in or creates a project.
51
54
 
52
55
  `agentful push --dry-run` shows that check on its own: what would be uploaded
package/dist/index.cjs CHANGED
@@ -3044,7 +3044,7 @@ var VERSION, brand, useColor, wrap, paint, tuiDesign, sym, ui;
3044
3044
  var init_branding = __esm({
3045
3045
  "src/branding.ts"() {
3046
3046
  "use strict";
3047
- VERSION = "0.4.16" ? "0.4.16" : null.version;
3047
+ VERSION = "0.4.17" ? "0.4.17" : null.version;
3048
3048
  brand = {
3049
3049
  name: "agentful",
3050
3050
  // the command users type
@@ -5780,17 +5780,49 @@ var contract_default = {
5780
5780
 
5781
5781
  // src/lib/uploadLimits.ts
5782
5782
  var MAX_ZIP_UPLOAD_BYTES = contract_default.limits.max_zip_upload_bytes;
5783
+ var MAX_DIRECT_ZIP_UPLOAD_BYTES = 32 * 1024 * 1024;
5783
5784
  var MAX_UNPACKED_BYTES = 25 * 1024 * 1024;
5784
5785
  var MAX_UPLOAD_FILES = 2e3;
5785
5786
  var MAX_INLINE_REQUEST_BYTES = 6 * 1024 * 1024 - 64 * 1024;
5786
5787
  function uploadRequestBody(zipBase64, mode, declaration) {
5787
5788
  return { zip_base64: zipBase64, mode, clean: true, ...declaration ? { backend_declaration: declaration } : {} };
5788
5789
  }
5789
- function maxZipBytesForUpload(mode = "source", declaration = null) {
5790
+ function maxInlineZipBytes(mode = "source", declaration = null) {
5790
5791
  const around = Buffer.byteLength(JSON.stringify(uploadRequestBody("", mode, declaration)), "utf8");
5791
5792
  const base64Budget = Math.max(0, MAX_INLINE_REQUEST_BYTES - around);
5792
5793
  return Math.min(MAX_ZIP_UPLOAD_BYTES, Math.floor(base64Budget / 4) * 3);
5793
5794
  }
5795
+ function maxZipBytesForUpload() {
5796
+ return MAX_DIRECT_ZIP_UPLOAD_BYTES;
5797
+ }
5798
+ function announceRequestBody(zipBytes, sha256Hex, mode) {
5799
+ return { upload: { size: zipBytes, sha256: sha256Hex }, mode };
5800
+ }
5801
+ function completeRequestBody(uploadId, mode, declaration) {
5802
+ return { upload_id: uploadId, mode, clean: true, ...declaration ? { backend_declaration: declaration } : {} };
5803
+ }
5804
+ var PREVIEW_BINARY_MEASURED_PASS_BYTES = 4666163;
5805
+ var PREVIEW_MAX_BINARY_BYTES = PREVIEW_BINARY_MEASURED_PASS_BYTES;
5806
+ var PREVIEW_MAX_TEXT_BYTES = 5.5 * 1024 * 1024;
5807
+ var PREVIEW_BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
5808
+ "avif",
5809
+ "apng",
5810
+ "webp",
5811
+ "png",
5812
+ "jpg",
5813
+ "jpeg",
5814
+ "gif",
5815
+ "svg",
5816
+ "ico",
5817
+ "bmp",
5818
+ "tiff",
5819
+ "woff",
5820
+ "woff2",
5821
+ "ttf",
5822
+ "otf",
5823
+ "eot",
5824
+ "pdf"
5825
+ ]);
5794
5826
  var SERVER_SKIP_DIRS = /* @__PURE__ */ new Set([
5795
5827
  "node_modules",
5796
5828
  ".git",
@@ -5954,7 +5986,7 @@ function scanTree(rootDir, mode) {
5954
5986
  function collectFiles(rootDir, mode) {
5955
5987
  return scanTree(rootDir, mode).files;
5956
5988
  }
5957
- function zipToBase64(files, maxBytes = maxZipBytesForUpload()) {
5989
+ function zipFiles(files, maxBytes = maxZipBytesForUpload()) {
5958
5990
  if (files.length === 0) {
5959
5991
  throw new ApiError(0, "empty_project", "Nothing to push \u2014 no files found after applying ignore rules.");
5960
5992
  }
@@ -5970,11 +6002,7 @@ function zipToBase64(files, maxBytes = maxZipBytesForUpload()) {
5970
6002
  `The upload is ${zipped.byteLength.toLocaleString("en-US")} bytes compressed \u2014 a push carries at most ${maxBytes.toLocaleString("en-US")}. \`agentful push --dry-run\` shows what takes the space.`
5971
6003
  );
5972
6004
  }
5973
- return {
5974
- base64: Buffer.from(zipped).toString("base64"),
5975
- bytes: zipped.byteLength,
5976
- count: files.length
5977
- };
6005
+ return { data: zipped, bytes: zipped.byteLength, count: files.length };
5978
6006
  }
5979
6007
  function detectDistDir(rootDir, override) {
5980
6008
  if (override) {
@@ -6500,6 +6528,16 @@ var CLASS_BY_EXTENSION = new Map(
6500
6528
  );
6501
6529
  var EMBEDDED_SCAN_CLASSES = /* @__PURE__ */ new Set(["html", "css", "script", "data"]);
6502
6530
  var baseName = (rel) => rel.slice(rel.lastIndexOf("/") + 1);
6531
+ var TEXT_CLASSES = /* @__PURE__ */ new Set(["html", "css", "script", "data"]);
6532
+ var UNDELIVERED_CLASSES = /* @__PURE__ */ new Set(["image", "font", "document", "video", "audio", "archive"]);
6533
+ function previewDelivery(rel, bytes2) {
6534
+ const base = baseName(rel);
6535
+ const ext = base.includes(".") ? base.slice(base.lastIndexOf(".") + 1).toLowerCase() : "";
6536
+ if (PREVIEW_BINARY_EXTENSIONS.has(ext)) return bytes2 > PREVIEW_MAX_BINARY_BYTES ? "too_large" : "ok";
6537
+ const cls = classOf(rel);
6538
+ if (TEXT_CLASSES.has(cls)) return bytes2 > PREVIEW_MAX_TEXT_BYTES ? "too_large" : "ok";
6539
+ return UNDELIVERED_CLASSES.has(cls) || ext === "wasm" ? "not_delivered" : "ok";
6540
+ }
6503
6541
  function classOf(rel) {
6504
6542
  const base = baseName(rel);
6505
6543
  const dot = base.lastIndexOf(".");
@@ -6612,7 +6650,7 @@ function failedAnalysis(mode, projectDir, blocker) {
6612
6650
  ok: false,
6613
6651
  framework: null,
6614
6652
  project: projectLink(projectDir),
6615
- limits: limits(maxZipBytesForUpload(mode)),
6653
+ limits: limits(maxZipBytesForUpload()),
6616
6654
  totals: {
6617
6655
  files: 0,
6618
6656
  bytes: 0,
@@ -6728,10 +6766,14 @@ function analyzePush(options) {
6728
6766
  }
6729
6767
  duplicates.sort((a, b) => b.redundant_zip_bytes - a.redundant_zip_bytes || (a.paths[0] < b.paths[0] ? -1 : 1));
6730
6768
  const redundantZipBytes = duplicates.reduce((total, group) => total + group.redundant_zip_bytes, 0);
6731
- const maxZipBytes = maxZipBytesForUpload(mode, mode === "source" ? readBackendDeclaration(rootDir) : null);
6732
- const oversized = measured.filter((file) => file.zipLower > maxZipBytes).sort((a, b) => b.zipLower - a.zipLower || byPath(a, b));
6769
+ const maxZipBytes = maxZipBytesForUpload();
6770
+ const tooBigAsFile = (file) => view.stored.has(file.path) && file.bytes > MAX_UNPACKED_BYTES;
6771
+ const tooBigInZip = (file) => file.zipLower > maxZipBytes;
6772
+ const oversized = measured.filter((file) => tooBigAsFile(file) || tooBigInZip(file)).sort((a, b) => b.bytes - a.bytes || byPath(a, b));
6733
6773
  const oversizedPaths = new Set(oversized.map((file) => file.path));
6774
+ const oversizedAsFile = oversized.filter(tooBigAsFile);
6734
6775
  const restZipLower = zipLower - oversized.reduce((total, file) => total + file.zipLower, 0);
6776
+ const restStoredBytes = storedBytes - oversizedAsFile.reduce((total, file) => total + file.bytes, 0);
6735
6777
  const preflight = mode === "source" ? runPreflight(rootDir) : null;
6736
6778
  const framework = preflight?.framework ?? null;
6737
6779
  const hasManifest = storedNames.has("package.json");
@@ -6747,7 +6789,7 @@ function analyzePush(options) {
6747
6789
  title: file.title,
6748
6790
  sha256: file.sha256,
6749
6791
  same_content_as: first && first !== file.path ? first : null,
6750
- ...file.zipLower > maxZipBytes ? { larger_than_a_push: true } : {}
6792
+ ...oversizedPaths.has(file.path) ? { larger_than_a_push: true } : {}
6751
6793
  };
6752
6794
  });
6753
6795
  const fittingCandidates = candidates.filter((candidate) => !candidate.larger_than_a_push);
@@ -6778,28 +6820,39 @@ function analyzePush(options) {
6778
6820
  message: `${plural(measured.length, "file")} \u2014 the platform accepts at most ${MAX_UPLOAD_FILES.toLocaleString("en-US")} files per push.`
6779
6821
  });
6780
6822
  }
6781
- if (storedBytes > MAX_UNPACKED_BYTES) {
6823
+ const largerOnTheirOwn = (files, sizeOf, limit) => {
6824
+ const named = samplePaths(files.map((file) => `${file.path} (${sizeOverLimit(sizeOf(file), limit)[0]})`));
6825
+ return files.length === 1 ? ` ${named} is larger than that on its own, so no push can carry it.` : ` ${plural(files.length, "file")} are each larger than that on their own, so no push can carry them: ${named}.`;
6826
+ };
6827
+ const filesTooLarge = storedBytes > MAX_UNPACKED_BYTES;
6828
+ if (filesTooLarge) {
6782
6829
  const [size, allowed] = sizeOverLimit(storedBytes, MAX_UNPACKED_BYTES);
6830
+ let message = `The files add up to ${size} \u2014 the platform accepts at most ${allowed} of files per push.`;
6831
+ if (oversizedAsFile.length > 0) {
6832
+ message += largerOnTheirOwn(oversizedAsFile, (file) => file.bytes, MAX_UNPACKED_BYTES);
6833
+ message += ` Without ${oversizedAsFile.length === 1 ? "it" : "them"} the files ${restStoredBytes > MAX_UNPACKED_BYTES ? `still add up to ${sizeOverLimit(restStoredBytes, MAX_UNPACKED_BYTES)[0]}` : `add up to ${formatBytes(restStoredBytes)}, which is within that limit`}.`;
6834
+ }
6783
6835
  blockers.push({
6784
6836
  code: "unpacked_too_large",
6785
6837
  overridable: false,
6786
- message: `The files add up to ${size} \u2014 the platform accepts at most ${allowed} of files per push.`
6838
+ message,
6839
+ ...oversizedAsFile.length > 0 ? { paths: oversizedAsFile.map((file) => file.path).slice(0, FINDING_PATHS) } : {}
6787
6840
  });
6788
6841
  }
6789
6842
  if (zipLower > maxZipBytes) {
6790
6843
  const [size, allowed] = sizeOverLimit(zipExact ? zipBytes : zipLower, maxZipBytes);
6791
6844
  let message = `The upload is ${zipExact ? "" : "at least "}${size} compressed \u2014 a push carries at most ${allowed} compressed.`;
6845
+ const namedHere = oversized.filter((file) => tooBigInZip(file) && !(filesTooLarge && tooBigAsFile(file)));
6846
+ if (namedHere.length > 0) message += largerOnTheirOwn(namedHere, (file) => file.zipLower, maxZipBytes);
6792
6847
  if (oversized.length > 0) {
6793
- const one = oversized.length === 1;
6794
- const named = samplePaths(oversized.map((file) => `${file.path} (${sizeOverLimit(file.zipLower, maxZipBytes)[0]})`));
6795
- message += one ? ` ${named} is larger than that on its own, so no push can carry it.` : ` ${plural(oversized.length, "file")} are each larger than that on their own, so no push can carry them: ${named}.`;
6796
- message += ` Without ${one ? "it" : "them"} the upload is ${restZipLower > maxZipBytes ? `still ${sizeOverLimit(restZipLower, maxZipBytes)[0]} compressed` : `${formatBytes(restZipLower)} compressed, which is within that limit`}.`;
6848
+ const without = namedHere.length < oversized.length ? "the files no push can carry" : namedHere.length === 1 ? "it" : "them";
6849
+ message += ` Without ${without} the upload is ${restZipLower > maxZipBytes ? `still ${sizeOverLimit(restZipLower, maxZipBytes)[0]} compressed` : `${formatBytes(restZipLower)} compressed, which is within that limit`}.`;
6797
6850
  }
6798
6851
  blockers.push({
6799
6852
  code: "zip_too_large",
6800
6853
  overridable: false,
6801
6854
  message,
6802
- ...oversized.length > 0 ? { paths: oversized.map((file) => file.path).slice(0, FINDING_PATHS) } : {}
6855
+ ...namedHere.length > 0 ? { paths: namedHere.map((file) => file.path).slice(0, FINDING_PATHS) } : {}
6803
6856
  });
6804
6857
  } else if (!zipExact && zipBytes > maxZipBytes) {
6805
6858
  warnings.push({
@@ -6845,6 +6898,25 @@ function analyzePush(options) {
6845
6898
  });
6846
6899
  }
6847
6900
  }
6901
+ const servedAsIs = stored.filter((file) => mode === "dist" || framework === "vanilla" || /^(public|static)\//.test(view.stored.get(file.path) ?? ""));
6902
+ const tooLargeForPreview = servedAsIs.filter((file) => previewDelivery(file.path, file.bytes) === "too_large").sort((a, b) => b.bytes - a.bytes || byPath(a, b));
6903
+ if (tooLargeForPreview.length > 0) {
6904
+ const one = tooLargeForPreview.length === 1;
6905
+ warnings.push({
6906
+ code: "preview_file_too_large",
6907
+ message: `${plural(tooLargeForPreview.length, "file is", "files are")} larger than the preview delivers (about ${formatBytes(PREVIEW_MAX_BINARY_BYTES, "down")} for images, fonts and PDFs, about ${formatBytes(PREVIEW_MAX_TEXT_BYTES, "down")} for pages, scripts and other text): ${samplePaths(tooLargeForPreview.map((file) => `${file.path} (${formatBytes(file.bytes)})`))}. ${one ? "It is" : "They are"} uploaded, but the preview answers with an error instead of the file.`,
6908
+ paths: tooLargeForPreview.map((file) => file.path).slice(0, FINDING_PATHS)
6909
+ });
6910
+ }
6911
+ const notDeliveredByPreview = servedAsIs.filter((file) => previewDelivery(file.path, file.bytes) === "not_delivered").sort(byPath);
6912
+ if (notDeliveredByPreview.length > 0) {
6913
+ const one = notDeliveredByPreview.length === 1;
6914
+ warnings.push({
6915
+ code: "preview_type_not_delivered",
6916
+ message: `${plural(notDeliveredByPreview.length, "file is", "files are")} of a kind the preview does not deliver intact (video, audio, archives and other binary formats): ${samplePaths(notDeliveredByPreview.map((file) => file.path))}. ${one ? "It is" : "They are"} uploaded, but ${one ? "it does" : "they do"} not play or open in the preview.`,
6917
+ paths: notDeliveredByPreview.map((file) => file.path).slice(0, FINDING_PATHS)
6918
+ });
6919
+ }
6848
6920
  const filesOmitted = Math.max(0, measured.length - budgets.fileListCap);
6849
6921
  if (filesOmitted > 0) {
6850
6922
  warnings.push({
@@ -6888,14 +6960,16 @@ function analyzePush(options) {
6888
6960
  ].filter(Boolean);
6889
6961
  const toFit = ` To fit, at least ${mustStayOut.join(" and ")} ${mustStayOut.length > 1 ? "have" : "has"} to stay out.`;
6890
6962
  const spareCopies = duplicates.filter((group) => !oversizedPaths.has(group.paths[0]));
6891
- const spareZipBytes = spareCopies.reduce((total, group) => total + group.redundant_zip_bytes, 0);
6892
- const base = oversized.length > 0 ? restZipLower : zipBytes;
6893
- if (spareCopies.length > 0 && spareZipBytes >= base / 20) {
6963
+ const extraStored = (group) => group.paths.slice(1).filter((path) => view.stored.has(path)).length * group.bytes;
6964
+ const spare = spareCopies.reduce((total, group) => total + (filesTooLarge ? extraStored(group) : group.redundant_zip_bytes), 0);
6965
+ const base = filesTooLarge ? oversized.length > 0 ? restStoredBytes : storedBytes : oversized.length > 0 ? restZipLower : zipBytes;
6966
+ const limit = filesTooLarge ? MAX_UNPACKED_BYTES : maxZipBytes;
6967
+ if (spareCopies.length > 0 && spare >= base / 20) {
6894
6968
  const copies = spareCopies.reduce((total, group) => total + group.paths.length - 1, 0);
6895
- const remaining = base - spareZipBytes;
6969
+ const remaining = base - spare;
6896
6970
  suggestions.push({
6897
6971
  code: "remove_identical_copies",
6898
- message: `${plural(copies, "file is an identical copy", "files are identical copies")} of another file and ${copies === 1 ? "adds" : "add"} ${formatBytes(spareZipBytes)} to the compressed upload. If nothing links to the extra names, one copy each brings ${oversized.length > 0 ? "what is left without the files no push can carry" : "it"} to ${formatBytes(remaining)}${remaining > maxZipBytes ? ` \u2014 still above the ${zipLimit} limit` : ""}.`,
6972
+ message: `${plural(copies, "file is an identical copy", "files are identical copies")} of another file and ${copies === 1 ? "adds" : "add"} ${formatBytes(spare)} to the ${filesTooLarge ? "files of the upload" : "compressed upload"}. If nothing links to the extra names, one copy each brings ${oversized.length > 0 ? "what is left without the files no push can carry" : "it"} to ${formatBytes(remaining)}${remaining > limit ? ` \u2014 still above the ${formatBytes(limit, "down")} limit` : ""}.`,
6899
6973
  paths: spareCopies.flatMap((group) => group.paths.slice(1)).slice(0, FINDING_PATHS)
6900
6974
  });
6901
6975
  }
@@ -7058,6 +7132,103 @@ function wouldUpload(analysis, force = false) {
7058
7132
  return analysis.blockers.every((blocker) => blocker.overridable && force);
7059
7133
  }
7060
7134
 
7135
+ // src/lib/zipTransfer.ts
7136
+ var import_node_crypto2 = require("crypto");
7137
+ var INLINE_UPLOAD_TIMEOUT_MS = 12e4;
7138
+ var ANNOUNCE_TIMEOUT_MS = 3e4;
7139
+ var PUT_TIMEOUT_MS = 6e5;
7140
+ var COMPLETE_TIMEOUT_MS = 12e4;
7141
+ var PUT_ATTEMPTS = 2;
7142
+ async function uploadZip(upload, deps = { request, fetch }) {
7143
+ const { route, token, zip, mode, declaration, retry } = upload;
7144
+ if (zip.byteLength <= maxInlineZipBytes(mode, declaration)) {
7145
+ return deps.request(route, {
7146
+ token,
7147
+ retry,
7148
+ timeoutMs: INLINE_UPLOAD_TIMEOUT_MS,
7149
+ body: uploadRequestBody(Buffer.from(zip).toString("base64"), mode, declaration)
7150
+ });
7151
+ }
7152
+ const ticket = await announce(upload, deps);
7153
+ await putZip(ticket, zip, deps);
7154
+ return deps.request(route, {
7155
+ token,
7156
+ retry,
7157
+ timeoutMs: COMPLETE_TIMEOUT_MS,
7158
+ body: completeRequestBody(ticket.upload_id, mode, declaration)
7159
+ });
7160
+ }
7161
+ async function announce({ route, token, zip, mode, declaration, retry }, deps) {
7162
+ let answer;
7163
+ try {
7164
+ answer = await deps.request(route, {
7165
+ token,
7166
+ retry,
7167
+ timeoutMs: ANNOUNCE_TIMEOUT_MS,
7168
+ body: announceRequestBody(zip.byteLength, (0, import_node_crypto2.createHash)("sha256").update(zip).digest("hex"), mode)
7169
+ });
7170
+ } catch (err2) {
7171
+ if (err2 instanceof ApiError && (err2.code === "direct_upload_unavailable" || err2.code === "missing_zip")) {
7172
+ const [size, allowed] = sizeOverLimit(zip.byteLength, maxInlineZipBytes(mode, declaration));
7173
+ throw new ApiError(
7174
+ err2.status,
7175
+ "direct_upload_unavailable",
7176
+ `The upload is ${size} compressed, and the platform does not take uploads of that size at the moment \u2014 only up to ${allowed}. Nothing was uploaded. Try again later.`
7177
+ );
7178
+ }
7179
+ throw err2;
7180
+ }
7181
+ const headers = answer?.required_headers;
7182
+ const understood = typeof answer?.upload_id === "string" && isHttpUrl(answer?.upload_url) && headers !== null && typeof headers === "object" && !Array.isArray(headers) && Object.values(headers).every((value) => typeof value === "string");
7183
+ if (!understood) {
7184
+ throw new ApiError(
7185
+ 0,
7186
+ "invalid_upload_answer",
7187
+ "The platform answered the upload request in a form this version of the CLI does not understand. Nothing was uploaded."
7188
+ );
7189
+ }
7190
+ return { upload_id: answer.upload_id, upload_url: answer.upload_url, required_headers: headers };
7191
+ }
7192
+ function isHttpUrl(value) {
7193
+ if (typeof value !== "string") return false;
7194
+ try {
7195
+ return ["https:", "http:"].includes(new URL(value).protocol);
7196
+ } catch {
7197
+ return false;
7198
+ }
7199
+ }
7200
+ async function putZip(ticket, zip, deps) {
7201
+ const deadline = Date.now() + PUT_TIMEOUT_MS;
7202
+ for (let attempt = 1; ; attempt++) {
7203
+ let failure;
7204
+ let again = attempt < PUT_ATTEMPTS;
7205
+ try {
7206
+ const resp = await deps.fetch(ticket.upload_url, {
7207
+ method: "PUT",
7208
+ headers: ticket.required_headers,
7209
+ body: zip,
7210
+ signal: AbortSignal.timeout(Math.max(1, deadline - Date.now()))
7211
+ });
7212
+ if (resp.ok) return;
7213
+ const code = /<Code>([\w.]{1,80})<\/Code>/.exec(await resp.text())?.[1];
7214
+ failure = `the storage answered HTTP ${resp.status}${code ? ` (${code})` : ""}`;
7215
+ again &&= resp.status >= 500;
7216
+ } catch (err2) {
7217
+ const timedOut = err2?.name === "TimeoutError";
7218
+ const code = typeof err2?.cause?.code === "string" && /^\w{1,40}$/.test(err2.cause.code) ? ` (${err2.cause.code})` : "";
7219
+ failure = timedOut ? `it did not finish within ${PUT_TIMEOUT_MS / 6e4} minutes` : `the connection to the storage broke${code}`;
7220
+ again &&= !timedOut;
7221
+ }
7222
+ if (!again) {
7223
+ throw new ApiError(
7224
+ 0,
7225
+ "upload_failed",
7226
+ `The upload did not go through: ${failure}. Nothing of the project was changed \u2014 run the push again.`
7227
+ );
7228
+ }
7229
+ }
7230
+ }
7231
+
7061
7232
  // src/lib/pushReport.ts
7062
7233
  init_branding();
7063
7234
  var EXCLUDE_REASON = {
@@ -7599,7 +7770,7 @@ async function pushCommand(opts) {
7599
7770
  ui.step(mode === "source" ? "Packing source tree\u2026" : `Packing built output (${rootDir})\u2026`);
7600
7771
  const declaration = mode === "source" ? readBackendDeclaration() : null;
7601
7772
  const files = collectFiles(rootDir, mode);
7602
- const zip = zipToBase64(files, maxZipBytesForUpload(mode, declaration));
7773
+ const zip = zipFiles(files, maxZipBytesForUpload());
7603
7774
  ui.info(`${zip.count} files, ${(zip.bytes / 1024).toFixed(0)} KiB compressed`);
7604
7775
  if (declaration) {
7605
7776
  const consent = await ensureDatabaseConsent({
@@ -7617,15 +7788,14 @@ async function pushCommand(opts) {
7617
7788
  }
7618
7789
  }
7619
7790
  ui.step("Uploading to your workspace\u2026");
7620
- const upload = await request(
7621
- `${API_URL}/api/workspaces/${userId}/${projectId}/source-zip`,
7622
- {
7623
- token: auth.pb_token,
7624
- body: uploadRequestBody(zip.base64, mode, declaration),
7625
- timeoutMs: 12e4,
7626
- retry: platformRetry
7627
- }
7628
- );
7791
+ const upload = await uploadZip({
7792
+ route: `${API_URL}/api/workspaces/${userId}/${projectId}/source-zip`,
7793
+ token: auth.pb_token,
7794
+ zip: zip.data,
7795
+ mode,
7796
+ declaration,
7797
+ retry: platformRetry
7798
+ });
7629
7799
  ui.info(`${upload.files_written} files written`);
7630
7800
  if (mode === "source") {
7631
7801
  ui.step("Building in the cloud (this can take a few minutes)\u2026");
@@ -7840,7 +8010,7 @@ init_branding();
7840
8010
 
7841
8011
  // src/lib/engine.ts
7842
8012
  var import_node_fs11 = require("fs");
7843
- var import_node_crypto2 = require("crypto");
8013
+ var import_node_crypto3 = require("crypto");
7844
8014
  var import_node_path13 = require("path");
7845
8015
  var import_node_os4 = require("os");
7846
8016
  var import_node_child_process3 = require("child_process");
@@ -7897,7 +8067,7 @@ async function fetchManifest() {
7897
8067
  }
7898
8068
  }
7899
8069
  async function sha256File(path) {
7900
- const hash = (0, import_node_crypto2.createHash)("sha256");
8070
+ const hash = (0, import_node_crypto3.createHash)("sha256");
7901
8071
  await (0, import_promises3.pipeline)((0, import_node_fs11.createReadStream)(path), hash);
7902
8072
  return hash.digest("hex");
7903
8073
  }
@@ -8214,7 +8384,7 @@ site can never be uploaded in several parts.
8214
8384
  source), and Managed Server Actions and \`.agentful/backend.json\` are NOT
8215
8385
  deployed by it.
8216
8386
 
8217
- Limits of one push, either way: about ${formatBytes(maxZipBytesForUpload(), "down")} compressed, ${formatBytes(MAX_UNPACKED_BYTES)} of files, ${MAX_UPLOAD_FILES.toLocaleString("en-US")} files.
8387
+ Limits of one push, either way: ${formatBytes(MAX_UNPACKED_BYTES)} of files (their sizes added up), ${formatBytes(maxZipBytesForUpload(), "down")} compressed, ${MAX_UPLOAD_FILES.toLocaleString("en-US")} files.
8218
8388
  \`--force\` lifts none of them: it only lets the cloud build try despite a
8219
8389
  compatibility finding.
8220
8390
 
@@ -8225,8 +8395,9 @@ written. Sizes, limits, what is left out and what blocks the upload come from
8225
8395
  that report and from nowhere else: quote its figures, never estimate or
8226
8396
  recompute them. Next to every byte count the report prints the size as text
8227
8397
  (\`size\` for \`bytes\`, \`zip_size\` for \`zip_bytes\`): use that text, do not
8228
- convert bytes yourself. \`limits.max_zip_bytes\` is the exact limit for this
8229
- push. Run it before the first push of a session, after a push was stopped,
8398
+ convert bytes yourself. \`limits.max_unpacked_bytes\` is the limit a site
8399
+ runs into: the sizes of its files added up. Run it before the first push of a
8400
+ session, after a push was stopped,
8230
8401
  and after you changed what gets uploaded. In between, \`agentful push\` applies the same
8231
8402
  check itself and stops before sign-in when something blocks. A blocker with
8232
8403
  \`"overridable": false\` is final, the platform rejects that upload: do not
@@ -8249,11 +8420,11 @@ answer, such as whether a \`.gitignore\` is in effect
8249
8420
  up, its relative links would break. If that folder is the whole site, push
8250
8421
  the folder (\`suggestions\` carries the command); otherwise adjust the links
8251
8422
  and show the diff. Without candidates, ask which folder holds the site.
8252
- - **Too large or too many files** (\`zip_too_large\`, \`unpacked_too_large\`,
8253
- \`too_many_files\`): settle first whether the site can fit at all. The
8254
- \`zip_too_large\` blocker names the files that are each larger than a whole
8255
- push on their own (all of them in its \`paths\`), and its message says what
8256
- is left without them. Such a file can only stay out. If
8423
+ - **Too large or too many files** (\`unpacked_too_large\`, \`zip_too_large\`,
8424
+ \`too_many_files\`): settle first whether the site can fit at all. A size
8425
+ blocker names the files that are each larger than a whole push on their own
8426
+ (all of them in its \`paths\`), and its message says what is left without
8427
+ them. Such a file can only stay out. If
8257
8428
  the site needs one of them (the page the user wants as the start page, or a
8258
8429
  file a page loads: search the sources for references), the site cannot be
8259
8430
  pushed yet: say that plainly, with the file and its size. Do not offer
@@ -8270,6 +8441,12 @@ answer, such as whether a \`.gitignore\` is in effect
8270
8441
  than a source push only where a build produces the output folder; a site
8271
8442
  without a build step is the same upload either way. Never shrink,
8272
8443
  re-encode, move or delete the user's files on your own.
8444
+ - **Files the preview does not show** (\`preview_file_too_large\`,
8445
+ \`preview_type_not_delivered\` in \`warnings\`): the push goes through, but
8446
+ the preview does not deliver these files, so a page that needs one of them
8447
+ looks broken there. Say which files and why, with the wording of the
8448
+ warning, and ask whether to push anyway. Never shrink, convert or split
8449
+ them on your own.
8273
8450
  - **Compatibility findings** (\`"overridable": true\`): fix the cause in the
8274
8451
  code. \`--force\` only when the user asks to try the cloud build anyway.
8275
8452
  - **Files that would be missing on the site:** the warnings
@@ -8463,7 +8640,7 @@ ${backendSessionLines(backend)}
8463
8640
  }
8464
8641
 
8465
8642
  // src/branding/commands.ts
8466
- var PUSH_TOOL_TIMEOUT_MS = 9e5;
8643
+ var PUSH_TOOL_TIMEOUT_MS = 156e4;
8467
8644
  var NEEDS_A_PUSH = 'If the command reports "No Agentful project here", nothing has been deployed from this directory yet \u2014 run `agentful push` first (it creates the project and a preview), then retry.';
8468
8645
  var SLASH_COMMANDS = {
8469
8646
  setup: {
@@ -8482,7 +8659,7 @@ var SLASH_COMMANDS = {
8482
8659
  template: `Deploy this project, following "Pushing to the platform" in AGENTFUL_CLOUD.md. Run every command from the project root using the bash tool. What I typed after the command (may be empty): "$ARGUMENTS". If it names \`--prebuilt\`, a folder for \`--dir\`, or \`--force\`, pass that on to both commands below.
8483
8660
 
8484
8661
  1. Run \`${PUSH_ANALYSIS_COMMAND}\`. It only reads files: no sign-in, no upload. Its JSON report is the only source for sizes, limits and what blocks the upload: quote its figures as printed (the \`size\` texts next to the byte counts), never estimate or convert them.
8485
- 2. If the report or the project calls for a decision (a blocker, a file the site needs that would not be uploaded, a build step the cloud does not run), explain the finding in plain words with the figures of the report. If the report shows that what I want to publish cannot fit (a page or file it needs is larger than a whole push, or it stays above the limits without the files it does not load), say that plainly and do not offer ways out that do not get it under the limits. Otherwise ask me how to proceed: exactly one question per reply, and never one the report or the project files answer. Then wait for my answer. Until then do not change any file, do not run project scripts and do not push.
8662
+ 2. If the report or the project calls for a decision (a blocker, a file the site needs that would not be uploaded or that the preview would not show, a build step the cloud does not run), explain the finding in plain words with the figures of the report. If the report shows that what I want to publish cannot fit (a page or file it needs is larger than a whole push, or it stays above the limits without the files it does not load), say that plainly and do not offer ways out that do not get it under the limits. Otherwise ask me how to proceed: exactly one question per reply, and never one the report or the project files answer. Then wait for my answer. Until then do not change any file, do not run project scripts and do not push.
8486
8663
  3. After a change I approved, run the analysis again.
8487
8664
  4. When nothing blocks, run \`agentful push\` with the bash tool's \`timeout\` set to ${PUSH_TOOL_TIMEOUT_MS} (milliseconds): the cloud build takes minutes and the default timeout would cut the push off halfway. The command never waits for input. It creates the project automatically if this directory is not linked yet.
8488
8665
  5. Report the preview URL on its own line and what the push printed about the backend. If it fails: quote the printed error verbatim, and if the failure happened in the cloud (not before the upload), also run \`agentful build-status\` and quote its phase/error/details as the cause. Diagnose ONLY from those records \u2014 never invent causes, env vars, or settings they do not show.
@@ -9315,7 +9492,7 @@ function describeModelRoute(model, catalog, localProviders, connectedProviders)
9315
9492
  }
9316
9493
 
9317
9494
  // src/lib/engineEvents.ts
9318
- var import_node_crypto3 = require("crypto");
9495
+ var import_node_crypto4 = require("crypto");
9319
9496
  var import_node_net = require("net");
9320
9497
  function payloadOf(event) {
9321
9498
  return event.payload && typeof event.payload === "object" ? event.payload : event;
@@ -9441,7 +9618,7 @@ var EngineLivenessPolicy = class {
9441
9618
  function engineServerCredentials(env = process.env) {
9442
9619
  return {
9443
9620
  username: env.OPENCODE_SERVER_USERNAME || void 0,
9444
- password: env.OPENCODE_SERVER_PASSWORD || (0, import_node_crypto3.randomUUID)()
9621
+ password: env.OPENCODE_SERVER_PASSWORD || (0, import_node_crypto4.randomUUID)()
9445
9622
  };
9446
9623
  }
9447
9624
  async function reserveLoopbackPort() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentful",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "description": "Agentful in your terminal — local development with push-to-cloud previews",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://agentful.dev",