combined updates

This commit is contained in:
Corey Butler
2024-12-28 22:23:51 -06:00
parent 096e0a20fc
commit 7de073ee16
41 changed files with 2197 additions and 1111 deletions

View File

@@ -7,7 +7,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'This PR is stale because it has been open 45 days with no activity.'
@@ -18,4 +18,4 @@ jobs:
start-date: '2021-09-15T05:00:00Z'
exempt-all-assignees: true
exempt-all-milestones: true
exempt-issue-labels: 'help wanted,consider for rt'
exempt-issue-labels: 'bug,not stale,help wanted,consider for rt'

5
.gitignore vendored
View File

@@ -2,10 +2,12 @@
*.o
*.a
*.so
*.old
# Folders
_obj
_test
bin
# Architecture specific extensions/prefixes
*.[568vq]
@@ -29,7 +31,10 @@ _testmain.go
dist
src/v*
bin/*.exe
bin/*.log
bin/LICENSE
!bin/buildtools/*
!assets/buildtools/*
bin/*.zip
bin/nvm/*

View File

@@ -1,4 +1,4 @@
<div align="center"><h2>Notice: We have started full time work on <a href="https://github.com/coreybutler/nvm-windows/wiki/Runtime">Runtime</a>, the successor to NVM for Windows.</h2>Complete <a href="https://t.co/oGqQCM9FPx">this form</a> to provide your thoughts and sign up for progress updates</div>
<div align="center"><h2>Notice: We are working full time on <a href="https://github.com/coreybutler/nvm-windows/wiki/Runtime">Runtime</a>, the successor to NVM for Windows.</h2>Complete <a href="https://t.co/oGqQCM9FPx">this form</a> to provide your thoughts and sign up for progress updates</div>
<br/><br/>
<h1 align="center">NVM for Windows</h1>
@@ -19,10 +19,12 @@ _The original [nvm](https://github.com/nvm-sh/nvm) is a completely separate proj
<div align="center">
<table cellpadding="5" cellspacing="0" border="0" align="center">
<tr>
<td><a href="https://metadoc.io"><img src="https://github.com/coreybutler/staticassets/raw/master/sponsors/metadoclogobig.png" width="200px"/></a></td>
<td><a href="https://linkedin.com/company/authorsoftware"><img src="https://github.com/coreybutler/staticassets/blob/master/sponsors/logo_author_software_flat.png" width="200px"/></a></td>
<td><a href="https://linkedin.com/company/authorsoftware"><img src="https://avatars.githubusercontent.com/u/8259581?s=200&v=4" width="200px"/></a></td>
<!-- <td><a href="https://metadoc.io"><img src="https://github.com/coreybutler/staticassets/raw/master/sponsors/metadoclogobig.png" width="200px"/></a></td>
<td><a href="https://enabledb.com"><img src="https://github.com/coreybutler/staticassets/raw/master/images/logos/logo_enabledb_w_text.png" width="200px"/></a></td>
<td><a href="https://butlerlogic.com"><img src="https://github.com/coreybutler/staticassets/raw/master/sponsors/butlerlogic_logo.png" width="200px"/></a></td>
<td width="25%" align="center"><a href="https://github.com/microsoft"><img src="https://user-images.githubusercontent.com/770982/195955265-5c3dca78-7140-4ec6-b05a-f308518643ee.png" height="30px"/></a></td>
<td><a href="https://butlerlogic.com"><img src="https://github.com/coreybutler/staticassets/raw/master/sponsors/butlerlogic_logo.png" width="200px"/></a></td> -->
<td width="33%" align="center"><a href="https://github.com/microsoft"><img src="https://user-images.githubusercontent.com/770982/195955265-5c3dca78-7140-4ec6-b05a-f308518643ee.png" height="30px"/></a></td>
</tr>
<tr>
<td colspan="4" align="center">
@@ -80,7 +82,7 @@ If you attempt to configure the `NVM_SYMLINK` to use an existing directory (like
_PATH Conflicts_
If you do not uninstall the original version, running `nvm use` may appear to do nothing at all. Running `node -v` will always show the original installation version. This is due to a [`PATH` conflict](https://github.com/coreybutler/nvm-windows/wiki/Common-Issues#why-do-i-need-to-uninstall-nodejs-before-installing-nvm-for-windows) that presents when the same application is installed multiple times. In NVM4W 1.1.11+, run `nvm debug` to determine if you have a `PATH` conflict.
For simpliciy, we recommend uninstalling any existing versions of Node.js before using NVM for Windows. Delete any existing Node.js installation directories (e.g., `%ProgramFiles%\nodejs`) that might remain. NVM's generated symlink will not overwrite an existing (even empty) installation directory.
For simpliciy, we recommend uninstalling any existing versions of Node.js before using NVM for Windows. Delete any existing Node.js installation directories (e.g., `%ProgramFiles%\nodejs`) that might remain. NVM's generated symlink will not overwrite an existing (even empty) installation directory.
:eyes: **Backup any global `npmrc` config** :eyes:
(e.g. `%AppData%\npm\etc\npmrc`)

BIN
assets/alert.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/author.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/download.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/nvm.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

2
assets/run.cmd Normal file
View File

@@ -0,0 +1,2 @@
@echo off
powershell -NoProfile -WindowStyle Hidden -Command "Start-Process -Filepath 'C:\Users\corey\OneDrive\Documents\workspace\libraries\oss\coreybutler\nvm-windows\bin\nvm.exe' -ArgumentList '\"%*\"' -NoNewWindow"

BIN
assets/success.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

106
build.bat
View File

@@ -1,106 +0,0 @@
@echo off
SET INNOSETUP=%CD%\nvm.iss
SET ORIG=%CD%
REM SET GOPATH=%CD%\src
SET GOBIN=%CD%\bin
REM Support for older architectures
SET GOARCH=386
REM Cleanup existing build if it exists
if exist src\nvm.exe (
del src\nvm.exe
)
REM Make the executable and add to the binary directory
echo ----------------------------
echo Building nvm.exe
echo ----------------------------
cd .\src
go build nvm.go
REM Group the file with the helper binaries
move nvm.exe "%GOBIN%"
cd ..\
REM Codesign the executable
echo ----------------------------
echo Sign the nvm executable...
echo ----------------------------
buildtools\signtool.exe sign /debug /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a "%GOBIN%\nvm.exe"
for /f %%i in ('"%GOBIN%\nvm.exe" version') do set AppVersion=%%i
echo nvm.exe v%AppVersion% built.
REM Create the distribution folder
SET DIST=%CD%\dist\%AppVersion%
REM Remove old build files if they exist.
if exist "%DIST%" (
echo ----------------------------
echo Clearing old build in %DIST%
echo ----------------------------
rd /s /q "%DIST%"
)
REM Create the distribution directory
mkdir "%DIST%"
REM Create the "no install" zip version
for %%a in ("%GOBIN%") do (buildtools\zip -j -9 -r "%DIST%\nvm-noinstall.zip" "%CD%\LICENSE" %%a\* -x "%GOBIN%\nodejs.ico")
REM Generate update utility
echo ----------------------------
echo Generating update utility...
echo ----------------------------
cd .\updater
go build nvm-update.go
move nvm-update.exe "%DIST%"
cd ..\
REM Generate the installer (InnoSetup)
echo ----------------------------
echo Generating installer...
echo ----------------------------
buildtools\iscc "%INNOSETUP%" "/o%DIST%"
echo ----------------------------
echo Sign the installer
echo ----------------------------
buildtools\signtool.exe sign /debug /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a "%DIST%\nvm-setup.exe"
echo ----------------------------
echo Sign the updater...
echo ----------------------------
buildtools\signtool.exe sign /debug /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a "%DIST%\nvm-update.exe"
echo ----------------------------
echo Bundle the installer...
echo ----------------------------
buildtools\zip -j -9 -r "%DIST%\nvm-setup.zip" "%DIST%\nvm-setup.exe"
echo ----------------------------
echo Bundle the updater...
echo ----------------------------
buildtools\zip -j -9 -r "%DIST%\nvm-update.zip" "%DIST%\nvm-update.exe"
del "%DIST%\nvm-update.exe"
del "%DIST%\nvm-setup.exe"
REM Generate checksums
echo ----------------------------
echo Generating checksums...
echo ----------------------------
for %%f in ("%DIST%"\*.*) do (certutil -hashfile "%%f" MD5 | find /i /v "md5" | find /i /v "certutil" >> "%%f.checksum.txt")
echo complete
echo ----------------------------
echo Cleaning up...
echo ----------------------------
del "%GOBIN%\nvm.exe"
echo complete
@REM del %GOBIN%\nvm-update.exe
@REM del %GOBIN%\nvm-setup.exe
echo NVM for Windows v%AppVersion% build completed. Available in %DIST%
@echo on

View File

@@ -1,35 +0,0 @@
[MRU List]
MRU1=C:\Users\corey\OneDrive\Documents\workspace\libraries\oss\coreybutler\nvm-windows\dist\nvm.exe
MRU2=C:\Users\corey\nvm test\nvm.exe
MRU3=
MRU4=
MRU5=
MRU6=
MRU7=
MRU8=
[Setup]
left=4150
top=798
width=1200
height=660
MaximizedState=0
MenuEditMode=0
DisableGridlines=0
vsplit=300
LastDir=C:\Users\corey\OneDrive\Documents\workspace\libraries\oss\coreybutler\nvm-windows\dist
ToolbarSize=3
[MonospaceFont]
Name=Courier New
Size=9
Color=-16777208
Style=0
[Font]
Name=Tahoma
Size=9
Color=-16777208
CharSet=1
Style=0

Binary file not shown.

108
nvm.iss
View File

@@ -1,25 +1,24 @@
#define MyAppName "NVM for Windows"
#define MyAppShortName "nvm"
#define MyAppLCShortName "nvm"
#define MyAppVersion "1.2.0"
#define MyAppVersion "{{VERSION}}"
#define MyAppPublisher "Author Software Inc."
#define MyAppURL "https://github.com/coreybutler/nvm-windows"
#define MyAppExeName "nvm.exe"
#define MyIcon "bin\nodejs.ico"
#define MyIcon "bin\nvm.ico"
#define MyAppId "40078385-F676-4C61-9A9C-F9028599D6D3"
#define ProjectRoot "."
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
PrivilegesRequired=admin
; SignTool=MsSign $f
; SignedUninstaller=yes
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppCopyright=Copyright (C) 2018-2024 Author Software Inc., Ecor Ventures LLC, Corey Butler, and contributors.
AppCopyright=Copyright (C) 2018-{code:GetCurrentYear} Author Software Inc., Ecor Ventures LLC, Corey Butler, and contributors.
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
@@ -37,12 +36,14 @@ Compression=lzma
SolidCompression=yes
ChangesEnvironment=yes
DisableProgramGroupPage=yes
ArchitecturesInstallIn64BitMode=x64 ia64
UninstallDisplayIcon={app}\{#MyIcon}
VersionInfoVersion={#MyAppVersion}
VersionInfoCopyright=Copyright (C) 2018-20234 Author Software Inc., Ecor Ventures LLC, Corey Butler, and contributors.
ArchitecturesInstallIn64BitMode=x64
UninstallDisplayIcon={#ProjectRoot}\{#MyIcon}
; Version information
VersionInfoVersion={{VERSION}}.0
VersionInfoCopyright=Copyright © {code:GetCurrentYear} Author Software Inc., Ecor Ventures LLC, Corey Butler, and contributors.
VersionInfoCompany=Author Software Inc.
VersionInfoDescription=Node version manager for Windows
VersionInfoDescription=Node.js version manager for Windows
VersionInfoProductName={#MyAppShortName}
VersionInfoProductTextVersion={#MyAppVersion}
@@ -61,10 +62,10 @@ Name: "{group}\Uninstall {#MyAppShortName}"; Filename: "{uninstallexe}"
[Registry]
; Register the URL protocol 'nvm'
Root: HKCR; Subkey: "{#MyAppShortName}"; ValueType: string; ValueName: ""; ValueData: "URL:NVM Protocol"; Flags: uninsdeletekey
Root: HKCR; Subkey: "{#MyAppShortName}"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""
Root: HKCR; Subkey: "{#MyAppShortName}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
Root: HKCR; Subkey: "{#MyAppShortName}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
Root: HKCR; Subkey: "{#MyAppShortName}"; ValueType: string; ValueName: ""; ValueData: "URL:nvm"; Flags: uninsdeletekey
Root: HKCR; Subkey: "{#MyAppShortName}"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey
Root: HKCR; Subkey: "{#MyAppShortName}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey
Root: HKCR; Subkey: "{#MyAppShortName}\shell\launch\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey
[Code]
var
@@ -75,6 +76,11 @@ var
EmailLabel: TLabel;
PreText: TLabel;
function GetCurrentYear(Param: String): String;
begin
result := GetDateTimeString('yyyy', '-', ':');
end;
function IsDirEmpty(dir: string): Boolean;
var
FindRec: TFindRec;
@@ -94,7 +100,7 @@ begin
end;
end;
//function getInstalledVErsions(dir: string):
//function getInstalledVersions(dir: string):
var
nodeInUse: string;
@@ -299,6 +305,30 @@ begin
EmailEdit.Text := ''; // Default value
end;
function LastPos(const SubStr, S: string): Integer;
var
I: Integer;
begin
Result := 0;
for I := Length(S) downto 1 do
begin
if Copy(S, I, Length(SubStr)) = SubStr then
begin
Result := I;
Exit;
end;
end;
end;
function IsValidEmail(const Email: string): Boolean;
var
AtPos, DotPos: Integer;
begin
AtPos := Pos('@', Email);
DotPos := LastPos('.', Email);
Result := (AtPos > 1) and (DotPos > AtPos + 1) and (DotPos < Length(Email));
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
Email: string;
@@ -327,10 +357,14 @@ begin
if CurPageID = EmailPage.ID then
begin
Email := Trim(EmailEdit.Text); // Remove leading/trailing spaces
if (Email <> '') and not ((Pos('@', Email) > 1) and (Pos('.', Email) > Pos('@', Email) + 1)) then
if (Email <> '') and not IsValidEmail(Email) then
begin
MsgBox('Please enter a valid email address or leave the field blank.', mbError, MB_OK);
Result := False; // Prevent navigation to the next page
end
else
begin
WizardForm.NextButton.Enabled := True; // Allow navigation to the next page
end;
end;
@@ -444,20 +478,28 @@ begin
RegWriteExpandStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', path);
end;
end;
end;
function GetNotificationParameters: string;
function GetNotificationString(Param: String): String;
begin
Result := '';
Result := ' subscribe';
if NotificationOptionPage.Values[0] then
Result := Result + '--lts ';
begin
Result := Result + ' --lts';
end;
if NotificationOptionPage.Values[1] then
Result := Result + '--current ';
begin
Result := Result + ' --current';
end;
if NotificationOptionPage.Values[2] then
Result := Result + '--nvm4w ';
begin
Result := Result + ' --nvm4w';
end;
if NotificationOptionPage.Values[3] then
Result := Result + '--author ';
// Trim the trailing space
begin
Result := Result + ' --author';
end;
Result := Trim(Result);
end;
@@ -476,12 +518,24 @@ begin
Result := Length(nodeInUse) > 0;
end;
function isEmailSupplied(): boolean;
begin
Result := Trim(EmailEdit.Text) <> '';
end;
function GetEmailRegistrationString(Param: String): string;
begin
Result := ' author newsletter --notify ' + Trim(EmailEdit.Text);
end;
[Run]
Filename: "{app}\nvm.exe"; Parameters: "register {code:GetNotificationParameters}"; Flags: runhidden;
Filename: "{cmd}"; Parameters: "/C ""mklink /D ""{code:getSymLink}"" ""{code:getCurrentVersion}"""" "; Check: isNodeAlreadyInUse; Flags: runhidden;
Filename: "{app}\nvm.exe"; Parameters: "{code:GetNotificationString}"; Flags: waituntilidle runhidden;
Filename: "{app}\nvm.exe"; Parameters: "{code:GetEmailRegistrationString}"; Check: isEmailSupplied; Flags: waituntilidle runhidden;
Filename: "{cmd}"; Parameters: "/C ""mklink /D ""{code:getSymLink}"" ""{code:getCurrentVersion}"""" "; Check: isNodeAlreadyInUse; Flags: waituntilidle runhidden;
Filename: "powershell.exe"; Parameters: "-NoExit -Command Write-Host 'Welcome to NVM for Windows v{{VERSION}}'"; Description: "Open with Powershell"; Flags: postinstall skipifsilent;
[UninstallRun]
Filename: "{app}\nvm.exe"; Parameters: "unregister --lts --current --nvm4w --author"; Flags: runhidden;
Filename: "{app}\nvm.exe"; Parameters: "unsubscribe --lts --current --nvm4w --author"; Flags: runhidden; RunOnceId: "UnregisterNVMForWindows";
[UninstallDelete]
Type: files; Name: "{app}\nvm.exe";
@@ -489,5 +543,5 @@ Type: files; Name: "{app}\elevate.cmd";
Type: files; Name: "{app}\elevate.vbs";
Type: files; Name: "{app}\nodejs.ico";
Type: files; Name: "{app}\settings.txt";
Type: dir; Name: "{app}";
Type: regkey; Name: "HKCR\Software\{#MyAppShortName}";
Type: filesandordirs; Name: "{userappdata}\.nvm";
Type: filesandordirs; Name: "{app}";

104
src/author/bridge.go Normal file
View File

@@ -0,0 +1,104 @@
package author
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/coreybutler/go-fsutil"
"golang.org/x/sys/windows"
)
const (
// Constants for SetWindowPos
SWP_NOMOVE = 0x0002
SWP_NOZORDER = 0x0004
SWP_SHOWWINDOW = 0x0040
SW_HIDE = 0 // Hide the window (for the main console window)
)
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
moduser32 = syscall.NewLazyDLL("user32.dll")
procGetConsole = modkernel32.NewProc("GetConsoleWindow")
procShowWindow = moduser32.NewProc("ShowWindow")
)
// Hide the main console window (the one running the Go app)
func hideConsole() {
hwnd, _, _ := procGetConsole.Call()
if hwnd != 0 {
procShowWindow.Call(hwnd, uintptr(SW_HIDE)) // Hide the main console window
}
}
func Bridge(args ...string) {
exe, _ := os.Executable()
bridge := filepath.Join(filepath.Dir(exe), "author-nvm.exe")
if !fsutil.Exists(bridge) {
fmt.Println("error: author bridge not found")
os.Exit(1)
}
if len(args) < 2 {
fmt.Printf("error: invalid number of arguments passed to author bridge: %d\n", len(args))
os.Exit(1)
}
command := args[0]
args = args[1:]
// fmt.Printf("running author bridge: %s %v\n", command, args)
hideConsole()
cmd := exec.Command(bridge, append([]string{command}, args...)...)
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.CREATE_NEW_PROCESS_GROUP | windows.DETACHED_PROCESS | windows.CREATE_NO_WINDOW,
}
// Create pipes for Stdout and Stderr
stdoutPipe, _ := cmd.StdoutPipe()
stderrPipe, _ := cmd.StderrPipe()
// Start the command
if err := cmd.Start(); err != nil {
fmt.Println("error starting bridge command:", err)
os.Exit(1)
}
// Stream Stdout
go func() {
scanner := bufio.NewScanner(stdoutPipe)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()
// Stream Stderr
go func() {
scanner := bufio.NewScanner(stderrPipe)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()
if command == "upgrade" {
for _, arg := range args {
if strings.Contains(arg, "--rollback") {
fmt.Println("exiting to rollback nvm.exe...")
time.Sleep(1 * time.Second)
os.Exit(0)
}
}
}
// Wait for the command to finish
if err := cmd.Wait(); err != nil {
fmt.Println("bridge command finished with error:", err)
}
}

View File

@@ -6,14 +6,20 @@ 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/dustin/go-humanize v1.0.1
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/ncruces/zenity v0.10.14
github.com/olekukonko/tablewriter v0.0.5
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
golang.org/x/sys v0.1.0
golang.org/x/sys v0.25.0
)
require (
aead.dev/minisign v0.2.0 // indirect
github.com/akavel/rsrc v0.10.2 // indirect
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
github.com/josephspurrier/goversioninfo v1.4.1 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect
golang.org/x/image v0.20.0 // indirect
)

View File

@@ -1,46 +1,59 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
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=
github.com/coreybutler/go-fsutil v1.2.0/go.mod h1:B+6ufEkkRZgFwyR2sHEVG9dMzVBU3GbyGyYmCq7YkEk=
github.com/coreybutler/go-where v1.0.2 h1:Omit67KeTtKpvSJjezVxnVD4qMtvlXDlItiKpVCdcl4=
github.com/coreybutler/go-where v1.0.2/go.mod h1:IqV4saJiDXdNJURfTfVRywDHvY1IG5F+GXb2kmnmEe8=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f h1:OGqDDftRTwrvUoL6pOG7rYTmWsTCvyEWFsMjg+HcOaA=
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/josephspurrier/goversioninfo v1.4.1 h1:5LvrkP+n0tg91J9yTkoVnt/QgNnrI1t4uSsWjIonrqY=
github.com/josephspurrier/goversioninfo v1.4.1/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
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/ncruces/zenity v0.10.14 h1:OBFl7qfXcvsdo1NUEGxTlZvAakgWMqz9nG38TuiaGLI=
github.com/ncruces/zenity v0.10.14/go.mod h1:ZBW7uVe/Di3IcRYH0Br8X59pi+O6EPnNIOU66YHpOO4=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc=
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
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/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
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/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

35
src/manifest.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "nvm",
"version": "1.1.12",
"description": "Node.js version manager for Windows",
"license": "SEE LICENSE IN LICENSE",
"author": "coreybutler",
"build": "nvm.go",
"output": "../bin",
"update": true,
"upx": false,
"tiny": false,
"variables": {
"main.NvmVersion": "manifest.version",
"nvm/web.nvmversion": "manifest.version"
},
"minify": true,
"default_profiles": [
"dev"
],
"profile": {
"release": {
"upx": false,
"variables": {
"main.debug": "false",
"main.mode": "prod"
}
},
"dev": {
"variables": {
"main.debug": "true",
"main.mode": "dev"
}
}
}
}

View File

@@ -34,6 +34,8 @@ func GetCurrentVersion() (string, string) {
if err == nil {
if string(str) == "x64" {
bit = "64"
} else if string(str) == "arm64" {
bit = "arm64"
} else {
bit = "32"
}

1083
src/nvm.go

File diff suppressed because it is too large Load Diff

View File

@@ -5,78 +5,78 @@
package semver
import (
"errors"
"fmt"
"strconv"
"strings"
"errors"
"fmt"
"strconv"
"strings"
)
const (
numbers string = "0123456789"
alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
alphanum = alphas + numbers
dot = "."
hyphen = "-"
plus = "+"
numbers string = "0123456789"
alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
alphanum = alphas + numbers
dot = "."
hyphen = "-"
plus = "+"
)
// Latest fully supported spec version
var SPEC_VERSION = Version{
Major: 2,
Minor: 0,
Patch: 0,
Major: 2,
Minor: 0,
Patch: 0,
}
type Version struct {
Major uint64
Minor uint64
Patch uint64
Pre []*PRVersion
Build []string //No Precedence
Major uint64
Minor uint64
Patch uint64
Pre []*PRVersion
Build []string //No Precedence
}
// Version to string
func (v *Version) String() string {
versionArray := []string{
strconv.FormatUint(v.Major, 10),
dot,
strconv.FormatUint(v.Minor, 10),
dot,
strconv.FormatUint(v.Patch, 10),
}
if len(v.Pre) > 0 {
versionArray = append(versionArray, hyphen)
for i, pre := range v.Pre {
if i > 0 {
versionArray = append(versionArray, dot)
}
versionArray = append(versionArray, pre.String())
}
}
if len(v.Build) > 0 {
versionArray = append(versionArray, plus, strings.Join(v.Build, dot))
}
return strings.Join(versionArray, "")
versionArray := []string{
strconv.FormatUint(v.Major, 10),
dot,
strconv.FormatUint(v.Minor, 10),
dot,
strconv.FormatUint(v.Patch, 10),
}
if len(v.Pre) > 0 {
versionArray = append(versionArray, hyphen)
for i, pre := range v.Pre {
if i > 0 {
versionArray = append(versionArray, dot)
}
versionArray = append(versionArray, pre.String())
}
}
if len(v.Build) > 0 {
versionArray = append(versionArray, plus, strings.Join(v.Build, dot))
}
return strings.Join(versionArray, "")
}
// Checks if v is greater than o.
func (v *Version) GT(o *Version) bool {
return (v.Compare(o) == 1)
return (v.Compare(o) == 1)
}
// Checks if v is greater than or equal to o.
func (v *Version) GTE(o *Version) bool {
return (v.Compare(o) >= 0)
return (v.Compare(o) >= 0)
}
// Checks if v is less than o.
func (v *Version) LT(o *Version) bool {
return (v.Compare(o) == -1)
return (v.Compare(o) == -1)
}
// Checks if v is less than or equal to o.
func (v *Version) LTE(o *Version) bool {
return (v.Compare(o) <= 0)
return (v.Compare(o) <= 0)
}
// Compares Versions v to o:
@@ -84,241 +84,242 @@ func (v *Version) LTE(o *Version) bool {
// 0 == v is equal to o
// 1 == v is greater than o
func (v *Version) Compare(o *Version) int {
if v.Major != o.Major {
if v.Major > o.Major {
return 1
} else {
return -1
}
}
if v.Minor != o.Minor {
if v.Minor > o.Minor {
return 1
} else {
return -1
}
}
if v.Patch != o.Patch {
if v.Patch > o.Patch {
return 1
} else {
return -1
}
}
if v.Major != o.Major {
if v.Major > o.Major {
return 1
} else {
return -1
}
}
if v.Minor != o.Minor {
if v.Minor > o.Minor {
return 1
} else {
return -1
}
}
if v.Patch != o.Patch {
if v.Patch > o.Patch {
return 1
} else {
return -1
}
}
// Quick comparison if a version has no prerelease versions
if len(v.Pre) == 0 && len(o.Pre) == 0 {
return 0
} else if len(v.Pre) == 0 && len(o.Pre) > 0 {
return 1
} else if len(v.Pre) > 0 && len(o.Pre) == 0 {
return -1
} else {
// Quick comparison if a version has no prerelease versions
if len(v.Pre) == 0 && len(o.Pre) == 0 {
return 0
} else if len(v.Pre) == 0 && len(o.Pre) > 0 {
return 1
} else if len(v.Pre) > 0 && len(o.Pre) == 0 {
return -1
} else {
i := 0
for ; i < len(v.Pre) && i < len(o.Pre); i++ {
if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 {
continue
} else if comp == 1 {
return 1
} else {
return -1
}
}
i := 0
for ; i < len(v.Pre) && i < len(o.Pre); i++ {
if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 {
continue
} else if comp == 1 {
return 1
} else {
return -1
}
}
// If all pr versions are the equal but one has further pr version, this one greater
if i == len(v.Pre) && i == len(o.Pre) {
return 0
} else if i == len(v.Pre) && i < len(o.Pre) {
return -1
} else {
return 1
}
// If all pr versions are the equal but one has further pr version, this one greater
if i == len(v.Pre) && i == len(o.Pre) {
return 0
} else if i == len(v.Pre) && i < len(o.Pre) {
return -1
} else {
return 1
}
}
}
}
// Validates v and returns error in case
func (v *Version) Validate() error {
// Major, Minor, Patch already validated using uint64
// Major, Minor, Patch already validated using uint64
if len(v.Pre) > 0 {
for _, pre := range v.Pre {
if !pre.IsNum { //Numeric prerelease versions already uint64
if len(pre.VersionStr) == 0 {
return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr)
}
if !containsOnly(pre.VersionStr, alphanum) {
return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr)
}
}
}
}
if len(v.Pre) > 0 {
for _, pre := range v.Pre {
if !pre.IsNum { //Numeric prerelease versions already uint64
if len(pre.VersionStr) == 0 {
return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr)
}
if !containsOnly(pre.VersionStr, alphanum) {
return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr)
}
}
}
}
if len(v.Build) > 0 {
for _, build := range v.Build {
if len(build) == 0 {
return fmt.Errorf("Build meta data can not be empty %q", build)
}
if !containsOnly(build, alphanum) {
return fmt.Errorf("Invalid character(s) found in build meta data %q", build)
}
}
}
if len(v.Build) > 0 {
for _, build := range v.Build {
if len(build) == 0 {
return fmt.Errorf("Build meta data can not be empty %q", build)
}
if !containsOnly(build, alphanum) {
return fmt.Errorf("Invalid character(s) found in build meta data %q", build)
}
}
}
return nil
return nil
}
// Alias for Parse, parses version string and returns a validated Version or error
func New(s string) (*Version, error) {
return Parse(s)
return Parse(s)
}
// Parses version string and returns a validated Version or error
func Parse(s string) (*Version, error) {
if len(s) == 0 {
return nil, errors.New("Version string empty")
}
s = strings.Replace(s, "v", "", 1)
if len(s) == 0 {
return nil, errors.New("Version string empty")
}
// Split into major.minor.(patch+pr+meta)
parts := strings.SplitN(s, ".", 3)
if len(parts) != 3 {
return nil, errors.New("No Major.Minor.Patch elements found")
}
// Split into major.minor.(patch+pr+meta)
parts := strings.SplitN(s, ".", 3)
if len(parts) != 3 {
return nil, errors.New("No Major.Minor.Patch elements found")
}
// Major
if !containsOnly(parts[0], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0])
}
if hasLeadingZeroes(parts[0]) {
return nil, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0])
}
major, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return nil, err
}
// Major
if !containsOnly(parts[0], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0])
}
if hasLeadingZeroes(parts[0]) {
return nil, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0])
}
major, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return nil, err
}
// Minor
if !containsOnly(parts[1], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1])
}
if hasLeadingZeroes(parts[1]) {
return nil, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1])
}
minor, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
return nil, err
}
// Minor
if !containsOnly(parts[1], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1])
}
if hasLeadingZeroes(parts[1]) {
return nil, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1])
}
minor, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
return nil, err
}
preIndex := strings.Index(parts[2], "-")
buildIndex := strings.Index(parts[2], "+")
preIndex := strings.Index(parts[2], "-")
buildIndex := strings.Index(parts[2], "+")
// Determine last index of patch version (first of pre or build versions)
var subVersionIndex int
if preIndex != -1 && buildIndex == -1 {
subVersionIndex = preIndex
} else if preIndex == -1 && buildIndex != -1 {
subVersionIndex = buildIndex
} else if preIndex == -1 && buildIndex == -1 {
subVersionIndex = len(parts[2])
} else {
// if there is no actual pr version but a hyphen inside the build meta data
if buildIndex < preIndex {
subVersionIndex = buildIndex
preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions
} else {
subVersionIndex = preIndex
}
}
// Determine last index of patch version (first of pre or build versions)
var subVersionIndex int
if preIndex != -1 && buildIndex == -1 {
subVersionIndex = preIndex
} else if preIndex == -1 && buildIndex != -1 {
subVersionIndex = buildIndex
} else if preIndex == -1 && buildIndex == -1 {
subVersionIndex = len(parts[2])
} else {
// if there is no actual pr version but a hyphen inside the build meta data
if buildIndex < preIndex {
subVersionIndex = buildIndex
preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions
} else {
subVersionIndex = preIndex
}
}
if !containsOnly(parts[2][:subVersionIndex], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex])
}
if hasLeadingZeroes(parts[2][:subVersionIndex]) {
return nil, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex])
}
patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64)
if err != nil {
return nil, err
}
v := &Version{}
v.Major = major
v.Minor = minor
v.Patch = patch
if !containsOnly(parts[2][:subVersionIndex], numbers) {
return nil, fmt.Errorf("Invalid character(s) found in patch number %q", parts[2][:subVersionIndex])
}
if hasLeadingZeroes(parts[2][:subVersionIndex]) {
return nil, fmt.Errorf("Patch number must not contain leading zeroes %q", parts[2][:subVersionIndex])
}
patch, err := strconv.ParseUint(parts[2][:subVersionIndex], 10, 64)
if err != nil {
return nil, err
}
v := &Version{}
v.Major = major
v.Minor = minor
v.Patch = patch
// There are PreRelease versions
if preIndex != -1 {
var preRels string
if buildIndex != -1 {
preRels = parts[2][subVersionIndex+1 : buildIndex]
} else {
preRels = parts[2][subVersionIndex+1:]
}
prparts := strings.Split(preRels, ".")
for _, prstr := range prparts {
parsedPR, err := NewPRVersion(prstr)
if err != nil {
return nil, err
}
v.Pre = append(v.Pre, parsedPR)
}
}
// There are PreRelease versions
if preIndex != -1 {
var preRels string
if buildIndex != -1 {
preRels = parts[2][subVersionIndex+1 : buildIndex]
} else {
preRels = parts[2][subVersionIndex+1:]
}
prparts := strings.Split(preRels, ".")
for _, prstr := range prparts {
parsedPR, err := NewPRVersion(prstr)
if err != nil {
return nil, err
}
v.Pre = append(v.Pre, parsedPR)
}
}
// There is build meta data
if buildIndex != -1 {
buildStr := parts[2][buildIndex+1:]
buildParts := strings.Split(buildStr, ".")
for _, str := range buildParts {
if len(str) == 0 {
return nil, errors.New("Build meta data is empty")
}
if !containsOnly(str, alphanum) {
return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str)
}
v.Build = append(v.Build, str)
}
}
// There is build meta data
if buildIndex != -1 {
buildStr := parts[2][buildIndex+1:]
buildParts := strings.Split(buildStr, ".")
for _, str := range buildParts {
if len(str) == 0 {
return nil, errors.New("Build meta data is empty")
}
if !containsOnly(str, alphanum) {
return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str)
}
v.Build = append(v.Build, str)
}
}
return v, nil
return v, nil
}
// PreRelease Version
type PRVersion struct {
VersionStr string
VersionNum uint64
IsNum bool
VersionStr string
VersionNum uint64
IsNum bool
}
// Creates a new valid prerelease version
func NewPRVersion(s string) (*PRVersion, error) {
if len(s) == 0 {
return nil, errors.New("Prerelease is empty")
}
v := &PRVersion{}
if containsOnly(s, numbers) {
if hasLeadingZeroes(s) {
return nil, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s)
}
num, err := strconv.ParseUint(s, 10, 64)
if len(s) == 0 {
return nil, errors.New("Prerelease is empty")
}
v := &PRVersion{}
if containsOnly(s, numbers) {
if hasLeadingZeroes(s) {
return nil, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s)
}
num, err := strconv.ParseUint(s, 10, 64)
// Might never be hit, but just in case
if err != nil {
return nil, err
}
v.VersionNum = num
v.IsNum = true
} else if containsOnly(s, alphanum) {
v.VersionStr = s
v.IsNum = false
} else {
return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s)
}
return v, nil
// Might never be hit, but just in case
if err != nil {
return nil, err
}
v.VersionNum = num
v.IsNum = true
} else if containsOnly(s, alphanum) {
v.VersionStr = s
v.IsNum = false
} else {
return nil, fmt.Errorf("Invalid character(s) found in prerelease %q", s)
}
return v, nil
}
// Is pre release version numeric?
func (v *PRVersion) IsNumeric() bool {
return v.IsNum
return v.IsNum
}
// Compares PreRelease Versions v to o:
@@ -326,54 +327,54 @@ func (v *PRVersion) IsNumeric() bool {
// 0 == v is equal to o
// 1 == v is greater than o
func (v *PRVersion) Compare(o *PRVersion) int {
if v.IsNum && !o.IsNum {
return -1
} else if !v.IsNum && o.IsNum {
return 1
} else if v.IsNum && o.IsNum {
if v.VersionNum == o.VersionNum {
return 0
} else if v.VersionNum > o.VersionNum {
return 1
} else {
return -1
}
} else { // both are Alphas
if v.VersionStr == o.VersionStr {
return 0
} else if v.VersionStr > o.VersionStr {
return 1
} else {
return -1
}
}
if v.IsNum && !o.IsNum {
return -1
} else if !v.IsNum && o.IsNum {
return 1
} else if v.IsNum && o.IsNum {
if v.VersionNum == o.VersionNum {
return 0
} else if v.VersionNum > o.VersionNum {
return 1
} else {
return -1
}
} else { // both are Alphas
if v.VersionStr == o.VersionStr {
return 0
} else if v.VersionStr > o.VersionStr {
return 1
} else {
return -1
}
}
}
// PreRelease version to string
func (v *PRVersion) String() string {
if v.IsNum {
return strconv.FormatUint(v.VersionNum, 10)
}
return v.VersionStr
if v.IsNum {
return strconv.FormatUint(v.VersionNum, 10)
}
return v.VersionStr
}
func containsOnly(s string, set string) bool {
return strings.IndexFunc(s, func(r rune) bool {
return !strings.ContainsRune(set, r)
}) == -1
return strings.IndexFunc(s, func(r rune) bool {
return !strings.ContainsRune(set, r)
}) == -1
}
func hasLeadingZeroes(s string) bool {
return len(s) > 1 && s[0] == '0'
return len(s) > 1 && s[0] == '0'
}
// Creates a new valid build version
func NewBuildVersion(s string) (string, error) {
if len(s) == 0 {
return "", errors.New("Buildversion is empty")
}
if !containsOnly(s, alphanum) {
return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s)
}
return s, nil
if len(s) == 0 {
return "", errors.New("Buildversion is empty")
}
if !containsOnly(s, alphanum) {
return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s)
}
return s, nil
}

198
src/upgrade/check.go Normal file
View File

@@ -0,0 +1,198 @@
package upgrade
import (
"encoding/json"
"fmt"
"nvm/node"
"nvm/semver"
"nvm/web"
"os"
"path/filepath"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/go-toast/toast"
)
func Check(root string, nvmversion string) {
// Store the recognized version to prevent duplicates
notices := LoadNotices()
defer notices.Save()
reg := LoadRegistration(os.Args[2:]...)
// Check for Node.js updates
if reg.LTS || reg.Current {
buf, err := get(web.GetFullNodeUrl("index.json"))
abortOnError(err)
var data = make([]map[string]interface{}, 0)
abortOnError(json.Unmarshal([]byte(buf), &data))
lastLTS := notices.LastLTS()
lastCurrent := notices.LastCurrent()
installed := node.GetInstalled(root)
changes := 0
for _, value := range data {
switch value["lts"].(type) {
// LTS versions always have the lts attribute as a string
case string:
if reg.LTS {
versionDate, _ := time.Parse("2006-01-02", value["date"].(string))
if versionDate.After(lastLTS) && versionDate.After(time.Now().AddDate(0, -1, 0)) && !in(strings.Replace(value["version"].(string), "v", "", 1), installed) {
abortOnError(alertRelease(value))
changes++
}
}
// Current versions always have the lts attribute as a false boolean
case bool:
if reg.Current && !value["lts"].(bool) {
versionDate, _ := time.Parse("2006-01-02", value["date"].(string))
if versionDate.After(lastCurrent) && versionDate.After(time.Now().AddDate(0, -1, 0)) && !in(strings.Replace(value["version"].(string), "v", "", 1), installed) {
abortOnError(alertRelease(value))
changes++
}
}
}
}
if changes == 0 {
noupdate()
return
}
}
// Check for NVM for Windows updates
if reg.NVM4W {
buf, err := get("https://api.github.com/repos/coreybutler/nvm-windows/releases/latest")
abortOnError(err)
var data map[string]interface{}
abortOnError(json.Unmarshal([]byte(buf), &data))
current, err := semver.New(nvmversion)
abortOnError(err)
next, err := semver.New(data["tag_name"].(string))
abortOnError(err)
notices.NVM4W = current.String()
if !next.GT(current) {
alertNvmRelease(current, next, data)
notices.NVM4W = next.String()
}
}
now := time.Now().Format("2006-01-02")
if reg.LTS {
notices.LTS = now
}
if reg.Current {
notices.Current = now
}
}
func alertNvmRelease(current, next *semver.Version, data map[string]interface{}) {
exe, _ := os.Executable()
path := filepath.Dir(exe)
iconPath := filepath.Join(path, "nodejs.ico")
pubDate, _ := time.Parse("2006-01-02T15:04:05Z", data["published_at"].(string))
age := humanize.Time(pubDate)
notification := toast.Notification{
AppID: "NVM for Windows",
Title: "NVM for Windows Update Available",
Message: fmt.Sprintf("Version %s is was released %s.\n(currently using v%s)", next.String(), age, current.String()),
Icon: iconPath,
Actions: []toast.Action{
{"protocol", "Install", "nvm://launch?action=upgrade"},
{"protocol", "View", data["html_url"].(string)},
},
}
// Display the notification
err := notification.Push()
if err != nil {
abortOnError(err)
}
}
func in(item string, set []string) bool {
for _, i := range set {
if i == item {
return true
}
}
return false
}
func noupdate() {
fmt.Println("no new releases detected")
}
func UpgradeCompleteAlert(version string) {
exe, _ := os.Executable()
path := filepath.Dir(exe)
iconPath := filepath.Join(path, "checkmark.ico")
notification := toast.Notification{
AppID: "NVM for Windows",
Title: "Upgrade Complete",
Message: fmt.Sprintf("The upgrade to NVM for Windows v%s completed successfully.", version),
Icon: iconPath,
Actions: []toast.Action{
{"protocol", "Open PowerShell", "nvm://launch?action=open_terminal&amp;type=pwsh"},
{"protocol", "Open CMD Prompt", "nvm://launch?action=open_terminal&amp;type=cmd"},
},
}
// Display the notification
err := notification.Push()
if err != nil {
abortOnError(err)
}
}
func alertRelease(data map[string]interface{}) error {
version, err := semver.New(data["version"].(string))
if err != nil {
return err
}
exe, _ := os.Executable()
path := filepath.Dir(exe)
iconPath := filepath.Join(path, "nodejs.ico")
releaseName := ""
releaseDate, err := time.Parse("2006-01-02", data["date"].(string))
if err != nil {
return err
}
age := humanize.Time(releaseDate)
msg := fmt.Sprintf("with npm v%s & V8 v%s\nReleased %s.", data["npm"].(string), data["v8"].(string), age)
switch data["lts"].(type) {
case string:
releaseName = " (LTS " + data["lts"].(string) + ")"
}
if data["security"].(bool) {
msg += "\nThis is a security release."
}
title := fmt.Sprintf("Node.js v%s Available%s", version.String(), releaseName)
notification := toast.Notification{
AppID: "NVM for Windows",
Title: title,
Message: msg,
Icon: iconPath,
}
// Display the notification
err = notification.Push()
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,69 @@
package upgrade
import (
"encoding/json"
"os"
"path/filepath"
"time"
)
type LastNotification struct {
outpath string
LTS string `json:"lts,omitempty"`
Current string `json:"current,omitempty"`
NVM4W string `json:"nvm4w,omitempty"`
Author string `json:"author,omitempty"`
}
func LoadNotices() *LastNotification {
ln := &LastNotification{}
noticedata, err := os.ReadFile(ln.File())
if err != nil {
if !os.IsNotExist(err) {
abortOnError(err)
}
}
if noticedata != nil {
abortOnError(json.Unmarshal(noticedata, &ln))
}
return ln
}
func (ln *LastNotification) Path() string {
if ln.outpath == "" {
ln.outpath = filepath.Join(os.Getenv("APPDATA"), ".nvm")
}
return ln.outpath
}
func (ln *LastNotification) File() string {
return filepath.Join(ln.Path(), ".updates.json")
}
func (ln *LastNotification) Save() {
output, err := json.Marshal(ln)
abortOnError(err)
abortOnError(os.MkdirAll(ln.Path(), os.ModePerm))
abortOnError(os.WriteFile(ln.File(), output, os.ModePerm))
abortOnError(setHidden(ln.Path()))
}
func (ln *LastNotification) LastLTS() time.Time {
if ln.LTS == "" {
return time.Now()
}
t, _ := time.Parse("2006-01-02", ln.LTS)
return t
}
func (ln *LastNotification) LastCurrent() time.Time {
if ln.Current == "" {
return time.Now()
}
t, _ := time.Parse("2006-01-02", ln.Current)
return t
}

211
src/upgrade/register.go Normal file
View File

@@ -0,0 +1,211 @@
package upgrade
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
const (
NODE_LTS_SCHEDULE_NAME = "NVM for Windows Node.js LTS Update Check"
NODE_CURRENT_SCHEDULE_NAME = "NVM for Windows Node.js Current Update Check"
NVM4W_SCHEDULE_NAME = "NVM for Windows Update Check"
AUTHOR_SCHEDULE_NAME = "NVM for Windows Author Update Check"
)
type Registration struct {
LTS bool
Current bool
NVM4W bool
Author bool
}
func LoadRegistration(args ...string) *Registration {
reg := &Registration{
LTS: false,
Current: false,
NVM4W: false,
Author: false,
}
for _, arg := range args {
arg = strings.ToLower(strings.ReplaceAll(arg, "--", ""))
switch arg {
case "lts":
reg.LTS = true
case "current":
reg.Current = true
case "nvm4w":
reg.NVM4W = true
case "author":
reg.Author = true
}
}
return reg
}
func abortOnError(err error) {
if err != nil {
fmt.Println(err)
os.WriteFile("./error.log", []byte(err.Error()), os.ModePerm)
os.Exit(1)
}
}
func logError(err error) {
fmt.Println(err)
if err != nil {
os.WriteFile("./error.log", []byte(err.Error()), os.ModePerm)
}
}
func Register() {
reg := LoadRegistration(os.Args[2:]...)
exe, _ := os.Executable()
if reg.LTS {
abortOnError(ScheduleTask(NODE_LTS_SCHEDULE_NAME, fmt.Sprintf(`"%s" checkForUpdates lts`, exe), "HOURLY", "00:30"))
}
if reg.Current {
abortOnError(ScheduleTask(NODE_CURRENT_SCHEDULE_NAME, fmt.Sprintf(`"%s" checkForUpdates current`, exe), "HOURLY", "00:25"))
}
if reg.NVM4W {
abortOnError(ScheduleTask(NVM4W_SCHEDULE_NAME, fmt.Sprintf(`"%s" checkForUpdates nvm4w`, exe), "HOURLY", "00:15"))
}
if reg.Author {
abortOnError(ScheduleTask(AUTHOR_SCHEDULE_NAME, fmt.Sprintf(`"%s" checkForUpdates author`, exe), "HOURLY", "00:45"))
}
}
func Unregister() {
reg := LoadRegistration(os.Args[2:]...)
if reg.LTS {
abortOnError(UnscheduleTask(NODE_LTS_SCHEDULE_NAME))
}
if reg.Current {
abortOnError(UnscheduleTask(NODE_CURRENT_SCHEDULE_NAME))
}
if reg.NVM4W {
abortOnError(UnscheduleTask(NVM4W_SCHEDULE_NAME))
}
if reg.Author {
abortOnError(UnscheduleTask(AUTHOR_SCHEDULE_NAME))
}
}
// interval can be:
// MINUTE Runs the task every N minutes. Requires /MO (modifier) to specify the interval.
// HOURLY Runs the task every N hours. Requires /MO to specify the interval.
// DAILY Runs the task every N days. Requires /MO to specify the interval.
// WEEKLY Runs the task every N weeks. Requires /MO and /D (days of the week) to specify the schedule.
// MONTHLY Runs the task on specific days of the month. Requires /MO, /D, or /M for further specifics.
// ONCE Runs the task only once at the specified time. Requires /ST (start time).
// ONSTART Runs the task every time the system starts.
// ONLOGON Runs the task every time the user logs on.
// ONIDLE Runs the task when the system is idle for a specified amount of time.
// EVENT Runs the task when a specific event is triggered. Requires /EC (event log) and /ID (event ID).
func ScheduleTask(name string, command string, interval string, startTime ...string) error {
switch strings.ToUpper(interval) {
case "MINUTE":
fallthrough
case "HOURLY":
fallthrough
case "DAILY":
fallthrough
case "WEEKLY":
fallthrough
case "MONTHLY":
fallthrough
case "ONCE":
fallthrough
case "ONSTART":
fallthrough
case "ONLOGON":
fallthrough
case "ONIDLE":
fallthrough
case "EVENT":
interval = strings.ToUpper(interval)
default:
return fmt.Errorf("scheduling error: invalid interval %q", interval)
}
start := "00:00"
if len(startTime) > 0 {
start = startTime[0]
}
tmp, err := os.MkdirTemp("", "nvm4w-regitration-*")
if err != nil {
return fmt.Errorf("scheduling error: %v", err)
}
defer os.RemoveAll(tmp)
script := fmt.Sprintf(`
@echo off
set errorlog="error.log"
set output="%s\output.log"
schtasks /create /tn "%s" /tr "cmd.exe /c %s" /sc %s /st %s /F > %%output%% 2>&1
if not errorlevel 0 (
echo ERROR: Failed to create scheduled task: exit code: %%errorlevel%% >> %%errorlog%%
type %%output%% >> %%errorlog%%
exit /b %%errorlevel%%
)
`, tmp, name, escapeBackslashes(command), strings.ToLower(interval), start)
err = os.WriteFile(filepath.Join(tmp, "schedule.bat"), []byte(script), os.ModePerm)
if err != nil {
return fmt.Errorf("scheduling error: %v", err)
}
cmd := exec.Command(filepath.Join(tmp, "schedule.bat"))
// Capture standard output and standard error
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("scheduling error: %v\n%s", err, out)
}
// fmt.Sprintf(`"%s" task scheduled successfully!`, name)
return nil
}
func UnscheduleTask(name string) error {
tmp, err := os.MkdirTemp("", "nvm4w-registration-*")
if err != nil {
return fmt.Errorf("scheduling error: %v", err)
}
defer os.RemoveAll(tmp)
script := fmt.Sprintf(`
@echo off
set errorlog="error.log"
set output="%s\output.log"
schtasks /delete /tn "%s" /f > %%output%% 2>&1
if not errorlevel 0 (
echo failed to remove scheduled task: exit code: %%errorlevel%% >> %%errorlog%%
type %%output%% >> %%errorlog%%
exit /b %%errorlevel%%
)
`, tmp, name)
err = os.WriteFile(filepath.Join(tmp, "unschedule.bat"), []byte(script), os.ModePerm)
if err != nil {
return fmt.Errorf("unscheduling error: %v", err)
}
cmd := exec.Command(filepath.Join(tmp, "unschedule.bat"))
// Capture standard output and standard error
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("unscheduling error: %v\n%s", err, out)
}
return nil
}

View File

@@ -7,16 +7,20 @@ import (
"fmt"
"io"
"net/http"
"nvm/author"
"nvm/semver"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"github.com/coreybutler/go-fsutil"
"github.com/ncruces/zenity"
"golang.org/x/sys/windows"
)
@@ -36,6 +40,28 @@ const (
// exclamationIcon = "❗"
)
type Notification struct {
AppID string `json:"app_id"`
Title string `json:"title"`
Message string `json:"message"`
Icon string `json:"icon"`
Actions []Action `json:"actions"`
Duration string `json:"duration"`
Link string `json:"link"`
}
type Action struct {
Type string `json:"type"`
Label string `json:"label"`
URI string `json:"uri"`
}
func display(data Notification) {
data.AppID = "NVM for Windows"
content, _ := json.Marshal(data)
go author.Bridge("notify", string(content))
}
type Update struct {
Version string `json:"version"`
Assets []string `json:"assets"`
@@ -44,6 +70,442 @@ type Update struct {
SourceURL string `json:"sourceTpl"`
}
func Run(version string) error {
show_progress := false
for _, arg := range os.Args[2:] {
if strings.ToLower(arg) == "--show-progress-ui" {
show_progress = true
break
}
}
status := make(chan Status)
if !show_progress {
// Setup signal handling first
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signalChan)
// Add signal handler
go func() {
<-signalChan
fmt.Println("Installation canceled by user")
os.Exit(0)
}()
return run(version, status)
}
wg := &sync.WaitGroup{}
wg.Add(1)
var dlg zenity.ProgressDialog
var exitCode = 0
var u *Update
// Display visual progress UI
go func() {
defer wg.Done()
for {
select {
case s := <-status:
if s.Cancel {
display(Notification{
Title: "Installation Canceled",
Message: fmt.Sprintf("Installation of NVM for Windows v%s was canceled by the user.", u.Version),
Icon: "error",
Actions: []Action{
{Type: "protocol", Label: "Install Again", URI: fmt.Sprintf("nvm://launch?action=upgrade&version=%s", u.Version)},
},
})
return
}
if s.Err != nil {
exitCode = 1
display(Notification{
Title: "Installation Error",
Message: s.Err.Error(),
Icon: "error",
})
dlg.Text(fmt.Sprintf("error: %v", s.Err))
dlg.Close()
// err := restore(status, verbose)
// if err != nil {
// if show_progress {
// display(notify.Notification{
// Title: "Rollback Error",
// Message: err.Error(),
// Icon: "error",
// })
// } else {
// fmt.Printf("rollback error: %v\n", err)
// }
// }
return
}
if s.Done {
display(Notification{
Title: "Upgrade Complete",
Message: fmt.Sprintf("Now running version %s.", u.Version),
Icon: "success",
})
dlg.Text("Upgrade complete")
time.Sleep(1 * time.Second)
return
}
if s.Warn != "" {
display(Notification{
Message: s.Warn,
Icon: "nvm",
})
}
var empty string
if s.Text != empty && len(strings.TrimSpace(s.Text)) > 0 {
dlg.Text(s.Text)
}
}
}
}()
// Wait for the prior subroutine to initialize before starting the next dependent thread
time.Sleep(300 * time.Millisecond)
// Run the update
go func() {
exe, _ := os.Executable()
winIco := filepath.Join(filepath.Dir(exe), "nvm.ico")
ico := filepath.Join(filepath.Dir(exe), "download.ico")
var err error
u, err = checkForUpdate(UPDATE_URL)
if err != nil {
display(Notification{
Title: "Update Error",
Message: err.Error(),
Icon: "error",
})
status <- Status{Err: fmt.Errorf("error: failed to obtain update data: %v\n", err)}
}
var perr error
dlg, perr = zenity.Progress(
zenity.Title(fmt.Sprintf("Installing NVM for Windows v%s", u.Version)),
zenity.Icon(ico),
zenity.WindowIcon(winIco),
zenity.AutoClose(),
zenity.NoCancel(),
zenity.Pulsate())
if perr != nil {
fmt.Println("Failed to create progress dialog")
}
go func() {
for {
select {
case <-dlg.Done():
if err := dlg.Complete(); err == zenity.ErrCanceled {
status <- Status{Cancel: true}
}
}
}
}()
status <- Status{Text: "Validating version..."}
run(version, status, u)
}()
wg.Wait()
os.Exit(exitCode)
return nil
}
func run(version string, status chan Status, updateMetadata ...*Update) error {
args := os.Args[2:]
colorize := true
if err := EnableVirtualTerminalProcessing(); err != nil {
colorize = false
}
// Retrieve remote metadata
var update *Update
if len(updateMetadata) > 0 {
update = updateMetadata[0]
} else {
var err error
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 {
status <- Status{Warn: warning}
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 {
status <- Status{Warn: warning}
Warn(warning, colorize)
}
fmt.Println("")
}
status <- Status{Text: "downloading..."}
fmt.Printf("upgrading from v%s-->%s\n\ndownloading...\n", version, highlight(update.Version))
} else {
status <- Status{Text: "nvm is up to date", Done: true}
fmt.Println("nvm is up to date")
return nil
}
// Make temp directory
tmp, err := os.MkdirTemp("", "nvm-upgrade-*")
if err != nil {
status <- Status{Err: err}
return fmt.Errorf("error: failed to create temporary directory: %v\n", err)
}
defer os.RemoveAll(tmp)
// Download the new app
source := fmt.Sprintf(update.SourceURL, update.Version)
// source := fmt.Sprintf(update.SourceURL, "1.1.11") // testing
body, err := get(source)
if err != nil {
status <- Status{Err: err}
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...")
status <- Status{Text: "verifying checksum..."}
computedChecksum, err := computeMD5Checksum(filePath)
if err != nil {
status <- Status{Err: err}
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 {
status <- Status{Err: err}
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) {
status <- Status{Err: fmt.Errorf("cannot validate update file (checksum mismatch)")}
return fmt.Errorf("cannot validate update file (checksum mismatch)")
}
fmt.Println("extracting update...")
status <- Status{Text: "extracting update..."}
if err := unzip(filepath.Join(tmp, "assets.zip"), filepath.Join(tmp, "assets")); err != nil {
status <- Status{Err: err}
return err
}
// Get any additional assets
if len(update.Assets) > 0 {
status <- Status{Text: fmt.Sprintf("downloading %d additional assets...", len(update.Assets))}
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 {
status <- Status{Err: err}
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...")
status <- Status{Text: "applying update..."}
currentExe, _ := os.Executable()
currentPath := filepath.Dir(currentExe)
bkp, err := os.MkdirTemp("", "nvm-backup-*")
if err != nil {
status <- Status{Err: fmt.Errorf("error: failed to create backup directory: %v\n", err)}
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 {
status <- Status{Err: fmt.Errorf("error: failed to create backup: %v\n", err)}
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 {
status <- Status{Err: err}
fmt.Println("error running nvm.exe:", err)
}
}
// Debugging
if verbose {
tree(currentPath, "final directory contents:")
}
// Hide the update directory
setHidden(filepath.Join(currentPath, ".update"))
// 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 {
status <- Status{Err: err}
fmt.Println(fmt.Errorf("error: failed to copy update.exe: %v\n", err))
os.Exit(1)
}
}
autoupdate(status)
return nil
}
type Status struct {
Text string
Err error
Done bool
Help bool
Cancel bool
Warn string
}
func (u *Update) Available(sinceVersion string) (string, bool, error) {
currentVersion, err := semver.New(sinceVersion)
if err != nil {
@@ -70,259 +532,14 @@ func Warn(msg string, colorized ...bool) {
}
}
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() {
func autoupdate(status chan Status) {
currentPath, err := os.Executable()
if err != nil {
status <- Status{Err: err}
fmt.Println("error getting updater path:", err)
os.Exit(1)
}
@@ -334,9 +551,12 @@ func autoupdate() {
// Temporary batch file that deletes the directory and the scheduled task
tmp, err := os.MkdirTemp("", "nvm4w-remove-*")
if err != nil {
status <- Status{Err: err}
fmt.Printf("error creating temporary directory: %v", err)
os.Exit(1)
}
// schedule removal of restoration folder for 30 days from now
tempBatchFile := filepath.Join(tmp, "remove_backup.bat")
now := time.Now()
futureDate := now.AddDate(0, 0, 7)
@@ -350,6 +570,7 @@ rmdir /s /q "%s"
// Write the batch file to a temporary location
err = os.WriteFile(tempBatchFile, []byte(batchContent), os.ModePerm)
if err != nil {
status <- Status{Err: err}
fmt.Printf("error creating temporary batch file: %v", err)
os.Exit(1)
}
@@ -422,11 +643,13 @@ echo Update complete >> error.log
del error.log
del "%%~f0"
start "nvm://launch?action=upgrade_notify"
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 {
status <- Status{Err: err}
fmt.Printf("error creating updater script: %v", err)
os.Exit(1)
}
@@ -435,12 +658,15 @@ exit /b 0
cmd := exec.Command(scriptPath, fmt.Sprintf("%d", os.Getpid()), filepath.Join(tempDir, ".update", "nvm.exe"), currentPath)
err = cmd.Start()
if err != nil {
status <- Status{Err: err}
fmt.Printf("error starting updater script: %v", err)
os.Exit(1)
}
// Exit the current process (delay for cleanup)
time.Sleep(300 * time.Millisecond)
status <- Status{Text: "Restarting app...", Done: true}
time.Sleep(2 * time.Second)
os.Exit(0)
}

View File

@@ -23,6 +23,7 @@ import (
fs "github.com/coreybutler/go-fsutil"
)
var nvmversion = ""
var client = &http.Client{}
var nodeBaseAddress = "https://nodejs.org/dist/"
var npmBaseAddress = "https://github.com/npm/cli/archive/"
@@ -130,8 +131,7 @@ func Download(url string, target string, version string) bool {
return false
}
// TODO: Add version to user agent
req.Header.Set("User-Agent", fmt.Sprintf("NVM for Windows %s", version))
req.Header.Set("User-Agent", fmt.Sprintf("NVM for Windows %s", nvmversion))
response, err := client.Do(req)
if err != nil {
@@ -166,7 +166,7 @@ func Download(url string, target string, version string) bool {
} else {
_, err = io.Copy(output, response.Body)
if err != nil {
fmt.Println("Error while downloading", url, "-", err)
fmt.Printf("Error while downloading %s: %v\n", url, err)
}
}