From a7716d902cedc5f88ba6a3173191f4768d8e9df0 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:35:53 +0000 Subject: [PATCH 1/5] CLI: Update SDK to 9e90177b921114c93e264ca9792537bf2d8de754 and add missing flags Keep the CLI aligned with the latest kernel-go-sdk release while exposing browser process env/TTY options and browser pool chrome policy support that were already present in the SDK. Tested: go test ./cmd/... && go build ./... Tested: kernel browsers process exec --env Tested: kernel browsers process spawn --allocate-tty --cols --rows --env Tested: kernel browser-pools create/update --chrome-policy Made-with: Cursor --- cmd/browser_pools.go | 56 +++++++++++++++++++++++ cmd/browser_pools_test.go | 72 ++++++++++++++++++++++++++++++ cmd/browsers.go | 93 ++++++++++++++++++++++++++++++++++----- cmd/browsers_test.go | 47 ++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 6 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 cmd/browser_pools_test.go diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index fbb63f8..f445ac3 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "fmt" "strings" @@ -86,6 +87,7 @@ type BrowserPoolsCreateInput struct { ProfileName string ProfileSaveChanges BoolFlag ProxyID string + ChromePolicy string Extensions []string Viewport string Output string @@ -131,6 +133,14 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) if in.ProxyID != "" { params.ProxyID = kernel.String(in.ProxyID) } + chromePolicy, err := parseChromePolicy(in.ChromePolicy) + if err != nil { + pterm.Error.Println(err.Error()) + return nil + } + if len(chromePolicy) > 0 { + params.ChromePolicy = chromePolicy + } params.Extensions = buildExtensionsParam(in.Extensions) @@ -196,6 +206,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)}, + {"Chrome Policy", formatChromePolicy(cfg.ChromePolicy)}, {"Extensions", formatExtensions(cfg.Extensions)}, {"Viewport", formatViewport(cfg.Viewport)}, } @@ -217,6 +228,7 @@ type BrowserPoolsUpdateInput struct { ProfileName string ProfileSaveChanges BoolFlag ProxyID string + ChromePolicy string Extensions []string Viewport string DiscardAllIdle BoolFlag @@ -267,6 +279,14 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) if in.ProxyID != "" { params.ProxyID = kernel.String(in.ProxyID) } + chromePolicy, err := parseChromePolicy(in.ChromePolicy) + if err != nil { + pterm.Error.Println(err.Error()) + return nil + } + if len(chromePolicy) > 0 { + params.ChromePolicy = chromePolicy + } params.Extensions = buildExtensionsParam(in.Extensions) @@ -472,6 +492,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("chrome-policy", "", "JSON object of Chrome enterprise policy overrides to apply to all browsers in the pool") browserPoolsCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names") browserPoolsCreateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)") @@ -488,6 +509,7 @@ 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("chrome-policy", "", "JSON object of Chrome enterprise policy overrides to apply to all browsers in the pool") 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") @@ -539,6 +561,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") + chromePolicy, _ := cmd.Flags().GetString("chrome-policy") extensions, _ := cmd.Flags().GetStringSlice("extension") viewport, _ := cmd.Flags().GetString("viewport") output, _ := cmd.Flags().GetString("output") @@ -555,6 +578,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { ProfileName: profileName, ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, ProxyID: proxyID, + ChromePolicy: chromePolicy, Extensions: extensions, Viewport: viewport, Output: output, @@ -585,6 +609,7 @@ 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") + chromePolicy, _ := cmd.Flags().GetString("chrome-policy") extensions, _ := cmd.Flags().GetStringSlice("extension") viewport, _ := cmd.Flags().GetString("viewport") discardIdle, _ := cmd.Flags().GetBool("discard-all-idle") @@ -603,6 +628,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { ProfileName: profileName, ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, ProxyID: proxyID, + ChromePolicy: chromePolicy, Extensions: extensions, Viewport: viewport, DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle}, @@ -687,6 +713,23 @@ func buildExtensionsParam(extensions []string) []kernel.BrowserExtensionParam { return result } +func parseChromePolicy(raw string) (map[string]any, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + + var policy map[string]any + if err := json.Unmarshal([]byte(raw), &policy); err != nil { + return nil, fmt.Errorf("invalid --chrome-policy JSON: %w", err) + } + if policy == nil { + return nil, fmt.Errorf("--chrome-policy must be a JSON object") + } + + return policy, nil +} + func buildViewportParam(viewport string) (*kernel.BrowserViewportParam, error) { if viewport == "" { return nil, nil @@ -735,6 +778,19 @@ func formatExtensions(extensions []kernel.BrowserExtension) string { return util.JoinOrDash(names...) } +func formatChromePolicy(policy map[string]any) string { + if len(policy) == 0 { + return "-" + } + + data, err := json.Marshal(policy) + if err != nil { + return fmt.Sprintf("%v", policy) + } + + return string(data) +} + func formatViewport(viewport kernel.BrowserViewport) string { if viewport.Width == 0 || viewport.Height == 0 { return "-" diff --git a/cmd/browser_pools_test.go b/cmd/browser_pools_test.go new file mode 100644 index 0000000..826ba65 --- /dev/null +++ b/cmd/browser_pools_test.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "context" + "testing" + + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/stretchr/testify/assert" +) + +type fakeBrowserPoolsService struct { + newFunc func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) +} + +func (f *fakeBrowserPoolsService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.BrowserPool, error) { + return &[]kernel.BrowserPool{}, nil +} + +func (f *fakeBrowserPoolsService) New(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + if f.newFunc != nil { + return f.newFunc(ctx, body, opts...) + } + return &kernel.BrowserPool{}, nil +} + +func (f *fakeBrowserPoolsService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + return &kernel.BrowserPool{}, nil +} + +func (f *fakeBrowserPoolsService) Update(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + return &kernel.BrowserPool{}, nil +} + +func (f *fakeBrowserPoolsService) Delete(ctx context.Context, id string, body kernel.BrowserPoolDeleteParams, opts ...option.RequestOption) error { + return nil +} + +func (f *fakeBrowserPoolsService) Acquire(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (*kernel.BrowserPoolAcquireResponse, error) { + return &kernel.BrowserPoolAcquireResponse{}, nil +} + +func (f *fakeBrowserPoolsService) Release(ctx context.Context, id string, body kernel.BrowserPoolReleaseParams, opts ...option.RequestOption) error { + return nil +} + +func (f *fakeBrowserPoolsService) Flush(ctx context.Context, id string, opts ...option.RequestOption) error { + return nil +} + +func TestBrowserPoolsCreate_MapsChromePolicy(t *testing.T) { + setupStdoutCapture(t) + + fake := &fakeBrowserPoolsService{ + newFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + assert.Equal(t, map[string]any{ + "HomepageLocation": "https://example.com", + "ShowHomeButton": true, + }, body.ChromePolicy) + return &kernel.BrowserPool{ID: "pool_123", Name: "test-pool"}, nil + }, + } + + cmd := BrowserPoolsCmd{client: fake} + err := cmd.Create(context.Background(), BrowserPoolsCreateInput{ + Name: "test-pool", + Size: 1, + ChromePolicy: `{"HomepageLocation":"https://example.com","ShowHomeButton":true}`, + }) + + assert.NoError(t, err) +} diff --git a/cmd/browsers.go b/cmd/browsers.go index d799667..9103082 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -170,6 +170,23 @@ func parseViewport(viewport string) (width, height, refreshRate int64, err error return w, h, refreshRate, nil } +func parseStringMapFlag(values []string, flagName string) (map[string]string, error) { + if len(values) == 0 { + return nil, nil + } + + parsed := make(map[string]string, len(values)) + for _, pair := range values { + parts := strings.SplitN(pair, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid %s value: %s (expected KEY=value)", flagName, pair) + } + parsed[parts[0]] = parts[1] + } + + return parsed, nil +} + // Inputs for each command type BrowsersCreateInput struct { PersistenceID string @@ -1257,18 +1274,23 @@ type BrowsersProcessExecInput struct { Timeout int AsUser string AsRoot BoolFlag + Env []string Output string } type BrowsersProcessSpawnInput struct { - Identifier string - Command string - Args []string - Cwd string - Timeout int - AsUser string - AsRoot BoolFlag - Output string + Identifier string + Command string + Args []string + Cwd string + Timeout int + AsUser string + AsRoot BoolFlag + AllocateTTY BoolFlag + Cols int64 + Rows int64 + Env []string + Output string } type BrowsersProcessKillInput struct { @@ -1396,6 +1418,13 @@ func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInpu if in.AsRoot.Set { params.AsRoot = kernel.Opt(in.AsRoot.Value) } + env, err := parseStringMapFlag(in.Env, "--env") + if err != nil { + return err + } + if len(env) > 0 { + params.Env = env + } res, err := b.process.Exec(ctx, br.SessionID, params) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -1463,6 +1492,27 @@ func (b BrowsersCmd) ProcessSpawn(ctx context.Context, in BrowsersProcessSpawnIn if in.AsRoot.Set { params.AsRoot = kernel.Opt(in.AsRoot.Value) } + if in.AllocateTTY.Set { + params.AllocateTty = kernel.Opt(in.AllocateTTY.Value) + } + if in.Cols > 0 || in.Rows > 0 { + if !in.AllocateTTY.Set || !in.AllocateTTY.Value { + return fmt.Errorf("--cols and --rows require --allocate-tty") + } + if in.Cols > 0 { + params.Cols = kernel.Opt(in.Cols) + } + if in.Rows > 0 { + params.Rows = kernel.Opt(in.Rows) + } + } + env, err := parseStringMapFlag(in.Env, "--env") + if err != nil { + return err + } + if len(env) > 0 { + params.Env = env + } res, err := b.process.Spawn(ctx, br.SessionID, params) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -2297,6 +2347,7 @@ func init() { procExec.Flags().Int("timeout", 0, "Timeout in seconds") procExec.Flags().String("as-user", "", "Run as user") procExec.Flags().Bool("as-root", false, "Run as root") + procExec.Flags().StringArray("env", nil, "Environment variable in KEY=value format (repeatable)") procExec.Flags().StringP("output", "o", "", "Output format: json for raw API response") procSpawn := &cobra.Command{Use: "spawn [--] [command...]", Short: "Execute a command asynchronously", Args: cobra.MinimumNArgs(1), RunE: runBrowsersProcessSpawn} procSpawn.Flags().String("command", "", "Command to execute (optional; if omitted, trailing args are executed via /bin/bash -c)") @@ -2305,6 +2356,10 @@ func init() { procSpawn.Flags().Int("timeout", 0, "Timeout in seconds") procSpawn.Flags().String("as-user", "", "Run as user") procSpawn.Flags().Bool("as-root", false, "Run as root") + procSpawn.Flags().Bool("allocate-tty", false, "Allocate a pseudo-terminal (PTY) for interactive shells") + procSpawn.Flags().Int64("cols", 0, "Initial terminal columns when --allocate-tty is enabled") + procSpawn.Flags().Int64("rows", 0, "Initial terminal rows when --allocate-tty is enabled") + procSpawn.Flags().StringArray("env", nil, "Environment variable in KEY=value format (repeatable)") procSpawn.Flags().StringP("output", "o", "", "Output format: json for raw API response") procKill := &cobra.Command{Use: "kill ", Short: "Send a signal to a process", Args: cobra.ExactArgs(2), RunE: runBrowsersProcessKill} procKill.Flags().String("signal", "TERM", "Signal to send (TERM, KILL, INT, HUP)") @@ -2806,6 +2861,7 @@ func runBrowsersProcessExec(cmd *cobra.Command, args []string) error { timeout, _ := cmd.Flags().GetInt("timeout") asUser, _ := cmd.Flags().GetString("as-user") asRoot, _ := cmd.Flags().GetBool("as-root") + env, _ := cmd.Flags().GetStringArray("env") if command == "" && len(args) > 1 { // Treat trailing args after identifier as a shell command shellCmd := strings.Join(args[1:], " ") @@ -2814,7 +2870,7 @@ func runBrowsersProcessExec(cmd *cobra.Command, args []string) error { } output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, process: &svc.Process} - return b.ProcessExec(cmd.Context(), BrowsersProcessExecInput{Identifier: args[0], Command: command, Args: argv, Cwd: cwd, Timeout: timeout, AsUser: asUser, AsRoot: BoolFlag{Set: cmd.Flags().Changed("as-root"), Value: asRoot}, Output: output}) + return b.ProcessExec(cmd.Context(), BrowsersProcessExecInput{Identifier: args[0], Command: command, Args: argv, Cwd: cwd, Timeout: timeout, AsUser: asUser, AsRoot: BoolFlag{Set: cmd.Flags().Changed("as-root"), Value: asRoot}, Env: env, Output: output}) } func runBrowsersProcessSpawn(cmd *cobra.Command, args []string) error { @@ -2826,6 +2882,10 @@ func runBrowsersProcessSpawn(cmd *cobra.Command, args []string) error { timeout, _ := cmd.Flags().GetInt("timeout") asUser, _ := cmd.Flags().GetString("as-user") asRoot, _ := cmd.Flags().GetBool("as-root") + allocateTTY, _ := cmd.Flags().GetBool("allocate-tty") + cols, _ := cmd.Flags().GetInt64("cols") + rows, _ := cmd.Flags().GetInt64("rows") + env, _ := cmd.Flags().GetStringArray("env") if command == "" && len(args) > 1 { shellCmd := strings.Join(args[1:], " ") command = "/bin/bash" @@ -2833,7 +2893,20 @@ func runBrowsersProcessSpawn(cmd *cobra.Command, args []string) error { } output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, process: &svc.Process} - return b.ProcessSpawn(cmd.Context(), BrowsersProcessSpawnInput{Identifier: args[0], Command: command, Args: argv, Cwd: cwd, Timeout: timeout, AsUser: asUser, AsRoot: BoolFlag{Set: cmd.Flags().Changed("as-root"), Value: asRoot}, Output: output}) + return b.ProcessSpawn(cmd.Context(), BrowsersProcessSpawnInput{ + Identifier: args[0], + Command: command, + Args: argv, + Cwd: cwd, + Timeout: timeout, + AsUser: asUser, + AsRoot: BoolFlag{Set: cmd.Flags().Changed("as-root"), Value: asRoot}, + AllocateTTY: BoolFlag{Set: cmd.Flags().Changed("allocate-tty"), Value: allocateTTY}, + Cols: cols, + Rows: rows, + Env: env, + Output: output, + }) } func runBrowsersProcessKill(cmd *cobra.Command, args []string) error { diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 2bb2c71..125179c 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -907,6 +907,25 @@ func TestBrowsersProcessExec_PrintsSummary(t *testing.T) { assert.Contains(t, out, "Duration") } +func TestBrowsersProcessExec_MapsEnv(t *testing.T) { + setupStdoutCapture(t) + fake := &FakeProcessService{ + ExecFunc: func(ctx context.Context, id string, body kernel.BrowserProcessExecParams, opts ...option.RequestOption) (*kernel.BrowserProcessExecResponse, error) { + assert.Equal(t, "id", id) + assert.Equal(t, map[string]string{"FOO": "bar", "HELLO": "world"}, body.Env) + return &kernel.BrowserProcessExecResponse{ExitCode: 0, DurationMs: 10}, nil + }, + } + fakeBrowsers := newFakeBrowsersServiceWithSimpleGet() + b := BrowsersCmd{browsers: fakeBrowsers, process: fake} + err := b.ProcessExec(context.Background(), BrowsersProcessExecInput{ + Identifier: "id", + Command: "env", + Env: []string{"FOO=bar", "HELLO=world"}, + }) + assert.NoError(t, err) +} + func TestBrowsersProcessSpawn_PrintsInfo(t *testing.T) { setupStdoutCapture(t) fake := &FakeProcessService{} @@ -918,6 +937,34 @@ func TestBrowsersProcessSpawn_PrintsInfo(t *testing.T) { assert.Contains(t, out, "PID") } +func TestBrowsersProcessSpawn_MapsTTYAndEnv(t *testing.T) { + setupStdoutCapture(t) + fake := &FakeProcessService{ + SpawnFunc: func(ctx context.Context, id string, body kernel.BrowserProcessSpawnParams, opts ...option.RequestOption) (*kernel.BrowserProcessSpawnResponse, error) { + assert.Equal(t, "id", id) + assert.True(t, body.AllocateTty.Valid()) + assert.True(t, body.AllocateTty.Value) + assert.True(t, body.Cols.Valid()) + assert.Equal(t, int64(120), body.Cols.Value) + assert.True(t, body.Rows.Valid()) + assert.Equal(t, int64(40), body.Rows.Value) + assert.Equal(t, map[string]string{"FOO": "bar"}, body.Env) + return &kernel.BrowserProcessSpawnResponse{ProcessID: "proc-1", Pid: 123, StartedAt: time.Now()}, nil + }, + } + fakeBrowsers := newFakeBrowsersServiceWithSimpleGet() + b := BrowsersCmd{browsers: fakeBrowsers, process: fake} + err := b.ProcessSpawn(context.Background(), BrowsersProcessSpawnInput{ + Identifier: "id", + Command: "bash", + AllocateTTY: BoolFlag{Set: true, Value: true}, + Cols: 120, + Rows: 40, + Env: []string{"FOO=bar"}, + }) + assert.NoError(t, err) +} + func TestBrowsersProcessKill_PrintsSuccess(t *testing.T) { setupStdoutCapture(t) fake := &FakeProcessService{} diff --git a/go.mod b/go.mod index bbc599b..b826424 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.44.1-0.20260323174449-5e56fc5d99a6 + github.com/kernel/kernel-go-sdk v0.45.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index 2c777ed..d7c31ef 100644 --- a/go.sum +++ b/go.sum @@ -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.44.1-0.20260323174449-5e56fc5d99a6 h1:RBlGCN3IagI0b+XrWsb5FOUV/18tniuL6oHFAb7MMHE= -github.com/kernel/kernel-go-sdk v0.44.1-0.20260323174449-5e56fc5d99a6/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.45.0 h1:RIFpSDmhAWllo692FZL3Os3TRce5oHvyj8LPfwXce5Y= +github.com/kernel/kernel-go-sdk v0.45.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From 26d22ee28767fcd6d72d33e9a5997b6d041ff450 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:05:05 +0000 Subject: [PATCH 2/5] CLI: Update SDK to 91f2aa6572a40330669e39ec4d40cd0b1ee75812 and add missing flags Align the CLI with the latest kernel-go-sdk by exposing browser default stealth proxy control and the new proxy health check URL parameter. This also updates the CLI dependency to the SDK release that includes these API changes. Tested: go test ./cmd ./cmd/proxies -run 'TestBrowsersUpdate_|TestProxyCheck_' Tested: go build ./... Tested: /tmp/kernel-cli/bin/kernel browsers create --headless --stealth -t 30 -o json Tested: /tmp/kernel-cli/bin/kernel browsers update --disable-default-proxy -o json Tested: /tmp/kernel-cli/bin/kernel proxies create --type datacenter --country US --name -o json Tested: /tmp/kernel-cli/bin/kernel proxies check --url https://example.com -o json Made-with: Cursor --- cmd/browsers.go | 48 ++++++++++++++++++++++---------------- cmd/browsers_test.go | 19 +++++++++++++++ cmd/proxies/check.go | 10 ++++++-- cmd/proxies/check_test.go | 27 ++++++++++++++++++++- cmd/proxies/common_test.go | 6 ++--- cmd/proxies/proxies.go | 3 ++- cmd/proxies/types.go | 3 ++- go.mod | 2 +- go.sum | 4 ++-- 9 files changed, 91 insertions(+), 31 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 9103082..22c3a46 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -221,15 +221,16 @@ type BrowsersGetInput struct { } type BrowsersUpdateInput struct { - Identifier string - ProxyID string - ClearProxy bool - ProfileID string - ProfileName string - ProfileSaveChanges BoolFlag - Viewport string - Force bool - Output string + Identifier string + ProxyID string + ClearProxy bool + DisableDefaultProxy BoolFlag + ProfileID string + ProfileName string + ProfileSaveChanges BoolFlag + Viewport string + Force bool + Output string } // BrowsersCmd is a cobra-independent command handler for browsers operations. @@ -610,7 +611,7 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { return fmt.Errorf("cannot specify both --proxy-id and --clear-proxy") } - hasProxyChange := in.ProxyID != "" || in.ClearProxy + hasProxyChange := in.ProxyID != "" || in.ClearProxy || in.DisableDefaultProxy.Set hasProfileChange := in.ProfileID != "" || in.ProfileName != "" hasViewportChange := in.Viewport != "" @@ -626,7 +627,7 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { // Validate that at least one update option is provided if !hasProxyChange && !hasProfileChange && !hasViewportChange { - return fmt.Errorf("must specify at least one of: --proxy-id, --clear-proxy, --profile-id, --profile-name, or --viewport") + return fmt.Errorf("must specify at least one of: --proxy-id, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, or --viewport") } params := kernel.BrowserUpdateParams{} @@ -637,6 +638,9 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { } else if in.ProxyID != "" { params.ProxyID = kernel.Opt(in.ProxyID) } + if in.DisableDefaultProxy.Set { + params.DisableDefaultProxy = kernel.Opt(in.DisableDefaultProxy.Value) + } // Handle profile changes if hasProfileChange { @@ -2260,6 +2264,7 @@ var browsersUpdateCmd = &cobra.Command{ Supported operations: - Change or remove proxy (--proxy-id or --clear-proxy) + - Disable the default stealth proxy (--disable-default-proxy) - Load a profile into a session that doesn't have one (--profile-id or --profile-name) - Change viewport dimensions (--viewport) - Force viewport resize during active live view or recording (--force with --viewport) @@ -2297,6 +2302,7 @@ func init() { browsersUpdateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") browsersUpdateCmd.Flags().String("proxy-id", "", "ID of the proxy to use for the browser session") browsersUpdateCmd.Flags().Bool("clear-proxy", false, "Remove the proxy from the browser session") + browsersUpdateCmd.Flags().Bool("disable-default-proxy", false, "Disable the default stealth proxy so the browser connects directly; use --disable-default-proxy=false to re-enable it") browsersUpdateCmd.Flags().String("profile-id", "", "Profile ID to load into the browser session (mutually exclusive with --profile-name)") browsersUpdateCmd.Flags().String("profile-name", "", "Profile name to load into the browser session (mutually exclusive with --profile-id)") browsersUpdateCmd.Flags().Bool("save-changes", false, "If set, save changes back to the profile when the session ends") @@ -2781,6 +2787,7 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { out, _ := cmd.Flags().GetString("output") proxyID, _ := cmd.Flags().GetString("proxy-id") clearProxy, _ := cmd.Flags().GetBool("clear-proxy") + disableDefaultProxy, _ := cmd.Flags().GetBool("disable-default-proxy") profileID, _ := cmd.Flags().GetString("profile-id") profileName, _ := cmd.Flags().GetString("profile-name") saveChanges, _ := cmd.Flags().GetBool("save-changes") @@ -2790,15 +2797,16 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { svc := client.Browsers b := BrowsersCmd{browsers: &svc} return b.Update(cmd.Context(), BrowsersUpdateInput{ - Identifier: args[0], - ProxyID: proxyID, - ClearProxy: clearProxy, - ProfileID: profileID, - ProfileName: profileName, - ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, - Viewport: viewport, - Force: force, - Output: out, + Identifier: args[0], + ProxyID: proxyID, + ClearProxy: clearProxy, + DisableDefaultProxy: BoolFlag{Set: cmd.Flags().Changed("disable-default-proxy"), Value: disableDefaultProxy}, + ProfileID: profileID, + ProfileName: profileName, + ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, + Viewport: viewport, + Force: force, + Output: out, }) } diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 125179c..86d4947 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -1491,6 +1491,25 @@ func TestBrowsersUpdate_WithViewportNoForce(t *testing.T) { assert.False(t, captured.Viewport.Force.Valid()) } +func TestBrowsersUpdate_WithDisableDefaultProxy(t *testing.T) { + setupStdoutCapture(t) + var captured kernel.BrowserUpdateParams + fake := &FakeBrowsersService{UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserUpdateParams, opts ...option.RequestOption) (*kernel.BrowserUpdateResponse, error) { + captured = body + return &kernel.BrowserUpdateResponse{SessionID: "session123"}, nil + }} + b := BrowsersCmd{browsers: fake} + + err := b.Update(context.Background(), BrowsersUpdateInput{ + Identifier: "session123", + DisableDefaultProxy: BoolFlag{Set: true, Value: true}, + }) + + assert.NoError(t, err) + assert.True(t, captured.DisableDefaultProxy.Valid()) + assert.True(t, captured.DisableDefaultProxy.Value) +} + func TestBrowsersUpdate_ForceWithoutViewport_Errors(t *testing.T) { setupStdoutCapture(t) fake := &FakeBrowsersService{} diff --git a/cmd/proxies/check.go b/cmd/proxies/check.go index 5f47fae..debf1e1 100644 --- a/cmd/proxies/check.go +++ b/cmd/proxies/check.go @@ -20,7 +20,12 @@ func (p ProxyCmd) Check(ctx context.Context, in ProxyCheckInput) error { pterm.Info.Printf("Running health check on proxy %s...\n", in.ID) } - proxy, err := p.proxies.Check(ctx, in.ID) + params := kernel.ProxyCheckParams{} + if in.URL != "" { + params.URL = kernel.Opt(in.URL) + } + + proxy, err := p.proxies.Check(ctx, in.ID, params) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -154,7 +159,8 @@ func getProxyCheckConfigRows(proxy *kernel.ProxyCheckResponse) [][]string { func runProxiesCheck(cmd *cobra.Command, args []string) error { client := util.GetKernelClient(cmd) output, _ := cmd.Flags().GetString("output") + url, _ := cmd.Flags().GetString("url") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Check(cmd.Context(), ProxyCheckInput{ID: args[0], Output: output}) + return p.Check(cmd.Context(), ProxyCheckInput{ID: args[0], URL: url, Output: output}) } diff --git a/cmd/proxies/check_test.go b/cmd/proxies/check_test.go index 8f24ecb..82de52f 100644 --- a/cmd/proxies/check_test.go +++ b/cmd/proxies/check_test.go @@ -13,7 +13,7 @@ func TestProxyCheck_ShowsBypassHosts(t *testing.T) { buf := captureOutput(t) fake := &FakeProxyService{ - CheckFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { + CheckFunc: func(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { return &kernel.ProxyCheckResponse{ ID: id, Name: "Proxy 1", @@ -34,3 +34,28 @@ func TestProxyCheck_ShowsBypassHosts(t *testing.T) { assert.Contains(t, output, "internal.service.local") assert.Contains(t, output, "Proxy health check passed") } + +func TestProxyCheck_PassesURL(t *testing.T) { + buf := captureOutput(t) + var captured kernel.ProxyCheckParams + + fake := &FakeProxyService{ + CheckFunc: func(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { + captured = body + return &kernel.ProxyCheckResponse{ + ID: id, + Name: "Proxy 1", + Type: kernel.ProxyCheckResponseTypeDatacenter, + Status: kernel.ProxyCheckResponseStatusAvailable, + }, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Check(context.Background(), ProxyCheckInput{ID: "proxy-1", URL: "https://example.com"}) + + assert.NoError(t, err) + assert.True(t, captured.URL.Valid()) + assert.Equal(t, "https://example.com", captured.URL.Value) + assert.Contains(t, buf.String(), "Proxy health check passed") +} diff --git a/cmd/proxies/common_test.go b/cmd/proxies/common_test.go index 48f13cf..df49b76 100644 --- a/cmd/proxies/common_test.go +++ b/cmd/proxies/common_test.go @@ -41,7 +41,7 @@ type FakeProxyService struct { GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyGetResponse, error) NewFunc func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error - CheckFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) + CheckFunc func(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) } func (f *FakeProxyService) List(ctx context.Context, opts ...option.RequestOption) (*[]kernel.ProxyListResponse, error) { @@ -73,9 +73,9 @@ func (f *FakeProxyService) Delete(ctx context.Context, id string, opts ...option return nil } -func (f *FakeProxyService) Check(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { +func (f *FakeProxyService) Check(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (*kernel.ProxyCheckResponse, error) { if f.CheckFunc != nil { - return f.CheckFunc(ctx, id, opts...) + return f.CheckFunc(ctx, id, body, opts...) } return &kernel.ProxyCheckResponse{ID: id, Type: kernel.ProxyCheckResponseTypeDatacenter}, nil } diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index 2440d3a..3a93843 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -66,7 +66,7 @@ var proxiesDeleteCmd = &cobra.Command{ var proxiesCheckCmd = &cobra.Command{ Use: "check ", Short: "Run a health check on a proxy", - Long: "Run a health check on a proxy to verify it's working and update its status.", + Long: "Run a health check on a proxy to verify it's working and update its status. Optionally test against a specific public URL.", Args: cobra.ExactArgs(1), RunE: runProxiesCheck, } @@ -115,4 +115,5 @@ func init() { // Check flags proxiesCheckCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + proxiesCheckCmd.Flags().String("url", "", "Optional public HTTP or HTTPS URL to test reachability against") } diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go index c8e7a38..eb0220e 100644 --- a/cmd/proxies/types.go +++ b/cmd/proxies/types.go @@ -13,7 +13,7 @@ type ProxyService interface { Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyGetResponse, err error) New(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (res *kernel.ProxyNewResponse, err error) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) - Check(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProxyCheckResponse, err error) + Check(ctx context.Context, id string, body kernel.ProxyCheckParams, opts ...option.RequestOption) (res *kernel.ProxyCheckResponse, err error) } // ProxyCmd handles proxy operations independent of cobra. @@ -62,5 +62,6 @@ type ProxyDeleteInput struct { type ProxyCheckInput struct { ID string + URL string Output string } diff --git a/go.mod b/go.mod index b826424..ec1bab5 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.45.0 + github.com/kernel/kernel-go-sdk v0.46.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index d7c31ef..431809d 100644 --- a/go.sum +++ b/go.sum @@ -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.45.0 h1:RIFpSDmhAWllo692FZL3Os3TRce5oHvyj8LPfwXce5Y= -github.com/kernel/kernel-go-sdk v0.45.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.46.0 h1:S6OICIzyc6zTy1UdDgWFe9FFOsyJAcPaewKR/U0ZSHA= +github.com/kernel/kernel-go-sdk v0.46.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From 69570a75ef9ffe1b015a14196dbf22aa81e85fc3 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:55:37 +0000 Subject: [PATCH 3/5] CLI: Update Go SDK to c223294ecc21cee581e9095306b75f069cfd92b8 Bring the CLI onto the latest kernel-go-sdk release so it stays aligned with the updated SDK. A full SDK/CLI coverage audit found no missing commands or flags; tested with `go build ./...`. Made-with: Cursor --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ec1bab5..5cff256 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.46.0 + github.com/kernel/kernel-go-sdk v0.47.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index 431809d..a2db638 100644 --- a/go.sum +++ b/go.sum @@ -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.46.0 h1:S6OICIzyc6zTy1UdDgWFe9FFOsyJAcPaewKR/U0ZSHA= -github.com/kernel/kernel-go-sdk v0.46.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.47.0 h1:4bCalC81XACIybgXBxuRGzX7yZ9QnxABG8LSGni7wF8= +github.com/kernel/kernel-go-sdk v0.47.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From 6d2fab0eaec9ca73bf02fc374816941610186491 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:55:18 +0000 Subject: [PATCH 4/5] CLI: Update SDK to 82b88d8f8050949f53eee233bfb1b67d6f9fe49e and add project commands Add CLI coverage for the hidden-but-supported project and project-limit endpoints while bumping the Go SDK to the latest release containing this revision. Tested: go test ./cmd/..., go build ./..., kernel projects create/get/update/list/limits get/limits update/delete Made-with: Cursor --- cmd/projects.go | 572 +++++++++++++++++++++++++++++++++++++++++++ cmd/projects_test.go | 138 +++++++++++ cmd/root.go | 1 + go.mod | 2 +- go.sum | 4 +- 5 files changed, 714 insertions(+), 3 deletions(-) create mode 100644 cmd/projects.go create mode 100644 cmd/projects_test.go diff --git a/cmd/projects.go b/cmd/projects.go new file mode 100644 index 0000000..f67ca89 --- /dev/null +++ b/cmd/projects.go @@ -0,0 +1,572 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/kernel/cli/pkg/util" + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/kernel/kernel-go-sdk/packages/pagination" + "github.com/pterm/pterm" + "github.com/samber/lo" + "github.com/spf13/cobra" +) + +// ProjectsService defines the subset of the Kernel SDK project client that we use. +type ProjectsService interface { + New(ctx context.Context, body kernel.ProjectNewParams, opts ...option.RequestOption) (res *kernel.Project, err error) + Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.Project, err error) + Update(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (res *kernel.Project, err error) + List(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[kernel.Project], err error) + Delete(ctx context.Context, id string, opts ...option.RequestOption) error +} + +// ProjectLimitsService defines the subset of the Kernel SDK project limits client that we use. +type ProjectLimitsService interface { + Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ProjectLimits, err error) + Update(ctx context.Context, id string, body kernel.ProjectLimitUpdateParams, opts ...option.RequestOption) (res *kernel.ProjectLimits, err error) +} + +// ProjectsCmd handles project operations independent of cobra. +type ProjectsCmd struct { + projects ProjectsService + limits ProjectLimitsService +} + +type ProjectsListInput struct { + Output string + Page int + PerPage int +} + +type ProjectsGetInput struct { + ID string + Output string +} + +type ProjectsCreateInput struct { + Name string + Output string +} + +type ProjectsUpdateInput struct { + ID string + Name string + Status string + Output string +} + +type ProjectsDeleteInput struct { + ID string + SkipConfirm bool +} + +type ProjectLimitsGetInput struct { + ID string + Output string +} + +type ProjectLimitsUpdateInput struct { + ID string + MaxConcurrentInvocations Int64Flag + MaxConcurrentSessions Int64Flag + MaxPersistentSessions Int64Flag + MaxPooledSessions Int64Flag + Output string +} + +func (p ProjectsCmd) List(ctx context.Context, in ProjectsListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + page := in.Page + perPage := in.PerPage + if page <= 0 { + page = 1 + } + if perPage <= 0 { + perPage = 20 + } + + params := kernel.ProjectListParams{ + Limit: kernel.Opt(int64(perPage + 1)), + Offset: kernel.Opt(int64((page - 1) * perPage)), + } + + result, err := p.projects.List(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + var items []kernel.Project + if result != nil { + items = result.Items + } + + hasMore := len(items) > perPage + if hasMore { + items = items[:perPage] + } + itemsThisPage := len(items) + + if in.Output == "json" { + if len(items) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(items) + } + + if len(items) == 0 { + pterm.Info.Println("No projects found") + return nil + } + + rows := pterm.TableData{{"Project ID", "Name", "Status", "Created At", "Updated At"}} + for _, project := range items { + rows = append(rows, []string{ + project.ID, + project.Name, + string(project.Status), + util.FormatLocal(project.CreatedAt), + util.FormatLocal(project.UpdatedAt), + }) + } + PrintTableNoPad(rows, true) + + pterm.Printf("\nPage: %d Per-page: %d Items this page: %d Has more: %s\n", page, perPage, itemsThisPage, lo.Ternary(hasMore, "yes", "no")) + if hasMore { + pterm.Printf("Next: kernel projects list --page %d --per-page %d\n", page+1, perPage) + } + + return nil +} + +func (p ProjectsCmd) Get(ctx context.Context, in ProjectsGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + project, err := p.projects.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(project) + } + + rows := projectTable(project) + PrintTableNoPad(rows, true) + return nil +} + +func (p ProjectsCmd) Create(ctx context.Context, in ProjectsCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + if in.Name == "" { + return fmt.Errorf("--name is required") + } + + project, err := p.projects.New(ctx, kernel.ProjectNewParams{ + CreateProjectRequest: kernel.CreateProjectRequestParam{Name: in.Name}, + }) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(project) + } + + pterm.Success.Printf("Created project: %s\n", project.ID) + PrintTableNoPad(projectTable(project), true) + return nil +} + +func (p ProjectsCmd) Update(ctx context.Context, in ProjectsUpdateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + params := kernel.ProjectUpdateParams{ + UpdateProjectRequest: kernel.UpdateProjectRequestParam{}, + } + changed := false + + if in.Name != "" { + params.UpdateProjectRequest.Name = kernel.Opt(in.Name) + changed = true + } + + if in.Status != "" { + status, err := parseProjectStatus(in.Status) + if err != nil { + return err + } + params.UpdateProjectRequest.Status = status + changed = true + } + + if !changed { + return fmt.Errorf("at least one of --name or --status is required") + } + + project, err := p.projects.Update(ctx, in.ID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(project) + } + + pterm.Success.Printf("Updated project: %s\n", project.ID) + PrintTableNoPad(projectTable(project), true) + return nil +} + +func (p ProjectsCmd) Delete(ctx context.Context, in ProjectsDeleteInput) error { + if !in.SkipConfirm { + msg := fmt.Sprintf("Are you sure you want to delete project '%s'? The project must be empty and this cannot be undone.", in.ID) + pterm.DefaultInteractiveConfirm.DefaultText = msg + ok, _ := pterm.DefaultInteractiveConfirm.Show() + if !ok { + pterm.Info.Println("Deletion cancelled") + return nil + } + } + + if err := p.projects.Delete(ctx, in.ID); err != nil { + if util.IsNotFound(err) { + pterm.Info.Printf("Project '%s' not found\n", in.ID) + return nil + } + return util.CleanedUpSdkError{Err: err} + } + + pterm.Success.Printf("Deleted project: %s\n", in.ID) + return nil +} + +func (p ProjectsCmd) GetLimits(ctx context.Context, in ProjectLimitsGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + limits, err := p.limits.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(limits) + } + + PrintTableNoPad(projectLimitsTable(limits), true) + return nil +} + +func (p ProjectsCmd) UpdateLimits(ctx context.Context, in ProjectLimitsUpdateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + params := kernel.ProjectLimitUpdateParams{ + UpdateProjectLimitsRequest: kernel.UpdateProjectLimitsRequestParam{}, + } + changed := false + + if in.MaxConcurrentInvocations.Set { + params.UpdateProjectLimitsRequest.MaxConcurrentInvocations = kernel.Opt(in.MaxConcurrentInvocations.Value) + changed = true + } + if in.MaxConcurrentSessions.Set { + params.UpdateProjectLimitsRequest.MaxConcurrentSessions = kernel.Opt(in.MaxConcurrentSessions.Value) + changed = true + } + if in.MaxPersistentSessions.Set { + params.UpdateProjectLimitsRequest.MaxPersistentSessions = kernel.Opt(in.MaxPersistentSessions.Value) + changed = true + } + if in.MaxPooledSessions.Set { + params.UpdateProjectLimitsRequest.MaxPooledSessions = kernel.Opt(in.MaxPooledSessions.Value) + changed = true + } + + if !changed { + return fmt.Errorf("at least one limit flag is required") + } + + limits, err := p.limits.Update(ctx, in.ID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(limits) + } + + pterm.Success.Printf("Updated project limits for: %s\n", in.ID) + PrintTableNoPad(projectLimitsTable(limits), true) + return nil +} + +func parseProjectStatus(status string) (kernel.UpdateProjectRequestStatus, error) { + switch strings.ToLower(strings.TrimSpace(status)) { + case "active": + return kernel.UpdateProjectRequestStatusActive, nil + case "archived": + return kernel.UpdateProjectRequestStatusArchived, nil + default: + return "", fmt.Errorf("invalid --status value: %s (must be 'active' or 'archived')", status) + } +} + +func projectTable(project *kernel.Project) pterm.TableData { + return pterm.TableData{ + {"Property", "Value"}, + {"ID", project.ID}, + {"Name", project.Name}, + {"Status", string(project.Status)}, + {"Created At", util.FormatLocal(project.CreatedAt)}, + {"Updated At", util.FormatLocal(project.UpdatedAt)}, + } +} + +func projectLimitsTable(limits *kernel.ProjectLimits) pterm.TableData { + return pterm.TableData{ + {"Property", "Value"}, + {"Max Concurrent Invocations", formatProjectLimit(limits.MaxConcurrentInvocations)}, + {"Max Concurrent Sessions", formatProjectLimit(limits.MaxConcurrentSessions)}, + {"Max Persistent Sessions", formatProjectLimit(limits.MaxPersistentSessions)}, + {"Max Pooled Sessions", formatProjectLimit(limits.MaxPooledSessions)}, + } +} + +func formatProjectLimit(value int64) string { + if value == 0 { + return "-" + } + return fmt.Sprintf("%d", value) +} + +var projectsCmd = &cobra.Command{ + Use: "projects", + Aliases: []string{"project"}, + Short: "Manage organization projects", + Long: "Commands for managing Kernel projects and project-level resource limits", +} + +var projectsListCmd = &cobra.Command{ + Use: "list", + Short: "List projects", + Args: cobra.NoArgs, + RunE: runProjectsList, +} + +var projectsGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get a project by ID", + Args: cobra.ExactArgs(1), + RunE: runProjectsGet, +} + +var projectsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new project", + Args: cobra.NoArgs, + RunE: runProjectsCreate, +} + +var projectsUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update a project's name or status", + Args: cobra.ExactArgs(1), + RunE: runProjectsUpdate, +} + +var projectsDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a project", + Args: cobra.ExactArgs(1), + RunE: runProjectsDelete, +} + +var projectLimitsCmd = &cobra.Command{ + Use: "limits", + Short: "Manage project resource limits", +} + +var projectLimitsGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get project resource limits", + Args: cobra.ExactArgs(1), + RunE: runProjectLimitsGet, +} + +var projectLimitsUpdateCmd = &cobra.Command{ + Use: "update ", + Short: "Update project resource limits", + Args: cobra.ExactArgs(1), + RunE: runProjectLimitsUpdate, +} + +func init() { + projectsCmd.AddCommand(projectsListCmd) + projectsCmd.AddCommand(projectsGetCmd) + projectsCmd.AddCommand(projectsCreateCmd) + projectsCmd.AddCommand(projectsUpdateCmd) + projectsCmd.AddCommand(projectsDeleteCmd) + projectsCmd.AddCommand(projectLimitsCmd) + + projectLimitsCmd.AddCommand(projectLimitsGetCmd) + projectLimitsCmd.AddCommand(projectLimitsUpdateCmd) + + projectsListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + projectsListCmd.Flags().Int("per-page", 20, "Items per page (default 20)") + projectsListCmd.Flags().Int("page", 1, "Page number (1-based)") + + projectsGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + projectsCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + projectsCreateCmd.Flags().String("name", "", "Project name") + _ = projectsCreateCmd.MarkFlagRequired("name") + + projectsUpdateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + projectsUpdateCmd.Flags().String("name", "", "New project name") + projectsUpdateCmd.Flags().String("status", "", "New project status: active or archived") + + projectsDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + + projectLimitsGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + projectLimitsUpdateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + projectLimitsUpdateCmd.Flags().Int64("max-concurrent-invocations", 0, "Maximum concurrent app invocations for this project; set to 0 to remove the cap") + projectLimitsUpdateCmd.Flags().Int64("max-concurrent-sessions", 0, "Maximum concurrent browser sessions for this project; set to 0 to remove the cap") + projectLimitsUpdateCmd.Flags().Int64("max-persistent-sessions", 0, "Maximum persistent browser sessions for this project; set to 0 to remove the cap") + projectLimitsUpdateCmd.Flags().Int64("max-pooled-sessions", 0, "Maximum pooled browser sessions for this project; set to 0 to remove the cap") +} + +func runProjectsList(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + page, _ := cmd.Flags().GetInt("page") + perPage, _ := cmd.Flags().GetInt("per-page") + + projectSvc := client.Projects + limitSvc := client.Projects.Limits + p := ProjectsCmd{projects: &projectSvc, limits: &limitSvc} + return p.List(cmd.Context(), ProjectsListInput{ + Output: output, + Page: page, + PerPage: perPage, + }) +} + +func runProjectsGet(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + projectSvc := client.Projects + limitSvc := client.Projects.Limits + p := ProjectsCmd{projects: &projectSvc, limits: &limitSvc} + return p.Get(cmd.Context(), ProjectsGetInput{ + ID: args[0], + Output: output, + }) +} + +func runProjectsCreate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + + projectSvc := client.Projects + limitSvc := client.Projects.Limits + p := ProjectsCmd{projects: &projectSvc, limits: &limitSvc} + return p.Create(cmd.Context(), ProjectsCreateInput{ + Name: name, + Output: output, + }) +} + +func runProjectsUpdate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + status, _ := cmd.Flags().GetString("status") + + projectSvc := client.Projects + limitSvc := client.Projects.Limits + p := ProjectsCmd{projects: &projectSvc, limits: &limitSvc} + return p.Update(cmd.Context(), ProjectsUpdateInput{ + ID: args[0], + Name: name, + Status: status, + Output: output, + }) +} + +func runProjectsDelete(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + skipConfirm, _ := cmd.Flags().GetBool("yes") + + projectSvc := client.Projects + limitSvc := client.Projects.Limits + p := ProjectsCmd{projects: &projectSvc, limits: &limitSvc} + return p.Delete(cmd.Context(), ProjectsDeleteInput{ + ID: args[0], + SkipConfirm: skipConfirm, + }) +} + +func runProjectLimitsGet(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + projectSvc := client.Projects + limitSvc := client.Projects.Limits + p := ProjectsCmd{projects: &projectSvc, limits: &limitSvc} + return p.GetLimits(cmd.Context(), ProjectLimitsGetInput{ + ID: args[0], + Output: output, + }) +} + +func runProjectLimitsUpdate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + maxConcurrentInvocations, _ := cmd.Flags().GetInt64("max-concurrent-invocations") + maxConcurrentSessions, _ := cmd.Flags().GetInt64("max-concurrent-sessions") + maxPersistentSessions, _ := cmd.Flags().GetInt64("max-persistent-sessions") + maxPooledSessions, _ := cmd.Flags().GetInt64("max-pooled-sessions") + + projectSvc := client.Projects + limitSvc := client.Projects.Limits + p := ProjectsCmd{projects: &projectSvc, limits: &limitSvc} + return p.UpdateLimits(cmd.Context(), ProjectLimitsUpdateInput{ + ID: args[0], + MaxConcurrentInvocations: Int64Flag{ + Set: cmd.Flags().Changed("max-concurrent-invocations"), + Value: maxConcurrentInvocations, + }, + MaxConcurrentSessions: Int64Flag{ + Set: cmd.Flags().Changed("max-concurrent-sessions"), + Value: maxConcurrentSessions, + }, + MaxPersistentSessions: Int64Flag{ + Set: cmd.Flags().Changed("max-persistent-sessions"), + Value: maxPersistentSessions, + }, + MaxPooledSessions: Int64Flag{ + Set: cmd.Flags().Changed("max-pooled-sessions"), + Value: maxPooledSessions, + }, + Output: output, + }) +} diff --git a/cmd/projects_test.go b/cmd/projects_test.go new file mode 100644 index 0000000..05fd24d --- /dev/null +++ b/cmd/projects_test.go @@ -0,0 +1,138 @@ +package cmd + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/kernel/kernel-go-sdk/packages/pagination" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type FakeProjectsService struct { + NewFunc func(ctx context.Context, body kernel.ProjectNewParams, opts ...option.RequestOption) (*kernel.Project, error) + GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.Project, error) + UpdateFunc func(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) + ListFunc func(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) + DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error +} + +func (f *FakeProjectsService) New(ctx context.Context, body kernel.ProjectNewParams, opts ...option.RequestOption) (*kernel.Project, error) { + if f.NewFunc != nil { + return f.NewFunc(ctx, body, opts...) + } + return &kernel.Project{ID: "proj-new", Name: body.CreateProjectRequest.Name}, nil +} + +func (f *FakeProjectsService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.Project, error) { + if f.GetFunc != nil { + return f.GetFunc(ctx, id, opts...) + } + return &kernel.Project{ID: id, Name: "project", Status: kernel.ProjectStatusActive}, nil +} + +func (f *FakeProjectsService) Update(ctx context.Context, id string, body kernel.ProjectUpdateParams, opts ...option.RequestOption) (*kernel.Project, error) { + if f.UpdateFunc != nil { + return f.UpdateFunc(ctx, id, body, opts...) + } + return &kernel.Project{ID: id, Name: body.UpdateProjectRequest.Name.Value, Status: kernel.ProjectStatusActive}, nil +} + +func (f *FakeProjectsService) List(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) { + if f.ListFunc != nil { + return f.ListFunc(ctx, query, opts...) + } + return &pagination.OffsetPagination[kernel.Project]{Items: []kernel.Project{}}, nil +} + +func (f *FakeProjectsService) Delete(ctx context.Context, id string, opts ...option.RequestOption) error { + if f.DeleteFunc != nil { + return f.DeleteFunc(ctx, id, opts...) + } + return nil +} + +type FakeProjectLimitsService struct { + GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProjectLimits, error) + UpdateFunc func(ctx context.Context, id string, body kernel.ProjectLimitUpdateParams, opts ...option.RequestOption) (*kernel.ProjectLimits, error) +} + +func (f *FakeProjectLimitsService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ProjectLimits, error) { + if f.GetFunc != nil { + return f.GetFunc(ctx, id, opts...) + } + return &kernel.ProjectLimits{}, nil +} + +func (f *FakeProjectLimitsService) Update(ctx context.Context, id string, body kernel.ProjectLimitUpdateParams, opts ...option.RequestOption) (*kernel.ProjectLimits, error) { + if f.UpdateFunc != nil { + return f.UpdateFunc(ctx, id, body, opts...) + } + return &kernel.ProjectLimits{}, nil +} + +func TestProjectsList_HasMore(t *testing.T) { + buf := captureProfilesOutput(t) + created := time.Unix(0, 0) + items := make([]kernel.Project, 3) + for i := range items { + items[i] = kernel.Project{ + ID: fmt.Sprintf("proj-%d", i), + Name: fmt.Sprintf("Project %d", i), + Status: kernel.ProjectStatusActive, + CreatedAt: created, + UpdatedAt: created, + } + } + + fakeProjects := &FakeProjectsService{ + ListFunc: func(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) { + require.True(t, query.Limit.Valid()) + require.True(t, query.Offset.Valid()) + assert.Equal(t, int64(3), query.Limit.Value) + assert.Equal(t, int64(0), query.Offset.Value) + return &pagination.OffsetPagination[kernel.Project]{Items: items}, nil + }, + } + + p := ProjectsCmd{projects: fakeProjects, limits: &FakeProjectLimitsService{}} + err := p.List(context.Background(), ProjectsListInput{Page: 1, PerPage: 2}) + require.NoError(t, err) + + out := buf.String() + assert.Contains(t, out, "proj-0") + assert.Contains(t, out, "proj-1") + assert.NotContains(t, out, "proj-2") + assert.Contains(t, out, "Has more: yes") + assert.Contains(t, out, "Next: kernel projects list --page 2 --per-page 2") +} + +func TestProjectsUpdateLimits_OnlyChangedFields(t *testing.T) { + fakeLimits := &FakeProjectLimitsService{ + UpdateFunc: func(ctx context.Context, id string, body kernel.ProjectLimitUpdateParams, opts ...option.RequestOption) (*kernel.ProjectLimits, error) { + assert.Equal(t, "proj_123", id) + assert.True(t, body.UpdateProjectLimitsRequest.MaxConcurrentInvocations.Valid()) + assert.Equal(t, int64(15), body.UpdateProjectLimitsRequest.MaxConcurrentInvocations.Value) + assert.False(t, body.UpdateProjectLimitsRequest.MaxConcurrentSessions.Valid()) + assert.False(t, body.UpdateProjectLimitsRequest.MaxPersistentSessions.Valid()) + assert.True(t, body.UpdateProjectLimitsRequest.MaxPooledSessions.Valid()) + assert.Equal(t, int64(0), body.UpdateProjectLimitsRequest.MaxPooledSessions.Value) + + return &kernel.ProjectLimits{ + MaxConcurrentInvocations: 15, + }, nil + }, + } + + p := ProjectsCmd{projects: &FakeProjectsService{}, limits: fakeLimits} + err := p.UpdateLimits(context.Background(), ProjectLimitsUpdateInput{ + ID: "proj_123", + MaxConcurrentInvocations: Int64Flag{Set: true, Value: 15}, + MaxPooledSessions: Int64Flag{Set: true, Value: 0}, + }) + require.NoError(t, err) +} diff --git a/cmd/root.go b/cmd/root.go index 8179d8c..b8de57a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -139,6 +139,7 @@ func init() { rootCmd.AddCommand(browsersCmd) rootCmd.AddCommand(browserPoolsCmd) rootCmd.AddCommand(appCmd) + rootCmd.AddCommand(projectsCmd) rootCmd.AddCommand(profilesCmd) rootCmd.AddCommand(proxies.ProxiesCmd) rootCmd.AddCommand(extensionsCmd) diff --git a/go.mod b/go.mod index 5cff256..8da7e41 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.47.0 + github.com/kernel/kernel-go-sdk v0.48.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index a2db638..2b1d6dd 100644 --- a/go.sum +++ b/go.sum @@ -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.47.0 h1:4bCalC81XACIybgXBxuRGzX7yZ9QnxABG8LSGni7wF8= -github.com/kernel/kernel-go-sdk v0.47.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.48.0 h1:XX1VVs8D5q+rBMkZovXmKAQa94w+6oEJzxBLikfPaxw= +github.com/kernel/kernel-go-sdk v0.48.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From fe8d7e7adbbd4b8cac5814333e7499cb951829a7 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:34:33 +0000 Subject: [PATCH 5/5] CLI: Update Go SDK to c04f920b6dc5892ae2e2188f57a9df5373caa5cd A full SDK and CLI coverage enumeration found no missing commands or flags, so this updates the dependency to the v0.49.0 SDK release for commit c04f920b6dc5892ae2e2188f57a9df5373caa5cd. Tested: go build ./... Tested: go build -o /tmp/kernel-cli/bin/kernel ./cmd/kernel Made-with: Cursor --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 8da7e41..4e00a02 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.48.0 + github.com/kernel/kernel-go-sdk v0.49.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 @@ -20,6 +20,7 @@ require ( golang.org/x/crypto v0.47.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.19.0 + golang.org/x/term v0.39.0 ) require ( @@ -55,7 +56,6 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2b1d6dd..27494e3 100644 --- a/go.sum +++ b/go.sum @@ -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.48.0 h1:XX1VVs8D5q+rBMkZovXmKAQa94w+6oEJzxBLikfPaxw= -github.com/kernel/kernel-go-sdk v0.48.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.49.0 h1:vJxqkbvEKhrGOESTWluHvEd3Y8ReyAc9VuHnF1aGu3k= +github.com/kernel/kernel-go-sdk v0.49.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=