feat: add kappa add and kappa remove subcommands

- kappa add <pkg> [version] — adds/updates package in system config
  packages{} block; creates config if it doesn't exist

- kappa remove <pkg> — removes package from config; idempotent
  (no error if package isn't present)

- Supports --root flag for custom KAPPA_ROOT

- User workflow: kappa add make && kappa rebuild
  No manual config editing needed.
This commit is contained in:
2026-08-04 14:00:51 -04:00
parent 7b0538c392
commit 7345b026cc
+106 -1
View File
@@ -53,6 +53,8 @@ Subcommands:
list List installed packages list List installed packages
rollback Show available generations rollback Show available generations
index <dir> Build an index.kap from .kap files in a directory index <dir> Build an index.kap from .kap files in a directory
add <pkg> [v] Add a package to system config (optional version)
remove <pkg> Remove a package from system config
Options: Options:
-h, --help Show this help message -h, --help Show this help message
@@ -218,7 +220,9 @@ int main(int argc, char* argv[]) {
|| (subcommand == "list") || (subcommand == "list")
|| (subcommand == "fetch-package") || (subcommand == "fetch-package")
|| (subcommand == "rollback") || (subcommand == "rollback")
|| (subcommand == "index"); || (subcommand == "index")
|| (subcommand == "add")
|| (subcommand == "remove");
if (!valid_subcommand) { if (!valid_subcommand) {
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n"; std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
@@ -227,6 +231,7 @@ int main(int argc, char* argv[]) {
} }
const char* file_arg = nullptr; const char* file_arg = nullptr;
const char* version_arg = nullptr;
bool dry_run = false; bool dry_run = false;
for (int i = arg_start + 1; i < argc; ++i) { for (int i = arg_start + 1; i < argc; ++i) {
if (std::string_view(argv[i]) == "-h" if (std::string_view(argv[i]) == "-h"
@@ -254,10 +259,14 @@ int main(int argc, char* argv[]) {
continue; continue;
} }
if (!is_flag(argv[i])) { if (!is_flag(argv[i])) {
if (file_arg == nullptr) {
file_arg = argv[i]; file_arg = argv[i];
} else if (version_arg == nullptr) {
version_arg = argv[i];
break; break;
} }
} }
}
if (subcommand == "list") { if (subcommand == "list") {
auto entries = install::read_installed(); auto entries = install::read_installed();
@@ -320,6 +329,102 @@ int main(int argc, char* argv[]) {
} }
} }
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 (subcommand == "fetch-package") {
if (file_arg == nullptr) { if (file_arg == nullptr) {
std::cerr << "error: no package name specified\n"; std::cerr << "error: no package name specified\n";