agentful 0.4.14 → 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.
Files changed (3) hide show
  1. package/README.md +12 -5
  2. package/dist/index.cjs +333 -100
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,7 +26,7 @@ agentful open # open the preview
26
26
  | `login` / `logout` / `whoami` | Browser-based sign-in; the token is kept in the macOS Keychain (on Linux: a `0600` file under `~/.config/agentful/`) |
27
27
  | `init [--link <id>] [--title <t>]` | Create a cloud project or link an existing one (`.agentful/project.json`) |
28
28
  | `push [--prebuilt [--dir <path>]]` | Upload the source and build in the cloud; `--prebuilt` uploads a local build |
29
- | `push --dry-run [--json]` | Analyse what a push would upload and what would block it — uploads nothing |
29
+ | `push --dry-run [--json [--summary]]` | Analyse what a push would upload and what would block it — uploads nothing |
30
30
  | `open` | Open the live preview |
31
31
  | `share [--private]` | Make the preview public, or private again |
32
32
  | `publish <subdomain>` | Publish at `https://<subdomain>.agentful.page` |
@@ -39,6 +39,9 @@ agentful open # open the preview
39
39
 
40
40
  Inside the interface, `/` lists the Agentful commands (`/setup`, `/push`,
41
41
  `/preview`, `/publish`, `/share`, `/status`) alongside the built-in ones.
42
+ `/push` checks before it uploads: the agent reads the same analysis as
43
+ `push --dry-run`, explains what would block the upload or go missing on the
44
+ site, and asks you before it changes a file or pushes.
42
45
 
43
46
  `push` respects your `.gitignore` and always excludes `.git`, `node_modules`,
44
47
  build output and every `.env` file. A push carries at most about 4.4 MiB
@@ -48,10 +51,14 @@ in or creates a project.
48
51
 
49
52
  `agentful push --dry-run` shows that check on its own: what would be uploaded
50
53
  and what is left out, the largest files, identical copies, media embedded in
51
- HTML, and every reason the upload would be rejected. It uploads nothing,
52
- creates no project and changes no file; it exits with 1 when the push would be
53
- blocked. `--dry-run --json` prints the same analysis as one JSON document
54
- (`"schema": "agentful.push-analysis"`) for scripts and agents.
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
59
+ (`"schema": "agentful.push-analysis"`) for scripts and agents; with `--summary`
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.
55
62
 
56
63
  ## What you can deploy
57
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.14" ? "0.4.14" : 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) {
@@ -6606,6 +6606,7 @@ function failedAnalysis(mode, projectDir, blocker) {
6606
6606
  schema: ANALYSIS_SCHEMA,
6607
6607
  schema_version: ANALYSIS_SCHEMA_VERSION,
6608
6608
  cli_version: VERSION,
6609
+ view: "full",
6609
6610
  mode,
6610
6611
  root: null,
6611
6612
  ok: false,
@@ -6727,6 +6728,10 @@ function analyzePush(options) {
6727
6728
  }
6728
6729
  duplicates.sort((a, b) => b.redundant_zip_bytes - a.redundant_zip_bytes || (a.paths[0] < b.paths[0] ? -1 : 1));
6729
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);
6730
6735
  const preflight = mode === "source" ? runPreflight(rootDir) : null;
6731
6736
  const framework = preflight?.framework ?? null;
6732
6737
  const hasManifest = storedNames.has("package.json");
@@ -6738,12 +6743,14 @@ function analyzePush(options) {
6738
6743
  return {
6739
6744
  path: file.path,
6740
6745
  bytes: file.bytes,
6746
+ zip_bytes: file.zip_bytes,
6741
6747
  title: file.title,
6742
6748
  sha256: file.sha256,
6743
- 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 } : {}
6744
6751
  };
6745
6752
  });
6746
- const maxZipBytes = maxZipBytesForUpload(mode, mode === "source" ? readBackendDeclaration(rootDir) : null);
6753
+ const fittingCandidates = candidates.filter((candidate) => !candidate.larger_than_a_push);
6747
6754
  const zipLimit = formatBytes(maxZipBytes, "down");
6748
6755
  const blockers = [];
