From 8c97faced60c8c16bf0ef07aaffa58135855cdbb Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 10:01:33 +0200 Subject: [PATCH 1/7] aitools: promote skills-management surface to top-level Move install/update/uninstall/list/version source files (and the agents/installer libs they depend on) from experimental/aitools/ to a new top-level aitools/ package. Wire `databricks aitools ` at top-level. The tools subtree (query, discover-schema, get-default-warehouse, statement) and the hidden skills alias group stay under experimental/aitools/ where their stability story still lives. The old paths under `databricks experimental aitools install/update/uninstall/list/version` and `databricks experimental aitools skills install/list` keep working as silent backward-compat aliases (no deprecation warnings). Co-authored-by: Isaac --- .agent/skills/pr-checklist/SKILL.md | 4 +- .github/OWNERS | 3 +- NEXT_CHANGELOG.md | 2 + Taskfile.yml | 5 +- acceptance/help/output.txt | 1 + aitools/README.md | 15 ++ aitools/cmd/aitools.go | 25 +++ .../aitools => aitools}/cmd/flags.go | 0 .../aitools => aitools}/cmd/flags_test.go | 0 .../aitools => aitools}/cmd/install.go | 49 +++++- .../aitools => aitools}/cmd/install_test.go | 166 ++++-------------- {experimental/aitools => aitools}/cmd/list.go | 10 +- .../aitools => aitools}/cmd/list_test.go | 31 +--- .../aitools => aitools}/cmd/scope.go | 2 +- .../aitools => aitools}/cmd/scope_test.go | 2 +- .../aitools => aitools}/cmd/uninstall.go | 4 +- .../aitools => aitools}/cmd/update.go | 6 +- .../aitools => aitools}/cmd/version.go | 4 +- .../aitools => aitools}/cmd/version_test.go | 6 +- .../aitools => aitools}/lib/agents/agents.go | 0 .../lib/agents/recommend.go | 0 .../lib/agents/recommend_test.go | 0 .../aitools => aitools}/lib/agents/skills.go | 0 .../lib/agents/skills_test.go | 0 .../lib/installer/SKILLS_VERSION | 0 .../lib/installer/installer.go | 2 +- .../lib/installer/installer_test.go | 2 +- .../lib/installer/source.go | 0 .../lib/installer/state.go | 0 .../lib/installer/state_test.go | 0 .../lib/installer/uninstall.go | 2 +- .../lib/installer/uninstall_test.go | 2 +- .../lib/installer/update.go | 2 +- .../lib/installer/update_test.go | 2 +- .../lib/installer/version.go | 0 cmd/apps/init.go | 4 +- cmd/cmd.go | 2 + experimental/aitools/README.md | 6 +- experimental/aitools/cmd/aitools.go | 22 ++- experimental/aitools/cmd/skills.go | 51 +----- experimental/aitools/cmd/skills_test.go | 158 +++++++++++++++++ 41 files changed, 347 insertions(+), 243 deletions(-) create mode 100644 aitools/README.md create mode 100644 aitools/cmd/aitools.go rename {experimental/aitools => aitools}/cmd/flags.go (100%) rename {experimental/aitools => aitools}/cmd/flags_test.go (100%) rename {experimental/aitools => aitools}/cmd/install.go (74%) rename {experimental/aitools => aitools}/cmd/install_test.go (75%) rename {experimental/aitools => aitools}/cmd/list.go (95%) rename {experimental/aitools => aitools}/cmd/list_test.go (55%) rename {experimental/aitools => aitools}/cmd/scope.go (99%) rename {experimental/aitools => aitools}/cmd/scope_test.go (99%) rename {experimental/aitools => aitools}/cmd/uninstall.go (92%) rename {experimental/aitools => aitools}/cmd/update.go (93%) rename {experimental/aitools => aitools}/cmd/version.go (96%) rename {experimental/aitools => aitools}/cmd/version_test.go (96%) rename {experimental/aitools => aitools}/lib/agents/agents.go (100%) rename {experimental/aitools => aitools}/lib/agents/recommend.go (100%) rename {experimental/aitools => aitools}/lib/agents/recommend_test.go (100%) rename {experimental/aitools => aitools}/lib/agents/skills.go (100%) rename {experimental/aitools => aitools}/lib/agents/skills_test.go (100%) rename {experimental/aitools => aitools}/lib/installer/SKILLS_VERSION (100%) rename {experimental/aitools => aitools}/lib/installer/installer.go (99%) rename {experimental/aitools => aitools}/lib/installer/installer_test.go (99%) rename {experimental/aitools => aitools}/lib/installer/source.go (100%) rename {experimental/aitools => aitools}/lib/installer/state.go (100%) rename {experimental/aitools => aitools}/lib/installer/state_test.go (100%) rename {experimental/aitools => aitools}/lib/installer/uninstall.go (99%) rename {experimental/aitools => aitools}/lib/installer/uninstall_test.go (99%) rename {experimental/aitools => aitools}/lib/installer/update.go (99%) rename {experimental/aitools => aitools}/lib/installer/update_test.go (99%) rename {experimental/aitools => aitools}/lib/installer/version.go (100%) create mode 100644 experimental/aitools/cmd/skills_test.go diff --git a/.agent/skills/pr-checklist/SKILL.md b/.agent/skills/pr-checklist/SKILL.md index 3eae258d6e7..349df461ea8 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 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 7cae525465a..d21de6c009b 100644 --- a/.github/OWNERS +++ b/.github/OWNERS @@ -59,5 +59,6 @@ # Internal /internal/ team:platform -# Experimental +# AI tools +/aitools/ team:eng-apps-devex @lennartkats-db /experimental/aitools/ team:eng-apps-devex @lennartkats-db diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 00152d550ea..5f7601a57cc 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,6 +4,8 @@ ### CLI +* Promote the aitools skills-management surface (`install`, `update`, `uninstall`, `list`, `version`) from `databricks experimental aitools` to top-level `databricks aitools`. The old paths under `databricks experimental aitools` continue to work as silent backward-compat aliases. The `tools` subtree (`query`, `discover-schema`, `get-default-warehouse`, `statement …`) and the `skills` alias group remain under `databricks experimental aitools`. + ### Bundles ### Dependency updates diff --git a/Taskfile.yml b/Taskfile.yml index 912b3f666a3..11ebe3261ee 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -606,8 +606,9 @@ 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: + - aitools/** - experimental/aitools/** - acceptance/apps/** - "{{.EMBED_SOURCES}}" @@ -616,7 +617,7 @@ tasks: {{.GO_TOOL}} gotestsum \ --format ${GOTESTSUM_FORMAT:-pkgname-and-test-fails} \ --no-summary=skipped \ - --packages ./experimental/aitools/... \ + --packages "./aitools/... ./experimental/aitools/..." \ -- -timeout=${LOCAL_TIMEOUT:-30m} - | {{.GO_TOOL}} gotestsum \ diff --git a/acceptance/help/output.txt b/acceptance/help/output.txt index d9f379f5bbf..159eca916b3 100644 --- a/acceptance/help/output.txt +++ b/acceptance/help/output.txt @@ -167,6 +167,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/aitools/README.md b/aitools/README.md new file mode 100644 index 00000000000..3aec1e8df6e --- /dev/null +++ b/aitools/README.md @@ -0,0 +1,15 @@ +# Databricks AI Tools + +`databricks aitools` installs and manages Databricks skills for detected coding agents. + +## Commands + +- `databricks aitools install [skill-name]` +- `databricks aitools update` +- `databricks aitools uninstall` +- `databricks aitools list` +- `databricks aitools version` + +Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity. + +The `tools` subtree (`query`, `discover-schema`, `get-default-warehouse`, `statement …`) and the `skills` alias group remain under `databricks experimental aitools` while their stability story is still in flux. diff --git a/aitools/cmd/aitools.go b/aitools/cmd/aitools.go new file mode 100644 index 00000000000..22fe0ad94fd --- /dev/null +++ b/aitools/cmd/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: `Manage Databricks AI Tools. + +Provides commands to install, update, and manage Databricks skills for +detected coding 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/aitools/cmd/flags.go similarity index 100% rename from experimental/aitools/cmd/flags.go rename to aitools/cmd/flags.go diff --git a/experimental/aitools/cmd/flags_test.go b/aitools/cmd/flags_test.go similarity index 100% rename from experimental/aitools/cmd/flags_test.go rename to aitools/cmd/flags_test.go diff --git a/experimental/aitools/cmd/install.go b/aitools/cmd/install.go similarity index 74% rename from experimental/aitools/cmd/install.go rename to aitools/cmd/install.go index b6e87d68b1e..2a2aa07ea92 100644 --- a/experimental/aitools/cmd/install.go +++ b/aitools/cmd/install.go @@ -6,14 +6,53 @@ 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/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/installer" "github.com/databricks/cli/libs/cmdio" "github.com/fatih/color" "github.com/spf13/cobra" ) -func newInstallCmd() *cobra.Command { +// PromptAgentSelection and InstallSkillsForAgentsFn are package-level for +// testability. They are exported so wrappers in other packages +// (experimental/aitools/cmd/skills.go) can override them in tests. +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 @@ -65,7 +104,7 @@ Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Anti case len(detected) == 1: targetAgents = detected case cmdio.IsPromptSupported(ctx): - targetAgents, err = promptAgentSelection(ctx, detected) + targetAgents, err = PromptAgentSelection(ctx, detected) if err != nil { return err } @@ -84,7 +123,7 @@ Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Anti installer.PrintInstallingFor(ctx, targetAgents) src := &installer.GitHubManifestSource{} - return installSkillsForAgentsFn(ctx, src, targetAgents, opts) + return InstallSkillsForAgentsFn(ctx, src, targetAgents, opts) }, } diff --git a/experimental/aitools/cmd/install_test.go b/aitools/cmd/install_test.go similarity index 75% rename from experimental/aitools/cmd/install_test.go rename to aitools/cmd/install_test.go index 38639705ea4..54685387f6c 100644 --- a/experimental/aitools/cmd/install_test.go +++ b/aitools/cmd/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/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/installer" "github.com/databricks/cli/libs/cmdio" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,11 +16,11 @@ import ( func setupInstallMock(t *testing.T) *[]installCall { t.Helper() - orig := installSkillsForAgentsFn - t.Cleanup(func() { installSkillsForAgentsFn = orig }) + orig := InstallSkillsForAgentsFn + t.Cleanup(func() { InstallSkillsForAgentsFn = orig }) var calls []installCall - installSkillsForAgentsFn = func(_ context.Context, _ installer.ManifestSource, targetAgents []*agents.Agent, opts installer.InstallOptions) error { + 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 @@ -64,7 +64,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 +80,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 +96,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 +112,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 +128,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 +145,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"}) @@ -161,11 +161,11 @@ func TestInstallInteractivePrompt(t *testing.T) { calls := setupInstallMock(t) setupScopeMock(t, installer.ScopeGlobal) - origPrompt := promptAgentSelection - t.Cleanup(func() { promptAgentSelection = origPrompt }) + origPrompt := PromptAgentSelection + t.Cleanup(func() { PromptAgentSelection = origPrompt }) promptCalled := false - promptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) { + PromptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) { promptCalled = true return detected[:1], nil } @@ -185,7 +185,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) @@ -200,17 +200,17 @@ func TestInstallNonInteractiveUsesAllAgents(t *testing.T) { setupTestAgents(t) calls := setupInstallMock(t) - origPrompt := promptAgentSelection - t.Cleanup(func() { promptAgentSelection = origPrompt }) + origPrompt := PromptAgentSelection + t.Cleanup(func() { PromptAgentSelection = origPrompt }) promptCalled := false - promptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) { + PromptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) { promptCalled = true return detected, nil } ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -228,7 +228,7 @@ func TestInstallNoAgentsDetected(t *testing.T) { calls := setupInstallMock(t) ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) err := cmd.RunE(cmd, nil) @@ -240,17 +240,17 @@ func TestInstallAgentsFlagSkipsPrompt(t *testing.T) { setupTestAgents(t) calls := setupInstallMock(t) - origPrompt := promptAgentSelection - t.Cleanup(func() { promptAgentSelection = origPrompt }) + origPrompt := PromptAgentSelection + t.Cleanup(func() { PromptAgentSelection = origPrompt }) promptCalled := false - promptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) { + PromptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) { promptCalled = true return detected, nil } ctx := cmdio.MockDiscard(t.Context()) - cmd := newInstallCmd() + cmd := NewInstallCmd() cmd.SetContext(ctx) cmd.SetArgs([]string{"--agents", "claude-code,cursor"}) @@ -262,101 +262,9 @@ 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.SilenceErrors = true @@ -369,7 +277,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 +290,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 +303,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 +316,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 +366,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 +382,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 +398,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 +414,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) @@ -522,9 +430,9 @@ func TestInstallNoFlagInteractiveShowsScopePrompt(t *testing.T) { scopePromptCalled := setupScopeMock(t, installer.ScopeProject) // Also mock agent prompt since interactive mode triggers it. - origPrompt := promptAgentSelection - t.Cleanup(func() { promptAgentSelection = origPrompt }) - promptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) { + origPrompt := PromptAgentSelection + t.Cleanup(func() { PromptAgentSelection = origPrompt }) + PromptAgentSelection = func(_ context.Context, detected []*agents.Agent) ([]*agents.Agent, error) { return detected, nil } @@ -543,7 +451,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/experimental/aitools/cmd/list.go b/aitools/cmd/list.go similarity index 95% rename from experimental/aitools/cmd/list.go rename to aitools/cmd/list.go index 1be1538c9a0..2d236c688dc 100644 --- a/experimental/aitools/cmd/list.go +++ b/aitools/cmd/list.go @@ -8,17 +8,17 @@ import ( "strings" "text/tabwriter" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/aitools/lib/installer" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/spf13/cobra" ) -// listSkillsFn is the function used to render the skills list. +// ListSkillsFn is the function used to render the skills list. // It is a package-level var so tests can replace the data-fetching layer. -var listSkillsFn = defaultListSkills +var ListSkillsFn = defaultListSkills -func newListCmd() *cobra.Command { +func NewListCmd() *cobra.Command { var projectFlag, globalFlag bool cmd := &cobra.Command{ @@ -36,7 +36,7 @@ func newListCmd() *cobra.Command { } else if globalFlag { scope = installer.ScopeGlobal } - return listSkillsFn(cmd, scope) + return ListSkillsFn(cmd, scope) }, } diff --git a/experimental/aitools/cmd/list_test.go b/aitools/cmd/list_test.go similarity index 55% rename from experimental/aitools/cmd/list_test.go rename to aitools/cmd/list_test.go index 31390110c01..f564d4c8bce 100644 --- a/experimental/aitools/cmd/list_test.go +++ b/aitools/cmd/list_test.go @@ -10,22 +10,22 @@ import ( ) func TestListCommandExists(t *testing.T) { - cmd := newListCmd() + cmd := NewListCmd() assert.Equal(t, "list", cmd.Use) } func TestListCommandCallsListFn(t *testing.T) { - orig := listSkillsFn - t.Cleanup(func() { listSkillsFn = orig }) + orig := ListSkillsFn + t.Cleanup(func() { ListSkillsFn = orig }) called := false - listSkillsFn = func(cmd *cobra.Command, scope string) error { + ListSkillsFn = func(cmd *cobra.Command, scope string) error { called = true return nil } 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/aitools/cmd/scope.go similarity index 99% rename from experimental/aitools/cmd/scope.go rename to aitools/cmd/scope.go index 8c6ce0f0130..5277ed671de 100644 --- a/experimental/aitools/cmd/scope.go +++ b/aitools/cmd/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/aitools/lib/installer" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/env" ) diff --git a/experimental/aitools/cmd/scope_test.go b/aitools/cmd/scope_test.go similarity index 99% rename from experimental/aitools/cmd/scope_test.go rename to aitools/cmd/scope_test.go index ecda25faade..80e5a976a9e 100644 --- a/experimental/aitools/cmd/scope_test.go +++ b/aitools/cmd/scope_test.go @@ -7,7 +7,7 @@ import ( "path/filepath" "testing" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/aitools/lib/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/aitools/cmd/uninstall.go similarity index 92% rename from experimental/aitools/cmd/uninstall.go rename to aitools/cmd/uninstall.go index 3eda84cfbc9..e450f48b930 100644 --- a/experimental/aitools/cmd/uninstall.go +++ b/aitools/cmd/uninstall.go @@ -1,11 +1,11 @@ package aitools import ( - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/aitools/lib/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/aitools/cmd/update.go similarity index 93% rename from experimental/aitools/cmd/update.go rename to aitools/cmd/update.go index c5072d1fb19..127dd0f7748 100644 --- a/experimental/aitools/cmd/update.go +++ b/aitools/cmd/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/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/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/aitools/cmd/version.go similarity index 96% rename from experimental/aitools/cmd/version.go rename to aitools/cmd/version.go index 67c38fec42a..a0eb8eb59a7 100644 --- a/experimental/aitools/cmd/version.go +++ b/aitools/cmd/version.go @@ -5,12 +5,12 @@ import ( "fmt" "strings" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/aitools/lib/installer" "github.com/databricks/cli/libs/cmdio" "github.com/spf13/cobra" ) -func newVersionCmd() *cobra.Command { +func NewVersionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "version", Short: "Show installed AI skills version", diff --git a/experimental/aitools/cmd/version_test.go b/aitools/cmd/version_test.go similarity index 96% rename from experimental/aitools/cmd/version_test.go rename to aitools/cmd/version_test.go index d24f7e99f81..f1980465e39 100644 --- a/experimental/aitools/cmd/version_test.go +++ b/aitools/cmd/version_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/databricks/cli/experimental/aitools/lib/installer" + "github.com/databricks/cli/aitools/lib/installer" "github.com/databricks/cli/libs/cmdio" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -51,7 +51,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) @@ -90,7 +90,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/experimental/aitools/lib/agents/agents.go b/aitools/lib/agents/agents.go similarity index 100% rename from experimental/aitools/lib/agents/agents.go rename to aitools/lib/agents/agents.go diff --git a/experimental/aitools/lib/agents/recommend.go b/aitools/lib/agents/recommend.go similarity index 100% rename from experimental/aitools/lib/agents/recommend.go rename to aitools/lib/agents/recommend.go diff --git a/experimental/aitools/lib/agents/recommend_test.go b/aitools/lib/agents/recommend_test.go similarity index 100% rename from experimental/aitools/lib/agents/recommend_test.go rename to aitools/lib/agents/recommend_test.go diff --git a/experimental/aitools/lib/agents/skills.go b/aitools/lib/agents/skills.go similarity index 100% rename from experimental/aitools/lib/agents/skills.go rename to aitools/lib/agents/skills.go diff --git a/experimental/aitools/lib/agents/skills_test.go b/aitools/lib/agents/skills_test.go similarity index 100% rename from experimental/aitools/lib/agents/skills_test.go rename to aitools/lib/agents/skills_test.go diff --git a/experimental/aitools/lib/installer/SKILLS_VERSION b/aitools/lib/installer/SKILLS_VERSION similarity index 100% rename from experimental/aitools/lib/installer/SKILLS_VERSION rename to aitools/lib/installer/SKILLS_VERSION diff --git a/experimental/aitools/lib/installer/installer.go b/aitools/lib/installer/installer.go similarity index 99% rename from experimental/aitools/lib/installer/installer.go rename to aitools/lib/installer/installer.go index 982df0c1631..5995a4f35d7 100644 --- a/experimental/aitools/lib/installer/installer.go +++ b/aitools/lib/installer/installer.go @@ -14,7 +14,7 @@ import ( "strings" "time" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/agents" "github.com/databricks/cli/internal/build" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/env" diff --git a/experimental/aitools/lib/installer/installer_test.go b/aitools/lib/installer/installer_test.go similarity index 99% rename from experimental/aitools/lib/installer/installer_test.go rename to aitools/lib/installer/installer_test.go index b769143906d..8bd926637e2 100644 --- a/experimental/aitools/lib/installer/installer_test.go +++ b/aitools/lib/installer/installer_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/agents" "github.com/databricks/cli/internal/build" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" diff --git a/experimental/aitools/lib/installer/source.go b/aitools/lib/installer/source.go similarity index 100% rename from experimental/aitools/lib/installer/source.go rename to aitools/lib/installer/source.go diff --git a/experimental/aitools/lib/installer/state.go b/aitools/lib/installer/state.go similarity index 100% rename from experimental/aitools/lib/installer/state.go rename to aitools/lib/installer/state.go diff --git a/experimental/aitools/lib/installer/state_test.go b/aitools/lib/installer/state_test.go similarity index 100% rename from experimental/aitools/lib/installer/state_test.go rename to aitools/lib/installer/state_test.go diff --git a/experimental/aitools/lib/installer/uninstall.go b/aitools/lib/installer/uninstall.go similarity index 99% rename from experimental/aitools/lib/installer/uninstall.go rename to aitools/lib/installer/uninstall.go index 1ad9f58511c..1fa4dc3b47d 100644 --- a/experimental/aitools/lib/installer/uninstall.go +++ b/aitools/lib/installer/uninstall.go @@ -9,7 +9,7 @@ import ( "path/filepath" "strings" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/agents" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" ) diff --git a/experimental/aitools/lib/installer/uninstall_test.go b/aitools/lib/installer/uninstall_test.go similarity index 99% rename from experimental/aitools/lib/installer/uninstall_test.go rename to aitools/lib/installer/uninstall_test.go index 6c7589f6f29..cf7ada3a9a7 100644 --- a/experimental/aitools/lib/installer/uninstall_test.go +++ b/aitools/lib/installer/uninstall_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/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/aitools/lib/installer/update.go similarity index 99% rename from experimental/aitools/lib/installer/update.go rename to aitools/lib/installer/update.go index 663ad5e908e..47f7542f36b 100644 --- a/experimental/aitools/lib/installer/update.go +++ b/aitools/lib/installer/update.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/agents" "github.com/databricks/cli/internal/build" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/env" diff --git a/experimental/aitools/lib/installer/update_test.go b/aitools/lib/installer/update_test.go similarity index 99% rename from experimental/aitools/lib/installer/update_test.go rename to aitools/lib/installer/update_test.go index 97e3014be65..45c4cb6a135 100644 --- a/experimental/aitools/lib/installer/update_test.go +++ b/aitools/lib/installer/update_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - "github.com/databricks/cli/experimental/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/agents" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/stretchr/testify/assert" diff --git a/experimental/aitools/lib/installer/version.go b/aitools/lib/installer/version.go similarity index 100% rename from experimental/aitools/lib/installer/version.go rename to aitools/lib/installer/version.go diff --git a/cmd/apps/init.go b/cmd/apps/init.go index 6f7269e6fd0..fdc2b2c4ddf 100644 --- a/cmd/apps/init.go +++ b/cmd/apps/init.go @@ -16,9 +16,9 @@ import ( "text/template" "github.com/charmbracelet/huh" + "github.com/databricks/cli/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/installer" "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/apps/generator" "github.com/databricks/cli/libs/apps/initializer" "github.com/databricks/cli/libs/apps/manifest" diff --git a/cmd/cmd.go b/cmd/cmd.go index 014471f7638..10209bd6400 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -4,6 +4,7 @@ import ( "context" "strings" + aitoolscmd "github.com/databricks/cli/aitools/cmd" "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..4921280f044 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 silent backward-compat aliases. + 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..9730b51f3b3 100644 --- a/experimental/aitools/cmd/aitools.go +++ b/experimental/aitools/cmd/aitools.go @@ -1,6 +1,7 @@ package aitools import ( + aitoolscmd "github.com/databricks/cli/aitools/cmd" "github.com/spf13/cobra" ) @@ -17,11 +18,22 @@ Provides commands to: - Access tools directly (tools)`, } - cmd.AddCommand(newInstallCmd()) - cmd.AddCommand(newUpdateCmd()) - cmd.AddCommand(newUninstallCmd()) - cmd.AddCommand(newListCmd()) - cmd.AddCommand(newVersionCmd()) + // Hidden silent backward-compatibility aliases for the skills-management + // commands. They now live at top-level `databricks aitools `; the old + // paths under `databricks experimental aitools ` keep working but are + // hidden so the canonical path is what shows in --help. + for _, mk := range []func() *cobra.Command{ + aitoolscmd.NewInstallCmd, + aitoolscmd.NewUpdateCmd, + aitoolscmd.NewUninstallCmd, + aitoolscmd.NewListCmd, + aitoolscmd.NewVersionCmd, + } { + sub := mk() + sub.Hidden = true + cmd.AddCommand(sub) + } + cmd.AddCommand(newSkillsCmd()) cmd.AddCommand(newToolsCmd()) diff --git a/experimental/aitools/cmd/skills.go b/experimental/aitools/cmd/skills.go index 9995ff72a07..a8c6f566ec9 100644 --- a/experimental/aitools/cmd/skills.go +++ b/experimental/aitools/cmd/skills.go @@ -1,51 +1,10 @@ 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" + aitoolscmd "github.com/databricks/cli/aitools/cmd" "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", @@ -54,7 +13,7 @@ func newSkillsCmd() *cobra.Command { Long: `Manage Databricks skills that extend coding agents with Databricks-specific capabilities.`, } - // Subcommands delegate to the flat top-level commands. + // Subcommands delegate cross-package to the canonical top-level commands. cmd.AddCommand(newSkillsListCmd()) cmd.AddCommand(newSkillsInstallCmd()) @@ -67,7 +26,7 @@ func newSkillsListCmd() *cobra.Command { Short: "List available skills", RunE: func(cmd *cobra.Command, args []string) error { // Default to showing all scopes (empty scope = both). - return listSkillsFn(cmd, "") + return aitoolscmd.ListSkillsFn(cmd, "") }, } } @@ -80,8 +39,8 @@ func newSkillsInstallCmd() *cobra.Command { 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() + // Delegate to the flat top-level install command. + installCmd := aitoolscmd.NewInstallCmd() installCmd.SetContext(cmd.Context()) var delegateArgs []string diff --git a/experimental/aitools/cmd/skills_test.go b/experimental/aitools/cmd/skills_test.go new file mode 100644 index 00000000000..954061d45de --- /dev/null +++ b/experimental/aitools/cmd/skills_test.go @@ -0,0 +1,158 @@ +package aitools + +import ( + "context" + "os" + "path/filepath" + "testing" + + aitoolscmd "github.com/databricks/cli/aitools/cmd" + "github.com/databricks/cli/aitools/lib/agents" + "github.com/databricks/cli/aitools/lib/installer" + "github.com/databricks/cli/libs/cmdio" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type installCall struct { + agents []string + opts installer.InstallOptions +} + +func setupInstallMock(t *testing.T) *[]installCall { + t.Helper() + orig := aitoolscmd.InstallSkillsForAgentsFn + t.Cleanup(func() { aitoolscmd.InstallSkillsForAgentsFn = orig }) + + var calls []installCall + aitoolscmd.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 +} + +func setupTestAgents(t *testing.T) string { + t.Helper() + tmp := t.TempDir() + t.Setenv("HOME", tmp) + require.NoError(t, os.MkdirAll(filepath.Join(tmp, ".claude"), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Join(tmp, ".cursor"), 0o755)) + return tmp +} + +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 TestSkillsListDelegatesToListFn(t *testing.T) { + orig := aitoolscmd.ListSkillsFn + t.Cleanup(func() { aitoolscmd.ListSkillsFn = orig }) + + called := false + aitoolscmd.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) +} From 514cceb2cccc80361c3761815e2511abe0a6b1d3 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 10:18:59 +0200 Subject: [PATCH 2/7] aitools: update tip and error messages to canonical top-level path User-facing strings that pointed callers at `databricks experimental aitools install/update` now point at the new canonical `databricks aitools install/update`. Test assertions updated to match. Co-authored-by: Isaac --- aitools/cmd/scope.go | 4 ++-- aitools/cmd/version.go | 4 ++-- aitools/lib/agents/recommend.go | 2 +- aitools/lib/agents/recommend_test.go | 2 +- aitools/lib/installer/installer.go | 6 +++--- aitools/lib/installer/uninstall.go | 2 +- aitools/lib/installer/update.go | 4 ++-- aitools/lib/installer/update_test.go | 2 +- cmd/apps/init.go | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/aitools/cmd/scope.go b/aitools/cmd/scope.go index 5277ed671de..acd012135a3 100644 --- a/aitools/cmd/scope.go +++ b/aitools/cmd/scope.go @@ -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/aitools/cmd/version.go b/aitools/cmd/version.go index a0eb8eb59a7..3877eb69377 100644 --- a/aitools/cmd/version.go +++ b/aitools/cmd/version.go @@ -40,7 +40,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 } @@ -89,6 +89,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/aitools/lib/agents/recommend.go b/aitools/lib/agents/recommend.go index bf10c67bfd9..de906752316 100644 --- a/aitools/lib/agents/recommend.go +++ b/aitools/lib/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/aitools/lib/agents/recommend_test.go b/aitools/lib/agents/recommend_test.go index c2c27699212..247a91da80b 100644 --- a/aitools/lib/agents/recommend_test.go +++ b/aitools/lib/agents/recommend_test.go @@ -67,7 +67,7 @@ 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) { diff --git a/aitools/lib/installer/installer.go b/aitools/lib/installer/installer.go index 5995a4f35d7..5db3b151c7d 100644 --- a/aitools/lib/installer/installer.go +++ b/aitools/lib/installer/installer.go @@ -135,7 +135,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") } } @@ -314,7 +314,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) @@ -323,7 +323,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/aitools/lib/installer/uninstall.go b/aitools/lib/installer/uninstall.go index 1fa4dc3b47d..92c69f7ec91 100644 --- a/aitools/lib/installer/uninstall.go +++ b/aitools/lib/installer/uninstall.go @@ -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/aitools/lib/installer/update.go b/aitools/lib/installer/update.go index 47f7542f36b..87b62e76ed5 100644 --- a/aitools/lib/installer/update.go +++ b/aitools/lib/installer/update.go @@ -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 := GetSkillsRef(ctx) diff --git a/aitools/lib/installer/update_test.go b/aitools/lib/installer/update_test.go index 45c4cb6a135..4339ddead44 100644 --- a/aitools/lib/installer/update_test.go +++ b/aitools/lib/installer/update_test.go @@ -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) { diff --git a/cmd/apps/init.go b/cmd/apps/init.go index fdc2b2c4ddf..09e0d9be89a 100644 --- a/cmd/apps/init.go +++ b/cmd/apps/init.go @@ -1146,7 +1146,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) From 506a8efd72bccbb40b9513db6f4b3d68f694d9c6 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 10:28:46 +0200 Subject: [PATCH 3/7] aitools install: accept [skill-name] positional arg The README and PR description advertise `databricks aitools install [skill-name]`, but the promoted command had `Use: "install"` and `cobra.NoArgs`, so the documented form would fail. Match the docs: accept an optional positional skill name and map it to the same path as `--skills`. The two are mutually exclusive to keep behavior unambiguous. Replace the old reject-positional-args test with coverage for the positional path and the conflict case. Co-authored-by: Isaac --- aitools/cmd/install.go | 20 ++++++++++++++++---- aitools/cmd/install_test.go | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/aitools/cmd/install.go b/aitools/cmd/install.go index 2a2aa07ea92..404bb05d045 100644 --- a/aitools/cmd/install.go +++ b/aitools/cmd/install.go @@ -58,17 +58,20 @@ func NewInstallCmd() *cobra.Command { var projectFlag, globalFlag bool cmd := &cobra.Command{ - Use: "install", + Use: "install [skill-name]", Short: "Install AI skills for coding agents", Long: `Install Databricks AI skills for detected coding agents. +Pass an optional skill name as a positional argument to install only that +skill, or use --skills with a comma-separated list to install multiple. + By default, skills are installed globally to each agent's skills directory. 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. Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity`, - Args: cobra.NoArgs, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -113,12 +116,21 @@ Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Anti } } - // Build install options. + // Build install options. The positional arg, if provided, is a + // single skill name; --skills accepts a comma-separated list. They + // are mutually exclusive to keep behavior unambiguous. + if len(args) > 0 && skillsFlag != "" { + return errors.New("provide either a positional skill name or --skills, not both") + } opts := installer.InstallOptions{ IncludeExperimental: includeExperimental, Scope: scope, } - opts.SpecificSkills = splitAndTrim(skillsFlag) + if len(args) > 0 { + opts.SpecificSkills = []string{args[0]} + } else { + opts.SpecificSkills = splitAndTrim(skillsFlag) + } installer.PrintInstallingFor(ctx, targetAgents) diff --git a/aitools/cmd/install_test.go b/aitools/cmd/install_test.go index 54685387f6c..25a185c4350 100644 --- a/aitools/cmd/install_test.go +++ b/aitools/cmd/install_test.go @@ -262,17 +262,49 @@ func TestInstallAgentsFlagSkipsPrompt(t *testing.T) { assert.Equal(t, []string{"claude-code", "cursor"}, (*calls)[0].agents) } -func TestInstallRejectsPositionalArgs(t *testing.T) { +func TestInstallAcceptsPositionalSkillName(t *testing.T) { + setupTestAgents(t) + calls := setupInstallMock(t) + ctx := cmdio.MockDiscard(t.Context()) cmd := NewInstallCmd() 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 TestInstallRejectsTwoPositionalArgs(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + cmd := NewInstallCmd() + 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(), "unknown command") + assert.Contains(t, err.Error(), "accepts at most 1 arg") +} + +func TestInstallRejectsPositionalAndSkillsFlag(t *testing.T) { + setupTestAgents(t) + setupInstallMock(t) + + ctx := cmdio.MockDiscard(t.Context()) + cmd := NewInstallCmd() + cmd.SetContext(ctx) + cmd.SetArgs([]string{"databricks", "--skills", "foo"}) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "either a positional skill name or --skills") } func TestUpdateRejectsPositionalArgs(t *testing.T) { From fbb2a1b1121939d66852f826eba1d74cc4b703e3 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 10:30:37 +0200 Subject: [PATCH 4/7] Revert "aitools install: accept [skill-name] positional arg" This reverts commit 99a6273d2b9be4c402c6ada557c2db795677ea79. --- aitools/cmd/install.go | 20 ++++---------------- aitools/cmd/install_test.go | 36 ++---------------------------------- 2 files changed, 6 insertions(+), 50 deletions(-) diff --git a/aitools/cmd/install.go b/aitools/cmd/install.go index 404bb05d045..2a2aa07ea92 100644 --- a/aitools/cmd/install.go +++ b/aitools/cmd/install.go @@ -58,20 +58,17 @@ func NewInstallCmd() *cobra.Command { var projectFlag, globalFlag bool cmd := &cobra.Command{ - Use: "install [skill-name]", + Use: "install", Short: "Install AI skills for coding agents", Long: `Install Databricks AI skills for detected coding agents. -Pass an optional skill name as a positional argument to install only that -skill, or use --skills with a comma-separated list to install multiple. - By default, skills are installed globally to each agent's skills directory. 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. Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity`, - Args: cobra.MaximumNArgs(1), + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -116,21 +113,12 @@ Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Anti } } - // Build install options. The positional arg, if provided, is a - // single skill name; --skills accepts a comma-separated list. They - // are mutually exclusive to keep behavior unambiguous. - if len(args) > 0 && skillsFlag != "" { - return errors.New("provide either a positional skill name or --skills, not both") - } + // Build install options. opts := installer.InstallOptions{ IncludeExperimental: includeExperimental, Scope: scope, } - if len(args) > 0 { - opts.SpecificSkills = []string{args[0]} - } else { - opts.SpecificSkills = splitAndTrim(skillsFlag) - } + opts.SpecificSkills = splitAndTrim(skillsFlag) installer.PrintInstallingFor(ctx, targetAgents) diff --git a/aitools/cmd/install_test.go b/aitools/cmd/install_test.go index 25a185c4350..54685387f6c 100644 --- a/aitools/cmd/install_test.go +++ b/aitools/cmd/install_test.go @@ -262,49 +262,17 @@ func TestInstallAgentsFlagSkipsPrompt(t *testing.T) { assert.Equal(t, []string{"claude-code", "cursor"}, (*calls)[0].agents) } -func TestInstallAcceptsPositionalSkillName(t *testing.T) { - setupTestAgents(t) - calls := setupInstallMock(t) - +func TestInstallRejectsPositionalArgs(t *testing.T) { ctx := cmdio.MockDiscard(t.Context()) cmd := NewInstallCmd() 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 TestInstallRejectsTwoPositionalArgs(t *testing.T) { - ctx := cmdio.MockDiscard(t.Context()) - cmd := NewInstallCmd() - 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 TestInstallRejectsPositionalAndSkillsFlag(t *testing.T) { - setupTestAgents(t) - setupInstallMock(t) - - ctx := cmdio.MockDiscard(t.Context()) - cmd := NewInstallCmd() - cmd.SetContext(ctx) - cmd.SetArgs([]string{"databricks", "--skills", "foo"}) - cmd.SilenceErrors = true - cmd.SilenceUsage = true - - err := cmd.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "either a positional skill name or --skills") + assert.Contains(t, err.Error(), "unknown command") } func TestUpdateRejectsPositionalArgs(t *testing.T) { From 7efa0d3f3a97ee7a40d4c5b2194f1b818264fcf1 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 10:30:59 +0200 Subject: [PATCH 5/7] aitools/README: fix install command shape The README listed `databricks aitools install [skill-name]`, but the command is `Use: "install"` with `cobra.NoArgs` and uses `--skills ` for specific skills. Match the actual command. Co-authored-by: Isaac --- aitools/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aitools/README.md b/aitools/README.md index 3aec1e8df6e..aa340f823be 100644 --- a/aitools/README.md +++ b/aitools/README.md @@ -4,7 +4,7 @@ ## Commands -- `databricks aitools install [skill-name]` +- `databricks aitools install` (use `--skills [,...]` to install specific skills) - `databricks aitools update` - `databricks aitools uninstall` - `databricks aitools list` From e7f09a4c8c8bbde046bf86fefd8b1305662e0d6a Mon Sep 17 00:00:00 2001 From: James Broadhead Date: Thu, 30 Apr 2026 15:47:44 +0000 Subject: [PATCH 6/7] experimental/aitools: fix stale Long help text After the silent-alias restructure, install/update/uninstall/list/version and the skills alias group are all Hidden under `experimental aitools`, leaving only `tools` visible. The Long string still advertised install and skills as if they were active here. Reword to: state that skills-management has moved to top-level, and that `tools` is the only experimental surface still maintained under this path. Co-authored-by: Isaac --- experimental/aitools/cmd/aitools.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/experimental/aitools/cmd/aitools.go b/experimental/aitools/cmd/aitools.go index 9730b51f3b3..1168e270d91 100644 --- a/experimental/aitools/cmd/aitools.go +++ b/experimental/aitools/cmd/aitools.go @@ -10,12 +10,7 @@ func NewAitoolsCmd() *cobra.Command { Use: "aitools", Hidden: true, Short: "Databricks AI Tools for coding agents", - Long: `Manage Databricks AI Tools. - -Provides commands to: -- Install the AI tools in coding agents (install) -- Manage skills (skills) -- Access tools directly (tools)`, + Long: `Experimental coding-agent helpers. Skills management is at "databricks aitools".`, } // Hidden silent backward-compatibility aliases for the skills-management From c677e6f8223548f6ce864162f79b1512cd68b27a Mon Sep 17 00:00:00 2001 From: James Broadhead Date: Thu, 30 Apr 2026 15:50:28 +0000 Subject: [PATCH 7/7] aitools install: accept [skill-name] positional to match alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores the positional skill-name form on the canonical command. The legacy alias `databricks experimental aitools skills install ` already accepted a positional and forwarded it via --skills; the canonical `databricks aitools install ` previously rejected positionals (cobra.NoArgs), so users mechanically migrating scripts hit "unknown command". Behavior: - `databricks aitools install` — install all skills (unchanged) - `databricks aitools install ` — install just that skill - `databricks aitools install --skills a,b` — install multiple - positional + --skills together — error, mutually exclusive Replaces TestInstallRejectsPositionalArgs with positive coverage for the positional path, the conflict case, and the >1-positional rejection. Co-authored-by: Isaac --- aitools/README.md | 2 +- aitools/cmd/install.go | 14 ++++++++++++-- aitools/cmd/install_test.go | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/aitools/README.md b/aitools/README.md index aa340f823be..7ac633ebbfb 100644 --- a/aitools/README.md +++ b/aitools/README.md @@ -4,7 +4,7 @@ ## Commands -- `databricks aitools install` (use `--skills [,...]` to install specific skills) +- `databricks aitools install [skill-name]` (or `--skills [,...]` for multiple) - `databricks aitools update` - `databricks aitools uninstall` - `databricks aitools list` diff --git a/aitools/cmd/install.go b/aitools/cmd/install.go index 2a2aa07ea92..8380264b87e 100644 --- a/aitools/cmd/install.go +++ b/aitools/cmd/install.go @@ -58,7 +58,7 @@ func NewInstallCmd() *cobra.Command { var projectFlag, globalFlag bool cmd := &cobra.Command{ - Use: "install", + Use: "install [skill-name]", Short: "Install AI skills for coding agents", Long: `Install Databricks AI skills for detected coding agents. @@ -67,11 +67,21 @@ 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. +Pass a single skill name as a positional argument to install just that skill, +or use --skills name1,name2 for multiple. The two forms are mutually exclusive. + Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity`, - Args: cobra.NoArgs, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() + if len(args) == 1 { + if skillsFlag != "" { + return errors.New("cannot use positional [skill-name] together with --skills; pick one") + } + skillsFlag = args[0] + } + // Resolve scope. scope, err := resolveScopeWithPrompt(ctx, projectFlag, globalFlag) if err != nil { diff --git a/aitools/cmd/install_test.go b/aitools/cmd/install_test.go index 54685387f6c..867f7f4ea0b 100644 --- a/aitools/cmd/install_test.go +++ b/aitools/cmd/install_test.go @@ -262,17 +262,45 @@ func TestInstallAgentsFlagSkipsPrompt(t *testing.T) { assert.Equal(t, []string{"claude-code", "cursor"}, (*calls)[0].agents) } -func TestInstallRejectsPositionalArgs(t *testing.T) { +func TestInstallPositionalSkillName(t *testing.T) { + setupTestAgents(t) + calls := setupInstallMock(t) + ctx := cmdio.MockDiscard(t.Context()) cmd := NewInstallCmd() cmd.SetContext(ctx) - cmd.SetArgs([]string{"databricks"}) + cmd.SetArgs([]string{"databricks-jobs"}) + + require.NoError(t, cmd.Execute()) + require.Len(t, *calls, 1) + assert.Equal(t, []string{"databricks-jobs"}, (*calls)[0].opts.SpecificSkills) +} + +func TestInstallPositionalAndSkillsFlagConflict(t *testing.T) { + setupTestAgents(t) + setupInstallMock(t) + + ctx := cmdio.MockDiscard(t.Context()) + cmd := NewInstallCmd() + cmd.SetContext(ctx) + cmd.SetArgs([]string{"databricks-jobs", "--skills", "databricks-pipelines"}) cmd.SilenceErrors = true cmd.SilenceUsage = true err := cmd.Execute() require.Error(t, err) - assert.Contains(t, err.Error(), "unknown command") + assert.Contains(t, err.Error(), "cannot use positional") +} + +func TestInstallRejectsTwoPositionalArgs(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + cmd := NewInstallCmd() + cmd.SetContext(ctx) + cmd.SetArgs([]string{"databricks-jobs", "databricks-pipelines"}) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + require.Error(t, cmd.Execute()) } func TestUpdateRejectsPositionalArgs(t *testing.T) {