My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

Squashed 'monopam/' changes from 9bd8701..9407fff

9407fff Remove redundant dev_repo from config and add verse clone support
49db60e Add --url option to devcontainer and update CLI help
478f3b7 Add monopam devcontainer command
3e00d15 Update sources.toml when forking packages

git-subtree-dir: monopam
git-subtree-split: 9407fff3d599bb02353b622c759543b6f1e2af39

+245 -42
+126 -5
bin/main.ml
··· 43 43 List.fold_left 44 44 (fun cfg (name, override) -> 45 45 let open Monopam.Verse_config in 46 - Monopam.Config.with_package_override cfg ~name 47 - ?branch:override.branch ?dev_repo:override.dev_repo ()) 46 + Monopam.Config.with_package_override cfg ~name ?branch:override.branch 47 + ()) 48 48 base_config 49 49 (Monopam.Verse_config.packages verse_config) 50 50 in ··· 769 769 (List.length result.packages_forked) result.source_handle; 770 770 List.iter (fun p -> Fmt.pr " %s@." p) result.packages_forked 771 771 end else begin 772 + (* Update sources.toml with fork information *) 773 + let mono_path = Monopam.Verse_config.mono_path config in 774 + let sources_path = Fpath.(mono_path / "sources.toml") in 775 + let sources = 776 + match Monopam.Sources_registry.load ~fs:(fs :> _ Eio.Path.t) sources_path with 777 + | Ok s -> s 778 + | Error _ -> Monopam.Sources_registry.empty 779 + in 780 + let entry = Monopam.Sources_registry.{ 781 + url = result.fork_url; 782 + upstream = Some result.upstream_url; 783 + branch = None; 784 + reason = Some (Fmt.str "Forked from %s" result.source_handle); 785 + } in 786 + let sources = Monopam.Sources_registry.add sources ~subtree:result.subtree_name entry in 787 + (match Monopam.Sources_registry.save ~fs:(fs :> _ Eio.Path.t) sources_path sources with 788 + | Ok () -> 789 + Fmt.pr "Updated sources.toml with fork entry for %s@." result.subtree_name 790 + | Error msg -> 791 + Fmt.epr "Warning: Failed to update sources.toml: %s@." msg); 772 792 Fmt.pr "Forked %d package(s): %a@." 773 793 (List.length result.packages_forked) 774 794 Fmt.(list ~sep:(any ", ") string) result.packages_forked; ··· 1345 1365 let info = Cmd.info "feature" ~doc ~man in 1346 1366 Cmd.group info [ feature_add_cmd; feature_remove_cmd; feature_list_cmd ] 1347 1367 1368 + (* Devcontainer command *) 1369 + 1370 + let default_devcontainer_url = 1371 + "https://raw.githubusercontent.com/avsm/claude-ocaml-devcontainer/refs/heads/main/.devcontainer/devcontainer.json" 1372 + 1373 + let devcontainer_cmd = 1374 + let doc = "Setup and enter a devcontainer environment" in 1375 + let man = 1376 + [ 1377 + `S Manpage.s_description; 1378 + `P 1379 + "Creates and enters a devcontainer environment for OCaml development \ 1380 + with monopam and Claude. If the target directory doesn't have a \ 1381 + .devcontainer configuration, it will be created automatically."; 1382 + `P 1383 + "This is the recommended way to get started with monopam. The \ 1384 + devcontainer provides a consistent environment with OCaml, opam, \ 1385 + and all required tools pre-installed."; 1386 + `S "WHAT IT DOES"; 1387 + `P "For a new directory (no .devcontainer/):"; 1388 + `I ("1.", "Creates the target directory if needed"); 1389 + `I ("2.", "Creates .devcontainer/ subdirectory"); 1390 + `I ("3.", "Downloads devcontainer.json from the template repository"); 1391 + `I ("4.", "Builds and starts the devcontainer"); 1392 + `I ("5.", "Opens an interactive shell inside the container"); 1393 + `P "For an existing directory with .devcontainer/:"; 1394 + `I ("1.", "Starts the devcontainer if not running"); 1395 + `I ("2.", "Opens an interactive shell inside the container"); 1396 + `S Manpage.s_options; 1397 + `P "Use $(b,--url) to specify a custom devcontainer.json URL if you want \ 1398 + to use a different base configuration."; 1399 + `S Manpage.s_examples; 1400 + `P "Create a new devcontainer workspace:"; 1401 + `Pre "monopam devcontainer ~/my-ocaml-project"; 1402 + `P "Enter an existing devcontainer:"; 1403 + `Pre "monopam devcontainer ~/my-ocaml-project"; 1404 + `P "Use a custom devcontainer.json:"; 1405 + `Pre "monopam devcontainer --url https://example.com/devcontainer.json ~/project"; 1406 + ] 1407 + in 1408 + let info = Cmd.info "devcontainer" ~doc ~man in 1409 + let path_arg = 1410 + let doc = "Target directory for the devcontainer workspace." in 1411 + Arg.(required & pos 0 (some string) None & info [] ~docv:"PATH" ~doc) 1412 + in 1413 + let url_arg = 1414 + let doc = "URL to fetch devcontainer.json from. Defaults to the claude-ocaml-devcontainer template." in 1415 + Arg.(value & opt string default_devcontainer_url & info ["url"] ~docv:"URL" ~doc) 1416 + in 1417 + let run path url () = 1418 + (* Resolve to absolute path *) 1419 + let abs_path = 1420 + if Filename.is_relative path then 1421 + Filename.concat (Sys.getcwd ()) path 1422 + else path 1423 + in 1424 + let devcontainer_dir = Filename.concat abs_path ".devcontainer" in 1425 + let devcontainer_json = Filename.concat devcontainer_dir "devcontainer.json" in 1426 + (* Check if .devcontainer exists *) 1427 + let needs_init = not (Sys.file_exists devcontainer_dir && Sys.is_directory devcontainer_dir) in 1428 + if needs_init then begin 1429 + Fmt.pr "Initializing devcontainer in %s...@." abs_path; 1430 + (* Create directories *) 1431 + (try Unix.mkdir abs_path 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ()); 1432 + (try Unix.mkdir devcontainer_dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ()); 1433 + (* Fetch devcontainer.json using curl *) 1434 + Fmt.pr "Fetching devcontainer.json from %s...@." url; 1435 + let curl_cmd = Printf.sprintf "curl -fsSL '%s' -o '%s'" url devcontainer_json in 1436 + let ret = Sys.command curl_cmd in 1437 + if ret <> 0 then begin 1438 + Fmt.epr "Error: Failed to fetch devcontainer.json (curl exit code %d)@." ret; 1439 + exit 1 1440 + end; 1441 + Fmt.pr "Created %s@." devcontainer_json; 1442 + (* Build and start the devcontainer *) 1443 + Fmt.pr "Building devcontainer (this may take a while on first run)...@."; 1444 + let up_cmd = Printf.sprintf "npx @devcontainers/cli up --workspace-folder '%s' --remove-existing-container" abs_path in 1445 + let ret = Sys.command up_cmd in 1446 + if ret <> 0 then begin 1447 + Fmt.epr "Error: Failed to start devcontainer (exit code %d)@." ret; 1448 + exit 1 1449 + end 1450 + end; 1451 + (* Exec into the devcontainer *) 1452 + Fmt.pr "Entering devcontainer...@."; 1453 + let exec_cmd = Printf.sprintf "npx @devcontainers/cli exec --workspace-folder '%s' bash -l" abs_path in 1454 + let ret = Sys.command exec_cmd in 1455 + if ret <> 0 then 1456 + `Error (false, Printf.sprintf "devcontainer exec failed with code %d" ret) 1457 + else 1458 + `Ok () 1459 + in 1460 + Cmd.v info Term.(ret (const run $ path_arg $ url_arg $ logging_term)) 1461 + 1348 1462 (* Main command group *) 1349 1463 1350 1464 let main_cmd = ··· 1355 1469 `P 1356 1470 "Monopam synchronizes packages between an opam overlay repository, \ 1357 1471 individual git checkouts, and a monorepo using git subtrees."; 1472 + `P 1473 + "Monopam is designed to run inside a devcontainer that provides a \ 1474 + consistent OCaml development environment with all required tools \ 1475 + pre-installed."; 1358 1476 `S "QUICK START"; 1359 - `P "First time setup:"; 1477 + `P "Start by creating a devcontainer workspace:"; 1478 + `Pre 1479 + "monopam devcontainer ~/tangled"; 1480 + `P "Inside the devcontainer, initialize your workspace:"; 1360 1481 `Pre 1361 - "mkdir ~/tangled && cd ~/tangled\n\ 1482 + "cd ~/tangled\n\ 1362 1483 monopam verse init --handle yourname.bsky.social\n\ 1363 1484 cd mono"; 1364 1485 `P "Daily workflow:"; ··· 1440 1561 in 1441 1562 let info = Cmd.info "monopam" ~version:"%%VERSION%%" ~doc ~man in 1442 1563 Cmd.group info 1443 - [ status_cmd; diff_cmd; pull_cmd; cherrypick_cmd; sync_cmd; changes_cmd; opam_cmd; doctor_cmd; verse_cmd; feature_cmd ] 1564 + [ status_cmd; diff_cmd; pull_cmd; cherrypick_cmd; sync_cmd; changes_cmd; opam_cmd; doctor_cmd; verse_cmd; feature_cmd; devcontainer_cmd ] 1444 1565 1445 1566 let () = exit (Cmd.eval main_cmd)
+7 -12
lib/config.ml
··· 1 1 module Package_config = struct 2 - type t = { 3 - branch : string option; 4 - dev_repo : string option; (** Override dev-repo URL for vendored packages *) 5 - } 2 + type t = { branch : string option } 6 3 7 4 let branch t = t.branch 8 - let dev_repo t = t.dev_repo 9 5 10 6 let codec : t Tomlt.t = 11 7 Tomlt.( 12 8 Table.( 13 - obj (fun branch dev_repo -> { branch; dev_repo }) 9 + obj (fun branch -> { branch }) 14 10 |> opt_mem "branch" string ~enc:(fun c -> c.branch) 15 - |> opt_mem "dev_repo" string ~enc:(fun c -> c.dev_repo) 16 11 |> finish)) 17 12 end 18 13 ··· 36 31 let create ~opam_repo ~checkouts ~monorepo ?(default_branch = "main") () = 37 32 { opam_repo; checkouts; monorepo; default_branch; packages = [] } 38 33 39 - let with_package_override t ~name ?branch:branch_opt ?dev_repo:dev_repo_opt () = 34 + let with_package_override t ~name ?branch:branch_opt () = 40 35 let existing = List.assoc_opt name t.packages in 41 36 let existing_branch = Option.bind existing Package_config.branch in 42 - let existing_dev_repo = Option.bind existing Package_config.dev_repo in 43 - let new_branch = match branch_opt with Some _ -> branch_opt | None -> existing_branch in 44 - let new_dev_repo = match dev_repo_opt with Some _ -> dev_repo_opt | None -> existing_dev_repo in 45 - let pkg_config = Package_config.{ branch = new_branch; dev_repo = new_dev_repo } in 37 + let new_branch = 38 + match branch_opt with Some _ -> branch_opt | None -> existing_branch 39 + in 40 + let pkg_config = Package_config.{ branch = new_branch } in 46 41 let packages = (name, pkg_config) :: List.remove_assoc name t.packages in 47 42 { t with packages } 48 43
+4 -10
lib/config.mli
··· 14 14 15 15 val branch : t -> string option 16 16 (** [branch t] returns the branch override for this package, if set. *) 17 - 18 - val dev_repo : t -> string option 19 - (** [dev_repo t] returns the dev-repo URL override for this package, if set. 20 - Use this to override the source URL for vendored packages. *) 21 17 end 22 18 23 19 type t ··· 112 108 @param monorepo Path to the monorepo 113 109 @param default_branch Default branch to track (default: "main") *) 114 110 115 - val with_package_override : 116 - t -> name:string -> ?branch:string -> ?dev_repo:string -> unit -> t 117 - (** [with_package_override t ~name ?branch ?dev_repo ()] returns a new config 111 + val with_package_override : t -> name:string -> ?branch:string -> unit -> t 112 + (** [with_package_override t ~name ?branch ()] returns a new config 118 113 with overrides for the named package. 119 114 120 115 @param branch Override the git branch for this package 121 - @param dev_repo Override the dev-repo URL for vendored packages. 122 - Use this when you've forked someone else's project and want the opam-repo 123 - to point to your fork instead of the original. *) 116 + 117 + Note: For dev-repo URL overrides, use [sources.toml] in the monorepo root. *) 124 118 125 119 (** {1 Pretty Printing} *) 126 120
+71 -4
lib/monopam.ml
··· 245 245 in 246 246 247 247 (* URL resolution order: 248 - 1. sources.toml override 249 - 2. dune-project source/homepage 250 - 3. existing opam-repo dev-repo (fallback) *) 251 - (* URL resolution order: 252 248 1. Explicit sources.toml entry for this subtree 253 249 2. dune-project source/homepage 254 250 3. sources.toml default_url_base + subtree name *) ··· 1478 1474 if !updated > 0 then 1479 1475 Log.info (fun m -> m "Regenerated %d opam-repo entries from monorepo" !updated) 1480 1476 1477 + (** Clone monorepo and opam-repo from verse registry if they don't exist locally. 1478 + This enables `monopam sync` to work in a fresh devcontainer. *) 1479 + let clone_from_verse_if_needed ~proc ~fs ~config () = 1480 + let monorepo = Config.Paths.monorepo config in 1481 + let opam_repo = Config.Paths.opam_repo config in 1482 + let monorepo_exists = Git.is_repo ~proc ~fs monorepo in 1483 + let opam_repo_exists = Git.is_repo ~proc ~fs opam_repo in 1484 + 1485 + (* If both exist, nothing to do *) 1486 + if monorepo_exists && opam_repo_exists then Ok () 1487 + else 1488 + (* Try to load verse config to get handle *) 1489 + match Verse_config.load ~fs () with 1490 + | Error _ -> 1491 + (* No verse config - can't clone from registry *) 1492 + Log.debug (fun m -> m "No verse config found, will initialize fresh repos"); 1493 + Ok () 1494 + | Ok verse_config -> 1495 + let handle = Verse_config.handle verse_config in 1496 + Log.info (fun m -> m "Found verse config for handle: %s" handle); 1497 + (* Load registry to look up URLs *) 1498 + match Verse_registry.clone_or_pull ~proc ~fs ~config:verse_config () with 1499 + | Error msg -> 1500 + Log.warn (fun m -> m "Could not load verse registry: %s" msg); 1501 + Ok () (* Continue without cloning - will init fresh *) 1502 + | Ok registry -> 1503 + match Verse_registry.find_member registry ~handle with 1504 + | None -> 1505 + Log.warn (fun m -> m "Handle %s not found in registry" handle); 1506 + Ok () 1507 + | Some member -> 1508 + (* Clone monorepo if needed *) 1509 + let result = 1510 + if monorepo_exists then Ok () 1511 + else begin 1512 + Log.app (fun m -> m "Cloning monorepo from %s..." member.monorepo); 1513 + let url = Uri.of_string member.monorepo in 1514 + let branch = Option.value ~default:"main" member.monorepo_branch in 1515 + match Git.clone ~proc ~fs ~url ~branch monorepo with 1516 + | Ok () -> 1517 + Log.app (fun m -> m "Monorepo cloned successfully"); 1518 + Ok () 1519 + | Error e -> 1520 + Log.err (fun m -> m "Failed to clone monorepo: %a" Git.pp_error e); 1521 + Error (Git_error e) 1522 + end 1523 + in 1524 + match result with 1525 + | Error e -> Error e 1526 + | Ok () -> 1527 + (* Clone opam-repo if needed *) 1528 + if opam_repo_exists then Ok () 1529 + else begin 1530 + Log.app (fun m -> m "Cloning opam-repo from %s..." member.opamrepo); 1531 + let url = Uri.of_string member.opamrepo in 1532 + let branch = Option.value ~default:"main" member.opamrepo_branch in 1533 + match Git.clone ~proc ~fs ~url ~branch opam_repo with 1534 + | Ok () -> 1535 + Log.app (fun m -> m "Opam-repo cloned successfully"); 1536 + Ok () 1537 + | Error e -> 1538 + Log.err (fun m -> m "Failed to clone opam-repo: %a" Git.pp_error e); 1539 + Error (Git_error e) 1540 + end 1541 + 1481 1542 let sync ~proc ~fs ~config ?package ?(remote = false) ?(skip_push = false) 1482 1543 ?(skip_pull = false) () = 1483 1544 let fs_t = fs_typed fs in 1545 + 1546 + (* Clone from verse registry if repos don't exist *) 1547 + match clone_from_verse_if_needed ~proc ~fs:fs_t ~config () with 1548 + | Error e -> Error e 1549 + | Ok () -> 1550 + 1484 1551 (* Update the opam repo first - clone if needed *) 1485 1552 let opam_repo = Config.Paths.opam_repo config in 1486 1553 if (not skip_pull) && Git.is_repo ~proc ~fs:fs_t opam_repo then begin
+26 -3
lib/verse.ml
··· 419 419 packages_forked : string list; (** Package names that were forked *) 420 420 source_handle : string; (** Handle of the verse member we forked from *) 421 421 fork_url : string; (** URL of the fork *) 422 + upstream_url : string; (** Original dev-repo URL (upstream) *) 423 + subtree_name : string; (** Name for the subtree directory (derived from fork URL) *) 422 424 } 423 425 426 + (** Extract subtree name from a URL (last path component without .git suffix) *) 427 + let subtree_name_from_url url = 428 + let uri = Uri.of_string url in 429 + let path = Uri.path uri in 430 + (* Remove leading slash and .git suffix *) 431 + let path = if String.length path > 0 && path.[0] = '/' then 432 + String.sub path 1 (String.length path - 1) 433 + else path in 434 + let path = if String.ends_with ~suffix:".git" path then 435 + String.sub path 0 (String.length path - 4) 436 + else path in 437 + (* Get last component *) 438 + match String.rindex_opt path '/' with 439 + | Some i -> String.sub path (i + 1) (String.length path - i - 1) 440 + | None -> path 441 + 424 442 let pp_fork_result ppf r = 425 - Fmt.pf ppf "@[<v>Forked %d package(s) from %s:@, @[<v>%a@]@,Fork URL: %s@]" 443 + Fmt.pf ppf "@[<v>Forked %d package(s) from %s:@, @[<v>%a@]@,Fork URL: %s@,Upstream: %s@,Subtree: %s@]" 426 444 (List.length r.packages_forked) 427 445 r.source_handle 428 446 Fmt.(list ~sep:cut string) r.packages_forked 429 447 r.fork_url 448 + r.upstream_url 449 + r.subtree_name 430 450 431 451 (** Fork a package from a verse member's opam repo into your workspace. 432 452 ··· 466 486 List.filter (fun p -> Package.same_repo p pkg) pkgs 467 487 in 468 488 let pkg_names = List.map Package.name related_pkgs in 489 + (* Get upstream URL and subtree name *) 490 + let upstream_url = Uri.to_string (Package.dev_repo pkg) in 491 + let subtree_name = subtree_name_from_url fork_url in 469 492 (* Check for conflicts in user's opam-repo *) 470 493 let user_opam_repo = Verse_config.opam_repo_path config in 471 494 let conflicts = ··· 477 500 Error (Package_already_exists conflicts) 478 501 else if dry_run then 479 502 (* Dry run - just report what would be done *) 480 - Ok { packages_forked = pkg_names; source_handle = handle; fork_url } 503 + Ok { packages_forked = pkg_names; source_handle = handle; fork_url; upstream_url; subtree_name } 481 504 else begin 482 505 (* Fork each package *) 483 506 let results = ··· 504 527 | Some (Error e) -> Error e 505 528 | _ -> 506 529 let forked_names = List.filter_map (function Ok n -> Some n | Error _ -> None) results in 507 - Ok { packages_forked = forked_names; source_handle = handle; fork_url } 530 + Ok { packages_forked = forked_names; source_handle = handle; fork_url; upstream_url; subtree_name } 508 531 end
+2
lib/verse.mli
··· 154 154 packages_forked : string list; (** Package names that were forked *) 155 155 source_handle : string; (** Handle of the verse member we forked from *) 156 156 fork_url : string; (** URL of the fork *) 157 + upstream_url : string; (** Original dev-repo URL (upstream) *) 158 + subtree_name : string; (** Name for the subtree directory (derived from fork URL) *) 157 159 } 158 160 159 161 val pp_fork_result : fork_result Fmt.t
+3 -5
lib/verse_config.ml
··· 2 2 3 3 (** Package-level override for vendored packages *) 4 4 type package_override = { 5 - dev_repo : string option; (** Override dev-repo URL *) 6 5 branch : string option; (** Override branch *) 7 6 } 8 7 ··· 95 94 handle = "anil.recoil.org" 96 95 knot = "git.recoil.org" 97 96 98 - # Optional package overrides for vendored projects 97 + # Optional package overrides (branch only; URL overrides go in sources.toml) 99 98 [packages.braid] 100 - dev_repo = "git+https://github.com/avsm/braid" 99 + branch = "backport-fix" 101 100 *) 102 101 103 102 type workspace_section = { w_root : Fpath.t } ··· 123 122 let package_override_codec : package_override Tomlt.t = 124 123 Tomlt.( 125 124 Table.( 126 - obj (fun dev_repo branch -> { dev_repo; branch }) 127 - |> opt_mem "dev_repo" string ~enc:(fun p -> p.dev_repo) 125 + obj (fun branch -> { branch }) 128 126 |> opt_mem "branch" string ~enc:(fun p -> p.branch) 129 127 |> finish)) 130 128
+6 -3
lib/verse_config.mli
··· 12 12 13 13 (** {1 Types} *) 14 14 15 - (** Package-level override for vendored packages. *) 15 + (** Package-level override for vendored packages. 16 + 17 + Note: For dev-repo URL overrides, use [sources.toml] in the monorepo root instead. 18 + This type only supports branch overrides. *) 16 19 type package_override = { 17 - dev_repo : string option; (** Override dev-repo URL for opam-repo generation *) 18 20 branch : string option; (** Override git branch *) 19 21 } 20 22 ··· 38 40 Each entry is [(subtree_name, override)] where subtree_name is the 39 41 directory name in the monorepo (e.g., "braid" for mono/braid/). 40 42 41 - Use this to override dev-repo URLs for vendored packages. *) 43 + Use this to override git branches. For dev-repo URL overrides, 44 + use [sources.toml] in the monorepo root instead. *) 42 45 43 46 (** {1 Derived Paths} *) 44 47