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,695 @@
1
+ # Sanity Smart Merge Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Replace the blind tiged overlay with a smart merge system that preserves template-specific code while adding Sanity integration.
6
+
7
+ **Architecture:** Clone integration to temp directory, then process files in two modes: (1) additive files are copied directly, (2) merge files are intelligently combined using string manipulation to inject Sanity-specific imports, variables, and components.
8
+
9
+ **Tech Stack:** Node.js, fs-extra (already installed), no new dependencies needed (using string manipulation instead of ts-morph for simplicity).
10
+
11
+ ---
12
+
13
+ ## Task 1: Create Merger Config
14
+
15
+ **Files:**
16
+ - Create: `src/mergers/config.js`
17
+
18
+ **Step 1: Create the mergers directory**
19
+
20
+ ```bash
21
+ mkdir -p src/mergers
22
+ ```
23
+
24
+ **Step 2: Write the config file**
25
+
26
+ ```js
27
+ // src/mergers/config.js
28
+
29
+ /**
30
+ * Files that need smart merging (exist in both template and integration)
31
+ */
32
+ export const MERGE_FILES = [
33
+ 'app/layout.tsx',
34
+ 'app/sitemap.ts',
35
+ 'lib/integrations/check-integration.ts',
36
+ ]
37
+
38
+ /**
39
+ * Paths that are purely additive (copy directly, no conflict)
40
+ */
41
+ export const ADDITIVE_PATHS = [
42
+ 'lib/integrations/sanity',
43
+ 'components/ui/sanity-image',
44
+ 'app/api/draft-mode',
45
+ 'lib/utils/metadata.ts',
46
+ 'lib/scripts/generate-page.ts',
47
+ 'lib/integrations/README.md',
48
+ ]
49
+ ```
50
+
51
+ **Step 3: Commit**
52
+
53
+ ```bash
54
+ git add src/mergers/config.js
55
+ git commit -m "feat(mergers): add file classification config"
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Task 2: Create Layout Merger
61
+
62
+ **Files:**
63
+ - Create: `src/mergers/layout-merger.js`
64
+
65
+ **Step 1: Write the layout merger**
66
+
67
+ ```js
68
+ // src/mergers/layout-merger.js
69
+ import fs from 'fs-extra'
70
+
71
+ /**
72
+ * Sanity-specific imports to inject into layout
73
+ */
74
+ const SANITY_IMPORTS = `import { draftMode } from "next/headers"
75
+ import { VisualEditing } from "next-sanity/visual-editing"
76
+ import { isSanityConfigured } from "@/lib/integrations/check-integration"
77
+ import { SanityLive } from "@/lib/integrations/sanity/live"`
78
+
79
+ /**
80
+ * Sanity-specific variables to inject before return statement
81
+ */
82
+ const SANITY_VARIABLES = ` const { isEnabled: isDraftMode } = await draftMode()
83
+ const sanityConfigured = isSanityConfigured()
84
+ `
85
+
86
+ /**
87
+ * Sanity components to inject after {children}
88
+ */
89
+ const SANITY_COMPONENTS = `
90
+ {/* Sanity Visual Editing - only when draft mode is enabled */}
91
+ {sanityConfigured && isDraftMode && (
92
+ <Suspense fallback={null}>
93
+ <VisualEditing />
94
+ <SanityLive />
95
+ </Suspense>
96
+ )}`
97
+
98
+ /**
99
+ * Merge Sanity integration into template layout
100
+ * @param {string} templatePath - Path to the template's layout.tsx
101
+ */
102
+ export async function mergeLayout(templatePath) {
103
+ let content = await fs.readFile(templatePath, 'utf-8')
104
+
105
+ // Skip if already has Sanity integration
106
+ if (content.includes('isSanityConfigured')) {
107
+ return { skipped: true, reason: 'Already has Sanity integration' }
108
+ }
109
+
110
+ // 1. Add Sanity imports after existing imports
111
+ // Find the last import statement
112
+ const importMatches = [...content.matchAll(/^import .+$/gm)]
113
+ if (importMatches.length > 0) {
114
+ const lastImport = importMatches[importMatches.length - 1]
115
+ const insertPos = lastImport.index + lastImport[0].length
116
+ content = content.slice(0, insertPos) + '\n' + SANITY_IMPORTS + content.slice(insertPos)
117
+ }
118
+
119
+ // 2. Add Sanity variables before the return statement in the Layout function
120
+ // Find "export default async function Layout" or similar and its return statement
121
+ const returnMatch = content.match(/export default async function \w+[^{]*\{[\s\S]*?(return\s*\()/m)
122
+ if (returnMatch) {
123
+ const returnIndex = content.indexOf(returnMatch[1], returnMatch.index)
124
+ content = content.slice(0, returnIndex) + SANITY_VARIABLES + '\n ' + content.slice(returnIndex)
125
+ }
126
+
127
+ // 3. Add Sanity components after {children}
128
+ // Find {children} inside <body> and add Sanity components after it
129
+ const childrenMatch = content.match(/(\{children\})([\s\S]*?)(<\/body>)/m)
130
+ if (childrenMatch) {
131
+ content = content.replace(
132
+ /(\{children\})([\s\S]*?)(<\/body>)/m,
133
+ `$1${SANITY_COMPONENTS}$2$3`
134
+ )
135
+ }
136
+
137
+ await fs.writeFile(templatePath, content)
138
+ return { success: true }
139
+ }
140
+ ```
141
+
142
+ **Step 2: Commit**
143
+
144
+ ```bash
145
+ git add src/mergers/layout-merger.js
146
+ git commit -m "feat(mergers): add layout merger for Sanity integration"
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Task 3: Create Sitemap Merger
152
+
153
+ **Files:**
154
+ - Create: `src/mergers/sitemap-merger.js`
155
+
156
+ **Step 1: Write the sitemap merger**
157
+
158
+ ```js
159
+ // src/mergers/sitemap-merger.js
160
+ import fs from 'fs-extra'
161
+
162
+ /**
163
+ * Sanity-specific import to add
164
+ */
165
+ const SANITY_IMPORT = `import { isSanityConfigured } from "@/lib/integrations/check-integration"`
166
+
167
+ /**
168
+ * Sanity fetch logic to inject before return
169
+ */
170
+ const SANITY_FETCH_LOGIC = `
171
+ // Only fetch Sanity pages if Sanity is configured
172
+ if (isSanityConfigured()) {
173
+ try {
174
+ const sanityModule = await import("@/lib/integrations/sanity/client")
175
+ const sanityGroq = await import("next-sanity")
176
+
177
+ const client = sanityModule?.client
178
+ const groq = sanityGroq?.groq
179
+
180
+ // Skip if client is null (shouldn't happen since we check isSanityConfigured)
181
+ if (client && groq) {
182
+ type SanityDocument = {
183
+ slug: { current: string }
184
+ _updatedAt: string
185
+ metadata?: { noIndex?: boolean }
186
+ }
187
+
188
+ // Fetch all published pages and articles
189
+ const pages = (await client.fetch(
190
+ groq\`*[_type == "page" && defined(slug.current)] {
191
+ slug,
192
+ _updatedAt,
193
+ metadata
194
+ }\`
195
+ )) as SanityDocument[]
196
+
197
+ const articles = (await client.fetch(
198
+ groq\`*[_type == "article" && defined(slug.current)] {
199
+ slug,
200
+ _updatedAt,
201
+ metadata
202
+ }\`
203
+ )) as SanityDocument[]
204
+
205
+ // Add pages to sitemap (exclude noIndex pages)
206
+ const pageEntries: MetadataRoute.Sitemap = pages
207
+ .filter((page: SanityDocument) => !page.metadata?.noIndex)
208
+ .map((page: SanityDocument) => ({
209
+ url: \`\${APP_BASE_URL}/sanity/\${page.slug.current}\`,
210
+ lastModified: new Date(page._updatedAt),
211
+ changeFrequency: "weekly" as const,
212
+ priority: 0.8,
213
+ }))
214
+
215
+ // Add articles to sitemap (exclude noIndex articles)
216
+ const articleEntries: MetadataRoute.Sitemap = articles
217
+ .filter((article: SanityDocument) => !article.metadata?.noIndex)
218
+ .map((article: SanityDocument) => ({
219
+ url: \`\${APP_BASE_URL}/sanity/\${article.slug.current}\`,
220
+ lastModified: new Date(article._updatedAt),
221
+ changeFrequency: "weekly" as const,
222
+ priority: 0.7,
223
+ }))
224
+
225
+ baseRoutes.push(...pageEntries, ...articleEntries)
226
+ }
227
+ } catch (error) {
228
+ console.error("Error generating sitemap from Sanity:", error)
229
+ }
230
+ }
231
+ `
232
+
233
+ /**
234
+ * Merge Sanity integration into template sitemap
235
+ * @param {string} templatePath - Path to the template's sitemap.ts
236
+ */
237
+ export async function mergeSitemap(templatePath) {
238
+ let content = await fs.readFile(templatePath, 'utf-8')
239
+
240
+ // Skip if already has Sanity integration
241
+ if (content.includes('isSanityConfigured')) {
242
+ return { skipped: true, reason: 'Already has Sanity integration' }
243
+ }
244
+
245
+ // 1. Add Sanity import after existing imports
246
+ const importMatches = [...content.matchAll(/^import .+$/gm)]
247
+ if (importMatches.length > 0) {
248
+ const lastImport = importMatches[importMatches.length - 1]
249
+ const insertPos = lastImport.index + lastImport[0].length
250
+ content = content.slice(0, insertPos) + '\n' + SANITY_IMPORT + content.slice(insertPos)
251
+ }
252
+
253
+ // 2. Add Sanity fetch logic before the return statement
254
+ // Find "return baseRoutes" or similar return statement
255
+ const returnMatch = content.match(/(\n)([ \t]*)(return\s+\w+)/m)
256
+ if (returnMatch) {
257
+ const returnIndex = content.indexOf(returnMatch[0])
258
+ content = content.slice(0, returnIndex) + SANITY_FETCH_LOGIC + content.slice(returnIndex)
259
+ }
260
+
261
+ await fs.writeFile(templatePath, content)
262
+ return { success: true }
263
+ }
264
+ ```
265
+
266
+ **Step 2: Commit**
267
+
268
+ ```bash
269
+ git add src/mergers/sitemap-merger.js
270
+ git commit -m "feat(mergers): add sitemap merger for Sanity integration"
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Task 4: Create Check-Integration Merger
276
+
277
+ **Files:**
278
+ - Create: `src/mergers/check-integration-merger.js`
279
+
280
+ **Step 1: Write the check-integration merger**
281
+
282
+ ```js
283
+ // src/mergers/check-integration-merger.js
284
+ import fs from 'fs-extra'
285
+
286
+ /**
287
+ * Sanity configuration check function to add
288
+ */
289
+ const SANITY_FUNCTION = `
290
+ /**
291
+ * Check if Sanity CMS is configured
292
+ * Requires: NEXT_PUBLIC_SANITY_PROJECT_ID and NEXT_PUBLIC_SANITY_DATASET
293
+ */
294
+ export function isSanityConfigured(): boolean {
295
+ return Boolean(
296
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID &&
297
+ process.env.NEXT_PUBLIC_SANITY_DATASET
298
+ )
299
+ }
300
+ `
301
+
302
+ /**
303
+ * Update to getConfiguredIntegrations to include Sanity
304
+ */
305
+ const SANITY_CONFIGURED_CHECK = ` if (isSanityConfigured()) integrations.push("Sanity")`
306
+
307
+ /**
308
+ * Update to getUnconfiguredIntegrations to include Sanity
309
+ */
310
+ const SANITY_UNCONFIGURED_CHECK = ` if (!isSanityConfigured()) integrations.push("Sanity")`
311
+
312
+ /**
313
+ * Merge Sanity integration into template check-integration
314
+ * @param {string} templatePath - Path to the template's check-integration.ts
315
+ */
316
+ export async function mergeCheckIntegration(templatePath) {
317
+ let content = await fs.readFile(templatePath, 'utf-8')
318
+
319
+ // Skip if already has isSanityConfigured
320
+ if (content.includes('isSanityConfigured')) {
321
+ return { skipped: true, reason: 'Already has isSanityConfigured' }
322
+ }
323
+
324
+ // 1. Add isSanityConfigured function after isAnalyticsConfigured
325
+ const analyticsMatch = content.match(/(export function isAnalyticsConfigured\(\)[^}]+\})/s)
326
+ if (analyticsMatch) {
327
+ const insertPos = analyticsMatch.index + analyticsMatch[0].length
328
+ content = content.slice(0, insertPos) + '\n' + SANITY_FUNCTION + content.slice(insertPos)
329
+ } else {
330
+ // If no isAnalyticsConfigured, add before getConfiguredIntegrations
331
+ const getConfiguredMatch = content.match(/export function getConfiguredIntegrations/)
332
+ if (getConfiguredMatch) {
333
+ content = content.slice(0, getConfiguredMatch.index) + SANITY_FUNCTION + '\n' + content.slice(getConfiguredMatch.index)
334
+ } else {
335
+ // Fallback: append to end
336
+ content = content.trimEnd() + '\n' + SANITY_FUNCTION
337
+ }
338
+ }
339
+
340
+ // 2. Update getConfiguredIntegrations to include Sanity check
341
+ const configuredFnMatch = content.match(/(export function getConfiguredIntegrations\(\)[^{]*\{[\s\S]*?)(if \(isAnalyticsConfigured\(\)\))/m)
342
+ if (configuredFnMatch) {
343
+ const insertPos = content.indexOf(configuredFnMatch[2], configuredFnMatch.index)
344
+ content = content.slice(0, insertPos) + SANITY_CONFIGURED_CHECK + '\n ' + content.slice(insertPos)
345
+ }
346
+
347
+ // 3. Update getUnconfiguredIntegrations to include Sanity check
348
+ const unconfiguredFnMatch = content.match(/(export function getUnconfiguredIntegrations\(\)[^{]*\{[\s\S]*?)(if \(!isAnalyticsConfigured\(\)\))/m)
349
+ if (unconfiguredFnMatch) {
350
+ const insertPos = content.indexOf(unconfiguredFnMatch[2], unconfiguredFnMatch.index)
351
+ content = content.slice(0, insertPos) + SANITY_UNCONFIGURED_CHECK + '\n ' + content.slice(insertPos)
352
+ }
353
+
354
+ await fs.writeFile(templatePath, content)
355
+ return { success: true }
356
+ }
357
+ ```
358
+
359
+ **Step 2: Commit**
360
+
361
+ ```bash
362
+ git add src/mergers/check-integration-merger.js
363
+ git commit -m "feat(mergers): add check-integration merger for Sanity"
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Task 5: Create Merger Orchestrator
369
+
370
+ **Files:**
371
+ - Create: `src/mergers/index.js`
372
+
373
+ **Step 1: Write the orchestrator**
374
+
375
+ ```js
376
+ // src/mergers/index.js
377
+ import fs from 'fs-extra'
378
+ import path from 'path'
379
+ import os from 'os'
380
+ import tiged from 'tiged'
381
+ import { MERGE_FILES, ADDITIVE_PATHS } from './config.js'
382
+ import { mergeLayout } from './layout-merger.js'
383
+ import { mergeSitemap } from './sitemap-merger.js'
384
+ import { mergeCheckIntegration } from './check-integration-merger.js'
385
+
386
+ /**
387
+ * Map of file paths to their merger functions
388
+ */
389
+ const MERGERS = {
390
+ 'app/layout.tsx': mergeLayout,
391
+ 'app/sitemap.ts': mergeSitemap,
392
+ 'lib/integrations/check-integration.ts': mergeCheckIntegration,
393
+ }
394
+
395
+ /**
396
+ * Inject Sanity integration using smart merge
397
+ * @param {string} targetDir - The project directory
398
+ * @param {string} cms - The CMS name (e.g., 'sanity')
399
+ * @param {object} spinner - Ora spinner for progress
400
+ * @returns {object} Results of the merge operation
401
+ */
402
+ export async function injectIntegration(targetDir, cms, spinner) {
403
+ const results = {
404
+ merged: [],
405
+ copied: [],
406
+ skipped: [],
407
+ failed: [],
408
+ }
409
+
410
+ // 1. Clone integration to temp directory
411
+ const tempDir = path.join(os.tmpdir(), `integration-${Date.now()}`)
412
+
413
+ try {
414
+ spinner.text = `Downloading ${cms} integration...`
415
+
416
+ const integrationRepoPath = `github:basementstudio/basement-cli/integrations/${cms}#sanity-integration`
417
+ const emitter = tiged(integrationRepoPath, {
418
+ cache: false,
419
+ force: true,
420
+ mode: 'git',
421
+ })
422
+
423
+ await emitter.clone(tempDir)
424
+
425
+ // 2. Copy additive files/directories
426
+ spinner.text = `Adding ${cms} files...`
427
+
428
+ for (const additivePath of ADDITIVE_PATHS) {
429
+ const src = path.join(tempDir, additivePath)
430
+ const dest = path.join(targetDir, additivePath)
431
+
432
+ try {
433
+ if (await fs.pathExists(src)) {
434
+ await fs.copy(src, dest, { overwrite: false })
435
+ results.copied.push(additivePath)
436
+ }
437
+ } catch (error) {
438
+ results.failed.push({ path: additivePath, error: error.message })
439
+ }
440
+ }
441
+
442
+ // 3. Smart merge files that exist in both
443
+ spinner.text = `Merging ${cms} integration...`
444
+
445
+ for (const mergeFile of MERGE_FILES) {
446
+ const templateFile = path.join(targetDir, mergeFile)
447
+ const integrationFile = path.join(tempDir, mergeFile)
448
+ const merger = MERGERS[mergeFile]
449
+
450
+ try {
451
+ // Check if template file exists
452
+ if (!(await fs.pathExists(templateFile))) {
453
+ // Template doesn't have this file - copy from integration
454
+ if (await fs.pathExists(integrationFile)) {
455
+ await fs.copy(integrationFile, templateFile)
456
+ results.copied.push(mergeFile)
457
+ } else {
458
+ results.skipped.push({ path: mergeFile, reason: 'Not in integration' })
459
+ }
460
+ continue
461
+ }
462
+
463
+ // Check if integration file exists
464
+ if (!(await fs.pathExists(integrationFile))) {
465
+ results.skipped.push({ path: mergeFile, reason: 'Not in integration' })
466
+ continue
467
+ }
468
+
469
+ // Run the merger
470
+ if (merger) {
471
+ const result = await merger(templateFile)
472
+ if (result.skipped) {
473
+ results.skipped.push({ path: mergeFile, reason: result.reason })
474
+ } else {
475
+ results.merged.push(mergeFile)
476
+ }
477
+ }
478
+ } catch (error) {
479
+ results.failed.push({ path: mergeFile, error: error.message })
480
+ }
481
+ }
482
+
483
+ } finally {
484
+ // 4. Cleanup temp directory
485
+ try {
486
+ await fs.remove(tempDir)
487
+ } catch {
488
+ // Ignore cleanup errors
489
+ }
490
+ }
491
+
492
+ return results
493
+ }
494
+
495
+ /**
496
+ * Format merge results for display
497
+ * @param {object} results - Results from injectIntegration
498
+ * @returns {string[]} Lines to display
499
+ */
500
+ export function formatMergeResults(results) {
501
+ const lines = []
502
+
503
+ if (results.merged.length > 0) {
504
+ lines.push(...results.merged.map(f => ` ✓ Merged: ${f}`))
505
+ }
506
+
507
+ if (results.copied.length > 0) {
508
+ lines.push(...results.copied.map(f => ` ✓ Added: ${f}`))
509
+ }
510
+
511
+ if (results.skipped.length > 0) {
512
+ lines.push(...results.skipped.map(s => ` ○ Skipped: ${s.path} (${s.reason})`))
513
+ }
514
+
515
+ if (results.failed.length > 0) {
516
+ lines.push(...results.failed.map(f => ` ✗ Failed: ${f.path} (${f.error})`))
517
+ }
518
+
519
+ return lines
520
+ }
521
+ ```
522
+
523
+ **Step 2: Commit**
524
+
525
+ ```bash
526
+ git add src/mergers/index.js
527
+ git commit -m "feat(mergers): add orchestrator with smart merge logic"
528
+ ```
529
+
530
+ ---
531
+
532
+ ## Task 6: Update create.js to Use Smart Merge
533
+
534
+ **Files:**
535
+ - Modify: `src/commands/create.js:137-164`
536
+
537
+ **Step 1: Update the integration injection section**
538
+
539
+ Replace lines 137-164 in `src/commands/create.js`:
540
+
541
+ ```js
542
+ // 3.5. Inject Integration Files (Smart Merge)
543
+ if (cms && cms !== "none") {
544
+ const integrationSpinner = ora(
545
+ `Integrating ${pc.cyan(cms)}...`
546
+ ).start();
547
+
548
+ try {
549
+ const { injectIntegration, formatMergeResults } = await import('../mergers/index.js');
550
+ const results = await injectIntegration(targetDir, cms, integrationSpinner);
551
+
552
+ const hasErrors = results.failed.length > 0;
553
+ const resultLines = formatMergeResults(results);
554
+
555
+ if (hasErrors) {
556
+ integrationSpinner.warn(pc.yellow(`${cms} integration completed with warnings:`));
557
+ } else {
558
+ integrationSpinner.succeed(pc.green(`${cms} integration added:`));
559
+ }
560
+
561
+ // Display detailed results
562
+ resultLines.forEach(line => console.log(pc.dim(line)));
563
+
564
+ } catch (e) {
565
+ integrationSpinner.warn(
566
+ pc.yellow(`Failed to inject ${cms} integration: ${e.message}`)
567
+ );
568
+ }
569
+ }
570
+ ```
571
+
572
+ **Step 2: Commit**
573
+
574
+ ```bash
575
+ git add src/commands/create.js
576
+ git commit -m "feat(create): use smart merge for CMS integration"
577
+ ```
578
+
579
+ ---
580
+
581
+ ## Task 7: Test with Default Template
582
+
583
+ **Step 1: Link the CLI locally**
584
+
585
+ ```bash
586
+ npm link
587
+ ```
588
+
589
+ **Step 2: Create a test project with default template + Sanity**
590
+
591
+ ```bash
592
+ cd /tmp
593
+ basement
594
+ # Select: my-test-app, default, sanity, none, claude, no hooks
595
+ ```
596
+
597
+ **Step 3: Verify the merged files**
598
+
599
+ Check `app/layout.tsx`:
600
+ - Should have original template structure
601
+ - Should have Sanity imports added
602
+ - Should have draftMode variables
603
+ - Should have Sanity components after {children}
604
+
605
+ Check `app/sitemap.ts`:
606
+ - Should have original baseRoutes
607
+ - Should have isSanityConfigured import
608
+ - Should have Sanity fetch logic
609
+
610
+ Check `lib/integrations/check-integration.ts`:
611
+ - Should have isAnalyticsConfigured (from template)
612
+ - Should have isSanityConfigured (added)
613
+ - Should have updated getConfiguredIntegrations
614
+
615
+ Check additive files exist:
616
+ - `lib/integrations/sanity/` directory
617
+ - `components/ui/sanity-image/`
618
+ - `app/api/draft-mode/`
619
+
620
+ **Step 4: Verify the project builds**
621
+
622
+ ```bash
623
+ cd my-test-app
624
+ bun install
625
+ bun build
626
+ ```
627
+
628
+ Expected: Build succeeds without errors
629
+
630
+ **Step 5: Commit test results (if any fixes needed)**
631
+
632
+ ```bash
633
+ git add -A
634
+ git commit -m "fix(mergers): address issues found during testing"
635
+ ```
636
+
637
+ ---
638
+
639
+ ## Task 8: Test with WebGPU Template
640
+
641
+ **Step 1: Create a test project with webgpu template + Sanity**
642
+
643
+ ```bash
644
+ cd /tmp
645
+ rm -rf my-test-webgpu
646
+ basement
647
+ # Select: my-test-webgpu, webgpu, sanity, none, claude, no hooks
648
+ ```
649
+
650
+ **Step 2: Verify the merged files preserve WebGPU-specific code**
651
+
652
+ Check `app/layout.tsx`:
653
+ - Should preserve WebGPU providers/imports
654
+ - Should have Sanity additions
655
+
656
+ **Step 3: Verify the project builds**
657
+
658
+ ```bash
659
+ cd my-test-webgpu
660
+ bun install
661
+ bun build
662
+ ```
663
+
664
+ Expected: Build succeeds without errors
665
+
666
+ ---
667
+
668
+ ## Task 9: Final Cleanup and Documentation
669
+
670
+ **Step 1: Update the CLI's README if needed**
671
+
672
+ Document the new smart merge behavior.
673
+
674
+ **Step 2: Final commit**
675
+
676
+ ```bash
677
+ git add -A
678
+ git commit -m "docs: document smart merge integration system"
679
+ ```
680
+
681
+ ---
682
+
683
+ ## Summary
684
+
685
+ | Task | Description | Files |
686
+ |------|-------------|-------|
687
+ | 1 | Create merger config | `src/mergers/config.js` |
688
+ | 2 | Create layout merger | `src/mergers/layout-merger.js` |
689
+ | 3 | Create sitemap merger | `src/mergers/sitemap-merger.js` |
690
+ | 4 | Create check-integration merger | `src/mergers/check-integration-merger.js` |
691
+ | 5 | Create orchestrator | `src/mergers/index.js` |
692
+ | 6 | Update create.js | `src/commands/create.js` |
693
+ | 7 | Test with default template | Manual testing |
694
+ | 8 | Test with webgpu template | Manual testing |
695
+ | 9 | Final cleanup | Documentation |