Skip to content

Commit ee3e909

Browse files
committed
docs(agents): cli modularization architecture + implementation plan
1 parent 84dd1b5 commit ee3e909

1 file changed

Lines changed: 134 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# CLI Modularization — Architecture & Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Reduce `src/cli.cppm` (6192 lines) to a thin command-dispatch layer (≤ 500 lines) by moving every implementation concern into focused C++20 modules, with zero behavior change.
6+
7+
**Architecture:** Continue the PR-R4/PR-R5 extraction series (`2026-05-08-pm-subsystem-architecture.md`). `cli.cppm` keeps only the canonical usage screen and the `cmdline::App` dispatcher; commands move into `src/cli/cmd_*.cppm` modules grouped by CLI surface; cross-cutting CLI plumbing moves into `mcpp.cli.common` / `mcpp.cli.install_ui`; the build-orchestration core (`prepare_build`) becomes `mcpp.cli.build`; toolchain payload post-install fixups move into the toolchain domain as `mcpp.toolchain.post_install`. Every move is a strict zero-behavior-change relocation (same statement order, same messages, same exit codes), exactly like the PR-R5 precedent.
8+
9+
**Tech Stack:** C++23 named modules (GCC 16 self-host), mcpp convention-mode source globbing (`src/**/*.cppm` — no build-file edits needed), existing `mcpp test` unit suite + `tests/e2e/run_all.sh`.
10+
11+
---
12+
13+
## 1. Current state (analysis)
14+
15+
`src/cli.cppm` mixes five unrelated responsibilities in one 6192-line module:
16+
17+
| Lines (pre-refactor) | Content | Problem |
18+
|---|---|---|
19+
| 75–181 | usage screen, project/workspace root discovery | shared helpers trapped in CLI module (pm.commands keeps a private copy of `find_manifest_root` for this reason) |
20+
| 183–517 | toolchain version matching, xlings NDJSON install-progress UI | UI plumbing interleaved with command logic |
21+
| 519–1038 | fingerprint flag canonicalization, gcc/clang payload post-install fixups (patchelf, specs, cfg rewrite) | toolchain-domain logic living in the CLI layer |
22+
| 1051–3561 | `cmd_new` + templates, `BuildContext` + `prepare_build` (≈ 2 240 lines: workspace → toolchain → dependency resolution → features → modgraph → fingerprint → plan → lockfile) | the build pipeline is unreachable for unit testing and unreviewable as a diff target |
23+
| 3563–5789 | build cache/fast-path + 25 `cmd_*` entry points | every command edit rebuilds/reviews the whole module |
24+
| 5793–6192 | `run()` dispatcher | the only part that belongs here |
25+
26+
Consequences: slowest incremental rebuild unit in the repo, high merge-conflict surface, no module-level ownership boundaries, and `mcpp.cli` exports nothing reusable (`pm.commands` duplicates helpers to avoid a circular import).
27+
28+
## 2. Target architecture
29+
30+
```
31+
src/main.cpp ──▶ mcpp.cli (dispatcher only: usage + cmdline::App + run())
32+
33+
├─ mcpp.cli.cmd_build build / run / test / clean / dyndep
34+
│ └─ mcpp.cli.build BuildContext, BuildOverrides, prepare_build
35+
├─ mcpp.cli.cmd_new new + package templates
36+
├─ mcpp.cli.cmd_registry search / index *
37+
├─ mcpp.cli.cmd_cache cache list|info|prune|clean
38+
├─ mcpp.cli.cmd_toolchain toolchain install|list|default|remove
39+
│ └─ mcpp.toolchain.post_install patchelf/specs/cfg fixups
40+
├─ mcpp.cli.cmd_publish publish / pack / emit xpkg
41+
├─ mcpp.cli.cmd_self self * / doctor / why / env / explain
42+
└─ mcpp.pm.commands add / remove / update (PR-R5, unchanged)
43+
44+
shared CLI plumbing:
45+
mcpp.cli.common project/workspace root discovery, target_dir, fs size helpers
46+
mcpp.cli.install_ui xlings NDJSON → ui::DownloadProgress adapters, PathContext
47+
```
48+
49+
Layering rules (enforced by module imports — cycles are compile errors):
50+
51+
1. `mcpp.cli` imports only `cmd_*` modules (+ `pm.commands`, `ui`, `log`, `cmdline`, `toolchain.fingerprint` for the version string).
52+
2. `cmd_*` modules never import each other; shared code lives in `common` / `install_ui` / `build`.
53+
3. `mcpp.cli.build` is the single owner of `prepare_build`; consumers are `cmd_build`, `cmd_publish` (pack), `cmd_self` (doctor/why).
54+
4. `mcpp.toolchain.post_install` is CLI-independent (imports only config/xlings/platform/log/ui) so future non-CLI callers (e.g. a daemonized installer) can reuse it.
55+
5. Export surface is explicit per symbol (`export` on the declaration) — internals keep module linkage.
56+
57+
### Module inventory
58+
59+
| New file | Module | Exports | Body source (line ranges in pre-refactor `src/cli.cppm`) |
60+
|---|---|---|---|
61+
| `src/cli/common.cppm` | `mcpp.cli.common` | `find_manifest_root`, `find_workspace_root`, `merge_workspace_deps`, `target_dir`, `dir_size`, `human_bytes` | 116–181, 4630–4650 (drop `static`) |
62+
| `src/cli/install_ui.cppm` | `mcpp.cli.install_ui` | `make_path_ctx`, `make_bootstrap_progress_callback`, `CliInstallProgress` | 300–517 |
63+
| `src/toolchain/post_install.cppm` | `mcpp.toolchain.post_install` (ns `mcpp::toolchain`) | `patchelf_walk`, `fixup_clang_cfg`, `gcc_post_install_fixup` | 722–1038 |
64+
| `src/cli/build.cppm` | `mcpp.cli.build` | `BuildContext`, `BuildOverrides`, `prepare_build` | 519–720, 1321–3561 |
65+
| `src/cli/cmd_build.cppm` | `mcpp.cli.cmd_build` | `cmd_build`, `cmd_run`, `cmd_test`, `cmd_clean`, `cmd_dyndep` | 3563–3976, 4406–4570, 4608–4627, 5741–5789 |
66+
| `src/cli/cmd_new.cppm` | `mcpp.cli.cmd_new` | `cmd_new` | 1051–1319 |
67+
| `src/cli/cmd_registry.cppm` | `mcpp.cli.cmd_registry` | `cmd_search`, `cmd_index_{list,add,remove,update,pin,unpin}` | 3996–4402 |
68+
| `src/cli/cmd_cache.cppm` | `mcpp.cli.cmd_cache` | `cmd_cache_{list,info,prune,clean}` | 4837–4976 |
69+
| `src/cli/cmd_toolchain.cppm` | `mcpp.cli.cmd_toolchain` | `cmd_toolchain` | 183–298, 4978–5317 |
70+
| `src/cli/cmd_publish.cppm` | `mcpp.cli.cmd_publish` | `cmd_publish`, `cmd_pack`, `cmd_emit_xpkg` | 4572–4606, 5319–5571 |
71+
| `src/cli/cmd_self.cppm` | `mcpp.cli.cmd_self` | `cmd_env`, `cmd_doctor`, `cmd_why`, `cmd_explain`, `cmd_explain_action`, `cmd_self_{version,init,config}` | 3978–3994, 4652–4835, 5573–5739 |
72+
73+
`src/cli.cppm` keeps: file header, `print_usage` (75–114), `run()` (5795–6190, minus the now-dead `using namespace mcpp::cli::detail;`). All moved code switches namespace `mcpp::cli::detail``mcpp::cli` (or `mcpp::toolchain` for post_install); unqualified cross-references keep working because callers share the namespace. The three post-install call sites outside the toolchain namespace gain explicit `mcpp::toolchain::` qualification.
74+
75+
### Cross-platform notes
76+
77+
- No platform-conditional code is touched; `if constexpr (mcpp::platform::is_macos/…)` blocks move verbatim, so Linux/macOS/Windows behavior is bit-identical.
78+
- Source discovery is glob-based (`src/**/*`), so the new `src/cli/` directory needs no manifest change on any platform.
79+
- Each new module keeps the `module; #include <cstdio> #include <cstdlib>` global-module-fragment prologue `cli.cppm` used (for `stderr`/`stdout` macros) — required on all three toolchains.
80+
81+
## 3. Implementation plan
82+
83+
The extraction is a single mechanical transformation of one immutable source revision, so it is scripted with `sed -n 'A,Bp'` range extraction (no hand-retyping; byte-identical bodies), then compiled and tested. Verification = full self-host build + unit suite + e2e suite.
84+
85+
### Task 1: Architecture doc (this file)
86+
87+
- [x] **Step 1:** Write this document.
88+
- [x] **Step 2:** Commit on branch `refactor/cli-modularization`.
89+
90+
### Task 2: Scripted extraction
91+
92+
- [ ] **Step 1:** `cp src/cli.cppm /tmp/cli_orig.cppm` (immutable line-range source).
93+
- [ ] **Step 2:** For each row of the module inventory: write the module header (GMF prologue, `export module`, imports per §2, `namespace … {`), then `sed -n 'A,Bp' /tmp/cli_orig.cppm >>` the body ranges in table order, then close the namespace. Import lists per module:
94+
- common: `std, mcpp.manifest, mcpp.toolchain.detect, mcpp.toolchain.fingerprint`
95+
- install_ui: `std, mcpp.ui, mcpp.log, mcpp.config, mcpp.fetcher`
96+
- post_install: `std, mcpp.config, mcpp.xlings, mcpp.platform, mcpp.log, mcpp.ui`
97+
- build: `std, mcpp.libs.json, mcpp.manifest, mcpp.modgraph.{graph,scanner,validate}, mcpp.toolchain.{clang,detect,fingerprint,registry,stdmod}, mcpp.toolchain.post_install, mcpp.build.plan, mcpp.lockfile, mcpp.config, mcpp.xlings, mcpp.platform, mcpp.fetcher, mcpp.pm.{resolver,index_spec,mangle,compat,dep_spec}, mcpp.ui, mcpp.log, mcpp.fallback.install_integrity, mcpp.bmi_cache, mcpp.cli.common, mcpp.cli.install_ui`
98+
- cmd_build: `std, mcpplibs.cmdline, mcpp.cli.{build,common,install_ui}, mcpp.build.{plan,backend,ninja}, mcpp.bmi_cache, mcpp.dyndep, mcpp.manifest, mcpp.modgraph.scanner, mcpp.toolchain.stdmod, mcpp.xlings, mcpp.platform, mcpp.ui, mcpp.log`
99+
- cmd_new: `std, mcpplibs.cmdline, mcpp.cli.install_ui, mcpp.scaffold, mcpp.config, mcpp.manifest, mcpp.fetcher, mcpp.pm.resolver, mcpp.ui`
100+
- cmd_registry: `std, mcpplibs.cmdline, mcpp.cli.{common,install_ui}, mcpp.config, mcpp.xlings, mcpp.fetcher, mcpp.manifest, mcpp.lockfile, mcpp.ui`
101+
- cmd_cache: `std, mcpplibs.cmdline, mcpp.cli.common, mcpp.toolchain.stdmod, mcpp.ui`
102+
- cmd_toolchain: `std, mcpplibs.cmdline, mcpp.cli.{common,install_ui}, mcpp.toolchain.{detect,registry,post_install}, mcpp.config, mcpp.xlings, mcpp.fetcher, mcpp.manifest, mcpp.ui, mcpp.log`
103+
- cmd_publish: `std, mcpplibs.cmdline, mcpp.cli.{common,build,install_ui}, mcpp.manifest, mcpp.modgraph.scanner, mcpp.publish.xpkg_emit, mcpp.pack, mcpp.build.{backend,ninja}, mcpp.config, mcpp.platform, mcpp.ui`
104+
- cmd_self: `std, mcpplibs.cmdline, mcpp.cli.{common,build,install_ui}, mcpp.config, mcpp.xlings, mcpp.fallback.install_integrity, mcpp.toolchain.{detect,fingerprint,stdmod}, mcpp.build.plan, mcpp.ui`
105+
- [ ] **Step 3:** Rewrite `src/cli.cppm`: header + imports (`std, mcpplibs.cmdline, mcpp.ui, mcpp.log, mcpp.toolchain.fingerprint, mcpp.pm.commands, mcpp.cli.cmd_*×7`) + exported `run()` decl + `print_usage` + `run()` body (drop the `using namespace …::detail;` line).
106+
- [ ] **Step 4:** Add `export` keywords on the symbols listed in §2 (Edit per declaration); drop `static` from `dir_size`/`human_bytes`; qualify the three `mcpp::toolchain::` post-install call sites (one in build.cppm: `gcc_post_install_fixup`; two-plus-one in cmd_toolchain.cppm: `gcc_post_install_fixup`, `patchelf_walk`, `fixup_clang_cfg`).
107+
108+
### Task 3: Build & fix loop
109+
110+
- [ ] **Step 1:** `mcpp build` — fix any missing-import / linkage errors (expected failure mode: a helper referenced across modules that wasn't exported; fix = add `export` or import, never duplicate code).
111+
- [ ] **Step 2:** `wc -l src/cli.cppm` — expected < 500.
112+
- [ ] **Step 3:** Commit `refactor(cli): split cli.cppm into focused modules`.
113+
114+
### Task 4: Verification
115+
116+
- [ ] **Step 1:** `mcpp test` with the freshly built binary — expect all unit tests pass.
117+
- [ ] **Step 2:** `MCPP=<fresh binary> tests/e2e/run_all.sh` — expect all e2e tests pass (covers help/version text, exit codes 0/1/2/127, all command surfaces).
118+
- [ ] **Step 3:** Sanity: `mcpp --help`, `mcpp version`, `mcpp self doctor`, unknown-command exit 127.
119+
120+
### Task 5: PR + CI
121+
122+
- [ ] **Step 1:** Push branch, open PR describing motivation, module map, zero-behavior-change guarantee, verification evidence.
123+
- [ ] **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).
124+
- [ ] **Step 3:** Update this doc's status section.
125+
126+
## 4. Follow-ups (out of scope here)
127+
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).
131+
132+
## 5. Status
133+
134+
- 2026-06-10: doc written; extraction in progress.

0 commit comments

Comments
 (0)