Skip to content

Commit 191c318

Browse files
authored
browser profiles CLI commands (#9)
- adds profile options to `browsers create` - adds `profiles <create|get|delete|list|download>` commands --- <!-- mesa-description-start --> ## TL;DR Adds CLI commands to create and manage browser profiles, allowing browser sessions to be saved and reused. ## Why we made these changes To fulfill a feature request (KERNEL-229) for saving and managing browser contexts. This allows users to maintain login states, cookies, and other session data across multiple browser instances, streamlining workflows that require persistent authentication. ## What changed? - **New `profiles` command**: Added a new top-level `profiles` command with subcommands for `create`, `get`, `delete`, `list`, and `download`. - **Browser creation with profiles**: Extended the `browsers create` command with flags (`--profile-id`, `--profile-name`, `--profile-save`) to launch browsers using a specific, persistent profile. - **Consistent Timestamps**: Refactored timestamp formatting across `logs`, `browsers`, and `app` commands to use a new `util.FormatLocal` utility for consistent local time display. - **Testing**: Added a comprehensive test suite for the new `profiles` command functionality. <sup>_Description generated by Mesa. [Update settings](https://app.mesa.dev/onkernel/settings/pull-requests)_</sup> <!-- mesa-description-end -->
1 parent 8629c38 commit 191c318

File tree

10 files changed

+652
-39
lines changed

10 files changed

+652
-39
lines changed

cmd/app.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package cmd
22

33
import (
44
"strings"
5-
"time"
65

6+
"github.com/onkernel/cli/pkg/util"
77
"github.com/onkernel/kernel-go-sdk"
88
"github.com/pterm/pterm"
99
"github.com/samber/lo"
@@ -133,7 +133,7 @@ func runAppHistory(cmd *cobra.Command, args []string) error {
133133
}
134134

135135
for _, dep := range *deployments {
136-
created := dep.CreatedAt.Format(time.RFC3339)
136+
created := util.FormatLocal(dep.CreatedAt)
137137
status := string(dep.Status)
138138

139139
tableData = append(tableData, []string{

cmd/browsers.go

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmd
33
import (
44
"context"
55
"encoding/base64"
6-
"errors"
76
"fmt"
87
"io"
98
"net/http"
@@ -76,10 +75,13 @@ type BoolFlag struct {
7675

7776
// Inputs for each command
7877
type BrowsersCreateInput struct {
79-
PersistenceID string
80-
TimeoutSeconds int
81-
Stealth BoolFlag
82-
Headless BoolFlag
78+
PersistenceID string
79+
TimeoutSeconds int
80+
Stealth BoolFlag
81+
Headless BoolFlag
82+
ProfileID string
83+
ProfileName string
84+
ProfileSaveChanges BoolFlag
8385
}
8486

8587
type BrowsersDeleteInput struct {
@@ -115,7 +117,7 @@ func (b BrowsersCmd) List(ctx context.Context) error {
115117

116118
// Prepare table data
117119
tableData := pterm.TableData{
118-
{"Browser ID", "Created At", "Persistent ID", "CDP WS URL", "Live View URL"},
120+
{"Browser ID", "Created At", "Persistent ID", "Profile", "CDP WS URL", "Live View URL"},
119121
}
120122

121123
for _, browser := range *browsers {
@@ -124,10 +126,18 @@ func (b BrowsersCmd) List(ctx context.Context) error {
124126
persistentID = browser.Persistence.ID
125127
}
126128

129+
profile := "-"
130+
if browser.Profile.Name != "" {
131+
profile = browser.Profile.Name
132+
} else if browser.Profile.ID != "" {
133+
profile = browser.Profile.ID
134+
}
135+
127136
tableData = append(tableData, []string{
128137
browser.SessionID,
129-
browser.CreatedAt.Format("2006-01-02 15:04:05"),
138+
util.FormatLocal(browser.CreatedAt),
130139
persistentID,
140+
profile,
131141
truncateURL(browser.CdpWsURL, 50),
132142
truncateURL(browser.BrowserLiveViewURL, 50),
133143
})
@@ -153,6 +163,21 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
153163
params.Headless = kernel.Opt(in.Headless.Value)
154164
}
155165

166+
// Validate profile selection: at most one of profile-id or profile-name must be provided
167+
if in.ProfileID != "" && in.ProfileName != "" {
168+
pterm.Error.Println("must specify at most one of --profile-id or --profile-name")
169+
return nil
170+
} else if in.ProfileID != "" || in.ProfileName != "" {
171+
params.Profile = kernel.BrowserNewParamsProfile{
172+
SaveChanges: kernel.Opt(in.ProfileSaveChanges.Value),
173+
}
174+
if in.ProfileID != "" {
175+
params.Profile.ID = kernel.Opt(in.ProfileID)
176+
} else if in.ProfileName != "" {
177+
params.Profile.Name = kernel.Opt(in.ProfileName)
178+
}
179+
}
180+
156181
browser, err := b.browsers.New(ctx, params)
157182
if err != nil {
158183
return util.CleanedUpSdkError{Err: err}
@@ -169,23 +194,19 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
169194
if browser.Persistence.ID != "" {
170195
tableData = append(tableData, []string{"Persistent ID", browser.Persistence.ID})
171196
}
197+
if browser.Profile.ID != "" || browser.Profile.Name != "" {
198+
profVal := browser.Profile.Name
199+
if profVal == "" {
200+
profVal = browser.Profile.ID
201+
}
202+
tableData = append(tableData, []string{"Profile", profVal})
203+
}
172204

173205
printTableNoPad(tableData, true)
174206
return nil
175207
}
176208

177209
func (b BrowsersCmd) Delete(ctx context.Context, in BrowsersDeleteInput) error {
178-
isNotFound := func(err error) bool {
179-
if err == nil {
180-
return false
181-
}
182-
var apierr *kernel.Error
183-
if errors.As(err, &apierr) {
184-
return apierr != nil && apierr.StatusCode == http.StatusNotFound
185-
}
186-
return false
187-
}
188-
189210
if !in.SkipConfirm {
190211
browsers, err := b.browsers.List(ctx)
191212
if err != nil {
@@ -225,7 +246,7 @@ func (b BrowsersCmd) Delete(ctx context.Context, in BrowsersDeleteInput) error {
225246
if found.Persistence.ID == in.Identifier {
226247
pterm.Info.Printf("Deleting browser with persistent ID: %s\n", in.Identifier)
227248
err = b.browsers.Delete(ctx, kernel.BrowserDeleteParams{PersistentID: in.Identifier})
228-
if err != nil && !isNotFound(err) {
249+
if err != nil && !util.IsNotFound(err) {
229250
return util.CleanedUpSdkError{Err: err}
230251
}
231252
pterm.Success.Printf("Successfully deleted browser with persistent ID: %s\n", in.Identifier)
@@ -234,7 +255,7 @@ func (b BrowsersCmd) Delete(ctx context.Context, in BrowsersDeleteInput) error {
234255

235256
pterm.Info.Printf("Deleting browser with ID: %s\n", in.Identifier)
236257
err = b.browsers.DeleteByID(ctx, in.Identifier)
237-
if err != nil && !isNotFound(err) {
258+
if err != nil && !util.IsNotFound(err) {
238259
return util.CleanedUpSdkError{Err: err}
239260
}
240261
pterm.Success.Printf("Successfully deleted browser with ID: %s\n", in.Identifier)
@@ -247,14 +268,14 @@ func (b BrowsersCmd) Delete(ctx context.Context, in BrowsersDeleteInput) error {
247268

248269
// Attempt by session ID
249270
if err := b.browsers.DeleteByID(ctx, in.Identifier); err != nil {
250-
if !isNotFound(err) {
271+
if !util.IsNotFound(err) {
251272
nonNotFoundErrors = append(nonNotFoundErrors, err)
252273
}
253274
}
254275

255276
// Attempt by persistent ID
256277
if err := b.browsers.Delete(ctx, kernel.BrowserDeleteParams{PersistentID: in.Identifier}); err != nil {
257-
if !isNotFound(err) {
278+
if !util.IsNotFound(err) {
258279
nonNotFoundErrors = append(nonNotFoundErrors, err)
259280
}
260281
}
@@ -337,7 +358,7 @@ func (b BrowsersCmd) LogsStream(ctx context.Context, in BrowsersLogsStreamInput)
337358
defer stream.Close()
338359
for stream.Next() {
339360
ev := stream.Current()
340-
pterm.Println(fmt.Sprintf("[%s] %s", ev.Timestamp.Format("2006-01-02 15:04:05"), ev.Message))
361+
pterm.Println(fmt.Sprintf("[%s] %s", util.FormatLocal(ev.Timestamp), ev.Message))
341362
}
342363
if err := stream.Err(); err != nil {
343364
return util.CleanedUpSdkError{Err: err}
@@ -386,7 +407,7 @@ func (b BrowsersCmd) ReplaysList(ctx context.Context, in BrowsersReplaysListInpu
386407
}
387408
rows := pterm.TableData{{"Replay ID", "Started At", "Finished At", "View URL"}}
388409
for _, r := range *items {
389-
rows = append(rows, []string{r.ReplayID, r.StartedAt.Format("2006-01-02 15:04:05"), r.FinishedAt.Format("2006-01-02 15:04:05"), truncateURL(r.ReplayViewURL, 60)})
410+
rows = append(rows, []string{r.ReplayID, util.FormatLocal(r.StartedAt), util.FormatLocal(r.FinishedAt), truncateURL(r.ReplayViewURL, 60)})
390411
}
391412
printTableNoPad(rows, true)
392413
return nil
@@ -412,7 +433,7 @@ func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartIn
412433
if err != nil {
413434
return util.CleanedUpSdkError{Err: err}
414435
}
415-
rows := pterm.TableData{{"Property", "Value"}, {"Replay ID", res.ReplayID}, {"View URL", res.ReplayViewURL}, {"Started At", res.StartedAt.Format("2006-01-02 15:04:05")}}
436+
rows := pterm.TableData{{"Property", "Value"}, {"Replay ID", res.ReplayID}, {"View URL", res.ReplayViewURL}, {"Started At", util.FormatLocal(res.StartedAt)}}
416437
printTableNoPad(rows, true)
417438
return nil
418439
}
@@ -597,7 +618,7 @@ func (b BrowsersCmd) ProcessSpawn(ctx context.Context, in BrowsersProcessSpawnIn
597618
if err != nil {
598619
return util.CleanedUpSdkError{Err: err}
599620
}
600-
rows := pterm.TableData{{"Property", "Value"}, {"Process ID", res.ProcessID}, {"PID", fmt.Sprintf("%d", res.Pid)}, {"Started At", res.StartedAt.Format("2006-01-02 15:04:05")}}
621+
rows := pterm.TableData{{"Property", "Value"}, {"Process ID", res.ProcessID}, {"PID", fmt.Sprintf("%d", res.Pid)}, {"Started At", util.FormatLocal(res.StartedAt)}}
601622
printTableNoPad(rows, true)
602623
return nil
603624
}
@@ -900,7 +921,7 @@ func (b BrowsersCmd) FSFileInfo(ctx context.Context, in BrowsersFSFileInfoInput)
900921
if err != nil {
901922
return util.CleanedUpSdkError{Err: err}
902923
}
903-
rows := pterm.TableData{{"Property", "Value"}, {"Path", res.Path}, {"Name", res.Name}, {"Mode", res.Mode}, {"IsDir", fmt.Sprintf("%t", res.IsDir)}, {"SizeBytes", fmt.Sprintf("%d", res.SizeBytes)}, {"ModTime", res.ModTime.Format("2006-01-02 15:04:05")}}
924+
rows := pterm.TableData{{"Property", "Value"}, {"Path", res.Path}, {"Name", res.Name}, {"Mode", res.Mode}, {"IsDir", fmt.Sprintf("%t", res.IsDir)}, {"SizeBytes", fmt.Sprintf("%d", res.SizeBytes)}, {"ModTime", util.FormatLocal(res.ModTime)}}
904925
printTableNoPad(rows, true)
905926
return nil
906927
}
@@ -928,7 +949,7 @@ func (b BrowsersCmd) FSListFiles(ctx context.Context, in BrowsersFSListFilesInpu
928949
}
929950
rows := pterm.TableData{{"Mode", "Size", "ModTime", "Name", "Path"}}
930951
for _, f := range *res {
931-
rows = append(rows, []string{f.Mode, fmt.Sprintf("%d", f.SizeBytes), f.ModTime.Format("2006-01-02 15:04:05"), f.Name, f.Path})
952+
rows = append(rows, []string{f.Mode, fmt.Sprintf("%d", f.SizeBytes), util.FormatLocal(f.ModTime), f.Name, f.Path})
932953
}
933954
printTableNoPad(rows, true)
934955
return nil
@@ -1297,6 +1318,9 @@ func init() {
12971318
browsersCreateCmd.Flags().BoolP("stealth", "s", false, "Launch browser in stealth mode to avoid detection")
12981319
browsersCreateCmd.Flags().BoolP("headless", "H", false, "Launch browser without GUI access")
12991320
browsersCreateCmd.Flags().IntP("timeout", "t", 60, "Timeout in seconds for the browser session")
1321+
browsersCreateCmd.Flags().String("profile-id", "", "Profile ID to load into the browser session (mutually exclusive with --profile-name)")
1322+
browsersCreateCmd.Flags().String("profile-name", "", "Profile name to load into the browser session (mutually exclusive with --profile-id)")
1323+
browsersCreateCmd.Flags().Bool("save-changes", false, "If set, save changes back to the profile when the session ends")
13001324

13011325
// Add flags for delete command
13021326
browsersDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
@@ -1319,12 +1343,18 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
13191343
stealthVal, _ := cmd.Flags().GetBool("stealth")
13201344
headlessVal, _ := cmd.Flags().GetBool("headless")
13211345
timeout, _ := cmd.Flags().GetInt("timeout")
1346+
profileID, _ := cmd.Flags().GetString("profile-id")
1347+
profileName, _ := cmd.Flags().GetString("profile-name")
1348+
saveChanges, _ := cmd.Flags().GetBool("save-changes")
13221349

13231350
in := BrowsersCreateInput{
1324-
PersistenceID: persistenceID,
1325-
TimeoutSeconds: timeout,
1326-
Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealthVal},
1327-
Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal},
1351+
PersistenceID: persistenceID,
1352+
TimeoutSeconds: timeout,
1353+
Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealthVal},
1354+
Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal},
1355+
ProfileID: profileID,
1356+
ProfileName: profileName,
1357+
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
13281358
}
13291359

13301360
svc := client.Browsers

cmd/logs.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"time"
66

7+
"github.com/onkernel/cli/pkg/util"
78
"github.com/onkernel/kernel-go-sdk"
89
"github.com/onkernel/kernel-go-sdk/option"
910
"github.com/pterm/pterm"
@@ -83,7 +84,7 @@ func runLogs(cmd *cobra.Command, args []string) error {
8384
case "log":
8485
logEntry := data.AsLog()
8586
if timestamps {
86-
fmt.Printf("%s %s\n", logEntry.Timestamp.Format(time.RFC3339Nano), logEntry.Message)
87+
fmt.Printf("%s %s\n", util.FormatLocal(logEntry.Timestamp), logEntry.Message)
8788
} else {
8889
fmt.Println(logEntry.Message)
8990
}
@@ -117,7 +118,7 @@ func runLogs(cmd *cobra.Command, args []string) error {
117118
case "log":
118119
logEntry := data.AsLog()
119120
if timestamps {
120-
fmt.Printf("%s %s\n", logEntry.Timestamp.Format(time.RFC3339Nano), logEntry.Message)
121+
fmt.Printf("%s %s\n", util.FormatLocal(logEntry.Timestamp), logEntry.Message)
121122
} else {
122123
fmt.Println(logEntry.Message)
123124
}

0 commit comments

Comments
 (0)