Skip to content

Commit d97ab2f

Browse files
committed
refactor(pm): move package CLI commands into pm/commands.cppm (PR-R5)
Step five of the package-management subsystem refactor (see `.agents/docs/2026-05-08-pm-subsystem-architecture.md`). Strictly zero behavior change. * New module `mcpp.pm.commands` (`src/pm/commands.cppm`) hosts the three package-management CLI commands previously in `cli.cppm`'s detail namespace: `cmd_add`, `cmd_remove`, `cmd_update`. Bodies are identical line-for-line. * `find_manifest_root` is duplicated as a local helper in `mcpp::pm::commands::detail` to avoid a circular `mcpp.cli` ↔ `mcpp.pm.commands` dependency. A future "shared project utilities" module can collapse the duplicate; tracked. * `cli.cppm` drops the three implementations (~280 lines) and the command dispatch table now binds `mcpp::pm::commands::cmd_add`, `::cmd_remove`, `::cmd_update`. Verification: * `mcpp build` compiles unchanged. * `mcpp test` — 9/9 unit binaries pass. * e2e subset (02 / 09 / 12 / 23 / 27) all pass — 12 and 23 specifically exercise the moved commands.
1 parent 4a038a0 commit d97ab2f

2 files changed

Lines changed: 297 additions & 249 deletions

File tree

src/cli.cppm

Lines changed: 6 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import mcpp.pack;
3131
import mcpp.config;
3232
import mcpp.fetcher;
3333
import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm
34+
import mcpp.pm.commands; // PR-R5: cmd_add / cmd_remove / cmd_update live here now
3435
import mcpp.ui;
3536
import mcpp.bmi_cache;
3637
import mcpp.dyndep;
@@ -1706,84 +1707,7 @@ int cmd_index_update(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) {
17061707
return 0;
17071708
}
17081709

1709-
int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) {
1710-
std::string spec = parsed.positional(0);
1711-
if (spec.empty()) {
1712-
mcpp::ui::error("usage: mcpp add [<ns>:]<pkg>[@<ver>]");
1713-
return 2;
1714-
}
1715-
1716-
// Split @<version> tail.
1717-
std::string nameSpec, version;
1718-
if (auto at = spec.find('@'); at == std::string::npos) {
1719-
nameSpec = spec;
1720-
} else {
1721-
nameSpec = spec.substr(0, at);
1722-
version = spec.substr(at + 1);
1723-
}
1724-
1725-
// Split <ns>:<name>. xpkg-style namespace separator. Bare `name` keeps
1726-
// the default namespace (mcpp); legacy `ns.name` is also accepted on
1727-
// input for ergonomics, but written out in the new subtable form.
1728-
std::string ns, shortName;
1729-
if (auto col = nameSpec.find(':'); col != std::string::npos) {
1730-
ns = nameSpec.substr(0, col);
1731-
shortName = nameSpec.substr(col + 1);
1732-
} else if (auto dot = nameSpec.find('.'); dot != std::string::npos) {
1733-
ns = nameSpec.substr(0, dot);
1734-
shortName = nameSpec.substr(dot + 1);
1735-
} else {
1736-
ns = std::string{mcpp::manifest::kDefaultNamespace};
1737-
shortName = nameSpec;
1738-
}
1739-
if (shortName.empty()) {
1740-
mcpp::ui::error(std::format("invalid spec '{}': empty package name", spec));
1741-
return 2;
1742-
}
1743-
1744-
auto root = find_manifest_root(std::filesystem::current_path());
1745-
if (!root) { mcpp::ui::error("no mcpp.toml in current dir or parents"); return 2; }
1746-
auto manifestPath = *root / "mcpp.toml";
1747-
1748-
if (version.empty()) {
1749-
mcpp::ui::error(std::format(
1750-
"package version required: `mcpp add {}@<version>` (M2 supports exact-version only)",
1751-
spec));
1752-
return 2;
1753-
}
1754-
1755-
std::ifstream in(manifestPath);
1756-
std::stringstream ss; ss << in.rdbuf();
1757-
std::string text = ss.str();
1758-
1759-
// Insertion strategy:
1760-
// - Default namespace → `[dependencies] ... name = "version"` (no quotes).
1761-
// - Other namespace → `[dependencies.<ns>] ... name = "version"`,
1762-
// creating the subtable if absent.
1763-
const bool isDefaultNs = (ns == mcpp::manifest::kDefaultNamespace);
1764-
const std::string section = isDefaultNs
1765-
? "[dependencies]"
1766-
: std::format("[dependencies.{}]", ns);
1767-
auto pos = text.find(section);
1768-
if (pos == std::string::npos) {
1769-
if (!text.empty() && text.back() != '\n') text += "\n";
1770-
text += std::format("\n{}\n{} = \"{}\"\n", section, shortName, version);
1771-
} else {
1772-
auto nl = text.find('\n', pos);
1773-
if (nl == std::string::npos) nl = text.size();
1774-
text.insert(nl, std::format("\n{} = \"{}\"", shortName, version));
1775-
}
1776-
{
1777-
std::ofstream os(manifestPath);
1778-
os << text;
1779-
}
1780-
1781-
std::string display = isDefaultNs ? shortName : std::format("{}:{}", ns, shortName);
1782-
mcpp::ui::status("Adding", std::format("{} v{} to dependencies", display, version));
1783-
std::println("");
1784-
std::println("Run `mcpp build` to fetch and build with the new dependency.");
1785-
return 0;
1786-
}
1710+
// `cmd_add` has moved to `mcpp.pm.commands` (PR-R5).
17871711

