diff --git a/src/main.cpp b/src/main.cpp index c9dcf95..b3224e2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,7 +19,9 @@ #include "kappa/tools/format.hpp" #include +#include #include +#include #include #include #include @@ -27,6 +29,8 @@ #include #include #include +#include +#include using namespace std::string_view_literals; using namespace kappa; @@ -59,6 +63,8 @@ Subcommands: --config "key=val,key2=val2" Set config values remove Remove a package from system config upgrade Check indexes for newer versions of installed packages + bump Update a .kap package to a new version (auto-download, rehash) + bump-all Scan directory and bump all .kap files to latest available Options: -h, --help Show this help message @@ -246,7 +252,9 @@ int main(int argc, char* argv[]) { || (subcommand == "search") || (subcommand == "add") || (subcommand == "remove") - || (subcommand == "upgrade"); + || (subcommand == "upgrade") + || (subcommand == "bump") + || (subcommand == "bump-all"); if (!valid_subcommand) { std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n"; @@ -663,6 +671,216 @@ int main(int argc, char* argv[]) { return 0; } + if (subcommand == "bump") { + if (file_arg == nullptr || version_arg == nullptr) { + std::cerr << "error: bump requires \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 buf; + std::string hash_out; + ssize_t n; + while ((n = read(pipefd[0], buf.data(), buf.size() - 1)) > 0) { + buf[static_cast(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 hbuf; + std::string hout; + ssize_t hn; + while ((hn = read(hpipe[0], hbuf.data(), hbuf.size() - 1)) > 0) { + hbuf[static_cast(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";