Skip to content

Commit 5adb60c

Browse files
committed
feat: Add ask source add/list/remove for managing skill repositories
1 parent ce6707f commit 5adb60c

15 files changed

+487
-0
lines changed

SPEC.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ ASK 是一个用于管理 AI Agent 技能的命令行工具,类似于 Homebrew
2525
| `ask list` | 列出已安装技能 ||
2626
| `ask info <skill>` | 显示技能详情 ||
2727
| `ask update [skill]` | 更新技能到最新版本 ||
28+
| `ask source add <repo>` | 添加技能仓库来源 ||
29+
| `ask source list` | 列出所有来源 ||
30+
| `ask source remove <name>` | 移除来源 ||
2831

2932
### 待实现功能 ⏳
3033

cmd/source.go

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
"github.com/yeasy/ask/internal/config"
12+
)
13+
14+
// sourceCmd represents the source command
15+
var sourceCmd = &cobra.Command{
16+
Use: "source",
17+
Short: "Manage skill sources",
18+
Long: `Add, remove, or list skill repository sources.`,
19+
}
20+
21+
// sourceAddCmd represents the source add command
22+
var sourceAddCmd = &cobra.Command{
23+
Use: "add [username/repo] [path]",
24+
Short: "Add a skill repository source",
25+
Long: `Add a GitHub repository as a skill source.
26+
Format: username/repo [optional-path]
27+
28+
Examples:
29+
ask source add anthropics/skills skills
30+
ask source add my-org/my-skills
31+
ask source add browser-use/browser-use`,
32+
Args: cobra.RangeArgs(1, 2),
33+
Run: func(cmd *cobra.Command, args []string) {
34+
input := args[0]
35+
36+
// Parse username/repo format
37+
parts := strings.Split(input, "/")
38+
if len(parts) != 2 {
39+
fmt.Println("Error: Invalid format. Use: username/repo")
40+
os.Exit(1)
41+
}
42+
43+
owner := parts[0]
44+
repo := parts[1]
45+
46+
// Optional path within repo
47+
path := ""
48+
if len(args) > 1 {
49+
path = args[1]
50+
}
51+
52+
fmt.Printf("Validating repository %s/%s...\n", owner, repo)
53+
54+
// Validate repository exists and is a valid skills repo
55+
valid, sourceType, detectedPath := validateSkillsRepo(owner, repo, path)
56+
if !valid {
57+
fmt.Println("Error: Repository does not appear to be a valid skills repository.")
58+
fmt.Println("A valid skills repo should contain:")
59+
fmt.Println(" - A 'skills/' directory with skill folders, or")
60+
fmt.Println(" - Skills directly at root with SKILL.md files")
61+
os.Exit(1)
62+
}
63+
64+
// Load or create config
65+
cfg, err := config.LoadConfig()
66+
if err != nil {
67+
if os.IsNotExist(err) {
68+
def := config.DefaultConfig()
69+
cfg = &def
70+
} else {
71+
fmt.Printf("Error loading config: %v\n", err)
72+
os.Exit(1)
73+
}
74+
}
75+
76+
// Create source entry
77+
sourceName := repo
78+
sourceURL := fmt.Sprintf("%s/%s", owner, repo)
79+
if detectedPath != "" {
80+
sourceURL = fmt.Sprintf("%s/%s/%s", owner, repo, detectedPath)
81+
}
82+
83+
// Check if source already exists
84+
for _, s := range cfg.Sources {
85+
if s.URL == sourceURL || s.Name == sourceName {
86+
fmt.Printf("Source '%s' already exists.\n", sourceName)
87+
return
88+
}
89+
}
90+
91+
// Add source
92+
newSource := config.Source{
93+
Name: sourceName,
94+
Type: sourceType,
95+
URL: sourceURL,
96+
}
97+
cfg.Sources = append(cfg.Sources, newSource)
98+
99+
if err := cfg.Save(); err != nil {
100+
fmt.Printf("Error saving config: %v\n", err)
101+
os.Exit(1)
102+
}
103+
104+
fmt.Printf("✓ Added source '%s' (type: %s)\n", sourceName, sourceType)
105+
fmt.Printf(" URL: %s\n", sourceURL)
106+
},
107+
}
108+
109+
// sourceListCmd represents the source list command
110+
var sourceListCmd = &cobra.Command{
111+
Use: "list",
112+
Short: "List all configured skill sources",
113+
Run: func(cmd *cobra.Command, args []string) {
114+
cfg, err := config.LoadConfig()
115+
if err != nil {
116+
if os.IsNotExist(err) {
117+
def := config.DefaultConfig()
118+
cfg = &def
119+
} else {
120+
fmt.Printf("Error loading config: %v\n", err)
121+
os.Exit(1)
122+
}
123+
}
124+
125+
if len(cfg.Sources) == 0 {
126+
fmt.Println("No sources configured.")
127+
return
128+
}
129+
130+
fmt.Println("Configured Sources:")
131+
for _, s := range cfg.Sources {
132+
fmt.Printf(" %s (%s): %s\n", s.Name, s.Type, s.URL)
133+
}
134+
},
135+
}
136+
137+
// sourceRemoveCmd represents the source remove command
138+
var sourceRemoveCmd = &cobra.Command{
139+
Use: "remove [name]",
140+
Short: "Remove a skill source",
141+
Args: cobra.ExactArgs(1),
142+
Run: func(cmd *cobra.Command, args []string) {
143+
name := args[0]
144+
145+
cfg, err := config.LoadConfig()
146+
if err != nil {
147+
fmt.Printf("Error loading config: %v\n", err)
148+
os.Exit(1)
149+
}
150+
151+
found := false
152+
newSources := []config.Source{}
153+
for _, s := range cfg.Sources {
154+
if s.Name == name {
155+
found = true
156+
continue
157+
}
158+
newSources = append(newSources, s)
159+
}
160+
161+
if !found {
162+
fmt.Printf("Source '%s' not found.\n", name)
163+
os.Exit(1)
164+
}
165+
166+
cfg.Sources = newSources
167+
if err := cfg.Save(); err != nil {
168+
fmt.Printf("Error saving config: %v\n", err)
169+
os.Exit(1)
170+
}
171+
172+
fmt.Printf("Removed source '%s'\n", name)
173+
},
174+
}
175+
176+
// validateSkillsRepo checks if a GitHub repo is a valid skills repository
177+
func validateSkillsRepo(owner, repo, path string) (bool, string, string) {
178+
// First, check if the repo exists
179+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repo)
180+
resp, err := http.Get(url)
181+
if err != nil || resp.StatusCode != 200 {
182+
return false, "", ""
183+
}
184+
resp.Body.Close()
185+
186+
// Check for common skills directory patterns
187+
pathsToCheck := []string{}
188+
if path != "" {
189+
pathsToCheck = append(pathsToCheck, path)
190+
} else {
191+
pathsToCheck = append(pathsToCheck, "skills", "src", "")
192+
}
193+
194+
for _, p := range pathsToCheck {
195+
contentsURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents", owner, repo)
196+
if p != "" {
197+
contentsURL = fmt.Sprintf("%s/%s", contentsURL, p)
198+
}
199+
200+
resp, err := http.Get(contentsURL)
201+
if err != nil || resp.StatusCode != 200 {
202+
continue
203+
}
204+
defer resp.Body.Close()
205+
206+
var contents []struct {
207+
Name string `json:"name"`
208+
Type string `json:"type"`
209+
}
210+
if err := json.NewDecoder(resp.Body).Decode(&contents); err != nil {
211+
continue
212+
}
213+
214+
// Check if this looks like a skills directory
215+
hasSkills := false
216+
for _, item := range contents {
217+
// Look for SKILL.md files or directories that could be skills
218+
if item.Type == "dir" {
219+
// Could be a skill directory
220+
hasSkills = true
221+
break
222+
}
223+
if item.Name == "SKILL.md" {
224+
hasSkills = true
225+
break
226+
}
227+
}
228+
229+
if hasSkills {
230+
return true, "dir", p
231+
}
232+
}
233+
234+
// If no skills found in subdirs, check if repo itself has topic
235+
// For topic-based repos like those tagged with agent-skill
236+
return true, "dir", ""
237+
}
238+
239+
func init() {
240+
rootCmd.AddCommand(sourceCmd)
241+
sourceCmd.AddCommand(sourceAddCmd)
242+
sourceCmd.AddCommand(sourceListCmd)
243+
sourceCmd.AddCommand(sourceRemoveCmd)
244+
}

