A monorepo management tool for the agentic ages

Add comprehensive 'unpac status' command

Shows the overall state of the workspace including:
- Main worktree status (clean/dirty)
- All projects with merge counts
- All opam packages with patch counts and merge status
- All git repos with patch counts and merge status
- Active worktrees and uncommitted changes

Use `unpac status --short` for a compact summary.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+223 -1
+223 -1
bin/main.ml
··· 1434 1434 let info = Cmd.info "vendor" ~doc in 1435 1435 Cmd.group info [vendor_status_cmd] 1436 1436 1437 + (* Status command - comprehensive workspace status *) 1438 + let status_cmd = 1439 + let doc = "Show comprehensive workspace status." in 1440 + let man = [ 1441 + `S Manpage.s_description; 1442 + `P "Shows the overall state of the unpac workspace including:"; 1443 + `I ("Projects", "All project branches and their merge status"); 1444 + `I ("Opam packages", "Vendored packages, patch counts, and merge status"); 1445 + `I ("Git repos", "Vendored git repositories and their status"); 1446 + `I ("Worktrees", "Any active worktrees with uncommitted changes"); 1447 + `S Manpage.s_examples; 1448 + `Pre " unpac status # Full status 1449 + unpac status --short # Compact summary"; 1450 + ] in 1451 + let short_flag = 1452 + let doc = "Show compact summary only." in 1453 + Arg.(value & flag & info ["s"; "short"] ~doc) 1454 + in 1455 + let run () short = 1456 + with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root -> 1457 + let git = Unpac.Worktree.git_dir root in 1458 + let main_wt = Unpac.Worktree.path root Unpac.Worktree.Main in 1459 + 1460 + (* Get all branches *) 1461 + let all_branches = Unpac.Git.run_lines ~proc_mgr ~cwd:git 1462 + ["branch"; "--format=%(refname:short)"] in 1463 + 1464 + (* Categorize branches *) 1465 + let project_branches = List.filter (fun b -> 1466 + String.starts_with ~prefix:"project/" b 1467 + ) all_branches in 1468 + let opam_packages = Unpac_opam.Opam.list_packages ~proc_mgr ~root in 1469 + let git_repos = Unpac.Git_backend.list_repos ~proc_mgr ~root in 1470 + 1471 + (* Helper to count commits between branches *) 1472 + let commit_count from_ref to_ref = 1473 + try 1474 + let output = Unpac.Git.run_exn ~proc_mgr ~cwd:git 1475 + ["rev-list"; "--count"; from_ref ^ ".." ^ to_ref] in 1476 + int_of_string (String.trim output) 1477 + with _ -> 0 1478 + in 1479 + 1480 + (* Helper to check if branch A is ancestor of B *) 1481 + let is_ancestor a b = 1482 + match Unpac.Git.run ~proc_mgr ~cwd:git 1483 + ["merge-base"; "--is-ancestor"; a; b] with 1484 + | Ok _ -> true 1485 + | Error _ -> false 1486 + in 1487 + 1488 + (* Check for uncommitted changes in a worktree *) 1489 + let has_changes wt_path = 1490 + if Sys.file_exists (snd wt_path) then 1491 + let status = Unpac.Git.run_exn ~proc_mgr ~cwd:wt_path ["status"; "--porcelain"] in 1492 + String.trim status <> "" 1493 + else false 1494 + in 1495 + 1496 + (* Project names *) 1497 + let project_names = List.map (fun b -> 1498 + String.sub b 8 (String.length b - 8) 1499 + ) project_branches in 1500 + 1501 + if short then begin 1502 + (* Short summary *) 1503 + Format.printf "Workspace: %s@." (snd (Unpac.Worktree.git_dir root) |> Filename.dirname); 1504 + Format.printf "Projects: %d | Opam: %d | Git: %d@." 1505 + (List.length project_branches) 1506 + (List.length opam_packages) 1507 + (List.length git_repos); 1508 + 1509 + (* Count total patches *) 1510 + let opam_patches = List.fold_left (fun acc pkg -> 1511 + acc + commit_count (Unpac_opam.Opam.vendor_branch pkg) (Unpac_opam.Opam.patches_branch pkg) 1512 + ) 0 opam_packages in 1513 + let git_patches = List.fold_left (fun acc repo -> 1514 + acc + commit_count (Unpac.Git_backend.vendor_branch repo) (Unpac.Git_backend.patches_branch repo) 1515 + ) 0 git_repos in 1516 + if opam_patches + git_patches > 0 then 1517 + Format.printf "Local patches: %d commits@." (opam_patches + git_patches); 1518 + 1519 + (* Check main for uncommitted *) 1520 + if has_changes main_wt then 1521 + Format.printf "Warning: Uncommitted changes in main@." 1522 + end else begin 1523 + (* Full status *) 1524 + Format.printf "=== Unpac Workspace Status ===@.@."; 1525 + 1526 + (* Main worktree status *) 1527 + Format.printf "Main worktree: %s@." (snd main_wt); 1528 + if has_changes main_wt then 1529 + Format.printf " @{<yellow>Warning: Uncommitted changes@}@." 1530 + else 1531 + Format.printf " Clean@."; 1532 + Format.printf "@."; 1533 + 1534 + (* Projects *) 1535 + Format.printf "=== Projects (%d) ===@." (List.length project_names); 1536 + if project_names = [] then 1537 + Format.printf " (none)@." 1538 + else begin 1539 + List.iter (fun proj -> 1540 + let proj_branch = "project/" ^ proj in 1541 + let proj_wt = Unpac.Worktree.path root (Unpac.Worktree.Project proj) in 1542 + let wt_exists = Sys.file_exists (snd proj_wt) in 1543 + let dirty = wt_exists && has_changes proj_wt in 1544 + 1545 + (* Count merged packages *) 1546 + let merged_opam = List.filter (fun pkg -> 1547 + is_ancestor (Unpac_opam.Opam.patches_branch pkg) proj_branch 1548 + ) opam_packages in 1549 + let merged_git = List.filter (fun repo -> 1550 + is_ancestor (Unpac.Git_backend.patches_branch repo) proj_branch 1551 + ) git_repos in 1552 + 1553 + Format.printf " %s" proj; 1554 + if wt_exists then Format.printf " [worktree]"; 1555 + if dirty then Format.printf " @{<yellow>*dirty*@}"; 1556 + Format.printf "@."; 1557 + Format.printf " Merged: %d opam, %d git@." 1558 + (List.length merged_opam) (List.length merged_git) 1559 + ) project_names 1560 + end; 1561 + Format.printf "@."; 1562 + 1563 + (* Opam packages *) 1564 + Format.printf "=== Opam Packages (%d) ===@." (List.length opam_packages); 1565 + if opam_packages = [] then 1566 + Format.printf " (none)@." 1567 + else begin 1568 + Format.printf " %-25s %8s %s@." "Package" "Patches" "Merged into"; 1569 + Format.printf " %s@." (String.make 60 '-'); 1570 + List.iter (fun pkg -> 1571 + let vendor_branch = Unpac_opam.Opam.vendor_branch pkg in 1572 + let patches_branch = Unpac_opam.Opam.patches_branch pkg in 1573 + let patch_count = commit_count vendor_branch patches_branch in 1574 + 1575 + (* Check active worktrees *) 1576 + let patches_wt = Unpac.Worktree.path root (Unpac.Worktree.Opam_patches pkg) in 1577 + let has_wt = Sys.file_exists (snd patches_wt) in 1578 + let dirty = has_wt && has_changes patches_wt in 1579 + 1580 + (* Check merged into which projects *) 1581 + let merged_into = List.filter (fun proj -> 1582 + is_ancestor patches_branch ("project/" ^ proj) 1583 + ) project_names in 1584 + 1585 + let merged_str = if merged_into = [] then "-" 1586 + else String.concat ", " merged_into in 1587 + 1588 + Format.printf " %-25s" pkg; 1589 + if has_wt then Format.printf "*" else Format.printf " "; 1590 + Format.printf "%7d %s" patch_count merged_str; 1591 + if dirty then Format.printf " @{<yellow>(uncommitted)@}"; 1592 + Format.printf "@." 1593 + ) opam_packages 1594 + end; 1595 + Format.printf "@."; 1596 + 1597 + (* Git repos *) 1598 + Format.printf "=== Git Repositories (%d) ===@." (List.length git_repos); 1599 + if git_repos = [] then 1600 + Format.printf " (none)@." 1601 + else begin 1602 + Format.printf " %-25s %8s %s@." "Repository" "Patches" "Merged into"; 1603 + Format.printf " %s@." (String.make 60 '-'); 1604 + List.iter (fun repo -> 1605 + let vendor_branch = Unpac.Git_backend.vendor_branch repo in 1606 + let patches_branch = Unpac.Git_backend.patches_branch repo in 1607 + let patch_count = commit_count vendor_branch patches_branch in 1608 + 1609 + let patches_wt = Unpac.Worktree.path root (Unpac.Worktree.Git_patches repo) in 1610 + let has_wt = Sys.file_exists (snd patches_wt) in 1611 + let dirty = has_wt && has_changes patches_wt in 1612 + 1613 + let merged_into = List.filter (fun proj -> 1614 + is_ancestor patches_branch ("project/" ^ proj) 1615 + ) project_names in 1616 + 1617 + let merged_str = if merged_into = [] then "-" 1618 + else String.concat ", " merged_into in 1619 + 1620 + Format.printf " %-25s" repo; 1621 + if has_wt then Format.printf "*" else Format.printf " "; 1622 + Format.printf "%7d %s" patch_count merged_str; 1623 + if dirty then Format.printf " @{<yellow>(uncommitted)@}"; 1624 + Format.printf "@." 1625 + ) git_repos 1626 + end; 1627 + Format.printf "@."; 1628 + 1629 + (* Active worktrees summary *) 1630 + let active_worktrees = ref [] in 1631 + List.iter (fun pkg -> 1632 + let wt = Unpac.Worktree.path root (Unpac.Worktree.Opam_patches pkg) in 1633 + if Sys.file_exists (snd wt) then 1634 + active_worktrees := ("opam/" ^ pkg ^ "-patches", has_changes wt) :: !active_worktrees 1635 + ) opam_packages; 1636 + List.iter (fun repo -> 1637 + let wt = Unpac.Worktree.path root (Unpac.Worktree.Git_patches repo) in 1638 + if Sys.file_exists (snd wt) then 1639 + active_worktrees := ("git/" ^ repo ^ "-patches", has_changes wt) :: !active_worktrees 1640 + ) git_repos; 1641 + 1642 + if !active_worktrees <> [] then begin 1643 + Format.printf "=== Active Worktrees ===@."; 1644 + List.iter (fun (name, dirty) -> 1645 + Format.printf " %s" name; 1646 + if dirty then Format.printf " @{<yellow>*uncommitted*@}"; 1647 + Format.printf "@." 1648 + ) (List.rev !active_worktrees); 1649 + Format.printf "@." 1650 + end; 1651 + 1652 + (* Legend *) 1653 + Format.printf "Legend: * = worktree active@." 1654 + end 1655 + in 1656 + let info = Cmd.info "status" ~doc ~man in 1657 + Cmd.v info Term.(const run $ logging_term $ short_flag) 1658 + 1437 1659 (* Main command *) 1438 1660 let main_cmd = 1439 1661 let doc = "Multi-backend vendoring tool using git worktrees." in ··· 1462 1684 `S "COMMANDS"; 1463 1685 ] in 1464 1686 let info = Cmd.info "unpac" ~version:"0.1.0" ~doc ~man in 1465 - Cmd.group info [init_cmd; project_cmd; opam_cmd; git_cmd; vendor_cmd; push_cmd; log_cmd] 1687 + Cmd.group info [init_cmd; status_cmd; project_cmd; opam_cmd; git_cmd; vendor_cmd; push_cmd; log_cmd] 1466 1688 1467 1689 let () = exit (Cmd.eval main_cmd)