@thanaen/worktree-cleanup 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,60 +1,100 @@
1
1
  # worktree-cleanup
2
2
 
3
- Safely remove stale Git worktrees created by coding agents and other tooling.
4
- The command is conservative: a worktree must be registered, clean, unlocked,
5
- and already merged into a trusted base branch before it can be offered for
6
- deletion.
3
+ Clean up stale Git worktrees left behind by coding agents and other tooling,
4
+ without putting uncommitted work at risk.
7
5
 
8
- > The npm package will be published as `@thanaen/worktree-cleanup`. npm package
9
- > scopes are lowercase even though the GitHub account is displayed as Thanaen.
6
+ `worktree-cleanup` first shows exactly what it found and why each directory is
7
+ removable or skipped. Nothing is deleted until you confirm the plan.
10
8
 
11
9
  ## Install
12
10
 
11
+ Requires Git and Node.js 22 or newer.
12
+
13
13
  ```bash
14
- pnpm add -g @thanaen/worktree-cleanup
14
+ npm install --global @thanaen/worktree-cleanup
15
15
  ```
16
16
 
17
- ## Usage
17
+ Also available with pnpm or Bun:
18
+
19
+ ```bash
20
+ pnpm add --global @thanaen/worktree-cleanup
21
+ bun add --global @thanaen/worktree-cleanup
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ Run the command from your project directory:
18
27
 
19
28
  ```bash
20
- # Inspect smart defaults below the current directory
21
29
  worktree-cleanup
30
+ ```
31
+
32
+ The shorter `worktree-clean` command does the same thing.
33
+
34
+ By default, the CLI looks for these directories in the current directory:
22
35
 
23
- # Equivalent shorter executable
24
- worktree-clean
36
+ - `worktrees`
37
+ - `.claude/worktrees`
38
+ - `.codex/worktrees`
25
39
 
26
- # Inspect exactly one root and ignore smart defaults
40
+ Only immediate child directories are inspected. If you keep worktrees
41
+ somewhere else, select that directory explicitly:
42
+
43
+ ```bash
27
44
  worktree-cleanup --dir ../agent-worktrees
45
+ ```
28
46
 
29
- # Approve the displayed plan without prompting
30
- worktree-cleanup --yes
47
+ Using `--dir` disables the smart defaults. To approve the displayed plan
48
+ without an interactive prompt, pass `-y` or `--yes`:
49
+
50
+ ```bash
51
+ worktree-cleanup --dir ../agent-worktrees --yes
31
52
  ```
32
53
 
33
- Smart defaults are `worktrees`, `.claude/worktrees`, and `.codex/worktrees`.
34
- Only their immediate child directories are inspected.
54
+ In a non-interactive environment, `--yes` is required.
55
+
56
+ ## What is safe to remove?
57
+
58
+ A worktree is offered for removal only when every check passes:
59
+
60
+ - Git recognizes it as a registered worktree.
61
+ - It is neither the repository's main worktree nor the worktree running the
62
+ command.
63
+ - It is unlocked.
64
+ - It has no tracked changes or untracked files.
65
+ - Its `HEAD` is already an ancestor of the detected base branch, or its
66
+ attached branch can be merged without changing the base branch's content.
67
+
68
+ The base branch is detected from `origin/HEAD`, then local `main`, local
69
+ `master`, and finally the main worktree's current branch. If the CLI cannot
70
+ prove that a directory is safe, it skips it and explains why.
35
71
 
36
- The command never force-removes a worktree and never deletes its branch. It
37
- asks once before deletion unless `-y` or `--yes` is passed. If stdin is not an
38
- interactive terminal, `--yes` is required.
72
+ Candidates are checked again immediately before removal. The CLI uses
73
+ `git worktree remove` without `--force` and never deletes branches.
39
74
 
40
- ## What counts as stale?
75
+ The content-equivalence check recognizes branches integrated through rebase or
76
+ squash merge. It uses `git merge-tree`, does not modify either worktree, and is
77
+ only available with Git 2.38 or newer. Detached worktrees and repositories with
78
+ custom merge drivers keep the stricter ancestry-only rule. Older Git versions
79
+ remain supported but also keep that conservative behavior.
41
80
 
42
- A worktree is removable only when all of these are true:
81
+ ### A note about remote branches
43
82
 
44
- - Git lists it as a registered worktree.
45
- - It is not the main or currently executing worktree.
46
- - It is unlocked and clean, including untracked files.
47
- - Its HEAD is an ancestor of the detected base ref.
83
+ The CLI does not contact GitHub, and a deleted remote branch alone does not
84
+ prove that its work was merged. Run `git fetch --prune` first if you want the
85
+ decision to use the latest remote refs. Squash-merged or rebased branches may
86
+ still be skipped when Git cannot prove that merging them would leave the base
87
+ unchanged. This conservative behavior avoids discarding work based on a guess.
48
88
 
49
- Base detection prefers `origin/HEAD`, then local `main`, local `master`, and
50
- finally the main worktree's current branch. Uncertain state is always skipped.
51
- Every candidate is revalidated immediately before removal.
89
+ If a repository has been moved, Git may still hold worktree paths from its old
90
+ location. In that case, repair the metadata with `git worktree repair` before
91
+ running the cleanup again.
52
92
 
53
93
  ## Development
54
94
 
55
- This repository follows GitHub Spec Kit. The feature contract is in
56
- `specs/001-core-cleanup/`. Effect's official source is vendored under `repos/`
57
- as a Git subtree and used as read-only reference material.
95
+ The project is built with Effect v4 and follows the GitHub Spec Kit workflow.
96
+ The feature contract lives in `specs/001-core-cleanup/`. Effect's official
97
+ source is vendored under `repos/` as a read-only reference using a Git subtree.
58
98
 
59
99
  ```bash
