feat: add interactive wizard, contextual errors, and code improvements

- Add interactive configuration wizard with auto-detection and multi-format export
- Implement contextual error system with 14 error codes and actionable suggestions
- Add centralized progress indicators with consistent theming
- Fix all cyclomatic complexity issues (8 functions refactored)
- Eliminate code duplication with centralized utilities and error handling
- Add comprehensive test coverage for all new components
- Update TODO.md with completed tasks and accurate completion dates
This commit is contained in:
2025-08-04 23:33:28 +03:00
parent 7a8dc8d2ba
commit f9823eef3e
17 changed files with 4104 additions and 82 deletions

View File

@@ -3,8 +3,11 @@ package internal
import (
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/ivuorinen/gh-action-readme/internal/errors"
)
// ColoredOutput provides methods for colored terminal output.
@@ -107,3 +110,139 @@ func (co *ColoredOutput) Printf(format string, args ...any) {
func (co *ColoredOutput) Fprintf(w *os.File, format string, args ...any) {
_, _ = fmt.Fprintf(w, format, args...)
}
// ErrorWithSuggestions prints a ContextualError with suggestions and help.
func (co *ColoredOutput) ErrorWithSuggestions(err *errors.ContextualError) {
if err == nil {
return
}
// Print main error message
if co.NoColor {
fmt.Fprintf(os.Stderr, "❌ %s\n", err.Error())
} else {
color.Red("❌ %s", err.Error())
}
}
// ErrorWithContext creates and prints a contextual error with suggestions.
func (co *ColoredOutput) ErrorWithContext(
code errors.ErrorCode,
message string,
context map[string]string,
) {
suggestions := errors.GetSuggestions(code, context)
helpURL := errors.GetHelpURL(code)
contextualErr := errors.New(code, message).
WithSuggestions(suggestions...).
WithHelpURL(helpURL)
if len(context) > 0 {
contextualErr = contextualErr.WithDetails(context)
}
co.ErrorWithSuggestions(contextualErr)
}
// ErrorWithSimpleFix prints an error with a simple suggestion.
func (co *ColoredOutput) ErrorWithSimpleFix(message, suggestion string) {
contextualErr := errors.New(errors.ErrCodeUnknown, message).
WithSuggestions(suggestion)
co.ErrorWithSuggestions(contextualErr)
}
// FormatContextualError formats a ContextualError for display.
func (co *ColoredOutput) FormatContextualError(err *errors.ContextualError) string {
if err == nil {
return ""
}
var parts []string
// Add main error message
parts = append(parts, co.formatMainError(err))
// Add details section
if len(err.Details) > 0 {
parts = append(parts, co.formatDetailsSection(err.Details)...)
}
// Add suggestions section
if len(err.Suggestions) > 0 {
parts = append(parts, co.formatSuggestionsSection(err.Suggestions)...)
}
// Add help URL section
if err.HelpURL != "" {
parts = append(parts, co.formatHelpURLSection(err.HelpURL))
}
return strings.Join(parts, "\n")
}
// formatMainError formats the main error message with code.
func (co *ColoredOutput) formatMainError(err *errors.ContextualError) string {
mainMsg := fmt.Sprintf("%s [%s]", err.Error(), err.Code)
if co.NoColor {
return "❌ " + mainMsg
}
return color.RedString("❌ ") + mainMsg
}
// formatDetailsSection formats the details section.
func (co *ColoredOutput) formatDetailsSection(details map[string]string) []string {
var parts []string
if co.NoColor {
parts = append(parts, "\nDetails:")
} else {
parts = append(parts, color.New(color.Bold).Sprint("\nDetails:"))
}
for key, value := range details {
if co.NoColor {
parts = append(parts, fmt.Sprintf(" %s: %s", key, value))
} else {
parts = append(parts, fmt.Sprintf(" %s: %s",
color.CyanString(key),
color.WhiteString(value)))
}
}
return parts
}
// formatSuggestionsSection formats the suggestions section.
func (co *ColoredOutput) formatSuggestionsSection(suggestions []string) []string {
var parts []string
if co.NoColor {
parts = append(parts, "\nSuggestions:")
} else {
parts = append(parts, color.New(color.Bold).Sprint("\nSuggestions:"))
}
for _, suggestion := range suggestions {
if co.NoColor {
parts = append(parts, fmt.Sprintf(" • %s", suggestion))
} else {
parts = append(parts, fmt.Sprintf(" %s %s",
color.YellowString("•"),
color.WhiteString(suggestion)))
}
}
return parts
}
// formatHelpURLSection formats the help URL section.
func (co *ColoredOutput) formatHelpURLSection(helpURL string) string {
if co.NoColor {
return fmt.Sprintf("\nFor more help: %s", helpURL)
}
return fmt.Sprintf("\n%s: %s",
color.New(color.Bold).Sprint("For more help"),
color.BlueString(helpURL))
}