Skip to content

Commit 5a121bf

Browse files
Merge pull request cli#13265 from SamMorrowDrums/sammorrowdrums/preview-allow-hidden-dirs-flag
feat(skills): add --allow-hidden-dirs flag to preview command
2 parents 352a00e + d961de4 commit 5a121bf

5 files changed

Lines changed: 364 additions & 39 deletions

File tree

internal/skills/discovery/discovery.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,26 @@ func HasHiddenDirSkills(skills []Skill) bool {
101101
return false
102102
}
103103

104+
// HiddenDirFilterResult holds the outcome of partitioning skills into standard
105+
// and hidden-dir buckets.
106+
type HiddenDirFilterResult struct {
107+
Standard []Skill
108+
HiddenCount int
109+
}
110+
111+
// PartitionHiddenDirSkills splits skills into standard and hidden-dir groups.
112+
func PartitionHiddenDirSkills(skills []Skill) HiddenDirFilterResult {
113+
var r HiddenDirFilterResult
114+
for _, s := range skills {
115+
if s.IsHiddenDirConvention() {
116+
r.HiddenCount++
117+
} else {
118+
r.Standard = append(r.Standard, s)
119+
}
120+
}
121+
return r
122+
}
123+
104124
// ResolvedRef contains the resolved git reference and its SHA.
105125
type ResolvedRef struct {
106126
Ref string // fully qualified ref (refs/heads/*, refs/tags/*) or commit SHA

pkg/cmd/skills/install/install.go

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ func installRun(opts *InstallOptions) error {
391391
}
392392

393393
printFileTree(opts.IO.ErrOut, cs, result.Dir, result.Installed)
394-
printReviewHint(opts.IO.ErrOut, cs, repoSource, resolved.SHA, result.Installed)
394+
printReviewHint(opts.IO.ErrOut, cs, repoSource, resolved.SHA, result.Installed, opts.AllowHiddenDirs)
395395
printHostHints(opts.IO.ErrOut, cs, plan.hosts, result.Installed, result.Dir, gitRoot)
396396
}
397397

@@ -536,7 +536,7 @@ func runLocalInstall(opts *InstallOptions) error {
536536
}
537537

538538
printFileTree(opts.IO.ErrOut, cs, result.Dir, result.Installed)
539-
printReviewHint(opts.IO.ErrOut, cs, "", "", result.Installed)
539+
printReviewHint(opts.IO.ErrOut, cs, "", "", result.Installed, false)
540540
printHostHints(opts.IO.ErrOut, cs, plan.hosts, result.Installed, result.Dir, gitRoot)
541541
}
542542

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

