minutework 0.1.40 → 0.1.42

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 (180) hide show
  1. package/EXTERNAL_ALPHA.md +17 -1
  2. package/README.md +21 -1
  3. package/assets/claude-local/bundle.json +2 -1
  4. package/assets/claude-local/preambles/base.md +13 -0
  5. package/assets/claude-local/preambles/mobile.md +17 -0
  6. package/assets/claude-local/preambles/tenant.md +17 -0
  7. package/assets/claude-local/preambles/vuilder.md +29 -0
  8. package/assets/claude-local/skills/README.md +5 -0
  9. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +15 -0
  10. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +25 -9
  11. package/assets/claude-local/skills/shell-architecture/SKILL.md +15 -0
  12. package/assets/claude-local/skills/vuilder-public-site-authoring/SKILL.md +10 -4
  13. package/assets/claude-local/skills/vuilder-workspace-architecture/SKILL.md +78 -0
  14. package/assets/templates/vuilder-public-site/.env.example +13 -0
  15. package/assets/templates/vuilder-public-site/README.md +15 -0
  16. package/assets/templates/vuilder-public-site/eslint.config.mjs +6 -0
  17. package/assets/templates/vuilder-public-site/next-env.d.ts +4 -0
  18. package/assets/templates/vuilder-public-site/next.config.mjs +28 -0
  19. package/assets/templates/vuilder-public-site/package.json +40 -0
  20. package/assets/templates/vuilder-public-site/postcss.config.mjs +5 -0
  21. package/assets/templates/vuilder-public-site/src/app/api/onboarding/start/route.ts +19 -0
  22. package/assets/templates/vuilder-public-site/src/app/blog/[slug]/page.tsx +25 -0
  23. package/assets/templates/vuilder-public-site/src/app/blog/page.tsx +26 -0
  24. package/assets/templates/vuilder-public-site/src/app/globals.css +103 -0
  25. package/assets/templates/vuilder-public-site/src/app/layout.tsx +18 -0
  26. package/assets/templates/vuilder-public-site/src/app/onboarding/[[...step]]/page.tsx +39 -0
  27. package/assets/templates/vuilder-public-site/src/app/page.tsx +43 -0
  28. package/assets/templates/vuilder-public-site/src/lib/env.server.test.ts +47 -0
  29. package/assets/templates/vuilder-public-site/src/lib/env.server.ts +92 -0
  30. package/assets/templates/vuilder-public-site/src/lib/platform.server.ts +47 -0
  31. package/assets/templates/vuilder-public-site/src/lib/public-dj.server.ts +86 -0
  32. package/assets/templates/vuilder-public-site/src/lib/public-dj.test.ts +8 -0
  33. package/assets/templates/vuilder-public-site/src/lib/routes.test.ts +13 -0
  34. package/assets/templates/vuilder-public-site/src/lib/routes.ts +12 -0
  35. package/assets/templates/vuilder-public-site/template.json +21 -0
  36. package/assets/templates/vuilder-public-site/test/server-only-stub.ts +1 -0
  37. package/assets/templates/vuilder-public-site/tools/env/check-dev-env.mjs +109 -0
  38. package/assets/templates/vuilder-public-site/tools/env/check-dev-env.test.ts +49 -0
  39. package/assets/templates/vuilder-public-site/tools/template/validate-template.mjs +44 -0
  40. package/assets/templates/vuilder-public-site/tsconfig.json +23 -0
  41. package/assets/templates/vuilder-public-site/vitest.config.ts +15 -0
  42. package/assets/templates/vuilder-shell/.env.example +8 -0
  43. package/assets/templates/vuilder-shell/.storybook/main.ts +19 -0
  44. package/assets/templates/vuilder-shell/.storybook/preview.tsx +38 -0
  45. package/assets/templates/vuilder-shell/README.md +49 -0
  46. package/assets/templates/vuilder-shell/components.json +21 -0
  47. package/assets/templates/vuilder-shell/eslint.config.mjs +41 -0
  48. package/assets/templates/vuilder-shell/next-env.d.ts +6 -0
  49. package/assets/templates/vuilder-shell/next.config.mjs +33 -0
  50. package/assets/templates/vuilder-shell/package.json +61 -0
  51. package/assets/templates/vuilder-shell/postcss.config.mjs +8 -0
  52. package/assets/templates/vuilder-shell/public/.gitkeep +1 -0
  53. package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.test.ts +105 -0
  54. package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.ts +63 -0
  55. package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.test.ts +63 -0
  56. package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.ts +24 -0
  57. package/assets/templates/vuilder-shell/src/app/api/auth/session/route.test.ts +70 -0
  58. package/assets/templates/vuilder-shell/src/app/api/auth/session/route.ts +27 -0
  59. package/assets/templates/vuilder-shell/src/app/app/layout.tsx +17 -0
  60. package/assets/templates/vuilder-shell/src/app/app/page.tsx +30 -0
  61. package/assets/templates/vuilder-shell/src/app/blog/[slug]/page.tsx +15 -0
  62. package/assets/templates/vuilder-shell/src/app/blog/page.tsx +15 -0
  63. package/assets/templates/vuilder-shell/src/app/docs/[...slug]/page.tsx +15 -0
  64. package/assets/templates/vuilder-shell/src/app/docs/page.tsx +15 -0
  65. package/assets/templates/vuilder-shell/src/app/globals.css +70 -0
  66. package/assets/templates/vuilder-shell/src/app/layout.tsx +69 -0
  67. package/assets/templates/vuilder-shell/src/app/login/route.test.ts +33 -0
  68. package/assets/templates/vuilder-shell/src/app/login/route.ts +21 -0
  69. package/assets/templates/vuilder-shell/src/app/page.tsx +16 -0
  70. package/assets/templates/vuilder-shell/src/app/pricing/page.tsx +15 -0
  71. package/assets/templates/vuilder-shell/src/app/providers.tsx +25 -0
  72. package/assets/templates/vuilder-shell/src/app/robots.ts +21 -0
  73. package/assets/templates/vuilder-shell/src/app/sitemap.ts +33 -0
  74. package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/connect/page.tsx +31 -0
  75. package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/page.tsx +54 -0
  76. package/assets/templates/vuilder-shell/src/components/ui/button.tsx +59 -0
  77. package/assets/templates/vuilder-shell/src/components/ui/input.tsx +21 -0
  78. package/assets/templates/vuilder-shell/src/design-system/docs/governance.mdx +26 -0
  79. package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.stories.tsx +48 -0
  80. package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.tsx +26 -0
  81. package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.stories.tsx +26 -0
  82. package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.tsx +35 -0
  83. package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
  84. package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
  85. package/assets/templates/vuilder-shell/src/design-system/primitives/button.stories.tsx +37 -0
  86. package/assets/templates/vuilder-shell/src/design-system/primitives/button.ts +1 -0
  87. package/assets/templates/vuilder-shell/src/design-system/primitives/input.stories.tsx +26 -0
  88. package/assets/templates/vuilder-shell/src/design-system/primitives/input.ts +1 -0
  89. package/assets/templates/vuilder-shell/src/design-system/recipes/chrome.ts +28 -0
  90. package/assets/templates/vuilder-shell/src/design-system/tokens/foundation.css +31 -0
  91. package/assets/templates/vuilder-shell/src/design-system/tokens/index.css +3 -0
  92. package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.json +85 -0
  93. package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.ts +87 -0
  94. package/assets/templates/vuilder-shell/src/design-system/tokens/semantic.css +105 -0
  95. package/assets/templates/vuilder-shell/src/design-system/tokens/theme.css +59 -0
  96. package/assets/templates/vuilder-shell/src/design-system/tokens/tokens.stories.tsx +71 -0
  97. package/assets/templates/vuilder-shell/src/features/dashboard/components/tenant-dashboard.tsx +134 -0
  98. package/assets/templates/vuilder-shell/src/features/public-shell/components/static-public-page.tsx +58 -0
  99. package/assets/templates/vuilder-shell/src/features/shell/components/authenticated-app-layout-shell.tsx +84 -0
  100. package/assets/templates/vuilder-shell/src/features/shell/components/private-app-shell.tsx +22 -0
  101. package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-connect-screen.tsx +89 -0
  102. package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-workspace-screen.tsx +49 -0
  103. package/assets/templates/vuilder-shell/src/lib/app-routes.test.ts +37 -0
  104. package/assets/templates/vuilder-shell/src/lib/app-routes.ts +86 -0
  105. package/assets/templates/vuilder-shell/src/lib/auth-routes.server.test.ts +26 -0
  106. package/assets/templates/vuilder-shell/src/lib/auth-routes.server.ts +53 -0
  107. package/assets/templates/vuilder-shell/src/lib/http/same-origin.test.ts +23 -0
  108. package/assets/templates/vuilder-shell/src/lib/http/same-origin.ts +18 -0
  109. package/assets/templates/vuilder-shell/src/lib/platform/client.server.test.ts +201 -0
  110. package/assets/templates/vuilder-shell/src/lib/platform/client.server.ts +540 -0
  111. package/assets/templates/vuilder-shell/src/lib/platform/contracts.ts +190 -0
  112. package/assets/templates/vuilder-shell/src/lib/platform/endpoints.server.ts +29 -0
  113. package/assets/templates/vuilder-shell/src/lib/platform/env.server.ts +82 -0
  114. package/assets/templates/vuilder-shell/src/lib/platform/route-response.ts +33 -0
  115. package/assets/templates/vuilder-shell/src/lib/platform/session.server.ts +145 -0
  116. package/assets/templates/vuilder-shell/src/lib/public-site.test.ts +20 -0
  117. package/assets/templates/vuilder-shell/src/lib/public-site.ts +48 -0
  118. package/assets/templates/vuilder-shell/src/lib/theme-config.ts +10 -0
  119. package/assets/templates/vuilder-shell/src/lib/theme.tsx +159 -0
  120. package/assets/templates/vuilder-shell/src/lib/utils.ts +6 -0
  121. package/assets/templates/vuilder-shell/template.json +28 -0
  122. package/assets/templates/vuilder-shell/template.schema.json +171 -0
  123. package/assets/templates/vuilder-shell/test/server-only-stub.ts +1 -0
  124. package/assets/templates/vuilder-shell/tools/design-system/build-token-manifest.mjs +3 -0
  125. package/assets/templates/vuilder-shell/tools/design-system/check-imports.mjs +9 -0
  126. package/assets/templates/vuilder-shell/tools/design-system/check-stories.mjs +9 -0
  127. package/assets/templates/vuilder-shell/tools/design-system/check-values.mjs +9 -0
  128. package/assets/templates/vuilder-shell/tools/design-system/checks.mjs +238 -0
  129. package/assets/templates/vuilder-shell/tools/design-system/eslint-plugin-design-system.mjs +184 -0
  130. package/assets/templates/vuilder-shell/tools/design-system/playwright.config.mjs +34 -0
  131. package/assets/templates/vuilder-shell/tools/design-system/run-checks.mjs +22 -0
  132. package/assets/templates/vuilder-shell/tools/design-system/shared.mjs +166 -0
  133. package/assets/templates/vuilder-shell/tools/design-system/visual.spec.ts +41 -0
  134. package/assets/templates/vuilder-shell/tools/template/validate-route-contract.mjs +373 -0
  135. package/assets/templates/vuilder-shell/tools/template/validate-template.mjs +45 -0
  136. package/assets/templates/vuilder-shell/tools/template/with-public-site-fixture.mjs +45 -0
  137. package/assets/templates/vuilder-shell/tsconfig.json +42 -0
  138. package/assets/templates/vuilder-shell/vitest.config.ts +23 -0
  139. package/dist/auth.js +97 -31
  140. package/dist/auth.js.map +1 -1
  141. package/dist/deploy-state.d.ts +1 -0
  142. package/dist/deploy-state.js.map +1 -1
  143. package/dist/deploy.js +18 -4
  144. package/dist/deploy.js.map +1 -1
  145. package/dist/developer-client.d.ts +2 -1
  146. package/dist/developer-client.js.map +1 -1
  147. package/dist/index.js +12 -2
  148. package/dist/index.js.map +1 -1
  149. package/dist/init-prompt.js +21 -13
  150. package/dist/init-prompt.js.map +1 -1
  151. package/dist/init.d.ts +3 -1
  152. package/dist/init.js +105 -13
  153. package/dist/init.js.map +1 -1
  154. package/dist/orchestrator-context.js +17 -5
  155. package/dist/orchestrator-context.js.map +1 -1
  156. package/dist/orchestrator-state.d.ts +2 -2
  157. package/dist/orchestrator-state.js.map +1 -1
  158. package/dist/publish.js +12 -2
  159. package/dist/publish.js.map +1 -1
  160. package/dist/sandbox.js +11 -1
  161. package/dist/sandbox.js.map +1 -1
  162. package/dist/state.d.ts +2 -0
  163. package/dist/state.js +9 -0
  164. package/dist/state.js.map +1 -1
  165. package/dist/workspace-assets.d.ts +13 -0
  166. package/dist/workspace-assets.js +86 -5
  167. package/dist/workspace-assets.js.map +1 -1
  168. package/dist/workspace-bootstrap.d.ts +47 -2
  169. package/dist/workspace-bootstrap.js +45 -1
  170. package/dist/workspace-bootstrap.js.map +1 -1
  171. package/package.json +3 -3
  172. package/vendor/workspace-mcp/context.d.ts +3 -1
  173. package/vendor/workspace-mcp/context.js +134 -21
  174. package/vendor/workspace-mcp/context.js.map +1 -1
  175. package/vendor/workspace-mcp/types.d.ts +72 -7
  176. package/vendor/workspace-mcp/types.js +8 -4
  177. package/vendor/workspace-mcp/types.js.map +1 -1
  178. package/assets/templates/fastapi-sidecar/poetry.lock +0 -757
  179. package/assets/templates/next-tenant-app/package-lock.json +0 -9682
  180. package/assets/templates/next-tenant-app/pnpm-lock.yaml +0 -6062
