#include "kappa/build/build.hpp" #include "kappa/cli/diagnostic.hpp" #include "kappa/config/eval.hpp" #include "kappa/dsl/parser.hpp" #include "kappa/dsl/system.hpp" #include "kappa/fetch/fetch.hpp" #include "kappa/fetch/recipe.hpp" #include "kappa/install/install.hpp" #include "kappa/paths.hpp" #include "kappa/rebuild/rebuild.hpp" #include "kappa/resolve/plan.hpp" #include "kappa/boot/bootloader.hpp" #include "kappa/boot/types.hpp" #include "kappa/sched/scheduler.hpp" #include "kappa/service/service.hpp" #include "kappa/service/types.hpp" #include "kappa/system/activate.hpp" #include "kappa/tools/doctor.hpp" #include "kappa/tools/format.hpp" #include #include #include #include #include #include #include #include #include using namespace std::string_view_literals; using namespace kappa; static constexpr auto version = "kappa 0.1.0"sv; static void print_usage() { std::cout << R"(kappa — a declarative source-based package manager Usage: kappa [options] Subcommands: parse-package Parse and validate a package definition (.kap) parse-config Parse and validate a system configuration validate Validate any kappa file (package or config) format Format a .kap file to canonical style (printed to stdout) doctor Check a .kap file for issues and warnings resolve Resolve a build plan from a system config fetch Download and verify source for a package fetch-package Fetch a package recipe from configured remotes build Build a package from its source directory rebuild Compare config to installed state, rebuild changed list List installed packages rollback Show available generations index Build an index.kap from .kap files in a directory add [v] Add a package to system config (optional version) remove Remove a package from system config Options: -h, --help Show this help message -V, --version Show version information -j, --jobs Jobs per package (default: 1) -w Concurrent package builds (default: 1) --root Set kappa root directory (default: /usr/local/kappa) --dry-run Report changes without building (rebuild only) )"; } static bool is_flag(std::string_view arg) { return arg == "-h" || arg == "--help" || arg == "-V" || arg == "--version"; } static std::string read_file(const char* path) { std::ifstream in(path); if (!in) { throw std::runtime_error(std::format("cannot open '{}'", path)); } std::ostringstream buf; buf << in.rdbuf(); return buf.str(); } static void handle_parse_error(const char* path, std::string_view source, const std::runtime_error& e) { auto msg = std::string_view(e.what()); auto first_colon = msg.find(':'); auto second_colon = msg.find(':', first_colon + 1); if (first_colon != std::string_view::npos && second_colon != std::string_view::npos) { try { int line = std::stoi(std::string(msg.substr(0, first_colon))); int col = std::stoi(std::string( msg.substr(first_colon + 1, second_colon - first_colon - 1))); auto message = msg.substr(second_colon + 2); cli::print_error(std::cerr, source, {path, line, col}, message, "check the syntax at this location"); return; } catch (const std::exception&) {} } std::cerr << "error: " << e.what() << '\n'; } static resolve::Registry build_registry(const dsl::SystemConfig& cfg) { resolve::Registry registry; for (const auto& pref : cfg.packages) { bool found = false; // Check standard locations: local .kap, examples/, cache/packages/ std::vector search_paths = { std::string(pref.name) + ".kap", std::string("examples/") + pref.name + ".kap", (paths::packages_dir() / (pref.name + ".kap")).string(), }; for (const auto& sp : search_paths) { std::ifstream in(sp); if (!in) continue; std::ostringstream buf; buf << in.rdbuf(); try { auto pkg = dsl::parse(buf.str()); registry[pkg.name] = std::move(pkg); found = true; break; } catch (const std::exception& e) { std::cerr << "warning: parse error in " << sp << ": " << e.what() << "\n"; } catch (...) { std::cerr << "warning: unknown parse error in " << sp << "\n"; } } // If not found locally, try repos (index-aware) first, then legacy remotes if (!found && !cfg.repos.empty()) { auto result = fetch::fetch_recipe_from_repos(pref.name, cfg.repos); if (result.ok && !result.path.empty()) { std::ifstream in(result.path); if (in) { std::ostringstream buf; buf << in.rdbuf(); try { auto pkg = dsl::parse(buf.str()); registry[pkg.name] = std::move(pkg); found = true; } catch (const std::exception& e) { std::cerr << "warning: parse error in remote recipe " << pref.name << ": " << e.what() << "\n"; } catch (...) { std::cerr << "warning: unknown parse error in remote recipe " << pref.name << "\n"; } } } } if (!found && !cfg.remotes.empty()) { auto result = fetch::fetch_recipe(pref.name, cfg.remotes); if (result.ok && !result.path.empty()) { std::ifstream in(result.path); if (in) { std::ostringstream buf; buf << in.rdbuf(); try { auto pkg = dsl::parse(buf.str()); registry[pkg.name] = std::move(pkg); found = true; } catch (const std::exception& e) { std::cerr << "warning: parse error in remote recipe " << pref.name << ": " << e.what() << "\n"; } catch (...) { std::cerr << "warning: unknown parse error in remote recipe " << pref.name << "\n"; } } } } if (!found) { std::cerr << "warning: package '" << pref.name << "' not found locally or in remotes\n"; } } return registry; } int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "kappa: missing subcommand\n\n"; print_usage(); return 1; } // Parse leading --root before subcommand int arg_start = 1; while (arg_start < argc && std::string_view(argv[arg_start]) == "--root" && arg_start + 1 < argc) { paths::set_root(argv[arg_start + 1]); arg_start += 2; } auto subcommand = std::string_view(argv[arg_start]); if (subcommand == "-h" || subcommand == "--help") { print_usage(); return 0; } if (subcommand == "-V" || subcommand == "--version") { std::cout << version << '\n'; return 0; } bool valid_subcommand = (subcommand == "parse-package") || (subcommand == "parse-config") || (subcommand == "validate") || (subcommand == "format") || (subcommand == "doctor") || (subcommand == "resolve") || (subcommand == "fetch") || (subcommand == "build") || (subcommand == "rebuild") || (subcommand == "list") || (subcommand == "fetch-package") || (subcommand == "rollback") || (subcommand == "index") || (subcommand == "add") || (subcommand == "remove"); if (!valid_subcommand) { std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n"; print_usage(); return 1; } const char* file_arg = nullptr; const char* version_arg = nullptr; bool dry_run = false; for (int i = arg_start + 1; i < argc; ++i) { if (std::string_view(argv[i]) == "-h" || std::string_view(argv[i]) == "--help") { print_usage(); return 0; } if (std::string_view(argv[i]) == "-V" || std::string_view(argv[i]) == "--version") { std::cout << version << '\n'; return 0; } if (std::string_view(argv[i]) == "-j" || std::string_view(argv[i]) == "--jobs" || (std::string_view(argv[i]) == "-w" && i + 1 < argc)) { ++i; continue; } if (std::string_view(argv[i]) == "--root" && i + 1 < argc) { paths::set_root(argv[++i]); continue; } if (std::string_view(argv[i]) == "--dry-run") { dry_run = true; continue; } if (!is_flag(argv[i])) { if (file_arg == nullptr) { file_arg = argv[i]; } else if (version_arg == nullptr) { version_arg = argv[i]; break; } } } if (subcommand == "list") { auto entries = install::read_installed(); if (entries.empty()) { std::cout << "no packages installed\n"; } else { std::cout << entries.size() << " packages installed:\n"; for (auto& e : entries) { std::cout << " " << e.name << " " << e.version << " (" << e.hash << ")"; if (!e.provides.empty()) { std::cout << " provides:"; for (auto& p : e.provides) { std::cout << " " << p; } } std::cout << "\n"; } } return 0; } if (subcommand == "rollback") { auto gen_dir = paths::db_dir() / "generations"; std::error_code ec; if (!std::filesystem::exists(gen_dir)) { std::cout << "no generations found\n"; return 0; } std::vector gens; for (auto& entry : std::filesystem::directory_iterator(gen_dir, ec)) { gens.push_back(entry.path().filename().string()); } std::sort(gens.begin(), gens.end()); std::cout << gens.size() << " generations:\n"; for (auto& g : gens) { std::cout << " " << g << "\n"; } return 0; } if (subcommand == "index") { if (file_arg == nullptr) { std::cerr << "error: no directory specified\n"; return 1; } try { auto idx = dsl::build_index(file_arg); auto out_path = std::filesystem::path(file_arg) / "index.kap"; std::ofstream out(out_path); if (!out) { std::cerr << "error: cannot write " << out_path.string() << "\n"; return 1; } tools::format_index(out, idx); std::cout << idx.packages.size() << " packages indexed → " << out_path.string() << "\n"; return 0; } catch (const std::exception& e) { std::cerr << "index error: " << e.what() << "\n"; return 1; } } if (subcommand == "add") { if (file_arg == nullptr) { std::cerr << "error: no package name specified\n"; return 1; } try { auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap"; dsl::SystemConfig cfg; if (std::filesystem::exists(config_path)) { auto src = read_file(config_path.c_str()); cfg = dsl::parse_system_config(src); } bool found = false; for (auto& p : cfg.packages) { if (p.name == file_arg) { found = true; if (version_arg != nullptr) { p.version = version_arg; std::cout << "updated " << file_arg << " → version " << version_arg << "\n"; } else { std::cout << file_arg << " already in packages\n"; } break; } } if (!found) { dsl::PackageRef pref; pref.name = file_arg; if (version_arg != nullptr) { pref.version = version_arg; } cfg.packages.push_back(std::move(pref)); std::cout << "added " << file_arg; if (version_arg != nullptr) { std::cout << " " << version_arg; } std::cout << "\n"; } std::error_code ec; std::filesystem::create_directories(paths::system_dir(), ec); std::ofstream out(config_path); if (!out) { std::cerr << "error: cannot write " << config_path.string() << "\n"; return 1; } tools::format_config(out, cfg); return 0; } catch (const std::exception& e) { std::cerr << "add error: " << e.what() << "\n"; return 1; } } if (subcommand == "remove") { if (file_arg == nullptr) { std::cerr << "error: no package name specified\n"; return 1; } try { auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap"; if (!std::filesystem::exists(config_path)) { std::cout << file_arg << " not found (no config)\n"; return 0; } auto src = read_file(config_path.c_str()); auto cfg = dsl::parse_system_config(src); auto it = std::remove_if(cfg.packages.begin(), cfg.packages.end(), [&](const dsl::PackageRef& p) { return p.name == file_arg; }); if (it == cfg.packages.end()) { std::cout << file_arg << " not in packages\n"; return 0; } cfg.packages.erase(it, cfg.packages.end()); std::cout << "removed " << file_arg << "\n"; std::ofstream out(config_path); if (!out) { std::cerr << "error: cannot write " << config_path.string() << "\n"; return 1; } tools::format_config(out, cfg); return 0; } catch (const std::exception& e) { std::cerr << "remove error: " << e.what() << "\n"; return 1; } } if (subcommand == "fetch-package") { if (file_arg == nullptr) { std::cerr << "error: no package name specified\n"; return 1; } try { std::vector remotes; std::vector repos; auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap"; if (std::filesystem::exists(config_path)) { auto cfg_src = read_file(config_path.c_str()); try { auto cfg = dsl::parse_system_config(cfg_src); remotes = cfg.remotes; repos = std::move(cfg.repos); } catch (const std::exception& e) { std::cerr << "warning: config parse error: " << e.what() << "\n"; } catch (...) { std::cerr << "warning: unknown config parse error\n"; } } fetch::RecipeResult result; if (!repos.empty()) { result = fetch::fetch_recipe_from_repos(file_arg, repos); } else { result = fetch::fetch_recipe(file_arg, remotes); } if (result.ok) { if (result.updated) { std::cout << "fetched " << file_arg << " " << result.version << " → " << result.path << "\n"; } else { std::cout << file_arg << " " << result.version << " (cached, up to date)\n"; } return 0; } std::cerr << "fetch failed: " << result.error << "\n"; return 1; } catch (const std::exception& e) { std::cerr << "fetch error: " << e.what() << "\n"; return 1; } } // Subcommands that MUST have writable directories to function. bool needs_write = (subcommand == "build" || subcommand == "fetch" || subcommand == "fetch-package"); // rebuild benefits from cache dirs for registry lookups but // degrades gracefully — it only reports, never writes. bool needs_dirs = needs_write || subcommand == "rebuild" || subcommand == "resolve"; if (needs_dirs && !paths::directories_exist()) { if (!paths::ensure_directories()) { if (needs_write) { std::cerr << "error: cannot create kappa directories (check permissions)\n"; return 1; } } } if (file_arg == nullptr && subcommand != "list" && subcommand != "rollback" && subcommand != "index") { std::cerr << "error: no input file specified\n"; return 1; } try { auto source = file_arg ? read_file(file_arg) : ""; if (subcommand == "parse-package") { try { auto pkg = dsl::parse(source); std::cout << "package \"" << pkg.name << "\" " << pkg.version << " — valid\n"; return 0; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); return 1; } } if (subcommand == "parse-config") { try { auto cfg = dsl::parse_system_config(source); std::cout << "system config — valid (" << cfg.packages.size() << " packages, " << cfg.services.size() << " services, " << cfg.users.size() << " users)\n"; if (!cfg.boot.init.empty()) { auto is = kappa::service::parse_init_system(cfg.boot.init); std::cout << " init: " << cfg.boot.init; if (is != kappa::service::InitSystem::Unknown) { std::cout << " (" << kappa::service::init_description(is) << ")"; } else { std::cout << " (unrecognized)"; } std::cout << "\n"; } if (!cfg.boot.bootloader.empty()) { auto bl = kappa::boot::parse_bootloader(cfg.boot.bootloader); std::cout << " bootloader: " << cfg.boot.bootloader; if (bl != kappa::boot::Bootloader::Unknown) { std::cout << " (" << kappa::boot::bootloader_description(bl) << ")"; } else { std::cout << " (unrecognized)"; } std::cout << "\n"; } if (!cfg.repos.empty()) { int total_channels = 0; for (auto& r : cfg.repos) total_channels += r.channels.size(); std::cout << " repos: " << cfg.repos.size() << " (" << total_channels << " channels)\n"; } return 0; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); return 1; } } if (subcommand == "validate") { try { dsl::parse(source); std::cout << file_arg << ": valid package definition\n"; return 0; } catch (const std::runtime_error&) { try { auto cfg = dsl::parse_system_config(source); auto failures = config::evaluate_assertions(cfg); if (failures.empty()) { std::cout << file_arg << ": valid system configuration (" << cfg.packages.size() << " packages, " << cfg.services.size() << " services, " << cfg.users.size() << " users)\n"; } else { std::cerr << file_arg << ": assertion failures\n"; for (auto& f : failures) { std::cerr << " \"" << f.message << "\"\n" << " " << f.field << " = \"" << f.actual << "\""; if (f.expected.empty()) { std::cerr << " (must not be empty)\n"; } else { std::cerr << " (expected \"" << f.expected << "\")\n"; } } return 1; } return 0; } catch (const std::runtime_error&) { try { auto idx = dsl::parse_index(source); std::cout << file_arg << ": valid index (" << idx.name << ", " << idx.packages.size() << " packages)\n"; return 0; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); return 1; } } } } if (subcommand == "build") { try { auto pkg = dsl::parse(source); int jobs = 1; for (int i = arg_start + 1; i < argc; ++i) { auto arg = std::string_view(argv[i]); if ((arg == "-j" || arg == "--jobs") && i + 1 < argc) { jobs = std::stoi(argv[++i]); } } resolve::Registry reg; reg[pkg.name] = pkg; resolve::BuildStep step; step.name = pkg.name; step.package = ®.at(pkg.name); step.resolved = config::resolve_package(pkg, {}, {}); auto work_dir = paths::temp_dir() / "build"; auto result = build::build(step, work_dir, jobs); if (result.ok) { auto inst = install::install(step, work_dir); if (inst.ok) { std::cout << "build successful — installed to " << inst.store_path << "\n"; return 0; } std::cerr << "install failed: " << inst.error << "\n"; return 1; } std::cerr << "build failed in phase '" << result.phase << "': " << result.error << "\n"; return 1; } catch (const std::exception& e) { std::cerr << "build error: " << e.what() << "\n"; return 1; } } if (subcommand == "format") { try { auto pkg = dsl::parse(source); tools::format_package(std::cout, pkg); return 0; } catch (const std::runtime_error&) { try { auto cfg = dsl::parse_system_config(source); tools::format_config(std::cout, cfg); return 0; } catch (const std::runtime_error&) { try { auto idx = dsl::parse_index(source); tools::format_index(std::cout, idx); return 0; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); return 1; } } } } if (subcommand == "doctor") { try { auto pkg = dsl::parse(source); auto diags = tools::check_package(pkg); if (diags.empty()) { std::cout << file_arg << ": no issues found\n"; } else { for (auto& d : diags) { std::cerr << (d.severity == tools::DiagSeverity::Error ? "error" : "warning") << ": " << d.message << "\n"; } } return diags.empty() ? 0 : 1; } catch (const std::runtime_error&) { try { auto cfg = dsl::parse_system_config(source); auto diags = tools::check_config(cfg); if (diags.empty()) { std::cout << file_arg << ": no issues found\n"; } else { for (auto& d : diags) { std::cerr << (d.severity == tools::DiagSeverity::Error ? "error" : "warning") << ": " << d.message << "\n"; } } return diags.empty() ? 0 : 1; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); return 1; } } } if (subcommand == "resolve") { try { auto cfg = dsl::parse_system_config(source); auto registry = build_registry(cfg); auto plan = resolve::resolve(cfg, registry); if (!plan.missing.empty()) { for (auto& m : plan.missing) { std::cerr << "warning: package '" << m << "' not found in registry\n"; } } if (!plan.cycles.empty()) { std::cerr << "error: dependency cycle detected:\n"; for (auto& c : plan.cycles) { std::cerr << " " << c << "\n"; } return 1; } if (!plan.conflicts.empty()) { std::cerr << "error: package conflicts detected:\n"; for (auto& c : plan.conflicts) { std::cerr << " " << c << "\n"; } return 1; } std::cout << plan.steps.size() << " packages in build order:\n"; for (auto& step : plan.steps) { std::cout << " " << step.name << " (" << step.dependencies.size() << " deps"; if (!step.resolved.features.empty()) { std::cout << ", features:"; for (auto& [k, f] : step.resolved.features) { if (f.enabled && !f.flag.empty()) { std::cout << " " << k; } } } std::cout << ")\n"; } return plan.missing.empty() ? 0 : 1; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); return 1; } } if (subcommand == "fetch") { try { auto pkg = dsl::parse(source); auto result = fetch::fetch(pkg); if (result.ok()) { std::cout << "fetched to " << result.work_dir << "\n"; return 0; } std::cerr << "fetch failed: " << result.error << "\n"; return 1; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); return 1; } } if (subcommand == "rebuild") { try { auto cfg = dsl::parse_system_config(source); auto cs = rebuild::compute_changes(cfg); if (cs.added.empty() && cs.changed.empty() && !cs.kernel_changed && !cs.init_changed && !cs.bootloader_changed && !cs.services_changed) { std::cout << "nothing to rebuild\n"; return 0; } if (dry_run) { // Dry run: report changes without executing std::cout << cs.added.size() + cs.changed.size() << " packages to rebuild\n"; return 0; } auto registry = build_registry(cfg); auto plan = resolve::resolve(cfg, registry); if (!plan.missing.empty()) { for (auto& m : plan.missing) std::cerr << "warning: package '" << m << "' not found in registry\n"; } if (!plan.cycles.empty()) { std::cerr << "error: dependency cycle detected:\n"; for (auto& c : plan.cycles) std::cerr << " " << c << "\n"; return 1; } if (!plan.conflicts.empty()) { std::cerr << "error: package conflicts detected:\n"; for (auto& c : plan.conflicts) std::cerr << " " << c << "\n"; return 1; } if (plan.steps.empty()) { std::cout << "nothing to build\n"; return 0; } // Parse -w/-j flags after subcommand int workers = 1, jobs = 1; for (int i = arg_start + 1; i < argc; ++i) { auto arg = std::string_view(argv[i]); if (arg == "-w" && i + 1 < argc) workers = std::stoi(argv[++i]); if ((arg == "-j" || arg == "--jobs") && i + 1 < argc) jobs = std::stoi(argv[++i]); } // Build std::cout << cs.added.size() + cs.changed.size() << " packages to rebuild\n"; auto work_root = paths::temp_dir(); auto sr = sched::run(plan, work_root.string(), workers, jobs); if (!sr.ok) { std::cerr << sr.failed.size() << " packages failed to build\n"; for (auto& f : sr.failed) std::cerr << " " << f << "\n"; return 1; } // Install built packages for (auto& step : plan.steps) { if (std::find(sr.built.begin(), sr.built.end(), step.name) == sr.built.end()) continue; auto work_dir = work_root / step.name; auto inst = install::install(step, work_dir); if (!inst.ok) { std::cerr << "install failed for " << step.name << ": " << inst.error << "\n"; return 1; } } // Generate service files for enabled services auto is = kappa::service::parse_init_system(cfg.boot.init); if (is != kappa::service::InitSystem::Unknown && !cfg.services.empty()) { auto resolved = config::resolve_services(cfg, registry); for (auto& sref : cfg.services) { if (!sref.enable) continue; auto it = resolved.find(sref.name); if (it == resolved.end()) { std::cerr << "warning: service '" << sref.name << "' not found in any package\n"; continue; } auto spec = service::ServiceSpec::from_service_init(it->second); auto svc_result = service::install_service(is, spec); if (!svc_result.ok) std::cerr << "service generation failed for " << sref.name << ": " << svc_result.error << "\n"; } } // Generate bootloader config if (cs.bootloader_changed) { auto bl = kappa::boot::parse_bootloader(cfg.boot.bootloader); if (bl != kappa::boot::Bootloader::Unknown) { boot::BootSpec boot_spec; boot_spec.kernel_path = cfg.boot.kernel; boot_spec.init_path = cfg.boot.init; boot_spec.root = cfg.boot.root; for (auto& [k, v] : cfg.boot.params) boot_spec.kernel_params += k + "=" + v + " "; auto boot_result = boot::install_bootloader_config(bl, boot_spec); if (!boot_result.ok) std::cerr << "bootloader config failed: " << boot_result.error << "\n"; } } // Activate system configuration if (!cfg.system.hostname.empty()) system::write_hostname(cfg.system.hostname); if (!cfg.system.timezone.empty()) system::write_timezone(cfg.system.timezone); std::cout << "rebuild complete — " << sr.built.size() << " packages built\n"; return 0; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); return 1; } } } catch (const std::exception& e) { std::cerr << "error: " << e.what() << "\n"; return 1; } return 1; // unreachable }