From 6420e8a3c514ac0781cb130eef42ab791c0fa877 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:10:13 +0000 Subject: [PATCH 1/3] feat: add --org flag to shell and open commands Add --org flag support to brev shell and brev open commands, following the existing pattern from brev start and brev ls. When provided, the --org flag overrides the active org for workspace resolution, preventing 'instance not found' errors when users haven't set their org context. Changes: - Add ResolveOrgFromFlag utility in pkg/cmd/util for shared org resolution - Add GetUserWorkspaceByNameOrIDErrInOrg for org-specific workspace lookup - Add ResolveWorkspaceOrNodeInOrg for org-specific workspace/node resolution - Add --org/-o flag with tab completion to shell command - Add --org/-o flag with tab completion to open command Co-Authored-By: Alec Fong --- pkg/cmd/open/open.go | 19 ++++++++++++---- pkg/cmd/shell/shell.go | 19 ++++++++++++---- pkg/cmd/util/externalnode.go | 14 ++++++++++++ pkg/cmd/util/util.go | 42 +++++++++++++++++++++++++++++++++--- 4 files changed, 83 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/open/open.go b/pkg/cmd/open/open.go index 8ff3857bc..24367630e 100644 --- a/pkg/cmd/open/open.go +++ b/pkg/cmd/open/open.go @@ -118,6 +118,7 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto var host bool var setDefault string var editor string + var org string cmd := &cobra.Command{ Annotations: map[string]string{"access": ""}, @@ -162,7 +163,7 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto if len(instanceNames) > 1 { fmt.Fprintf(os.Stderr, "Opening %s...\n", instanceName) } - err = runOpenCommand(t, store, instanceName, setupDoneString, directory, host, editorType) + err = runOpenCommand(t, store, instanceName, setupDoneString, directory, host, editorType, org) if err != nil { if len(instanceNames) > 1 { fmt.Fprintf(os.Stderr, "Error opening %s: %v\n", instanceName, err) @@ -187,6 +188,12 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto cmd.Flags().StringVarP(&directory, "dir", "d", "", "directory to open") cmd.Flags().StringVar(&setDefault, "set-default", "", "set default editor (code, cursor, windsurf, terminal, or tmux)") cmd.Flags().StringVarP(&editor, "editor", "e", "", "editor to use (code, cursor, windsurf, terminal, or tmux)") + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") + errRegComp := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(noLoginStartStore, t)) + if errRegComp != nil { + breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(errRegComp)) + fmt.Print(breverrors.WrapAndTrace(errRegComp)) + } return cmd } @@ -279,11 +286,15 @@ func handleSetDefault(t *terminal.Terminal, editorType string) error { } // Fetch workspace info, then open code editor -func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, setupDoneString string, directory string, host bool, editorType string) error { //nolint:funlen,gocyclo // define brev command +func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, setupDoneString string, directory string, host bool, editorType string, orgFlag string) error { //nolint:funlen,gocyclo // define brev command // todo check if workspace is stopped and start if it if it is stopped fmt.Println("finding your instance...") res := refresh.RunRefreshAsync(tstore) - target, err := util.ResolveWorkspaceOrNode(tstore, wsIDOrName) + resolvedOrg, err := util.ResolveOrgFromFlag(tstore, orgFlag) + if err != nil { + return breverrors.WrapAndTrace(err) + } + target, err := util.ResolveWorkspaceOrNodeInOrg(tstore, wsIDOrName, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -306,7 +317,7 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s return breverrors.WrapAndTrace(err) } - workspace, err = util.GetUserWorkspaceByNameOrIDErr(tstore, wsIDOrName) + workspace, err = util.GetUserWorkspaceByNameOrIDErrInOrg(tstore, wsIDOrName, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } diff --git a/pkg/cmd/shell/shell.go b/pkg/cmd/shell/shell.go index a13a3b17d..c2bdba0f8 100644 --- a/pkg/cmd/shell/shell.go +++ b/pkg/cmd/shell/shell.go @@ -50,6 +50,7 @@ type ShellStore interface { func NewCmdShell(t *terminal.Terminal, store ShellStore, noLoginStartStore ShellStore) *cobra.Command { var host bool + var org string cmd := &cobra.Command{ Annotations: map[string]string{"access": ""}, Use: "shell ", @@ -62,7 +63,7 @@ func NewCmdShell(t *terminal.Terminal, store ShellStore, noLoginStartStore Shell ValidArgsFunction: completions.GetAllWorkspaceNameCompletionHandler(noLoginStartStore, t), RunE: func(cmd *cobra.Command, args []string) error { instanceName := args[0] - err := runShellCommand(t, store, instanceName, host) + err := runShellCommand(t, store, instanceName, host, org) if err != nil { return breverrors.WrapAndTrace(err) } @@ -70,15 +71,25 @@ func NewCmdShell(t *terminal.Terminal, store ShellStore, noLoginStartStore Shell }, } cmd.Flags().BoolVarP(&host, "host", "", false, "ssh into the host machine instead of the container") + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") + err := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(noLoginStartStore, t)) + if err != nil { + breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(err)) + fmt.Print(breverrors.WrapAndTrace(err)) + } return cmd } const pollTimeout = 10 * time.Minute -func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID string, host bool) error { +func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID string, host bool, orgFlag string) error { s := t.NewSpinner() - target, err := util.ResolveWorkspaceOrNode(sstore, workspaceNameOrID) + resolvedOrg, err := util.ResolveOrgFromFlag(sstore, orgFlag) + if err != nil { + return breverrors.WrapAndTrace(err) + } + target, err := util.ResolveWorkspaceOrNodeInOrg(sstore, workspaceNameOrID, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -99,7 +110,7 @@ func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID } refreshRes := refresh.RunRefreshAsync(sstore) - workspace, err = util.GetUserWorkspaceByNameOrIDErr(sstore, workspaceNameOrID) + workspace, err = util.GetUserWorkspaceByNameOrIDErrInOrg(sstore, workspaceNameOrID, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } diff --git a/pkg/cmd/util/externalnode.go b/pkg/cmd/util/externalnode.go index bbf29f47c..a638f9120 100644 --- a/pkg/cmd/util/externalnode.go +++ b/pkg/cmd/util/externalnode.go @@ -48,6 +48,20 @@ func ResolveWorkspaceOrNode(store WorkspaceOrNodeResolver, nameOrID string, return &WorkspaceOrNode{Node: node}, nil } +// ResolveWorkspaceOrNodeInOrg is like ResolveWorkspaceOrNode but looks up workspaces in a specific org. +func ResolveWorkspaceOrNodeInOrg(store WorkspaceOrNodeResolver, nameOrID string, orgID string, +) (*WorkspaceOrNode, error) { + workspace, wsErr := GetUserWorkspaceByNameOrIDErrInOrg(store, nameOrID, orgID) + if wsErr == nil { + return &WorkspaceOrNode{Workspace: workspace}, nil + } + node, nodeErr := FindExternalNode(store, nameOrID) + if nodeErr != nil || node == nil { + return nil, wsErr // return original workspace error + } + return &WorkspaceOrNode{Node: node}, nil +} + // ExternalNodeSSHInfo holds resolved SSH connection details for an external node. type ExternalNodeSSHInfo struct { Node *nodev1.ExternalNode diff --git a/pkg/cmd/util/util.go b/pkg/cmd/util/util.go index 8d8413498..cc96130ec 100644 --- a/pkg/cmd/util/util.go +++ b/pkg/cmd/util/util.go @@ -8,6 +8,37 @@ import ( "github.com/brevdev/brev-cli/pkg/store" ) +// OrgResolver is the interface needed to resolve an org from a --org flag. +type OrgResolver interface { + GetActiveOrganizationOrDefault() (*entity.Organization, error) + GetOrganizations(options *store.GetOrganizationsOptions) ([]entity.Organization, error) +} + +// ResolveOrgFromFlag resolves an organization from the --org flag value. +// If orgFlag is empty, returns the active org. If orgFlag is set, looks up the org by name. +func ResolveOrgFromFlag(s OrgResolver, orgFlag string) (*entity.Organization, error) { + if orgFlag == "" { + org, err := s.GetActiveOrganizationOrDefault() + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + if org == nil { + return nil, breverrors.NewValidationError("no orgs exist") + } + return org, nil + } + orgs, err := s.GetOrganizations(&store.GetOrganizationsOptions{Name: orgFlag}) + if err != nil { + return nil, breverrors.WrapAndTrace(err) + } + if len(orgs) == 0 { + return nil, breverrors.NewValidationError(fmt.Sprintf("no org found with name %s", orgFlag)) + } else if len(orgs) > 1 { + return nil, breverrors.NewValidationError(fmt.Sprintf("more than one org found with name %s", orgFlag)) + } + return &orgs[0], nil +} + type GetWorkspaceByNameOrIDErrStore interface { GetActiveOrganizationOrDefault() (*entity.Organization, error) GetWorkspaceByNameOrID(orgID string, nameOrID string) ([]entity.Workspace, error) @@ -15,15 +46,20 @@ type GetWorkspaceByNameOrIDErrStore interface { } func GetUserWorkspaceByNameOrIDErr(storeQ GetWorkspaceByNameOrIDErrStore, workspaceNameOrID string) (*entity.Workspace, error) { - user, err := storeQ.GetCurrentUser() + org, err := storeQ.GetActiveOrganizationOrDefault() if err != nil { return nil, breverrors.WrapAndTrace(err) } - org, err := storeQ.GetActiveOrganizationOrDefault() + return GetUserWorkspaceByNameOrIDErrInOrg(storeQ, workspaceNameOrID, org.ID) +} + +// GetUserWorkspaceByNameOrIDErrInOrg looks up a workspace by name or ID in a specific org. +func GetUserWorkspaceByNameOrIDErrInOrg(storeQ GetWorkspaceByNameOrIDErrStore, workspaceNameOrID string, orgID string) (*entity.Workspace, error) { + user, err := storeQ.GetCurrentUser() if err != nil { return nil, breverrors.WrapAndTrace(err) } - workspaces, err := storeQ.GetWorkspaceByNameOrID(org.ID, workspaceNameOrID) + workspaces, err := storeQ.GetWorkspaceByNameOrID(orgID, workspaceNameOrID) if err != nil { return nil, breverrors.WrapAndTrace(err) } From f7172bf40959f0ce99e936981f9eac6a1a3ccda2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:30:00 +0000 Subject: [PATCH 2/3] feat: add --org flag to all relevant CLI commands Add --org / -o flag to: stop, copy, exec, portforward, gpucreate, reset, recreate, ollama, workspacegroups commands. Also adds GetAnyWorkspaceByIDOrNameInOrgErr utility for org-specific workspace lookups and updates notebook.go caller for new RunPortforward signature. Co-Authored-By: Alec Fong --- pkg/cmd/copy/copy.go | 35 +++++++++------ pkg/cmd/exec/exec.go | 18 ++++++-- pkg/cmd/gpucreate/gpucreate.go | 16 +++---- pkg/cmd/notebook/notebook.go | 2 +- pkg/cmd/ollama/ollama.go | 9 ++-- pkg/cmd/portforward/portforward.go | 17 ++++++-- pkg/cmd/recreate/recreate.go | 49 +++++++++------------ pkg/cmd/reset/reset.go | 51 ++++++++++------------ pkg/cmd/stop/stop.go | 29 +++++++----- pkg/cmd/util/util.go | 7 ++- pkg/cmd/workspacegroups/workspacegroups.go | 11 +++-- 11 files changed, 140 insertions(+), 104 deletions(-) diff --git a/pkg/cmd/copy/copy.go b/pkg/cmd/copy/copy.go index 5aadf4efd..cabcb1c6e 100644 --- a/pkg/cmd/copy/copy.go +++ b/pkg/cmd/copy/copy.go @@ -41,6 +41,7 @@ type CopyStore interface { func NewCmdCopy(t *terminal.Terminal, store CopyStore, noLoginStartStore CopyStore) *cobra.Command { var host bool + var org string cmd := &cobra.Command{ Annotations: map[string]string{"access": ""}, Use: "copy", @@ -52,7 +53,7 @@ func NewCmdCopy(t *terminal.Terminal, store CopyStore, noLoginStartStore CopySto Args: cmderrors.TransformToValidationError(cobra.ExactArgs(2)), ValidArgsFunction: completions.GetAllWorkspaceNameCompletionHandler(noLoginStartStore, t), RunE: func(cmd *cobra.Command, args []string) error { - err := runCopyCommand(t, store, args[0], args[1], host) + err := runCopyCommand(t, store, args[0], args[1], host, org) if err != nil { return breverrors.WrapAndTrace(err) } @@ -60,11 +61,17 @@ func NewCmdCopy(t *terminal.Terminal, store CopyStore, noLoginStartStore CopySto }, } cmd.Flags().BoolVarP(&host, "host", "", false, "copy to/from the host machine instead of the container") + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") + errRegComp := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(noLoginStartStore, t)) + if errRegComp != nil { + breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(errRegComp)) + fmt.Print(breverrors.WrapAndTrace(errRegComp)) + } return cmd } -func runCopyCommand(t *terminal.Terminal, cstore CopyStore, source, dest string, host bool) error { +func runCopyCommand(t *terminal.Terminal, cstore CopyStore, source, dest string, host bool, orgFlag string) error { workspaceNameOrID, remotePath, localPath, isUpload, err := parseCopyArguments(source, dest) if err != nil { return breverrors.WrapAndTrace(err) @@ -77,7 +84,11 @@ func runCopyCommand(t *terminal.Terminal, cstore CopyStore, source, dest string, } } - target, err := util.ResolveWorkspaceOrNode(cstore, workspaceNameOrID) + resolvedOrg, err := util.ResolveOrgFromFlag(cstore, orgFlag) + if err != nil { + return breverrors.WrapAndTrace(err) + } + target, err := util.ResolveWorkspaceOrNodeInOrg(cstore, workspaceNameOrID, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -85,7 +96,7 @@ func runCopyCommand(t *terminal.Terminal, cstore CopyStore, source, dest string, return copyExternalNode(t, cstore, target.Node, localPath, remotePath, isUpload) } - workspace, err := prepareWorkspace(t, cstore, target.Workspace) + workspace, err := prepareWorkspace(t, cstore, target.Workspace, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -126,11 +137,11 @@ func parseCopyArguments(source, dest string) (workspaceNameOrID, remotePath, loc return destWorkspace, destPath, source, true, nil } -func prepareWorkspace(t *terminal.Terminal, cstore CopyStore, workspace *entity.Workspace) (*entity.Workspace, error) { +func prepareWorkspace(t *terminal.Terminal, cstore CopyStore, workspace *entity.Workspace, orgID string) (*entity.Workspace, error) { s := t.NewSpinner() if workspace.Status == "STOPPED" { - err := startWorkspaceIfStopped(t, s, cstore, workspace.Name, workspace) + err := startWorkspaceIfStopped(t, s, cstore, workspace.Name, workspace, orgID) if err != nil { return nil, breverrors.WrapAndTrace(err) } @@ -141,7 +152,7 @@ func prepareWorkspace(t *terminal.Terminal, cstore CopyStore, workspace *entity. return nil, breverrors.WrapAndTrace(err) } - workspace, err = util.GetUserWorkspaceByNameOrIDErr(cstore, workspace.Name) + workspace, err = util.GetUserWorkspaceByNameOrIDErrInOrg(cstore, workspace.Name, orgID) if err != nil { return nil, breverrors.WrapAndTrace(err) } @@ -268,12 +279,8 @@ func waitForSSHToBeAvailable(sshAlias string, s *spinner.Spinner) error { } } -func startWorkspaceIfStopped(t *terminal.Terminal, s *spinner.Spinner, tstore CopyStore, wsIDOrName string, workspace *entity.Workspace) error { - activeOrg, err := tstore.GetActiveOrganizationOrDefault() - if err != nil { - return breverrors.WrapAndTrace(err) - } - workspaces, err := tstore.GetWorkspaceByNameOrID(activeOrg.ID, wsIDOrName) +func startWorkspaceIfStopped(t *terminal.Terminal, s *spinner.Spinner, tstore CopyStore, wsIDOrName string, workspace *entity.Workspace, orgID string) error { + workspaces, err := tstore.GetWorkspaceByNameOrID(orgID, wsIDOrName) if err != nil { return breverrors.WrapAndTrace(err) } @@ -286,7 +293,7 @@ func startWorkspaceIfStopped(t *terminal.Terminal, s *spinner.Spinner, tstore Co if err != nil { return breverrors.WrapAndTrace(err) } - workspace, err = util.GetUserWorkspaceByNameOrIDErr(tstore, wsIDOrName) + workspace, err = util.GetUserWorkspaceByNameOrIDErrInOrg(tstore, wsIDOrName, orgID) if err != nil { return breverrors.WrapAndTrace(err) } diff --git a/pkg/cmd/exec/exec.go b/pkg/cmd/exec/exec.go index 3e6d2ad82..693d764ee 100644 --- a/pkg/cmd/exec/exec.go +++ b/pkg/cmd/exec/exec.go @@ -57,6 +57,7 @@ type ExecStore interface { func NewCmdExec(t *terminal.Terminal, store ExecStore, noLoginStartStore ExecStore) *cobra.Command { var host bool + var org string cmd := &cobra.Command{ Annotations: map[string]string{"access": ""}, Use: "exec [instance...] ", @@ -87,13 +88,18 @@ func NewCmdExec(t *terminal.Terminal, store ExecStore, noLoginStartStore ExecSto return breverrors.NewValidationError("command is required") } + resolvedOrg, err := util.ResolveOrgFromFlag(store, org) + if err != nil { + return breverrors.WrapAndTrace(err) + } + // Run on each instance var errors error for _, instanceName := range instanceNames { if len(instanceNames) > 1 { fmt.Fprintf(os.Stderr, "\n=== %s ===\n", instanceName) } - err = runExecCommand(t, store, instanceName, host, cmdToRun) + err = runExecCommand(t, store, instanceName, host, cmdToRun, resolvedOrg.ID) if err != nil { if len(instanceNames) > 1 { fmt.Fprintf(os.Stderr, "Error on %s: %v\n", instanceName, err) @@ -114,6 +120,12 @@ func NewCmdExec(t *terminal.Terminal, store ExecStore, noLoginStartStore ExecSto }, } cmd.Flags().BoolVarP(&host, "host", "", false, "ssh into the host machine instead of the container") + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") + errRegComp := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(noLoginStartStore, t)) + if errRegComp != nil { + breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(errRegComp)) + fmt.Print(breverrors.WrapAndTrace(errRegComp)) + } return cmd } @@ -175,7 +187,7 @@ func parseCommand(command string) (string, error) { const pollTimeout = 10 * time.Minute -func runExecCommand(t *terminal.Terminal, sstore ExecStore, workspaceNameOrID string, host bool, command string) error { +func runExecCommand(t *terminal.Terminal, sstore ExecStore, workspaceNameOrID string, host bool, command string, orgID string) error { // Determine SSH alias: use the workspace name directly (with -host suffix if needed) sshName := workspaceNameOrID if host { @@ -196,7 +208,7 @@ func runExecCommand(t *terminal.Terminal, sstore ExecStore, workspaceNameOrID st // SSH failed — now check what's going on with the instance fmt.Fprintf(os.Stderr, "Connection failed, checking instance status...\n") - workspace, lookupErr := util.GetUserWorkspaceByNameOrIDErr(sstore, workspaceNameOrID) + workspace, lookupErr := util.GetUserWorkspaceByNameOrIDErrInOrg(sstore, workspaceNameOrID, orgID) if lookupErr != nil { return breverrors.WrapAndTrace(fmt.Errorf( "ssh connection failed and could not look up instance %q: %w\nPlease check your instances with: brev ls", diff --git a/pkg/cmd/gpucreate/gpucreate.go b/pkg/cmd/gpucreate/gpucreate.go index 3576cce18..c27779d71 100644 --- a/pkg/cmd/gpucreate/gpucreate.go +++ b/pkg/cmd/gpucreate/gpucreate.go @@ -94,6 +94,7 @@ type GPUCreateStore interface { util.GetWorkspaceByNameOrIDErrStore gpusearch.GPUSearchStore GetActiveOrganizationOrDefault() (*entity.Organization, error) + GetOrganizations(options *store.GetOrganizationsOptions) ([]entity.Organization, error) GetCurrentUser() (*entity.User, error) GetWorkspace(workspaceID string) (*entity.Workspace, error) CreateWorkspace(organizationID string, options *store.CreateWorkspacesOptions) (*entity.Workspace, error) @@ -154,6 +155,7 @@ func NewCmdGPUCreate(t *terminal.Terminal, gpuCreateStore GPUCreateStore) *cobra var containerImage string var composeFile string var filters searchFilterFlags + var org string cmd := &cobra.Command{ Annotations: map[string]string{"workspace": ""}, @@ -230,7 +232,7 @@ func NewCmdGPUCreate(t *terminal.Terminal, gpuCreateStore GPUCreateStore) *cobra ComposeFile: composeFile, } - err = RunGPUCreate(t, gpuCreateStore, opts) + err = RunGPUCreate(t, gpuCreateStore, opts, org) if err != nil { return breverrors.WrapAndTrace(err) } @@ -239,6 +241,7 @@ func NewCmdGPUCreate(t *terminal.Terminal, gpuCreateStore GPUCreateStore) *cobra } registerCreateFlags(cmd, &name, &instanceTypes, &count, ¶llel, &detached, &timeout, &startupScript, &dryRun, &mode, &jupyter, &containerImage, &composeFile, &filters) + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") return cmd } @@ -551,7 +554,7 @@ type createContext struct { } // newCreateContext initializes the context for instance creation -func newCreateContext(t *terminal.Terminal, store GPUCreateStore, opts GPUCreateOptions) (*createContext, error) { +func newCreateContext(t *terminal.Terminal, store GPUCreateStore, opts GPUCreateOptions, orgFlag string) (*createContext, error) { piped := gpusearch.IsStdoutPiped() ctx := &createContext{ @@ -578,13 +581,10 @@ func newCreateContext(t *terminal.Terminal, store GPUCreateStore, opts GPUCreate ctx.user = user // Get organization - org, err := store.GetActiveOrganizationOrDefault() + org, err := util.ResolveOrgFromFlag(store, orgFlag) if err != nil { return nil, breverrors.WrapAndTrace(err) } - if org == nil { - return nil, breverrors.NewValidationError("no organization found") - } ctx.org = org // Fetch instance types with workspace groups @@ -754,8 +754,8 @@ func (c *createContext) printSummary(workspaces []*entity.Workspace) { } // RunGPUCreate executes the GPU create with retry logic -func RunGPUCreate(t *terminal.Terminal, gpuCreateStore GPUCreateStore, opts GPUCreateOptions) error { - ctx, err := newCreateContext(t, gpuCreateStore, opts) +func RunGPUCreate(t *terminal.Terminal, gpuCreateStore GPUCreateStore, opts GPUCreateOptions, orgFlag string) error { + ctx, err := newCreateContext(t, gpuCreateStore, opts, orgFlag) if err != nil { return err } diff --git a/pkg/cmd/notebook/notebook.go b/pkg/cmd/notebook/notebook.go index 46d221da3..afc8de0bd 100644 --- a/pkg/cmd/notebook/notebook.go +++ b/pkg/cmd/notebook/notebook.go @@ -66,7 +66,7 @@ func NewCmdNotebook(store NotebookStore, t *terminal.Terminal) *cobra.Command { hello.TypeItToMeUnskippable27("\nClick here to go to your Jupyter notebook:\n\t 👉" + urlType("http://localhost:8888") + "👈\n\n\n") // Port forward on 8888 - err2 := portforward.RunPortforward(t, store, args[0], "8888:8888", false) + err2 := portforward.RunPortforward(t, store, args[0], "8888:8888", false, "") if err2 != nil { return breverrors.WrapAndTrace(err2) } diff --git a/pkg/cmd/ollama/ollama.go b/pkg/cmd/ollama/ollama.go index c65fd7e36..4f6e9fd9c 100644 --- a/pkg/cmd/ollama/ollama.go +++ b/pkg/cmd/ollama/ollama.go @@ -39,6 +39,7 @@ type OllamaStore interface { refresh.RefreshStore util.GetWorkspaceByNameOrIDErrStore GetActiveOrganizationOrDefault() (*entity.Organization, error) + GetOrganizations(options *store.GetOrganizationsOptions) ([]entity.Organization, error) GetCurrentUser() (*entity.User, error) CreateWorkspace(organizationID string, options *store.CreateWorkspacesOptions) (*entity.Workspace, error) GetWorkspace(workspaceID string) (*entity.Workspace, error) @@ -71,6 +72,7 @@ func validateModelType(input string) (bool, error) { func NewCmdOllama(t *terminal.Terminal, ollamaStore OllamaStore) *cobra.Command { var model string var gpu string + var org string cmd := &cobra.Command{ Use: "ollama", @@ -106,7 +108,7 @@ func NewCmdOllama(t *terminal.Terminal, ollamaStore OllamaStore) *cobra.Command err := runOllamaWorkspace(t, RunOptions{ Model: model, GPUType: gpu, - }, ollamaStore) + }, ollamaStore, org) if err != nil { return nil, breverrors.WrapAndTrace(err) } @@ -127,6 +129,7 @@ func NewCmdOllama(t *terminal.Terminal, ollamaStore OllamaStore) *cobra.Command } cmd.Flags().StringVarP(&model, "model", "m", "", "AI/ML model type (e.g., llama2, llama3, mistral7b)") cmd.Flags().StringVarP(&gpu, "gpu", "g", "g5.xlarge", "GPU instance type. See https://brev.dev/docs/reference/gpu for details") + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") return cmd } @@ -135,13 +138,13 @@ type RunOptions struct { GPUType string } -func runOllamaWorkspace(t *terminal.Terminal, opts RunOptions, ollamaStore OllamaStore) error { //nolint:funlen, gocyclo // todo +func runOllamaWorkspace(t *terminal.Terminal, opts RunOptions, ollamaStore OllamaStore, orgFlag string) error { //nolint:funlen, gocyclo // todo _, err := ollamaStore.GetCurrentUser() if err != nil { return breverrors.WrapAndTrace(err) } - org, err := ollamaStore.GetActiveOrganizationOrDefault() + org, err := util.ResolveOrgFromFlag(ollamaStore, orgFlag) if err != nil { return breverrors.WrapAndTrace(err) } diff --git a/pkg/cmd/portforward/portforward.go b/pkg/cmd/portforward/portforward.go index d0a6fa977..d026e729c 100644 --- a/pkg/cmd/portforward/portforward.go +++ b/pkg/cmd/portforward/portforward.go @@ -36,6 +36,7 @@ type PortforwardStore interface { func NewCmdPortForwardSSH(pfStore PortforwardStore, t *terminal.Terminal) *cobra.Command { var port string var useHost bool + var org string cmd := &cobra.Command{ Annotations: map[string]string{"access": ""}, Use: "port-forward", @@ -49,7 +50,7 @@ func NewCmdPortForwardSSH(pfStore PortforwardStore, t *terminal.Terminal) *cobra if port == "" { port = startInput(t) } - err := RunPortforward(t, pfStore, args[0], port, useHost) + err := RunPortforward(t, pfStore, args[0], port, useHost, org) if err != nil { return breverrors.WrapAndTrace(err) } @@ -58,6 +59,12 @@ func NewCmdPortForwardSSH(pfStore PortforwardStore, t *terminal.Terminal) *cobra } cmd.Flags().StringVarP(&port, "port", "p", "", "port forward string, local_port:remote_port") cmd.Flags().BoolVar(&useHost, "host", false, "Use the -host version of the instance") + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") + errOrgComp := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(pfStore, t)) + if errOrgComp != nil { + breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(errOrgComp)) + fmt.Print(breverrors.WrapAndTrace(errOrgComp)) + } err := cmd.RegisterFlagCompletionFunc("port", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoSpace }) @@ -86,7 +93,7 @@ func isPortAlreadyAllocatedError(err error) bool { return err != nil && strings.Contains(err.Error(), "already allocated") } -func RunPortforward(t *terminal.Terminal, pfStore PortforwardStore, nameOrID string, portString string, useHost bool) error { +func RunPortforward(t *terminal.Terminal, pfStore PortforwardStore, nameOrID string, portString string, useHost bool, orgFlag string) error { localPort, remotePort, err := parsePortString(portString) if err != nil { return err @@ -94,7 +101,11 @@ func RunPortforward(t *terminal.Terminal, pfStore PortforwardStore, nameOrID str res := refresh.RunRefreshAsync(pfStore) - target, err := util.ResolveWorkspaceOrNode(pfStore, nameOrID) + resolvedOrg, err := util.ResolveOrgFromFlag(pfStore, orgFlag) + if err != nil { + return breverrors.WrapAndTrace(err) + } + target, err := util.ResolveWorkspaceOrNodeInOrg(pfStore, nameOrID, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } diff --git a/pkg/cmd/recreate/recreate.go b/pkg/cmd/recreate/recreate.go index f9a481b32..bc70161f5 100644 --- a/pkg/cmd/recreate/recreate.go +++ b/pkg/cmd/recreate/recreate.go @@ -3,6 +3,7 @@ package recreate import ( _ "embed" + "fmt" "strings" "time" @@ -34,6 +35,7 @@ type recreateStore interface { } func NewCmdRecreate(t *terminal.Terminal, store recreateStore) *cobra.Command { + var org string cmd := &cobra.Command{ Use: "recreate", DisableFlagsInUseLine: true, @@ -41,19 +43,29 @@ func NewCmdRecreate(t *terminal.Terminal, store recreateStore) *cobra.Command { Long: stripmd.Strip(long), Example: "TODO", RunE: func(cmd *cobra.Command, args []string) error { - err := RunRecreate(t, args, store) + err := RunRecreate(t, args, store, org) if err != nil { return breverrors.WrapAndTrace(err) } return nil }, } + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") + errRegComp := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(store, t)) + if errRegComp != nil { + breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(errRegComp)) + fmt.Print(breverrors.WrapAndTrace(errRegComp)) + } return cmd } -func RunRecreate(t *terminal.Terminal, args []string, recreateStore recreateStore) error { +func RunRecreate(t *terminal.Terminal, args []string, recreateStore recreateStore, orgFlag string) error { + resolvedOrg, err := util.ResolveOrgFromFlag(recreateStore, orgFlag) + if err != nil { + return breverrors.WrapAndTrace(err) + } for _, arg := range args { - err := hardResetProcess(arg, t, recreateStore) + err := hardResetProcess(arg, t, recreateStore, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -62,9 +74,9 @@ func RunRecreate(t *terminal.Terminal, args []string, recreateStore recreateStor } // hardResetProcess deletes an existing workspace and creates a new one -func hardResetProcess(workspaceName string, t *terminal.Terminal, recreateStore recreateStore) error { +func hardResetProcess(workspaceName string, t *terminal.Terminal, recreateStore recreateStore, orgID string) error { t.Vprint(t.Green("recreating 🤙 " + t.Yellow("This can take a couple of minutes.\n"))) - workspace, err := util.GetUserWorkspaceByNameOrIDErr(recreateStore, workspaceName) + workspace, err := util.GetUserWorkspaceByNameOrIDErrInOrg(recreateStore, workspaceName, orgID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -78,12 +90,12 @@ func hardResetProcess(workspaceName string, t *terminal.Terminal, recreateStore time.Sleep(10 * time.Second) if len(deletedWorkspace.GitRepo) != 0 { - err := hardResetCreateWorkspaceFromRepo(t, recreateStore, deletedWorkspace) + err := hardResetCreateWorkspaceFromRepo(t, recreateStore, deletedWorkspace, orgID) if err != nil { return breverrors.WrapAndTrace(err) } } else { - err := hardResetCreateEmptyWorkspace(t, recreateStore, deletedWorkspace) + err := hardResetCreateEmptyWorkspace(t, recreateStore, deletedWorkspace, orgID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -92,17 +104,8 @@ func hardResetProcess(workspaceName string, t *terminal.Terminal, recreateStore } // hardResetCreateWorkspaceFromRepo clone a GIT repository, triggeres from the --hardreset flag -func hardResetCreateWorkspaceFromRepo(t *terminal.Terminal, recreateStore recreateStore, workspace *entity.Workspace) error { +func hardResetCreateWorkspaceFromRepo(t *terminal.Terminal, recreateStore recreateStore, workspace *entity.Workspace, orgID string) error { t.Vprint(t.Green("Instance is starting. ") + t.Yellow("This can take up to 2 minutes the first time.")) - var orgID string - activeorg, err := recreateStore.GetActiveOrganizationOrDefault() - if err != nil { - return breverrors.WrapAndTrace(err) - } - if activeorg == nil { - return breverrors.NewValidationError("no org exist") - } - orgID = activeorg.ID clusterID := config.GlobalConfig.GetDefaultClusterID() options := store.NewCreateWorkspacesOptions(clusterID, workspace.Name).WithGitRepo(workspace.GitRepo) @@ -134,7 +137,7 @@ func hardResetCreateWorkspaceFromRepo(t *terminal.Terminal, recreateStore recrea } // hardResetCreateEmptyWorkspace creates a new empty worksapce, triggered from the --hardreset flag -func hardResetCreateEmptyWorkspace(t *terminal.Terminal, recreateStore recreateStore, workspace *entity.Workspace) error { +func hardResetCreateEmptyWorkspace(t *terminal.Terminal, recreateStore recreateStore, workspace *entity.Workspace, orgID string) error { t.Vprint(t.Green("Instance is starting. ") + t.Yellow("This can take up to 2 minutes the first time.\n")) // ensure name @@ -142,16 +145,6 @@ func hardResetCreateEmptyWorkspace(t *terminal.Terminal, recreateStore recreateS return breverrors.NewValidationError("name field is required for empty instances") } - // ensure org - var orgID string - activeorg, err := recreateStore.GetActiveOrganizationOrDefault() - if err != nil { - return breverrors.WrapAndTrace(err) - } - if activeorg == nil { - return breverrors.NewValidationError("no org exist") - } - orgID = activeorg.ID clusterID := config.GlobalConfig.GetDefaultClusterID() options := store.NewCreateWorkspacesOptions(clusterID, workspace.Name) diff --git a/pkg/cmd/reset/reset.go b/pkg/cmd/reset/reset.go index b2ff61c0c..4b8af171e 100644 --- a/pkg/cmd/reset/reset.go +++ b/pkg/cmd/reset/reset.go @@ -2,6 +2,7 @@ package reset import ( _ "embed" + "fmt" "strings" "time" @@ -37,6 +38,7 @@ type ResetStore interface { func NewCmdReset(t *terminal.Terminal, loginResetStore ResetStore, noLoginResetStore ResetStore) *cobra.Command { var hardreset bool + var org string cmd := &cobra.Command{ Annotations: map[string]string{"provider-dependent": ""}, @@ -47,14 +49,18 @@ func NewCmdReset(t *terminal.Terminal, loginResetStore ResetStore, noLoginResetS Example: startExample, ValidArgsFunction: completions.GetAllWorkspaceNameCompletionHandler(noLoginResetStore, t), RunE: func(cmd *cobra.Command, args []string) error { + resolvedOrg, err := util.ResolveOrgFromFlag(loginResetStore, org) + if err != nil { + return breverrors.WrapAndTrace(err) + } for _, arg := range args { if hardreset { - err := hardResetProcess(arg, t, loginResetStore) + err := hardResetProcess(arg, t, loginResetStore, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } } else { - err := resetWorkspace(arg, t, loginResetStore) + err := resetWorkspace(arg, t, loginResetStore, resolvedOrg.ID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -65,13 +71,19 @@ func NewCmdReset(t *terminal.Terminal, loginResetStore ResetStore, noLoginResetS } cmd.Flags().BoolVarP(&hardreset, "hard", "", false, "DEPRECATED: use brev recreate") + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") + errRegComp := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(noLoginResetStore, t)) + if errRegComp != nil { + breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(errRegComp)) + fmt.Print(breverrors.WrapAndTrace(errRegComp)) + } return cmd } // hardResetProcess deletes an existing workspace and creates a new one -func hardResetProcess(workspaceName string, t *terminal.Terminal, resetStore ResetStore) error { +func hardResetProcess(workspaceName string, t *terminal.Terminal, resetStore ResetStore, orgID string) error { t.Vprint(t.Green("Starting hard reset 🤙 " + t.Yellow("This can take a couple of minutes.\n"))) - workspace, err := util.GetUserWorkspaceByNameOrIDErr(resetStore, workspaceName) + workspace, err := util.GetUserWorkspaceByNameOrIDErrInOrg(resetStore, workspaceName, orgID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -85,12 +97,12 @@ func hardResetProcess(workspaceName string, t *terminal.Terminal, resetStore Res time.Sleep(10 * time.Second) if len(deletedWorkspace.GitRepo) != 0 { - err := hardResetCreateWorkspaceFromRepo(t, resetStore, deletedWorkspace) + err := hardResetCreateWorkspaceFromRepo(t, resetStore, deletedWorkspace, orgID) if err != nil { return breverrors.WrapAndTrace(err) } } else { - err := hardResetCreateEmptyWorkspace(t, resetStore, deletedWorkspace) + err := hardResetCreateEmptyWorkspace(t, resetStore, deletedWorkspace, orgID) if err != nil { return breverrors.WrapAndTrace(err) } @@ -101,17 +113,8 @@ func hardResetProcess(workspaceName string, t *terminal.Terminal, resetStore Res } // hardResetCreateWorkspaceFromRepo clone a GIT repository, triggeres from the --hardreset flag -func hardResetCreateWorkspaceFromRepo(t *terminal.Terminal, resetStore ResetStore, workspace *entity.Workspace) error { +func hardResetCreateWorkspaceFromRepo(t *terminal.Terminal, resetStore ResetStore, workspace *entity.Workspace, orgID string) error { t.Vprint(t.Green("Instance is starting. ") + t.Yellow("This can take up to 2 minutes the first time.")) - var orgID string - activeorg, err := resetStore.GetActiveOrganizationOrDefault() - if err != nil { - return breverrors.WrapAndTrace(err) - } - if activeorg == nil { - return breverrors.NewValidationError("no org exist") - } - orgID = activeorg.ID clusterID := config.GlobalConfig.GetDefaultClusterID() options := store.NewCreateWorkspacesOptions(clusterID, workspace.Name).WithGitRepo(workspace.GitRepo) @@ -143,7 +146,7 @@ func hardResetCreateWorkspaceFromRepo(t *terminal.Terminal, resetStore ResetStor } // hardResetCreateEmptyWorkspace creates a new empty worksapce, triggered from the --hardreset flag -func hardResetCreateEmptyWorkspace(t *terminal.Terminal, resetStore ResetStore, workspace *entity.Workspace) error { +func hardResetCreateEmptyWorkspace(t *terminal.Terminal, resetStore ResetStore, workspace *entity.Workspace, orgID string) error { t.Vprint(t.Green("Instance is starting. ") + t.Yellow("This can take up to 2 minutes the first time.\n")) // ensure name @@ -151,16 +154,6 @@ func hardResetCreateEmptyWorkspace(t *terminal.Terminal, resetStore ResetStore, return breverrors.NewValidationError("name field is required for empty instances") } - // ensure org - var orgID string - activeorg, err := resetStore.GetActiveOrganizationOrDefault() - if err != nil { - return breverrors.WrapAndTrace(err) - } - if activeorg == nil { - return breverrors.NewValidationError("no org exist") - } - orgID = activeorg.ID clusterID := config.GlobalConfig.GetDefaultClusterID() options := store.NewCreateWorkspacesOptions(clusterID, workspace.Name) @@ -234,8 +227,8 @@ func resolveWorkspaceUserOptions(options *store.CreateWorkspacesOptions, user *e return options } -func resetWorkspace(workspaceName string, t *terminal.Terminal, resetStore ResetStore) error { - workspace, err := util.GetUserWorkspaceByNameOrIDErr(resetStore, workspaceName) +func resetWorkspace(workspaceName string, t *terminal.Terminal, resetStore ResetStore, orgID string) error { + workspace, err := util.GetUserWorkspaceByNameOrIDErrInOrg(resetStore, workspaceName, orgID) if err != nil { return breverrors.WrapAndTrace(err) } diff --git a/pkg/cmd/stop/stop.go b/pkg/cmd/stop/stop.go index bdf855f2a..5a41b1e95 100644 --- a/pkg/cmd/stop/stop.go +++ b/pkg/cmd/stop/stop.go @@ -32,6 +32,7 @@ type StopStore interface { func NewCmdStop(t *terminal.Terminal, loginStopStore StopStore, noLoginStopStore StopStore) *cobra.Command { var all bool + var org string cmd := &cobra.Command{ Annotations: map[string]string{"provider-dependent": ""}, @@ -44,8 +45,12 @@ func NewCmdStop(t *terminal.Terminal, loginStopStore StopStore, noLoginStopStore ValidArgsFunction: completions.GetAllWorkspaceNameCompletionHandler(noLoginStopStore, t), RunE: func(cmd *cobra.Command, args []string) error { piped := util.IsStdoutPiped() + resolvedOrg, err := util.ResolveOrgFromFlag(loginStopStore, org) + if err != nil { + return breverrors.WrapAndTrace(err) + } if all { - return stopAllWorkspaces(t, loginStopStore, piped) + return stopAllWorkspaces(t, loginStopStore, piped, resolvedOrg.ID) } else { names, err := util.GetInstanceNames(args) if err != nil { @@ -54,7 +59,7 @@ func NewCmdStop(t *terminal.Terminal, loginStopStore StopStore, noLoginStopStore var allErr error var stoppedNames []string for _, name := range names { - err := stopWorkspace(name, t, loginStopStore, piped) + err := stopWorkspace(name, t, loginStopStore, piped, resolvedOrg.ID) if err != nil { allErr = multierror.Append(allErr, err) } else { @@ -75,20 +80,22 @@ func NewCmdStop(t *terminal.Terminal, loginStopStore StopStore, noLoginStopStore }, } cmd.Flags().BoolVarP(&all, "all", "a", false, "stop all instances") + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") + err := cmd.RegisterFlagCompletionFunc("org", completions.GetOrgsNameCompletionHandler(noLoginStopStore, t)) + if err != nil { + breverrors.GetDefaultErrorReporter().ReportError(breverrors.WrapAndTrace(err)) + fmt.Print(breverrors.WrapAndTrace(err)) + } return cmd } -func stopAllWorkspaces(t *terminal.Terminal, stopStore StopStore, piped bool) error { +func stopAllWorkspaces(t *terminal.Terminal, stopStore StopStore, piped bool, orgID string) error { user, err := stopStore.GetCurrentUser() if err != nil { return breverrors.WrapAndTrace(err) } - org, err := stopStore.GetActiveOrganizationOrDefault() - if err != nil { - return breverrors.WrapAndTrace(err) - } - workspaces, err := stopStore.GetWorkspaces(org.ID, &store.GetWorkspacesOptions{UserID: user.ID}) + workspaces, err := stopStore.GetWorkspaces(orgID, &store.GetWorkspacesOptions{UserID: user.ID}) if err != nil { return breverrors.WrapAndTrace(err) } @@ -118,7 +125,7 @@ func stopAllWorkspaces(t *terminal.Terminal, stopStore StopStore, piped bool) er return nil } -func stopWorkspace(workspaceName string, t *terminal.Terminal, stopStore StopStore, piped bool) error { +func stopWorkspace(workspaceName string, t *terminal.Terminal, stopStore StopStore, piped bool, orgID string) error { user, err := stopStore.GetCurrentUser() if err != nil { return breverrors.WrapAndTrace(err) @@ -136,7 +143,7 @@ func stopWorkspace(workspaceName string, t *terminal.Terminal, stopStore StopSto } workspaceID = wsID } else { - workspace, err3 := util.GetUserWorkspaceByNameOrIDErr(stopStore, workspaceName) + workspace, err3 := util.GetUserWorkspaceByNameOrIDErrInOrg(stopStore, workspaceName, orgID) if err3 != nil { if !strings.Contains(err3.Error(), "not found") { return breverrors.WrapAndTrace(err3) @@ -145,7 +152,7 @@ func stopWorkspace(workspaceName string, t *terminal.Terminal, stopStore StopSto if !piped { fmt.Println("admin trying to stop any instance") } - workspace, err = util.GetAnyWorkspaceByIDOrNameInActiveOrgErr(stopStore, workspaceName) + workspace, err = util.GetAnyWorkspaceByIDOrNameInOrgErr(stopStore, workspaceName, orgID) if err != nil { return breverrors.WrapAndTrace(err) } diff --git a/pkg/cmd/util/util.go b/pkg/cmd/util/util.go index cc96130ec..b2059f7dd 100644 --- a/pkg/cmd/util/util.go +++ b/pkg/cmd/util/util.go @@ -76,7 +76,12 @@ func GetAnyWorkspaceByIDOrNameInActiveOrgErr(storeQ GetWorkspaceByNameOrIDErrSto if err != nil { return nil, breverrors.WrapAndTrace(err) } - workspaces, err := storeQ.GetWorkspaceByNameOrID(org.ID, workspaceNameOrID) + return GetAnyWorkspaceByIDOrNameInOrgErr(storeQ, workspaceNameOrID, org.ID) +} + +// GetAnyWorkspaceByIDOrNameInOrgErr looks up any workspace (not just the user's) by name or ID in a specific org. +func GetAnyWorkspaceByIDOrNameInOrgErr(storeQ GetWorkspaceByNameOrIDErrStore, workspaceNameOrID string, orgID string) (*entity.Workspace, error) { + workspaces, err := storeQ.GetWorkspaceByNameOrID(orgID, workspaceNameOrID) if err != nil { return nil, breverrors.WrapAndTrace(err) } diff --git a/pkg/cmd/workspacegroups/workspacegroups.go b/pkg/cmd/workspacegroups/workspacegroups.go index 13fa5423d..d6b75cf0a 100644 --- a/pkg/cmd/workspacegroups/workspacegroups.go +++ b/pkg/cmd/workspacegroups/workspacegroups.go @@ -6,17 +6,21 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" + "github.com/brevdev/brev-cli/pkg/cmd/util" "github.com/brevdev/brev-cli/pkg/entity" breverrors "github.com/brevdev/brev-cli/pkg/errors" + "github.com/brevdev/brev-cli/pkg/store" "github.com/brevdev/brev-cli/pkg/terminal" ) type WorkspaceGroupsStore interface { GetWorkspaceGroups(organizationID string) ([]entity.WorkspaceGroup, error) GetActiveOrganizationOrDefault() (*entity.Organization, error) + GetOrganizations(options *store.GetOrganizationsOptions) ([]entity.Organization, error) } func NewCmdWorkspaceGroups(t *terminal.Terminal, store WorkspaceGroupsStore) *cobra.Command { + var org string cmd := &cobra.Command{ Use: "workspacegroups", DisableFlagsInUseLine: true, @@ -24,18 +28,19 @@ func NewCmdWorkspaceGroups(t *terminal.Terminal, store WorkspaceGroupsStore) *co Long: "TODO", Example: "TODO", RunE: func(cmd *cobra.Command, args []string) error { - err := RunWorkspaceGroups(t, args, store) + err := RunWorkspaceGroups(t, args, store, org) if err != nil { return breverrors.WrapAndTrace(err) } return nil }, } + cmd.Flags().StringVarP(&org, "org", "o", "", "organization (will override active org)") return cmd } -func RunWorkspaceGroups(_ *terminal.Terminal, _ []string, store WorkspaceGroupsStore) error { - org, err := store.GetActiveOrganizationOrDefault() +func RunWorkspaceGroups(_ *terminal.Terminal, _ []string, store WorkspaceGroupsStore, orgFlag string) error { + org, err := util.ResolveOrgFromFlag(store, orgFlag) if err != nil { return breverrors.WrapAndTrace(err) } From df9ccaebf5713f8fa9e1378a0cb72e81f9d687df Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:34:22 +0000 Subject: [PATCH 3/3] fix: add GetOrganizations to MockGPUCreateStore for test compatibility Co-Authored-By: Alec Fong --- pkg/cmd/gpucreate/gpucreate_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/gpucreate/gpucreate_test.go b/pkg/cmd/gpucreate/gpucreate_test.go index ea82269c6..1bad10a9a 100644 --- a/pkg/cmd/gpucreate/gpucreate_test.go +++ b/pkg/cmd/gpucreate/gpucreate_test.go @@ -48,6 +48,13 @@ func (m *MockGPUCreateStore) GetActiveOrganizationOrDefault() (*entity.Organizat return m.Org, nil } +func (m *MockGPUCreateStore) GetOrganizations(_ *store.GetOrganizationsOptions) ([]entity.Organization, error) { + if m.Org != nil { + return []entity.Organization{*m.Org}, nil + } + return []entity.Organization{}, nil +} + func (m *MockGPUCreateStore) GetWorkspace(workspaceID string) (*entity.Workspace, error) { if ws, ok := m.Workspaces[workspaceID]; ok { return ws, nil