@@ -2,9 +2,11 @@ package create
22
33import (
44 "fmt"
5+ "os"
56 "strings"
67 "time"
78
9+ "github.com/brevdev/brev-cli/pkg/cmd/gpusearch"
810 "github.com/brevdev/brev-cli/pkg/cmd/util"
911 "github.com/brevdev/brev-cli/pkg/config"
1012 "github.com/brevdev/brev-cli/pkg/entity"
@@ -30,12 +32,18 @@ type CreateStore interface {
3032 GetCurrentUser () (* entity.User , error )
3133 GetWorkspace (workspaceID string ) (* entity.Workspace , error )
3234 CreateWorkspace (organizationID string , options * store.CreateWorkspacesOptions ) (* entity.Workspace , error )
35+ GetAllInstanceTypesWithWorkspaceGroups (orgID string ) (* gpusearch.AllInstanceTypesResponse , error )
3336}
3437
3538func NewCmdCreate (t * terminal.Terminal , createStore CreateStore ) * cobra.Command {
3639 var detached bool
3740 var gpu string
3841 var cpu string
42+ var mode string
43+ var jupyter bool
44+ var startupScript string
45+ var containerImage string
46+ var composeFile string
3947
4048 cmd := & cobra.Command {
4149 Annotations : map [string ]string {"workspace" : "" },
@@ -50,11 +58,20 @@ func NewCmdCreate(t *terminal.Terminal, createStore CreateStore) *cobra.Command
5058 name = args [0 ]
5159 }
5260
61+ // Default jupyter to true unless explicitly set to false
62+ jupyterSet := cmd .Flags ().Changed ("jupyter" )
63+
5364 err := runCreateWorkspace (t , CreateOptions {
5465 Name : name ,
5566 WorkspaceClass : cpu ,
5667 Detached : detached ,
5768 InstanceType : gpu ,
69+ Mode : mode ,
70+ Jupyter : jupyter ,
71+ JupyterSet : jupyterSet ,
72+ StartupScript : startupScript ,
73+ ContainerImage : containerImage ,
74+ ComposeFile : composeFile ,
5875 }, createStore )
5976 if err != nil {
6077 if strings .Contains (err .Error (), "duplicate instance with name" ) {
@@ -70,6 +87,11 @@ func NewCmdCreate(t *terminal.Terminal, createStore CreateStore) *cobra.Command
7087 cmd .Flags ().BoolVarP (& detached , "detached" , "d" , false , "run the command in the background instead of blocking the shell" )
7188 cmd .Flags ().StringVarP (& cpu , "cpu" , "c" , "" , "CPU instance type. Defaults to 2x8 [2x8, 4x16, 8x32, 16x32]. See docs.brev.dev/cpu for details" )
7289 cmd .Flags ().StringVarP (& gpu , "gpu" , "g" , "n1-highmem-4:nvidia-tesla-t4:1" , "GPU instance type. See https://brev.dev/docs/reference/gpu for details" )
90+ cmd .Flags ().StringVarP (& mode , "mode" , "m" , "vm" , "Build mode: vm (default), k8s, container, compose" )
91+ cmd .Flags ().BoolVar (& jupyter , "jupyter" , true , "Install Jupyter (default true for vm/k8s modes)" )
92+ cmd .Flags ().StringVar (& startupScript , "startup-script" , "" , "Lifecycle script to run on boot" )
93+ cmd .Flags ().StringVar (& containerImage , "container-image" , "" , "Container image URL (required for container mode)" )
94+ cmd .Flags ().StringVar (& composeFile , "compose-file" , "" , "Docker compose file path or URL (required for compose mode)" )
7395 return cmd
7496}
7597
@@ -78,6 +100,12 @@ type CreateOptions struct {
78100 WorkspaceClass string
79101 Detached bool
80102 InstanceType string
103+ Mode string
104+ Jupyter bool
105+ JupyterSet bool // whether --jupyter was explicitly set
106+ StartupScript string
107+ ContainerImage string
108+ ComposeFile string
81109}
82110
83111func runCreateWorkspace (t * terminal.Terminal , options CreateOptions , createStore CreateStore ) error {
@@ -98,6 +126,22 @@ func createEmptyWorkspace(user *entity.User, t *terminal.Terminal, options Creat
98126 return breverrors .NewValidationError ("A name field is required to create a workspace!" )
99127 }
100128
129+ // validate mode
130+ switch options .Mode {
131+ case "vm" , "k8s" , "container" , "compose" :
132+ // valid
133+ default :
134+ return breverrors .NewValidationError (fmt .Sprintf ("invalid mode %q: must be one of vm, k8s, container, compose" , options .Mode ))
135+ }
136+
137+ // validate mode-specific required flags
138+ if options .Mode == "container" && options .ContainerImage == "" {
139+ return breverrors .NewValidationError ("--container-image is required for container mode" )
140+ }
141+ if options .Mode == "compose" && options .ComposeFile == "" {
142+ return breverrors .NewValidationError ("--compose-file is required for compose mode" )
143+ }
144+
101145 // ensure org
102146 var orgID string
103147 activeorg , err := createStore .GetActiveOrganizationOrDefault ()
@@ -122,14 +166,28 @@ func createEmptyWorkspace(user *entity.User, t *terminal.Terminal, options Creat
122166 cwOptions .WithInstanceType (options .InstanceType )
123167 }
124168
125- t .Vprintf ("Creating instane %s in org %s\n " , t .Green (cwOptions .Name ), t .Green (orgID ))
169+ // Resolve workspace group dynamically from instance type
170+ resolveWorkspaceGroupID (t , createStore , cwOptions , orgID )
171+
172+ // Apply build mode
173+ err = applyBuildMode (cwOptions , options )
174+ if err != nil {
175+ return breverrors .WrapAndTrace (err )
176+ }
177+
178+ t .Vprintf ("Creating instance %s in org %s\n " , t .Green (cwOptions .Name ), t .Green (orgID ))
126179 t .Vprintf ("\t name %s\n " , t .Green (cwOptions .Name ))
127180 if options .InstanceType != "" {
128181 t .Vprintf ("\t GPU instance %s\n " , t .Green (options .InstanceType ))
129182 } else {
130183 t .Vprintf ("\t CPU instance %s\n " , t .Green (cwOptions .WorkspaceClassID ))
131184 }
132- t .Vprintf ("\t Cloud %s\n \n " , t .Green (cwOptions .WorkspaceGroupID ))
185+ t .Vprintf ("\t mode %s\n " , t .Green (options .Mode ))
186+ if cwOptions .WorkspaceGroupID != "" {
187+ t .Vprintf ("\t Cloud %s\n \n " , t .Green (cwOptions .WorkspaceGroupID ))
188+ } else {
189+ t .Vprintf ("\t Cloud %s\n \n " , t .Green ("(auto)" ))
190+ }
133191
134192 s := t .NewSpinner ()
135193 s .Suffix = " Creating your instance. Hang tight 🤙"
@@ -156,6 +214,90 @@ func createEmptyWorkspace(user *entity.User, t *terminal.Terminal, options Creat
156214 }
157215}
158216
217+ // resolveWorkspaceGroupID fetches workspace groups for the instance type and sets the workspace group ID
218+ func resolveWorkspaceGroupID (t * terminal.Terminal , createStore CreateStore , cwOptions * store.CreateWorkspacesOptions , orgID string ) {
219+ allInstanceTypes , err := createStore .GetAllInstanceTypesWithWorkspaceGroups (orgID )
220+ if err != nil {
221+ t .Vprintf ("Warning: could not fetch workspace groups: %s\n " , err .Error ())
222+ t .Vprintf ("Falling back to server default\n " )
223+ return
224+ }
225+ if allInstanceTypes != nil && cwOptions .InstanceType != "" {
226+ if wgID := allInstanceTypes .GetWorkspaceGroupID (cwOptions .InstanceType ); wgID != "" {
227+ cwOptions .WorkspaceGroupID = wgID
228+ }
229+ }
230+ }
231+
232+ // applyBuildMode configures the workspace options based on the selected build mode.
233+ // The server determines the mode from which build field is present (vmBuild, customContainer,
234+ // dockerCompose) — we do NOT send vmOnlyMode or onContainer, matching the UI behavior.
235+ func applyBuildMode (cwOptions * store.CreateWorkspacesOptions , options CreateOptions ) error {
236+ switch options .Mode {
237+ case "vm" :
238+ jupyter := true
239+ if options .JupyterSet {
240+ jupyter = options .Jupyter
241+ }
242+ cwOptions .VMBuild = & store.VMBuild {
243+ ForceJupyterInstall : jupyter ,
244+ }
245+ if options .StartupScript != "" {
246+ cwOptions .VMBuild .LifeCycleScriptAttr = & store.LifeCycleScriptAttr {
247+ Script : options .StartupScript ,
248+ }
249+ }
250+
251+ case "k8s" :
252+ jupyter := false // UI defaults to false for k8s
253+ if options .JupyterSet {
254+ jupyter = options .Jupyter
255+ }
256+ cwOptions .VMBuild = & store.VMBuild {
257+ ForceJupyterInstall : jupyter ,
258+ K8s : & store.K8sConfig {
259+ IsDisabled : false ,
260+ IsDashboardEnabled : true ,
261+ },
262+ }
263+ if options .StartupScript != "" {
264+ cwOptions .VMBuild .LifeCycleScriptAttr = & store.LifeCycleScriptAttr {
265+ Script : options .StartupScript ,
266+ }
267+ }
268+
269+ case "container" :
270+ cwOptions .VMBuild = nil
271+ cwOptions .CustomContainer = & store.CustomContainer {
272+ ContainerURL : options .ContainerImage ,
273+ }
274+
275+ case "compose" :
276+ cwOptions .VMBuild = nil
277+ composeConfig := & store.DockerCompose {}
278+
279+ // Check if it's a file path or URL
280+ if strings .HasPrefix (options .ComposeFile , "http://" ) || strings .HasPrefix (options .ComposeFile , "https://" ) {
281+ composeConfig .FileURL = options .ComposeFile
282+ } else {
283+ content , err := os .ReadFile (options .ComposeFile )
284+ if err != nil {
285+ return breverrors .WrapAndTrace (fmt .Errorf ("could not read compose file %s: %w" , options .ComposeFile , err ))
286+ }
287+ composeConfig .YamlString = string (content )
288+ }
289+
290+ jupyter := false
291+ if options .JupyterSet {
292+ jupyter = options .Jupyter
293+ }
294+ composeConfig .JupyterInstall = jupyter
295+ cwOptions .DockerCompose = composeConfig
296+ }
297+
298+ return nil
299+ }
300+
159301func resolveWorkspaceUserOptions (options * store.CreateWorkspacesOptions , user * entity.User ) * store.CreateWorkspacesOptions {
160302 if options .WorkspaceTemplateID == "" {
161303 if featureflag .IsAdmin (user .GlobalUserType ) {
0 commit comments