From 6336f167efbbc7429154dc245183d1b19c091574 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:36:31 +0000 Subject: [PATCH 1/4] CLI: Update hypeman SDK to a897d4c032e104d1f60b1f29415b8f691cd818bb and add new commands Add hypeman-go SDK dependency and implement CLI commands for all SDK methods: - kernel hypeman instance create/list/get/delete/start/stop/restore/standby/logs/stat - kernel hypeman instance volume attach/detach - kernel hypeman image create/list/get/delete - kernel hypeman volume create/list/get/delete/create-from-archive - kernel hypeman device create/list/get/delete/list-available - kernel hypeman ingress create/list/get/delete - kernel hypeman resource get - kernel hypeman build create/list/get/cancel/events Triggered by: kernel/hypeman-go@a897d4c032e104d1f60b1f29415b8f691cd818bb Co-authored-by: Cursor --- cmd/hypeman/build.go | 261 ++++++++++++++++++ cmd/hypeman/device.go | 228 ++++++++++++++++ cmd/hypeman/hypeman.go | 48 ++++ cmd/hypeman/image.go | 183 +++++++++++++ cmd/hypeman/ingress.go | 241 +++++++++++++++++ cmd/hypeman/instance.go | 579 ++++++++++++++++++++++++++++++++++++++++ cmd/hypeman/resource.go | 102 +++++++ cmd/hypeman/volume.go | 243 +++++++++++++++++ cmd/root.go | 4 +- go.mod | 5 +- go.sum | 11 +- 11 files changed, 1894 insertions(+), 11 deletions(-) create mode 100644 cmd/hypeman/build.go create mode 100644 cmd/hypeman/device.go create mode 100644 cmd/hypeman/hypeman.go create mode 100644 cmd/hypeman/image.go create mode 100644 cmd/hypeman/ingress.go create mode 100644 cmd/hypeman/instance.go create mode 100644 cmd/hypeman/resource.go create mode 100644 cmd/hypeman/volume.go diff --git a/cmd/hypeman/build.go b/cmd/hypeman/build.go new file mode 100644 index 0000000..0844508 --- /dev/null +++ b/cmd/hypeman/build.go @@ -0,0 +1,261 @@ +package hypeman + +import ( + "fmt" + "os" + + "github.com/kernel/cli/pkg/table" + "github.com/kernel/cli/pkg/util" + hypemansdk "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/packages/param" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +var buildCmd = &cobra.Command{ + Use: "build", + Aliases: []string{"builds"}, + Short: "Manage Hypeman builds", +} + +var buildCreateCmd = &cobra.Command{ + Use: "create ", + Short: "Create a new build job from a source tarball", + Args: cobra.ExactArgs(1), + RunE: runBuildCreate, +} + +var buildListCmd = &cobra.Command{ + Use: "list", + Short: "List builds", + RunE: runBuildList, +} + +var buildGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get build details", + Args: cobra.ExactArgs(1), + RunE: runBuildGet, +} + +var buildCancelCmd = &cobra.Command{ + Use: "cancel ", + Short: "Cancel a build", + Args: cobra.ExactArgs(1), + RunE: runBuildCancel, +} + +var buildEventsCmd = &cobra.Command{ + Use: "events ", + Short: "Stream build events", + Args: cobra.ExactArgs(1), + RunE: runBuildEvents, +} + +func init() { + buildCmd.AddCommand(buildCreateCmd) + buildCmd.AddCommand(buildListCmd) + buildCmd.AddCommand(buildGetCmd) + buildCmd.AddCommand(buildCancelCmd) + buildCmd.AddCommand(buildEventsCmd) + + // build create flags + buildCreateCmd.Flags().String("dockerfile", "", "Dockerfile content (if not in source tarball)") + buildCreateCmd.Flags().String("base-image-digest", "", "Optional pinned base image digest") + buildCreateCmd.Flags().String("cache-scope", "", "Tenant-specific cache key prefix") + buildCreateCmd.Flags().String("global-cache-key", "", "Global cache identifier (e.g., 'node', 'python')") + buildCreateCmd.Flags().String("is-admin-build", "", "Set to 'true' for admin builds with global cache push access") + buildCreateCmd.Flags().String("secrets", "", "JSON array of secret references to inject during build") + buildCreateCmd.Flags().Int64("timeout-seconds", 0, "Build timeout in seconds (default 600)") + buildCreateCmd.Flags().StringP("output", "o", "", "Output format: json") + + // build list flags + buildListCmd.Flags().StringP("output", "o", "", "Output format: json") + + // build get flags + buildGetCmd.Flags().StringP("output", "o", "", "Output format: json") + + // build events flags + buildEventsCmd.Flags().BoolP("follow", "f", false, "Continue streaming new events") +} + +func runBuildCreate(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + sourcePath := args[0] + file, err := os.Open(sourcePath) + if err != nil { + return fmt.Errorf("failed to open source tarball: %w", err) + } + defer file.Close() + + params := hypemansdk.BuildNewParams{ + Source: file, + } + + if v, _ := cmd.Flags().GetString("dockerfile"); v != "" { + params.Dockerfile = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("base-image-digest"); v != "" { + params.BaseImageDigest = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("cache-scope"); v != "" { + params.CacheScope = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("global-cache-key"); v != "" { + params.GlobalCacheKey = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("is-admin-build"); v != "" { + params.IsAdminBuild = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("secrets"); v != "" { + params.Secrets = param.NewOpt(v) + } + if cmd.Flags().Changed("timeout-seconds") { + v, _ := cmd.Flags().GetInt64("timeout-seconds") + params.TimeoutSeconds = param.NewOpt(v) + } + + build, err := client.Builds.New(cmd.Context(), params) + if err != nil { + return fmt.Errorf("failed to create build: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(build) + } + + pterm.Success.Printf("Created build %s (status: %s)\n", build.ID, build.Status) + printBuildDetail(build) + return nil +} + +func runBuildList(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + builds, err := client.Builds.List(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to list builds: %w", err) + } + + if output == "json" { + if builds == nil || len(*builds) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(*builds) + } + + if builds == nil || len(*builds) == 0 { + pterm.Info.Println("No builds found") + return nil + } + + tableData := pterm.TableData{{"ID", "Status", "Image Ref", "Duration (ms)", "Created At"}} + for _, b := range *builds { + durationStr := "-" + if b.DurationMs > 0 { + durationStr = fmt.Sprintf("%d", b.DurationMs) + } + tableData = append(tableData, []string{ + b.ID, + string(b.Status), + util.OrDash(b.ImageRef), + durationStr, + util.FormatLocal(b.CreatedAt), + }) + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runBuildGet(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + build, err := client.Builds.Get(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to get build: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(build) + } + + printBuildDetail(build) + return nil +} + +func runBuildCancel(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + if err := client.Builds.Cancel(cmd.Context(), args[0]); err != nil { + return fmt.Errorf("failed to cancel build: %w", err) + } + + pterm.Success.Printf("Cancelled build %s\n", args[0]) + return nil +} + +func runBuildEvents(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + follow, _ := cmd.Flags().GetBool("follow") + + params := hypemansdk.BuildEventsParams{ + Follow: param.NewOpt(follow), + } + + stream := client.Builds.EventsStreaming(cmd.Context(), args[0], params) + for stream.Next() { + event := stream.Current() + switch event.Type { + case hypemansdk.BuildEventTypeLog: + fmt.Print(event.Content) + case hypemansdk.BuildEventTypeStatus: + pterm.Info.Printf("Build status: %s\n", event.Status) + case hypemansdk.BuildEventTypeHeartbeat: + // ignore heartbeats + } + } + if stream.Err() != nil { + return fmt.Errorf("event stream error: %w", stream.Err()) + } + return nil +} + +func printBuildDetail(b *hypemansdk.Build) { + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", b.ID}, + {"Status", string(b.Status)}, + {"Image Ref", util.OrDash(b.ImageRef)}, + {"Image Digest", util.OrDash(b.ImageDigest)}, + {"Duration (ms)", fmt.Sprintf("%d", b.DurationMs)}, + {"Created At", util.FormatLocal(b.CreatedAt)}, + } + if b.Error != "" { + tableData = append(tableData, []string{"Error", b.Error}) + } + if b.BuilderInstanceID != "" { + tableData = append(tableData, []string{"Builder Instance", b.BuilderInstanceID}) + } + table.PrintTableNoPad(tableData, true) +} diff --git a/cmd/hypeman/device.go b/cmd/hypeman/device.go new file mode 100644 index 0000000..842573d --- /dev/null +++ b/cmd/hypeman/device.go @@ -0,0 +1,228 @@ +package hypeman + +import ( + "fmt" + + "github.com/kernel/cli/pkg/table" + "github.com/kernel/cli/pkg/util" + hypemansdk "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/packages/param" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +var deviceCmd = &cobra.Command{ + Use: "device", + Aliases: []string{"devices"}, + Short: "Manage Hypeman devices", +} + +var deviceCreateCmd = &cobra.Command{ + Use: "create", + Short: "Register a device for passthrough", + RunE: runDeviceCreate, +} + +var deviceListCmd = &cobra.Command{ + Use: "list", + Short: "List registered devices", + RunE: runDeviceList, +} + +var deviceGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get device details", + Args: cobra.ExactArgs(1), + RunE: runDeviceGet, +} + +var deviceDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Unregister a device", + Args: cobra.ExactArgs(1), + RunE: runDeviceDelete, +} + +var deviceListAvailableCmd = &cobra.Command{ + Use: "list-available", + Short: "Discover passthrough-capable devices on host", + RunE: runDeviceListAvailable, +} + +func init() { + deviceCmd.AddCommand(deviceCreateCmd) + deviceCmd.AddCommand(deviceListCmd) + deviceCmd.AddCommand(deviceGetCmd) + deviceCmd.AddCommand(deviceDeleteCmd) + deviceCmd.AddCommand(deviceListAvailableCmd) + + // device create flags + deviceCreateCmd.Flags().String("pci-address", "", "PCI address of the device (required, e.g., '0000:a2:00.0')") + _ = deviceCreateCmd.MarkFlagRequired("pci-address") + deviceCreateCmd.Flags().String("name", "", "Optional device name") + deviceCreateCmd.Flags().StringP("output", "o", "", "Output format: json") + + // device list flags + deviceListCmd.Flags().StringP("output", "o", "", "Output format: json") + + // device get flags + deviceGetCmd.Flags().StringP("output", "o", "", "Output format: json") + + // device list-available flags + deviceListAvailableCmd.Flags().StringP("output", "o", "", "Output format: json") +} + +func runDeviceCreate(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + pciAddress, _ := cmd.Flags().GetString("pci-address") + + params := hypemansdk.DeviceNewParams{ + PciAddress: pciAddress, + } + if v, _ := cmd.Flags().GetString("name"); v != "" { + params.Name = param.NewOpt(v) + } + + device, err := client.Devices.New(cmd.Context(), params) + if err != nil { + return fmt.Errorf("failed to register device: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(device) + } + + pterm.Success.Printf("Registered device %s (%s, type: %s)\n", device.Name, device.ID, device.Type) + return nil +} + +func runDeviceList(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + devices, err := client.Devices.List(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to list devices: %w", err) + } + + if output == "json" { + if devices == nil || len(*devices) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(*devices) + } + + if devices == nil || len(*devices) == 0 { + pterm.Info.Println("No devices found") + return nil + } + + tableData := pterm.TableData{{"ID", "Name", "Type", "PCI Address", "VFIO Bound", "Attached To", "Created At"}} + for _, dev := range *devices { + tableData = append(tableData, []string{ + dev.ID, + util.OrDash(dev.Name), + string(dev.Type), + dev.PciAddress, + fmt.Sprintf("%t", dev.BoundToVfio), + util.OrDash(dev.AttachedTo), + util.FormatLocal(dev.CreatedAt), + }) + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runDeviceGet(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + device, err := client.Devices.Get(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to get device: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(device) + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", device.ID}, + {"Name", util.OrDash(device.Name)}, + {"Type", string(device.Type)}, + {"PCI Address", device.PciAddress}, + {"Vendor ID", device.VendorID}, + {"Device ID", device.DeviceID}, + {"IOMMU Group", fmt.Sprintf("%d", device.IommuGroup)}, + {"VFIO Bound", fmt.Sprintf("%t", device.BoundToVfio)}, + {"Attached To", util.OrDash(device.AttachedTo)}, + {"Created At", util.FormatLocal(device.CreatedAt)}, + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runDeviceDelete(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + if err := client.Devices.Delete(cmd.Context(), args[0]); err != nil { + return fmt.Errorf("failed to unregister device: %w", err) + } + + pterm.Success.Printf("Unregistered device %s\n", args[0]) + return nil +} + +func runDeviceListAvailable(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + devices, err := client.Devices.ListAvailable(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to list available devices: %w", err) + } + + if output == "json" { + if devices == nil || len(*devices) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(*devices) + } + + if devices == nil || len(*devices) == 0 { + pterm.Info.Println("No available devices found") + return nil + } + + tableData := pterm.TableData{{"PCI Address", "Vendor", "Device", "IOMMU Group", "Current Driver"}} + for _, dev := range *devices { + tableData = append(tableData, []string{ + dev.PciAddress, + fmt.Sprintf("%s (%s)", util.OrDash(dev.VendorName), dev.VendorID), + fmt.Sprintf("%s (%s)", util.OrDash(dev.DeviceName), dev.DeviceID), + fmt.Sprintf("%d", dev.IommuGroup), + util.OrDash(dev.CurrentDriver), + }) + } + table.PrintTableNoPad(tableData, true) + return nil +} diff --git a/cmd/hypeman/hypeman.go b/cmd/hypeman/hypeman.go new file mode 100644 index 0000000..b8dfdf4 --- /dev/null +++ b/cmd/hypeman/hypeman.go @@ -0,0 +1,48 @@ +package hypeman + +import ( + "fmt" + "os" + + hypemansdk "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/option" + "github.com/spf13/cobra" +) + +// HypemanCmd is the top-level command for Hypeman operations. +var HypemanCmd = &cobra.Command{ + Use: "hypeman", + Short: "Manage Hypeman instances, images, volumes, devices, ingresses, builds, and resources", + Long: "Commands for interacting with the Hypeman API for VM instance management", +} + +func init() { + HypemanCmd.AddCommand(instanceCmd) + HypemanCmd.AddCommand(imageCmd) + HypemanCmd.AddCommand(volumeCmd) + HypemanCmd.AddCommand(deviceCmd) + HypemanCmd.AddCommand(ingressCmd) + HypemanCmd.AddCommand(resourceCmd) + HypemanCmd.AddCommand(buildCmd) +} + +// getHypemanClient creates a Hypeman SDK client. +// It reads HYPEMAN_API_KEY and HYPEMAN_BASE_URL from the environment. +func getHypemanClient() (hypemansdk.Client, error) { + apiKey := os.Getenv("HYPEMAN_API_KEY") + if apiKey == "" { + return hypemansdk.Client{}, fmt.Errorf("HYPEMAN_API_KEY environment variable is required") + } + opts := []option.RequestOption{ + option.WithAPIKey(apiKey), + } + if baseURL := os.Getenv("HYPEMAN_BASE_URL"); baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } + return hypemansdk.NewClient(opts...), nil +} + +// mustGetClient is a helper that returns the client or sets the error on the command. +func mustGetClient(cmd *cobra.Command) (hypemansdk.Client, error) { + return getHypemanClient() +} diff --git a/cmd/hypeman/image.go b/cmd/hypeman/image.go new file mode 100644 index 0000000..192a969 --- /dev/null +++ b/cmd/hypeman/image.go @@ -0,0 +1,183 @@ +package hypeman + +import ( + "fmt" + + "github.com/kernel/cli/pkg/table" + "github.com/kernel/cli/pkg/util" + hypemansdk "github.com/kernel/hypeman-go" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +var imageCmd = &cobra.Command{ + Use: "image", + Aliases: []string{"images"}, + Short: "Manage Hypeman images", +} + +var imageCreateCmd = &cobra.Command{ + Use: "create", + Short: "Pull and convert an OCI image", + RunE: runImageCreate, +} + +var imageListCmd = &cobra.Command{ + Use: "list", + Short: "List images", + RunE: runImageList, +} + +var imageGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get image details", + Args: cobra.ExactArgs(1), + RunE: runImageGet, +} + +var imageDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete an image", + Args: cobra.ExactArgs(1), + RunE: runImageDelete, +} + +func init() { + imageCmd.AddCommand(imageCreateCmd) + imageCmd.AddCommand(imageListCmd) + imageCmd.AddCommand(imageGetCmd) + imageCmd.AddCommand(imageDeleteCmd) + + imageCreateCmd.Flags().String("name", "", "OCI image reference (e.g., docker.io/library/nginx:latest) (required)") + _ = imageCreateCmd.MarkFlagRequired("name") + imageCreateCmd.Flags().StringP("output", "o", "", "Output format: json") + + imageListCmd.Flags().StringP("output", "o", "", "Output format: json") + imageGetCmd.Flags().StringP("output", "o", "", "Output format: json") +} + +func runImageCreate(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + + image, err := client.Images.New(cmd.Context(), hypemansdk.ImageNewParams{ + Name: name, + }) + if err != nil { + return fmt.Errorf("failed to create image: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(image) + } + + pterm.Success.Printf("Created image %s (status: %s)\n", image.Name, image.Status) + printImageDetail(image) + return nil +} + +func runImageList(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + images, err := client.Images.List(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to list images: %w", err) + } + + if output == "json" { + if images == nil || len(*images) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(*images) + } + + if images == nil || len(*images) == 0 { + pterm.Info.Println("No images found") + return nil + } + + tableData := pterm.TableData{{"Name", "Status", "Digest", "Size (bytes)", "Created At"}} + for _, img := range *images { + sizeStr := "-" + if img.SizeBytes > 0 { + sizeStr = fmt.Sprintf("%d", img.SizeBytes) + } + tableData = append(tableData, []string{ + img.Name, + string(img.Status), + truncate(img.Digest, 20), + sizeStr, + util.FormatLocal(img.CreatedAt), + }) + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runImageGet(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + image, err := client.Images.Get(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to get image: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(image) + } + + printImageDetail(image) + return nil +} + +func runImageDelete(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + if err := client.Images.Delete(cmd.Context(), args[0]); err != nil { + return fmt.Errorf("failed to delete image: %w", err) + } + + pterm.Success.Printf("Deleted image %s\n", args[0]) + return nil +} + +func printImageDetail(img *hypemansdk.Image) { + tableData := pterm.TableData{ + {"Property", "Value"}, + {"Name", img.Name}, + {"Status", string(img.Status)}, + {"Digest", img.Digest}, + {"Size (bytes)", fmt.Sprintf("%d", img.SizeBytes)}, + {"Created At", util.FormatLocal(img.CreatedAt)}, + } + if img.Error != "" { + tableData = append(tableData, []string{"Error", img.Error}) + } + if img.WorkingDir != "" { + tableData = append(tableData, []string{"Working Dir", img.WorkingDir}) + } + table.PrintTableNoPad(tableData, true) +} + +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen-3] + "..." +} diff --git a/cmd/hypeman/ingress.go b/cmd/hypeman/ingress.go new file mode 100644 index 0000000..56f025a --- /dev/null +++ b/cmd/hypeman/ingress.go @@ -0,0 +1,241 @@ +package hypeman + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/kernel/cli/pkg/table" + "github.com/kernel/cli/pkg/util" + hypemansdk "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/packages/param" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +var ingressCmd = &cobra.Command{ + Use: "ingress", + Aliases: []string{"ingresses"}, + Short: "Manage Hypeman ingresses", +} + +var ingressCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create an ingress", + RunE: runIngressCreate, +} + +var ingressListCmd = &cobra.Command{ + Use: "list", + Short: "List ingresses", + RunE: runIngressList, +} + +var ingressGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get ingress details", + Args: cobra.ExactArgs(1), + RunE: runIngressGet, +} + +var ingressDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete an ingress", + Args: cobra.ExactArgs(1), + RunE: runIngressDelete, +} + +func init() { + ingressCmd.AddCommand(ingressCreateCmd) + ingressCmd.AddCommand(ingressListCmd) + ingressCmd.AddCommand(ingressGetCmd) + ingressCmd.AddCommand(ingressDeleteCmd) + + // ingress create flags + ingressCreateCmd.Flags().String("name", "", "Human-readable name (required)") + _ = ingressCreateCmd.MarkFlagRequired("name") + ingressCreateCmd.Flags().StringArray("rule", nil, "Routing rule as JSON (repeatable, required)") + _ = ingressCreateCmd.MarkFlagRequired("rule") + ingressCreateCmd.Flags().StringP("output", "o", "", "Output format: json") + + // Simple rule flags for single-rule ingresses + ingressCreateCmd.Flags().String("hostname", "", "Match hostname (shorthand for single rule)") + ingressCreateCmd.Flags().Int64("match-port", 0, "Match port (shorthand for single rule)") + ingressCreateCmd.Flags().String("target-instance", "", "Target instance name or ID (shorthand for single rule)") + ingressCreateCmd.Flags().Int64("target-port", 0, "Target port (shorthand for single rule)") + ingressCreateCmd.Flags().Bool("tls", false, "Enable TLS termination (shorthand for single rule)") + ingressCreateCmd.Flags().Bool("redirect-http", false, "Redirect HTTP to HTTPS (shorthand for single rule)") + + // ingress list flags + ingressListCmd.Flags().StringP("output", "o", "", "Output format: json") + + // ingress get flags + ingressGetCmd.Flags().StringP("output", "o", "", "Output format: json") +} + +func runIngressCreate(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + + // Build rules from --rule JSON flags + ruleStrs, _ := cmd.Flags().GetStringArray("rule") + var rules []hypemansdk.IngressRuleParam + for _, ruleStr := range ruleStrs { + var rule hypemansdk.IngressRuleParam + if err := json.Unmarshal([]byte(ruleStr), &rule); err != nil { + return fmt.Errorf("invalid rule JSON: %w", err) + } + rules = append(rules, rule) + } + + // If no --rule flags but shorthand flags are provided, build a single rule + if len(rules) == 0 { + hostname, _ := cmd.Flags().GetString("hostname") + targetInstance, _ := cmd.Flags().GetString("target-instance") + targetPort, _ := cmd.Flags().GetInt64("target-port") + + if hostname != "" && targetInstance != "" && targetPort > 0 { + rule := hypemansdk.IngressRuleParam{ + Match: hypemansdk.IngressMatchParam{ + Hostname: hostname, + }, + Target: hypemansdk.IngressTargetParam{ + Instance: targetInstance, + Port: targetPort, + }, + } + if matchPort, _ := cmd.Flags().GetInt64("match-port"); matchPort > 0 { + rule.Match.Port = param.NewOpt(matchPort) + } + if cmd.Flags().Changed("tls") { + tls, _ := cmd.Flags().GetBool("tls") + rule.Tls = param.NewOpt(tls) + } + if cmd.Flags().Changed("redirect-http") { + redir, _ := cmd.Flags().GetBool("redirect-http") + rule.RedirectHTTP = param.NewOpt(redir) + } + rules = append(rules, rule) + } + } + + if len(rules) == 0 { + return fmt.Errorf("at least one --rule is required") + } + + ingress, err := client.Ingresses.New(cmd.Context(), hypemansdk.IngressNewParams{ + Name: name, + Rules: rules, + }) + if err != nil { + return fmt.Errorf("failed to create ingress: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(ingress) + } + + pterm.Success.Printf("Created ingress %s (%s)\n", ingress.Name, ingress.ID) + printIngressDetail(ingress) + return nil +} + +func runIngressList(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + ingresses, err := client.Ingresses.List(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to list ingresses: %w", err) + } + + if output == "json" { + if ingresses == nil || len(*ingresses) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(*ingresses) + } + + if ingresses == nil || len(*ingresses) == 0 { + pterm.Info.Println("No ingresses found") + return nil + } + + tableData := pterm.TableData{{"ID", "Name", "Rules", "Created At"}} + for _, ing := range *ingresses { + var ruleDescs []string + for _, r := range ing.Rules { + ruleDescs = append(ruleDescs, fmt.Sprintf("%s -> %s:%d", r.Match.Hostname, r.Target.Instance, r.Target.Port)) + } + tableData = append(tableData, []string{ + ing.ID, + ing.Name, + strings.Join(ruleDescs, "; "), + util.FormatLocal(ing.CreatedAt), + }) + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runIngressGet(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + ingress, err := client.Ingresses.Get(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to get ingress: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(ingress) + } + + printIngressDetail(ingress) + return nil +} + +func runIngressDelete(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + if err := client.Ingresses.Delete(cmd.Context(), args[0]); err != nil { + return fmt.Errorf("failed to delete ingress: %w", err) + } + + pterm.Success.Printf("Deleted ingress %s\n", args[0]) + return nil +} + +func printIngressDetail(ing *hypemansdk.Ingress) { + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", ing.ID}, + {"Name", ing.Name}, + {"Created At", util.FormatLocal(ing.CreatedAt)}, + } + for i, r := range ing.Rules { + prefix := fmt.Sprintf("Rule %d", i+1) + tableData = append(tableData, + []string{prefix + " Hostname", r.Match.Hostname}, + []string{prefix + " Match Port", fmt.Sprintf("%d", r.Match.Port)}, + []string{prefix + " Target", fmt.Sprintf("%s:%d", r.Target.Instance, r.Target.Port)}, + []string{prefix + " TLS", fmt.Sprintf("%t", r.Tls)}, + []string{prefix + " Redirect HTTP", fmt.Sprintf("%t", r.RedirectHTTP)}, + ) + } + table.PrintTableNoPad(tableData, true) +} diff --git a/cmd/hypeman/instance.go b/cmd/hypeman/instance.go new file mode 100644 index 0000000..9e1d217 --- /dev/null +++ b/cmd/hypeman/instance.go @@ -0,0 +1,579 @@ +package hypeman + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/kernel/cli/pkg/table" + "github.com/kernel/cli/pkg/util" + hypemansdk "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/packages/param" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +var instanceCmd = &cobra.Command{ + Use: "instance", + Aliases: []string{"instances"}, + Short: "Manage Hypeman instances", +} + +var instanceCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create and start an instance", + RunE: runInstanceCreate, +} + +var instanceListCmd = &cobra.Command{ + Use: "list", + Short: "List instances", + RunE: runInstanceList, +} + +var instanceGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get instance details", + Args: cobra.ExactArgs(1), + RunE: runInstanceGet, +} + +var instanceDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Stop and delete an instance", + Args: cobra.ExactArgs(1), + RunE: runInstanceDelete, +} + +var instanceStartCmd = &cobra.Command{ + Use: "start ", + Short: "Start a stopped instance", + Args: cobra.ExactArgs(1), + RunE: runInstanceStart, +} + +var instanceStopCmd = &cobra.Command{ + Use: "stop ", + Short: "Stop an instance (graceful shutdown)", + Args: cobra.ExactArgs(1), + RunE: runInstanceStop, +} + +var instanceRestoreCmd = &cobra.Command{ + Use: "restore ", + Short: "Restore an instance from standby", + Args: cobra.ExactArgs(1), + RunE: runInstanceRestore, +} + +var instanceStandbyCmd = &cobra.Command{ + Use: "standby ", + Short: "Put an instance in standby (pause, snapshot, delete VMM)", + Args: cobra.ExactArgs(1), + RunE: runInstanceStandby, +} + +var instanceLogsCmd = &cobra.Command{ + Use: "logs ", + Short: "Stream instance logs", + Args: cobra.ExactArgs(1), + RunE: runInstanceLogs, +} + +var instanceStatCmd = &cobra.Command{ + Use: "stat ", + Short: "Get file information from the guest filesystem", + Args: cobra.ExactArgs(1), + RunE: runInstanceStat, +} + +var instanceVolumeAttachCmd = &cobra.Command{ + Use: "attach ", + Short: "Attach a volume to an instance", + Args: cobra.ExactArgs(1), + RunE: runInstanceVolumeAttach, +} + +var instanceVolumeDetachCmd = &cobra.Command{ + Use: "detach ", + Short: "Detach a volume from an instance", + Args: cobra.ExactArgs(1), + RunE: runInstanceVolumeDetach, +} + +var instanceVolumeCmd = &cobra.Command{ + Use: "volume", + Short: "Manage instance volume attachments", +} + +func init() { + instanceCmd.AddCommand(instanceCreateCmd) + instanceCmd.AddCommand(instanceListCmd) + instanceCmd.AddCommand(instanceGetCmd) + instanceCmd.AddCommand(instanceDeleteCmd) + instanceCmd.AddCommand(instanceStartCmd) + instanceCmd.AddCommand(instanceStopCmd) + instanceCmd.AddCommand(instanceRestoreCmd) + instanceCmd.AddCommand(instanceStandbyCmd) + instanceCmd.AddCommand(instanceLogsCmd) + instanceCmd.AddCommand(instanceStatCmd) + instanceCmd.AddCommand(instanceVolumeCmd) + instanceVolumeCmd.AddCommand(instanceVolumeAttachCmd) + instanceVolumeCmd.AddCommand(instanceVolumeDetachCmd) + + // instance create flags + instanceCreateCmd.Flags().String("image", "", "OCI image reference (required)") + instanceCreateCmd.Flags().String("name", "", "Human-readable name (required)") + _ = instanceCreateCmd.MarkFlagRequired("image") + _ = instanceCreateCmd.MarkFlagRequired("name") + instanceCreateCmd.Flags().String("size", "", "Base memory size (e.g., '1GB', '512MB')") + instanceCreateCmd.Flags().String("hotplug-size", "", "Additional memory for hotplug (e.g., '3GB')") + instanceCreateCmd.Flags().String("overlay-size", "", "Writable overlay disk size (e.g., '10GB')") + instanceCreateCmd.Flags().String("disk-io-bps", "", "Disk I/O rate limit (e.g., '100MB/s')") + instanceCreateCmd.Flags().Int64("vcpus", 0, "Number of virtual CPUs") + instanceCreateCmd.Flags().Bool("skip-guest-agent", false, "Skip guest-agent installation during boot") + instanceCreateCmd.Flags().Bool("skip-kernel-headers", false, "Skip kernel headers installation during boot") + instanceCreateCmd.Flags().StringSlice("devices", nil, "Device IDs or names to attach for GPU/PCI passthrough") + instanceCreateCmd.Flags().StringArrayP("env", "e", nil, "Environment variables (KEY=value, repeatable)") + instanceCreateCmd.Flags().String("gpu-profile", "", "vGPU profile name (e.g., 'L40S-1Q')") + instanceCreateCmd.Flags().String("hypervisor", "", "Hypervisor to use (cloud-hypervisor or qemu)") + instanceCreateCmd.Flags().Bool("network-enabled", true, "Attach instance to the default network") + instanceCreateCmd.Flags().String("bandwidth-download", "", "Download bandwidth limit (e.g., '1Gbps')") + instanceCreateCmd.Flags().String("bandwidth-upload", "", "Upload bandwidth limit (e.g., '1Gbps')") + instanceCreateCmd.Flags().StringArray("volume", nil, "Volume mounts (volume-id:mount-path[:ro], repeatable)") + instanceCreateCmd.Flags().StringP("output", "o", "", "Output format: json") + + // instance list flags + instanceListCmd.Flags().StringP("output", "o", "", "Output format: json") + + // instance get flags + instanceGetCmd.Flags().StringP("output", "o", "", "Output format: json") + + // instance logs flags + instanceLogsCmd.Flags().BoolP("follow", "f", false, "Continue streaming new lines") + instanceLogsCmd.Flags().Int64("tail", 0, "Number of lines to return from end") + instanceLogsCmd.Flags().String("source", "", "Log source: app, vmm, or hypeman") + + // instance stat flags + instanceStatCmd.Flags().String("path", "", "Path to stat in the guest filesystem (required)") + _ = instanceStatCmd.MarkFlagRequired("path") + instanceStatCmd.Flags().Bool("follow-links", false, "Follow symbolic links") + instanceStatCmd.Flags().StringP("output", "o", "", "Output format: json") + + // instance volume attach flags + instanceVolumeAttachCmd.Flags().String("instance-id", "", "Instance ID (required)") + _ = instanceVolumeAttachCmd.MarkFlagRequired("instance-id") + instanceVolumeAttachCmd.Flags().String("mount-path", "", "Path where volume should be mounted (required)") + _ = instanceVolumeAttachCmd.MarkFlagRequired("mount-path") + instanceVolumeAttachCmd.Flags().Bool("readonly", false, "Mount as read-only") + instanceVolumeAttachCmd.Flags().StringP("output", "o", "", "Output format: json") + + // instance volume detach flags + instanceVolumeDetachCmd.Flags().String("instance-id", "", "Instance ID (required)") + _ = instanceVolumeDetachCmd.MarkFlagRequired("instance-id") + instanceVolumeDetachCmd.Flags().StringP("output", "o", "", "Output format: json") +} + +func runInstanceCreate(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + image, _ := cmd.Flags().GetString("image") + name, _ := cmd.Flags().GetString("name") + + params := hypemansdk.InstanceNewParams{ + Image: image, + Name: name, + } + + if v, _ := cmd.Flags().GetString("size"); v != "" { + params.Size = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("hotplug-size"); v != "" { + params.HotplugSize = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("overlay-size"); v != "" { + params.OverlaySize = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("disk-io-bps"); v != "" { + params.DiskIoBps = param.NewOpt(v) + } + if cmd.Flags().Changed("vcpus") { + v, _ := cmd.Flags().GetInt64("vcpus") + params.Vcpus = param.NewOpt(v) + } + if cmd.Flags().Changed("skip-guest-agent") { + v, _ := cmd.Flags().GetBool("skip-guest-agent") + params.SkipGuestAgent = param.NewOpt(v) + } + if cmd.Flags().Changed("skip-kernel-headers") { + v, _ := cmd.Flags().GetBool("skip-kernel-headers") + params.SkipKernelHeaders = param.NewOpt(v) + } + if devices, _ := cmd.Flags().GetStringSlice("devices"); len(devices) > 0 { + params.Devices = devices + } + if envPairs, _ := cmd.Flags().GetStringArray("env"); len(envPairs) > 0 { + envMap := make(map[string]string) + for _, kv := range envPairs { + parts := strings.SplitN(kv, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid env format: %s (expected KEY=value)", kv) + } + envMap[parts[0]] = parts[1] + } + params.Env = envMap + } + if v, _ := cmd.Flags().GetString("gpu-profile"); v != "" { + params.GPU = hypemansdk.InstanceNewParamsGPU{ + Profile: param.NewOpt(v), + } + } + if v, _ := cmd.Flags().GetString("hypervisor"); v != "" { + params.Hypervisor = hypemansdk.InstanceNewParamsHypervisor(v) + } + if cmd.Flags().Changed("network-enabled") { + v, _ := cmd.Flags().GetBool("network-enabled") + params.Network = hypemansdk.InstanceNewParamsNetwork{ + Enabled: param.NewOpt(v), + } + } + if v, _ := cmd.Flags().GetString("bandwidth-download"); v != "" { + params.Network.BandwidthDownload = param.NewOpt(v) + } + if v, _ := cmd.Flags().GetString("bandwidth-upload"); v != "" { + params.Network.BandwidthUpload = param.NewOpt(v) + } + if volumeSpecs, _ := cmd.Flags().GetStringArray("volume"); len(volumeSpecs) > 0 { + var mounts []hypemansdk.VolumeMountParam + for _, spec := range volumeSpecs { + parts := strings.SplitN(spec, ":", 3) + if len(parts) < 2 { + return fmt.Errorf("invalid volume format: %s (expected volume-id:mount-path[:ro])", spec) + } + mount := hypemansdk.VolumeMountParam{ + VolumeID: parts[0], + MountPath: parts[1], + } + if len(parts) == 3 && parts[2] == "ro" { + mount.Readonly = param.NewOpt(true) + } + mounts = append(mounts, mount) + } + params.Volumes = mounts + } + + instance, err := client.Instances.New(cmd.Context(), params) + if err != nil { + return fmt.Errorf("failed to create instance: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(instance) + } + + pterm.Success.Printf("Created instance %s (%s)\n", instance.Name, instance.ID) + printInstanceDetail(instance) + return nil +} + +func runInstanceList(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + instances, err := client.Instances.List(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to list instances: %w", err) + } + + if output == "json" { + if instances == nil || len(*instances) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(*instances) + } + + if instances == nil || len(*instances) == 0 { + pterm.Info.Println("No instances found") + return nil + } + + tableData := pterm.TableData{{"ID", "Name", "Image", "State", "vCPUs", "Size", "Created At"}} + for _, inst := range *instances { + tableData = append(tableData, []string{ + inst.ID, + inst.Name, + inst.Image, + string(inst.State), + fmt.Sprintf("%d", inst.Vcpus), + util.OrDash(inst.Size), + util.FormatLocal(inst.CreatedAt), + }) + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runInstanceGet(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + instance, err := client.Instances.Get(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to get instance: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(instance) + } + + printInstanceDetail(instance) + return nil +} + +func runInstanceDelete(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + if err := client.Instances.Delete(cmd.Context(), args[0]); err != nil { + return fmt.Errorf("failed to delete instance: %w", err) + } + + pterm.Success.Printf("Deleted instance %s\n", args[0]) + return nil +} + +func runInstanceStart(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + instance, err := client.Instances.Start(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to start instance: %w", err) + } + + pterm.Success.Printf("Started instance %s (state: %s)\n", instance.Name, instance.State) + return nil +} + +func runInstanceStop(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + instance, err := client.Instances.Stop(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to stop instance: %w", err) + } + + pterm.Success.Printf("Stopped instance %s (state: %s)\n", instance.Name, instance.State) + return nil +} + +func runInstanceRestore(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + instance, err := client.Instances.Restore(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to restore instance: %w", err) + } + + pterm.Success.Printf("Restored instance %s (state: %s)\n", instance.Name, instance.State) + return nil +} + +func runInstanceStandby(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + instance, err := client.Instances.Standby(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to put instance in standby: %w", err) + } + + pterm.Success.Printf("Instance %s is now in standby (state: %s)\n", instance.Name, instance.State) + return nil +} + +func runInstanceLogs(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + follow, _ := cmd.Flags().GetBool("follow") + tail, _ := cmd.Flags().GetInt64("tail") + source, _ := cmd.Flags().GetString("source") + + params := hypemansdk.InstanceLogsParams{ + Follow: param.NewOpt(follow), + } + if tail > 0 { + params.Tail = param.NewOpt(tail) + } + if source != "" { + params.Source = hypemansdk.InstanceLogsParamsSource(source) + } + + stream := client.Instances.LogsStreaming(cmd.Context(), args[0], params) + for stream.Next() { + fmt.Println(stream.Current()) + } + if stream.Err() != nil { + return fmt.Errorf("log stream error: %w", stream.Err()) + } + return nil +} + +func runInstanceStat(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + path, _ := cmd.Flags().GetString("path") + + params := hypemansdk.InstanceStatParams{ + Path: path, + } + if cmd.Flags().Changed("follow-links") { + v, _ := cmd.Flags().GetBool("follow-links") + params.FollowLinks = param.NewOpt(v) + } + + info, err := client.Instances.Stat(cmd.Context(), args[0], params) + if err != nil { + return fmt.Errorf("failed to stat path: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(info) + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"Exists", fmt.Sprintf("%t", info.Exists)}, + {"Is File", fmt.Sprintf("%t", info.IsFile)}, + {"Is Dir", fmt.Sprintf("%t", info.IsDir)}, + {"Is Symlink", fmt.Sprintf("%t", info.IsSymlink)}, + {"Size", fmt.Sprintf("%d", info.Size)}, + {"Mode", fmt.Sprintf("%o", info.Mode)}, + } + if info.LinkTarget != "" { + tableData = append(tableData, []string{"Link Target", info.LinkTarget}) + } + if info.Error != "" { + tableData = append(tableData, []string{"Error", info.Error}) + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runInstanceVolumeAttach(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + instanceID, _ := cmd.Flags().GetString("instance-id") + mountPath, _ := cmd.Flags().GetString("mount-path") + + params := hypemansdk.InstanceVolumeAttachParams{ + ID: instanceID, + MountPath: mountPath, + } + if cmd.Flags().Changed("readonly") { + v, _ := cmd.Flags().GetBool("readonly") + params.Readonly = param.NewOpt(v) + } + + instance, err := client.Instances.Volumes.Attach(cmd.Context(), args[0], params) + if err != nil { + return fmt.Errorf("failed to attach volume: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(instance) + } + + pterm.Success.Printf("Attached volume %s to instance %s at %s\n", args[0], instanceID, mountPath) + return nil +} + +func runInstanceVolumeDetach(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + instanceID, _ := cmd.Flags().GetString("instance-id") + + params := hypemansdk.InstanceVolumeDetachParams{ + ID: instanceID, + } + + instance, err := client.Instances.Volumes.Detach(cmd.Context(), args[0], params) + if err != nil { + return fmt.Errorf("failed to detach volume: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(instance) + } + + pterm.Success.Printf("Detached volume %s from instance %s\n", args[0], instanceID) + return nil +} + +func printInstanceDetail(inst *hypemansdk.Instance) { + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", inst.ID}, + {"Name", inst.Name}, + {"Image", inst.Image}, + {"State", string(inst.State)}, + {"vCPUs", fmt.Sprintf("%d", inst.Vcpus)}, + {"Size", util.OrDash(inst.Size)}, + {"Overlay Size", util.OrDash(inst.OverlaySize)}, + {"Hotplug Size", util.OrDash(inst.HotplugSize)}, + {"Disk I/O BPS", util.OrDash(inst.DiskIoBps)}, + {"Hypervisor", string(inst.Hypervisor)}, + {"Has Snapshot", fmt.Sprintf("%t", inst.HasSnapshot)}, + {"Created At", util.FormatLocal(inst.CreatedAt)}, + } + if inst.Network.Enabled { + tableData = append(tableData, []string{"Network IP", util.OrDash(inst.Network.IP)}) + } + if len(inst.Volumes) > 0 { + var volStrs []string + for _, v := range inst.Volumes { + volStrs = append(volStrs, fmt.Sprintf("%s@%s", v.VolumeID, v.MountPath)) + } + tableData = append(tableData, []string{"Volumes", strings.Join(volStrs, ", ")}) + } + if len(inst.Env) > 0 { + envBytes, _ := json.Marshal(inst.Env) + tableData = append(tableData, []string{"Env", string(envBytes)}) + } + table.PrintTableNoPad(tableData, true) +} diff --git a/cmd/hypeman/resource.go b/cmd/hypeman/resource.go new file mode 100644 index 0000000..d3e5e78 --- /dev/null +++ b/cmd/hypeman/resource.go @@ -0,0 +1,102 @@ +package hypeman + +import ( + "fmt" + + "github.com/kernel/cli/pkg/table" + "github.com/kernel/cli/pkg/util" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +var resourceCmd = &cobra.Command{ + Use: "resource", + Aliases: []string{"resources"}, + Short: "View Hypeman host resources", +} + +var resourceGetCmd = &cobra.Command{ + Use: "get", + Short: "Get current host resource capacity and allocation status", + RunE: runResourceGet, +} + +func init() { + resourceCmd.AddCommand(resourceGetCmd) + + resourceGetCmd.Flags().StringP("output", "o", "", "Output format: json") +} + +func runResourceGet(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + resources, err := client.Resources.Get(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to get resources: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(resources) + } + + // Resource summary table + tableData := pterm.TableData{ + {"Resource", "Capacity", "Effective Limit", "Allocated", "Available", "Oversub Ratio"}, + { + resources.CPU.Type, + fmt.Sprintf("%d", resources.CPU.Capacity), + fmt.Sprintf("%d", resources.CPU.EffectiveLimit), + fmt.Sprintf("%d", resources.CPU.Allocated), + fmt.Sprintf("%d", resources.CPU.Available), + fmt.Sprintf("%.2f", resources.CPU.OversubRatio), + }, + { + resources.Memory.Type, + fmt.Sprintf("%d", resources.Memory.Capacity), + fmt.Sprintf("%d", resources.Memory.EffectiveLimit), + fmt.Sprintf("%d", resources.Memory.Allocated), + fmt.Sprintf("%d", resources.Memory.Available), + fmt.Sprintf("%.2f", resources.Memory.OversubRatio), + }, + { + resources.Disk.Type, + fmt.Sprintf("%d", resources.Disk.Capacity), + fmt.Sprintf("%d", resources.Disk.EffectiveLimit), + fmt.Sprintf("%d", resources.Disk.Allocated), + fmt.Sprintf("%d", resources.Disk.Available), + fmt.Sprintf("%.2f", resources.Disk.OversubRatio), + }, + { + resources.Network.Type, + fmt.Sprintf("%d", resources.Network.Capacity), + fmt.Sprintf("%d", resources.Network.EffectiveLimit), + fmt.Sprintf("%d", resources.Network.Allocated), + fmt.Sprintf("%d", resources.Network.Available), + fmt.Sprintf("%.2f", resources.Network.OversubRatio), + }, + } + table.PrintTableNoPad(tableData, true) + + // Allocations + if len(resources.Allocations) > 0 { + pterm.Println() + pterm.Info.Println("Allocations:") + allocData := pterm.TableData{{"Instance", "Name", "CPU", "Memory (bytes)", "Disk (bytes)"}} + for _, a := range resources.Allocations { + allocData = append(allocData, []string{ + a.InstanceID, + a.InstanceName, + fmt.Sprintf("%d", a.CPU), + fmt.Sprintf("%d", a.MemoryBytes), + fmt.Sprintf("%d", a.DiskBytes), + }) + } + table.PrintTableNoPad(allocData, true) + } + + return nil +} diff --git a/cmd/hypeman/volume.go b/cmd/hypeman/volume.go new file mode 100644 index 0000000..b06aac9 --- /dev/null +++ b/cmd/hypeman/volume.go @@ -0,0 +1,243 @@ +package hypeman + +import ( + "fmt" + "os" + "strings" + + "github.com/kernel/cli/pkg/table" + "github.com/kernel/cli/pkg/util" + hypemansdk "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/packages/param" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +var volumeCmd = &cobra.Command{ + Use: "volume", + Aliases: []string{"volumes"}, + Short: "Manage Hypeman volumes", +} + +var volumeCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new empty volume", + RunE: runVolumeCreate, +} + +var volumeListCmd = &cobra.Command{ + Use: "list", + Short: "List volumes", + RunE: runVolumeList, +} + +var volumeGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get volume details", + Args: cobra.ExactArgs(1), + RunE: runVolumeGet, +} + +var volumeDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a volume", + Args: cobra.ExactArgs(1), + RunE: runVolumeDelete, +} + +var volumeCreateFromArchiveCmd = &cobra.Command{ + Use: "create-from-archive ", + Short: "Create a volume from a tar.gz archive", + Args: cobra.ExactArgs(1), + RunE: runVolumeCreateFromArchive, +} + +func init() { + volumeCmd.AddCommand(volumeCreateCmd) + volumeCmd.AddCommand(volumeListCmd) + volumeCmd.AddCommand(volumeGetCmd) + volumeCmd.AddCommand(volumeDeleteCmd) + volumeCmd.AddCommand(volumeCreateFromArchiveCmd) + + // volume create flags + volumeCreateCmd.Flags().String("name", "", "Volume name (required)") + _ = volumeCreateCmd.MarkFlagRequired("name") + volumeCreateCmd.Flags().Int64("size-gb", 0, "Size in gigabytes (required)") + _ = volumeCreateCmd.MarkFlagRequired("size-gb") + volumeCreateCmd.Flags().String("id", "", "Optional custom identifier") + volumeCreateCmd.Flags().StringP("output", "o", "", "Output format: json") + + // volume list flags + volumeListCmd.Flags().StringP("output", "o", "", "Output format: json") + + // volume get flags + volumeGetCmd.Flags().StringP("output", "o", "", "Output format: json") + + // volume create-from-archive flags + volumeCreateFromArchiveCmd.Flags().String("name", "", "Volume name (required)") + _ = volumeCreateFromArchiveCmd.MarkFlagRequired("name") + volumeCreateFromArchiveCmd.Flags().Int64("size-gb", 0, "Maximum size in GB (required)") + _ = volumeCreateFromArchiveCmd.MarkFlagRequired("size-gb") + volumeCreateFromArchiveCmd.Flags().String("id", "", "Optional custom volume ID") + volumeCreateFromArchiveCmd.Flags().StringP("output", "o", "", "Output format: json") +} + +func runVolumeCreate(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + sizeGB, _ := cmd.Flags().GetInt64("size-gb") + + params := hypemansdk.VolumeNewParams{ + Name: name, + SizeGB: sizeGB, + } + if v, _ := cmd.Flags().GetString("id"); v != "" { + params.ID = param.NewOpt(v) + } + + volume, err := client.Volumes.New(cmd.Context(), params) + if err != nil { + return fmt.Errorf("failed to create volume: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(volume) + } + + pterm.Success.Printf("Created volume %s (%s, %dGB)\n", volume.Name, volume.ID, volume.SizeGB) + return nil +} + +func runVolumeList(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + volumes, err := client.Volumes.List(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to list volumes: %w", err) + } + + if output == "json" { + if volumes == nil || len(*volumes) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(*volumes) + } + + if volumes == nil || len(*volumes) == 0 { + pterm.Info.Println("No volumes found") + return nil + } + + tableData := pterm.TableData{{"ID", "Name", "Size (GB)", "Attachments", "Created At"}} + for _, vol := range *volumes { + attachStr := "-" + if len(vol.Attachments) > 0 { + var parts []string + for _, a := range vol.Attachments { + parts = append(parts, fmt.Sprintf("%s@%s", a.InstanceID, a.MountPath)) + } + attachStr = strings.Join(parts, ", ") + } + tableData = append(tableData, []string{ + vol.ID, + vol.Name, + fmt.Sprintf("%d", vol.SizeGB), + attachStr, + util.FormatLocal(vol.CreatedAt), + }) + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runVolumeGet(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + + volume, err := client.Volumes.Get(cmd.Context(), args[0]) + if err != nil { + return fmt.Errorf("failed to get volume: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(volume) + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", volume.ID}, + {"Name", volume.Name}, + {"Size (GB)", fmt.Sprintf("%d", volume.SizeGB)}, + {"Created At", util.FormatLocal(volume.CreatedAt)}, + } + if len(volume.Attachments) > 0 { + for _, a := range volume.Attachments { + tableData = append(tableData, []string{"Attachment", fmt.Sprintf("%s@%s (readonly: %t)", a.InstanceID, a.MountPath, a.Readonly)}) + } + } + table.PrintTableNoPad(tableData, true) + return nil +} + +func runVolumeDelete(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + + if err := client.Volumes.Delete(cmd.Context(), args[0]); err != nil { + return fmt.Errorf("failed to delete volume: %w", err) + } + + pterm.Success.Printf("Deleted volume %s\n", args[0]) + return nil +} + +func runVolumeCreateFromArchive(cmd *cobra.Command, args []string) error { + client, err := mustGetClient(cmd) + if err != nil { + return err + } + output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + sizeGB, _ := cmd.Flags().GetInt64("size-gb") + + archivePath := args[0] + file, err := os.Open(archivePath) + if err != nil { + return fmt.Errorf("failed to open archive: %w", err) + } + defer file.Close() + + params := hypemansdk.VolumeNewFromArchiveParams{ + Name: name, + SizeGB: sizeGB, + } + if v, _ := cmd.Flags().GetString("id"); v != "" { + params.ID = param.NewOpt(v) + } + + volume, err := client.Volumes.NewFromArchive(cmd.Context(), file, params) + if err != nil { + return fmt.Errorf("failed to create volume from archive: %w", err) + } + + if output == "json" { + return util.PrintPrettyJSON(volume) + } + + pterm.Success.Printf("Created volume %s from archive (%s, %dGB)\n", volume.Name, volume.ID, volume.SizeGB) + return nil +} diff --git a/cmd/root.go b/cmd/root.go index 5542dcb..4a03223 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/fang" "github.com/charmbracelet/lipgloss/v2" + "github.com/kernel/cli/cmd/hypeman" "github.com/kernel/cli/cmd/mcp" "github.com/kernel/cli/cmd/proxies" "github.com/kernel/cli/pkg/auth" @@ -90,7 +91,7 @@ func isAuthExempt(cmd *cobra.Command) bool { // Check if the top-level command is in the exempt list switch topLevel.Name() { - case "login", "logout", "help", "completion", "create", "mcp", "upgrade": + case "login", "logout", "help", "completion", "create", "mcp", "upgrade", "hypeman": return true case "auth": // Only exempt the auth command itself (status display), not its subcommands @@ -146,6 +147,7 @@ func init() { rootCmd.AddCommand(credentialProvidersCmd) rootCmd.AddCommand(createCmd) rootCmd.AddCommand(mcp.MCPCmd) + rootCmd.AddCommand(hypeman.HypemanCmd) rootCmd.AddCommand(upgradeCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { diff --git a/go.mod b/go.mod index 436fdb3..986b0a4 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,14 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 + github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1 github.com/kernel/kernel-go-sdk v0.33.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 - github.com/stretchr/testify v1.11.0 + github.com/stretchr/testify v1.11.1 github.com/zalando/go-keyring v0.2.6 golang.org/x/crypto v0.47.0 golang.org/x/oauth2 v0.30.0 @@ -27,7 +27,6 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/charmbracelet/colorprofile v0.3.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect diff --git a/go.sum b/go.sum index d254c19..366955a 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,6 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= -github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boyter/gocodewalker v1.4.0 h1:fVmFeQxKpj5tlpjPcyTtJ96btgaHYd9yn6m+T/66et4= github.com/boyter/gocodewalker v1.4.0/go.mod h1:hXG8xzR1uURS+99P5/3xh3uWHjaV2XfoMMmvPyhrCDg= github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= @@ -66,6 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1 h1:/Xquk6Ryqnhf6X2UTUbdGeSonqz4L1xfygvfdqaw34c= +github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= github.com/kernel/kernel-go-sdk v0.33.0 h1:kfk2bwrw3mbR4IW3JMnOj6Tecxor44YjM8YV153xDTY= github.com/kernel/kernel-go-sdk v0.33.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -99,8 +99,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= -github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -125,12 +123,11 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= -github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= From 1d5b939b1c95e8875590c8cdbd47fce45a6a8600 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:58:13 +0000 Subject: [PATCH 2/4] feat(browsers): add computer get-mouse-position and batch commands Update kernel-go-sdk to v0.34.0 which includes new Browsers.Computer methods. Add CLI support for: - `kernel browsers computer get-mouse-position ` - Get current mouse position - `kernel browsers computer batch ` - Execute batch of computer actions from JSON Co-authored-by: Cursor --- cmd/browsers.go | 84 +++++++++++++++++++++++++++++++++++++++++++- cmd/browsers_test.go | 14 ++++++++ go.mod | 2 +- go.sum | 4 +-- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 37f3943..54ab236 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -92,9 +92,11 @@ type BrowserPlaywrightService interface { // BrowserComputerService defines the subset we use for OS-level mouse & screen. type BrowserComputerService interface { + Batch(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) (err error) CaptureScreenshot(ctx context.Context, id string, body kernel.BrowserComputerCaptureScreenshotParams, opts ...option.RequestOption) (res *http.Response, err error) ClickMouse(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) (err error) DragMouse(ctx context.Context, id string, body kernel.BrowserComputerDragMouseParams, opts ...option.RequestOption) (err error) + GetMousePosition(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.BrowserComputerGetMousePositionResponse, err error) MoveMouse(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) (err error) PressKey(ctx context.Context, id string, body kernel.BrowserComputerPressKeyParams, opts ...option.RequestOption) (err error) Scroll(ctx context.Context, id string, body kernel.BrowserComputerScrollParams, opts ...option.RequestOption) (err error) @@ -740,6 +742,16 @@ type BrowsersComputerSetCursorInput struct { Hidden bool } +type BrowsersComputerGetMousePositionInput struct { + Identifier string + Output string +} + +type BrowsersComputerBatchInput struct { + Identifier string + ActionsJSON string +} + func (b BrowsersCmd) ComputerClickMouse(ctx context.Context, in BrowsersComputerClickMouseInput) error { if b.computer == nil { pterm.Error.Println("computer service not available") @@ -956,6 +968,49 @@ func (b BrowsersCmd) ComputerSetCursor(ctx context.Context, in BrowsersComputerS return nil } +func (b BrowsersCmd) ComputerGetMousePosition(ctx context.Context, in BrowsersComputerGetMousePositionInput) error { + if b.computer == nil { + pterm.Error.Println("computer service not available") + return nil + } + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + res, err := b.computer.GetMousePosition(ctx, br.SessionID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + if in.Output == "json" { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(res) + } + fmt.Printf("x: %d\ny: %d\n", res.X, res.Y) + return nil +} + +func (b BrowsersCmd) ComputerBatch(ctx context.Context, in BrowsersComputerBatchInput) error { + if b.computer == nil { + pterm.Error.Println("computer service not available") + return nil + } + br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + var body kernel.BrowserComputerBatchParams + if err := json.Unmarshal([]byte(in.ActionsJSON), &body); err != nil { + pterm.Error.Printf("Invalid JSON: %v\n", err) + return nil + } + if err := b.computer.Batch(ctx, br.SessionID, body); err != nil { + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Println("Batch actions executed") + return nil +} + // Replays type BrowsersReplaysListInput struct { Identifier string @@ -2300,7 +2355,16 @@ func init() { computerSetCursor.Flags().String("hidden", "", "Whether to hide the cursor: true or false") _ = computerSetCursor.MarkFlagRequired("hidden") - computerRoot.AddCommand(computerClick, computerMove, computerScreenshot, computerType, computerPressKey, computerScroll, computerDrag, computerSetCursor) + // computer get-mouse-position + computerGetMousePosition := &cobra.Command{Use: "get-mouse-position ", Short: "Get current mouse cursor position", Args: cobra.ExactArgs(1), RunE: runBrowsersComputerGetMousePosition} + computerGetMousePosition.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // computer batch + computerBatch := &cobra.Command{Use: "batch ", Short: "Execute a batch of computer actions from JSON", Args: cobra.ExactArgs(1), RunE: runBrowsersComputerBatch} + computerBatch.Flags().String("actions", "", "JSON array of actions (e.g., [{\"type\":\"click_mouse\",...}])") + _ = computerBatch.MarkFlagRequired("actions") + + computerRoot.AddCommand(computerClick, computerMove, computerScreenshot, computerType, computerPressKey, computerScroll, computerDrag, computerSetCursor, computerGetMousePosition, computerBatch) browsersCmd.AddCommand(computerRoot) // playwright @@ -3001,6 +3065,24 @@ func runBrowsersComputerSetCursor(cmd *cobra.Command, args []string) error { return b.ComputerSetCursor(cmd.Context(), BrowsersComputerSetCursorInput{Identifier: args[0], Hidden: hidden}) } +func runBrowsersComputerGetMousePosition(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + output, _ := cmd.Flags().GetString("output") + + b := BrowsersCmd{browsers: &svc, computer: &svc.Computer} + return b.ComputerGetMousePosition(cmd.Context(), BrowsersComputerGetMousePositionInput{Identifier: args[0], Output: output}) +} + +func runBrowsersComputerBatch(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + svc := client.Browsers + actionsJSON, _ := cmd.Flags().GetString("actions") + + b := BrowsersCmd{browsers: &svc, computer: &svc.Computer} + return b.ComputerBatch(cmd.Context(), BrowsersComputerBatchInput{Identifier: args[0], ActionsJSON: actionsJSON}) +} + func truncateURL(url string, maxLen int) string { if len(url) <= maxLen { return url diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 696e29a..0a1b13a 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -671,7 +671,9 @@ func makeStream[T any](vals []T) *ssestream.Stream[T] { // --- Fake for Computer --- type FakeComputerService struct { + BatchFunc func(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) error ClickMouseFunc func(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) error + GetMousePositionFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserComputerGetMousePositionResponse, error) MoveMouseFunc func(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error CaptureScreenshotFunc func(ctx context.Context, id string, body kernel.BrowserComputerCaptureScreenshotParams, opts ...option.RequestOption) (*http.Response, error) PressKeyFunc func(ctx context.Context, id string, body kernel.BrowserComputerPressKeyParams, opts ...option.RequestOption) error @@ -681,12 +683,24 @@ type FakeComputerService struct { SetCursorVisibilityFunc func(ctx context.Context, id string, body kernel.BrowserComputerSetCursorVisibilityParams, opts ...option.RequestOption) (*kernel.BrowserComputerSetCursorVisibilityResponse, error) } +func (f *FakeComputerService) Batch(ctx context.Context, id string, body kernel.BrowserComputerBatchParams, opts ...option.RequestOption) error { + if f.BatchFunc != nil { + return f.BatchFunc(ctx, id, body, opts...) + } + return nil +} func (f *FakeComputerService) ClickMouse(ctx context.Context, id string, body kernel.BrowserComputerClickMouseParams, opts ...option.RequestOption) error { if f.ClickMouseFunc != nil { return f.ClickMouseFunc(ctx, id, body, opts...) } return nil } +func (f *FakeComputerService) GetMousePosition(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.BrowserComputerGetMousePositionResponse, error) { + if f.GetMousePositionFunc != nil { + return f.GetMousePositionFunc(ctx, id, opts...) + } + return &kernel.BrowserComputerGetMousePositionResponse{X: 100, Y: 200}, nil +} func (f *FakeComputerService) MoveMouse(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error { if f.MoveMouseFunc != nil { return f.MoveMouseFunc(ctx, id, body, opts...) diff --git a/go.mod b/go.mod index 986b0a4..50c4c59 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1 - github.com/kernel/kernel-go-sdk v0.33.0 + github.com/kernel/kernel-go-sdk v0.34.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index 366955a..a2821c5 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1 h1:/Xquk6Ryqnhf6X2UTUbdGeSonqz4L1xfygvfdqaw34c= github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= -github.com/kernel/kernel-go-sdk v0.33.0 h1:kfk2bwrw3mbR4IW3JMnOj6Tecxor44YjM8YV153xDTY= -github.com/kernel/kernel-go-sdk v0.33.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.34.0 h1:zyuJPzjZJnpcFVlh8R8F5RPPjcKLU5yorasRTb4NPxU= +github.com/kernel/kernel-go-sdk v0.34.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From fe7b346f2ffb9bb16b3540b22f1f6926194a49ee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 18 Feb 2026 20:05:31 +0000 Subject: [PATCH 3/4] chore: remove all hypeman commands and dependency - Remove cmd/hypeman directory with all hypeman subcommands - Remove hypeman import and command registration from root.go - Remove hypeman from auth exemption list - Remove github.com/kernel/hypeman-go dependency from go.mod Co-authored-by: Mason Williams --- cmd/hypeman/build.go | 261 ------------------ cmd/hypeman/device.go | 228 ---------------- cmd/hypeman/hypeman.go | 48 ---- cmd/hypeman/image.go | 183 ------------- cmd/hypeman/ingress.go | 241 ----------------- cmd/hypeman/instance.go | 579 ---------------------------------------- cmd/hypeman/resource.go | 102 ------- cmd/hypeman/volume.go | 243 ----------------- cmd/root.go | 4 +- go.mod | 1 - go.sum | 2 - 11 files changed, 1 insertion(+), 1891 deletions(-) delete mode 100644 cmd/hypeman/build.go delete mode 100644 cmd/hypeman/device.go delete mode 100644 cmd/hypeman/hypeman.go delete mode 100644 cmd/hypeman/image.go delete mode 100644 cmd/hypeman/ingress.go delete mode 100644 cmd/hypeman/instance.go delete mode 100644 cmd/hypeman/resource.go delete mode 100644 cmd/hypeman/volume.go diff --git a/cmd/hypeman/build.go b/cmd/hypeman/build.go deleted file mode 100644 index 0844508..0000000 --- a/cmd/hypeman/build.go +++ /dev/null @@ -1,261 +0,0 @@ -package hypeman - -import ( - "fmt" - "os" - - "github.com/kernel/cli/pkg/table" - "github.com/kernel/cli/pkg/util" - hypemansdk "github.com/kernel/hypeman-go" - "github.com/kernel/hypeman-go/packages/param" - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -var buildCmd = &cobra.Command{ - Use: "build", - Aliases: []string{"builds"}, - Short: "Manage Hypeman builds", -} - -var buildCreateCmd = &cobra.Command{ - Use: "create ", - Short: "Create a new build job from a source tarball", - Args: cobra.ExactArgs(1), - RunE: runBuildCreate, -} - -var buildListCmd = &cobra.Command{ - Use: "list", - Short: "List builds", - RunE: runBuildList, -} - -var buildGetCmd = &cobra.Command{ - Use: "get ", - Short: "Get build details", - Args: cobra.ExactArgs(1), - RunE: runBuildGet, -} - -var buildCancelCmd = &cobra.Command{ - Use: "cancel ", - Short: "Cancel a build", - Args: cobra.ExactArgs(1), - RunE: runBuildCancel, -} - -var buildEventsCmd = &cobra.Command{ - Use: "events ", - Short: "Stream build events", - Args: cobra.ExactArgs(1), - RunE: runBuildEvents, -} - -func init() { - buildCmd.AddCommand(buildCreateCmd) - buildCmd.AddCommand(buildListCmd) - buildCmd.AddCommand(buildGetCmd) - buildCmd.AddCommand(buildCancelCmd) - buildCmd.AddCommand(buildEventsCmd) - - // build create flags - buildCreateCmd.Flags().String("dockerfile", "", "Dockerfile content (if not in source tarball)") - buildCreateCmd.Flags().String("base-image-digest", "", "Optional pinned base image digest") - buildCreateCmd.Flags().String("cache-scope", "", "Tenant-specific cache key prefix") - buildCreateCmd.Flags().String("global-cache-key", "", "Global cache identifier (e.g., 'node', 'python')") - buildCreateCmd.Flags().String("is-admin-build", "", "Set to 'true' for admin builds with global cache push access") - buildCreateCmd.Flags().String("secrets", "", "JSON array of secret references to inject during build") - buildCreateCmd.Flags().Int64("timeout-seconds", 0, "Build timeout in seconds (default 600)") - buildCreateCmd.Flags().StringP("output", "o", "", "Output format: json") - - // build list flags - buildListCmd.Flags().StringP("output", "o", "", "Output format: json") - - // build get flags - buildGetCmd.Flags().StringP("output", "o", "", "Output format: json") - - // build events flags - buildEventsCmd.Flags().BoolP("follow", "f", false, "Continue streaming new events") -} - -func runBuildCreate(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - sourcePath := args[0] - file, err := os.Open(sourcePath) - if err != nil { - return fmt.Errorf("failed to open source tarball: %w", err) - } - defer file.Close() - - params := hypemansdk.BuildNewParams{ - Source: file, - } - - if v, _ := cmd.Flags().GetString("dockerfile"); v != "" { - params.Dockerfile = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("base-image-digest"); v != "" { - params.BaseImageDigest = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("cache-scope"); v != "" { - params.CacheScope = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("global-cache-key"); v != "" { - params.GlobalCacheKey = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("is-admin-build"); v != "" { - params.IsAdminBuild = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("secrets"); v != "" { - params.Secrets = param.NewOpt(v) - } - if cmd.Flags().Changed("timeout-seconds") { - v, _ := cmd.Flags().GetInt64("timeout-seconds") - params.TimeoutSeconds = param.NewOpt(v) - } - - build, err := client.Builds.New(cmd.Context(), params) - if err != nil { - return fmt.Errorf("failed to create build: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(build) - } - - pterm.Success.Printf("Created build %s (status: %s)\n", build.ID, build.Status) - printBuildDetail(build) - return nil -} - -func runBuildList(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - builds, err := client.Builds.List(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to list builds: %w", err) - } - - if output == "json" { - if builds == nil || len(*builds) == 0 { - fmt.Println("[]") - return nil - } - return util.PrintPrettyJSONSlice(*builds) - } - - if builds == nil || len(*builds) == 0 { - pterm.Info.Println("No builds found") - return nil - } - - tableData := pterm.TableData{{"ID", "Status", "Image Ref", "Duration (ms)", "Created At"}} - for _, b := range *builds { - durationStr := "-" - if b.DurationMs > 0 { - durationStr = fmt.Sprintf("%d", b.DurationMs) - } - tableData = append(tableData, []string{ - b.ID, - string(b.Status), - util.OrDash(b.ImageRef), - durationStr, - util.FormatLocal(b.CreatedAt), - }) - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runBuildGet(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - build, err := client.Builds.Get(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to get build: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(build) - } - - printBuildDetail(build) - return nil -} - -func runBuildCancel(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - if err := client.Builds.Cancel(cmd.Context(), args[0]); err != nil { - return fmt.Errorf("failed to cancel build: %w", err) - } - - pterm.Success.Printf("Cancelled build %s\n", args[0]) - return nil -} - -func runBuildEvents(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - follow, _ := cmd.Flags().GetBool("follow") - - params := hypemansdk.BuildEventsParams{ - Follow: param.NewOpt(follow), - } - - stream := client.Builds.EventsStreaming(cmd.Context(), args[0], params) - for stream.Next() { - event := stream.Current() - switch event.Type { - case hypemansdk.BuildEventTypeLog: - fmt.Print(event.Content) - case hypemansdk.BuildEventTypeStatus: - pterm.Info.Printf("Build status: %s\n", event.Status) - case hypemansdk.BuildEventTypeHeartbeat: - // ignore heartbeats - } - } - if stream.Err() != nil { - return fmt.Errorf("event stream error: %w", stream.Err()) - } - return nil -} - -func printBuildDetail(b *hypemansdk.Build) { - tableData := pterm.TableData{ - {"Property", "Value"}, - {"ID", b.ID}, - {"Status", string(b.Status)}, - {"Image Ref", util.OrDash(b.ImageRef)}, - {"Image Digest", util.OrDash(b.ImageDigest)}, - {"Duration (ms)", fmt.Sprintf("%d", b.DurationMs)}, - {"Created At", util.FormatLocal(b.CreatedAt)}, - } - if b.Error != "" { - tableData = append(tableData, []string{"Error", b.Error}) - } - if b.BuilderInstanceID != "" { - tableData = append(tableData, []string{"Builder Instance", b.BuilderInstanceID}) - } - table.PrintTableNoPad(tableData, true) -} diff --git a/cmd/hypeman/device.go b/cmd/hypeman/device.go deleted file mode 100644 index 842573d..0000000 --- a/cmd/hypeman/device.go +++ /dev/null @@ -1,228 +0,0 @@ -package hypeman - -import ( - "fmt" - - "github.com/kernel/cli/pkg/table" - "github.com/kernel/cli/pkg/util" - hypemansdk "github.com/kernel/hypeman-go" - "github.com/kernel/hypeman-go/packages/param" - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -var deviceCmd = &cobra.Command{ - Use: "device", - Aliases: []string{"devices"}, - Short: "Manage Hypeman devices", -} - -var deviceCreateCmd = &cobra.Command{ - Use: "create", - Short: "Register a device for passthrough", - RunE: runDeviceCreate, -} - -var deviceListCmd = &cobra.Command{ - Use: "list", - Short: "List registered devices", - RunE: runDeviceList, -} - -var deviceGetCmd = &cobra.Command{ - Use: "get ", - Short: "Get device details", - Args: cobra.ExactArgs(1), - RunE: runDeviceGet, -} - -var deviceDeleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Unregister a device", - Args: cobra.ExactArgs(1), - RunE: runDeviceDelete, -} - -var deviceListAvailableCmd = &cobra.Command{ - Use: "list-available", - Short: "Discover passthrough-capable devices on host", - RunE: runDeviceListAvailable, -} - -func init() { - deviceCmd.AddCommand(deviceCreateCmd) - deviceCmd.AddCommand(deviceListCmd) - deviceCmd.AddCommand(deviceGetCmd) - deviceCmd.AddCommand(deviceDeleteCmd) - deviceCmd.AddCommand(deviceListAvailableCmd) - - // device create flags - deviceCreateCmd.Flags().String("pci-address", "", "PCI address of the device (required, e.g., '0000:a2:00.0')") - _ = deviceCreateCmd.MarkFlagRequired("pci-address") - deviceCreateCmd.Flags().String("name", "", "Optional device name") - deviceCreateCmd.Flags().StringP("output", "o", "", "Output format: json") - - // device list flags - deviceListCmd.Flags().StringP("output", "o", "", "Output format: json") - - // device get flags - deviceGetCmd.Flags().StringP("output", "o", "", "Output format: json") - - // device list-available flags - deviceListAvailableCmd.Flags().StringP("output", "o", "", "Output format: json") -} - -func runDeviceCreate(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - pciAddress, _ := cmd.Flags().GetString("pci-address") - - params := hypemansdk.DeviceNewParams{ - PciAddress: pciAddress, - } - if v, _ := cmd.Flags().GetString("name"); v != "" { - params.Name = param.NewOpt(v) - } - - device, err := client.Devices.New(cmd.Context(), params) - if err != nil { - return fmt.Errorf("failed to register device: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(device) - } - - pterm.Success.Printf("Registered device %s (%s, type: %s)\n", device.Name, device.ID, device.Type) - return nil -} - -func runDeviceList(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - devices, err := client.Devices.List(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to list devices: %w", err) - } - - if output == "json" { - if devices == nil || len(*devices) == 0 { - fmt.Println("[]") - return nil - } - return util.PrintPrettyJSONSlice(*devices) - } - - if devices == nil || len(*devices) == 0 { - pterm.Info.Println("No devices found") - return nil - } - - tableData := pterm.TableData{{"ID", "Name", "Type", "PCI Address", "VFIO Bound", "Attached To", "Created At"}} - for _, dev := range *devices { - tableData = append(tableData, []string{ - dev.ID, - util.OrDash(dev.Name), - string(dev.Type), - dev.PciAddress, - fmt.Sprintf("%t", dev.BoundToVfio), - util.OrDash(dev.AttachedTo), - util.FormatLocal(dev.CreatedAt), - }) - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runDeviceGet(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - device, err := client.Devices.Get(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to get device: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(device) - } - - tableData := pterm.TableData{ - {"Property", "Value"}, - {"ID", device.ID}, - {"Name", util.OrDash(device.Name)}, - {"Type", string(device.Type)}, - {"PCI Address", device.PciAddress}, - {"Vendor ID", device.VendorID}, - {"Device ID", device.DeviceID}, - {"IOMMU Group", fmt.Sprintf("%d", device.IommuGroup)}, - {"VFIO Bound", fmt.Sprintf("%t", device.BoundToVfio)}, - {"Attached To", util.OrDash(device.AttachedTo)}, - {"Created At", util.FormatLocal(device.CreatedAt)}, - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runDeviceDelete(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - if err := client.Devices.Delete(cmd.Context(), args[0]); err != nil { - return fmt.Errorf("failed to unregister device: %w", err) - } - - pterm.Success.Printf("Unregistered device %s\n", args[0]) - return nil -} - -func runDeviceListAvailable(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - devices, err := client.Devices.ListAvailable(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to list available devices: %w", err) - } - - if output == "json" { - if devices == nil || len(*devices) == 0 { - fmt.Println("[]") - return nil - } - return util.PrintPrettyJSONSlice(*devices) - } - - if devices == nil || len(*devices) == 0 { - pterm.Info.Println("No available devices found") - return nil - } - - tableData := pterm.TableData{{"PCI Address", "Vendor", "Device", "IOMMU Group", "Current Driver"}} - for _, dev := range *devices { - tableData = append(tableData, []string{ - dev.PciAddress, - fmt.Sprintf("%s (%s)", util.OrDash(dev.VendorName), dev.VendorID), - fmt.Sprintf("%s (%s)", util.OrDash(dev.DeviceName), dev.DeviceID), - fmt.Sprintf("%d", dev.IommuGroup), - util.OrDash(dev.CurrentDriver), - }) - } - table.PrintTableNoPad(tableData, true) - return nil -} diff --git a/cmd/hypeman/hypeman.go b/cmd/hypeman/hypeman.go deleted file mode 100644 index b8dfdf4..0000000 --- a/cmd/hypeman/hypeman.go +++ /dev/null @@ -1,48 +0,0 @@ -package hypeman - -import ( - "fmt" - "os" - - hypemansdk "github.com/kernel/hypeman-go" - "github.com/kernel/hypeman-go/option" - "github.com/spf13/cobra" -) - -// HypemanCmd is the top-level command for Hypeman operations. -var HypemanCmd = &cobra.Command{ - Use: "hypeman", - Short: "Manage Hypeman instances, images, volumes, devices, ingresses, builds, and resources", - Long: "Commands for interacting with the Hypeman API for VM instance management", -} - -func init() { - HypemanCmd.AddCommand(instanceCmd) - HypemanCmd.AddCommand(imageCmd) - HypemanCmd.AddCommand(volumeCmd) - HypemanCmd.AddCommand(deviceCmd) - HypemanCmd.AddCommand(ingressCmd) - HypemanCmd.AddCommand(resourceCmd) - HypemanCmd.AddCommand(buildCmd) -} - -// getHypemanClient creates a Hypeman SDK client. -// It reads HYPEMAN_API_KEY and HYPEMAN_BASE_URL from the environment. -func getHypemanClient() (hypemansdk.Client, error) { - apiKey := os.Getenv("HYPEMAN_API_KEY") - if apiKey == "" { - return hypemansdk.Client{}, fmt.Errorf("HYPEMAN_API_KEY environment variable is required") - } - opts := []option.RequestOption{ - option.WithAPIKey(apiKey), - } - if baseURL := os.Getenv("HYPEMAN_BASE_URL"); baseURL != "" { - opts = append(opts, option.WithBaseURL(baseURL)) - } - return hypemansdk.NewClient(opts...), nil -} - -// mustGetClient is a helper that returns the client or sets the error on the command. -func mustGetClient(cmd *cobra.Command) (hypemansdk.Client, error) { - return getHypemanClient() -} diff --git a/cmd/hypeman/image.go b/cmd/hypeman/image.go deleted file mode 100644 index 192a969..0000000 --- a/cmd/hypeman/image.go +++ /dev/null @@ -1,183 +0,0 @@ -package hypeman - -import ( - "fmt" - - "github.com/kernel/cli/pkg/table" - "github.com/kernel/cli/pkg/util" - hypemansdk "github.com/kernel/hypeman-go" - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -var imageCmd = &cobra.Command{ - Use: "image", - Aliases: []string{"images"}, - Short: "Manage Hypeman images", -} - -var imageCreateCmd = &cobra.Command{ - Use: "create", - Short: "Pull and convert an OCI image", - RunE: runImageCreate, -} - -var imageListCmd = &cobra.Command{ - Use: "list", - Short: "List images", - RunE: runImageList, -} - -var imageGetCmd = &cobra.Command{ - Use: "get ", - Short: "Get image details", - Args: cobra.ExactArgs(1), - RunE: runImageGet, -} - -var imageDeleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Delete an image", - Args: cobra.ExactArgs(1), - RunE: runImageDelete, -} - -func init() { - imageCmd.AddCommand(imageCreateCmd) - imageCmd.AddCommand(imageListCmd) - imageCmd.AddCommand(imageGetCmd) - imageCmd.AddCommand(imageDeleteCmd) - - imageCreateCmd.Flags().String("name", "", "OCI image reference (e.g., docker.io/library/nginx:latest) (required)") - _ = imageCreateCmd.MarkFlagRequired("name") - imageCreateCmd.Flags().StringP("output", "o", "", "Output format: json") - - imageListCmd.Flags().StringP("output", "o", "", "Output format: json") - imageGetCmd.Flags().StringP("output", "o", "", "Output format: json") -} - -func runImageCreate(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - name, _ := cmd.Flags().GetString("name") - - image, err := client.Images.New(cmd.Context(), hypemansdk.ImageNewParams{ - Name: name, - }) - if err != nil { - return fmt.Errorf("failed to create image: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(image) - } - - pterm.Success.Printf("Created image %s (status: %s)\n", image.Name, image.Status) - printImageDetail(image) - return nil -} - -func runImageList(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - images, err := client.Images.List(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to list images: %w", err) - } - - if output == "json" { - if images == nil || len(*images) == 0 { - fmt.Println("[]") - return nil - } - return util.PrintPrettyJSONSlice(*images) - } - - if images == nil || len(*images) == 0 { - pterm.Info.Println("No images found") - return nil - } - - tableData := pterm.TableData{{"Name", "Status", "Digest", "Size (bytes)", "Created At"}} - for _, img := range *images { - sizeStr := "-" - if img.SizeBytes > 0 { - sizeStr = fmt.Sprintf("%d", img.SizeBytes) - } - tableData = append(tableData, []string{ - img.Name, - string(img.Status), - truncate(img.Digest, 20), - sizeStr, - util.FormatLocal(img.CreatedAt), - }) - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runImageGet(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - image, err := client.Images.Get(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to get image: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(image) - } - - printImageDetail(image) - return nil -} - -func runImageDelete(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - if err := client.Images.Delete(cmd.Context(), args[0]); err != nil { - return fmt.Errorf("failed to delete image: %w", err) - } - - pterm.Success.Printf("Deleted image %s\n", args[0]) - return nil -} - -func printImageDetail(img *hypemansdk.Image) { - tableData := pterm.TableData{ - {"Property", "Value"}, - {"Name", img.Name}, - {"Status", string(img.Status)}, - {"Digest", img.Digest}, - {"Size (bytes)", fmt.Sprintf("%d", img.SizeBytes)}, - {"Created At", util.FormatLocal(img.CreatedAt)}, - } - if img.Error != "" { - tableData = append(tableData, []string{"Error", img.Error}) - } - if img.WorkingDir != "" { - tableData = append(tableData, []string{"Working Dir", img.WorkingDir}) - } - table.PrintTableNoPad(tableData, true) -} - -func truncate(s string, maxLen int) string { - if len(s) <= maxLen { - return s - } - return s[:maxLen-3] + "..." -} diff --git a/cmd/hypeman/ingress.go b/cmd/hypeman/ingress.go deleted file mode 100644 index 56f025a..0000000 --- a/cmd/hypeman/ingress.go +++ /dev/null @@ -1,241 +0,0 @@ -package hypeman - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/kernel/cli/pkg/table" - "github.com/kernel/cli/pkg/util" - hypemansdk "github.com/kernel/hypeman-go" - "github.com/kernel/hypeman-go/packages/param" - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -var ingressCmd = &cobra.Command{ - Use: "ingress", - Aliases: []string{"ingresses"}, - Short: "Manage Hypeman ingresses", -} - -var ingressCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create an ingress", - RunE: runIngressCreate, -} - -var ingressListCmd = &cobra.Command{ - Use: "list", - Short: "List ingresses", - RunE: runIngressList, -} - -var ingressGetCmd = &cobra.Command{ - Use: "get ", - Short: "Get ingress details", - Args: cobra.ExactArgs(1), - RunE: runIngressGet, -} - -var ingressDeleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Delete an ingress", - Args: cobra.ExactArgs(1), - RunE: runIngressDelete, -} - -func init() { - ingressCmd.AddCommand(ingressCreateCmd) - ingressCmd.AddCommand(ingressListCmd) - ingressCmd.AddCommand(ingressGetCmd) - ingressCmd.AddCommand(ingressDeleteCmd) - - // ingress create flags - ingressCreateCmd.Flags().String("name", "", "Human-readable name (required)") - _ = ingressCreateCmd.MarkFlagRequired("name") - ingressCreateCmd.Flags().StringArray("rule", nil, "Routing rule as JSON (repeatable, required)") - _ = ingressCreateCmd.MarkFlagRequired("rule") - ingressCreateCmd.Flags().StringP("output", "o", "", "Output format: json") - - // Simple rule flags for single-rule ingresses - ingressCreateCmd.Flags().String("hostname", "", "Match hostname (shorthand for single rule)") - ingressCreateCmd.Flags().Int64("match-port", 0, "Match port (shorthand for single rule)") - ingressCreateCmd.Flags().String("target-instance", "", "Target instance name or ID (shorthand for single rule)") - ingressCreateCmd.Flags().Int64("target-port", 0, "Target port (shorthand for single rule)") - ingressCreateCmd.Flags().Bool("tls", false, "Enable TLS termination (shorthand for single rule)") - ingressCreateCmd.Flags().Bool("redirect-http", false, "Redirect HTTP to HTTPS (shorthand for single rule)") - - // ingress list flags - ingressListCmd.Flags().StringP("output", "o", "", "Output format: json") - - // ingress get flags - ingressGetCmd.Flags().StringP("output", "o", "", "Output format: json") -} - -func runIngressCreate(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - name, _ := cmd.Flags().GetString("name") - - // Build rules from --rule JSON flags - ruleStrs, _ := cmd.Flags().GetStringArray("rule") - var rules []hypemansdk.IngressRuleParam - for _, ruleStr := range ruleStrs { - var rule hypemansdk.IngressRuleParam - if err := json.Unmarshal([]byte(ruleStr), &rule); err != nil { - return fmt.Errorf("invalid rule JSON: %w", err) - } - rules = append(rules, rule) - } - - // If no --rule flags but shorthand flags are provided, build a single rule - if len(rules) == 0 { - hostname, _ := cmd.Flags().GetString("hostname") - targetInstance, _ := cmd.Flags().GetString("target-instance") - targetPort, _ := cmd.Flags().GetInt64("target-port") - - if hostname != "" && targetInstance != "" && targetPort > 0 { - rule := hypemansdk.IngressRuleParam{ - Match: hypemansdk.IngressMatchParam{ - Hostname: hostname, - }, - Target: hypemansdk.IngressTargetParam{ - Instance: targetInstance, - Port: targetPort, - }, - } - if matchPort, _ := cmd.Flags().GetInt64("match-port"); matchPort > 0 { - rule.Match.Port = param.NewOpt(matchPort) - } - if cmd.Flags().Changed("tls") { - tls, _ := cmd.Flags().GetBool("tls") - rule.Tls = param.NewOpt(tls) - } - if cmd.Flags().Changed("redirect-http") { - redir, _ := cmd.Flags().GetBool("redirect-http") - rule.RedirectHTTP = param.NewOpt(redir) - } - rules = append(rules, rule) - } - } - - if len(rules) == 0 { - return fmt.Errorf("at least one --rule is required") - } - - ingress, err := client.Ingresses.New(cmd.Context(), hypemansdk.IngressNewParams{ - Name: name, - Rules: rules, - }) - if err != nil { - return fmt.Errorf("failed to create ingress: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(ingress) - } - - pterm.Success.Printf("Created ingress %s (%s)\n", ingress.Name, ingress.ID) - printIngressDetail(ingress) - return nil -} - -func runIngressList(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - ingresses, err := client.Ingresses.List(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to list ingresses: %w", err) - } - - if output == "json" { - if ingresses == nil || len(*ingresses) == 0 { - fmt.Println("[]") - return nil - } - return util.PrintPrettyJSONSlice(*ingresses) - } - - if ingresses == nil || len(*ingresses) == 0 { - pterm.Info.Println("No ingresses found") - return nil - } - - tableData := pterm.TableData{{"ID", "Name", "Rules", "Created At"}} - for _, ing := range *ingresses { - var ruleDescs []string - for _, r := range ing.Rules { - ruleDescs = append(ruleDescs, fmt.Sprintf("%s -> %s:%d", r.Match.Hostname, r.Target.Instance, r.Target.Port)) - } - tableData = append(tableData, []string{ - ing.ID, - ing.Name, - strings.Join(ruleDescs, "; "), - util.FormatLocal(ing.CreatedAt), - }) - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runIngressGet(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - ingress, err := client.Ingresses.Get(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to get ingress: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(ingress) - } - - printIngressDetail(ingress) - return nil -} - -func runIngressDelete(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - if err := client.Ingresses.Delete(cmd.Context(), args[0]); err != nil { - return fmt.Errorf("failed to delete ingress: %w", err) - } - - pterm.Success.Printf("Deleted ingress %s\n", args[0]) - return nil -} - -func printIngressDetail(ing *hypemansdk.Ingress) { - tableData := pterm.TableData{ - {"Property", "Value"}, - {"ID", ing.ID}, - {"Name", ing.Name}, - {"Created At", util.FormatLocal(ing.CreatedAt)}, - } - for i, r := range ing.Rules { - prefix := fmt.Sprintf("Rule %d", i+1) - tableData = append(tableData, - []string{prefix + " Hostname", r.Match.Hostname}, - []string{prefix + " Match Port", fmt.Sprintf("%d", r.Match.Port)}, - []string{prefix + " Target", fmt.Sprintf("%s:%d", r.Target.Instance, r.Target.Port)}, - []string{prefix + " TLS", fmt.Sprintf("%t", r.Tls)}, - []string{prefix + " Redirect HTTP", fmt.Sprintf("%t", r.RedirectHTTP)}, - ) - } - table.PrintTableNoPad(tableData, true) -} diff --git a/cmd/hypeman/instance.go b/cmd/hypeman/instance.go deleted file mode 100644 index 9e1d217..0000000 --- a/cmd/hypeman/instance.go +++ /dev/null @@ -1,579 +0,0 @@ -package hypeman - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/kernel/cli/pkg/table" - "github.com/kernel/cli/pkg/util" - hypemansdk "github.com/kernel/hypeman-go" - "github.com/kernel/hypeman-go/packages/param" - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -var instanceCmd = &cobra.Command{ - Use: "instance", - Aliases: []string{"instances"}, - Short: "Manage Hypeman instances", -} - -var instanceCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create and start an instance", - RunE: runInstanceCreate, -} - -var instanceListCmd = &cobra.Command{ - Use: "list", - Short: "List instances", - RunE: runInstanceList, -} - -var instanceGetCmd = &cobra.Command{ - Use: "get ", - Short: "Get instance details", - Args: cobra.ExactArgs(1), - RunE: runInstanceGet, -} - -var instanceDeleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Stop and delete an instance", - Args: cobra.ExactArgs(1), - RunE: runInstanceDelete, -} - -var instanceStartCmd = &cobra.Command{ - Use: "start ", - Short: "Start a stopped instance", - Args: cobra.ExactArgs(1), - RunE: runInstanceStart, -} - -var instanceStopCmd = &cobra.Command{ - Use: "stop ", - Short: "Stop an instance (graceful shutdown)", - Args: cobra.ExactArgs(1), - RunE: runInstanceStop, -} - -var instanceRestoreCmd = &cobra.Command{ - Use: "restore ", - Short: "Restore an instance from standby", - Args: cobra.ExactArgs(1), - RunE: runInstanceRestore, -} - -var instanceStandbyCmd = &cobra.Command{ - Use: "standby ", - Short: "Put an instance in standby (pause, snapshot, delete VMM)", - Args: cobra.ExactArgs(1), - RunE: runInstanceStandby, -} - -var instanceLogsCmd = &cobra.Command{ - Use: "logs ", - Short: "Stream instance logs", - Args: cobra.ExactArgs(1), - RunE: runInstanceLogs, -} - -var instanceStatCmd = &cobra.Command{ - Use: "stat ", - Short: "Get file information from the guest filesystem", - Args: cobra.ExactArgs(1), - RunE: runInstanceStat, -} - -var instanceVolumeAttachCmd = &cobra.Command{ - Use: "attach ", - Short: "Attach a volume to an instance", - Args: cobra.ExactArgs(1), - RunE: runInstanceVolumeAttach, -} - -var instanceVolumeDetachCmd = &cobra.Command{ - Use: "detach ", - Short: "Detach a volume from an instance", - Args: cobra.ExactArgs(1), - RunE: runInstanceVolumeDetach, -} - -var instanceVolumeCmd = &cobra.Command{ - Use: "volume", - Short: "Manage instance volume attachments", -} - -func init() { - instanceCmd.AddCommand(instanceCreateCmd) - instanceCmd.AddCommand(instanceListCmd) - instanceCmd.AddCommand(instanceGetCmd) - instanceCmd.AddCommand(instanceDeleteCmd) - instanceCmd.AddCommand(instanceStartCmd) - instanceCmd.AddCommand(instanceStopCmd) - instanceCmd.AddCommand(instanceRestoreCmd) - instanceCmd.AddCommand(instanceStandbyCmd) - instanceCmd.AddCommand(instanceLogsCmd) - instanceCmd.AddCommand(instanceStatCmd) - instanceCmd.AddCommand(instanceVolumeCmd) - instanceVolumeCmd.AddCommand(instanceVolumeAttachCmd) - instanceVolumeCmd.AddCommand(instanceVolumeDetachCmd) - - // instance create flags - instanceCreateCmd.Flags().String("image", "", "OCI image reference (required)") - instanceCreateCmd.Flags().String("name", "", "Human-readable name (required)") - _ = instanceCreateCmd.MarkFlagRequired("image") - _ = instanceCreateCmd.MarkFlagRequired("name") - instanceCreateCmd.Flags().String("size", "", "Base memory size (e.g., '1GB', '512MB')") - instanceCreateCmd.Flags().String("hotplug-size", "", "Additional memory for hotplug (e.g., '3GB')") - instanceCreateCmd.Flags().String("overlay-size", "", "Writable overlay disk size (e.g., '10GB')") - instanceCreateCmd.Flags().String("disk-io-bps", "", "Disk I/O rate limit (e.g., '100MB/s')") - instanceCreateCmd.Flags().Int64("vcpus", 0, "Number of virtual CPUs") - instanceCreateCmd.Flags().Bool("skip-guest-agent", false, "Skip guest-agent installation during boot") - instanceCreateCmd.Flags().Bool("skip-kernel-headers", false, "Skip kernel headers installation during boot") - instanceCreateCmd.Flags().StringSlice("devices", nil, "Device IDs or names to attach for GPU/PCI passthrough") - instanceCreateCmd.Flags().StringArrayP("env", "e", nil, "Environment variables (KEY=value, repeatable)") - instanceCreateCmd.Flags().String("gpu-profile", "", "vGPU profile name (e.g., 'L40S-1Q')") - instanceCreateCmd.Flags().String("hypervisor", "", "Hypervisor to use (cloud-hypervisor or qemu)") - instanceCreateCmd.Flags().Bool("network-enabled", true, "Attach instance to the default network") - instanceCreateCmd.Flags().String("bandwidth-download", "", "Download bandwidth limit (e.g., '1Gbps')") - instanceCreateCmd.Flags().String("bandwidth-upload", "", "Upload bandwidth limit (e.g., '1Gbps')") - instanceCreateCmd.Flags().StringArray("volume", nil, "Volume mounts (volume-id:mount-path[:ro], repeatable)") - instanceCreateCmd.Flags().StringP("output", "o", "", "Output format: json") - - // instance list flags - instanceListCmd.Flags().StringP("output", "o", "", "Output format: json") - - // instance get flags - instanceGetCmd.Flags().StringP("output", "o", "", "Output format: json") - - // instance logs flags - instanceLogsCmd.Flags().BoolP("follow", "f", false, "Continue streaming new lines") - instanceLogsCmd.Flags().Int64("tail", 0, "Number of lines to return from end") - instanceLogsCmd.Flags().String("source", "", "Log source: app, vmm, or hypeman") - - // instance stat flags - instanceStatCmd.Flags().String("path", "", "Path to stat in the guest filesystem (required)") - _ = instanceStatCmd.MarkFlagRequired("path") - instanceStatCmd.Flags().Bool("follow-links", false, "Follow symbolic links") - instanceStatCmd.Flags().StringP("output", "o", "", "Output format: json") - - // instance volume attach flags - instanceVolumeAttachCmd.Flags().String("instance-id", "", "Instance ID (required)") - _ = instanceVolumeAttachCmd.MarkFlagRequired("instance-id") - instanceVolumeAttachCmd.Flags().String("mount-path", "", "Path where volume should be mounted (required)") - _ = instanceVolumeAttachCmd.MarkFlagRequired("mount-path") - instanceVolumeAttachCmd.Flags().Bool("readonly", false, "Mount as read-only") - instanceVolumeAttachCmd.Flags().StringP("output", "o", "", "Output format: json") - - // instance volume detach flags - instanceVolumeDetachCmd.Flags().String("instance-id", "", "Instance ID (required)") - _ = instanceVolumeDetachCmd.MarkFlagRequired("instance-id") - instanceVolumeDetachCmd.Flags().StringP("output", "o", "", "Output format: json") -} - -func runInstanceCreate(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - image, _ := cmd.Flags().GetString("image") - name, _ := cmd.Flags().GetString("name") - - params := hypemansdk.InstanceNewParams{ - Image: image, - Name: name, - } - - if v, _ := cmd.Flags().GetString("size"); v != "" { - params.Size = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("hotplug-size"); v != "" { - params.HotplugSize = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("overlay-size"); v != "" { - params.OverlaySize = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("disk-io-bps"); v != "" { - params.DiskIoBps = param.NewOpt(v) - } - if cmd.Flags().Changed("vcpus") { - v, _ := cmd.Flags().GetInt64("vcpus") - params.Vcpus = param.NewOpt(v) - } - if cmd.Flags().Changed("skip-guest-agent") { - v, _ := cmd.Flags().GetBool("skip-guest-agent") - params.SkipGuestAgent = param.NewOpt(v) - } - if cmd.Flags().Changed("skip-kernel-headers") { - v, _ := cmd.Flags().GetBool("skip-kernel-headers") - params.SkipKernelHeaders = param.NewOpt(v) - } - if devices, _ := cmd.Flags().GetStringSlice("devices"); len(devices) > 0 { - params.Devices = devices - } - if envPairs, _ := cmd.Flags().GetStringArray("env"); len(envPairs) > 0 { - envMap := make(map[string]string) - for _, kv := range envPairs { - parts := strings.SplitN(kv, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid env format: %s (expected KEY=value)", kv) - } - envMap[parts[0]] = parts[1] - } - params.Env = envMap - } - if v, _ := cmd.Flags().GetString("gpu-profile"); v != "" { - params.GPU = hypemansdk.InstanceNewParamsGPU{ - Profile: param.NewOpt(v), - } - } - if v, _ := cmd.Flags().GetString("hypervisor"); v != "" { - params.Hypervisor = hypemansdk.InstanceNewParamsHypervisor(v) - } - if cmd.Flags().Changed("network-enabled") { - v, _ := cmd.Flags().GetBool("network-enabled") - params.Network = hypemansdk.InstanceNewParamsNetwork{ - Enabled: param.NewOpt(v), - } - } - if v, _ := cmd.Flags().GetString("bandwidth-download"); v != "" { - params.Network.BandwidthDownload = param.NewOpt(v) - } - if v, _ := cmd.Flags().GetString("bandwidth-upload"); v != "" { - params.Network.BandwidthUpload = param.NewOpt(v) - } - if volumeSpecs, _ := cmd.Flags().GetStringArray("volume"); len(volumeSpecs) > 0 { - var mounts []hypemansdk.VolumeMountParam - for _, spec := range volumeSpecs { - parts := strings.SplitN(spec, ":", 3) - if len(parts) < 2 { - return fmt.Errorf("invalid volume format: %s (expected volume-id:mount-path[:ro])", spec) - } - mount := hypemansdk.VolumeMountParam{ - VolumeID: parts[0], - MountPath: parts[1], - } - if len(parts) == 3 && parts[2] == "ro" { - mount.Readonly = param.NewOpt(true) - } - mounts = append(mounts, mount) - } - params.Volumes = mounts - } - - instance, err := client.Instances.New(cmd.Context(), params) - if err != nil { - return fmt.Errorf("failed to create instance: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(instance) - } - - pterm.Success.Printf("Created instance %s (%s)\n", instance.Name, instance.ID) - printInstanceDetail(instance) - return nil -} - -func runInstanceList(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - instances, err := client.Instances.List(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to list instances: %w", err) - } - - if output == "json" { - if instances == nil || len(*instances) == 0 { - fmt.Println("[]") - return nil - } - return util.PrintPrettyJSONSlice(*instances) - } - - if instances == nil || len(*instances) == 0 { - pterm.Info.Println("No instances found") - return nil - } - - tableData := pterm.TableData{{"ID", "Name", "Image", "State", "vCPUs", "Size", "Created At"}} - for _, inst := range *instances { - tableData = append(tableData, []string{ - inst.ID, - inst.Name, - inst.Image, - string(inst.State), - fmt.Sprintf("%d", inst.Vcpus), - util.OrDash(inst.Size), - util.FormatLocal(inst.CreatedAt), - }) - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runInstanceGet(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - instance, err := client.Instances.Get(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to get instance: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(instance) - } - - printInstanceDetail(instance) - return nil -} - -func runInstanceDelete(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - if err := client.Instances.Delete(cmd.Context(), args[0]); err != nil { - return fmt.Errorf("failed to delete instance: %w", err) - } - - pterm.Success.Printf("Deleted instance %s\n", args[0]) - return nil -} - -func runInstanceStart(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - instance, err := client.Instances.Start(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to start instance: %w", err) - } - - pterm.Success.Printf("Started instance %s (state: %s)\n", instance.Name, instance.State) - return nil -} - -func runInstanceStop(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - instance, err := client.Instances.Stop(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to stop instance: %w", err) - } - - pterm.Success.Printf("Stopped instance %s (state: %s)\n", instance.Name, instance.State) - return nil -} - -func runInstanceRestore(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - instance, err := client.Instances.Restore(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to restore instance: %w", err) - } - - pterm.Success.Printf("Restored instance %s (state: %s)\n", instance.Name, instance.State) - return nil -} - -func runInstanceStandby(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - instance, err := client.Instances.Standby(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to put instance in standby: %w", err) - } - - pterm.Success.Printf("Instance %s is now in standby (state: %s)\n", instance.Name, instance.State) - return nil -} - -func runInstanceLogs(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - follow, _ := cmd.Flags().GetBool("follow") - tail, _ := cmd.Flags().GetInt64("tail") - source, _ := cmd.Flags().GetString("source") - - params := hypemansdk.InstanceLogsParams{ - Follow: param.NewOpt(follow), - } - if tail > 0 { - params.Tail = param.NewOpt(tail) - } - if source != "" { - params.Source = hypemansdk.InstanceLogsParamsSource(source) - } - - stream := client.Instances.LogsStreaming(cmd.Context(), args[0], params) - for stream.Next() { - fmt.Println(stream.Current()) - } - if stream.Err() != nil { - return fmt.Errorf("log stream error: %w", stream.Err()) - } - return nil -} - -func runInstanceStat(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - path, _ := cmd.Flags().GetString("path") - - params := hypemansdk.InstanceStatParams{ - Path: path, - } - if cmd.Flags().Changed("follow-links") { - v, _ := cmd.Flags().GetBool("follow-links") - params.FollowLinks = param.NewOpt(v) - } - - info, err := client.Instances.Stat(cmd.Context(), args[0], params) - if err != nil { - return fmt.Errorf("failed to stat path: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(info) - } - - tableData := pterm.TableData{ - {"Property", "Value"}, - {"Exists", fmt.Sprintf("%t", info.Exists)}, - {"Is File", fmt.Sprintf("%t", info.IsFile)}, - {"Is Dir", fmt.Sprintf("%t", info.IsDir)}, - {"Is Symlink", fmt.Sprintf("%t", info.IsSymlink)}, - {"Size", fmt.Sprintf("%d", info.Size)}, - {"Mode", fmt.Sprintf("%o", info.Mode)}, - } - if info.LinkTarget != "" { - tableData = append(tableData, []string{"Link Target", info.LinkTarget}) - } - if info.Error != "" { - tableData = append(tableData, []string{"Error", info.Error}) - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runInstanceVolumeAttach(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - instanceID, _ := cmd.Flags().GetString("instance-id") - mountPath, _ := cmd.Flags().GetString("mount-path") - - params := hypemansdk.InstanceVolumeAttachParams{ - ID: instanceID, - MountPath: mountPath, - } - if cmd.Flags().Changed("readonly") { - v, _ := cmd.Flags().GetBool("readonly") - params.Readonly = param.NewOpt(v) - } - - instance, err := client.Instances.Volumes.Attach(cmd.Context(), args[0], params) - if err != nil { - return fmt.Errorf("failed to attach volume: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(instance) - } - - pterm.Success.Printf("Attached volume %s to instance %s at %s\n", args[0], instanceID, mountPath) - return nil -} - -func runInstanceVolumeDetach(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - instanceID, _ := cmd.Flags().GetString("instance-id") - - params := hypemansdk.InstanceVolumeDetachParams{ - ID: instanceID, - } - - instance, err := client.Instances.Volumes.Detach(cmd.Context(), args[0], params) - if err != nil { - return fmt.Errorf("failed to detach volume: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(instance) - } - - pterm.Success.Printf("Detached volume %s from instance %s\n", args[0], instanceID) - return nil -} - -func printInstanceDetail(inst *hypemansdk.Instance) { - tableData := pterm.TableData{ - {"Property", "Value"}, - {"ID", inst.ID}, - {"Name", inst.Name}, - {"Image", inst.Image}, - {"State", string(inst.State)}, - {"vCPUs", fmt.Sprintf("%d", inst.Vcpus)}, - {"Size", util.OrDash(inst.Size)}, - {"Overlay Size", util.OrDash(inst.OverlaySize)}, - {"Hotplug Size", util.OrDash(inst.HotplugSize)}, - {"Disk I/O BPS", util.OrDash(inst.DiskIoBps)}, - {"Hypervisor", string(inst.Hypervisor)}, - {"Has Snapshot", fmt.Sprintf("%t", inst.HasSnapshot)}, - {"Created At", util.FormatLocal(inst.CreatedAt)}, - } - if inst.Network.Enabled { - tableData = append(tableData, []string{"Network IP", util.OrDash(inst.Network.IP)}) - } - if len(inst.Volumes) > 0 { - var volStrs []string - for _, v := range inst.Volumes { - volStrs = append(volStrs, fmt.Sprintf("%s@%s", v.VolumeID, v.MountPath)) - } - tableData = append(tableData, []string{"Volumes", strings.Join(volStrs, ", ")}) - } - if len(inst.Env) > 0 { - envBytes, _ := json.Marshal(inst.Env) - tableData = append(tableData, []string{"Env", string(envBytes)}) - } - table.PrintTableNoPad(tableData, true) -} diff --git a/cmd/hypeman/resource.go b/cmd/hypeman/resource.go deleted file mode 100644 index d3e5e78..0000000 --- a/cmd/hypeman/resource.go +++ /dev/null @@ -1,102 +0,0 @@ -package hypeman - -import ( - "fmt" - - "github.com/kernel/cli/pkg/table" - "github.com/kernel/cli/pkg/util" - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -var resourceCmd = &cobra.Command{ - Use: "resource", - Aliases: []string{"resources"}, - Short: "View Hypeman host resources", -} - -var resourceGetCmd = &cobra.Command{ - Use: "get", - Short: "Get current host resource capacity and allocation status", - RunE: runResourceGet, -} - -func init() { - resourceCmd.AddCommand(resourceGetCmd) - - resourceGetCmd.Flags().StringP("output", "o", "", "Output format: json") -} - -func runResourceGet(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - resources, err := client.Resources.Get(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to get resources: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(resources) - } - - // Resource summary table - tableData := pterm.TableData{ - {"Resource", "Capacity", "Effective Limit", "Allocated", "Available", "Oversub Ratio"}, - { - resources.CPU.Type, - fmt.Sprintf("%d", resources.CPU.Capacity), - fmt.Sprintf("%d", resources.CPU.EffectiveLimit), - fmt.Sprintf("%d", resources.CPU.Allocated), - fmt.Sprintf("%d", resources.CPU.Available), - fmt.Sprintf("%.2f", resources.CPU.OversubRatio), - }, - { - resources.Memory.Type, - fmt.Sprintf("%d", resources.Memory.Capacity), - fmt.Sprintf("%d", resources.Memory.EffectiveLimit), - fmt.Sprintf("%d", resources.Memory.Allocated), - fmt.Sprintf("%d", resources.Memory.Available), - fmt.Sprintf("%.2f", resources.Memory.OversubRatio), - }, - { - resources.Disk.Type, - fmt.Sprintf("%d", resources.Disk.Capacity), - fmt.Sprintf("%d", resources.Disk.EffectiveLimit), - fmt.Sprintf("%d", resources.Disk.Allocated), - fmt.Sprintf("%d", resources.Disk.Available), - fmt.Sprintf("%.2f", resources.Disk.OversubRatio), - }, - { - resources.Network.Type, - fmt.Sprintf("%d", resources.Network.Capacity), - fmt.Sprintf("%d", resources.Network.EffectiveLimit), - fmt.Sprintf("%d", resources.Network.Allocated), - fmt.Sprintf("%d", resources.Network.Available), - fmt.Sprintf("%.2f", resources.Network.OversubRatio), - }, - } - table.PrintTableNoPad(tableData, true) - - // Allocations - if len(resources.Allocations) > 0 { - pterm.Println() - pterm.Info.Println("Allocations:") - allocData := pterm.TableData{{"Instance", "Name", "CPU", "Memory (bytes)", "Disk (bytes)"}} - for _, a := range resources.Allocations { - allocData = append(allocData, []string{ - a.InstanceID, - a.InstanceName, - fmt.Sprintf("%d", a.CPU), - fmt.Sprintf("%d", a.MemoryBytes), - fmt.Sprintf("%d", a.DiskBytes), - }) - } - table.PrintTableNoPad(allocData, true) - } - - return nil -} diff --git a/cmd/hypeman/volume.go b/cmd/hypeman/volume.go deleted file mode 100644 index b06aac9..0000000 --- a/cmd/hypeman/volume.go +++ /dev/null @@ -1,243 +0,0 @@ -package hypeman - -import ( - "fmt" - "os" - "strings" - - "github.com/kernel/cli/pkg/table" - "github.com/kernel/cli/pkg/util" - hypemansdk "github.com/kernel/hypeman-go" - "github.com/kernel/hypeman-go/packages/param" - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -var volumeCmd = &cobra.Command{ - Use: "volume", - Aliases: []string{"volumes"}, - Short: "Manage Hypeman volumes", -} - -var volumeCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create a new empty volume", - RunE: runVolumeCreate, -} - -var volumeListCmd = &cobra.Command{ - Use: "list", - Short: "List volumes", - RunE: runVolumeList, -} - -var volumeGetCmd = &cobra.Command{ - Use: "get ", - Short: "Get volume details", - Args: cobra.ExactArgs(1), - RunE: runVolumeGet, -} - -var volumeDeleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Delete a volume", - Args: cobra.ExactArgs(1), - RunE: runVolumeDelete, -} - -var volumeCreateFromArchiveCmd = &cobra.Command{ - Use: "create-from-archive ", - Short: "Create a volume from a tar.gz archive", - Args: cobra.ExactArgs(1), - RunE: runVolumeCreateFromArchive, -} - -func init() { - volumeCmd.AddCommand(volumeCreateCmd) - volumeCmd.AddCommand(volumeListCmd) - volumeCmd.AddCommand(volumeGetCmd) - volumeCmd.AddCommand(volumeDeleteCmd) - volumeCmd.AddCommand(volumeCreateFromArchiveCmd) - - // volume create flags - volumeCreateCmd.Flags().String("name", "", "Volume name (required)") - _ = volumeCreateCmd.MarkFlagRequired("name") - volumeCreateCmd.Flags().Int64("size-gb", 0, "Size in gigabytes (required)") - _ = volumeCreateCmd.MarkFlagRequired("size-gb") - volumeCreateCmd.Flags().String("id", "", "Optional custom identifier") - volumeCreateCmd.Flags().StringP("output", "o", "", "Output format: json") - - // volume list flags - volumeListCmd.Flags().StringP("output", "o", "", "Output format: json") - - // volume get flags - volumeGetCmd.Flags().StringP("output", "o", "", "Output format: json") - - // volume create-from-archive flags - volumeCreateFromArchiveCmd.Flags().String("name", "", "Volume name (required)") - _ = volumeCreateFromArchiveCmd.MarkFlagRequired("name") - volumeCreateFromArchiveCmd.Flags().Int64("size-gb", 0, "Maximum size in GB (required)") - _ = volumeCreateFromArchiveCmd.MarkFlagRequired("size-gb") - volumeCreateFromArchiveCmd.Flags().String("id", "", "Optional custom volume ID") - volumeCreateFromArchiveCmd.Flags().StringP("output", "o", "", "Output format: json") -} - -func runVolumeCreate(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - name, _ := cmd.Flags().GetString("name") - sizeGB, _ := cmd.Flags().GetInt64("size-gb") - - params := hypemansdk.VolumeNewParams{ - Name: name, - SizeGB: sizeGB, - } - if v, _ := cmd.Flags().GetString("id"); v != "" { - params.ID = param.NewOpt(v) - } - - volume, err := client.Volumes.New(cmd.Context(), params) - if err != nil { - return fmt.Errorf("failed to create volume: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(volume) - } - - pterm.Success.Printf("Created volume %s (%s, %dGB)\n", volume.Name, volume.ID, volume.SizeGB) - return nil -} - -func runVolumeList(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - volumes, err := client.Volumes.List(cmd.Context()) - if err != nil { - return fmt.Errorf("failed to list volumes: %w", err) - } - - if output == "json" { - if volumes == nil || len(*volumes) == 0 { - fmt.Println("[]") - return nil - } - return util.PrintPrettyJSONSlice(*volumes) - } - - if volumes == nil || len(*volumes) == 0 { - pterm.Info.Println("No volumes found") - return nil - } - - tableData := pterm.TableData{{"ID", "Name", "Size (GB)", "Attachments", "Created At"}} - for _, vol := range *volumes { - attachStr := "-" - if len(vol.Attachments) > 0 { - var parts []string - for _, a := range vol.Attachments { - parts = append(parts, fmt.Sprintf("%s@%s", a.InstanceID, a.MountPath)) - } - attachStr = strings.Join(parts, ", ") - } - tableData = append(tableData, []string{ - vol.ID, - vol.Name, - fmt.Sprintf("%d", vol.SizeGB), - attachStr, - util.FormatLocal(vol.CreatedAt), - }) - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runVolumeGet(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - - volume, err := client.Volumes.Get(cmd.Context(), args[0]) - if err != nil { - return fmt.Errorf("failed to get volume: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(volume) - } - - tableData := pterm.TableData{ - {"Property", "Value"}, - {"ID", volume.ID}, - {"Name", volume.Name}, - {"Size (GB)", fmt.Sprintf("%d", volume.SizeGB)}, - {"Created At", util.FormatLocal(volume.CreatedAt)}, - } - if len(volume.Attachments) > 0 { - for _, a := range volume.Attachments { - tableData = append(tableData, []string{"Attachment", fmt.Sprintf("%s@%s (readonly: %t)", a.InstanceID, a.MountPath, a.Readonly)}) - } - } - table.PrintTableNoPad(tableData, true) - return nil -} - -func runVolumeDelete(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - - if err := client.Volumes.Delete(cmd.Context(), args[0]); err != nil { - return fmt.Errorf("failed to delete volume: %w", err) - } - - pterm.Success.Printf("Deleted volume %s\n", args[0]) - return nil -} - -func runVolumeCreateFromArchive(cmd *cobra.Command, args []string) error { - client, err := mustGetClient(cmd) - if err != nil { - return err - } - output, _ := cmd.Flags().GetString("output") - name, _ := cmd.Flags().GetString("name") - sizeGB, _ := cmd.Flags().GetInt64("size-gb") - - archivePath := args[0] - file, err := os.Open(archivePath) - if err != nil { - return fmt.Errorf("failed to open archive: %w", err) - } - defer file.Close() - - params := hypemansdk.VolumeNewFromArchiveParams{ - Name: name, - SizeGB: sizeGB, - } - if v, _ := cmd.Flags().GetString("id"); v != "" { - params.ID = param.NewOpt(v) - } - - volume, err := client.Volumes.NewFromArchive(cmd.Context(), file, params) - if err != nil { - return fmt.Errorf("failed to create volume from archive: %w", err) - } - - if output == "json" { - return util.PrintPrettyJSON(volume) - } - - pterm.Success.Printf("Created volume %s from archive (%s, %dGB)\n", volume.Name, volume.ID, volume.SizeGB) - return nil -} diff --git a/cmd/root.go b/cmd/root.go index 4a03223..5542dcb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,7 +11,6 @@ import ( "github.com/charmbracelet/fang" "github.com/charmbracelet/lipgloss/v2" - "github.com/kernel/cli/cmd/hypeman" "github.com/kernel/cli/cmd/mcp" "github.com/kernel/cli/cmd/proxies" "github.com/kernel/cli/pkg/auth" @@ -91,7 +90,7 @@ func isAuthExempt(cmd *cobra.Command) bool { // Check if the top-level command is in the exempt list switch topLevel.Name() { - case "login", "logout", "help", "completion", "create", "mcp", "upgrade", "hypeman": + case "login", "logout", "help", "completion", "create", "mcp", "upgrade": return true case "auth": // Only exempt the auth command itself (status display), not its subcommands @@ -147,7 +146,6 @@ func init() { rootCmd.AddCommand(credentialProvidersCmd) rootCmd.AddCommand(createCmd) rootCmd.AddCommand(mcp.MCPCmd) - rootCmd.AddCommand(hypeman.HypemanCmd) rootCmd.AddCommand(upgradeCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { diff --git a/go.mod b/go.mod index 50c4c59..48654bc 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1 github.com/kernel/kernel-go-sdk v0.34.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index a2821c5..6916255 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1 h1:/Xquk6Ryqnhf6X2UTUbdGeSonqz4L1xfygvfdqaw34c= -github.com/kernel/hypeman-go v0.9.7-0.20260211202932-a897d4c032e1/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= github.com/kernel/kernel-go-sdk v0.34.0 h1:zyuJPzjZJnpcFVlh8R8F5RPPjcKLU5yorasRTb4NPxU= github.com/kernel/kernel-go-sdk v0.34.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From 1730338cf486259ff11372ed26550fc27198596d Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:22:00 +0000 Subject: [PATCH 4/4] Update Go SDK to v0.35.0 Bump github.com/kernel/kernel-go-sdk from v0.34.0 to v0.35.0. Triggered by: kernel/kernel-go-sdk@f8178335e91ed37927459b03d712ca1f32f6391a Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 48654bc..1e7285d 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.34.0 + github.com/kernel/kernel-go-sdk v0.35.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 github.com/samber/lo v1.51.0 diff --git a/go.sum b/go.sum index 6916255..b50a470 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.34.0 h1:zyuJPzjZJnpcFVlh8R8F5RPPjcKLU5yorasRTb4NPxU= -github.com/kernel/kernel-go-sdk v0.34.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.35.0 h1:zQcDPxq7N1njnNVoFmxvi3XMKoqemOVlnkVYuYPqAE0= +github.com/kernel/kernel-go-sdk v0.35.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=