Skip to content

Commit ccc1732

Browse files
committed
feat(cli): select latest local release
Support set latest by choosing the highest stable semantic version already installed locally. Ignore nightly and prerelease directories, and keep selection free of network or installation side effects. Document the separate current and set commands. Refs: #12
1 parent 12b228f commit ccc1732

4 files changed

Lines changed: 111 additions & 17 deletions

File tree

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,20 +126,31 @@ nvimim upgrade -y
126126

127127
---
128128

129-
### `nvimim current` — Show or switch the active version
129+
### `nvimim current` — Show the active version
130130

131-
Show which version is currently active:
131+
Displays the version currently selected as active. This command is
132+
read-only:
132133

133134
```bash
134135
nvimim current
135136

136137
* 0.11.5
137138
```
138139

139-
Switch to a different installed version:
140+
### `nvimim set <version>` — Select an installed version
141+
142+
Selects a version that is already installed locally. It never downloads or
143+
installs a release:
144+
145+
```bash
146+
nvimim set 0.11.3
147+
```
148+
149+
Use `latest` to select the highest stable version installed locally. Nightly
150+
and prerelease directories are ignored:
140151

141152
```bash
142-
nvimim current 0.11.3
153+
nvimim set latest
143154
```
144155

145156
---

internal/cli/command.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,19 @@ func (cmd *SetCommand) Execute(args []string) error {
6363
return fmt.Errorf("nvim path does not exist: %s", cmd.appOpts.Path)
6464
}
6565

