Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ docs/uv.lock
*.code-workspace
*.vscode/
*.claude/
__pycache__/
6 changes: 6 additions & 0 deletions cmd/plugin/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
package plugin

import (
"github.com/datarobot/cli/cmd/plugin/install"
"github.com/datarobot/cli/cmd/plugin/list"
"github.com/datarobot/cli/cmd/plugin/uninstall"
"github.com/datarobot/cli/cmd/plugin/update"
"github.com/spf13/cobra"
)

Expand All @@ -30,6 +33,9 @@ func Cmd() *cobra.Command {

cmd.AddCommand(
list.Cmd(),
install.Cmd(),
uninstall.Cmd(),
update.Cmd(),
)

return cmd
Expand Down
150 changes: 150 additions & 0 deletions cmd/plugin/install/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package install

import (
"fmt"

"github.com/datarobot/cli/cmd/plugin/shared"
"github.com/datarobot/cli/internal/plugin"
"github.com/datarobot/cli/tui"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var (
versionConstraint string
registryURL string
listPlugins bool
listVersions bool
)

func Cmd() *cobra.Command {
cmd := &cobra.Command{
Use: "install <plugin-name>",
Short: "Install a plugin from the remote registry",
Long: `Install a plugin from the remote plugin registry.

The plugin name should match an entry in the plugin registry.
Use --version to specify a version constraint:
- Exact version: 1.2.3
- Caret (compatible): ^1.2.3 (any 1.x.x >= 1.2.3)
- Tilde (patch-level): ~1.2.3 (any 1.2.x >= 1.2.3)
- Minimum: >=1.0.0
- Latest: latest (default)`,
Example: ` dr plugin install apps
dr plugin install apps
dr plugin install apps --version 1.0.0
dr plugin install apps --version "^1.0.0"
dr plugin install apps --versions
dr plugin install --list`,
Args: cobra.MaximumNArgs(1),
RunE: runInstall,
}

cmd.Flags().StringVar(&versionConstraint, "version", "latest", "Version constraint")
cmd.Flags().BoolVar(&listVersions, "versions", false, "List available versions for a plugin")
cmd.Flags().StringVar(&registryURL, "registry-url", plugin.PluginRegistryURL, "URL of the plugin registry")
cmd.Flags().BoolVar(&listPlugins, "list", false, "List available plugins from the registry")

return cmd
}

func runInstall(_ *cobra.Command, args []string) error {
finalRegistryURL := shared.NormalizeRegistryURL(registryURL)
if viper.GetBool("verbose") {
fmt.Printf("Fetching plugin registry from %s...\n", finalRegistryURL)
}

registry, baseURL, err := plugin.FetchRegistry(finalRegistryURL)
if err != nil {
return fmt.Errorf("failed to fetch plugin registry: %w", err)
}

// Handle --list flag or no args (show list by default)
if listPlugins || len(args) == 0 {
fmt.Println()
fmt.Println(tui.SubTitleStyle.Render("Available Plugins"))
printAvailablePlugins(registry)

return nil
}

pluginName := args[0]

// Handle --versions flag
if listVersions {
pluginEntry, ok := registry.Plugins[pluginName]
if !ok {
printAvailablePlugins(registry)

return fmt.Errorf("plugin %q not found in registry", pluginName)
}

fmt.Println()
fmt.Println(tui.SubTitleStyle.Render("Available Versions for " + pluginName))
printAvailableVersions(pluginEntry.Versions)

return nil
}

fmt.Println()
fmt.Println(tui.SubTitleStyle.Render("Installing Plugin"))

pluginEntry, ok := registry.Plugins[pluginName]
if !ok {
printAvailablePlugins(registry)

return fmt.Errorf("plugin %q not found in registry", pluginName)
}

version, err := plugin.ResolveVersion(pluginEntry.Versions, versionConstraint)
if err != nil {
printAvailableVersions(pluginEntry.Versions)

return fmt.Errorf("failed to resolve version: %w", err)
}

fmt.Printf("Installing %s version %s...\n", pluginEntry.Name, version.Version)
fmt.Printf("Downloading from: %s/%s\n", baseURL, version.URL)

if err := plugin.InstallPlugin(pluginEntry, *version, baseURL); err != nil {
return fmt.Errorf("failed to install plugin: %w", err)
}

fmt.Println()
fmt.Println(tui.SuccessStyle.Render("✓ Successfully installed " + pluginEntry.Name + " " + version.Version))
fmt.Println()
fmt.Printf("Run `dr %s --help` to get started.\n", pluginEntry.Name)

return nil
}

func printAvailablePlugins(registry *plugin.PluginRegistry) {
for name, p := range registry.Plugins {
latestVersion := "-"
if len(p.Versions) > 0 {
latestVersion = p.Versions[0].Version
}

fmt.Printf(" - %s (%s): %s\n", name, latestVersion, p.Description)
}
}

func printAvailableVersions(versions []plugin.RegistryVersion) {
for _, v := range versions {
fmt.Printf(" - %s\n", v.Version)
}
}
2 changes: 1 addition & 1 deletion cmd/plugin/list/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func runList(_ *cobra.Command, _ []string) error {
return nil
}

fmt.Println(tui.TitleStyle.Render("Discovered Plugins"))
fmt.Println(tui.SubTitleStyle.Render("Discovered Plugins"))
Comment thread
carsongee marked this conversation as resolved.

nameStyle := tui.BaseTextStyle.
Foreground(tui.GetAdaptiveColor(tui.DrPurple, tui.DrPurpleDark)).
Expand Down
28 changes: 28 additions & 0 deletions cmd/plugin/shared/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package shared

// NormalizeRegistryURL ensures the URL ends with index.json
func NormalizeRegistryURL(url string) string {
if len(url) > 0 && url[len(url)-1] == '/' {
return url + "index.json"
}

if len(url) > 5 && url[len(url)-5:] != ".json" {
return url + "/index.json"
}

return url
}
69 changes: 69 additions & 0 deletions cmd/plugin/uninstall/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package uninstall

import (
"fmt"

"github.com/datarobot/cli/internal/plugin"
"github.com/datarobot/cli/tui"
"github.com/spf13/cobra"
)

func Cmd() *cobra.Command {
return &cobra.Command{
Use: "uninstall <plugin-name>",
Short: "Uninstall a managed plugin",
Long: "Remove a plugin that was installed via `dr plugin install`.",
Example: " dr plugin uninstall apps",
Args: cobra.ExactArgs(1),
RunE: runUninstall,
}
}

func runUninstall(_ *cobra.Command, args []string) error {
pluginName := args[0]

installed, err := plugin.GetInstalledPlugins()
if err != nil {
return fmt.Errorf("failed to get installed plugins: %w", err)
}

var found bool

for _, p := range installed {
if p.Name == pluginName {
found = true

break
}
}

if !found {
return fmt.Errorf("plugin %q is not installed as a managed plugin", pluginName)
}

fmt.Printf("Uninstalling %s...\n", pluginName)

if err := plugin.UninstallPlugin(pluginName); err != nil {
return err
}

fmt.Println()
fmt.Println(tui.SuccessStyle.Render("✓ Successfully uninstalled " + pluginName))
fmt.Println()

return nil
}
Loading
Loading