From 93feb4c96697a0f378eb330a4e36b338ff726d87 Mon Sep 17 00:00:00 2001 From: E Spelt Date: Wed, 26 Aug 2026 20:14:31 +0200 Subject: [PATCH 1/2] fix(installer): match install/uninstall args against boss.json keys by normalized name "boss install https://host/owner/repo" reported "No dependencies to install" and saved nothing. EnsureDependency strips the protocol prefix when it stores the key in boss.json (parseURLAndVersion's regex consumes it), but collectDependenciesToInstall compared boss.json keys against ParseDependency(arg), which keeps the prefix. The two never matched for a protocol-prefixed argument, so the requested dependency was filtered out and DoInstall returned before saving the package. Compare through the existing Dependency.Name() normalization instead (protocol, credentials and .git suffix ignored, case-insensitive), via a new domain.RepositoryName helper: - collectDependenciesToInstall and the requestedDeps filter in the install context now match by normalized name. - Package.AddDependency and Package.UninstallDependency match existing keys by normalized name too, so a protocol-prefixed argument updates or removes the bare key (and vice versa) instead of creating a duplicate entry or silently missing it. Co-Authored-By: Claude Fable 5 --- internal/core/domain/dependency.go | 9 +++++++++ internal/core/domain/package.go | 6 ++++-- internal/core/services/installer/core.go | 11 ++++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/internal/core/domain/dependency.go b/internal/core/domain/dependency.go index df0878aa..997b18a5 100644 --- a/internal/core/domain/dependency.go +++ b/internal/core/domain/dependency.go @@ -154,6 +154,15 @@ func (p *Dependency) GetKey() string { return strings.ToLower(p.Repository) } +// RepositoryName returns the normalized, collision-free name for a raw +// repository string, ignoring protocol, credentials and a .git suffix. +// Use it to compare user-supplied dependency arguments against boss.json +// keys, which may or may not carry a protocol prefix. +func RepositoryName(repo string) string { + d := Dependency{Repository: repo} + return d.Name() +} + // NeedsVersionUpdate checks if a version update is needed based on semver comparison. func NeedsVersionUpdate(currentVersion, newVersion string) bool { parsedNew, err := semver.NewVersion(newVersion) diff --git a/internal/core/domain/package.go b/internal/core/domain/package.go index e19fadbd..608d6390 100644 --- a/internal/core/domain/package.go +++ b/internal/core/domain/package.go @@ -48,8 +48,9 @@ func NewPackage() *Package { // AddDependency adds or updates a dependency in the package. func (p *Package) AddDependency(dep string, ver string) { + depName := RepositoryName(dep) for key := range p.Dependencies { - if strings.EqualFold(key, dep) { + if strings.EqualFold(key, dep) || strings.EqualFold(RepositoryName(key), depName) { p.Dependencies[key] = ver return } @@ -74,8 +75,9 @@ func (p *Package) GetParsedDependencies() []Dependency { // UninstallDependency removes a dependency from the package. func (p *Package) UninstallDependency(dep string) { if p.Dependencies != nil { + depName := RepositoryName(dep) for key := range p.Dependencies { - if strings.EqualFold(key, dep) { + if strings.EqualFold(key, dep) || strings.EqualFold(RepositoryName(key), depName) { delete(p.Dependencies, key) return } diff --git a/internal/core/services/installer/core.go b/internal/core/services/installer/core.go index 3a1ca212..d7a10378 100644 --- a/internal/core/services/installer/core.go +++ b/internal/core/services/installer/core.go @@ -54,8 +54,7 @@ func newInstallContext(config env.ConfigProvider, pkg *domain.Package, options I requestedDeps := make(map[string]bool) if len(options.Args) > 0 { for _, arg := range options.Args { - normalized := ParseDependency(arg) - requestedDeps[normalized] = true + requestedDeps[strings.ToLower(domain.RepositoryName(ParseDependency(arg)))] = true } } @@ -172,9 +171,11 @@ func collectDependenciesToInstall(pkg *domain.Package, args []string) []domain.D var filtered []domain.Dependency for _, arg := range args { - normalized := ParseDependency(arg) + // Compare by normalized name: boss.json keys may lack the protocol + // prefix the user typed (EnsureDependency strips it on save). + requestedName := domain.RepositoryName(ParseDependency(arg)) for _, dep := range allDeps { - if dep.Repository == normalized { + if strings.EqualFold(dep.Name(), requestedName) { filtered = append(filtered, dep) break } @@ -201,7 +202,7 @@ func (ic *installContext) ensureDependencies(pkg *domain.Package) ([]domain.Depe var deps []domain.Dependency if pkg == ic.root && len(ic.requestedDeps) > 0 { for _, dep := range allDeps { - if ic.requestedDeps[dep.Repository] { + if ic.requestedDeps[strings.ToLower(dep.Name())] { deps = append(deps, dep) } } From 9043a738419487caba5743d531bf3452490fc278 Mon Sep 17 00:00:00 2001 From: E Spelt Date: Wed, 26 Aug 2026 20:24:54 +0200 Subject: [PATCH 2/2] fix(installer): also match version-suffixed install/uninstall args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "boss install https://host/owner/repo:1.2.3" still reported "No dependencies to install": the argument was normalized including the ":1.2.3" suffix (Dependency.Name() turns the colon into a path separator), while the stored key has the version stripped. Normalize through parseURLAndVersion first — new requestedDependencyName helper used by collectDependenciesToInstall and the requestedDeps filter — and strip the version suffix in UninstallModules before matching, so ":version" and "@version" arguments resolve to the same key as their plain counterparts. Co-Authored-By: Claude Fable 5 --- internal/core/services/installer/core.go | 7 ++++--- internal/core/services/installer/installer.go | 5 +++++ internal/core/services/installer/utils.go | 12 ++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/internal/core/services/installer/core.go b/internal/core/services/installer/core.go index d7a10378..9e14e034 100644 --- a/internal/core/services/installer/core.go +++ b/internal/core/services/installer/core.go @@ -54,7 +54,7 @@ func newInstallContext(config env.ConfigProvider, pkg *domain.Package, options I requestedDeps := make(map[string]bool) if len(options.Args) > 0 { for _, arg := range options.Args { - requestedDeps[strings.ToLower(domain.RepositoryName(ParseDependency(arg)))] = true + requestedDeps[strings.ToLower(requestedDependencyName(arg))] = true } } @@ -172,8 +172,9 @@ func collectDependenciesToInstall(pkg *domain.Package, args []string) []domain.D var filtered []domain.Dependency for _, arg := range args { // Compare by normalized name: boss.json keys may lack the protocol - // prefix the user typed (EnsureDependency strips it on save). - requestedName := domain.RepositoryName(ParseDependency(arg)) + // prefix the user typed, and the argument may carry a version suffix + // (EnsureDependency strips both on save). + requestedName := requestedDependencyName(arg) for _, dep := range allDeps { if strings.EqualFold(dep.Name(), requestedName) { filtered = append(filtered, dep) diff --git a/internal/core/services/installer/installer.go b/internal/core/services/installer/installer.go index e3238297..7ce5602b 100644 --- a/internal/core/services/installer/installer.go +++ b/internal/core/services/installer/installer.go @@ -62,6 +62,11 @@ func UninstallModules(args []string, noSave bool) { for _, arg := range args { dependencyRepository := ParseDependency(arg) + // Drop any ":version"/"@version" suffix so the argument matches the + // stored key regardless of how the dependency was installed. + if url, _ := parseURLAndVersion(dependencyRepository); url != "" { + dependencyRepository = url + } pkg.UninstallDependency(dependencyRepository) } diff --git a/internal/core/services/installer/utils.go b/internal/core/services/installer/utils.go index 5b994c4a..15f45458 100644 --- a/internal/core/services/installer/utils.go +++ b/internal/core/services/installer/utils.go @@ -59,6 +59,18 @@ func parseURLAndVersion(input string) (string, string) { return url, version } +// requestedDependencyName normalizes a user-supplied dependency argument — +// optionally carrying a ":version" or "@version" suffix — to the same +// collision-free name boss.json keys resolve to. +func requestedDependencyName(arg string) string { + parsed := ParseDependency(arg) + url, _ := parseURLAndVersion(parsed) + if url == "" { + url = parsed + } + return domain.RepositoryName(url) +} + // EnsureDependency ensures that the dependencies are added to the package. func EnsureDependency(pkg *domain.Package, args []string) { for _, dependency := range args {