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
cmd/entire/cli
Mauth.go+23/-15
Mauth_context.go+31/-3
Mauth_context_test.go+10/-2
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