diff --git a/cmd/browsers.go b/cmd/browsers.go index f3de63c..e96eb12 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -124,9 +124,9 @@ func getAvailableViewports() []string { "1920x1080@25", "1920x1200@25", "1440x900@25", + "1280x800@60", "1024x768@60", "1200x800@60", - "1280x800@60", } } @@ -222,6 +222,7 @@ type BrowsersCmd struct { type BrowsersListInput struct { Output string IncludeDeleted bool + Status string Limit int Offset int } @@ -232,7 +233,19 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { } params := kernel.BrowserListParams{} - if in.IncludeDeleted { + // Use new Status parameter if provided, otherwise fall back to deprecated IncludeDeleted + if in.Status != "" { + switch in.Status { + case "active": + params.Status = kernel.BrowserListParamsStatusActive + case "deleted": + params.Status = kernel.BrowserListParamsStatusDeleted + case "all": + params.Status = kernel.BrowserListParamsStatusAll + default: + return fmt.Errorf("invalid --status value: %s (must be 'active', 'deleted', or 'all')", in.Status) + } + } else if in.IncludeDeleted { params.IncludeDeleted = kernel.Opt(true) } if in.Limit > 0 { @@ -263,7 +276,8 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { // Prepare table data headers := []string{"Browser ID", "Created At", "Persistent ID", "Profile", "CDP WS URL", "Live View URL"} - if in.IncludeDeleted { + showDeletedAt := in.IncludeDeleted || in.Status == "deleted" || in.Status == "all" + if showDeletedAt { headers = append(headers, "Deleted At") } tableData := pterm.TableData{headers} @@ -290,7 +304,7 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { truncateURL(browser.BrowserLiveViewURL, 50), } - if in.IncludeDeleted { + if showDeletedAt { deletedAt := "-" if !browser.DeletedAt.IsZero() { deletedAt = util.FormatLocal(browser.DeletedAt) @@ -2053,7 +2067,8 @@ Note: Profiles can only be loaded into sessions that don't already have a profil func init() { // list flags browsersListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - browsersListCmd.Flags().Bool("include-deleted", false, "Include soft-deleted browser sessions in the results") + browsersListCmd.Flags().Bool("include-deleted", false, "DEPRECATED: Use --status instead. Include soft-deleted browser sessions in the results") + browsersListCmd.Flags().String("status", "", "Filter by status: 'active' (default), 'deleted', or 'all'") browsersListCmd.Flags().Int("limit", 0, "Maximum number of results to return (default 20, max 100)") browsersListCmd.Flags().Int("offset", 0, "Number of results to skip (for pagination)") @@ -2322,11 +2337,13 @@ func runBrowsersList(cmd *cobra.Command, args []string) error { b := BrowsersCmd{browsers: &svc} out, _ := cmd.Flags().GetString("output") includeDeleted, _ := cmd.Flags().GetBool("include-deleted") + status, _ := cmd.Flags().GetString("status") limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") return b.List(cmd.Context(), BrowsersListInput{ Output: out, IncludeDeleted: includeDeleted, + Status: status, Limit: limit, Offset: offset, }) diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 49d770d..696e29a 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -1151,8 +1151,8 @@ func TestGetAvailableViewports_ReturnsExpectedOptions(t *testing.T) { assert.Contains(t, viewports, "1920x1080@25") assert.Contains(t, viewports, "1920x1200@25") assert.Contains(t, viewports, "1440x900@25") - assert.Contains(t, viewports, "1200x800@60") assert.Contains(t, viewports, "1280x800@60") + assert.Contains(t, viewports, "1200x800@60") assert.Contains(t, viewports, "1024x768@60") } diff --git a/cmd/invoke.go b/cmd/invoke.go index 4b80b67..3b5091c 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -35,11 +35,20 @@ var invocationHistoryCmd = &cobra.Command{ RunE: runInvocationHistory, } +var invocationBrowsersCmd = &cobra.Command{ + Use: "browsers ", + Short: "List browser sessions for an invocation", + Long: "List all active browser sessions created within a specific invocation.", + Args: cobra.ExactArgs(1), + RunE: runInvocationBrowsers, +} + func init() { invokeCmd.Flags().StringP("version", "v", "latest", "Specify a version of the app to invoke (optional, defaults to 'latest')") invokeCmd.Flags().StringP("payload", "p", "", "JSON payload for the invocation (optional)") invokeCmd.Flags().StringP("payload-file", "f", "", "Path to a JSON file containing the payload (use '-' for stdin)") invokeCmd.Flags().BoolP("sync", "s", false, "Invoke synchronously (default false). A synchronous invocation will open a long-lived HTTP POST to the Kernel API to wait for the invocation to complete. This will time out after 60 seconds, so only use this option if you expect your invocation to complete in less than 60 seconds. The default is to invoke asynchronously, in which case the CLI will open an SSE connection to the Kernel API after submitting the invocation and wait for the invocation to complete.") + invokeCmd.Flags().Int64("async-timeout", 0, "Timeout in seconds for async invocations (min 10, max 3600). Only applies when async mode is used.") invokeCmd.Flags().StringP("output", "o", "", "Output format: json for JSONL streaming output") invokeCmd.MarkFlagsMutuallyExclusive("payload", "payload-file") @@ -48,6 +57,9 @@ func init() { invocationHistoryCmd.Flags().String("version", "", "Filter by invocation version") invocationHistoryCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") invokeCmd.AddCommand(invocationHistoryCmd) + + invocationBrowsersCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + invokeCmd.AddCommand(invocationBrowsersCmd) } func runInvoke(cmd *cobra.Command, args []string) error { @@ -70,12 +82,16 @@ func runInvoke(cmd *cobra.Command, args []string) error { return fmt.Errorf("version cannot be an empty string") } isSync, _ := cmd.Flags().GetBool("sync") + asyncTimeout, _ := cmd.Flags().GetInt64("async-timeout") params := kernel.InvocationNewParams{ AppName: appName, ActionName: actionName, Version: version, Async: kernel.Opt(!isSync), } + if asyncTimeout > 0 { + params.AsyncTimeoutSeconds = kernel.Opt(asyncTimeout) + } payloadStr, hasPayload, err := getPayload(cmd) if err != nil { @@ -428,3 +444,56 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error { } return nil } + +func runInvocationBrowsers(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + invocationID := args[0] + output, _ := cmd.Flags().GetString("output") + + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + resp, err := client.Invocations.ListBrowsers(cmd.Context(), invocationID) + if err != nil { + pterm.Error.Printf("Failed to list browsers for invocation: %v\n", err) + return nil + } + + if output == "json" { + if len(resp.Browsers) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(resp.Browsers) + } + + if len(resp.Browsers) == 0 { + pterm.Info.Printf("No active browsers found for invocation %s\n", invocationID) + return nil + } + + table := pterm.TableData{{"Session ID", "Created At", "Headless", "Stealth", "Timeout", "CDP WS URL", "Live View URL"}} + + for _, browser := range resp.Browsers { + created := util.FormatLocal(browser.CreatedAt) + liveView := browser.BrowserLiveViewURL + if liveView == "" { + liveView = "-" + } + + table = append(table, []string{ + browser.SessionID, + created, + fmt.Sprintf("%v", browser.Headless), + fmt.Sprintf("%v", browser.Stealth), + fmt.Sprintf("%d", browser.TimeoutSeconds), + truncateURL(browser.CdpWsURL, 40), + truncateURL(liveView, 40), + }) + } + + pterm.Info.Printf("Browsers for invocation %s:\n", invocationID) + pterm.DefaultTable.WithHasHeader().WithData(table).Render() + return nil +} diff --git a/go.mod b/go.mod index b415eb5..15d5dbd 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.28.0 + github.com/kernel/kernel-go-sdk v0.30.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 82d1c54..1082c5b 100644 --- a/go.sum +++ b/go.sum @@ -66,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.28.0 h1:cvaCWP25UIB5w6oOdQ5J+rVboNGq3VaWYhtmshlPrhg= -github.com/kernel/kernel-go-sdk v0.28.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.30.0 h1:FN9G84mbqqTETSBRHRTuG4rBoUVu3xRhDIaWG3AyYNI= +github.com/kernel/kernel-go-sdk v0.30.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=