8 Commits
Author SHA1 Message Date
huntedbytheirs 70d59a3f9c comma
CI / build-and-test (push) Successful in 56s
2026-08-17 22:52:29 -04:00
huntedbytheirs d0ff52a5d7 test: add 7 tests for kappa bump and bump-all
CI / build-and-test (push) Successful in 42s
- 4 tests for kappa bump: version update, version written to file,
  source URL preserved, requires version arg
- 3 tests for kappa bump-all: finds newer version, reports count,
  requires directory arg

Total: 103 → 110 in test-full.sh (30+23+110 = 163 overall)
2026-08-05 07:58:35 -04:00
huntedbytheirs 1f61f116f8 feat: add kappa bump and kappa bump-all subcommands
CI / build-and-test (push) Successful in 37s
kappa bump <file> <version> — updates .kap file to new version.
Downloads tarball, computes sha256, rewrites file with new
version and hash. Preserves source URL with ${version} template.

kappa bump-all <dir> — scans directory of .kap files. For each,
auto-detects newer patch versions by incrementing the version
number and trying up to 15 candidates. Downloads and hashes the
first one that exists.
2026-08-05 07:55:08 -04:00
huntedbytheirs d918f7b6fc test: add 18 tests for 0.3 features
CI / build-and-test (push) Successful in 38s
- 8 tests: --features and --config flags (add, update, idempotent, file verification)
- 3 tests: transitive dependency resolution (auto-resolve chain)
- 4 tests: kappa search (name, substring, repo source, no-match)
- 3 tests: kappa upgrade (detection, version display, dry-run)

Total: 85 → 103 in test-full.sh (30+23+103 = 156 overall)
2026-08-05 07:42:59 -04:00
huntedbytheirs 01fcb20a57 feat: transitive dependency resolution
CI / build-and-test (push) Successful in 39s
Resolver now pulls in transitive dependencies automatically.
Declaring 'packages { root {} }' where root→mid→leaf resolves
all three packages in correct topological order. Previously
every package had to be listed explicitly in the config.

