{#snippet child({ props })}
{/snippet}
@@ -558,6 +566,7 @@ function selectTemplate(t: { name: string; source: string }) {
onSelect={() => { selectedIde = ide.value; ideComboOpen = false; ideSearch = "" }}
>
+
{ide.label}
{/each}
@@ -628,7 +637,12 @@ function selectTemplate(t: { name: string; source: string }) {
{/if}
IDE
-
{ideLabel}
+
+ {#if ideIconSrc}
+
+ {/if}
+ {ideLabel}
+
Workspace ID
diff --git a/pkg/config/ide.go b/pkg/config/ide.go
index 441142d99..828ce6f09 100644
--- a/pkg/config/ide.go
+++ b/pkg/config/ide.go
@@ -7,6 +7,7 @@ const (
IDEVSCode IDE = "vscode"
IDEVSCodeInsiders IDE = "vscode-insiders"
IDEOpenVSCode IDE = "openvscode"
+ IDECodeServer IDE = "code-server"
IDEIntellij IDE = "intellij"
IDEGoland IDE = "goland"
IDERustRover IDE = "rustrover"
diff --git a/pkg/ide/codeserver/codeserver.go b/pkg/ide/codeserver/codeserver.go
new file mode 100644
index 000000000..7479cb07c
--- /dev/null
+++ b/pkg/ide/codeserver/codeserver.go
@@ -0,0 +1,336 @@
+package codeserver
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+
+ "github.com/devsy-org/devsy/pkg/command"
+ "github.com/devsy-org/devsy/pkg/config"
+ copy2 "github.com/devsy-org/devsy/pkg/copy"
+ "github.com/devsy-org/devsy/pkg/extract"
+ devsyhttp "github.com/devsy-org/devsy/pkg/http"
+ "github.com/devsy-org/devsy/pkg/ide"
+ "github.com/devsy-org/devsy/pkg/ide/vscode"
+ "github.com/devsy-org/devsy/pkg/log"
+ "github.com/devsy-org/devsy/pkg/util"
+)
+
+// Release URL format: tag has the `v` prefix; the asset filename does not.
+const (
+ downloadAmd64Template = "https://github.com/coder/code-server/releases/download/" +
+ "v%s/code-server-%s-linux-amd64.tar.gz"
+ downloadArm64Template = "https://github.com/coder/code-server/releases/download/" +
+ "v%s/code-server-%s-linux-arm64.tar.gz"
+)
+
+const (
+ ForwardPortsOption = "FORWARD_PORTS"
+ BindAddressOption = "BIND_ADDRESS"
+ VersionOption = "VERSION"
+ DownloadAmd64Option = "DOWNLOAD_AMD64"
+ DownloadArm64Option = "DOWNLOAD_ARM64"
+)
+
+var Options = ide.Options{
+ ForwardPortsOption: {
+ Name: ForwardPortsOption,
+ Description: "If Devsy should automatically do port-forwarding",
+ Default: "true",
+ Enum: []string{"true", "false"},
+ },
+ BindAddressOption: {
+ Name: BindAddressOption,
+ Description: "The address to bind code-server to locally, e.g. 0.0.0.0:12345",
+ Default: "",
+ },
+ VersionOption: {
+ Name: VersionOption,
+ Description: "The version for the code-server binary (without the leading v)",
+ Default: "4.121.0",
+ },
+ DownloadArm64Option: {
+ Name: DownloadArm64Option,
+ Description: "The download url for the arm64 code-server binary",
+ },
+ DownloadAmd64Option: {
+ Name: DownloadAmd64Option,
+ Description: "The download url for the amd64 code-server binary",
+ },
+}
+
+// DefaultCodeServerPort sits next to openvscode's 10800 so a host running both
+// IDEs only collides under the FindAvailablePort fallback path, not by default.
+const DefaultCodeServerPort = 10801
+
+// ServerOptions configures a CodeServer instance.
+type ServerOptions struct {
+ Extensions []string
+ Settings string
+ UserName string
+ Host string
+ Port string
+ Values map[string]config.OptionValue
+}
+
+// CodeServer installs and runs code-server (coder.com) inside the workspace.
+type CodeServer struct {
+ values map[string]config.OptionValue
+ extensions []string
+ settings string
+ userName string
+ host string
+ port string
+}
+
+func NewCodeServer(opts ServerOptions) *CodeServer {
+ host := opts.Host
+ if host == "" {
+ host = "0.0.0.0"
+ }
+ port := opts.Port
+ if port == "" {
+ port = strconv.Itoa(DefaultCodeServerPort)
+ }
+ return &CodeServer{
+ values: opts.Values,
+ extensions: opts.Extensions,
+ settings: opts.Settings,
+ userName: opts.UserName,
+ host: host,
+ port: port,
+ }
+}
+
+// Install downloads the code-server release tarball, extracts it under
+// /.code-server, and writes settings.json. Idempotent: returns nil
+// without re-downloading when the binary is already present.
+func (c *CodeServer) Install() error {
+ location, err := prepareCodeServerLocation(c.userName)
+ if err != nil {
+ return err
+ }
+
+ if _, err := os.Stat(filepath.Join(location, "bin", "code-server")); err == nil {
+ return nil
+ }
+
+ vscode.InstallAPKRequirements()
+
+ if err := downloadAndExtract(c.getReleaseURL(), location); err != nil {
+ return err
+ }
+
+ if c.userName != "" {
+ if err := copy2.ChownR(location, c.userName); err != nil {
+ return fmt.Errorf("chown: %w", err)
+ }
+ }
+
+ if err := c.installSettings(); err != nil {
+ return fmt.Errorf("install settings: %w", err)
+ }
+
+ return nil
+}
+
+// InstallExtensions installs each requested extension via the code-server CLI.
+// Partial failures are logged and tolerated; an aggregated error is returned
+// only when every requested extension fails to install.
+func (c *CodeServer) InstallExtensions() error {
+ if err := c.installExtensions(); err != nil {
+ return fmt.Errorf("install extensions: %w", err)
+ }
+ return nil
+}
+
+// Start launches code-server as a background process bound to host:port.
+// Idempotent via command.StartBackgroundOnce.
+func (c *CodeServer) Start() error {
+ location, err := prepareCodeServerLocation(c.userName)
+ if err != nil {
+ return err
+ }
+
+ binaryPath := filepath.Join(location, "bin", "code-server")
+ if _, err := os.Stat(binaryPath); err != nil {
+ return fmt.Errorf("find binary: %w", err)
+ }
+
+ homeFolder, err := userHome(c.userName)
+ if err != nil {
+ return err
+ }
+ // Pin --user-data-dir so the settings install path stays stable regardless
+ // of XDG_DATA_HOME in the container environment.
+ userDataDir := filepath.Join(homeFolder, ".local", "share", "code-server")
+
+ return command.StartBackgroundOnce("code-server", func() (*exec.Cmd, error) {
+ log.Infof("Starting code-server in background...")
+ // --auth none is safe for clients outside the container because the
+ // only published path is the devsy port-forward tunnel, which is
+ // itself authenticated. Intra-container access is still unauthenticated.
+ runCommand := fmt.Sprintf(
+ "%s --bind-addr '%s' --user-data-dir '%s' --auth none "+
+ "--disable-telemetry --disable-update-check",
+ binaryPath, c.host+":"+c.port, userDataDir,
+ )
+ return suOrSh(c.userName, runCommand, location), nil
+ })
+}
+
+func (c *CodeServer) getReleaseURL() string {
+ version := Options.GetValue(c.values, VersionOption)
+
+ var url string
+ if runtime.GOARCH == "arm64" {
+ url = Options.GetValue(c.values, DownloadArm64Option)
+ if url == "" {
+ url = fmt.Sprintf(downloadArm64Template, version, version)
+ }
+ } else {
+ url = Options.GetValue(c.values, DownloadAmd64Option)
+ if url == "" {
+ url = fmt.Sprintf(downloadAmd64Template, version, version)
+ }
+ }
+ return url
+}
+
+func (c *CodeServer) installExtensions() error {
+ if len(c.extensions) == 0 {
+ return nil
+ }
+
+ location, err := prepareCodeServerLocation(c.userName)
+ if err != nil {
+ return err
+ }
+
+ out := log.Writer(log.LevelInfo)
+ defer func() { _ = out.Close() }()
+
+ binaryPath := filepath.Join(location, "bin", "code-server")
+ var failed []string
+ for _, extension := range c.extensions {
+ log.Info("Install extension " + extension + "...")
+ runCommand := fmt.Sprintf("%s --install-extension '%s'", binaryPath, extension)
+ cmd := suOrSh(c.userName, runCommand, "")
+ cmd.Stdout = out
+ cmd.Stderr = out
+ if err := cmd.Run(); err != nil {
+ log.Errorf("failed installing extension: extension=%s, error=%v", extension, err)
+ failed = append(failed, extension)
+ } else {
+ log.Infof("installed extension: extension=%s", extension)
+ }
+ }
+
+ // Escalate only on total failure — partial failures stay as logged
+ // warnings to match openvscode's behavior.
+ if len(failed) == len(c.extensions) {
+ return fmt.Errorf("all %d extensions failed to install: %v", len(failed), failed)
+ }
+ return nil
+}
+
+// installSettings writes settings.json to code-server's user config dir,
+// scoping chown to the code-server subtree to avoid clobbering other ~/.local
+// ownership.
+func (c *CodeServer) installSettings() error {
+ if len(c.settings) == 0 {
+ return nil
+ }
+
+ homeFolder, err := userHome(c.userName)
+ if err != nil {
+ return err
+ }
+ codeServerDataDir := filepath.Join(homeFolder, ".local", "share", "code-server")
+ settingsDir := filepath.Join(codeServerDataDir, "User")
+ // #nosec G301 -- match openvscode-server convention for parity.
+ if err := os.MkdirAll(settingsDir, 0o755); err != nil {
+ return err
+ }
+ if err := os.WriteFile(
+ filepath.Join(settingsDir, "settings.json"),
+ []byte(c.settings),
+ 0o600,
+ ); err != nil {
+ return err
+ }
+ if c.userName != "" {
+ if err := copy2.ChownR(codeServerDataDir, c.userName); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// downloadAndExtract fetches the release tarball at url and extracts it under
+// location. Cleans up a partial extraction on failure so retries start fresh.
+func downloadAndExtract(url, location string) error {
+ resp, err := devsyhttp.GetHTTPClient().Get(url) // #nosec G107 -- URL comes from VersionOption.
+ if err != nil {
+ return err
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode >= http.StatusBadRequest {
+ return fmt.Errorf("download code-server: %s returned %s", url, resp.Status)
+ }
+
+ if err := extract.Extract(resp.Body, location, extract.StripLevels(1)); err != nil {
+ if rmErr := os.RemoveAll(location); rmErr != nil {
+ log.Warnf("cleanup partial install: path=%s err=%v", location, rmErr)
+ }
+ return fmt.Errorf("extract code-server: %w", err)
+ }
+ return nil
+}
+
+// suOrSh builds an *exec.Cmd that runs runCommand either as the unprivileged
+// user (via `su -c`) or as the current user (via `sh -c`). All arg
+// elements are constants from this package — runCommand itself is built from
+// constants plus internal values, not user-controlled binary paths.
+func suOrSh(userName, runCommand, workingDir string) *exec.Cmd {
+ var cmd *exec.Cmd
+ if userName != "" {
+ // #nosec G204 -- args are fixed constants; runCommand is internally built.
+ cmd = exec.Command("su", userName, "-c", runCommand)
+ } else {
+ // #nosec G204 -- args are fixed constants; runCommand is internally built.
+ cmd = exec.Command("sh", "-c", runCommand)
+ }
+ if workingDir != "" {
+ cmd.Dir = workingDir
+ }
+ return cmd
+}
+
+func userHome(userName string) (string, error) {
+ if userName != "" {
+ return command.GetHome(userName)
+ }
+ return util.UserHomeDir()
+}
+
+// prepareCodeServerLocation returns the install dir for code-server, creating
+// it if needed. Layout mirrors the upstream tarball after StripLevels(1):
+// /.code-server/bin/code-server.
+func prepareCodeServerLocation(userName string) (string, error) {
+ homeFolder, err := userHome(userName)
+ if err != nil {
+ return "", err
+ }
+ folder := filepath.Join(homeFolder, ".code-server")
+ // #nosec G301 -- match openvscode-server convention for parity.
+ if err := os.MkdirAll(folder, 0o755); err != nil {
+ return "", err
+ }
+ return folder, nil
+}
diff --git a/pkg/ide/ideparse/parse.go b/pkg/ide/ideparse/parse.go
index 6f64d0f0f..173909fc2 100644
--- a/pkg/ide/ideparse/parse.go
+++ b/pkg/ide/ideparse/parse.go
@@ -11,6 +11,7 @@ import (
"github.com/devsy-org/devsy/pkg/command"
"github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/ide"
+ "github.com/devsy-org/devsy/pkg/ide/codeserver"
"github.com/devsy-org/devsy/pkg/ide/fleet"
"github.com/devsy-org/devsy/pkg/ide/jetbrains"
"github.com/devsy-org/devsy/pkg/ide/jupyter"
@@ -31,8 +32,6 @@ type AllowedIDE struct {
Icon string `json:"icon,omitempty"`
// IconDark holds an image URL that will be displayed in dark mode
IconDark string `json:"iconDark,omitempty"`
- // Experimental indicates that this IDE is experimental
- Experimental bool `json:"experimental,omitempty"`
// Group this IDE belongs to, e.g. for navigation
Group config.IDEGroup `json:"group,omitempty"`
}
@@ -61,28 +60,32 @@ var AllowedIDEs = []AllowedIDE{
Group: config.IDEGroupPrimary,
},
{
- Name: config.IDECursor,
- DisplayName: "Cursor",
- Options: vscode.Options,
- Icon: config.WebsiteAssetsURL + "/cursor.svg",
- Experimental: true,
- Group: config.IDEGroupPrimary,
+ Name: config.IDECodeServer,
+ DisplayName: "code-server",
+ Options: codeserver.Options,
+ Icon: "https://raw.githubusercontent.com/coder/code-server/main/src/browser/media/favicon.svg",
+ Group: config.IDEGroupPrimary,
},
{
- Name: config.IDEZed,
- DisplayName: "Zed",
- Options: ide.Options{},
- Icon: config.WebsiteAssetsURL + "/zed.svg",
- Experimental: true,
- Group: config.IDEGroupPrimary,
+ Name: config.IDECursor,
+ DisplayName: "Cursor",
+ Options: vscode.Options,
+ Icon: config.WebsiteAssetsURL + "/cursor.svg",
+ Group: config.IDEGroupPrimary,
},
{
- Name: config.IDECodium,
- DisplayName: "VSCodium",
- Options: vscode.Options,
- Icon: config.WebsiteAssetsURL + "/codium.svg",
- Experimental: true,
- Group: config.IDEGroupPrimary,
+ Name: config.IDEZed,
+ DisplayName: "Zed",
+ Options: ide.Options{},
+ Icon: config.WebsiteAssetsURL + "/zed.svg",
+ Group: config.IDEGroupPrimary,
+ },
+ {
+ Name: config.IDECodium,
+ DisplayName: "VSCodium",
+ Options: vscode.Options,
+ Icon: config.WebsiteAssetsURL + "/codium.svg",
+ Group: config.IDEGroupPrimary,
},
{
Name: config.IDEIntellij,
@@ -113,12 +116,11 @@ var AllowedIDEs = []AllowedIDE{
Group: config.IDEGroupJetBrains,
},
{
- Name: config.IDEFleet,
- DisplayName: "Fleet",
- Options: fleet.Options,
- Icon: config.WebsiteAssetsURL + "/fleet.svg",
- Experimental: true,
- Group: config.IDEGroupJetBrains,
+ Name: config.IDEFleet,
+ DisplayName: "Fleet",
+ Options: fleet.Options,
+ Icon: config.WebsiteAssetsURL + "/fleet.svg",
+ Group: config.IDEGroupJetBrains,
},
{
Name: config.IDEGoland,
@@ -163,61 +165,54 @@ var AllowedIDEs = []AllowedIDE{
Group: config.IDEGroupJetBrains,
},
{
- Name: config.IDEJupyterNotebook,
- DisplayName: "Jupyter Notebook",
- Options: jupyter.Options,
- Icon: config.WebsiteAssetsURL + "/jupyter.svg",
- IconDark: config.WebsiteAssetsURL + "/jupyter_dark.svg",
- Experimental: true,
- Group: config.IDEGroupOther,
+ Name: config.IDEJupyterNotebook,
+ DisplayName: "Jupyter Notebook",
+ Options: jupyter.Options,
+ Icon: config.WebsiteAssetsURL + "/jupyter.svg",
+ IconDark: config.WebsiteAssetsURL + "/jupyter_dark.svg",
+ Group: config.IDEGroupOther,
},
{
- Name: config.IDEVSCodeInsiders,
- DisplayName: "VS Code Insiders",
- Options: vscode.Options,
- Icon: config.WebsiteAssetsURL + "/vscode_insiders.svg",
- Experimental: true,
- Group: config.IDEGroupOther,
+ Name: config.IDEVSCodeInsiders,
+ DisplayName: "VS Code Insiders",
+ Options: vscode.Options,
+ Icon: config.WebsiteAssetsURL + "/vscode_insiders.svg",
+ Group: config.IDEGroupOther,
},
{
- Name: config.IDEPositron,
- DisplayName: "Positron",
- Options: vscode.Options,
- Icon: config.WebsiteAssetsURL + "/positron.svg",
- Experimental: true,
- Group: config.IDEGroupOther,
+ Name: config.IDEPositron,
+ DisplayName: "Positron",
+ Options: vscode.Options,
+ Icon: config.WebsiteAssetsURL + "/positron.svg",
+ Group: config.IDEGroupOther,
},
{
- Name: config.IDERStudio,
- DisplayName: "RStudio Server",
- Options: rstudio.Options,
- Icon: config.WebsiteAssetsURL + "/rstudio.svg",
- Experimental: true,
- Group: config.IDEGroupOther,
+ Name: config.IDERStudio,
+ DisplayName: "RStudio Server",
+ Options: rstudio.Options,
+ Icon: config.WebsiteAssetsURL + "/rstudio.svg",
+ Group: config.IDEGroupOther,
},
{
- Name: config.IDEWindsurf,
- DisplayName: "Windsurf Editor",
- Options: vscode.Options,
- Icon: config.WebsiteAssetsURL + "/windsurf.svg",
- Experimental: true,
- Group: config.IDEGroupPrimary,
+ Name: config.IDEWindsurf,
+ DisplayName: "Windsurf Editor",
+ Options: vscode.Options,
+ Icon: config.WebsiteAssetsURL + "/windsurf.svg",
+ Group: config.IDEGroupPrimary,
},
{
- Name: config.IDEAntigravity,
- DisplayName: "Google Antigravity",
- Options: vscode.Options,
- Icon: config.WebsiteAssetsURL + "/antigravity.svg",
- Experimental: true,
- Group: config.IDEGroupPrimary,
+ Name: config.IDEAntigravity,
+ DisplayName: "Google Antigravity",
+ Options: vscode.Options,
+ Icon: config.WebsiteAssetsURL + "/antigravity.svg",
+ Group: config.IDEGroupPrimary,
},
{
- Name: config.IDEBob,
- DisplayName: "IBM Bob",
- Options: vscode.Options,
- Icon: "https://devsy.sh/assets/bob.svg",
- Experimental: true,
- Group: config.IDEGroupPrimary,
+ Name: config.IDEBob,
+ DisplayName: "IBM Bob",
+ Options: vscode.Options,
+ Icon: "https://devsy.sh/assets/bob.svg",
+ Group: config.IDEGroupPrimary,
},
}
diff --git a/pkg/ide/opener/browser_tunnel_state.go b/pkg/ide/opener/browser_tunnel_state.go
index 513d56852..7e4e13893 100644
--- a/pkg/ide/opener/browser_tunnel_state.go
+++ b/pkg/ide/opener/browser_tunnel_state.go
@@ -30,6 +30,10 @@ const TunnelLogFileName = "tunnel.log"
// duplicating the literal.
const LabelVSCodeBrowser = "vscode"
+// LabelCodeServer is the TunnelState.Label value used for the code-server
+// (coder.com) browser-IDE tunnel.
+const LabelCodeServer = "code-server"
+
// TunnelState describes a running browser tunnel for a workspace.
//
// CreateTime is the helper process's creation timestamp in milliseconds
diff --git a/pkg/ide/opener/opener.go b/pkg/ide/opener/opener.go
index 62c32292d..d9c2e25eb 100644
--- a/pkg/ide/opener/opener.go
+++ b/pkg/ide/opener/opener.go
@@ -14,6 +14,7 @@ import (
"github.com/devsy-org/devsy/pkg/config"
config2 "github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/gpg"
+ "github.com/devsy-org/devsy/pkg/ide/codeserver"
"github.com/devsy-org/devsy/pkg/ide/fleet"
"github.com/devsy-org/devsy/pkg/ide/jetbrains"
"github.com/devsy-org/devsy/pkg/ide/jupyter"
@@ -71,6 +72,8 @@ func browserIDEOpener(
switch ideName {
case string(config.IDEOpenVSCode):
return openVSCodeBrowser, true
+ case string(config.IDECodeServer):
+ return openCodeServerBrowser, true
case string(config.IDEJupyterNotebook):
return openJupyterBrowser, true
case string(config.IDERStudio):
@@ -274,84 +277,86 @@ func makeDaemonStartFunc(
}
}
-func openJupyterBrowser(
- ctx context.Context,
- ideOptions map[string]config.OptionValue,
- params IDEParams,
-) (string, error) {
+// browserIDESpec describes the per-IDE knobs needed to open a browser-based
+// IDE through the detached tunnel. The shared openBrowserIDE function below
+// consumes one of these to dispatch any of the four browser IDEs.
+type browserIDESpec struct {
+ BindAddrOption string
+ DefaultPort int
+ ForwardPorts bool
+ Label string
+ LogName string
+ // TargetURLFn builds the host-side URL once the bound port is known.
+ // folder is the container workspace folder (relevant for VS Code-style
+ // IDEs that take it as a query param; jupyter / rstudio ignore it).
+ TargetURLFn func(port int, folder string) string
+}
+
+func openBrowserIDE(ctx context.Context, params IDEParams, spec browserIDESpec) (string, error) {
if params.GPGAgentForwarding {
if err := gpg.ForwardAgent(params.Client); err != nil {
return "", err
}
}
- addr, jupyterPort, err := ParseAddressAndPort(
- jupyter.Options.GetValue(ideOptions, jupyter.BindAddressOption),
- jupyter.DefaultServerPort,
- )
+ addr, port, err := ParseAddressAndPort(spec.BindAddrOption, spec.DefaultPort)
if err != nil {
return "", err
}
- targetURL := fmt.Sprintf("http://localhost:%d/lab", jupyterPort)
+ folder := params.Result.SubstitutionContext.ContainerWorkspaceFolder
+ targetURL := spec.TargetURLFn(port, folder)
openBrowser := params.Launch == LaunchAuto
- pkglog.Infof("Starting jupyter notebook in browser mode at %s", targetURL)
- extraPorts := []string{fmt.Sprintf("%s:%d", addr, jupyter.DefaultServerPort)}
+ pkglog.Infof("Starting %s in browser mode at %s", spec.LogName, targetURL)
+ extraPorts := []string{fmt.Sprintf("%s:%d", addr, spec.DefaultPort)}
effectiveURL, err := startDetachedBrowserTunnel(ctx, params, tunnel.BrowserTunnelParams{
DevsyConfig: params.DevsyConfig,
Client: params.Client,
User: params.User,
TargetURL: targetURL,
+ ForwardPorts: spec.ForwardPorts,
ExtraPorts: extraPorts,
AuthSockID: params.SSHAuthSockID,
GitSSHSigningKey: params.GitSSHSigningKey,
- DaemonStartFunc: makeDaemonStartFunc(params, false, extraPorts),
- }, browserIDEInvocation{Label: "jupyter", OpenBrowser: openBrowser})
+ DaemonStartFunc: makeDaemonStartFunc(params, spec.ForwardPorts, extraPorts),
+ }, browserIDEInvocation{Label: spec.Label, OpenBrowser: openBrowser})
if err != nil {
return "", err
}
return effectiveURL, nil
}
-func openRStudioBrowser(
+func openJupyterBrowser(
ctx context.Context,
ideOptions map[string]config.OptionValue,
params IDEParams,
) (string, error) {
- if params.GPGAgentForwarding {
- if err := gpg.ForwardAgent(params.Client); err != nil {
- return "", err
- }
- }
-
- addr, rsPort, err := ParseAddressAndPort(
- rstudio.Options.GetValue(ideOptions, rstudio.BindAddressOption),
- rstudio.DefaultServerPort,
- )
- if err != nil {
- return "", err
- }
-
- targetURL := fmt.Sprintf("http://localhost:%d", rsPort)
- openBrowser := params.Launch == LaunchAuto
+ return openBrowserIDE(ctx, params, browserIDESpec{
+ BindAddrOption: jupyter.Options.GetValue(ideOptions, jupyter.BindAddressOption),
+ DefaultPort: jupyter.DefaultServerPort,
+ Label: "jupyter",
+ LogName: "jupyter notebook",
+ TargetURLFn: func(port int, _ string) string {
+ return fmt.Sprintf("http://localhost:%d/lab", port)
+ },
+ })
+}
- pkglog.Infof("Starting RStudio server in browser mode at %s", targetURL)
- extraPorts := []string{fmt.Sprintf("%s:%d", addr, rstudio.DefaultServerPort)}
- effectiveURL, err := startDetachedBrowserTunnel(ctx, params, tunnel.BrowserTunnelParams{
- DevsyConfig: params.DevsyConfig,
- Client: params.Client,
- User: params.User,
- TargetURL: targetURL,
- ExtraPorts: extraPorts,
- AuthSockID: params.SSHAuthSockID,
- GitSSHSigningKey: params.GitSSHSigningKey,
- DaemonStartFunc: makeDaemonStartFunc(params, false, extraPorts),
- }, browserIDEInvocation{Label: "rstudio", OpenBrowser: openBrowser})
- if err != nil {
- return "", err
- }
- return effectiveURL, nil
+func openRStudioBrowser(
+ ctx context.Context,
+ ideOptions map[string]config.OptionValue,
+ params IDEParams,
+) (string, error) {
+ return openBrowserIDE(ctx, params, browserIDESpec{
+ BindAddrOption: rstudio.Options.GetValue(ideOptions, rstudio.BindAddressOption),
+ DefaultPort: rstudio.DefaultServerPort,
+ Label: "rstudio",
+ LogName: "RStudio server",
+ TargetURLFn: func(port int, _ string) string {
+ return fmt.Sprintf("http://localhost:%d", port)
+ },
+ })
}
func openVSCodeBrowser(
@@ -359,45 +364,37 @@ func openVSCodeBrowser(
ideOptions map[string]config.OptionValue,
params IDEParams,
) (string, error) {
- if params.GPGAgentForwarding {
- if err := gpg.ForwardAgent(params.Client); err != nil {
- return "", err
- }
- }
-
- folder := params.Result.SubstitutionContext.ContainerWorkspaceFolder
- addr, vscodePort, err := ParseAddressAndPort(
- openvscode.Options.GetValue(ideOptions, openvscode.BindAddressOption),
- openvscode.DefaultVSCodePort,
- )
- if err != nil {
- return "", err
- }
-
- targetURL := fmt.Sprintf("http://localhost:%d/?folder=%s", vscodePort, folder)
- openBrowser := params.Launch == LaunchAuto
+ return openBrowserIDE(ctx, params, browserIDESpec{
+ BindAddrOption: openvscode.Options.GetValue(ideOptions, openvscode.BindAddressOption),
+ DefaultPort: openvscode.DefaultVSCodePort,
+ ForwardPorts: openvscode.Options.GetValue(
+ ideOptions, openvscode.ForwardPortsOption,
+ ) == config.BoolTrue,
+ Label: LabelVSCodeBrowser,
+ LogName: "vscode",
+ TargetURLFn: func(port int, folder string) string {
+ return fmt.Sprintf("http://localhost:%d/?folder=%s", port, folder)
+ },
+ })
+}
- pkglog.Infof("Starting vscode in browser mode at %s", targetURL)
- forwardPorts := openvscode.Options.GetValue(
- ideOptions,
- openvscode.ForwardPortsOption,
- ) == config.BoolTrue
- extraPorts := []string{fmt.Sprintf("%s:%d", addr, openvscode.DefaultVSCodePort)}
- effectiveURL, err := startDetachedBrowserTunnel(ctx, params, tunnel.BrowserTunnelParams{
- DevsyConfig: params.DevsyConfig,
- Client: params.Client,
- User: params.User,
- TargetURL: targetURL,
- ForwardPorts: forwardPorts,
- ExtraPorts: extraPorts,
- AuthSockID: params.SSHAuthSockID,
- GitSSHSigningKey: params.GitSSHSigningKey,
- DaemonStartFunc: makeDaemonStartFunc(params, forwardPorts, extraPorts),
- }, browserIDEInvocation{Label: LabelVSCodeBrowser, OpenBrowser: openBrowser})
- if err != nil {
- return "", err
- }
- return effectiveURL, nil
+func openCodeServerBrowser(
+ ctx context.Context,
+ ideOptions map[string]config.OptionValue,
+ params IDEParams,
+) (string, error) {
+ return openBrowserIDE(ctx, params, browserIDESpec{
+ BindAddrOption: codeserver.Options.GetValue(ideOptions, codeserver.BindAddressOption),
+ DefaultPort: codeserver.DefaultCodeServerPort,
+ ForwardPorts: codeserver.Options.GetValue(
+ ideOptions, codeserver.ForwardPortsOption,
+ ) == config.BoolTrue,
+ Label: LabelCodeServer,
+ LogName: "code-server",
+ TargetURLFn: func(port int, folder string) string {
+ return fmt.Sprintf("http://localhost:%d/?folder=%s", port, folder)
+ },
+ })
}
func startFleet(ctx context.Context, params IDEParams) (string, error) {
diff --git a/pkg/ide/types.go b/pkg/ide/types.go
index 41b72ff84..3e35404ff 100644
--- a/pkg/ide/types.go
+++ b/pkg/ide/types.go
@@ -51,6 +51,8 @@ func ReusesAuthSock(ide string) bool {
switch ide {
case string(config.IDEOpenVSCode):
return true
+ case string(config.IDECodeServer):
+ return true
case string(config.IDEJupyterNotebook):
return true
default:
diff --git a/pkg/tunnel/services.go b/pkg/tunnel/services.go
index cf0b1247a..f4a961930 100644
--- a/pkg/tunnel/services.go
+++ b/pkg/tunnel/services.go
@@ -19,6 +19,7 @@ import (
"github.com/devsy-org/devsy/pkg/config"
config2 "github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/gitsshsigning"
+ "github.com/devsy-org/devsy/pkg/ide/codeserver"
"github.com/devsy-org/devsy/pkg/ide/openvscode"
"github.com/devsy-org/devsy/pkg/log"
"github.com/devsy-org/devsy/pkg/netstat"
@@ -85,6 +86,7 @@ func createForwarder(
}
ports := append([]string{}, forwardedPorts...)
ports = append(ports, fmt.Sprintf("%d", openvscode.DefaultVSCodePort))
+ ports = append(ports, fmt.Sprintf("%d", codeserver.DefaultCodeServerPort))
return newForwarder(opts.ContainerClient, ports, resolver)
}