From d9949130684a46853c613b4a60d3b0fa8ee1e171 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:07:43 +0000 Subject: [PATCH 1/8] CLI: Update Hypeman Go SDK to fae578c6868a3ad232e67a8bd323b9fb307451d3 Update hypeman-go dependency from v0.11.0 to v0.11.1. A full enumeration of SDK methods and CLI commands was performed. No coverage gaps were found - all SDK methods have corresponding CLI commands and all param fields have corresponding flags. Co-authored-by: Cursor --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index f591003..c28c31b 100644 --- a/go.mod +++ b/go.mod @@ -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.11.1 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 @@ -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 diff --git a/go.sum b/go.sum index 9c04859..2ec574e 100644 --- a/go.sum +++ b/go.sum @@ -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.11.1 h1:RtBrlyjQ3Mwv5ohUklklJmQV1kBdqqmQMfKaCl/aY7E= +github.com/kernel/hypeman-go v0.11.1/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= From ebaa09f9ac4a45e8919617877fcc6e83d8e75105 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:47:50 +0000 Subject: [PATCH 2/8] CLI: Update hypeman SDK to 40bbd485e7 and add --state/--metadata flags to ps - Updated hypeman-go to v0.12.0 (40bbd485e7a89cd21ae08554502e9dedf4999efc) - Added --state flag to `hypeman ps` for server-side state filtering - Added --metadata flag to `hypeman ps` for metadata key-value filtering - Updated Instances.List calls to pass InstanceListParams (new required param) Made-with: Cursor --- go.mod | 2 +- go.sum | 4 ++-- pkg/cmd/format.go | 2 +- pkg/cmd/ps.go | 32 ++++++++++++++++++++++++++++++-- pkg/cmd/rm.go | 2 +- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index c28c31b..8748ab0 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ 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.1 + github.com/kernel/hypeman-go v0.12.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 diff --git a/go.sum b/go.sum index 2ec574e..0d38dfe 100644 --- a/go.sum +++ b/go.sum @@ -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.1 h1:RtBrlyjQ3Mwv5ohUklklJmQV1kBdqqmQMfKaCl/aY7E= -github.com/kernel/hypeman-go v0.11.1/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= +github.com/kernel/hypeman-go v0.12.0 h1:++9kCmvbrmAYgbbOVCaTzsLPHVSut4faLC2YMWM8rQo= +github.com/kernel/hypeman-go v0.12.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= diff --git a/pkg/cmd/format.go b/pkg/cmd/format.go index 3aa3048..9716caa 100644 --- a/pkg/cmd/format.go +++ b/pkg/cmd/format.go @@ -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) } diff --git a/pkg/cmd/ps.go b/pkg/cmd/ps.go index 2a957f2..ce18050 100644 --- a/pkg/cmd/ps.go +++ b/pkg/cmd/ps.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/kernel/hypeman-go" "github.com/kernel/hypeman-go/option" @@ -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, @@ -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 + } + instances, err := client.Instances.List( ctx, + params, opts..., ) if err != nil { @@ -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" { filtered = append(filtered, inst) } } diff --git a/pkg/cmd/rm.go b/pkg/cmd/rm.go index 4d7884a..3066978 100644 --- a/pkg/cmd/rm.go +++ b/pkg/cmd/rm.go @@ -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) } From f66ded9144ae2c4d51254fe60ef1827adec1d393 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:52:31 +0000 Subject: [PATCH 3/8] CLI: Update hypeman SDK to 1f34cf2541 and add --cpus/--memory flags to build Update hypeman-go SDK from v0.12.0 to v0.13.0 (1f34cf2541337d9ec4a39f74581bba9cdcb1ec1b). Add missing CLI flags for BuildNewParams fields: - --cpus: Number of vCPUs for builder VM - --memory: Memory limit for builder VM in MB Made-with: Cursor --- go.mod | 2 +- go.sum | 4 ++-- pkg/cmd/build.go | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8748ab0..43b31c1 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ 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.12.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 diff --git a/go.sum b/go.sum index 0d38dfe..d8bb6c4 100644 --- a/go.sum +++ b/go.sum @@ -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.12.0 h1:++9kCmvbrmAYgbbOVCaTzsLPHVSut4faLC2YMWM8rQo= -github.com/kernel/hypeman-go v0.12.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= diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index 21deedb..a4c8c88 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -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, @@ -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...) From 25ff1e42dbeea47d085ce547da9dc111a6e4c1d3 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:53:22 +0000 Subject: [PATCH 4/8] CLI: Update Hypeman Go SDK to 9cd8f5ca0926682c2f1c1148adf3f5d1e289b7f6 Update hypeman-go dependency from v0.13.0 to v0.14.0. Full coverage analysis performed: all SDK methods have corresponding CLI commands and all param fields have corresponding CLI flags. No coverage gaps found. Made-with: Cursor --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 43b31c1..3df0e42 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ 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.13.0 + github.com/kernel/hypeman-go v0.14.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 diff --git a/go.sum b/go.sum index d8bb6c4..e2aa3a3 100644 --- a/go.sum +++ b/go.sum @@ -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.13.0 h1:5GIeSkQ9BIkL+wEJnhsPmsJzuKof6zZmqcTWK67+Kcc= -github.com/kernel/hypeman-go v0.13.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= +github.com/kernel/hypeman-go v0.14.0 h1:FeeVJly5TzkAYZdxuSfn/8Sz5qZZtUlPQQvUQOAOhg4= +github.com/kernel/hypeman-go v0.14.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= From cf007008a39b33bc97a7d27c3bafeaaec622eae9 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Tue, 3 Mar 2026 11:09:42 -0500 Subject: [PATCH 5/8] fix ps filtering and metadata handling --- pkg/cmd/ps.go | 45 ++++++++++++++++++++++++++++++--------------- pkg/cmd/ps_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/ps.go b/pkg/cmd/ps.go index ce18050..739d6fb 100644 --- a/pkg/cmd/ps.go +++ b/pkg/cmd/ps.go @@ -47,20 +47,17 @@ func handlePs(ctx context.Context, cmd *cli.Command) error { } params := hypeman.InstanceListParams{} - - if state := cmd.String("state"); state != "" { - params.State = hypeman.InstanceListParamsState(state) + stateFilter := cmd.String("state") + if stateFilter != "" { + params.State = hypeman.InstanceListParamsState(stateFilter) } - 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 + metadataFilters, malformedMetadata := parseMetadataFilters(cmd.StringSlice("metadata")) + for _, malformed := range malformedMetadata { + fmt.Fprintf(os.Stderr, "Warning: ignoring malformed metadata filter: %s\n", malformed) + } + if len(metadataFilters) > 0 { + params.Metadata = metadataFilters } instances, err := client.Instances.List( @@ -74,12 +71,12 @@ func handlePs(ctx context.Context, cmd *cli.Command) error { showAll := cmd.Bool("all") quietMode := cmd.Bool("quiet") - stateFilter := cmd.String("state") + serverSideFilterActive := stateFilter != "" || len(metadataFilters) > 0 // Filter instances client-side only when no server-side filter is active var filtered []hypeman.Instance for _, inst := range *instances { - if showAll || stateFilter != "" || inst.State == "Running" { + if showAll || serverSideFilterActive || inst.State == "Running" { filtered = append(filtered, inst) } } @@ -94,7 +91,7 @@ func handlePs(ctx context.Context, cmd *cli.Command) error { // Table output if len(filtered) == 0 { - if !showAll { + if !showAll && !serverSideFilterActive { fmt.Fprintln(os.Stderr, "No running instances. Use -a to show all.") } return nil @@ -138,6 +135,8 @@ func formatHypervisor(hv hypeman.InstanceHypervisor) string { return "ch" case hypeman.InstanceHypervisorQemu: return "qemu" + case hypeman.InstanceHypervisorFirecracker: + return "fc" case hypeman.InstanceHypervisorVz: return "vz" default: @@ -147,3 +146,19 @@ func formatHypervisor(hv hypeman.InstanceHypervisor) string { return string(hv) } } + +func parseMetadataFilters(specs []string) (map[string]string, []string) { + metadata := make(map[string]string) + var malformed []string + + for _, spec := range specs { + parts := strings.SplitN(spec, "=", 2) + if len(parts) != 2 || parts[0] == "" { + malformed = append(malformed, spec) + continue + } + metadata[parts[0]] = parts[1] + } + + return metadata, malformed +} diff --git a/pkg/cmd/ps_test.go b/pkg/cmd/ps_test.go index 3e48553..65b8509 100644 --- a/pkg/cmd/ps_test.go +++ b/pkg/cmd/ps_test.go @@ -5,6 +5,7 @@ import ( "github.com/kernel/hypeman-go" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFormatGPU(t *testing.T) { @@ -59,6 +60,11 @@ func TestFormatHypervisor(t *testing.T) { hypervisor: hypeman.InstanceHypervisorQemu, expected: "qemu", }, + { + name: "firecracker", + hypervisor: hypeman.InstanceHypervisorFirecracker, + expected: "fc", + }, { name: "empty defaults to ch", hypervisor: "", @@ -78,3 +84,36 @@ func TestFormatHypervisor(t *testing.T) { }) } } + +func TestParseMetadataFilters(t *testing.T) { + t.Run("parses valid entries", func(t *testing.T) { + metadata, malformed := parseMetadataFilters([]string{ + "team=backend", + "env=staging", + }) + + require.Empty(t, malformed) + assert.Equal(t, map[string]string{ + "team": "backend", + "env": "staging", + }, metadata) + }) + + t.Run("returns malformed entries and only valid metadata", func(t *testing.T) { + metadata, malformed := parseMetadataFilters([]string{ + "team=backend", + "missing-delimiter", + "=empty-key", + "region=us-east-1", + }) + + assert.Equal(t, map[string]string{ + "team": "backend", + "region": "us-east-1", + }, metadata) + assert.Equal(t, []string{ + "missing-delimiter", + "=empty-key", + }, malformed) + }) +} From d39f873563aab5326186a360b7ba5884d18c082a Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Tue, 3 Mar 2026 11:11:19 -0500 Subject: [PATCH 6/8] add inspect and fork commands with sdk-based calls --- pkg/cmd/cmd.go | 42 ++++++++-------- pkg/cmd/fork.go | 113 +++++++++++++++++++++++++++++++++++++++++++ pkg/cmd/fork_test.go | 56 +++++++++++++++++++++ pkg/cmd/inspect.go | 52 ++++++++++++++++++++ pkg/cmd/run.go | 6 ++- 5 files changed, 247 insertions(+), 22 deletions(-) create mode 100644 pkg/cmd/fork.go create mode 100644 pkg/cmd/fork_test.go create mode 100644 pkg/cmd/inspect.go diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index e2614ca..693dfc8 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -67,26 +67,28 @@ func init() { Usage: "The GJSON transformation for errors.", }, }, - Commands: []*cli.Command{ - &buildCmd, - &execCmd, - &cpCmd, - &pullCmd, - &pushCmd, - &runCmd, - &psCmd, - &logsCmd, - &rmCmd, - &stopCmd, - &startCmd, - &standbyCmd, - &restoreCmd, - &imageCmd, - &ingressCmd, - &volumeCmd, - &resourcesCmd, - &deviceCmd, - { + Commands: []*cli.Command{ + &buildCmd, + &execCmd, + &cpCmd, + &pullCmd, + &pushCmd, + &runCmd, + &psCmd, + &inspectCmd, + &logsCmd, + &rmCmd, + &stopCmd, + &startCmd, + &standbyCmd, + &restoreCmd, + &forkCmd, + &imageCmd, + &ingressCmd, + &volumeCmd, + &resourcesCmd, + &deviceCmd, + { Name: "@manpages", Usage: "Generate documentation for 'man'", UsageText: "hypeman @manpages [-o hypeman.1] [--gzip]", diff --git a/pkg/cmd/fork.go b/pkg/cmd/fork.go new file mode 100644 index 0000000..7a98657 --- /dev/null +++ b/pkg/cmd/fork.go @@ -0,0 +1,113 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var forkCmd = cli.Command{ + Name: "fork", + Usage: "Fork an instance into a new instance", + ArgsUsage: " ", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "from-running", + Usage: "Allow forking from a running source by doing standby -> fork -> restore", + }, + &cli.StringFlag{ + Name: "target-state", + Usage: "Target state for the forked instance: Stopped, Standby, or Running", + }, + }, + Action: handleFork, + HideHelpCommand: true, +} + +func handleFork(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 2 { + return fmt.Errorf("source instance and target name required\nUsage: hypeman fork [flags] ") + } + + source := args[0] + targetName := args[1] + + targetState, err := normalizeForkTargetState(cmd.String("target-state")) + if err != nil { + return err + } + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + sourceID, err := ResolveInstance(ctx, &client, source) + if err != nil { + return err + } + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + params := instanceForkParams{ + Name: targetName, + } + if cmd.IsSet("from-running") { + fromRunning := cmd.Bool("from-running") + params.FromRunning = &fromRunning + } + if targetState != "" { + params.TargetState = &targetState + } + + fmt.Fprintf(os.Stderr, "Forking %s to %s...\n", source, targetName) + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + var raw []byte + if format != "auto" { + opts = append(opts, option.WithResponseBodyInto(&raw)) + } + + var forked hypeman.Instance + if err := client.Post(ctx, fmt.Sprintf("instances/%s/fork", sourceID), params, &forked, opts...); err != nil { + return err + } + + if format != "auto" { + obj := gjson.ParseBytes(raw) + return ShowJSON(os.Stdout, "instance fork", obj, format, transform) + } + + fmt.Fprintf(os.Stderr, "Forked %s as %s (state: %s)\n", source, forked.Name, forked.State) + return nil +} + +func normalizeForkTargetState(state string) (string, error) { + switch strings.ToLower(state) { + case "": + return "", nil + case "stopped": + return "Stopped", nil + case "standby": + return "Standby", nil + case "running": + return "Running", nil + default: + return "", fmt.Errorf("invalid target state: %s (must be Stopped, Standby, or Running)", state) + } +} + +type instanceForkParams struct { + Name string `json:"name"` + FromRunning *bool `json:"from_running,omitempty"` + TargetState *string `json:"target_state,omitempty"` +} diff --git a/pkg/cmd/fork_test.go b/pkg/cmd/fork_test.go new file mode 100644 index 0000000..4e1ad25 --- /dev/null +++ b/pkg/cmd/fork_test.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNormalizeForkTargetState(t *testing.T) { + tests := []struct { + name string + input string + expected string + shouldErr bool + }{ + { + name: "empty state", + input: "", + expected: "", + }, + { + name: "stopped lowercase", + input: "stopped", + expected: "Stopped", + }, + { + name: "standby mixed case", + input: "StAnDbY", + expected: "Standby", + }, + { + name: "running title case", + input: "Running", + expected: "Running", + }, + { + name: "invalid state", + input: "paused", + shouldErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := normalizeForkTargetState(tt.input) + if tt.shouldErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/cmd/inspect.go b/pkg/cmd/inspect.go new file mode 100644 index 0000000..f7ad3a2 --- /dev/null +++ b/pkg/cmd/inspect.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var inspectCmd = cli.Command{ + Name: "inspect", + Usage: "Get instance details by ID or name", + ArgsUsage: "", + Action: handleInspect, + HideHelpCommand: true, +} + +func handleInspect(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("instance ID or name required\nUsage: hypeman inspect ") + } + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + instanceID, err := ResolveInstance(ctx, &client, args[0]) + if err != nil { + return err + } + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + var raw []byte + opts = append(opts, option.WithResponseBodyInto(&raw)) + _, err = client.Instances.Get(ctx, instanceID, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(raw) + return ShowJSON(os.Stdout, "instance inspect", obj, format, transform) +} diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 771d557..2ebce80 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -84,7 +84,7 @@ Examples: // Hypervisor flag &cli.StringFlag{ Name: "hypervisor", - Usage: `Hypervisor to use: "cloud-hypervisor", "qemu", or "vz"`, + Usage: `Hypervisor to use: "cloud-hypervisor", "firecracker", "qemu", or "vz"`, }, // Resource limit flags &cli.StringFlag{ @@ -236,12 +236,14 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { switch hypervisor { case "cloud-hypervisor", "ch": params.Hypervisor = hypeman.InstanceNewParamsHypervisorCloudHypervisor + case "firecracker", "fc": + params.Hypervisor = hypeman.InstanceNewParamsHypervisorFirecracker case "qemu": params.Hypervisor = hypeman.InstanceNewParamsHypervisorQemu case "vz": params.Hypervisor = hypeman.InstanceNewParamsHypervisorVz default: - return fmt.Errorf("invalid hypervisor: %s (must be 'cloud-hypervisor', 'qemu', or 'vz')", hypervisor) + return fmt.Errorf("invalid hypervisor: %s (must be 'cloud-hypervisor', 'firecracker', 'qemu', or 'vz')", hypervisor) } } From 7130271a8e7f1caf22bb8657a5d1c902582f572e Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Tue, 3 Mar 2026 11:20:18 -0500 Subject: [PATCH 7/8] Hide envs by default on output --- .gitignore | 1 + pkg/cmd/inspect.go | 39 +++++++++++++++++++++++++++++++++------ pkg/cmd/inspect_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 pkg/cmd/inspect_test.go diff --git a/.gitignore b/.gitignore index 12c4508..ca34579 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ .env hypeman/** bin/hypeman +demos/ diff --git a/pkg/cmd/inspect.go b/pkg/cmd/inspect.go index f7ad3a2..6941dbc 100644 --- a/pkg/cmd/inspect.go +++ b/pkg/cmd/inspect.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "fmt" "os" @@ -12,9 +13,15 @@ import ( ) var inspectCmd = cli.Command{ - Name: "inspect", - Usage: "Get instance details by ID or name", - ArgsUsage: "", + Name: "inspect", + Usage: "Get instance details by ID or name", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "show-env", + Usage: "Show environment variable values (default: hidden)", + }, + }, Action: handleInspect, HideHelpCommand: true, } @@ -37,16 +44,36 @@ func handleInspect(ctx context.Context, cmd *cli.Command) error { opts = append(opts, debugMiddlewareOption) } - var raw []byte - opts = append(opts, option.WithResponseBodyInto(&raw)) - _, err = client.Instances.Get(ctx, instanceID, opts...) + instance, err := client.Instances.Get(ctx, instanceID, opts...) if err != nil { return err } + if !cmd.Bool("show-env") { + instance.Env = redactEnvValues(instance.Env) + } + + raw, err := json.Marshal(instance) + if err != nil { + return fmt.Errorf("failed to encode instance response: %w", err) + } + format := cmd.Root().String("format") transform := cmd.Root().String("transform") obj := gjson.ParseBytes(raw) return ShowJSON(os.Stdout, "instance inspect", obj, format, transform) } + +func redactEnvValues(env map[string]string) map[string]string { + if len(env) == 0 { + return env + } + + redacted := make(map[string]string, len(env)) + for key := range env { + redacted[key] = "[hidden]" + } + + return redacted +} diff --git a/pkg/cmd/inspect_test.go b/pkg/cmd/inspect_test.go new file mode 100644 index 0000000..a9f3d9b --- /dev/null +++ b/pkg/cmd/inspect_test.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRedactEnvValues(t *testing.T) { + t.Run("returns empty map as-is", func(t *testing.T) { + var env map[string]string + assert.Nil(t, redactEnvValues(env)) + + empty := map[string]string{} + assert.Equal(t, empty, redactEnvValues(empty)) + }) + + t.Run("redacts all values and preserves keys", func(t *testing.T) { + env := map[string]string{ + "FOO": "bar", + "BAZ": "qux", + } + + redacted := redactEnvValues(env) + + assert.Equal(t, map[string]string{ + "FOO": "[hidden]", + "BAZ": "[hidden]", + }, redacted) + require.Equal(t, "bar", env["FOO"]) + require.Equal(t, "qux", env["BAZ"]) + }) +} From a7132f6e30f15bf10a9a28c96fc640212fb4ebc0 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Tue, 3 Mar 2026 11:43:02 -0500 Subject: [PATCH 8/8] show id of forked vm --- .gitignore | 1 + pkg/cmd/fork.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ca34579..2b5398f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ hypeman/** bin/hypeman demos/ +.DS_Store diff --git a/pkg/cmd/fork.go b/pkg/cmd/fork.go index 7a98657..8ca13a6 100644 --- a/pkg/cmd/fork.go +++ b/pkg/cmd/fork.go @@ -87,6 +87,8 @@ func handleFork(ctx context.Context, cmd *cli.Command) error { return ShowJSON(os.Stdout, "instance fork", obj, format, transform) } + // Output instance ID (useful for scripting) + fmt.Println(forked.ID) fmt.Fprintf(os.Stderr, "Forked %s as %s (state: %s)\n", source, forked.Name, forked.State) return nil }