From 9f8ee40305242ba7d8cb9e387c1aa2d59adee801 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:44:53 +0000 Subject: [PATCH 1/2] CLI: Update hypeman SDK to a9a0d6c96059 and add new commands/flags Update hypeman-go SDK and add CLI coverage for missing SDK methods: New commands: - `hypeman image list` for client.Images.List() - `hypeman image get` for client.Images.Get() - `hypeman image delete` for client.Images.Delete() - `hypeman volume create` for client.Volumes.New() - `hypeman volume list` for client.Volumes.List() - `hypeman volume get` for client.Volumes.Get() - `hypeman volume delete` for client.Volumes.Delete() - `hypeman volume attach` for client.Instances.Volumes.Attach() - `hypeman volume detach` for client.Instances.Volumes.Detach() - `hypeman ingress get` for client.Ingresses.Get() - `hypeman build list` for client.Builds.List() - `hypeman build get` for client.Builds.Get() - `hypeman build cancel` for client.Builds.Cancel() New flags: - `--skip-guest-agent` on `hypeman run` for InstanceNewParams.SkipGuestAgent - `--skip-kernel-headers` on `hypeman run` for InstanceNewParams.SkipKernelHeaders Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 +- pkg/cmd/build.go | 150 ++++++++++++++++++ pkg/cmd/cmd.go | 2 + pkg/cmd/imagecmd.go | 175 +++++++++++++++++++++ pkg/cmd/ingresscmd.go | 38 +++++ pkg/cmd/run.go | 17 ++ pkg/cmd/volumecmd.go | 349 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 734 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/imagecmd.go create mode 100644 pkg/cmd/volumecmd.go diff --git a/go.mod b/go.mod index 5aafcea..a5af45a 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.9.6 + github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059 github.com/muesli/reflow v0.3.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 diff --git a/go.sum b/go.sum index 2e5c727..c8816c8 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,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.9.6 h1:gkKbUiTYPWVDa9GwX/xaf9+z+eiTYIj6oglPlir4Xbo= -github.com/kernel/hypeman-go v0.9.6/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= +github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059 h1:C5ixkSnUllJJSWBVAusdj8uX7FcS/eJE1TbzqGAvwQc= +github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059/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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index 15f2c32..f867c26 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -12,6 +12,7 @@ import ( "github.com/kernel/hypeman-go" "github.com/kernel/hypeman-go/option" + "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) @@ -24,6 +25,11 @@ var buildCmd = cli.Command{ The path argument specifies the build context directory containing the source code and Dockerfile. If not specified, the current directory is used. +Subcommands are available for managing builds: + hypeman build list List builds + hypeman build get Get build details + hypeman build cancel Cancel a build + Examples: # Build from current directory hypeman build @@ -48,6 +54,11 @@ Examples: Value: 600, }, }, + Commands: []*cli.Command{ + &buildListCmd, + &buildGetCmd, + &buildCancelCmd, + }, Action: handleBuild, HideHelpCommand: true, } @@ -281,3 +292,142 @@ func createSourceTarball(contextPath string) (*bytes.Buffer, error) { return buf, nil } + +var buildListCmd = cli.Command{ + Name: "list", + Usage: "List builds", + Action: handleBuildList, + HideHelpCommand: true, +} + +var buildGetCmd = cli.Command{ + Name: "get", + Usage: "Get build details", + ArgsUsage: "", + Action: handleBuildGet, + HideHelpCommand: true, +} + +var buildCancelCmd = cli.Command{ + Name: "cancel", + Usage: "Cancel a build", + ArgsUsage: "", + Action: handleBuildCancel, + HideHelpCommand: true, +} + +func handleBuildList(ctx context.Context, cmd *cli.Command) error { + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format != "auto" { + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Builds.List(ctx, opts...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "build list", obj, format, transform) + } + + builds, err := client.Builds.List(ctx, opts...) + if err != nil { + return err + } + + if len(*builds) == 0 { + fmt.Fprintln(os.Stderr, "No builds found.") + return nil + } + + table := NewTableWriter(os.Stdout, "ID", "STATUS", "IMAGE", "DURATION", "CREATED") + table.TruncOrder = []int{2, 0, 4} // IMAGE first, then ID, CREATED + for _, b := range *builds { + imageRef := b.ImageRef + if imageRef == "" { + imageRef = "-" + } + + duration := "-" + if b.DurationMs > 0 { + secs := b.DurationMs / 1000 + if secs < 60 { + duration = fmt.Sprintf("%ds", secs) + } else { + duration = fmt.Sprintf("%dm%ds", secs/60, secs%60) + } + } + + table.AddRow( + TruncateID(b.ID), + string(b.Status), + imageRef, + duration, + FormatTimeAgo(b.CreatedAt), + ) + } + table.Render() + + return nil +} + +func handleBuildGet(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("build ID required\nUsage: hypeman build get ") + } + + id := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Builds.Get(ctx, id, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "build get", obj, format, transform) +} + +func handleBuildCancel(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("build ID required\nUsage: hypeman build cancel ") + } + + id := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + err := client.Builds.Cancel(ctx, id, opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Cancelled build %s\n", id) + return nil +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 0087cd2..e2614ca 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -81,7 +81,9 @@ func init() { &startCmd, &standbyCmd, &restoreCmd, + &imageCmd, &ingressCmd, + &volumeCmd, &resourcesCmd, &deviceCmd, { diff --git a/pkg/cmd/imagecmd.go b/pkg/cmd/imagecmd.go new file mode 100644 index 0000000..c178ef7 --- /dev/null +++ b/pkg/cmd/imagecmd.go @@ -0,0 +1,175 @@ +package cmd + +import ( + "context" + "fmt" + "net/url" + "os" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var imageCmd = cli.Command{ + Name: "image", + Usage: "Manage images", + Commands: []*cli.Command{ + &imageListCmd, + &imageGetCmd, + &imageDeleteCmd, + }, + HideHelpCommand: true, +} + +var imageListCmd = cli.Command{ + Name: "list", + Usage: "List images", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display image names", + }, + }, + Action: handleImageList, + HideHelpCommand: true, +} + +var imageGetCmd = cli.Command{ + Name: "get", + Usage: "Get image details", + ArgsUsage: "", + Action: handleImageGet, + HideHelpCommand: true, +} + +var imageDeleteCmd = cli.Command{ + Name: "delete", + Aliases: []string{"rm"}, + Usage: "Delete an image", + ArgsUsage: "", + Action: handleImageDelete, + HideHelpCommand: true, +} + +func handleImageList(ctx context.Context, cmd *cli.Command) error { + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format != "auto" { + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Images.List(ctx, opts...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "image list", obj, format, transform) + } + + images, err := client.Images.List(ctx, opts...) + if err != nil { + return err + } + + quietMode := cmd.Bool("quiet") + + if quietMode { + for _, img := range *images { + fmt.Println(img.Name) + } + return nil + } + + if len(*images) == 0 { + fmt.Fprintln(os.Stderr, "No images found.") + return nil + } + + table := NewTableWriter(os.Stdout, "NAME", "STATUS", "DIGEST", "SIZE", "CREATED") + table.TruncOrder = []int{0, 2, 4} // NAME first, then DIGEST, CREATED + for _, img := range *images { + digest := img.Digest + if len(digest) > 19 { + digest = digest[:19] + } + + size := "-" + if img.SizeBytes > 0 { + size = formatBytes(img.SizeBytes) + } + + table.AddRow( + img.Name, + string(img.Status), + digest, + size, + FormatTimeAgo(img.CreatedAt), + ) + } + table.Render() + + return nil +} + +func handleImageGet(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("image name required\nUsage: hypeman image get ") + } + + name := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Images.Get(ctx, url.PathEscape(name), opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "image get", obj, format, transform) +} + +func handleImageDelete(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("image name required\nUsage: hypeman image delete ") + } + + name := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + err := client.Images.Delete(ctx, url.PathEscape(name), opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Deleted image %s\n", name) + return nil +} diff --git a/pkg/cmd/ingresscmd.go b/pkg/cmd/ingresscmd.go index 2e83b87..0a4cc32 100644 --- a/pkg/cmd/ingresscmd.go +++ b/pkg/cmd/ingresscmd.go @@ -18,6 +18,7 @@ var ingressCmd = cli.Command{ Commands: []*cli.Command{ &ingressCreateCmd, &ingressListCmd, + &ingressGetCmd, &ingressDeleteCmd, }, HideHelpCommand: true, @@ -76,6 +77,14 @@ var ingressListCmd = cli.Command{ HideHelpCommand: true, } +var ingressGetCmd = cli.Command{ + Name: "get", + Usage: "Get ingress details", + ArgsUsage: "", + Action: handleIngressGet, + HideHelpCommand: true, +} + var ingressDeleteCmd = cli.Command{ Name: "delete", Usage: "Delete an ingress", @@ -213,6 +222,35 @@ func handleIngressList(ctx context.Context, cmd *cli.Command) error { return nil } +func handleIngressGet(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("ingress ID required\nUsage: hypeman ingress get ") + } + + id := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Ingresses.Get(ctx, id, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "ingress get", obj, format, transform) +} + func handleIngressDelete(ctx context.Context, cmd *cli.Command) error { args := cmd.Args().Slice() if len(args) < 1 { diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index e23eac3..64eb920 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -99,6 +99,15 @@ Examples: Name: "bandwidth-up", Usage: `Upload bandwidth limit (e.g., "1Gbps", "125MB/s")`, }, + // Boot option flags + &cli.BoolFlag{ + Name: "skip-guest-agent", + Usage: "Skip guest-agent installation during boot (exec and stat APIs will not work)", + }, + &cli.BoolFlag{ + Name: "skip-kernel-headers", + Usage: "Skip kernel headers installation during boot for faster startup (DKMS will not work)", + }, }, Action: handleRun, HideHelpCommand: true, @@ -219,6 +228,14 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { params.DiskIoBps = hypeman.Opt(diskIO) } + // Boot options + if cmd.IsSet("skip-guest-agent") { + params.SkipGuestAgent = hypeman.Opt(cmd.Bool("skip-guest-agent")) + } + if cmd.IsSet("skip-kernel-headers") { + params.SkipKernelHeaders = hypeman.Opt(cmd.Bool("skip-kernel-headers")) + } + fmt.Fprintf(os.Stderr, "Creating instance %s...\n", name) var opts []option.RequestOption diff --git a/pkg/cmd/volumecmd.go b/pkg/cmd/volumecmd.go new file mode 100644 index 0000000..7476778 --- /dev/null +++ b/pkg/cmd/volumecmd.go @@ -0,0 +1,349 @@ +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 volumeCmd = cli.Command{ + Name: "volume", + Usage: "Manage volumes", + Commands: []*cli.Command{ + &volumeCreateCmd, + &volumeListCmd, + &volumeGetCmd, + &volumeDeleteCmd, + &volumeAttachCmd, + &volumeDetachCmd, + }, + HideHelpCommand: true, +} + +var volumeCreateCmd = cli.Command{ + Name: "create", + Usage: "Create a new volume", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "Volume name", + Required: true, + }, + &cli.IntFlag{ + Name: "size", + Usage: "Size in gigabytes", + Required: true, + }, + &cli.StringFlag{ + Name: "id", + Usage: "Optional custom identifier (auto-generated if not provided)", + }, + }, + Action: handleVolumeCreate, + HideHelpCommand: true, +} + +var volumeListCmd = cli.Command{ + Name: "list", + Usage: "List volumes", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display volume IDs", + }, + }, + Action: handleVolumeList, + HideHelpCommand: true, +} + +var volumeGetCmd = cli.Command{ + Name: "get", + Usage: "Get volume details", + ArgsUsage: "", + Action: handleVolumeGet, + HideHelpCommand: true, +} + +var volumeDeleteCmd = cli.Command{ + Name: "delete", + Aliases: []string{"rm"}, + Usage: "Delete a volume", + ArgsUsage: "", + Action: handleVolumeDelete, + HideHelpCommand: true, +} + +var volumeAttachCmd = cli.Command{ + Name: "attach", + Usage: "Attach a volume to an instance", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"i"}, + Usage: "Instance ID or name", + Required: true, + }, + &cli.StringFlag{ + Name: "mount-path", + Usage: "Path where volume should be mounted in the guest", + Required: true, + }, + &cli.BoolFlag{ + Name: "readonly", + Usage: "Mount as read-only", + }, + }, + Action: handleVolumeAttach, + HideHelpCommand: true, +} + +var volumeDetachCmd = cli.Command{ + Name: "detach", + Usage: "Detach a volume from an instance", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"i"}, + Usage: "Instance ID or name", + Required: true, + }, + }, + Action: handleVolumeDetach, + HideHelpCommand: true, +} + +func handleVolumeCreate(ctx context.Context, cmd *cli.Command) error { + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + params := hypeman.VolumeNewParams{ + Name: cmd.String("name"), + SizeGB: int64(cmd.Int("size")), + } + + if id := cmd.String("id"); id != "" { + params.ID = hypeman.Opt(id) + } + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Volumes.New(ctx, params, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format == "auto" || format == "" { + vol := gjson.ParseBytes(res) + fmt.Printf("%s\n", vol.Get("id").String()) + return nil + } + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "volume create", obj, format, transform) +} + +func handleVolumeList(ctx context.Context, cmd *cli.Command) error { + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format != "auto" { + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Volumes.List(ctx, opts...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "volume list", obj, format, transform) + } + + volumes, err := client.Volumes.List(ctx, opts...) + if err != nil { + return err + } + + quietMode := cmd.Bool("quiet") + + if quietMode { + for _, vol := range *volumes { + fmt.Println(vol.ID) + } + return nil + } + + if len(*volumes) == 0 { + fmt.Fprintln(os.Stderr, "No volumes found.") + return nil + } + + table := NewTableWriter(os.Stdout, "ID", "NAME", "SIZE", "ATTACHMENTS", "CREATED") + table.TruncOrder = []int{0, 1, 4} // ID first, then NAME, CREATED + for _, vol := range *volumes { + attachments := fmt.Sprintf("%d", len(vol.Attachments)) + if len(vol.Attachments) == 0 { + attachments = "-" + } + + table.AddRow( + TruncateID(vol.ID), + vol.Name, + fmt.Sprintf("%d GB", vol.SizeGB), + attachments, + FormatTimeAgo(vol.CreatedAt), + ) + } + table.Render() + + return nil +} + +func handleVolumeGet(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("volume ID required\nUsage: hypeman volume get ") + } + + id := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Volumes.Get(ctx, id, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "volume get", obj, format, transform) +} + +func handleVolumeDelete(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("volume ID required\nUsage: hypeman volume delete ") + } + + id := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + err := client.Volumes.Delete(ctx, id, opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Deleted volume %s\n", id) + return nil +} + +func handleVolumeAttach(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("volume ID required\nUsage: hypeman volume attach --instance --mount-path ") + } + + volumeID := args[0] + instanceIdentifier := cmd.String("instance") + mountPath := cmd.String("mount-path") + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + // Resolve instance + instanceID, err := ResolveInstance(ctx, &client, instanceIdentifier) + if err != nil { + return err + } + + params := hypeman.InstanceVolumeAttachParams{ + ID: instanceID, + MountPath: mountPath, + } + + if cmd.IsSet("readonly") { + params.Readonly = hypeman.Opt(cmd.Bool("readonly")) + } + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + _, err = client.Instances.Volumes.Attach(ctx, volumeID, params, opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Attached volume %s to instance %s at %s\n", volumeID, instanceIdentifier, mountPath) + return nil +} + +func handleVolumeDetach(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("volume ID required\nUsage: hypeman volume detach --instance ") + } + + volumeID := args[0] + instanceIdentifier := cmd.String("instance") + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + // Resolve instance + instanceID, err := ResolveInstance(ctx, &client, instanceIdentifier) + if err != nil { + return err + } + + params := hypeman.InstanceVolumeDetachParams{ + ID: instanceID, + } + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + _, err = client.Instances.Volumes.Detach(ctx, volumeID, params, opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Detached volume %s from instance %s\n", volumeID, instanceIdentifier) + return nil +} From 4abb3c2e1e03a09386ac4d7c12ff919d1c3b98ee Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Wed, 11 Feb 2026 14:03:54 -0800 Subject: [PATCH 2/2] Consistency tweaks --- pkg/cmd/build.go | 20 ++++++++++++++++++-- pkg/cmd/ingresscmd.go | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index f867c26..301f7ff 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -294,8 +294,15 @@ func createSourceTarball(contextPath string) (*bytes.Buffer, error) { } var buildListCmd = cli.Command{ - Name: "list", - Usage: "List builds", + Name: "list", + Usage: "List builds", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display build IDs", + }, + }, Action: handleBuildList, HideHelpCommand: true, } @@ -343,6 +350,15 @@ func handleBuildList(ctx context.Context, cmd *cli.Command) error { return err } + quietMode := cmd.Bool("quiet") + + if quietMode { + for _, b := range *builds { + fmt.Println(b.ID) + } + return nil + } + if len(*builds) == 0 { fmt.Fprintln(os.Stderr, "No builds found.") return nil diff --git a/pkg/cmd/ingresscmd.go b/pkg/cmd/ingresscmd.go index 0a4cc32..6da8a52 100644 --- a/pkg/cmd/ingresscmd.go +++ b/pkg/cmd/ingresscmd.go @@ -271,7 +271,7 @@ func handleIngressDelete(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Fprintf(os.Stderr, "Ingress %s deleted.\n", id) + fmt.Fprintf(os.Stderr, "Deleted ingress %s\n", id) return nil }