@@ -159,9 +159,8 @@ func runCreate(ctx context.Context, c *cli.Command) error {
159159 featureRef = devFeatureRef
160160 }
161161
162- w := c .Root ().Writer
163162 r := c .Root ().Reader
164- p := interact .NewPrinter (w )
163+ p := interact .NewPrinter (c . Root (). Writer )
165164
166165 // Step 1: Resolve device identity.
167166 deviceID , err := identity .GetDeviceID ()
@@ -176,117 +175,95 @@ func runCreate(ctx context.Context, c *cli.Command) error {
176175 var adm admin.Admin
177176 var existingBridges []* admin.BridgeInfo
178177
179- err = interact .RunSteps (ctx , []interact.Step {
180- {
181- Title : "Connecting to bridge administrator..." ,
182- Run : func (ctx context.Context ) error {
183- remote , dialErr := admin .NewRemote (adminAddr )
184- if dialErr != nil {
185- return errAdminUnavailable {}
186- }
187- // Probe with a quick ListBridges call to verify the admin is up.
188- probeCtx , cancel := context .WithTimeout (ctx , 5 * time .Second )
189- defer cancel ()
190- bridges , probeErr := remote .ListBridges (probeCtx , deviceID )
191- if probeErr != nil {
192- remote .Close ()
193- return errAdminUnavailable {}
194- }
195- existingBridges = bridges
196- adm = remote
197- return nil
198- },
199- },
200- })
201-
202- if _ , ok := err .(errAdminUnavailable ); ok {
203- err = nil
178+ sp := interact .NewSpinner ("Connecting to bridge administrator..." )
179+ go sp .Start (ctx )
180+
181+ remote , dialErr := admin .NewRemote (adminAddr )
182+ if dialErr == nil {
183+ probeCtx , cancel := context .WithTimeout (ctx , 5 * time .Second )
184+ bridges , probeErr := remote .ListBridges (probeCtx , deviceID )
185+ cancel ()
186+ if probeErr == nil {
187+ existingBridges = bridges
188+ adm = remote
189+ } else {
190+ remote .Close ()
191+ }
192+ }
193+ sp .Stop ()
204194
195+ if adm == nil {
205196 // Remote admin not available — offer local fallback.
206197 if ! yes {
207198 p .Newline ()
208199 p .Warn ("No bridge administrator found in the cluster." )
209200 p .Info (fmt .Sprintf ("Should Bridge use your local credentials for cluster %q instead." , kubeContext ))
210- fmt . Fprintf ( w , "Continue? [y/N] " )
201+ p . Prompt ( "Continue? [y/N] " )
211202
212- answer := promptYN (r )
213- if answer != "y" && answer != "yes" {
214- fmt .Fprintf (w , "Aborted.\n " )
203+ if answer := promptYN (r ); answer != "y" && answer != "yes" {
204+ p .Println ("Aborted." )
215205 return nil
216206 }
217207 }
218208
219- err = interact .RunSteps (ctx , []interact.Step {
220- {
221- Title : "Initializing local administrator..." ,
222- Run : func (ctx context.Context ) error {
223- localAdm , localErr := admin .NewLocal (admin.LocalConfig {
224- ProxyImage : proxyImage ,
225- })
226- if localErr != nil {
227- return fmt .Errorf ("failed to initialize: %w" , localErr )
228- }
229- adm = localAdm
230- bridges , listErr := localAdm .ListBridges (ctx , deviceID )
231- if listErr != nil {
232- slog .Warn ("Failed to list existing bridges" , "error" , listErr )
233- } else {
234- existingBridges = bridges
235- }
236- return nil
237- },
238- },
209+ sp = interact .NewSpinner ("Initializing local administrator..." )
210+ go sp .Start (ctx )
211+
212+ localAdm , localErr := admin .NewLocal (admin.LocalConfig {
213+ ProxyImage : proxyImage ,
239214 })
240- if err != nil {
241- return err
215+ if localErr != nil {
216+ sp .Stop ()
217+ return fmt .Errorf ("failed to initialize: %w" , localErr )
242218 }
243- } else if err != nil {
244- return err
219+ adm = localAdm
220+ bridges , listErr := localAdm .ListBridges (ctx , deviceID )
221+ if listErr != nil {
222+ slog .Warn ("Failed to list existing bridges" , "error" , listErr )
223+ } else {
224+ existingBridges = bridges
225+ }
226+
227+ sp .Stop ()
245228 }
246229 defer adm .Close ()
247230
248231 // Step 3: Check for existing bridges.
249- var conflictingBridge * admin.BridgeInfo
250232 if ! yes {
251233 for _ , bridge := range existingBridges {
252234 if bridge .SourceDeployment == deploymentName {
253- conflictingBridge = bridge
235+ p .Newline ()
236+ p .Warn ("An existing bridge already exists:" )
237+ p .KeyValue ("Name" , bridge .SourceDeployment )
238+ p .KeyValue ("Created" , bridge .CreatedAt )
239+ p .KeyValue ("Context" , kubeContext )
240+ p .Newline ()
241+ p .Muted ("This will tear down the existing bridge and recreate it." )
242+ p .Prompt ("Continue? [y/N] " )
243+
244+ if answer := promptYN (r ); answer != "y" && answer != "yes" {
245+ p .Println ("Aborted." )
246+ return nil
247+ }
248+ yes = true
254249 break
255250 }
256251 }
257252 }
258253
259- if conflictingBridge != nil {
260- p .Newline ()
261- p .Warn ("An existing bridge already exists:" )
262- p .KeyValue ("Name" , conflictingBridge .SourceDeployment )
263- p .KeyValue ("Created" , conflictingBridge .CreatedAt )
264- p .KeyValue ("Context" , kubeContext )
265- p .Newline ()
266- p .Muted ("This will tear down the existing bridge and recreate it." )
267- fmt .Fprintf (w , "Continue? [y/N] " )
268-
269- answer := promptYN (r )
270- if answer != "y" && answer != "yes" {
271- fmt .Fprintf (w , "Aborted.\n " )
272- return nil
273- }
274- yes = true
275- }
276-
277254 // Step 4: Create bridge.
278255 var createResp * admin.CreateResponse
279256
280- err = interact .RunWithSpinner (ctx , "Creating bridge..." , func (ctx context.Context ) error {
281- var createErr error
282- createResp , createErr = adm .CreateBridge (ctx , admin.CreateRequest {
283- DeviceID : deviceID ,
284- SourceDeployment : deploymentName ,
285- SourceNamespace : sourceNamespace ,
286- Force : yes ,
287- })
288- return createErr
257+ sp = interact .NewSpinner ("Creating bridge..." )
258+ go sp .Start (ctx )
259+
260+ createResp , err = adm .CreateBridge (ctx , admin.CreateRequest {
261+ DeviceID : deviceID ,
262+ SourceDeployment : deploymentName ,
263+ SourceNamespace : sourceNamespace ,
264+ Force : yes ,
289265 })
266+ sp .Stop ()
290267 if err != nil {
291268 return err
292269 }
@@ -306,12 +283,12 @@ func runCreate(ctx context.Context, c *cli.Command) error {
306283 if err != nil {
307284 return err
308285 }
309- dcConfigPath , err := generateDevcontainerConfig (p , w , deploymentName , baseConfig , featureRef , c .Int ("listen" ), createResp )
286+ dcConfigPath , err := generateDevcontainerConfig (p , baseConfig , featureRef , c .Int ("listen" ), createResp )
310287 if err != nil {
311288 return err
312289 }
313290 if connectFlag {
314- return startDevcontainer (ctx , dcConfigPath , r , w )
291+ return startDevcontainer (ctx , p , dcConfigPath , r )
315292 }
316293 }
317294
@@ -329,11 +306,8 @@ func promptYN(r io.Reader) string {
329306// It respects the KUBECONFIG env var by bind-mounting it into the container,
330307// unless the base config already sets containerEnv.KUBECONFIG.
331308// Returns the path to the generated config.
332- func generateDevcontainerConfig (p * interact.Printer , w io.Writer , deploymentName , baseConfigPath , featureRef string , appPort int , resp * admin.CreateResponse ) (string , error ) {
333- dcName := deploymentName
334- if dcName == "" {
335- dcName = "proxy"
336- }
309+ func generateDevcontainerConfig (p interact.Printer , baseConfigPath , featureRef string , appPort int , resp * admin.CreateResponse ) (string , error ) {
310+ dcName := resp .DeploymentName
337311
338312 // Place the generated config under the .devcontainer/ directory that contains
339313 // the base config. If the base config isn't already in a .devcontainer/ folder,
@@ -581,21 +555,24 @@ func currentKubeContext() string {
581555}
582556
583557// startDevcontainer starts the devcontainer and attaches an interactive shell.
584- func startDevcontainer (ctx context.Context , dcConfigPath string , r io.Reader , w io. Writer ) error {
558+ func startDevcontainer (ctx context.Context , p interact. Printer , dcConfigPath string , r io.Reader ) error {
585559 // <workspace>/.devcontainer/bridge-<name>/devcontainer.json → <workspace>
586560 workspaceFolder := filepath .Dir (filepath .Dir (filepath .Dir (dcConfigPath )))
587561 dcClient := & devcontainer.Client {
588562 WorkspaceFolder : workspaceFolder ,
589563 ConfigPath : dcConfigPath ,
590564 Stdin : r ,
591- Stdout : w ,
592- Stderr : w ,
565+ Stdout : os . Stdout ,
566+ Stderr : os . Stderr ,
593567 }
594568
595569 slog .Debug ("Starting devcontainer" , "config" , dcConfigPath , "workspace" , workspaceFolder )
596- err := interact .RunWithSpinner (ctx , "Starting devcontainer..." , func (ctx context.Context ) error {
597- return dcClient .Up (ctx )
598- })
570+
571+ sp := interact .NewSpinner ("Starting devcontainer..." )
572+ go sp .Start (ctx )
573+
574+ err := dcClient .Up (ctx )
575+ sp .Stop ()
599576 if err != nil {
600577 return fmt .Errorf ("failed to start devcontainer: %w" , err )
601578 }
0 commit comments