Skip to content

Commit b3961ad

Browse files
kernel-internal[bot]cursoragentsjmiller609
authored
CLI: Update Hypeman Go SDK to 9cd8f5ca0926682c2f1c1148adf3f5d1e289b7f6 (#41)
* 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 <cursoragent@cursor.com> * 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 * 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 * 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 * fix ps filtering and metadata handling * add inspect and fork commands with sdk-based calls * Hide envs by default on output * show id of forked vm --------- Co-authored-by: kernel-internal[bot] <260533166+kernel-internal[bot]@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Steven Miller <sjmiller609@gmail.com>
1 parent 180e409 commit b3961ad

14 files changed

Lines changed: 417 additions & 31 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ dist/
44
.env
55
hypeman/**
66
bin/hypeman
7+
demos/
8+
.DS_Store

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ require (
1010
github.com/google/go-containerregistry v0.20.7
1111
github.com/gorilla/websocket v1.5.3
1212
github.com/itchyny/json2yaml v0.1.4
13-
github.com/kernel/hypeman-go v0.11.0
13+
github.com/kernel/hypeman-go v0.14.0
1414
github.com/knadh/koanf/parsers/yaml v1.1.0
15+
github.com/knadh/koanf/providers/env v1.1.0
1516
github.com/knadh/koanf/providers/file v1.2.1
1617
github.com/knadh/koanf/v2 v2.3.2
1718
github.com/muesli/reflow v0.3.0
@@ -52,7 +53,6 @@ require (
5253
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
5354
github.com/klauspost/compress v1.18.1 // indirect
5455
github.com/knadh/koanf/maps v0.1.2 // indirect
55-
github.com/knadh/koanf/providers/env v1.1.0 // indirect
5656
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
5757
github.com/mattn/go-isatty v0.0.20 // indirect
5858
github.com/mattn/go-localereader v0.0.1 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
7676
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
7777
github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8=
7878
github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI=
79-
github.com/kernel/hypeman-go v0.11.0 h1:hCXNUHtrhGKswJapzyWyozBOXhKK/oreKvm0AXHuE6c=
80-
github.com/kernel/hypeman-go v0.11.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
79+
github.com/kernel/hypeman-go v0.14.0 h1:FeeVJly5TzkAYZdxuSfn/8Sz5qZZtUlPQQvUQOAOhg4=
80+
github.com/kernel/hypeman-go v0.14.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
8181
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
8282
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
8383
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=

pkg/cmd/build.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ Examples:
7777
Name: "image-name",
7878
Usage: `Custom image name for the build output (pushed to {registry}/{image_name} instead of {registry}/builds/{id})`,
7979
},
80+
&cli.IntFlag{
81+
Name: "cpus",
82+
Usage: "Number of vCPUs for builder VM (default 2)",
83+
},
84+
&cli.IntFlag{
85+
Name: "memory",
86+
Usage: "Memory limit for builder VM in MB (default 2048)",
87+
},
8088
},
8189
Commands: []*cli.Command{
8290
&buildListCmd,
@@ -172,6 +180,12 @@ func handleBuild(ctx context.Context, cmd *cli.Command) error {
172180
if v := cmd.String("image-name"); v != "" {
173181
params.ImageName = hypeman.Opt(v)
174182
}
183+
if cmd.IsSet("cpus") {
184+
params.CPUs = hypeman.Opt(int64(cmd.Int("cpus")))
185+
}
186+
if cmd.IsSet("memory") {
187+
params.MemoryMB = hypeman.Opt(int64(cmd.Int("memory")))
188+
}
175189

176190
// Start build
177191
build, err := client.Builds.New(ctx, params, opts...)

pkg/cmd/cmd.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,26 +67,28 @@ func init() {
6767
Usage: "The GJSON transformation for errors.",
6868
},
6969
},
70-
Commands: []*cli.Command{
71-
&buildCmd,
72-
&execCmd,
73-
&cpCmd,
74-
&pullCmd,
75-
&pushCmd,
76-
&runCmd,
77-
&psCmd,
78-
&logsCmd,
79-
&rmCmd,
80-
&stopCmd,
81-
&startCmd,
82-
&standbyCmd,
83-
&restoreCmd,
84-
&imageCmd,
85-
&ingressCmd,
86-
&volumeCmd,
87-
&resourcesCmd,
88-
&deviceCmd,
89-
{
70+
Commands: []*cli.Command{
71+
&buildCmd,
72+
&execCmd,
73+
&cpCmd,
74+
&pullCmd,
75+
&pushCmd,
76+
&runCmd,
77+
&psCmd,
78+
&inspectCmd,
79+
&logsCmd,
80+
&rmCmd,
81+
&stopCmd,
82+
&startCmd,
83+
&standbyCmd,
84+
&restoreCmd,
85+
&forkCmd,
86+
&imageCmd,
87+
&ingressCmd,
88+
&volumeCmd,
89+
&resourcesCmd,
90+
&deviceCmd,
91+
{
9092
Name: "@manpages",
9193
Usage: "Generate documentation for 'man'",
9294
UsageText: "hypeman @manpages [-o hypeman.1] [--gzip]",

pkg/cmd/fork.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/kernel/hypeman-go"
10+
"github.com/kernel/hypeman-go/option"
11+
"github.com/tidwall/gjson"
12+
"github.com/urfave/cli/v3"
13+
)
14+
15+
var forkCmd = cli.Command{
16+
Name: "fork",
17+
Usage: "Fork an instance into a new instance",
18+
ArgsUsage: "<source> <name>",
19+
Flags: []cli.Flag{
20+
&cli.BoolFlag{
21+
Name: "from-running",
22+
Usage: "Allow forking from a running source by doing standby -> fork -> restore",
23+
},
24+
&cli.StringFlag{
25+
Name: "target-state",
26+
Usage: "Target state for the forked instance: Stopped, Standby, or Running",
27+
},
28+
},
29+
Action: handleFork,
30+
HideHelpCommand: true,
31+
}
32+
33+
func handleFork(ctx context.Context, cmd *cli.Command) error {
34+
args := cmd.Args().Slice()
35+
if len(args) < 2 {
36+
return fmt.Errorf("source instance and target name required\nUsage: hypeman fork [flags] <source> <name>")
37+
}
38+
39+
source := args[0]
40+
targetName := args[1]
41+
42+
targetState, err := normalizeForkTargetState(cmd.String("target-state"))
43+
if err != nil {
44+
return err
45+
}
46+
47+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
48+
49+
sourceID, err := ResolveInstance(ctx, &client, source)
50+
if err != nil {
51+
return err
52+
}
53+
54+
var opts []option.RequestOption
55+
if cmd.Root().Bool("debug") {
56+
opts = append(opts, debugMiddlewareOption)
57+
}
58+
59+
params := instanceForkParams{
60+
Name: targetName,
61+
}
62+
if cmd.IsSet("from-running") {
63+
fromRunning := cmd.Bool("from-running")
64+
params.FromRunning = &fromRunning
65+
}
66+
if targetState != "" {
67+
params.TargetState = &targetState
68+
}
69+
70+
fmt.Fprintf(os.Stderr, "Forking %s to %s...\n", source, targetName)
71+
72+
format := cmd.Root().String("format")
73+
transform := cmd.Root().String("transform")
74+
75+
var raw []byte
76+
if format != "auto" {
77+
opts = append(opts, option.WithResponseBodyInto(&raw))
78+
}
79+
80+
var forked hypeman.Instance
81+
if err := client.Post(ctx, fmt.Sprintf("instances/%s/fork", sourceID), params, &forked, opts...); err != nil {
82+
return err
83+
}
84+
85+
if format != "auto" {
86+
obj := gjson.ParseBytes(raw)
87+
return ShowJSON(os.Stdout, "instance fork", obj, format, transform)
88+
}
89+
90+
// Output instance ID (useful for scripting)
91+
fmt.Println(forked.ID)
92+
fmt.Fprintf(os.Stderr, "Forked %s as %s (state: %s)\n", source, forked.Name, forked.State)
93+
return nil
94+
}
95+
96+
func normalizeForkTargetState(state string) (string, error) {
97+
switch strings.ToLower(state) {
98+
case "":
99+
return "", nil
100+
case "stopped":
101+
return "Stopped", nil
102+
case "standby":
103+
return "Standby", nil
104+
case "running":
105+
return "Running", nil
106+
default:
107+
return "", fmt.Errorf("invalid target state: %s (must be Stopped, Standby, or Running)", state)
108+
}
109+
}
110+
111+
type instanceForkParams struct {
112+
Name string `json:"name"`
113+
FromRunning *bool `json:"from_running,omitempty"`
114+
TargetState *string `json:"target_state,omitempty"`
115+
}

pkg/cmd/fork_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestNormalizeForkTargetState(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
input string
14+
expected string
15+
shouldErr bool
16+
}{
17+
{
18+
name: "empty state",
19+
input: "",
20+
expected: "",
21+
},
22+
{
23+
name: "stopped lowercase",
24+
input: "stopped",
25+
expected: "Stopped",
26+
},
27+
{
28+
name: "standby mixed case",
29+
input: "StAnDbY",
30+
expected: "Standby",
31+
},
32+
{
33+
name: "running title case",
34+
input: "Running",
35+
expected: "Running",
36+
},
37+
{
38+
name: "invalid state",
39+
input: "paused",
40+
shouldErr: true,
41+
},
42+
}
43+
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
result, err := normalizeForkTargetState(tt.input)
47+
if tt.shouldErr {
48+
require.Error(t, err)
49+
return
50+
}
51+
52+
require.NoError(t, err)
53+
assert.Equal(t, tt.expected, result)
54+
})
55+
}
56+
}

pkg/cmd/format.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ func randomSuffix(n int) string {
261261
// Returns an error if the identifier is ambiguous or not found.
262262
func ResolveInstance(ctx context.Context, client *hypeman.Client, identifier string) (string, error) {
263263
// List all instances
264-
instances, err := client.Instances.List(ctx)
264+
instances, err := client.Instances.List(ctx, hypeman.InstanceListParams{})
265265
if err != nil {
266266
return "", fmt.Errorf("failed to list instances: %w", err)
267267
}

pkg/cmd/inspect.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
9+
"github.com/kernel/hypeman-go"
10+
"github.com/kernel/hypeman-go/option"
11+
"github.com/tidwall/gjson"
12+
"github.com/urfave/cli/v3"
13+
)
14+
15+
var inspectCmd = cli.Command{
16+
Name: "inspect",
17+
Usage: "Get instance details by ID or name",
18+
ArgsUsage: "<instance>",
19+
Flags: []cli.Flag{
20+
&cli.BoolFlag{
21+
Name: "show-env",
22+
Usage: "Show environment variable values (default: hidden)",
23+
},
24+
},
25+
Action: handleInspect,
26+
HideHelpCommand: true,
27+
}
28+
29+
func handleInspect(ctx context.Context, cmd *cli.Command) error {
30+
args := cmd.Args().Slice()
31+
if len(args) < 1 {
32+
return fmt.Errorf("instance ID or name required\nUsage: hypeman inspect <instance>")
33+
}
34+
35+
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)
36+
37+
instanceID, err := ResolveInstance(ctx, &client, args[0])
38+
if err != nil {
39+
return err
40+
}
41+
42+
var opts []option.RequestOption
43+
if cmd.Root().Bool("debug") {
44+
opts = append(opts, debugMiddlewareOption)
45+
}
46+
47+
instance, err := client.Instances.Get(ctx, instanceID, opts...)
48+
if err != nil {
49+
return err
50+
}
51+
52+
if !cmd.Bool("show-env") {
53+
instance.Env = redactEnvValues(instance.Env)
54+
}
55+
56+
raw, err := json.Marshal(instance)
57+
if err != nil {
58+
return fmt.Errorf("failed to encode instance response: %w", err)
59+
}
60+
61+
format := cmd.Root().String("format")
62+
transform := cmd.Root().String("transform")
63+
64+
obj := gjson.ParseBytes(raw)
65+
return ShowJSON(os.Stdout, "instance inspect", obj, format, transform)
66+
}
67+
68+
func redactEnvValues(env map[string]string) map[string]string {
69+
if len(env) == 0 {
70+
return env
71+
}
72+
73+
redacted := make(map[string]string, len(env))
74+
for key := range env {
75+
redacted[key] = "[hidden]"
76+
}
77+
78+
return redacted
79+
}

0 commit comments

Comments
 (0)