/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: repo.reef Authors: Chris Tusa License: Description: Repo command - manage package repositories ******************************************************************************/ module commands.repo import sys.args import sys.process import io.file import io.dir import core.str import core.repository import util.color import exitcodes as ec export fn execute(): int end export fn execute(): int let argc = args.count() if argc < 3 print_usage() return ec.EXIT_USAGE() end if let subcommand = args.get(2) if subcommand == "list" list_repos() return ec.EXIT_SUCCESS() elif subcommand == "add" if argc < 4 color.print_error("Missing repository URL") println("Usage: coral repo add [name]") return ec.EXIT_USAGE() end if let url = args.get(3) mut name = "" if argc >= 5 name = args.get(4) end if add_repo(url, name) return ec.EXIT_SUCCESS() elif subcommand == "remove" if argc < 4 color.print_error("Missing repository name") println("Usage: coral repo remove ") return ec.EXIT_USAGE() end if remove_repo(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "enable" if argc < 4 color.print_error("Missing repository name") return ec.EXIT_USAGE() end if enable_repo(args.get(3), true) return ec.EXIT_SUCCESS() elif subcommand == "disable" if argc < 4 color.print_error("Missing repository name") return ec.EXIT_USAGE() end if enable_repo(args.get(3), false) return ec.EXIT_SUCCESS() elif subcommand == "init" if argc < 4 color.print_error("Missing repository path") println("Usage: coral repo init ") return ec.EXIT_USAGE() end if init_repo(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "rebuild" if argc < 4 color.print_error("Missing repository path") println("Usage: coral repo rebuild ") return ec.EXIT_USAGE() end if rebuild_repo(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "sign" if argc < 4 color.print_error("Missing repository path") println("Usage: coral repo sign [keyname]") return ec.EXIT_USAGE() end if let repo_path = args.get(3) mut key_name = "default" if argc >= 5 key_name = args.get(4) end if sign_repo(repo_path, key_name) return ec.EXIT_SUCCESS() elif subcommand == "verify" if argc < 4 color.print_error("Missing repository path") println("Usage: coral repo verify ") return ec.EXIT_USAGE() end if verify_repo(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "mirror" if argc < 5 color.print_error("Missing source or destination") println("Usage: coral repo mirror ") return ec.EXIT_USAGE() end if mirror_repo(args.get(3), args.get(4)) return ec.EXIT_SUCCESS() elif subcommand == "export" if argc < 5 color.print_error("Missing repository path or output") println("Usage: coral repo export ") return ec.EXIT_USAGE() end if export_repo(args.get(3), args.get(4)) return ec.EXIT_SUCCESS() else color.print_error("Unknown subcommand: " + subcommand) print_usage() return ec.EXIT_USAGE() end if end execute proc list_repos() color.print_action("Configured repositories:") println("") mut repos: [repository.Repository] = new [repository.Repository](32) let count = repository.list_repos(repos, 32) if count == 0 color.print_info("No repositories configured") println("") color.print_info("Add a repository with: coral repo add ") return end if mut i = 0 while i < count let repo = repos[i] print(" ") print(repo.name) print(" - ") print(repo.url) if not repo.enabled print(" [disabled]") end if if repo.is_signed print(" [signed]") end if println("") i = i + 1 end while println("") color.print_info("Total: " + int_to_str(count) + " repository(ies)") end list_repos proc add_repo(url: string, name: string) color.print_action("Adding repository...") // Generate name from URL if not provided mut repo_name = name if str.length(repo_name) == 0 repo_name = extract_repo_name(url) end if if str.length(repo_name) == 0 color.print_error("Could not determine repository name. Please specify one.") return end if // Create repo file let repos_dir = "/etc/coral/repos.d" let repo_path = repos_dir + "/" + repo_name + ".repo" // Check if it exists if file.fileExists(repo_path) color.print_error("Repository already exists: " + repo_name) return end if // Ensure directory exists if not dir.dir_exists(repos_dir) if not dir.create_dir_all(repos_dir) color.print_error("Failed to create repos directory") return end if end if // Create repo file content mut content = "[repository]\n" content = content + "name = \"" + repo_name + "\"\n" content = content + "url = \"" + url + "\"\n" content = content + "priority = 100\n" content = content + "enabled = true\n" content = content + "signed = false\n" if not file.writeFile(repo_path, content) color.print_error("Failed to write repository file") return end if color.print_success("Added repository: " + repo_name) end add_repo proc remove_repo(name: string) let repos_dir = "/etc/coral/repos.d" let repo_path = repos_dir + "/" + name + ".repo" if not file.fileExists(repo_path) color.print_error("Repository not found: " + name) return end if // Remove the file let cmd = "rm -f \"" + repo_path + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if color.print_success("Removed repository: " + name) end remove_repo proc enable_repo(name: string, enable: bool) let repos_dir = "/etc/coral/repos.d" let repo_path = repos_dir + "/" + name + ".repo" if not file.fileExists(repo_path) color.print_error("Repository not found: " + name) return end if // Read current content let content = file.readFile(repo_path) // Replace enabled line mut new_content = "" if enable new_content = str.replace(content, "enabled = false", "enabled = true") else new_content = str.replace(content, "enabled = true", "enabled = false") end if if not file.writeFile(repo_path, new_content) color.print_error("Failed to update repository file") return end if if enable color.print_success("Enabled repository: " + name) else color.print_success("Disabled repository: " + name) end if end enable_repo fn extract_repo_name(url: string): string // Extract name from URL (e.g., https://repo.example.com/zygaena -> zygaena) let len = str.length(url) if len == 0 return "" end if // Find last slash mut last_slash = 0 - 1 mut i = 0 while i < len if url[i] == '/' last_slash = i end if i = i + 1 end while if last_slash < 0 or last_slash >= len - 1 return "repo" end if return str.substring(url, last_slash + 1, len - last_slash - 1) end extract_repo_name // Initialize a new repository at the given path proc init_repo(repo_path: string) color.print_action("Initializing repository at " + repo_path) // Create the repository directory if not dir.dir_exists(repo_path) if not dir.create_dir_all(repo_path) color.print_error("Failed to create repository directory") return end if end if // Create packages subdirectory let packages_dir = repo_path + "/packages" if not dir.dir_exists(packages_dir) if not dir.create_dir_all(packages_dir) color.print_error("Failed to create packages directory") return end if end if // Create repo.toml manifest let manifest_path = repo_path + "/repo.toml" if file.fileExists(manifest_path) color.print_warning("repo.toml already exists, skipping") else mut content = "# Coral Repository Manifest\n" content = content + "# Generated by coral repo init\n\n" content = content + "[repository]\n" content = content + "name = \"unnamed\"\n" content = content + "description = \"A Coral package repository\"\n" content = content + "url = \"\"\n" content = content + "arch = \"x86_64\"\n" content = content + "created = \"2025-01-24\"\n" content = content + "signed = false\n\n" content = content + "# Packages will be listed below by coral repo rebuild\n" content = content + "[packages]\n" if not file.writeFile(manifest_path, content) color.print_error("Failed to write repo.toml") return end if end if color.print_success("Repository initialized at " + repo_path) color.print_info("Place packages in: " + packages_dir) color.print_info("Run 'coral repo rebuild " + repo_path + "' to update manifest") end init_repo // Rebuild repository manifest by scanning packages proc rebuild_repo(repo_path: string) color.print_action("Rebuilding repository manifest...") let packages_dir = repo_path + "/packages" if not dir.dir_exists(packages_dir) color.print_error("Packages directory not found: " + packages_dir) return end if // Scan for package files mut entries: [string] = new [string](1024) let entry_count = dir.list_dir(packages_dir, entries, 1024) mut pkg_count = 0 mut packages_content = "" mut i = 0 while i < entry_count let entry = entries[i] // Look for .pkg.tar.xz files if str.ends_with(entry, ".pkg.tar.xz") // Parse package name and version from filename // Format: name-version-release.arch.pkg.tar.xz let info = parse_package_filename(entry) if str.length(info) > 0 packages_content = packages_content + info + "\n" pkg_count = pkg_count + 1 end if end if i = i + 1 end while // Generate new repo.toml let manifest_path = repo_path + "/repo.toml" mut content = "# Coral Repository Manifest\n" content = content + "# Rebuilt by coral repo rebuild\n\n" content = content + "[repository]\n" content = content + "name = \"unnamed\"\n" content = content + "description = \"A Coral package repository\"\n" content = content + "url = \"\"\n" content = content + "arch = \"x86_64\"\n" content = content + "created = \"2025-01-24\"\n" content = content + "signed = false\n\n" content = content + "[packages]\n" content = content + packages_content if not file.writeFile(manifest_path, content) color.print_error("Failed to write repo.toml") return end if color.print_success("Repository rebuilt: " + int_to_str(pkg_count) + " package(s) found") end rebuild_repo // Parse package filename into TOML entry fn parse_package_filename(filename: string): string // Extract name-version-release from filename // Example: vim-9.0-1.x86_64.pkg.tar.xz -> vim = { version = "9.0", release = 1 } // Remove .pkg.tar.xz suffix let base = str.substring(filename, 0, str.length(filename) - 11) // Find arch (e.g., .x86_64) let arch_pos = str.last_index_of_char(base, '.') if arch_pos < 0 return "" end if let name_ver_rel = str.substring(base, 0, arch_pos) // Find release number (last hyphen) let rel_pos = str.last_index_of_char(name_ver_rel, '-') if rel_pos < 0 return "" end if let release = str.substring(name_ver_rel, rel_pos + 1, str.length(name_ver_rel) - rel_pos - 1) // Find version (second to last hyphen) let name_ver = str.substring(name_ver_rel, 0, rel_pos) let ver_pos = str.last_index_of_char(name_ver, '-') if ver_pos < 0 return "" end if let name = str.substring(name_ver, 0, ver_pos) let version = str.substring(name_ver, ver_pos + 1, str.length(name_ver) - ver_pos - 1) return name + " = { version = \"" + version + "\", release = " + release + ", file = \"" + filename + "\" }" end parse_package_filename // Sign the repository manifest proc sign_repo(repo_path: string, key_name: string) color.print_action("Signing repository manifest...") let manifest_path = repo_path + "/repo.toml" if not file.fileExists(manifest_path) color.print_error("repo.toml not found in: " + repo_path) return end if // Find the key let keyring = "/etc/coral/keys" let key_path = keyring + "/" + key_name + ".key" if not file.fileExists(key_path) color.print_error("Key not found: " + key_name) color.print_info("Generate a key with: coral key generate " + key_name) return end if // Sign the manifest using openssl let sig_path = repo_path + "/repo.toml.sig" let cmd = "openssl pkeyutl -sign -inkey \"" + key_path + "\" -in \"" + manifest_path + "\" -out \"" + sig_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to start signing process") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Signing failed") return end if // Update repo.toml to indicate it's signed let content = file.readFile(manifest_path) let new_content = str.replace(content, "signed = false", "signed = true") file.writeFile(manifest_path, new_content) color.print_success("Repository signed with key: " + key_name) color.print_info("Signature: " + sig_path) end sign_repo // Verify repository signature proc verify_repo(repo_path: string) color.print_action("Verifying repository signature...") let manifest_path = repo_path + "/repo.toml" let sig_path = repo_path + "/repo.toml.sig" if not file.fileExists(manifest_path) color.print_error("repo.toml not found") return end if if not file.fileExists(sig_path) color.print_error("No signature found (repo.toml.sig)") return end if // Try to find a matching public key let keyring = "/etc/coral/keys" if not dir.dir_exists(keyring) color.print_error("No keys in keyring") return end if // List all public keys and try each mut entries: [string] = new [string](64) let count = dir.list_dir(keyring, entries, 64) mut i = 0 mut verified = false while i < count and not verified let entry = entries[i] if str.ends_with(entry, ".pub") let key_path = keyring + "/" + entry let cmd = "openssl pkeyutl -verify -pubin -inkey \"" + key_path + "\" -in \"" + manifest_path + "\" -sigfile \"" + sig_path + "\" 2>/dev/null" let pid = process.process_spawn_shell(cmd) if pid > 0 let exit_code = process.process_wait(pid) if exit_code == 0 let key_name = str.substring(entry, 0, str.length(entry) - 4) color.print_success("Signature verified with key: " + key_name) verified = true end if end if end if i = i + 1 end while if not verified color.print_error("Signature verification failed - no matching key found") end if end verify_repo // Mirror repository to remote destination proc mirror_repo(source: string, dest: string) color.print_action("Mirroring repository...") color.print_info("Source: " + source) color.print_info("Destination: " + dest) // Use rsync for mirroring let cmd = "rsync -av --delete \"" + source + "/\" \"" + dest + "/\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to start rsync") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Mirror sync failed") return end if color.print_success("Repository mirrored to: " + dest) end mirror_repo // Export repository for offline distribution (ISO/USB) proc export_repo(repo_path: string, output_path: string) color.print_action("Exporting repository for offline distribution...") if not dir.dir_exists(repo_path) color.print_error("Repository not found: " + repo_path) return end if // Determine output format based on extension if str.ends_with(output_path, ".tar.xz") // Create compressed tarball let cmd = "gtar -C \"" + repo_path + "\" -cJf \"" + output_path + "\" ." let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to create archive") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Archive creation failed") return end if elif str.ends_with(output_path, ".tar.gz") let cmd = "gtar -C \"" + repo_path + "\" -czf \"" + output_path + "\" ." let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to create archive") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Archive creation failed") return end if else // Copy directory let cmd = "cp -r \"" + repo_path + "\" \"" + output_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to copy repository") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Copy failed") return end if end if color.print_success("Repository exported to: " + output_path) end export_repo proc print_usage() println("Usage: coral repo [options]") println("") println("Manage package repositories.") println("") println("Commands:") println(" list List configured repositories") println(" add [name] Add a new repository") println(" remove Remove a repository") println(" enable Enable a repository") println(" disable Disable a repository") println("") println("Repository maintenance:") println(" init Create a new repository structure") println(" rebuild Scan packages and rebuild manifest") println(" sign [key] Sign repository with Ed25519 key") println(" verify Verify repository signature") println(" mirror Sync repository to mirror (rsync)") println(" export Export for offline distribution") println("") println("Examples:") println(" coral repo list") println(" coral repo add https://repo.zygaena.org/packages") println(" coral repo init /var/www/repo/myrepo") println(" coral repo rebuild /var/www/repo/myrepo") println(" coral repo sign /var/www/repo/myrepo maintainer") end print_usage // Helper: convert int to string fn int_to_str(n: int): string if n == 0 return "0" end if mut negative = false mut value = n if n < 0 negative = true value = 0 - n end if mut result = "" while value > 0 let digit = value % 10 result = str.concat(str.substring("0123456789", digit, 1), result) value = value / 10 end while if negative result = str.concat("-", result) end if return result end int_to_str end module