git-worktree-organize 1.0.13 → 1.1.1

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 (3) hide show
  1. package/README.md +247 -26
  2. package/dist/cli.js +767 -110
  3. package/package.json +6 -5
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve3, join as join3, dirname as dirname2, basename as basename2 } from "node:path";
5
- import { existsSync as existsSync3 } from "node:fs";
4
+ import { resolve as resolve3, join as join5, dirname as dirname3, basename as basename3 } from "node:path";
5
+ import { existsSync as existsSync5, statSync as statSync5, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
6
6
 
7
7
  // src/run.ts
8
8
  import { spawnSync } from "node:child_process";
@@ -46,8 +46,7 @@ async function detect(repoPath) {
46
46
  }
47
47
  } else if (stat.isFile()) {
48
48
  const contents = readFileSync(gitEntryPath, "utf8");
49
- const firstLine = contents.split(`
50
- `)[0].trim();
49
+ const firstLine = contents.split("\n")[0].trim();
51
50
  const match = firstLine.match(/^gitdir:\s*(.+)$/);
52
51
  if (!match) {
53
52
  throw new Error(`not a git repository: ${repoPath}`);
@@ -80,10 +79,8 @@ function parsePorcelain(output) {
80
79
  const worktrees = [];
81
80
  const blocks = output.trim().split(/\n\n+/);
82
81
  for (const block of blocks) {
83
- if (!block.trim())
84
- continue;
85
- const lines = block.trim().split(`
86
- `);
82
+ if (!block.trim()) continue;
83
+ const lines = block.trim().split("\n");
87
84
  let path = "";
88
85
  let head = "";
89
86
  let branch = null;
@@ -113,131 +110,296 @@ async function listWorktrees(repoPath) {
113
110
  }
114
111
 
115
112
  // src/migrate.ts
116
- import { mkdirSync, writeFileSync, readFileSync as readFileSync2, existsSync as existsSync2, renameSync, statSync as statSync2, readdirSync } from "node:fs";
117
- import { join as join2, dirname, basename, resolve as resolve2 } from "node:path";
113
+ import { mkdirSync, writeFileSync, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync3, readdirSync, renameSync as renameSync2 } from "node:fs";
114
+ import { join as join2, dirname as dirname2, basename, resolve as resolve2 } from "node:path";
118
115
 
119
116
  // src/git.ts
120
117
  async function git(args, options) {
121
118
  const result = run("git", args, {
122
119
  cwd: options?.cwd,
123
- env: options?.env ? { ...process.env, ...options.env } : undefined
120
+ env: options?.env ? { ...process.env, ...options.env } : void 0
124
121
  });
125
122
  return result.stdout;
126
123
  }
127
124
  async function setGitConfig(key, value, options) {
128
125
  const env = {};
129
- if (options?.gitdir)
130
- env["GIT_DIR"] = options.gitdir;
126
+ if (options?.gitdir) env["GIT_DIR"] = options.gitdir;
131
127
  await git(["config", key, value], { cwd: options?.cwd, env });
132
128
  }
133
129
 
134
- // src/migrate.ts
135
- async function moveDir(src, dest) {
130
+ // src/fs.ts
131
+ import { statSync as statSync2, renameSync, existsSync as existsSync2 } from "node:fs";
132
+ import { dirname } from "node:path";
133
+ async function move(src, dest) {
136
134
  const destForStat = existsSync2(dest) ? dest : dirname(dest);
137
- if (statSync2(src).dev === statSync2(destForStat).dev) {
135
+ if (samefs(src, destForStat)) {
138
136
  renameSync(src, dest);
139
137
  } else {
140
138
  run("cp", ["-a", src, dest]);
141
139
  run("rm", ["-rf", src]);
142
140
  }
143
141
  }
142
+ function samefs(a, b) {
143
+ return statSync2(a).dev === statSync2(b).dev;
144
+ }
145
+
146
+ // src/migrate.ts
147
+ var AGENTS_MD_TEMPLATE = `# Git Worktree Layout
148
+
149
+ This repository uses **git worktrees** with a bare repository pattern for parallel development across multiple branches.
150
+
151
+ ## Directory Structure
152
+
153
+ \`\`\`
154
+ <project>/ # Root project directory
155
+ \u251C\u2500\u2500 .bare/ # Bare git repository (shared git data)
156
+ \u2502 \u251C\u2500\u2500 worktrees/ # Worktree metadata
157
+ \u2502 \u2502 \u251C\u2500\u2500 main/ # Main branch metadata
158
+ \u2502 \u2502 \u2514\u2500\u2500 <branch-name>/ # Per-branch worktree metadata
159
+ \u2502 \u251C\u2500\u2500 objects/ # Git objects (shared)
160
+ \u2502 \u251C\u2500\u2500 refs/ # Git refs (shared)
161
+ \u2502 \u2514\u2500\u2500 config # Repository config
162
+ \u251C\u2500\u2500 .git # Points to .bare (gitdir: ./.bare)
163
+ \u251C\u2500\u2500 main/ # Main branch worktree (primary)
164
+ \u251C\u2500\u2500 <branch-name>/ # Feature/fix branch worktrees
165
+ \u2514\u2500\u2500 *.code-workspace # VS Code multi-root workspace
166
+ \`\`\`
167
+
168
+ ## How It Works
169
+
170
+ - **Bare Repository**: \`.bare/\` contains all git data (objects, refs, config)
171
+ - **Worktrees**: Each branch checkout is a separate directory at the root level
172
+ - **Shared History**: All worktrees share the same git history from \`.bare/\`
173
+
174
+ ## Working with Worktrees
175
+
176
+ ### Create a new worktree
177
+
178
+ \`\`\`bash
179
+ # From any worktree or the root
180
+ git worktree add <branch-name>
181
+
182
+ # Create new branch and worktree
183
+ git worktree add -b <new-branch> <directory-name>
184
+ \`\`\`
185
+
186
+ ### List worktrees
187
+
188
+ \`\`\`bash
189
+ git worktree list
190
+ \`\`\`
191
+
192
+ ### Remove a worktree
193
+
194
+ \`\`\`bash
195
+ # After merging/deleting the branch
196
+ git worktree remove <branch-name>
197
+
198
+ # Force removal (if untracked files exist)
199
+ git worktree remove --force <branch-name>
200
+ \`\`\`
201
+
202
+ ### Prune stale worktree references
203
+
204
+ \`\`\`bash
205
+ git worktree prune
206
+ \`\`\`
207
+
208
+ ## Conventions
209
+
210
+ 1. **Naming**: Worktree directories match the branch name (e.g., \`feature-auth\`, \`fix-login-bug\`)
211
+ 2. **Main worktree**: \`main/\` is the primary worktree for the main branch
212
+ 3. **Workspace file**: Open \`*.code-workspace\` in VS Code to work with multiple worktrees
213
+
214
+ ## Tips
215
+
216
+ - Each worktree has its own \`.git\` file pointing back to \`.bare/\`
217
+ - You can run different branches simultaneously without stashing
218
+ - IDEs can open multiple worktrees as separate folders in one workspace
219
+ - Run \`git worktree prune\` periodically to clean up deleted worktree references
220
+ `;
221
+ function writeAgentsMd(dest) {
222
+ const agentsPath = join2(dest, "AGENTS.md");
223
+ if (!existsSync3(agentsPath)) {
224
+ writeFileSync(agentsPath, AGENTS_MD_TEMPLATE);
225
+ }
226
+ }
227
+ function worktreeConfigEnabled(bareDir) {
228
+ const configFile = join2(bareDir, "config");
229
+ if (!existsSync3(configFile)) return false;
230
+ const content = readFileSync2(configFile, "utf8");
231
+ const extensionsMatch = content.match(/\[extensions\]([\s\S]*?)(?=\[|$)/i);
232
+ if (!extensionsMatch) return false;
233
+ return /worktreeconfig\s*=\s*true/i.test(extensionsMatch[1]);
234
+ }
235
+ var WORKTREE_CONFIG_CONTENT = "[core]\n bare = false\n";
236
+ function ensureWorktreeConfig(adminDir, bareDir, log) {
237
+ if (!worktreeConfigEnabled(bareDir)) return;
238
+ const configWtFile = join2(adminDir, "config.worktree");
239
+ if (!existsSync3(configWtFile) || readFileSync2(configWtFile, "utf8") !== WORKTREE_CONFIG_CONTENT) {
240
+ log?.(`Writing config.worktree for [${basename(adminDir)}]`);
241
+ writeFileSync(configWtFile, WORKTREE_CONFIG_CONTENT);
242
+ }
243
+ }
144
244
  function sanitizeBranch(branch) {
145
245
  return branch.replace(/\//g, "-");
146
246
  }
147
247
  function resolveWorktreePath(worktreePath, dest, sourceParent) {
148
- if (existsSync2(worktreePath))
149
- return worktreePath;
248
+ if (existsSync3(worktreePath)) return worktreePath;
150
249
  if (worktreePath.startsWith(dest + "/")) {
151
250
  const remapped = sourceParent + worktreePath.slice(dest.length);
152
- if (existsSync2(remapped))
153
- return remapped;
251
+ if (existsSync3(remapped)) return remapped;
154
252
  }
155
253
  return worktreePath;
156
254
  }
157
255
  function isPartialMigration(dest) {
158
256
  const gitFile = join2(dest, ".git");
159
- return existsSync2(join2(dest, ".bare")) && existsSync2(gitFile) && statSync2(gitFile).isFile();
257
+ return existsSync3(join2(dest, ".bare")) && existsSync3(gitFile) && statSync3(gitFile).isFile();
160
258
  }
161
259
  function findHub(startPath) {
162
260
  let current = resolve2(startPath);
163
261
  while (true) {
164
- if (isPartialMigration(current))
165
- return current;
166
- const parent = dirname(current);
167
- if (parent === current)
168
- return null;
262
+ if (isPartialMigration(current)) return current;
263
+ const parent = dirname2(current);
264
+ if (parent === current) return null;
169
265
  current = parent;
170
266
  }
171
267
  }
172
268
  async function repairHub(dest, log = console.log) {
173
269
  const adminBase = join2(dest, ".bare", "worktrees");
174
- if (!existsSync2(adminBase))
175
- return;
270
+ if (!existsSync3(adminBase)) return;
176
271
  for (const adminName of readdirSync(adminBase)) {
177
272
  const adminDir = join2(adminBase, adminName);
178
- if (!statSync2(adminDir).isDirectory())
179
- continue;
273
+ if (!statSync3(adminDir).isDirectory()) continue;
180
274
  const gitdirFile = join2(adminDir, "gitdir");
181
- if (!existsSync2(gitdirFile))
182
- continue;
275
+ if (!existsSync3(gitdirFile)) continue;
183
276
  const registeredGitFile = readFileSync2(gitdirFile, "utf8").trim();
184
- const worktreePath = dirname(registeredGitFile);
185
- if (!worktreePath.startsWith(dest + "/"))
186
- continue;
187
- if (!existsSync2(registeredGitFile) || !statSync2(registeredGitFile).isFile())
188
- continue;
277
+ const worktreePath = dirname2(registeredGitFile);
278
+ if (!worktreePath.startsWith(dest + "/")) continue;
279
+ if (!existsSync3(registeredGitFile) || !statSync3(registeredGitFile).isFile()) continue;
280
+ const commondirFile = join2(adminDir, "commondir");
281
+ const expectedCommondir = "../../\n";
282
+ if (!existsSync3(commondirFile) || readFileSync2(commondirFile, "utf8") !== expectedCommondir) {
283
+ log(`Repairing commondir for [${basename(worktreePath)}]`);
284
+ writeFileSync(commondirFile, expectedCommondir);
285
+ }
286
+ ensureWorktreeConfig(adminDir, join2(dest, ".bare"), log);
189
287
  const content = readFileSync2(registeredGitFile, "utf8");
190
288
  const match = content.match(/^gitdir:\s*(.+)/m);
191
- if (!match)
192
- continue;
193
- if (match[1].trim() === adminDir)
194
- continue;
289
+ if (!match) continue;
290
+ if (match[1].trim() === adminDir) continue;
195
291
  log(`Repairing .git for [${basename(worktreePath)}]`);
196
292
  writeFileSync(registeredGitFile, `gitdir: ${adminDir}
197
293
  `);
198
294
  }
199
295
  }
200
- async function resumeMigrate(dest, log = console.log) {
296
+ async function resumeMigrate(dest, log = console.log, warn) {
201
297
  const destBare = join2(dest, ".bare");
202
298
  const hubWorktrees = await listWorktrees(dest);
203
299
  const pending = hubWorktrees.filter((wt) => {
204
- if (wt.isBare)
205
- return false;
300
+ if (wt.isBare) return false;
206
301
  const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
207
302
  return wt.path !== join2(dest, sanitizeBranch(branch));
208
303
  });
209
304
  if (pending.length === 0) {
210
- log("Nothing to resume all worktrees are already in place.");
305
+ log("Nothing to resume \u2014 all worktrees are already in place.");
211
306
  } else {
212
307
  for (const wt of pending) {
213
308
  const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
214
309
  const expectedPath = join2(dest, sanitizeBranch(branch));
215
310
  let wtPath = wt.path;
216
- if (!existsSync2(wtPath)) {
217
- if (existsSync2(expectedPath)) {
311
+ if (!existsSync3(wtPath)) {
312
+ if (existsSync3(expectedPath)) {
218
313
  wtPath = expectedPath;
219
314
  } else {
220
- log(`warn: Skipping [${branch}] path no longer exists: ${wt.path}`);
315
+ log(`warn: Skipping [${branch}] \u2014 path no longer exists: ${wt.path}`);
221
316
  continue;
222
317
  }
223
318
  }
224
- log(`Moving [${branch}] ${expectedPath}`);
225
- await processLinkedWorktree({ ...wt, path: wtPath }, dest, destBare);
319
+ log(`Moving [${branch}] \u2192 ${expectedPath}`);
320
+ await processLinkedWorktree({ ...wt, path: wtPath }, dest, destBare, log, warn);
226
321
  }
227
322
  }
228
323
  await repairHub(dest, log);
229
324
  return dest;
230
325
  }
231
- async function migrate(config, options) {
326
+ async function migrateInPlace(source, log = console.log, warn) {
327
+ const resolvedSource = resolve2(source);
328
+ const oldPath = resolvedSource + ".old";
329
+ if (existsSync3(oldPath)) {
330
+ throw new Error(`'${oldPath}' already exists. Remove it and try again.`);
331
+ }
332
+ log(`Renaming ${bold(resolvedSource)} to ${bold(oldPath)}`);
333
+ await move(resolvedSource, oldPath);
334
+ const allWorktrees = await listWorktrees(oldPath);
335
+ const worktrees = allWorktrees.filter((wt) => !wt.isBare);
336
+ if (worktrees.length === 0) {
337
+ throw new Error("No worktrees found in source repository");
338
+ }
339
+ const seen = /* @__PURE__ */ new Map();
340
+ for (const wt of worktrees) {
341
+ const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
342
+ const safe = sanitizeBranch(branch);
343
+ if (seen.has(safe)) {
344
+ throw new Error(`branch name collision: '${seen.get(safe)}' and '${branch}' both map to '${safe}'`);
345
+ }
346
+ seen.set(safe, branch);
347
+ }
348
+ const mainBranch = worktrees[0].branch;
349
+ const mainSafe = sanitizeBranch(mainBranch);
350
+ const destBare = join2(resolvedSource, ".bare");
351
+ const mainDest = join2(resolvedSource, mainSafe);
352
+ mkdirSync(destBare, { recursive: true });
353
+ const gitDir = join2(oldPath, ".git");
354
+ for (const entry of readdirSync(gitDir)) {
355
+ const srcPath = join2(gitDir, entry);
356
+ const destPath = join2(destBare, entry);
357
+ if (statSync3(srcPath).isDirectory()) {
358
+ run("cp", ["-a", srcPath + "/.", destPath + "/"]);
359
+ } else {
360
+ run("cp", ["-a", srcPath, destPath]);
361
+ }
362
+ }
363
+ await setGitConfig("core.bare", "true", { gitdir: destBare });
364
+ await setGitConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*", { gitdir: destBare });
365
+ writeFileSync(join2(resolvedSource, ".git"), "gitdir: ./.bare\n");
366
+ const mainHeadContent = readFileSync2(join2(destBare, "HEAD"), "utf8");
367
+ log(`Creating main worktree at ${bold(mainDest)}`);
368
+ run("cp", ["-a", oldPath + "/.", mainDest + "/"]);
369
+ const mainAdminDir = join2(destBare, "worktrees", mainSafe);
370
+ mkdirSync(mainAdminDir, { recursive: true });
371
+ writeFileSync(join2(mainAdminDir, "gitdir"), mainDest + "/.git\n");
372
+ writeFileSync(join2(mainAdminDir, "commondir"), "../../\n");
373
+ ensureWorktreeConfig(mainAdminDir, destBare);
374
+ const headToWrite = mainHeadContent.endsWith("\n") ? mainHeadContent : mainHeadContent + "\n";
375
+ writeFileSync(join2(mainAdminDir, "HEAD"), headToWrite);
376
+ const bareIndex = join2(destBare, "index");
377
+ if (existsSync3(bareIndex)) {
378
+ renameSync2(bareIndex, join2(mainAdminDir, "index"));
379
+ }
380
+ run("rm", ["-rf", join2(mainDest, ".git")]);
381
+ writeFileSync(join2(mainDest, ".git"), `gitdir: ${mainAdminDir}
382
+ `);
383
+ for (let i = 1; i < worktrees.length; i++) {
384
+ await processLinkedWorktree(worktrees[i], resolvedSource, destBare, log, warn);
385
+ }
386
+ log(`Original repo backed up at: ${oldPath}`);
387
+ writeAgentsMd(resolvedSource);
388
+ return resolvedSource;
389
+ }
390
+ function bold(s) {
391
+ return `\x1B[1m${s}\x1B[0m`;
392
+ }
393
+ async function migrate(config, options, log, warn) {
232
394
  const source = resolve2(options.source);
233
- const dest = options.dest ? resolve2(options.dest) : join2(dirname(source), basename(source) + "-bare");
395
+ const dest = options.dest ? resolve2(options.dest) : join2(dirname2(source), basename(source) + "-bare");
234
396
  const destBare = join2(dest, ".bare");
235
- if (existsSync2(destBare)) {
397
+ if (existsSync3(destBare)) {
236
398
  throw new Error(`'${destBare}' already exists`);
237
399
  }
238
400
  const allWorktrees = await listWorktrees(source);
239
401
  const worktrees = allWorktrees.filter((wt) => !wt.isBare);
240
- const seen = new Map;
402
+ const seen = /* @__PURE__ */ new Map();
241
403
  for (const wt of worktrees) {
242
404
  const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
243
405
  const safe = sanitizeBranch(branch);
@@ -250,53 +412,52 @@ async function migrate(config, options) {
250
412
  run("cp", ["-a", config.gitdir + "/.", destBare + "/"]);
251
413
  await setGitConfig("core.bare", "true", { gitdir: destBare });
252
414
  await setGitConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*", { gitdir: destBare });
253
- writeFileSync(join2(dest, ".git"), `gitdir: ./.bare
254
- `);
255
- const sourceParent = dirname(source);
256
- const worktreesResolved = worktrees.map((wt, i) => i === 0 && config.type === "standard" ? wt : { ...wt, path: resolveWorktreePath(wt.path, dest, sourceParent) });
415
+ writeFileSync(join2(dest, ".git"), "gitdir: ./.bare\n");
416
+ const sourceParent = dirname2(source);
417
+ const worktreesResolved = worktrees.map(
418
+ (wt, i) => i === 0 && config.type === "standard" ? wt : { ...wt, path: resolveWorktreePath(wt.path, dest, sourceParent) }
419
+ );
257
420
  if (config.type === "standard") {
258
421
  const mainBranch = worktrees[0].branch;
259
422
  const mainSafe = sanitizeBranch(mainBranch);
260
423
  const mainDest = join2(dest, mainSafe);
261
424
  const mainHeadContent = readFileSync2(join2(destBare, "HEAD"), "utf8");
262
425
  run("rm", ["-rf", join2(source, ".git")]);
263
- await moveDir(source, mainDest);
426
+ await move(source, mainDest);
264
427
  const mainAdminDir = join2(destBare, "worktrees", mainSafe);
265
428
  mkdirSync(mainAdminDir, { recursive: true });
266
- writeFileSync(join2(mainAdminDir, "gitdir"), mainDest + `/.git
267
- `);
268
- writeFileSync(join2(mainAdminDir, "commondir"), `../../
269
- `);
270
- const headToWrite = mainHeadContent.endsWith(`
271
- `) ? mainHeadContent : mainHeadContent + `
272
- `;
429
+ writeFileSync(join2(mainAdminDir, "gitdir"), mainDest + "/.git\n");
430
+ writeFileSync(join2(mainAdminDir, "commondir"), "../../\n");
431
+ ensureWorktreeConfig(mainAdminDir, destBare);
432
+ const headToWrite = mainHeadContent.endsWith("\n") ? mainHeadContent : mainHeadContent + "\n";
273
433
  writeFileSync(join2(mainAdminDir, "HEAD"), headToWrite);
274
434
  const bareIndex = join2(destBare, "index");
275
- if (existsSync2(bareIndex)) {
276
- renameSync(bareIndex, join2(mainAdminDir, "index"));
435
+ if (existsSync3(bareIndex)) {
436
+ renameSync2(bareIndex, join2(mainAdminDir, "index"));
277
437
  }
278
438
  writeFileSync(join2(mainDest, ".git"), `gitdir: ${mainAdminDir}
279
439
  `);
280
- for (let i = 1;i < worktreesResolved.length; i++) {
281
- await processLinkedWorktree(worktreesResolved[i], dest, destBare);
440
+ for (let i = 1; i < worktreesResolved.length; i++) {
441
+ await processLinkedWorktree(worktreesResolved[i], dest, destBare, log, warn);
282
442
  }
283
443
  } else {
284
444
  for (const wt of worktreesResolved) {
285
- await processLinkedWorktree(wt, dest, destBare);
445
+ await processLinkedWorktree(wt, dest, destBare, log, warn);
286
446
  }
287
447
  }
448
+ writeAgentsMd(dest);
288
449
  return dest;
289
450
  }
290
- async function processLinkedWorktree(wt, dest, destBare) {
451
+ async function processLinkedWorktree(wt, dest, destBare, log, warn) {
291
452
  const wtSrc = wt.path;
292
453
  const wtBranch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
293
454
  const wtSafe = sanitizeBranch(wtBranch);
294
455
  const wtDest = join2(dest, wtSafe);
295
- await moveDir(wtSrc, wtDest);
456
+ await move(wtSrc, wtDest);
296
457
  const gitFileContent = readFileSync2(join2(wtDest, ".git"), "utf8");
297
458
  const match = gitFileContent.match(/^gitdir:\s*(.+)/m);
298
459
  if (!match) {
299
- console.warn(`Could not parse .git file in ${wtDest}`);
460
+ warn?.(`Could not parse .git file in ${wtDest}`);
300
461
  return;
301
462
  }
302
463
  const oldPath = match[1].trim();
@@ -304,17 +465,102 @@ async function processLinkedWorktree(wt, dest, destBare) {
304
465
  const newAdmin = join2(destBare, "worktrees", adminName);
305
466
  writeFileSync(join2(wtDest, ".git"), `gitdir: ${newAdmin}
306
467
  `);
307
- if (existsSync2(newAdmin)) {
308
- writeFileSync(join2(newAdmin, "gitdir"), wtDest + `/.git
309
- `);
468
+ if (existsSync3(newAdmin)) {
469
+ writeFileSync(join2(newAdmin, "gitdir"), wtDest + "/.git\n");
470
+ writeFileSync(join2(newAdmin, "commondir"), "../../\n");
471
+ ensureWorktreeConfig(newAdmin, destBare);
310
472
  } else {
311
- console.warn(`Admin dir ${newAdmin} does not exist for worktree ${wtDest}`);
473
+ warn?.(`Admin dir ${newAdmin} does not exist for worktree ${wtDest}`);
312
474
  }
313
475
  }
314
476
 
477
+ // src/recover.ts
478
+ import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
479
+ import { join as join3, basename as basename2 } from "node:path";
480
+ async function searchForWorktree(branch, options, log) {
481
+ const sanitizedBranch = sanitizeBranch(branch);
482
+ const candidates = [];
483
+ for (const searchDir of options.searchDirs) {
484
+ searchAtDepth(searchDir, sanitizedBranch, 0, options.maxDepth, candidates, log);
485
+ }
486
+ const validCandidates = candidates.filter((c) => {
487
+ const gitPath = join3(c, ".git");
488
+ return existsSync4(gitPath) && statSync4(gitPath).isFile();
489
+ });
490
+ return {
491
+ branch,
492
+ sanitizedBranch,
493
+ foundPath: validCandidates.length === 1 ? validCandidates[0] : null,
494
+ candidates: validCandidates
495
+ };
496
+ }
497
+ function searchAtDepth(dir, targetName, currentDepth, maxDepth, candidates, log) {
498
+ if (currentDepth > maxDepth) return;
499
+ if (!existsSync4(dir)) return;
500
+ const entries = readdirSync2(dir);
501
+ for (const entry of entries) {
502
+ if (entry.startsWith(".") || entry === "node_modules") continue;
503
+ const fullPath = join3(dir, entry);
504
+ if (!statSync4(fullPath).isDirectory()) continue;
505
+ if (entry === targetName) {
506
+ log?.(`Found candidate: ${fullPath}`);
507
+ candidates.push(fullPath);
508
+ }
509
+ searchAtDepth(fullPath, targetName, currentDepth + 1, maxDepth, candidates, log);
510
+ }
511
+ }
512
+ async function findMissingWorktrees(hubPath, searchDirs, log) {
513
+ const worktrees = await listWorktrees(hubPath);
514
+ const results = [];
515
+ for (const wt of worktrees) {
516
+ if (wt.isBare) continue;
517
+ if (existsSync4(wt.path)) continue;
518
+ const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
519
+ log?.(`Searching for missing worktree [${branch}]...`);
520
+ const result = await searchForWorktree(branch, { searchDirs, maxDepth: 3 }, log);
521
+ results.push(result);
522
+ }
523
+ return results;
524
+ }
525
+ async function repairWorktree(worktreePath, hubPath, log) {
526
+ const bareDir = join3(hubPath, ".bare");
527
+ const adminBase = join3(bareDir, "worktrees");
528
+ const gitFile = join3(worktreePath, ".git");
529
+ const content = readFileSync3(gitFile, "utf8");
530
+ const match = content.match(/^gitdir:\s*(.+)/m);
531
+ if (!match) {
532
+ throw new Error(`Cannot parse .git file in ${worktreePath}`);
533
+ }
534
+ const oldAdminPath = match[1].trim();
535
+ const adminName = basename2(oldAdminPath);
536
+ const newAdminPath = join3(adminBase, adminName);
537
+ log?.(`Repairing ${worktreePath} -> ${newAdminPath}`);
538
+ writeFileSync2(gitFile, `gitdir: ${newAdminPath}
539
+ `);
540
+ const gitdirFile = join3(newAdminPath, "gitdir");
541
+ if (existsSync4(gitdirFile)) {
542
+ writeFileSync2(gitdirFile, `${worktreePath}/.git
543
+ `);
544
+ }
545
+ }
546
+
547
+ // src/version.ts
548
+ import { readFileSync as readFileSync4 } from "node:fs";
549
+ import { join as join4 } from "node:path";
550
+ function readVersion() {
551
+ const pkgPath = join4(import.meta.dirname, "..", "package.json");
552
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
553
+ return pkg.version;
554
+ }
555
+ var VERSION = readVersion();
556
+ function getVersion(withPrefix = false) {
557
+ return withPrefix ? `v${VERSION}` : VERSION;
558
+ }
559
+
315
560
  // src/cli.ts
316
561
  var GREEN = "\x1B[32m";
317
562
  var YELLOW = "\x1B[33m";
563
+ var RED = "\x1B[31m";
318
564
  var BOLD = "\x1B[1m";
319
565
  var RESET = "\x1B[0m";
320
566
  function green(s) {
@@ -323,7 +569,10 @@ function green(s) {
323
569
  function yellow(s) {
324
570
  return `${YELLOW}${s}${RESET}`;
325
571
  }
326
- function bold(s) {
572
+ function red(s) {
573
+ return `${RED}${s}${RESET}`;
574
+ }
575
+ function bold2(s) {
327
576
  return `${BOLD}${s}${RESET}`;
328
577
  }
329
578
  function prompt() {
@@ -332,37 +581,254 @@ function prompt() {
332
581
  process.stdin.once("data", (chunk) => res(chunk.toString().trim()));
333
582
  });
334
583
  }
584
+ function isGitPointerValid(worktreePath, hubPath) {
585
+ const gitFile = join5(worktreePath, ".git");
586
+ if (!existsSync5(gitFile) || !statSync5(gitFile).isFile()) {
587
+ return false;
588
+ }
589
+ const content = readFileSync5(gitFile, "utf8");
590
+ const match = content.match(/^gitdir:\s*(.+)$/m);
591
+ if (!match) {
592
+ return false;
593
+ }
594
+ const adminDir = match[1].trim();
595
+ const bareDir = join5(hubPath, ".bare");
596
+ if (!adminDir.includes(bareDir) || !adminDir.includes("/worktrees/")) {
597
+ return false;
598
+ }
599
+ if (!existsSync5(adminDir)) {
600
+ return false;
601
+ }
602
+ const adminGitdirFile = join5(adminDir, "gitdir");
603
+ if (!existsSync5(adminGitdirFile)) {
604
+ return false;
605
+ }
606
+ const adminGitdir = readFileSync5(adminGitdirFile, "utf8").trim();
607
+ if (adminGitdir !== gitFile) {
608
+ return false;
609
+ }
610
+ const commondirFile = join5(adminDir, "commondir");
611
+ if (!existsSync5(commondirFile) || readFileSync5(commondirFile, "utf8").trim() !== "../..") {
612
+ return false;
613
+ }
614
+ const sharedConfig = join5(hubPath, ".bare", "config");
615
+ if (existsSync5(sharedConfig)) {
616
+ const cfg = readFileSync5(sharedConfig, "utf8");
617
+ const extMatch = cfg.match(/\[extensions\]([\s\S]*?)(?=\[|$)/i);
618
+ if (extMatch && /worktreeconfig\s*=\s*true/i.test(extMatch[1])) {
619
+ const configWt = join5(adminDir, "config.worktree");
620
+ if (!existsSync5(configWt) || !readFileSync5(configWt, "utf8").includes("bare = false")) {
621
+ return false;
622
+ }
623
+ }
624
+ }
625
+ try {
626
+ run("git", ["-C", worktreePath, "rev-parse", "HEAD"]);
627
+ return true;
628
+ } catch {
629
+ return false;
630
+ }
631
+ }
632
+ function listWorktreesFromAdminDirs(hubPath) {
633
+ const adminBase = join5(hubPath, ".bare", "worktrees");
634
+ if (!existsSync5(adminBase)) return [];
635
+ const worktrees = [];
636
+ for (const adminName of readdirSync3(adminBase)) {
637
+ const adminDir = join5(adminBase, adminName);
638
+ if (!statSync5(adminDir).isDirectory()) continue;
639
+ const gitdirFile = join5(adminDir, "gitdir");
640
+ if (!existsSync5(gitdirFile)) continue;
641
+ const wtGitFile = readFileSync5(gitdirFile, "utf8").trim();
642
+ const wtPath = dirname3(wtGitFile);
643
+ const headFile = join5(adminDir, "HEAD");
644
+ let branch = null;
645
+ let head = "";
646
+ if (existsSync5(headFile)) {
647
+ const headContent = readFileSync5(headFile, "utf8").trim();
648
+ if (headContent.startsWith("ref: refs/heads/")) {
649
+ branch = headContent.slice("ref: refs/heads/".length);
650
+ } else {
651
+ head = headContent;
652
+ }
653
+ }
654
+ worktrees.push({ path: wtPath, head, branch, isBare: false });
655
+ }
656
+ return worktrees;
657
+ }
658
+ async function runValidationMode(hubPath) {
659
+ let worktrees;
660
+ try {
661
+ worktrees = await listWorktrees(hubPath);
662
+ } catch {
663
+ console.log(`${yellow("warn:")} git worktree list failed; scanning admin dirs to enumerate worktrees`);
664
+ worktrees = listWorktreesFromAdminDirs(hubPath);
665
+ }
666
+ const validated = [];
667
+ for (const wt of worktrees) {
668
+ if (wt.isBare) continue;
669
+ let status;
670
+ if (!existsSync5(wt.path)) {
671
+ status = "missing";
672
+ } else if (!isGitPointerValid(wt.path, hubPath)) {
673
+ status = "stale";
674
+ } else {
675
+ status = "healthy";
676
+ }
677
+ validated.push({ worktree: wt, status });
678
+ }
679
+ console.log();
680
+ console.log(bold2("Validation Report"));
681
+ console.log(`Hub: ${hubPath}`);
682
+ console.log();
683
+ if (validated.length === 0) {
684
+ console.log("No worktrees found.");
685
+ return;
686
+ }
687
+ const maxBranchLen = validated.reduce((m, v) => {
688
+ const branch = v.worktree.branch ?? `detached-${v.worktree.head.slice(0, 8)}`;
689
+ return Math.max(m, branch.length);
690
+ }, 0);
691
+ const headerBranch = "Branch".padEnd(maxBranchLen);
692
+ const headerStatus = "Status";
693
+ const headerPath = "Path";
694
+ console.log(` ${bold2(headerBranch)} ${bold2(headerStatus.padEnd(7))} ${bold2(headerPath)}`);
695
+ const counts = { healthy: 0, missing: 0, stale: 0 };
696
+ for (const v of validated) {
697
+ const branch = v.worktree.branch ?? `detached-${v.worktree.head.slice(0, 8)}`;
698
+ const branchCol = branch.padEnd(maxBranchLen);
699
+ let statusCol;
700
+ if (v.status === "healthy") {
701
+ statusCol = green("healthy");
702
+ counts.healthy++;
703
+ } else if (v.status === "missing") {
704
+ statusCol = red("missing");
705
+ counts.missing++;
706
+ } else {
707
+ statusCol = yellow("stale");
708
+ counts.stale++;
709
+ }
710
+ console.log(` ${branchCol} ${statusCol.padEnd(7 + (statusCol.length - v.status.length))} ${v.worktree.path}`);
711
+ }
712
+ console.log();
713
+ const summaryParts = [];
714
+ if (counts.healthy > 0) summaryParts.push(`${counts.healthy} healthy`);
715
+ if (counts.missing > 0) summaryParts.push(`${counts.missing} missing`);
716
+ if (counts.stale > 0) summaryParts.push(`${counts.stale} stale`);
717
+ console.log(`Summary: ${summaryParts.join(", ")}`);
718
+ const staleWorktrees = validated.filter((v) => v.status === "stale");
719
+ if (staleWorktrees.length > 0) {
720
+ console.log();
721
+ console.log(`${green("==>")} Auto-repairing ${staleWorktrees.length} stale worktree(s)...`);
722
+ await repairHub(hubPath, (msg) => console.log(` ${msg}`));
723
+ }
724
+ const needsRepair = validated.filter((v) => v.status === "missing");
725
+ if (needsRepair.length === 0) {
726
+ return;
727
+ }
728
+ console.log();
729
+ console.log(`${yellow("warn:")} ${needsRepair.length} missing worktree(s) need repair.`);
730
+ const searchDirs = [dirname3(hubPath)];
731
+ console.log(`${green("==>")} Searching for missing worktrees...`);
732
+ const results = await findMissingWorktrees(
733
+ hubPath,
734
+ searchDirs,
735
+ (msg) => console.log(` ${msg}`)
736
+ );
737
+ const found = results.filter((r) => r.candidates.length > 0);
738
+ const notFound = results.filter((r) => r.candidates.length === 0);
739
+ const multiple = results.filter((r) => r.candidates.length > 1);
740
+ if (notFound.length > 0) {
741
+ console.log(`
742
+ ${yellow("Not found:")}`);
743
+ for (const r of notFound) {
744
+ console.log(` [${r.branch}]`);
745
+ }
746
+ }
747
+ if (found.length === 0) {
748
+ console.log(`
749
+ No worktrees could be located. Consider pruning them with 'git worktree prune'.`);
750
+ return;
751
+ }
752
+ const selections = /* @__PURE__ */ new Map();
753
+ for (const r of multiple) {
754
+ console.log(`
755
+ ${bold2(`[${r.branch}]`)} has multiple candidates:`);
756
+ for (let i = 0; i < r.candidates.length; i++) {
757
+ console.log(` ${i + 1}) ${r.candidates[i]}`);
758
+ }
759
+ process.stdout.write(`Select which to use (1-${r.candidates.length}) [skip]: `);
760
+ const sel = await prompt();
761
+ const idx = parseInt(sel) - 1;
762
+ if (idx >= 0 && idx < r.candidates.length) {
763
+ selections.set(r.branch, r.candidates[idx]);
764
+ }
765
+ }
766
+ console.log(`
767
+ ${green("Found:")}`);
768
+ const maxFoundBranchLen = found.reduce((m, r) => Math.max(m, r.branch.length), 0);
769
+ for (const r of found) {
770
+ const path = selections.get(r.branch) ?? r.candidates[0];
771
+ const branchCol = bold2(`[${r.branch}]`).padEnd(maxFoundBranchLen + 2 + BOLD.length + RESET.length);
772
+ console.log(` ${branchCol} ${path}`);
773
+ }
774
+ console.log();
775
+ process.stdout.write("Repair these worktrees? [y/N] ");
776
+ const repairAns = await prompt();
777
+ process.stdin.destroy();
778
+ if (!/^[Yy]$/.test(repairAns)) {
779
+ console.log("Aborted.");
780
+ return;
781
+ }
782
+ console.log();
783
+ for (const r of found) {
784
+ const path = selections.get(r.branch) ?? r.candidates[0];
785
+ await repairWorktree(path, hubPath, (msg) => console.log(`${green("==>")} ${msg}`));
786
+ }
787
+ console.log(`${green("==>")} Repaired ${found.length} worktree(s).`);
788
+ }
335
789
  function usage() {
336
790
  console.log(`Usage: git-worktree-organize <source> [destination]
337
791
 
338
792
  Convert a git repository into the canonical bare-hub worktree layout:
339
793
 
340
- <dest>/.bare/ bare git repo
341
- <dest>/.git plain file: "gitdir: ./.bare"
342
- <dest>/<branch>/ one directory per worktree
794
+ <dest>/.bare/ \u2190 bare git repo
795
+ <dest>/.git \u2190 plain file: "gitdir: ./.bare"
796
+ <dest>/<branch>/ \u2190 one directory per worktree
343
797
 
344
798
  Arguments:
345
799
  source Path to existing git repository
346
800
  destination Target hub directory (default: <parent>/<name>-bare)
347
801
 
348
802
  Options:
349
- -h, --help Show help`);
803
+ -h, --help Show help
804
+ -v, --version Show version`);
350
805
  }
351
806
  async function main() {
352
807
  const args = process.argv.slice(2);
808
+ if (args[0] === "-v" || args[0] === "--version") {
809
+ console.log(`git-worktree-organize ${getVersion(true)}`);
810
+ process.exit(0);
811
+ }
353
812
  if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
354
813
  usage();
355
814
  process.exit(0);
356
815
  }
816
+ console.log(`${bold2("git-worktree-organize")} ${getVersion(true)}
817
+ `);
357
818
  const sourcePath = args[0];
358
819
  const destArg = args[1];
359
820
  const source = resolve3(sourcePath);
360
- const dest = isPartialMigration(source) ? source : destArg ? resolve3(destArg) : join3(dirname2(source), basename2(source) + "-bare");
821
+ const dest = isPartialMigration(source) ? source : destArg ? resolve3(destArg) : join5(dirname3(source), basename3(source) + "-bare");
822
+ const config = await detect(source);
823
+ if (config.type === "bare-hub") {
824
+ await runValidationMode(source);
825
+ process.exit(0);
826
+ }
361
827
  if (!isPartialMigration(source) && !destArg) {
362
- const ancestorHub = findHub(dirname2(source));
828
+ const ancestorHub = findHub(dirname3(source));
363
829
  if (ancestorHub) {
364
830
  console.log(`
365
- ${yellow("warn:")} ${bold(source)} is inside an existing hub at ${bold(ancestorHub)}`);
831
+ ${yellow("warn:")} ${bold2(source)} is inside an existing hub at ${bold2(ancestorHub)}`);
366
832
  console.log(`
367
833
  This looks like manually-placed worktrees with stale .git files.`);
368
834
  console.log(`Running repair will fix all worktree .git connections in the hub.
@@ -386,24 +852,104 @@ This looks like manually-placed worktrees with stale .git files.`);
386
852
  if (isPartialMigration(dest)) {
387
853
  const hubWorktrees = await listWorktrees(dest);
388
854
  const pending = hubWorktrees.filter((wt) => {
389
- if (wt.isBare)
390
- return false;
855
+ if (wt.isBare) return false;
391
856
  const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
392
- return wt.path !== join3(dest, sanitizeBranch(branch));
857
+ return wt.path !== join5(dest, sanitizeBranch(branch));
858
+ });
859
+ const missing2 = hubWorktrees.filter((wt) => {
860
+ if (wt.isBare) return false;
861
+ return !existsSync5(wt.path);
393
862
  });
394
863
  console.log(`
395
- ${yellow("warn:")} Partial migration detected at ${bold(dest)}`);
864
+ ${yellow("warn:")} Partial migration detected at ${bold2(dest)}`);
865
+ if (missing2.length > 0) {
866
+ console.log(`
867
+ The following worktree paths no longer exist:`);
868
+ for (const wt of missing2) {
869
+ const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
870
+ console.log(` [${branch}] ${wt.path}`);
871
+ }
872
+ console.log();
873
+ const searchDirs = [dirname3(dest)];
874
+ console.log(`${green("==>")} Searching for missing worktrees...`);
875
+ const results = await findMissingWorktrees(
876
+ dest,
877
+ searchDirs,
878
+ (msg) => console.log(` ${msg}`)
879
+ );
880
+ const found = results.filter((r) => r.candidates.length > 0);
881
+ const notFound = results.filter((r) => r.candidates.length === 0);
882
+ const multiple = results.filter((r) => r.candidates.length > 1);
883
+ if (notFound.length > 0) {
884
+ console.log(`
885
+ ${yellow("Not found:")}`);
886
+ for (const r of notFound) {
887
+ console.log(` [${r.branch}]`);
888
+ }
889
+ }
890
+ if (found.length === 0) {
891
+ console.log(`
892
+ No worktrees could be located. Consider pruning them with 'git worktree prune'.`);
893
+ process.exit(0);
894
+ }
895
+ const selections = /* @__PURE__ */ new Map();
896
+ for (const r of multiple) {
897
+ console.log(`
898
+ ${bold2(`[${r.branch}]`)} has multiple candidates:`);
899
+ for (let i = 0; i < r.candidates.length; i++) {
900
+ console.log(` ${i + 1}) ${r.candidates[i]}`);
901
+ }
902
+ process.stdout.write(`Select which to use (1-${r.candidates.length}) [skip]: `);
903
+ const sel = await prompt();
904
+ const idx = parseInt(sel) - 1;
905
+ if (idx >= 0 && idx < r.candidates.length) {
906
+ selections.set(r.branch, r.candidates[idx]);
907
+ }
908
+ }
909
+ console.log(`
910
+ ${green("Found:")}`);
911
+ const maxBranchLen = found.reduce((m, r) => Math.max(m, r.branch.length), 0);
912
+ for (const r of found) {
913
+ const path = selections.get(r.branch) ?? r.candidates[0];
914
+ const branchCol = bold2(`[${r.branch}]`).padEnd(maxBranchLen + 2 + BOLD.length + RESET.length);
915
+ console.log(` ${branchCol} ${path}`);
916
+ }
917
+ console.log();
918
+ process.stdout.write("Repair these worktrees? [y/N] ");
919
+ const repairAns = await prompt();
920
+ if (!/^[Yy]$/.test(repairAns)) {
921
+ console.log("Aborted.");
922
+ process.stdin.destroy();
923
+ process.exit(0);
924
+ }
925
+ console.log();
926
+ for (const r of found) {
927
+ const path = selections.get(r.branch) ?? r.candidates[0];
928
+ await repairWorktree(path, dest, (msg) => console.log(`${green("==>")} ${msg}`));
929
+ }
930
+ console.log(`${green("==>")} Repaired ${found.length} worktree(s).
931
+ `);
932
+ const refreshed = await listWorktrees(dest);
933
+ hubWorktrees.length = 0;
934
+ hubWorktrees.push(...refreshed);
935
+ pending.length = 0;
936
+ pending.push(...hubWorktrees.filter((wt) => {
937
+ if (wt.isBare) return false;
938
+ const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
939
+ return wt.path !== join5(dest, sanitizeBranch(branch));
940
+ }));
941
+ }
396
942
  if (pending.length === 0) {
397
- console.log("All worktrees are already in place — nothing to resume.");
943
+ await runValidationMode(dest);
398
944
  process.exit(0);
399
945
  }
400
946
  console.log(`
401
947
  Worktrees still to move:`);
402
948
  for (const wt of pending) {
403
949
  const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
404
- const exists = existsSync3(wt.path);
950
+ const exists = existsSync5(wt.path);
405
951
  const status = exists ? "" : ` ${yellow("(path missing)")}`;
406
- console.log(` [${bold(branch)}] ${wt.path} ${join3(dest, sanitizeBranch(branch))}${status}`);
952
+ console.log(` [${bold2(branch)}] ${wt.path} \u2192 ${join5(dest, sanitizeBranch(branch))}${status}`);
407
953
  }
408
954
  console.log();
409
955
  process.stdout.write("Resume migration? [y/N] ");
@@ -413,7 +959,11 @@ Worktrees still to move:`);
413
959
  process.exit(0);
414
960
  }
415
961
  console.log();
416
- const hubPath2 = await resumeMigrate(dest, (msg) => console.log(`${green("==>")} ${msg}`));
962
+ const hubPath2 = await resumeMigrate(
963
+ dest,
964
+ (msg) => console.log(`${green("==>")} ${msg}`),
965
+ (msg) => console.log(`${yellow("warn:")} ${msg}`)
966
+ );
417
967
  console.log();
418
968
  console.log(`${green("==>")} Verifying with git worktree list...`);
419
969
  console.log(run("git", ["-C", hubPath2, "worktree", "list"]).stdout);
@@ -423,30 +973,132 @@ Worktrees still to move:`);
423
973
  console.log(`
424
974
  ${green("==>")} Reading worktrees from ${source}
425
975
  `);
426
- const config = await detect(source);
427
976
  const allWorktrees = await listWorktrees(source);
977
+ if (config.type === "standard" && !destArg) {
978
+ const repoName = basename3(source);
979
+ let mainBranch2 = "main";
980
+ if (allWorktrees.length > 0) {
981
+ mainBranch2 = allWorktrees[0].branch ?? "main";
982
+ }
983
+ console.log("Worktrees to migrate:");
984
+ const maxNameLen2 = allWorktrees.reduce((m, wt) => {
985
+ const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
986
+ return Math.max(m, branch.length);
987
+ }, 0);
988
+ for (const wt of allWorktrees) {
989
+ const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
990
+ const safe = sanitizeBranch(branch);
991
+ const isMain = branch === mainBranch2;
992
+ const destDir = join5(source, safe);
993
+ const nameCol = bold2(`[${branch}]`).padEnd(maxNameLen2 + 2 + BOLD.length + RESET.length);
994
+ const annotation = isMain ? ` (labeled ${yellow("[main]")})`.padEnd(18 + YELLOW.length + RESET.length) : "".padEnd(18);
995
+ console.log(` ${nameCol}${annotation} \u2192 ${destDir}`);
996
+ }
997
+ console.log();
998
+ console.log(`Hub destination: ${bold2(source)} (bare repo at ${source}/.bare)`);
999
+ console.log();
1000
+ console.log(`No destination specified. Migrate in-place?`);
1001
+ console.log(`This will rename '${bold2(repoName)}' to '${bold2(repoName + ".old")}' and create the hub here.`);
1002
+ console.log();
1003
+ process.stdout.write("Proceed with in-place migration? [y/N] ");
1004
+ const inPlaceAns = await prompt();
1005
+ process.stdin.destroy();
1006
+ if (!/^[Yy]$/.test(inPlaceAns)) {
1007
+ console.log("Aborted.");
1008
+ console.log("Tip: Specify a destination directory to migrate to a new location.");
1009
+ process.exit(0);
1010
+ }
1011
+ console.log();
1012
+ const hubPath2 = await migrateInPlace(
1013
+ source,
1014
+ (msg) => console.log(`${green("==>")} ${msg}`),
1015
+ (msg) => console.log(`${yellow("warn:")} ${msg}`)
1016
+ );
1017
+ console.log(`${green("==>")} Verifying with git worktree list...`);
1018
+ const verifyOutput2 = run("git", ["-C", hubPath2, "worktree", "list"]).stdout;
1019
+ console.log(verifyOutput2);
1020
+ console.log(`Done! Hub: ${hubPath2}`);
1021
+ console.log(`Backup: ${source}.old`);
1022
+ console.log();
1023
+ console.log("Useful commands:");
1024
+ console.log(` git -C ${hubPath2} worktree list`);
1025
+ console.log(` git -C ${hubPath2}/main log --oneline -5`);
1026
+ process.exit(0);
1027
+ }
428
1028
  const missing = allWorktrees.filter((wt) => {
429
- if (wt.isBare)
430
- return false;
431
- const actual = resolveWorktreePath(wt.path, dest, dirname2(source));
432
- return !existsSync3(actual);
1029
+ if (wt.isBare) return false;
1030
+ const actual = resolveWorktreePath(wt.path, dest, dirname3(source));
1031
+ return !existsSync5(actual);
433
1032
  });
434
1033
  if (missing.length > 0) {
435
- console.log(`${yellow("warn:")} The following worktree paths no longer exist:`);
1034
+ console.log(`
1035
+ ${yellow("warn:")} The following worktree paths no longer exist:`);
436
1036
  for (const wt of missing) {
437
1037
  const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
438
1038
  console.log(` [${branch}] ${wt.path}`);
439
1039
  }
440
1040
  console.log();
441
- process.stdout.write("Remove them with `git worktree prune` and continue? [y/N] ");
442
- const pruneAns = await prompt();
443
- if (!/^[Yy]$/.test(pruneAns)) {
1041
+ const searchDirs = [dirname3(source)];
1042
+ if (dest !== source) {
1043
+ searchDirs.push(dest);
1044
+ }
1045
+ console.log(`${green("==>")} Searching for missing worktrees...`);
1046
+ const results = await findMissingWorktrees(
1047
+ source,
1048
+ searchDirs,
1049
+ (msg) => console.log(` ${msg}`)
1050
+ );
1051
+ const found = results.filter((r) => r.candidates.length > 0);
1052
+ const notFound = results.filter((r) => r.candidates.length === 0);
1053
+ const multiple = results.filter((r) => r.candidates.length > 1);
1054
+ if (notFound.length > 0) {
1055
+ console.log(`
1056
+ ${yellow("Not found:")}`);
1057
+ for (const r of notFound) {
1058
+ console.log(` [${r.branch}]`);
1059
+ }
1060
+ }
1061
+ if (found.length === 0) {
1062
+ console.log(`
1063
+ No worktrees could be located. Consider pruning them with 'git worktree prune'.`);
1064
+ process.exit(0);
1065
+ }
1066
+ const selections = /* @__PURE__ */ new Map();
1067
+ for (const r of multiple) {
1068
+ console.log(`
1069
+ ${bold2(`[${r.branch}]`)} has multiple candidates:`);
1070
+ for (let i = 0; i < r.candidates.length; i++) {
1071
+ console.log(` ${i + 1}) ${r.candidates[i]}`);
1072
+ }
1073
+ process.stdout.write(`Select which to use (1-${r.candidates.length}) [skip]: `);
1074
+ const sel = await prompt();
1075
+ const idx = parseInt(sel) - 1;
1076
+ if (idx >= 0 && idx < r.candidates.length) {
1077
+ selections.set(r.branch, r.candidates[idx]);
1078
+ }
1079
+ }
1080
+ console.log(`
1081
+ ${green("Found:")}`);
1082
+ const maxBranchLen = found.reduce((m, r) => Math.max(m, r.branch.length), 0);
1083
+ for (const r of found) {
1084
+ const path = selections.get(r.branch) ?? r.candidates[0];
1085
+ const branchCol = bold2(`[${r.branch}]`).padEnd(maxBranchLen + 2 + BOLD.length + RESET.length);
1086
+ console.log(` ${branchCol} ${path}`);
1087
+ }
1088
+ console.log();
1089
+ process.stdout.write("Repair these worktrees? [y/N] ");
1090
+ const repairAns = await prompt();
1091
+ if (!/^[Yy]$/.test(repairAns)) {
444
1092
  console.log("Aborted.");
445
1093
  process.stdin.destroy();
446
1094
  process.exit(0);
447
1095
  }
448
- run("git", ["-C", source, "worktree", "prune"]);
449
- console.log(`${green("==>")} Pruned stale worktrees.
1096
+ console.log();
1097
+ for (const r of found) {
1098
+ const path = selections.get(r.branch) ?? r.candidates[0];
1099
+ await repairWorktree(path, source, (msg) => console.log(`${green("==>")} ${msg}`));
1100
+ }
1101
+ console.log(`${green("==>")} Repaired ${found.length} worktree(s).
450
1102
  `);
451
1103
  const refreshed = await listWorktrees(source);
452
1104
  allWorktrees.length = 0;
@@ -462,19 +1114,19 @@ ${green("==>")} Reading worktrees from ${source}
462
1114
  const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
463
1115
  const safe = sanitizeBranch(branch);
464
1116
  const isMain = branch === mainBranch;
465
- const destDir = join3(dest, safe);
1117
+ const destDir = join5(dest, safe);
466
1118
  return { branch, isMain, destDir };
467
1119
  });
468
1120
  const maxNameLen = entries.reduce((m, e) => Math.max(m, e.branch.length), 0);
469
1121
  for (const { branch, isMain, destDir } of entries) {
470
1122
  const tag = isMain ? yellow("[main]") : "";
471
1123
  const tagPad = isMain ? ` (labeled ${yellow("[main]")})` : "";
472
- const nameCol = bold(`[${branch}]`).padEnd(maxNameLen + 2 + BOLD.length + RESET.length);
1124
+ const nameCol = bold2(`[${branch}]`).padEnd(maxNameLen + 2 + BOLD.length + RESET.length);
473
1125
  const annotation = isMain ? ` (labeled ${yellow("[main]")})`.padEnd(18 + YELLOW.length + RESET.length) : "".padEnd(18);
474
- console.log(` ${nameCol}${annotation} ${destDir}`);
1126
+ console.log(` ${nameCol}${annotation} \u2192 ${destDir}`);
475
1127
  }
476
1128
  console.log();
477
- console.log(`Hub destination: ${bold(dest)} (bare repo at ${dest}/.bare)`);
1129
+ console.log(`Hub destination: ${bold2(dest)} (bare repo at ${dest}/.bare)`);
478
1130
  console.log();
479
1131
  process.stdout.write("Proceed? [y/N] ");
480
1132
  const ans = await prompt();
@@ -484,7 +1136,12 @@ ${green("==>")} Reading worktrees from ${source}
484
1136
  process.exit(0);
485
1137
  }
486
1138
  console.log();
487
- const hubPath = await migrate(config, { source: sourcePath, dest: destArg ?? "" });
1139
+ const hubPath = await migrate(
1140
+ config,
1141
+ { source: sourcePath, dest: destArg ?? "" },
1142
+ (msg) => console.log(`${green("==>")} ${msg}`),
1143
+ (msg) => console.log(`${yellow("warn:")} ${msg}`)
1144
+ );
488
1145
  console.log(`${green("==>")} Verifying with git worktree list...`);
489
1146
  const verifyOutput = run("git", ["-C", hubPath, "worktree", "list"]).stdout;
490
1147
  console.log(verifyOutput);