diff --git a/cmd/ide/ide.go b/cmd/ide/ide.go index 2ae6ffd4c..d2621f5e1 100644 --- a/cmd/ide/ide.go +++ b/cmd/ide/ide.go @@ -13,6 +13,7 @@ func NewIDECmd(flags *flags.GlobalFlags) *cobra.Command { } ideCmd.AddCommand(NewUseCmd(flags)) + ideCmd.AddCommand(NewSetCmd(flags)) ideCmd.AddCommand(NewSetOptionsCmd(flags)) ideCmd.AddCommand(NewOptionsCmd(flags)) ideCmd.AddCommand(NewListCmd(flags)) diff --git a/cmd/ide/set.go b/cmd/ide/set.go new file mode 100644 index 000000000..2d5db7997 --- /dev/null +++ b/cmd/ide/set.go @@ -0,0 +1,78 @@ +package ide + +import ( + "context" + "fmt" + "strings" + + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/ide/ideparse" + "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/provider" + "github.com/spf13/cobra" +) + +// SetCmd holds the set cmd flags. +type SetCmd struct { + *flags.GlobalFlags + + Options []string +} + +// NewSetCmd creates a command that sets the IDE for an existing workspace +// without starting the workspace. +func NewSetCmd(flags *flags.GlobalFlags) *cobra.Command { + cmd := &SetCmd{ + GlobalFlags: flags, + } + setCmd := &cobra.Command{ + Use: "set [workspace] [ide]", + Short: "Set the IDE for an existing workspace without starting it", + Long: `Set the IDE for an existing workspace without starting it. + +The change is persisted to the workspace config and will be used on the next +'devsy up'. Available IDEs can be listed with 'devsy ide list'.`, + RunE: func(cobraCmd *cobra.Command, args []string) error { + if len(args) != 2 { + return fmt.Errorf("usage: devsy ide set ") + } + return cmd.Run(cobraCmd.Context(), args[0], args[1]) + }, + } + + setCmd.Flags(). + StringArrayVarP(&cmd.Options, "option", "o", []string{}, "IDE option in the form KEY=VALUE") + return setCmd +} + +// Run runs the command logic. +func (cmd *SetCmd) Run(_ context.Context, workspaceID, ideName string) error { + devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider) + if err != nil { + return err + } + + contextName := devsyConfig.DefaultContext + if !provider.WorkspaceExists(contextName, workspaceID) { + return fmt.Errorf("workspace %q not found in context %q", workspaceID, contextName) + } + + workspace, err := provider.LoadWorkspaceConfig(contextName, workspaceID) + if err != nil { + return fmt.Errorf("load workspace config: %w", err) + } + + ideName = strings.ToLower(ideName) + if _, err := ideparse.GetIDEOptions(ideName); err != nil { + return err + } + + workspace, err = ideparse.RefreshIDEOptions(devsyConfig, workspace, ideName, cmd.Options) + if err != nil { + return fmt.Errorf("refresh ide options: %w", err) + } + + log.Infof("set IDE for workspace %q to %q", workspace.ID, workspace.IDE.Name) + return nil +} diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index c8905b9ab..72a797207 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -79,6 +79,14 @@ export function registerIpcHandlers(deps: IpcDependencies): { tunnelProcesses: M }, ) + ipcMain.handle( + "workspace_set_ide", + async (_event, args: { workspaceId: string; ide: string }) => { + trackEvent("workspace_set_ide", { ide: args.ide }) + await cli.runRaw(["ide", "set", args.workspaceId, args.ide]) + }, + ) + // ── Providers ── ipcMain.handle("provider_list", async () => { const raw = await cli.run>([ diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte index a1e338e18..bafb54ec6 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte @@ -292,7 +292,7 @@ async function handleLaunch() { source: source.trim(), workspaceId, provider: selectedProvider || undefined, - ide: selectedIde && selectedIde !== "none" ? selectedIde : undefined, + ide: selectedIde, ideLaunch: "auto", workspaceFolder: workspaceFolder.trim() || undefined, debug: true, diff --git a/desktop/src/renderer/src/lib/ipc/commands.ts b/desktop/src/renderer/src/lib/ipc/commands.ts index fe77565a0..5c13a7b8f 100644 --- a/desktop/src/renderer/src/lib/ipc/commands.ts +++ b/desktop/src/renderer/src/lib/ipc/commands.ts @@ -67,6 +67,13 @@ export async function workspaceRename( return invoke("workspace_rename", { workspaceId, newWorkspaceId }) } +export async function workspaceSetIde( + workspaceId: string, + ide: string, +): Promise { + return invoke("workspace_set_ide", { workspaceId, ide }) +} + // Provider commands export async function providerList(): Promise { return invoke("provider_list") diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index 332f8f3fd..a037de904 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -41,6 +41,7 @@ import { workspaceReset, workspaceDelete, workspaceRename, + workspaceSetIde, workspaceLogsList, workspaceLogRead, workspaceLogDelete, @@ -124,6 +125,7 @@ let connecting = $state(false) let ideComboOpen = $state(false) let ideSearch = $state("") let selectedIde = $state(null) +let ideSetSeq = 0 let renaming = $state(false) let renameValue = $state("") let renameSaving = $state(false) @@ -305,9 +307,16 @@ function startStreamingOp(label: string) { } async function handleStart() { + const ide = currentIde + const folder = customFolder || undefined startStreamingOp("Start") try { - commandId = await workspaceUp({ source: id, debug: isDebug() }) + commandId = await workspaceUp({ + source: id, + ide, + debug: isDebug(), + workspaceFolder: folder, + }) } catch (err) { operationRunning = false toasts.error(`Failed to start: ${extractErrorMessage(err)}`) @@ -315,7 +324,7 @@ async function handleStart() { } async function handleOpenIde() { - const ide = currentIde !== "none" ? currentIde : undefined + const ide = currentIde const folder = customFolder || undefined startStreamingOp("Open IDE") try { @@ -450,7 +459,7 @@ async function handleRename() { {/if} - @@ -545,10 +554,19 @@ async function handleRename() { { + onSelect={async () => { + const seq = ++ideSetSeq + const prev = selectedIde selectedIde = ide.value ideComboOpen = false ideSearch = "" + try { + await workspaceSetIde(id, ide.value) + } catch (err) { + if (seq !== ideSetSeq) return + selectedIde = prev + toasts.error(`Failed to set IDE: ${extractErrorMessage(err)}`) + } }} > diff --git a/pkg/ide/opener/opener.go b/pkg/ide/opener/opener.go index 62c32292d..b5d393562 100644 --- a/pkg/ide/opener/opener.go +++ b/pkg/ide/opener/opener.go @@ -57,6 +57,10 @@ func Open( ideOptions map[string]config.OptionValue, params IDEParams, ) (string, error) { + if ideName == string(config.IDENone) { + return "", nil + } + if fn, ok := browserIDEOpener(ideName); ok { return fn(ctx, ideOptions, params) }