diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d04f223..4208b5c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.5.1" + ".": "0.6.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bdac6d6..7ddae34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.6.0 (2025-12-06) + +Full Changelog: [v0.5.1...v0.6.0](https://github.com/onkernel/hypeman-cli/compare/v0.5.1...v0.6.0) + +### Features + +* **cli:** automatic streaming for paginated endpoints ([9af6924](https://github.com/onkernel/hypeman-cli/commit/9af69246d62010c32d39583c8b1eba39a663d3fa)) + ## 0.5.1 (2025-12-05) Full Changelog: [v0.5.0...v0.5.1](https://github.com/onkernel/hypeman-cli/compare/v0.5.0...v0.5.1) diff --git a/cmd/hypeman/main.go b/cmd/hypeman/main.go index 05214d5..aaf0f90 100644 --- a/cmd/hypeman/main.go +++ b/cmd/hypeman/main.go @@ -28,7 +28,7 @@ func main() { fmt.Fprintf(os.Stderr, "%s %q: %d %s\n", apierr.Request.Method, apierr.Request.URL, apierr.Response.StatusCode, http.StatusText(apierr.Response.StatusCode)) format := app.String("format-error") json := gjson.Parse(apierr.RawJSON()) - show_err := cmd.ShowJSON("Error", json, format, app.String("transform-error")) + show_err := cmd.ShowJSON(os.Stdout, "Error", json, format, app.String("transform-error")) if show_err != nil { // Just print the original error: fmt.Fprintf(os.Stderr, "%s\n", err.Error()) diff --git a/go.mod b/go.mod index 3efa46f..3ab548a 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/tidwall/sjson v1.2.5 github.com/urfave/cli-docs/v3 v3.0.0-alpha6 github.com/urfave/cli/v3 v3.3.2 + golang.org/x/sys v0.38.0 golang.org/x/term v0.37.0 ) @@ -68,7 +69,6 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 1934a88..23ef303 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -17,7 +17,7 @@ import ( var ( Command *cli.Command - OutputFormats = []string{"auto", "explore", "json", "pretty", "raw", "yaml"} + OutputFormats = []string{"auto", "explore", "json", "jsonl", "pretty", "raw", "yaml"} ) func init() { diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 67de708..9c7d162 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -7,7 +7,11 @@ import ( "net/http" "net/http/httputil" "os" + "os/exec" + "os/signal" + "runtime" "strings" + "syscall" "github.com/onkernel/hypeman-cli/pkg/jsonview" "github.com/onkernel/hypeman-go/option" @@ -16,6 +20,7 @@ import ( "github.com/tidwall/gjson" "github.com/tidwall/pretty" "github.com/urfave/cli/v3" + "golang.org/x/sys/unix" "golang.org/x/term" ) @@ -71,9 +76,123 @@ func isTerminal(w io.Writer) bool { } } +func streamOutput(label string, generateOutput func(w *os.File) error) error { + // For non-tty output (probably a pipe), write directly to stdout + if !isTerminal(os.Stdout) { + return streamToStdout(generateOutput) + } + + pagerInput, outputFile, isSocketPair, err := createPagerFiles() + if err != nil { + return err + } + defer pagerInput.Close() + defer outputFile.Close() + + cmd, err := startPagerCommand(pagerInput, label, isSocketPair) + if err != nil { + return err + } + + if err := pagerInput.Close(); err != nil { + return err + } + + // If the pager exits before reading all input, then generateOutput() will + // produce a broken pipe error, which is fine and we don't want to propagate it. + if err := generateOutput(outputFile); err != nil && !strings.Contains(err.Error(), "broken pipe") { + return err + } + + return cmd.Wait() +} + +func streamToStdout(generateOutput func(w *os.File) error) error { + signal.Ignore(syscall.SIGPIPE) + err := generateOutput(os.Stdout) + if err != nil && strings.Contains(err.Error(), "broken pipe") { + return nil + } + return err +} + +func createPagerFiles() (*os.File, *os.File, bool, error) { + // Windows lacks UNIX socket APIs, so we fall back to pipes there or if + // socket creation fails. We prefer sockets when available because they + // allow for smaller buffer sizes, preventing unnecessary data streaming + // from the backend. Pipes typically have large buffers but serve as a + // decent alternative when sockets aren't available. + if runtime.GOOS != "windows" { + pagerInput, outputFile, isSocketPair, err := createSocketPair() + if err == nil { + return pagerInput, outputFile, isSocketPair, nil + } + } + + r, w, err := os.Pipe() + return r, w, false, err +} + +// In order to avoid large buffers on pipes, this function create a pair of +// files for reading and writing through a barely buffered socket. +func createSocketPair() (*os.File, *os.File, bool, error) { + fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0) + if err != nil { + return nil, nil, false, err + } + + parentSock, childSock := fds[0], fds[1] + + // Use small buffer sizes so we don't ask the server for more paginated + // values than we actually need. + if err := unix.SetsockoptInt(parentSock, unix.SOL_SOCKET, unix.SO_SNDBUF, 128); err != nil { + return nil, nil, false, err + } + if err := unix.SetsockoptInt(childSock, unix.SOL_SOCKET, unix.SO_RCVBUF, 128); err != nil { + return nil, nil, false, err + } + + pagerInput := os.NewFile(uintptr(childSock), "child_socket") + outputFile := os.NewFile(uintptr(parentSock), "parent_socket") + return pagerInput, outputFile, true, nil +} + +// Start a subprocess running the user's preferred pager (or `less` if `$PAGER` is unset) +func startPagerCommand(pagerInput *os.File, label string, useSocketpair bool) (*exec.Cmd, error) { + pagerProgram := os.Getenv("PAGER") + if pagerProgram == "" { + pagerProgram = "less" + } + + if shouldUseColors(os.Stdout) { + os.Setenv("FORCE_COLOR", "1") + } + + var cmd *exec.Cmd + if useSocketpair { + cmd = exec.Command(pagerProgram, fmt.Sprintf("/dev/fd/%d", pagerInput.Fd())) + cmd.ExtraFiles = []*os.File{pagerInput} + } else { + cmd = exec.Command(pagerProgram) + cmd.Stdin = pagerInput + } + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), + "LESS=-r -f -P "+label, + "MORE=-r -f -P "+label, + ) + + if err := cmd.Start(); err != nil { + return nil, err + } + + return cmd, nil +} + func shouldUseColors(w io.Writer) bool { force, ok := os.LookupEnv("FORCE_COLOR") - if ok { if force == "1" { return true @@ -82,11 +201,10 @@ func shouldUseColors(w io.Writer) bool { return false } } - return isTerminal(w) } -func ShowJSON(title string, res gjson.Result, format string, transform string) error { +func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error { if format != "raw" && transform != "" { transformed := res.Get(transform) if transformed.Exists() { @@ -95,22 +213,36 @@ func ShowJSON(title string, res gjson.Result, format string, transform string) e } switch strings.ToLower(format) { case "auto": - return ShowJSON(title, res, "json", "") + return ShowJSON(out, title, res, "json", "") case "explore": return jsonview.ExploreJSON(title, res) case "pretty": - jsonview.DisplayJSON(title, res) - return nil + _, err := out.WriteString(jsonview.RenderJSON(title, res) + "\n") + return err case "json": prettyJSON := pretty.Pretty([]byte(res.Raw)) - if shouldUseColors(os.Stdout) { - fmt.Print(string(pretty.Color(prettyJSON, pretty.TerminalStyle))) + if shouldUseColors(out) { + _, err := out.Write(pretty.Color(prettyJSON, pretty.TerminalStyle)) + return err } else { - fmt.Print(string(prettyJSON)) + _, err := out.Write(prettyJSON) + return err + } + case "jsonl": + // @ugly is gjson syntax for "no whitespace", so it fits on one line + oneLineJSON := res.Get("@ugly").Raw + if shouldUseColors(out) { + bytes := append(pretty.Color([]byte(oneLineJSON), pretty.TerminalStyle), '\n') + _, err := out.Write(bytes) + return err + } else { + _, err := out.Write([]byte(oneLineJSON + "\n")) + return err } - return nil case "raw": - fmt.Println(res.Raw) + if _, err := out.Write([]byte(res.Raw + "\n")); err != nil { + return err + } return nil case "yaml": input := strings.NewReader(res.Raw) @@ -118,8 +250,8 @@ func ShowJSON(title string, res gjson.Result, format string, transform string) e if err := json2yaml.Convert(&yaml, input); err != nil { return err } - fmt.Print(yaml.String()) - return nil + _, err := out.Write([]byte(yaml.String())) + return err default: return fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", ")) } diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go new file mode 100644 index 0000000..027f3d4 --- /dev/null +++ b/pkg/cmd/cmdutil_test.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "os" + "testing" +) + +func TestStreamOutput(t *testing.T) { + t.Setenv("PAGER", "cat") + err := streamOutput("stream test", func(w *os.File) error { + _, writeErr := w.WriteString("Hello world\n") + return writeErr + }) + if err != nil { + t.Errorf("streamOutput failed: %v", err) + } +} diff --git a/pkg/cmd/health.go b/pkg/cmd/health.go index 4d6b861..7a3d7f7 100644 --- a/pkg/cmd/health.go +++ b/pkg/cmd/health.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "os" "github.com/onkernel/hypeman-cli/internal/apiquery" "github.com/onkernel/hypeman-go" @@ -24,6 +25,7 @@ var healthCheck = cli.Command{ func handleHealthCheck(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -36,6 +38,7 @@ func handleHealthCheck(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Health.Check(ctx, options...) @@ -43,8 +46,8 @@ func handleHealthCheck(ctx context.Context, cmd *cli.Command) error { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("health check", json, format, transform) + return ShowJSON(os.Stdout, "health check", obj, format, transform) } diff --git a/pkg/cmd/image.go b/pkg/cmd/image.go index 0dab5ef..9f19236 100644 --- a/pkg/cmd/image.go +++ b/pkg/cmd/image.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "os" "github.com/onkernel/hypeman-cli/internal/apiquery" "github.com/onkernel/hypeman-cli/internal/requestflag" @@ -65,6 +66,7 @@ var imagesGet = cli.Command{ func handleImagesCreate(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -79,26 +81,24 @@ func handleImagesCreate(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Images.New( - ctx, - params, - options..., - ) + _, err = client.Images.New(ctx, params, options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("images create", json, format, transform) + return ShowJSON(os.Stdout, "images create", obj, format, transform) } func handleImagesList(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -111,6 +111,7 @@ func handleImagesList(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Images.List(ctx, options...) @@ -118,10 +119,10 @@ func handleImagesList(ctx context.Context, cmd *cli.Command) error { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("images list", json, format, transform) + return ShowJSON(os.Stdout, "images list", obj, format, transform) } func handleImagesDelete(ctx context.Context, cmd *cli.Command) error { @@ -143,11 +144,8 @@ func handleImagesDelete(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - return client.Images.Delete( - ctx, - requestflag.CommandRequestValue[string](cmd, "name"), - options..., - ) + + return client.Images.Delete(ctx, requestflag.CommandRequestValue[string](cmd, "name"), options...) } func handleImagesGet(ctx context.Context, cmd *cli.Command) error { @@ -169,19 +167,16 @@ func handleImagesGet(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Images.Get( - ctx, - requestflag.CommandRequestValue[string](cmd, "name"), - options..., - ) + _, err = client.Images.Get(ctx, requestflag.CommandRequestValue[string](cmd, "name"), options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("images get", json, format, transform) + return ShowJSON(os.Stdout, "images get", obj, format, transform) } diff --git a/pkg/cmd/ingress.go b/pkg/cmd/ingress.go index 3a646f9..1d81758 100644 --- a/pkg/cmd/ingress.go +++ b/pkg/cmd/ingress.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "os" "github.com/onkernel/hypeman-cli/internal/apiquery" "github.com/onkernel/hypeman-cli/internal/requestflag" @@ -72,6 +73,7 @@ var ingressesGet = cli.Command{ func handleIngressesCreate(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -86,26 +88,24 @@ func handleIngressesCreate(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Ingresses.New( - ctx, - params, - options..., - ) + _, err = client.Ingresses.New(ctx, params, options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("ingresses create", json, format, transform) + return ShowJSON(os.Stdout, "ingresses create", obj, format, transform) } func handleIngressesList(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -118,6 +118,7 @@ func handleIngressesList(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Ingresses.List(ctx, options...) @@ -125,10 +126,10 @@ func handleIngressesList(ctx context.Context, cmd *cli.Command) error { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("ingresses list", json, format, transform) + return ShowJSON(os.Stdout, "ingresses list", obj, format, transform) } func handleIngressesDelete(ctx context.Context, cmd *cli.Command) error { @@ -150,11 +151,8 @@ func handleIngressesDelete(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - return client.Ingresses.Delete( - ctx, - requestflag.CommandRequestValue[string](cmd, "id"), - options..., - ) + + return client.Ingresses.Delete(ctx, requestflag.CommandRequestValue[string](cmd, "id"), options...) } func handleIngressesGet(ctx context.Context, cmd *cli.Command) error { @@ -176,19 +174,16 @@ func handleIngressesGet(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Ingresses.Get( - ctx, - requestflag.CommandRequestValue[string](cmd, "id"), - options..., - ) + _, err = client.Ingresses.Get(ctx, requestflag.CommandRequestValue[string](cmd, "id"), options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("ingresses get", json, format, transform) + return ShowJSON(os.Stdout, "ingresses get", obj, format, transform) } diff --git a/pkg/cmd/instance.go b/pkg/cmd/instance.go index 5cc41b5..8665f89 100644 --- a/pkg/cmd/instance.go +++ b/pkg/cmd/instance.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "os" "github.com/onkernel/hypeman-cli/internal/apiquery" "github.com/onkernel/hypeman-cli/internal/requestflag" @@ -176,6 +177,7 @@ var instancesStandby = cli.Command{ func handleInstancesCreate(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -190,26 +192,24 @@ func handleInstancesCreate(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Instances.New( - ctx, - params, - options..., - ) + _, err = client.Instances.New(ctx, params, options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("instances create", json, format, transform) + return ShowJSON(os.Stdout, "instances create", obj, format, transform) } func handleInstancesList(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -222,6 +222,7 @@ func handleInstancesList(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Instances.List(ctx, options...) @@ -229,10 +230,10 @@ func handleInstancesList(ctx context.Context, cmd *cli.Command) error { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("instances list", json, format, transform) + return ShowJSON(os.Stdout, "instances list", obj, format, transform) } func handleInstancesDelete(ctx context.Context, cmd *cli.Command) error { @@ -254,11 +255,8 @@ func handleInstancesDelete(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - return client.Instances.Delete( - ctx, - requestflag.CommandRequestValue[string](cmd, "id"), - options..., - ) + + return client.Instances.Delete(ctx, requestflag.CommandRequestValue[string](cmd, "id"), options...) } func handleInstancesGet(ctx context.Context, cmd *cli.Command) error { @@ -280,21 +278,18 @@ func handleInstancesGet(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Instances.Get( - ctx, - requestflag.CommandRequestValue[string](cmd, "id"), - options..., - ) + _, err = client.Instances.Get(ctx, requestflag.CommandRequestValue[string](cmd, "id"), options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("instances get", json, format, transform) + return ShowJSON(os.Stdout, "instances get", obj, format, transform) } func handleInstancesLogs(ctx context.Context, cmd *cli.Command) error { @@ -318,6 +313,7 @@ func handleInstancesLogs(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + stream := client.Instances.LogsStreaming( ctx, requestflag.CommandRequestValue[string](cmd, "id"), @@ -350,21 +346,18 @@ func handleInstancesRestore(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Instances.Restore( - ctx, - requestflag.CommandRequestValue[string](cmd, "id"), - options..., - ) + _, err = client.Instances.Restore(ctx, requestflag.CommandRequestValue[string](cmd, "id"), options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("instances restore", json, format, transform) + return ShowJSON(os.Stdout, "instances restore", obj, format, transform) } func handleInstancesStandby(ctx context.Context, cmd *cli.Command) error { @@ -386,19 +379,16 @@ func handleInstancesStandby(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Instances.Standby( - ctx, - requestflag.CommandRequestValue[string](cmd, "id"), - options..., - ) + _, err = client.Instances.Standby(ctx, requestflag.CommandRequestValue[string](cmd, "id"), options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("instances standby", json, format, transform) + return ShowJSON(os.Stdout, "instances standby", obj, format, transform) } diff --git a/pkg/cmd/instancevolume.go b/pkg/cmd/instancevolume.go index 1c3e079..f0ac631 100644 --- a/pkg/cmd/instancevolume.go +++ b/pkg/cmd/instancevolume.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "os" "github.com/onkernel/hypeman-cli/internal/apiquery" "github.com/onkernel/hypeman-cli/internal/requestflag" @@ -81,6 +82,7 @@ func handleInstancesVolumesAttach(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Instances.Volumes.Attach( @@ -93,10 +95,10 @@ func handleInstancesVolumesAttach(ctx context.Context, cmd *cli.Command) error { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("instances:volumes attach", json, format, transform) + return ShowJSON(os.Stdout, "instances:volumes attach", obj, format, transform) } func handleInstancesVolumesDetach(ctx context.Context, cmd *cli.Command) error { @@ -122,6 +124,7 @@ func handleInstancesVolumesDetach(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Instances.Volumes.Detach( @@ -134,8 +137,8 @@ func handleInstancesVolumesDetach(ctx context.Context, cmd *cli.Command) error { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("instances:volumes detach", json, format, transform) + return ShowJSON(os.Stdout, "instances:volumes detach", obj, format, transform) } diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index a76967f..c1f2884 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.5.1" // x-release-please-version +const Version = "0.6.0" // x-release-please-version diff --git a/pkg/cmd/volume.go b/pkg/cmd/volume.go index 2da5077..b3b8535 100644 --- a/pkg/cmd/volume.go +++ b/pkg/cmd/volume.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "os" "github.com/onkernel/hypeman-cli/internal/apiquery" "github.com/onkernel/hypeman-cli/internal/requestflag" @@ -79,6 +80,7 @@ var volumesGet = cli.Command{ func handleVolumesCreate(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -93,26 +95,24 @@ func handleVolumesCreate(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Volumes.New( - ctx, - params, - options..., - ) + _, err = client.Volumes.New(ctx, params, options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("volumes create", json, format, transform) + return ShowJSON(os.Stdout, "volumes create", obj, format, transform) } func handleVolumesList(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -125,6 +125,7 @@ func handleVolumesList(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Volumes.List(ctx, options...) @@ -132,10 +133,10 @@ func handleVolumesList(ctx context.Context, cmd *cli.Command) error { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("volumes list", json, format, transform) + return ShowJSON(os.Stdout, "volumes list", obj, format, transform) } func handleVolumesDelete(ctx context.Context, cmd *cli.Command) error { @@ -157,11 +158,8 @@ func handleVolumesDelete(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - return client.Volumes.Delete( - ctx, - requestflag.CommandRequestValue[string](cmd, "id"), - options..., - ) + + return client.Volumes.Delete(ctx, requestflag.CommandRequestValue[string](cmd, "id"), options...) } func handleVolumesGet(ctx context.Context, cmd *cli.Command) error { @@ -183,19 +181,16 @@ func handleVolumesGet(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Volumes.Get( - ctx, - requestflag.CommandRequestValue[string](cmd, "id"), - options..., - ) + _, err = client.Volumes.Get(ctx, requestflag.CommandRequestValue[string](cmd, "id"), options...) if err != nil { return err } - json := gjson.Parse(string(res)) + obj := gjson.ParseBytes(res) format := cmd.Root().String("format") transform := cmd.Root().String("transform") - return ShowJSON("volumes get", json, format, transform) + return ShowJSON(os.Stdout, "volumes get", obj, format, transform) } diff --git a/pkg/jsonview/explorer.go b/pkg/jsonview/explorer.go index 96a7a6f..8d725eb 100644 --- a/pkg/jsonview/explorer.go +++ b/pkg/jsonview/explorer.go @@ -1,6 +1,7 @@ package jsonview import ( + "errors" "fmt" "math" "strings" @@ -226,7 +227,8 @@ func ExploreJSON(title string, json gjson.Result) error { viewer := &JSONViewer{stack: []JSONView{view}, root: title, rawMode: false, help: help.New()} _, err = tea.NewProgram(viewer).Run() if viewer.message != "" { - fmt.Println("\n" + viewer.message) + _, msgErr := fmt.Println("\n" + viewer.message) + err = errors.Join(err, msgErr) } return err } diff --git a/pkg/jsonview/staticdisplay.go b/pkg/jsonview/staticdisplay.go index 4eaf65b..768ea34 100644 --- a/pkg/jsonview/staticdisplay.go +++ b/pkg/jsonview/staticdisplay.go @@ -133,7 +133,3 @@ func RenderJSON(title string, json gjson.Result) string { content := strings.TrimLeft(formatJSON(json, width), "\n") return titleStyle.Render(title) + "\n" + containerStyle.Render(content) } - -func DisplayJSON(title string, json gjson.Result) { - fmt.Println(RenderJSON(title, json)) -}