From 34f3b8383bfa96766c93922a3cea4039bd9e2e02 Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Sun, 2 Nov 2025 23:19:54 -0700 Subject: [PATCH 01/14] Added load multiple repos preliminary support Adds a function that loads a filename (Should have the list of repos urls in it) and then loads each one. It then calls the origional fetch function on each of the repo urls and loads all of those. --- KindleForge/KindleForge/script.js | 107 +++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 30 deletions(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index b878b1a..0c00af5 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -55,18 +55,19 @@ window.kindle.appmgr.ongo = function() { pkgs = []; lock = false; - _fetch( - "https://kf.penguins184.xyz/registry.json", - function() { - _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { - var joined = data.replace(/\d+\.\s*/g, "\n").trim(); - var installed = joined.split(/\n+/).map(function(line) { - return line.replace(/^\d+\.\s*/, "").trim(); - }).filter(Boolean); - render(installed); - }); - } - ); + loadAllRegistries("file://mnt/us/.KFPM/registrylist.txt") + // _fetch( + // "https://kf.penguins184.xyz/registry.json", + // function() { + // _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { + // var joined = data.replace(/\d+\.\s*/g, "\n").trim(); + // var installed = joined.split(/\n+/).map(function(line) { + // return line.replace(/^\d+\.\s*/, "").trim(); + // }).filter(Boolean); + // render(installed); + // }); + // } + // ); } else if (id === "KFORGE_UPDATE") { window.kindle.messaging.sendStringMessage("com.kindlemodding.utild", "runCMD", "curl https://kf.penguins184.xyz/update.sh | sh"); }; @@ -130,7 +131,7 @@ function isPackageSupported(pkgsJson, pkg, loopedDeps) { loopedDeps = loopedDeps.slice(); loopedDeps.push(pkg.uri); - + var deps = pkg.dependencies || []; for (var i = 0; i < deps.length; i++) { var dep = getPackage(deps[i], pkgsJson); @@ -150,7 +151,7 @@ function _fetch(url, cb) { try { var tempPkgs = JSON.parse(xhr.responseText); for (var i = 0; i < tempPkgs.length; i++) { - var pkg = tempPkgs[i]; + var pkg = tempPkgs[i]; if (!isPackageSupported(tempPkgs, pkg, [])) continue; @@ -269,7 +270,7 @@ function render(installed) { var pkgId = btn.getAttribute("data-id"); var name = btn.getAttribute("data-name"); var wasInstalled = btn.getAttribute("data-installed") === "true"; - + if (lock) { btn.innerHTML = icons.progress + " Another Operation In Progress..."; btn.blur(); btn.offsetHeight; //Blur & Reflow @@ -279,36 +280,36 @@ function render(installed) { btn.offsetHeight; //Ensure Reflow }); }); - + setTimeout(function() { btn.innerHTML = (wasInstalled ? icons.x : icons.download) + (wasInstalled ? " Uninstall Package" : " Install Package"); }, 2000); - - setTimeout(function() {}, 50); //UI Update Time + + setTimeout(function() { }, 50); //UI Update Time return; } - + lock = true; btn.disabled = true; - + var action = wasInstalled ? "-r" : "-i"; btn.innerHTML = icons.progress + (wasInstalled ? " Uninstalling " : " Installing ") + name + "..."; - + btn.offsetHeight; //Reflow - + var eventName = wasInstalled ? "packageUninstallStatus" : "packageInstallStatus"; (window.kindle || top.kindle).messaging.receiveMessage( eventName, function(eventType, data) { lock = false; btn.disabled = false; - + var success = typeof data === "string" && data.indexOf("success") !== -1; if (success) { @@ -318,7 +319,7 @@ function render(installed) { (wasInstalled ? " Install Package" : " Uninstall Package"); - + // Update dependency buttons if (!wasInstalled) { var deps = getPackage(pkgId, pkgs).dependencies || []; @@ -330,7 +331,7 @@ function render(installed) { btn.offsetHeight; //Reflow } } - + } else { btn.innerHTML = icons.x + @@ -342,7 +343,7 @@ function render(installed) { } } ); - + setTimeout(function() { (window.kindle || top.kindle).messaging.sendStringMessage( "com.kindlemodding.utild", @@ -359,6 +360,51 @@ function render(installed) { gCard(cIndex); } +/* This function loads the lines from file://mnt/us/.KFPM/registrylist.txt and then loads each one as a registry. */ +/* Then it combines them all together and renders based on that, not on each one individually*/ +function loadAllRegistries(fileUrl) { + + _file(fileUrl).then(function(data) { + var joined = data.replace(/\d+\.\s*/g, "\n").trim(); + var repos = joined.split(/\n+/).map(function(line) { + return line.replace(/^\d+\.\s*/, "").trim(); + }).filter(Boolean); + + numberOfRepos = repos.length; + + if (numberOfRepos == 0) { + _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { + var joined = data.replace(/\d+\.\s*/g, "\n").trim(); + var installed = joined.split(/\n+/).map(function(line) { + return line.replace(/^\d+\.\s*/, "").trim(); + }).filter(Boolean); + render(installed); + }); + console.error("0 repos!"); + return; + } + + reposToLoad = numberOfRepos; + + function oneRepoLoaded() { + reposToLoad--; + if (reposToLoad == 0) { + _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { + var joined = data.replace(/\d+\.\s*/g, "\n").trim(); + var installed = joined.split(/\n+/).map(function(line) { + return line.replace(/^\d+\.\s*/, "").trim(); + }).filter(Boolean); + render(installed); + }); + }; + } + repos.forEach(function(url) { + _fetch(url, oneRepoLoaded); + }); + + }); +}; + document.addEventListener("DOMContentLoaded", function() { (window.kindle || top.kindle).messaging.receiveMessage("deviceABI", function(eventType, ABI) { deviceABI = ABI; @@ -372,9 +418,10 @@ document.addEventListener("DOMContentLoaded", function() { "/var/local/mesquite/KindleForge/binaries/KFPM -abi" ); }, 10); - - _fetch( - "https://kf.penguins184.xyz/registry.json" - ); + + loadAllRegistries("file://mnt/us/.KFPM/registrylist.txt") + // _fetch( + // "https://kf.penguins184.xyz/registry.json" + // ); document.getElementById("js-status").innerText = "JS Working!"; }); From 07ef28ff99e156d116b362923bae0a0de110067e Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Sun, 2 Nov 2025 23:28:22 -0700 Subject: [PATCH 02/14] Cleaned up dead code Init() was no longer needed, as it was only used on the document start and can be replaced with the new load function --- KindleForge/KindleForge/script.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index 0c00af5..daa0480 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -56,18 +56,6 @@ window.kindle.appmgr.ongo = function() { lock = false; loadAllRegistries("file://mnt/us/.KFPM/registrylist.txt") - // _fetch( - // "https://kf.penguins184.xyz/registry.json", - // function() { - // _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { - // var joined = data.replace(/\d+\.\s*/g, "\n").trim(); - // var installed = joined.split(/\n+/).map(function(line) { - // return line.replace(/^\d+\.\s*/, "").trim(); - // }).filter(Boolean); - // render(installed); - // }); - // } - // ); } else if (id === "KFORGE_UPDATE") { window.kindle.messaging.sendStringMessage("com.kindlemodding.utild", "runCMD", "curl https://kf.penguins184.xyz/update.sh | sh"); }; @@ -158,7 +146,6 @@ function _fetch(url, cb) { pkgs.push(pkg); } if (cb) cb(); - else init(); } catch (e) { console.log("JSON Parse Failed", e); } @@ -186,15 +173,6 @@ function _file(url) { }); } -function init() { - _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { - var joined = data.replace(/\d+\.\s*/g, "\n").trim(); - var installed = joined.split(/\n+/).map(function(line) { - return line.replace(/^\d+\.\s*/, "").trim(); - }).filter(Boolean); - render(installed); - }); -} function render(installed) { var icons = { @@ -420,8 +398,5 @@ document.addEventListener("DOMContentLoaded", function() { }, 10); loadAllRegistries("file://mnt/us/.KFPM/registrylist.txt") - // _fetch( - // "https://kf.penguins184.xyz/registry.json" - // ); document.getElementById("js-status").innerText = "JS Working!"; }); From ad3257fcf1cb3f044dc4ea23a92e1cc1748fedfc Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Sun, 2 Nov 2025 23:44:53 -0700 Subject: [PATCH 03/14] Moved (file data) -> (array of strings) to an external function it was used all over, so i just moved it out. --- KindleForge/KindleForge/script.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index daa0480..ed4be0f 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -338,31 +338,30 @@ function render(installed) { gCard(cIndex); } -/* This function loads the lines from file://mnt/us/.KFPM/registrylist.txt and then loads each one as a registry. */ -/* Then it combines them all together and renders based on that, not on each one individually*/ -function loadAllRegistries(fileUrl) { +function dataLinesToArray(data) { + var joined = data.replace(/\d+\.\s*/g, "\n").trim(); + var list = joined.split(/\n+/).map(function(line) { + return line.replace(/^\d+\.\s*/, "").trim(); + }).filter(Boolean); + return list; +} +function loadAllRegistries(fileUrl) { _file(fileUrl).then(function(data) { - var joined = data.replace(/\d+\.\s*/g, "\n").trim(); - var repos = joined.split(/\n+/).map(function(line) { - return line.replace(/^\d+\.\s*/, "").trim(); - }).filter(Boolean); + var repos = dataLinesToArray(data) - numberOfRepos = repos.length; + var numberOfRepos = repos.length; if (numberOfRepos == 0) { _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { - var joined = data.replace(/\d+\.\s*/g, "\n").trim(); - var installed = joined.split(/\n+/).map(function(line) { - return line.replace(/^\d+\.\s*/, "").trim(); - }).filter(Boolean); + installed = dataLinesToArray(data); render(installed); }); console.error("0 repos!"); return; } - reposToLoad = numberOfRepos; + var reposToLoad = numberOfRepos; function oneRepoLoaded() { reposToLoad--; From ab883e87ddc0aaea631243fd59fd28bf22b9024e Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Sun, 2 Nov 2025 23:50:49 -0700 Subject: [PATCH 04/14] Move file URLs to consts at top of file I figured it would make refactoring easier in the future. I wanted it so I didn't have to pass the url into the loadAll function every time or "hard code" it --- KindleForge/KindleForge/script.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index ed4be0f..0a5f9e1 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -45,6 +45,9 @@ function update() { window.kindle.messaging.sendMessage("com.lab126.chromebar", "configureChrome", chromebar); } +const installedFileURL = "file:///mnt/us/.KFPM/installed.txt"; +const registriesFileURL = "file:///mnt/us/.KFPM/registrylist.txt"; + window.kindle.appmgr.ongo = function() { update(); window.kindle.messaging.receiveMessage("systemMenuItemSelected", function(eventType, id) { @@ -55,7 +58,7 @@ window.kindle.appmgr.ongo = function() { pkgs = []; lock = false; - loadAllRegistries("file://mnt/us/.KFPM/registrylist.txt") + loadAllRegistries(registriesFileURL) } else if (id === "KFORGE_UPDATE") { window.kindle.messaging.sendStringMessage("com.kindlemodding.utild", "runCMD", "curl https://kf.penguins184.xyz/update.sh | sh"); }; @@ -353,7 +356,7 @@ function loadAllRegistries(fileUrl) { var numberOfRepos = repos.length; if (numberOfRepos == 0) { - _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { + _file(installedFileURL).then(function(data) { installed = dataLinesToArray(data); render(installed); }); @@ -366,7 +369,7 @@ function loadAllRegistries(fileUrl) { function oneRepoLoaded() { reposToLoad--; if (reposToLoad == 0) { - _file("file:///mnt/us/.KFPM/installed.txt").then(function(data) { + _file(installedFileURL).then(function(data) { var joined = data.replace(/\d+\.\s*/g, "\n").trim(); var installed = joined.split(/\n+/).map(function(line) { return line.replace(/^\d+\.\s*/, "").trim(); @@ -374,8 +377,7 @@ function loadAllRegistries(fileUrl) { render(installed); }); }; - } - repos.forEach(function(url) { + } repos.forEach(function(url) { _fetch(url, oneRepoLoaded); }); @@ -396,6 +398,6 @@ document.addEventListener("DOMContentLoaded", function() { ); }, 10); - loadAllRegistries("file://mnt/us/.KFPM/registrylist.txt") + loadAllRegistries(registriesFileURL) document.getElementById("js-status").innerText = "JS Working!"; }); From 5e4da5b10970bffff43e5ce7c9b9f2f40946ef06 Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Sun, 2 Nov 2025 23:52:54 -0700 Subject: [PATCH 05/14] Forgot to replace a block of code with dataLinesToArray --- KindleForge/KindleForge/script.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index 0a5f9e1..7522d1c 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -370,10 +370,7 @@ function loadAllRegistries(fileUrl) { reposToLoad--; if (reposToLoad == 0) { _file(installedFileURL).then(function(data) { - var joined = data.replace(/\d+\.\s*/g, "\n").trim(); - var installed = joined.split(/\n+/).map(function(line) { - return line.replace(/^\d+\.\s*/, "").trim(); - }).filter(Boolean); + var installed = dataLinesToArray(data); render(installed); }); }; From 4026739c337dcd27d58303feb94266faf4509812 Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Mon, 3 Nov 2025 00:00:54 -0700 Subject: [PATCH 06/14] renamed the repo file I checked the discord roadmap, and thats what its named there. It doesn't matter much. --- KindleForge/KindleForge/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index 7522d1c..99e9327 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -46,7 +46,7 @@ function update() { } const installedFileURL = "file:///mnt/us/.KFPM/installed.txt"; -const registriesFileURL = "file:///mnt/us/.KFPM/registrylist.txt"; +const registriesFileURL = "file:///mnt/us/.KFPM/repositories.txt"; window.kindle.appmgr.ongo = function() { update(); From ae6b197516b3892b9dd0cf704ff8eb606f2dac25 Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Mon, 3 Nov 2025 00:48:28 -0700 Subject: [PATCH 07/14] fixed a stupid race condition and a stupid syntax error --- KindleForge/KindleForge/script.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index 99e9327..6bb7ebb 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -374,7 +374,8 @@ function loadAllRegistries(fileUrl) { render(installed); }); }; - } repos.forEach(function(url) { + }; + repos.forEach(function(url) { _fetch(url, oneRepoLoaded); }); @@ -385,6 +386,8 @@ document.addEventListener("DOMContentLoaded", function() { (window.kindle || top.kindle).messaging.receiveMessage("deviceABI", function(eventType, ABI) { deviceABI = ABI; document.getElementById("abi-status").innerText = "ABI: " + ABI; + + loadAllRegistries(registriesFileURL) }); setTimeout(function() { @@ -395,6 +398,5 @@ document.addEventListener("DOMContentLoaded", function() { ); }, 10); - loadAllRegistries(registriesFileURL) document.getElementById("js-status").innerText = "JS Working!"; }); From 5b076ece9670eca874c0115f7fc1a940b016ac30 Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:09:22 -0700 Subject: [PATCH 08/14] repositories.txt should point to the base url, not the registry.json --- KindleForge/KindleForge/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index 6bb7ebb..34ee452 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -376,7 +376,7 @@ function loadAllRegistries(fileUrl) { }; }; repos.forEach(function(url) { - _fetch(url, oneRepoLoaded); + _fetch(url + "/registry.json", oneRepoLoaded); }); }); From 29a56b5162115318c0d846d7b3d3dcf52f79d51e Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:38:12 -0700 Subject: [PATCH 09/14] go fmt. Not necissary, but it follows the default go code conventions. (My editor auto runs it on save) --- KFPM/main.go | 495 +++++++++++++++++++++++++++------------------------ 1 file changed, 265 insertions(+), 230 deletions(-) diff --git a/KFPM/main.go b/KFPM/main.go index 5a12f95..55c2f49 100644 --- a/KFPM/main.go +++ b/KFPM/main.go @@ -8,101 +8,104 @@ package main import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "slices" - "strings" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "slices" + "strings" ) const ( - registryURL = "https://kf.penguins184.xyz/registry.json" - registryBase = "https://kf.penguins184.xyz/" - installedFile = "/mnt/us/.KFPM/installed.txt" + registryBase = "https://kf.penguins184.xyz/" + installedFile = "/mnt/us/.KFPM/installed.txt" + registryFile = "/mnt/us/.KFPM/repositories.txt" ) var ( - registry = fetchRegistry() - installed = getInstalled() - ABI = fetchABI() + registry = fetchRegistry("https://kf.penguins184.xyz/") + installed = getInstalled() + ABI = fetchABI() + repositories []string ) type Package struct { - Name string `json:"name"` - Uri string `json:"uri"` - Dependencies []string `json:"dependencies"` - Description string `json:"description"` - Author string `json:"author"` - ABI []string `json:"ABI"` + Name string `json:"name"` + Uri string `json:"uri"` + Dependencies []string `json:"dependencies"` + Description string `json:"description"` + Author string `json:"author"` + ABI []string `json:"ABI"` + Repo string `json:` } func main() { - ensureInstalledDir() - - args := os.Args[1:] - - if len(args) == 0 { - help() - return - } - - verbose := len(args) > 2 && args[2] == "-v" - - switch args[0] { - case "-i": - if len(args) < 2 { - fmt.Println("Oops! -i Requires A Package Name!") - return - } - pkgId := args[1] - err := install(pkgId, verbose, []string{}) - if err != nil { - fmt.Printf("[KFPM] %s\n", err.Error()) - } - - case "-r", "-u": - if len(args) < 2 { - fmt.Println("Error: -r/-u Requires A Package Name!") - return - } - pkg := args[1] - - if !isInstalled(pkg) { - fmt.Println("[KFPM] Package ID Not Installed.") - return - } - - if runScript(pkg, "uninstall", verbose) { - fmt.Println("[KFPM] Removal Success!") - removeInstalled(pkg) - setStatus("packageUninstallStatus", "success") - } else { - fmt.Println("[KFPM] Removal Failure!") - setStatus("packageUninstallStatus", "failure") - } - - case "-l": - listInstalled() - - case "-a": - listAvailable() - - case "-abi": - fmt.Printf("[KFPM] ABI: %s\n", ABI) - setStatus("deviceABI", ABI) - - default: - fmt.Println("Unknown Option:", args[0]) - help() - } + ensureInstalledDir() + // checkRepositoryFile() + + args := os.Args[1:] + + if len(args) == 0 { + help() + return + } + + verbose := len(args) > 2 && args[2] == "-v" + + switch args[0] { + case "-i": + if len(args) < 2 { + fmt.Println("Oops! -i Requires A Package Name!") + return + } + pkgId := args[1] + err := install(pkgId, verbose, []string{}) + if err != nil { + fmt.Printf("[KFPM] %s\n", err.Error()) + } + + case "-r", "-u": + if len(args) < 2 { + fmt.Println("Error: -r/-u Requires A Package Name!") + return + } + pkg := args[1] + + if !isInstalled(pkg) { + fmt.Println("[KFPM] Package ID Not Installed.") + return + } + + if runScript(pkg, "uninstall", verbose) { + fmt.Println("[KFPM] Removal Success!") + removeInstalled(pkg) + setStatus("packageUninstallStatus", "success") + } else { + fmt.Println("[KFPM] Removal Failure!") + setStatus("packageUninstallStatus", "failure") + } + + case "-l": + listInstalled() + + case "-a": + listAvailable() + + case "-abi": + fmt.Printf("[KFPM] ABI: %s\n", ABI) + setStatus("deviceABI", ABI) + + default: + fmt.Println("Unknown Option:", args[0]) + help() + } } func help() { - fmt.Println(`KindleForge Package Manager + fmt.Println(`KindleForge Package Manager ==================== v1.1b, made by Penguins184, ThatPotatoDev @@ -114,198 +117,230 @@ kfpm -a Lists All Available Packages`) // Ensure Data Directory Exists func ensureInstalledDir() { - os.MkdirAll("/mnt/us/.KFPM", 0755) + os.MkdirAll("/mnt/us/.KFPM", 0755) +} + +func checkRepositoryFile() { + _, err := os.Stat(registryFile) + + if os.IsNotExist(err) { + f, err := os.OpenFile(registryFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Println("error opening registry file", err) + return + } + defer f.Close() + defaultRepoURL := "https://kf.penguins184.xyz/" + _, err2 := f.WriteString(defaultRepoURL) + + if err2 != nil { + fmt.Println("error writing default repo file", err) + return + } + } +} + +func fetchAllRegistries() []Package { + data, err := os.ReadFile(registryFile) + if err != nil { + fmt.Println("Couldn't open registry list, fetchallregistires", err) + return nil + } } func install(pkgId string, verbose bool, loopedDeps []string) error { - if isInstalled(pkgId) { - fmt.Printf("[KFPM] Package '%s' Is Already Installed, Skipping\n", pkgId) - return nil - } - - if slices.Contains(loopedDeps, pkgId) { - return errors.New("Dependency Loop Detected, Aborting") - } - - loopedDeps = append(loopedDeps, pkgId) - - pkg, err := getPackage(pkgId) - - if err != nil { - return fmt.Errorf("Invalid Package ID '%s'!", pkgId) - } - - if len(pkg.ABI) == 0 { - pkg.ABI = []string{"sf", "hf"} - } - - if !slices.Contains(pkg.ABI, ABI) { - return fmt.Errorf("Package '%s' Does Not Support Device ABI!", pkgId) - } - - for _, depId := range pkg.Dependencies { - if err := install(depId, verbose, loopedDeps); err != nil { - return err - } - } - - if runScript(pkgId, "install", verbose) { - fmt.Printf("[KFPM] Successfully Installed '%s'!\n", pkgId) - appendInstalled(pkgId) - setStatus("packageInstallStatus", "success") - return nil - } else { - setStatus("packageInstallStatus", "failure") - return errors.New("Failed to install!") - } + if isInstalled(pkgId) { + fmt.Printf("[KFPM] Package '%s' Is Already Installed, Skipping\n", pkgId) + return nil + } + + if slices.Contains(loopedDeps, pkgId) { + return errors.New("Dependency Loop Detected, Aborting") + } + + loopedDeps = append(loopedDeps, pkgId) + + pkg, err := getPackage(pkgId) + + if err != nil { + return fmt.Errorf("Invalid Package ID '%s'!", pkgId) + } + + if len(pkg.ABI) == 0 { + pkg.ABI = []string{"sf", "hf"} + } + + if !slices.Contains(pkg.ABI, ABI) { + return fmt.Errorf("Package '%s' Does Not Support Device ABI!", pkgId) + } + + for _, depId := range pkg.Dependencies { + if err := install(depId, verbose, loopedDeps); err != nil { + return err + } + } + + if runScript(pkgId, "install", verbose) { + fmt.Printf("[KFPM] Successfully Installed '%s'!\n", pkgId) + appendInstalled(pkgId) + setStatus("packageInstallStatus", "success") + return nil + } else { + setStatus("packageInstallStatus", "failure") + return errors.New("Failed to install!") + } } // Install/Uninstall Runners func runScript(pkg string, action string, verbose bool) bool { - url := fmt.Sprintf("%s%s/%s.sh", registryBase, pkg, action) - cmd := exec.Command("/bin/sh", "-c", "curl -fSL --progress-bar "+url+" | sh") + url := fmt.Sprintf("%s%s/%s.sh", registryBase, pkg, action) + cmd := exec.Command("/bin/sh", "-c", "curl -fSL --progress-bar "+url+" | sh") - if verbose { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } + if verbose { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } - err := cmd.Run() - return err == nil + err := cmd.Run() + return err == nil } // Append Package To List func appendInstalled(pkg string) { - data, _ := os.ReadFile(installedFile) - text := strings.TrimSpace(string(data)) - - lines := strings.Split(text, "\n") - for _, line := range lines { - if strings.TrimSpace(line) == pkg { - return - } - } - - f, err := os.OpenFile(installedFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return - } - defer f.Close() - - // Ensure Newline - if len(text) > 0 && !strings.HasSuffix(text, "\n") { - f.WriteString("\n") - } - - f.WriteString(strings.TrimSpace(pkg) + "\n") - installed = getInstalled() + data, _ := os.ReadFile(installedFile) + text := strings.TrimSpace(string(data)) + + lines := strings.Split(text, "\n") + for _, line := range lines { + if strings.TrimSpace(line) == pkg { + return + } + } + + f, err := os.OpenFile(installedFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return + } + defer f.Close() + + // Ensure Newline + if len(text) > 0 && !strings.HasSuffix(text, "\n") { + f.WriteString("\n") + } + + f.WriteString(strings.TrimSpace(pkg) + "\n") + installed = getInstalled() } // Remove Package From List func removeInstalled(pkg string) { - data, err := os.ReadFile(installedFile) - if err != nil { - return - } - - lines := strings.Split(strings.TrimSpace(string(data)), "\n") - var out []string - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed != "" && trimmed != pkg { - out = append(out, trimmed) - } - } - - os.WriteFile(installedFile, []byte(strings.Join(out, "\n")+"\n"), 0644) + data, err := os.ReadFile(installedFile) + if err != nil { + return + } + + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + var out []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" && trimmed != pkg { + out = append(out, trimmed) + } + } + + os.WriteFile(installedFile, []byte(strings.Join(out, "\n")+"\n"), 0644) } // List Installed Packages func listInstalled() { - data, err := os.ReadFile(installedFile) - if err != nil || len(strings.TrimSpace(string(data))) == 0 { - fmt.Println("[KFPM] No Installed Packages Found!") - return - } - - lines := strings.Split(strings.TrimSpace(string(data)), "\n") - fmt.Println("Installed Packages:") - for i, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed != "" { - fmt.Printf("%d. %s\n", i+1, trimmed) - } - } + data, err := os.ReadFile(installedFile) + if err != nil || len(strings.TrimSpace(string(data))) == 0 { + fmt.Println("[KFPM] No Installed Packages Found!") + return + } + + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + fmt.Println("Installed Packages:") + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" { + fmt.Printf("%d. %s\n", i+1, trimmed) + } + } } // List Available Packages From Remote func listAvailable() { - pkgs := registry - if pkgs == nil { - return - } - - fmt.Println("Available Packages:") - for i, p := range pkgs { - fmt.Printf("%d. %s\n", i+1, p.Name) - fmt.Printf(" - Description: %s\n", p.Description) - fmt.Printf(" - Author: %s\n", p.Author) - fmt.Printf(" - ID: %s\n", p.Uri) - fmt.Printf(" - Dependencies: %s\n", p.Dependencies) - fmt.Printf(" - ABI: %s\n\n", p.ABI) - } + pkgs := registry + if pkgs == nil { + return + } + + fmt.Println("Available Packages:") + for i, p := range pkgs { + fmt.Printf("%d. %s\n", i+1, p.Name) + fmt.Printf(" - Description: %s\n", p.Description) + fmt.Printf(" - Author: %s\n", p.Author) + fmt.Printf(" - ID: %s\n", p.Uri) + fmt.Printf(" - Dependencies: %s\n", p.Dependencies) + fmt.Printf(" - ABI: %s\n\n", p.ABI) + } } // Helpers -func fetchRegistry() []Package { - resp, err := http.Get(registryURL) - if err != nil { - fmt.Println("[KFPM] Failed To Fetch Registry:", err) - return nil - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - var pkgs []Package - if err := json.Unmarshal(body, &pkgs); err != nil { - fmt.Println("[KFPM] Invalid Registry Format:", err) - return nil - } - return pkgs +func fetchRegistry(baseURL string) []Package { + resp, err := http.Get(baseURL + "/registry.txt") + if err != nil { + fmt.Println("[KFPM] Failed To Fetch Registry:", err) + return nil + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var pkgs []Package + if err := json.Unmarshal(body, &pkgs); err != nil { + fmt.Println("[KFPM] Invalid Registry Format:", err) + return nil + } + + for i := range pkgs { + pkgs[i].Repo = baseURL + } + return pkgs } func getPackage(id string) (Package, error) { - for _, p := range registry { - if p.Uri == id { - return p, nil - } - } - return Package{}, errors.New("Package Not Found!") + for _, p := range registry { + if p.Uri == id { + return p, nil + } + } + return Package{}, errors.New("Package Not Found!") } func isInstalled(id string) bool { - return slices.Contains(installed, id) + return slices.Contains(installed, id) } func getInstalled() []string { - data, err := os.ReadFile(installedFile) - if err != nil { - return nil - } - return strings.Split(strings.TrimSpace(string(data)), "\n") + data, err := os.ReadFile(installedFile) + if err != nil { + return nil + } + return strings.Split(strings.TrimSpace(string(data)), "\n") } func setStatus(prop string, status string) { - exec.Command( - "/bin/sh", "-c", - fmt.Sprintf(`lipc-set-prop xyz.penguins184.kindleforge %s -s "%s"`, prop, status), - ).Run() + exec.Command( + "/bin/sh", "-c", + fmt.Sprintf(`lipc-set-prop xyz.penguins184.kindleforge %s -s "%s"`, prop, status), + ).Run() } func fetchABI() string { - if _, err := os.Stat("/lib/ld-linux-armhf.so.3"); !os.IsNotExist(err) { - return "hf" - } - return "sf" + if _, err := os.Stat("/lib/ld-linux-armhf.so.3"); !os.IsNotExist(err) { + return "hf" + } + return "sf" } From 677b32b6b578cd8609baa36085a46bfe7cf1828f Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:57:19 -0700 Subject: [PATCH 10/14] Transition to using multiple sources It searches all repos instead of the hardcoded one --- KFPM/main.go | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/KFPM/main.go b/KFPM/main.go index 55c2f49..980f98c 100644 --- a/KFPM/main.go +++ b/KFPM/main.go @@ -26,10 +26,9 @@ const ( ) var ( - registry = fetchRegistry("https://kf.penguins184.xyz/") - installed = getInstalled() - ABI = fetchABI() - repositories []string + registry = fetchAllRegistries() + installed = getInstalled() + ABI = fetchABI() ) type Package struct { @@ -39,12 +38,12 @@ type Package struct { Description string `json:"description"` Author string `json:"author"` ABI []string `json:"ABI"` - Repo string `json:` + Repo string `json:"-"` } func main() { ensureInstalledDir() - // checkRepositoryFile() + checkRepositoryFile() args := os.Args[1:] @@ -72,16 +71,21 @@ func main() { fmt.Println("Error: -r/-u Requires A Package Name!") return } - pkg := args[1] + pkgId := args[1] - if !isInstalled(pkg) { + if !isInstalled(pkgId) { fmt.Println("[KFPM] Package ID Not Installed.") return } + pkg, err := getPackage(pkgId) + if err != nil { + fmt.Println("package not found main case ru", err) + return + } if runScript(pkg, "uninstall", verbose) { fmt.Println("[KFPM] Removal Success!") - removeInstalled(pkg) + removeInstalled(pkgId) setStatus("packageUninstallStatus", "success") } else { fmt.Println("[KFPM] Removal Failure!") @@ -134,7 +138,7 @@ func checkRepositoryFile() { _, err2 := f.WriteString(defaultRepoURL) if err2 != nil { - fmt.Println("error writing default repo file", err) + fmt.Println("error writing default repo file", err2) return } } @@ -146,6 +150,23 @@ func fetchAllRegistries() []Package { fmt.Println("Couldn't open registry list, fetchallregistires", err) return nil } + + urls := strings.Split(strings.TrimSpace(string(data)), "\n") + var allPackageList []Package + + for _, url := range urls { + baseURL := strings.TrimSpace(url) + //blank line + if baseURL == "" { + continue + } + + pkgs := fetchRegistry(baseURL) + if pkgs != nil { + allPackageList = append(allPackageList, pkgs...) + } + } + return allPackageList } func install(pkgId string, verbose bool, loopedDeps []string) error { @@ -181,7 +202,7 @@ func install(pkgId string, verbose bool, loopedDeps []string) error { } } - if runScript(pkgId, "install", verbose) { + if runScript(pkg, "install", verbose) { fmt.Printf("[KFPM] Successfully Installed '%s'!\n", pkgId) appendInstalled(pkgId) setStatus("packageInstallStatus", "success") @@ -193,8 +214,8 @@ func install(pkgId string, verbose bool, loopedDeps []string) error { } // Install/Uninstall Runners -func runScript(pkg string, action string, verbose bool) bool { - url := fmt.Sprintf("%s%s/%s.sh", registryBase, pkg, action) +func runScript(pkg Package, action string, verbose bool) bool { + url := fmt.Sprintf("%s%s/%s.sh", pkg.Repo, pkg.Uri, action) cmd := exec.Command("/bin/sh", "-c", "curl -fSL --progress-bar "+url+" | sh") if verbose { @@ -290,7 +311,7 @@ func listAvailable() { // Helpers func fetchRegistry(baseURL string) []Package { - resp, err := http.Get(baseURL + "/registry.txt") + resp, err := http.Get(baseURL + "/registry.json") if err != nil { fmt.Println("[KFPM] Failed To Fetch Registry:", err) return nil From f64a0b921782b08c59ec6138e0b55b665359e0f9 Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:12:32 -0700 Subject: [PATCH 11/14] Fix race condition if repositories.txt doesn't exist Move the fetchallrepositories stage to after the check for the file runs. --- KFPM/main.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/KFPM/main.go b/KFPM/main.go index 980f98c..7b51840 100644 --- a/KFPM/main.go +++ b/KFPM/main.go @@ -20,15 +20,14 @@ import ( ) const ( - registryBase = "https://kf.penguins184.xyz/" installedFile = "/mnt/us/.KFPM/installed.txt" registryFile = "/mnt/us/.KFPM/repositories.txt" ) var ( - registry = fetchAllRegistries() - installed = getInstalled() - ABI = fetchABI() + registry []Package + installed []string + ABI string ) type Package struct { @@ -45,6 +44,9 @@ func main() { ensureInstalledDir() checkRepositoryFile() + registry = fetchAllRegistries() + installed = getInstalled() + ABI = fetchABI() args := os.Args[1:] if len(args) == 0 { From 8a9366208efada03dee10aec7e2299facf3fb120 Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:38:44 -0700 Subject: [PATCH 12/14] URL was parsing wrong Should've been parsing by newline. Nothing was showing up on screen because the main repo got split into two urls, both invalid. --- KindleForge/KindleForge/script.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/KindleForge/KindleForge/script.js b/KindleForge/KindleForge/script.js index 34ee452..275d3bc 100644 --- a/KindleForge/KindleForge/script.js +++ b/KindleForge/KindleForge/script.js @@ -342,9 +342,8 @@ function render(installed) { } function dataLinesToArray(data) { - var joined = data.replace(/\d+\.\s*/g, "\n").trim(); - var list = joined.split(/\n+/).map(function(line) { - return line.replace(/^\d+\.\s*/, "").trim(); + var list = data.split(/\n+/).map(function(line) { + return line.trim(); }).filter(Boolean); return list; } From db4a64aa0485b650598065cb250cfbae620c0cd5 Mon Sep 17 00:00:00 2001 From: lewhfree <48800753+lewhfree@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:40:39 -0700 Subject: [PATCH 13/14] Use new KFPM binary for release. --- KindleForge/KindleForge/binaries/KFPM | Bin 8472070 -> 8653416 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/KindleForge/KindleForge/binaries/KFPM b/KindleForge/KindleForge/binaries/KFPM index d97c0e44d2a35fba82790ff1c5b6f09b1b19136f..c16c9be3e92c031259cec12fc5abeaba26c0ae3b 100644 GIT binary patch literal 8653416 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