From 51c3b178d02fbf9fa26e3f13d540c725787dc027 Mon Sep 17 00:00:00 2001 From: Corey Butler <770982+coreybutler@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:10:33 -0600 Subject: [PATCH] Added upgrade and reinstall commands --- src/go.mod | 9 +- src/go.sum | 16 + src/nvm.go | 203 ++++++++--- src/upgrade/upgrade.go | 765 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 941 insertions(+), 52 deletions(-) create mode 100644 src/upgrade/upgrade.go diff --git a/src/go.mod b/src/go.mod index 2be2b0b..72fce34 100644 --- a/src/go.mod +++ b/src/go.mod @@ -2,15 +2,18 @@ module nvm go 1.18 -// replace github.com/coreybutler/go-fsutil => C:\Users\corey\OneDrive\Documents\workspace\oss\coreybutler\go-fsutil - require ( github.com/blang/semver v3.5.1+incompatible github.com/coreybutler/go-fsutil v1.2.0 github.com/coreybutler/go-where v1.0.2 + github.com/minio/selfupdate v0.6.0 github.com/olekukonko/tablewriter v0.0.5 github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d golang.org/x/sys v0.1.0 ) -require github.com/mattn/go-runewidth v0.0.9 // indirect +require ( + aead.dev/minisign v0.2.0 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect +) diff --git a/src/go.sum b/src/go.sum index 5fd56b9..9cd7f10 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,3 +1,5 @@ +aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk= +aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/coreybutler/go-fsutil v1.2.0 h1:kbm62NSofawglUppEOhpHC3NDf/J7ZpguBirBnsgUwU= @@ -6,6 +8,8 @@ github.com/coreybutler/go-where v1.0.2 h1:Omit67KeTtKpvSJjezVxnVD4qMtvlXDlItiKpV github.com/coreybutler/go-where v1.0.2/go.mod h1:IqV4saJiDXdNJURfTfVRywDHvY1IG5F+GXb2kmnmEe8= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU= +github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= @@ -13,17 +17,29 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4= +golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190825031127-d72b05d2b1b6/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/src/nvm.go b/src/nvm.go index 9bcf596..7e2a51a 100644 --- a/src/nvm.go +++ b/src/nvm.go @@ -22,10 +22,12 @@ import ( "nvm/encoding" "nvm/file" "nvm/node" + "nvm/upgrade" "nvm/web" "github.com/blang/semver" // "github.com/fatih/color" + "github.com/coreybutler/go-where" "github.com/olekukonko/tablewriter" "golang.org/x/sys/windows" @@ -33,7 +35,7 @@ import ( ) const ( - NvmVersion = "1.1.11" + NvmVersion = "1.2.0" ) type Environment struct { @@ -70,10 +72,10 @@ func main() { detail := "" procarch := arch.Validate(env.arch) - if !isTerminal() { - alert("NVM for Windows should be run from a terminal such as CMD or PowerShell.", "Terminal Only") - os.Exit(0) - } + // if !isTerminal() { + // alert("NVM for Windows should be run from a terminal such as CMD or PowerShell.", "Terminal Only") + // os.Exit(0) + // } // Capture any additional arguments if len(args) > 2 { @@ -99,6 +101,8 @@ func main() { install(detail, procarch) case "uninstall": uninstall(detail) + case "reinstall": + reinstall(detail, procarch) case "use": use(detail, procarch) case "list": @@ -167,6 +171,8 @@ func main() { setNpmMirror(detail) case "debug": checkLocalEnvironment() + case "upgrade": + upgrade.Run(NvmVersion) default: help() } @@ -185,13 +191,13 @@ func setNpmMirror(uri string) { saveSettings() } -func isTerminal() bool { - fileInfo, err := os.Stdout.Stat() - if err != nil { - return false - } - return (fileInfo.Mode() & os.ModeCharDevice) != 0 -} +// func isTerminal() bool { +// fileInfo, err := os.Stdout.Stat() +// if err != nil { +// return false +// } +// return (fileInfo.Mode() & os.ModeCharDevice) != 0 +// } // const ( // MB_YESNOCANCEL = 0x00000003 @@ -263,23 +269,23 @@ func isTerminal() bool { // ) // } -func alert(msg string, caption ...string) { - user32 := windows.NewLazySystemDLL("user32.dll") - mbox := user32.NewProc("MessageBoxW") - getForegroundWindow := user32.NewProc("GetForegroundWindow") - var hwnd uintptr - ret, _, _ := getForegroundWindow.Call() - if ret != 0 { - hwnd = ret - } +// func alert(msg string, caption ...string) { +// user32 := windows.NewLazySystemDLL("user32.dll") +// mbox := user32.NewProc("MessageBoxW") +// getForegroundWindow := user32.NewProc("GetForegroundWindow") +// var hwnd uintptr +// ret, _, _ := getForegroundWindow.Call() +// if ret != 0 { +// hwnd = ret +// } - title := "Alert" - if len(caption) > 0 { - title = caption[0] - } +// title := "Alert" +// if len(caption) > 0 { +// title = caption[0] +// } - mbox.Call(hwnd, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(msg))), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(title))), uintptr(windows.MB_OK)) -} +// mbox.Call(hwnd, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(msg))), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(title))), uintptr(windows.MB_OK)) +// } /* func update() { @@ -474,6 +480,7 @@ func install(version string, cpuarch string) { npmv := getNpmVersion(version) fmt.Println("npm v" + npmv + " installed successfully.") fmt.Println("\n\nInstallation complete. If you want to use this version, type\n\nnvm use " + version) + // fmt.Printf("Installed to %v\n", filepath.Join(env.root, "v"+version)) return } @@ -557,7 +564,57 @@ func install(version string, cpuarch string) { fmt.Println("Version " + version + " is already installed.") return } +} +func reinstall(version, cpuarch string) { + // Make sure a version is specified + if len(version) == 0 { + fmt.Println("Provide the version you want to uninstall.") + help() + return + } + + if strings.ToLower(version) == "latest" || strings.ToLower(version) == "node" { + version = getLatest() + } else if strings.ToLower(version) == "lts" { + version = getLTS() + } else if strings.ToLower(version) == "newest" { + installed := node.GetInstalled(env.root) + if len(installed) == 0 { + fmt.Println("No versions of node.js found. Try installing the latest by typing nvm install latest.") + return + } + + version = installed[0] + } + + version = cleanVersion(version) + + // Determine if the version exists and skip if it doesn't + if node.IsVersionInstalled(env.root, version, "32") || node.IsVersionInstalled(env.root, version, "64") { + v, _ := node.GetCurrentVersion() + + fmt.Printf("Removing v%v...\n", version) + + if v == version { + // _, err := runElevated(fmt.Sprintf(`"%s" cmd /C rmdir "%s"`, filepath.Join(env.root, "elevate.cmd"), filepath.Clean(env.symlink))) + _, err := elevatedRun("rmdir", filepath.Clean(env.symlink)) + if err != nil { + fmt.Println(fmt.Sprint(err)) + return + } + } + + e := os.RemoveAll(filepath.Join(env.root, "v"+version)) + if e != nil { + fmt.Printf("error: failed to remove v%v: %v\n", version, e) + os.Exit(1) + } + } else { + fmt.Printf("node v%v is not installed. Type \"nvm list\" to see what is installed.\n", version) + } + + install(version, cpuarch) } func uninstall(version string) { @@ -1062,11 +1119,9 @@ func checkLocalEnvironment() { } else { consoleTitle := syscall.UTF16ToString(title[:]) - if !strings.Contains(strings.ToLower(consoleTitle), "command prompt") && !strings.Contains(strings.ToLower(consoleTitle), "powershell") { - problems = append(problems, fmt.Sprintf("\"%v\" is not an officially supported shell. Some features may not work as expected.\n", consoleTitle)) + if !strings.Contains(strings.ToLower(consoleTitle), "command prompt") && !strings.Contains(strings.ToLower(consoleTitle), "powershell") && !strings.Contains(strings.ToLower(consoleTitle), "cmd.exe") && !strings.Contains(strings.ToLower(consoleTitle), "pwsh.exe") { + problems = append(problems, fmt.Sprintf("\"%v\" not recognized: the Command Prompt and Powershell are the only officially supported consoles. Some features may not work as expected.\n", consoleTitle)) } - - fmt.Printf("\n%v", consoleTitle) } } } @@ -1082,7 +1137,7 @@ func checkLocalEnvironment() { } // fmt.Printf(" %d.%d\n", versionInfo.MajorVersion, versionInfo.MinorVersion) maj, min, patch := windows.RtlGetNtVersionNumbers() - fmt.Printf("\nWindows Version: %d.%d (Build %d)\n", maj, min, patch) + fmt.Printf("\nWindows Version: %d.%d (Build %d)\n", maj, min, patch) // SHELL in Linux // TERM in Windows @@ -1119,7 +1174,15 @@ func checkLocalEnvironment() { v := node.GetInstalled(env.root) nvmhome := os.Getenv("NVM_HOME") - fmt.Printf("\nNVM4W Version: %v\nNVM4W Path: %v\nNVM4W Settings: %v\nNVM_HOME: %v\nNVM_SYMLINK: %v\nNode Installations: %v\n\nTotal Node.js Versions: %v\nActive Node.js Version: %v", NvmVersion, path, home, nvmhome, symlink, env.root, len(v), out) + mirrors := "No mirrors configured" + if len(env.node_mirror) > 0 && len(env.npm_mirror) > 0 { + mirrors = env.node_mirror + " (node) and " + env.npm_mirror + " (npm)" + } else if len(env.node_mirror) > 0 { + mirrors = env.node_mirror + " (node)" + } else if len(env.npm_mirror) > 0 { + mirrors = env.npm_mirror + " (npm)" + } + fmt.Printf("\nNVM4W Version: %v\nNVM4W Path: %v\nNVM4W Settings: %v\nNVM_HOME: %v\nNVM_SYMLINK: %v\nNode Installations: %v\nDefault Architecture: %v-bit\nMirrors: %v\nHTTP Proxy: %v\n\nTotal Node.js Versions: %v\nActive Node.js Version: %v", NvmVersion, path, home, nvmhome, symlink, env.root, env.arch, mirrors, env.proxy, len(v), out) if !nvmsymlinkfound { problems = append(problems, "The NVM4W symlink ("+env.symlink+") was not found in the PATH environment variable.") @@ -1138,12 +1201,8 @@ func checkLocalEnvironment() { } } else { if fileInfo.Mode()&os.ModeSymlink != 0 { - targetPath, err := os.Readlink(symlink) - if err != nil { - fmt.Println(err) - } - - targetFileInfo, err := os.Lstat(targetPath) + targetPath, _ := os.Readlink(symlink) + targetFileInfo, _ := os.Lstat(targetPath) if !targetFileInfo.Mode().IsDir() { problems = append(problems, "NVM_SYMLINK is a symlink linking to a file instead of a directory.") @@ -1153,11 +1212,15 @@ func checkLocalEnvironment() { } } + if strings.Contains(symlink, home) { + problems = append(problems, "Storing the NVM_SYMLINK ("+symlink+") within the NVM_HOME directory ("+home+") has been known to cause problems in many Windows environments. Change NVM_SYMLINK to a different directory that does not already exist.") + } + ipv6, err := web.IsLocalIPv6() if err != nil { problems = append(problems, "Connection type cannot be determined: "+err.Error()) } else if !ipv6 { - fmt.Println("\nIPv6 is enabled. This can slow downloads significantly.") + fmt.Println("\nIPv6 is enabled. This has been known to slow downloads significantly.") } nodelist := web.Ping(web.GetFullNodeUrl("index.json")) @@ -1179,6 +1242,7 @@ func checkLocalEnvironment() { if _, err = os.Stat(filepath.Join(env.root, v[i], "node.exe")); err != nil { invalid = append(invalid, v[i]) } else if _, err = os.Stat(filepath.Join(env.root, v[i], "npm.cmd")); err != nil { + fmt.Println(err) invalidnpm = append(invalid, v[i]) } } @@ -1222,6 +1286,36 @@ func checkLocalEnvironment() { } } + // Check for updates + colorize := true + if err := upgrade.EnableVirtualTerminalProcessing(); err != nil { + colorize = false + } + update, checkerr := upgrade.Get() + if checkerr == nil { + if len(update.Warnings) > 0 { + fmt.Println("") + } + for _, warning := range update.Warnings { + upgrade.Warn(warning, colorize) + } + if len(update.Warnings) > 0 { + fmt.Println("") + } + } + + if checkerr != nil { + fmt.Println("error checking for updates: " + checkerr.Error()) + } else { + newVersion, available, err := update.Available(NvmVersion) + if err != nil { + fmt.Println("Error checking for updates: " + err.Error()) + } else if available { + upgrade.Warn(fmt.Sprintf("An upgrade is available: v%s", newVersion), colorize) + fmt.Println(" run \"nvm upgrade\" to update.\n") + } + } + fmt.Println("\n" + "Find help at https://github.com/coreybutler/nvm-windows/wiki/Common-Issues") } @@ -1244,10 +1338,11 @@ func help() { fmt.Println(" nvm node_mirror [url] : Set the node mirror. Defaults to https://nodejs.org/dist/. Leave [url] blank to use default url.") fmt.Println(" nvm npm_mirror [url] : Set the npm mirror. Defaults to https://github.com/npm/cli/archive/. Leave [url] blank to default url.") fmt.Println(" nvm uninstall : The version must be a specific version.") - // fmt.Println(" nvm update : Automatically update nvm to the latest version.") + fmt.Println(" nvm upgrade [restore] : Update nvm to the latest version. Use \"restore\" to revert to the previously installed version.") fmt.Println(" nvm use [version] [arch] : Switch to use the specified version. Optionally use \"latest\", \"lts\", or \"newest\".") fmt.Println(" \"newest\" is the latest installed version. Optionally specify 32/64bit architecture.") fmt.Println(" nvm use will continue using the selected version, but switch to 32/64 bit mode.") + fmt.Println(" nvm reinstall : A shortcut method to clean and reinstall a specific version.") fmt.Println(" nvm root [path] : Set the directory where nvm should store different versions of node.js.") fmt.Println(" If is not set, the current root will be displayed.") fmt.Println(" nvm [--]version : Displays the current running version of nvm for Windows. Aliased as v.") @@ -1304,6 +1399,10 @@ func cleanVersion(version string) string { // Given a node.js version, returns the associated npm version func getNpmVersion(nodeversion string) string { _, _, _, _, _, npm := node.GetAvailable() + if len(npm) == 0 { + fmt.Println("Error looking up versions: Remote host returned no results. This usually indicates a problem with with Node.js web server. Please try again in a few minutes.") + os.Exit(0) + } return npm[nodeversion] } @@ -1316,14 +1415,20 @@ func getLatest() string { } func getLTS() string { - all, ltsList, current, stable, unstable, npm := node.GetAvailable() - fmt.Println(all) - fmt.Println(ltsList) - fmt.Println(current) - fmt.Println(stable) - fmt.Println(unstable) - fmt.Println(npm) - // _, ltsList, _, _, _, _ := node.GetAvailable() + // all, ltsList, current, stable, unstable, npm := node.GetAvailable() + // fmt.Println(all) + // fmt.Println(ltsList) + // fmt.Println(current) + // fmt.Println(stable) + // fmt.Println(unstable) + // fmt.Println(npm) + _, ltsList, _, _, _, _ := node.GetAvailable() + + if len(ltsList) == 0 { + fmt.Println("Error looking up LTS version: Remote host returned no results. This usually indicates a problem with with Node.js web server. Please try again in a few minutes.") + os.Exit(0) + } + // ltsList has already been numerically sorted return ltsList[0] } diff --git a/src/upgrade/upgrade.go b/src/upgrade/upgrade.go new file mode 100644 index 0000000..407cddb --- /dev/null +++ b/src/upgrade/upgrade.go @@ -0,0 +1,765 @@ +package upgrade + +import ( + "archive/zip" + "crypto/md5" + "encoding/json" + "fmt" + "io" + "net/http" + "nvm/semver" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" + "unsafe" + + "github.com/coreybutler/go-fsutil" + "golang.org/x/sys/windows" +) + +const ( + UPDATE_URL = "https://gist.githubusercontent.com/coreybutler/a12af0f17956a0f25b60369b5f8a661a/raw/nvm4w.json" + // Color codes + yellow = "\033[33m" + reset = "\033[0m" + + // Windows console modes + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + FILE_ATTRIBUTE_HIDDEN = 0x2 + CREATE_NEW_CONSOLE = 0x00000010 // Create a new console for the child process + DETACHED_PROCESS = 0x00000008 // Detach the child process from the parent + + warningIcon = "⚠️" + // exclamationIcon = "❗" +) + +type Update struct { + Version string `json:"version"` + Assets []string `json:"assets"` + Warnings []string `json:"notices"` + VersionWarnings []string `json:"versionNotices"` + SourceURL string `json:"sourceTpl"` +} + +func (u *Update) Available(sinceVersion string) (string, bool, error) { + currentVersion, err := semver.New(sinceVersion) + if err != nil { + return "", false, err + } + + updateVersion, err := semver.New(u.Version) + if err != nil { + return "", false, err + } + + if currentVersion.LT(updateVersion) { + return u.Version, true, nil + } + + return "", false, nil +} + +func Warn(msg string, colorized ...bool) { + if len(colorized) > 0 && colorized[0] { + fmt.Println(warningIcon + " " + highlight(msg)) + } else { + fmt.Println(strings.ToUpper(msg)) + } +} + +func Run(version string) error { + args := os.Args[2:] + + colorize := true + if err := EnableVirtualTerminalProcessing(); err != nil { + colorize = false + } + + // Retrieve remote metadata + update, err := checkForUpdate(UPDATE_URL) + if err != nil { + return fmt.Errorf("error: failed to obtain update data: %v\n", err) + } + + for _, warning := range update.Warnings { + Warn(warning, colorize) + } + + verbose := false + rollback := false + for _, arg := range args { + switch strings.ToLower(arg) { + case "--verbose": + verbose = true + case "rollback": + rollback = true + } + } + + // Check for a backup + if rollback { + if fsutil.Exists(filepath.Join(".", ".update", "nvm4w-backup.zip")) { + fmt.Println("restoring NVM4W backup...") + rbtmp, err := os.MkdirTemp("", "nvm-rollback-*") + if err != nil { + fmt.Printf("error: failed to create rollback directory: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(rbtmp) + + err = unzip(filepath.Join(".", ".update", "nvm4w-backup.zip"), rbtmp) + if err != nil { + fmt.Printf("error: failed to extract backup: %v\n", err) + os.Exit(1) + } + + // Copy the backup files to the current directory + err = copyDirContents(rbtmp, ".") + if err != nil { + fmt.Printf("error: failed to restore backup files: %v\n", err) + os.Exit(1) + } + + // Remove the restoration directory + os.RemoveAll(filepath.Join(".", ".update")) + + fmt.Println("rollback complete") + rbcmd := exec.Command("nvm.exe", "version") + o, err := rbcmd.Output() + if err != nil { + fmt.Println("error running nvm.exe:", err) + os.Exit(1) + } + + exec.Command("schtasks", "/delete", "/tn", "\"RemoveNVM4WBackup\"", "/f").Run() + fmt.Printf("rollback to v%s complete\n", string(o)) + os.Exit(0) + } else { + fmt.Println("no backup available: backups are only available for 7 days after upgrading") + os.Exit(0) + } + } + + currentVersion, err := semver.New(version) + if err != nil { + return err + } + + updateVersion, err := semver.New(update.Version) + if err != nil { + return err + } + + if currentVersion.LT(updateVersion) { + if len(update.VersionWarnings) > 0 { + if len(update.Warnings) > 0 || len(update.VersionWarnings) > 0 { + fmt.Println("") + } + for _, warning := range update.VersionWarnings { + Warn(warning, colorize) + } + fmt.Println("") + } + fmt.Printf("upgrading from v%s-->%s\n\ndownloading...\n", version, highlight(update.Version)) + } else { + fmt.Println("nvm is up to date") + return nil + } + + // Make temp directory + tmp, err := os.MkdirTemp("", "nvm-upgrade-*") + if err != nil { + return fmt.Errorf("error: failed to create temporary directory: %v\n", err) + } + defer os.RemoveAll(tmp) + + // Download the new app + // TODO: Replace version with update.Version + // source := fmt.Sprintf(update.SourceURL, update.Version) + source := fmt.Sprintf(update.SourceURL, "1.1.11") + body, err := get(source) + if err != nil { + return fmt.Errorf("error: failed to download new version: %v\n", err) + } + + os.WriteFile(filepath.Join(tmp, "assets.zip"), body, os.ModePerm) + os.Mkdir(filepath.Join(tmp, "assets"), os.ModePerm) + + source = source + ".checksum.txt" + body, err = get(source) + if err != nil { + return fmt.Errorf("error: failed to download checksum: %v\n", err) + } + + os.WriteFile(filepath.Join(tmp, "assets.zip.checksum.txt"), body, os.ModePerm) + + filePath := filepath.Join(tmp, "assets.zip") // path to the file you want to validate + checksumFile := filepath.Join(tmp, "assets.zip.checksum.txt") // path to the checksum file + + // Step 1: Compute the MD5 checksum of the file + fmt.Println("verifying checksum...") + computedChecksum, err := computeMD5Checksum(filePath) + if err != nil { + return fmt.Errorf("Error computing checksum: %v", err) + } + + // Step 2: Read the checksum from the .checksum.txt file + storedChecksum, err := readChecksumFromFile(checksumFile) + if err != nil { + return fmt.Errorf("Error readirng checksum from file: %v", err) + } + + // Step 3: Compare the computed checksum with the stored checksum + if strings.ToLower(computedChecksum) != strings.ToLower(storedChecksum) { + return fmt.Errorf("cannot validate update file (checksum mismatch)") + } + + fmt.Println("extracting update...") + if err := unzip(filepath.Join(tmp, "assets.zip"), filepath.Join(tmp, "assets")); err != nil { + return err + } + + // Get any additional assets + if len(update.Assets) > 0 { + fmt.Printf("downloading %d additional assets...\n", len(update.Assets)) + for _, asset := range update.Assets { + var assetURL string + if !strings.HasPrefix(asset, "http") { + assetURL = fmt.Sprintf(update.SourceURL, asset) + } else { + assetURL = asset + } + assetBody, err := get(assetURL) + if err != nil { + return fmt.Errorf("error: failed to download asset: %v\n", err) + } + + assetPath := filepath.Join(tmp, "assets", asset) + os.WriteFile(assetPath, assetBody, os.ModePerm) + } + } + + // Debugging + if verbose { + tree(tmp, "downloaded files (extracted):") + nvmtestcmd := exec.Command(filepath.Join(tmp, "assets", "nvm.exe"), "version") + nvmtestcmd.Stdout = os.Stdout + nvmtestcmd.Stderr = os.Stderr + err = nvmtestcmd.Run() + if err != nil { + fmt.Println("error running nvm.exe:", err) + } + } + + // Backup current version to zip + fmt.Println("applying update...") + currentExe, _ := os.Executable() + currentPath := filepath.Dir(currentExe) + bkp, err := os.MkdirTemp("", "nvm-backup-*") + if err != nil { + return fmt.Errorf("error: failed to create backup directory: %v\n", err) + } + defer os.RemoveAll(bkp) + + err = zipDirectory(currentPath, filepath.Join(bkp, "backup.zip")) + if err != nil { + return fmt.Errorf("error: failed to create backup: %v\n", err) + } + + os.MkdirAll(filepath.Join(currentPath, ".update"), os.ModePerm) + copyFile(filepath.Join(bkp, "backup.zip"), filepath.Join(currentPath, ".update", "nvm4w-backup.zip")) + + // Copy the new files to the current directory + // copyFile(currentExe, fmt.Sprintf("%s.%s.bak", currentExe, version)) + copyDirContents(filepath.Join(tmp, "assets"), currentPath) + copyFile(filepath.Join(tmp, "assets", "nvm.exe"), filepath.Join(currentPath, ".update/nvm.exe")) + + if verbose { + nvmtestcmd := exec.Command(filepath.Join(currentPath, ".update/nvm.exe"), "version") + nvmtestcmd.Stdout = os.Stdout + nvmtestcmd.Stderr = os.Stderr + err = nvmtestcmd.Run() + if err != nil { + fmt.Println("error running nvm.exe:", err) + } + } + + // TODO: schedule removal of .backup folder for 30 days from now + // TODO: warn user that the restore function is available for 30 days + + // Debugging + if verbose { + tree(currentPath, "final directory contents:") + } + + // Hide the update directory + setHidden(filepath.Join(currentPath, ".update")) + + // The upgrade process should be able to roll back if there is a failure. + // TODO: Upgrade the registry data to reflect the new version + // Potentially provide a desktop notification when the upgrade is complete. + + // If an "update.exe" exists, run it + if fsutil.IsExecutable(filepath.Join(tmp, "assets", "update.exe")) { + err = copyFile(filepath.Join(tmp, "assets", "update.exe"), filepath.Join(currentPath, ".update", "update.exe")) + if err != nil { + fmt.Println(fmt.Errorf("error: failed to copy update.exe: %v\n", err)) + os.Exit(1) + } + } + + autoupdate() + + return nil +} + +func Get() (*Update, error) { + return checkForUpdate(UPDATE_URL) +} + +func autoupdate() { + currentPath, err := os.Executable() + if err != nil { + fmt.Println("error getting updater path:", err) + os.Exit(1) + } + + // Create temporary directory for the updater script + tempDir := filepath.Dir(currentPath) // Use the same temp dir as the new executable + scriptPath := filepath.Join(tempDir, "updater.bat") + + // Temporary batch file that deletes the directory and the scheduled task + tmp, err := os.MkdirTemp("", "nvm4w-remove-*") + if err != nil { + fmt.Printf("error creating temporary directory: %v", err) + os.Exit(1) + } + tempBatchFile := filepath.Join(tmp, "remove_backup.bat") + now := time.Now() + futureDate := now.AddDate(0, 0, 7) + formattedDate := futureDate.Format("01/02/2006") + batchContent := fmt.Sprintf(` +@echo off +schtasks /delete /tn "RemoveNVM4WBackup" /f +rmdir /s /q "%s" +`, escapeBackslashes(filepath.Join(filepath.Dir(currentPath), ".update"))) + + // Write the batch file to a temporary location + err = os.WriteFile(tempBatchFile, []byte(batchContent), os.ModePerm) + if err != nil { + fmt.Printf("error creating temporary batch file: %v", err) + os.Exit(1) + } + + updaterScript := fmt.Sprintf(`@echo off +setlocal enabledelayedexpansion + +echo ========= Update Script Started ========= >> error.log +echo Started updater script with PID %%1 at %%TIME%% >> error.log +echo Source: %%~2 >> error.log +echo Target: %%~3 >> error.log + +:wait +timeout /t 1 /nobreak >nul +tasklist /fi "PID eq %%1" 2>nul | find "%%1" >nul +if not errorlevel 1 ( + echo Waiting for PID %%1 to exit at %%TIME%%... >> error.log + goto :wait +) + +echo ========= Starting Copy Operation ========= >> error.log +echo Checking if source (%%~2) exists... >> error.log +if not exist "%%~2" ( + echo ERROR: Source file does not exist: %%~2 >> error.log + exit /b 1 +) +echo Source file exists >> error.log + +del "%%~3" >> error.log + +echo Checking if target location is writable... >> error.log +echo Test > "%%~dp3test.txt" 2>>error.log +if errorlevel 1 ( + echo ERROR: Target location is not writable: %%~dp3 >> error.log + exit /b 1 +) +del "%%~dp3test.txt" +echo Target location is writable >> error.log + +echo Attempting copy at %%TIME%%... >> error.log +echo Running: copy /y "%%~2" "%%~3" >> error.log +copy /y "%%~2" "%%~3" >> error.log 2>&1 +if errorlevel 1 ( + echo ERROR: Copy failed with error level %%errorlevel%% >> error.log + exit /b %%errorlevel%% +) + +echo Verifying copy... >> error.log +if not exist "%%~3" ( + echo ERROR: Target file does not exist after copy: %%~3 >> error.log + exit /b 1 +) + +del "%%~2" >> error.log +if exist "%%~2" ( + echo ERROR: Source file still exists after deletion: %%~2 >> error.log + exit /b 1 +) + +:: Schedule the task to delete the directory +echo schtasks /create /tn "RemoveNVM4WBackup" /tr "cmd.exe /c %s" /sc once /sd %s /st 12:00 /f >> error.log +schtasks /create /tn "RemoveNVM4WBackup" /tr "cmd.exe /c %s" /sc once /sd %s /st 12:00 /f +if not errorlevel 0 ( + echo ERROR: Failed to create scheduled task: exit code: %%errorlevel%% >> error.log + exit /b %%errorlevel%% +) + +echo Update complete >> error.log + +del error.log + +del "%%~f0" +exit /b 0 +`, escapeBackslashes(tempBatchFile), formattedDate, escapeBackslashes(tempBatchFile), formattedDate) + + err = os.WriteFile(scriptPath, []byte(updaterScript), os.ModePerm) // Use standard Windows file permissions + if err != nil { + fmt.Printf("error creating updater script: %v", err) + os.Exit(1) + } + + // Start the updater script + cmd := exec.Command(scriptPath, fmt.Sprintf("%d", os.Getpid()), filepath.Join(tempDir, ".update", "nvm.exe"), currentPath) + err = cmd.Start() + if err != nil { + fmt.Printf("error starting updater script: %v", err) + os.Exit(1) + } + + // Exit the current process (delay for cleanup) + time.Sleep(300 * time.Millisecond) + os.Exit(0) +} + +func escapeBackslashes(path string) string { + return strings.Replace(path, "\\", "\\\\", -1) +} + +func tree(dir string, title ...string) { + if len(title) > 0 { + fmt.Println("\n" + highlight(title[0])) + } + cmd := exec.Command("tree", dir, "/F") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Println("Error executing command:", err) + } +} + +func get(url string, verbose ...bool) ([]byte, error) { + if len(verbose) == 0 || verbose[0] { + fmt.Printf(" GET %s\n", url) + } + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return []byte{}, fmt.Errorf("error: received status code %d\n", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +func checkForUpdate(url string) (*Update, error) { + u := Update{} + + // Make the HTTP GET request + body, err := get(url, false) + if err != nil { + return &u, fmt.Errorf("error: reading response body: %v", err) + } + + // Parse JSON into the struct + err = json.Unmarshal(body, &u) + if err != nil { + return &u, fmt.Errorf("error: parsing update: %v", err) + } + + return &u, nil +} + +func EnableVirtualTerminalProcessing() error { + // Get the handle to the standard output + handle := windows.Stdout + + // Retrieve the current console mode + var mode uint32 + if err := windows.GetConsoleMode(handle, &mode); err != nil { + return err + } + + // Enable the virtual terminal processing mode + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING + if err := windows.SetConsoleMode(handle, mode); err != nil { + return err + } + + return nil +} + +func highlight(message string) string { + return fmt.Sprintf("%s%s%s", yellow, message, reset) +} + +// Unzip function extracts a zip file to a specified directory +func unzip(src string, dest string) error { + // Open the zip archive for reading + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + // Iterate over each file in the zip archive + for _, f := range r.File { + // Build the path for each file in the destination directory + fpath := filepath.Join(dest, f.Name) + + // Check if the file is a directory + if f.FileInfo().IsDir() { + // Create directory if it doesn't exist + if err := os.MkdirAll(fpath, os.ModePerm); err != nil { + return err + } + continue + } + + // Create directories leading to the file if they don't exist + if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return err + } + + // Open the file in the zip archive + rc, err := f.Open() + if err != nil { + return err + } + defer rc.Close() + + // Create the destination file + outFile, err := os.Create(fpath) + if err != nil { + return err + } + defer outFile.Close() + + // Copy the file contents from the archive to the destination file + _, err = io.Copy(outFile, rc) + if err != nil { + return err + } + } + return nil +} + +// function to compute the MD5 checksum of a file +func computeMD5Checksum(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + hasher := md5.New() + _, err = io.Copy(hasher, file) + if err != nil { + return "", err + } + + // Return the hex string representation of the MD5 hash + return fmt.Sprintf("%x", hasher.Sum(nil)), nil +} + +// function to read the checksum from the .checksum.txt file +func readChecksumFromFile(checksumFile string) (string, error) { + file, err := os.Open(checksumFile) + if err != nil { + return "", err + } + defer file.Close() + + var checksum string + _, err = fmt.Fscan(file, &checksum) + if err != nil { + return "", err + } + + return checksum, nil +} + +func copyFile(src, dst string) error { + // Open the source file + sourceFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file: %v", err) + } + defer sourceFile.Close() + + // Create the destination file + destinationFile, err := os.Create(dst) + if err != nil { + return fmt.Errorf("failed to create destination file: %v", err) + } + defer destinationFile.Close() + + // Copy contents from the source file to the destination file + _, err = io.Copy(destinationFile, sourceFile) + if err != nil { + return fmt.Errorf("failed to copy file: %v", err) + } + + // Optionally, copy file permissions (this can be skipped if not needed) + sourceInfo, err := os.Stat(src) + if err != nil { + return fmt.Errorf("failed to get source file info: %v", err) + } + + err = os.Chmod(dst, sourceInfo.Mode()) + if err != nil { + return fmt.Errorf("failed to set file permissions: %v", err) + } + + return nil +} + +// copyDirContents copies all the contents (files and subdirectories) of a source directory to a destination directory. +func copyDirContents(srcDir, dstDir string) error { + // Ensure destination directory exists + err := os.MkdirAll(dstDir, 0755) + if err != nil { + return fmt.Errorf("failed to create destination directory %s: %v", dstDir, err) + } + + // Walk through the source directory recursively + err = filepath.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("error accessing %s: %v", srcPath, err) + } + + // Construct the corresponding path in the destination directory + relPath, err := filepath.Rel(srcDir, srcPath) + if err != nil { + return fmt.Errorf("failed to get relative path for %s: %v", srcPath, err) + } + + dstPath := filepath.Join(dstDir, relPath) + + // If it's a directory, ensure it's created in the destination + if info.IsDir() { + return os.MkdirAll(dstPath, info.Mode()) + } + + // If it's a file, copy it + return copyFile(srcPath, dstPath) + }) + + return err +} + +// zipDirectory zips the contents of a directory. +func zipDirectory(sourceDir, outputZip string) error { + // Create the zip file. + zipFile, err := os.Create(outputZip) + if err != nil { + return err + } + defer zipFile.Close() + + // Create a new zip writer. + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + // Walk through the directory. + return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Get the relative path. + relPath, err := filepath.Rel(sourceDir, path) + if err != nil { + return err + } + + // Skip the directory itself but include subdirectories. + if info.IsDir() { + if relPath == "." { + return nil + } + // Add a trailing slash for directories in the zip archive. + relPath += "/" + } + + // Create a zip header. + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name = relPath + if info.IsDir() { + header.Method = zip.Store + } else { + header.Method = zip.Deflate + } + + // Create a writer for the file in the zip archive. + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + // If the file is not a directory, copy its contents into the archive. + if !info.IsDir() { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(writer, file) + if err != nil { + return err + } + } + + return nil + }) +} + +func setHidden(path string) error { + // Convert the path to a UTF-16 encoded string + lpFileName, err := syscall.UTF16PtrFromString(path) + if err != nil { + return fmt.Errorf("failed to encode path: %w", err) + } + + // Call the Windows API function + ret, _, err := syscall.NewLazyDLL("kernel32.dll"). + NewProc("SetFileAttributesW"). + Call( + uintptr(unsafe.Pointer(lpFileName)), + uintptr(FILE_ATTRIBUTE_HIDDEN), + ) + + // Check the result + if ret == 0 { + return fmt.Errorf("failed to set hidden attribute: %w", err) + } + return nil +}