bsmnt 0.0.0

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 (98) hide show
  1. package/.changeset/2026-02-11-test-patch-bump.md +5 -0
  2. package/.changeset/README.md +10 -0
  3. package/.changeset/config.json +16 -0
  4. package/.cursor/rules/README.md +184 -0
  5. package/.cursor/rules/architecture.mdc +437 -0
  6. package/.cursor/rules/components.mdc +436 -0
  7. package/.cursor/rules/integrations.mdc +447 -0
  8. package/.cursor/rules/main.mdc +278 -0
  9. package/.cursor/rules/styling.mdc +433 -0
  10. package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  11. package/.github/workflows/.gitkeep +0 -0
  12. package/.github/workflows/ci.yml +37 -0
  13. package/.github/workflows/release.yml +54 -0
  14. package/.tldr/cache/call_graph.json +7 -0
  15. package/.tldr/languages.json +6 -0
  16. package/.tldr/status +1 -0
  17. package/.tldrignore +84 -0
  18. package/.vscode/extensions.json +20 -0
  19. package/.vscode/settings.json +98 -0
  20. package/CHANGELOG.md +13 -0
  21. package/CLAUDE.md +138 -0
  22. package/README.md +176 -0
  23. package/bin/index.js +262 -0
  24. package/biome.json +44 -0
  25. package/bun.lock +496 -0
  26. package/changelog/04-02-26.md +86 -0
  27. package/changelog/05-02-26.md +101 -0
  28. package/changelog/09-02-26.md +83 -0
  29. package/docs/fix-studio-hydration.md +46 -0
  30. package/docs/plans/2026-01-29-sanity-smart-merge-design.md +196 -0
  31. package/docs/plans/2026-01-29-sanity-smart-merge-implementation.md +695 -0
  32. package/docs/sanity-setup-steps.md +199 -0
  33. package/integrations/basehub/README.md +3 -0
  34. package/integrations/sanity/app/api/draft-mode/disable/route.ts +7 -0
  35. package/integrations/sanity/app/api/draft-mode/enable/route.ts +21 -0
  36. package/integrations/sanity/app/api/revalidate/route.ts +37 -0
  37. package/integrations/sanity/app/layout.tsx +111 -0
  38. package/integrations/sanity/app/sitemap.ts +80 -0
  39. package/integrations/sanity/app/studio/[[...tool]]/page.tsx +8 -0
  40. package/integrations/sanity/app/studio/layout.tsx +7 -0
  41. package/integrations/sanity/components/ui/sanity-image/index.tsx +37 -0
  42. package/integrations/sanity/lib/integrations/README.md +58 -0
  43. package/integrations/sanity/lib/integrations/check-integration.ts +62 -0
  44. package/integrations/sanity/lib/integrations/sanity/README.md +144 -0
  45. package/integrations/sanity/lib/integrations/sanity/client.ts +30 -0
  46. package/integrations/sanity/lib/integrations/sanity/components/disable-draft-mode.tsx +29 -0
  47. package/integrations/sanity/lib/integrations/sanity/components/rich-text.tsx +73 -0
  48. package/integrations/sanity/lib/integrations/sanity/env.ts +38 -0
  49. package/integrations/sanity/lib/integrations/sanity/live/index.tsx +34 -0
  50. package/integrations/sanity/lib/integrations/sanity/queries.ts +99 -0
  51. package/integrations/sanity/lib/integrations/sanity/sanity.cli.ts +20 -0
  52. package/integrations/sanity/lib/integrations/sanity/sanity.config.ts +94 -0
  53. package/integrations/sanity/lib/integrations/sanity/sanity.types.ts +337 -0
  54. package/integrations/sanity/lib/integrations/sanity/schema.json +1850 -0
  55. package/integrations/sanity/lib/integrations/sanity/schemas/article.ts +132 -0
  56. package/integrations/sanity/lib/integrations/sanity/schemas/example.ts +203 -0
  57. package/integrations/sanity/lib/integrations/sanity/schemas/index.ts +37 -0
  58. package/integrations/sanity/lib/integrations/sanity/schemas/link.ts +127 -0
  59. package/integrations/sanity/lib/integrations/sanity/schemas/metadata.ts +68 -0
  60. package/integrations/sanity/lib/integrations/sanity/schemas/navigation.ts +39 -0
  61. package/integrations/sanity/lib/integrations/sanity/schemas/page.ts +77 -0
  62. package/integrations/sanity/lib/integrations/sanity/schemas/richText.ts +59 -0
  63. package/integrations/sanity/lib/integrations/sanity/structure.ts +5 -0
  64. package/integrations/sanity/lib/integrations/sanity/utils/image.ts +11 -0
  65. package/integrations/sanity/lib/integrations/sanity/utils/link.ts +61 -0
  66. package/integrations/sanity/lib/scripts/copy-sanity-mcp.ts +23 -0
  67. package/integrations/sanity/lib/scripts/generate-page.ts +310 -0
  68. package/integrations/sanity/lib/utils/metadata.ts +190 -0
  69. package/layers/experiment/components/layout/header/index.tsx +58 -0
  70. package/layers/experiment/components/layout/navigation-menu.tsx +127 -0
  71. package/layers/experiment/lib/constants.ts +12 -0
  72. package/layers/webgl/app/page.tsx +10 -0
  73. package/layers/webgl/components/webgl/canvas/dynamic.tsx +34 -0
  74. package/layers/webgl/components/webgl/canvas/index.tsx +43 -0
  75. package/layers/webgl/components/webgl/components/scene/index.tsx +21 -0
  76. package/layers/webgpu/.gitkeep +0 -0
  77. package/package.json +44 -0
  78. package/plugins/README.md +21 -0
  79. package/plugins/no-anchor-element.grit +11 -0
  80. package/plugins/no-relative-parent-imports.grit +6 -0
  81. package/plugins/no-unnecessary-forwardref.grit +5 -0
  82. package/src/commands/add-integration.js +325 -0
  83. package/src/commands/create.js +415 -0
  84. package/src/commands/setup-sanity.js +426 -0
  85. package/src/commands/worktree.js +805 -0
  86. package/src/mergers/check-integration-merger.js +105 -0
  87. package/src/mergers/config.js +137 -0
  88. package/src/mergers/index.js +355 -0
  89. package/src/mergers/layout-merger.js +223 -0
  90. package/src/mergers/next-config-merger.js +63 -0
  91. package/src/mergers/sitemap-merger.js +121 -0
  92. package/tasks/prd-next-starter-dynamic-layers.md +184 -0
  93. package/tasks/prd.json +153 -0
  94. package/tasks/progress.txt +115 -0
  95. package/template-hooks/use-battery.ts +126 -0
  96. package/template-hooks/use-device-perf.ts +184 -0
  97. package/template-hooks/use-intersection-observer.ts +32 -0
  98. package/template-hooks/use-media.ts +33 -0
