minutework 0.1.40 → 0.1.41

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 (162) hide show
  1. package/EXTERNAL_ALPHA.md +17 -1
  2. package/README.md +21 -1
  3. package/assets/claude-local/skills/README.md +5 -0
  4. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +15 -0
  5. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +25 -9
  6. package/assets/claude-local/skills/shell-architecture/SKILL.md +15 -0
  7. package/assets/claude-local/skills/vuilder-public-site-authoring/SKILL.md +10 -4
  8. package/assets/claude-local/skills/vuilder-workspace-architecture/SKILL.md +78 -0
  9. package/assets/templates/vuilder-public-site/.env.example +11 -0
  10. package/assets/templates/vuilder-public-site/README.md +15 -0
  11. package/assets/templates/vuilder-public-site/eslint.config.mjs +6 -0
  12. package/assets/templates/vuilder-public-site/next-env.d.ts +4 -0
  13. package/assets/templates/vuilder-public-site/next.config.mjs +28 -0
  14. package/assets/templates/vuilder-public-site/package.json +39 -0
  15. package/assets/templates/vuilder-public-site/postcss.config.mjs +5 -0
  16. package/assets/templates/vuilder-public-site/src/app/api/onboarding/start/route.ts +19 -0
  17. package/assets/templates/vuilder-public-site/src/app/blog/[slug]/page.tsx +25 -0
  18. package/assets/templates/vuilder-public-site/src/app/blog/page.tsx +26 -0
  19. package/assets/templates/vuilder-public-site/src/app/globals.css +103 -0
  20. package/assets/templates/vuilder-public-site/src/app/layout.tsx +18 -0
  21. package/assets/templates/vuilder-public-site/src/app/onboarding/[[...step]]/page.tsx +39 -0
  22. package/assets/templates/vuilder-public-site/src/app/page.tsx +43 -0
  23. package/assets/templates/vuilder-public-site/src/lib/env.server.ts +31 -0
  24. package/assets/templates/vuilder-public-site/src/lib/platform.server.ts +47 -0
  25. package/assets/templates/vuilder-public-site/src/lib/public-dj.server.ts +86 -0
  26. package/assets/templates/vuilder-public-site/src/lib/public-dj.test.ts +8 -0
  27. package/assets/templates/vuilder-public-site/src/lib/routes.test.ts +13 -0
  28. package/assets/templates/vuilder-public-site/src/lib/routes.ts +12 -0
  29. package/assets/templates/vuilder-public-site/template.json +21 -0
  30. package/assets/templates/vuilder-public-site/tools/template/validate-template.mjs +44 -0
  31. package/assets/templates/vuilder-public-site/tsconfig.json +23 -0
  32. package/assets/templates/vuilder-public-site/vitest.config.ts +13 -0
  33. package/assets/templates/vuilder-shell/.env.example +8 -0
  34. package/assets/templates/vuilder-shell/.storybook/main.ts +19 -0
  35. package/assets/templates/vuilder-shell/.storybook/preview.tsx +38 -0
  36. package/assets/templates/vuilder-shell/README.md +49 -0
  37. package/assets/templates/vuilder-shell/components.json +21 -0
  38. package/assets/templates/vuilder-shell/eslint.config.mjs +41 -0
  39. package/assets/templates/vuilder-shell/next-env.d.ts +6 -0
  40. package/assets/templates/vuilder-shell/next.config.mjs +33 -0
  41. package/assets/templates/vuilder-shell/package.json +61 -0
  42. package/assets/templates/vuilder-shell/postcss.config.mjs +8 -0
  43. package/assets/templates/vuilder-shell/public/.gitkeep +1 -0
  44. package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.test.ts +105 -0
  45. package/assets/templates/vuilder-shell/src/app/api/auth/accept-shell-session/route.ts +63 -0
  46. package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.test.ts +63 -0
  47. package/assets/templates/vuilder-shell/src/app/api/auth/logout/route.ts +24 -0
  48. package/assets/templates/vuilder-shell/src/app/api/auth/session/route.test.ts +70 -0
  49. package/assets/templates/vuilder-shell/src/app/api/auth/session/route.ts +27 -0
  50. package/assets/templates/vuilder-shell/src/app/app/layout.tsx +17 -0
  51. package/assets/templates/vuilder-shell/src/app/app/page.tsx +30 -0
  52. package/assets/templates/vuilder-shell/src/app/blog/[slug]/page.tsx +15 -0
  53. package/assets/templates/vuilder-shell/src/app/blog/page.tsx +15 -0
  54. package/assets/templates/vuilder-shell/src/app/docs/[...slug]/page.tsx +15 -0
  55. package/assets/templates/vuilder-shell/src/app/docs/page.tsx +15 -0
  56. package/assets/templates/vuilder-shell/src/app/globals.css +70 -0
  57. package/assets/templates/vuilder-shell/src/app/layout.tsx +69 -0
  58. package/assets/templates/vuilder-shell/src/app/login/route.test.ts +33 -0
  59. package/assets/templates/vuilder-shell/src/app/login/route.ts +21 -0
  60. package/assets/templates/vuilder-shell/src/app/page.tsx +16 -0
  61. package/assets/templates/vuilder-shell/src/app/pricing/page.tsx +15 -0
  62. package/assets/templates/vuilder-shell/src/app/providers.tsx +25 -0
  63. package/assets/templates/vuilder-shell/src/app/robots.ts +21 -0
  64. package/assets/templates/vuilder-shell/src/app/sitemap.ts +33 -0
  65. package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/connect/page.tsx +31 -0
  66. package/assets/templates/vuilder-shell/src/app/w/[workspace_slug]/page.tsx +54 -0
  67. package/assets/templates/vuilder-shell/src/components/ui/button.tsx +59 -0
  68. package/assets/templates/vuilder-shell/src/components/ui/input.tsx +21 -0
  69. package/assets/templates/vuilder-shell/src/design-system/docs/governance.mdx +26 -0
  70. package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.stories.tsx +48 -0
  71. package/assets/templates/vuilder-shell/src/design-system/patterns/panel-frame.tsx +26 -0
  72. package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.stories.tsx +26 -0
  73. package/assets/templates/vuilder-shell/src/design-system/patterns/status-badge.tsx +35 -0
  74. package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
  75. package/assets/templates/vuilder-shell/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
  76. package/assets/templates/vuilder-shell/src/design-system/primitives/button.stories.tsx +37 -0
  77. package/assets/templates/vuilder-shell/src/design-system/primitives/button.ts +1 -0
  78. package/assets/templates/vuilder-shell/src/design-system/primitives/input.stories.tsx +26 -0
  79. package/assets/templates/vuilder-shell/src/design-system/primitives/input.ts +1 -0
  80. package/assets/templates/vuilder-shell/src/design-system/recipes/chrome.ts +28 -0
  81. package/assets/templates/vuilder-shell/src/design-system/tokens/foundation.css +31 -0
  82. package/assets/templates/vuilder-shell/src/design-system/tokens/index.css +3 -0
  83. package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.json +85 -0
  84. package/assets/templates/vuilder-shell/src/design-system/tokens/manifest.ts +87 -0
  85. package/assets/templates/vuilder-shell/src/design-system/tokens/semantic.css +105 -0
  86. package/assets/templates/vuilder-shell/src/design-system/tokens/theme.css +59 -0
  87. package/assets/templates/vuilder-shell/src/design-system/tokens/tokens.stories.tsx +71 -0
  88. package/assets/templates/vuilder-shell/src/features/dashboard/components/tenant-dashboard.tsx +134 -0
  89. package/assets/templates/vuilder-shell/src/features/public-shell/components/static-public-page.tsx +58 -0
  90. package/assets/templates/vuilder-shell/src/features/shell/components/authenticated-app-layout-shell.tsx +84 -0
  91. package/assets/templates/vuilder-shell/src/features/shell/components/private-app-shell.tsx +22 -0
  92. package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-connect-screen.tsx +89 -0
  93. package/assets/templates/vuilder-shell/src/features/vuilder-shell/components/vuilder-workspace-screen.tsx +49 -0
  94. package/assets/templates/vuilder-shell/src/lib/app-routes.test.ts +37 -0
  95. package/assets/templates/vuilder-shell/src/lib/app-routes.ts +86 -0
  96. package/assets/templates/vuilder-shell/src/lib/auth-routes.server.test.ts +26 -0
  97. package/assets/templates/vuilder-shell/src/lib/auth-routes.server.ts +53 -0
  98. package/assets/templates/vuilder-shell/src/lib/http/same-origin.test.ts +23 -0
  99. package/assets/templates/vuilder-shell/src/lib/http/same-origin.ts +18 -0
  100. package/assets/templates/vuilder-shell/src/lib/platform/client.server.test.ts +201 -0
  101. package/assets/templates/vuilder-shell/src/lib/platform/client.server.ts +540 -0
  102. package/assets/templates/vuilder-shell/src/lib/platform/contracts.ts +190 -0
  103. package/assets/templates/vuilder-shell/src/lib/platform/endpoints.server.ts +29 -0
  104. package/assets/templates/vuilder-shell/src/lib/platform/env.server.ts +82 -0
  105. package/assets/templates/vuilder-shell/src/lib/platform/route-response.ts +33 -0
  106. package/assets/templates/vuilder-shell/src/lib/platform/session.server.ts +145 -0
  107. package/assets/templates/vuilder-shell/src/lib/public-site.test.ts +20 -0
  108. package/assets/templates/vuilder-shell/src/lib/public-site.ts +48 -0
  109. package/assets/templates/vuilder-shell/src/lib/theme-config.ts +10 -0
  110. package/assets/templates/vuilder-shell/src/lib/theme.tsx +159 -0
  111. package/assets/templates/vuilder-shell/src/lib/utils.ts +6 -0
  112. package/assets/templates/vuilder-shell/template.json +28 -0
  113. package/assets/templates/vuilder-shell/template.schema.json +171 -0
  114. package/assets/templates/vuilder-shell/test/server-only-stub.ts +1 -0
  115. package/assets/templates/vuilder-shell/tools/design-system/build-token-manifest.mjs +3 -0
  116. package/assets/templates/vuilder-shell/tools/design-system/check-imports.mjs +9 -0
  117. package/assets/templates/vuilder-shell/tools/design-system/check-stories.mjs +9 -0
  118. package/assets/templates/vuilder-shell/tools/design-system/check-values.mjs +9 -0
  119. package/assets/templates/vuilder-shell/tools/design-system/checks.mjs +238 -0
  120. package/assets/templates/vuilder-shell/tools/design-system/eslint-plugin-design-system.mjs +184 -0
  121. package/assets/templates/vuilder-shell/tools/design-system/playwright.config.mjs +34 -0
  122. package/assets/templates/vuilder-shell/tools/design-system/run-checks.mjs +22 -0
  123. package/assets/templates/vuilder-shell/tools/design-system/shared.mjs +166 -0
  124. package/assets/templates/vuilder-shell/tools/design-system/visual.spec.ts +41 -0
  125. package/assets/templates/vuilder-shell/tools/template/validate-route-contract.mjs +373 -0
  126. package/assets/templates/vuilder-shell/tools/template/validate-template.mjs +45 -0
  127. package/assets/templates/vuilder-shell/tools/template/with-public-site-fixture.mjs +45 -0
  128. package/assets/templates/vuilder-shell/tsconfig.json +42 -0
  129. package/assets/templates/vuilder-shell/vitest.config.ts +23 -0
  130. package/dist/auth.js +66 -14
  131. package/dist/auth.js.map +1 -1
  132. package/dist/deploy-state.d.ts +1 -0
  133. package/dist/deploy-state.js.map +1 -1
  134. package/dist/deploy.js +18 -4
  135. package/dist/deploy.js.map +1 -1
  136. package/dist/developer-client.d.ts +1 -1
  137. package/dist/index.js +12 -2
  138. package/dist/index.js.map +1 -1
  139. package/dist/init-prompt.js +21 -13
  140. package/dist/init-prompt.js.map +1 -1
  141. package/dist/init.d.ts +3 -1
  142. package/dist/init.js +103 -12
  143. package/dist/init.js.map +1 -1
  144. package/dist/orchestrator-context.js +17 -5
  145. package/dist/orchestrator-context.js.map +1 -1
  146. package/dist/orchestrator-state.d.ts +2 -2
  147. package/dist/orchestrator-state.js.map +1 -1
  148. package/dist/publish.js +12 -2
  149. package/dist/publish.js.map +1 -1
  150. package/dist/state.d.ts +2 -0
  151. package/dist/state.js +9 -0
  152. package/dist/state.js.map +1 -1
  153. package/package.json +3 -3
  154. package/vendor/workspace-mcp/context.d.ts +3 -1
  155. package/vendor/workspace-mcp/context.js +134 -21
  156. package/vendor/workspace-mcp/context.js.map +1 -1
  157. package/vendor/workspace-mcp/types.d.ts +72 -7
  158. package/vendor/workspace-mcp/types.js +8 -4
  159. package/vendor/workspace-mcp/types.js.map +1 -1
  160. package/assets/templates/fastapi-sidecar/poetry.lock +0 -757
  161. package/assets/templates/next-tenant-app/package-lock.json +0 -9682
  162. package/assets/templates/next-tenant-app/pnpm-lock.yaml +0 -6062
