Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2e472de
chore(provider): scaffold new CLI files
skevetter May 30, 2026
d09dfba
feat(workspace): scaffold ProviderVersion type and listing API
skevetter May 30, 2026
e57a338
feat(workspace): classify provider source shape for version listing
skevetter May 30, 2026
ec05280
feat(workspace): list provider versions from GitHub Releases
skevetter May 30, 2026
00eb241
feat(workspace): list provider versions from sibling manifest
skevetter May 30, 2026
5e1bf9d
feat(workspace): dispatch ListProviderVersions by source kind
skevetter May 30, 2026
dcc8c06
feat(workspace): implement SetProviderVersion
skevetter May 30, 2026
32d6c6f
feat(workspace): persistent provider-version cache
skevetter May 30, 2026
b47bdad
feat(workspace): cache provider version listings
skevetter May 30, 2026
d7bd735
feat(workspace): aggregate per-provider update checks via CheckAllPro…
skevetter May 30, 2026
2d5e7bd
refactor(provider): extract shared configure helpers; configure no lo…
skevetter May 30, 2026
49b14e0
refactor(provider): add/update --use explicitly writes DefaultProvider
skevetter May 30, 2026
7942b7a
feat(provider): add 'configure' subcommand
skevetter May 30, 2026
289908f
feat(provider): add 'default' subcommand
skevetter May 30, 2026
a60f435
refactor(provider): rename delete subcommand to remove
skevetter May 30, 2026
fb35c9e
refactor(provider): merge list-available into list --available
skevetter May 30, 2026
ceb47af
refactor(provider): rename options/set-options to get/set
skevetter May 30, 2026
92a4138
refactor(provider): delete 'use' subcommand
skevetter May 30, 2026
bb12ea7
feat(provider): update --version flag
skevetter May 30, 2026
3104d90
feat(provider): add 'versions' subcommand
skevetter May 30, 2026
be92387
refactor: update internal references to renamed provider subcommands
skevetter May 30, 2026
1a39157
fix(desktop): update IPC handlers to new provider CLI verbs
skevetter May 30, 2026
f99e42d
feat(desktop): add IPC for provider version listing, swapping, and up…
skevetter May 30, 2026
ce9c987
feat(desktop): periodic provider update check on launch and every 6h
skevetter May 30, 2026
c398fdb
feat(desktop): providerVersions store
skevetter May 30, 2026
3294b5c
feat(desktop): default badge + update indicator on provider cards
skevetter May 30, 2026
36be045
feat(desktop): default badge in provider sheet
skevetter May 30, 2026
a810e64
feat(desktop): version selection and default badge on provider detail
skevetter May 30, 2026
cc62219
feat(desktop): wizard removes default-promotion step
skevetter May 30, 2026
b7be022
feat(desktop): pre-select default provider in workspace wizard
skevetter May 30, 2026
d274e66
docs: update provider CLI references to new subcommand layout
skevetter May 30, 2026
a83a1b6
fix: address CI lint, e2e, and commitlint failures
skevetter May 30, 2026
6b44ffe
fix: address coderabbit review comments on PR #465
skevetter May 30, 2026
f31aa96
refactor: apply remaining coderabbit nits
skevetter May 30, 2026
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
6 changes: 3 additions & 3 deletions cmd/provider/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ func (cmd *AddCmd) Run(ctx context.Context, devsyConfig *config.Config, args []s
return fmt.Errorf("configure provider: %w", configureErr)
}

return nil
return writeDefaultProvider(cmd.Context, providerConfig.Name)
}

log.Infof("To use the provider, please run the following command:")
log.Infof("devsy provider use %s", providerConfig.Name)
log.Infof("To configure the provider, please run the following command:")
log.Infof("devsy provider configure %s", providerConfig.Name)
return nil
}

Expand Down
76 changes: 76 additions & 0 deletions cmd/provider/configure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package provider

import (
"github.com/devsy-org/devsy/cmd/completion"
"github.com/devsy-org/devsy/cmd/flags"
"github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/workspace"
"github.com/spf13/cobra"
)

// ConfigureCmd holds flags for the `provider configure` subcommand.
type ConfigureCmd struct {
*flags.GlobalFlags
Reconfigure bool
SingleMachine bool
Options []string
SkipInit bool
}