11191119
// printReviewHint warns the user to review installed skills and suggests preview commands.
11201120
// When sha is non-empty the suggested commands include @SHA so the user previews
1121-
// exactly the version that was installed.
1122-
func printReviewHint(w io.Writer, cs *iostreams.ColorScheme, repo, sha string, skillNames []string) {
1121+
// exactly the version that was installed. When allowHiddenDirs is true, the
1122+
// suggested commands include --allow-hidden-dirs so previewing hidden-dir
1123+
// skills works without an extra manual step.
1124+
func printReviewHint(w io.Writer, cs *iostreams.ColorScheme, repo, sha string, skillNames []string, allowHiddenDirs bool) {
11231125
if len(skillNames) == 0 {
11241126
return
11251127
}
@@ -1130,11 +1132,15 @@ func printReviewHint(w io.Writer, cs *iostreams.ColorScheme, repo, sha string, s
11301132
}
11311133
fmt.Fprintln(w, " Review installed content before use:")
11321134
fmt.Fprintln(w)
1135+
hiddenFlag := ""
1136+
if allowHiddenDirs {
1137+
hiddenFlag = " --allow-hidden-dirs"
1138+
}
11331139
for _, name := range skillNames {
11341140
if sha != "" {
1135-
fmt.Fprintf(w, " gh skill preview %s %s@%s\n", repo, name, sha)
1141+
fmt.Fprintf(w, " gh skill preview %s %s@%s%s\n", repo, name, sha, hiddenFlag)
11361142
} else {
1137-
fmt.Fprintf(w, " gh skill preview %s %s\n", repo, name)
1143+
fmt.Fprintf(w, " gh skill preview %s %s%s\n", repo, name, hiddenFlag)
11381144
}
11391145
}
11401146
fmt.Fprintln(w)
@@ -1180,10 +1186,9 @@ func kiroResourcePath(installDir, gitRoot string) string {
11801186
return filepath.ToSlash(installDir)
11811187
}
11821188

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

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

1201-
var standard []discovery.Skill
1202-
var hiddenCount int
1203-
for _, s := range allSkills {
1204-
if s.IsHiddenDirConvention() {
1205-
hiddenCount++
1206-
} else {
1207-
standard = append(standard, s)
1208-
}
1209-
}
1210-
1211-
if len(standard) == 0 && hiddenCount > 0 {
1206+
r := discovery.PartitionHiddenDirSkills(allSkills)
1207+
if len(r.Standard) == 0 && r.HiddenCount > 0 {
12121208
return nil, fmt.Errorf(
12131209
"no standard skills found, but %d skill(s) exist in hidden directories\n"+
12141210
" Use --allow-hidden-dirs to include them",
1215-
hiddenCount,
1211+
r.HiddenCount,
12161212
)
12171213
}
12181214

1219-
return standard, nil
1215+
return r.Standard, nil
12201216
}
12211217

12221218
// checkUpstreamProvenance fetches the skill's SKILL.md via the contents API

pkg/cmd/skills/install/install_test.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2141,11 +2141,12 @@ func Test_isSkillPath(t *testing.T) {
21412141

21422142
func Test_printReviewHint(t *testing.T) {
21432143
tests := []struct {
2144-
name string
2145-
repo string
2146-
sha string
2147-
skillNames []string
2148-
wantOutput string
2144+
name string
2145+
repo string
2146+
sha string
2147+
skillNames []string
2148+
allowHiddenDirs bool
2149+
wantOutput string
21492150
}{
21502151
{
21512152
name: "remote install with SHA includes SHA in preview command",
@@ -2182,14 +2183,30 @@ func Test_printReviewHint(t *testing.T) {
21822183
skillNames: []string{},
21832184
wantOutput: "",
21842185
},
2186+
{
2187+
name: "allow-hidden-dirs appends flag to preview command",
2188+
repo: "owner/repo",
2189+
sha: "abc123",
2190+
skillNames: []string{"hidden-skill"},
2191+
allowHiddenDirs: true,
2192+
wantOutput: "gh skill preview owner/repo hidden-skill@abc123 --allow-hidden-dirs",
2193+
},
2194+
{
2195+
name: "allow-hidden-dirs without SHA",
2196+
repo: "owner/repo",
2197+
sha: "",
2198+
skillNames: []string{"hidden-skill"},
2199+
allowHiddenDirs: true,
2200+
wantOutput: "gh skill preview owner/repo hidden-skill --allow-hidden-dirs",
2201+
},
21852202
}
21862203

21872204
for _, tt := range tests {
21882205
t.Run(tt.name, func(t *testing.T) {
21892206
ios, _, _, _ := iostreams.Test()
21902207
cs := ios.ColorScheme()
21912208
var buf strings.Builder
2192-
printReviewHint(&buf, cs, tt.repo, tt.sha, tt.skillNames)
2209+
printReviewHint(&buf, cs, tt.repo, tt.sha, tt.skillNames, tt.allowHiddenDirs)
21932210
if tt.wantOutput == "" {
21942211
assert.Empty(t, buf.String())
21952212
} else {

pkg/cmd/skills/preview/preview.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ type PreviewOptions struct {
3232
ExecutablePath string
3333
RenderFile func(string, string) string
3434

35-
RepoArg string
36-
SkillName string
37-
Version string // resolved from @suffix on SkillName
35+
RepoArg string
36+
SkillName string
37+
Version string // resolved from @suffix on SkillName
38+
AllowHiddenDirs bool // include skills in dot-prefixed directories
3839

3940
repo ghrepo.Interface
4041
}
@@ -110,6 +111,8 @@ func NewCmdPreview(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
110111
},
111112
}
112113

114+
cmd.Flags().BoolVar(&opts.AllowHiddenDirs, "allow-hidden-dirs", false, "Include skills in hidden directories (e.g. .claude/skills/, .agents/skills/)")
115+
113116
return cmd
114117
}
115118

@@ -151,12 +154,17 @@ func previewRun(opts *PreviewOptions) error {
151154
}
152155

153156
opts.IO.StartProgressIndicatorWithLabel("Discovering skills")
154-
skills, err := discovery.DiscoverSkills(apiClient, hostname, owner, repoName, resolved.SHA)
157+
allSkills, err := discovery.DiscoverSkillsWithOptions(apiClient, hostname, owner, repoName, resolved.SHA, discovery.DiscoverOptions{})
155158
opts.IO.StopProgressIndicator()
156159
if err != nil {
157160
return err
158161
}
159162

163+
skills, err := filterHiddenDirSkills(opts, allSkills)
164+
if err != nil {
165+
return err
166+
}
167+
160168
sort.Slice(skills, func(i, j int) bool {
161169
return skills[i].DisplayName() < skills[j].DisplayName()
162170
})
@@ -388,6 +396,39 @@ func isMarkdownFile(filePath string) bool {
388396
}
389397
}
390398

399+
// filterHiddenDirSkills applies the --allow-hidden-dirs flag logic. When the
400+
// flag is set, all skills are returned with a warning. Otherwise, hidden-dir
401+
// skills are excluded with a hint or error.
402+
func filterHiddenDirSkills(opts *PreviewOptions, allSkills []discovery.Skill) ([]discovery.Skill, error) {
403+
cs := opts.IO.ColorScheme()
404+
405+
if opts.AllowHiddenDirs {
406+
if discovery.HasHiddenDirSkills(allSkills) {
407+
fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(`
408+
%[1]s Skills in hidden directories (e.g. .claude/, .agents/) may be installed
409+
copies from another publisher. Verify the skill's origin and check for a
410+
canonical source.
411+
`, cs.WarningIcon()))
412+
}
413+
return allSkills, nil
414+
}
415+
416+
r := discovery.PartitionHiddenDirSkills(allSkills)
417+
if r.HiddenCount > 0 {
418+
if len(r.Standard) == 0 {
419+
return nil, fmt.Errorf(
420+
"no standard skills found, but %d skill(s) exist in hidden directories\n"+
421+
" Use --allow-hidden-dirs to include them",
422+
r.HiddenCount,
423+
)
424+
}
425+
fmt.Fprintf(opts.IO.ErrOut, "%s %d skill(s) in hidden directories were excluded, use --%s to include them\n",
426+
cs.Yellow("!"), r.HiddenCount, "allow-hidden-dirs")
427+
}
428+
429+
return r.Standard, nil
430+
}
431+
391432
func selectSkill(opts *PreviewOptions, skills []discovery.Skill) (discovery.Skill, error) {
392433
if opts.SkillName != "" {
393434
for _, s := range skills {

0 commit comments

Comments
 (0)