Skip to content
Open
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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ require (
github.com/google/go-containerregistry v0.20.7
github.com/gorilla/websocket v1.5.3
github.com/itchyny/json2yaml v0.1.4
github.com/kernel/hypeman-go v0.11.0
github.com/kernel/hypeman-go v0.13.0
github.com/knadh/koanf/parsers/yaml v1.1.0
github.com/knadh/koanf/providers/env v1.1.0
github.com/knadh/koanf/providers/file v1.2.1
github.com/knadh/koanf/v2 v2.3.2
github.com/muesli/reflow v0.3.0
Expand Down Expand Up @@ -52,7 +53,6 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/knadh/koanf/providers/env v1.1.0 // 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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8=
github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI=
github.com/kernel/hypeman-go v0.11.0 h1:hCXNUHtrhGKswJapzyWyozBOXhKK/oreKvm0AXHuE6c=
github.com/kernel/hypeman-go v0.11.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/kernel/hypeman-go v0.13.0 h1:5GIeSkQ9BIkL+wEJnhsPmsJzuKof6zZmqcTWK67+Kcc=
github.com/kernel/hypeman-go v0.13.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
Expand Down
14 changes: 14 additions & 0 deletions pkg/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ Examples:
Name: "image-name",
Usage: `Custom image name for the build output (pushed to {registry}/{image_name} instead of {registry}/builds/{id})`,
},
&cli.IntFlag{
Name: "cpus",
Usage: "Number of vCPUs for builder VM (default 2)",
},
&cli.IntFlag{
Name: "memory",
Usage: "Memory limit for builder VM in MB (default 2048)",
},
},
Commands: []*cli.Command{
&buildListCmd,
Expand Down Expand Up @@ -172,6 +180,12 @@ func handleBuild(ctx context.Context, cmd *cli.Command) error {
if v := cmd.String("image-name"); v != "" {
params.ImageName = hypeman.Opt(v)
}
if cmd.IsSet("cpus") {
params.CPUs = hypeman.Opt(int64(cmd.Int("cpus")))
}
if cmd.IsSet("memory") {
params.MemoryMB = hypeman.Opt(int64(cmd.Int("memory")))
}

// Start build
build, err := client.Builds.New(ctx, params, opts...)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func randomSuffix(n int) string {
// Returns an error if the identifier is ambiguous or not found.
func ResolveInstance(ctx context.Context, client *hypeman.Client, identifier string) (string, error) {
// List all instances
instances, err := client.Instances.List(ctx)
instances, err := client.Instances.List(ctx, hypeman.InstanceListParams{})
if err != nil {
return "", fmt.Errorf("failed to list instances: %w", err)
}
Expand Down
32 changes: 30 additions & 2 deletions pkg/cmd/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strings"

"github.com/kernel/hypeman-go"
"github.com/kernel/hypeman-go/option"
Expand All @@ -24,6 +25,14 @@ var psCmd = cli.Command{
Aliases: []string{"q"},
Usage: "Only display instance IDs",
},
&cli.StringFlag{
Name: "state",
Usage: "Filter instances by state (e.g., Running, Stopped, Standby)",
},
&cli.StringSliceFlag{
Name: "metadata",
Usage: "Filter by metadata key-value pair (KEY=VALUE, can be repeated)",
},
},
Action: handlePs,
HideHelpCommand: true,
Expand All @@ -37,8 +46,26 @@ func handlePs(ctx context.Context, cmd *cli.Command) error {
opts = append(opts, debugMiddlewareOption)
}

params := hypeman.InstanceListParams{}

if state := cmd.String("state"); state != "" {
params.State = hypeman.InstanceListParamsState(state)
}

if metadataSpecs := cmd.StringSlice("metadata"); len(metadataSpecs) > 0 {
metadata := make(map[string]string)
for _, m := range metadataSpecs {
parts := strings.SplitN(m, "=", 2)
if len(parts) == 2 {
metadata[parts[0]] = parts[1]
}
}
params.Metadata = metadata
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Malformed metadata flags silently set empty map on params

Low Severity

If all --metadata values are malformed (missing =), the loop skips every entry but params.Metadata is still set to an empty map[string]string{}. An empty map may serialize differently than an omitted/nil field in the API request, potentially causing unexpected server-side behavior. The user also gets no feedback that their filter was invalid and silently ignored.

Fix in Cursor Fix in Web


instances, err := client.Instances.List(
ctx,
params,
opts...,
)
if err != nil {
Expand All @@ -47,11 +74,12 @@ func handlePs(ctx context.Context, cmd *cli.Command) error {

showAll := cmd.Bool("all")
quietMode := cmd.Bool("quiet")
stateFilter := cmd.String("state")

// Filter instances
// Filter instances client-side only when no server-side filter is active
var filtered []hypeman.Instance
for _, inst := range *instances {
if showAll || inst.State == "Running" {
if showAll || stateFilter != "" || inst.State == "Running" {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata filter results silently dropped by client-side filtering

High Severity

When --metadata is used without --state or --all, the server correctly filters by metadata, but the client-side filter at line 82 still only keeps instances where inst.State == "Running". This silently drops non-running instances that matched the metadata filter. The condition checks stateFilter != "" to bypass client-side filtering but doesn't account for the --metadata server-side filter being active. The comment on line 79 ("only when no server-side filter is active") confirms the intent, but metadata isn't included in the check.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading empty-result message when using --state filter

Medium Severity

When a user passes --state Stopped (or any non-Running state) and no instances match, the empty-results block at line 97 checks !showAll and prints "No running instances. Use -a to show all.". This message is incorrect and confusing — the user explicitly filtered by a specific state, so the message about "running instances" and suggesting -a doesn't apply. The condition needs to account for stateFilter being set.

Fix in Cursor Fix in Web

filtered = append(filtered, inst)
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func handleRm(ctx context.Context, cmd *cli.Command) error {
// If --all, get all instance IDs
var identifiers []string
if all {
instances, err := client.Instances.List(ctx)
instances, err := client.Instances.List(ctx, hypeman.InstanceListParams{})
if err != nil {
return fmt.Errorf("failed to list instances: %w", err)
}
Expand Down