feat: add repos DSL, index format, and kappa index command

- Add repos { } block to system config with named repos, channels,
  mirrors, and priority

- Add index "name" { } file format for package indexes

- Add kappa index <dir> subcommand that scans .kap files and
  generates index.kap

- parse_index() and build_index() in DSL module

- format_index() roundtrip support in tools module

- validate and format subcommands handle all three formats
  (package, config, index) with automatic detection

- Backward compatible: remotes = [...] still works unchanged
This commit is contained in:
2026-08-04 13:48:25 -04:00
parent 86f578bd4a
commit 4f1ce17ec8
9 changed files with 285 additions and 11 deletions
+4
View File
@@ -33,6 +33,8 @@ static const std::unordered_map<std::string_view, TokenType> keywords = {
{"uninstall", TokenType::KwUninstall},
{"true", TokenType::KwTrue},
{"false", TokenType::KwFalse},
{"index", TokenType::KwIndex},
{"repos", TokenType::KwRepos},
};
std::string_view token_name(TokenType type) {
@@ -74,6 +76,8 @@ std::string_view token_name(TokenType type) {
case TokenType::KwUninstall: return "uninstall";
case TokenType::KwTrue: return "true";
case TokenType::KwFalse: return "false";
case TokenType::KwIndex: return "index";
case TokenType::KwRepos: return "repos";
}
return "?";
}
+157 -1
View File
@@ -1,6 +1,7 @@
#include "kappa/dsl/system.hpp"
#include "kappa/dsl/error.hpp"
#include "kappa/dsl/lexer.hpp"
#include "kappa/dsl/parser.hpp"
#include <filesystem>
#include <format>
@@ -29,6 +30,7 @@ private:
void parse_boot_block(SystemConfig& cfg);
void parse_users_block(SystemConfig& cfg);
void parse_groups_block(SystemConfig& cfg);
void parse_repos_block(SystemConfig& cfg);
std::string consume_ident();
std::string consume_string();
@@ -85,7 +87,9 @@ std::string SysParser::consume_ident() {
at(TokenType::KwCheck) ||
at(TokenType::KwInstall) ||
at(TokenType::KwTrue) ||
at(TokenType::KwFalse)) {
at(TokenType::KwFalse) ||
at(TokenType::KwIndex) ||
at(TokenType::KwRepos)) {
auto lexeme = current_.lexeme;
advance();
return lexeme;
@@ -184,6 +188,7 @@ SystemConfig SysParser::parse() {
else if (kw == "boot") { parse_boot_block(cfg); }
else if (kw == "users") { parse_users_block(cfg); }
else if (kw == "groups") { parse_groups_block(cfg); }
else if (kw == "repos") { parse_repos_block(cfg); }
else {
throw ParseError(current_.line, current_.col,
std::format("unknown section '{}'", kw));
@@ -456,6 +461,63 @@ void SysParser::parse_groups_block(SystemConfig& cfg) {
consume(TokenType::Rbrace);
}
void SysParser::parse_repos_block(SystemConfig& cfg) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace)) break;
RepoDef repo;
repo.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 == "url") {
consume(TokenType::Equals);
repo.url = consume_string();
} else if (key == "channels") {
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
repo.channels.push_back(consume_string());
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
} else if (key == "mirrors") {
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
repo.mirrors.push_back(consume_string());
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
} else if (key == "priority") {
consume(TokenType::Equals);
repo.priority = parse_int(current_.line, current_.col,
current_.lexeme);
advance();
}
skip_newlines();
}
consume(TokenType::Rbrace);
}
cfg.repos.push_back(std::move(repo));
}
consume(TokenType::Rbrace);
}
SystemConfig parse_system_config(std::string_view source) {
SysParser p(source);
return p.parse();
@@ -466,6 +528,7 @@ static void merge_config(SystemConfig& base, SystemConfig&& imported) {
if (!imported.system.timezone.empty()) { base.system.timezone = std::move(imported.system.timezone); }
for (auto& e : imported.system.env) { base.system.env.push_back(std::move(e)); }
for (auto& r : imported.remotes) { base.remotes.push_back(std::move(r)); }
for (auto& r : imported.repos) { base.repos.push_back(std::move(r)); }
for (auto& [k, v] : imported.system.config) { base.system.config[k] = std::move(v); }
for (auto& [k, v] : imported.system.features) { base.system.features[k] = std::move(v); }
if (imported.system.rollback.keep > 0) { base.system.rollback.keep = imported.system.rollback.keep; }
@@ -497,4 +560,97 @@ SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_di
return resolved;
}
IndexDef parse_index(std::string_view source) {
Lexer lexer(source);
Token current;
auto advance = [&] { current = lexer.next(); };
advance();
auto skip_newlines = [&] {
while (current.type == TokenType::Newline) { advance(); }
};
auto consume = [&](TokenType type) -> Token {
if (current.type != type) {
throw ParseError(current.line, current.col,
msg_expected(token_name(type),
token_name(current.type)));
}
Token t = std::move(current);
advance();
return t;
};
skip_newlines();
consume(TokenType::KwIndex);
// index "kappa-os/stable" { ... }
auto name_tok = consume(TokenType::String);
consume(TokenType::Lbrace);
IndexDef idx;
idx.name = std::move(name_tok.lexeme);
skip_newlines();
while (current.type != TokenType::Rbrace && current.type != TokenType::Eof) {
skip_newlines();
if (current.type == TokenType::Rbrace) break;
IndexEntry entry;
entry.name = current.lexeme;
advance();
if (current.type == TokenType::Lbrace) {
advance();
skip_newlines();
while (current.type != TokenType::Rbrace && current.type != TokenType::Eof) {
if (current.type == TokenType::Newline) { advance(); continue; }
auto key = current.lexeme;
advance();
consume(TokenType::Equals);
if (key == "version") {
entry.version = consume(TokenType::String).lexeme;
}
skip_newlines();
}
consume(TokenType::Rbrace);
}
idx.packages.push_back(std::move(entry));
skip_newlines();
}
consume(TokenType::Rbrace);
return idx;
}
IndexDef build_index(const std::string& directory) {
IndexDef idx;
auto dirname = std::filesystem::path(directory).filename().string();
// If the directory is empty, drop the "."
if (dirname.empty() || dirname == ".") {
dirname = "local";
}
idx.name = dirname;
std::error_code ec;
for (auto& entry : std::filesystem::directory_iterator(directory, ec)) {
if (ec) break;
if (!entry.is_regular_file()) continue;
auto ext = entry.path().extension().string();
if (ext != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
try {
auto pkg = parse(buf.str());
idx.packages.push_back({pkg.name, pkg.version});
} catch (...) {
continue;
}
}
return idx;
}
} // namespace kappa::dsl
+55 -9
View File
@@ -52,6 +52,7 @@ Subcommands:
rebuild <config> Compare config to installed state, rebuild changed
list List installed packages
rollback Show available generations
index <dir> Build an index.kap from .kap files in a directory
Options:
-h, --help Show this help message
@@ -196,8 +197,9 @@ int main(int argc, char* argv[]) {
|| (subcommand == "build")
|| (subcommand == "rebuild")
|| (subcommand == "list")
|| (subcommand == "fetch-package")
|| (subcommand == "rollback");
|| (subcommand == "fetch-package")
|| (subcommand == "rollback")
|| (subcommand == "index");
if (!valid_subcommand) {
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
@@ -276,6 +278,29 @@ int main(int argc, char* argv[]) {
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 == "fetch-package") {
if (file_arg == nullptr) {
std::cerr << "error: no package name specified\n";
@@ -331,7 +356,8 @@ int main(int argc, char* argv[]) {
if (file_arg == nullptr
&& subcommand != "list"
&& subcommand != "rollback") {
&& subcommand != "rollback"
&& subcommand != "index") {
std::cerr << "error: no input file specified\n";
return 1;
}
@@ -378,6 +404,12 @@ int main(int argc, char* argv[]) {
}
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);
@@ -414,9 +446,17 @@ int main(int argc, char* argv[]) {
return 1;
}
return 0;
} catch (const std::runtime_error& e) {
handle_parse_error(file_arg, source, e);
return 1;
} 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;
}
}
}
}
@@ -472,9 +512,15 @@ int main(int argc, char* argv[]) {
auto cfg = dsl::parse_system_config(source);
tools::format_config(std::cout, cfg);
return 0;
} catch (const std::runtime_error& e) {
handle_parse_error(file_arg, source, e);
return 1;
} 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;
}
}
}
}
+36
View File
@@ -237,6 +237,34 @@ void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
os << "]\n\n";
}
if (!cfg.repos.empty()) {
os << "repos {\n";
for (auto& repo : cfg.repos) {
os << " " << repo.name << " {\n";
os << " url = \"" << repo.url << "\"\n";
if (!repo.channels.empty()) {
os << " channels = [";
for (std::size_t i = 0; i < repo.channels.size(); ++i) {
if (i > 0) { os << ", "; }
os << '"' << repo.channels[i] << '"';
}
os << "]\n";
}
if (!repo.mirrors.empty()) {
os << " mirrors = [\n";
for (auto& m : repo.mirrors) {
os << " \"" << m << "\",\n";
}
os << " ]\n";
}
if (repo.priority != 50) {
os << " priority = " << repo.priority << "\n";
}
os << " }\n";
}
os << "}\n\n";
}
if (!cfg.assertions.empty()) {
os << "assert {\n";
for (auto& a : cfg.assertions) {
@@ -345,4 +373,12 @@ void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
}
}
void format_index(std::ostream& os, const dsl::IndexDef& idx) {
os << "index \"" << idx.name << "\" {\n";
for (auto& pkg : idx.packages) {
os << " " << pkg.name << " { version = \"" << pkg.version << "\" }\n";
}
os << "}\n";
}
} // namespace kappa::tools