Skip to content

Commit ec80910

Browse files
committed
feat: grouped pretty print status output
1 parent bdf03d6 commit ec80910

File tree

4 files changed

+173
-36
lines changed

4 files changed

+173
-36
lines changed

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ require (
3636
github.com/mithrandie/csvq-driver v1.7.0
3737
github.com/muesli/reflow v0.3.0
3838
github.com/oapi-codegen/nullable v1.1.0
39+
github.com/olekukonko/tablewriter v1.1.0
3940
github.com/slack-go/slack v0.17.3
4041
github.com/spf13/afero v1.15.0
4142
github.com/spf13/cobra v1.10.1
@@ -251,7 +252,8 @@ require (
251252
github.com/oapi-codegen/runtime v1.1.2 // indirect
252253
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
253254
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
254-
github.com/olekukonko/tablewriter v0.0.5 // indirect
255+
github.com/olekukonko/errors v1.1.0 // indirect
256+
github.com/olekukonko/ll v0.0.9 // indirect
255257
github.com/opencontainers/go-digest v1.0.0 // indirect
256258
github.com/opencontainers/image-spec v1.1.1 // indirect
257259
github.com/pelletier/go-toml/v2 v2.2.4 // indirect

go.sum

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
687687
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
688688
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
689689
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
690-
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
691690
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
692691
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
693692
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -770,8 +769,12 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J
770769
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
771770
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
772771
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
773-
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
774-
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
772+
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
773+
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
774+
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
775+
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
776+
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
777+
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
775778
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
776779
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
777780
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=

internal/status/status.go

Lines changed: 160 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import (
1010
"net/http"
1111
"net/url"
1212
"os"
13-
"reflect"
14-
"strings"
1513
"sync"
1614
"time"
1715

1816
"github.com/docker/docker/api/types"
1917
"github.com/docker/docker/api/types/container"
2018
"github.com/go-errors/errors"
19+
"github.com/olekukonko/tablewriter"
20+
"github.com/olekukonko/tablewriter/tw"
2121
"github.com/spf13/afero"
2222
"github.com/supabase/cli/internal/utils"
2323
"github.com/supabase/cli/internal/utils/flags"
@@ -26,9 +26,11 @@ import (
2626

2727
type CustomName struct {
2828
ApiURL string `env:"api.url,default=API_URL"`
29+
RestURL string `env:"api.rest_url,default=REST_URL"`
2930
GraphqlURL string `env:"api.graphql_url,default=GRAPHQL_URL"`
3031
StorageS3URL string `env:"api.storage_s3_url,default=STORAGE_S3_URL"`
3132
McpURL string `env:"api.mcp_url,default=MCP_URL"`
33+
FunctionsURL string `env:"api.functions_url,default=FUNCTIONS_URL"`
3234
DbURL string `env:"db.url,default=DB_URL"`
3335
StudioURL string `env:"studio.url,default=STUDIO_URL"`
3436
InbucketURL string `env:"inbucket.url,default=INBUCKET_URL,deprecated"`
@@ -42,6 +44,24 @@ type CustomName struct {
4244
StorageS3SecretAccessKey string `env:"storage.s3_secret_access_key,default=S3_PROTOCOL_ACCESS_KEY_SECRET"`
4345
StorageS3Region string `env:"storage.s3_region,default=S3_PROTOCOL_REGION"`
4446
}
47+
type OutputType string
48+
49+
const (
50+
Text OutputType = "text"
51+
Link OutputType = "link"
52+
Key OutputType = "key"
53+
)
54+
55+
type OutputItem struct {
56+
Label string
57+
Value string
58+
Type OutputType
59+
}
60+
61+
type OutputGroup struct {
62+
Name string
63+
Items []OutputItem
64+
}
4565

4666
func (c *CustomName) toValues(exclude ...string) map[string]string {
4767
values := map[string]string{
@@ -56,7 +76,9 @@ func (c *CustomName) toValues(exclude ...string) map[string]string {
5676

5777
if apiEnabled {
5878
values[c.ApiURL] = utils.Config.Api.ExternalUrl
79+
values[c.RestURL] = utils.GetApiUrl("/rest/v1")
5980
values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1")
81+
values[c.FunctionsURL] = utils.GetApiUrl("/functions/v1")
6082
if studioEnabled {
6183
values[c.McpURL] = utils.GetApiUrl("/mcp")
6284
}
@@ -210,43 +232,149 @@ func printStatus(names CustomName, format string, w io.Writer, exclude ...string
210232

211233
func PrettyPrint(w io.Writer, exclude ...string) {
212234
names := CustomName{
213-
ApiURL: " " + utils.Aqua("API URL"),
214-
GraphqlURL: " " + utils.Aqua("GraphQL URL"),
215-
StorageS3URL: " " + utils.Aqua("S3 Storage URL"),
216-
McpURL: " " + utils.Aqua("MCP URL"),
217-
DbURL: " " + utils.Aqua("Database URL"),
218-
StudioURL: " " + utils.Aqua("Studio URL"),
219-
InbucketURL: " " + utils.Aqua("Inbucket URL"),
220-
MailpitURL: " " + utils.Aqua("Mailpit URL"),
221-
PublishableKey: " " + utils.Aqua("Publishable key"),
222-
SecretKey: " " + utils.Aqua("Secret key"),
223-
JWTSecret: " " + utils.Aqua("JWT secret"),
224-
AnonKey: " " + utils.Aqua("anon key"),
225-
ServiceRoleKey: "" + utils.Aqua("service_role key"),
226-
StorageS3AccessKeyId: " " + utils.Aqua("S3 Access Key"),
227-
StorageS3SecretAccessKey: " " + utils.Aqua("S3 Secret Key"),
228-
StorageS3Region: " " + utils.Aqua("S3 Region"),
235+
ApiURL: "API_URL",
236+
RestURL: "REST_URL",
237+
GraphqlURL: "GRAPHQL_URL",
238+
FunctionsURL: "FUNCTIONS_URL",
239+
StorageS3URL: "STORAGE_S3_URL",
240+
McpURL: "MCP_URL",
241+
DbURL: "DB_URL",
242+
StudioURL: "STUDIO_URL",
243+
InbucketURL: "INBUCKET_URL",
244+
MailpitURL: "MAILPIT_URL",
245+
PublishableKey: "PUBLISHABLE_KEY",
246+
SecretKey: "SECRET_KEY",
247+
JWTSecret: "JWT_SECRET",
248+
AnonKey: "ANON_KEY",
249+
ServiceRoleKey: "SERVICE_ROLE_KEY",
250+
StorageS3AccessKeyId: "S3_PROTOCOL_ACCESS_KEY_ID",
251+
StorageS3SecretAccessKey: "S3_PROTOCOL_SECRET_ACCESS_KEY",
252+
StorageS3Region: "S3_PROTOCOL_REGION",
229253
}
230254
values := names.toValues(exclude...)
231-
// Iterate through map in order of declared struct fields
232-
t := reflect.TypeOf(names)
233-
val := reflect.ValueOf(names)
234-
for i := 0; i < val.NumField(); i++ {
235-
k := val.Field(i).String()
236-
if tag := t.Field(i).Tag.Get("env"); isDeprecated(tag) {
237-
continue
255+
256+
groups := []OutputGroup{
257+
{
258+
Name: "🛠️ Development Tools",
259+
Items: []OutputItem{
260+
{Label: "Studio", Value: values[names.StudioURL], Type: Link},
261+
{Label: "Mailpit", Value: values[names.MailpitURL], Type: Link},
262+
{Label: "MCP", Value: values[names.McpURL], Type: Link},
263+
},
264+
},
265+
{
266+
Name: "🌐 APIs",
267+
Items: []OutputItem{
268+
{Label: "Project URL", Value: values[names.ApiURL], Type: Link},
269+
{Label: "REST", Value: values[names.RestURL], Type: Link},
270+
{Label: "GraphQL", Value: values[names.GraphqlURL], Type: Link},
271+
{Label: "Edge Functions", Value: values[names.FunctionsURL], Type: Link},
272+
},
273+
},
274+
{
275+
Name: "🗄️ Database",
276+
Items: []OutputItem{
277+
{Label: "URL", Value: values[names.DbURL], Type: Link},
278+
},
279+
},
280+
{
281+
Name: "🔑 Authentication Keys",
282+
Items: []OutputItem{
283+
{Label: "Publishable", Value: values[names.PublishableKey], Type: Key},
284+
{Label: "Secret", Value: values[names.SecretKey], Type: Key},
285+
},
286+
},
287+
{
288+
Name: "📦 Storage (S3)",
289+
Items: []OutputItem{
290+
{Label: "URL", Value: values[names.StorageS3URL], Type: Link},
291+
{Label: "Access Key", Value: values[names.StorageS3AccessKeyId], Type: Key},
292+
{Label: "Secret Key", Value: values[names.StorageS3SecretAccessKey], Type: Key},
293+
{Label: "Region", Value: values[names.StorageS3Region], Type: Text},
294+
},
295+
},
296+
}
297+
298+
for _, group := range groups {
299+
// ensure at least one item in the group is non-empty
300+
shouldPrint := false
301+
for _, item := range group.Items {
302+
if item.Value != "" {
303+
shouldPrint = true
304+
break
305+
}
238306
}
239-
if v, ok := values[k]; ok {
240-
fmt.Fprintf(w, "%s: %s\n", k, v)
307+
if shouldPrint {
308+
printTable(w, group.Name, group.Items)
309+
fmt.Fprintln(w)
241310
}
242311
}
312+
243313
}
244314

245-
func isDeprecated(tag string) bool {
246-
for part := range strings.SplitSeq(tag, ",") {
247-
if strings.EqualFold(part, "deprecated") {
248-
return true
315+
func printTable(w io.Writer, title string, rows []OutputItem) {
316+
table := tablewriter.NewTable(w,
317+
// Rounded corners
318+
tablewriter.WithSymbols(tw.NewSymbols(tw.StyleRounded)),
319+
320+
// Table content formatting
321+
tablewriter.WithConfig(tablewriter.Config{
322+
Header: tw.CellConfig{
323+
Formatting: tw.CellFormatting{
324+
AutoFormat: tw.Off,
325+
MergeMode: tw.MergeHorizontal,
326+
},
327+
Alignment: tw.CellAlignment{
328+
Global: tw.AlignLeft,
329+
},
330+
Filter: tw.CellFilter{
331+
Global: func(s []string) []string {
332+
for i := range s {
333+
s[i] = utils.Bold(s[i])
334+
}
335+
return s
336+
},
337+
},
338+
},
339+
Row: tw.CellConfig{
340+
Alignment: tw.CellAlignment{
341+
Global: tw.AlignLeft,
342+
},
343+
ColMaxWidths: tw.CellWidth{
344+
PerColumn: map[int]int{0: 16},
345+
},
346+
Filter: tw.CellFilter{
347+
PerColumn: []func(string) string{
348+
func(s string) string {
349+
return utils.Green(s)
350+
},
351+
},
352+
},
353+
},
354+
Behavior: tw.Behavior{
355+
Compact: tw.Compact{
356+
Merge: tw.On,
357+
},
358+
},
359+
}),
360+
)
361+
362+
// Set title as header (merged across all columns)
363+
table.Header(title, title)
364+
365+
// Add data rows with values colored based on type
366+
for _, row := range rows {
367+
if row.Value != "" {
368+
switch row.Type {
369+
case Link:
370+
table.Append(row.Label, utils.Aqua(row.Value))
371+
case Key:
372+
table.Append(row.Label, utils.Yellow(row.Value))
373+
case Text:
374+
table.Append(row.Label, row.Value)
375+
}
249376
}
250377
}
251-
return false
378+
379+
table.Render()
252380
}

internal/utils/colors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ func Yellow(str string) string {
1313
return lipgloss.NewStyle().Foreground(lipgloss.Color("11")).Render(str)
1414
}
1515

16+
func Green(str string) string {
17+
return lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render(str)
18+
}
19+
1620
// For errors.
1721
func Red(str string) string {
1822
return lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Render(str)

0 commit comments

Comments
 (0)