(argv: list[str] | None = None)
| 265 | |
| 266 | |
| 267 | def main(argv: list[str] | None = None) -> int: |
| 268 | parser = argparse.ArgumentParser( |
| 269 | description="Clean up stale agent worktrees under .claude/worktrees/", |
| 270 | ) |
| 271 | parser.add_argument( |
| 272 | "--dry-run", |
| 273 | action="store_true", |
| 274 | help="List worktrees and their safety status without removing anything.", |
| 275 | ) |
| 276 | parser.add_argument( |
| 277 | "--all", |
| 278 | action="store_true", |
| 279 | help="Remove every worktree, including unsafe ones (dangerous).", |
| 280 | ) |
| 281 | args = parser.parse_args(argv) |
| 282 | |
| 283 | paths = list_worktree_paths() |
| 284 | if not paths: |
| 285 | print(f"No worktrees found under {WORKTREES_DIR}") |
| 286 | return 0 |
| 287 | |
| 288 | classifications: list[WorktreeStatus] = [] |
| 289 | for p in paths: |
| 290 | classifications.append(classify_worktree(p)) |
| 291 | removed = 0 |
| 292 | skipped = 0 |
| 293 | failed = 0 |
| 294 | reclaimed = 0 |
| 295 | |
| 296 | for cls in classifications: |
| 297 | size = directory_size_bytes(cls.path) |
| 298 | target = cls.safe or args.all |
| 299 | prefix = "[dry-run] " if args.dry_run else "" |
| 300 | action = "REMOVE" if target else "SKIP" |
| 301 | print( |
| 302 | f"{prefix}{action:6} {cls.path.name:40} " |
| 303 | f"({format_bytes(size):>10}) {cls.reason}" |
| 304 | ) |
| 305 | |
| 306 | if not target: |
| 307 | skipped += 1 |
| 308 | continue |
| 309 | if args.dry_run: |
| 310 | continue |
| 311 | |
| 312 | result = remove_worktree(cls.path) |
| 313 | if result.removed: |
| 314 | removed += 1 |
| 315 | reclaimed += size |
| 316 | print(f" removed via {result.method}") |
| 317 | else: |
| 318 | failed += 1 |
| 319 | print(f" FAILED: {result.error}", file=sys.stderr) |
| 320 | |
| 321 | # Always prune admin entries at the end (cheap and harmless). |
| 322 | if not args.dry_run: |
| 323 | run_git(["worktree", "prune"], cwd=PROJECT_ROOT) |
| 324 |
no test coverage detected