@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/template-sync.ts +96 -0
- package/corpus/core/src/server/builder-design-systems.ts +1 -1
- package/corpus/core/src/templates/chat/_gitignore +2 -0
- package/corpus/templates/chat/_gitignore +2 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -0
- package/corpus/templates/design/actions/create-file.ts +15 -0
- package/corpus/templates/design/actions/generate-design.ts +40 -0
- package/corpus/templates/design/actions/present-design-variants.ts +16 -0
- package/corpus/templates/design/app/pages/design-editor/save-failure.ts +7 -1
- package/corpus/templates/design/changelog/2026-07-28-generated-designs-with-malformed-html-are-now-rejected-at-sa.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +608 -18
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -6
- package/dist/cli/template-sync.d.ts.map +1 -1
- package/dist/cli/template-sync.js +88 -0
- package/dist/cli/template-sync.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/builder-design-systems.js +1 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/chat/_gitignore +2 -0
- package/package.json +1 -1
- package/src/cli/template-sync.ts +96 -0
- package/src/server/builder-design-systems.ts +1 -1
- package/src/templates/chat/_gitignore +2 -0
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.127.
|
|
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())}/
|
|
143
|
+
`${trimTrailingSlash(getBuilderProxyOrigin())}/design-systems/v1`
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -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 {
|
|
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*/, "");
|
|
@@ -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)) {
|