A monorepo management tool for the agentic ages

group

+90 -43
+90 -43
bin/main.ml
··· 55 55 String.starts_with ~prefix:"../" s || (* Relative path *) 56 56 String.contains s ':' (* URL with scheme *) 57 57 58 + (* Normalize a dev-repo URL for grouping comparison *) 59 + let normalize_dev_repo url = 60 + let s = url in 61 + (* Strip git+ prefix *) 62 + let s = if String.starts_with ~prefix:"git+" s then 63 + String.sub s 4 (String.length s - 4) else s in 64 + (* Strip trailing .git *) 65 + let s = if String.ends_with ~suffix:".git" s then 66 + String.sub s 0 (String.length s - 4) else s in 67 + (* Strip trailing slash *) 68 + let s = if String.ends_with ~suffix:"/" s then 69 + String.sub s 0 (String.length s - 1) else s in 70 + (* Normalize github URLs: git@github.com:x/y -> https://github.com/x/y *) 71 + let s = if String.starts_with ~prefix:"git@github.com:" s then 72 + "https://github.com/" ^ String.sub s 15 (String.length s - 15) else s in 73 + String.lowercase_ascii s 74 + 75 + (* Group solved packages by their dev-repo *) 76 + type package_group = { 77 + canonical_name : string; (* First package name, used as vendor name *) 78 + dev_repo : string; (* Original dev-repo URL *) 79 + packages : string list; (* All package names in this group *) 80 + } 81 + 82 + let group_packages_by_dev_repo ~config (pkgs : OpamPackage.t list) : package_group list = 83 + let repos = config.Unpac.Config.opam.repositories in 84 + (* Build a map from normalized dev-repo to package info *) 85 + let groups = Hashtbl.create 16 in 86 + List.iter (fun pkg -> 87 + let name = OpamPackage.Name.to_string (OpamPackage.name pkg) in 88 + let version = OpamPackage.Version.to_string (OpamPackage.version pkg) in 89 + match Unpac_opam.Repo.find_package ~repos ~name ~version () with 90 + | None -> () (* Skip packages not found *) 91 + | Some result -> 92 + match result.metadata.dev_repo with 93 + | None -> () (* Skip packages without dev-repo *) 94 + | Some dev_repo -> 95 + let key = normalize_dev_repo dev_repo in 96 + match Hashtbl.find_opt groups key with 97 + | None -> 98 + Hashtbl.add groups key (dev_repo, [name]) 99 + | Some (orig_url, names) -> 100 + Hashtbl.replace groups key (orig_url, name :: names) 101 + ) pkgs; 102 + (* Convert to list of groups *) 103 + Hashtbl.fold (fun _key (dev_repo, names) acc -> 104 + let names = List.rev names in (* Preserve order *) 105 + let canonical_name = List.hd names in 106 + { canonical_name; dev_repo; packages = names } :: acc 107 + ) groups [] 108 + |> List.sort (fun a b -> String.compare a.canonical_name b.canonical_name) 109 + 58 110 (* Helper to resolve vendor cache *) 59 111 let resolve_cache ~proc_mgr ~fs ~config ~cli_cache = 60 112 match Unpac.Config.resolve_vendor_cache ?cli_override:cli_cache config with ··· 239 291 let info = Cmd.info "config" ~doc in 240 292 Cmd.group info [opam_config_compiler_cmd] 241 293 242 - (* Helper to add a single package by name *) 243 - let add_single_package ~proc_mgr ~root ?cache ~config ~name ~version_opt ~branch_opt () = 244 - let repos = config.Unpac.Config.opam.repositories in 245 - match Unpac_opam.Repo.find_package ~repos ~name ?version:version_opt () with 246 - | None -> 247 - Format.eprintf "Package '%s' not found in configured repositories@." name; 248 - `Failed 249 - | Some result -> 250 - match result.metadata.dev_repo with 251 - | None -> 252 - Format.eprintf "Package '%s' has no dev-repo field, skipping@." name; 253 - `Skipped 254 - | Some dev_repo -> 255 - (* Strip git+ prefix if present (opam dev-repo format) *) 256 - let url = if String.starts_with ~prefix:"git+" dev_repo then 257 - String.sub dev_repo 4 (String.length dev_repo - 4) 258 - else dev_repo in 259 - let info : Unpac.Backend.package_info = { 260 - name; 261 - url; 262 - branch = branch_opt; 263 - } in 264 - match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with 265 - | Unpac.Backend.Added { name = pkg_name; sha } -> 266 - Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7); 267 - `Added 268 - | Unpac.Backend.Already_exists pkg_name -> 269 - Format.printf "Package %s already vendored@." pkg_name; 270 - `Exists 271 - | Unpac.Backend.Failed { name = pkg_name; error } -> 272 - Format.eprintf "Error adding %s: %s@." pkg_name error; 273 - `Failed 274 - 275 294 (* Opam add command - enhanced to support package names and dependency solving *) 276 295 let opam_add_cmd = 277 296 let doc = "Vendor an opam package (by name or git URL)." in ··· 337 356 (OpamPackage.Name.to_string (OpamPackage.name p)) 338 357 (OpamPackage.Version.to_string (OpamPackage.version p)) 339 358 ) pkgs; 340 - Format.printf "@.Vendoring packages...@."; 359 + 360 + (* Group packages by dev-repo to avoid duplicating sources *) 361 + let groups = group_packages_by_dev_repo ~config pkgs in 362 + Format.printf "@.Grouped into %d unique repositories:@." (List.length groups); 363 + List.iter (fun (g : package_group) -> 364 + if List.length g.packages > 1 then 365 + Format.printf " %s (%d packages: %s)@." 366 + g.canonical_name 367 + (List.length g.packages) 368 + (String.concat ", " g.packages) 369 + else 370 + Format.printf " %s@." g.canonical_name 371 + ) groups; 372 + 373 + Format.printf "@.Vendoring repositories...@."; 341 374 let added = ref 0 in 342 375 let failed = ref 0 in 343 - List.iter (fun p -> 344 - let name = OpamPackage.Name.to_string (OpamPackage.name p) in 345 - match add_single_package ~proc_mgr ~root ?cache ~config ~name ~version_opt:None ~branch_opt () with 346 - | `Added -> incr added 347 - | `Exists -> () 348 - | `Skipped -> () 349 - | `Failed -> incr failed 350 - ) pkgs; 351 - Format.printf "@.Done: %d added, %d failed@." !added !failed; 376 + List.iter (fun (g : package_group) -> 377 + (* Use canonical name as vendor name, dev-repo as URL *) 378 + let url = if String.starts_with ~prefix:"git+" g.dev_repo then 379 + String.sub g.dev_repo 4 (String.length g.dev_repo - 4) 380 + else g.dev_repo in 381 + let info : Unpac.Backend.package_info = { 382 + name = g.canonical_name; 383 + url; 384 + branch = None; 385 + } in 386 + match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with 387 + | Unpac.Backend.Added { name = pkg_name; sha } -> 388 + Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7); 389 + if List.length g.packages > 1 then 390 + Format.printf " Contains: %s@." (String.concat ", " g.packages); 391 + incr added 392 + | Unpac.Backend.Already_exists pkg_name -> 393 + Format.printf "Package %s already vendored@." pkg_name 394 + | Unpac.Backend.Failed { name = pkg_name; error } -> 395 + Format.eprintf "Error adding %s: %s@." pkg_name error; 396 + incr failed 397 + ) groups; 398 + Format.printf "@.Done: %d repositories added, %d failed@." !added !failed; 352 399 if !failed > 0 then exit 1 353 400 end else begin 354 401 (* Single package mode *)