@@ -10,6 +10,7 @@ import (
1010
1111 nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
1212 "connectrpc.com/connect"
13+ "github.com/briandowns/spinner"
1314 "github.com/google/uuid"
1415
1516 "github.com/brevdev/brev-cli/pkg/config"
@@ -141,28 +142,51 @@ func runRegister(ctx context.Context, t *terminal.Terminal, s RegisterStore, nam
141142// runRegisterSteps performs netbird install, hardware profile, AddNode, save registration, and runSetup.
142143// It does not prompt or enable SSH. Used by both flag-driven and prompt-driven flows.
143144func runRegisterSteps (ctx context.Context , t * terminal.Terminal , s RegisterStore , name string , org * entity.Organization , deps registerDeps ) (* DeviceRegistration , error ) {
145+ const clearLine = "\033 [2K\n "
144146 t .Vprint ("" )
147+
148+ // Stop spinner (and clear its line) before any of our prints so stdout and stderr don't interleave.
149+ var sp * spinner.Spinner
150+ stopSpinner := func () {
151+ if sp != nil {
152+ sp .FinalMSG = clearLine
153+ sp .Stop ()
154+ sp = nil
155+ }
156+ }
157+ defer stopSpinner ()
158+
145159 t .Vprint (t .Yellow ("[Step 1/3] Setting up Brev tunnel..." ))
146- if err := deps .netbird .Install (); err != nil {
160+ sp = t .NewSpinner ()
161+ sp .Suffix = " Registering device..."
162+ sp .Start ()
163+ err := deps .netbird .Install ()
164+ stopSpinner ()
165+ if err != nil {
147166 return nil , fmt .Errorf ("brev tunnel setup failed: %w" , err )
148167 }
149- t .Vprint ( t . Green ( " Brev tunnel ready." ))
168+ t .Vprintf ( "%s Brev tunnel ready.\n " , t . Green ( " ✓ " ))
150169
151170 t .Vprint ("" )
152171 t .Vprint (t .Yellow ("[Step 2/3] Collecting hardware profile..." ))
153- t .Vprint ("" )
154-
172+ sp = t .NewSpinner ()
173+ sp .Suffix = " Registering device..."
174+ sp .Start ()
155175 hwProfile , err := deps .hardwareProfiler .Profile ()
176+ stopSpinner ()
156177 if err != nil {
157178 return nil , fmt .Errorf ("failed to collect hardware profile: %w" , err )
158179 }
159-
180+ t .Vprintf ("%s Hardware profile collected.\n " , t .Green (" ✓" ))
181+ t .Vprint ("" )
160182 t .Vprint (" Hardware profile:" )
161183 t .Vprint (FormatHardwareProfile (hwProfile ))
162184
163185 t .Vprint ("" )
164186 t .Vprint (t .Yellow ("[Step 3/3] Registering with Brev..." ))
165-
187+ sp = t .NewSpinner ()
188+ sp .Suffix = " Registering device..."
189+ sp .Start ()
166190 deviceID := uuid .New ().String ()
167191 client := deps .nodeClients .NewNodeClient (s , config .GlobalConfig .GetBrevPublicAPIURL ())
168192 addResp , err := client .AddNode (ctx , connect .NewRequest (& nodev1.AddNodeRequest {
@@ -172,6 +196,7 @@ func runRegisterSteps(ctx context.Context, t *terminal.Terminal, s RegisterStore
172196 NodeSpec : toProtoNodeSpec (hwProfile ),
173197 }))
174198 if err != nil {
199+ stopSpinner ()
175200 return nil , fmt .Errorf ("failed to register node: %w" , err )
176201 }
177202
@@ -180,21 +205,25 @@ func runRegisterSteps(ctx context.Context, t *terminal.Terminal, s RegisterStore
180205 ExternalNodeID : node .GetExternalNodeId (),
181206 DisplayName : name ,
182207 OrgID : org .ID ,
208+ OrgName : org .Name ,
183209 DeviceID : deviceID ,
184210 RegisteredAt : time .Now ().UTC ().Format (time .RFC3339 ),
185211 HardwareProfile : * hwProfile ,
186212 }
187213 if err := deps .registrationStore .Save (reg ); err != nil {
214+ stopSpinner ()
188215 return nil , fmt .Errorf ("node registered but failed to save locally: %w" , err )
189216 }
190217
191- t .Vprint (t .Green (" Registration complete." ))
192218 runSetup (node , t , deps )
219+ stopSpinner ()
220+ t .Vprintf ("%s Node registered.\n " , t .Green (" ✓" ))
221+ t .Vprintf ("%s Registration complete.\n " , t .Green (" ✓" ))
193222 return reg , nil
194223}
195224
196225// resolveOrgPromptDriven resolves organization for prompt-driven flow: by name if --org given, else always list and select with arrow keys.
197- func resolveOrgPromptDriven (s RegisterStore , orgName string , deps registerDeps ) (* entity.Organization , error ) {
226+ func resolveOrgPromptDriven (t * terminal. Terminal , s RegisterStore , orgName string , deps registerDeps ) (* entity.Organization , error ) {
198227 if orgName != "" {
199228 orgs , err := s .GetOrganizationsByName (orgName )
200229 if err != nil {
@@ -217,6 +246,7 @@ func resolveOrgPromptDriven(s RegisterStore, orgName string, deps registerDeps)
217246 return nil , fmt .Errorf ("no organization found; please create or join an organization first" )
218247 }
219248
249+ t .Vprint ("" )
220250 names := make ([]string , len (list ))
221251 for i := range list {
222252 names [i ] = list [i ].Name
@@ -239,6 +269,11 @@ func runRegisterPromptDriven(ctx context.Context, t *terminal.Terminal, s Regist
239269 return fmt .Errorf ("sudo issue: %w" , err )
240270 }
241271
272+ // Ensure user is logged in before any other prompts (email/login happens here if needed).
273+ if _ , err := s .GetCurrentUser (); err != nil {
274+ return breverrors .WrapAndTrace (err )
275+ }
276+
242277 alreadyRegistered , err := deps .registrationStore .Exists ()
243278 if err != nil {
244279 return breverrors .WrapAndTrace (err )
@@ -248,6 +283,7 @@ func runRegisterPromptDriven(ctx context.Context, t *terminal.Terminal, s Regist
248283 }
249284
250285 if name == "" {
286+ t .Vprint ("" )
251287 name = terminal .PromptGetInput (terminal.PromptContent {
252288 Label : "Device name" ,
253289 ErrorMsg : "name is required" ,
@@ -259,7 +295,8 @@ func runRegisterPromptDriven(ctx context.Context, t *terminal.Terminal, s Regist
259295 return breverrors .WrapAndTrace (err )
260296 }
261297
262- org , err := resolveOrgPromptDriven (s , orgName , deps )
298+ t .Vprint ("" )
299+ org , err := resolveOrgPromptDriven (t , s , orgName , deps )
263300 if err != nil {
264301 return err
265302 }
@@ -274,16 +311,20 @@ func runRegisterPromptDriven(ctx context.Context, t *terminal.Terminal, s Regist
274311 }
275312
276313 t .Vprint ("" )
277- t .Vprint (t .Green ("Registering your device with Brev" ))
314+ t .Vprint (t .White ("══════════════════════════════════════════════════" ))
315+ t .Vprint (t .White (" Registering your device with Brev" ))
316+ t .Vprint (t .White ("══════════════════════════════════════════════════" ))
317+ t .Vprint ("" )
318+ t .Vprint (t .Green (" Please confirm before continuing:" ))
278319 t .Vprint ("" )
279- t .Vprintf (" Name: %s\n " , t .Yellow (name ))
280- t .Vprintf (" Organization: %s\n " , org .Name )
281- t .Vprintf (" Registering for Linux user: %s \n " , osUser .Username )
320+ t .Vprintf (" %s %s\n " , t .Green ( fmt . Sprintf ( "%-14s" , "Device name:" )), t . BoldBlue (name ))
321+ t .Vprintf (" %s %s\n " , t . Green ( fmt . Sprintf ( "%-14s" , "Organization:" )), t . BoldBlue ( org .Name ) )
322+ t .Vprintf (" %s %s \n " , t . Green ( fmt . Sprintf ( "%-14s" , " Linux user:" )), t . BoldBlue ( osUser .Username ) )
282323 t .Vprint ("" )
283- t .Vprint ("This will perform the following steps:" )
284- t .Vprint (" 1. Set up Brev tunnel" )
285- t .Vprint (" 2. Collect hardware profile" )
286- t .Vprint (" 3. Register this machine with Brev" )
324+ t .Vprint (t . Yellow ( " This will:" ) )
325+ t .Vprint (" 1. Set up Brev tunnel" )
326+ t .Vprint (" 2. Collect hardware profile" )
327+ t .Vprint (" 3. Register this machine with Brev" )
287328 t .Vprint ("" )
288329
289330 if ! deps .prompter .ConfirmYesNo ("Proceed with registration?" ) {
@@ -465,22 +506,28 @@ func runSetup(node *nodev1.ExternalNode, t *terminal.Terminal, deps registerDeps
465506// grantSSHAccessWithPort enables SSH; if port is 0, prompts for port (prompt-driven). Otherwise uses the given port (flag-driven).
466507func grantSSHAccessWithPort (ctx context.Context , t * terminal.Terminal , deps registerDeps , tokenProvider externalnode.TokenProvider , reg * DeviceRegistration , brevUser * entity.User , osUser * user.User , port int32 ) error {
467508 t .Vprint ("" )
468- t .Vprint (t .Green ("Enabling SSH access on this device" ))
509+ t .Vprint (t .White ("══════════════════════════════════════════════════" ))
510+ t .Vprint (t .White (" Enabling SSH access on this device" ))
511+ t .Vprint (t .White ("══════════════════════════════════════════════════" ))
469512 t .Vprint ("" )
470- t .Vprintf (" Node: %s (%s)\n " , reg .DisplayName , reg .ExternalNodeID )
471- t .Vprintf (" Brev user: %s\n " , brevUser .ID )
472- t .Vprintf (" Linux user: %s\n " , osUser .Username )
513+ t .Vprint (t .Green (" Please confirm before continuing:" ))
473514 t .Vprint ("" )
515+ t .Vprintf (" %s %s\n " , t .Green (fmt .Sprintf ("%-14s" , "Device name:" )), t .BoldBlue (reg .DisplayName ))
516+ t .Vprintf (" %s %s\n " , t .Green (fmt .Sprintf ("%-14s" , "Node ID:" )), t .BoldBlue (reg .ExternalNodeID ))
517+ t .Vprintf (" %s %s\n " , t .Green (fmt .Sprintf ("%-14s" , "Brev user:" )), t .BoldBlue (brevUser .ID ))
518+ t .Vprintf (" %s %s\n " , t .Green (fmt .Sprintf ("%-14s" , "Linux user:" )), t .BoldBlue (osUser .Username ))
474519
475520 var err error
476521 if port == 0 {
522+ t .Vprint ("" )
477523 port , err = PromptSSHPort (t )
478524 if err != nil {
479525 return fmt .Errorf ("SSH port: %w" , err )
480526 }
481527 } else {
482- t .Vprintf (" SSH port: %d \n " , port )
528+ t .Vprintf (" %s %s \n " , t . Green ( fmt . Sprintf ( "%-14s" , " SSH port:" )), t . BoldBlue ( fmt . Sprintf ( "%d " , port )) )
483529 }
530+ t .Vprint ("" )
484531
485532 if err := OpenSSHPort (ctx , t , deps .nodeClients , tokenProvider , reg , port ); err != nil {
486533 return fmt .Errorf ("allocate SSH port failed: %w" , err )
@@ -491,6 +538,7 @@ func grantSSHAccessWithPort(ctx context.Context, t *terminal.Terminal, deps regi
491538 return fmt .Errorf ("grant SSH failed: %w" , err )
492539 }
493540
541+ t .Vprint ("" )
494542 t .Vprint (t .Green (fmt .Sprintf ("SSH access enabled. You can now SSH to this device via: brev shell %s" , reg .DisplayName )))
495543 return nil
496544}
0 commit comments