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
This commit is contained in:
2026-07-29 22:50:39 -04:00
parent a8b53681fc
commit d5f7d397ed
12 changed files with 850 additions and 19 deletions
+2
View File
@@ -27,7 +27,9 @@ add_executable(kappa
src/paths.cpp src/paths.cpp
src/dsl/lexer.cpp src/dsl/lexer.cpp
src/dsl/parser.cpp src/dsl/parser.cpp
src/dsl/system.cpp
src/eval/vars.cpp src/eval/vars.cpp
src/cli/diagnostic.cpp
) )
target_include_directories(kappa PRIVATE include) target_include_directories(kappa PRIVATE include)
target_link_libraries(kappa PRIVATE tomlplusplus::tomlplusplus) target_link_libraries(kappa PRIVATE tomlplusplus::tomlplusplus)
+87
View File
@@ -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/<name>.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"
}
}
+14
View File
@@ -59,6 +59,20 @@ package "foo" {
CFLAGS ?= "-g" 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 { prepare {
patch "fix-build.patch" patch "fix-build.patch"
} }
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <ostream>
#include <string>
#include <string_view>
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
+9
View File
@@ -44,6 +44,14 @@ struct EnvEntry {
bool soft = false; bool soft = false;
}; };
struct ServiceInit {
std::string exec;
std::string type;
std::string user;
std::vector<int> ports;
std::unordered_map<std::string, std::string> env;
};
struct PackageDef { struct PackageDef {
std::string name; std::string name;
std::string version; std::string version;
@@ -56,6 +64,7 @@ struct PackageDef {
std::vector<ConfigFile> config_files; std::vector<ConfigFile> config_files;
std::vector<Patch> patches; std::vector<Patch> patches;
std::vector<EnvEntry> env_entries; std::vector<EnvEntry> env_entries;
std::unordered_map<std::string, ServiceInit> service;
std::unordered_set<std::string> const_keys; std::unordered_set<std::string> const_keys;
Phase prepare; Phase prepare;
Phase build; Phase build;
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace kappa::dsl {
struct RollbackConfig {
int keep = 0;
};
struct SystemBlock {
std::string hostname;
std::string timezone;
std::vector<EnvEntry> env;
std::unordered_map<std::string, std::string> config;
std::unordered_map<std::string, FeatureDef> features;
RollbackConfig rollback;
};
struct PackageRef {
std::string name;
std::string version;
std::unordered_map<std::string, FeatureDef> features;
std::unordered_map<std::string, std::string> config;
};
struct BootBlock {
std::string kernel;
std::string init;
std::string efi;
std::string swap;
std::string bootloader;
std::unordered_map<std::string, std::string> params;
};
struct UserRef {
std::string name;
std::string shell;
std::vector<std::string> groups;
std::unordered_map<std::string, std::string> extra;
};
struct ServiceRef {
std::string name;
bool enable = false;
std::unordered_map<std::string, std::string> config;
};
struct SystemConfig {
SystemBlock system;
std::vector<PackageRef> packages;
std::vector<ServiceRef> services;
BootBlock boot;
std::vector<UserRef> users;
};
SystemConfig parse_system_config(std::string_view source);
} // namespace kappa::dsl
+1
View File
@@ -34,6 +34,7 @@ enum class TokenType {
KwLicense, KwLicense,
KwPatches, KwPatches,
KwEnv, KwEnv,
KwService,
KwPrepare, KwPrepare,
KwBuild, KwBuild,
KwCheck, KwCheck,
+94
View File
@@ -0,0 +1,94 @@
#include "kappa/cli/diagnostic.hpp"
#include <algorithm>
#include <format>
#include <string_view>
#include <vector>
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
+2
View File
@@ -19,6 +19,7 @@ static const std::unordered_map<std::string_view, TokenType> keywords = {
{"license", TokenType::KwLicense}, {"license", TokenType::KwLicense},
{"patches", TokenType::KwPatches}, {"patches", TokenType::KwPatches},
{"env", TokenType::KwEnv}, {"env", TokenType::KwEnv},
{"service", TokenType::KwService},
{"prepare", TokenType::KwPrepare}, {"prepare", TokenType::KwPrepare},
{"build", TokenType::KwBuild}, {"build", TokenType::KwBuild},
{"check", TokenType::KwCheck}, {"check", TokenType::KwCheck},
@@ -51,6 +52,7 @@ std::string_view token_name(TokenType type) {
case TokenType::KwLicense: return "license"; case TokenType::KwLicense: return "license";
case TokenType::KwPatches: return "patches"; case TokenType::KwPatches: return "patches";
case TokenType::KwEnv: return "env"; case TokenType::KwEnv: return "env";
case TokenType::KwService: return "service";
case TokenType::KwPrepare: return "prepare"; case TokenType::KwPrepare: return "prepare";
case TokenType::KwBuild: return "build"; case TokenType::KwBuild: return "build";
case TokenType::KwCheck: return "check"; case TokenType::KwCheck: return "check";
+46
View File
@@ -209,6 +209,52 @@ void Parser::parse_body(PackageDef& pkg) {
consume(TokenType::Rbrace); consume(TokenType::Rbrace);
break; 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: case TokenType::KwPrepare:
consume(TokenType::KwPrepare); consume(TokenType::KwPrepare);
pkg.prepare = parse_phase(); pkg.prepare = parse_phase();
+358
View File
@@ -0,0 +1,358 @@
#include "kappa/dsl/system.hpp"
#include "kappa/dsl/lexer.hpp"
#include <format>
#include <stdexcept>
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
+142 -19
View File
@@ -1,28 +1,151 @@
#include "kappa/package.hpp" #include "kappa/cli/diagnostic.hpp"
#include "kappa/dsl/parser.hpp"
#include "kappa/dsl/system.hpp"
#include <toml++/toml.hpp>
#include <cstdlib> #include <cstdlib>
#include <exception> #include <fstream>
#include <filesystem>
#include <iostream> #include <iostream>
#include <sstream>
#include <string_view>
int main(int argc, char* argv[]) { using namespace std::string_view_literals;
std::filesystem::path toml_path = (argc > 1) ? argv[1] : "package.toml"; using namespace kappa;
if (!std::filesystem::exists(toml_path)) { static constexpr auto version = "kappa 0.1.0"sv;
std::cerr << "Error: '" << toml_path << "' not found\n";
return 1; static void print_usage() {
std::cout << R"(kappa — a declarative source-based package manager
Usage:
kappa <subcommand> [options] <file>
Subcommands:
parse-package <file> Parse and validate a package definition (.kap)
parse-config <file> Parse and validate a system configuration
validate <file> 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 { static void handle_parse_error(const char* path,
auto pkg = kappa::parse_package(toml_path.native()); std::string_view source,
kappa::print_package(std::cout, pkg); const std::runtime_error& e) {
return 0; // ParseError format: "line:col: message"
} catch (const toml::parse_error& e) { auto msg = std::string_view(e.what());
std::cerr << "Parse error:\n" << e << '\n'; auto first_colon = msg.find(':');
return 1; auto second_colon = msg.find(':', first_colon + 1);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n'; if (first_colon != std::string_view::npos &&
return 1; 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;
}