From ec809102786daa490b9710113314533a7669d62e Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 9 Oct 2025 08:49:15 -0600 Subject: [PATCH 1/3] feat: grouped pretty print status output --- go.mod | 4 +- go.sum | 9 +- internal/status/status.go | 192 +++++++++++++++++++++++++++++++------- internal/utils/colors.go | 4 + 4 files changed, 173 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index 25ad32be3..4f7bf73a5 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,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 @@ -251,7 +252,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/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index ca58a3d3c..e65d8cf76 100644 --- a/go.sum +++ b/go.sum @@ -687,7 +687,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= @@ -770,8 +769,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 4ed7afde2..955eb3721 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -10,14 +10,14 @@ import ( "net/http" "net/url" "os" - "reflect" - "strings" "sync" "time" "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" @@ -26,9 +26,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"` @@ -42,6 +44,24 @@ type CustomName struct { StorageS3SecretAccessKey string `env:"storage.s3_secret_access_key,default=S3_PROTOCOL_ACCESS_KEY_SECRET"` StorageS3Region string `env:"storage.s3_region,default=S3_PROTOCOL_REGION"` } +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 (c *CustomName) toValues(exclude ...string) map[string]string { values := map[string]string{ @@ -56,7 +76,9 @@ func (c *CustomName) toValues(exclude ...string) map[string]string { if apiEnabled { values[c.ApiURL] = utils.Config.Api.ExternalUrl + values[c.RestURL] = utils.GetApiUrl("/rest/v1") values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1") + values[c.FunctionsURL] = utils.GetApiUrl("/functions/v1") if studioEnabled { values[c.McpURL] = utils.GetApiUrl("/mcp") } @@ -210,43 +232,149 @@ 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"), + ApiURL: "API_URL", + RestURL: "REST_URL", + GraphqlURL: "GRAPHQL_URL", + FunctionsURL: "FUNCTIONS_URL", + StorageS3URL: "STORAGE_S3_URL", + McpURL: "MCP_URL", + DbURL: "DB_URL", + StudioURL: "STUDIO_URL", + InbucketURL: "INBUCKET_URL", + MailpitURL: "MAILPIT_URL", + PublishableKey: "PUBLISHABLE_KEY", + SecretKey: "SECRET_KEY", + JWTSecret: "JWT_SECRET", + AnonKey: "ANON_KEY", + ServiceRoleKey: "SERVICE_ROLE_KEY", + StorageS3AccessKeyId: "S3_PROTOCOL_ACCESS_KEY_ID", + StorageS3SecretAccessKey: "S3_PROTOCOL_SECRET_ACCESS_KEY", + StorageS3Region: "S3_PROTOCOL_REGION", } 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) { - continue + + 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 { + // ensure at least one item in the group is non-empty + shouldPrint := false + for _, item := range group.Items { + if item.Value != "" { + shouldPrint = true + break + } } - if v, ok := values[k]; ok { - fmt.Fprintf(w, "%s: %s\n", k, v) + if shouldPrint { + printTable(w, group.Name, group.Items) + fmt.Fprintln(w) } } + } -func isDeprecated(tag string) bool { - for part := range strings.SplitSeq(tag, ",") { - if strings.EqualFold(part, "deprecated") { - return true +func printTable(w io.Writer, title string, rows []OutputItem) { + 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) + table.Header(title, title) + + // Add data rows with values colored based on type + for _, row := range rows { + if row.Value != "" { + switch row.Type { + case Link: + table.Append(row.Label, utils.Aqua(row.Value)) + case Key: + table.Append(row.Label, utils.Yellow(row.Value)) + case Text: + table.Append(row.Label, row.Value) + } } } - return false + + table.Render() } 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) From d277740b65e2b1c6c3327c496633423264214bcd Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 9 Oct 2025 14:45:02 -0600 Subject: [PATCH 2/3] fix: re-gen openapi types --- pkg/api/types.gen.go | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index d7f36b7d0..2cdd4d0ec 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -657,12 +657,6 @@ const ( RefreshToken OAuthTokenBodyGrantType = "refresh_token" ) -// Defines values for OAuthTokenBodyResource. -const ( - OAuthTokenBodyResourceHttpsapiSupabaseGreenmcp OAuthTokenBodyResource = "https://api.supabase.green/mcp" - OAuthTokenBodyResourceHttpsmcpSupabaseGreenmcp OAuthTokenBodyResource = "https://mcp.supabase.green/mcp" -) - // Defines values for OAuthTokenResponseTokenType. const ( Bearer OAuthTokenResponseTokenType = "Bearer" @@ -1232,12 +1226,6 @@ const ( V1AuthorizeUserParamsCodeChallengeMethodSha256 V1AuthorizeUserParamsCodeChallengeMethod = "sha256" ) -// Defines values for V1AuthorizeUserParamsResource. -const ( - V1AuthorizeUserParamsResourceHttpsapiSupabaseGreenmcp V1AuthorizeUserParamsResource = "https://api.supabase.green/mcp" - V1AuthorizeUserParamsResourceHttpsmcpSupabaseGreenmcp V1AuthorizeUserParamsResource = "https://mcp.supabase.green/mcp" -) - // Defines values for V1OauthAuthorizeProjectClaimParamsResponseType. const ( V1OauthAuthorizeProjectClaimParamsResponseTypeCode V1OauthAuthorizeProjectClaimParamsResponseType = "code" @@ -2569,15 +2557,13 @@ type OAuthTokenBody struct { RefreshToken *string `json:"refresh_token,omitempty"` // Resource Resource indicator for MCP (Model Context Protocol) clients - Resource *OAuthTokenBodyResource `json:"resource,omitempty"` + Resource *string `json:"resource,omitempty"` + Scope *string `json:"scope,omitempty"` } // OAuthTokenBodyGrantType defines model for OAuthTokenBody.GrantType. type OAuthTokenBodyGrantType string -// OAuthTokenBodyResource Resource indicator for MCP (Model Context Protocol) clients -type OAuthTokenBodyResource string - // OAuthTokenResponse defines model for OAuthTokenResponse. type OAuthTokenResponse struct { AccessToken string `json:"access_token"` @@ -2607,12 +2593,10 @@ type OrganizationProjectClaimResponse struct { Limit float32 `json:"limit"` Name string `json:"name"` } `json:"members_exceeding_free_project_limit"` - SourceSubscriptionPlan OrganizationProjectClaimResponsePreviewSourceSubscriptionPlan `json:"source_subscription_plan"` - TargetOrganizationEligible nullable.Nullable[bool] `json:"target_organization_eligible"` - TargetOrganizationHasFreeProjectSlots nullable.Nullable[bool] `json:"target_organization_has_free_project_slots"` - TargetSubscriptionPlan nullable.Nullable[OrganizationProjectClaimResponsePreviewTargetSubscriptionPlan] `json:"target_subscription_plan"` - Valid bool `json:"valid"` - Warnings []struct { + SourceSubscriptionPlan OrganizationProjectClaimResponsePreviewSourceSubscriptionPlan `json:"source_subscription_plan"` + TargetSubscriptionPlan nullable.Nullable[OrganizationProjectClaimResponsePreviewTargetSubscriptionPlan] `json:"target_subscription_plan"` + Valid bool `json:"valid"` + Warnings []struct { Key string `json:"key"` Message string `json:"message"` } `json:"warnings"` @@ -3871,7 +3855,7 @@ type V1AuthorizeUserParams struct { OrganizationSlug *string `form:"organization_slug,omitempty" json:"organization_slug,omitempty"` // Resource Resource indicator for MCP (Model Context Protocol) clients - Resource *V1AuthorizeUserParamsResource `form:"resource,omitempty" json:"resource,omitempty"` + Resource *string `form:"resource,omitempty" json:"resource,omitempty"` } // V1AuthorizeUserParamsResponseType defines parameters for V1AuthorizeUser. @@ -3880,9 +3864,6 @@ type V1AuthorizeUserParamsResponseType string // V1AuthorizeUserParamsCodeChallengeMethod defines parameters for V1AuthorizeUser. type V1AuthorizeUserParamsCodeChallengeMethod string -// V1AuthorizeUserParamsResource defines parameters for V1AuthorizeUser. -type V1AuthorizeUserParamsResource string - // V1OauthAuthorizeProjectClaimParams defines parameters for V1OauthAuthorizeProjectClaim. type V1OauthAuthorizeProjectClaimParams struct { // ProjectRef Project ref From 3b1adbd9eb07e4e5655c332668fe04a2cbaadb0a Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 9 Oct 2025 14:49:47 -0600 Subject: [PATCH 3/3] fix: lints --- internal/status/status.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/status/status.go b/internal/status/status.go index 955eb3721..50a842419 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -309,7 +309,6 @@ func PrettyPrint(w io.Writer, exclude ...string) { fmt.Fprintln(w) } } - } func printTable(w io.Writer, title string, rows []OutputItem) { @@ -362,19 +361,28 @@ func printTable(w io.Writer, title string, rows []OutputItem) { // Set title as header (merged across all columns) table.Header(title, title) + var appendError error + // Add data rows with values colored based on type for _, row := range rows { if row.Value != "" { switch row.Type { case Link: - table.Append(row.Label, utils.Aqua(row.Value)) + appendError = table.Append(row.Label, utils.Aqua(row.Value)) case Key: - table.Append(row.Label, utils.Yellow(row.Value)) + appendError = table.Append(row.Label, utils.Yellow(row.Value)) case Text: - table.Append(row.Label, row.Value) + appendError = table.Append(row.Label, row.Value) } } } - table.Render() + if appendError != nil { + fmt.Fprintln(utils.GetDebugLogger(), appendError) + } + + renderError := table.Render() + if renderError != nil { + fmt.Fprintln(utils.GetDebugLogger(), renderError) + } }