Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions internal/skills/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ func HasHiddenDirSkills(skills []Skill) bool {
return false
}

// HiddenDirFilterResult holds the outcome of partitioning skills into standard
// and hidden-dir buckets.
type HiddenDirFilterResult struct {
Standard []Skill
HiddenCount int
}

// PartitionHiddenDirSkills splits skills into standard and hidden-dir groups.
func PartitionHiddenDirSkills(skills []Skill) HiddenDirFilterResult {
var r HiddenDirFilterResult
Comment on lines +104 to +113
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HiddenDirFilterResult is returned by PartitionHiddenDirSkills, but the name suggests a filtering operation rather than a partition. Renaming the type to something like HiddenDirPartitionResult (or similar) would better match the function name and reduce confusion for callers.

Suggested change
// HiddenDirFilterResult holds the outcome of partitioning skills into standard
// and hidden-dir buckets.
type HiddenDirFilterResult struct {
Standard []Skill
HiddenCount int
}
// PartitionHiddenDirSkills splits skills into standard and hidden-dir groups.
func PartitionHiddenDirSkills(skills []Skill) HiddenDirFilterResult {
var r HiddenDirFilterResult
// HiddenDirPartitionResult holds the outcome of partitioning skills into standard
// and hidden-dir buckets.
type HiddenDirPartitionResult struct {
Standard []Skill
HiddenCount int
}
// PartitionHiddenDirSkills splits skills into standard and hidden-dir groups.
func PartitionHiddenDirSkills(skills []Skill) HiddenDirPartitionResult {
var r HiddenDirPartitionResult

Copilot uses AI. Check for mistakes.
for _, s := range skills {
if s.IsHiddenDirConvention() {
r.HiddenCount++
} else {
r.Standard = append(r.Standard, s)
}
}
return r
}

// ResolvedRef contains the resolved git reference and its SHA.
type ResolvedRef struct {
Ref string // fully qualified ref (refs/heads/*, refs/tags/*) or commit SHA
Expand Down
42 changes: 19 additions & 23 deletions pkg/cmd/skills/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ func installRun(opts *InstallOptions) error {
}

printFileTree(opts.IO.ErrOut, cs, result.Dir, result.Installed)
printReviewHint(opts.IO.ErrOut, cs, repoSource, resolved.SHA, result.Installed)
printReviewHint(opts.IO.ErrOut, cs, repoSource, resolved.SHA, result.Installed, opts.AllowHiddenDirs)
printHostHints(opts.IO.ErrOut, cs, plan.hosts, result.Installed, result.Dir, gitRoot)
}

Expand Down Expand Up @@ -536,7 +536,7 @@ func runLocalInstall(opts *InstallOptions) error {
}

printFileTree(opts.IO.ErrOut, cs, result.Dir, result.Installed)
printReviewHint(opts.IO.ErrOut, cs, "", "", result.Installed)
printReviewHint(opts.IO.ErrOut, cs, "", "", result.Installed, false)
printHostHints(opts.IO.ErrOut, cs, plan.hosts, result.Installed, result.Dir, gitRoot)
}

Expand Down Expand Up @@ -1118,8 +1118,10 @@ func printPreInstallDisclaimer(w io.Writer, cs *iostreams.ColorScheme) {

// printReviewHint warns the user to review installed skills and suggests preview commands.
// When sha is non-empty the suggested commands include @SHA so the user previews
// exactly the version that was installed.
func printReviewHint(w io.Writer, cs *iostreams.ColorScheme, repo, sha string, skillNames []string) {
// exactly the version that was installed. When allowHiddenDirs is true, the
// suggested commands include --allow-hidden-dirs so previewing hidden-dir
// skills works without an extra manual step.
func printReviewHint(w io.Writer, cs *iostreams.ColorScheme, repo, sha string, skillNames []string, allowHiddenDirs bool) {
if len(skillNames) == 0 {
return
}
Expand All @@ -1130,11 +1132,15 @@ func printReviewHint(w io.Writer, cs *iostreams.ColorScheme, repo, sha string, s
}
fmt.Fprintln(w, " Review installed content before use:")
fmt.Fprintln(w)
hiddenFlag := ""
if allowHiddenDirs {
hiddenFlag = " --allow-hidden-dirs"
}
for _, name := range skillNames {
if sha != "" {
fmt.Fprintf(w, " gh skill preview %s %s@%s\n", repo, name, sha)
fmt.Fprintf(w, " gh skill preview %s %s@%s%s\n", repo, name, sha, hiddenFlag)
} else {
fmt.Fprintf(w, " gh skill preview %s %s\n", repo, name)
fmt.Fprintf(w, " gh skill preview %s %s%s\n", repo, name, hiddenFlag)
}
}
fmt.Fprintln(w)
Expand Down Expand Up @@ -1180,10 +1186,9 @@ func kiroResourcePath(installDir, gitRoot string) string {
return filepath.ToSlash(installDir)
}

// filterHiddenDirSkills separates hidden-dir skills from the full list and
// applies the --allow-hidden-dirs flag logic. When the flag is set, all skills
// are returned and a warning is printed. When the flag is not set, hidden-dir
// skills are excluded and an error is returned if no standard skills remain.
// filterHiddenDirSkills applies the --allow-hidden-dirs flag logic. When the
// flag is set, all skills are returned with a warning. Otherwise, hidden-dir
// skills are excluded with an error if no standard skills remain.
func filterHiddenDirSkills(opts *InstallOptions, allSkills []discovery.Skill) ([]discovery.Skill, error) {
cs := opts.IO.ColorScheme()

Expand All @@ -1198,25 +1203,16 @@ func filterHiddenDirSkills(opts *InstallOptions, allSkills []discovery.Skill) ([
return allSkills, nil
}

var standard []discovery.Skill
var hiddenCount int
for _, s := range allSkills {
if s.IsHiddenDirConvention() {
hiddenCount++
} else {
standard = append(standard, s)
}
}

if len(standard) == 0 && hiddenCount > 0 {
r := discovery.PartitionHiddenDirSkills(allSkills)
if len(r.Standard) == 0 && r.HiddenCount > 0 {
return nil, fmt.Errorf(
"no standard skills found, but %d skill(s) exist in hidden directories\n"+
" Use --allow-hidden-dirs to include them",
hiddenCount,
r.HiddenCount,
)
}

return standard, nil
return r.Standard, nil
}

// checkUpstreamProvenance fetches the skill's SKILL.md via the contents API
Expand Down
29 changes: 23 additions & 6 deletions pkg/cmd/skills/install/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2141,11 +2141,12 @@ func Test_isSkillPath(t *testing.T) {

func Test_printReviewHint(t *testing.T) {
tests := []struct {
name string
repo string
sha string
skillNames []string
wantOutput string
name string
repo string
sha string
skillNames []string
allowHiddenDirs bool
wantOutput string
}{
{
name: "remote install with SHA includes SHA in preview command",
Expand Down Expand Up @@ -2182,14 +2183,30 @@ func Test_printReviewHint(t *testing.T) {
skillNames: []string{},
wantOutput: "",
},
{
name: "allow-hidden-dirs appends flag to preview command",
repo: "owner/repo",
sha: "abc123",
skillNames: []string{"hidden-skill"},
allowHiddenDirs: true,
wantOutput: "gh skill preview owner/repo hidden-skill@abc123 --allow-hidden-dirs",
},
{
name: "allow-hidden-dirs without SHA",
repo: "owner/repo",
sha: "",
skillNames: []string{"hidden-skill"},
allowHiddenDirs: true,
wantOutput: "gh skill preview owner/repo hidden-skill --allow-hidden-dirs",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
cs := ios.ColorScheme()
var buf strings.Builder
printReviewHint(&buf, cs, tt.repo, tt.sha, tt.skillNames)
printReviewHint(&buf, cs, tt.repo, tt.sha, tt.skillNames, tt.allowHiddenDirs)
if tt.wantOutput == "" {
assert.Empty(t, buf.String())
} else {
Expand Down
49 changes: 45 additions & 4 deletions pkg/cmd/skills/preview/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ type PreviewOptions struct {
ExecutablePath string
RenderFile func(string, string) string

RepoArg string
SkillName string
Version string // resolved from @suffix on SkillName
RepoArg string
SkillName string
Version string // resolved from @suffix on SkillName
AllowHiddenDirs bool // include skills in dot-prefixed directories

repo ghrepo.Interface
}
Expand Down Expand Up @@ -110,6 +111,8 @@ func NewCmdPreview(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
},
}

cmd.Flags().BoolVar(&opts.AllowHiddenDirs, "allow-hidden-dirs", false, "Include skills in hidden directories (e.g. .claude/skills/, .agents/skills/)")

return cmd
}

Expand Down Expand Up @@ -151,12 +154,17 @@ func previewRun(opts *PreviewOptions) error {
}

opts.IO.StartProgressIndicatorWithLabel("Discovering skills")
skills, err := discovery.DiscoverSkills(apiClient, hostname, owner, repoName, resolved.SHA)
allSkills, err := discovery.DiscoverSkillsWithOptions(apiClient, hostname, owner, repoName, resolved.SHA, discovery.DiscoverOptions{})
opts.IO.StopProgressIndicator()
if err != nil {
return err
}

skills, err := filterHiddenDirSkills(opts, allSkills)
if err != nil {
return err
}

sort.Slice(skills, func(i, j int) bool {
return skills[i].DisplayName() < skills[j].DisplayName()
})
Expand Down Expand Up @@ -388,6 +396,39 @@ func isMarkdownFile(filePath string) bool {
}
}

// filterHiddenDirSkills applies the --allow-hidden-dirs flag logic. When the
// flag is set, all skills are returned with a warning. Otherwise, hidden-dir
// skills are excluded with a hint or error.
func filterHiddenDirSkills(opts *PreviewOptions, allSkills []discovery.Skill) ([]discovery.Skill, error) {
cs := opts.IO.ColorScheme()

if opts.AllowHiddenDirs {
if discovery.HasHiddenDirSkills(allSkills) {
fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(`
%[1]s Skills in hidden directories (e.g. .claude/, .agents/) may be installed
copies from another publisher. Verify the skill's origin and check for a
Comment on lines +408 to +409
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning text reads a bit ungrammatically: "may be installed copies from another publisher". Consider rephrasing to something like "may be copies installed from another publisher" or "may be installed as copies from another publisher" to make the message clearer.

Suggested change
%[1]s Skills in hidden directories (e.g. .claude/, .agents/) may be installed
copies from another publisher. Verify the skill's origin and check for a
%[1]s Skills in hidden directories (e.g. .claude/, .agents/) may be copies
installed from another publisher. Verify the skill's origin and check for a

Copilot uses AI. Check for mistakes.
canonical source.
`, cs.WarningIcon()))
}
return allSkills, nil
}

r := discovery.PartitionHiddenDirSkills(allSkills)
if r.HiddenCount > 0 {
if len(r.Standard) == 0 {
return nil, fmt.Errorf(
"no standard skills found, but %d skill(s) exist in hidden directories\n"+
" Use --allow-hidden-dirs to include them",
r.HiddenCount,
)
}
fmt.Fprintf(opts.IO.ErrOut, "%s %d skill(s) in hidden directories were excluded, use --%s to include them\n",
cs.Yellow("!"), r.HiddenCount, "allow-hidden-dirs")
}

return r.Standard, nil
}

func selectSkill(opts *PreviewOptions, skills []discovery.Skill) (discovery.Skill, error) {
if opts.SkillName != "" {
for _, s := range skills {
Expand Down
Loading
Loading