agentful 0.4.15 → 0.4.16

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/README.md CHANGED
@@ -51,11 +51,14 @@ in or creates a project.
51
51
 
52
52
  `agentful push --dry-run` shows that check on its own: what would be uploaded
53
53
  and what is left out, the largest files, identical copies, media embedded in
54
- HTML, and every reason the upload would be rejected. It uploads nothing,
55
- creates no project and changes no file; it exits with 1 when the push would be
56
- blocked. `--dry-run --json` prints the same analysis as one JSON document
54
+ HTML, and every reason the upload would be rejected. A file that is larger
55
+ than a whole push on its own is named as such: it cannot be uploaded at all,
56
+ whatever else is left out. It uploads nothing, creates no project and changes
57
+ no file; it exits with 1 when the push would be blocked. `--dry-run --json`
58
+ prints the same analysis as one JSON document
57
59
  (`"schema": "agentful.push-analysis"`) for scripts and agents; with `--summary`
58
- it leaves out the list of every file and keeps totals, limits and findings.
60
+ it leaves out the list of every file, keeps totals, limits and findings, and
61
+ prints every size as text next to its byte count.
59
62
 
60
63
  ## What you can deploy
61
64
 
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.15" ? "0.4.15" : null.version;
3047
+ VERSION = "0.4.16" ? "0.4.16" : null.version;
3048
3048
  brand = {
3049
3049
  name: "agentful",
3050
3050
  // the command users type
@@ -6561,7 +6561,7 @@ function formatBytes(bytes2, rounding = "nearest") {
6561
6561
  }
6562
6562
  if (unit === 0 || Number.isInteger(value)) return `${value} ${units[unit]}`;
6563
6563
  const tenths = value * 10;
6564
- const rounded = (rounding === "down" ? Math.floor(tenths) : Math.round(tenths)) / 10;
6564
+ const rounded = (rounding === "down" ? Math.floor(tenths) : rounding === "up" ? Math.ceil(tenths) : Math.round(tenths)) / 10;
6565
6565
  return `${rounded.toFixed(1)} ${units[unit]}`;
6566
6566
  }
6567
6567
  function sizeOverLimit(bytes2, limit) {
@@ -6728,6 +6728,10 @@ function analyzePush(options) {
6728
6728
  }
6729
6729
  duplicates.sort((a, b) => b.redundant_zip_bytes - a.redundant_zip_bytes || (a.paths[0] < b.paths[0] ? -1 : 1));
6730
6730
  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));
6733
+ const oversizedPaths = new Set(oversized.map((file) => file.path));
6734
+ const restZipLower = zipLower - oversized.reduce((total, file) => total + file.zipLower, 0);
6731
6735
  const preflight = mode === "source" ? runPreflight(rootDir) : null;
6732
6736
  const framework = preflight?.framework ?? null;
6733
6737
  const hasManifest = storedNames.has("package.json");
@@ -6739,12 +6743,14 @@ function analyzePush(options) {
6739
6743
  return {
6740
6744
  path: file.path,
6741
6745
  bytes: file.bytes,
6746
+ zip_bytes: file.zip_bytes,
6742
6747
  title: file.title,
6743
6748
  sha256: file.sha256,
6744
- same_content_as: first && first !== file.path ? first : null
6749
+ same_content_as: first && first !== file.path ? first : null,
6750
+ ...file.zipLower > maxZipBytes ? { larger_than_a_push: true } : {}
6745
6751
  };
6746
6752
  });
6747
- const maxZipBytes = maxZipBytesForUpload(mode, mode === "source" ? readBackendDeclaration(rootDir) : null);
6753
+ const fittingCandidates = candidates.filter((candidate) => !candidate.larger_than_a_push);
6748
6754
  const zipLimit = formatBytes(maxZipBytes, "down");
6749
6755
  const blockers = [];
6750
6756
  const warnings = [];
@@ -6782,10 +6788,18 @@ function analyzePush(options) {
6782
6788
  }
6783
6789
  if (zipLower > maxZipBytes) {
6784
6790
  const [size, allowed] = sizeOverLimit(zipExact ? zipBytes : zipLower, maxZipBytes);
6791
+ let message = `The upload is ${zipExact ? "" : "at least "}${size} compressed \u2014 a push carries at most ${allowed} compressed.`;
6792
+ 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`}.`;
6797
+ }
6785
6798
  blockers.push({
6786
6799
  code: "zip_too_large",
6787
6800
  overridable: false,
6788
- message: `The upload is ${zipExact ? "" : "at least "}${size} compressed \u2014 a push carries at most ${allowed} compressed.`
6801
+ message,
6802
+ ...oversized.length > 0 ? { paths: oversized.map((file) => file.path).slice(0, FINDING_PATHS) } : {}
6789
6803
  });
