aboutsummaryrefslogtreecommitdiffstats
path: root/source.go
diff options
context:
space:
mode:
authorSimen A. W. Olsen <hello@simenandre.no>2026-10-25 12:41:28 +0100
committerSimen A. W. Olsen <hello@simenandre.no>2026-10-25 12:41:28 +0100
commitc596038adc7cdbd64988e61fee856d533b58c25d (patch)
treea02fde25835416d143ff56b807e640f6a7e9cf82 /source.go
downloadskills-main.tar.gz
skills-main.zip
feat: skills, a package manager for agent skillsHEADv0.1.0main
Diffstat (limited to 'source.go')
-rw-r--r--source.go80
1 files changed, 80 insertions, 0 deletions
diff --git a/source.go b/source.go
new file mode 100644
index 0000000..577d00a
--- /dev/null
+++ b/source.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+// Spec is a parsed source as the user wrote it: a git URL and an optional ref.
+type Spec struct {
+ URL string
+ Ref string
+}
+
+var (
+ ghShorthand = regexp.MustCompile(`^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$`)
+ scpLike = regexp.MustCompile(`^([A-Za-z0-9_.-]+@)?([A-Za-z0-9_.-]+):([^/].*)$`)
+ fullRev = regexp.MustCompile(`^[0-9a-f]{40}$`)
+)
+
+// parseSpec turns what a user typed into a git URL. It accepts:
+//
+// owner/repo, github:owner/repo GitHub
+// https://…, ssh://…, git://…, file://…
+// user@host:path scp-like, as git understands it
+// ./path, /path, ~/path a local repository
+//
+// each optionally followed by #ref (a branch, tag or commit).
+func parseSpec(s string) (Spec, error) {
+ raw, ref, _ := strings.Cut(s, "#")
+ if raw == "" {
+ return Spec{}, fmt.Errorf("empty source %q", s)
+ }
+ var url string
+ switch {
+ case strings.HasPrefix(raw, "github:"):
+ url = "https://github.com/" + strings.TrimPrefix(raw, "github:")
+ case strings.Contains(raw, "://"):
+ // Accept Nix's git+https:// spelling too.
+ url = strings.TrimPrefix(raw, "git+")
+ case strings.HasPrefix(raw, "/"), strings.HasPrefix(raw, "."), strings.HasPrefix(raw, "~"):
+ p := raw
+ if strings.HasPrefix(p, "~") {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return Spec{}, err
+ }
+ p = filepath.Join(home, strings.TrimPrefix(p, "~"))
+ }
+ abs, err := filepath.Abs(p)
+ if err != nil {
+ return Spec{}, err
+ }
+ url = "file://" + abs
+ case scpLike.MatchString(raw):
+ // Nix only takes URLs, and the lock is shared with it.
+ m := scpLike.FindStringSubmatch(raw)
+ url = "ssh://" + m[1] + m[2] + "/" + m[3]
+ case ghShorthand.MatchString(raw):
+ url = "https://github.com/" + raw
+ default:
+ return Spec{}, fmt.Errorf("%q is not a source; use owner/repo, a git URL or a path", s)
+ }
+ return Spec{URL: strings.TrimSuffix(url, "/"), Ref: ref}, nil
+}
+
+// defaultSourceName names a source after its only skill, or else after its
+// repository.
+func defaultSourceName(spec Spec, selected []Skill) string {
+ if len(selected) == 1 {
+ return selected[0].Name
+ }
+ u := spec.URL
+ if i := strings.LastIndexAny(u, "/:"); i >= 0 {
+ u = u[i+1:]
+ }
+ return strings.TrimSuffix(u, ".git")
+}