Skip to content

Commit 7a1d560

Browse files
committed
refactor(arch): relocate domain logic out of the cli layer (cli = parse + route)
Phase 2 of the cli modularization: every implementation now lives in its owning subsystem; cli/cmd_* modules contain only argument handling, CLI-shape validation and routing (no ParsedArgs below the cli layer). mcpp.project manifest/workspace discovery (pm.commands' private copy folded in) mcpp.fetcher.progress xlings NDJSON -> ui adapters (InstallProgressHandler) mcpp.build.prepare BuildContext + prepare_build mcpp.build.execute build cache/fast-path, run_build_plan, run/test/clean mcpp.toolchain.manager toolchain list/install/set-default/remove mcpp.pm.index_ops search + index list/add/remove/update/pin/unpin mcpp.bmi_cache.ops cache walk/list/info/prune/clean + fs size helpers mcpp.scaffold.ops package templates + builtin project creation mcpp.publish.ops publish pipeline + emit xpkg mcpp.pack.ops pack orchestration mcpp.doctor doctor/why/env/explain + self init/config mcpp.cli.{common,install_ui,build} are gone; the whole cli layer is now cli.cppm (481) + seven adapters (450 lines total). Bodies moved verbatim (zero behavior change). Doc: .agents/docs/2026-06-10-cli-modularization.md
1 parent 8f76103 commit 7a1d560

21 files changed

Lines changed: 2759 additions & 2524 deletions

.agents/docs/2026-06-10-cli-modularization.md

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,54 @@ The extraction is a single mechanical transformation of one immutable source rev
123123
- [x] **Step 2:** Watch ci-linux / ci-macos / ci-windows / fresh-install lanes; fix-forward on any platform-specific module issue (most likely candidate: MSVC/clang module-linkage strictness on exported-vs-internal helpers).
124124
- [x] **Step 3:** Update this doc's status section.
125125

126-
## 4. Follow-ups (out of scope here)
126+
## 4. Phase 2 — domain relocation (cli = parse + route only)
127127

128-
- Decompose `prepare_build` internally (workspace / toolchain / dep-resolution / feature phases as named functions) now that it has a home module.
129-
- Fold `pm.commands`' private `find_manifest_root` copy into a shared project-location module once a `cli`-independent home exists (`mcpp.cli.common` is still CLI-layer; a `mcpp.project` module would let pm import it without layering violations).
130-
- Tighten `mcpp.cli.build`'s import list (it inherited the union of the old `cli.cppm` imports).
128+
Phase 1 made `cli.cppm` a dispatcher but left implementations in `mcpp.cli.*`
129+
modules. Phase 2 finishes the architecture: every implementation lives in its
130+
owning subsystem, and `cli/cmd_*` modules contain ONLY argument handling,
131+
validation of CLI shapes, and routing. Relocation map (bodies verbatim again):
131132

132-
## 5. Status
133+
| Phase-1 location | Phase-2 owner | Contents |
134+
|---|---|---|
135+
| `mcpp.cli.common` | `mcpp.project` (`src/project.cppm`) | `find_manifest_root`, `find_workspace_root`, `merge_workspace_deps` (also folds the private copy `pm.commands` kept) |
136+
| `mcpp.cli.common` | `mcpp.bmi_cache.ops` | `dir_size`, `human_bytes` |
137+
| `mcpp.cli.common` | `mcpp.build.prepare` (internal) | `target_dir` |
138+
| `mcpp.cli.install_ui` | `mcpp.fetcher.progress` (`src/fetcher/progress.cppm`) | NDJSON→ui adapters (`CliInstallProgress``InstallProgressHandler`), `make_bootstrap_progress_callback`, `make_path_ctx` |
139+
| `mcpp.cli.build` | `mcpp.build.prepare` (`src/build/prepare.cppm`) | `BuildContext`, `BuildOverrides`, `prepare_build` |
140+
| `mcpp.cli.cmd_build` | `mcpp.build.execute` (`src/build/execute.cppm`) | build cache + fast path, `run_build_plan`, `try_fast_build`, `build_run_target`, `run_tests`, `clean_project` |
141+
| `mcpp.cli.cmd_toolchain` | `mcpp.toolchain.manager` (`src/toolchain/manager.cppm`) | version matching + `toolchain_list/install/set_default/remove` |
142+
| `mcpp.cli.cmd_registry` | `mcpp.pm.index_ops` (`src/pm/index_ops.cppm`) | `search_packages`, `index_list/add/remove/update/pin/unpin` |
143+
| `mcpp.cli.cmd_cache` | `mcpp.bmi_cache.ops` (`src/bmi_cache/ops.cppm`) | `cache_list/info/prune/clean` |
144+
| `mcpp.cli.cmd_new` | `mcpp.scaffold.ops` (`src/scaffold/ops.cppm`) | template fetch/instantiate + `create_builtin_project` |
145+
| `mcpp.cli.cmd_publish` | `mcpp.publish.ops` (`src/publish/ops.cppm`) | `publish_package`, `emit_xpkg_to` |
146+
| `mcpp.cli.cmd_publish` | `mcpp.pack.ops` (`src/pack/ops.cppm`) | `build_and_pack` |
147+
| `mcpp.cli.cmd_self` | `mcpp.doctor` (`src/doctor.cppm`) | `env_report`, `doctor_report`, `why_report`, `explain_code`, `self_init`, `self_config` |
148+
149+
Resulting cli layer: `cli.cppm` (dispatcher, 481) + seven `cmd_*` adapters
150+
totalling ~450 lines, none containing domain logic. Domain ops take plain
151+
typed parameters (never `ParsedArgs`); `mcpplibs.cmdline` is imported only by
152+
the cli layer. The split rule for each command: CLI-shape validation and
153+
usage errors stay in the adapter; everything after lives in the domain op
154+
with identical statements, messages and exit codes.
155+
156+
## 5. Follow-ups (out of scope here)
157+
158+
- Decompose `prepare_build` internally (workspace / toolchain / dep-resolution / feature phases) now that it lives in `mcpp.build.prepare`.
159+
- Tighten `mcpp.build.prepare`'s import list (it inherited the union of the old `cli.cppm` imports).
160+
- `pm.commands` still takes `ParsedArgs` (it predates the parse/route rule); migrating add/remove/update bodies behind typed ops would complete the pattern.
161+
162+
## 6. Status
133163

134164
- 2026-06-10: extraction done. `cli.cppm` 6192 -> 481 lines; 11 new modules.
135165
Verified: self-host build clean; unit suite 18/18 pass; e2e 67 pass /
136166
1 skip, with the only failures being the 6 `llvm_*` tests that fail
137167
identically with the pre-refactor baseline binary on the same host
138168
(local LLVM payload cannot exec — environment issue, not a regression).
139-
PR opened; awaiting CI.
169+
PR opened; CI green on linux/windows/macos.
170+
- 2026-06-10 (phase 2): domain relocation executed — implementations moved out
171+
of `mcpp.cli.*` into `mcpp.project`, `mcpp.fetcher.progress`,
172+
`mcpp.build.{prepare,execute}`, `mcpp.toolchain.manager`,
173+
`mcpp.pm.index_ops`, `mcpp.bmi_cache.ops`, `mcpp.scaffold.ops`,
174+
`mcpp.publish.ops`, `mcpp.pack.ops`, `mcpp.doctor`; `cli/cmd_*` reduced to
175+
parse + route adapters (~450 lines total). Self-host build + 18/18 unit
176+
tests pass; e2e parity with baseline re-verified.

src/bmi_cache/ops.cppm

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// mcpp.bmi_cache.ops — global BMI cache inspection + pruning, and the
2+
// shared fs-size/byte-formatting helpers they are built on.
3+
// Bodies moved verbatim from the CLI layer. Zero behavior change.
4+
5+
module;
6+
#include <cstdio>
7+
#include <cstdlib>
8+
9+
export module mcpp.bmi_cache.ops;
10+
11+
import std;
12+
import mcpp.toolchain.stdmod;
13+
import mcpp.ui;
14+
15+
namespace mcpp::bmi_cache {
16+
17+
18+
export std::uintmax_t dir_size(const std::filesystem::path& p) {
19+
std::error_code ec;
20+
if (!std::filesystem::exists(p, ec)) return 0;
21+
std::uintmax_t total = 0;
22+
for (auto& e : std::filesystem::recursive_directory_iterator(p, ec)) {
23+
if (ec) break;
24+
std::error_code ec2;
25+
if (e.is_regular_file(ec2) && !ec2) {
26+
total += e.file_size(ec2);
27+
}
28+
}
29+
return total;
30+
}
31+
32+
export std::string human_bytes(std::uintmax_t n) {
33+
constexpr const char* units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
34+
double v = static_cast<double>(n);
35+
int u = 0;
36+
while (v >= 1024.0 && u < 4) { v /= 1024.0; ++u; }
37+
return std::format("{:.1f} {}", v, units[u]);
38+
}
39+
40+
41+
// ─── M4 #4: mcpp cache list / prune / clean / info ──────────────────────
42+
struct CacheEntry {
43+
std::filesystem::path dir;
44+
std::string fingerprint;
45+
std::string pkgAtVer; // "<idx>/<pkg>@<ver>"
46+
std::uintmax_t size = 0;
47+
std::filesystem::file_time_type lastWrite{};
48+
std::size_t fileCount = 0;
49+
};
50+
51+
static std::vector<CacheEntry> walk_cache_entries() {
52+
std::vector<CacheEntry> entries;
53+
auto bmi = mcpp::toolchain::default_cache_root();
54+
std::error_code ec;
55+
if (!std::filesystem::exists(bmi, ec)) return entries;
56+
57+
for (auto& fpEntry : std::filesystem::directory_iterator(bmi, ec)) {
58+
auto fpDir = fpEntry.path();
59+
auto depsDir = fpDir / "deps";
60+
if (!std::filesystem::exists(depsDir, ec)) continue;
61+
for (auto& idxEntry : std::filesystem::directory_iterator(depsDir, ec)) {
62+
for (auto& pkgEntry : std::filesystem::directory_iterator(idxEntry.path(), ec)) {
63+
CacheEntry e;
64+
e.dir = pkgEntry.path();
65+
e.fingerprint = fpDir.filename().string();
66+
e.pkgAtVer = idxEntry.path().filename().string()
67+
+ "/" + pkgEntry.path().filename().string();
68+
e.size = dir_size(e.dir);
69+
e.lastWrite = std::filesystem::last_write_time(e.dir, ec);
70+
for (auto& _ : std::filesystem::recursive_directory_iterator(e.dir, ec)) {
71+
if (!ec) ++e.fileCount;
72+
}
73+
entries.push_back(std::move(e));
74+
}
75+
}
76+
}
77+
return entries;
78+
}
79+
80+
static std::string format_age(std::filesystem::file_time_type t) {
81+
auto now = std::chrono::file_clock::now();
82+
auto diff = std::chrono::duration_cast<std::chrono::seconds>(now - t).count();
83+
if (diff < 60) return std::format("{}s ago", diff);
84+
if (diff < 3600) return std::format("{}m ago", diff / 60);
85+
if (diff < 86400) return std::format("{}h ago", diff / 3600);
86+
return std::format("{}d ago", diff / 86400);
87+
}
88+
89+
// `mcpp cache` is dispatched at the App level — list / info / prune / clean
90+
// each get their own action lambda invoking one of these helpers.
91+
92+
// `mcpp cache list`.
93+
export int cache_list() {
94+
auto entries = walk_cache_entries();
95+
if (entries.empty()) {
96+
std::println("(BMI cache is empty)");
97+
return 0;
98+
}
99+
std::println("{:<18} {:>10} {:>14} {}",
100+
"fingerprint", "size", "last accessed", "package");
101+
for (auto& e : entries) {
102+
auto fp = e.fingerprint.size() > 16
103+
? e.fingerprint.substr(0, 16) : e.fingerprint;
104+
std::println("{:<18} {:>10} {:>14} {}",
105+
fp, human_bytes(e.size), format_age(e.lastWrite), e.pkgAtVer);
106+
}
107+
return 0;
108+
}
109+
110+
// `mcpp cache info <pkg>@<ver>`.
111+
export int cache_info(const std::string& needle) {
112+
auto entries = walk_cache_entries();
113+
for (auto& e : entries) {
114+
if (e.pkgAtVer.ends_with(needle)) {
115+
std::println("dir = {}", e.dir.string());
116+
std::println("fingerprint = {}", e.fingerprint);
117+
std::println("package = {}", e.pkgAtVer);
118+
std::println("size = {}", human_bytes(e.size));
119+
std::println("file count = {}", e.fileCount);
120+
std::println("last write = {}", format_age(e.lastWrite));
121+
return 0;
122+
}
123+
}
124+
std::println("no cache entry matching '{}'", needle);
125+
return 1;
126+
}
127+
128+
// `mcpp cache prune --older-than <N>{s,m,h,d}` (v = raw option value).
129+
export int cache_prune(const std::string& v) {
130+
if (v.empty()) {
131+
mcpp::ui::error("`mcpp cache prune` requires --older-than <N>{s,m,h,d}");
132+
return 2;
133+
}
134+
char unit = v.back();
135+
long long n = 0;
136+
try { n = std::stoll(v.substr(0, v.size() - 1)); }
137+
catch (...) { mcpp::ui::error(std::format("bad --older-than value '{}'", v)); return 2; }
138+
std::chrono::seconds threshold{0};
139+
if (unit == 's') threshold = std::chrono::seconds(n);
140+
else if (unit == 'm') threshold = std::chrono::seconds(n * 60);
141+
else if (unit == 'h') threshold = std::chrono::seconds(n * 3600);
142+
else if (unit == 'd') threshold = std::chrono::seconds(n * 86400);
143+
else { mcpp::ui::error(std::format("bad time unit '{}': use s/m/h/d", unit)); return 2; }
144+
auto cutoff = std::chrono::file_clock::now() - threshold;
145+
auto entries = walk_cache_entries();
146+
int removed = 0;
147+
std::uintmax_t freed = 0;
148+
for (auto& e : entries) {
149+
if (e.lastWrite < cutoff) {
150+
std::error_code ec;
151+
std::filesystem::remove_all(e.dir, ec);
152+
if (!ec) {
153+
++removed;
154+
freed += e.size;
155+
mcpp::ui::status("Pruned",
156+
std::format("{} ({})", e.pkgAtVer, human_bytes(e.size)));
157+
}
158+
}
159+
}
160+
std::println("");
161+
std::println("Pruned {} entries, freed {}", removed, human_bytes(freed));
162+
return 0;
163+
}
164+
165+
// `mcpp cache clean` — drop dep entries, preserve std BMIs.
166+
export int cache_clean() {
167+
auto bmi = mcpp::toolchain::default_cache_root();
168+
std::error_code ec;
169+
std::filesystem::remove_all(bmi / "deps", ec); // deps only; preserve std.gcm
170+
if (std::filesystem::exists(bmi)) {
171+
for (auto& f : std::filesystem::directory_iterator(bmi, ec)) {
172+
auto deps = f.path() / "deps";
173+
std::filesystem::remove_all(deps, ec);
174+
}
175+
}
176+
std::println("Cleaned all dep BMI cache entries (std.gcm preserved)");
177+
return 0;
178+
}
179+
180+
} // namespace mcpp::bmi_cache

0 commit comments

Comments
 (0)