diff --git a/src/cli.cppm b/src/cli.cppm index 6ca62ae..a10ecdd 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -4268,6 +4268,53 @@ int cmd_self_version(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { return 0; } +int cmd_self_init(const mcpplibs::cmdline::ParsedArgs& parsed) { + bool force = parsed.is_flag_set("force"); + + if (force) { + // --force: delete registry (sandbox) + caches and re-bootstrap. + // Preserves: bin/mcpp (self-contained mode), config.toml, log/. + mcpp::ui::info("Resetting", "mcpp sandbox (registry, caches)"); + + // Resolve MCPP_HOME without running bootstrap (which may fail). + std::filesystem::path home; + if (auto* e = std::getenv("MCPP_HOME"); e && *e) + home = e; + else { + const char* userHome = nullptr; +#if defined(_WIN32) + userHome = std::getenv("USERPROFILE"); +#endif + if (!userHome) userHome = std::getenv("HOME"); + if (userHome) home = std::filesystem::path(userHome) / ".mcpp"; + } + if (!home.empty()) { + std::error_code ec; + std::filesystem::remove_all(home / "registry", ec); + std::filesystem::remove_all(home / "bmi", ec); + std::filesystem::remove_all(home / "cache", ec); + } + } + + // (Re-)run the full load_or_init, which does bootstrap. + mcpp::ui::info("Initializing", "mcpp sandbox"); + auto cfg = mcpp::config::load_or_init(); + if (!cfg) { + mcpp::ui::error(cfg.error().message); + return 1; + } + + // Verify result. + auto problem = mcpp::config::check_base_init(*cfg); + if (!problem.empty()) { + mcpp::ui::error(std::format("init incomplete: {}", problem)); + return 1; + } + + mcpp::ui::status("Ready", "sandbox initialized"); + return 0; +} + std::string upper_ascii(std::string s) { for (char& ch : s) { if (ch >= 'a' && ch <= 'z') ch = static_cast(ch - 'a' + 'A'); @@ -4635,6 +4682,10 @@ int run(int argc, char** argv) { // ─── about mcpp itself ───────────────────────────────────────── .subcommand(cl::App("self") .description("Inspect and manage mcpp itself") + .subcommand(cl::App("init") + .description("Initialize or repair mcpp sandbox") + .option(cl::Option("force") + .help("Delete registry and re-initialize from scratch"))) .subcommand(cl::App("doctor") .description("Diagnose mcpp environment health")) .subcommand(cl::App("env") @@ -4650,11 +4701,12 @@ int run(int argc, char** argv) { .arg(cl::Arg("code").help("Error code such as E0001").required())) .action(wrap_rc([&dispatch_sub](const cl::ParsedArgs& p) { return dispatch_sub("self", p, { - {"doctor", cmd_doctor}, - {"env", cmd_env}, - {"config", cmd_self_config}, - {"version", cmd_self_version}, - {"explain", cmd_explain_action}, + {"init", cmd_self_init}, + {"doctor", cmd_doctor}, + {"env", cmd_env}, + {"config", cmd_self_config}, + {"version", cmd_self_version}, + {"explain", cmd_explain_action}, }); }))) diff --git a/src/config.cppm b/src/config.cppm index aa5f4b5..e3fd137 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -102,6 +102,61 @@ mcpp::xlings::Env make_project_xlings_env(const GlobalConfig& cfg, return { cfg.xlingsBinary, cfg.xlingsHome(), projectDir / ".mcpp" }; } +// Check that the sandbox bootstrap completed successfully. +// Returns empty string if ok, or a description of what's missing. +std::string check_base_init(const GlobalConfig& cfg) { + auto xlEnv = make_xlings_env(cfg); + struct Check { std::string_view name; std::filesystem::path path; }; + + auto patchelfBin = mcpp::xlings::paths::xim_tool(xlEnv, "patchelf", + mcpp::xlings::pinned::kPatchelfVersion) / "bin" / "patchelf"; + auto ninjaRoot = mcpp::xlings::paths::xim_tool_root(xlEnv, "ninja"); + auto ninja_name = std::string("ninja") + std::string(mcpp::platform::exe_suffix); + + // Check xlings binary + if (!std::filesystem::exists(cfg.xlingsBinary)) + return std::format("xlings binary missing: {}", cfg.xlingsBinary.string()); + + // Check sandbox init marker + auto marker = xlEnv.home / "subos" / "default" / ".xlings.json"; + if (!std::filesystem::exists(marker)) + return "sandbox not initialized (missing subos/default/.xlings.json)"; + + // Check patchelf (Linux only) +#if !defined(__APPLE__) && !defined(_WIN32) + if (!std::filesystem::exists(patchelfBin)) + return std::format("patchelf missing: {}", patchelfBin.string()); +#endif + + // Check ninja + bool ninjaOk = false; + if (std::filesystem::exists(ninjaRoot)) { + std::error_code ec; + for (auto& v : std::filesystem::directory_iterator(ninjaRoot, ec)) { + if (std::filesystem::exists(v.path() / ninja_name)) { ninjaOk = true; break; } + } + } + if (!ninjaOk) + return "ninja missing from sandbox"; + + return {}; // all ok +} + +// Delete the registry directory (sandbox) for a clean re-init. +// Preserves: bin/mcpp (self-contained mode), config.toml, log/. +void reset_registry(const GlobalConfig& cfg) { + std::error_code ec; + auto registry = cfg.xlingsHome(); + if (std::filesystem::exists(registry)) + std::filesystem::remove_all(registry, ec); + + // Also clear BMI and metadata caches (stale after registry reset). + if (std::filesystem::exists(cfg.bmiCacheDir)) + std::filesystem::remove_all(cfg.bmiCacheDir, ec); + if (std::filesystem::exists(cfg.metaCacheDir)) + std::filesystem::remove_all(cfg.metaCacheDir, ec); +} + // Ensure the project-level .mcpp/ directory exists and contains a // .xlings.json seeded with the custom (non-builtin, non-local) index // entries. Returns true if a .mcpp/ directory was created/updated. @@ -489,6 +544,16 @@ std::expected load_or_init( #endif ensure_sandbox_ninja(cfg, quiet, onBootstrapProgress); + // 8. Verify bootstrap completed. If something is missing (e.g. Ctrl+C + // interrupted a previous bootstrap), report the problem up-front + // rather than letting a cryptic error surface later. + auto initProblem = check_base_init(cfg); + if (!initProblem.empty()) { + return std::unexpected(ConfigError{std::format( + "{}\n hint: run `mcpp self init --force` to reset and re-initialize", + initProblem)}); + } + return cfg; } diff --git a/src/xlings.cppm b/src/xlings.cppm index 0d58e5a..b69ac8d 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -833,9 +833,17 @@ void ensure_init(const Env& env, bool quiet) { void ensure_patchelf(const Env& env, bool quiet, const BootstrapProgressCallback& cb) { - auto marker = paths::xim_tool(env, "patchelf", pinned::kPatchelfVersion) - / "bin" / "patchelf"; - if (std::filesystem::exists(marker)) return; + auto toolDir = paths::xim_tool(env, "patchelf", pinned::kPatchelfVersion); + auto binary = toolDir / "bin" / "patchelf"; + if (std::filesystem::exists(binary)) return; + + // Clean up incomplete installation residue (e.g. from Ctrl+C interrupt). + if (std::filesystem::exists(toolDir)) { + if (!quiet) + print_status("Repairing", "patchelf (incomplete installation, cleaning up)"); + std::error_code ec; + std::filesystem::remove_all(toolDir, ec); + } if (!quiet) print_status("Bootstrap", "patchelf into mcpp sandbox (one-time)"); @@ -852,12 +860,16 @@ void ensure_ninja(const Env& env, bool quiet, const BootstrapProgressCallback& cb) { auto root = paths::xim_tool_root(env, "ninja"); + auto ninja_name = std::string("ninja") + std::string(mcpp::platform::exe_suffix); if (std::filesystem::exists(root)) { std::error_code ec; - auto ninja_name = std::string("ninja") + std::string(mcpp::platform::exe_suffix); for (auto& v : std::filesystem::directory_iterator(root, ec)) { if (std::filesystem::exists(v.path() / ninja_name)) return; } + // Directory exists but no version has a working binary — residue. + if (!quiet) + print_status("Repairing", "ninja (incomplete installation, cleaning up)"); + std::filesystem::remove_all(root, ec); } if (!quiet) print_status("Bootstrap", "ninja into mcpp sandbox (one-time)");