17881712
int cmd_test(const mcpplibs::cmdline::ParsedArgs& /*parsed*/,
17891713
std::span<const std::string> passthrough) {
@@ -2219,174 +2143,7 @@ int cmd_cache_clean(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) {
22192143
}
22202144

22212145
// ─── M4 #3: mcpp remove / mcpp update ───────────────────────────────────
2222-
int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) {
2223-
std::string name = parsed.positional(0);
2224-
if (name.empty()) {
2225-
mcpp::ui::error("usage: mcpp remove <pkg>");
2226-
return 2;
2227-
}
2228-
2229-
auto root = find_manifest_root(std::filesystem::current_path());
2230-
if (!root) { mcpp::ui::error("no mcpp.toml in current dir or parents"); return 2; }
2231-
auto manifestPath = *root / "mcpp.toml";
2232-
2233-
std::ifstream in(manifestPath);
2234-
std::stringstream ss; ss << in.rdbuf();
2235-
std::string text = ss.str();
2236-
2237-
// Accept the same forms as `mcpp add`: bare `name` (default ns),
2238-
// `<ns>:<name>`, or legacy `<ns>.<name>`. The line we want to delete
2239-
// depends on which form the user wrote in mcpp.toml — try every one.
2240-
std::string ns, shortName;
2241-
if (auto col = name.find(':'); col != std::string::npos) {
2242-
ns = name.substr(0, col); shortName = name.substr(col + 1);
2243-
} else if (auto dot = name.find('.'); dot != std::string::npos) {
2244-
ns = name.substr(0, dot); shortName = name.substr(dot + 1);
2245-
} else {
2246-
ns = std::string{mcpp::manifest::kDefaultNamespace};
2247-
shortName = name;
2248-
}
2249-
const bool isDefaultNs = (ns == mcpp::manifest::kDefaultNamespace);
2250-
2251-
bool changed = false;
2252-
auto erase_line_at = [&](std::size_t p) {
2253-
auto bol = text.rfind('\n', p);
2254-
auto eol = text.find('\n', p);
2255-
if (bol == std::string::npos) bol = 0; else ++bol;
2256-
if (eol == std::string::npos) eol = text.size();
2257-
text.erase(bol, (eol - bol) + (eol < text.size() ? 1 : 0));
2258-
changed = true;
2259-
};
2260-
2261-
// Try bare `<short> = ` and quoted `"<short>" = ` (default-ns flat form).
2262-
if (isDefaultNs) {
2263-
for (const auto& needle : {
2264-
std::format("\n{} = ", shortName),
2265-
std::format("\n\"{}\" = ", shortName),
2266-
}) {
2267-
if (auto p = text.find(needle); p != std::string::npos) {
2268-
erase_line_at(p + 1);
2269-
break;
2270-
}
2271-
}
2272-
}
2273-
2274-
// Try the namespaced subtable form `[dependencies.<ns>] <short> = `.
2275-
// After deleting the dep line, prune the `[dependencies.<ns>]` header
2276-
// if no entries remain under it.
2277-
if (!isDefaultNs) {
2278-
auto sectHeader = std::format("[dependencies.{}]", ns);
2279-
if (auto sp = text.find(sectHeader); sp != std::string::npos) {
2280-
auto bodyStart = text.find('\n', sp);
2281-
if (bodyStart == std::string::npos) bodyStart = text.size();
2282-
auto sectEnd = text.find("\n[", bodyStart);
2283-
if (sectEnd == std::string::npos) sectEnd = text.size();
2284-
std::string section = text.substr(bodyStart, sectEnd - bodyStart);
2285-
for (const auto& needle : {
2286-
std::format("\n{} = ", shortName),
2287-
std::format("\n\"{}\" = ", shortName),
2288-
}) {
2289-
if (auto p = section.find(needle); p != std::string::npos) {
2290-
auto absStart = bodyStart + p + 1;
2291-
erase_line_at(absStart);
2292-
break;
2293-
}
2294-
}
2295-
// If the subtable now contains no `name = ...` lines, drop it.
2296-
auto headerPos = text.find(sectHeader);
2297-
if (changed && headerPos != std::string::npos) {
2298-
auto bodyAfter = text.find('\n', headerPos);
2299-
auto endAfter = text.find("\n[", bodyAfter == std::string::npos ? headerPos : bodyAfter);
2300-
if (endAfter == std::string::npos) endAfter = text.size();
2301-
std::string body = text.substr(bodyAfter == std::string::npos ? headerPos : bodyAfter,
2302-
endAfter - (bodyAfter == std::string::npos ? headerPos : bodyAfter));
2303-
bool hasEntry = false;
2304-
std::size_t i = 0;
2305-
while (i < body.size()) {
2306-
auto j = body.find('\n', i);
2307-
auto line = body.substr(i, (j == std::string::npos ? body.size() : j) - i);
2308-
auto first = line.find_first_not_of(" \t");
2309-
if (first != std::string::npos
2310-
&& line[first] != '#' && line[first] != '\n'
2311-
&& line[first] != '[') {
2312-
hasEntry = true; break;
2313-
}
2314-
if (j == std::string::npos) break;
2315-
i = j + 1;
2316-
}
2317-
if (!hasEntry) {
2318-
auto headerLineStart = text.rfind('\n', headerPos);
2319-
if (headerLineStart == std::string::npos) headerLineStart = 0;
2320-
text.erase(headerLineStart, endAfter - headerLineStart);
2321-
}
2322-
}
2323-
}
2324-
}
2325-
2326-
// Legacy: `[dependencies.<name>] ...` — pre-namespace inline-spec subtable
2327-
// shape (e.g. when path/git deps were authored as their own subtable). We
2328-
// only honour this for the default-ns input form to avoid colliding with
2329-
// the new `[dependencies.<ns>]` namespacing semantics.
2330-
if (!changed && isDefaultNs) {
2331-
auto block = std::format("[dependencies.{}]", shortName);
2332-
if (auto p = text.find(block); p != std::string::npos) {
2333-
auto bol = text.rfind('\n', p);
2334-
if (bol == std::string::npos) bol = 0; else ++bol;
2335-
auto end = text.find("\n[", p + block.size());
2336-
if (end == std::string::npos) end = text.size();
2337-
else end += 1;
2338-
text.erase(bol, end - bol);
2339-
changed = true;
2340-
}
2341-
}
2342-
2343-
if (!changed) {
2344-
mcpp::ui::error(std::format("no dependency '{}' in mcpp.toml", name));
2345-
return 1;
2346-
}
2347-
std::ofstream os(manifestPath);
2348-
os << text;
2349-
mcpp::ui::status("Removing", std::format("{} from dependencies", name));
2350-
// Also clean lockfile entry if present
2351-
auto lockPath = *root / "mcpp.lock";
2352-
if (std::filesystem::exists(lockPath)) {
2353-
if (auto lock = mcpp::lockfile::load(lockPath); lock) {
2354-
std::erase_if(lock->packages,
2355-
[&](const auto& p) { return p.name == name; });
2356-
(void)mcpp::lockfile::write(*lock, lockPath);
2357-
}
2358-
}
2359-
return 0;
2360-
}
2361-
2362-
int cmd_update(const mcpplibs::cmdline::ParsedArgs& parsed) {
2363-
std::optional<std::string> only;
2364-
if (parsed.positional_count() > 0) only = parsed.positional(0);
2365-
2366-
auto root = find_manifest_root(std::filesystem::current_path());
2367-
if (!root) { mcpp::ui::error("no mcpp.toml in current dir or parents"); return 2; }
2368-
auto lockPath = *root / "mcpp.lock";
2369-
if (only) {
2370-
// Targeted update — drop just that lock entry; next build will refetch.
2371-
if (std::filesystem::exists(lockPath)) {
2372-
auto lock = mcpp::lockfile::load(lockPath);
2373-
if (lock) {
2374-
std::erase_if(lock->packages,
2375-
[&](const auto& p) { return p.name == *only; });
2376-
(void)mcpp::lockfile::write(*lock, lockPath);
2377-
}
2378-
}
2379-
mcpp::ui::status("Updating", std::format("{} in mcpp.lock", *only));
2380-
} else {
2381-
// Wholesale update — wipe the lockfile.
2382-
std::error_code ec;
2383-
std::filesystem::remove(lockPath, ec);
2384-
mcpp::ui::status("Updating", "all dependencies (mcpp.lock cleared)");
2385-
}
2386-
std::println("");
2387-
std::println("Run `mcpp build` to re-resolve and rewrite mcpp.lock.");
2388-
return 0;
2389-
}
2146+
// `cmd_remove` and `cmd_update` have moved to `mcpp.pm.commands` (PR-R5).
23902147

