Skip to content

Commit eb7c3ee

Browse files
authored
Merge branch 'main' into fix/windows-shim-exit-code
2 parents 19c2eb5 + 92c5247 commit eb7c3ee

3 files changed

Lines changed: 216 additions & 55 deletions

File tree

src/cmd/init.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,13 @@ Example:
3535

3636
spinner.Success("Directories created")
3737

38-
// Setup PATH
38+
// Setup PATH - AddToPath handles checking position and moving if needed
3939
shimsDir := path.ShimsDir()
4040

41-
if path.IsInPath(shimsDir) {
42-
ui.Success("PATH is already configured correctly")
43-
ui.Info("Shims directory: %s", ui.Highlight(shimsDir))
44-
} else {
45-
ui.Info("Setting up PATH...")
46-
if err := path.AddToPath(shimsDir); err != nil {
47-
ui.Error("Failed to configure PATH: %v", err)
48-
ui.Info("You can manually add %s to your PATH", shimsDir)
49-
return
50-
}
41+
if err := path.AddToPath(shimsDir); err != nil {
42+
ui.Error("Failed to configure PATH: %v", err)
43+
ui.Info("You can manually add %s to your PATH", shimsDir)
44+
return
5145
}
5246

5347
ui.Success("dtvem initialized successfully!")

src/cmd/list.go

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,96 @@ import (
77
)
88

99
var listCmd = &cobra.Command{
10-
Use: "list <runtime>",
11-
Short: "List installed versions of a runtime",
12-
Long: `List all installed versions of a specific runtime.
10+
Use: "list [runtime]",
11+
Short: "List installed versions",
12+
Long: `List all installed versions of a specific runtime, or all runtimes if none specified.
1313
1414
Examples:
15-
dtvem list python
16-
dtvem list node`,
17-
Args: cobra.ExactArgs(1),
15+
dtvem list # List all installed versions
16+
dtvem list python # List installed Python versions
17+
dtvem list node # List installed Node.js versions`,
18+
Args: cobra.MaximumNArgs(1),
1819
Run: func(cmd *cobra.Command, args []string) {
19-
runtimeName := args[0]
20-
21-
provider, err := runtime.Get(runtimeName)
22-
if err != nil {
23-
ui.Error("%v", err)
24-
ui.Info("Available runtimes: %v", runtime.List())
25-
return
20+
if len(args) == 0 {
21+
listAllRuntimes()
22+
} else {
23+
listSingleRuntime(args[0])
2624
}
25+
},
26+
}
27+
28+
// listAllRuntimes lists installed versions for all runtimes
29+
func listAllRuntimes() {
30+
providers := runtime.GetAll()
2731

28-
ui.Header("Installed %s versions:", provider.DisplayName())
32+
if len(providers) == 0 {
33+
ui.Info("No runtime providers registered")
34+
return
35+
}
2936

37+
ui.Header("Installed versions:")
38+
39+
hasAny := false
40+
for _, provider := range providers {
3041
versions, err := provider.ListInstalled()
3142
if err != nil {
32-
ui.Error("%v", err)
33-
return
43+
ui.Error(" %s: %v", provider.DisplayName(), err)
44+
continue
3445
}
3546

3647
if len(versions) == 0 {
37-
ui.Info("No versions installed")
38-
return
48+
continue
3949
}
4050

51+
hasAny = true
52+
globalVersion, _ := provider.GlobalVersion()
53+
54+
ui.Printf(" %s:\n", ui.Highlight(provider.DisplayName()))
4155
for _, v := range versions {
56+
if v.String() == globalVersion {
57+
ui.Printf(" %s (global)\n", ui.HighlightVersion(v.String()))
58+
} else {
59+
ui.Printf(" %s\n", ui.HighlightVersion(v.String()))
60+
}
61+
}
62+
}
63+
64+
if !hasAny {
65+
ui.Info("No versions installed")
66+
}
67+
}
68+
69+
// listSingleRuntime lists installed versions for a specific runtime
70+
func listSingleRuntime(runtimeName string) {
71+
provider, err := runtime.Get(runtimeName)
72+
if err != nil {
73+
ui.Error("%v", err)
74+
ui.Info("Available runtimes: %v", runtime.List())
75+
return
76+
}
77+
78+
ui.Header("Installed %s versions:", provider.DisplayName())
79+
80+
versions, err := provider.ListInstalled()
81+
if err != nil {
82+
ui.Error("%v", err)
83+
return
84+
}
85+
86+
if len(versions) == 0 {
87+
ui.Info("No versions installed")
88+
return
89+
}
90+
91+
globalVersion, _ := provider.GlobalVersion()
92+
93+
for _, v := range versions {
94+
if v.String() == globalVersion {
95+
ui.Printf(" %s (global)\n", ui.HighlightVersion(v.String()))
96+
} else {
4297
ui.Printf(" %s\n", ui.HighlightVersion(v.String()))
4398
}
44-
},
99+
}
45100
}
46101

47102
func init() {

src/internal/path/path_windows.go

Lines changed: 137 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/dtvem/dtvem/src/internal/constants"
1515
"github.com/dtvem/dtvem/src/internal/ui"
16+
"golang.org/x/sys/windows"
1617
"golang.org/x/sys/windows/registry"
1718
)
1819

@@ -27,69 +28,180 @@ const (
2728
SMTO_ABORTIFHUNG = 0x0002
2829
)
2930

30-
// AddToPath adds the shims directory to the user's PATH on Windows
31+
// AddToPath adds the shims directory to the System PATH on Windows.
32+
// This requires administrator privileges. If not elevated, it will prompt
33+
// the user to re-run with elevation.
3134
func AddToPath(shimsDir string) error {
32-
// Check if already in PATH
33-
if IsInPath(shimsDir) {
34-
ui.Info("%s is already in your PATH", shimsDir)
35+
// Check current System PATH status
36+
needsUpdate, action, err := checkSystemPath(shimsDir)
37+
if err != nil {
38+
return err
39+
}
40+
41+
if !needsUpdate {
42+
ui.Success("%s is already at the beginning of your System PATH", shimsDir)
3543
return nil
3644
}
3745

38-
// Prompt user for confirmation
39-
ui.Header("PATH Setup Required")
40-
ui.Info("dtvem needs to add the shims directory to your PATH")
41-
ui.Info("Directory: %s", ui.Highlight(shimsDir))
42-
ui.Info("This will modify your user PATH environment variable")
43-
fmt.Printf("\nProceed? [Y/n]: ")
46+
// Check if we have admin privileges
47+
if !isAdmin() {
48+
return promptForElevation(shimsDir, action)
49+
}
50+
51+
// We have admin privileges - proceed with modification
52+
return modifySystemPath(shimsDir, action)
53+
}
54+
55+
// checkSystemPath checks if the shims directory needs to be added/moved in System PATH
56+
// Returns: needsUpdate, action ("add" or "move"), error
57+
func checkSystemPath(shimsDir string) (bool, string, error) {
58+
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`, registry.QUERY_VALUE)
59+
if err != nil {
60+
return false, "", fmt.Errorf("failed to open System PATH registry key: %w", err)
61+
}
62+
defer func() { _ = key.Close() }()
63+
64+
currentPath, _, err := key.GetStringValue("Path")
65+
if err != nil && !errors.Is(err, registry.ErrNotExist) {
66+
return false, "", fmt.Errorf("failed to read System PATH: %w", err)
67+
}
68+
69+
paths := strings.Split(currentPath, ";")
70+
foundAt := -1
71+
72+
for i, p := range paths {
73+
trimmed := strings.TrimSpace(p)
74+
if strings.EqualFold(trimmed, shimsDir) {
75+
foundAt = i
76+
break
77+
}
78+
}
79+
80+
if foundAt == 0 {
81+
return false, "", nil // Already at beginning
82+
} else if foundAt > 0 {
83+
return true, "move", nil // Exists but not at beginning
84+
}
85+
return true, "add", nil // Not in PATH
86+
}
87+
88+
// isAdmin checks if the current process has administrator privileges
89+
func isAdmin() bool {
90+
_, err := os.Open("\\\\.\\PHYSICALDRIVE0")
91+
if err != nil {
92+
return false
93+
}
94+
return true
95+
}
96+
97+
// promptForElevation prompts the user to re-run dtvem init with admin privileges
98+
func promptForElevation(shimsDir, action string) error {
99+
if action == "move" {
100+
ui.Header("PATH Fix Required (Administrator)")
101+
ui.Warning("%s is in your System PATH but not at the beginning", shimsDir)
102+
ui.Info("It needs to be first to take priority over other installations")
103+
} else {
104+
ui.Header("PATH Setup Required (Administrator)")
105+
ui.Info("dtvem needs to add the shims directory to your System PATH")
106+
ui.Info("Directory: %s", ui.Highlight(shimsDir))
107+
}
108+
109+
ui.Info("")
110+
ui.Info("On Windows, System PATH takes priority over User PATH.")
111+
ui.Info("Modifying System PATH requires administrator privileges.")
112+
113+
fmt.Printf("\nRe-run with administrator privileges? [Y/n]: ")
44114

45115
var response string
46116
_, _ = fmt.Scanln(&response)
47117
response = strings.ToLower(strings.TrimSpace(response))
48118

49119
if response != "" && response != constants.ResponseY && response != constants.ResponseYes {
50-
ui.Warning("PATH not modified. You can add it manually later by running: dtvem init")
120+
ui.Warning("PATH not modified. You can run 'dtvem init' again later.")
51121
return nil
52122
}
53123

54-
// Get current user PATH from registry
55-
key, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.QUERY_VALUE|registry.SET_VALUE)
124+
// Re-launch with elevation
125+
return relaunchElevated()
126+
}
127+
128+
// relaunchElevated re-launches the current executable with administrator privileges
129+
func relaunchElevated() error {
130+
exe, err := os.Executable()
131+
if err != nil {
132+
return fmt.Errorf("failed to get executable path: %w", err)
133+
}
134+
135+
cwd, err := os.Getwd()
136+
if err != nil {
137+
return fmt.Errorf("failed to get working directory: %w", err)
138+
}
139+
140+
// Use ShellExecute with "runas" verb to request elevation
141+
verb := windows.StringToUTF16Ptr("runas")
142+
exePath := windows.StringToUTF16Ptr(exe)
143+
args := windows.StringToUTF16Ptr("init")
144+
dir := windows.StringToUTF16Ptr(cwd)
145+
146+
err = windows.ShellExecute(0, verb, exePath, args, dir, windows.SW_SHOWNORMAL)
147+
if err != nil {
148+
return fmt.Errorf("failed to elevate: %w", err)
149+
}
150+
151+
ui.Info("Elevated process launched. Please complete the setup in the new window.")
152+
return nil
153+
}
154+
155+
// modifySystemPath modifies the System PATH (requires admin privileges)
156+
func modifySystemPath(shimsDir, action string) error {
157+
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`, registry.QUERY_VALUE|registry.SET_VALUE)
56158
if err != nil {
57-
return fmt.Errorf("failed to open registry key: %w", err)
159+
return fmt.Errorf("failed to open System PATH registry key for writing: %w", err)
58160
}
59161
defer func() { _ = key.Close() }()
60162

61163
currentPath, _, err := key.GetStringValue("Path")
62164
if err != nil && !errors.Is(err, registry.ErrNotExist) {
63-
return fmt.Errorf("failed to read current PATH: %w", err)
165+
return fmt.Errorf("failed to read System PATH: %w", err)
64166
}
65167

66-
// Check if already present (double-check)
168+
// Parse and filter current PATH entries
67169
paths := strings.Split(currentPath, ";")
170+
var filteredPaths []string
171+
68172
for _, p := range paths {
69-
if strings.EqualFold(strings.TrimSpace(p), shimsDir) {
70-
ui.Info("%s is already in your registry PATH", shimsDir)
71-
return nil
173+
trimmed := strings.TrimSpace(p)
174+
if trimmed == "" {
175+
continue
176+
}
177+
// Skip if it's the shims dir (we'll prepend it)
178+
if strings.EqualFold(trimmed, shimsDir) {
179+
continue
72180
}
181+
filteredPaths = append(filteredPaths, trimmed)
73182
}
74183

75-
// Prepend the shims directory to the BEGINNING for priority
184+
// Build new PATH with shimsDir at the beginning
76185
newPath := shimsDir
77-
if currentPath != "" {
78-
newPath += ";" + currentPath
186+
if len(filteredPaths) > 0 {
187+
newPath += ";" + strings.Join(filteredPaths, ";")
79188
}
80189

81190
// Write back to registry
82191
err = key.SetStringValue("Path", newPath)
83192
if err != nil {
84-
return fmt.Errorf("failed to update PATH in registry: %w", err)
193+
return fmt.Errorf("failed to update System PATH in registry: %w", err)
85194
}
86195

87196
// Broadcast WM_SETTINGCHANGE to notify running processes
88197
broadcastSettingChange()
89198

90-
ui.Success("Added %s to your PATH", shimsDir)
199+
if action == "move" {
200+
ui.Success("Moved %s to the beginning of your System PATH", shimsDir)
201+
} else {
202+
ui.Success("Added %s to your System PATH", shimsDir)
203+
}
91204
ui.Warning("Please restart your terminal for the changes to take effect")
92-
ui.Info("You can verify by running: echo %%PATH%%")
93205

94206
return nil
95207
}

0 commit comments

Comments
 (0)