// registry.bux — package registry index for selfhost (session 81) // Parity with bootstrap registry.nim: local file + HTTP(S) cache. module Registry { extern func Print(s: String); extern func PrintLine(s: String); extern func bux_strlen(s: String) -> uint; extern func bux_read_file(path: String) -> String; extern func bux_write_file(path: String, content: String) -> bool; extern func bux_file_exists(path: String) -> int; extern func bux_dir_exists(path: String) -> int; extern func bux_mkdir_if_needed(path: String) -> int; extern func bux_path_join(a: String, b: String) -> String; extern func bux_path_parent(path: String) -> String; extern func bux_getenv(name: String) -> String; extern func bux_getcwd() -> String; extern func bux_system(cmd: String) -> int; extern func bux_process_output(cmd: String) -> String; const REG_MAX: int = 64; struct RegistryPackage { name: String; version: String; source: String; description: String; resolvedPath: String; } struct Registry { path: String; // local index path (or cache path) sourceUrl: String; // non-empty if from HTTP(S) count: int; // fixed slots (no dynamic arrays in selfhost compiler easily) p0: RegistryPackage; p1: RegistryPackage; p2: RegistryPackage; p3: RegistryPackage; p4: RegistryPackage; p5: RegistryPackage; p6: RegistryPackage; p7: RegistryPackage; p8: RegistryPackage; p9: RegistryPackage; p10: RegistryPackage; p11: RegistryPackage; p12: RegistryPackage; p13: RegistryPackage; p14: RegistryPackage; p15: RegistryPackage; // up to 16 packages is enough for smoke/demo; expand if needed } func Reg_FileExists(path: String) -> bool { return bux_file_exists(path) != 0; } func Reg_DirExists(path: String) -> bool { return bux_dir_exists(path) != 0; } func Reg_IsHttpUrl(s: String) -> bool { if String_StartsWith(s, "http://") { return true; } if String_StartsWith(s, "https://") { return true; } return false; } func Reg_EnvTruthy(name: String) -> bool { let v: String = bux_getenv(name); if v == null as String { return false; } if String_Eq(v, "") { return false; } return true; } func Reg_HomeCacheDir() -> String { let home: String = bux_getenv("HOME"); if home == null as String || String_Eq(home, "") { return ".bux/cache"; } return bux_path_join(bux_path_join(home, ".bux"), "cache"); } func Reg_FetchHttp(url: String) -> String { // Download to ~/.bux/cache/registry_http.toml; return local path or "". let cacheDir: String = Reg_HomeCacheDir(); discard bux_mkdir_if_needed(bux_path_join(bux_getenv("HOME"), ".bux")); discard bux_mkdir_if_needed(cacheDir); let cachePath: String = bux_path_join(cacheDir, "registry_http.toml"); let metaPath: String = bux_path_join(cacheDir, "registry_http.url"); let force: bool = Reg_EnvTruthy("BUX_REGISTRY_REFRESH"); if !force && Reg_FileExists(cachePath) && Reg_FileExists(metaPath) { let cachedUrl: String = String_Trim(bux_read_file(metaPath)); if String_Eq(cachedUrl, url) { return cachePath; } } var kflag: String = ""; if Reg_EnvTruthy("BUX_REGISTRY_INSECURE") { kflag = " -k"; } // prefer curl let curlCmd: String = String_Concat( "curl -fsSL", String_Concat(kflag, String_Concat(" --max-time 30 -o \"", String_Concat(cachePath, String_Concat("\" \"", String_Concat(url, "\""))))) ); var ok: bool = false; if bux_system("command -v curl >/dev/null 2>&1") == 0 { ok = bux_system(curlCmd) == 0 && Reg_FileExists(cachePath); } else if bux_system("command -v wget >/dev/null 2>&1") == 0 { var nflag: String = ""; if Reg_EnvTruthy("BUX_REGISTRY_INSECURE") { nflag = " --no-check-certificate"; } let wgetCmd: String = String_Concat( "wget -q", String_Concat(nflag, String_Concat(" -T 30 -O \"", String_Concat(cachePath, String_Concat("\" \"", String_Concat(url, "\""))))) ); ok = bux_system(wgetCmd) == 0 && Reg_FileExists(cachePath); } if !ok { return ""; } discard bux_write_file(metaPath, String_Concat(url, "\n")); return cachePath; } func Reg_StripQuotes(val: String) -> String { var v: String = String_Trim(val); if String_StartsWith(v, "\"") && String_EndsWith(v, "\"") { let n: uint = bux_strlen(v); if n >= 2 { return String_Slice(v, 1, n - 2); } } return v; } func Reg_ResolveSource(src: String, indexDir: String) -> String { // file: or path: → absolute path; else "" var p: String = src; if String_StartsWith(src, "file:") { p = String_Slice(src, 5, bux_strlen(src) - 5); if String_StartsWith(p, "//") { p = String_Slice(p, 2, bux_strlen(p) - 2); } } else if String_StartsWith(src, "path:") { p = String_Slice(src, 5, bux_strlen(src) - 5); } else { return ""; } if String_StartsWith(p, "/") { return p; } return bux_path_join(indexDir, p); } func Reg_SetPkg(reg: *Registry, idx: int, pkg: RegistryPackage) { if idx == 0 { reg.p0 = pkg; } else if idx == 1 { reg.p1 = pkg; } else if idx == 2 { reg.p2 = pkg; } else if idx == 3 { reg.p3 = pkg; } else if idx == 4 { reg.p4 = pkg; } else if idx == 5 { reg.p5 = pkg; } else if idx == 6 { reg.p6 = pkg; } else if idx == 7 { reg.p7 = pkg; } else if idx == 8 { reg.p8 = pkg; } else if idx == 9 { reg.p9 = pkg; } else if idx == 10 { reg.p10 = pkg; } else if idx == 11 { reg.p11 = pkg; } else if idx == 12 { reg.p12 = pkg; } else if idx == 13 { reg.p13 = pkg; } else if idx == 14 { reg.p14 = pkg; } else if idx == 15 { reg.p15 = pkg; } } func Reg_GetPkg(reg: Registry, idx: int) -> RegistryPackage { if idx == 0 { return reg.p0; } if idx == 1 { return reg.p1; } if idx == 2 { return reg.p2; } if idx == 3 { return reg.p3; } if idx == 4 { return reg.p4; } if idx == 5 { return reg.p5; } if idx == 6 { return reg.p6; } if idx == 7 { return reg.p7; } if idx == 8 { return reg.p8; } if idx == 9 { return reg.p9; } if idx == 10 { return reg.p10; } if idx == 11 { return reg.p11; } if idx == 12 { return reg.p12; } if idx == 13 { return reg.p13; } if idx == 14 { return reg.p14; } return reg.p15; } func Reg_ParseContent(content: String, indexPath: String) -> Registry { var reg: Registry; reg.path = indexPath; reg.sourceUrl = ""; reg.count = 0; let indexDir: String = bux_path_parent(indexPath); var cur: RegistryPackage; var inPkg: bool = false; let nlines: uint = String_SplitCount(content, "\n"); var i: uint = 0; while i <= nlines { var line: String = ""; if i < nlines { line = String_Trim(String_SplitPart(content, "\n", i)); } let flush: bool = (i == nlines) || String_Eq(line, "[[package]]") || String_Eq(line, "[[Package]]"); if flush && inPkg && !String_Eq(cur.name, "") { cur.resolvedPath = Reg_ResolveSource(cur.source, indexDir); if reg.count < 16 { Reg_SetPkg(®, reg.count, cur); reg.count = reg.count + 1; } cur.name = ""; cur.version = ""; cur.source = ""; cur.description = ""; cur.resolvedPath = ""; } if i == nlines { break; } if String_Eq(line, "") || String_StartsWith(line, "#") { i = i + 1; continue; } if String_Eq(line, "[[package]]") || String_Eq(line, "[[Package]]") { inPkg = true; i = i + 1; continue; } if !inPkg { i = i + 1; continue; } let eqc: uint = String_SplitCount(line, "="); if eqc >= 2 { let key: String = String_Trim(String_SplitPart(line, "=", 0)); let val: String = Reg_StripQuotes(String_SplitPart(line, "=", 1)); // lowercase-ish compare for common keys if String_Eq(key, "name") || String_Eq(key, "Name") { cur.name = val; } else if String_Eq(key, "version") || String_Eq(key, "Version") { cur.version = val; } else if String_Eq(key, "source") || String_Eq(key, "Source") { cur.source = val; } else if String_Eq(key, "description") || String_Eq(key, "Description") { cur.description = val; } } i = i + 1; } return reg; } func Reg_FindIndex() -> Registry { var reg: Registry; reg.path = ""; reg.sourceUrl = ""; reg.count = 0; let env: String = bux_getenv("BUX_REGISTRY"); if env != null as String && !String_Eq(env, "") { if Reg_IsHttpUrl(env) { let local: String = Reg_FetchHttp(env); if String_Eq(local, "") { reg.sourceUrl = env; return reg; } let content: String = bux_read_file(local); reg = Reg_ParseContent(content, local); reg.sourceUrl = env; reg.path = local; return reg; } if Reg_FileExists(env) { let content: String = bux_read_file(env); reg = Reg_ParseContent(content, env); reg.path = env; return reg; } } let home: String = bux_getenv("HOME"); if home != null as String && !String_Eq(home, "") { let homeIdx: String = bux_path_join(bux_path_join(home, ".bux"), "registry.toml"); if Reg_FileExists(homeIdx) { let content: String = bux_read_file(homeIdx); reg = Reg_ParseContent(content, homeIdx); reg.path = homeIdx; return reg; } } // cwd-relative candidates let cwd: String = bux_getcwd(); var c0: String = bux_path_join(cwd, "config/registry.toml"); if Reg_FileExists(c0) { let content: String = bux_read_file(c0); reg = Reg_ParseContent(content, c0); reg.path = c0; return reg; } c0 = bux_path_join(cwd, "../config/registry.toml"); if Reg_FileExists(c0) { let content: String = bux_read_file(c0); reg = Reg_ParseContent(content, c0); reg.path = c0; return reg; } c0 = bux_path_join(cwd, "../../config/registry.toml"); if Reg_FileExists(c0) { let content: String = bux_read_file(c0); reg = Reg_ParseContent(content, c0); reg.path = c0; return reg; } return reg; } func Reg_VersionOk(have: String, req: String) -> bool { if String_Eq(req, "") || String_Eq(req, "*") { return true; } return String_Eq(have, req); } // Lookup by name; versionReq "*" = last matching entry (highest listed last). func Reg_Lookup(reg: Registry, name: String, versionReq: String) -> RegistryPackage { var found: RegistryPackage; found.name = ""; var i: int = 0; while i < reg.count { let p: RegistryPackage = Reg_GetPkg(reg, i); if String_Eq(p.name, name) && Reg_VersionOk(p.version, versionReq) { found = p; } i = i + 1; } return found; } func Reg_Search(reg: Registry, query: String) -> int { // print hits; return count var hits: int = 0; // Dedupe by name: keep last version // Simple O(n^2): for each pkg, if last occurrence of name, print var i: int = 0; while i < reg.count { let p: RegistryPackage = Reg_GetPkg(reg, i); var isLast: bool = true; var j: int = i + 1; while j < reg.count { let q: RegistryPackage = Reg_GetPkg(reg, j); if String_Eq(q.name, p.name) { isLast = false; break; } j = j + 1; } if isLast { var ok: bool = true; if !String_Eq(query, "") { ok = String_Contains(p.name, query) || String_Contains(p.description, query); } if ok { Print(" "); Print(p.name); Print(" "); Print(p.version); Print(" — "); if !String_Eq(p.description, "") { PrintLine(p.description); } else { PrintLine(p.source); } hits = hits + 1; } } i = i + 1; } return hits; } }