Monorepo management for opam overlays
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve monopam CLI with hints, skip options, and consolidate sync

- Add helpful hints to error messages via pp_error_with_hint functions
for both Monopam and Verse modules, guiding users to next steps
- Remove separate pull and push commands, consolidating into sync
- Add --skip-push and --skip-pull options to sync for granular control
- Update sync to write CLAUDE.md with usage tips during finalize phase
- Improve CLI documentation with better examples and status explanations
- Update CLAUDE.md content template to include quick reference table,
status indicator explanations, and CLAUDE.local.md hint

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

+427 -291
+146 -166
bin/main.ml
··· 49 49 [ 50 50 `S Manpage.s_description; 51 51 `P 52 - "Displays package status and verse fork analysis in a dense format."; 53 - `S "FORK SYMBOLS"; 54 - `I ("+N", "They have N commits you don't (consider pulling)"); 55 - `I ("-N", "You have N commits they don't"); 56 - `I ("+N/-M", "Diverged: they +N, you +M"); 57 - `I ("=", "In sync (same URL or same commit)"); 52 + "Displays package status showing both local sync state (monorepo vs \ 53 + checkout) and remote sync state (checkout vs upstream)."; 54 + `S "STATUS COLUMNS"; 55 + `P "Each repository shows two sync indicators:"; 56 + `I ("local:", "Sync between your monorepo (mono/) and checkout (src/)"); 57 + `I ("remote:", "Sync between your checkout (src/) and upstream git remote"); 58 + `S "LOCAL SYNC INDICATORS"; 59 + `I ("local:=", "Monorepo and checkout are in sync"); 60 + `I ("local:+N", "Monorepo has N commits not yet in checkout (run $(b,monopam sync))"); 61 + `I ("local:-N", "Checkout has N commits not yet in monorepo (run $(b,monopam sync))"); 62 + `I ("local:sync", "Trees differ, needs sync (run $(b,monopam sync))"); 63 + `S "REMOTE SYNC INDICATORS"; 64 + `I ("remote:=", "Checkout and upstream remote are in sync"); 65 + `I ("remote:+N", "Checkout has N commits to push (run $(b,monopam sync --remote))"); 66 + `I ("remote:-N", "Upstream has N commits to pull (run $(b,monopam sync))"); 67 + `I ("remote:+N/-M", "Diverged: checkout +N ahead, upstream +M ahead"); 68 + `S "FORK ANALYSIS"; 69 + `P "If tracking other members via verse, shows fork comparison:"; 70 + `I ("+N", "They have N commits you don't have"); 71 + `I ("-N", "You have N commits they don't have"); 72 + `I ("=", "Same commit or same URL"); 58 73 `I ("~", "Not in your workspace (use --all to list)"); 74 + `S "NEXT STEPS"; 75 + `P "Based on the status output:"; 76 + `I ("local:+N or local:-N", "Run $(b,monopam sync) to synchronize"); 77 + `I ("remote:-N", "Run $(b,monopam sync) to pull upstream changes"); 78 + `I ("remote:+N", "Run $(b,monopam sync --remote) to push to upstream"); 59 79 ] 60 80 in 61 81 let info = Cmd.info "status" ~doc ~man in ··· 104 124 Fmt.pr "%a" (Monopam.Forks.pp_summary' ~show_all) forks); 105 125 `Ok () 106 126 | Error e -> 107 - Fmt.epr "Error: %a@." Monopam.pp_error e; 127 + Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 108 128 `Error (false, "status failed") 109 129 in 110 130 Cmd.v info Term.(ret (const run $ all_arg $ logging_term)) 111 131 112 - (* Pull command *) 113 - 114 - let pull_cmd = 115 - let doc = "Pull updates from remotes into monorepo" in 116 - let man = 117 - [ 118 - `S Manpage.s_description; 119 - `P 120 - "Fetches the latest changes from git remotes and updates both the \ 121 - individual checkouts and the monorepo subtrees."; 122 - `P "For each unique repository:"; 123 - `I 124 - ( "1.", 125 - "Clones the repository if not present, or fetches and fast-forward \ 126 - merges" ); 127 - `I ("2.", "Adds or pulls the git subtree into the monorepo"); 128 - `P 129 - "If the opam-repo doesn't exist locally, it will be cloned from the \ 130 - URL registered for your account in the opamverse registry."; 131 - `P 132 - "If a specific package is given, only that package's repository is \ 133 - processed."; 134 - `P "The operation will fail if any checkout has uncommitted changes."; 135 - ] 136 - in 137 - let info = Cmd.info "pull" ~doc ~man in 138 - let run package () = 139 - Eio_main.run @@ fun env -> 140 - with_config env @@ fun config -> 141 - let fs = Eio.Stdenv.fs env in 142 - let proc = Eio.Stdenv.process_mgr env in 143 - (* Look up opam-repo URL from registry using verse config *) 144 - let opam_repo_url = 145 - match Monopam.Verse_config.load ~fs () with 146 - | Error _ -> None 147 - | Ok verse_config -> 148 - let handle = Monopam.Verse_config.handle verse_config in 149 - match Monopam.Verse_registry.clone_or_pull ~proc ~fs ~config:verse_config () with 150 - | Error _ -> None 151 - | Ok registry -> 152 - match Monopam.Verse_registry.find_member registry ~handle with 153 - | None -> None 154 - | Some member -> Some member.opamrepo 155 - in 156 - match Monopam.pull ~proc ~fs ~config ?package ?opam_repo_url () with 157 - | Ok () -> 158 - Fmt.pr "Pull completed.@."; 159 - `Ok () 160 - | Error e -> 161 - Fmt.epr "Error: %a@." Monopam.pp_error e; 162 - `Error (false, "pull failed") 163 - in 164 - Cmd.v info 165 - Term.(ret (const run $ package_arg $ logging_term)) 166 - 167 - (* Push command *) 168 - 169 - let push_cmd = 170 - let doc = "Push changes from monorepo to checkouts" in 171 - let man = 172 - [ 173 - `S Manpage.s_description; 174 - `P 175 - "Extracts changes made in the monorepo and merges them into the \ 176 - individual git checkouts using git subtree split."; 177 - `P "For each unique repository:"; 178 - `I ("1.", "Splits the subtree commits from the monorepo"); 179 - `I ("2.", "Fast-forward merges the split commits into the checkout"); 180 - `I 181 - ( "3.", 182 - "If --upstream is specified, pushes each checkout to its git remote" 183 - ); 184 - `P 185 - "Without --upstream, you can review the changes in each checkout and \ 186 - manually push them to the git remotes."; 187 - `P "The operation will fail if any checkout has uncommitted changes."; 188 - ] 189 - in 190 - let info = Cmd.info "push" ~doc ~man in 191 - let upstream_arg = 192 - let doc = 193 - "Also push each checkout to its upstream git remote after extracting \ 194 - changes." 195 - in 196 - Arg.(value & flag & info [ "upstream" ] ~doc) 197 - in 198 - let run package upstream () = 199 - Eio_main.run @@ fun env -> 200 - with_config env @@ fun config -> 201 - let fs = Eio.Stdenv.fs env in 202 - let proc = Eio.Stdenv.process_mgr env in 203 - match Monopam.push ~proc ~fs ~config ?package ~upstream () with 204 - | Ok () -> 205 - Fmt.pr "Push completed.@."; 206 - `Ok () 207 - | Error e -> 208 - Fmt.epr "Error: %a@." Monopam.pp_error e; 209 - `Error (false, "push failed") 210 - in 211 - Cmd.v info 212 - Term.( 213 - ret (const run $ package_arg $ upstream_arg $ logging_term)) 214 - 215 132 (* Sync command *) 216 133 217 134 let sync_cmd = ··· 220 137 [ 221 138 `S Manpage.s_description; 222 139 `P 223 - "Performs both push and pull operations in the correct order to fully \ 224 - synchronize the monorepo with upstream repositories."; 225 - `P "The sync command executes the following phases:"; 226 - `I ("1. Validate", "Check for dirty state (abort if dirty)"); 227 - `I ("2. Push", "Export monorepo changes to checkouts (sequential)"); 228 - `I ("3. Fetch", "Clone/fetch from remotes (parallel)"); 229 - `I ("4. Merge", "Fast-forward merge checkouts (sequential)"); 230 - `I ("5. Subtree", "Pull subtrees into monorepo (sequential)"); 231 - `I ("6. Finalize", "Write README.md and dune-project (sequential)"); 140 + "$(b,This is the primary command for all workflows.) It performs both \ 141 + push and pull operations in the correct order to fully synchronize \ 142 + your monorepo with upstream repositories."; 143 + `S "COMMON USAGE"; 144 + `I ("monopam sync", "Full sync: push local changes + pull remote changes"); 145 + `I ("monopam sync --remote", "Full sync + push to upstream git remotes"); 146 + `I ("monopam sync eio", "Sync only the eio repository"); 147 + `I ("monopam sync --skip-push", "Pull only: skip exporting local changes"); 148 + `I ("monopam sync --skip-pull", "Push only: skip fetching remote changes"); 149 + `S Manpage.s_examples; 150 + `P "After making changes:"; 151 + `Pre 152 + "cd mono\n\ 153 + # ... edit files ...\n\ 154 + git add -A && git commit -m \"Add feature\"\n\ 155 + monopam sync --remote # sync and push upstream"; 156 + `P "Pull latest from all upstreams (no local changes to export):"; 157 + `Pre "monopam sync --skip-push"; 158 + `P "Export local changes for review without pulling:"; 159 + `Pre "monopam sync --skip-pull"; 160 + `S "PHASES"; 161 + `P "The sync command executes these phases in order:"; 162 + `I ("1. Validate", "Abort if the monorepo has uncommitted changes"); 163 + `I ("2. Push", "Export monorepo changes to checkouts (parallel) [--skip-push skips]"); 164 + `I ("3. Fetch", "Clone/fetch from remotes (parallel) [--skip-pull skips]"); 165 + `I ("4. Merge", "Fast-forward merge checkouts [--skip-pull skips]"); 166 + `I ("5. Subtree", "Pull subtrees into monorepo [--skip-pull skips]"); 167 + `I ("6. Finalize", "Update README.md, CLAUDE.md, and dune-project"); 232 168 `I ("7. Remote", "Push to upstream remotes if --remote (parallel)"); 233 - `P 234 - "The fetch and remote push phases run concurrently across repositories \ 235 - for improved performance on network-bound operations."; 236 - `P 237 - "If a specific package is given, only that package's repository is \ 238 - processed."; 239 - `P "The operation will fail if any checkout has uncommitted changes."; 240 - `S "WHY PUSH BEFORE PULL"; 241 - `P 242 - "Local monorepo changes must be exported to checkouts before fetching \ 243 - remote changes, otherwise local work could be lost during merge."; 169 + `S "SKIP OPTIONS"; 170 + `I ("--skip-push", "Skip exporting monorepo changes to checkouts. Use when \ 171 + you know you have no local changes to export."); 172 + `I ("--skip-pull", "Skip fetching and pulling from remotes. Use when you \ 173 + only want to export local changes without pulling remote updates."); 174 + `S "PREREQUISITES"; 175 + `P "Before running sync:"; 176 + `I ("-", "Commit all changes in the monorepo: $(b,git add -A && git commit)"); 177 + `I ("-", "For --remote: ensure git credentials/SSH keys are configured"); 244 178 ] 245 179 in 246 180 let info = Cmd.info "sync" ~doc ~man in ··· 250 184 in 251 185 Arg.(value & flag & info [ "remote" ] ~doc) 252 186 in 253 - let run package remote () = 187 + let skip_push_arg = 188 + let doc = "Skip exporting monorepo changes to checkouts." in 189 + Arg.(value & flag & info [ "skip-push" ] ~doc) 190 + in 191 + let skip_pull_arg = 192 + let doc = "Skip fetching and pulling from remotes." in 193 + Arg.(value & flag & info [ "skip-pull" ] ~doc) 194 + in 195 + let run package remote skip_push skip_pull () = 254 196 Eio_main.run @@ fun env -> 255 197 with_config env @@ fun config -> 256 198 let fs = Eio.Stdenv.fs env in 257 199 let proc = Eio.Stdenv.process_mgr env in 258 - match Monopam.sync ~proc ~fs ~config ?package ~remote () with 200 + match Monopam.sync ~proc ~fs ~config ?package ~remote ~skip_push ~skip_pull () with 259 201 | Ok summary -> 260 202 if summary.errors = [] then 261 203 `Ok () ··· 264 206 `Ok () 265 207 end 266 208 | Error e -> 267 - Fmt.epr "Error: %a@." Monopam.pp_error e; 209 + Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 268 210 `Error (false, "sync failed") 269 211 in 270 212 Cmd.v info 271 - Term.(ret (const run $ package_arg $ remote_arg $ logging_term)) 213 + Term.(ret (const run $ package_arg $ remote_arg $ skip_push_arg $ skip_pull_arg $ logging_term)) 272 214 273 215 (* Add command *) 274 216 ··· 298 240 Fmt.pr "Added %s to monorepo.@." package; 299 241 `Ok () 300 242 | Error e -> 301 - Fmt.epr "Error: %a@." Monopam.pp_error e; 243 + Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 302 244 `Error (false, "add failed") 303 245 in 304 246 Cmd.v info ··· 332 274 Fmt.pr "Removed %s from monorepo.@." package; 333 275 `Ok () 334 276 | Error e -> 335 - Fmt.epr "Error: %a@." Monopam.pp_error e; 277 + Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 336 278 `Error (false, "remove failed") 337 279 in 338 280 Cmd.v info ··· 426 368 else Fmt.pr "Weekly changelog updated.@."; 427 369 `Ok () 428 370 | Error e -> 429 - Fmt.epr "Error: %a@." Monopam.pp_error e; 371 + Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 430 372 `Error (false, "changes failed") 431 373 in 432 374 Cmd.v info ··· 530 472 Fmt.pr "Monoverse workspace initialized at %a@." Fpath.pp root; 531 473 `Ok () 532 474 | Error e -> 533 - Fmt.epr "Error: %a@." Monopam.Verse.pp_error e; 475 + Fmt.epr "Error: %a@." Monopam.Verse.pp_error_with_hint e; 534 476 `Error (false, "init failed") 535 477 in 536 478 Cmd.v info Term.(ret (const run $ verse_root_arg $ verse_handle_arg $ logging_term)) ··· 573 515 Fmt.pr "%a@." Monopam.Verse.pp_status status; 574 516 `Ok () 575 517 | Error e -> 576 - Fmt.epr "Error: %a@." Monopam.Verse.pp_error e; 518 + Fmt.epr "Error: %a@." Monopam.Verse.pp_error_with_hint e; 577 519 `Error (false, "status failed") 578 520 in 579 521 Cmd.v info Term.(ret (const run $ logging_term)) ··· 624 566 members; 625 567 `Ok () 626 568 | Error e -> 627 - Fmt.epr "Error: %a@." Monopam.Verse.pp_error e; 569 + Fmt.epr "Error: %a@." Monopam.Verse.pp_error_with_hint e; 628 570 `Error (false, "members failed") 629 571 in 630 572 Cmd.v info Term.(ret (const run $ logging_term)) ··· 668 610 Fmt.pr "Sync completed.@."; 669 611 `Ok () 670 612 | Error e -> 671 - Fmt.epr "Error: %a@." Monopam.Verse.pp_error e; 613 + Fmt.epr "Error: %a@." Monopam.Verse.pp_error_with_hint e; 672 614 `Error (false, "pull failed") 673 615 in 674 616 Cmd.v info Term.(ret (const run $ verse_handle_opt_pos_arg $ logging_term)) ··· 712 654 Fmt.pr "Sync completed.@."; 713 655 `Ok () 714 656 | Error e -> 715 - Fmt.epr "Error: %a@." Monopam.Verse.pp_error e; 657 + Fmt.epr "Error: %a@." Monopam.Verse.pp_error_with_hint e; 716 658 `Error (false, "sync failed") 717 659 in 718 660 Cmd.v info Term.(ret (const run $ logging_term)) ··· 837 779 `P 838 780 "Monopam synchronizes packages between an opam overlay repository, \ 839 781 individual git checkouts, and a monorepo using git subtrees."; 782 + `S "QUICK START"; 783 + `P "First time setup:"; 784 + `Pre 785 + "mkdir ~/tangled && cd ~/tangled\n\ 786 + monopam verse init --handle yourname.bsky.social\n\ 787 + cd mono"; 788 + `P "Daily workflow:"; 789 + `Pre 790 + "cd ~/tangled/mono\n\ 791 + monopam sync # sync local and remote (most common)\n\ 792 + # ... make edits ...\n\ 793 + git add -A && git commit # commit your changes\n\ 794 + monopam sync --remote # sync and push to upstream"; 840 795 `S "DIRECTORY STRUCTURE"; 841 796 `P "Monopam manages three directory trees:"; 842 797 `I 843 - ( "opam-repo/", 844 - "The opam overlay repository containing package metadata. Each \ 845 - package's opam file specifies a dev-repo URL pointing to its git \ 846 - source." ); 798 + ( "mono/", 799 + "The monorepo combining all packages as git subtrees. This is where you \ 800 + make changes." ); 847 801 `I 848 802 ( "src/", 849 - "Individual git checkouts of each unique repository. Multiple \ 850 - packages may share a checkout if they come from the same dev-repo. \ 851 - Directory names are the repository basename (e.g., ocaml-yaml from \ 852 - https://github.com/foo/ocaml-yaml.git)." ); 803 + "Individual git checkouts of each unique repository. Used for review \ 804 + and manual operations." ); 853 805 `I 854 - ( "mono/", 855 - "The monorepo combining all packages as git subtrees. Each subtree \ 856 - directory is named after the repository basename. This is where you \ 857 - make changes that span multiple packages." ); 806 + ( "opam-repo/", 807 + "The opam overlay repository containing package metadata." ); 858 808 `S "WORKFLOW"; 859 - `P "The typical workflow is:"; 809 + `P "The recommended workflow uses $(b,sync) as the primary command:"; 860 810 `I 861 - ( "1. monopam pull", 862 - "Fetch latest from all remotes, update checkouts, merge into \ 863 - monorepo subtrees" ); 811 + ( "1. monopam sync", 812 + "Synchronize your monorepo with all upstream repos. This both \ 813 + exports your local changes to checkouts AND pulls remote changes." ); 864 814 `I ("2. Edit code", "Make changes in the mono/ directory"); 865 815 `I ("3. git commit", "Commit your changes in mono/"); 866 - `I ("4. monopam push", "Extract changes back to individual checkouts"); 867 816 `I 868 - ( "5. Review and push", 869 - "Review changes in src/*/, then git push each one" ); 817 + ( "4. monopam sync --remote", 818 + "Sync again, including pushing to upstream git remotes" ); 819 + `P 820 + "For finer control, use $(b,push) and $(b,pull) separately:"; 821 + `I 822 + ( "monopam push", 823 + "Export monorepo changes to checkouts (for manual review/push)" ); 824 + `I 825 + ( "monopam pull", 826 + "Pull remote changes into monorepo (when you know there are no local changes)" ); 827 + `S "CHECKING STATUS"; 828 + `P "Run $(b,monopam status) to see the state of all repositories:"; 829 + `I ("local:+N", "Your monorepo is N commits ahead of the checkout"); 830 + `I ("local:-N", "The checkout is N commits ahead of your monorepo"); 831 + `I ("local:sync", "Trees differ but need syncing (run $(b,monopam sync))"); 832 + `I ("remote:+N", "Your checkout is N commits ahead of upstream"); 833 + `I ("remote:-N", "Upstream is N commits ahead (run $(b,monopam sync))"); 834 + `S "COMMON TASKS"; 835 + `I ("Start fresh", "monopam verse init --handle you.bsky.social"); 836 + `I ("Check status", "monopam status"); 837 + `I ("Sync everything", "monopam sync"); 838 + `I ("Sync and push upstream", "monopam sync --remote"); 839 + `I ("Add a new package", "monopam add <package-name>"); 840 + `I ("Sync one package", "monopam sync <package-name>"); 870 841 `S "CONFIGURATION"; 871 842 `P 872 843 "Run $(b,monopam verse init --handle <handle>) to create a workspace. \ 873 - Configuration is stored in ~/.config/monopam/opamverse.toml and \ 874 - all paths are derived from the workspace root."; 844 + Configuration is stored in ~/.config/monopam/opamverse.toml."; 875 845 `P "Workspace structure:"; 876 846 `Pre 877 847 "root/\n\ 878 - ├── mono/ # Your monorepo\n\ 879 - ├── src/ # Git checkouts\n\ 848 + ├── mono/ # Your monorepo (work here)\n\ 849 + ├── src/ # Git checkouts (for review)\n\ 880 850 ├── opam-repo/ # Opam overlay\n\ 881 851 └── verse/ # Other members' monorepos"; 852 + `S "TROUBLESHOOTING"; 853 + `I 854 + ( "\"Dirty packages\" error", 855 + "You have uncommitted changes. Run: cd mono && git status" ); 856 + `I 857 + ( "\"local:sync\" in status", 858 + "The monorepo and checkout are out of sync. Run: monopam sync" ); 859 + `I 860 + ( "Merge conflicts", 861 + "Resolve conflicts in mono/, commit, then run: monopam sync" ); 882 862 `S Manpage.s_commands; 883 863 `P "Use $(b,monopam COMMAND --help) for help on a specific command."; 884 864 ] 885 865 in 886 866 let info = Cmd.info "monopam" ~version:"%%VERSION%%" ~doc ~man in 887 867 Cmd.group info 888 - [ status_cmd; pull_cmd; push_cmd; sync_cmd; add_cmd; remove_cmd; changes_cmd; verse_cmd ] 868 + [ status_cmd; sync_cmd; add_cmd; remove_cmd; changes_cmd; verse_cmd ] 889 869 890 870 let () = exit (Cmd.eval main_cmd)
+234 -120
lib/monopam.ml
··· 33 33 | Package_not_found name -> Fmt.pf ppf "Package not found: %s" name 34 34 | Claude_error msg -> Fmt.pf ppf "Claude error: %s" msg 35 35 36 + (** Returns a hint string for the given error, or None if no hint is available. *) 37 + let error_hint = function 38 + | Config_error _ -> 39 + Some "Run 'monopam verse init --handle <your-handle>' to create a workspace." 40 + | Repo_error (Opam_repo.No_dev_repo _) -> 41 + Some "Add a 'dev-repo' field to the package's opam file pointing to a git URL." 42 + | Repo_error (Opam_repo.Not_git_remote _) -> 43 + Some "The dev-repo must be a git URL (git+https:// or git://)." 44 + | Repo_error _ -> None 45 + | Git_error (Git.Dirty_worktree _) -> 46 + Some "Commit or stash your changes first: cd <repo> && git status" 47 + | Git_error (Git.Not_a_repo _) -> 48 + Some "Run 'monopam sync' to clone missing repositories." 49 + | Git_error (Git.Subtree_prefix_missing _) -> 50 + Some "Run 'monopam sync' to set up the subtree." 51 + | Git_error (Git.Remote_not_found _) -> 52 + Some "Check that the remote is configured: git remote -v" 53 + | Git_error (Git.Branch_not_found _) -> 54 + Some "Check available branches: git branch -a" 55 + | Git_error (Git.Command_failed (cmd, _)) when String.starts_with ~prefix:"git push" cmd -> 56 + Some "Check your network connection and git credentials." 57 + | Git_error (Git.Command_failed (cmd, _)) when String.starts_with ~prefix:"git subtree" cmd -> 58 + Some "Run 'monopam status' to check repository state." 59 + | Git_error _ -> None 60 + | Dirty_state _ -> 61 + Some "Commit changes in the monorepo first: cd mono && git add -A && git commit" 62 + | Package_not_found _ -> 63 + Some "Check available packages: ls opam-repo/packages/" 64 + | Claude_error msg when String.starts_with ~prefix:"Failed to decode" msg -> 65 + Some "The Claude API may have returned an unexpected response. Try again." 66 + | Claude_error _ -> 67 + Some "Check ANTHROPIC_API_KEY is set. See: https://console.anthropic.com/" 68 + 69 + (** Pretty-print an error with an optional hint for next steps. *) 70 + let pp_error_with_hint ppf e = 71 + pp_error ppf e; 72 + match error_hint e with 73 + | Some hint -> Fmt.pf ppf "@.@[<v 2>Hint: %s@]" hint 74 + | None -> () 75 + 36 76 let fs_typed (fs : _ Eio.Path.t) : Eio.Fs.dir_ty Eio.Path.t = 37 77 let dir, _ = fs in 38 78 (dir, "") ··· 221 261 This is a monorepo managed by `monopam`. Each subdirectory is a git subtree 222 262 from a separate upstream repository. 223 263 224 - ## Making Changes 264 + > **Note:** Check `CLAUDE.local.md` (if it exists) for additional local 265 + > configuration or preferences specific to this workspace. 266 + 267 + ## Quick Reference 225 268 226 - 1. Edit code in any subdirectory as normal 227 - 2. Build and test: `opam exec -- dune build` and `opam exec -- dune test` 228 - 3. Commit your changes to this monorepo with git 269 + | Task | Command | 270 + |------|---------| 271 + | Check status | `monopam status` | 272 + | Sync all repos | `monopam sync` | 273 + | Sync and push upstream | `monopam sync --remote` | 274 + | Sync one repo | `monopam sync <repo-name>` | 275 + | Build | `opam exec -- dune build` | 276 + | Test | `opam exec -- dune test` | 229 277 230 - ## Synchronizing with Upstream 278 + ## Daily Workflow 231 279 232 - The recommended way to keep your monorepo in sync is: 280 + ```bash 281 + # 1. Check what needs syncing 282 + monopam status 233 283 234 - ``` 284 + # 2. Sync your monorepo with all upstreams 235 285 monopam sync 236 - ``` 237 286 238 - This performs push and pull in the correct order: 239 - 1. Exports your monorepo changes to checkouts 240 - 2. Fetches latest from all remotes (in parallel) 241 - 3. Merges and updates subtrees 287 + # 3. Make your changes, build and test 288 + opam exec -- dune build && opam exec -- dune test 242 289 243 - To also push to upstream git remotes: 290 + # 4. Commit your changes 291 + git add -A && git commit -m "Description of changes" 244 292 245 - ``` 293 + # 5. Sync and push to upstream remotes 246 294 monopam sync --remote 247 295 ``` 248 296 249 - ## Manual Push/Pull 297 + ## Understanding Status Output 250 298 251 - For finer control, you can use the individual commands: 299 + Run `monopam status` to see the sync state: 252 300 253 - ### Exporting Changes to Upstream 301 + - `local:=` - Monorepo and checkout in sync 302 + - `local:+N` - Monorepo is N commits ahead (run `monopam sync`) 303 + - `local:-N` - Checkout is N commits ahead (run `monopam sync`) 304 + - `local:sync` - Trees differ, needs sync (run `monopam sync`) 305 + - `remote:=` - Checkout and upstream in sync 306 + - `remote:+N` - You have N commits to push (run `monopam sync --remote`) 307 + - `remote:-N` - Upstream has N commits to pull (run `monopam sync`) 308 + 309 + ## Making Changes 254 310 255 - ``` 256 - monopam push 257 - ``` 311 + 1. **Edit code** in any subdirectory as normal 312 + 2. **Build and test**: `opam exec -- dune build && opam exec -- dune test` 313 + 3. **Commit** your changes: `git add -A && git commit` 314 + 4. **Sync**: `monopam sync --remote` to push to upstreams 258 315 259 - This extracts your commits into the individual checkouts in `../src/`. 260 - You then review and push each one manually: 316 + ## Important Notes 261 317 262 - ``` 263 - cd ../src/<repo-name> 264 - git log --oneline -5 # review the changes 265 - git push origin main # push to upstream 266 - ``` 318 + - **Always commit before sync**: `monopam sync` only exports committed changes 319 + - **Check status first**: Run `monopam status` to see what needs attention 320 + - **One repo per directory**: Each subdirectory maps to exactly one git remote 267 321 268 - ### Pulling Updates from Upstream 322 + ## Troubleshooting 269 323 324 + ### "Dirty packages" Error 325 + Commit your changes first: 326 + ```bash 327 + git status && git add -A && git commit -m "Your message" 270 328 ``` 271 - monopam pull 329 + 330 + ### "local:sync" in Status 331 + Trees differ but need syncing: 332 + ```bash 333 + monopam sync 272 334 ``` 273 335 274 - This updates both the checkouts and merges changes into this monorepo. 336 + ### Merge Conflicts 337 + Resolve conflicts, commit, then sync: 338 + ```bash 339 + git add -A && git commit -m "Resolve merge conflicts" 340 + monopam sync 341 + ``` 275 342 276 - ## Important Notes 343 + ### Push Fails 344 + Check credentials: 345 + ```bash 346 + cd ../src/<repo-name> 347 + git push origin main # For better error messages 348 + ``` 277 349 278 - - **Always commit before sync/push**: These commands only export committed changes 279 - - **Check status first**: Run `monopam status` to see which repos have changes 280 - - **One repo per directory**: Each subdirectory maps to exactly one git remote 281 - - **Shared repos**: Multiple opam packages may live in the same subdirectory 282 - if they share an upstream repository 350 + ## Getting Help 283 351 284 - ## Troubleshooting 285 - 286 - If `monopam sync` or `monopam push` fails with "dirty state", you have 287 - uncommitted changes. Commit or stash them first. 288 - 289 - If merge conflicts occur during sync/pull, resolve them in this monorepo, 290 - commit, then the next sync will succeed. 352 + ```bash 353 + monopam --help # Main help 354 + monopam sync --help # Sync command help 355 + monopam status --help # Status command help 356 + ``` 291 357 |} 292 358 293 359 let gitignore_content = {|_build ··· 515 581 Log.app (fun m -> m "Updated README.md with %d packages" (List.length pkgs)) 516 582 end 517 583 584 + (* Write CLAUDE.md to monorepo with usage tips *) 585 + let write_claude_md ~proc ~fs ~config = 586 + let monorepo = Config.Paths.monorepo config in 587 + let monorepo_eio = Eio.Path.(fs / Fpath.to_string monorepo) in 588 + let claude_path = Eio.Path.(monorepo_eio / "CLAUDE.md") in 589 + (* Check if CLAUDE.md already exists with same content *) 590 + let needs_update = 591 + match Eio.Path.load claude_path with 592 + | existing -> existing <> claude_md_content 593 + | exception Eio.Io _ -> true 594 + in 595 + if needs_update then begin 596 + Log.info (fun m -> m "Updating CLAUDE.md in monorepo"); 597 + Eio.Path.save ~create:(`Or_truncate 0o644) claude_path claude_md_content; 598 + (* Stage and commit the CLAUDE.md *) 599 + Eio.Switch.run (fun sw -> 600 + let child = 601 + Eio.Process.spawn proc ~sw ~cwd:monorepo_eio 602 + [ "git"; "add"; "CLAUDE.md" ] 603 + in 604 + ignore (Eio.Process.await child)); 605 + Eio.Switch.run (fun sw -> 606 + let child = 607 + Eio.Process.spawn proc ~sw ~cwd:monorepo_eio 608 + [ "git"; "commit"; "-m"; "Update CLAUDE.md usage tips" ] 609 + in 610 + ignore (Eio.Process.await child)); 611 + Log.app (fun m -> m "Updated CLAUDE.md") 612 + end 613 + 518 614 (** Convert a clone URL to a push URL. 519 615 - GitHub HTTPS URLs are converted to SSH format 520 616 - Tangled URLs (tangled.org) are converted to git.recoil.org SSH format ··· 787 883 else if unchanged > 0 then 788 884 Log.app (fun m -> 789 885 m "%d repositories unchanged." unchanged); 790 - (* Update README.md with package summary *) 886 + (* Update README.md, CLAUDE.md, and dune-project *) 791 887 write_readme ~proc ~fs:fs_t ~config all_pkgs; 792 - (* Update dune-project with external dependencies *) 888 + write_claude_md ~proc ~fs:fs_t ~config; 793 889 write_dune_project ~proc ~fs:fs_t ~config all_pkgs; 794 890 Ok ()) 795 891 end ··· 1054 1150 Log.warn (fun m -> m "Failed to set push URL: %a" Git.pp_error e)); 1055 1151 Git.push_remote ~proc ~fs ~branch checkout_dir 1056 1152 1057 - let sync ~proc ~fs ~config ?package ?(remote = false) () = 1153 + let sync ~proc ~fs ~config ?package ?(remote = false) ?(skip_push = false) ?(skip_pull = false) () = 1058 1154 let fs_t = fs_typed fs in 1059 1155 (* Update the opam repo first - clone if needed *) 1060 1156 let opam_repo = Config.Paths.opam_repo config in 1061 - if Git.is_repo ~proc ~fs:fs_t opam_repo then begin 1157 + if (not skip_pull) && Git.is_repo ~proc ~fs:fs_t opam_repo then begin 1062 1158 Log.info (fun m -> m "Updating opam repo at %a" Fpath.pp opam_repo); 1063 1159 let result = 1064 1160 let ( let* ) = Result.bind in ··· 1102 1198 1103 1199 (* Step 2: Push phase - export monorepo changes to checkouts (PARALLEL) *) 1104 1200 (* git subtree push is read-only on the monorepo, so safe to parallelize *) 1105 - Log.app (fun m -> m " Pushing monorepo changes to checkouts (parallel)..."); 1106 - let push_results = Eio.Fiber.List.map (fun pkg -> 1107 - let repo_name = Package.repo_name pkg in 1108 - Log.info (fun m -> m "Push to checkout: %s" repo_name); 1109 - match push_one ~proc ~fs ~config pkg with 1110 - | Ok () -> Ok repo_name 1111 - | Error (Git_error e) -> 1112 - Error { repo_name; phase = `Push_checkout; error = e } 1113 - | Error _ -> Ok repo_name) 1114 - repos 1201 + let push_results = 1202 + if skip_push then begin 1203 + Log.app (fun m -> m " Skipping push to checkouts (--skip-push)"); 1204 + List.map (fun pkg -> Ok (Package.repo_name pkg)) repos 1205 + end 1206 + else begin 1207 + Log.app (fun m -> m " Pushing monorepo changes to checkouts (parallel)..."); 1208 + Eio.Fiber.List.map (fun pkg -> 1209 + let repo_name = Package.repo_name pkg in 1210 + Log.info (fun m -> m "Push to checkout: %s" repo_name); 1211 + match push_one ~proc ~fs ~config pkg with 1212 + | Ok () -> Ok repo_name 1213 + | Error (Git_error e) -> 1214 + Error { repo_name; phase = `Push_checkout; error = e } 1215 + | Error _ -> Ok repo_name) 1216 + repos 1217 + end 1115 1218 in 1116 1219 let push_errors = 1117 1220 List.filter_map (function Error e -> Some e | Ok _ -> None) push_results 1118 1221 in 1119 1222 1120 - (* Step 3: Fetch phase - clone/fetch from remotes (PARALLEL) *) 1121 - Log.app (fun m -> m " Fetching from remotes (parallel)..."); 1122 - let fetch_results = Eio.Fiber.List.map (fun pkg -> 1123 - let repo_name = Package.repo_name pkg in 1124 - (* First ensure checkout exists *) 1125 - match ensure_checkout_safe ~proc ~fs:fs_t ~config pkg with 1126 - | Error e -> Error { repo_name; phase = `Fetch; error = e } 1127 - | Ok (was_cloned, _) -> 1128 - if was_cloned then Ok (repo_name, true, 0) 1129 - else 1130 - match fetch_checkout_safe ~proc ~fs:fs_t ~config pkg with 1131 - | Error e -> Error { repo_name; phase = `Fetch; error = e } 1132 - | Ok commits -> Ok (repo_name, false, commits)) 1133 - repos 1134 - in 1135 - let fetch_errors, fetch_successes = 1136 - List.partition_map (function 1137 - | Error e -> Left e 1138 - | Ok r -> Right r) 1139 - fetch_results 1140 - in 1141 - let cloned = List.filter (fun (_, c, _) -> c) fetch_successes in 1142 - let updated = List.filter (fun (_, c, commits) -> not c && commits > 0) fetch_successes in 1143 - let unchanged_count = List.length fetch_successes - List.length cloned - List.length updated in 1144 - let total_commits_pulled = List.fold_left (fun acc (_, _, c) -> acc + c) 0 fetch_successes in 1145 - Log.app (fun m -> m " Pulled: %d cloned, %d updated, %d unchanged" 1146 - (List.length cloned) (List.length updated) unchanged_count); 1223 + (* Steps 3-5: Pull phases (fetch, merge, subtree) - skip if --skip-pull *) 1224 + let fetch_errors, unchanged_count, total_commits_pulled, merge_errors, subtree_errors = 1225 + if skip_pull then begin 1226 + Log.app (fun m -> m " Skipping pull from remotes (--skip-pull)"); 1227 + ([], List.length repos, 0, ref [], ref []) 1228 + end 1229 + else begin 1230 + (* Step 3: Fetch phase - clone/fetch from remotes (PARALLEL) *) 1231 + Log.app (fun m -> m " Fetching from remotes (parallel)..."); 1232 + let fetch_results = Eio.Fiber.List.map (fun pkg -> 1233 + let repo_name = Package.repo_name pkg in 1234 + (* First ensure checkout exists *) 1235 + match ensure_checkout_safe ~proc ~fs:fs_t ~config pkg with 1236 + | Error e -> Error { repo_name; phase = `Fetch; error = e } 1237 + | Ok (was_cloned, _) -> 1238 + if was_cloned then Ok (repo_name, true, 0) 1239 + else 1240 + match fetch_checkout_safe ~proc ~fs:fs_t ~config pkg with 1241 + | Error e -> Error { repo_name; phase = `Fetch; error = e } 1242 + | Ok commits -> Ok (repo_name, false, commits)) 1243 + repos 1244 + in 1245 + let fetch_errs, fetch_successes = 1246 + List.partition_map (function 1247 + | Error e -> Left e 1248 + | Ok r -> Right r) 1249 + fetch_results 1250 + in 1251 + let cloned = List.filter (fun (_, c, _) -> c) fetch_successes in 1252 + let updated = List.filter (fun (_, c, commits) -> not c && commits > 0) fetch_successes in 1253 + let unchanged = List.length fetch_successes - List.length cloned - List.length updated in 1254 + let commits_pulled = List.fold_left (fun acc (_, _, c) -> acc + c) 0 fetch_successes in 1255 + Log.app (fun m -> m " Pulled: %d cloned, %d updated, %d unchanged" 1256 + (List.length cloned) (List.length updated) unchanged); 1147 1257 1148 - (* Step 4: Merge phase - fast-forward merge checkouts (SEQUENTIAL) *) 1149 - Log.app (fun m -> m " Merging checkouts..."); 1150 - let merge_errors = ref [] in 1151 - List.iter (fun pkg -> 1152 - match merge_checkout_safe ~proc ~fs:fs_t ~config pkg with 1153 - | Ok () -> () 1154 - | Error e -> 1155 - merge_errors := { repo_name = Package.repo_name pkg; 1156 - phase = `Merge; error = e } :: !merge_errors) 1157 - repos; 1258 + (* Step 4: Merge phase - fast-forward merge checkouts (SEQUENTIAL) *) 1259 + Log.app (fun m -> m " Merging checkouts..."); 1260 + let merge_errs = ref [] in 1261 + List.iter (fun pkg -> 1262 + match merge_checkout_safe ~proc ~fs:fs_t ~config pkg with 1263 + | Ok () -> () 1264 + | Error e -> 1265 + merge_errs := { repo_name = Package.repo_name pkg; 1266 + phase = `Merge; error = e } :: !merge_errs) 1267 + repos; 1158 1268 1159 - (* Step 5: Subtree phase - pull subtrees into monorepo (SEQUENTIAL) *) 1160 - (* Check if monorepo has local modifications first *) 1161 - let monorepo = Config.Paths.monorepo config in 1162 - let monorepo_dirty = Git.is_dirty ~proc ~fs:fs_t monorepo in 1163 - let subtree_errors = ref [] in 1164 - if monorepo_dirty then begin 1165 - Log.warn (fun m -> 1166 - m "Monorepo has uncommitted changes, skipping subtree pulls"); 1167 - Log.app (fun m -> m " Skipping subtree updates (local modifications)...") 1168 - end 1169 - else begin 1170 - Log.app (fun m -> m " Updating subtrees..."); 1171 - List.iteri (fun i pkg -> 1172 - Log.info (fun m -> 1173 - m "[%d/%d] Subtree %s" (i + 1) total 1174 - (Package.subtree_prefix pkg)); 1175 - match pull_subtree ~proc ~fs ~config pkg with 1176 - | Ok _ -> () 1177 - | Error (Git_error e) -> 1178 - subtree_errors := { repo_name = Package.repo_name pkg; 1179 - phase = `Subtree; error = e } :: !subtree_errors 1180 - | Error _ -> ()) 1181 - repos 1182 - end; 1269 + (* Step 5: Subtree phase - pull subtrees into monorepo (SEQUENTIAL) *) 1270 + (* Check if monorepo has local modifications first *) 1271 + let monorepo = Config.Paths.monorepo config in 1272 + let monorepo_dirty = Git.is_dirty ~proc ~fs:fs_t monorepo in 1273 + let subtree_errs = ref [] in 1274 + if monorepo_dirty then begin 1275 + Log.warn (fun m -> 1276 + m "Monorepo has uncommitted changes, skipping subtree pulls"); 1277 + Log.app (fun m -> m " Skipping subtree updates (local modifications)...") 1278 + end 1279 + else begin 1280 + Log.app (fun m -> m " Updating subtrees..."); 1281 + List.iteri (fun i pkg -> 1282 + Log.info (fun m -> 1283 + m "[%d/%d] Subtree %s" (i + 1) total 1284 + (Package.subtree_prefix pkg)); 1285 + match pull_subtree ~proc ~fs ~config pkg with 1286 + | Ok _ -> () 1287 + | Error (Git_error e) -> 1288 + subtree_errs := { repo_name = Package.repo_name pkg; 1289 + phase = `Subtree; error = e } :: !subtree_errs 1290 + | Error _ -> ()) 1291 + repos 1292 + end; 1293 + (fetch_errs, unchanged, commits_pulled, merge_errs, subtree_errs) 1294 + end 1295 + in 1183 1296 1184 - (* Step 6: Finalize - write README.md and dune-project (SEQUENTIAL) *) 1185 - Log.app (fun m -> m " Writing README.md and dune-project..."); 1297 + (* Step 6: Finalize - write README.md, CLAUDE.md, and dune-project (SEQUENTIAL) *) 1298 + Log.app (fun m -> m " Writing README.md, CLAUDE.md, and dune-project..."); 1186 1299 write_readme ~proc ~fs:fs_t ~config all_pkgs; 1300 + write_claude_md ~proc ~fs:fs_t ~config; 1187 1301 write_dune_project ~proc ~fs:fs_t ~config all_pkgs; 1188 1302 1189 1303 (* Step 7: Remote phase - push to upstream remotes if --remote (LIMITED PARALLEL) *)
+16 -5
lib/monopam.mli
··· 49 49 val pp_error : error Fmt.t 50 50 (** [pp_error] formats errors. *) 51 51 52 + val pp_error_with_hint : error Fmt.t 53 + (** [pp_error_with_hint] formats errors with a helpful hint for resolving them. *) 54 + 55 + val error_hint : error -> string option 56 + (** [error_hint e] returns a hint string for the given error, if available. *) 57 + 52 58 (** {2 Status} *) 53 59 54 60 val status : ··· 154 160 config:Config.t -> 155 161 ?package:string -> 156 162 ?remote:bool -> 163 + ?skip_push:bool -> 164 + ?skip_pull:bool -> 157 165 unit -> 158 166 (sync_summary, error) result 159 - (** [sync ~proc ~fs ~config ?package ?remote ()] synchronizes the monorepo with 160 - upstream repositories. 167 + (** [sync ~proc ~fs ~config ?package ?remote ?skip_push ?skip_pull ()] 168 + synchronizes the monorepo with upstream repositories. 161 169 162 - This performs both push and pull operations in the correct order: 170 + This is the primary command for all sync operations. It performs both 171 + push and pull operations in the correct order: 163 172 1. Validate: check for dirty state (abort if dirty) 164 - 2. Push phase: export monorepo changes to checkouts (sequential) 173 + 2. Push phase: export monorepo changes to checkouts (parallel) 165 174 3. Fetch phase: clone/fetch from remotes (parallel) 166 175 4. Merge phase: fast-forward merge checkouts (sequential) 167 176 5. Subtree phase: pull subtrees into monorepo (sequential) ··· 174 183 @param fs Eio filesystem 175 184 @param config Monopam configuration 176 185 @param package Optional specific package to sync 177 - @param remote If true, also push checkouts to their upstream git remotes *) 186 + @param remote If true, also push checkouts to their upstream git remotes 187 + @param skip_push If true, skip pushing monorepo changes to checkouts 188 + @param skip_pull If true, skip fetching and pulling from remotes *) 178 189 179 190 (** {2 Package Management} *) 180 191
+25
lib/verse.ml
··· 14 14 | Workspace_exists p -> Fmt.pf ppf "Workspace already exists: %a" Fpath.pp p 15 15 | Not_a_workspace p -> Fmt.pf ppf "Not a opamverse workspace: %a" Fpath.pp p 16 16 17 + let error_hint = function 18 + | Config_error _ -> 19 + Some "Run 'monopam verse init --handle <your-handle>' to create a workspace." 20 + | Git_error (Git.Dirty_worktree _) -> 21 + Some "Commit or stash your changes first: git status" 22 + | Git_error (Git.Command_failed (cmd, _)) when String.starts_with ~prefix:"git clone" cmd -> 23 + Some "Check the URL is correct and you have network access." 24 + | Git_error (Git.Command_failed (cmd, _)) when String.starts_with ~prefix:"git pull" cmd -> 25 + Some "Check your network connection. Try: git fetch origin" 26 + | Git_error _ -> None 27 + | Registry_error _ -> 28 + Some "The registry may be temporarily unavailable. Try again later." 29 + | Member_not_found h -> 30 + Some (Fmt.str "Check available members: monopam verse members (looking for '%s')" h) 31 + | Workspace_exists _ -> 32 + Some "Use a different directory, or remove the existing workspace." 33 + | Not_a_workspace _ -> 34 + Some "Run 'monopam verse init --handle <your-handle>' to create a workspace here." 35 + 36 + let pp_error_with_hint ppf e = 37 + pp_error ppf e; 38 + match error_hint e with 39 + | Some hint -> Fmt.pf ppf "@.@[<v 2>Hint: %s@]" hint 40 + | None -> () 41 + 17 42 type member_status = { 18 43 handle : string; 19 44 monorepo_url : string;
+6
lib/verse.mli
··· 16 16 val pp_error : error Fmt.t 17 17 (** [pp_error] formats errors. *) 18 18 19 + val pp_error_with_hint : error Fmt.t 20 + (** [pp_error_with_hint] formats errors with a helpful hint for resolving them. *) 21 + 22 + val error_hint : error -> string option 23 + (** [error_hint e] returns a hint string for the given error, if available. *) 24 + 19 25 (** {1 Status Types} *) 20 26 21 27 type member_status = {