diff --git a/internal/appcore/auth_recovery.go b/internal/appcore/auth_recovery.go index 13cea6e..8f9bead 100644 --- a/internal/appcore/auth_recovery.go +++ b/internal/appcore/auth_recovery.go @@ -19,6 +19,9 @@ import ( "github.com/HexmosTech/git-lrc/storage" ) +// ErrAuthHandled indicates an auth failure already handled visually to suppress redundant logs. +var ErrAuthHandled = errors.New("authentication failure already handled") + const liveReviewAPIKeyInvalidCode = "LIVE_REVIEW_API_KEY_INVALID" type createAPIKeyRuntimeRequest struct { @@ -100,7 +103,7 @@ func submitReviewWithRecovery(config Config, base64Diff, repoName string, verbos recoveredConfig, recErr := recoverAPIKeyAndTokens(config, "submit") if recErr != nil { - return reviewmodel.DiffReviewCreateResponse{}, config, fmt.Errorf("auto-recovery failed after %s: %w", liveReviewAPIKeyInvalidCode, recErr) + return reviewmodel.DiffReviewCreateResponse{}, config, ErrAuthHandled } fmt.Println("Retrying review submission with refreshed credentials...") @@ -122,7 +125,7 @@ func pollReviewWithRecovery(config Config, reviewID string, pollInterval, timeou recoveredConfig, recErr := recoverAPIKeyAndTokens(config, "poll") if recErr != nil { - return nil, config, fmt.Errorf("auto-recovery failed after %s: %w", liveReviewAPIKeyInvalidCode, recErr) + return nil, config, ErrAuthHandled } fmt.Println("Retrying review polling with refreshed credentials...") @@ -133,6 +136,28 @@ func pollReviewWithRecovery(config Config, reviewID string, pollInterval, timeou return retryResult, recoveredConfig, nil } +func highlightCommand(cmd string) string { + return "\033[36m" + cmd + "\033[0m" +} + +func printManualReauthInstructions() { + const ( + cReset = "\033[0m" + cBold = "\033[1m" + cYellow = "\033[33m" + cDim = "\033[2m" + ) + + fmt.Println() + fmt.Printf(" %s%sšŸ” MANUAL RE-AUTHENTICATION REQUIRED%s\n", cBold, cYellow, cReset) + fmt.Printf(" %s─────────────────────────────────────────────────────%s\n", cDim, cReset) + fmt.Println() + fmt.Printf(" 1. Open the LRC UI by running: %s\n", highlightCommand("lrc ui")) + fmt.Printf(" 2. Click the %sRe-authenticate%s button\n", cBold, cReset) + fmt.Printf(" 3. Once authenticated, run the command again to continue\n") + fmt.Println() +} + func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) { started := time.Now() diag := authRecoveryDiagnostic{ @@ -158,6 +183,7 @@ func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) { diag.FailureReason = "missing org_id in config" reportDiagnosticWriteError(persistAuthRecoveryDiagnostic(&diag, time.Since(started))) fmt.Println("Automatic recovery unavailable: missing org_id in ~/.lrc.toml.") + printManualReauthInstructions() return config, fmt.Errorf("missing org_id in config") } @@ -186,6 +212,7 @@ func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) { if createStatus != http.StatusUnauthorized || strings.TrimSpace(config.RefreshToken) == "" { diag.FailureReason = fmt.Sprintf("create API key failed before refresh: status=%d", createStatus) reportDiagnosticWriteError(persistAuthRecoveryDiagnostic(&diag, time.Since(started))) + printManualReauthInstructions() return config, fmt.Errorf("create API key failed: %w body=%s", err, strings.TrimSpace(createBody)) } @@ -195,6 +222,7 @@ func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) { if refreshErr != nil { diag.FailureReason = fmt.Sprintf("refresh token failed: status=%d", refreshStatus) reportDiagnosticWriteError(persistAuthRecoveryDiagnostic(&diag, time.Since(started))) + printManualReauthInstructions() return config, fmt.Errorf("failed to refresh session: %w body=%s", refreshErr, strings.TrimSpace(refreshBody)) } @@ -218,9 +246,10 @@ func recoverAPIKeyAndTokens(config Config, phase string) (Config, error) { newKey, createStatus, createBody, err = createAPIKeyWithJWT(updated.APIURL, updated.OrgID, updated.JWT) if err != nil { - diag.FailureReason = fmt.Sprintf("create API key after refresh failed: status=%d", createStatus) + diag.FailureReason = fmt.Sprintf("create API key failed after refresh: status=%d", createStatus) reportDiagnosticWriteError(persistAuthRecoveryDiagnostic(&diag, time.Since(started))) - return config, fmt.Errorf("failed to create API key after refresh: %w body=%s", err, strings.TrimSpace(createBody)) + printManualReauthInstructions() + return config, fmt.Errorf("failed to create API key after session refresh: %w body=%s", err, strings.TrimSpace(createBody)) } updated.APIKey = newKey diff --git a/main.go b/main.go index cfa433d..c9539a9 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,8 @@ package main import ( - "log" + "errors" + "fmt" "os" cmdapp "github.com/HexmosTech/git-lrc/cmd" @@ -72,7 +73,11 @@ func main() { }) if err := app.Run(os.Args); err != nil { - log.Fatal(err) + if errors.Is(err, appcore.ErrAuthHandled) { + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) } }