Skip to content

Commit b2ee61c

Browse files
anton-107Isaac
andcommitted
aitools: refresh only the Databricks marketplace, and do not fail the install on it
Claude Code's `plugin marketplace update` with no name refreshes every marketplace the user has added, so one unreachable third-party marketplace failed the Databricks plugin install. The refresh now names the marketplace the Databricks plugin is listed in, and a failed refresh no longer stops the install or the update: both go on from the local copy of the marketplace and, once that succeeds, warn and record the plugin version as unknown. Codex's `plugin marketplace upgrade` in `aitools update` names the marketplace too. ```diff InstallPluginForAgent (libs/aitools/installer/plugin.go) marketplace add (if absent) - claude plugin marketplace update - on error: remove the marketplace we added, return install-failed + claude plugin marketplace update <marketplace> + on error: keep the error, continue claude plugin install databricks@<marketplace> on error: remove the marketplace we added - return install-failed with the install error + return install-failed with the install error + + "(refreshing the marketplace also failed: <refresh error>)" if the refresh failed - record Version = <ref> + if the refresh failed and the marketplace was not just added by us: + warn that the plugin came from the local copy (with the agent's stderr) + record Version = "" (version unknown) + else record Version = <ref> UpdatePluginForAgent (aitools update) - claude plugin marketplace update + claude plugin marketplace update <recorded marketplace, or the agent's current one> - on error: return install-failed + on error: keep the error, continue claude plugin update ... - codex plugin marketplace upgrade + codex plugin marketplace upgrade <recorded marketplace, or the agent's current one> + on error: same as above codex plugin add ... on update error: return install-failed + + "(refreshing the marketplace also failed: <refresh error>)" if the refresh failed - record Version = <ref> + if the refresh failed: warn as above, and record Version = "" (version unknown) + unless the record already holds <ref> + else record Version = <ref> ``` A record without a version still counts as an installed plugin for the `databricks version --check` tip (`InstalledPluginAgents`), and `aitools update --check` shows it as "version unknown (update may be available)". Changelog fragment: `.nextchanges/cli/aitools-marketplace-refresh.md`. Co-authored-by: Isaac <no-reply@databricks.com>
1 parent e9ddafa commit b2ee61c

12 files changed

