Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions cmd/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"time"

"github.com/jongio/grut/internal/crashlog"
"github.com/jongio/grut/internal/panels"
Expand All @@ -15,8 +17,22 @@ const (
githubRepo = "jongio/grut"
issueURL = "https://github.com/" + githubRepo + "/issues/new"
maxURLLen = 4000

reportJSONFlag = "json"
)

type crashReportSummary struct {
ID string `json:"id"`
Timestamp string `json:"timestamp"`
Version string `json:"version"`
GoVersion string `json:"go_version"`
OS string `json:"os"`
Arch string `json:"arch"`
Terminal string `json:"terminal"`
PanicValue string `json:"panic_value"`
Context string `json:"context"`
}

// newReportCmd creates the report command for viewing crash reports
// and filing GitHub issues.
func newReportCmd() *cobra.Command {
Expand All @@ -36,6 +52,7 @@ all stored reports, --show to inspect one, or --clear to remove them all.`,
cmd.Flags().String("show", "", "Show a specific crash report by ID")
cmd.Flags().Bool("clear", false, "Clear all crash reports")
cmd.Flags().Bool("no-browser", false, "Print the GitHub issue URL instead of opening the browser")
cmd.Flags().Bool(reportJSONFlag, false, "Print list output as JSON")

return cmd
}
Expand All @@ -47,15 +64,20 @@ func runReport(cmd *cobra.Command, _ []string) error {
}

list, _ := cmd.Flags().GetBool("list")
jsonOutput, _ := cmd.Flags().GetBool(reportJSONFlag)
if list {
return runReportList()
return runReportList(cmd, jsonOutput)
}

showID, _ := cmd.Flags().GetString("show")
if showID != "" {
return runReportShow(showID)
}

if jsonOutput {
return fmt.Errorf("--json can only be used with --list")
}

// Default / --latest: open issue for most recent crash.
noBrowser, _ := cmd.Flags().GetBool("no-browser")
return runReportLatest(noBrowser)
Expand All @@ -70,30 +92,53 @@ func runReportClear() error {
return nil
}

func runReportList() error {
func runReportList(cmd *cobra.Command, jsonOutput bool) error {
reports, err := crashlog.List()
if err != nil {
return fmt.Errorf("listing crash reports: %w", err)
}
if jsonOutput {
return writeCrashReportListJSON(cmd.OutOrStdout(), reports)
}

w := cmd.OutOrStdout()
if len(reports) == 0 {
fmt.Println("No crash reports found.")
fmt.Fprintln(w, "No crash reports found.")
return nil
}

fmt.Printf("Crash Reports (%d found)\n\n", len(reports))
fmt.Printf(" %-12s %-22s %-42s %s\n", "ID", "Timestamp", "Panic", "Context")
fmt.Fprintf(w, "Crash Reports (%d found)\n\n", len(reports))
fmt.Fprintf(w, " %-12s %-22s %-42s %s\n", "ID", "Timestamp", "Panic", "Context")
for _, r := range reports {
id := r.ID
if runes := []rune(id); len(runes) > 8 {
id = string(runes[:8]) + ".."
}
ts := r.Timestamp.Format("2006-01-02 15:04:05")
pv := truncate(r.PanicValue, 50)
fmt.Printf(" %-12s %-22s %-42s %s\n", id, ts, pv, r.Context)
fmt.Fprintf(w, " %-12s %-22s %-42s %s\n", id, ts, pv, r.Context)
}
return nil
}

func writeCrashReportListJSON(w io.Writer, reports []*crashlog.CrashReport) error {
summaries := make([]crashReportSummary, 0, len(reports))
for _, r := range reports {
summaries = append(summaries, crashReportSummary{
ID: r.ID,
Timestamp: r.Timestamp.Format(time.RFC3339),
Version: r.Version,
GoVersion: r.GoVersion,
OS: r.OS,
Arch: r.Arch,
Terminal: r.Terminal,
PanicValue: r.PanicValue,
Context: r.Context,
})
}
return json.NewEncoder(w).Encode(summaries)
}

func runReportShow(id string) error {
report, err := crashlog.Read(id)
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions cmd/report_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package cmd

import (
"bytes"
"encoding/json"
"strings"
"testing"
"time"

"github.com/jongio/grut/internal/crashlog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -146,6 +149,7 @@ func TestNewReportCmd_FlagsRegistered(t *testing.T) {
{"show", "string"},
{"clear", "bool"},
{"no-browser", "bool"},
{"json", "bool"},
}

for _, tt := range tests {
Expand Down Expand Up @@ -192,6 +196,53 @@ func TestRunReport_ShowFlag_NonexistentID(t *testing.T) {
assert.Error(t, err)
}

func TestRunReport_JSONWithoutListReturnsError(t *testing.T) {
cmd := newReportCmd()
cmd.SetArgs([]string{"--json"})

err := cmd.Execute()

assert.Error(t, err)
assert.Contains(t, err.Error(), "--json can only be used with --list")
}

func TestWriteCrashReportListJSON_Empty(t *testing.T) {
var out bytes.Buffer

err := writeCrashReportListJSON(&out, nil)

assert.NoError(t, err)
assert.JSONEq(t, `[]`, out.String())
}

func TestWriteCrashReportListJSON_SummaryFields(t *testing.T) {
when := time.Date(2026, 7, 11, 18, 30, 0, 0, time.UTC)
reports := []*crashlog.CrashReport{{
ID: "crash-1",
Timestamp: when,
Version: "1.2.3",
GoVersion: "go1.26.5",
OS: "windows",
Arch: "amd64",
Terminal: "Windows Terminal",
PanicValue: "nil pointer",
Context: "preview render",
StackTrace: "full stack stays out of the summary",
}}
var out bytes.Buffer

err := writeCrashReportListJSON(&out, reports)

require.NoError(t, err)
var got []crashReportSummary
require.NoError(t, json.Unmarshal(out.Bytes(), &got))
require.Len(t, got, 1)
assert.Equal(t, "crash-1", got[0].ID)
assert.Equal(t, "2026-07-11T18:30:00Z", got[0].Timestamp)
assert.Equal(t, "nil pointer", got[0].PanicValue)
assert.Equal(t, "preview render", got[0].Context)
}

func TestRunReport_LatestDefault_NoReports(t *testing.T) {
// Default behavior (no flags) when no crash reports exist should
// print "No crash reports found." and succeed.
Expand Down
9 changes: 9 additions & 0 deletions docs/report-json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Crash report JSON

Use JSON output when scripts need to inspect stored crash reports:

```bash
grut report --list --json
```

The command prints an array of summaries. Use `grut report --show <id>` to print one full report.
Loading