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/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 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/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 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)) - } -}