git-worktree-organize 1.1.0 → 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 +200 -78
  2. package/dist/cli.js +188 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,24 +1,24 @@
1
1
  # git-worktree-organize
2
2
 
3
- > ⚠️ **Use at your own risk.** This tool works for me and passes all tested scenarios, but it modifies your git repository structure. **Make a full backup first** before running on any repository you care about.
3
+ Convert any git repository into a bare-hub worktree layout where every branch lives in its own directory. No more stashing, no more switching.
4
4
 
5
- Convert any git repository into the canonical bare-hub worktree layout, so every branch lives in its own directory and you never need to stash or switch again.
5
+ > **Use at your own risk.** This tool modifies your git repository structure. Always ensure you have a copy of important repositories before running it.
6
6
 
7
- ## What it does
7
+ > **Opinionated layout.** This tool enforces a specific structure where every branch is a sibling directory under a single hub root. If you prefer a different worktree arrangement, this tool may not be for you. See [Why this layout?](#why-this-layout) for the rationale.
8
8
 
9
- Takes an existing git repo (any type) and migrates it into this structure:
9
+ ## The target layout
10
10
 
11
11
  ```
12
- <dest>/
12
+ myrepo/
13
13
  ├── .bare/ ← bare git repo (the actual git database)
14
14
  ├── .git ← plain file: "gitdir: ./.bare"
15
15
  ├── main/ ← worktree for the main branch
16
16
  └── feature-x/ ← worktree for each other branch
17
17
  ```
18
18
 
19
- Each branch directory is a fully functional working tree. Open them in separate terminals or IDE windows simultaneously — no stashing, no switching.
19
+ Each branch directory is a fully functional working tree. Open them in separate terminals or IDE windows simultaneously.
20
20
 
21
- The `.git` file at the hub root makes tools that walk up looking for `.git` (IDEs, linters, etc.) find the repo correctly.
21
+ The `.git` file at the hub root means `git worktree list` (and any git command) works from **anywhere** -- the hub root, any worktree directory, or even nested subdirectories. IDEs, linters, and other tools that walk up looking for `.git` also find the repo correctly.
22
22
 
23
23
  ## Installation
24
24
 
@@ -39,145 +39,267 @@ git-worktree-organize <source> [destination]
39
39
  git-worktree-organize <source> [destination]
40
40
  ```
41
41
 
42
- | Argument | Description |
43
- |---------------|------------------------------------------------------------------|
44
- | `source` | Path to the existing git repository to migrate |
45
- | `destination` | Target hub directory (omit for in-place migration prompt) |
46
-
47
- **Without a destination**, the tool prompts for in-place migration:
48
- - Renames `<source>` to `<source>.old`
49
- - Creates the hub at the original `<source>` path
50
-
51
- **With a destination**, the tool migrates to the specified path.
42
+ | Argument | Description |
43
+ |---------------|------------------------------------------------------------|
44
+ | `source` | Path to the existing git repository to migrate |
45
+ | `destination` | Target hub directory (omit for in-place migration) |
52
46
 
53
47
  The tool shows a preview of what will be created and asks for confirmation before making any changes.
54
48
 
55
- ## Examples
49
+ ### In-place migration
56
50
 
57
- ### In-place migration (recommended)
51
+ When no destination is provided, the tool prompts to migrate in place:
52
+
53
+ 1. Renames `<source>` to `<source>.old`
54
+ 2. Creates the hub at the original `<source>` path
58
55
 
59
56
  ```sh
60
57
  git-worktree-organize /projects/myrepo
61
58
  ```
62
59
 
63
- Prompts to reorganize in place, resulting in:
60
+ Result:
64
61
 
65
62
  ```
66
- /projects/myrepo/ ← hub (was renamed from myrepo.old)
63
+ /projects/myrepo/ new hub
67
64
  ├── .bare/
68
65
  ├── .git
69
66
  ├── main/
70
67
  └── feature-x/
71
68
 
72
- /projects/myrepo.old/ backup of original
69
+ /projects/myrepo.old/ original repo (renamed)
73
70
  ```
74
71
 
75
- ### Migrate to new location
72
+ The `.old` directory is kept so you can verify the migration before deleting it manually.
73
+
74
+ ### Migration to a new location
75
+
76
+ When a destination is provided, the source repo is moved into the new hub as the main branch worktree:
76
77
 
77
78
  ```sh
78
- git-worktree-organize /projects/myrepo /projects/myrepo-organized
79
+ git-worktree-organize /projects/myrepo /projects/myrepo-hub
79
80
  ```
80
81
 
81
82
  Result:
82
83
 
83
84
  ```
84
- /projects/myrepo-organized/
85
+ /projects/myrepo-hub/
85
86
  ├── .bare/
86
87
  ├── .git
87
- ├── main/ ← original /projects/myrepo moved here
88
+ ├── main/ ← original /projects/myrepo moved here
88
89
  ├── feature-x/
89
90
  └── hotfix/
90
91
  ```
91
92
 
92
- The original `/projects/myrepo` becomes the `main/` worktree. No data is lost.
93
+ The original source directory becomes the `main/` worktree. No data is lost.
94
+
95
+ ## Supported repository types
96
+
97
+ The tool detects and migrates five different git repository layouts:
93
98
 
94
- ## Features
99
+ | Type | Description |
100
+ |------|-------------|
101
+ | **Standard** | Ordinary repo with a `.git` directory |
102
+ | **Bare root** | Bare repo with git internals at the root (`HEAD`, `refs/`, `objects/`) |
103
+ | **Bare dotgit** | Repo where `.git` is a bare directory (`core.bare = true`) |
104
+ | **Bare external** | Repo where `.git` is a file pointing to a gitdir elsewhere |
105
+ | **Bare hub** | Already in the bare-hub layout (re-organizes worktrees into canonical structure) |
95
106
 
96
- ### Repository Migration
107
+ See [Layout conversions](#layout-conversions) for detailed before/after diagrams of each type.
97
108
 
98
- Convert any git repository type to the bare-hub layout:
109
+ ## Recovery
99
110
 
100
- - **Standard repos** ordinary repos with a `.git` directory
101
- - **Bare-root** — bare repo with git internals at the root (`HEAD`, `refs/`, `objects/`)
102
- - **Bare-dotgit** — repo where `.git` is a bare git directory (`core.bare = true`)
103
- - **Bare-external** — repo where `.git` is a file pointing to a gitdir elsewhere
104
- - **Bare-hub** — already in the bare-hub layout (re-organizes worktrees into the canonical structure)
111
+ Running the tool on an existing hub detects problems and offers to fix them:
105
112
 
106
- ### Resume & Recovery
113
+ - **Partial migrations** -- Resumes moving worktrees that weren't fully processed (e.g. after an interruption).
114
+ - **Stale `.git` pointers** -- Repairs worktrees with broken connections to the bare repo.
115
+ - **Missing worktrees** -- Searches for worktrees that were moved outside the hub (up to 3 directory levels deep).
116
+ - **Parent directory renames** -- Detects and repairs when a hub's parent directory was renamed.
107
117
 
108
- If a migration was interrupted or worktrees have moved, running the tool on the hub directory will:
118
+ ```sh
119
+ # Resume a partial migration or repair an existing hub
120
+ git-worktree-organize /path/to/hub
109
121
 
110
- 1. **Resume partial migrations** Continue moving worktrees that weren't fully processed
111
- 2. **Repair stale `.git` pointers** — Fix worktrees with broken connections to the bare repo
112
- 3. **Search for missing worktrees** — Find worktrees that were moved outside the hub (searches up to 3 directory levels deep)
113
- 4. **Fix parent directory renames** — Automatically detect and repair when a hub's parent directory was renamed
122
+ # Repair after renaming a parent directory (can point to any worktree inside the hub)
123
+ git-worktree-organize /new/path/to/hub/some-worktree
124
+ ```
114
125
 
115
- ### Safety Features
126
+ ## Safety
116
127
 
117
- - **Interactive confirmation** Preview all changes before execution
118
- - **Branch name sanitization** Names with slashes (e.g. `feature/auth`) become hyphenated directories (`feature-auth`)
119
- - **Collision detection** Warns if sanitized names would conflict
120
- - **Zero runtime dependencies** Only requires Node.js and git
128
+ - **Interactive confirmation** -- Preview all changes before execution
129
+ - **Branch name sanitization** -- Slashes become hyphens (e.g. `feature/auth` becomes `feature-auth`)
130
+ - **Collision detection** -- Warns if sanitized names would conflict
131
+ - **AI agent documentation** -- Creates an `AGENTS.md` at the hub root for AI coding agents (never overwrites existing files)
132
+ - **Zero runtime dependencies** -- Only requires Node.js and git
121
133
 
122
- ## Recovery Scenarios
134
+ ## Why this layout?
123
135
 
124
- ### Partial Migration
136
+ Every branch as a sibling directory means you can work on multiple branches simultaneously. Run branch-specific builds side by side. No stashing, no context switching.
125
137
 
126
- If migration was interrupted:
138
+ The `.git` file at the hub root is the key detail. Because it points to `.bare/`, git resolves the repository from any location in the tree:
127
139
 
128
140
  ```sh
129
- git-worktree-organize /path/to/hub
141
+ # All of these work -- no -C flag or cd needed:
142
+ ~/projects/myrepo $ git worktree list
143
+ ~/projects/myrepo/main $ git worktree list
144
+ ~/projects/myrepo/feature-x $ git worktree list
145
+ ~/projects/myrepo/main/src/deep/nested $ git worktree list
130
146
  ```
131
147
 
132
- The tool detects the partial state, shows which worktrees still need to be moved, and offers to resume.
148
+ IDEs, linters, pre-commit hooks, and AI coding agents all discover the repo automatically regardless of which worktree directory they're opened in.
149
+
150
+ ## Requirements
133
151
 
134
- ### Moved Worktrees
152
+ - Node.js 18+
153
+ - Git 2.5+ (for worktree support)
135
154
 
136
- If worktrees were manually moved outside the hub:
155
+ ## Development
137
156
 
138
157
  ```sh
139
- git-worktree-organize /path/to/hub
158
+ git clone https://github.com/drmikecrowe/git-worktree-organize.git
159
+ cd git-worktree-organize
160
+ npm install
161
+
162
+ npm test # Run tests
163
+ npm run build # Build
164
+ node dist/cli.js /path/to/test/repo # Test locally
140
165
  ```
141
166
 
142
- The tool searches for missing worktrees by branch name and offers to repair their `.git` pointers.
167
+ ## Layout conversions
143
168
 
144
- ### Parent Directory Rename
169
+ Detailed before/after diagrams for each supported repository type.
145
170
 
146
- If you renamed a parent directory, worktree `.git` files will have stale paths. Run the tool on any worktree path inside the hub:
171
+ ### Standard repository
147
172
 
148
- ```sh
149
- git-worktree-organize /new/path/to/hub/some-worktree
173
+ The most common case -- a normal repo with a `.git` directory and optionally some linked worktrees.
174
+
175
+ **Before:**
176
+ ```
177
+ myrepo/
178
+ ├── .git/ ← standard git directory
179
+ ├── src/
180
+ └── package.json
181
+
182
+ myrepo-feature/ ← linked worktree (created by git worktree add)
183
+ ├── .git ← file pointing back to myrepo/.git/worktrees/feature
184
+ ├── src/
185
+ └── package.json
150
186
  ```
151
187
 
152
- The tool detects the hub, navigates to it, and repairs all worktree connections.
188
+ **After:**
189
+ ```
190
+ myrepo/
191
+ ├── .bare/ ← bare git repo (contents of old .git/)
192
+ ├── .git ← file: "gitdir: ./.bare"
193
+ ├── AGENTS.md
194
+ ├── main/ ← main branch worktree (was myrepo/)
195
+ │ ├── src/
196
+ │ └── package.json
197
+ └── feature/ ← feature worktree (was myrepo-feature/)
198
+ ├── .git ← file pointing to .bare/worktrees/feature
199
+ ├── src/
200
+ └── package.json
201
+ ```
153
202
 
154
- ## Why this layout
203
+ ### Bare root
155
204
 
156
- Having every branch as a sibling directory means you can work on multiple branches simultaneously without stashing or switching. It is also easier to run branch-specific build artifacts side by side, and the `.git` file at the hub root ensures IDE and tooling compatibility without any special configuration.
205
+ A bare repository where git internals sit directly at the root. Often created by `git clone --bare` or `git init --bare`.
157
206
 
158
- ## Requirements
207
+ **Before:**
208
+ ```
209
+ myrepo.git/
210
+ ├── HEAD
211
+ ├── config
212
+ ├── objects/
213
+ └── refs/
214
+ ```
159
215
 
160
- - Node.js 18+
161
- - Git 2.5+ (for worktree support)
216
+ **After:**
217
+ ```
218
+ myrepo-hub/
219
+ ├── .bare/ ← git internals moved here
220
+ │ ├── HEAD
221
+ │ ├── config
222
+ │ ├── objects/
223
+ │ └── refs/
224
+ ├── .git ← file: "gitdir: ./.bare"
225
+ └── AGENTS.md
226
+ ```
162
227
 
163
- ## Development
228
+ No worktrees are created since a bare repo has no checked-out branches. Use `git worktree add <branch>` from the hub to start working.
164
229
 
165
- ```sh
166
- # Clone and install
167
- git clone https://github.com/drmikecrowe/git-worktree-organize.git
168
- cd git-worktree-organize
169
- npm install
230
+ ### Bare dotgit
231
+
232
+ A repository where `.git` is a directory but `core.bare = true`.
233
+
234
+ **Before:**
235
+ ```
236
+ myrepo/
237
+ └── .git/ ← directory, but core.bare = true
238
+ ├── HEAD
239
+ ├── config ← contains core.bare = true
240
+ ├── objects/
241
+ └── refs/
242
+ ```
243
+
244
+ **After:**
245
+ ```
246
+ myrepo-hub/
247
+ ├── .bare/ ← contents of old .git/
248
+ ├── .git ← file: "gitdir: ./.bare"
249
+ └── AGENTS.md
250
+ ```
170
251
 
171
- # Run tests
172
- npm test
252
+ ### Bare external
173
253
 
174
- # Build
175
- npm run build
254
+ A repository where `.git` is a file pointing to a gitdir stored elsewhere on the filesystem.
176
255
 
177
- # Test locally
178
- node dist/cli.js /path/to/test/repo
256
+ **Before:**
257
+ ```
258
+ myrepo/
259
+ ├── .git ← file: "gitdir: /somewhere/else/myrepo.git"
260
+ ├── src/
261
+ └── package.json
262
+
263
+ /somewhere/else/myrepo.git/
264
+ ├── HEAD
265
+ ├── objects/
266
+ └── refs/
267
+ ```
268
+
269
+ **After:**
270
+ ```
271
+ myrepo-hub/
272
+ ├── .bare/ ← copy of /somewhere/else/myrepo.git/
273
+ ├── .git ← file: "gitdir: ./.bare"
274
+ └── AGENTS.md
275
+ ```
276
+
277
+ ### Bare hub (re-organize)
278
+
279
+ Already in the bare-hub layout but with worktrees scattered in non-standard locations.
280
+
281
+ **Before:**
282
+ ```
283
+ myrepo/
284
+ ├── .bare/
285
+ ├── .git ← file: "gitdir: ./.bare"
286
+ └── main/
287
+
288
+ /other/path/feature/ ← worktree outside the hub
289
+ └── .git
290
+ ```
291
+
292
+ **After:**
293
+ ```
294
+ myrepo/
295
+ ├── .bare/
296
+ ├── .git ← file: "gitdir: ./.bare"
297
+ ├── AGENTS.md
298
+ ├── main/
299
+ └── feature/ ← moved into the hub
300
+ └── .git ← repaired to point to .bare/worktrees/feature
179
301
  ```
180
302
 
181
303
  ## License
182
304
 
183
- MIT see [github.com/drmikecrowe/git-worktree-organize](https://github.com/drmikecrowe/git-worktree-organize).
305
+ MIT -- see [github.com/drmikecrowe/git-worktree-organize](https://github.com/drmikecrowe/git-worktree-organize).
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
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 } from "node:fs";
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";
@@ -144,6 +144,103 @@ function samefs(a, b) {
144
144
  }
145
145
 
146
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
+ }
147
244
  function sanitizeBranch(branch) {
148
245
  return branch.replace(/\//g, "-");
149
246
  }
@@ -180,6 +277,13 @@ async function repairHub(dest, log = console.log) {
180
277
  const worktreePath = dirname2(registeredGitFile);
181
278
  if (!worktreePath.startsWith(dest + "/")) continue;
182
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);
183
287
  const content = readFileSync2(registeredGitFile, "utf8");
184
288
  const match = content.match(/^gitdir:\s*(.+)/m);
185
289
  if (!match) continue;
@@ -266,6 +370,7 @@ async function migrateInPlace(source, log = console.log, warn) {
266
370
  mkdirSync(mainAdminDir, { recursive: true });
267
371
  writeFileSync(join2(mainAdminDir, "gitdir"), mainDest + "/.git\n");
268
372
  writeFileSync(join2(mainAdminDir, "commondir"), "../../\n");
373
+ ensureWorktreeConfig(mainAdminDir, destBare);
269
374
  const headToWrite = mainHeadContent.endsWith("\n") ? mainHeadContent : mainHeadContent + "\n";
270
375
  writeFileSync(join2(mainAdminDir, "HEAD"), headToWrite);
271
376
  const bareIndex = join2(destBare, "index");
@@ -279,6 +384,7 @@ async function migrateInPlace(source, log = console.log, warn) {
279
384
  await processLinkedWorktree(worktrees[i], resolvedSource, destBare, log, warn);
280
385
  }
281
386
  log(`Original repo backed up at: ${oldPath}`);
387
+ writeAgentsMd(resolvedSource);
282
388
  return resolvedSource;
283
389
  }
284
390
  function bold(s) {
@@ -322,6 +428,7 @@ async function migrate(config, options, log, warn) {
322
428
  mkdirSync(mainAdminDir, { recursive: true });
323
429
  writeFileSync(join2(mainAdminDir, "gitdir"), mainDest + "/.git\n");
324
430
  writeFileSync(join2(mainAdminDir, "commondir"), "../../\n");
431
+ ensureWorktreeConfig(mainAdminDir, destBare);
325
432
  const headToWrite = mainHeadContent.endsWith("\n") ? mainHeadContent : mainHeadContent + "\n";
326
433
  writeFileSync(join2(mainAdminDir, "HEAD"), headToWrite);
327
434
  const bareIndex = join2(destBare, "index");
@@ -338,6 +445,7 @@ async function migrate(config, options, log, warn) {
338
445
  await processLinkedWorktree(wt, dest, destBare, log, warn);
339
446
  }
340
447
  }
448
+ writeAgentsMd(dest);
341
449
  return dest;
342
450
  }
343
451
  async function processLinkedWorktree(wt, dest, destBare, log, warn) {
@@ -359,6 +467,8 @@ async function processLinkedWorktree(wt, dest, destBare, log, warn) {
359
467
  `);
