From c6912660c6a34562b9b6661c226abcd7749889c7 Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Tue, 9 Dec 2025 14:37:18 -0500 Subject: [PATCH] fix(init): use system path on windows for correct priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows combines PATH as System + User, meaning system installations always take priority. This fix modifies System PATH instead of User PATH to ensure dtvem shims take priority over system-level installations. Changes: - Modify System PATH (HKLM) instead of User PATH (HKCU) on Windows - Check for admin privileges before attempting modification - Prompt for UAC elevation if not already elevated - Use ShellExecute with "runas" verb to re-launch with elevation - Detect if shims already exist but not at beginning and offer to fix - Simplify init.go to delegate all PATH logic to AddToPath Unix/macOS behavior unchanged (prepend to shell config files). Fixes #55 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/cmd/init.go | 16 +-- src/internal/path/path_windows.go | 162 +++++++++++++++++++++++++----- 2 files changed, 142 insertions(+), 36 deletions(-) diff --git a/src/cmd/init.go b/src/cmd/init.go index 992fc89..0ee3d8b 100644 --- a/src/cmd/init.go +++ b/src/cmd/init.go @@ -35,19 +35,13 @@ Example: spinner.Success("Directories created") - // Setup PATH + // Setup PATH - AddToPath handles checking position and moving if needed shimsDir := path.ShimsDir() - if path.IsInPath(shimsDir) { - ui.Success("PATH is already configured correctly") - ui.Info("Shims directory: %s", ui.Highlight(shimsDir)) - } else { - ui.Info("Setting up PATH...") - if err := path.AddToPath(shimsDir); err != nil { - ui.Error("Failed to configure PATH: %v", err) - ui.Info("You can manually add %s to your PATH", shimsDir) - return - } + if err := path.AddToPath(shimsDir); err != nil { + ui.Error("Failed to configure PATH: %v", err) + ui.Info("You can manually add %s to your PATH", shimsDir) + return } ui.Success("dtvem initialized successfully!") diff --git a/src/internal/path/path_windows.go b/src/internal/path/path_windows.go index 8d5d375..7f0f3b5 100644 --- a/src/internal/path/path_windows.go +++ b/src/internal/path/path_windows.go @@ -13,6 +13,7 @@ import ( "github.com/dtvem/dtvem/src/internal/constants" "github.com/dtvem/dtvem/src/internal/ui" + "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) @@ -27,69 +28,180 @@ const ( SMTO_ABORTIFHUNG = 0x0002 ) -// AddToPath adds the shims directory to the user's PATH on Windows +// AddToPath adds the shims directory to the System PATH on Windows. +// This requires administrator privileges. If not elevated, it will prompt +// the user to re-run with elevation. func AddToPath(shimsDir string) error { - // Check if already in PATH - if IsInPath(shimsDir) { - ui.Info("%s is already in your PATH", shimsDir) + // Check current System PATH status + needsUpdate, action, err := checkSystemPath(shimsDir) + if err != nil { + return err + } + + if !needsUpdate { + ui.Success("%s is already at the beginning of your System PATH", shimsDir) return nil } - // Prompt user for confirmation - ui.Header("PATH Setup Required") - ui.Info("dtvem needs to add the shims directory to your PATH") - ui.Info("Directory: %s", ui.Highlight(shimsDir)) - ui.Info("This will modify your user PATH environment variable") - fmt.Printf("\nProceed? [Y/n]: ") + // Check if we have admin privileges + if !isAdmin() { + return promptForElevation(shimsDir, action) + } + + // We have admin privileges - proceed with modification + return modifySystemPath(shimsDir, action) +} + +// checkSystemPath checks if the shims directory needs to be added/moved in System PATH +// Returns: needsUpdate, action ("add" or "move"), error +func checkSystemPath(shimsDir string) (bool, string, error) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`, registry.QUERY_VALUE) + if err != nil { + return false, "", fmt.Errorf("failed to open System PATH registry key: %w", err) + } + defer func() { _ = key.Close() }() + + currentPath, _, err := key.GetStringValue("Path") + if err != nil && !errors.Is(err, registry.ErrNotExist) { + return false, "", fmt.Errorf("failed to read System PATH: %w", err) + } + + paths := strings.Split(currentPath, ";") + foundAt := -1 + + for i, p := range paths { + trimmed := strings.TrimSpace(p) + if strings.EqualFold(trimmed, shimsDir) { + foundAt = i + break + } + } + + if foundAt == 0 { + return false, "", nil // Already at beginning + } else if foundAt > 0 { + return true, "move", nil // Exists but not at beginning + } + return true, "add", nil // Not in PATH +} + +// isAdmin checks if the current process has administrator privileges +func isAdmin() bool { + _, err := os.Open("\\\\.\\PHYSICALDRIVE0") + if err != nil { + return false + } + return true +} + +// promptForElevation prompts the user to re-run dtvem init with admin privileges +func promptForElevation(shimsDir, action string) error { + if action == "move" { + ui.Header("PATH Fix Required (Administrator)") + ui.Warning("%s is in your System PATH but not at the beginning", shimsDir) + ui.Info("It needs to be first to take priority over other installations") + } else { + ui.Header("PATH Setup Required (Administrator)") + ui.Info("dtvem needs to add the shims directory to your System PATH") + ui.Info("Directory: %s", ui.Highlight(shimsDir)) + } + + ui.Info("") + ui.Info("On Windows, System PATH takes priority over User PATH.") + ui.Info("Modifying System PATH requires administrator privileges.") + + fmt.Printf("\nRe-run with administrator privileges? [Y/n]: ") var response string _, _ = fmt.Scanln(&response) response = strings.ToLower(strings.TrimSpace(response)) if response != "" && response != constants.ResponseY && response != constants.ResponseYes { - ui.Warning("PATH not modified. You can add it manually later by running: dtvem init") + ui.Warning("PATH not modified. You can run 'dtvem init' again later.") return nil } - // Get current user PATH from registry - key, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.QUERY_VALUE|registry.SET_VALUE) + // Re-launch with elevation + return relaunchElevated() +} + +// relaunchElevated re-launches the current executable with administrator privileges +func relaunchElevated() error { + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + // Use ShellExecute with "runas" verb to request elevation + verb := windows.StringToUTF16Ptr("runas") + exePath := windows.StringToUTF16Ptr(exe) + args := windows.StringToUTF16Ptr("init") + dir := windows.StringToUTF16Ptr(cwd) + + err = windows.ShellExecute(0, verb, exePath, args, dir, windows.SW_SHOWNORMAL) + if err != nil { + return fmt.Errorf("failed to elevate: %w", err) + } + + ui.Info("Elevated process launched. Please complete the setup in the new window.") + return nil +} + +// modifySystemPath modifies the System PATH (requires admin privileges) +func modifySystemPath(shimsDir, action string) error { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`, registry.QUERY_VALUE|registry.SET_VALUE) if err != nil { - return fmt.Errorf("failed to open registry key: %w", err) + return fmt.Errorf("failed to open System PATH registry key for writing: %w", err) } defer func() { _ = key.Close() }() currentPath, _, err := key.GetStringValue("Path") if err != nil && !errors.Is(err, registry.ErrNotExist) { - return fmt.Errorf("failed to read current PATH: %w", err) + return fmt.Errorf("failed to read System PATH: %w", err) } - // Check if already present (double-check) + // Parse and filter current PATH entries paths := strings.Split(currentPath, ";") + var filteredPaths []string + for _, p := range paths { - if strings.EqualFold(strings.TrimSpace(p), shimsDir) { - ui.Info("%s is already in your registry PATH", shimsDir) - return nil + trimmed := strings.TrimSpace(p) + if trimmed == "" { + continue + } + // Skip if it's the shims dir (we'll prepend it) + if strings.EqualFold(trimmed, shimsDir) { + continue } + filteredPaths = append(filteredPaths, trimmed) } - // Prepend the shims directory to the BEGINNING for priority + // Build new PATH with shimsDir at the beginning newPath := shimsDir - if currentPath != "" { - newPath += ";" + currentPath + if len(filteredPaths) > 0 { + newPath += ";" + strings.Join(filteredPaths, ";") } // Write back to registry err = key.SetStringValue("Path", newPath) if err != nil { - return fmt.Errorf("failed to update PATH in registry: %w", err) + return fmt.Errorf("failed to update System PATH in registry: %w", err) } // Broadcast WM_SETTINGCHANGE to notify running processes broadcastSettingChange() - ui.Success("Added %s to your PATH", shimsDir) + if action == "move" { + ui.Success("Moved %s to the beginning of your System PATH", shimsDir) + } else { + ui.Success("Added %s to your System PATH", shimsDir) + } ui.Warning("Please restart your terminal for the changes to take effect") - ui.Info("You can verify by running: echo %%PATH%%") return nil }