diff --git a/go.mod b/go.mod index ef3b25e6..a7526c07 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/mdp/qrterminal/v3 v3.2.1 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect @@ -59,6 +60,7 @@ require ( golang.org/x/crypto v0.36.0 // indirect golang.org/x/term v0.30.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + rsc.io/qr v0.2.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 8fe187fd..10bba001 100644 --- a/go.sum +++ b/go.sum @@ -166,6 +166,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= +github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -374,3 +376,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/internal/errsystem/console.go b/internal/errsystem/console.go index 72437625..c9c07846 100644 --- a/internal/errsystem/console.go +++ b/internal/errsystem/console.go @@ -1,6 +1,7 @@ package errsystem import ( + "bytes" "encoding/json" "errors" "fmt" @@ -19,6 +20,7 @@ import ( "github.com/agentuity/go-common/tui" "github.com/charmbracelet/lipgloss" "github.com/mattn/go-isatty" + "github.com/mdp/qrterminal/v3" ) var Version string = "dev" @@ -94,6 +96,19 @@ func (e *errSystem) sendReport(filename string) { } } +// generateQRCode creates a QR code for the given URL and returns it as a string +func generateQRCode(url string) string { + var buf bytes.Buffer + config := qrterminal.Config{ + Level: qrterminal.M, + Writer: &buf, + HalfBlocks: true, // Use half blocks to make QR code more square and compact + QuietZone: 1, + } + qrterminal.GenerateWithConfig(url, config) + return buf.String() +} + // ShowErrorAndExit shows an error message and exits the program. // If the program is running in a terminal, it will wait for a key press // and then upload the error report to the Agentuity team. @@ -107,6 +122,14 @@ func (e *errSystem) ShowErrorAndExit() { } else { body.WriteString(e.code.Message + "\n\n") } + + // Add community help message and QR code when running in terminal + qrCode := generateQRCode(discordURL) + body.WriteString(qrCode) + body.WriteString("\n" + tui.Bold("Get help from the Agentuity community at ")) + body.WriteString(tui.Link(discordURL) + " ") + body.WriteString(tui.Muted("(or scan the QR code)") + "\n\n") + var detail []string if e.err != nil { var apiError *util.APIError @@ -138,6 +161,7 @@ func (e *errSystem) ShowErrorAndExit() { for _, d := range detail { body.WriteString(tui.Muted(d) + "\n") } + if !tui.HasTTY { fmt.Println(body.String()) for k, v := range e.attributes { diff --git a/internal/errsystem/console_test.go b/internal/errsystem/console_test.go new file mode 100644 index 00000000..a0cfa0ce --- /dev/null +++ b/internal/errsystem/console_test.go @@ -0,0 +1,70 @@ +package errsystem + +import ( + "errors" + "os" + "testing" +) + +func TestShowErrorAndExitWithQRCode(t *testing.T) { + // This test demonstrates the QR code functionality + // It will show the error banner with QR code, but we need to prevent actual exit + // We'll use a deferred recover to catch the os.Exit call + + // Skip in CI or non-interactive environments + if os.Getenv("CI") != "" { + t.Skip("Skipping interactive test in CI environment") + } + + t.Log("This test will show an error with QR code. You should see the Discord QR code in the output.") + + // Create a test error + testErr := errors.New("This is a test error to demonstrate the QR code feature") + errSys := New(ErrInvalidConfiguration, testErr, + WithContextMessage("Testing QR code display in unit test"), + WithUserMessage("This test shows the QR code feature working properly")) + + // Note: In a real scenario, this would call os.Exit(1) + // For testing purposes, you can comment out the next line to see the QR code + // and manually verify it works, then uncomment it for automated testing + + t.Log("Calling ShowErrorAndExit - this will show the QR code and then exit") + t.Log("The QR code should point to: https://discord.gg/agentuity") + + // This will actually exit the test, but that's okay for a manual verification test + errSys.ShowErrorAndExit() +} + +func TestGenerateQRCode(t *testing.T) { + // Test that QR code generation works + qrCode := generateQRCode("https://discord.gg/agentuity") + + // Basic validation that something was generated + if len(qrCode) == 0 { + t.Error("QR code generation returned empty string") + } + + // Check that it contains expected QR code characters + if !contains(qrCode, "█") { + t.Error("QR code should contain block characters") + } + + t.Logf("Generated QR code length: %d characters", len(qrCode)) +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && + (s == substr || + (len(s) > len(substr) && (s[:len(substr)] == substr || + s[len(s)-len(substr):] == substr || + containsInner(s, substr)))) +} + +func containsInner(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +}