Skip to content

Commit 20e1f89

Browse files
committed
Add start URL browser flags
1 parent 8d26914 commit 20e1f89

7 files changed

Lines changed: 190 additions & 10 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ Commands with JSON output support:
208208
- `-s, --stealth` - Launch browser in stealth mode to avoid detection
209209
- `-H, --headless` - Launch browser without GUI access
210210
- `--kiosk` - Launch browser in kiosk mode
211+
- `--start-url <url>` - Initial page to open on launch
211212
- `--pool-id <id>` - Acquire a browser from the specified pool (mutually exclusive with --pool-name; ignores other session flags)
212213
- `--pool-name <name>` - Acquire a browser from the pool name (mutually exclusive with --pool-id; ignores other session flags)
213214
- `--output json`, `-o json` - Output raw JSON object
@@ -242,12 +243,12 @@ Commands with JSON output support:
242243
- `--fill-rate <n>` - Percentage of the pool to fill per minute
243244
- `--timeout <seconds>` - Idle timeout for browsers acquired from the pool
244245
- `--stealth`, `--headless`, `--kiosk` - Default pool configuration
245-
- `--profile-id`, `--profile-name`, `--save-changes`, `--proxy-id`, `--extension`, `--viewport` - Same semantics as `kernel browsers create`
246+
- `--profile-id`, `--profile-name`, `--save-changes`, `--proxy-id`, `--start-url`, `--extension`, `--viewport` - Same semantics as `kernel browsers create`
246247
- `--output json`, `-o json` - Output raw JSON object
247248
- `kernel browser-pools get <id-or-name>` - Get pool details
248249
- `--output json`, `-o json` - Output raw JSON object
249250
- `kernel browser-pools update <id-or-name>` - Update pool configuration
250-
- Same flags as create plus `--discard-all-idle` to discard all idle browsers in the pool and refill at the specified fill rate
251+
- Same flags as create plus `--clear-start-url` and `--discard-all-idle`
251252
- `--output json`, `-o json` - Output raw JSON object
252253
- `kernel browser-pools delete <id-or-name>` - Delete a pool
253254
- `--force` - Force delete even if browsers are leased