@@ -0,0 +1,103 @@
1
+ :root {
2
+ color-scheme: light;
3
+ --background: #f8f5ef;
4
+ --foreground: #25231f;
5
+ --muted: #6d675f;
6
+ --line: #ded6ca;
7
+ --accent: #335c67;
8
+ --accent-strong: #1f3f46;
9
+ --surface: #fffdf8;
10
+ }
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ margin: 0;
18
+ background: var(--background);
19
+ color: var(--foreground);
20
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
+ }
22
+
23
+ a {
24
+ color: inherit;
25
+ }
26
+
27
+ .site-shell {
28
+ min-height: 100vh;
29
+ }
30
+
31
+ .site-header,
32
+ .site-main,
33
+ .site-footer {
34
+ width: min(1120px, calc(100vw - 40px));
35
+ margin: 0 auto;
36
+ }
37
+
38
+ .site-header {
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: space-between;
42
+ padding: 24px 0;
43
+ border-bottom: 1px solid var(--line);
44
+ }
45
+
46
+ .site-main {
47
+ padding: 72px 0;
48
+ }
49
+
50
+ .hero {
51
+ display: grid;
52
+ gap: 24px;
53
+ max-width: 760px;
54
+ }
55
+
56
+ .eyebrow {
57
+ margin: 0;
58
+ color: var(--accent);
59
+ font-size: 0.82rem;
60
+ font-weight: 700;
61
+ letter-spacing: 0.08em;
62
+ text-transform: uppercase;
63
+ }
64
+
65
+ h1 {
66
+ margin: 0;
67
+ font-family: Georgia, "Times New Roman", serif;
68
+ font-size: clamp(3rem, 8vw, 6.75rem);
69
+ font-weight: 500;
70
+ line-height: 0.95;
71
+ }
72
+
73
+ p {
74
+ color: var(--muted);
75
+ font-size: 1.08rem;
76
+ line-height: 1.7;
77
+ }
78
+
79
+ .button {
80
+ display: inline-flex;
81
+ min-height: 44px;
82
+ width: fit-content;
83
+ align-items: center;
84
+ justify-content: center;
85
+ border: 1px solid var(--accent-strong);
86
+ background: var(--accent-strong);
87
+ color: white;
88
+ padding: 0 18px;
89
+ text-decoration: none;
90
+ }
91
+
92
+ .panel {
93
+ margin-top: 40px;
94
+ border: 1px solid var(--line);
95
+ background: var(--surface);
96
+ padding: 24px;
97
+ }
98
+
99
+ .site-footer {
100
+ padding: 32px 0;
101
+ border-top: 1px solid var(--line);
102
+ color: var(--muted);
103
+ }
@@ -0,0 +1,18 @@
1
+ import type { Metadata } from "next";
2
+
3
+ import "@/app/globals.css";
4
+ import { env } from "@/lib/env.server";
5
+
6
+ export const metadata: Metadata = {
7
+ metadataBase: new URL(env.MW_PUBLIC_BASE_URL),
8
+ title: "Vuilder Public Site",
9
+ description: "A Vuilder public website backed by public-dj content revisions.",
10
+ };
11
+
12
+ export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
13
+ return (
14
+ <html lang="en">
15
+ <body>{children}</body>
16
+ </html>
17
+ );
18
+ }
@@ -0,0 +1,39 @@
1
+ import { getSiteManifest } from "@/lib/public-dj.server";
2
+
3
+ export default async function OnboardingPage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ step?: string[] }>;
7
+ }) {
8
+ const { step = [] } = await params;
9
+ const manifest = await getSiteManifest();
10
+
11
+ return (
12
+ <main className="site-main">
13
+ <p className="eyebrow">Onboarding</p>
14
+ <h1>Start with {manifest.display_name}</h1>
15
+ <div className="panel">
16
+ <p>Current step: {step.join("/") || "intake"}</p>
17
+ <form action="/api/onboarding/start" method="post">
18
+ <p>
19
+ <label>
20
+ Email
21
+ <br />
22
+ <input name="email" type="email" required />
23
+ </label>
24
+ </p>
25
+ <p>
26
+ <label>
27
+ Name
28
+ <br />
29
+ <input name="name" type="text" />
30
+ </label>
31
+ </p>
32
+ <button className="button" type="submit">
33
+ Continue
34
+ </button>
35
+ </form>
36
+ </div>
37
+ </main>
38
+ );
39
+ }
@@ -0,0 +1,43 @@
1
+ import Link from "next/link";
2
+
3
+ import { appRoutes } from "@/lib/routes";
4
+ import { getHomePageRef, getSiteManifest } from "@/lib/public-dj.server";
5
+
6
+ export default async function HomePage() {
7
+ const [manifest, homePage] = await Promise.all([getSiteManifest(), getHomePageRef()]);
8
+ const headline = String(manifest.payload.metadata.headline ?? manifest.display_name);
9
+ const summary = String(
10
+ manifest.payload.metadata.summary ??
11
+ "A custom Vuilder public site rendered from pinned public-dj content.",
12
+ );
13
+
14
+ return (
15
+ <div className="site-shell">
16
+ <header className="site-header">
17
+ <strong>{manifest.display_name}</strong>
18
+ <nav>
19
+ <Link href={appRoutes.blog}>Blog</Link>
20
+ {" / "}
21
+ <Link href={appRoutes.onboarding()}>Start</Link>
22
+ </nav>
23
+ </header>
24
+ <main className="site-main">
25
+ <section className="hero">
26
+ <p className="eyebrow">{manifest.vertical_slug}</p>
27
+ <h1>{headline}</h1>
28
+ <p>{summary}</p>
29
+ <Link className="button" href={appRoutes.onboarding()}>
30
+ Start onboarding
31
+ </Link>
32
+ </section>
33
+ <section className="panel">
34
+ <p>
35
+ Release manifest: {manifest.content_ref} / {manifest.digest}
36
+ </p>
37
+ <p>Home page ref: {homePage?.content_ref ?? "not configured"}</p>
38
+ </section>
39
+ </main>
40
+ <footer className="site-footer">Powered by MinuteWork public-dj CMS.</footer>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { parseVuilderPublicSiteEnv } from "./env.server";
4
+
5
+ const validEnv = {
6
+ MW_AUTH_BASE_URL: "https://auth.minutework.ai",
7
+ MW_PLATFORM_BASE_URL: "https://platform.minutework.ai",
8
+ MW_PUBLIC_BASE_URL: "http://127.0.0.1:3300",
9
+ MW_PUBLIC_CONTENT_DIGEST: "public-content-digest",
10
+ MW_PUBLIC_CONTENT_REF: "runtime:test-public:content",
11
+ MW_PUBLIC_DJ_BASE_URL: "https://public-dj.minutework.ai",
12
+ MW_PUBLIC_DJ_READ_TOKEN: "public-dj-read-token",
13
+ MW_PUBLIC_RELEASE_CLASS: "ssr_container",
14
+ MW_PUBLIC_SITE_ENV: "preview",
15
+ MW_PUBLIC_SITE_PROPERTY_KEY: "vuilder-app",
16
+ MW_VUILDER_ONBOARDING_INTENT_TOKEN: "onboarding-intent-token",
17
+ };
18
+
19
+ describe("parseVuilderPublicSiteEnv", () => {
20
+ it("parses complete vuilder public-site env", () => {
21
+ expect(parseVuilderPublicSiteEnv(validEnv)).toMatchObject({
22
+ MW_AUTH_BASE_URL: "https://auth.minutework.ai",
23
+ MW_PLATFORM_BASE_URL: "https://platform.minutework.ai",
24
+ MW_PUBLIC_DJ_BASE_URL: "https://public-dj.minutework.ai",
25
+ MW_PUBLIC_SITE_PROPERTY_KEY: "vuilder-app",
26
+ });
27
+ });
28
+
29
+ it("throws a plain formatted Error for missing and invalid fields", () => {
30
+ expect(() =>
31
+ parseVuilderPublicSiteEnv({
32
+ ...validEnv,
33
+ MW_AUTH_BASE_URL: "not-a-url",
34
+ MW_PUBLIC_CONTENT_DIGEST: "",
35
+ MW_PUBLIC_CONTENT_REF: undefined,
36
+ MW_PUBLIC_DJ_READ_TOKEN: "",
37
+ }),
38
+ ).toThrowError(
39
+ [
40
+ "vuilder-app local dev is missing required sandbox environment.",
41
+ "Missing or empty: MW_PUBLIC_CONTENT_DIGEST, MW_PUBLIC_CONTENT_REF, MW_PUBLIC_DJ_READ_TOKEN",
42
+ "Invalid: MW_AUTH_BASE_URL (Invalid url)",
43
+ "Run `minutework sandbox create` or `minutework sandbox connect`",
44
+ ].join("\n"),
45
+ );
46
+ });
47
+ });
@@ -0,0 +1,92 @@
1
+ import "server-only";
2
+
3
+ import { z } from "zod";
4
+
5
+ const envSchema = z.object({
6
+ MW_PUBLIC_DJ_BASE_URL: z.string().url(),
7
+ MW_AUTH_BASE_URL: z.string().url(),
8
+ MW_PLATFORM_BASE_URL: z.string().url(),
9
+ MW_PUBLIC_BASE_URL: z.string().url(),
10
+ MW_PUBLIC_SITE_PROPERTY_KEY: z.string().min(1),
11
+ MW_PUBLIC_SITE_ENV: z.enum(["preview", "live"]).default("preview"),
12
+ MW_PUBLIC_RELEASE_CLASS: z.enum(["static_export", "ssr_container"]).default("ssr_container"),
13
+ MW_PUBLIC_CONTENT_REF: z.string().min(1),
14
+ MW_PUBLIC_CONTENT_DIGEST: z.string().min(1),
15
+ MW_PUBLIC_DJ_READ_TOKEN: z.string().min(1),
16
+ MW_VUILDER_ONBOARDING_INTENT_TOKEN: z.string().min(1),
17
+ });
18
+
19
+ export type VuilderPublicSiteEnv = z.infer<typeof envSchema>;
20
+
21
+ export function parseVuilderPublicSiteEnv(
22
+ input: Record<string, string | undefined>,
23
+ ): VuilderPublicSiteEnv {
24
+ try {
25
+ return envSchema.parse({
26
+ MW_PUBLIC_DJ_BASE_URL: input.MW_PUBLIC_DJ_BASE_URL,
27
+ MW_AUTH_BASE_URL: input.MW_AUTH_BASE_URL,
28
+ MW_PLATFORM_BASE_URL: input.MW_PLATFORM_BASE_URL,
29
+ MW_PUBLIC_BASE_URL: input.MW_PUBLIC_BASE_URL,
30
+ MW_PUBLIC_SITE_PROPERTY_KEY: input.MW_PUBLIC_SITE_PROPERTY_KEY,
31
+ MW_PUBLIC_SITE_ENV: input.MW_PUBLIC_SITE_ENV,
32
+ MW_PUBLIC_RELEASE_CLASS: input.MW_PUBLIC_RELEASE_CLASS,
33
+ MW_PUBLIC_CONTENT_REF: input.MW_PUBLIC_CONTENT_REF,
34
+ MW_PUBLIC_CONTENT_DIGEST: input.MW_PUBLIC_CONTENT_DIGEST,
35
+ MW_PUBLIC_DJ_READ_TOKEN: input.MW_PUBLIC_DJ_READ_TOKEN,
36
+ MW_VUILDER_ONBOARDING_INTENT_TOKEN: input.MW_VUILDER_ONBOARDING_INTENT_TOKEN,
37
+ });
38
+ } catch (error) {
39
+ if (error instanceof z.ZodError) {
40
+ throw formatVuilderPublicSiteEnvError(error);
41
+ }
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ function formatVuilderPublicSiteEnvError(error: z.ZodError): Error {
47
+ const missing = new Set<string>();
48
+ const invalid: string[] = [];
49
+
50
+ for (const issue of error.issues) {
51
+ const name = issue.path.join(".") || "(root)";
52
+ if (issue.code === "invalid_type" && issue.received === "undefined") {
53
+ missing.add(name);
54
+ continue;
55
+ }
56
+ if (issue.code === "too_small" && issue.type === "string") {
57
+ missing.add(name);
58
+ continue;
59
+ }
60
+ invalid.push(`${name} (${issue.message})`);
61
+ }
62
+
63
+ const lines = ["vuilder-app local dev is missing required sandbox environment."];
64
+ if (missing.size > 0) {
65
+ lines.push(`Missing or empty: ${Array.from(missing).sort().join(", ")}`);
66
+ }
67
+ if (invalid.length > 0) {
68
+ lines.push(`Invalid: ${invalid.sort().join(", ")}`);
69
+ }
70
+ lines.push(
71
+ "Run `minutework sandbox create` or `minutework sandbox connect` from the workspace root before `pnpm --dir vuilder-app dev`.",
72
+ );
73
+ return new Error(lines.join("\n"));
74
+ }
75
+
76
+ let cachedEnv: VuilderPublicSiteEnv | null = null;
77
+
78
+ export function getEnv(): VuilderPublicSiteEnv {
79
+ if (!cachedEnv) {
80
+ cachedEnv = parseVuilderPublicSiteEnv(process.env);
81
+ }
82
+ return cachedEnv;
83
+ }
84
+
85
+ export const env = new Proxy({} as VuilderPublicSiteEnv, {
86
+ get(_target, property: string | symbol) {
87
+ if (typeof property === "symbol") {
88
+ return undefined;
89
+ }
90
+ return getEnv()[property as keyof VuilderPublicSiteEnv];
91
+ },
92
+ });
@@ -0,0 +1,47 @@
1
+ import "server-only";
2
+
3
+ import { env } from "@/lib/env.server";
4
+ import { getSiteManifest } from "@/lib/public-dj.server";
5
+
6
+ export async function createCustomerVerticalSignupIntent(input: {
7
+ email?: string;
8
+ name?: string;
9
+ answers?: Record<string, unknown>;
10
+ }) {
11
+ const manifest = await getSiteManifest();
12
+ const url = new URL("/api/v1/vuilder/onboarding-intents/", env.MW_PLATFORM_BASE_URL);
13
+ const response = await fetch(url.toString(), {
14
+ method: "POST",
15
+ headers: {
16
+ accept: "application/json",
17
+ authorization: `Bearer ${env.MW_VUILDER_ONBOARDING_INTENT_TOKEN}`,
18
+ "content-type": "application/json",
19
+ },
20
+ body: JSON.stringify({
21
+ kind: "customer_vertical_signup",
22
+ source: "vuilder_public_site",
23
+ vuilder_workspace_external_key: String(
24
+ manifest.payload.metadata.vuilder_workspace_external_key ?? "",
25
+ ),
26
+ property_key: env.MW_PUBLIC_SITE_PROPERTY_KEY,
27
+ site_release_key: String(manifest.payload.metadata.site_release_key ?? ""),
28
+ public_site_manifest_ref: manifest.content_ref,
29
+ public_site_manifest_digest: manifest.digest,
30
+ vertical_package_ref: manifest.payload.vertical_package?.content_ref ?? "",
31
+ vertical_package_digest: manifest.payload.vertical_package?.digest ?? "",
32
+ onboarding_flow_ref: manifest.payload.onboarding_flow?.content_ref ?? "",
33
+ onboarding_flow_digest: manifest.payload.onboarding_flow?.digest ?? "",
34
+ intake_payload: {
35
+ email: input.email ?? "",
36
+ name: input.name ?? "",
37
+ answers: input.answers ?? {},
38
+ },
39
+ }),
40
+ });
41
+
42
+ if (!response.ok) {
43
+ throw new Error(`Unable to create onboarding intent (${response.status}).`);
44
+ }
45
+
46
+ return response.json();
47
+ }
@@ -0,0 +1,86 @@
1
+ import "server-only";
2
+
3
+ import { cache } from "react";
4
+ import { z } from "zod";
5
+
6
+ import { env } from "@/lib/env.server";
7
+
8
+ const refDigestSchema = z.object({
9
+ content_ref: z.string().min(1),
10
+ digest: z.string().min(1),
11
+ });
12
+
13
+ const marketplacePackageRefSchema = z
14
+ .string()
15
+ .min(1)
16
+ .regex(/^core:app_pack_catalog:[^:]+:[^:]+:[^:]+$/);
17
+
18
+ const verticalPackageRefDigestSchema = refDigestSchema.extend({
19
+ content_ref: marketplacePackageRefSchema,
20
+ });
21
+
22
+ const siteManifestSchema = z.object({
23
+ content_ref: z.string(),
24
+ digest: z.string(),
25
+ content_kind: z.literal("public_site_manifest"),
26
+ vertical_key: z.string(),
27
+ vertical_slug: z.string(),
28
+ property_key: z.string(),
29
+ display_name: z.string(),
30
+ payload: z.object({
31
+ pages: z.array(
32
+ refDigestSchema.extend({
33
+ page_key: z.string(),
34
+ path: z.string(),
35
+ }),
36
+ ),
37
+ blog_posts: z.array(refDigestSchema).default([]),
38
+ onboarding_flow: refDigestSchema.nullable().optional(),
39
+ vertical_package: verticalPackageRefDigestSchema.nullable().optional(),
40
+ navigation: z.array(z.unknown()).default([]),
41
+ media: z.array(z.unknown()).default([]),
42
+ theme_tokens: z.record(z.unknown()).default({}),
43
+ metadata: z.record(z.unknown()).default({}),
44
+ }),
45
+ });
46
+
47
+ export type PublicSiteManifest = z.infer<typeof siteManifestSchema>;
48
+
49
+ async function resolvePublicContent(contentRef: string, digest: string) {
50
+ const url = new URL("/api/v1/content/resolve/", env.MW_PUBLIC_DJ_BASE_URL);
51
+ url.searchParams.set("content_ref", contentRef);
52
+ url.searchParams.set("digest", digest);
53
+ const response = await fetch(url.toString(), {
54
+ headers: {
55
+ accept: "application/json",
56
+ authorization: `Bearer ${env.MW_PUBLIC_DJ_READ_TOKEN}`,
57
+ },
58
+ next: {
59
+ revalidate: env.MW_PUBLIC_SITE_ENV === "live" ? 300 : 0,
60
+ tags: ["vuilder-public-site", env.MW_PUBLIC_SITE_PROPERTY_KEY],
61
+ },
62
+ });
63
+
64
+ if (!response.ok) {
65
+ throw new Error(`Unable to resolve public-dj content (${response.status}).`);
66
+ }
67
+
68
+ return response.json();
69
+ }
70
+
71
+ export const getSiteManifest = cache(async (): Promise<PublicSiteManifest> => {
72
+ const payload = await resolvePublicContent(
73
+ env.MW_PUBLIC_CONTENT_REF,
74
+ env.MW_PUBLIC_CONTENT_DIGEST,
75
+ );
76
+ return siteManifestSchema.parse(payload);
77
+ });
78
+
79
+ export async function getHomePageRef() {
80
+ const manifest = await getSiteManifest();
81
+ return (
82
+ manifest.payload.pages.find((page) => page.path === "/") ??
83
+ manifest.payload.pages[0] ??
84
+ null
85
+ );
86
+ }
@@ -0,0 +1,8 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ describe("public-dj template contract", () => {
4
+ it("uses public site manifest refs instead of storing component code in env", () => {
5
+ expect("MW_PUBLIC_CONTENT_REF").toContain("CONTENT_REF");
6
+ expect("MW_PUBLIC_CONTENT_DIGEST").toContain("DIGEST");
7
+ });
8
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { appRoutes } from "@/lib/routes";
4
+
5
+ describe("vuilder public site routes", () => {
6
+ it("declares public website and onboarding routes only", () => {
7
+ expect(appRoutes.home).toBe("/");
8
+ expect(appRoutes.blog).toBe("/blog");
9
+ expect(appRoutes.onboarding()).toBe("/onboarding");
10
+ expect(Object.values(appRoutes).join(" ")).not.toContain("/w/");
11
+ expect(Object.values(appRoutes).join(" ")).not.toContain("/app");
12
+ });
13
+ });
@@ -0,0 +1,12 @@
1
+ import type { Route } from "next";
2
+
3
+ export const appRoutes = {
4
+ home: "/" as Route,
5
+ blog: "/blog" as Route,
6
+ blogPost(slug: string) {
7
+ return `/blog/${encodeURIComponent(slug)}` as Route;
8
+ },
9
+ onboarding(step = "") {
10
+ return step ? (`/onboarding/${encodeURIComponent(step)}` as Route) : ("/onboarding" as Route);
11
+ },
12
+ };
@@ -0,0 +1,21 @@
1
+ {
2
+ "template_id": "vuilder-public-site",
3
+ "template_kind": "public_site",
4
+ "template_profile": "public_dj_cms",
5
+ "template_bundle_ref": "runtime/builder/templates/vuilder-public-site",
6
+ "template_version": "0.1.0",
7
+ "materialize": {
8
+ "destination": "app"
9
+ },
10
+ "builder_edit_mode": "workspace_copy_only",
11
+ "seed_source": "runtime/builder/templates/vuilder-public-site",
12
+ "required_bootstrap_steps": [
13
+ "next_typegen"
14
+ ],
15
+ "runtime_contract_refs": [
16
+ "reference/mwv3-dj6-docs/published_web_property_contract.md",
17
+ "reference/mwv3-dj6-docs/runtime_app_deployment_and_provisioning_contract.md",
18
+ "reference/mwv3-dj6-docs/auth_and_credential_contract.md"
19
+ ],
20
+ "example_features": {}
21
+ }
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const templateRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
6
+
7
+ const requiredKeys = [
8
+ "MW_PUBLIC_DJ_BASE_URL",
9
+ "MW_AUTH_BASE_URL",
10
+ "MW_PLATFORM_BASE_URL",
11
+ "MW_PUBLIC_BASE_URL",
12
+ "MW_PUBLIC_SITE_PROPERTY_KEY",
13
+ "MW_PUBLIC_SITE_ENV",
14
+ "MW_PUBLIC_RELEASE_CLASS",
15
+ "MW_PUBLIC_CONTENT_REF",
16
+ "MW_PUBLIC_CONTENT_DIGEST",
17
+ "MW_PUBLIC_DJ_READ_TOKEN",
18
+ "MW_VUILDER_ONBOARDING_INTENT_TOKEN",
19
+ ];
20
+
21
+ const urlKeys = new Set([
22
+ "MW_PUBLIC_DJ_BASE_URL",
23
+ "MW_AUTH_BASE_URL",
24
+ "MW_PLATFORM_BASE_URL",
25
+ "MW_PUBLIC_BASE_URL",
26
+ ]);
27
+ const enumValues = {
28
+ MW_PUBLIC_RELEASE_CLASS: new Set(["static_export", "ssr_container"]),
29
+ MW_PUBLIC_SITE_ENV: new Set(["preview", "live"]),
30
+ };
31
+
32
+ const env = {
33
+ ...loadEnvFile(".env"),
34
+ ...loadEnvFile(".env.development"),
35
+ ...loadEnvFile(".env.local"),
36
+ ...loadEnvFile(".env.development.local"),
37
+ ...process.env,
38
+ };
39
+
40
+ const missing = requiredKeys.filter((key) => !String(env[key] ?? "").trim());
41
+ const invalid = [];
42
+
43
+ for (const key of requiredKeys) {
44
+ const value = String(env[key] ?? "").trim();
45
+ if (!value) {
46
+ continue;
47
+ }
48
+ if (urlKeys.has(key) && !isValidUrl(value)) {
49
+ invalid.push(`${key} must be a URL`);
50
+ }
51
+ const allowedValues = enumValues[key];
52
+ if (allowedValues && !allowedValues.has(value)) {
53
+ invalid.push(`${key} must be one of ${Array.from(allowedValues).join(", ")}`);
54
+ }
55
+ }
56
+
57
+ if (missing.length > 0 || invalid.length > 0) {
58
+ const lines = ["vuilder-app local dev is missing managed sandbox env."];
59
+ if (missing.length > 0) {
60
+ lines.push(`Missing or empty: ${missing.sort().join(", ")}`);
61
+ }
62
+ if (invalid.length > 0) {
63
+ lines.push(`Invalid: ${invalid.sort().join(", ")}`);
64
+ }
65
+ lines.push(
66
+ "Run `minutework sandbox create` or `minutework sandbox connect` from the workspace root before `pnpm --dir vuilder-app dev`.",
67
+ );
68
+ console.error(lines.join("\n"));
69
+ process.exit(1);
70
+ }
71
+
72
+ function loadEnvFile(relativePath) {
73
+ const envPath = path.join(templateRoot, relativePath);
74
+ if (!existsSync(envPath)) {
75
+ return {};
76
+ }
77
+ const values = {};
78
+ for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed || trimmed.startsWith("#")) {
81
+ continue;
82
+ }
83
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
84
+ if (!match) {
85
+ continue;
86
+ }
87
+ values[match[1]] = unquote(match[2].trim());
88
+ }
89
+ return values;
90
+ }
91
+
92
+ function unquote(value) {
93
+ if (
94
+ (value.startsWith('"') && value.endsWith('"')) ||
95
+ (value.startsWith("'") && value.endsWith("'"))
96
+ ) {
97
+ return value.slice(1, -1);
98
+ }
99
+ return value;
100
+ }
101
+
102
+ function isValidUrl(value) {
103
+ try {
104
+ const url = new URL(value);
105
+ return Boolean(url.protocol && url.host);
106
+ } catch {
107
+ return false;
108
+ }
109
+ }