Skip to content

Commit 44c0006

Browse files
authored
fix(agents): ux feedback (#571)
* fix(agents): allow creation without secrets * fix(agents) don't require toml file in clean project
1 parent b29d903 commit 44c0006

File tree

5 files changed

+121
-86
lines changed

5 files changed

+121
-86
lines changed

cmd/lk/agent.go

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,11 @@ func createAgentClient(ctx context.Context, cmd *cli.Command) (context.Context,
253253
workingDir = cmd.Args().First()
254254
}
255255

256-
// Verify that the project and agent config match, if it exists.
256+
// If a project has been manually selected that conflicts with the agent's config,
257+
// or if the config file is malformed, this is an error. If the config does not exist,
258+
// we assume it gets created later.
257259
lkConfig, configExists, err := config.LoadTOMLFile(workingDir, tomlFilename)
258-
if err != nil {
260+
if !errors.Is(err, os.ErrNotExist) {
259261
return nil, err
260262
}
261263
if configExists {
@@ -266,10 +268,6 @@ func createAgentClient(ctx context.Context, cmd *cli.Command) (context.Context,
266268
if projectSubdomainMatch[1] != lkConfig.Project.Subdomain {
267269
return nil, fmt.Errorf("project does not match agent subdomain [%s]", lkConfig.Project.Subdomain)
268270
}
269-
} else {
270-
if !errors.Is(err, os.ErrNotExist) {
271-
return nil, err
272-
}
273271
}
274272

275273
agentsClient, err = lksdk.NewAgentClient(project.URL, project.APIKey, project.APISecret)
@@ -295,7 +293,9 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
295293
return err
296294
}
297295
if !useProject {
298-
return fmt.Errorf("cancelled")
296+
if _, err := selectProject(ctx, cmd); err != nil {
297+
return err
298+
}
299299
}
300300
}
301301

@@ -360,7 +360,7 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
360360
fmt.Printf("Creating agent [%s]\n", util.Accented(lkConfig.Agent.Name))
361361
}
362362

363-
secrets, err := requireSecrets(ctx, cmd, true, false)
363+
secrets, err := requireSecrets(ctx, cmd, false, false)
364364
if err != nil {
365365
return err
366366
}
@@ -721,10 +721,22 @@ func deleteAgent(ctx context.Context, cmd *cli.Command) error {
721721
return nil
722722
}
723723

