A monorepo management tool for the agentic ages

Track vendored packages in config for remote recreation

- Add vendored_package type to Config with name, url, and optional branch
- Add TOML codec for [[opam.vendored]] sections
- Update opam add command to record packages in config after vendoring
- Add ensure_vendored_remotes function to Git module to recreate remotes
from config on-demand (useful after cloning a workspace)
- Add helper functions: add/remove/find/list vendored packages

This allows workspaces to be cloned and have their remotes automatically
recreated based on the tracked vendored packages in unpac.toml.

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

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

+98 -2
+15
bin/main.ml
··· 427 427 Format.printf "@.Vendoring repositories...@."; 428 428 let added = ref 0 in 429 429 let failed = ref 0 in 430 + let config = ref config in 430 431 List.iter (fun (g : package_group) -> 431 432 (* Use canonical name as vendor name, dev-repo as URL *) 432 433 let url = if String.starts_with ~prefix:"git+" g.dev_repo then ··· 439 440 } in 440 441 match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with 441 442 | Unpac.Backend.Added { name = pkg_name; sha } -> 443 + (* Record in config for remote recreation *) 444 + let vendored : Unpac.Config.vendored_package = { 445 + pkg_name; pkg_url = url; pkg_branch = None 446 + } in 447 + config := Unpac.Config.add_vendored_package !config vendored; 442 448 Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7); 443 449 if List.length g.packages > 1 then 444 450 Format.printf " Contains: %s@." (String.concat ", " g.packages); ··· 449 455 Format.eprintf "Error adding %s: %s@." pkg_name error; 450 456 incr failed 451 457 ) groups; 458 + (* Save config with all vendored packages *) 459 + if !added > 0 then 460 + save_config ~proc_mgr root !config "Record vendored packages in config"; 452 461 Format.printf "@.Done: %d repositories added, %d failed@." !added !failed; 453 462 if !failed > 0 then exit 1 454 463 end else begin ··· 498 507 } in 499 508 match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with 500 509 | Unpac.Backend.Added { name = pkg_name; sha } -> 510 + (* Record in config for remote recreation on fresh clones *) 511 + let vendored : Unpac.Config.vendored_package = { 512 + pkg_name; pkg_url = url; pkg_branch = branch_opt 513 + } in 514 + let config = Unpac.Config.add_vendored_package config vendored in 515 + save_config ~proc_mgr root config "Record vendored package in config"; 501 516 Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7); 502 517 Format.printf "@.Next steps:@."; 503 518 Format.printf " unpac opam edit %s # make local changes@." pkg_name;
+39 -2
lib/config.ml
··· 13 13 source : repo_source; 14 14 } 15 15 16 + type vendored_package = { 17 + pkg_name : string; (** Package name (used as vendor name) *) 18 + pkg_url : string; (** Original remote URL *) 19 + pkg_branch : string option; (** Original branch if specified *) 20 + } 21 + 16 22 type opam_config = { 17 23 repositories : repo_config list; 18 24 compiler : string option; 25 + vendored : vendored_package list; (** Tracked vendored packages *) 19 26 } 20 27 21 28 (** Git repository configuration for direct git vendoring *) ··· 69 76 |> opt_mem "url" string ~enc:enc_url 70 77 |> finish 71 78 79 + let vendored_package_codec : vendored_package Tomlt.t = 80 + let open Tomlt in 81 + let open Table in 82 + obj (fun pkg_name pkg_url pkg_branch : vendored_package -> 83 + { pkg_name; pkg_url; pkg_branch }) 84 + |> mem "name" string ~enc:(fun (p : vendored_package) -> p.pkg_name) 85 + |> mem "url" string ~enc:(fun (p : vendored_package) -> p.pkg_url) 86 + |> opt_mem "branch" string ~enc:(fun (p : vendored_package) -> p.pkg_branch) 87 + |> finish 88 + 72 89 let opam_config_codec : opam_config Tomlt.t = 73 90 let open Tomlt in 74 91 let open Table in 75 - obj (fun repositories compiler : opam_config -> { repositories; compiler }) 92 + obj (fun repositories compiler vendored : opam_config -> 93 + { repositories; compiler; vendored = Option.value ~default:[] vendored }) 76 94 |> mem "repositories" (list repo_config_codec) 77 95 ~enc:(fun (c : opam_config) -> c.repositories) 78 96 |> opt_mem "compiler" string ~enc:(fun (c : opam_config) -> c.compiler) 97 + |> opt_mem "vendored" (list vendored_package_codec) 98 + ~enc:(fun (c : opam_config) -> if c.vendored = [] then None else Some c.vendored) 79 99 |> finish 80 100 81 101 let git_repo_config_codec : git_repo_config Tomlt.t = ··· 146 166 147 167 (** {1 Helpers} *) 148 168 149 - let empty_opam = { repositories = []; compiler = None } 169 + let empty_opam = { repositories = []; compiler = None; vendored = [] } 150 170 let empty = { opam = empty_opam; git = empty_git; vendor_cache = None; projects = [] } 151 171 152 172 let find_project config name = ··· 200 220 match Sys.getenv_opt "UNPAC_VENDOR_CACHE" with 201 221 | Some path -> Some path 202 222 | None -> config.vendor_cache 223 + 224 + (* Vendored package helpers *) 225 + let add_vendored_package config (pkg : vendored_package) = 226 + (* Replace if exists, otherwise append *) 227 + let vendored = List.filter (fun p -> p.pkg_name <> pkg.pkg_name) config.opam.vendored in 228 + let vendored = vendored @ [pkg] in 229 + { config with opam = { config.opam with vendored } } 230 + 231 + let remove_vendored_package config name = 232 + let vendored = List.filter (fun p -> p.pkg_name <> name) config.opam.vendored in 233 + { config with opam = { config.opam with vendored } } 234 + 235 + let find_vendored_package config name = 236 + List.find_opt (fun p -> p.pkg_name = name) config.opam.vendored 237 + 238 + let list_vendored_packages config = 239 + config.opam.vendored
+21
lib/config.mli
··· 13 13 source : repo_source; 14 14 } 15 15 16 + type vendored_package = { 17 + pkg_name : string; (** Package name (used as vendor name) *) 18 + pkg_url : string; (** Original remote URL *) 19 + pkg_branch : string option; (** Original branch if specified *) 20 + } 21 + 16 22 type opam_config = { 17 23 repositories : repo_config list; 18 24 compiler : string option; 25 + vendored : vendored_package list; (** Tracked vendored packages *) 19 26 } 20 27 21 28 (** Git repository configuration for direct git vendoring *) ··· 108 115 (** [resolve_vendor_cache ?cli_override config] resolves vendor cache path. 109 116 Priority: CLI flag > UNPAC_VENDOR_CACHE env var > config file. 110 117 Returns None if not configured anywhere. *) 118 + 119 + (** {2 Vendored Package Helpers} *) 120 + 121 + val add_vendored_package : t -> vendored_package -> t 122 + (** [add_vendored_package config pkg] adds or replaces a vendored package entry. *) 123 + 124 + val remove_vendored_package : t -> string -> t 125 + (** [remove_vendored_package config name] removes a vendored package by name. *) 126 + 127 + val find_vendored_package : t -> string -> vendored_package option 128 + (** [find_vendored_package config name] finds a vendored package by name. *) 129 + 130 + val list_vendored_packages : t -> vendored_package list 131 + (** [list_vendored_packages config] returns all vendored packages. *) 111 132 112 133 (** {1 Codecs} *) 113 134
+14
lib/git.ml
··· 296 296 `Created 297 297 end 298 298 299 + let ensure_vendored_remotes ~proc_mgr ~cwd (packages : Config.vendored_package list) = 300 + let created = ref 0 in 301 + List.iter (fun (pkg : Config.vendored_package) -> 302 + let remote_name = "origin-" ^ pkg.pkg_name in 303 + match ensure_remote ~proc_mgr ~cwd ~name:remote_name ~url:pkg.pkg_url with 304 + | `Created -> 305 + Log.info (fun m -> m "Recreated remote %s -> %s" remote_name pkg.pkg_url); 306 + incr created 307 + | `Updated -> 308 + Log.info (fun m -> m "Updated remote %s -> %s" remote_name pkg.pkg_url) 309 + | `Existed -> () 310 + ) packages; 311 + !created 312 + 299 313 (* State-changing operations *) 300 314 301 315 let init ~proc_mgr ~cwd =
+9
lib/git.mli
··· 200 200 [ `Created | `Existed ] 201 201 (** [ensure_branch] creates branch if it doesn't exist. *) 202 202 203 + val ensure_vendored_remotes : 204 + proc_mgr:proc_mgr -> 205 + cwd:path -> 206 + Config.vendored_package list -> 207 + int 208 + (** [ensure_vendored_remotes ~proc_mgr ~cwd packages] ensures remotes exist for 209 + all vendored packages. Returns the number of remotes created. 210 + Use this to recreate remotes after cloning a workspace. *) 211 + 203 212 (** {1 State-changing operations} *) 204 213 205 214 val init :