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..047e6f6 --- /dev/null +++ b/cmd/proxies/check.go @@ -0,0 +1,159 @@ +package proxies + +import ( + "context" + "fmt" + + "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" +) + +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("Running health check on 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 proxy details after check + 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)}) + + // Display protocol (default to https if not set) + protocol := string(proxy.Protocol) + if protocol == "" { + protocol = "https" + } + rows = append(rows, []string{"Protocol", protocol}) + + // Display IP address if available + if proxy.IPAddress != "" { + rows = append(rows, []string{"IP Address", proxy.IPAddress}) + } + + // Display type-specific config details + rows = append(rows, getProxyCheckConfigRows(proxy)...) + + // Display status with color + status := string(proxy.Status) + if status == "" { + status = "-" + } else if proxy.Status == kernel.ProxyCheckResponseStatusAvailable { + status = pterm.Green(status) + } else if proxy.Status == kernel.ProxyCheckResponseStatusUnavailable { + status = pterm.Red(status) + } + rows = append(rows, []string{"Status", status}) + + // Display last checked timestamp + lastChecked := util.FormatLocal(proxy.LastChecked) + rows = append(rows, []string{"Last Checked", lastChecked}) + + table.PrintTableNoPad(rows, true) + + // Print a summary message + if proxy.Status == kernel.ProxyCheckResponseStatusAvailable { + pterm.Success.Println("Proxy health check passed") + } else { + 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}) +} diff --git a/cmd/proxies/common_test.go b/cmd/proxies/common_test.go index 9f815a6..48f13cf 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}, 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 b6e7ffa..a266a6e 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: "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, +} + 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..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.20260117115631-ebae1efd3449 + 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 5fd947b..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.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.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=