From 51bb415b4d8365514963a436339f4aaf8334beff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:37:19 +0000 Subject: [PATCH 1/5] chore(internal): codegen related update --- go.mod | 2 +- pkg/cmd/util.go | 149 -------------------- pkg/jsonflag/json_flag.go | 248 ---------------------------------- pkg/jsonflag/mutation.go | 104 -------------- pkg/jsonflag/mutation_test.go | 37 ----- 5 files changed, 1 insertion(+), 539 deletions(-) delete mode 100644 pkg/cmd/util.go delete mode 100644 pkg/jsonflag/json_flag.go delete mode 100644 pkg/jsonflag/mutation.go delete mode 100644 pkg/jsonflag/mutation_test.go diff --git a/go.mod b/go.mod index 1fb7a42..d7f661d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/onkernel/hypeman-go v0.8.0 github.com/tidwall/gjson v1.18.0 github.com/tidwall/pretty v1.2.1 - 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 @@ -61,6 +60,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go deleted file mode 100644 index 5a17ff1..0000000 --- a/pkg/cmd/util.go +++ /dev/null @@ -1,149 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/http/httputil" - "os" - "reflect" - "strings" - - "github.com/onkernel/hypeman-go/option" - - "github.com/tidwall/sjson" - "github.com/urfave/cli/v3" -) - - -type fileReader struct { - Value io.Reader - Base64Encoded bool -} - -func (f *fileReader) Set(filename string) error { - reader, err := os.Open(filename) - if err != nil { - return fmt.Errorf("failed to read file %q: %w", filename, err) - } - f.Value = reader - return nil -} - -func (f *fileReader) String() string { - if f.Value == nil { - return "" - } - buf := new(bytes.Buffer) - buf.ReadFrom(f.Value) - if f.Base64Encoded { - return base64.StdEncoding.EncodeToString(buf.Bytes()) - } - return buf.String() -} - -func (f *fileReader) Get() any { - return f.String() -} - -func unmarshalWithReaders(data []byte, v any) error { - var fields map[string]json.RawMessage - if err := json.Unmarshal(data, &fields); err != nil { - return err - } - - rv := reflect.ValueOf(v).Elem() - rt := rv.Type() - - for i := 0; i < rv.NumField(); i++ { - fv := rv.Field(i) - ft := rt.Field(i) - - jsonKey := ft.Tag.Get("json") - if jsonKey == "" { - jsonKey = ft.Name - } else if idx := strings.Index(jsonKey, ","); idx != -1 { - jsonKey = jsonKey[:idx] - } - - rawVal, ok := fields[jsonKey] - if !ok { - continue - } - - if ft.Type == reflect.TypeOf((*io.Reader)(nil)).Elem() { - var s string - if err := json.Unmarshal(rawVal, &s); err != nil { - return fmt.Errorf("field %s: %w", ft.Name, err) - } - fv.Set(reflect.ValueOf(strings.NewReader(s))) - } else { - ptr := fv.Addr().Interface() - if err := json.Unmarshal(rawVal, ptr); err != nil { - return fmt.Errorf("field %s: %w", ft.Name, err) - } - } - } - - return nil -} - -func unmarshalStdinWithFlags(cmd *cli.Command, flags map[string]string, target any) error { - var data []byte - if isInputPiped() { - var err error - if data, err = io.ReadAll(os.Stdin); err != nil { - return err - } - } - - // Merge CLI flags into the body - for flag, path := range flags { - if cmd.IsSet(flag) { - var err error - data, err = sjson.SetBytes(data, path, cmd.Value(flag)) - if err != nil { - return err - } - } - } - - if data != nil { - if err := unmarshalWithReaders(data, target); err != nil { - return fmt.Errorf("failed to unmarshal JSON: %w", err) - } - } - - return nil -} - -func debugMiddleware(debug bool) option.Middleware { - return func(r *http.Request, mn option.MiddlewareNext) (*http.Response, error) { - if debug { - logger := log.Default() - - if reqBytes, err := httputil.DumpRequest(r, true); err == nil { - logger.Printf("Request Content:\n%s\n", reqBytes) - } - - resp, err := mn(r) - if err != nil { - return resp, err - } - - if respBytes, err := httputil.DumpResponse(resp, true); err == nil { - logger.Printf("Response Content:\n%s\n", respBytes) - } - - return resp, err - } - - return mn(r) - } -} diff --git a/pkg/jsonflag/json_flag.go b/pkg/jsonflag/json_flag.go deleted file mode 100644 index 605f883..0000000 --- a/pkg/jsonflag/json_flag.go +++ /dev/null @@ -1,248 +0,0 @@ -package jsonflag - -import ( - "fmt" - "strconv" - "time" - - "github.com/urfave/cli/v3" -) - -type JSONConfig struct { - Kind MutationKind - Path string - // For boolean flags that set a specific value when present - SetValue any -} - -type JSONValueCreator[T any] struct{} - -func (c JSONValueCreator[T]) Create(val T, dest *T, config JSONConfig) cli.Value { - *dest = val - return &jsonValue[T]{ - destination: dest, - config: config, - } -} - -func (c JSONValueCreator[T]) ToString(val T) string { - switch v := any(val).(type) { - case string: - if v == "" { - return v - } - return fmt.Sprintf("%q", v) - case bool: - return strconv.FormatBool(v) - case int: - return strconv.Itoa(v) - case float64: - return strconv.FormatFloat(v, 'g', -1, 64) - case time.Time: - return v.Format(time.RFC3339) - default: - return fmt.Sprintf("%v", v) - } -} - -type jsonValue[T any] struct { - destination *T - config JSONConfig -} - -func (v *jsonValue[T]) Set(val string) error { - var parsed T - var err error - - // If SetValue is configured, use that value instead of parsing the input - if v.config.SetValue != nil { - // For boolean flags with SetValue, register the configured value - if _, isBool := any(parsed).(bool); isBool { - globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) - *v.destination = any(true).(T) // Set the flag itself to true - return nil - } - // For any flags with SetValue, register the configured value - globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) - *v.destination = any(v.config.SetValue).(T) - return nil - } - - switch any(parsed).(type) { - case string: - parsed = any(val).(T) - case bool: - boolVal, parseErr := strconv.ParseBool(val) - if parseErr != nil { - return fmt.Errorf("invalid boolean value %q: %w", val, parseErr) - } - parsed = any(boolVal).(T) - case int: - intVal, parseErr := strconv.Atoi(val) - if parseErr != nil { - return fmt.Errorf("invalid integer value %q: %w", val, parseErr) - } - parsed = any(intVal).(T) - case float64: - floatVal, parseErr := strconv.ParseFloat(val, 64) - if parseErr != nil { - return fmt.Errorf("invalid float value %q: %w", val, parseErr) - } - parsed = any(floatVal).(T) - case time.Time: - // Try common datetime formats - formats := []string{ - time.RFC3339, - "2006-01-02T15:04:05Z07:00", - "2006-01-02T15:04:05", - "2006-01-02 15:04:05", - "2006-01-02", - "15:04:05", - "15:04", - } - var timeVal time.Time - var parseErr error - for _, format := range formats { - timeVal, parseErr = time.Parse(format, val) - if parseErr == nil { - break - } - } - if parseErr != nil { - return fmt.Errorf("invalid datetime value %q: %w", val, parseErr) - } - parsed = any(timeVal).(T) - case any: - // For `any`, store the string value directly - parsed = any(val).(T) - default: - return fmt.Errorf("unsupported type for JSON flag") - } - - *v.destination = parsed - globalRegistry.Mutate(v.config.Kind, v.config.Path, parsed) - return err -} - -func (v *jsonValue[T]) Get() any { - if v.destination != nil { - return *v.destination - } - var zero T - return zero -} - -func (v *jsonValue[T]) String() string { - if v.destination != nil { - switch val := any(*v.destination).(type) { - case string: - return val - case bool: - return strconv.FormatBool(val) - case int: - return strconv.Itoa(val) - case float64: - return strconv.FormatFloat(val, 'g', -1, 64) - case time.Time: - return val.Format(time.RFC3339) - default: - return fmt.Sprintf("%v", val) - } - } - var zero T - switch any(zero).(type) { - case string: - return "" - case bool: - return "false" - case int: - return "0" - case float64: - return "0" - case time.Time: - return "" - default: - return fmt.Sprintf("%v", zero) - } -} - -func (v *jsonValue[T]) IsBoolFlag() bool { - return v.config.SetValue != nil -} - -// JSONDateValueCreator is a specialized creator for date-only values -type JSONDateValueCreator struct{} - -func (c JSONDateValueCreator) Create(val time.Time, dest *time.Time, config JSONConfig) cli.Value { - *dest = val - return &jsonDateValue{ - destination: dest, - config: config, - } -} - -func (c JSONDateValueCreator) ToString(val time.Time) string { - return val.Format("2006-01-02") -} - -type jsonDateValue struct { - destination *time.Time - config JSONConfig -} - -func (v *jsonDateValue) Set(val string) error { - // Try date-only formats first, then fall back to datetime formats - formats := []string{ - "2006-01-02", - "01/02/2006", - "Jan 2, 2006", - "January 2, 2006", - "2-Jan-2006", - time.RFC3339, - "2006-01-02T15:04:05Z07:00", - "2006-01-02T15:04:05", - "2006-01-02 15:04:05", - } - - var timeVal time.Time - var parseErr error - for _, format := range formats { - timeVal, parseErr = time.Parse(format, val) - if parseErr == nil { - break - } - } - if parseErr != nil { - return fmt.Errorf("invalid date value %q: %w", val, parseErr) - } - - *v.destination = timeVal - globalRegistry.Mutate(v.config.Kind, v.config.Path, timeVal.Format("2006-01-02")) - return nil -} - -func (v *jsonDateValue) Get() any { - if v.destination != nil { - return *v.destination - } - return time.Time{} -} - -func (v *jsonDateValue) String() string { - if v.destination != nil { - return v.destination.Format("2006-01-02") - } - return "" -} - -func (v *jsonDateValue) IsBoolFlag() bool { - return false -} - -type JSONStringFlag = cli.FlagBase[string, JSONConfig, JSONValueCreator[string]] -type JSONBoolFlag = cli.FlagBase[bool, JSONConfig, JSONValueCreator[bool]] -type JSONIntFlag = cli.FlagBase[int, JSONConfig, JSONValueCreator[int]] -type JSONFloatFlag = cli.FlagBase[float64, JSONConfig, JSONValueCreator[float64]] -type JSONDatetimeFlag = cli.FlagBase[time.Time, JSONConfig, JSONValueCreator[time.Time]] -type JSONDateFlag = cli.FlagBase[time.Time, JSONConfig, JSONDateValueCreator] -type JSONAnyFlag = cli.FlagBase[any, JSONConfig, JSONValueCreator[any]] diff --git a/pkg/jsonflag/mutation.go b/pkg/jsonflag/mutation.go deleted file mode 100644 index 46c115b..0000000 --- a/pkg/jsonflag/mutation.go +++ /dev/null @@ -1,104 +0,0 @@ -package jsonflag - -import ( - "fmt" - "strconv" - "strings" - - "github.com/tidwall/gjson" - "github.com/tidwall/sjson" -) - -type MutationKind string - -const ( - Body MutationKind = "body" - Query MutationKind = "query" - Header MutationKind = "header" -) - -type Mutation struct { - Kind MutationKind - Path string - Value any -} - -type registry struct { - mutations []Mutation -} - -var globalRegistry = ®istry{} - -func (r *registry) Mutate(kind MutationKind, path string, value any) { - r.mutations = append(r.mutations, Mutation{ - Kind: kind, - Path: path, - Value: value, - }) -} - -func (r *registry) Apply(body, query, header []byte) ([]byte, []byte, []byte, error) { - var err error - - for _, mutation := range r.mutations { - switch mutation.Kind { - case Body: - body, err = jsonSet(body, mutation.Path, mutation.Value) - case Query: - query, err = jsonSet(query, mutation.Path, mutation.Value) - case Header: - header, err = jsonSet(header, mutation.Path, mutation.Value) - } - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to apply mutation %s.%s: %w", mutation.Kind, mutation.Path, err) - } - } - - return body, query, header, nil -} - -func (r *registry) Clear() { - r.mutations = nil -} - -func (r *registry) List() []Mutation { - result := make([]Mutation, len(r.mutations)) - copy(result, r.mutations) - return result -} - -// Mutate adds a mutation that will be applied to the specified kind of data -func Mutate(kind MutationKind, path string, value any) { - globalRegistry.Mutate(kind, path, value) -} - -// ApplyMutations applies all registered mutations to the provided JSON data -func ApplyMutations(body, query, header []byte) ([]byte, []byte, []byte, error) { - return globalRegistry.Apply(body, query, header) -} - -// ClearMutations removes all registered mutations from the global registry -func ClearMutations() { - globalRegistry.Clear() -} - -// ListMutations returns a copy of all currently registered mutations -func ListMutations() []Mutation { - return globalRegistry.List() -} - -func jsonSet(json []byte, path string, value any) ([]byte, error) { - keys := strings.Split(path, ".") - path = "" - for _, key := range keys { - if key == "#" { - key = strconv.Itoa(len(gjson.GetBytes(json, path).Array()) - 1) - } - - if len(path) > 0 { - path += "." - } - path += key - } - return sjson.SetBytes(json, path, value) -} diff --git a/pkg/jsonflag/mutation_test.go b/pkg/jsonflag/mutation_test.go deleted file mode 100644 index e87e518..0000000 --- a/pkg/jsonflag/mutation_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package jsonflag - -import ( - "testing" -) - -func TestApply(t *testing.T) { - ClearMutations() - - Mutate(Body, "name", "test") - Mutate(Query, "page", 1) - Mutate(Header, "authorization", "Bearer token") - - body, query, header, err := ApplyMutations( - []byte(`{}`), - []byte(`{}`), - []byte(`{}`), - ) - - if err != nil { - t.Fatalf("Failed to apply mutations: %v", err) - } - - expectedBody := `{"name":"test"}` - expectedQuery := `{"page":1}` - expectedHeader := `{"authorization":"Bearer token"}` - - if string(body) != expectedBody { - t.Errorf("Body mismatch. Expected: %s, Got: %s", expectedBody, string(body)) - } - if string(query) != expectedQuery { - t.Errorf("Query mismatch. Expected: %s, Got: %s", expectedQuery, string(query)) - } - if string(header) != expectedHeader { - t.Errorf("Header mismatch. Expected: %s, Got: %s", expectedHeader, string(header)) - } -} From 7c0094d0b5ea9e3b6b116d8aa2de5fa9d01a069f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 05:25:46 +0000 Subject: [PATCH 2/5] fix(mcp): correct code tool API endpoint --- go.mod | 2 +- pkg/cmd/util.go | 239 ++++++++++++++++++++++++++++++++ pkg/jsonflag/json_flag.go | 248 ++++++++++++++++++++++++++++++++++ pkg/jsonflag/mutation.go | 104 ++++++++++++++ pkg/jsonflag/mutation_test.go | 37 +++++ 5 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 pkg/cmd/util.go create mode 100644 pkg/jsonflag/json_flag.go create mode 100644 pkg/jsonflag/mutation.go create mode 100644 pkg/jsonflag/mutation_test.go diff --git a/go.mod b/go.mod index d7f661d..1fb7a42 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/onkernel/hypeman-go v0.8.0 github.com/tidwall/gjson v1.18.0 github.com/tidwall/pretty v1.2.1 + 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 @@ -60,7 +61,6 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go new file mode 100644 index 0000000..d2f7de7 --- /dev/null +++ b/pkg/cmd/util.go @@ -0,0 +1,239 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "os" + "reflect" + "strings" + + "golang.org/x/term" + + "github.com/onkernel/hypeman-cli/pkg/jsonview" + "github.com/onkernel/hypeman-go/option" + + "github.com/itchyny/json2yaml" + "github.com/tidwall/gjson" + "github.com/tidwall/pretty" + "github.com/tidwall/sjson" + "github.com/urfave/cli/v3" +) + +func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { + opts := []option.RequestOption{ + option.WithHeader("User-Agent", fmt.Sprintf("Hypeman/CLI %s", Version)), + option.WithHeader("X-Stainless-Lang", "cli"), + option.WithHeader("X-Stainless-Package-Version", Version), + option.WithHeader("X-Stainless-Runtime", "cli"), + option.WithHeader("X-Stainless-CLI-Command", cmd.FullName()), + } + + // Override base URL if the --base-url flag is provided + if baseURL := cmd.String("base-url"); baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } + + return opts +} + +type fileReader struct { + Value io.Reader + Base64Encoded bool +} + +func (f *fileReader) Set(filename string) error { + reader, err := os.Open(filename) + if err != nil { + return fmt.Errorf("failed to read file %q: %w", filename, err) + } + f.Value = reader + return nil +} + +func (f *fileReader) String() string { + if f.Value == nil { + return "" + } + buf := new(bytes.Buffer) + buf.ReadFrom(f.Value) + if f.Base64Encoded { + return base64.StdEncoding.EncodeToString(buf.Bytes()) + } + return buf.String() +} + +func (f *fileReader) Get() any { + return f.String() +} + +func unmarshalWithReaders(data []byte, v any) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + + rv := reflect.ValueOf(v).Elem() + rt := rv.Type() + + for i := 0; i < rv.NumField(); i++ { + fv := rv.Field(i) + ft := rt.Field(i) + + jsonKey := ft.Tag.Get("json") + if jsonKey == "" { + jsonKey = ft.Name + } else if idx := strings.Index(jsonKey, ","); idx != -1 { + jsonKey = jsonKey[:idx] + } + + rawVal, ok := fields[jsonKey] + if !ok { + continue + } + + if ft.Type == reflect.TypeOf((*io.Reader)(nil)).Elem() { + var s string + if err := json.Unmarshal(rawVal, &s); err != nil { + return fmt.Errorf("field %s: %w", ft.Name, err) + } + fv.Set(reflect.ValueOf(strings.NewReader(s))) + } else { + ptr := fv.Addr().Interface() + if err := json.Unmarshal(rawVal, ptr); err != nil { + return fmt.Errorf("field %s: %w", ft.Name, err) + } + } + } + + return nil +} + +func unmarshalStdinWithFlags(cmd *cli.Command, flags map[string]string, target any) error { + var data []byte + if isInputPiped() { + var err error + if data, err = io.ReadAll(os.Stdin); err != nil { + return err + } + } + + // Merge CLI flags into the body + for flag, path := range flags { + if cmd.IsSet(flag) { + var err error + data, err = sjson.SetBytes(data, path, cmd.Value(flag)) + if err != nil { + return err + } + } + } + + if data != nil { + if err := unmarshalWithReaders(data, target); err != nil { + return fmt.Errorf("failed to unmarshal JSON: %w", err) + } + } + + return nil +} + +func debugMiddleware(debug bool) option.Middleware { + return func(r *http.Request, mn option.MiddlewareNext) (*http.Response, error) { + if debug { + logger := log.Default() + + if reqBytes, err := httputil.DumpRequest(r, true); err == nil { + logger.Printf("Request Content:\n%s\n", reqBytes) + } + + resp, err := mn(r) + if err != nil { + return resp, err + } + + if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + logger.Printf("Response Content:\n%s\n", respBytes) + } + + return resp, err + } + + return mn(r) + } +} + +func isInputPiped() bool { + stat, _ := os.Stdin.Stat() + return (stat.Mode() & os.ModeCharDevice) == 0 +} + +func isTerminal(w io.Writer) bool { + switch v := w.(type) { + case *os.File: + return term.IsTerminal(int(v.Fd())) + default: + return false + } +} + +func shouldUseColors(w io.Writer) bool { + force, ok := os.LookupEnv("FORCE_COLOR") + + if ok { + if force == "1" { + return true + } + if force == "0" { + return false + } + } + + return isTerminal(w) +} + +func ShowJSON(title string, res gjson.Result, format string, transform string) error { + if format != "raw" && transform != "" { + transformed := res.Get(transform) + if transformed.Exists() { + res = transformed + } + } + switch strings.ToLower(format) { + case "auto": + return ShowJSON(title, res, "json", "") + case "explore": + return jsonview.ExploreJSON(title, res) + case "pretty": + jsonview.DisplayJSON(title, res) + return nil + case "json": + prettyJSON := pretty.Pretty([]byte(res.Raw)) + if shouldUseColors(os.Stdout) { + fmt.Print(string(pretty.Color(prettyJSON, pretty.TerminalStyle))) + } else { + fmt.Print(string(prettyJSON)) + } + return nil + case "raw": + fmt.Println(res.Raw) + return nil + case "yaml": + input := strings.NewReader(res.Raw) + var yaml strings.Builder + if err := json2yaml.Convert(&yaml, input); err != nil { + return err + } + fmt.Print(yaml.String()) + return nil + default: + return fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", ")) + } +} diff --git a/pkg/jsonflag/json_flag.go b/pkg/jsonflag/json_flag.go new file mode 100644 index 0000000..605f883 --- /dev/null +++ b/pkg/jsonflag/json_flag.go @@ -0,0 +1,248 @@ +package jsonflag + +import ( + "fmt" + "strconv" + "time" + + "github.com/urfave/cli/v3" +) + +type JSONConfig struct { + Kind MutationKind + Path string + // For boolean flags that set a specific value when present + SetValue any +} + +type JSONValueCreator[T any] struct{} + +func (c JSONValueCreator[T]) Create(val T, dest *T, config JSONConfig) cli.Value { + *dest = val + return &jsonValue[T]{ + destination: dest, + config: config, + } +} + +func (c JSONValueCreator[T]) ToString(val T) string { + switch v := any(val).(type) { + case string: + if v == "" { + return v + } + return fmt.Sprintf("%q", v) + case bool: + return strconv.FormatBool(v) + case int: + return strconv.Itoa(v) + case float64: + return strconv.FormatFloat(v, 'g', -1, 64) + case time.Time: + return v.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", v) + } +} + +type jsonValue[T any] struct { + destination *T + config JSONConfig +} + +func (v *jsonValue[T]) Set(val string) error { + var parsed T + var err error + + // If SetValue is configured, use that value instead of parsing the input + if v.config.SetValue != nil { + // For boolean flags with SetValue, register the configured value + if _, isBool := any(parsed).(bool); isBool { + globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) + *v.destination = any(true).(T) // Set the flag itself to true + return nil + } + // For any flags with SetValue, register the configured value + globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) + *v.destination = any(v.config.SetValue).(T) + return nil + } + + switch any(parsed).(type) { + case string: + parsed = any(val).(T) + case bool: + boolVal, parseErr := strconv.ParseBool(val) + if parseErr != nil { + return fmt.Errorf("invalid boolean value %q: %w", val, parseErr) + } + parsed = any(boolVal).(T) + case int: + intVal, parseErr := strconv.Atoi(val) + if parseErr != nil { + return fmt.Errorf("invalid integer value %q: %w", val, parseErr) + } + parsed = any(intVal).(T) + case float64: + floatVal, parseErr := strconv.ParseFloat(val, 64) + if parseErr != nil { + return fmt.Errorf("invalid float value %q: %w", val, parseErr) + } + parsed = any(floatVal).(T) + case time.Time: + // Try common datetime formats + formats := []string{ + time.RFC3339, + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + "2006-01-02", + "15:04:05", + "15:04", + } + var timeVal time.Time + var parseErr error + for _, format := range formats { + timeVal, parseErr = time.Parse(format, val) + if parseErr == nil { + break + } + } + if parseErr != nil { + return fmt.Errorf("invalid datetime value %q: %w", val, parseErr) + } + parsed = any(timeVal).(T) + case any: + // For `any`, store the string value directly + parsed = any(val).(T) + default: + return fmt.Errorf("unsupported type for JSON flag") + } + + *v.destination = parsed + globalRegistry.Mutate(v.config.Kind, v.config.Path, parsed) + return err +} + +func (v *jsonValue[T]) Get() any { + if v.destination != nil { + return *v.destination + } + var zero T + return zero +} + +func (v *jsonValue[T]) String() string { + if v.destination != nil { + switch val := any(*v.destination).(type) { + case string: + return val + case bool: + return strconv.FormatBool(val) + case int: + return strconv.Itoa(val) + case float64: + return strconv.FormatFloat(val, 'g', -1, 64) + case time.Time: + return val.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", val) + } + } + var zero T + switch any(zero).(type) { + case string: + return "" + case bool: + return "false" + case int: + return "0" + case float64: + return "0" + case time.Time: + return "" + default: + return fmt.Sprintf("%v", zero) + } +} + +func (v *jsonValue[T]) IsBoolFlag() bool { + return v.config.SetValue != nil +} + +// JSONDateValueCreator is a specialized creator for date-only values +type JSONDateValueCreator struct{} + +func (c JSONDateValueCreator) Create(val time.Time, dest *time.Time, config JSONConfig) cli.Value { + *dest = val + return &jsonDateValue{ + destination: dest, + config: config, + } +} + +func (c JSONDateValueCreator) ToString(val time.Time) string { + return val.Format("2006-01-02") +} + +type jsonDateValue struct { + destination *time.Time + config JSONConfig +} + +func (v *jsonDateValue) Set(val string) error { + // Try date-only formats first, then fall back to datetime formats + formats := []string{ + "2006-01-02", + "01/02/2006", + "Jan 2, 2006", + "January 2, 2006", + "2-Jan-2006", + time.RFC3339, + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + } + + var timeVal time.Time + var parseErr error + for _, format := range formats { + timeVal, parseErr = time.Parse(format, val) + if parseErr == nil { + break + } + } + if parseErr != nil { + return fmt.Errorf("invalid date value %q: %w", val, parseErr) + } + + *v.destination = timeVal + globalRegistry.Mutate(v.config.Kind, v.config.Path, timeVal.Format("2006-01-02")) + return nil +} + +func (v *jsonDateValue) Get() any { + if v.destination != nil { + return *v.destination + } + return time.Time{} +} + +func (v *jsonDateValue) String() string { + if v.destination != nil { + return v.destination.Format("2006-01-02") + } + return "" +} + +func (v *jsonDateValue) IsBoolFlag() bool { + return false +} + +type JSONStringFlag = cli.FlagBase[string, JSONConfig, JSONValueCreator[string]] +type JSONBoolFlag = cli.FlagBase[bool, JSONConfig, JSONValueCreator[bool]] +type JSONIntFlag = cli.FlagBase[int, JSONConfig, JSONValueCreator[int]] +type JSONFloatFlag = cli.FlagBase[float64, JSONConfig, JSONValueCreator[float64]] +type JSONDatetimeFlag = cli.FlagBase[time.Time, JSONConfig, JSONValueCreator[time.Time]] +type JSONDateFlag = cli.FlagBase[time.Time, JSONConfig, JSONDateValueCreator] +type JSONAnyFlag = cli.FlagBase[any, JSONConfig, JSONValueCreator[any]] diff --git a/pkg/jsonflag/mutation.go b/pkg/jsonflag/mutation.go new file mode 100644 index 0000000..46c115b --- /dev/null +++ b/pkg/jsonflag/mutation.go @@ -0,0 +1,104 @@ +package jsonflag + +import ( + "fmt" + "strconv" + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +type MutationKind string + +const ( + Body MutationKind = "body" + Query MutationKind = "query" + Header MutationKind = "header" +) + +type Mutation struct { + Kind MutationKind + Path string + Value any +} + +type registry struct { + mutations []Mutation +} + +var globalRegistry = ®istry{} + +func (r *registry) Mutate(kind MutationKind, path string, value any) { + r.mutations = append(r.mutations, Mutation{ + Kind: kind, + Path: path, + Value: value, + }) +} + +func (r *registry) Apply(body, query, header []byte) ([]byte, []byte, []byte, error) { + var err error + + for _, mutation := range r.mutations { + switch mutation.Kind { + case Body: + body, err = jsonSet(body, mutation.Path, mutation.Value) + case Query: + query, err = jsonSet(query, mutation.Path, mutation.Value) + case Header: + header, err = jsonSet(header, mutation.Path, mutation.Value) + } + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to apply mutation %s.%s: %w", mutation.Kind, mutation.Path, err) + } + } + + return body, query, header, nil +} + +func (r *registry) Clear() { + r.mutations = nil +} + +func (r *registry) List() []Mutation { + result := make([]Mutation, len(r.mutations)) + copy(result, r.mutations) + return result +} + +// Mutate adds a mutation that will be applied to the specified kind of data +func Mutate(kind MutationKind, path string, value any) { + globalRegistry.Mutate(kind, path, value) +} + +// ApplyMutations applies all registered mutations to the provided JSON data +func ApplyMutations(body, query, header []byte) ([]byte, []byte, []byte, error) { + return globalRegistry.Apply(body, query, header) +} + +// ClearMutations removes all registered mutations from the global registry +func ClearMutations() { + globalRegistry.Clear() +} + +// ListMutations returns a copy of all currently registered mutations +func ListMutations() []Mutation { + return globalRegistry.List() +} + +func jsonSet(json []byte, path string, value any) ([]byte, error) { + keys := strings.Split(path, ".") + path = "" + for _, key := range keys { + if key == "#" { + key = strconv.Itoa(len(gjson.GetBytes(json, path).Array()) - 1) + } + + if len(path) > 0 { + path += "." + } + path += key + } + return sjson.SetBytes(json, path, value) +} diff --git a/pkg/jsonflag/mutation_test.go b/pkg/jsonflag/mutation_test.go new file mode 100644 index 0000000..e87e518 --- /dev/null +++ b/pkg/jsonflag/mutation_test.go @@ -0,0 +1,37 @@ +package jsonflag + +import ( + "testing" +) + +func TestApply(t *testing.T) { + ClearMutations() + + Mutate(Body, "name", "test") + Mutate(Query, "page", 1) + Mutate(Header, "authorization", "Bearer token") + + body, query, header, err := ApplyMutations( + []byte(`{}`), + []byte(`{}`), + []byte(`{}`), + ) + + if err != nil { + t.Fatalf("Failed to apply mutations: %v", err) + } + + expectedBody := `{"name":"test"}` + expectedQuery := `{"page":1}` + expectedHeader := `{"authorization":"Bearer token"}` + + if string(body) != expectedBody { + t.Errorf("Body mismatch. Expected: %s, Got: %s", expectedBody, string(body)) + } + if string(query) != expectedQuery { + t.Errorf("Query mismatch. Expected: %s, Got: %s", expectedQuery, string(query)) + } + if string(header) != expectedHeader { + t.Errorf("Header mismatch. Expected: %s, Got: %s", expectedHeader, string(header)) + } +} From 32b38c75d9fc8e62a1163680d36e1cc4d9da7a49 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 05:26:19 +0000 Subject: [PATCH 3/5] chore(internal): codegen related update --- go.mod | 2 +- pkg/cmd/util.go | 239 -------------------------------- pkg/jsonflag/json_flag.go | 248 ---------------------------------- pkg/jsonflag/mutation.go | 104 -------------- pkg/jsonflag/mutation_test.go | 37 ----- 5 files changed, 1 insertion(+), 629 deletions(-) delete mode 100644 pkg/cmd/util.go delete mode 100644 pkg/jsonflag/json_flag.go delete mode 100644 pkg/jsonflag/mutation.go delete mode 100644 pkg/jsonflag/mutation_test.go diff --git a/go.mod b/go.mod index 1fb7a42..d731790 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/onkernel/hypeman-go v0.8.0 github.com/tidwall/gjson v1.18.0 github.com/tidwall/pretty v1.2.1 - 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 @@ -62,6 +61,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/vbatts/tar-split v0.12.2 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go deleted file mode 100644 index d2f7de7..0000000 --- a/pkg/cmd/util.go +++ /dev/null @@ -1,239 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/http/httputil" - "os" - "reflect" - "strings" - - "golang.org/x/term" - - "github.com/onkernel/hypeman-cli/pkg/jsonview" - "github.com/onkernel/hypeman-go/option" - - "github.com/itchyny/json2yaml" - "github.com/tidwall/gjson" - "github.com/tidwall/pretty" - "github.com/tidwall/sjson" - "github.com/urfave/cli/v3" -) - -func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { - opts := []option.RequestOption{ - option.WithHeader("User-Agent", fmt.Sprintf("Hypeman/CLI %s", Version)), - option.WithHeader("X-Stainless-Lang", "cli"), - option.WithHeader("X-Stainless-Package-Version", Version), - option.WithHeader("X-Stainless-Runtime", "cli"), - option.WithHeader("X-Stainless-CLI-Command", cmd.FullName()), - } - - // Override base URL if the --base-url flag is provided - if baseURL := cmd.String("base-url"); baseURL != "" { - opts = append(opts, option.WithBaseURL(baseURL)) - } - - return opts -} - -type fileReader struct { - Value io.Reader - Base64Encoded bool -} - -func (f *fileReader) Set(filename string) error { - reader, err := os.Open(filename) - if err != nil { - return fmt.Errorf("failed to read file %q: %w", filename, err) - } - f.Value = reader - return nil -} - -func (f *fileReader) String() string { - if f.Value == nil { - return "" - } - buf := new(bytes.Buffer) - buf.ReadFrom(f.Value) - if f.Base64Encoded { - return base64.StdEncoding.EncodeToString(buf.Bytes()) - } - return buf.String() -} - -func (f *fileReader) Get() any { - return f.String() -} - -func unmarshalWithReaders(data []byte, v any) error { - var fields map[string]json.RawMessage - if err := json.Unmarshal(data, &fields); err != nil { - return err - } - - rv := reflect.ValueOf(v).Elem() - rt := rv.Type() - - for i := 0; i < rv.NumField(); i++ { - fv := rv.Field(i) - ft := rt.Field(i) - - jsonKey := ft.Tag.Get("json") - if jsonKey == "" { - jsonKey = ft.Name - } else if idx := strings.Index(jsonKey, ","); idx != -1 { - jsonKey = jsonKey[:idx] - } - - rawVal, ok := fields[jsonKey] - if !ok { - continue - } - - if ft.Type == reflect.TypeOf((*io.Reader)(nil)).Elem() { - var s string - if err := json.Unmarshal(rawVal, &s); err != nil { - return fmt.Errorf("field %s: %w", ft.Name, err) - } - fv.Set(reflect.ValueOf(strings.NewReader(s))) - } else { - ptr := fv.Addr().Interface() - if err := json.Unmarshal(rawVal, ptr); err != nil { - return fmt.Errorf("field %s: %w", ft.Name, err) - } - } - } - - return nil -} - -func unmarshalStdinWithFlags(cmd *cli.Command, flags map[string]string, target any) error { - var data []byte - if isInputPiped() { - var err error - if data, err = io.ReadAll(os.Stdin); err != nil { - return err - } - } - - // Merge CLI flags into the body - for flag, path := range flags { - if cmd.IsSet(flag) { - var err error - data, err = sjson.SetBytes(data, path, cmd.Value(flag)) - if err != nil { - return err - } - } - } - - if data != nil { - if err := unmarshalWithReaders(data, target); err != nil { - return fmt.Errorf("failed to unmarshal JSON: %w", err) - } - } - - return nil -} - -func debugMiddleware(debug bool) option.Middleware { - return func(r *http.Request, mn option.MiddlewareNext) (*http.Response, error) { - if debug { - logger := log.Default() - - if reqBytes, err := httputil.DumpRequest(r, true); err == nil { - logger.Printf("Request Content:\n%s\n", reqBytes) - } - - resp, err := mn(r) - if err != nil { - return resp, err - } - - if respBytes, err := httputil.DumpResponse(resp, true); err == nil { - logger.Printf("Response Content:\n%s\n", respBytes) - } - - return resp, err - } - - return mn(r) - } -} - -func isInputPiped() bool { - stat, _ := os.Stdin.Stat() - return (stat.Mode() & os.ModeCharDevice) == 0 -} - -func isTerminal(w io.Writer) bool { - switch v := w.(type) { - case *os.File: - return term.IsTerminal(int(v.Fd())) - default: - return false - } -} - -func shouldUseColors(w io.Writer) bool { - force, ok := os.LookupEnv("FORCE_COLOR") - - if ok { - if force == "1" { - return true - } - if force == "0" { - return false - } - } - - return isTerminal(w) -} - -func ShowJSON(title string, res gjson.Result, format string, transform string) error { - if format != "raw" && transform != "" { - transformed := res.Get(transform) - if transformed.Exists() { - res = transformed - } - } - switch strings.ToLower(format) { - case "auto": - return ShowJSON(title, res, "json", "") - case "explore": - return jsonview.ExploreJSON(title, res) - case "pretty": - jsonview.DisplayJSON(title, res) - return nil - case "json": - prettyJSON := pretty.Pretty([]byte(res.Raw)) - if shouldUseColors(os.Stdout) { - fmt.Print(string(pretty.Color(prettyJSON, pretty.TerminalStyle))) - } else { - fmt.Print(string(prettyJSON)) - } - return nil - case "raw": - fmt.Println(res.Raw) - return nil - case "yaml": - input := strings.NewReader(res.Raw) - var yaml strings.Builder - if err := json2yaml.Convert(&yaml, input); err != nil { - return err - } - fmt.Print(yaml.String()) - return nil - default: - return fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", ")) - } -} diff --git a/pkg/jsonflag/json_flag.go b/pkg/jsonflag/json_flag.go deleted file mode 100644 index 605f883..0000000 --- a/pkg/jsonflag/json_flag.go +++ /dev/null @@ -1,248 +0,0 @@ -package jsonflag - -import ( - "fmt" - "strconv" - "time" - - "github.com/urfave/cli/v3" -) - -type JSONConfig struct { - Kind MutationKind - Path string - // For boolean flags that set a specific value when present - SetValue any -} - -type JSONValueCreator[T any] struct{} - -func (c JSONValueCreator[T]) Create(val T, dest *T, config JSONConfig) cli.Value { - *dest = val - return &jsonValue[T]{ - destination: dest, - config: config, - } -} - -func (c JSONValueCreator[T]) ToString(val T) string { - switch v := any(val).(type) { - case string: - if v == "" { - return v - } - return fmt.Sprintf("%q", v) - case bool: - return strconv.FormatBool(v) - case int: - return strconv.Itoa(v) - case float64: - return strconv.FormatFloat(v, 'g', -1, 64) - case time.Time: - return v.Format(time.RFC3339) - default: - return fmt.Sprintf("%v", v) - } -} - -type jsonValue[T any] struct { - destination *T - config JSONConfig -} - -func (v *jsonValue[T]) Set(val string) error { - var parsed T - var err error - - // If SetValue is configured, use that value instead of parsing the input - if v.config.SetValue != nil { - // For boolean flags with SetValue, register the configured value - if _, isBool := any(parsed).(bool); isBool { - globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) - *v.destination = any(true).(T) // Set the flag itself to true - return nil - } - // For any flags with SetValue, register the configured value - globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) - *v.destination = any(v.config.SetValue).(T) - return nil - } - - switch any(parsed).(type) { - case string: - parsed = any(val).(T) - case bool: - boolVal, parseErr := strconv.ParseBool(val) - if parseErr != nil { - return fmt.Errorf("invalid boolean value %q: %w", val, parseErr) - } - parsed = any(boolVal).(T) - case int: - intVal, parseErr := strconv.Atoi(val) - if parseErr != nil { - return fmt.Errorf("invalid integer value %q: %w", val, parseErr) - } - parsed = any(intVal).(T) - case float64: - floatVal, parseErr := strconv.ParseFloat(val, 64) - if parseErr != nil { - return fmt.Errorf("invalid float value %q: %w", val, parseErr) - } - parsed = any(floatVal).(T) - case time.Time: - // Try common datetime formats - formats := []string{ - time.RFC3339, - "2006-01-02T15:04:05Z07:00", - "2006-01-02T15:04:05", - "2006-01-02 15:04:05", - "2006-01-02", - "15:04:05", - "15:04", - } - var timeVal time.Time - var parseErr error - for _, format := range formats { - timeVal, parseErr = time.Parse(format, val) - if parseErr == nil { - break - } - } - if parseErr != nil { - return fmt.Errorf("invalid datetime value %q: %w", val, parseErr) - } - parsed = any(timeVal).(T) - case any: - // For `any`, store the string value directly - parsed = any(val).(T) - default: - return fmt.Errorf("unsupported type for JSON flag") - } - - *v.destination = parsed - globalRegistry.Mutate(v.config.Kind, v.config.Path, parsed) - return err -} - -func (v *jsonValue[T]) Get() any { - if v.destination != nil { - return *v.destination - } - var zero T - return zero -} - -func (v *jsonValue[T]) String() string { - if v.destination != nil { - switch val := any(*v.destination).(type) { - case string: - return val - case bool: - return strconv.FormatBool(val) - case int: - return strconv.Itoa(val) - case float64: - return strconv.FormatFloat(val, 'g', -1, 64) - case time.Time: - return val.Format(time.RFC3339) - default: - return fmt.Sprintf("%v", val) - } - } - var zero T - switch any(zero).(type) { - case string: - return "" - case bool: - return "false" - case int: - return "0" - case float64: - return "0" - case time.Time: - return "" - default: - return fmt.Sprintf("%v", zero) - } -} - -func (v *jsonValue[T]) IsBoolFlag() bool { - return v.config.SetValue != nil -} - -// JSONDateValueCreator is a specialized creator for date-only values -type JSONDateValueCreator struct{} - -func (c JSONDateValueCreator) Create(val time.Time, dest *time.Time, config JSONConfig) cli.Value { - *dest = val - return &jsonDateValue{ - destination: dest, - config: config, - } -} - -func (c JSONDateValueCreator) ToString(val time.Time) string { - return val.Format("2006-01-02") -} - -type jsonDateValue struct { - destination *time.Time - config JSONConfig -} - -func (v *jsonDateValue) Set(val string) error { - // Try date-only formats first, then fall back to datetime formats - formats := []string{ - "2006-01-02", - "01/02/2006", - "Jan 2, 2006", - "January 2, 2006", - "2-Jan-2006", - time.RFC3339, - "2006-01-02T15:04:05Z07:00", - "2006-01-02T15:04:05", - "2006-01-02 15:04:05", - } - - var timeVal time.Time - var parseErr error - for _, format := range formats { - timeVal, parseErr = time.Parse(format, val) - if parseErr == nil { - break - } - } - if parseErr != nil { - return fmt.Errorf("invalid date value %q: %w", val, parseErr) - } - - *v.destination = timeVal - globalRegistry.Mutate(v.config.Kind, v.config.Path, timeVal.Format("2006-01-02")) - return nil -} - -func (v *jsonDateValue) Get() any { - if v.destination != nil { - return *v.destination - } - return time.Time{} -} - -func (v *jsonDateValue) String() string { - if v.destination != nil { - return v.destination.Format("2006-01-02") - } - return "" -} - -func (v *jsonDateValue) IsBoolFlag() bool { - return false -} - -type JSONStringFlag = cli.FlagBase[string, JSONConfig, JSONValueCreator[string]] -type JSONBoolFlag = cli.FlagBase[bool, JSONConfig, JSONValueCreator[bool]] -type JSONIntFlag = cli.FlagBase[int, JSONConfig, JSONValueCreator[int]] -type JSONFloatFlag = cli.FlagBase[float64, JSONConfig, JSONValueCreator[float64]] -type JSONDatetimeFlag = cli.FlagBase[time.Time, JSONConfig, JSONValueCreator[time.Time]] -type JSONDateFlag = cli.FlagBase[time.Time, JSONConfig, JSONDateValueCreator] -type JSONAnyFlag = cli.FlagBase[any, JSONConfig, JSONValueCreator[any]] diff --git a/pkg/jsonflag/mutation.go b/pkg/jsonflag/mutation.go deleted file mode 100644 index 46c115b..0000000 --- a/pkg/jsonflag/mutation.go +++ /dev/null @@ -1,104 +0,0 @@ -package jsonflag - -import ( - "fmt" - "strconv" - "strings" - - "github.com/tidwall/gjson" - "github.com/tidwall/sjson" -) - -type MutationKind string - -const ( - Body MutationKind = "body" - Query MutationKind = "query" - Header MutationKind = "header" -) - -type Mutation struct { - Kind MutationKind - Path string - Value any -} - -type registry struct { - mutations []Mutation -} - -var globalRegistry = ®istry{} - -func (r *registry) Mutate(kind MutationKind, path string, value any) { - r.mutations = append(r.mutations, Mutation{ - Kind: kind, - Path: path, - Value: value, - }) -} - -func (r *registry) Apply(body, query, header []byte) ([]byte, []byte, []byte, error) { - var err error - - for _, mutation := range r.mutations { - switch mutation.Kind { - case Body: - body, err = jsonSet(body, mutation.Path, mutation.Value) - case Query: - query, err = jsonSet(query, mutation.Path, mutation.Value) - case Header: - header, err = jsonSet(header, mutation.Path, mutation.Value) - } - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to apply mutation %s.%s: %w", mutation.Kind, mutation.Path, err) - } - } - - return body, query, header, nil -} - -func (r *registry) Clear() { - r.mutations = nil -} - -func (r *registry) List() []Mutation { - result := make([]Mutation, len(r.mutations)) - copy(result, r.mutations) - return result -} - -// Mutate adds a mutation that will be applied to the specified kind of data -func Mutate(kind MutationKind, path string, value any) { - globalRegistry.Mutate(kind, path, value) -} - -// ApplyMutations applies all registered mutations to the provided JSON data -func ApplyMutations(body, query, header []byte) ([]byte, []byte, []byte, error) { - return globalRegistry.Apply(body, query, header) -} - -// ClearMutations removes all registered mutations from the global registry -func ClearMutations() { - globalRegistry.Clear() -} - -// ListMutations returns a copy of all currently registered mutations -func ListMutations() []Mutation { - return globalRegistry.List() -} - -func jsonSet(json []byte, path string, value any) ([]byte, error) { - keys := strings.Split(path, ".") - path = "" - for _, key := range keys { - if key == "#" { - key = strconv.Itoa(len(gjson.GetBytes(json, path).Array()) - 1) - } - - if len(path) > 0 { - path += "." - } - path += key - } - return sjson.SetBytes(json, path, value) -} diff --git a/pkg/jsonflag/mutation_test.go b/pkg/jsonflag/mutation_test.go deleted file mode 100644 index e87e518..0000000 --- a/pkg/jsonflag/mutation_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package jsonflag - -import ( - "testing" -) - -func TestApply(t *testing.T) { - ClearMutations() - - Mutate(Body, "name", "test") - Mutate(Query, "page", 1) - Mutate(Header, "authorization", "Bearer token") - - body, query, header, err := ApplyMutations( - []byte(`{}`), - []byte(`{}`), - []byte(`{}`), - ) - - if err != nil { - t.Fatalf("Failed to apply mutations: %v", err) - } - - expectedBody := `{"name":"test"}` - expectedQuery := `{"page":1}` - expectedHeader := `{"authorization":"Bearer token"}` - - if string(body) != expectedBody { - t.Errorf("Body mismatch. Expected: %s, Got: %s", expectedBody, string(body)) - } - if string(query) != expectedQuery { - t.Errorf("Query mismatch. Expected: %s, Got: %s", expectedQuery, string(query)) - } - if string(header) != expectedHeader { - t.Errorf("Header mismatch. Expected: %s, Got: %s", expectedHeader, string(header)) - } -} From fb7c6f804b899409310ae310cb7bb0cc888b5c49 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:04:11 +0000 Subject: [PATCH 4/5] feat: Start and Stop VM From 0c2a9981ecfe065043e72d4c4d99ac4f7ef5c5ca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:37:40 +0000 Subject: [PATCH 5/5] release: 0.8.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 19 +++++++++++++++++++ pkg/cmd/version.go | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1b77f50..6538ca9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.0" + ".": "0.8.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e1e0a6b..e043d03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.8.0 (2025-12-23) + +Full Changelog: [v0.7.0...v0.8.0](https://github.com/onkernel/hypeman-cli/compare/v0.7.0...v0.8.0) + +### Features + +* Start and Stop VM ([fb7c6f8](https://github.com/onkernel/hypeman-cli/commit/fb7c6f804b899409310ae310cb7bb0cc888b5c49)) + + +### Bug Fixes + +* **mcp:** correct code tool API endpoint ([7c0094d](https://github.com/onkernel/hypeman-cli/commit/7c0094d0b5ea9e3b6b116d8aa2de5fa9d01a069f)) + + +### Chores + +* **internal:** codegen related update ([32b38c7](https://github.com/onkernel/hypeman-cli/commit/32b38c75d9fc8e62a1163680d36e1cc4d9da7a49)) +* **internal:** codegen related update ([51bb415](https://github.com/onkernel/hypeman-cli/commit/51bb415b4d8365514963a436339f4aaf8334beff)) + ## 0.7.0 (2025-12-23) Full Changelog: [v0.6.1...v0.7.0](https://github.com/onkernel/hypeman-cli/compare/v0.6.1...v0.7.0) diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index f37c8ca..f838a39 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.7.0" // x-release-please-version +const Version = "0.8.0" // x-release-please-version