@@ -0,0 +1,223 @@
1
+ // src/mergers/layout-merger.js
2
+
3
+ import path from "node:path";
4
+ import fs from "fs-extra";
5
+
6
+ /**
7
+ * Sanity-specific imports to inject into layout.
8
+ * Only two lightweight components — no dynamic functions (draftMode, headers, etc.)
9
+ */
10
+ const SANITY_IMPORTS = `import { SanityStudioGuard } from "@/components/sanity/studio-guard"
11
+ import { SanityVisualEditing } from "@/components/sanity/visual-editing"`;
12
+
13
+ /**
14
+ * Client component that hides site chrome on /studio routes.
15
+ * Uses useSelectedLayoutSegment() — a stable Next.js client API.
16
+ */
17
+ const STUDIO_GUARD_COMPONENT = `"use client"
18
+
19
+ import { useSelectedLayoutSegment } from "next/navigation"
20
+
21
+ export function SanityStudioGuard({ children }: { children: React.ReactNode }) {
22
+ const segment = useSelectedLayoutSegment()
23
+ if (segment === "studio") return null
24
+ return <>{children}</>
25
+ }
26
+ `;
27
+
28
+ /**
29
+ * Server component that handles Sanity visual editing.
30
+ * Keeps draftMode() inside its own <Suspense> boundary so the root layout
31
+ * never calls dynamic functions directly — preserving static rendering.
32
+ */
33
+ const VISUAL_EDITING_COMPONENT = `import { Suspense } from "react"
34
+ import { draftMode } from "next/headers"
35
+ import { VisualEditing } from "next-sanity/visual-editing"
36
+ import { isSanityConfigured } from "@/lib/integrations/check-integration"
37
+ import { SanityLive } from "@/lib/integrations/sanity/live"
38
+
39
+ async function VisualEditingInner() {
40
+ const { isEnabled: isDraftMode } = await draftMode()
41
+ const sanityConfigured = isSanityConfigured()
42
+
43
+ if (!sanityConfigured || !isDraftMode) return null
44
+
45
+ return (
46
+ <>
47
+ <VisualEditing />
48
+ <SanityLive />
49
+ </>
50
+ )
51
+ }
52
+
53
+ export function SanityVisualEditing() {
54
+ return (
55
+ <Suspense fallback={null}>
56
+ <VisualEditingInner />
57
+ </Suspense>
58
+ )
59
+ }
60
+ `;
61
+
62
+ /**
63
+ * Merge Sanity integration into template layout.
64
+ * The layout itself stays free of dynamic functions (draftMode, headers, etc.)
65
+ * All dynamic behavior is encapsulated in generated components.
66
+ *
67
+ * Strategy:
68
+ * 1. Inject two imports (SanityStudioGuard, SanityVisualEditing)
69
+ * 2. Wrap body chrome in <SanityStudioGuard>
70
+ * 3. Add <SanityVisualEditing /> before </body>
71
+ * 4. Generate the two component files
72
+ *
73
+ * @param {string} templatePath - Path to the template's layout.tsx
74
+ * @param {object} options - Options from the merge orchestrator
75
+ * @param {string} options.targetDir - The project root directory
76
+ * @param {string} options.pathPrefix - Path prefix (e.g., 'src/' or '')
77
+ * @returns {Promise<{skipped?: boolean, reason?: string, success?: boolean}>}
78
+ */
79
+ export async function mergeLayout(
80
+ templatePath,
81
+ { targetDir, pathPrefix } = {},
82
+ ) {
83
+ let content = await fs.readFile(templatePath, "utf-8");
84
+
85
+ // Skip if already has Sanity integration
86
+ if (
87
+ content.includes("SanityStudioGuard") ||
88
+ content.includes("isSanityConfigured")
89
+ ) {
90
+ return { skipped: true, reason: "Already has Sanity integration" };
91
+ }
92
+
93
+ // 1. Add Sanity imports after existing imports
94
+ const importMatches = [...content.matchAll(/^import .+$/gm)];
95
+ if (importMatches.length > 0) {
96
+ const lastImport = importMatches[importMatches.length - 1];
97
+ const insertPos = lastImport.index + lastImport[0].length;
98
+ content =
99
+ content.slice(0, insertPos) +
100
+ "\n" +
101
+ SANITY_IMPORTS +
102
+ content.slice(insertPos);
103
+ }
104
+
105
+ // 2. Ensure Suspense is imported from react
106
+ if (!content.match(/import\s+.*Suspense.*from\s+["']react["']/)) {
107
+ const reactImportMatch = content.match(
108
+ /import\s+\{([^}]*)\}\s+from\s+["']react["']/,
109
+ );
110
+ if (reactImportMatch) {
111
+ if (!reactImportMatch[1].includes("Suspense")) {
112
+ content = content.replace(
113
+ reactImportMatch[0],
114
+ reactImportMatch[0].replace(
115
+ reactImportMatch[1],
116
+ `${reactImportMatch[1].trim()}, Suspense`,
117
+ ),
118
+ );
119
+ }
120
+ } else {
121
+ const lastImportMatch = content.match(/^(import\s+.+\n)/gm);
122
+ if (lastImportMatch) {
123
+ const lastImport = lastImportMatch[lastImportMatch.length - 1];
124
+ content = content.replace(
125
+ lastImport,
126
+ `${lastImport}import { Suspense } from "react"\n`,
127
+ );
128
+ }
129
+ }
130
+ }
131
+
132
+ // 3. Wrap body chrome in <SanityStudioGuard> and add <SanityVisualEditing />
133
+ content = content.replace(
134
+ /(\s*)(<body[^>]*>)([\s\S]*?)(\{children\})([\s\S]*?)(<\/body>)/m,
135
+ (
136
+ _match,
137
+ bodyIndent,
138
+ bodyOpen,
139
+ beforeChildren,
140
+ _childrenExpr,
141
+ afterChildren,
142
+ bodyClose,
143
+ ) => {
144
+ const baseIndent = bodyIndent || " ";
145
+ const indent = `${baseIndent} `;
146
+ const innerIndent = `${indent} `;
147
+
148
+ const trimmedBefore = beforeChildren.trim();
149
+ const trimmedAfter = afterChildren.trim();
150
+ const hasChrome = trimmedBefore.length > 0 || trimmedAfter.length > 0;
151
+
152
+ // Preserve relative indentation of chrome content
153
+ const reindent = (text, targetIndent) => {
154
+ const lines = text.split("\n").filter((line) => line.trim());
155
+ if (lines.length === 0) return "";
156
+ const minIndent = Math.min(
157
+ ...lines
158
+ .map((line) => line.match(/^(\s*)/)[1].length)
159
+ .filter((n) => n > 0),
160
+ Infinity,
161
+ );
162
+ return lines
163
+ .map((line) => {
164
+ const currentIndent = line.match(/^(\s*)/)[1].length;
165
+ const relativeIndent = Math.max(
166
+ 0,
167
+ currentIndent - (minIndent === Infinity ? 0 : minIndent),
168
+ );
169
+ return targetIndent + " ".repeat(relativeIndent) + line.trim();
170
+ })
171
+ .join("\n");
172
+ };
173
+
174
+ let result = `${baseIndent}${bodyOpen}\n`;
175
+
176
+ if (hasChrome && trimmedBefore) {
177
+ result += `${indent}<Suspense fallback={null}>\n`;
178
+ result += `${innerIndent}<SanityStudioGuard>\n`;
179
+ result += reindent(trimmedBefore, `${innerIndent} `);
180
+ result += `\n${innerIndent}</SanityStudioGuard>\n`;
181
+ result += `${indent}</Suspense>\n\n`;
182
+ }
183
+
184
+ result += `${indent}{children}\n`;
185
+
186
+ if (hasChrome && trimmedAfter) {
187
+ result += `\n${indent}<Suspense fallback={null}>\n`;
188
+ result += `${innerIndent}<SanityStudioGuard>\n`;
189
+ result += reindent(trimmedAfter, `${innerIndent} `);
190
+ result += `\n${innerIndent}</SanityStudioGuard>\n`;
191
+ result += `${indent}</Suspense>\n`;
192
+ }
193
+
194
+ result += `\n${indent}<SanityVisualEditing />\n${baseIndent}${bodyClose}`;
195
+ return result;
196
+ },
197
+ );
198
+
199
+ await fs.writeFile(templatePath, content);
200
+
201
+ // 3. Generate component files
202
+ if (targetDir) {
203
+ const sanityDir = path.join(
204
+ targetDir,
205
+ pathPrefix || "",
206
+ "components",
207
+ "sanity",
208
+ );
209
+ await fs.ensureDir(sanityDir);
210
+
211
+ const guardPath = path.join(sanityDir, "studio-guard.tsx");
212
+ if (!(await fs.pathExists(guardPath))) {
213
+ await fs.writeFile(guardPath, STUDIO_GUARD_COMPONENT);
214
+ }
215
+
216
+ const visualEditingPath = path.join(sanityDir, "visual-editing.tsx");
217
+ if (!(await fs.pathExists(visualEditingPath))) {
218
+ await fs.writeFile(visualEditingPath, VISUAL_EDITING_COMPONENT);
219
+ }
220
+ }
221
+
222
+ return { success: true };
223
+ }
@@ -0,0 +1,63 @@
1
+ // src/mergers/next-config-merger.js
2
+ import fs from "fs-extra";
3
+
4
+ const SANITY_PACKAGES =
5
+ '"@sanity/client", "@sanity/image-url", "@sanity/asset-utils"';
6
+
7
+ const IMAGES_BLOCK = `images: {
8
+ remotePatterns: [
9
+ {
10
+ protocol: "https",
11
+ hostname: "cdn.sanity.io",
12
+ },
13
+ ],
14
+ },`;
15
+
16
+ /**
17
+ * Merge Sanity-specific config into template next.config.ts
18
+ * Adds images.remotePatterns for cdn.sanity.io and
19
+ * experimental.optimizePackageImports for Sanity packages
20
+ *
21
+ * @param {string} templatePath - Path to the template's next.config.ts
22
+ * @returns {Promise<{skipped?: boolean, reason?: string, success?: boolean}>}
23
+ */
24
+ export async function mergeNextConfig(templatePath) {
25
+ let content = await fs.readFile(templatePath, "utf-8");
26
+
27
+ // Skip if already has Sanity config
28
+ if (content.includes("cdn.sanity.io")) {
29
+ return { skipped: true, reason: "Already has Sanity image config" };
30
+ }
31
+
32
+ // Match the NextConfig object opening: `const nextConfig: NextConfig = {`
33
+ const configObjectPattern = /(const\s+\w+\s*:\s*NextConfig\s*=\s*\{)/;
34
+
35
+ // 1. Add images.remotePatterns for Sanity CDN
36
+ if (!content.includes("remotePatterns")) {
37
+ content = content.replace(configObjectPattern, `$1\n ${IMAGES_BLOCK}`);
38
+ }
39
+
40
+ // 2. Add optimizePackageImports for Sanity packages
41
+ if (content.includes("optimizePackageImports")) {
42
+ // Already has optimizePackageImports array — prepend Sanity packages
43
+ content = content.replace(
44
+ /(optimizePackageImports\s*:\s*\[)/,
45
+ `$1${SANITY_PACKAGES}, `,
46
+ );
47
+ } else if (content.includes("experimental")) {
48
+ // Has experimental block but no optimizePackageImports — add it inside
49
+ content = content.replace(
50
+ /(experimental\s*:\s*\{)/,
51
+ `$1\n optimizePackageImports: [${SANITY_PACKAGES}],`,
52
+ );
53
+ } else {
54
+ // No experimental block — add it to the config object
55
+ content = content.replace(
56
+ configObjectPattern,
57
+ `$1\n experimental: {\n optimizePackageImports: [${SANITY_PACKAGES}],\n },`,
58
+ );
59
+ }
60
+
61
+ await fs.writeFile(templatePath, content);
62
+ return { success: true };
63
+ }
@@ -0,0 +1,121 @@
1
+ // src/mergers/sitemap-merger.js
2
+ import fs from "fs-extra";
3
+
4
+ /**
5
+ * Sanity-specific import to add
6
+ */
7
+ const SANITY_IMPORT = `import { isSanityConfigured } from "@/lib/integrations/check-integration"`;
8
+
9
+ /**
10
+ * Sanity fetch logic to inject before return statement
11
+ * Uses early return pattern and spread operator for cleaner code
12
+ */
13
+ const SANITY_FETCH_LOGIC = `
14
+ // Only fetch Sanity pages if Sanity is configured
15
+ if (isSanityConfigured()) {
16
+ try {
17
+ const sanityModule = await import("@/lib/integrations/sanity/client")
18
+ const sanityGroq = await import("next-sanity")
19
+
20
+ const client = sanityModule?.client
21
+ const groq = sanityGroq?.groq
22
+
23
+ // Skip if client is null (shouldn't happen since we check isSanityConfigured)
24
+ if (!(client && groq)) return baseRoutes
25
+
26
+ type SanityDocument = {
27
+ slug: { current: string }
28
+ _updatedAt: string
29
+ metadata?: { noIndex?: boolean }
30
+ }
31
+
32
+ // Fetch all published pages and articles
33
+ const pages = (await client.fetch(
34
+ groq\`*[_type == "page" && defined(slug.current)] {
35
+ slug,
36
+ _updatedAt,
37
+ metadata
38
+ }\`
39
+ )) as SanityDocument[]
40
+
41
+ const articles = (await client.fetch(
42
+ groq\`*[_type == "article" && defined(slug.current)] {
43
+ slug,
44
+ _updatedAt,
45
+ metadata
46
+ }\`
47
+ )) as SanityDocument[]
48
+
49
+ // Add pages to sitemap (exclude noIndex pages)
50
+ const pageEntries: MetadataRoute.Sitemap = pages
51
+ .filter((page: SanityDocument) => !page.metadata?.noIndex)
52
+ .map((page: SanityDocument) => ({
53
+ url: \`\${APP_BASE_URL}/\${page.slug.current}\`,
54
+ lastModified: new Date(page._updatedAt),
55
+ changeFrequency: "weekly" as const,
56
+ priority: 0.8,
57
+ }))
58
+
59
+ // Add articles to sitemap (exclude noIndex articles)
60
+ const articleEntries: MetadataRoute.Sitemap = articles
61
+ .filter((article: SanityDocument) => !article.metadata?.noIndex)
62
+ .map((article: SanityDocument) => ({
63
+ url: \`\${APP_BASE_URL}/blog/\${article.slug.current}\`,
64
+ lastModified: new Date(article._updatedAt),
65
+ changeFrequency: "weekly" as const,
66
+ priority: 0.7,
67
+ }))
68
+
69
+ return [...baseRoutes, ...pageEntries, ...articleEntries]
70
+ } catch (error) {
71
+ console.error("Error generating sitemap from Sanity:", error)
72
+ return baseRoutes
73
+ }
74
+ }
75
+
76
+ `;
77
+
78
+ /**
79
+ * Merge Sanity integration into template sitemap
80
+ * Preserves all existing agnostic code (routes, baseRoutes structure)
81
+ * and injects Sanity-specific fetching logic
82
+ *
83
+ * @param {string} templatePath - Path to the template's sitemap.ts
84
+ * @returns {Promise<{skipped?: boolean, reason?: string, success?: boolean}>}
85
+ */
86
+ export async function mergeSitemap(templatePath) {
87
+ let content = await fs.readFile(templatePath, "utf-8");
88
+
89
+ // Skip if already has Sanity integration
90
+ if (content.includes("isSanityConfigured")) {
91
+ return { skipped: true, reason: "Already has Sanity integration" };
92
+ }
93
+
94
+ // 1. Add Sanity import after existing imports
95
+ const importMatches = [...content.matchAll(/^import .+$/gm)];
96
+ if (importMatches.length > 0) {
97
+ const lastImport = importMatches[importMatches.length - 1];
98
+ const insertPos = lastImport.index + lastImport[0].length;
99
+ content =
100
+ content.slice(0, insertPos) +
101
+ "\n" +
102
+ SANITY_IMPORT +
103
+ content.slice(insertPos);
104
+ }
105
+
106
+ // 2. Add Sanity fetch logic before the final return statement
107
+ // Find the last "return baseRoutes" or "return [...baseRoutes" pattern
108
+ const returnMatch = content.match(
109
+ /(\n)([ \t]*)(return\s+(?:baseRoutes|\[\.\.\.baseRoutes))/m,
110
+ );
111
+ if (returnMatch) {
112
+ const returnIndex = content.indexOf(returnMatch[0]);
113
+ content =
114
+ content.slice(0, returnIndex) +
115
+ SANITY_FETCH_LOGIC +
116
+ content.slice(returnIndex);
117
+ }
118
+
119
+ await fs.writeFile(templatePath, content);
120
+ return { success: true };
121
+ }
@@ -0,0 +1,184 @@
1
+ # PRD: Replace Static Templates with next-starter + Dynamic Layers
2
+
3
+ ## Introduction
4
+
5
+ The CLI currently maintains 4 complete, nearly-identical template copies (default, webgl, webgpu, experiment). Every bug fix, dependency bump, or structural change must be applied 4 times. The `default` template is essentially a stale snapshot of the `basementstudio/next-starter` repo.
6
+
7
+ This feature replaces the static templates with a single `next-starter` clone as the base, then dynamically overlays technology-specific files and dependencies using a "layers" system — reusing the same smart-merge pattern already proven by the CMS integration system.
8
+
9
+ ## Goals
10
+
11
+ - Eliminate template duplication by using `basementstudio/next-starter` as the single source of truth
12
+ - Maintain the same user-facing CLI experience (same 4 template choices, same prompt flow)
13
+ - Create a `layers/` system for technology-specific additions (WebGL, WebGPU, Experiment)
14
+ - Reuse the existing merger infrastructure (`src/mergers/`) for layer injection
15
+ - Delete the entire `template/` directory from the repo
16
+
17
+ ## User Stories
18
+
19
+ ### US-001: Clone next-starter as base template
20
+ **Description:** As a developer using the CLI, I want my project to be scaffolded from the latest `next-starter` repo so that I always get the most up-to-date base configuration.
21
+
22
+ **Acceptance Criteria:**
23
+ - [ ] `create.js` clones from `github:basementstudio/next-starter#main` instead of `github:basementstudio/basement/template/{type}#main`
24
+ - [ ] Selecting "Default" template produces a project identical to cloning next-starter directly
25
+ - [ ] `bun.lock` is deleted after clone (since dependencies will be modified)
26
+ - [ ] Download spinner text updated to reflect new source
27
+ - [ ] Error/troubleshooting messages updated to reference `basementstudio/next-starter`
28
+ - [ ] Typecheck passes
29
+
30
+ ### US-002: Create layer configuration system
31
+ **Description:** As a CLI maintainer, I want technology layers defined in a config file so that adding or modifying layers only requires changing one file.
32
+
33
+ **Acceptance Criteria:**
34
+ - [ ] `LAYER_CONFIG` export added to `src/mergers/config.js` alongside existing `CMS_CONFIG`
35
+ - [ ] Each layer config defines: `replaceFiles`, `additivePaths`, `dependencies`, `devDependencies`
36
+ - [ ] `getLayerConfig(layer)` helper function exported
37
+ - [ ] Config contains entries for `webgl`, `webgpu`, and `experiment`
38
+ - [ ] `webgpu` is a deps-only layer (empty `replaceFiles` and `additivePaths`)
39
+ - [ ] Dependencies match what's currently in each template's `package.template.json`
40
+
41
+ ### US-003: Implement layer injection function
42
+ **Description:** As the CLI system, I need to overlay layer-specific files on top of the next-starter base so that WebGL/WebGPU/Experiment projects get their unique components.
43
+
44
+ **Acceptance Criteria:**
45
+ - [ ] `injectLayer(targetDir, layer, spinner)` function added to `src/mergers/index.js`
46
+ - [ ] Function copies additive files from local `layers/{type}/` directory (no tiged needed)
47
+ - [ ] Function replaces files listed in `replaceFiles` (overwrites base version)
48
+ - [ ] Function uses existing `detectPathPrefix()` and `transformPath()` for `src/` directory support
49
+ - [ ] Function returns results object with `replaced`, `copied`, `skipped`, `failed` arrays
50
+ - [ ] `formatMergeResults()` extended to handle `replaced` entries
51
+ - [ ] Skipped layers (type === 'default') produce no errors
52
+
53
+ ### US-004: Extract layer files from templates
54
+ **Description:** As a CLI maintainer, I need the unique files from each template extracted into a `layers/` directory so they can be overlaid on next-starter.
55
+
56
+ **Acceptance Criteria:**
57
+ - [ ] `layers/webgl/app/page.tsx` contains the WebGL-specific page (DynamicCanvas import)
58
+ - [ ] `layers/webgl/components/webgl/canvas/dynamic.tsx` exists (from template/webgl)
59
+ - [ ] `layers/webgl/components/webgl/canvas/index.tsx` exists (from template/webgl)
60
+ - [ ] `layers/webgl/components/webgl/components/scene/index.tsx` exists (from template/webgl)
61
+ - [ ] `layers/experiment/components/layout/header/index.tsx` exists (custom header with NavigationMenu)
62
+ - [ ] `layers/experiment/components/layout/navigation-menu.tsx` exists
63
+ - [ ] `layers/experiment/lib/constats.ts` exists
64
+ - [ ] `layers/experiment/lib/utils/cn.ts` exists
65
+ - [ ] `layers/webgpu/` directory exists (empty, deps-only layer)
66
+ - [ ] Files are byte-for-byte identical to their template counterparts
67
+
68
+ ### US-005: Integrate layer injection into create flow
69
+ **Description:** As the CLI system, I need the layer injection step wired into the project creation flow so it runs between template download and CMS integration.
70
+
71
+ **Acceptance Criteria:**
72
+ - [ ] Layer injection runs after next-starter clone, before CMS integration
73
+ - [ ] Layer injection only runs when `type !== 'default'`
74
+ - [ ] Progress spinner shows layer name during injection
75
+ - [ ] Injection results displayed to user (files added/replaced)
76
+ - [ ] Failures produce warnings, not hard errors
77
+ - [ ] CMS integration (Sanity) still works correctly after layer injection
78
+ - [ ] Typecheck passes
79
+
80
+ ### US-006: Config-driven dependency injection
81
+ **Description:** As the CLI system, I need package.json hydration to read layer dependencies from config instead of being hardcoded in create.js.
82
+
83
+ **Acceptance Criteria:**
84
+ - [ ] Layer dependencies read from `LAYER_CONFIG` and merged into `package.json`
85
+ - [ ] CMS dependencies still injected as before (unchanged)
86
+ - [ ] Animation library dependencies still injected as before (unchanged)
87
+ - [ ] `package.template.json` rename logic simplified (next-starter uses `package.json` directly)
88
+ - [ ] Project name and version still set correctly
89
+ - [ ] Typecheck passes
90
+
91
+ ### US-007: Delete template directory
92
+ **Description:** As a CLI maintainer, I want the `template/` directory removed so there's no duplicated code to maintain.
93
+
94
+ **Acceptance Criteria:**
95
+ - [ ] `template/default/` deleted
96
+ - [ ] `template/webgl/` deleted
97
+ - [ ] `template/webgpu/` deleted
98
+ - [ ] `template/experiment/` deleted
99
+ - [ ] No remaining references to `template/` paths in source code
100
+ - [ ] No remaining references to `basementstudio/basement/template/` in source code
101
+
102
+ ### US-008: End-to-end verification
103
+ **Description:** As a CLI maintainer, I need to verify all template + CMS + animation combinations work correctly.
104
+
105
+ **Acceptance Criteria:**
106
+ - [ ] `basement -c test-default -d -no-cms -no-animation -claude -no-hooks` produces a working project
107
+ - [ ] `basement -c test-webgl -webgl -no-cms -no-animation -claude -no-hooks` produces a working project with R3F components
108
+ - [ ] `basement -c test-webgpu -webgpu -no-cms -no-animation -claude -no-hooks` produces a working project with WebGPU deps
109
+ - [ ] `basement -c test-experiment -exp -no-cms -no-animation -claude -no-hooks` produces a working project with experiment header
110
+ - [ ] `basement -c test-sanity -webgl -sanity -gsap -claude -no-hooks` produces a working project with CMS + animation + layer
111
+ - [ ] All generated projects pass `bun install && bun dev` (dev server starts)
112
+
113
+ ## Functional Requirements
114
+
115
+ - FR-1: The CLI must clone `basementstudio/next-starter#main` as the base for all template types
116
+ - FR-2: When `type !== 'default'`, the CLI must apply the corresponding layer from the local `layers/` directory
117
+ - FR-3: Layer injection must copy additive files without overwriting existing files
118
+ - FR-4: Layer injection must replace files listed in `replaceFiles`, overwriting the base version
119
+ - FR-5: Layer injection must respect `src/` directory structure (using existing `detectPathPrefix`)
120
+ - FR-6: Layer dependencies must be merged into `package.json` from `LAYER_CONFIG`
121
+ - FR-7: CMS integration must continue to work identically after layer injection
122
+ - FR-8: The `bun.lock` file must be deleted after cloning next-starter
123
+ - FR-9: The user-facing prompt flow must remain unchanged (same 4 template choices)
124
+ - FR-10: The `template/` directory must be deleted from the repository
125
+
126
+ ## Non-Goals
127
+
128
+ - No multi-layer support (combining WebGL + Experiment) — one layer per project
129
+ - No `add-layer` command for existing projects (only `create` flow)
130
+ - No version pinning UI — always clones latest `main`
131
+ - No changes to the CMS integration system (`src/mergers/` for Sanity/BaseHub)
132
+ - No changes to the hooks, plugins, or agent skills systems
133
+ - No new merger functions for layers (layers use simple copy/replace, not smart merge)
134
+
135
+ ## Technical Considerations
136
+
137
+ - **Reuse existing infrastructure:** `detectPathPrefix()`, `transformPath()`, and `formatMergeResults()` from `src/mergers/index.js` are already built for this pattern
138
+ - **Layer files are local:** Unlike CMS integrations (cloned via tiged from a remote branch), layer files live in the CLI repo's `layers/` directory. No network request needed.
139
+ - **Application order matters:** Layers run before CMS integration. Layers do NOT modify `layout.tsx`, so the Sanity layout merger continues to work on the unmodified base layout.
140
+ - **Package.json format:** `next-starter` uses `package.json` (not `package.template.json`), simplifying hydration logic.
141
+
142
+ ### Files to Modify
143
+
144
+ | File | Change Type |
145
+ |------|-------------|
146
+ | `src/mergers/config.js` | Add `LAYER_CONFIG`, `getLayerConfig()` |
147
+ | `src/mergers/index.js` | Add `injectLayer()`, extend `formatMergeResults()` |
148
+ | `src/commands/create.js` | Change tiged source, add layer step, config-driven deps |
149
+
150
+ ### Files to Create
151
+
152
+ | File | Source |
153
+ |------|--------|
154
+ | `layers/webgl/app/page.tsx` | `template/webgl/app/page.tsx` |
155
+ | `layers/webgl/components/webgl/canvas/dynamic.tsx` | `template/webgl/components/webgl/canvas/dynamic.tsx` |
156
+ | `layers/webgl/components/webgl/canvas/index.tsx` | `template/webgl/components/webgl/canvas/index.tsx` |
157
+ | `layers/webgl/components/webgl/components/scene/index.tsx` | `template/webgl/components/webgl/components/scene/index.tsx` |
158
+ | `layers/experiment/components/layout/header/index.tsx` | `template/experiment/components/layout/header/index.tsx` |
159
+ | `layers/experiment/components/layout/navigation-menu.tsx` | `template/experiment/components/layout/navigation-menu.tsx` |
160
+ | `layers/experiment/lib/constats.ts` | `template/experiment/lib/constats.ts` |
161
+ | `layers/experiment/lib/utils/cn.ts` | `template/experiment/lib/utils/cn.ts` |
162
+
163
+ ### Files/Directories to Delete
164
+
165
+ | Path | Reason |
166
+ |------|--------|
167
+ | `template/default/` | Replaced by next-starter clone |
168
+ | `template/webgl/` | Replaced by `layers/webgl/` |
169
+ | `template/webgpu/` | Replaced by `layers/webgpu/` |
170
+ | `template/experiment/` | Replaced by `layers/experiment/` |
171
+
172
+ ## Success Metrics
173
+
174
+ - Template directory eliminated (0 duplicated files to maintain)
175
+ - All 4 template types produce working projects with `bun dev`
176
+ - CMS + layer combinations work correctly
177
+ - CLI prompt flow unchanged (invisible to users)
178
+ - Layer addition requires only: adding files to `layers/`, adding config to `LAYER_CONFIG`
179
+
180
+ ## Open Questions
181
+
182
+ - Should we add a `--starter-ref` CLI flag in a future iteration for version pinning?
183
+ - If next-starter changes its directory structure significantly, should the CLI validate expected paths before applying layers?
184
+ - Should the `experiment` template's `constats.ts` typo be fixed to `constants.ts`?