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,426 @@
1
+ import { execFileSync, execSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
+ import ora from "ora";
5
+ import pc from "picocolors";
6
+ import prompts from "prompts";
7
+
8
+ /**
9
+ * Strip ANSI escape codes from a string (covers all SGR and cursor sequences).
10
+ */
11
+ function stripAnsi(str) {
12
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI codes need control chars
13
+ const ansiRegex = /\u001b\[[0-9;?]*[A-Za-z]/g;
14
+ return str.replace(ansiRegex, "");
15
+ }
16
+ /**
17
+ * Step 1: Verify Sanity CLI is available
18
+ */
19
+ function verifySanityCli() {
20
+ try {
21
+ execSync("npx --yes sanity --version", { stdio: "pipe", timeout: 30000 });
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Step 2: Verify user is authenticated with Sanity
30
+ */
31
+ function verifyAuth() {
32
+ try {
33
+ execSync("npx --yes sanity projects list", {
34
+ stdio: "pipe",
35
+ timeout: 30000,
36
+ });
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Fetch the user's Sanity organizations via the Management API.
45
+ */
46
+ async function fetchOrganizations(authToken) {
47
+ const response = await fetch(
48
+ "https://api.sanity.io/v2021-06-07/organizations",
49
+ {
50
+ headers: { Authorization: `Bearer ${authToken}` },
51
+ signal: AbortSignal.timeout(15000),
52
+ },
53
+ );
54
+
55
+ if (!response.ok) {
56
+ throw new Error(`Failed to fetch organizations (${response.status})`);
57
+ }
58
+
59
+ return response.json();
60
+ }
61
+
62
+ /**
63
+ * Step 3: Create a Sanity project and return the project ID.
64
+ * Uses execFileSync to avoid shell injection via project name.
65
+ */
66
+ function createSanityProject(projectName, organizationId) {
67
+ const args = [
68
+ "--yes",
69
+ "sanity",
70
+ "projects",
71
+ "create",
72
+ "--name",
73
+ projectName,
74
+ "--dataset",
75
+ "production",
76
+ ];
77
+ if (organizationId) {
78
+ args.push("--organization", organizationId);
79
+ }
80
+
81
+ const output = execFileSync("npx", args, {
82
+ encoding: "utf-8",
83
+ timeout: 120000,
84
+ });
85
+
86
+ const match = stripAnsi(output).match(/ID:\s*(\S+)/);
87
+ if (!match) {
88
+ throw new Error(
89
+ `Could not parse project ID from Sanity CLI output:\n${output}`,
90
+ );
91
+ }
92
+
93
+ return match[1];
94
+ }
95
+
96
+ /**
97
+ * Step 4: Get the user's Sanity auth token from the CLI.
98
+ */
99
+ function getAuthToken() {
100
+ const output = execSync("npx --yes sanity debug --secrets 2>&1", {
101
+ encoding: "utf-8",
102
+ timeout: 30000,
103
+ });
104
+
105
+ const clean = stripAnsi(output);
106
+ const match = clean.match(/Auth token:\s*'([^']+)'/);
107
+ if (!match) {
108
+ throw new Error("Could not extract auth token from Sanity CLI.");
109
+ }
110
+
111
+ return match[1];
112
+ }
113
+
114
+ /**
115
+ * Step 5: Create an API read token via the Sanity HTTP API.
116
+ * Uses native fetch to avoid shell injection and curl dependency.
117
+ */
118
+ async function createApiToken(projectId, authToken) {
119
+ const url = `https://api.sanity.io/v2021-06-07/projects/${encodeURIComponent(projectId)}/tokens`;
120
+
121
+ const response = await fetch(url, {
122
+ method: "POST",
123
+ headers: {
124
+ Authorization: `Bearer ${authToken}`,
125
+ "Content-Type": "application/json",
126
+ },
127
+ body: JSON.stringify({
128
+ label: "Viewer private token",
129
+ roleName: "viewer",
130
+ }),
131
+ signal: AbortSignal.timeout(30000),
132
+ });
133
+
134
+ const data = await response.json();
135
+
136
+ if (response.status === 401 || response.status === 403) {
137
+ throw new Error("Auth token expired or invalid. Run: npx sanity login");
138
+ }
139
+
140
+ if (!response.ok) {
141
+ throw new Error(
142
+ `Sanity API error (${response.status}): ${JSON.stringify(data, null, 2)}`,
143
+ );
144
+ }
145
+
146
+ if (!data.key) {
147
+ throw new Error(
148
+ `Failed to create API token. Response:\n${JSON.stringify(data, null, 2)}`,
149
+ );
150
+ }
151
+
152
+ return data.key;
153
+ }
154
+
155
+ /**
156
+ * Step 6: Write .env.local with Sanity credentials.
157
+ * Appends to existing file if present.
158
+ */
159
+ async function writeEnvLocal(targetDir, projectId, apiToken) {
160
+ const envPath = path.join(targetDir, ".env.local");
161
+
162
+ const sanityBlock = `# Sanity CMS
163
+ NEXT_PUBLIC_SANITY_PROJECT_ID=${projectId}
164
+ NEXT_PUBLIC_SANITY_DATASET=production
165
+ SANITY_PRIVATE_TOKEN=${apiToken}
166
+ NEXT_PUBLIC_SANITY_API_READ_TOKEN=${apiToken}
167
+ `;
168
+
169
+ if (fs.existsSync(envPath)) {
170
+ const existing = await fs.readFile(envPath, "utf-8");
171
+ if (existing.includes("NEXT_PUBLIC_SANITY_PROJECT_ID")) {
172
+ return;
173
+ }
174
+ await fs.appendFile(envPath, `\n${sanityBlock}`);
175
+ } else {
176
+ await fs.writeFile(envPath, sanityBlock);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Step 7: Verify the connection to Sanity by querying the API.
182
+ * Uses native fetch instead of curl.
183
+ */
184
+ async function verifyConnection(projectId, apiToken) {
185
+ try {
186
+ const url = `https://${encodeURIComponent(projectId)}.api.sanity.io/v2024-03-15/data/query/production?query=*%5B0%5D`;
187
+
188
+ const response = await fetch(url, {
189
+ headers: { Authorization: `Bearer ${apiToken}` },
190
+ signal: AbortSignal.timeout(15000),
191
+ });
192
+
193
+ const data = await response.json();
194
+ return Object.hasOwn(data, "result");
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Step 8: Ensure .env.local is in .gitignore.
202
+ */
203
+ async function ensureGitignore(targetDir) {
204
+ const gitignorePath = path.join(targetDir, ".gitignore");
205
+
206
+ let content = "";
207
+ if (fs.existsSync(gitignorePath)) {
208
+ content = await fs.readFile(gitignorePath, "utf-8");
209
+ }
210
+
211
+ if (!content.includes(".env.local")) {
212
+ const newline = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
213
+ await fs.writeFile(gitignorePath, `${content + newline}.env.local\n`);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Main entry point: Automate Sanity project creation, token setup, and .env.local generation.
219
+ *
220
+ * @param {string} targetDir - The project directory
221
+ * @param {object} [options] - Optional overrides
222
+ * @param {string} [options.projectName] - Pre-set project name (skip prompt)
223
+ */
224
+ export async function setupSanityProject(targetDir, options = {}) {
225
+ console.log();
226
+
227
+ const { wantSetup } = await prompts({
228
+ type: "confirm",
229
+ name: "wantSetup",
230
+ message:
231
+ "Set up Sanity project automatically? (creates project, tokens, .env.local)",
232
+ initial: true,
233
+ });
234
+
235
+ if (!wantSetup) {
236
+ console.log(pc.dim(" Skipped. You can set up Sanity manually later.\n"));
237
+ return false;
238
+ }
239
+
240
+ // Step 1: Verify Sanity CLI
241
+ const cliSpinner = ora("Checking Sanity CLI...").start();
242
+ if (!verifySanityCli()) {
243
+ cliSpinner.fail(
244
+ pc.red(
245
+ "Sanity CLI not available. Make sure sanity packages are installed.",
246
+ ),
247
+ );
248
+ console.log(
249
+ pc.dim(
250
+ " Run: bun add next-sanity @sanity/client @sanity/image-url sanity @sanity/vision\n",
251
+ ),
252
+ );
253
+ return false;
254
+ }
255
+ cliSpinner.succeed(pc.green("Sanity CLI available."));
256
+
257
+ // Step 2: Verify authentication
258
+ const authSpinner = ora("Verifying Sanity authentication...").start();
259
+ if (!verifyAuth()) {
260
+ authSpinner.info(pc.yellow("Not logged in. Opening Sanity login..."));
261
+ try {
262
+ execSync("npx --yes sanity login", { stdio: "inherit" });
263
+ } catch {
264
+ // login command failed or user cancelled
265
+ }
266
+ const retrySpinner = ora("Re-verifying Sanity authentication...").start();
267
+ if (!verifyAuth()) {
268
+ retrySpinner.fail(pc.red("Still not logged in to Sanity."));
269
+ console.log(pc.yellow(" Run: npx sanity login"));
270
+ console.log(
271
+ pc.dim(
272
+ " This opens a browser for authentication. Re-run this command after logging in.\n",
273
+ ),
274
+ );
275
+ return false;
276
+ }
277
+ retrySpinner.succeed(pc.green("Authenticated with Sanity."));
278
+ } else {
279
+ authSpinner.succeed(pc.green("Authenticated with Sanity."));
280
+ }
281
+
282
+ // Step 3: Get auth token (needed for org selection and API token creation)
283
+ const tokenSpinner = ora("Retrieving auth token...").start();
284
+ let authToken;
285
+ try {
286
+ authToken = getAuthToken();
287
+ tokenSpinner.succeed(pc.green("Auth token retrieved."));
288
+ } catch (e) {
289
+ tokenSpinner.fail(pc.red(`Failed to get auth token: ${e.message}`));
290
+ console.log(pc.dim(" Fallback: check ~/.config/sanity/config.json\n"));
291
+ return false;
292
+ }
293
+
294
+ // Step 4: Select organization
295
+ let organizationId = null;
296
+ try {
297
+ const orgSpinner = ora("Fetching organizations...").start();
298
+ const organizations = await fetchOrganizations(authToken);
299
+
300
+ if (organizations.length > 0) {
301
+ orgSpinner.succeed(
302
+ pc.green(
303
+ `Found ${organizations.length} organization${organizations.length > 1 ? "s" : ""}.`,
304
+ ),
305
+ );
306
+
307
+ const choices = [
308
+ { title: "Personal account", value: null },
309
+ ...organizations.map((org) => ({
310
+ title: org.name,
311
+ description: org.slug || org.id,
312
+ value: org.id,
313
+ })),
314
+ ];
315
+
316
+ const { selectedOrg } = await prompts({
317
+ type: "select",
318
+ name: "selectedOrg",
319
+ message: "Which organization should own this project?",
320
+ choices,
321
+ initial: organizations.length === 1 ? 1 : 0,
322
+ });
323
+
324
+ if (selectedOrg === undefined) {
325
+ console.log(pc.yellow("\n Setup cancelled.\n"));
326
+ return false;
327
+ }
328
+
329
+ organizationId = selectedOrg;
330
+ } else {
331
+ orgSpinner.succeed(pc.green("Using personal account."));
332
+ }
333
+ } catch (_e) {
334
+ // Non-critical: fall back to no org (personal account)
335
+ console.log(
336
+ pc.dim(" Could not fetch organizations, using personal account."),
337
+ );
338
+ }
339
+
340
+ // Step 5: Get project name and create project
341
+ const dirName = path.basename(targetDir);
342
+ const projectName =
343
+ options.projectName ||
344
+ (
345
+ await prompts({
346
+ type: "text",
347
+ name: "name",
348
+ message: "Sanity project name?",
349
+ initial: dirName,
350
+ validate: (v) => (v.length > 0 ? true : "Project name is required"),
351
+ })
352
+ ).name;
353
+
354
+ if (!projectName) {
355
+ console.log(pc.yellow("\n Setup cancelled.\n"));
356
+ return false;
357
+ }
358
+
359
+ const projectSpinner = ora(
360
+ `Creating Sanity project "${pc.cyan(projectName)}"${organizationId ? ` in organization` : ""}...`,
361
+ ).start();
362
+
363
+ let projectId;
364
+ try {
365
+ projectId = createSanityProject(projectName, organizationId);
366
+ projectSpinner.succeed(
367
+ pc.green(`Sanity project created: ${pc.cyan(projectId)}`),
368
+ );
369
+ } catch (e) {
370
+ projectSpinner.fail(pc.red(`Failed to create project: ${e.message}`));
371
+ return false;
372
+ }
373
+
374
+ // Step 6: Create API read token
375
+ const apiSpinner = ora("Creating API read token...").start();
376
+ let apiToken;
377
+ try {
378
+ apiToken = await createApiToken(projectId, authToken);
379
+ apiSpinner.succeed(pc.green("API read token created."));
380
+ } catch (e) {
381
+ apiSpinner.fail(pc.red(`Failed to create API token: ${e.message}`));
382
+ return false;
383
+ }
384
+
385
+ // Step 7: Write .env.local
386
+ const envSpinner = ora("Writing .env.local...").start();
387
+ try {
388
+ await writeEnvLocal(targetDir, projectId, apiToken);
389
+ envSpinner.succeed(
390
+ pc.green(".env.local configured with Sanity credentials."),
391
+ );
392
+ } catch (e) {
393
+ envSpinner.fail(pc.red(`Failed to write .env.local: ${e.message}`));
394
+ return false;
395
+ }
396
+
397
+ // Step 8: Verify connection
398
+ const verifySpinner = ora("Verifying Sanity connection...").start();
399
+ const connected = await verifyConnection(projectId, apiToken);
400
+ if (connected) {
401
+ verifySpinner.succeed(pc.green("Sanity connection verified."));
402
+ } else {
403
+ verifySpinner.warn(
404
+ pc.yellow(
405
+ "Could not verify connection. Check credentials after bun install.",
406
+ ),
407
+ );
408
+ }
409
+
410
+ // Step 9: Ensure .env.local is gitignored
411
+ try {
412
+ await ensureGitignore(targetDir);
413
+ } catch {
414
+ // Non-critical, don't block
415
+ }
416
+
417
+ console.log(
418
+ pc.green(`\n Sanity project "${projectName}" (${projectId}) is ready.`),
419
+ );
420
+ console.log(
421
+ pc.dim(` Manage at: https://sanity.io/manage/project/${projectId}`),
422
+ );
423
+ console.log(pc.dim(" Studio will be available at: /studio\n"));
424
+
425
+ return true;
426
+ }