6790
6804
  } else if (!zipExact && zipBytes > maxZipBytes) {
6791
6805
  warnings.push({
@@ -6794,10 +6808,12 @@ function analyzePush(options) {
6794
6808
  });
6795
6809
  }
6796
6810
  if (entryMissing) {
6811
+ const tooBig = candidates.filter((candidate) => candidate.larger_than_a_push);
6812
+ const cannotBecomeIt = tooBig.length === 0 ? "" : fittingCandidates.length === 0 ? candidates.length === 1 ? " The one HTML file here cannot become it as it is: it is larger than a whole push." : ` None of the ${candidates.length} HTML files here can become it as they are: each is larger than a whole push.` : ` ${tooBig.length} of the ${candidates.length} HTML files here cannot become it as ${tooBig.length === 1 ? "it is" : "they are"}, larger than a whole push: ${samplePaths(tooBig.map((candidate) => candidate.path))}.`;
6797
6813
  blockers.push({
6798
6814
  code: "entry_missing",
6799
6815
  overridable: false,
6800
- message: mode === "dist" ? `No index.html at the top level of ${dirLabel === "." ? "the uploaded folder" : `${dirLabel}/`}. A prebuilt upload is served as it is, and index.html is its start page.` : "No index.html at the top level. Without a package.json this is a static site, and the platform serves index.html as its start page."
6816
+ message: (mode === "dist" ? `No index.html at the top level of ${dirLabel === "." ? "the uploaded folder" : `${dirLabel}/`}. A prebuilt upload is served as it is, and index.html is its start page.` : "No index.html at the top level. Without a package.json this is a static site, and the platform serves index.html as its start page.") + cannotBecomeIt
6801
6817
  });
6802
6818
  }
6803
6819
  const backend = mode === "source" ? checkBackendDeclaration(rootDir) : null;
@@ -6838,11 +6854,11 @@ function analyzePush(options) {
6838
6854
  }
6839
6855
  const tooLarge = blockers.some((b) => b.code === "zip_too_large" || b.code === "unpacked_too_large");
6840
6856
  if (entryMissing) {
6841
- if (candidates.length > 0) {
6857
+ if (fittingCandidates.length > 0) {
6842
6858
  suggestions.push({
6843
6859
  code: "name_start_page",
6844
6860
  message: "Rename or copy the page that should open first to index.html at the top level, check that its relative links still resolve, then push again.",
6845
- paths: candidates.map((candidate) => candidate.path).slice(0, FINDING_PATHS)
6861
+ paths: fittingCandidates.map((candidate) => candidate.path).slice(0, FINDING_PATHS)
6846
6862
  });
6847
6863
  }
6848
6864
  const siteDirs = /* @__PURE__ */ new Set();
@@ -6864,29 +6880,31 @@ function analyzePush(options) {
6864
6880
  }
6865
6881
  }
6866
6882
  if (tooLarge) {
6867
- const oversized = measured.filter((file) => file.zipLower > maxZipBytes).sort((a, b) => b.bytes - a.bytes || byPath(a, b));
6868
- if (oversized.length > 0) {
6869
- suggestions.push({
6870
- code: "oversized_files",
6871
- message: `${samplePaths(oversized.map((file) => `${file.path} (${formatBytes(file.zipLower)} compressed)`))} ${oversized.length === 1 ? "is" : "are each"} larger than a whole push can carry. The upload cannot fit while ${oversized.length === 1 ? "it is" : "they are"} part of it.`,
6872
- paths: oversized.map((file) => file.path).slice(0, FINDING_PATHS)
6873
- });
6874
- }
6875
- if (duplicates.length > 0 && redundantZipBytes >= zipBytes / 20) {
6876
- const copies = duplicates.reduce((total, group) => total + group.paths.length - 1, 0);
6877
- const remaining = zipBytes - redundantZipBytes;
6883
+ const overZip = zipLower - maxZipBytes;
6884
+ const overFiles = storedBytes - MAX_UNPACKED_BYTES;
6885
+ const mustStayOut = [
6886
+ overZip > 0 ? `${formatBytes(overZip, "up")} of the ${formatBytes(zipLower)} compressed` : "",
6887
+ overFiles > 0 ? `${formatBytes(overFiles, "up")} of the ${formatBytes(storedBytes)} of files` : ""
6888
+ ].filter(Boolean);
6889
+ const toFit = ` To fit, at least ${mustStayOut.join(" and ")} ${mustStayOut.length > 1 ? "have" : "has"} to stay out.`;
6890
+ 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) {
6894
+ const copies = spareCopies.reduce((total, group) => total + group.paths.length - 1, 0);
6895
+ const remaining = base - spareZipBytes;
6878
6896
  suggestions.push({
6879
6897
  code: "remove_identical_copies",
6880
- message: `${plural(copies, "file is an identical copy", "files are identical copies")} of another file and ${copies === 1 ? "adds" : "add"} ${formatBytes(redundantZipBytes)} to the compressed upload. If nothing links to the extra names, one copy each brings it to ${formatBytes(remaining)}${remaining > maxZipBytes ? ` \u2014 still above the ${zipLimit} limit` : ""}.`,
6881
- paths: duplicates.flatMap((group) => group.paths.slice(1)).slice(0, FINDING_PATHS)
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` : ""}.`,
6899
+ paths: spareCopies.flatMap((group) => group.paths.slice(1)).slice(0, FINDING_PATHS)
6882
6900
  });
6883
6901
  }
6884
6902
  suggestions.push(mode === "source" ? {
6885
6903
  code: "exclude_with_gitignore",
6886
- message: "Files and folders listed in .gitignore are not uploaded. Leave out what the site does not serve \u2014 originals, working files, exports."
6904
+ message: `Files and folders listed in .gitignore are not uploaded. Leave out what the site does not serve \u2014 originals, working files, exports.${toFit}`
6887
6905
  } : {
6888
6906
  code: "narrow_prebuilt_directory",
6889
- message: "A prebuilt upload takes every file in the folder. Put the finished site into a folder of its own and pass it with --dir."
6907
+ message: `A prebuilt upload takes every file in the folder. Put the finished site into a folder of its own and pass it with --dir.${toFit}`
6890
6908
  });
6891
6909
  suggestions.push({
6892
6910
  code: "upload_limit",
@@ -6961,7 +6979,19 @@ var SUMMARY_LISTS = {
6961
6979
  finding_paths: 8
6962
6980
  };
6963
6981
  var SUMMARY_MAX_BYTES = 24 * 1024;
6964
- var printAnalysis = (analysis) => `${JSON.stringify(analysis, null, 2)}
6982
+ var sizeLabelKey = (key) => key === "base64_chars" ? "base64_size" : /(^|_)bytes(_|$)/.test(key) ? key.replace("bytes", "size") : null;
6983
+ function withSizeLabels(value) {
6984
+ if (Array.isArray(value)) return value.map(withSizeLabels);
6985
+ if (!value || typeof value !== "object") return value;
6986
+ const labelled = {};
6987
+ for (const [key, inner] of Object.entries(value)) {
6988
+ labelled[key] = withSizeLabels(inner);
6989
+ const label = typeof inner === "number" ? sizeLabelKey(key) : null;
6990
+ if (label) labelled[label] = formatBytes(inner, key.startsWith("max_") ? "down" : "nearest");
6991
+ }
6992
+ return labelled;
6993
+ }
6994
+ var printAnalysis = (analysis) => `${JSON.stringify(analysis.view === "summary" ? withSizeLabels(analysis) : analysis, null, 2)}
6965
6995
  `;
6966
6996
  function summarize(analysis, divisor) {
6967
6997
  const shortened = {};
@@ -7088,7 +7118,7 @@ function renderAnalysis(analysis, options = {}) {
7088
7118
  lines.push(" HTML files found:");
7089
7119
  for (const candidate of analysis.entry.candidates.slice(0, SHOWN_FILES)) {
7090
7120
  lines.push(
7091
- ` ${sizeColumn(candidate.bytes)} ${candidate.path}` + (candidate.title ? paint.dim(` "${candidate.title}"`) : "") + (candidate.same_content_as ? paint.dim(` (same content as ${candidate.same_content_as})`) : "")
7121
+ ` ${sizeColumn(candidate.bytes)} ${candidate.path}` + (candidate.title ? paint.dim(` "${candidate.title}"`) : "") + (candidate.same_content_as ? paint.dim(` (same content as ${candidate.same_content_as})`) : "") + (candidate.larger_than_a_push ? paint.dim(" (larger than a whole push)") : "")
7092
7122
  );
7093
7123
  }
7094
7124
  lines.push(...more(analysis.entry.candidates.length, SHOWN_FILES, "HTML file"));
@@ -8193,36 +8223,53 @@ compatibility finding.
8193
8223
  reports what a push would upload: no sign-in, no project, no upload, nothing
8194
8224
  written. Sizes, limits, what is left out and what blocks the upload come from
8195
8225
  that report and from nowhere else: quote its figures, never estimate or
8196
- recompute them (\`limits.max_zip_bytes\` is the exact limit for this push).
8197
- Run it before the first push of a session, after a push was stopped, and after
8198
- you changed what gets uploaded. In between, \`agentful push\` applies the same
8226
+ recompute them. Next to every byte count the report prints the size as text
8227
+ (\`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,
8230
+ and after you changed what gets uploaded. In between, \`agentful push\` applies the same
8199
8231
  check itself and stops before sign-in when something blocks. A blocker with
8200
8232
  \`"overridable": false\` is final, the platform rejects that upload: do not
8201
8233
  retry, do not add \`--force\`, do not look for another route.
8202
8234
 
8203
8235
  Every change to the user's files for a push (including \`.gitignore\`) and every
8204
8236
  project script you run for it needs the user's OK first: name the concrete
8205
- change, ask, wait for the answer.
8237
+ change, ask, wait for the answer. One question per reply: one decision, not
8238
+ several joined together. Never ask what the report or the project files
8239
+ answer, such as whether a \`.gitignore\` is in effect
8240
+ (\`excluded.gitignore_applied\`), which files are large or which are copies.
8206
8241
 
8207
8242
  - **No start page** (\`entry_missing\`): the platform serves \`index.html\` from
8208
8243
  the top level. With \`entry.candidates\`: show them (path and title;
8209
- \`same_content_as\` marks copies) and ask which page opens first. A candidate
8210
- at the top level can be renamed to \`index.html\`, or copied, which keeps the
8211
- old address and adds the file's size to the upload: ask which. A candidate
8212
- in a subfolder must not be copied up, its relative links would break. If
8213
- that folder is the whole site, push the folder (\`suggestions\` carries the
8214
- command); otherwise adjust the links and show the diff. Without candidates,
8215
- ask which folder holds the site.
8244
+ \`same_content_as\` marks copies, \`larger_than_a_push\` marks pages no push
8245
+ can carry: they cannot be the start page as they are, say so) and ask which
8246
+ of the others opens first. A candidate at the top level can be renamed to
8247
+ \`index.html\`, or copied, which keeps the old address and adds the file's
8248
+ size to the upload: ask which. A candidate in a subfolder must not be copied
8249
+ up, its relative links would break. If that folder is the whole site, push
8250
+ the folder (\`suggestions\` carries the command); otherwise adjust the links
8251
+ and show the diff. Without candidates, ask which folder holds the site.
8216
8252
  - **Too large or too many files** (\`zip_too_large\`, \`unpacked_too_large\`,
8217
- \`too_many_files\`): explain where the size is, from \`largest_files\`,
8218
- \`by_top_level\`, \`duplicates\` and \`embedded_media\` (media stored inside text
8219
- files as \`data:\` URLs). The ways out that exist: leave out files the site
8220
- does not load (\`.gitignore\`, source push), or push only the finished output
8221
- folder (prebuilt). Before proposing to leave a file out, search the sources
8222
- for references to it and say what you checked: identical content does not
8223
- make a copy unused. Never shrink, re-encode, move or delete the user's files
8224
- on your own. If what the site needs stays above the limits, say plainly that
8225
- it cannot be pushed yet. Larger uploads are not available.
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
8257
+ the site needs one of them (the page the user wants as the start page, or a
8258
+ file a page loads: search the sources for references), the site cannot be
8259
+ pushed yet: say that plainly, with the file and its size. Do not offer
8260
+ \`.gitignore\` or a prebuilt push as a way around it, neither lifts a limit,
8261
+ and larger uploads are not available.
8262
+ Where no such file is needed: explain where the size is, from
8263
+ \`largest_files\`, \`by_top_level\`, \`duplicates\` and \`embedded_media\` (media
8264
+ stored inside text files as \`data:\` URLs), and propose which files the site
8265
+ does not load to leave out with \`.gitignore\`. The \`exclude_with_gitignore\`
8266
+ suggestion says how much has to stay out; if the site needs more than fits,
8267
+ say plainly that it cannot be pushed yet. Before proposing to leave a file
8268
+ out, search the sources for references to it and say what you checked:
8269
+ identical content does not make a copy unused. A prebuilt push is smaller
8270
+ than a source push only where a build produces the output folder; a site
8271
+ without a build step is the same upload either way. Never shrink,
8272
+ re-encode, move or delete the user's files on your own.
8226
8273
  - **Compatibility findings** (\`"overridable": true\`): fix the cause in the
8227
8274
  code. \`--force\` only when the user asks to try the cloud build anyway.
8228
8275
  - **Files that would be missing on the site:** the warnings
@@ -8434,8 +8481,8 @@ var SLASH_COMMANDS = {
8434
8481
  description: `${brand.displayName}: check, deploy and get a preview URL`,
8435
8482
  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.
8436
8483
 
8437
- 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, never estimate them.
8438
- 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 and ask me how to proceed. One question at a time, then wait for my answer. Until then do not change any file, do not run project scripts and do not push.
8484
+ 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.
8439
8486
  3. After a change I approved, run the analysis again.
8440
8487
  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.
8441
8488
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentful",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
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",