23912148
// ─── M4 #2: mcpp publish ────────────────────────────────────────────────
23922149
// ─── M5.5 #toolchain: mcpp toolchain install/list/default/remove ────────
@@ -3230,15 +2987,15 @@ int run(int argc, char** argv) {
32302987
.subcommand(cl::App("add")
32312988
.description("Add a dependency to mcpp.toml")
32322989
.arg(cl::Arg("pkg").help("Package spec, e.g. foo@1.0.0").required())
3233-
.action(wrap_rc(cmd_add)))
2990+
.action(wrap_rc(mcpp::pm::commands::cmd_add)))
32342991
.subcommand(cl::App("remove")
32352992
.description("Remove a dependency from mcpp.toml")
32362993
.arg(cl::Arg("pkg").help("Package name").required())
3237-
.action(wrap_rc(cmd_remove)))
2994+
.action(wrap_rc(mcpp::pm::commands::cmd_remove)))
32382995
.subcommand(cl::App("update")
32392996
.description("Re-resolve dependencies and rewrite mcpp.lock")
32402997
.arg(cl::Arg("pkg").help("If given, update only that package"))
3241-
.action(wrap_rc(cmd_update)))
2998+
.action(wrap_rc(mcpp::pm::commands::cmd_update)))
32422999
.subcommand(cl::App("search")
32433000
.description("Search packages in configured registries")
32443001
.arg(cl::Arg("keyword").help("Search keyword (substring match)").required())

0 commit comments

Comments
 (0)