@agent-native/core 0.127.1 → 0.127.3

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 (35) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/template-sync.ts +96 -0
  5. package/corpus/core/src/server/builder-design-systems.ts +1 -1
  6. package/corpus/core/src/templates/chat/_gitignore +2 -0
  7. package/corpus/templates/chat/_gitignore +2 -0
  8. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -0
  9. package/corpus/templates/design/actions/create-file.ts +15 -0
  10. package/corpus/templates/design/actions/generate-design.ts +40 -0
  11. package/corpus/templates/design/actions/present-design-variants.ts +16 -0
  12. package/corpus/templates/design/app/pages/design-editor/save-failure.ts +7 -1
  13. package/corpus/templates/design/changelog/2026-07-28-generated-designs-with-malformed-html-are-now-rejected-at-sa.md +6 -0
  14. package/corpus/templates/design/server/source-workspace.ts +1 -0
  15. package/corpus/templates/design/shared/html-integrity.ts +608 -18
  16. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -6
  17. package/dist/cli/template-sync.d.ts.map +1 -1
  18. package/dist/cli/template-sync.js +88 -0
  19. package/dist/cli/template-sync.js.map +1 -1
  20. package/dist/collab/routes.d.ts +1 -1
  21. package/dist/collab/struct-routes.d.ts +1 -1
  22. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  23. package/dist/observability/routes.d.ts +3 -3
  24. package/dist/progress/routes.d.ts +1 -1
  25. package/dist/resources/handlers.d.ts +1 -1
  26. package/dist/secrets/routes.d.ts +9 -9
  27. package/dist/server/builder-design-systems.js +1 -1
  28. package/dist/server/builder-design-systems.js.map +1 -1
  29. package/dist/server/realtime-token.d.ts +1 -1
  30. package/dist/server/transcribe-voice.d.ts +1 -1
  31. package/dist/templates/chat/_gitignore +2 -0
  32. package/package.json +1 -1
  33. package/src/cli/template-sync.ts +96 -0
  34. package/src/server/builder-design-systems.ts +1 -1
  35. package/src/templates/chat/_gitignore +2 -0
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1739
32
32
  - toolkit files: 160
33
- - template files: 6815
33
+ - template files: 6816
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.127.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 750e90e: Add a `materialize --out <dir>` subcommand to `agent-native template` that writes the post-processed standalone template tree (e.g. `--template chat`, with the template's own identity, `_gitignore`→`.gitignore`, resolved deps, standalone `netlify.toml`) to a directory — no existing app required. This is the monorepo half of the vendor-branch starter mirror: the public monorepo materializes `templates/chat` and pushes it to the private starter's `template` branch, which the starter merges into `main` with git. Reuses the existing `materializeTemplate` engine.
8
+
9
+ ## 0.127.2
10
+
11
+ ### Patch Changes
12
+
13
+ - c6ca76a: Fix the Builder Design Systems API base URL fallback incorrectly including an `agent-native/` prefix. The real route is registered as `/design-systems/v1/...` with no `agent-native/` prefix, so requests using the fallback base URL (when `BUILDER_DESIGN_SYSTEMS_BASE_URL` is unset) were hitting the wrong path.
14
+
3
15
  ## 0.127.1
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.127.1",
3
+ "version": "0.127.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -476,6 +476,18 @@ export async function runTemplate(
476
476
  }
477
477
 
478
478
  const rest = args.slice(1);
479
+
480
+ // `materialize` produces a fresh post-processed template tree at --out; it has
481
+ // no existing app to resolve, so it runs before target resolution.
482
+ if (command === "materialize") {
483
+ try {
484
+ return await materializeCommand(rest, io);
485
+ } catch (err) {
486
+ io.err(err instanceof Error ? err.message : String(err));
487
+ return 1;
488
+ }
489
+ }
490
+
479
491
  const appArg = positionalArgs(rest)[0];
480
492
  const flags = {
481
493
  to: flagValue(rest, "--to"),
@@ -540,6 +552,87 @@ async function runForTarget(
540
552
  }
541
553
  }
542
554
 