60
100
  pnpm install
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+ import { Console, Context, Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
3
+ import { Prompt } from "effect/unstable/cli";
4
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
5
+ //#region src/domain.ts
6
+ const skipReasons = [
7
+ "not-registered",
8
+ "symlink",
9
+ "main-worktree",
10
+ "current-worktree",
11
+ "locked",
12
+ "dirty",
13
+ "base-ref-unknown",
14
+ "not-merged",
15
+ "git-error",
16
+ "outside-root"
17
+ ];
18
+ const finishRecord = (records, current) => {
19
+ if (current.path === void 0) return;
20
+ records.push({
21
+ path: current.path,
22
+ ...current.head === void 0 ? {} : { head: current.head },
23
+ ...current.branch === void 0 ? {} : { branch: current.branch },
24
+ detached: current.detached,
25
+ ...current.lockedReason === void 0 ? {} : { lockedReason: current.lockedReason },
26
+ ...current.prunableReason === void 0 ? {} : { prunableReason: current.prunableReason }
27
+ });
28
+ };
29
+ /** Parse `git worktree list --porcelain -z` without interpreting path bytes as shell text. */
30
+ const parseWorktreePorcelain = (input) => {
31
+ const records = [];
32
+ let current = { detached: false };
33
+ for (const field of input.split("\0")) {
34
+ if (field.length === 0) {
35
+ finishRecord(records, current);
36
+ current = { detached: false };
37
+ continue;
38
+ }
39
+ const separator = field.indexOf(" ");
40
+ const key = separator === -1 ? field : field.slice(0, separator);
41
+ const value = separator === -1 ? "" : field.slice(separator + 1);
42
+ if (key === "worktree") {
43
+ if (current.path !== void 0) {
44
+ finishRecord(records, current);
45
+ current = { detached: false };
46
+ }
47
+ current.path = value;
48
+ } else if (key === "HEAD") current.head = value;
49
+ else if (key === "branch") current.branch = value;
50
+ else if (key === "detached") current.detached = true;
51
+ else if (key === "locked") current.lockedReason = value.length === 0 ? "locked" : value;
52
+ else if (key === "prunable") current.prunableReason = value.length === 0 ? "prunable" : value;
53
+ }
54
+ finishRecord(records, current);
55
+ return records.map((record, index) => ({
56
+ ...record,
57
+ isMain: index === 0
58
+ }));
59
+ };
60
+ const skipReasonLabel = {
61
+ "not-registered": "not a registered Git worktree",
62
+ symlink: "symlinked directories are never removed",
63
+ "main-worktree": "repository main worktree",
64
+ "current-worktree": "worktree running this command",
65
+ locked: "worktree is locked",
66
+ dirty: "worktree has tracked or untracked changes",
67
+ "base-ref-unknown": "could not determine a trusted base branch",
68
+ "not-merged": "HEAD is not integrated into the base branch",
69
+ "git-error": "Git state could not be proven",
70
+ "outside-root": "canonical path is outside the selected root"
71
+ };
72
+ //#endregion
73
+ //#region src/errors.ts
74
+ var GitExecutionError = class extends Schema.TaggedError()("GitExecutionError", {
75
+ operation: Schema.String,
76
+ message: Schema.String,
77
+ cause: Schema.Defect()
78
+ }) {};
79
+ var InputError = class extends Schema.TaggedError()("InputError", {
80
+ message: Schema.String,
81
+ exitCode: Schema.Int
82
+ }) {};
83
+ var DiscoveryError = class extends Schema.TaggedError()("DiscoveryError", {
84
+ path: Schema.String,
85
+ message: Schema.String,
86
+ cause: Schema.Defect()
87
+ }) {};
88
+ //#endregion
89
+ //#region src/git.ts
90
+ var Git = class Git extends Context.Service()("@thanaen/worktree-cleanup/Git") {
91
+ static layer = Layer.effect(Git, Effect.gen(function* () {
92
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
93
+ const run = Effect.fn("Git.run")(function* (cwd, args) {
94
+ const operation = `git ${args.join(" ")}`;
95
+ const handle = yield* spawner.spawn(ChildProcess.make("git", [...args], { cwd })).pipe(Effect.mapError((cause) => new GitExecutionError({
96
+ operation,
97
+ message: `Could not start Git in ${cwd}`,
98
+ cause
99
+ })));
100
+ const [stdout, stderr, exitCode] = yield* Effect.all([
101
+ Stream.mkString(Stream.decodeText(handle.stdout)),
102
+ Stream.mkString(Stream.decodeText(handle.stderr)),
103
+ handle.exitCode
104
+ ], { concurrency: "unbounded" }).pipe(Effect.mapError((cause) => new GitExecutionError({
105
+ operation,
106
+ message: `Git execution failed in ${cwd}`,
107
+ cause
108
+ })));
109
+ return {
110
+ stdout,
111
+ stderr,
112
+ exitCode: Number(exitCode)
113
+ };
114
+ }, Effect.scoped);
115
+ return Git.of({ run });
116
+ }));
117
+ };
118
+ //#endregion
119
+ //#region src/cleanup.ts
120
+ const smartRoots = [
121
+ {
122
+ relative: "worktrees",
123
+ source: "worktrees"
124
+ },
125
+ {
126
+ relative: ".claude/worktrees",
127
+ source: "claude"
128
+ },
129
+ {
130
+ relative: ".codex/worktrees",
131
+ source: "codex"
132
+ }
133
+ ];
134
+ const trim = (value) => value.trim();
135
+ const pathKey = (pathService, value) => {
136
+ const normalized = pathService.normalize(value);
137
+ return pathService.sep === "\\" ? normalized.toLowerCase() : normalized;
138
+ };
139
+ const discoveryFailure = (path, message) => (cause) => new DiscoveryError({
140
+ path,
141
+ message,
142
+ cause
143
+ });
144
+ const discoverRoots = Effect.fn("discoverRoots")(function* (cwd, explicitDirectory) {
145
+ const fs = yield* FileSystem.FileSystem;
146
+ const path = yield* Path.Path;
147
+ if (Option.isSome(explicitDirectory)) {
148
+ const requested = path.resolve(cwd, explicitDirectory.value);
149
+ if (!(yield* fs.exists(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect --dir"))))) return yield* new InputError({
150
+ message: `--dir does not exist: ${requested}`,
151
+ exitCode: 2
152
+ });
153
+ if ((yield* fs.stat(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect --dir")))).type !== "Directory") return yield* new InputError({
154
+ message: `--dir is not a directory: ${requested}`,
155
+ exitCode: 2
156
+ });
157
+ return [{
158
+ path: yield* fs.realPath(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not resolve --dir"))),
159
+ source: "explicit"
160
+ }];
161
+ }
162
+ const roots = [];
163
+ for (const entry of smartRoots) {
164
+ const requested = path.resolve(cwd, entry.relative);
165
+ if (!(yield* fs.exists(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect smart default"))))) continue;
166
+ if ((yield* fs.stat(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect smart default")))).type !== "Directory") continue;
167
+ const canonical = yield* fs.realPath(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not resolve smart default")));
168
+ roots.push({
169
+ path: canonical,
170
+ source: entry.source
171
+ });
172
+ }
173
+ return roots;
174
+ });
175
+ const enumerateCandidates = Effect.fn("enumerateCandidates")(function* (roots) {
176
+ const fs = yield* FileSystem.FileSystem;
177
+ const path = yield* Path.Path;
178
+ const candidates = [];
179
+ for (const root of roots) {
180
+ const names = yield* fs.readDirectory(root.path).pipe(Effect.mapError(discoveryFailure(root.path, "Could not list worktree root")));
181
+ for (const name of names.toSorted()) {
182
+ const requested = path.resolve(root.path, name);
183
+ if ((yield* fs.stat(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not inspect candidate")))).type !== "Directory") continue;
184
+ const canonical = yield* fs.realPath(requested).pipe(Effect.mapError(discoveryFailure(requested, "Could not resolve candidate")));
185
+ const normalizedRequested = path.normalize(requested);
186
+ if (canonical !== normalizedRequested) {
187
+ candidates.push({
188
+ path: normalizedRequested,
189
+ root,
190
+ structuralSkip: {
191
+ reason: path.dirname(canonical) === root.path ? "symlink" : "outside-root",
192
+ detail: `resolves to ${canonical}`
193
+ }
194
+ });
195
+ continue;
196
+ }
197
+ if (path.dirname(canonical) !== root.path) {
198
+ candidates.push({
199
+ path: canonical,
200
+ root,
201
+ structuralSkip: {
202
+ reason: "outside-root",
203
+ detail: `canonical parent is ${path.dirname(canonical)}`
204
+ }
205
+ });
206
+ continue;
207
+ }
208
+ candidates.push({
209
+ path: canonical,
210
+ root
211
+ });
212
+ }
213
+ }
214
+ const unique = /* @__PURE__ */ new Map();
215
+ for (const candidate of candidates) unique.set(candidate.path, candidate);
216
+ return [...unique.values()];
217
+ });
218
+ const findCurrentWorktree = Effect.fn("findCurrentWorktree")(function* (cwd) {
219
+ const git = yield* Git;
220
+ const fs = yield* FileSystem.FileSystem;
221
+ const result = yield* git.run(cwd, ["rev-parse", "--show-toplevel"]);
222
+ if (result.exitCode !== 0) return void 0;
223
+ return yield* fs.realPath(trim(result.stdout)).pipe(Effect.orElseSucceed(() => trim(result.stdout)));
224
+ });
225
+ const detectBaseRef = Effect.fn("detectBaseRef")(function* (repositoryPath, mainWorktree) {
226
+ const git = yield* Git;
227
+ const originHead = yield* git.run(repositoryPath, [
228
+ "symbolic-ref",
229
+ "--quiet",
230
+ "refs/remotes/origin/HEAD"
231
+ ]);
232
+ if (originHead.exitCode === 0 && trim(originHead.stdout).length > 0) return trim(originHead.stdout);
233
+ for (const ref of ["refs/heads/main", "refs/heads/master"]) if ((yield* git.run(repositoryPath, [
234
+ "show-ref",
235
+ "--verify",
236
+ "--quiet",
237
+ ref
238
+ ])).exitCode === 0) return ref;
239
+ if (mainWorktree.branch !== void 0) {
240
+ if ((yield* git.run(repositoryPath, [
241
+ "show-ref",
242
+ "--verify",
243
+ "--quiet",
244
+ mainWorktree.branch
245
+ ])).exitCode === 0) return mainWorktree.branch;
246
+ }
247
+ });
248
+ const skipped = (candidate, reason, detail, context) => ({
249
+ candidate,
250
+ status: "skipped",
251
+ ...reason === void 0 ? {} : { reason },
252
+ ...detail === void 0 ? {} : { detail },
253
+ ...context?.repositoryPath === void 0 ? {} : { repositoryPath: context.repositoryPath },
254
+ ...context?.worktree === void 0 ? {} : { worktree: context.worktree },
255
+ ...context?.baseRef === void 0 ? {} : { baseRef: context.baseRef }
256
+ });
257
+ const integrated = (evidence) => ({
258
+ status: "integrated",
259
+ evidence
260
+ });
261
+ const notIntegrated = (detail) => ({
262
+ status: "not-integrated",
263
+ detail
264
+ });
265
+ const integrationGitError = (detail) => ({
266
+ status: "git-error",
267
+ detail
268
+ });
269
+ const assessIntegration = Effect.fn("assessIntegration")(function* (repositoryPath, worktree, baseRef) {
270
+ const git = yield* Git;
271
+ const baseCommitResult = yield* git.run(repositoryPath, [
272
+ "rev-parse",
273
+ "--verify",
274
+ "--end-of-options",
275
+ `${baseRef}^{commit}`
276
+ ]);
277
+ if (baseCommitResult.exitCode !== 0) return integrationGitError(trim(baseCommitResult.stderr));
278
+ const baseCommit = trim(baseCommitResult.stdout);
279
+ const ancestor = yield* git.run(repositoryPath, [
280
+ "merge-base",
281
+ "--is-ancestor",
282
+ worktree.head,
283
+ baseCommit
284
+ ]);
285
+ if (ancestor.exitCode === 0) return integrated("ancestor");
286
+ if (ancestor.exitCode !== 1) return integrationGitError(trim(ancestor.stderr));
287
+ const baseDetail = `base: ${baseRef}`;
288
+ if (worktree.detached || worktree.branch === void 0) return notIntegrated(`${baseDetail}; content-equivalence proof requires an attached branch`);
289
+ const customDrivers = yield* git.run(repositoryPath, [
290
+ "config",
291
+ "--get-regexp",
292
+ "^merge\\..*\\.driver$"
293
+ ]);
294
+ if (customDrivers.exitCode === 0) return notIntegrated(`${baseDetail}; content-equivalence proof is disabled when custom merge drivers are configured`);
295
+ if (customDrivers.exitCode !== 1) return integrationGitError(trim(customDrivers.stderr));
296
+ const defaultDriver = yield* git.run(repositoryPath, [
297
+ "config",
298
+ "--get-all",
299
+ "merge.default"
300
+ ]);
301
+ if (defaultDriver.exitCode === 0) return notIntegrated(`${baseDetail}; content-equivalence proof is disabled when a default merge driver is configured`);
302
+ if (defaultDriver.exitCode !== 1) return integrationGitError(trim(defaultDriver.stderr));
303
+ const baseTreeResult = yield* git.run(repositoryPath, [
304
+ "rev-parse",
305
+ "--verify",
306
+ "--end-of-options",
307
+ `${baseCommit}^{tree}`
308
+ ]);
309
+ if (baseTreeResult.exitCode !== 0) return integrationGitError(trim(baseTreeResult.stderr));
310
+ const mergeTree = yield* git.run(repositoryPath, [
311
+ "merge-tree",
312
+ "--write-tree",
313
+ baseCommit,
314
+ worktree.head
315
+ ]);
316
+ if (mergeTree.exitCode === 1) return notIntegrated(`${baseDetail}; simulated merge has conflicts`);
317
+ if (mergeTree.exitCode !== 0) {
318
+ if (mergeTree.exitCode === 129) return integrationGitError("content-equivalence proof requires Git 2.38 or newer");
319
+ const message = trim(mergeTree.stderr);
320
+ return integrationGitError(message.length === 0 ? "content-equivalence proof requires Git 2.38 or newer" : message);
321
+ }
322
+ if (trim(mergeTree.stdout) !== trim(baseTreeResult.stdout)) return notIntegrated(`${baseDetail}; branch still contributes content not present in the base`);
323
+ const branchHead = yield* git.run(repositoryPath, [
324
+ "rev-parse",
325
+ "--verify",
326
+ "--end-of-options",
327
+ `${worktree.branch}^{commit}`
328
+ ]);
329
+ if (branchHead.exitCode !== 0 || trim(branchHead.stdout) !== worktree.head) return notIntegrated(`${baseDetail}; attached branch no longer points at the assessed HEAD`);
330
+ return integrated("content-equivalent");
331
+ });
332
+ const assessCandidate = Effect.fn("assessCandidate")(function* (candidate, currentWorktree) {
333
+ const git = yield* Git;
334
+ const fs = yield* FileSystem.FileSystem;
335
+ const path = yield* Path.Path;
336
+ if (candidate.structuralSkip !== void 0) return skipped(candidate, candidate.structuralSkip.reason, candidate.structuralSkip.detail);
337
+ const repository = yield* git.run(candidate.path, [
338
+ "rev-parse",
339
+ "--path-format=absolute",
340
+ "--git-common-dir"
341
+ ]);
342
+ if (repository.exitCode !== 0) return skipped(candidate, "not-registered", trim(repository.stderr));
343
+ const listResult = yield* git.run(candidate.path, [
344
+ "worktree",
345
+ "list",
346
+ "--porcelain",
347
+ "-z"
348
+ ]);
349
+ if (listResult.exitCode !== 0) return skipped(candidate, "git-error", trim(listResult.stderr));
350
+ const worktrees = parseWorktreePorcelain(listResult.stdout);
351
+ const mainWorktree = worktrees[0];
352
+ if (mainWorktree === void 0) return skipped(candidate, "git-error", "Git returned an empty worktree list");
353
+ const topLevel = yield* git.run(candidate.path, [
354
+ "rev-parse",
355
+ "--path-format=absolute",
356
+ "--show-toplevel"
357
+ ]);
358
+ if (topLevel.exitCode !== 0) return skipped(candidate, "git-error", trim(topLevel.stderr));
359
+ const gitCandidatePath = pathKey(path, trim(topLevel.stdout));
360
+ const registered = worktrees.find((entry) => pathKey(path, entry.path) === gitCandidatePath);
361
+ if (registered === void 0) return skipped(candidate, "not-registered");
362
+ const worktree = {
363
+ ...registered,
364
+ path: candidate.path
365
+ };
366
+ const canonicalMain = {
367
+ ...mainWorktree,
368
+ path: yield* fs.realPath(mainWorktree.path).pipe(Effect.orElseSucceed(() => path.normalize(mainWorktree.path)))
369
+ };
370
+ const repositoryPath = canonicalMain.path;
371
+ const context = {
372
+ repositoryPath,
373
+ worktree
374
+ };
375
+ if (worktree.isMain) return skipped(candidate, "main-worktree", void 0, context);
376
+ if (currentWorktree === candidate.path) return skipped(candidate, "current-worktree", void 0, context);
377
+ if (worktree.lockedReason !== void 0) return skipped(candidate, "locked", worktree.lockedReason, context);
378
+ if (worktree.head === void 0) return skipped(candidate, "git-error", "Git did not report a HEAD", context);
379
+ const status = yield* git.run(candidate.path, [
380
+ "status",
381
+ "--porcelain",
382
+ "--untracked-files=all"
383
+ ]);
384
+ if (status.exitCode !== 0) return skipped(candidate, "git-error", trim(status.stderr), context);
385
+ if (status.stdout.length > 0) return skipped(candidate, "dirty", void 0, context);
386
+ const baseRef = yield* detectBaseRef(repositoryPath, canonicalMain);
387
+ if (baseRef === void 0) return skipped(candidate, "base-ref-unknown", void 0, context);
388
+ const integration = yield* assessIntegration(repositoryPath, {
389
+ ...worktree,
390
+ head: worktree.head
391
+ }, baseRef);
392
+ if (integration.status === "not-integrated") return skipped(candidate, "not-merged", integration.detail, {
393
+ ...context,
394
+ baseRef
395
+ });
396
+ if (integration.status === "git-error") return skipped(candidate, "git-error", integration.detail, {
397
+ ...context,
398
+ baseRef
399
+ });
400
+ return {
401
+ candidate,
402
+ repositoryPath,
403
+ worktree,
404
+ baseRef,
405
+ integrationEvidence: integration.evidence,
406
+ status: "removable"
407
+ };
408
+ });
409
+ const renderPlan = Effect.fn("renderPlan")(function* (roots, assessments) {
410
+ yield* Console.log("Worktree cleanup plan");
411
+ yield* Console.log("Roots:");
412
+ for (const root of roots) yield* Console.log(` - ${root.path} (${root.source})`);
413
+ const removable = assessments.filter((assessment) => assessment.status === "removable");
414
+ const skippedItems = assessments.filter((assessment) => assessment.status === "skipped");
415
+ yield* Console.log(`Removable (${removable.length}):`);
416
+ if (removable.length === 0) yield* Console.log(" - none");
417
+ for (const assessment of removable) {
418
+ const evidence = assessment.integrationEvidence ?? "unknown";
419
+ yield* Console.log(` REMOVE ${assessment.candidate.path} [${assessment.baseRef}; ${evidence}]`);
420
+ }
421
+ yield* Console.log(`Skipped (${skippedItems.length}):`);
422
+ if (skippedItems.length === 0) yield* Console.log(" - none");
423
+ for (const assessment of skippedItems) {
424
+ const reason = assessment.reason === void 0 ? "unknown" : skipReasonLabel[assessment.reason];
425
+ const detail = assessment.detail === void 0 || assessment.detail.length === 0 ? "" : `: ${assessment.detail}`;
426
+ yield* Console.log(` skip ${assessment.candidate.path} — ${reason}${detail}`);
427
+ }
428
+ });
429
+ const runCleanup = Effect.fn("runCleanup")(function* (options) {
430
+ const git = yield* Git;
431
+ const roots = yield* discoverRoots(options.cwd, options.directory);
432
+ if (roots.length === 0) {
433
+ yield* Console.log("No worktree roots found (checked worktrees, .claude/worktrees, .codex/worktrees).");
434
+ return {
435
+ removed: [],
436
+ revalidationSkipped: [],
437
+ failures: []
438
+ };
439
+ }
440
+ const candidates = yield* enumerateCandidates(roots);
441
+ const currentWorktree = yield* findCurrentWorktree(options.cwd);
442
+ const assessments = yield* Effect.forEach(candidates, (candidate) => assessCandidate(candidate, currentWorktree), { concurrency: 4 });
443
+ yield* renderPlan(roots, assessments);
444
+ const removable = assessments.filter((assessment) => assessment.status === "removable" && assessment.repositoryPath !== void 0);
445
+ if (removable.length === 0) {
446
+ yield* Console.log("Nothing to remove.");
447
+ return {
448
+ removed: [],
449
+ revalidationSkipped: [],
450
+ failures: []
451
+ };
452
+ }
453
+ if (!options.yes && !options.interactive) return yield* new InputError({
454
+ message: "Refusing to delete without an interactive terminal. Pass --yes to approve.",
455
+ exitCode: 2
456
+ });
457
+ if (!(options.yes ? true : yield* Prompt.run(Prompt.confirm({
458
+ message: `Remove ${removable.length} stale worktree${removable.length === 1 ? "" : "s"}?`,
459
+ initial: false
460
+ })).pipe(Effect.orElseSucceed(() => false)))) {
461
+ yield* Console.log("Cleanup cancelled; nothing was removed.");
462
+ return {
463
+ removed: [],
464
+ revalidationSkipped: [],
465
+ failures: []
466
+ };
467
+ }
468
+ const removed = [];
469
+ const revalidationSkipped = [];
470
+ const failures = [];
471
+ for (const planned of removable) {
472
+ const revalidated = yield* assessCandidate(planned.candidate, currentWorktree);
473
+ if (revalidated.status !== "removable" || revalidated.repositoryPath === void 0) {
474
+ revalidationSkipped.push(revalidated);
475
+ continue;
476
+ }
477
+ const removal = yield* git.run(revalidated.repositoryPath, [
478
+ "worktree",
479
+ "remove",
480
+ "--",
481
+ revalidated.candidate.path
482
+ ]);
483
+ if (removal.exitCode === 0) removed.push(revalidated.candidate.path);
484
+ else failures.push({
485
+ path: revalidated.candidate.path,
486
+ message: trim(removal.stderr) || `git exited with ${removal.exitCode}`
487
+ });
488
+ }
489
+ yield* Console.log(`Cleanup complete: ${removed.length} removed, ${revalidationSkipped.length} skipped after revalidation, ${failures.length} failed.`);
490
+ for (const failure of failures) yield* Console.error(`Failed ${failure.path}: ${failure.message}`);
491
+ if (failures.length > 0) process.exitCode = 1;
492
+ return {
493
+ removed,
494
+ revalidationSkipped,
495
+ failures
496
+ };
497
+ });
498
+ //#endregion
499
+ export { skipReasons as a, skipReasonLabel as i, Git as n, parseWorktreePorcelain as r, runCleanup as t };
500
+
501
+ //# sourceMappingURL=cleanup-DeusOTMj.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup-DeusOTMj.mjs","names":[],"sources":["../src/domain.ts","../src/errors.ts","../src/git.ts","../src/cleanup.ts"],"sourcesContent":["export const skipReasons = [\n \"not-registered\",\n \"symlink\",\n \"main-worktree\",\n \"current-worktree\",\n \"locked\",\n \"dirty\",\n \"base-ref-unknown\",\n \"not-merged\",\n \"git-error\",\n \"outside-root\"\n] as const\n\nexport type SkipReason = (typeof skipReasons)[number]\n\nexport type IntegrationEvidence = \"ancestor\" | \"content-equivalent\"\n\nexport interface TargetRoot {\n readonly path: string\n readonly source: \"explicit\" | \"worktrees\" | \"claude\" | \"codex\"\n}\n\nexport interface RegisteredWorktree {\n readonly path: string\n readonly head?: string\n readonly branch?: string\n readonly detached: boolean\n readonly lockedReason?: string\n readonly prunableReason?: string\n readonly isMain: boolean\n}\n\nexport interface Candidate {\n readonly path: string\n readonly root: TargetRoot\n readonly structuralSkip?: {\n readonly reason: \"symlink\" | \"outside-root\"\n readonly detail: string\n }\n}\n\nexport interface Assessment {\n readonly candidate: Candidate\n readonly repositoryPath?: string\n readonly worktree?: RegisteredWorktree\n readonly baseRef?: string\n readonly integrationEvidence?: IntegrationEvidence\n readonly status: \"removable\" | \"skipped\"\n readonly reason?: SkipReason\n readonly detail?: string\n}\n\nexport interface CleanupResult {\n readonly removed: ReadonlyArray<string>\n readonly revalidationSkipped: ReadonlyArray<Assessment>\n readonly failures: ReadonlyArray<{\n readonly path: string\n readonly message: string\n }>\n}\n\ninterface MutableWorktree {\n path?: string\n head?: string\n branch?: string\n detached: boolean\n lockedReason?: string\n prunableReason?: string\n}\n\nconst finishRecord = (\n records: Array<Omit<RegisteredWorktree, \"isMain\">>,\n current: MutableWorktree\n): void => {\n if (current.path === undefined) return\n records.push({\n path: current.path,\n ...(current.head === undefined ? {} : { head: current.head }),\n ...(current.branch === undefined ? {} : { branch: current.branch }),\n detached: current.detached,\n ...(current.lockedReason === undefined ? {} : { lockedReason: current.lockedReason }),\n ...(current.prunableReason === undefined ? {} : { prunableReason: current.prunableReason })\n })\n}\n\n/** Parse `git worktree list --porcelain -z` without interpreting path bytes as shell text. */\nexport const parseWorktreePorcelain = (input: string): ReadonlyArray<RegisteredWorktree> => {\n const records: Array<Omit<RegisteredWorktree, \"isMain\">> = []\n let current: MutableWorktree = { detached: false }\n\n for (const field of input.split(\"\\0\")) {\n if (field.length === 0) {\n finishRecord(records, current)\n current = { detached: false }\n continue\n }\n\n const separator = field.indexOf(\" \")\n const key = separator === -1 ? field : field.slice(0, separator)\n const value = separator === -1 ? \"\" : field.slice(separator + 1)\n\n if (key === \"worktree\") {\n if (current.path !== undefined) {\n finishRecord(records, current)\n current = { detached: false }\n }\n current.path = value\n } else if (key === \"HEAD\") {\n current.head = value\n } else if (key === \"branch\") {\n current.branch = value\n } else if (key === \"detached\") {\n current.detached = true\n } else if (key === \"locked\") {\n current.lockedReason = value.length === 0 ? \"locked\" : value\n } else if (key === \"prunable\") {\n current.prunableReason = value.length === 0 ? \"prunable\" : value\n }\n }\n\n finishRecord(records, current)\n return records.map((record, index) => ({ ...record, isMain: index === 0 }))\n}\n\nexport const skipReasonLabel: Readonly<Record<SkipReason, string>> = {\n \"not-registered\": \"not a registered Git worktree\",\n symlink: \"symlinked directories are never removed\",\n \"main-worktree\": \"repository main worktree\",\n \"current-worktree\": \"worktree running this command\",\n locked: \"worktree is locked\",\n dirty: \"worktree has tracked or untracked changes\",\n \"base-ref-unknown\": \"could not determine a trusted base branch\",\n \"not-merged\": \"HEAD is not integrated into the base branch\",\n \"git-error\": \"Git state could not be proven\",\n \"outside-root\": \"canonical path is outside the selected root\"\n}\n","import { Schema } from \"effect\"\n\nexport class GitExecutionError extends Schema.TaggedError<GitExecutionError>()(\n \"GitExecutionError\",\n {\n operation: Schema.String,\n message: Schema.String,\n cause: Schema.Defect()\n }\n) {}\n\nexport class InputError extends Schema.TaggedError<InputError>()(\"InputError\", {\n message: Schema.String,\n exitCode: Schema.Int\n}) {}\n\nexport class DiscoveryError extends Schema.TaggedError<DiscoveryError>()(\"DiscoveryError\", {\n path: Schema.String,\n message: Schema.String,\n cause: Schema.Defect()\n}) {}\n\nexport type AppError = GitExecutionError | InputError | DiscoveryError\n","import { Context, Effect, Layer, Stream } from \"effect\"\nimport { ChildProcess, ChildProcessSpawner } from \"effect/unstable/process\"\n\nimport { GitExecutionError } from \"./errors.js\"\n\nexport interface GitResult {\n readonly exitCode: number\n readonly stdout: string\n readonly stderr: string\n}\n\nexport interface GitService {\n readonly run: (\n cwd: string,\n args: ReadonlyArray<string>\n ) => Effect.Effect<GitResult, GitExecutionError>\n}\n\nexport class Git extends Context.Service<Git, GitService>()(\"@thanaen/worktree-cleanup/Git\") {\n static readonly layer = Layer.effect(\n Git,\n Effect.gen(function* () {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n\n const run = Effect.fn(\"Git.run\")(function* (cwd: string, args: ReadonlyArray<string>) {\n const operation = `git ${args.join(\" \")}`\n const handle = yield* spawner.spawn(ChildProcess.make(\"git\", [...args], { cwd })).pipe(\n Effect.mapError(\n (cause) =>\n new GitExecutionError({\n operation,\n message: `Could not start Git in ${cwd}`,\n cause\n })\n )\n )\n\n const [stdout, stderr, exitCode] = yield* Effect.all(\n [\n Stream.mkString(Stream.decodeText(handle.stdout)),\n Stream.mkString(Stream.decodeText(handle.stderr)),\n handle.exitCode\n ] as const,\n { concurrency: \"unbounded\" }\n ).pipe(\n Effect.mapError(\n (cause) =>\n new GitExecutionError({\n operation,\n message: `Git execution failed in ${cwd}`,\n cause\n })\n )\n )\n\n return {\n stdout,\n stderr,\n exitCode: Number(exitCode)\n }\n }, Effect.scoped)\n\n return Git.of({ run })\n })\n )\n}\n","import { Console, Effect, FileSystem, Option, Path } from \"effect\"\nimport { Prompt } from \"effect/unstable/cli\"\n\nimport type {\n Assessment,\n Candidate,\n CleanupResult,\n IntegrationEvidence,\n RegisteredWorktree,\n TargetRoot\n} from \"./domain.js\"\nimport { parseWorktreePorcelain, skipReasonLabel } from \"./domain.js\"\nimport { DiscoveryError, InputError } from \"./errors.js\"\nimport { Git } from \"./git.js\"\n\nconst smartRoots = [\n { relative: \"worktrees\", source: \"worktrees\" },\n { relative: \".claude/worktrees\", source: \"claude\" },\n { relative: \".codex/worktrees\", source: \"codex\" }\n] as const\n\nconst trim = (value: string): string => value.trim()\n\nconst pathKey = (pathService: Path.Path, value: string): string => {\n const normalized = pathService.normalize(value)\n return pathService.sep === \"\\\\\" ? normalized.toLowerCase() : normalized\n}\n\nconst discoveryFailure = (path: string, message: string) => (cause: unknown) =>\n new DiscoveryError({ path, message, cause })\n\nexport const discoverRoots = Effect.fn(\"discoverRoots\")(function* (\n cwd: string,\n explicitDirectory: Option.Option<string>\n) {\n const fs = yield* FileSystem.FileSystem\n const path = yield* Path.Path\n\n if (Option.isSome(explicitDirectory)) {\n const requested = path.resolve(cwd, explicitDirectory.value)\n const exists = yield* fs\n .exists(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect --dir\")))\n if (!exists) {\n return yield* new InputError({\n message: `--dir does not exist: ${requested}`,\n exitCode: 2\n })\n }\n const info = yield* fs\n .stat(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect --dir\")))\n if (info.type !== \"Directory\") {\n return yield* new InputError({\n message: `--dir is not a directory: ${requested}`,\n exitCode: 2\n })\n }\n const canonical = yield* fs\n .realPath(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not resolve --dir\")))\n return [{ path: canonical, source: \"explicit\" }] satisfies ReadonlyArray<TargetRoot>\n }\n\n const roots: Array<TargetRoot> = []\n for (const entry of smartRoots) {\n const requested = path.resolve(cwd, entry.relative)\n const exists = yield* fs\n .exists(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect smart default\")))\n if (!exists) continue\n const info = yield* fs\n .stat(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect smart default\")))\n if (info.type !== \"Directory\") continue\n const canonical = yield* fs\n .realPath(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not resolve smart default\")))\n roots.push({ path: canonical, source: entry.source })\n }\n\n return roots\n})\n\nexport const enumerateCandidates = Effect.fn(\"enumerateCandidates\")(function* (\n roots: ReadonlyArray<TargetRoot>\n) {\n const fs = yield* FileSystem.FileSystem\n const path = yield* Path.Path\n const candidates: Array<Candidate> = []\n\n for (const root of roots) {\n const names = yield* fs\n .readDirectory(root.path)\n .pipe(Effect.mapError(discoveryFailure(root.path, \"Could not list worktree root\")))\n\n for (const name of names.toSorted()) {\n const requested = path.resolve(root.path, name)\n const info = yield* fs\n .stat(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not inspect candidate\")))\n if (info.type !== \"Directory\") continue\n\n const canonical = yield* fs\n .realPath(requested)\n .pipe(Effect.mapError(discoveryFailure(requested, \"Could not resolve candidate\")))\n const normalizedRequested = path.normalize(requested)\n\n if (canonical !== normalizedRequested) {\n candidates.push({\n path: normalizedRequested,\n root,\n structuralSkip: {\n reason: path.dirname(canonical) === root.path ? \"symlink\" : \"outside-root\",\n detail: `resolves to ${canonical}`\n }\n })\n continue\n }\n if (path.dirname(canonical) !== root.path) {\n candidates.push({\n path: canonical,\n root,\n structuralSkip: {\n reason: \"outside-root\",\n detail: `canonical parent is ${path.dirname(canonical)}`\n }\n })\n continue\n }\n candidates.push({ path: canonical, root })\n }\n }\n\n const unique = new Map<string, Candidate>()\n for (const candidate of candidates) unique.set(candidate.path, candidate)\n return [...unique.values()]\n})\n\nconst findCurrentWorktree = Effect.fn(\"findCurrentWorktree\")(function* (cwd: string) {\n const git = yield* Git\n const fs = yield* FileSystem.FileSystem\n const result = yield* git.run(cwd, [\"rev-parse\", \"--show-toplevel\"])\n if (result.exitCode !== 0) return undefined\n return yield* fs\n .realPath(trim(result.stdout))\n .pipe(Effect.orElseSucceed(() => trim(result.stdout)))\n})\n\nconst detectBaseRef = Effect.fn(\"detectBaseRef\")(function* (\n repositoryPath: string,\n mainWorktree: RegisteredWorktree\n) {\n const git = yield* Git\n const originHead = yield* git.run(repositoryPath, [\n \"symbolic-ref\",\n \"--quiet\",\n \"refs/remotes/origin/HEAD\"\n ])\n if (originHead.exitCode === 0 && trim(originHead.stdout).length > 0)\n return trim(originHead.stdout)\n\n for (const ref of [\"refs/heads/main\", \"refs/heads/master\"]) {\n const exists = yield* git.run(repositoryPath, [\"show-ref\", \"--verify\", \"--quiet\", ref])\n if (exists.exitCode === 0) return ref\n }\n\n if (mainWorktree.branch !== undefined) {\n const exists = yield* git.run(repositoryPath, [\n \"show-ref\",\n \"--verify\",\n \"--quiet\",\n mainWorktree.branch\n ])\n if (exists.exitCode === 0) return mainWorktree.branch\n }\n return undefined\n})\n\nconst skipped = (\n candidate: Candidate,\n reason: Assessment[\"reason\"],\n detail?: string,\n context?: {\n readonly repositoryPath?: string\n readonly worktree?: RegisteredWorktree\n readonly baseRef?: string\n }\n): Assessment => ({\n candidate,\n status: \"skipped\",\n ...(reason === undefined ? {} : { reason }),\n ...(detail === undefined ? {} : { detail }),\n ...(context?.repositoryPath === undefined ? {} : { repositoryPath: context.repositoryPath }),\n ...(context?.worktree === undefined ? {} : { worktree: context.worktree }),\n ...(context?.baseRef === undefined ? {} : { baseRef: context.baseRef })\n})\n\ntype IntegrationAssessment =\n | { readonly status: \"integrated\"; readonly evidence: IntegrationEvidence }\n | { readonly status: \"not-integrated\"; readonly detail: string }\n | { readonly status: \"git-error\"; readonly detail: string }\n\nconst integrated = (evidence: IntegrationEvidence): IntegrationAssessment => ({\n status: \"integrated\",\n evidence\n})\n\nconst notIntegrated = (detail: string): IntegrationAssessment => ({\n status: \"not-integrated\",\n detail\n})\n\nconst integrationGitError = (detail: string): IntegrationAssessment => ({\n status: \"git-error\",\n detail\n})\n\nconst assessIntegration = Effect.fn(\"assessIntegration\")(function* (\n repositoryPath: string,\n worktree: RegisteredWorktree & { readonly head: string },\n baseRef: string\n) {\n const git = yield* Git\n const baseCommitResult = yield* git.run(repositoryPath, [\n \"rev-parse\",\n \"--verify\",\n \"--end-of-options\",\n `${baseRef}^{commit}`\n ])\n if (baseCommitResult.exitCode !== 0) {\n return integrationGitError(trim(baseCommitResult.stderr))\n }\n const baseCommit = trim(baseCommitResult.stdout)\n\n const ancestor = yield* git.run(repositoryPath, [\n \"merge-base\",\n \"--is-ancestor\",\n worktree.head,\n baseCommit\n ])\n if (ancestor.exitCode === 0) return integrated(\"ancestor\")\n if (ancestor.exitCode !== 1) {\n return integrationGitError(trim(ancestor.stderr))\n }\n\n const baseDetail = `base: ${baseRef}`\n if (worktree.detached || worktree.branch === undefined) {\n return notIntegrated(`${baseDetail}; content-equivalence proof requires an attached branch`)\n }\n\n const customDrivers = yield* git.run(repositoryPath, [\n \"config\",\n \"--get-regexp\",\n \"^merge\\\\..*\\\\.driver$\"\n ])\n if (customDrivers.exitCode === 0) {\n return notIntegrated(\n `${baseDetail}; content-equivalence proof is disabled when custom merge drivers are configured`\n )\n }\n if (customDrivers.exitCode !== 1) {\n return integrationGitError(trim(customDrivers.stderr))\n }\n\n const defaultDriver = yield* git.run(repositoryPath, [\"config\", \"--get-all\", \"merge.default\"])\n if (defaultDriver.exitCode === 0) {\n return notIntegrated(\n `${baseDetail}; content-equivalence proof is disabled when a default merge driver is configured`\n )\n }\n if (defaultDriver.exitCode !== 1) {\n return integrationGitError(trim(defaultDriver.stderr))\n }\n\n const baseTreeResult = yield* git.run(repositoryPath, [\n \"rev-parse\",\n \"--verify\",\n \"--end-of-options\",\n `${baseCommit}^{tree}`\n ])\n if (baseTreeResult.exitCode !== 0) {\n return integrationGitError(trim(baseTreeResult.stderr))\n }\n\n const mergeTree = yield* git.run(repositoryPath, [\n \"merge-tree\",\n \"--write-tree\",\n baseCommit,\n worktree.head\n ])\n if (mergeTree.exitCode === 1) {\n return notIntegrated(`${baseDetail}; simulated merge has conflicts`)\n }\n if (mergeTree.exitCode !== 0) {\n if (mergeTree.exitCode === 129) {\n return integrationGitError(\"content-equivalence proof requires Git 2.38 or newer\")\n }\n const message = trim(mergeTree.stderr)\n return integrationGitError(\n message.length === 0 ? \"content-equivalence proof requires Git 2.38 or newer\" : message\n )\n }\n if (trim(mergeTree.stdout) !== trim(baseTreeResult.stdout)) {\n return notIntegrated(`${baseDetail}; branch still contributes content not present in the base`)\n }\n\n const branchHead = yield* git.run(repositoryPath, [\n \"rev-parse\",\n \"--verify\",\n \"--end-of-options\",\n `${worktree.branch}^{commit}`\n ])\n if (branchHead.exitCode !== 0 || trim(branchHead.stdout) !== worktree.head) {\n return notIntegrated(`${baseDetail}; attached branch no longer points at the assessed HEAD`)\n }\n\n return integrated(\"content-equivalent\")\n})\n\nexport const assessCandidate = Effect.fn(\"assessCandidate\")(function* (\n candidate: Candidate,\n currentWorktree: string | undefined\n) {\n const git = yield* Git\n const fs = yield* FileSystem.FileSystem\n const path = yield* Path.Path\n\n if (candidate.structuralSkip !== undefined) {\n return skipped(candidate, candidate.structuralSkip.reason, candidate.structuralSkip.detail)\n }\n\n const repository = yield* git.run(candidate.path, [\n \"rev-parse\",\n \"--path-format=absolute\",\n \"--git-common-dir\"\n ])\n if (repository.exitCode !== 0) {\n return skipped(candidate, \"not-registered\", trim(repository.stderr))\n }\n\n const listResult = yield* git.run(candidate.path, [\"worktree\", \"list\", \"--porcelain\", \"-z\"])\n if (listResult.exitCode !== 0) {\n return skipped(candidate, \"git-error\", trim(listResult.stderr))\n }\n const worktrees = parseWorktreePorcelain(listResult.stdout)\n const mainWorktree = worktrees[0]\n if (mainWorktree === undefined) {\n return skipped(candidate, \"git-error\", \"Git returned an empty worktree list\")\n }\n\n const topLevel = yield* git.run(candidate.path, [\n \"rev-parse\",\n \"--path-format=absolute\",\n \"--show-toplevel\"\n ])\n if (topLevel.exitCode !== 0) {\n return skipped(candidate, \"git-error\", trim(topLevel.stderr))\n }\n\n // Git for Windows can spell the same path using either its long form or an\n // 8.3 component (for example `runneradmin` versus `RUNNER~1`). Match the\n // path reported by Git from inside the candidate against Git's own list.\n const gitCandidatePath = pathKey(path, trim(topLevel.stdout))\n const registered = worktrees.find((entry) => pathKey(path, entry.path) === gitCandidatePath)\n if (registered === undefined) {\n return skipped(candidate, \"not-registered\")\n }\n const worktree = { ...registered, path: candidate.path }\n const canonicalMain = {\n ...mainWorktree,\n path: yield* fs\n .realPath(mainWorktree.path)\n .pipe(Effect.orElseSucceed(() => path.normalize(mainWorktree.path)))\n }\n const repositoryPath = canonicalMain.path\n const context = { repositoryPath, worktree }\n\n if (worktree.isMain) return skipped(candidate, \"main-worktree\", undefined, context)\n if (currentWorktree === candidate.path) {\n return skipped(candidate, \"current-worktree\", undefined, context)\n }\n if (worktree.lockedReason !== undefined) {\n return skipped(candidate, \"locked\", worktree.lockedReason, context)\n }\n if (worktree.head === undefined) {\n return skipped(candidate, \"git-error\", \"Git did not report a HEAD\", context)\n }\n\n const status = yield* git.run(candidate.path, [\"status\", \"--porcelain\", \"--untracked-files=all\"])\n if (status.exitCode !== 0) return skipped(candidate, \"git-error\", trim(status.stderr), context)\n if (status.stdout.length > 0) return skipped(candidate, \"dirty\", undefined, context)\n\n const baseRef = yield* detectBaseRef(repositoryPath, canonicalMain)\n if (baseRef === undefined) return skipped(candidate, \"base-ref-unknown\", undefined, context)\n\n const integration = yield* assessIntegration(\n repositoryPath,\n { ...worktree, head: worktree.head },\n baseRef\n )\n if (integration.status === \"not-integrated\") {\n return skipped(candidate, \"not-merged\", integration.detail, { ...context, baseRef })\n }\n if (integration.status === \"git-error\") {\n return skipped(candidate, \"git-error\", integration.detail, { ...context, baseRef })\n }\n\n return {\n candidate,\n repositoryPath,\n worktree,\n baseRef,\n integrationEvidence: integration.evidence,\n status: \"removable\" as const\n }\n})\n\nconst renderPlan = Effect.fn(\"renderPlan\")(function* (\n roots: ReadonlyArray<TargetRoot>,\n assessments: ReadonlyArray<Assessment>\n) {\n yield* Console.log(\"Worktree cleanup plan\")\n yield* Console.log(\"Roots:\")\n for (const root of roots) yield* Console.log(` - ${root.path} (${root.source})`)\n\n const removable = assessments.filter((assessment) => assessment.status === \"removable\")\n const skippedItems = assessments.filter((assessment) => assessment.status === \"skipped\")\n\n yield* Console.log(`Removable (${removable.length}):`)\n if (removable.length === 0) yield* Console.log(\" - none\")\n for (const assessment of removable) {\n const evidence = assessment.integrationEvidence ?? \"unknown\"\n yield* Console.log(` REMOVE ${assessment.candidate.path} [${assessment.baseRef}; ${evidence}]`)\n }\n\n yield* Console.log(`Skipped (${skippedItems.length}):`)\n if (skippedItems.length === 0) yield* Console.log(\" - none\")\n for (const assessment of skippedItems) {\n const reason = assessment.reason === undefined ? \"unknown\" : skipReasonLabel[assessment.reason]\n const detail =\n assessment.detail === undefined || assessment.detail.length === 0\n ? \"\"\n : `: ${assessment.detail}`\n yield* Console.log(` skip ${assessment.candidate.path} — ${reason}${detail}`)\n }\n})\n\nexport interface CleanupOptions {\n readonly cwd: string\n readonly directory: Option.Option<string>\n readonly yes: boolean\n readonly interactive: boolean\n}\n\nexport const runCleanup = Effect.fn(\"runCleanup\")(function* (options: CleanupOptions) {\n const git = yield* Git\n const roots = yield* discoverRoots(options.cwd, options.directory)\n if (roots.length === 0) {\n yield* Console.log(\n \"No worktree roots found (checked worktrees, .claude/worktrees, .codex/worktrees).\"\n )\n return {\n removed: [],\n revalidationSkipped: [],\n failures: []\n } satisfies CleanupResult\n }\n\n const candidates = yield* enumerateCandidates(roots)\n const currentWorktree = yield* findCurrentWorktree(options.cwd)\n const assessments = yield* Effect.forEach(\n candidates,\n (candidate) => assessCandidate(candidate, currentWorktree),\n { concurrency: 4 }\n )\n yield* renderPlan(roots, assessments)\n\n const removable = assessments.filter(\n (\n assessment\n ): assessment is Assessment & {\n readonly repositoryPath: string\n readonly status: \"removable\"\n } => assessment.status === \"removable\" && assessment.repositoryPath !== undefined\n )\n if (removable.length === 0) {\n yield* Console.log(\"Nothing to remove.\")\n return {\n removed: [],\n revalidationSkipped: [],\n failures: []\n } satisfies CleanupResult\n }\n\n if (!options.yes && !options.interactive) {\n return yield* new InputError({\n message: \"Refusing to delete without an interactive terminal. Pass --yes to approve.\",\n exitCode: 2\n })\n }\n\n const confirmed = options.yes\n ? true\n : yield* Prompt.run(\n Prompt.confirm({\n message: `Remove ${removable.length} stale worktree${removable.length === 1 ? \"\" : \"s\"}?`,\n initial: false\n })\n ).pipe(Effect.orElseSucceed(() => false))\n\n if (!confirmed) {\n yield* Console.log(\"Cleanup cancelled; nothing was removed.\")\n return {\n removed: [],\n revalidationSkipped: [],\n failures: []\n } satisfies CleanupResult\n }\n\n const removed: Array<string> = []\n const revalidationSkipped: Array<Assessment> = []\n const failures: Array<{ path: string; message: string }> = []\n\n for (const planned of removable) {\n const revalidated = yield* assessCandidate(planned.candidate, currentWorktree)\n if (revalidated.status !== \"removable\" || revalidated.repositoryPath === undefined) {\n revalidationSkipped.push(revalidated)\n continue\n }\n\n const removal = yield* git.run(revalidated.repositoryPath, [\n \"worktree\",\n \"remove\",\n \"--\",\n revalidated.candidate.path\n ])\n if (removal.exitCode === 0) {\n removed.push(revalidated.candidate.path)\n } else {\n failures.push({\n path: revalidated.candidate.path,\n message: trim(removal.stderr) || `git exited with ${removal.exitCode}`\n })\n }\n }\n\n yield* Console.log(\n `Cleanup complete: ${removed.length} removed, ${revalidationSkipped.length} skipped after revalidation, ${failures.length} failed.`\n )\n for (const failure of failures) yield* Console.error(`Failed ${failure.path}: ${failure.message}`)\n\n if (failures.length > 0) {\n process.exitCode = 1\n }\n return { removed, revalidationSkipped, failures } satisfies CleanupResult\n})\n"],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+
2
+ import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
3
+ import { Prompt } from "effect/unstable/cli";
4
+ import { ChildProcessSpawner } from "effect/unstable/process";
5
+ //#region src/errors.d.ts
6
+ declare const GitExecutionError_base: Schema.Class<GitExecutionError, Schema.TaggedStruct<"GitExecutionError", {
7
+ readonly operation: Schema.String;
8
+ readonly message: Schema.String;
9
+ readonly cause: Schema.Defect;
10
+ }>, import("effect/Cause").YieldableError>;
11
+ declare class GitExecutionError extends GitExecutionError_base {}
12
+ declare const InputError_base: Schema.Class<InputError, Schema.TaggedStruct<"InputError", {
13
+ readonly message: Schema.String;
14
+ readonly exitCode: Schema.Int;
15
+ }>, import("effect/Cause").YieldableError>;
16
+ declare class InputError extends InputError_base {}
17
+ declare const DiscoveryError_base: Schema.Class<DiscoveryError, Schema.TaggedStruct<"DiscoveryError", {
18
+ readonly path: Schema.String;
19
+ readonly message: Schema.String;
20
+ readonly cause: Schema.Defect;
21
+ }>, import("effect/Cause").YieldableError>;
22
+ declare class DiscoveryError extends DiscoveryError_base {}
23
+ //#endregion
24
+ //#region src/domain.d.ts
25
+ declare const skipReasons: readonly ["not-registered", "symlink", "main-worktree", "current-worktree", "locked", "dirty", "base-ref-unknown", "not-merged", "git-error", "outside-root"];
26
+ type SkipReason = (typeof skipReasons)[number];
27
+ type IntegrationEvidence = "ancestor" | "content-equivalent";
28
+ interface TargetRoot {
29
+ readonly path: string;
30
+ readonly source: "explicit" | "worktrees" | "claude" | "codex";
31
+ }
32
+ interface RegisteredWorktree {
33
+ readonly path: string;
34
+ readonly head?: string;
35
+ readonly branch?: string;
36
+ readonly detached: boolean;
37
+ readonly lockedReason?: string;
38
+ readonly prunableReason?: string;
39
+ readonly isMain: boolean;
40
+ }
41
+ interface Candidate {
42
+ readonly path: string;
43
+ readonly root: TargetRoot;
44
+ readonly structuralSkip?: {
45
+ readonly reason: "symlink" | "outside-root";
46
+ readonly detail: string;
47
+ };
48
+ }
49
+ interface Assessment {
50
+ readonly candidate: Candidate;
51
+ readonly repositoryPath?: string;
52
+ readonly worktree?: RegisteredWorktree;
53
+ readonly baseRef?: string;
54
+ readonly integrationEvidence?: IntegrationEvidence;
55
+ readonly status: "removable" | "skipped";
56
+ readonly reason?: SkipReason;
57
+ readonly detail?: string;
58
+ }
59
+ interface CleanupResult {
60
+ readonly removed: ReadonlyArray<string>;
61
+ readonly revalidationSkipped: ReadonlyArray<Assessment>;
62
+ readonly failures: ReadonlyArray<{
63
+ readonly path: string;
64
+ readonly message: string;
65
+ }>;
66
+ }
67
+ /** Parse `git worktree list --porcelain -z` without interpreting path bytes as shell text. */
68
+ declare const parseWorktreePorcelain: (input: string) => ReadonlyArray<RegisteredWorktree>;
69
+ declare const skipReasonLabel: Readonly<Record<SkipReason, string>>;
70
+ //#endregion
71
+ //#region src/git.d.ts
72
+ interface GitResult {
73
+ readonly exitCode: number;
74
+ readonly stdout: string;
75
+ readonly stderr: string;
76
+ }
77
+ interface GitService {
78
+ readonly run: (cwd: string, args: ReadonlyArray<string>) => Effect.Effect<GitResult, GitExecutionError>;
79
+ }
80
+ declare const Git_base: Context.ServiceClass<Git, "@thanaen/worktree-cleanup/Git", GitService>;
81
+ declare class Git extends Git_base {
82
+ static readonly layer: Layer.Layer<Git, never, ChildProcessSpawner.ChildProcessSpawner>;
83
+ }
84
+ //#endregion
85
+ //#region src/cleanup.d.ts
86
+ interface CleanupOptions {
87
+ readonly cwd: string;
88
+ readonly directory: Option.Option<string>;
89
+ readonly yes: boolean;
90
+ readonly interactive: boolean;
91
+ }
92
+ declare const runCleanup: (options: CleanupOptions) => Effect.Effect<{
93
+ removed: string[];
94
+ revalidationSkipped: Assessment[];
95
+ failures: {
96
+ path: string;
97
+ message: string;
98
+ }[];
99
+ }, DiscoveryError | GitExecutionError | InputError, Git | Prompt.Environment>;
100
+ //#endregion
101
+ export { type Assessment, type Candidate, type CleanupOptions, type CleanupResult, Git, type GitResult, type GitService, type IntegrationEvidence, type RegisteredWorktree, type SkipReason, type TargetRoot, parseWorktreePorcelain, runCleanup, skipReasonLabel, skipReasons };
102
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts","../src/domain.ts","../src/git.ts","../src/cleanup.ts"],"mappings":";;;;;;;;;;cAEa,0BAA0B;;;;;cAS1B,mBAAmB;;;;;;cAKnB,uBAAuB;;;cChBvB;KAaD,qBAAqB;KAErB;UAEK;WACN;WACA;;UAGM;WACN;WACA;WACA;WACA;WACA;WACA;WACA;;UAGM;WACN;WACA,MAAM;WACN;aACE;aACA;;;UAII;WACN,WAAW;WACX;WACA,WAAW;WACX;WACA,sBAAsB;WACtB;WACA,SAAS;WACT;;UAGM;WACN,SAAS;WACT,qBAAqB,cAAc;WACnC,UAAU;aACR;aACA;;;;cA6BA,yBAAsB,kBAAoB,cAAc;cAsCxD,iBAAiB,SAAS,OAAO;;;UCvH7B;WACN;WACA;WACA;;UAGM;WACN,MACP,aACA,MAAM,0BACH,OAAO,OAAO,WAAW;;;cAGnB,YAAY;kBACP,OAAK,MAAA,MAAA,YAAA,oBAAA;;;;UC6aN;WACN;WACA,WAAW,OAAO;WAClB;WACA;;cAGE,aAAU,SAAA,mBAAA,OAAA;;;;IAmES;IAAiB;;GAkC/C,iBAAA,oBAAA,YAAA,MAAA,OAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { a as skipReasons, i as skipReasonLabel, n as Git, r as parseWorktreePorcelain, t as runCleanup } from "./cleanup-DeusOTMj.mjs";
3
+ export { Git, parseWorktreePorcelain, runCleanup, skipReasonLabel, skipReasons };
@@ -0,0 +1,2 @@
1
+
2
+ export {}
package/dist/main.mjs ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import { n as Git, t as runCleanup } from "./cleanup-DeusOTMj.mjs";
3
+ import { Console, Effect, Layer } from "effect";
4
+ import { Command, Flag } from "effect/unstable/cli";
5
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
6
+ //#region package.json
7
+ var version = "0.2.0";
8
+ //#endregion
9
+ //#region src/cli.ts
10
+ const directory = Flag.string("dir").pipe(Flag.withDescription("Inspect exactly this worktree root and ignore smart defaults"), Flag.optional);
11
+ const yes = Flag.boolean("yes").pipe(Flag.withAlias("y"), Flag.withDescription("Approve the displayed cleanup plan without prompting"));
12
+ const handleError = (error) => {
13
+ const candidate = error;
14
+ const message = typeof candidate.message === "string" ? candidate.message : String(error);
15
+ const exitCode = typeof candidate.exitCode === "number" ? candidate.exitCode : 1;
16
+ return Console.error(`Error: ${message}`).pipe(Effect.andThen(Effect.sync(() => {
17
+ process.exitCode = exitCode;
18
+ })));
19
+ };
20
+ const program = Command.make("worktree-cleanup", {
21
+ directory,
22
+ yes
23
+ }, Effect.fn("worktree-cleanup")(function* ({ directory, yes }) {
24
+ yield* runCleanup({
25
+ cwd: process.cwd(),
26
+ directory,
27
+ yes,
28
+ interactive: process.stdin.isTTY === true
29
+ }).pipe(Effect.catch(handleError));
30
+ })).pipe(Command.withAlias("worktree-clean"), Command.withDescription("Safely remove clean Git worktrees already integrated into the base branch"), Command.withExamples([{
31
+ command: "worktree-cleanup",
32
+ description: "Inspect smart-default worktree roots and ask before deletion"
33
+ }, {
34
+ command: "worktree-cleanup --dir ../worktrees --yes",
35
+ description: "Clean one explicit root non-interactively"
36
+ }])).pipe(Command.run({ version }));
37
+ //#endregion
38
+ //#region src/main.ts
39
+ const MainLayer = Git.layer.pipe(Layer.provideMerge(NodeServices.layer));
40
+ program.pipe(Effect.provide(MainLayer), NodeRuntime.runMain);
41
+ //#endregion
42
+ export {};
43
+
44
+ //# sourceMappingURL=main.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.mjs","names":["packageJson.version"],"sources":["../package.json","../src/cli.ts","../src/main.ts"],"sourcesContent":["","import { Console, Effect } from \"effect\"\nimport { Command, Flag } from \"effect/unstable/cli\"\n\nimport packageJson from \"../package.json\" with { type: \"json\" }\nimport { runCleanup } from \"./cleanup.js\"\n\nconst directory = Flag.string(\"dir\").pipe(\n Flag.withDescription(\"Inspect exactly this worktree root and ignore smart defaults\"),\n Flag.optional\n)\n\nconst yes = Flag.boolean(\"yes\").pipe(\n Flag.withAlias(\"y\"),\n Flag.withDescription(\"Approve the displayed cleanup plan without prompting\")\n)\n\nconst handleError = (error: unknown) => {\n const candidate = error as { readonly message?: unknown; readonly exitCode?: unknown }\n const message = typeof candidate.message === \"string\" ? candidate.message : String(error)\n const exitCode = typeof candidate.exitCode === \"number\" ? candidate.exitCode : 1\n return Console.error(`Error: ${message}`).pipe(\n Effect.andThen(\n Effect.sync(() => {\n process.exitCode = exitCode\n })\n )\n )\n}\n\nexport const command = Command.make(\n \"worktree-cleanup\",\n { directory, yes },\n Effect.fn(\"worktree-cleanup\")(function* ({ directory, yes }) {\n yield* runCleanup({\n cwd: process.cwd(),\n directory,\n yes,\n interactive: process.stdin.isTTY === true\n }).pipe(Effect.catch(handleError))\n })\n).pipe(\n Command.withAlias(\"worktree-clean\"),\n Command.withDescription(\n \"Safely remove clean Git worktrees already integrated into the base branch\"\n ),\n Command.withExamples([\n {\n command: \"worktree-cleanup\",\n description: \"Inspect smart-default worktree roots and ask before deletion\"\n },\n {\n command: \"worktree-cleanup --dir ../worktrees --yes\",\n description: \"Clean one explicit root non-interactively\"\n }\n ])\n)\n\nexport const program = command.pipe(Command.run({ version: packageJson.version }))\n","import { NodeRuntime, NodeServices } from \"@effect/platform-node\"\nimport { Effect, Layer } from \"effect\"\n\nimport { program } from \"./cli.js\"\nimport { Git } from \"./git.js\"\n\nconst MainLayer = Git.layer.pipe(Layer.provideMerge(NodeServices.layer))\n\nprogram.pipe(Effect.provide(MainLayer), NodeRuntime.runMain)\n"],"mappings":";;;;;;;;;ACMA,MAAM,YAAY,KAAK,OAAO,KAAK,CAAC,CAAC,KACnC,KAAK,gBAAgB,8DAA8D,GACnF,KAAK,QACP;AAEA,MAAM,MAAM,KAAK,QAAQ,KAAK,CAAC,CAAC,KAC9B,KAAK,UAAU,GAAG,GAClB,KAAK,gBAAgB,sDAAsD,CAC7E;AAEA,MAAM,eAAe,UAAmB;CACtC,MAAM,YAAY;CAClB,MAAM,UAAU,OAAO,UAAU,YAAY,WAAW,UAAU,UAAU,OAAO,KAAK;CACxF,MAAM,WAAW,OAAO,UAAU,aAAa,WAAW,UAAU,WAAW;CAC/E,OAAO,QAAQ,MAAM,UAAU,SAAS,CAAC,CAAC,KACxC,OAAO,QACL,OAAO,WAAW;EAChB,QAAQ,WAAW;CACrB,CAAC,CACH,CACF;AACF;AA8BA,MAAa,UA5BU,QAAQ,KAC7B,oBACA;CAAE;CAAW;AAAI,GACjB,OAAO,GAAG,kBAAkB,CAAC,CAAC,WAAW,EAAE,WAAW,OAAO;CAC3D,OAAO,WAAW;EAChB,KAAK,QAAQ,IAAI;EACjB;EACA;EACA,aAAa,QAAQ,MAAM,UAAU;CACvC,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,WAAW,CAAC;AACnC,CAAC,CACH,CAAC,CAAC,KACA,QAAQ,UAAU,gBAAgB,GAClC,QAAQ,gBACN,2EACF,GACA,QAAQ,aAAa,CACnB;CACE,SAAS;CACT,aAAa;AACf,GACA;CACE,SAAS;CACT,aAAa;AACf,CACF,CAAC,CAGoB,CAAA,CAAQ,KAAK,QAAQ,IAAI,EAAWA,QAAoB,CAAC,CAAC;;;ACnDjF,MAAM,YAAY,IAAI,MAAM,KAAK,MAAM,aAAa,aAAa,KAAK,CAAC;AAEvE,QAAQ,KAAK,OAAO,QAAQ,SAAS,GAAG,YAAY,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thanaen/worktree-cleanup",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Safely discover and remove stale Git worktrees.",
5
5
  "keywords": [
6
6
  "cleanup",
@@ -38,6 +38,17 @@
38
38
  "access": "public",
39
39
  "provenance": true
40
40
  },
41
+ "scripts": {
42
+ "build": "tsdown",
43
+ "check": "pnpm format:check && pnpm diagnostics && pnpm typecheck && pnpm test && pnpm build",
44
+ "diagnostics": "effect-tsgo diagnostics --project tsconfig.json",
45
+ "format": "oxfmt --ignore-path=.oxfmtignore .",
46
+ "format:check": "oxfmt --check --ignore-path=.oxfmtignore .",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "typecheck": "tsc --noEmit -p tsconfig.json",
50
+ "prepare": "effect-tsgo patch --typescript --no-oxlint"
51
+ },
41
52
  "dependencies": {
42
53
  "@effect/platform-node": "4.0.0-rc.108",
43
54
  "effect": "4.0.0-rc.108"
@@ -54,14 +65,5 @@
54
65
  "engines": {
55
66
  "node": ">=22"
56
67
  },
57
- "scripts": {
58
- "build": "tsdown",
59
- "check": "pnpm format:check && pnpm diagnostics && pnpm typecheck && pnpm test && pnpm build",
60
- "diagnostics": "effect-tsgo diagnostics --project tsconfig.json",
61
- "format": "oxfmt --ignore-path=.oxfmtignore .",
62
- "format:check": "oxfmt --check --ignore-path=.oxfmtignore .",
63
- "test": "vitest run",
64
- "test:watch": "vitest",
65
- "typecheck": "tsc --noEmit -p tsconfig.json"
66
- }
67
- }
68
+ "packageManager": "pnpm@10.17.1"
69
+ }