@@ -33,6 +33,7 @@ type LoginStore interface {
3333 auth.AuthStore
3434 GetCurrentUser () (* entity.User , error )
3535 CreateUser (idToken string ) (* entity.User , error )
36+ SetDefaultOrganization (org * entity.Organization ) error
3637 GetOrganizations (options * store.GetOrganizationsOptions ) ([]entity.Organization , error )
3738 GetActiveOrganizationOrDefault () (* entity.Organization , error )
3839 CreateOrganization (req store.CreateOrganizationRequest ) (* entity.Organization , error )
@@ -47,6 +48,7 @@ type LoginStore interface {
4748type Auth interface {
4849 Login (skipBrowser bool ) (* auth.LoginTokens , error )
4950 LoginWithToken (token string ) error
51+ LoginWithAPIKey (apiKey string ) error
5052}
5153
5254// loginStore must be a no prompt store
@@ -57,9 +59,11 @@ func NewCmdLogin(t *terminal.Terminal, loginStore LoginStore, auth Auth) *cobra.
5759 }
5860
5961 var loginToken string
62+ var apiKey string
6063 var skipBrowser bool
6164 var emailFlag string
6265 var authProviderFlag string
66+ apiKeyLogin := false
6367
6468 cmd := & cobra.Command {
6569 Annotations : map [string ]string {"configuration" : "" },
@@ -69,6 +73,9 @@ func NewCmdLogin(t *terminal.Terminal, loginStore LoginStore, auth Auth) *cobra.
6973 Long : "Log into brev" ,
7074 Example : "brev login" ,
7175 PostRunE : func (cmd * cobra.Command , args []string ) error {
76+ if apiKeyLogin {
77+ return nil
78+ }
7279 shouldWe := hello .ShouldWeRunOnboarding (loginStore )
7380 if shouldWe {
7481 user , err := loginStore .GetCurrentUser ()
@@ -84,7 +91,8 @@ func NewCmdLogin(t *terminal.Terminal, loginStore LoginStore, auth Auth) *cobra.
8491 },
8592 Args : cmderrors .TransformToValidationError (cobra .NoArgs ),
8693 RunE : func (cmd * cobra.Command , args []string ) error {
87- err := opts .RunLogin (t , loginToken , skipBrowser , emailFlag , authProviderFlag )
94+ apiKeyLogin = strings .TrimSpace (apiKey ) != ""
95+ err := opts .RunLogin (t , loginToken , apiKey , skipBrowser , emailFlag , authProviderFlag )
8896 if err != nil {
8997 // if err is ImportIDEConfigError, log err with sentry but continue
9098 if _ , ok := err .(* importideconfig.ImportIDEConfigError ); ! ok {
@@ -97,6 +105,9 @@ func NewCmdLogin(t *terminal.Terminal, loginStore LoginStore, auth Auth) *cobra.
97105 }
98106 return err //nolint:wrapcheck // we want to return the error from the login
99107 }
108+ if apiKeyLogin {
109+ return nil
110+ }
100111 // Offer Claude Code skill installation after successful login
101112 homeDir , homeErr := opts .LoginStore .UserHomeDir ()
102113 if homeErr == nil {
@@ -106,6 +117,7 @@ func NewCmdLogin(t *terminal.Terminal, loginStore LoginStore, auth Auth) *cobra.
106117 },
107118 }
108119 cmd .Flags ().StringVarP (& loginToken , "token" , "" , "" , "token provided to auto login" )
120+ cmd .Flags ().StringVar (& apiKey , "api-key" , "" , "api key and org ID provided as <api-key>::<org-id> to authenticate CLI requests" )
109121 cmd .Flags ().BoolVar (& skipBrowser , "skip-browser" , false , "print url instead of auto opening browser" )
110122 cmd .Flags ().StringVar (& emailFlag , "email" , "" , "email to use for authentication" )
111123 cmd .Flags ().StringVar (& authProviderFlag , "auth" , "" , "authentication provider to use (nvidia or legacy, default is nvidia)" )
@@ -157,7 +169,12 @@ func (o LoginOptions) getOrCreateOrg(username string) (*entity.Organization, err
157169 return org , nil
158170}
159171
160- func (o LoginOptions ) RunLogin (t * terminal.Terminal , loginToken string , skipBrowser bool , emailFlag string , authProviderFlag string ) error {
172+ func (o LoginOptions ) RunLogin (t * terminal.Terminal , loginToken string , apiKey string , skipBrowser bool , emailFlag string , authProviderFlag string ) error {
173+ apiKey = strings .TrimSpace (apiKey )
174+ if apiKey != "" {
175+ return o .doApiKeyLogin (t , loginToken , apiKey , skipBrowser , emailFlag , authProviderFlag )
176+ }
177+
161178 tokens , _ := o .LoginStore .GetAuthTokens ()
162179
163180 if authProviderFlag != "" && authProviderFlag != "nvidia" && authProviderFlag != "legacy" {
@@ -200,6 +217,41 @@ func (o LoginOptions) RunLogin(t *terminal.Terminal, loginToken string, skipBrow
200217 return nil
201218}
202219
220+ const apiKeyOrgIDSeparator = "::"
221+
222+ func parseAPIKeyAndOrgID (apiKeyArg string ) (string , string , error ) {
223+ apiKey , orgID , ok := strings .Cut (strings .TrimSpace (apiKeyArg ), apiKeyOrgIDSeparator )
224+ if ! ok {
225+ return "" , "" , breverrors .NewValidationError (fmt .Sprintf ("api-key must be formatted <api-key>%s<org-id>" , apiKeyOrgIDSeparator ))
226+ }
227+ orgID = strings .TrimSpace (orgID )
228+ if orgID == "" {
229+ return "" , "" , breverrors .NewValidationError ("org-id is required with api-key" )
230+ }
231+ return strings .TrimSpace (apiKey ), orgID , nil
232+ }
233+
234+ func (o LoginOptions ) doApiKeyLogin (t * terminal.Terminal , loginToken string , apiKeyArg string , skipBrowser bool , emailFlag string , authProviderFlag string ) error {
235+ if loginToken != "" || skipBrowser || emailFlag != "" || authProviderFlag != "" {
236+ return breverrors .NewValidationError ("api-key cannot be used with token, skip-browser, email, or auth flags" )
237+ }
238+ apiKey , orgID , err := parseAPIKeyAndOrgID (apiKeyArg )
239+ if err != nil {
240+ return breverrors .WrapAndTrace (err )
241+ }
242+ if err := o .Auth .LoginWithAPIKey (apiKey ); err != nil {
243+ return breverrors .WrapAndTrace (err )
244+ }
245+ if err := o .LoginStore .SetDefaultOrganization (& entity.Organization {
246+ ID : orgID ,
247+ Name : orgID ,
248+ }); err != nil {
249+ return breverrors .WrapAndTrace (err )
250+ }
251+ t .Vprint (t .Green (fmt .Sprintf ("API key saved for org %s" , orgID )))
252+ return nil
253+ }
254+
203255func (o LoginOptions ) handleOnboarding (user * entity.User , _ * terminal.Terminal ) error {
204256 // figure out if we should onboard the user
205257 currentOnboardingStatus , err := user .GetOnboardingData ()
0 commit comments