Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions cmd/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cmd

import (
"fmt"

"github.com/hdresearch/vers-cli/internal/utils"
"github.com/spf13/cobra"
)

var aliasCmd = &cobra.Command{
Use: "alias [name]",
Short: "Show VM ID for an alias, or list all aliases",
Long: `Look up the VM ID for a given alias, or list all aliases if no argument is provided.

Examples:
vers alias myvm # Show VM ID for alias 'myvm'
vers alias # List all aliases`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return listAliases()
}
return showAlias(args[0])
},
}

func listAliases() error {
aliases, err := utils.LoadAliases()
if err != nil {
return fmt.Errorf("failed to load aliases: %w", err)
}

if len(aliases) == 0 {
fmt.Println("No aliases defined.")
return nil
}

for alias, vmID := range aliases {
fmt.Printf("%s -> %s\n", alias, vmID)
}
return nil
}

func showAlias(name string) error {
aliases, err := utils.LoadAliases()
if err != nil {
return fmt.Errorf("failed to load aliases: %w", err)
}

vmID, ok := aliases[name]
if !ok {
return fmt.Errorf("alias '%s' not found", name)
}

fmt.Println(vmID)
return nil
}

func init() {
rootCmd.AddCommand(aliasCmd)
}
7 changes: 6 additions & 1 deletion internal/handlers/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@ func HandleBranch(ctx context.Context, a *app.App, r BranchReq) (presenters.Bran

// New VM ID now available from Branch response in SDK alpha.24
res.NewID = resp.VmID
res.NewAlias = "" // Alias not available in new SDK
res.NewState = "unknown" // State not available in new SDK

// Save alias locally if provided
if r.Alias != "" {
_ = utils.SetAlias(r.Alias, resp.VmID)
res.NewAlias = r.Alias
}

if r.Checkout {
if err := utils.SetHead(resp.VmID); err != nil {
res.CheckoutErr = err
Expand Down
8 changes: 7 additions & 1 deletion internal/handlers/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/hdresearch/vers-cli/internal/app"
"github.com/hdresearch/vers-cli/internal/presenters"
"github.com/hdresearch/vers-cli/internal/utils"
vers "github.com/hdresearch/vers-sdk-go"
)

Expand Down Expand Up @@ -52,7 +53,12 @@ func HandleRun(ctx context.Context, a *app.App, r RunReq) (presenters.RunView, e
// SDK alpha.24 now returns the VM ID
vmID := resp.VmID

return presenters.RunView{RootVmID: vmID, VmAlias: "", HeadTarget: vmID}, nil
// Save alias locally if provided
if r.VMAlias != "" {
_ = utils.SetAlias(r.VMAlias, vmID)
}

return presenters.RunView{RootVmID: vmID, VmAlias: r.VMAlias, HeadTarget: vmID}, nil
}

func validateAndNormalize(r *RunReq) error {
Expand Down
6 changes: 6 additions & 0 deletions internal/handlers/run_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/hdresearch/vers-cli/internal/app"
"github.com/hdresearch/vers-cli/internal/presenters"
"github.com/hdresearch/vers-cli/internal/utils"
vers "github.com/hdresearch/vers-sdk-go"
)

Expand Down Expand Up @@ -32,5 +33,10 @@ func HandleRunCommit(ctx context.Context, a *app.App, r RunCommitReq) (presenter
// SDK alpha.24 now returns the VM ID
vmID := resp.VmID

// Save alias locally if provided
if r.VMAlias != "" {
_ = utils.SetAlias(r.VMAlias, vmID)
}

return presenters.RunCommitView{RootVmID: vmID, HeadTarget: vmID, CommitKey: r.CommitKey}, nil
}
23 changes: 3 additions & 20 deletions internal/presenters/status_presenter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,14 @@ func capOut(t *testing.T, fn func()) string {

func TestRenderVMStatus_PrintsDetails(t *testing.T) {
s := styles.NewStatusStyles()
// Use the new vers.Vm type instead of APIVmGetResponseData
vm := &vers.Vm{
VmID: "vm1",
Parent: "root",
VmID: "vm1",
}
out := capOut(t, func() { presenters.RenderVMStatus(&s, vm) })
if !strings.Contains(out, "Getting status for VM: vm1") {
t.Fatalf("missing VM header: %s", out)
}
if !strings.Contains(out, "Parent: root") {
t.Fatalf("missing parent field: %s", out)
}
}

func TestRenderVMStatus_HidesParentWhenEmpty(t *testing.T) {
s := styles.NewStatusStyles()
vm := &vers.Vm{
VmID: "vm1",
Parent: "",
}
out := capOut(t, func() { presenters.RenderVMStatus(&s, vm) })
if !strings.Contains(out, "Getting status for VM: vm1") {
t.Fatalf("missing VM header: %s", out)
}
if strings.Contains(out, "Parent:") {
t.Fatalf("should not show parent field when empty: %s", out)
if !strings.Contains(out, "VM: vm1") {
t.Fatalf("missing VM details: %s", out)
}
}
125 changes: 125 additions & 0 deletions internal/utils/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package utils

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)

// GetAliasesPath returns the path to the aliases file (~/.vers/aliases.json)
func GetAliasesPath() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}

configDir := filepath.Join(homeDir, ".vers")
if err := os.MkdirAll(configDir, 0755); err != nil {
return "", fmt.Errorf("failed to create config directory: %w", err)
}

return filepath.Join(configDir, "aliases.json"), nil
}

// LoadAliases loads the alias map from ~/.vers/aliases.json
// Returns an empty map if the file doesn't exist
func LoadAliases() (map[string]string, error) {
aliasPath, err := GetAliasesPath()
if err != nil {
return nil, err
}

if _, err := os.Stat(aliasPath); os.IsNotExist(err) {
return make(map[string]string), nil
}

data, err := os.ReadFile(aliasPath)
if err != nil {
return nil, fmt.Errorf("failed to read aliases file: %w", err)
}

var aliases map[string]string
if err := json.Unmarshal(data, &aliases); err != nil {
return nil, fmt.Errorf("failed to parse aliases file: %w", err)
}

if aliases == nil {
aliases = make(map[string]string)
}

return aliases, nil
}

// SaveAliases saves the alias map to ~/.vers/aliases.json
func SaveAliases(aliases map[string]string) error {
aliasPath, err := GetAliasesPath()
if err != nil {
return err
}

data, err := json.MarshalIndent(aliases, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal aliases: %w", err)
}

if err := os.WriteFile(aliasPath, data, 0644); err != nil {
return fmt.Errorf("failed to write aliases file: %w", err)
}

return nil
}

// SetAlias adds or updates an alias mapping
func SetAlias(alias, vmID string) error {
aliases, err := LoadAliases()
if err != nil {
return err
}

aliases[alias] = vmID
return SaveAliases(aliases)
}

// RemoveAlias removes an alias mapping
func RemoveAlias(alias string) error {
aliases, err := LoadAliases()
if err != nil {
return err
}

delete(aliases, alias)
return SaveAliases(aliases)
}

// ResolveAlias checks if the identifier is an alias and returns the VM ID
// If not an alias, returns the identifier unchanged
func ResolveAlias(identifier string) string {
aliases, err := LoadAliases()
if err != nil {
return identifier
}

if vmID, ok := aliases[identifier]; ok {
return vmID
}

return identifier
}

// GetAliasByVMID performs a reverse lookup to find an alias for a VM ID
// Returns empty string if no alias exists
func GetAliasByVMID(vmID string) string {
aliases, err := LoadAliases()
if err != nil {
return ""
}

for alias, id := range aliases {
if id == vmID {
return alias
}
}

return ""
}
11 changes: 7 additions & 4 deletions internal/utils/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ type VMInfo struct {
DisplayName string
}

// ResolveVMIdentifier takes a VM ID and returns the VM info
// Note: Alias lookups are no longer supported in the new SDK
func ResolveVMIdentifier(ctx context.Context, client *vers.Client, vmID string) (*VMInfo, error) {
// ResolveVMIdentifier takes a VM ID or alias and returns the VM info
// Aliases are resolved locally from ~/.vers/aliases.json
func ResolveVMIdentifier(ctx context.Context, client *vers.Client, identifier string) (*VMInfo, error) {
// Resolve alias to VM ID if applicable
vmID := ResolveAlias(identifier)

vms, err := client.Vm.List(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list VMs: %w", err)
Expand All @@ -30,7 +33,7 @@ func ResolveVMIdentifier(ctx context.Context, client *vers.Client, vmID string)
}
}

return nil, fmt.Errorf("VM '%s' not found", vmID)
return nil, fmt.Errorf("VM '%s' not found", identifier)
}

// CreateVMInfoFromVM creates VMInfo from a Vm struct
Expand Down