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/6] 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/6] 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 From 7f12a7cb086c44c416456bd1614e99545bed828c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Titsworth-Morin?= Date: Fri, 15 May 2026 15:07:52 +0000 Subject: [PATCH 3/6] fix: remove hardcoded prices from CLI output, use consistent s.defang.io URL - Remove dollar amounts from estimate mode summaries and GPU error - Use s.defang.io/subscription consistently (matching Fabric) - Prices change; tier names are stable Co-Authored-By: Claude Opus 4.6 --- src/cmd/cli/command/generate.go | 2 +- src/pkg/cli/client/byoc/baseclient.go | 2 +- src/pkg/cli/estimate.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cmd/cli/command/generate.go b/src/cmd/cli/command/generate.go index f7254836e..c2736640f 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 (Starter: limited generations/month, Pro: unlimited)", + Short: "Generate a sample Defang project (Starter: limited, Pro: unlimited)", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/src/pkg/cli/client/byoc/baseclient.go b/src/pkg/cli/client/byoc/baseclient.go index 96ee44894..7973f0735 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("GPU access requires a Pro subscription ($49/mo). Upgrade at https://portal.defang.io/pricing") + return nil, ErrNoPermission("GPU access requires a Pro subscription. Upgrade at https://s.defang.io/subscription") } serviceInfoMap := make(map[string]*Node) diff --git a/src/pkg/cli/estimate.go b/src/pkg/cli/estimate.go index 334b98956..705fb1e28 100644 --- a/src/pkg/cli/estimate.go +++ b/src/pkg/cli/estimate.go @@ -104,14 +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). +Requires Pro tier. 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). +Requires Enterprise tier. 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. From 8d6386190905bfe34ce789f2cdc8bc3bd00ee28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Titsworth-Morin?= Date: Fri, 15 May 2026 18:36:24 +0200 Subject: [PATCH 4/6] Update debug command short description Removed mention of Pro subscription requirement from debug command description. --- src/cmd/cli/command/debug.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/cli/command/debug.go b/src/cmd/cli/command/debug.go index c4c077b39..22f6cd730 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 (Pro subscription required for full debugging)", + Short: "Debug a build, deployment, or service failure", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() etag, _ := cmd.Flags().GetString("etag") From e499c05447d09f1ea14f0e52f0a8eea10bb86eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Titsworth-Morin?= Date: Fri, 15 May 2026 18:38:15 +0200 Subject: [PATCH 5/6] Simplify command description for generate command --- src/cmd/cli/command/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/cli/command/generate.go b/src/cmd/cli/command/generate.go index c2736640f..aabef1818 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 (Starter: limited, Pro: unlimited)", + Short: "Generate a sample Defang project", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() From dfeae445d38e255315fe35a4d48fb2cc49cfde29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Titsworth-Morin?= Date: Fri, 15 May 2026 16:48:51 +0000 Subject: [PATCH 6/6] fix: rename PERSONAL tier display to "Personal (Legacy)" The legacy tier was called Personal, not Starter. Co-Authored-By: Claude Opus 4.6 --- src/pkg/utils.go | 2 +- src/pkg/utils_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pkg/utils.go b/src/pkg/utils.go index 3710747ee..29af7b4fb 100644 --- a/src/pkg/utils.go +++ b/src/pkg/utils.go @@ -136,7 +136,7 @@ func SubscriptionTierToString(tier defangv1.SubscriptionTier) string { case defangv1.SubscriptionTier_HOBBY: return "Starter" case defangv1.SubscriptionTier_PERSONAL: - return "Starter (Legacy)" + return "Personal (Legacy)" case defangv1.SubscriptionTier_PRO: return "Pro" case defangv1.SubscriptionTier_TEAM: diff --git a/src/pkg/utils_test.go b/src/pkg/utils_test.go index 89e55978c..19ea970b5 100644 --- a/src/pkg/utils_test.go +++ b/src/pkg/utils_test.go @@ -32,7 +32,7 @@ func TestSubscriptionTierToString(t *testing.T) { }{ {"unspecified", defangv1.SubscriptionTier_SUBSCRIPTION_TIER_UNSPECIFIED, "Starter"}, {"hobby", defangv1.SubscriptionTier_HOBBY, "Starter"}, - {"personal", defangv1.SubscriptionTier_PERSONAL, "Starter (Legacy)"}, + {"personal", defangv1.SubscriptionTier_PERSONAL, "Personal (Legacy)"}, {"pro", defangv1.SubscriptionTier_PRO, "Pro"}, {"team", defangv1.SubscriptionTier_TEAM, "Enterprise"}, {"expired", defangv1.SubscriptionTier_EXPIRED, "Expired"},