360
468
  if (existsSync3(newAdmin)) {
361
469
  writeFileSync(join2(newAdmin, "gitdir"), wtDest + "/.git\n");
470
+ writeFileSync(join2(newAdmin, "commondir"), "../../\n");
471
+ ensureWorktreeConfig(newAdmin, destBare);
362
472
  } else {
363
473
  warn?.(`Admin dir ${newAdmin} does not exist for worktree ${wtDest}`);
364
474
  }
@@ -481,12 +591,78 @@ function isGitPointerValid(worktreePath, hubPath) {
481
591
  if (!match) {
482
592
  return false;
483
593
  }
484
- const gitdir = match[1].trim();
594
+ const adminDir = match[1].trim();
485
595
  const bareDir = join5(hubPath, ".bare");
486
- return gitdir.includes(bareDir) && gitdir.includes("/worktrees/");
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;
487
657
  }
488
658
  async function runValidationMode(hubPath) {
489
- const worktrees = await listWorktrees(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
+ }
490
666
  const validated = [];
491
667
  for (const wt of worktrees) {
492
668
  if (wt.isBare) continue;
@@ -539,12 +715,18 @@ async function runValidationMode(hubPath) {
539
715
  if (counts.missing > 0) summaryParts.push(`${counts.missing} missing`);
540
716
  if (counts.stale > 0) summaryParts.push(`${counts.stale} stale`);
541
717
  console.log(`Summary: ${summaryParts.join(", ")}`);
542
- const needsRepair = validated.filter((v) => v.status === "missing" || v.status === "stale");
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");
543
725
  if (needsRepair.length === 0) {
544
726
  return;
545
727
  }
546
728
  console.log();
547
- console.log(`${yellow("warn:")} ${needsRepair.length} worktree(s) need repair.`);
729
+ console.log(`${yellow("warn:")} ${needsRepair.length} missing worktree(s) need repair.`);
548
730
  const searchDirs = [dirname3(hubPath)];
549
731
  console.log(`${green("==>")} Searching for missing worktrees...`);
550
732
  const results = await findMissingWorktrees(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-worktree-organize",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Convert any git repo into the canonical bare-hub worktree layout",
5
5
  "type": "module",
6
6
  "bin": {