Skip to content

Commit 2bd90ec

Browse files
CLI: Update hypeman SDK to 7f21c67 and add capabilities command
Bump github.com/kernel/hypeman-go to 7f21c67d750f6dd66c6b6af04e88c710841f2daf, which adds the GET /capabilities resource. Expose it as `hypeman capabilities` so users can discover which runtimes and features a host actually supports instead of hard-coding hypervisor knowledge. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2a14070 commit 2bd90ec

6 files changed

Lines changed: 244 additions & 3 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,25 @@ The CLI also provides resource-based commands for more advanced usage:
130130
hypeman [resource] [command] [flags]
131131
```
132132

133+
## Host Capabilities
134+
135+
Check what the server build supports on this host before relying on a runtime or feature:
136+
137+
```bash
138+
# Show server/API version, host OS/arch, runtimes, image platforms, and networking
139+
hypeman capabilities
140+
141+
# Show capabilities as JSON
142+
hypeman capabilities --format json
143+
144+
# Show only the runtimes this host supports
145+
hypeman capabilities --transform runtimes
146+
```
147+
148+
Each runtime is listed with an `available` flag and its own feature IDs (for example
149+
`snapshots`, `standby`, `fork`, `gpu-passthrough`), so a runtime is only launchable when
150+
its `available` flag is `yes`.
151+
133152
## Resource Management
134153

135154
### Viewing Server Resources

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/google/go-containerregistry v0.20.7
1212
github.com/gorilla/websocket v1.5.3
1313
github.com/itchyny/json2yaml v0.1.4
14-
github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432
14+
github.com/kernel/hypeman-go v0.24.1-0.20260817185642-7f21c67d750f
1515
github.com/knadh/koanf/parsers/yaml v1.1.0
1616
github.com/knadh/koanf/providers/env v1.1.0
1717
github.com/knadh/koanf/providers/file v1.2.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
7878
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
7979
github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8=
8080
github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI=
81-
github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432 h1:p2zzyxdjm4gEjorDwu+GIGfpRLhIb8Wv+F83uzxy1TQ=
82-
github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48=
81+
github.com/kernel/hypeman-go v0.24.1-0.20260817185642-7f21c67d750f h1:vgFyvKK4pXteI49Dd+0jTea0ZAK2/0Acy055MKu0ZXI=
82+
github.com/kernel/hypeman-go v0.24.1-0.20260817185642-7f21c67d750f/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48=
8383
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
8484
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
8585
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=

pkg/cmd/capabilitiescmd.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"strings"
9+
10+
"github.com/kernel/hypeman-go"
11+
"github.com/kernel/hypeman-go/option"
12+
"github.com/tidwall/gjson"
13+
"github.com/urfave/cli/v3"
14+
)
15+
16+
var capabilitiesCmd = cli.Command{
17+
Name: "capabilities",
18+
Aliases: []string{"capability"},
19+
Usage: "Show machine-readable host capabilities",
20+
Description: `Report server and API version, host OS/architecture, every runtime available on
21+
this host with its per-runtime feature IDs, the configured default runtime and
22+
whether it is available, guest networking model and host gateway, supported
23+
image platforms, and stable server-level feature IDs.
24+
25+
Runtime-derived values reflect the actual host (for example, snapshot and
26+
standby support on macOS is gated on the host OS version), so clients can gate
27+
behavior on capabilities without hard-coding hypervisor knowledge.
28+
29+
Examples:
30+
# Show capabilities (default table format)
31+
hypeman capabilities
32+
33+
# Show capabilities as JSON
34+
hypeman capabilities --format json
35+
36+
# Show only the runtimes this host supports
37+
hypeman capabilities --transform runtimes`,
38+
Action: handleCapabilities,
39+
HideHelpCommand: true,
40+
}
41+
42+
func handleCapabilities(ctx context.Context, cmd *cli.Command) error {
43+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
44+
45+
var opts []option.RequestOption
46+
if cmd.Root().Bool("debug") {
47+
opts = append(opts, debugMiddlewareOption)
48+
}
49+
50+
var res []byte
51+
opts = append(opts, option.WithResponseBodyInto(&res))
52+
_, err := client.Capabilities.Get(ctx, opts...)
53+
if err != nil {
54+
return err
55+
}
56+
57+
format := cmd.Root().String("format")
58+
transform := cmd.Root().String("transform")
59+
60+
if format == "auto" || format == "" {
61+
return showCapabilities(os.Stdout, res)
62+
}
63+
64+
obj := gjson.ParseBytes(res)
65+
return ShowJSON(os.Stdout, "capabilities", obj, format, transform)
66+
}
67+
68+
func showCapabilities(w io.Writer, data []byte) error {
69+
obj := gjson.ParseBytes(data)
70+
71+
server := obj.Get("server")
72+
fmt.Fprintln(w, "SERVER")
73+
fmt.Fprintf(w, " Version: %s\n", orDash(server.Get("version").String()))
74+
fmt.Fprintf(w, " API version: %s\n", orDash(server.Get("api_version").String()))
75+
76+
host := obj.Get("host")
77+
fmt.Fprintln(w)
78+
fmt.Fprintln(w, "HOST")
79+
fmt.Fprintf(w, " OS: %s\n", orDash(host.Get("os").String()))
80+
fmt.Fprintf(w, " Arch: %s\n", orDash(host.Get("arch").String()))
81+
82+
defaultRuntime := obj.Get("default_runtime")
83+
fmt.Fprintln(w)
84+
fmt.Fprintln(w, "DEFAULT RUNTIME")
85+
fmt.Fprintf(w, " Name: %s\n", orDash(defaultRuntime.Get("name").String()))
86+
fmt.Fprintf(w, " Available: %s\n", yesNo(defaultRuntime.Get("available").Bool()))
87+
88+
runtimes := obj.Get("runtimes")
89+
if runtimes.IsArray() && len(runtimes.Array()) > 0 {
90+
fmt.Fprintln(w)
91+
fmt.Fprintln(w, "RUNTIMES")
92+
table := NewTableWriter(w, "NAME", "AVAILABLE", "FEATURES")
93+
table.TruncOrder = []int{2}
94+
runtimes.ForEach(func(_, value gjson.Result) bool {
95+
table.AddRow(
96+
value.Get("name").String(),
97+
yesNo(value.Get("available").Bool()),
98+
orDash(joinStrings(value.Get("features"))),
99+
)
100+
return true
101+
})
102+
table.Render()
103+
}
104+
105+
images := obj.Get("images")
106+
fmt.Fprintln(w)
107+
fmt.Fprintln(w, "IMAGES")
108+
fmt.Fprintf(w, " Default platform: %s\n", orDash(images.Get("default_platform").String()))
109+
fmt.Fprintf(w, " Platforms: %s\n", orDash(joinStrings(images.Get("platforms"))))
110+
111+
network := obj.Get("network")
112+
fmt.Fprintln(w)
113+
fmt.Fprintln(w, "NETWORK")
114+
fmt.Fprintf(w, " Model: %s\n", orDash(network.Get("model").String()))
115+
fmt.Fprintf(w, " Gateway: %s\n", orDash(network.Get("gateway").String()))
116+
fmt.Fprintf(w, " Subnet: %s\n", orDash(network.Get("subnet").String()))
117+
fmt.Fprintf(w, " Guest to guest: %s\n", yesNo(network.Get("guest_to_guest").Bool()))
118+
119+
fmt.Fprintln(w)
120+
fmt.Fprintln(w, "SERVER FEATURES")
121+
fmt.Fprintf(w, " %s\n", orDash(joinStrings(obj.Get("features"))))
122+
123+
return nil
124+
}
125+
126+
func joinStrings(arr gjson.Result) string {
127+
if !arr.IsArray() {
128+
return ""
129+
}
130+
values := make([]string, 0, len(arr.Array()))
131+
arr.ForEach(func(_, value gjson.Result) bool {
132+
values = append(values, value.String())
133+
return true
134+
})
135+
return strings.Join(values, ", ")
136+
}
137+
138+
func orDash(s string) string {
139+
if s == "" {
140+
return "-"
141+
}
142+
return s
143+
}
144+
145+
func yesNo(b bool) string {
146+
if b {
147+
return "yes"
148+
}
149+
return "no"
150+
}

pkg/cmd/capabilitiescmd_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestCapabilitiesCmdStructure(t *testing.T) {
12+
assert.Equal(t, "capabilities", capabilitiesCmd.Name)
13+
assert.Contains(t, capabilitiesCmd.Aliases, "capability")
14+
assert.NotNil(t, capabilitiesCmd.Action)
15+
}
16+
17+
func TestShowCapabilities(t *testing.T) {
18+
payload := []byte(`{
19+
"default_runtime": {"available": true, "name": "cloud-hypervisor"},
20+
"features": ["instances", "images", "devices"],
21+
"host": {"arch": "amd64", "os": "linux"},
22+
"images": {"default_platform": "linux/amd64", "platforms": ["linux/amd64", "linux/arm64"]},
23+
"network": {"guest_to_guest": false, "model": "bridge", "gateway": "192.168.100.1", "subnet": "192.168.100.0/24"},
24+
"runtimes": [
25+
{"available": true, "features": ["snapshots", "standby"], "name": "cloud-hypervisor"},
26+
{"available": false, "features": [], "name": "qemu"}
27+
],
28+
"server": {"api_version": "1.2.3", "version": "abc1234"}
29+
}`)
30+
31+
var buf bytes.Buffer
32+
require.NoError(t, showCapabilities(&buf, payload))
33+
out := buf.String()
34+
35+
assert.Contains(t, out, "Version: abc1234")
36+
assert.Contains(t, out, "API version: 1.2.3")
37+
assert.Contains(t, out, "OS: linux")
38+
assert.Contains(t, out, "Arch: amd64")
39+
assert.Contains(t, out, "Name: cloud-hypervisor")
40+
assert.Contains(t, out, "cloud-hypervisor yes")
41+
assert.Contains(t, out, "snapshots, standby")
42+
assert.Contains(t, out, "qemu no")
43+
assert.Contains(t, out, "Default platform: linux/amd64")
44+
assert.Contains(t, out, "Platforms: linux/amd64, linux/arm64")
45+
assert.Contains(t, out, "Model: bridge")
46+
assert.Contains(t, out, "Gateway: 192.168.100.1")
47+
assert.Contains(t, out, "Subnet: 192.168.100.0/24")
48+
assert.Contains(t, out, "Guest to guest: no")
49+
assert.Contains(t, out, "instances, images, devices")
50+
}
51+
52+
func TestShowCapabilitiesOmitsMissingOptionalFields(t *testing.T) {
53+
payload := []byte(`{
54+
"default_runtime": {"available": false, "name": "vz"},
55+
"features": [],
56+
"host": {"arch": "arm64", "os": "darwin"},
57+
"images": {"default_platform": "linux/arm64", "platforms": ["linux/arm64"]},
58+
"network": {"guest_to_guest": true, "model": "nat"},
59+
"runtimes": [],
60+
"server": {"api_version": "1.2.3", "version": "unknown"}
61+
}`)
62+
63+
var buf bytes.Buffer
64+
require.NoError(t, showCapabilities(&buf, payload))
65+
out := buf.String()
66+
67+
assert.Contains(t, out, "Gateway: -")
68+
assert.Contains(t, out, "Subnet: -")
69+
assert.NotContains(t, out, "RUNTIMES")
70+
assert.Contains(t, out, "SERVER FEATURES\n -")
71+
}

pkg/cmd/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func init() {
9494
&volumeCmd,
9595
&resourcesCmd,
9696
&healthCmd,
97+
&capabilitiesCmd,
9798
&deviceCmd,
9899
&composeCmd,
99100
{

0 commit comments

Comments
 (0)