feat(init): auto-prompt for token in reconfigure

Amolith created

When running `lune init` with an existing config but no keyring token
(e.g., after copying config to a new device), immediately prompt for the
access token before showing the configuration menu.

Also validates existing tokens and offers to replace them if ping fails.

Assisted-by: Claude Sonnet 4 via Crush

Change summary

cmd/init/apikey.go | 62 ++++++++++++++++++++++++++++++++++++++++++++---
cmd/init/init.go   |  4 +++
2 files changed, 62 insertions(+), 4 deletions(-)

Detailed changes

cmd/init/apikey.go 🔗

@@ -22,15 +22,20 @@ import (
 
 const tokenValidationTimeout = 10 * time.Second
 
+// handleKeyringError prints keyring error messages and returns a wrapped error.
+func handleKeyringError(out io.Writer, err error) error {
+	fmt.Fprintln(out, ui.Error.Render("Failed to access system keyring: "+err.Error()))
+	fmt.Fprintln(out, "Please resolve the keyring issue and try again.")
+
+	return fmt.Errorf("keyring access failed: %w", err)
+}
+
 func configureAccessToken(cmd *cobra.Command) error {
 	out := cmd.OutOrStdout()
 
 	hasToken, keyringErr := client.HasKeyringToken()
 	if keyringErr != nil {
-		fmt.Fprintln(out, ui.Error.Render("Failed to access system keyring: "+keyringErr.Error()))
-		fmt.Fprintln(out, "Please resolve the keyring issue and try again.")
-
-		return fmt.Errorf("keyring access failed: %w", keyringErr)
+		return handleKeyringError(out, keyringErr)
 	}
 
 	if hasToken {
@@ -47,6 +52,55 @@ func configureAccessToken(cmd *cobra.Command) error {
 	return promptForToken(out)
 }
 
+// ensureAccessToken checks for a token in the keyring and prompts for one if
+// missing or invalid. Used at the start of reconfigure mode so users copying
+// config to a new device are immediately prompted for their token.
+func ensureAccessToken(cmd *cobra.Command) error {
+	out := cmd.OutOrStdout()
+
+	hasToken, keyringErr := client.HasKeyringToken()
+	if keyringErr != nil {
+		return handleKeyringError(out, keyringErr)
+	}
+
+	if !hasToken {
+		fmt.Fprintln(out, ui.Warning.Render("No access token found in system keyring."))
+
+		return promptForToken(out)
+	}
+
+	existingToken, err := client.GetToken()
+	if err != nil {
+		return fmt.Errorf("reading access token from keyring: %w", err)
+	}
+
+	if err := validateWithSpinner(existingToken); err != nil {
+		fmt.Fprintln(out, ui.Warning.Render("Existing access token failed validation: "+err.Error()))
+
+		var replace bool
+
+		confirmErr := huh.NewConfirm().
+			Title("Would you like to provide a new access token?").
+			Affirmative("Yes").
+			Negative("No").
+			Value(&replace).
+			Run()
+		if confirmErr != nil {
+			if errors.Is(confirmErr, huh.ErrUserAborted) {
+				return errQuit
+			}
+
+			return confirmErr
+		}
+
+		if replace {
+			return promptForToken(out)
+		}
+	}
+
+	return nil
+}
+
 func handleExistingToken(out io.Writer) (bool, error) {
 	existingToken, err := client.GetToken()
 	if err != nil {

cmd/init/init.go 🔗

@@ -131,6 +131,10 @@ func runReconfigure(cmd *cobra.Command, cfg *config.Config) error {
 	fmt.Fprintln(cmd.OutOrStdout(), ui.Bold.Render("lune configuration"))
 	fmt.Fprintln(cmd.OutOrStdout())
 
+	if err := ensureAccessToken(cmd); err != nil {
+		return err
+	}
+
 	handlers := map[string]func() error{
 		"areas":     func() error { return manageAreas(cfg) },
 		"notebooks": func() error { return manageNotebooks(cfg) },