Lines changed: 424 additions & 75 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* `databricks aitools install` and `databricks aitools update` refresh only the marketplace the Databricks plugin is listed in, so another unreachable marketplace no longer blocks them. When that refresh fails, both commands warn, install or update the plugin from the local copy of the marketplace, and record the plugin version as unknown. ([#7473](https://github.com/databricks/cli/pull/7473))

‎cmd/aitools/list.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,15 +379,17 @@ func renderSkillTable(skills []skillEntry, bothScopes bool) string {
379379
// are skipped: this line reports the plugin, and their versions would otherwise
380380
// decide the status of an agent that also has a plugin in another scope.
381381
func agentStatusLabel(a agentEntry, release string) string {
382-
version, upToDate := "", true
382+
// picked is separate from version because an empty version ("version
383+
// unknown") is a real pick, not "nothing picked yet".
384+
version, picked, upToDate := "", false, true
383385
for _, scope := range []string{installer.ScopeProject, installer.ScopeGlobal} {
384386
info, ok := a.Installed[scope]
385387
if !ok || info.Delivery != deliveryPlugin.String() {
386388
continue
387389
}
388390
stale := info.Version != release
389-
if version == "" || (upToDate && stale) {
390-
version = info.Version
391+
if !picked || (upToDate && stale) {
392+
version, picked = info.Version, true
391393
}
392394
if stale {
393395
upToDate = false

‎cmd/aitools/list_test.go‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,17 @@ func TestBuildAgentEntriesRecordsPerScopeVersions(t *testing.T) {
348348
assert.Equal(t, "databricks plugin · v0.2.5 · update available", agentStatusLabel(cc, "0.2.6"))
349349
}
350350

351+
func TestAgentStatusLabelKeepsUnknownStaleScope(t *testing.T) {
352+
// The project record has no version (its last install could not refresh the
353+
// marketplace), the global one is current. The unknown scope is stale and
354+
// must not be hidden behind the up-to-date version.
355+
a := agentEntry{Installed: map[string]installInfo{
356+
installer.ScopeProject: {Delivery: deliveryPlugin.String(), Version: ""},
357+
installer.ScopeGlobal: {Delivery: deliveryPlugin.String(), Version: "0.2.7"},
358+
}}
359+
assert.Equal(t, "databricks plugin · version unknown · update available", agentStatusLabel(a, "0.2.7"))
360+
}
361+
351362
func TestRenderListJSONScopeFiltersSummary(t *testing.T) {
352363
out := listOutput{
353364
Release: "0.1.0",

‎cmd/aitools/update.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ func printPluginCheckResults(ctx context.Context, state *installer.InstallState,
300300
rec := state.Plugins[name]
301301
status := "up to date"
302302
if rec.Version == "" {
303-
status = "version unknown"
303+
status = "update may be available"
304304
} else if rec.Version != latest {
305305
status = "update available"
306306
}

‎cmd/aitools/update_test.go‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ func TestUpdateCheckPrintsNoChanges(t *testing.T) {
5959
}
6060

6161
func TestUpdateCheckPrintsPluginState(t *testing.T) {
62+
for _, tc := range []struct {
63+
name string
64+
version string
65+
want string
66+
}{
67+
{"current", "0.2.6", "Claude Code databricks plugin v0.2.6 (up to date)"},
68+
{"older", "0.2.5", "Claude Code databricks plugin v0.2.5 (update available)"},
69+
// A record without a version (the last install or update could not
70+
// refresh the marketplace) may be behind, and `aitools update` would fix it.
71+
{"unknown", "", "Claude Code databricks plugin version unknown (update may be available)"},
72+
} {
73+
t.Run(tc.name, func(t *testing.T) {
74+
testUpdateCheckPrintsPluginState(t, tc.version, tc.want)
75+
})
76+
}
77+
}
78+
79+
func testUpdateCheckPrintsPluginState(t *testing.T, version, want string) {
6280
noTelemetryAuth(t)
6381
setupTestAgents(t)
6482
t.Setenv("DATABRICKS_SKILLS_REF", "v0.2.6")
@@ -85,7 +103,7 @@ func TestUpdateCheckPrintsPluginState(t *testing.T) {
85103
SchemaVersion: 2,
86104
Release: "v0.2.6",
87105
Plugins: map[string]installer.PluginRecord{
88-
agents.NameClaudeCode: {Marketplace: "claude-plugins-official", Plugin: "databricks", Version: "0.2.6"},
106+
agents.NameClaudeCode: {Marketplace: "claude-plugins-official", Plugin: "databricks", Version: version},
89107
},
90108
}))
91109

@@ -94,7 +112,7 @@ func TestUpdateCheckPrintsPluginState(t *testing.T) {
94112
cmd.SetArgs([]string{"--scope", "global", "--check"})
95113

96114
require.NoError(t, cmd.Execute())
97-
assert.Contains(t, stderr.String(), "Claude Code databricks plugin v0.2.6 (up to date)")
115+
assert.Contains(t, stderr.String(), want)
98116
}
99117

100118
func TestUpdateMigratesManagedRawSkillsToPlugins(t *testing.T) {

‎cmd/version/version.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func renderUpdateCheck(ctx context.Context, result *versioncheck.Result, output
4444
return err
4545
}
4646
if output == flags.OutputText {
47-
agents.MaybeVersionCheckTip(ctx, installer.InstalledTools)
47+
agents.MaybeVersionCheckTip(ctx, installer.InstalledPluginAgents)
4848
}
4949
return nil
5050
}

‎libs/aitools/agents/recommend.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ func PrintSkillsTip(ctx context.Context) {
2525
// when a coding agent is detected without Databricks tooling. Claude Code
2626
// callers are skipped because MaybeHint already covers them, with its
2727
// 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).
28+
// cannot import the installer; callers pass installer.InstalledPluginAgents,
29+
// which counts a plugin recorded in the CLI install state with or without a
30+
// known version. No telemetry is logged: `databricks version` never resolves
31+
// auth, so the event would not be uploaded (see logHintShown).
3132
func MaybeVersionCheckTip(ctx context.Context, installedPlugins func(context.Context) []string) {
3233
if useragent.AgentProvider() == claudeAgentProvider {
3334
return

‎libs/aitools/installer/detect.go‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import (
1313
// agent's own plugin cache (ground truth, catches installs made directly through
1414
// the agent), falling back to the version recorded in the CLI install state when
1515
// the plugin is not found on disk. An agent with no resolvable version is
16-
// omitted rather than reported without one.
16+
// omitted rather than reported without one. That includes an agent without a
17+
// plugin cache reader (Codex) whose record has no version because its last
18+
// update ran from a marketplace copy that could not be refreshed; use
19+
// InstalledPluginAgents to ask whether a plugin is installed at all.
1720
func InstalledTools(ctx context.Context) []string {
1821
recorded := recordedPluginVersions(ctx)
1922

@@ -34,10 +37,33 @@ func InstalledTools(ctx context.Context) []string {
3437
return tools
3538
}
3639

40+
// InstalledPluginAgents returns the names of the coding tools that have the
41+
// Databricks plugin installed, sorted: found in the agent's own plugin cache or
42+
// recorded in the CLI install state, whether or not a version is known.
43+
func InstalledPluginAgents(ctx context.Context) []string {
44+
recorded := recordedPluginVersions(ctx)
45+
46+
var names []string
47+
for _, a := range agents.Registry {
48+
if a.Plugin == nil {
49+
continue
50+
}
51+
_, inCache := a.DatabricksPluginVersion(ctx)
52+
_, inState := recorded[a.Name]
53+
if inCache || inState {
54+
names = append(names, a.Name)
55+
}
56+
}
57+
slices.Sort(names)
58+
return names
59+
}
60+
3761
// recordedPluginVersions maps agent name to the databricks plugin version
3862
// recorded in the CLI install state, merged across global and project scope.
39-
// When both scopes record a version, the project scope wins (it is the more
40-
// specific, closer-to-the-invocation install).
63+
// An agent whose record has no version maps to "". When both scopes record a
64+
// version, the project scope wins (it is the more specific,
65+
// closer-to-the-invocation install); a scope without a version never replaces
66+
// one with a version.
4167
func recordedPluginVersions(ctx context.Context) map[string]string {
4268
versions := map[string]string{}
4369
// Load global first, then project, so project overwrites on conflict.
@@ -55,7 +81,7 @@ func recordedPluginVersions(ctx context.Context) map[string]string {
5581
continue
5682
}
5783
for name, rec := range state.Plugins {
58-
if rec.Version != "" {
84+
if _, seen := versions[name]; !seen || rec.Version != "" {
5985
versions[name] = rec.Version
6086
}
6187
}

‎libs/aitools/installer/detect_test.go‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ func TestInstalledTools(t *testing.T) {
5454
projectPlugins: map[string]string{"codex": "0.2.7"},
5555
want: []string{"codex_0.2.7"},
5656
},
57+
{
58+
// A record without a version (its last update ran from a marketplace
59+
// copy that could not be refreshed) yields no token.
60+
name: "state without version omitted",
61+
globalPlugins: map[string]string{"codex": ""},
62+
want: nil,
63+
},
64+
{
65+
// A project record without a version does not hide the global one.
66+
name: "project state without version keeps global",
67+
globalPlugins: map[string]string{"codex": "0.2.7"},
68+
projectPlugins: map[string]string{"codex": ""},
69+
want: []string{"codex_0.2.7"},
70+
},
5771
{
5872
// Manifest (claude) and state (codex) contribute; output is sorted.
5973
name: "mixed sources sorted",
@@ -82,6 +96,57 @@ func TestInstalledTools(t *testing.T) {
8296
}
8397
}
8498

99+
// TestInstalledPluginAgents counts a plugin as installed whether or not its
100+
// version is known, so `databricks version --check` does not suggest installing
101+
// Databricks tooling to a user who has the plugin.
102+
func TestInstalledPluginAgents(t *testing.T) {
103+
tests := []struct {
104+
name string
105+
claudeManifestVersion string
106+
globalPlugins map[string]string
107+
projectPlugins map[string]string
108+
want []string
109+
}{
110+
{
111+
name: "none installed",
112+
want: nil,
113+
},
114+
{
115+
name: "state with version",
116+
globalPlugins: map[string]string{"codex": "0.2.7"},
117+
want: []string{"codex"},
118+
},
119+
{
120+
name: "state without version",
121+
projectPlugins: map[string]string{"codex": ""},
122+
want: []string{"codex"},
123+
},
124+
{
125+
name: "manifest and state sorted",
126+
claudeManifestVersion: "0.2.9",
127+
globalPlugins: map[string]string{"codex": ""},
128+
want: []string{"claude-code", "codex"},
129+
},
130+
}
131+
132+
for _, tc := range tests {
133+
t.Run(tc.name, func(t *testing.T) {
134+
tmp := t.TempDir()
135+
t.Setenv("HOME", tmp)
136+
t.Setenv("USERPROFILE", tmp)
137+
t.Chdir(t.TempDir())
138+
139+
if tc.claudeManifestVersion != "" {
140+
writeClaudeManifest(t, filepath.Join(tmp, ".claude"), tc.claudeManifestVersion)
141+
}
142+
savePluginState(t, GlobalSkillsDir, tc.globalPlugins)
143+
savePluginState(t, ProjectSkillsDir, tc.projectPlugins)
144+
145+
assert.Equal(t, tc.want, InstalledPluginAgents(t.Context()))
146+
})
147+
}
148+
}
149+
85150
// writeClaudeManifest writes a Claude installed_plugins.json recording the
86151
// databricks plugin at the given version under configDir.
87152
func writeClaudeManifest(t *testing.T, configDir, version string) {

0 commit comments

Comments
 (0)