From 0e2c40ef505acd2eb258ea6bcbc3c37ec284ed57 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Wed, 26 Nov 2025 12:23:34 -0800 Subject: [PATCH 01/11] vibing --- README.md | 23 ++ cmd/browser_pools.go | 640 +++++++++++++++++++++++++++++++++++++++++++ cmd/browsers.go | 88 +++++- cmd/root.go | 1 + 4 files changed, 739 insertions(+), 13 deletions(-) create mode 100644 cmd/browser_pools.go diff --git a/README.md b/README.md index 81ca1c2..0a0ce92 100644 --- a/README.md +++ b/README.md @@ -153,10 +153,33 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `-s, --stealth` - Launch browser in stealth mode to avoid detection - `-H, --headless` - Launch browser without GUI access - `--kiosk` - Launch browser in kiosk mode + - `--pool-id ` - Acquire a browser from the specified pool (mutually exclusive with --pool-name; ignores other session flags) + - `--pool-name ` - Acquire a browser from the pool name (mutually exclusive with --pool-id; ignores other session flags) + - _Note: When a pool is specified, omit other session configuration flags—pool settings determine profile, proxy, viewport, etc._ - `kernel browsers delete ` - Delete a browser - `-y, --yes` - Skip confirmation prompt - `kernel browsers view ` - Get live view URL for a browser +### Browser Pools + +- `kernel browser-pools list` - List browser pools +- `kernel browser-pools create` - Create a browser pool + - `--size ` - Number of browsers in the pool (required) + - `--fill-rate ` - Percentage of the pool to fill per minute + - `--timeout ` - Idle timeout for browsers acquired from the pool + - `--stealth`, `--headless`, `--kiosk` - Default pool configuration + - `--profile-id`, `--profile-name`, `--save-changes`, `--proxy-id`, `--extension`, `--viewport` - Same semantics as `kernel browsers create` +- `kernel browser-pools get ` - Get pool details +- `kernel browser-pools update ` - Update pool configuration (same flags as create plus `--discard-all-idle`) +- `kernel browser-pools delete ` - Delete a pool + - `--force` - Force delete even if browsers are leased +- `kernel browser-pools acquire ` - Acquire a browser from the pool + - `--timeout ` - Acquire timeout before returning 204 +- `kernel browser-pools release ` - Release a browser back to the pool + - `--session-id ` - Browser session ID to release (required) + - `--reuse` - Reuse the browser instance (default: true) +- `kernel browser-pools flush ` - Destroy all idle browsers in the pool + ### Browser Logs - `kernel browsers logs stream ` - Stream browser logs diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go new file mode 100644 index 0000000..03bef3b --- /dev/null +++ b/cmd/browser_pools.go @@ -0,0 +1,640 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/onkernel/cli/pkg/util" + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/option" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +// BrowserPoolsService defines the subset of the Kernel SDK browser pools client that we use. +type BrowserPoolsService interface { + List(ctx context.Context, opts ...option.RequestOption) (res *[]kernel.BrowserPool, err error) + New(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (res *kernel.BrowserPool, err error) + Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.BrowserPool, err error) + Update(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (res *kernel.BrowserPool, err error) + Delete(ctx context.Context, id string, body kernel.BrowserPoolDeleteParams, opts ...option.RequestOption) (err error) + Acquire(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (res *kernel.BrowserPoolAcquireResponse, err error) + Release(ctx context.Context, id string, body kernel.BrowserPoolReleaseParams, opts ...option.RequestOption) (err error) + Flush(ctx context.Context, id string, opts ...option.RequestOption) (err error) +} + +type BrowserPoolsCmd struct { + client BrowserPoolsService +} + +type BrowserPoolsListInput struct { + Output string +} + +func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) error { + if in.Output != "" && in.Output != "json" { + pterm.Error.Println("unsupported --output value: use 'json'") + return nil + } + + pools, err := c.client.List(ctx) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + if pools == nil { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(*pools, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + + if pools == nil || len(*pools) == 0 { + pterm.Info.Println("No browser pools found") + return nil + } + + tableData := pterm.TableData{ + {"ID", "Available", "Acquired", "Created At", "Size"}, + } + + for _, p := range *pools { + tableData = append(tableData, []string{ + p.ID, + fmt.Sprintf("%d", p.AvailableCount), + fmt.Sprintf("%d", p.AcquiredCount), + util.FormatLocal(p.CreatedAt), + fmt.Sprintf("%d", p.BrowserPoolConfig.Size), + }) + } + + PrintTableNoPad(tableData, true) + return nil +} + +type BrowserPoolsCreateInput struct { + Size int64 + FillRate int64 + TimeoutSeconds int64 + Stealth BoolFlag + Headless BoolFlag + Kiosk BoolFlag + ProfileID string + ProfileName string + ProfileSaveChanges BoolFlag + ProxyID string + Extensions []string + Viewport string +} + +func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) error { + params := kernel.BrowserPoolNewParams{ + Size: in.Size, + } + + if in.FillRate > 0 { + params.FillRatePerMinute = kernel.Int(in.FillRate) + } + if in.TimeoutSeconds > 0 { + params.TimeoutSeconds = kernel.Int(in.TimeoutSeconds) + } + if in.Stealth.Set { + params.Stealth = kernel.Bool(in.Stealth.Value) + } + if in.Headless.Set { + params.Headless = kernel.Bool(in.Headless.Value) + } + if in.Kiosk.Set { + params.KioskMode = kernel.Bool(in.Kiosk.Value) + } + + // Profile + if in.ProfileID != "" && in.ProfileName != "" { + pterm.Error.Println("must specify at most one of --profile-id or --profile-name") + return nil + } else if in.ProfileID != "" || in.ProfileName != "" { + params.Profile = kernel.BrowserPoolNewParamsProfile{ + SaveChanges: kernel.Bool(in.ProfileSaveChanges.Value), + } + if in.ProfileID != "" { + params.Profile.ID = kernel.String(in.ProfileID) + } else if in.ProfileName != "" { + params.Profile.Name = kernel.String(in.ProfileName) + } + } + + if in.ProxyID != "" { + params.ProxyID = kernel.String(in.ProxyID) + } + + // Extensions + if len(in.Extensions) > 0 { + for _, ext := range in.Extensions { + val := strings.TrimSpace(ext) + if val == "" { + continue + } + item := kernel.BrowserPoolNewParamsExtension{} + if cuidRegex.MatchString(val) { + item.ID = kernel.String(val) + } else { + item.Name = kernel.String(val) + } + params.Extensions = append(params.Extensions, item) + } + } + + // Viewport + if in.Viewport != "" { + width, height, refreshRate, err := parseViewport(in.Viewport) + if err != nil { + pterm.Error.Printf("Invalid viewport format: %v\n", err) + return nil + } + params.Viewport = kernel.BrowserPoolNewParamsViewport{ + Width: width, + Height: height, + } + if refreshRate > 0 { + params.Viewport.RefreshRate = kernel.Int(refreshRate) + } + } + + pool, err := c.client.New(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + pterm.Success.Printf("Created browser pool %s\n", pool.ID) + return nil +} + +type BrowserPoolsGetInput struct { + IDOrName string + Output string +} + +func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error { + if in.Output != "" && in.Output != "json" { + pterm.Error.Println("unsupported --output value: use 'json'") + return nil + } + + pool, err := c.client.Get(ctx, in.IDOrName) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + bs, err := json.MarshalIndent(pool, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", pool.ID}, + {"Size", fmt.Sprintf("%d", pool.BrowserPoolConfig.Size)}, + {"Available", fmt.Sprintf("%d", pool.AvailableCount)}, + {"Acquired", fmt.Sprintf("%d", pool.AcquiredCount)}, + {"Timeout", fmt.Sprintf("%d", pool.BrowserPoolConfig.TimeoutSeconds)}, + {"Created At", util.FormatLocal(pool.CreatedAt)}, + } + PrintTableNoPad(tableData, true) + return nil +} + +type BrowserPoolsUpdateInput struct { + IDOrName string + Size int64 + FillRate int64 + TimeoutSeconds int64 + Stealth BoolFlag + Headless BoolFlag + Kiosk BoolFlag + ProfileID string + ProfileName string + ProfileSaveChanges BoolFlag + ProxyID string + Extensions []string + Viewport string + DiscardAllIdle BoolFlag +} + +func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) error { + params := kernel.BrowserPoolUpdateParams{} + + if in.Size > 0 { + params.Size = in.Size + } + if in.FillRate > 0 { + params.FillRatePerMinute = kernel.Int(in.FillRate) + } + if in.TimeoutSeconds > 0 { + params.TimeoutSeconds = kernel.Int(in.TimeoutSeconds) + } + if in.Stealth.Set { + params.Stealth = kernel.Bool(in.Stealth.Value) + } + if in.Headless.Set { + params.Headless = kernel.Bool(in.Headless.Value) + } + if in.Kiosk.Set { + params.KioskMode = kernel.Bool(in.Kiosk.Value) + } + if in.DiscardAllIdle.Set { + params.DiscardAllIdle = kernel.Bool(in.DiscardAllIdle.Value) + } + + // Profile + if in.ProfileID != "" && in.ProfileName != "" { + pterm.Error.Println("must specify at most one of --profile-id or --profile-name") + return nil + } else if in.ProfileID != "" || in.ProfileName != "" { + params.Profile = kernel.BrowserPoolUpdateParamsProfile{ + SaveChanges: kernel.Bool(in.ProfileSaveChanges.Value), + } + if in.ProfileID != "" { + params.Profile.ID = kernel.String(in.ProfileID) + } else if in.ProfileName != "" { + params.Profile.Name = kernel.String(in.ProfileName) + } + } + + if in.ProxyID != "" { + params.ProxyID = kernel.String(in.ProxyID) + } + + // Extensions + if len(in.Extensions) > 0 { + for _, ext := range in.Extensions { + val := strings.TrimSpace(ext) + if val == "" { + continue + } + item := kernel.BrowserPoolUpdateParamsExtension{} + if cuidRegex.MatchString(val) { + item.ID = kernel.String(val) + } else { + item.Name = kernel.String(val) + } + params.Extensions = append(params.Extensions, item) + } + } + + // Viewport + if in.Viewport != "" { + width, height, refreshRate, err := parseViewport(in.Viewport) + if err != nil { + pterm.Error.Printf("Invalid viewport format: %v\n", err) + return nil + } + params.Viewport = kernel.BrowserPoolUpdateParamsViewport{ + Width: width, + Height: height, + } + if refreshRate > 0 { + params.Viewport.RefreshRate = kernel.Int(refreshRate) + } + } + + pool, err := c.client.Update(ctx, in.IDOrName, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Updated browser pool %s\n", pool.ID) + return nil +} + +type BrowserPoolsDeleteInput struct { + IDOrName string + Force bool +} + +func (c BrowserPoolsCmd) Delete(ctx context.Context, in BrowserPoolsDeleteInput) error { + params := kernel.BrowserPoolDeleteParams{} + if in.Force { + params.Force = kernel.Bool(true) + } + err := c.client.Delete(ctx, in.IDOrName, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Deleted browser pool %s\n", in.IDOrName) + return nil +} + +type BrowserPoolsAcquireInput struct { + IDOrName string + TimeoutSeconds int64 +} + +func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInput) error { + params := kernel.BrowserPoolAcquireParams{} + if in.TimeoutSeconds > 0 { + params.AcquireTimeoutSeconds = kernel.Int(in.TimeoutSeconds) + } + resp, err := c.client.Acquire(ctx, in.IDOrName, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + if resp == nil { + pterm.Warning.Println("Acquire request timed out (no browser available). Retry to continue waiting.") + return nil + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"Session ID", resp.SessionID}, + {"CDP WebSocket URL", resp.CdpWsURL}, + {"Live View URL", resp.BrowserLiveViewURL}, + } + PrintTableNoPad(tableData, true) + return nil +} + +type BrowserPoolsReleaseInput struct { + IDOrName string + SessionID string + Reuse BoolFlag +} + +func (c BrowserPoolsCmd) Release(ctx context.Context, in BrowserPoolsReleaseInput) error { + params := kernel.BrowserPoolReleaseParams{ + SessionID: kernel.String(in.SessionID), + } + if in.Reuse.Set { + params.Reuse = kernel.Bool(in.Reuse.Value) + } + err := c.client.Release(ctx, in.IDOrName, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Released browser %s back to pool %s\n", in.SessionID, in.IDOrName) + return nil +} + +type BrowserPoolsFlushInput struct { + IDOrName string +} + +func (c BrowserPoolsCmd) Flush(ctx context.Context, in BrowserPoolsFlushInput) error { + err := c.client.Flush(ctx, in.IDOrName) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Flushed idle browsers from pool %s\n", in.IDOrName) + return nil +} + +// Cobra commands (same as before) +// ... + +var browserPoolsCmd = &cobra.Command{ + Use: "browser-pools", + Aliases: []string{"browser-pool", "pool", "pools"}, + Short: "Manage browser pools", + Long: "Commands for managing Kernel browser pools", +} + +var browserPoolsListCmd = &cobra.Command{ + Use: "list", + Short: "List browser pools", + RunE: runBrowserPoolsList, +} + +var browserPoolsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new browser pool", + RunE: runBrowserPoolsCreate, +} + +var browserPoolsGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get details of a browser pool", + Args: cobra.ExactArgs(1), + RunE: runBrowserPoolsGet, +} + +var browserPoolsUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update a browser pool", + Args: cobra.ExactArgs(1), + RunE: runBrowserPoolsUpdate, +} + +var browserPoolsDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a browser pool", + Args: cobra.ExactArgs(1), + RunE: runBrowserPoolsDelete, +} + +var browserPoolsAcquireCmd = &cobra.Command{ + Use: "acquire ", + Short: "Acquire a browser from the pool", + Args: cobra.ExactArgs(1), + RunE: runBrowserPoolsAcquire, +} + +var browserPoolsReleaseCmd = &cobra.Command{ + Use: "release ", + Short: "Release a browser back to the pool", + Args: cobra.ExactArgs(1), + RunE: runBrowserPoolsRelease, +} + +var browserPoolsFlushCmd = &cobra.Command{ + Use: "flush ", + Short: "Flush idle browsers from the pool", + Args: cobra.ExactArgs(1), + RunE: runBrowserPoolsFlush, +} + +func init() { + // list flags + browserPoolsListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // create flags + browserPoolsCreateCmd.Flags().Int64("size", 0, "Number of browsers in the pool") + _ = browserPoolsCreateCmd.MarkFlagRequired("size") + browserPoolsCreateCmd.Flags().Int64("fill-rate", 0, "Fill rate per minute") + browserPoolsCreateCmd.Flags().Int64("timeout", 0, "Idle timeout in seconds") + browserPoolsCreateCmd.Flags().Bool("stealth", false, "Enable stealth mode") + browserPoolsCreateCmd.Flags().Bool("headless", false, "Enable headless mode") + browserPoolsCreateCmd.Flags().Bool("kiosk", false, "Enable kiosk mode") + browserPoolsCreateCmd.Flags().String("profile-id", "", "Profile ID") + browserPoolsCreateCmd.Flags().String("profile-name", "", "Profile name") + browserPoolsCreateCmd.Flags().Bool("save-changes", false, "Save changes to profile") + browserPoolsCreateCmd.Flags().String("proxy-id", "", "Proxy ID") + browserPoolsCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names") + browserPoolsCreateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)") + + // get flags + browserPoolsGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // update flags + browserPoolsUpdateCmd.Flags().Int64("size", 0, "Number of browsers in the pool") + browserPoolsUpdateCmd.Flags().Int64("fill-rate", 0, "Fill rate per minute") + browserPoolsUpdateCmd.Flags().Int64("timeout", 0, "Idle timeout in seconds") + browserPoolsUpdateCmd.Flags().Bool("stealth", false, "Enable stealth mode") + browserPoolsUpdateCmd.Flags().Bool("headless", false, "Enable headless mode") + browserPoolsUpdateCmd.Flags().Bool("kiosk", false, "Enable kiosk mode") + browserPoolsUpdateCmd.Flags().String("profile-id", "", "Profile ID") + browserPoolsUpdateCmd.Flags().String("profile-name", "", "Profile name") + browserPoolsUpdateCmd.Flags().Bool("save-changes", false, "Save changes to profile") + browserPoolsUpdateCmd.Flags().String("proxy-id", "", "Proxy ID") + browserPoolsUpdateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names") + browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)") + browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", true, "Discard all idle browsers") + + // delete flags + browserPoolsDeleteCmd.Flags().Bool("force", false, "Force delete even if browsers are leased") + + // acquire flags + browserPoolsAcquireCmd.Flags().Int64("timeout", 5, "Acquire timeout in seconds") + + // release flags + browserPoolsReleaseCmd.Flags().String("session-id", "", "Browser session ID to release") + _ = browserPoolsReleaseCmd.MarkFlagRequired("session-id") + browserPoolsReleaseCmd.Flags().Bool("reuse", true, "Reuse the browser instance") + + browserPoolsCmd.AddCommand(browserPoolsListCmd) + browserPoolsCmd.AddCommand(browserPoolsCreateCmd) + browserPoolsCmd.AddCommand(browserPoolsGetCmd) + browserPoolsCmd.AddCommand(browserPoolsUpdateCmd) + browserPoolsCmd.AddCommand(browserPoolsDeleteCmd) + browserPoolsCmd.AddCommand(browserPoolsAcquireCmd) + browserPoolsCmd.AddCommand(browserPoolsReleaseCmd) + browserPoolsCmd.AddCommand(browserPoolsFlushCmd) +} + +func runBrowserPoolsList(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + out, _ := cmd.Flags().GetString("output") + c := BrowserPoolsCmd{client: &client.BrowserPools} + return c.List(cmd.Context(), BrowserPoolsListInput{Output: out}) +} + +func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + + size, _ := cmd.Flags().GetInt64("size") + fillRate, _ := cmd.Flags().GetInt64("fill-rate") + timeout, _ := cmd.Flags().GetInt64("timeout") + stealth, _ := cmd.Flags().GetBool("stealth") + headless, _ := cmd.Flags().GetBool("headless") + kiosk, _ := cmd.Flags().GetBool("kiosk") + profileID, _ := cmd.Flags().GetString("profile-id") + profileName, _ := cmd.Flags().GetString("profile-name") + saveChanges, _ := cmd.Flags().GetBool("save-changes") + proxyID, _ := cmd.Flags().GetString("proxy-id") + extensions, _ := cmd.Flags().GetStringSlice("extension") + viewport, _ := cmd.Flags().GetString("viewport") + + in := BrowserPoolsCreateInput{ + Size: size, + FillRate: fillRate, + TimeoutSeconds: timeout, + Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealth}, + Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headless}, + Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kiosk}, + ProfileID: profileID, + ProfileName: profileName, + ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, + ProxyID: proxyID, + Extensions: extensions, + Viewport: viewport, + } + + c := BrowserPoolsCmd{client: &client.BrowserPools} + return c.Create(cmd.Context(), in) +} + +func runBrowserPoolsGet(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + out, _ := cmd.Flags().GetString("output") + c := BrowserPoolsCmd{client: &client.BrowserPools} + return c.Get(cmd.Context(), BrowserPoolsGetInput{IDOrName: args[0], Output: out}) +} + +func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + + size, _ := cmd.Flags().GetInt64("size") + fillRate, _ := cmd.Flags().GetInt64("fill-rate") + timeout, _ := cmd.Flags().GetInt64("timeout") + stealth, _ := cmd.Flags().GetBool("stealth") + headless, _ := cmd.Flags().GetBool("headless") + kiosk, _ := cmd.Flags().GetBool("kiosk") + profileID, _ := cmd.Flags().GetString("profile-id") + profileName, _ := cmd.Flags().GetString("profile-name") + saveChanges, _ := cmd.Flags().GetBool("save-changes") + proxyID, _ := cmd.Flags().GetString("proxy-id") + extensions, _ := cmd.Flags().GetStringSlice("extension") + viewport, _ := cmd.Flags().GetString("viewport") + discardIdle, _ := cmd.Flags().GetBool("discard-all-idle") + + in := BrowserPoolsUpdateInput{ + IDOrName: args[0], + Size: size, + FillRate: fillRate, + TimeoutSeconds: timeout, + Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealth}, + Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headless}, + Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kiosk}, + ProfileID: profileID, + ProfileName: profileName, + ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, + ProxyID: proxyID, + Extensions: extensions, + Viewport: viewport, + DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle}, + } + + c := BrowserPoolsCmd{client: &client.BrowserPools} + return c.Update(cmd.Context(), in) +} + +func runBrowserPoolsDelete(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + force, _ := cmd.Flags().GetBool("force") + c := BrowserPoolsCmd{client: &client.BrowserPools} + return c.Delete(cmd.Context(), BrowserPoolsDeleteInput{IDOrName: args[0], Force: force}) +} + +func runBrowserPoolsAcquire(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + timeout, _ := cmd.Flags().GetInt64("timeout") + c := BrowserPoolsCmd{client: &client.BrowserPools} + return c.Acquire(cmd.Context(), BrowserPoolsAcquireInput{IDOrName: args[0], TimeoutSeconds: timeout}) +} + +func runBrowserPoolsRelease(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + sessionID, _ := cmd.Flags().GetString("session-id") + reuse, _ := cmd.Flags().GetBool("reuse") + c := BrowserPoolsCmd{client: &client.BrowserPools} + return c.Release(cmd.Context(), BrowserPoolsReleaseInput{ + IDOrName: args[0], + SessionID: sessionID, + Reuse: BoolFlag{Set: cmd.Flags().Changed("reuse"), Value: reuse}, + }) +} + +func runBrowserPoolsFlush(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + c := BrowserPoolsCmd{client: &client.BrowserPools} + return c.Flush(cmd.Context(), BrowserPoolsFlushInput{IDOrName: args[0]}) +} diff --git a/cmd/browsers.go b/cmd/browsers.go index f07b6e7..2799215 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -227,7 +227,7 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { return nil } - if browsers == nil || len(browsers) == 0 { + if len(browsers) == 0 { pterm.Info.Println("No running or persistent browsers found") return nil } @@ -353,27 +353,31 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { return util.CleanedUpSdkError{Err: err} } + printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Persistence, browser.Profile) + return nil +} + +func printBrowserSessionResult(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile) { tableData := pterm.TableData{ {"Property", "Value"}, - {"Session ID", browser.SessionID}, - {"CDP WebSocket URL", browser.CdpWsURL}, + {"Session ID", sessionID}, + {"CDP WebSocket URL", cdpURL}, } - if browser.BrowserLiveViewURL != "" { - tableData = append(tableData, []string{"Live View URL", browser.BrowserLiveViewURL}) + if liveViewURL != "" { + tableData = append(tableData, []string{"Live View URL", liveViewURL}) } - if browser.Persistence.ID != "" { - tableData = append(tableData, []string{"Persistent ID", browser.Persistence.ID}) + if persistence.ID != "" { + tableData = append(tableData, []string{"Persistent ID", persistence.ID}) } - if browser.Profile.ID != "" || browser.Profile.Name != "" { - profVal := browser.Profile.Name + if profile.ID != "" || profile.Name != "" { + profVal := profile.Name if profVal == "" { - profVal = browser.Profile.ID + profVal = profile.ID } tableData = append(tableData, []string{"Profile", profVal}) } PrintTableNoPad(tableData, true) - return nil } func (b BrowsersCmd) Delete(ctx context.Context, in BrowsersDeleteInput) error { @@ -2048,6 +2052,8 @@ func init() { browsersCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names to load (repeatable; may be passed multiple times or comma-separated)") browsersCreateCmd.Flags().String("viewport", "", "Browser viewport size (e.g., 1920x1080@25). Supported: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60") browsersCreateCmd.Flags().Bool("viewport-interactive", false, "Interactively select viewport size from list") + browsersCreateCmd.Flags().String("pool-id", "", "Browser pool ID to acquire from (mutually exclusive with --pool-name)") + browsersCreateCmd.Flags().String("pool-name", "", "Browser pool name to acquire from (mutually exclusive with --pool-id)") // Add flags for delete command browsersDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") @@ -2087,6 +2093,62 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { extensions, _ := cmd.Flags().GetStringSlice("extension") viewport, _ := cmd.Flags().GetString("viewport") viewportInteractive, _ := cmd.Flags().GetBool("viewport-interactive") + poolID, _ := cmd.Flags().GetString("pool-id") + poolName, _ := cmd.Flags().GetString("pool-name") + + if poolID != "" && poolName != "" { + pterm.Error.Println("must specify at most one of --pool-id or --pool-name") + return nil + } + + if poolID != "" || poolName != "" { + conflictFlags := []string{ + "persistent-id", + "stealth", + "headless", + "kiosk", + "timeout", + "profile-id", + "profile-name", + "save-changes", + "proxy-id", + "extension", + "viewport", + "viewport-interactive", + } + var conflicts []string + for _, name := range conflictFlags { + if cmd.Flags().Changed(name) { + conflicts = append(conflicts, "--"+name) + } + } + if len(conflicts) > 0 { + flagLabel := "--pool-id" + if poolName != "" { + flagLabel = "--pool-name" + } + pterm.Error.Printf("%s cannot be combined with %s\n", flagLabel, strings.Join(conflicts, ", ")) + return nil + } + + pool := poolID + if pool == "" { + pool = poolName + } + + pterm.Info.Printf("Acquiring browser from pool %s...\n", pool) + poolSvc := client.BrowserPools + resp, err := (&poolSvc).Acquire(cmd.Context(), pool, kernel.BrowserPoolAcquireParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + if resp == nil { + pterm.Error.Println("Acquire request timed out (no browser available). Retry to continue waiting.") + return nil + } + printBrowserSessionResult(resp.SessionID, resp.CdpWsURL, resp.BrowserLiveViewURL, resp.Persistence, resp.Profile) + return nil + } // Handle interactive viewport selection if viewportInteractive { @@ -2544,7 +2606,7 @@ func runBrowsersComputerSetCursor(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) svc := client.Browsers hiddenStr, _ := cmd.Flags().GetString("hidden") - + var hidden bool switch strings.ToLower(hiddenStr) { case "true", "1", "yes": @@ -2555,7 +2617,7 @@ func runBrowsersComputerSetCursor(cmd *cobra.Command, args []string) error { pterm.Error.Printf("Invalid value for --hidden: %s (expected true or false)\n", hiddenStr) return nil } - + b := BrowsersCmd{browsers: &svc, computer: &svc.Computer} return b.ComputerSetCursor(cmd.Context(), BrowsersComputerSetCursorInput{Identifier: args[0], Hidden: hidden}) } diff --git a/cmd/root.go b/cmd/root.go index f23f4a1..06cce7c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -123,6 +123,7 @@ func init() { rootCmd.AddCommand(deployCmd) rootCmd.AddCommand(invokeCmd) rootCmd.AddCommand(browsersCmd) + rootCmd.AddCommand(browserPoolsCmd) rootCmd.AddCommand(appCmd) rootCmd.AddCommand(profilesCmd) rootCmd.AddCommand(proxies.ProxiesCmd) From 8249362023b13a2c1af549f38b1369b2da90ba13 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Wed, 26 Nov 2025 12:23:57 -0800 Subject: [PATCH 02/11] temp --- go.mod | 2 ++ go.sum | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index cdbc232..f4b7e91 100644 --- a/go.mod +++ b/go.mod @@ -58,3 +58,5 @@ require ( golang.org/x/text v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/onkernel/kernel-go-sdk => github.com/stainless-sdks/kernel-go v0.0.0-20251126005244-235816dbdb53 diff --git a/go.sum b/go.sum index e1ca8b8..4fd5f58 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,6 @@ github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= -github.com/onkernel/kernel-go-sdk v0.20.0 h1:KBMBjs54QlzlbQOFZLcN0PHD2QR8wJIrpzEfBaf6YZ0= -github.com/onkernel/kernel-go-sdk v0.20.0/go.mod h1:t80buN1uCA/hwvm4D2SpjTJzZWcV7bWOFo9d7qdXD8M= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -118,6 +116,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stainless-sdks/kernel-go v0.0.0-20251126005244-235816dbdb53 h1:QrWZlxqBSN2jI366RmBs0yZMCxLSpaksdec4wT7wo94= +github.com/stainless-sdks/kernel-go v0.0.0-20251126005244-235816dbdb53/go.mod h1:t80buN1uCA/hwvm4D2SpjTJzZWcV7bWOFo9d7qdXD8M= 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= From 464e8b6551fb8c8fc8a7b66f49458a3e7cb9fb6a Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Mon, 1 Dec 2025 16:03:53 -0800 Subject: [PATCH 03/11] missing names --- cmd/browser_pools.go | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index 03bef3b..1f61fc8 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -63,12 +63,17 @@ func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) err } tableData := pterm.TableData{ - {"ID", "Available", "Acquired", "Created At", "Size"}, + {"ID", "Name", "Available", "Acquired", "Created At", "Size"}, } for _, p := range *pools { + name := p.Name + if name == "" { + name = "-" + } tableData = append(tableData, []string{ p.ID, + name, fmt.Sprintf("%d", p.AvailableCount), fmt.Sprintf("%d", p.AcquiredCount), util.FormatLocal(p.CreatedAt), @@ -81,6 +86,7 @@ func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) err } type BrowserPoolsCreateInput struct { + Name string Size int64 FillRate int64 TimeoutSeconds int64 @@ -100,6 +106,9 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) Size: in.Size, } + if in.Name != "" { + params.Name = kernel.String(in.Name) + } if in.FillRate > 0 { params.FillRatePerMinute = kernel.Int(in.FillRate) } @@ -173,7 +182,11 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) return util.CleanedUpSdkError{Err: err} } - pterm.Success.Printf("Created browser pool %s\n", pool.ID) + if pool.Name != "" { + pterm.Success.Printf("Created browser pool %s (%s)\n", pool.Name, pool.ID) + } else { + pterm.Success.Printf("Created browser pool %s\n", pool.ID) + } return nil } @@ -202,9 +215,14 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error return nil } + name := pool.Name + if name == "" { + name = "-" + } tableData := pterm.TableData{ {"Property", "Value"}, {"ID", pool.ID}, + {"Name", name}, {"Size", fmt.Sprintf("%d", pool.BrowserPoolConfig.Size)}, {"Available", fmt.Sprintf("%d", pool.AvailableCount)}, {"Acquired", fmt.Sprintf("%d", pool.AcquiredCount)}, @@ -217,6 +235,7 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error type BrowserPoolsUpdateInput struct { IDOrName string + Name string Size int64 FillRate int64 TimeoutSeconds int64 @@ -235,6 +254,9 @@ type BrowserPoolsUpdateInput struct { func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) error { params := kernel.BrowserPoolUpdateParams{} + if in.Name != "" { + params.Name = kernel.String(in.Name) + } if in.Size > 0 { params.Size = in.Size } @@ -313,7 +335,11 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) if err != nil { return util.CleanedUpSdkError{Err: err} } - pterm.Success.Printf("Updated browser pool %s\n", pool.ID) + if pool.Name != "" { + pterm.Success.Printf("Updated browser pool %s (%s)\n", pool.Name, pool.ID) + } else { + pterm.Success.Printf("Updated browser pool %s\n", pool.ID) + } return nil } @@ -398,9 +424,6 @@ func (c BrowserPoolsCmd) Flush(ctx context.Context, in BrowserPoolsFlushInput) e return nil } -// Cobra commands (same as before) -// ... - var browserPoolsCmd = &cobra.Command{ Use: "browser-pools", Aliases: []string{"browser-pool", "pool", "pools"}, @@ -467,6 +490,7 @@ func init() { browserPoolsListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") // create flags + browserPoolsCreateCmd.Flags().String("name", "", "Optional unique name for the pool") browserPoolsCreateCmd.Flags().Int64("size", 0, "Number of browsers in the pool") _ = browserPoolsCreateCmd.MarkFlagRequired("size") browserPoolsCreateCmd.Flags().Int64("fill-rate", 0, "Fill rate per minute") @@ -485,6 +509,7 @@ func init() { browserPoolsGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") // update flags + browserPoolsUpdateCmd.Flags().String("name", "", "Update the pool name") browserPoolsUpdateCmd.Flags().Int64("size", 0, "Number of browsers in the pool") browserPoolsUpdateCmd.Flags().Int64("fill-rate", 0, "Fill rate per minute") browserPoolsUpdateCmd.Flags().Int64("timeout", 0, "Idle timeout in seconds") @@ -530,6 +555,7 @@ func runBrowserPoolsList(cmd *cobra.Command, args []string) error { func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) + name, _ := cmd.Flags().GetString("name") size, _ := cmd.Flags().GetInt64("size") fillRate, _ := cmd.Flags().GetInt64("fill-rate") timeout, _ := cmd.Flags().GetInt64("timeout") @@ -544,6 +570,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { viewport, _ := cmd.Flags().GetString("viewport") in := BrowserPoolsCreateInput{ + Name: name, Size: size, FillRate: fillRate, TimeoutSeconds: timeout, @@ -572,6 +599,7 @@ func runBrowserPoolsGet(cmd *cobra.Command, args []string) error { func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) + name, _ := cmd.Flags().GetString("name") size, _ := cmd.Flags().GetInt64("size") fillRate, _ := cmd.Flags().GetInt64("fill-rate") timeout, _ := cmd.Flags().GetInt64("timeout") @@ -588,6 +616,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { in := BrowserPoolsUpdateInput{ IDOrName: args[0], + Name: name, Size: size, FillRate: fillRate, TimeoutSeconds: timeout, From d1eb8755dfb1b0c6644cf8ebdf445d57fe471935 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Dec 2025 12:48:31 -0800 Subject: [PATCH 04/11] temp: replace sdk --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f4b7e91..5a1c374 100644 --- a/go.mod +++ b/go.mod @@ -59,4 +59,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/onkernel/kernel-go-sdk => github.com/stainless-sdks/kernel-go v0.0.0-20251126005244-235816dbdb53 +replace github.com/onkernel/kernel-go-sdk => github.com/stainless-sdks/kernel-go v0.0.0-20251202202420-69dcf3471d1b diff --git a/go.sum b/go.sum index 4fd5f58..1175189 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stainless-sdks/kernel-go v0.0.0-20251126005244-235816dbdb53 h1:QrWZlxqBSN2jI366RmBs0yZMCxLSpaksdec4wT7wo94= -github.com/stainless-sdks/kernel-go v0.0.0-20251126005244-235816dbdb53/go.mod h1:t80buN1uCA/hwvm4D2SpjTJzZWcV7bWOFo9d7qdXD8M= +github.com/stainless-sdks/kernel-go v0.0.0-20251202202420-69dcf3471d1b h1:lpVRUIZg34hFzHDjPHKIMzhCwLGMUh3F49D2jsVc6E4= +github.com/stainless-sdks/kernel-go v0.0.0-20251202202420-69dcf3471d1b/go.mod h1:t80buN1uCA/hwvm4D2SpjTJzZWcV7bWOFo9d7qdXD8M= 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= From 26f85ee0302481c6639a2679d2f672fa63c51201 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Dec 2025 12:48:44 -0800 Subject: [PATCH 05/11] update for sdk changes --- cmd/browser_pools.go | 88 +++++++++++++++++++++++++------------------- cmd/browsers.go | 6 +-- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index 1f61fc8..fd79374 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -102,27 +102,27 @@ type BrowserPoolsCreateInput struct { } func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) error { - params := kernel.BrowserPoolNewParams{ + req := kernel.BrowserPoolRequestParam{ Size: in.Size, } if in.Name != "" { - params.Name = kernel.String(in.Name) + req.Name = kernel.String(in.Name) } if in.FillRate > 0 { - params.FillRatePerMinute = kernel.Int(in.FillRate) + req.FillRatePerMinute = kernel.Int(in.FillRate) } if in.TimeoutSeconds > 0 { - params.TimeoutSeconds = kernel.Int(in.TimeoutSeconds) + req.TimeoutSeconds = kernel.Int(in.TimeoutSeconds) } if in.Stealth.Set { - params.Stealth = kernel.Bool(in.Stealth.Value) + req.Stealth = kernel.Bool(in.Stealth.Value) } if in.Headless.Set { - params.Headless = kernel.Bool(in.Headless.Value) + req.Headless = kernel.Bool(in.Headless.Value) } if in.Kiosk.Set { - params.KioskMode = kernel.Bool(in.Kiosk.Value) + req.KioskMode = kernel.Bool(in.Kiosk.Value) } // Profile @@ -130,18 +130,18 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) pterm.Error.Println("must specify at most one of --profile-id or --profile-name") return nil } else if in.ProfileID != "" || in.ProfileName != "" { - params.Profile = kernel.BrowserPoolNewParamsProfile{ + req.Profile = kernel.BrowserProfileParam{ SaveChanges: kernel.Bool(in.ProfileSaveChanges.Value), } if in.ProfileID != "" { - params.Profile.ID = kernel.String(in.ProfileID) + req.Profile.ID = kernel.String(in.ProfileID) } else if in.ProfileName != "" { - params.Profile.Name = kernel.String(in.ProfileName) + req.Profile.Name = kernel.String(in.ProfileName) } } if in.ProxyID != "" { - params.ProxyID = kernel.String(in.ProxyID) + req.ProxyID = kernel.String(in.ProxyID) } // Extensions @@ -151,13 +151,13 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) if val == "" { continue } - item := kernel.BrowserPoolNewParamsExtension{} + item := kernel.BrowserExtensionParam{} if cuidRegex.MatchString(val) { item.ID = kernel.String(val) } else { item.Name = kernel.String(val) } - params.Extensions = append(params.Extensions, item) + req.Extensions = append(req.Extensions, item) } } @@ -168,15 +168,19 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) pterm.Error.Printf("Invalid viewport format: %v\n", err) return nil } - params.Viewport = kernel.BrowserPoolNewParamsViewport{ + req.Viewport = kernel.BrowserViewportParam{ Width: width, Height: height, } if refreshRate > 0 { - params.Viewport.RefreshRate = kernel.Int(refreshRate) + req.Viewport.RefreshRate = kernel.Int(refreshRate) } } + params := kernel.BrowserPoolNewParams{ + BrowserPoolRequest: req, + } + pool, err := c.client.New(ctx, params) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -252,31 +256,31 @@ type BrowserPoolsUpdateInput struct { } func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) error { - params := kernel.BrowserPoolUpdateParams{} + req := kernel.BrowserPoolUpdateRequestParam{} if in.Name != "" { - params.Name = kernel.String(in.Name) + req.Name = kernel.String(in.Name) } if in.Size > 0 { - params.Size = in.Size + req.Size = in.Size } if in.FillRate > 0 { - params.FillRatePerMinute = kernel.Int(in.FillRate) + req.FillRatePerMinute = kernel.Int(in.FillRate) } if in.TimeoutSeconds > 0 { - params.TimeoutSeconds = kernel.Int(in.TimeoutSeconds) + req.TimeoutSeconds = kernel.Int(in.TimeoutSeconds) } if in.Stealth.Set { - params.Stealth = kernel.Bool(in.Stealth.Value) + req.Stealth = kernel.Bool(in.Stealth.Value) } if in.Headless.Set { - params.Headless = kernel.Bool(in.Headless.Value) + req.Headless = kernel.Bool(in.Headless.Value) } if in.Kiosk.Set { - params.KioskMode = kernel.Bool(in.Kiosk.Value) + req.KioskMode = kernel.Bool(in.Kiosk.Value) } if in.DiscardAllIdle.Set { - params.DiscardAllIdle = kernel.Bool(in.DiscardAllIdle.Value) + req.DiscardAllIdle = kernel.Bool(in.DiscardAllIdle.Value) } // Profile @@ -284,18 +288,18 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) pterm.Error.Println("must specify at most one of --profile-id or --profile-name") return nil } else if in.ProfileID != "" || in.ProfileName != "" { - params.Profile = kernel.BrowserPoolUpdateParamsProfile{ + req.Profile = kernel.BrowserProfileParam{ SaveChanges: kernel.Bool(in.ProfileSaveChanges.Value), } if in.ProfileID != "" { - params.Profile.ID = kernel.String(in.ProfileID) + req.Profile.ID = kernel.String(in.ProfileID) } else if in.ProfileName != "" { - params.Profile.Name = kernel.String(in.ProfileName) + req.Profile.Name = kernel.String(in.ProfileName) } } if in.ProxyID != "" { - params.ProxyID = kernel.String(in.ProxyID) + req.ProxyID = kernel.String(in.ProxyID) } // Extensions @@ -305,13 +309,13 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) if val == "" { continue } - item := kernel.BrowserPoolUpdateParamsExtension{} + item := kernel.BrowserExtensionParam{} if cuidRegex.MatchString(val) { item.ID = kernel.String(val) } else { item.Name = kernel.String(val) } - params.Extensions = append(params.Extensions, item) + req.Extensions = append(req.Extensions, item) } } @@ -322,15 +326,19 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) pterm.Error.Printf("Invalid viewport format: %v\n", err) return nil } - params.Viewport = kernel.BrowserPoolUpdateParamsViewport{ + req.Viewport = kernel.BrowserViewportParam{ Width: width, Height: height, } if refreshRate > 0 { - params.Viewport.RefreshRate = kernel.Int(refreshRate) + req.Viewport.RefreshRate = kernel.Int(refreshRate) } } + params := kernel.BrowserPoolUpdateParams{ + BrowserPoolUpdateRequest: req, + } + pool, err := c.client.Update(ctx, in.IDOrName, params) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -367,9 +375,12 @@ type BrowserPoolsAcquireInput struct { } func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInput) error { - params := kernel.BrowserPoolAcquireParams{} + req := kernel.BrowserPoolAcquireRequestParam{} if in.TimeoutSeconds > 0 { - params.AcquireTimeoutSeconds = kernel.Int(in.TimeoutSeconds) + req.AcquireTimeoutSeconds = kernel.Int(in.TimeoutSeconds) + } + params := kernel.BrowserPoolAcquireParams{ + BrowserPoolAcquireRequest: req, } resp, err := c.client.Acquire(ctx, in.IDOrName, params) if err != nil { @@ -397,11 +408,14 @@ type BrowserPoolsReleaseInput struct { } func (c BrowserPoolsCmd) Release(ctx context.Context, in BrowserPoolsReleaseInput) error { - params := kernel.BrowserPoolReleaseParams{ - SessionID: kernel.String(in.SessionID), + req := kernel.BrowserPoolReleaseRequestParam{ + SessionID: in.SessionID, } if in.Reuse.Set { - params.Reuse = kernel.Bool(in.Reuse.Value) + req.Reuse = kernel.Bool(in.Reuse.Value) + } + params := kernel.BrowserPoolReleaseParams{ + BrowserPoolReleaseRequest: req, } err := c.client.Release(ctx, in.IDOrName, params) if err != nil { diff --git a/cmd/browsers.go b/cmd/browsers.go index 961db50..3633c65 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -300,7 +300,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { pterm.Error.Println("must specify at most one of --profile-id or --profile-name") return nil } else if in.ProfileID != "" || in.ProfileName != "" { - params.Profile = kernel.BrowserNewParamsProfile{ + params.Profile = kernel.BrowserProfileParam{ SaveChanges: kernel.Opt(in.ProfileSaveChanges.Value), } if in.ProfileID != "" { @@ -322,7 +322,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { if val == "" { continue } - item := kernel.BrowserNewParamsExtension{} + item := kernel.BrowserExtensionParam{} if cuidRegex.MatchString(val) { item.ID = kernel.Opt(val) } else { @@ -339,7 +339,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { pterm.Error.Printf("Invalid viewport format: %v\n", err) return nil } - params.Viewport = kernel.BrowserNewParamsViewport{ + params.Viewport = kernel.BrowserViewportParam{ Width: width, Height: height, } From 136945a65993991fde90ebe34ccba4580333cd54 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Dec 2025 13:46:22 -0800 Subject: [PATCH 06/11] update readme --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dbdd375..2f50bb7 100644 --- a/README.md +++ b/README.md @@ -162,14 +162,18 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). ### Browser Pools - `kernel browser-pools list` - List browser pools + - `-o, --output json` - Output raw JSON response - `kernel browser-pools create` - Create a browser pool + - `--name ` - Optional unique name for the pool - `--size ` - Number of browsers in the pool (required) - - `--fill-rate ` - Percentage of the pool to fill per minute + - `--fill-rate ` - Percentage of the pool to fill per minute - `--timeout ` - Idle timeout for browsers acquired from the pool - `--stealth`, `--headless`, `--kiosk` - Default pool configuration - `--profile-id`, `--profile-name`, `--save-changes`, `--proxy-id`, `--extension`, `--viewport` - Same semantics as `kernel browsers create` - `kernel browser-pools get ` - Get pool details -- `kernel browser-pools update ` - Update pool configuration (same flags as create plus `--discard-all-idle`) + - `-o, --output json` - Output raw JSON response +- `kernel browser-pools update ` - Update pool configuration + - Same flags as create plus `--discard-all-idle` to discard all idle browsers in the pool and refill at the specified fill rate - `kernel browser-pools delete ` - Delete a pool - `--force` - Force delete even if browsers are leased - `kernel browser-pools acquire ` - Acquire a browser from the pool From 450fbe7efda555083c2e90a813d29b5790fbbb8c Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Dec 2025 13:46:31 -0800 Subject: [PATCH 07/11] small tweaks --- cmd/browsers.go | 58 +++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 3633c65..de60548 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -23,6 +23,7 @@ import ( "github.com/onkernel/kernel-go-sdk/shared" "github.com/pterm/pterm" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // BrowsersService defines the subset of the Kernel SDK browser client that we use. @@ -227,7 +228,7 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { return nil } - if browsers == nil || len(browsers) == 0 { + if len(browsers) == 0 { pterm.Info.Println("No running browsers found") return nil } @@ -2100,33 +2101,39 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { } if poolID != "" || poolName != "" { - conflictFlags := []string{ - "persistent-id", - "stealth", - "headless", - "kiosk", - "timeout", - "profile-id", - "profile-name", - "save-changes", - "proxy-id", - "extension", - "viewport", - "viewport-interactive", + // When using a pool, configuration comes from the pool itself. + allowedFlags := map[string]bool{ + "pool-id": true, + "pool-name": true, + "timeout": true, + // Global persistent flags that don't configure browsers + "no-color": true, + "log-level": true, } + + // Check if any browser configuration flags were set (which would conflict). var conflicts []string - for _, name := range conflictFlags { - if cmd.Flags().Changed(name) { - conflicts = append(conflicts, "--"+name) + cmd.Flags().Visit(func(f *pflag.Flag) { + if !allowedFlags[f.Name] { + conflicts = append(conflicts, "--"+f.Name) } - } + }) + if len(conflicts) > 0 { flagLabel := "--pool-id" if poolName != "" { flagLabel = "--pool-name" } - pterm.Error.Printf("%s cannot be combined with %s\n", flagLabel, strings.Join(conflicts, ", ")) - return nil + pterm.Warning.Printf("You specified %s, but also provided browser configuration flags: %s\n", flagLabel, strings.Join(conflicts, ", ")) + pterm.Info.Println("When using a pool, all browser configuration comes from the pool itself.") + pterm.Info.Println("The conflicting flags will be ignored.") + + result, _ := pterm.DefaultInteractiveConfirm.Show("Continue with pool configuration?") + if !result { + pterm.Info.Println("Cancelled. Remove conflicting flags or omit the pool flag.") + return nil + } + pterm.Success.Println("Proceeding with pool configuration...") } pool := poolID @@ -2136,7 +2143,16 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { pterm.Info.Printf("Acquiring browser from pool %s...\n", pool) poolSvc := client.BrowserPools - resp, err := (&poolSvc).Acquire(cmd.Context(), pool, kernel.BrowserPoolAcquireParams{}) + + req := kernel.BrowserPoolAcquireRequestParam{} + if cmd.Flags().Changed("timeout") && timeout > 0 { + req.AcquireTimeoutSeconds = kernel.Int(int64(timeout)) + } + acquireParams := kernel.BrowserPoolAcquireParams{ + BrowserPoolAcquireRequest: req, + } + + resp, err := (&poolSvc).Acquire(cmd.Context(), pool, acquireParams) if err != nil { return util.CleanedUpSdkError{Err: err} } From de04522d008e2efb9a2689f987cbf73c87b7cb4f Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Dec 2025 14:33:01 -0800 Subject: [PATCH 08/11] bump to 0.21 --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5a1c374..de0e894 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/charmbracelet/fang v0.2.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/onkernel/kernel-go-sdk v0.20.0 + github.com/onkernel/kernel-go-sdk v0.21.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 @@ -58,5 +58,3 @@ require ( golang.org/x/text v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/onkernel/kernel-go-sdk => github.com/stainless-sdks/kernel-go v0.0.0-20251202202420-69dcf3471d1b diff --git a/go.sum b/go.sum index 1175189..24c7040 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,8 @@ github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= +github.com/onkernel/kernel-go-sdk v0.21.0 h1:ah1uBl71pk5DJmge0Z8eyyk1dZw6ik9ETuyd+3tIrl4= +github.com/onkernel/kernel-go-sdk v0.21.0/go.mod h1:t80buN1uCA/hwvm4D2SpjTJzZWcV7bWOFo9d7qdXD8M= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From 590d5c9bf8572b14db0238dce134ebb0e8a41dc8 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Wed, 3 Dec 2025 15:39:33 -0800 Subject: [PATCH 09/11] script --- scripts/test-browser-pools.sh | 190 ++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100755 scripts/test-browser-pools.sh diff --git a/scripts/test-browser-pools.sh b/scripts/test-browser-pools.sh new file mode 100755 index 0000000..0334eea --- /dev/null +++ b/scripts/test-browser-pools.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +set -e + +# Browser Pool Lifecycle Test +# +# This script tests the full lifecycle of browser pools: +# 1. Create a pool +# 2. Acquire a browser from it +# 3. Use the browser (simulated with sleep) +# 4. Release the browser back to the pool +# 5. Check pool state +# 6. Flush idle browsers +# 7. Delete the pool + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +KERNEL="${KERNEL:-kernel}" # Use $KERNEL env var or default to 'kernel' +POOL_NAME="test-pool-$(date +%s)" +POOL_SIZE=2 +SLEEP_TIME=5 + +# Helper functions +log_step() { + echo -e "${BLUE}==>${NC} $1" +} + +log_success() { + echo -e "${GREEN}✓${NC} $1" +} + +log_error() { + echo -e "${RED}✗${NC} $1" +} + +log_info() { + echo -e "${YELLOW}ℹ${NC} $1" +} + +# Check if kernel CLI is available +if ! command -v "$KERNEL" &> /dev/null && [ ! -x "$KERNEL" ]; then + log_error "kernel CLI not found at '$KERNEL'. Please install it or set KERNEL env var." + exit 1 +fi + +# Cleanup function (only runs if script exits early/unexpectedly) +cleanup() { + if [ -n "$POOL_ID" ]; then + echo "" + log_step "Script exited early - cleaning up pool $POOL_ID" + "$KERNEL" browser-pools delete "$POOL_ID" --force --no-color || true + log_info "Cleanup complete (used --force to ensure deletion)" + fi +} + +trap cleanup EXIT + +echo "" +log_step "Starting browser pool integration test" +echo "" + +# Step 1: Create a pool +log_step "Step 1: Creating browser pool with name '$POOL_NAME' and size $POOL_SIZE" +"$KERNEL" browser-pools create \ + --name "$POOL_NAME" \ + --size "$POOL_SIZE" \ + --timeout 300 \ + --no-color + +# Extract pool ID using the list command +POOL_ID=$("$KERNEL" browser-pools list --output json --no-color | jq -r ".[] | select(.name == \"$POOL_NAME\") | .id") + +if [ -z "$POOL_ID" ]; then + log_error "Failed to create pool or extract pool ID" + exit 1 +fi + +log_success "Created pool: $POOL_ID" +echo "" + +# Step 2: List pools to verify +log_step "Step 2: Listing all pools" +"$KERNEL" browser-pools list --no-color +echo "" + +# Step 3: Get pool details +log_step "Step 3: Getting pool details" +"$KERNEL" browser-pools get "$POOL_ID" --no-color +echo "" + +# Wait for pool to be ready +log_info "Waiting for pool to initialize..." +sleep 3 + +# Step 4: Acquire a browser from the pool +log_step "Step 4: Acquiring a browser from the pool" +ACQUIRE_OUTPUT=$("$KERNEL" browser-pools acquire "$POOL_ID" --timeout 10 --no-color 2>&1) + +if echo "$ACQUIRE_OUTPUT" | grep -q "timed out"; then + log_error "Failed to acquire browser (timeout or no browsers available)" + exit 1 +fi + +# Parse the session ID from the table output (format: "Session ID | ") +SESSION_ID=$(echo "$ACQUIRE_OUTPUT" | grep "Session ID" | awk -F'|' '{print $2}' | xargs) + +if [ -z "$SESSION_ID" ]; then + log_error "Failed to extract session ID from acquire response" + echo "Response: $ACQUIRE_OUTPUT" + exit 1 +fi + +log_success "Acquired browser with session ID: $SESSION_ID" +echo "" + +# Step 5: Get pool details again to see the acquired browser +log_step "Step 5: Checking pool state (should show 1 acquired)" +POOL_DETAILS=$("$KERNEL" browser-pools get "$POOL_ID" --output json --no-color) +ACQUIRED_COUNT=$(echo "$POOL_DETAILS" | jq -r '.acquired_count // .acquiredCount // 0') +AVAILABLE_COUNT=$(echo "$POOL_DETAILS" | jq -r '.available_count // .availableCount // 0') + +log_info "Acquired: $ACQUIRED_COUNT, Available: $AVAILABLE_COUNT" +"$KERNEL" browser-pools get "$POOL_ID" --no-color +echo "" + +# Step 6: Sleep to simulate usage +log_step "Step 6: Simulating browser usage (sleeping for ${SLEEP_TIME}s)" +sleep "$SLEEP_TIME" +log_success "Usage simulation complete" +echo "" + +# Step 7: Release the browser back to the pool +log_step "Step 7: Releasing browser back to pool" +"$KERNEL" browser-pools release "$POOL_ID" \ + --session-id "$SESSION_ID" \ + --reuse \ + --no-color + +log_success "Browser released" +echo "" + +# Step 8: Get pool details again +log_step "Step 8: Checking pool state after release" +"$KERNEL" browser-pools get "$POOL_ID" --no-color +echo "" + +# Step 9: Flush the pool +log_step "Step 9: Flushing idle browsers from pool" +"$KERNEL" browser-pools flush "$POOL_ID" --no-color +log_success "Pool flushed" +echo "" + +# Step 10: Delete the pool (should succeed if browsers are properly released) +log_step "Step 10: Deleting the pool" +DELETED_POOL_ID="$POOL_ID" +set +e # Temporarily disable exit-on-error to see the result +"$KERNEL" browser-pools delete "$POOL_ID" --no-color +DELETE_EXIT=$? +set -e # Re-enable exit-on-error + +if [ $DELETE_EXIT -eq 0 ]; then + log_success "Pool deleted successfully" + POOL_ID="" # Clear so cleanup doesn't try again + echo "" +else + log_error "Failed to delete pool - browsers may still be in acquired state" + log_info "This suggests the release operation hasn't fully completed" + log_info "Pool $POOL_ID left for debugging (clean up manually if needed)" + POOL_ID="" # Clear to prevent cleanup trap from trying + echo "" +fi + +# Verify deletion +log_step "Verifying pool deletion" +if "$KERNEL" browser-pools list --output json --no-color | jq -e ".[] | select(.id == \"$DELETED_POOL_ID\") | .id" > /dev/null 2>&1; then + log_error "Pool may still exist" +else + log_success "Pool successfully deleted and no longer exists" +fi + +echo "" +log_success "Integration test completed successfully!" +echo "" + From 5420d371398cb4440f7a1050a6e1c32553ac7701 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Wed, 3 Dec 2025 15:45:44 -0800 Subject: [PATCH 10/11] go mod tidy --- go.mod | 2 +- go.sum | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/go.mod b/go.mod index de0e894..b17f5cc 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.11.0 github.com/zalando/go-keyring v0.2.6 golang.org/x/oauth2 v0.30.0 @@ -46,7 +47,6 @@ require ( github.com/muesli/roff v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/pflag v1.0.6 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/go.sum b/go.sum index 24c7040..f5b74b9 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,6 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stainless-sdks/kernel-go v0.0.0-20251202202420-69dcf3471d1b h1:lpVRUIZg34hFzHDjPHKIMzhCwLGMUh3F49D2jsVc6E4= -github.com/stainless-sdks/kernel-go v0.0.0-20251202202420-69dcf3471d1b/go.mod h1:t80buN1uCA/hwvm4D2SpjTJzZWcV7bWOFo9d7qdXD8M= 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= From 1b1935ecea30d69b919364d24933ef5c7d16f2e3 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Wed, 3 Dec 2025 16:01:24 -0800 Subject: [PATCH 11/11] pr feedback --- cmd/browser_pools.go | 171 +++++++++++++++++++++++-------------------- 1 file changed, 91 insertions(+), 80 deletions(-) diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index fd79374..76b5f3e 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -126,18 +126,13 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) } // Profile - if in.ProfileID != "" && in.ProfileName != "" { - pterm.Error.Println("must specify at most one of --profile-id or --profile-name") + profile, err := buildProfileParam(in.ProfileID, in.ProfileName, in.ProfileSaveChanges) + if err != nil { + pterm.Error.Println(err.Error()) return nil - } else if in.ProfileID != "" || in.ProfileName != "" { - req.Profile = kernel.BrowserProfileParam{ - SaveChanges: kernel.Bool(in.ProfileSaveChanges.Value), - } - if in.ProfileID != "" { - req.Profile.ID = kernel.String(in.ProfileID) - } else if in.ProfileName != "" { - req.Profile.Name = kernel.String(in.ProfileName) - } + } + if profile != nil { + req.Profile = *profile } if in.ProxyID != "" { @@ -145,36 +140,16 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) } // Extensions - if len(in.Extensions) > 0 { - for _, ext := range in.Extensions { - val := strings.TrimSpace(ext) - if val == "" { - continue - } - item := kernel.BrowserExtensionParam{} - if cuidRegex.MatchString(val) { - item.ID = kernel.String(val) - } else { - item.Name = kernel.String(val) - } - req.Extensions = append(req.Extensions, item) - } - } + req.Extensions = buildExtensionsParam(in.Extensions) // Viewport - if in.Viewport != "" { - width, height, refreshRate, err := parseViewport(in.Viewport) - if err != nil { - pterm.Error.Printf("Invalid viewport format: %v\n", err) - return nil - } - req.Viewport = kernel.BrowserViewportParam{ - Width: width, - Height: height, - } - if refreshRate > 0 { - req.Viewport.RefreshRate = kernel.Int(refreshRate) - } + viewport, err := buildViewportParam(in.Viewport) + if err != nil { + pterm.Error.Println(err.Error()) + return nil + } + if viewport != nil { + req.Viewport = *viewport } params := kernel.BrowserPoolNewParams{ @@ -284,18 +259,13 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) } // Profile - if in.ProfileID != "" && in.ProfileName != "" { - pterm.Error.Println("must specify at most one of --profile-id or --profile-name") + profile, err := buildProfileParam(in.ProfileID, in.ProfileName, in.ProfileSaveChanges) + if err != nil { + pterm.Error.Println(err.Error()) return nil - } else if in.ProfileID != "" || in.ProfileName != "" { - req.Profile = kernel.BrowserProfileParam{ - SaveChanges: kernel.Bool(in.ProfileSaveChanges.Value), - } - if in.ProfileID != "" { - req.Profile.ID = kernel.String(in.ProfileID) - } else if in.ProfileName != "" { - req.Profile.Name = kernel.String(in.ProfileName) - } + } + if profile != nil { + req.Profile = *profile } if in.ProxyID != "" { @@ -303,36 +273,16 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) } // Extensions - if len(in.Extensions) > 0 { - for _, ext := range in.Extensions { - val := strings.TrimSpace(ext) - if val == "" { - continue - } - item := kernel.BrowserExtensionParam{} - if cuidRegex.MatchString(val) { - item.ID = kernel.String(val) - } else { - item.Name = kernel.String(val) - } - req.Extensions = append(req.Extensions, item) - } - } + req.Extensions = buildExtensionsParam(in.Extensions) // Viewport - if in.Viewport != "" { - width, height, refreshRate, err := parseViewport(in.Viewport) - if err != nil { - pterm.Error.Printf("Invalid viewport format: %v\n", err) - return nil - } - req.Viewport = kernel.BrowserViewportParam{ - Width: width, - Height: height, - } - if refreshRate > 0 { - req.Viewport.RefreshRate = kernel.Int(refreshRate) - } + viewport, err := buildViewportParam(in.Viewport) + if err != nil { + pterm.Error.Println(err.Error()) + return nil + } + if viewport != nil { + req.Viewport = *viewport } params := kernel.BrowserPoolUpdateParams{ @@ -536,13 +486,13 @@ func init() { browserPoolsUpdateCmd.Flags().String("proxy-id", "", "Proxy ID") browserPoolsUpdateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names") browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)") - browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", true, "Discard all idle browsers") + browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers") // delete flags browserPoolsDeleteCmd.Flags().Bool("force", false, "Force delete even if browsers are leased") // acquire flags - browserPoolsAcquireCmd.Flags().Int64("timeout", 5, "Acquire timeout in seconds") + browserPoolsAcquireCmd.Flags().Int64("timeout", 0, "Acquire timeout in seconds") // release flags browserPoolsReleaseCmd.Flags().String("session-id", "", "Browser session ID to release") @@ -681,3 +631,64 @@ func runBrowserPoolsFlush(cmd *cobra.Command, args []string) error { c := BrowserPoolsCmd{client: &client.BrowserPools} return c.Flush(cmd.Context(), BrowserPoolsFlushInput{IDOrName: args[0]}) } + +func buildProfileParam(profileID, profileName string, saveChanges BoolFlag) (*kernel.BrowserProfileParam, error) { + if profileID != "" && profileName != "" { + return nil, fmt.Errorf("must specify at most one of --profile-id or --profile-name") + } + if profileID == "" && profileName == "" { + return nil, nil + } + + profile := kernel.BrowserProfileParam{ + SaveChanges: kernel.Bool(saveChanges.Value), + } + if profileID != "" { + profile.ID = kernel.String(profileID) + } else if profileName != "" { + profile.Name = kernel.String(profileName) + } + return &profile, nil +} + +func buildExtensionsParam(extensions []string) []kernel.BrowserExtensionParam { + if len(extensions) == 0 { + return nil + } + + var result []kernel.BrowserExtensionParam + for _, ext := range extensions { + val := strings.TrimSpace(ext) + if val == "" { + continue + } + item := kernel.BrowserExtensionParam{} + if cuidRegex.MatchString(val) { + item.ID = kernel.String(val) + } else { + item.Name = kernel.String(val) + } + result = append(result, item) + } + return result +} + +func buildViewportParam(viewport string) (*kernel.BrowserViewportParam, error) { + if viewport == "" { + return nil, nil + } + + width, height, refreshRate, err := parseViewport(viewport) + if err != nil { + return nil, fmt.Errorf("invalid viewport format: %v", err) + } + + vp := kernel.BrowserViewportParam{ + Width: width, + Height: height, + } + if refreshRate > 0 { + vp.RefreshRate = kernel.Int(refreshRate) + } + return &vp, nil +}