From d58d21a9a3d32e62bed1f7966333d4d2c8722b05 Mon Sep 17 00:00:00 2001 From: phani Date: Wed, 25 Feb 2026 16:30:53 -0500 Subject: [PATCH 1/3] feat(cli): add `kernel status` command Displays overall system status and per-group/component breakdown from the API's /status endpoint, with color-coded output matching the dashboard indicator. --- cmd/root.go | 3 +- cmd/status.go | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 cmd/status.go diff --git a/cmd/root.go b/cmd/root.go index 5542dcb..8179d8c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -90,7 +90,7 @@ func isAuthExempt(cmd *cobra.Command) bool { // Check if the top-level command is in the exempt list switch topLevel.Name() { - case "login", "logout", "help", "completion", "create", "mcp", "upgrade": + case "login", "logout", "help", "completion", "create", "mcp", "upgrade", "status": return true case "auth": // Only exempt the auth command itself (status display), not its subcommands @@ -147,6 +147,7 @@ func init() { rootCmd.AddCommand(createCmd) rootCmd.AddCommand(mcp.MCPCmd) rootCmd.AddCommand(upgradeCmd) + rootCmd.AddCommand(statusCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { // running synchronously so we never slow the command diff --git a/cmd/status.go b/cmd/status.go new file mode 100644 index 0000000..20fe5d2 --- /dev/null +++ b/cmd/status.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type statusComponent struct { + Name string `json:"name"` + Status string `json:"status"` +} + +type statusGroup struct { + Name string `json:"name"` + Status string `json:"status"` + Components []statusComponent `json:"components"` +} + +type statusResponse struct { + Status string `json:"status"` + Groups []statusGroup `json:"groups"` +} + +const defaultBaseURL = "https://api.onkernel.com" + +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Check the operational status of Kernel services", + RunE: runStatus, +} + +func init() { + statusCmd.Flags().StringP("output", "o", "", "Output format (json)") +} + +func getBaseURL() string { + if u := os.Getenv("KERNEL_BASE_URL"); strings.TrimSpace(u) != "" { + return strings.TrimRight(u, "/") + } + return defaultBaseURL +} + +func runStatus(cmd *cobra.Command, args []string) error { + output, _ := cmd.Flags().GetString("output") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(getBaseURL() + "/status") + if err != nil { + pterm.Error.Println("Could not reach Kernel API. Check https://status.kernel.sh for updates.") + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + var status statusResponse + if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + return fmt.Errorf("invalid response: %w", err) + } + + if output == "json" { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(status) + } + + printStatus(status) + return nil +} + +// Colors match the dashboard's api-status-indicator.tsx +var statusDisplay = map[string]struct { + label string + rgb pterm.RGB +}{ + "operational": {label: "Operational", rgb: pterm.NewRGB(31, 163, 130)}, + "degraded_performance": {label: "Degraded Performance", rgb: pterm.NewRGB(245, 158, 11)}, + "partial_outage": {label: "Partial Outage", rgb: pterm.NewRGB(242, 85, 51)}, + "full_outage": {label: "Major Outage", rgb: pterm.NewRGB(239, 68, 68)}, + "maintenance": {label: "Maintenance", rgb: pterm.NewRGB(36, 99, 235)}, + "unknown": {label: "Unknown", rgb: pterm.NewRGB(128, 128, 128)}, +} + +func getStatusDisplay(status string) (string, pterm.RGB) { + if d, ok := statusDisplay[status]; ok { + return d.label, d.rgb + } + return "Unknown", pterm.NewRGB(128, 128, 128) +} + +func coloredDot(rgb pterm.RGB) string { + return rgb.Sprint("●") +} + +func printStatus(resp statusResponse) { + label, rgb := getStatusDisplay(resp.Status) + header := fmt.Sprintf("Kernel Status: %s", rgb.Sprint(label)) + pterm.Println() + pterm.Println(" " + header) + + for _, group := range resp.Groups { + pterm.Println() + if len(group.Components) == 0 { + groupLabel, groupColor := getStatusDisplay(group.Status) + pterm.Printf(" %s %s %s\n", coloredDot(groupColor), pterm.Bold.Sprint(group.Name), groupLabel) + } else { + pterm.Println(" " + pterm.Bold.Sprint(group.Name)) + for _, comp := range group.Components { + compLabel, compColor := getStatusDisplay(comp.Status) + pterm.Printf(" %s %-20s %s\n", coloredDot(compColor), comp.Name, compLabel) + } + } + } + pterm.Println() +} From c19a361591a60d5fd85f6b2b6ea0f8c9b316d74f Mon Sep 17 00:00:00 2001 From: phani Date: Wed, 25 Feb 2026 17:15:42 -0500 Subject: [PATCH 2/3] fix(cmd/status): handle non-2xx responses from Kernel API Added error handling to the `runStatus` function to log an error message and return an error when the response status code from the Kernel API is not in the 2xx range. This improves user feedback when the API is unreachable. --- cmd/status.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/status.go b/cmd/status.go index 20fe5d2..749416c 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -58,6 +58,11 @@ func runStatus(cmd *cobra.Command, args []string) error { } defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + pterm.Error.Println("Could not reach Kernel API. Check https://status.kernel.sh for updates.") + return fmt.Errorf("status request failed: %s", resp.Status) + } + var status statusResponse if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { return fmt.Errorf("invalid response: %w", err) From 6c3272b485be1149ab0a9f1717ee5f4d229026f7 Mon Sep 17 00:00:00 2001 From: phani Date: Sat, 28 Feb 2026 12:48:19 -0500 Subject: [PATCH 3/3] Address PR review: extract shared GetBaseURL helper, fix double error output - Move base URL resolution to shared util.GetBaseURL() so status.go and deploy.go stay in sync. - Fix status command returning both pterm error and fmt.Errorf (double output); use pterm + return nil to match codebase convention. - Run gofmt to fix pre-existing indentation in deploy.go. --- cmd/deploy.go | 33 +++++++++++++++------------------ cmd/status.go | 17 ++++------------- pkg/util/client.go | 10 ++++++++++ 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index d82d7ea..dd57dae 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -147,10 +147,7 @@ func runDeployGithub(cmd *cobra.Command, args []string) error { if strings.TrimSpace(apiKey) == "" { return fmt.Errorf("KERNEL_API_KEY is required for github deploy") } - baseURL := os.Getenv("KERNEL_BASE_URL") - if strings.TrimSpace(baseURL) == "" { - baseURL = "https://api.onkernel.com" - } + baseURL := util.GetBaseURL() var body bytes.Buffer mw := multipart.NewWriter(&body) @@ -561,22 +558,22 @@ func followDeployment(ctx context.Context, client kernel.Client, deploymentID st if err == nil { fmt.Println(string(bs)) } - // Check for terminal states - if data.Event == "deployment_state" { - deploymentState := data.AsDeploymentState() - status := deploymentState.Deployment.Status - if status == string(kernel.DeploymentGetResponseStatusFailed) || - status == string(kernel.DeploymentGetResponseStatusStopped) { - return fmt.Errorf("deployment %s: %s", status, deploymentState.Deployment.StatusReason) + // Check for terminal states + if data.Event == "deployment_state" { + deploymentState := data.AsDeploymentState() + status := deploymentState.Deployment.Status + if status == string(kernel.DeploymentGetResponseStatusFailed) || + status == string(kernel.DeploymentGetResponseStatusStopped) { + return fmt.Errorf("deployment %s: %s", status, deploymentState.Deployment.StatusReason) + } + if status == string(kernel.DeploymentGetResponseStatusRunning) { + return nil + } } - if status == string(kernel.DeploymentGetResponseStatusRunning) { - return nil + if data.Event == "error" { + errorEv := data.AsErrorEvent() + return fmt.Errorf("%s: %s", errorEv.Error.Code, errorEv.Error.Message) } - } - if data.Event == "error" { - errorEv := data.AsErrorEvent() - return fmt.Errorf("%s: %s", errorEv.Error.Code, errorEv.Error.Message) - } continue } diff --git a/cmd/status.go b/cmd/status.go index 749416c..4d1ca70 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -5,9 +5,9 @@ import ( "fmt" "net/http" "os" - "strings" "time" + "github.com/kernel/cli/pkg/util" "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -28,8 +28,6 @@ type statusResponse struct { Groups []statusGroup `json:"groups"` } -const defaultBaseURL = "https://api.onkernel.com" - var statusCmd = &cobra.Command{ Use: "status", Short: "Check the operational status of Kernel services", @@ -40,27 +38,20 @@ func init() { statusCmd.Flags().StringP("output", "o", "", "Output format (json)") } -func getBaseURL() string { - if u := os.Getenv("KERNEL_BASE_URL"); strings.TrimSpace(u) != "" { - return strings.TrimRight(u, "/") - } - return defaultBaseURL -} - func runStatus(cmd *cobra.Command, args []string) error { output, _ := cmd.Flags().GetString("output") client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Get(getBaseURL() + "/status") + resp, err := client.Get(util.GetBaseURL() + "/status") if err != nil { pterm.Error.Println("Could not reach Kernel API. Check https://status.kernel.sh for updates.") - return fmt.Errorf("request failed: %w", err) + return nil } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { pterm.Error.Println("Could not reach Kernel API. Check https://status.kernel.sh for updates.") - return fmt.Errorf("status request failed: %s", resp.Status) + return nil } var status statusResponse diff --git a/pkg/util/client.go b/pkg/util/client.go index 20faf5f..c831cc6 100644 --- a/pkg/util/client.go +++ b/pkg/util/client.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "os" + "strings" "sync/atomic" "github.com/kernel/cli/pkg/update" @@ -84,6 +85,15 @@ func showUpgradeMessage() { } } +// GetBaseURL returns the Kernel API base URL, falling back to production. +// KERNEL_BASE_URL is never set in .env; it exists solely for internal dev/staging overrides. +func GetBaseURL() string { + if u := os.Getenv("KERNEL_BASE_URL"); strings.TrimSpace(u) != "" { + return u + } + return "https://api.onkernel.com" +} + // IsNotFound returns true if the error is a Kernel API error with HTTP 404. func IsNotFound(err error) bool { if err == nil {