A monorepo management tool for the agentic ages
1(** Project initialization for unpac.
2
3 Creates the bare repository structure and initial main worktree. *)
4
5let default_unpac_toml = {|[opam]
6repositories = []
7# compiler = "5.4.0"
8
9# Vendor cache location (default: XDG cache directory)
10# vendor_cache = "/path/to/cache"
11
12[projects]
13# Projects will be added here
14|}
15
16let project_dune_project name = Printf.sprintf {|(lang dune 3.20)
17(name %s)
18|} name
19
20let project_dune = {|(vendored_dirs vendor)
21|}
22
23let project_gitignore = {|_build/
24*.install
25|}
26
27let vendor_dune = {|(vendored_dirs opam)
28|}
29
30(** Initialize a new unpac project at the given path. *)
31let init ~proc_mgr ~fs path =
32 (* Convert relative paths to absolute *)
33 let abs_path =
34 if Filename.is_relative path then
35 Filename.concat (Sys.getcwd ()) path
36 else path
37 in
38 let root = Eio.Path.(fs / abs_path) in
39
40 (* Create root directory *)
41 Eio.Path.mkdirs ~exists_ok:false ~perm:0o755 root;
42
43 (* Initialize bare repository *)
44 let git_path = Eio.Path.(root / "git") in
45 Eio.Path.mkdirs ~exists_ok:false ~perm:0o755 git_path;
46 Git.run_exn ~proc_mgr ~cwd:git_path ["init"; "--bare"] |> ignore;
47
48 (* Create initial main branch with unpac.toml *)
49 (* First create a temporary worktree to make the initial commit *)
50 let main_path = Eio.Path.(root / "main") in
51 Eio.Path.mkdirs ~exists_ok:false ~perm:0o755 main_path;
52
53 (* Initialize as a regular repo temporarily to create first commit *)
54 Git.run_exn ~proc_mgr ~cwd:main_path ["init"] |> ignore;
55
56 (* Write unpac.toml *)
57 Eio.Path.save ~create:(`Or_truncate 0o644)
58 Eio.Path.(main_path / "unpac.toml")
59 default_unpac_toml;
60
61 (* Create initial commit *)
62 Git.run_exn ~proc_mgr ~cwd:main_path ["add"; "unpac.toml"] |> ignore;
63 Git.run_exn ~proc_mgr ~cwd:main_path
64 ["commit"; "-m"; "Initial commit"] |> ignore;
65
66 (* Rename branch to main if needed *)
67 Git.run_exn ~proc_mgr ~cwd:main_path ["branch"; "-M"; "main"] |> ignore;
68
69 (* Push to bare repo and convert to worktree *)
70 Git.run_exn ~proc_mgr ~cwd:main_path
71 ["remote"; "add"; "origin"; "../git"] |> ignore;
72 Git.run_exn ~proc_mgr ~cwd:main_path
73 ["push"; "-u"; "origin"; "main"] |> ignore;
74
75 (* Remove the temporary clone and add main as a worktree of the bare repo *)
76 Eio.Path.rmtree main_path;
77
78 (* Add main as a worktree of the bare repo *)
79 Git.run_exn ~proc_mgr ~cwd:git_path
80 ["worktree"; "add"; "../main"; "main"] |> ignore;
81
82 root
83
84(** Check if a path is an unpac project root. *)
85let is_unpac_root path =
86 Eio.Path.is_directory Eio.Path.(path / "git") &&
87 Eio.Path.is_directory Eio.Path.(path / "main") &&
88 Eio.Path.is_file Eio.Path.(path / "main" / "unpac.toml")
89
90(** Find the unpac root by walking up from current directory. *)
91let find_root ~fs ~cwd =
92 let rec go path =
93 if is_unpac_root path then Some path
94 else match Eio.Path.split path with
95 | Some (parent, _) -> go parent
96 | None -> None
97 in
98 go Eio.Path.(fs / cwd)
99
100(** Create a new project branch with template. *)
101let create_project ~proc_mgr root name =
102 let project_path = Worktree.path root (Project name) in
103
104 (* Ensure project directory parent exists *)
105 let project_dir = Eio.Path.(root / "project") in
106 Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 project_dir;
107
108 (* Create orphan branch *)
109 Worktree.ensure_orphan ~proc_mgr root (Project name);
110
111 (* Write template files *)
112 Eio.Path.save ~create:(`Or_truncate 0o644)
113 Eio.Path.(project_path / "dune-project")
114 (project_dune_project name);
115
116 Eio.Path.save ~create:(`Or_truncate 0o644)
117 Eio.Path.(project_path / "dune")
118 project_dune;
119
120 Eio.Path.save ~create:(`Or_truncate 0o644)
121 Eio.Path.(project_path / ".gitignore")
122 project_gitignore;
123
124 (* Create vendor directory structure with dune file *)
125 Eio.Path.mkdirs ~exists_ok:true ~perm:0o755
126 Eio.Path.(project_path / "vendor" / "opam");
127
128 Eio.Path.save ~create:(`Or_truncate 0o644)
129 Eio.Path.(project_path / "vendor" / "dune")
130 vendor_dune;
131
132 (* Commit template *)
133 Git.run_exn ~proc_mgr ~cwd:project_path ["add"; "-A"] |> ignore;
134 Git.run_exn ~proc_mgr ~cwd:project_path
135 ["commit"; "-m"; "Initialize project " ^ name] |> ignore;
136
137 (* Update main/unpac.toml to register project *)
138 let main_path = Worktree.path root Main in
139 let toml_path = Eio.Path.(main_path / "unpac.toml") in
140 let content = Eio.Path.load toml_path in
141
142 (* Simple append to [projects] section - a proper implementation would parse TOML *)
143 let updated =
144 if content = "" || not (String.ends_with ~suffix:"\n" content)
145 then content ^ "\n" ^ name ^ " = {}\n"
146 else content ^ name ^ " = {}\n"
147 in
148 Eio.Path.save ~create:(`Or_truncate 0o644) toml_path updated;
149
150 Git.run_exn ~proc_mgr ~cwd:main_path ["add"; "unpac.toml"] |> ignore;
151 Git.run_exn ~proc_mgr ~cwd:main_path
152 ["commit"; "-m"; "Add project " ^ name] |> ignore;
153
154 project_path
155
156(** Remove a project branch and worktree. *)
157let remove_project ~proc_mgr root name =
158 (* Remove worktree if exists *)
159 Worktree.remove_force ~proc_mgr root (Project name);
160
161 (* Delete the branch *)
162 let git = Worktree.git_dir root in
163 let branch = Worktree.branch (Project name) in
164 Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; branch] |> ignore