555
+ /**
556
+ * Write the post-processed template tree to a directory — the pristine,
557
+ * template-derived output `create` would produce (no install/git/skills).
558
+ * `--to` defaults to the local template (walked up from the package); pass a ref
559
+ * to fetch from GitHub instead.
560
+ */
561
+ async function materializeCommand(
562
+ rest: string[],
563
+ io: TemplateIO,
564
+ ): Promise<number> {
565
+ const outDir = flagValue(rest, "--out");
566
+ const template = flagValue(rest, "--template");
567
+ const name = flagValue(rest, "--name");
568
+ const ref = flagValue(rest, "--to") ?? null;
569
+ if (!outDir) {
570
+ io.err("template materialize requires --out <dir>.");
571
+ return 1;
572
+ }
573
+ if (!template) {
574
+ io.err("template materialize requires --template <name>.");
575
+ return 1;
576
+ }
577
+ // Stage into a unique sibling dir, then swap it in crash-safely: move the old
578
+ // tree aside, move the staged tree in, and restore the old one if that fails.
579
+ // Directory replacement isn't atomic on POSIX (you can't rename onto a non-empty
580
+ // dir), so the old tree is only ever moved — never deleted before the new one is
581
+ // in place — and a crash between the two renames leaves it recoverable under
582
+ // `backup`. Unique same-filesystem siblings keep each rename atomic and
583
+ // collision-free.
584
+ const parent = path.dirname(path.resolve(outDir));
585
+ fs.mkdirSync(parent, { recursive: true });
586
+ const tmp = fs.mkdtempSync(path.join(parent, ".template-materialize-"));
587
+ // `backup` is a unique dir too, so a concurrent process can't occupy the path;
588
+ // renaming a directory onto an existing *empty* dir is a valid replace on POSIX,
589
+ // so the `hadOld` rename below still lands cleanly on it.
590
+ const backup = fs.mkdtempSync(
591
+ path.join(parent, ".template-materialize-backup-"),
592
+ );
593
+ let backupHoldsOriginal = false;
594
+ try {
595
+ const result = await materializeTemplate({
596
+ appName: name ?? template,
597
+ template,
598
+ ref,
599
+ shape: "standalone",
600
+ destDir: tmp,
601
+ });
602
+ const hadOld = fs.existsSync(outDir);
603
+ if (hadOld) {
604
+ fs.renameSync(outDir, backup);
605
+ backupHoldsOriginal = true;
606
+ }
607
+ try {
608
+ fs.renameSync(tmp, outDir);
609
+ backupHoldsOriginal = false;
610
+ } catch (err) {
611
+ if (hadOld) {
612
+ fs.renameSync(backup, outDir);
613
+ backupHoldsOriginal = false;
614
+ }
615
+ throw err;
616
+ }
617
+ io.out(
618
+ `Materialized "${template}" (${result.source}@${result.ref}) to ${outDir}.`,
619
+ );
620
+ return 0;
621
+ } finally {
622
+ fs.rmSync(tmp, { recursive: true, force: true });
623
+ // If restoring from `backup` above itself threw, `backupHoldsOriginal` is still
624
+ // true and `backup` is the only remaining copy of the pre-existing output —
625
+ // leave it on disk instead of deleting the last copy of the user's data.
626
+ if (!backupHoldsOriginal) {
627
+ fs.rmSync(backup, { recursive: true, force: true });
628
+ } else {
629
+ io.err(
630
+ `Failed to restore ${outDir}; the previous contents were preserved at ${backup}.`,
631
+ );
632
+ }
633
+ }
634
+ }
635
+
543
636
  function templateName(target: AppTarget, flags: TemplateFlags): string {
544
637
  const name = flags.template ?? target.provenance.template;
545
638
  if (!name) {
@@ -995,6 +1088,9 @@ function templateUsage(): string {
995
1088
  " baseline [app] Record a baseline for an app scaffolded before",
996
1089
  " provenance existed [--ref <ref>] [--template <name>]",
997
1090
  " accept [app] Advance the baseline after resolving conflicts",
1091
+ " materialize --template <name> --out <dir>",
1092
+ " Write the post-processed template tree to a dir",
1093
+ " (no app needed) [--name <appName>] [--to <ref>]",
998
1094
  "",
999
1095
  "With no [app], operates on the current app, or on every app in a workspace",
1000
1096
  "when run from the workspace root. --to defaults to the ref matching the",
@@ -140,7 +140,7 @@ function trimTrailingSlash(value: string): string {
140
140
  export function getBuilderDesignSystemsBaseUrl(): string {
141
141
  return (
142
142
  process.env.BUILDER_DESIGN_SYSTEMS_BASE_URL ||
143
- `${trimTrailingSlash(getBuilderProxyOrigin())}/agent-native/design-systems/v1`
143
+ `${trimTrailingSlash(getBuilderProxyOrigin())}/design-systems/v1`
144
144
  );
145
145
  }
146
146
 
@@ -9,8 +9,10 @@ npm-debug.log*
9
9
  pnpm-debug.log*
10
10
 
11
11
  node_modules
12
+ build/
12
13
  dist
13
14
  dist-ssr
15
+ .output/
14
16
  *.local
15
17
 
16
18
  # Editor directories and files
@@ -9,8 +9,10 @@ npm-debug.log*
9
9
  pnpm-debug.log*
10
10
 
11
11
  node_modules
12
+ build/
12
13
  dist
13
14
  dist-ssr
15
+ .output/
14
16
  *.local
15
17
 
16
18
  # Editor directories and files
@@ -377,6 +377,29 @@ view only when the user asked to focus one specific screen.
377
377
 
378
378
  ## HTML Structure Requirements
379
379
 
380
+ ### The save gate rejects malformed markup
381
+
382
+ Every path that persists model-authored HTML — `generate-design`, `create-file`,
383
+ `present-design-variants`, `edit-design`, `update-file`, canvas edits — runs an
384
+ integrity check, and a **new** file is held to it strictly. Import paths
385
+ (Figma, localhost, templates, duplication) deliberately do not fail closed:
386
+ that markup comes from a real app or an existing design, and rejecting it would
387
+ block bringing work in rather than prevent a bad generation.
388
+ Rejections name the file, line, column, and offending source line — fix exactly
389
+ what it points at instead of re-sending the payload differently. Blocked: an
390
+ attribute quote that is never closed; an unclosed element or a closing tag with
391
+ no opener; content ending mid-tag (a payload cut off in transit); a
392
+ `<style>`/`<script>` missing its opener or closer; duplicated editor-managed
393
+ style blocks; more than one `<html>`/`<body>`.
394
+
395
+ This exists because the HTML parser never throws — it recovers silently and
396
+ renders a page, just not the one you wrote. An unterminated quote in `<head>`
397
+ swallows every following tag into that attribute, so the Tailwind runtime stops
398
+ applying and the screen renders unstyled with no console error to show it.
399
+
400
+ Saves may also return non-blocking `warnings` (e.g. utility classes with no
401
+ reachable Tailwind runtime). Fix those before reporting the design as ready.
402
+
380
403
  ### Mandatory Elements
381
404
 
382
405
  Every `index.html` must include:
@@ -6,6 +6,10 @@ import { nanoid } from "nanoid";
6
6
  import { z } from "zod";
7
7
 
8
8
  import { getDb, schema } from "../server/db/index.js";
9
+ import {
10
+ assertDesignHtmlCreateIntegrity,
11
+ describeDesignHtmlIntegrityIssue,
12
+ } from "../shared/html-integrity.js";
9
13
  import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
10
14
 
11
15
  export default defineAction({
@@ -63,6 +67,14 @@ export default defineAction({
63
67
  // the first time someone opens it.
64
68
  const annotatedContent = annotateScreenHtmlForPersist(content, fileType);
65
69
 
70
+ // Reject malformed HTML before the row exists — creation went through raw
71
+ // inserts, so it was the one write path with no integrity gate.
72
+ const advisory = assertDesignHtmlCreateIntegrity({
73
+ content: annotatedContent,
74
+ fileType: fileType ?? "html",
75
+ filename,
76
+ });
77
+
66
78
  await db.insert(schema.designFiles).values({
67
79
  id,
68
80
  designId,
@@ -94,6 +106,9 @@ export default defineAction({
94
106
  fileType: resolvedFileType,
95
107
  renderable,
96
108
  urlPath: renderable ? `/design/${designId}` : null,
109
+ ...(advisory.length > 0
110
+ ? { warnings: advisory.map(describeDesignHtmlIntegrityIssue) }
111
+ : {}),
97
112
  };
98
113
  },
99
114
  });
@@ -45,6 +45,11 @@ import {
45
45
  type DesignGenerationSession,
46
46
  updateGenerationSessionWithSavedFiles,
47
47
  } from "../shared/generation-session.js";
48
+ import {
49
+ assertDesignHtmlCreateIntegrity,
50
+ describeDesignHtmlIntegrityIssue,
51
+ inspectDesignHtmlDocumentIntegrity,
52
+ } from "../shared/html-integrity.js";
48
53
  import { assertLockedLayersPreserved } from "../shared/locked-layers.js";
49
54
  import { widthToPrefix } from "../shared/responsive-classes.js";
50
55
  import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
@@ -769,6 +774,38 @@ const generateDesignAction = defineAction({
769
774
  content: annotateScreenHtmlForPersist(file.content, file.fileType),
770
775
  }));
771
776
 
777
+ // Gate every NEW file up front so a rejected file cannot orphan the ones
778
+ // written before it. Existing files take the edit transition inside
779
+ // writeInlineSourceFile, which still allows repairing a malformed screen.
780
+ const integrityWarnings: Array<{ filename: string; message: string }> = [];
781
+ for (const file of annotatedFiles) {
782
+ if ((file.fileType ?? "html") !== "html") continue;
783
+ const existing = existingByName.get(file.filename);
784
+ // A row changing type into HTML is new HTML, not an edit: the edit
785
+ // transition validates against the row's CURRENT type, so a css→html
786
+ // candidate would skip the HTML checks and still be stored as HTML below.
787
+ const becomesHtml =
788
+ existing !== undefined && (existing.fileType ?? "html") !== "html";
789
+ // Blocking applies to new files and type transitions. An existing HTML row
790
+ // takes the lenient edit transition instead, so a legacy-malformed screen
791
+ // stays repairable — but its advisories are still reported, or the same
792
+ // content would warn as a new file and save silently as a regeneration.
793
+ const advisory =
794
+ !existing || becomesHtml
795
+ ? assertDesignHtmlCreateIntegrity({
796
+ content: file.content,
797
+ fileType: "html",
798
+ filename: file.filename,
799
+ })
800
+ : (inspectDesignHtmlDocumentIntegrity(file.content).advisory ?? []);
801
+ for (const entry of advisory) {
802
+ integrityWarnings.push({
803
+ filename: file.filename,
804
+ message: describeDesignHtmlIntegrityIssue(entry),
805
+ });
806
+ }
807
+ }
808
+
772
809
  for (const file of annotatedFiles) {
773
810
  const existing = existingByName.get(file.filename);
774
811
  if (existing) {
@@ -1198,6 +1235,9 @@ const generateDesignAction = defineAction({
1198
1235
  savedFiles,
1199
1236
  placedFrames,
1200
1237
  fileCount: savedFiles.length,
1238
+ // Non-blocking: a well-formed screen with no Tailwind runtime renders
1239
+ // unstyled, which reads as a layout bug rather than a missing runtime.
1240
+ ...(integrityWarnings.length > 0 ? { warnings: integrityWarnings } : {}),
1201
1241
  ...creativeContextProvenance,
1202
1242
  };
1203
1243
  },
@@ -19,6 +19,7 @@ import {
19
19
  type CanvasFramePlacement,
20
20
  } from "../shared/canvas-frames.js";
21
21
  import { isUniqueConstraintViolation } from "../shared/db-conflict.js";
22
+ import { assertDesignHtmlWellFormed } from "../shared/html-integrity.js";
22
23
  import { widthToPrefix } from "../shared/responsive-classes.js";
23
24
  import { annotateScreenHtmlForPersist } from "../shared/screen-annotation.js";
24
25
 
@@ -826,6 +827,21 @@ export default defineAction({
826
827
  run: async ({ designId, prompt, variants, deleteSupersededSetIds }) => {
827
828
  await assertAccess("design", designId, "editor");
828
829
 
830
+ // Before any mutation. These are model-authored screens created by raw
831
+ // insert, so they need the same well-formedness gate as generate-design —
832
+ // though not its document-shape rules, since a variant may be a sketch with
833
+ // `<html>`/`<body>` implied. Ordering is the load-bearing part: the
834
+ // supersession and deletion below are irreversible, so throwing after them
835
+ // would destroy existing variant sets and create nothing to replace them.
836
+ for (const variant of variants) {
837
+ const candidate = variant.content?.trim();
838
+ if (!candidate) continue;
839
+ assertDesignHtmlWellFormed({
840
+ content: annotateScreenHtmlForPersist(candidate, "html"),
841
+ filename: `variant-${slugify(variant.label.trim(), "option")}.html`,
842
+ });
843
+ }
844
+
829
845
  // Non-destructive bookkeeping: flag earlier still-complete variant sets
830
846
  // as superseded. Files are NEVER deleted automatically — a user's pick
831
847
  // exists only as a chat instruction until the agent's delete-file turn
@@ -1,4 +1,7 @@
1
- import { isDesignHtmlIntegrityError } from "@shared/html-integrity";
1
+ import {
2
+ DESIGN_HTML_INTEGRITY_SUMMARY,
3
+ isDesignHtmlIntegrityError,
4
+ } from "@shared/html-integrity";
2
5
 
3
6
  export type DesignSaveFailureKind =
4
7
  | "offline"
@@ -14,6 +17,9 @@ function errorField(error: unknown, field: string): unknown {
14
17
  }
15
18
 
16
19
  export function designSaveErrorMessage(error: unknown): string | null {
20
+ // Integrity errors carry located, agent-facing guidance in `message`. Toasts
21
+ // get the summary instead; the two audiences need different text.
22
+ if (isDesignHtmlIntegrityError(error)) return DESIGN_HTML_INTEGRITY_SUMMARY;
17
23
  const message = errorField(error, "message");
18
24
  if (typeof message !== "string" || !message.trim()) return null;
19
25
  return message.replace(/^DESIGN_HTML_INTEGRITY:\s*/, "");
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-28
4
+ ---
5
+
6
+ Generated designs with malformed HTML are now rejected at save with the exact line and character to fix, instead of persisting and rendering broken.
@@ -361,6 +361,7 @@ export async function writeInlineSourceFile(args: {
361
361
  previousContent: current.content,
362
362
  nextContent: args.content,
363
363
  fileType: currentFile.fileType ?? args.file.fileType ?? "html",
364
+ filename: currentFile.filename ?? args.file.filename,
364
365
  });
365
366
 
366
367
  if (await hasCollabState(args.file.id)) {