|
| 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