@@ -5,10 +5,10 @@ import (
55 "fmt"
66 "os"
77 "os/exec"
8- "runtime"
98 "strconv"
109 "strings"
1110
11+ "github.com/dtvem/dtvem/src/internal/migration"
1212 internalRuntime "github.com/dtvem/dtvem/src/internal/runtime"
1313 "github.com/dtvem/dtvem/src/internal/ui"
1414 "github.com/spf13/cobra"
@@ -41,14 +41,27 @@ Examples:
4141 spinner := ui .NewSpinner (fmt .Sprintf ("Scanning for %s installations..." , provider .DisplayName ()))
4242 spinner .Start ()
4343
44- // Detect existing installations
45- detected , err := provider .DetectInstalled ()
46- if err != nil {
47- spinner .Error ("Scan failed" )
48- ui .Error ("Error detecting installations: %v" , err )
49- return
44+ // Get migration providers for this runtime
45+ migrationProviders := migration .GetByRuntime (runtimeName )
46+
47+ // Collect all detected versions from all migration providers
48+ detected := make ([]detectedVersionWithProvider , 0 )
49+ for _ , mp := range migrationProviders {
50+ versions , err := mp .DetectVersions ()
51+ if err != nil {
52+ continue // Skip providers that fail
53+ }
54+ for _ , v := range versions {
55+ detected = append (detected , detectedVersionWithProvider {
56+ DetectedVersion : v ,
57+ MigrationProvider : mp ,
58+ })
59+ }
5060 }
5161
62+ // Deduplicate by path
63+ detected = deduplicateByPath (detected )
64+
5265 if len (detected ) == 0 {
5366 spinner .Warning ("No installations found" )
5467 ui .Info ("Use 'dtvem install %s <version>' to install a version" , runtimeName )
@@ -62,7 +75,7 @@ Examples:
6275 for i , dv := range detected {
6376 validatedMark := ""
6477 if dv .Validated {
65- validatedMark = " " + ui .Highlight ("✓ " )
78+ validatedMark = " " + ui .Highlight ("\u2713 " )
6679 }
6780 fmt .Printf (" [%d] %s (%s) %s%s\n " ,
6881 i + 1 ,
@@ -97,7 +110,7 @@ Examples:
97110 }
98111
99112 // Get selected versions
100- selectedVersions := make ([]internalRuntime. DetectedVersion , 0 )
113+ selectedVersions := make ([]detectedVersionWithProvider , 0 )
101114 for _ , idx := range selectedIndices {
102115 selectedVersions = append (selectedVersions , detected [idx ])
103116 }
@@ -126,11 +139,6 @@ Examples:
126139 }
127140 }
128141
129- // TODO: Detect and preserve configuration files/settings
130- // For Node.js: Check for .npmrc in installation dir or ~/.npmrc
131- // For Python: Check for pip.conf/pip.ini
132- // Handle sensitive data (auth tokens) appropriately
133-
134142 // Call the provider's Install method
135143 if err := provider .Install (dv .Version ); err != nil {
136144 ui .Error ("%v" , err )
@@ -150,9 +158,6 @@ Examples:
150158 ui .Success ("Reinstalled %d global package(s)" , len (globalPackages ))
151159 }
152160 }
153-
154- // TODO: Copy/merge configuration files to new installation
155- // Ensure settings like registry URLs, proxies, etc. are preserved
156161 }
157162 fmt .Println ()
158163 }
@@ -210,6 +215,27 @@ Examples:
210215 },
211216}
212217
218+ // detectedVersionWithProvider pairs a detected version with its migration provider.
219+ type detectedVersionWithProvider struct {
220+ migration.DetectedVersion
221+ MigrationProvider migration.Provider
222+ }
223+
224+ // deduplicateByPath removes duplicate versions based on their path.
225+ func deduplicateByPath (versions []detectedVersionWithProvider ) []detectedVersionWithProvider {
226+ seen := make (map [string ]bool )
227+ result := make ([]detectedVersionWithProvider , 0 )
228+
229+ for _ , v := range versions {
230+ if ! seen [v .Path ] {
231+ seen [v .Path ] = true
232+ result = append (result , v )
233+ }
234+ }
235+
236+ return result
237+ }
238+
213239// parseSelection parses user selection input like "1,3,5" or "all"
214240func parseSelection (input string , maxCount int ) []int {
215241 indices := make ([]int , 0 , maxCount )
@@ -237,7 +263,7 @@ func parseSelection(input string, maxCount int) []int {
237263}
238264
239265// promptCleanupOldInstallations prompts the user to clean up old installations after successful migration
240- func promptCleanupOldInstallations (versions []internalRuntime. DetectedVersion , runtimeDisplayName string ) {
266+ func promptCleanupOldInstallations (versions []detectedVersionWithProvider , runtimeDisplayName string ) {
241267 ui .Header ("Cleanup Old Installations" )
242268 ui .Info ("You have successfully migrated to dtvem. Would you like to clean up the old installations?" )
243269 ui .Info ("This helps prevent PATH conflicts and version confusion." )
@@ -251,10 +277,12 @@ func promptCleanupOldInstallations(versions []internalRuntime.DetectedVersion, r
251277 fmt .Printf ("Old installation: %s %s\n " , ui .HighlightVersion ("v" + dv .Version ), ui .Highlight ("(" + dv .Source + ")" ))
252278 fmt .Printf (" Location: %s\n " , dv .Path )
253279
254- // Get uninstall instructions/command based on source
255- instructions , command , automatable := getUninstallInstructions (dv , runtimeDisplayName )
280+ mp := dv .MigrationProvider
281+ canAuto := mp .CanAutoUninstall ()
282+ command := mp .UninstallCommand (dv .Version )
283+ instructions := mp .ManualInstructions ()
256284
257- if automatable {
285+ if canAuto && command != "" {
258286 fmt .Printf ("\n Remove this installation? [y/N]: " )
259287 input , err := reader .ReadString ('\n' )
260288 if err != nil || strings .ToLower (strings .TrimSpace (input )) != "y" {
@@ -277,7 +305,7 @@ func promptCleanupOldInstallations(versions []internalRuntime.DetectedVersion, r
277305 removedCount ++
278306 }
279307 } else {
280- // System installs - provide instructions only
308+ // System installs or version managers without auto-uninstall - provide instructions only
281309 ui .Warning ("Manual removal required" )
282310 ui .Info ("%s" , instructions )
283311 skippedCount ++
@@ -297,63 +325,6 @@ func promptCleanupOldInstallations(versions []internalRuntime.DetectedVersion, r
297325 }
298326}
299327
300- // getUninstallInstructions returns instructions, command, and whether it's automatable
301- func getUninstallInstructions (dv internalRuntime.DetectedVersion , runtimeDisplayName string ) (instructions string , command string , automatable bool ) {
302- version := dv .Version
303- source := strings .ToLower (dv .Source )
304-
305- switch source {
306- case "nvm" :
307- return "" ,
308- fmt .Sprintf ("nvm uninstall %s" , version ),
309- true
310- case "pyenv" :
311- return "" ,
312- fmt .Sprintf ("pyenv uninstall %s" , version ),
313- true
314- case "fnm" :
315- return "" ,
316- fmt .Sprintf ("fnm uninstall %s" , version ),
317- true
318- case "rbenv" :
319- return "" ,
320- fmt .Sprintf ("rbenv uninstall %s" , version ),
321- true
322- case "system" :
323- // OS-specific instructions
324- instructions := getSystemUninstallInstructions (runtimeDisplayName , dv .Path )
325- return instructions , "" , false
326- default :
327- // Unknown source - provide generic instructions
328- return fmt .Sprintf ("Manually remove the installation directory:\n %s" , dv .Path ), "" , false
329- }
330- }
331-
332- // getSystemUninstallInstructions provides OS-specific uninstall instructions for system packages
333- func getSystemUninstallInstructions (runtimeDisplayName string , path string ) string {
334- switch runtime .GOOS {
335- case "windows" :
336- return "To uninstall:\n " +
337- " 1. Open Settings → Apps → Installed apps\n " +
338- " 2. Search for " + runtimeDisplayName + "\n " +
339- " 3. Click Uninstall\n " +
340- " Or use PowerShell to find the uninstaller"
341- case "darwin" :
342- return "To uninstall:\n " +
343- " If installed via Homebrew: brew uninstall " + strings .ToLower (runtimeDisplayName ) + "\n " +
344- " If installed via package: check /Applications or use the installer's uninstaller\n " +
345- " Manual removal: sudo rm -rf " + path
346- case "linux" :
347- return "To uninstall:\n " +
348- " If installed via apt: sudo apt remove " + strings .ToLower (runtimeDisplayName ) + "\n " +
349- " If installed via yum: sudo yum remove " + strings .ToLower (runtimeDisplayName ) + "\n " +
350- " If installed via dnf: sudo dnf remove " + strings .ToLower (runtimeDisplayName ) + "\n " +
351- " Manual removal: sudo rm -rf " + path
352- default :
353- return "Manually remove the installation directory:\n " + path
354- }
355- }
356-
357328// executeUninstallCommand executes the uninstall command for automated cleanup
358329func executeUninstallCommand (command string ) error {
359330 // Parse the command into parts
0 commit comments