From 1242723ac12c3df782801cb3a5724550e38f9893 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 10:44:28 +0000 Subject: [PATCH] fix(pkgcmd): remove packages from registry Co-Authored-By: Virgil --- cmd/core/pkgcmd/cmd_remove.go | 69 ++++++++++++++++++++++++++++++ cmd/core/pkgcmd/cmd_remove_test.go | 38 ++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/cmd/core/pkgcmd/cmd_remove.go b/cmd/core/pkgcmd/cmd_remove.go index ba3fa58..8a45e0f 100644 --- a/cmd/core/pkgcmd/cmd_remove.go +++ b/cmd/core/pkgcmd/cmd_remove.go @@ -18,6 +18,7 @@ import ( coreio "forge.lthn.ai/core/go-io" "forge.lthn.ai/core/go-scm/repos" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) var removeForce bool @@ -87,10 +88,78 @@ func runPkgRemove(name string, force bool) error { return err } + if err := removeRepoFromRegistry(regPath, name); err != nil { + return fmt.Errorf("removed %s from disk, but failed to update registry: %w", name, err) + } + fmt.Printf("%s\n", successStyle.Render("ok")) return nil } +func removeRepoFromRegistry(regPath, name string) error { + content, err := coreio.Local.Read(regPath) + if err != nil { + return err + } + + var doc yaml.Node + if err := yaml.Unmarshal([]byte(content), &doc); err != nil { + return fmt.Errorf("failed to parse registry file: %w", err) + } + if len(doc.Content) == 0 { + return errors.New("registry file is empty") + } + + root := doc.Content[0] + reposNode := mappingValue(root, "repos") + if reposNode == nil { + return errors.New("registry file has no repos section") + } + if reposNode.Kind != yaml.MappingNode { + return errors.New("registry repos section is malformed") + } + + if removeMappingEntry(reposNode, name) { + out, err := yaml.Marshal(&doc) + if err != nil { + return fmt.Errorf("failed to format registry file: %w", err) + } + return coreio.Local.Write(regPath, string(out)) + } + + return nil +} + +func mappingValue(node *yaml.Node, key string) *yaml.Node { + if node == nil || node.Kind != yaml.MappingNode { + return nil + } + + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i].Value == key { + return node.Content[i+1] + } + } + + return nil +} + +func removeMappingEntry(node *yaml.Node, key string) bool { + if node == nil || node.Kind != yaml.MappingNode { + return false + } + + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i].Value != key { + continue + } + node.Content = append(node.Content[:i], node.Content[i+2:]...) + return true + } + + return false +} + // checkRepoSafety checks a git repo for uncommitted changes and unpushed branches. func checkRepoSafety(repoPath string) (blocked bool, reasons []string) { // Check for uncommitted changes (staged, unstaged, untracked) diff --git a/cmd/core/pkgcmd/cmd_remove_test.go b/cmd/core/pkgcmd/cmd_remove_test.go index 442a08e..262b3b9 100644 --- a/cmd/core/pkgcmd/cmd_remove_test.go +++ b/cmd/core/pkgcmd/cmd_remove_test.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -78,6 +79,43 @@ func TestCheckRepoSafety_Stash(t *testing.T) { assert.True(t, found, "expected stash warning in reasons: %v", reasons) } +func TestRunPkgRemove_RemovesRegistryEntry_Good(t *testing.T) { + tmp := t.TempDir() + repoPath := setupTestRepo(t, tmp, "core-alpha") + + registry := strings.TrimSpace(` +version: 1 +org: host-uk +base_path: . +repos: + core-alpha: + type: foundation + description: Alpha package + core-beta: + type: module + description: Beta package +`) + "\n" + + require.NoError(t, os.WriteFile(filepath.Join(tmp, "repos.yaml"), []byte(registry), 0644)) + + oldwd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(tmp)) + t.Cleanup(func() { + require.NoError(t, os.Chdir(oldwd)) + }) + + require.NoError(t, runPkgRemove("core-alpha", false)) + + _, err = os.Stat(repoPath) + assert.True(t, os.IsNotExist(err)) + + updated, err := os.ReadFile(filepath.Join(tmp, "repos.yaml")) + require.NoError(t, err) + assert.NotContains(t, string(updated), "core-alpha") + assert.Contains(t, string(updated), "core-beta") +} + func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr)) }