724-
resp, err := agentsClient.DeleteAgent(ctx, &lkproto.DeleteAgentRequest{
725-
AgentName: agentName,
726-
})
727-
if err != nil {
724+
var res *lkproto.DeleteAgentResponse
725+
var innerErr error
726+
if err := util.Await(
727+
"Deleting agent ["+util.Accented(agentName)+"]",
728+
func() {
729+
if res, innerErr = agentsClient.DeleteAgent(ctx, &lkproto.DeleteAgentRequest{
730+
AgentName: agentName,
731+
}); err != nil {
732+
733+
}
734+
},
735+
); err != nil {
736+
return err
737+
}
738+
739+
if innerErr != nil {
728740
if twerr, ok := err.(twirp.Error); ok {
729741
if twerr.Code() == twirp.PermissionDenied {
730742
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
@@ -733,8 +745,8 @@ func deleteAgent(ctx context.Context, cmd *cli.Command) error {
733745
return err
734746
}
735747

736-
if !resp.Success {
737-
return fmt.Errorf("failed to delete agent %s", resp.Message)
748+
if !res.Success {
749+
return fmt.Errorf("failed to delete agent %s", res.Message)
738750
}
739751

740752
fmt.Printf("Deleted agent [%s]\n", util.Accented(agentName))

cmd/lk/app.go

Lines changed: 60 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import (
2323
"strings"
2424

2525
"github.com/charmbracelet/huh"
26-
"github.com/charmbracelet/huh/spinner"
27-
"github.com/charmbracelet/lipgloss"
2826
"github.com/livekit/livekit-cli/v2/pkg/bootstrap"
2927
"github.com/livekit/livekit-cli/v2/pkg/config"
3028
"github.com/livekit/livekit-cli/v2/pkg/util"
@@ -146,54 +144,61 @@ func requireProject(ctx context.Context, cmd *cli.Command) (context.Context, err
146144
if project != nil {
147145
return ctx, nil
148146
}
147+
if _, err = loadProjectConfig(ctx, cmd); err != nil {
148+
// something is wrong with config file
149+
return nil, err
150+
}
149151
if project, err = loadProjectDetails(cmd); err != nil {
150152
if errors.Is(err, config.ErrInvalidConfig) {
151153
return ctx, err
152154
}
153-
if _, err = loadProjectConfig(ctx, cmd); err != nil {
154-
// something is wrong with config file
155+
// choose from existing credentials or authenticate
156+
return selectProject(ctx, cmd)
157+
}
158+
159+
return nil, err
160+
}
161+
162+
func selectProject(ctx context.Context, cmd *cli.Command) (context.Context, error) {
163+
var err error
164+
165+
if cliConfig != nil && len(cliConfig.Projects) > 0 {
166+
var options []huh.Option[*config.ProjectConfig]
167+
for _, p := range cliConfig.Projects {
168+
options = append(options, huh.NewOption(p.Name+" ["+util.ExtractSubdomain(p.URL)+"]", &p))
169+
}
170+
if err = huh.NewForm(
171+
huh.NewGroup(huh.NewSelect[*config.ProjectConfig]().
172+
Title("Select a project to use for this action").
173+
Description("To use a different project, run `lk cloud auth` to add credentials").
174+
Options(options...).
175+
Value(&project).
176+
WithTheme(util.Theme))).
177+
Run(); err != nil {
155178
return nil, err
156179
}
157-
158-
// choose from existing credentials or authenticate
159-
if len(cliConfig.Projects) > 0 {
160-
var options []huh.Option[*config.ProjectConfig]
161-
for _, p := range cliConfig.Projects {
162-
options = append(options, huh.NewOption(p.Name+" ["+util.ExtractSubdomain(p.URL)+"]", &p))
163-
}
164-
if err = huh.NewForm(
165-
huh.NewGroup(huh.NewSelect[*config.ProjectConfig]().
166-
Title("Select a project to use for this action").
167-
Description("To use a different project, run `lk cloud auth` to add credentials").
168-
Options(options...).
169-
Value(&project).
170-
WithTheme(util.Theme))).
171-
Run(); err != nil {
180+
} else {
181+
shouldAuth := true
182+
if err = huh.NewConfirm().
183+
Title("No local projects found. Authenticate one?").
184+
Inline(true).
185+
Value(&shouldAuth).
186+
WithTheme(util.Theme).
187+
Run(); err != nil {
188+
return nil, err
189+
}
190+
if shouldAuth {
191+
initAuth(ctx, cmd)
192+
if err = tryAuthIfNeeded(ctx, cmd); err != nil {
172193
return nil, err
173194
}
195+
return requireProject(ctx, cmd)
174196
} else {
175-
shouldAuth := true
176-
if err = huh.NewConfirm().
177-
Title("No local projects found. Authenticate one?").
178-
Inline(true).
179-
Value(&shouldAuth).
180-
WithTheme(util.Theme).
181-
Run(); err != nil {
182-
return nil, err
183-
}
184-
if shouldAuth {
185-
initAuth(ctx, cmd)
186-
if err = tryAuthIfNeeded(ctx, cmd); err != nil {
187-
return nil, err
188-
}
189-
return requireProject(ctx, cmd)
190-
} else {
191-
return nil, ErrNoProjectSelected
192-
}
197+
return nil, ErrNoProjectSelected
193198
}
194199
}
195200

196-
return nil, err
201+
return ctx, nil
197202
}
198203

199204
func listTemplates(ctx context.Context, cmd *cli.Command) error {
@@ -376,13 +381,12 @@ func cloneTemplate(_ context.Context, cmd *cli.Command, url, appName string) err
376381
tempName, relocate, cleanup := util.UseTempPath(appName)
377382
defer cleanup()
378383

379-
if err := spinner.New().
380-
Title("Cloning template from " + url).
381-
Action(func() {
384+
if err := util.Await(
385+
"Cloning template from "+url,
386+
func() {
382387
stdout, stderr, cmdErr = bootstrap.CloneTemplate(url, tempName)
383-
}).
384-
Style(util.Theme.Focused.Title).
385-
Run(); err != nil {
388+
},
389+
); err != nil {
386390
return err
387391
}
388392

@@ -484,13 +488,10 @@ func doPostCreate(ctx context.Context, _ *cli.Command, rootPath string, verbose
484488
}
485489

486490
var cmdErr error
487-
if err := spinner.New().
488-
Title("Cleaning up...").
489-
TitleStyle(lipgloss.NewStyle()).
490-
Style(lipgloss.NewStyle()).
491-
Action(func() { cmdErr = task() }).
492-
Accessible(true).
493-
Run(); err != nil {
491+
if err := util.Await(
492+
"Cleaning up...",
493+
func() { cmdErr = task() },
494+
); err != nil {
494495
return err
495496
}
496497
return cmdErr
@@ -508,12 +509,10 @@ func doInstall(ctx context.Context, task bootstrap.KnownTask, rootPath string, v
508509
}
509510

510511
var cmdErr error
511-
if err := spinner.New().
512-
Title("Installing...").
513-
Action(func() { cmdErr = install() }).
514-
Style(util.Theme.Focused.Title).
515-
Accessible(true).
516-
Run(); err != nil {
512+
if err := util.Await(
513+
"Installing...",
514+
func() { cmdErr = install() },
515+
); err != nil {
517516
return err
518517
}
519518
return cmdErr
@@ -550,12 +549,10 @@ func runTask(ctx context.Context, cmd *cli.Command) error {
550549
return err
551550
}
552551
var cmdErr error
553-
if err := spinner.New().
554-
Title("Running task " + taskName + "...").
555-
Action(func() { cmdErr = task() }).
556-
Style(util.Theme.Focused.Title).
557-
Accessible(verbose).
558-
Run(); err != nil {
552+
if err := util.Await(
553+
"Running task "+taskName+"...",
554+
func() { cmdErr = task() },
555+
); err != nil {
559556
return err
560557
}
561558
return cmdErr

cmd/lk/cloud.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"time"
2525

2626
"github.com/charmbracelet/huh"
27-
"github.com/charmbracelet/huh/spinner"
2827
"github.com/pkg/browser"
2928
"github.com/urfave/cli/v3"
3029

@@ -302,13 +301,12 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error {
302301

303302
var ak *ClaimAccessKeyResponse
304303
var pollErr error
305-
if err := spinner.New().
306-
Title("Awaiting confirmation...").
307-
Action(func() {
304+
if err := util.Await(
305+
"Awaiting confirmation...",
306+
func() {
308307
ak, pollErr = pollClaim(ctx, cmd)
309-
}).
310-
Style(util.Theme.Focused.Title).
311-
Run(); err != nil {
308+
},
309+
); err != nil {
312310
return err
313311
}
314312
if pollErr != nil {

cmd/lk/utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func loadProjectDetails(c *cli.Command, opts ...loadOption) (*config.ProjectConf
192192
if err != nil {
193193
return nil, err
194194
}
195-
fmt.Println("Using project [" + util.Theme.Focused.Title.Render(c.String("project")) + "]")
195+
fmt.Println("Using project [" + util.Accented(c.String("project")) + "]")
196196
logDetails(c, pc)
197197
return pc, nil
198198
}
@@ -206,7 +206,7 @@ func loadProjectDetails(c *cli.Command, opts ...loadOption) (*config.ProjectConf
206206
if err != nil {
207207
return nil, err
208208
}
209-
fmt.Println("Using project [" + util.Theme.Focused.Title.Render(pc.Name) + "]")
209+
fmt.Println("Using project [" + util.Accented(pc.Name) + "]")
210210
logDetails(c, pc)
211211
return pc, nil
212212
}

pkg/util/action.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2025 LiveKit, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package util
16+
17+
import (
18+
"github.com/charmbracelet/huh/spinner"
19+
)
20+
21+
// Call an action and show a spinner while waiting for it to finish.
22+
func Await(title string, action func()) error {
23+
return spinner.New().
24+
Title(title).
25+
Action(action).
26+
Style(Theme.Focused.Title).
27+
Run()
28+
}

0 commit comments

Comments
 (0)