diff --git a/cmd/app.go b/cmd/app.go index f420b7e..98793bc 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "fmt" "strings" @@ -107,12 +106,7 @@ func runAppList(cmd *cobra.Command, args []string) error { fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(apps.Items, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(apps.Items) } if apps == nil || len(apps.Items) == 0 { @@ -242,12 +236,7 @@ func runAppHistory(cmd *cobra.Command, args []string) error { fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(deployments.Items, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(deployments.Items) } if deployments == nil || len(deployments.Items) == 0 { diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index 0d18eab..91dd1eb 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "encoding/json" "fmt" "strings" @@ -44,16 +43,11 @@ func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) err } if in.Output == "json" { - if pools == nil { + if pools == nil || len(*pools) == 0 { fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(*pools, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(*pools) } if pools == nil || len(*pools) == 0 { @@ -155,12 +149,7 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) } if in.Output == "json" { - bs, err := json.MarshalIndent(pool, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(pool) } if pool.Name != "" { @@ -187,12 +176,7 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error } if in.Output == "json" { - bs, err := json.MarshalIndent(pool, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(pool) } cfg := pool.BrowserPoolConfig @@ -301,12 +285,7 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) } if in.Output == "json" { - bs, err := json.MarshalIndent(pool, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(pool) } if pool.Name != "" { @@ -364,12 +343,7 @@ func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInpu } if in.Output == "json" { - bs, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(resp) } tableData := pterm.TableData{ diff --git a/cmd/browsers.go b/cmd/browsers.go index 4563269..4db20f5 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -226,16 +226,7 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { } if in.Output == "json" { - if len(browsers) == 0 { - fmt.Println("[]") - return nil - } - bs, err := json.MarshalIndent(browsers, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(browsers) } if len(browsers) == 0 { @@ -371,12 +362,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { } if in.Output == "json" { - bs, err := json.MarshalIndent(browser, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(browser) } printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Persistence, browser.Profile) @@ -482,9 +468,13 @@ func (b BrowsersCmd) View(ctx context.Context, in BrowsersViewInput) error { } if in.Output == "json" { - result := map[string]string{"liveViewUrl": browser.BrowserLiveViewURL} - bs, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(bs)) + // View command returns a custom response, not the full browser object + // Use json.Marshal to ensure proper JSON escaping of the URL + urlBytes, err := json.Marshal(browser.BrowserLiveViewURL) + if err != nil { + return err + } + fmt.Printf("{\n \"liveViewUrl\": %s\n}\n", urlBytes) return nil } @@ -511,12 +501,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) 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 + return util.PrintPrettyJSON(browser) } // Build table starting with common browser fields @@ -922,12 +907,7 @@ func (b BrowsersCmd) ReplaysList(ctx context.Context, in BrowsersReplaysListInpu fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(*items, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(*items) } if items == nil || len(*items) == 0 { @@ -964,12 +944,7 @@ func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartIn } if in.Output == "json" { - bs, err := json.MarshalIndent(res, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(res) } rows := pterm.TableData{{"Property", "Value"}, {"Replay ID", res.ReplayID}, {"View URL", res.ReplayViewURL}, {"Started At", util.FormatLocal(res.StartedAt)}} @@ -1148,12 +1123,7 @@ func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInpu } if in.Output == "json" { - bs, err := json.MarshalIndent(res, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(res) } rows := pterm.TableData{{"Property", "Value"}, {"Exit Code", fmt.Sprintf("%d", res.ExitCode)}, {"Duration (ms)", fmt.Sprintf("%d", res.DurationMs)}} @@ -1220,12 +1190,7 @@ func (b BrowsersCmd) ProcessSpawn(ctx context.Context, in BrowsersProcessSpawnIn } if in.Output == "json" { - bs, err := json.MarshalIndent(res, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(res) } rows := pterm.TableData{{"Property", "Value"}, {"Process ID", res.ProcessID}, {"PID", fmt.Sprintf("%d", res.Pid)}, {"Started At", util.FormatLocal(res.StartedAt)}} @@ -1508,12 +1473,7 @@ func (b BrowsersCmd) FSFileInfo(ctx context.Context, in BrowsersFSFileInfoInput) } if in.Output == "json" { - bs, err := json.MarshalIndent(res, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(res) } 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)}} @@ -1544,12 +1504,7 @@ func (b BrowsersCmd) FSListFiles(ctx context.Context, in BrowsersFSListFilesInpu fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(*res, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(*res) } if res == nil || len(*res) == 0 { @@ -2227,12 +2182,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { return nil } if output == "json" { - bs, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(resp) } printBrowserSessionResult(resp.SessionID, resp.CdpWsURL, resp.BrowserLiveViewURL, resp.Persistence, resp.Profile) return nil diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 901480d..e2a9eab 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -407,10 +407,13 @@ func TestBrowsersGet_JSONOutput(t *testing.T) { fake := &FakeBrowsersService{ GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) { - return &kernel.BrowserGetResponse{ - SessionID: "sess-json", - CdpWsURL: "ws://cdp", - }, nil + // Unmarshal JSON to populate RawJSON() properly + jsonData := `{"session_id": "sess-json", "cdp_ws_url": "ws://cdp", "created_at": "2024-01-01T00:00:00Z", "headless": false, "stealth": false, "timeout_seconds": 60}` + var resp kernel.BrowserGetResponse + if err := json.Unmarshal([]byte(jsonData), &resp); err != nil { + t.Fatalf("failed to unmarshal test response: %v", err) + } + return &resp, nil }, } b := BrowsersCmd{browsers: fake} diff --git a/cmd/deploy.go b/cmd/deploy.go index ee734ea..72301b8 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -433,12 +433,7 @@ func runDeployHistory(cmd *cobra.Command, args []string) error { fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(deployments.Items, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(deployments.Items) } if deployments == nil || len(deployments.Items) == 0 { diff --git a/cmd/extensions.go b/cmd/extensions.go index fc97e90..e7e2a4d 100644 --- a/cmd/extensions.go +++ b/cmd/extensions.go @@ -3,7 +3,6 @@ package cmd import ( "bytes" "context" - "encoding/json" "fmt" "io" "net/http" @@ -76,12 +75,7 @@ func (e ExtensionsCmd) List(ctx context.Context, in ExtensionsListInput) error { fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(*items, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(*items) } if items == nil || len(*items) == 0 { @@ -325,12 +319,7 @@ func (e ExtensionsCmd) Upload(ctx context.Context, in ExtensionsUploadInput) err } if in.Output == "json" { - bs, err := json.MarshalIndent(item, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(item) } name := item.Name diff --git a/cmd/invoke.go b/cmd/invoke.go index a90b16c..4b80b67 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -379,12 +379,7 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error { fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(invocations.Items, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(invocations.Items) } table := pterm.TableData{{"Invocation ID", "App Name", "Action", "Version", "Status", "Started At", "Duration", "Output"}} diff --git a/cmd/profiles.go b/cmd/profiles.go index 71049f9..03b8c41 100644 --- a/cmd/profiles.go +++ b/cmd/profiles.go @@ -73,12 +73,7 @@ func (p ProfilesCmd) List(ctx context.Context, in ProfilesListInput) error { fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(*items, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(*items) } if items == nil || len(*items) == 0 { @@ -122,12 +117,7 @@ func (p ProfilesCmd) Get(ctx context.Context, in ProfilesGetInput) error { } if in.Output == "json" { - bs, err := json.MarshalIndent(item, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(item) } name := item.Name @@ -159,12 +149,7 @@ func (p ProfilesCmd) Create(ctx context.Context, in ProfilesCreateInput) error { } if in.Output == "json" { - bs, err := json.MarshalIndent(item, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(item) } name := item.Name diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index 673fbbf..96606b2 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -2,7 +2,6 @@ package proxies import ( "context" - "encoding/json" "fmt" "github.com/kernel/cli/pkg/table" @@ -175,12 +174,7 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { } if in.Output == "json" { - bs, err := json.MarshalIndent(proxy, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(proxy) } pterm.Success.Printf("Successfully created proxy\n") diff --git a/cmd/proxies/get.go b/cmd/proxies/get.go index c565642..0b7b091 100644 --- a/cmd/proxies/get.go +++ b/cmd/proxies/get.go @@ -2,7 +2,6 @@ package proxies import ( "context" - "encoding/json" "fmt" "github.com/kernel/cli/pkg/table" @@ -23,12 +22,7 @@ func (p ProxyCmd) Get(ctx context.Context, in ProxyGetInput) error { } if in.Output == "json" { - bs, err := json.MarshalIndent(item, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSON(item) } // Display proxy details diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go index 86f36d4..f34a5d7 100644 --- a/cmd/proxies/list.go +++ b/cmd/proxies/list.go @@ -2,7 +2,6 @@ package proxies import ( "context" - "encoding/json" "fmt" "strings" @@ -32,12 +31,7 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error { fmt.Println("[]") return nil } - bs, err := json.MarshalIndent(*items, "", " ") - if err != nil { - return err - } - fmt.Println(string(bs)) - return nil + return util.PrintPrettyJSONSlice(*items) } if items == nil || len(*items) == 0 { diff --git a/pkg/util/json.go b/pkg/util/json.go new file mode 100644 index 0000000..aa20d3b --- /dev/null +++ b/pkg/util/json.go @@ -0,0 +1,67 @@ +package util + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// RawJSONProvider is an interface for SDK types that provide raw JSON responses. +type RawJSONProvider interface { + RawJSON() string +} + +// PrintPrettyJSON prints the raw JSON from an SDK response type with indentation. +// It uses the RawJSON() method to get the original API response, avoiding +// zero-value fields that would appear when re-marshaling the Go struct. +func PrintPrettyJSON(v RawJSONProvider) error { + raw := v.RawJSON() + if raw == "" { + fmt.Println("{}") + return nil + } + + var buf bytes.Buffer + if err := json.Indent(&buf, []byte(raw), "", " "); err != nil { + return err + } + fmt.Println(buf.String()) + return nil +} + +// PrintPrettyJSONSlice prints a slice of SDK response types as a JSON array. +// Each element must implement RawJSONProvider. +func PrintPrettyJSONSlice[T RawJSONProvider](items []T) error { + if len(items) == 0 { + fmt.Println("[]") + return nil + } + + // Build a JSON array from raw JSON elements + var buf bytes.Buffer + buf.WriteString("[\n") + for i, item := range items { + raw := item.RawJSON() + if raw == "" { + raw = "{}" + } + // Indent each element + var elemBuf bytes.Buffer + if err := json.Indent(&elemBuf, []byte(raw), " ", " "); err != nil { + // Fallback to raw if indentation fails + buf.WriteString(" ") + buf.WriteString(raw) + } else { + // json.Indent adds prefix after newlines, not before first line + buf.WriteString(" ") + buf.Write(elemBuf.Bytes()) + } + if i < len(items)-1 { + buf.WriteString(",") + } + buf.WriteString("\n") + } + buf.WriteString("]") + fmt.Println(buf.String()) + return nil +}