6749
6756
  const warnings = [];
@@ -6781,10 +6788,18 @@ function analyzePush(options) {
6781
6788
  }
6782
6789
  if (zipLower > maxZipBytes) {
6783
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
+ }
6784
6798
  blockers.push({
6785
6799
  code: "zip_too_large",
6786
6800
  overridable: false,
6787
- 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) } : {}
6788
6803
  });
6789
6804
  } else if (!zipExact && zipBytes > maxZipBytes) {
6790
6805
  warnings.push({
@@ -6793,10 +6808,12 @@ function analyzePush(options) {
6793
6808
  });
6794
6809
  }
6795
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))}.`;
6796
6813
  blockers.push({
6797
6814
  code: "entry_missing",
6798
6815
  overridable: false,
6799
- 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
6800
6817
  });
6801
6818
  }
6802
6819
  const backend = mode === "source" ? checkBackendDeclaration(rootDir) : null;
@@ -6837,11 +6854,11 @@ function analyzePush(options) {
6837
6854
  }
6838
6855
  const tooLarge = blockers.some((b) => b.code === "zip_too_large" || b.code === "unpacked_too_large");
6839
6856
  if (entryMissing) {
6840
- if (candidates.length > 0) {
6857
+ if (fittingCandidates.length > 0) {
6841
6858
  suggestions.push({
6842
6859
  code: "name_start_page",
6843
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.",
6844
- paths: candidates.map((candidate) => candidate.path).slice(0, FINDING_PATHS)
6861
+ paths: fittingCandidates.map((candidate) => candidate.path).slice(0, FINDING_PATHS)
6845
6862
  });
6846
6863
  }
6847
6864
  const siteDirs = /* @__PURE__ */ new Set();
@@ -6863,29 +6880,31 @@ function analyzePush(options) {
6863
6880
  }
6864
6881
  }
6865
6882
  if (tooLarge) {
6866
- const oversized = measured.filter((file) => file.zipLower > maxZipBytes).sort((a, b) => b.bytes - a.bytes || byPath(a, b));
6867
- if (oversized.length > 0) {
6868
- suggestions.push({
6869
- code: "oversized_files",
6870
- 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.`,
6871
- paths: oversized.map((file) => file.path).slice(0, FINDING_PATHS)
6872
- });
6873
- }
6874
- if (duplicates.length > 0 && redundantZipBytes >= zipBytes / 20) {
6875
- const copies = duplicates.reduce((total, group) => total + group.paths.length - 1, 0);
6876
- 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;
6877
6896
  suggestions.push({
6878
6897
  code: "remove_identical_copies",
6879
- 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` : ""}.`,
6880
- 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)
6881
6900
  });
6882
6901
  }
6883
6902
  suggestions.push(mode === "source" ? {
6884
6903
  code: "exclude_with_gitignore",
6885
- 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}`
6886
6905
  } : {
6887
6906
  code: "narrow_prebuilt_directory",
6888
- 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}`
6889
6908
  });
