cli/internal/skills/hosts/hosts.go
2026-04-15 15:43:43 +02:00

175 lines
4.2 KiB
Go

package hosts
import (
"fmt"
"path/filepath"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghrepo"
)
// Host represents an AI agent that can use skills.
type Host struct {
// ID is the canonical identifier for this host.
ID string
// Name is the human-readable display name.
Name string
// ProjectDir is the relative path within a project for skills.
ProjectDir string
// UserDir is the relative path within the user's home directory for skills.
UserDir string
}
// Scope determines where skills are installed.
type Scope string
const (
ScopeProject Scope = "project"
ScopeUser Scope = "user"
)
// Registry contains all known agent hosts.
var Registry = []Host{
{
ID: "github-copilot",
Name: "GitHub Copilot",
ProjectDir: ".github/skills",
UserDir: ".copilot/skills",
},
{
ID: "claude-code",
Name: "Claude Code",
ProjectDir: ".claude/skills",
UserDir: ".claude/skills",
},
{
ID: "cursor",
Name: "Cursor",
ProjectDir: ".cursor/skills",
UserDir: ".cursor/skills",
},
{
ID: "codex",
Name: "Codex",
ProjectDir: ".agents/skills",
UserDir: ".codex/skills",
},
{
ID: "gemini",
Name: "Gemini CLI",
ProjectDir: ".agent/skills",
UserDir: ".gemini/skills",
},
{
ID: "antigravity",
Name: "Antigravity",
ProjectDir: ".agent/skills",
UserDir: ".gemini/antigravity/skills",
},
}
// FindByID returns the host with the given ID, or an error if not found.
func FindByID(id string) (*Host, error) {
for i := range Registry {
if Registry[i].ID == id {
return &Registry[i], nil
}
}
return nil, fmt.Errorf("unknown host %q, valid hosts: %s", id, ValidHostIDs())
}
// ValidHostIDs returns a comma-separated list of valid host IDs.
func ValidHostIDs() string {
ids := ""
for i, h := range Registry {
if i > 0 {
ids += ", "
}
ids += h.ID
}
return ids
}
// HostIDs returns the IDs of all known hosts as a slice.
func HostIDs() []string {
ids := make([]string, len(Registry))
for i, h := range Registry {
ids[i] = h.ID
}
return ids
}
// HostNames returns the display names of all hosts for prompting.
func HostNames() []string {
names := make([]string, len(Registry))
for i, h := range Registry {
names[i] = h.Name
}
return names
}
// UniqueProjectDirs returns the deduplicated set of project-scope skill
// directories from the Registry, preserving insertion order.
func UniqueProjectDirs() []string {
seen := map[string]bool{}
var dirs []string
for _, h := range Registry {
if !seen[h.ProjectDir] {
seen[h.ProjectDir] = true
dirs = append(dirs, h.ProjectDir)
}
}
return dirs
}
// InstallDir resolves the absolute installation directory for a host and scope.
// For project scope, it uses the provided git root directory so that skills are
// installed at the top level regardless of which subdirectory the user is in.
// Returns an error when gitRoot is empty (not in a git repository).
// For user scope, it uses the home directory.
func (h *Host) InstallDir(scope Scope, gitRoot, homeDir string) (string, error) {
switch scope {
case ScopeProject:
if gitRoot == "" {
return "", fmt.Errorf("could not determine project root directory")
}
return filepath.Join(gitRoot, h.ProjectDir), nil
case ScopeUser:
if homeDir == "" {
return "", fmt.Errorf("could not determine home directory")
}
return filepath.Join(homeDir, h.UserDir), nil
default:
return "", fmt.Errorf("invalid scope %q", scope)
}
}
// ScopeLabels returns the display labels for the scope selection prompt.
// If repoName is non-empty, it is included in the project-scope label
// for additional context.
func ScopeLabels(repoName string) []string {
projectLabel := "Project — install in current repository (recommended)"
if repoName != "" {
projectLabel = fmt.Sprintf("Project — %s (recommended)", repoName)
}
return []string{
projectLabel,
"Global — install in home directory (available everywhere)",
}
}
// RepoNameFromRemote extracts "owner/repo" from a git remote URL.
func RepoNameFromRemote(remote string) string {
if remote == "" {
return ""
}
u, err := git.ParseURL(remote)
if err != nil {
return ""
}
repo, err := ghrepo.FromURL(u)
if err != nil {
return ""
}
return ghrepo.FullName(repo)
}