refactor(init): wizard UX improvements

Amolith created

- Token validation uses errTokenRequired instead of errKeyRequired
- Access-token step offers 'Skip for now' on errors instead of
  discarding config
- Ctrl+C on save prompt shows clearer message
- Color summary shows 'auto' when config value is empty
- Extract printConfigSummary helper to reduce function length
- Add colorAuto constant to avoid magic string duplication

Assisted-by: Claude Opus 4.5 via Amp

Change summary

cmd/init/apikey.go |  6 ++++--
cmd/init/init.go   | 28 +++++++++++++++++++---------
cmd/init/steps.go  | 28 +++++++++++++++++++++++++---
cmd/init/ui.go     |  6 ++++--
4 files changed, 52 insertions(+), 16 deletions(-)

Detailed changes

cmd/init/apikey.go 🔗

@@ -20,6 +20,8 @@ import (
 	"git.secluded.site/lune/internal/ui"
 )
 
+const tokenValidationTimeout = 10 * time.Second
+
 func configureAccessToken(cmd *cobra.Command) error {
 	out := cmd.OutOrStdout()
 
@@ -110,7 +112,7 @@ func promptForToken(out io.Writer) error {
 			Value(&token).
 			Validate(func(s string) error {
 				if s == "" {
-					return errKeyRequired
+					return errTokenRequired
 				}
 
 				return nil
@@ -167,7 +169,7 @@ func validateWithSpinner(token string) error {
 func validateTokenWithPing(token string) error {
 	c := lunatask.NewClient(token, lunatask.UserAgent("lune/init"))
 
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(context.Background(), tokenValidationTimeout)
 	defer cancel()
 
 	_, err := c.Ping(ctx)

cmd/init/init.go 🔗

@@ -8,6 +8,7 @@ package init
 import (
 	"errors"
 	"fmt"
+	"io"
 
 	"github.com/charmbracelet/huh"
 	"github.com/spf13/cobra"
@@ -177,18 +178,12 @@ func runReconfigure(cmd *cobra.Command, cfg *config.Config) error {
 	}
 }
 
-func saveWithSummary(cmd *cobra.Command, cfg *config.Config) error {
-	path, err := config.Path()
-	if err != nil {
-		return err
-	}
-
+func printConfigSummary(out io.Writer, cfg *config.Config) {
 	goalCount := 0
 	for _, area := range cfg.Areas {
 		goalCount += len(area.Goals)
 	}
 
-	out := cmd.OutOrStdout()
 	fmt.Fprintln(out)
 	fmt.Fprintln(out, ui.Bold.Render("Configuration summary:"))
 	fmt.Fprintf(out, "  Areas:     %d (%d goals)\n", len(cfg.Areas), goalCount)
@@ -203,8 +198,23 @@ func saveWithSummary(cmd *cobra.Command, cfg *config.Config) error {
 		fmt.Fprintf(out, "  Default notebook: %s\n", cfg.Defaults.Notebook)
 	}
 
-	fmt.Fprintf(out, "  Color: %s\n", cfg.UI.Color)
+	color := cfg.UI.Color
+	if color == "" {
+		color = colorAuto
+	}
+
+	fmt.Fprintf(out, "  Color: %s\n", color)
 	fmt.Fprintln(out)
+}
+
+func saveWithSummary(cmd *cobra.Command, cfg *config.Config) error {
+	path, err := config.Path()
+	if err != nil {
+		return err
+	}
+
+	out := cmd.OutOrStdout()
+	printConfigSummary(out, cfg)
 
 	var save bool
 
@@ -216,7 +226,7 @@ func saveWithSummary(cmd *cobra.Command, cfg *config.Config) error {
 		Run()
 	if err != nil {
 		if errors.Is(err, huh.ErrUserAborted) {
-			fmt.Fprintln(out, ui.Warning.Render("Changes discarded."))
+			fmt.Fprintln(out, ui.Warning.Render("Setup cancelled; configuration was not saved."))
 
 			return errQuit
 		}

cmd/init/steps.go 🔗

@@ -6,25 +6,27 @@ package init
 
 import (
 	"errors"
+	"fmt"
 
 	"github.com/charmbracelet/huh"
 	"github.com/spf13/cobra"
 
 	"git.secluded.site/lune/internal/config"
+	"git.secluded.site/lune/internal/ui"
 )
 
 // runUIPrefsStep runs the UI preferences configuration step.
 func runUIPrefsStep(cfg *config.Config) wizardNav {
 	color := cfg.UI.Color
 	if color == "" {
-		color = "auto"
+		color = colorAuto
 	}
 
 	err := huh.NewSelect[string]().
 		Title("Color output").
 		Description("When should lune use colored output?").
 		Options(
-			huh.NewOption("Auto (detect terminal capability)", "auto"),
+			huh.NewOption("Auto (detect terminal capability)", colorAuto),
 			huh.NewOption("Always", "always"),
 			huh.NewOption("Never", "never"),
 		).
@@ -89,7 +91,27 @@ func runAccessTokenStep(cmd *cobra.Command) wizardNav {
 	}
 
 	if err != nil {
-		return navQuit
+		out := cmd.OutOrStdout()
+		fmt.Fprintln(out, ui.Error.Render("Token configuration failed: "+err.Error()))
+
+		var skip bool
+
+		skipErr := huh.NewConfirm().
+			Title("Skip token configuration?").
+			Description("You can configure it later with 'lune init'.").
+			Affirmative("Skip for now").
+			Negative("Go back").
+			Value(&skip).
+			Run()
+		if skipErr != nil {
+			return navQuit
+		}
+
+		if skip {
+			return navNext
+		}
+
+		return navBack
 	}
 
 	return navNext

cmd/init/ui.go 🔗

@@ -28,11 +28,13 @@ const (
 	actionEdit   = "edit"
 	actionDelete = "delete"
 	actionGoals  = "goals"
+	colorAuto    = "auto"
 )
 
 var (
 	errNameRequired  = errors.New("name is required")
 	errKeyRequired   = errors.New("key is required")
+	errTokenRequired = errors.New("access token is required")
 	errKeyFormat     = errors.New("key must be lowercase letters, numbers, and hyphens (e.g. 'work' or 'q1-goals')")
 	errKeyDuplicate  = errors.New("this key is already in use")
 	errRefRequired   = errors.New("reference is required")
@@ -44,14 +46,14 @@ var (
 func configureUIPrefs(cfg *config.Config) error {
 	color := cfg.UI.Color
 	if color == "" {
-		color = "auto"
+		color = colorAuto
 	}
 
 	err := huh.NewSelect[string]().
 		Title("Color output").
 		Description("When should lune use colored output?").
 		Options(
-			huh.NewOption("Auto (detect terminal capability)", "auto"),
+			huh.NewOption("Auto (detect terminal capability)", colorAuto),
 			huh.NewOption("Always", "always"),
 			huh.NewOption("Never", "never"),
 		).