Skip to content

Commit 83bdff8

Browse files
Merge pull request #142 from hdresearch/ty/local-alias
local vm aliases
2 parents e43b1aa + 1e13d6c commit 83bdff8

File tree

7 files changed

+215
-26
lines changed

7 files changed

+215
-26
lines changed

cmd/alias.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hdresearch/vers-cli/internal/utils"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var aliasCmd = &cobra.Command{
11+
Use: "alias [name]",
12+
Short: "Show VM ID for an alias, or list all aliases",
13+
Long: `Look up the VM ID for a given alias, or list all aliases if no argument is provided.
14+
15+
Examples:
16+
vers alias myvm # Show VM ID for alias 'myvm'
17+
vers alias # List all aliases`,
18+
Args: cobra.MaximumNArgs(1),
19+
RunE: func(cmd *cobra.Command, args []string) error {
20+
if len(args) == 0 {
21+
return listAliases()
22+
}
23+
return showAlias(args[0])
24+
},
25+
}
26+
27+
func listAliases() error {
28+
aliases, err := utils.LoadAliases()
29+
if err != nil {
30+
return fmt.Errorf("failed to load aliases: %w", err)
31+
}
32+
33+
if len(aliases) == 0 {
34+
fmt.Println("No aliases defined.")
35+
return nil
36+
}
37+
38+
for alias, vmID := range aliases {
39+
fmt.Printf("%s -> %s\n", alias, vmID)
40+
}
41+
return nil
42+
}
43+
44+
func showAlias(name string) error {
45+
aliases, err := utils.LoadAliases()
46+
if err != nil {
47+
return fmt.Errorf("failed to load aliases: %w", err)
48+
}
49+
50+
vmID, ok := aliases[name]
51+
if !ok {
52+
return fmt.Errorf("alias '%s' not found", name)
53+
}
54+
55+
fmt.Println(vmID)
56+
return nil
57+
}
58+
59+
func init() {
60+
rootCmd.AddCommand(aliasCmd)
61+
}

internal/handlers/branch.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ func HandleBranch(ctx context.Context, a *app.App, r BranchReq) (presenters.Bran
4949

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

54+
// Save alias locally if provided
55+
if r.Alias != "" {
56+
_ = utils.SetAlias(r.Alias, resp.VmID)
57+
res.NewAlias = r.Alias
58+
}
59+
5560
if r.Checkout {
5661
if err := utils.SetHead(resp.VmID); err != nil {
5762
res.CheckoutErr = err

internal/handlers/run.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/hdresearch/vers-cli/internal/app"
77
"github.com/hdresearch/vers-cli/internal/presenters"
8+
"github.com/hdresearch/vers-cli/internal/utils"
89
vers "github.com/hdresearch/vers-sdk-go"
910
)
1011

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

55-
return presenters.RunView{RootVmID: vmID, VmAlias: "", HeadTarget: vmID}, nil
56+
// Save alias locally if provided
57+
if r.VMAlias != "" {
58+
_ = utils.SetAlias(r.VMAlias, vmID)
59+
}
60+
61+
return presenters.RunView{RootVmID: vmID, VmAlias: r.VMAlias, HeadTarget: vmID}, nil
5662
}
5763

5864
func validateAndNormalize(r *RunReq) error {

internal/handlers/run_commit.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/hdresearch/vers-cli/internal/app"
77
"github.com/hdresearch/vers-cli/internal/presenters"
8+
"github.com/hdresearch/vers-cli/internal/utils"
89
vers "github.com/hdresearch/vers-sdk-go"
910
)
1011

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

36+
// Save alias locally if provided
37+
if r.VMAlias != "" {
38+
_ = utils.SetAlias(r.VMAlias, vmID)
39+
}
40+
3541
return presenters.RunCommitView{RootVmID: vmID, HeadTarget: vmID, CommitKey: r.CommitKey}, nil
3642
}

