From 6bea087b9afa73d0c881055e53c109d3bdb54948 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 19 Oct 2026 04:39:57 -0600 Subject: [PATCH] refactor(agentcontainer): split IDE setup out of setup.go and share helpers --- cmd/internal/agentcontainer/setup.go | 224 ---------------------- cmd/internal/agentcontainer/setup_ide.go | 232 +++++++++++++++++++++++ 2 files changed, 232 insertions(+), 224 deletions(-) create mode 100644 cmd/internal/agentcontainer/setup_ide.go diff --git a/cmd/internal/agentcontainer/setup.go b/cmd/internal/agentcontainer/setup.go index ea2c4d1ca..a281bd008 100644 --- a/cmd/internal/agentcontainer/setup.go +++ b/cmd/internal/agentcontainer/setup.go @@ -34,12 +34,6 @@ import ( cliflags "github.com/devsy-org/devsy/pkg/flags" "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/git" - "github.com/devsy-org/devsy/pkg/ide/fleet" - "github.com/devsy-org/devsy/pkg/ide/jetbrains" - "github.com/devsy-org/devsy/pkg/ide/jupyter" - "github.com/devsy-org/devsy/pkg/ide/marimo" - "github.com/devsy-org/devsy/pkg/ide/rstudio" - "github.com/devsy-org/devsy/pkg/ide/vscode" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/status" @@ -675,224 +669,6 @@ func fillContainerEnv(setupInfo *config.Result) error { return nil } -var vscodeFlavors = map[string]vscode.Flavor{ - string(config2.IDEVSCode): vscode.FlavorStable, - string(config2.IDEVSCodeInsiders): vscode.FlavorInsiders, - string(config2.IDECursor): vscode.FlavorCursor, - string(config2.IDEPositron): vscode.FlavorPositron, - string(config2.IDECodium): vscode.FlavorCodium, - string(config2.IDEWindsurf): vscode.FlavorWindsurf, - string(config2.IDEAntigravity): vscode.FlavorAntigravity, - string(config2.IDEBob): vscode.FlavorBob, -} - -type jetbrainsServerFactory func( - string, - map[string]config2.OptionValue, -) *jetbrains.GenericJetBrainsServer - -var jetbrainsServers = map[string]jetbrainsServerFactory{ - string(config2.IDEGoland): jetbrains.NewGolandServer, - string(config2.IDERustRover): jetbrains.NewRustRoverServer, - string(config2.IDEPyCharm): jetbrains.NewPyCharmServer, - string(config2.IDEPhpStorm): jetbrains.NewPhpStorm, - string(config2.IDEIntellij): jetbrains.NewIntellij, - string(config2.IDECLion): jetbrains.NewCLionServer, - string(config2.IDERider): jetbrains.NewRiderServer, - string(config2.IDERubyMine): jetbrains.NewRubyMineServer, - string(config2.IDEWebStorm): jetbrains.NewWebStormServer, - string(config2.IDEDataSpell): jetbrains.NewDataSpellServer, -} - -func (cmd *SetupContainerCmd) installIDE( - setupInfo *config.Result, - ide *provider2.WorkspaceIDEConfig, -) error { - if flavor, ok := vscodeFlavors[ide.Name]; ok { - return cmd.setupVSCode(setupInfo, ide.Options, flavor) - } - if newServer, ok := jetbrainsServers[ide.Name]; ok { - return newServer(config.GetRemoteUser(setupInfo), ide.Options).Install(setupInfo) - } - switch ide.Name { - case string(config2.IDENone): - return nil - case string(config2.IDEOpenVSCode), string(config2.IDECodeServer), string(config2.IDEVSCodeWeb): - return cmd.setupBrowserIDE(ide.Name, setupInfo, ide.Options) - } - - return installNotebookIDE(setupInfo, ide) -} - -func installNotebookIDE( - setupInfo *config.Result, - ide *provider2.WorkspaceIDEConfig, -) error { - user := config.GetRemoteUser(setupInfo) - folder := setupInfo.SubstitutionContext.ContainerWorkspaceFolder - switch ide.Name { - case string(config2.IDEFleet): - return fleet.NewFleetServer(user, ide.Options).Install(folder) - case string(config2.IDEJupyterNotebook): - return jupyter.NewJupyterNotebookServer(folder, user, ide.Options).Install() - case string(config2.IDEMarimo): - return marimo.NewMarimoServer(folder, user, ide.Options).Install() - case string(config2.IDERStudio): - return rstudio.NewRStudioServer(folder, user, ide.Options).Install() - } - - return nil -} - -func (cmd *SetupContainerCmd) setupVSCode( - setupInfo *config.Result, - ideOptions map[string]config2.OptionValue, - flavor vscode.Flavor, -) error { - log.Debugf("setup %s", flavor.DisplayName()) - vsCodeConfiguration := config.GetVSCodeConfiguration(setupInfo.MergedConfig) - formattedSettings, err := formatVSCodeSettings(vsCodeConfiguration.Settings) - if err != nil { - return err - } - log.Debugf("vscode settings:\n%s", formattedSettings) - settings := "" - if len(vsCodeConfiguration.Settings) > 0 { - out, err := json.Marshal(vsCodeConfiguration.Settings) - if err != nil { - return err - } - - settings = string(out) - } - - user := config.GetRemoteUser(setupInfo) - err = vscode.NewVSCodeServer(vscode.ServerOptions{ - Extensions: vsCodeConfiguration.Extensions, - Settings: settings, - UserName: user, - Values: ideOptions, - Flavor: flavor, - }).Install() - if err != nil { - return err - } - - // don't install code-server if we don't have settings or extensions - if len(vsCodeConfiguration.Settings) == 0 && len(vsCodeConfiguration.Extensions) == 0 { - return nil - } - - if len(vsCodeConfiguration.Extensions) == 0 { - return nil - } - - return command.StartBackgroundOnce( - fmt.Sprintf("%s-async", flavor), - func() (*exec.Cmd, error) { - log.Infof( - "installing extensions in the background: %s", - strings.Join(vsCodeConfiguration.Extensions, ","), - ) - binaryPath, err := os.Executable() - if err != nil { - return nil, err - } - - args := []string{ - cmdInternal, cmdAgent, cmdContainer, "vscode-async", - names.Flag(names.SetupInfo), cmd.SetupInfo, - names.Flag(names.Flavor), string(flavor), - } - - //nolint:gosec // binaryPath is from os.Executable(), not user input - return exec.Command(binaryPath, args...), nil - }) -} - -func formatVSCodeSettings(settings map[string]any) (string, error) { - formatted, err := json.MarshalIndent(settings, "", " ") - if err != nil { - return "", err - } - - return string(formatted), nil -} - -// setupBrowserIDE installs and starts any of the browser-based IDEs -// (openvscode, code-server, VS Code Web). The per-IDE differences live in the -// browserIDEs registry, so the install/extensions/start flow is written once. -func (cmd *SetupContainerCmd) setupBrowserIDE( - ideName string, - setupInfo *config.Result, - ideOptions map[string]config2.OptionValue, -) error { - b, ok := browserIDEByName(ideName) - if !ok { - return fmt.Errorf("unknown browser IDE %q", ideName) - } - log.Debugf("setup %s", b.name) - - vsCodeConfiguration := config.GetVSCodeConfiguration(setupInfo.MergedConfig) - settings := "" - if len(vsCodeConfiguration.Settings) > 0 { - out, err := json.Marshal(vsCodeConfiguration.Settings) - if err != nil { - return err - } - settings = string(out) - } - - server := b.newServer(browserServerSpec{ - extensions: vsCodeConfiguration.Extensions, - settings: settings, - userName: config.GetRemoteUser(setupInfo), - host: "0.0.0.0", - port: strconv.Itoa(b.defaultPort), - values: ideOptions, - }) - - if err := server.Install(); err != nil { - return err - } - - if len(vsCodeConfiguration.Extensions) > 0 { - if err := cmd.startBrowserExtensionsInstall(b, vsCodeConfiguration.Extensions); err != nil { - return fmt.Errorf("install extensions: %w", err) - } - } - - return server.Start() -} - -// startBrowserExtensionsInstall launches the IDE's `-async` command as a -// background process so a slow marketplace fetch never blocks setup. -func (cmd *SetupContainerCmd) startBrowserExtensionsInstall( - b browserIDE, - extensions []string, -) error { - return command.StartBackgroundOnce(b.asyncCmd, func() (*exec.Cmd, error) { - log.Infof( - "installing extensions in the background: %s", - strings.Join(extensions, ","), - ) - binaryPath, err := os.Executable() - if err != nil { - return nil, err - } - //nolint:gosec // binaryPath is from os.Executable(), not user input - return exec.Command( - binaryPath, - cmdInternal, - cmdAgent, - cmdContainer, - b.asyncCmd, - names.Flag(names.SetupInfo), - cmd.SetupInfo, - ), nil - }) -} - func configureSystemGitCredentials( ctx context.Context, client tunnel.TunnelClient, diff --git a/cmd/internal/agentcontainer/setup_ide.go b/cmd/internal/agentcontainer/setup_ide.go new file mode 100644 index 000000000..d7b845da7 --- /dev/null +++ b/cmd/internal/agentcontainer/setup_ide.go @@ -0,0 +1,232 @@ +//go:build !windows + +package agentcontainer + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/devsy-org/devsy/pkg/command" + config2 "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" + "github.com/devsy-org/devsy/pkg/ide/fleet" + "github.com/devsy-org/devsy/pkg/ide/jetbrains" + "github.com/devsy-org/devsy/pkg/ide/jupyter" + "github.com/devsy-org/devsy/pkg/ide/marimo" + "github.com/devsy-org/devsy/pkg/ide/rstudio" + "github.com/devsy-org/devsy/pkg/ide/vscode" + "github.com/devsy-org/devsy/pkg/log" + provider2 "github.com/devsy-org/devsy/pkg/provider" +) + +var vscodeFlavors = map[string]vscode.Flavor{ + string(config2.IDEVSCode): vscode.FlavorStable, + string(config2.IDEVSCodeInsiders): vscode.FlavorInsiders, + string(config2.IDECursor): vscode.FlavorCursor, + string(config2.IDEPositron): vscode.FlavorPositron, + string(config2.IDECodium): vscode.FlavorCodium, + string(config2.IDEWindsurf): vscode.FlavorWindsurf, + string(config2.IDEAntigravity): vscode.FlavorAntigravity, + string(config2.IDEBob): vscode.FlavorBob, +} + +type jetbrainsServerFactory func( + string, + map[string]config2.OptionValue, +) *jetbrains.GenericJetBrainsServer + +var jetbrainsServers = map[string]jetbrainsServerFactory{ + string(config2.IDEGoland): jetbrains.NewGolandServer, + string(config2.IDERustRover): jetbrains.NewRustRoverServer, + string(config2.IDEPyCharm): jetbrains.NewPyCharmServer, + string(config2.IDEPhpStorm): jetbrains.NewPhpStorm, + string(config2.IDEIntellij): jetbrains.NewIntellij, + string(config2.IDECLion): jetbrains.NewCLionServer, + string(config2.IDERider): jetbrains.NewRiderServer, + string(config2.IDERubyMine): jetbrains.NewRubyMineServer, + string(config2.IDEWebStorm): jetbrains.NewWebStormServer, + string(config2.IDEDataSpell): jetbrains.NewDataSpellServer, +} + +func (cmd *SetupContainerCmd) installIDE( + setupInfo *config.Result, + ide *provider2.WorkspaceIDEConfig, +) error { + if flavor, ok := vscodeFlavors[ide.Name]; ok { + return cmd.setupVSCode(setupInfo, ide.Options, flavor) + } + if newServer, ok := jetbrainsServers[ide.Name]; ok { + return newServer(config.GetRemoteUser(setupInfo), ide.Options).Install(setupInfo) + } + switch ide.Name { + case string(config2.IDENone): + return nil + case string(config2.IDEOpenVSCode), string(config2.IDECodeServer), string(config2.IDEVSCodeWeb): + return cmd.setupBrowserIDE(ide.Name, setupInfo, ide.Options) + } + + return installNotebookIDE(setupInfo, ide) +} + +func installNotebookIDE( + setupInfo *config.Result, + ide *provider2.WorkspaceIDEConfig, +) error { + user := config.GetRemoteUser(setupInfo) + folder := setupInfo.SubstitutionContext.ContainerWorkspaceFolder + switch ide.Name { + case string(config2.IDEFleet): + return fleet.NewFleetServer(user, ide.Options).Install(folder) + case string(config2.IDEJupyterNotebook): + return jupyter.NewJupyterNotebookServer(folder, user, ide.Options).Install() + case string(config2.IDEMarimo): + return marimo.NewMarimoServer(folder, user, ide.Options).Install() + case string(config2.IDERStudio): + return rstudio.NewRStudioServer(folder, user, ide.Options).Install() + } + + return nil +} + +func (cmd *SetupContainerCmd) setupVSCode( + setupInfo *config.Result, + ideOptions map[string]config2.OptionValue, + flavor vscode.Flavor, +) error { + log.Debugf("setup %s", flavor.DisplayName()) + vsCodeConfiguration := config.GetVSCodeConfiguration(setupInfo.MergedConfig) + formattedSettings, err := formatVSCodeSettings(vsCodeConfiguration.Settings) + if err != nil { + return err + } + log.Debugf("vscode settings:\n%s", formattedSettings) + settings, err := vscodeSettingsJSON(vsCodeConfiguration.Settings) + if err != nil { + return err + } + + user := config.GetRemoteUser(setupInfo) + err = vscode.NewVSCodeServer(vscode.ServerOptions{ + Extensions: vsCodeConfiguration.Extensions, + Settings: settings, + UserName: user, + Values: ideOptions, + Flavor: flavor, + }).Install() + if err != nil { + return err + } + + if len(vsCodeConfiguration.Extensions) == 0 { + return nil + } + + return cmd.startExtensionsInstallInBackground( + fmt.Sprintf("%s-async", flavor), + "vscode-async", + vsCodeConfiguration.Extensions, + names.Flag(names.Flavor), string(flavor), + ) +} + +func formatVSCodeSettings(settings map[string]any) (string, error) { + formatted, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return "", err + } + + return string(formatted), nil +} + +func vscodeSettingsJSON(settings map[string]any) (string, error) { + if len(settings) == 0 { + return "", nil + } + out, err := json.Marshal(settings) + if err != nil { + return "", err + } + + return string(out), nil +} + +// setupBrowserIDE installs and starts any of the browser-based IDEs +// (openvscode, code-server, VS Code Web). The per-IDE differences live in the +// browserIDEs registry, so the install/extensions/start flow is written once. +func (cmd *SetupContainerCmd) setupBrowserIDE( + ideName string, + setupInfo *config.Result, + ideOptions map[string]config2.OptionValue, +) error { + b, ok := browserIDEByName(ideName) + if !ok { + return fmt.Errorf("unknown browser IDE %q", ideName) + } + log.Debugf("setup %s", b.name) + + vsCodeConfiguration := config.GetVSCodeConfiguration(setupInfo.MergedConfig) + settings, err := vscodeSettingsJSON(vsCodeConfiguration.Settings) + if err != nil { + return err + } + + server := b.newServer(browserServerSpec{ + extensions: vsCodeConfiguration.Extensions, + settings: settings, + userName: config.GetRemoteUser(setupInfo), + host: "0.0.0.0", + port: strconv.Itoa(b.defaultPort), + values: ideOptions, + }) + + if err := server.Install(); err != nil { + return err + } + + if len(vsCodeConfiguration.Extensions) > 0 { + if err := cmd.startBrowserExtensionsInstall(b, vsCodeConfiguration.Extensions); err != nil { + return fmt.Errorf("install extensions: %w", err) + } + } + + return server.Start() +} + +// startBrowserExtensionsInstall launches the IDE's `-async` command as a +// background process so a slow marketplace fetch never blocks setup. +func (cmd *SetupContainerCmd) startBrowserExtensionsInstall( + b browserIDE, + extensions []string, +) error { + return cmd.startExtensionsInstallInBackground(b.asyncCmd, b.asyncCmd, extensions) +} + +func (cmd *SetupContainerCmd) startExtensionsInstallInBackground( + lockKey, subcommand string, + extensions []string, + extraArgs ...string, +) error { + return command.StartBackgroundOnce(lockKey, func() (*exec.Cmd, error) { + log.Infof( + "installing extensions in the background: %s", + strings.Join(extensions, ","), + ) + binaryPath, err := os.Executable() + if err != nil { + return nil, err + } + + args := []string{ + cmdInternal, cmdAgent, cmdContainer, subcommand, + names.Flag(names.SetupInfo), cmd.SetupInfo, + } + args = append(args, extraArgs...) + //nolint:gosec // binaryPath is from os.Executable(), not user input + return exec.Command(binaryPath, args...), nil + }) +}