From d5f7d397edb3a403a1a6fe6d0c99031cf2337408 Mon Sep 17 00:00:00 2001 From: HuntedByTheIRS Date: Wed, 29 Jul 2026 22:50:39 -0400 Subject: [PATCH] feat: system config DSL, services, CLI with rustc diagnostics - System config parser (system/packages/services/boot/users blocks) - Boot block: kernel, init, efi, swap, bootloader, root partition - System-wide features, rollback config, swap partition - Service block: per-init variants (runit, s6, etc.), system init selection via boot.init - Feature-gated dependencies, config values accept numbers/bools/strings - CLI: parse-package, parse-config, validate subcommands - Rustc-style error diagnostics with source context, carets, help text - Diagnostic module with ANSI color output --- CMakeLists.txt | 2 + examples/config.kap | 87 ++++++++ examples/foo.kap | 14 ++ include/kappa/cli/diagnostic.hpp | 31 +++ include/kappa/dsl/ast.hpp | 9 + include/kappa/dsl/system.hpp | 64 ++++++ include/kappa/dsl/token.hpp | 1 + src/cli/diagnostic.cpp | 94 ++++++++ src/dsl/lexer.cpp | 2 + src/dsl/parser.cpp | 46 ++++ src/dsl/system.cpp | 358 +++++++++++++++++++++++++++++++ src/main.cpp | 161 ++++++++++++-- 12 files changed, 850 insertions(+), 19 deletions(-) create mode 100644 examples/config.kap create mode 100644 include/kappa/cli/diagnostic.hpp create mode 100644 include/kappa/dsl/system.hpp create mode 100644 src/cli/diagnostic.cpp create mode 100644 src/dsl/system.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 043d788..fe59ac6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,9 @@ add_executable(kappa src/paths.cpp src/dsl/lexer.cpp src/dsl/parser.cpp + src/dsl/system.cpp src/eval/vars.cpp + src/cli/diagnostic.cpp ) target_include_directories(kappa PRIVATE include) target_link_libraries(kappa PRIVATE tomlplusplus::tomlplusplus) diff --git a/examples/config.kap b/examples/config.kap new file mode 100644 index 0000000..9e610fe --- /dev/null +++ b/examples/config.kap @@ -0,0 +1,87 @@ +/* + * Kappa system configuration. + * Lives at /kappa/system/config.kap + */ +system { + hostname = "kappa.local" + timezone = "America/New_York" + + features { + ssl = true + wayland = false + pulseaudio = false + } + + env { + CFLAGS = "-O2 -march=native" + LDFLAGS = "-Wl,--as-needed" + MAKEFLAGS = "-j8" + } + + config { + prefix = "/usr" + log_dir = "/var/log" + } + + rollback { + keep = 5 + } +} + +packages { + foo { + version = ">=1.2" + features { + gui = true + } + config { + port = "9090" + ssl = true + } + } + bar {} + baz { + features { + ssl = false + } + } + + /* + // Per-package overrides can also live in + // /kappa/system/builds/.kap + */ +} + +services { + nginx { + enable = true + port = 80 + ssl = true + } + sshd { + enable = true + port = 22 + } + cron { + enable = false + } +} + +boot { + kernel = "linux" + init = "s6" + efi = "/dev/sda2" + swap = "/dev/sda3" + bootloader = "limine" + root = "/dev/sda1" +} + +users { + specter { + shell = "/bin/zsh" + groups = ["wheel", "audio", "docker"] + } + root { + shell = "/bin/zsh" + } +} diff --git a/examples/foo.kap b/examples/foo.kap index aaaf1b8..b268465 100644 --- a/examples/foo.kap +++ b/examples/foo.kap @@ -59,6 +59,20 @@ package "foo" { CFLAGS ?= "-g" } + service { + runit { + exec = "/usr/bin/foo --daemon" + type = "forking" + user = "foo" + } + s6 { + exec = "/usr/bin/foo" + type = "longrun" + ports = [80, 443] + user = "foo" + } + } + prepare { patch "fix-build.patch" } diff --git a/include/kappa/cli/diagnostic.hpp b/include/kappa/cli/diagnostic.hpp new file mode 100644 index 0000000..4799c2a --- /dev/null +++ b/include/kappa/cli/diagnostic.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +namespace kappa::cli { + +struct SourceLocation { + std::string_view file; + int line = 0; + int col = 0; +}; + +void print_error(std::ostream& os, + std::string_view source, + SourceLocation loc, + std::string_view message); + +void print_error(std::ostream& os, + std::string_view source, + SourceLocation loc, + std::string_view message, + std::string_view help); + +void print_note(std::ostream& os, + std::string_view source, + SourceLocation loc, + std::string_view message); + +} // namespace kappa::cli diff --git a/include/kappa/dsl/ast.hpp b/include/kappa/dsl/ast.hpp index 7b1ad78..a2cd72a 100644 --- a/include/kappa/dsl/ast.hpp +++ b/include/kappa/dsl/ast.hpp @@ -44,6 +44,14 @@ struct EnvEntry { bool soft = false; }; +struct ServiceInit { + std::string exec; + std::string type; + std::string user; + std::vector ports; + std::unordered_map env; +}; + struct PackageDef { std::string name; std::string version; @@ -56,6 +64,7 @@ struct PackageDef { std::vector config_files; std::vector patches; std::vector env_entries; + std::unordered_map service; std::unordered_set const_keys; Phase prepare; Phase build; diff --git a/include/kappa/dsl/system.hpp b/include/kappa/dsl/system.hpp new file mode 100644 index 0000000..1022a2e --- /dev/null +++ b/include/kappa/dsl/system.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "kappa/dsl/ast.hpp" + +#include +#include +#include +#include + +namespace kappa::dsl { + +struct RollbackConfig { + int keep = 0; +}; + +struct SystemBlock { + std::string hostname; + std::string timezone; + std::vector env; + std::unordered_map config; + std::unordered_map features; + RollbackConfig rollback; +}; + +struct PackageRef { + std::string name; + std::string version; + std::unordered_map features; + std::unordered_map config; +}; + +struct BootBlock { + std::string kernel; + std::string init; + std::string efi; + std::string swap; + std::string bootloader; + std::unordered_map params; +}; + +struct UserRef { + std::string name; + std::string shell; + std::vector groups; + std::unordered_map extra; +}; + +struct ServiceRef { + std::string name; + bool enable = false; + std::unordered_map config; +}; + +struct SystemConfig { + SystemBlock system; + std::vector packages; + std::vector services; + BootBlock boot; + std::vector users; +}; + +SystemConfig parse_system_config(std::string_view source); + +} // namespace kappa::dsl diff --git a/include/kappa/dsl/token.hpp b/include/kappa/dsl/token.hpp index c72bda5..e3fab7f 100644 --- a/include/kappa/dsl/token.hpp +++ b/include/kappa/dsl/token.hpp @@ -34,6 +34,7 @@ enum class TokenType { KwLicense, KwPatches, KwEnv, + KwService, KwPrepare, KwBuild, KwCheck, diff --git a/src/cli/diagnostic.cpp b/src/cli/diagnostic.cpp new file mode 100644 index 0000000..9799056 --- /dev/null +++ b/src/cli/diagnostic.cpp @@ -0,0 +1,94 @@ +#include "kappa/cli/diagnostic.hpp" + +#include +#include +#include +#include + +namespace kappa::cli { + +using namespace std::string_view_literals; + +static constexpr auto bold = "\033[1m"sv; +static constexpr auto red = "\033[31m"sv; +static constexpr auto cyan = "\033[36m"sv; +static constexpr auto yellow = "\033[33m"sv; +static constexpr auto reset = "\033[0m"sv; + +static std::string_view line_at(std::string_view source, int target_line) { + int current = 1; + std::size_t start = 0; + for (std::size_t i = 0; i < source.size(); ++i) { + if (current == target_line) { start = i; break; } + if (source[i] == '\n') { ++current; } + } + if (current != target_line) { return ""sv; } + + auto end = source.find('\n', start); + if (end == std::string_view::npos) { end = source.size(); } + return source.substr(start, end - start); +} + +static void print_header(std::ostream& os, + std::string_view severity, + std::string_view color, + std::string_view file, + int line, int col, + std::string_view message) { + os << color << bold << severity << ": " << reset + << color << message << reset << '\n' + << " " << cyan << "\u250C\u2500 " << file << ':' << line << ':' << col + << reset << '\n'; +} + +void print_error(std::ostream& os, + std::string_view source, + SourceLocation loc, + std::string_view message) { + print_header(os, "error", red, loc.file, loc.line, loc.col, message); + + auto line_src = line_at(source, loc.line); + if (!line_src.empty()) { + auto line_num = std::format("{}", loc.line); + os << " " << cyan << "\u2502" << reset << '\n' + << ' ' << line_num << " " << cyan << "\u2502 " << reset + << line_src << '\n'; + + os << " " << std::string(line_num.size(), ' ') + << ' ' << cyan << "\u2502 " << reset; + for (int i = 0; i < loc.col - 1; ++i) { os << ' '; } + os << red << bold << '^' << reset << '\n'; + } + + os << " " << cyan << "\u2502" << reset << '\n'; +} + +void print_error(std::ostream& os, + std::string_view source, + SourceLocation loc, + std::string_view message, + std::string_view help) { + print_error(os, source, loc, message); + os << " " << cyan << "\u2570\u2500\u2500 " << reset + << yellow << "help: " << reset << help << '\n'; +} + +void print_note(std::ostream& os, + std::string_view source, + SourceLocation loc, + std::string_view message) { + print_header(os, "note", cyan, loc.file, loc.line, loc.col, message); + auto line_src = line_at(source, loc.line); + if (!line_src.empty()) { + auto line_num = std::format("{}", loc.line); + os << " " << cyan << "\u2502" << reset << '\n' + << ' ' << line_num << " " << cyan << "\u2502 " << reset + << line_src << '\n' + << " " << std::string(line_num.size(), ' ') + << ' ' << cyan << "\u2502" << reset << '\n'; + } + os << " " << cyan << "\u2570\u2500\u2500 " << reset + << cyan << "note: " << reset << message << '\n'; +} + +} // namespace kappa::cli diff --git a/src/dsl/lexer.cpp b/src/dsl/lexer.cpp index 2e769e8..6446e3e 100644 --- a/src/dsl/lexer.cpp +++ b/src/dsl/lexer.cpp @@ -19,6 +19,7 @@ static const std::unordered_map keywords = { {"license", TokenType::KwLicense}, {"patches", TokenType::KwPatches}, {"env", TokenType::KwEnv}, + {"service", TokenType::KwService}, {"prepare", TokenType::KwPrepare}, {"build", TokenType::KwBuild}, {"check", TokenType::KwCheck}, @@ -51,6 +52,7 @@ std::string_view token_name(TokenType type) { case TokenType::KwLicense: return "license"; case TokenType::KwPatches: return "patches"; case TokenType::KwEnv: return "env"; + case TokenType::KwService: return "service"; case TokenType::KwPrepare: return "prepare"; case TokenType::KwBuild: return "build"; case TokenType::KwCheck: return "check"; diff --git a/src/dsl/parser.cpp b/src/dsl/parser.cpp index bd1c602..d68c22f 100644 --- a/src/dsl/parser.cpp +++ b/src/dsl/parser.cpp @@ -209,6 +209,52 @@ void Parser::parse_body(PackageDef& pkg) { consume(TokenType::Rbrace); break; + case TokenType::KwService: + consume(TokenType::KwService); + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto init_name = current_.lexeme; // e.g. "runit", "s6" + advance(); + consume(TokenType::Lbrace); + skip_newlines(); + ServiceInit si; + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto key = current_.lexeme; + advance(); + consume(TokenType::Equals); + if (key == "exec") { + si.exec = consume(TokenType::String).lexeme; + } else if (key == "type") { + si.type = consume(TokenType::String).lexeme; + } else if (key == "user") { + si.user = consume(TokenType::String).lexeme; + } else if (key == "ports") { + consume(TokenType::Lbracket); + skip_newlines(); + while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) { + si.ports.push_back( + std::stoi(std::string(current_.lexeme))); + advance(); + skip_newlines(); + if (at(TokenType::Comma)) { consume(TokenType::Comma); } + skip_newlines(); + } + consume(TokenType::Rbracket); + } else { + si.env[key] = consume(TokenType::String).lexeme; + } + skip_newlines(); + } + consume(TokenType::Rbrace); + pkg.service[std::string(init_name)] = std::move(si); + skip_newlines(); + } + consume(TokenType::Rbrace); + break; + case TokenType::KwPrepare: consume(TokenType::KwPrepare); pkg.prepare = parse_phase(); diff --git a/src/dsl/system.cpp b/src/dsl/system.cpp new file mode 100644 index 0000000..905a490 --- /dev/null +++ b/src/dsl/system.cpp @@ -0,0 +1,358 @@ +#include "kappa/dsl/system.hpp" +#include "kappa/dsl/lexer.hpp" + +#include +#include + +namespace kappa::dsl { + +class ParseError : public std::runtime_error { +public: + ParseError(int line, int col, const std::string& msg) + : std::runtime_error(std::format("{}:{}: {}", line, col, msg)) {} +}; + +class SysParser { +public: + explicit SysParser(std::string_view source) : lexer_(source) { advance(); } + + SystemConfig parse(); + +private: + void advance(); + Token consume(TokenType type); + void skip_newlines(); + bool at(TokenType type) const { return current_.type == type; } + + bool parse_bool(); + void parse_system_block(SystemConfig& cfg); + void parse_packages_block(SystemConfig& cfg); + void parse_services_block(SystemConfig& cfg); + void parse_boot_block(SystemConfig& cfg); + void parse_users_block(SystemConfig& cfg); + + std::string consume_ident(); + std::string consume_string(); + + Token current_; + Lexer lexer_; +}; + +void SysParser::advance() { current_ = lexer_.next(); } + +void SysParser::skip_newlines() { + while (at(TokenType::Newline)) { advance(); } +} + +Token SysParser::consume(TokenType type) { + if (!at(type)) { + throw ParseError(current_.line, current_.col, + std::format("expected '{}', got '{}'", + token_name(type), token_name(current_.type))); + } + Token t = std::move(current_); + advance(); + return t; +} + +std::string SysParser::consume_ident() { + if (at(TokenType::Ident) || + at(TokenType::KwEnv) || + at(TokenType::KwConfig) || + at(TokenType::KwFeatures) || + at(TokenType::KwVersion) || + at(TokenType::KwSource) || + at(TokenType::KwLicense) || + at(TokenType::KwPatches) || + at(TokenType::KwConst) || + at(TokenType::KwPackage) || + at(TokenType::KwDepends) || + at(TokenType::KwProvides) || + at(TokenType::KwOutputs) || + at(TokenType::KwPrepare) || + at(TokenType::KwBuild) || + at(TokenType::KwCheck) || + at(TokenType::KwInstall) || + at(TokenType::KwTrue) || + at(TokenType::KwFalse)) { + auto lexeme = current_.lexeme; + advance(); + return lexeme; + } + throw ParseError(current_.line, current_.col, + std::format("expected identifier, got '{}'", + token_name(current_.type))); +} + +std::string SysParser::consume_string() { + if (at(TokenType::KwTrue)) { advance(); return "true"; } + if (at(TokenType::KwFalse)) { advance(); return "false"; } + return consume(TokenType::String).lexeme; +} + +bool SysParser::parse_bool() { + if (at(TokenType::KwTrue)) { advance(); return true; } + if (at(TokenType::KwFalse)) { advance(); return false; } + throw ParseError(current_.line, current_.col, + std::format("expected 'true' or 'false', got '{}'", + token_name(current_.type))); +} + +SystemConfig SysParser::parse() { + skip_newlines(); + SystemConfig cfg; + + while (!at(TokenType::Eof)) { + skip_newlines(); + if (at(TokenType::Eof)) { break; } + + auto kw = consume_ident(); + + if (kw == "system") { parse_system_block(cfg); } + else if (kw == "packages") { parse_packages_block(cfg); } + else if (kw == "services") { parse_services_block(cfg); } + else if (kw == "boot") { parse_boot_block(cfg); } + else if (kw == "users") { parse_users_block(cfg); } + else { + throw ParseError(current_.line, current_.col, + std::format("unknown section '{}'", kw)); + } + } + + return cfg; +} + +void SysParser::parse_system_block(SystemConfig& cfg) { + consume(TokenType::Lbrace); + skip_newlines(); + + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + skip_newlines(); + if (at(TokenType::Rbrace)) { break; } + + auto key = consume_ident(); + if (key == "hostname") { + consume(TokenType::Equals); + cfg.system.hostname = consume_string(); + } else if (key == "timezone") { + consume(TokenType::Equals); + cfg.system.timezone = consume_string(); + } else if (key == "env") { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto k = consume_ident(); + bool soft = false; + if (at(TokenType::Ident) && current_.lexeme == "?=") { + soft = true; + advance(); + } else { + consume(TokenType::Equals); + } + cfg.system.env.push_back({std::move(k), consume_string(), soft}); + skip_newlines(); + } + consume(TokenType::Rbrace); + } else if (key == "config") { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto k = consume_ident(); + consume(TokenType::Equals); + cfg.system.config[k] = consume_string(); + skip_newlines(); + } + consume(TokenType::Rbrace); + } else if (key == "features") { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto k = consume_ident(); + consume(TokenType::Equals); + cfg.system.features[k] = FeatureDef{parse_bool(), false, ""}; + skip_newlines(); + } + consume(TokenType::Rbrace); + } else if (key == "rollback") { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto k = consume_ident(); + consume(TokenType::Equals); + if (k == "keep") { + cfg.system.rollback.keep = std::stoi(std::string(current_.lexeme)); + advance(); + } + skip_newlines(); + } + consume(TokenType::Rbrace); + } else { + throw ParseError(current_.line, current_.col, + std::format("unknown system key '{}'", key)); + } + } + consume(TokenType::Rbrace); +} + +void SysParser::parse_packages_block(SystemConfig& cfg) { + consume(TokenType::Lbrace); + skip_newlines(); + + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + skip_newlines(); + if (at(TokenType::Rbrace)) { break; } + + PackageRef pkg; + pkg.name = consume_ident(); + + if (at(TokenType::Lbrace)) { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + skip_newlines(); + if (at(TokenType::Rbrace)) { break; } + auto key = consume_ident(); + if (key == "version") { + consume(TokenType::Equals); + pkg.version = consume_string(); + } else if (key == "features") { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto k = consume_ident(); + consume(TokenType::Equals); + pkg.features[k] = FeatureDef{parse_bool(), false, ""}; + skip_newlines(); + } + consume(TokenType::Rbrace); + } else if (key == "config") { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto k = consume_ident(); + consume(TokenType::Equals); + pkg.config[k] = current_.lexeme; + advance(); + skip_newlines(); + } + consume(TokenType::Rbrace); + } + } + consume(TokenType::Rbrace); + } + cfg.packages.push_back(std::move(pkg)); + } + consume(TokenType::Rbrace); +} + +void SysParser::parse_services_block(SystemConfig& cfg) { + consume(TokenType::Lbrace); + skip_newlines(); + + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + skip_newlines(); + if (at(TokenType::Rbrace)) { break; } + + ServiceRef svc; + svc.name = consume_ident(); + + if (at(TokenType::Lbrace)) { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto key = consume_ident(); + if (key == "enable") { + consume(TokenType::Equals); + svc.enable = parse_bool(); + } else { + consume(TokenType::Equals); + svc.config[key] = current_.lexeme; + advance(); + } + skip_newlines(); + } + consume(TokenType::Rbrace); + } + cfg.services.push_back(std::move(svc)); + } + consume(TokenType::Rbrace); +} + +void SysParser::parse_boot_block(SystemConfig& cfg) { + consume(TokenType::Lbrace); + skip_newlines(); + + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + skip_newlines(); + if (at(TokenType::Rbrace)) { break; } + auto key = consume_ident(); + consume(TokenType::Equals); + auto val = consume_string(); + if (key == "kernel") { cfg.boot.kernel = std::move(val); } + else if (key == "init") { cfg.boot.init = std::move(val); } + else if (key == "efi") { cfg.boot.efi = std::move(val); } + else if (key == "swap") { cfg.boot.swap = std::move(val); } + else if (key == "bootloader"){ cfg.boot.bootloader = std::move(val); } + else { cfg.boot.params[key] = std::move(val); } + skip_newlines(); + } + consume(TokenType::Rbrace); +} + +void SysParser::parse_users_block(SystemConfig& cfg) { + consume(TokenType::Lbrace); + skip_newlines(); + + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + skip_newlines(); + if (at(TokenType::Rbrace)) { break; } + + UserRef u; + u.name = consume_ident(); + + if (at(TokenType::Lbrace)) { + consume(TokenType::Lbrace); + skip_newlines(); + while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { + if (at(TokenType::Newline)) { advance(); continue; } + auto key = consume_ident(); + if (key == "shell") { + consume(TokenType::Equals); + u.shell = consume_string(); + } else if (key == "groups") { + consume(TokenType::Equals); + consume(TokenType::Lbracket); + skip_newlines(); + while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) { + u.groups.push_back(consume_string()); + skip_newlines(); + if (at(TokenType::Comma)) { consume(TokenType::Comma); } + skip_newlines(); + } + consume(TokenType::Rbracket); + } else { + consume(TokenType::Equals); + u.extra[key] = consume_string(); + } + skip_newlines(); + } + consume(TokenType::Rbrace); + } + cfg.users.push_back(std::move(u)); + } + consume(TokenType::Rbrace); +} + +SystemConfig parse_system_config(std::string_view source) { + SysParser p(source); + return p.parse(); +} + +} // namespace kappa::dsl diff --git a/src/main.cpp b/src/main.cpp index 42311b4..9df2efc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,28 +1,151 @@ -#include "kappa/package.hpp" +#include "kappa/cli/diagnostic.hpp" +#include "kappa/dsl/parser.hpp" +#include "kappa/dsl/system.hpp" -#include #include -#include -#include +#include #include +#include +#include -int main(int argc, char* argv[]) { - std::filesystem::path toml_path = (argc > 1) ? argv[1] : "package.toml"; +using namespace std::string_view_literals; +using namespace kappa; - if (!std::filesystem::exists(toml_path)) { - std::cerr << "Error: '" << toml_path << "' not found\n"; - return 1; +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) + +Options: + -h, --help Show this help message + -V, --version Show version information +)"; +} + +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) { + std::cerr << "error: cannot open '" << path << "'\n"; + std::exit(1); } + std::ostringstream buf; + buf << in.rdbuf(); + return buf.str(); +} - try { - auto pkg = kappa::parse_package(toml_path.native()); - kappa::print_package(std::cout, pkg); - return 0; - } catch (const toml::parse_error& e) { - std::cerr << "Parse error:\n" << e << '\n'; - return 1; - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << '\n'; - return 1; +static void handle_parse_error(const char* path, + std::string_view source, + const std::runtime_error& e) { + // ParseError format: "line:col: message" + 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) { + 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); // skip ": " + + cli::print_error(std::cerr, source, + {path, line, col}, message, + "check the syntax at this location"); + } else { + std::cerr << "error: " << e.what() << '\n'; } } + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "kappa: missing subcommand\n\n"; + print_usage(); + return 1; + } + + auto subcommand = std::string_view(argv[1]); + + if (subcommand == "-h" || subcommand == "--help") { + print_usage(); + return 0; + } + if (subcommand == "-V" || subcommand == "--version") { + std::cout << version << '\n'; + return 0; + } + + // Find the file argument (skip flags) + const char* file_arg = nullptr; + for (int i = 2; i < argc; ++i) { + if (!is_flag(argv[i])) { + file_arg = argv[i]; + break; + } + } + + if (file_arg == nullptr) { + std::cerr << "error: no input file specified\n"; + return 1; + } + + auto source = 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"; + 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 { + dsl::parse_system_config(source); + std::cout << file_arg << ": valid system configuration\n"; + return 0; + } catch (const std::runtime_error& e) { + handle_parse_error(file_arg, source, e); + return 1; + } + } + } + + std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n"; + print_usage(); + return 1; +}