// NewConfigureCmd creates the cobra command for `provider configure`.
func NewConfigureCmd(f *flags.GlobalFlags) *cobra.Command {
cmd := &ConfigureCmd{GlobalFlags: f}
configureCmd := &cobra.Command{
Use: "configure [name]",
Short: "Re-run init and option resolution for an existing provider",
Args: cobra.MaximumNArgs(1),
RunE: func(cobraCmd *cobra.Command, args []string) error {
devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
if err != nil {
return err
}
name, err := resolveProviderName(args, devsyConfig.Current().DefaultProvider)
if err != nil {
return err
}
p, err := workspace.FindProvider(devsyConfig, name)
if err != nil {
return err
}
return ConfigureProvider(cobraCmd.Context(), ProviderOptionsConfig{
Provider: p.Config,
Context: devsyConfig.DefaultContext,
UserOptions: cmd.Options,
Reconfigure: cmd.Reconfigure,
SkipRequired: false,
SkipInit: cmd.SkipInit,
SkipSubOptions: false,
SingleMachine: &cmd.SingleMachine,
})
},
ValidArgsFunction: func(
rootCmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
return completion.GetProviderSuggestions(
rootCmd,
cmd.Context,
cmd.Provider,
args,
toComplete,
cmd.Owner,
)
},
}
configureCmd.Flags().
BoolVar(&cmd.Reconfigure, "reconfigure", false, "Force re-resolution of all options")
configureCmd.Flags().
BoolVar(&cmd.SingleMachine, "single-machine", false, "Use a single machine for all workspaces")
configureCmd.Flags().
StringArrayVarP(&cmd.Options, "option", "o", []string{}, "Provider option in the form KEY=VALUE")
configureCmd.Flags().
BoolVar(&cmd.SkipInit, "skip-init", false, "Skip provider init (testing only)")
_ = configureCmd.Flags().MarkHidden("skip-init")
return configureCmd
}
166 changes: 48 additions & 118 deletions cmd/provider/use.go → cmd/provider/configure_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,123 +6,14 @@ import (
"fmt"
"io"

"github.com/devsy-org/devsy/cmd/completion"
"github.com/devsy-org/devsy/cmd/flags"
"github.com/devsy-org/devsy/pkg/client/clientimplementation"
"github.com/devsy-org/devsy/pkg/config"
cliErrors "github.com/devsy-org/devsy/pkg/errors"
"github.com/devsy-org/devsy/pkg/log"
options2 "github.com/devsy-org/devsy/pkg/options"
provider2 "github.com/devsy-org/devsy/pkg/provider"
"github.com/devsy-org/devsy/pkg/workspace"
"github.com/spf13/cobra"
)

// UseCmd holds the use cmd flags.
type UseCmd struct {
*flags.GlobalFlags

Reconfigure bool
SingleMachine bool
Options []string

// only for testing
SkipInit bool
}

// NewUseCmd creates a new command.
func NewUseCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &UseCmd{
GlobalFlags: flags,
}
useCmd := &cobra.Command{
Use: "use [name]",
Short: "Configure an existing provider and set as default",
RunE: func(cobraCmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("please specify the provider to use")
}

return cmd.Run(cobraCmd.Context(), args[0])
},
ValidArgsFunction: func(rootCmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completion.GetProviderSuggestions(
rootCmd,
cmd.Context,
cmd.Provider,
args,
toComplete,
cmd.Owner,
)
},
}

AddFlags(useCmd, cmd)
return useCmd
}

func AddFlags(useCmd *cobra.Command, cmd *UseCmd) {
useCmd.Flags().
BoolVar(&cmd.SingleMachine, "single-machine", false, "If enabled will use a single machine for all workspaces")
useCmd.Flags().
BoolVar(&cmd.Reconfigure, "reconfigure", false, "If enabled will not merge existing provider config")
useCmd.Flags().
StringArrayVarP(&cmd.Options, "option", "o", []string{}, "Provider option in the form KEY=VALUE")

useCmd.Flags().
BoolVar(&cmd.SkipInit, "skip-init", false, "ONLY FOR TESTING: If true will skip init")
_ = useCmd.Flags().MarkHidden("skip-init")
}

// Run runs the command logic.
func (cmd *UseCmd) Run(ctx context.Context, providerName string) error {
devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
if err != nil {
return err
}

providerWithOptions, err := workspace.FindProvider(devsyConfig, providerName)
if err != nil {
return err
}

// should reconfigure?
shouldReconfigure := cmd.Reconfigure || len(cmd.Options) > 0 ||
providerWithOptions.State == nil ||
cmd.SingleMachine
if shouldReconfigure {
return ConfigureProvider(ctx, ProviderOptionsConfig{
Provider: providerWithOptions.Config,
Context: devsyConfig.DefaultContext,
UserOptions: cmd.Options,
Reconfigure: cmd.Reconfigure,
SkipRequired: false,
SkipInit: cmd.SkipInit,
SkipSubOptions: false,
SingleMachine: &cmd.SingleMachine,
})
} else {
log.Infof(
"To reconfigure provider %s, run with '--reconfigure' to reconfigure the provider",
providerWithOptions.Config.Name,
)
}

// set options
defaultContext := devsyConfig.Current()
defaultContext.DefaultProvider = providerWithOptions.Config.Name

// save provider config
err = config.SaveConfig(devsyConfig)
if err != nil {
return fmt.Errorf("save config: %w", err)
}

// print success message
log.Infof("switched default provider: providerName=%s", providerWithOptions.Config.Name)
return nil
}

