auth: styled table with headers for `entire auth contexts` · Entire

auth: styled table with headers for entire auth contexts

f78b7e2·

toothbrush·1mo ago·3 files·+64 added/-20 removed

Replace the tab-separated, headerless context listing with an aligned, styled table (CONTEXT / HANDLE / CORE URL columns, "*" marks the active context), matching the auth status active-sessions table.

Extract the column-sizing/writing loop into a shared renderAlignedTable helper and rename the shared style set authTableStyles, since both auth tables now use it.

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

Sessions

52bdb2ae5d4eView transcript

[?
Refactor Auth Commands for Session ManagementClaude Code·Opus 4.8[1m]·1 step](/content/gh/entireio/cli/session/d51ce65e-9ad9-4c0d-b6af-911eed02c1ee#timeline-52bdb2ae5d4e/index.html)

Changes

3

214 unmodified lines

215
216
217
218
218
219
220
221
14 unmodified lines

236
237
238
239
239
240
241
242
243
244
245
241
242
243
244
245
246
247
248
5 unmodified lines

254
255
256
257
257
258
259
259
260
261
262
7 unmodified lines

270
271
272
273
273
274
275
276
4 unmodified lines

281
282
283
284
284
285
286
287
12 unmodified lines

300
301
302
303
303
304
305
306
307
308
309
310
311
312
313
314
21 unmodified lines

336
337
338
331
339
340
341
342
343
344
345
338
346
347
348
349
350
351
352
345
353
354
355
356

214 unmodified lines

fmt.Fprintln(w, "Active sessions:")
sortSessionsByRecency(sessions)
rendeSessionsTable(w, newSessionsTableStyles(w), sessions, time.Now())
rendeSessionsTable(w, newAuthTableStyles(w), sessions, time.Now())
return nil
}

14 unmodified lines

}
// --- active-sessions table ---------------------------------------------------
// --- auth tables -------------------------------------------------------------

