From f3bdba46546dba0dac75a77a36a05a8e5038f722 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:59:38 +0530 Subject: [PATCH 01/51] Add logs collector enable flag to kubernetes command Added a flag to enable or disable logs collection for the Civo platform dashboard. --- cmd/kubernetes/kubernetes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/kubernetes/kubernetes.go b/cmd/kubernetes/kubernetes.go index 1da9d0de..48c880e7 100644 --- a/cmd/kubernetes/kubernetes.go +++ b/cmd/kubernetes/kubernetes.go @@ -104,6 +104,7 @@ func init() { kubernetesCreateCmd.Flags().StringVarP(&cniPlugin, "cni-plugin", "p", "flannel", "optional, possible options: flannel,cilium.") kubernetesCreateCmd.Flags().StringVarP(&clusterType, "cluster-type", "", "k3s", "optional, possible options: k3s,talos.") kubernetesCreateCmd.Flags().StringVar(&volumeType, "volume-type", "", "optional, volume-type name as returned by 'civo volumetypes ls'") + kubernetesCreateCmd.Flags().BoolVar(&logsCollectorEnabled, "logs-collector-enabled", true, "optional, disabling this will prevent you from seeing logs in the Civo platform dashboard, but it will save node resources. Disabled by default on small and xsmall standard nodes") kubernetesRenameCmd.Flags().StringVarP(&kubernetesNewName, "name", "n", "", "the new name for the cluster.") From b54cc6bf3f51cf513500723c16278670265c1ba4 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:00:34 +0530 Subject: [PATCH 02/51] Enable logs collector in Kubernetes create command Added logs collector feature to Kubernetes creation command. --- cmd/kubernetes/kubernetes_create.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmd/kubernetes/kubernetes_create.go b/cmd/kubernetes/kubernetes_create.go index 53d3402f..3ad1f024 100644 --- a/cmd/kubernetes/kubernetes_create.go +++ b/cmd/kubernetes/kubernetes_create.go @@ -18,6 +18,7 @@ var numTargetNodes int var rulesFirewall string var waitKubernetes, saveConfigKubernetes, mergeConfigKubernetes, switchConfigKubernetes, createFirewall bool var kubernetesVersion, targetNodesSize, clusterName, clusterType, applications, removeapplications, networkID, existingFirewall, cniPlugin, volumeType string +var logsCollectorEnabled bool var kubernetesCluster *civogo.KubernetesCluster var kubernetesCreateCmdExample = `civo kubernetes create CLUSTER_NAME [flags] @@ -140,6 +141,16 @@ var kubernetesCreateCmd = &cobra.Command{ CNIPlugin: cni, } + logsCollectorEnabledValue, logsCollectorMessage := utility.ResolveLogsCollectorEnabled( + targetNodesSize, + cmd.Flags().Changed("logs-collector-enabled"), + logsCollectorEnabled, + ) + configKubernetes.LogsCollectorEnabled = logsCollectorEnabledValue + if logsCollectorMessage != "" { + utility.Info(logsCollectorMessage) + } + if rulesFirewall != "default" && !createFirewall { utility.Error("You can't use --firewall-rules without --create-firewall flag") os.Exit(1) From 188ad7021e6c04ee3a69f17ea95d6ceb9c13636f Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:02:49 +0530 Subject: [PATCH 03/51] Add functions to handle logs collector settings --- utility/kubernetes.go | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/utility/kubernetes.go b/utility/kubernetes.go index 2378ec2c..aa818c9b 100644 --- a/utility/kubernetes.go +++ b/utility/kubernetes.go @@ -326,3 +326,56 @@ func SizeType(size string) string { return "Instance" } } + +// IsStandardSmallOrXSmallKubeSize returns true when the given node size is a +// "standard" tier (the "g4s" family) small or xsmall Kubernetes node, e.g. +// "g4s.kube.small" or "g4s.kube.xsmall". This works dynamically off the size +// name so it doesn't need updating if new standard node sizes are added - +// only small/xsmall standard nodes qualify, matching the resource-saving +// default requested for those specific sizes. +func IsStandardSmallOrXSmallKubeSize(size string) bool { + if !strings.HasPrefix(size, "g4s.") { + return false + } + + parts := strings.Split(size, ".") + if len(parts) == 0 { + return false + } + + switch parts[len(parts)-1] { + case "small", "xsmall": + return true + default: + return false + } +} + +// LogsCollectorDisabledMessage is shown to the user when the logs collector +// is automatically disabled on a standard small/xsmall node, so they know why +// and how to override it. +const LogsCollectorDisabledMessage = "Logs collector not installed by default to save resource on small and xsmall standard nodes. If you wish to enable logs collector create the cluster with LogsCollectorEnable set to true" + +// ResolveLogsCollectorEnabled works out the value that should be sent to the +// API for a cluster's LogsCollectorEnabled setting, and an informational +// message to display to the user (empty if there's nothing to say). +// +// - If the user explicitly passed --logs-collector-enabled, their choice is +// always respected, regardless of node size. +// - Otherwise, the logs collector is disabled by default on standard small +// and xsmall nodes to save node resources. +// - Otherwise, nil is returned so the API's own default applies (currently +// enabled). +func ResolveLogsCollectorEnabled(size string, explicitlySet bool, value bool) (enabled *bool, message string) { + if explicitlySet { + v := value + return &v, "" + } + + if IsStandardSmallOrXSmallKubeSize(size) { + v := false + return &v, LogsCollectorDisabledMessage + } + + return nil, "" +} From 13d86f47f651dcef4c273b54cb319d7b2378ebde Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:03:21 +0530 Subject: [PATCH 04/51] Add tests for Kubernetes size and logs collector --- utility/kubernetes_test.go | 106 +++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/utility/kubernetes_test.go b/utility/kubernetes_test.go index a7939ef6..20037ec1 100644 --- a/utility/kubernetes_test.go +++ b/utility/kubernetes_test.go @@ -119,3 +119,109 @@ func TestIsAppCompatibleWithClusterType(t *testing.T) { }) } } + +func TestIsStandardSmallOrXSmallKubeSize(t *testing.T) { + tests := []struct { + name string + size string + want bool + }{ + {name: "standard xsmall", size: "g4s.kube.xsmall", want: true}, + {name: "standard small", size: "g4s.kube.small", want: true}, + {name: "standard medium is not small/xsmall", size: "g4s.kube.medium", want: false}, + {name: "standard large is not small/xsmall", size: "g4s.kube.large", want: false}, + {name: "performance small is not standard tier", size: "g4p.kube.small", want: false}, + {name: "ram-optimized small is not standard tier", size: "g4m.kube.small", want: false}, + {name: "cpu-optimized small is not standard tier", size: "g4c.kube.small", want: false}, + {name: "legacy non-kube instance size", size: "g3.xsmall", want: false}, + {name: "empty size", size: "", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsStandardSmallOrXSmallKubeSize(tt.size); got != tt.want { + t.Errorf("IsStandardSmallOrXSmallKubeSize(%q) = %v, want %v", tt.size, got, tt.want) + } + }) + } +} + +func TestResolveLogsCollectorEnabled(t *testing.T) { + tests := []struct { + name string + size string + explicitlySet bool + value bool + wantEnabled *bool + wantMessage string + }{ + { + name: "explicit true is respected even on xsmall", + size: "g4s.kube.xsmall", + explicitlySet: true, + value: true, + wantEnabled: boolPtr(true), + wantMessage: "", + }, + { + name: "explicit false is respected even on a large node", + size: "g4s.kube.large", + explicitlySet: true, + value: false, + wantEnabled: boolPtr(false), + wantMessage: "", + }, + { + name: "auto-disabled on standard small when not explicit", + size: "g4s.kube.small", + explicitlySet: false, + value: true, + wantEnabled: boolPtr(false), + wantMessage: LogsCollectorDisabledMessage, + }, + { + name: "auto-disabled on standard xsmall when not explicit", + size: "g4s.kube.xsmall", + explicitlySet: false, + value: true, + wantEnabled: boolPtr(false), + wantMessage: LogsCollectorDisabledMessage, + }, + { + name: "left to API default on standard medium when not explicit", + size: "g4s.kube.medium", + explicitlySet: false, + value: true, + wantEnabled: nil, + wantMessage: "", + }, + { + name: "performance-tier small is not auto-disabled", + size: "g4p.kube.small", + explicitlySet: false, + value: true, + wantEnabled: nil, + wantMessage: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotEnabled, gotMessage := ResolveLogsCollectorEnabled(tt.size, tt.explicitlySet, tt.value) + + if (gotEnabled == nil) != (tt.wantEnabled == nil) { + t.Fatalf("ResolveLogsCollectorEnabled(%q, %v, %v) enabled = %v, want %v", tt.size, tt.explicitlySet, tt.value, gotEnabled, tt.wantEnabled) + } + if gotEnabled != nil && *gotEnabled != *tt.wantEnabled { + t.Errorf("ResolveLogsCollectorEnabled(%q, %v, %v) enabled = %v, want %v", tt.size, tt.explicitlySet, tt.value, *gotEnabled, *tt.wantEnabled) + } + if gotMessage != tt.wantMessage { + t.Errorf("ResolveLogsCollectorEnabled(%q, %v, %v) message = %q, want %q", tt.size, tt.explicitlySet, tt.value, gotMessage, tt.wantMessage) + } + }) + } +} + +func boolPtr(b bool) *bool { + return &b +} From 97d1a659019c78d4feec5ee47f2baddca348fc96 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:45:09 +0530 Subject: [PATCH 05/51] Add 'quiet' flag for non-interactive mode Added a 'quiet' flag to suppress prompts and output. --- cmd/root.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/root.go b/cmd/root.go index 63e4a216..f84ecf24 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -142,6 +142,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&common.DefaultYes, "yes", "y", false, "Automatic yes to prompts; assume \"yes\" as answer to all prompts and run non-interactively") rootCmd.PersistentFlags().StringVarP(&common.RegionSet, "region", "", "", "Choose the region to connect to, if you use this option it will use it over the default region") rootCmd.PersistentFlags().BoolVarP(&common.PrettySet, "pretty", "", false, "Print pretty the json output") + rootCmd.PersistentFlags().BoolVarP(&common.Quiet, "quiet", "q", false, "Suppress interactive prompts, progress indicators and non-error output; useful for scripting. Prompts that would otherwise require confirmation will fail the command instead - combine with --yes to auto-confirm them") rootCmd.Flags().BoolVarP(&version, "version", "v", false, "Print the version of the CLI") rootCmd.AddCommand(apikey.APIKeyCmd) From 673bf043f3a6215b57600cca3f7d1984e25909eb Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:48:34 +0530 Subject: [PATCH 06/51] Change print statement to utility error logging --- cmd/apikey/apikey_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/apikey/apikey_remove.go b/cmd/apikey/apikey_remove.go index afd0ea51..34cc79d2 100644 --- a/cmd/apikey/apikey_remove.go +++ b/cmd/apikey/apikey_remove.go @@ -58,7 +58,7 @@ var apikeyRemoveCmd = &cobra.Command{ os.Exit(1) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, From cfa21c333e2249928a8b28901f5746ed0c072321 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:49:26 +0530 Subject: [PATCH 07/51] Require name and API key when --quiet is set Add error handling for missing name and API key in quiet mode. --- cmd/apikey/apikey_save.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/apikey/apikey_save.go b/cmd/apikey/apikey_save.go index 08c3b8a1..2ffce744 100644 --- a/cmd/apikey/apikey_save.go +++ b/cmd/apikey/apikey_save.go @@ -60,6 +60,11 @@ var apikeySaveCmd = &cobra.Command{ } if len(args) == 0 && !loadAPIKeyFromEnv { + if common.Quiet { + utility.Error("name and API key are required when --quiet is set (interactive prompts are disabled); run 'civo apikey save NAME APIKEY' or use --load-from-env") + os.Exit(1) + } + reader := bufio.NewReader(os.Stdin) fmt.Printf("Enter a nice name for this account/API Key: ") @@ -140,7 +145,9 @@ var apikeySaveCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("Saved the API Key %s\n", utility.Green(name)) + if !common.Quiet { + fmt.Printf("Saved the API Key %s\n", utility.Green(name)) + } } }, From dcea5d29e63ebb1cff88725bf8dda0e7997dad0c Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:50:53 +0530 Subject: [PATCH 08/51] Change print to error logging on operation abort --- cmd/database/database_backup_delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/database/database_backup_delete.go b/cmd/database/database_backup_delete.go index f9010040..042e5a84 100644 --- a/cmd/database/database_backup_delete.go +++ b/cmd/database/database_backup_delete.go @@ -113,7 +113,7 @@ var dbBackupDeleteCmd = &cobra.Command{ pluralize.Has(len(backupList))) } } else { - fmt.Println("Operation aborted") + utility.Error("Operation aborted.") } }, } From 5cd7499fc8dde65fa005ee84fb33cc176c866161 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:51:46 +0530 Subject: [PATCH 09/51] Refactor database creation output and spinner usage --- cmd/database/database_create.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/database/database_create.go b/cmd/database/database_create.go index a0cd5aff..b7f21309 100644 --- a/cmd/database/database_create.go +++ b/cmd/database/database_create.go @@ -176,8 +176,7 @@ var dbCreateCmd = &cobra.Command{ startTime := utility.StartTime() stillCreating := true - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = fmt.Sprintf("Create a database called %s ", db.Name) s.Start() @@ -205,10 +204,12 @@ var dbCreateCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - if executionTime != "" { - fmt.Printf("Database (%s) type %s version %s with ID %s and size %s has been created in %s\nTo get fetch the database credentials use the command:\n\ncivo database credentials %s\n", utility.Green(db.Name), strings.ToLower(db.Software), db.SoftwareVersion, db.ID, db.Size, executionTime, db.Name) - } else { - fmt.Printf("Database (%s) type %s version %s with ID %s and size %s has been created\nTo get fetch the database credentials use the command:\n\ncivo database credentials %s\n", utility.Green(db.Name), strings.ToLower(db.Software), db.SoftwareVersion, db.ID, db.Size, db.Name) + if !common.Quiet { + if executionTime != "" { + fmt.Printf("Database (%s) type %s version %s with ID %s and size %s has been created in %s\nTo get fetch the database credentials use the command:\n\ncivo database credentials %s\n", utility.Green(db.Name), strings.ToLower(db.Software), db.SoftwareVersion, db.ID, db.Size, executionTime, db.Name) + } else { + fmt.Printf("Database (%s) type %s version %s with ID %s and size %s has been created\nTo get fetch the database credentials use the command:\n\ncivo database credentials %s\n", utility.Green(db.Name), strings.ToLower(db.Software), db.SoftwareVersion, db.ID, db.Size, db.Name) + } } } }, From 9e905292975f80a15a6402a44dc3e028bf95f0e8 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:52:42 +0530 Subject: [PATCH 10/51] Refactor database status check and spinner initialization --- cmd/database/database_credential.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/database/database_credential.go b/cmd/database/database_credential.go index a5c7676a..4cb34d42 100644 --- a/cmd/database/database_credential.go +++ b/cmd/database/database_credential.go @@ -46,10 +46,11 @@ var dbCredentialCmd = &cobra.Command{ // Add check for database status if db.Status == "Pending" { - fmt.Printf("The DB %s is currently being provisioned, please wait...\n", utility.Green(db.Name)) + if !common.Quiet { + fmt.Printf("The DB %s is currently being provisioned, please wait...\n", utility.Green(db.Name)) + } - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = fmt.Sprintf("Waiting for database (%s)... ", db.Name) s.Start() From 6a78d68f262dc0d751e1abbe5c7e74c3307c5a83 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:53:17 +0530 Subject: [PATCH 11/51] Replace fmt.Println with utility.Error for abort message --- cmd/database/database_delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/database/database_delete.go b/cmd/database/database_delete.go index eaad6568..2eaf5df6 100644 --- a/cmd/database/database_delete.go +++ b/cmd/database/database_delete.go @@ -102,7 +102,7 @@ var dbDeleteCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted") + utility.Error("Operation aborted.") } }, } From 52373130907dc7b807b283ec93a27feb5016290c Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:54:28 +0530 Subject: [PATCH 12/51] Replace fmt.Println with utility.Error for abort message --- cmd/domain/domain_record_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/domain/domain_record_remove.go b/cmd/domain/domain_record_remove.go index 22c346f0..6fea30dc 100644 --- a/cmd/domain/domain_record_remove.go +++ b/cmd/domain/domain_record_remove.go @@ -105,7 +105,7 @@ var domainRecordRemoveCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, From 5733c57b238291819d78788d67a74a4581a8c290 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:03:27 +0530 Subject: [PATCH 13/51] Add newline at end of domain_record_remove.go Fix missing newline at end of file From bdbaf0b62c129166e32032fd8e5432ef30d66a2d Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:03:55 +0530 Subject: [PATCH 14/51] Replace fmt.Println with utility.Error for abort message --- cmd/domain/domain_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/domain/domain_remove.go b/cmd/domain/domain_remove.go index c64e9fc7..df9b022f 100644 --- a/cmd/domain/domain_remove.go +++ b/cmd/domain/domain_remove.go @@ -93,7 +93,7 @@ var domainRemoveCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted") + utility.Error("Operation aborted.") } }, } From 656722a430af1a0546974fabd0e667634bdb664d Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:04:54 +0530 Subject: [PATCH 15/51] Refactor firewall command to handle rules removal --- cmd/firewall/firewall_remove.go | 73 +++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/cmd/firewall/firewall_remove.go b/cmd/firewall/firewall_remove.go index 9cca91f5..85820c68 100644 --- a/cmd/firewall/firewall_remove.go +++ b/cmd/firewall/firewall_remove.go @@ -16,13 +16,13 @@ import ( "github.com/spf13/cobra" ) -var firewallList []utility.Resource -var firewallRemoveCmd = &cobra.Command{ - Use: "remove [NAME]", - Aliases: []string{"rm", "delete", "destroy"}, - Example: "civo firewall remove NAME", - Short: "Remove a firewall", - Args: cobra.MinimumNArgs(1), +var firewallRuleList []utility.Resource +var firewallRuleRemoveCmd = &cobra.Command{ + Use: "remove", + Aliases: []string{"delete", "destroy", "rm"}, + Args: cobra.MinimumNArgs(2), + Short: "Remove firewall rule", + Example: "civo firewall rule remove FIREWALL_NAME/FIREWALL_ID FIREWALL_RULE_ID", Run: func(cmd *cobra.Command, args []string) { utility.EnsureCurrentRegion() @@ -35,54 +35,66 @@ var firewallRemoveCmd = &cobra.Command{ os.Exit(1) } - if len(args) == 1 { - firewall, err := client.FindFirewall(args[0]) + firewall, err := client.FindFirewall(args[0]) + if err != nil { + if errors.Is(err, civogo.ZeroMatchesError) { + utility.Error("sorry there is no %s firewall in your account", utility.Red(args[0])) + os.Exit(1) + } + if errors.Is(err, civogo.MultipleMatchesError) { + utility.Error("sorry we found more than one firewall with that name in your account") + os.Exit(1) + } + } + + if len(args) == 2 { + rule, err := client.FindFirewallRule(firewall.ID, args[1]) if err != nil { if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s firewall in your account", utility.Red(args[0])) + utility.Error("sorry there is no %s firewall rule in your account", utility.Red(args[1])) os.Exit(1) } if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one firewall with that name in your account") + utility.Error("sorry we found more than one firewall rule in your account") os.Exit(1) } } - firewallList = append(firewallList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) + firewallRuleList = append(firewallRuleList, utility.Resource{ID: rule.ID, Name: rule.Label}) } else { - for _, v := range args { - firewall, err := client.FindFirewall(v) + for _, v := range args[1:] { + rule, err := client.FindFirewallRule(firewall.ID, v) if err == nil { - firewallList = append(firewallList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) + firewallRuleList = append(firewallRuleList, utility.Resource{ID: rule.ID, Name: rule.Label}) } } } - firewallNameList := []string{} - for _, v := range firewallList { - firewallNameList = append(firewallNameList, v.Name) + firewallRuleNameList := []string{} + for _, v := range firewallRuleList { + firewallRuleNameList = append(firewallRuleNameList, v.Name) } - if utility.UserConfirmedDeletion(pluralize.Pluralize(len(firewallList), "firewall"), common.DefaultYes, strings.Join(firewallNameList, ", ")) { + if utility.UserConfirmedDeletion(fmt.Sprintf("firewall %s", pluralize.Pluralize(len(firewallRuleList), "rule")), common.DefaultYes, strings.Join(firewallRuleNameList, ", ")) { - for _, v := range firewallList { - _, err = client.DeleteFirewall(v.ID) + for _, v := range firewallRuleList { + _, err = client.DeleteFirewallRule(firewall.ID, v.ID) if err != nil { - utility.Error("error deleting the firewall: %s", err) + utility.Error("error deleting the firewall rule: %s", err) os.Exit(1) } } ow := utility.NewOutputWriter() - for _, v := range firewallList { + for _, v := range firewallRuleList { ow.StartLine() ow.AppendDataWithLabel("id", v.ID, "ID") - ow.AppendDataWithLabel("name", v.Name, "Name") + ow.AppendDataWithLabel("label", v.Name, "Label") } switch common.OutputFormat { case "json": - if len(firewallList) == 1 { + if len(firewallRuleList) == 1 { ow.WriteSingleObjectJSON(common.PrettySet) } else { ow.WriteMultipleObjectsJSON(common.PrettySet) @@ -90,14 +102,15 @@ var firewallRemoveCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("The %s (%s) %s been deleted\n", - pluralize.Pluralize(len(firewallList), "firewall"), - utility.Green(strings.Join(firewallNameList, ", ")), - pluralize.Has(len(firewallList)), + fmt.Printf("The firewall %s (%s) %s been deleted\n", + pluralize.Pluralize(len(firewallRuleList), "rule"), + strings.Join(firewallRuleNameList, ", "), + pluralize.Has(len(firewallRuleList)), ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } + }, } From 015a777be753451a8bd08e8f58270d2e35c6e1a5 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:05:34 +0530 Subject: [PATCH 16/51] Refactor firewall removal command to handle single firewall --- cmd/firewall/firewall_remove.go | 71 ++++++++++++++------------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/cmd/firewall/firewall_remove.go b/cmd/firewall/firewall_remove.go index 85820c68..b3f180b8 100644 --- a/cmd/firewall/firewall_remove.go +++ b/cmd/firewall/firewall_remove.go @@ -16,13 +16,13 @@ import ( "github.com/spf13/cobra" ) -var firewallRuleList []utility.Resource -var firewallRuleRemoveCmd = &cobra.Command{ - Use: "remove", - Aliases: []string{"delete", "destroy", "rm"}, - Args: cobra.MinimumNArgs(2), - Short: "Remove firewall rule", - Example: "civo firewall rule remove FIREWALL_NAME/FIREWALL_ID FIREWALL_RULE_ID", +var firewallList []utility.Resource +var firewallRemoveCmd = &cobra.Command{ + Use: "remove [NAME]", + Aliases: []string{"rm", "delete", "destroy"}, + Example: "civo firewall remove NAME", + Short: "Remove a firewall", + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { utility.EnsureCurrentRegion() @@ -35,66 +35,54 @@ var firewallRuleRemoveCmd = &cobra.Command{ os.Exit(1) } - firewall, err := client.FindFirewall(args[0]) - if err != nil { - if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s firewall in your account", utility.Red(args[0])) - os.Exit(1) - } - if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one firewall with that name in your account") - os.Exit(1) - } - } - - if len(args) == 2 { - rule, err := client.FindFirewallRule(firewall.ID, args[1]) + if len(args) == 1 { + firewall, err := client.FindFirewall(args[0]) if err != nil { if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s firewall rule in your account", utility.Red(args[1])) + utility.Error("sorry there is no %s firewall in your account", utility.Red(args[0])) os.Exit(1) } if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one firewall rule in your account") + utility.Error("sorry we found more than one firewall with that name in your account") os.Exit(1) } } - firewallRuleList = append(firewallRuleList, utility.Resource{ID: rule.ID, Name: rule.Label}) + firewallList = append(firewallList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) } else { - for _, v := range args[1:] { - rule, err := client.FindFirewallRule(firewall.ID, v) + for _, v := range args { + firewall, err := client.FindFirewall(v) if err == nil { - firewallRuleList = append(firewallRuleList, utility.Resource{ID: rule.ID, Name: rule.Label}) + firewallList = append(firewallList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) } } } - firewallRuleNameList := []string{} - for _, v := range firewallRuleList { - firewallRuleNameList = append(firewallRuleNameList, v.Name) + firewallNameList := []string{} + for _, v := range firewallList { + firewallNameList = append(firewallNameList, v.Name) } - if utility.UserConfirmedDeletion(fmt.Sprintf("firewall %s", pluralize.Pluralize(len(firewallRuleList), "rule")), common.DefaultYes, strings.Join(firewallRuleNameList, ", ")) { + if utility.UserConfirmedDeletion(pluralize.Pluralize(len(firewallList), "firewall"), common.DefaultYes, strings.Join(firewallNameList, ", ")) { - for _, v := range firewallRuleList { - _, err = client.DeleteFirewallRule(firewall.ID, v.ID) + for _, v := range firewallList { + _, err = client.DeleteFirewall(v.ID) if err != nil { - utility.Error("error deleting the firewall rule: %s", err) + utility.Error("error deleting the firewall: %s", err) os.Exit(1) } } ow := utility.NewOutputWriter() - for _, v := range firewallRuleList { + for _, v := range firewallList { ow.StartLine() ow.AppendDataWithLabel("id", v.ID, "ID") - ow.AppendDataWithLabel("label", v.Name, "Label") + ow.AppendDataWithLabel("name", v.Name, "Name") } switch common.OutputFormat { case "json": - if len(firewallRuleList) == 1 { + if len(firewallList) == 1 { ow.WriteSingleObjectJSON(common.PrettySet) } else { ow.WriteMultipleObjectsJSON(common.PrettySet) @@ -102,15 +90,14 @@ var firewallRuleRemoveCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("The firewall %s (%s) %s been deleted\n", - pluralize.Pluralize(len(firewallRuleList), "rule"), - strings.Join(firewallRuleNameList, ", "), - pluralize.Has(len(firewallRuleList)), + fmt.Printf("The %s (%s) %s been deleted\n", + pluralize.Pluralize(len(firewallList), "firewall"), + utility.Green(strings.Join(firewallNameList, ", ")), + pluralize.Has(len(firewallList)), ) } } else { utility.Error("Operation aborted.") } - }, } From 13f601fa1b4d2575d6460a13978e1dbc4853b0b1 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:06:10 +0530 Subject: [PATCH 17/51] Replace fmt.Println with utility.Error for abort message --- cmd/firewall/firewall_rule_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/firewall/firewall_rule_remove.go b/cmd/firewall/firewall_rule_remove.go index cf6a2311..85820c68 100644 --- a/cmd/firewall/firewall_rule_remove.go +++ b/cmd/firewall/firewall_rule_remove.go @@ -109,7 +109,7 @@ var firewallRuleRemoveCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, From 6630a3978cd148b62c3d9525671489fb59c44ff8 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:07:29 +0530 Subject: [PATCH 18/51] Refactor spinner and enhance output handling Replace spinner initialization with utility.NewSpinner and add quiet mode check for output. --- cmd/instance/instance_stop.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/instance/instance_stop.go b/cmd/instance/instance_stop.go index a33bcbd9..00f4f3f6 100644 --- a/cmd/instance/instance_stop.go +++ b/cmd/instance/instance_stop.go @@ -50,8 +50,7 @@ If you wish to use a custom format, the available fields are: if waitStop { stillStopping := true - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = "Stopping instance... " s.Start() @@ -71,7 +70,9 @@ If you wish to use a custom format, the available fields are: } if common.OutputFormat == common.OutputFormatHuman { - fmt.Printf("The instance %s (%s) is being stopped\n", utility.Green(instance.Hostname), instance.ID) + if !common.Quiet { + fmt.Printf("The instance %s (%s) is being stopped\n", utility.Green(instance.Hostname), instance.ID) + } } else { ow := utility.NewOutputWriter() ow.StartLine() From b4ade33c55ed2e5e0f5d823c2566eb0d0f917f1d Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:07:53 +0530 Subject: [PATCH 19/51] Refactor spinner and output handling in instance creation --- cmd/instance/instance_create.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/instance/instance_create.go b/cmd/instance/instance_create.go index 13e56c7a..f231152e 100644 --- a/cmd/instance/instance_create.go +++ b/cmd/instance/instance_create.go @@ -300,8 +300,7 @@ If you wish to use a custom format, the available fields are: if wait { stillCreating := true - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = fmt.Sprintf("Creating instance (%s)... ", resp.Hostname) s.Start() @@ -331,10 +330,12 @@ If you wish to use a custom format, the available fields are: } if common.OutputFormat == common.OutputFormatHuman { - if executionTime != "" { - fmt.Printf("The instance %s %s has been created in %s\n", utility.Green(instance.Hostname), publicIP, executionTime) - } else { - fmt.Printf("The instance %s has been created\n", utility.Green(instance.Hostname)) + if !common.Quiet { + if executionTime != "" { + fmt.Printf("The instance %s %s has been created in %s\n", utility.Green(instance.Hostname), publicIP, executionTime) + } else { + fmt.Printf("The instance %s has been created\n", utility.Green(instance.Hostname)) + } } } else { ow := utility.NewOutputWriter() From 51b79bcd1a0ad66207cb411ca47ffae5ac98cb63 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:08:20 +0530 Subject: [PATCH 20/51] Replace fmt.Println with utility.Error for abort message --- cmd/instance/instance_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/instance/instance_remove.go b/cmd/instance/instance_remove.go index 688a1bf0..34619512 100644 --- a/cmd/instance/instance_remove.go +++ b/cmd/instance/instance_remove.go @@ -104,7 +104,7 @@ If you wish to use a custom format, the available fields are: ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From dbe96f5b1bc884ca73f0dc549b3a45f740215bcb Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:09:57 +0530 Subject: [PATCH 21/51] Replace fmt.Println with utility.Error for abort message --- cmd/ip/ip_delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ip/ip_delete.go b/cmd/ip/ip_delete.go index ef1675cf..2526a5d4 100644 --- a/cmd/ip/ip_delete.go +++ b/cmd/ip/ip_delete.go @@ -66,7 +66,7 @@ Please make sure to delete your domains aren't pointed to it before deleting it. fmt.Printf("IP called %s with ID %s was deleted\n", utility.Green(ip.Name), utility.Green(ip.ID)) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From b33bcd9285fe85ebd21aa4353030f0fccacdc976 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:11:56 +0530 Subject: [PATCH 22/51] Change abort message to use utility.Error --- cmd/kubernetes/kubernetes_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubernetes/kubernetes_config.go b/cmd/kubernetes/kubernetes_config.go index feca5750..7660ee42 100644 --- a/cmd/kubernetes/kubernetes_config.go +++ b/cmd/kubernetes/kubernetes_config.go @@ -93,7 +93,7 @@ If you wish to use a custom format, the available fields are: os.Exit(1) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") os.Exit(1) } } else { From 5ca4b90e4d0ada63bbebb5272d970bb9e3781940 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:12:25 +0530 Subject: [PATCH 23/51] Refactor kubernetes_create.go to remove logs collector Removed logs collector functionality and updated spinner usage. --- cmd/kubernetes/kubernetes_create.go | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/cmd/kubernetes/kubernetes_create.go b/cmd/kubernetes/kubernetes_create.go index 3ad1f024..cbe7c7b4 100644 --- a/cmd/kubernetes/kubernetes_create.go +++ b/cmd/kubernetes/kubernetes_create.go @@ -18,7 +18,6 @@ var numTargetNodes int var rulesFirewall string var waitKubernetes, saveConfigKubernetes, mergeConfigKubernetes, switchConfigKubernetes, createFirewall bool var kubernetesVersion, targetNodesSize, clusterName, clusterType, applications, removeapplications, networkID, existingFirewall, cniPlugin, volumeType string -var logsCollectorEnabled bool var kubernetesCluster *civogo.KubernetesCluster var kubernetesCreateCmdExample = `civo kubernetes create CLUSTER_NAME [flags] @@ -141,16 +140,6 @@ var kubernetesCreateCmd = &cobra.Command{ CNIPlugin: cni, } - logsCollectorEnabledValue, logsCollectorMessage := utility.ResolveLogsCollectorEnabled( - targetNodesSize, - cmd.Flags().Changed("logs-collector-enabled"), - logsCollectorEnabled, - ) - configKubernetes.LogsCollectorEnabled = logsCollectorEnabledValue - if logsCollectorMessage != "" { - utility.Info(logsCollectorMessage) - } - if rulesFirewall != "default" && !createFirewall { utility.Error("You can't use --firewall-rules without --create-firewall flag") os.Exit(1) @@ -223,7 +212,7 @@ var kubernetesCreateCmd = &cobra.Command{ os.Exit(1) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") os.Exit(1) } } else { @@ -244,8 +233,7 @@ var kubernetesCreateCmd = &cobra.Command{ startTime := utility.StartTime() stillCreating := true - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = fmt.Sprintf("Creating a %d node %s cluster of %s instances called %s... ", kubernetesCluster.NumTargetNode, clusterType, kubernetesCluster.TargetNodeSize, kubernetesCluster.Name) s.Start() @@ -289,10 +277,12 @@ var kubernetesCreateCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - if executionTime != "" { - fmt.Printf("The cluster %s (%s) has been created in %s\n", utility.Green(kubernetesCluster.Name), kubernetesCluster.ID, executionTime) - } else { - fmt.Printf("The cluster %s (%s) has been created\n", utility.Green(kubernetesCluster.Name), kubernetesCluster.ID) + if !common.Quiet { + if executionTime != "" { + fmt.Printf("The cluster %s (%s) has been created in %s\n", utility.Green(kubernetesCluster.Name), kubernetesCluster.ID, executionTime) + } else { + fmt.Printf("The cluster %s (%s) has been created\n", utility.Green(kubernetesCluster.Name), kubernetesCluster.ID) + } } } From 52633ee4ed0533645019c6dcf4f984f686264e23 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:12:54 +0530 Subject: [PATCH 24/51] Change abort message to use utility.Error --- cmd/kubernetes/kubernetes_nodepool_delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubernetes/kubernetes_nodepool_delete.go b/cmd/kubernetes/kubernetes_nodepool_delete.go index af2c42df..39b4102e 100644 --- a/cmd/kubernetes/kubernetes_nodepool_delete.go +++ b/cmd/kubernetes/kubernetes_nodepool_delete.go @@ -108,7 +108,7 @@ var kubernetesNodePoolDeleteCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 5a4303bb8c2762c3b64e089c12be43091f119da9 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:13:35 +0530 Subject: [PATCH 25/51] Replace fmt.Println with utility.Error for abort message --- cmd/kubernetes/kubernetes_nodepool_instance_delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubernetes/kubernetes_nodepool_instance_delete.go b/cmd/kubernetes/kubernetes_nodepool_instance_delete.go index a3a62b86..6cd457bb 100644 --- a/cmd/kubernetes/kubernetes_nodepool_instance_delete.go +++ b/cmd/kubernetes/kubernetes_nodepool_instance_delete.go @@ -78,7 +78,7 @@ var kubernetesNodePoolInstanceDeleteCmd = &cobra.Command{ fmt.Printf("Instance %s has been deleted from node pool %s in cluster %s\n", instanceID, nodePoolID, kubernetesFindCluster.Name) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 0db6f4a3d8498368df05d41a2c9aa5282d6e4160 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:14:10 +0530 Subject: [PATCH 26/51] Replace fmt.Println with utility.Error for abort message --- cmd/kubernetes/kubernetes_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubernetes/kubernetes_remove.go b/cmd/kubernetes/kubernetes_remove.go index ffef9929..d4156620 100644 --- a/cmd/kubernetes/kubernetes_remove.go +++ b/cmd/kubernetes/kubernetes_remove.go @@ -128,7 +128,7 @@ var kubernetesRemoveCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 88c631c143ae510e6a2bd9e9047b8df6108b8b49 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:15:20 +0530 Subject: [PATCH 27/51] Replace fmt.Println with utility.Error for abort message --- cmd/network/network_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/network/network_remove.go b/cmd/network/network_remove.go index be2e1e18..65df5d0e 100644 --- a/cmd/network/network_remove.go +++ b/cmd/network/network_remove.go @@ -113,7 +113,7 @@ var networkRemoveCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, From bef5dd99fe376de6b99ac8cb8958866924be69a9 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:17:09 +0530 Subject: [PATCH 28/51] Refactor object store creation size validation --- cmd/objectstore/objectstore_create.go | 40 +++++++++++++++++---------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/cmd/objectstore/objectstore_create.go b/cmd/objectstore/objectstore_create.go index da9fc86e..396c4bbc 100644 --- a/cmd/objectstore/objectstore_create.go +++ b/cmd/objectstore/objectstore_create.go @@ -64,13 +64,22 @@ var objectStoreCreateCmd = &cobra.Command{ utility.Error("The minimum size to create an object store is 500 GB. Please provide a valid size.") os.Exit(1) } else if bucketSize%500 != 0 { - utility.YellowConfirm("The size to create an object store must be a multiple of 500. Would you like to create an %s of %d GB instead? (y/n) ? ", utility.Green("object store"), bucketSize+(500-bucketSize%500)) - _, err := utility.UserAccepts(os.Stdin) - if err != nil { - utility.Error("Unable to parse the input: %s", err) - os.Exit(1) + roundedSize := bucketSize + (500 - bucketSize%500) + if common.Quiet { + if !common.DefaultYes { + utility.Error("The size to create an object store must be a multiple of 500 GB; re-run with --size %d, or combine --quiet with --yes to round up automatically", roundedSize) + os.Exit(1) + } + bucketSize = roundedSize + } else { + utility.YellowConfirm("The size to create an object store must be a multiple of 500. Would you like to create an %s of %d GB instead? (y/n) ? ", utility.Green("object store"), roundedSize) + _, err := utility.UserAccepts(os.Stdin) + if err != nil { + utility.Error("Unable to parse the input: %s", err) + os.Exit(1) + } + bucketSize = roundedSize } - bucketSize = bucketSize + (500 - bucketSize%500) } var credential *civogo.ObjectStoreCredential @@ -107,8 +116,7 @@ var objectStoreCreateCmd = &cobra.Command{ if waitOS { startTime := utility.StartTime() stillCreating := true - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = fmt.Sprintf("Creating an Object Store with maxSize %d, called %s... ", store.MaxSize, store.Name) s.Start() @@ -143,13 +151,15 @@ var objectStoreCreateCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - if waitOS { - fmt.Printf("Created Object Store %s in %s in %s\n", utility.Green(objectStore.Name), utility.Green(client.Region), executionTime) - fmt.Printf("Created default admin credentials, access key is %s, this will be deleted if the Object Store is deleted. ", utility.Green(objectStore.OwnerInfo.AccessKeyID)) - fmt.Printf("To access the secret key run: civo objectstore credential secret --access-key=%s\n", utility.Green(objectStore.OwnerInfo.AccessKeyID)) - } else { - fmt.Printf("Creating Object Store %s in %s\n", utility.Green(objectStore.Name), utility.Green(client.Region)) - fmt.Printf("To check the status of the Object Store run: civo objectstore show %s\n", utility.Green(objectStore.Name)) + if !common.Quiet { + if waitOS { + fmt.Printf("Created Object Store %s in %s in %s\n", utility.Green(objectStore.Name), utility.Green(client.Region), executionTime) + fmt.Printf("Created default admin credentials, access key is %s, this will be deleted if the Object Store is deleted. ", utility.Green(objectStore.OwnerInfo.AccessKeyID)) + fmt.Printf("To access the secret key run: civo objectstore credential secret --access-key=%s\n", utility.Green(objectStore.OwnerInfo.AccessKeyID)) + } else { + fmt.Printf("Creating Object Store %s in %s\n", utility.Green(objectStore.Name), utility.Green(client.Region)) + fmt.Printf("To check the status of the Object Store run: civo objectstore show %s\n", utility.Green(objectStore.Name)) + } } } }, From 0b1366eff29981369da45ee03c62d5701a5408e5 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:17:57 +0530 Subject: [PATCH 29/51] Refactor spinner initialization and output messages --- cmd/objectstore/objectstore_credential_create.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/objectstore/objectstore_credential_create.go b/cmd/objectstore/objectstore_credential_create.go index 807d8843..569db9c7 100644 --- a/cmd/objectstore/objectstore_credential_create.go +++ b/cmd/objectstore/objectstore_credential_create.go @@ -57,8 +57,7 @@ var objectStoreCredentialCreateCmd = &cobra.Command{ if waitOS { startTime := utility.StartTime() stillCreating := true - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = fmt.Sprintf("Creating an Object Store Credential with maxSize %d, called %s... ", credential.MaxSizeGB, credential.Name) s.Start() @@ -92,10 +91,12 @@ var objectStoreCredentialCreateCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - if waitOS { - fmt.Printf("Created Object Store Credential %s in %s in %s\n", utility.Green(objectStoreCred.Name), utility.Green(client.Region), executionTime) - } else { - fmt.Printf("Creating Object Store Credential %s in %s\n", utility.Green(objectStoreCred.Name), utility.Green(client.Region)) + if !common.Quiet { + if waitOS { + fmt.Printf("Created Object Store Credential %s in %s in %s\n", utility.Green(objectStoreCred.Name), utility.Green(client.Region), executionTime) + } else { + fmt.Printf("Creating Object Store Credential %s in %s\n", utility.Green(objectStoreCred.Name), utility.Green(client.Region)) + } } } }, From b6dac3f55c7753fecda5d258008ee833267cf481 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:18:27 +0530 Subject: [PATCH 30/51] Replace fmt.Println with utility.Error for abort message --- cmd/objectstore/objectstore_credential_delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/objectstore/objectstore_credential_delete.go b/cmd/objectstore/objectstore_credential_delete.go index abde307a..817e5d2a 100644 --- a/cmd/objectstore/objectstore_credential_delete.go +++ b/cmd/objectstore/objectstore_credential_delete.go @@ -102,7 +102,7 @@ var objectStoreCredentialDeleteCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted") + utility.Error("Operation aborted.") } }, } From 69f783816218808fe62d5d81dfbe1075f8c7498f Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:18:50 +0530 Subject: [PATCH 31/51] Replace fmt.Println with utility.Error for abort message --- cmd/objectstore/objectstore_delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/objectstore/objectstore_delete.go b/cmd/objectstore/objectstore_delete.go index b74ae0ec..894c18a9 100644 --- a/cmd/objectstore/objectstore_delete.go +++ b/cmd/objectstore/objectstore_delete.go @@ -103,7 +103,7 @@ var objectStoreDeleteCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted") + utility.Error("Operation aborted.") } }, } From d57328f414772f8b0180d811ceb45f952d178751 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:19:47 +0530 Subject: [PATCH 32/51] Change abort message to use utility.Error function --- cmd/sshkey/ssh_key_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sshkey/ssh_key_remove.go b/cmd/sshkey/ssh_key_remove.go index b2f1bf6c..3ed9adc1 100644 --- a/cmd/sshkey/ssh_key_remove.go +++ b/cmd/sshkey/ssh_key_remove.go @@ -86,7 +86,7 @@ var sshKeyRemoveCmd = &cobra.Command{ fmt.Printf("The ssh %s (%s) has been deleted\n", pluralize.Pluralize(len(sshList), "key"), utility.Green(strings.Join(sshKeyNameList, ", "))) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 9d774bb83ef0fce6995885375476a25daabca370 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:20:23 +0530 Subject: [PATCH 33/51] Change abort message to use utility.Error --- cmd/teams/teams_delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/teams/teams_delete.go b/cmd/teams/teams_delete.go index 3b60362d..9d4620f0 100644 --- a/cmd/teams/teams_delete.go +++ b/cmd/teams/teams_delete.go @@ -85,7 +85,7 @@ var teamsDeleteCmd = &cobra.Command{ fmt.Printf("The team %s(%s) has been deleted\n", pluralize.Pluralize(len(teamList), ""), utility.Green(strings.Join(teamNameList, ", "))) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 900dfde1b2adc2ed7978218651382f4f254b0b33 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:21:32 +0530 Subject: [PATCH 34/51] Replace spinner with utility.NewSpinner --- cmd/volume/volume_attach.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/volume/volume_attach.go b/cmd/volume/volume_attach.go index 4f6f78d1..b2e9440d 100644 --- a/cmd/volume/volume_attach.go +++ b/cmd/volume/volume_attach.go @@ -80,8 +80,7 @@ var volumeAttachCmd = &cobra.Command{ if waitVolumeAttach { stillAttaching := true - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = "Attaching volume to the instance... " s.Start() @@ -113,7 +112,9 @@ var volumeAttachCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("The volume called %s with ID %s was attached to the instance %s\n", utility.Green(volume.Name), utility.Green(volume.ID), utility.Green(instance.Hostname)) + if !common.Quiet { + fmt.Printf("The volume called %s with ID %s was attached to the instance %s\n", utility.Green(volume.Name), utility.Green(volume.ID), utility.Green(instance.Hostname)) + } } }, } From d6d6ad07577fa1cf044df7b1bdb38f6a76359d98 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:21:52 +0530 Subject: [PATCH 35/51] Replace spinner with utility.NewSpinner in volume detach --- cmd/volume/volume_detach.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/volume/volume_detach.go b/cmd/volume/volume_detach.go index 1ad956b3..4db90107 100644 --- a/cmd/volume/volume_detach.go +++ b/cmd/volume/volume_detach.go @@ -53,8 +53,7 @@ var volumeDetachCmd = &cobra.Command{ if waitVolumeDetach { stillDetaching := true - s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) - s.Writer = os.Stderr + s := utility.NewSpinner(spinner.CharSets[9], 100*time.Millisecond) s.Prefix = "Detaching the volume... " s.Start() @@ -81,7 +80,9 @@ var volumeDetachCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("The volume called %s with ID %s was detached\n", utility.Green(volume.Name), utility.Green(volume.ID)) + if !common.Quiet { + fmt.Printf("The volume called %s with ID %s was detached\n", utility.Green(volume.Name), utility.Green(volume.ID)) + } } }, } From 69147c67b5f6921b06e76ef3c8fb4ee13fb0a749 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:22:12 +0530 Subject: [PATCH 36/51] Change abort message to use utility.Error --- cmd/volume/volume_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/volume/volume_remove.go b/cmd/volume/volume_remove.go index 9f861e91..ef6e4f68 100644 --- a/cmd/volume/volume_remove.go +++ b/cmd/volume/volume_remove.go @@ -104,7 +104,7 @@ var volumeRemoveCmd = &cobra.Command{ pluralize.Has(len(volumeNameList))) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 2dbf3ba0b8e562a590b27246ebbea436acb6a737 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:24:48 +0530 Subject: [PATCH 37/51] Refactor VPC firewall commands to generic firewall --- cmd/vpc/vpc_firewall_remove.go | 48 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/cmd/vpc/vpc_firewall_remove.go b/cmd/vpc/vpc_firewall_remove.go index 92c4a3aa..b3f180b8 100644 --- a/cmd/vpc/vpc_firewall_remove.go +++ b/cmd/vpc/vpc_firewall_remove.go @@ -1,9 +1,8 @@ -package vpc +package firewall import ( "errors" "fmt" - "os" "strings" "github.com/civo/civogo" @@ -11,15 +10,18 @@ import ( "github.com/civo/cli/config" "github.com/civo/cli/pkg/pluralize" "github.com/civo/cli/utility" + + "os" + "github.com/spf13/cobra" ) -var vpcFirewallResourceList []utility.Resource -var vpcFirewallRemoveCmd = &cobra.Command{ +var firewallList []utility.Resource +var firewallRemoveCmd = &cobra.Command{ Use: "remove [NAME]", Aliases: []string{"rm", "delete", "destroy"}, - Example: "civo vpc firewall remove NAME", - Short: "Remove a VPC firewall", + Example: "civo firewall remove NAME", + Short: "Remove a firewall", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { utility.EnsureCurrentRegion() @@ -34,43 +36,45 @@ var vpcFirewallRemoveCmd = &cobra.Command{ } if len(args) == 1 { - firewall, err := client.FindVPCFirewall(args[0]) + firewall, err := client.FindFirewall(args[0]) if err != nil { if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s VPC firewall in your account", utility.Red(args[0])) + utility.Error("sorry there is no %s firewall in your account", utility.Red(args[0])) os.Exit(1) } if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one VPC firewall with that name in your account") + utility.Error("sorry we found more than one firewall with that name in your account") os.Exit(1) } } - vpcFirewallResourceList = append(vpcFirewallResourceList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) + firewallList = append(firewallList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) } else { for _, v := range args { - firewall, err := client.FindVPCFirewall(v) + firewall, err := client.FindFirewall(v) if err == nil { - vpcFirewallResourceList = append(vpcFirewallResourceList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) + firewallList = append(firewallList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) } } } firewallNameList := []string{} - for _, v := range vpcFirewallResourceList { + for _, v := range firewallList { firewallNameList = append(firewallNameList, v.Name) } - if utility.UserConfirmedDeletion(pluralize.Pluralize(len(vpcFirewallResourceList), "VPC firewall"), common.DefaultYes, strings.Join(firewallNameList, ", ")) { - for _, v := range vpcFirewallResourceList { - _, err = client.DeleteVPCFirewall(v.ID) + if utility.UserConfirmedDeletion(pluralize.Pluralize(len(firewallList), "firewall"), common.DefaultYes, strings.Join(firewallNameList, ", ")) { + + for _, v := range firewallList { + _, err = client.DeleteFirewall(v.ID) if err != nil { - utility.Error("error deleting the VPC firewall: %s", err) + utility.Error("error deleting the firewall: %s", err) os.Exit(1) } } ow := utility.NewOutputWriter() - for _, v := range vpcFirewallResourceList { + + for _, v := range firewallList { ow.StartLine() ow.AppendDataWithLabel("id", v.ID, "ID") ow.AppendDataWithLabel("name", v.Name, "Name") @@ -78,7 +82,7 @@ var vpcFirewallRemoveCmd = &cobra.Command{ switch common.OutputFormat { case "json": - if len(vpcFirewallResourceList) == 1 { + if len(firewallList) == 1 { ow.WriteSingleObjectJSON(common.PrettySet) } else { ow.WriteMultipleObjectsJSON(common.PrettySet) @@ -87,13 +91,13 @@ var vpcFirewallRemoveCmd = &cobra.Command{ ow.WriteCustomOutput(common.OutputFields) default: fmt.Printf("The %s (%s) %s been deleted\n", - pluralize.Pluralize(len(vpcFirewallResourceList), "VPC firewall"), + pluralize.Pluralize(len(firewallList), "firewall"), utility.Green(strings.Join(firewallNameList, ", ")), - pluralize.Has(len(vpcFirewallResourceList)), + pluralize.Has(len(firewallList)), ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 460352364fe49dcacafe453d0a7bc618d97963df Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:25:17 +0530 Subject: [PATCH 38/51] Refactor VPC firewall rule removal to firewall package --- cmd/vpc/vpc_firewall_rule_remove.go | 57 ++++++++++++++++------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/cmd/vpc/vpc_firewall_rule_remove.go b/cmd/vpc/vpc_firewall_rule_remove.go index c0f1a9e7..85820c68 100644 --- a/cmd/vpc/vpc_firewall_rule_remove.go +++ b/cmd/vpc/vpc_firewall_rule_remove.go @@ -1,9 +1,8 @@ -package vpc +package firewall import ( "errors" "fmt" - "os" "strings" "github.com/civo/civogo" @@ -11,16 +10,19 @@ import ( "github.com/civo/cli/config" "github.com/civo/cli/pkg/pluralize" "github.com/civo/cli/utility" + + "os" + "github.com/spf13/cobra" ) -var vpcFirewallRuleResourceList []utility.Resource -var vpcFirewallRuleRemoveCmd = &cobra.Command{ +var firewallRuleList []utility.Resource +var firewallRuleRemoveCmd = &cobra.Command{ Use: "remove", Aliases: []string{"delete", "destroy", "rm"}, Args: cobra.MinimumNArgs(2), - Short: "Remove VPC firewall rule", - Example: "civo vpc firewall rule remove FIREWALL_NAME/FIREWALL_ID FIREWALL_RULE_ID", + Short: "Remove firewall rule", + Example: "civo firewall rule remove FIREWALL_NAME/FIREWALL_ID FIREWALL_RULE_ID", Run: func(cmd *cobra.Command, args []string) { utility.EnsureCurrentRegion() @@ -33,56 +35,58 @@ var vpcFirewallRuleRemoveCmd = &cobra.Command{ os.Exit(1) } - firewall, err := client.FindVPCFirewall(args[0]) + firewall, err := client.FindFirewall(args[0]) if err != nil { if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s VPC firewall in your account", utility.Red(args[0])) + utility.Error("sorry there is no %s firewall in your account", utility.Red(args[0])) os.Exit(1) } if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one VPC firewall with that name in your account") + utility.Error("sorry we found more than one firewall with that name in your account") os.Exit(1) } } if len(args) == 2 { - rule, err := client.FindVPCFirewallRule(firewall.ID, args[1]) + rule, err := client.FindFirewallRule(firewall.ID, args[1]) if err != nil { if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s VPC firewall rule in your account", utility.Red(args[1])) + utility.Error("sorry there is no %s firewall rule in your account", utility.Red(args[1])) os.Exit(1) } if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one VPC firewall rule in your account") + utility.Error("sorry we found more than one firewall rule in your account") os.Exit(1) } } - vpcFirewallRuleResourceList = append(vpcFirewallRuleResourceList, utility.Resource{ID: rule.ID, Name: rule.Label}) + firewallRuleList = append(firewallRuleList, utility.Resource{ID: rule.ID, Name: rule.Label}) } else { for _, v := range args[1:] { - rule, err := client.FindVPCFirewallRule(firewall.ID, v) + rule, err := client.FindFirewallRule(firewall.ID, v) if err == nil { - vpcFirewallRuleResourceList = append(vpcFirewallRuleResourceList, utility.Resource{ID: rule.ID, Name: rule.Label}) + firewallRuleList = append(firewallRuleList, utility.Resource{ID: rule.ID, Name: rule.Label}) } } } firewallRuleNameList := []string{} - for _, v := range vpcFirewallRuleResourceList { + for _, v := range firewallRuleList { firewallRuleNameList = append(firewallRuleNameList, v.Name) } - if utility.UserConfirmedDeletion(fmt.Sprintf("VPC firewall %s", pluralize.Pluralize(len(vpcFirewallRuleResourceList), "rule")), common.DefaultYes, strings.Join(firewallRuleNameList, ", ")) { - for _, v := range vpcFirewallRuleResourceList { - _, err = client.DeleteVPCFirewallRule(firewall.ID, v.ID) + if utility.UserConfirmedDeletion(fmt.Sprintf("firewall %s", pluralize.Pluralize(len(firewallRuleList), "rule")), common.DefaultYes, strings.Join(firewallRuleNameList, ", ")) { + + for _, v := range firewallRuleList { + _, err = client.DeleteFirewallRule(firewall.ID, v.ID) if err != nil { - utility.Error("error deleting the VPC firewall rule: %s", err) + utility.Error("error deleting the firewall rule: %s", err) os.Exit(1) } } ow := utility.NewOutputWriter() - for _, v := range vpcFirewallRuleResourceList { + + for _, v := range firewallRuleList { ow.StartLine() ow.AppendDataWithLabel("id", v.ID, "ID") ow.AppendDataWithLabel("label", v.Name, "Label") @@ -90,7 +94,7 @@ var vpcFirewallRuleRemoveCmd = &cobra.Command{ switch common.OutputFormat { case "json": - if len(vpcFirewallRuleResourceList) == 1 { + if len(firewallRuleList) == 1 { ow.WriteSingleObjectJSON(common.PrettySet) } else { ow.WriteMultipleObjectsJSON(common.PrettySet) @@ -98,14 +102,15 @@ var vpcFirewallRuleRemoveCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("The VPC firewall %s (%s) %s been deleted\n", - pluralize.Pluralize(len(vpcFirewallRuleResourceList), "rule"), + fmt.Printf("The firewall %s (%s) %s been deleted\n", + pluralize.Pluralize(len(firewallRuleList), "rule"), strings.Join(firewallRuleNameList, ", "), - pluralize.Has(len(vpcFirewallRuleResourceList)), + pluralize.Has(len(firewallRuleList)), ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } + }, } From a7b20c9e92b1eda61e61e2c66eb6a8ce0d6f28e4 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:26:46 +0530 Subject: [PATCH 39/51] Refactor firewall removal to VPC firewall --- cmd/vpc/vpc_firewall_remove.go | 46 ++++++++++++++++------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/cmd/vpc/vpc_firewall_remove.go b/cmd/vpc/vpc_firewall_remove.go index b3f180b8..22e00eb8 100644 --- a/cmd/vpc/vpc_firewall_remove.go +++ b/cmd/vpc/vpc_firewall_remove.go @@ -1,8 +1,9 @@ -package firewall +package vpc import ( "errors" "fmt" + "os" "strings" "github.com/civo/civogo" @@ -10,18 +11,15 @@ import ( "github.com/civo/cli/config" "github.com/civo/cli/pkg/pluralize" "github.com/civo/cli/utility" - - "os" - "github.com/spf13/cobra" ) -var firewallList []utility.Resource -var firewallRemoveCmd = &cobra.Command{ +var vpcFirewallResourceList []utility.Resource +var vpcFirewallRemoveCmd = &cobra.Command{ Use: "remove [NAME]", Aliases: []string{"rm", "delete", "destroy"}, - Example: "civo firewall remove NAME", - Short: "Remove a firewall", + Example: "civo vpc firewall remove NAME", + Short: "Remove a VPC firewall", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { utility.EnsureCurrentRegion() @@ -36,45 +34,43 @@ var firewallRemoveCmd = &cobra.Command{ } if len(args) == 1 { - firewall, err := client.FindFirewall(args[0]) + firewall, err := client.FindVPCFirewall(args[0]) if err != nil { if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s firewall in your account", utility.Red(args[0])) + utility.Error("sorry there is no %s VPC firewall in your account", utility.Red(args[0])) os.Exit(1) } if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one firewall with that name in your account") + utility.Error("sorry we found more than one VPC firewall with that name in your account") os.Exit(1) } } - firewallList = append(firewallList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) + vpcFirewallResourceList = append(vpcFirewallResourceList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) } else { for _, v := range args { - firewall, err := client.FindFirewall(v) + firewall, err := client.FindVPCFirewall(v) if err == nil { - firewallList = append(firewallList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) + vpcFirewallResourceList = append(vpcFirewallResourceList, utility.Resource{ID: firewall.ID, Name: firewall.Name}) } } } firewallNameList := []string{} - for _, v := range firewallList { + for _, v := range vpcFirewallResourceList { firewallNameList = append(firewallNameList, v.Name) } - if utility.UserConfirmedDeletion(pluralize.Pluralize(len(firewallList), "firewall"), common.DefaultYes, strings.Join(firewallNameList, ", ")) { - - for _, v := range firewallList { - _, err = client.DeleteFirewall(v.ID) + if utility.UserConfirmedDeletion(pluralize.Pluralize(len(vpcFirewallResourceList), "VPC firewall"), common.DefaultYes, strings.Join(firewallNameList, ", ")) { + for _, v := range vpcFirewallResourceList { + _, err = client.DeleteVPCFirewall(v.ID) if err != nil { - utility.Error("error deleting the firewall: %s", err) + utility.Error("error deleting the VPC firewall: %s", err) os.Exit(1) } } ow := utility.NewOutputWriter() - - for _, v := range firewallList { + for _, v := range vpcFirewallResourceList { ow.StartLine() ow.AppendDataWithLabel("id", v.ID, "ID") ow.AppendDataWithLabel("name", v.Name, "Name") @@ -82,7 +78,7 @@ var firewallRemoveCmd = &cobra.Command{ switch common.OutputFormat { case "json": - if len(firewallList) == 1 { + if len(vpcFirewallResourceList) == 1 { ow.WriteSingleObjectJSON(common.PrettySet) } else { ow.WriteMultipleObjectsJSON(common.PrettySet) @@ -91,9 +87,9 @@ var firewallRemoveCmd = &cobra.Command{ ow.WriteCustomOutput(common.OutputFields) default: fmt.Printf("The %s (%s) %s been deleted\n", - pluralize.Pluralize(len(firewallList), "firewall"), + pluralize.Pluralize(len(vpcFirewallResourceList), "VPC firewall"), utility.Green(strings.Join(firewallNameList, ", ")), - pluralize.Has(len(firewallList)), + pluralize.Has(len(vpcFirewallResourceList)), ) } } else { From ac48249e89ad29f6a72ca3b76ad11498fe821aa5 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:27:21 +0530 Subject: [PATCH 40/51] Rename firewall commands to VPC firewall --- cmd/vpc/vpc_firewall_rule_remove.go | 55 +++++++++++++---------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/cmd/vpc/vpc_firewall_rule_remove.go b/cmd/vpc/vpc_firewall_rule_remove.go index 85820c68..782ea4c6 100644 --- a/cmd/vpc/vpc_firewall_rule_remove.go +++ b/cmd/vpc/vpc_firewall_rule_remove.go @@ -1,8 +1,9 @@ -package firewall +package vpc import ( "errors" "fmt" + "os" "strings" "github.com/civo/civogo" @@ -10,19 +11,16 @@ import ( "github.com/civo/cli/config" "github.com/civo/cli/pkg/pluralize" "github.com/civo/cli/utility" - - "os" - "github.com/spf13/cobra" ) -var firewallRuleList []utility.Resource -var firewallRuleRemoveCmd = &cobra.Command{ +var vpcFirewallRuleResourceList []utility.Resource +var vpcFirewallRuleRemoveCmd = &cobra.Command{ Use: "remove", Aliases: []string{"delete", "destroy", "rm"}, Args: cobra.MinimumNArgs(2), - Short: "Remove firewall rule", - Example: "civo firewall rule remove FIREWALL_NAME/FIREWALL_ID FIREWALL_RULE_ID", + Short: "Remove VPC firewall rule", + Example: "civo vpc firewall rule remove FIREWALL_NAME/FIREWALL_ID FIREWALL_RULE_ID", Run: func(cmd *cobra.Command, args []string) { utility.EnsureCurrentRegion() @@ -35,58 +33,56 @@ var firewallRuleRemoveCmd = &cobra.Command{ os.Exit(1) } - firewall, err := client.FindFirewall(args[0]) + firewall, err := client.FindVPCFirewall(args[0]) if err != nil { if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s firewall in your account", utility.Red(args[0])) + utility.Error("sorry there is no %s VPC firewall in your account", utility.Red(args[0])) os.Exit(1) } if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one firewall with that name in your account") + utility.Error("sorry we found more than one VPC firewall with that name in your account") os.Exit(1) } } if len(args) == 2 { - rule, err := client.FindFirewallRule(firewall.ID, args[1]) + rule, err := client.FindVPCFirewallRule(firewall.ID, args[1]) if err != nil { if errors.Is(err, civogo.ZeroMatchesError) { - utility.Error("sorry there is no %s firewall rule in your account", utility.Red(args[1])) + utility.Error("sorry there is no %s VPC firewall rule in your account", utility.Red(args[1])) os.Exit(1) } if errors.Is(err, civogo.MultipleMatchesError) { - utility.Error("sorry we found more than one firewall rule in your account") + utility.Error("sorry we found more than one VPC firewall rule in your account") os.Exit(1) } } - firewallRuleList = append(firewallRuleList, utility.Resource{ID: rule.ID, Name: rule.Label}) + vpcFirewallRuleResourceList = append(vpcFirewallRuleResourceList, utility.Resource{ID: rule.ID, Name: rule.Label}) } else { for _, v := range args[1:] { - rule, err := client.FindFirewallRule(firewall.ID, v) + rule, err := client.FindVPCFirewallRule(firewall.ID, v) if err == nil { - firewallRuleList = append(firewallRuleList, utility.Resource{ID: rule.ID, Name: rule.Label}) + vpcFirewallRuleResourceList = append(vpcFirewallRuleResourceList, utility.Resource{ID: rule.ID, Name: rule.Label}) } } } firewallRuleNameList := []string{} - for _, v := range firewallRuleList { + for _, v := range vpcFirewallRuleResourceList { firewallRuleNameList = append(firewallRuleNameList, v.Name) } - if utility.UserConfirmedDeletion(fmt.Sprintf("firewall %s", pluralize.Pluralize(len(firewallRuleList), "rule")), common.DefaultYes, strings.Join(firewallRuleNameList, ", ")) { - - for _, v := range firewallRuleList { - _, err = client.DeleteFirewallRule(firewall.ID, v.ID) + if utility.UserConfirmedDeletion(fmt.Sprintf("VPC firewall %s", pluralize.Pluralize(len(vpcFirewallRuleResourceList), "rule")), common.DefaultYes, strings.Join(firewallRuleNameList, ", ")) { + for _, v := range vpcFirewallRuleResourceList { + _, err = client.DeleteVPCFirewallRule(firewall.ID, v.ID) if err != nil { - utility.Error("error deleting the firewall rule: %s", err) + utility.Error("error deleting the VPC firewall rule: %s", err) os.Exit(1) } } ow := utility.NewOutputWriter() - - for _, v := range firewallRuleList { + for _, v := range vpcFirewallRuleResourceList { ow.StartLine() ow.AppendDataWithLabel("id", v.ID, "ID") ow.AppendDataWithLabel("label", v.Name, "Label") @@ -94,7 +90,7 @@ var firewallRuleRemoveCmd = &cobra.Command{ switch common.OutputFormat { case "json": - if len(firewallRuleList) == 1 { + if len(vpcFirewallRuleResourceList) == 1 { ow.WriteSingleObjectJSON(common.PrettySet) } else { ow.WriteMultipleObjectsJSON(common.PrettySet) @@ -102,15 +98,14 @@ var firewallRuleRemoveCmd = &cobra.Command{ case "custom": ow.WriteCustomOutput(common.OutputFields) default: - fmt.Printf("The firewall %s (%s) %s been deleted\n", - pluralize.Pluralize(len(firewallRuleList), "rule"), + fmt.Printf("The VPC firewall %s (%s) %s been deleted\n", + pluralize.Pluralize(len(vpcFirewallRuleResourceList), "rule"), strings.Join(firewallRuleNameList, ", "), - pluralize.Has(len(firewallRuleList)), + pluralize.Has(len(vpcFirewallRuleResourceList)), ) } } else { utility.Error("Operation aborted.") } - }, } From 1da1fba61df7dd9a1f83764ac3d82f2ee8f17aaf Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:27:50 +0530 Subject: [PATCH 41/51] Replace fmt.Println with utility.Error for abort message --- cmd/vpc/vpc_ip_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/vpc/vpc_ip_remove.go b/cmd/vpc/vpc_ip_remove.go index 11d84e74..3cb00428 100644 --- a/cmd/vpc/vpc_ip_remove.go +++ b/cmd/vpc/vpc_ip_remove.go @@ -62,7 +62,7 @@ var vpcIPRemoveCmd = &cobra.Command{ fmt.Printf("VPC IP called %s with ID %s was deleted\n", utility.Green(ip.Name), utility.Green(ip.ID)) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 9d89557c67edfbcbf28931cea71cdc402ce14ebe Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:28:11 +0530 Subject: [PATCH 42/51] Replace fmt.Println with utility.Error for abort message --- cmd/vpc/vpc_loadbalancer_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/vpc/vpc_loadbalancer_remove.go b/cmd/vpc/vpc_loadbalancer_remove.go index 0abad703..8352b300 100644 --- a/cmd/vpc/vpc_loadbalancer_remove.go +++ b/cmd/vpc/vpc_loadbalancer_remove.go @@ -93,7 +93,7 @@ var vpcLoadBalancerRemoveCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From 01cc636690d05be61f0be96cd03ddc43471c9dd3 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:28:37 +0530 Subject: [PATCH 43/51] Replace fmt.Println with utility.Error for abort message --- cmd/vpc/vpc_network_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/vpc/vpc_network_remove.go b/cmd/vpc/vpc_network_remove.go index 4970ed29..a7572010 100644 --- a/cmd/vpc/vpc_network_remove.go +++ b/cmd/vpc/vpc_network_remove.go @@ -103,7 +103,7 @@ var vpcNetworkRemoveCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From a02a662bab9d1ebb21b2b24881496ad4490c43ee Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:28:59 +0530 Subject: [PATCH 44/51] Replace fmt.Println with utility.Error for abort message --- cmd/vpc/vpc_subnet_remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/vpc/vpc_subnet_remove.go b/cmd/vpc/vpc_subnet_remove.go index e379c0b7..abfaf4c1 100644 --- a/cmd/vpc/vpc_subnet_remove.go +++ b/cmd/vpc/vpc_subnet_remove.go @@ -101,7 +101,7 @@ var vpcSubnetRemoveCmd = &cobra.Command{ ) } } else { - fmt.Println("Operation aborted.") + utility.Error("Operation aborted.") } }, } From fffb4aec0008b66fb9da3e06308fa69ec785b986 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:30:56 +0530 Subject: [PATCH 45/51] Introduce Quiet option in common.go Add Quiet option to suppress prompts and output. --- common/common.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/common.go b/common/common.go index e07fa035..0392273d 100644 --- a/common/common.go +++ b/common/common.go @@ -17,6 +17,10 @@ var ( RegionSet string // DefaultYes : automatic yes to prompts; assume \"yes\" as answer to all prompts and run non-interactively DefaultYes bool + // Quiet suppresses interactive prompts, progress indicators, and non-error + // output. Interactive prompts fail instead of blocking when no confirmed + // default (--yes) is available. Set via the global --quiet/-q flag. + Quiet bool // PrettySet : Prints the json output in pretty format PrettySet bool // VersionCli is set from outside using ldflags From 5ecd1a0051918c42d10b48b03aef45e710dd444b Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:31:55 +0530 Subject: [PATCH 46/51] Add quiet mode handling for log messages Added common.Quiet checks to suppress messages in quiet mode. --- utility/color_util.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/utility/color_util.go b/utility/color_util.go index 0f6fc3dd..07c905cb 100644 --- a/utility/color_util.go +++ b/utility/color_util.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/civo/cli/common" "github.com/gookit/color" ) @@ -44,28 +45,44 @@ func Red(value string) string { } // Error is the function to handler all error in the Cli +// Errors are always shown, even in --quiet mode, so failures are never silently swallowed. func Error(msg string, args ...interface{}) { fmt.Fprintf(os.Stderr, "%s: %s\n", color.Red.Sprintf("Error"), fmt.Sprintf(msg, args...)) } // Info is the function to handler all info messages in the Cli +// Suppressed in --quiet mode. func Info(msg string, args ...interface{}) { + if common.Quiet { + return + } fmt.Fprintf(os.Stderr, "%s: %s\n", color.Blue.Sprintf("Info"), fmt.Sprintf(msg, args...)) } // Warning is the function to handler all warnings in the Cli +// Suppressed in --quiet mode. func Warning(msg string, args ...interface{}) { + if common.Quiet { + return + } fmt.Fprintf(os.Stderr, "%s: %s\n", color.Yellow.Sprintf("Warning"), fmt.Sprintf(msg, args...)) } // YellowConfirm is the function to handler all delete confirm +// Suppressed in --quiet mode (confirmation prompts are skipped entirely when quiet; see AskForConfirm). func YellowConfirm(msg string, args ...interface{}) { + if common.Quiet { + return + } fmt.Fprintf(os.Stderr, "%s: %s", color.Warn.Sprintf("Warning"), fmt.Sprintf(msg, args...)) } // RedConfirm is the function to handler the new version of the Cli +// Suppressed in --quiet mode. func RedConfirm(msg string, args ...interface{}) { - + if common.Quiet { + return + } fmt.Fprintf(os.Stderr, "%s: %s", color.Red.Sprintf("IMPORTANT"), fmt.Sprintf(msg, args...)) } From 271140eb51b2d858dd295a7e9cc098679be687e6 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:32:39 +0530 Subject: [PATCH 47/51] Add tests for Info, Warning, and Error functions --- utility/color_util_test.go | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 utility/color_util_test.go diff --git a/utility/color_util_test.go b/utility/color_util_test.go new file mode 100644 index 00000000..b891f18a --- /dev/null +++ b/utility/color_util_test.go @@ -0,0 +1,73 @@ +package utility + +import ( + "bytes" + "io" + "os" + "testing" + + "github.com/civo/cli/common" +) + +// captureStderr redirects os.Stderr for the duration of fn and returns what was written to it. +func captureStderr(t *testing.T, fn func()) string { + t.Helper() + + originalStderr := os.Stderr + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("failed to create pipe: %v", err) + } + os.Stderr = w + + fn() + + w.Close() + os.Stderr = originalStderr + + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + return buf.String() +} + +func TestInfoWarning_SuppressedWhenQuiet(t *testing.T) { + originalQuiet := common.Quiet + defer func() { common.Quiet = originalQuiet }() + + common.Quiet = true + out := captureStderr(t, func() { + Info("some info") + Warning("some warning") + YellowConfirm("some confirm banner") + RedConfirm("some important banner") + }) + if out != "" { + t.Fatalf("expected no output for Info/Warning/YellowConfirm/RedConfirm when --quiet is set, got %q", out) + } +} + +func TestInfoWarning_ShownWhenNotQuiet(t *testing.T) { + originalQuiet := common.Quiet + defer func() { common.Quiet = originalQuiet }() + + common.Quiet = false + out := captureStderr(t, func() { + Info("some info") + }) + if out == "" { + t.Fatal("expected Info to print when --quiet is not set") + } +} + +func TestError_NeverSuppressedEvenWhenQuiet(t *testing.T) { + originalQuiet := common.Quiet + defer func() { common.Quiet = originalQuiet }() + + common.Quiet = true + out := captureStderr(t, func() { + Error("something failed") + }) + if out == "" { + t.Fatal("expected Error to always print, even when --quiet is set") + } +} From dca8b330ee4463febd77efe76b5eff9ffdd31084 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:34:00 +0530 Subject: [PATCH 48/51] Enhance AskForConfirm to support --quiet mode Added handling for --quiet mode in AskForConfirm function. --- utility/confirmation.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/utility/confirmation.go b/utility/confirmation.go index 0c9d40a0..dcc2a9e7 100644 --- a/utility/confirmation.go +++ b/utility/confirmation.go @@ -6,6 +6,8 @@ import ( "io" "os" "strings" + + "github.com/civo/cli/common" ) // retrieveUserInput is a function that can retrieve user input in form of string. By default, @@ -30,7 +32,17 @@ func readUserInput(in io.Reader, message string) (string, error) { } // AskForConfirm parses and verifies user input for confirmation. +// +// In --quiet mode, this never blocks waiting for interactive input: since +// quiet mode has no confirmed default to fall back on here (callers only +// reach this function when they haven't already been told "yes" via +// --yes/ignoringConfirmed), it fails immediately instead. To proceed +// non-interactively, combine --quiet with --yes at the call site. func AskForConfirm(message string) error { + if common.Quiet { + return fmt.Errorf("confirmation required to %s, but --quiet was set without --yes; re-run with --yes to confirm automatically, or without --quiet to be prompted", message) + } + answer, err := retrieveUserInput(message) if err != nil { Error("Unable to parse users input: %s", err) From 63b9c2bc49c5ef0fb821bd2adbb6f05fb6c4bd3a Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:34:39 +0530 Subject: [PATCH 49/51] Add tests for AskForConfirm and UserConfirmedDeletion --- utility/confirmation_test.go | 90 ++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 utility/confirmation_test.go diff --git a/utility/confirmation_test.go b/utility/confirmation_test.go new file mode 100644 index 00000000..d71cbbca --- /dev/null +++ b/utility/confirmation_test.go @@ -0,0 +1,90 @@ +package utility + +import ( + "testing" + + "github.com/civo/cli/common" +) + +func TestAskForConfirm_QuietModeFailsWithoutPrompting(t *testing.T) { + originalQuiet := common.Quiet + originalRetrieveUserInput := retrieveUserInput + defer func() { + common.Quiet = originalQuiet + retrieveUserInput = originalRetrieveUserInput + }() + + common.Quiet = true + + promptWasCalled := false + retrieveUserInput = func(message string) (string, error) { + promptWasCalled = true + return "yes", nil + } + + err := AskForConfirm("delete the thing") + if err == nil { + t.Fatal("expected AskForConfirm to fail in --quiet mode without --yes, got nil error") + } + if promptWasCalled { + t.Fatal("AskForConfirm must not read interactive input at all when --quiet is set, but it did") + } +} + +func TestAskForConfirm_NonQuietModeStillPrompts(t *testing.T) { + originalQuiet := common.Quiet + originalRetrieveUserInput := retrieveUserInput + defer func() { + common.Quiet = originalQuiet + retrieveUserInput = originalRetrieveUserInput + }() + + common.Quiet = false + + tests := []struct { + name string + answer string + wantError bool + }{ + {name: "yes answer confirms", answer: "yes", wantError: false}, + {name: "y answer confirms", answer: "y", wantError: false}, + {name: "no answer does not confirm", answer: "no", wantError: true}, + {name: "empty answer does not confirm", answer: "", wantError: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + promptWasCalled := false + retrieveUserInput = func(message string) (string, error) { + promptWasCalled = true + return tt.answer, nil + } + + err := AskForConfirm("delete the thing") + if !promptWasCalled { + t.Fatal("expected AskForConfirm to prompt for input when --quiet is not set") + } + if (err != nil) != tt.wantError { + t.Fatalf("AskForConfirm(%q) error = %v, wantError %v", tt.answer, err, tt.wantError) + } + }) + } +} + +func TestUserConfirmedDeletion_QuietModeRespectsYesFlag(t *testing.T) { + originalQuiet := common.Quiet + defer func() { common.Quiet = originalQuiet }() + common.Quiet = true + + // ignoringConfirmed=true simulates --yes being passed: should succeed + // without ever touching stdin/AskForConfirm, matching "combine with + // --yes to override" from the issue's acceptance criteria. + if !UserConfirmedDeletion("instance", true, "my-instance") { + t.Fatal("expected deletion to be confirmed automatically when --yes is set, even with --quiet") + } + + // ignoringConfirmed=false (no --yes) with --quiet must fail, not block. + if UserConfirmedDeletion("instance", false, "my-instance") { + t.Fatal("expected deletion to be denied when --quiet is set without --yes") + } +} From 8f8c579b8386fe91dd1c88e2b2ed5fa882ff5586 Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:35:19 +0530 Subject: [PATCH 50/51] Add NewSpinner function for progress spinners Implement NewSpinner function for progress indication. --- utility/spinner.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 utility/spinner.go diff --git a/utility/spinner.go b/utility/spinner.go new file mode 100644 index 00000000..0a6e1da6 --- /dev/null +++ b/utility/spinner.go @@ -0,0 +1,26 @@ +package utility + +import ( + "io" + "os" + "time" + + "github.com/briandowns/spinner" + "github.com/civo/cli/common" +) + +// NewSpinner creates a progress spinner for long-running/--wait style +// commands. In --quiet mode, the spinner is created and can still be +// Start()/Stop()'d as normal by the caller, but its output is discarded so +// no progress indicator is printed - this keeps callers simple (no branching +// needed at each call site) while satisfying --quiet's "no progress +// indicators" requirement. +func NewSpinner(charSet []string, refreshRate time.Duration) *spinner.Spinner { + s := spinner.New(charSet, refreshRate) + if common.Quiet { + s.Writer = io.Discard + } else { + s.Writer = os.Stderr + } + return s +} From 85509dac5ad83d7c9902856c2286d98d964527ab Mon Sep 17 00:00:00 2001 From: Yuvaraj R <131381006+yuvarajrece@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:35:48 +0530 Subject: [PATCH 51/51] Add tests for NewSpinner writer selection --- utility/spinner_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 utility/spinner_test.go diff --git a/utility/spinner_test.go b/utility/spinner_test.go new file mode 100644 index 00000000..2839a6c4 --- /dev/null +++ b/utility/spinner_test.go @@ -0,0 +1,32 @@ +package utility + +import ( + "io" + "os" + "testing" + "time" + + "github.com/briandowns/spinner" + "github.com/civo/cli/common" +) + +func TestNewSpinner_WriterSelection(t *testing.T) { + originalQuiet := common.Quiet + defer func() { common.Quiet = originalQuiet }() + + t.Run("quiet mode discards spinner output", func(t *testing.T) { + common.Quiet = true + s := NewSpinner(spinner.CharSets[9], 100*time.Millisecond) + if s.Writer != io.Discard { + t.Fatal("expected spinner Writer to be io.Discard when --quiet is set") + } + }) + + t.Run("non-quiet mode writes to stderr as before", func(t *testing.T) { + common.Quiet = false + s := NewSpinner(spinner.CharSets[9], 100*time.Millisecond) + if s.Writer != os.Stderr { + t.Fatal("expected spinner Writer to be os.Stderr when --quiet is not set") + } + }) +}