6890
6909
  suggestions.push({
6891
6910
  code: "upload_limit",
@@ -6911,6 +6930,7 @@ function analyzePush(options) {
6911
6930
  schema: ANALYSIS_SCHEMA,
6912
6931
  schema_version: ANALYSIS_SCHEMA_VERSION,
6913
6932
  cli_version: VERSION,
6933
+ view: "full",
6914
6934
  mode,
6915
6935
  root: rootDir,
6916
6936
  ok: blockers.length === 0,
@@ -6947,6 +6967,93 @@ function analyzePush(options) {
6947
6967
  fingerprint: `sha256:${fingerprint}`
6948
6968
  };
6949
6969
  }
6970
+ var SUMMARY_LISTS = {
6971
+ largest_files: 8,
6972
+ by_top_level: 8,
6973
+ duplicates: 8,
6974
+ duplicate_paths: 8,
6975
+ embedded_media: 8,
6976
+ entry_candidates: 20,
6977
+ excluded_entries: 30,
6978
+ dropped_by_platform: 20,
6979
+ finding_paths: 8
6980
+ };
6981
+ var SUMMARY_MAX_BYTES = 24 * 1024;
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)}
6995
+ `;
6996
+ function summarize(analysis, divisor) {
6997
+ const shortened = {};
6998
+ const cut = (name, list, cap, alreadyOmitted = 0) => {
6999
+ const keep = Math.max(1, Math.floor(cap / divisor));
7000
+ const omitted = Math.max(0, list.length - keep) + alreadyOmitted;
7001
+ if (omitted > 0) shortened[name] = (shortened[name] ?? 0) + omitted;
7002
+ return list.slice(0, keep);
7003
+ };
7004
+ const finding = (kind) => (item) => item.paths ? { ...item, paths: cut(`${kind}.paths`, item.paths, SUMMARY_LISTS.finding_paths) } : item;
7005
+ if (analysis.totals.files > 0) shortened.files = analysis.totals.files;
7006
+ const excludedEntries = cut(
7007
+ "excluded.entries",
7008
+ analysis.excluded.entries,
7009
+ SUMMARY_LISTS.excluded_entries,
7010
+ analysis.excluded.omitted
7011
+ );
7012
+ return {
7013
+ schema: analysis.schema,
7014
+ schema_version: analysis.schema_version,
7015
+ cli_version: analysis.cli_version,
7016
+ view: "summary",
7017
+ shortened,
7018
+ mode: analysis.mode,
7019
+ root: analysis.root,
7020
+ ok: analysis.ok,
7021
+ framework: analysis.framework,
7022
+ project: analysis.project,
7023
+ limits: analysis.limits,
7024
+ totals: analysis.totals,
7025
+ blockers: analysis.blockers.map(finding("blockers")),
7026
+ // The note about the cap of the full file list describes a list that is not here.
7027
+ warnings: analysis.warnings.filter((warning) => warning.code !== "file_list_truncated").map(finding("warnings")),
7028
+ suggestions: analysis.suggestions.map(finding("suggestions")),
7029
+ entry: {
7030
+ ...analysis.entry,
7031
+ candidates: cut("entry.candidates", analysis.entry.candidates, SUMMARY_LISTS.entry_candidates)
7032
+ },
7033
+ flattened_prefix: analysis.flattened_prefix,
7034
+ largest_files: cut("largest_files", analysis.largest_files, SUMMARY_LISTS.largest_files),
7035
+ by_class: analysis.by_class,
7036
+ by_top_level: cut("by_top_level", analysis.by_top_level, SUMMARY_LISTS.by_top_level),
7037
+ duplicates: cut("duplicates", analysis.duplicates, SUMMARY_LISTS.duplicates).map((group) => ({ ...group, paths: cut("duplicates.paths", group.paths, SUMMARY_LISTS.duplicate_paths) })),
7038
+ embedded_media: cut("embedded_media", analysis.embedded_media, SUMMARY_LISTS.embedded_media),
7039
+ excluded: {
7040
+ ...analysis.excluded,
7041
+ entries: excludedEntries,
7042
+ omitted: analysis.excluded.omitted + analysis.excluded.entries.length - excludedEntries.length
7043
+ },
7044
+ dropped_by_platform: cut("dropped_by_platform", analysis.dropped_by_platform, SUMMARY_LISTS.dropped_by_platform),
7045
+ files: [],
7046
+ files_omitted: analysis.totals.files,
7047
+ fingerprint: analysis.fingerprint
7048
+ };
7049
+ }
7050
+ function summarizeAnalysis(analysis) {
7051
+ let summary = summarize(analysis, 1);
7052
+ for (let divisor = 2; divisor <= 32 && Buffer.byteLength(printAnalysis(summary), "utf8") > SUMMARY_MAX_BYTES; divisor *= 2) {
7053
+ summary = summarize(analysis, divisor);
7054
+ }
7055
+ return summary;
7056
+ }
6950
7057
  function wouldUpload(analysis, force = false) {
6951
7058
  return analysis.blockers.every((blocker) => blocker.overridable && force);
6952
7059
  }
@@ -7011,7 +7118,7 @@ function renderAnalysis(analysis, options = {}) {
7011
7118
  lines.push(" HTML files found:");
7012
7119
  for (const candidate of analysis.entry.candidates.slice(0, SHOWN_FILES)) {
7013
7120
  lines.push(
7014
- ` ${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)") : "")
7015
7122
  );
