Skip to content

Commit 7e2cf0e

Browse files
anton-107Isaac
andcommitted
aitools: mention databricks aitools install in version --check
`databricks version --check` (text output) is followed by the `apps init` skills tip on stderr when a coding agent is detected without Databricks tooling. Stdout keeps only the update-check result: ```diff Databricks CLI v0.245.0 You're on the latest version. +Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them. ``` The tip comes from `agents.MaybeVersionCheckTip`. It is not printed when an agent has the databricks plugin (in its own manifest or recorded in the CLI install state) or Databricks skills (in the shared store, in the agent's own skills dir, or in the project dirs up to the repository root), on Databricks Runtime, or when Claude Code drives the CLI (`agents.MaybeHint` already covers that caller). Changelog fragment: `.nextchanges/cli/version-check-aitools-hint.md`. Co-authored-by: Isaac <no-reply@databricks.com>
1 parent e1c95af commit 7e2cf0e

9 files changed

Lines changed: 247 additions & 2 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* `databricks version --check` now suggests `databricks aitools install` on stderr when a coding agent is detected without the Databricks plugin or skills. ([#7471](https://github.com/databricks/cli/pull/7471))

‎acceptance/cmd/version/claude-home/.claude/.gitkeep‎

Whitespace-only changes.

‎acceptance/cmd/version/codex-home/.codex/.gitkeep‎

Whitespace-only changes.

‎acceptance/cmd/version/output.txt‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,26 @@ This is a development build; skipping the update check.
1818
"update_available": false,
1919
"development_build": true
2020
}
21+
22+
=== version --check with a coding agent and no Databricks tooling
23+
24+
>>> [CLI] version --check
25+
Databricks CLI v[CLI_VERSION]
26+
This is a development build; skipping the update check.
27+
Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.
28+
29+
=== version --check --output json with a coding agent prints no tip
30+
31+
>>> [CLI] version --check --output json
32+
{
33+
"current_version": "[CLI_VERSION]",
34+
"update_available": false,
35+
"development_build": true
36+
}
37+
38+
=== version --check from Claude Code prints only the root hint
39+
40+
>>> [CLI] version --check
41+
Databricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install
42+
Databricks CLI v[CLI_VERSION]
43+
This is a development build; skipping the update check.

‎acceptance/cmd/version/script‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,21 @@ trace $CLI version --check
88

99
title "version --check --output json"
1010
trace $CLI version --check --output json
11+
12+
# The tip is printed on stderr only when a coding agent is detected without
13+
# Databricks tooling. The committed codex-home/.codex is a Codex CLI config dir
14+
# without the databricks plugin or skills.
15+
title "version --check with a coding agent and no Databricks tooling"
16+
sethome ./codex-home
17+
trace $CLI version --check
18+
19+
title "version --check --output json with a coding agent prints no tip"
20+
trace $CLI version --check --output json
21+
22+
# Claude Code callers get only the root hint (agents.MaybeHint), not the tip.
23+
title "version --check from Claude Code prints only the root hint"
24+
sethome ./claude-home
25+
export CLAUDECODE=1
26+
unset CLAUDE_CODE_SESSION_ID
27+
unset CLAUDE_CONFIG_DIR
28+
trace $CLI version --check

‎cmd/version/version.go‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
package version
44

55
import (
6+
"context"
7+
68
"github.com/databricks/cli/cmd/root"
79
"github.com/databricks/cli/internal/build"
10+
"github.com/databricks/cli/libs/aitools/agents"
11+
"github.com/databricks/cli/libs/aitools/installer"
812
"github.com/databricks/cli/libs/cmdio"
13+
"github.com/databricks/cli/libs/flags"
914
"github.com/databricks/cli/libs/versioncheck"
1015
"github.com/spf13/cobra"
1116
)
@@ -30,6 +35,20 @@ Download the latest release: https://github.com/databricks/cli/releases
3035
{{- end}}
3136
`
3237

38+
// renderUpdateCheck renders result and, in text mode, follows it with the
39+
// aitools install tip on stderr (agents.MaybeVersionCheckTip), so stdout keeps
40+
// only the update-check result.
41+
func renderUpdateCheck(ctx context.Context, result *versioncheck.Result, output flags.Output) error {
42+
err := cmdio.RenderWithTemplate(ctx, result, "", updateCheckTemplate)
43+
if err != nil {
44+
return err
45+
}
46+
if output == flags.OutputText {
47+
agents.MaybeVersionCheckTip(ctx, installer.InstalledTools)
48+
}
49+
return nil
50+
}
51+
3352
func New() *cobra.Command {
3453
cmd := &cobra.Command{
3554
Use: "version",
@@ -46,7 +65,7 @@ func New() *cobra.Command {
4665
cmd.RunE = func(cmd *cobra.Command, args []string) error {
4766
ctx := cmd.Context()
4867
if check {
49-
return cmdio.RenderWithTemplate(ctx, versioncheck.Check(ctx), "", updateCheckTemplate)
68+
return renderUpdateCheck(ctx, versioncheck.Check(ctx), root.OutputType(cmd))
5069
}
5170
return cmdio.Render(ctx, build.GetInfo())
5271
}

‎cmd/version/version_test.go‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
package version
22

33
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
47
"testing"
58

9+
"github.com/databricks/cli/internal/testutil"
10+
"github.com/databricks/cli/libs/aitools/agents"
11+
"github.com/databricks/cli/libs/aitools/installer"
612
"github.com/databricks/cli/libs/cmdio"
13+
"github.com/databricks/cli/libs/dbr"
14+
"github.com/databricks/cli/libs/flags"
715
"github.com/databricks/cli/libs/versioncheck"
816
"github.com/stretchr/testify/assert"
917
"github.com/stretchr/testify/require"
@@ -85,3 +93,74 @@ Could not reach GitHub to check for a newer version. See https://github.com/data
8593
})
8694
}
8795
}
96+
97+
func TestRenderUpdateCheckAIToolsTip(t *testing.T) {
98+
upToDate := &versioncheck.Result{CurrentVersion: "0.245.0", LatestVersion: "0.245.0"}
99+
devBuild := &versioncheck.Result{CurrentVersion: "0.0.0-dev+abc123", DevelopmentBuild: true}
100+
upToDateText := "Databricks CLI v0.245.0\nYou're on the latest version.\n"
101+
tip := "Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n"
102+
103+
tests := []struct {
104+
name string
105+
result *versioncheck.Result
106+
output flags.Output
107+
recordedCodexPlugin bool
108+
wantStdout string
109+
wantStderr string
110+
}{
111+
{
112+
name: "text output prints the tip on stderr",
113+
result: upToDate,
114+
output: flags.OutputText,
115+
wantStdout: upToDateText,
116+
wantStderr: tip,
117+
},
118+
{
119+
name: "codex plugin recorded in the install state",
120+
result: upToDate,
121+
output: flags.OutputText,
122+
recordedCodexPlugin: true,
123+
wantStdout: upToDateText,
124+
},
125+
{
126+
name: "development build prints the tip too",
127+
result: devBuild,
128+
output: flags.OutputText,
129+
wantStdout: "Databricks CLI v0.0.0-dev+abc123\nThis is a development build; skipping the update check.\n",
130+
wantStderr: tip,
131+
},
132+
{
133+
name: "json output",
134+
result: upToDate,
135+
output: flags.OutputJSON,
136+
wantStdout: "{\n \"current_version\": \"0.245.0\",\n \"latest_version\": \"0.245.0\",\n \"update_available\": false\n}\n",
137+
},
138+
}
139+
140+
for _, tt := range tests {
141+
t.Run(tt.name, func(t *testing.T) {
142+
home := t.TempDir()
143+
testutil.SetAgentEnv(t, "HOME", home, "USERPROFILE", home)
144+
t.Chdir(t.TempDir())
145+
// Codex CLI is detected by its config dir; a plugin install through
146+
// `databricks aitools install` is recorded only in the install state.
147+
require.NoError(t, os.MkdirAll(filepath.Join(home, ".codex"), 0o755))
148+
if tt.recordedCodexPlugin {
149+
state := &installer.InstallState{
150+
Plugins: map[string]installer.PluginRecord{
151+
"codex": {Plugin: "databricks", Version: "0.2.6"},
152+
},
153+
}
154+
require.NoError(t, installer.SaveState(filepath.Join(home, agents.CanonicalSkillsDir), state))
155+
}
156+
157+
var stdout, stderr bytes.Buffer
158+
ctx := dbr.MockRuntime(t.Context(), dbr.Environment{})
159+
ctx = cmdio.InContext(ctx, cmdio.NewIO(ctx, tt.output, nil, &stdout, &stderr, "", ""))
160+
err := renderUpdateCheck(ctx, tt.result, tt.output)
161+
require.NoError(t, err)
162+
assert.Equal(t, tt.wantStdout, stdout.String())
163+
assert.Equal(t, tt.wantStderr, stderr.String())
164+
})
165+
}
166+
}

‎libs/aitools/agents/recommend.go‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,52 @@ import (
44
"context"
55

66
"github.com/databricks/cli/libs/cmdio"
7+
"github.com/databricks/cli/libs/dbr"
78
"github.com/databricks/cli/libs/log"
89
"github.com/databricks/cli/libs/telemetry/protos"
10+
"github.com/databricks/databricks-sdk-go/useragent"
911
)
1012

13+
// skillsTipText is the tip printed by PrintSkillsTip and MaybeVersionCheckTip.
14+
const skillsTipText = "Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them."
15+
1116
// PrintSkillsTip prints the non-interactive `apps init` tip to install skills
1217
// and records that it was shown. Callers decide whether the tooling is missing
1318
// (see HasDatabricksToolingInstalled).
1419
func PrintSkillsTip(ctx context.Context) {
15-
cmdio.LogString(ctx, "Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.")
20+
cmdio.LogString(ctx, skillsTipText)
1621
logHintShown(ctx, protos.AitoolsHintKindAppsInitTip)
1722
}
1823

24+
// MaybeVersionCheckTip prints skillsTipText after `databricks version --check`
25+
// when a coding agent is detected without Databricks tooling. Claude Code
26+
// callers are skipped because MaybeHint already covers them, with its
27+
// per-session throttle. installedPlugins is a parameter because this package
28+
// cannot import the installer; callers pass installer.InstalledTools. No
29+
// telemetry is logged: `databricks version` never resolves auth, so the event
30+
// would not be uploaded (see logHintShown).
31+
func MaybeVersionCheckTip(ctx context.Context, installedPlugins func(context.Context) []string) {
32+
if useragent.AgentProvider() == claudeAgentProvider {
33+
return
34+
}
35+
if dbr.HasDetection(ctx) && dbr.RunsOnRuntime(ctx) {
36+
return
37+
}
38+
if HasDatabricksToolingInstalled(ctx) || len(installedPlugins(ctx)) > 0 {
39+
return
40+
}
41+
// Recorded plugins are checked above, so DatabricksTooling only needs the
42+
// agent's own manifest, its skills dir and the project dirs up to the
43+
// repository root.
44+
noRecordedPlugin := func() bool { return false }
45+
for _, a := range Registry {
46+
if a.DatabricksTooling(ctx, noRecordedPlugin) != ToolingNone {
47+
return
48+
}
49+
}
50+
cmdio.LogString(ctx, skillsTipText)
51+
}
52+
1953
// RecommendSkillsInstall checks if coding agents are detected without Databricks tooling (skills
2054
// or the databricks plugin) installed.
2155
// In interactive mode, prompts the user to install now using installFn. In non-interactive mode, prints a hint.

‎libs/aitools/agents/recommend_test.go‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"path/filepath"
88
"testing"
99

10+
"github.com/databricks/cli/internal/testutil"
1011
"github.com/databricks/cli/libs/cmdio"
12+
"github.com/databricks/cli/libs/dbr"
1113
"github.com/databricks/cli/libs/telemetry"
1214
"github.com/databricks/cli/libs/telemetry/protos"
1315
"github.com/stretchr/testify/assert"
@@ -132,3 +134,72 @@ func TestRecommendSkillsInstallInteractiveDecline(t *testing.T) {
132134
// The prompt is logged when shown, so a declined prompt still counts.
133135
assert.Equal(t, []protos.AitoolsHintKind{protos.AitoolsHintKindAppsInitPrompt}, hintKinds(t, ctx))
134136
}
137+
138+
func TestMaybeVersionCheckTip(t *testing.T) {
139+
noPlugins := func(context.Context) []string { return nil }
140+
recordedCodexPlugin := func(context.Context) []string { return []string{"codex_0.2.6"} }
141+
142+
for _, tc := range []struct {
143+
name string
144+
agentEnv []string
145+
agentDirs []string
146+
globalSkills bool
147+
claudePlugin bool
148+
projectSkills bool
149+
agentSkills bool
150+
fromSubdir bool
151+
onDBR bool
152+
installedPlugins func(context.Context) []string
153+
wantTip bool
154+
}{
155+
{name: "agent without tooling", agentDirs: []string{".codex"}, installedPlugins: noPlugins, wantTip: true},
156+
{name: "no agent detected", installedPlugins: noPlugins},
157+
{name: "plugin in agent manifest", agentDirs: []string{".claude"}, claudePlugin: true, installedPlugins: noPlugins},
158+
{name: "plugin recorded in install state", agentDirs: []string{".codex"}, installedPlugins: recordedCodexPlugin},
159+
{name: "global skills", agentDirs: []string{".kiro"}, globalSkills: true, installedPlugins: noPlugins},
160+
{name: "project skills", agentDirs: []string{".kiro"}, projectSkills: true, installedPlugins: noPlugins},
161+
{name: "project skills at the repository root, run from a subdirectory", agentDirs: []string{".kiro"}, projectSkills: true, fromSubdir: true, installedPlugins: noPlugins},
162+
{name: "skills only in the agent's own skills dir", agentDirs: []string{".cursor"}, agentSkills: true, installedPlugins: noPlugins},
163+
{name: "databricks runtime", agentDirs: []string{".codex"}, onDBR: true, installedPlugins: noPlugins},
164+
{name: "claude code caller is left to MaybeHint", agentEnv: []string{"CLAUDECODE", "1"}, agentDirs: []string{".claude"}, installedPlugins: noPlugins},
165+
} {
166+
t.Run(tc.name, func(t *testing.T) {
167+
home := t.TempDir()
168+
testutil.SetAgentEnv(t, append([]string{"HOME", home, "USERPROFILE", home}, tc.agentEnv...)...)
169+
project := t.TempDir()
170+
if tc.fromSubdir {
171+
// The walk up to the repository root stops at the nearest .git.
172+
require.NoError(t, os.Mkdir(filepath.Join(project, ".git"), 0o755))
173+
subdir := filepath.Join(project, "src")
174+
require.NoError(t, os.Mkdir(subdir, 0o755))
175+
t.Chdir(subdir)
176+
} else {
177+
t.Chdir(project)
178+
}
179+
for _, dir := range tc.agentDirs {
180+
require.NoError(t, os.MkdirAll(filepath.Join(home, dir), 0o755))
181+
}
182+
if tc.claudePlugin {
183+
writePluginManifest(t, home, `{"plugins":{"databricks@claude-plugins-official":[{"version":"0.2.6"}]}}`)
184+
}
185+
if tc.globalSkills {
186+
require.NoError(t, os.MkdirAll(filepath.Join(home, CanonicalSkillsDir, "databricks"), 0o755))
187+
}
188+
if tc.projectSkills {
189+
require.NoError(t, os.MkdirAll(filepath.Join(project, ".kiro", "skills", "databricks-apps"), 0o755))
190+
}
191+
if tc.agentSkills {
192+
require.NoError(t, os.MkdirAll(filepath.Join(home, ".cursor", "skills", "databricks-apps"), 0o755))
193+
}
194+
195+
ctx, stderr := cmdio.NewTestContextWithStderr(t.Context())
196+
ctx = dbr.MockRuntime(ctx, dbr.Environment{IsDbr: tc.onDBR})
197+
MaybeVersionCheckTip(ctx, tc.installedPlugins)
198+
if tc.wantTip {
199+
assert.Equal(t, skillsTipText+"\n", stderr.String())
200+
} else {
201+
assert.Empty(t, stderr.String())
202+
}
203+
})
204+
}
205+
}

0 commit comments

Comments
 (0)