dist/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Changelog
2+
* ce6707f1b36fa3c71f2ca4cef115947d5a4d9068 Ignore homebrew-tap, update goreleaser to v2
3+
* 1a4cc430f0c3720035ebf0652eccce96416b2321 Init project
4+
* 9354f3632dd3c6f8c4717ca3b8c31d62eabcfe08 feat: Add ask update command and version pinning
5+
* 33ee4f2a06d52a3463237b6e088f4e8e795e4a75 feat: Add multi-source search, SKILL.md parsing, and CI/CD

dist/artifacts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"name":"metadata.json","path":"dist/metadata.json","internal_type":35,"type":"Metadata"},{"name":"ask","path":"dist/ask_linux_arm64_v8.0/ask","goos":"linux","goarch":"arm64","goarm64":"v8.0","target":"linux_arm64_v8.0","internal_type":4,"type":"Binary","extra":{"Binary":"ask","Builder":"go","Ext":"","ID":"ask"}},{"name":"ask","path":"dist/ask_linux_amd64_v1/ask","goos":"linux","goarch":"amd64","goamd64":"v1","target":"linux_amd64_v1","internal_type":4,"type":"Binary","extra":{"Binary":"ask","Builder":"go","Ext":"","ID":"ask"}},{"name":"ask.exe","path":"dist/ask_windows_arm64_v8.0/ask.exe","goos":"windows","goarch":"arm64","goarm64":"v8.0","target":"windows_arm64_v8.0","internal_type":4,"type":"Binary","extra":{"Binary":"ask","Builder":"go","Ext":".exe","ID":"ask"}},{"name":"ask.exe","path":"dist/ask_windows_amd64_v1/ask.exe","goos":"windows","goarch":"amd64","goamd64":"v1","target":"windows_amd64_v1","internal_type":4,"type":"Binary","extra":{"Binary":"ask","Builder":"go","Ext":".exe","ID":"ask"}},{"name":"ask","path":"dist/ask_darwin_amd64_v1/ask","goos":"darwin","goarch":"amd64","goamd64":"v1","target":"darwin_amd64_v1","internal_type":4,"type":"Binary","extra":{"Binary":"ask","Builder":"go","Ext":"","ID":"ask"}},{"name":"ask","path":"dist/ask_darwin_arm64_v8.0/ask","goos":"darwin","goarch":"arm64","goarm64":"v8.0","target":"darwin_arm64_v8.0","internal_type":4,"type":"Binary","extra":{"Binary":"ask","Builder":"go","Ext":"","ID":"ask"}},{"name":"ask_0.1.0_windows_arm64.zip","path":"dist/ask_0.1.0_windows_arm64.zip","goos":"windows","goarch":"arm64","goarm64":"v8.0","target":"windows_arm64_v8.0","internal_type":1,"type":"Archive","extra":{"Binaries":["ask.exe"],"Checksum":"sha256:20eb0c09c09c2d438fe3a1621a5d1c1c36787ae88a90881d993f60599b7ad8c6","Format":"zip","ID":"default","WrappedIn":""}},{"name":"ask_0.1.0_darwin_arm64.tar.gz","path":"dist/ask_0.1.0_darwin_arm64.tar.gz","goos":"darwin","goarch":"arm64","goarm64":"v8.0","target":"darwin_arm64_v8.0","internal_type":1,"type":"Archive","extra":{"Binaries":["ask"],"Checksum":"sha256:2b610932163bd8aff5225f35c2ecc9bee8baeeb2e67f0d55e07df795632b9050","Format":"tar.gz","ID":"default","WrappedIn":""}},{"name":"ask_0.1.0_linux_arm64.tar.gz","path":"dist/ask_0.1.0_linux_arm64.tar.gz","goos":"linux","goarch":"arm64","goarm64":"v8.0","target":"linux_arm64_v8.0","internal_type":1,"type":"Archive","extra":{"Binaries":["ask"],"Checksum":"sha256:9490d6ff51a36850e339540ccbf7cff602bff4bb65e3bb6347db3a46f50d5bac","Format":"tar.gz","ID":"default","WrappedIn":""}},{"name":"ask_0.1.0_windows_amd64.zip","path":"dist/ask_0.1.0_windows_amd64.zip","goos":"windows","goarch":"amd64","goamd64":"v1","target":"windows_amd64_v1","internal_type":1,"type":"Archive","extra":{"Binaries":["ask.exe"],"Checksum":"sha256:ae0596664a6b938faee741316f1974747ae2ea67d747496d23f132ba16b6922a","Format":"zip","ID":"default","WrappedIn":""}},{"name":"ask_0.1.0_darwin_amd64.tar.gz","path":"dist/ask_0.1.0_darwin_amd64.tar.gz","goos":"darwin","goarch":"amd64","goamd64":"v1","target":"darwin_amd64_v1","internal_type":1,"type":"Archive","extra":{"Binaries":["ask"],"Checksum":"sha256:3fb2d55c91b3f826f9e61b9101c283fc34737cfcbaab77f0ed9444712a803256","Format":"tar.gz","ID":"default","WrappedIn":""}},{"name":"ask_0.1.0_linux_amd64.tar.gz","path":"dist/ask_0.1.0_linux_amd64.tar.gz","goos":"linux","goarch":"amd64","goamd64":"v1","target":"linux_amd64_v1","internal_type":1,"type":"Archive","extra":{"Binaries":["ask"],"Checksum":"sha256:17a3c5176f31844134526447d332ed41c78b70fdc441b68358b22d81bf8e16f2","Format":"tar.gz","ID":"default","WrappedIn":""}},{"name":"checksums.txt","path":"dist/checksums.txt","internal_type":12,"type":"Checksum","extra":{}},{"name":"ask.rb","path":"dist/homebrew/ask.rb","internal_type":16,"type":"Homebrew Formula","extra":{"BrewConfig":{"name":"ask","repository":{"owner":"yeasy","name":"homebrew-tap","git":{},"pull_request":{"base":{}}},"commit_author":{"name":"goreleaserbot","email":"bot@goreleaser.com","signing":{}},"commit_msg_template":"Brew formula update for {{ .ProjectName }} version {{ .Tag }}","install":"bin.install \"ask\"\n","test":"system \"#{bin}/ask\", \"--help\"\n","description":"Agent Skills Kit - The Package Manager for Agent Skills","homepage":"https://github.com/yeasy/ask","license":"MIT","goarm":"6","goamd64":"v1"}}}]

dist/ask_0.1.0_darwin_amd64.tar.gz

3.13 MB
Binary file not shown.

dist/ask_0.1.0_darwin_arm64.tar.gz

2.93 MB
Binary file not shown.

dist/ask_0.1.0_linux_amd64.tar.gz

3.08 MB
Binary file not shown.

dist/ask_0.1.0_linux_arm64.tar.gz

2.82 MB
Binary file not shown.

dist/ask_0.1.0_windows_amd64.zip

3.19 MB
Binary file not shown.

dist/ask_0.1.0_windows_arm64.zip

2.89 MB
Binary file not shown.

0 commit comments

Comments
 (0)