7016
7123
  }
7017
7124
  lines.push(...more(analysis.entry.candidates.length, SHOWN_FILES, "HTML file"));
@@ -7425,6 +7532,13 @@ async function pushCommand(opts) {
7425
7532
  "--json belongs to --dry-run: `agentful push --dry-run --json` prints the upload analysis and uploads nothing."
7426
7533
  );
7427
7534
  }
7535
+ if (opts.summary && !opts.json) {
7536
+ throw new ApiError(
7537
+ 0,
7538
+ "invalid_options",
7539
+ "--summary belongs to --dry-run --json: it prints the analysis without the list of every file."
7540
+ );
7541
+ }
7428
7542
  if (!opts.json) console.log(banner());
7429
7543
  const mode = opts.prebuilt ? "dist" : "source";
7430
7544
  const cwd = process.cwd();
@@ -7438,8 +7552,7 @@ async function pushCommand(opts) {
7438
7552
  analysis = failedAnalysis(mode, cwd, { code: err2.code, message: err2.message });
7439
7553
  }
7440
7554
  if (opts.dryRun) {
7441
- if (opts.json) process.stdout.write(`${JSON.stringify(analysis, null, 2)}
7442
- `);
7555
+ if (opts.json) process.stdout.write(printAnalysis(opts.summary ? summarizeAnalysis(analysis) : analysis));
7443
7556
  else for (const line of renderAnalysis(analysis, { force: opts.force, dryRun: true })) console.log(line);
7444
7557
  if (!wouldUpload(analysis, opts.force)) process.exitCode = 1;
7445
7558
  return;
@@ -7968,75 +8081,6 @@ var agentful_theme_default = {
7968
8081
 
7969
8082
  // src/branding/commands.ts
7970
8083
  init_branding();
7971
- 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.';
7972
- var SLASH_COMMANDS = {
7973
- setup: {
7974
- description: `${brand.displayName}: create or link the cloud project for this directory`,
7975
- template: 'Check whether .agentful/project.json exists. If it does, report the linked project title and id and stop. Otherwise run `agentful init --title "<name>"` using the bash tool, where <name> is $ARGUMENTS if I gave one, otherwise the current directory name. Report what it created and mention the title I can change it to. Do not modify any other project files.'
7976
- },
7977
- push: {
7978
- description: `${brand.displayName}: deploy this project and get a preview URL`,
7979
- template: "Run `agentful push` from the project root using the bash tool. It creates the project automatically if this directory is not linked yet. Stream what it prints, and when it finishes report the preview URL on its own line. If it fails: quote the printed error verbatim, and if the failure happened in the cloud (not in the pre-upload preflight), 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. Do not modify project files unless I ask."
7980
- },
7981
- "build-status": {
7982
- description: `${brand.displayName}: show the last cloud build result`,
7983
- template: "Run `agentful build-status` from the project root using the bash tool and report what it prints: status, phase, framework, when it was recorded, and the error/details verbatim if it failed. That record is the only source of truth about the cloud build \u2014 do not speculate beyond it. " + NEEDS_A_PUSH
7984
- },
7985
- backend: {
7986
- description: `${brand.displayName}: open the Backend tab (managed DB + server actions)`,
7987
- template: "Run `agentful backend` from the project root using the bash tool and report the URL it opened. If it prints a declared backend (managed DB / server actions), summarize that and remind me that declared-in-code serves nothing until enabled in that tab. " + NEEDS_A_PUSH
7988
- },
7989
- preview: {
7990
- description: `${brand.displayName}: open the live preview in the browser`,
7991
- template: "Run `agentful open` from the project root using the bash tool and report the URL it opened. Note that `open` neither builds nor uploads. " + NEEDS_A_PUSH
7992
- },
7993
- publish: {
7994
- description: `${brand.displayName}: publish on a public subdomain`,
7995
- template: "Ask me for the subdomain if I did not provide one in $ARGUMENTS, then run `agentful publish <subdomain>` from the project root using the bash tool and report the public URL. " + NEEDS_A_PUSH
7996
- },
7997
- share: {
7998
- description: `${brand.displayName}: make the live preview public (or private again)`,
7999
- template: "Run `agentful share` from the project root using the bash tool (add `--private` if $ARGUMENTS says private) and report the resulting visibility and URL. " + NEEDS_A_PUSH
8000
- },
8001
- status: {
8002
- description: `${brand.displayName}: show the linked project and account`,
8003
- template: "Run `agentful whoami` using the bash tool. Then read .agentful/project.json and report the linked project title and id, plus the preview URL https://preview-<projectId>.agentful.page/. If the file does not exist, say that this directory is not linked yet and that `/setup` creates the link."
8004
- }
8005
- };
8006
- function commandMarkdown(name) {
8007
- const cmd = SLASH_COMMANDS[name];
8008
- const description = JSON.stringify(cmd.description);
8009
- return `---
8010
- description: ${description}
8011
- agent: ${cmd.agent ?? "agentful-build"}
8012
- ---
8013
-
8014
- ${cmd.template}
8015
- `;
8016
- }
8017
-
8018
- // src/lib/tools.ts
8019
- var import_node_fs13 = require("fs");
8020
- var import_node_path15 = require("path");
8021
- function assetsDir() {
8022
- const here = (0, import_node_path15.dirname)(process.argv[1] || "");
8023
- for (const candidate of [(0, import_node_path15.join)(here, "..", "assets"), (0, import_node_path15.join)(here, "assets")]) {
8024
- if ((0, import_node_fs13.existsSync)((0, import_node_path15.join)(candidate, "tools", "imagegen.ts"))) return candidate;
8025
- }
8026
- return null;
8027
- }
8028
- function installEngineTools(configDir2) {
8029
- const assets = assetsDir();
8030
- if (!assets) return false;
8031
- const target = (0, import_node_path15.join)(configDir2, "tools");
8032
- (0, import_node_fs13.mkdirSync)(target, { recursive: true });
8033
- try {
8034
- (0, import_node_fs13.cpSync)((0, import_node_path15.join)(assets, "tools"), target, { recursive: true });
8035
- return true;
8036
- } catch {
8037
- return false;
8038
- }
8039
- }
8040
8084
 
8041
8085
  // src/lib/cloudTarget.ts
8042
8086
  var CLOUD_INSTRUCTIONS_FILENAME = "AGENTFUL_CLOUD.md";
@@ -8146,6 +8190,104 @@ function serverSideSection() {
8146
8190
  )
8147
8191
  ].join("\n\n");
8148
8192
  }
