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.
This commit is contained in:
2026-08-05 07:55:08 -04:00
parent d918f7b6fc
commit 1f61f116f8
+219 -1
View File
@@ -19,7 +19,9 @@
#include "kappa/tools/format.hpp" #include "kappa/tools/format.hpp"
#include <algorithm> #include <algorithm>
#include <array>
#include <cstdlib> #include <cstdlib>
#include <fcntl.h>
#include <filesystem> #include <filesystem>
#include <format> #include <format>
#include <fstream> #include <fstream>
@@ -27,6 +29,8 @@
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
#include <string_view> #include <string_view>
#include <sys/wait.h>
#include <unistd.h>
using namespace std::string_view_literals; using namespace std::string_view_literals;
using namespace kappa; using namespace kappa;
@@ -59,6 +63,8 @@ Subcommands:
--config "key=val,key2=val2" Set config values --config "key=val,key2=val2" Set config values
remove <pkg> Remove a package from system config remove <pkg> Remove a package from system config
upgrade Check indexes for newer versions of installed packages 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: Options:
-h, --help Show this help message -h, --help Show this help message
@@ -246,7 +252,9 @@ int main(int argc, char* argv[]) {
|| (subcommand == "search") || (subcommand == "search")
|| (subcommand == "add") || (subcommand == "add")
|| (subcommand == "remove") || (subcommand == "remove")
|| (subcommand == "upgrade"); || (subcommand == "upgrade")
|| (subcommand == "bump")
|| (subcommand == "bump-all");
if (!valid_subcommand) { if (!valid_subcommand) {
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n"; std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
@@ -663,6 +671,216 @@ int main(int argc, char* argv[]) {
return 0; 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 (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";