Skip to content

Commit e6fa2fa

Browse files
authored
Merge pull request cli#13471 from cli/tommy/skill-install-all-flag
add --all flag to install all skills in a repo
2 parents 5cfb648 + fb67170 commit e6fa2fa

2 files changed

Lines changed: 78 additions & 4 deletions

File tree

pkg/cmd/skills/install/install.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type InstallOptions struct {
5555
ScopeChanged bool // true when --scope was explicitly set
5656
Pin string
5757
Dir string // overrides --agent and --scope
58+
All bool
5859
Force bool
5960
FromLocal bool // treat SkillSource as a local directory path
6061
AllowHiddenDirs bool // include skills in dot-prefixed directories
@@ -135,9 +136,9 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
135136
frontmatter. This metadata identifies the source repository and
136137
enables %[1]sgh skill update%[1]s to detect changes.
137138
138-
When run interactively, the command prompts for any missing arguments.
139-
When run non-interactively, %[1]srepository%[1]s and a skill name are
140-
required.
139+
Use %[1]s--all%[1]s to install every discovered skill from the repository
140+
without prompting for skill selection. When run non-interactively, %[1]srepository%[1]s and either
141+
a skill name or %[1]s--all%[1]s are required.
141142
`, "`", registry.AgentHelpList()),
142143
Example: heredoc.Doc(`
143144
# Interactive: choose repo, skill, and agent
@@ -149,6 +150,9 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
149150
# Install a specific skill
150151
$ gh skill install github/awesome-copilot git-commit
151152
153+
# Install all skills from a repository
154+
$ gh skill install github/awesome-copilot --all
155+
152156
# Install a specific version
153157
$ gh skill install github/awesome-copilot git-commit@v1.2.0
154158
@@ -181,6 +185,10 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
181185
}
182186
opts.ScopeChanged = cmd.Flags().Changed("scope")
183187

188+
if opts.All && opts.SkillName != "" {
189+
return cmdutil.FlagErrorf("cannot use --all with a skill argument")
190+
}
191+
184192
// Resolve the source type early so installRun can branch directly.
185193
if opts.FromLocal {
186194
if opts.SkillSource == "" {
@@ -215,6 +223,7 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
215223
cmdutil.StringEnumFlag(cmd, &opts.Scope, "scope", "", "project", []string{"project", "user"}, "Installation scope")
216224
cmd.Flags().StringVar(&opts.Pin, "pin", "", "Pin to a specific git tag or commit SHA")
217225
cmd.Flags().StringVar(&opts.Dir, "dir", "", "Install to a custom directory (overrides --agent and --scope)")
226+
cmd.Flags().BoolVar(&opts.All, "all", false, "Install all skills without prompting for skill selection")
218227
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Overwrite existing skills without prompting")
219228
cmd.Flags().BoolVar(&opts.FromLocal, "from-local", false, "Treat the argument as a local directory path instead of a repository")
220229
cmd.Flags().BoolVar(&opts.AllowHiddenDirs, "allow-hidden-dirs", false, "Include skills in hidden directories (e.g. .claude/skills/, .agents/skills/)")
@@ -682,6 +691,13 @@ func selectSkillsWithSelector(opts *InstallOptions, skills []discovery.Skill, ca
682691
return nil
683692
}
684693

694+
if opts.All {
695+
if err := checkCollisions(skills); err != nil {
696+
return nil, err
697+
}
698+
return skills, nil
699+
}
700+
685701
if opts.SkillName != "" {
686702
return sel.matchByName(opts, skills)
687703
}

pkg/cmd/skills/install/install_test.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ func TestNewCmdInstall(t *testing.T) {
4545
cli: "monalisa/skills-repo git-commit",
4646
wantOpts: InstallOptions{SkillSource: "monalisa/skills-repo", SkillName: "git-commit", Scope: "project"},
4747
},
48+
{
49+
name: "repo and all flag",
50+
cli: "monalisa/skills-repo --all",
51+
wantOpts: InstallOptions{SkillSource: "monalisa/skills-repo", All: true, Scope: "project"},
52+
},
4853
{
4954
name: "all flags",
5055
cli: "monalisa/skills-repo git-commit --agent github-copilot --scope user --pin v1.0.0 --force",
@@ -77,6 +82,11 @@ func TestNewCmdInstall(t *testing.T) {
7782
cli: "monalisa/skills-repo git-commit@v1.0.0 --pin v2.0.0",
7883
wantErr: true,
7984
},
85+
{
86+
name: "all conflicts with skill name",
87+
cli: "monalisa/skills-repo git-commit --all",
88+
wantErr: true,
89+
},
8090
{
8191
name: "alias add works",
8292
cli: "monalisa/skills-repo git-commit",
@@ -171,6 +181,7 @@ func TestNewCmdInstall(t *testing.T) {
171181
assert.Equal(t, tt.wantOpts.Scope, gotOpts.Scope)
172182
assert.Equal(t, tt.wantOpts.Pin, gotOpts.Pin)
173183
assert.Equal(t, tt.wantOpts.Dir, gotOpts.Dir)
184+
assert.Equal(t, tt.wantOpts.All, gotOpts.All)
174185
assert.Equal(t, tt.wantOpts.Force, gotOpts.Force)
175186
assert.Equal(t, tt.wantOpts.FromLocal, gotOpts.FromLocal)
176187
assert.Equal(t, tt.wantOpts.AllowHiddenDirs, gotOpts.AllowHiddenDirs)
@@ -194,7 +205,7 @@ func TestNewCmdInstall(t *testing.T) {
194205
assert.NotEmpty(t, cmd.Example)
195206
assert.Contains(t, cmd.Aliases, "add")
196207

197-
for _, flag := range []string{"agent", "scope", "pin", "dir", "force"} {
208+
for _, flag := range []string{"agent", "scope", "pin", "dir", "all", "force"} {
198209
assert.NotNil(t, cmd.Flags().Lookup(flag), "missing flag: --%s", flag)
199210
}
200211
})
@@ -265,6 +276,14 @@ var gitCommitContent = heredoc.Doc(`
265276
# Git Commit
266277
`)
267278

279+
var codeReviewContent = heredoc.Doc(`
280+
---
281+
name: code-review
282+
description: Reviews code
283+
---
284+
# Code Review
285+
`)
286+
268287
// singleSkillTreeJSON returns tree entries for a single skill with the given name.
269288
func singleSkillTreeJSON(name, treeSHA, blobSHA string) string {
270289
return fmt.Sprintf(
@@ -1529,6 +1548,45 @@ func TestInstallRun(t *testing.T) {
15291548
}
15301549
}
15311550

1551+
func TestInstallRun_AllInstallsRemoteSkills(t *testing.T) {
1552+
homeDir := t.TempDir()
1553+
t.Setenv("HOME", homeDir)
1554+
t.Setenv("USERPROFILE", homeDir)
1555+
1556+
reg := &httpmock.Registry{}
1557+
defer reg.Verify(t)
1558+
1559+
stubResolveVersion(reg, "monalisa", "skills-repo", "v1.0.0", "abc123")
1560+
stubDiscoverTree(reg, "monalisa", "skills-repo", "abc123",
1561+
singleSkillTreeJSON("code-review", "tree-cr", "blob-cr")+", "+
1562+
singleSkillTreeJSON("git-commit", "tree-gc", "blob-gc"))
1563+
stubInstallFiles(reg, "monalisa", "skills-repo", "tree-cr", "blob-cr", codeReviewContent)
1564+
stubInstallFiles(reg, "monalisa", "skills-repo", "tree-gc", "blob-gc", gitCommitContent)
1565+
1566+
ios, _, stdout, stderr := iostreams.Test()
1567+
targetDir := t.TempDir()
1568+
1569+
err := installRun(&InstallOptions{
1570+
IO: ios,
1571+
HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil },
1572+
GitClient: &git.Client{RepoDir: t.TempDir()},
1573+
SkillSource: "monalisa/skills-repo",
1574+
All: true,
1575+
Force: true,
1576+
Agent: "github-copilot",
1577+
Scope: "project",
1578+
ScopeChanged: true,
1579+
Dir: targetDir,
1580+
Telemetry: &telemetry.NoOpService{},
1581+
})
1582+
require.NoError(t, err)
1583+
assert.Contains(t, stdout.String(), "Installed code-review")
1584+
assert.Contains(t, stdout.String(), "Installed git-commit")
1585+
assert.NotContains(t, stderr.String(), "must specify a skill name")
1586+
require.FileExists(t, filepath.Join(targetDir, "code-review", "SKILL.md"))
1587+
require.FileExists(t, filepath.Join(targetDir, "git-commit", "SKILL.md"))
1588+
}
1589+
15321590
func TestInstallProgress(t *testing.T) {
15331591
ios, _, _, _ := iostreams.Test()
15341592

0 commit comments

Comments
 (0)