internal/presenters/status_presenter_test.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,14 @@ func capOut(t *testing.T, fn func()) string {
2828

2929
func TestRenderVMStatus_PrintsDetails(t *testing.T) {
3030
s := styles.NewStatusStyles()
31-
// Use the new vers.Vm type instead of APIVmGetResponseData
3231
vm := &vers.Vm{
33-
VmID: "vm1",
34-
Parent: "root",
32+
VmID: "vm1",
3533
}
3634
out := capOut(t, func() { presenters.RenderVMStatus(&s, vm) })
3735
if !strings.Contains(out, "Getting status for VM: vm1") {
3836
t.Fatalf("missing VM header: %s", out)
3937
}
40-
if !strings.Contains(out, "Parent: root") {
41-
t.Fatalf("missing parent field: %s", out)
42-
}
43-
}
44-
45-
func TestRenderVMStatus_HidesParentWhenEmpty(t *testing.T) {
46-
s := styles.NewStatusStyles()
47-
vm := &vers.Vm{
48-
VmID: "vm1",
49-
Parent: "",
50-
}
51-
out := capOut(t, func() { presenters.RenderVMStatus(&s, vm) })
52-
if !strings.Contains(out, "Getting status for VM: vm1") {
53-
t.Fatalf("missing VM header: %s", out)
54-
}
55-
if strings.Contains(out, "Parent:") {
56-
t.Fatalf("should not show parent field when empty: %s", out)
38+
if !strings.Contains(out, "VM: vm1") {
39+
t.Fatalf("missing VM details: %s", out)
5740
}
5841
}

internal/utils/alias.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package utils
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// GetAliasesPath returns the path to the aliases file (~/.vers/aliases.json)
11+
func GetAliasesPath() (string, error) {
12+
homeDir, err := os.UserHomeDir()
13+
if err != nil {
14+
return "", fmt.Errorf("failed to get user home directory: %w", err)
15+
}
16+
17+
configDir := filepath.Join(homeDir, ".vers")
18+
if err := os.MkdirAll(configDir, 0755); err != nil {
19+
return "", fmt.Errorf("failed to create config directory: %w", err)
20+
}
21+
22+
return filepath.Join(configDir, "aliases.json"), nil
23+
}
24+
25+
// LoadAliases loads the alias map from ~/.vers/aliases.json
26+
// Returns an empty map if the file doesn't exist
27+
func LoadAliases() (map[string]string, error) {
28+
aliasPath, err := GetAliasesPath()
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
if _, err := os.Stat(aliasPath); os.IsNotExist(err) {
34+
return make(map[string]string), nil
35+
}
36+
37+
data, err := os.ReadFile(aliasPath)
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to read aliases file: %w", err)
40+
}
41+
42+
var aliases map[string]string
43+
if err := json.Unmarshal(data, &aliases); err != nil {
44+
return nil, fmt.Errorf("failed to parse aliases file: %w", err)
45+
}
46+
47+
if aliases == nil {
48+
aliases = make(map[string]string)
49+
}
50+
51+
return aliases, nil
52+
}
53+
54+
// SaveAliases saves the alias map to ~/.vers/aliases.json
55+
func SaveAliases(aliases map[string]string) error {
56+
aliasPath, err := GetAliasesPath()
57+
if err != nil {
58+
return err
59+
}
60+
61+
data, err := json.MarshalIndent(aliases, "", " ")
62+
if err != nil {
63+
return fmt.Errorf("failed to marshal aliases: %w", err)
64+
}
65+
66+
if err := os.WriteFile(aliasPath, data, 0644); err != nil {
67+
return fmt.Errorf("failed to write aliases file: %w", err)
68+
}
69+
70+
return nil
71+
}
72+
73+
// SetAlias adds or updates an alias mapping
74+
func SetAlias(alias, vmID string) error {
75+
aliases, err := LoadAliases()
76+
if err != nil {
77+
return err
78+
}
79+
80+
aliases[alias] = vmID
81+
return SaveAliases(aliases)
82+
}
83+
84+
// RemoveAlias removes an alias mapping
85+
func RemoveAlias(alias string) error {
86+
aliases, err := LoadAliases()
87+
if err != nil {
88+
return err
89+
}
90+
91+
delete(aliases, alias)
92+
return SaveAliases(aliases)
93+
}
94+
95+
// ResolveAlias checks if the identifier is an alias and returns the VM ID
96+
// If not an alias, returns the identifier unchanged
97+
func ResolveAlias(identifier string) string {
98+
aliases, err := LoadAliases()
99+
if err != nil {
100+
return identifier
101+
}
102+
103+
if vmID, ok := aliases[identifier]; ok {
104+
return vmID
105+
}
106+
107+
return identifier
108+
}
109+
110+
// GetAliasByVMID performs a reverse lookup to find an alias for a VM ID
111+
// Returns empty string if no alias exists
112+
func GetAliasByVMID(vmID string) string {
113+
aliases, err := LoadAliases()
114+
if err != nil {
115+
return ""
116+
}
117+
118+
for alias, id := range aliases {
119+
if id == vmID {
120+
return alias
121+
}
122+
}
123+
124+
return ""
125+
}

internal/utils/vm.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ type VMInfo struct {
1313
DisplayName string
1414
}
1515

16-
// ResolveVMIdentifier takes a VM ID and returns the VM info
17-
// Note: Alias lookups are no longer supported in the new SDK
18-
func ResolveVMIdentifier(ctx context.Context, client *vers.Client, vmID string) (*VMInfo, error) {
16+
// ResolveVMIdentifier takes a VM ID or alias and returns the VM info
17+
// Aliases are resolved locally from ~/.vers/aliases.json
18+
func ResolveVMIdentifier(ctx context.Context, client *vers.Client, identifier string) (*VMInfo, error) {
19+
// Resolve alias to VM ID if applicable
20+
vmID := ResolveAlias(identifier)
21+
1922
vms, err := client.Vm.List(ctx)
2023
if err != nil {
2124
return nil, fmt.Errorf("failed to list VMs: %w", err)
@@ -30,7 +33,7 @@ func ResolveVMIdentifier(ctx context.Context, client *vers.Client, vmID string)
3033
}
3134
}
3235

33-
return nil, fmt.Errorf("VM '%s' not found", vmID)
36+
return nil, fmt.Errorf("VM '%s' not found", identifier)
3437
}
3538

3639
// CreateVMInfoFromVM creates VMInfo from a Vm struct

0 commit comments

Comments
 (0)