// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.Review:Driver_impl; import :Driver; import :Core; import :Text; import :Platform; import :Process; import :Inventory; import :Config; import :Freeze; import :PgWire; import :Totp; import std; namespace fs = std::filesystem; namespace Crafter::Review { namespace { constexpr std::string_view Usage = R"(usage: crafter-review [--config FILE] {run,freeze,slots,config} ... crafter-review {seats,inventory,sql,totp,init,doc,browser,stop} ... crafter-review --version The pre-push review pipeline: adversarial lenses, one claude process per inventory unit, against a frozen commit range.)"; std::int32_t PrintVersion() { std::println("{}", DriverLine()); try { std::println("home {}", Home().string()); for (const auto& [name, sha] : CharterHashes()) std::println("charter {} {}", name, sha); } catch (const std::exception& e) { std::println("home not found: {}", e.what()); } return 0; } // A usage error: exit 3, the preflight's code, for every command that // cannot start. std::int32_t UsageError(std::string_view usage, std::string_view prog, std::string_view message) { std::println(std::cerr, "{}\n{}: error: {}", usage, prog, message); return 3; } // die(): the preflight's message and exit 3. std::int32_t Die(std::string_view message) { std::println(std::cerr, "preflight: {}", message); return 3; } // ---- the option parser these commands use --------------------------------- struct OptionSpec { std::string_view name; // A value follows (`--x V` or `--x=V`); else a store_true flag. bool takesValue; std::string_view metavar; std::string_view help; }; struct Parsed { std::map> values; std::set> flags; bool help = false; std::string error; std::optional Value(std::string_view name) const { const auto found = values.find(name); if (found == values.end()) return std::nullopt; return found->second; } bool Flag(std::string_view name) const { return flags.contains(name); } }; std::string UsageLine(std::string_view prog, std::span specs) { std::string line = std::format("usage: {} [-h]", prog); for (const OptionSpec& spec : specs) { line += spec.takesValue ? std::format(" [{} {}]", spec.name, spec.metavar) : std::format(" [{}]", spec.name); } return line; } std::string Help(std::string_view prog, std::string_view description, std::span specs) { std::string text = std::format("{}\n\n{}\n\noptions:\n -h, --help show this help message and exit\n", UsageLine(prog, specs), description); for (const OptionSpec& spec : specs) { const std::string left = spec.takesValue ? std::format("{} {}", spec.name, spec.metavar) : std::string(spec.name); text += left.size() < 20 ? std::format(" {:<20} {}\n", left, spec.help) : std::format(" {}\n {}\n", left, spec.help); } return text; } // `--opt value`, `--opt=value`, any unique prefix of a long option, the // last of a repeated option, every unrecognized argument named at the // end. Parsed ParseOptions(std::span args, std::span specs) { Parsed parsed; std::vector unrecognized; for (std::size_t i = 0; i < args.size(); ++i) { const std::string_view arg = args[i]; if (arg == "-h") { parsed.help = true; continue; } if (!arg.starts_with("--") || arg == "--") { unrecognized.emplace_back(arg); continue; } const std::size_t equals = arg.find('='); const std::string_view name = arg.substr(0, equals); if (name.size() > 2 && std::string_view("--help").starts_with(name) && std::ranges::none_of(specs, [&](const OptionSpec& s) { return s.name.starts_with(name); })) { parsed.help = true; continue; } std::vector matches; for (const OptionSpec& spec : specs) { if (spec.name == name) { matches = {&spec}; break; } if (spec.name.starts_with(name)) matches.push_back(&spec); } if (matches.empty()) { unrecognized.emplace_back(arg); continue; } if (matches.size() > 1) { std::vector names; for (const OptionSpec* spec : matches) names.emplace_back(spec->name); parsed.error = std::format("ambiguous option: {} could match {}", name, Dyn::Join(names, ", ")); return parsed; } const OptionSpec& spec = *matches.front(); const std::string key(spec.name.substr(2)); if (!spec.takesValue) { if (equals != std::string_view::npos) { parsed.error = std::format("argument {}: ignored explicit argument {}", spec.name, Dyn::Repr(arg.substr(equals + 1))); return parsed; } parsed.flags.insert(key); continue; } if (equals != std::string_view::npos) { parsed.values[key] = std::string(arg.substr(equals + 1)); } else if (i + 1 < args.size() && !(args[i + 1].starts_with('-') && args[i + 1].size() > 1)) { parsed.values[key] = std::string(args[++i]); } else { parsed.error = std::format("argument {}: expected one argument", spec.name); return parsed; } } if (!unrecognized.empty()) parsed.error = std::format("unrecognized arguments: {}", Dyn::Join(unrecognized, " ")); return parsed; } // The consumer config, loaded before every command but `config`: // warnings on stderr, a problem stops it. std::optional LoadConfig(const std::optional& configPath) { Config::Loaded loaded = Config::Load(fs::current_path(), configPath ? fs::path(*configPath) : fs::path()); for (const Json& warning : Dyn::Or(Dyn::Get(loaded.config, "_warnings"), Dyn::EmptyList())) std::println(std::cerr, "config: warning: {}", warning.get()); if (!loaded.problems.empty()) { Die(std::format("config: {}", Dyn::Join(loaded.problems, "; "))); return std::nullopt; } return std::move(loaded.config); } constexpr std::array FreezeOptions = {{ {"--repo", true, "REPO", "the repository (toplevel of a checkout; default: the working directory)"}, {"--slug", true, "SLUG", "the artifact dir name: .review-gates/push//"}, {"--out", true, "OUT", "the artifact dir (overrides --slug)"}, {"--base", true, "BASE", "the range base; default @{upstream}, else merge-base with the configured base branch"}, {"--replace", false, "", "re-freeze into an out dir that already holds patches (gate-state/ is kept) and supersede this repo's worktrees"}, {"--worktree", false, "", "check out a detached worktree of HEAD under .review-gates/trees/ for the lenses (printed as --trees)"}, {"--require-signed", false, "", "an unsigned commit fails instead of warning"}, {"--date", true, "DATE", "the run date (default today)"}, {"--verify", false, "", "push-time check: HEAD, a clean tree and a fresh export still match freeze.json"}, }}; std::int32_t FreezeCommand(std::span args, const std::optional& configPath) { constexpr std::string_view Prog = "crafter-review freeze"; const Parsed parsed = ParseOptions(args, FreezeOptions); if (parsed.help) { std::print("{}", Help(Prog, "cut the frozen commit-range artifact for the push gates (process/prepush.md Gate 0)", FreezeOptions)); return 0; } if (!parsed.error.empty()) return UsageError(UsageLine(Prog, FreezeOptions), Prog, parsed.error); const std::optional config = LoadConfig(configPath); if (!config) return 3; Freeze::Arguments arguments; arguments.repo = parsed.Value("repo"); arguments.slug = parsed.Value("slug"); arguments.out = parsed.Value("out"); arguments.base = parsed.Value("base"); arguments.replace = parsed.Flag("replace"); arguments.worktree = parsed.Flag("worktree"); arguments.requireSigned = parsed.Flag("require-signed"); arguments.date = parsed.Value("date").value_or(Platform::TodayIso()); arguments.verify = parsed.Flag("verify"); // `%G?` starts gpg, which can leave a daemon behind: as a subreaper // this process inherits it, and takes it down on the way out. Process::BecomeSubreaper(); const std::int32_t code = Freeze::Main(arguments, *config); Process::KillDescendants(); return code; } } std::int32_t Main(std::span args) { const std::span rest = args.empty() ? args : args.subspan(1); // The supervisor first and alone: it is single-threaded and has to // stay so, and it runs between the driver and claude, not as a // command anyone types. if (!rest.empty() && rest.front() == Process::SuperviseVerb) return Process::SuperviseMain(rest.subspan(1)); Platform::Init(); // The options before the command: --config FILE or --config=FILE, and // any unique prefix of a long option. std::optional configPath; std::size_t at = 0; while (at < rest.size() && rest[at].starts_with("--") && rest[at] != "--") { const std::string_view arg = rest[at]; const std::size_t equals = arg.find('='); const std::string_view name = arg.substr(0, equals); if (name.size() > 2 && std::string_view("--config").starts_with(name)) { if (equals != std::string_view::npos) { configPath = std::string(arg.substr(equals + 1)); } else if (at + 1 < rest.size()) { configPath = std::string(rest[++at]); } else { return UsageError(Usage, "crafter-review", "argument --config: expected one argument"); } } else if (name.size() > 2 && std::string_view("--version").starts_with(name)) { return PrintVersion(); } else if (name.size() > 2 && std::string_view("--help").starts_with(name)) { std::println("{}", Usage); return 0; } else { return UsageError(Usage, "crafter-review", std::format("unrecognized arguments: {}", arg)); } ++at; } if (at < rest.size() && rest[at] == "-h") { std::println("{}", Usage); return 0; } if (at >= rest.size()) return UsageError(Usage, "crafter-review", "a command is required"); const std::string_view command = rest[at]; const std::span after = rest.subspan(at + 1); try { // The helpers a lens runs: they read neither the home nor the // config, only their arguments, stdin and REVIEW_GATES_DB. if (command == "sql") return PgWire::SqlMain(after); if (command == "totp") return Totp::Main(after); if (command == "inventory" && !configPath) return Inventory::Main(after); if (command == "config") { if (!after.empty()) return UsageError(Usage, "crafter-review", std::format("unrecognized arguments: {}", after.front())); return Config::Main(fs::current_path(), configPath ? fs::path(*configPath) : fs::path()); } if (command == "freeze") return FreezeCommand(after, configPath); } catch (const Dyn::Error& error) { // An error no command catches: its message, exit 1. std::println(std::cerr, "crafter-review: {}: {}", error.Kind(), error.what()); return 1; } return UsageError(Usage, "crafter-review", std::format("argument cmd: invalid choice: '{}'", command)); } }