Monorepo management for opam overlays
0
fork

Configure Feed

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

Remove add/remove commands and document CLI improvement plan

- Remove monopam add and remove commands from CLI (use agent skills instead)
- Add comprehensive CLI improvement plan covering:
- Verse remotes: auto-managed during sync
- Opam metadata sync: local always trumps opam-repo
- Doctor command: Claude-powered analysis with JSON/text output

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

+330 -70
+329
PLAN-cli-improvements.md
··· 1 + # Monopam CLI Improvements Plan 2 + 3 + ## Design Decisions (Clarified) 4 + 5 + 1. **Verse remotes**: Auto-add on `monopam sync`, remove outdated ones for all verse members 6 + 2. **Add/remove commands**: Removed from CLI - use agent skills instead 7 + 3. **Doctor output**: Structured JSON with per-repo recommendations, rendered to text by CLI with `--json` option 8 + 4. **Verse remote URL**: Point to `src/` checkout (individual repo) 9 + 5. **Opam sync direction**: Local metadata always trumps opam-repo metadata 10 + 6. **Claude usage**: Always use Claude (via ocaml-claude library) for doctor command 11 + 12 + --- 13 + 14 + ## Current CLI Commands 15 + 16 + After removing add/remove: 17 + - `monopam status` - Show sync status and verse fork analysis 18 + - `monopam sync [--remote] [--skip-push] [--skip-pull] [package]` - Primary sync command 19 + - `monopam changes` - Generate changelogs with Claude 20 + - `monopam verse init` - Initialize workspace 21 + - `monopam verse members` - List registry members 22 + - `monopam verse pull` - Pull verse member repos 23 + - `monopam verse sync` - Sync verse workspace 24 + 25 + --- 26 + 27 + ## Implementation Plan 28 + 29 + ### Phase 1: Verse Remotes (Auto-managed) 30 + 31 + #### Changes to `monopam sync` 32 + 33 + Add verse remote management to the sync process: 34 + 35 + 1. **During fetch phase**: For each verse member in registry: 36 + - Scan their monorepo for subtrees 37 + - For matching subtrees in our `src/`: 38 + - Add git remote named `verse/<handle>` pointing to their `src/` checkout 39 + - If remote exists but URL changed, update it 40 + - Fetch from the remote 41 + 42 + 2. **Cleanup**: Remove verse remotes for: 43 + - Members no longer in registry 44 + - Repos we no longer have 45 + 46 + #### Data Flow 47 + 48 + ``` 49 + sync starts 50 + ├── push phase (existing) 51 + ├── fetch phase (existing) 52 + │ └── NEW: for each verse member with matching repos: 53 + │ └── ensure git remote in src/<repo> → verse/<member>/src/<repo> 54 + │ └── git fetch verse/<handle> 55 + ├── merge phase (existing) 56 + ├── subtree phase (existing) 57 + ├── finalize phase (existing) 58 + └── remote phase (existing) 59 + ``` 60 + 61 + #### Implementation in monopam.ml 62 + 63 + ```ocaml 64 + (* New function to manage verse remotes for a repo *) 65 + let ensure_verse_remotes ~proc ~fs ~config ~verse_config pkg = 66 + let checkouts_root = Config.Paths.checkouts config in 67 + let checkout_dir = Package.checkout_dir ~checkouts_root pkg in 68 + let repo_name = Package.repo_name pkg in 69 + 70 + (* Get all verse members who have this repo *) 71 + let verse_subtrees = Verse.get_verse_subtrees ~proc ~fs ~config:verse_config () in 72 + let members_with_repo = 73 + Hashtbl.find_opt verse_subtrees repo_name 74 + |> Option.value ~default:[] 75 + in 76 + 77 + (* For each member, ensure remote exists *) 78 + List.iter (fun (handle, verse_mono_path) -> 79 + let remote_name = "verse/" ^ handle in 80 + let verse_src = Fpath.(verse_mono_path / ".." / "src" / repo_name) in 81 + (* Add or update remote *) 82 + Git.ensure_remote ~proc ~fs ~name:remote_name ~url:verse_src checkout_dir 83 + ) members_with_repo 84 + ``` 85 + 86 + --- 87 + 88 + ### Phase 2: Opam Metadata Sync 89 + 90 + #### New Command: `monopam opam sync` 91 + 92 + Synchronize `.opam` files from monorepo subtrees to opam-repo. 93 + 94 + ```bash 95 + monopam opam sync # Sync all packages 96 + monopam opam sync eio # Sync specific package 97 + ``` 98 + 99 + **Behavior:** 100 + - For each package in monorepo: 101 + - Read `.opam` file from subtree 102 + - Compare with opam-repo version 103 + - If different, copy monorepo → opam-repo (local always wins) 104 + - Stage changes in opam-repo 105 + 106 + #### Integration with `monopam sync` 107 + 108 + Add `--opam` flag: 109 + 110 + ```bash 111 + monopam sync --opam --remote # Full sync including opam metadata 112 + ``` 113 + 114 + Or make it part of finalize phase (always sync opam). 115 + 116 + #### Implementation 117 + 118 + ```ocaml 119 + let sync_opam_files ~proc ~fs ~config pkgs = 120 + let monorepo = Config.Paths.monorepo config in 121 + let opam_repo = Config.Paths.opam_repo config in 122 + 123 + List.iter (fun pkg -> 124 + let name = Package.name pkg in 125 + let subtree_opam = Fpath.(monorepo / Package.subtree_prefix pkg / (name ^ ".opam")) in 126 + let repo_opam = Fpath.(opam_repo / "packages" / name / (name ^ ".dev") / "opam") in 127 + 128 + (* Read both files *) 129 + let subtree_content = read_file_opt ~fs subtree_opam in 130 + let repo_content = read_file_opt ~fs repo_opam in 131 + 132 + match subtree_content with 133 + | None -> () (* No opam file in subtree, skip *) 134 + | Some content when Some content <> repo_content -> 135 + (* Copy to opam-repo *) 136 + write_file ~fs repo_opam content; 137 + Git.add ~proc ~fs opam_repo [Fpath.to_string repo_opam] 138 + | _ -> () (* Already in sync *) 139 + ) pkgs 140 + ``` 141 + 142 + --- 143 + 144 + ### Phase 3: Doctor Command 145 + 146 + #### New Command: `monopam doctor` 147 + 148 + Claude-powered workspace health analysis. 149 + 150 + ```bash 151 + monopam doctor # Full analysis, text output 152 + monopam doctor --json # JSON output for tooling 153 + monopam doctor eio # Analyze specific repo 154 + ``` 155 + 156 + #### Output Structure (JSON) 157 + 158 + ```json 159 + { 160 + "timestamp": "2026-01-21T12:00:00Z", 161 + "workspace": "/home/user/tangled", 162 + "summary": { 163 + "repos_total": 39, 164 + "repos_need_sync": 2, 165 + "repos_behind_upstream": 3, 166 + "verse_divergences": 5 167 + }, 168 + "repos": [ 169 + { 170 + "name": "eio", 171 + "local_sync": "in_sync", 172 + "remote_sync": { "ahead": 0, "behind": 0 }, 173 + "verse_analysis": [ 174 + { 175 + "handle": "alice.bsky.social", 176 + "their_commits": [ 177 + { 178 + "hash": "abc1234", 179 + "subject": "Add Eio.Path.symlink support", 180 + "category": "feature", 181 + "priority": "medium", 182 + "recommendation": "review-first", 183 + "conflict_risk": "low", 184 + "summary": "Adds symlink creation support to Eio.Path module" 185 + }, 186 + { 187 + "hash": "def5678", 188 + "subject": "Fix race condition in Eio.Fiber.fork", 189 + "category": "bug-fix", 190 + "priority": "high", 191 + "recommendation": "merge-now", 192 + "conflict_risk": "none", 193 + "summary": "Fixes potential deadlock when forking fibers under load" 194 + } 195 + ], 196 + "suggested_action": "git fetch verse/alice.bsky.social && git cherry-pick def5678" 197 + } 198 + ] 199 + } 200 + ], 201 + "recommendations": [ 202 + { 203 + "priority": "high", 204 + "action": "Merge alice's bug fix for eio (def5678)", 205 + "command": "cd src/eio && git cherry-pick def5678" 206 + }, 207 + { 208 + "priority": "medium", 209 + "action": "Run monopam sync to resolve local sync issues", 210 + "command": "monopam sync" 211 + } 212 + ], 213 + "warnings": [ 214 + "opam-repo has uncommitted changes", 215 + "verse/alice.bsky.social/ is 10 commits behind" 216 + ] 217 + } 218 + ``` 219 + 220 + #### Text Rendering 221 + 222 + ``` 223 + === Monopam Doctor Report === 224 + Generated: 2026-01-21 12:00:00 225 + 226 + Summary: 227 + 39 repos tracked 228 + 2 need local sync 229 + 3 behind upstream 230 + 5 verse divergences 231 + 232 + ───────────────────────────────────────── 233 + 234 + eio (diverged from alice.bsky.social) 235 + 236 + Their commits (2): 237 + 238 + [HIGH] def5678 Fix race condition in Eio.Fiber.fork 239 + Category: bug-fix | Risk: none | Action: merge-now 240 + → Fixes potential deadlock when forking fibers under load 241 + 242 + [MED] abc1234 Add Eio.Path.symlink support 243 + Category: feature | Risk: low | Action: review-first 244 + → Adds symlink creation support to Eio.Path module 245 + 246 + Suggested: git fetch verse/alice.bsky.social && git cherry-pick def5678 247 + 248 + ───────────────────────────────────────── 249 + 250 + Recommendations: 251 + 1. [HIGH] Merge alice's bug fix for eio (def5678) 252 + $ cd src/eio && git cherry-pick def5678 253 + 254 + 2. [MED] Run monopam sync to resolve local sync issues 255 + $ monopam sync 256 + 257 + Warnings: 258 + • opam-repo has uncommitted changes 259 + • verse/alice.bsky.social/ is 10 commits behind 260 + ``` 261 + 262 + #### Claude Integration 263 + 264 + Use the existing `claude` OCaml library for analysis: 265 + 266 + ```ocaml 267 + module Doctor = struct 268 + type commit_analysis = { 269 + hash: string; 270 + subject: string; 271 + category: [`Security_fix | `Bug_fix | `Feature | `Refactor | `Docs | `Test]; 272 + priority: [`Critical | `High | `Medium | `Low]; 273 + recommendation: [`Merge_now | `Review_first | `Skip | `Needs_discussion]; 274 + conflict_risk: [`None | `Low | `Medium | `High]; 275 + summary: string; 276 + } 277 + 278 + let analyze_commits ~commits ~our_branch ~their_handle = 279 + let prompt = Format.asprintf {| 280 + You are analyzing git commits from a collaborator's repository. 281 + 282 + Repository context: 283 + - Our branch: %s 284 + - Their handle: %s 285 + 286 + Commits to analyze: 287 + %s 288 + 289 + For each commit, provide JSON with: 290 + - category: security-fix, bug-fix, feature, refactor, docs, test 291 + - priority: critical, high, medium, low 292 + - recommendation: merge-now, review-first, skip, needs-discussion 293 + - conflict_risk: none, low, medium, high 294 + - summary: one-line description of what the commit does 295 + 296 + Respond with a JSON array of analyses. 297 + |} our_branch their_handle (format_commits commits) in 298 + 299 + Claude.chat ~model:"claude-sonnet-4-20250514" ~messages:[ 300 + Claude.Message.user prompt 301 + ] () 302 + |> parse_analysis_response 303 + end 304 + ``` 305 + 306 + --- 307 + 308 + ## Implementation Order 309 + 310 + 1. **Phase 1a**: Add `ensure_verse_remotes` function to monopam.ml 311 + 2. **Phase 1b**: Integrate verse remote management into sync fetch phase 312 + 3. **Phase 2a**: Add `sync_opam_files` function 313 + 4. **Phase 2b**: Add `monopam opam sync` command (or integrate into sync) 314 + 5. **Phase 3a**: Create `doctor.ml` module with types and Claude integration 315 + 6. **Phase 3b**: Add `monopam doctor` command with JSON/text output 316 + 317 + --- 318 + 319 + ## Files to Modify/Create 320 + 321 + ### Modify 322 + - `monopam/lib/monopam.ml` - Add verse remote management, opam sync 323 + - `monopam/lib/monopam.mli` - Export new functions 324 + - `monopam/bin/main.ml` - Add doctor command, opam subcommand 325 + 326 + ### Create 327 + - `monopam/lib/doctor.ml` - Doctor analysis logic 328 + - `monopam/lib/doctor.mli` - Doctor interface 329 + - `monopam/lib/opam_sync.ml` - Opam metadata sync logic (optional, could be in monopam.ml)
+1 -70
bin/main.ml
··· 212 212 Cmd.v info 213 213 Term.(ret (const run $ package_arg $ remote_arg $ skip_push_arg $ skip_pull_arg $ logging_term)) 214 214 215 - (* Add command *) 216 - 217 - let add_cmd = 218 - let doc = "Add a package to the monorepo" in 219 - let man = 220 - [ 221 - `S Manpage.s_description; 222 - `P "Adds a single package from the opam overlay to the monorepo."; 223 - `P 224 - "This clones the package's git repository if not already present, then \ 225 - adds it as a git subtree in the monorepo."; 226 - ] 227 - in 228 - let info = Cmd.info "add" ~doc ~man in 229 - let package_arg = 230 - let doc = "Package name to add" in 231 - Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc) 232 - in 233 - let run package () = 234 - Eio_main.run @@ fun env -> 235 - with_config env @@ fun config -> 236 - let fs = Eio.Stdenv.fs env in 237 - let proc = Eio.Stdenv.process_mgr env in 238 - match Monopam.add ~proc ~fs ~config ~package () with 239 - | Ok () -> 240 - Fmt.pr "Added %s to monorepo.@." package; 241 - `Ok () 242 - | Error e -> 243 - Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 244 - `Error (false, "add failed") 245 - in 246 - Cmd.v info 247 - Term.(ret (const run $ package_arg $ logging_term)) 248 - 249 - (* Remove command *) 250 - 251 - let remove_cmd = 252 - let doc = "Remove a package from the monorepo" in 253 - let man = 254 - [ 255 - `S Manpage.s_description; 256 - `P "Removes a package's subtree directory from the monorepo."; 257 - `P 258 - "This does not delete the git checkout - only the subtree in the \ 259 - monorepo."; 260 - ] 261 - in 262 - let info = Cmd.info "remove" ~doc ~man in 263 - let package_arg = 264 - let doc = "Package name to remove" in 265 - Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc) 266 - in 267 - let run package () = 268 - Eio_main.run @@ fun env -> 269 - with_config env @@ fun config -> 270 - let fs = Eio.Stdenv.fs env in 271 - let proc = Eio.Stdenv.process_mgr env in 272 - match Monopam.remove ~proc ~fs ~config ~package () with 273 - | Ok () -> 274 - Fmt.pr "Removed %s from monorepo.@." package; 275 - `Ok () 276 - | Error e -> 277 - Fmt.epr "Error: %a@." Monopam.pp_error_with_hint e; 278 - `Error (false, "remove failed") 279 - in 280 - Cmd.v info 281 - Term.(ret (const run $ package_arg $ logging_term)) 282 - 283 215 (* Changes command *) 284 216 285 217 let changes_cmd = ··· 792 724 `I ("Check status", "monopam status"); 793 725 `I ("Sync everything", "monopam sync"); 794 726 `I ("Sync and push upstream", "monopam sync --remote"); 795 - `I ("Add a new package", "monopam add <package-name>"); 796 727 `I ("Sync one package", "monopam sync <package-name>"); 797 728 `S "CONFIGURATION"; 798 729 `P ··· 821 752 in 822 753 let info = Cmd.info "monopam" ~version:"%%VERSION%%" ~doc ~man in 823 754 Cmd.group info 824 - [ status_cmd; sync_cmd; add_cmd; remove_cmd; changes_cmd; verse_cmd ] 755 + [ status_cmd; sync_cmd; changes_cmd; verse_cmd ] 825 756 826 757 let () = exit (Cmd.eval main_cmd)