@@ -0,0 +1,166 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
+ import { extname, join, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
7
+
8
+ const SOURCE_EXTENSIONS = new Set([
9
+ ".ts",
10
+ ".tsx",
11
+ ".js",
12
+ ".jsx",
13
+ ".mjs",
14
+ ".cjs",
15
+ ".css",
16
+ ".mdx",
17
+ ]);
18
+
19
+ const DEFAULT_ROOTS = ["src", "tools/design-system", ".storybook"];
20
+
21
+ export const rawColorPattern = /#(?:[\da-fA-F]{3,8})\b|(?:rgb|hsl|oklch)a?\([^)]*\)/g;
22
+ export const arbitraryVisualUtilityPattern =
23
+ /\b(?:bg|text|border|ring|shadow|from|to|via|fill|stroke)-\[[^\]]+\]/g;
24
+ export const literalStyleVisualPattern =
25
+ /style=\{\{[^}]*\b(?:color|background(?:Color)?|border(?:Color)?|boxShadow|fill|stroke)\s*:\s*['"`](?!var\(--)[^'"`]+['"`]/gs;
26
+
27
+ function toPosix(filePath) {
28
+ return filePath.split(sep).join("/");
29
+ }
30
+
31
+ export function normalizePath(filePath) {
32
+ return toPosix(relative(repoRoot, resolve(repoRoot, filePath)));
33
+ }
34
+
35
+ export function absolutePath(filePath) {
36
+ return resolve(repoRoot, filePath);
37
+ }
38
+
39
+ function walkDirectory(directoryPath) {
40
+ const entries = readdirSync(directoryPath, { withFileTypes: true });
41
+ const files = [];
42
+
43
+ for (const entry of entries) {
44
+ const absoluteEntryPath = join(directoryPath, entry.name);
45
+
46
+ if (entry.isDirectory()) {
47
+ files.push(...walkDirectory(absoluteEntryPath));
48
+ continue;
49
+ }
50
+
51
+ if (SOURCE_EXTENSIONS.has(extname(entry.name))) {
52
+ files.push(normalizePath(absoluteEntryPath));
53
+ }
54
+ }
55
+
56
+ return files;
57
+ }
58
+
59
+ export function getSourceFiles() {
60
+ const files = new Set();
61
+
62
+ for (const root of DEFAULT_ROOTS) {
63
+ const absoluteRoot = absolutePath(root);
64
+ if (!existsSync(absoluteRoot) || !statSync(absoluteRoot).isDirectory()) {
65
+ continue;
66
+ }
67
+
68
+ for (const filePath of walkDirectory(absoluteRoot)) {
69
+ files.add(filePath);
70
+ }
71
+ }
72
+
73
+ return [...files].sort();
74
+ }
75
+
76
+ export function getStagedFiles() {
77
+ try {
78
+ const output = execFileSync(
79
+ "git",
80
+ ["diff", "--name-only", "--cached", "--diff-filter=ACMR"],
81
+ { cwd: repoRoot, encoding: "utf8" },
82
+ );
83
+
84
+ return output
85
+ .split("\n")
86
+ .map((line) => line.trim())
87
+ .filter(Boolean)
88
+ .map(normalizePath);
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+
94
+ export function parseCliArgs(argv = process.argv.slice(2)) {
95
+ const options = {
96
+ staged: false,
97
+ files: [],
98
+ };
99
+
100
+ for (let index = 0; index < argv.length; index += 1) {
101
+ const arg = argv[index];
102
+ if (arg === "--staged") {
103
+ options.staged = true;
104
+ continue;
105
+ }
106
+
107
+ if (arg === "--files") {
108
+ options.files.push(...argv.slice(index + 1));
109
+ break;
110
+ }
111
+
112
+ options.files.push(arg);
113
+ }
114
+
115
+ return options;
116
+ }
117
+
118
+ export function resolveTargetFiles({ staged = false, files = [] } = {}) {
119
+ const candidates =
120
+ files.length > 0 ? files : staged ? getStagedFiles() : getSourceFiles();
121
+ const normalizedCandidates = candidates
122
+ .map(normalizePath)
123
+ .filter((candidate) => SOURCE_EXTENSIONS.has(extname(candidate)));
124
+
125
+ if (normalizedCandidates.length > 0) {
126
+ return [...new Set(normalizedCandidates)].sort();
127
+ }
128
+
129
+ return getSourceFiles();
130
+ }
131
+
132
+ export function isTokenFile(filePath) {
133
+ const normalizedPath = normalizePath(filePath);
134
+ return (
135
+ normalizedPath.startsWith("src/design-system/tokens/") &&
136
+ (normalizedPath.endsWith(".css") ||
137
+ normalizedPath.endsWith("/manifest.ts") ||
138
+ normalizedPath.endsWith("/manifest.json"))
139
+ );
140
+ }
141
+
142
+ export function isFeatureSurfaceFile(filePath) {
143
+ const normalizedPath = normalizePath(filePath);
144
+ return (
145
+ normalizedPath.startsWith("src/app/") ||
146
+ normalizedPath.startsWith("src/features/")
147
+ );
148
+ }
149
+
150
+ export function isGovernedVisualFile(filePath) {
151
+ const normalizedPath = normalizePath(filePath);
152
+ return (
153
+ normalizedPath.startsWith("src/app/") ||
154
+ normalizedPath.startsWith("src/features/") ||
155
+ normalizedPath.startsWith("src/design-system/")
156
+ );
157
+ }
158
+
159
+ export function isLegacyUiImport(specifier) {
160
+ return specifier.startsWith("@/components/ui/");
161
+ }
162
+
163
+ export function matchesPattern(pattern, value) {
164
+ pattern.lastIndex = 0;
165
+ return pattern.test(value);
166
+ }
@@ -0,0 +1,41 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ const storyCases = [
4
+ {
5
+ id: "design-system-primitives-button--primary",
6
+ name: "button-primary.png",
7
+ viewport: { width: 480, height: 220 },
8
+ },
9
+ {
10
+ id: "design-system-primitives-input--default",
11
+ name: "input-default.png",
12
+ viewport: { width: 520, height: 220 },
13
+ },
14
+ {
15
+ id: "design-system-patterns-panel-frame--raised",
16
+ name: "panel-frame-raised.png",
17
+ viewport: { width: 560, height: 340 },
18
+ },
19
+ {
20
+ id: "design-system-patterns-status-badge--all-tones",
21
+ name: "status-badge-tones.png",
22
+ viewport: { width: 520, height: 220 },
23
+ },
24
+ {
25
+ id: "design-system-patterns-theme-mode-toggle--default",
26
+ name: "theme-mode-toggle-default.png",
27
+ viewport: { width: 420, height: 260 },
28
+ },
29
+ ];
30
+
31
+ for (const storyCase of storyCases) {
32
+ test(storyCase.id, async ({ page }) => {
33
+ await page.setViewportSize(storyCase.viewport);
34
+ await page.goto(`/iframe.html?id=${storyCase.id}&viewMode=story`);
35
+ await page.addStyleTag({
36
+ content:
37
+ "*{animation:none !important;transition:none !important;caret-color:transparent !important;}",
38
+ });
39
+ await expect(page.locator("#storybook-root")).toHaveScreenshot(storyCase.name);
40
+ });
41
+ }
@@ -0,0 +1,373 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const templateRoot = path.resolve(__dirname, "..", "..");
7
+
8
+ const requiredPaths = [
9
+ "src/app/w/[workspace_slug]/page.tsx",
10
+ "src/app/w/[workspace_slug]/connect/page.tsx",
11
+ "src/app/app/page.tsx",
12
+ "src/app/api/auth/accept-shell-session/route.ts",
13
+ "src/app/api/auth/session/route.ts",
14
+ "src/app/api/auth/logout/route.ts",
15
+ "src/app/login/route.ts",
16
+ "src/features/vuilder-shell/components/vuilder-workspace-screen.tsx",
17
+ "src/features/vuilder-shell/components/vuilder-connect-screen.tsx",
18
+ "src/lib/auth-routes.server.ts",
19
+ "src/lib/app-routes.ts",
20
+ "src/lib/platform/client.server.ts",
21
+ "src/lib/platform/contracts.ts",
22
+ "src/lib/platform/session.server.ts",
23
+ ];
24
+
25
+ const forbiddenPaths = [
26
+ "src/app/api/auth/login/route.ts",
27
+ "src/features/auth/components/login-screen.tsx",
28
+ ];
29
+
30
+ const missingPaths = requiredPaths.filter(
31
+ (relativePath) => !existsSync(path.join(templateRoot, relativePath)),
32
+ );
33
+
34
+ if (missingPaths.length > 0) {
35
+ throw new Error(
36
+ `Vuilder shell route contract is incomplete.\n${missingPaths
37
+ .map((relativePath) => `- ${relativePath}`)
38
+ .join("\n")}`,
39
+ );
40
+ }
41
+
42
+ const presentForbiddenPaths = forbiddenPaths.filter((relativePath) =>
43
+ existsSync(path.join(templateRoot, relativePath)),
44
+ );
45
+
46
+ if (presentForbiddenPaths.length > 0) {
47
+ throw new Error(
48
+ `vuilder-shell must hand credential auth to central SSO, not local password routes.\n${presentForbiddenPaths
49
+ .map((relativePath) => `- ${relativePath}`)
50
+ .join("\n")}`,
51
+ );
52
+ }
53
+
54
+ const sourcePaths = collectNamedFiles(
55
+ path.join(templateRoot, "src"),
56
+ new Set(["ts", "tsx"]),
57
+ ).filter((absolutePath) => !absolutePath.includes(".test."));
58
+ const webAuthReferences = sourcePaths.filter((absolutePath) =>
59
+ readFileSync(absolutePath, "utf8").includes("@minutework/web-auth"),
60
+ );
61
+
62
+ if (webAuthReferences.length > 0) {
63
+ throw new Error(
64
+ `vuilder-shell must use platform member SSO, not tenant customer web-auth.\n${webAuthReferences
65
+ .map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
66
+ .join("\n")}`,
67
+ );
68
+ }
69
+
70
+ const credentialAuthReferences = sourcePaths.filter((absolutePath) => {
71
+ const source = readFileSync(absolutePath, "utf8");
72
+ return (
73
+ source.includes("loginWithPassword") ||
74
+ source.includes("loginRequestSchema") ||
75
+ source.includes("platformAuthEndpoints.login") ||
76
+ source.includes("/api/auth/login")
77
+ );
78
+ });
79
+
80
+ if (credentialAuthReferences.length > 0) {
81
+ throw new Error(
82
+ `vuilder-shell must redirect credential auth through central SSO.\n${credentialAuthReferences
83
+ .map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
84
+ .join("\n")}`,
85
+ );
86
+ }
87
+
88
+ const rawPlatformCredentialReferences = sourcePaths.filter((absolutePath) => {
89
+ const relativePath = path.relative(templateRoot, absolutePath);
90
+ const source = readFileSync(absolutePath, "utf8");
91
+ if (relativePath === path.join("src", "lib", "platform", "session.server.ts")) {
92
+ return false;
93
+ }
94
+ return [
95
+ "mw_platform_session",
96
+ "mw_platform_csrf",
97
+ "sessionid=",
98
+ "csrftoken",
99
+ "X-CSRFToken",
100
+ "platformAuthEndpoints.currentSession",
101
+ "platformAuthEndpoints.logout",
102
+ "logoutPlatformSession",
103
+ "ensureCsrfToken",
104
+ ].some((pattern) => source.includes(pattern));
105
+ });
106
+
107
+ if (rawPlatformCredentialReferences.length > 0) {
108
+ throw new Error(
109
+ `vuilder-shell must consume the tenant-bound shell token, not raw platform session credentials.\n${rawPlatformCredentialReferences
110
+ .map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
111
+ .join("\n")}`,
112
+ );
113
+ }
114
+
115
+ const sharedShellCookieDomainReferences = sourcePaths.filter((absolutePath) => {
116
+ const source = readFileSync(absolutePath, "utf8");
117
+ return source.includes("MW_VUILDER_SHELL_COOKIE_DOMAIN");
118
+ });
119
+
120
+ if (sharedShellCookieDomainReferences.length > 0) {
121
+ throw new Error(
122
+ `vuilder-shell must set shell sessions as host-only cookies on the branded shell origin.\n${sharedShellCookieDomainReferences
123
+ .map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
124
+ .join("\n")}`,
125
+ );
126
+ }
127
+
128
+ const platformClientSource = readFileSync(
129
+ path.join(templateRoot, "src", "lib", "platform", "client.server.ts"),
130
+ "utf8",
131
+ );
132
+ for (const requiredSnippet of [
133
+ "platformAuthEndpoints.shellTokenExchange",
134
+ "platformAuthEndpoints.shellTokenContext",
135
+ "platformAuthEndpoints.shellTokenRevoke",
136
+ "exchangeVuilderShellSessionHandoffCode",
137
+ "revokeVuilderShellSession",
138
+ "X-Vuilder-Shell-Session",
139
+ "loadWorkspaceShellSession",
140
+ "shellContextCanViewWorkspace",
141
+ "shellContextCanOpenWorkspace",
142
+ ]) {
143
+ if (!platformClientSource.includes(requiredSnippet)) {
144
+ throw new Error(
145
+ `src/lib/platform/client.server.ts must resolve server-authored shell token context (${requiredSnippet}).`,
146
+ );
147
+ }
148
+ }
149
+
150
+ const workspaceRoutesRoot = path.join(templateRoot, "src", "app", "w");
151
+ const workspaceRouteHandlerPaths = collectNamedFiles(
152
+ workspaceRoutesRoot,
153
+ new Set(["route.ts", "route.tsx"]),
154
+ );
155
+ if (workspaceRouteHandlerPaths.length > 0) {
156
+ throw new Error(
157
+ `Route handlers under /w bypass the platform shell/session contract.\n${workspaceRouteHandlerPaths
158
+ .map((absolutePath) => `- ${path.relative(templateRoot, absolutePath)}`)
159
+ .join("\n")}`,
160
+ );
161
+ }
162
+
163
+ const routesSource = readFileSync(path.join(templateRoot, "src", "lib", "app-routes.ts"), "utf8");
164
+ for (const requiredExport of [
165
+ "workspaceShell",
166
+ "workspaceConnect",
167
+ "loginForWorkspace",
168
+ "loginForWorkspaceConnect",
169
+ ]) {
170
+ if (!routesSource.includes(requiredExport)) {
171
+ throw new Error(`src/lib/app-routes.ts must expose ${requiredExport}.`);
172
+ }
173
+ }
174
+
175
+ const loginRouteSource = readFileSync(
176
+ path.join(templateRoot, "src", "app", "login", "route.ts"),
177
+ "utf8",
178
+ );
179
+ if (
180
+ !loginRouteSource.includes("buildCentralSsoLoginUrl") ||
181
+ !loginRouteSource.includes("createVuilderShellHandoffState") ||
182
+ !loginRouteSource.includes("applyVuilderShellHandoffStateToResponse") ||
183
+ !loginRouteSource.includes("NextResponse.redirect")
184
+ ) {
185
+ throw new Error("/login must set shell handoff state before central SSO redirect.");
186
+ }
187
+
188
+ const workspacePageSource = readFileSync(
189
+ path.join(templateRoot, "src", "app", "w", "[workspace_slug]", "page.tsx"),
190
+ "utf8",
191
+ );
192
+ if (!workspacePageSource.includes("VuilderWorkspaceScreen")) {
193
+ throw new Error("/w/[workspace_slug] must render VuilderWorkspaceScreen.");
194
+ }
195
+ for (const requiredSnippet of [
196
+ "loadWorkspaceShellSession",
197
+ "workspaceCanView",
198
+ "workspaceCanOpen",
199
+ "appRoutes.loginForWorkspace(workspaceSlug)",
200
+ "appRoutes.workspaceConnect(workspaceSlug)",
201
+ ]) {
202
+ if (!workspacePageSource.includes(requiredSnippet)) {
203
+ throw new Error(
204
+ `/w/[workspace_slug] must preserve the platform-authored shell context and requested workspace (${requiredSnippet}).`,
205
+ );
206
+ }
207
+ }
208
+
209
+ const connectPageSource = readFileSync(
210
+ path.join(templateRoot, "src", "app", "w", "[workspace_slug]", "connect", "page.tsx"),
211
+ "utf8",
212
+ );
213
+ if (!connectPageSource.includes("VuilderConnectScreen")) {
214
+ throw new Error("/w/[workspace_slug]/connect must render VuilderConnectScreen.");
215
+ }
216
+ for (const requiredSnippet of [
217
+ "loadWorkspaceShellSession",
218
+ "workspaceCanView",
219
+ "notFound()",
220
+ ]) {
221
+ if (!connectPageSource.includes(requiredSnippet)) {
222
+ throw new Error(
223
+ `/w/[workspace_slug]/connect must fail closed for authenticated shell-context mismatch (${requiredSnippet}).`,
224
+ );
225
+ }
226
+ }
227
+
228
+ const appPageSource = readFileSync(
229
+ path.join(templateRoot, "src", "app", "app", "page.tsx"),
230
+ "utf8",
231
+ );
232
+ for (const requiredSnippet of [
233
+ "loadCurrentWorkspaceShellSession",
234
+ "workspaceCanView",
235
+ "workspaceCanOpen",
236
+ "notFound()",
237
+ "appRoutes.workspaceConnect(membership.workspace_slug)",
238
+ "redirect(appRoutes.workspaceShell(membership.workspace_slug))",
239
+ ]) {
240
+ if (!appPageSource.includes(requiredSnippet)) {
241
+ throw new Error(
242
+ `/app must apply the same platform-authored shell context gate before redirecting (${requiredSnippet}).`,
243
+ );
244
+ }
245
+ }
246
+ if (appPageSource.includes("PrivateAppShell") || appPageSource.includes("TenantDashboard")) {
247
+ throw new Error("/app must not render private workspace UI outside the /w gate.");
248
+ }
249
+
250
+ const acceptShellSessionRouteSource = readFileSync(
251
+ path.join(
252
+ templateRoot,
253
+ "src",
254
+ "app",
255
+ "api",
256
+ "auth",
257
+ "accept-shell-session",
258
+ "route.ts",
259
+ ),
260
+ "utf8",
261
+ );
262
+ for (const requiredSnippet of [
263
+ "exchangeVuilderShellSessionHandoffCode",
264
+ "applyVuilderShellSessionToResponse",
265
+ "readVuilderShellHandoffState",
266
+ "clearVuilderShellHandoffStateFromResponse",
267
+ "code",
268
+ "state",
269
+ "return_to",
270
+ "NextResponse.redirect",
271
+ ]) {
272
+ if (!acceptShellSessionRouteSource.includes(requiredSnippet)) {
273
+ throw new Error(
274
+ `/api/auth/accept-shell-session must consume SSO handoff on the shell origin (${requiredSnippet}).`,
275
+ );
276
+ }
277
+ }
278
+ if (acceptShellSessionRouteSource.includes("clearPlatformSessionFromResponse")) {
279
+ throw new Error(
280
+ "/api/auth/accept-shell-session must not clear an existing shell session when a forged handoff fails.",
281
+ );
282
+ }
283
+
284
+ const sessionRouteSource = readFileSync(
285
+ path.join(templateRoot, "src", "app", "api", "auth", "session", "route.ts"),
286
+ "utf8",
287
+ );
288
+ const sessionNoStoreHeaderCount = Array.from(
289
+ sessionRouteSource.matchAll(/Cache-Control["'],\s*["']no-store/g),
290
+ ).length;
291
+ if (sessionNoStoreHeaderCount < 2) {
292
+ throw new Error(
293
+ "/api/auth/session must set Cache-Control: no-store on success and error responses.",
294
+ );
295
+ }
296
+
297
+ const logoutRouteSource = readFileSync(
298
+ path.join(templateRoot, "src", "app", "api", "auth", "logout", "route.ts"),
299
+ "utf8",
300
+ );
301
+ for (const requiredSnippet of [
302
+ "hasTrustedBrowserOrigin",
303
+ "readPlatformAuthState",
304
+ "revokeVuilderShellSession",
305
+ "clearPlatformSessionFromResponse",
306
+ ]) {
307
+ if (!logoutRouteSource.includes(requiredSnippet)) {
308
+ throw new Error(
309
+ `/api/auth/logout must preserve the shell-token logout contract (${requiredSnippet}).`,
310
+ );
311
+ }
312
+ }
313
+ const logoutPostHandlerIndex = logoutRouteSource.indexOf("export async function POST");
314
+ const logoutPostHandlerSource =
315
+ logoutPostHandlerIndex >= 0 ? logoutRouteSource.slice(logoutPostHandlerIndex) : "";
316
+ const originGuardIndex = logoutPostHandlerSource.indexOf("hasTrustedBrowserOrigin");
317
+ const revokeIndex = logoutPostHandlerSource.indexOf("revokeVuilderShellSession");
318
+ const clearCookieIndex = logoutPostHandlerSource.indexOf("clearPlatformSessionFromResponse");
319
+ if (
320
+ originGuardIndex < 0 ||
321
+ revokeIndex < 0 ||
322
+ clearCookieIndex < 0 ||
323
+ originGuardIndex > revokeIndex ||
324
+ revokeIndex > clearCookieIndex
325
+ ) {
326
+ throw new Error(
327
+ "/api/auth/logout must check the browser origin, revoke the shell token, then clear shell cookies.",
328
+ );
329
+ }
330
+
331
+ const connectScreenSource = readFileSync(
332
+ path.join(
333
+ templateRoot,
334
+ "src",
335
+ "features",
336
+ "vuilder-shell",
337
+ "components",
338
+ "vuilder-connect-screen.tsx",
339
+ ),
340
+ "utf8",
341
+ );
342
+ if (!connectScreenSource.includes("appRoutes.loginForWorkspaceConnect(workspaceSlug)")) {
343
+ throw new Error(
344
+ "VuilderConnectScreen must preserve workspace context when linking to login.",
345
+ );
346
+ }
347
+
348
+ console.log("vuilder shell route contract is valid");
349
+
350
+ function collectNamedFiles(directory, names) {
351
+ const entries = readdirSync(directory, { withFileTypes: true });
352
+ const files = [];
353
+
354
+ for (const entry of entries) {
355
+ const absolutePath = path.join(directory, entry.name);
356
+ if (entry.isDirectory()) {
357
+ files.push(...collectNamedFiles(absolutePath, names));
358
+ continue;
359
+ }
360
+ if (
361
+ !entry.isFile() ||
362
+ (!names.has(entry.name) && !names.has(path.extname(entry.name).slice(1)))
363
+ ) {
364
+ continue;
365
+ }
366
+ if (!statSync(absolutePath).isFile()) {
367
+ continue;
368
+ }
369
+ files.push(absolutePath);
370
+ }
371
+
372
+ return files;
373
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import Ajv2020 from "ajv/dist/2020.js";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const templateRoot = path.resolve(__dirname, "..", "..");
9
+ const bundledSchemaPath = path.join(templateRoot, "template.schema.json");
10
+ const sharedSchemaPath = path.resolve(templateRoot, "..", "template.schema.json");
11
+ const manifestPath = path.join(templateRoot, "template.json");
12
+
13
+ const bundledSchemaSource = readFileSync(bundledSchemaPath, "utf8");
14
+ const sharedSchemaSource = existsSync(sharedSchemaPath)
15
+ ? readFileSync(sharedSchemaPath, "utf8")
16
+ : null;
17
+
18
+ function normalizeJson(source) {
19
+ return JSON.stringify(JSON.parse(source));
20
+ }
21
+
22
+ if (
23
+ sharedSchemaSource &&
24
+ normalizeJson(sharedSchemaSource) !== normalizeJson(bundledSchemaSource)
25
+ ) {
26
+ throw new Error(
27
+ `Bundled template schema is out of sync with the shared schema at ${sharedSchemaPath}`,
28
+ );
29
+ }
30
+
31
+ const schema = JSON.parse(sharedSchemaSource ?? bundledSchemaSource);
32
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
33
+
34
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
35
+ const validate = ajv.compile(schema);
36
+
37
+ if (!validate(manifest)) {
38
+ const details = (validate.errors ?? [])
39
+ .map((error) => `${error.instancePath || "/"} ${error.message ?? "invalid"}`)
40
+ .join("\n");
41
+
42
+ throw new Error(`Template manifest validation failed.\n${details}`);
43
+ }
44
+
45
+ console.log("template.json is valid");
@@ -0,0 +1,45 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const [command, ...args] = process.argv.slice(2);
4
+ const storybookHeapMb = process.env.MW_STORYBOOK_NODE_HEAP_MB ?? "768";
5
+
6
+ if (!command) {
7
+ console.error("Usage: node tools/template/with-public-site-fixture.mjs <command> [args...]");
8
+ process.exit(1);
9
+ }
10
+
11
+ const childEnv = {
12
+ ...process.env,
13
+ NEXT_PUBLIC_MW_APP_ID: process.env.NEXT_PUBLIC_MW_APP_ID ?? "vuilder.vertical",
14
+ MW_TEMPLATE_APP_NAME: process.env.MW_TEMPLATE_APP_NAME ?? "Vuilder Shell",
15
+ MW_PUBLIC_BASE_URL: process.env.MW_PUBLIC_BASE_URL ?? "https://shell.example.com",
16
+ };
17
+
18
+ if (
19
+ command === "storybook" &&
20
+ !/\bmax-old-space-size=/.test(childEnv.NODE_OPTIONS ?? "")
21
+ ) {
22
+ childEnv.NODE_OPTIONS = [childEnv.NODE_OPTIONS, `--max-old-space-size=${storybookHeapMb}`]
23
+ .filter(Boolean)
24
+ .join(" ");
25
+ }
26
+
27
+ const child = spawn(command, args, {
28
+ env: childEnv,
29
+ stdio: "inherit",
30
+ });
31
+
32
+ child.once("error", (error) => {
33
+ console.error(error instanceof Error ? error.message : String(error));
34
+ process.exitCode = 1;
35
+ });
36
+
37
+ child.once("exit", (code, signal) => {
38
+ if (signal) {
39
+ console.error(`Validation fixture command exited via signal ${signal}.`);
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+
44
+ process.exitCode = code ?? 1;
45
+ });