diff --git a/go.mod b/go.mod index 5ce13247d..beb3211dc 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/mithrandie/csvq-driver v1.7.0 github.com/muesli/reflow v0.3.0 github.com/oapi-codegen/nullable v1.1.0 + github.com/olekukonko/tablewriter v1.1.0 github.com/slack-go/slack v0.17.3 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.1 @@ -315,7 +316,8 @@ require ( github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect diff --git a/go.sum b/go.sum index c18199eef..6ad28c6de 100644 --- a/go.sum +++ b/go.sum @@ -719,7 +719,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -829,8 +828,12 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= diff --git a/internal/status/status.go b/internal/status/status.go index ee087062b..f4770e94d 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -10,15 +10,16 @@ import ( "net/http" "net/url" "os" - "reflect" "slices" - "strings" "sync" "time" + "github.com/Netflix/go-env" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/go-errors/errors" + "github.com/olekukonko/tablewriter" + "github.com/olekukonko/tablewriter/tw" "github.com/spf13/afero" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" @@ -27,9 +28,11 @@ import ( type CustomName struct { ApiURL string `env:"api.url,default=API_URL"` + RestURL string `env:"api.rest_url,default=REST_URL"` GraphqlURL string `env:"api.graphql_url,default=GRAPHQL_URL"` StorageS3URL string `env:"api.storage_s3_url,default=STORAGE_S3_URL"` McpURL string `env:"api.mcp_url,default=MCP_URL"` + FunctionsURL string `env:"api.functions_url,default=FUNCTIONS_URL"` DbURL string `env:"db.url,default=DB_URL"` StudioURL string `env:"studio.url,default=STUDIO_URL"` InbucketURL string `env:"inbucket.url,default=INBUCKET_URL,deprecated"` @@ -54,10 +57,15 @@ func (c *CustomName) toValues(exclude ...string) map[string]string { authEnabled := utils.Config.Auth.Enabled && !slices.Contains(exclude, utils.GotrueId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image)) inbucketEnabled := utils.Config.Inbucket.Enabled && !slices.Contains(exclude, utils.InbucketId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image)) storageEnabled := utils.Config.Storage.Enabled && !slices.Contains(exclude, utils.StorageId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) + functionsEnabled := utils.Config.EdgeRuntime.Enabled && !slices.Contains(exclude, utils.EdgeRuntimeId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.EdgeRuntime.Image)) if apiEnabled { values[c.ApiURL] = utils.Config.Api.ExternalUrl + values[c.RestURL] = utils.GetApiUrl("/rest/v1") values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1") + if functionsEnabled { + values[c.FunctionsURL] = utils.GetApiUrl("/functions/v1") + } if studioEnabled { values[c.McpURL] = utils.GetApiUrl("/mcp") } @@ -210,44 +218,159 @@ func printStatus(names CustomName, format string, w io.Writer, exclude ...string } func PrettyPrint(w io.Writer, exclude ...string) { - names := CustomName{ - ApiURL: " " + utils.Aqua("API URL"), - GraphqlURL: " " + utils.Aqua("GraphQL URL"), - StorageS3URL: " " + utils.Aqua("S3 Storage URL"), - McpURL: " " + utils.Aqua("MCP URL"), - DbURL: " " + utils.Aqua("Database URL"), - StudioURL: " " + utils.Aqua("Studio URL"), - InbucketURL: " " + utils.Aqua("Inbucket URL"), - MailpitURL: " " + utils.Aqua("Mailpit URL"), - PublishableKey: " " + utils.Aqua("Publishable key"), - SecretKey: " " + utils.Aqua("Secret key"), - JWTSecret: " " + utils.Aqua("JWT secret"), - AnonKey: " " + utils.Aqua("anon key"), - ServiceRoleKey: "" + utils.Aqua("service_role key"), - StorageS3AccessKeyId: " " + utils.Aqua("S3 Access Key"), - StorageS3SecretAccessKey: " " + utils.Aqua("S3 Secret Key"), - StorageS3Region: " " + utils.Aqua("S3 Region"), + logger := utils.GetDebugLogger() + + names := CustomName{} + if err := env.Unmarshal(env.EnvSet{}, &names); err != nil { + fmt.Fprintln(logger, err) } values := names.toValues(exclude...) - // Iterate through map in order of declared struct fields - t := reflect.TypeOf(names) - val := reflect.ValueOf(names) - for i := 0; i < val.NumField(); i++ { - k := val.Field(i).String() - if tag := t.Field(i).Tag.Get("env"); isDeprecated(tag) { + + groups := []OutputGroup{ + { + Name: "🛠️ Development Tools", + Items: []OutputItem{ + {Label: "Studio", Value: values[names.StudioURL], Type: Link}, + {Label: "Mailpit", Value: values[names.MailpitURL], Type: Link}, + {Label: "MCP", Value: values[names.McpURL], Type: Link}, + }, + }, + { + Name: "🌐 APIs", + Items: []OutputItem{ + {Label: "Project URL", Value: values[names.ApiURL], Type: Link}, + {Label: "REST", Value: values[names.RestURL], Type: Link}, + {Label: "GraphQL", Value: values[names.GraphqlURL], Type: Link}, + {Label: "Edge Functions", Value: values[names.FunctionsURL], Type: Link}, + }, + }, + { + Name: "🗄️ Database", + Items: []OutputItem{ + {Label: "URL", Value: values[names.DbURL], Type: Link}, + }, + }, + { + Name: "🔑 Authentication Keys", + Items: []OutputItem{ + {Label: "Publishable", Value: values[names.PublishableKey], Type: Key}, + {Label: "Secret", Value: values[names.SecretKey], Type: Key}, + }, + }, + { + Name: "📦 Storage (S3)", + Items: []OutputItem{ + {Label: "URL", Value: values[names.StorageS3URL], Type: Link}, + {Label: "Access Key", Value: values[names.StorageS3AccessKeyId], Type: Key}, + {Label: "Secret Key", Value: values[names.StorageS3SecretAccessKey], Type: Key}, + {Label: "Region", Value: values[names.StorageS3Region], Type: Text}, + }, + }, + } + + for _, group := range groups { + if err := group.printTable(w); err != nil { + fmt.Fprintln(logger, err) + } else { + fmt.Fprintln(w) + } + } +} + +type OutputType string + +const ( + Text OutputType = "text" + Link OutputType = "link" + Key OutputType = "key" +) + +type OutputItem struct { + Label string + Value string + Type OutputType +} + +type OutputGroup struct { + Name string + Items []OutputItem +} + +func (g *OutputGroup) printTable(w io.Writer) error { + table := tablewriter.NewTable(w, + // Rounded corners + tablewriter.WithSymbols(tw.NewSymbols(tw.StyleRounded)), + + // Table content formatting + tablewriter.WithConfig(tablewriter.Config{ + Header: tw.CellConfig{ + Formatting: tw.CellFormatting{ + AutoFormat: tw.Off, + MergeMode: tw.MergeHorizontal, + }, + Alignment: tw.CellAlignment{ + Global: tw.AlignLeft, + }, + Filter: tw.CellFilter{ + Global: func(s []string) []string { + for i := range s { + s[i] = utils.Bold(s[i]) + } + return s + }, + }, + }, + Row: tw.CellConfig{ + Alignment: tw.CellAlignment{ + Global: tw.AlignLeft, + }, + ColMaxWidths: tw.CellWidth{ + PerColumn: map[int]int{0: 16}, + }, + Filter: tw.CellFilter{ + PerColumn: []func(string) string{ + func(s string) string { + return utils.Green(s) + }, + }, + }, + }, + Behavior: tw.Behavior{ + Compact: tw.Compact{ + Merge: tw.On, + }, + }, + }), + + // Set title as header (merged across all columns) + tablewriter.WithHeader([]string{g.Name, g.Name}), + ) + + // Add data rows with values colored based on type + shouldRender := false + for _, row := range g.Items { + if row.Value == "" { continue } - if v, ok := values[k]; ok { - fmt.Fprintf(w, "%s: %s\n", k, v) + value := row.Value + switch row.Type { + case Link: + value = utils.Aqua(row.Value) + case Key: + value = utils.Yellow(row.Value) + } + if err := table.Append(row.Label, value); err != nil { + return errors.Errorf("failed to append row: %w", err) } + shouldRender = true } -} -func isDeprecated(tag string) bool { - for part := range strings.SplitSeq(tag, ",") { - if strings.EqualFold(part, "deprecated") { - return true + // Ensure at least one item in the group is non-empty + if shouldRender { + if err := table.Render(); err != nil { + return errors.Errorf("failed to render table: %w", err) } } - return false + + return nil } diff --git a/internal/utils/colors.go b/internal/utils/colors.go index ed710c4a2..f4f82652c 100644 --- a/internal/utils/colors.go +++ b/internal/utils/colors.go @@ -13,6 +13,10 @@ func Yellow(str string) string { return lipgloss.NewStyle().Foreground(lipgloss.Color("11")).Render(str) } +func Green(str string) string { + return lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render(str) +} + // For errors. func Red(str string) string { return lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Render(str)