diff --git a/README.md b/README.md index af92648..398bb45 100644 --- a/README.md +++ b/README.md @@ -427,6 +427,76 @@ Commands with JSON output support: - `kernel proxies delete ` - Delete a proxy configuration - `-y, --yes` - Skip confirmation prompt +### Agent Auth + +Automated authentication for web services. The `run` command orchestrates the full auth flow automatically. + +- `kernel agents auth run` - Run a complete authentication flow + - `--domain ` - Target domain for authentication (required) + - `--profile ` - Profile name to use/create (required) + - `--value ` - Field name=value pair (repeatable, e.g., `--value username=foo --value password=bar`) + - `--credential ` - Existing credential name to use + - `--save-credential-as ` - Save provided credentials under this name + - `--totp-secret ` - Base32 TOTP secret for automatic 2FA + - `--proxy-id ` - Proxy ID to use + - `--login-url ` - Custom login page URL + - `--allowed-domain ` - Additional allowed domains (repeatable) + - `--timeout ` - Maximum time to wait for auth completion (default: 5m) + - `--open` - Open live view URL in browser when human intervention needed + - `--output json`, `-o json` - Output JSONL events + +- `kernel agents auth create` - Create an auth agent + - `--domain ` - Target domain for authentication (required) + - `--profile-name ` - Name of the profile to use (required) + - `--credential-name ` - Optional credential name to link + - `--login-url ` - Optional login page URL + - `--allowed-domain ` - Additional allowed domains (repeatable) + - `--proxy-id ` - Optional proxy ID to use + - `--output json`, `-o json` - Output raw JSON object + +- `kernel agents auth list` - List auth agents + - `--domain ` - Filter by domain + - `--profile-name ` - Filter by profile name + - `--limit ` - Maximum number of results to return + - `--offset ` - Number of results to skip + - `--output json`, `-o json` - Output raw JSON array + +- `kernel agents auth get ` - Get an auth agent by ID + - `--output json`, `-o json` - Output raw JSON object + +- `kernel agents auth delete ` - Delete an auth agent + - `-y, --yes` - Skip confirmation prompt + +### Credentials + +- `kernel credentials create` - Create a new credential + - `--name ` - Unique name for the credential (required) + - `--domain ` - Target domain (required) + - `--value ` - Field name=value pair (repeatable) + - `--sso-provider ` - SSO provider (google, github, microsoft) + - `--totp-secret ` - Base32-encoded TOTP secret for 2FA + - `--output json`, `-o json` - Output raw JSON object + +- `kernel credentials list` - List credentials + - `--domain ` - Filter by domain + - `--output json`, `-o json` - Output raw JSON array + +- `kernel credentials get ` - Get a credential by ID or name + - `--output json`, `-o json` - Output raw JSON object + +- `kernel credentials update ` - Update a credential + - `--name ` - New name + - `--value ` - Field values to update (repeatable) + - `--sso-provider ` - SSO provider + - `--totp-secret ` - TOTP secret + - `--output json`, `-o json` - Output raw JSON object + +- `kernel credentials delete ` - Delete a credential + - `-y, --yes` - Skip confirmation prompt + +- `kernel credentials totp-code ` - Get current TOTP code + - `--output json`, `-o json` - Output raw JSON object + ## Examples ### Create a new app @@ -641,6 +711,35 @@ kernel proxies get prx_123 kernel proxies delete prx_123 --yes ``` +### Agent auth + +```bash +# Run a complete auth flow with inline credentials +kernel agents auth run --domain github.com --profile my-github \ + --value username=myuser --value password=mypass + +# Auth with TOTP for automatic 2FA handling +kernel agents auth run --domain github.com --profile my-github \ + --value username=myuser --value password=mypass \ + --totp-secret JBSWY3DPEHPK3PXP + +# Save credentials for future re-auth +kernel agents auth run --domain github.com --profile my-github \ + --value username=myuser --value password=mypass \ + --save-credential-as github-creds + +# Re-use existing saved credential +kernel agents auth run --domain github.com --profile my-github \ + --credential github-creds + +# Auto-open browser when human intervention is needed +kernel agents auth run --domain github.com --profile my-github \ + --credential github-creds --open + +# Use the authenticated profile with a browser +kernel browsers create --profile-name my-github +``` + ## Getting Help - `kernel --help` - Show all available commands diff --git a/cmd/agents.go b/cmd/agents.go new file mode 100644 index 0000000..718306f --- /dev/null +++ b/cmd/agents.go @@ -0,0 +1,1360 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "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/pkg/browser" + "github.com/pquerna/otp/totp" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +// AgentAuthService defines the subset of the Kernel SDK agent auth client that we use. +type AgentAuthService interface { + New(ctx context.Context, body kernel.AgentAuthNewParams, opts ...option.RequestOption) (res *kernel.AuthAgent, err error) + Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.AuthAgent, err error) + List(ctx context.Context, query kernel.AgentAuthListParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[kernel.AuthAgent], err error) + Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) +} + +// AgentAuthInvocationsService defines the subset of the Kernel SDK agent auth invocations client that we use. +type AgentAuthInvocationsService interface { + New(ctx context.Context, body kernel.AgentAuthInvocationNewParams, opts ...option.RequestOption) (res *kernel.AuthAgentInvocationCreateResponse, err error) + Get(ctx context.Context, invocationID string, opts ...option.RequestOption) (res *kernel.AgentAuthInvocationResponse, err error) + Exchange(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationExchangeParams, opts ...option.RequestOption) (res *kernel.AgentAuthInvocationExchangeResponse, err error) + Submit(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationSubmitParams, opts ...option.RequestOption) (res *kernel.AgentAuthSubmitResponse, err error) +} + +// AgentAuthCmd handles agent auth operations independent of cobra. +type AgentAuthCmd struct { + auth AgentAuthService + invocations AgentAuthInvocationsService +} + +type AgentAuthCreateInput struct { + Domain string + ProfileName string + CredentialName string + LoginURL string + AllowedDomains []string + ProxyID string + Output string +} + +type AgentAuthGetInput struct { + ID string + Output string +} + +type AgentAuthListInput struct { + Domain string + ProfileName string + Limit int + Offset int + Output string +} + +type AgentAuthDeleteInput struct { + ID string + SkipConfirm bool +} + +type AgentAuthInvocationCreateInput struct { + AuthAgentID string + SaveCredentialAs string + Output string +} + +type AgentAuthInvocationGetInput struct { + InvocationID string + Output string +} + +type AgentAuthInvocationExchangeInput struct { + InvocationID string + Code string + Output string +} + +type AgentAuthInvocationSubmitInput struct { + InvocationID string + FieldValues map[string]string + SSOButton string + SelectedMfaType string + Output string +} + +// AgentAuthRunInput contains all parameters for the automated auth run flow. +type AgentAuthRunInput struct { + Domain string + ProfileName string + Values map[string]string + CredentialName string + SaveCredentialAs string + TotpSecret string + ProxyID string + LoginURL string + AllowedDomains []string + Timeout time.Duration + OpenLiveView bool + Output string +} + +// AgentAuthRunResult is the result of a successful auth run. +type AgentAuthRunResult struct { + ProfileName string `json:"profile_name"` + ProfileID string `json:"profile_id"` + Domain string `json:"domain"` + AuthAgentID string `json:"auth_agent_id"` +} + +// AgentAuthRunEvent represents a status update during the auth run (for JSON output). +type AgentAuthRunEvent struct { + Type string `json:"type"` // status, error, success, waiting + Step string `json:"step,omitempty"` + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` + LiveViewURL string `json:"live_view_url,omitempty"` +} + +// AgentAuthRunCmd handles the automated auth run flow. +type AgentAuthRunCmd struct { + auth AgentAuthService + invocations AgentAuthInvocationsService + profiles ProfilesService + credentials CredentialsService +} + +func (c AgentAuthCmd) Create(ctx context.Context, in AgentAuthCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Domain == "" { + return fmt.Errorf("--domain is required") + } + if in.ProfileName == "" { + return fmt.Errorf("--profile-name is required") + } + + params := kernel.AgentAuthNewParams{ + AuthAgentCreateRequest: kernel.AuthAgentCreateRequestParam{ + Domain: in.Domain, + ProfileName: in.ProfileName, + }, + } + if in.CredentialName != "" { + params.AuthAgentCreateRequest.CredentialName = kernel.Opt(in.CredentialName) + } + if in.LoginURL != "" { + params.AuthAgentCreateRequest.LoginURL = kernel.Opt(in.LoginURL) + } + if len(in.AllowedDomains) > 0 { + params.AuthAgentCreateRequest.AllowedDomains = in.AllowedDomains + } + if in.ProxyID != "" { + params.AuthAgentCreateRequest.Proxy = kernel.AuthAgentCreateRequestProxyParam{ + ProxyID: kernel.Opt(in.ProxyID), + } + } + + if in.Output != "json" { + pterm.Info.Printf("Creating auth agent for %s...\n", in.Domain) + } + + agent, err := c.auth.New(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(agent) + } + + pterm.Success.Printf("Created auth agent: %s\n", agent.ID) + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", agent.ID}, + {"Domain", agent.Domain}, + {"Profile Name", agent.ProfileName}, + {"Status", string(agent.Status)}, + {"Can Reauth", fmt.Sprintf("%t", agent.CanReauth)}, + } + if agent.CredentialName != "" { + tableData = append(tableData, []string{"Credential Name", agent.CredentialName}) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AgentAuthCmd) Get(ctx context.Context, in AgentAuthGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + agent, err := c.auth.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(agent) + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", agent.ID}, + {"Domain", agent.Domain}, + {"Profile Name", agent.ProfileName}, + {"Status", string(agent.Status)}, + {"Can Reauth", fmt.Sprintf("%t", agent.CanReauth)}, + {"Has Selectors", fmt.Sprintf("%t", agent.HasSelectors)}, + } + if agent.CredentialID != "" { + tableData = append(tableData, []string{"Credential ID", agent.CredentialID}) + } + if agent.CredentialName != "" { + tableData = append(tableData, []string{"Credential Name", agent.CredentialName}) + } + if agent.PostLoginURL != "" { + tableData = append(tableData, []string{"Post-Login URL", agent.PostLoginURL}) + } + if !agent.LastAuthCheckAt.IsZero() { + tableData = append(tableData, []string{"Last Auth Check", util.FormatLocal(agent.LastAuthCheckAt)}) + } + if len(agent.AllowedDomains) > 0 { + tableData = append(tableData, []string{"Allowed Domains", strings.Join(agent.AllowedDomains, ", ")}) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AgentAuthCmd) List(ctx context.Context, in AgentAuthListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + params := kernel.AgentAuthListParams{} + if in.Domain != "" { + params.Domain = kernel.Opt(in.Domain) + } + if in.ProfileName != "" { + params.ProfileName = kernel.Opt(in.ProfileName) + } + 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.auth.List(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + var agents []kernel.AuthAgent + if page != nil { + agents = page.Items + } + + if in.Output == "json" { + if len(agents) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(agents) + } + + if len(agents) == 0 { + pterm.Info.Println("No auth agents found") + return nil + } + + tableData := pterm.TableData{{"ID", "Domain", "Profile Name", "Status", "Can Reauth"}} + for _, agent := range agents { + tableData = append(tableData, []string{ + agent.ID, + agent.Domain, + agent.ProfileName, + string(agent.Status), + fmt.Sprintf("%t", agent.CanReauth), + }) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AgentAuthCmd) Delete(ctx context.Context, in AgentAuthDeleteInput) error { + if !in.SkipConfirm { + msg := fmt.Sprintf("Are you sure you want to delete auth agent '%s'?", in.ID) + pterm.DefaultInteractiveConfirm.DefaultText = msg + ok, _ := pterm.DefaultInteractiveConfirm.Show() + if !ok { + pterm.Info.Println("Deletion cancelled") + return nil + } + } + + if err := c.auth.Delete(ctx, in.ID); err != nil { + if util.IsNotFound(err) { + pterm.Info.Printf("Auth agent '%s' not found\n", in.ID) + return nil + } + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Deleted auth agent: %s\n", in.ID) + return nil +} + +func (c AgentAuthCmd) InvocationCreate(ctx context.Context, in AgentAuthInvocationCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.AuthAgentID == "" { + return fmt.Errorf("--auth-agent-id is required") + } + + params := kernel.AgentAuthInvocationNewParams{ + AuthAgentInvocationCreateRequest: kernel.AuthAgentInvocationCreateRequestParam{ + AuthAgentID: in.AuthAgentID, + }, + } + if in.SaveCredentialAs != "" { + params.AuthAgentInvocationCreateRequest.SaveCredentialAs = kernel.Opt(in.SaveCredentialAs) + } + + if in.Output != "json" { + pterm.Info.Println("Creating auth invocation...") + } + + resp, err := c.invocations.New(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(resp) + } + + pterm.Success.Printf("Created invocation: %s\n", resp.InvocationID) + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"Invocation ID", resp.InvocationID}, + {"Type", string(resp.Type)}, + {"Handoff Code", resp.HandoffCode}, + {"Hosted URL", resp.HostedURL}, + {"Expires At", util.FormatLocal(resp.ExpiresAt)}, + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AgentAuthCmd) InvocationGet(ctx context.Context, in AgentAuthInvocationGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + resp, err := c.invocations.Get(ctx, in.InvocationID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(resp) + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"App Name", resp.AppName}, + {"Domain", resp.Domain}, + {"Type", string(resp.Type)}, + {"Status", string(resp.Status)}, + {"Step", string(resp.Step)}, + {"Expires At", util.FormatLocal(resp.ExpiresAt)}, + } + if resp.LiveViewURL != "" { + tableData = append(tableData, []string{"Live View URL", resp.LiveViewURL}) + } + if resp.ErrorMessage != "" { + tableData = append(tableData, []string{"Error Message", resp.ErrorMessage}) + } + if resp.ExternalActionMessage != "" { + tableData = append(tableData, []string{"External Action", resp.ExternalActionMessage}) + } + if len(resp.PendingFields) > 0 { + var fields []string + for _, f := range resp.PendingFields { + fields = append(fields, f.Name) + } + tableData = append(tableData, []string{"Pending Fields", strings.Join(fields, ", ")}) + } + if len(resp.SubmittedFields) > 0 { + tableData = append(tableData, []string{"Submitted Fields", strings.Join(resp.SubmittedFields, ", ")}) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AgentAuthCmd) InvocationExchange(ctx context.Context, in AgentAuthInvocationExchangeInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Code == "" { + return fmt.Errorf("--code is required") + } + + params := kernel.AgentAuthInvocationExchangeParams{ + Code: in.Code, + } + + resp, err := c.invocations.Exchange(ctx, in.InvocationID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(resp) + } + + pterm.Success.Printf("Exchanged code for JWT\n") + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"Invocation ID", resp.InvocationID}, + {"JWT", resp.Jwt}, + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AgentAuthCmd) InvocationSubmit(ctx context.Context, in AgentAuthInvocationSubmitInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + // Validate that exactly one of the submit types is provided + hasFields := len(in.FieldValues) > 0 + hasSSO := in.SSOButton != "" + hasMFA := in.SelectedMfaType != "" + + count := 0 + if hasFields { + count++ + } + if hasSSO { + count++ + } + if hasMFA { + count++ + } + + if count == 0 { + return fmt.Errorf("must provide one of: --field (field values), --sso-button, or --mfa-type") + } + if count > 1 { + return fmt.Errorf("can only provide one of: --field (field values), --sso-button, or --mfa-type") + } + + var params kernel.AgentAuthInvocationSubmitParams + if hasFields { + params.OfFieldValues = &kernel.AgentAuthInvocationSubmitParamsBodyFieldValues{ + FieldValues: in.FieldValues, + } + } else if hasSSO { + params.OfSSOButton = &kernel.AgentAuthInvocationSubmitParamsBodySSOButton{ + SSOButton: in.SSOButton, + } + } else if hasMFA { + params.OfSelectedMfaType = &kernel.AgentAuthInvocationSubmitParamsBodySelectedMfaType{ + SelectedMfaType: in.SelectedMfaType, + } + } + + if in.Output != "json" { + pterm.Info.Println("Submitting to invocation...") + } + + resp, err := c.invocations.Submit(ctx, in.InvocationID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(resp) + } + + if resp.Accepted { + pterm.Success.Println("Submission accepted") + } else { + pterm.Warning.Println("Submission not accepted") + } + return nil +} + +const ( + totpPeriod = 30 // TOTP codes are valid for 30-second windows + minSecondsRemaining = 5 // Minimum seconds remaining before we wait for next window +) + +// generateTOTPCode generates a TOTP code from a base32 secret. +// Waits for a fresh window if needed to ensure enough time to submit the code. +// If quiet is true, suppresses human-readable console output (for JSON mode). +func generateTOTPCode(secret string, quiet bool) (string, error) { + // Check if we have enough time in the current window + now := time.Now().Unix() + secondsIntoWindow := now % totpPeriod + remaining := totpPeriod - secondsIntoWindow + + if remaining < minSecondsRemaining { + waitTime := remaining + 1 // Wait until just after the new window starts + if !quiet { + pterm.Info.Printf("TOTP window has only %ds remaining, waiting %ds for fresh window...\n", remaining, waitTime) + } + time.Sleep(time.Duration(waitTime) * time.Second) + } + + // Clean the secret (remove spaces that may be added for readability) + cleanSecret := strings.ReplaceAll(strings.ToUpper(secret), " ", "") + + code, err := totp.GenerateCode(cleanSecret, time.Now()) + if err != nil { + return "", fmt.Errorf("failed to generate TOTP code: %w", err) + } + return code, nil +} + +// Run executes the full automated auth flow: create profile, credential, auth agent, and run invocation to completion. +func (c AgentAuthRunCmd) Run(ctx context.Context, in AgentAuthRunInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Domain == "" { + return fmt.Errorf("--domain is required") + } + if in.ProfileName == "" { + return fmt.Errorf("--profile is required") + } + + // Validate that we have credentials to work with + if in.CredentialName == "" && len(in.Values) == 0 { + return fmt.Errorf("must provide either --credential or --value flags with credentials") + } + + jsonOutput := in.Output == "json" + emitEvent := func(event AgentAuthRunEvent) { + if jsonOutput { + data, _ := json.Marshal(event) + fmt.Println(string(data)) + } + } + + // Step 1: Find or create the profile + if !jsonOutput { + pterm.Info.Printf("Looking for profile '%s'...\n", in.ProfileName) + } + emitEvent(AgentAuthRunEvent{Type: "status", Message: "Looking for profile"}) + + var profileID string + profile, err := c.profiles.Get(ctx, in.ProfileName) + if err != nil { + if !util.IsNotFound(err) { + return util.CleanedUpSdkError{Err: err} + } + // Profile not found, create it + if !jsonOutput { + pterm.Info.Printf("Creating profile '%s'...\n", in.ProfileName) + } + emitEvent(AgentAuthRunEvent{Type: "status", Message: "Creating profile"}) + + newProfile, err := c.profiles.New(ctx, kernel.ProfileNewParams{ + Name: kernel.Opt(in.ProfileName), + }) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + profileID = newProfile.ID + if !jsonOutput { + pterm.Success.Printf("Created profile: %s\n", newProfile.ID) + } + } else { + profileID = profile.ID + if !jsonOutput { + pterm.Success.Printf("Found existing profile: %s\n", profile.ID) + } + } + + // Step 2: Handle credentials + var credentialName string + if in.CredentialName != "" { + // Using existing credential + credentialName = in.CredentialName + if !jsonOutput { + pterm.Info.Printf("Using existing credential '%s'\n", credentialName) + } + emitEvent(AgentAuthRunEvent{Type: "status", Message: "Using existing credential"}) + } else if in.SaveCredentialAs != "" { + // Create new credential with provided values + credentialName = in.SaveCredentialAs + if !jsonOutput { + pterm.Info.Printf("Creating credential '%s'...\n", credentialName) + } + emitEvent(AgentAuthRunEvent{Type: "status", Message: "Creating credential"}) + + params := kernel.CredentialNewParams{ + CreateCredentialRequest: kernel.CreateCredentialRequestParam{ + Name: credentialName, + Domain: in.Domain, + Values: in.Values, + }, + } + if in.TotpSecret != "" { + params.CreateCredentialRequest.TotpSecret = kernel.Opt(in.TotpSecret) + } + + _, err := c.credentials.New(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + if !jsonOutput { + pterm.Success.Printf("Created credential: %s\n", credentialName) + } + } + + // Step 3: Create auth agent + if !jsonOutput { + pterm.Info.Printf("Creating auth agent for %s...\n", in.Domain) + } + emitEvent(AgentAuthRunEvent{Type: "status", Message: "Creating auth agent"}) + + agentParams := kernel.AgentAuthNewParams{ + AuthAgentCreateRequest: kernel.AuthAgentCreateRequestParam{ + Domain: in.Domain, + ProfileName: in.ProfileName, + }, + } + if credentialName != "" { + agentParams.AuthAgentCreateRequest.CredentialName = kernel.Opt(credentialName) + } + if in.LoginURL != "" { + agentParams.AuthAgentCreateRequest.LoginURL = kernel.Opt(in.LoginURL) + } + if len(in.AllowedDomains) > 0 { + agentParams.AuthAgentCreateRequest.AllowedDomains = in.AllowedDomains + } + if in.ProxyID != "" { + agentParams.AuthAgentCreateRequest.Proxy = kernel.AuthAgentCreateRequestProxyParam{ + ProxyID: kernel.Opt(in.ProxyID), + } + } + + agent, err := c.auth.New(ctx, agentParams) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + if !jsonOutput { + pterm.Success.Printf("Created auth agent: %s\n", agent.ID) + } + + // Step 4: Create invocation + if !jsonOutput { + pterm.Info.Println("Starting authentication flow...") + } + emitEvent(AgentAuthRunEvent{Type: "status", Message: "Starting authentication"}) + + invocationParams := kernel.AgentAuthInvocationNewParams{ + AuthAgentInvocationCreateRequest: kernel.AuthAgentInvocationCreateRequestParam{ + AuthAgentID: agent.ID, + }, + } + if in.SaveCredentialAs != "" && credentialName == "" { + // Save credential during invocation if we have values but didn't create upfront + invocationParams.AuthAgentInvocationCreateRequest.SaveCredentialAs = kernel.Opt(in.SaveCredentialAs) + } + + invocation, err := c.invocations.New(ctx, invocationParams) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + // Step 5: Polling loop + deadline := time.Now().Add(in.Timeout) + pollInterval := 2 * time.Second + var lastStep string + liveViewShown := false + fieldsSubmitted := make(map[string]bool) + + if !jsonOutput { + pterm.Info.Println("Waiting for authentication to complete...") + } + + for { + if time.Now().After(deadline) { + emitEvent(AgentAuthRunEvent{Type: "error", Message: "Timeout waiting for authentication"}) + return fmt.Errorf("timeout waiting for authentication to complete") + } + + resp, err := c.invocations.Get(ctx, invocation.InvocationID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + // Emit status update if step changed + if string(resp.Step) != lastStep { + lastStep = string(resp.Step) + emitEvent(AgentAuthRunEvent{ + Type: "status", + Step: lastStep, + Status: string(resp.Status), + LiveViewURL: resp.LiveViewURL, + }) + if !jsonOutput { + pterm.Info.Printf("Step: %s (Status: %s)\n", resp.Step, resp.Status) + } + } + + // Check terminal states + switch resp.Status { + case kernel.AgentAuthInvocationResponseStatusSuccess: + if !jsonOutput { + pterm.Success.Println("Authentication successful!") + pterm.Success.Printf("Profile '%s' is now authenticated for %s\n", in.ProfileName, in.Domain) + } + result := AgentAuthRunResult{ + ProfileName: in.ProfileName, + ProfileID: profileID, + Domain: in.Domain, + AuthAgentID: agent.ID, + } + if jsonOutput { + emitEvent(AgentAuthRunEvent{Type: "success", Message: "Authentication successful"}) + data, err := json.MarshalIndent(result, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + return nil + } + return nil + + case kernel.AgentAuthInvocationResponseStatusFailed: + errMsg := "Authentication failed" + if resp.ErrorMessage != "" { + errMsg = resp.ErrorMessage + } + emitEvent(AgentAuthRunEvent{Type: "error", Message: errMsg}) + return fmt.Errorf("authentication failed: %s", errMsg) + + case kernel.AgentAuthInvocationResponseStatusExpired: + emitEvent(AgentAuthRunEvent{Type: "error", Message: "Authentication session expired"}) + return fmt.Errorf("authentication session expired") + + case kernel.AgentAuthInvocationResponseStatusCanceled: + emitEvent(AgentAuthRunEvent{Type: "error", Message: "Authentication was canceled"}) + return fmt.Errorf("authentication was canceled") + } + + // Handle awaiting_input step + if resp.Step == kernel.AgentAuthInvocationResponseStepAwaitingInput { + // Check for pending fields + if len(resp.PendingFields) > 0 { + // Build field values to submit + submitValues := make(map[string]string) + missingFields := []string{} + + for _, field := range resp.PendingFields { + fieldName := field.Name + // Check if we already submitted this field + if fieldsSubmitted[fieldName] { + continue + } + + // Try to find a matching value + if val, ok := in.Values[fieldName]; ok { + submitValues[fieldName] = val + } else { + // Check common field name aliases + matched := false + aliases := map[string][]string{ + "identifier": {"username", "email", "login"}, + "username": {"identifier", "email", "login"}, + "email": {"identifier", "username", "login"}, + "password": {"pass", "passwd"}, + } + if alts, ok := aliases[fieldName]; ok { + for _, alt := range alts { + if val, ok := in.Values[alt]; ok { + submitValues[fieldName] = val + matched = true + break + } + } + } + + // Check if this looks like a TOTP/verification code field + if !matched && in.TotpSecret != "" { + fieldLower := strings.ToLower(fieldName) + totpPatterns := []string{"totp", "code", "verification", "otp", "2fa", "mfa", "authenticator", "token"} + for _, pattern := range totpPatterns { + if strings.Contains(fieldLower, pattern) { + code, err := generateTOTPCode(in.TotpSecret, jsonOutput) + if err == nil { + submitValues[fieldName] = code + matched = true + if !jsonOutput { + pterm.Info.Printf("Generated TOTP code for field: %s\n", fieldName) + } + } + break + } + } + } + + if !matched { + missingFields = append(missingFields, fieldName) + } + } + } + + // Submit if we have values + if len(submitValues) > 0 { + if !jsonOutput { + var fieldNames []string + for k := range submitValues { + fieldNames = append(fieldNames, k) + } + pterm.Info.Printf("Submitting fields: %s\n", strings.Join(fieldNames, ", ")) + } + + submitParams := kernel.AgentAuthInvocationSubmitParams{ + OfFieldValues: &kernel.AgentAuthInvocationSubmitParamsBodyFieldValues{ + FieldValues: submitValues, + }, + } + _, err := c.invocations.Submit(ctx, invocation.InvocationID, submitParams) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + // Mark fields as submitted + for k := range submitValues { + fieldsSubmitted[k] = true + } + } + + // Show live view if we have missing fields + if len(missingFields) > 0 && !liveViewShown && resp.LiveViewURL != "" { + liveViewShown = true + emitEvent(AgentAuthRunEvent{ + Type: "waiting", + Message: fmt.Sprintf("Need human input for: %s", strings.Join(missingFields, ", ")), + LiveViewURL: resp.LiveViewURL, + }) + if !jsonOutput { + pterm.Warning.Printf("Missing values for fields: %s\n", strings.Join(missingFields, ", ")) + pterm.Info.Printf("Live view: %s\n", resp.LiveViewURL) + } + if in.OpenLiveView { + _ = browser.OpenURL(resp.LiveViewURL) + } + } + } + + // Check for MFA options + if len(resp.MfaOptions) > 0 { + // Check if TOTP is available and we have a secret + hasTOTP := false + for _, opt := range resp.MfaOptions { + if opt.Type == "totp" { + hasTOTP = true + break + } + } + + if hasTOTP && in.TotpSecret != "" { + // Generate and submit TOTP code + code, err := generateTOTPCode(in.TotpSecret, jsonOutput) + if err != nil { + return err + } + + if !jsonOutput { + pterm.Info.Println("Submitting TOTP code...") + } + + submitParams := kernel.AgentAuthInvocationSubmitParams{ + OfFieldValues: &kernel.AgentAuthInvocationSubmitParamsBodyFieldValues{ + FieldValues: map[string]string{"totp": code}, + }, + } + _, err = c.invocations.Submit(ctx, invocation.InvocationID, submitParams) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + } else if !liveViewShown && resp.LiveViewURL != "" { + // Need human for MFA + liveViewShown = true + var optTypes []string + for _, opt := range resp.MfaOptions { + optTypes = append(optTypes, opt.Type) + } + emitEvent(AgentAuthRunEvent{ + Type: "waiting", + Message: fmt.Sprintf("MFA required: %s", strings.Join(optTypes, ", ")), + LiveViewURL: resp.LiveViewURL, + }) + if !jsonOutput { + pterm.Warning.Printf("MFA required. Options: %s\n", strings.Join(optTypes, ", ")) + pterm.Info.Printf("Complete MFA at: %s\n", resp.LiveViewURL) + } + if in.OpenLiveView { + _ = browser.OpenURL(resp.LiveViewURL) + } + } + } + } + + // Handle awaiting_external_action step + if resp.Step == kernel.AgentAuthInvocationResponseStepAwaitingExternalAction && !liveViewShown { + liveViewShown = true + msg := "External action required" + if resp.ExternalActionMessage != "" { + msg = resp.ExternalActionMessage + } + emitEvent(AgentAuthRunEvent{ + Type: "waiting", + Message: msg, + LiveViewURL: resp.LiveViewURL, + }) + if !jsonOutput { + pterm.Warning.Printf("%s\n", msg) + if resp.LiveViewURL != "" { + pterm.Info.Printf("Live view: %s\n", resp.LiveViewURL) + } + } + if in.OpenLiveView && resp.LiveViewURL != "" { + _ = browser.OpenURL(resp.LiveViewURL) + } + } + + // Wait before next poll + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(pollInterval): + } + } +} + +// --- Cobra wiring --- + +var agentsCmd = &cobra.Command{ + Use: "agents", + Short: "Manage agents", + Long: "Commands for managing Kernel agents (auth, etc.)", +} + +var agentsAuthCmd = &cobra.Command{ + Use: "auth", + Short: "Manage auth agents", + Long: "Commands for managing authentication agents that handle login flows", +} + +var agentsAuthCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create an auth agent", + Long: "Create or find an auth agent for a specific domain and profile combination", + Args: cobra.NoArgs, + RunE: runAgentsAuthCreate, +} + +var agentsAuthGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get an auth agent by ID", + Args: cobra.ExactArgs(1), + RunE: runAgentsAuthGet, +} + +var agentsAuthListCmd = &cobra.Command{ + Use: "list", + Short: "List auth agents", + Args: cobra.NoArgs, + RunE: runAgentsAuthList, +} + +var agentsAuthDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete an auth agent", + Args: cobra.ExactArgs(1), + RunE: runAgentsAuthDelete, +} + +var agentsAuthInvocationsCmd = &cobra.Command{ + Use: "invocations", + Short: "Manage auth invocations", + Long: "Commands for managing authentication invocations (login flows)", +} + +var agentsAuthInvocationsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create an auth invocation", + Long: "Start a new authentication flow for an auth agent", + Args: cobra.NoArgs, + RunE: runAgentsAuthInvocationsCreate, +} + +var agentsAuthInvocationsGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get an auth invocation", + Args: cobra.ExactArgs(1), + RunE: runAgentsAuthInvocationsGet, +} + +var agentsAuthInvocationsExchangeCmd = &cobra.Command{ + Use: "exchange ", + Short: "Exchange a handoff code for a JWT", + Args: cobra.ExactArgs(1), + RunE: runAgentsAuthInvocationsExchange, +} + +var agentsAuthInvocationsSubmitCmd = &cobra.Command{ + Use: "submit ", + Short: "Submit field values to an invocation", + Long: `Submit field values, SSO button click, or MFA selection to an auth invocation. + +Examples: + # Submit field values + kernel agents auth invocations submit --field username=myuser --field password=mypass + + # Click an SSO button + kernel agents auth invocations submit --sso-button "//button[@id='google-sso']" + + # Select an MFA method + kernel agents auth invocations submit --mfa-type sms`, + Args: cobra.ExactArgs(1), + RunE: runAgentsAuthInvocationsSubmit, +} + +var agentsAuthRunCmd = &cobra.Command{ + Use: "run", + Short: "Run a complete auth flow", + Long: `Run a complete authentication flow for a domain, automatically handling credential submission and polling. + +This command orchestrates the entire agent auth process: +1. Creates or finds a profile with the given name +2. Creates a credential if --save-credential-as is specified +3. Creates an auth agent linking domain, profile, and credential +4. Starts an invocation and polls until completion +5. Auto-submits credentials when prompted +6. Auto-submits TOTP codes if --totp-secret is provided +7. Shows live view URL when human intervention is needed + +Examples: + # Basic auth with inline credentials + kernel agents auth run --domain github.com --profile my-github \ + --value username=myuser --value password=mypass + + # With TOTP for automatic 2FA + kernel agents auth run --domain github.com --profile my-github \ + --value username=myuser --value password=mypass \ + --totp-secret JBSWY3DPEHPK3PXP + + # Save credentials for future re-auth + kernel agents auth run --domain github.com --profile my-github \ + --value username=myuser --value password=mypass \ + --save-credential-as github-creds + + # Re-use existing saved credential + kernel agents auth run --domain github.com --profile my-github \ + --credential github-creds + + # Auto-open browser for human intervention + kernel agents auth run --domain github.com --profile my-github \ + --credential github-creds --open`, + Args: cobra.NoArgs, + RunE: runAgentsAuthRun, +} + +func init() { + // Auth create flags + agentsAuthCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + agentsAuthCreateCmd.Flags().String("domain", "", "Target domain for authentication (required)") + agentsAuthCreateCmd.Flags().String("profile-name", "", "Name of the profile to use (required)") + agentsAuthCreateCmd.Flags().String("credential-name", "", "Optional credential name to link for auto-fill") + agentsAuthCreateCmd.Flags().String("login-url", "", "Optional login page URL") + agentsAuthCreateCmd.Flags().StringSlice("allowed-domain", []string{}, "Additional allowed domains (repeatable)") + agentsAuthCreateCmd.Flags().String("proxy-id", "", "Optional proxy ID to use") + _ = agentsAuthCreateCmd.MarkFlagRequired("domain") + _ = agentsAuthCreateCmd.MarkFlagRequired("profile-name") + + // Auth get flags + agentsAuthGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // Auth list flags + agentsAuthListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + agentsAuthListCmd.Flags().String("domain", "", "Filter by domain") + agentsAuthListCmd.Flags().String("profile-name", "", "Filter by profile name") + agentsAuthListCmd.Flags().Int("limit", 0, "Maximum number of results to return") + agentsAuthListCmd.Flags().Int("offset", 0, "Number of results to skip") + + // Auth delete flags + agentsAuthDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + + // Invocations create flags + agentsAuthInvocationsCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + agentsAuthInvocationsCreateCmd.Flags().String("auth-agent-id", "", "ID of the auth agent (required)") + agentsAuthInvocationsCreateCmd.Flags().String("save-credential-as", "", "Save credentials under this name on success") + _ = agentsAuthInvocationsCreateCmd.MarkFlagRequired("auth-agent-id") + + // Invocations get flags + agentsAuthInvocationsGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // Invocations exchange flags + agentsAuthInvocationsExchangeCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + agentsAuthInvocationsExchangeCmd.Flags().String("code", "", "Handoff code from the start endpoint (required)") + _ = agentsAuthInvocationsExchangeCmd.MarkFlagRequired("code") + + // Invocations submit flags + agentsAuthInvocationsSubmitCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + agentsAuthInvocationsSubmitCmd.Flags().StringArray("field", []string{}, "Field name=value pair (repeatable)") + agentsAuthInvocationsSubmitCmd.Flags().String("sso-button", "", "Selector of SSO button to click") + agentsAuthInvocationsSubmitCmd.Flags().String("mfa-type", "", "MFA type to select (sms, call, email, totp, push, security_key)") + + // Auth run flags + agentsAuthRunCmd.Flags().StringP("output", "o", "", "Output format: json for JSONL events") + agentsAuthRunCmd.Flags().String("domain", "", "Target domain for authentication (required)") + agentsAuthRunCmd.Flags().String("profile", "", "Profile name to use/create (required)") + agentsAuthRunCmd.Flags().StringArray("value", []string{}, "Field name=value pair (e.g., --value username=foo --value password=bar)") + agentsAuthRunCmd.Flags().String("credential", "", "Existing credential name to use") + agentsAuthRunCmd.Flags().String("save-credential-as", "", "Save provided credentials under this name") + agentsAuthRunCmd.Flags().String("totp-secret", "", "Base32 TOTP secret for automatic 2FA") + agentsAuthRunCmd.Flags().String("proxy-id", "", "Proxy ID to use") + agentsAuthRunCmd.Flags().String("login-url", "", "Custom login page URL") + agentsAuthRunCmd.Flags().StringSlice("allowed-domain", []string{}, "Additional allowed domains") + agentsAuthRunCmd.Flags().Duration("timeout", 5*time.Minute, "Maximum time to wait for auth completion") + agentsAuthRunCmd.Flags().Bool("open", false, "Open live view URL in browser when human intervention needed") + _ = agentsAuthRunCmd.MarkFlagRequired("domain") + _ = agentsAuthRunCmd.MarkFlagRequired("profile") + + // Wire up commands + agentsAuthInvocationsCmd.AddCommand(agentsAuthInvocationsCreateCmd) + agentsAuthInvocationsCmd.AddCommand(agentsAuthInvocationsGetCmd) + agentsAuthInvocationsCmd.AddCommand(agentsAuthInvocationsExchangeCmd) + agentsAuthInvocationsCmd.AddCommand(agentsAuthInvocationsSubmitCmd) + + agentsAuthCmd.AddCommand(agentsAuthCreateCmd) + agentsAuthCmd.AddCommand(agentsAuthGetCmd) + agentsAuthCmd.AddCommand(agentsAuthListCmd) + agentsAuthCmd.AddCommand(agentsAuthDeleteCmd) + agentsAuthCmd.AddCommand(agentsAuthInvocationsCmd) + agentsAuthCmd.AddCommand(agentsAuthRunCmd) + + agentsCmd.AddCommand(agentsAuthCmd) +} + +func runAgentsAuthCreate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + domain, _ := cmd.Flags().GetString("domain") + profileName, _ := cmd.Flags().GetString("profile-name") + credentialName, _ := cmd.Flags().GetString("credential-name") + loginURL, _ := cmd.Flags().GetString("login-url") + allowedDomains, _ := cmd.Flags().GetStringSlice("allowed-domain") + proxyID, _ := cmd.Flags().GetString("proxy-id") + + svc := client.Agents.Auth + c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} + return c.Create(cmd.Context(), AgentAuthCreateInput{ + Domain: domain, + ProfileName: profileName, + CredentialName: credentialName, + LoginURL: loginURL, + AllowedDomains: allowedDomains, + ProxyID: proxyID, + Output: output, + }) +} + +func runAgentsAuthGet(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.Agents.Auth + c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} + return c.Get(cmd.Context(), AgentAuthGetInput{ + ID: args[0], + Output: output, + }) +} + +func runAgentsAuthList(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + domain, _ := cmd.Flags().GetString("domain") + profileName, _ := cmd.Flags().GetString("profile-name") + limit, _ := cmd.Flags().GetInt("limit") + offset, _ := cmd.Flags().GetInt("offset") + + svc := client.Agents.Auth + c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} + return c.List(cmd.Context(), AgentAuthListInput{ + Domain: domain, + ProfileName: profileName, + Limit: limit, + Offset: offset, + Output: output, + }) +} + +func runAgentsAuthDelete(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + skip, _ := cmd.Flags().GetBool("yes") + + svc := client.Agents.Auth + c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} + return c.Delete(cmd.Context(), AgentAuthDeleteInput{ + ID: args[0], + SkipConfirm: skip, + }) +} + +func runAgentsAuthInvocationsCreate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + authAgentID, _ := cmd.Flags().GetString("auth-agent-id") + saveCredentialAs, _ := cmd.Flags().GetString("save-credential-as") + + svc := client.Agents.Auth + c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} + return c.InvocationCreate(cmd.Context(), AgentAuthInvocationCreateInput{ + AuthAgentID: authAgentID, + SaveCredentialAs: saveCredentialAs, + Output: output, + }) +} + +func runAgentsAuthInvocationsGet(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.Agents.Auth + c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} + return c.InvocationGet(cmd.Context(), AgentAuthInvocationGetInput{ + InvocationID: args[0], + Output: output, + }) +} + +func runAgentsAuthInvocationsExchange(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + code, _ := cmd.Flags().GetString("code") + + svc := client.Agents.Auth + c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} + return c.InvocationExchange(cmd.Context(), AgentAuthInvocationExchangeInput{ + InvocationID: args[0], + Code: code, + Output: output, + }) +} + +func runAgentsAuthInvocationsSubmit(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + fieldPairs, _ := cmd.Flags().GetStringArray("field") + ssoButton, _ := cmd.Flags().GetString("sso-button") + mfaType, _ := cmd.Flags().GetString("mfa-type") + + // Parse field pairs into map + fieldValues := make(map[string]string) + for _, pair := range fieldPairs { + parts := strings.SplitN(pair, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid field format: %s (expected key=value)", pair) + } + fieldValues[parts[0]] = parts[1] + } + + svc := client.Agents.Auth + c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} + return c.InvocationSubmit(cmd.Context(), AgentAuthInvocationSubmitInput{ + InvocationID: args[0], + FieldValues: fieldValues, + SSOButton: ssoButton, + SelectedMfaType: mfaType, + Output: output, + }) +} + +func runAgentsAuthRun(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + + output, _ := cmd.Flags().GetString("output") + domain, _ := cmd.Flags().GetString("domain") + profileName, _ := cmd.Flags().GetString("profile") + valuePairs, _ := cmd.Flags().GetStringArray("value") + credentialName, _ := cmd.Flags().GetString("credential") + saveCredentialAs, _ := cmd.Flags().GetString("save-credential-as") + totpSecret, _ := cmd.Flags().GetString("totp-secret") + proxyID, _ := cmd.Flags().GetString("proxy-id") + loginURL, _ := cmd.Flags().GetString("login-url") + allowedDomains, _ := cmd.Flags().GetStringSlice("allowed-domain") + timeout, _ := cmd.Flags().GetDuration("timeout") + openLiveView, _ := cmd.Flags().GetBool("open") + + // 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] + } + + authSvc := client.Agents.Auth + profilesSvc := client.Profiles + credentialsSvc := client.Credentials + + c := AgentAuthRunCmd{ + auth: &authSvc, + invocations: &authSvc.Invocations, + profiles: &profilesSvc, + credentials: &credentialsSvc, + } + + return c.Run(cmd.Context(), AgentAuthRunInput{ + Domain: domain, + ProfileName: profileName, + Values: values, + CredentialName: credentialName, + SaveCredentialAs: saveCredentialAs, + TotpSecret: totpSecret, + ProxyID: proxyID, + LoginURL: loginURL, + AllowedDomains: allowedDomains, + Timeout: timeout, + OpenLiveView: openLiveView, + Output: output, + }) +} diff --git a/cmd/browsers.go b/cmd/browsers.go index 9297ef7..b26f38a 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -29,7 +29,7 @@ import ( // BrowsersService defines the subset of the Kernel SDK browser client that we use. // See https://github.com/kernel/kernel-go-sdk/blob/main/browser.go type BrowsersService interface { - Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.BrowserGetResponse, err error) + Get(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (res *kernel.BrowserGetResponse, err error) List(ctx context.Context, query kernel.BrowserListParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[kernel.BrowserListResponse], err error) New(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (res *kernel.BrowserNewResponse, err error) Update(ctx context.Context, id string, body kernel.BrowserUpdateParams, opts ...option.RequestOption) (res *kernel.BrowserUpdateResponse, err error) @@ -66,12 +66,20 @@ type BrowserFSService interface { type BrowserProcessService interface { Exec(ctx context.Context, id string, body kernel.BrowserProcessExecParams, opts ...option.RequestOption) (res *kernel.BrowserProcessExecResponse, err error) Kill(ctx context.Context, processID string, params kernel.BrowserProcessKillParams, opts ...option.RequestOption) (res *kernel.BrowserProcessKillResponse, err error) + Resize(ctx context.Context, processID string, params kernel.BrowserProcessResizeParams, opts ...option.RequestOption) (res *kernel.BrowserProcessResizeResponse, err error) Spawn(ctx context.Context, id string, body kernel.BrowserProcessSpawnParams, opts ...option.RequestOption) (res *kernel.BrowserProcessSpawnResponse, err error) Status(ctx context.Context, processID string, query kernel.BrowserProcessStatusParams, opts ...option.RequestOption) (res *kernel.BrowserProcessStatusResponse, err error) Stdin(ctx context.Context, processID string, params kernel.BrowserProcessStdinParams, opts ...option.RequestOption) (res *kernel.BrowserProcessStdinResponse, err error) StdoutStreamStreaming(ctx context.Context, processID string, query kernel.BrowserProcessStdoutStreamParams, opts ...option.RequestOption) (stream *ssestream.Stream[kernel.BrowserProcessStdoutStreamResponse]) } +// BrowserFWatchService defines the subset we use for browser filesystem watch APIs. +type BrowserFWatchService interface { + EventsStreaming(ctx context.Context, watchID string, query kernel.BrowserFWatchEventsParams, opts ...option.RequestOption) (stream *ssestream.Stream[kernel.BrowserFWatchEventsResponse]) + Start(ctx context.Context, id string, body kernel.BrowserFWatchStartParams, opts ...option.RequestOption) (res *kernel.BrowserFWatchStartResponse, err error) + Stop(ctx context.Context, watchID string, body kernel.BrowserFWatchStopParams, opts ...option.RequestOption) (err error) +} + // BrowserLogService defines the subset we use for browser log APIs. type BrowserLogService interface { StreamStreaming(ctx context.Context, id string, query kernel.BrowserLogStreamParams, opts ...option.RequestOption) (stream *ssestream.Stream[shared.LogEvent]) @@ -193,6 +201,7 @@ type BrowsersCmd struct { browsers BrowsersService replays BrowserReplaysService fs BrowserFSService + fsWatch BrowserFWatchService process BrowserProcessService logs BrowserLogService computer BrowserComputerService @@ -437,7 +446,7 @@ func (b BrowsersCmd) View(ctx context.Context, in BrowsersViewInput) error { return fmt.Errorf("unsupported --output value: use 'json'") } - browser, err := b.browsers.Get(ctx, in.Identifier) + browser, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -471,7 +480,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error { return fmt.Errorf("unsupported --output value: use 'json'") } - browser, err := b.browsers.Get(ctx, in.Identifier) + browser, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -574,7 +583,7 @@ func (b BrowsersCmd) LogsStream(ctx context.Context, in BrowsersLogsStreamInput) pterm.Error.Println("logs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -676,7 +685,7 @@ func (b BrowsersCmd) ComputerClickMouse(ctx context.Context, in BrowsersComputer pterm.Error.Println("computer service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -705,7 +714,7 @@ func (b BrowsersCmd) ComputerMoveMouse(ctx context.Context, in BrowsersComputerM pterm.Error.Println("computer service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -725,7 +734,7 @@ func (b BrowsersCmd) ComputerScreenshot(ctx context.Context, in BrowsersComputer pterm.Error.Println("computer service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -761,7 +770,7 @@ func (b BrowsersCmd) ComputerTypeText(ctx context.Context, in BrowsersComputerTy pterm.Error.Println("computer service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -781,7 +790,7 @@ func (b BrowsersCmd) ComputerPressKey(ctx context.Context, in BrowsersComputerPr pterm.Error.Println("computer service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -808,7 +817,7 @@ func (b BrowsersCmd) ComputerScroll(ctx context.Context, in BrowsersComputerScro pterm.Error.Println("computer service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -834,7 +843,7 @@ func (b BrowsersCmd) ComputerDragMouse(ctx context.Context, in BrowsersComputerD pterm.Error.Println("computer service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -870,7 +879,7 @@ func (b BrowsersCmd) ComputerSetCursor(ctx context.Context, in BrowsersComputerS pterm.Error.Println("computer service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -916,7 +925,7 @@ func (b BrowsersCmd) ReplaysList(ctx context.Context, in BrowsersReplaysListInpu return fmt.Errorf("unsupported --output value: use 'json'") } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -950,7 +959,7 @@ func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartIn return fmt.Errorf("unsupported --output value: use 'json'") } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -976,7 +985,7 @@ func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartIn } func (b BrowsersCmd) ReplaysStop(ctx context.Context, in BrowsersReplaysStopInput) error { - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -989,7 +998,7 @@ func (b BrowsersCmd) ReplaysStop(ctx context.Context, in BrowsersReplaysStopInpu } func (b BrowsersCmd) ReplaysDownload(ctx context.Context, in BrowsersReplaysDownloadInput) error { - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1062,6 +1071,31 @@ type BrowsersProcessStdoutStreamInput struct { ProcessID string } +type BrowsersProcessResizeInput struct { + Identifier string + ProcessID string + Cols int64 + Rows int64 +} + +// FS Watch +type BrowsersFSWatchStartInput struct { + Identifier string + Path string + Recursive BoolFlag + Output string +} + +type BrowsersFSWatchStopInput struct { + Identifier string + WatchID string +} + +type BrowsersFSWatchEventsInput struct { + Identifier string + WatchID string +} + // Playwright type BrowsersPlaywrightExecuteInput struct { Identifier string @@ -1074,7 +1108,7 @@ func (b BrowsersCmd) PlaywrightExecute(ctx context.Context, in BrowsersPlaywrigh pterm.Error.Println("playwright service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1120,7 +1154,7 @@ func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInpu pterm.Error.Println("process service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1187,7 +1221,7 @@ func (b BrowsersCmd) ProcessSpawn(ctx context.Context, in BrowsersProcessSpawnIn pterm.Error.Println("process service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1226,7 +1260,7 @@ func (b BrowsersCmd) ProcessKill(ctx context.Context, in BrowsersProcessKillInpu pterm.Error.Println("process service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1244,7 +1278,7 @@ func (b BrowsersCmd) ProcessStatus(ctx context.Context, in BrowsersProcessStatus pterm.Error.Println("process service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1262,7 +1296,7 @@ func (b BrowsersCmd) ProcessStdin(ctx context.Context, in BrowsersProcessStdinIn pterm.Error.Println("process service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1279,7 +1313,7 @@ func (b BrowsersCmd) ProcessStdoutStream(ctx context.Context, in BrowsersProcess pterm.Error.Println("process service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1308,6 +1342,97 @@ func (b BrowsersCmd) ProcessStdoutStream(ctx context.Context, in BrowsersProcess return nil } +func (b BrowsersCmd) ProcessResize(ctx context.Context, in BrowsersProcessResizeInput) error { + if b.process == nil { + pterm.Error.Println("process service not available") + return nil + } + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + params := kernel.BrowserProcessResizeParams{ID: br.SessionID, Cols: in.Cols, Rows: in.Rows} + _, err = b.process.Resize(ctx, in.ProcessID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Resized process %s PTY to %dx%d\n", in.ProcessID, in.Cols, in.Rows) + return nil +} + +// FS Watch +func (b BrowsersCmd) FSWatchStart(ctx context.Context, in BrowsersFSWatchStartInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if b.fsWatch == nil { + pterm.Error.Println("fs watch service not available") + return nil + } + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + params := kernel.BrowserFWatchStartParams{Path: in.Path} + if in.Recursive.Set { + params.Recursive = kernel.Opt(in.Recursive.Value) + } + res, err := b.fsWatch.Start(ctx, br.SessionID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(res) + } + + pterm.Success.Printf("Started watch on %s with ID: %s\n", in.Path, res.WatchID) + return nil +} + +func (b BrowsersCmd) FSWatchStop(ctx context.Context, in BrowsersFSWatchStopInput) error { + if b.fsWatch == nil { + pterm.Error.Println("fs watch service not available") + return nil + } + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + err = b.fsWatch.Stop(ctx, in.WatchID, kernel.BrowserFWatchStopParams{ID: br.SessionID}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Stopped watch %s\n", in.WatchID) + return nil +} + +func (b BrowsersCmd) FSWatchEvents(ctx context.Context, in BrowsersFSWatchEventsInput) error { + if b.fsWatch == nil { + pterm.Error.Println("fs watch service not available") + return nil + } + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + stream := b.fsWatch.EventsStreaming(ctx, in.WatchID, kernel.BrowserFWatchEventsParams{ID: br.SessionID}) + if stream == nil { + pterm.Error.Println("failed to open watch events stream") + return nil + } + defer stream.Close() + for stream.Next() { + ev := stream.Current() + pterm.Printf("[%s] %s: %s\n", ev.Type, ev.Name, ev.Path) + } + if err := stream.Err(); err != nil { + return util.CleanedUpSdkError{Err: err} + } + return nil +} + // FS (minimal scaffolding) type BrowsersFSNewDirInput struct { Identifier string @@ -1397,7 +1522,7 @@ func (b BrowsersCmd) FSNewDirectory(ctx context.Context, in BrowsersFSNewDirInpu pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1417,7 +1542,7 @@ func (b BrowsersCmd) FSDeleteDirectory(ctx context.Context, in BrowsersFSDeleteD pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1433,7 +1558,7 @@ func (b BrowsersCmd) FSDeleteFile(ctx context.Context, in BrowsersFSDeleteFileIn pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1449,7 +1574,7 @@ func (b BrowsersCmd) FSDownloadDirZip(ctx context.Context, in BrowsersFSDownload pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1486,7 +1611,7 @@ func (b BrowsersCmd) FSFileInfo(ctx context.Context, in BrowsersFSFileInfoInput) pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1513,7 +1638,7 @@ func (b BrowsersCmd) FSListFiles(ctx context.Context, in BrowsersFSListFilesInpu pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1547,7 +1672,7 @@ func (b BrowsersCmd) FSMove(ctx context.Context, in BrowsersFSMoveInput) error { pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1563,7 +1688,7 @@ func (b BrowsersCmd) FSReadFile(ctx context.Context, in BrowsersFSReadFileInput) pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1595,7 +1720,7 @@ func (b BrowsersCmd) FSSetPermissions(ctx context.Context, in BrowsersFSSetPerms pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1618,7 +1743,7 @@ func (b BrowsersCmd) FSUpload(ctx context.Context, in BrowsersFSUploadInput) err pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1676,7 +1801,7 @@ func (b BrowsersCmd) FSUploadZip(ctx context.Context, in BrowsersFSUploadZipInpu pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1698,7 +1823,7 @@ func (b BrowsersCmd) FSWriteFile(ctx context.Context, in BrowsersFSWriteFileInpu pterm.Error.Println("fs service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1731,7 +1856,7 @@ func (b BrowsersCmd) ExtensionsUpload(ctx context.Context, in BrowsersExtensions pterm.Error.Println("browsers service not available") return nil } - br, err := b.browsers.Get(ctx, in.Identifier) + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -1947,7 +2072,12 @@ func init() { procStdin.Flags().String("data-b64", "", "Base64-encoded data to write to stdin") _ = procStdin.MarkFlagRequired("data-b64") procStdoutStream := &cobra.Command{Use: "stdout-stream ", Short: "Stream process stdout/stderr", Args: cobra.ExactArgs(2), RunE: runBrowsersProcessStdoutStream} - procRoot.AddCommand(procExec, procSpawn, procKill, procStatus, procStdin, procStdoutStream) + procResize := &cobra.Command{Use: "resize ", Short: "Resize a PTY-backed process terminal", Args: cobra.ExactArgs(2), RunE: runBrowsersProcessResize} + procResize.Flags().Int64("cols", 0, "New terminal columns (required)") + procResize.Flags().Int64("rows", 0, "New terminal rows (required)") + _ = procResize.MarkFlagRequired("cols") + _ = procResize.MarkFlagRequired("rows") + procRoot.AddCommand(procExec, procSpawn, procKill, procStatus, procStdin, procStdoutStream, procResize) browsersCmd.AddCommand(procRoot) // fs @@ -2012,7 +2142,18 @@ func init() { fsWriteFile.Flags().String("source", "", "Local source file path") _ = fsWriteFile.MarkFlagRequired("source") - fsRoot.AddCommand(fsNewDir, fsDelDir, fsDelFile, fsDownloadZip, fsFileInfo, fsListFiles, fsMove, fsReadFile, fsSetPerms, fsUpload, fsUploadZip, fsWriteFile) + // fs watch + fsWatchRoot := &cobra.Command{Use: "watch", Short: "Watch directories for changes"} + fsWatchStart := &cobra.Command{Use: "start ", Short: "Start watching a directory", Args: cobra.ExactArgs(1), RunE: runBrowsersFSWatchStart} + fsWatchStart.Flags().String("path", "", "Directory to watch (required)") + _ = fsWatchStart.MarkFlagRequired("path") + fsWatchStart.Flags().Bool("recursive", false, "Watch recursively") + fsWatchStart.Flags().StringP("output", "o", "", "Output format: json for raw API response") + fsWatchStop := &cobra.Command{Use: "stop ", Short: "Stop watching a directory", Args: cobra.ExactArgs(2), RunE: runBrowsersFSWatchStop} + fsWatchEvents := &cobra.Command{Use: "events ", Short: "Stream filesystem events", Args: cobra.ExactArgs(2), RunE: runBrowsersFSWatchEvents} + fsWatchRoot.AddCommand(fsWatchStart, fsWatchStop, fsWatchEvents) + + fsRoot.AddCommand(fsNewDir, fsDelDir, fsDelFile, fsDownloadZip, fsFileInfo, fsListFiles, fsMove, fsReadFile, fsSetPerms, fsUpload, fsUploadZip, fsWriteFile, fsWatchRoot) browsersCmd.AddCommand(fsRoot) // extensions @@ -2441,6 +2582,44 @@ func runBrowsersProcessStdoutStream(cmd *cobra.Command, args []string) error { return b.ProcessStdoutStream(cmd.Context(), BrowsersProcessStdoutStreamInput{Identifier: args[0], ProcessID: args[1]}) } +func runBrowsersProcessResize(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + cols, _ := cmd.Flags().GetInt64("cols") + rows, _ := cmd.Flags().GetInt64("rows") + b := BrowsersCmd{browsers: &svc, process: &svc.Process} + return b.ProcessResize(cmd.Context(), BrowsersProcessResizeInput{Identifier: args[0], ProcessID: args[1], Cols: cols, Rows: rows}) +} + +func runBrowsersFSWatchStart(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + path, _ := cmd.Flags().GetString("path") + recursive, _ := cmd.Flags().GetBool("recursive") + output, _ := cmd.Flags().GetString("output") + b := BrowsersCmd{browsers: &svc, fsWatch: &svc.Fs.Watch} + return b.FSWatchStart(cmd.Context(), BrowsersFSWatchStartInput{ + Identifier: args[0], + Path: path, + Recursive: BoolFlag{Set: cmd.Flags().Changed("recursive"), Value: recursive}, + Output: output, + }) +} + +func runBrowsersFSWatchStop(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + b := BrowsersCmd{browsers: &svc, fsWatch: &svc.Fs.Watch} + return b.FSWatchStop(cmd.Context(), BrowsersFSWatchStopInput{Identifier: args[0], WatchID: args[1]}) +} + +func runBrowsersFSWatchEvents(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + b := BrowsersCmd{browsers: &svc, fsWatch: &svc.Fs.Watch} + return b.FSWatchEvents(cmd.Context(), BrowsersFSWatchEventsInput{Identifier: args[0], WatchID: args[1]}) +} + func runBrowsersPlaywrightExecute(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) svc := client.Browsers diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 2e9a6e8..447b6bd 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -54,7 +54,7 @@ func setupStdoutCapture(t *testing.T) { // FakeBrowsersService is a configurable fake implementing BrowsersService. type FakeBrowsersService struct { - GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) + GetFunc func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) ListFunc func(ctx context.Context, query kernel.BrowserListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserListResponse], error) NewFunc func(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (*kernel.BrowserNewResponse, error) UpdateFunc func(ctx context.Context, id string, body kernel.BrowserUpdateParams, opts ...option.RequestOption) (*kernel.BrowserUpdateResponse, error) @@ -63,9 +63,9 @@ type FakeBrowsersService struct { LoadExtensionsFunc func(ctx context.Context, id string, body kernel.BrowserLoadExtensionsParams, opts ...option.RequestOption) error } -func (f *FakeBrowsersService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { +func (f *FakeBrowsersService) Get(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { if f.GetFunc != nil { - return f.GetFunc(ctx, id, opts...) + return f.GetFunc(ctx, id, query, opts...) } return nil, errors.New("not found") } @@ -292,7 +292,7 @@ func TestBrowsersView_ByID_PrintsURL(t *testing.T) { }) fake := &FakeBrowsersService{ - GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { return &kernel.BrowserGetResponse{ SessionID: "abc", BrowserLiveViewURL: "http://live-url", @@ -325,7 +325,7 @@ func TestBrowsersView_HeadlessBrowser_ShowsWarning(t *testing.T) { setupStdoutCapture(t) fake := &FakeBrowsersService{ - GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { return &kernel.BrowserGetResponse{ SessionID: "abc", Headless: true, @@ -344,7 +344,7 @@ func TestBrowsersView_PrintsErrorOnGetFailure(t *testing.T) { setupStdoutCapture(t) fake := &FakeBrowsersService{ - GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { return nil, errors.New("get error") }, } @@ -360,7 +360,7 @@ func TestBrowsersGet_PrintsDetails(t *testing.T) { created := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC) fake := &FakeBrowsersService{ - GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { return &kernel.BrowserGetResponse{ SessionID: "sess-123", CdpWsURL: "ws://cdp-url", @@ -404,7 +404,7 @@ func TestBrowsersGet_JSONOutput(t *testing.T) { }) fake := &FakeBrowsersService{ - GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { // Unmarshal JSON to populate RawJSON() properly jsonData := `{"session_id": "sess-json", "cdp_ws_url": "ws://cdp", "created_at": "2024-01-01T00:00:00Z", "headless": false, "stealth": false, "timeout_seconds": 60}` var resp kernel.BrowserGetResponse @@ -442,7 +442,7 @@ func TestBrowsersGet_Error(t *testing.T) { setupStdoutCapture(t) fake := &FakeBrowsersService{ - GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { return nil, errors.New("get failed") }, } @@ -580,6 +580,7 @@ func (f *FakeFSService) WriteFile(ctx context.Context, id string, contents io.Re type FakeProcessService struct { ExecFunc func(ctx context.Context, id string, body kernel.BrowserProcessExecParams, opts ...option.RequestOption) (*kernel.BrowserProcessExecResponse, error) KillFunc func(ctx context.Context, processID string, params kernel.BrowserProcessKillParams, opts ...option.RequestOption) (*kernel.BrowserProcessKillResponse, error) + ResizeFunc func(ctx context.Context, processID string, params kernel.BrowserProcessResizeParams, opts ...option.RequestOption) (*kernel.BrowserProcessResizeResponse, error) SpawnFunc func(ctx context.Context, id string, body kernel.BrowserProcessSpawnParams, opts ...option.RequestOption) (*kernel.BrowserProcessSpawnResponse, error) StatusFunc func(ctx context.Context, processID string, query kernel.BrowserProcessStatusParams, opts ...option.RequestOption) (*kernel.BrowserProcessStatusResponse, error) StdinFunc func(ctx context.Context, processID string, params kernel.BrowserProcessStdinParams, opts ...option.RequestOption) (*kernel.BrowserProcessStdinResponse, error) @@ -598,6 +599,12 @@ func (f *FakeProcessService) Kill(ctx context.Context, processID string, params } return &kernel.BrowserProcessKillResponse{Ok: true}, nil } +func (f *FakeProcessService) Resize(ctx context.Context, processID string, params kernel.BrowserProcessResizeParams, opts ...option.RequestOption) (*kernel.BrowserProcessResizeResponse, error) { + if f.ResizeFunc != nil { + return f.ResizeFunc(ctx, processID, params, opts...) + } + return &kernel.BrowserProcessResizeResponse{Ok: true}, nil +} func (f *FakeProcessService) Spawn(ctx context.Context, id string, body kernel.BrowserProcessSpawnParams, opts ...option.RequestOption) (*kernel.BrowserProcessSpawnResponse, error) { if f.SpawnFunc != nil { return f.SpawnFunc(ctx, id, body, opts...) @@ -730,7 +737,7 @@ func (f *FakeComputerService) SetCursorVisibility(ctx context.Context, id string // newFakeBrowsersServiceWithSimpleGet returns a FakeBrowsersService with a GetFunc that returns a browser with SessionID "id". func newFakeBrowsersServiceWithSimpleGet() *FakeBrowsersService { return &FakeBrowsersService{ - GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { return &kernel.BrowserGetResponse{SessionID: "id"}, nil }, } diff --git a/cmd/credential_providers.go b/cmd/credential_providers.go new file mode 100644 index 0000000..66dbedb --- /dev/null +++ b/cmd/credential_providers.go @@ -0,0 +1,466 @@ +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/pterm/pterm" + "github.com/spf13/cobra" +) + +// CredentialProvidersService defines the subset of the Kernel SDK credential provider client that we use. +type CredentialProvidersService interface { + New(ctx context.Context, body kernel.CredentialProviderNewParams, opts ...option.RequestOption) (res *kernel.CredentialProvider, err error) + Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.CredentialProvider, err error) + Update(ctx context.Context, id string, body kernel.CredentialProviderUpdateParams, opts ...option.RequestOption) (res *kernel.CredentialProvider, err error) + List(ctx context.Context, opts ...option.RequestOption) (res *[]kernel.CredentialProvider, err error) + Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) + Test(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.CredentialProviderTestResult, err error) +} + +// CredentialProvidersCmd handles credential provider operations independent of cobra. +type CredentialProvidersCmd struct { + providers CredentialProvidersService +} + +type CredentialProvidersListInput struct { + Output string +} + +type CredentialProvidersGetInput struct { + ID string + Output string +} + +type CredentialProvidersCreateInput struct { + ProviderType string + Token string + CacheTtlSeconds int64 + Output string +} + +type CredentialProvidersUpdateInput struct { + ID string + Token string + CacheTtlSeconds int64 + Enabled BoolFlag + Priority int64 + Output string +} + +type CredentialProvidersDeleteInput struct { + ID string + SkipConfirm bool +} + +type CredentialProvidersTestInput struct { + ID string + Output string +} + +func (c CredentialProvidersCmd) List(ctx context.Context, in CredentialProvidersListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + providers, err := c.providers.List(ctx) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + if providers == nil || len(*providers) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(*providers) + } + + if providers == nil || len(*providers) == 0 { + pterm.Info.Println("No credential providers found") + return nil + } + + tableData := pterm.TableData{{"ID", "Provider Type", "Enabled", "Priority", "Created At"}} + for _, p := range *providers { + tableData = append(tableData, []string{ + p.ID, + string(p.ProviderType), + fmt.Sprintf("%t", p.Enabled), + fmt.Sprintf("%d", p.Priority), + util.FormatLocal(p.CreatedAt), + }) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c CredentialProvidersCmd) Get(ctx context.Context, in CredentialProvidersGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + provider, err := c.providers.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(provider) + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", provider.ID}, + {"Provider Type", string(provider.ProviderType)}, + {"Enabled", fmt.Sprintf("%t", provider.Enabled)}, + {"Priority", fmt.Sprintf("%d", provider.Priority)}, + {"Created At", util.FormatLocal(provider.CreatedAt)}, + {"Updated At", util.FormatLocal(provider.UpdatedAt)}, + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c CredentialProvidersCmd) Create(ctx context.Context, in CredentialProvidersCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.ProviderType == "" { + return fmt.Errorf("--provider-type is required") + } + if in.Token == "" { + return fmt.Errorf("--token is required") + } + + // Validate provider type + providerType := strings.ToLower(in.ProviderType) + if providerType != "onepassword" { + return fmt.Errorf("invalid provider type: %s (must be 'onepassword')", in.ProviderType) + } + + params := kernel.CredentialProviderNewParams{ + CreateCredentialProviderRequest: kernel.CreateCredentialProviderRequestParam{ + Token: in.Token, + ProviderType: kernel.CreateCredentialProviderRequestProviderTypeOnepassword, + }, + } + if in.CacheTtlSeconds > 0 { + params.CreateCredentialProviderRequest.CacheTtlSeconds = kernel.Opt(in.CacheTtlSeconds) + } + + if in.Output != "json" { + pterm.Info.Printf("Creating credential provider (%s)...\n", providerType) + } + + provider, err := c.providers.New(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(provider) + } + + pterm.Success.Printf("Created credential provider: %s\n", provider.ID) + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", provider.ID}, + {"Provider Type", string(provider.ProviderType)}, + {"Enabled", fmt.Sprintf("%t", provider.Enabled)}, + {"Priority", fmt.Sprintf("%d", provider.Priority)}, + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c CredentialProvidersCmd) Update(ctx context.Context, in CredentialProvidersUpdateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + params := kernel.CredentialProviderUpdateParams{ + UpdateCredentialProviderRequest: kernel.UpdateCredentialProviderRequestParam{}, + } + if in.Token != "" { + params.UpdateCredentialProviderRequest.Token = kernel.Opt(in.Token) + } + if in.CacheTtlSeconds > 0 { + params.UpdateCredentialProviderRequest.CacheTtlSeconds = kernel.Opt(in.CacheTtlSeconds) + } + if in.Enabled.Set { + params.UpdateCredentialProviderRequest.Enabled = kernel.Opt(in.Enabled.Value) + } + if in.Priority > 0 { + params.UpdateCredentialProviderRequest.Priority = kernel.Opt(in.Priority) + } + + if in.Output != "json" { + pterm.Info.Printf("Updating credential provider '%s'...\n", in.ID) + } + + provider, err := c.providers.Update(ctx, in.ID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(provider) + } + + pterm.Success.Printf("Updated credential provider: %s\n", provider.ID) + return nil +} + +func (c CredentialProvidersCmd) Delete(ctx context.Context, in CredentialProvidersDeleteInput) error { + if !in.SkipConfirm { + msg := fmt.Sprintf("Are you sure you want to delete credential provider '%s'?", in.ID) + pterm.DefaultInteractiveConfirm.DefaultText = msg + ok, _ := pterm.DefaultInteractiveConfirm.Show() + if !ok { + pterm.Info.Println("Deletion cancelled") + return nil + } + } + + if err := c.providers.Delete(ctx, in.ID); err != nil { + if util.IsNotFound(err) { + pterm.Info.Printf("Credential provider '%s' not found\n", in.ID) + return nil + } + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Deleted credential provider: %s\n", in.ID) + return nil +} + +func (c CredentialProvidersCmd) Test(ctx context.Context, in CredentialProvidersTestInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Output != "json" { + pterm.Info.Printf("Testing credential provider '%s'...\n", in.ID) + } + + result, err := c.providers.Test(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(result) + } + + if result.Success { + pterm.Success.Println("Connection test successful") + } else { + pterm.Error.Printf("Connection test failed: %s\n", result.Error) + } + + if len(result.Vaults) > 0 { + pterm.Info.Println("Accessible vaults:") + tableData := pterm.TableData{{"Vault ID", "Vault Name"}} + for _, v := range result.Vaults { + tableData = append(tableData, []string{v.ID, v.Name}) + } + PrintTableNoPad(tableData, true) + } else { + pterm.Info.Println("No vaults accessible") + } + + return nil +} + +// --- Cobra wiring --- + +var credentialProvidersCmd = &cobra.Command{ + Use: "credential-providers", + Aliases: []string{"credential-provider", "cred-providers", "cred-provider"}, + Short: "Manage external credential providers", + Long: "Commands for managing external credential providers (e.g., 1Password) for automatic credential lookup", +} + +var credentialProvidersListCmd = &cobra.Command{ + Use: "list", + Short: "List credential providers", + Args: cobra.NoArgs, + RunE: runCredentialProvidersList, +} + +var credentialProvidersGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get a credential provider by ID", + Args: cobra.ExactArgs(1), + RunE: runCredentialProvidersGet, +} + +var credentialProvidersCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new credential provider", + Long: `Create a new external credential provider for automatic credential lookup. + +Currently supported provider types: + - onepassword: 1Password service account integration + +Examples: + # Create a 1Password credential provider + kernel credential-providers create --provider-type onepassword --token "ops_xxx..." + + # Create with custom cache TTL + kernel credential-providers create --provider-type onepassword --token "ops_xxx..." --cache-ttl 600`, + Args: cobra.NoArgs, + RunE: runCredentialProvidersCreate, +} + +var credentialProvidersUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update a credential provider", + Long: `Update a credential provider's configuration (token, cache TTL, enabled status, or priority).`, + Args: cobra.ExactArgs(1), + RunE: runCredentialProvidersUpdate, +} + +var credentialProvidersDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a credential provider", + Args: cobra.ExactArgs(1), + RunE: runCredentialProvidersDelete, +} + +var credentialProvidersTestCmd = &cobra.Command{ + Use: "test ", + Short: "Test a credential provider connection", + Long: `Validate the credential provider's token and list accessible vaults.`, + Args: cobra.ExactArgs(1), + RunE: runCredentialProvidersTest, +} + +func init() { + credentialProvidersCmd.AddCommand(credentialProvidersListCmd) + credentialProvidersCmd.AddCommand(credentialProvidersGetCmd) + credentialProvidersCmd.AddCommand(credentialProvidersCreateCmd) + credentialProvidersCmd.AddCommand(credentialProvidersUpdateCmd) + credentialProvidersCmd.AddCommand(credentialProvidersDeleteCmd) + credentialProvidersCmd.AddCommand(credentialProvidersTestCmd) + + // List flags + credentialProvidersListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // Get flags + credentialProvidersGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // Create flags + credentialProvidersCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + credentialProvidersCreateCmd.Flags().String("provider-type", "", "Provider type (e.g., onepassword)") + credentialProvidersCreateCmd.Flags().String("token", "", "Service account token for the provider") + credentialProvidersCreateCmd.Flags().Int64("cache-ttl", 0, "How long to cache credential lists in seconds (default 300)") + _ = credentialProvidersCreateCmd.MarkFlagRequired("provider-type") + _ = credentialProvidersCreateCmd.MarkFlagRequired("token") + + // Update flags + credentialProvidersUpdateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + credentialProvidersUpdateCmd.Flags().String("token", "", "New service account token (to rotate credentials)") + credentialProvidersUpdateCmd.Flags().Int64("cache-ttl", 0, "How long to cache credential lists in seconds") + credentialProvidersUpdateCmd.Flags().Bool("enabled", true, "Whether the provider is enabled for credential lookups") + credentialProvidersUpdateCmd.Flags().Int64("priority", 0, "Priority order for credential lookups (lower numbers are checked first)") + + // Delete flags + credentialProvidersDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + + // Test flags + credentialProvidersTestCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") +} + +func runCredentialProvidersList(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.CredentialProviders + c := CredentialProvidersCmd{providers: &svc} + return c.List(cmd.Context(), CredentialProvidersListInput{ + Output: output, + }) +} + +func runCredentialProvidersGet(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.CredentialProviders + c := CredentialProvidersCmd{providers: &svc} + return c.Get(cmd.Context(), CredentialProvidersGetInput{ + ID: args[0], + Output: output, + }) +} + +func runCredentialProvidersCreate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + providerType, _ := cmd.Flags().GetString("provider-type") + token, _ := cmd.Flags().GetString("token") + cacheTtl, _ := cmd.Flags().GetInt64("cache-ttl") + + svc := client.CredentialProviders + c := CredentialProvidersCmd{providers: &svc} + return c.Create(cmd.Context(), CredentialProvidersCreateInput{ + ProviderType: providerType, + Token: token, + CacheTtlSeconds: cacheTtl, + Output: output, + }) +} + +func runCredentialProvidersUpdate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + token, _ := cmd.Flags().GetString("token") + cacheTtl, _ := cmd.Flags().GetInt64("cache-ttl") + enabled, _ := cmd.Flags().GetBool("enabled") + priority, _ := cmd.Flags().GetInt64("priority") + + svc := client.CredentialProviders + c := CredentialProvidersCmd{providers: &svc} + return c.Update(cmd.Context(), CredentialProvidersUpdateInput{ + ID: args[0], + Token: token, + CacheTtlSeconds: cacheTtl, + Enabled: BoolFlag{Set: cmd.Flags().Changed("enabled"), Value: enabled}, + Priority: priority, + Output: output, + }) +} + +func runCredentialProvidersDelete(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + skip, _ := cmd.Flags().GetBool("yes") + + svc := client.CredentialProviders + c := CredentialProvidersCmd{providers: &svc} + return c.Delete(cmd.Context(), CredentialProvidersDeleteInput{ + ID: args[0], + SkipConfirm: skip, + }) +} + +func runCredentialProvidersTest(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.CredentialProviders + c := CredentialProvidersCmd{providers: &svc} + return c.Test(cmd.Context(), CredentialProvidersTestInput{ + ID: args[0], + Output: output, + }) +} diff --git a/cmd/root.go b/cmd/root.go index 318a14d..de4b5bb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -140,6 +140,8 @@ func init() { rootCmd.AddCommand(proxies.ProxiesCmd) rootCmd.AddCommand(extensionsCmd) rootCmd.AddCommand(credentialsCmd) + rootCmd.AddCommand(credentialProvidersCmd) + rootCmd.AddCommand(agentsCmd) rootCmd.AddCommand(createCmd) rootCmd.AddCommand(mcp.MCPCmd) rootCmd.AddCommand(upgradeCmd) diff --git a/go.mod b/go.mod index ff373fe..343f7db 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,9 @@ 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.0 + github.com/kernel/kernel-go-sdk v0.27.1-0.20260121054822-cee2050be3f8 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c + github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 github.com/spf13/cobra v1.9.1 @@ -25,6 +26,7 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/charmbracelet/colorprofile v0.3.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect diff --git a/go.sum b/go.sum index 02041b3..f760efe 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boyter/gocodewalker v1.4.0 h1:fVmFeQxKpj5tlpjPcyTtJ96btgaHYd9yn6m+T/66et4= github.com/boyter/gocodewalker v1.4.0/go.mod h1:hXG8xzR1uURS+99P5/3xh3uWHjaV2XfoMMmvPyhrCDg= github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= @@ -64,8 +66,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.0 h1:IBiEohSSZN5MEZjmnfqseT3tEip6+xg7Zxr79vJYMBA= -github.com/kernel/kernel-go-sdk v0.26.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.27.1-0.20260121054822-cee2050be3f8 h1:D44gjEjkLww0lwnhNNJgaNLNVwkEgtkyt5w66epvE/Y= +github.com/kernel/kernel-go-sdk v0.27.1-0.20260121054822-cee2050be3f8/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= @@ -97,6 +99,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -121,6 +125,7 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=