// sessionsTableStyles holds the lipgloss styles for the `entire auth status`
// active-sessions table. Mirrors the approach in activity_render.go: keep
// style construction tied to color detection, and render plain text when
// color is disabled.
type sessionsTableStyles struct {
// authTableStyles holds the lipgloss styles shared by the `entire auth status`
// active-sessions table and the `entire auth contexts` table. Mirrors the
// approach in activity_render.go: keep style construction tied to color
// detection, and render plain text when color is disabled.
type authTableStyles struct {
colorEnabled bool

header  lipgloss.Style // bold + dim, used for column headers
5 unmodified lines

expired lipgloss.Style // already expired
}

func newSessionsTableStyles(w io.Writer) sessionsTableStyles {
func newAuthTableStyles(w io.Writer) authTableStyles {
    useColor := shouldUseColor(w)
    s := sessionsTableStyles{colorEnabled: useColor}
    s := authTableStyles{colorEnabled: useColor}
    if !useColor {
        return s
    }
7 unmodified lines

return s
}

func (s sessionsTableStyles) render(style lipgloss.Style, text string) string {
func (s authTableStyles) render(style lipgloss.Style, text string) string {
    if !s.colorEnabled {
        return text
    }
4 unmodified lines

// Column padding is computed via lipgloss.Width — it strips ANSI escapes, so a
// styled cell's visible width matches its plain text. tabwriter can't be used
// here once cells contain ANSI codes.
func renderSessionsTable(w io.Writer, sty sessionsTableStyles, tokens []api.Token, now time.Time) {
func renderSessionsTable(w io.Writer, sty authTableStyles, tokens []api.Token, now time.Time) {
    headCells := []string{"ID", "NAME", "SCOPE", "CREATED", "LAST USED", "EXPIRES"}
    header := make([]string, len(headerCells))
    for i, h := range headerCells {
12 unmodified lines

})

widths := make([]int, len(headerCells))
    renderAlignedTable(w, header, rows)
}

// renderAlignedTable writes header followed by rows in left-aligned columns,
// sizing each column to its widest (possibly pre-styled) cell. Column widths
// use lipgloss.Width so ANSI escapes don't inflate the padding. Shared by the
// auth-status and auth-contexts tables.
func renderAlignedTable(w io.Writer, header []string, rows [][]string) {
    widths := make([]int, len(header))
    for i, h := range header {
        widths[i] = lipgloss.Width(h)
    }
21 unmodified lines

fmt.Fprintln(w)
}

func styleName(sty sessionsTableStyles, name string) string {
func styleName(sty authTableStyles, name string) string {
    if name == "" {
        return sty.render(sty.dim, placeholderDash)
    }
    return sty.render(sty.name, name)
}

func styleLastUsed(sty sessionsTableStyles, lastUsed *string, now time.Time) string {
func styleLastUsed(sty authTableStyles, lastUsed *string, now time.Time) string {
    if lastUsed == nil {
        return sty.render(sty.dim, lastUsedNever)
    }
    return sty.render(sty.value, formatAuthLastUsed(lastUsed, now))
}

func styleExpires(sty sessionsTableStyles, expiresAt string, now time.Time) string {
func styleExpires(sty authTableStyles, expiresAt string, now time.Time) string {
    formatted := formatAuthDate(expiresAt)
    switch classifyExpiresAt(expiresAt, now) {
    case expiresExpired:

Mcmd/entire/cli/auth.go+23/-15


5 unmodified lines

6
7
8
9
10
11
12
79 unmodified lines

92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
97
118
119
120
99
121
122
123
124
125
126
127
101
128
129
130

5 unmodified lines

"github.com/entireio/cli/cmd/entire/cli/api"
    "github.com/entireio/cli/cmd/entire/cli/auth"
    "github.com/entireio/cli/internal/entireclient/contexts"
    "github.com/spf13/cobra"
)

79 unmodified lines

fmt.Fprintln(w, "No login contexts. Run 'entire login' to authenticate.")
    return nil
    }
    renderContextsTable(w, all, current)
    return nil
}

// renderContextsTable prints the saved login contexts as a styled, aligned
// table with column headers. The active context is flagged with "*" in the
// leading column. Purely local data — no network, no timestamps — so it
// reuses the auth-table styles but only the header/name/value/accent slots.
func renderContextsTable(w io.Writer, all []*contexts.Context, current string) {
    sty := newAuthTableStyles(w)

header := []string{
        "", // active marker
        sty.render(sty.header, "CONTEXT"),
        sty.render(sty.header, "HANDLE"),
        sty.render(sty.header, "CORE URL"),
    }

rows := make([][]string, 0, len(all))
    for _, c := range all {
        marker := " "
        name := sty.render(sty.value, c.Name)
        if c.Name == current {
            marker = "*"
            marker = sty.render(sty.id, "*")
            name = sty.render(sty.name, c.Name)
        }
        fmt.Fprintf(w, "%s %s\t%s\t%s\n", marker, c.Name, c.Handle, c.CoreURL)
        rows = append(rows, []string{
            marker,
            name,
            sty.render(sty.value, fallback(c.Handle, placeholderDash)),
            sty.render(sty.value, fallback(c.CoreURL, placeholderDash)),
        })
    }
    return nil

renderAlignedTable(w, header, rows)
}

Mcmd/entire/cli/auth_context.go+31/-3


46 unmodified lines

47
48
49
50
51
50
51
52
53
54
55
56
57
58
59
60
61
62

46 unmodified lines

t.Fatalf("runAuthContexts: %v", err)
    }
    got := out.String()
    if !strings.Contains(got, "* core.example.com") {
        t.Fatalf("listing = %q, want current-marked core.example.com", got)
    }
    for _, hdr := range []string{"CONTEXT", "HANDLE", "CORE URL"} {
        if !strings.Contains(got, hdr) {
            t.Fatalf("listing = %q, want column header %q", got, hdr)
        }
    }
    if !strings.Contains(got, "*") {
        t.Fatalf("listing = %q, want an active-context marker", got)
    }
    if !strings.Contains(got, "core.example.com") {
        t.Fatalf("listing = %q, want context core.example.com", got)
    }
    if !strings.Contains(got, "alice") {
        t.Fatalf("listing = %q, want handle alice", got)
    }

Mcmd/entire/cli/auth_context_test.go+10/-2