Skip to content
9 changes: 7 additions & 2 deletions experimental/aitools/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import (
)

func newInstallCmd() *cobra.Command {
return &cobra.Command{
var includeExperimental bool

cmd := &cobra.Command{
Use: "install [skill-name]",
Short: "Alias for skills install",
Long: `Alias for "databricks experimental aitools skills install".

Installs Databricks skills for detected coding agents.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runSkillsInstall(cmd.Context(), args)
return runSkillsInstall(cmd.Context(), args, includeExperimental)
},
}

cmd.Flags().BoolVar(&includeExperimental, "experimental", false, "Include experimental skills")
return cmd
}
202 changes: 163 additions & 39 deletions experimental/aitools/cmd/install_test.go
Original file line number Diff line number Diff line change
@@ -1,75 +1,199 @@
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
flags []string
wantAgents int
wantSkills []string
wantExperimental bool
}{
{
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"},
wantAgents: 2,
wantSkills: []string{"bundle/review"},
},
{
name: "skills install forwards skill name",
newCmd: newSkillsInstallCmd,
args: []string{"bundle/review"},
wantSkillCalls: []string{"bundle/review"},
name: "skills install with --experimental",
newCmd: newSkillsInstallCmd,
flags: []string{"--experimental"},
wantAgents: 2,
wantExperimental: true,
},
{
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"},
},
{
name: "top level install with --experimental",
newCmd: newInstallCmd,
flags: []string{"--experimental"},
wantAgents: 2,
wantExperimental: true,
},
}

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)
if len(tt.flags) > 0 {
require.NoError(t, cmd.ParseFlags(tt.flags))
}

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)
assert.Equal(t, tt.wantExperimental, (*calls)[0].opts.IncludeExperimental)
})
}
}

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, false)
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, false)
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, false)
require.NoError(t, err)
assert.Empty(t, *calls, "install should not be called when no agents detected")
}
85 changes: 78 additions & 7 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 All @@ -36,7 +72,9 @@ func newSkillsListCmd() *cobra.Command {
}

func newSkillsInstallCmd() *cobra.Command {
return &cobra.Command{
var includeExperimental bool

cmd := &cobra.Command{
Use: "install [skill-name]",
Short: "Install Databricks skills for detected coding agents",
Long: `Install Databricks skills to all detected coding agents.
Expand All @@ -47,15 +85,48 @@ and symlinked to each agent to avoid duplication.

Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity`,
RunE: func(cmd *cobra.Command, args []string) error {
return runSkillsInstall(cmd.Context(), args)
return runSkillsInstall(cmd.Context(), args, includeExperimental)
},
}

cmd.Flags().BoolVar(&includeExperimental, "experimental", false, "Include experimental skills")
return cmd
}

func runSkillsInstall(ctx context.Context, args []string) error {
func runSkillsInstall(ctx context.Context, args []string, includeExperimental bool) 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{
IncludeExperimental: includeExperimental,
}
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