minutework 0.1.6 → 0.1.8
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/EXTERNAL_ALPHA.md +18 -1
- package/README.md +9 -2
- package/assets/claude-local/CLAUDE.md.template +22 -2
- package/assets/claude-local/skills/README.md +9 -0
- package/assets/claude-local/skills/app-pack-authoring.md +5 -1
- package/assets/claude-local/skills/content-structure-and-sections.md +15 -0
- package/assets/claude-local/skills/published-web-and-mw-core-site.md +17 -0
- package/assets/claude-local/skills/schema-engine.md +4 -0
- package/assets/claude-local/skills/secrets-runtime-bridge.md +3 -0
- package/assets/claude-local/skills/sidecar-generation.md +4 -0
- package/assets/templates/fastapi-sidecar/template.schema.json +2 -1
- package/assets/templates/next-tenant-app/.storybook/main.ts +1 -1
- package/assets/templates/next-tenant-app/README.md +20 -3
- package/assets/templates/next-tenant-app/next.config.mjs +33 -0
- package/assets/templates/next-tenant-app/package.json +3 -1
- package/assets/templates/next-tenant-app/public/.gitkeep +1 -0
- package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +73 -0
- package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +5 -3
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +38 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +145 -0
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +2 -2
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +47 -1
- package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +138 -0
- package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +59 -0
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +70 -0
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +106 -0
- package/assets/templates/next-tenant-app/template.json +2 -2
- package/assets/templates/next-tenant-app/template.schema.json +2 -1
- package/assets/templates/next-tenant-app/tools/template/with-public-site-fixture.mjs +130 -0
- package/dist/agent.d.ts +19 -0
- package/dist/agent.js +308 -0
- package/dist/agent.js.map +1 -0
- package/dist/developer-client.d.ts +59 -0
- package/dist/developer-client.js +35 -0
- package/dist/developer-client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/assets/templates/next-tenant-app/next.config.ts +0 -18
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
|
|
3
3
|
import { cache } from "react";
|
|
4
|
-
import type
|
|
4
|
+
import { z, type ZodType } from "zod";
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
|
+
ContentStructurePage,
|
|
7
8
|
MarketingPage,
|
|
8
9
|
MarketingPageKey,
|
|
9
10
|
PublicContentAdapter,
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
SiteConfig,
|
|
15
16
|
} from "@/lib/content/contracts";
|
|
16
17
|
import {
|
|
18
|
+
contentStructurePageSchema,
|
|
17
19
|
marketingPageResultSchema,
|
|
18
20
|
publicEntryResultSchema,
|
|
19
21
|
publicEntrySummaryListSchema,
|
|
@@ -110,6 +112,26 @@ export class ValidatedPublicContentAdapter implements PublicContentAdapter {
|
|
|
110
112
|
await this.adapter.getEntry(kind, slugParts),
|
|
111
113
|
);
|
|
112
114
|
}
|
|
115
|
+
|
|
116
|
+
async getPageByPath(path: string): Promise<ContentStructurePage | null> {
|
|
117
|
+
if (!this.adapter.getPageByPath) return null;
|
|
118
|
+
return validateAdapterResult(
|
|
119
|
+
this.adapterLabel,
|
|
120
|
+
`CMS page for path "${path}"`,
|
|
121
|
+
contentStructurePageSchema.nullable(),
|
|
122
|
+
await this.adapter.getPageByPath(path),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async listPages(status?: string): Promise<ContentStructurePage[]> {
|
|
127
|
+
if (!this.adapter.listPages) return [];
|
|
128
|
+
return validateAdapterResult(
|
|
129
|
+
this.adapterLabel,
|
|
130
|
+
"CMS page list",
|
|
131
|
+
z.array(contentStructurePageSchema),
|
|
132
|
+
await this.adapter.listPages(status),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
113
135
|
}
|
|
114
136
|
|
|
115
137
|
export async function fetchPublicSiteSnapshotEnvelope(input: {
|
|
@@ -197,6 +219,22 @@ export class MinuteWorkPublicSiteContentAdapter implements PublicContentAdapter
|
|
|
197
219
|
) ?? null
|
|
198
220
|
);
|
|
199
221
|
}
|
|
222
|
+
|
|
223
|
+
async getPageByPath(path: string): Promise<ContentStructurePage | null> {
|
|
224
|
+
const snapshot = await this.loadSnapshot();
|
|
225
|
+
const pages = snapshot.pages ?? [];
|
|
226
|
+
const normalizedPath = path.replace(/\/+$/, "") || "/";
|
|
227
|
+
return pages.find((p) => p.path === normalizedPath) ?? null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async listPages(status?: string): Promise<ContentStructurePage[]> {
|
|
231
|
+
const snapshot = await this.loadSnapshot();
|
|
232
|
+
const pages = snapshot.pages ?? [];
|
|
233
|
+
if (status) {
|
|
234
|
+
return pages.filter((p) => p.status === status);
|
|
235
|
+
}
|
|
236
|
+
return pages;
|
|
237
|
+
}
|
|
200
238
|
}
|
|
201
239
|
|
|
202
240
|
const defaultPublicContentAdapter = new ValidatedPublicContentAdapter(
|
|
@@ -230,3 +268,11 @@ export const listEntries = cache(async (kind: PublicContentKind) =>
|
|
|
230
268
|
export const getEntry = cache(async (kind: PublicContentKind, slugParts: string[]) =>
|
|
231
269
|
getPublicContentAdapter().getEntry(kind, slugParts),
|
|
232
270
|
);
|
|
271
|
+
|
|
272
|
+
export const getPageByPath = cache(async (path: string) =>
|
|
273
|
+
getPublicContentAdapter().getPageByPath(path),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
export const listPages = cache(async (status?: string) =>
|
|
277
|
+
getPublicContentAdapter().listPages(status),
|
|
278
|
+
);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
contentSectionSchema,
|
|
5
|
+
contentStructurePageSchema,
|
|
6
|
+
publicContentSnapshotSchema,
|
|
7
|
+
} from "./contracts";
|
|
8
|
+
|
|
9
|
+
describe("content_structure contracts", () => {
|
|
10
|
+
it("validates a page with arbitrary section types", () => {
|
|
11
|
+
const page = {
|
|
12
|
+
page_key: "landing",
|
|
13
|
+
path: "/landing",
|
|
14
|
+
title: "Landing Page",
|
|
15
|
+
status: "published",
|
|
16
|
+
sort_order: 0,
|
|
17
|
+
content_structure: {
|
|
18
|
+
sections: [
|
|
19
|
+
{
|
|
20
|
+
section_key: "hero",
|
|
21
|
+
section_type: "hero_banner",
|
|
22
|
+
sort_order: 0,
|
|
23
|
+
data: { heading: "Welcome", subheading: "To our site" },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
section_key: "features",
|
|
27
|
+
section_type: "custom_feature_grid",
|
|
28
|
+
sort_order: 1,
|
|
29
|
+
data: {
|
|
30
|
+
columns: 3,
|
|
31
|
+
items: [
|
|
32
|
+
{ title: "Fast", icon: "bolt" },
|
|
33
|
+
{ title: "Secure", icon: "lock" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
section_key: "tenant_custom",
|
|
39
|
+
section_type: "my_custom_widget",
|
|
40
|
+
sort_order: 2,
|
|
41
|
+
data: { arbitrary: "tenant-defined", nested: { deep: true } },
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
seo_metadata: {
|
|
46
|
+
title: "Landing Page",
|
|
47
|
+
description: "A landing page with arbitrary sections",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const result = contentStructurePageSchema.safeParse(page);
|
|
52
|
+
expect(result.success).toBe(true);
|
|
53
|
+
if (result.success) {
|
|
54
|
+
expect(result.data.content_structure.sections).toHaveLength(3);
|
|
55
|
+
expect(result.data.content_structure.sections[2]!.section_type).toBe(
|
|
56
|
+
"my_custom_widget",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("rejects a section without section_key", () => {
|
|
62
|
+
const section = {
|
|
63
|
+
section_type: "text",
|
|
64
|
+
data: { heading: "Heading" },
|
|
65
|
+
};
|
|
66
|
+
const result = contentSectionSchema.safeParse(section);
|
|
67
|
+
expect(result.success).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("allows empty sections array", () => {
|
|
71
|
+
const page = {
|
|
72
|
+
page_key: "empty",
|
|
73
|
+
path: "/empty",
|
|
74
|
+
title: "Empty Page",
|
|
75
|
+
status: "draft",
|
|
76
|
+
content_structure: { sections: [] },
|
|
77
|
+
};
|
|
78
|
+
const result = contentStructurePageSchema.safeParse(page);
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("snapshot schema accepts optional pages array", () => {
|
|
83
|
+
const snapshot = {
|
|
84
|
+
site: {
|
|
85
|
+
siteName: "Test",
|
|
86
|
+
siteDescription: "Test site",
|
|
87
|
+
organizationName: "Org",
|
|
88
|
+
footerBlurb: "Footer",
|
|
89
|
+
primaryCta: { label: "CTA", href: "/" },
|
|
90
|
+
secondaryCta: { label: "CTA2", href: "/docs" },
|
|
91
|
+
primaryNavigation: [{ label: "Home", href: "/" }],
|
|
92
|
+
collections: {
|
|
93
|
+
docs: { eyebrow: "Docs", title: "Docs", description: "Documentation" },
|
|
94
|
+
blog: { eyebrow: "Blog", title: "Blog", description: "Updates" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
marketingPages: [],
|
|
98
|
+
docs: [],
|
|
99
|
+
blog: [],
|
|
100
|
+
pages: [
|
|
101
|
+
{
|
|
102
|
+
page_key: "about",
|
|
103
|
+
path: "/about",
|
|
104
|
+
title: "About",
|
|
105
|
+
status: "published",
|
|
106
|
+
content_structure: { sections: [] },
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = publicContentSnapshotSchema.safeParse(snapshot);
|
|
112
|
+
expect(result.success).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("snapshot schema works without pages field (backward compat)", () => {
|
|
116
|
+
const snapshot = {
|
|
117
|
+
site: {
|
|
118
|
+
siteName: "Test",
|
|
119
|
+
siteDescription: "Test site",
|
|
120
|
+
organizationName: "Org",
|
|
121
|
+
footerBlurb: "Footer",
|
|
122
|
+
primaryCta: { label: "CTA", href: "/" },
|
|
123
|
+
secondaryCta: { label: "CTA2", href: "/docs" },
|
|
124
|
+
primaryNavigation: [{ label: "Home", href: "/" }],
|
|
125
|
+
collections: {
|
|
126
|
+
docs: { eyebrow: "Docs", title: "Docs", description: "Documentation" },
|
|
127
|
+
blog: { eyebrow: "Blog", title: "Blog", description: "Updates" },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
marketingPages: [],
|
|
131
|
+
docs: [],
|
|
132
|
+
blog: [],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const result = publicContentSnapshotSchema.safeParse(snapshot);
|
|
136
|
+
expect(result.success).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -93,11 +93,48 @@ export type DocsEntry = DocsEntrySummary & PublicEntryBody;
|
|
|
93
93
|
export type BlogEntry = BlogEntrySummary & PublicEntryBody;
|
|
94
94
|
export type PublicEntry = DocsEntry | BlogEntry;
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* A single section/block within a CMS page's content_structure.
|
|
98
|
+
*
|
|
99
|
+
* Section types are extensible strings — not a closed enum. This allows
|
|
100
|
+
* tenant-specific or extension-pack sections without modifying the baseline.
|
|
101
|
+
*/
|
|
102
|
+
export type ContentSection = {
|
|
103
|
+
section_key: string;
|
|
104
|
+
section_type: string;
|
|
105
|
+
sort_order?: number;
|
|
106
|
+
data: Record<string, unknown>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A CMS page backed by the mw.core.site runtime baseline.
|
|
111
|
+
*
|
|
112
|
+
* content_structure is an open, ordered section/block graph — not a fixed
|
|
113
|
+
* template. The section renderer maps section_type to React components.
|
|
114
|
+
*/
|
|
115
|
+
export type ContentStructurePage = {
|
|
116
|
+
page_key: string;
|
|
117
|
+
path: string;
|
|
118
|
+
title: string;
|
|
119
|
+
status: string;
|
|
120
|
+
sort_order?: number;
|
|
121
|
+
content_structure: {
|
|
122
|
+
sections: ContentSection[];
|
|
123
|
+
};
|
|
124
|
+
seo_metadata?: SeoMetadata;
|
|
125
|
+
og_metadata?: Record<string, string>;
|
|
126
|
+
visibility?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
96
129
|
export interface PublicContentAdapter {
|
|
97
130
|
getSiteConfig(): Promise<SiteConfig>;
|
|
98
131
|
getMarketingPage(pageKey: MarketingPageKey): Promise<MarketingPage | null>;
|
|
99
132
|
listEntries(kind: PublicContentKind): Promise<PublicEntrySummary[]>;
|
|
100
133
|
getEntry(kind: PublicContentKind, slugParts: string[]): Promise<PublicEntry | null>;
|
|
134
|
+
/** Retrieve a CMS page by its URL path. Returns null if not found. */
|
|
135
|
+
getPageByPath?(path: string): Promise<ContentStructurePage | null>;
|
|
136
|
+
/** List all CMS pages, optionally filtered by status. */
|
|
137
|
+
listPages?(status?: string): Promise<ContentStructurePage[]>;
|
|
101
138
|
}
|
|
102
139
|
|
|
103
140
|
export const publicLinkSchema = z.object({
|
|
@@ -234,11 +271,33 @@ export const blogEntrySchema = blogEntrySummarySchema.extend(
|
|
|
234
271
|
|
|
235
272
|
export const publicEntrySchema = z.union([docsEntrySchema, blogEntrySchema]);
|
|
236
273
|
|
|
274
|
+
export const contentSectionSchema = z.object({
|
|
275
|
+
section_key: z.string().min(1),
|
|
276
|
+
section_type: z.string().min(1),
|
|
277
|
+
sort_order: z.number().optional(),
|
|
278
|
+
data: z.record(z.unknown()),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
export const contentStructurePageSchema = z.object({
|
|
282
|
+
page_key: z.string().min(1),
|
|
283
|
+
path: z.string().min(1),
|
|
284
|
+
title: z.string().min(1),
|
|
285
|
+
status: z.string().min(1),
|
|
286
|
+
sort_order: z.number().optional(),
|
|
287
|
+
content_structure: z.object({
|
|
288
|
+
sections: z.array(contentSectionSchema),
|
|
289
|
+
}),
|
|
290
|
+
seo_metadata: seoMetadataSchema.optional(),
|
|
291
|
+
og_metadata: z.record(z.string()).optional(),
|
|
292
|
+
visibility: z.string().optional(),
|
|
293
|
+
});
|
|
294
|
+
|
|
237
295
|
export const publicContentSnapshotSchema = z.object({
|
|
238
296
|
site: siteConfigSchema,
|
|
239
297
|
marketingPages: marketingPagesSchema,
|
|
240
298
|
docs: z.array(docsEntrySchema),
|
|
241
299
|
blog: z.array(blogEntrySchema),
|
|
300
|
+
pages: z.array(contentStructurePageSchema).optional(),
|
|
242
301
|
});
|
|
243
302
|
|
|
244
303
|
export const publicSiteSnapshotEnvironmentSchema = z.enum(["preview", "live"]);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { buildReleaseManifest, releaseManifestSchema } from "./release-manifest";
|
|
4
|
+
|
|
5
|
+
describe("release-manifest", () => {
|
|
6
|
+
it("builds a valid manifest from composition input", () => {
|
|
7
|
+
const manifest = buildReleaseManifest({
|
|
8
|
+
appPackId: "mw.core.site",
|
|
9
|
+
snapshotSchemaVersion: 1,
|
|
10
|
+
releaseClass: "static_export",
|
|
11
|
+
config: { site_title: "Test Site" },
|
|
12
|
+
pages: [
|
|
13
|
+
{
|
|
14
|
+
page_key: "home",
|
|
15
|
+
path: "/",
|
|
16
|
+
title: "Home",
|
|
17
|
+
sections: [{ section_key: "hero", section_type: "hero", data: {} }],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
page_key: "about",
|
|
21
|
+
path: "/about",
|
|
22
|
+
title: "About",
|
|
23
|
+
sections: [],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
navigation: [
|
|
27
|
+
{ nav_key: "header", slot: "header", items: [{ label: "Home" }] },
|
|
28
|
+
],
|
|
29
|
+
forms: [
|
|
30
|
+
{ form_key: "contact", schema: { fields: [{ name: "email" }] } },
|
|
31
|
+
],
|
|
32
|
+
contentDigest: "sha256-abc123",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const parsed = releaseManifestSchema.safeParse(manifest);
|
|
36
|
+
expect(parsed.success).toBe(true);
|
|
37
|
+
expect(manifest.manifest_version).toBe(1);
|
|
38
|
+
expect(manifest.composition.config_updated).toBe(true);
|
|
39
|
+
expect(manifest.composition.pages).toHaveLength(2);
|
|
40
|
+
expect(manifest.composition.pages[0]!.section_count).toBe(1);
|
|
41
|
+
expect(manifest.composition.pages[1]!.section_count).toBe(0);
|
|
42
|
+
expect(manifest.composition.navigation[0]!.item_count).toBe(1);
|
|
43
|
+
expect(manifest.composition.forms[0]!.field_count).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles null config", () => {
|
|
47
|
+
const manifest = buildReleaseManifest({
|
|
48
|
+
appPackId: "mw.core.site",
|
|
49
|
+
snapshotSchemaVersion: 1,
|
|
50
|
+
releaseClass: "ssr_container",
|
|
51
|
+
config: null,
|
|
52
|
+
pages: [],
|
|
53
|
+
navigation: [],
|
|
54
|
+
forms: [],
|
|
55
|
+
contentDigest: "sha256-empty",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(manifest.composition.config_updated).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("validates against the schema", () => {
|
|
62
|
+
const invalid = {
|
|
63
|
+
manifest_version: 2, // wrong version
|
|
64
|
+
app_pack_id: "",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const parsed = releaseManifestSchema.safeParse(invalid);
|
|
68
|
+
expect(parsed.success).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { contentStructurePageSchema, siteConfigSchema } from "./contracts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Release manifest contract for Builder output.
|
|
7
|
+
*
|
|
8
|
+
* When Builder composes a site, it emits a manifest that describes what
|
|
9
|
+
* was generated. This manifest is inspectable, diffable, and can be
|
|
10
|
+
* used to verify that releases match expectations.
|
|
11
|
+
*
|
|
12
|
+
* The manifest is NOT the release artifact itself — it describes the
|
|
13
|
+
* composition that produced the artifact.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const releaseManifestSchema = z.object({
|
|
17
|
+
/** Manifest format version. */
|
|
18
|
+
manifest_version: z.literal(1),
|
|
19
|
+
|
|
20
|
+
/** The app pack ID that was composed. */
|
|
21
|
+
app_pack_id: z.string().min(1),
|
|
22
|
+
|
|
23
|
+
/** Snapshot schema version for compatibility gating. */
|
|
24
|
+
snapshot_schema_version: z.number().int().positive(),
|
|
25
|
+
|
|
26
|
+
/** Release class from the hosted vocabulary. */
|
|
27
|
+
release_class: z.enum(["static_export", "ssr_container", "runtime_local_sidecar"]),
|
|
28
|
+
|
|
29
|
+
/** Summary of what was composed. */
|
|
30
|
+
composition: z.object({
|
|
31
|
+
config_updated: z.boolean(),
|
|
32
|
+
pages: z.array(
|
|
33
|
+
z.object({
|
|
34
|
+
page_key: z.string().min(1),
|
|
35
|
+
path: z.string().min(1),
|
|
36
|
+
title: z.string().min(1),
|
|
37
|
+
section_count: z.number().int().min(0),
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
navigation: z.array(
|
|
41
|
+
z.object({
|
|
42
|
+
nav_key: z.string().min(1),
|
|
43
|
+
slot: z.string().min(1),
|
|
44
|
+
item_count: z.number().int().min(0),
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
forms: z.array(
|
|
48
|
+
z.object({
|
|
49
|
+
form_key: z.string().min(1),
|
|
50
|
+
field_count: z.number().int().min(0),
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
/** Digest of the composed content for integrity verification. */
|
|
56
|
+
content_digest: z.string().min(1),
|
|
57
|
+
|
|
58
|
+
/** Timestamp of the manifest generation. */
|
|
59
|
+
generated_at: z.string().min(1),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type ReleaseManifest = z.infer<typeof releaseManifestSchema>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a release manifest from a Builder composition result.
|
|
66
|
+
*
|
|
67
|
+
* This is called by Builder after compose_site_from_builder_payload
|
|
68
|
+
* to produce an inspectable record of what was generated.
|
|
69
|
+
*/
|
|
70
|
+
export function buildReleaseManifest(input: {
|
|
71
|
+
appPackId: string;
|
|
72
|
+
snapshotSchemaVersion: number;
|
|
73
|
+
releaseClass: "static_export" | "ssr_container" | "runtime_local_sidecar";
|
|
74
|
+
config: Record<string, unknown> | null;
|
|
75
|
+
pages: { page_key: string; path: string; title: string; sections: unknown[] }[];
|
|
76
|
+
navigation: { nav_key: string; slot: string; items: unknown[] }[];
|
|
77
|
+
forms: { form_key: string; schema?: { fields?: unknown[] } }[];
|
|
78
|
+
contentDigest: string;
|
|
79
|
+
}): ReleaseManifest {
|
|
80
|
+
return {
|
|
81
|
+
manifest_version: 1,
|
|
82
|
+
app_pack_id: input.appPackId,
|
|
83
|
+
snapshot_schema_version: input.snapshotSchemaVersion,
|
|
84
|
+
release_class: input.releaseClass,
|
|
85
|
+
composition: {
|
|
86
|
+
config_updated: input.config !== null,
|
|
87
|
+
pages: input.pages.map((p) => ({
|
|
88
|
+
page_key: p.page_key,
|
|
89
|
+
path: p.path,
|
|
90
|
+
title: p.title,
|
|
91
|
+
section_count: p.sections?.length ?? 0,
|
|
92
|
+
})),
|
|
93
|
+
navigation: input.navigation.map((n) => ({
|
|
94
|
+
nav_key: n.nav_key,
|
|
95
|
+
slot: n.slot,
|
|
96
|
+
item_count: n.items?.length ?? 0,
|
|
97
|
+
})),
|
|
98
|
+
forms: input.forms.map((f) => ({
|
|
99
|
+
form_key: f.form_key,
|
|
100
|
+
field_count: f.schema?.fields?.length ?? 0,
|
|
101
|
+
})),
|
|
102
|
+
},
|
|
103
|
+
content_digest: input.contentDigest,
|
|
104
|
+
generated_at: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"template_id": "next-tenant-app",
|
|
3
|
-
"template_kind": "
|
|
3
|
+
"template_kind": "combined_web",
|
|
4
4
|
"template_profile": "platform_session_bff",
|
|
5
5
|
"template_bundle_ref": "runtime/builder/templates/next-tenant-app",
|
|
6
|
-
"template_version": "0.
|
|
6
|
+
"template_version": "0.3.0",
|
|
7
7
|
"materialize": {
|
|
8
8
|
"destination": "app"
|
|
9
9
|
},
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
|
|
4
|
+
const [command, ...args] = process.argv.slice(2);
|
|
5
|
+
const storybookHeapMb = process.env.MW_STORYBOOK_NODE_HEAP_MB ?? "768";
|
|
6
|
+
|
|
7
|
+
if (!command) {
|
|
8
|
+
console.error("Usage: node tools/template/with-public-site-fixture.mjs <command> [args...]");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const emptySnapshot = {
|
|
13
|
+
site: {
|
|
14
|
+
siteName: "MinuteWork Combined Starter",
|
|
15
|
+
siteDescription: "Validation fixture snapshot for the combined starter.",
|
|
16
|
+
organizationName: "MinuteWork",
|
|
17
|
+
footerBlurb: "Validation fixture content.",
|
|
18
|
+
primaryCta: {
|
|
19
|
+
label: "Sign In",
|
|
20
|
+
href: "/login",
|
|
21
|
+
},
|
|
22
|
+
secondaryCta: {
|
|
23
|
+
label: "Docs",
|
|
24
|
+
href: "/docs",
|
|
25
|
+
},
|
|
26
|
+
primaryNavigation: [
|
|
27
|
+
{ label: "Home", href: "/" },
|
|
28
|
+
{ label: "Pricing", href: "/pricing" },
|
|
29
|
+
{ label: "Docs", href: "/docs" },
|
|
30
|
+
{ label: "Blog", href: "/blog" },
|
|
31
|
+
],
|
|
32
|
+
collections: {
|
|
33
|
+
docs: {
|
|
34
|
+
eyebrow: "Docs",
|
|
35
|
+
title: "Documentation",
|
|
36
|
+
description: "Validation fixture docs collection.",
|
|
37
|
+
},
|
|
38
|
+
blog: {
|
|
39
|
+
eyebrow: "Blog",
|
|
40
|
+
title: "Updates",
|
|
41
|
+
description: "Validation fixture blog collection.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
marketingPages: [],
|
|
46
|
+
docs: [],
|
|
47
|
+
blog: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const server = createServer((request, response) => {
|
|
51
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
52
|
+
if (
|
|
53
|
+
request.method !== "GET" ||
|
|
54
|
+
requestUrl.pathname !== "/api/v1/developer/public-site/snapshots/main-site/"
|
|
55
|
+
) {
|
|
56
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
57
|
+
response.end(JSON.stringify({ detail: "Not found" }));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const environment = requestUrl.searchParams.get("environment") ?? "preview";
|
|
62
|
+
const sourceBoundary = environment === "live" ? "published_live" : "runtime_preview";
|
|
63
|
+
|
|
64
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
65
|
+
response.end(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
environment,
|
|
68
|
+
source_boundary: sourceBoundary,
|
|
69
|
+
snapshot: emptySnapshot,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const baseUrl = await new Promise((resolve, reject) => {
|
|
76
|
+
server.once("error", reject);
|
|
77
|
+
server.listen(0, "127.0.0.1", () => {
|
|
78
|
+
const address = server.address();
|
|
79
|
+
if (!address || typeof address === "string") {
|
|
80
|
+
reject(new Error("Unable to resolve validation fixture server address."));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
resolve(`http://127.0.0.1:${address.port}`);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
88
|
+
const childEnv = {
|
|
89
|
+
...process.env,
|
|
90
|
+
MW_CONTENT_API_TOKEN: "validate-content-token",
|
|
91
|
+
MW_PLATFORM_BASE_URL: baseUrl,
|
|
92
|
+
MW_PUBLIC_BASE_URL: "https://public.example.com",
|
|
93
|
+
MW_PUBLIC_SITE_ENV: "preview",
|
|
94
|
+
MW_PUBLIC_SITE_PROPERTY_KEY: "main-site",
|
|
95
|
+
};
|
|
96
|
+
if (
|
|
97
|
+
command === "storybook" &&
|
|
98
|
+
!/\bmax-old-space-size=/.test(childEnv.NODE_OPTIONS ?? "")
|
|
99
|
+
) {
|
|
100
|
+
childEnv.NODE_OPTIONS = [childEnv.NODE_OPTIONS, `--max-old-space-size=${storybookHeapMb}`]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" ");
|
|
103
|
+
}
|
|
104
|
+
const child = spawn(command, args, {
|
|
105
|
+
env: childEnv,
|
|
106
|
+
stdio: "inherit",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
child.once("error", reject);
|
|
110
|
+
child.once("exit", (code, signal) => {
|
|
111
|
+
if (signal) {
|
|
112
|
+
reject(new Error(`Validation fixture command exited via signal ${signal}.`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
resolve(code ?? 1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
process.exitCode = exitCode;
|
|
120
|
+
} finally {
|
|
121
|
+
await new Promise((resolve, reject) => {
|
|
122
|
+
server.close((error) => {
|
|
123
|
+
if (error) {
|
|
124
|
+
reject(error);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
resolve(undefined);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CliIo } from "./index.js";
|
|
2
|
+
import type { CliStatePaths, PlatformName } from "./paths.js";
|
|
3
|
+
export type AgentDependencies = {
|
|
4
|
+
closeInput?: () => void;
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
heartbeatIntervalMs?: number;
|
|
7
|
+
inputLines?: AsyncIterable<string>;
|
|
8
|
+
pollIntervalMs?: number;
|
|
9
|
+
sleepImpl?: (ms: number) => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
export declare function runAgentCommand(options: {
|
|
12
|
+
args: string[];
|
|
13
|
+
cwd: string;
|
|
14
|
+
dependencies?: AgentDependencies;
|
|
15
|
+
env: NodeJS.ProcessEnv;
|
|
16
|
+
io: CliIo;
|
|
17
|
+
paths: CliStatePaths;
|
|
18
|
+
platform: PlatformName;
|
|
19
|
+
}): Promise<number>;
|