Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 143 additions & 39 deletions experimental/aitools/cmd/install_test.go
Original file line number Diff line number Diff line change
@@ -1,75 +1,179 @@
package aitools

import (
"bufio"
"context"
"os"
"path/filepath"
"testing"

"github.com/databricks/cli/experimental/aitools/lib/agents"
"github.com/databricks/cli/experimental/aitools/lib/installer"
"github.com/databricks/cli/libs/cmdio"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func setupInstallMock(t *testing.T) *[]installCall {
t.Helper()
orig := installSkillsForAgentsFn
t.Cleanup(func() { installSkillsForAgentsFn = orig })

var calls []installCall
installSkillsForAgentsFn = func(_ context.Context, _ installer.ManifestSource, targetAgents []*agents.Agent, opts installer.InstallOptions) error {
names := make([]string, len(targetAgents))
for i, a := range targetAgents {
names[i] = a.Name
}
calls = append(calls, installCall{agents: names, opts: opts})
return nil
}
return &calls
}

type installCall struct {
agents []string
opts installer.InstallOptions
}

func setupTestAgents(t *testing.T) string {
t.Helper()
tmp := t.TempDir()
t.Setenv("HOME", tmp)
// Create config dirs for two agents.
require.NoError(t, os.MkdirAll(filepath.Join(tmp, ".claude"), 0o755))
require.NoError(t, os.MkdirAll(filepath.Join(tmp, ".cursor"), 0o755))
return tmp
}

func TestInstallCommandsDelegateToSkillsInstall(t *testing.T) {
originalInstallAllSkills := installAllSkills
originalInstallSkill := installSkill
t.Cleanup(func() {
installAllSkills = originalInstallAllSkills
installSkill = originalInstallSkill
})
setupTestAgents(t)
calls := setupInstallMock(t)

tests := []struct {
name string
newCmd func() *cobra.Command
args []string
wantAllCalls int
wantSkillCalls []string
name string
newCmd func() *cobra.Command
args []string
wantAgents int
wantSkills []string
}{
{
name: "skills install installs all skills",
newCmd: newSkillsInstallCmd,
wantAllCalls: 1,
name: "skills install installs all skills for all agents",
newCmd: newSkillsInstallCmd,
wantAgents: 2,
},
{
name: "skills install forwards skill name",
newCmd: newSkillsInstallCmd,
args: []string{"bundle/review"},
wantSkillCalls: []string{"bundle/review"},
name: "skills install forwards skill name",
newCmd: newSkillsInstallCmd,
args: []string{"bundle/review"},
wantAgents: 2,
wantSkills: []string{"bundle/review"},
},
{
name: "top level install installs all skills",
newCmd: newInstallCmd,
wantAllCalls: 1,
name: "top level install installs all skills",
newCmd: newInstallCmd,
wantAgents: 2,
},
{
name: "top level install forwards skill name",
newCmd: newInstallCmd,
args: []string{"bundle/review"},
wantSkillCalls: []string{"bundle/review"},
name: "top level install forwards skill name",
newCmd: newInstallCmd,
args: []string{"bundle/review"},
wantAgents: 2,
wantSkills: []string{"bundle/review"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
allCalls := 0
var skillCalls []string

installAllSkills = func(context.Context) error {
allCalls++
return nil
}
installSkill = func(_ context.Context, skillName string) error {
skillCalls = append(skillCalls, skillName)
return nil
}
*calls = nil

ctx := cmdio.MockDiscard(t.Context())
cmd := tt.newCmd()
cmd.SetContext(t.Context())
cmd.SetContext(ctx)

err := cmd.RunE(cmd, tt.args)
require.NoError(t, err)

assert.Equal(t, tt.wantAllCalls, allCalls)
assert.Equal(t, tt.wantSkillCalls, skillCalls)
require.Len(t, *calls, 1)
assert.Len(t, (*calls)[0].agents, tt.wantAgents)
assert.Equal(t, tt.wantSkills, (*calls)[0].opts.SpecificSkills)
})
}
}

func TestRunSkillsInstallInteractivePrompt(t *testing.T) {
setupTestAgents(t)
calls := setupInstallMock(t)

origPrompt := promptAgentSelection
t.Cleanup(func() { promptAgentSelection = origPrompt })

promptCalled := false
promptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) {
promptCalled = true
// Return only the first agent.
return detected[:1], nil
}

// Use SetupTest with PromptSupported=true to simulate interactive terminal.
ctx, test := cmdio.SetupTest(t.Context(), cmdio.TestOptions{PromptSupported: true})
defer test.Done()

// Drain both pipes in background to prevent blocking.
drain := func(r *bufio.Reader) {
buf := make([]byte, 4096)
for {
_, err := r.Read(buf)
if err != nil {
return
}
}
}
go drain(test.Stdout)
go drain(test.Stderr)

err := runSkillsInstall(ctx, nil)
require.NoError(t, err)

assert.True(t, promptCalled, "prompt should be called when 2+ agents detected and interactive")
require.Len(t, *calls, 1)
assert.Len(t, (*calls)[0].agents, 1, "only the selected agent should be passed")
}