type ProviderOptionsConfig struct {
Provider *provider2.ProviderConfig
Context string
Expand All @@ -140,11 +31,8 @@ func ConfigureProvider(ctx context.Context, cfg ProviderOptionsConfig) error {
return err
}

// set options
defaultContext := devsyConfig.Current()
defaultContext.DefaultProvider = cfg.Provider.Name

// save provider config
// save provider config (configureProviderOptions may have mutated state,
// e.g. via initProvider marking the provider Initialized)
err = config.SaveConfig(devsyConfig)
if err != nil {
return fmt.Errorf("save config: %w", err)
Expand Down Expand Up @@ -208,7 +96,7 @@ func configureProviderOptions(
stderr := log.Writer(log.LevelError)
defer func() { _ = stderr.Close() }()

err = initProvider(ctx, devsyConfig, cfg.Provider, stdout, stderr)
err = initProvider(ctx, devsyConfig, cfg.Provider, initIO{stdout: stdout, stderr: stderr})
if err != nil {
return nil, err
}
Expand All @@ -217,11 +105,53 @@ func configureProviderOptions(
return devsyConfig, nil
}

// writeDefaultProvider reloads the config for the given context and writes providerName
// as the active context's DefaultProvider.
func writeDefaultProvider(contextName, providerName string) error {
cfg, err := config.LoadConfig(contextName, "")
if err != nil {
return fmt.Errorf("reload config: %w", err)
}
cfg.Current().DefaultProvider = providerName
if err := config.SaveConfig(cfg); err != nil {
return fmt.Errorf("save default provider: %w", err)
}
return nil
}

// resolveProviderName returns the provider name from args[0] if present, else the fallback
// (typically the active context's DefaultProvider). Errors when neither is available.
func resolveProviderName(args []string, defaultProvider string) (string, error) {
if len(args) > 0 {
return args[0], nil
}
if defaultProvider == "" {
return "", fmt.Errorf("please specify a provider")
}
return defaultProvider, nil
}

// assertProviderMatchesGlobal returns an error when both the resolved provider name and
// the --provider global flag are set but disagree.
func assertProviderMatchesGlobal(resolved, globalFlag string) error {
if resolved == "" || globalFlag == "" || resolved == globalFlag {
return nil
}
log.Infof("providerName=%+v", resolved)
log.Infof("GlobalFlags.Provider=%+v", globalFlag)
return fmt.Errorf("ambiguous provider configuration detected")
}

type initIO struct {
stdout io.Writer
stderr io.Writer
}

func initProvider(
ctx context.Context,
devsyConfig *config.Config,
provider *provider2.ProviderConfig,
stdout, stderr io.Writer,
io2 initIO,
) error {
// Capture the sub-binary's stderr in parallel with forwarding it to the
// regular log sink so that errors.Classify has the real provider output
Expand All @@ -234,8 +164,8 @@ func initProvider(
Context: devsyConfig.DefaultContext,
Options: devsyConfig.ProviderOptions(provider.Name),
Config: provider,
Stdout: stdout,
Stderr: io.MultiWriter(stderr, stderrBuf),
Stdout: io2.stdout,
Stderr: io.MultiWriter(io2.stderr, stderrBuf),
})
if err != nil {
return cliErrors.Classify(fmt.Errorf("init: %w", err), cliErrors.ClassifyContext{
Expand Down
40 changes: 40 additions & 0 deletions cmd/provider/configure_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package provider

import (
"testing"

"github.com/devsy-org/devsy/cmd/flags"
)

func TestNewConfigureCmd(t *testing.T) {
cmd := NewConfigureCmd(&flags.GlobalFlags{})
if cmd.Use != "configure [name]" {
t.Errorf("Use: got %q want %q", cmd.Use, "configure [name]")
}
if cmd.Short == "" {
t.Error("Short must be set")
}
// Verify flags exist
for _, flag := range []string{"reconfigure", "single-machine", "option", "skip-init"} {
if cmd.Flag(flag) == nil {
t.Errorf("missing flag %q", flag)
}
}
// Verify skip-init is hidden
if !cmd.Flag("skip-init").Hidden {
t.Error("skip-init must be hidden")
}
}

func TestResolveProviderName(t *testing.T) {
if got, err := resolveProviderName([]string{testProviderFoo}, "fallback"); err != nil ||
got != testProviderFoo {
t.Fatalf("explicit arg should win: got %q err %v", got, err)
}
if got, err := resolveProviderName([]string{}, "fallback"); err != nil || got != "fallback" {
t.Fatalf("fallback should be used: got %q err %v", got, err)
}
if _, err := resolveProviderName([]string{}, ""); err == nil {
t.Fatal("empty args + empty fallback must error")
}
}
Loading
Loading