8193
+ var PUSH_ANALYSIS_COMMAND = "agentful push --dry-run --json --summary";
8194
+ function pushSection() {
8195
+ const excludedDirs = [...ALWAYS_EXCLUDE_DIRS].filter((name) => /^[a-z]/.test(name)).map((name) => `\`${name}\``).join(", ");
8196
+ return `## Pushing to the platform (\`agentful push\`)
8197
+
8198
+ Two ways to upload. Every push replaces the previous upload completely, so a
8199
+ site can never be uploaded in several parts.
8200
+
8201
+ - **Source push**, \`agentful push\`: uploads this directory, then builds it in
8202
+ the cloud. Never uploaded: paths listed in \`.gitignore\`, \`.env*\` files,
8203
+ symlinks, tool folders such as \`.git\` or \`.next\`, and any folder named
8204
+ ${excludedDirs}, at any depth.
8205
+ The cloud build runs \`npm ci\` (with a \`package-lock.json\`, otherwise
8206
+ \`npm install\`) and then the build command from the table above. Nothing
8207
+ else runs: no other package manager, no separate data or prepare script, no
8208
+ CI workflow step.
8209
+ - **Prebuilt push**, \`agentful push --prebuilt --dir <folder>\`: uploads a
8210
+ finished output folder and serves it as it is. No cloud build, \`.gitignore\`
8211
+ is not applied, and \`index.html\` must sit at the top of that folder. It
8212
+ carries the website only: the project source in the cloud stays as it was (a
8213
+ later build in the cloud workspace replaces the site with a build of that
8214
+ source), and Managed Server Actions and \`.agentful/backend.json\` are NOT
8215
+ deployed by it.
8216
+
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.
8218
+ \`--force\` lifts none of them: it only lets the cloud build try despite a
8219
+ compatibility finding.
8220
+
8221
+ **The analysis decides, not an estimate.** \`${PUSH_ANALYSIS_COMMAND}\`
8222
+ (with \`--prebuilt --dir <folder>\` for a prebuilt push) reads the files and
8223
+ reports what a push would upload: no sign-in, no project, no upload, nothing
8224
+ written. Sizes, limits, what is left out and what blocks the upload come from
8225
+ that report and from nowhere else: quote its figures, never estimate or
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
8231
+ check itself and stops before sign-in when something blocks. A blocker with
8232
+ \`"overridable": false\` is final, the platform rejects that upload: do not
8233
+ retry, do not add \`--force\`, do not look for another route.
8234
+
8235
+ Every change to the user's files for a push (including \`.gitignore\`) and every
8236
+ project script you run for it needs the user's OK first: name the concrete
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.
8241
+
8242
+ - **No start page** (\`entry_missing\`): the platform serves \`index.html\` from
8243
+ the top level. With \`entry.candidates\`: show them (path and title;
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.
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
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.
8273
+ - **Compatibility findings** (\`"overridable": true\`): fix the cause in the
8274
+ code. \`--force\` only when the user asks to try the cloud build anyway.
8275
+ - **Files that would be missing on the site:** the warnings
8276
+ \`dropped_by_platform\` and \`excluded_static_directory\`, and entries of
8277
+ \`excluded.entries\` with reason \`gitignore\` that the site loads at runtime
8278
+ (generated data under \`public/\`, for example). The build can succeed and the
8279
+ site still be broken: tell the user before pushing and ask.
8280
+ - **Steps the cloud build does not run:** before the first push of a project
8281
+ you did not create in this session, read the \`package.json\` scripts, the
8282
+ README and the CI workflows for data generation, code generation or another
8283
+ package manager. If the site needs such a step, a source push is the wrong
8284
+ way: build locally with the project's own commands and its real data, keep
8285
+ its checks, and push the output folder prebuilt. Never put sample data in
8286
+ place of real data to get a push through.
8287
+
8288
+ A finished build or upload does not prove that the site works: say what you
8289
+ verified and what you did not.`;
8290
+ }
8149
8291
  function renderCloudInstructions(framework, backend) {
8150
8292
  return `# Agentful Cloud target (platform contract)
8151
8293
 
@@ -8300,6 +8442,8 @@ user's conscious step \u2014 in that tab, or by answering the question
8300
8442
  \`agentful push\` asks once (it names both consequences; a non-interactive
8301
8443
  push never enables anything).
8302
8444
 
8445
+ ${pushSection()}
8446
+
8303
8447
  ## Diagnosing failed pushes and cloud builds (hard rule)
8304
8448
 
8305
8449
  Diagnose **only from evidence**: the preflight output of \`agentful push\`, and
@@ -8318,6 +8462,93 @@ ${backendSessionLines(backend)}
8318
8462
  `;
8319
8463
  }
