175 lines
4.2 KiB
Go
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)
|
|
}
|