cmd/browser_pools.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ type BrowserPoolsCreateInput struct {
8686
ProfileName string
8787
ProfileSaveChanges BoolFlag
8888
ProxyID string
89+
StartURL string
8990
Extensions []string
9091
Viewport string
9192
Output string
@@ -131,6 +132,9 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
131132
if in.ProxyID != "" {
132133
params.ProxyID = kernel.String(in.ProxyID)
133134
}
135+
if in.StartURL != "" {
136+
params.StartURL = kernel.String(in.StartURL)
137+
}
134138

135139
params.Extensions = buildExtensionsParam(in.Extensions)
136140

@@ -196,6 +200,7 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error
196200
{"Kiosk Mode", fmt.Sprintf("%t", cfg.KioskMode)},
197201
{"Profile", formatProfile(cfg.Profile)},
198202
{"Proxy ID", util.OrDash(cfg.ProxyID)},
203+
{"Start URL", util.OrDash(cfg.StartURL)},
199204
{"Extensions", formatExtensions(cfg.Extensions)},
200205
{"Viewport", formatViewport(cfg.Viewport)},
201206
}
@@ -217,6 +222,8 @@ type BrowserPoolsUpdateInput struct {
217222
ProfileName string
218223
ProfileSaveChanges BoolFlag
219224
ProxyID string
225+
StartURL string
226+
ClearStartURL bool
220227
Extensions []string
221228
Viewport string
222229
DiscardAllIdle BoolFlag
@@ -227,6 +234,9 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
227234
if in.Output != "" && in.Output != "json" {
228235
return fmt.Errorf("unsupported --output value: use 'json'")
229236
}
237+
if in.StartURL != "" && in.ClearStartURL {
238+
return fmt.Errorf("cannot specify both --start-url and --clear-start-url")
239+
}
230240

231241
params := kernel.BrowserPoolUpdateParams{}
232242

@@ -267,6 +277,11 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
267277
if in.ProxyID != "" {
268278
params.ProxyID = kernel.String(in.ProxyID)
269279
}
280+
if in.ClearStartURL {
281+
params.StartURL = kernel.String("")
282+
} else if in.StartURL != "" {
283+
params.StartURL = kernel.String(in.StartURL)
284+
}
270285

271286
params.Extensions = buildExtensionsParam(in.Extensions)
272287

@@ -352,6 +367,9 @@ func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInpu
352367
{"CDP WebSocket URL", resp.CdpWsURL},
353368
{"Live View URL", resp.BrowserLiveViewURL},
354369
}
370+
if resp.StartURL != "" {
371+
tableData = append(tableData, []string{"Start URL", resp.StartURL})
372+
}
355373
PrintTableNoPad(tableData, true)
356374
return nil
357375
}
@@ -472,6 +490,7 @@ func init() {
472490
browserPoolsCreateCmd.Flags().String("profile-name", "", "Profile name")
473491
browserPoolsCreateCmd.Flags().Bool("save-changes", false, "Save changes to profile")
474492
browserPoolsCreateCmd.Flags().String("proxy-id", "", "Proxy ID")
493+
browserPoolsCreateCmd.Flags().String("start-url", "", "Initial page to open for new browsers")
475494
browserPoolsCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
476495
browserPoolsCreateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
477496

@@ -488,6 +507,8 @@ func init() {
488507
browserPoolsUpdateCmd.Flags().String("profile-name", "", "Profile name")
489508
browserPoolsUpdateCmd.Flags().Bool("save-changes", false, "Save changes to profile")
490509
browserPoolsUpdateCmd.Flags().String("proxy-id", "", "Proxy ID")
510+
browserPoolsUpdateCmd.Flags().String("start-url", "", "Initial page to open for new browsers")
511+
browserPoolsUpdateCmd.Flags().Bool("clear-start-url", false, "Clear the pool start URL")
491512
browserPoolsUpdateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names")
492513
browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
493514
browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers")
@@ -539,6 +560,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
539560
profileName, _ := cmd.Flags().GetString("profile-name")
540561
saveChanges, _ := cmd.Flags().GetBool("save-changes")
541562
proxyID, _ := cmd.Flags().GetString("proxy-id")
563+
startURL, _ := cmd.Flags().GetString("start-url")
542564
extensions, _ := cmd.Flags().GetStringSlice("extension")
543565
viewport, _ := cmd.Flags().GetString("viewport")
544566
output, _ := cmd.Flags().GetString("output")
@@ -555,6 +577,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
555577
ProfileName: profileName,
556578
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
557579
ProxyID: proxyID,
580+
StartURL: startURL,
558581
Extensions: extensions,
559582
Viewport: viewport,
560583
Output: output,
@@ -585,6 +608,8 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
585608
profileName, _ := cmd.Flags().GetString("profile-name")
586609
saveChanges, _ := cmd.Flags().GetBool("save-changes")
587610
proxyID, _ := cmd.Flags().GetString("proxy-id")
611+
startURL, _ := cmd.Flags().GetString("start-url")
612+
clearStartURL, _ := cmd.Flags().GetBool("clear-start-url")
588613
extensions, _ := cmd.Flags().GetStringSlice("extension")
589614
viewport, _ := cmd.Flags().GetString("viewport")
590615
discardIdle, _ := cmd.Flags().GetBool("discard-all-idle")
@@ -603,6 +628,8 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
603628
ProfileName: profileName,
604629
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
605630
ProxyID: proxyID,
631+
StartURL: startURL,
632+
ClearStartURL: clearStartURL,
606633
Extensions: extensions,
607634
Viewport: viewport,
608635
DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle},

cmd/browser_pools_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/kernel/kernel-go-sdk"
8+
"github.com/kernel/kernel-go-sdk/option"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
type fakeBrowserPoolsService struct {
13+
newFunc func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error)
14+
updateFunc func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error)
15+
}
16+
17+
func (f *fakeBrowserPoolsService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.BrowserPool, error) {
18+
return &[]kernel.BrowserPool{}, nil
19+
}
20+
21+
func (f *fakeBrowserPoolsService) New(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
22+
if f.newFunc != nil {
23+
return f.newFunc(ctx, body, opts...)
24+
}
25+
return &kernel.BrowserPool{ID: "pool123"}, nil
26+
}
27+
28+
func (f *fakeBrowserPoolsService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
29+
return &kernel.BrowserPool{ID: id}, nil
30+
}
31+
32+
func (f *fakeBrowserPoolsService) Update(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
33+
if f.updateFunc != nil {
34+
return f.updateFunc(ctx, id, body, opts...)
35+
}
36+
return &kernel.BrowserPool{ID: id}, nil
37+
}
38+
39+
func (f *fakeBrowserPoolsService) Delete(ctx context.Context, id string, body kernel.BrowserPoolDeleteParams, opts ...option.RequestOption) error {
40+
return nil
41+
}
42+
43+
func (f *fakeBrowserPoolsService) Acquire(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (*kernel.BrowserPoolAcquireResponse, error) {
44+
return &kernel.BrowserPoolAcquireResponse{}, nil
45+
}
46+
47+
func (f *fakeBrowserPoolsService) Release(ctx context.Context, id string, body kernel.BrowserPoolReleaseParams, opts ...option.RequestOption) error {
48+
return nil
49+
}
50+
51+
func (f *fakeBrowserPoolsService) Flush(ctx context.Context, id string, opts ...option.RequestOption) error {
52+
return nil
53+
}
54+
55+
func TestBrowserPoolsCreate_WithStartURL(t *testing.T) {
56+
setupStdoutCapture(t)
57+
var captured kernel.BrowserPoolNewParams
58+
fake := &fakeBrowserPoolsService{newFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
59+
captured = body
60+
return &kernel.BrowserPool{ID: "pool123"}, nil
61+
}}
62+
c := BrowserPoolsCmd{client: fake}
63+
64+
err := c.Create(context.Background(), BrowserPoolsCreateInput{
65+
Size: 1,
66+
StartURL: "https://example.com",
67+
})
68+
69+
assert.NoError(t, err)
70+
assert.True(t, captured.StartURL.Valid())
71+
assert.Equal(t, "https://example.com", captured.StartURL.Value)
72+
}
73+
74+
func TestBrowserPoolsUpdate_WithStartURL(t *testing.T) {
75+
setupStdoutCapture(t)
76+
var captured kernel.BrowserPoolUpdateParams
77+
fake := &fakeBrowserPoolsService{updateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
78+
captured = body
79+
return &kernel.BrowserPool{ID: id}, nil
80+
}}
81+
c := BrowserPoolsCmd{client: fake}
82+
83+
err := c.Update(context.Background(), BrowserPoolsUpdateInput{
84+
IDOrName: "pool123",
85+
StartURL: "https://example.com",
86+
})
87+
88+
assert.NoError(t, err)
89+
assert.True(t, captured.StartURL.Valid())
90+
assert.Equal(t, "https://example.com", captured.StartURL.Value)
91+
}
92+
93+
func TestBrowserPoolsUpdate_ClearStartURL(t *testing.T) {
94+
setupStdoutCapture(t)
95+
var captured kernel.BrowserPoolUpdateParams
96+
fake := &fakeBrowserPoolsService{updateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
97+
captured = body
98+
return &kernel.BrowserPool{ID: id}, nil
99+
}}
100+
c := BrowserPoolsCmd{client: fake}
101+
102+
err := c.Update(context.Background(), BrowserPoolsUpdateInput{
103+
IDOrName: "pool123",
104+
ClearStartURL: true,
105+
})
106+
107+
assert.NoError(t, err)
108+
assert.True(t, captured.StartURL.Valid())
109+
assert.Equal(t, "", captured.StartURL.Value)
110+
}
111+
112+
func TestBrowserPoolsUpdate_StartURLConflict(t *testing.T) {
113+
setupStdoutCapture(t)
114+
c := BrowserPoolsCmd{client: &fakeBrowserPoolsService{}}
115+
116+
err := c.Update(context.Background(), BrowserPoolsUpdateInput{
117+
IDOrName: "pool123",
118+
StartURL: "https://example.com",
119+
ClearStartURL: true,
120+
})
121+
122+
assert.ErrorContains(t, err, "cannot specify both --start-url and --clear-start-url")
123+
}

