Skip to content

Commit 07ee722

Browse files
committed
refactor: Improve the installation of skills
1 parent 06b6436 commit 07ee722

5 files changed

Lines changed: 144 additions & 9 deletions

File tree

.infer/shortcuts/skills.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
# Usage:
66
# - /skills list - List discovered skills
7-
# - /skills install <github-url> - Install a skill from a public GitHub repo
7+
# - /skills install <skill | org/skill | github-url> - Install a skill
88
# - /skills uninstall <name> - Uninstall a skill by name
99

1010
shortcuts:
@@ -17,6 +17,6 @@ shortcuts:
1717
- name: list
1818
description: "List discovered skills"
1919
- name: install
20-
description: "Install a skill from a public GitHub repo (usage: <github-url> [--user] [--overwrite])"
20+
description: "Install a skill (usage: <skill> | <org>/<skill> | <github-url>) [--user] [--overwrite]"
2121
- name: uninstall
2222
description: "Uninstall a skill by name (usage: <name> [--user])"

cmd/init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ func createSkillsShortcutsFile(path string) error {
705705
#
706706
# Usage:
707707
# - /skills list - List discovered skills
708-
# - /skills install <github-url> - Install a skill from a public GitHub repo
708+
# - /skills install <skill | org/skill | github-url> - Install a skill
709709
# - /skills uninstall <name> - Uninstall a skill by name
710710
711711
shortcuts:
@@ -718,7 +718,7 @@ shortcuts:
718718
- name: list
719719
description: "List discovered skills"
720720
- name: install
721-
description: "Install a skill from a public GitHub repo (usage: <github-url> [--user] [--overwrite])"
721+
description: "Install a skill (usage: <skill> | <org>/<skill> | <github-url>) [--user] [--overwrite]"
722722
- name: uninstall
723723
description: "Uninstall a skill by name (usage: <name> [--user])"
724724
`

cmd/skills.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,25 @@ func listSkills(cmd *cobra.Command, _ []string) error {
113113
}
114114

115115
var skillsInstallCmd = &cobra.Command{
116-
Use: "install <github-url>",
116+
Use: "install <skill | org/skill | github-url>",
117117
Short: "Install a skill from a public GitHub repository",
118-
Long: `Install a skill folder directly from a public GitHub repository.
118+
Long: `Install a skill folder from a public GitHub repository.
119119
120-
The URL must point at a directory inside a repo, formatted as:
121-
https://github.com/<owner>/<repo>/tree/<ref>/<path-to-skill-folder>
120+
You can pass any of the following:
122121
123-
Example:
122+
- A skill name: skill-creator
123+
→ https://github.com/inference-gateway/skills/tree/main/skills/skill-creator
124+
- An <org>/<skill> pair: acme/skill-creator
125+
→ https://github.com/acme/skills/tree/main/skills/skill-creator
126+
- A full GitHub tree URL: https://github.com/<owner>/<repo>/tree/<ref>/<path>
127+
128+
Shorthand forms assume the skill lives under skills/<name>/ inside a repo
129+
named "skills" on the given org, and resolve against the "main" branch.
130+
For any other layout, branch, or tag, use the full URL form.
131+
132+
Examples:
133+
infer skills install skill-creator
134+
infer skills install acme/internal-comms
124135
infer skills install https://github.com/anthropics/skills/tree/main/skills/pdf
125136
126137
By default the skill is written to .infer/skills/<dirname>/. Pass --user

internal/services/skills/install.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"path"
1212
"path/filepath"
13+
"slices"
1314
"strings"
1415
"time"
1516

@@ -22,8 +23,46 @@ const (
2223
installerTimeout = 30 * time.Second
2324
installerUA = "inference-gateway-cli"
2425
treePartsExpected = 5
26+
27+
defaultSkillsOrg = "inference-gateway"
28+
defaultSkillsRepo = "skills"
29+
defaultSkillsRef = "main"
30+
defaultSkillsSubdir = "skills"
2531
)
2632

33+
// ExpandShorthand turns shorthand install targets into full GitHub
34+
// tree URLs. Accepted forms:
35+
//
36+
// - "<skill>" → https://github.com/inference-gateway/skills/tree/main/skills/<skill>
37+
// - "<org>/<skill>" → https://github.com/<org>/skills/tree/main/skills/<skill>
38+
// - any http(s):// URL → returned unchanged
39+
//
40+
// Anything else (3+ slash-separated segments, empty segments, etc.) is
41+
// returned unchanged so ParseGitHubTreeURL produces its existing error.
42+
func ExpandShorthand(input string) string {
43+
if strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://") {
44+
return input
45+
}
46+
trimmed := strings.Trim(input, "/")
47+
if trimmed == "" {
48+
return input
49+
}
50+
parts := strings.Split(trimmed, "/")
51+
if slices.Contains(parts, "") {
52+
return input
53+
}
54+
switch len(parts) {
55+
case 1:
56+
return fmt.Sprintf("https://github.com/%s/%s/tree/%s/%s/%s",
57+
defaultSkillsOrg, defaultSkillsRepo, defaultSkillsRef, defaultSkillsSubdir, parts[0])
58+
case 2:
59+
return fmt.Sprintf("https://github.com/%s/%s/tree/%s/%s/%s",
60+
parts[0], defaultSkillsRepo, defaultSkillsRef, defaultSkillsSubdir, parts[1])
61+
default:
62+
return input
63+
}
64+
}
65+
2766
// GitHubLocation identifies a directory inside a public GitHub repository,
2867
// parsed out of a /tree/<ref>/<path> URL.
2968
type GitHubLocation struct {
@@ -105,6 +144,7 @@ func NewInstaller() *Installer {
105144
//
106145
// Returns the absolute path of the installed skill on success.
107146
func (i *Installer) InstallFromGitHub(ctx context.Context, rawURL, destBase string, overwrite bool) (string, error) {
147+
rawURL = ExpandShorthand(rawURL)
108148
loc, err := ParseGitHubTreeURL(rawURL)
109149
if err != nil {
110150
return "", err

internal/services/skills/install_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,66 @@ import (
1313
require "github.com/stretchr/testify/require"
1414
)
1515

16+
func TestExpandShorthand(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
input string
20+
want string
21+
}{
22+
{
23+
name: "single segment uses default org and skills repo",
24+
input: "skill-creator",
25+
want: "https://github.com/inference-gateway/skills/tree/main/skills/skill-creator",
26+
},
27+
{
28+
name: "two segments use given org and skills repo",
29+
input: "acme/foo",
30+
want: "https://github.com/acme/skills/tree/main/skills/foo",
31+
},
32+
{
33+
name: "https URL is returned unchanged",
34+
input: "https://github.com/anthropics/skills/tree/main/skills/pdf",
35+
want: "https://github.com/anthropics/skills/tree/main/skills/pdf",
36+
},
37+
{
38+
name: "http URL is returned unchanged",
39+
input: "http://example.com/x",
40+
want: "http://example.com/x",
41+
},
42+
{
43+
name: "empty input is returned unchanged",
44+
input: "",
45+
want: "",
46+
},
47+
{
48+
name: "three segments fall through unchanged",
49+
input: "a/b/c",
50+
want: "a/b/c",
51+
},
52+
{
53+
name: "leading and trailing slashes are trimmed",
54+
input: "/skill/",
55+
want: "https://github.com/inference-gateway/skills/tree/main/skills/skill",
56+
},
57+
{
58+
name: "empty middle segment falls through unchanged",
59+
input: "a//b",
60+
want: "a//b",
61+
},
62+
{
63+
name: "single slash falls through unchanged",
64+
input: "/",
65+
want: "/",
66+
},
67+
}
68+
69+
for _, tt := range tests {
70+
t.Run(tt.name, func(t *testing.T) {
71+
require.Equal(t, tt.want, ExpandShorthand(tt.input))
72+
})
73+
}
74+
}
75+
1676
func TestParseGitHubTreeURL(t *testing.T) {
1777
tests := []struct {
1878
name string
@@ -168,6 +228,30 @@ func TestInstallFromGitHub_HappyPath(t *testing.T) {
168228
require.True(t, os.IsNotExist(err))
169229
}
170230

231+
func TestInstallFromGitHub_ShorthandResolves(t *testing.T) {
232+
// Shorthand "acme/skill-creator" should resolve to the "skills" repo
233+
// under the acme org with path "skills/skill-creator". The mock server
234+
// doesn't care about owner/repo/ref segments — it just looks at the
235+
// repo path prefix — so we need a SKILL.md at
236+
// skills/skill-creator/SKILL.md to match the resolved tree path.
237+
repo := fakeRepo{
238+
Files: map[string]string{
239+
"skills/skill-creator/SKILL.md": validSkillBody("skill-creator", "Test skill."),
240+
},
241+
}
242+
srv := newMockServer(t, repo)
243+
defer srv.Close()
244+
245+
dest := t.TempDir()
246+
got, err := newTestInstaller(srv.URL).InstallFromGitHub(context.Background(),
247+
"acme/skill-creator", dest, false)
248+
require.NoError(t, err)
249+
250+
abs, _ := filepath.Abs(filepath.Join(dest, "skill-creator"))
251+
require.Equal(t, abs, got)
252+
require.FileExists(t, filepath.Join(dest, "skill-creator", "SKILL.md"))
253+
}
254+
171255
func TestInstallFromGitHub_RepoNotFound(t *testing.T) {
172256
mux := http.NewServeMux()
173257
mux.HandleFunc("/repos/", func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)