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,805 @@
1
+ import { execSync } from "node:child_process";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import fs from "fs-extra";
5
+ import ora from "ora";
6
+ import pc from "picocolors";
7
+ import prompts from "prompts";
8
+
9
+ const WORKTREE_BASE = path.join(os.homedir(), ".claude", "worktrees");
10
+
11
+ /**
12
+ * Check if we're inside a git repository
13
+ */
14
+ function isGitRepo() {
15
+ try {
16
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Get the project name from git root directory
25
+ */
26
+ function getProjectName() {
27
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
28
+ encoding: "utf-8",
29
+ }).trim();
30
+ return path.basename(gitRoot);
31
+ }
32
+
33
+ /**
34
+ * Get the git root directory
35
+ */
36
+ function _getGitRoot() {
37
+ return execSync("git rev-parse --show-toplevel", {
38
+ encoding: "utf-8",
39
+ }).trim();
40
+ }
41
+
42
+ /**
43
+ * Convert branch name to directory-safe name (feature/auth -> feature-auth)
44
+ */
45
+ function sanitizeBranchName(branchName) {
46
+ return branchName.replace(/\//g, "-");
47
+ }
48
+
49
+ /**
50
+ * Get the worktree path for a branch
51
+ */
52
+ function getWorktreePath(projectName, branchName) {
53
+ const safeName = sanitizeBranchName(branchName);
54
+ return path.join(WORKTREE_BASE, projectName, safeName);
55
+ }
56
+
57
+ /**
58
+ * Get all existing branches
59
+ */
60
+ function _getExistingBranches() {
61
+ try {
62
+ const output = execSync('git branch --format="%(refname:short)"', {
63
+ encoding: "utf-8",
64
+ });
65
+ return output.trim().split("\n").filter(Boolean);
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if a branch exists
73
+ */
74
+ function branchExists(branchName) {
75
+ try {
76
+ execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, {
77
+ stdio: "pipe",
78
+ });
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Detect package manager and install dependencies
87
+ */
88
+ async function detectAndInstallDeps(worktreePath) {
89
+ const spinner = ora(pc.dim("Detecting dependencies...")).start();
90
+
91
+ // Node.js projects
92
+ if (fs.existsSync(path.join(worktreePath, "package.json"))) {
93
+ if (
94
+ fs.existsSync(path.join(worktreePath, "bun.lock")) ||
95
+ fs.existsSync(path.join(worktreePath, "bun.lockb"))
96
+ ) {
97
+ spinner.text = pc.dim("Installing dependencies with bun...");
98
+ try {
99
+ execSync("bun install", { cwd: worktreePath, stdio: "pipe" });
100
+ spinner.succeed(pc.green("Dependencies installed with bun"));
101
+ } catch {
102
+ spinner.warn(pc.yellow("bun install failed"));
103
+ }
104
+ return;
105
+ }
106
+
107
+ if (fs.existsSync(path.join(worktreePath, "pnpm-lock.yaml"))) {
108
+ spinner.text = pc.dim("Installing dependencies with pnpm...");
109
+ try {
110
+ execSync("pnpm install", { cwd: worktreePath, stdio: "pipe" });
111
+ spinner.succeed(pc.green("Dependencies installed with pnpm"));
112
+ } catch {
113
+ spinner.warn(pc.yellow("pnpm install failed"));
114
+ }
115
+ return;
116
+ }
117
+
118
+ if (fs.existsSync(path.join(worktreePath, "yarn.lock"))) {
119
+ spinner.text = pc.dim("Installing dependencies with yarn...");
120
+ try {
121
+ execSync("yarn install", { cwd: worktreePath, stdio: "pipe" });
122
+ spinner.succeed(pc.green("Dependencies installed with yarn"));
123
+ } catch {
124
+ spinner.warn(pc.yellow("yarn install failed"));
125
+ }
126
+ return;
127
+ }
128
+
129
+ spinner.text = pc.dim("Installing dependencies with npm...");
130
+ try {
131
+ execSync("npm install", { cwd: worktreePath, stdio: "pipe" });
132
+ spinner.succeed(pc.green("Dependencies installed with npm"));
133
+ } catch {
134
+ spinner.warn(pc.yellow("npm install failed"));
135
+ }
136
+ return;
137
+ }
138
+
139
+ // Rust projects
140
+ if (fs.existsSync(path.join(worktreePath, "Cargo.toml"))) {
141
+ spinner.text = pc.dim("Building Rust project...");
142
+ try {
143
+ execSync("cargo build", { cwd: worktreePath, stdio: "pipe" });
144
+ spinner.succeed(pc.green("Rust project built"));
145
+ } catch {
146
+ spinner.warn(pc.yellow("cargo build failed"));
147
+ }
148
+ return;
149
+ }
150
+
151
+ // Python projects
152
+ if (fs.existsSync(path.join(worktreePath, "requirements.txt"))) {
153
+ spinner.text = pc.dim("Installing Python dependencies...");
154
+ try {
155
+ execSync("pip install -r requirements.txt", {
156
+ cwd: worktreePath,
157
+ stdio: "pipe",
158
+ });
159
+ spinner.succeed(pc.green("Python dependencies installed"));
160
+ } catch {
161
+ spinner.warn(pc.yellow("pip install failed"));
162
+ }
163
+ return;
164
+ }
165
+
166
+ if (fs.existsSync(path.join(worktreePath, "pyproject.toml"))) {
167
+ const pyprojectContent = fs.readFileSync(
168
+ path.join(worktreePath, "pyproject.toml"),
169
+ "utf-8",
170
+ );
171
+ if (pyprojectContent.includes("[tool.poetry]")) {
172
+ spinner.text = pc.dim("Installing with poetry...");
173
+ try {
174
+ execSync("poetry install", { cwd: worktreePath, stdio: "pipe" });
175
+ spinner.succeed(pc.green("Poetry dependencies installed"));
176
+ } catch {
177
+ spinner.warn(pc.yellow("poetry install failed"));
178
+ }
179
+ } else {
180
+ spinner.text = pc.dim("Installing with pip...");
181
+ try {
182
+ execSync("pip install -e .", { cwd: worktreePath, stdio: "pipe" });
183
+ spinner.succeed(pc.green("Python package installed"));
184
+ } catch {
185
+ spinner.warn(pc.yellow("pip install failed"));
186
+ }
187
+ }
188
+ return;
189
+ }
190
+
191
+ // Go projects
192
+ if (fs.existsSync(path.join(worktreePath, "go.mod"))) {
193
+ spinner.text = pc.dim("Downloading Go modules...");
194
+ try {
195
+ execSync("go mod download", { cwd: worktreePath, stdio: "pipe" });
196
+ spinner.succeed(pc.green("Go modules downloaded"));
197
+ } catch {
198
+ spinner.warn(pc.yellow("go mod download failed"));
199
+ }
200
+ return;
201
+ }
202
+
203
+ spinner.info(pc.dim("No recognized dependency files found"));
204
+ }
205
+
206
+ /**
207
+ * Get list of worktrees for the current project
208
+ */
209
+ function getWorktrees(projectName) {
210
+ const projectWorktreeDir = path.join(WORKTREE_BASE, projectName);
211
+
212
+ if (!fs.existsSync(projectWorktreeDir)) {
213
+ return [];
214
+ }
215
+
216
+ const worktrees = [];
217
+ const dirs = fs.readdirSync(projectWorktreeDir, { withFileTypes: true });
218
+
219
+ for (const dir of dirs) {
220
+ if (dir.isDirectory()) {
221
+ const worktreePath = path.join(projectWorktreeDir, dir.name);
222
+ const gitFile = path.join(worktreePath, ".git");
223
+
224
+ let branchName = "unknown";
225
+ if (fs.existsSync(gitFile)) {
226
+ try {
227
+ const gitContent = fs.readFileSync(gitFile, "utf-8");
228
+ const gitDirMatch = gitContent.match(/gitdir: (.+)/);
229
+ if (gitDirMatch) {
230
+ const headFile = path.join(gitDirMatch[1].trim(), "HEAD");
231
+ if (fs.existsSync(headFile)) {
232
+ const headContent = fs.readFileSync(headFile, "utf-8");
233
+ branchName = headContent.replace("ref: refs/heads/", "").trim();
234
+ }
235
+ }
236
+ } catch {
237
+ // ignore
238
+ }
239
+ }
240
+
241
+ worktrees.push({
242
+ name: dir.name,
243
+ path: worktreePath,
244
+ branch: branchName,
245
+ });
246
+ }
247
+ }
248
+
249
+ return worktrees;
250
+ }
251
+
252
+ /**
253
+ * Create a single worktree (helper function)
254
+ * Returns { success: boolean, path: string, branch: string, error?: string }
255
+ */
256
+ async function createSingleWorktree(
257
+ projectName,
258
+ branchName,
259
+ installDeps = true,
260
+ ) {
261
+ const worktreePath = getWorktreePath(projectName, branchName);
262
+
263
+ // Check if worktree path already exists
264
+ if (fs.existsSync(worktreePath)) {
265
+ return {
266
+ success: false,
267
+ path: worktreePath,
268
+ branch: branchName,
269
+ error: "Worktree already exists",
270
+ };
271
+ }
272
+
273
+ // Create parent directory
274
+ fs.ensureDirSync(path.dirname(worktreePath));
275
+
276
+ const spinner = ora(`Creating worktree '${branchName}'...`).start();
277
+
278
+ try {
279
+ // Check if branch already exists
280
+ if (branchExists(branchName)) {
281
+ spinner.text = pc.dim(`Using existing branch '${branchName}'...`);
282
+ execSync(`git worktree add "${worktreePath}" "${branchName}"`, {
283
+ stdio: "pipe",
284
+ });
285
+ } else {
286
+ spinner.text = pc.dim(`Creating new branch '${branchName}'...`);
287
+ execSync(`git worktree add "${worktreePath}" -b "${branchName}"`, {
288
+ stdio: "pipe",
289
+ });
290
+ }
291
+ spinner.succeed(pc.green(`Worktree created: ${pc.cyan(branchName)}`));
292
+ } catch (e) {
293
+ spinner.fail(
294
+ pc.red(`Failed to create worktree '${branchName}': ${e.message}`),
295
+ );
296
+ return {
297
+ success: false,
298
+ path: worktreePath,
299
+ branch: branchName,
300
+ error: e.message,
301
+ };
302
+ }
303
+
304
+ // Install dependencies
305
+ if (installDeps) {
306
+ await detectAndInstallDeps(worktreePath);
307
+ }
308
+
309
+ return { success: true, path: worktreePath, branch: branchName };
310
+ }
311
+
312
+ /**
313
+ * Create new worktree(s)
314
+ */
315
+ async function cmdCreate(options = {}) {
316
+ const projectName = getProjectName();
317
+
318
+ // If branch is provided via flag, create worktree(s)
319
+ // Supports comma-separated branches: -b=feature/auth,feature/dashboard
320
+ if (options.branch) {
321
+ const branches = options.branch
322
+ .split(",")
323
+ .map((b) => b.trim())
324
+ .filter(Boolean);
325
+
326
+ if (branches.length === 1) {
327
+ // Single worktree
328
+ const result = await createSingleWorktree(projectName, branches[0]);
329
+ if (result.success) {
330
+ console.log(`\n${pc.bold(pc.green("✅ Worktree ready!"))}`);
331
+ console.log(` ${pc.bold("Path:")} ${result.path}`);
332
+ console.log(` ${pc.bold("Branch:")} ${result.branch}`);
333
+ console.log(`\n${pc.bold("To switch to this worktree, run:")}`);
334
+ console.log(pc.cyan(` cd ${result.path}`));
335
+ console.log();
336
+ }
337
+ return;
338
+ }
339
+
340
+ // Multiple worktrees via flag
341
+ const results = [];
342
+ for (const branchName of branches) {
343
+ const result = await createSingleWorktree(projectName, branchName, false);
344
+ results.push(result);
345
+ }
346
+
347
+ // Install dependencies for all successful worktrees
348
+ const successful = results.filter((r) => r.success);
349
+ if (successful.length > 0) {
350
+ console.log(`\n${pc.dim("Installing dependencies...")}`);
351
+ for (const result of successful) {
352
+ await detectAndInstallDeps(result.path);
353
+ }
354
+ }
355
+
356
+ // Summary
357
+ const failed = results.filter((r) => !r.success);
358
+
359
+ console.log(
360
+ `\n${pc.bold(pc.green(`✅ ${successful.length} worktree${successful.length !== 1 ? "s" : ""} created!`))}`,
361
+ );
362
+
363
+ if (successful.length > 0) {
364
+ console.log(`\n${pc.bold("Created worktrees:")}`);
365
+ for (const result of successful) {
366
+ console.log(` ${pc.green("●")} ${pc.bold(result.branch)}`);
367
+ console.log(` ${pc.cyan(result.path)}`);
368
+ }
369
+ }
370
+
371
+ if (failed.length > 0) {
372
+ console.log(`\n${pc.bold(pc.red(`Failed (${failed.length}):`))}`);
373
+ for (const result of failed) {
374
+ console.log(` ${pc.red("●")} ${result.branch}: ${result.error}`);
375
+ }
376
+ }
377
+
378
+ console.log();
379
+ return;
380
+ }
381
+
382
+ // Ask how many worktrees to create
383
+ const { count } = await prompts({
384
+ type: "number",
385
+ name: "count",
386
+ message: "How many worktrees to create?",
387
+ initial: 1,
388
+ min: 1,
389
+ max: 10,
390
+ validate: (value) => {
391
+ if (value < 1) return "Must create at least 1 worktree";
392
+ if (value > 10) return "Maximum 10 worktrees at once";
393
+ return true;
394
+ },
395
+ });
396
+
397
+ if (count === undefined) {
398
+ console.log(pc.yellow("\n⚠ Operation cancelled.\n"));
399
+ return;
400
+ }
401
+
402
+ // Collect branch names
403
+ const branches = [];
404
+ for (let i = 0; i < count; i++) {
405
+ const { branch } = await prompts({
406
+ type: "text",
407
+ name: "branch",
408
+ message: count > 1 ? `Branch name (${i + 1}/${count}):` : "Branch name:",
409
+ initial: "feature/",
410
+ validate: (value) => {
411
+ if (!value.trim()) return "Branch name is required";
412
+ if (value.includes(" ")) return "Branch name cannot contain spaces";
413
+ if (branches.includes(value)) return "Branch name already entered";
414
+ return true;
415
+ },
416
+ });
417
+
418
+ if (!branch) {
419
+ console.log(pc.yellow("\n⚠ Operation cancelled.\n"));
420
+ return;
421
+ }
422
+ branches.push(branch);
423
+ }
424
+
425
+ console.log();
426
+
427
+ // Create all worktrees (without installing deps yet for speed)
428
+ const results = [];
429
+ for (const branchName of branches) {
430
+ const result = await createSingleWorktree(projectName, branchName, false);
431
+ results.push(result);
432
+ }
433
+
434
+ // Install dependencies for all successful worktrees
435
+ const successful = results.filter((r) => r.success);
436
+ if (successful.length > 0) {
437
+ console.log(`\n${pc.dim("Installing dependencies...")}`);
438
+ for (const result of successful) {
439
+ await detectAndInstallDeps(result.path);
440
+ }
441
+ }
442
+
443
+ // Summary
444
+ const failed = results.filter((r) => !r.success);
445
+
446
+ console.log(
447
+ `\n${pc.bold(pc.green(`✅ ${successful.length} worktree${successful.length !== 1 ? "s" : ""} created!`))}`,
448
+ );
449
+
450
+ if (successful.length > 0) {
451
+ console.log(`\n${pc.bold("Created worktrees:")}`);
452
+ for (const result of successful) {
453
+ console.log(` ${pc.green("●")} ${pc.bold(result.branch)}`);
454
+ console.log(` ${pc.cyan(result.path)}`);
455
+ }
456
+ }
457
+
458
+ if (failed.length > 0) {
459
+ console.log(`\n${pc.bold(pc.red(`Failed (${failed.length}):`))}`);
460
+ for (const result of failed) {
461
+ console.log(` ${pc.red("●")} ${result.branch}: ${result.error}`);
462
+ }
463
+ }
464
+
465
+ if (successful.length === 1) {
466
+ console.log(`\n${pc.bold("To switch to this worktree, run:")}`);
467
+ console.log(pc.cyan(` cd ${successful[0].path}`));
468
+ } else if (successful.length > 1) {
469
+ console.log(`\n${pc.bold("To switch to a worktree, run:")}`);
470
+ console.log(pc.cyan(` cd <worktree-path>`));
471
+ }
472
+
473
+ console.log();
474
+ }
475
+
476
+ /**
477
+ * List all worktrees for current project
478
+ */
479
+ async function cmdList() {
480
+ const projectName = getProjectName();
481
+ const worktrees = getWorktrees(projectName);
482
+
483
+ console.log(`\n${pc.bold(`Worktrees for '${pc.cyan(projectName)}':`)}\n`);
484
+
485
+ if (worktrees.length === 0) {
486
+ console.log(pc.dim(" No worktrees found\n"));
487
+ return;
488
+ }
489
+
490
+ for (const wt of worktrees) {
491
+ console.log(` ${pc.green("●")} ${pc.bold(wt.branch)}`);
492
+ console.log(` ${pc.blue(wt.path)}`);
493
+ }
494
+ console.log();
495
+ }
496
+
497
+ /**
498
+ * Switch to a worktree (show cd command)
499
+ */
500
+ async function cmdSwitch(options = {}) {
501
+ const projectName = getProjectName();
502
+ const worktrees = getWorktrees(projectName);
503
+
504
+ if (worktrees.length === 0) {
505
+ console.log(
506
+ pc.yellow(
507
+ "\n⚠ No worktrees found. Create one first with: basement -w create\n",
508
+ ),
509
+ );
510
+ return;
511
+ }
512
+
513
+ let selectedWorktree = options.branch;
514
+
515
+ if (!selectedWorktree) {
516
+ const { selected } = await prompts({
517
+ type: "select",
518
+ name: "selected",
519
+ message: "Switch to worktree:",
520
+ choices: worktrees.map((wt) => ({
521
+ title: wt.branch,
522
+ description: wt.path,
523
+ value: wt.branch,
524
+ })),
525
+ });
526
+
527
+ if (!selected) {
528
+ console.log(pc.yellow("\n⚠ Operation cancelled.\n"));
529
+ return;
530
+ }
531
+ selectedWorktree = selected;
532
+ }
533
+
534
+ const worktree = worktrees.find((wt) => wt.branch === selectedWorktree);
535
+
536
+ if (!worktree) {
537
+ console.log(pc.red(`\n✗ Worktree not found: ${selectedWorktree}\n`));
538
+ return;
539
+ }
540
+
541
+ console.log(`\n${pc.bold("To switch to this worktree, run:")}`);
542
+ console.log(pc.cyan(` cd ${worktree.path}`));
543
+ console.log();
544
+ }
545
+
546
+ /**
547
+ * Remove a worktree
548
+ */
549
+ async function cmdRemove(options = {}) {
550
+ const projectName = getProjectName();
551
+ const worktrees = getWorktrees(projectName);
552
+
553
+ if (worktrees.length === 0) {
554
+ console.log(pc.yellow("\n⚠ No worktrees found.\n"));
555
+ return;
556
+ }
557
+
558
+ let selectedWorktree = options.branch;
559
+
560
+ if (!selectedWorktree) {
561
+ const { selected } = await prompts({
562
+ type: "select",
563
+ name: "selected",
564
+ message: "Remove worktree:",
565
+ choices: worktrees.map((wt) => ({
566
+ title: wt.branch,
567
+ description: wt.path,
568
+ value: wt.branch,
569
+ })),
570
+ });
571
+
572
+ if (!selected) {
573
+ console.log(pc.yellow("\n⚠ Operation cancelled.\n"));
574
+ return;
575
+ }
576
+ selectedWorktree = selected;
577
+ }
578
+
579
+ const worktree = worktrees.find((wt) => wt.branch === selectedWorktree);
580
+
581
+ if (!worktree) {
582
+ console.log(pc.red(`\n✗ Worktree not found: ${selectedWorktree}\n`));
583
+ return;
584
+ }
585
+
586
+ // Confirm removal
587
+ if (!options.force) {
588
+ const { confirm } = await prompts({
589
+ type: "confirm",
590
+ name: "confirm",
591
+ message: `Remove worktree at ${pc.cyan(worktree.path)}?`,
592
+ initial: false,
593
+ });
594
+
595
+ if (!confirm) {
596
+ console.log(pc.yellow("\n⚠ Operation cancelled.\n"));
597
+ return;
598
+ }
599
+ }
600
+
601
+ // Ask about branch deletion
602
+ let deleteBranch = options.deleteBranch;
603
+ if (deleteBranch === undefined) {
604
+ const { shouldDelete } = await prompts({
605
+ type: "confirm",
606
+ name: "shouldDelete",
607
+ message: `Also delete branch '${worktree.branch}'?`,
608
+ initial: false,
609
+ });
610
+ deleteBranch = shouldDelete;
611
+ }
612
+
613
+ const spinner = ora("Removing worktree...").start();
614
+
615
+ try {
616
+ execSync(`git worktree remove "${worktree.path}" --force`, {
617
+ stdio: "pipe",
618
+ });
619
+ spinner.succeed(pc.green(`Worktree removed: ${worktree.path}`));
620
+ } catch {
621
+ spinner.warn(
622
+ pc.yellow("git worktree remove failed, removing directory manually..."),
623
+ );
624
+ try {
625
+ fs.removeSync(worktree.path);
626
+ spinner.succeed(pc.green(`Directory removed: ${worktree.path}`));
627
+ } catch (e) {
628
+ spinner.fail(pc.red(`Failed to remove: ${e.message}`));
629
+ return;
630
+ }
631
+ }
632
+
633
+ // Delete the branch if requested
634
+ if (deleteBranch) {
635
+ const branchSpinner = ora(
636
+ `Deleting branch '${worktree.branch}'...`,
637
+ ).start();
638
+ try {
639
+ execSync(`git branch -D "${worktree.branch}"`, { stdio: "pipe" });
640
+ branchSpinner.succeed(pc.green(`Branch deleted: ${worktree.branch}`));
641
+ } catch {
642
+ branchSpinner.warn(
643
+ pc.yellow(
644
+ "Could not delete branch (may be checked out elsewhere or already deleted)",
645
+ ),
646
+ );
647
+ }
648
+ }
649
+
650
+ console.log();
651
+ }
652
+
653
+ /**
654
+ * Prune stale worktrees
655
+ */
656
+ async function cmdPrune() {
657
+ const projectName = getProjectName();
658
+
659
+ const spinner = ora("Pruning stale worktree references...").start();
660
+
661
+ try {
662
+ execSync("git worktree prune", { stdio: "pipe" });
663
+ spinner.succeed(pc.green("Git worktree references pruned"));
664
+ } catch (e) {
665
+ spinner.warn(pc.yellow(`git worktree prune warning: ${e.message}`));
666
+ }
667
+
668
+ const projectWorktreeDir = path.join(WORKTREE_BASE, projectName);
669
+
670
+ if (!fs.existsSync(projectWorktreeDir)) {
671
+ console.log(pc.green("\n✓ No worktrees to clean up\n"));
672
+ return;
673
+ }
674
+
675
+ const checkSpinner = ora(
676
+ "Checking for orphaned worktree directories...",
677
+ ).start();
678
+
679
+ let pruned = 0;
680
+ const dirs = fs.readdirSync(projectWorktreeDir, { withFileTypes: true });
681
+
682
+ for (const dir of dirs) {
683
+ if (dir.isDirectory()) {
684
+ const worktreePath = path.join(projectWorktreeDir, dir.name);
685
+ const gitFile = path.join(worktreePath, ".git");
686
+
687
+ let isValid = false;
688
+
689
+ if (fs.existsSync(gitFile)) {
690
+ try {
691
+ const gitContent = fs.readFileSync(gitFile, "utf-8");
692
+ const gitDirMatch = gitContent.match(/gitdir: (.+)/);
693
+ if (gitDirMatch && fs.existsSync(gitDirMatch[1].trim())) {
694
+ isValid = true;
695
+ }
696
+ } catch {
697
+ // invalid
698
+ }
699
+ }
700
+
701
+ if (!isValid) {
702
+ console.log(pc.yellow(` Removing orphaned: ${worktreePath}`));
703
+ fs.removeSync(worktreePath);
704
+ pruned++;
705
+ }
706
+ }
707
+ }
708
+
709
+ // Remove empty project directory
710
+ const remaining = fs.readdirSync(projectWorktreeDir);
711
+ if (remaining.length === 0) {
712
+ fs.rmdirSync(projectWorktreeDir);
713
+ }
714
+
715
+ if (pruned === 0) {
716
+ checkSpinner.succeed(pc.green("No orphaned directories found"));
717
+ } else {
718
+ checkSpinner.succeed(
719
+ pc.green(
720
+ `Pruned ${pruned} orphaned director${pruned === 1 ? "y" : "ies"}`,
721
+ ),
722
+ );
723
+ }
724
+
725
+ console.log();
726
+ }
727
+
728
+ /**
729
+ * Main worktree command
730
+ */
731
+ export async function worktreeCommand(options = {}) {
732
+ // Verify we're in a git repo
733
+ if (!isGitRepo()) {
734
+ console.log(pc.red("\n✗ Not in a git repository\n"));
735
+ process.exit(1);
736
+ }
737
+
738
+ // Ensure worktree base directory exists
739
+ fs.ensureDirSync(WORKTREE_BASE);
740
+
741
+ let action = options.action;
742
+
743
+ // If action specified via flag, use it directly
744
+ if (!action) {
745
+ const { selectedAction } = await prompts({
746
+ type: "select",
747
+ name: "selectedAction",
748
+ message: "What would you like to do?",
749
+ choices: [
750
+ {
751
+ title: "Create worktree",
752
+ description: "Create a new worktree with a branch",
753
+ value: "create",
754
+ },
755
+ {
756
+ title: "List worktrees",
757
+ description: "Show all worktrees for this project",
758
+ value: "list",
759
+ },
760
+ {
761
+ title: "Switch worktree",
762
+ description: "Get cd command to switch to a worktree",
763
+ value: "switch",
764
+ },
765
+ {
766
+ title: "Remove worktree",
767
+ description: "Remove a worktree and optionally its branch",
768
+ value: "remove",
769
+ },
770
+ {
771
+ title: "Prune worktrees",
772
+ description: "Clean up stale worktrees and orphaned directories",
773
+ value: "prune",
774
+ },
775
+ ],
776
+ });
777
+
778
+ if (!selectedAction) {
779
+ console.log(pc.yellow("\n⚠ Operation cancelled.\n"));
780
+ return;
781
+ }
782
+
783
+ action = selectedAction;
784
+ }
785
+
786
+ switch (action) {
787
+ case "create":
788
+ await cmdCreate(options);
789
+ break;
790
+ case "list":
791
+ await cmdList();
792
+ break;
793
+ case "switch":
794
+ await cmdSwitch(options);
795
+ break;
796
+ case "remove":
797
+ await cmdRemove(options);
798
+ break;
799
+ case "prune":
800
+ await cmdPrune();
801
+ break;
802
+ default:
803
+ console.log(pc.red(`\n✗ Unknown action: ${action}\n`));
804
+ }
805
+ }