diff --git a/.agent/skills/pr-checklist/SKILL.md b/.agent/skills/pr-checklist/SKILL.md index 3eae258d6e7..86559354fcb 100644 --- a/.agent/skills/pr-checklist/SKILL.md +++ b/.agent/skills/pr-checklist/SKILL.md @@ -22,8 +22,8 @@ Before submitting a PR, run these commands to match what CI checks. CI uses the # 5. If you changed files in python/: ./task pydabs-codegen pydabs-test pydabs-lint pydabs-docs -# 6. If you changed experimental/aitools or experimental/ssh: -./task test-exp-aitools # only if aitools code changed +# 6. If you changed cmd/aitools/, libs/aitools/, experimental/aitools/, or experimental/ssh/: +./task test-exp-aitools # only if aitools code changed (top-level or experimental) ./task test-exp-ssh # only if ssh code changed ``` diff --git a/.github/OWNERS b/.github/OWNERS index 579fcc2d44b..cf8464e99e7 100644 --- a/.github/OWNERS +++ b/.github/OWNERS @@ -59,6 +59,10 @@ # Internal /internal/ team:platform +# AI tools +/cmd/aitools/ team:eng-apps-devex team:platform @lennartkats-db +/libs/aitools/ team:eng-apps-devex team:platform @lennartkats-db + # CLI compatibility manifest /internal/build/cli-compat.json team:eng-apps-devex team:platform /libs/clicompat/ team:eng-apps-devex team:platform diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 36cbb445d2c..fa2adeb1e8a 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -6,6 +6,8 @@ ### CLI +* Added `databricks aitools` command group for installing Databricks skills into your coding agents (Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity). Skills are fetched from [github.com/databricks/databricks-agent-skills](https://github.com/databricks/databricks-agent-skills) and either symlinked into each agent's skills directory or copied into the current project. Use `databricks aitools install` to set up, `update` to pull newer versions, `list` to see what's available, and `uninstall` to remove them. + ### Bundles * Make sure warnings asking for approval are understood by agents ([#5239](https://github.com/databricks/cli/pull/5239)) diff --git a/Taskfile.yml b/Taskfile.yml index 38f690bd56a..0d582a1954e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -609,8 +609,10 @@ tasks: # generic `test` target (the catch-all) instead. test-exp-aitools: - desc: Run experimental aitools unit and acceptance tests + desc: Run aitools (top-level + experimental) unit and acceptance tests sources: + - cmd/aitools/** + - libs/aitools/** - experimental/aitools/** - acceptance/apps/** - "{{.EMBED_SOURCES}}" @@ -619,7 +621,7 @@ tasks: {{.GO_TOOL}} gotestsum \ --format ${GOTESTSUM_FORMAT:-pkgname-and-test-fails} \ --no-summary=skipped \ - --packages ./experimental/aitools/... \ + --packages "./cmd/aitools/... ./libs/aitools/... ./experimental/aitools/..." \ -- -timeout=${LOCAL_TIMEOUT:-30m} - | {{.GO_TOOL}} gotestsum \ diff --git a/acceptance/help/output.txt b/acceptance/help/output.txt index 6be3d28d0b3..af62067b5c6 100644 --- a/acceptance/help/output.txt +++ b/acceptance/help/output.txt @@ -168,6 +168,7 @@ Developer Tools Additional Commands: account Databricks Account Commands + aitools Databricks AI Tools for coding agents api Perform Databricks API call auth Authentication related commands cache Local cache related commands diff --git a/cmd/aitools/aitools.go b/cmd/aitools/aitools.go new file mode 100644 index 00000000000..99ffbb8bc82 --- /dev/null +++ b/cmd/aitools/aitools.go @@ -0,0 +1,25 @@ +package aitools + +import ( + "github.com/spf13/cobra" +) + +func NewAitoolsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "aitools", + Short: "Databricks AI Tools for coding agents", + Long: `Install Databricks skills into your coding agent so it can work +effectively with Databricks resources (bundles, jobs, SQL, and more). + +Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub +Copilot, Antigravity.`, + } + + cmd.AddCommand(NewInstallCmd()) + cmd.AddCommand(NewUpdateCmd()) + cmd.AddCommand(NewUninstallCmd()) + cmd.AddCommand(NewListCmd()) + cmd.AddCommand(NewVersionCmd()) + + return cmd +} diff --git a/experimental/aitools/cmd/flags.go b/cmd/aitools/flags.go similarity index 100% rename from experimental/aitools/cmd/flags.go rename to cmd/aitools/flags.go diff --git a/experimental/aitools/cmd/flags_test.go b/cmd/aitools/flags_test.go similarity index 100% rename from experimental/aitools/cmd/flags_test.go rename to cmd/aitools/flags_test.go diff --git a/experimental/aitools/cmd/install.go b/cmd/aitools/install.go similarity index 76% rename from experimental/aitools/cmd/install.go rename to cmd/aitools/install.go index 8e95e511cf5..069de9bbec4 100644 --- a/experimental/aitools/cmd/install.go +++ b/cmd/aitools/install.go @@ -6,13 +6,51 @@ import ( "fmt" "strings" - "github.com/databricks/cli/experimental/aitools/lib/agents" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/charmbracelet/huh" + "github.com/databricks/cli/libs/aitools/agents" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/cmdio" "github.com/spf13/cobra" ) -func newInstallCmd() *cobra.Command { +// Package-level for testability. Tests in this package override them via +// helpers in install_test.go. +var ( + 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 NewInstallCmd() *cobra.Command { var skillsFlag, agentsFlag string var includeExperimental bool var projectFlag, globalFlag bool @@ -27,6 +65,8 @@ Use --project to install to the current project directory instead. When multiple agents are detected, skills are stored in a canonical location and symlinked to each agent to avoid duplication. +Use --skills name1,name2 to install specific skills. + Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/experimental/aitools/cmd/install_test.go b/cmd/aitools/install_test.go similarity index 80% rename from experimental/aitools/cmd/install_test.go rename to cmd/aitools/install_test.go index 38639705ea4..0ed99bcbfd4 100644 --- a/experimental/aitools/cmd/install_test.go +++ b/cmd/aitools/install_test.go @@ -7,8 +7,8 @@ import ( "path/filepath" "testing" - "github.com/databricks/cli/experimental/aitools/lib/agents" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/agents" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/cmdio" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -53,6 +53,7 @@ func setupTestAgents(t *testing.T) string { t.Helper() tmp := t.TempDir() t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", 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)) @@ -64,7 +65,7 @@ func TestInstallAllSkillsForAllAgents(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -80,7 +81,7 @@ func TestInstallSpecificSkills(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--skills", "databricks,databricks-apps"}) @@ -96,7 +97,7 @@ func TestInstallSingleSkill(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--skills", "databricks"}) @@ -112,7 +113,7 @@ func TestInstallSpecificAgents(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--agents", "claude-code"}) @@ -128,7 +129,7 @@ func TestInstallUnknownAgentErrors(t *testing.T) { setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--agents", "invalid-agent"}) cmd.SilenceErrors = true @@ -145,7 +146,7 @@ func TestInstallIncludeExperimental(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--experimental"}) @@ -185,7 +186,7 @@ func TestInstallInteractivePrompt(t *testing.T) { go drain(test.Stdout) go drain(test.Stderr) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -210,7 +211,7 @@ func TestInstallNonInteractiveUsesAllAgents(t *testing.T) { } ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -224,11 +225,12 @@ func TestInstallNonInteractiveUsesAllAgents(t *testing.T) { func TestInstallNoAgentsDetected(t *testing.T) { tmp := t.TempDir() t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", tmp) calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -250,7 +252,7 @@ func TestInstallAgentsFlagSkipsPrompt(t *testing.T) { } ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--agents", "claude-code,cursor"}) @@ -262,103 +264,11 @@ func TestInstallAgentsFlagSkipsPrompt(t *testing.T) { assert.Equal(t, []string{"claude-code", "cursor"}, (*calls)[0].agents) } -func TestSkillsInstallDelegatesToInstall(t *testing.T) { - setupTestAgents(t) - calls := setupInstallMock(t) - - ctx := cmdio.MockDiscard(t.Context()) - cmd := newSkillsInstallCmd() - cmd.SetContext(ctx) - - err := cmd.RunE(cmd, nil) - require.NoError(t, err) - - require.Len(t, *calls, 1) - assert.Len(t, (*calls)[0].agents, 2) -} - -func TestSkillsInstallForwardsSkillName(t *testing.T) { - setupTestAgents(t) - calls := setupInstallMock(t) - - ctx := cmdio.MockDiscard(t.Context()) - cmd := newSkillsInstallCmd() - cmd.SetContext(ctx) - - err := cmd.RunE(cmd, []string{"databricks"}) - require.NoError(t, err) - - require.Len(t, *calls, 1) - assert.Equal(t, []string{"databricks"}, (*calls)[0].opts.SpecificSkills) -} - -func TestSkillsInstallExecuteNoArgs(t *testing.T) { - setupTestAgents(t) - calls := setupInstallMock(t) - - ctx := cmdio.MockDiscard(t.Context()) - cmd := newSkillsInstallCmd() - cmd.SetContext(ctx) - cmd.SetArgs([]string{}) - - err := cmd.Execute() - require.NoError(t, err) - - require.Len(t, *calls, 1) - assert.Len(t, (*calls)[0].agents, 2) - assert.Nil(t, (*calls)[0].opts.SpecificSkills) -} - -func TestSkillsInstallExecuteWithSkillName(t *testing.T) { - setupTestAgents(t) - calls := setupInstallMock(t) - - ctx := cmdio.MockDiscard(t.Context()) - cmd := newSkillsInstallCmd() - cmd.SetContext(ctx) - cmd.SetArgs([]string{"databricks"}) - - err := cmd.Execute() - require.NoError(t, err) - - require.Len(t, *calls, 1) - assert.Equal(t, []string{"databricks"}, (*calls)[0].opts.SpecificSkills) -} - -func TestSkillsInstallForwardsExperimental(t *testing.T) { - setupTestAgents(t) - calls := setupInstallMock(t) - - ctx := cmdio.MockDiscard(t.Context()) - cmd := newSkillsInstallCmd() - cmd.SetContext(ctx) - cmd.SetArgs([]string{"--experimental"}) - - err := cmd.Execute() - require.NoError(t, err) - - require.Len(t, *calls, 1) - assert.True(t, (*calls)[0].opts.IncludeExperimental, "--experimental should be forwarded") -} - -func TestSkillsInstallExecuteRejectsTwoArgs(t *testing.T) { - ctx := cmdio.MockDiscard(t.Context()) - cmd := newSkillsInstallCmd() - cmd.SetContext(ctx) - cmd.SetArgs([]string{"a", "b"}) - cmd.SilenceErrors = true - cmd.SilenceUsage = true - - err := cmd.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "accepts at most 1 arg") -} - func TestInstallRejectsPositionalArgs(t *testing.T) { ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) - cmd.SetArgs([]string{"databricks"}) + cmd.SetArgs([]string{"databricks-jobs"}) cmd.SilenceErrors = true cmd.SilenceUsage = true @@ -369,7 +279,7 @@ func TestInstallRejectsPositionalArgs(t *testing.T) { func TestUpdateRejectsPositionalArgs(t *testing.T) { ctx := cmdio.MockDiscard(t.Context()) - cmd := newUpdateCmd() + cmd := NewUpdateCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"databricks"}) cmd.SilenceErrors = true @@ -382,7 +292,7 @@ func TestUpdateRejectsPositionalArgs(t *testing.T) { func TestUninstallRejectsPositionalArgs(t *testing.T) { ctx := cmdio.MockDiscard(t.Context()) - cmd := newUninstallCmd() + cmd := NewUninstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"databricks"}) cmd.SilenceErrors = true @@ -395,7 +305,7 @@ func TestUninstallRejectsPositionalArgs(t *testing.T) { func TestListRejectsPositionalArgs(t *testing.T) { ctx := cmdio.MockDiscard(t.Context()) - cmd := newListCmd() + cmd := NewListCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"databricks"}) cmd.SilenceErrors = true @@ -408,7 +318,7 @@ func TestListRejectsPositionalArgs(t *testing.T) { func TestVersionRejectsPositionalArgs(t *testing.T) { ctx := cmdio.MockDiscard(t.Context()) - cmd := newVersionCmd() + cmd := NewVersionCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"databricks"}) cmd.SilenceErrors = true @@ -458,7 +368,7 @@ func TestInstallProjectFlag(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--project"}) @@ -474,7 +384,7 @@ func TestInstallGlobalFlag(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--global"}) @@ -490,7 +400,7 @@ func TestInstallGlobalAndProjectErrors(t *testing.T) { setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--global", "--project"}) cmd.SilenceErrors = true @@ -506,7 +416,7 @@ func TestInstallNoFlagNonInteractiveUsesGlobal(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -543,7 +453,7 @@ func TestInstallNoFlagInteractiveShowsScopePrompt(t *testing.T) { go drain(test.Stdout) go drain(test.Stderr) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) diff --git a/cmd/aitools/legacy_skills.go b/cmd/aitools/legacy_skills.go new file mode 100644 index 00000000000..d258039f105 --- /dev/null +++ b/cmd/aitools/legacy_skills.go @@ -0,0 +1,62 @@ +package aitools + +import ( + "github.com/spf13/cobra" +) + +// NewLegacySkillsCmd returns the deprecated `skills` subgroup used under +// `databricks experimental aitools`. It is only mounted there for backward +// compatibility; new code should call the top-level install/list commands. +func NewLegacySkillsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "skills", + Hidden: true, + Short: "Manage Databricks skills for coding agents", + Long: `Manage Databricks skills that extend coding agents with Databricks-specific capabilities.`, + Deprecated: `use "databricks aitools" instead.`, + } + + cmd.AddCommand(newLegacySkillsListCmd()) + cmd.AddCommand(newLegacySkillsInstallCmd()) + + return cmd +} + +func newLegacySkillsListCmd() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List available skills", + Deprecated: `use "databricks aitools list" instead.`, + RunE: func(cmd *cobra.Command, args []string) error { + return listSkillsFn(cmd, "") + }, + } +} + +func newLegacySkillsInstallCmd() *cobra.Command { + var includeExperimental bool + + cmd := &cobra.Command{ + Use: "install [skill-name]", + Short: "Install Databricks skills for detected coding agents", + Deprecated: `use "databricks aitools install --skills " instead.`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + installCmd := NewInstallCmd() + installCmd.SetContext(cmd.Context()) + + var delegateArgs []string + if len(args) > 0 { + delegateArgs = append(delegateArgs, "--skills", args[0]) + } + if includeExperimental { + delegateArgs = append(delegateArgs, "--experimental") + } + installCmd.SetArgs(delegateArgs) + return installCmd.Execute() + }, + } + + cmd.Flags().BoolVar(&includeExperimental, "experimental", false, "Include experimental skills") + return cmd +} diff --git a/cmd/aitools/legacy_skills_test.go b/cmd/aitools/legacy_skills_test.go new file mode 100644 index 00000000000..250d910960b --- /dev/null +++ b/cmd/aitools/legacy_skills_test.go @@ -0,0 +1,121 @@ +package aitools + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLegacySkillsInstallDelegatesToInstall(t *testing.T) { + setupTestAgents(t) + calls := setupInstallMock(t) + + ctx := cmdio.MockDiscard(t.Context()) + cmd := newLegacySkillsInstallCmd() + cmd.SetContext(ctx) + + err := cmd.RunE(cmd, nil) + require.NoError(t, err) + + require.Len(t, *calls, 1) + assert.Len(t, (*calls)[0].agents, 2) +} + +func TestLegacySkillsInstallForwardsSkillName(t *testing.T) { + setupTestAgents(t) + calls := setupInstallMock(t) + + ctx := cmdio.MockDiscard(t.Context()) + cmd := newLegacySkillsInstallCmd() + cmd.SetContext(ctx) + + err := cmd.RunE(cmd, []string{"databricks"}) + require.NoError(t, err) + + require.Len(t, *calls, 1) + assert.Equal(t, []string{"databricks"}, (*calls)[0].opts.SpecificSkills) +} + +func TestLegacySkillsInstallExecuteNoArgs(t *testing.T) { + setupTestAgents(t) + calls := setupInstallMock(t) + + ctx := cmdio.MockDiscard(t.Context()) + cmd := newLegacySkillsInstallCmd() + cmd.SetContext(ctx) + cmd.SetArgs([]string{}) + + err := cmd.Execute() + require.NoError(t, err) + + require.Len(t, *calls, 1) + assert.Len(t, (*calls)[0].agents, 2) + assert.Nil(t, (*calls)[0].opts.SpecificSkills) +} + +func TestLegacySkillsInstallExecuteWithSkillName(t *testing.T) { + setupTestAgents(t) + calls := setupInstallMock(t) + + ctx := cmdio.MockDiscard(t.Context()) + cmd := newLegacySkillsInstallCmd() + cmd.SetContext(ctx) + cmd.SetArgs([]string{"databricks"}) + + err := cmd.Execute() + require.NoError(t, err) + + require.Len(t, *calls, 1) + assert.Equal(t, []string{"databricks"}, (*calls)[0].opts.SpecificSkills) +} + +func TestLegacySkillsInstallForwardsExperimental(t *testing.T) { + setupTestAgents(t) + calls := setupInstallMock(t) + + ctx := cmdio.MockDiscard(t.Context()) + cmd := newLegacySkillsInstallCmd() + cmd.SetContext(ctx) + cmd.SetArgs([]string{"--experimental"}) + + err := cmd.Execute() + require.NoError(t, err) + + require.Len(t, *calls, 1) + assert.True(t, (*calls)[0].opts.IncludeExperimental, "--experimental should be forwarded") +} + +func TestLegacySkillsInstallExecuteRejectsTwoArgs(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + cmd := newLegacySkillsInstallCmd() + cmd.SetContext(ctx) + cmd.SetArgs([]string{"a", "b"}) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "accepts at most 1 arg") +} + +func TestLegacySkillsListDelegatesToListFn(t *testing.T) { + orig := listSkillsFn + t.Cleanup(func() { listSkillsFn = orig }) + + called := false + listSkillsFn = func(cmd *cobra.Command, scope string) error { + called = true + return nil + } + + ctx := cmdio.MockDiscard(t.Context()) + cmd := newLegacySkillsListCmd() + cmd.SetContext(ctx) + + err := cmd.RunE(cmd, nil) + require.NoError(t, err) + assert.True(t, called) +} diff --git a/experimental/aitools/cmd/list.go b/cmd/aitools/list.go similarity index 98% rename from experimental/aitools/cmd/list.go rename to cmd/aitools/list.go index 90d397aba6f..bb9a01b3bd2 100644 --- a/experimental/aitools/cmd/list.go +++ b/cmd/aitools/list.go @@ -8,7 +8,7 @@ import ( "strings" "text/tabwriter" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/spf13/cobra" @@ -18,7 +18,7 @@ import ( // It is a package-level var so tests can replace the data-fetching layer. var listSkillsFn = defaultListSkills -func newListCmd() *cobra.Command { +func NewListCmd() *cobra.Command { var projectFlag, globalFlag bool cmd := &cobra.Command{ diff --git a/experimental/aitools/cmd/list_test.go b/cmd/aitools/list_test.go similarity index 65% rename from experimental/aitools/cmd/list_test.go rename to cmd/aitools/list_test.go index 31390110c01..cb4ac9d9db8 100644 --- a/experimental/aitools/cmd/list_test.go +++ b/cmd/aitools/list_test.go @@ -10,7 +10,7 @@ import ( ) func TestListCommandExists(t *testing.T) { - cmd := newListCmd() + cmd := NewListCmd() assert.Equal(t, "list", cmd.Use) } @@ -25,7 +25,7 @@ func TestListCommandCallsListFn(t *testing.T) { } ctx := cmdio.MockDiscard(t.Context()) - cmd := newListCmd() + cmd := NewListCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -34,28 +34,9 @@ func TestListCommandCallsListFn(t *testing.T) { } func TestListCommandHasScopeFlags(t *testing.T) { - cmd := newListCmd() + cmd := NewListCmd() f := cmd.Flags().Lookup("project") require.NotNil(t, f, "--project flag should exist") f = cmd.Flags().Lookup("global") require.NotNil(t, f, "--global flag should exist") } - -func TestSkillsListDelegatesToListFn(t *testing.T) { - orig := listSkillsFn - t.Cleanup(func() { listSkillsFn = orig }) - - called := false - listSkillsFn = func(cmd *cobra.Command, scope string) error { - called = true - return nil - } - - ctx := cmdio.MockDiscard(t.Context()) - cmd := newSkillsListCmd() - cmd.SetContext(ctx) - - err := cmd.RunE(cmd, nil) - require.NoError(t, err) - assert.True(t, called) -} diff --git a/experimental/aitools/cmd/scope.go b/cmd/aitools/scope.go similarity index 97% rename from experimental/aitools/cmd/scope.go rename to cmd/aitools/scope.go index 8c6ce0f0130..3679b464113 100644 --- a/experimental/aitools/cmd/scope.go +++ b/cmd/aitools/scope.go @@ -8,7 +8,7 @@ import ( "path/filepath" "github.com/charmbracelet/huh" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/env" ) @@ -230,10 +230,10 @@ func scopeNotInstalledError(scope, verb, projectDir string, hasGlobal, hasProjec "no project-scoped skills found in the current directory.\n\n"+ "Project-scoped skills are detected based on your working directory.\n"+ "Make sure you are in the project root where you originally ran\n"+ - "'databricks experimental aitools install --project'.\n\n"+ + "'databricks aitools install --project'.\n\n"+ "Expected location: %s/", expectedPath) } else { - msg = "no globally-scoped skills installed. Run 'databricks experimental aitools install --global' to install" + msg = "no globally-scoped skills installed. Run 'databricks aitools install --global' to install" } hint := crossScopeHint(scope, verb, hasGlobal, hasProject) diff --git a/experimental/aitools/cmd/scope_test.go b/cmd/aitools/scope_test.go similarity index 99% rename from experimental/aitools/cmd/scope_test.go rename to cmd/aitools/scope_test.go index ecda25faade..371e91264c8 100644 --- a/experimental/aitools/cmd/scope_test.go +++ b/cmd/aitools/scope_test.go @@ -7,7 +7,7 @@ import ( "path/filepath" "testing" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/cmdio" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/experimental/aitools/cmd/uninstall.go b/cmd/aitools/uninstall.go similarity index 92% rename from experimental/aitools/cmd/uninstall.go rename to cmd/aitools/uninstall.go index 3eda84cfbc9..a13fc6de4db 100644 --- a/experimental/aitools/cmd/uninstall.go +++ b/cmd/aitools/uninstall.go @@ -1,11 +1,11 @@ package aitools import ( - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/installer" "github.com/spf13/cobra" ) -func newUninstallCmd() *cobra.Command { +func NewUninstallCmd() *cobra.Command { var skillsFlag string var projectFlag, globalFlag bool diff --git a/experimental/aitools/cmd/update.go b/cmd/aitools/update.go similarity index 93% rename from experimental/aitools/cmd/update.go rename to cmd/aitools/update.go index c5072d1fb19..7979c29f3b6 100644 --- a/experimental/aitools/cmd/update.go +++ b/cmd/aitools/update.go @@ -3,13 +3,13 @@ package aitools import ( "fmt" - "github.com/databricks/cli/experimental/aitools/lib/agents" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/agents" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/cmdio" "github.com/spf13/cobra" ) -func newUpdateCmd() *cobra.Command { +func NewUpdateCmd() *cobra.Command { var check, force, noNew bool var skillsFlag string var projectFlag, globalFlag bool diff --git a/experimental/aitools/cmd/version.go b/cmd/aitools/version.go similarity index 91% rename from experimental/aitools/cmd/version.go rename to cmd/aitools/version.go index fd3d086e95f..11ef2f03bcf 100644 --- a/experimental/aitools/cmd/version.go +++ b/cmd/aitools/version.go @@ -5,13 +5,13 @@ import ( "fmt" "strings" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/spf13/cobra" ) -func newVersionCmd() *cobra.Command { +func NewVersionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "version", Short: "Show installed AI skills version", @@ -41,7 +41,7 @@ func newVersionCmd() *cobra.Command { if globalState == nil && projectState == nil { cmdio.LogString(ctx, "No Databricks AI Tools components installed.") cmdio.LogString(ctx, "") - cmdio.LogString(ctx, "Run 'databricks experimental aitools install' to get started.") + cmdio.LogString(ctx, "Run 'databricks aitools install' to get started.") return nil } @@ -99,6 +99,6 @@ func printVersionLine(ctx context.Context, label string, state *installer.Instal cmdio.LogString(ctx, " Update available: v"+latestVersion) cmdio.LogString(ctx, " Last updated: "+state.LastUpdated.Format("2006-01-02")) cmdio.LogString(ctx, "") - cmdio.LogString(ctx, "Run 'databricks experimental aitools update' to update.") + cmdio.LogString(ctx, "Run 'databricks aitools update' to update.") } } diff --git a/experimental/aitools/cmd/version_test.go b/cmd/aitools/version_test.go similarity index 94% rename from experimental/aitools/cmd/version_test.go rename to cmd/aitools/version_test.go index d24f7e99f81..1192e8e3c86 100644 --- a/experimental/aitools/cmd/version_test.go +++ b/cmd/aitools/version_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/cmdio" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,6 +15,7 @@ import ( func TestVersionShowsBothScopes(t *testing.T) { tmp := t.TempDir() t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", tmp) t.Setenv("DATABRICKS_SKILLS_REF", "v0.1.0") // Create global state. @@ -51,7 +52,7 @@ func TestVersionShowsBothScopes(t *testing.T) { require.NoError(t, installer.SaveState(projectSkillsDir, projectState)) ctx, stderr := cmdio.NewTestContextWithStderr(t.Context()) - cmd := newVersionCmd() + cmd := NewVersionCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -70,6 +71,7 @@ func TestVersionShowsBothScopes(t *testing.T) { func TestVersionShowsSingleScopeWithoutQualifier(t *testing.T) { tmp := t.TempDir() t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", tmp) t.Setenv("DATABRICKS_SKILLS_REF", "v0.1.0") // Create only global state. @@ -90,7 +92,7 @@ func TestVersionShowsSingleScopeWithoutQualifier(t *testing.T) { t.Chdir(projectDir) ctx, stderr := cmdio.NewTestContextWithStderr(t.Context()) - cmd := newVersionCmd() + cmd := NewVersionCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) diff --git a/cmd/apps/init.go b/cmd/apps/init.go index bb0862dd07e..ba48738cb00 100644 --- a/cmd/apps/init.go +++ b/cmd/apps/init.go @@ -17,8 +17,8 @@ import ( "github.com/charmbracelet/huh" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/experimental/aitools/lib/agents" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/libs/aitools/agents" + "github.com/databricks/cli/libs/aitools/installer" "github.com/databricks/cli/libs/apps/generator" "github.com/databricks/cli/libs/apps/initializer" "github.com/databricks/cli/libs/apps/manifest" @@ -1269,7 +1269,7 @@ func runCreate(ctx context.Context, opts createOptions) error { // In flags mode, only print a hint — never prompt interactively. if flagsMode { if !agents.HasDatabricksSkillsInstalled(ctx) { - cmdio.LogString(ctx, "Tip: coding agents detected without Databricks skills. Run 'databricks experimental aitools skills install' to install them.") + cmdio.LogString(ctx, "Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.") } } else if err := agents.RecommendSkillsInstall(ctx, installer.InstallAllSkills); err != nil { log.Warnf(ctx, "Skills recommendation failed: %v", err) diff --git a/cmd/cmd.go b/cmd/cmd.go index 014471f7638..263c1da0c11 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -4,6 +4,7 @@ import ( "context" "strings" + aitoolscmd "github.com/databricks/cli/cmd/aitools" "github.com/databricks/cli/cmd/psql" ssh "github.com/databricks/cli/experimental/ssh/cmd" @@ -93,6 +94,7 @@ func New(ctx context.Context) *cobra.Command { } // Add other subcommands. + cli.AddCommand(aitoolscmd.NewAitoolsCmd()) cli.AddCommand(api.New()) cli.AddCommand(auth.New()) cli.AddCommand(completion.New()) diff --git a/experimental/aitools/README.md b/experimental/aitools/README.md index ec12ed10f7c..092552250c0 100644 --- a/experimental/aitools/README.md +++ b/experimental/aitools/README.md @@ -2,11 +2,12 @@ `databricks experimental aitools` is the remaining experimental surface for coding-agent workflows. +The skills-management commands (`install`, `update`, `uninstall`, `list`, `version`) have been promoted to top-level `databricks aitools`. The old paths under `databricks experimental aitools` keep working as deprecated aliases that print a notice pointing to the new path. + Current commands: - `databricks experimental aitools skills list` - `databricks experimental aitools skills install [skill-name]` -- `databricks experimental aitools install [skill-name]` - `databricks experimental aitools tools query` - `databricks experimental aitools tools discover-schema` - `databricks experimental aitools tools get-default-warehouse` @@ -17,8 +18,7 @@ Current commands: Current behavior: -- `skills install` installs Databricks skills for detected coding agents. -- `install` is a compatibility alias for `skills install`. +- `skills install` installs Databricks skills for detected coding agents (delegates to `databricks aitools install`). - `tools` exposes a small set of AI-oriented workspace helpers. - `tools query` accepts a single SQL or multiple SQLs in one invocation. Pass several positional arguments and/or repeat `--file` to run them in parallel diff --git a/experimental/aitools/cmd/aitools.go b/experimental/aitools/cmd/aitools.go index f037ac1a22e..c3e6227392b 100644 --- a/experimental/aitools/cmd/aitools.go +++ b/experimental/aitools/cmd/aitools.go @@ -1,6 +1,9 @@ package aitools import ( + "fmt" + + aitoolscmd "github.com/databricks/cli/cmd/aitools" "github.com/spf13/cobra" ) @@ -9,20 +12,30 @@ func NewAitoolsCmd() *cobra.Command { Use: "aitools", Hidden: true, Short: "Databricks AI Tools for coding agents", - Long: `Manage Databricks AI Tools. + Long: `Experimental coding-agent helpers. Skills management is at "databricks aitools".`, + } -Provides commands to: -- Install the AI tools in coding agents (install) -- Manage skills (skills) -- Access tools directly (tools)`, + // Backward-compat aliases for the skills-management commands. They live + // at top-level `databricks aitools ` now; the old paths still work + // but print a deprecation notice that points to the new path. + aliases := []struct { + name string + mk func() *cobra.Command + }{ + {"install", aitoolscmd.NewInstallCmd}, + {"update", aitoolscmd.NewUpdateCmd}, + {"uninstall", aitoolscmd.NewUninstallCmd}, + {"list", aitoolscmd.NewListCmd}, + {"version", aitoolscmd.NewVersionCmd}, + } + for _, a := range aliases { + sub := a.mk() + sub.Hidden = true + sub.Deprecated = fmt.Sprintf(`use "databricks aitools %s" instead.`, a.name) + cmd.AddCommand(sub) } - cmd.AddCommand(newInstallCmd()) - cmd.AddCommand(newUpdateCmd()) - cmd.AddCommand(newUninstallCmd()) - cmd.AddCommand(newListCmd()) - cmd.AddCommand(newVersionCmd()) - cmd.AddCommand(newSkillsCmd()) + cmd.AddCommand(aitoolscmd.NewLegacySkillsCmd()) cmd.AddCommand(newToolsCmd()) return cmd diff --git a/experimental/aitools/cmd/skills.go b/experimental/aitools/cmd/skills.go deleted file mode 100644 index 9995ff72a07..00000000000 --- a/experimental/aitools/cmd/skills.go +++ /dev/null @@ -1,101 +0,0 @@ -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/spf13/cobra" -) - -// Package-level vars for testability. -var ( - 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", - Hidden: true, - Short: "Manage Databricks skills for coding agents", - Long: `Manage Databricks skills that extend coding agents with Databricks-specific capabilities.`, - } - - // Subcommands delegate to the flat top-level commands. - cmd.AddCommand(newSkillsListCmd()) - cmd.AddCommand(newSkillsInstallCmd()) - - return cmd -} - -func newSkillsListCmd() *cobra.Command { - return &cobra.Command{ - Use: "list", - Short: "List available skills", - RunE: func(cmd *cobra.Command, args []string) error { - // Default to showing all scopes (empty scope = both). - return listSkillsFn(cmd, "") - }, - } -} - -func newSkillsInstallCmd() *cobra.Command { - var includeExperimental bool - - cmd := &cobra.Command{ - Use: "install [skill-name]", - Short: "Install Databricks skills for detected coding agents", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Delegate to the flat install command's logic. - installCmd := newInstallCmd() - installCmd.SetContext(cmd.Context()) - - var delegateArgs []string - if len(args) > 0 { - delegateArgs = append(delegateArgs, "--skills", args[0]) - } - if includeExperimental { - delegateArgs = append(delegateArgs, "--experimental") - } - installCmd.SetArgs(delegateArgs) - return installCmd.Execute() - }, - } - - cmd.Flags().BoolVar(&includeExperimental, "experimental", false, "Include experimental skills") - return cmd -} diff --git a/experimental/aitools/lib/agents/agents.go b/libs/aitools/agents/agents.go similarity index 100% rename from experimental/aitools/lib/agents/agents.go rename to libs/aitools/agents/agents.go diff --git a/experimental/aitools/lib/agents/recommend.go b/libs/aitools/agents/recommend.go similarity index 90% rename from experimental/aitools/lib/agents/recommend.go rename to libs/aitools/agents/recommend.go index bf10c67bfd9..de906752316 100644 --- a/experimental/aitools/lib/agents/recommend.go +++ b/libs/aitools/agents/recommend.go @@ -15,7 +15,7 @@ func RecommendSkillsInstall(ctx context.Context, installFn func(context.Context) } if !cmdio.IsPromptSupported(ctx) { - cmdio.LogString(ctx, "Tip: coding agents detected without Databricks skills. Run 'databricks experimental aitools skills install' to install them.") + cmdio.LogString(ctx, "Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.") return nil } diff --git a/experimental/aitools/lib/agents/recommend_test.go b/libs/aitools/agents/recommend_test.go similarity index 91% rename from experimental/aitools/lib/agents/recommend_test.go rename to libs/aitools/agents/recommend_test.go index c2c27699212..ede6d4eed12 100644 --- a/experimental/aitools/lib/agents/recommend_test.go +++ b/libs/aitools/agents/recommend_test.go @@ -17,6 +17,7 @@ func noopInstall(context.Context) error { return nil } func TestRecommendSkillsInstallSkipsWhenSkillsExist(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) + t.Setenv("USERPROFILE", tmpDir) // Skills must be in canonical location to be detected. require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, CanonicalSkillsDir, "databricks"), 0o755)) @@ -39,7 +40,9 @@ func TestRecommendSkillsInstallSkipsWhenSkillsExist(t *testing.T) { } func TestRecommendSkillsInstallSkipsWhenNoAgents(t *testing.T) { - t.Setenv("HOME", t.TempDir()) + tmp := t.TempDir() + t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", tmp) origRegistry := Registry Registry = []Agent{} @@ -53,6 +56,7 @@ func TestRecommendSkillsInstallSkipsWhenNoAgents(t *testing.T) { func TestRecommendSkillsInstallNonInteractive(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) + t.Setenv("USERPROFILE", tmpDir) origRegistry := Registry Registry = []Agent{ @@ -67,12 +71,13 @@ func TestRecommendSkillsInstallNonInteractive(t *testing.T) { ctx, stderr := cmdio.NewTestContextWithStderr(t.Context()) err := RecommendSkillsInstall(ctx, noopInstall) require.NoError(t, err) - assert.Contains(t, stderr.String(), "databricks experimental aitools skills install") + assert.Contains(t, stderr.String(), "databricks aitools install") } func TestRecommendSkillsInstallInteractiveDecline(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) + t.Setenv("USERPROFILE", tmpDir) origRegistry := Registry Registry = []Agent{ diff --git a/experimental/aitools/lib/agents/skills.go b/libs/aitools/agents/skills.go similarity index 100% rename from experimental/aitools/lib/agents/skills.go rename to libs/aitools/agents/skills.go diff --git a/experimental/aitools/lib/agents/skills_test.go b/libs/aitools/agents/skills_test.go similarity index 95% rename from experimental/aitools/lib/agents/skills_test.go rename to libs/aitools/agents/skills_test.go index 09192d721c9..e7b3d57d081 100644 --- a/experimental/aitools/lib/agents/skills_test.go +++ b/libs/aitools/agents/skills_test.go @@ -21,6 +21,7 @@ func TestHasDatabricksSkillsInstalledNoAgents(t *testing.T) { func TestHasDatabricksSkillsInstalledCanonicalOnly(t *testing.T) { tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) + t.Setenv("USERPROFILE", tmpHome) require.NoError(t, os.MkdirAll(filepath.Join(tmpHome, CanonicalSkillsDir, "databricks"), 0o755)) origRegistry := Registry @@ -39,6 +40,7 @@ func TestHasDatabricksSkillsInstalledCanonicalOnly(t *testing.T) { func TestHasDatabricksSkillsInstalledIgnoresAgentDir(t *testing.T) { tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) + t.Setenv("USERPROFILE", tmpHome) // Skills in agent dir only (e.g., installed by another tool) should not count. agentDir := filepath.Join(tmpHome, ".claude") require.NoError(t, os.MkdirAll(filepath.Join(agentDir, "skills", "databricks"), 0o755)) @@ -59,6 +61,7 @@ func TestHasDatabricksSkillsInstalledIgnoresAgentDir(t *testing.T) { func TestHasDatabricksSkillsInstalledWithOnlyNonDatabricksSkills(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) + t.Setenv("USERPROFILE", tmpDir) // Non-databricks skills should not count. require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "skills", "mcp-builder"), 0o755)) require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "skills", "rust-webapp"), 0o755)) @@ -79,6 +82,7 @@ func TestHasDatabricksSkillsInstalledWithOnlyNonDatabricksSkills(t *testing.T) { func TestHasDatabricksSkillsInstalledNoSkillsDir(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) + t.Setenv("USERPROFILE", tmpDir) origRegistry := Registry Registry = []Agent{ @@ -96,6 +100,7 @@ func TestHasDatabricksSkillsInstalledNoSkillsDir(t *testing.T) { func TestHasDatabricksSkillsInstalledCustomSubdirNotChecked(t *testing.T) { tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) + t.Setenv("USERPROFILE", tmpHome) // Skills in agent's custom subdir should not count — only canonical matters. require.NoError(t, os.MkdirAll(filepath.Join(tmpHome, ".gemini", "antigravity", "global_skills", "databricks"), 0o755)) @@ -116,6 +121,7 @@ func TestHasDatabricksSkillsInstalledCustomSubdirNotChecked(t *testing.T) { func TestHasDatabricksSkillsInstalledDatabricksAppsCanonical(t *testing.T) { tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) + t.Setenv("USERPROFILE", tmpHome) // databricks-apps prefix should match in canonical location. require.NoError(t, os.MkdirAll(filepath.Join(tmpHome, CanonicalSkillsDir, "databricks-apps"), 0o755)) @@ -138,6 +144,7 @@ func TestHasDatabricksSkillsInstalledDatabricksAppsCanonical(t *testing.T) { func TestHasDatabricksSkillsInstalledLegacyPath(t *testing.T) { tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) + t.Setenv("USERPROFILE", tmpHome) // Skills only in the legacy location should still be detected. require.NoError(t, os.MkdirAll(filepath.Join(tmpHome, legacySkillsDir, "databricks"), 0o755)) diff --git a/experimental/aitools/lib/installer/installer.go b/libs/aitools/installer/installer.go similarity index 98% rename from experimental/aitools/lib/installer/installer.go rename to libs/aitools/installer/installer.go index dbf511720bf..b905202d4c5 100644 --- a/experimental/aitools/lib/installer/installer.go +++ b/libs/aitools/installer/installer.go @@ -14,8 +14,8 @@ import ( "strings" "time" - "github.com/databricks/cli/experimental/aitools/lib/agents" "github.com/databricks/cli/internal/build" + "github.com/databricks/cli/libs/aitools/agents" "github.com/databricks/cli/libs/clicompat" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/env" @@ -164,7 +164,7 @@ func InstallSkillsForAgents(ctx context.Context, src ManifestSource, targetAgent if state == nil && scope == ScopeGlobal { isLegacy := checkLegacyInstall(ctx, baseDir) if isLegacy && len(opts.SpecificSkills) > 0 { - return errors.New("legacy install detected without state tracking; run 'databricks experimental aitools install' (without a skill name) first to rebuild state") + return errors.New("legacy install detected without state tracking; run 'databricks aitools install' (without a skill name) first to rebuild state") } } @@ -342,7 +342,7 @@ func printNoAgentsDetected(ctx context.Context) { // Returns true if a legacy install was detected. func checkLegacyInstall(ctx context.Context, globalDir string) bool { if hasSkillsOnDisk(globalDir) { - cmdio.LogString(ctx, "Found skills installed before state tracking was added. Run 'databricks experimental aitools install' to refresh.") + cmdio.LogString(ctx, "Found skills installed before state tracking was added. Run 'databricks aitools install' to refresh.") return true } homeDir, err := env.UserHomeDir(ctx) @@ -351,7 +351,7 @@ func checkLegacyInstall(ctx context.Context, globalDir string) bool { } legacyDir := filepath.Join(homeDir, ".databricks", "agent-skills") if hasSkillsOnDisk(legacyDir) { - cmdio.LogString(ctx, "Found skills installed before state tracking was added. Run 'databricks experimental aitools install' to refresh.") + cmdio.LogString(ctx, "Found skills installed before state tracking was added. Run 'databricks aitools install' to refresh.") return true } return false diff --git a/experimental/aitools/lib/installer/installer_test.go b/libs/aitools/installer/installer_test.go similarity index 99% rename from experimental/aitools/lib/installer/installer_test.go rename to libs/aitools/installer/installer_test.go index ab64f8605bf..3ace56e952f 100644 --- a/experimental/aitools/lib/installer/installer_test.go +++ b/libs/aitools/installer/installer_test.go @@ -9,8 +9,8 @@ import ( "path/filepath" "testing" - "github.com/databricks/cli/experimental/aitools/lib/agents" "github.com/databricks/cli/internal/build" + "github.com/databricks/cli/libs/aitools/agents" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/stretchr/testify/assert" @@ -72,6 +72,7 @@ func setupTestHome(t *testing.T) string { t.Helper() tmp := t.TempDir() t.Setenv("HOME", tmp) + t.Setenv("USERPROFILE", tmp) // Create agent config dir so the agent is "detected". require.NoError(t, os.MkdirAll(filepath.Join(tmp, ".test-agent"), 0o755)) return tmp diff --git a/experimental/aitools/lib/installer/source.go b/libs/aitools/installer/source.go similarity index 100% rename from experimental/aitools/lib/installer/source.go rename to libs/aitools/installer/source.go diff --git a/experimental/aitools/lib/installer/state.go b/libs/aitools/installer/state.go similarity index 100% rename from experimental/aitools/lib/installer/state.go rename to libs/aitools/installer/state.go diff --git a/experimental/aitools/lib/installer/state_test.go b/libs/aitools/installer/state_test.go similarity index 100% rename from experimental/aitools/lib/installer/state_test.go rename to libs/aitools/installer/state_test.go diff --git a/experimental/aitools/lib/installer/uninstall.go b/libs/aitools/installer/uninstall.go similarity index 97% rename from experimental/aitools/lib/installer/uninstall.go rename to libs/aitools/installer/uninstall.go index 1ad9f58511c..da4cc6378c0 100644 --- a/experimental/aitools/lib/installer/uninstall.go +++ b/libs/aitools/installer/uninstall.go @@ -9,7 +9,7 @@ import ( "path/filepath" "strings" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/libs/aitools/agents" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" ) @@ -54,7 +54,7 @@ func UninstallSkillsOpts(ctx context.Context, opts UninstallOptions) error { if state == nil { if scope == ScopeGlobal && hasLegacyInstall(ctx, baseDir) { - return errors.New("found skills from a previous install without state tracking; run 'databricks experimental aitools install' first, then uninstall") + return errors.New("found skills from a previous install without state tracking; run 'databricks aitools install' first, then uninstall") } return errors.New("no skills installed") } diff --git a/experimental/aitools/lib/installer/uninstall_test.go b/libs/aitools/installer/uninstall_test.go similarity index 99% rename from experimental/aitools/lib/installer/uninstall_test.go rename to libs/aitools/installer/uninstall_test.go index 444fb773af7..3dc8e03af35 100644 --- a/experimental/aitools/lib/installer/uninstall_test.go +++ b/libs/aitools/installer/uninstall_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/libs/aitools/agents" "github.com/databricks/cli/libs/cmdio" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/experimental/aitools/lib/installer/update.go b/libs/aitools/installer/update.go similarity index 96% rename from experimental/aitools/lib/installer/update.go rename to libs/aitools/installer/update.go index 720359d3340..9965ff7fc63 100644 --- a/experimental/aitools/lib/installer/update.go +++ b/libs/aitools/installer/update.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "github.com/databricks/cli/experimental/aitools/lib/agents" "github.com/databricks/cli/internal/build" + "github.com/databricks/cli/libs/aitools/agents" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/env" "github.com/databricks/cli/libs/log" @@ -76,9 +76,9 @@ func UpdateSkills(ctx context.Context, src ManifestSource, targetAgents []*agent if state == nil { if scope == ScopeGlobal && hasLegacyInstall(ctx, baseDir) { - return nil, errors.New("found skills from a previous install without state tracking; run 'databricks experimental aitools install' to refresh before updating") + return nil, errors.New("found skills from a previous install without state tracking; run 'databricks aitools install' to refresh before updating") } - return nil, errors.New("no skills installed. Run 'databricks experimental aitools install' to install") + return nil, errors.New("no skills installed. Run 'databricks aitools install' to install") } latestTag, explicit, err := GetSkillsRef(ctx) diff --git a/experimental/aitools/lib/installer/update_test.go b/libs/aitools/installer/update_test.go similarity index 99% rename from experimental/aitools/lib/installer/update_test.go rename to libs/aitools/installer/update_test.go index 3272a77babe..a82be1d0289 100644 --- a/experimental/aitools/lib/installer/update_test.go +++ b/libs/aitools/installer/update_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/libs/aitools/agents" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/stretchr/testify/assert" @@ -24,7 +24,7 @@ func TestUpdateNoStateReturnsInstallHint(t *testing.T) { _, err := UpdateSkills(ctx, src, nil, UpdateOptions{}) require.Error(t, err) assert.Contains(t, err.Error(), "no skills installed") - assert.Contains(t, err.Error(), "databricks experimental aitools install") + assert.Contains(t, err.Error(), "databricks aitools install") } func TestUpdateLegacyInstallDetected(t *testing.T) {