fix: preserve namespace in skills search deduplication#13170
fix: preserve namespace in skills search deduplication#13170SamMorrowDrums merged 2 commits intosm/add-skills-commandfrom
Conversation
Skills with the same name but different namespaces (e.g. skills/kynan/commit and skills/will/commit) were being collapsed into a single search result because extractSkillName discarded the namespace. This also caused deduplicateByName to cap results across different namespaces as if they were the same skill. Changes: - Add MatchSkillPath to discovery package returning both name and namespace (the existing MatchesSkillPath is kept for compat) - Add Namespace field to skillResult in search - Fix deduplicateResults to use repo/namespace/name as the dedup key - Fix deduplicateByName to cap by namespace-qualified name - Update table, prompt, and JSON output to show qualified names - Use skill path for install subprocess when namespace is present, ensuring unambiguous install of namespaced skills - Add namespace to --json fields and relevance scoring/filtering - Add unit tests for namespace dedup, qualified names, and filtering - Add acceptance test for namespaced skill search and install Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes gh skill search collapsing skills that share a base name by making deduplication and display namespace-aware (e.g. kynan/commit vs will/commit), and improves install disambiguation for namespaced skills.
Changes:
- Add
discovery.MatchSkillPathto extract both skill name and namespace from repo paths. - Update search result modeling/output/deduplication to key on
repo/namespace/name, display qualified names, and consider namespace in relevance scoring/filtering. - Add/adjust unit tests and add an acceptance test fixture for namespaced skills.
Show a summary per file
| File | Description |
|---|---|
internal/skills/discovery/discovery.go |
Adds MatchSkillPath returning (name, namespace) for skill path parsing. |
internal/skills/discovery/discovery_test.go |
Adds coverage for MatchSkillPath including namespaced and plugin conventions. |
pkg/cmd/skills/search/search.go |
Introduces Namespace + qualifiedName(), updates dedup keys, output, install arg selection, and relevance logic. |
pkg/cmd/skills/search/search_test.go |
Updates expectations and adds tests for namespaced dedup/qualified name and namespace relevance. |
acceptance/testdata/skills/skills-search-namespaced.txtar |
Adds an acceptance scenario creating/publishing/installing two namespaced skills. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 5/5 changed files
- Comments generated: 4
- Keep skillName as bare name in JSON output for backward compat; namespace is available as a separate --json field - Fix Namespace field comment to cover plugin namespaces too - Trim /SKILL.md from install path arg to match comment - Rename acceptance test to skills-install-namespaced since it tests install disambiguation, not search Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
|
||
| func TestMatchSkillPath(t *testing.T) { | ||
| tests := []struct { | ||
| testName string |
There was a problem hiding this comment.
nitpick: I'd rather change testName to either name or about.
| var result []skillResult | ||
| for _, s := range skills { | ||
| key := strings.ToLower(s.SkillName) | ||
| key := strings.ToLower(s.qualifiedName()) |
There was a problem hiding this comment.
question: what if two skill names only differ in casing?
| displayName := s.qualifiedName() | ||
| fmt.Fprintf(opts.IO.ErrOut, "\n%s Installing %s from %s...\n", | ||
| cs.Blue("::"), s.SkillName, s.Repo) | ||
| cs.Blue("::"), displayName, s.Repo) |
There was a problem hiding this comment.
nitpick: indented with whitespaces.
| key := item.Repository.FullName + "/" + namespace + "/" + skillName | ||
| if _, ok := seen[key]; ok { | ||
| continue | ||
| } | ||
| seen[key] = struct{}{} |
There was a problem hiding this comment.
nitpick: the nicer/idiomatic way is to avoid string keys (and string-based collisions) and use typed map keys:
func deduplicateResults(items []codeSearchItem) []skillResult {
type key struct {
repo string
skill string
namespace string
}
seen := make(map[key]struct{}{})
// ...
k := key{item.Repository.FullName, namespace, skillName}
if _, ok := seen[k]; ok {
continue
}
seen[k] = struct{}{}
}- Remove direct opts.client injection in publish; use HttpClient factory pattern (PR #13168 feedback) - Rename testName to name in discovery test struct (PR #13170 feedback) - Use typed struct keys for dedup map with case-insensitive comparison in deduplicateResults (PR #13170 feedback) - Simplify remote selection to use Remotes() ordering instead of manual origin-first logic (PR #13171 feedback) - Fix push icon timing: show no icon before push, SuccessIcon after success (PR #13171 feedback) Closes #13184 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Skills with the same name but different namespaces (e.g.
skills/kynan/commitandskills/will/commit) were being collapsed into a single search result. This PR fixes the deduplication logic to be namespace-aware.Problem
extractSkillNamecalledMatchesSkillPathwhich only returned the bare name, discarding the namespacededuplicateResultsusedrepo/nameas the dedup key — two namespaced skills in the same repo with the same base name collapsed into onededuplicateByNamecapped by bare name — skills from different namespaces were treated as duplicatespromptInstallpassed just the bare skill name to the install subprocess, which could be ambiguousChanges
internal/skills/discovery/discovery.goMatchSkillPath(filePath) (name, namespace string)— returns both name and namespacepkg/cmd/skills/search/search.goNamespacefield toskillResultandqualifiedName()helperextractSkillName→extractSkillInfousing newMatchSkillPathdeduplicateResultskey:repo/namespace/namededuplicateByNamekey:namespace/namenamespaceto--jsonfields and relevance scoring/filteringTests
TestDeduplicateResults_Namespaced— verifies same-name skills with different namespaces are preservedTestDeduplicateByName_Namespaced— verifies cap is per namespace-qualified nameTestQualifiedName— verifies qualified name formattingTestMatchSkillPath— verifies new discovery functionTestExtractSkillInfo— replaces oldTestExtractSkillNamewith namespace coverageTestFilterByRelevance— includes namespace match caseskills-search-namespaced.txtarStacked on
sm/add-skills-command)