From e79c2b95072515f5092873648d61d6b02140e58c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 17 Jan 2026 21:01:17 +0000 Subject: [PATCH 1/3] CLI: Update Go SDK to 8dc9875576ae and add credentials + proxy check commands ## SDK Update - Updated kernel-go-sdk to 8dc9875576aea5daa08fa44faeee0bd0e99888a8 ## New Commands - `kernel credentials list` - List stored credentials - `kernel credentials get ` - Get credential details - `kernel credentials create` - Create a new credential with name, domain, values - `kernel credentials update ` - Update credential properties - `kernel credentials delete ` - Delete a credential - `kernel credentials totp-code ` - Get current TOTP code for 2FA - `kernel proxies check ` - Check proxy health status ## Coverage Analysis Full enumeration of SDK methods (api.md) vs CLI commands performed. --- cmd/credentials.go | 545 +++++++++++++++++++++++++++++++++++++++++ cmd/proxies/check.go | 73 ++++++ cmd/proxies/proxies.go | 12 + cmd/proxies/types.go | 6 + cmd/root.go | 1 + go.mod | 2 +- go.sum | 4 +- 7 files changed, 640 insertions(+), 3 deletions(-) create mode 100644 cmd/credentials.go create mode 100644 cmd/proxies/check.go diff --git a/cmd/credentials.go b/cmd/credentials.go new file mode 100644 index 0000000..15952c6 --- /dev/null +++ b/cmd/credentials.go @@ -0,0 +1,545 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/kernel/cli/pkg/util" + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/kernel/kernel-go-sdk/packages/pagination" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +// CredentialsService defines the subset of the Kernel SDK credential client that we use. +type CredentialsService interface { + New(ctx context.Context, body kernel.CredentialNewParams, opts ...option.RequestOption) (res *kernel.Credential, err error) + Get(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *kernel.Credential, err error) + Update(ctx context.Context, idOrName string, body kernel.CredentialUpdateParams, opts ...option.RequestOption) (res *kernel.Credential, err error) + List(ctx context.Context, query kernel.CredentialListParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[kernel.Credential], err error) + Delete(ctx context.Context, idOrName string, opts ...option.RequestOption) (err error) + TotpCode(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *kernel.CredentialTotpCodeResponse, err error) +} + +// CredentialsCmd handles credential operations independent of cobra. +type CredentialsCmd struct { + credentials CredentialsService +} + +type CredentialsListInput struct { + Domain string + Limit int + Offset int + Output string +} + +type CredentialsGetInput struct { + Identifier string + Output string +} + +type CredentialsCreateInput struct { + Name string + Domain string + Values map[string]string + SSOProvider string + TotpSecret string + Output string +} + +type CredentialsUpdateInput struct { + Identifier string + Name string + SSOProvider string + TotpSecret string + Values map[string]string + Output string +} + +type CredentialsDeleteInput struct { + Identifier string + SkipConfirm bool +} + +type CredentialsTotpCodeInput struct { + Identifier string + Output string +} + +func (c CredentialsCmd) List(ctx context.Context, in CredentialsListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + params := kernel.CredentialListParams{} + if in.Domain != "" { + params.Domain = kernel.Opt(in.Domain) + } + if in.Limit > 0 { + params.Limit = kernel.Opt(int64(in.Limit)) + } + if in.Offset > 0 { + params.Offset = kernel.Opt(int64(in.Offset)) + } + + page, err := c.credentials.List(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + var credentials []kernel.Credential + if page != nil { + credentials = page.Items + } + + if in.Output == "json" { + if len(credentials) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(credentials) + } + + if len(credentials) == 0 { + pterm.Info.Println("No credentials found") + return nil + } + + tableData := pterm.TableData{{"ID", "Name", "Domain", "Has TOTP", "SSO Provider", "Created At"}} + for _, cred := range credentials { + ssoProvider := cred.SSOProvider + if ssoProvider == "" { + ssoProvider = "-" + } + hasTOTP := "-" + if cred.HasTotpSecret { + hasTOTP = "Yes" + } + tableData = append(tableData, []string{ + cred.ID, + cred.Name, + cred.Domain, + hasTOTP, + ssoProvider, + util.FormatLocal(cred.CreatedAt), + }) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c CredentialsCmd) Get(ctx context.Context, in CredentialsGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + cred, err := c.credentials.Get(ctx, in.Identifier) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(cred) + } + + ssoProvider := cred.SSOProvider + if ssoProvider == "" { + ssoProvider = "-" + } + hasTOTP := "No" + if cred.HasTotpSecret { + hasTOTP = "Yes" + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", cred.ID}, + {"Name", cred.Name}, + {"Domain", cred.Domain}, + {"Has TOTP Secret", hasTOTP}, + {"SSO Provider", ssoProvider}, + {"Created At", util.FormatLocal(cred.CreatedAt)}, + {"Updated At", util.FormatLocal(cred.UpdatedAt)}, + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c CredentialsCmd) Create(ctx context.Context, in CredentialsCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Name == "" { + return fmt.Errorf("--name is required") + } + if in.Domain == "" { + return fmt.Errorf("--domain is required") + } + if len(in.Values) == 0 { + return fmt.Errorf("at least one --value is required") + } + + params := kernel.CredentialNewParams{ + CreateCredentialRequest: kernel.CreateCredentialRequestParam{ + Name: in.Name, + Domain: in.Domain, + Values: in.Values, + }, + } + if in.SSOProvider != "" { + params.CreateCredentialRequest.SSOProvider = kernel.Opt(in.SSOProvider) + } + if in.TotpSecret != "" { + params.CreateCredentialRequest.TotpSecret = kernel.Opt(in.TotpSecret) + } + + if in.Output != "json" { + pterm.Info.Printf("Creating credential '%s'...\n", in.Name) + } + + cred, err := c.credentials.New(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(cred) + } + + pterm.Success.Printf("Created credential: %s\n", cred.ID) + + ssoProvider := cred.SSOProvider + if ssoProvider == "" { + ssoProvider = "-" + } + hasTOTP := "No" + if cred.HasTotpSecret { + hasTOTP = "Yes" + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", cred.ID}, + {"Name", cred.Name}, + {"Domain", cred.Domain}, + {"Has TOTP Secret", hasTOTP}, + {"SSO Provider", ssoProvider}, + } + + PrintTableNoPad(tableData, true) + + // If TOTP was configured and we got a code back, show it + if cred.TotpCode != "" { + pterm.Info.Printf("Initial TOTP Code: %s (expires: %s)\n", cred.TotpCode, util.FormatLocal(cred.TotpCodeExpiresAt)) + } + + return nil +} + +func (c CredentialsCmd) Update(ctx context.Context, in CredentialsUpdateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + params := kernel.CredentialUpdateParams{ + UpdateCredentialRequest: kernel.UpdateCredentialRequestParam{}, + } + if in.Name != "" { + params.UpdateCredentialRequest.Name = kernel.Opt(in.Name) + } + if in.SSOProvider != "" { + params.UpdateCredentialRequest.SSOProvider = kernel.Opt(in.SSOProvider) + } + if in.TotpSecret != "" { + params.UpdateCredentialRequest.TotpSecret = kernel.Opt(in.TotpSecret) + } + if len(in.Values) > 0 { + params.UpdateCredentialRequest.Values = in.Values + } + + if in.Output != "json" { + pterm.Info.Printf("Updating credential '%s'...\n", in.Identifier) + } + + cred, err := c.credentials.Update(ctx, in.Identifier, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(cred) + } + + pterm.Success.Printf("Updated credential: %s\n", cred.ID) + return nil +} + +func (c CredentialsCmd) Delete(ctx context.Context, in CredentialsDeleteInput) error { + if !in.SkipConfirm { + msg := fmt.Sprintf("Are you sure you want to delete credential '%s'?", in.Identifier) + pterm.DefaultInteractiveConfirm.DefaultText = msg + ok, _ := pterm.DefaultInteractiveConfirm.Show() + if !ok { + pterm.Info.Println("Deletion cancelled") + return nil + } + } + + if err := c.credentials.Delete(ctx, in.Identifier); err != nil { + if util.IsNotFound(err) { + pterm.Info.Printf("Credential '%s' not found\n", in.Identifier) + return nil + } + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Deleted credential: %s\n", in.Identifier) + return nil +} + +func (c CredentialsCmd) TotpCode(ctx context.Context, in CredentialsTotpCodeInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + resp, err := c.credentials.TotpCode(ctx, in.Identifier) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(resp) + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"TOTP Code", resp.Code}, + {"Expires At", util.FormatLocal(resp.ExpiresAt)}, + } + + PrintTableNoPad(tableData, true) + return nil +} + +// --- Cobra wiring --- + +var credentialsCmd = &cobra.Command{ + Use: "credentials", + Aliases: []string{"credential", "creds", "cred"}, + Short: "Manage stored credentials", + Long: "Commands for managing stored credentials for automatic re-authentication", +} + +var credentialsListCmd = &cobra.Command{ + Use: "list", + Short: "List credentials", + Args: cobra.NoArgs, + RunE: runCredentialsList, +} + +var credentialsGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get a credential by ID or name", + Args: cobra.ExactArgs(1), + RunE: runCredentialsGet, +} + +var credentialsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new credential", + Long: `Create a new credential for storing login information. + +Examples: + # Create a simple credential with username/password + kernel credentials create --name "my-site" --domain "example.com" --value "username=myuser" --value "password=mypass" + + # Create a credential with TOTP for 2FA + kernel credentials create --name "my-2fa-site" --domain "example.com" --value "username=myuser" --value "password=mypass" --totp-secret "JBSWY3DPEHPK3PXP" + + # Create a credential with SSO provider + kernel credentials create --name "google-sso" --domain "example.com" --value "email=user@gmail.com" --value "password=mypass" --sso-provider google`, + Args: cobra.NoArgs, + RunE: runCredentialsCreate, +} + +var credentialsUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update a credential", + Long: `Update a credential's name, SSO provider, TOTP secret, or values.`, + Args: cobra.ExactArgs(1), + RunE: runCredentialsUpdate, +} + +var credentialsDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a credential", + Args: cobra.ExactArgs(1), + RunE: runCredentialsDelete, +} + +var credentialsTotpCodeCmd = &cobra.Command{ + Use: "totp-code ", + Short: "Get the current TOTP code for a credential", + Long: `Returns the current 6-digit TOTP code for a credential with a configured totp_secret.`, + Args: cobra.ExactArgs(1), + RunE: runCredentialsTotpCode, +} + +func init() { + credentialsCmd.AddCommand(credentialsListCmd) + credentialsCmd.AddCommand(credentialsGetCmd) + credentialsCmd.AddCommand(credentialsCreateCmd) + credentialsCmd.AddCommand(credentialsUpdateCmd) + credentialsCmd.AddCommand(credentialsDeleteCmd) + credentialsCmd.AddCommand(credentialsTotpCodeCmd) + + // List flags + credentialsListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + credentialsListCmd.Flags().String("domain", "", "Filter by domain") + credentialsListCmd.Flags().Int("limit", 0, "Maximum number of results to return") + credentialsListCmd.Flags().Int("offset", 0, "Number of results to skip") + + // Get flags + credentialsGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // Create flags + credentialsCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + credentialsCreateCmd.Flags().String("name", "", "Unique name for the credential (required)") + credentialsCreateCmd.Flags().String("domain", "", "Target domain this credential is for (required)") + credentialsCreateCmd.Flags().StringArray("value", []string{}, "Field name=value pair (repeatable, e.g., --value username=myuser --value password=mypass)") + credentialsCreateCmd.Flags().String("sso-provider", "", "SSO provider (e.g., google, github, microsoft)") + credentialsCreateCmd.Flags().String("totp-secret", "", "Base32-encoded TOTP secret for 2FA") + _ = credentialsCreateCmd.MarkFlagRequired("name") + _ = credentialsCreateCmd.MarkFlagRequired("domain") + + // Update flags + credentialsUpdateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + credentialsUpdateCmd.Flags().String("name", "", "New name for the credential") + credentialsUpdateCmd.Flags().String("sso-provider", "", "SSO provider (set to empty string to remove)") + credentialsUpdateCmd.Flags().String("totp-secret", "", "Base32-encoded TOTP secret (set to empty string to remove)") + credentialsUpdateCmd.Flags().StringArray("value", []string{}, "Field name=value pair to update (repeatable)") + + // Delete flags + credentialsDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + + // TOTP code flags + credentialsTotpCodeCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") +} + +func runCredentialsList(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + domain, _ := cmd.Flags().GetString("domain") + limit, _ := cmd.Flags().GetInt("limit") + offset, _ := cmd.Flags().GetInt("offset") + + svc := client.Credentials + c := CredentialsCmd{credentials: &svc} + return c.List(cmd.Context(), CredentialsListInput{ + Domain: domain, + Limit: limit, + Offset: offset, + Output: output, + }) +} + +func runCredentialsGet(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.Credentials + c := CredentialsCmd{credentials: &svc} + return c.Get(cmd.Context(), CredentialsGetInput{ + Identifier: args[0], + Output: output, + }) +} + +func runCredentialsCreate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + domain, _ := cmd.Flags().GetString("domain") + valuePairs, _ := cmd.Flags().GetStringArray("value") + ssoProvider, _ := cmd.Flags().GetString("sso-provider") + totpSecret, _ := cmd.Flags().GetString("totp-secret") + + // Parse value pairs into map + values := make(map[string]string) + for _, pair := range valuePairs { + parts := strings.SplitN(pair, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid value format: %s (expected key=value)", pair) + } + values[parts[0]] = parts[1] + } + + svc := client.Credentials + c := CredentialsCmd{credentials: &svc} + return c.Create(cmd.Context(), CredentialsCreateInput{ + Name: name, + Domain: domain, + Values: values, + SSOProvider: ssoProvider, + TotpSecret: totpSecret, + Output: output, + }) +} + +func runCredentialsUpdate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + ssoProvider, _ := cmd.Flags().GetString("sso-provider") + totpSecret, _ := cmd.Flags().GetString("totp-secret") + valuePairs, _ := cmd.Flags().GetStringArray("value") + + // Parse value pairs into map + values := make(map[string]string) + for _, pair := range valuePairs { + parts := strings.SplitN(pair, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid value format: %s (expected key=value)", pair) + } + values[parts[0]] = parts[1] + } + + svc := client.Credentials + c := CredentialsCmd{credentials: &svc} + return c.Update(cmd.Context(), CredentialsUpdateInput{ + Identifier: args[0], + Name: name, + SSOProvider: ssoProvider, + TotpSecret: totpSecret, + Values: values, + Output: output, + }) +} + +func runCredentialsDelete(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + skip, _ := cmd.Flags().GetBool("yes") + + svc := client.Credentials + c := CredentialsCmd{credentials: &svc} + return c.Delete(cmd.Context(), CredentialsDeleteInput{ + Identifier: args[0], + SkipConfirm: skip, + }) +} + +func runCredentialsTotpCode(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.Credentials + c := CredentialsCmd{credentials: &svc} + return c.TotpCode(cmd.Context(), CredentialsTotpCodeInput{ + Identifier: args[0], + Output: output, + }) +} diff --git a/cmd/proxies/check.go b/cmd/proxies/check.go new file mode 100644 index 0000000..2cdb1ef --- /dev/null +++ b/cmd/proxies/check.go @@ -0,0 +1,73 @@ +package proxies + +import ( + "context" + "fmt" + + "github.com/kernel/cli/pkg/table" + "github.com/kernel/cli/pkg/util" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +func (p ProxyCmd) Check(ctx context.Context, in ProxyCheckInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Output != "json" { + pterm.Info.Printf("Checking proxy %s...\n", in.ID) + } + + proxy, err := p.proxies.Check(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(proxy) + } + + // Display check result + rows := pterm.TableData{{"Property", "Value"}} + rows = append(rows, []string{"ID", proxy.ID}) + + name := proxy.Name + if name == "" { + name = "-" + } + rows = append(rows, []string{"Name", name}) + rows = append(rows, []string{"Type", string(proxy.Type)}) + rows = append(rows, []string{"Status", string(proxy.Status)}) + rows = append(rows, []string{"IP Address", proxy.IPAddress}) + + protocol := string(proxy.Protocol) + if protocol == "" { + protocol = "https" + } + rows = append(rows, []string{"Protocol", protocol}) + rows = append(rows, []string{"Last Checked", util.FormatLocal(proxy.LastChecked)}) + + table.PrintTableNoPad(rows, true) + + // Show status message + if proxy.Status == "available" { + pterm.Success.Println("Proxy is available and working") + } else { + pterm.Warning.Println("Proxy is unavailable") + } + + return nil +} + +func runProxiesCheck(cmd *cobra.Command, args []string) error { + client := util.GetKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.Proxies + p := ProxyCmd{proxies: &svc} + return p.Check(cmd.Context(), ProxyCheckInput{ + ID: args[0], + Output: output, + }) +} diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index b6e7ffa..e18d672 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -60,12 +60,21 @@ var proxiesDeleteCmd = &cobra.Command{ RunE: runProxiesDelete, } +var proxiesCheckCmd = &cobra.Command{ + Use: "check ", + Short: "Check a proxy's health status", + Long: `Perform a health check on a proxy configuration and return its current status.`, + Args: cobra.ExactArgs(1), + RunE: runProxiesCheck, +} + func init() { // Add subcommands ProxiesCmd.AddCommand(proxiesListCmd) ProxiesCmd.AddCommand(proxiesGetCmd) ProxiesCmd.AddCommand(proxiesCreateCmd) ProxiesCmd.AddCommand(proxiesDeleteCmd) + ProxiesCmd.AddCommand(proxiesCheckCmd) // Add output flags proxiesListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") @@ -99,4 +108,7 @@ func init() { // Delete flags proxiesDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + + // Check flags + proxiesCheckCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") } diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go index 6da63df..1583023 100644 --- a/cmd/proxies/types.go +++ b/cmd/proxies/types.go @@ -13,6 +13,7 @@ type ProxyService interface { Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyGetResponse, err error) New(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (res *kernel.ProxyNewResponse, err error) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) + Check(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyCheckResponse, err error) } // ProxyCmd handles proxy operations independent of cobra. @@ -56,3 +57,8 @@ type ProxyDeleteInput struct { ID string SkipConfirm bool } + +type ProxyCheckInput struct { + ID string + Output string +} diff --git a/cmd/root.go b/cmd/root.go index d397e88..318a14d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -139,6 +139,7 @@ func init() { rootCmd.AddCommand(profilesCmd) rootCmd.AddCommand(proxies.ProxiesCmd) rootCmd.AddCommand(extensionsCmd) + rootCmd.AddCommand(credentialsCmd) rootCmd.AddCommand(createCmd) rootCmd.AddCommand(mcp.MCPCmd) rootCmd.AddCommand(upgradeCmd) diff --git a/go.mod b/go.mod index dcc9f04..2f4d413 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.26.1-0.20260117115631-ebae1efd3449 + github.com/kernel/kernel-go-sdk v0.26.1-0.20260117205732-8dc9875576ae github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index 5fd947b..3d4ed39 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.26.1-0.20260117115631-ebae1efd3449 h1:NDrHon1ahRBI1xlatalhEUxjRk03EX5MtZ7Q1sapsLs= -github.com/kernel/kernel-go-sdk v0.26.1-0.20260117115631-ebae1efd3449/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.26.1-0.20260117205732-8dc9875576ae h1:sd0kXEx51lv8iqWxXZKpiTYoKjmso/qIaHUe23VVj28= +github.com/kernel/kernel-go-sdk v0.26.1-0.20260117205732-8dc9875576ae/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From 945bd3b1d8405d6e44d5932e64687deb98e2dfd8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 17 Jan 2026 20:44:25 +0000 Subject: [PATCH 2/3] CLI: Update SDK to 1372840d2981 and add proxies check command Update kernel-go-sdk to 1372840d29819177742d1951a0a0594261e753dd. Add missing CLI command for SDK method: - `kernel proxies check ` for `client.Proxies.Check()` The check command runs a health check on a proxy to verify it's working and displays the updated status. --- cmd/proxies/check.go | 126 +++++++++++++++++++++++++++++++------ cmd/proxies/common_test.go | 8 +++ cmd/proxies/proxies.go | 4 +- 3 files changed, 116 insertions(+), 22 deletions(-) diff --git a/cmd/proxies/check.go b/cmd/proxies/check.go index 2cdb1ef..ea7baee 100644 --- a/cmd/proxies/check.go +++ b/cmd/proxies/check.go @@ -6,6 +6,7 @@ import ( "github.com/kernel/cli/pkg/table" "github.com/kernel/cli/pkg/util" + "github.com/kernel/kernel-go-sdk" "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -16,58 +17,143 @@ func (p ProxyCmd) Check(ctx context.Context, in ProxyCheckInput) error { } if in.Output != "json" { - pterm.Info.Printf("Checking proxy %s...\n", in.ID) + pterm.Info.Printf("Running health check on proxy %s...\n", in.ID) } - proxy, err := p.proxies.Check(ctx, in.ID) + item, err := p.proxies.Check(ctx, in.ID) if err != nil { return util.CleanedUpSdkError{Err: err} } if in.Output == "json" { - return util.PrintPrettyJSON(proxy) + return util.PrintPrettyJSON(item) } - // Display check result + // Display proxy details after check rows := pterm.TableData{{"Property", "Value"}} - rows = append(rows, []string{"ID", proxy.ID}) - name := proxy.Name + rows = append(rows, []string{"ID", item.ID}) + + name := item.Name if name == "" { name = "-" } rows = append(rows, []string{"Name", name}) - rows = append(rows, []string{"Type", string(proxy.Type)}) - rows = append(rows, []string{"Status", string(proxy.Status)}) - rows = append(rows, []string{"IP Address", proxy.IPAddress}) + rows = append(rows, []string{"Type", string(item.Type)}) - protocol := string(proxy.Protocol) + // Display protocol (default to https if not set) + protocol := string(item.Protocol) if protocol == "" { protocol = "https" } rows = append(rows, []string{"Protocol", protocol}) - rows = append(rows, []string{"Last Checked", util.FormatLocal(proxy.LastChecked)}) + + // Display IP address if available + if item.IPAddress != "" { + rows = append(rows, []string{"IP Address", item.IPAddress}) + } + + // Display type-specific config details + rows = append(rows, getProxyCheckConfigRows(item)...) + + // Display status with color + status := string(item.Status) + if status == "" { + status = "-" + } else if item.Status == kernel.ProxyCheckResponseStatusAvailable { + status = pterm.Green(status) + } else if item.Status == kernel.ProxyCheckResponseStatusUnavailable { + status = pterm.Red(status) + } + rows = append(rows, []string{"Status", status}) + + // Display last checked timestamp + lastChecked := util.FormatLocal(item.LastChecked) + rows = append(rows, []string{"Last Checked", lastChecked}) table.PrintTableNoPad(rows, true) - // Show status message - if proxy.Status == "available" { - pterm.Success.Println("Proxy is available and working") + // Print a summary message + if item.Status == kernel.ProxyCheckResponseStatusAvailable { + pterm.Success.Println("Proxy health check passed") } else { - pterm.Warning.Println("Proxy is unavailable") + pterm.Warning.Println("Proxy health check failed - proxy is unavailable") } return nil } +func getProxyCheckConfigRows(proxy *kernel.ProxyCheckResponse) [][]string { + var rows [][]string + config := &proxy.Config + + switch proxy.Type { + case kernel.ProxyCheckResponseTypeDatacenter, kernel.ProxyCheckResponseTypeIsp: + if config.Country != "" { + rows = append(rows, []string{"Country", config.Country}) + } + case kernel.ProxyCheckResponseTypeResidential: + if config.Country != "" { + rows = append(rows, []string{"Country", config.Country}) + } + if config.City != "" { + rows = append(rows, []string{"City", config.City}) + } + if config.State != "" { + rows = append(rows, []string{"State", config.State}) + } + if config.Zip != "" { + rows = append(rows, []string{"ZIP", config.Zip}) + } + if config.Asn != "" { + rows = append(rows, []string{"ASN", config.Asn}) + } + if config.Os != "" { + rows = append(rows, []string{"OS", config.Os}) + } + case kernel.ProxyCheckResponseTypeMobile: + if config.Country != "" { + rows = append(rows, []string{"Country", config.Country}) + } + if config.City != "" { + rows = append(rows, []string{"City", config.City}) + } + if config.State != "" { + rows = append(rows, []string{"State", config.State}) + } + if config.Zip != "" { + rows = append(rows, []string{"ZIP", config.Zip}) + } + if config.Asn != "" { + rows = append(rows, []string{"ASN", config.Asn}) + } + if config.Carrier != "" { + rows = append(rows, []string{"Carrier", config.Carrier}) + } + case kernel.ProxyCheckResponseTypeCustom: + if config.Host != "" { + rows = append(rows, []string{"Host", config.Host}) + } + if config.Port != 0 { + rows = append(rows, []string{"Port", fmt.Sprintf("%d", config.Port)}) + } + if config.Username != "" { + rows = append(rows, []string{"Username", config.Username}) + } + hasPassword := "No" + if config.HasPassword { + hasPassword = "Yes" + } + rows = append(rows, []string{"Has Password", hasPassword}) + } + + return rows +} + func runProxiesCheck(cmd *cobra.Command, args []string) error { client := util.GetKernelClient(cmd) output, _ := cmd.Flags().GetString("output") - svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Check(cmd.Context(), ProxyCheckInput{ - ID: args[0], - Output: output, - }) + return p.Check(cmd.Context(), ProxyCheckInput{ID: args[0], Output: output}) } diff --git a/cmd/proxies/common_test.go b/cmd/proxies/common_test.go index 9f815a6..8612845 100644 --- a/cmd/proxies/common_test.go +++ b/cmd/proxies/common_test.go @@ -41,6 +41,7 @@ type FakeProxyService struct { GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) NewFunc func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error + CheckFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) } func (f *FakeProxyService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) { @@ -72,6 +73,13 @@ func (f *FakeProxyService) Delete(ctx context.Context, id string, opts ...option return nil } +func (f *FakeProxyService) Check(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { + if f.CheckFunc != nil { + return f.CheckFunc(ctx, id, opts...) + } + return &kernel.ProxyCheckResponse{ID: id, Type: kernel.ProxyCheckResponseTypeDatacenter, Status: kernel.ProxyCheckResponseStatusAvailable}, nil +} + // Helper function to create test proxy responses func createDatacenterProxy(id, name, country string) kernel.ProxyListResponse { return kernel.ProxyListResponse{ diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index e18d672..a266a6e 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -62,8 +62,8 @@ var proxiesDeleteCmd = &cobra.Command{ var proxiesCheckCmd = &cobra.Command{ Use: "check ", - Short: "Check a proxy's health status", - Long: `Perform a health check on a proxy configuration and return its current status.`, + Short: "Run a health check on a proxy", + Long: "Run a health check on a proxy to verify it's working and update its status.", Args: cobra.ExactArgs(1), RunE: runProxiesCheck, } From 057e59a76b3fc766baa29d45c4a6170208a41874 Mon Sep 17 00:00:00 2001 From: Mason Williams <43387599+masnwilliams@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:09:09 -0500 Subject: [PATCH 3/3] Ci fix/cli coverage update (#81) > [!NOTE] > Streamlines proxy check command and aligns tests with updated defaults. > > - Refactors `cmd/proxies/check.go` to use `proxy` var consistently; updates all field references in table/status output > - Test fake updated: `FakeProxyService.Check` no longer defaults `Status` to `Available` > - Updates `github.com/kernel/kernel-go-sdk` pseudo-version in `go.mod`/`go.sum` > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d999adb0b4f15e448872219cd6719c39e29fe9c0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent Co-authored-by: Cursor Agent --- cmd/proxies/check.go | 28 ++++++++++++++-------------- cmd/proxies/common_test.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cmd/proxies/check.go b/cmd/proxies/check.go index ea7baee..047e6f6 100644 --- a/cmd/proxies/check.go +++ b/cmd/proxies/check.go @@ -20,61 +20,61 @@ func (p ProxyCmd) Check(ctx context.Context, in ProxyCheckInput) error { pterm.Info.Printf("Running health check on proxy %s...\n", in.ID) } - item, err := p.proxies.Check(ctx, in.ID) + proxy, err := p.proxies.Check(ctx, in.ID) if err != nil { return util.CleanedUpSdkError{Err: err} } if in.Output == "json" { - return util.PrintPrettyJSON(item) + return util.PrintPrettyJSON(proxy) } // Display proxy details after check rows := pterm.TableData{{"Property", "Value"}} - rows = append(rows, []string{"ID", item.ID}) + rows = append(rows, []string{"ID", proxy.ID}) - name := item.Name + name := proxy.Name if name == "" { name = "-" } rows = append(rows, []string{"Name", name}) - rows = append(rows, []string{"Type", string(item.Type)}) + rows = append(rows, []string{"Type", string(proxy.Type)}) // Display protocol (default to https if not set) - protocol := string(item.Protocol) + protocol := string(proxy.Protocol) if protocol == "" { protocol = "https" } rows = append(rows, []string{"Protocol", protocol}) // Display IP address if available - if item.IPAddress != "" { - rows = append(rows, []string{"IP Address", item.IPAddress}) + if proxy.IPAddress != "" { + rows = append(rows, []string{"IP Address", proxy.IPAddress}) } // Display type-specific config details - rows = append(rows, getProxyCheckConfigRows(item)...) + rows = append(rows, getProxyCheckConfigRows(proxy)...) // Display status with color - status := string(item.Status) + status := string(proxy.Status) if status == "" { status = "-" - } else if item.Status == kernel.ProxyCheckResponseStatusAvailable { + } else if proxy.Status == kernel.ProxyCheckResponseStatusAvailable { status = pterm.Green(status) - } else if item.Status == kernel.ProxyCheckResponseStatusUnavailable { + } else if proxy.Status == kernel.ProxyCheckResponseStatusUnavailable { status = pterm.Red(status) } rows = append(rows, []string{"Status", status}) // Display last checked timestamp - lastChecked := util.FormatLocal(item.LastChecked) + lastChecked := util.FormatLocal(proxy.LastChecked) rows = append(rows, []string{"Last Checked", lastChecked}) table.PrintTableNoPad(rows, true) // Print a summary message - if item.Status == kernel.ProxyCheckResponseStatusAvailable { + if proxy.Status == kernel.ProxyCheckResponseStatusAvailable { pterm.Success.Println("Proxy health check passed") } else { pterm.Warning.Println("Proxy health check failed - proxy is unavailable") diff --git a/cmd/proxies/common_test.go b/cmd/proxies/common_test.go index 8612845..48f13cf 100644 --- a/cmd/proxies/common_test.go +++ b/cmd/proxies/common_test.go @@ -77,7 +77,7 @@ func (f *FakeProxyService) Check(ctx context.Context, id string, opts ...option. if f.CheckFunc != nil { return f.CheckFunc(ctx, id, opts...) } - return &kernel.ProxyCheckResponse{ID: id, Type: kernel.ProxyCheckResponseTypeDatacenter, Status: kernel.ProxyCheckResponseStatusAvailable}, nil + return &kernel.ProxyCheckResponse{ID: id, Type: kernel.ProxyCheckResponseTypeDatacenter}, nil } // Helper function to create test proxy responses diff --git a/go.mod b/go.mod index 2f4d413..ff373fe 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.26.1-0.20260117205732-8dc9875576ae + github.com/kernel/kernel-go-sdk v0.26.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index 3d4ed39..02041b3 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.26.1-0.20260117205732-8dc9875576ae h1:sd0kXEx51lv8iqWxXZKpiTYoKjmso/qIaHUe23VVj28= -github.com/kernel/kernel-go-sdk v0.26.1-0.20260117205732-8dc9875576ae/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.26.0 h1:IBiEohSSZN5MEZjmnfqseT3tEip6+xg7Zxr79vJYMBA= +github.com/kernel/kernel-go-sdk v0.26.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=