diff --git a/README.md b/README.md index 5adee9c..c55d88b 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,32 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `--no-color` - Disable color output - `--log-level ` - Set log level (trace, debug, info, warn, error, fatal, print) +## JSON Output + +Many commands support JSON output for scripting and automation. Use `--output json` or `-o json` to get machine-readable output: + +```bash +# Get browser session details as JSON +kernel browsers create -o json + +# List apps as JSON +kernel app list -o json + +# Deploy with JSONL streaming output (one JSON object per line) +kernel deploy index.ts -o json +``` + +Commands with JSON output support: +- **Browsers**: `create`, `list`, `get`, `view` +- **Browser Pools**: `create`, `list`, `get`, `update`, `acquire` +- **Profiles**: `create`, `list`, `get` +- **Extensions**: `upload`, `list` +- **Proxies**: `create`, `list`, `get` +- **Apps**: `list`, `history` +- **Deploy**: `deploy` (JSONL streaming), `history` +- **Invoke**: `invoke` (JSONL streaming), `history` +- **Browser Sub-commands**: `replays list/start`, `process exec/spawn`, `fs file-info/list-files` + ### Authentication - `kernel login [--force]` - Login via OAuth 2.0 @@ -134,6 +160,7 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `--force` - Allow overwriting existing version - `--env `, `-e` - Set environment variables (can be used multiple times) - `--env-file ` - Load environment variables from file (can be used multiple times) + - `--output json`, `-o json` - Output JSONL (one JSON object per line for each event) - `kernel deploy logs ` - Stream logs for a deployment @@ -143,6 +170,7 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `kernel deploy history [app_name]` - Show deployment history - `--limit ` - Max deployments to return (default: 100; 0 = all) + - `--output json`, `-o json` - Output raw JSON array ### App Management @@ -152,14 +180,17 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `--payload `, `-p` - JSON payload for the action - `--payload-file `, `-f` - Read JSON payload from a file (use `-` for stdin) - `--sync`, `-s` - Invoke synchronously (timeout after 60s) + - `--output json`, `-o json` - Output JSONL (one JSON object per line for each event) - `kernel app list` - List deployed apps - `--name ` - Filter by app name - `--version ` - Filter by version + - `--output json`, `-o json` - Output raw JSON array - `kernel app history ` - Show deployment history for an app - `--limit ` - Max deployments to return (default: 100; 0 = all) + - `--output json`, `-o json` - Output raw JSON array ### Logs @@ -172,21 +203,26 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). ### Browser Management - `kernel browsers list` - List running browsers + - `--output json`, `-o json` - Output raw JSON array - `kernel browsers create` - Create a new browser session - `-s, --stealth` - Launch browser in stealth mode to avoid detection - `-H, --headless` - Launch browser without GUI access - `--kiosk` - Launch browser in kiosk mode - `--pool-id ` - Acquire a browser from the specified pool (mutually exclusive with --pool-name; ignores other session flags) - `--pool-name ` - Acquire a browser from the pool name (mutually exclusive with --pool-id; ignores other session flags) + - `--output json`, `-o json` - Output raw JSON object - _Note: When a pool is specified, omit other session configuration flags—pool settings determine profile, proxy, viewport, etc._ - `kernel browsers delete ` - Delete a browser - `-y, --yes` - Skip confirmation prompt - `kernel browsers view ` - Get live view URL for a browser + - `--output json`, `-o json` - Output JSON with liveViewUrl +- `kernel browsers get ` - Get detailed browser session info + - `--output json`, `-o json` - Output raw JSON object ### Browser Pools - `kernel browser-pools list` - List browser pools - - `-o, --output json` - Output raw JSON response + - `--output json`, `-o json` - Output raw JSON array - `kernel browser-pools create` - Create a browser pool - `--name ` - Optional unique name for the pool - `--size ` - Number of browsers in the pool (required) @@ -194,14 +230,17 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `--timeout ` - Idle timeout for browsers acquired from the pool - `--stealth`, `--headless`, `--kiosk` - Default pool configuration - `--profile-id`, `--profile-name`, `--save-changes`, `--proxy-id`, `--extension`, `--viewport` - Same semantics as `kernel browsers create` + - `--output json`, `-o json` - Output raw JSON object - `kernel browser-pools get ` - Get pool details - - `-o, --output json` - Output raw JSON response + - `--output json`, `-o json` - Output raw JSON object - `kernel browser-pools update ` - Update pool configuration - Same flags as create plus `--discard-all-idle` to discard all idle browsers in the pool and refill at the specified fill rate + - `--output json`, `-o json` - Output raw JSON object - `kernel browser-pools delete ` - Delete a pool - `--force` - Force delete even if browsers are leased - `kernel browser-pools acquire ` - Acquire a browser from the pool - `--timeout ` - Acquire timeout before returning 204 + - `--output json`, `-o json` - Output raw JSON object - `kernel browser-pools release ` - Release a browser back to the pool - `--session-id ` - Browser session ID to release (required) - `--reuse` - Reuse the browser instance (default: true) @@ -218,12 +257,14 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). ### Browser Replays - `kernel browsers replays list ` - List replays for a browser + - `--output json`, `-o json` - Output raw JSON array - `kernel browsers replays start ` - Start a replay recording - `--framerate ` - Recording framerate (fps) - `--max-duration ` - Maximum duration in seconds + - `--output json`, `-o json` - Output raw JSON object - `kernel browsers replays stop ` - Stop a replay recording - `kernel browsers replays download ` - Download a replay video - - `-o, --output ` - Output file path for the replay video + - `-f, --output-file ` - Output file path for the replay video ### Browser Process Control @@ -234,6 +275,7 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `--timeout ` - Timeout in seconds - `--as-user ` - Run as user - `--as-root` - Run as root + - `--output json`, `-o json` - Output raw JSON object - `kernel browsers process spawn [--] [command...]` - Execute a command asynchronously - `--command ` - Command to execute (optional; if omitted, trailing args are executed via /bin/bash -c) - `--args ` - Command arguments @@ -241,6 +283,7 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `--timeout ` - Timeout in seconds - `--as-user ` - Run as user - `--as-root` - Run as root + - `--output json`, `-o json` - Output raw JSON object - `kernel browsers process kill ` - Send a signal to a process - `--signal ` - Signal to send: TERM, KILL, INT, HUP (default: TERM) - `kernel browsers process status ` - Get process status @@ -262,8 +305,10 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `-o, --output ` - Output zip file path - `kernel browsers fs file-info ` - Get file or directory info - `--path ` - Absolute file or directory path (required) + - `--output json`, `-o json` - Output raw JSON object - `kernel browsers fs list-files ` - List files in a directory - `--path ` - Absolute directory path (required) + - `--output json`, `-o json` - Output raw JSON array - `kernel browsers fs move ` - Move or rename a file or directory - `--src ` - Absolute source path (required) - `--dest ` - Absolute destination path (required) @@ -344,8 +389,10 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). ### Extension Management - `kernel extensions list` - List all uploaded extensions + - `--output json`, `-o json` - Output raw JSON array - `kernel extensions upload ` - Upload an unpacked browser extension directory - `--name ` - Optional unique extension name + - `--output json`, `-o json` - Output raw JSON object - `kernel extensions download ` - Download an extension archive - `--to ` - Output directory (required) - `kernel extensions download-web-store ` - Download an extension from the Chrome Web Store @@ -357,8 +404,11 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). ### Proxy Management - `kernel proxies list` - List proxy configurations + - `--output json`, `-o json` - Output raw JSON array - `kernel proxies get ` - Get a proxy configuration by ID + - `--output json`, `-o json` - Output raw JSON object - `kernel proxies create` - Create a new proxy configuration + - `--output json`, `-o json` - Output raw JSON object - `--name ` - Proxy configuration name - `--type ` - Proxy type: datacenter, isp, residential, mobile, custom (required) diff --git a/cmd/app.go b/cmd/app.go index 80949af..f420b7e 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "fmt" "strings" @@ -44,9 +45,11 @@ func init() { appListCmd.Flags().Int("limit", 20, "Max apps to return (default 20)") appListCmd.Flags().Int("per-page", 20, "Items per page (alias of --limit)") appListCmd.Flags().Int("page", 1, "Page number (1-based)") + appListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") // Limit rows returned for app history (0 = all) appHistoryCmd.Flags().Int("limit", 20, "Max deployments to return (default 20)") + appHistoryCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") } func runAppList(cmd *cobra.Command, args []string) error { @@ -56,6 +59,11 @@ func runAppList(cmd *cobra.Command, args []string) error { lim, _ := cmd.Flags().GetInt("limit") perPage, _ := cmd.Flags().GetInt("per-page") page, _ := cmd.Flags().GetInt("page") + output, _ := cmd.Flags().GetString("output") + + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } // Determine pagination inputs: prefer page/per-page if provided; else map legacy --limit usePager := cmd.Flags().Changed("per-page") || cmd.Flags().Changed("page") @@ -73,7 +81,9 @@ func runAppList(cmd *cobra.Command, args []string) error { page = 1 } - pterm.Debug.Println("Fetching deployed applications...") + if output != "json" { + pterm.Debug.Println("Fetching deployed applications...") + } params := kernel.AppListParams{} if appName != "" { @@ -92,6 +102,19 @@ func runAppList(cmd *cobra.Command, args []string) error { return nil } + if output == "json" { + if apps == nil || len(apps.Items) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(apps.Items, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if apps == nil || len(apps.Items) == 0 { pterm.Info.Println("No applications found") return nil @@ -193,8 +216,15 @@ func runAppHistory(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) appName := args[0] lim, _ := cmd.Flags().GetInt("limit") + output, _ := cmd.Flags().GetString("output") - pterm.Debug.Printf("Fetching deployment history for app '%s'...\n", appName) + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if output != "json" { + pterm.Debug.Printf("Fetching deployment history for app '%s'...\n", appName) + } params := kernel.DeploymentListParams{} if appName != "" { @@ -207,6 +237,19 @@ func runAppHistory(cmd *cobra.Command, args []string) error { return nil } + if output == "json" { + if deployments == nil || len(deployments.Items) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(deployments.Items, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if deployments == nil || len(deployments.Items) == 0 { pterm.Info.Println("No deployments found for this application") return nil diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index 8540002..0d18eab 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -35,8 +35,7 @@ type BrowserPoolsListInput struct { func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) error { if in.Output != "" && in.Output != "json" { - pterm.Error.Println("unsupported --output value: use 'json'") - return nil + return fmt.Errorf("unsupported --output value: use 'json'") } pools, err := c.client.List(ctx) @@ -95,9 +94,14 @@ type BrowserPoolsCreateInput struct { ProxyID string Extensions []string Viewport string + Output string } func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + params := kernel.BrowserPoolNewParams{ Size: in.Size, } @@ -150,6 +154,15 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) return util.CleanedUpSdkError{Err: err} } + if in.Output == "json" { + bs, err := json.MarshalIndent(pool, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if pool.Name != "" { pterm.Success.Printf("Created browser pool %s (%s)\n", pool.Name, pool.ID) } else { @@ -165,8 +178,7 @@ type BrowserPoolsGetInput struct { func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error { if in.Output != "" && in.Output != "json" { - pterm.Error.Println("unsupported --output value: use 'json'") - return nil + return fmt.Errorf("unsupported --output value: use 'json'") } pool, err := c.client.Get(ctx, in.IDOrName) @@ -224,9 +236,14 @@ type BrowserPoolsUpdateInput struct { Extensions []string Viewport string DiscardAllIdle BoolFlag + Output string } func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + params := kernel.BrowserPoolUpdateParams{} if in.Name != "" { @@ -282,6 +299,16 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + bs, err := json.MarshalIndent(pool, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if pool.Name != "" { pterm.Success.Printf("Updated browser pool %s (%s)\n", pool.Name, pool.ID) } else { @@ -311,9 +338,14 @@ func (c BrowserPoolsCmd) Delete(ctx context.Context, in BrowserPoolsDeleteInput) type BrowserPoolsAcquireInput struct { IDOrName string TimeoutSeconds int64 + Output string } func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + params := kernel.BrowserPoolAcquireParams{} if in.TimeoutSeconds > 0 { params.AcquireTimeoutSeconds = kernel.Int(in.TimeoutSeconds) @@ -323,10 +355,23 @@ func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInpu return util.CleanedUpSdkError{Err: err} } if resp == nil { + if in.Output == "json" { + fmt.Println("null") + return nil + } pterm.Warning.Println("Acquire request timed out (no browser available). Retry to continue waiting.") return nil } + if in.Output == "json" { + bs, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + tableData := pterm.TableData{ {"Property", "Value"}, {"Session ID", resp.SessionID}, @@ -435,6 +480,7 @@ var browserPoolsFlushCmd = &cobra.Command{ func init() { browserPoolsListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + browserPoolsCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") browserPoolsCreateCmd.Flags().String("name", "", "Optional unique name for the pool") browserPoolsCreateCmd.Flags().Int64("size", 0, "Number of browsers in the pool") _ = browserPoolsCreateCmd.MarkFlagRequired("size") @@ -466,10 +512,12 @@ func init() { 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") + browserPoolsUpdateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") browserPoolsDeleteCmd.Flags().Bool("force", false, "Force delete even if browsers are leased") browserPoolsAcquireCmd.Flags().Int64("timeout", 0, "Acquire timeout in seconds") + browserPoolsAcquireCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") browserPoolsReleaseCmd.Flags().String("session-id", "", "Browser session ID to release") _ = browserPoolsReleaseCmd.MarkFlagRequired("session-id") @@ -508,6 +556,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { proxyID, _ := cmd.Flags().GetString("proxy-id") extensions, _ := cmd.Flags().GetStringSlice("extension") viewport, _ := cmd.Flags().GetString("viewport") + output, _ := cmd.Flags().GetString("output") in := BrowserPoolsCreateInput{ Name: name, @@ -523,6 +572,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { ProxyID: proxyID, Extensions: extensions, Viewport: viewport, + Output: output, } c := BrowserPoolsCmd{client: &client.BrowserPools} @@ -553,6 +603,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { extensions, _ := cmd.Flags().GetStringSlice("extension") viewport, _ := cmd.Flags().GetString("viewport") discardIdle, _ := cmd.Flags().GetBool("discard-all-idle") + output, _ := cmd.Flags().GetString("output") in := BrowserPoolsUpdateInput{ IDOrName: args[0], @@ -570,6 +621,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { Extensions: extensions, Viewport: viewport, DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle}, + Output: output, } c := BrowserPoolsCmd{client: &client.BrowserPools} @@ -586,8 +638,9 @@ func runBrowserPoolsDelete(cmd *cobra.Command, args []string) error { func runBrowserPoolsAcquire(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) timeout, _ := cmd.Flags().GetInt64("timeout") + output, _ := cmd.Flags().GetString("output") c := BrowserPoolsCmd{client: &client.BrowserPools} - return c.Acquire(cmd.Context(), BrowserPoolsAcquireInput{IDOrName: args[0], TimeoutSeconds: timeout}) + return c.Acquire(cmd.Context(), BrowserPoolsAcquireInput{IDOrName: args[0], TimeoutSeconds: timeout, Output: output}) } func runBrowserPoolsRelease(cmd *cobra.Command, args []string) error { diff --git a/cmd/browsers.go b/cmd/browsers.go index 67ff175..4563269 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -163,6 +163,7 @@ type BrowsersCreateInput struct { ProxyID string Extensions []string Viewport string + Output string } type BrowsersDeleteInput struct { @@ -172,6 +173,7 @@ type BrowsersDeleteInput struct { type BrowsersViewInput struct { Identifier string + Output string } type BrowsersGetInput struct { @@ -199,8 +201,7 @@ type BrowsersListInput struct { func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { if in.Output != "" && in.Output != "json" { - pterm.Error.Println("unsupported --output value: use 'json'") - return nil + return fmt.Errorf("unsupported --output value: use 'json'") } params := kernel.BrowserListParams{} @@ -287,7 +288,13 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { } func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { - pterm.Info.Println("Creating browser session...") + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Output != "json" { + pterm.Info.Println("Creating browser session...") + } params := kernel.BrowserNewParams{} if in.PersistenceID != "" { params.Persistence = kernel.BrowserPersistenceParam{ID: in.PersistenceID} @@ -363,6 +370,15 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { return util.CleanedUpSdkError{Err: err} } + if in.Output == "json" { + bs, err := json.MarshalIndent(browser, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Persistence, browser.Profile) return nil } @@ -456,10 +472,22 @@ func (b BrowsersCmd) Delete(ctx context.Context, in BrowsersDeleteInput) error { } func (b BrowsersCmd) View(ctx context.Context, in BrowsersViewInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + browser, err := b.browsers.Get(ctx, in.Identifier) if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + result := map[string]string{"liveViewUrl": browser.BrowserLiveViewURL} + bs, _ := json.MarshalIndent(result, "", " ") + fmt.Println(string(bs)) + return nil + } + if browser.BrowserLiveViewURL == "" { if browser.Headless { pterm.Warning.Println("This browser is running in headless mode and does not have a live view URL") @@ -475,8 +503,7 @@ func (b BrowsersCmd) View(ctx context.Context, in BrowsersViewInput) error { func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error { if in.Output != "" && in.Output != "json" { - pterm.Error.Println("unsupported --output value: use 'json'") - return nil + return fmt.Errorf("unsupported --output value: use 'json'") } browser, err := b.browsers.Get(ctx, in.Identifier) @@ -855,12 +882,14 @@ func (b BrowsersCmd) ComputerSetCursor(ctx context.Context, in BrowsersComputerS // Replays type BrowsersReplaysListInput struct { Identifier string + Output string } type BrowsersReplaysStartInput struct { Identifier string Framerate int MaxDurationSeconds int + Output string } type BrowsersReplaysStopInput struct { @@ -875,6 +904,10 @@ type BrowsersReplaysDownloadInput struct { } func (b BrowsersCmd) ReplaysList(ctx context.Context, in BrowsersReplaysListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + br, err := b.browsers.Get(ctx, in.Identifier) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -883,6 +916,20 @@ func (b BrowsersCmd) ReplaysList(ctx context.Context, in BrowsersReplaysListInpu if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + if items == nil || len(*items) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(*items, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if items == nil || len(*items) == 0 { pterm.Info.Println("No replays found") return nil @@ -896,6 +943,10 @@ func (b BrowsersCmd) ReplaysList(ctx context.Context, in BrowsersReplaysListInpu } func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + br, err := b.browsers.Get(ctx, in.Identifier) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -911,6 +962,16 @@ func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartIn if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + bs, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + rows := pterm.TableData{{"Property", "Value"}, {"Replay ID", res.ReplayID}, {"View URL", res.ReplayViewURL}, {"Started At", util.FormatLocal(res.StartedAt)}} PrintTableNoPad(rows, true) return nil @@ -967,9 +1028,19 @@ type BrowsersProcessExecInput struct { Timeout int AsUser string AsRoot BoolFlag + Output string } -type BrowsersProcessSpawnInput = BrowsersProcessExecInput +type BrowsersProcessSpawnInput struct { + Identifier string + Command string + Args []string + Cwd string + Timeout int + AsUser string + AsRoot BoolFlag + Output string +} type BrowsersProcessKillInput struct { Identifier string @@ -1043,6 +1114,10 @@ func (b BrowsersCmd) PlaywrightExecute(ctx context.Context, in BrowsersPlaywrigh } func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + if b.process == nil { pterm.Error.Println("process service not available") return nil @@ -1071,6 +1146,16 @@ func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInpu if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + bs, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + rows := pterm.TableData{{"Property", "Value"}, {"Exit Code", fmt.Sprintf("%d", res.ExitCode)}, {"Duration (ms)", fmt.Sprintf("%d", res.DurationMs)}} PrintTableNoPad(rows, true) if res.StdoutB64 != "" { @@ -1101,6 +1186,10 @@ func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInpu } func (b BrowsersCmd) ProcessSpawn(ctx context.Context, in BrowsersProcessSpawnInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + if b.process == nil { pterm.Error.Println("process service not available") return nil @@ -1129,6 +1218,16 @@ func (b BrowsersCmd) ProcessSpawn(ctx context.Context, in BrowsersProcessSpawnIn if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + bs, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + rows := pterm.TableData{{"Property", "Value"}, {"Process ID", res.ProcessID}, {"PID", fmt.Sprintf("%d", res.Pid)}, {"Started At", util.FormatLocal(res.StartedAt)}} PrintTableNoPad(rows, true) return nil @@ -1247,11 +1346,13 @@ type BrowsersFSDownloadDirZipInput struct { type BrowsersFSFileInfoInput struct { Identifier string Path string + Output string } type BrowsersFSListFilesInput struct { Identifier string Path string + Output string } type BrowsersFSMoveInput struct { @@ -1389,6 +1490,10 @@ func (b BrowsersCmd) FSDownloadDirZip(ctx context.Context, in BrowsersFSDownload } func (b BrowsersCmd) FSFileInfo(ctx context.Context, in BrowsersFSFileInfoInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + if b.fs == nil { pterm.Error.Println("fs service not available") return nil @@ -1401,12 +1506,26 @@ func (b BrowsersCmd) FSFileInfo(ctx context.Context, in BrowsersFSFileInfoInput) if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + bs, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + 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)}} PrintTableNoPad(rows, true) return nil } func (b BrowsersCmd) FSListFiles(ctx context.Context, in BrowsersFSListFilesInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + if b.fs == nil { pterm.Error.Println("fs service not available") return nil @@ -1419,6 +1538,20 @@ func (b BrowsersCmd) FSListFiles(ctx context.Context, in BrowsersFSListFilesInpu if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + if res == nil || len(*res) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(*res, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if res == nil || len(*res) == 0 { pterm.Info.Println("No files found") return nil @@ -1755,6 +1888,9 @@ func init() { // get flags browsersGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + // view flags + browsersViewCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + browsersCmd.AddCommand(browsersListCmd) browsersCmd.AddCommand(browsersCreateCmd) browsersCmd.AddCommand(browsersDeleteCmd) @@ -1775,12 +1911,14 @@ func init() { // replays replaysRoot := &cobra.Command{Use: "replays", Short: "Manage browser replays"} replaysList := &cobra.Command{Use: "list ", Short: "List replays for a browser", Args: cobra.ExactArgs(1), RunE: runBrowsersReplaysList} + replaysList.Flags().StringP("output", "o", "", "Output format: json for raw API response") replaysStart := &cobra.Command{Use: "start ", Short: "Start a replay recording", Args: cobra.ExactArgs(1), RunE: runBrowsersReplaysStart} replaysStart.Flags().Int("framerate", 0, "Recording framerate (fps)") replaysStart.Flags().Int("max-duration", 0, "Maximum duration in seconds") + replaysStart.Flags().StringP("output", "o", "", "Output format: json for raw API response") replaysStop := &cobra.Command{Use: "stop ", Short: "Stop a replay recording", Args: cobra.ExactArgs(2), RunE: runBrowsersReplaysStop} replaysDownload := &cobra.Command{Use: "download ", Short: "Download a replay video", Args: cobra.ExactArgs(2), RunE: runBrowsersReplaysDownload} - replaysDownload.Flags().StringP("output", "o", "", "Output file path for the replay video") + replaysDownload.Flags().StringP("output-file", "f", "", "Output file path for the replay video") replaysRoot.AddCommand(replaysList, replaysStart, replaysStop, replaysDownload) browsersCmd.AddCommand(replaysRoot) @@ -1793,6 +1931,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().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)") procSpawn.Flags().StringSlice("args", []string{}, "Command arguments") @@ -1800,6 +1939,7 @@ 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().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)") procStatus := &cobra.Command{Use: "status ", Short: "Get process status", Args: cobra.ExactArgs(2), RunE: runBrowsersProcessStatus} @@ -1829,9 +1969,11 @@ func init() { fsFileInfo := &cobra.Command{Use: "file-info ", Short: "Get file or directory info", Args: cobra.ExactArgs(1), RunE: runBrowsersFSFileInfo} fsFileInfo.Flags().String("path", "", "Absolute file or directory path") _ = fsFileInfo.MarkFlagRequired("path") + fsFileInfo.Flags().StringP("output", "o", "", "Output format: json for raw API response") fsListFiles := &cobra.Command{Use: "list-files ", Short: "List files in a directory", Args: cobra.ExactArgs(1), RunE: runBrowsersFSListFiles} fsListFiles.Flags().String("path", "", "Absolute directory path") _ = fsListFiles.MarkFlagRequired("path") + fsListFiles.Flags().StringP("output", "o", "", "Output format: json for raw API response") fsMove := &cobra.Command{Use: "move ", Short: "Move or rename a file or directory", Args: cobra.ExactArgs(1), RunE: runBrowsersFSMove} fsMove.Flags().String("src", "", "Absolute source path") fsMove.Flags().String("dest", "", "Absolute destination path") @@ -1953,6 +2095,7 @@ func init() { browsersCmd.AddCommand(playwrightRoot) // Add flags for create command + browsersCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") browsersCreateCmd.Flags().StringP("persistent-id", "p", "", "[DEPRECATED] Use --timeout and profiles instead. Unique identifier for browser session persistence") _ = browsersCreateCmd.Flags().MarkDeprecated("persistent-id", "use --timeout (up to 72 hours) and profiles instead") browsersCreateCmd.Flags().BoolP("stealth", "s", false, "Launch browser in stealth mode to avoid detection") @@ -2012,6 +2155,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { viewportInteractive, _ := cmd.Flags().GetBool("viewport-interactive") poolID, _ := cmd.Flags().GetString("pool-id") poolName, _ := cmd.Flags().GetString("pool-name") + output, _ := cmd.Flags().GetString("output") if poolID != "" && poolName != "" { pterm.Error.Println("must specify at most one of --pool-id or --pool-name") @@ -2024,6 +2168,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { "pool-id": true, "pool-name": true, "timeout": true, + "output": true, // Global persistent flags that don't configure browsers "no-color": true, "log-level": true, @@ -2059,7 +2204,9 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { pool = poolName } - pterm.Info.Printf("Acquiring browser from pool %s...\n", pool) + if output != "json" { + pterm.Info.Printf("Acquiring browser from pool %s...\n", pool) + } poolSvc := client.BrowserPools acquireParams := kernel.BrowserPoolAcquireParams{} @@ -2072,9 +2219,21 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { return util.CleanedUpSdkError{Err: err} } if resp == nil { + if output == "json" { + fmt.Println("null") + return nil + } pterm.Error.Println("Acquire request timed out (no browser available). Retry to continue waiting.") return nil } + if output == "json" { + bs, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } printBrowserSessionResult(resp.SessionID, resp.CdpWsURL, resp.BrowserLiveViewURL, resp.Persistence, resp.Profile) return nil } @@ -2108,6 +2267,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { ProxyID: proxyID, Extensions: extensions, Viewport: viewport, + Output: output, } svc := client.Browsers @@ -2132,10 +2292,11 @@ func runBrowsersDelete(cmd *cobra.Command, args []string) error { func runBrowsersView(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") identifier := args[0] - in := BrowsersViewInput{Identifier: identifier} + in := BrowsersViewInput{Identifier: identifier, Output: output} svc := client.Browsers b := BrowsersCmd{browsers: &svc} return b.View(cmd.Context(), in) @@ -2173,8 +2334,9 @@ func runBrowsersLogsStream(cmd *cobra.Command, args []string) error { func runBrowsersReplaysList(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) svc := client.Browsers + output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, replays: &svc.Replays} - return b.ReplaysList(cmd.Context(), BrowsersReplaysListInput{Identifier: args[0]}) + return b.ReplaysList(cmd.Context(), BrowsersReplaysListInput{Identifier: args[0], Output: output}) } func runBrowsersReplaysStart(cmd *cobra.Command, args []string) error { @@ -2182,8 +2344,9 @@ func runBrowsersReplaysStart(cmd *cobra.Command, args []string) error { svc := client.Browsers fr, _ := cmd.Flags().GetInt("framerate") md, _ := cmd.Flags().GetInt("max-duration") + output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, replays: &svc.Replays} - return b.ReplaysStart(cmd.Context(), BrowsersReplaysStartInput{Identifier: args[0], Framerate: fr, MaxDurationSeconds: md}) + return b.ReplaysStart(cmd.Context(), BrowsersReplaysStartInput{Identifier: args[0], Framerate: fr, MaxDurationSeconds: md, Output: output}) } func runBrowsersReplaysStop(cmd *cobra.Command, args []string) error { @@ -2196,7 +2359,7 @@ func runBrowsersReplaysStop(cmd *cobra.Command, args []string) error { func runBrowsersReplaysDownload(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) svc := client.Browsers - out, _ := cmd.Flags().GetString("output") + out, _ := cmd.Flags().GetString("output-file") b := BrowsersCmd{browsers: &svc, replays: &svc.Replays} return b.ReplaysDownload(cmd.Context(), BrowsersReplaysDownloadInput{Identifier: args[0], ReplayID: args[1], Output: out}) } @@ -2216,8 +2379,9 @@ func runBrowsersProcessExec(cmd *cobra.Command, args []string) error { command = "/bin/bash" argv = []string{"-c", shellCmd} } + 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}}) + 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}) } func runBrowsersProcessSpawn(cmd *cobra.Command, args []string) error { @@ -2234,8 +2398,9 @@ func runBrowsersProcessSpawn(cmd *cobra.Command, args []string) error { command = "/bin/bash" argv = []string{"-c", shellCmd} } + 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}}) + 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}) } func runBrowsersProcessKill(cmd *cobra.Command, args []string) error { @@ -2332,16 +2497,18 @@ func runBrowsersFSFileInfo(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) svc := client.Browsers path, _ := cmd.Flags().GetString("path") + output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, fs: &svc.Fs} - return b.FSFileInfo(cmd.Context(), BrowsersFSFileInfoInput{Identifier: args[0], Path: path}) + return b.FSFileInfo(cmd.Context(), BrowsersFSFileInfoInput{Identifier: args[0], Path: path, Output: output}) } func runBrowsersFSListFiles(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) svc := client.Browsers path, _ := cmd.Flags().GetString("path") + output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, fs: &svc.Fs} - return b.FSListFiles(cmd.Context(), BrowsersFSListFilesInput{Identifier: args[0], Path: path}) + return b.FSListFiles(cmd.Context(), BrowsersFSListFilesInput{Identifier: args[0], Path: path, Output: output}) } func runBrowsersFSMove(cmd *cobra.Command, args []string) error { diff --git a/cmd/deploy.go b/cmd/deploy.go index 2928610..ee734ea 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -57,6 +57,7 @@ func init() { deployCmd.Flags().Bool("force", false, "Allow overwrite of an existing version with the same name") deployCmd.Flags().StringArrayP("env", "e", []string{}, "Set environment variables (e.g., KEY=value). May be specified multiple times") deployCmd.Flags().StringArray("env-file", []string{}, "Read environment variables from a file (.env format). May be specified multiple times") + deployCmd.Flags().StringP("output", "o", "", "Output format: json for JSONL streaming output") // Subcommands under deploy deployLogsCmd.Flags().BoolP("follow", "f", false, "Follow logs in real-time (stream continuously)") @@ -67,6 +68,7 @@ func init() { deployHistoryCmd.Flags().Int("limit", 20, "Max deployments to return (default 20)") deployHistoryCmd.Flags().Int("per-page", 20, "Items per page (alias of --limit)") deployHistoryCmd.Flags().Int("page", 1, "Page number (1-based)") + deployHistoryCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") deployCmd.AddCommand(deployHistoryCmd) // Flags for GitHub deploy @@ -92,6 +94,11 @@ func runDeployGithub(cmd *cobra.Command, args []string) error { version, _ := cmd.Flags().GetString("version") force, _ := cmd.Flags().GetBool("force") + output, _ := cmd.Flags().GetString("output") + + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } // Collect env vars similar to runDeploy envPairs, _ := cmd.Flags().GetStringArray("env") @@ -119,7 +126,9 @@ func runDeployGithub(cmd *cobra.Command, args []string) error { // Build the multipart request body directly for source-based deploy - pterm.Info.Println("Deploying from GitHub source...") + if output != "json" { + pterm.Info.Println("Deploying from GitHub source...") + } startTime := time.Now() // Manually POST multipart with a JSON 'source' field to match backend expectations @@ -190,7 +199,7 @@ func runDeployGithub(cmd *cobra.Command, args []string) error { return fmt.Errorf("decode deployment response: %w", err) } - return followDeployment(cmd.Context(), client, depCreated.ID, startTime, + return followDeployment(cmd.Context(), client, depCreated.ID, startTime, output, option.WithBaseURL(baseURL), option.WithHeader("Authorization", "Bearer "+apiKey), option.WithMaxRetries(0), @@ -203,6 +212,12 @@ func runDeploy(cmd *cobra.Command, args []string) (err error) { entrypoint := args[0] version, _ := cmd.Flags().GetString("version") force, _ := cmd.Flags().GetBool("force") + output, _ := cmd.Flags().GetString("output") + + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + if version == "" { version = "latest" } @@ -215,14 +230,21 @@ func runDeploy(cmd *cobra.Command, args []string) (err error) { } sourceDir := filepath.Dir(resolvedEntrypoint) - spinner, _ := pterm.DefaultSpinner.Start("Compressing files...") + var spinner *pterm.SpinnerPrinter + if output != "json" { + spinner, _ = pterm.DefaultSpinner.Start("Compressing files...") + } tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("kernel_%d.zip", time.Now().UnixNano())) logger.Debug("compressing files", logger.Args("sourceDir", sourceDir, "tmpFile", tmpFile)) if err := util.ZipDirectory(sourceDir, tmpFile); err != nil { - spinner.Fail("Failed to compress files") + if spinner != nil { + spinner.Fail("Failed to compress files") + } return err } - spinner.Success("Compressed files") + if spinner != nil { + spinner.Success("Compressed files") + } defer os.Remove(tmpFile) // make io.Reader from tmpFile @@ -259,7 +281,9 @@ func runDeploy(cmd *cobra.Command, args []string) (err error) { } logger.Debug("deploying app", logger.Args("version", version, "force", force, "entrypoint", filepath.Base(resolvedEntrypoint))) - pterm.Info.Println("Deploying...") + if output != "json" { + pterm.Info.Println("Deploying...") + } resp, err := client.Deployments.New(cmd.Context(), kernel.DeploymentNewParams{ File: file, @@ -272,7 +296,7 @@ func runDeploy(cmd *cobra.Command, args []string) (err error) { return util.CleanedUpSdkError{Err: err} } - return followDeployment(cmd.Context(), client, resp.ID, startTime, option.WithMaxRetries(0)) + return followDeployment(cmd.Context(), client, resp.ID, startTime, output, option.WithMaxRetries(0)) } func quoteIfNeeded(s string) string { @@ -359,6 +383,11 @@ func runDeployHistory(cmd *cobra.Command, args []string) error { lim, _ := cmd.Flags().GetInt("limit") perPage, _ := cmd.Flags().GetInt("per-page") page, _ := cmd.Flags().GetInt("page") + output, _ := cmd.Flags().GetString("output") + + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } // Prefer page/per-page when provided; map legacy --limit otherwise usePager := cmd.Flags().Changed("per-page") || cmd.Flags().Changed("page") @@ -390,12 +419,28 @@ func runDeployHistory(cmd *cobra.Command, args []string) error { params.Limit = kernel.Opt(int64(perPage + 1)) params.Offset = kernel.Opt(int64((page - 1) * perPage)) - pterm.Debug.Println("Fetching deployments...") + if output != "json" { + pterm.Debug.Println("Fetching deployments...") + } deployments, err := client.Deployments.List(cmd.Context(), params) if err != nil { pterm.Error.Printf("Failed to list deployments: %v\n", err) return nil } + + if output == "json" { + if deployments == nil || len(deployments.Items) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(deployments.Items, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if deployments == nil || len(deployments.Items) == 0 { pterm.Info.Println("No deployments found") return nil @@ -470,10 +515,38 @@ func runDeployHistory(cmd *cobra.Command, args []string) error { return nil } -func followDeployment(ctx context.Context, client kernel.Client, deploymentID string, startTime time.Time, opts ...option.RequestOption) error { +func followDeployment(ctx context.Context, client kernel.Client, deploymentID string, startTime time.Time, output string, opts ...option.RequestOption) error { stream := client.Deployments.FollowStreaming(ctx, deploymentID, kernel.DeploymentFollowParams{}, opts...) + jsonOutput := output == "json" + for stream.Next() { data := stream.Current() + + if jsonOutput { + // Output each event as a JSON line + bs, err := json.Marshal(data) + if err == nil { + fmt.Println(string(bs)) + } + // Check for terminal states + if data.Event == "deployment_state" { + deploymentState := data.AsDeploymentState() + status := deploymentState.Deployment.Status + if status == string(kernel.DeploymentGetResponseStatusFailed) || + status == string(kernel.DeploymentGetResponseStatusStopped) { + return fmt.Errorf("deployment %s: %s", status, deploymentState.Deployment.StatusReason) + } + if status == string(kernel.DeploymentGetResponseStatusRunning) { + return nil + } + } + if data.Event == "error" { + errorEv := data.AsErrorEvent() + return fmt.Errorf("%s: %s", errorEv.Error.Code, errorEv.Error.Message) + } + continue + } + switch data.Event { case "log": logEv := data.AsLog() @@ -510,9 +583,11 @@ func followDeployment(ctx context.Context, client kernel.Client, deploymentID st } if serr := stream.Err(); serr != nil { - pterm.Error.Println("✖ Stream error") - pterm.Error.Printf("Deployment ID: %s\n", deploymentID) - pterm.Info.Printf("View logs: kernel deploy logs %s --since 1h\n", deploymentID) + if !jsonOutput { + pterm.Error.Println("✖ Stream error") + pterm.Error.Printf("Deployment ID: %s\n", deploymentID) + pterm.Info.Printf("View logs: kernel deploy logs %s --since 1h\n", deploymentID) + } return fmt.Errorf("stream error: %w", serr) } return nil diff --git a/cmd/extensions.go b/cmd/extensions.go index 2c9118d..fc97e90 100644 --- a/cmd/extensions.go +++ b/cmd/extensions.go @@ -3,6 +3,7 @@ package cmd import ( "bytes" "context" + "encoding/json" "fmt" "io" "net/http" @@ -26,7 +27,9 @@ type ExtensionsService interface { Upload(ctx context.Context, body kernel.ExtensionUploadParams, opts ...option.RequestOption) (res *kernel.ExtensionUploadResponse, err error) } -type ExtensionsListInput struct{} +type ExtensionsListInput struct { + Output string +} type ExtensionsDeleteInput struct { Identifier string @@ -45,8 +48,9 @@ type ExtensionsDownloadWebStoreInput struct { } type ExtensionsUploadInput struct { - Dir string - Name string + Dir string + Name string + Output string } // ExtensionsCmd handles extension operations independent of cobra. @@ -54,12 +58,32 @@ type ExtensionsCmd struct { extensions ExtensionsService } -func (e ExtensionsCmd) List(ctx context.Context, _ ExtensionsListInput) error { - pterm.Info.Println("Fetching extensions...") +func (e ExtensionsCmd) List(ctx context.Context, in ExtensionsListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Output != "json" { + pterm.Info.Println("Fetching extensions...") + } items, err := e.extensions.List(ctx) if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + if items == nil || len(*items) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(*items, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if items == nil || len(*items) == 0 { pterm.Info.Println("No extensions found") return nil @@ -259,6 +283,10 @@ func (e ExtensionsCmd) DownloadWebStore(ctx context.Context, in ExtensionsDownlo } func (e ExtensionsCmd) Upload(ctx context.Context, in ExtensionsUploadInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + if in.Dir == "" { return fmt.Errorf("missing directory argument") } @@ -272,7 +300,9 @@ func (e ExtensionsCmd) Upload(ctx context.Context, in ExtensionsUploadInput) err } tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("kernel_ext_%d.zip", time.Now().UnixNano())) - pterm.Info.Println("Zipping extension directory...") + if in.Output != "json" { + pterm.Info.Println("Zipping extension directory...") + } if err := util.ZipDirectory(absDir, tmpFile); err != nil { pterm.Error.Println("Failed to zip directory") return err @@ -294,6 +324,15 @@ func (e ExtensionsCmd) Upload(ctx context.Context, in ExtensionsUploadInput) err return util.CleanedUpSdkError{Err: err} } + if in.Output == "json" { + bs, err := json.MarshalIndent(item, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + name := item.Name if name == "" { name = "-" @@ -322,9 +361,10 @@ var extensionsListCmd = &cobra.Command{ Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") svc := client.Extensions e := ExtensionsCmd{extensions: &svc} - return e.List(cmd.Context(), ExtensionsListInput{}) + return e.List(cmd.Context(), ExtensionsListInput{Output: output}) }, } @@ -375,9 +415,10 @@ var extensionsUploadCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) name, _ := cmd.Flags().GetString("name") + output, _ := cmd.Flags().GetString("output") svc := client.Extensions e := ExtensionsCmd{extensions: &svc} - return e.Upload(cmd.Context(), ExtensionsUploadInput{Dir: args[0], Name: name}) + return e.Upload(cmd.Context(), ExtensionsUploadInput{Dir: args[0], Name: name, Output: output}) }, } @@ -388,9 +429,11 @@ func init() { extensionsCmd.AddCommand(extensionsDownloadWebStoreCmd) extensionsCmd.AddCommand(extensionsUploadCmd) + extensionsListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") extensionsDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") extensionsDownloadCmd.Flags().String("to", "", "Output zip file path") extensionsDownloadWebStoreCmd.Flags().String("to", "", "Output zip file path for the downloaded archive") extensionsDownloadWebStoreCmd.Flags().String("os", "", "Target OS: mac, win, or linux (default linux)") + extensionsUploadCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") extensionsUploadCmd.Flags().String("name", "", "Optional unique extension name") } diff --git a/cmd/invoke.go b/cmd/invoke.go index 744b2c1..a90b16c 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -40,11 +40,13 @@ func init() { invokeCmd.Flags().StringP("payload", "p", "", "JSON payload for the invocation (optional)") invokeCmd.Flags().StringP("payload-file", "f", "", "Path to a JSON file containing the payload (use '-' for stdin)") invokeCmd.Flags().BoolP("sync", "s", false, "Invoke synchronously (default false). A synchronous invocation will open a long-lived HTTP POST to the Kernel API to wait for the invocation to complete. This will time out after 60 seconds, so only use this option if you expect your invocation to complete in less than 60 seconds. The default is to invoke asynchronously, in which case the CLI will open an SSE connection to the Kernel API after submitting the invocation and wait for the invocation to complete.") + invokeCmd.Flags().StringP("output", "o", "", "Output format: json for JSONL streaming output") invokeCmd.MarkFlagsMutuallyExclusive("payload", "payload-file") invocationHistoryCmd.Flags().Int("limit", 100, "Max invocations to return (default 100)") invocationHistoryCmd.Flags().StringP("app", "a", "", "Filter by app name") invocationHistoryCmd.Flags().String("version", "", "Filter by invocation version") + invocationHistoryCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") invokeCmd.AddCommand(invocationHistoryCmd) } @@ -57,6 +59,13 @@ func runInvoke(cmd *cobra.Command, args []string) error { appName := args[0] actionName := args[1] version, _ := cmd.Flags().GetString("version") + output, _ := cmd.Flags().GetString("output") + + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + jsonOutput := output == "json" + if version == "" { return fmt.Errorf("version cannot be an empty string") } @@ -79,15 +88,29 @@ func runInvoke(cmd *cobra.Command, args []string) error { ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) cmd.SetContext(ctx) - pterm.Info.Printf("Invoking \"%s\" (action: %s, version: %s)…\n", appName, actionName, version) + if !jsonOutput { + pterm.Info.Printf("Invoking \"%s\" (action: %s, version: %s)…\n", appName, actionName, version) + } // Create the invocation resp, err := client.Invocations.New(cmd.Context(), params, option.WithMaxRetries(0)) if err != nil { + if jsonOutput { + // In JSON mode, output error as JSON object + errObj := map[string]interface{}{"error": err.Error()} + if apiErr, ok := err.(*kernel.Error); ok { + errObj["status_code"] = apiErr.StatusCode + } + bs, _ := json.Marshal(errObj) + fmt.Println(string(bs)) + return fmt.Errorf("invocation failed: %w", err) + } return handleSdkError(err) } // Log the invocation ID for user reference - pterm.Info.Printfln("Invocation ID: %s", resp.ID) + if !jsonOutput { + pterm.Info.Printfln("Invocation ID: %s", resp.ID) + } // coordinate the cleanup with the polling loop to ensure this is given enough time to run // before this function returns cleanupDone := make(chan struct{}) @@ -99,6 +122,11 @@ func runInvoke(cmd *cobra.Command, args []string) error { }() if resp.Status != kernel.InvocationNewResponseStatusQueued { + if jsonOutput { + bs, _ := json.Marshal(resp) + fmt.Println(string(bs)) + return nil + } succeeded := resp.Status == kernel.InvocationNewResponseStatusSucceeded printResult(succeeded, resp.Output) @@ -116,7 +144,9 @@ func runInvoke(cmd *cobra.Command, args []string) error { once.Do(func() { cleanupStarted.Store(true) defer close(cleanupDone) - pterm.Warning.Println("Invocation cancelled...cleaning up...") + if !jsonOutput { + pterm.Warning.Println("Invocation cancelled...cleaning up...") + } if _, err := client.Invocations.Update( context.Background(), resp.ID, @@ -126,10 +156,14 @@ func runInvoke(cmd *cobra.Command, args []string) error { }, option.WithRequestTimeout(30*time.Second), ); err != nil { - pterm.Error.Printf("Failed to mark invocation as failed: %v\n", err) + if !jsonOutput { + pterm.Error.Printf("Failed to mark invocation as failed: %v\n", err) + } } if err := client.Invocations.DeleteBrowsers(context.Background(), resp.ID, option.WithRequestTimeout(30*time.Second)); err != nil { - pterm.Error.Printf("Failed to cancel invocation: %v\n", err) + if !jsonOutput { + pterm.Error.Printf("Failed to cancel invocation: %v\n", err) + } } }) }) @@ -139,6 +173,30 @@ func runInvoke(cmd *cobra.Command, args []string) error { for stream.Next() { ev := stream.Current() + if jsonOutput { + // Output each event as a JSON line + bs, err := json.Marshal(ev) + if err == nil { + fmt.Println(string(bs)) + } + // Check for terminal states + if ev.Event == "invocation_state" { + stateEv := ev.AsInvocationState() + status := stateEv.Invocation.Status + if status == string(kernel.InvocationGetResponseStatusSucceeded) { + return nil + } + if status == string(kernel.InvocationGetResponseStatusFailed) { + return fmt.Errorf("invocation failed") + } + } + if ev.Event == "error" { + errEv := ev.AsError() + return fmt.Errorf("%s: %s", errEv.Error.Code, errEv.Error.Message) + } + continue + } + switch ev.Event { case "log": logEv := ev.AsLog() @@ -275,6 +333,11 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error { lim, _ := cmd.Flags().GetInt("limit") appFilter, _ := cmd.Flags().GetString("app") versionFilter, _ := cmd.Flags().GetString("version") + output, _ := cmd.Flags().GetString("output") + + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } // Build parameters for the API call params := kernel.InvocationListParams{ @@ -292,14 +355,16 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error { } // Build debug message based on filters - if appFilter != "" && versionFilter != "" { - pterm.Debug.Printf("Listing invocations for app '%s' version '%s'...\n", appFilter, versionFilter) - } else if appFilter != "" { - pterm.Debug.Printf("Listing invocations for app '%s'...\n", appFilter) - } else if versionFilter != "" { - pterm.Debug.Printf("Listing invocations for version '%s'...\n", versionFilter) - } else { - pterm.Debug.Printf("Listing all invocations...\n") + if output != "json" { + if appFilter != "" && versionFilter != "" { + pterm.Debug.Printf("Listing invocations for app '%s' version '%s'...\n", appFilter, versionFilter) + } else if appFilter != "" { + pterm.Debug.Printf("Listing invocations for app '%s'...\n", appFilter) + } else if versionFilter != "" { + pterm.Debug.Printf("Listing invocations for version '%s'...\n", versionFilter) + } else { + pterm.Debug.Printf("Listing all invocations...\n") + } } // Make a single API call to get invocations @@ -309,6 +374,19 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error { return nil } + if output == "json" { + if len(invocations.Items) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(invocations.Items, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + table := pterm.TableData{{"Invocation ID", "App Name", "Action", "Version", "Status", "Started At", "Duration", "Output"}} for _, inv := range invocations.Items { diff --git a/cmd/profiles.go b/cmd/profiles.go index f356377..71049f9 100644 --- a/cmd/profiles.go +++ b/cmd/profiles.go @@ -27,10 +27,16 @@ type ProfilesService interface { type ProfilesGetInput struct { Identifier string + Output string +} + +type ProfilesListInput struct { + Output string } type ProfilesCreateInput struct { - Name string + Name string + Output string } type ProfilesDeleteInput struct { @@ -49,12 +55,32 @@ type ProfilesCmd struct { profiles ProfilesService } -func (p ProfilesCmd) List(ctx context.Context) error { - pterm.Info.Println("Fetching profiles...") +func (p ProfilesCmd) List(ctx context.Context, in ProfilesListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Output != "json" { + pterm.Info.Println("Fetching profiles...") + } items, err := p.profiles.List(ctx) if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + if items == nil || len(*items) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(*items, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if items == nil || len(*items) == 0 { pterm.Info.Println("No profiles found") return nil @@ -78,14 +104,32 @@ func (p ProfilesCmd) List(ctx context.Context) error { } func (p ProfilesCmd) Get(ctx context.Context, in ProfilesGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + item, err := p.profiles.Get(ctx, in.Identifier) if err != nil { return util.CleanedUpSdkError{Err: err} } if item == nil || item.ID == "" { + if in.Output == "json" { + fmt.Println("null") + return nil + } pterm.Error.Printf("Profile '%s' not found\n", in.Identifier) return nil } + + if in.Output == "json" { + bs, err := json.MarshalIndent(item, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + name := item.Name if name == "" { name = "-" @@ -101,6 +145,10 @@ func (p ProfilesCmd) Get(ctx context.Context, in ProfilesGetInput) error { } func (p ProfilesCmd) Create(ctx context.Context, in ProfilesCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + params := kernel.ProfileNewParams{} if in.Name != "" { params.Name = kernel.Opt(in.Name) @@ -109,6 +157,16 @@ func (p ProfilesCmd) Create(ctx context.Context, in ProfilesCreateInput) error { if err != nil { return util.CleanedUpSdkError{Err: err} } + + if in.Output == "json" { + bs, err := json.MarshalIndent(item, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + name := item.Name if name == "" { name = "-" @@ -255,6 +313,9 @@ func init() { profilesCmd.AddCommand(profilesDeleteCmd) profilesCmd.AddCommand(profilesDownloadCmd) + profilesListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + profilesGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + profilesCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") profilesCreateCmd.Flags().String("name", "", "Optional unique profile name") profilesDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") profilesDownloadCmd.Flags().String("to", "", "Output zip file path") @@ -263,24 +324,27 @@ func init() { func runProfilesList(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") svc := client.Profiles p := ProfilesCmd{profiles: &svc} - return p.List(cmd.Context()) + return p.List(cmd.Context(), ProfilesListInput{Output: output}) } func runProfilesGet(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") svc := client.Profiles p := ProfilesCmd{profiles: &svc} - return p.Get(cmd.Context(), ProfilesGetInput{Identifier: args[0]}) + return p.Get(cmd.Context(), ProfilesGetInput{Identifier: args[0], Output: output}) } func runProfilesCreate(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) name, _ := cmd.Flags().GetString("name") + output, _ := cmd.Flags().GetString("output") svc := client.Profiles p := ProfilesCmd{profiles: &svc} - return p.Create(cmd.Context(), ProfilesCreateInput{Name: name}) + return p.Create(cmd.Context(), ProfilesCreateInput{Name: name, Output: output}) } func runProfilesDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/profiles_test.go b/cmd/profiles_test.go index 3833924..cf9b9fa 100644 --- a/cmd/profiles_test.go +++ b/cmd/profiles_test.go @@ -86,7 +86,7 @@ func TestProfilesList_Empty(t *testing.T) { buf := captureProfilesOutput(t) fake := &FakeProfilesService{} p := ProfilesCmd{profiles: fake} - _ = p.List(context.Background()) + _ = p.List(context.Background(), ProfilesListInput{}) assert.Contains(t, buf.String(), "No profiles found") } @@ -96,7 +96,7 @@ func TestProfilesList_WithRows(t *testing.T) { rows := []kernel.Profile{{ID: "p1", Name: "alpha", CreatedAt: created, UpdatedAt: created}, {ID: "p2", Name: "", CreatedAt: created, UpdatedAt: created}} fake := &FakeProfilesService{ListFunc: func(ctx context.Context, opts ...option.RequestOption) (*[]kernel.Profile, error) { return &rows, nil }} p := ProfilesCmd{profiles: fake} - _ = p.List(context.Background()) + _ = p.List(context.Background(), ProfilesListInput{}) out := buf.String() assert.Contains(t, out, "p1") assert.Contains(t, out, "alpha") diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index 633544e..673fbbf 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -2,6 +2,7 @@ package proxies import ( "context" + "encoding/json" "fmt" "github.com/kernel/cli/pkg/table" @@ -12,6 +13,10 @@ import ( ) func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + // Validate proxy type var proxyType kernel.ProxyNewParamsType switch in.Type { @@ -160,13 +165,24 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { } } - pterm.Info.Printf("Creating %s proxy...\n", proxyType) + if in.Output != "json" { + pterm.Info.Printf("Creating %s proxy...\n", proxyType) + } proxy, err := p.proxies.New(ctx, params) if err != nil { return util.CleanedUpSdkError{Err: err} } + if in.Output == "json" { + bs, err := json.MarshalIndent(proxy, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + pterm.Success.Printf("Successfully created proxy\n") // Display created proxy details @@ -210,6 +226,8 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error { username, _ := cmd.Flags().GetString("username") password, _ := cmd.Flags().GetString("password") + output, _ := cmd.Flags().GetString("output") + svc := client.Proxies p := ProxyCmd{proxies: &svc} return p.Create(cmd.Context(), ProxyCreateInput{ @@ -227,5 +245,6 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error { Port: port, Username: username, Password: password, + Output: output, }) } diff --git a/cmd/proxies/get.go b/cmd/proxies/get.go index 97fdc21..c565642 100644 --- a/cmd/proxies/get.go +++ b/cmd/proxies/get.go @@ -2,6 +2,7 @@ package proxies import ( "context" + "encoding/json" "fmt" "github.com/kernel/cli/pkg/table" @@ -12,11 +13,24 @@ import ( ) func (p ProxyCmd) Get(ctx context.Context, in ProxyGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + item, err := p.proxies.Get(ctx, in.ID) if err != nil { return util.CleanedUpSdkError{Err: err} } + if in.Output == "json" { + bs, err := json.MarshalIndent(item, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + // Display proxy details rows := pterm.TableData{{"Property", "Value"}} @@ -127,7 +141,8 @@ func getProxyConfigRows(proxy *kernel.ProxyGetResponse) [][]string { func runProxiesGet(cmd *cobra.Command, args []string) error { client := util.GetKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.Get(cmd.Context(), ProxyGetInput{ID: args[0]}) + return p.Get(cmd.Context(), ProxyGetInput{ID: args[0], Output: output}) } diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go index b90781c..86f36d4 100644 --- a/cmd/proxies/list.go +++ b/cmd/proxies/list.go @@ -2,6 +2,7 @@ package proxies import ( "context" + "encoding/json" "fmt" "strings" @@ -12,14 +13,33 @@ import ( "github.com/spf13/cobra" ) -func (p ProxyCmd) List(ctx context.Context) error { - pterm.Info.Println("Fetching proxy configurations...") +func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Output != "json" { + pterm.Info.Println("Fetching proxy configurations...") + } items, err := p.proxies.List(ctx) if err != nil { return util.CleanedUpSdkError{Err: err} } + if in.Output == "json" { + if items == nil || len(*items) == 0 { + fmt.Println("[]") + return nil + } + bs, err := json.MarshalIndent(*items, "", " ") + if err != nil { + return err + } + fmt.Println(string(bs)) + return nil + } + if items == nil || len(*items) == 0 { pterm.Info.Println("No proxy configurations found") return nil @@ -119,7 +139,8 @@ func formatProxyConfig(proxy *kernel.ProxyListResponse) string { func runProxiesList(cmd *cobra.Command, args []string) error { client := util.GetKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.List(cmd.Context()) + return p.List(cmd.Context(), ProxyListInput{Output: output}) } diff --git a/cmd/proxies/list_test.go b/cmd/proxies/list_test.go index dcb6f71..7a235f6 100644 --- a/cmd/proxies/list_test.go +++ b/cmd/proxies/list_test.go @@ -20,7 +20,7 @@ func TestProxyList_Empty(t *testing.T) { } p := ProxyCmd{proxies: fake} - err := p.List(context.Background()) + err := p.List(context.Background(), ProxyListInput{}) assert.NoError(t, err) assert.Contains(t, buf.String(), "No proxy configurations found") @@ -59,7 +59,7 @@ func TestProxyList_WithProxies(t *testing.T) { } p := ProxyCmd{proxies: fake} - err := p.List(context.Background()) + err := p.List(context.Background(), ProxyListInput{}) assert.NoError(t, err) output := buf.String() @@ -101,7 +101,7 @@ func TestProxyList_Error(t *testing.T) { } p := ProxyCmd{proxies: fake} - err := p.List(context.Background()) + err := p.List(context.Background(), ProxyListInput{}) assert.Error(t, err) assert.Contains(t, err.Error(), "API error") diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index 531a606..b6e7ffa 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -67,6 +67,11 @@ func init() { ProxiesCmd.AddCommand(proxiesCreateCmd) ProxiesCmd.AddCommand(proxiesDeleteCmd) + // Add output flags + proxiesListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + proxiesGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + proxiesCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + // Add flags for create command proxiesCreateCmd.Flags().String("name", "", "Proxy configuration name") proxiesCreateCmd.Flags().String("type", "", "Proxy type (datacenter|isp|residential|mobile|custom)") diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go index 979f071..6da63df 100644 --- a/cmd/proxies/types.go +++ b/cmd/proxies/types.go @@ -21,10 +21,13 @@ type ProxyCmd struct { } // Input types for proxy operations -type ProxyListInput struct{} +type ProxyListInput struct { + Output string +} type ProxyGetInput struct { - ID string + ID string + Output string } type ProxyCreateInput struct { @@ -46,6 +49,7 @@ type ProxyCreateInput struct { Port int Username string Password string + Output string } type ProxyDeleteInput struct {