Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ Commands with JSON output support:
- `-s, --stealth` - Launch browser in stealth mode to avoid detection
- `-H, --headless` - Launch browser without GUI access
- `--kiosk` - Launch browser in kiosk mode
- `--start-url <url>` - Initial page to open on launch
- `--pool-id <id>` - Acquire a browser from the specified pool (mutually exclusive with --pool-name; ignores other session flags)
- `--pool-name <name>` - Acquire a browser from the pool name (mutually exclusive with --pool-id; ignores other session flags)
- `--output json`, `-o json` - Output raw JSON object
Expand Down Expand Up @@ -242,12 +243,12 @@ Commands with JSON output support:
- `--fill-rate <n>` - Percentage of the pool to fill per minute
- `--timeout <seconds>` - 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`
- `--profile-id`, `--profile-name`, `--save-changes`, `--proxy-id`, `--start-url`, `--extension`, `--viewport` - Same semantics as `kernel browsers create`
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools get <id-or-name>` - Get pool details
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools update <id-or-name>` - 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
- Same flags as create plus `--clear-start-url` (remove the pool's start URL) and `--discard-all-idle` (discard all idle browsers and refill)
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools delete <id-or-name>` - Delete a pool
- `--force` - Force delete even if browsers are leased
Expand Down
40 changes: 40 additions & 0 deletions cmd/browser_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ type BrowserPoolsCreateInput struct {
ProfileName string
ProfileSaveChanges BoolFlag
ProxyID string
StartURL string
Extensions []string
Viewport string
Output string
Expand All @@ -95,6 +96,9 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
if in.Output != "" && in.Output != "json" {
return fmt.Errorf("unsupported --output value: use 'json'")
}
if err := validateStartURLFlag(in.StartURL); err != nil {
return err
}

params := kernel.BrowserPoolNewParams{
Size: in.Size,
Expand Down Expand Up @@ -131,6 +135,9 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
if in.ProxyID != "" {
params.ProxyID = kernel.String(in.ProxyID)
}
if in.StartURL != "" {
params.StartURL = kernel.String(in.StartURL)
}

params.Extensions = buildExtensionsParam(in.Extensions)

Expand Down Expand Up @@ -196,6 +203,7 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error
{"Kiosk Mode", fmt.Sprintf("%t", cfg.KioskMode)},
{"Profile", formatProfile(cfg.Profile)},
{"Proxy ID", util.OrDash(cfg.ProxyID)},
{"Start URL", util.OrDash(cfg.StartURL)},
{"Extensions", formatExtensions(cfg.Extensions)},
{"Viewport", formatViewport(cfg.Viewport)},
}
Expand All @@ -217,6 +225,8 @@ type BrowserPoolsUpdateInput struct {
ProfileName string
ProfileSaveChanges BoolFlag
ProxyID string
StartURL string
ClearStartURL bool
Extensions []string
Viewport string
DiscardAllIdle BoolFlag
Expand All @@ -227,6 +237,12 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
if in.Output != "" && in.Output != "json" {
return fmt.Errorf("unsupported --output value: use 'json'")
}
if err := validateStartURLFlag(in.StartURL); err != nil {
return err
}
if in.StartURL != "" && in.ClearStartURL {
return fmt.Errorf("cannot specify both --start-url and --clear-start-url")
}

params := kernel.BrowserPoolUpdateParams{}

Expand Down Expand Up @@ -267,6 +283,11 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
if in.ProxyID != "" {
params.ProxyID = kernel.String(in.ProxyID)
}
if in.ClearStartURL {
params.StartURL = kernel.String("")
} else if in.StartURL != "" {
params.StartURL = kernel.String(in.StartURL)
}

params.Extensions = buildExtensionsParam(in.Extensions)

Expand Down Expand Up @@ -352,6 +373,9 @@ func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInpu
{"CDP WebSocket URL", resp.CdpWsURL},
{"Live View URL", resp.BrowserLiveViewURL},
}
if resp.StartURL != "" {
tableData = append(tableData, []string{"Start URL", resp.StartURL})
}
PrintTableNoPad(tableData, true)
return nil
}
Expand Down Expand Up @@ -472,6 +496,7 @@ func init() {
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().String("start-url", "", "Initial page to open for new browsers")
browserPoolsCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
browserPoolsCreateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")

Expand All @@ -488,6 +513,8 @@ func init() {
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().String("start-url", "", "Initial page to open for new browsers")
browserPoolsUpdateCmd.Flags().Bool("clear-start-url", false, "Clear the pool start URL")
browserPoolsUpdateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers")
Expand Down Expand Up @@ -539,6 +566,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
profileName, _ := cmd.Flags().GetString("profile-name")
saveChanges, _ := cmd.Flags().GetBool("save-changes")
proxyID, _ := cmd.Flags().GetString("proxy-id")
startURL, _ := cmd.Flags().GetString("start-url")
extensions, _ := cmd.Flags().GetStringSlice("extension")
viewport, _ := cmd.Flags().GetString("viewport")
output, _ := cmd.Flags().GetString("output")
Expand All @@ -555,6 +583,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
ProfileName: profileName,
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
ProxyID: proxyID,
StartURL: startURL,
Extensions: extensions,
Viewport: viewport,
Output: output,
Expand Down Expand Up @@ -585,6 +614,8 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
profileName, _ := cmd.Flags().GetString("profile-name")
saveChanges, _ := cmd.Flags().GetBool("save-changes")
proxyID, _ := cmd.Flags().GetString("proxy-id")
startURL, _ := cmd.Flags().GetString("start-url")
clearStartURL, _ := cmd.Flags().GetBool("clear-start-url")
extensions, _ := cmd.Flags().GetStringSlice("extension")
viewport, _ := cmd.Flags().GetString("viewport")
discardIdle, _ := cmd.Flags().GetBool("discard-all-idle")
Expand All @@ -603,6 +634,8 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
ProfileName: profileName,
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
ProxyID: proxyID,
StartURL: startURL,
ClearStartURL: clearStartURL,
Extensions: extensions,
Viewport: viewport,
DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle},
Expand Down Expand Up @@ -665,6 +698,13 @@ func buildProfileParam(profileID, profileName string, saveChanges BoolFlag) (*ke
return &profile, nil
}

func validateStartURLFlag(startURL string) error {
if strings.HasPrefix(startURL, "-") {
return fmt.Errorf("--start-url requires a URL value")
}
return nil
}

func buildExtensionsParam(extensions []string) []kernel.BrowserExtensionParam {
if len(extensions) == 0 {
return nil
Expand Down
24 changes: 19 additions & 5 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ type BrowsersCreateInput struct {
ProfileName string
ProfileSaveChanges BoolFlag
ProxyID string
StartURL string
Extensions []string
Viewport string
Output string
Expand Down Expand Up @@ -346,6 +347,9 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
if in.Output != "" && in.Output != "json" {
return fmt.Errorf("unsupported --output value: use 'json'")
}
if err := validateStartURLFlag(in.StartURL); err != nil {
return err
}

if in.Output != "json" {
pterm.Info.Println("Creating browser session...")
Expand Down Expand Up @@ -392,6 +396,9 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
if in.ProxyID != "" {
params.ProxyID = kernel.Opt(in.ProxyID)
}
if in.StartURL != "" {
params.StartURL = kernel.Opt(in.StartURL)
}

// Map extensions (IDs or names) into params.Extensions
if len(in.Extensions) > 0 {
Expand Down Expand Up @@ -435,17 +442,17 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
return util.PrintPrettyJSON(browser)
}

printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Persistence, browser.Profile)
printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Persistence, browser.Profile, browser.StartURL)
return nil
}

func printBrowserSessionResult(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile) {
tableData := buildBrowserTableData(sessionID, cdpURL, liveViewURL, persistence, profile)
func printBrowserSessionResult(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile, startURL string) {
tableData := buildBrowserTableData(sessionID, cdpURL, liveViewURL, persistence, profile, startURL)
PrintTableNoPad(tableData, true)
}

// buildBrowserTableData creates a base table with common browser session fields.
func buildBrowserTableData(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile) pterm.TableData {
func buildBrowserTableData(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile, startURL string) pterm.TableData {
tableData := pterm.TableData{
{"Property", "Value"},
{"Session ID", sessionID},
Expand All @@ -464,6 +471,9 @@ func buildBrowserTableData(sessionID, cdpURL, liveViewURL string, persistence ke
}
tableData = append(tableData, []string{"Profile", profVal})
}
if startURL != "" {
tableData = append(tableData, []string{"Start URL", startURL})
}
return tableData
}

Expand Down Expand Up @@ -554,6 +564,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error {
browser.BrowserLiveViewURL,
browser.Persistence,
browser.Profile,
browser.StartURL,
)

// Append additional detailed fields
Expand Down Expand Up @@ -2525,6 +2536,7 @@ func init() {
browsersCreateCmd.Flags().String("profile-name", "", "Profile name to load into the browser session (mutually exclusive with --profile-id)")
browsersCreateCmd.Flags().Bool("save-changes", false, "If set, save changes back to the profile when the session ends")
browsersCreateCmd.Flags().String("proxy-id", "", "Proxy ID to use for the browser session")
browsersCreateCmd.Flags().String("start-url", "", "Initial page to open on launch")
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, 1280x800@60")
browsersCreateCmd.Flags().Bool("viewport-interactive", false, "Interactively select viewport size from list")
Expand Down Expand Up @@ -2597,6 +2609,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
profileName, _ := cmd.Flags().GetString("profile-name")
saveChanges, _ := cmd.Flags().GetBool("save-changes")
proxyID, _ := cmd.Flags().GetString("proxy-id")
startURL, _ := cmd.Flags().GetString("start-url")
extensions, _ := cmd.Flags().GetStringSlice("extension")
viewport, _ := cmd.Flags().GetString("viewport")
viewportInteractive, _ := cmd.Flags().GetBool("viewport-interactive")
Expand Down Expand Up @@ -2676,7 +2689,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
if output == "json" {
return util.PrintPrettyJSON(resp)
}
printBrowserSessionResult(resp.SessionID, resp.CdpWsURL, resp.BrowserLiveViewURL, resp.Persistence, resp.Profile)
printBrowserSessionResult(resp.SessionID, resp.CdpWsURL, resp.BrowserLiveViewURL, resp.Persistence, resp.Profile, resp.StartURL)
return nil
}

Expand Down Expand Up @@ -2709,6 +2722,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
ProfileName: profileName,
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
ProxyID: proxyID,
StartURL: startURL,
Extensions: extensions,
Viewport: viewport,
Output: output,
Expand Down
35 changes: 35 additions & 0 deletions cmd/browsers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,41 @@ func TestBrowsersCreate_WithViewportNoRefreshRate(t *testing.T) {
assert.False(t, captured.Viewport.RefreshRate.Valid())
}

func TestBrowsersCreate_WithStartURL(t *testing.T) {
setupStdoutCapture(t)
var captured kernel.BrowserNewParams
fake := &FakeBrowsersService{NewFunc: func(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (*kernel.BrowserNewResponse, error) {
captured = body
return &kernel.BrowserNewResponse{SessionID: "session123", CdpWsURL: "ws://example"}, nil
}}
b := BrowsersCmd{browsers: fake}

err := b.Create(context.Background(), BrowsersCreateInput{
StartURL: "https://example.com",
})

assert.NoError(t, err)
assert.True(t, captured.StartURL.Valid())
assert.Equal(t, "https://example.com", captured.StartURL.Value)
}

func TestBrowsersCreate_RejectsStartURLFlagToken(t *testing.T) {
called := false
fake := &FakeBrowsersService{NewFunc: func(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (*kernel.BrowserNewResponse, error) {
called = true
return &kernel.BrowserNewResponse{}, nil
}}
b := BrowsersCmd{browsers: fake}

err := b.Create(context.Background(), BrowsersCreateInput{
StartURL: "--headless",
})

require.Error(t, err)
assert.Contains(t, err.Error(), "--start-url requires a URL value")
assert.False(t, called)
}

func TestBrowsersCreate_WithInvalidViewport(t *testing.T) {
setupStdoutCapture(t)
fake := &FakeBrowsersService{}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.52.0
github.com/kernel/kernel-go-sdk v0.53.0
github.com/klauspost/compress v1.18.5
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pterm/pterm v0.12.80
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kernel/kernel-go-sdk v0.52.0 h1:ChRAMo6oMAEmazC610FtcqKFO/cqHzU9v1ECF0MiR8E=
github.com/kernel/kernel-go-sdk v0.52.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/kernel/kernel-go-sdk v0.53.0 h1:XgcuJv3G4a6nr9LYBZ21gLUWvsIDLSG4YhZAngNrqE0=
github.com/kernel/kernel-go-sdk v0.53.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
Expand Down
Loading