Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 15 additions & 18 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions cmd/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package cmd

import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"

"github.com/kernel/cli/pkg/util"
"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"`
}

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 runStatus(cmd *cobra.Command, args []string) error {
output, _ := cmd.Flags().GetString("output")

client := &http.Client{Timeout: 10 * time.Second}
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 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 nil
}

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()
}
10 changes: 10 additions & 0 deletions pkg/util/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net/http"
"os"
"strings"
"sync/atomic"

"github.com/kernel/cli/pkg/update"
Expand Down Expand Up @@ -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 {
Expand Down