Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.5.0"
".": "0.5.1"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 18
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-ce51f144a3d2de556750203edbaa5bfeefe874660737c35a4fc37dfb30057dd5.yml
openapi_spec_hash: 27663b6503056317abcb578ac7b67c06
config_hash: b4e65d240d7bca1ba6162ee2098c8ac2
configured_endpoints: 22
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-da3f4038bb544acae375f44527f515dc58308f67822905258b155192041e65ed.yml
openapi_spec_hash: 4c7f6f453c20eda7fd8689e8917c65f9
config_hash: a7d0557c72de54fd6baded5b189777c3
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Changelog

## 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)

### Features

* **api:** manual updates ([a3f2ec1](https://github.com/onkernel/hypeman-cli/commit/a3f2ec15101a6afd6feb1da1addcb3a2589acb53))
* fix edge cases for sending request data and add YAML support ([3e740a9](https://github.com/onkernel/hypeman-cli/commit/3e740a94698f4704e79cc5c3b6434cbb1bfcb935))
* Ingress ([bfb79c5](https://github.com/onkernel/hypeman-cli/commit/bfb79c5a160a3b92cac3793ea49da49ddcc7c8c6))
* Initialize volume with data ([ef9997c](https://github.com/onkernel/hypeman-cli/commit/ef9997cc2c6d0fc14531bdf9d1238f3447e3a454))
* **push:** add hypeman push command for local image upload ([e120ec6](https://github.com/onkernel/hypeman-cli/commit/e120ec6d96531ab49909a3d55895f5fcc4d43dc2))
* respect HYPEMAN_BASE_URL environment variable ([17122d7](https://github.com/onkernel/hypeman-cli/commit/17122d7b2d6041c57d4e2d341b52f18697aef5d4))


### Bug Fixes

* fix for default flag values ([812e009](https://github.com/onkernel/hypeman-cli/commit/812e0091f73ab5e8992adab5ca1c2cef76b60c63))
* **run:** wait for image to be ready before creating instance ([048ee73](https://github.com/onkernel/hypeman-cli/commit/048ee7311c39d6c3c7efad9c662fa2a1993ced97))
* use correct user agent value ([580e468](https://github.com/onkernel/hypeman-cli/commit/580e468e95a11c8c57016954464039af3b0586f1))


### Chores

* add scripts ([c3e4955](https://github.com/onkernel/hypeman-cli/commit/c3e4955f932edc7567d929f22f3e93f22ae69e1a))
* update dependencies ([4ed31f6](https://github.com/onkernel/hypeman-cli/commit/4ed31f6294c1b94ef764bb7959dc99e89af62cfb))

## 0.5.0 (2025-11-26)

Full Changelog: [v0.4.0...v0.5.0](https://github.com/onkernel/hypeman-cli/compare/v0.4.0...v0.5.0)
Expand Down
16 changes: 6 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ require (
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/term v0.2.1
github.com/goccy/go-yaml v1.18.0
github.com/google/go-containerregistry v0.20.7
github.com/gorilla/websocket v1.5.3
github.com/itchyny/json2yaml v0.1.4
github.com/muesli/reflow v0.3.0
github.com/onkernel/hypeman-go v0.4.0
github.com/onkernel/hypeman-go v0.5.0
github.com/tidwall/gjson v1.18.0
github.com/tidwall/pretty v1.2.1
github.com/tidwall/sjson v1.2.5
Expand All @@ -21,10 +22,8 @@ require (
)

require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
Expand All @@ -43,14 +42,14 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
Expand All @@ -66,15 +65,12 @@ require (
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/net v0.47.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/protobuf v1.36.10 // 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
google.golang.org/grpc v1.75.1 // indirect
)
7 changes: 4 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
Expand Down Expand Up @@ -103,8 +105,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/onkernel/hypeman-go v0.4.0 h1:3zDpB/WOPhkdJ/Lug3DmBDUcGR5zHwpOTzLEbsO4AnE=
github.com/onkernel/hypeman-go v0.4.0/go.mod h1:pxRRFfVcLvafZpDD1O6IjwHnem3hKEuZTCClrnGiIKA=
github.com/onkernel/hypeman-go v0.5.0 h1:ILe+n18aN5MXx0ARxDJ/ZYqcX2MdfJqWrE4sn14gJ5I=
github.com/onkernel/hypeman-go v0.5.0/go.mod h1:BPT1yh0gbby1E+As/xLM3GVjw7752+2C5SaEiJV9rRc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
Expand Down Expand Up @@ -169,7 +171,6 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
227 changes: 227 additions & 0 deletions internal/apiform/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package apiform

import (
"fmt"
"io"
"mime/multipart"
"net/textproto"
"path"
"reflect"
"sort"
"strconv"
"strings"
)

// Marshal encodes a value as multipart form data using default settings
func Marshal(value any, writer *multipart.Writer) error {
e := &encoder{
format: FormatRepeat,
}
return e.marshal(value, writer)
}

// MarshalWithSettings encodes a value with custom array format
func MarshalWithSettings(value any, writer *multipart.Writer, arrayFormat FormFormat) error {
e := &encoder{
format: arrayFormat,
}
return e.marshal(value, writer)
}

type encoder struct {
format FormFormat
}

func (e *encoder) marshal(value any, writer *multipart.Writer) error {
val := reflect.ValueOf(value)
if !val.IsValid() {
return nil
}
return e.encodeValue("", val, writer)
}

func (e *encoder) encodeValue(key string, val reflect.Value, writer *multipart.Writer) error {
if !val.IsValid() {
return writer.WriteField(key, "")
}

t := val.Type()

if t.Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()) {
return e.encodeReader(key, val, writer)
}

switch t.Kind() {
case reflect.Pointer:
if val.IsNil() || !val.IsValid() {
return writer.WriteField(key, "")
}
return e.encodeValue(key, val.Elem(), writer)

case reflect.Slice, reflect.Array:
return e.encodeArray(key, val, writer)

case reflect.Map:
return e.encodeMap(key, val, writer)

case reflect.Interface:
if val.IsNil() {
return writer.WriteField(key, "")
}
return e.encodeValue(key, val.Elem(), writer)

case reflect.String:
return writer.WriteField(key, val.String())

case reflect.Bool:
if val.Bool() {
return writer.WriteField(key, "true")
}
return writer.WriteField(key, "false")

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return writer.WriteField(key, strconv.FormatInt(val.Int(), 10))

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return writer.WriteField(key, strconv.FormatUint(val.Uint(), 10))

case reflect.Float32:
return writer.WriteField(key, strconv.FormatFloat(val.Float(), 'f', -1, 32))

case reflect.Float64:
return writer.WriteField(key, strconv.FormatFloat(val.Float(), 'f', -1, 64))

default:
return fmt.Errorf("unknown type: %s", t.String())
}
}

func (e *encoder) encodeArray(key string, val reflect.Value, writer *multipart.Writer) error {
if e.format == FormatComma {
var values []string
for i := 0; i < val.Len(); i++ {
item := val.Index(i)
var strValue string
switch item.Kind() {
case reflect.String:
strValue = item.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
strValue = strconv.FormatInt(item.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
strValue = strconv.FormatUint(item.Uint(), 10)
case reflect.Float32, reflect.Float64:
strValue = strconv.FormatFloat(item.Float(), 'f', -1, 64)
case reflect.Bool:
strValue = strconv.FormatBool(item.Bool())
default:
return fmt.Errorf("comma format not supported for complex array elements")
}
values = append(values, strValue)
}
return writer.WriteField(key, strings.Join(values, ","))
}

for i := 0; i < val.Len(); i++ {
var formattedKey string
switch e.format {
case FormatRepeat:
formattedKey = key
case FormatBrackets:
formattedKey = key + "[]"
case FormatIndicesDots:
if key == "" {
formattedKey = strconv.Itoa(i)
} else {
formattedKey = key + "." + strconv.Itoa(i)
}
case FormatIndicesBrackets:
if key == "" {
formattedKey = strconv.Itoa(i)
} else {
formattedKey = key + "[" + strconv.Itoa(i) + "]"
}
default:
return fmt.Errorf("apiform: unsupported array format")
}

if err := e.encodeValue(formattedKey, val.Index(i), writer); err != nil {
return err
}
}
return nil
}

var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")

func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}

func (e *encoder) encodeReader(key string, val reflect.Value, writer *multipart.Writer) error {
reader, ok := val.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader)
if !ok {
return nil
}

// Set defaults
filename := "anonymous_file"
contentType := "application/octet-stream"

// Get filename if available
if named, ok := reader.(interface{ Filename() string }); ok {
filename = named.Filename()
} else if named, ok := reader.(interface{ Name() string }); ok {
filename = path.Base(named.Name())
}

// Get content type if available
if typed, ok := reader.(interface{ ContentType() string }); ok {
contentType = typed.ContentType()
}

h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
escapeQuotes(key), escapeQuotes(filename)))
h.Set("Content-Type", contentType)

filewriter, err := writer.CreatePart(h)
if err != nil {
return err
}
_, err = io.Copy(filewriter, reader)
return err
}

func (e *encoder) encodeMap(key string, val reflect.Value, writer *multipart.Writer) error {
type mapPair struct {
key string
value reflect.Value
}

if key != "" {
key = key + "."
}

// Collect and sort map entries for deterministic output
pairs := []mapPair{}
iter := val.MapRange()
for iter.Next() {
if iter.Key().Type().Kind() != reflect.String {
return fmt.Errorf("cannot encode a map with a non string key")
}
pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()})
}

sort.Slice(pairs, func(i, j int) bool {
return pairs[i].key < pairs[j].key
})

// Process sorted pairs
for _, p := range pairs {
if err := e.encodeValue(key+p.key, p.value, writer); err != nil {
return err
}
}

return nil
}
20 changes: 20 additions & 0 deletions internal/apiform/form.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package apiform

type Marshaler interface {
MarshalMultipart() ([]byte, string, error)
}

type FormFormat int

const (
// FormatRepeat represents arrays as repeated keys with the same value
FormatRepeat FormFormat = iota
// Comma-separated values 1,2,3
FormatComma
// FormatBrackets uses the key[] notation for arrays
FormatBrackets
// FormatIndicesDots uses key.0, key.1, etc. notation
FormatIndicesDots
// FormatIndicesBrackets uses key[0], key[1], etc. notation
FormatIndicesBrackets
)
Loading