feat(trail): show trail URL in 'trail list' (table column + --json) · Entire

feat(trail): show trail URL in 'trail list' (table column + --json)

fb6ce0f→main·

toothbrush·3w ago·3 files·+51 added/-2 removed

Consume the server-provided url field in the list path: attach it to each trail's metadata (server-preferred, locally constructed fallback for older servers), add a URL column to the table when present, and surface it in --json. Mirrors create/show.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

f2d02933f5adView transcript

[?
Add Web URLs to Trail CommandsClaude Code·Opus 4.8[1m]·4 steps](/content/gh/entireio/cli/session/18a97739-bf62-4c0b-9cf2-9a1673f59583#timeline-f2d02933f5ad/index.html)

Changes

3

152 unmodified lines

153
154
155
156
157
158
159

152 unmodified lines

type Metadata struct {
    Number    int        `json:"number,omitempty"`
    TrailID   ID         `json:"trail_id"`
    URL       string     `json:"url,omitempty"`
    Branch    string     `json:"branch"`
    Base      string     `json:"base"`
    Title     string     `json:"title"`
}

Mcmd/entire/cli/trail/trail.go+1

379 unmodified lines

380
381
382
383
383
384
385
386
386
387
388
389
390
391
392
182 unmodified lines

575
576
577
578
579
580
581
5 unmodified lines

587
588
589
590
591
592
593
594
595
15 unmodified lines

611
612
613
614
615
616
617
618
619
8 unmodified lines

628
629
630
631
632
633
634
635
636
637
638
639
640
641
642

379 unmodified lines

return fmt.Errorf("failed to decode trail list: %w", err)
    }

// Convert to metadata for display
    // Convert to metadata for display, attaching the browser URL (server-provided
    // when present, locally constructed as a fallback for older servers).
    trails := make([]*trail.Metadata, 0, len(listResp.Trails))
    for i := range listResp.Trails {
        trails = append(trails, listResp.Trails[i].ToMetadata())
        m := listResp.Trails[i].ToMetadata()
        m.URL = trailDisplayURL(listResp.Trails[i], forge, owner, repo)
        trails = append(trails, m)
    }

totalMatched := listResp.Total
182 unmodified lines

// branch names or logins don't throw off the table.
    tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
    showPhase := trailListHasPhase(trails)
    showURL := trailListHasURL(trails)
    columns := []string{"NUM", "BRANCH", "TITLE"}
    if showStatus {
        columns = append(columns, "STATUS")
    }
    columns = append(columns, "AUTHOR")
    columns = append(columns, "UPDATED")
    if showURL {
        columns = append(columns, "URL")
    }
    fmt.Fprintln(tw, "  "+strings.Join(columns, "\t"))
    for _, t := range trails {
        number := "-"
15 unmodified lines

fields = append(fields, t.AuthorLogin())
        }
        fields = append(fields, timeAgo(t.UpdatedAt))
        if showURL {
            fields = append(fields, t.URL)
        }
        fmt.Fprintln(tw, "  "+strings.Join(fields, "\t"))
    }
    _ = tw.Flush()
8 unmodified lines

return false
}

func trailListHasURL(trails []*trail.Metadata) bool {
    for _, t := range trails {
        if t != nil && strings.TrimSpace(t.URL) != "" {
            return true
        }
    }
    return false
}

func trailPhaseDisplay(phase string) string {
    phase = strings.TrimSpace(phase)
    if phase == "" {

Mcmd/entire/cli/trail_cmd.go+21/-2

892 unmodified lines

893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927

892 unmodified lines

}
}

func TestPrintTrailListShowsURLColumnWhenPresent(t *testing.T) {
    t.Parallel()
    alice := trailListTestAuthorAlice
    var out bytes.Buffer
    printTrailList(&out, []*trail.Metadata{
        {Number: 5, Branch: "feat/a", Status: trail.StatusOpen, URL: "https://entire.io/gh/acme/repo/trails/5", Author: &trail.Author{Login: &alice}, UpdatedAt: time.Now()},
    }, trailListDisplayOptions{StatusFilters: []trail.Status{trail.StatusOpen}})

text := out.String()
    if !strings.Contains(text, "URL") || !strings.Contains(text, "https://entire.io/gh/acme/repo/trails/5") {
        t.Fatalf("expected a URL column with the trail url, got:\n%s", text)
    }
}

func TestPrintTrailListOmitsURLColumnWhenAbsent(t *testing.T) {
    t.Parallel()
    alice := trailListTestAuthorAlice
    var out bytes.Buffer
    printTrailList(&out, []*trail.Metadata{
        {Number: 5, Branch: "feat/a", Status: trail.StatusOpen, Author: &trail.Author{Login: &alice}, UpdatedAt: time.Now()},
    }, trailListDisplayOptions{StatusFilters: []trail.Status{trail.StatusOpen}})

// The column header must not appear when no trail carries a URL (e.g. an
    // older server that omits the field and no local fallback was attached).
    if text := out.String(); strings.Contains(text, "URL") {
        t.Fatalf("expected URL column omitted when no trail has a url, got:\n%s", text)
    }
}

func TestPrintTrailListAnyStatusShowsStatusColumn(t *testing.T) {
    t.Parallel()
    alice := trailListTestAuthorAlice