cmd/browsers.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ type BrowsersCreateInput struct {
186186
ProfileName string
187187
ProfileSaveChanges BoolFlag
188188
ProxyID string
189+
StartURL string
189190
Extensions []string
190191
Viewport string
191192
Output string
@@ -392,6 +393,9 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
392393
if in.ProxyID != "" {
393394
params.ProxyID = kernel.Opt(in.ProxyID)
394395
}
396+
if in.StartURL != "" {
397+
params.StartURL = kernel.Opt(in.StartURL)
398+
}
395399

396400
// Map extensions (IDs or names) into params.Extensions
397401
if len(in.Extensions) > 0 {
@@ -435,17 +439,17 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
435439
return util.PrintPrettyJSON(browser)
436440
}
437441

438-
printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Persistence, browser.Profile)
442+
printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Persistence, browser.Profile, browser.StartURL)
439443
return nil
440444
}
441445

442-
func printBrowserSessionResult(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile) {
443-
tableData := buildBrowserTableData(sessionID, cdpURL, liveViewURL, persistence, profile)
446+
func printBrowserSessionResult(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile, startURL string) {
447+
tableData := buildBrowserTableData(sessionID, cdpURL, liveViewURL, persistence, profile, startURL)
444448
PrintTableNoPad(tableData, true)
445449
}
446450

447451
// buildBrowserTableData creates a base table with common browser session fields.
448-
func buildBrowserTableData(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile) pterm.TableData {
452+
func buildBrowserTableData(sessionID, cdpURL, liveViewURL string, persistence kernel.BrowserPersistence, profile kernel.Profile, startURL string) pterm.TableData {
449453
tableData := pterm.TableData{
450454
{"Property", "Value"},
451455
{"Session ID", sessionID},
@@ -464,6 +468,9 @@ func buildBrowserTableData(sessionID, cdpURL, liveViewURL string, persistence ke
464468
}
465469
tableData = append(tableData, []string{"Profile", profVal})
466470
}
471+
if startURL != "" {
472+
tableData = append(tableData, []string{"Start URL", startURL})
473+
}
467474
return tableData
468475
}
469476

@@ -554,6 +561,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error {
554561
browser.BrowserLiveViewURL,
555562
browser.Persistence,
556563
browser.Profile,
564+
browser.StartURL,
557565
)
558566

559567
// Append additional detailed fields
@@ -2525,6 +2533,7 @@ func init() {
25252533
browsersCreateCmd.Flags().String("profile-name", "", "Profile name to load into the browser session (mutually exclusive with --profile-id)")
25262534
browsersCreateCmd.Flags().Bool("save-changes", false, "If set, save changes back to the profile when the session ends")
25272535
browsersCreateCmd.Flags().String("proxy-id", "", "Proxy ID to use for the browser session")
2536+
browsersCreateCmd.Flags().String("start-url", "", "Initial page to open on launch")
25282537
browsersCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names to load (repeatable; may be passed multiple times or comma-separated)")
25292538
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")
25302539
browsersCreateCmd.Flags().Bool("viewport-interactive", false, "Interactively select viewport size from list")
@@ -2597,6 +2606,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
25972606
profileName, _ := cmd.Flags().GetString("profile-name")
25982607
saveChanges, _ := cmd.Flags().GetBool("save-changes")
25992608
proxyID, _ := cmd.Flags().GetString("proxy-id")
2609+
startURL, _ := cmd.Flags().GetString("start-url")
26002610
extensions, _ := cmd.Flags().GetStringSlice("extension")
26012611
viewport, _ := cmd.Flags().GetString("viewport")
26022612
viewportInteractive, _ := cmd.Flags().GetBool("viewport-interactive")
@@ -2676,7 +2686,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
26762686
if output == "json" {
26772687
return util.PrintPrettyJSON(resp)
26782688
}
2679-
printBrowserSessionResult(resp.SessionID, resp.CdpWsURL, resp.BrowserLiveViewURL, resp.Persistence, resp.Profile)
2689+
printBrowserSessionResult(resp.SessionID, resp.CdpWsURL, resp.BrowserLiveViewURL, resp.Persistence, resp.Profile, resp.StartURL)
26802690
return nil
26812691
}
26822692

@@ -2709,6 +2719,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
27092719
ProfileName: profileName,
27102720
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
27112721
ProxyID: proxyID,
2722+
StartURL: startURL,
27122723
Extensions: extensions,
27132724
Viewport: viewport,
27142725
Output: output,

cmd/browsers_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,24 @@ func TestBrowsersCreate_WithViewportNoRefreshRate(t *testing.T) {
16321632
assert.False(t, captured.Viewport.RefreshRate.Valid())
16331633
}
16341634

1635+
func TestBrowsersCreate_WithStartURL(t *testing.T) {
1636+
setupStdoutCapture(t)
1637+
var captured kernel.BrowserNewParams
1638+
fake := &FakeBrowsersService{NewFunc: func(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (*kernel.BrowserNewResponse, error) {
1639+
captured = body
1640+
return &kernel.BrowserNewResponse{SessionID: "session123", CdpWsURL: "ws://example"}, nil
1641+
}}
1642+
b := BrowsersCmd{browsers: fake}
1643+
1644+
err := b.Create(context.Background(), BrowsersCreateInput{
1645+
StartURL: "https://example.com",
1646+
})
1647+
1648+
assert.NoError(t, err)
1649+
assert.True(t, captured.StartURL.Valid())
1650+
assert.Equal(t, "https://example.com", captured.StartURL.Value)
1651+
}
1652+
16351653
func TestBrowsersCreate_WithInvalidViewport(t *testing.T) {
16361654
setupStdoutCapture(t)
16371655
fake := &FakeBrowsersService{}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
1010
github.com/golang-jwt/jwt/v5 v5.2.2
1111
github.com/joho/godotenv v1.5.1
12-
github.com/kernel/kernel-go-sdk v0.52.0
12+
github.com/kernel/kernel-go-sdk v0.53.0
1313
github.com/klauspost/compress v1.18.5
1414
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
1515
github.com/pterm/pterm v0.12.80

0 commit comments

Comments
 (0)