8320
8464
 
8465
+ // src/branding/commands.ts
8466
+ var PUSH_TOOL_TIMEOUT_MS = 9e5;
8467
+ 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
+ var SLASH_COMMANDS = {
8469
+ setup: {
8470
+ description: `${brand.displayName}: create or link the cloud project for this directory`,
8471
+ template: 'Check whether .agentful/project.json exists. If it does, report the linked project title and id and stop. Otherwise run `agentful init --title "<name>"` using the bash tool, where <name> is $ARGUMENTS if I gave one, otherwise the current directory name. Report what it created and mention the title I can change it to. Do not modify any other project files.'
8472
+ },
8473
+ // The analysis comes first (upload plan AP7): the report, not the model,
8474
+ // says what would be uploaded, how large it is and what blocks it, and the
8475
+ // rules for acting on it are in AGENTFUL_CLOUD.md. Two things the agent's
8476
+ // shell tool forces on this template: `--summary`, because the tool cuts
8477
+ // output after 2,000 lines or 50 KB and the full report lists every file;
8478
+ // and an explicit timeout, because the tool ends a command after two
8479
+ // minutes by default and a cloud build takes longer (engine 1.18.32).
8480
+ push: {
8481
+ description: `${brand.displayName}: check, deploy and get a preview URL`,
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.
8483
+
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.
8486
+ 3. After a change I approved, run the analysis again.
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.
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.
8489
+
8490
+ Apart from a change I approved in step 2, do not modify project files.`
8491
+ },
8492
+ "build-status": {
8493
+ description: `${brand.displayName}: show the last cloud build result`,
8494
+ template: "Run `agentful build-status` from the project root using the bash tool and report what it prints: status, phase, framework, when it was recorded, and the error/details verbatim if it failed. That record is the only source of truth about the cloud build \u2014 do not speculate beyond it. " + NEEDS_A_PUSH
8495
+ },
8496
+ backend: {
8497
+ description: `${brand.displayName}: open the Backend tab (managed DB + server actions)`,
8498
+ template: "Run `agentful backend` from the project root using the bash tool and report the URL it opened. If it prints a declared backend (managed DB / server actions), summarize that and remind me that declared-in-code serves nothing until enabled in that tab. " + NEEDS_A_PUSH
8499
+ },
8500
+ preview: {
8501
+ description: `${brand.displayName}: open the live preview in the browser`,
8502
+ template: "Run `agentful open` from the project root using the bash tool and report the URL it opened. Note that `open` neither builds nor uploads. " + NEEDS_A_PUSH
8503
+ },
8504
+ publish: {
8505
+ description: `${brand.displayName}: publish on a public subdomain`,
8506
+ template: "Ask me for the subdomain if I did not provide one in $ARGUMENTS, then run `agentful publish <subdomain>` from the project root using the bash tool and report the public URL. " + NEEDS_A_PUSH
8507
+ },
8508
+ share: {
8509
+ description: `${brand.displayName}: make the live preview public (or private again)`,
8510
+ template: "Run `agentful share` from the project root using the bash tool (add `--private` if $ARGUMENTS says private) and report the resulting visibility and URL. " + NEEDS_A_PUSH
8511
+ },
8512
+ status: {
8513
+ description: `${brand.displayName}: show the linked project and account`,
8514
+ template: "Run `agentful whoami` using the bash tool. Then read .agentful/project.json and report the linked project title and id, plus the preview URL https://preview-<projectId>.agentful.page/. If the file does not exist, say that this directory is not linked yet and that `/setup` creates the link."
8515
+ }
8516
+ };
8517
+ function commandMarkdown(name) {
8518
+ const cmd = SLASH_COMMANDS[name];
8519
+ const description = JSON.stringify(cmd.description);
8520
+ return `---
8521
+ description: ${description}
8522
+ agent: ${cmd.agent ?? "agentful-build"}
8523
+ ---
8524
+
8525
+ ${cmd.template}
8526
+ `;
8527
+ }
8528
+
8529
+ // src/lib/tools.ts
8530
+ var import_node_fs13 = require("fs");
8531
+ var import_node_path15 = require("path");
8532
+ function assetsDir() {
8533
+ const here = (0, import_node_path15.dirname)(process.argv[1] || "");
8534
+ for (const candidate of [(0, import_node_path15.join)(here, "..", "assets"), (0, import_node_path15.join)(here, "assets")]) {
8535
+ if ((0, import_node_fs13.existsSync)((0, import_node_path15.join)(candidate, "tools", "imagegen.ts"))) return candidate;
8536
+ }
8537
+ return null;
8538
+ }
8539
+ function installEngineTools(configDir2) {
8540
+ const assets = assetsDir();
8541
+ if (!assets) return false;
8542
+ const target = (0, import_node_path15.join)(configDir2, "tools");
8543
+ (0, import_node_fs13.mkdirSync)(target, { recursive: true });
8544
+ try {
8545
+ (0, import_node_fs13.cpSync)((0, import_node_path15.join)(assets, "tools"), target, { recursive: true });
8546
+ return true;
8547
+ } catch {
8548
+ return false;
8549
+ }
8550
+ }
8551
+
8321
8552
  // src/lib/engineConfig.ts
8322
8553
  var PROVIDER_ID = "agentful";
8323
8554
  var DISABLED_ENGINE_PLUGINS = {
@@ -8555,18 +8786,20 @@ var SCAFFOLD_BY_FRAMEWORK = {
8555
8786
  // cra/nuxt get no scaffold skill: the react/vue skills are Vite-specific
8556
8787
  // and would teach the wrong shape — the template contract still applies.
8557
8788
  };
8558
- function hasVueDependency(rootDir) {
8789
+ function dependencies(rootDir) {
8559
8790
  try {
8560
8791
  const pkg = JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path17.join)(rootDir, "package.json"), "utf8"));
8561
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["vue"]);
8792
+ return { ...pkg.dependencies, ...pkg.devDependencies };
8562
8793
  } catch {
8563
- return false;
8794
+ return {};
8564
8795
  }
8565
8796
  }
8566
8797
  function selectSkills(framework, rootDir) {
8567
8798
  const names = [CONTRACT_SKILL];
8568
8799
  if (framework === "vite") {
8569
- names.push(hasVueDependency(rootDir) ? "vue-scaffold" : "react-scaffold");
8800
+ const deps = dependencies(rootDir);
8801
+ if (deps.vue) names.push("vue-scaffold");
8802
+ else if (deps.react) names.push("react-scaffold");
8570
8803
  } else {
8571
8804
  const scaffold = SCAFFOLD_BY_FRAMEWORK[framework];
8572
8805
  if (scaffold) names.push(scaffold);
@@ -9895,7 +10128,7 @@ program2.command("login").description("Sign in to your Agentful account (device
9895
10128
  program2.command("logout").description("Remove the stored credentials").action(run(logoutCommand));
9896
10129
  program2.command("whoami").description("Show the signed-in account").action(run(whoamiCommand));
9897
10130
  program2.command("init").description("Create (or link) the Agentful cloud project for this directory").argument("[directory]", "with --template: new folder to scaffold into (created for you)").option("--link <projectId>", "link an existing project instead of creating one").option("--title <title>", "project title (skips the prompt)").option("--template <framework>", "scaffold from a platform starter (nextjs, react, vue, vanilla)").action(run((directory, opts) => initCommand(opts, directory)));
9898
- program2.command("push").description("Upload this project and get a live preview URL").option("--prebuilt", "upload a locally built output instead of building in the cloud").option("--dir <path>", "built-output directory for --prebuilt (default: auto-detect)").option("--force", "push even when the pre-upload compatibility check finds blocking issues").option("-y, --yes", "enable the declared managed database without asking (CI); a non-interactive push never enables it").option("--dry-run", "analyse only: what would be uploaded, how large, and what would block it \u2014 no sign-in, no project, no upload").option("--json", "with --dry-run: print the analysis as JSON (for scripts and agents)").action(run((opts) => pushCommand(opts)));
10131
+ program2.command("push").description("Upload this project and get a live preview URL").option("--prebuilt", "upload a locally built output instead of building in the cloud").option("--dir <path>", "built-output directory for --prebuilt (default: auto-detect)").option("--force", "push even when the pre-upload compatibility check finds blocking issues").option("-y, --yes", "enable the declared managed database without asking (CI); a non-interactive push never enables it").option("--dry-run", "analyse only: what would be uploaded, how large, and what would block it \u2014 no sign-in, no project, no upload").option("--json", "with --dry-run: print the analysis as JSON (for scripts and agents)").option("--summary", "with --dry-run --json: totals and findings without the list of every file").action(run((opts) => pushCommand(opts)));
9899
10132
  program2.command("build-status").description("Show the cloud's record of the last build (the real error on failures)").action(run(buildStatusCommand));
9900
10133
  program2.command("backend").description("Open this project's Backend tab (managed DB + server actions) in the browser").action(run(backendCommand));
9901
10134
  program2.command("open").description("Open the live preview in the browser").action(run(openCommand));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentful",
3
- "version": "0.4.14",
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",