build_registry pre-populates from $KAPPA_ROOT/cache/packages/
so transitive deps can be found without remote fetching.
2026-08-05 07:39:36 -04:00
huntedbytheirs 33ff453cd1 feat: add kappa upgrade subcommand
CI / build-and-test (push) Successful in 39s
Compares installed packages against cached indexes to find newer
versions. kappa upgrade shows available upgrades and updates the
system config with new version constraints. --dry-run shows what
would change without applying.
2026-08-05 07:37:35 -04:00
huntedbytheirs b0b54be70e feat: add kappa search <query> subcommand
CI / build-and-test (push) Successful in 40s
Searches all cached indexes in $KAPPA_ROOT/cache/indexes/
for packages matching the query as a substring. Displays
name, version, and repo source (e.g. 'make 4.4.1 @ kappa-os/stable').
Offline — no network requests.
2026-08-05 07:36:06 -04:00
huntedbytheirs e69ebf220d feat: add --features and --config flags to kappa add
CI / build-and-test (push) Successful in 49s
kappa add nginx --features "ssl=on,gzip=off" --config "port=8080"
adds/updates feature flags and config values in the system config.
Comma-separated key=value pairs. Idempotent — only prints 'updated'
when something actually changed.
2026-08-05 07:34:12 -04:00
4 changed files with 726 additions and 8 deletions
+3 -1
View File
@@ -8,7 +8,7 @@ project, keep reading.
These aren't guidelines. They're the deal.
### 1. C++23 or don't bother
### 1. C++23, or don't bother
We compile with Clang, `-std=c++23`, and zero warnings. If your code needs a
polyfill for `std::format` or can't handle designated initializers, it
@@ -88,6 +88,7 @@ change means someone's `config.kap` stops parsing, it doesn't ship.
### Pick something
Good first issues:
- Adding a 6th init system backend (we have 5: systemd, openrc, s6, runit, dinit)
- Adding a 3rd bootloader backend (we have 2: grub, limine)
- Improving the `kappa doctor` diagnostics for package recipes
@@ -95,6 +96,7 @@ Good first issues:
- Shell completion scripts (bash, zsh, fish)
Ambitious issues:
- Binary package support (pre-built caches)
- Remote build farm (distcc-style)
- Signed package verification with index signing
+462 -6
View File
@@ -19,7 +19,9 @@
#include "kappa/tools/format.hpp"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <fcntl.h>
#include <filesystem>
#include <format>
#include <fstream>
@@ -27,6 +29,8 @@
#include <sstream>
#include <stdexcept>
#include <string_view>
#include <sys/wait.h>
#include <unistd.h>
using namespace std::string_view_literals;
using namespace kappa;
@@ -53,8 +57,14 @@ Subcommands:
list List installed packages
rollback Show available generations
index <dir> Build an index.kap from .kap files in a directory
search <query> Search cached indexes for packages
add <pkg> [v] Add a package to system config (optional version)
--features "key=on,key2=off" Set feature flags
--config "key=val,key2=val2" Set config values
remove <pkg> Remove a package from system config
upgrade Check indexes for newer versions of installed packages
bump <file> <v> Update a .kap package to a new version (auto-download, rehash)
bump-all <dir> Scan directory and bump all .kap files to latest available
Options:
-h, --help Show this help message
@@ -107,6 +117,24 @@ static void handle_parse_error(const char* path,
static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
resolve::Registry registry;
// Pre-populate from cached packages so transitive deps can be resolved
auto pkg_dir = paths::packages_dir();
std::error_code ec;
if (std::filesystem::exists(pkg_dir)) {
for (auto& entry : std::filesystem::directory_iterator(pkg_dir, ec)) {
if (ec) break;
if (entry.path().extension() != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
try {
auto pkg = dsl::parse(buf.str());
registry.try_emplace(pkg.name, std::move(pkg));
} catch (...) { continue; }
}
}
for (const auto& pref : cfg.packages) {
bool found = false;
@@ -221,8 +249,12 @@ int main(int argc, char* argv[]) {
|| (subcommand == "fetch-package")
|| (subcommand == "rollback")
|| (subcommand == "index")
|| (subcommand == "search")
|| (subcommand == "add")
|| (subcommand == "remove");
|| (subcommand == "remove")
|| (subcommand == "upgrade")
|| (subcommand == "bump")
|| (subcommand == "bump-all");
if (!valid_subcommand) {
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
@@ -232,6 +264,8 @@ int main(int argc, char* argv[]) {
const char* file_arg = nullptr;
const char* version_arg = nullptr;
const char* features_arg = nullptr;
const char* config_arg = nullptr;
bool dry_run = false;
for (int i = arg_start + 1; i < argc; ++i) {
if (std::string_view(argv[i]) == "-h"
@@ -254,6 +288,14 @@ int main(int argc, char* argv[]) {
paths::set_root(argv[++i]);
continue;
}
if (std::string_view(argv[i]) == "--features" && i + 1 < argc) {
features_arg = argv[++i];
continue;
}
if (std::string_view(argv[i]) == "--config" && i + 1 < argc) {
config_arg = argv[++i];
continue;
}
if (std::string_view(argv[i]) == "--dry-run") {
dry_run = true;
continue;
@@ -329,11 +371,101 @@ int main(int argc, char* argv[]) {
}
}
if (subcommand == "search") {
if (file_arg == nullptr) {
std::cerr << "error: no search query specified\n";
return 1;
}
auto idx_dir = paths::cache_dir() / "indexes";
std::error_code ec;
if (!std::filesystem::exists(idx_dir)) {
std::cout << "no cached indexes — run kappa fetch-package <name> first\n";
return 0;
}
std::string_view query(file_arg);
int found = 0;
for (auto& entry : std::filesystem::directory_iterator(idx_dir, ec)) {
if (ec) break;
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 idx = dsl::parse_index(buf.str());
for (auto& pkg : idx.packages) {
if (pkg.name.find(query) != std::string::npos) {
std::cout << pkg.name << " " << pkg.version
<< " @ " << idx.name << "\n";
found++;
}
}
} catch (...) { continue; }
}
if (found == 0) {
std::cout << "no packages matching '" << query << "'\n";
} else {
std::cout << found << " result(s)\n";
}
return 0;
}
if (subcommand == "add") {
if (file_arg == nullptr) {
std::cerr << "error: no package name specified\n";
return 1;
}
// Parse --features key=on,key2=off
std::unordered_map<std::string, dsl::FeatureDef> features;
if (features_arg != nullptr) {
std::string_view fsv(features_arg);
std::size_t pos = 0;
while (pos < fsv.size()) {
auto comma = fsv.find(',', pos);
auto pair = fsv.substr(pos, comma == std::string_view::npos
? std::string_view::npos
: comma - pos);
auto eq = pair.find('=');
if (eq != std::string_view::npos) {
auto key = std::string(pair.substr(0, eq));
auto val = pair.substr(eq + 1);
while (!key.empty() && key.back() == ' ') key.pop_back();
while (!val.empty() && val.front() == ' ') val.remove_prefix(1);
features[key] = {val == "on" || val == "true" || val == "1",
false, ""};
}
pos = comma == std::string_view::npos ? fsv.size() : comma + 1;
}
}
// Parse --config key=val,key2=val2
std::unordered_map<std::string, std::string> config;
if (config_arg != nullptr) {
std::string_view csv(config_arg);
std::size_t pos = 0;
while (pos < csv.size()) {
auto comma = csv.find(',', pos);
auto pair = csv.substr(pos, comma == std::string_view::npos
? std::string_view::npos
: comma - pos);
auto eq = pair.find('=');
if (eq != std::string_view::npos) {
auto key = std::string(pair.substr(0, eq));
auto val = std::string(pair.substr(eq + 1));
while (!key.empty() && key.back() == ' ') key.pop_back();
while (!val.empty() && val.front() == ' ') val.erase(0, 1);
config[key] = val;
}
pos = comma == std::string_view::npos ? csv.size() : comma + 1;
}
}
try {
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
dsl::SystemConfig cfg;
@@ -347,10 +479,21 @@ int main(int argc, char* argv[]) {
for (auto& p : cfg.packages) {
if (p.name == file_arg) {
found = true;
bool changed = false;
if (version_arg != nullptr) {
p.version = version_arg;
std::cout << "updated " << file_arg
<< " → version " << version_arg << "\n";
changed = true;
}
if (!features.empty()) {
for (auto& [k, v] : features) { p.features[k] = v; changed = true; }
}
if (!config.empty()) {
for (auto& [k, v] : config) { p.config[k] = v; changed = true; }
}
if (changed) {
std::cout << "updated " << file_arg;
if (version_arg != nullptr) std::cout << " → " << version_arg;
std::cout << "\n";
} else {
std::cout << file_arg << " already in packages\n";
}
@@ -364,11 +507,11 @@ int main(int argc, char* argv[]) {
if (version_arg != nullptr) {
pref.version = version_arg;
}
pref.features = std::move(features);
pref.config = std::move(config);
cfg.packages.push_back(std::move(pref));
std::cout << "added " << file_arg;
if (version_arg != nullptr) {
std::cout << " " << version_arg;
}
if (version_arg != nullptr) std::cout << " " << version_arg;
std::cout << "\n";
}
@@ -425,6 +568,319 @@ int main(int argc, char* argv[]) {
}
}
if (subcommand == "upgrade") {
auto installed = install::read_installed();
if (installed.empty()) {
std::cout << "no packages installed\n";
return 0;
}
// Build a map of latest versions from cached indexes
std::unordered_map<std::string, std::string> latest;
auto idx_dir = paths::cache_dir() / "indexes";
std::error_code ec;
if (std::filesystem::exists(idx_dir)) {
for (auto& entry : std::filesystem::directory_iterator(idx_dir, ec)) {
if (ec) break;
if (entry.path().extension() != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
try {
auto idx = dsl::parse_index(buf.str());
for (auto& pkg : idx.packages) {
auto it = latest.find(pkg.name);
if (it == latest.end() || pkg.version > it->second) {
latest[pkg.name] = pkg.version;
}
}
} catch (...) { continue; }
}
}
if (latest.empty()) {
std::cout << "no cached indexes — run kappa fetch-package <name> first\n";
return 0;
}
std::vector<std::pair<std::string, std::string>> upgrades;
for (auto& e : installed) {
auto it = latest.find(e.name);
if (it != latest.end() && it->second > e.version) {
upgrades.emplace_back(e.name, it->second);
}
}
if (upgrades.empty()) {
std::cout << "all " << installed.size() << " packages up to date\n";
return 0;
}
std::cout << upgrades.size() << " upgrade(s) available:\n";
for (auto& [name, ver] : upgrades) {
// Find old version
std::string old_ver;
for (auto& e : installed) {
if (e.name == name) { old_ver = e.version; break; }
}
std::cout << " " << name << " " << old_ver << " → " << ver << "\n";
}
if (dry_run) {
std::cout << "run kappa add <pkg> <version> for each to apply\n";
return 0;
}
// Apply: update system config with new versions
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());
try {
cfg = dsl::parse_system_config(src);
} catch (...) { cfg = {}; }
}
for (auto& [name, ver] : upgrades) {
bool found = false;
for (auto& p : cfg.packages) {
if (p.name == name) {
p.version = ver;
found = true;
break;
}
}
if (!found) {
dsl::PackageRef pref;
pref.name = name;
pref.version = ver;
cfg.packages.push_back(std::move(pref));
}
}
std::filesystem::create_directories(paths::system_dir(), ec);
std::ofstream out(config_path);
if (!out) {
std::cerr << "error: cannot write config\n";
return 1;
}
tools::format_config(out, cfg);
std::cout << "config updated — run kappa rebuild to apply\n";
return 0;
}
if (subcommand == "bump") {
if (file_arg == nullptr || version_arg == nullptr) {
std::cerr << "error: bump requires <file> <new-version>\n";
return 1;
}
try {
auto src = read_file(file_arg);
auto pkg = dsl::parse(src);
auto old_version = pkg.version;
auto old_sha = pkg.sha256;
// Construct new source URL with the new version
auto new_source = pkg.source;
std::string_view new_ver(version_arg);
for (auto& [from, to] : {
std::pair{"${version}"sv, new_ver},
std::pair{"${name}"sv, std::string_view(pkg.name)}}) {
std::size_t pos = 0;
while ((pos = new_source.find(from, pos)) != std::string::npos) {
new_source.replace(pos, from.size(), to);
pos += to.size();
}
}
std::cout << "downloading " << new_source << "...\n";
auto temp_tarball = paths::temp_dir() / "bump-download.tar.gz";
std::filesystem::create_directories(paths::temp_dir());
pid_t pid = fork();
if (pid == 0) {
execlp("curl", "curl", "-Lsf", "-o", temp_tarball.c_str(),
new_source.c_str(), nullptr);
_exit(1);
}
int status = 0;
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
std::cerr << "download failed\n";
std::filesystem::remove(temp_tarball);
return 1;
}
// Compute new hash
int pipefd[2];
if (pipe(pipefd) != 0) { return 1; }
pid_t pid2 = fork();
if (pid2 == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
execlp("sha256sum", "sha256sum", temp_tarball.c_str(), nullptr);
_exit(1);
}
close(pipefd[1]);
std::array<char, 128> buf;
std::string hash_out;
ssize_t n;
while ((n = read(pipefd[0], buf.data(), buf.size() - 1)) > 0) {
buf[static_cast<std::size_t>(n)] = '\0';
hash_out += buf.data();
}
close(pipefd[0]);
while (waitpid(pid2, nullptr, 0) == -1 && errno == EINTR) {}
auto space = hash_out.find(' ');
auto new_sha = (space != std::string::npos)
? hash_out.substr(0, space) : hash_out;
std::filesystem::remove(temp_tarball);
// Update the .kap file
pkg.version = version_arg;
pkg.sha256 = new_sha;
std::ofstream out(file_arg);
if (!out) {
std::cerr << "error: cannot write " << file_arg << "\n";
return 1;
}
tools::format_package(out, pkg);
std::cout << "bumped " << pkg.name << " " << old_version
<< " → " << version_arg << " sha256: " << new_sha << "\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "bump error: " << e.what() << "\n";
return 1;
}
}
if (subcommand == "bump-all") {
if (file_arg == nullptr) {
std::cerr << "error: bump-all requires a directory\n";
return 1;
}
std::string dir(file_arg);
int bumped = 0;
int skipped = 0;
std::error_code ec;
for (auto& entry : std::filesystem::directory_iterator(dir, ec)) {
if (ec) break;
if (entry.path().extension() != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
in.close();
dsl::PackageDef pkg;
try { pkg = dsl::parse(buf.str()); }
catch (...) { skipped++; continue; }
// Try to auto-detect next version: increment patch number
auto dot1 = pkg.version.rfind('.');
if (dot1 == std::string::npos) { skipped++; continue; }
auto base = pkg.version.substr(0, dot1 + 1);
auto patch_str = pkg.version.substr(dot1 + 1);
int patch = 0;
try { patch = std::stoi(patch_str); }
catch (...) { skipped++; continue; }
// Try next 15 patch versions
bool found = false;
for (int next = patch + 1; next <= patch + 15; ++next) {
auto candidate = base + std::to_string(next);
auto url = pkg.source;
std::string_view cand_ver(candidate);
for (auto& [from, to] : {
std::pair{"${version}"sv, cand_ver},
std::pair{"${name}"sv, std::string_view(pkg.name)}}) {
std::size_t pos = 0;
while ((pos = url.find(from, pos)) != std::string::npos) {
url.replace(pos, from.size(), to);
pos += to.size();
}
}
// Try downloading the candidate URL
auto temp = paths::temp_dir() / "bumpall-download.tar.gz";
std::filesystem::create_directories(paths::temp_dir());
pid_t dl_pid = fork();
if (dl_pid == 0) {
// Redirect stdout/stderr to /dev/null for quiet operation
int devnull = open("/dev/null", O_WRONLY);
if (devnull >= 0) {
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
execlp("curl", "curl", "-Lsf", "-o", temp.c_str(), url.c_str(), nullptr);
_exit(1);
}
int dl_status = 0;
while (waitpid(dl_pid, &dl_status, 0) == -1 && errno == EINTR) {}
if (!WIFEXITED(dl_status) || WEXITSTATUS(dl_status) != 0) {
std::filesystem::remove(temp);
continue;
}
// Hash
int hpipe[2];
if (pipe(hpipe) != 0) continue;
pid_t hpid = fork();
if (hpid == 0) {
close(hpipe[0]); dup2(hpipe[1], STDOUT_FILENO); close(hpipe[1]);
execlp("sha256sum", "sha256sum", temp.c_str(), nullptr);
_exit(1);
}
close(hpipe[1]);
std::array<char, 128> hbuf;
std::string hout;
ssize_t hn;
while ((hn = read(hpipe[0], hbuf.data(), hbuf.size() - 1)) > 0) {
hbuf[static_cast<std::size_t>(hn)] = '\0';
hout += hbuf.data();
}
close(hpipe[0]);
while (waitpid(hpid, nullptr, 0) == -1 && errno == EINTR) {}
std::filesystem::remove(temp);
auto sp = hout.find(' ');
auto new_sha = (sp != std::string::npos) ? hout.substr(0, sp) : hout;
// Update file
auto old_ver = pkg.version;
pkg.version = candidate;
pkg.sha256 = new_sha;
std::ofstream out(entry.path());
if (!out) continue;
tools::format_package(out, pkg);
std::cout << pkg.name << " " << old_ver
<< " → " << candidate << "\n";
found = true;
bumped++;
break;
}
if (!found) skipped++;
}
std::cout << bumped << " bumped, " << skipped << " skipped\n";
return 0;
}
if (subcommand == "fetch-package") {
if (file_arg == nullptr) {
std::cerr << "error: no package name specified\n";
+52 -1
View File
@@ -42,7 +42,58 @@ BuildPlan resolve(const dsl::SystemConfig& cfg, const Registry& registry) {
nodes.push_back(std::move(step));
}
// Detect conflicts between selected packages
// Transitive dependency discovery: pull in deps that are in the
// registry but not yet in the plan. Continue until no new deps
// are discovered — handles arbitrarily deep dependency chains.
bool changed = true;
while (changed) {
changed = false;
std::size_t current_size = nodes.size();
for (std::size_t i = 0; i < current_size; ++i) {
if (!nodes[i].package) continue;
for (auto& dep : nodes[i].package->depends) {
if (name_to_idx.contains(dep.name)) continue;
// Skip feature-conditional deps that aren't enabled
if (!dep.feature.empty()) {
auto fit = nodes[i].resolved.features.find(dep.feature);
if (fit == nodes[i].resolved.features.end() || !fit->second.enabled) {
continue;
}
}
auto rit = registry.find(dep.name);
if (rit == registry.end()) continue;
auto& pkg = rit->second;
auto resolved = config::resolve_package(pkg, cfg.system, {});
BuildStep step;
step.name = pkg.name;
step.package = &pkg;
step.resolved = std::move(resolved);
step.enabled_init = cfg.boot.init;
for (auto& ddep : pkg.depends) {
if (!ddep.feature.empty()) {
auto fit = step.resolved.features.find(ddep.feature);
if (fit == step.resolved.features.end() || !fit->second.enabled) {
continue;
}
}
step.dependencies.push_back({ddep.name, ddep.version});
}
name_to_idx[step.name] = nodes.size();
nodes.push_back(std::move(step));
changed = true;
}
}
}
// Detect conflicts between all selected packages (including transitive)
std::unordered_set<std::string> selected;
for (auto& node : nodes) { selected.insert(node.name); }
+209
View File
@@ -521,6 +521,39 @@ check_fail "remove needs name" \
"$KAPPA remove" \
'no package name'
# --features and --config flags
check "add with --features" \
"$KAPPA add featpkg --features 'ssl=on,gzip=off'" \
'added featpkg'
check "add features present in config" \
"grep -c 'ssl = true' $KAPPA_ROOT/system/config.kap" \
'1'
check "update existing with --features" \
"$KAPPA add featpkg --features 'http2=on'" \
'updated featpkg'
check "add with --config" \
"$KAPPA add cfgpkg --config 'port=9090,worker=4'" \
'added cfgpkg'
check "config values written to file" \
"grep 'port = 9090' $KAPPA_ROOT/system/config.kap" \
'port = 9090'
check "add with features and config together" \
"$KAPPA add combopkg --features 'ssl=on' --config 'host=localhost'" \
'added combopkg'
check "features and config both in file" \
"grep -c ssl $KAPPA_ROOT/system/config.kap" \
'2'
check "add idempotent with no flags" \
"$KAPPA add cfgpkg" \
'already in packages'
# ═══════════════════════════════════════════
echo ""
echo "=== Install: list and rollback ==="
@@ -670,6 +703,64 @@ check "resolve deptree (c has dep)" \
"$KAPPA resolve $TMPDIR/deptree-config.kap 2>&1" \
'dep-c (1 deps)'
# Transitive dependency resolution
cat > "$KAPPA_ROOT/cache/packages/trans-a.kap" << 'TRANSEOF'
package "trans-a" {
version = "1.0"
source = "a.tar.gz"
sha256 = "abc"
license = "MIT"
build { echo a }
install { echo a }
}
TRANSEOF
cat > "$KAPPA_ROOT/cache/packages/trans-b.kap" << 'TRANSEOF'
package "trans-b" {
version = "1.0"
source = "b.tar.gz"
sha256 = "abc"
license = "MIT"
depends = [{ name = "trans-a" }]
build { echo b }
install { echo b }
}
TRANSEOF
cat > "$KAPPA_ROOT/cache/packages/trans-c.kap" << 'TRANSEOF'
package "trans-c" {
version = "1.0"
source = "c.tar.gz"
sha256 = "abc"
license = "MIT"
depends = [{ name = "trans-b" }]
build { echo c }
install { echo c }
}
TRANSEOF
cat > "$TMPDIR/transitive-config.kap" << 'TRANSEOF'
system { hostname = "test" }
packages { trans-c {} }
services {}
boot {
kernel = "linux"
init = "s6"
root = "/dev/sda1"
bootloader = "limine"
}
TRANSEOF
check "transitive deps auto-resolve (only c declared)" \
"$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \
'trans-a (0 deps)'
check "transitive deps include intermediate" \
"$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \
'trans-b (1 deps)'
check "transitive deps resolve root last" \
"$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \
'trans-c (1 deps)'
# ═══════════════════════════════════════════
echo ""
echo "=== Build: simple execution ==="
@@ -955,6 +1046,124 @@ check "parse-config with features" \
# ═══════════════════════════════════════════
echo ""
echo ""
echo "=== Search: cached index queries ==="
echo ""
mkdir -p "$KAPPA_ROOT/cache/indexes"
cat > "$KAPPA_ROOT/cache/indexes/test-repo-stable.kap" << 'SRCHEOF'
index "test-repo/stable" {
trans-a { version = "1.0" }
trans-b { version = "1.0" }
trans-c { version = "1.0" }
testpkg { version = "1.0" }
}
SRCHEOF
check "search finds package by name" \
"$KAPPA search trans-a" \
'trans-a'
check "search finds by substring" \
"$KAPPA search ans" \
'trans-a'
check "search shows repo source" \
"$KAPPA search trans-a" \
'test-repo/stable'
check "search no match" \
"$KAPPA search zzz999" \
'no packages matching'
echo ""
echo "=== Upgrade: version comparison ==="
echo ""
mkdir -p "$KAPPA_ROOT/db"
cat > "$KAPPA_ROOT/db/installed" << 'UPGEOF'
trans-a 0.9 a1b2c3d4e5f6a7b8
trans-b 0.9 b2c3d4e5f6a7b8c9
trans-c 1.0 c3d4e5f6a7b8c9d0
UPGEOF
check "upgrade detects newer versions" \
"$KAPPA upgrade 2>&1" \
'trans-a'
check "upgrade shows old version" \
"$KAPPA upgrade 2>&1" \
'0.9'
check "upgrade dry-run does not apply" \
"$KAPPA upgrade --dry-run 2>&1" \
'run kappa add'
echo ""
echo "=== Bump: explicit version update ==="
echo ""
cat > "$TMPDIR/bump-test.kap" << 'BMPEOF'
package "bumpme" {
const version = "4.4.0"
const source = "https://ftp.gnu.org/gnu/make/make-${version}.tar.gz"
sha256 = "oldhash"
license = "GPL-3.0-or-later"
build { make }
install { make install }
}
BMPEOF
check "bump updates version" \
"$KAPPA bump $TMPDIR/bump-test.kap 4.4.1" \
'bumped bumpme'
check "bump writes correct version" \
"grep -c '4.4.1' $TMPDIR/bump-test.kap" \
'1'
check "bump preserves source URL" \
"grep -c 'make-' $TMPDIR/bump-test.kap" \
'1'
check_fail "bump requires version arg" \
"$KAPPA bump $TMPDIR/bump-test.kap" \
'requires'
echo ""
echo "=== Bump-all: directory scan ==="
echo ""
BPALLDIR="$TMPDIR/bumpall-pkgs"
mkdir -p "$BPALLDIR"
cat > "$BPALLDIR/make.kap" << 'BMPEOF'
package "make" {
const version = "4.4.0"
const source = "https://ftp.gnu.org/gnu/make/make-${version}.tar.gz"
sha256 = "old"
license = "GPL-3.0-or-later"
build { make }
install { make install }
}
BMPEOF
check "bump-all finds newer version" \
"$KAPPA bump-all $BPALLDIR" \
'4.4.0 → 4.4.1'
check "bump-all reports count" \
"$KAPPA bump-all $BPALLDIR" \
'bumped'
check_fail "bump-all needs directory" \
"$KAPPA bump-all" \
'requires a directory'
rm -f "$KAPPA_ROOT/db/installed"
echo "=== Rebuild: dry-run detection ==="
echo ""