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.
- package/.changeset/2026-02-11-test-patch-bump.md +5 -0
- package/.changeset/README.md +10 -0
- package/.changeset/config.json +16 -0
- package/.cursor/rules/README.md +184 -0
- package/.cursor/rules/architecture.mdc +437 -0
- package/.cursor/rules/components.mdc +436 -0
- package/.cursor/rules/integrations.mdc +447 -0
- package/.cursor/rules/main.mdc +278 -0
- package/.cursor/rules/styling.mdc +433 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/.gitkeep +0 -0
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/release.yml +54 -0
- package/.tldr/cache/call_graph.json +7 -0
- package/.tldr/languages.json +6 -0
- package/.tldr/status +1 -0
- package/.tldrignore +84 -0
- package/.vscode/extensions.json +20 -0
- package/.vscode/settings.json +98 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +138 -0
- package/README.md +176 -0
- package/bin/index.js +262 -0
- package/biome.json +44 -0
- package/bun.lock +496 -0
- package/changelog/04-02-26.md +86 -0
- package/changelog/05-02-26.md +101 -0
- package/changelog/09-02-26.md +83 -0
- package/docs/fix-studio-hydration.md +46 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-design.md +196 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-implementation.md +695 -0
- package/docs/sanity-setup-steps.md +199 -0
- package/integrations/basehub/README.md +3 -0
- package/integrations/sanity/app/api/draft-mode/disable/route.ts +7 -0
- package/integrations/sanity/app/api/draft-mode/enable/route.ts +21 -0
- package/integrations/sanity/app/api/revalidate/route.ts +37 -0
- package/integrations/sanity/app/layout.tsx +111 -0
- package/integrations/sanity/app/sitemap.ts +80 -0
- package/integrations/sanity/app/studio/[[...tool]]/page.tsx +8 -0
- package/integrations/sanity/app/studio/layout.tsx +7 -0
- package/integrations/sanity/components/ui/sanity-image/index.tsx +37 -0
- package/integrations/sanity/lib/integrations/README.md +58 -0
- package/integrations/sanity/lib/integrations/check-integration.ts +62 -0
- package/integrations/sanity/lib/integrations/sanity/README.md +144 -0
- package/integrations/sanity/lib/integrations/sanity/client.ts +30 -0
- package/integrations/sanity/lib/integrations/sanity/components/disable-draft-mode.tsx +29 -0
- package/integrations/sanity/lib/integrations/sanity/components/rich-text.tsx +73 -0
- package/integrations/sanity/lib/integrations/sanity/env.ts +38 -0
- package/integrations/sanity/lib/integrations/sanity/live/index.tsx +34 -0
- package/integrations/sanity/lib/integrations/sanity/queries.ts +99 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.cli.ts +20 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.config.ts +94 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.types.ts +337 -0
- package/integrations/sanity/lib/integrations/sanity/schema.json +1850 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/article.ts +132 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/example.ts +203 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/index.ts +37 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/link.ts +127 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/metadata.ts +68 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/navigation.ts +39 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/page.ts +77 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/richText.ts +59 -0
- package/integrations/sanity/lib/integrations/sanity/structure.ts +5 -0
- package/integrations/sanity/lib/integrations/sanity/utils/image.ts +11 -0
- package/integrations/sanity/lib/integrations/sanity/utils/link.ts +61 -0
- package/integrations/sanity/lib/scripts/copy-sanity-mcp.ts +23 -0
- package/integrations/sanity/lib/scripts/generate-page.ts +310 -0
- package/integrations/sanity/lib/utils/metadata.ts +190 -0
- package/layers/experiment/components/layout/header/index.tsx +58 -0
- package/layers/experiment/components/layout/navigation-menu.tsx +127 -0
- package/layers/experiment/lib/constants.ts +12 -0
- package/layers/webgl/app/page.tsx +10 -0
- package/layers/webgl/components/webgl/canvas/dynamic.tsx +34 -0
- package/layers/webgl/components/webgl/canvas/index.tsx +43 -0
- package/layers/webgl/components/webgl/components/scene/index.tsx +21 -0
- package/layers/webgpu/.gitkeep +0 -0
- package/package.json +44 -0
- package/plugins/README.md +21 -0
- package/plugins/no-anchor-element.grit +11 -0
- package/plugins/no-relative-parent-imports.grit +6 -0
- package/plugins/no-unnecessary-forwardref.grit +5 -0
- package/src/commands/add-integration.js +325 -0
- package/src/commands/create.js +415 -0
- package/src/commands/setup-sanity.js +426 -0
- package/src/commands/worktree.js +805 -0
- package/src/mergers/check-integration-merger.js +105 -0
- package/src/mergers/config.js +137 -0
- package/src/mergers/index.js +355 -0
- package/src/mergers/layout-merger.js +223 -0
- package/src/mergers/next-config-merger.js +63 -0
- package/src/mergers/sitemap-merger.js +121 -0
- package/tasks/prd-next-starter-dynamic-layers.md +184 -0
- package/tasks/prd.json +153 -0
- package/tasks/progress.txt +115 -0
- package/template-hooks/use-battery.ts +126 -0
- package/template-hooks/use-device-perf.ts +184 -0
- package/template-hooks/use-intersection-observer.ts +32 -0
- package/template-hooks/use-media.ts +33 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/mergers/check-integration-merger.js
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanity configuration check function to add
|
|
6
|
+
*/
|
|
7
|
+
const SANITY_FUNCTION = `
|
|
8
|
+
/**
|
|
9
|
+
* Check if Sanity CMS is configured
|
|
10
|
+
* Requires: NEXT_PUBLIC_SANITY_PROJECT_ID and NEXT_PUBLIC_SANITY_DATASET
|
|
11
|
+
*/
|
|
12
|
+
export function isSanityConfigured(): boolean {
|
|
13
|
+
return Boolean(
|
|
14
|
+
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID &&
|
|
15
|
+
process.env.NEXT_PUBLIC_SANITY_DATASET
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Update to getConfiguredIntegrations to include Sanity
|
|
22
|
+
*/
|
|
23
|
+
const SANITY_CONFIGURED_CHECK = ` if (isSanityConfigured()) integrations.push("Sanity")`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Update to getUnconfiguredIntegrations to include Sanity
|
|
27
|
+
*/
|
|
28
|
+
const SANITY_UNCONFIGURED_CHECK = ` if (!isSanityConfigured()) integrations.push("Sanity")`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Merge Sanity integration into template check-integration
|
|
32
|
+
* @param {string} templatePath - Path to the template's check-integration.ts
|
|
33
|
+
* @returns {Promise<{skipped?: boolean, reason?: string, success?: boolean}>}
|
|
34
|
+
*/
|
|
35
|
+
export async function mergeCheckIntegration(templatePath) {
|
|
36
|
+
let content = await fs.readFile(templatePath, "utf-8");
|
|
37
|
+
|
|
38
|
+
// Skip if already has isSanityConfigured
|
|
39
|
+
if (content.includes("isSanityConfigured")) {
|
|
40
|
+
return { skipped: true, reason: "Already has isSanityConfigured" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 1. Add isSanityConfigured function after isAnalyticsConfigured
|
|
44
|
+
const analyticsMatch = content.match(
|
|
45
|
+
/(export function isAnalyticsConfigured\(\)[^}]+\})/s,
|
|
46
|
+
);
|
|
47
|
+
if (analyticsMatch) {
|
|
48
|
+
const insertPos = analyticsMatch.index + analyticsMatch[0].length;
|
|
49
|
+
content =
|
|
50
|
+
content.slice(0, insertPos) +
|
|
51
|
+
"\n" +
|
|
52
|
+
SANITY_FUNCTION +
|
|
53
|
+
content.slice(insertPos);
|
|
54
|
+
} else {
|
|
55
|
+
// If no isAnalyticsConfigured, add before getConfiguredIntegrations
|
|
56
|
+
const getConfiguredMatch = content.match(
|
|
57
|
+
/export function getConfiguredIntegrations/,
|
|
58
|
+
);
|
|
59
|
+
if (getConfiguredMatch) {
|
|
60
|
+
content =
|
|
61
|
+
content.slice(0, getConfiguredMatch.index) +
|
|
62
|
+
SANITY_FUNCTION +
|
|
63
|
+
"\n" +
|
|
64
|
+
content.slice(getConfiguredMatch.index);
|
|
65
|
+
} else {
|
|
66
|
+
// Fallback: append to end
|
|
67
|
+
content = `${content.trimEnd()}\n${SANITY_FUNCTION}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Update getConfiguredIntegrations to include Sanity check
|
|
72
|
+
const configuredFnMatch = content.match(
|
|
73
|
+
/(export function getConfiguredIntegrations\(\)[^{]*\{[\s\S]*?)(if \(isAnalyticsConfigured\(\)\))/m,
|
|
74
|
+
);
|
|
75
|
+
if (configuredFnMatch) {
|
|
76
|
+
const insertPos = content.indexOf(
|
|
77
|
+
configuredFnMatch[2],
|
|
78
|
+
configuredFnMatch.index,
|
|
79
|
+
);
|
|
80
|
+
content =
|
|
81
|
+
content.slice(0, insertPos) +
|
|
82
|
+
SANITY_CONFIGURED_CHECK +
|
|
83
|
+
"\n " +
|
|
84
|
+
content.slice(insertPos);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Update getUnconfiguredIntegrations to include Sanity check
|
|
88
|
+
const unconfiguredFnMatch = content.match(
|
|
89
|
+
/(export function getUnconfiguredIntegrations\(\)[^{]*\{[\s\S]*?)(if \(!isAnalyticsConfigured\(\)\))/m,
|
|
90
|
+
);
|
|
91
|
+
if (unconfiguredFnMatch) {
|
|
92
|
+
const insertPos = content.indexOf(
|
|
93
|
+
unconfiguredFnMatch[2],
|
|
94
|
+
unconfiguredFnMatch.index,
|
|
95
|
+
);
|
|
96
|
+
content =
|
|
97
|
+
content.slice(0, insertPos) +
|
|
98
|
+
SANITY_UNCONFIGURED_CHECK +
|
|
99
|
+
"\n " +
|
|
100
|
+
content.slice(insertPos);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await fs.writeFile(templatePath, content);
|
|
104
|
+
return { success: true };
|
|
105
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/mergers/config.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CMS-specific configuration for smart merge integration
|
|
5
|
+
* Each CMS has its own branch, merge files, and additive paths
|
|
6
|
+
*/
|
|
7
|
+
export const CMS_CONFIG = {
|
|
8
|
+
sanity: {
|
|
9
|
+
// Git branch containing the Sanity integration files
|
|
10
|
+
branch: "sanity-integration",
|
|
11
|
+
|
|
12
|
+
// Files that need smart merging (exist in both template and integration)
|
|
13
|
+
mergeFiles: [
|
|
14
|
+
"app/layout.tsx",
|
|
15
|
+
"app/sitemap.ts",
|
|
16
|
+
"lib/integrations/check-integration.ts",
|
|
17
|
+
],
|
|
18
|
+
|
|
19
|
+
// Paths that are purely additive (copy directly, no conflict)
|
|
20
|
+
additivePaths: [
|
|
21
|
+
"lib/integrations/sanity",
|
|
22
|
+
"components/ui/sanity-image",
|
|
23
|
+
"app/api/draft-mode",
|
|
24
|
+
"app/api/revalidate",
|
|
25
|
+
"app/studio",
|
|
26
|
+
"lib/utils/metadata.ts",
|
|
27
|
+
"lib/scripts/generate-page.ts",
|
|
28
|
+
"lib/scripts/copy-sanity-mcp.ts",
|
|
29
|
+
"lib/integrations/README.md",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
basehub: {
|
|
34
|
+
// Git branch containing the BaseHub integration files
|
|
35
|
+
branch: "basehub-integration",
|
|
36
|
+
|
|
37
|
+
// Files that need smart merging for BaseHub
|
|
38
|
+
mergeFiles: [],
|
|
39
|
+
|
|
40
|
+
// Paths that are purely additive for BaseHub
|
|
41
|
+
additivePaths: ["lib/integrations/basehub"],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get configuration for a specific CMS
|
|
47
|
+
* @param {string} cms - The CMS name (e.g., 'sanity', 'basehub')
|
|
48
|
+
* @returns {object|null} CMS configuration or null if not supported
|
|
49
|
+
*/
|
|
50
|
+
export function getCmsConfig(cms) {
|
|
51
|
+
return CMS_CONFIG[cms] || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a CMS is supported
|
|
56
|
+
* @param {string} cms - The CMS name
|
|
57
|
+
* @returns {boolean} Whether the CMS has a configuration
|
|
58
|
+
*/
|
|
59
|
+
export function isCmsSupported(cms) {
|
|
60
|
+
return cms in CMS_CONFIG;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Technology layer configuration for overlay on next-starter base
|
|
65
|
+
* Each layer defines files to replace, additive paths, and dependencies
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Technology layer configuration for overlay on next-starter base.
|
|
69
|
+
*
|
|
70
|
+
* next-starter already provides: clsx, tailwind-merge, zustand, zod, cva, etc.
|
|
71
|
+
* Layers only add packages that are NOT in next-starter.
|
|
72
|
+
*/
|
|
73
|
+
export const LAYER_CONFIG = {
|
|
74
|
+
webgl: {
|
|
75
|
+
replaceFiles: ["app/page.tsx"],
|
|
76
|
+
additivePaths: [
|
|
77
|
+
"components/webgl/canvas/dynamic.tsx",
|
|
78
|
+
"components/webgl/canvas/index.tsx",
|
|
79
|
+
"components/webgl/components/scene/index.tsx",
|
|
80
|
+
],
|
|
81
|
+
dependencies: {
|
|
82
|
+
"@react-three/fiber": "^9.5.0",
|
|
83
|
+
"@react-three/drei": "^10.7.7",
|
|
84
|
+
three: "^0.182.0",
|
|
85
|
+
"@radix-ui/react-navigation-menu": "^1.2.5",
|
|
86
|
+
leva: "^0.9.35",
|
|
87
|
+
},
|
|
88
|
+
devDependencies: {
|
|
89
|
+
"@types/three": "^0.182.0",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
webgpu: {
|
|
94
|
+
replaceFiles: [],
|
|
95
|
+
additivePaths: [],
|
|
96
|
+
dependencies: {
|
|
97
|
+
"@react-three/fiber": "10.0.0-alpha.2",
|
|
98
|
+
"@react-three/drei": "^11.0.0-alpha.4",
|
|
99
|
+
three: "^0.182.0",
|
|
100
|
+
leva: "^0.9.35",
|
|
101
|
+
"lucide-react": "^0.474.0",
|
|
102
|
+
"@radix-ui/react-navigation-menu": "^1.2.5",
|
|
103
|
+
},
|
|
104
|
+
devDependencies: {
|
|
105
|
+
"@types/three": "^0.182.0",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
experiment: {
|
|
110
|
+
replaceFiles: ["components/layout/header/index.tsx"],
|
|
111
|
+
additivePaths: [
|
|
112
|
+
"components/layout/navigation-menu.tsx",
|
|
113
|
+
"lib/constants.ts",
|
|
114
|
+
],
|
|
115
|
+
dependencies: {
|
|
116
|
+
"@react-three/fiber": "^9.5.0",
|
|
117
|
+
"@react-three/drei": "^10.7.7",
|
|
118
|
+
three: "^0.182.0",
|
|
119
|
+
"class-variance-authority": "^0.7.0",
|
|
120
|
+
leva: "^0.9.35",
|
|
121
|
+
"lucide-react": "^0.474.0",
|
|
122
|
+
"@radix-ui/react-navigation-menu": "^1.2.5",
|
|
123
|
+
},
|
|
124
|
+
devDependencies: {
|
|
125
|
+
"@types/three": "^0.182.0",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get configuration for a specific technology layer
|
|
132
|
+
* @param {string} layer - The layer name (e.g., 'webgl', 'webgpu', 'experiment')
|
|
133
|
+
* @returns {object|null} Layer configuration or null if not found
|
|
134
|
+
*/
|
|
135
|
+
export function getLayerConfig(layer) {
|
|
136
|
+
return LAYER_CONFIG[layer] || null;
|
|
137
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// src/mergers/index.js
|
|
2
|
+
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import fs from "fs-extra";
|
|
7
|
+
import tiged from "tiged";
|
|
8
|
+
import { getCmsConfig, getLayerConfig } from "./config.js";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CMS-specific merger functions (using base paths without src/ prefix)
|
|
15
|
+
* Only loaded when the specific CMS is selected
|
|
16
|
+
*/
|
|
17
|
+
const CMS_MERGERS = {
|
|
18
|
+
sanity: {
|
|
19
|
+
"app/layout.tsx": () =>
|
|
20
|
+
import("./layout-merger.js").then((m) => m.mergeLayout),
|
|
21
|
+
"app/sitemap.ts": () =>
|
|
22
|
+
import("./sitemap-merger.js").then((m) => m.mergeSitemap),
|
|
23
|
+
"lib/integrations/check-integration.ts": () =>
|
|
24
|
+
import("./check-integration-merger.js").then(
|
|
25
|
+
(m) => m.mergeCheckIntegration,
|
|
26
|
+
),
|
|
27
|
+
},
|
|
28
|
+
basehub: {
|
|
29
|
+
// BaseHub mergers would go here when implemented
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detect if the target project uses src/ prefix for app, lib, components
|
|
35
|
+
* @param {string} targetDir - The project directory
|
|
36
|
+
* @returns {Promise<string>} Empty string or 'src/' prefix
|
|
37
|
+
*/
|
|
38
|
+
async function detectPathPrefix(targetDir) {
|
|
39
|
+
// Check for src/app/ directory (Next.js App Router in src/)
|
|
40
|
+
const srcAppExists = await fs.pathExists(path.join(targetDir, "src/app"));
|
|
41
|
+
if (srcAppExists) {
|
|
42
|
+
return "src/";
|
|
43
|
+
}
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Transform a path by adding prefix where needed
|
|
49
|
+
* Only transforms paths that start with app/, lib/, or components/
|
|
50
|
+
* @param {string} filePath - Original path
|
|
51
|
+
* @param {string} prefix - Prefix to add (e.g., 'src/')
|
|
52
|
+
* @returns {string} Transformed path
|
|
53
|
+
*/
|
|
54
|
+
function transformPath(filePath, prefix) {
|
|
55
|
+
if (!prefix) return filePath;
|
|
56
|
+
|
|
57
|
+
// Paths that should be prefixed when using src/ structure
|
|
58
|
+
const prefixablePaths = ["app/", "lib/", "components/"];
|
|
59
|
+
|
|
60
|
+
for (const prefixable of prefixablePaths) {
|
|
61
|
+
if (filePath.startsWith(prefixable)) {
|
|
62
|
+
return prefix + filePath;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const prefixableFiles = ["middleware.ts", "middleware.js"];
|
|
67
|
+
for (const prefixableFile of prefixableFiles) {
|
|
68
|
+
if (filePath === prefixableFile) {
|
|
69
|
+
return prefix + filePath;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return filePath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get merger function for a specific CMS and file
|
|
78
|
+
* @param {string} cms - The CMS name
|
|
79
|
+
* @param {string} basePath - The base file path (without src/ prefix)
|
|
80
|
+
* @returns {Promise<Function|null>} Merger function or null
|
|
81
|
+
*/
|
|
82
|
+
async function getMerger(cms, basePath) {
|
|
83
|
+
const cmsmergers = CMS_MERGERS[cms];
|
|
84
|
+
if (!cmsmergers || !cmsmergers[basePath]) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return await cmsmergers[basePath]();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Inject CMS integration using smart merge
|
|
92
|
+
* @param {string} targetDir - The project directory
|
|
93
|
+
* @param {string} cms - The CMS name (e.g., 'sanity', 'basehub')
|
|
94
|
+
* @param {object} spinner - Ora spinner for progress
|
|
95
|
+
* @returns {object} Results of the merge operation
|
|
96
|
+
*/
|
|
97
|
+
export async function injectIntegration(targetDir, cms, spinner) {
|
|
98
|
+
const results = {
|
|
99
|
+
merged: [],
|
|
100
|
+
copied: [],
|
|
101
|
+
skipped: [],
|
|
102
|
+
failed: [],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Get CMS-specific configuration
|
|
106
|
+
const cmsConfig = getCmsConfig(cms);
|
|
107
|
+
|
|
108
|
+
if (!cmsConfig) {
|
|
109
|
+
results.failed.push({
|
|
110
|
+
path: cms,
|
|
111
|
+
error: `CMS "${cms}" is not supported. Supported: sanity, basehub`,
|
|
112
|
+
});
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { branch, mergeFiles, additivePaths } = cmsConfig;
|
|
117
|
+
|
|
118
|
+
// Detect if project uses src/ prefix (e.g., src/app/ instead of app/)
|
|
119
|
+
const pathPrefix = await detectPathPrefix(targetDir);
|
|
120
|
+
if (pathPrefix) {
|
|
121
|
+
spinner.text = `Detected src/ directory structure...`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 1. Clone integration to temp directory
|
|
125
|
+
const tempDir = path.join(os.tmpdir(), `integration-${Date.now()}`);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
spinner.text = `Downloading ${cms} integration...`;
|
|
129
|
+
|
|
130
|
+
// Use CMS-specific branch
|
|
131
|
+
const integrationRepoPath = `github:basementstudio/basement-cli/integrations/${cms}#${branch}`;
|
|
132
|
+
const emitter = tiged(integrationRepoPath, {
|
|
133
|
+
cache: false,
|
|
134
|
+
force: true,
|
|
135
|
+
mode: "git",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await emitter.clone(tempDir);
|
|
139
|
+
|
|
140
|
+
// 2. Copy additive files/directories (CMS-specific paths)
|
|
141
|
+
spinner.text = `Adding ${cms} files...`;
|
|
142
|
+
|
|
143
|
+
for (const additivePath of additivePaths) {
|
|
144
|
+
const src = path.join(tempDir, additivePath);
|
|
145
|
+
// Transform destination path based on project structure
|
|
146
|
+
const transformedPath = transformPath(additivePath, pathPrefix);
|
|
147
|
+
const dest = path.join(targetDir, transformedPath);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
if (await fs.pathExists(src)) {
|
|
151
|
+
await fs.copy(src, dest, { overwrite: false });
|
|
152
|
+
results.copied.push(transformedPath);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
results.failed.push({ path: transformedPath, error: error.message });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3. Smart merge files that exist in both (CMS-specific mergers)
|
|
160
|
+
if (mergeFiles.length > 0) {
|
|
161
|
+
spinner.text = `Merging ${cms} integration...`;
|
|
162
|
+
|
|
163
|
+
for (const mergeFile of mergeFiles) {
|
|
164
|
+
// Transform path for the target project
|
|
165
|
+
const transformedPath = transformPath(mergeFile, pathPrefix);
|
|
166
|
+
const templateFile = path.join(targetDir, transformedPath);
|
|
167
|
+
const integrationFile = path.join(tempDir, mergeFile);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
// Check if template file exists
|
|
171
|
+
if (!(await fs.pathExists(templateFile))) {
|
|
172
|
+
// Template doesn't have this file - copy from integration
|
|
173
|
+
if (await fs.pathExists(integrationFile)) {
|
|
174
|
+
await fs.ensureDir(path.dirname(templateFile));
|
|
175
|
+
await fs.copy(integrationFile, templateFile);
|
|
176
|
+
results.copied.push(transformedPath);
|
|
177
|
+
} else {
|
|
178
|
+
results.skipped.push({
|
|
179
|
+
path: transformedPath,
|
|
180
|
+
reason: "Not in integration",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if integration file exists
|
|
187
|
+
if (!(await fs.pathExists(integrationFile))) {
|
|
188
|
+
results.skipped.push({
|
|
189
|
+
path: transformedPath,
|
|
190
|
+
reason: "Not in integration",
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Get the CMS-specific merger function (use base path for lookup)
|
|
196
|
+
const merger = await getMerger(cms, mergeFile);
|
|
197
|
+
|
|
198
|
+
if (merger) {
|
|
199
|
+
const result = await merger(templateFile, {
|
|
200
|
+
targetDir,
|
|
201
|
+
pathPrefix,
|
|
202
|
+
});
|
|
203
|
+
if (result.skipped) {
|
|
204
|
+
results.skipped.push({
|
|
205
|
+
path: transformedPath,
|
|
206
|
+
reason: result.reason,
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
results.merged.push(transformedPath);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
results.skipped.push({
|
|
213
|
+
path: transformedPath,
|
|
214
|
+
reason: `No merger for ${cms}`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
results.failed.push({ path: transformedPath, error: error.message });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} finally {
|
|
223
|
+
// 4. Cleanup temp directory
|
|
224
|
+
try {
|
|
225
|
+
await fs.remove(tempDir);
|
|
226
|
+
} catch {
|
|
227
|
+
// Ignore cleanup errors
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Inject technology layer files on top of next-starter base
|
|
236
|
+
* @param {string} targetDir - The project directory
|
|
237
|
+
* @param {string} layer - The layer name (e.g., 'webgl', 'webgpu', 'experiment')
|
|
238
|
+
* @param {object} spinner - Ora spinner for progress
|
|
239
|
+
* @returns {object} Results of the layer injection
|
|
240
|
+
*/
|
|
241
|
+
export async function injectLayer(targetDir, layer, spinner) {
|
|
242
|
+
const results = {
|
|
243
|
+
replaced: [],
|
|
244
|
+
copied: [],
|
|
245
|
+
skipped: [],
|
|
246
|
+
failed: [],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const layerConfig = getLayerConfig(layer);
|
|
250
|
+
|
|
251
|
+
if (!layerConfig || layer === "default") {
|
|
252
|
+
results.skipped.push({ path: layer, reason: "No layer to apply" });
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Resolve the layer directory from the CLI repo's layers/{type}/ path
|
|
257
|
+
const layerDir = path.resolve(__dirname, "../../layers", layer);
|
|
258
|
+
|
|
259
|
+
if (!(await fs.pathExists(layerDir))) {
|
|
260
|
+
results.skipped.push({
|
|
261
|
+
path: layer,
|
|
262
|
+
reason: `Layer directory not found: ${layerDir}`,
|
|
263
|
+
});
|
|
264
|
+
return results;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Detect if project uses src/ prefix
|
|
268
|
+
const pathPrefix = await detectPathPrefix(targetDir);
|
|
269
|
+
if (pathPrefix && spinner) {
|
|
270
|
+
spinner.text = `Detected src/ directory structure...`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { replaceFiles, additivePaths } = layerConfig;
|
|
274
|
+
|
|
275
|
+
// 1. Replace files (overwrite base version)
|
|
276
|
+
for (const replaceFile of replaceFiles) {
|
|
277
|
+
const src = path.join(layerDir, replaceFile);
|
|
278
|
+
const transformedPath = transformPath(replaceFile, pathPrefix);
|
|
279
|
+
const dest = path.join(targetDir, transformedPath);
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
if (await fs.pathExists(src)) {
|
|
283
|
+
await fs.ensureDir(path.dirname(dest));
|
|
284
|
+
await fs.copy(src, dest, { overwrite: true });
|
|
285
|
+
results.replaced.push(transformedPath);
|
|
286
|
+
} else {
|
|
287
|
+
results.skipped.push({
|
|
288
|
+
path: transformedPath,
|
|
289
|
+
reason: "Source file not found in layer",
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
results.failed.push({ path: transformedPath, error: error.message });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 2. Copy additive files
|
|
298
|
+
for (const additivePath of additivePaths) {
|
|
299
|
+
const src = path.join(layerDir, additivePath);
|
|
300
|
+
const transformedPath = transformPath(additivePath, pathPrefix);
|
|
301
|
+
const dest = path.join(targetDir, transformedPath);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
if (await fs.pathExists(src)) {
|
|
305
|
+
await fs.ensureDir(path.dirname(dest));
|
|
306
|
+
await fs.copy(src, dest, { overwrite: false });
|
|
307
|
+
results.copied.push(transformedPath);
|
|
308
|
+
} else {
|
|
309
|
+
results.skipped.push({
|
|
310
|
+
path: transformedPath,
|
|
311
|
+
reason: "Source file not found in layer",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
results.failed.push({ path: transformedPath, error: error.message });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return results;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Format merge results for display
|
|
324
|
+
* @param {object} results - Results from injectIntegration or injectLayer
|
|
325
|
+
* @returns {string[]} Lines to display
|
|
326
|
+
*/
|
|
327
|
+
export function formatMergeResults(results) {
|
|
328
|
+
const lines = [];
|
|
329
|
+
|
|
330
|
+
if (results.replaced && results.replaced.length > 0) {
|
|
331
|
+
lines.push(...results.replaced.map((f) => ` ✓ Replaced: ${f}`));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (results.merged && results.merged.length > 0) {
|
|
335
|
+
lines.push(...results.merged.map((f) => ` ✓ Merged: ${f}`));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (results.copied && results.copied.length > 0) {
|
|
339
|
+
lines.push(...results.copied.map((f) => ` ✓ Added: ${f}`));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (results.skipped && results.skipped.length > 0) {
|
|
343
|
+
lines.push(
|
|
344
|
+
...results.skipped.map((s) => ` ○ Skipped: ${s.path} (${s.reason})`),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (results.failed && results.failed.length > 0) {
|
|
349
|
+
lines.push(
|
|
350
|
+
...results.failed.map((f) => ` ✗ Failed: ${f.path} (${f.error})`),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return lines;
|
|
355
|
+
}
|