func TestRunSkillsInstallNonInteractiveUsesAllAgents(t *testing.T) {
setupTestAgents(t)
calls := setupInstallMock(t)

origPrompt := promptAgentSelection
t.Cleanup(func() { promptAgentSelection = origPrompt })

promptCalled := false
promptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) {
promptCalled = true
return detected, nil
}

// MockDiscard gives a non-interactive context.
ctx := cmdio.MockDiscard(t.Context())

err := runSkillsInstall(ctx, nil)
require.NoError(t, err)

assert.False(t, promptCalled, "prompt should not be called in non-interactive mode")
require.Len(t, *calls, 1)
assert.Len(t, (*calls)[0].agents, 2, "all detected agents should be used")
}

func TestRunSkillsInstallNoAgents(t *testing.T) {
// Set HOME to empty dir so no agents are detected.
tmp := t.TempDir()
t.Setenv("HOME", tmp)

calls := setupInstallMock(t)
ctx := cmdio.MockDiscard(t.Context())

err := runSkillsInstall(ctx, nil)
require.NoError(t, err)
assert.Empty(t, *calls, "install should not be called when no agents detected")
}
72 changes: 68 additions & 4 deletions experimental/aitools/cmd/skills.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,52 @@ package aitools

import (
"context"
"errors"

"github.com/charmbracelet/huh"
"github.com/databricks/cli/experimental/aitools/lib/agents"
"github.com/databricks/cli/experimental/aitools/lib/installer"
"github.com/databricks/cli/libs/cmdio"
"github.com/fatih/color"
"github.com/spf13/cobra"
)

// Package-level vars for testability.
var (
installAllSkills = installer.InstallAllSkills
installSkill = installer.InstallSkill
promptAgentSelection = defaultPromptAgentSelection
installSkillsForAgentsFn = installer.InstallSkillsForAgents
)

func defaultPromptAgentSelection(ctx context.Context, detected []*agents.Agent) ([]*agents.Agent, error) {
options := make([]huh.Option[string], 0, len(detected))
agentsByName := make(map[string]*agents.Agent, len(detected))
for _, a := range detected {
options = append(options, huh.NewOption(a.DisplayName, a.Name).Selected(true))
agentsByName[a.Name] = a
}

var selected []string
err := huh.NewMultiSelect[string]().
Title("Select coding agents to install skills for").
Description("space to toggle, enter to confirm").
Options(options...).
Value(&selected).
Run()
if err != nil {
return nil, err
}

if len(selected) == 0 {
return nil, errors.New("at least one agent must be selected")
}

result := make([]*agents.Agent, 0, len(selected))
for _, name := range selected {
result = append(result, agentsByName[name])
}
return result, nil
}

func newSkillsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "skills",
Expand Down Expand Up @@ -53,9 +89,37 @@ Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Anti
}

func runSkillsInstall(ctx context.Context, args []string) error {
detected := agents.DetectInstalled(ctx)
if len(detected) == 0 {
cmdio.LogString(ctx, color.YellowString("No supported coding agents detected."))
cmdio.LogString(ctx, "")
cmdio.LogString(ctx, "Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity")
cmdio.LogString(ctx, "Please install at least one coding agent first.")
return nil
}

var targetAgents []*agents.Agent
switch {
case len(detected) == 1:
targetAgents = detected
case cmdio.IsPromptSupported(ctx):
var err error
targetAgents, err = promptAgentSelection(ctx, detected)
if err != nil {
return err
}
default:
// Non-interactive: install for all detected agents.
targetAgents = detected
}

installer.PrintInstallingFor(ctx, targetAgents)

opts := installer.InstallOptions{}
if len(args) > 0 {
return installSkill(ctx, args[0])
opts.SpecificSkills = []string{args[0]}
}

return installAllSkills(ctx)
src := &installer.GitHubManifestSource{}
return installSkillsForAgentsFn(ctx, src, targetAgents, opts)
}
Loading
Loading