66-
release := args[0]
67-
if release == "latest" {
68-
return fmt.Errorf("latest release selection is not available yet")
66+
selectedRelease := args[0]
67+
var err error
68+
if selectedRelease == "latest" {
69+
selectedRelease, err = release.LatestInstalledStable(cmd.appOpts.Path)
70+
if err != nil {
71+
return err
72+
}
6973
}
70-
if !pathx.Exists(filepath.Join(cmd.appOpts.Path, release)) {
71-
return fmt.Errorf("the release %s is not installed", release)
74+
if !pathx.Exists(filepath.Join(cmd.appOpts.Path, selectedRelease)) {
75+
return fmt.Errorf("the release %s is not installed", selectedRelease)
7276
}
7377

74-
return setCurrentRelease(cmd.appOpts.Path, release)
78+
return setCurrentRelease(cmd.appOpts.Path, selectedRelease)
7579
}
7680

7781
func (cmd *SetCommand) SetAppOptions(opts *config.AppOptions) {

internal/cli/current_test.go

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,71 @@ import (
99
"github.com/stretchr/testify/require"
1010
)
1111

12+
const (
13+
explicitVersion = "0.12.0"
14+
olderStableVersion = "0.11.5"
15+
prereleaseVersion = "0.13.0-rc1"
16+
)
17+
1218
func TestSetCommandSelectsInstalledRelease(t *testing.T) {
1319
installPath := t.TempDir()
14-
releasePath := filepath.Join(installPath, "0.12.0")
15-
otherReleasePath := filepath.Join(installPath, "0.11.5")
20+
releasePath := filepath.Join(installPath, explicitVersion)
21+
otherReleasePath := filepath.Join(installPath, olderStableVersion)
1622
require.NoError(t, os.Mkdir(releasePath, 0o755))
1723
require.NoError(t, os.Mkdir(otherReleasePath, 0o755))
1824

1925
cmd := &SetCommand{}
2026
cmd.SetAppOptions(&config.AppOptions{Path: installPath})
2127

22-
require.NoError(t, cmd.Execute([]string{"0.12.0"}))
28+
require.NoError(t, cmd.Execute([]string{explicitVersion}))
2329

2430
current, err := os.Readlink(filepath.Join(installPath, "current"))
2531
require.NoError(t, err)
2632
require.Equal(t, releasePath, current)
2733
}
2834

35+
func TestSetCommandLatestSelectsHighestStableRelease(t *testing.T) {
36+
installPath := t.TempDir()
37+
for _, name := range []string{
38+
olderStableVersion,
39+
explicitVersion,
40+
"nightly",
41+
prereleaseVersion,
42+
} {
43+
require.NoError(t, os.Mkdir(filepath.Join(installPath, name), 0o755))
44+
}
45+
46+
cmd := &SetCommand{}
47+
cmd.SetAppOptions(&config.AppOptions{Path: installPath})
48+
49+
require.NoError(t, cmd.Execute([]string{"latest"}))
50+
51+
current, err := os.Readlink(filepath.Join(installPath, "current"))
52+
require.NoError(t, err)
53+
require.Equal(t, filepath.Join(installPath, explicitVersion), current)
54+
}
55+
2956
func TestSetCommandOnlySelectsInstalledReleases(t *testing.T) {
3057
installPath := t.TempDir()
3158
cmd := &SetCommand{}
3259
cmd.SetAppOptions(&config.AppOptions{Path: installPath})
3360

34-
err := cmd.Execute([]string{"0.12.0"})
35-
require.EqualError(t, err, "the release 0.12.0 is not installed")
61+
err := cmd.Execute([]string{explicitVersion})
62+
require.EqualError(t, err,
63+
"the release "+explicitVersion+" is not installed")
3664
}
3765

3866
func TestCurrentCommandIsReadOnly(t *testing.T) {
3967
installPath := t.TempDir()
40-
releasePath := filepath.Join(installPath, "0.12.0")
68+
releasePath := filepath.Join(installPath, explicitVersion)
4169
require.NoError(t, os.Mkdir(releasePath, 0o755))
4270
require.NoError(t, os.Symlink(releasePath,
4371
filepath.Join(installPath, "current")))
4472

4573
cmd := &CurrentCommand{}
4674
cmd.SetAppOptions(&config.AppOptions{Path: installPath})
4775

48-
err := cmd.Execute([]string{"0.12.0"})
76+
err := cmd.Execute([]string{explicitVersion})
4977
require.EqualError(t, err,
5078
"current does not accept arguments; use 'set <release>'")
5179

internal/release/info.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package release
33
import (
44
"encoding/json"
55
"fmt"
6+
"os"
67
"path/filepath"
78
"regexp"
89
"strconv"
@@ -45,6 +46,56 @@ func (rs *Releases) Installed(path string) []Info {
4546
return installed
4647
}
4748

49+
// LatestInstalledStable returns the highest stable semantic version found in
50+
// the installation path. Nightly, prerelease, and non-version entries are
51+
// ignored.
52+
func LatestInstalledStable(path string) (string, error) {
53+
entries, err := os.ReadDir(path)
54+
if err != nil {
55+
return "", fmt.Errorf("failed to read install path: %w", err)
56+
}
57+
58+
latest := ""
59+
for _, entry := range entries {
60+
if !entry.IsDir() || !isStableVersion(entry.Name()) {
61+
continue
62+
}
63+
if latest == "" || versionLess(latest, entry.Name()) {
64+
latest = entry.Name()
65+
}
66+
}
67+
if latest == "" {
68+
return "", fmt.Errorf("no stable releases installed")
69+
}
70+
return latest, nil
71+
}
72+
73+
func isStableVersion(version string) bool {
74+
parts := strings.Split(version, ".")
75+
if len(parts) != 3 {
76+
return false
77+
}
78+
for _, part := range parts {
79+
if _, err := strconv.Atoi(part); err != nil {
80+
return false
81+
}
82+
}
83+
return true
84+
}
85+
86+
func versionLess(left, right string) bool {
87+
leftParts := strings.Split(left, ".")
88+
rightParts := strings.Split(right, ".")
89+
for i := range leftParts {
90+
leftPart, _ := strconv.Atoi(leftParts[i])
91+
rightPart, _ := strconv.Atoi(rightParts[i])
92+
if leftPart != rightPart {
93+
return leftPart < rightPart
94+
}
95+
}
96+
return false
97+
}
98+
4899
// Available returns a list of releases that are not present in the installed
49100
// releases.
50101
func (rs *Releases) Available(installed []Info) []Info {

0 commit comments

Comments
 (0)