From 1f678a4c74b1391c177592ad58681cf458890306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Titsworth-Morin?= Date: Tue, 5 May 2026 12:42:32 +0000 Subject: [PATCH 1/2] feat(cli): update tier display names for V5 pricing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename user-facing tier strings to match V5 pricing: - HOBBY → Starter - PERSONAL → Starter (Legacy) - PRO → Pro (unchanged) - TEAM → Enterprise - EXPIRED → Expired (new) Update error messages, deployment mode descriptions, and command help text to reference new tier names and pricing. Proto enum values remain unchanged for backward compatibility. Co-Authored-By: Claude Opus 4.6 --- src/cmd/cli/command/debug.go | 2 +- src/cmd/cli/command/estimate_test.go | 1 + src/cmd/cli/command/generate.go | 2 +- src/cmd/cli/command/whoami.go | 4 ---- src/pkg/cli/client/byoc/baseclient.go | 2 +- src/pkg/cli/estimate.go | 3 +++ src/pkg/cli/whoami.go | 21 ++++++++++----------- src/pkg/cli/whoami_test.go | 2 +- src/pkg/utils.go | 8 +++++--- 9 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/cmd/cli/command/debug.go b/src/cmd/cli/command/debug.go index 22f6cd730..c4c077b39 100644 --- a/src/cmd/cli/command/debug.go +++ b/src/cmd/cli/command/debug.go @@ -13,7 +13,7 @@ var debugCmd = &cobra.Command{ Use: "debug [SERVICE...]", Annotations: authNeededAlways, Hidden: true, - Short: "Debug a build, deployment, or service failure", + Short: "Debug a build, deployment, or service failure (Pro subscription required for full debugging)", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() etag, _ := cmd.Flags().GetString("etag") diff --git a/src/cmd/cli/command/estimate_test.go b/src/cmd/cli/command/estimate_test.go index a0161bb33..5f71a4120 100644 --- a/src/cmd/cli/command/estimate_test.go +++ b/src/cmd/cli/command/estimate_test.go @@ -129,6 +129,7 @@ func TestPrintEstimate(t *testing.T) { expectedOutput := ` Estimate for Deployment Mode: AFFORDABLE +Available on all tiers. This mode is optimized for low cost and rapid iteration. Your application will be deployed with spot instances. Databases will be provisioned using resources optimized for burstable memory. Deployments are replaced entirely on diff --git a/src/cmd/cli/command/generate.go b/src/cmd/cli/command/generate.go index aabef1818..f7254836e 100644 --- a/src/cmd/cli/command/generate.go +++ b/src/cmd/cli/command/generate.go @@ -19,7 +19,7 @@ var generateCmd = &cobra.Command{ Use: "generate", Args: cobra.MaximumNArgs(1), Aliases: []string{"gen"}, - Short: "Generate a sample Defang project", + Short: "Generate a sample Defang project (Starter: limited generations/month, Pro: unlimited)", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/src/cmd/cli/command/whoami.go b/src/cmd/cli/command/whoami.go index 96353fb31..1e21e5bff 100644 --- a/src/cmd/cli/command/whoami.go +++ b/src/cmd/cli/command/whoami.go @@ -5,7 +5,6 @@ import ( "github.com/DefangLabs/defang/src/pkg/cli" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/term" - defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "github.com/spf13/cobra" ) @@ -49,9 +48,6 @@ var whoamiCmd = &cobra.Command{ if !global.Verbose { data.Tenant = "" data.TenantID = "" - if data.SubscriberTier == defangv1.SubscriptionTier_SUBSCRIPTION_TIER_UNSPECIFIED { - data.SubscriberTier = defangv1.SubscriptionTier_HOBBY // don't show "SUBSCRIPTION_TIER_UNSPECIFIED" - } } cols := []string{ diff --git a/src/pkg/cli/client/byoc/baseclient.go b/src/pkg/cli/client/byoc/baseclient.go index ca816b12d..96ee44894 100644 --- a/src/pkg/cli/client/byoc/baseclient.go +++ b/src/pkg/cli/client/byoc/baseclient.go @@ -122,7 +122,7 @@ func (e ErrNoPermission) Error() string { func (b *ByocBaseClient) GetServiceInfos(ctx context.Context, projectName, delegateDomain, etag string, services map[string]composeTypes.ServiceConfig) ([]*defangv1.ServiceInfo, error) { numGPUS := compose.GetNumOfGPUs(services) if numGPUS > 0 && !b.AllowGPU { - return nil, ErrNoPermission("usage of GPUs. Please upgrade on https://s.defang.io/subscription") + return nil, ErrNoPermission("GPU access requires a Pro subscription ($49/mo). Upgrade at https://portal.defang.io/pricing") } serviceInfoMap := make(map[string]*Node) diff --git a/src/pkg/cli/estimate.go b/src/pkg/cli/estimate.go index 4ea2ffd8f..334b98956 100644 --- a/src/pkg/cli/estimate.go +++ b/src/pkg/cli/estimate.go @@ -95,6 +95,7 @@ func GeneratePreview(ctx context.Context, project *compose.Project, client clien } var affordableModeEstimateSummary = ` +Available on all tiers. This mode is optimized for low cost and rapid iteration. Your application will be deployed with spot instances. Databases will be provisioned using resources optimized for burstable memory. Deployments are replaced entirely on @@ -103,12 +104,14 @@ Services will be exposed directly to the public internet for easy debugging. This mode emphasizes affordability over availability.` var balancedModeEstimateSummary = ` +Requires Pro ($49/mo). This mode strikes a balance between cost and availability. Your application will be deployed with spot instances. Databases will be provisioned using resources optimized for production. Services in the "internal" network will be deployed to a private subnet with a NAT gateway for outbound internet access.` var highAvailabilityModeEstimateSummary = ` +Requires Enterprise ($499/mo). This mode prioritizes availability. Your application will be deployed with on-demand instances in multiple availability zones. Databases will be provisioned using resources optimized for production. diff --git a/src/pkg/cli/whoami.go b/src/pkg/cli/whoami.go index 6e17e0b1c..77b224ee1 100644 --- a/src/pkg/cli/whoami.go +++ b/src/pkg/cli/whoami.go @@ -3,23 +3,22 @@ package cli import ( "context" + "github.com/DefangLabs/defang/src/pkg" "github.com/DefangLabs/defang/src/pkg/auth" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/term" "github.com/DefangLabs/defang/src/pkg/types" - - defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" ) type ShowAccountData struct { - Provider client.ProviderID `json:"provider"` - SubscriberTier defangv1.SubscriptionTier `json:"subscriberTier"` - Region string `json:"region"` - Workspace string `json:"workspace"` - Tenant string `json:"tenant,omitempty"` // this is the subdomain - TenantID string `json:"tenantId,omitempty"` - Email string `json:"email"` - Name string `json:"name"` + Provider client.ProviderID `json:"provider"` + SubscriberTier string `json:"subscriberTier"` + Region string `json:"region"` + Workspace string `json:"workspace"` + Tenant string `json:"tenant,omitempty"` // this is the subdomain + TenantID string `json:"tenantId,omitempty"` + Email string `json:"email"` + Name string `json:"name"` } func Whoami(ctx context.Context, fabric client.FabricClient, maybeProvider client.Provider, userInfo *auth.UserInfo, tenantSelection types.TenantNameOrID) (ShowAccountData, error) { @@ -36,7 +35,7 @@ func Whoami(ctx context.Context, fabric client.FabricClient, maybeProvider clien term.Debug("User ID: " + resp.UserId) showData := ShowAccountData{ Region: resp.Region, - SubscriberTier: resp.Tier, + SubscriberTier: pkg.SubscriptionTierToString(resp.Tier), Tenant: resp.Tenant, TenantID: resp.TenantId, Workspace: ResolveWorkspaceName(userInfo, tenantSelection), diff --git a/src/pkg/cli/whoami_test.go b/src/pkg/cli/whoami_test.go index 4d95c2eb7..28880ebc1 100644 --- a/src/pkg/cli/whoami_test.go +++ b/src/pkg/cli/whoami_test.go @@ -58,7 +58,7 @@ func TestWhoami(t *testing.T) { want := ShowAccountData{ Provider: client.ProviderDefang, - SubscriberTier: defangv1.SubscriptionTier_PRO, + SubscriberTier: "Pro", Region: "us-west-2", Workspace: "Tenant One", Tenant: "tenant-1", diff --git a/src/pkg/utils.go b/src/pkg/utils.go index 8c8926f75..3710747ee 100644 --- a/src/pkg/utils.go +++ b/src/pkg/utils.go @@ -134,13 +134,15 @@ func SubscriptionTierToString(tier defangv1.SubscriptionTier) string { case defangv1.SubscriptionTier_SUBSCRIPTION_TIER_UNSPECIFIED: fallthrough // free tier case defangv1.SubscriptionTier_HOBBY: - return "Hobby" + return "Starter" case defangv1.SubscriptionTier_PERSONAL: - return "Personal" + return "Starter (Legacy)" case defangv1.SubscriptionTier_PRO: return "Pro" case defangv1.SubscriptionTier_TEAM: - return "Team" + return "Enterprise" + case defangv1.SubscriptionTier_EXPIRED: + return "Expired" default: return "Unknown" } From 25337810ed96e469fa56385c977a9a5dca95e778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Titsworth-Morin?= Date: Thu, 7 May 2026 09:07:02 +0000 Subject: [PATCH 2/2] test(cli): cover V5 tier display names --- src/pkg/utils_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/pkg/utils_test.go b/src/pkg/utils_test.go index 494d8bd7b..89e55978c 100644 --- a/src/pkg/utils_test.go +++ b/src/pkg/utils_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -23,6 +24,30 @@ func TestGetenvBool(t *testing.T) { } } +func TestSubscriptionTierToString(t *testing.T) { + tests := []struct { + name string + tier defangv1.SubscriptionTier + want string + }{ + {"unspecified", defangv1.SubscriptionTier_SUBSCRIPTION_TIER_UNSPECIFIED, "Starter"}, + {"hobby", defangv1.SubscriptionTier_HOBBY, "Starter"}, + {"personal", defangv1.SubscriptionTier_PERSONAL, "Starter (Legacy)"}, + {"pro", defangv1.SubscriptionTier_PRO, "Pro"}, + {"team", defangv1.SubscriptionTier_TEAM, "Enterprise"}, + {"expired", defangv1.SubscriptionTier_EXPIRED, "Expired"}, + {"unknown", defangv1.SubscriptionTier(999), "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := SubscriptionTierToString(tt.tier); got != tt.want { + t.Errorf("SubscriptionTierToString(%v) = %q, want %q", tt.tier, got, tt.want) + } + }) + } +} + func TestIsValidServiceName(t *testing.T) { tests := []struct { name string