Add support for installation in multiple agent hosts in gh skills install (#13209)
* add support for installation in multiple agent host * print correct dir in warning * remove dir as it depends on user vs project installation scope * Move comment closer to assertion in registry test Move the explanatory comment from above the map initialization to directly above the assertions it describes, per review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * List supported agent names and IDs in help text Replace the self-referencing "run --help" sentence with an inline list of all supported --agent values showing Name (id) pairs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use heredoc.Docf for Kiro CLI post-install hint Replace individual fmt.Fprintln calls with a single heredoc.Docf block for the Kiro CLI post-install guidance, per review feedback. Also shorten the --agent flag usage line by overriding the auto-generated enum list with a reference to the supported values in the help text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
998b6212b3
commit
082f15a8fd
5 changed files with 434 additions and 21 deletions
|
|
@ -34,7 +34,16 @@ const (
|
|||
)
|
||||
|
||||
// Agents contains all known agent hosts.
|
||||
//
|
||||
// The slice is ordered so that the most widely used agents appear first,
|
||||
// followed by the rest in alphabetical order. This order is used for
|
||||
// interactive selection, help output, and flag enum suggestions.
|
||||
//
|
||||
// Agents sharing a ProjectDir (such as the shared .agents/skills directory)
|
||||
// install skills to the same project-scope location, so selecting multiple
|
||||
// such agents writes each skill only once.
|
||||
var Agents = []AgentHost{
|
||||
// Popular agents, listed first for discoverability.
|
||||
{
|
||||
ID: "github-copilot",
|
||||
Name: "GitHub Copilot",
|
||||
|
|
@ -60,7 +69,7 @@ var Agents = []AgentHost{
|
|||
UserDir: ".codex/skills",
|
||||
},
|
||||
{
|
||||
ID: "gemini",
|
||||
ID: "gemini-cli",
|
||||
Name: "Gemini CLI",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".gemini/skills",
|
||||
|
|
@ -71,6 +80,242 @@ var Agents = []AgentHost{
|
|||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".gemini/antigravity/skills",
|
||||
},
|
||||
|
||||
// All other supported agents, alphabetical by ID.
|
||||
{
|
||||
ID: "adal",
|
||||
Name: "AdaL",
|
||||
ProjectDir: ".adal/skills",
|
||||
UserDir: ".adal/skills",
|
||||
},
|
||||
{
|
||||
ID: "amp",
|
||||
Name: "Amp",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".config/agents/skills",
|
||||
},
|
||||
{
|
||||
ID: "augment",
|
||||
Name: "Augment",
|
||||
ProjectDir: ".augment/skills",
|
||||
UserDir: ".augment/skills",
|
||||
},
|
||||
{
|
||||
ID: "bob",
|
||||
Name: "IBM Bob",
|
||||
ProjectDir: ".bob/skills",
|
||||
UserDir: ".bob/skills",
|
||||
},
|
||||
{
|
||||
ID: "cline",
|
||||
Name: "Cline",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".agents/skills",
|
||||
},
|
||||
{
|
||||
ID: "codebuddy",
|
||||
Name: "CodeBuddy",
|
||||
ProjectDir: ".codebuddy/skills",
|
||||
UserDir: ".codebuddy/skills",
|
||||
},
|
||||
{
|
||||
ID: "command-code",
|
||||
Name: "Command Code",
|
||||
ProjectDir: ".commandcode/skills",
|
||||
UserDir: ".commandcode/skills",
|
||||
},
|
||||
{
|
||||
ID: "continue",
|
||||
Name: "Continue",
|
||||
ProjectDir: ".continue/skills",
|
||||
UserDir: ".continue/skills",
|
||||
},
|
||||
{
|
||||
ID: "cortex",
|
||||
Name: "Cortex Code",
|
||||
ProjectDir: ".cortex/skills",
|
||||
UserDir: ".snowflake/cortex/skills",
|
||||
},
|
||||
{
|
||||
ID: "crush",
|
||||
Name: "Crush",
|
||||
ProjectDir: ".crush/skills",
|
||||
UserDir: ".config/crush/skills",
|
||||
},
|
||||
{
|
||||
ID: "deepagents",
|
||||
Name: "Deep Agents",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".deepagents/agent/skills",
|
||||
},
|
||||
{
|
||||
ID: "droid",
|
||||
Name: "Droid",
|
||||
ProjectDir: ".factory/skills",
|
||||
UserDir: ".factory/skills",
|
||||
},
|
||||
{
|
||||
ID: "firebender",
|
||||
Name: "Firebender",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".firebender/skills",
|
||||
},
|
||||
{
|
||||
ID: "goose",
|
||||
Name: "Goose",
|
||||
ProjectDir: ".goose/skills",
|
||||
UserDir: ".config/goose/skills",
|
||||
},
|
||||
{
|
||||
ID: "iflow-cli",
|
||||
Name: "iFlow CLI",
|
||||
ProjectDir: ".iflow/skills",
|
||||
UserDir: ".iflow/skills",
|
||||
},
|
||||
{
|
||||
ID: "junie",
|
||||
Name: "Junie",
|
||||
ProjectDir: ".junie/skills",
|
||||
UserDir: ".junie/skills",
|
||||
},
|
||||
{
|
||||
ID: "kilo",
|
||||
Name: "Kilo Code",
|
||||
ProjectDir: ".kilocode/skills",
|
||||
UserDir: ".kilocode/skills",
|
||||
},
|
||||
{
|
||||
ID: "kimi-cli",
|
||||
Name: "Kimi Code CLI",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".config/agents/skills",
|
||||
},
|
||||
{
|
||||
ID: "kiro-cli",
|
||||
Name: "Kiro CLI",
|
||||
ProjectDir: ".kiro/skills",
|
||||
UserDir: ".kiro/skills",
|
||||
},
|
||||
{
|
||||
ID: "kode",
|
||||
Name: "Kode",
|
||||
ProjectDir: ".kode/skills",
|
||||
UserDir: ".kode/skills",
|
||||
},
|
||||
{
|
||||
ID: "mcpjam",
|
||||
Name: "MCPJam",
|
||||
ProjectDir: ".mcpjam/skills",
|
||||
UserDir: ".mcpjam/skills",
|
||||
},
|
||||
{
|
||||
ID: "mistral-vibe",
|
||||
Name: "Mistral Vibe",
|
||||
ProjectDir: ".vibe/skills",
|
||||
UserDir: ".vibe/skills",
|
||||
},
|
||||
{
|
||||
ID: "mux",
|
||||
Name: "Mux",
|
||||
ProjectDir: ".mux/skills",
|
||||
UserDir: ".mux/skills",
|
||||
},
|
||||
{
|
||||
ID: "neovate",
|
||||
Name: "Neovate",
|
||||
ProjectDir: ".neovate/skills",
|
||||
UserDir: ".neovate/skills",
|
||||
},
|
||||
{
|
||||
ID: "openclaw",
|
||||
Name: "OpenClaw",
|
||||
ProjectDir: "skills",
|
||||
UserDir: ".openclaw/skills",
|
||||
},
|
||||
{
|
||||
ID: "opencode",
|
||||
Name: "OpenCode",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".config/opencode/skills",
|
||||
},
|
||||
{
|
||||
ID: "openhands",
|
||||
Name: "OpenHands",
|
||||
ProjectDir: ".openhands/skills",
|
||||
UserDir: ".openhands/skills",
|
||||
},
|
||||
{
|
||||
ID: "pi",
|
||||
Name: "Pi",
|
||||
ProjectDir: ".pi/skills",
|
||||
UserDir: ".pi/agent/skills",
|
||||
},
|
||||
{
|
||||
ID: "pochi",
|
||||
Name: "Pochi",
|
||||
ProjectDir: ".pochi/skills",
|
||||
UserDir: ".pochi/skills",
|
||||
},
|
||||
{
|
||||
ID: "qoder",
|
||||
Name: "Qoder",
|
||||
ProjectDir: ".qoder/skills",
|
||||
UserDir: ".qoder/skills",
|
||||
},
|
||||
{
|
||||
ID: "qwen-code",
|
||||
Name: "Qwen Code",
|
||||
ProjectDir: ".qwen/skills",
|
||||
UserDir: ".qwen/skills",
|
||||
},
|
||||
{
|
||||
ID: "replit",
|
||||
Name: "Replit",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".config/agents/skills",
|
||||
},
|
||||
{
|
||||
ID: "roo",
|
||||
Name: "Roo Code",
|
||||
ProjectDir: ".roo/skills",
|
||||
UserDir: ".roo/skills",
|
||||
},
|
||||
{
|
||||
ID: "trae",
|
||||
Name: "Trae",
|
||||
ProjectDir: ".trae/skills",
|
||||
UserDir: ".trae/skills",
|
||||
},
|
||||
{
|
||||
ID: "trae-cn",
|
||||
Name: "Trae CN",
|
||||
ProjectDir: ".trae/skills",
|
||||
UserDir: ".trae-cn/skills",
|
||||
},
|
||||
{
|
||||
ID: "universal",
|
||||
Name: "Universal",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".config/agents/skills",
|
||||
},
|
||||
{
|
||||
ID: "warp",
|
||||
Name: "Warp",
|
||||
ProjectDir: sharedProjectSkillsDir,
|
||||
UserDir: ".agents/skills",
|
||||
},
|
||||
{
|
||||
ID: "windsurf",
|
||||
Name: "Windsurf",
|
||||
ProjectDir: ".windsurf/skills",
|
||||
UserDir: ".codeium/windsurf/skills",
|
||||
},
|
||||
{
|
||||
ID: "zencoder",
|
||||
Name: "Zencoder",
|
||||
ProjectDir: ".zencoder/skills",
|
||||
UserDir: ".zencoder/skills",
|
||||
},
|
||||
}
|
||||
|
||||
// FindByID returns the agent host with the given ID, or an error if not found.
|
||||
|
|
@ -97,6 +342,15 @@ func AgentIDs() []string {
|
|||
return ids
|
||||
}
|
||||
|
||||
// AgentHelpList returns a newline-separated bulleted list of agents for help text.
|
||||
func AgentHelpList() string {
|
||||
lines := make([]string, len(Agents))
|
||||
for i, h := range Agents {
|
||||
lines[i] = fmt.Sprintf(" - %s (%s)", h.Name, h.ID)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// AgentNames returns the display names of all agents for prompting.
|
||||
func AgentNames() []string {
|
||||
names := make([]string, len(Agents))
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func TestFindByID(t *testing.T) {
|
|||
{name: "claude-code", id: "claude-code", wantName: "Claude Code"},
|
||||
{name: "cursor", id: "cursor", wantName: "Cursor"},
|
||||
{name: "codex", id: "codex", wantName: "Codex"},
|
||||
{name: "gemini", id: "gemini", wantName: "Gemini CLI"},
|
||||
{name: "gemini-cli", id: "gemini-cli", wantName: "Gemini CLI"},
|
||||
{name: "antigravity", id: "antigravity", wantName: "Antigravity"},
|
||||
{name: "unknown agent", id: "nonexistent", wantErr: "unknown agent"},
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ func TestInstallDir(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "gemini project scope",
|
||||
hostID: "gemini",
|
||||
hostID: "gemini-cli",
|
||||
scope: ScopeProject,
|
||||
gitRoot: "/tmp/monalisa-repo",
|
||||
homeDir: "/home/monalisa",
|
||||
|
|
@ -167,7 +167,18 @@ func TestRepoNameFromRemote(t *testing.T) {
|
|||
|
||||
func TestUniqueProjectDirs(t *testing.T) {
|
||||
dirs := UniqueProjectDirs()
|
||||
assert.Equal(t, []string{".agents/skills", ".claude/skills"}, dirs)
|
||||
seen := map[string]int{}
|
||||
for _, d := range dirs {
|
||||
seen[d]++
|
||||
}
|
||||
// The shared .agents/skills dir and .claude/skills must both be present
|
||||
// and listed exactly once each.
|
||||
assert.Equal(t, 1, seen[".agents/skills"], "expected .agents/skills exactly once")
|
||||
assert.Equal(t, 1, seen[".claude/skills"], "expected .claude/skills exactly once")
|
||||
// No project dir should appear more than once.
|
||||
for d, n := range seen {
|
||||
assert.LessOrEqualf(t, n, 1, "project dir %q appears %d times", d, n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeLabels(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -80,24 +80,24 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
|
|||
Install agent skills from a GitHub repository or local directory into
|
||||
your local environment. Skills are placed in a host-specific directory
|
||||
at either project scope (inside the current git repository) or user
|
||||
scope (in your home directory, available everywhere). Supported hosts
|
||||
and their storage directories are (project, user):
|
||||
scope (in your home directory, available everywhere).
|
||||
|
||||
- GitHub Copilot (%[1]s.agents/skills%[1]s, %[1]s~/.copilot/skills%[1]s)
|
||||
- Claude Code (%[1]s.claude/skills%[1]s, %[1]s~/.claude/skills%[1]s)
|
||||
- Cursor (%[1]s.agents/skills%[1]s, %[1]s~/.cursor/skills%[1]s)
|
||||
- Codex (%[1]s.agents/skills%[1]s, %[1]s~/.codex/skills%[1]s)
|
||||
- Gemini CLI (%[1]s.agents/skills%[1]s, %[1]s~/.gemini/skills%[1]s)
|
||||
- Antigravity (%[1]s.agents/skills%[1]s, %[1]s~/.gemini/antigravity/skills%[1]s)
|
||||
A wide range of AI coding agents are supported, including GitHub
|
||||
Copilot, Claude Code, Cursor, Codex, Gemini CLI, Antigravity, Amp,
|
||||
Goose, Junie, OpenCode, Windsurf, and many more.
|
||||
|
||||
Supported %[1]s--agent%[1]s values:
|
||||
|
||||
%[2]s
|
||||
|
||||
Use %[1]s--agent%[1]s and %[1]s--scope%[1]s to control placement, or %[1]s--dir%[1]s for a
|
||||
custom directory. The default scope is %[1]sproject%[1]s, and the default
|
||||
agent is %[1]sgithub-copilot%[1]s (when running non-interactively).
|
||||
|
||||
At project scope, GitHub Copilot, Cursor, Codex, Gemini CLI, and
|
||||
Antigravity all use the shared %[1]s.agents/skills%[1]s directory. If you
|
||||
select multiple hosts that resolve to the same destination, each skill is
|
||||
installed there only once.
|
||||
At project scope, several agents (including GitHub Copilot, Cursor,
|
||||
Codex, Gemini CLI, Antigravity, Amp, Cline, OpenCode, and Warp) share
|
||||
the %[1]s.agents/skills%[1]s directory. If you select multiple hosts that
|
||||
resolve to the same destination, each skill is installed there only once.
|
||||
|
||||
The first argument is a GitHub repository in %[1]sOWNER/REPO%[1]s format.
|
||||
Use %[1]s--from-local%[1]s to install from a local directory instead.
|
||||
|
|
@ -133,7 +133,7 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
|
|||
When run interactively, the command prompts for any missing arguments.
|
||||
When run non-interactively, %[1]srepository%[1]s and a skill name are
|
||||
required.
|
||||
`, "`"),
|
||||
`, "`", registry.AgentHelpList()),
|
||||
Example: heredoc.Doc(`
|
||||
# Interactive: choose repo, skill, and agent
|
||||
$ gh skill install
|
||||
|
|
@ -198,7 +198,8 @@ func NewCmdInstall(f *cmdutil.Factory, telemetry ghtelemetry.CommandRecorder, ru
|
|||
},
|
||||
}
|
||||
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Agent, "agent", "", "", registry.AgentIDs(), "Target agent")
|
||||
agentFlag := cmdutil.StringEnumFlag(cmd, &opts.Agent, "agent", "", "", registry.AgentIDs(), "Target agent")
|
||||
agentFlag.Usage = "Target agent (see supported values above)"
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Scope, "scope", "", "project", []string{"project", "user"}, "Installation scope")
|
||||
cmd.Flags().StringVar(&opts.Pin, "pin", "", "Pin to a specific git tag or commit SHA")
|
||||
cmd.Flags().StringVar(&opts.Dir, "dir", "", "Install to a custom directory (overrides --agent and --scope)")
|
||||
|
|
@ -336,6 +337,7 @@ func installRun(opts *InstallOptions) error {
|
|||
|
||||
printFileTree(opts.IO.ErrOut, cs, result.Dir, result.Installed)
|
||||
printReviewHint(opts.IO.ErrOut, cs, repoSource, resolved.SHA, result.Installed)
|
||||
printHostHints(opts.IO.ErrOut, cs, plan.hosts, result.Installed, result.Dir, gitRoot)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -474,6 +476,7 @@ func runLocalInstall(opts *InstallOptions) error {
|
|||
|
||||
printFileTree(opts.IO.ErrOut, cs, result.Dir, result.Installed)
|
||||
printReviewHint(opts.IO.ErrOut, cs, "", "", result.Installed)
|
||||
printHostHints(opts.IO.ErrOut, cs, plan.hosts, result.Installed, result.Dir, gitRoot)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -789,8 +792,18 @@ func resolveHosts(opts *InstallOptions, canPrompt bool) ([]*registry.AgentHost,
|
|||
}
|
||||
|
||||
fmt.Fprintln(opts.IO.ErrOut)
|
||||
names := registry.AgentNames()
|
||||
indices, err := opts.Prompter.MultiSelect("Select target agent(s):", []string{names[0]}, names)
|
||||
labels := make([]string, len(registry.Agents))
|
||||
defaultLabel := ""
|
||||
for i, h := range registry.Agents {
|
||||
labels[i] = h.Name
|
||||
if h.ID == registry.DefaultAgentID {
|
||||
defaultLabel = labels[i]
|
||||
}
|
||||
}
|
||||
if defaultLabel == "" {
|
||||
defaultLabel = labels[0]
|
||||
}
|
||||
indices, err := opts.Prompter.MultiSelect("Select target agent(s):", []string{defaultLabel}, labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1058,3 +1071,43 @@ func printReviewHint(w io.Writer, cs *iostreams.ColorScheme, repo, sha string, s
|
|||
}
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
|
||||
// printHostHints prints any agent-specific post-install guidance for the
|
||||
// hosts that were installed to. Most agents need no extra steps; this is
|
||||
// currently used for Kiro CLI, which requires skills to be registered as
|
||||
// resources on a custom agent. The path in the example is derived from
|
||||
// the actual install directory so it matches the chosen scope or --dir.
|
||||
func printHostHints(w io.Writer, cs *iostreams.ColorScheme, hosts []*registry.AgentHost, installed []string, installDir, gitRoot string) {
|
||||
if len(installed) == 0 {
|
||||
return
|
||||
}
|
||||
for _, h := range hosts {
|
||||
if h.ID == "kiro-cli" {
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprint(w, heredoc.Docf(`
|
||||
%s Kiro CLI: register these skills on a custom agent by adding them to
|
||||
.kiro/agents/<agent>.json under "resources", for example:
|
||||
|
||||
{
|
||||
"resources": ["skill://%s/**/SKILL.md"]
|
||||
}
|
||||
`, cs.WarningIcon(), kiroResourcePath(installDir, gitRoot)))
|
||||
fmt.Fprintln(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kiroResourcePath returns a slash-separated path suitable for use in the
|
||||
// "resources" field of a Kiro agent config. When the install directory is
|
||||
// inside the current git repository the path is made relative to the repo
|
||||
// root so the example works for project-scoped agent configs; otherwise
|
||||
// the absolute install path is used (e.g. for --scope user or --dir).
|
||||
func kiroResourcePath(installDir, gitRoot string) string {
|
||||
if gitRoot != "" && installDir != "" {
|
||||
if rel, err := filepath.Rel(gitRoot, installDir); err == nil && !strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel) {
|
||||
return filepath.ToSlash(rel)
|
||||
}
|
||||
}
|
||||
return filepath.ToSlash(installDir)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/internal/skills/discovery"
|
||||
"github.com/cli/cli/v2/internal/skills/registry"
|
||||
"github.com/cli/cli/v2/internal/telemetry"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
|
|
@ -1403,7 +1404,15 @@ func TestInstallRun_DeduplicatesSharedProjectDirAcrossHosts(t *testing.T) {
|
|||
|
||||
pm := &prompter.PrompterMock{
|
||||
MultiSelectFunc: func(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
return []int{0, 2}, nil // GitHub Copilot + Cursor share .agents/skills
|
||||
// Select two agents that share the .agents/skills project dir
|
||||
// (GitHub Copilot and Cursor) to exercise deduplication.
|
||||
var indices []int
|
||||
for i, label := range options {
|
||||
if label == "GitHub Copilot" || label == "Cursor" {
|
||||
indices = append(indices, i)
|
||||
}
|
||||
}
|
||||
return indices, nil
|
||||
},
|
||||
SelectFunc: func(prompt, defaultValue string, options []string) (int, error) {
|
||||
return 0, nil // project scope
|
||||
|
|
@ -1947,6 +1956,87 @@ func Test_printReviewHint(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_printHostHints(t *testing.T) {
|
||||
kiro := ®istry.AgentHost{ID: "kiro-cli", Name: "Kiro CLI", ProjectDir: ".kiro/skills", UserDir: ".kiro/skills"}
|
||||
copilot := ®istry.AgentHost{ID: "copilot-cli", Name: "GitHub Copilot CLI", ProjectDir: ".github/skills"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hosts []*registry.AgentHost
|
||||
installed []string
|
||||
installDir string
|
||||
gitRoot string
|
||||
wantSub []string
|
||||
wantNot []string
|
||||
}{
|
||||
{
|
||||
name: "no installs produces no output",
|
||||
hosts: []*registry.AgentHost{kiro},
|
||||
installed: nil,
|
||||
installDir: "/repo/.kiro/skills",
|
||||
gitRoot: "/repo",
|
||||
wantNot: []string{"Kiro CLI"},
|
||||
},
|
||||
{
|
||||
name: "non-kiro host produces no output",
|
||||
hosts: []*registry.AgentHost{copilot},
|
||||
installed: []string{"s1"},
|
||||
installDir: "/repo/.github/skills",
|
||||
gitRoot: "/repo",
|
||||
wantNot: []string{"Kiro CLI"},
|
||||
},
|
||||
{
|
||||
name: "kiro project scope uses relative path",
|
||||
hosts: []*registry.AgentHost{kiro},
|
||||
installed: []string{"s1"},
|
||||
installDir: filepath.Join("/repo", ".kiro", "skills"),
|
||||
gitRoot: "/repo",
|
||||
wantSub: []string{"Kiro CLI", `"skill://.kiro/skills/**/SKILL.md"`},
|
||||
},
|
||||
{
|
||||
name: "kiro user scope uses absolute install dir",
|
||||
hosts: []*registry.AgentHost{kiro},
|
||||
installed: []string{"s1"},
|
||||
installDir: "/home/user/.kiro/skills",
|
||||
gitRoot: "/repo",
|
||||
wantSub: []string{`"skill:///home/user/.kiro/skills/**/SKILL.md"`},
|
||||
wantNot: []string{`skill://.kiro/skills`},
|
||||
},
|
||||
{
|
||||
name: "kiro custom dir outside git root uses absolute path",
|
||||
hosts: []*registry.AgentHost{kiro},
|
||||
installed: []string{"s1"},
|
||||
installDir: "/tmp/my-skills",
|
||||
gitRoot: "/repo",
|
||||
wantSub: []string{`"skill:///tmp/my-skills/**/SKILL.md"`},
|
||||
},
|
||||
{
|
||||
name: "kiro without git root falls back to install dir",
|
||||
hosts: []*registry.AgentHost{kiro},
|
||||
installed: []string{"s1"},
|
||||
installDir: "/home/user/.kiro/skills",
|
||||
gitRoot: "",
|
||||
wantSub: []string{`"skill:///home/user/.kiro/skills/**/SKILL.md"`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
cs := ios.ColorScheme()
|
||||
var buf strings.Builder
|
||||
printHostHints(&buf, cs, tt.hosts, tt.installed, tt.installDir, tt.gitRoot)
|
||||
got := buf.String()
|
||||
for _, s := range tt.wantSub {
|
||||
assert.Contains(t, got, s)
|
||||
}
|
||||
for _, s := range tt.wantNot {
|
||||
assert.NotContains(t, got, s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_printPreInstallDisclaimer(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
cs := ios.ColorScheme()
|
||||
|
|
|
|||
|
|
@ -859,6 +859,11 @@ func checkInstalledSkillDirs(gitClient *git.Client, repoDir string) []publishDia
|
|||
var diagnostics []publishDiagnostic
|
||||
|
||||
for _, relPath := range registry.UniqueProjectDirs() {
|
||||
// Skip non-hidden project dirs (such as "skills") to avoid
|
||||
// flagging the canonical authoring layout used when publishing.
|
||||
if !strings.HasPrefix(relPath, ".") {
|
||||
continue
|
||||
}
|
||||
absPath := filepath.Join(repoDir, relPath)
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue