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,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
|
+
}
|