package term import ( "encoding/json" "fmt" "os" "strings" shared "plandex-shared" "github.com/fatih/color" ) var openUnauthenticatedCloudURL func(msg, path string) var openAuthenticatedURL func(msg, path string) var convertTrial func() func SetOpenUnauthenticatedCloudURLFn(fn func(msg, path string)) { openUnauthenticatedCloudURL = fn } func SetOpenAuthenticatedURLFn(fn func(msg, path string)) { openAuthenticatedURL = fn } func SetConvertTrialFn(fn func()) { convertTrial = fn } func OutputSimpleError(msg string, args ...interface{}) { msg = fmt.Sprintf(msg, args...) fmt.Fprintln(os.Stderr, color.New(ColorHiRed, color.Bold).Sprint("🚨 "+shared.Capitalize(msg))) } func OutputErrorAndExit(msg string, args ...interface{}) { StopSpinner() msg = fmt.Sprintf(msg, args...) msg = strings.ReplaceAll(msg, "status code:", "status code") msg = strings.ReplaceAll(msg, ", body:", ":") displayMsg := "" errorParts := strings.Split(msg, ": ") addedErrors := map[string]bool{} if len(errorParts) > 1 { var lastPart string i := 0 for idx, part := range errorParts { // don't repeat the same error message if _, ok := addedErrors[strings.ToLower(part)]; ok { continue } tail := strings.Join(errorParts[idx:], ": ") if maybeJSON(tail) { prettyJSON := prettyJSON(tail) indent := strings.Repeat(" ", i) // prepend indent to **each** line in the pretty JSON indentedJSON := strings.ReplaceAll(prettyJSON, "\n", "\n"+indent+" ") // now write the block displayMsg += "\n" + indent + "→ " + indentedJSON break } if len(lastPart) < 10 && i > 0 { lastPart = lastPart + ": " + part displayMsg += ": " + part addedErrors[strings.ToLower(lastPart)] = true addedErrors[strings.ToLower(part)] = true continue } if i != 0 { displayMsg += "\n" } // indent the error message for n := 0; n < i; n++ { displayMsg += " " } if i > 0 { displayMsg += "→ " } s := shared.Capitalize(part) if i == 0 { s = color.New(ColorHiRed, color.Bold).Sprint("🚨 " + s) } displayMsg += s addedErrors[strings.ToLower(part)] = true lastPart = part i++ } } else { displayMsg = color.New(ColorHiRed, color.Bold).Sprint("🚨 " + msg) } fmt.Fprintln(os.Stderr, color.New(ColorHiRed, color.Bold).Sprint(displayMsg)) os.Exit(1) } func OutputUnformattedErrorAndExit(msg string) { StopSpinner() fmt.Fprintln(os.Stderr, msg) os.Exit(1) } func OutputNoCurrentPlanErrorAndExit() { fmt.Println("šŸ¤·ā€ā™‚ļø No current plan") fmt.Println() PrintCmds("", "new", "cd") os.Exit(1) } func HandleApiError(apiError *shared.ApiError) { if apiError.Type == shared.ApiErrorTypeCloudSubscriptionPaused { if apiError.BillingError.HasBillingPermission { StopSpinner() fmt.Println("Your org's Plandex Cloud subscription is paused.") res, err := ConfirmYesNo("Go to billing settings?") if err != nil { OutputErrorAndExit("error getting confirmation") } if res { openAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing") os.Exit(0) } else { os.Exit(0) } } else { OutputErrorAndExit("Your org's subscription is paused. Please contact an org owner to continue.") } } if apiError.Type == shared.ApiErrorTypeCloudSubscriptionOverdue { if apiError.BillingError.HasBillingPermission { StopSpinner() OutputSimpleError("Your org's Plandex Cloud subscription is overdue.") res, err := ConfirmYesNo("Go to billing settings?") if err != nil { OutputErrorAndExit("error getting confirmation") } if res { openAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing") os.Exit(0) } else { os.Exit(0) } } else { OutputErrorAndExit("Your org's subscription is overdue. Please contact an org owner to continue.") } } if apiError.Type == shared.ApiErrorTypeCloudMonthlyMaxReached { if apiError.BillingError.HasBillingPermission { StopSpinner() OutputSimpleError("Your org has reached its monthly limit for Plandex Cloud.") res, err := ConfirmYesNo("Go to billing settings?") if err != nil { OutputErrorAndExit("error getting confirmation") } if res { openAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing") os.Exit(0) } else { os.Exit(0) } } else { OutputErrorAndExit("Your org has reached its monthly limit for Plandex Cloud.") } } if apiError.Type == shared.ApiErrorTypeCloudInsufficientCredits { if apiError.BillingError.HasBillingPermission { StopSpinner() OutputSimpleError("Insufficient credits") res, err := ConfirmYesNo("Go to billing settings?") if err != nil { OutputErrorAndExit("error getting confirmation") } if res { openAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing") os.Exit(0) } else { os.Exit(0) } } else { OutputErrorAndExit("Insufficient credits") } } if apiError.Type == shared.ApiErrorTypeTrialMessagesExceeded { StopSpinner() fmt.Fprintf(os.Stderr, "\n🚨 You've reached the Plandex Cloud trial limit of %d messages per plan\n", apiError.TrialMessagesExceededError.MaxReplies) res, err := ConfirmYesNo("Upgrade now?") if err != nil { OutputErrorAndExit("Error prompting upgrade trial: %v", err) } if res { convertTrial() PrintCmds("", "continue") os.Exit(0) } } StopSpinner() OutputErrorAndExit(apiError.Msg) } func maybeJSON(s string) bool { s = strings.TrimSpace(s) if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") { return true } if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { return true } return false } func prettyJSON(s string) string { var v any if err := json.Unmarshal([]byte(s), &v); err != nil { return s // not JSON } out, _ := json.MarshalIndent(v, "", " ") return string(out) }